did_tz/
lib.rs

1use iref::{Iri, Uri, UriBuf};
2use json_patch::patch;
3use serde::Deserialize;
4use ssi_dids_core::{
5    document::{
6        self,
7        representation::{self, MediaType},
8        verification_method::ValueOrReference,
9        DIDVerificationMethod, Resource, Service,
10    },
11    resolution::Error,
12    resolution::{self, Content, DIDMethodResolver, DerefError, Output, Parameter},
13    DIDBuf, DIDMethod, DIDResolver, DIDURLBuf, Document, DID, DIDURL,
14};
15use ssi_jwk::{p256_parse, secp256k1_parse, Base64urlUInt, OctetParams, Params, JWK};
16use ssi_jws::{decode_unverified, decode_verify};
17use static_iref::iri;
18use std::{collections::BTreeMap, future::Future};
19
20mod explorer;
21mod prefix;
22
23pub use prefix::*;
24
25#[derive(Debug, thiserror::Error)]
26pub enum UpdateError {
27    #[error("missing key id in patch")]
28    MissingPatchKeyId,
29
30    #[error("key id `{0}` in patch is not a DID URL")]
31    InvalidPatchKeyId(String),
32
33    #[error("invalid public key `{0}`")]
34    InvalidPublicKey(String, ssi_jwk::Error),
35
36    #[error("invalid public key `{0}`: not base58")]
37    InvalidPublicKeyEncoding(String),
38
39    #[error("{0} support not enabled")]
40    PrefixNotEnabled(Prefix),
41
42    #[error("dereference failed: {0}")]
43    DereferenceFailed(DerefError),
44
45    #[error("expected a DID document")]
46    NotADocument,
47
48    #[error("missing public key for patch")]
49    MissingPublicKey,
50
51    #[error("invalid JWS: {0}")]
52    InvalidJws(ssi_jws::Error),
53
54    #[error("invalid patch: {0}")]
55    InvalidPatch(serde_json::Error),
56
57    #[error(transparent)]
58    Patch(json_patch::PatchError),
59
60    #[error("invalid patched document: {0}")]
61    InvalidPatchedDocument(serde_json::Error),
62}
63
64/// `did:tz` DID Method
65///
66/// [Specification](https://github.com/spruceid/did-tezos/)
67///
68/// # Resolution options
69///
70/// ## `tzkt_url`
71/// Custom indexer endpoint URL.
72///
73/// ## `updates`
74/// [Off-Chain DID Document Updates](https://did-tezos.spruceid.com/#off-chain-did-document-updates), as specified in the Tezos DID Method Specification.
75///
76/// ## `public_key`
77/// Public key in Base58 format ([publicKeyBase58](https://w3c-ccg.github.io/security-vocab/#publicKeyBase58)) to add to a [derived DID document (implicit resolution)](https://did-tezos.spruceid.com/#deriving-did-documents).
78#[derive(Default, Clone)]
79pub struct DIDTz {
80    tzkt_url: Option<UriBuf>,
81}
82
83impl DIDTz {
84    pub const fn new(tzkt_url: Option<UriBuf>) -> Self {
85        Self { tzkt_url }
86    }
87}
88
89impl DIDMethod for DIDTz {
90    const DID_METHOD_NAME: &'static str = "tz";
91}
92
93impl DIDMethodResolver for DIDTz {
94    async fn resolve_method_representation<'a>(
95        &'a self,
96        id: &'a str,
97        options: ssi_dids_core::resolution::Options,
98    ) -> Result<Output<Vec<u8>>, Error> {
99        let did = DIDBuf::new(format!("did:tz:{id}").into_bytes()).unwrap();
100        let (network, address) = id.split_once(':').unwrap_or(("mainnet", id));
101
102        if address.len() != 36 {
103            return Err(Error::InvalidMethodSpecificId(id.to_owned()));
104        }
105
106        // https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-26.md
107        let genesis_block_hash = match network {
108            "mainnet" => "NetXdQprcVkpaWU",
109            "delphinet" => "NetXm8tYqnMWky1",
110            "granadanet" => "NetXz969SFaFn8k",
111            "edonet" => "NetXSgo1ZT2DRUG",
112            "florencenet" => "NetXxkAx4woPLyu",
113            _ => return Err(Error::InvalidMethodSpecificId(id.to_owned())),
114        };
115
116        let prefix = Prefix::from_address(address)
117            .map_err(|_| Error::InvalidMethodSpecificId(id.to_owned()))?;
118
119        let vm = TezosVerificationMethod {
120            id: DIDURLBuf::new(format!("{did}#blockchainAccountId").into_bytes()).unwrap(),
121            type_: VerificationMethodType::from_prefix(prefix),
122            controller: did.clone(),
123            blockchain_account_id: Some(format!("tezos:{}:{}", genesis_block_hash, address)),
124            public_key: None,
125        };
126
127        let authentication_vm = options
128            .parameters
129            .additional
130            .get("public_key")
131            .map(|value| {
132                value
133                    .as_string()
134                    .ok_or_else(|| Error::InvalidMethodSpecificId(id.to_owned()))
135            })
136            .transpose()?
137            .map(|public_key| TezosVerificationMethod {
138                id: vm.id.clone(),
139                type_: vm.type_,
140                controller: vm.controller.clone(),
141                blockchain_account_id: None,
142                public_key: Some(public_key.to_owned()),
143            });
144
145        let mut json_ld_context = JsonLdContext::default();
146        json_ld_context.add_verification_method(&vm);
147        if let Some(vm) = &authentication_vm {
148            json_ld_context.add_verification_method(vm);
149        }
150
151        let mut doc = DIDTz::tier1_derivation(&did, vm, authentication_vm);
152
153        let tzkt_url = match options.parameters.additional.get("tzkt_url") {
154            Some(value) => match value {
155                Parameter::String(v) => match UriBuf::new(v.as_bytes().to_vec()) {
156                    Ok(url) => url,
157                    Err(_) => return Err(Error::InvalidOptions),
158                },
159                _ => return Err(Error::InvalidOptions),
160            },
161            None => match &self.tzkt_url {
162                Some(u) => u.clone(),
163                None => UriBuf::new(format!("https://api.{network}.tzkt.io").into_bytes()).unwrap(),
164            },
165        };
166
167        if let (Some(service), Some(vm_url)) =
168            DIDTz::tier2_resolution(prefix, &tzkt_url, &did, address).await?
169        {
170            doc.service.push(service);
171            doc.verification_relationships
172                .authentication
173                .push(ValueOrReference::Reference(vm_url.into()));
174        }
175
176        if let Some(updates_metadata) = options.parameters.additional.get("updates") {
177            let conversion: String = match updates_metadata {
178                Parameter::String(s) => s.clone(),
179                // Parameter::Map(m) => match serde_json::to_string(m) {
180                //     Ok(s) => s.clone(),
181                //     Err(e) => {
182                //         return (
183                //             ResolutionMetadata {
184                //                 error: Some(e.to_string()),
185                //                 ..Default::default()
186                //             },
187                //             Some(doc),
188                //             None,
189                //         )
190                //     }
191                // },
192                _ => return Err(Error::InvalidOptions),
193            };
194
195            let updates: Updates = match serde_json::from_str(&conversion) {
196                Ok(uu) => uu,
197                Err(_) => return Err(Error::InvalidOptions),
198            };
199
200            self.tier3_updates(prefix, &mut doc, updates)
201                .await
202                .map_err(Error::internal)?;
203        }
204
205        let content_type = options.accept.unwrap_or(MediaType::JsonLd);
206        let represented = doc.into_representation(representation::Options::from_media_type(
207            content_type,
208            move || representation::json_ld::Options {
209                context: representation::json_ld::Context::array(
210                    representation::json_ld::DIDContext::V1,
211                    vec![json_ld_context.into()],
212                ),
213            },
214        ));
215
216        Ok(Output::new(
217            represented.to_bytes(),
218            document::Metadata::default(),
219            resolution::Metadata::from_content_type(Some(content_type.to_string())),
220        ))
221    }
222}
223
224fn get_public_key_from_doc<'a>(doc: &'a Document, auth_vm_id: &DIDURL) -> Option<&'a str> {
225    let mut is_authentication_method = false;
226    for vm in &doc.verification_relationships.authentication {
227        #[allow(clippy::single_match)]
228        match vm {
229            ValueOrReference::Value(vm) => {
230                if vm.id == auth_vm_id {
231                    return vm
232                        .properties
233                        .get("publicKeyBase58")
234                        .and_then(serde_json::Value::as_str);
235                }
236            }
237            ValueOrReference::Reference(_) => is_authentication_method = true,
238        }
239    }
240
241    if is_authentication_method {
242        for vm in &doc.verification_method {
243            if vm.id == auth_vm_id {
244                return vm
245                    .properties
246                    .get("publicKeyBase58")
247                    .and_then(serde_json::Value::as_str);
248            }
249        }
250    }
251
252    None
253}
254
255pub struct TezosVerificationMethod {
256    id: DIDURLBuf,
257    type_: VerificationMethodType,
258    controller: DIDBuf,
259    blockchain_account_id: Option<String>,
260    public_key: Option<String>,
261}
262
263impl From<TezosVerificationMethod> for DIDVerificationMethod {
264    fn from(value: TezosVerificationMethod) -> Self {
265        let mut properties = BTreeMap::new();
266
267        if let Some(v) = value.blockchain_account_id {
268            properties.insert(
269                "blockchainAccountId".to_string(),
270                serde_json::Value::String(v),
271            );
272        }
273
274        if let Some(v) = value.public_key {
275            properties.insert("publicKeyBase58".to_string(), serde_json::Value::String(v));
276        }
277
278        DIDVerificationMethod::new(
279            value.id,
280            value.type_.name().to_string(),
281            value.controller,
282            properties,
283        )
284    }
285}
286
287#[derive(Debug, Clone, Copy)]
288pub enum VerificationMethodType {
289    Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021,
290    EcdsaSecp256k1RecoveryMethod2020,
291    P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021,
292}
293
294impl VerificationMethodType {
295    pub fn from_prefix(prefix: Prefix) -> Self {
296        match prefix {
297            Prefix::TZ1 | Prefix::KT1 => {
298                Self::Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021
299            }
300            Prefix::TZ2 => VerificationMethodType::EcdsaSecp256k1RecoveryMethod2020,
301            Prefix::TZ3 => {
302                VerificationMethodType::P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021
303            }
304        }
305    }
306
307    pub fn curve(&self) -> &'static str {
308        match self {
309            Self::Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 => "Ed25519",
310            Self::EcdsaSecp256k1RecoveryMethod2020 => "secp256k1",
311            Self::P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 => "P-256",
312        }
313    }
314
315    pub fn name(&self) -> &'static str {
316        match self {
317            Self::Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 => {
318                "Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021"
319            }
320            Self::EcdsaSecp256k1RecoveryMethod2020 => "EcdsaSecp256k1RecoveryMethod2020",
321            Self::P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 => {
322                "P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021"
323            }
324        }
325    }
326
327    pub fn as_iri(&self) -> &'static Iri {
328        match self {
329            Self::Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 => iri!("https://w3id.org/security#Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021"),
330            Self::EcdsaSecp256k1RecoveryMethod2020 => iri!("https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020"),
331            Self::P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 => iri!("https://w3id.org/security#P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021")
332        }
333    }
334}
335
336fn decode_public_key(public_key: &str) -> Result<Vec<u8>, UpdateError> {
337    Ok(bs58::decode(public_key)
338        .with_check(None)
339        .into_vec()
340        .map_err(|_| {
341            // Couldn't decode public key
342            UpdateError::InvalidPublicKeyEncoding(public_key.to_owned())
343        })?[4..]
344        .to_vec())
345}
346
347#[derive(Deserialize)]
348#[serde(rename_all = "kebab-case")]
349struct SignedIetfJsonPatchPayload {
350    ietf_json_patch: serde_json::Value,
351}
352
353#[derive(Deserialize)]
354#[serde(rename_all = "kebab-case")]
355#[serde(tag = "type", content = "value")]
356enum Updates {
357    SignedIetfJsonPatch(Vec<String>),
358}
359
360#[derive(Debug, Default)]
361struct JsonLdContext {
362    ecdsa_secp256k1_recovery_method_2020: bool,
363    ed_25519_public_key_blake2b_digest_size_20_base58_check_encoded2021: bool,
364    p256_public_key_blake2b_digest_size_20_base58_check_encoded2021: bool,
365    blockchain_account_id: bool,
366    public_key_base58: bool,
367}
368
369impl JsonLdContext {
370    pub fn add_verification_method(&mut self, m: &TezosVerificationMethod) {
371        self.blockchain_account_id |= m.blockchain_account_id.is_some();
372        self.public_key_base58 |= m.public_key.is_some();
373        self.add_verification_method_type(m.type_);
374    }
375
376    pub fn add_verification_method_type(&mut self, ty: VerificationMethodType) {
377        match ty {
378            VerificationMethodType::EcdsaSecp256k1RecoveryMethod2020 => {
379                self.ecdsa_secp256k1_recovery_method_2020 = true
380            }
381            VerificationMethodType::Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 => {
382                self.ed_25519_public_key_blake2b_digest_size_20_base58_check_encoded2021 = true
383            }
384            VerificationMethodType::P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 => {
385                self.p256_public_key_blake2b_digest_size_20_base58_check_encoded2021 = true
386            }
387        }
388    }
389}
390
391impl From<JsonLdContext> for representation::json_ld::ContextEntry {
392    fn from(value: JsonLdContext) -> Self {
393        use representation::json_ld::context::{Definition, TermDefinition};
394        let mut def = Definition::new();
395
396        if value.ecdsa_secp256k1_recovery_method_2020 {
397            let ty = VerificationMethodType::EcdsaSecp256k1RecoveryMethod2020;
398            def.bindings.insert(
399                ty.name().into(),
400                TermDefinition::Simple(ty.as_iri().to_owned().into()).into(),
401            );
402        }
403
404        if value.ed_25519_public_key_blake2b_digest_size_20_base58_check_encoded2021 {
405            let ty =
406                VerificationMethodType::Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021;
407            def.bindings.insert(
408                ty.name().into(),
409                TermDefinition::Simple(ty.as_iri().to_owned().into()).into(),
410            );
411        }
412
413        if value.p256_public_key_blake2b_digest_size_20_base58_check_encoded2021 {
414            let ty = VerificationMethodType::P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021;
415            def.bindings.insert(
416                ty.name().into(),
417                TermDefinition::Simple(ty.as_iri().to_owned().into()).into(),
418            );
419        }
420
421        if value.blockchain_account_id {
422            def.bindings.insert(
423                "blockchainAccountId".into(),
424                TermDefinition::Simple(
425                    iri!("https://w3id.org/security#blockchainAccountId")
426                        .to_owned()
427                        .into(),
428                )
429                .into(),
430            );
431        }
432
433        Self::Definition(def)
434    }
435}
436
437impl DIDTz {
438    // TODO need to handle different networks
439    pub fn generate(&self, jwk: &JWK) -> Result<DIDBuf, ssi_jwk::Error> {
440        let hash = ssi_jwk::blakesig::hash_public_key(jwk)?;
441        Ok(DIDBuf::from_string(format!("did:tz:{hash}")).unwrap())
442    }
443
444    fn tier1_derivation(
445        did: &DID,
446        verification_method: TezosVerificationMethod,
447        authentication_verification_method: Option<TezosVerificationMethod>,
448    ) -> Document {
449        // let mut context = BTreeMap::new();
450
451        // context.insert(
452        //     "blockchainAccountId".to_string(),
453        //     Value::String("https://w3id.org/security#blockchainAccountId".to_string()),
454        // );
455
456        // context.insert(
457        //     proof_type.to_string(),
458        //     Value::String(proof_type_iri.to_string()),
459        // );
460
461        // if public_key.is_some() {
462        //     context.insert(
463        //         "publicKeyBase58".to_string(),
464        //         Value::String("https://w3id.org/security#publicKeyBase58".to_string()),
465        //     );
466        // }
467
468        let mut document = Document::new(did.to_owned());
469
470        let authentication_verification_method = match authentication_verification_method {
471            Some(vm) => ValueOrReference::Value(vm.into()),
472            None => ValueOrReference::Reference(verification_method.id.clone().into()),
473        };
474
475        document
476            .verification_relationships
477            .assertion_method
478            .push(ValueOrReference::Reference(
479                verification_method.id.clone().into(),
480            ));
481        document
482            .verification_relationships
483            .authentication
484            .push(authentication_verification_method);
485        document
486            .verification_method
487            .push(verification_method.into());
488
489        document
490    }
491
492    async fn tier2_resolution(
493        prefix: Prefix,
494        tzkt_url: &Uri,
495        did: &DID,
496        address: &str,
497    ) -> Result<(Option<Service>, Option<DIDURLBuf>), Error> {
498        if let Some(did_manager) = match prefix {
499            Prefix::KT1 => Some(address.to_string()),
500            _ => explorer::retrieve_did_manager(tzkt_url, address).await?,
501        } {
502            Ok((
503                Some(explorer::execute_service_view(tzkt_url, did, &did_manager).await?),
504                Some(explorer::execute_auth_view(tzkt_url, &did_manager).await?),
505            ))
506        } else {
507            Ok((None, None))
508        }
509    }
510
511    fn tier3_updates<'a>(
512        &'a self,
513        prefix: Prefix,
514        doc: &'a mut Document,
515        updates: Updates,
516    ) -> impl 'a + Future<Output = Result<(), UpdateError>> {
517        Box::pin(async move {
518            match updates {
519                Updates::SignedIetfJsonPatch(patches) => {
520                    for jws in patches {
521                        let mut doc_json = serde_json::to_value(&*doc).unwrap();
522                        let (patch_metadata, _) =
523                            decode_unverified(&jws).map_err(UpdateError::InvalidJws)?;
524                        let curve = VerificationMethodType::from_prefix(prefix)
525                            .curve()
526                            .to_string();
527
528                        let kid = match patch_metadata.key_id {
529                            Some(k) => DIDURLBuf::from_string(k)
530                                .map_err(|e| UpdateError::InvalidPatchKeyId(e.0)),
531                            None => {
532                                // No kid in JWS JSON patch.
533                                Err(UpdateError::MissingPatchKeyId)
534                            }
535                        }?;
536
537                        // TODO need to compare address + network instead of the String
538                        // did:tz:tz1blahblah == did:tz:mainnet:tz1blahblah
539                        let kid_doc = if kid.did() == &doc.id {
540                            doc.clone()
541                        } else {
542                            let deref = self
543                                .dereference(&kid)
544                                .await
545                                .map_err(UpdateError::DereferenceFailed)?;
546                            match deref.content {
547                                Content::Resource(Resource::Document(d)) => d,
548                                _ => {
549                                    // Dereferenced content not a DID document.
550                                    return Err(UpdateError::NotADocument);
551                                }
552                            }
553                        };
554
555                        if let Some(public_key) = get_public_key_from_doc(&kid_doc, &kid) {
556                            let jwk = match prefix {
557                                Prefix::TZ1 | Prefix::KT1 => {
558                                    let pk = decode_public_key(public_key)?;
559
560                                    JWK {
561                                        params: Params::OKP(OctetParams {
562                                            curve,
563                                            public_key: Base64urlUInt(pk),
564                                            private_key: None,
565                                        }),
566                                        public_key_use: None,
567                                        key_operations: None,
568                                        algorithm: None,
569                                        key_id: None,
570                                        x509_url: None,
571                                        x509_thumbprint_sha1: None,
572                                        x509_certificate_chain: None,
573                                        x509_thumbprint_sha256: None,
574                                    }
575                                }
576                                Prefix::TZ2 => {
577                                    let pk = decode_public_key(public_key)?;
578                                    secp256k1_parse(&pk).map_err(|e| {
579                                        // Couldn't create JWK from secp256k1 public key: {e}
580                                        UpdateError::InvalidPublicKey(public_key.to_owned(), e)
581                                    })?
582                                }
583                                Prefix::TZ3 => {
584                                    let pk = decode_public_key(public_key)?;
585                                    p256_parse(&pk).map_err(|e| {
586                                        // Couldn't create JWK from P-256 public key: {e}
587                                        UpdateError::InvalidPublicKey(public_key.to_owned(), e)
588                                    })?
589                                }
590                                #[allow(unreachable_patterns)]
591                                p => {
592                                    // {p} support not enabled.
593                                    return Err(UpdateError::PrefixNotEnabled(p));
594                                }
595                            };
596                            let (_, patch_) =
597                                decode_verify(&jws, &jwk).map_err(UpdateError::InvalidJws)?;
598                            patch(
599                                &mut doc_json,
600                                &serde_json::from_slice(
601                                    serde_json::from_slice::<SignedIetfJsonPatchPayload>(&patch_)
602                                        .map_err(UpdateError::InvalidPatch)?
603                                        .ietf_json_patch
604                                        .to_string()
605                                        .as_bytes(),
606                                )
607                                .map_err(UpdateError::InvalidPatch)?,
608                            )
609                            .map_err(UpdateError::Patch)?;
610
611                            *doc = serde_json::from_value(doc_json)
612                                .map_err(UpdateError::InvalidPatchedDocument)?;
613                        } else {
614                            // Need public key for signed patches
615                            return Err(UpdateError::MissingPublicKey);
616                        }
617                    }
618                }
619            }
620
621            Ok(())
622        })
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629    use serde_json::json;
630    use ssi_core::one_or_many::OneOrMany;
631    use ssi_dids_core::document::service;
632    use ssi_jws::encode_sign;
633    use static_iref::uri;
634
635    const DIDTZ: DIDTz = DIDTz { tzkt_url: None };
636
637    const JSON_PATCH: &str = r#"{"ietf-json-patch": [
638                                {
639                                    "op": "add",
640                                    "path": "/service/1",
641                                    "value": {
642                                        "id": "http://example.org/test_service_id",
643                                        "type": "test_service",
644                                        "serviceEndpoint": "http://example.org/test_service_endpoint"
645                                    }
646                                }
647                            ]}"#;
648
649    #[tokio::test]
650    async fn test_json_patch_tz1() {
651        let address = "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb";
652        let pk = "edpkvGfYw3LyB1UcCahKQk4rF2tvbMUk8GFiTuMjL75uGXrpvKXhjn";
653        let sk = "edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq";
654        let did = format!("did:tz:{}:{}", "sandbox", address);
655        let mut doc: Document = serde_json::from_value(json!({
656            "@context": "https://www.w3.org/ns/did/v1",
657            "id": did,
658            "authentication": [{
659                "id": format!("{did}#blockchainAccountId"),
660                "type": "Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021",
661                "controller": did,
662                "blockchainAccountId": format!("tezos:sandbox:{address}"),
663                "publicKeyBase58": pk
664            }],
665            "service": [{
666                "id": format!("{did}#discovery"),
667                "type": "TezosDiscoveryService",
668                "serviceEndpoint": "test_service"
669            }]
670        }))
671        .unwrap();
672        let key = JWK {
673            key_id: Some(format!("{}#blockchainAccountId", did)),
674            ..ssi_tzkey::jwk_from_tezos_key(sk).unwrap()
675        };
676        let jws = encode_sign(ssi_jwk::Algorithm::EdDSA, JSON_PATCH, &key).unwrap();
677        let json_update = Updates::SignedIetfJsonPatch(vec![jws.clone()]);
678        DIDTZ
679            .tier3_updates(Prefix::TZ1, &mut doc, json_update)
680            .await
681            .unwrap();
682        assert_eq!(
683            doc.service[1],
684            Service {
685                id: uri!("http://example.org/test_service_id").to_owned(),
686                type_: OneOrMany::One("test_service".to_string()),
687                service_endpoint: Some(OneOrMany::One(service::Endpoint::Uri(
688                    uri!("http://example.org/test_service_endpoint").to_owned()
689                ))),
690                property_set: BTreeMap::new()
691            }
692        );
693    }
694
695    #[tokio::test]
696    async fn test_json_patch_tz2() {
697        let address = "tz2RZoj9oqoA8bDeUoAKLjf8nLPQKmYjaj6Q";
698        let pk = "sppk7bRNbJ2n9PNQo295UJiYQ8iMma8ysRH9mCRFB14yhzLCwdGay9y";
699        let sk = "spsk1Uc5MDutpZmwPVeSLL2BbtCAqfrG8zbMs6dwoaeXX8kw35S474";
700        let did = format!("did:tz:{}:{}", "sandbox", address);
701        let mut doc: Document = serde_json::from_value(json!({
702            "@context": "https://www.w3.org/ns/did/v1",
703            "id": did,
704            "authentication": [{
705            "id": format!("{}#blockchainAccountId", did),
706            "type": "EcdsaSecp256k1RecoveryMethod2020",
707            "controller": did,
708            "blockchainAccountId": format!("tezos:sandbox:{}", address),
709            "publicKeyBase58": pk
710            }],
711            "service": [{
712            "id": format!("{}#discovery", did),
713            "type": "TezosDiscoveryService",
714            "serviceEndpoint": "test_service"
715            }]
716        }))
717        .unwrap();
718        // let public_key = pk.from_base58check().unwrap()[4..].to_vec();
719        let private_key = bs58::decode(&sk).with_check(None).into_vec().unwrap()[4..].to_owned();
720        use ssi_jwk::ECParams;
721        let key = JWK {
722            params: ssi_jwk::Params::EC(ECParams {
723                curve: Some("secp256k1".to_string()),
724                x_coordinate: None,
725                y_coordinate: None,
726                ecc_private_key: Some(Base64urlUInt(private_key)),
727            }),
728            public_key_use: None,
729            key_operations: None,
730            algorithm: None,
731            key_id: Some(format!("{}#blockchainAccountId", did)),
732            x509_url: None,
733            x509_certificate_chain: None,
734            x509_thumbprint_sha1: None,
735            x509_thumbprint_sha256: None,
736        };
737        let jws = encode_sign(ssi_jwk::Algorithm::ES256KR, JSON_PATCH, &key).unwrap();
738        let json_update = Updates::SignedIetfJsonPatch(vec![jws.clone()]);
739        DIDTZ
740            .tier3_updates(Prefix::TZ2, &mut doc, json_update)
741            .await
742            .unwrap();
743        assert_eq!(
744            doc.service[1],
745            Service {
746                id: uri!("http://example.org/test_service_id").to_owned(),
747                type_: OneOrMany::One("test_service".to_string()),
748                service_endpoint: Some(OneOrMany::One(service::Endpoint::Uri(
749                    uri!("http://example.org/test_service_endpoint").to_owned()
750                ))),
751                property_set: BTreeMap::new()
752            }
753        );
754    }
755
756    #[tokio::test]
757    async fn test_json_patch_tz3() {
758        let address = "tz3agP9LGe2cXmKQyYn6T68BHKjjktDbbSWX";
759        let pk = "p2pk679D18uQNkdjpRxuBXL5CqcDKTKzsiXVtc9oCUT6xb82zQmgUks";
760        let sk = "p2sk3PM77YMR99AvD3fSSxeLChMdiQ6kkEzqoPuSwQqhPsh29irGLC";
761        let did = format!("did:tz:{}:{}", "sandbox", address);
762        let mut doc: Document = serde_json::from_value(json!({
763            "@context": "https://www.w3.org/ns/did/v1",
764            "id": did,
765            "authentication": [{
766            "id": format!("{}#blockchainAccountId", did),
767            "type": "JsonWebKey2020",
768            "controller": did,
769            "blockchainAccountId": format!("tezos:sandbox:{}", address),
770            "publicKeyBase58": pk
771            }],
772            "service": [{
773            "id": format!("{}#discovery", did),
774            "type": "TezosDiscoveryService",
775            "serviceEndpoint": "test_service"
776            }]
777        }))
778        .unwrap();
779        // let public_key = pk.from_base58check().unwrap()[4..].to_vec();
780        let private_key = bs58::decode(&sk).with_check(None).into_vec().unwrap()[4..].to_owned();
781        let key = JWK {
782            params: ssi_jwk::Params::EC(ssi_jwk::ECParams {
783                curve: Some("P-256".to_string()),
784                x_coordinate: None,
785                y_coordinate: None,
786                ecc_private_key: Some(Base64urlUInt(private_key)),
787            }),
788            public_key_use: None,
789            key_operations: None,
790            algorithm: None,
791            key_id: Some(format!("{}#blockchainAccountId", did)),
792            x509_url: None,
793            x509_certificate_chain: None,
794            x509_thumbprint_sha1: None,
795            x509_thumbprint_sha256: None,
796        };
797        let jws = encode_sign(ssi_jwk::Algorithm::ES256, JSON_PATCH, &key).unwrap();
798        let json_update = Updates::SignedIetfJsonPatch(vec![jws.clone()]);
799        DIDTZ
800            .tier3_updates(Prefix::TZ3, &mut doc, json_update)
801            .await
802            .unwrap();
803        assert_eq!(
804            doc.service[1],
805            Service {
806                id: uri!("http://example.org/test_service_id").to_owned(),
807                type_: OneOrMany::One("test_service".to_string()),
808                service_endpoint: Some(OneOrMany::One(service::Endpoint::Uri(
809                    uri!("http://example.org/test_service_endpoint").to_owned()
810                ))),
811                property_set: BTreeMap::new()
812            }
813        );
814    }
815}