From 16178af767e2b6b9ea1bba06c65db66edc394e62 Mon Sep 17 00:00:00 2001 From: parazyd Date: Tue, 15 Nov 2022 17:26:20 +0100 Subject: [PATCH] contract/money: Initial outline in src/contract/money. --- Cargo.lock | 9 + Cargo.toml | 2 + src/contract/README.md | 1 + src/contract/money/.gitignore | 2 + src/contract/money/Cargo.toml | 18 ++ src/contract/money/Makefile | 37 ++++ src/contract/money/proof/burn.zk | 66 +++++++ src/contract/money/proof/mint.zk | 46 +++++ src/contract/money/src/lib.rs | 299 +++++++++++++++++++++++++++++++ src/contract/money/src/state.rs | 95 ++++++++++ src/sdk/src/crypto/coin.rs | 85 +++++++++ src/sdk/src/crypto/mod.rs | 4 + 12 files changed, 664 insertions(+) create mode 100644 src/contract/README.md create mode 100644 src/contract/money/.gitignore create mode 100644 src/contract/money/Cargo.toml create mode 100644 src/contract/money/Makefile create mode 100644 src/contract/money/proof/burn.zk create mode 100644 src/contract/money/proof/mint.zk create mode 100644 src/contract/money/src/lib.rs create mode 100644 src/contract/money/src/state.rs create mode 100644 src/sdk/src/crypto/coin.rs diff --git a/Cargo.lock b/Cargo.lock index 3e259a92f..3506b0489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1297,6 +1297,15 @@ dependencies = [ "syn", ] +[[package]] +name = "darkfi-money-contract" +version = "0.3.0" +dependencies = [ + "darkfi-sdk", + "darkfi-serial", + "getrandom 0.2.8", +] + [[package]] name = "darkfi-sdk" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 2ac3bb964..ce3e43021 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,8 @@ members = [ "src/serial/derive", "src/serial/derive-internal", + "src/contract/money", + "example/dchat", "example/dao", ] diff --git a/src/contract/README.md b/src/contract/README.md new file mode 100644 index 000000000..4b29656d4 --- /dev/null +++ b/src/contract/README.md @@ -0,0 +1 @@ +This directory contains native WASM contracts on DarkFi. diff --git a/src/contract/money/.gitignore b/src/contract/money/.gitignore new file mode 100644 index 000000000..2c9c32e68 --- /dev/null +++ b/src/contract/money/.gitignore @@ -0,0 +1,2 @@ +money_contract.wasm +proof/*.zk.bin diff --git a/src/contract/money/Cargo.toml b/src/contract/money/Cargo.toml new file mode 100644 index 000000000..4119b8929 --- /dev/null +++ b/src/contract/money/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "darkfi-money-contract" +version = "0.3.0" +authors = ["Dyne.org foundation "] +license = "AGPL-3.0-only" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +darkfi-sdk = { path = "../../sdk" } +darkfi-serial = { path = "../../serial", features = ["derive", "crypto"] } + +# We need to disable random using "custom" which makes the crate a noop +# so the wasm32-unknown-unknown target is enabled. +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2.8", features = ["custom"] } diff --git a/src/contract/money/Makefile b/src/contract/money/Makefile new file mode 100644 index 000000000..96c1fd3d1 --- /dev/null +++ b/src/contract/money/Makefile @@ -0,0 +1,37 @@ +# Cargo binary +CARGO ?= cargo + +# Zkas binary +ZKAS ?= ../../../zkas + +# zkas circuit source files +ZKAS_SRC = $(shell find proof -type f -name '*.zk') + +# wasm source files +WASM_SRC = \ + $(shell find src -type f) \ + $(shell find ../../sdk/src -type f) \ + $(shell find ../../serial/src -type f) + +# zkas circuit bin files +ZKAS_BIN = $(ZKAS_SRC:=.bin) + +# Contract WASM binaries +WASM_BIN = money_contract.wasm + +all: $(ZKAS_BIN) $(WASM_BIN) + +$(ZKAS_BIN): $(ZKAS_SRC) + $(ZKAS) $(basename $@) -o $@ + +money_contract.wasm: $(ZKAS_BIN) $(WASM_SRC) + $(CARGO) build --release --package darkfi-money-contract --target wasm32-unknown-unknown + cp -f ../../../target/wasm32-unknown-unknown/release/darkfi_money_contract.wasm $@ + +test: + +clean: + rm -f $(ZKAS_BIN) $(WASM_BIN) + +# We always rebuild the wasm no matter what +.PHONY: $(WASM_BIN) all test clean diff --git a/src/contract/money/proof/burn.zk b/src/contract/money/proof/burn.zk new file mode 100644 index 000000000..302a7ed3d --- /dev/null +++ b/src/contract/money/proof/burn.zk @@ -0,0 +1,66 @@ +constant "Burn" { + EcFixedPointShort VALUE_COMMIT_VALUE, + EcFixedPoint VALUE_COMMIT_RANDOM, + EcFixedPointBase NULLIFIER_K, +} + +contract "Burn" { + Base secret, + Base serial, + Base value, + Base token, + Base coin_blind, + Scalar value_blind, + Scalar token_blind, + Uint32 leaf_pos, + MerklePath path, + Base signature_secret, +} + +circuit "Burn" { + # Poseidon hash of the nullifier + nullifier = poseidon_hash(secret, serial); + constrain_instance(nullifier); + + # Pedersen commitment for coin's value + vcv = ec_mul_short(value, VALUE_COMMIT_VALUE); + vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM); + value_commit = ec_add(vcv, vcr); + # Since value_commit is a curve point, we fetch its coordinates + # and constrain them: + value_commit_x = ec_get_x(value_commit); + value_commit_y = ec_get_y(value_commit); + constrain_instance(value_commit_x); + constrain_instance(value_commit_y); + + # Pedersen commitment for coin's token ID + tcv = ec_mul_base(token, NULLIFIER_K); + tcr = ec_mul(token_blind, VALUE_COMMIT_RANDOM); + token_commit = ec_add(tcv, tcr); + # Since token_commit is also a curve point, we'll do the same + # coordinate dance: + token_commit_x = ec_get_x(token_commit); + token_commit_y = ec_get_y(token_commit); + constrain_instance(token_commit_x); + constrain_instance(token_commit_y); + + # Coin hash + pub = ec_mul_base(secret, NULLIFIER_K); + pub_x = ec_get_x(pub); + pub_y = ec_get_y(pub); + C = poseidon_hash(pub_x, pub_y, value, token, serial, coin_blind); + + # Merkle root + root = merkle_root(leaf_pos, path, C); + constrain_instance(root); + + # Finally, we derive a public key for the signature and + # constrain its coordinates: + signature_public = ec_mul_base(signature_secret, NULLIFIER_K); + signature_x = ec_get_x(signature_public); + signature_y = ec_get_y(signature_public); + constrain_instance(signature_x); + constrain_instance(signature_y); + + # At this point we've enforced all of our public inputs. +} diff --git a/src/contract/money/proof/mint.zk b/src/contract/money/proof/mint.zk new file mode 100644 index 000000000..5a839a2ea --- /dev/null +++ b/src/contract/money/proof/mint.zk @@ -0,0 +1,46 @@ +constant "Mint" { + EcFixedPointShort VALUE_COMMIT_VALUE, + EcFixedPoint VALUE_COMMIT_RANDOM, + EcFixedPointBase NULLIFIER_K, +} + +contract "Mint" { + Base pub_x, + Base pub_y, + Base value, + Base token, + Base serial, + Base coin_blind, + Scalar value_blind, + Scalar token_blind, +} + +circuit "Mint" { + # Poseidon hash of the coin + C = poseidon_hash(pub_x, pub_y, value, token, serial, coin_blind); + constrain_instance(C); + + # Pedersen commitment for coin's value + vcv = ec_mul_short(value, VALUE_COMMIT_VALUE); + vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM); + value_commit = ec_add(vcv, vcr); + # Since the value commit is a curve point, we fetch its coordinates + # and constrain them: + value_commit_x = ec_get_x(value_commit); + value_commit_y = ec_get_y(value_commit); + constrain_instance(value_commit_x); + constrain_instance(value_commit_y); + + # Pedersen commitment for coin's token ID + tcv = ec_mul_base(token, NULLIFIER_K); + tcr = ec_mul(token_blind, VALUE_COMMIT_RANDOM); + token_commit = ec_add(tcv, tcr); + # Since token_commit is also a curve point, we'll do the same + # coordinate dance: + token_commit_x = ec_get_x(token_commit); + token_commit_y = ec_get_y(token_commit); + constrain_instance(token_commit_x); + constrain_instance(token_commit_y); + + # At this point we've enforced all of our public inputs. +} diff --git a/src/contract/money/src/lib.rs b/src/contract/money/src/lib.rs new file mode 100644 index 000000000..73a66c17a --- /dev/null +++ b/src/contract/money/src/lib.rs @@ -0,0 +1,299 @@ +/* This file is part of DarkFi (https://dark.fi) + * + * Copyright (C) 2020-2022 Dyne.org foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use darkfi_sdk::{ + crypto::{ContractId, MerkleNode, MerkleTree, PublicKey}, + db::{db_contains_key, db_get, db_init, db_lookup, db_set}, + define_contract, + error::{ContractError, ContractResult}, + merkle::merkle_add, + msg, + pasta::{arithmetic::CurveAffine, group::Curve, pallas}, + tx::ContractCall, + util::set_return_data, +}; +use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; + +/// Functions we allow in this contract +#[repr(u8)] +pub enum MoneyFunction { + Transfer = 0x00, +} + +impl From for MoneyFunction { + fn from(b: u8) -> Self { + match b { + 0x00 => Self::Transfer, + _ => panic!("Invalid function ID: {:#04x?}", b), + } + } +} + +/// Structures and object definitions +pub mod state; +use state::{MoneyTransferParams, MoneyTransferUpdate}; + +define_contract!( + init: init_contract, + exec: process_instruction, + apply: process_update, + metadata: get_metadata +); + +// These are the different sled trees that will be created +pub const ZKAS_TREE: &str = "zkas"; +pub const COIN_ROOTS_TREE: &str = "coin_roots"; +pub const NULLIFIERS_TREE: &str = "nullifiers"; +pub const INFO_TREE: &str = "info"; + +// This is a key inside the info tree +pub const COIN_MERKLE_TREE: &str = "coin_tree"; +pub const FAUCET_PUBKEYS: &str = "faucet_pubkeys"; + +/// zkas mint contract namespace +pub const ZKAS_MINT_NS: &str = "Mint"; +/// zkas burn contract namespace +pub const ZKAS_BURN_NS: &str = "Burn"; + +/// This function runs when the contract is (re)deployed and initialized. +fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult { + // The zkas circuits can simply be embedded in the wasm and set up by + // the initialization. Note that the tree should then be called "zkas". + // The lookups can then be done by `contract_id+zkas+namespace`. + let zkas_db = db_init(cid, ZKAS_TREE)?; + let mint_bincode = include_bytes!("../proof/mint.zk.bin"); + let burn_bincode = include_bytes!("../proof/burn.zk.bin"); + + /* TODO: Do I really want to make zkas a dependency? Yeah, in the future. + For now we take anything. + let zkbin = ZkBinary::decode(mint_bincode)?; + let mint_namespace = zkbin.namespace.clone(); + assert_eq!(&mint_namespace, ZKAS_MINT_NS); + let zkbin = ZkBinary::decode(burn_bincode)?; + let burn_namespace = zkbin.namespace.clone(); + assert_eq!(&burn_namespace, ZKAS_BURN_NS); + db_set(zkas_db, &serialize(&mint_namespace), &mint_bincode[..])?; + db_set(zkas_db, &serialize(&burn_namespace), &burn_bincode[..])?; + */ + + db_set(zkas_db, &serialize(&ZKAS_MINT_NS.to_string()), &mint_bincode[..])?; + db_set(zkas_db, &serialize(&ZKAS_BURN_NS.to_string()), &burn_bincode[..])?; + + // Set up a database tree to hold Merkle roots + let _ = db_init(cid, COIN_ROOTS_TREE)?; + + // Set up a database tree to hold nullifiers + let _ = db_init(cid, NULLIFIERS_TREE)?; + + // Set up a database tree for arbitrary data + let info_db = db_init(cid, INFO_TREE)?; + + // Add a Merkle tree to the info db: + let coin_tree = MerkleTree::new(100); + let mut coin_tree_data = vec![]; + // TODO: FIXME: What is this write_u32 doing here? + coin_tree_data.write_u32(0)?; + coin_tree.encode(&mut coin_tree_data)?; + db_set(info_db, &serialize(&COIN_MERKLE_TREE.to_string()), &coin_tree_data)?; + + // Whitelisted faucets + let faucet_pubkeys: Vec = vec![]; + db_set(info_db, &serialize(&FAUCET_PUBKEYS.to_string()), &serialize(&faucet_pubkeys))?; + + Ok(()) +} + +/// This function is used by the VM's host to fetch the necessary metadata for +/// verifying signatures and zk proofs. +fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult { + let (call_idx, call): (u32, Vec) = deserialize(ix)?; + assert!(call_idx < call.len() as u32); + + let self_ = &call[call_idx as usize]; + + match MoneyFunction::from(self_.data[0]) { + MoneyFunction::Transfer => { + let params: MoneyTransferParams = deserialize(&self_.data[1..])?; + + let mut zk_public_values: Vec<(String, Vec)> = vec![]; + let mut signature_pubkeys: Vec = vec![]; + + for input in ¶ms.clear_inputs { + signature_pubkeys.push(input.signature_public); + } + + for input in ¶ms.inputs { + let value_coords = input.value_commit.to_affine().coordinates().unwrap(); + let token_coords = input.token_commit.to_affine().coordinates().unwrap(); + let (sig_x, sig_y) = input.signature_public.xy(); + + zk_public_values.push(( + ZKAS_BURN_NS.to_string(), + vec![ + input.nullifier.inner(), + *value_coords.x(), + *value_coords.y(), + *token_coords.x(), + *token_coords.y(), + input.merkle_root.inner(), + input.user_data_enc, + sig_x, + sig_y, + ], + )); + + signature_pubkeys.push(input.signature_public); + } + + for output in ¶ms.outputs { + let value_coords = output.value_commit.to_affine().coordinates().unwrap(); + let token_coords = output.token_commit.to_affine().coordinates().unwrap(); + + zk_public_values.push(( + ZKAS_MINT_NS.to_string(), + vec![ + output.coin.inner(), + *value_coords.x(), + *value_coords.y(), + *token_coords.x(), + *token_coords.y(), + ], + )); + } + + let mut metadata = vec![]; + zk_public_values.encode(&mut metadata)?; + signature_pubkeys.encode(&mut metadata)?; + + // Using this, we pass the above data to the host. + set_return_data(&metadata)?; + } + }; + + Ok(()) +} + +/// This function verifies a state transition and produces an +/// update if everything is successful. +fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult { + let (call_idx, call): (u32, Vec) = deserialize(ix)?; + assert!(call_idx < call.len() as u32); + + let self_ = &call[call_idx as usize]; + + match MoneyFunction::from(self_.data[0]) { + MoneyFunction::Transfer => { + let params: MoneyTransferParams = deserialize(&self_.data[1..])?; + + let info_db = db_lookup(cid, INFO_TREE)?; + let nullifier_db = db_lookup(cid, NULLIFIERS_TREE)?; + let coin_roots_db = db_lookup(cid, COIN_ROOTS_TREE)?; + + let Some(faucet_pubkeys) = db_get(info_db, &serialize(&FAUCET_PUBKEYS.to_string()))? else { + msg!("[Transfer] Error: Missing faucet pubkeys from info db"); + return Err(ContractError::Internal); + }; + let faucet_pubkeys: Vec = deserialize(&faucet_pubkeys)?; + + // State transition for payments + msg!("[Transfer] Iterating over clear inputs"); + for (i, input) in params.clear_inputs.iter().enumerate() { + let pk = input.signature_public; + + if !faucet_pubkeys.contains(&pk) { + msg!("[Transfer] Error: Clear input {} has invalid faucet pubkey", i); + return Err(ContractError::Custom(20)) + } + } + + let mut new_coin_roots = vec![]; + let mut new_nullifiers = vec![]; + + msg!("[Transfer] Iterating over anonymous inputs"); + for (i, input) in params.inputs.iter().enumerate() { + // The Merkle root is used to know whether this is a coin that existed + // in a previous state. + if new_coin_roots.contains(&input.merkle_root) || + db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? + { + msg!("[Transfer] Error: Duplicate Merkle root found in input {}", i); + return Err(ContractError::Custom(21)) + } + + // The nullifiers should not already exist. It is the double-spend protection. + if new_nullifiers.contains(&input.nullifier) || + db_contains_key(nullifier_db, &serialize(&input.nullifier))? + { + msg!("[Transfer] Error: Duplicate nullifier found in input {}", i); + return Err(ContractError::Custom(22)) + } + + new_coin_roots.push(input.merkle_root); + new_nullifiers.push(input.nullifier); + } + + // Newly created coins for this transaction are in the outputs. + let new_coins = Vec::with_capacity(params.outputs.len()); + for (i, output) in params.outputs.iter().enumerate() { + // TODO: Should we have coins in a sled tree too to check dupes? + if new_coins.contains(&output.coin) { + msg!("[Transfer] Error: Duplicate coin found in output {}", i); + return Err(ContractError::Custom(23)) + } + } + + // Create a state update + let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins }; + let mut update_data = vec![]; + update_data.write_u8(MoneyFunction::Transfer as u8)?; + update.encode(&mut update_data)?; + set_return_data(&update_data)?; + msg!("[Transfer] State update set!"); + + Ok(()) + } + } +} + +fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult { + match MoneyFunction::from(update_data[0]) { + MoneyFunction::Transfer => { + let update: MoneyTransferUpdate = deserialize(&update_data[1..])?; + + let info_db = db_lookup(cid, INFO_TREE)?; + let nullifiers_db = db_lookup(cid, NULLIFIERS_TREE)?; + let coin_roots_db = db_lookup(cid, COIN_ROOTS_TREE)?; + + for nullifier in update.nullifiers { + db_set(nullifiers_db, &serialize(&nullifier), &[])?; + } + + for coin in update.coins { + // TODO: merkle_add() should take a list of coins and batch add them for efficiency + merkle_add( + info_db, + coin_roots_db, + &serialize(&COIN_MERKLE_TREE.to_string()), + &MerkleNode::from(coin.inner()), + )?; + } + + Ok(()) + } + } +} diff --git a/src/contract/money/src/state.rs b/src/contract/money/src/state.rs new file mode 100644 index 000000000..1099b35f7 --- /dev/null +++ b/src/contract/money/src/state.rs @@ -0,0 +1,95 @@ +/* This file is part of DarkFi (https://dark.fi) + * + * Copyright (C) 2020-2022 Dyne.org foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use darkfi_sdk::{ + crypto::{ + pedersen::{ValueBlind, ValueCommit}, + Coin, MerkleNode, Nullifier, PublicKey, TokenId, + }, + pasta::pallas, +}; +use darkfi_serial::{SerialDecodable, SerialEncodable}; + +/// Inputs and outputs for a payment +#[derive(Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyTransferParams { + /// Clear inputs + pub clear_inputs: Vec, + /// Anonymous inputs + pub inputs: Vec, + /// Anonymous outputs + pub outputs: Vec, +} + +/// State update produced by a payment +#[derive(Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyTransferUpdate { + /// Revealed nullifiers + pub nullifiers: Vec, + /// Minted coins + pub coins: Vec, +} + +/// A transaction's clear input +#[derive(Debug, SerialEncodable, SerialDecodable)] +pub struct ClearInput { + /// Input's value (amount) + pub value: u64, + /// Input's token ID + pub token_id: TokenId, + /// Blinding factor for `value` + pub value_blind: ValueBlind, + /// Blinding factor for `token_id` + pub token_blind: ValueBlind, + /// Public key for the signature + pub signature_public: PublicKey, +} + +/// A transaction's anonymous input +#[derive(Debug, SerialEncodable, SerialDecodable)] +pub struct Input { + /// Pedersen commitment for the input's value + pub value_commit: ValueCommit, + /// Pedersen commitment for the input's token ID + pub token_commit: ValueCommit, + /// Revealed nullifier + pub nullifier: Nullifier, + /// Revealed Merkle root + pub merkle_root: MerkleNode, + /// spend hook (TODO: document) + pub spend_hook: pallas::Base, + /// user data enc (TODO: document) + pub user_data_enc: pallas::Base, + /// Public key for the signature + pub signature_public: PublicKey, +} + +/// A transaction's anonymous output +#[derive(Debug, SerialEncodable, SerialDecodable)] +pub struct Output { + /// Pedersen commitment for the output's value + pub value_commit: ValueCommit, + /// Pedersen commitment for the output's token ID + pub token_commit: ValueCommit, + /// Minted coin + pub coin: Coin, + /// The encrypted note ciphertext + pub ciphertext: Vec, + /// The ephemeral public key + pub ephem_public: PublicKey, +} diff --git a/src/sdk/src/crypto/coin.rs b/src/sdk/src/crypto/coin.rs new file mode 100644 index 000000000..27f825bbf --- /dev/null +++ b/src/sdk/src/crypto/coin.rs @@ -0,0 +1,85 @@ +/* This file is part of DarkFi (https://dark.fi) + * + * Copyright (C) 2020-2022 Dyne.org foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use core::{fmt, str::FromStr}; +use std::io; + +use darkfi_serial::{SerialDecodable, SerialEncodable}; +use pasta_curves::{group::ff::PrimeField, pallas}; + +/// The `Coin` is represented as a base field element. +#[repr(C)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, SerialEncodable, SerialDecodable)] +pub struct Coin(pallas::Base); + +impl Coin { + /// Reference the raw inner base field element + pub fn inner(&self) -> pallas::Base { + self.0 + } + + /// Try to create a `Coin` type from the given 32 bytes. + /// Returns `Some` if the bytes fit in the base field, and `None` if not. + pub fn from_bytes(bytes: [u8; 32]) -> Option { + let n = pallas::Base::from_repr(bytes); + match bool::from(n.is_some()) { + true => Some(Self(n.unwrap())), + false => None, + } + } + + /// Convert the `Coin` type into 32 raw bytes + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_repr() + } +} + +impl From for Coin { + fn from(x: pallas::Base) -> Self { + Self(x) + } +} + +impl fmt::Display for Coin { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", bs58::encode(self.to_bytes()).into_string()) + } +} + +impl FromStr for Coin { + type Err = io::Error; + + /// Tries to decode a base58 string into a `Coin` type. + /// This string is the same string received by calling `Coin::to_string()`. + fn from_str(s: &str) -> Result { + let bytes = match bs58::decode(s).into_vec() { + Ok(v) => v, + Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), + }; + + if bytes.len() != 32 { + return Err(io::Error::new(io::ErrorKind::Other, "Length of decoded bytes is not 32")) + } + + if let Some(coin) = Self::from_bytes(bytes.try_into().unwrap()) { + return Ok(coin) + } + + Err(io::Error::new(io::ErrorKind::Other, "Invalid bytes for Coin")) + } +} diff --git a/src/sdk/src/crypto/mod.rs b/src/sdk/src/crypto/mod.rs index 83d35db90..438e77476 100644 --- a/src/sdk/src/crypto/mod.rs +++ b/src/sdk/src/crypto/mod.rs @@ -42,6 +42,10 @@ pub use keypair::{Keypair, PublicKey, SecretKey}; pub mod address; pub use address::Address; +/// Coin definitions and methods +pub mod coin; +pub use coin::Coin; + /// Contract ID definitions and methods pub mod contract_id; pub use contract_id::ContractId;