Skip to content

Commit

Permalink
Merge pull request #398 from Concordium/derive-macros
Browse files Browse the repository at this point in the history
Procedural macros for creating constant values from strings
  • Loading branch information
abizjak authored Apr 4, 2024
2 parents 670ffab + afba76b commit aae1e44
Show file tree
Hide file tree
Showing 71 changed files with 643 additions and 103 deletions.
1 change: 1 addition & 0 deletions .github/workflows/linter-testing-lib.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ on:
- 'contract-testing/**/*.toml'
- 'rustfmt.toml'
- 'concordium-rust-sdk'
- 'concordium-std-derive'

workflow_dispatch: # allows manual trigger

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
strategy:
matrix:
crates:
- concordium-std-derive/Cargo.toml
- concordium-std/Cargo.toml
- concordium-cis2/Cargo.toml
- examples/voting/Cargo.toml
Expand Down Expand Up @@ -377,6 +378,7 @@ jobs:
sed -i "s/{version = \"10.0\", default-features = false}/{path = \"..\/..\/concordium-std\", default-features = false}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml
sed -i "s/{version = \"6.1\", default-features = false}/{path = \"..\/..\/concordium-cis2\", default-features = false}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml
sed -i "s/concordium-smart-contract-testing = \"4.2\"/concordium-smart-contract-testing = {path = \"..\/..\/contract-testing\"}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml
sed -i "s/concordium-std-derive = \"6.0\"/concordium-std-derive = {path = \"..\/..\/concordium-std-derive\"}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml
diff ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml examples/cis2-nft/Cargo.toml
diff ${{ runner.temp }}/$PROJECT_NAME/src/lib.rs examples/cis2-nft/src/lib.rs
diff ${{ runner.temp }}/$PROJECT_NAME/tests/tests.rs examples/cis2-nft/tests/tests.rs
Expand Down Expand Up @@ -425,6 +427,7 @@ jobs:
sed -i "s/version = \"10.0\", default-features = false/path = \"..\/..\/concordium-std\", version = \"10.0\", default-features = false/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml
sed -i "s/version = \"6.1\", default-features = false/path = \"..\/..\/concordium-cis2\", version = \"6.1\", default-features = false/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml
sed -i "s/concordium-smart-contract-testing = \"4.2\"/concordium-smart-contract-testing = {path = \"..\/..\/contract-testing\"}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml
sed -i "s/concordium-std-derive = \"6.0\"/concordium-std-derive = {path = \"..\/..\/concordium-std-derive\"}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml
diff ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml examples/credential-registry/Cargo.toml
diff ${{ runner.temp }}/$PROJECT_NAME/src/lib.rs examples/credential-registry/src/lib.rs
diff ${{ runner.temp }}/$PROJECT_NAME/tests/tests.rs examples/credential-registry/tests/tests.rs
Expand Down
2 changes: 2 additions & 0 deletions concordium-cis2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased changes

- Bump MSRV to 1.72

## concordium-cis2 6.1.0 (2024-02-22)

- Support version 10 of concordium-std.
Expand Down
1 change: 1 addition & 0 deletions concordium-cis2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "concordium-cis2"
version = "6.1.0"
authors = ["Concordium <[email protected]>"]
rust-version = "1.73"
edition = "2021"
license = "MPL-2.0"
description = "A collection of types for implementing CIS-2 Concordium Token Standard."
Expand Down
14 changes: 14 additions & 0 deletions concordium-std-derive/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

## Unreleased changes

## concordium-std-derive 6.0.0 (2024-04-04)

- Macros for creating constant values from strings for the following types:
- `AccountAddress`
- `ContractAddress`
- `ModuleReference`
- `PublicKeyEcdsaSecp256k1`
- `PublicKeyEd25519`
- `SignatureEcdsaSecp256k1`
- `SignatureEd25519`
24 changes: 24 additions & 0 deletions concordium-std-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "concordium-std-derive"
authors = ["Concordium <[email protected]>"]
version = "6.0.0"
edition = "2021"
rust-version = "1.73"
license = "MPL-2.0"
description = "Procedural macros to generate concordium type constants from strings, that makes writing smart contracts on the Concordium blockchain easier."
homepage = "https://github.com/Concordium/concordium-rust-smart-contracts/"
repository = "https://github.com/Concordium/concordium-rust-smart-contracts/"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true

[dependencies]
concordium-contracts-common = { path = "../concordium-rust-sdk/concordium-base/smart-contracts/contracts-common/concordium-contracts-common", features = [ "derive-serde" ], version = "9" }
syn = "2.0"
proc-macro2 = "1.0"
quote = "1.0"

[dev-dependencies]
trybuild = "1.0"

264 changes: 264 additions & 0 deletions concordium-std-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
extern crate proc_macro;

use concordium_contracts_common::{
AccountAddress, ContractAddress, ModuleReference, PublicKeyEcdsaSecp256k1, PublicKeyEd25519,
SignatureEcdsaSecp256k1, SignatureEd25519,
};
use proc_macro::TokenStream;
use proc_macro2::{Group, Punct, Spacing, Span};
use quote::{quote, ToTokens};
use std::str::FromStr;
use syn::LitStr;

// Helper functions:

// Tokenizes a slice of bytes.
fn tokenize_slice(slice: &[u8]) -> proc_macro2::TokenStream {
let mut t = proc_macro2::TokenStream::new();
for byte in slice {
byte.to_tokens(&mut t);
Punct::new(',', Spacing::Alone).to_tokens(&mut t);
}
Group::new(proc_macro2::Delimiter::Bracket, t).to_token_stream()
}

// Parses the input tokens of a proc macro and returns the given error
// message on parse error.
fn parse_input(item: TokenStream, msg: &str) -> syn::Result<LitStr> {
match syn::parse::<LitStr>(item) {
Ok(string_literal) => Ok(string_literal),
Err(_) => Err(syn::Error::new(Span::call_site(), msg)),
}
}

// Parses the given tokens and transforms them using the provided worker
// function. If parsing fails, then an error with the given message is produced.
fn get_token_res(
item: TokenStream,
msg: &str,
worker_fun: impl FnOnce(String, Span) -> syn::Result<TokenStream>,
) -> syn::Result<TokenStream> {
let input = parse_input(item, msg)?;
worker_fun(input.value(), input.span())
}

// Parses the given tokens, looks up an environment variable with the parsed
// input as key and transforms the corresponding value using the provided worker
// function. If parsing fails, then an error with the given message is produced.
fn get_token_res_env(
item: TokenStream,
msg: &str,
worker_fun: impl FnOnce(String, Span) -> syn::Result<TokenStream>,
) -> syn::Result<TokenStream> {
let input = parse_input(item, msg)?;
let environment_var_value = match std::env::var(input.value()) {
Ok(value) => value,
Err(e) => {
return Err(syn::Error::new(
input.span(),
format!("Environment variable error: {:?}", e),
))
}
};
worker_fun(environment_var_value, input.span())
}

fn unwrap_or_report(res: syn::Result<TokenStream>) -> TokenStream {
res.unwrap_or_else(|e| e.into_compile_error().into())
}

// Worker functions

fn acc_address_worker(str: String, span: Span) -> syn::Result<TokenStream> {
let address = match AccountAddress::from_str(&str) {
Ok(addr) => tokenize_slice(&addr.0),
Err(e) => return Err(syn::Error::new(span, format!("Invalid account address: {}", e))),
};

Ok(quote!(concordium_std::AccountAddress(#address)).into())
}

fn pubkey_ed25519_worker(str: String, span: Span) -> syn::Result<TokenStream> {
let public_key = match PublicKeyEd25519::from_str(&str) {
Ok(pk) => tokenize_slice(&pk.0),
Err(e) => return Err(syn::Error::new(span, format!("Invalid Ed25519 public key: {}", e))),
};

Ok(quote!(concordium_std::PublicKeyEd25519(#public_key)).into())
}

fn pubkey_ecdsa_worker(str: String, span: Span) -> syn::Result<TokenStream> {
let public_key = match PublicKeyEcdsaSecp256k1::from_str(&str) {
Ok(pk) => tokenize_slice(&pk.0),
Err(e) => return Err(syn::Error::new(span, format!("Invalid ECDSA public key: {}", e))),
};

Ok(quote!(concordium_std::PublicKeyEcdsaSecp256k1(#public_key)).into())
}

fn signature_ed25519_worker(str: String, span: Span) -> syn::Result<TokenStream> {
let signature = match SignatureEd25519::from_str(&str) {
Ok(sig) => tokenize_slice(&sig.0),
Err(e) => return Err(syn::Error::new(span, format!("Invalid Ed25519 signature: {}", e))),
};

Ok(quote!(concordium_std::SignatureEd25519(#signature)).into())
}

fn signature_ecdsa_worker(str: String, span: Span) -> syn::Result<TokenStream> {
let signature = match SignatureEcdsaSecp256k1::from_str(&str) {
Ok(sig) => tokenize_slice(&sig.0),
Err(e) => return Err(syn::Error::new(span, format!("Invalid ECDSA signature: {}", e))),
};

Ok(quote!(concordium_std::SignatureEcdsaSecp256k1(#signature)).into())
}

fn contract_address_worker(str: String, span: Span) -> syn::Result<TokenStream> {
let (index, subindex) = match ContractAddress::from_str(&str) {
Ok(con) => (con.index, con.subindex),
Err(e) => return Err(syn::Error::new(span, format!("Invalid contract address: {}", e))),
};

Ok(quote!(concordium_std::ContractAddress::new(#index, #subindex)).into())
}

fn module_ref_worker(str: String, span: Span) -> syn::Result<TokenStream> {
let module_ref = match ModuleReference::from_str(&str) {
Ok(mod_ref) => tokenize_slice(&mod_ref.bytes),
Err(e) => return Err(syn::Error::new(span, format!("Invalid module reference: {}", e))),
};

Ok(quote!(concordium_std::ModuleReference::new(#module_ref)).into())
}

/// Procedural macro for instantiating account addresses.
/// Input must be a valid base58-encoding.
#[proc_macro]
pub fn account_address(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal of a hex-encoded account address";
unwrap_or_report(get_token_res(item, msg, acc_address_worker))
}

/// Procedural macro for instantiating account addresses from an environment
/// variable. Input must be the key of an environment variable whose value is
/// set to a valid base58-encoding of an account address.
#[proc_macro]
pub fn account_address_env(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal of a hex-encoded account address";
unwrap_or_report(get_token_res_env(item, msg, acc_address_worker))
}

/// Procedural macro for instantiating `PublicKeyEd25519` public keys.
/// Input must be a (case-insensitive) hex-encoding and have a length of 64
/// characters representing 32 bytes.
#[proc_macro]
pub fn public_key_ed25519(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal of a hex-encoded ED25519 public key";
unwrap_or_report(get_token_res(item, msg, pubkey_ed25519_worker))
}

/// Procedural macro for instantiating `PublicKeyEd25519` public keys from
/// an environment variable. Input must be the key of an environment variable
/// whose value is set to a hex-encoded public key which must have a length of
/// 64 characters representing 32 bytes.
#[proc_macro]
pub fn public_key_ed25519_env(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal of a hex-encoded ED25519 public key";
unwrap_or_report(get_token_res_env(item, msg, pubkey_ed25519_worker))
}

/// Procedural macro for instantiating `PublicKeyEcdsaSecp256k1` public keys.
/// Input must be a (case-insensitive) hex-encoding and have a length of 66
/// characters representing 33 bytes.
#[proc_macro]
pub fn public_key_ecdsa(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal of a hex-encoded ECDSA public key";
unwrap_or_report(get_token_res(item, msg, pubkey_ecdsa_worker))
}

/// Procedural macro for instantiating `PublicKeyEcdsaSecp256k1` public keys
/// from an environment variable. Input must be the key of an environment
/// variable whose value is set to a hex-encoded public key which must have a
/// length of 66 characters representing 33 bytes.
#[proc_macro]
pub fn public_key_ecdsa_env(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal of a hex-encoded ECDSA public key";
unwrap_or_report(get_token_res_env(item, msg, pubkey_ecdsa_worker))
}

/// Procedural macro for instantiating `SignatureEd25519` signatures.
/// Input must be a (case-insensitive) hex-encoding and have a length of 128
/// characters representing 64 bytes.
#[proc_macro]
pub fn signature_ed25519(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal of a hex-encoded ED25519 signature";
unwrap_or_report(get_token_res(item, msg, signature_ed25519_worker))
}

/// Procedural macro for instantiating `SignatureEd25519` signatures from
/// an environment variable. Input must be the key of an environment variable
/// whose value is set to a hex-encoded signature which must have a length of
/// 128 characters representing 64 bytes.
#[proc_macro]
pub fn signature_ed25519_env(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal of a hex-encoded ED25519 signature";
unwrap_or_report(get_token_res_env(item, msg, signature_ed25519_worker))
}

/// Procedural macro for instantiating `SignatureEcdsaSecp256k1` signatures.
/// Input must be a (case-insensitive) hex-encoding and have a length of 128
/// characters representing 64 bytes.
#[proc_macro]
pub fn signature_ecdsa(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal of a hex-encoded ECDSA signature";
unwrap_or_report(get_token_res(item, msg, signature_ecdsa_worker))
}

/// Procedural macro for instantiating `SignatureEcdsaSecp256k1` signatures from
/// an environment variable. Input must be the key of an environment variable
/// whose value is set to a hex-encoded signature which must have a length of
/// 128 characters representing 64 bytes.
#[proc_macro]
pub fn signature_ecdsa_env(item: TokenStream) -> TokenStream {
let msg = "Expected a string literal a hex-encoded ECDSA signature";
unwrap_or_report(get_token_res_env(item, msg, signature_ecdsa_worker))
}

/// Procedural macro for instantiating contract addresses.
/// Input must be of the form "<index,subindex>", where index and subindex
/// are integers.
#[proc_macro]
pub fn contract_address(item: TokenStream) -> TokenStream {
let msg = "Expected string literal of a contract address in the form of \"<index,subindex>\"";
unwrap_or_report(get_token_res(item, msg, contract_address_worker))
}

/// Procedural macro for instantiating contract addresses from an environment
/// variable. Input must be the key of an environment variable whose value is
/// set to a contract address of the form "<index,subindex>", where index and
/// subindex are integers
#[proc_macro]
pub fn contract_address_env(item: TokenStream) -> TokenStream {
let msg = "Expected string literal of a contract address in the form of \"<index,subindex>\"";
unwrap_or_report(get_token_res_env(item, msg, contract_address_worker))
}

/// Procedural macro for instantiating module references.
/// Input must be a (case-insensitive) hex-encoding and have a length of 64
/// characters representing 32 bytes.
#[proc_macro]
pub fn module_reference(item: TokenStream) -> TokenStream {
let msg = "Expected string literal of a hex-encoded module reference";
unwrap_or_report(get_token_res(item, msg, module_ref_worker))
}

/// Procedural macro for instantiating module references from an environment
/// variable. Input must be the key of an environment variable whose value is
/// set to a hex-encoded module reference which must have a length of 64
/// characters representing 32 bytes.
#[proc_macro]
pub fn module_reference_env(item: TokenStream) -> TokenStream {
let msg = "Expected string literal of a hex-encoded module reference";
unwrap_or_report(get_token_res_env(item, msg, module_ref_worker))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! Ensure that the macros generate compilable code.

use concordium_contracts_common::*;
use concordium_std_derive::*;

const ACC: AccountAddress = account_address!("30Bx2h5Y2veb4hZgAJWPrr8RyQESKm5TjzF3ti1QQ4VSYLwK1G");

fn f() {
println!("{:?}", ACC.to_string());
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: Invalid account address: Invalid Base58Check encoding.
--> tests/test-programs/fail-acc-invalid-base58.rs:6:46
|
6 | const ACC: AccountAddress = account_address!("30Bx2h5Y2veb4hZgAJWPrr8RyQESKm5TjzF3ti1QQ4VSYLwK1G");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! Ensure that the macros generate compilable code.

use concordium_contracts_common::*;
use concordium_std_derive::*;

const ACC: AccountAddress = account_address!("3kBx2h5Y2veb4hZgAJWPrr8RyQESKm5TjzF3ti1QQ4VSYLwK1G", "3kBx2h5Y2veb4hZgAJWPrr8RyQESKm5TjzF3ti1QQ4VSYLwK1G");

fn f() {
println!("{:?}", ACC.to_string());
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
error: Expected a string literal of a hex-encoded account address
--> tests/test-programs/fail-acc-multiple-arguments.rs:6:29
|
6 | ...ss = account_address!("3kBx2h5Y2veb4hZgAJWPrr8RyQESKm5TjzF3ti1QQ4VSYLwK1G", "3kBx2h5Y2veb4hZgAJWPrr8RyQESKm5TjzF3ti1QQ4VSYLwK1G");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `account_address` (in Nightly builds, run with -Z macro-backtrace for more info)
Loading

0 comments on commit aae1e44

Please sign in to comment.