From 1578266f44f6f209d89967894566ab89b0ffd2c7 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 4 Nov 2024 09:57:15 -0800 Subject: [PATCH 01/17] initial componet --- Cargo.lock | 12 ++++++++++++ Cargo.toml | 1 + components/clarity-format/Cargo.toml | 26 ++++++++++++++++++++++++++ components/clarity-format/src/main.rs | 3 +++ 4 files changed, 42 insertions(+) create mode 100644 components/clarity-format/Cargo.toml create mode 100644 components/clarity-format/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 95a24c9ee..5e89cd959 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,6 +831,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "clarity-format" +version = "0.1.0" +dependencies = [ + "clap", + "clarinet-files", + "clarity-repl", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "clarity-lsp" version = "2.11.2" diff --git a/Cargo.toml b/Cargo.toml index 504c9fc1c..e70f92088 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "components/clarity-lsp", "components/clarity-repl", "components/clarity-events", + "components/clarity-format", "components/hiro-system-kit", "components/stacks-codec", "components/stacks-devnet-js", diff --git a/components/clarity-format/Cargo.toml b/components/clarity-format/Cargo.toml new file mode 100644 index 000000000..923295638 --- /dev/null +++ b/components/clarity-format/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "clarity-format" +version = "0.1.0" +edition = "2021" + +[dependencies] +clarinet-files = { path = "../clarinet-files", default-features = false, optional = true } +clarity-repl = { path = "../clarity-repl", default-features = false } +clap = { version = "4.4.8", features = ["derive"], optional = true } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0.79", features = ["preserve_order"] } +serde_derive = "1" + +[features] +default = ["cli"] +cli = ["clarity-repl/cli", "clarinet-files/cli", "clap"] +lib = ["clarity-repl/cli"] + +[lib] +crate-type = ["cdylib", "rlib"] +name = "clarity_format" +path = "src/lib.rs" + +[[bin]] +name = "clarity-format" +path = "src/bin.rs" diff --git a/components/clarity-format/src/main.rs b/components/clarity-format/src/main.rs new file mode 100644 index 000000000..e7a11a969 --- /dev/null +++ b/components/clarity-format/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From c114c34b535fadc647bceccdd530d2ab27c8ef79 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 4 Nov 2024 18:48:33 -0800 Subject: [PATCH 02/17] clarinet fmt boilerplate hooked up --- Cargo.lock | 21 ++--- Cargo.toml | 2 +- components/clarinet-cli/Cargo.toml | 1 + components/clarinet-cli/src/frontend/cli.rs | 62 ++++++++++++++ components/clarinet-format/Cargo.toml | 12 +++ .../src/formatter/formatters.rs | 5 ++ .../clarinet-format/src/formatter/mod.rs | 83 +++++++++++++++++++ components/clarinet-format/src/lib.rs | 1 + components/clarity-format/Cargo.toml | 26 ------ components/clarity-format/src/main.rs | 3 - 10 files changed, 174 insertions(+), 42 deletions(-) create mode 100644 components/clarinet-format/Cargo.toml create mode 100644 components/clarinet-format/src/formatter/formatters.rs create mode 100644 components/clarinet-format/src/formatter/mod.rs create mode 100644 components/clarinet-format/src/lib.rs delete mode 100644 components/clarity-format/Cargo.toml delete mode 100644 components/clarity-format/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 5e89cd959..f31b93aa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -696,6 +696,7 @@ dependencies = [ "clap_complete", "clarinet-deployments", "clarinet-files", + "clarinet-format", "clarity-lsp", "clarity-repl", "crossbeam-channel", @@ -767,6 +768,14 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "clarinet-format" +version = "0.1.0" +dependencies = [ + "clarinet-files", + "clarity-repl", +] + [[package]] name = "clarinet-sdk-wasm" version = "2.11.2" @@ -831,18 +840,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "clarity-format" -version = "0.1.0" -dependencies = [ - "clap", - "clarinet-files", - "clarity-repl", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "clarity-lsp" version = "2.11.2" diff --git a/Cargo.toml b/Cargo.toml index e70f92088..7a8c83ffd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,12 @@ members = [ "components/clarinet-cli", "components/clarinet-deployments", "components/clarinet-files", + "components/clarinet-format", "components/clarinet-utils", "components/clarinet-sdk-wasm", "components/clarity-lsp", "components/clarity-repl", "components/clarity-events", - "components/clarity-format", "components/hiro-system-kit", "components/stacks-codec", "components/stacks-devnet-js", diff --git a/components/clarinet-cli/Cargo.toml b/components/clarinet-cli/Cargo.toml index d88174d09..d01311be6 100644 --- a/components/clarinet-cli/Cargo.toml +++ b/components/clarinet-cli/Cargo.toml @@ -40,6 +40,7 @@ clarity_repl = { package = "clarity-repl", path = "../clarity-repl", features = ] } clarinet-files = { path = "../clarinet-files", features = ["cli"] } clarity-lsp = { path = "../clarity-lsp", features = ["cli"] } +clarinet-format = { path = "../clarinet-format" } clarinet-deployments = { path = "../clarinet-deployments", features = ["cli"] } hiro-system-kit = { path = "../hiro-system-kit" } stacks-network = { path = "../stacks-network" } diff --git a/components/clarinet-cli/src/frontend/cli.rs b/components/clarinet-cli/src/frontend/cli.rs index 2f90d8f48..18962df6f 100644 --- a/components/clarinet-cli/src/frontend/cli.rs +++ b/components/clarinet-cli/src/frontend/cli.rs @@ -27,6 +27,7 @@ use clarinet_files::{ get_manifest_location, FileLocation, NetworkManifest, ProjectManifest, ProjectManifestFile, RequirementConfig, }; +use clarinet_format::formatter::{ClarityFormatter, Settings}; use clarity_repl::analysis::call_checker::ContractAnalysis; use clarity_repl::clarity::vm::analysis::AnalysisDatabase; use clarity_repl::clarity::vm::costs::LimitedCostTracker; @@ -94,11 +95,26 @@ enum Command { /// Get Clarity autocompletion and inline errors from your code editor (VSCode, vim, emacs, etc) #[clap(name = "lsp", bin_name = "lsp")] LSP, + /// Format clarity code files + #[clap(name = "format", aliases = &["fmt"], bin_name = "format")] + Format(Format), /// Step by step debugging and breakpoints from your code editor (VSCode, vim, emacs, etc) #[clap(name = "dap", bin_name = "dap")] DAP, } +#[derive(Parser, PartialEq, Clone, Debug)] +struct Format { + /// Path to clarity files + #[clap(long = "path", short = 'p')] + pub code_path: Option, + /// If specified, format only this file + #[clap(long = "file", short = 'f')] + pub file: Option, + #[clap(long = "dry-run")] + pub dry_run: bool, +} + #[derive(Subcommand, PartialEq, Clone, Debug)] enum Devnet { /// Generate package of all required devnet artifacts @@ -1180,6 +1196,44 @@ pub fn main() { process::exit(1); } }, + Command::Format(cmd) => { + // look for files at the default code path (./contracts/) if cmd.code_path is not specified OR if cmd.file is not specified + // Default to "./contracts/" if no path is specified + let path = cmd.code_path.unwrap_or_else(|| "./contracts/".to_string()); + + // Collect file paths and load source code + let files: Vec = match cmd.file { + Some(file_name) => vec![format!("{}/{}", path, file_name)], + None => match fs::read_dir(&path) { + Ok(entries) => entries + .filter_map(Result::ok) + .filter(|entry| entry.path().is_file()) + .map(|entry| entry.path().to_string_lossy().into_owned()) + .collect(), + Err(message) => { + eprintln!("{}", format_err!(message)); + std::process::exit(1) + } + }, + }; + // Map each file to its source code + let sources: Vec<(String, String)> = files + .into_iter() + .map(|file_path| { + let source = fs::read_to_string(&file_path) + .unwrap_or_else(|_| "// Failed to read file".to_string()); + (file_path, source) + }) + .collect(); + + let settings = Settings::default(); + let mut formatter = ClarityFormatter::new(settings); + + for (file_path, source) in &sources { + println!("here: {}", source); + formatter.format(file_path, source); + } + } Command::Devnet(subcommand) => match subcommand { Devnet::Package(cmd) => { let manifest = load_manifest_or_exit(cmd.manifest_path); @@ -1250,6 +1304,14 @@ fn load_manifest_or_warn(path: Option) -> Option { } } +fn load_clarity_code( + code_path: &Option, + file: &Option, + dry_run: bool, +) -> (Option) { + Some("".to_string()) +} + fn load_deployment_and_artifacts_or_exit( manifest: &ProjectManifest, deployment_plan_path: &Option, diff --git a/components/clarinet-format/Cargo.toml b/components/clarinet-format/Cargo.toml new file mode 100644 index 000000000..15593fe40 --- /dev/null +++ b/components/clarinet-format/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "clarinet-format" +version = "0.1.0" +edition = "2021" + +[dependencies] +clarinet-files = { path = "../clarinet-files", default-features = false, optional = true } +clarity-repl = { path = "../clarity-repl", default-features = false } + +[lib] +name = "clarinet_format" +path = "src/lib.rs" diff --git a/components/clarinet-format/src/formatter/formatters.rs b/components/clarinet-format/src/formatter/formatters.rs new file mode 100644 index 000000000..8744ec090 --- /dev/null +++ b/components/clarinet-format/src/formatter/formatters.rs @@ -0,0 +1,5 @@ +use clarity_repl::clarity::vm::functions::{define::DefineFunctions, NativeFunctions}; +use clarity_repl::clarity::SymbolicExpression; +pub fn format(expressions: &[SymbolicExpression], acc: &str) -> String { + "here".to_string() +} diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs new file mode 100644 index 000000000..e889e7f89 --- /dev/null +++ b/components/clarinet-format/src/formatter/mod.rs @@ -0,0 +1,83 @@ +use clarity_repl::clarity::ast::build_ast_with_rules; +use clarity_repl::clarity::vm::types::QualifiedContractIdentifier; +use clarity_repl::clarity::ClarityVersion; +use clarity_repl::clarity::StacksEpochId; +mod formatters; +use self::formatters::format; +// +pub enum Indentation { + Space(u8), + Tab, +} +pub struct Settings { + pub indentation: Indentation, + pub max_line_length: u8, +} +impl Settings { + pub fn default() -> Settings { + Settings { + indentation: Indentation::Space(2), + max_line_length: 80, + } + } +} +pub struct ClarityFormatter { + settings: Settings, +} +impl ClarityFormatter { + pub fn new(settings: Settings) -> Self { + Self { settings: settings } + } + pub fn format(&mut self, file_path: &str, source: &str) -> String { + let ast = build_ast_with_rules( + &QualifiedContractIdentifier::transient(), + source, + &mut (), + ClarityVersion::Clarity3, + StacksEpochId::Epoch30, + clarity_repl::clarity::ast::ASTRules::Typical, + ) + .unwrap(); + let output = format(&ast.expressions, ""); + println!("output: {}", output); + // @todo mut output and reinject comments based on start and end expr_ids + output + } +} +#[cfg(test)] +mod tests_formatter { + use super::{ClarityFormatter, Settings}; + fn format_with_default(source: &str) -> String { + let mut formatter = ClarityFormatter::new(Settings::default()); + formatter.format(source) + } + #[test] + fn test_simplest_formatter() { + let result = format_with_default(&String::from("( ok true )")); + assert_eq!(result, "(ok true)"); + } + #[test] + fn test_two_expr_formatter() { + let result = format_with_default(&String::from("(ok true)(ok true)")); + assert_eq!(result, "(ok true)\n(ok true)"); + } + #[test] + fn test_function_formatter() { + let result = format_with_default(&String::from("(define-private (my-func) (ok true))")); + assert_eq!(result, "(define-private (my-func)\n (ok true)\n)"); + } + #[test] + fn test_tuple_formatter() { + let result = format_with_default(&String::from("{n1:1,n2:2,n3:3}")); + assert_eq!(result, "{ n1: 1, n2: 2, n3: 3 }"); + } + #[test] + fn test_function_and_tuple_formatter() { + let src = "(define-private (my-func) (ok { n1: 1, n2: 2, n3: 3 }))"; + let result = format_with_default(&String::from(src)); + assert_eq!( + result, + "(define-private (my-func)\n (ok { n1: 1, n2: 2, n3: 3 })\n)" + ); + } +} diff --git a/components/clarinet-format/src/lib.rs b/components/clarinet-format/src/lib.rs new file mode 100644 index 000000000..96dc2d95d --- /dev/null +++ b/components/clarinet-format/src/lib.rs @@ -0,0 +1 @@ +pub mod formatter; diff --git a/components/clarity-format/Cargo.toml b/components/clarity-format/Cargo.toml deleted file mode 100644 index 923295638..000000000 --- a/components/clarity-format/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "clarity-format" -version = "0.1.0" -edition = "2021" - -[dependencies] -clarinet-files = { path = "../clarinet-files", default-features = false, optional = true } -clarity-repl = { path = "../clarity-repl", default-features = false } -clap = { version = "4.4.8", features = ["derive"], optional = true } -serde = { version = "1", features = ["derive"] } -serde_json = { version = "1.0.79", features = ["preserve_order"] } -serde_derive = "1" - -[features] -default = ["cli"] -cli = ["clarity-repl/cli", "clarinet-files/cli", "clap"] -lib = ["clarity-repl/cli"] - -[lib] -crate-type = ["cdylib", "rlib"] -name = "clarity_format" -path = "src/lib.rs" - -[[bin]] -name = "clarity-format" -path = "src/bin.rs" diff --git a/components/clarity-format/src/main.rs b/components/clarity-format/src/main.rs deleted file mode 100644 index e7a11a969..000000000 --- a/components/clarity-format/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} From 02a7db688e666408087ef5bb4be4fa26ff53b973 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Sun, 10 Nov 2024 16:17:09 -0800 Subject: [PATCH 03/17] refactor functions a bit --- components/clarinet-cli/src/frontend/cli.rs | 79 +++++++++++-------- .../src/formatter/formatters.rs | 5 -- .../clarinet-format/src/formatter/mod.rs | 23 ++++-- 3 files changed, 63 insertions(+), 44 deletions(-) delete mode 100644 components/clarinet-format/src/formatter/formatters.rs diff --git a/components/clarinet-cli/src/frontend/cli.rs b/components/clarinet-cli/src/frontend/cli.rs index 18962df6f..49a79ac7a 100644 --- a/components/clarinet-cli/src/frontend/cli.rs +++ b/components/clarinet-cli/src/frontend/cli.rs @@ -40,7 +40,7 @@ use clarity_repl::{analysis, repl, Terminal}; use stacks_network::{self, DevnetOrchestrator}; use std::collections::HashMap; use std::fs::{self, File}; -use std::io::prelude::*; +use std::io::{self, prelude::*}; use std::{env, process}; use toml; @@ -1197,41 +1197,15 @@ pub fn main() { } }, Command::Format(cmd) => { - // look for files at the default code path (./contracts/) if cmd.code_path is not specified OR if cmd.file is not specified - // Default to "./contracts/" if no path is specified - let path = cmd.code_path.unwrap_or_else(|| "./contracts/".to_string()); - - // Collect file paths and load source code - let files: Vec = match cmd.file { - Some(file_name) => vec![format!("{}/{}", path, file_name)], - None => match fs::read_dir(&path) { - Ok(entries) => entries - .filter_map(Result::ok) - .filter(|entry| entry.path().is_file()) - .map(|entry| entry.path().to_string_lossy().into_owned()) - .collect(), - Err(message) => { - eprintln!("{}", format_err!(message)); - std::process::exit(1) - } - }, - }; - // Map each file to its source code - let sources: Vec<(String, String)> = files - .into_iter() - .map(|file_path| { - let source = fs::read_to_string(&file_path) - .unwrap_or_else(|_| "// Failed to read file".to_string()); - (file_path, source) - }) - .collect(); - + let sources = get_source_with_path(cmd.code_path, cmd.file); let settings = Settings::default(); let mut formatter = ClarityFormatter::new(settings); for (file_path, source) in &sources { - println!("here: {}", source); - formatter.format(file_path, source); + let output = formatter.format(source); + if !cmd.dry_run { + let _ = overwrite_formatted(file_path, output); + } } } Command::Devnet(subcommand) => match subcommand { @@ -1247,6 +1221,47 @@ pub fn main() { }; } +fn overwrite_formatted(file_path: &String, output: String) -> io::Result<()> { + // Open the file in write mode, overwriting existing content + let mut file = fs::File::create(file_path)?; + + file.write_all(output.as_bytes())?; + + // flush the contents to ensure it's written immediately + file.flush() +} + +fn get_source_with_path(code_path: Option, file: Option) -> Vec<(String, String)> { + // look for files at the default code path (./contracts/) if + // cmd.code_path is not specified OR if cmd.file is not specified + let path = code_path.unwrap_or_else(|| "./contracts/".to_string()); + + // Collect file paths and load source code + let files: Vec = match file { + Some(file_name) => vec![format!("{}/{}", path, file_name)], + None => match fs::read_dir(&path) { + Ok(entries) => entries + .filter_map(Result::ok) + .filter(|entry| entry.path().is_file()) + .map(|entry| entry.path().to_string_lossy().into_owned()) + .collect(), + Err(message) => { + eprintln!("{}", format_err!(message)); + std::process::exit(1) + } + }, + }; + // Map each file to its source code + files + .into_iter() + .map(|file_path| { + let source = fs::read_to_string(&file_path) + .unwrap_or_else(|_| "// Failed to read file".to_string()); + (file_path, source) + }) + .collect() +} + fn get_manifest_location_or_exit(path: Option) -> FileLocation { match get_manifest_location(path) { Some(manifest_location) => manifest_location, diff --git a/components/clarinet-format/src/formatter/formatters.rs b/components/clarinet-format/src/formatter/formatters.rs deleted file mode 100644 index 8744ec090..000000000 --- a/components/clarinet-format/src/formatter/formatters.rs +++ /dev/null @@ -1,5 +0,0 @@ -use clarity_repl::clarity::vm::functions::{define::DefineFunctions, NativeFunctions}; -use clarity_repl::clarity::SymbolicExpression; -pub fn format(expressions: &[SymbolicExpression], acc: &str) -> String { - "here".to_string() -} diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index e889e7f89..f79d3e74f 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -1,14 +1,15 @@ use clarity_repl::clarity::ast::build_ast_with_rules; +use clarity_repl::clarity::vm::functions::{define::DefineFunctions, NativeFunctions}; use clarity_repl::clarity::vm::types::QualifiedContractIdentifier; use clarity_repl::clarity::ClarityVersion; use clarity_repl::clarity::StacksEpochId; -mod formatters; -use self::formatters::format; -// +use clarity_repl::clarity::SymbolicExpression; + pub enum Indentation { Space(u8), Tab, } + pub struct Settings { pub indentation: Indentation, pub max_line_length: u8, @@ -21,14 +22,15 @@ impl Settings { } } } +// pub struct ClarityFormatter { settings: Settings, } impl ClarityFormatter { pub fn new(settings: Settings) -> Self { - Self { settings: settings } + Self { settings } } - pub fn format(&mut self, file_path: &str, source: &str) -> String { + pub fn format(&mut self, source: &str) -> String { let ast = build_ast_with_rules( &QualifiedContractIdentifier::transient(), source, @@ -38,12 +40,19 @@ impl ClarityFormatter { clarity_repl::clarity::ast::ASTRules::Typical, ) .unwrap(); - let output = format(&ast.expressions, ""); + let output = format_source_exprs(&self.settings, &ast.expressions, ""); println!("output: {}", output); - // @todo mut output and reinject comments based on start and end expr_ids output } } + +pub fn format_source_exprs( + settings: &Settings, + expressions: &[SymbolicExpression], + acc: &str, +) -> String { + "here".to_string() +} #[cfg(test)] mod tests_formatter { use super::{ClarityFormatter, Settings}; From 0cef41e9d0a0b37fdb003cf269ea2dcb55085e19 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 11 Nov 2024 20:21:33 -0800 Subject: [PATCH 04/17] add basic formatter blocks --- components/clarinet-format/Cargo.toml | 1 + .../clarinet-format/src/formatter/mod.rs | 130 +++++++++++++++++- 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/components/clarinet-format/Cargo.toml b/components/clarinet-format/Cargo.toml index 15593fe40..65c0f0ea2 100644 --- a/components/clarinet-format/Cargo.toml +++ b/components/clarinet-format/Cargo.toml @@ -10,3 +10,4 @@ clarity-repl = { path = "../clarity-repl", default-features = false } [lib] name = "clarinet_format" path = "src/lib.rs" +crate-type = ["lib"] diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index f79d3e74f..b3249a136 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -46,12 +46,124 @@ impl ClarityFormatter { } } +// * functions + +// Top level define- should have a line break above and after (except on first line) +// options always on new lines +// Functions Always on multiple lines, even if short +// *begin* never on one line +// *let* never on one line + +// * match * +// One line if less than max length (unless the original source has line breaks?) +// Multiple lines if more than max length (should the first arg be on the first line if it fits?) pub fn format_source_exprs( settings: &Settings, expressions: &[SymbolicExpression], acc: &str, ) -> String { - "here".to_string() + if let Some((expr, remaining)) = expressions.split_first() { + if let Some(list) = expr.match_list() { + let atom = list.split_first().and_then(|(f, _)| f.match_atom()); + use NativeFunctions::*; + let formatted = if let Some( + DefineFunctions::PublicFunction + | DefineFunctions::ReadOnlyFunction + | DefineFunctions::PrivateFunction, + ) = atom.and_then(|a| DefineFunctions::lookup_by_name(a)) + { + format_function(settings, list) + } else if let Some(Begin) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) { + format_begin(settings, list) + } else if let Some(Let) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) { + format_let(settings, list) + } else if let Some(TupleCons) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) { + format_tuple(settings, list) + } else { + format!("({})", format_source_exprs(settings, list, acc)) + }; + return format!( + "{formatted} {}", + format_source_exprs(settings, remaining, acc) + ) + .trim() + .to_owned(); + } + return format!("{} {}", expr, format_source_exprs(settings, remaining, acc)) + .trim() + .to_owned(); + }; + acc.to_owned() +} + +fn format_begin(settings: &Settings, exprs: &[SymbolicExpression]) -> String { + let mut begin_acc = "(begin\n".to_string(); + for arg in exprs.get(1..).unwrap_or_default() { + if let Some(list) = arg.match_list() { + begin_acc.push_str(&format!( + "\n ({})", + format_source_exprs(settings, list, "") + )) + } + } + begin_acc.push_str("\n)\n"); + begin_acc.to_owned() +} + +fn format_let(settings: &Settings, exprs: &[SymbolicExpression]) -> String { + let mut begin_acc = "(let (\n".to_string(); + for arg in exprs.get(1..).unwrap_or_default() { + if let Some(list) = arg.match_list() { + begin_acc.push_str(&format!( + "\n ({})", + format_source_exprs(settings, list, "") + )) + } + } + begin_acc.push_str("\n) \n"); + begin_acc.to_owned() +} + +fn format_tuple(settings: &Settings, exprs: &[SymbolicExpression]) -> String { + let mut tuple_acc = "{ ".to_string(); + for (i, expr) in exprs[1..].iter().enumerate() { + let (key, value) = expr + .match_list() + .and_then(|list| list.split_first()) + .unwrap(); + if i < exprs.len() - 2 { + tuple_acc.push_str(&format!( + "{key}: {}, ", + format_source_exprs(settings, value, "") + )); + } else { + tuple_acc.push_str(&format!( + "{key}: {}", + format_source_exprs(settings, value, "") + )); + } + } + tuple_acc.push_str(" }"); + tuple_acc.to_string() +} + +fn format_function(settings: &Settings, exprs: &[SymbolicExpression]) -> String { + let func_type = exprs.first().unwrap(); + let name_and_args = exprs.get(1).and_then(|f| f.match_list()).unwrap(); + let mut func_acc = format!( + "({func_type} ({})", + format_source_exprs(settings, name_and_args, "") + ); + for arg in exprs.get(2..).unwrap_or_default() { + if let Some(list) = arg.match_list() { + func_acc.push_str(&format!( + "\n ({})", + format_source_exprs(settings, list, "") + )) + } + } + func_acc.push_str("\n)"); + func_acc.to_owned() } #[cfg(test)] mod tests_formatter { @@ -89,4 +201,20 @@ mod tests_formatter { "(define-private (my-func)\n (ok { n1: 1, n2: 2, n3: 3 })\n)" ); } + + #[test] + fn test_function_args_multiline() { + let src = "(define-public (my-func (amount uint) (sender principal)) (ok true))"; + let result = format_with_default(&String::from(src)); + assert_eq!( + result, + "(define-public (my-func\n (amount uint)\n (sender principal)\n )\n (ok true)\n)" + ); + } + #[test] + fn test_begin_never_one_line() { + let src = "(begin (ok true))"; + let result = format_with_default(&String::from(src)); + assert_eq!(result, "(begin\n (ok true)\n)"); + } } From 07bd0ecd409a3032ebc293be89bb24c41aac2644 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Thu, 14 Nov 2024 09:54:21 -0800 Subject: [PATCH 05/17] fix build removing clarity-repl cargo flags --- components/clarinet-format/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/clarinet-format/Cargo.toml b/components/clarinet-format/Cargo.toml index 65c0f0ea2..36fe29916 100644 --- a/components/clarinet-format/Cargo.toml +++ b/components/clarinet-format/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] clarinet-files = { path = "../clarinet-files", default-features = false, optional = true } -clarity-repl = { path = "../clarity-repl", default-features = false } +clarity-repl = { path = "../clarity-repl" } [lib] name = "clarinet_format" From 6d33c3661037a0722d958eb049aa5278015c9553 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Fri, 15 Nov 2024 10:15:56 -0800 Subject: [PATCH 06/17] remove dep on clarity-repl --- Cargo.lock | 3 +- components/clarinet-format/Cargo.toml | 19 +++++- .../clarinet-format/src/formatter/mod.rs | 63 ++++++++++++++----- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f31b93aa2..a9c65c937 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,8 +772,7 @@ dependencies = [ name = "clarinet-format" version = "0.1.0" dependencies = [ - "clarinet-files", - "clarity-repl", + "clarity", ] [[package]] diff --git a/components/clarinet-format/Cargo.toml b/components/clarinet-format/Cargo.toml index 36fe29916..cb653fd5d 100644 --- a/components/clarinet-format/Cargo.toml +++ b/components/clarinet-format/Cargo.toml @@ -4,8 +4,23 @@ version = "0.1.0" edition = "2021" [dependencies] -clarinet-files = { path = "../clarinet-files", default-features = false, optional = true } -clarity-repl = { path = "../clarity-repl" } +# clarity-repl = { path = "../clarity-repl" } +clarity = { workspace = true} + +[features] +default = ["cli"] +cli = [ + "clarity/canonical", + "clarity/developer-mode", + "clarity/devtools", + "clarity/log", +] +wasm = [ + "clarity/wasm", + "clarity/developer-mode", + "clarity/devtools", +] + [lib] name = "clarinet_format" diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index b3249a136..71f383dd9 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -1,21 +1,29 @@ -use clarity_repl::clarity::ast::build_ast_with_rules; -use clarity_repl::clarity::vm::functions::{define::DefineFunctions, NativeFunctions}; -use clarity_repl::clarity::vm::types::QualifiedContractIdentifier; -use clarity_repl::clarity::ClarityVersion; -use clarity_repl::clarity::StacksEpochId; -use clarity_repl::clarity::SymbolicExpression; +use clarity::types::StacksEpochId; +use clarity::vm::ast::{build_ast_with_rules, ASTRules}; +use clarity::vm::functions::{define::DefineFunctions, NativeFunctions}; +use clarity::vm::types::QualifiedContractIdentifier; +use clarity::vm::{ClarityVersion, SymbolicExpression}; pub enum Indentation { - Space(u8), + Space(usize), Tab, } pub struct Settings { pub indentation: Indentation, - pub max_line_length: u8, + pub max_line_length: usize, } + impl Settings { - pub fn default() -> Settings { + pub fn new(indentation: Indentation, max_line_length: usize) -> Self { + Settings { + indentation, + max_line_length, + } + } +} +impl Default for Settings { + fn default() -> Settings { Settings { indentation: Indentation::Space(2), max_line_length: 80, @@ -37,7 +45,7 @@ impl ClarityFormatter { &mut (), ClarityVersion::Clarity3, StacksEpochId::Epoch30, - clarity_repl::clarity::ast::ASTRules::Typical, + ASTRules::Typical, ) .unwrap(); let output = format_source_exprs(&self.settings, &ast.expressions, ""); @@ -96,12 +104,21 @@ pub fn format_source_exprs( acc.to_owned() } +fn indentation_to_string(indentation: &Indentation) -> String { + match indentation { + Indentation::Space(i) => " ".repeat(*i), + Indentation::Tab => "\t".to_string(), + } +} + fn format_begin(settings: &Settings, exprs: &[SymbolicExpression]) -> String { - let mut begin_acc = "(begin\n".to_string(); + let mut begin_acc = "(begin".to_string(); + let indentation = indentation_to_string(&settings.indentation); for arg in exprs.get(1..).unwrap_or_default() { if let Some(list) = arg.match_list() { begin_acc.push_str(&format!( - "\n ({})", + "\n{}({})", + indentation, format_source_exprs(settings, list, "") )) } @@ -111,11 +128,13 @@ fn format_begin(settings: &Settings, exprs: &[SymbolicExpression]) -> String { } fn format_let(settings: &Settings, exprs: &[SymbolicExpression]) -> String { - let mut begin_acc = "(let (\n".to_string(); + let mut begin_acc = "(let (".to_string(); + let indentation = indentation_to_string(&settings.indentation); for arg in exprs.get(1..).unwrap_or_default() { if let Some(list) = arg.match_list() { begin_acc.push_str(&format!( - "\n ({})", + "\n{}({})", + indentation, format_source_exprs(settings, list, "") )) } @@ -149,6 +168,7 @@ fn format_tuple(settings: &Settings, exprs: &[SymbolicExpression]) -> String { fn format_function(settings: &Settings, exprs: &[SymbolicExpression]) -> String { let func_type = exprs.first().unwrap(); + let indentation = indentation_to_string(&settings.indentation); let name_and_args = exprs.get(1).and_then(|f| f.match_list()).unwrap(); let mut func_acc = format!( "({func_type} ({})", @@ -157,7 +177,8 @@ fn format_function(settings: &Settings, exprs: &[SymbolicExpression]) -> String for arg in exprs.get(2..).unwrap_or_default() { if let Some(list) = arg.match_list() { func_acc.push_str(&format!( - "\n ({})", + "\n{}({})", + indentation, format_source_exprs(settings, list, "") )) } @@ -168,10 +189,15 @@ fn format_function(settings: &Settings, exprs: &[SymbolicExpression]) -> String #[cfg(test)] mod tests_formatter { use super::{ClarityFormatter, Settings}; + use crate::formatter::Indentation; fn format_with_default(source: &str) -> String { let mut formatter = ClarityFormatter::new(Settings::default()); formatter.format(source) } + fn format_with(source: &str, settings: Settings) -> String { + let mut formatter = ClarityFormatter::new(settings); + formatter.format(source) + } #[test] fn test_simplest_formatter() { let result = format_with_default(&String::from("( ok true )")); @@ -217,4 +243,11 @@ mod tests_formatter { let result = format_with_default(&String::from(src)); assert_eq!(result, "(begin\n (ok true)\n)"); } + + #[test] + fn test_custom_tab_setting() { + let src = "(begin (ok true))"; + let result = format_with(&String::from(src), Settings::new(Indentation::Space(4), 80)); + assert_eq!(result, "(begin\n (ok true)\n)"); + } } From cb252fed1471e86b7be17011b5092223f323a482 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 18 Nov 2024 14:20:44 -0800 Subject: [PATCH 07/17] fix file path --- components/clarinet-cli/src/frontend/cli.rs | 17 ++++------------- components/clarinet-format/src/formatter/mod.rs | 4 +--- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/components/clarinet-cli/src/frontend/cli.rs b/components/clarinet-cli/src/frontend/cli.rs index 49a79ac7a..d6997aee5 100644 --- a/components/clarinet-cli/src/frontend/cli.rs +++ b/components/clarinet-cli/src/frontend/cli.rs @@ -1205,6 +1205,8 @@ pub fn main() { let output = formatter.format(source); if !cmd.dry_run { let _ = overwrite_formatted(file_path, output); + } else { + println!("{}", output); } } } @@ -1222,13 +1224,10 @@ pub fn main() { } fn overwrite_formatted(file_path: &String, output: String) -> io::Result<()> { - // Open the file in write mode, overwriting existing content let mut file = fs::File::create(file_path)?; file.write_all(output.as_bytes())?; - - // flush the contents to ensure it's written immediately - file.flush() + Ok(()) } fn get_source_with_path(code_path: Option, file: Option) -> Vec<(String, String)> { @@ -1238,7 +1237,7 @@ fn get_source_with_path(code_path: Option, file: Option) -> Vec< // Collect file paths and load source code let files: Vec = match file { - Some(file_name) => vec![format!("{}/{}", path, file_name)], + Some(file_name) => vec![format!("{}", file_name)], None => match fs::read_dir(&path) { Ok(entries) => entries .filter_map(Result::ok) @@ -1319,14 +1318,6 @@ fn load_manifest_or_warn(path: Option) -> Option { } } -fn load_clarity_code( - code_path: &Option, - file: &Option, - dry_run: bool, -) -> (Option) { - Some("".to_string()) -} - fn load_deployment_and_artifacts_or_exit( manifest: &ProjectManifest, deployment_plan_path: &Option, diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index 71f383dd9..84e01e7c2 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -48,9 +48,7 @@ impl ClarityFormatter { ASTRules::Typical, ) .unwrap(); - let output = format_source_exprs(&self.settings, &ast.expressions, ""); - println!("output: {}", output); - output + format_source_exprs(&self.settings, &ast.expressions, "") } } From 29c4618e32a4a2bfad53ca1116107eee1f70841a Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Wed, 20 Nov 2024 13:10:47 -0800 Subject: [PATCH 08/17] basic tests working --- .../clarinet-format/src/formatter/mod.rs | 66 +- .../clarinet-format/tests/golden/BNS-V2.clar | 1879 +++++++++++++++++ .../flash-loan-user-margin-usda-wbtc.clar | 130 ++ .../tests/golden/sbtc-deposit.clar | 108 + 4 files changed, 2172 insertions(+), 11 deletions(-) create mode 100644 components/clarinet-format/tests/golden/BNS-V2.clar create mode 100644 components/clarinet-format/tests/golden/flash-loan-user-margin-usda-wbtc.clar create mode 100644 components/clarinet-format/tests/golden/sbtc-deposit.clar diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index 84e01e7c2..acaf33c73 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -86,10 +86,10 @@ pub fn format_source_exprs( } else if let Some(TupleCons) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) { format_tuple(settings, list) } else { - format!("({})", format_source_exprs(settings, list, acc)) + format!("({})\n", format_source_exprs(settings, list, acc)) }; return format!( - "{formatted} {}", + "{formatted}{}", format_source_exprs(settings, remaining, acc) ) .trim() @@ -168,10 +168,25 @@ fn format_function(settings: &Settings, exprs: &[SymbolicExpression]) -> String let func_type = exprs.first().unwrap(); let indentation = indentation_to_string(&settings.indentation); let name_and_args = exprs.get(1).and_then(|f| f.match_list()).unwrap(); - let mut func_acc = format!( - "({func_type} ({})", - format_source_exprs(settings, name_and_args, "") - ); + + let mut func_acc = format!("({func_type} ("); + + if let Some((name, args)) = name_and_args.split_first() { + func_acc.push_str(&format!("{}", name)); + if args.is_empty() { + func_acc.push(')'); + } else { + for arg in args { + func_acc.push_str(&format!( + "\n{}{}{}", + indentation, + indentation, + format_source_exprs(settings, &[arg.clone()], "") + )); + } + func_acc.push_str(&format!("\n{})", indentation)); + } + } for arg in exprs.get(2..).unwrap_or_default() { if let Some(list) = arg.match_list() { func_acc.push_str(&format!( @@ -188,6 +203,8 @@ fn format_function(settings: &Settings, exprs: &[SymbolicExpression]) -> String mod tests_formatter { use super::{ClarityFormatter, Settings}; use crate::formatter::Indentation; + use std::fs; + use std::path::Path; fn format_with_default(source: &str) -> String { let mut formatter = ClarityFormatter::new(Settings::default()); formatter.format(source) @@ -207,11 +224,6 @@ mod tests_formatter { assert_eq!(result, "(ok true)\n(ok true)"); } #[test] - fn test_function_formatter() { - let result = format_with_default(&String::from("(define-private (my-func) (ok true))")); - assert_eq!(result, "(define-private (my-func)\n (ok true)\n)"); - } - #[test] fn test_tuple_formatter() { let result = format_with_default(&String::from("{n1:1,n2:2,n3:3}")); assert_eq!(result, "{ n1: 1, n2: 2, n3: 3 }"); @@ -226,6 +238,12 @@ mod tests_formatter { ); } + #[test] + fn test_function_formatter() { + let result = format_with_default(&String::from("(define-private (my-func) (ok true))")); + assert_eq!(result, "(define-private (my-func)\n (ok true)\n)"); + } + #[test] fn test_function_args_multiline() { let src = "(define-public (my-func (amount uint) (sender principal)) (ok true))"; @@ -248,4 +266,30 @@ mod tests_formatter { let result = format_with(&String::from(src), Settings::new(Indentation::Space(4), 80)); assert_eq!(result, "(begin\n (ok true)\n)"); } + + // #[test] + // fn test_irl_contracts() { + // let golden_dir = "./tests/golden"; + // let intended_dir = "./tests/golden-intended"; + + // // Iterate over files in the golden directory + // for entry in fs::read_dir(golden_dir).expect("Failed to read golden directory") { + // let entry = entry.expect("Failed to read directory entry"); + // let path = entry.path(); + + // if path.is_file() { + // let src = fs::read_to_string(&path).expect("Failed to read source file"); + + // let file_name = path.file_name().expect("Failed to get file name"); + // let intended_path = Path::new(intended_dir).join(file_name); + + // let intended = + // fs::read_to_string(&intended_path).expect("Failed to read intended file"); + + // // Apply formatting and compare + // let result = format_with_default(&src); + // assert_eq!(result, intended, "Mismatch for file: {:?}", file_name); + // } + // } + // } } diff --git a/components/clarinet-format/tests/golden/BNS-V2.clar b/components/clarinet-format/tests/golden/BNS-V2.clar new file mode 100644 index 000000000..d9374a718 --- /dev/null +++ b/components/clarinet-format/tests/golden/BNS-V2.clar @@ -0,0 +1,1879 @@ +;; title: BNS-V2 +;; version: V-2 +;; summary: Updated BNS contract, handles the creation of new namespaces and new names on each namespace + +;; traits +;; (new) Import SIP-09 NFT trait +(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) +;; (new) Import a custom commission trait for handling commissions for NFT marketplaces functions +(use-trait commission-trait .commission-trait.commission) + +;; token definition +;; (new) Define the non-fungible token (NFT) called BNS-V2 with unique identifiers as unsigned integers +(define-non-fungible-token BNS-V2 uint) +;; Time-to-live (TTL) constants for namespace preorders and name preorders, and the duration for name grace period. +;; The TTL for namespace and names preorders. (1 day) +(define-constant PREORDER-CLAIMABILITY-TTL u144) +;; The duration after revealing a namespace within which it must be launched. (1 year) +(define-constant NAMESPACE-LAUNCHABILITY-TTL u52595) +;; The grace period duration for name renewals post-expiration. (34 days) +(define-constant NAME-GRACE-PERIOD-DURATION u5000) +;; (new) The length of the hash should match this +(define-constant HASH160LEN u20) +;; Defines the price tiers for namespaces based on their lengths. +(define-constant NAMESPACE-PRICE-TIERS (list + u640000000000 + u64000000000 u64000000000 + u6400000000 u6400000000 u6400000000 u6400000000 + u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000) +) + +;; Only authorized caller to flip the switch and update URI +(define-constant DEPLOYER tx-sender) + +;; (new) Var to store the token URI, allowing for metadata association with the NFT +(define-data-var token-uri (string-ascii 256) "ipfs://QmUQY1aZ799SPRaNBFqeCvvmZ4fTQfZvWHauRvHAukyQDB") + +(define-public (update-token-uri (new-token-uri (string-ascii 256))) + (ok + (begin + (asserts! (is-eq contract-caller DEPLOYER) ERR-NOT-AUTHORIZED) + (var-set token-uri new-token-uri) + ) + ) +) + +(define-data-var contract-uri (string-ascii 256) "ipfs://QmWKTZEMQNWngp23i7bgPzkineYC9LDvcxYkwNyVQVoH8y") + +(define-public (update-contract-uri (new-contract-uri (string-ascii 256))) + (ok + (begin + (asserts! (is-eq contract-caller DEPLOYER) ERR-NOT-AUTHORIZED) + (var-set token-uri new-contract-uri) + ) + ) +) + +;; errors +(define-constant ERR-UNWRAP (err u101)) +(define-constant ERR-NOT-AUTHORIZED (err u102)) +(define-constant ERR-NOT-LISTED (err u103)) +(define-constant ERR-WRONG-COMMISSION (err u104)) +(define-constant ERR-LISTED (err u105)) +(define-constant ERR-NO-NAME (err u106)) +(define-constant ERR-HASH-MALFORMED (err u107)) +(define-constant ERR-STX-BURNT-INSUFFICIENT (err u108)) +(define-constant ERR-PREORDER-NOT-FOUND (err u109)) +(define-constant ERR-CHARSET-INVALID (err u110)) +(define-constant ERR-NAMESPACE-ALREADY-EXISTS (err u111)) +(define-constant ERR-PREORDER-CLAIMABILITY-EXPIRED (err u112)) +(define-constant ERR-NAMESPACE-NOT-FOUND (err u113)) +(define-constant ERR-OPERATION-UNAUTHORIZED (err u114)) +(define-constant ERR-NAMESPACE-ALREADY-LAUNCHED (err u115)) +(define-constant ERR-NAMESPACE-PREORDER-LAUNCHABILITY-EXPIRED (err u116)) +(define-constant ERR-NAMESPACE-NOT-LAUNCHED (err u117)) +(define-constant ERR-NAME-NOT-AVAILABLE (err u118)) +(define-constant ERR-NAMESPACE-BLANK (err u119)) +(define-constant ERR-NAME-BLANK (err u120)) +(define-constant ERR-NAME-PREORDERED-BEFORE-NAMESPACE-LAUNCH (err u121)) +(define-constant ERR-NAMESPACE-HAS-MANAGER (err u122)) +(define-constant ERR-OVERFLOW (err u123)) +(define-constant ERR-NO-NAMESPACE-MANAGER (err u124)) +(define-constant ERR-FAST-MINTED-BEFORE (err u125)) +(define-constant ERR-PREORDERED-BEFORE (err u126)) +(define-constant ERR-NAME-NOT-CLAIMABLE-YET (err u127)) +(define-constant ERR-IMPORTED-BEFORE (err u128)) +(define-constant ERR-LIFETIME-EQUAL-0 (err u129)) +(define-constant ERR-MIGRATION-IN-PROGRESS (err u130)) +(define-constant ERR-NO-PRIMARY-NAME (err u131)) + +;; variables +;; (new) Variable to see if migration is complete +(define-data-var migration-complete bool false) + +;; (new) Counter to keep track of the last minted NFT ID, ensuring unique identifiers +(define-data-var bns-index uint u0) + +;; maps +;; (new) Map to track market listings, associating NFT IDs with price and commission details +(define-map market uint {price: uint, commission: principal}) + +;; (new) Define a map to link NFT IDs to their respective names and namespaces. +(define-map index-to-name uint + { + name: (buff 48), namespace: (buff 20) + } +) +;; (new) Define a map to link names and namespaces to their respective NFT IDs. +(define-map name-to-index + { + name: (buff 48), namespace: (buff 20) + } + uint +) + +;; (updated) Contains detailed properties of names, including registration and importation times +(define-map name-properties + { name: (buff 48), namespace: (buff 20) } + { + registered-at: (optional uint), + imported-at: (optional uint), + ;; The fqn used to make the earliest preorder at any given point + hashed-salted-fqn-preorder: (optional (buff 20)), + ;; Added this field in name-properties to know exactly who has the earliest preorder at any given point + preordered-by: (optional principal), + renewal-height: uint, + stx-burn: uint, + owner: principal, + } +) + +;; (update) Stores properties of namespaces, including their import principals, reveal and launch times, and pricing functions. +(define-map namespaces (buff 20) + { + namespace-manager: (optional principal), + manager-transferable: bool, + manager-frozen: bool, + namespace-import: principal, + revealed-at: uint, + launched-at: (optional uint), + lifetime: uint, + can-update-price-function: bool, + price-function: + { + buckets: (list 16 uint), + base: uint, + coeff: uint, + nonalpha-discount: uint, + no-vowel-discount: uint + } + } +) + +;; Records namespace preorder transactions with their creation times, and STX burned. +(define-map namespace-preorders + { hashed-salted-namespace: (buff 20), buyer: principal } + { created-at: uint, stx-burned: uint, claimed: bool} +) + +;; Tracks preorders, to avoid attacks +(define-map namespace-single-preorder (buff 20) bool) + +;; Tracks preorders, to avoid attacks +(define-map name-single-preorder (buff 20) bool) + +;; Tracks preorders for names, including their creation times, and STX burned. +(define-map name-preorders + { hashed-salted-fqn: (buff 20), buyer: principal } + { created-at: uint, stx-burned: uint, claimed: bool} +) + +;; It maps a user's principal to the ID of their primary name. +(define-map primary-name principal uint) + +;; read-only +;; @desc (new) SIP-09 compliant function to get the last minted token's ID +(define-read-only (get-last-token-id) + ;; Returns the current value of bns-index variable, which tracks the last token ID + (ok (var-get bns-index)) +) + +(define-read-only (get-renewal-height (id uint)) + (let + ( + (name-namespace (unwrap! (get-bns-from-id id) ERR-NO-NAME)) + (namespace-props (unwrap! (map-get? namespaces (get namespace name-namespace)) ERR-NAMESPACE-NOT-FOUND)) + (name-props (unwrap! (map-get? name-properties name-namespace) ERR-NO-NAME)) + (renewal-height (get renewal-height name-props)) + (namespace-lifetime (get lifetime namespace-props)) + ) + ;; Check if the namespace requires renewals + (asserts! (not (is-eq namespace-lifetime u0)) ERR-LIFETIME-EQUAL-0) + ;; If the check passes then check the renewal-height of the name + (ok + (if (is-eq renewal-height u0) + ;; If it is true then it means it was imported so return the namespace launch blockheight + lifetime + (+ (unwrap! (get launched-at namespace-props) ERR-NAMESPACE-NOT-LAUNCHED) namespace-lifetime) + renewal-height + ) + ) + ) +) + +(define-read-only (can-resolve-name (namespace (buff 20)) (name (buff 48))) + (let + ( + (name-id (unwrap! (get-id-from-bns name namespace) ERR-NO-NAME)) + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (name-props (unwrap! (map-get? name-properties {name: name, namespace: namespace}) ERR-NO-NAME)) + (renewal-height (get renewal-height name-props)) + (namespace-lifetime (get lifetime namespace-props)) + ) + ;; Check if the name can resolve + (ok + (if (is-eq u0 namespace-lifetime) + ;; If true it means that the name is in a managed namespace or the namespace does not require renewals + {renewal: u0, owner: (get owner name-props)} + ;; If false then calculate renewal-height + {renewal: (try! (get-renewal-height name-id)), owner: (get owner name-props)} + ) + ) + ) +) + +;; @desc (new) SIP-09 compliant function to get token URI +(define-read-only (get-token-uri (id uint)) + ;; Returns a predefined set URI for the token metadata + (ok (some (var-get token-uri))) +) + +(define-read-only (get-contract-uri) + ;; Returns a predefined set URI for the contract metadata + (ok (some (var-get contract-uri))) +) + +;; @desc (new) SIP-09 compliant function to get the owner of a specific token by its ID +(define-read-only (get-owner (id uint)) + ;; Check and return the owner of the specified NFT + (ok (nft-get-owner? BNS-V2 id)) +) + +;; @desc (new) New get owner function +(define-read-only (get-owner-name (name (buff 48)) (namespace (buff 20))) + ;; Check and return the owner of the specified NFT + (ok (nft-get-owner? BNS-V2 (unwrap! (get-id-from-bns name namespace) ERR-NO-NAME))) +) + +;; Read-only function `get-namespace-price` calculates the registration price for a namespace based on its length. +;; @params: + ;; namespace (buff 20): The namespace for which the price is being calculated. +(define-read-only (get-namespace-price (namespace (buff 20))) + (let + ( + ;; Calculate the length of the namespace. + (namespace-len (len namespace)) + ) + ;; Ensure the namespace is not blank, its length is greater than 0. + (asserts! (> namespace-len u0) ERR-NAMESPACE-BLANK) + ;; Retrieve the price for the namespace based on its length from the NAMESPACE-PRICE-TIERS list. + ;; The price tier is determined by the minimum of 7 or the namespace length minus one. + (ok (unwrap! (element-at? NAMESPACE-PRICE-TIERS (min u7 (- namespace-len u1))) ERR-UNWRAP)) + ) +) + +;; Read-only function `get-name-price` calculates the registration price for a name based on the price buckets of the namespace +;; @params: + ;; namespace (buff 20): The namespace for which the price is being calculated. + ;; name (buff 48): The name for which the price is being calculated. +(define-read-only (get-name-price (namespace (buff 20)) (name (buff 48))) + (let + ( + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + (ok (compute-name-price name (get price-function namespace-props))) + ) +) + +;; Read-only function `can-namespace-be-registered` checks if a namespace is available for registration. +;; @params: + ;; namespace (buff 20): The namespace being checked for availability. +(define-read-only (can-namespace-be-registered (namespace (buff 20))) + ;; Returns the result of `is-namespace-available` directly, indicating if the namespace can be registered. + (ok (is-namespace-available namespace)) +) + +;; Read-only function `get-namespace-properties` for retrieving properties of a specific namespace. +;; @params: + ;; namespace (buff 20): The namespace whose properties are being queried. +(define-read-only (get-namespace-properties (namespace (buff 20))) + (let + ( + ;; Fetch the properties of the specified namespace from the `namespaces` map. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + ;; Returns the namespace along with its associated properties. + (ok { namespace: namespace, properties: namespace-props }) + ) +) + +;; Read only function to get name properties +(define-read-only (get-bns-info (name (buff 48)) (namespace (buff 20))) + (map-get? name-properties {name: name, namespace: namespace}) +) + +;; (new) Defines a read-only function to fetch the unique ID of a BNS name given its name and the namespace it belongs to. +(define-read-only (get-id-from-bns (name (buff 48)) (namespace (buff 20))) + ;; Attempts to retrieve the ID from the 'name-to-index' map using the provided name and namespace as the key. + (map-get? name-to-index {name: name, namespace: namespace}) +) + +;; (new) Defines a read-only function to fetch the BNS name and the namespace given a unique ID. +(define-read-only (get-bns-from-id (id uint)) + ;; Attempts to retrieve the name and namespace from the 'index-to-name' map using the provided id as the key. + (map-get? index-to-name id) +) + +;; (new) Fetcher for primary name +(define-read-only (get-primary-name (owner principal)) + (map-get? primary-name owner) +) + +;; (new) Fetcher for primary name returns name and namespace +(define-read-only (get-primary (owner principal)) + (ok (get-bns-from-id (unwrap! (map-get? primary-name owner) ERR-NO-PRIMARY-NAME))) +) + +;; public functions +;; @desc (new) SIP-09 compliant function to transfer a token from one owner to another. +;; @param id: ID of the NFT being transferred. +;; @param owner: Principal of the current owner of the NFT. +;; @param recipient: Principal of the recipient of the NFT. +(define-public (transfer (id uint) (owner principal) (recipient principal)) + (let + ( + ;; Get the name and namespace of the NFT. + (name-and-namespace (unwrap! (get-bns-from-id id) ERR-NO-NAME)) + (namespace (get namespace name-and-namespace)) + (name (get name name-and-namespace)) + ;; Get namespace properties and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (manager-transfers (get manager-transferable namespace-props)) + ;; Get name properties and owner. + (name-props (unwrap! (map-get? name-properties name-and-namespace) ERR-NO-NAME)) + (registered-at-value (get registered-at name-props)) + (nft-current-owner (unwrap! (nft-get-owner? BNS-V2 id) ERR-NO-NAME)) + ) + ;; First check if the name was registered + (match registered-at-value + is-registered + ;; If it was registered, check if registered-at is lower than current blockheight + ;; This check works to make sure that if a name is fast-claimed they have to wait 1 block to transfer it + (asserts! (< is-registered burn-block-height) ERR-OPERATION-UNAUTHORIZED) + ;; If it is not registered then continue + true + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Check that the namespace is launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Check owner and recipient is not the same + (asserts! (not (is-eq nft-current-owner recipient)) ERR-OPERATION-UNAUTHORIZED) + ;; We only need to check if manager transfers are true or false, if true then they have to do transfers through the manager contract that calls into mng-transfer, if false then they can call into this function + (asserts! (not manager-transfers) ERR-NOT-AUTHORIZED) + ;; Check contract-caller + (asserts! (is-eq contract-caller nft-current-owner) ERR-NOT-AUTHORIZED) + ;; Check if in fact the owner is-eq to nft-current-owner + (asserts! (is-eq owner nft-current-owner) ERR-NOT-AUTHORIZED) + ;; Ensures the NFT is not currently listed in the market. + (asserts! (is-none (map-get? market id)) ERR-LISTED) + ;; Update the name properties with the new owner + (map-set name-properties name-and-namespace (merge name-props {owner: recipient})) + ;; Update primary name if needed for owner + (update-primary-name-owner id owner) + ;; Update primary name if needed for recipient + (update-primary-name-recipient id recipient) + ;; Execute the NFT transfer. + (try! (nft-transfer? BNS-V2 id nft-current-owner recipient)) + (print + { + topic: "transfer-name", + owner: recipient, + name: {name: name, namespace: namespace}, + id: id, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + (ok true) + ) +) + +;; @desc (new) manager function to be called by managed namespaces that allows manager transfers. +;; @param id: ID of the NFT being transferred. +;; @param owner: Principal of the current owner of the NFT. +;; @param recipient: Principal of the recipient of the NFT. +(define-public (mng-transfer (id uint) (owner principal) (recipient principal)) + (let + ( + ;; Get the name and namespace of the NFT. + (name-and-namespace (unwrap! (get-bns-from-id id) ERR-NO-NAME)) + (namespace (get namespace name-and-namespace)) + (name (get name name-and-namespace)) + ;; Get namespace properties and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (manager-transfers (get manager-transferable namespace-props)) + (manager (get namespace-manager namespace-props)) + ;; Get name properties and owner. + (name-props (unwrap! (map-get? name-properties name-and-namespace) ERR-NO-NAME)) + (registered-at-value (get registered-at name-props)) + (nft-current-owner (unwrap! (nft-get-owner? BNS-V2 id) ERR-NO-NAME)) + ) + ;; First check if the name was registered + (match registered-at-value + is-registered + ;; If it was registered, check if registered-at is lower than current blockheight + ;; This check works to make sure that if a name is fast-claimed they have to wait 1 block to transfer it + (asserts! (< is-registered burn-block-height) ERR-OPERATION-UNAUTHORIZED) + ;; If it is not registered then continue + true + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Check that the namespace is launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Check owner and recipient is not the same + (asserts! (not (is-eq nft-current-owner recipient)) ERR-OPERATION-UNAUTHORIZED) + ;; We only need to check if manager transfers are true or false, if true then continue, if false then they can call into `transfer` function + (asserts! manager-transfers ERR-NOT-AUTHORIZED) + ;; Check contract-caller, we unwrap-panic because if manager-transfers is true then there has to be a manager + (asserts! (is-eq contract-caller (unwrap-panic manager)) ERR-NOT-AUTHORIZED) + ;; Check if in fact the owner is-eq to nft-current-owner + (asserts! (is-eq owner nft-current-owner) ERR-NOT-AUTHORIZED) + ;; Ensures the NFT is not currently listed in the market. + (asserts! (is-none (map-get? market id)) ERR-LISTED) + ;; Update primary name if needed for owner + (update-primary-name-owner id owner) + ;; Update primary name if needed for recipient + (update-primary-name-recipient id recipient) + ;; Update the name properties with the new owner + (map-set name-properties name-and-namespace (merge name-props {owner: recipient})) + ;; Execute the NFT transfer. + (try! (nft-transfer? BNS-V2 id nft-current-owner recipient)) + (print + { + topic: "transfer-name", + owner: recipient, + name: {name: name, namespace: namespace}, + id: id, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + (ok true) + ) +) + +;; @desc (new) Function to list an NFT for sale. +;; @param id: ID of the NFT being listed. +;; @param price: Listing price. +;; @param comm-trait: Address of the commission-trait. +(define-public (list-in-ustx (id uint) (price uint) (comm-trait )) + (let + ( + ;; Get the name and namespace of the NFT. + (name-and-namespace (unwrap! (map-get? index-to-name id) ERR-NO-NAME)) + (namespace (get namespace name-and-namespace)) + ;; Get namespace properties and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (namespace-manager (get namespace-manager namespace-props)) + ;; Get name properties and registered-at value. + (name-props (unwrap! (map-get? name-properties name-and-namespace) ERR-NO-NAME)) + (registered-at-value (get registered-at name-props)) + ;; Creates a listing record with price and commission details + (listing {price: price, commission: (contract-of comm-trait)}) + ) + ;; Checks if the name was registered + (match registered-at-value + is-registered + ;; If it was registered, check if registered-at is lower than current blockheight + ;; Same as transfers, this check works to make sure that if a name is fast-claimed they have to wait 1 block to list it + (asserts! (< is-registered burn-block-height) ERR-OPERATION-UNAUTHORIZED) + ;; If it is not registered then continue + true + ) + ;; Check if there is a namespace manager + (match namespace-manager + manager + ;; If there is then check that the contract-caller is the manager + (asserts! (is-eq manager contract-caller) ERR-NOT-AUTHORIZED) + ;; If there isn't assert that the owner is the contract-caller + (asserts! (is-eq (some contract-caller) (nft-get-owner? BNS-V2 id)) ERR-NOT-AUTHORIZED) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Updates the market map with the new listing details + (map-set market id listing) + ;; Prints listing details + (ok (print (merge listing {a: "list-in-ustx", id: id}))) + ) +) + +;; @desc (new) Function to remove an NFT listing from the market. +;; @param id: ID of the NFT being unlisted. +(define-public (unlist-in-ustx (id uint)) + (let + ( + ;; Get the name and namespace of the NFT. + (name-and-namespace (unwrap! (map-get? index-to-name id) ERR-NO-NAME)) + (namespace (get namespace name-and-namespace)) + ;; Verify if the NFT is listed in the market. + (market-map (unwrap! (map-get? market id) ERR-NOT-LISTED)) + ;; Get namespace properties and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (namespace-manager (get namespace-manager namespace-props)) + ) + ;; Check if there is a namespace manager + (match namespace-manager + manager + ;; If there is then check that the contract-caller is the manager + (asserts! (is-eq manager contract-caller) ERR-NOT-AUTHORIZED) + ;; If there isn't assert that the owner is the contract-caller + (asserts! (is-eq (some contract-caller) (nft-get-owner? BNS-V2 id)) ERR-NOT-AUTHORIZED) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Deletes the listing from the market map + (map-delete market id) + ;; Prints unlisting details + (ok (print {a: "unlist-in-ustx", id: id})) + ) +) + +;; @desc (new) Function to buy an NFT listed for sale, transferring ownership and handling commission. +;; @param id: ID of the NFT being purchased. +;; @param comm-trait: Address of the commission-trait. +(define-public (buy-in-ustx (id uint) (comm-trait )) + (let + ( + ;; Retrieves current owner and listing details + (owner (unwrap! (nft-get-owner? BNS-V2 id) ERR-NO-NAME)) + (listing (unwrap! (map-get? market id) ERR-NOT-LISTED)) + (price (get price listing)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Verifies the commission details match the listing + (asserts! (is-eq (contract-of comm-trait) (get commission listing)) ERR-WRONG-COMMISSION) + ;; Transfers STX from buyer to seller + (try! (stx-transfer? price contract-caller owner)) + ;; Handle commission payment + (try! (contract-call? comm-trait pay id price)) + ;; Transfers the NFT to the buyer + ;; This function differs from the `transfer` method by not checking who the contract-caller is, otherwise trasnfers would never be executed + (try! (purchase-transfer id owner contract-caller)) + ;; Removes the listing from the market map + (map-delete market id) + ;; Prints purchase details + (ok (print {a: "buy-in-ustx", id: id})) + ) +) + +;; @desc (new) Sets the primary name for the caller to a specific BNS name they own. +;; @param primary-name-id: ID of the name to be set as primary. +(define-public (set-primary-name (primary-name-id uint)) + (begin + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Verify the contract-caller is the owner of the name. + (asserts! (is-eq (unwrap! (nft-get-owner? BNS-V2 primary-name-id) ERR-NO-NAME) contract-caller) ERR-NOT-AUTHORIZED) + ;; Update the contract-caller's primary name. + (map-set primary-name contract-caller primary-name-id) + ;; Return true upon successful execution. + (ok true) + ) +) + +;; @desc (new) Defines a public function to burn an NFT, under managed namespaces. +;; @param id: ID of the NFT to be burned. +(define-public (mng-burn (id uint)) + (let + ( + ;; Get the name details associated with the given ID. + (name-and-namespace (unwrap! (get-bns-from-id id) ERR-NO-NAME)) + ;; Get the owner of the name. + (owner (unwrap! (nft-get-owner? BNS-V2 id) ERR-UNWRAP)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the caller is the current namespace manager. + (asserts! (is-eq contract-caller (unwrap! (get namespace-manager (unwrap! (map-get? namespaces (get namespace name-and-namespace)) ERR-NAMESPACE-NOT-FOUND)) ERR-NO-NAMESPACE-MANAGER)) ERR-NOT-AUTHORIZED) + ;; Unlist the NFT if it is listed. + (match (map-get? market id) + listed-name + (map-delete market id) + true + ) + ;; Update primary name if needed for the owner of the name + (update-primary-name-owner id owner) + ;; Delete the name from all maps: + ;; Remove the name-to-index. + (map-delete name-to-index name-and-namespace) + ;; Remove the index-to-name. + (map-delete index-to-name id) + ;; Remove the name-properties. + (map-delete name-properties name-and-namespace) + ;; Executes the burn operation for the specified NFT. + (try! (nft-burn? BNS-V2 id (unwrap! (nft-get-owner? BNS-V2 id) ERR-UNWRAP))) + (print + { + topic: "burn-name", + owner: "", + name: {name: (get name name-and-namespace), namespace: (get namespace name-and-namespace)}, + id: id + } + ) + (ok true) + ) +) + +;; @desc (new) Transfers the management role of a specific namespace to a new principal. +;; @param new-manager: Principal of the new manager. +;; @param namespace: Buffer of the namespace. +(define-public (mng-manager-transfer (new-manager (optional principal)) (namespace (buff 20))) + (let + ( + ;; Retrieve namespace properties and current manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the caller is the current namespace manager. + (asserts! (is-eq contract-caller (unwrap! (get namespace-manager namespace-props) ERR-NO-NAMESPACE-MANAGER)) ERR-NOT-AUTHORIZED) + ;; Ensure manager can be changed + (asserts! (not (get manager-frozen namespace-props)) ERR-NOT-AUTHORIZED) + ;; Update the namespace manager to the new manager. + (map-set namespaces namespace + (merge + namespace-props + {namespace-manager: new-manager} + ) + ) + (print { namespace: namespace, status: "transfer-manager", properties: (map-get? namespaces namespace) }) + (ok true) + ) +) + +;; @desc (new) freezes the ability to make manager transfers +;; @param namespace: Buffer of the namespace. +(define-public (freeze-manager (namespace (buff 20))) + (let + ( + ;; Retrieve namespace properties and current manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the caller is the current namespace manager. + (asserts! (is-eq contract-caller (unwrap! (get namespace-manager namespace-props) ERR-NO-NAMESPACE-MANAGER)) ERR-NOT-AUTHORIZED) + ;; Update the namespace manager to the new manager. + (map-set namespaces namespace + (merge + namespace-props + {manager-frozen: true} + ) + ) + (print { namespace: namespace, status: "freeze-manager", properties: (map-get? namespaces namespace) }) + (ok true) + ) +) + +;;;; NAMESPACES +;; @desc Public function `namespace-preorder` initiates the registration process for a namespace by sending a transaction with a salted hash of the namespace. +;; This transaction burns the registration fee as a commitment. +;; @params: hashed-salted-namespace (buff 20): The hashed and salted namespace being preordered. +;; @params: stx-to-burn (uint): The amount of STX tokens to be burned as part of the preorder process. +(define-public (namespace-preorder (hashed-salted-namespace (buff 20)) (stx-to-burn uint)) + (begin + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Validate that the hashed-salted-namespace is exactly 20 bytes long. + (asserts! (is-eq (len hashed-salted-namespace) HASH160LEN) ERR-HASH-MALFORMED) + ;; Check if the same hashed-salted-fqn has been used before + (asserts! (is-none (map-get? namespace-single-preorder hashed-salted-namespace)) ERR-PREORDERED-BEFORE) + ;; Confirm that the STX amount to be burned is positive + (asserts! (> stx-to-burn u0) ERR-STX-BURNT-INSUFFICIENT) + ;; Execute the token burn operation. + (try! (stx-burn? stx-to-burn contract-caller)) + ;; Record the preorder details in the `namespace-preorders` map + (map-set namespace-preorders + { hashed-salted-namespace: hashed-salted-namespace, buyer: contract-caller } + { created-at: burn-block-height, stx-burned: stx-to-burn, claimed: false } + ) + ;; Sets the map with just the hashed-salted-namespace as the key + (map-set namespace-single-preorder hashed-salted-namespace true) + ;; Return the block height at which the preorder claimability expires. + (ok (+ burn-block-height PREORDER-CLAIMABILITY-TTL)) + ) +) + +;; @desc Public function `namespace-reveal` completes the second step in the namespace registration process. +;; It associates the revealed namespace with its corresponding preorder, establishes the namespace's pricing function, and sets its lifetime and ownership details. +;; @param: namespace (buff 20): The namespace being revealed. +;; @param: namespace-salt (buff 20): The salt used during the preorder to generate a unique hash. +;; @param: p-func-base, p-func-coeff, p-func-b1 to p-func-b16: Parameters defining the price function for registering names within this namespace. +;; @param: p-func-non-alpha-discount (uint): Discount applied to names with non-alphabetic characters. +;; @param: p-func-no-vowel-discount (uint): Discount applied to names without vowels. +;; @param: lifetime (uint): Duration that names within this namespace are valid before needing renewal. +;; @param: namespace-import (principal): The principal authorized to import names into this namespace. +;; @param: namespace-manager (optional principal): The principal authorized to manage the namespace. +(define-public (namespace-reveal + (namespace (buff 20)) + (namespace-salt (buff 20)) + (p-func-base uint) + (p-func-coeff uint) + (p-func-b1 uint) + (p-func-b2 uint) + (p-func-b3 uint) + (p-func-b4 uint) + (p-func-b5 uint) + (p-func-b6 uint) + (p-func-b7 uint) + (p-func-b8 uint) + (p-func-b9 uint) + (p-func-b10 uint) + (p-func-b11 uint) + (p-func-b12 uint) + (p-func-b13 uint) + (p-func-b14 uint) + (p-func-b15 uint) + (p-func-b16 uint) + (p-func-non-alpha-discount uint) + (p-func-no-vowel-discount uint) + (lifetime uint) + (namespace-import principal) + (namespace-manager (optional principal)) + (can-update-price bool) + (manager-transfers bool) + (manager-frozen bool) +) + (let + ( + ;; Generate the hashed, salted namespace identifier to match with its preorder. + (hashed-salted-namespace (hash160 (concat (concat namespace 0x2e) namespace-salt))) + ;; Define the price function based on the provided parameters. + (price-function + { + buckets: (list p-func-b1 p-func-b2 p-func-b3 p-func-b4 p-func-b5 p-func-b6 p-func-b7 p-func-b8 p-func-b9 p-func-b10 p-func-b11 p-func-b12 p-func-b13 p-func-b14 p-func-b15 p-func-b16), + base: p-func-base, + coeff: p-func-coeff, + nonalpha-discount: p-func-non-alpha-discount, + no-vowel-discount: p-func-no-vowel-discount + } + ) + ;; Retrieve the preorder record to ensure it exists and is valid for the revealing namespace + (preorder (unwrap! (map-get? namespace-preorders { hashed-salted-namespace: hashed-salted-namespace, buyer: contract-caller}) ERR-PREORDER-NOT-FOUND)) + ;; Calculate the namespace's registration price for validation. + (namespace-price (try! (get-namespace-price namespace))) + ) + ;; Ensure the preorder has not been claimed before + (asserts! (not (get claimed preorder)) ERR-NAMESPACE-ALREADY-EXISTS) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the namespace consists of valid characters only. + (asserts! (not (has-invalid-chars namespace)) ERR-CHARSET-INVALID) + ;; Check that the namespace is available for reveal. + (asserts! (unwrap! (can-namespace-be-registered namespace) ERR-NAMESPACE-ALREADY-EXISTS) ERR-NAMESPACE-ALREADY-EXISTS) + ;; Verify the burned amount during preorder meets or exceeds the namespace's registration price. + (asserts! (>= (get stx-burned preorder) namespace-price) ERR-STX-BURNT-INSUFFICIENT) + ;; Confirm the reveal action is performed within the allowed timeframe from the preorder. + (asserts! (< burn-block-height (+ (get created-at preorder) PREORDER-CLAIMABILITY-TTL)) ERR-PREORDER-CLAIMABILITY-EXPIRED) + ;; Ensure at least 1 block has passed after the preorder to avoid namespace sniping. + (asserts! (>= burn-block-height (+ (get created-at preorder) u1)) ERR-OPERATION-UNAUTHORIZED) + ;; Check if the namespace manager is assigned + (match namespace-manager + namespace-m + ;; If namespace-manager is assigned, then assign everything except the lifetime, that is set to u0 sinces renewals will be made in the namespace manager contract and set the can update price function to false, since no changes will ever need to be made there. + (map-set namespaces namespace + { + namespace-manager: namespace-manager, + manager-transferable: manager-transfers, + manager-frozen: manager-frozen, + namespace-import: namespace-import, + revealed-at: burn-block-height, + launched-at: none, + lifetime: u0, + can-update-price-function: can-update-price, + price-function: price-function + } + ) + ;; If no manager is assigned + (map-set namespaces namespace + { + namespace-manager: none, + manager-transferable: manager-transfers, + manager-frozen: manager-frozen, + namespace-import: namespace-import, + revealed-at: burn-block-height, + launched-at: none, + lifetime: lifetime, + can-update-price-function: can-update-price, + price-function: price-function + } + ) + ) + ;; Update the claimed value for the preorder + (map-set namespace-preorders { hashed-salted-namespace: hashed-salted-namespace, buyer: contract-caller } + (merge preorder + { + claimed: true + } + ) + ) + ;; Confirm successful reveal of the namespace + (ok true) + ) +) + +;; @desc Public function `namespace-launch` marks a namespace as launched and available for public name registrations. +;; @param: namespace (buff 20): The namespace to be launched and made available for public registrations. +(define-public (namespace-launch (namespace (buff 20))) + (let + ( + ;; Retrieve the properties of the namespace to ensure it exists and to check its current state. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the transaction sender is the namespace's designated import principal. + (asserts! (is-eq (get namespace-import namespace-props) contract-caller) ERR-OPERATION-UNAUTHORIZED) + ;; Verify the namespace has not already been launched. + (asserts! (is-none (get launched-at namespace-props)) ERR-NAMESPACE-ALREADY-LAUNCHED) + ;; Confirm that the action is taken within the permissible time frame since the namespace was revealed. + (asserts! (< burn-block-height (+ (get revealed-at namespace-props) NAMESPACE-LAUNCHABILITY-TTL)) ERR-NAMESPACE-PREORDER-LAUNCHABILITY-EXPIRED) + ;; Update the `namespaces` map with the newly launched status. + (map-set namespaces namespace (merge namespace-props { launched-at: (some burn-block-height) })) + ;; Emit an event to indicate the namespace is now ready and launched. + (print { namespace: namespace, status: "launch", properties: (map-get? namespaces namespace) }) + ;; Confirm the successful launch of the namespace. + (ok true) + ) +) + +;; @desc (new) Public function `turn-off-manager-transfers` disables manager transfers for a namespace (callable only once). +;; @param: namespace (buff 20): The namespace for which manager transfers will be disabled. +(define-public (turn-off-manager-transfers (namespace (buff 20))) + (let + ( + ;; Retrieve the properties of the namespace and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (namespace-manager (unwrap! (get namespace-manager namespace-props) ERR-NO-NAMESPACE-MANAGER)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the function caller is the namespace manager. + (asserts! (is-eq contract-caller namespace-manager) ERR-NOT-AUTHORIZED) + ;; Disable manager transfers. + (map-set namespaces namespace (merge namespace-props {manager-transferable: false})) + (print { namespace: namespace, status: "turn-off-manager-transfers", properties: (map-get? namespaces namespace) }) + ;; Confirm successful execution. + (ok true) + ) +) + +;; @desc Public function `name-import` allows the insertion of names into a namespace that has been revealed but not yet launched. +;; This facilitates pre-populating the namespace with specific names, assigning owners. +;; @param: namespace (buff 20): The namespace into which the name is being imported. +;; @param: name (buff 48): The name being imported into the namespace. +;; @param: beneficiary (principal): The principal who will own the imported name. +;; @param: stx-burn (uint): The amount of STX tokens to be burned as part of the import process. +(define-public (name-import (namespace (buff 20)) (name (buff 48)) (beneficiary principal)) + (let + ( + ;; Fetch properties of the specified namespace. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ;; Fetch the latest index to mint + (current-mint (+ (var-get bns-index) u1)) + (price (if (is-none (get namespace-manager namespace-props)) + (try! (compute-name-price name (get price-function namespace-props))) + u0 + ) + ) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the name is not already registered. + (asserts! (is-none (map-get? name-properties {name: name, namespace: namespace})) ERR-NAME-NOT-AVAILABLE) + ;; Verify that the name contains only valid characters. + (asserts! (not (has-invalid-chars name)) ERR-CHARSET-INVALID) + ;; Ensure the contract-caller is the namespace's designated import principal or the namespace manager + (asserts! (or (is-eq (get namespace-import namespace-props) contract-caller) (is-eq (get namespace-manager namespace-props) (some contract-caller))) ERR-OPERATION-UNAUTHORIZED) + ;; Check that the namespace has not been launched yet, as names can only be imported to namespaces that are revealed but not launched. + (asserts! (is-none (get launched-at namespace-props)) ERR-NAMESPACE-ALREADY-LAUNCHED) + ;; Confirm that the import is occurring within the allowed timeframe since the namespace was revealed. + (asserts! (< burn-block-height (+ (get revealed-at namespace-props) NAMESPACE-LAUNCHABILITY-TTL)) ERR-NAMESPACE-PREORDER-LAUNCHABILITY-EXPIRED) + ;; Set the name properties + (map-set name-properties {name: name, namespace: namespace} + { + registered-at: none, + imported-at: (some burn-block-height), + hashed-salted-fqn-preorder: none, + preordered-by: none, + renewal-height: u0, + stx-burn: price, + owner: beneficiary, + } + ) + (map-set name-to-index {name: name, namespace: namespace} current-mint) + (map-set index-to-name current-mint {name: name, namespace: namespace}) + ;; Update primary name if needed for send-to + (update-primary-name-recipient current-mint beneficiary) + ;; Update the index of the minting + (var-set bns-index current-mint) + ;; Mint the name to the beneficiary + (try! (nft-mint? BNS-V2 current-mint beneficiary)) + ;; Log the new name registration + (print + { + topic: "new-name", + owner: beneficiary, + name: {name: name, namespace: namespace}, + id: current-mint, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Confirm successful import of the name. + (ok true) + ) +) + +;; @desc Public function `namespace-update-price` updates the pricing function for a specific namespace. +;; @param: namespace (buff 20): The namespace for which the price function is being updated. +;; @param: p-func-base (uint): The base price used in the pricing function. +;; @param: p-func-coeff (uint): The coefficient used in the pricing function. +;; @param: p-func-b1 to p-func-b16 (uint): The bucket-specific multipliers for the pricing function. +;; @param: p-func-non-alpha-discount (uint): The discount applied for non-alphabetic characters. +;; @param: p-func-no-vowel-discount (uint): The discount applied when no vowels are present. +(define-public (namespace-update-price + (namespace (buff 20)) + (p-func-base uint) + (p-func-coeff uint) + (p-func-b1 uint) + (p-func-b2 uint) + (p-func-b3 uint) + (p-func-b4 uint) + (p-func-b5 uint) + (p-func-b6 uint) + (p-func-b7 uint) + (p-func-b8 uint) + (p-func-b9 uint) + (p-func-b10 uint) + (p-func-b11 uint) + (p-func-b12 uint) + (p-func-b13 uint) + (p-func-b14 uint) + (p-func-b15 uint) + (p-func-b16 uint) + (p-func-non-alpha-discount uint) + (p-func-no-vowel-discount uint) +) + (let + ( + ;; Retrieve the current properties of the namespace. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ;; Construct the new price function. + (price-function + { + buckets: (list p-func-b1 p-func-b2 p-func-b3 p-func-b4 p-func-b5 p-func-b6 p-func-b7 p-func-b8 p-func-b9 p-func-b10 p-func-b11 p-func-b12 p-func-b13 p-func-b14 p-func-b15 p-func-b16), + base: p-func-base, + coeff: p-func-coeff, + nonalpha-discount: p-func-non-alpha-discount, + no-vowel-discount: p-func-no-vowel-discount + } + ) + ) + (match (get namespace-manager namespace-props) + manager + ;; Ensure that the transaction sender is the namespace's designated import principal. + (asserts! (is-eq manager contract-caller) ERR-OPERATION-UNAUTHORIZED) + ;; Ensure that the contract-caller is the namespace's designated import principal. + (asserts! (is-eq (get namespace-import namespace-props) contract-caller) ERR-OPERATION-UNAUTHORIZED) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Verify the namespace's price function can still be updated. + (asserts! (get can-update-price-function namespace-props) ERR-OPERATION-UNAUTHORIZED) + ;; Update the namespace's record in the `namespaces` map with the new price function. + (map-set namespaces namespace (merge namespace-props { price-function: price-function })) + (print { namespace: namespace, status: "update-price-manager", properties: (map-get? namespaces namespace) }) + ;; Confirm the successful update of the price function. + (ok true) + ) +) + +;; @desc Public function `namespace-freeze-price` disables the ability to update the price function for a given namespace. +;; @param: namespace (buff 20): The target namespace for which the price function update capability is being revoked. +(define-public (namespace-freeze-price (namespace (buff 20))) + (let + ( + ;; Retrieve the properties of the specified namespace to verify its existence and fetch its current settings. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + (match (get namespace-manager namespace-props) + manager + ;; Ensure that the transaction sender is the same as the namespace's designated import principal. + (asserts! (is-eq manager contract-caller) ERR-OPERATION-UNAUTHORIZED) + ;; Ensure that the contract-caller is the same as the namespace's designated import principal. + (asserts! (is-eq (get namespace-import namespace-props) contract-caller) ERR-OPERATION-UNAUTHORIZED) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Update the namespace properties in the `namespaces` map, setting `can-update-price-function` to false. + (map-set namespaces namespace + (merge namespace-props { can-update-price-function: false }) + ) + (print { namespace: namespace, status: "freeze-price-manager", properties: (map-get? namespaces namespace) }) + ;; Return a success confirmation. + (ok true) + ) +) + +;; @desc (new) A 'fast' one-block registration function: (name-claim-fast) +;; Warning: this *is* snipeable, for a slower but un-snipeable claim, use the pre-order & register functions +;; @param: name (buff 48): The name being claimed. +;; @param: namespace (buff 20): The namespace under which the name is being claimed. +;; @param: stx-burn (uint): The amount of STX to burn for the claim. +;; @param: send-to (principal): The principal to whom the name will be sent. +(define-public (name-claim-fast (name (buff 48)) (namespace (buff 20)) (send-to principal)) + (let + ( + ;; Retrieve namespace properties. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (current-namespace-manager (get namespace-manager namespace-props)) + ;; Calculates the ID for the new name to be minted. + (id-to-be-minted (+ (var-get bns-index) u1)) + ;; Check if the name already exists. + (name-props (map-get? name-properties {name: name, namespace: namespace})) + ;; new to get the price of the name + (name-price (if (is-none current-namespace-manager) + (try! (compute-name-price name (get price-function namespace-props))) + u0 + ) + ) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the name is not already registered. + (asserts! (is-none name-props) ERR-NAME-NOT-AVAILABLE) + ;; Verify that the name contains only valid characters. + (asserts! (not (has-invalid-chars name)) ERR-CHARSET-INVALID) + ;; Ensure that the namespace is launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Check namespace manager + (match current-namespace-manager + manager + ;; If manager, check contract-caller is manager + (asserts! (is-eq contract-caller manager) ERR-NOT-AUTHORIZED) + ;; If no manager + (begin + ;; Asserts contract-caller is the send-to if not a managed namespace + (asserts! (is-eq contract-caller send-to) ERR-NOT-AUTHORIZED) + ;; Updated this to burn the actual ammount of the name-price + (try! (stx-burn? name-price send-to)) + ) + ) + ;; Update the index + (var-set bns-index id-to-be-minted) + ;; Sets properties for the newly registered name. + (map-set name-properties + { + name: name, namespace: namespace + } + { + registered-at: (some (+ burn-block-height u1)), + imported-at: none, + hashed-salted-fqn-preorder: none, + preordered-by: none, + ;; Updated this to actually start with the registered-at date/block, and also to be u0 if it is a managed namespace + renewal-height: (if (is-eq (get lifetime namespace-props) u0) + u0 + (+ (get lifetime namespace-props) burn-block-height u1) + ), + stx-burn: name-price, + owner: send-to, + } + ) + (map-set name-to-index {name: name, namespace: namespace} id-to-be-minted) + (map-set index-to-name id-to-be-minted {name: name, namespace: namespace}) + ;; Update primary name if needed for send-to + (update-primary-name-recipient id-to-be-minted send-to) + ;; Mints the new BNS name. + (try! (nft-mint? BNS-V2 id-to-be-minted send-to)) + ;; Log the new name registration + (print + { + topic: "new-name", + owner: send-to, + name: {name: name, namespace: namespace}, + id: id-to-be-minted, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Signals successful completion. + (ok id-to-be-minted) + ) +) + +;; @desc Defines a public function `name-preorder` for preordering BNS names by burning the registration fee and submitting the salted hash. +;; Callable by anyone; the actual check for authorization happens in the `name-register` function. +;; @param: hashed-salted-fqn (buff 20): The hashed and salted fully qualified name. +;; @param: stx-to-burn (uint): The amount of STX to burn for the preorder. +(define-public (name-preorder (hashed-salted-fqn (buff 20)) (stx-to-burn uint)) + (begin + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Validate the length of the hashed-salted FQN. + (asserts! (is-eq (len hashed-salted-fqn) HASH160LEN) ERR-HASH-MALFORMED) + ;; Ensures that the amount of STX specified to burn is greater than zero. + (asserts! (> stx-to-burn u0) ERR-STX-BURNT-INSUFFICIENT) + ;; Check if the same hashed-salted-fqn has been used before + (asserts! (is-none (map-get? name-single-preorder hashed-salted-fqn)) ERR-PREORDERED-BEFORE) + ;; Transfers the specified amount of stx to the BNS contract to burn on register + (try! (stx-transfer? stx-to-burn contract-caller .BNS-V2)) + ;; Records the preorder in the 'name-preorders' map. + (map-set name-preorders + { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller } + { created-at: burn-block-height, stx-burned: stx-to-burn, claimed: false} + ) + ;; Sets the map with just the hashed-salted-fqn as the key + (map-set name-single-preorder hashed-salted-fqn true) + ;; Returns the block height at which the preorder's claimability period will expire. + (ok (+ burn-block-height PREORDER-CLAIMABILITY-TTL)) + ) +) + +;; @desc Public function `name-register` finalizes the registration of a BNS name for users from unmanaged namespaces. +;; @param: namespace (buff 20): The namespace to which the name belongs. +;; @param: name (buff 48): The name to be registered. +;; @param: salt (buff 20): The salt used during the preorder. +(define-public (name-register (namespace (buff 20)) (name (buff 48)) (salt (buff 20))) + (let + ( + ;; Generate a unique identifier for the name by hashing the fully-qualified name with salt + (hashed-salted-fqn (hash160 (concat (concat (concat name 0x2e) namespace) salt))) + ;; Retrieve the preorder details for this name + (preorder (unwrap! (map-get? name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller }) ERR-PREORDER-NOT-FOUND)) + ;; Fetch the properties of the namespace + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ;; Get the amount of burned STX + (stx-burned (get stx-burned preorder)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure that the namespace is launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Ensure the preorder hasn't been claimed before + (asserts! (not (get claimed preorder)) ERR-OPERATION-UNAUTHORIZED) + ;; Check that the namespace doesn't have a manager (implying it's open for registration) + (asserts! (is-none (get namespace-manager namespace-props)) ERR-NOT-AUTHORIZED) + ;; Verify that the preorder was made after the namespace was launched + (asserts! (> (get created-at preorder) (unwrap! (get launched-at namespace-props) ERR-UNWRAP)) ERR-NAME-PREORDERED-BEFORE-NAMESPACE-LAUNCH) + ;; Ensure the registration is happening within the allowed time window after preorder + (asserts! (< burn-block-height (+ (get created-at preorder) PREORDER-CLAIMABILITY-TTL)) ERR-PREORDER-CLAIMABILITY-EXPIRED) + ;; Make sure at least one block has passed since the preorder (prevents front-running) + (asserts! (> burn-block-height (+ (get created-at preorder) u1)) ERR-NAME-NOT-CLAIMABLE-YET) + ;; Verify that enough STX was burned during preorder to cover the name price + (asserts! (is-eq stx-burned (try! (compute-name-price name (get price-function namespace-props)))) ERR-STX-BURNT-INSUFFICIENT) + ;; Verify that the name contains only valid characters. + (asserts! (not (has-invalid-chars name)) ERR-CHARSET-INVALID) + ;; Mark the preorder as claimed to prevent double-spending + (map-set name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller } (merge preorder {claimed: true})) + ;; Check if the name already exists + (match (map-get? name-properties {name: name, namespace: namespace}) + name-props-exist + ;; If the name exists + (handle-existing-name name-props-exist hashed-salted-fqn (get created-at preorder) stx-burned name namespace (get lifetime namespace-props)) + ;; If the name does not exist + (register-new-name (+ (var-get bns-index) u1) hashed-salted-fqn stx-burned name namespace (get lifetime namespace-props)) + ) + ) +) + +;; @desc (new) Defines a public function `claim-preorder` for claiming back the STX commited to be burnt on registration. +;; This should only be allowed to go through if preorder-claimability-ttl has passed +;; @param: hashed-salted-fqn (buff 20): The hashed and salted fully qualified name. +(define-public (claim-preorder (hashed-salted-fqn (buff 20))) + (let + ( + ;; Retrieves the preorder details. + (preorder (unwrap! (map-get? name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller }) ERR-PREORDER-NOT-FOUND)) + (claimer contract-caller) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Check if the preorder-claimability-ttl has passed + (asserts! (> burn-block-height (+ (get created-at preorder) PREORDER-CLAIMABILITY-TTL)) ERR-OPERATION-UNAUTHORIZED) + ;; Asserts that the preorder has not been claimed + (asserts! (not (get claimed preorder)) ERR-OPERATION-UNAUTHORIZED) + ;; Transfers back the specified amount of stx from the BNS contract to the contract-caller + (try! (as-contract (stx-transfer? (get stx-burned preorder) .BNS-V2 claimer))) + ;; Deletes the preorder in the 'name-preorders' map. + (map-delete name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller }) + ;; Remove the entry from the name-single-preorder map + (map-delete name-single-preorder hashed-salted-fqn) + ;; Returns ok true + (ok true) + ) +) + +;; @desc (new) This function is similar to `name-preorder` but only for namespace managers, without the burning of STX tokens. +;; Intended only for managers as mng-name-register & name-register will validate. +;; @param: hashed-salted-fqn (buff 20): The hashed and salted fully-qualified name (FQN) being preordered. +(define-public (mng-name-preorder (hashed-salted-fqn (buff 20))) + (begin + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Validates that the length of the hashed and salted FQN is exactly 20 bytes. + (asserts! (is-eq (len hashed-salted-fqn) HASH160LEN) ERR-HASH-MALFORMED) + ;; Check if the same hashed-salted-fqn has been used before + (asserts! (is-none (map-get? name-single-preorder hashed-salted-fqn)) ERR-PREORDERED-BEFORE) + ;; Records the preorder in the 'name-preorders' map. Buyer set to contract-caller + (map-set name-preorders + { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller } + { created-at: burn-block-height, stx-burned: u0, claimed: false } + ) + ;; Sets the map with just the hashed-salted-fqn as the key + (map-set name-single-preorder hashed-salted-fqn true) + ;; Returns the block height at which the preorder's claimability period will expire. + (ok (+ burn-block-height PREORDER-CLAIMABILITY-TTL)) + ) +) + +;; @desc (new) This function uses provided details to verify the preorder, register the name, and assign it initial properties. +;; This should only allow Managers from MANAGED namespaces to register names. +;; @param: namespace (buff 20): The namespace for the name. +;; @param: name (buff 48): The name being registered. +;; @param: salt (buff 20): The salt used in hashing. +;; @param: send-to (principal): The principal to whom the name will be registered. +(define-public (mng-name-register (namespace (buff 20)) (name (buff 48)) (salt (buff 20)) (send-to principal)) + (let + ( + ;; Generates the hashed, salted fully-qualified name. + (hashed-salted-fqn (hash160 (concat (concat (concat name 0x2e) namespace) salt))) + ;; Retrieves the existing properties of the namespace to confirm its existence and management details. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (current-namespace-manager (unwrap! (get namespace-manager namespace-props) ERR-NO-NAMESPACE-MANAGER)) + ;; Retrieves the preorder information using the hashed-salted FQN to verify the preorder exists + (preorder (unwrap! (map-get? name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: current-namespace-manager }) ERR-PREORDER-NOT-FOUND)) + ;; Calculates the ID for the new name to be minted. + (id-to-be-minted (+ (var-get bns-index) u1)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the preorder has not been claimed before + (asserts! (not (get claimed preorder)) ERR-OPERATION-UNAUTHORIZED) + ;; Ensure the name is not already registered + (asserts! (is-none (map-get? name-properties {name: name, namespace: namespace})) ERR-NAME-NOT-AVAILABLE) + ;; Verify that the name contains only valid characters. + (asserts! (not (has-invalid-chars name)) ERR-CHARSET-INVALID) + ;; Verifies that the caller is the namespace manager. + (asserts! (is-eq contract-caller current-namespace-manager) ERR-NOT-AUTHORIZED) + ;; Validates that the preorder was made after the namespace was officially launched. + (asserts! (> (get created-at preorder) (unwrap! (get launched-at namespace-props) ERR-UNWRAP)) ERR-NAME-PREORDERED-BEFORE-NAMESPACE-LAUNCH) + ;; Verifies the registration is completed within the claimability period. + (asserts! (< burn-block-height (+ (get created-at preorder) PREORDER-CLAIMABILITY-TTL)) ERR-PREORDER-CLAIMABILITY-EXPIRED) + ;; Sets properties for the newly registered name. + (map-set name-properties + { + name: name, namespace: namespace + } + { + registered-at: (some burn-block-height), + imported-at: none, + hashed-salted-fqn-preorder: (some hashed-salted-fqn), + preordered-by: (some send-to), + ;; Updated this to be u0, so that renewals are handled through the namespace manager + renewal-height: u0, + stx-burn: u0, + owner: send-to, + } + ) + (map-set name-to-index {name: name, namespace: namespace} id-to-be-minted) + (map-set index-to-name id-to-be-minted {name: name, namespace: namespace}) + ;; Update primary name if needed for send-to + (update-primary-name-recipient id-to-be-minted send-to) + ;; Updates BNS-index variable to the newly minted ID. + (var-set bns-index id-to-be-minted) + ;; Update map to claimed for preorder, to avoid people reclaiming stx from an already registered name + (map-set name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: current-namespace-manager } (merge preorder {claimed: true})) + ;; Mints the BNS name as an NFT to the send-to address, finalizing the registration. + (try! (nft-mint? BNS-V2 id-to-be-minted send-to)) + ;; Log the new name registration + (print + { + topic: "new-name", + owner: send-to, + name: {name: name, namespace: namespace}, + id: id-to-be-minted, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Confirms successful registration of the name. + (ok id-to-be-minted) + ) +) + +;; Public function `name-renewal` for renewing ownership of a name. +;; @param: namespace (buff 20): The namespace of the name to be renewed. +;; @param: name (buff 48): The actual name to be renewed. +;; @param: stx-to-burn (uint): The amount of STX tokens to be burned for renewal. +(define-public (name-renewal (namespace (buff 20)) (name (buff 48))) + (let + ( + ;; Get the unique identifier for this name + (name-index (unwrap! (get-id-from-bns name namespace) ERR-NO-NAME)) + ;; Retrieve the properties of the namespace + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ;; Get the manager of the namespace, if any + (namespace-manager (get namespace-manager namespace-props)) + ;; Get the current owner of the name + (owner (unwrap! (nft-get-owner? BNS-V2 name-index) ERR-NO-NAME)) + ;; Retrieve the properties of the name + (name-props (unwrap! (map-get? name-properties { name: name, namespace: namespace }) ERR-NO-NAME)) + ;; Get the lifetime of names in this namespace + (lifetime (get lifetime namespace-props)) + ;; Get the current renewal height of the name + (renewal-height (try! (get-renewal-height name-index))) + ;; Calculate the new renewal height based on current block height + (new-renewal-height (+ burn-block-height lifetime)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Verify that the namespace has been launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Ensure the namespace doesn't have a manager + (asserts! (is-none namespace-manager) ERR-NAMESPACE-HAS-MANAGER) + ;; Check if renewals are required for this namespace + (asserts! (> lifetime u0) ERR-LIFETIME-EQUAL-0) + ;; Handle renewal based on whether it's within the grace period or not + (if (< burn-block-height (+ renewal-height NAME-GRACE-PERIOD-DURATION)) + (try! (handle-renewal-in-grace-period name namespace name-props owner lifetime new-renewal-height)) + (try! (handle-renewal-after-grace-period name namespace name-props owner name-index new-renewal-height)) + ) + ;; Burn the specified amount of STX + (try! (stx-burn? (try! (compute-name-price name (get price-function namespace-props))) contract-caller)) + ;; update the new stx-burn to the one paid in renewal + (map-set name-properties { name: name, namespace: namespace } (merge (unwrap-panic (map-get? name-properties { name: name, namespace: namespace })) {stx-burn: (try! (compute-name-price name (get price-function namespace-props)))})) + ;; Return success + (ok true) + ) +) + +;; Private function to handle renewals within the grace period +(define-private (handle-renewal-in-grace-period + (name (buff 48)) + (namespace (buff 20)) + (name-props + { + registered-at: (optional uint), + imported-at: (optional uint), + hashed-salted-fqn-preorder: (optional (buff 20)), + preordered-by: (optional principal), + renewal-height: uint, + stx-burn: uint, + owner: principal + } + ) + (owner principal) + (lifetime uint) + (new-renewal-height uint) +) + (begin + ;; Ensure only the owner can renew within the grace period + (asserts! (is-eq contract-caller owner) ERR-NOT-AUTHORIZED) + ;; Update the name properties with the new renewal height + (map-set name-properties {name: name, namespace: namespace} + (merge name-props + { + renewal-height: + ;; If still within lifetime, extend from current renewal height; otherwise, use new renewal height + (if (< burn-block-height (unwrap-panic (get-renewal-height (unwrap-panic (get-id-from-bns name namespace))))) + (+ (unwrap-panic (get-renewal-height (unwrap-panic (get-id-from-bns name namespace)))) lifetime) + new-renewal-height + ) + } + ) + ) + (print + { + topic: "renew-name", + owner: owner, + name: {name: name, namespace: namespace}, + id: (get-id-from-bns name namespace), + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + (ok true) + ) +) + +;; Private function to handle renewals after the grace period +(define-private (handle-renewal-after-grace-period + (name (buff 48)) + (namespace (buff 20)) + (name-props + { + registered-at: (optional uint), + imported-at: (optional uint), + hashed-salted-fqn-preorder: (optional (buff 20)), + preordered-by: (optional principal), + renewal-height: uint, + stx-burn: uint, + owner: principal + } + ) + (owner principal) + (name-index uint) + (new-renewal-height uint) +) + (if (is-eq contract-caller owner) + ;; If the owner is renewing, simply update the renewal height + (ok + (map-set name-properties {name: name, namespace: namespace} + (merge name-props {renewal-height: new-renewal-height}) + ) + ) + ;; If someone else is renewing (taking over the name) + (begin + ;; Check if the name is listed on the market and remove the listing if it is + (match (map-get? market name-index) + listed-name + (map-delete market name-index) + true + ) + (map-set name-properties {name: name, namespace: namespace} + (merge name-props {renewal-height: new-renewal-height}) + ) + ;; Update the name properties with the new renewal height and owner + (ok (try! (purchase-transfer name-index owner contract-caller))) + ) + ) +) + +;; Returns the minimum of two uint values. +(define-private (min (a uint) (b uint)) + ;; If 'a' is less than or equal to 'b', return 'a', else return 'b'. + (if (<= a b) a b) +) + +;; Returns the maximum of two uint values. +(define-private (max (a uint) (b uint)) + ;; If 'a' is greater than 'b', return 'a', else return 'b'. + (if (> a b) a b) +) + +;; Retrieves an exponent value from a list of buckets based on the provided index. +(define-private (get-exp-at-index (buckets (list 16 uint)) (index uint)) + ;; Retrieves the element at the specified index. + (unwrap-panic (element-at? buckets index)) +) + +;; Determines if a character is a digit (0-9). +(define-private (is-digit (char (buff 1))) + (or + ;; Checks if the character is between '0' and '9' using hex values. + (is-eq char 0x30) ;; 0 + (is-eq char 0x31) ;; 1 + (is-eq char 0x32) ;; 2 + (is-eq char 0x33) ;; 3 + (is-eq char 0x34) ;; 4 + (is-eq char 0x35) ;; 5 + (is-eq char 0x36) ;; 6 + (is-eq char 0x37) ;; 7 + (is-eq char 0x38) ;; 8 + (is-eq char 0x39) ;; 9 + ) +) + +;; Checks if a character is a lowercase alphabetic character (a-z). +(define-private (is-lowercase-alpha (char (buff 1))) + (or + ;; Checks for each lowercase letter using hex values. + (is-eq char 0x61) ;; a + (is-eq char 0x62) ;; b + (is-eq char 0x63) ;; c + (is-eq char 0x64) ;; d + (is-eq char 0x65) ;; e + (is-eq char 0x66) ;; f + (is-eq char 0x67) ;; g + (is-eq char 0x68) ;; h + (is-eq char 0x69) ;; i + (is-eq char 0x6a) ;; j + (is-eq char 0x6b) ;; k + (is-eq char 0x6c) ;; l + (is-eq char 0x6d) ;; m + (is-eq char 0x6e) ;; n + (is-eq char 0x6f) ;; o + (is-eq char 0x70) ;; p + (is-eq char 0x71) ;; q + (is-eq char 0x72) ;; r + (is-eq char 0x73) ;; s + (is-eq char 0x74) ;; t + (is-eq char 0x75) ;; u + (is-eq char 0x76) ;; v + (is-eq char 0x77) ;; w + (is-eq char 0x78) ;; x + (is-eq char 0x79) ;; y + (is-eq char 0x7a) ;; z + ) +) + +;; Determines if a character is a vowel (a, e, i, o, u, and y). +(define-private (is-vowel (char (buff 1))) + (or + (is-eq char 0x61) ;; a + (is-eq char 0x65) ;; e + (is-eq char 0x69) ;; i + (is-eq char 0x6f) ;; o + (is-eq char 0x75) ;; u + (is-eq char 0x79) ;; y + ) +) + +;; Identifies if a character is a special character, specifically '-' or '_'. +(define-private (is-special-char (char (buff 1))) + (or + (is-eq char 0x2d) ;; - + (is-eq char 0x5f)) ;; _ +) + +;; Determines if a character is valid within a name, based on allowed character sets. +(define-private (is-char-valid (char (buff 1))) + (or (is-lowercase-alpha char) (is-digit char) (is-special-char char)) +) + +;; Checks if a character is non-alphabetic, either a digit or a special character. +(define-private (is-nonalpha (char (buff 1))) + (or (is-digit char) (is-special-char char)) +) + +;; Evaluates if a name contains any vowel characters. +(define-private (has-vowels-chars (name (buff 48))) + (> (len (filter is-vowel name)) u0) +) + +;; Determines if a name contains non-alphabetic characters. +(define-private (has-nonalpha-chars (name (buff 48))) + (> (len (filter is-nonalpha name)) u0) +) + +;; Identifies if a name contains any characters that are not considered valid. +(define-private (has-invalid-chars (name (buff 48))) + (< (len (filter is-char-valid name)) (len name)) +) + +;; Private helper function `is-namespace-available` checks if a namespace is available for registration or other operations. +;; It considers if the namespace has been launched and whether it has expired. +;; @params: + ;; namespace (buff 20): The namespace to check for availability. +(define-private (is-namespace-available (namespace (buff 20))) + ;; Check if the namespace exists + (match (map-get? namespaces namespace) + namespace-props + ;; If it exists + ;; Check if the namespace has been launched. + (match (get launched-at namespace-props) + launched + ;; If the namespace is launched, it's considered unavailable if it hasn't expired. + false + ;; Check if the namespace is expired by comparing the current block height to the reveal time plus the launchability TTL. + (> burn-block-height (+ (get revealed-at namespace-props) NAMESPACE-LAUNCHABILITY-TTL)) + ) + ;; If the namespace doesn't exist in the map, it's considered available. + true + ) +) + +;; Private helper function `compute-name-price` calculates the registration price for a name based on its length and character composition. +;; It utilizes a configurable pricing function that can adjust prices based on the name's characteristics. +;; @params: +;; name (buff 48): The name for which the price is being calculated. +;; price-function (tuple): A tuple containing the parameters of the pricing function, including: +;; buckets (list 16 uint): A list defining price multipliers for different name lengths. +;; base (uint): The base price multiplier. +;; coeff (uint): A coefficient that adjusts the base price. +;; nonalpha-discount (uint): A discount applied to names containing non-alphabetic characters. +;; no-vowel-discount (uint): A discount applied to names lacking vowel characters. +(define-private (compute-name-price (name (buff 48)) (price-function {buckets: (list 16 uint), base: uint, coeff: uint, nonalpha-discount: uint, no-vowel-discount: uint})) + (let + ( + ;; Determine the appropriate exponent based on the name's length. + ;; This corresponds to a specific bucket in the pricing function. + ;; The length of the name is used to index into the buckets list, with a maximum index of 15. + (exponent (get-exp-at-index (get buckets price-function) (min u15 (- (len name) u1)))) + ;; Calculate the no-vowel discount. + ;; If the name has no vowels, apply the no-vowel discount from the price function. + ;; Otherwise, use 1 indicating no discount. + (no-vowel-discount (if (not (has-vowels-chars name)) (get no-vowel-discount price-function) u1)) + ;; Calculate the non-alphabetic character discount. + ;; If the name contains non-alphabetic characters, apply the non-alpha discount from the price function. + ;; Otherwise, use 1 indicating no discount. + (nonalpha-discount (if (has-nonalpha-chars name) (get nonalpha-discount price-function) u1)) + (len-name (len name)) + ) + (asserts! (> len-name u0) ERR-NAME-BLANK) + ;; Compute the final price. + ;; The base price, adjusted by the coefficient and exponent, is divided by the greater of the two discounts (non-alpha or no-vowel). + ;; The result is then multiplied by 10 to adjust for unit precision. + (ok (* (/ (* (get coeff price-function) (pow (get base price-function) exponent)) (max nonalpha-discount no-vowel-discount)) u10)) + ) +) + +;; This function is similar to the 'transfer' function but does not check that the owner is the contract-caller. +;; @param id: the id of the nft being transferred. +;; @param owner: the principal of the current owner of the nft being transferred. +;; @param recipient: the principal of the recipient to whom the nft is being transferred. +(define-private (purchase-transfer (id uint) (owner principal) (recipient principal)) + (let + ( + ;; Attempts to retrieve the name and namespace associated with the given NFT ID. + (name-and-namespace (unwrap! (map-get? index-to-name id) ERR-NO-NAME)) + ;; Retrieves the properties of the name within the namespace. + (name-props (unwrap! (map-get? name-properties name-and-namespace) ERR-NO-NAME)) + ) + ;; Check owner and recipient is not the same + (asserts! (not (is-eq owner recipient)) ERR-OPERATION-UNAUTHORIZED) + (asserts! (is-eq owner (get owner name-props)) ERR-NOT-AUTHORIZED) + ;; Update primary name if needed for owner + (update-primary-name-owner id owner) + ;; Update primary name if needed for recipient + (update-primary-name-recipient id recipient) + ;; Updates the owner to the recipient. + (map-set name-properties name-and-namespace (merge name-props {owner: recipient})) + ;; Executes the NFT transfer from the current owner to the recipient. + (try! (nft-transfer? BNS-V2 id owner recipient)) + (print + { + topic: "transfer-name", + owner: recipient, + name: {name: (get name name-and-namespace), namespace: (get namespace name-and-namespace)}, + id: id, + properties: (map-get? name-properties {name: (get name name-and-namespace), namespace: (get namespace name-and-namespace)}) + } + ) + (ok true) + ) +) + +;; Private function to update the primary name of an address when transfering a name +;; If the id is = to the primary name then it means that a transfer is happening and we should delete it +(define-private (update-primary-name-owner (id uint) (owner principal)) + ;; Check if the owner is transferring the primary name + (if (is-eq (map-get? primary-name owner) (some id)) + ;; If it is, then delete the primary name map + (map-delete primary-name owner) + ;; If it is not, do nothing, keep the current primary name + false + ) +) + +;; Private function to update the primary name of an address when recieving +(define-private (update-primary-name-recipient (id uint) (recipient principal)) + ;; Check if recipient has a primary name + (match (map-get? primary-name recipient) + recipient-primary-name + ;; If recipient has a primary name do nothing + true + ;; If recipient doesn't have a primary name + (map-set primary-name recipient id) + ) +) + +(define-private (handle-existing-name + (name-props + { + registered-at: (optional uint), + imported-at: (optional uint), + hashed-salted-fqn-preorder: (optional (buff 20)), + preordered-by: (optional principal), + renewal-height: uint, + stx-burn: uint, + owner: principal + } + ) + (hashed-salted-fqn (buff 20)) + (contract-caller-preorder-height uint) + (stx-burned uint) (name (buff 48)) + (namespace (buff 20)) + (renewal uint) +) + (let + ( + ;; Retrieve the index of the existing name + (name-index (unwrap-panic (map-get? name-to-index {name: name, namespace: namespace}))) + ) + ;; Straight up check if the name was imported + (asserts! (is-none (get imported-at name-props)) ERR-IMPORTED-BEFORE) + ;; If the check passes then it is registered, we can straight up check the hashed-salted-fqn-preorder + (match (get hashed-salted-fqn-preorder name-props) + fqn + ;; Compare both preorder's height + (asserts! (> (unwrap-panic (get created-at (map-get? name-preorders {hashed-salted-fqn: fqn, buyer: (unwrap-panic (get preordered-by name-props))}))) contract-caller-preorder-height) ERR-PREORDERED-BEFORE) + ;; Compare registered with preorder height + (asserts! (> (unwrap-panic (get registered-at name-props)) contract-caller-preorder-height) ERR-FAST-MINTED-BEFORE) + ) + ;; Update the name properties with the new preorder information since it is the best preorder + (map-set name-properties {name: name, namespace: namespace} + (merge name-props + { + hashed-salted-fqn-preorder: (some hashed-salted-fqn), + preordered-by: (some contract-caller), + registered-at: (some burn-block-height), + renewal-height: (if (is-eq renewal u0) + u0 + (+ burn-block-height renewal) + ), + stx-burn: stx-burned + } + ) + ) + (try! (as-contract (stx-transfer? stx-burned .BNS-V2 (get owner name-props)))) + ;; Transfer ownership of the name to the new owner + (try! (purchase-transfer name-index (get owner name-props) contract-caller)) + ;; Log the name transfer event + (print + { + topic: "transfer-name", + owner: contract-caller, + name: {name: name, namespace: namespace}, + id: name-index, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Return the name index + (ok name-index) + ) +) + +(define-private (register-new-name (id-to-be-minted uint) (hashed-salted-fqn (buff 20)) (stx-burned uint) (name (buff 48)) (namespace (buff 20)) (lifetime uint)) + (begin + ;; Set the properties for the newly registered name + (map-set name-properties + {name: name, namespace: namespace} + { + registered-at: (some burn-block-height), + imported-at: none, + hashed-salted-fqn-preorder: (some hashed-salted-fqn), + preordered-by: (some contract-caller), + renewal-height: (if (is-eq lifetime u0) + u0 + (+ burn-block-height lifetime) + ), + stx-burn: stx-burned, + owner: contract-caller, + } + ) + ;; Update the index-to-name and name-to-index mappings + (map-set index-to-name id-to-be-minted {name: name, namespace: namespace}) + (map-set name-to-index {name: name, namespace: namespace} id-to-be-minted) + ;; Increment the BNS index + (var-set bns-index id-to-be-minted) + ;; Update the primary name for the new owner if necessary + (update-primary-name-recipient id-to-be-minted contract-caller) + ;; Mint a new NFT for the BNS name + (try! (nft-mint? BNS-V2 id-to-be-minted contract-caller)) + ;; Burn the STX paid for the name registration + (try! (as-contract (stx-burn? stx-burned .BNS-V2))) + ;; Log the new name registration event + (print + { + topic: "new-name", + owner: contract-caller, + name: {name: name, namespace: namespace}, + id: id-to-be-minted, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Return the ID of the newly minted name + (ok id-to-be-minted) + ) +) + +;; Migration Functions +(define-public (namespace-airdrop + (namespace (buff 20)) + (pricing {base: uint, buckets: (list 16 uint), coeff: uint, no-vowel-discount: uint, nonalpha-discount: uint}) + (lifetime uint) + (namespace-import principal) + (namespace-manager (optional principal)) + (can-update-price bool) + (manager-transfers bool) + (manager-frozen bool) + (revealed-at uint) + (launched-at uint) +) + (begin + ;; Check if migration is complete + (asserts! (not (var-get migration-complete)) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the contract-caller is the airdrop contract. + (asserts! (is-eq DEPLOYER tx-sender) ERR-OPERATION-UNAUTHORIZED) + ;; Ensure the namespace consists of valid characters only. + (asserts! (not (has-invalid-chars namespace)) ERR-CHARSET-INVALID) + ;; Check that the namespace is available for reveal. + (asserts! (unwrap! (can-namespace-be-registered namespace) ERR-NAMESPACE-ALREADY-EXISTS) ERR-NAMESPACE-ALREADY-EXISTS) + ;; Set all properties + (map-set namespaces namespace + { + namespace-manager: namespace-manager, + manager-transferable: manager-transfers, + manager-frozen: manager-frozen, + namespace-import: namespace-import, + revealed-at: revealed-at, + launched-at: (some launched-at), + lifetime: lifetime, + can-update-price-function: can-update-price, + price-function: pricing + } + ) + ;; Emit an event to indicate the namespace is now ready and launched. + (print { namespace: namespace, status: "launch", properties: (map-get? namespaces namespace)}) + ;; Confirm successful airdrop of the namespace + (ok namespace) + ) +) + +(define-public (name-airdrop + (name (buff 48)) + (namespace (buff 20)) + (registered-at uint) + (lifetime uint) + (owner principal) +) + (let + ( + (mint-index (+ u1 (var-get bns-index))) + ) + ;; Check if migration is complete + (asserts! (not (var-get migration-complete)) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the contract-caller is the airdrop contract. + (asserts! (is-eq DEPLOYER tx-sender) ERR-OPERATION-UNAUTHORIZED) + ;; Set all properties + (map-set name-to-index {name: name, namespace: namespace} mint-index) + (map-set index-to-name mint-index {name: name, namespace: namespace}) + (map-set name-properties {name: name, namespace: namespace} + { + registered-at: (some registered-at), + imported-at: none, + hashed-salted-fqn-preorder: none, + preordered-by: none, + renewal-height: (if (is-eq lifetime u0) u0 (+ burn-block-height lifetime)), + stx-burn: u0, + owner: owner, + } + ) + ;; Update the index + (var-set bns-index mint-index) + ;; Update the primary name of the recipient + (map-set primary-name owner mint-index) + ;; Mint the Name to the owner + (try! (nft-mint? BNS-V2 mint-index owner)) + (print + { + topic: "new-airdrop", + owner: owner, + name: {name: name, namespace: namespace}, + id: mint-index, + registered-at: registered-at, + } + ) + ;; Confirm successful airdrop of the namespace + (ok mint-index) + ) +) + +(define-public (flip-migration-complete) + (ok + (begin + (asserts! (is-eq contract-caller DEPLOYER) ERR-NOT-AUTHORIZED) + (var-set migration-complete true) + ) + ) +) + diff --git a/components/clarinet-format/tests/golden/flash-loan-user-margin-usda-wbtc.clar b/components/clarinet-format/tests/golden/flash-loan-user-margin-usda-wbtc.clar new file mode 100644 index 000000000..5f79ac822 --- /dev/null +++ b/components/clarinet-format/tests/golden/flash-loan-user-margin-usda-wbtc.clar @@ -0,0 +1,130 @@ +(impl-trait .trait-flash-loan-user.flash-loan-user-trait) +(use-trait ft-trait .trait-sip-010.sip-010-trait) + +(define-constant ONE_8 u100000000) +(define-constant ERR-EXPIRY-IS-NONE (err u2027)) +(define-constant ERR-INVALID-TOKEN (err u2026)) + +;; @desc execute +;; @params collateral +;; @params amount +;; @params memo ; expiry +;; @returns (response boolean) +(define-public (execute (collateral ) (amount uint) (memo (optional (buff 16)))) + (let + ( + ;; gross amount * ltv / price = amount + ;; gross amount = amount * price / ltv + ;; buff to uint conversion + (memo-uint (buff-to-uint (unwrap! memo ERR-EXPIRY-IS-NONE))) + (ltv (try! (contract-call? .collateral-rebalancing-pool-v1 get-ltv .token-wbtc .token-wusda memo-uint))) + (price (try! (contract-call? .yield-token-pool get-price memo-uint .yield-wbtc))) + (gross-amount (mul-up amount (div-down price ltv))) + (minted-yield-token (get yield-token (try! (contract-call? .collateral-rebalancing-pool-v1 add-to-position .token-wbtc .token-wusda memo-uint .yield-wbtc .key-wbtc-usda gross-amount)))) + (swapped-token (get dx (try! (contract-call? .yield-token-pool swap-y-for-x memo-uint .yield-wbtc .token-wbtc minted-yield-token none)))) + ) + (asserts! (is-eq .token-wusda (contract-of collateral)) ERR-INVALID-TOKEN) + ;; swap token to collateral so we can return flash-loan + (try! (contract-call? .fixed-weight-pool-v1-01 swap-helper .token-wbtc .token-wusda u50000000 u50000000 swapped-token none)) + (print { object: "flash-loan-user-margin-usda-wbtc", action: "execute", data: gross-amount }) + (ok true) + ) +) + +;; @desc mul-up +;; @params a +;; @params b +;; @returns uint +(define-private (mul-up (a uint) (b uint)) + (let + ( + (product (* a b)) + ) + (if (is-eq product u0) + u0 + (+ u1 (/ (- product u1) ONE_8)) + ) + ) +) + +;; @desc div-down +;; @params a +;; @params b +;; @returns uint +(define-private (div-down (a uint) (b uint)) + (if (is-eq a u0) + u0 + (/ (* a ONE_8) b) + ) +) + +;; @desc buff-to-uint +;; @params bytes +;; @returns uint +(define-private (buff-to-uint (bytes (buff 16))) + (let + ( + (reverse-bytes (reverse-buff bytes)) + ) + (+ + (match (element-at reverse-bytes u0) byte (byte-to-uint byte) u0) + (match (element-at reverse-bytes u1) byte (* (byte-to-uint byte) u256) u0) + (match (element-at reverse-bytes u2) byte (* (byte-to-uint byte) u65536) u0) + (match (element-at reverse-bytes u3) byte (* (byte-to-uint byte) u16777216) u0) + (match (element-at reverse-bytes u4) byte (* (byte-to-uint byte) u4294967296) u0) + (match (element-at reverse-bytes u5) byte (* (byte-to-uint byte) u1099511627776) u0) + (match (element-at reverse-bytes u6) byte (* (byte-to-uint byte) u281474976710656) u0) + (match (element-at reverse-bytes u7) byte (* (byte-to-uint byte) u72057594037927936) u0) + (match (element-at reverse-bytes u8) byte (* (byte-to-uint byte) u18446744073709551616) u0) + (match (element-at reverse-bytes u9) byte (* (byte-to-uint byte) u4722366482869645213696) u0) + (match (element-at reverse-bytes u10) byte (* (byte-to-uint byte) u1208925819614629174706176) u0) + (match (element-at reverse-bytes u11) byte (* (byte-to-uint byte) u309485009821345068724781056) u0) + (match (element-at reverse-bytes u12) byte (* (byte-to-uint byte) u79228162514264337593543950336) u0) + (match (element-at reverse-bytes u13) byte (* (byte-to-uint byte) u20282409603651670423947251286016) u0) + (match (element-at reverse-bytes u14) byte (* (byte-to-uint byte) u5192296858534827628530496329220096) u0) + (match (element-at reverse-bytes u15) byte (* (byte-to-uint byte) u1329227995784915872903807060280344576) u0) + ) + ) +) + +;; lookup table for converting 1-byte buffers to uints via index-of +(define-constant BUFF-TO-BYTE (list + 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f + 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 0x1c 0x1d 0x1e 0x1f + 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 0x28 0x29 0x2a 0x2b 0x2c 0x2d 0x2e 0x2f + 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x3a 0x3b 0x3c 0x3d 0x3e 0x3f + 0x40 0x41 0x42 0x43 0x44 0x45 0x46 0x47 0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f + 0x50 0x51 0x52 0x53 0x54 0x55 0x56 0x57 0x58 0x59 0x5a 0x5b 0x5c 0x5d 0x5e 0x5f + 0x60 0x61 0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e 0x6f + 0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79 0x7a 0x7b 0x7c 0x7d 0x7e 0x7f + 0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87 0x88 0x89 0x8a 0x8b 0x8c 0x8d 0x8e 0x8f + 0x90 0x91 0x92 0x93 0x94 0x95 0x96 0x97 0x98 0x99 0x9a 0x9b 0x9c 0x9d 0x9e 0x9f + 0xa0 0xa1 0xa2 0xa3 0xa4 0xa5 0xa6 0xa7 0xa8 0xa9 0xaa 0xab 0xac 0xad 0xae 0xaf + 0xb0 0xb1 0xb2 0xb3 0xb4 0xb5 0xb6 0xb7 0xb8 0xb9 0xba 0xbb 0xbc 0xbd 0xbe 0xbf + 0xc0 0xc1 0xc2 0xc3 0xc4 0xc5 0xc6 0xc7 0xc8 0xc9 0xca 0xcb 0xcc 0xcd 0xce 0xcf + 0xd0 0xd1 0xd2 0xd3 0xd4 0xd5 0xd6 0xd7 0xd8 0xd9 0xda 0xdb 0xdc 0xdd 0xde 0xdf + 0xe0 0xe1 0xe2 0xe3 0xe4 0xe5 0xe6 0xe7 0xe8 0xe9 0xea 0xeb 0xec 0xed 0xee 0xef + 0xf0 0xf1 0xf2 0xf3 0xf4 0xf5 0xf6 0xf7 0xf8 0xf9 0xfa 0xfb 0xfc 0xfd 0xfe 0xff +)) + +;; @desc byte-to-uint +;; @params byte +;; @returns uint +(define-read-only (byte-to-uint (byte (buff 1))) + (unwrap-panic (index-of BUFF-TO-BYTE byte)) +) + +;; @desc concat-buff +;; @params a +;; @params b +;; @returns buff +(define-private (concat-buff (a (buff 16)) (b (buff 16))) + (unwrap-panic (as-max-len? (concat a b) u16)) +) + +;; @desc reverse-buff +;; @params a +;; @returns buff +(define-read-only (reverse-buff (a (buff 16))) + (fold concat-buff a 0x) +) \ No newline at end of file diff --git a/components/clarinet-format/tests/golden/sbtc-deposit.clar b/components/clarinet-format/tests/golden/sbtc-deposit.clar new file mode 100644 index 000000000..5a4b543ca --- /dev/null +++ b/components/clarinet-format/tests/golden/sbtc-deposit.clar @@ -0,0 +1,108 @@ +;; sBTC Deposit contract + +;; constants + +;; The required length of a txid +(define-constant txid-length u32) +(define-constant dust-limit u546) + +;; error codes +;; TXID used in deposit is not the correct length +(define-constant ERR_TXID_LEN (err u300)) +;; Deposit has already been completed +(define-constant ERR_DEPOSIT_REPLAY (err u301)) +(define-constant ERR_LOWER_THAN_DUST (err u302)) +(define-constant ERR_DEPOSIT_INDEX_PREFIX (unwrap-err! ERR_DEPOSIT (err true))) +(define-constant ERR_DEPOSIT (err u303)) +(define-constant ERR_INVALID_CALLER (err u304)) +(define-constant ERR_INVALID_BURN_HASH (err u305)) + +;; data vars + +;; data maps + +;; public functions + +;; Accept a new deposit request +;; Note that this function can only be called by the current +;; bootstrap signer set address - it cannot be called by users directly. +;; This function handles the validation & minting of sBTC, it then calls +;; into the sbtc-registry contract to update the state of the protocol +(define-public (complete-deposit-wrapper (txid (buff 32)) + (vout-index uint) + (amount uint) + (recipient principal) + (burn-hash (buff 32)) + (burn-height uint) + (sweep-txid (buff 32))) + (let + ( + (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) + (replay-fetch (contract-call? .sbtc-registry get-completed-deposit txid vout-index)) + ) + + ;; Check that the caller is the current signer principal + (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) + + ;; Check that amount is greater than dust limit + (asserts! (>= amount dust-limit) ERR_LOWER_THAN_DUST) + + ;; Check that txid is the correct length + (asserts! (is-eq (len txid) txid-length) ERR_TXID_LEN) + + ;; Check that sweep txid is the correct length + (asserts! (is-eq (len sweep-txid) txid-length) ERR_TXID_LEN) + + ;; Assert that the deposit has not already been completed (no replay) + (asserts! (is-none replay-fetch) ERR_DEPOSIT_REPLAY) + + ;; Verify that Bitcoin hasn't forked by comparing the burn hash provided + (asserts! (is-eq (some burn-hash) (get-burn-header burn-height)) ERR_INVALID_BURN_HASH) + + ;; Mint the sBTC to the recipient + (try! (contract-call? .sbtc-token protocol-mint amount recipient)) + + ;; Complete the deposit + (ok (contract-call? .sbtc-registry complete-deposit txid vout-index amount recipient burn-hash burn-height sweep-txid)) + ) +) + +;; Return the bitcoin header hash of the bitcoin block at the given height. +(define-read-only (get-burn-header (height uint)) + (get-burn-block-info? header-hash height) +) + +;; Accept multiple new deposit requests +;; Note that this function can only be called by the current +;; bootstrap signer set address - it cannot be called by users directly. +;; +;; This function handles the validation & minting of sBTC by handling multiple (up to 1000) deposits at a time, +;; it then calls into the sbtc-registry contract to update the state of the protocol. +(define-public (complete-deposits-wrapper + (deposits (list 650 {txid: (buff 32), vout-index: uint, amount: uint, recipient: principal, burn-hash: (buff 32), burn-height: uint, sweep-txid: (buff 32)})) + ) + (begin + ;; Check that the caller is the current signer principal + (asserts! (is-eq + (contract-call? .sbtc-registry get-current-signer-principal) + tx-sender + ) ERR_INVALID_CALLER) + + (fold complete-individual-deposits-helper deposits (ok u0)) + ) +) + +;; private functions +;; #[allow(unchecked_data)] +(define-private (complete-individual-deposits-helper (deposit {txid: (buff 32), vout-index: uint, amount: uint, recipient: principal, burn-hash: (buff 32), burn-height: uint, sweep-txid: (buff 32)}) (helper-response (response uint uint))) + (match helper-response + index + (begin + (try! (unwrap! (complete-deposit-wrapper (get txid deposit) (get vout-index deposit) (get amount deposit) (get recipient deposit) (get burn-hash deposit) (get burn-height deposit) (get sweep-txid deposit)) (err (+ ERR_DEPOSIT_INDEX_PREFIX (+ u10 index))))) + (ok (+ index u1)) + ) + err-response + (err err-response) + ) +) + From ec6d9e72f8e55fe9535832fcb8c429720e0e8107 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Wed, 20 Nov 2024 21:16:31 -0800 Subject: [PATCH 09/17] add comment handling and some max line length logic --- .../clarinet-format/src/formatter/mod.rs | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index acaf33c73..1304ec518 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -88,8 +88,18 @@ pub fn format_source_exprs( } else { format!("({})\n", format_source_exprs(settings, list, acc)) }; + let pre_comments = format_comments(&expr.pre_comments, settings.max_line_length); + let post_comments = format_comments(&expr.post_comments, settings.max_line_length); + let end_line_comment = if let Some(comment) = &expr.end_line_comment { + print!("here"); + format!(" ;; {}", comment) + } else { + print!("there"); + String::new() + }; + print!("{}", formatted); return format!( - "{formatted}{}", + "{pre_comments}{formatted}{end_line_comment}{post_comments}{}", format_source_exprs(settings, remaining, acc) ) .trim() @@ -102,6 +112,48 @@ pub fn format_source_exprs( acc.to_owned() } +fn format_comments( + comments: &[(String, clarity::vm::representations::Span)], + max_line_length: usize, +) -> String { + if !comments.is_empty() { + let joined = comments + .iter() + .map(|(comment, span)| { + let mut formatted = String::new(); + let mut current_line = String::new(); + let indent = " ".repeat(span.start_column as usize - 1); + let max_content_length = max_line_length - span.start_column as usize - 3; + + for word in comment.split_whitespace() { + if current_line.len() + word.len() + 1 > max_content_length { + // push the current line and start a new one + formatted.push_str(&format!("{};; {}\n", indent, current_line.trim_end())); + current_line.clear(); + } + // add a space if the current line isn't empty + if !current_line.is_empty() { + current_line.push(' '); + } + current_line.push_str(word); + } + + // push the rest if it exists + if !current_line.is_empty() { + formatted.push_str(&format!("{};; {}", indent, current_line.trim_end())); + } + + formatted + }) + .collect::>() + .join("\n"); + + format!("{joined}\n") + } else { + "".to_string() + } +} + fn indentation_to_string(indentation: &Indentation) -> String { match indentation { Indentation::Space(i) => " ".repeat(*i), @@ -254,6 +306,35 @@ mod tests_formatter { ); } #[test] + fn test_comments_included() { + let src = ";; this is a comment\n(ok true)"; + + let result = format_with_default(&String::from(src)); + assert_eq!(src, result); + } + // #[test] + // fn test_end_of_line_comments_included() { + // let src = "(ok true) ;; this is a comment"; + + // let result = format_with_default(&String::from(src)); + // assert_eq!(src, result); + // } + // #[test] + // fn test_end_of_line_comments_max_line_length() { + // let src = "(ok true) ;; this is a comment"; + + // let result = format_with(&String::from(src), Settings::new(Indentation::Space(2), 9)); + // let expected = ";; this is a comment\n(ok true)"; + // assert_eq!(result, expected); + // } + #[test] + fn test_comments_only() { + let src = ";; this is a comment\n(ok true)"; + + let result = format_with_default(&String::from(src)); + assert_eq!(src, result); + } + #[test] fn test_begin_never_one_line() { let src = "(begin (ok true))"; let result = format_with_default(&String::from(src)); @@ -267,6 +348,13 @@ mod tests_formatter { assert_eq!(result, "(begin\n (ok true)\n)"); } + #[test] + fn test_max_line_length() { + let src = ";; a comment with line length 32\n(ok true)"; + let result = format_with(&String::from(src), Settings::new(Indentation::Space(2), 32)); + let expected = ";; a comment with line length\n;; 32\n(ok true)"; + assert_eq!(result, expected); + } // #[test] // fn test_irl_contracts() { // let golden_dir = "./tests/golden"; From a7d0adf329923eaa4ab3d5b3298d7c79aea103c0 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Thu, 21 Nov 2024 06:27:27 -0800 Subject: [PATCH 10/17] settings flags for max line and indentation --- components/clarinet-cli/src/frontend/cli.rs | 23 +++++++++++++++---- .../clarinet-format/src/formatter/mod.rs | 6 ++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/components/clarinet-cli/src/frontend/cli.rs b/components/clarinet-cli/src/frontend/cli.rs index d6997aee5..9b3ab7621 100644 --- a/components/clarinet-cli/src/frontend/cli.rs +++ b/components/clarinet-cli/src/frontend/cli.rs @@ -97,20 +97,25 @@ enum Command { LSP, /// Format clarity code files #[clap(name = "format", aliases = &["fmt"], bin_name = "format")] - Format(Format), + Formatter(Formatter), /// Step by step debugging and breakpoints from your code editor (VSCode, vim, emacs, etc) #[clap(name = "dap", bin_name = "dap")] DAP, } #[derive(Parser, PartialEq, Clone, Debug)] -struct Format { - /// Path to clarity files +struct Formatter { + /// Path to clarity files (defaults to ./contracts) #[clap(long = "path", short = 'p')] pub code_path: Option, /// If specified, format only this file #[clap(long = "file", short = 'f')] pub file: Option, + #[clap(long = "max-line-length", short = 'l')] + pub max_line_length: Option, + #[clap(long = "tabs", short = 't')] + /// indentation size, e.g. 2 + pub indentation: Option, #[clap(long = "dry-run")] pub dry_run: bool, } @@ -1196,9 +1201,17 @@ pub fn main() { process::exit(1); } }, - Command::Format(cmd) => { + Command::Formatter(cmd) => { let sources = get_source_with_path(cmd.code_path, cmd.file); - let settings = Settings::default(); + let mut settings = Settings::default(); + + if let Some(max_line_length) = cmd.max_line_length { + settings.max_line_length = max_line_length; + } + + if let Some(indentation) = cmd.indentation { + settings.indentation = clarinet_format::formatter::Indentation::Space(indentation); + } let mut formatter = ClarityFormatter::new(settings); for (file_path, source) in &sources { diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index 1304ec518..93af2759f 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -75,7 +75,8 @@ pub fn format_source_exprs( let formatted = if let Some( DefineFunctions::PublicFunction | DefineFunctions::ReadOnlyFunction - | DefineFunctions::PrivateFunction, + | DefineFunctions::PrivateFunction + | DefineFunctions::Map, ) = atom.and_then(|a| DefineFunctions::lookup_by_name(a)) { format_function(settings, list) @@ -91,13 +92,10 @@ pub fn format_source_exprs( let pre_comments = format_comments(&expr.pre_comments, settings.max_line_length); let post_comments = format_comments(&expr.post_comments, settings.max_line_length); let end_line_comment = if let Some(comment) = &expr.end_line_comment { - print!("here"); format!(" ;; {}", comment) } else { - print!("there"); String::new() }; - print!("{}", formatted); return format!( "{pre_comments}{formatted}{end_line_comment}{post_comments}{}", format_source_exprs(settings, remaining, acc) From ffc88c946c3b4aeb2dea594a7082735d7872c692 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Thu, 21 Nov 2024 07:03:10 -0800 Subject: [PATCH 11/17] remove max line length check for comments --- .../clarinet-format/src/formatter/mod.rs | 91 +- .../tests/golden-intended/BNS-V2.clar | 1879 +++++++++++++++++ .../flash-loan-user-margin-usda-wbtc.clar | 130 ++ .../tests/golden-intended/sbtc-deposit.clar | 108 + 4 files changed, 2149 insertions(+), 59 deletions(-) create mode 100644 components/clarinet-format/tests/golden-intended/BNS-V2.clar create mode 100644 components/clarinet-format/tests/golden-intended/flash-loan-user-margin-usda-wbtc.clar create mode 100644 components/clarinet-format/tests/golden-intended/sbtc-deposit.clar diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index 93af2759f..601ce16cc 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -87,17 +87,23 @@ pub fn format_source_exprs( } else if let Some(TupleCons) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) { format_tuple(settings, list) } else { - format!("({})\n", format_source_exprs(settings, list, acc)) + format!("({})", format_source_exprs(settings, list, acc)) }; - let pre_comments = format_comments(&expr.pre_comments, settings.max_line_length); - let post_comments = format_comments(&expr.post_comments, settings.max_line_length); + let pre_comments = format_comments(&expr.pre_comments); + let post_comments = format_comments(&expr.post_comments); + let end_line_comment = if let Some(comment) = &expr.end_line_comment { - format!(" ;; {}", comment) + format!(" ;; {}\n", comment) } else { String::new() }; + let post_comment_prefix = if end_line_comment.is_empty() { + "\n" + } else { + "" + }; return format!( - "{pre_comments}{formatted}{end_line_comment}{post_comments}{}", + "{pre_comments}{formatted}{end_line_comment}{post_comment_prefix}{post_comments}{}", format_source_exprs(settings, remaining, acc) ) .trim() @@ -110,43 +116,19 @@ pub fn format_source_exprs( acc.to_owned() } -fn format_comments( - comments: &[(String, clarity::vm::representations::Span)], - max_line_length: usize, -) -> String { +fn format_comments(comments: &[(String, clarity::vm::representations::Span)]) -> String { if !comments.is_empty() { - let joined = comments + comments .iter() .map(|(comment, span)| { - let mut formatted = String::new(); - let mut current_line = String::new(); - let indent = " ".repeat(span.start_column as usize - 1); - let max_content_length = max_line_length - span.start_column as usize - 3; - - for word in comment.split_whitespace() { - if current_line.len() + word.len() + 1 > max_content_length { - // push the current line and start a new one - formatted.push_str(&format!("{};; {}\n", indent, current_line.trim_end())); - current_line.clear(); - } - // add a space if the current line isn't empty - if !current_line.is_empty() { - current_line.push(' '); - } - current_line.push_str(word); - } - - // push the rest if it exists - if !current_line.is_empty() { - formatted.push_str(&format!("{};; {}", indent, current_line.trim_end())); - } - - formatted + format!( + "{};; {}\n", + " ".repeat(span.start_column as usize - 1), + comment + ) }) .collect::>() - .join("\n"); - - format!("{joined}\n") + .join("\n") } else { "".to_string() } @@ -304,19 +286,24 @@ mod tests_formatter { ); } #[test] - fn test_comments_included() { - let src = ";; this is a comment\n(ok true)"; + fn test_pre_postcomments_included() { + let src = ";; this is a pre comment\n(ok true)"; + + let result = format_with_default(&String::from(src)); + assert_eq!(src, result); + + let src = "(ok true)\n;; this is a post comment"; let result = format_with_default(&String::from(src)); assert_eq!(src, result); } - // #[test] - // fn test_end_of_line_comments_included() { - // let src = "(ok true) ;; this is a comment"; + #[test] + fn test_end_of_line_comments_included() { + let src = "(ok true) ;; this is a comment"; - // let result = format_with_default(&String::from(src)); - // assert_eq!(src, result); - // } + let result = format_with_default(&String::from(src)); + assert_eq!(src, result); + } // #[test] // fn test_end_of_line_comments_max_line_length() { // let src = "(ok true) ;; this is a comment"; @@ -326,13 +313,6 @@ mod tests_formatter { // assert_eq!(result, expected); // } #[test] - fn test_comments_only() { - let src = ";; this is a comment\n(ok true)"; - - let result = format_with_default(&String::from(src)); - assert_eq!(src, result); - } - #[test] fn test_begin_never_one_line() { let src = "(begin (ok true))"; let result = format_with_default(&String::from(src)); @@ -346,13 +326,6 @@ mod tests_formatter { assert_eq!(result, "(begin\n (ok true)\n)"); } - #[test] - fn test_max_line_length() { - let src = ";; a comment with line length 32\n(ok true)"; - let result = format_with(&String::from(src), Settings::new(Indentation::Space(2), 32)); - let expected = ";; a comment with line length\n;; 32\n(ok true)"; - assert_eq!(result, expected); - } // #[test] // fn test_irl_contracts() { // let golden_dir = "./tests/golden"; diff --git a/components/clarinet-format/tests/golden-intended/BNS-V2.clar b/components/clarinet-format/tests/golden-intended/BNS-V2.clar new file mode 100644 index 000000000..d9374a718 --- /dev/null +++ b/components/clarinet-format/tests/golden-intended/BNS-V2.clar @@ -0,0 +1,1879 @@ +;; title: BNS-V2 +;; version: V-2 +;; summary: Updated BNS contract, handles the creation of new namespaces and new names on each namespace + +;; traits +;; (new) Import SIP-09 NFT trait +(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) +;; (new) Import a custom commission trait for handling commissions for NFT marketplaces functions +(use-trait commission-trait .commission-trait.commission) + +;; token definition +;; (new) Define the non-fungible token (NFT) called BNS-V2 with unique identifiers as unsigned integers +(define-non-fungible-token BNS-V2 uint) +;; Time-to-live (TTL) constants for namespace preorders and name preorders, and the duration for name grace period. +;; The TTL for namespace and names preorders. (1 day) +(define-constant PREORDER-CLAIMABILITY-TTL u144) +;; The duration after revealing a namespace within which it must be launched. (1 year) +(define-constant NAMESPACE-LAUNCHABILITY-TTL u52595) +;; The grace period duration for name renewals post-expiration. (34 days) +(define-constant NAME-GRACE-PERIOD-DURATION u5000) +;; (new) The length of the hash should match this +(define-constant HASH160LEN u20) +;; Defines the price tiers for namespaces based on their lengths. +(define-constant NAMESPACE-PRICE-TIERS (list + u640000000000 + u64000000000 u64000000000 + u6400000000 u6400000000 u6400000000 u6400000000 + u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000 u640000000) +) + +;; Only authorized caller to flip the switch and update URI +(define-constant DEPLOYER tx-sender) + +;; (new) Var to store the token URI, allowing for metadata association with the NFT +(define-data-var token-uri (string-ascii 256) "ipfs://QmUQY1aZ799SPRaNBFqeCvvmZ4fTQfZvWHauRvHAukyQDB") + +(define-public (update-token-uri (new-token-uri (string-ascii 256))) + (ok + (begin + (asserts! (is-eq contract-caller DEPLOYER) ERR-NOT-AUTHORIZED) + (var-set token-uri new-token-uri) + ) + ) +) + +(define-data-var contract-uri (string-ascii 256) "ipfs://QmWKTZEMQNWngp23i7bgPzkineYC9LDvcxYkwNyVQVoH8y") + +(define-public (update-contract-uri (new-contract-uri (string-ascii 256))) + (ok + (begin + (asserts! (is-eq contract-caller DEPLOYER) ERR-NOT-AUTHORIZED) + (var-set token-uri new-contract-uri) + ) + ) +) + +;; errors +(define-constant ERR-UNWRAP (err u101)) +(define-constant ERR-NOT-AUTHORIZED (err u102)) +(define-constant ERR-NOT-LISTED (err u103)) +(define-constant ERR-WRONG-COMMISSION (err u104)) +(define-constant ERR-LISTED (err u105)) +(define-constant ERR-NO-NAME (err u106)) +(define-constant ERR-HASH-MALFORMED (err u107)) +(define-constant ERR-STX-BURNT-INSUFFICIENT (err u108)) +(define-constant ERR-PREORDER-NOT-FOUND (err u109)) +(define-constant ERR-CHARSET-INVALID (err u110)) +(define-constant ERR-NAMESPACE-ALREADY-EXISTS (err u111)) +(define-constant ERR-PREORDER-CLAIMABILITY-EXPIRED (err u112)) +(define-constant ERR-NAMESPACE-NOT-FOUND (err u113)) +(define-constant ERR-OPERATION-UNAUTHORIZED (err u114)) +(define-constant ERR-NAMESPACE-ALREADY-LAUNCHED (err u115)) +(define-constant ERR-NAMESPACE-PREORDER-LAUNCHABILITY-EXPIRED (err u116)) +(define-constant ERR-NAMESPACE-NOT-LAUNCHED (err u117)) +(define-constant ERR-NAME-NOT-AVAILABLE (err u118)) +(define-constant ERR-NAMESPACE-BLANK (err u119)) +(define-constant ERR-NAME-BLANK (err u120)) +(define-constant ERR-NAME-PREORDERED-BEFORE-NAMESPACE-LAUNCH (err u121)) +(define-constant ERR-NAMESPACE-HAS-MANAGER (err u122)) +(define-constant ERR-OVERFLOW (err u123)) +(define-constant ERR-NO-NAMESPACE-MANAGER (err u124)) +(define-constant ERR-FAST-MINTED-BEFORE (err u125)) +(define-constant ERR-PREORDERED-BEFORE (err u126)) +(define-constant ERR-NAME-NOT-CLAIMABLE-YET (err u127)) +(define-constant ERR-IMPORTED-BEFORE (err u128)) +(define-constant ERR-LIFETIME-EQUAL-0 (err u129)) +(define-constant ERR-MIGRATION-IN-PROGRESS (err u130)) +(define-constant ERR-NO-PRIMARY-NAME (err u131)) + +;; variables +;; (new) Variable to see if migration is complete +(define-data-var migration-complete bool false) + +;; (new) Counter to keep track of the last minted NFT ID, ensuring unique identifiers +(define-data-var bns-index uint u0) + +;; maps +;; (new) Map to track market listings, associating NFT IDs with price and commission details +(define-map market uint {price: uint, commission: principal}) + +;; (new) Define a map to link NFT IDs to their respective names and namespaces. +(define-map index-to-name uint + { + name: (buff 48), namespace: (buff 20) + } +) +;; (new) Define a map to link names and namespaces to their respective NFT IDs. +(define-map name-to-index + { + name: (buff 48), namespace: (buff 20) + } + uint +) + +;; (updated) Contains detailed properties of names, including registration and importation times +(define-map name-properties + { name: (buff 48), namespace: (buff 20) } + { + registered-at: (optional uint), + imported-at: (optional uint), + ;; The fqn used to make the earliest preorder at any given point + hashed-salted-fqn-preorder: (optional (buff 20)), + ;; Added this field in name-properties to know exactly who has the earliest preorder at any given point + preordered-by: (optional principal), + renewal-height: uint, + stx-burn: uint, + owner: principal, + } +) + +;; (update) Stores properties of namespaces, including their import principals, reveal and launch times, and pricing functions. +(define-map namespaces (buff 20) + { + namespace-manager: (optional principal), + manager-transferable: bool, + manager-frozen: bool, + namespace-import: principal, + revealed-at: uint, + launched-at: (optional uint), + lifetime: uint, + can-update-price-function: bool, + price-function: + { + buckets: (list 16 uint), + base: uint, + coeff: uint, + nonalpha-discount: uint, + no-vowel-discount: uint + } + } +) + +;; Records namespace preorder transactions with their creation times, and STX burned. +(define-map namespace-preorders + { hashed-salted-namespace: (buff 20), buyer: principal } + { created-at: uint, stx-burned: uint, claimed: bool} +) + +;; Tracks preorders, to avoid attacks +(define-map namespace-single-preorder (buff 20) bool) + +;; Tracks preorders, to avoid attacks +(define-map name-single-preorder (buff 20) bool) + +;; Tracks preorders for names, including their creation times, and STX burned. +(define-map name-preorders + { hashed-salted-fqn: (buff 20), buyer: principal } + { created-at: uint, stx-burned: uint, claimed: bool} +) + +;; It maps a user's principal to the ID of their primary name. +(define-map primary-name principal uint) + +;; read-only +;; @desc (new) SIP-09 compliant function to get the last minted token's ID +(define-read-only (get-last-token-id) + ;; Returns the current value of bns-index variable, which tracks the last token ID + (ok (var-get bns-index)) +) + +(define-read-only (get-renewal-height (id uint)) + (let + ( + (name-namespace (unwrap! (get-bns-from-id id) ERR-NO-NAME)) + (namespace-props (unwrap! (map-get? namespaces (get namespace name-namespace)) ERR-NAMESPACE-NOT-FOUND)) + (name-props (unwrap! (map-get? name-properties name-namespace) ERR-NO-NAME)) + (renewal-height (get renewal-height name-props)) + (namespace-lifetime (get lifetime namespace-props)) + ) + ;; Check if the namespace requires renewals + (asserts! (not (is-eq namespace-lifetime u0)) ERR-LIFETIME-EQUAL-0) + ;; If the check passes then check the renewal-height of the name + (ok + (if (is-eq renewal-height u0) + ;; If it is true then it means it was imported so return the namespace launch blockheight + lifetime + (+ (unwrap! (get launched-at namespace-props) ERR-NAMESPACE-NOT-LAUNCHED) namespace-lifetime) + renewal-height + ) + ) + ) +) + +(define-read-only (can-resolve-name (namespace (buff 20)) (name (buff 48))) + (let + ( + (name-id (unwrap! (get-id-from-bns name namespace) ERR-NO-NAME)) + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (name-props (unwrap! (map-get? name-properties {name: name, namespace: namespace}) ERR-NO-NAME)) + (renewal-height (get renewal-height name-props)) + (namespace-lifetime (get lifetime namespace-props)) + ) + ;; Check if the name can resolve + (ok + (if (is-eq u0 namespace-lifetime) + ;; If true it means that the name is in a managed namespace or the namespace does not require renewals + {renewal: u0, owner: (get owner name-props)} + ;; If false then calculate renewal-height + {renewal: (try! (get-renewal-height name-id)), owner: (get owner name-props)} + ) + ) + ) +) + +;; @desc (new) SIP-09 compliant function to get token URI +(define-read-only (get-token-uri (id uint)) + ;; Returns a predefined set URI for the token metadata + (ok (some (var-get token-uri))) +) + +(define-read-only (get-contract-uri) + ;; Returns a predefined set URI for the contract metadata + (ok (some (var-get contract-uri))) +) + +;; @desc (new) SIP-09 compliant function to get the owner of a specific token by its ID +(define-read-only (get-owner (id uint)) + ;; Check and return the owner of the specified NFT + (ok (nft-get-owner? BNS-V2 id)) +) + +;; @desc (new) New get owner function +(define-read-only (get-owner-name (name (buff 48)) (namespace (buff 20))) + ;; Check and return the owner of the specified NFT + (ok (nft-get-owner? BNS-V2 (unwrap! (get-id-from-bns name namespace) ERR-NO-NAME))) +) + +;; Read-only function `get-namespace-price` calculates the registration price for a namespace based on its length. +;; @params: + ;; namespace (buff 20): The namespace for which the price is being calculated. +(define-read-only (get-namespace-price (namespace (buff 20))) + (let + ( + ;; Calculate the length of the namespace. + (namespace-len (len namespace)) + ) + ;; Ensure the namespace is not blank, its length is greater than 0. + (asserts! (> namespace-len u0) ERR-NAMESPACE-BLANK) + ;; Retrieve the price for the namespace based on its length from the NAMESPACE-PRICE-TIERS list. + ;; The price tier is determined by the minimum of 7 or the namespace length minus one. + (ok (unwrap! (element-at? NAMESPACE-PRICE-TIERS (min u7 (- namespace-len u1))) ERR-UNWRAP)) + ) +) + +;; Read-only function `get-name-price` calculates the registration price for a name based on the price buckets of the namespace +;; @params: + ;; namespace (buff 20): The namespace for which the price is being calculated. + ;; name (buff 48): The name for which the price is being calculated. +(define-read-only (get-name-price (namespace (buff 20)) (name (buff 48))) + (let + ( + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + (ok (compute-name-price name (get price-function namespace-props))) + ) +) + +;; Read-only function `can-namespace-be-registered` checks if a namespace is available for registration. +;; @params: + ;; namespace (buff 20): The namespace being checked for availability. +(define-read-only (can-namespace-be-registered (namespace (buff 20))) + ;; Returns the result of `is-namespace-available` directly, indicating if the namespace can be registered. + (ok (is-namespace-available namespace)) +) + +;; Read-only function `get-namespace-properties` for retrieving properties of a specific namespace. +;; @params: + ;; namespace (buff 20): The namespace whose properties are being queried. +(define-read-only (get-namespace-properties (namespace (buff 20))) + (let + ( + ;; Fetch the properties of the specified namespace from the `namespaces` map. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + ;; Returns the namespace along with its associated properties. + (ok { namespace: namespace, properties: namespace-props }) + ) +) + +;; Read only function to get name properties +(define-read-only (get-bns-info (name (buff 48)) (namespace (buff 20))) + (map-get? name-properties {name: name, namespace: namespace}) +) + +;; (new) Defines a read-only function to fetch the unique ID of a BNS name given its name and the namespace it belongs to. +(define-read-only (get-id-from-bns (name (buff 48)) (namespace (buff 20))) + ;; Attempts to retrieve the ID from the 'name-to-index' map using the provided name and namespace as the key. + (map-get? name-to-index {name: name, namespace: namespace}) +) + +;; (new) Defines a read-only function to fetch the BNS name and the namespace given a unique ID. +(define-read-only (get-bns-from-id (id uint)) + ;; Attempts to retrieve the name and namespace from the 'index-to-name' map using the provided id as the key. + (map-get? index-to-name id) +) + +;; (new) Fetcher for primary name +(define-read-only (get-primary-name (owner principal)) + (map-get? primary-name owner) +) + +;; (new) Fetcher for primary name returns name and namespace +(define-read-only (get-primary (owner principal)) + (ok (get-bns-from-id (unwrap! (map-get? primary-name owner) ERR-NO-PRIMARY-NAME))) +) + +;; public functions +;; @desc (new) SIP-09 compliant function to transfer a token from one owner to another. +;; @param id: ID of the NFT being transferred. +;; @param owner: Principal of the current owner of the NFT. +;; @param recipient: Principal of the recipient of the NFT. +(define-public (transfer (id uint) (owner principal) (recipient principal)) + (let + ( + ;; Get the name and namespace of the NFT. + (name-and-namespace (unwrap! (get-bns-from-id id) ERR-NO-NAME)) + (namespace (get namespace name-and-namespace)) + (name (get name name-and-namespace)) + ;; Get namespace properties and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (manager-transfers (get manager-transferable namespace-props)) + ;; Get name properties and owner. + (name-props (unwrap! (map-get? name-properties name-and-namespace) ERR-NO-NAME)) + (registered-at-value (get registered-at name-props)) + (nft-current-owner (unwrap! (nft-get-owner? BNS-V2 id) ERR-NO-NAME)) + ) + ;; First check if the name was registered + (match registered-at-value + is-registered + ;; If it was registered, check if registered-at is lower than current blockheight + ;; This check works to make sure that if a name is fast-claimed they have to wait 1 block to transfer it + (asserts! (< is-registered burn-block-height) ERR-OPERATION-UNAUTHORIZED) + ;; If it is not registered then continue + true + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Check that the namespace is launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Check owner and recipient is not the same + (asserts! (not (is-eq nft-current-owner recipient)) ERR-OPERATION-UNAUTHORIZED) + ;; We only need to check if manager transfers are true or false, if true then they have to do transfers through the manager contract that calls into mng-transfer, if false then they can call into this function + (asserts! (not manager-transfers) ERR-NOT-AUTHORIZED) + ;; Check contract-caller + (asserts! (is-eq contract-caller nft-current-owner) ERR-NOT-AUTHORIZED) + ;; Check if in fact the owner is-eq to nft-current-owner + (asserts! (is-eq owner nft-current-owner) ERR-NOT-AUTHORIZED) + ;; Ensures the NFT is not currently listed in the market. + (asserts! (is-none (map-get? market id)) ERR-LISTED) + ;; Update the name properties with the new owner + (map-set name-properties name-and-namespace (merge name-props {owner: recipient})) + ;; Update primary name if needed for owner + (update-primary-name-owner id owner) + ;; Update primary name if needed for recipient + (update-primary-name-recipient id recipient) + ;; Execute the NFT transfer. + (try! (nft-transfer? BNS-V2 id nft-current-owner recipient)) + (print + { + topic: "transfer-name", + owner: recipient, + name: {name: name, namespace: namespace}, + id: id, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + (ok true) + ) +) + +;; @desc (new) manager function to be called by managed namespaces that allows manager transfers. +;; @param id: ID of the NFT being transferred. +;; @param owner: Principal of the current owner of the NFT. +;; @param recipient: Principal of the recipient of the NFT. +(define-public (mng-transfer (id uint) (owner principal) (recipient principal)) + (let + ( + ;; Get the name and namespace of the NFT. + (name-and-namespace (unwrap! (get-bns-from-id id) ERR-NO-NAME)) + (namespace (get namespace name-and-namespace)) + (name (get name name-and-namespace)) + ;; Get namespace properties and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (manager-transfers (get manager-transferable namespace-props)) + (manager (get namespace-manager namespace-props)) + ;; Get name properties and owner. + (name-props (unwrap! (map-get? name-properties name-and-namespace) ERR-NO-NAME)) + (registered-at-value (get registered-at name-props)) + (nft-current-owner (unwrap! (nft-get-owner? BNS-V2 id) ERR-NO-NAME)) + ) + ;; First check if the name was registered + (match registered-at-value + is-registered + ;; If it was registered, check if registered-at is lower than current blockheight + ;; This check works to make sure that if a name is fast-claimed they have to wait 1 block to transfer it + (asserts! (< is-registered burn-block-height) ERR-OPERATION-UNAUTHORIZED) + ;; If it is not registered then continue + true + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Check that the namespace is launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Check owner and recipient is not the same + (asserts! (not (is-eq nft-current-owner recipient)) ERR-OPERATION-UNAUTHORIZED) + ;; We only need to check if manager transfers are true or false, if true then continue, if false then they can call into `transfer` function + (asserts! manager-transfers ERR-NOT-AUTHORIZED) + ;; Check contract-caller, we unwrap-panic because if manager-transfers is true then there has to be a manager + (asserts! (is-eq contract-caller (unwrap-panic manager)) ERR-NOT-AUTHORIZED) + ;; Check if in fact the owner is-eq to nft-current-owner + (asserts! (is-eq owner nft-current-owner) ERR-NOT-AUTHORIZED) + ;; Ensures the NFT is not currently listed in the market. + (asserts! (is-none (map-get? market id)) ERR-LISTED) + ;; Update primary name if needed for owner + (update-primary-name-owner id owner) + ;; Update primary name if needed for recipient + (update-primary-name-recipient id recipient) + ;; Update the name properties with the new owner + (map-set name-properties name-and-namespace (merge name-props {owner: recipient})) + ;; Execute the NFT transfer. + (try! (nft-transfer? BNS-V2 id nft-current-owner recipient)) + (print + { + topic: "transfer-name", + owner: recipient, + name: {name: name, namespace: namespace}, + id: id, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + (ok true) + ) +) + +;; @desc (new) Function to list an NFT for sale. +;; @param id: ID of the NFT being listed. +;; @param price: Listing price. +;; @param comm-trait: Address of the commission-trait. +(define-public (list-in-ustx (id uint) (price uint) (comm-trait )) + (let + ( + ;; Get the name and namespace of the NFT. + (name-and-namespace (unwrap! (map-get? index-to-name id) ERR-NO-NAME)) + (namespace (get namespace name-and-namespace)) + ;; Get namespace properties and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (namespace-manager (get namespace-manager namespace-props)) + ;; Get name properties and registered-at value. + (name-props (unwrap! (map-get? name-properties name-and-namespace) ERR-NO-NAME)) + (registered-at-value (get registered-at name-props)) + ;; Creates a listing record with price and commission details + (listing {price: price, commission: (contract-of comm-trait)}) + ) + ;; Checks if the name was registered + (match registered-at-value + is-registered + ;; If it was registered, check if registered-at is lower than current blockheight + ;; Same as transfers, this check works to make sure that if a name is fast-claimed they have to wait 1 block to list it + (asserts! (< is-registered burn-block-height) ERR-OPERATION-UNAUTHORIZED) + ;; If it is not registered then continue + true + ) + ;; Check if there is a namespace manager + (match namespace-manager + manager + ;; If there is then check that the contract-caller is the manager + (asserts! (is-eq manager contract-caller) ERR-NOT-AUTHORIZED) + ;; If there isn't assert that the owner is the contract-caller + (asserts! (is-eq (some contract-caller) (nft-get-owner? BNS-V2 id)) ERR-NOT-AUTHORIZED) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Updates the market map with the new listing details + (map-set market id listing) + ;; Prints listing details + (ok (print (merge listing {a: "list-in-ustx", id: id}))) + ) +) + +;; @desc (new) Function to remove an NFT listing from the market. +;; @param id: ID of the NFT being unlisted. +(define-public (unlist-in-ustx (id uint)) + (let + ( + ;; Get the name and namespace of the NFT. + (name-and-namespace (unwrap! (map-get? index-to-name id) ERR-NO-NAME)) + (namespace (get namespace name-and-namespace)) + ;; Verify if the NFT is listed in the market. + (market-map (unwrap! (map-get? market id) ERR-NOT-LISTED)) + ;; Get namespace properties and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (namespace-manager (get namespace-manager namespace-props)) + ) + ;; Check if there is a namespace manager + (match namespace-manager + manager + ;; If there is then check that the contract-caller is the manager + (asserts! (is-eq manager contract-caller) ERR-NOT-AUTHORIZED) + ;; If there isn't assert that the owner is the contract-caller + (asserts! (is-eq (some contract-caller) (nft-get-owner? BNS-V2 id)) ERR-NOT-AUTHORIZED) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Deletes the listing from the market map + (map-delete market id) + ;; Prints unlisting details + (ok (print {a: "unlist-in-ustx", id: id})) + ) +) + +;; @desc (new) Function to buy an NFT listed for sale, transferring ownership and handling commission. +;; @param id: ID of the NFT being purchased. +;; @param comm-trait: Address of the commission-trait. +(define-public (buy-in-ustx (id uint) (comm-trait )) + (let + ( + ;; Retrieves current owner and listing details + (owner (unwrap! (nft-get-owner? BNS-V2 id) ERR-NO-NAME)) + (listing (unwrap! (map-get? market id) ERR-NOT-LISTED)) + (price (get price listing)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Verifies the commission details match the listing + (asserts! (is-eq (contract-of comm-trait) (get commission listing)) ERR-WRONG-COMMISSION) + ;; Transfers STX from buyer to seller + (try! (stx-transfer? price contract-caller owner)) + ;; Handle commission payment + (try! (contract-call? comm-trait pay id price)) + ;; Transfers the NFT to the buyer + ;; This function differs from the `transfer` method by not checking who the contract-caller is, otherwise trasnfers would never be executed + (try! (purchase-transfer id owner contract-caller)) + ;; Removes the listing from the market map + (map-delete market id) + ;; Prints purchase details + (ok (print {a: "buy-in-ustx", id: id})) + ) +) + +;; @desc (new) Sets the primary name for the caller to a specific BNS name they own. +;; @param primary-name-id: ID of the name to be set as primary. +(define-public (set-primary-name (primary-name-id uint)) + (begin + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Verify the contract-caller is the owner of the name. + (asserts! (is-eq (unwrap! (nft-get-owner? BNS-V2 primary-name-id) ERR-NO-NAME) contract-caller) ERR-NOT-AUTHORIZED) + ;; Update the contract-caller's primary name. + (map-set primary-name contract-caller primary-name-id) + ;; Return true upon successful execution. + (ok true) + ) +) + +;; @desc (new) Defines a public function to burn an NFT, under managed namespaces. +;; @param id: ID of the NFT to be burned. +(define-public (mng-burn (id uint)) + (let + ( + ;; Get the name details associated with the given ID. + (name-and-namespace (unwrap! (get-bns-from-id id) ERR-NO-NAME)) + ;; Get the owner of the name. + (owner (unwrap! (nft-get-owner? BNS-V2 id) ERR-UNWRAP)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the caller is the current namespace manager. + (asserts! (is-eq contract-caller (unwrap! (get namespace-manager (unwrap! (map-get? namespaces (get namespace name-and-namespace)) ERR-NAMESPACE-NOT-FOUND)) ERR-NO-NAMESPACE-MANAGER)) ERR-NOT-AUTHORIZED) + ;; Unlist the NFT if it is listed. + (match (map-get? market id) + listed-name + (map-delete market id) + true + ) + ;; Update primary name if needed for the owner of the name + (update-primary-name-owner id owner) + ;; Delete the name from all maps: + ;; Remove the name-to-index. + (map-delete name-to-index name-and-namespace) + ;; Remove the index-to-name. + (map-delete index-to-name id) + ;; Remove the name-properties. + (map-delete name-properties name-and-namespace) + ;; Executes the burn operation for the specified NFT. + (try! (nft-burn? BNS-V2 id (unwrap! (nft-get-owner? BNS-V2 id) ERR-UNWRAP))) + (print + { + topic: "burn-name", + owner: "", + name: {name: (get name name-and-namespace), namespace: (get namespace name-and-namespace)}, + id: id + } + ) + (ok true) + ) +) + +;; @desc (new) Transfers the management role of a specific namespace to a new principal. +;; @param new-manager: Principal of the new manager. +;; @param namespace: Buffer of the namespace. +(define-public (mng-manager-transfer (new-manager (optional principal)) (namespace (buff 20))) + (let + ( + ;; Retrieve namespace properties and current manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the caller is the current namespace manager. + (asserts! (is-eq contract-caller (unwrap! (get namespace-manager namespace-props) ERR-NO-NAMESPACE-MANAGER)) ERR-NOT-AUTHORIZED) + ;; Ensure manager can be changed + (asserts! (not (get manager-frozen namespace-props)) ERR-NOT-AUTHORIZED) + ;; Update the namespace manager to the new manager. + (map-set namespaces namespace + (merge + namespace-props + {namespace-manager: new-manager} + ) + ) + (print { namespace: namespace, status: "transfer-manager", properties: (map-get? namespaces namespace) }) + (ok true) + ) +) + +;; @desc (new) freezes the ability to make manager transfers +;; @param namespace: Buffer of the namespace. +(define-public (freeze-manager (namespace (buff 20))) + (let + ( + ;; Retrieve namespace properties and current manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the caller is the current namespace manager. + (asserts! (is-eq contract-caller (unwrap! (get namespace-manager namespace-props) ERR-NO-NAMESPACE-MANAGER)) ERR-NOT-AUTHORIZED) + ;; Update the namespace manager to the new manager. + (map-set namespaces namespace + (merge + namespace-props + {manager-frozen: true} + ) + ) + (print { namespace: namespace, status: "freeze-manager", properties: (map-get? namespaces namespace) }) + (ok true) + ) +) + +;;;; NAMESPACES +;; @desc Public function `namespace-preorder` initiates the registration process for a namespace by sending a transaction with a salted hash of the namespace. +;; This transaction burns the registration fee as a commitment. +;; @params: hashed-salted-namespace (buff 20): The hashed and salted namespace being preordered. +;; @params: stx-to-burn (uint): The amount of STX tokens to be burned as part of the preorder process. +(define-public (namespace-preorder (hashed-salted-namespace (buff 20)) (stx-to-burn uint)) + (begin + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Validate that the hashed-salted-namespace is exactly 20 bytes long. + (asserts! (is-eq (len hashed-salted-namespace) HASH160LEN) ERR-HASH-MALFORMED) + ;; Check if the same hashed-salted-fqn has been used before + (asserts! (is-none (map-get? namespace-single-preorder hashed-salted-namespace)) ERR-PREORDERED-BEFORE) + ;; Confirm that the STX amount to be burned is positive + (asserts! (> stx-to-burn u0) ERR-STX-BURNT-INSUFFICIENT) + ;; Execute the token burn operation. + (try! (stx-burn? stx-to-burn contract-caller)) + ;; Record the preorder details in the `namespace-preorders` map + (map-set namespace-preorders + { hashed-salted-namespace: hashed-salted-namespace, buyer: contract-caller } + { created-at: burn-block-height, stx-burned: stx-to-burn, claimed: false } + ) + ;; Sets the map with just the hashed-salted-namespace as the key + (map-set namespace-single-preorder hashed-salted-namespace true) + ;; Return the block height at which the preorder claimability expires. + (ok (+ burn-block-height PREORDER-CLAIMABILITY-TTL)) + ) +) + +;; @desc Public function `namespace-reveal` completes the second step in the namespace registration process. +;; It associates the revealed namespace with its corresponding preorder, establishes the namespace's pricing function, and sets its lifetime and ownership details. +;; @param: namespace (buff 20): The namespace being revealed. +;; @param: namespace-salt (buff 20): The salt used during the preorder to generate a unique hash. +;; @param: p-func-base, p-func-coeff, p-func-b1 to p-func-b16: Parameters defining the price function for registering names within this namespace. +;; @param: p-func-non-alpha-discount (uint): Discount applied to names with non-alphabetic characters. +;; @param: p-func-no-vowel-discount (uint): Discount applied to names without vowels. +;; @param: lifetime (uint): Duration that names within this namespace are valid before needing renewal. +;; @param: namespace-import (principal): The principal authorized to import names into this namespace. +;; @param: namespace-manager (optional principal): The principal authorized to manage the namespace. +(define-public (namespace-reveal + (namespace (buff 20)) + (namespace-salt (buff 20)) + (p-func-base uint) + (p-func-coeff uint) + (p-func-b1 uint) + (p-func-b2 uint) + (p-func-b3 uint) + (p-func-b4 uint) + (p-func-b5 uint) + (p-func-b6 uint) + (p-func-b7 uint) + (p-func-b8 uint) + (p-func-b9 uint) + (p-func-b10 uint) + (p-func-b11 uint) + (p-func-b12 uint) + (p-func-b13 uint) + (p-func-b14 uint) + (p-func-b15 uint) + (p-func-b16 uint) + (p-func-non-alpha-discount uint) + (p-func-no-vowel-discount uint) + (lifetime uint) + (namespace-import principal) + (namespace-manager (optional principal)) + (can-update-price bool) + (manager-transfers bool) + (manager-frozen bool) +) + (let + ( + ;; Generate the hashed, salted namespace identifier to match with its preorder. + (hashed-salted-namespace (hash160 (concat (concat namespace 0x2e) namespace-salt))) + ;; Define the price function based on the provided parameters. + (price-function + { + buckets: (list p-func-b1 p-func-b2 p-func-b3 p-func-b4 p-func-b5 p-func-b6 p-func-b7 p-func-b8 p-func-b9 p-func-b10 p-func-b11 p-func-b12 p-func-b13 p-func-b14 p-func-b15 p-func-b16), + base: p-func-base, + coeff: p-func-coeff, + nonalpha-discount: p-func-non-alpha-discount, + no-vowel-discount: p-func-no-vowel-discount + } + ) + ;; Retrieve the preorder record to ensure it exists and is valid for the revealing namespace + (preorder (unwrap! (map-get? namespace-preorders { hashed-salted-namespace: hashed-salted-namespace, buyer: contract-caller}) ERR-PREORDER-NOT-FOUND)) + ;; Calculate the namespace's registration price for validation. + (namespace-price (try! (get-namespace-price namespace))) + ) + ;; Ensure the preorder has not been claimed before + (asserts! (not (get claimed preorder)) ERR-NAMESPACE-ALREADY-EXISTS) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the namespace consists of valid characters only. + (asserts! (not (has-invalid-chars namespace)) ERR-CHARSET-INVALID) + ;; Check that the namespace is available for reveal. + (asserts! (unwrap! (can-namespace-be-registered namespace) ERR-NAMESPACE-ALREADY-EXISTS) ERR-NAMESPACE-ALREADY-EXISTS) + ;; Verify the burned amount during preorder meets or exceeds the namespace's registration price. + (asserts! (>= (get stx-burned preorder) namespace-price) ERR-STX-BURNT-INSUFFICIENT) + ;; Confirm the reveal action is performed within the allowed timeframe from the preorder. + (asserts! (< burn-block-height (+ (get created-at preorder) PREORDER-CLAIMABILITY-TTL)) ERR-PREORDER-CLAIMABILITY-EXPIRED) + ;; Ensure at least 1 block has passed after the preorder to avoid namespace sniping. + (asserts! (>= burn-block-height (+ (get created-at preorder) u1)) ERR-OPERATION-UNAUTHORIZED) + ;; Check if the namespace manager is assigned + (match namespace-manager + namespace-m + ;; If namespace-manager is assigned, then assign everything except the lifetime, that is set to u0 sinces renewals will be made in the namespace manager contract and set the can update price function to false, since no changes will ever need to be made there. + (map-set namespaces namespace + { + namespace-manager: namespace-manager, + manager-transferable: manager-transfers, + manager-frozen: manager-frozen, + namespace-import: namespace-import, + revealed-at: burn-block-height, + launched-at: none, + lifetime: u0, + can-update-price-function: can-update-price, + price-function: price-function + } + ) + ;; If no manager is assigned + (map-set namespaces namespace + { + namespace-manager: none, + manager-transferable: manager-transfers, + manager-frozen: manager-frozen, + namespace-import: namespace-import, + revealed-at: burn-block-height, + launched-at: none, + lifetime: lifetime, + can-update-price-function: can-update-price, + price-function: price-function + } + ) + ) + ;; Update the claimed value for the preorder + (map-set namespace-preorders { hashed-salted-namespace: hashed-salted-namespace, buyer: contract-caller } + (merge preorder + { + claimed: true + } + ) + ) + ;; Confirm successful reveal of the namespace + (ok true) + ) +) + +;; @desc Public function `namespace-launch` marks a namespace as launched and available for public name registrations. +;; @param: namespace (buff 20): The namespace to be launched and made available for public registrations. +(define-public (namespace-launch (namespace (buff 20))) + (let + ( + ;; Retrieve the properties of the namespace to ensure it exists and to check its current state. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the transaction sender is the namespace's designated import principal. + (asserts! (is-eq (get namespace-import namespace-props) contract-caller) ERR-OPERATION-UNAUTHORIZED) + ;; Verify the namespace has not already been launched. + (asserts! (is-none (get launched-at namespace-props)) ERR-NAMESPACE-ALREADY-LAUNCHED) + ;; Confirm that the action is taken within the permissible time frame since the namespace was revealed. + (asserts! (< burn-block-height (+ (get revealed-at namespace-props) NAMESPACE-LAUNCHABILITY-TTL)) ERR-NAMESPACE-PREORDER-LAUNCHABILITY-EXPIRED) + ;; Update the `namespaces` map with the newly launched status. + (map-set namespaces namespace (merge namespace-props { launched-at: (some burn-block-height) })) + ;; Emit an event to indicate the namespace is now ready and launched. + (print { namespace: namespace, status: "launch", properties: (map-get? namespaces namespace) }) + ;; Confirm the successful launch of the namespace. + (ok true) + ) +) + +;; @desc (new) Public function `turn-off-manager-transfers` disables manager transfers for a namespace (callable only once). +;; @param: namespace (buff 20): The namespace for which manager transfers will be disabled. +(define-public (turn-off-manager-transfers (namespace (buff 20))) + (let + ( + ;; Retrieve the properties of the namespace and manager. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (namespace-manager (unwrap! (get namespace-manager namespace-props) ERR-NO-NAMESPACE-MANAGER)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the function caller is the namespace manager. + (asserts! (is-eq contract-caller namespace-manager) ERR-NOT-AUTHORIZED) + ;; Disable manager transfers. + (map-set namespaces namespace (merge namespace-props {manager-transferable: false})) + (print { namespace: namespace, status: "turn-off-manager-transfers", properties: (map-get? namespaces namespace) }) + ;; Confirm successful execution. + (ok true) + ) +) + +;; @desc Public function `name-import` allows the insertion of names into a namespace that has been revealed but not yet launched. +;; This facilitates pre-populating the namespace with specific names, assigning owners. +;; @param: namespace (buff 20): The namespace into which the name is being imported. +;; @param: name (buff 48): The name being imported into the namespace. +;; @param: beneficiary (principal): The principal who will own the imported name. +;; @param: stx-burn (uint): The amount of STX tokens to be burned as part of the import process. +(define-public (name-import (namespace (buff 20)) (name (buff 48)) (beneficiary principal)) + (let + ( + ;; Fetch properties of the specified namespace. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ;; Fetch the latest index to mint + (current-mint (+ (var-get bns-index) u1)) + (price (if (is-none (get namespace-manager namespace-props)) + (try! (compute-name-price name (get price-function namespace-props))) + u0 + ) + ) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the name is not already registered. + (asserts! (is-none (map-get? name-properties {name: name, namespace: namespace})) ERR-NAME-NOT-AVAILABLE) + ;; Verify that the name contains only valid characters. + (asserts! (not (has-invalid-chars name)) ERR-CHARSET-INVALID) + ;; Ensure the contract-caller is the namespace's designated import principal or the namespace manager + (asserts! (or (is-eq (get namespace-import namespace-props) contract-caller) (is-eq (get namespace-manager namespace-props) (some contract-caller))) ERR-OPERATION-UNAUTHORIZED) + ;; Check that the namespace has not been launched yet, as names can only be imported to namespaces that are revealed but not launched. + (asserts! (is-none (get launched-at namespace-props)) ERR-NAMESPACE-ALREADY-LAUNCHED) + ;; Confirm that the import is occurring within the allowed timeframe since the namespace was revealed. + (asserts! (< burn-block-height (+ (get revealed-at namespace-props) NAMESPACE-LAUNCHABILITY-TTL)) ERR-NAMESPACE-PREORDER-LAUNCHABILITY-EXPIRED) + ;; Set the name properties + (map-set name-properties {name: name, namespace: namespace} + { + registered-at: none, + imported-at: (some burn-block-height), + hashed-salted-fqn-preorder: none, + preordered-by: none, + renewal-height: u0, + stx-burn: price, + owner: beneficiary, + } + ) + (map-set name-to-index {name: name, namespace: namespace} current-mint) + (map-set index-to-name current-mint {name: name, namespace: namespace}) + ;; Update primary name if needed for send-to + (update-primary-name-recipient current-mint beneficiary) + ;; Update the index of the minting + (var-set bns-index current-mint) + ;; Mint the name to the beneficiary + (try! (nft-mint? BNS-V2 current-mint beneficiary)) + ;; Log the new name registration + (print + { + topic: "new-name", + owner: beneficiary, + name: {name: name, namespace: namespace}, + id: current-mint, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Confirm successful import of the name. + (ok true) + ) +) + +;; @desc Public function `namespace-update-price` updates the pricing function for a specific namespace. +;; @param: namespace (buff 20): The namespace for which the price function is being updated. +;; @param: p-func-base (uint): The base price used in the pricing function. +;; @param: p-func-coeff (uint): The coefficient used in the pricing function. +;; @param: p-func-b1 to p-func-b16 (uint): The bucket-specific multipliers for the pricing function. +;; @param: p-func-non-alpha-discount (uint): The discount applied for non-alphabetic characters. +;; @param: p-func-no-vowel-discount (uint): The discount applied when no vowels are present. +(define-public (namespace-update-price + (namespace (buff 20)) + (p-func-base uint) + (p-func-coeff uint) + (p-func-b1 uint) + (p-func-b2 uint) + (p-func-b3 uint) + (p-func-b4 uint) + (p-func-b5 uint) + (p-func-b6 uint) + (p-func-b7 uint) + (p-func-b8 uint) + (p-func-b9 uint) + (p-func-b10 uint) + (p-func-b11 uint) + (p-func-b12 uint) + (p-func-b13 uint) + (p-func-b14 uint) + (p-func-b15 uint) + (p-func-b16 uint) + (p-func-non-alpha-discount uint) + (p-func-no-vowel-discount uint) +) + (let + ( + ;; Retrieve the current properties of the namespace. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ;; Construct the new price function. + (price-function + { + buckets: (list p-func-b1 p-func-b2 p-func-b3 p-func-b4 p-func-b5 p-func-b6 p-func-b7 p-func-b8 p-func-b9 p-func-b10 p-func-b11 p-func-b12 p-func-b13 p-func-b14 p-func-b15 p-func-b16), + base: p-func-base, + coeff: p-func-coeff, + nonalpha-discount: p-func-non-alpha-discount, + no-vowel-discount: p-func-no-vowel-discount + } + ) + ) + (match (get namespace-manager namespace-props) + manager + ;; Ensure that the transaction sender is the namespace's designated import principal. + (asserts! (is-eq manager contract-caller) ERR-OPERATION-UNAUTHORIZED) + ;; Ensure that the contract-caller is the namespace's designated import principal. + (asserts! (is-eq (get namespace-import namespace-props) contract-caller) ERR-OPERATION-UNAUTHORIZED) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Verify the namespace's price function can still be updated. + (asserts! (get can-update-price-function namespace-props) ERR-OPERATION-UNAUTHORIZED) + ;; Update the namespace's record in the `namespaces` map with the new price function. + (map-set namespaces namespace (merge namespace-props { price-function: price-function })) + (print { namespace: namespace, status: "update-price-manager", properties: (map-get? namespaces namespace) }) + ;; Confirm the successful update of the price function. + (ok true) + ) +) + +;; @desc Public function `namespace-freeze-price` disables the ability to update the price function for a given namespace. +;; @param: namespace (buff 20): The target namespace for which the price function update capability is being revoked. +(define-public (namespace-freeze-price (namespace (buff 20))) + (let + ( + ;; Retrieve the properties of the specified namespace to verify its existence and fetch its current settings. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ) + (match (get namespace-manager namespace-props) + manager + ;; Ensure that the transaction sender is the same as the namespace's designated import principal. + (asserts! (is-eq manager contract-caller) ERR-OPERATION-UNAUTHORIZED) + ;; Ensure that the contract-caller is the same as the namespace's designated import principal. + (asserts! (is-eq (get namespace-import namespace-props) contract-caller) ERR-OPERATION-UNAUTHORIZED) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Update the namespace properties in the `namespaces` map, setting `can-update-price-function` to false. + (map-set namespaces namespace + (merge namespace-props { can-update-price-function: false }) + ) + (print { namespace: namespace, status: "freeze-price-manager", properties: (map-get? namespaces namespace) }) + ;; Return a success confirmation. + (ok true) + ) +) + +;; @desc (new) A 'fast' one-block registration function: (name-claim-fast) +;; Warning: this *is* snipeable, for a slower but un-snipeable claim, use the pre-order & register functions +;; @param: name (buff 48): The name being claimed. +;; @param: namespace (buff 20): The namespace under which the name is being claimed. +;; @param: stx-burn (uint): The amount of STX to burn for the claim. +;; @param: send-to (principal): The principal to whom the name will be sent. +(define-public (name-claim-fast (name (buff 48)) (namespace (buff 20)) (send-to principal)) + (let + ( + ;; Retrieve namespace properties. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (current-namespace-manager (get namespace-manager namespace-props)) + ;; Calculates the ID for the new name to be minted. + (id-to-be-minted (+ (var-get bns-index) u1)) + ;; Check if the name already exists. + (name-props (map-get? name-properties {name: name, namespace: namespace})) + ;; new to get the price of the name + (name-price (if (is-none current-namespace-manager) + (try! (compute-name-price name (get price-function namespace-props))) + u0 + ) + ) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the name is not already registered. + (asserts! (is-none name-props) ERR-NAME-NOT-AVAILABLE) + ;; Verify that the name contains only valid characters. + (asserts! (not (has-invalid-chars name)) ERR-CHARSET-INVALID) + ;; Ensure that the namespace is launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Check namespace manager + (match current-namespace-manager + manager + ;; If manager, check contract-caller is manager + (asserts! (is-eq contract-caller manager) ERR-NOT-AUTHORIZED) + ;; If no manager + (begin + ;; Asserts contract-caller is the send-to if not a managed namespace + (asserts! (is-eq contract-caller send-to) ERR-NOT-AUTHORIZED) + ;; Updated this to burn the actual ammount of the name-price + (try! (stx-burn? name-price send-to)) + ) + ) + ;; Update the index + (var-set bns-index id-to-be-minted) + ;; Sets properties for the newly registered name. + (map-set name-properties + { + name: name, namespace: namespace + } + { + registered-at: (some (+ burn-block-height u1)), + imported-at: none, + hashed-salted-fqn-preorder: none, + preordered-by: none, + ;; Updated this to actually start with the registered-at date/block, and also to be u0 if it is a managed namespace + renewal-height: (if (is-eq (get lifetime namespace-props) u0) + u0 + (+ (get lifetime namespace-props) burn-block-height u1) + ), + stx-burn: name-price, + owner: send-to, + } + ) + (map-set name-to-index {name: name, namespace: namespace} id-to-be-minted) + (map-set index-to-name id-to-be-minted {name: name, namespace: namespace}) + ;; Update primary name if needed for send-to + (update-primary-name-recipient id-to-be-minted send-to) + ;; Mints the new BNS name. + (try! (nft-mint? BNS-V2 id-to-be-minted send-to)) + ;; Log the new name registration + (print + { + topic: "new-name", + owner: send-to, + name: {name: name, namespace: namespace}, + id: id-to-be-minted, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Signals successful completion. + (ok id-to-be-minted) + ) +) + +;; @desc Defines a public function `name-preorder` for preordering BNS names by burning the registration fee and submitting the salted hash. +;; Callable by anyone; the actual check for authorization happens in the `name-register` function. +;; @param: hashed-salted-fqn (buff 20): The hashed and salted fully qualified name. +;; @param: stx-to-burn (uint): The amount of STX to burn for the preorder. +(define-public (name-preorder (hashed-salted-fqn (buff 20)) (stx-to-burn uint)) + (begin + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Validate the length of the hashed-salted FQN. + (asserts! (is-eq (len hashed-salted-fqn) HASH160LEN) ERR-HASH-MALFORMED) + ;; Ensures that the amount of STX specified to burn is greater than zero. + (asserts! (> stx-to-burn u0) ERR-STX-BURNT-INSUFFICIENT) + ;; Check if the same hashed-salted-fqn has been used before + (asserts! (is-none (map-get? name-single-preorder hashed-salted-fqn)) ERR-PREORDERED-BEFORE) + ;; Transfers the specified amount of stx to the BNS contract to burn on register + (try! (stx-transfer? stx-to-burn contract-caller .BNS-V2)) + ;; Records the preorder in the 'name-preorders' map. + (map-set name-preorders + { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller } + { created-at: burn-block-height, stx-burned: stx-to-burn, claimed: false} + ) + ;; Sets the map with just the hashed-salted-fqn as the key + (map-set name-single-preorder hashed-salted-fqn true) + ;; Returns the block height at which the preorder's claimability period will expire. + (ok (+ burn-block-height PREORDER-CLAIMABILITY-TTL)) + ) +) + +;; @desc Public function `name-register` finalizes the registration of a BNS name for users from unmanaged namespaces. +;; @param: namespace (buff 20): The namespace to which the name belongs. +;; @param: name (buff 48): The name to be registered. +;; @param: salt (buff 20): The salt used during the preorder. +(define-public (name-register (namespace (buff 20)) (name (buff 48)) (salt (buff 20))) + (let + ( + ;; Generate a unique identifier for the name by hashing the fully-qualified name with salt + (hashed-salted-fqn (hash160 (concat (concat (concat name 0x2e) namespace) salt))) + ;; Retrieve the preorder details for this name + (preorder (unwrap! (map-get? name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller }) ERR-PREORDER-NOT-FOUND)) + ;; Fetch the properties of the namespace + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ;; Get the amount of burned STX + (stx-burned (get stx-burned preorder)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure that the namespace is launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Ensure the preorder hasn't been claimed before + (asserts! (not (get claimed preorder)) ERR-OPERATION-UNAUTHORIZED) + ;; Check that the namespace doesn't have a manager (implying it's open for registration) + (asserts! (is-none (get namespace-manager namespace-props)) ERR-NOT-AUTHORIZED) + ;; Verify that the preorder was made after the namespace was launched + (asserts! (> (get created-at preorder) (unwrap! (get launched-at namespace-props) ERR-UNWRAP)) ERR-NAME-PREORDERED-BEFORE-NAMESPACE-LAUNCH) + ;; Ensure the registration is happening within the allowed time window after preorder + (asserts! (< burn-block-height (+ (get created-at preorder) PREORDER-CLAIMABILITY-TTL)) ERR-PREORDER-CLAIMABILITY-EXPIRED) + ;; Make sure at least one block has passed since the preorder (prevents front-running) + (asserts! (> burn-block-height (+ (get created-at preorder) u1)) ERR-NAME-NOT-CLAIMABLE-YET) + ;; Verify that enough STX was burned during preorder to cover the name price + (asserts! (is-eq stx-burned (try! (compute-name-price name (get price-function namespace-props)))) ERR-STX-BURNT-INSUFFICIENT) + ;; Verify that the name contains only valid characters. + (asserts! (not (has-invalid-chars name)) ERR-CHARSET-INVALID) + ;; Mark the preorder as claimed to prevent double-spending + (map-set name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller } (merge preorder {claimed: true})) + ;; Check if the name already exists + (match (map-get? name-properties {name: name, namespace: namespace}) + name-props-exist + ;; If the name exists + (handle-existing-name name-props-exist hashed-salted-fqn (get created-at preorder) stx-burned name namespace (get lifetime namespace-props)) + ;; If the name does not exist + (register-new-name (+ (var-get bns-index) u1) hashed-salted-fqn stx-burned name namespace (get lifetime namespace-props)) + ) + ) +) + +;; @desc (new) Defines a public function `claim-preorder` for claiming back the STX commited to be burnt on registration. +;; This should only be allowed to go through if preorder-claimability-ttl has passed +;; @param: hashed-salted-fqn (buff 20): The hashed and salted fully qualified name. +(define-public (claim-preorder (hashed-salted-fqn (buff 20))) + (let + ( + ;; Retrieves the preorder details. + (preorder (unwrap! (map-get? name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller }) ERR-PREORDER-NOT-FOUND)) + (claimer contract-caller) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Check if the preorder-claimability-ttl has passed + (asserts! (> burn-block-height (+ (get created-at preorder) PREORDER-CLAIMABILITY-TTL)) ERR-OPERATION-UNAUTHORIZED) + ;; Asserts that the preorder has not been claimed + (asserts! (not (get claimed preorder)) ERR-OPERATION-UNAUTHORIZED) + ;; Transfers back the specified amount of stx from the BNS contract to the contract-caller + (try! (as-contract (stx-transfer? (get stx-burned preorder) .BNS-V2 claimer))) + ;; Deletes the preorder in the 'name-preorders' map. + (map-delete name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller }) + ;; Remove the entry from the name-single-preorder map + (map-delete name-single-preorder hashed-salted-fqn) + ;; Returns ok true + (ok true) + ) +) + +;; @desc (new) This function is similar to `name-preorder` but only for namespace managers, without the burning of STX tokens. +;; Intended only for managers as mng-name-register & name-register will validate. +;; @param: hashed-salted-fqn (buff 20): The hashed and salted fully-qualified name (FQN) being preordered. +(define-public (mng-name-preorder (hashed-salted-fqn (buff 20))) + (begin + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Validates that the length of the hashed and salted FQN is exactly 20 bytes. + (asserts! (is-eq (len hashed-salted-fqn) HASH160LEN) ERR-HASH-MALFORMED) + ;; Check if the same hashed-salted-fqn has been used before + (asserts! (is-none (map-get? name-single-preorder hashed-salted-fqn)) ERR-PREORDERED-BEFORE) + ;; Records the preorder in the 'name-preorders' map. Buyer set to contract-caller + (map-set name-preorders + { hashed-salted-fqn: hashed-salted-fqn, buyer: contract-caller } + { created-at: burn-block-height, stx-burned: u0, claimed: false } + ) + ;; Sets the map with just the hashed-salted-fqn as the key + (map-set name-single-preorder hashed-salted-fqn true) + ;; Returns the block height at which the preorder's claimability period will expire. + (ok (+ burn-block-height PREORDER-CLAIMABILITY-TTL)) + ) +) + +;; @desc (new) This function uses provided details to verify the preorder, register the name, and assign it initial properties. +;; This should only allow Managers from MANAGED namespaces to register names. +;; @param: namespace (buff 20): The namespace for the name. +;; @param: name (buff 48): The name being registered. +;; @param: salt (buff 20): The salt used in hashing. +;; @param: send-to (principal): The principal to whom the name will be registered. +(define-public (mng-name-register (namespace (buff 20)) (name (buff 48)) (salt (buff 20)) (send-to principal)) + (let + ( + ;; Generates the hashed, salted fully-qualified name. + (hashed-salted-fqn (hash160 (concat (concat (concat name 0x2e) namespace) salt))) + ;; Retrieves the existing properties of the namespace to confirm its existence and management details. + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + (current-namespace-manager (unwrap! (get namespace-manager namespace-props) ERR-NO-NAMESPACE-MANAGER)) + ;; Retrieves the preorder information using the hashed-salted FQN to verify the preorder exists + (preorder (unwrap! (map-get? name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: current-namespace-manager }) ERR-PREORDER-NOT-FOUND)) + ;; Calculates the ID for the new name to be minted. + (id-to-be-minted (+ (var-get bns-index) u1)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the preorder has not been claimed before + (asserts! (not (get claimed preorder)) ERR-OPERATION-UNAUTHORIZED) + ;; Ensure the name is not already registered + (asserts! (is-none (map-get? name-properties {name: name, namespace: namespace})) ERR-NAME-NOT-AVAILABLE) + ;; Verify that the name contains only valid characters. + (asserts! (not (has-invalid-chars name)) ERR-CHARSET-INVALID) + ;; Verifies that the caller is the namespace manager. + (asserts! (is-eq contract-caller current-namespace-manager) ERR-NOT-AUTHORIZED) + ;; Validates that the preorder was made after the namespace was officially launched. + (asserts! (> (get created-at preorder) (unwrap! (get launched-at namespace-props) ERR-UNWRAP)) ERR-NAME-PREORDERED-BEFORE-NAMESPACE-LAUNCH) + ;; Verifies the registration is completed within the claimability period. + (asserts! (< burn-block-height (+ (get created-at preorder) PREORDER-CLAIMABILITY-TTL)) ERR-PREORDER-CLAIMABILITY-EXPIRED) + ;; Sets properties for the newly registered name. + (map-set name-properties + { + name: name, namespace: namespace + } + { + registered-at: (some burn-block-height), + imported-at: none, + hashed-salted-fqn-preorder: (some hashed-salted-fqn), + preordered-by: (some send-to), + ;; Updated this to be u0, so that renewals are handled through the namespace manager + renewal-height: u0, + stx-burn: u0, + owner: send-to, + } + ) + (map-set name-to-index {name: name, namespace: namespace} id-to-be-minted) + (map-set index-to-name id-to-be-minted {name: name, namespace: namespace}) + ;; Update primary name if needed for send-to + (update-primary-name-recipient id-to-be-minted send-to) + ;; Updates BNS-index variable to the newly minted ID. + (var-set bns-index id-to-be-minted) + ;; Update map to claimed for preorder, to avoid people reclaiming stx from an already registered name + (map-set name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: current-namespace-manager } (merge preorder {claimed: true})) + ;; Mints the BNS name as an NFT to the send-to address, finalizing the registration. + (try! (nft-mint? BNS-V2 id-to-be-minted send-to)) + ;; Log the new name registration + (print + { + topic: "new-name", + owner: send-to, + name: {name: name, namespace: namespace}, + id: id-to-be-minted, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Confirms successful registration of the name. + (ok id-to-be-minted) + ) +) + +;; Public function `name-renewal` for renewing ownership of a name. +;; @param: namespace (buff 20): The namespace of the name to be renewed. +;; @param: name (buff 48): The actual name to be renewed. +;; @param: stx-to-burn (uint): The amount of STX tokens to be burned for renewal. +(define-public (name-renewal (namespace (buff 20)) (name (buff 48))) + (let + ( + ;; Get the unique identifier for this name + (name-index (unwrap! (get-id-from-bns name namespace) ERR-NO-NAME)) + ;; Retrieve the properties of the namespace + (namespace-props (unwrap! (map-get? namespaces namespace) ERR-NAMESPACE-NOT-FOUND)) + ;; Get the manager of the namespace, if any + (namespace-manager (get namespace-manager namespace-props)) + ;; Get the current owner of the name + (owner (unwrap! (nft-get-owner? BNS-V2 name-index) ERR-NO-NAME)) + ;; Retrieve the properties of the name + (name-props (unwrap! (map-get? name-properties { name: name, namespace: namespace }) ERR-NO-NAME)) + ;; Get the lifetime of names in this namespace + (lifetime (get lifetime namespace-props)) + ;; Get the current renewal height of the name + (renewal-height (try! (get-renewal-height name-index))) + ;; Calculate the new renewal height based on current block height + (new-renewal-height (+ burn-block-height lifetime)) + ) + ;; Check if migration is complete + (asserts! (var-get migration-complete) ERR-MIGRATION-IN-PROGRESS) + ;; Verify that the namespace has been launched + (asserts! (is-some (get launched-at namespace-props)) ERR-NAMESPACE-NOT-LAUNCHED) + ;; Ensure the namespace doesn't have a manager + (asserts! (is-none namespace-manager) ERR-NAMESPACE-HAS-MANAGER) + ;; Check if renewals are required for this namespace + (asserts! (> lifetime u0) ERR-LIFETIME-EQUAL-0) + ;; Handle renewal based on whether it's within the grace period or not + (if (< burn-block-height (+ renewal-height NAME-GRACE-PERIOD-DURATION)) + (try! (handle-renewal-in-grace-period name namespace name-props owner lifetime new-renewal-height)) + (try! (handle-renewal-after-grace-period name namespace name-props owner name-index new-renewal-height)) + ) + ;; Burn the specified amount of STX + (try! (stx-burn? (try! (compute-name-price name (get price-function namespace-props))) contract-caller)) + ;; update the new stx-burn to the one paid in renewal + (map-set name-properties { name: name, namespace: namespace } (merge (unwrap-panic (map-get? name-properties { name: name, namespace: namespace })) {stx-burn: (try! (compute-name-price name (get price-function namespace-props)))})) + ;; Return success + (ok true) + ) +) + +;; Private function to handle renewals within the grace period +(define-private (handle-renewal-in-grace-period + (name (buff 48)) + (namespace (buff 20)) + (name-props + { + registered-at: (optional uint), + imported-at: (optional uint), + hashed-salted-fqn-preorder: (optional (buff 20)), + preordered-by: (optional principal), + renewal-height: uint, + stx-burn: uint, + owner: principal + } + ) + (owner principal) + (lifetime uint) + (new-renewal-height uint) +) + (begin + ;; Ensure only the owner can renew within the grace period + (asserts! (is-eq contract-caller owner) ERR-NOT-AUTHORIZED) + ;; Update the name properties with the new renewal height + (map-set name-properties {name: name, namespace: namespace} + (merge name-props + { + renewal-height: + ;; If still within lifetime, extend from current renewal height; otherwise, use new renewal height + (if (< burn-block-height (unwrap-panic (get-renewal-height (unwrap-panic (get-id-from-bns name namespace))))) + (+ (unwrap-panic (get-renewal-height (unwrap-panic (get-id-from-bns name namespace)))) lifetime) + new-renewal-height + ) + } + ) + ) + (print + { + topic: "renew-name", + owner: owner, + name: {name: name, namespace: namespace}, + id: (get-id-from-bns name namespace), + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + (ok true) + ) +) + +;; Private function to handle renewals after the grace period +(define-private (handle-renewal-after-grace-period + (name (buff 48)) + (namespace (buff 20)) + (name-props + { + registered-at: (optional uint), + imported-at: (optional uint), + hashed-salted-fqn-preorder: (optional (buff 20)), + preordered-by: (optional principal), + renewal-height: uint, + stx-burn: uint, + owner: principal + } + ) + (owner principal) + (name-index uint) + (new-renewal-height uint) +) + (if (is-eq contract-caller owner) + ;; If the owner is renewing, simply update the renewal height + (ok + (map-set name-properties {name: name, namespace: namespace} + (merge name-props {renewal-height: new-renewal-height}) + ) + ) + ;; If someone else is renewing (taking over the name) + (begin + ;; Check if the name is listed on the market and remove the listing if it is + (match (map-get? market name-index) + listed-name + (map-delete market name-index) + true + ) + (map-set name-properties {name: name, namespace: namespace} + (merge name-props {renewal-height: new-renewal-height}) + ) + ;; Update the name properties with the new renewal height and owner + (ok (try! (purchase-transfer name-index owner contract-caller))) + ) + ) +) + +;; Returns the minimum of two uint values. +(define-private (min (a uint) (b uint)) + ;; If 'a' is less than or equal to 'b', return 'a', else return 'b'. + (if (<= a b) a b) +) + +;; Returns the maximum of two uint values. +(define-private (max (a uint) (b uint)) + ;; If 'a' is greater than 'b', return 'a', else return 'b'. + (if (> a b) a b) +) + +;; Retrieves an exponent value from a list of buckets based on the provided index. +(define-private (get-exp-at-index (buckets (list 16 uint)) (index uint)) + ;; Retrieves the element at the specified index. + (unwrap-panic (element-at? buckets index)) +) + +;; Determines if a character is a digit (0-9). +(define-private (is-digit (char (buff 1))) + (or + ;; Checks if the character is between '0' and '9' using hex values. + (is-eq char 0x30) ;; 0 + (is-eq char 0x31) ;; 1 + (is-eq char 0x32) ;; 2 + (is-eq char 0x33) ;; 3 + (is-eq char 0x34) ;; 4 + (is-eq char 0x35) ;; 5 + (is-eq char 0x36) ;; 6 + (is-eq char 0x37) ;; 7 + (is-eq char 0x38) ;; 8 + (is-eq char 0x39) ;; 9 + ) +) + +;; Checks if a character is a lowercase alphabetic character (a-z). +(define-private (is-lowercase-alpha (char (buff 1))) + (or + ;; Checks for each lowercase letter using hex values. + (is-eq char 0x61) ;; a + (is-eq char 0x62) ;; b + (is-eq char 0x63) ;; c + (is-eq char 0x64) ;; d + (is-eq char 0x65) ;; e + (is-eq char 0x66) ;; f + (is-eq char 0x67) ;; g + (is-eq char 0x68) ;; h + (is-eq char 0x69) ;; i + (is-eq char 0x6a) ;; j + (is-eq char 0x6b) ;; k + (is-eq char 0x6c) ;; l + (is-eq char 0x6d) ;; m + (is-eq char 0x6e) ;; n + (is-eq char 0x6f) ;; o + (is-eq char 0x70) ;; p + (is-eq char 0x71) ;; q + (is-eq char 0x72) ;; r + (is-eq char 0x73) ;; s + (is-eq char 0x74) ;; t + (is-eq char 0x75) ;; u + (is-eq char 0x76) ;; v + (is-eq char 0x77) ;; w + (is-eq char 0x78) ;; x + (is-eq char 0x79) ;; y + (is-eq char 0x7a) ;; z + ) +) + +;; Determines if a character is a vowel (a, e, i, o, u, and y). +(define-private (is-vowel (char (buff 1))) + (or + (is-eq char 0x61) ;; a + (is-eq char 0x65) ;; e + (is-eq char 0x69) ;; i + (is-eq char 0x6f) ;; o + (is-eq char 0x75) ;; u + (is-eq char 0x79) ;; y + ) +) + +;; Identifies if a character is a special character, specifically '-' or '_'. +(define-private (is-special-char (char (buff 1))) + (or + (is-eq char 0x2d) ;; - + (is-eq char 0x5f)) ;; _ +) + +;; Determines if a character is valid within a name, based on allowed character sets. +(define-private (is-char-valid (char (buff 1))) + (or (is-lowercase-alpha char) (is-digit char) (is-special-char char)) +) + +;; Checks if a character is non-alphabetic, either a digit or a special character. +(define-private (is-nonalpha (char (buff 1))) + (or (is-digit char) (is-special-char char)) +) + +;; Evaluates if a name contains any vowel characters. +(define-private (has-vowels-chars (name (buff 48))) + (> (len (filter is-vowel name)) u0) +) + +;; Determines if a name contains non-alphabetic characters. +(define-private (has-nonalpha-chars (name (buff 48))) + (> (len (filter is-nonalpha name)) u0) +) + +;; Identifies if a name contains any characters that are not considered valid. +(define-private (has-invalid-chars (name (buff 48))) + (< (len (filter is-char-valid name)) (len name)) +) + +;; Private helper function `is-namespace-available` checks if a namespace is available for registration or other operations. +;; It considers if the namespace has been launched and whether it has expired. +;; @params: + ;; namespace (buff 20): The namespace to check for availability. +(define-private (is-namespace-available (namespace (buff 20))) + ;; Check if the namespace exists + (match (map-get? namespaces namespace) + namespace-props + ;; If it exists + ;; Check if the namespace has been launched. + (match (get launched-at namespace-props) + launched + ;; If the namespace is launched, it's considered unavailable if it hasn't expired. + false + ;; Check if the namespace is expired by comparing the current block height to the reveal time plus the launchability TTL. + (> burn-block-height (+ (get revealed-at namespace-props) NAMESPACE-LAUNCHABILITY-TTL)) + ) + ;; If the namespace doesn't exist in the map, it's considered available. + true + ) +) + +;; Private helper function `compute-name-price` calculates the registration price for a name based on its length and character composition. +;; It utilizes a configurable pricing function that can adjust prices based on the name's characteristics. +;; @params: +;; name (buff 48): The name for which the price is being calculated. +;; price-function (tuple): A tuple containing the parameters of the pricing function, including: +;; buckets (list 16 uint): A list defining price multipliers for different name lengths. +;; base (uint): The base price multiplier. +;; coeff (uint): A coefficient that adjusts the base price. +;; nonalpha-discount (uint): A discount applied to names containing non-alphabetic characters. +;; no-vowel-discount (uint): A discount applied to names lacking vowel characters. +(define-private (compute-name-price (name (buff 48)) (price-function {buckets: (list 16 uint), base: uint, coeff: uint, nonalpha-discount: uint, no-vowel-discount: uint})) + (let + ( + ;; Determine the appropriate exponent based on the name's length. + ;; This corresponds to a specific bucket in the pricing function. + ;; The length of the name is used to index into the buckets list, with a maximum index of 15. + (exponent (get-exp-at-index (get buckets price-function) (min u15 (- (len name) u1)))) + ;; Calculate the no-vowel discount. + ;; If the name has no vowels, apply the no-vowel discount from the price function. + ;; Otherwise, use 1 indicating no discount. + (no-vowel-discount (if (not (has-vowels-chars name)) (get no-vowel-discount price-function) u1)) + ;; Calculate the non-alphabetic character discount. + ;; If the name contains non-alphabetic characters, apply the non-alpha discount from the price function. + ;; Otherwise, use 1 indicating no discount. + (nonalpha-discount (if (has-nonalpha-chars name) (get nonalpha-discount price-function) u1)) + (len-name (len name)) + ) + (asserts! (> len-name u0) ERR-NAME-BLANK) + ;; Compute the final price. + ;; The base price, adjusted by the coefficient and exponent, is divided by the greater of the two discounts (non-alpha or no-vowel). + ;; The result is then multiplied by 10 to adjust for unit precision. + (ok (* (/ (* (get coeff price-function) (pow (get base price-function) exponent)) (max nonalpha-discount no-vowel-discount)) u10)) + ) +) + +;; This function is similar to the 'transfer' function but does not check that the owner is the contract-caller. +;; @param id: the id of the nft being transferred. +;; @param owner: the principal of the current owner of the nft being transferred. +;; @param recipient: the principal of the recipient to whom the nft is being transferred. +(define-private (purchase-transfer (id uint) (owner principal) (recipient principal)) + (let + ( + ;; Attempts to retrieve the name and namespace associated with the given NFT ID. + (name-and-namespace (unwrap! (map-get? index-to-name id) ERR-NO-NAME)) + ;; Retrieves the properties of the name within the namespace. + (name-props (unwrap! (map-get? name-properties name-and-namespace) ERR-NO-NAME)) + ) + ;; Check owner and recipient is not the same + (asserts! (not (is-eq owner recipient)) ERR-OPERATION-UNAUTHORIZED) + (asserts! (is-eq owner (get owner name-props)) ERR-NOT-AUTHORIZED) + ;; Update primary name if needed for owner + (update-primary-name-owner id owner) + ;; Update primary name if needed for recipient + (update-primary-name-recipient id recipient) + ;; Updates the owner to the recipient. + (map-set name-properties name-and-namespace (merge name-props {owner: recipient})) + ;; Executes the NFT transfer from the current owner to the recipient. + (try! (nft-transfer? BNS-V2 id owner recipient)) + (print + { + topic: "transfer-name", + owner: recipient, + name: {name: (get name name-and-namespace), namespace: (get namespace name-and-namespace)}, + id: id, + properties: (map-get? name-properties {name: (get name name-and-namespace), namespace: (get namespace name-and-namespace)}) + } + ) + (ok true) + ) +) + +;; Private function to update the primary name of an address when transfering a name +;; If the id is = to the primary name then it means that a transfer is happening and we should delete it +(define-private (update-primary-name-owner (id uint) (owner principal)) + ;; Check if the owner is transferring the primary name + (if (is-eq (map-get? primary-name owner) (some id)) + ;; If it is, then delete the primary name map + (map-delete primary-name owner) + ;; If it is not, do nothing, keep the current primary name + false + ) +) + +;; Private function to update the primary name of an address when recieving +(define-private (update-primary-name-recipient (id uint) (recipient principal)) + ;; Check if recipient has a primary name + (match (map-get? primary-name recipient) + recipient-primary-name + ;; If recipient has a primary name do nothing + true + ;; If recipient doesn't have a primary name + (map-set primary-name recipient id) + ) +) + +(define-private (handle-existing-name + (name-props + { + registered-at: (optional uint), + imported-at: (optional uint), + hashed-salted-fqn-preorder: (optional (buff 20)), + preordered-by: (optional principal), + renewal-height: uint, + stx-burn: uint, + owner: principal + } + ) + (hashed-salted-fqn (buff 20)) + (contract-caller-preorder-height uint) + (stx-burned uint) (name (buff 48)) + (namespace (buff 20)) + (renewal uint) +) + (let + ( + ;; Retrieve the index of the existing name + (name-index (unwrap-panic (map-get? name-to-index {name: name, namespace: namespace}))) + ) + ;; Straight up check if the name was imported + (asserts! (is-none (get imported-at name-props)) ERR-IMPORTED-BEFORE) + ;; If the check passes then it is registered, we can straight up check the hashed-salted-fqn-preorder + (match (get hashed-salted-fqn-preorder name-props) + fqn + ;; Compare both preorder's height + (asserts! (> (unwrap-panic (get created-at (map-get? name-preorders {hashed-salted-fqn: fqn, buyer: (unwrap-panic (get preordered-by name-props))}))) contract-caller-preorder-height) ERR-PREORDERED-BEFORE) + ;; Compare registered with preorder height + (asserts! (> (unwrap-panic (get registered-at name-props)) contract-caller-preorder-height) ERR-FAST-MINTED-BEFORE) + ) + ;; Update the name properties with the new preorder information since it is the best preorder + (map-set name-properties {name: name, namespace: namespace} + (merge name-props + { + hashed-salted-fqn-preorder: (some hashed-salted-fqn), + preordered-by: (some contract-caller), + registered-at: (some burn-block-height), + renewal-height: (if (is-eq renewal u0) + u0 + (+ burn-block-height renewal) + ), + stx-burn: stx-burned + } + ) + ) + (try! (as-contract (stx-transfer? stx-burned .BNS-V2 (get owner name-props)))) + ;; Transfer ownership of the name to the new owner + (try! (purchase-transfer name-index (get owner name-props) contract-caller)) + ;; Log the name transfer event + (print + { + topic: "transfer-name", + owner: contract-caller, + name: {name: name, namespace: namespace}, + id: name-index, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Return the name index + (ok name-index) + ) +) + +(define-private (register-new-name (id-to-be-minted uint) (hashed-salted-fqn (buff 20)) (stx-burned uint) (name (buff 48)) (namespace (buff 20)) (lifetime uint)) + (begin + ;; Set the properties for the newly registered name + (map-set name-properties + {name: name, namespace: namespace} + { + registered-at: (some burn-block-height), + imported-at: none, + hashed-salted-fqn-preorder: (some hashed-salted-fqn), + preordered-by: (some contract-caller), + renewal-height: (if (is-eq lifetime u0) + u0 + (+ burn-block-height lifetime) + ), + stx-burn: stx-burned, + owner: contract-caller, + } + ) + ;; Update the index-to-name and name-to-index mappings + (map-set index-to-name id-to-be-minted {name: name, namespace: namespace}) + (map-set name-to-index {name: name, namespace: namespace} id-to-be-minted) + ;; Increment the BNS index + (var-set bns-index id-to-be-minted) + ;; Update the primary name for the new owner if necessary + (update-primary-name-recipient id-to-be-minted contract-caller) + ;; Mint a new NFT for the BNS name + (try! (nft-mint? BNS-V2 id-to-be-minted contract-caller)) + ;; Burn the STX paid for the name registration + (try! (as-contract (stx-burn? stx-burned .BNS-V2))) + ;; Log the new name registration event + (print + { + topic: "new-name", + owner: contract-caller, + name: {name: name, namespace: namespace}, + id: id-to-be-minted, + properties: (map-get? name-properties {name: name, namespace: namespace}) + } + ) + ;; Return the ID of the newly minted name + (ok id-to-be-minted) + ) +) + +;; Migration Functions +(define-public (namespace-airdrop + (namespace (buff 20)) + (pricing {base: uint, buckets: (list 16 uint), coeff: uint, no-vowel-discount: uint, nonalpha-discount: uint}) + (lifetime uint) + (namespace-import principal) + (namespace-manager (optional principal)) + (can-update-price bool) + (manager-transfers bool) + (manager-frozen bool) + (revealed-at uint) + (launched-at uint) +) + (begin + ;; Check if migration is complete + (asserts! (not (var-get migration-complete)) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the contract-caller is the airdrop contract. + (asserts! (is-eq DEPLOYER tx-sender) ERR-OPERATION-UNAUTHORIZED) + ;; Ensure the namespace consists of valid characters only. + (asserts! (not (has-invalid-chars namespace)) ERR-CHARSET-INVALID) + ;; Check that the namespace is available for reveal. + (asserts! (unwrap! (can-namespace-be-registered namespace) ERR-NAMESPACE-ALREADY-EXISTS) ERR-NAMESPACE-ALREADY-EXISTS) + ;; Set all properties + (map-set namespaces namespace + { + namespace-manager: namespace-manager, + manager-transferable: manager-transfers, + manager-frozen: manager-frozen, + namespace-import: namespace-import, + revealed-at: revealed-at, + launched-at: (some launched-at), + lifetime: lifetime, + can-update-price-function: can-update-price, + price-function: pricing + } + ) + ;; Emit an event to indicate the namespace is now ready and launched. + (print { namespace: namespace, status: "launch", properties: (map-get? namespaces namespace)}) + ;; Confirm successful airdrop of the namespace + (ok namespace) + ) +) + +(define-public (name-airdrop + (name (buff 48)) + (namespace (buff 20)) + (registered-at uint) + (lifetime uint) + (owner principal) +) + (let + ( + (mint-index (+ u1 (var-get bns-index))) + ) + ;; Check if migration is complete + (asserts! (not (var-get migration-complete)) ERR-MIGRATION-IN-PROGRESS) + ;; Ensure the contract-caller is the airdrop contract. + (asserts! (is-eq DEPLOYER tx-sender) ERR-OPERATION-UNAUTHORIZED) + ;; Set all properties + (map-set name-to-index {name: name, namespace: namespace} mint-index) + (map-set index-to-name mint-index {name: name, namespace: namespace}) + (map-set name-properties {name: name, namespace: namespace} + { + registered-at: (some registered-at), + imported-at: none, + hashed-salted-fqn-preorder: none, + preordered-by: none, + renewal-height: (if (is-eq lifetime u0) u0 (+ burn-block-height lifetime)), + stx-burn: u0, + owner: owner, + } + ) + ;; Update the index + (var-set bns-index mint-index) + ;; Update the primary name of the recipient + (map-set primary-name owner mint-index) + ;; Mint the Name to the owner + (try! (nft-mint? BNS-V2 mint-index owner)) + (print + { + topic: "new-airdrop", + owner: owner, + name: {name: name, namespace: namespace}, + id: mint-index, + registered-at: registered-at, + } + ) + ;; Confirm successful airdrop of the namespace + (ok mint-index) + ) +) + +(define-public (flip-migration-complete) + (ok + (begin + (asserts! (is-eq contract-caller DEPLOYER) ERR-NOT-AUTHORIZED) + (var-set migration-complete true) + ) + ) +) + diff --git a/components/clarinet-format/tests/golden-intended/flash-loan-user-margin-usda-wbtc.clar b/components/clarinet-format/tests/golden-intended/flash-loan-user-margin-usda-wbtc.clar new file mode 100644 index 000000000..5f79ac822 --- /dev/null +++ b/components/clarinet-format/tests/golden-intended/flash-loan-user-margin-usda-wbtc.clar @@ -0,0 +1,130 @@ +(impl-trait .trait-flash-loan-user.flash-loan-user-trait) +(use-trait ft-trait .trait-sip-010.sip-010-trait) + +(define-constant ONE_8 u100000000) +(define-constant ERR-EXPIRY-IS-NONE (err u2027)) +(define-constant ERR-INVALID-TOKEN (err u2026)) + +;; @desc execute +;; @params collateral +;; @params amount +;; @params memo ; expiry +;; @returns (response boolean) +(define-public (execute (collateral ) (amount uint) (memo (optional (buff 16)))) + (let + ( + ;; gross amount * ltv / price = amount + ;; gross amount = amount * price / ltv + ;; buff to uint conversion + (memo-uint (buff-to-uint (unwrap! memo ERR-EXPIRY-IS-NONE))) + (ltv (try! (contract-call? .collateral-rebalancing-pool-v1 get-ltv .token-wbtc .token-wusda memo-uint))) + (price (try! (contract-call? .yield-token-pool get-price memo-uint .yield-wbtc))) + (gross-amount (mul-up amount (div-down price ltv))) + (minted-yield-token (get yield-token (try! (contract-call? .collateral-rebalancing-pool-v1 add-to-position .token-wbtc .token-wusda memo-uint .yield-wbtc .key-wbtc-usda gross-amount)))) + (swapped-token (get dx (try! (contract-call? .yield-token-pool swap-y-for-x memo-uint .yield-wbtc .token-wbtc minted-yield-token none)))) + ) + (asserts! (is-eq .token-wusda (contract-of collateral)) ERR-INVALID-TOKEN) + ;; swap token to collateral so we can return flash-loan + (try! (contract-call? .fixed-weight-pool-v1-01 swap-helper .token-wbtc .token-wusda u50000000 u50000000 swapped-token none)) + (print { object: "flash-loan-user-margin-usda-wbtc", action: "execute", data: gross-amount }) + (ok true) + ) +) + +;; @desc mul-up +;; @params a +;; @params b +;; @returns uint +(define-private (mul-up (a uint) (b uint)) + (let + ( + (product (* a b)) + ) + (if (is-eq product u0) + u0 + (+ u1 (/ (- product u1) ONE_8)) + ) + ) +) + +;; @desc div-down +;; @params a +;; @params b +;; @returns uint +(define-private (div-down (a uint) (b uint)) + (if (is-eq a u0) + u0 + (/ (* a ONE_8) b) + ) +) + +;; @desc buff-to-uint +;; @params bytes +;; @returns uint +(define-private (buff-to-uint (bytes (buff 16))) + (let + ( + (reverse-bytes (reverse-buff bytes)) + ) + (+ + (match (element-at reverse-bytes u0) byte (byte-to-uint byte) u0) + (match (element-at reverse-bytes u1) byte (* (byte-to-uint byte) u256) u0) + (match (element-at reverse-bytes u2) byte (* (byte-to-uint byte) u65536) u0) + (match (element-at reverse-bytes u3) byte (* (byte-to-uint byte) u16777216) u0) + (match (element-at reverse-bytes u4) byte (* (byte-to-uint byte) u4294967296) u0) + (match (element-at reverse-bytes u5) byte (* (byte-to-uint byte) u1099511627776) u0) + (match (element-at reverse-bytes u6) byte (* (byte-to-uint byte) u281474976710656) u0) + (match (element-at reverse-bytes u7) byte (* (byte-to-uint byte) u72057594037927936) u0) + (match (element-at reverse-bytes u8) byte (* (byte-to-uint byte) u18446744073709551616) u0) + (match (element-at reverse-bytes u9) byte (* (byte-to-uint byte) u4722366482869645213696) u0) + (match (element-at reverse-bytes u10) byte (* (byte-to-uint byte) u1208925819614629174706176) u0) + (match (element-at reverse-bytes u11) byte (* (byte-to-uint byte) u309485009821345068724781056) u0) + (match (element-at reverse-bytes u12) byte (* (byte-to-uint byte) u79228162514264337593543950336) u0) + (match (element-at reverse-bytes u13) byte (* (byte-to-uint byte) u20282409603651670423947251286016) u0) + (match (element-at reverse-bytes u14) byte (* (byte-to-uint byte) u5192296858534827628530496329220096) u0) + (match (element-at reverse-bytes u15) byte (* (byte-to-uint byte) u1329227995784915872903807060280344576) u0) + ) + ) +) + +;; lookup table for converting 1-byte buffers to uints via index-of +(define-constant BUFF-TO-BYTE (list + 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f + 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 0x1c 0x1d 0x1e 0x1f + 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 0x28 0x29 0x2a 0x2b 0x2c 0x2d 0x2e 0x2f + 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x3a 0x3b 0x3c 0x3d 0x3e 0x3f + 0x40 0x41 0x42 0x43 0x44 0x45 0x46 0x47 0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f + 0x50 0x51 0x52 0x53 0x54 0x55 0x56 0x57 0x58 0x59 0x5a 0x5b 0x5c 0x5d 0x5e 0x5f + 0x60 0x61 0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e 0x6f + 0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79 0x7a 0x7b 0x7c 0x7d 0x7e 0x7f + 0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87 0x88 0x89 0x8a 0x8b 0x8c 0x8d 0x8e 0x8f + 0x90 0x91 0x92 0x93 0x94 0x95 0x96 0x97 0x98 0x99 0x9a 0x9b 0x9c 0x9d 0x9e 0x9f + 0xa0 0xa1 0xa2 0xa3 0xa4 0xa5 0xa6 0xa7 0xa8 0xa9 0xaa 0xab 0xac 0xad 0xae 0xaf + 0xb0 0xb1 0xb2 0xb3 0xb4 0xb5 0xb6 0xb7 0xb8 0xb9 0xba 0xbb 0xbc 0xbd 0xbe 0xbf + 0xc0 0xc1 0xc2 0xc3 0xc4 0xc5 0xc6 0xc7 0xc8 0xc9 0xca 0xcb 0xcc 0xcd 0xce 0xcf + 0xd0 0xd1 0xd2 0xd3 0xd4 0xd5 0xd6 0xd7 0xd8 0xd9 0xda 0xdb 0xdc 0xdd 0xde 0xdf + 0xe0 0xe1 0xe2 0xe3 0xe4 0xe5 0xe6 0xe7 0xe8 0xe9 0xea 0xeb 0xec 0xed 0xee 0xef + 0xf0 0xf1 0xf2 0xf3 0xf4 0xf5 0xf6 0xf7 0xf8 0xf9 0xfa 0xfb 0xfc 0xfd 0xfe 0xff +)) + +;; @desc byte-to-uint +;; @params byte +;; @returns uint +(define-read-only (byte-to-uint (byte (buff 1))) + (unwrap-panic (index-of BUFF-TO-BYTE byte)) +) + +;; @desc concat-buff +;; @params a +;; @params b +;; @returns buff +(define-private (concat-buff (a (buff 16)) (b (buff 16))) + (unwrap-panic (as-max-len? (concat a b) u16)) +) + +;; @desc reverse-buff +;; @params a +;; @returns buff +(define-read-only (reverse-buff (a (buff 16))) + (fold concat-buff a 0x) +) \ No newline at end of file diff --git a/components/clarinet-format/tests/golden-intended/sbtc-deposit.clar b/components/clarinet-format/tests/golden-intended/sbtc-deposit.clar new file mode 100644 index 000000000..5a4b543ca --- /dev/null +++ b/components/clarinet-format/tests/golden-intended/sbtc-deposit.clar @@ -0,0 +1,108 @@ +;; sBTC Deposit contract + +;; constants + +;; The required length of a txid +(define-constant txid-length u32) +(define-constant dust-limit u546) + +;; error codes +;; TXID used in deposit is not the correct length +(define-constant ERR_TXID_LEN (err u300)) +;; Deposit has already been completed +(define-constant ERR_DEPOSIT_REPLAY (err u301)) +(define-constant ERR_LOWER_THAN_DUST (err u302)) +(define-constant ERR_DEPOSIT_INDEX_PREFIX (unwrap-err! ERR_DEPOSIT (err true))) +(define-constant ERR_DEPOSIT (err u303)) +(define-constant ERR_INVALID_CALLER (err u304)) +(define-constant ERR_INVALID_BURN_HASH (err u305)) + +;; data vars + +;; data maps + +;; public functions + +;; Accept a new deposit request +;; Note that this function can only be called by the current +;; bootstrap signer set address - it cannot be called by users directly. +;; This function handles the validation & minting of sBTC, it then calls +;; into the sbtc-registry contract to update the state of the protocol +(define-public (complete-deposit-wrapper (txid (buff 32)) + (vout-index uint) + (amount uint) + (recipient principal) + (burn-hash (buff 32)) + (burn-height uint) + (sweep-txid (buff 32))) + (let + ( + (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) + (replay-fetch (contract-call? .sbtc-registry get-completed-deposit txid vout-index)) + ) + + ;; Check that the caller is the current signer principal + (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) + + ;; Check that amount is greater than dust limit + (asserts! (>= amount dust-limit) ERR_LOWER_THAN_DUST) + + ;; Check that txid is the correct length + (asserts! (is-eq (len txid) txid-length) ERR_TXID_LEN) + + ;; Check that sweep txid is the correct length + (asserts! (is-eq (len sweep-txid) txid-length) ERR_TXID_LEN) + + ;; Assert that the deposit has not already been completed (no replay) + (asserts! (is-none replay-fetch) ERR_DEPOSIT_REPLAY) + + ;; Verify that Bitcoin hasn't forked by comparing the burn hash provided + (asserts! (is-eq (some burn-hash) (get-burn-header burn-height)) ERR_INVALID_BURN_HASH) + + ;; Mint the sBTC to the recipient + (try! (contract-call? .sbtc-token protocol-mint amount recipient)) + + ;; Complete the deposit + (ok (contract-call? .sbtc-registry complete-deposit txid vout-index amount recipient burn-hash burn-height sweep-txid)) + ) +) + +;; Return the bitcoin header hash of the bitcoin block at the given height. +(define-read-only (get-burn-header (height uint)) + (get-burn-block-info? header-hash height) +) + +;; Accept multiple new deposit requests +;; Note that this function can only be called by the current +;; bootstrap signer set address - it cannot be called by users directly. +;; +;; This function handles the validation & minting of sBTC by handling multiple (up to 1000) deposits at a time, +;; it then calls into the sbtc-registry contract to update the state of the protocol. +(define-public (complete-deposits-wrapper + (deposits (list 650 {txid: (buff 32), vout-index: uint, amount: uint, recipient: principal, burn-hash: (buff 32), burn-height: uint, sweep-txid: (buff 32)})) + ) + (begin + ;; Check that the caller is the current signer principal + (asserts! (is-eq + (contract-call? .sbtc-registry get-current-signer-principal) + tx-sender + ) ERR_INVALID_CALLER) + + (fold complete-individual-deposits-helper deposits (ok u0)) + ) +) + +;; private functions +;; #[allow(unchecked_data)] +(define-private (complete-individual-deposits-helper (deposit {txid: (buff 32), vout-index: uint, amount: uint, recipient: principal, burn-hash: (buff 32), burn-height: uint, sweep-txid: (buff 32)}) (helper-response (response uint uint))) + (match helper-response + index + (begin + (try! (unwrap! (complete-deposit-wrapper (get txid deposit) (get vout-index deposit) (get amount deposit) (get recipient deposit) (get burn-hash deposit) (get burn-height deposit) (get sweep-txid deposit)) (err (+ ERR_DEPOSIT_INDEX_PREFIX (+ u10 index))))) + (ok (+ index u1)) + ) + err-response + (err err-response) + ) +) + From b863acf055dc92b4fa6d1229719a484eca2063a5 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 25 Nov 2024 21:44:14 -0800 Subject: [PATCH 12/17] switch to use PSE and refactor the matchings --- .../clarinet-format/src/formatter/mod.rs | 409 +++++++++++++----- 1 file changed, 302 insertions(+), 107 deletions(-) diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index 601ce16cc..b73e57570 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -1,6 +1,7 @@ use clarity::types::StacksEpochId; use clarity::vm::ast::{build_ast_with_rules, ASTRules}; use clarity::vm::functions::{define::DefineFunctions, NativeFunctions}; +use clarity::vm::representations::{PreSymbolicExpression, PreSymbolicExpressionType}; use clarity::vm::types::QualifiedContractIdentifier; use clarity::vm::{ClarityVersion, SymbolicExpression}; @@ -39,111 +40,169 @@ impl ClarityFormatter { Self { settings } } pub fn format(&mut self, source: &str) -> String { - let ast = build_ast_with_rules( - &QualifiedContractIdentifier::transient(), - source, - &mut (), - ClarityVersion::Clarity3, - StacksEpochId::Epoch30, - ASTRules::Typical, - ) - .unwrap(); - format_source_exprs(&self.settings, &ast.expressions, "") + let pse = clarity::vm::ast::parser::v2::parse(source).unwrap(); + format_source_exprs(&self.settings, &pse, "") } } -// * functions - -// Top level define- should have a line break above and after (except on first line) -// options always on new lines -// Functions Always on multiple lines, even if short -// *begin* never on one line -// *let* never on one line - -// * match * -// One line if less than max length (unless the original source has line breaks?) -// Multiple lines if more than max length (should the first arg be on the first line if it fits?) pub fn format_source_exprs( settings: &Settings, - expressions: &[SymbolicExpression], + expressions: &[PreSymbolicExpression], acc: &str, ) -> String { if let Some((expr, remaining)) = expressions.split_first() { if let Some(list) = expr.match_list() { - let atom = list.split_first().and_then(|(f, _)| f.match_atom()); - use NativeFunctions::*; - let formatted = if let Some( - DefineFunctions::PublicFunction - | DefineFunctions::ReadOnlyFunction - | DefineFunctions::PrivateFunction - | DefineFunctions::Map, - ) = atom.and_then(|a| DefineFunctions::lookup_by_name(a)) - { - format_function(settings, list) - } else if let Some(Begin) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) { - format_begin(settings, list) - } else if let Some(Let) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) { - format_let(settings, list) - } else if let Some(TupleCons) = atom.and_then(|a| NativeFunctions::lookup_by_name(a)) { - format_tuple(settings, list) - } else { - format!("({})", format_source_exprs(settings, list, acc)) - }; - let pre_comments = format_comments(&expr.pre_comments); - let post_comments = format_comments(&expr.post_comments); + // println!("{:?}", list); + if let Some(atom_name) = list.split_first().and_then(|(f, _)| f.match_atom()) { + let formatted = if let Some(native) = NativeFunctions::lookup_by_name(atom_name) { + match native { + NativeFunctions::Let => format_let(settings, list), + NativeFunctions::Begin => format_begin(settings, list), + NativeFunctions::Match => format_match(settings, list), + NativeFunctions::TupleCons => format_tuple(settings, list), + NativeFunctions::ListCons => format_list(settings, list), + _ => format!("({})", format_source_exprs(settings, list, acc)), + } + } else if let Some(define) = DefineFunctions::lookup_by_name(atom_name) { + match define { + DefineFunctions::PublicFunction + | DefineFunctions::ReadOnlyFunction + | DefineFunctions::PrivateFunction => format_function(settings, list), + DefineFunctions::Constant => format_constant(settings, list), + DefineFunctions::UseTrait => format_use_trait(settings, list), + DefineFunctions::Trait => format_trait(settings, list), + DefineFunctions::Map => format_map(settings, list), + DefineFunctions::ImplTrait => format_impl_trait(settings, list), + // DefineFunctions::PersistedVariable + // DefineFunctions::FungibleToken + // DefineFunctions::NonFungibleToken + _ => format!("({})", format_source_exprs(settings, list, acc)), + } + } else { + format!("({})", format_source_exprs(settings, list, acc)) + }; - let end_line_comment = if let Some(comment) = &expr.end_line_comment { - format!(" ;; {}\n", comment) - } else { - String::new() - }; - let post_comment_prefix = if end_line_comment.is_empty() { - "\n" - } else { - "" - }; - return format!( - "{pre_comments}{formatted}{end_line_comment}{post_comment_prefix}{post_comments}{}", - format_source_exprs(settings, remaining, acc) - ) - .trim() - .to_owned(); + return format!( + "{formatted}{}", + format_source_exprs(settings, remaining, acc) + ) + .trim() + .to_owned(); + } } - return format!("{} {}", expr, format_source_exprs(settings, remaining, acc)) - .trim() - .to_owned(); + return format!( + "{} {}", + display_pse(expr), + format_source_exprs(settings, remaining, acc) + ) + .trim() + .to_owned(); }; acc.to_owned() } -fn format_comments(comments: &[(String, clarity::vm::representations::Span)]) -> String { - if !comments.is_empty() { - comments - .iter() - .map(|(comment, span)| { - format!( - "{};; {}\n", - " ".repeat(span.start_column as usize - 1), - comment - ) - }) - .collect::>() - .join("\n") +fn format_use_trait(_settings: &Settings, _exprs: &[PreSymbolicExpression]) -> String { + "".to_string() +} +fn format_impl_trait(_settings: &Settings, _exprs: &[PreSymbolicExpression]) -> String { + "".to_string() +} +fn format_trait(_settings: &Settings, _exprs: &[PreSymbolicExpression]) -> String { + "".to_string() +} + +fn name_and_args( + exprs: &[PreSymbolicExpression], +) -> Option<(&PreSymbolicExpression, &[PreSymbolicExpression])> { + if exprs.len() >= 2 { + Some((&exprs[1], &exprs[2..])) } else { - "".to_string() + None // Return None if there aren't enough items } } -fn indentation_to_string(indentation: &Indentation) -> String { - match indentation { - Indentation::Space(i) => " ".repeat(*i), - Indentation::Tab => "\t".to_string(), +// fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { +// let indentation = indentation_to_string(&settings.indentation); +// let mut acc = "(define-constant ".to_string(); + +// if let Some((name, args)) = exprs +// .get(1) +// .and_then(|f| f.match_list()) +// .and_then(|list| list.split_first()) +// { +// acc.push_str(&display_pse(name)); + +// for arg in args { +// if let Some(list) = arg.match_list() { +// acc.push_str(&format!( +// "\n{}({})", +// indentation, +// format_source_exprs(settings, list, "") +// )); +// } +// } +// acc.push_str("\n)\n"); +// acc.to_owned() +// } else { +// panic!("Expected a valid constant definition") +// } +// } + +fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + let indentation = indentation_to_string(&settings.indentation); + let mut acc = "(define-constant ".to_string(); + + if let Some((name, args)) = name_and_args(exprs) { + acc.push_str(&format!("{} ", display_pse(name))); + + // Access the value from args + if let Some(value) = args.first() { + if let Some(list) = value.match_list() { + acc.push_str(&format!( + "\n{}({})", + indentation, + format_source_exprs(settings, list, "") + )); + } else { + // Handle non-list values (e.g., literals or simple expressions) + acc.push_str(&display_pse(value)); + } + } + + acc.push_str("\n)\n"); + acc.to_owned() + } else { + panic!("Expected a valid constant definition with (name value)") } } +fn format_map(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + let indentation = indentation_to_string(&settings.indentation); + let mut acc = "(define-map (".to_string(); + let name_and_args = exprs.get(1).and_then(|f| f.match_list()).unwrap(); + + if let Some((name, args)) = name_and_args.split_first() { + acc.push_str(&format!("{}", display_pse(name))); -fn format_begin(settings: &Settings, exprs: &[SymbolicExpression]) -> String { + for arg in args { + if let Some(list) = arg.match_list() { + acc.push_str(&format!( + "\n{}{}", + indentation, + format_source_exprs(settings, list, "") + )) + } + } + acc.push_str("\n)\n"); + acc.to_owned() + } else { + String::new() // this is likely an error or unreachable + } +} +// *begin* never on one line +fn format_begin(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { let mut begin_acc = "(begin".to_string(); let indentation = indentation_to_string(&settings.indentation); + for arg in exprs.get(1..).unwrap_or_default() { if let Some(list) = arg.match_list() { begin_acc.push_str(&format!( @@ -157,7 +216,8 @@ fn format_begin(settings: &Settings, exprs: &[SymbolicExpression]) -> String { begin_acc.to_owned() } -fn format_let(settings: &Settings, exprs: &[SymbolicExpression]) -> String { +// *let* never on one line +fn format_let(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { let mut begin_acc = "(let (".to_string(); let indentation = indentation_to_string(&settings.indentation); for arg in exprs.get(1..).unwrap_or_default() { @@ -173,21 +233,63 @@ fn format_let(settings: &Settings, exprs: &[SymbolicExpression]) -> String { begin_acc.to_owned() } -fn format_tuple(settings: &Settings, exprs: &[SymbolicExpression]) -> String { +// * match * +// One line if less than max length (unless the original source has line breaks?) +// Multiple lines if more than max length (should the first arg be on the first line if it fits?) +fn format_match(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + println!("{:?}", exprs); + let mut acc = "(match ".to_string(); + let indentation = indentation_to_string(&settings.indentation); + + if let Some((name, args)) = name_and_args(exprs) { + acc.push_str(&display_pse(name)); + for arg in args.get(1..).unwrap_or_default() { + if let Some(list) = arg.match_list() { + acc.push_str(&format!( + "\n{}({})", + indentation, + format_source_exprs(settings, list, "") + )) + } + } + acc.push_str("\n)"); + acc.to_owned() + } else { + "".to_string() + } +} + +fn format_list(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + println!("here"); + let mut acc = "(".to_string(); + for (i, expr) in exprs[1..].iter().enumerate() { + let value = format_source_exprs(settings, &[expr.clone()], ""); + if i < exprs.len() - 2 { + acc.push_str(&format!("{value} ")); + } else { + acc.push_str(&value.to_string()); + } + } + acc.push(')'); + acc.to_string() +} + +fn format_tuple(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { let mut tuple_acc = "{ ".to_string(); for (i, expr) in exprs[1..].iter().enumerate() { let (key, value) = expr .match_list() .and_then(|list| list.split_first()) .unwrap(); + let fkey = display_pse(key); if i < exprs.len() - 2 { tuple_acc.push_str(&format!( - "{key}: {}, ", + "{fkey}: {}, ", format_source_exprs(settings, value, "") )); } else { tuple_acc.push_str(&format!( - "{key}: {}", + "{fkey}: {}", format_source_exprs(settings, value, "") )); } @@ -196,40 +298,101 @@ fn format_tuple(settings: &Settings, exprs: &[SymbolicExpression]) -> String { tuple_acc.to_string() } -fn format_function(settings: &Settings, exprs: &[SymbolicExpression]) -> String { - let func_type = exprs.first().unwrap(); +// This should panic on most things besides atoms and values. Added this to help +// debugging in the meantime +fn display_pse(pse: &PreSymbolicExpression) -> String { + match pse.pre_expr { + PreSymbolicExpressionType::Atom(ref value) => { + println!("atom: {}", value.as_str()); + value.as_str().trim().to_string() + } + PreSymbolicExpressionType::AtomValue(ref value) => { + println!("atomvalue: {}", value); + value.to_string() + } + PreSymbolicExpressionType::List(ref items) => { + println!("list: {:?}", items); + items.iter().map(display_pse).collect::>().join(" ") + } + PreSymbolicExpressionType::Tuple(ref items) => { + println!("tuple: {:?}", items); + items.iter().map(display_pse).collect::>().join(", ") + } + PreSymbolicExpressionType::SugaredContractIdentifier(ref name) => name.to_string(), + PreSymbolicExpressionType::SugaredFieldIdentifier(ref contract, ref field) => { + format!("{}.{}", contract, field) + } + PreSymbolicExpressionType::FieldIdentifier(ref trait_id) => { + println!("field id: {}", trait_id); + trait_id.to_string() + } + PreSymbolicExpressionType::TraitReference(ref name) => name.to_string(), + PreSymbolicExpressionType::Comment(ref text) => { + // println!("comment: {}", text); + format!(";; {}\n", text.trim()) + } + PreSymbolicExpressionType::Placeholder(ref _placeholder) => { + "".to_string() // Placeholder is for if parsing fails + } + } +} + +// * functions + +// Top level define- should have a line break above and after (except on first line) +// options always on new lines +// Functions Always on multiple lines, even if short +fn format_function(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + let func_type = display_pse(exprs.first().unwrap()); let indentation = indentation_to_string(&settings.indentation); - let name_and_args = exprs.get(1).and_then(|f| f.match_list()).unwrap(); - let mut func_acc = format!("({func_type} ("); + let mut acc = format!("({func_type} ("); - if let Some((name, args)) = name_and_args.split_first() { - func_acc.push_str(&format!("{}", name)); - if args.is_empty() { - func_acc.push(')'); - } else { - for arg in args { - func_acc.push_str(&format!( - "\n{}{}{}", - indentation, - indentation, - format_source_exprs(settings, &[arg.clone()], "") - )); + // function name and arguments + if let Some(def) = exprs.get(1).and_then(|f| f.match_list()) { + if let Some((name, args)) = def.split_first() { + acc.push_str(&display_pse(name)); + for arg in args.iter() { + if let Some(list) = arg.match_list() { + acc.push_str(&format!( + "\n{}{}({})", + indentation, + indentation, + format_source_exprs(settings, list, "") + )) + } else { + acc.push_str(&display_pse(arg)) + } + } + if args.is_empty() { + acc.push(')') + } else { + acc.push_str(&format!("\n{})", indentation)) } - func_acc.push_str(&format!("\n{})", indentation)); + } else { + panic!("can't have a nameless function") } } - for arg in exprs.get(2..).unwrap_or_default() { - if let Some(list) = arg.match_list() { - func_acc.push_str(&format!( + + // function body expressions + for expr in exprs.get(2..).unwrap_or_default() { + if let Some(list) = expr.match_list() { + acc.push_str(&format!( "\n{}({})", indentation, format_source_exprs(settings, list, "") )) } } - func_acc.push_str("\n)"); - func_acc.to_owned() + acc.push_str("\n)\n\n"); + acc.to_owned() +} + +fn indentation_to_string(indentation: &Indentation) -> String { + match indentation { + Indentation::Space(i) => " ".repeat(*i), + Indentation::Tab => "\t".to_string(), + } } #[cfg(test)] mod tests_formatter { @@ -276,6 +439,21 @@ mod tests_formatter { assert_eq!(result, "(define-private (my-func)\n (ok true)\n)"); } + #[test] + fn test_multi_function() { + let src = "(define-public (my-func) (ok true))\n(define-public (my-func2) (ok true))"; + let result = format_with_default(&String::from(src)); + let expected = r#"(define-public (my-func) + (ok true) +) + +(define-public (my-func2) + (ok true) +) + +"#; + assert_eq!(expected, result); + } #[test] fn test_function_args_multiline() { let src = "(define-public (my-func (amount uint) (sender principal)) (ok true))"; @@ -304,6 +482,23 @@ mod tests_formatter { let result = format_with_default(&String::from(src)); assert_eq!(src, result); } + + #[test] + fn test_map() { + let src = "(define-map something { name: (buff 48), namespace: (buff 20) } uint\n)"; + let result = format_with_default(&String::from(src)); + assert_eq!(result, "(define-map something\n uint\n uint)"); + // let src2 = "(define-map something { name: (buff 48), namespace: (buff 20) } uint\n)"; + // let result2 = format_with_default(&String::from(src2)); + // let expected2 = r#"(define-map something + // { + // name: (buff 48), + // namespace: (buff 20) + // } + // uint + // )"#; + // assert_eq!(result2, expected2); + } // #[test] // fn test_end_of_line_comments_max_line_length() { // let src = "(ok true) ;; this is a comment"; From 3c3dcbd271e541ad766035c1d1a2df4fe999bbae Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 2 Dec 2024 07:08:57 -0800 Subject: [PATCH 13/17] push settings into display_pse so we can attempt formatting Tuple --- .../clarinet-format/src/formatter/mod.rs | 165 +++++++++--------- 1 file changed, 87 insertions(+), 78 deletions(-) diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index b73e57570..83d9e2586 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -1,9 +1,5 @@ -use clarity::types::StacksEpochId; -use clarity::vm::ast::{build_ast_with_rules, ASTRules}; use clarity::vm::functions::{define::DefineFunctions, NativeFunctions}; use clarity::vm::representations::{PreSymbolicExpression, PreSymbolicExpressionType}; -use clarity::vm::types::QualifiedContractIdentifier; -use clarity::vm::{ClarityVersion, SymbolicExpression}; pub enum Indentation { Space(usize), @@ -92,7 +88,7 @@ pub fn format_source_exprs( } return format!( "{} {}", - display_pse(expr), + display_pse(settings, expr), format_source_exprs(settings, remaining, acc) ) .trim() @@ -121,39 +117,12 @@ fn name_and_args( } } -// fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { -// let indentation = indentation_to_string(&settings.indentation); -// let mut acc = "(define-constant ".to_string(); - -// if let Some((name, args)) = exprs -// .get(1) -// .and_then(|f| f.match_list()) -// .and_then(|list| list.split_first()) -// { -// acc.push_str(&display_pse(name)); - -// for arg in args { -// if let Some(list) = arg.match_list() { -// acc.push_str(&format!( -// "\n{}({})", -// indentation, -// format_source_exprs(settings, list, "") -// )); -// } -// } -// acc.push_str("\n)\n"); -// acc.to_owned() -// } else { -// panic!("Expected a valid constant definition") -// } -// } - fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { let indentation = indentation_to_string(&settings.indentation); let mut acc = "(define-constant ".to_string(); if let Some((name, args)) = name_and_args(exprs) { - acc.push_str(&format!("{} ", display_pse(name))); + acc.push_str(&format!("{}", display_pse(settings, name))); // Access the value from args if let Some(value) = args.first() { @@ -163,13 +132,16 @@ fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Stri indentation, format_source_exprs(settings, list, "") )); + acc.push_str("\n)"); } else { // Handle non-list values (e.g., literals or simple expressions) - acc.push_str(&display_pse(value)); + acc.push(' '); + acc.push_str(&display_pse(settings, value)); + acc.push(')'); } } - acc.push_str("\n)\n"); + acc.push('\n'); acc.to_owned() } else { panic!("Expected a valid constant definition with (name value)") @@ -177,25 +149,27 @@ fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Stri } fn format_map(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { let indentation = indentation_to_string(&settings.indentation); - let mut acc = "(define-map (".to_string(); - let name_and_args = exprs.get(1).and_then(|f| f.match_list()).unwrap(); - if let Some((name, args)) = name_and_args.split_first() { - acc.push_str(&format!("{}", display_pse(name))); + let mut acc = "(define-map ".to_string(); - for arg in args { - if let Some(list) = arg.match_list() { - acc.push_str(&format!( - "\n{}{}", - indentation, - format_source_exprs(settings, list, "") - )) - } + println!("{:?}", exprs); + if let Some((name, args)) = name_and_args(exprs) { + acc.push_str(&display_pse(settings, name)); + + // Access the value from args + for arg in args.iter() { + println!("{:?}", arg); + acc.push_str(&format!( + "\n{}{}", + indentation, + format_source_exprs(settings, &[arg.clone()], "") + )); } - acc.push_str("\n)\n"); + + acc.push('\n'); acc.to_owned() } else { - String::new() // this is likely an error or unreachable + panic!("define-map without a name is silly") } } // *begin* never on one line @@ -242,7 +216,7 @@ fn format_match(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String let indentation = indentation_to_string(&settings.indentation); if let Some((name, args)) = name_and_args(exprs) { - acc.push_str(&display_pse(name)); + acc.push_str(&display_pse(settings, name)); for arg in args.get(1..).unwrap_or_default() { if let Some(list) = arg.match_list() { acc.push_str(&format!( @@ -260,7 +234,6 @@ fn format_match(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String } fn format_list(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { - println!("here"); let mut acc = "(".to_string(); for (i, expr) in exprs[1..].iter().enumerate() { let value = format_source_exprs(settings, &[expr.clone()], ""); @@ -274,56 +247,83 @@ fn format_list(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { acc.to_string() } +// TupleCons - (tuple (name "something")) +// fn format_tuple_base(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { +// let indentation = indentation_to_string(&settings.indentation); +// let mut acc = "(tuple".to_string(); +// for expr in exprs[1..].iter() { +// let (key, value) = expr +// .match_list() +// .and_then(|list| list.split_first()) +// .unwrap(); +// let fkey = display_pse(settings, key); +// acc.push_str(&format!( +// "\n{}({fkey} {})", +// indentation, +// format_source_exprs(settings, value, "") +// )); +// } +// acc.push_str("\n)"); +// acc.to_string() +// } + fn format_tuple(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { - let mut tuple_acc = "{ ".to_string(); + let indentation = indentation_to_string(&settings.indentation); + let mut acc = "{".to_string(); for (i, expr) in exprs[1..].iter().enumerate() { + // println!("tuple: {:?}", items); let (key, value) = expr .match_list() .and_then(|list| list.split_first()) .unwrap(); - let fkey = display_pse(key); + let fkey = display_pse(settings, key); + println!("key: {}", fkey); if i < exprs.len() - 2 { - tuple_acc.push_str(&format!( - "{fkey}: {}, ", + acc.push_str(&format!( + "\n{}{fkey}: {},", + indentation, format_source_exprs(settings, value, "") )); } else { - tuple_acc.push_str(&format!( - "{fkey}: {}", + acc.push_str(&format!( + "\n{}{fkey}: {}\n", + indentation, format_source_exprs(settings, value, "") )); } } - tuple_acc.push_str(" }"); - tuple_acc.to_string() + acc.push('}'); + acc.to_string() } // This should panic on most things besides atoms and values. Added this to help // debugging in the meantime -fn display_pse(pse: &PreSymbolicExpression) -> String { +fn display_pse(settings: &Settings, pse: &PreSymbolicExpression) -> String { match pse.pre_expr { PreSymbolicExpressionType::Atom(ref value) => { - println!("atom: {}", value.as_str()); + // println!("atom: {}", value.as_str()); value.as_str().trim().to_string() } PreSymbolicExpressionType::AtomValue(ref value) => { - println!("atomvalue: {}", value); + // println!("atomvalue: {}", value); value.to_string() } PreSymbolicExpressionType::List(ref items) => { println!("list: {:?}", items); - items.iter().map(display_pse).collect::>().join(" ") + format_list(settings, items) + // items.iter().map(display_pse).collect::>().join(" ") } PreSymbolicExpressionType::Tuple(ref items) => { - println!("tuple: {:?}", items); - items.iter().map(display_pse).collect::>().join(", ") + // println!("tuple: {:?}", items); + format_tuple(settings, items) + // items.iter().map(display_pse).collect::>().join(", ") } PreSymbolicExpressionType::SugaredContractIdentifier(ref name) => name.to_string(), PreSymbolicExpressionType::SugaredFieldIdentifier(ref contract, ref field) => { format!("{}.{}", contract, field) } PreSymbolicExpressionType::FieldIdentifier(ref trait_id) => { - println!("field id: {}", trait_id); + // println!("field id: {}", trait_id); trait_id.to_string() } PreSymbolicExpressionType::TraitReference(ref name) => name.to_string(), @@ -343,7 +343,7 @@ fn display_pse(pse: &PreSymbolicExpression) -> String { // options always on new lines // Functions Always on multiple lines, even if short fn format_function(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { - let func_type = display_pse(exprs.first().unwrap()); + let func_type = display_pse(settings, exprs.first().unwrap()); let indentation = indentation_to_string(&settings.indentation); let mut acc = format!("({func_type} ("); @@ -351,7 +351,7 @@ fn format_function(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Stri // function name and arguments if let Some(def) = exprs.get(1).and_then(|f| f.match_list()) { if let Some((name, args)) = def.split_first() { - acc.push_str(&display_pse(name)); + acc.push_str(&display_pse(settings, name)); for arg in args.iter() { if let Some(list) = arg.match_list() { acc.push_str(&format!( @@ -361,7 +361,7 @@ fn format_function(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Stri format_source_exprs(settings, list, "") )) } else { - acc.push_str(&display_pse(arg)) + acc.push_str(&display_pse(settings, arg)) } } if args.is_empty() { @@ -418,18 +418,20 @@ mod tests_formatter { let result = format_with_default(&String::from("(ok true)(ok true)")); assert_eq!(result, "(ok true)\n(ok true)"); } + #[test] fn test_tuple_formatter() { - let result = format_with_default(&String::from("{n1:1,n2:2,n3:3}")); - assert_eq!(result, "{ n1: 1, n2: 2, n3: 3 }"); + let result = format_with_default(&String::from("(tuple (n1 1) (n2 2))")); + assert_eq!(result, "{\n n1: 1,\n n2: 2\n}"); + let result = format_with_default(&String::from("{n1: (buff 10), n2: 2}")); + assert_eq!(result, "{\n n1: (buff 10),\n n2: 2\n}"); } #[test] - fn test_function_and_tuple_formatter() { - let src = "(define-private (my-func) (ok { n1: 1, n2: 2, n3: 3 }))"; - let result = format_with_default(&String::from(src)); + fn test_map_formatter() { + let result = format_with_default(&String::from("(define-map a uint {n1: (buff 20)})")); assert_eq!( result, - "(define-private (my-func)\n (ok { n1: 1, n2: 2, n3: 3 })\n)" + "(define-map a\n uint\n {\n n1: (buff 20)\n }\n)" ); } @@ -449,9 +451,7 @@ mod tests_formatter { (define-public (my-func2) (ok true) -) - -"#; +)"#; assert_eq!(expected, result); } #[test] @@ -499,6 +499,15 @@ mod tests_formatter { // )"#; // assert_eq!(result2, expected2); } + #[test] + fn test_constant() { + let src = "(define-constant something 1)"; + let result = format_with_default(&String::from(src)); + assert_eq!(result, "(define-constant something 1)"); + let src2 = "(define-constant something (1 2))"; + let result2 = format_with_default(&String::from(src2)); + assert_eq!(result2, "(define-constant something\n (1 2)\n)"); + } // #[test] // fn test_end_of_line_comments_max_line_length() { // let src = "(ok true) ;; this is a comment"; From af8c35b80d50b62d115e0dda3041f2a5158d8c7d Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 2 Dec 2024 14:20:58 -0800 Subject: [PATCH 14/17] fix map/tuple formatting --- .../clarinet-format/src/formatter/mod.rs | 205 ++++++++++++------ 1 file changed, 133 insertions(+), 72 deletions(-) diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index 83d9e2586..8f7893163 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -1,5 +1,7 @@ use clarity::vm::functions::{define::DefineFunctions, NativeFunctions}; use clarity::vm::representations::{PreSymbolicExpression, PreSymbolicExpressionType}; +use clarity::vm::types::{TupleTypeSignature, TypeSignature}; +use clarity::vm::ClarityName; pub enum Indentation { Space(usize), @@ -43,19 +45,21 @@ impl ClarityFormatter { pub fn format_source_exprs( settings: &Settings, + // previous: &PreSymbolicExpression, expressions: &[PreSymbolicExpression], acc: &str, ) -> String { if let Some((expr, remaining)) = expressions.split_first() { if let Some(list) = expr.match_list() { - // println!("{:?}", list); if let Some(atom_name) = list.split_first().and_then(|(f, _)| f.match_atom()) { let formatted = if let Some(native) = NativeFunctions::lookup_by_name(atom_name) { match native { NativeFunctions::Let => format_let(settings, list), NativeFunctions::Begin => format_begin(settings, list), NativeFunctions::Match => format_match(settings, list), - NativeFunctions::TupleCons => format_tuple(settings, list), + // (tuple (name 1)) + // (Tuple [(PSE)]) + NativeFunctions::TupleCons => format_tuple_cons(settings, list), NativeFunctions::ListCons => format_list(settings, list), _ => format!("({})", format_source_exprs(settings, list, acc)), } @@ -86,9 +90,11 @@ pub fn format_source_exprs( .to_owned(); } } + let current = display_pse(settings, expr); return format!( - "{} {}", - display_pse(settings, expr), + "{}{}{}", + current, + if current.ends_with('\n') { "" } else { " " }, format_source_exprs(settings, remaining, acc) ) .trim() @@ -97,14 +103,17 @@ pub fn format_source_exprs( acc.to_owned() } -fn format_use_trait(_settings: &Settings, _exprs: &[PreSymbolicExpression]) -> String { - "".to_string() +fn format_use_trait(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + // delegates to display_pse + format_source_exprs(settings, exprs, "") } -fn format_impl_trait(_settings: &Settings, _exprs: &[PreSymbolicExpression]) -> String { - "".to_string() +fn format_impl_trait(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + // delegates to display_pse + format_source_exprs(settings, exprs, "") } -fn format_trait(_settings: &Settings, _exprs: &[PreSymbolicExpression]) -> String { - "".to_string() +fn format_trait(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + // delegates to display_pse + format_source_exprs(settings, exprs, "") } fn name_and_args( @@ -148,25 +157,28 @@ fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Stri } } fn format_map(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { - let indentation = indentation_to_string(&settings.indentation); - let mut acc = "(define-map ".to_string(); + let indentation = indentation_to_string(&settings.indentation); - println!("{:?}", exprs); if let Some((name, args)) = name_and_args(exprs) { acc.push_str(&display_pse(settings, name)); - // Access the value from args for arg in args.iter() { - println!("{:?}", arg); - acc.push_str(&format!( - "\n{}{}", - indentation, - format_source_exprs(settings, &[arg.clone()], "") - )); + match &arg.pre_expr { + PreSymbolicExpressionType::Tuple(list) => acc.push_str(&format!( + "\n{}{}", + indentation, + format_key_value_sugar(settings, &list.to_vec()) + )), + _ => acc.push_str(&format!( + "\n{}{}", + indentation, + format_source_exprs(settings, &[arg.clone()], "") + )), + } } - acc.push('\n'); + acc.push_str("\n)"); acc.to_owned() } else { panic!("define-map without a name is silly") @@ -247,47 +259,80 @@ fn format_list(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { acc.to_string() } -// TupleCons - (tuple (name "something")) -// fn format_tuple_base(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { -// let indentation = indentation_to_string(&settings.indentation); -// let mut acc = "(tuple".to_string(); -// for expr in exprs[1..].iter() { -// let (key, value) = expr -// .match_list() -// .and_then(|list| list.split_first()) -// .unwrap(); -// let fkey = display_pse(settings, key); -// acc.push_str(&format!( -// "\n{}({fkey} {})", -// indentation, -// format_source_exprs(settings, value, "") -// )); -// } -// acc.push_str("\n)"); -// acc.to_string() -// } - -fn format_tuple(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { +// used for { n1: 1 } syntax +fn format_key_value_sugar(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { let indentation = indentation_to_string(&settings.indentation); let mut acc = "{".to_string(); - for (i, expr) in exprs[1..].iter().enumerate() { - // println!("tuple: {:?}", items); - let (key, value) = expr - .match_list() - .and_then(|list| list.split_first()) - .unwrap(); - let fkey = display_pse(settings, key); - println!("key: {}", fkey); - if i < exprs.len() - 2 { - acc.push_str(&format!( - "\n{}{fkey}: {},", - indentation, - format_source_exprs(settings, value, "") - )); - } else { + + if exprs.len() > 2 { + for (i, chunk) in exprs.chunks(2).enumerate() { + if let [key, value] = chunk { + let fkey = display_pse(settings, key); + if i + 1 < exprs.len() / 2 { + acc.push_str(&format!( + "\n{}{fkey}: {},", + indentation, + format_source_exprs(settings, &[value.clone()], "") + )); + } else { + acc.push_str(&format!( + "\n{}{fkey}: {}\n", + indentation, + format_source_exprs(settings, &[value.clone()], "") + )); + } + } else { + panic!("Unpaired key values: {:?}", chunk); + } + } + } else { + // for cases where we keep it on the same line with 1 k/v pair + let fkey = display_pse(settings, &exprs[0]); + acc.push_str(&format!( + " {fkey}: {} ", + format_source_exprs(settings, &[exprs[1].clone()], "") + )); + } + acc.push('}'); + acc.to_string() +} + +// used for (tuple (n1 1)) syntax +fn format_key_value(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + let indentation = indentation_to_string(&settings.indentation); + let mut acc = "{".to_string(); + + if exprs.len() > 1 { + for (i, expr) in exprs.iter().enumerate() { + let (key, value) = expr + .match_list() + .and_then(|list| list.split_first()) + .unwrap(); + let fkey = display_pse(settings, key); + if i < exprs.len() - 1 { + acc.push_str(&format!( + "\n{}{fkey}: {},", + indentation, + format_source_exprs(settings, value, "") + )); + } else { + acc.push_str(&format!( + "\n{}{fkey}: {}\n", + indentation, + format_source_exprs(settings, value, "") + )); + } + } + } else { + // for cases where we keep it on the same line with 1 k/v pair + for expr in exprs[0..].iter() { + let (key, value) = expr + .match_list() + .and_then(|list| list.split_first()) + .unwrap(); + let fkey = display_pse(settings, key); acc.push_str(&format!( - "\n{}{fkey}: {}\n", - indentation, + " {fkey}: {} ", format_source_exprs(settings, value, "") )); } @@ -295,6 +340,11 @@ fn format_tuple(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String acc.push('}'); acc.to_string() } +fn format_tuple_cons(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { + // if the kv map is defined with (tuple (c 1)) then we have to strip the + // ClarityName("tuple") out first + format_key_value(settings, &exprs[1..]) +} // This should panic on most things besides atoms and values. Added this to help // debugging in the meantime @@ -315,7 +365,7 @@ fn display_pse(settings: &Settings, pse: &PreSymbolicExpression) -> String { } PreSymbolicExpressionType::Tuple(ref items) => { // println!("tuple: {:?}", items); - format_tuple(settings, items) + format_key_value_sugar(settings, items) // items.iter().map(display_pse).collect::>().join(", ") } PreSymbolicExpressionType::SugaredContractIdentifier(ref name) => name.to_string(), @@ -328,8 +378,7 @@ fn display_pse(settings: &Settings, pse: &PreSymbolicExpression) -> String { } PreSymbolicExpressionType::TraitReference(ref name) => name.to_string(), PreSymbolicExpressionType::Comment(ref text) => { - // println!("comment: {}", text); - format!(";; {}\n", text.trim()) + format!(";; {}\n", text) } PreSymbolicExpressionType::Placeholder(ref _placeholder) => { "".to_string() // Placeholder is for if parsing fails @@ -420,19 +469,24 @@ mod tests_formatter { } #[test] - fn test_tuple_formatter() { + fn test_manual_tuple() { + let result = format_with_default(&String::from("(tuple (n1 1))")); + assert_eq!(result, "{ n1: 1 }"); let result = format_with_default(&String::from("(tuple (n1 1) (n2 2))")); assert_eq!(result, "{\n n1: 1,\n n2: 2\n}"); - let result = format_with_default(&String::from("{n1: (buff 10), n2: 2}")); - assert_eq!(result, "{\n n1: (buff 10),\n n2: 2\n}"); } + #[test] + fn test_key_value_formatter() { + let result = format_with_default(&String::from("{n1: 1, n2: 2}")); + assert_eq!(result, "{\n n1: 1,\n n2: 2\n}"); + } + #[test] fn test_map_formatter() { + // let result = format_with_default(&String::from("(define-map a uint (buff 20))")); + // assert_eq!(result, "(define-map a\n uint\n (buff 20)\n)"); let result = format_with_default(&String::from("(define-map a uint {n1: (buff 20)})")); - assert_eq!( - result, - "(define-map a\n uint\n {\n n1: (buff 20)\n }\n)" - ); + assert_eq!(result, "(define-map a\n uint\n { n1: (buff 20) }\n)"); } #[test] @@ -464,14 +518,21 @@ mod tests_formatter { ); } #[test] - fn test_pre_postcomments_included() { + fn test_pre_comments_included() { let src = ";; this is a pre comment\n(ok true)"; - let result = format_with_default(&String::from(src)); assert_eq!(src, result); + } + #[test] + fn test_inline_comments_included() { + let src = "(ok true) ;; this is an inline comment\n"; + let result = format_with_default(&String::from(src)); + assert_eq!(src, result); + } + #[test] + fn test_postcomments_included() { let src = "(ok true)\n;; this is a post comment"; - let result = format_with_default(&String::from(src)); assert_eq!(src, result); } From b8e4451e9a958f279328bf93f39eeffc2104fd58 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 2 Dec 2024 15:48:53 -0800 Subject: [PATCH 15/17] add nested indentation --- .../clarinet-format/src/formatter/mod.rs | 219 +++++++++++------- 1 file changed, 140 insertions(+), 79 deletions(-) diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index 8f7893163..e2be30f97 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -8,6 +8,15 @@ pub enum Indentation { Tab, } +impl ToString for Indentation { + fn to_string(&self) -> String { + match self { + Indentation::Space(count) => " ".repeat(*count), + Indentation::Tab => "\t".to_string(), + } + } +} + pub struct Settings { pub indentation: Indentation, pub max_line_length: usize, @@ -39,14 +48,14 @@ impl ClarityFormatter { } pub fn format(&mut self, source: &str) -> String { let pse = clarity::vm::ast::parser::v2::parse(source).unwrap(); - format_source_exprs(&self.settings, &pse, "") + format_source_exprs(&self.settings, &pse, "", "") } } pub fn format_source_exprs( settings: &Settings, - // previous: &PreSymbolicExpression, expressions: &[PreSymbolicExpression], + previous_indentation: &str, acc: &str, ) -> String { if let Some((expr, remaining)) = expressions.split_first() { @@ -54,14 +63,18 @@ pub fn format_source_exprs( if let Some(atom_name) = list.split_first().and_then(|(f, _)| f.match_atom()) { let formatted = if let Some(native) = NativeFunctions::lookup_by_name(atom_name) { match native { - NativeFunctions::Let => format_let(settings, list), - NativeFunctions::Begin => format_begin(settings, list), - NativeFunctions::Match => format_match(settings, list), + NativeFunctions::Let => format_let(settings, list, previous_indentation), + NativeFunctions::Begin => { + format_begin(settings, list, previous_indentation) + } + NativeFunctions::Match => { + format_match(settings, list, previous_indentation) + } // (tuple (name 1)) // (Tuple [(PSE)]) NativeFunctions::TupleCons => format_tuple_cons(settings, list), NativeFunctions::ListCons => format_list(settings, list), - _ => format!("({})", format_source_exprs(settings, list, acc)), + _ => format!("({})", format_source_exprs(settings, list, "", acc)), } } else if let Some(define) = DefineFunctions::lookup_by_name(atom_name) { match define { @@ -71,31 +84,37 @@ pub fn format_source_exprs( DefineFunctions::Constant => format_constant(settings, list), DefineFunctions::UseTrait => format_use_trait(settings, list), DefineFunctions::Trait => format_trait(settings, list), - DefineFunctions::Map => format_map(settings, list), + DefineFunctions::Map => format_map(settings, list, previous_indentation), DefineFunctions::ImplTrait => format_impl_trait(settings, list), // DefineFunctions::PersistedVariable // DefineFunctions::FungibleToken // DefineFunctions::NonFungibleToken - _ => format!("({})", format_source_exprs(settings, list, acc)), + _ => format!( + "({})", + format_source_exprs(settings, list, previous_indentation, acc) + ), } } else { - format!("({})", format_source_exprs(settings, list, acc)) + format!( + "({})", + format_source_exprs(settings, list, previous_indentation, acc) + ) }; return format!( "{formatted}{}", - format_source_exprs(settings, remaining, acc) + format_source_exprs(settings, remaining, previous_indentation, acc) ) .trim() .to_owned(); } } - let current = display_pse(settings, expr); + let current = display_pse(settings, expr, ""); return format!( "{}{}{}", current, if current.ends_with('\n') { "" } else { " " }, - format_source_exprs(settings, remaining, acc) + format_source_exprs(settings, remaining, previous_indentation, acc) ) .trim() .to_owned(); @@ -105,15 +124,15 @@ pub fn format_source_exprs( fn format_use_trait(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { // delegates to display_pse - format_source_exprs(settings, exprs, "") + format_source_exprs(settings, exprs, "", "") } fn format_impl_trait(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { // delegates to display_pse - format_source_exprs(settings, exprs, "") + format_source_exprs(settings, exprs, "", "") } fn format_trait(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { // delegates to display_pse - format_source_exprs(settings, exprs, "") + format_source_exprs(settings, exprs, "", "") } fn name_and_args( @@ -127,11 +146,11 @@ fn name_and_args( } fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { - let indentation = indentation_to_string(&settings.indentation); + let indentation = &settings.indentation.to_string(); let mut acc = "(define-constant ".to_string(); if let Some((name, args)) = name_and_args(exprs) { - acc.push_str(&format!("{}", display_pse(settings, name))); + acc.push_str(&display_pse(settings, name, "")); // Access the value from args if let Some(value) = args.first() { @@ -139,13 +158,13 @@ fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Stri acc.push_str(&format!( "\n{}({})", indentation, - format_source_exprs(settings, list, "") + format_source_exprs(settings, list, "", "") )); acc.push_str("\n)"); } else { // Handle non-list values (e.g., literals or simple expressions) acc.push(' '); - acc.push_str(&display_pse(settings, value)); + acc.push_str(&display_pse(settings, value, "")); acc.push(')'); } } @@ -156,24 +175,30 @@ fn format_constant(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Stri panic!("Expected a valid constant definition with (name value)") } } -fn format_map(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { +fn format_map( + settings: &Settings, + exprs: &[PreSymbolicExpression], + previous_indentation: &str, +) -> String { let mut acc = "(define-map ".to_string(); - let indentation = indentation_to_string(&settings.indentation); + let indentation = &settings.indentation.to_string(); if let Some((name, args)) = name_and_args(exprs) { - acc.push_str(&display_pse(settings, name)); + acc.push_str(&display_pse(settings, name, "")); for arg in args.iter() { match &arg.pre_expr { + // this is hacked in to handle situations where the contents of + // map is a 'tuple' PreSymbolicExpressionType::Tuple(list) => acc.push_str(&format!( "\n{}{}", indentation, - format_key_value_sugar(settings, &list.to_vec()) + format_key_value_sugar(settings, &list.to_vec(), previous_indentation) )), _ => acc.push_str(&format!( "\n{}{}", indentation, - format_source_exprs(settings, &[arg.clone()], "") + format_source_exprs(settings, &[arg.clone()], previous_indentation, "") )), } } @@ -185,33 +210,43 @@ fn format_map(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { } } // *begin* never on one line -fn format_begin(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { +fn format_begin( + settings: &Settings, + exprs: &[PreSymbolicExpression], + previous_indentation: &str, +) -> String { let mut begin_acc = "(begin".to_string(); - let indentation = indentation_to_string(&settings.indentation); + let indentation = &settings.indentation.to_string(); for arg in exprs.get(1..).unwrap_or_default() { if let Some(list) = arg.match_list() { begin_acc.push_str(&format!( - "\n{}({})", + "\n{}{}({})", + previous_indentation, indentation, - format_source_exprs(settings, list, "") + format_source_exprs(settings, list, previous_indentation, "") )) } } - begin_acc.push_str("\n)\n"); + begin_acc.push_str(&format!("\n{})\n", previous_indentation)); begin_acc.to_owned() } // *let* never on one line -fn format_let(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { +fn format_let( + settings: &Settings, + exprs: &[PreSymbolicExpression], + previous_indentation: &str, +) -> String { let mut begin_acc = "(let (".to_string(); - let indentation = indentation_to_string(&settings.indentation); + let indentation = &settings.indentation.to_string(); for arg in exprs.get(1..).unwrap_or_default() { if let Some(list) = arg.match_list() { begin_acc.push_str(&format!( - "\n{}({})", + "\n{}{}({})", + previous_indentation, indentation, - format_source_exprs(settings, list, "") + format_source_exprs(settings, list, previous_indentation, "") )) } } @@ -222,19 +257,23 @@ fn format_let(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { // * match * // One line if less than max length (unless the original source has line breaks?) // Multiple lines if more than max length (should the first arg be on the first line if it fits?) -fn format_match(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { - println!("{:?}", exprs); +fn format_match( + settings: &Settings, + exprs: &[PreSymbolicExpression], + previous_indentation: &str, +) -> String { let mut acc = "(match ".to_string(); - let indentation = indentation_to_string(&settings.indentation); + let indentation = &settings.indentation.to_string(); if let Some((name, args)) = name_and_args(exprs) { - acc.push_str(&display_pse(settings, name)); + acc.push_str(&display_pse(settings, name, "")); for arg in args.get(1..).unwrap_or_default() { if let Some(list) = arg.match_list() { acc.push_str(&format!( - "\n{}({})", + "\n{}{}({})", indentation, - format_source_exprs(settings, list, "") + previous_indentation, + format_source_exprs(settings, list, previous_indentation, "") )) } } @@ -248,7 +287,7 @@ fn format_match(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String fn format_list(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { let mut acc = "(".to_string(); for (i, expr) in exprs[1..].iter().enumerate() { - let value = format_source_exprs(settings, &[expr.clone()], ""); + let value = format_source_exprs(settings, &[expr.clone()], "", ""); if i < exprs.len() - 2 { acc.push_str(&format!("{value} ")); } else { @@ -260,25 +299,29 @@ fn format_list(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { } // used for { n1: 1 } syntax -fn format_key_value_sugar(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { - let indentation = indentation_to_string(&settings.indentation); +fn format_key_value_sugar( + settings: &Settings, + exprs: &[PreSymbolicExpression], + previous_indentation: &str, +) -> String { + let indentation = &settings.indentation.to_string(); let mut acc = "{".to_string(); if exprs.len() > 2 { for (i, chunk) in exprs.chunks(2).enumerate() { if let [key, value] = chunk { - let fkey = display_pse(settings, key); + let fkey = display_pse(settings, key, ""); if i + 1 < exprs.len() / 2 { acc.push_str(&format!( "\n{}{fkey}: {},", indentation, - format_source_exprs(settings, &[value.clone()], "") + format_source_exprs(settings, &[value.clone()], previous_indentation, "") )); } else { acc.push_str(&format!( "\n{}{fkey}: {}\n", indentation, - format_source_exprs(settings, &[value.clone()], "") + format_source_exprs(settings, &[value.clone()], previous_indentation, "") )); } } else { @@ -287,10 +330,10 @@ fn format_key_value_sugar(settings: &Settings, exprs: &[PreSymbolicExpression]) } } else { // for cases where we keep it on the same line with 1 k/v pair - let fkey = display_pse(settings, &exprs[0]); + let fkey = display_pse(settings, &exprs[0], ""); acc.push_str(&format!( " {fkey}: {} ", - format_source_exprs(settings, &[exprs[1].clone()], "") + format_source_exprs(settings, &[exprs[1].clone()], previous_indentation, "") )); } acc.push('}'); @@ -298,8 +341,12 @@ fn format_key_value_sugar(settings: &Settings, exprs: &[PreSymbolicExpression]) } // used for (tuple (n1 1)) syntax -fn format_key_value(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { - let indentation = indentation_to_string(&settings.indentation); +fn format_key_value( + settings: &Settings, + exprs: &[PreSymbolicExpression], + previous_indentation: &str, +) -> String { + let indentation = &settings.indentation.to_string(); let mut acc = "{".to_string(); if exprs.len() > 1 { @@ -308,18 +355,18 @@ fn format_key_value(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Str .match_list() .and_then(|list| list.split_first()) .unwrap(); - let fkey = display_pse(settings, key); + let fkey = display_pse(settings, key, previous_indentation); if i < exprs.len() - 1 { acc.push_str(&format!( "\n{}{fkey}: {},", indentation, - format_source_exprs(settings, value, "") + format_source_exprs(settings, value, previous_indentation, "") )); } else { acc.push_str(&format!( "\n{}{fkey}: {}\n", indentation, - format_source_exprs(settings, value, "") + format_source_exprs(settings, value, previous_indentation, "") )); } } @@ -330,10 +377,10 @@ fn format_key_value(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Str .match_list() .and_then(|list| list.split_first()) .unwrap(); - let fkey = display_pse(settings, key); + let fkey = display_pse(settings, key, previous_indentation); acc.push_str(&format!( " {fkey}: {} ", - format_source_exprs(settings, value, "") + format_source_exprs(settings, value, &settings.indentation.to_string(), "") )); } } @@ -343,12 +390,16 @@ fn format_key_value(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Str fn format_tuple_cons(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { // if the kv map is defined with (tuple (c 1)) then we have to strip the // ClarityName("tuple") out first - format_key_value(settings, &exprs[1..]) + format_key_value(settings, &exprs[1..], "") } // This should panic on most things besides atoms and values. Added this to help // debugging in the meantime -fn display_pse(settings: &Settings, pse: &PreSymbolicExpression) -> String { +fn display_pse( + settings: &Settings, + pse: &PreSymbolicExpression, + previous_indentation: &str, +) -> String { match pse.pre_expr { PreSymbolicExpressionType::Atom(ref value) => { // println!("atom: {}", value.as_str()); @@ -359,13 +410,12 @@ fn display_pse(settings: &Settings, pse: &PreSymbolicExpression) -> String { value.to_string() } PreSymbolicExpressionType::List(ref items) => { - println!("list: {:?}", items); format_list(settings, items) // items.iter().map(display_pse).collect::>().join(" ") } PreSymbolicExpressionType::Tuple(ref items) => { // println!("tuple: {:?}", items); - format_key_value_sugar(settings, items) + format_key_value_sugar(settings, items, previous_indentation) // items.iter().map(display_pse).collect::>().join(", ") } PreSymbolicExpressionType::SugaredContractIdentifier(ref name) => name.to_string(), @@ -392,25 +442,30 @@ fn display_pse(settings: &Settings, pse: &PreSymbolicExpression) -> String { // options always on new lines // Functions Always on multiple lines, even if short fn format_function(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { - let func_type = display_pse(settings, exprs.first().unwrap()); - let indentation = indentation_to_string(&settings.indentation); + let func_type = display_pse(settings, exprs.first().unwrap(), ""); + let indentation = &settings.indentation.to_string(); let mut acc = format!("({func_type} ("); // function name and arguments if let Some(def) = exprs.get(1).and_then(|f| f.match_list()) { if let Some((name, args)) = def.split_first() { - acc.push_str(&display_pse(settings, name)); + acc.push_str(&display_pse(settings, name, "")); for arg in args.iter() { if let Some(list) = arg.match_list() { acc.push_str(&format!( "\n{}{}({})", indentation, indentation, - format_source_exprs(settings, list, "") + format_source_exprs(settings, list, &settings.indentation.to_string(), "") )) } else { - acc.push_str(&display_pse(settings, arg)) + acc.push_str(&format_source_exprs( + settings, + &[arg.clone()], + &settings.indentation.to_string(), + "", + )) } } if args.is_empty() { @@ -425,13 +480,16 @@ fn format_function(settings: &Settings, exprs: &[PreSymbolicExpression]) -> Stri // function body expressions for expr in exprs.get(2..).unwrap_or_default() { - if let Some(list) = expr.match_list() { - acc.push_str(&format!( - "\n{}({})", - indentation, - format_source_exprs(settings, list, "") - )) - } + acc.push_str(&format!( + "\n{}{}", + indentation, + format_source_exprs( + settings, + &[expr.clone()], + &settings.indentation.to_string(), + "" + ) + )) } acc.push_str("\n)\n\n"); acc.to_owned() @@ -546,9 +604,12 @@ mod tests_formatter { #[test] fn test_map() { - let src = "(define-map something { name: (buff 48), namespace: (buff 20) } uint\n)"; + let src = "(define-map something { name: (buff 48), a: uint } uint\n)"; let result = format_with_default(&String::from(src)); - assert_eq!(result, "(define-map something\n uint\n uint)"); + assert_eq!( + result, + "(define-map something\n {\n name: (buff 48),\n a: uint\n }\n uint)" + ); // let src2 = "(define-map something { name: (buff 48), namespace: (buff 20) } uint\n)"; // let result2 = format_with_default(&String::from(src2)); // let expected2 = r#"(define-map something @@ -569,14 +630,7 @@ mod tests_formatter { let result2 = format_with_default(&String::from(src2)); assert_eq!(result2, "(define-constant something\n (1 2)\n)"); } - // #[test] - // fn test_end_of_line_comments_max_line_length() { - // let src = "(ok true) ;; this is a comment"; - // let result = format_with(&String::from(src), Settings::new(Indentation::Space(2), 9)); - // let expected = ";; this is a comment\n(ok true)"; - // assert_eq!(result, expected); - // } #[test] fn test_begin_never_one_line() { let src = "(begin (ok true))"; @@ -584,6 +638,13 @@ mod tests_formatter { assert_eq!(result, "(begin\n (ok true)\n)"); } + #[test] + fn test_begin() { + let src = "(begin (+ 1 1) (ok true))"; + let result = format_with_default(&String::from(src)); + assert_eq!(result, "(begin\n (+ 1 1)\n (ok true)\n)"); + } + #[test] fn test_custom_tab_setting() { let src = "(begin (ok true))"; From 9017ce512e6fa52d2fd23f887b8ee1adc96ded9f Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 2 Dec 2024 15:55:24 -0800 Subject: [PATCH 16/17] fix format_map --- .../clarinet-format/src/formatter/mod.rs | 73 +++++++++---------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index e2be30f97..c4965d8e2 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -191,19 +191,21 @@ fn format_map( // this is hacked in to handle situations where the contents of // map is a 'tuple' PreSymbolicExpressionType::Tuple(list) => acc.push_str(&format!( - "\n{}{}", + "\n{}{}{}", + previous_indentation, indentation, - format_key_value_sugar(settings, &list.to_vec(), previous_indentation) + format_key_value_sugar(settings, &list.to_vec(), indentation) )), _ => acc.push_str(&format!( - "\n{}{}", + "\n{}{}{}", + previous_indentation, indentation, - format_source_exprs(settings, &[arg.clone()], previous_indentation, "") + format_source_exprs(settings, &[arg.clone()], indentation, "") )), } } - acc.push_str("\n)"); + acc.push_str(&format!("\n{})\n", previous_indentation)); acc.to_owned() } else { panic!("define-map without a name is silly") @@ -307,19 +309,23 @@ fn format_key_value_sugar( let indentation = &settings.indentation.to_string(); let mut acc = "{".to_string(); + // TODO this logic depends on comments not screwing up the even numbered + // chunkable attrs if exprs.len() > 2 { for (i, chunk) in exprs.chunks(2).enumerate() { if let [key, value] = chunk { let fkey = display_pse(settings, key, ""); if i + 1 < exprs.len() / 2 { acc.push_str(&format!( - "\n{}{fkey}: {},", + "\n{}{}{fkey}: {},\n", + previous_indentation, indentation, format_source_exprs(settings, &[value.clone()], previous_indentation, "") )); } else { acc.push_str(&format!( - "\n{}{fkey}: {}\n", + "{}{}{fkey}: {}\n", + previous_indentation, indentation, format_source_exprs(settings, &[value.clone()], previous_indentation, "") )); @@ -330,12 +336,15 @@ fn format_key_value_sugar( } } else { // for cases where we keep it on the same line with 1 k/v pair - let fkey = display_pse(settings, &exprs[0], ""); + let fkey = display_pse(settings, &exprs[0], previous_indentation); acc.push_str(&format!( " {fkey}: {} ", format_source_exprs(settings, &[exprs[1].clone()], previous_indentation, "") )); } + if exprs.len() > 2 { + acc.push_str(previous_indentation); + } acc.push('}'); acc.to_string() } @@ -520,11 +529,6 @@ mod tests_formatter { let result = format_with_default(&String::from("( ok true )")); assert_eq!(result, "(ok true)"); } - #[test] - fn test_two_expr_formatter() { - let result = format_with_default(&String::from("(ok true)(ok true)")); - assert_eq!(result, "(ok true)\n(ok true)"); - } #[test] fn test_manual_tuple() { @@ -533,20 +537,6 @@ mod tests_formatter { let result = format_with_default(&String::from("(tuple (n1 1) (n2 2))")); assert_eq!(result, "{\n n1: 1,\n n2: 2\n}"); } - #[test] - fn test_key_value_formatter() { - let result = format_with_default(&String::from("{n1: 1, n2: 2}")); - assert_eq!(result, "{\n n1: 1,\n n2: 2\n}"); - } - - #[test] - fn test_map_formatter() { - // let result = format_with_default(&String::from("(define-map a uint (buff 20))")); - // assert_eq!(result, "(define-map a\n uint\n (buff 20)\n)"); - let result = format_with_default(&String::from("(define-map a uint {n1: (buff 20)})")); - assert_eq!(result, "(define-map a\n uint\n { n1: (buff 20) }\n)"); - } - #[test] fn test_function_formatter() { let result = format_with_default(&String::from("(define-private (my-func) (ok true))")); @@ -604,23 +594,28 @@ mod tests_formatter { #[test] fn test_map() { - let src = "(define-map something { name: (buff 48), a: uint } uint\n)"; + // let result = format_with_default(&String::from("(define-map a uint (buff 20))")); + // assert_eq!(result, "(define-map a\n uint\n (buff 20)\n)"); + let result = format_with_default(&String::from("(define-map a uint {n1: (buff 20)})")); + assert_eq!(result, "(define-map a\n uint\n { n1: (buff 20) }\n)"); + let src = "(define-map something { name: (buff 48), a: uint } uint)"; let result = format_with_default(&String::from(src)); assert_eq!( result, - "(define-map something\n {\n name: (buff 48),\n a: uint\n }\n uint)" + "(define-map something\n {\n name: (buff 48),\n a: uint\n }\n uint\n)" ); - // let src2 = "(define-map something { name: (buff 48), namespace: (buff 20) } uint\n)"; - // let result2 = format_with_default(&String::from(src2)); - // let expected2 = r#"(define-map something - // { - // name: (buff 48), - // namespace: (buff 20) - // } - // uint - // )"#; - // assert_eq!(result2, expected2); } + + #[test] + fn test_key_value_sugar() { + let src = "{name: (buff 48)}"; + let result = format_with_default(&String::from(src)); + assert_eq!(result, "{ name: (buff 48) }"); + let src = "{ name: (buff 48), a: uint }"; + let result = format_with_default(&String::from(src)); + assert_eq!(result, "{\n name: (buff 48),\n a: uint\n}"); + } + #[test] fn test_constant() { let src = "(define-constant something 1)"; From 86016fdc8618c06636bf9e258886ab08c38a9361 Mon Sep 17 00:00:00 2001 From: "brady.ouren" Date: Mon, 2 Dec 2024 18:17:58 -0800 Subject: [PATCH 17/17] fix match and let formatting --- .../clarinet-format/src/formatter/mod.rs | 96 +++++++++++++------ 1 file changed, 68 insertions(+), 28 deletions(-) diff --git a/components/clarinet-format/src/formatter/mod.rs b/components/clarinet-format/src/formatter/mod.rs index c4965d8e2..42cdc46ae 100644 --- a/components/clarinet-format/src/formatter/mod.rs +++ b/components/clarinet-format/src/formatter/mod.rs @@ -240,25 +240,33 @@ fn format_let( exprs: &[PreSymbolicExpression], previous_indentation: &str, ) -> String { - let mut begin_acc = "(let (".to_string(); + let mut acc = "(let (".to_string(); let indentation = &settings.indentation.to_string(); - for arg in exprs.get(1..).unwrap_or_default() { - if let Some(list) = arg.match_list() { - begin_acc.push_str(&format!( - "\n{}{}({})", + if let Some(args) = exprs[1].match_list() { + for arg in args.iter() { + acc.push_str(&format!( + "\n{}{}{}", previous_indentation, indentation, - format_source_exprs(settings, list, previous_indentation, "") + format_source_exprs(settings, &[arg.clone()], previous_indentation, "") )) } } - begin_acc.push_str("\n) \n"); - begin_acc.to_owned() + acc.push_str(&format!("\n{})", previous_indentation)); + for e in exprs.get(2..).unwrap_or_default() { + acc.push_str(&format!( + "\n{}{}{}", + previous_indentation, + indentation, + format_source_exprs(settings, &[e.clone()], previous_indentation, "") + )) + } + acc.push_str(&format!("\n{})", previous_indentation)); + acc.to_owned() } // * match * -// One line if less than max length (unless the original source has line breaks?) -// Multiple lines if more than max length (should the first arg be on the first line if it fits?) +// always multiple lines fn format_match( settings: &Settings, exprs: &[PreSymbolicExpression], @@ -267,23 +275,34 @@ fn format_match( let mut acc = "(match ".to_string(); let indentation = &settings.indentation.to_string(); - if let Some((name, args)) = name_and_args(exprs) { - acc.push_str(&display_pse(settings, name, "")); - for arg in args.get(1..).unwrap_or_default() { - if let Some(list) = arg.match_list() { - acc.push_str(&format!( - "\n{}{}({})", - indentation, - previous_indentation, - format_source_exprs(settings, list, previous_indentation, "") - )) - } - } - acc.push_str("\n)"); - acc.to_owned() + acc.push_str(&display_pse(settings, &exprs[1], "").to_string()); + // first branch. some or ok binding + acc.push_str(&format!( + "\n{}{}{} {}", + previous_indentation, + indentation, + display_pse(settings, &exprs[2], previous_indentation), + format_source_exprs(settings, &[exprs[3].clone()], previous_indentation, "") + )); + // second branch. none or err binding + if let Some(some_branch) = exprs[4].match_list() { + acc.push_str(&format!( + "\n{}{}({})", + previous_indentation, + indentation, + format_source_exprs(settings, some_branch, previous_indentation, "") + )); } else { - "".to_string() + acc.push_str(&format!( + "\n{}{}{} {}", + previous_indentation, + indentation, + display_pse(settings, &exprs[4], previous_indentation), + format_source_exprs(settings, &[exprs[5].clone()], previous_indentation, "") + )); } + acc.push_str(&format!("\n{})", previous_indentation)); + acc.to_owned() } fn format_list(settings: &Settings, exprs: &[PreSymbolicExpression]) -> String { @@ -594,9 +613,8 @@ mod tests_formatter { #[test] fn test_map() { - // let result = format_with_default(&String::from("(define-map a uint (buff 20))")); - // assert_eq!(result, "(define-map a\n uint\n (buff 20)\n)"); - let result = format_with_default(&String::from("(define-map a uint {n1: (buff 20)})")); + let src = "(define-map a uint {n1: (buff 20)})"; + let result = format_with_default(&String::from(src)); assert_eq!(result, "(define-map a\n uint\n { n1: (buff 20) }\n)"); let src = "(define-map something { name: (buff 48), a: uint } uint)"; let result = format_with_default(&String::from(src)); @@ -606,6 +624,28 @@ mod tests_formatter { ); } + #[test] + fn test_let() { + let src = "(let ((a 1) (b 2)) (+ a b))"; + let result = format_with_default(&String::from(src)); + let expected = "(let (\n (a 1)\n (b 2)\n)\n (+ a b)\n)"; + assert_eq!(expected, result); + } + + #[test] + fn test_option_match() { + let src = "(match opt value (ok (handle-new-value value)) (ok 1))"; + let result = format_with_default(&String::from(src)); + let expected = "(match opt\n value (ok (handle-new-value value))\n (ok 1)\n)"; + assert_eq!(result, expected); + } + #[test] + fn test_response_match() { + let src = "(match x value (ok (+ to-add value)) err-value (err err-value))"; + let result = format_with_default(&String::from(src)); + let expected = "(match x\n value (ok (+ to-add value))\n err-value (err err-value)\n)"; + assert_eq!(result, expected); + } #[test] fn test_key_value_sugar() { let src = "{name: (buff 48)}";