diff --git a/packages/js-evo-sdk/README.md b/packages/js-evo-sdk/README.md index 9ffb26714c4..16e4fba5dd8 100644 --- a/packages/js-evo-sdk/README.md +++ b/packages/js-evo-sdk/README.md @@ -161,6 +161,10 @@ const contract = await sdk.contracts.fetch(contractId); for (const ref of contract.documentTypeReferences('note')) { // { path: 'author', type: 'identityPublicKey', keyIdProperty: 'authorKeyId' } + // A permanentDocument reference may additionally carry a write-time + // equality binding between the two documents' properties: + // { path: 'postId', type: 'permanentDocument', contractId, documentType: 'post', + // propertyAgreement: { hashtag: 'hashtag' } } console.log(ref.path, ref.type); } diff --git a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs index 8df2337ece5..8ab8617eef1 100644 --- a/packages/rs-dpp/src/data_contract/document_type/property/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/property/mod.rs @@ -7292,6 +7292,7 @@ mod tests { DocumentPropertyReferenceTarget::PermanentDocument { contract_id: None, document_type_name: "note".to_string(), + property_agreement: Default::default(), }, DocumentPropertyReferenceTarget::IdentityPublicKey { key_id_property: "signerKeyId".to_string(), diff --git a/packages/rs-dpp/src/document/document_factory/v0/mod.rs b/packages/rs-dpp/src/document/document_factory/v0/mod.rs index 2503d4788c8..faf131c8d5f 100644 --- a/packages/rs-dpp/src/document/document_factory/v0/mod.rs +++ b/packages/rs-dpp/src/document/document_factory/v0/mod.rs @@ -1,6 +1,8 @@ use crate::consensus::basic::document::InvalidDocumentTypeError; use crate::data_contract::accessors::v0::DataContractV0Getters; -use crate::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; +use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV2Getters, +}; use crate::data_contract::document_type::DocumentTypeRef; use crate::data_contract::errors::DataContractError; use crate::data_contract::DataContract; diff --git a/packages/wasm-dpp2/src/data_contract/document_type_reference.rs b/packages/wasm-dpp2/src/data_contract/document_type_reference.rs index f02c0ce69e1..259ed6e203c 100644 --- a/packages/wasm-dpp2/src/data_contract/document_type_reference.rs +++ b/packages/wasm-dpp2/src/data_contract/document_type_reference.rs @@ -53,6 +53,14 @@ export type DocumentPropertyReferenceTarget = * reference dangling. */ documentType: string; + /** + * Write-time equality bindings between the two documents: + * `{ : }`. + * Consensus refuses a write whose referring property does not equal + * the referenced document's property (code 40127). Absent — not + * `{}`-valued — when the declaration carries none. + */ + propertyAgreement?: Record; } | { type: 'identityPublicKey'; @@ -131,6 +139,7 @@ fn reference_to_js( DocumentPropertyReferenceTarget::PermanentDocument { contract_id, document_type_name, + property_agreement, } => { let effective = contract_id.unwrap_or(declaring_contract_id); set_field( @@ -145,6 +154,18 @@ fn reference_to_js( &JsValue::from_str(document_type_name), path, )?; + // `propertyAgreement` binds a referring property to a property + // of the referenced document (consensus-enforced equality at + // write time). Absent — not `{}`-valued — when the declaration + // carries none, matching the schema's own omission and the + // absent-field convention of the other optional target fields. + if !property_agreement.is_empty() { + let agreement = Object::new(); + for (referring, referenced) in property_agreement { + set_field(&agreement, referring, &JsValue::from_str(referenced), path)?; + } + set_field(&object, "propertyAgreement", &agreement, path)?; + } } DocumentPropertyReferenceTarget::IdentityPublicKey { key_id_property } => { set_field( diff --git a/packages/wasm-dpp2/tests/unit/DocumentPropertyReference.spec.ts b/packages/wasm-dpp2/tests/unit/DocumentPropertyReference.spec.ts index a63fbee87b5..e63c0ef1018 100644 --- a/packages/wasm-dpp2/tests/unit/DocumentPropertyReference.spec.ts +++ b/packages/wasm-dpp2/tests/unit/DocumentPropertyReference.spec.ts @@ -53,9 +53,12 @@ const schemas = { sourceContract: identifierProperty(1, { type: 'contract' }), paidWith: identifierProperty(2, { type: 'token' }), // `contractId` omitted: targets the declaring contract itself. + // `propertyAgreement` binds the referring document's property to + // the referenced document's (write-time equality, PV14 #4505). parentNoteId: identifierProperty(3, { type: 'permanentDocument', documentType: 'note', + propertyAgreement: { signerKeyId: 'signerKeyId' }, }), otherDoc: identifierProperty(4, { type: 'permanentDocument', @@ -104,6 +107,7 @@ type Reference = { contractId?: { toBase58(): string }; documentType?: string; keyIdProperty?: string; + propertyAgreement?: Record; }; describe('DataContract — refersTo declarations (v14)', () => { @@ -169,6 +173,27 @@ describe('DataContract — refersTo declarations (v14)', () => { expect(other.documentType).to.equal('thing'); }); + it('should carry the propertyAgreement map when declared', () => { + const contract = buildContract(14); + const references = contract.documentTypeReferences('note') as Reference[]; + const parent = references.find((reference) => reference.path === 'parentNoteId')!; + + expect(parent.propertyAgreement).to.deep.equal({ signerKeyId: 'signerKeyId' }); + }); + + /** + * Absent — not `{}`-valued — when the declaration carries none, + * matching the schema's own omission and the absent-field convention + * of the other optional target fields. + */ + it('should omit propertyAgreement for a reference declaring none', () => { + const contract = buildContract(14); + const references = contract.documentTypeReferences('note') as Reference[]; + const other = references.find((reference) => reference.path === 'otherDoc')!; + + expect(other).to.not.have.property('propertyAgreement'); + }); + it('should carry keyIdProperty for an identityPublicKey reference', () => { const contract = buildContract(14); const references = contract.documentTypeReferences('note') as Reference[];