1#[cfg(not(feature = "library"))]
2use cosmwasm_std::entry_point;
3use cosmwasm_std::{
4 instantiate2_address, to_json_binary, Binary, CodeInfoResponse, Deps, DepsMut, Env,
5 MessageInfo, Response, StdError, StdResult, WasmMsg,
6};
7use cw2::set_contract_version;
8use cw_utils::nonpayable;
9
10use crate::error::ContractError;
11use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
12use crate::state::{Dataverse, DATAVERSE};
13
14const CONTRACT_NAME: &str = concat!("crates.io:", env!("CARGO_PKG_NAME"));
16const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
17
18#[cfg_attr(not(feature = "library"), entry_point)]
19pub fn instantiate(
20 deps: DepsMut<'_>,
21 env: Env,
22 info: MessageInfo,
23 msg: InstantiateMsg,
24) -> Result<Response, ContractError> {
25 nonpayable(&info)?;
26 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
27
28 let creator = deps.api.addr_canonicalize(env.contract.address.as_str())?;
29 let CodeInfoResponse { checksum, .. } = deps
30 .querier
31 .query_wasm_code_info(msg.triplestore_config.code_id.u64())?;
32 let salt = Binary::from(msg.name.as_bytes());
33
34 let _triplestore_address = instantiate2_address(checksum.as_slice(), &creator, &salt)?;
35
36 let triplestore_address = {
38 #[cfg(not(test))]
39 {
40 deps.api.addr_humanize(&_triplestore_address)?
41 }
42 #[cfg(test)]
43 cosmwasm_std::Addr::unchecked("predicted address")
44 };
45
46 DATAVERSE.save(
47 deps.storage,
48 &Dataverse {
49 name: msg.name.clone(),
50 triplestore_address: triplestore_address.clone(),
51 },
52 )?;
53
54 Ok(Response::new()
55 .add_attribute("triplestore_address", triplestore_address.to_string())
56 .add_message(WasmMsg::Instantiate2 {
57 admin: Some(env.contract.address.to_string()),
58 code_id: msg.triplestore_config.code_id.u64(),
59 label: format!("{}_triplestore", msg.name),
60 msg: to_json_binary(&axone_cognitarium::msg::InstantiateMsg {
61 limits: msg.triplestore_config.limits.into(),
62 })?,
63 funds: vec![],
64 salt,
65 }))
66}
67
68#[cfg_attr(not(feature = "library"), entry_point)]
69pub fn execute(
70 deps: DepsMut<'_>,
71 env: Env,
72 info: MessageInfo,
73 msg: ExecuteMsg,
74) -> Result<Response, ContractError> {
75 nonpayable(&info)?;
76 match msg {
77 ExecuteMsg::SubmitClaims { claims, format: _ } => {
78 execute::submit_claims(deps, env, info, claims)
79 }
80 _ => Err(StdError::generic_err("Not implemented").into()),
81 }
82}
83
84pub mod execute {
85 use super::*;
86 use crate::credential::error::VerificationError;
87 use crate::credential::vc::VerifiableCredential;
88 use crate::registrar::credential::DataverseCredential;
89 use crate::registrar::registry::ClaimRegistrar;
90 use axone_rdf::dataset::Dataset;
91 use axone_rdf::serde::NQuadsReader;
92 use std::io::BufReader;
93
94 pub fn submit_claims(
95 deps: DepsMut<'_>,
96 env: Env,
97 info: MessageInfo,
98 claims: Binary,
99 ) -> Result<Response, ContractError> {
100 let buf = BufReader::new(claims.as_slice());
101 let mut reader = NQuadsReader::new(buf);
102 let rdf_quads = reader.read_all()?;
103 let vc_dataset = Dataset::from(rdf_quads.as_slice());
104 let vc = VerifiableCredential::try_from(&vc_dataset)?;
105
106 if !vc.proof.is_empty() {
110 vc.verify(&deps)?;
111 } else if !vc.is_issued_by(&info.sender) {
112 Err(VerificationError::NoSuitableProof)?;
113 }
114
115 let credential = DataverseCredential::try_from((env, info, &vc))?;
116 let registrar = ClaimRegistrar::try_new(deps.storage)?;
117
118 Ok(Response::default()
119 .add_attribute("action", "submit_claims")
120 .add_attribute("credential", credential.id)
121 .add_attribute("subject", credential.claim.id)
122 .add_attribute("type", credential.r#type)
123 .add_message(registrar.submit_claim(&deps, &credential)?))
124 }
125}
126
127#[cfg_attr(not(feature = "library"), entry_point)]
128pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
129 match msg {
130 QueryMsg::Dataverse {} => to_json_binary(&query::dataverse(deps)?),
131 }
132}
133
134pub mod query {
135 use crate::msg::DataverseResponse;
136 use crate::state::DATAVERSE;
137 use cosmwasm_std::{Deps, StdResult};
138
139 pub fn dataverse(deps: Deps<'_>) -> StdResult<DataverseResponse> {
140 DATAVERSE.load(deps.storage).map(|d| DataverseResponse {
141 name: d.name,
142 triplestore_address: d.triplestore_address,
143 })
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::msg::{
151 DataverseResponse, RdfDatasetFormat, TripleStoreConfig, TripleStoreLimitsInput,
152 };
153 use crate::testutil::testutil::read_test_data;
154 use axone_cognitarium::msg::{
155 DataFormat, Head, Node, Results, SelectItem, SelectQuery, SelectResponse, TriplePattern,
156 Value, VarOrNamedNode, VarOrNode, VarOrNodeOrLiteral, WhereClause, IRI,
157 };
158 use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
159 use cosmwasm_std::{
160 coins, from_json, Addr, Attribute, Checksum, ContractResult, CosmosMsg, SubMsg,
161 SystemError, SystemResult, Uint128, Uint64, WasmQuery,
162 };
163 use cw_utils::PaymentError::NonPayable;
164 use std::collections::BTreeMap;
165 use testing::addr::{addr, CREATOR, SENDER};
166 use testing::mock::mock_env_addr;
167
168 #[test]
169 fn proper_instantiate() {
170 let mut deps = mock_dependencies();
171 deps.querier.update_wasm(|query| match query {
172 WasmQuery::CodeInfo { code_id, .. } => {
173 let resp = CodeInfoResponse::new(
174 code_id.clone(),
175 addr(CREATOR),
176 Checksum::from_hex(
177 "3B94AAF0B7D804B5B458DED0D20CACF95D2A1C8DF78ED3C89B61291760454AEC",
178 )
179 .unwrap(),
180 );
181 SystemResult::Ok(ContractResult::Ok(to_json_binary(&resp).unwrap()))
182 }
183 _ => SystemResult::Err(SystemError::Unknown {}),
184 });
185
186 let store_limits = TripleStoreLimitsInput {
187 max_byte_size: Some(Uint128::from(50000u128)),
188 ..Default::default()
189 };
190
191 let msg = InstantiateMsg {
192 name: "my-dataverse".to_string(),
193 triplestore_config: TripleStoreConfig {
194 code_id: Uint64::from(17u64),
195 limits: store_limits.clone(),
196 },
197 };
198
199 let env = mock_env_addr();
200 let info = message_info(&addr(CREATOR), &[]);
201 let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
202
203 assert_eq!(
204 res.attributes,
205 vec![Attribute::new("triplestore_address", "predicted address")]
206 );
207 assert_eq!(
208 res.messages,
209 vec![SubMsg::new(WasmMsg::Instantiate2 {
210 admin: Some(env.contract.address.to_string()),
211 code_id: 17,
212 label: "my-dataverse_triplestore".to_string(),
213 msg: to_json_binary(&axone_cognitarium::msg::InstantiateMsg {
214 limits: store_limits.into(),
215 })
216 .unwrap(),
217 funds: vec![],
218 salt: Binary::from("my-dataverse".as_bytes()),
219 })]
220 );
221 assert_eq!(
222 DATAVERSE.load(&deps.storage).unwrap(),
223 Dataverse {
224 name: "my-dataverse".to_string(),
225 triplestore_address: Addr::unchecked("predicted address"),
226 }
227 )
228 }
229
230 #[test]
231 fn funds_initialization() {
232 let mut deps = mock_dependencies();
233 let env = mock_env();
234 let info = message_info(&addr(SENDER), &coins(10, "uaxone"));
235
236 let msg = InstantiateMsg {
237 name: "my-dataverse".to_string(),
238 triplestore_config: TripleStoreConfig {
239 code_id: Uint64::from(17u64),
240 limits: TripleStoreLimitsInput::default(),
241 },
242 };
243
244 let result = instantiate(deps.as_mut(), env, info, msg);
245 assert!(result.is_err());
246 assert!(matches!(
247 result.unwrap_err(),
248 ContractError::Payment(NonPayable {})
249 ));
250 }
251
252 #[test]
253 fn proper_dataverse() {
254 let mut deps = mock_dependencies();
255
256 DATAVERSE
257 .save(
258 deps.as_mut().storage,
259 &Dataverse {
260 name: "my-dataverse".to_string(),
261 triplestore_address: Addr::unchecked("my-dataverse-addr"),
262 },
263 )
264 .unwrap();
265
266 let res = query(deps.as_ref(), mock_env(), QueryMsg::Dataverse {});
267 assert!(res.is_ok());
268 let res: StdResult<DataverseResponse> = from_json(res.unwrap());
269 assert!(res.is_ok());
270 assert_eq!(
271 res.unwrap(),
272 DataverseResponse {
273 name: "my-dataverse".to_string(),
274 triplestore_address: Addr::unchecked("my-dataverse-addr"),
275 }
276 );
277 }
278
279 #[test]
280 fn execute_fail_with_funds() {
281 let mut deps = mock_dependencies();
282 let env = mock_env();
283 let info = message_info(&addr(SENDER), &coins(10, "uaxone"));
284
285 let msg = ExecuteMsg::SubmitClaims {
286 claims: Binary::from("data".as_bytes()),
287 format: Some(RdfDatasetFormat::NQuads),
288 };
289
290 let result = execute(deps.as_mut(), env, info, msg);
291 assert!(result.is_err());
292 assert!(matches!(
293 result.unwrap_err(),
294 ContractError::Payment(NonPayable {})
295 ));
296 }
297
298 #[test]
299 fn proper_submit_claims() {
300 let mut deps = mock_dependencies();
301 deps.querier.update_wasm(|query| match query {
302 WasmQuery::Smart { contract_addr, msg } => {
303 if contract_addr != "my-dataverse-addr" {
304 return SystemResult::Err(SystemError::NoSuchContract {
305 addr: contract_addr.to_string(),
306 });
307 }
308 let query_msg: StdResult<axone_cognitarium::msg::QueryMsg> = from_json(msg);
309 assert_eq!(
310 query_msg,
311 Ok(axone_cognitarium::msg::QueryMsg::Select {
312 query: SelectQuery {
313 prefixes: vec![],
314 limit: Some(1u32),
315 select: vec![SelectItem::Variable("p".to_string())],
316 r#where: WhereClause::Bgp {
317 patterns: vec![TriplePattern {
318 subject: VarOrNode::Node(Node::NamedNode(IRI::Full(
319 "http://example.edu/credentials/3732".to_string(),
320 ))),
321 predicate: VarOrNamedNode::Variable("p".to_string()),
322 object: VarOrNodeOrLiteral::Variable("o".to_string()),
323 }]
324 },
325 }
326 })
327 );
328
329 let select_resp = SelectResponse {
330 results: Results { bindings: vec![] },
331 head: Head { vars: vec![] },
332 };
333 SystemResult::Ok(ContractResult::Ok(to_json_binary(&select_resp).unwrap()))
334 }
335 _ => SystemResult::Err(SystemError::Unknown {}),
336 });
337
338 DATAVERSE
339 .save(
340 deps.as_mut().storage,
341 &Dataverse {
342 name: "my-dataverse".to_string(),
343 triplestore_address: Addr::unchecked("my-dataverse-addr"),
344 },
345 )
346 .unwrap();
347
348 let resp = execute(
349 deps.as_mut(),
350 mock_env(),
351 message_info(
352 &Addr::unchecked("axone1072nc6egexqr2v6vpp7yxwm68plvqnkf5uemr0"),
353 &[],
354 ),
355 ExecuteMsg::SubmitClaims {
356 claims: Binary::new(read_test_data("vc-eddsa-2020-ok.nq")),
357 format: Some(RdfDatasetFormat::NQuads),
358 },
359 );
360
361 assert!(resp.is_ok());
362 let resp = resp.unwrap();
363 assert_eq!(resp.messages.len(), 1);
364 assert_eq!(
365 resp.attributes,
366 vec![
367 Attribute::new("action", "submit_claims"),
368 Attribute::new("credential", "http://example.edu/credentials/3732"),
369 Attribute::new(
370 "subject",
371 "did:key:zDnaeUm3QkcyZWZTPttxB711jgqRDhkwvhF485SFw1bDZ9AQw"
372 ),
373 Attribute::new(
374 "type",
375 "https://example.org/examples#UniversityDegreeCredential"
376 ),
377 ]
378 );
379
380 let expected_data = r#"<http://example.edu/credentials/3732> <dataverse:credential:header#height> "12345" .
381<http://example.edu/credentials/3732> <dataverse:credential:header#timestamp> "1571797419" .
382<http://example.edu/credentials/3732> <dataverse:credential:header#sender> "axone1072nc6egexqr2v6vpp7yxwm68plvqnkf5uemr0" .
383<http://example.edu/credentials/3732> <dataverse:credential:body#issuer> <did:key:z6MkpwdnLPAm4apwcrRYQ6fZ3rAcqjLZR4AMk14vimfnozqY> .
384<http://example.edu/credentials/3732> <dataverse:credential:body#type> <https://example.org/examples#UniversityDegreeCredential> .
385<http://example.edu/credentials/3732> <dataverse:credential:body#validFrom> "2024-02-16T00:00:00Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
386<http://example.edu/credentials/3732> <dataverse:credential:body#subject> <did:key:zDnaeUm3QkcyZWZTPttxB711jgqRDhkwvhF485SFw1bDZ9AQw> .
387<http://example.edu/credentials/3732> <dataverse:credential:header#tx_index> "3" .
388_:c0 <https://example.org/examples#degree> _:b0 .
389_:b0 <http://schema.org/name> "Bachelor of Science and Arts"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#HTML> .
390_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://example.org/examples#BachelorDegree> .
391<http://example.edu/credentials/3732> <dataverse:credential:body#claim> _:c0 .
392<http://example.edu/credentials/3732> <dataverse:credential:body#validUntil> "2026-02-16T00:00:00Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
393"#;
394
395 match resp.messages[0].msg.clone() {
396 CosmosMsg::Wasm(WasmMsg::Execute {
397 contract_addr,
398 msg,
399 funds,
400 }) if contract_addr == "my-dataverse-addr".to_string() && funds == vec![] => {
401 let exec_msg: StdResult<axone_cognitarium::msg::ExecuteMsg> = from_json(msg);
402 assert!(exec_msg.is_ok());
403 match exec_msg.unwrap() {
404 axone_cognitarium::msg::ExecuteMsg::InsertData { format, data } => {
405 assert_eq!(format, Some(DataFormat::NTriples));
406 assert_eq!(String::from_utf8(data.to_vec()).unwrap(), expected_data);
407 }
408 _ => assert!(false),
409 }
410 }
411 _ => assert!(false),
412 }
413 }
414
415 #[test]
416 fn submit_nonrdf_claims() {
417 let resp = execute(
418 mock_dependencies().as_mut(),
419 mock_env(),
420 message_info(
421 &Addr::unchecked("axone1072nc6egexqr2v6vpp7yxwm68plvqnkf5uemr0"),
422 &[],
423 ),
424 ExecuteMsg::SubmitClaims {
425 claims: Binary::new("notrdf".as_bytes().to_vec()),
426 format: Some(RdfDatasetFormat::NQuads),
427 },
428 );
429
430 assert!(resp.is_err());
431 assert!(matches!(resp.err().unwrap(), ContractError::ParseRDF(_)))
432 }
433
434 #[test]
435 fn submit_invalid_claims() {
436 let resp = execute(
437 mock_dependencies().as_mut(),
438 mock_env(),
439 message_info(
440 &Addr::unchecked("axone1072nc6egexqr2v6vpp7yxwm68plvqnkf5uemr0"),
441 &[],
442 ),
443 ExecuteMsg::SubmitClaims {
444 claims: Binary::new(vec![]),
445 format: Some(RdfDatasetFormat::NQuads),
446 },
447 );
448
449 assert!(resp.is_err());
450 assert!(matches!(
451 resp.err().unwrap(),
452 ContractError::InvalidCredential(_)
453 ))
454 }
455
456 #[test]
457 fn submit_unverified_claims_matching_sender() {
458 let mut deps = mock_dependencies();
459 deps.querier.update_wasm(|query| match query {
460 WasmQuery::Smart { contract_addr, msg } => {
461 if contract_addr != "my-dataverse-addr" {
462 return SystemResult::Err(SystemError::NoSuchContract {
463 addr: contract_addr.to_string(),
464 });
465 }
466 let query_msg: StdResult<axone_cognitarium::msg::QueryMsg> = from_json(msg);
467 assert_eq!(
468 query_msg,
469 Ok(axone_cognitarium::msg::QueryMsg::Select {
470 query: SelectQuery {
471 prefixes: vec![],
472 limit: Some(1u32),
473 select: vec![SelectItem::Variable("p".to_string())],
474 r#where: WhereClause::Bgp {
475 patterns: vec![TriplePattern {
476 subject: VarOrNode::Node(Node::NamedNode(IRI::Full(
477 "http://example.edu/credentials/3732".to_string(),
478 ))),
479 predicate: VarOrNamedNode::Variable("p".to_string()),
480 object: VarOrNodeOrLiteral::Variable("o".to_string()),
481 }]
482 },
483 }
484 })
485 );
486
487 let select_resp = SelectResponse {
488 results: Results { bindings: vec![] },
489 head: Head { vars: vec![] },
490 };
491 SystemResult::Ok(ContractResult::Ok(to_json_binary(&select_resp).unwrap()))
492 }
493 _ => SystemResult::Err(SystemError::Unknown {}),
494 });
495
496 DATAVERSE
497 .save(
498 deps.as_mut().storage,
499 &Dataverse {
500 name: "my-dataverse".to_string(),
501 triplestore_address: Addr::unchecked("my-dataverse-addr"),
502 },
503 )
504 .unwrap();
505
506 let resp = execute(
507 deps.as_mut(),
508 mock_env(),
509 message_info(
510 &Addr::unchecked("axone178mjppxcf3n9q3q7utdwrajdal0tsqvymz0900"),
511 &[],
512 ),
513 ExecuteMsg::SubmitClaims {
514 claims: Binary::new(read_test_data("vc-eddsa-2020-ok-unsecured-trusted.nq")),
515 format: Some(RdfDatasetFormat::NQuads),
516 },
517 );
518
519 assert!(resp.is_ok());
520 }
521
522 #[test]
523 fn submit_unverified_claims() {
524 let resp = execute(
525 mock_dependencies().as_mut(),
526 mock_env(),
527 message_info(
528 &Addr::unchecked("axone1072nc6egexqr2v6vpp7yxwm68plvqnkf5uemr0"),
529 &[],
530 ),
531 ExecuteMsg::SubmitClaims {
532 claims: Binary::new(read_test_data("vc-eddsa-2020-ok-unsecured.nq")),
533 format: Some(RdfDatasetFormat::NQuads),
534 },
535 );
536
537 assert!(resp.is_err());
538 assert!(matches!(
539 resp.err().unwrap(),
540 ContractError::CredentialVerification(_)
541 ))
542 }
543
544 #[test]
545 fn submit_unsupported_claims() {
546 let resp = execute(
547 mock_dependencies().as_mut(),
548 mock_env(),
549 message_info(
550 &Addr::unchecked("axone1072nc6egexqr2v6vpp7yxwm68plvqnkf5uemr0"),
551 &[],
552 ),
553 ExecuteMsg::SubmitClaims {
554 claims: Binary::new(read_test_data("vc-unsupported-1.nq")),
555 format: Some(RdfDatasetFormat::NQuads),
556 },
557 );
558
559 assert!(resp.is_err());
560 assert!(matches!(
561 resp.err().unwrap(),
562 ContractError::UnsupportedCredential(_)
563 ))
564 }
565
566 #[test]
567 fn submit_existing_claims() {
568 let mut deps = mock_dependencies();
569 deps.querier.update_wasm(|query| match query {
570 WasmQuery::Smart { .. } => {
571 let select_resp = SelectResponse {
572 results: Results {
573 bindings: vec![BTreeMap::from([(
574 "p".to_string(),
575 Value::BlankNode {
576 value: "".to_string(),
577 },
578 )])],
579 },
580 head: Head { vars: vec![] },
581 };
582 SystemResult::Ok(ContractResult::Ok(to_json_binary(&select_resp).unwrap()))
583 }
584 _ => SystemResult::Err(SystemError::Unknown {}),
585 });
586
587 DATAVERSE
588 .save(
589 deps.as_mut().storage,
590 &Dataverse {
591 name: "my-dataverse".to_string(),
592 triplestore_address: Addr::unchecked("my-dataverse-addr"),
593 },
594 )
595 .unwrap();
596
597 let resp = execute(
598 deps.as_mut(),
599 mock_env(),
600 message_info(
601 &Addr::unchecked("axone1072nc6egexqr2v6vpp7yxwm68plvqnkf5uemr0"),
602 &[],
603 ),
604 ExecuteMsg::SubmitClaims {
605 claims: Binary::new(read_test_data("vc-eddsa-2020-ok.nq")),
606 format: Some(RdfDatasetFormat::NQuads),
607 },
608 );
609
610 assert!(resp.is_err());
611 assert!(
612 matches!(resp.err().unwrap(), ContractError::CredentialAlreadyExists(id) if id == "http://example.edu/credentials/3732")
613 );
614 }
615}