Skip to content

Commit 4ce86fa

Browse files
Implement mint_ft functionality in interpreter and console (#2033)
* implement mint_ft_balance in the interpreter; add a test which creates a ft via contract then uses the interpreter to mint while checking that the vm db knows about both the contract and interpreter mint events; add mint_ft session command which calls mint_ft_balance use asset identifier instead of passing contract identifier and asset name separately; use sugared asset identifier as token name when calling credit_token so db and interpreter balances will match remove unnecessary refs add session test for mint_ft that runs the command via a passed string and verifies that balances are correct add explicit test for failure cases in parse_asset_identifier use enum for error type in parse_asset_identifier test failure cases for Session::mint_ft map errors to remove unwrap/expect in mint_ft_balance remove unit let binding pass metadata into get_ft_balance so the clarity vm db can skip loading it again test that we fail to load_ft for an asset that doesn't exist change bad amount to -1 so there's no chance of multiple failures cancelling each other out * swap order of asset identifier and recipient address * add mint_ft to display_help with an explanation of the asset identifier format * let user skip the deployer part of the contract identifier; fix tests * use indoc::formatdoc to make contract src pretty * fix desugar_contract_id so it works with contracts that lead with a period * collapse else/if into else if; swap order of deployer and identifier parameters * rename deployer to default_deployer since it will always be that, but not always be the actual deployer of the contract * fix missing rename
1 parent 66082d3 commit 4ce86fa

File tree

3 files changed

+378
-7
lines changed

3 files changed

+378
-7
lines changed

components/clarity-repl/src/repl/interpreter.rs

Lines changed: 170 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use clarity::vm::{
1818
eval, eval_all, ClarityVersion, ContractEvaluationResult, CostSynthesis, EvalHook,
1919
EvaluationResult, ExecutionResult, ParsedContract, SnippetEvaluationResult,
2020
};
21-
use clarity_types::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData};
21+
use clarity_types::types::{
22+
AssetIdentifier, PrincipalData, QualifiedContractIdentifier, StandardPrincipalData,
23+
};
2224
use clarity_types::Value;
2325

2426
use super::datastore::StacksConstants;
@@ -817,6 +819,66 @@ impl ClarityInterpreter {
817819
Ok(format!("→ {recipient}: {final_balance} µSTX"))
818820
}
819821

822+
pub fn mint_ft_balance(
823+
&mut self,
824+
asset_identifier: &AssetIdentifier,
825+
recipient: &PrincipalData,
826+
amount: u64,
827+
) -> Result<String, String> {
828+
let contract_identifier = asset_identifier.contract_identifier.clone();
829+
let token_name = asset_identifier.asset_name.to_string();
830+
let final_balance = {
831+
let mut global_context = self.get_global_context(DEFAULT_EPOCH, false)?;
832+
833+
global_context.begin();
834+
835+
let metadata = global_context
836+
.database
837+
.load_ft(&contract_identifier, &token_name)
838+
.map_err(|e| {
839+
format!("failed to load_ft for {contract_identifier}.{token_name}: {e:?}")
840+
})?;
841+
842+
let cur_balance = global_context
843+
.database
844+
.get_ft_balance(&contract_identifier, &token_name, recipient, Some(&metadata))
845+
.map_err(|e| format!("failed to get_ft_balance for {contract_identifier}.{token_name} {recipient}: {e:?}"))?;
846+
847+
global_context.database.set_ft_balance(
848+
&contract_identifier,
849+
&token_name,
850+
recipient,
851+
cur_balance + amount as u128,
852+
).map_err(|e| format!("failed to set_ft_balance for {contract_identifier}.{token_name} {recipient}: {e:?}"))?;
853+
854+
let final_balance = global_context
855+
.database
856+
.get_ft_balance(&contract_identifier, &token_name, recipient, Some(&metadata))
857+
.map_err(|e| format!("failed to get_ft_balance for {contract_identifier}.{token_name} {recipient}: {e:?}"))?;
858+
859+
global_context
860+
.database
861+
.checked_increase_token_supply(
862+
&contract_identifier,
863+
&token_name,
864+
amount as u128,
865+
&metadata,
866+
)
867+
.map_err(|e| format!("failed to increase token supply for {contract_identifier}.{token_name}: {e:?}"))?;
868+
869+
global_context
870+
.commit()
871+
.map_err(|e| format!("failed to commit ctx: {e:?}"))?;
872+
final_balance
873+
};
874+
self.credit_token(
875+
recipient.to_string(),
876+
asset_identifier.sugared(),
877+
amount.into(),
878+
);
879+
Ok(format!("→ {recipient}: {final_balance} {token_name}"))
880+
}
881+
820882
pub fn set_tx_sender(&mut self, tx_sender: StandardPrincipalData) {
821883
self.tx_sender = tx_sender;
822884
}
@@ -938,7 +1000,7 @@ mod tests {
9381000
use clarity::util::hash::hex_bytes;
9391001
use clarity::vm::{self, ClarityVersion};
9401002
use clarity_types::types::TupleData;
941-
use indoc::indoc;
1003+
use indoc::{formatdoc, indoc};
9421004

9431005
use super::*;
9441006
use crate::analysis::Settings as AnalysisSettings;
@@ -1173,6 +1235,112 @@ mod tests {
11731235
assert_eq!(balance, amount.into());
11741236
}
11751237

1238+
#[test]
1239+
fn test_mint_ft_balance() {
1240+
let mut interpreter = get_interpreter(None);
1241+
let recipient = PrincipalData::Standard(StandardPrincipalData::transient());
1242+
let token_name = "ctb";
1243+
let src = formatdoc!(
1244+
r#"
1245+
(define-fungible-token {token_name})
1246+
(define-private (test-mint)
1247+
(ft-mint? ctb u100 tx-sender))
1248+
(define-private (test-burn)
1249+
(ft-burn? ctb u10 tx-sender))
1250+
(define-private (test-transfer)
1251+
(ft-transfer? ctb u10 tx-sender (as-contract tx-sender)))
1252+
(define-private (test-burn-1000)
1253+
(ft-burn? ctb u1000 tx-sender))
1254+
(define-private (test-transfer-1000)
1255+
(ft-transfer? ctb u1000 tx-sender (as-contract tx-sender)))
1256+
(test-mint)
1257+
(test-burn)
1258+
(test-transfer)
1259+
(test-burn-1000)
1260+
(test-transfer-1000)"#
1261+
);
1262+
1263+
let contract = ClarityContractBuilder::default().code_source(src).build();
1264+
let deploy_result = deploy_contract(&mut interpreter, &contract);
1265+
assert!(deploy_result.is_ok());
1266+
let ExecutionResult {
1267+
diagnostics,
1268+
events,
1269+
result,
1270+
..
1271+
} = deploy_result.unwrap();
1272+
assert!(diagnostics.is_empty());
1273+
assert_eq!(events.len(), 3);
1274+
assert!(matches!(
1275+
events[0],
1276+
StacksTransactionEvent::FTEvent(FTEventType::FTMintEvent(_))
1277+
));
1278+
assert!(matches!(
1279+
events[1],
1280+
StacksTransactionEvent::FTEvent(FTEventType::FTBurnEvent(_))
1281+
));
1282+
assert!(matches!(
1283+
events[2],
1284+
StacksTransactionEvent::FTEvent(FTEventType::FTTransferEvent(_))
1285+
));
1286+
1287+
let contract_identifier_string =
1288+
if let EvaluationResult::Contract(contract_evaluation_result) = result {
1289+
contract_evaluation_result
1290+
.contract
1291+
.contract_identifier
1292+
.clone()
1293+
} else {
1294+
panic!("didn't get EvaluationResult::Contract");
1295+
};
1296+
1297+
let contract_identifier =
1298+
QualifiedContractIdentifier::parse(&contract_identifier_string).unwrap();
1299+
let asset_identifier = AssetIdentifier {
1300+
contract_identifier: contract_identifier.clone(),
1301+
asset_name: token_name.into(),
1302+
};
1303+
1304+
let amount = 1000;
1305+
1306+
let result = interpreter.mint_ft_balance(&asset_identifier, &recipient.clone(), amount);
1307+
assert!(result.is_ok());
1308+
1309+
// in the contract we minted 100, burned 10, then transferred 10 to as-contract
1310+
// in the interpreter we minted 1000
1311+
// this should leave 1080 for recipient
1312+
let balance = interpreter
1313+
.get_balance_for_account(&recipient.to_string(), &asset_identifier.sugared());
1314+
assert_eq!(balance, 1080);
1315+
1316+
let mut global_context = interpreter
1317+
.get_global_context(DEFAULT_EPOCH, false)
1318+
.unwrap();
1319+
1320+
global_context.begin();
1321+
1322+
let supply = global_context
1323+
.database
1324+
.get_ft_supply(&contract_identifier, token_name)
1325+
.expect("failed to get ft supply");
1326+
1327+
// in the contract we minted 100 and burned 10
1328+
// in the interpreter we minted 1000
1329+
// this should leave 1090 total supply
1330+
assert_eq!(supply, 1090);
1331+
1332+
// if we used the correct token names everywhere the interpreter and db
1333+
// balances should match
1334+
let db_balance = global_context
1335+
.database
1336+
.get_ft_balance(&contract_identifier, token_name, &recipient, None)
1337+
.expect("failed to get ft balance");
1338+
1339+
assert_eq!(balance, db_balance);
1340+
1341+
global_context.commit().unwrap();
1342+
}
1343+
11761344
#[test]
11771345
fn test_run_valid_contract() {
11781346
let mut interpreter = get_interpreter(None);

components/clarity-repl/src/repl/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ impl ClarityContract {
217217
PrincipalData::parse_standard_principal(address).expect("unable to parse address")
218218
}
219219
ContractDeployer::DefaultDeployer => default_deployer
220-
.expect("default provider should have been provided")
220+
.expect("default deployer should have been provided")
221221
.clone(),
222222
_ => panic!("deployer expected to be resolved"),
223223
};

0 commit comments

Comments
 (0)