Skip to content

Commit e59d4a3

Browse files
committed
test: Add test coverage for CIP-25/68 handlers and CIP-68 resolver
Signed-off-by: William Hankins <[email protected]>
1 parent 53d3e22 commit e59d4a3

File tree

2 files changed

+362
-2
lines changed

2 files changed

+362
-2
lines changed

common/src/queries/assets.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub struct AssetMintRecord {
4747
pub burn: bool,
4848
}
4949

50-
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
50+
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
5151
pub enum AssetMetadataStandard {
5252
CIP25v1,
5353
CIP25v2,

modules/assets_state/src/state.rs

Lines changed: 361 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,11 +497,17 @@ impl State {
497497

498498
#[cfg(test)]
499499
mod tests {
500+
use std::collections::BTreeMap;
501+
500502
use crate::{
501503
asset_registry::{AssetId, AssetRegistry},
502504
state::{AssetsStorageConfig, State},
503505
};
504-
use acropolis_common::{AssetName, NativeAssetDelta, PolicyId, TxHash};
506+
use acropolis_common::{
507+
queries::assets::{AssetInfoRecord, AssetMetadataStandard},
508+
AssetName, Datum, NativeAsset, NativeAssetDelta, PolicyId, ShelleyAddress, TxHash, TxInput,
509+
TxOutput, UTXODelta, Value,
510+
};
505511

506512
fn dummy_policy(byte: u8) -> PolicyId {
507513
[byte; 28]
@@ -737,4 +743,358 @@ mod tests {
737743
assert!(state.get_asset_transactions(&fake_id).is_err());
738744
assert!(state.get_policy_assets(&dummy_policy(1), &AssetRegistry::new()).is_err());
739745
}
746+
747+
// CIP-25 tests
748+
fn setup_state_with_asset(
749+
registry: &mut AssetRegistry,
750+
policy_id: PolicyId,
751+
asset_name_bytes: &[u8],
752+
seed_info: bool,
753+
) -> (State, AssetId, AssetName) {
754+
let asset_name = AssetName::new(asset_name_bytes).unwrap();
755+
let asset_id = registry.get_or_insert(policy_id, asset_name.clone());
756+
757+
let mut state = State::new(AssetsStorageConfig {
758+
store_info: true,
759+
..Default::default()
760+
});
761+
762+
if seed_info {
763+
state
764+
.info
765+
.get_or_insert_with(Default::default)
766+
.insert(asset_id, AssetInfoRecord::default());
767+
}
768+
769+
(state, asset_id, asset_name)
770+
}
771+
772+
fn build_cip25_metadata(
773+
policy_id: PolicyId,
774+
asset_name: &AssetName,
775+
value: &str,
776+
version: Option<&str>,
777+
) -> Vec<u8> {
778+
let policy_hex = hex::encode(policy_id);
779+
let asset_hex = hex::encode(asset_name.as_slice());
780+
let metadata_value = serde_cbor::Value::Text(value.to_string());
781+
782+
let mut asset_map = BTreeMap::new();
783+
asset_map.insert(serde_cbor::Value::Text(asset_hex), metadata_value);
784+
785+
let mut policy_map = BTreeMap::new();
786+
policy_map.insert(
787+
serde_cbor::Value::Text(policy_hex),
788+
serde_cbor::Value::Map(asset_map),
789+
);
790+
791+
if let Some(ver) = version {
792+
policy_map.insert(
793+
serde_cbor::Value::Text("version".to_string()),
794+
serde_cbor::Value::Text(ver.to_string()),
795+
);
796+
}
797+
798+
serde_cbor::to_vec(&serde_cbor::Value::Map(policy_map)).unwrap()
799+
}
800+
801+
#[test]
802+
fn handle_cip25_metadata_updates_correct_asset() {
803+
let mut registry = AssetRegistry::new();
804+
let policy_id: PolicyId = [0u8; 28].into();
805+
806+
let (state, asset_id, asset_name) =
807+
setup_state_with_asset(&mut registry, policy_id, b"TestAsset", true);
808+
809+
let metadata_cbor = build_cip25_metadata(policy_id, &asset_name, "hello world", None);
810+
811+
let new_state = state.handle_cip25_metadata(&mut registry, &[metadata_cbor]).unwrap();
812+
let info = new_state.info.expect("info should be Some");
813+
let record = info.get(&asset_id).unwrap();
814+
815+
// Onchain metadata has been set
816+
assert!(record.onchain_metadata.is_some());
817+
// Metadata standard defaults to v1 if not present in map
818+
assert_eq!(
819+
record.metadata_standard,
820+
Some(AssetMetadataStandard::CIP25v1)
821+
);
822+
}
823+
824+
#[test]
825+
fn handle_cip25_metadata_version_field_sets_v2() {
826+
let mut registry = AssetRegistry::new();
827+
let policy_id: PolicyId = [1u8; 28].into();
828+
829+
let (state, asset_id, asset_name) =
830+
setup_state_with_asset(&mut registry, policy_id, b"VersionedAsset", true);
831+
832+
let metadata_cbor =
833+
build_cip25_metadata(policy_id, &asset_name, "metadata for v2", Some("2.0"));
834+
835+
let new_state = state.handle_cip25_metadata(&mut registry, &[metadata_cbor]).unwrap();
836+
let info = new_state.info.expect("info should be Some");
837+
let record = info.get(&asset_id).unwrap();
838+
839+
// Onchain metadata has been set
840+
assert!(record.onchain_metadata.is_some());
841+
// Metadata standard set to v2 when present in map
842+
assert_eq!(
843+
record.metadata_standard,
844+
Some(AssetMetadataStandard::CIP25v2)
845+
);
846+
}
847+
848+
#[test]
849+
fn handle_cip25_metadata_unknown_asset_is_ignored() {
850+
let mut registry = AssetRegistry::new();
851+
let policy_id: PolicyId = [2u8; 28].into();
852+
let (state, asset_id, _) =
853+
setup_state_with_asset(&mut registry, policy_id, b"KnownAsset", true);
854+
855+
let other_asset_name = AssetName::new(b"UnknownAsset").unwrap();
856+
let metadata_cbor =
857+
build_cip25_metadata(policy_id, &other_asset_name, "ignored metadata", None);
858+
859+
let new_state = state.handle_cip25_metadata(&mut registry, &[metadata_cbor]).unwrap();
860+
let info = new_state.info.expect("info should be Some");
861+
let record = info.get(&asset_id).unwrap();
862+
863+
// Metadata for known asset unchanged by unknown asset
864+
assert!(
865+
record.onchain_metadata.is_none(),
866+
"unknown asset should not update records"
867+
);
868+
}
869+
870+
#[test]
871+
fn handle_cip25_metadata_invalid_cbor_is_skipped() {
872+
let mut registry = AssetRegistry::new();
873+
let policy_id: PolicyId = [3u8; 28].into();
874+
let (state, asset_id, _) =
875+
setup_state_with_asset(&mut registry, policy_id, b"BadAsset", true);
876+
877+
let metadata_cbor = vec![0xff, 0x00, 0x13, 0x37];
878+
879+
let new_state = state.handle_cip25_metadata(&mut registry, &[metadata_cbor]).unwrap();
880+
let info = new_state.info.expect("info should be Some");
881+
let record = info.get(&asset_id).unwrap();
882+
883+
// Metadata not set when CBOR is invalid
884+
assert!(
885+
record.onchain_metadata.is_none(),
886+
"invalid CBOR should be ignored"
887+
);
888+
// Metadata standard not set when CBOR is invalid
889+
assert!(
890+
record.metadata_standard.is_none(),
891+
"invalid CBOR should not set a standard"
892+
);
893+
}
894+
895+
// CIP-68 tests
896+
fn dummy_address() -> acropolis_common::Address {
897+
acropolis_common::Address::Shelley(
898+
ShelleyAddress::from_string(
899+
"addr1q9g0u0aeuyvrn8ptc6yesgj6dtfgw2gadnc9y2p9cs8pneejrkwtdvk97yp2zayhvmm3wu0v672psdg2xn0temkz83ds7qfxdt",
900+
)
901+
.unwrap(),
902+
)
903+
}
904+
905+
fn make_output(policy_id: PolicyId, asset_name: AssetName, datum: Option<Vec<u8>>) -> TxOutput {
906+
TxOutput {
907+
tx_hash: [0u8; 32].into(),
908+
index: 0,
909+
address: dummy_address(),
910+
value: Value {
911+
lovelace: 0,
912+
assets: vec![(
913+
policy_id,
914+
vec![NativeAsset {
915+
name: asset_name,
916+
amount: 1,
917+
}],
918+
)],
919+
},
920+
datum: datum.map(Datum::Inline),
921+
}
922+
}
923+
924+
#[test]
925+
fn handle_cip68_metadata_updates_onchain_metadata() {
926+
let mut registry = AssetRegistry::new();
927+
let policy_id: PolicyId = [9u8; 28].into();
928+
929+
let (state, reference_id, reference_name) = setup_state_with_asset(
930+
&mut registry,
931+
policy_id,
932+
&[0x00, 0x06, 0x43, 0xb0, 0x01],
933+
true,
934+
);
935+
936+
let datum_blob = vec![1, 2, 3, 4];
937+
let output = make_output(policy_id, reference_name.clone(), Some(datum_blob.clone()));
938+
939+
let new_state =
940+
state.handle_cip68_metadata(&[UTXODelta::Output(output)], &mut registry).unwrap();
941+
let info = new_state.info.expect("info should be Some");
942+
let record = info.get(&reference_id).expect("record should exist");
943+
944+
// Onchain metadata set when asset already exists and TxOutput with inline datum is processed
945+
assert_eq!(record.onchain_metadata, Some(datum_blob));
946+
}
947+
948+
#[test]
949+
fn handle_cip68_metadata_ignores_non_reference_assets() {
950+
let mut registry = AssetRegistry::new();
951+
let policy_id: PolicyId = [9u8; 28].into();
952+
953+
let (state, normal_id, normal_name) =
954+
setup_state_with_asset(&mut registry, policy_id, &[0xAA, 0xBB, 0xCC], true);
955+
956+
let datum_blob = vec![1, 2, 3, 4];
957+
let output = make_output(policy_id, normal_name.clone(), Some(datum_blob.clone()));
958+
959+
let delta = UTXODelta::Output(output);
960+
let new_state = state.handle_cip68_metadata(&[delta], &mut registry).unwrap();
961+
962+
let info = new_state.info.expect("info should be Some");
963+
let record = info.get(&normal_id).expect("non reference asset should exist");
964+
965+
// Onchain metadata not updated for non reference asset
966+
assert_eq!(record.onchain_metadata, None);
967+
}
968+
969+
#[test]
970+
fn handle_cip68_metadata_ignores_unknown_reference_assets() {
971+
let mut registry = AssetRegistry::new();
972+
let policy_id: PolicyId = [9u8; 28].into();
973+
974+
let (state, asset_id, name) = setup_state_with_asset(
975+
&mut registry,
976+
policy_id,
977+
&[0x00, 0x06, 0x43, 0xb0, 0x02],
978+
false,
979+
);
980+
981+
let datum_blob = vec![1, 2, 3, 4];
982+
let output = make_output(policy_id, name, Some(datum_blob));
983+
984+
let delta = UTXODelta::Output(output);
985+
let new_state = state.handle_cip68_metadata(&[delta], &mut registry).unwrap();
986+
987+
let info = new_state.info.expect("info should be Some");
988+
989+
// Metadata not populated if asset does not exist
990+
assert!(
991+
info.get(&asset_id).is_none(),
992+
"unknown reference assets should be ignored"
993+
);
994+
}
995+
996+
#[test]
997+
fn handle_cip68_metadata_ignores_inputs_and_outputs_without_datum() {
998+
let mut registry = AssetRegistry::new();
999+
let policy_id: PolicyId = [7u8; 28].into();
1000+
1001+
let (state, asset_id, name) = setup_state_with_asset(
1002+
&mut registry,
1003+
policy_id,
1004+
&[0x00, 0x06, 0x43, 0xb0, 0x02],
1005+
true,
1006+
);
1007+
1008+
let input_delta = UTXODelta::Input(TxInput {
1009+
tx_hash: [1u8; 32].into(),
1010+
index: 0,
1011+
});
1012+
let output = make_output(policy_id, name, None);
1013+
let output_delta = UTXODelta::Output(output);
1014+
1015+
let new_state =
1016+
state.handle_cip68_metadata(&[input_delta, output_delta], &mut registry).unwrap();
1017+
1018+
let info = new_state.info.expect("info should be Some");
1019+
let record = info.get(&asset_id).unwrap();
1020+
1021+
// Metadata not populated for inputs or outputs without inline datum
1022+
assert!(
1023+
record.onchain_metadata.is_none(),
1024+
"inputs and outputs without datums should both be ignored"
1025+
);
1026+
}
1027+
1028+
#[test]
1029+
fn get_asset_info_reference_nft_strips_metadata() {
1030+
let mut registry = AssetRegistry::new();
1031+
let policy_id: PolicyId = [9u8; 28].into();
1032+
1033+
let (mut state, ref_id, _) =
1034+
setup_state_with_asset(&mut registry, policy_id, &[0x00, 0x06, 0x43, 0xb0], true);
1035+
1036+
let mut info = state.info.take().unwrap();
1037+
let rec = info.get_mut(&ref_id).unwrap();
1038+
rec.onchain_metadata = Some(vec![1, 2, 3]);
1039+
rec.metadata_standard = Some(AssetMetadataStandard::CIP68v1);
1040+
state.info = Some(info);
1041+
1042+
state.supply = Some(imbl::HashMap::new());
1043+
state.supply.as_mut().unwrap().insert(ref_id, 42);
1044+
1045+
let result = state.get_asset_info(&ref_id, &registry).unwrap().unwrap();
1046+
let (supply, rec) = result;
1047+
1048+
// Supply unchanged
1049+
assert_eq!(supply, 42);
1050+
// Metadata removed for reference asset
1051+
assert!(rec.onchain_metadata.is_none());
1052+
// Metadata standard removed for reference asset
1053+
assert!(rec.metadata_standard.is_none());
1054+
}
1055+
1056+
#[test]
1057+
fn resolve_cip68_metadata_overwrites_cip25_user_token_metadata() {
1058+
let mut registry = AssetRegistry::new();
1059+
let policy_id: PolicyId = [10u8; 28].into();
1060+
1061+
let user_name = AssetName::new(&[0x00, 0x0d, 0xe1, 0x40, 0xaa]).unwrap();
1062+
let user_id = registry.get_or_insert(policy_id, user_name.clone());
1063+
1064+
let mut ref_bytes = user_name.as_slice().to_vec();
1065+
ref_bytes[0..4].copy_from_slice(&[0x00, 0x06, 0x43, 0xb0]);
1066+
let ref_name = AssetName::new(&ref_bytes).unwrap();
1067+
let ref_id = registry.get_or_insert(policy_id, ref_name);
1068+
1069+
let mut state = State::new(AssetsStorageConfig {
1070+
store_info: true,
1071+
..Default::default()
1072+
});
1073+
let mut info_map = imbl::HashMap::new();
1074+
1075+
let mut user_record = AssetInfoRecord::default();
1076+
user_record.onchain_metadata = Some(vec![1, 2, 3]);
1077+
user_record.metadata_standard = Some(AssetMetadataStandard::CIP25v1);
1078+
info_map.insert(user_id, user_record);
1079+
1080+
let mut ref_record = AssetInfoRecord::default();
1081+
ref_record.onchain_metadata = Some(vec![9, 9, 9]);
1082+
ref_record.metadata_standard = Some(AssetMetadataStandard::CIP68v2);
1083+
info_map.insert(ref_id, ref_record);
1084+
1085+
state.info = Some(info_map);
1086+
1087+
state.supply = Some(imbl::HashMap::new());
1088+
state.supply.as_mut().unwrap().insert(user_id, 100);
1089+
1090+
let result = state.get_asset_info(&user_id, &registry).unwrap().unwrap();
1091+
let (supply, rec) = result;
1092+
1093+
// User asset supply unchanged
1094+
assert_eq!(supply, 100);
1095+
// User asset metadata overwritten with reference token metadata
1096+
assert_eq!(rec.onchain_metadata, Some(vec![9, 9, 9]));
1097+
// User asset metadata standard overwritten with reference token metadata standard
1098+
assert_eq!(rec.metadata_standard, Some(AssetMetadataStandard::CIP68v2));
1099+
}
7401100
}

0 commit comments

Comments
 (0)