From e3afa8da7c5dba41cac579f4204252eff452849a Mon Sep 17 00:00:00 2001 From: mansur-codes Date: Fri, 28 Aug 2026 08:16:31 +0100 Subject: [PATCH] `feat(tests): add credential/ZK subsystem test coverage (#883)` --- .../contracts/linkora-contracts/src/test.rs | 108 +++++++- .../test/test_tip_fee_split.1.json | 241 ++++++++++++++++-- .../test_verify_credential_empty_proof.1.json | 30 +-- 3 files changed, 328 insertions(+), 51 deletions(-) diff --git a/packages/contracts/contracts/linkora-contracts/src/test.rs b/packages/contracts/contracts/linkora-contracts/src/test.rs index e80f804b..7c83f183 100644 --- a/packages/contracts/contracts/linkora-contracts/src/test.rs +++ b/packages/contracts/contracts/linkora-contracts/src/test.rs @@ -502,6 +502,7 @@ fn test_tip_fee_split() { // Initialize with 2.5% fee (250 bps) client.initialize(&admin, &treasury, &250); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "Fee test post")); @@ -534,6 +535,7 @@ fn test_tip_blocked_by_author() { client.initialize(&admin, &treasury, &250); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "Test post")); @@ -559,6 +561,7 @@ fn test_tip_after_unblock() { client.initialize(&admin, &treasury, &250); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "Test post")); @@ -594,6 +597,7 @@ fn test_tip_non_blocked_user() { let token = setup_token(&env, &tipper1); StellarAssetClient::new(&env, &token).mint(&tipper2, &5000); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let post_id = client.create_post(&author, &String::from_str(&env, "Test post")); // Author blocks tipper1 @@ -631,6 +635,7 @@ fn test_tip_block_preserves_no_state_changes_on_panic() { client.initialize(&admin, &treasury, &250); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "block-prevented tip post")); @@ -724,6 +729,7 @@ fn test_tip_block_is_unidirectional_blocker_can_still_tip_blocked() { let token = setup_token(&env, &blocker); StellarAssetClient::new(&env, &token).mint(&blocked_user, &10_000); + client.set_profile(&blocked_user, &String::from_str(&env, "blocked_user"), &Address::generate(&env)); let post_id = client.create_post( &blocked_user, &String::from_str(&env, "blocked_user is the author here"), @@ -778,6 +784,7 @@ fn test_tip_block_multiple_blocked_tippers_panic_independently() { StellarAssetClient::new(&env, &token).mint(&blocked_b, &5_000); StellarAssetClient::new(&env, &token).mint(&unblocked, &10_000); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let post_id = client.create_post(&author, &String::from_str(&env, "multi-block post")); // Author blocks two different addresses. @@ -2951,6 +2958,7 @@ fn test_tip_full_flow_no_fee() { // Initialize with fee_bps = 0 (no fee) client.initialize(&admin, &treasury, &0); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "No-fee tip test")); @@ -2995,6 +3003,7 @@ fn test_tip_full_flow_with_5_percent_fee() { // Initialize with fee_bps = 500 (5%) client.initialize(&admin, &treasury, &500); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "5% fee tip test")); @@ -3047,6 +3056,7 @@ fn test_tip_total_increments_across_multiple_tips() { // Mint tokens for tipper2 as well StellarAssetClient::new(&env, &token).mint(&tipper2, &5000); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let post_id = client.create_post(&author, &String::from_str(&env, "Multi-tip test")); // First tip from tipper1 (advance ledger to bypass cooldown) @@ -3080,6 +3090,7 @@ fn test_tip_fee_split_matches_fee_bps_config() { // Use 250 bps (2.5%) client.initialize(&admin, &treasury, &250); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "Fee split config test")); @@ -3601,6 +3612,7 @@ fn test_tip_cooldown_rejects_within_window() { // Use a short window so both tips happen within it client.set_tip_cooldown_window(&admin, &10); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "cooldown test post")); @@ -3625,6 +3637,7 @@ fn test_tip_cooldown_allows_after_window() { client.initialize(&admin, &treasury, &0); client.set_tip_cooldown_window(&admin, &10); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "cooldown test post")); @@ -3936,6 +3949,7 @@ fn test_tip_cooldown_uses_typed_storage_key() { client.initialize(&admin, &treasury, &0); client.set_tip_cooldown_window(&admin, &100); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "cooldown key test")); @@ -5037,6 +5051,7 @@ fn test_tip_cooldown_100_ledgers_immediate_retip_panics() { client.initialize(&admin, &treasury, &0); client.set_tip_cooldown_window(&admin, &100); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "cooldown 100 post")); @@ -5062,6 +5077,7 @@ fn test_tip_cooldown_100_ledgers_allows_after_advance() { client.initialize(&admin, &treasury, &0); client.set_tip_cooldown_window(&admin, &100); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "cooldown 100 post")); @@ -5527,6 +5543,7 @@ fn test_tip_panics_when_tipper_blocked_author() { let tipper = Address::generate(&env); let author = Address::generate(&env); + client.set_profile(&author, &String::from_str(&env, "author"), &Address::generate(&env)); let token = setup_token(&env, &tipper); let post_id = client.create_post(&author, &String::from_str(&env, "test post")); @@ -7686,7 +7703,7 @@ fn test_verify_credential_empty_proof() { let user = Address::generate(&env); - // For empty proof, root should equal leaf + // For empty proof (depth-0), root should equal leaf let leaf = BytesN::from_array(&env, &[1u8; 32]); let root = leaf.clone(); let proof: Vec> = vec![&env]; @@ -7697,10 +7714,15 @@ fn test_verify_credential_empty_proof() { &sign_credential_root(&env, &signing_key, &root), ); - let nullifier = BytesN::from_array(&env, &[10u8; 32]); - let result = client.verify_credential(&user, &proof, &leaf, &nullifier); + let nullifier1 = BytesN::from_array(&env, &[10u8; 32]); + let result1 = client.verify_credential(&user, &proof, &leaf, &nullifier1); + assert!(result1, "empty proof should work when root equals leaf"); - assert!(result, "empty proof should work when root equals leaf"); + // Empty proof with wrong leaf (leaf != root) should fail + let wrong_leaf = BytesN::from_array(&env, &[2u8; 32]); + let nullifier2 = BytesN::from_array(&env, &[20u8; 32]); + let result2 = client.verify_credential(&user, &proof, &wrong_leaf, &nullifier2); + assert!(!result2, "empty proof with non-matching leaf should return false"); } #[test] @@ -7735,6 +7757,84 @@ fn test_verify_credential_max_depth_proof() { assert!(result, "max depth proof should verify correctly"); } +#[test] +fn test_update_credential_root_32_bytes_accepted() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _) = setup_contract(&env); + let signing_key = credential_authority_signing_key(1); + client.set_credential_authority(&admin, &credential_authority_pubkey(&env, &signing_key)); + + let user = Address::generate(&env); + let root = BytesN::from_array(&env, &[0xab; 32]); + + client.update_credential_root( + &user, + &root, + &sign_credential_root(&env, &signing_key, &root), + ); + + let retrieved = client.get_credential_root(&user).unwrap(); + assert_eq!(retrieved.to_array().len(), 32); + assert_eq!(retrieved, root); +} + +#[test] +fn test_get_credential_root_correct_after_update() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _) = setup_contract(&env); + let signing_key = credential_authority_signing_key(1); + client.set_credential_authority(&admin, &credential_authority_pubkey(&env, &signing_key)); + + let user = Address::generate(&env); + let root = BytesN::from_array(&env, &[42u8; 32]); + + assert!(client.get_credential_root(&user).is_none()); + + client.update_credential_root( + &user, + &root, + &sign_credential_root(&env, &signing_key, &root), + ); + + let retrieved = client.get_credential_root(&user); + assert_eq!(retrieved, Some(root)); +} + +#[test] +fn test_get_credential_root_none_after_cleared() { + // Contract has no explicit clear_root API; root is cleared when user profile is deleted via delete_profile + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _) = setup_contract(&env); + let signing_key = credential_authority_signing_key(1); + client.set_credential_authority(&admin, &credential_authority_pubkey(&env, &signing_key)); + + let user = Address::generate(&env); + client.set_profile( + &user, + &String::from_str(&env, "alice"), + &Address::generate(&env), + ); + + let root = BytesN::from_array(&env, &[7u8; 32]); + client.update_credential_root( + &user, + &root, + &sign_credential_root(&env, &signing_key, &root), + ); + assert_eq!(client.get_credential_root(&user), Some(root)); + + // Deleting profile removes StorageKey::CredentialRoot(user) + client.delete_profile(&user); + assert_eq!( + client.get_credential_root(&user), + None, + "should return None after credential root storage is cleared on delete_profile" + ); +} + // ── Issue #956: governance parameter bounds ────────────────────────────────── #[test] diff --git a/packages/contracts/contracts/linkora-contracts/test_snapshots/test/test_tip_fee_split.1.json b/packages/contracts/contracts/linkora-contracts/test_snapshots/test/test_tip_fee_split.1.json index cb52dba7..80116c14 100644 --- a/packages/contracts/contracts/linkora-contracts/test_snapshots/test/test_tip_fee_split.1.json +++ b/packages/contracts/contracts/linkora-contracts/test_snapshots/test/test_tip_fee_split.1.json @@ -1,6 +1,6 @@ { "generators": { - "address": 6, + "address": 7, "nonce": 0, "mux_id": 0 }, @@ -31,14 +31,39 @@ } ] ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "set_profile", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "string": "author" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], [], [ [ - "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANHUF", + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPP4V", { "function": { "contract_fn": { - "contract_address": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN", + "contract_address": "CDS3FDGQ4JA2V3F26Y4BMWWJEC5TT26RJBN7KIQKUMVO2MAOCMDTSZ7A", "function_name": "set_admin", "args": [ { @@ -57,7 +82,7 @@ { "function": { "contract_fn": { - "contract_address": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN", + "contract_address": "CDS3FDGQ4JA2V3F26Y4BMWWJEC5TT26RJBN7KIQKUMVO2MAOCMDTSZ7A", "function_name": "mint", "args": [ { @@ -111,7 +136,7 @@ "u64": "1" }, { - "address": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN" + "address": "CDS3FDGQ4JA2V3F26Y4BMWWJEC5TT26RJBN7KIQKUMVO2MAOCMDTSZ7A" }, { "i128": "1000" @@ -123,7 +148,7 @@ { "function": { "contract_fn": { - "contract_address": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN", + "contract_address": "CDS3FDGQ4JA2V3F26Y4BMWWJEC5TT26RJBN7KIQKUMVO2MAOCMDTSZ7A", "function_name": "transfer", "args": [ { @@ -143,7 +168,7 @@ { "function": { "contract_fn": { - "contract_address": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN", + "contract_address": "CDS3FDGQ4JA2V3F26Y4BMWWJEC5TT26RJBN7KIQKUMVO2MAOCMDTSZ7A", "function_name": "transfer", "args": [ { @@ -183,7 +208,7 @@ "last_modified_ledger_seq": 0, "data": { "account": { - "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANHUF", + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPP4V", "balance": "0", "seq_num": "0", "num_sub_entries": 0, @@ -205,10 +230,10 @@ "data": { "contract_data": { "ext": "v0", - "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANHUF", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPP4V", "key": { "ledger_key_nonce": { - "nonce": "5541220902715666415" + "nonce": "1033654523790656264" } }, "durability": "temporary", @@ -250,6 +275,60 @@ }, "live_until": 535000 }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "FollowersCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 535000 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "FollowingCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + "live_until": 535000 + }, { "entry": { "last_modified_ledger_seq": 0, @@ -383,6 +462,58 @@ }, "live_until": 535000 }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Profile" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "address" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "creator_token" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "username" + }, + "val": { + "string": "author" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 535000 + }, { "entry": { "last_modified_ledger_seq": 0, @@ -413,6 +544,33 @@ }, "live_until": 535000 }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "UsernameIndex" + }, + { + "string": "author" + } + ] + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + } + }, + "ext": "v0" + }, + "live_until": 535000 + }, { "entry": { "last_modified_ledger_seq": 0, @@ -507,6 +665,14 @@ "u64": "1" } }, + { + "key": { + "symbol": "PROF_CT" + }, + "val": { + "u64": "1" + } + }, { "key": { "symbol": "ROLES" @@ -524,6 +690,23 @@ ] } }, + { + "key": { + "symbol": "R_USERS" + }, + "val": { + "map": [ + { + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + "val": { + "bool": true + } + } + ] + } + }, { "key": { "symbol": "TIP_CD_W" @@ -578,7 +761,27 @@ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", "key": { "ledger_key_nonce": { - "nonce": "4837995959683129791" + "nonce": "2032731177588607455" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" } }, "durability": "temporary", @@ -598,7 +801,7 @@ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", "key": { "ledger_key_nonce": { - "nonce": "1033654523790656264" + "nonce": "4270020994084947596" } }, "durability": "temporary", @@ -618,7 +821,7 @@ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", "key": { "ledger_key_nonce": { - "nonce": "2032731177588607455" + "nonce": "4837995959683129791" } }, "durability": "temporary", @@ -635,7 +838,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN", + "contract": "CDS3FDGQ4JA2V3F26Y4BMWWJEC5TT26RJBN7KIQKUMVO2MAOCMDTSZ7A", "key": { "vec": [ { @@ -687,7 +890,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN", + "contract": "CDS3FDGQ4JA2V3F26Y4BMWWJEC5TT26RJBN7KIQKUMVO2MAOCMDTSZ7A", "key": { "vec": [ { @@ -739,7 +942,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN", + "contract": "CDS3FDGQ4JA2V3F26Y4BMWWJEC5TT26RJBN7KIQKUMVO2MAOCMDTSZ7A", "key": { "vec": [ { @@ -791,7 +994,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN", + "contract": "CDS3FDGQ4JA2V3F26Y4BMWWJEC5TT26RJBN7KIQKUMVO2MAOCMDTSZ7A", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -817,7 +1020,7 @@ "symbol": "name" }, "val": { - "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANHUF" + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPP4V" } }, { @@ -871,7 +1074,7 @@ "symbol": "issuer" }, "val": { - "bytes": "0000000000000000000000000000000000000000000000000000000000000006" + "bytes": "0000000000000000000000000000000000000000000000000000000000000007" } } ] diff --git a/packages/contracts/contracts/linkora-contracts/test_snapshots/test/test_verify_credential_empty_proof.1.json b/packages/contracts/contracts/linkora-contracts/test_snapshots/test/test_verify_credential_empty_proof.1.json index 03d4a91c..b53311c1 100644 --- a/packages/contracts/contracts/linkora-contracts/test_snapshots/test/test_verify_credential_empty_proof.1.json +++ b/packages/contracts/contracts/linkora-contracts/test_snapshots/test/test_verify_credential_empty_proof.1.json @@ -78,6 +78,7 @@ } ] ], + [], [] ], "ledger": { @@ -375,32 +376,5 @@ } ] }, - "events": [ - { - "event": { - "ext": "v0", - "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "credential_verified_event" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "bytes": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a" - } - ], - "data": { - "map": [] - } - } - } - }, - "failed_call": false - } - ] + "events": [] } \ No newline at end of file