amadeus_runtime/consensus/bic/
coin.rs

1use crate::consensus::consensus_apply::ApplyEnv;
2use crate::consensus::consensus_kv::{kv_get, kv_increment, kv_put};
3use crate::{Result, bcat, consensus};
4use amadeus_utils::vecpak::{self, Term};
5
6pub const DECIMALS: u32 = 9;
7pub const BURN_ADDRESS: [u8; 48] = [0u8; 48];
8
9pub fn to_flat(coins: i128) -> i128 {
10    coins.saturating_mul(1_000_000_000)
11}
12pub fn to_cents(coins: i128) -> i128 {
13    coins.saturating_mul(10_000_000)
14}
15pub fn to_tenthousandth(coins: i128) -> i128 {
16    coins.saturating_mul(100_000)
17}
18pub fn from_flat(coins: i128) -> f64 {
19    let whole = (coins / 1_000_000_000) as f64;
20    let frac = ((coins % 1_000_000_000).abs() as f64) / 1_000_000_000.0;
21    let x = if coins >= 0 { whole + frac } else { whole - frac };
22    (x * 1e9).round() / 1e9
23}
24
25pub fn balance_burnt(env: &ApplyEnv, symbol: &[u8]) -> Result<i128> {
26    balance(env, &BURN_ADDRESS, symbol)
27}
28
29pub fn balance(env: &ApplyEnv, address: &[u8], symbol: &[u8]) -> Result<i128> {
30    match kv_get(env, &bcat(&[b"account:", address, b":balance:", symbol]))? {
31        Some(amount) => {
32            let s = std::str::from_utf8(&amount).map_err(|_| "invalid_utf8")?;
33            let parsed = s.parse::<i128>().map_err(|_| "invalid_balance")?;
34            Ok(parsed)
35        }
36        None => Ok(0),
37    }
38}
39
40pub fn mintable(env: &ApplyEnv, symbol: &[u8]) -> Result<bool> {
41    match kv_get(env, &bcat(&[b"coin:", symbol, b":mintable"]))?.as_deref() {
42        Some(b"true") => Ok(true),
43        _ => Ok(false),
44    }
45}
46
47pub fn pausable(env: &ApplyEnv, symbol: &[u8]) -> Result<bool> {
48    match kv_get(env, &bcat(&[b"coin:", symbol, b":pausable"]))?.as_deref() {
49        Some(b"true") => Ok(true),
50        _ => Ok(false),
51    }
52}
53
54pub fn paused(env: &ApplyEnv, symbol: &[u8]) -> Result<bool> {
55    match kv_get(env, &bcat(&[b"coin:", symbol, b":paused"]))?.as_deref() {
56        Some(b"true") => pausable(env, symbol),
57        _ => Ok(false),
58    }
59}
60
61pub fn soulbound(env: &ApplyEnv, symbol: &[u8]) -> Result<bool> {
62    match kv_get(env, &bcat(&[b"coin:", symbol, b":soulbound"]))?.as_deref() {
63        Some(b"true") => Ok(true),
64        _ => Ok(false),
65    }
66}
67
68pub fn total_supply(env: &ApplyEnv, symbol: &[u8]) -> Result<i128> {
69    match kv_get(env, &bcat(&[b"coin:", symbol, b":totalSupply"]))? {
70        Some(amount) => {
71            let s = std::str::from_utf8(&amount).map_err(|_| "invalid_utf8")?;
72            let parsed = s.parse::<i128>().map_err(|_| "invalid_total_supply")?;
73            Ok(parsed)
74        }
75        None => Ok(0),
76    }
77}
78
79pub fn exists(env: &ApplyEnv, symbol: &[u8]) -> Result<bool> {
80    match kv_get(env, &bcat(&[b"coin:", symbol, b":totalSupply"]))? {
81        Some(_) => Ok(true),
82        None => Ok(false),
83    }
84}
85
86pub fn has_permission(env: &ApplyEnv, symbol: &[u8], signer: &[u8]) -> Result<bool> {
87    match kv_get(env, &bcat(&[b"coin:", symbol, b":permission"]))? {
88        None => Ok(false),
89        Some(permission_list) => {
90            let term = vecpak::decode(permission_list.as_slice()).map_err(|_| "invalid_vecpak")?;
91            match term {
92                Term::List(term_list) => Ok(term_list
93                    .iter()
94                    .any(|el| matches!(el, Term::Binary(b) if b.as_slice() == signer))),
95                _ => Ok(false),
96            }
97        }
98    }
99}
100
101pub fn call_transfer(env: &mut ApplyEnv, args: Vec<Vec<u8>>) -> Result<()> {
102    if args.len() != 3 {
103        return Err("invalid_args");
104    }
105    let receiver = args[0].as_slice();
106    let amount = args[1].as_slice();
107    let amount = std::str::from_utf8(&amount).ok().and_then(|s| s.parse::<i128>().ok()).ok_or("invalid_amount")?;
108    let symbol = args[2].as_slice();
109
110    if receiver.len() != 48 {
111        return Err("invalid_receiver_pk");
112    }
113    if !(amadeus_utils::bls12_381::validate_public_key(receiver).is_ok() || receiver == &BURN_ADDRESS) {
114        return Err("invalid_receiver_pk");
115    }
116    if amount <= 0 {
117        return Err("invalid_amount");
118    }
119    if amount > balance(env, env.caller_env.account_caller.as_slice(), symbol)? {
120        return Err("insufficient_funds");
121    }
122
123    if paused(env, symbol)? {
124        return Err("paused");
125    }
126    if soulbound(env, symbol)? {
127        return Err("soulbound");
128    }
129
130    kv_increment(env, &bcat(&[b"account:", env.caller_env.account_caller.as_slice(), b":balance:", symbol]), -amount)?;
131    kv_increment(env, &bcat(&[b"account:", receiver, b":balance:", symbol]), amount)?;
132
133    // Account burnt coins
134    if symbol != b"AMA" && receiver == &BURN_ADDRESS {
135        kv_increment(env, &bcat(&[b"coin:", symbol, b":totalSupply"]), -amount)?;
136    }
137    Ok(())
138}
139
140pub fn call_create_and_mint(env: &mut ApplyEnv, args: Vec<Vec<u8>>) -> Result<()> {
141    if args.len() < 2 {
142        return Err("invalid_args");
143    }
144    let symbol_original = args[0].as_slice();
145    let amount = args[1].as_slice();
146    let decimals = args.get(2).and_then(|v| if v.is_empty() { None } else { Some(v.as_slice()) }).unwrap_or(b"9");
147    let mintable = args.get(3).and_then(|v| if v.is_empty() { None } else { Some(v.as_slice()) }).unwrap_or(b"false");
148    let pausable = args.get(4).and_then(|v| if v.is_empty() { None } else { Some(v.as_slice()) }).unwrap_or(b"false");
149    let soulbound_arg = args.get(5).and_then(|v| if v.is_empty() { None } else { Some(v.as_slice()) }).unwrap_or(b"false");
150
151    let symbol: Vec<u8> = symbol_original.iter().copied().filter(u8::is_ascii_alphanumeric).collect();
152    if symbol_original != symbol.as_slice() {
153        return Err("invalid_symbol");
154    }
155    if symbol.is_empty() {
156        return Err("symbol_too_short");
157    }
158    if symbol.len() > 32 {
159        return Err("symbol_too_long");
160    }
161
162    if !consensus::bic::coin_symbol_reserved::is_free(&symbol, &env.caller_env.account_caller) {
163        return Err("symbol_reserved");
164    }
165    if exists(env, &symbol)? {
166        return Err("symbol_exists");
167    }
168
169    let amount = std::str::from_utf8(&amount).ok().and_then(|s| s.parse::<i128>().ok()).ok_or("invalid_amount")?;
170    if amount <= 0 {
171        return Err("invalid_amount");
172    }
173
174    let decimals = std::str::from_utf8(decimals).ok().and_then(|s| s.parse::<u64>().ok()).ok_or("invalid_decimals")?;
175    if decimals >= 10 {
176        return Err("invalid_decimals");
177    }
178
179    kv_increment(env, &bcat(&[b"account:", env.caller_env.account_caller.as_slice(), b":balance:", &symbol]), amount)?;
180    kv_increment(env, &bcat(&[b"coin:", &symbol, b":totalSupply"]), amount)?;
181
182    let admin = vec![Term::Binary(env.caller_env.account_caller.to_vec())];
183    let buf = vecpak::encode(Term::List(admin));
184    kv_put(env, &bcat(&[b"coin:", &symbol, b":permission"]), &buf)?;
185
186    if mintable == b"true" {
187        kv_put(env, &bcat(&[b"coin:", &symbol, b":mintable"]), b"true")?;
188    }
189    if pausable == b"true" {
190        kv_put(env, &bcat(&[b"coin:", &symbol, b":pausable"]), b"true")?;
191    }
192    if soulbound_arg == b"true" {
193        kv_put(env, &bcat(&[b"coin:", &symbol, b":soulbound"]), b"true")?;
194    }
195    Ok(())
196}
197
198pub fn call_mint(env: &mut ApplyEnv, args: Vec<Vec<u8>>) -> Result<()> {
199    if args.len() != 3 {
200        return Err("invalid_args");
201    }
202    let symbol = args[0].as_slice();
203    let amount = args[1].as_slice();
204    let amount = std::str::from_utf8(&amount).ok().and_then(|s| s.parse::<i128>().ok()).ok_or("invalid_amount")?;
205    let receiver = args[2].as_slice();
206    if receiver.len() != 48 {
207        return Err("invalid_receiver_pk");
208    }
209
210    if !has_permission(env, symbol, env.caller_env.account_caller.as_slice())? {
211        return Err("no_permissions");
212    }
213
214    mint(env, symbol, amount, receiver)
215}
216
217pub fn mint(env: &mut ApplyEnv, symbol: &[u8], amount: i128, receiver: &[u8]) -> Result<()> {
218    if !amadeus_utils::bls12_381::validate_public_key(receiver).is_ok() {
219        return Err("invalid_receiver_pk");
220    }
221    if amount <= 0 {
222        return Err("invalid_amount");
223    }
224
225    if !exists(env, symbol)? {
226        return Err("symbol_doesnt_exist");
227    }
228    if !mintable(env, symbol)? {
229        return Err("not_mintable");
230    }
231    if paused(env, symbol)? {
232        return Err("paused");
233    }
234
235    kv_increment(env, &bcat(&[b"account:", receiver, b":balance:", symbol]), amount)?;
236    kv_increment(env, &bcat(&[b"coin:", symbol, b":totalSupply"]), amount)?;
237    Ok(())
238}
239
240pub fn call_pause(env: &mut ApplyEnv, args: Vec<Vec<u8>>) -> Result<()> {
241    if args.len() != 2 {
242        return Err("invalid_args");
243    }
244    let symbol = args[0].as_slice();
245    let direction = args[1].as_slice();
246
247    if direction != b"true" && direction != b"false" {
248        return Err("invalid_direction");
249    }
250
251    if !exists(env, symbol)? {
252        return Err("symbol_doesnt_exist");
253    }
254    if !has_permission(env, symbol, env.caller_env.account_caller.as_slice())? {
255        return Err("no_permissions");
256    }
257    if !pausable(env, symbol)? {
258        return Err("not_pausable");
259    }
260
261    kv_put(env, &bcat(&[b"coin:", symbol, b":paused"]), direction)?;
262    Ok(())
263}
264
265pub fn burn_address() -> [u8; 48] {
266    BURN_ADDRESS
267}
268
269pub fn call(env: &mut ApplyEnv, function: &str, args: &[Vec<u8>]) -> Result<()> {
270    match function {
271        "transfer" => call_transfer(env, args.to_vec()),
272        "create_and_mint" => call_create_and_mint(env, args.to_vec()),
273        "mint" => call_mint(env, args.to_vec()),
274        "pause" => call_pause(env, args.to_vec()),
275        _ => Err("unimplemented_function"),
276    }
277}