axone_dataverse/
contract.rs

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
14// version info for migration info
15const 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    // Necessary stuff for testing purposes, see: https://github.com/CosmWasm/cosmwasm/issues/1648
37    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        // check proofs if any.
107        // accept unverified credentials if the issuer matches the sender, as the transaction's
108        // signature serves as proof.
109        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}