use cosmwasm_std::{
Addr, Api, BlockInfo, CanonicalAddr, ContractInfo, Empty, Env, Order, OwnedDeps, Querier,
RecoverPubkeyError, StdError, StdResult, Storage, Timestamp, Uint128, VerificationError,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use std::collections::HashMap;
use std::marker::PhantomData;
pub mod abi;
mod memory_storage;
pub use memory_storage::MemoryStorage;
#[cfg(feature = "macros")]
pub use ownable_std_macros::*;
const CANONICAL_LENGTH: usize = 54;
pub fn create_env() -> Env {
create_ownable_env(String::new(), None)
}
pub fn create_ownable_env(chain_id: impl Into<String>, time: Option<Timestamp>) -> Env {
Env {
block: BlockInfo {
height: 0,
time: time.unwrap_or_else(|| Timestamp::from_seconds(0)),
chain_id: chain_id.into(),
},
contract: ContractInfo {
address: Addr::unchecked(""),
},
transaction: None,
}
}
pub fn package_title_from_name(name: &str) -> String {
name.trim_start_matches("ownable-")
.split(['-', '_'])
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(first) => format!("{}{}", first.to_ascii_uppercase(), chars.as_str()),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
pub trait OwnerAddress {
fn owner_address(&self) -> &Addr;
}
impl OwnerAddress for Addr {
fn owner_address(&self) -> &Addr {
self
}
}
impl OwnerAddress for OwnableInfo {
fn owner_address(&self) -> &Addr {
&self.owner
}
}
pub fn ensure_owner<T, E>(
owner: &T,
sender: &Addr,
unauthorized: impl FnOnce() -> E,
) -> Result<(), E>
where
T: OwnerAddress + ?Sized,
{
if sender == owner.owner_address() {
Ok(())
} else {
Err(unauthorized())
}
}
pub fn load_owned_deps(
state_dump: Option<IdbStateDump>,
) -> OwnedDeps<MemoryStorage, EmptyApi, EmptyQuerier, Empty> {
match state_dump {
None => OwnedDeps {
storage: MemoryStorage::default(),
api: EmptyApi::default(),
querier: EmptyQuerier::default(),
custom_query_type: PhantomData,
},
Some(dump) => {
let idb_storage = IdbStorage::load(dump);
OwnedDeps {
storage: idb_storage.storage,
api: EmptyApi::default(),
querier: EmptyQuerier::default(),
custom_query_type: PhantomData,
}
}
}
}
pub fn get_random_color(hash: String) -> String {
let (red, green, blue) = derive_rgb_values(hash);
rgb_hex(red, green, blue)
}
pub fn derive_rgb_values(hash: String) -> (u8, u8, u8) {
let mut s = hash.trim().trim_start_matches("0x").to_string();
if s.len() % 2 == 1 {
s.insert(0, '0');
}
match hex::decode(&s) {
Ok(mut bytes) => {
bytes.reverse();
let r = *bytes.get(0).unwrap_or(&0);
let g = *bytes.get(1).unwrap_or(&0);
let b = *bytes.get(2).unwrap_or(&0);
(r, g, b)
}
Err(_) => (0, 0, 0),
}
}
pub fn rgb_hex(r: u8, g: u8, b: u8) -> String {
format!("#{:02X}{:02X}{:02X}", r, g, b)
}
pub struct IdbStorage {
pub storage: MemoryStorage,
}
impl IdbStorage {
pub fn load(idb: IdbStateDump) -> Self {
let mut store = IdbStorage {
storage: MemoryStorage::new(),
};
store.load_to_mem_storage(idb);
store
}
pub fn load_to_mem_storage(&mut self, idb_state: IdbStateDump) {
for (k, v) in idb_state.state_dump.into_iter() {
self.storage.set(&k, &v);
}
}
}
#[serde_as]
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct IdbStateDump {
#[serde_as(as = "Vec<(serde_with::Bytes, serde_with::Bytes)>")]
pub state_dump: HashMap<Vec<u8>, Vec<u8>>,
}
impl IdbStateDump {
pub fn from(store: MemoryStorage) -> IdbStateDump {
let mut state: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
for (key, value) in store.range(None, None, Order::Ascending) {
state.insert(key, value);
}
IdbStateDump { state_dump: state }
}
}
#[derive(Copy, Clone)]
pub struct EmptyApi {
canonical_length: usize,
}
impl Default for EmptyApi {
fn default() -> Self {
EmptyApi {
canonical_length: CANONICAL_LENGTH,
}
}
}
impl Api for EmptyApi {
fn addr_validate(&self, human: &str) -> StdResult<Addr> {
self.addr_canonicalize(human).map(|_canonical| ())?;
Ok(Addr::unchecked(human))
}
fn addr_canonicalize(&self, human: &str) -> StdResult<CanonicalAddr> {
if human.len() < 3 {
return Err(StdError::msg("Invalid input: human address too short"));
}
if human.len() > self.canonical_length {
return Err(StdError::msg("Invalid input: human address too long"));
}
let mut out = Vec::from(human);
out.resize(self.canonical_length, 0x00);
Ok(out.into())
}
fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult<Addr> {
if canonical.len() != self.canonical_length {
return Err(StdError::msg(
"Invalid input: canonical address length not correct",
));
}
let tmp: Vec<u8> = canonical.clone().into();
let trimmed = tmp.into_iter().filter(|&x| x != 0x00).collect();
let human = String::from_utf8(trimmed)?;
Ok(Addr::unchecked(human))
}
fn secp256k1_verify(
&self,
_message_hash: &[u8],
_signature: &[u8],
_public_key: &[u8],
) -> Result<bool, VerificationError> {
Err(VerificationError::unknown_err(0))
}
fn secp256k1_recover_pubkey(
&self,
_message_hash: &[u8],
_signature: &[u8],
_recovery_param: u8,
) -> Result<Vec<u8>, RecoverPubkeyError> {
Err(RecoverPubkeyError::unknown_err(0))
}
fn ed25519_verify(
&self,
_message: &[u8],
_signature: &[u8],
_public_key: &[u8],
) -> Result<bool, VerificationError> {
Ok(true)
}
fn ed25519_batch_verify(
&self,
_messages: &[&[u8]],
_signatures: &[&[u8]],
_public_keys: &[&[u8]],
) -> Result<bool, VerificationError> {
Ok(true)
}
fn debug(&self, message: &str) {
println!("{}", message);
}
}
#[derive(Default)]
pub struct EmptyQuerier {}
impl Querier for EmptyQuerier {
fn raw_query(&self, _bin_request: &[u8]) -> cosmwasm_std::QuerierResult {
todo!()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
enum TestError {
Unauthorized(&'static str),
}
#[test]
fn ensure_owner_accepts_owner_addr() {
let owner = Addr::unchecked("owner");
let sender = Addr::unchecked("owner");
let result = ensure_owner(&owner, &sender, || TestError::Unauthorized("forbidden"));
assert_eq!(result, Ok(()));
}
#[test]
fn ensure_owner_rejects_non_owner_addr() {
let owner = Addr::unchecked("owner");
let sender = Addr::unchecked("not-owner");
let result = ensure_owner(&owner, &sender, || TestError::Unauthorized("forbidden"));
assert_eq!(result, Err(TestError::Unauthorized("forbidden")));
}
#[test]
fn ensure_owner_accepts_owner_struct() {
let ownable_info = OwnableInfo {
owner: Addr::unchecked("owner"),
issuer: Addr::unchecked("issuer"),
ownable_type: Some("basic".to_string()),
};
let sender = Addr::unchecked("owner");
let result = ensure_owner(&ownable_info, &sender, || {
TestError::Unauthorized("forbidden")
});
assert_eq!(result, Ok(()));
}
#[test]
fn rgb_hex_formats_correctly() {
assert_eq!(rgb_hex(0, 0, 0), "#000000");
assert_eq!(rgb_hex(255, 255, 255), "#FFFFFF");
assert_eq!(rgb_hex(255, 0, 0), "#FF0000");
assert_eq!(rgb_hex(0, 128, 255), "#0080FF");
}
#[test]
fn derive_rgb_values_reads_last_three_bytes_reversed() {
assert_eq!(derive_rgb_values("010203".to_string()), (3, 2, 1));
}
#[test]
fn derive_rgb_values_strips_0x_prefix() {
assert_eq!(
derive_rgb_values("0x010203".to_string()),
derive_rgb_values("010203".to_string())
);
}
#[test]
fn derive_rgb_values_pads_odd_length_input() {
assert_eq!(derive_rgb_values("abc".to_string()), (0xbc, 0x0a, 0));
}
#[test]
fn derive_rgb_values_returns_zeros_for_invalid_hex() {
assert_eq!(derive_rgb_values("xyz".to_string()), (0, 0, 0));
}
#[test]
fn derive_rgb_values_returns_zeros_for_empty_input() {
assert_eq!(derive_rgb_values("".to_string()), (0, 0, 0));
}
#[test]
fn derive_rgb_values_uses_last_three_bytes_of_long_input() {
assert_eq!(
derive_rgb_values("aabbccdd11223344".to_string()),
(0x44, 0x33, 0x22)
);
}
#[test]
fn get_random_color_returns_hash_prefixed_hex() {
let color = get_random_color("010203".to_string());
assert!(color.starts_with('#'));
assert_eq!(color.len(), 7);
}
#[test]
fn get_random_color_is_deterministic() {
let hash = "deadbeef".to_string();
assert_eq!(get_random_color(hash.clone()), get_random_color(hash));
}
#[test]
fn idb_state_dump_round_trips_through_storage() {
let mut storage = MemoryStorage::new();
storage.set(b"key1", b"value1");
storage.set(b"key2", b"value2");
let dump = IdbStateDump::from(storage);
assert_eq!(
dump.state_dump.get(b"key1".as_ref()),
Some(&b"value1".to_vec())
);
assert_eq!(
dump.state_dump.get(b"key2".as_ref()),
Some(&b"value2".to_vec())
);
}
#[test]
fn idb_storage_load_restores_all_keys() {
let mut storage = MemoryStorage::new();
storage.set(b"foo", b"bar");
storage.set(b"baz", b"qux");
let dump = IdbStateDump::from(storage);
let loaded = IdbStorage::load(dump);
assert_eq!(loaded.storage.get(b"foo"), Some(b"bar".to_vec()));
assert_eq!(loaded.storage.get(b"baz"), Some(b"qux".to_vec()));
}
#[test]
fn idb_state_dump_empty_storage_produces_empty_map() {
let storage = MemoryStorage::new();
let dump = IdbStateDump::from(storage);
assert!(dump.state_dump.is_empty());
}
#[test]
fn create_env_produces_default_env() {
let env = create_env();
assert_eq!(env.block.height, 0);
assert_eq!(env.block.chain_id, "");
}
#[test]
fn create_ownable_env_sets_chain_id() {
let env = create_ownable_env("my-chain", None);
assert_eq!(env.block.chain_id, "my-chain");
}
#[test]
fn create_ownable_env_sets_timestamp() {
use cosmwasm_std::Timestamp;
let ts = Timestamp::from_seconds(12345);
let env = create_ownable_env("", Some(ts));
assert_eq!(env.block.time, ts);
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)]
pub struct Metadata {
pub image: Option<String>,
pub image_data: Option<String>,
pub external_url: Option<String>,
pub description: Option<String>,
pub name: Option<String>,
pub background_color: Option<String>,
pub animation_url: Option<String>,
pub youtube_url: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub struct ExternalEventMsg {
pub network: Option<String>,
pub event_type: String,
pub attributes: HashMap<String, String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct OwnableInfo {
pub owner: Addr,
pub issuer: Addr,
pub ownable_type: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct NFT {
pub network: String, pub id: Uint128,
pub address: String, pub lock_service: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InfoResponse {
pub owner: Addr,
pub issuer: Addr,
pub nft: Option<NFT>,
pub ownable_type: Option<String>,
}