diff --git a/demo/src/registry-tx.ts b/demo/src/registry-tx.ts new file mode 100644 index 00000000..2d9a53f2 --- /dev/null +++ b/demo/src/registry-tx.ts @@ -0,0 +1,288 @@ +import * as Cord from '@cord.network/sdk' +import { createAccount } from './utils/createAccount' + +import { + BN +} from 'bn.js'; + +async function getBalance(address: string, api) { + Cord.ConfigService.set({ submitTxResolveOn: Cord.Chain.IS_IN_BLOCK }) + + const { data: balance } = await api.query.system.account(address); + return balance.free.toString(); // Returns free balance as a string +} + +async function main() { + const networkAddress = process.env.NETWORK_ADDRESS + ? process.env.NETWORK_ADDRESS + : 'ws://127.0.0.1:9944' + + Cord.ConfigService.set({ submitTxResolveOn: Cord.Chain.IS_IN_BLOCK }) + await Cord.connect(networkAddress) + + const api = Cord.ConfigService.get('api'); + + // Step 1: Setup Membership + // Setup transaction author account - CORD Account. + + console.log(`\nโ„๏ธ New Network Member`) + const authorityAuthorIdentity = Cord.Utils.Crypto.makeKeypairFromUri( + process.env.ANCHOR_URI ? process.env.ANCHOR_URI : '//Alice', + 'sr25519' + ) + + // Setup network member account. + const { account: authorIdentity } = await createAccount() + console.log(`๐Ÿฆ Member (${authorIdentity.type}): ${authorIdentity.address}`) + + let tx = await api.tx.balances.transferAllowDeath(authorIdentity.address, new BN('1000000000000000')); + await Cord.Chain.signAndSubmitTx(tx, authorityAuthorIdentity); + + // Create a Registry. + const blob = { + "name": "Companies Registry", + "description": "A centralized registry that tracks the registration, incorporation status, and key business details of companies across various industries.", + "metadata": { + "category": "business", + "totalCompaniesRegistered": 15000, + "industriesCovered": [ + "Technology", + "Healthcare", + "Renewable Energy", + "Finance", + "Manufacturing" + ], + "lastUpdated": "01-10-2024", + "regulatoryAuthority": "National Business Bureau", + "registrationRequirements": { + "documentsNeeded": [ + "Incorporation Certificate", + "Tax Identification Number", + "Proof of Address", + "Board Resolution" + ], + "feeStructure": { + "smallBusiness": "INR500", + "mediumBusiness": "INR1000", + "largeBusiness": "INR5000" + } + } + } + }; + const stringified_blob = JSON.stringify(blob); + const digest = await Cord.Registries.getDigestFromRawData(stringified_blob); + + const registryDetails = await Cord.Registries.registryCreateProperties( + authorIdentity.address, + digest, //digest + null, //schemaId + blob, //blob + ); + + console.log(`\nโ„๏ธ Registry Create Details `, registryDetails); + + const registry = await Cord.Registries.dispatchCreateRegistryToChain( + registryDetails, + authorIdentity, + ); + + console.log('\nโœ… Registry created!'); + + // Update a existing Registry. + const new_blob = { + "name": "Companies Registry - A", + "description": "A centralized registry that tracks the registration, incorporation status, and key business details of companies across various industries.", + "metadata": { + "category": "business", + "totalCompaniesRegistered": 15000, + "industriesCovered": [ + "Technology", + "Healthcare", + "Renewable Energy", + "Finance", + "Manufacturing" + ], + "lastUpdated": "01-10-2024", + "regulatoryAuthority": "National Business Bureau", + "registrationRequirements": { + "documentsNeeded": [ + "Incorporation Certificate", + "Tax Identification Number", + "Proof of Address", + "Board Resolution" + ], + "feeStructure": { + "smallBusiness": "INR500", + "mediumBusiness": "INR1000", + "largeBusiness": "INR5000" + } + } + } + }; + const new_stringified_blob = JSON.stringify(new_blob); + const new_digest = await Cord.Registries.getDigestFromRawData(new_stringified_blob); + + const registryUpdateDetails = await Cord.Registries.registryUpdateProperties( + registry.uri, + registry.authorizationUri, + authorIdentity.address, + new_digest, //digest + new_blob, //blob + ); + + console.log(`\nโ„๏ธ Registry Update Details `, registryUpdateDetails); + + const registry_update = await Cord.Registries.dispatchUpdateRegistryToChain( + registryUpdateDetails, + authorIdentity, + ); + + console.log('\nโœ… Registry updated!'); + + // Revoke a Registry + console.log(`\nโ„๏ธ Revoking Registry `, registry.uri); + const registry_revoke = await Cord.Registries.dispatchRevokeToChain( + registry.uri, + registry.authorizationUri, + authorIdentity + ); + console.log('โœ… Registry Revoked!'); + + // Reinstate a Revoked Registry + console.log(`\nโ„๏ธ Reinstating Revoked Registry `, registry.uri); + const registry_reinstate = await Cord.Registries.dispatchReinstateToChain( + registry.uri, + registry.authorizationUri, + authorIdentity + ); + console.log('โœ… Revoked Registry Reinstated!'); + + // Archive a Registry + console.log(`\nโ„๏ธ Archiving Registry `, registry.uri); + const registry_archive = await Cord.Registries.dispatchArchiveToChain( + registry.uri, + registry.authorizationUri, + authorIdentity + ); + console.log('โœ… Registry Archived!'); + + // Restore a Archived Registry + console.log(`\nโ„๏ธ Restoring Archived Registry `, registry.uri); + const registry_restore = await Cord.Registries.dispatchRestoreToChain( + registry.uri, + registry.authorizationUri, + authorIdentity + ); + console.log('โœ… Archived Registry Restored!'); + + // Setup a account to be added as a `ASSERT` delegate. + const { account: assertIdentity } = await createAccount() + console.log(`\n๐Ÿฆ Delegate Member (${assertIdentity.type}): ${assertIdentity.address}`) + + console.log(`\nโ„๏ธ Registry Assert Authorization `); + + // Add a delegate with ASSERT permission + const assertPermission: Cord.RegistryPermissionType = Cord.RegistryPermission.ASSERT; + const registryAssertAuthProperties = + await Cord.Registries.registryAuthorizationProperties( + registry.uri, + assertIdentity.address, + assertPermission, + authorIdentity.address + ) + + console.dir(registryAssertAuthProperties, { + depth: null, + colors: true, + }) + + const delegateAssertAuthorizationUri = await Cord.Registries.dispatchDelegateAuthorization( + registryAssertAuthProperties, + registry.authorizationUri, + authorIdentity + ) + + console.log(`\nโœ… Registry Authorization added with ASSERT permission - ${delegateAssertAuthorizationUri} - added!`) + + // Setup a account to be added as a `DELEGATE` delegate. + const { account: delegateIdentity } = await createAccount() + console.log(`\n๐Ÿฆ Delegate Member (${delegateIdentity.type}): ${delegateIdentity.address}`) + + console.log(`\nโ„๏ธ Registry Delegate Authorization `); + + // Add a delegate with DELEGATE permission + const delegatePermission: Cord.RegistryPermissionType = Cord.RegistryPermission.DELEGATE; + const registryDelegateAuthProperties = + await Cord.Registries.registryAuthorizationProperties( + registry.uri, + delegateIdentity.address, + delegatePermission, + authorIdentity.address + ) + + console.dir(registryDelegateAuthProperties, { + depth: null, + colors: true, + }) + + const delegateAuthorizationUri = await Cord.Registries.dispatchDelegateAuthorization( + registryDelegateAuthProperties, + registry.authorizationUri, + authorIdentity + ) + + console.log(`\nโœ… Registry Authorization added with DELEGATE permission - ${delegateAuthorizationUri} - added!`) + + // Setup a account to be added as a `DELEGATE` delegate. + const { account: adminIdentity } = await createAccount() + console.log(`\n๐Ÿฆ Delegate Member (${adminIdentity.type}): ${adminIdentity.address}`) + + console.log(`\nโ„๏ธ Registry Admin Authorization `); + + // Add a delegate with DELEGATE permission + const adminPermission: Cord.RegistryPermissionType = Cord.RegistryPermission.ADMIN; + const registryAdminAuthProperties = + await Cord.Registries.registryAuthorizationProperties( + registry.uri, + adminIdentity.address, + adminPermission, + authorIdentity.address + ) + + console.dir(registryAdminAuthProperties, { + depth: null, + colors: true, + }) + + const delegateAdminAuthorizationUri = await Cord.Registries.dispatchDelegateAuthorization( + registryAdminAuthProperties, + registry.authorizationUri, + authorIdentity + ) + + console.log(`\nโœ… Registry Authorization added with ADMIN permission - ${delegateAdminAuthorizationUri} - added!`) + + console.log(`\nโ„๏ธ Remove Registry Assert Authorization `); + + // Remove a delegate with ASSERT permission + const removeAuthObj = await Cord.Registries.dispatchRemoveDelegateToChain( + registry.uri, + delegateAssertAuthorizationUri, + registry.authorizationUri, + authorIdentity + ) + + console.log(`\nโœ… Registry ASSERT Authorization removed - ${delegateAssertAuthorizationUri} - removed!`) + + console.log("Balance of Registry Creator after all transactions", await getBalance(authorIdentity.address, api)); +} + +main() + .then(() => console.log('\nBye! ๐Ÿ‘‹ ๐Ÿ‘‹ ๐Ÿ‘‹ ')) + .finally(Cord.disconnect) + +process.on('SIGINT', async () => { + console.log('\nBye! ๐Ÿ‘‹ ๐Ÿ‘‹ ๐Ÿ‘‹ \n') + Cord.disconnect() + process.exit(0) +}) diff --git a/packages/identifier/src/Identifier.ts b/packages/identifier/src/Identifier.ts index 6b3a0c60..77107427 100644 --- a/packages/identifier/src/Identifier.ts +++ b/packages/identifier/src/Identifier.ts @@ -66,6 +66,10 @@ import { u8aConcat, u8aToU8a, stringToU8a, + REGISTRY_IDENT, + REGISTRY_PREFIX, + REGISTRYAUTH_IDENT, + REGISTRYAUTH_PREFIX, } from '@cord.network/types' import { SDKErrors } from '@cord.network/utils' @@ -85,6 +89,8 @@ const VALID_IDENTS = new Set([ ACCOUNT_IDENT, ASSET_IDENT, ASSET_INSTANCE_IDENT, + REGISTRY_IDENT, + REGISTRYAUTH_IDENT, ]) const VALID_PREFIXES = [ @@ -95,6 +101,8 @@ const VALID_PREFIXES = [ AUTH_PREFIX, ACCOUNT_PREFIX, ASSET_PREFIX, + REGISTRY_PREFIX, + REGISTRYAUTH_PREFIX, ] const IDENT_TO_PREFIX_MAP = new Map([ @@ -106,6 +114,8 @@ const IDENT_TO_PREFIX_MAP = new Map([ [ACCOUNT_IDENT, ACCOUNT_PREFIX], [ASSET_IDENT, ASSET_PREFIX], [ASSET_INSTANCE_IDENT, ASSET_PREFIX], + [REGISTRY_IDENT, REGISTRY_PREFIX], + [REGISTRYAUTH_IDENT, REGISTRYAUTH_PREFIX] ]) /** diff --git a/packages/registries/package.json b/packages/registries/package.json new file mode 100644 index 00000000..ab3d5b51 --- /dev/null +++ b/packages/registries/package.json @@ -0,0 +1,42 @@ +{ + "name": "@cord.network/registries", + "version": "0.9.3-1rc4", + "description": "CORD Registry Management", + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/cjs/index.d.ts", + "exports": { + ".": { + "import": "./lib/esm/index.js", + "require": "./lib/cjs/index.js" + } + }, + "files": [ + "lib/**/*" + ], + "scripts": { + "clean": "rimraf ./lib", + "build": "yarn clean && yarn build:ts", + "build:ts": "yarn build:cjs && yarn build:esm", + "build:cjs": "tsc --declaration -p tsconfig.build.json && echo '{\"type\":\"commonjs\"}' > ./lib/cjs/package.json", + "build:esm": "tsc --declaration -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > ./lib/esm/package.json" + }, + "repository": "github:dhiway/cord-js", + "engines": { + "node": ">=20.0" + }, + "author": "Dhiway", + "bugs": "https://github.com/dhiway/cord.js/issues", + "homepage": "https://github.com/dhiway/cord.js#readme", + "devDependencies": { + "rimraf": "^5.0.5", + "typescript": "^5.3.3" + }, + "dependencies": { + "@cord.network/config": "workspace:*", + "@cord.network/identifier": "workspace:*", + "@cord.network/network": "workspace:*", + "@cord.network/types": "workspace:*", + "@cord.network/utils": "workspace:*" + } +} diff --git a/packages/registries/src/Registries.chain.ts b/packages/registries/src/Registries.chain.ts new file mode 100644 index 00000000..0ad0a648 --- /dev/null +++ b/packages/registries/src/Registries.chain.ts @@ -0,0 +1,781 @@ + +/** + * @packageDocumentation + * @module Registries/chain + * + * The Registries module provides a framework for creating and managing + * isolated registries within the CORD blockchain, offering fine-grained + * control through a permission system. It allows for the creation, + * status modification, and delegate management within these registries. + * + * ## Overview + * + * The Registries module enables the creation of distinct registries on the + * CORD blockchain, each with its own governance rules. These registries can + * be used to manage various ecosystems or communities within the larger + * blockchain network. Each registry is identified by a unique identifier + * and can be governed by appointed delegates. + * + * ## Interface + * + * The module provides various functions for managing registries: + * + * - `create`: Initializes a new registry with a unique identifier. + * - `update`: Updates an existing registry with new information. + * - `revoke`: Marks a registry as revoked, effectively deactivating it. + * - `reinstate`: Reactivates a previously revoked registry. + * - `archive`: Moves a registry to an archived state. + * - `restore`: Restores a previously archived registry to active status. + * - `addDelegate`: Adds a delegate to the registry with specific permissions. + * - `addAdminDelegate`: Adds an administrative delegate to manage the registry. + * - `addAuditDelegate`: Adds an audit delegate with auditing permissions. + * - `removeDelegate`: Removes a delegate, revoking their permissions. + * + * ## Permissions + * + * This module implements a granular permission system to manage actions + * that can be performed by delegates within a registry. Delegates can be + * assigned roles such as admin or regular delegate, each with defined permissions. + * + * ## Data Privacy + * + * The Registries module prioritizes data privacy, avoiding the storage of personal + * or sensitive data directly on-chain. Instead, it manages references to off-chain + * data, ensuring compliance with privacy regulations. Users and developers are responsible + * for managing off-chain data according to applicable laws and standards. + * + * ## Usage + * + * The Registries module can be leveraged by other modules (e.g., the Entries module) + * to create compartmentalized, governed sections within the blockchain. This is useful + * for applications requiring distinct governance models or privacy settings. + * + * ## Governance Integration + * + * The module integrates with on-chain governance tools, enabling registry + * administrators and delegates to propose changes, vote on initiatives, and manage + * registries in line with the collective decisions of its members. + * + * ## Examples + * + * - Create a registry for a community-driven project. + * - Archive and restore a registry for future use. + * - Revoke and reinstate registries based on inactivity or violations. + * - Add delegates to a registry to ensure compliance with governance standards. + */ + +import { + CordKeyringPair, +} from '@cord.network/types' + +import { Option } from '@polkadot/types'; + +import { Chain } from '@cord.network/network' + +import { SDKErrors } from '@cord.network/utils' + +import { ConfigService } from '@cord.network/config' + +import { + IRegistryCreate, IRegistryUpdate, + RegistryAuthorizationUri, + RegistryUri, RegistryPermissionType, + RegistryPermission, IRegistryAuthorization, +} from '@cord.network/types'; + +import { + uriToIdentifier, +} from '@cord.network/identifier' + +import type { + PalletRegistriesRegistryAuthorization, +} from '@cord.network/augment-api' + + +/** + * Checks whether a registry is stored in the CORD blockchain. + * + * This function queries the chain for the existence of a registry using the provided + * registry URI. It returns `true` if the registry exists; otherwise, it returns `false`. + * + * @param registryUri - The URI of the registry to check for existence. + * @returns A promise that resolves to a boolean indicating whether the registry exists. + * @throws {SDKErrors.CordQueryError} If an error occurs while querying the chain space. + * + * @example + * // Example: Checking if a registry exists + * const registryExists = await isRegistryStored('space:cord:example_registry_uri'); + * console.log('Registry exists:', registryExists); + */ +export async function isRegistryStored( + registryUri: RegistryUri +): Promise { + try { + const api = ConfigService.get('api'); + const identifier = uriToIdentifier(registryUri); + const encoded = await api.query.registries.registryInfo(identifier); + + return !encoded.isNone + } catch (error) { + throw new SDKErrors.CordQueryError( + `Error querying the chain space: ${error}` + ) + } +} + + +/** + * Checks whether a registry authorization is stored in the CORD blockchain. + * + * This function queries the chain for the existence of a registry authorization + * using the provided authorization URI. It returns `true` if the authorization exists; + * otherwise, it returns `false`. + * + * @param authorizationUri - The URI of the registry authorization to check for existence. + * @returns A promise that resolves to a boolean indicating whether the registry authorization exists. + * @throws {SDKErrors.CordQueryError} If an error occurs while querying the chain space. + * + * @example + * // Example: Checking if a registry authorization exists + * const authorizationExists = await isRegistryAuthorizationStored('auth:cord:example_authorization_uri'); + * console.log('Authorization exists:', authorizationExists); + * + */ +export async function isRegistryAuthorizationStored( + authorizationUri: RegistryAuthorizationUri +): Promise { + try { + const api = ConfigService.get('api') + const identifier = uriToIdentifier(authorizationUri) + const encoded = await api.query.registries.authorizations(identifier) as Option; + + return !encoded.isNone + } catch (error) { + throw new SDKErrors.CordQueryError( + `Error querying authorization existence: ${error}` + ) + } +} + + +/** + * Dispatches a request to create a new registry on the CORD blockchain. + * + * This function checks if a registry already exists at the specified URI. If it does, + * an error is thrown. If the registry does not exist, it creates a new registry + * using the provided details and submits the transaction to the chain. + * + * @param registryDetails - An object containing the details required to create the registry, including: + * - `uri`: The unique identifier for the registry. + * - `authorizationUri`: The URI for the associated authorization. + * - `digest`: A hash representing the registry's content. + * - `schemaId`: The identifier for the schema used. + * - `blob`: Additional data related to the registry. + * @param authorAccount - The account that will authorize the creation of the registry. + * @returns A promise that resolves to an object containing the created registry's URI and its authorization URI. + * @throws {SDKErrors.CordDispatchError} If the registry already exists or if an error occurs while dispatching to the chain. + * + * @example + * // Example: Creating a new registry + * const newRegistry = await dispatchCreateRegistryToChain({ + * uri: 'registry:cord:example_registry_uri', + * authorizationUri: 'auth:cord:example_authorization_uri', + * digest: '0xabc123...', + * schemaId: 'schema:cord:example_schema_id', + * blob: 'Registry data blob' + * }, authorAccount); + * console.log('Created Registry URI:', newRegistry.uri); + * + */ +export async function dispatchCreateRegistryToChain( + registryDetails: IRegistryCreate, + authorAccount: CordKeyringPair +): Promise<{ uri: RegistryUri, authorizationUri: RegistryAuthorizationUri }> { + const registryObj = { + uri: registryDetails.uri, + authorizationUri: registryDetails.authorizationUri + } + + const registryExists = await isRegistryStored(registryDetails.uri); + + if (registryExists) { + throw new SDKErrors.CordDispatchError( + `Registry already exists at URI: "${registryDetails.uri}".` + ); + } + + try { + const api = ConfigService.get('api'); + const registryId = uriToIdentifier(registryDetails.uri); + + const extrinsic = api.tx.registries.create( + registryId, + registryDetails.digest, + registryDetails.schemaId, + registryDetails.blob + ); + + await Chain.signAndSubmitTx(extrinsic, authorAccount); + + return registryObj; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error); + throw new SDKErrors.CordDispatchError( + `Error dispatching to chain: "${errorMessage}".` + ); + } +} + + +/** + * Dispatches a request to update an existing registry on the CORD blockchain. + * + * This function checks if the specified registry exists. If it does not exist, + * an error is thrown. If the registry is found, it updates the registry with + * the new details provided and submits the transaction to the chain. + * + * @param registryDetails - An object containing the details required to update the registry, including: + * - `uri`: The unique identifier for the registry to be updated. + * - `authorizationUri`: The URI for the associated authorization. + * - `digest`: A hash representing the updated content of the registry. + * - `blob`: Additional data related to the registry update. + * @param authorAccount - The account that will authorize the update of the registry. + * @returns A promise that resolves to an object containing the updated registry's URI and its authorization URI. + * @throws {SDKErrors.CordDispatchError} If the registry does not exist or if an error occurs while dispatching to the chain. + * + * @example + * // Example: Updating an existing registry + * const updatedRegistry = await dispatchUpdateRegistryToChain({ + * uri: 'registry:cord:example_registry_uri', + * authorizationUri: 'auth:cord:example_authorization_uri', + * digest: '0xdef456...', + * blob: 'Updated registry data blob' + * }, authorAccount); + * console.log('Updated Registry URI:', updatedRegistry.uri); + * + */ +export async function dispatchUpdateRegistryToChain( + registryDetails: IRegistryUpdate, + authorAccount: CordKeyringPair, +): Promise<{ uri: RegistryUri, authorizationUri: RegistryAuthorizationUri }> { + const registryObj = { + uri: registryDetails.uri, + authorizationUri: registryDetails.authorizationUri + } + + const registryExists = await isRegistryStored(registryDetails.uri); + + if (!registryExists) { + throw new SDKErrors.CordDispatchError( + `Registry URI does not exist: "${registryDetails.uri}".` + ); + } + + try { + const api = ConfigService.get('api') + + const registryId = uriToIdentifier(registryDetails.uri); + const authorizationId = uriToIdentifier(registryDetails.authorizationUri); + + const extrinsic = api.tx.registries.update( + registryId, + registryDetails.digest, + registryDetails.blob, + authorizationId, + ); + + await Chain.signAndSubmitTx(extrinsic, authorAccount); + + return registryObj + } catch(error) { + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error) + throw new SDKErrors.CordDispatchError( + `Error dispatching to chain: "${errorMessage}".` + ) + } +} + + +/** + * Dispatches a request to revoke authorization for a specified registry on the CORD blockchain. + * + * This function checks if the specified registry exists. If it does not exist, + * an error is thrown. If the registry is found, it revokes the specified authorization + * and submits the transaction to the chain. + * + * @param registryUri - The URI of the registry for which the authorization is to be revoked. + * @param authorizationUri - The URI of the authorization to be revoked. + * @param authorAccount - The account that will authorize the revocation of the authorization. + * @returns A promise that resolves to an object containing the revoked registry's URI and its authorization URI. + * @throws {SDKErrors.CordDispatchError} If the registry does not exist or if an error occurs while dispatching to the chain. + * + * @example + * // Example: Revoking authorization for a registry + * const revokedAuthorization = await dispatchRevokeToChain( + * 'registry:cord:example_registry_uri', + * 'auth:cord:example_authorization_uri', + * authorAccount + * ); + * console.log('Revoked Registry URI:', revokedAuthorization.uri); + * + */ +export async function dispatchRevokeToChain( + registryUri: RegistryUri, + authorizationUri: RegistryAuthorizationUri, + authorAccount: CordKeyringPair, +): Promise<{ uri: RegistryUri, authorizationUri: RegistryAuthorizationUri }> { + const registryObj = { + uri: registryUri, + authorizationUri: authorizationUri + } + + const registryExists = await isRegistryStored(registryUri); + + if (!registryExists) { + throw new SDKErrors.CordDispatchError( + `Registry URI does not exist: "${registryUri}".` + ); + } + + try { + const api = ConfigService.get('api') + + const registryId = uriToIdentifier(registryUri); + const authorizationId = uriToIdentifier(authorizationUri); + + const extrinsic = api.tx.registries.revoke( + registryId, + authorizationId, + ); + + await Chain.signAndSubmitTx(extrinsic, authorAccount); + + return registryObj + } catch(error) { + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error) + throw new SDKErrors.CordDispatchError( + `Error dispatching to chain: "${errorMessage}".` + ) + } +} + + +/** + * Dispatches a request to reinstate authorization for a specified registry on the CORD blockchain. + * + * This function checks if the specified registry exists. If it does not exist, + * an error is thrown. If the registry is found, it reinstates the specified authorization + * and submits the transaction to the chain. + * + * @param registryUri - The URI of the registry for which the authorization is to be reinstated. + * @param authorizationUri - The URI of the authorization to be reinstated. + * @param authorAccount - The account that will authorize the reinstatement of the authorization. + * @returns A promise that resolves to an object containing the reinstated registry's URI and its authorization URI. + * @throws {SDKErrors.CordDispatchError} If the registry does not exist or if an error occurs while dispatching to the chain. + * + * @example + * // Example: Reinstate authorization for a registry + * const reinstatedAuthorization = await dispatchReinstateToChain( + * 'registry:cord:example_registry_uri', + * 'auth:cord:example_authorization_uri', + * authorAccount + * ); + * console.log('Reinstated Registry URI:', reinstatedAuthorization.uri); + * + */ +export async function dispatchReinstateToChain( + registryUri: RegistryUri, + authorizationUri: RegistryAuthorizationUri, + authorAccount: CordKeyringPair, +): Promise<{ uri: RegistryUri, authorizationUri: RegistryAuthorizationUri }> { + const registryObj = { + uri: registryUri, + authorizationUri: authorizationUri + } + + const registryExists = await isRegistryStored(registryUri); + + if (!registryExists) { + throw new SDKErrors.CordDispatchError( + `Registry URI does not exist: "${registryUri}".` + ); + } + + try { + const api = ConfigService.get('api') + + const registryId = uriToIdentifier(registryUri); + const authorizationId = uriToIdentifier(authorizationUri); + + const extrinsic = api.tx.registries.reinstate( + registryId, + authorizationId, + ); + + await Chain.signAndSubmitTx(extrinsic, authorAccount); + + return registryObj + } catch(error) { + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error) + throw new SDKErrors.CordDispatchError( + `Error dispatching to chain: "${errorMessage}".` + ) + } +} + + +/** + * Dispatches a request to archive a specified registry on the CORD blockchain. + * + * This function checks if the specified registry exists. If it does not exist, + * an error is thrown. If the registry is found, it archives the specified registry + * and submits the transaction to the chain. + * + * @param registryUri - The URI of the registry to be archived. + * @param authorizationUri - The URI of the authorization associated with the registry. + * @param authorAccount - The account that will authorize the archiving of the registry. + * @returns A promise that resolves to an object containing the archived registry's URI and its authorization URI. + * @throws {SDKErrors.CordDispatchError} If the registry does not exist or if an error occurs while dispatching to the chain. + * + * @example + * // Example: Archive a registry + * const archivedRegistry = await dispatchArchiveToChain( + * 'registry:cord:example_registry_uri', + * 'auth:cord:example_authorization_uri', + * authorAccount + * ); + * console.log('Archived Registry URI:', archivedRegistry.uri); + * + */ +export async function dispatchArchiveToChain( + registryUri: RegistryUri, + authorizationUri: RegistryAuthorizationUri, + authorAccount: CordKeyringPair, +): Promise<{ uri: RegistryUri, authorizationUri: RegistryAuthorizationUri }> { + const registryObj = { + uri: registryUri, + authorizationUri: authorizationUri + } + + const registryExists = await isRegistryStored(registryUri); + + if (!registryExists) { + throw new SDKErrors.CordDispatchError( + `Registry URI does not exist: "${registryUri}".` + ); + } + + try { + const api = ConfigService.get('api') + + const registryId = uriToIdentifier(registryUri); + const authorizationId = uriToIdentifier(authorizationUri); + + const extrinsic = api.tx.registries.archive( + registryId, + authorizationId, + ); + + await Chain.signAndSubmitTx(extrinsic, authorAccount); + + return registryObj + } catch(error) { + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error) + throw new SDKErrors.CordDispatchError( + `Error dispatching to chain: "${errorMessage}".` + ) + } +} + + +/** + * Dispatches a request to restore a already archived registry on the CORD blockchain. + * + * This function checks if the specified registry exists. If it does not exist, + * an error is thrown. If the registry is found, it restores the specified registry + * and submits the transaction to the chain. + * + * @param registryUri - The URI of the registry to be restored. + * @param authorizationUri - The URI of the authorization associated with the registry. + * @param authorAccount - The account that will authorize the restoration of the registry. + * @returns A promise that resolves to an object containing the restored registry's URI and its authorization URI. + * @throws {SDKErrors.CordDispatchError} If the registry does not exist or if an error occurs while dispatching to the chain. + * + * @example + * // Example: Restore a registry + * const restoredRegistry = await dispatchRestoreToChain( + * 'registry:cord:example_registry_uri', + * 'auth:cord:example_authorization_uri', + * authorAccount + * ); + * console.log('Restored Registry URI:', restoredRegistry.uri); + * + */ +export async function dispatchRestoreToChain( + registryUri: RegistryUri, + authorizationUri: RegistryAuthorizationUri, + authorAccount: CordKeyringPair, +): Promise<{ uri: RegistryUri, authorizationUri: RegistryAuthorizationUri }> { + const registryObj = { + uri: registryUri, + authorizationUri: authorizationUri + } + + const registryExists = await isRegistryStored(registryUri); + + if (!registryExists) { + throw new SDKErrors.CordDispatchError( + `Registry URI does not exist: "${registryUri}".` + ); + } + + try { + const api = ConfigService.get('api') + + const registryId = uriToIdentifier(registryUri); + const authorizationId = uriToIdentifier(authorizationUri); + + const extrinsic = api.tx.registries.restore( + registryId, + authorizationId, + ); + + await Chain.signAndSubmitTx(extrinsic, authorAccount); + + return registryObj + } catch(error) { + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error) + throw new SDKErrors.CordDispatchError( + `Error dispatching to chain: "${errorMessage}".` + ) + } +} + + +/** + * Dispatches a transaction to add a delegate authorization for a specified registry. + * + * This function creates an extrinsic based on the provided permission type, which determines + * the kind of authorization to be granted to the specified delegate for the given registry. + * It throws an error if an invalid permission is provided. + * + * @param permission - The type of permission to grant to the delegate. Must be one of the + * defined `RegistryPermission` values (e.g., ASSERT, DELEGATE, ADMIN). + * @param registryId - The identifier of the registry to which the delegate is being added. + * @param delegateId - The identifier of the delegate to be authorized. + * @param authorizationId - The identifier of the authorization associated with the delegate. + * @returns An extrinsic that can be signed and submitted to the chain. + * @throws {SDKErrors.InvalidPermissionError} If the provided permission is not valid. + * + * @example + * // Example: Dispatch a transaction to add a delegate authorization + * const extrinsic = dispatchDelegateAuthorizationTx( + * RegistryPermission.ASSERT, + * 'registryId123', + * 'delegateId456', + * 'authorizationId789' + * ); + * console.log('Extrinsic to be dispatched:', extrinsic); + * + */ +function dispatchDelegateAuthorizationTx( + permission: RegistryPermissionType, + registryId: string, + delegateId: string, + authorizationId: string +) { + const api = ConfigService.get('api') + + switch (permission) { + case RegistryPermission.ASSERT: + return api.tx.registries.addDelegate(registryId, delegateId, authorizationId) + case RegistryPermission.DELEGATE: + return api.tx.registries.addDelegator(registryId, delegateId, authorizationId) + case RegistryPermission.ADMIN: + return api.tx.registries.addAdminDelegate(registryId, delegateId, authorizationId) + default: + throw new SDKErrors.InvalidPermissionError( + `Permission not valid:"${permission}".` + ) + } +} + + +/** + * Dispatches a transaction to authorize a delegate for a specified registry. + * + * This function checks the existence of the registry and the authorization for the delegator, + * then constructs an extrinsic to add the delegate authorization with the given permission. + * It submits the transaction to the chain and throws an error if any step fails. + * + * @param request - The authorization request object, containing the registry URI, delegate URI, and permission. + * @param delegatorAuthorizationUri - The authorization URI of the delegator authorizing the action. + * @param authorAccount - The account of the author who signs and submits the transaction. + * @returns The `RegistryAuthorizationUri` after successfully dispatching the authorization. + * @throws {SDKErrors.CordDispatchError} If the registry or authorization does not exist, or if there's an error during dispatch. + * + * @example + * // Example: Dispatch a delegate authorization to the chain + * const authorizationUri = await dispatchDelegateAuthorization( + * { + * uri: 'registryUri123', + * delegateUri: 'did:cord:3delegate123', + * permission: RegistryPermission.ADMIN + * }, + * 'delegatorAuthorizationUri456', + * authorAccount + * ); + * console.log('Authorization dispatched with URI:', authorizationUri); + * + */ +export async function dispatchDelegateAuthorization( + request: IRegistryAuthorization, + delegatorAuthorizationUri: RegistryAuthorizationUri, + authorAccount: CordKeyringPair, +): Promise { + try { + + const registryExists = await isRegistryStored(request.uri); + if (!registryExists) { + throw new SDKErrors.CordDispatchError( + `Registry URI does not exist: "${request.uri}".` + ); + } + + const authorizationExists = await isRegistryAuthorizationStored(delegatorAuthorizationUri); + if (!authorizationExists) { + throw new SDKErrors.CordDispatchError( + `Registry Authorization URI does not exist: "${delegatorAuthorizationUri}".` + ); + } + + const registryId = uriToIdentifier(request.uri); + const delegateId = request.delegateUri.replace("did:cord:3", ""); + const delegatorAuthorizationId = uriToIdentifier(delegatorAuthorizationUri); + + const extrinsic = dispatchDelegateAuthorizationTx( + request.permission, + registryId, + delegateId, + delegatorAuthorizationId, + ) + + await Chain.signAndSubmitTx(extrinsic, authorAccount) + + return request.authorizationUri + } catch (error) { + throw new SDKErrors.CordDispatchError( + `Error dispatching delegate authorization: ${JSON.stringify(error)}` + ) + } +} + + +/** + * Removes a delegate from a registry on the chain. + * + * This method is used to remove a delegate's authorization from a given registry. It checks whether the registry + * and the provided authorization exist on-chain, constructs the required parameters, and dispatches the extrinsic + * to the blockchain for execution. + * + * @async + * @function + * @param {RegistryUri} registryUri - The URI of the registry from which the delegate will be removed. + * @param {RegistryAuthorizationUri} removeAuthorizationUri - The URI of the authorization to be removed (i.e., the delegate's authorization). + * @param {RegistryAuthorizationUri} authorizationUri - The URI of the authorization of the account performing the removal (the caller's authorization). + * @param {CordKeyringPair} authorAccount - The account key pair of the entity removing the delegate, used for signing the transaction. + * + * @returns {Promise<{ uri: RegistryUri, removeAuthorizationUri: RegistryAuthorizationUri, authorizationUri: RegistryAuthorizationUri }>} + * An object containing the URIs related to the registry and authorizations. + * - `uri`: The URI of the registry. + * - `removeAuthorizationUri`: The authorization URI of the delegate being removed. + * - `authorizationUri`: The authorization URI of the signer performing the removal. + * + * @throws {SDKErrors.CordDispatchError} + * - If the registry URI does not exist on-chain. + * - If the authorization URI of the signer does not exist on-chain. + * - If an error occurs while dispatching the transaction to the chain. + * + * @example + * ```typescript + * const registryUri = 'did:cord:registry:3abc...'; + * const removeAuthorizationUri = 'did:cord:auth:3xyz...'; + * const authorizationUri = 'did:cord:auth:3signer...'; + * const authorAccount = keyring.addFromUri('//Alice'); + * + * dispatchRemoveDelegateToChain(registryUri, removeAuthorizationUri, authorizationUri, authorAccount) + * .then(result => { + * console.log('Delegate removed:', result); + * }) + * .catch(error => { + * console.error('Error removing delegate:', error); + * }); + * ``` + * + * @description + * The function first verifies the existence of the registry and the signerโ€™s authorization on the blockchain. + * It then encodes the provided URIs into identifiers and submits a signed transaction to remove the delegate + * from the registry. If the removal is successful, it returns an object with the registry URI and relevant authorization URIs. + * + */ +export async function dispatchRemoveDelegateToChain( + registryUri: RegistryUri, + removeAuthorizationUri: RegistryAuthorizationUri, + authorizationUri: RegistryAuthorizationUri, + authorAccount: CordKeyringPair, +): Promise<{ + uri: RegistryUri, + removeAuthorizationUri: RegistryAuthorizationUri, + authorizationUri: RegistryAuthorizationUri +}> { + const registryObj = { + uri: registryUri, + removeAuthorizationUri: removeAuthorizationUri, + authorizationUri: authorizationUri, + } + + const registryExists = await isRegistryStored(registryUri); + + if (!registryExists) { + throw new SDKErrors.CordDispatchError( + `Registry URI does not exist: "${registryUri}".` + ); + } + + const authorizationExists = await isRegistryAuthorizationStored(authorizationUri); + if (!authorizationExists) { + throw new SDKErrors.CordDispatchError( + `Registry remover Authorization URI does not exist: "${authorizationUri}".` + ); + } + + try { + const api = ConfigService.get('api') + + const registryId = uriToIdentifier(registryUri); + const removeAuthorizationId = uriToIdentifier(removeAuthorizationUri); + const authorizationId = uriToIdentifier(authorizationUri); + + const extrinsic = api.tx.registries.removeDelegate( + registryId, + removeAuthorizationId, + authorizationId, + ); + + await Chain.signAndSubmitTx(extrinsic, authorAccount); + + return registryObj + } catch(error) { + const errorMessage = + error instanceof Error ? error.message : JSON.stringify(error) + throw new SDKErrors.CordDispatchError( + `Error dispatching to chain: "${errorMessage}".` + ) + } +} diff --git a/packages/registries/src/Registries.ts b/packages/registries/src/Registries.ts new file mode 100644 index 00000000..71bfb347 --- /dev/null +++ b/packages/registries/src/Registries.ts @@ -0,0 +1,619 @@ +/** + * @packageDocumentation + * @module Registries + * @preferred + * + * The `Registries` module is a crucial component of the CORD SDK, providing a robust set of functionalities for + * creating, updating, and managing registries on the CORD blockchain. Registries serve as structured containers + * for various claims or records, facilitating the organization and retrieval of information securely and efficiently. + * + * Key functionalities include: + * - `registryCreateProperties`: Constructs properties for a new registry, including the registry URI, creator URI, + * digest, and optionally serialized and CBOR-encoded blob. This function is essential for initiating new + * registries with specified attributes. + * - `registryUpdateProperties`: Generates properties to update an existing registry, enabling modifications to + * the registry's content while maintaining its integrity. It allows for updating the registry with new digests + * and blobs as needed. + * - `registryAuthorizationProperties`: Creates authorization properties for a delegate within a specified registry, + * detailing the permissions granted by the delegator. This function is pivotal for managing access control + * within the registry framework. + * + * These functionalities are integral to the efficient management of registries on the CORD blockchain, + * ensuring that they are created, updated, and authorized properly while upholding data integrity and security. + * + * @example + * ```typescript + * // Example: Creating properties for a new registry + * const registryProperties = await registryCreateProperties( + * '5F3s...', // creatorAddress + * null, // digest + * 'schemaId123', // schemaId + * '{"key":"value"}'// blob + * ); + * console.log('Registry Properties:', registryProperties); + * + * // Example: Updating properties of an existing registry + * const updateProperties = await registryUpdateProperties( + * 'registryUri123', // registryUri + * 'authUri456', // authorizationUri + * '5F3s...', // creatorAddress + * null, // digest + * '{"key":"newValue"}' // blob + * ); + * console.log('Updated Registry Properties:', updateProperties); + * + * // Example: Creating authorization properties for a registry + * const authorizationProperties = await registryAuthorizationProperties( + * 'registryUri123', // registryUri + * '5F3s...', // delegateAddress + * 'delegate', // permission + * '5F3x...' // delegatorAddress + * ); + * console.log('Authorization Properties:', authorizationProperties); + * ``` + */ + +import type { + Bytes, + DidUri, + HexString, + IRegistryAuthorization, + IRegistryCreate, + IRegistryUpdate, + RegistryPermissionType, +} from '@cord.network/types'; + +import { SDKErrors, Cbor } from '@cord.network/utils'; + +import type { + RegistryDetails, + AccountId, + H256, + RegistryDigest, + RegistryAuthorizationUri, + RegistryUri, +} from '@cord.network/types'; + +import { + uriToIdentifier, + hashToUri, +} from '@cord.network/identifier'; + +import { + REGISTRY_IDENT, + REGISTRY_PREFIX, + REGISTRYAUTH_IDENT, + REGISTRYAUTH_PREFIX, + blake2AsHex, +} from '@cord.network/types' + +import { ConfigService } from '@cord.network/config'; + +/** + * Computes a Blake2 H256 hash digest from the provided raw data (blob). + * + * This function verifies if the input blob is serialized before hashing it. + * + * @param {string} blob - The raw data input for which the digest needs to be calculated. + * This should be a serialized string. + * + * @returns {Promise} A promise that resolves to the computed digest of the blob, + * represented as a hexadecimal string. + * + * @throws {SDKErrors.InputContentsMalformedError} Throws an error if the blob is not serialized. + * + * ## Usage Example: + * ```typescript + * const rawData = '{"key": "value"}'; // Example raw data + * try { + * const digest = await getDigestFromRawData(rawData); + * console.log(`Computed Digest: ${digest}`); // Logs the computed digest + * } catch (error) { + * console.error(error.message); // Handles any errors thrown + * } + * ``` + * + * This function first checks whether the provided blob is serialized. If not, it throws an error. + * Once confirmed, it encodes the blob into a byte array and calculates the Blake2 hash digest, + * returning the result as a hexadecimal string. + */ +export async function getDigestFromRawData ( + blob: string +) { + + const isASerializedBlob = await isBlobSerialized(blob); + if (!isASerializedBlob) { + throw new SDKErrors.InputContentsMalformedError( + `Input 'blob' is not serialized.` + ); + } + + const api = ConfigService.get('api') + const scaleEncodedRawData = api + .createType('Bytes', blob) + .toU8a() + const registryDigest = blake2AsHex( + Uint8Array.from([ + ...scaleEncodedRawData + ])); + + return registryDigest +} + + +/** + * Generates a URI for authorization based on the provided registry URI, + * delegate address, and creator address. + * + * This function computes a unique authorization URI by creating a digest + * from the registry identifier, the delegate's address, and the creator's address. + * + * @param {RegistryUri} registryUri - The URI of the registry for which authorization is requested. + * + * @param {string} delegateAddress - The address of the delegate for whom the authorization URI is generated. + * + * @param {string} creatorAddress - The address of the creator of the registry, used for authentication. + * + * @returns {Promise} A promise that resolves to the generated + * authorization URI for the specified registry. + * + * ## Usage Example: + * ```typescript + * const registryUri = 'some-registry-uri'; // Example registry URI + * const delegateAddress = 'some-delegate-address'; // Delegate address + * const creatorAddress = 'some-creator-address'; // Creator address + * + * try { + * const authorizationUri = await getUriForAuthorization(registryUri, delegateAddress, creatorAddress); + * console.log(`Authorization URI: ${authorizationUri}`); // Logs the generated authorization URI + * } catch (error) { + * console.error(error); + * } + * ``` + * + * This function first encodes the registry identifier and addresses into byte arrays, + * then calculates the Blake2 hash digest of the combined data to create a unique authorization URI. + */ +export async function getUriForAuthorization( + registryUri: RegistryUri, + delegateAddress: string, + creatorAddress: string +): Promise { + const api = ConfigService.get('api') + + const scaleEncodedRegistryId = api + .createType('Bytes', uriToIdentifier(registryUri)) + .toU8a() + const scaleEncodedAuthDelegate = api + .createType('AccountId', delegateAddress) + .toU8a() + const scaleEncodedAuthCreator = api + .createType('AccountId', creatorAddress) + .toU8a() + + const authDigest = blake2AsHex( + Uint8Array.from([ + ...scaleEncodedRegistryId, + ...scaleEncodedAuthDelegate, + ...scaleEncodedAuthCreator, + ]) + ) + + const authorizationUri = hashToUri( + authDigest, + REGISTRYAUTH_IDENT, + REGISTRYAUTH_PREFIX + ) as RegistryAuthorizationUri + + return authorizationUri +} + + +/** + * Generates URIs for a registry based on its digest and the creator's address. + * + * @param {RegistryDigest} registryDigest - The unique digest of the registry, used for identification. + * @param {string} creatorAddress - The address of the creator of the registry, represented as a string. + * + * @returns {Promise} A promise that resolves to an object containing the URIs: + * - `uri`: The unique URI for the registry. + * - `authorizationUri`: The URI for authorization related to the registry. + * + * @throws {Error} Throws an error if URI generation fails or if the API call encounters an issue. + * + * ## Usage Example: + * ```typescript + * const registryDetails = await getUriForRegistry(registryDigest, creatorAddress); + * console.log(registryDetails.uri); // Logs the registry URI + * console.log(registryDetails.authorizationUri); // Logs the authorization URI + * ``` + * + * This function constructs a unique registry URI by combining the scale-encoded registry digest + * with the creator's address, hashes them using Blake2, and formats the result into a URI structure. + * It also constructs an authorization URI to manage access and permissions related to the registry. + */ +export async function getUriForRegistry( + registryDigest: RegistryDigest, + creatorAddress: string +): Promise { + const api = ConfigService.get('api') + const scaleEncodedRegistryDigest = api + .createType('H256', registryDigest) + .toU8a() + const scaleEncodedCreator = api + .createType('AccountId', creatorAddress) + .toU8a() + const digest = blake2AsHex( + Uint8Array.from([...scaleEncodedRegistryDigest, ...scaleEncodedCreator]) + ) + + const registryUri = hashToUri(digest, REGISTRY_IDENT, REGISTRY_PREFIX) as RegistryUri + + const authorizationUri = await getUriForAuthorization( + registryUri, + creatorAddress, + creatorAddress + ); + + const registryUris = { + uri: registryUri, + authorizationUri, + } + + return registryUris +} + + +/** + * Checks if the provided blob is serialized. + * + * This function attempts to parse the input `blob` as JSON. If parsing is successful, + * it indicates that the blob is serialized. If the input is not a string or cannot be + * parsed as JSON, it returns false. + * + * @param blob - The input data to check for serialization. This can be of any type. + * + * @returns A promise that resolves to a boolean value: + * - `true` if the blob is a valid JSON string and is serialized. + * - `false` if the blob is not a string or if it cannot be parsed as JSON. + * + * @throws {Error} If the input is not a string and cannot be parsed. + */ +export async function isBlobSerialized( + blob: any +): Promise { + try { + if (typeof blob === 'string') { + JSON.parse(blob); + return true; + } + } catch (e) { + return false; + } + + return false; +} + + +/** + * Encodes a stringified blob into CBOR format. + * + * This function takes a string representing a serialized blob, validates its + * serialization, and then encodes it into the CBOR format. The resulting CBOR + * blob is returned as a base64-encoded string. + * + * @param blob - A string representing the serialized blob that needs to be encoded. + * + * @returns A promise that resolves to a base64-encoded string of the CBOR representation of the input blob. + * + * @throws {SDKErrors.InputContentsMalformedError} If the input blob is not a valid serialized string. + * + * @example + * const cborBlob = await encodeStringifiedBlobToCbor('{"key": "value"}'); + * // cborBlob will contain the base64-encoded CBOR representation of the input blob. + */ +export async function encodeStringifiedBlobToCbor( + blob: string +): Promise { + const isASerializedBlob = await isBlobSerialized(blob); + if (!isASerializedBlob) { + throw new SDKErrors.InputContentsMalformedError( + `Input 'blob' is not serialized.` + ); + } + + const encoder = new Cbor.Encoder({ pack: true, useRecords: true }); + const encodedBlob = encoder.encode(blob); + const cborBlob = encodedBlob.toString('base64'); + + return cborBlob; +} + + +/** + * Decodes a CBOR-encoded blob from a base64 string back to a stringified blob. + * + * This function takes a base64-encoded string representing a CBOR blob, + * decodes it to a buffer, and then decodes the buffer to retrieve the + * original stringified blob. + * + * @param cborBlob - A base64-encoded string representing the CBOR blob to decode. + * + * @returns A promise that resolves to the original stringified blob. + * + * @throws {Error} If decoding fails due to invalid CBOR format or other issues. + * + * @example + * const stringifiedBlob = await decodeCborToStringifiedBlob('base64EncodedCborBlob'); + * // stringifiedBlob will contain the original stringified blob that was encoded. + */ +export async function decodeCborToStringifiedBlob( + cborBlob: string +): Promise { + const decodedBuffer = Buffer.from(cborBlob, 'base64'); + const decodedBlob = Cbor.decode(decodedBuffer); + + return decodedBlob; +} + + +/** + * Creates properties for a new registry, including the registry URI, creator URI, + * digest, and the optionally serialized and CBOR-encoded blob. + * + * This function requires either a digest or a blob to generate the registry properties. + * If a blob is provided without a digest, the digest will be computed from the serialized + * blob. The blob will be CBOR-encoded before dispatching to the blockchain. + * + * If only digest is provided, it will be dispatched as is into CORD Registry. + * + * If both `digest` and `blob` are provided, the function will: + * - Validate the `blob` for serialization. + * - Encode the `blob` in CBOR before dispatching it. + * - Use the existing `digest` as-is for the registry creation process, + * without computing a new digest from the `blob`. + * + * @param creatorAddress - The address of the creator initiating the registry creation. + * @param digest - An optional hex string representing the digest. If not provided, it will + * be computed from the blob. + * @param schemaId - An optional string representing the schema ID for the registry. + * @param blob - An optional string representing the data to be stored in the registry. + * + * @returns A promise that resolves to an object containing the properties of the registry, + * including the URI, creator URI, digest, blob, schema ID, and authorization URI. + * + * @throws {SDKErrors.InputContentsMalformedError} If neither digest nor blob is provided, + * or if the digest is empty after processing. + * + * @example + * const registryProperties = await registryCreateProperties( + * '5F3s...', // creatorAddress + * null, // digest + * 'schemaId123', // schemaId + * '{"key":"value"}' // blob + * ); + * // registryProperties will contain the created registry properties. + * + */ +// TODO: Validate schemaId is a valid data-format and schemaId exists. +export async function registryCreateProperties( + creatorAddress: string, + digest: HexString | null = null, + schemaId: string | null = null, + blob: string | null = null, +): Promise { + + if (!digest && !blob) { + throw new SDKErrors.InputContentsMalformedError( + `Either 'digest' or 'blob' must be provided. Both cannot be null.` + ); + } + + /* Construct digest from serialized blob if digest is absent */ + if (!digest && blob) { + const isASerializedBlob = await isBlobSerialized(blob); + if (!isASerializedBlob) { + blob = JSON.stringify(blob); + } + + digest = await getDigestFromRawData(blob); + + /* Encode the serialized 'blob' in CBOR before dispatch to chain */ + blob = await encodeStringifiedBlobToCbor(blob); + } + + /* Process the blob to be serialized and CBOR encoded is digest is present */ + else if (digest && blob) { + const isASerializedBlob = await isBlobSerialized(blob); + if (!isASerializedBlob){ + blob = JSON.stringify(blob); + } + + /* Encode the 'blob' in CBOR before dispatch to chain */ + blob = await encodeStringifiedBlobToCbor(blob); + } + + if (!digest) { + throw new SDKErrors.InputContentsMalformedError( + `Digest cannot be empty.` + ); + } + + const { uri, authorizationUri } = await getUriForRegistry( + digest as HexString, + creatorAddress + ) + + // TODO: + // Revisit if use of creatorUri as below is correct. + const creatorUri = `did:cord:3${creatorAddress}` as DidUri; + + return { + uri, + creatorUri, + digest, + blob, + schemaId, + authorizationUri, + } +} + + +/** + * Updates properties for an existing registry, including the registry URI, + * creator URI, digest, and optionally serialized and CBOR-encoded blob. + * + * This function requires either a digest or a blob to update the registry properties. + * If a blob is provided without a digest, the digest will be computed from the serialized + * blob. The blob will be CBOR-encoded before dispatching to the blockchain. + * + * If only digest is provided, it will be dispatched as-is into CORD Registry. + * + * If both `digest` and `blob` are provided, the function will: + * - Validate the `blob` for serialization. + * - Encode the `blob` in CBOR before dispatching it. + * - Use the existing `digest` as-is for the registry update process, + * without computing a new digest from the `blob`. + * + * @param registryUri - The URI of the registry to be updated. + * @param authorizationUri - The authorization URI for the registry update. + * @param creatorAddress - The address of the creator initiating the registry update. + * @param digest - An optional hex string representing the digest. If not provided, it will + * be computed from the blob. + * @param blob - An optional string representing the data to be stored in the registry. + * + * @returns A promise that resolves to an object containing the updated properties of the registry, + * including the URI, creator URI, digest, blob, and authorization URI. + * + * @throws {SDKErrors.InputContentsMalformedError} If neither digest nor blob is provided, + * or if the digest is empty after processing. + * + * @example + * const updatedRegistryProperties = await registryUpdateProperties( + * 'registryUri', // registryUri + * 'authorizationUri', // authorizationUri + * '5F3s...', // creatorAddress + * null, // digest + * '{"key":"newValue"}' // blob + * ); + * // updatedRegistryProperties will contain the updated registry properties. + * + */ +export async function registryUpdateProperties( + registryUri: RegistryUri, + authorizationUri: RegistryAuthorizationUri, + creatorAddress: string, + digest: HexString | null = null, + blob: string | null = null, +): Promise { + + if (!digest && !blob) { + throw new SDKErrors.InputContentsMalformedError( + `Either 'digest' or 'blob' must be provided. Both cannot be null.` + ); + } + + /* Construct digest from serialized blob if digest is absent */ + if (!digest && blob) { + const isASerializedBlob = await isBlobSerialized(blob); + if (!isASerializedBlob) { + blob = JSON.stringify(blob); + } + + digest = await getDigestFromRawData(blob); + + /* Encode the 'blob' in CBOR before dispatch to chain */ + blob = await encodeStringifiedBlobToCbor(blob); + } + + /* Process the blob to be serialized and CBOR encoded is digest is present */ + else if (digest && blob) { + const isASerializedBlob = await isBlobSerialized(blob); + if (!isASerializedBlob) { + blob = JSON.stringify(blob); + } + + /* Encode the 'blob' in CBOR before dispatch to chain */ + blob = await encodeStringifiedBlobToCbor(blob); + } + + if (!digest) { + throw new SDKErrors.InputContentsMalformedError( + `Digest cannot be empty.` + ); + } + + // TODO: + // Revisit if use of creatorUri as below is correct. + const creatorUri = `did:cord:3${creatorAddress}` as DidUri; + + return { + uri: registryUri, + creatorUri, + digest, + blob, + authorizationUri, + } +} + + +/** + * Creates properties for registry authorization, including URIs for the registry, + * delegate, and delegator, as well as the associated permission for the authorization. + * + * This function constructs the authorization properties required for a delegate + * to act on behalf of a registrant in a specified registry. It generates the + * delegate and delegator URIs and retrieves the authorization URI for the + * specified registry. + * + * @param registryUri - The URI of the registry for which authorization is being created. + * @param delegateAddress - The address of the delegate who will be granted permissions. + * @param permission - The type of permission being granted to the delegate in the registry. + * @param delegatorAddress - The address of the delegator who is granting the permission to the delegate. + * + * @returns A promise that resolves to an object containing the properties of the registry + * authorization, including the registry URI, authorization URI, delegate URI, permission type, + * and delegator URI. + * + * @throws {SDKErrors.InputContentsMalformedError} If any input parameter is malformed or invalid. + * + * @example + * const authorizationProperties = await registryAuthorizationProperties( + * 'registryUri123', // registryUri + * '5F3s...', // delegateAddress + * 'delegate', // permission + * '5F3x...' // delegatorAddress + * ); + * // authorizationProperties will contain the created registry authorization properties. + * + */ +export async function registryAuthorizationProperties( + registryUri: RegistryUri, + delegateAddress: string, + permission: RegistryPermissionType, + delegatorAddress: string, +): Promise { + + // TOOD: Revisit below did-abstraction. + const delegateUri = `did:cord:3${delegateAddress}` as DidUri; + const delegatorUri = `did:cord:3${delegatorAddress}` as DidUri; + + const delegateAuthorizationUri = await getUriForAuthorization( + registryUri, + delegateAddress, + delegatorAddress + ); + + return { + uri: registryUri, + authorizationUri: delegateAuthorizationUri, + delegateUri: delegateUri, + permission, + delegatorUri: delegatorUri, + } +} + +// TODO: +// Check if there is a requirement of validating, +// the digest generated from the blob and incoming digest are same. +// That way there would not be any discreprencies b/w blob and digest. \ No newline at end of file diff --git a/packages/registries/src/index.ts b/packages/registries/src/index.ts new file mode 100644 index 00000000..660d3851 --- /dev/null +++ b/packages/registries/src/index.ts @@ -0,0 +1,2 @@ +export * from './Registries.js' +export * from './Registries.chain.js' diff --git a/packages/registries/tsconfig.build.json b/packages/registries/tsconfig.build.json new file mode 100644 index 00000000..d59aa31c --- /dev/null +++ b/packages/registries/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "module": "CommonJS", + "outDir": "./lib/cjs" + }, + + "include": [ + "src/**/*.ts", "src/**/*.js" + ], + + "exclude": [ + "coverage", + "**/*.spec.ts", + ] +} diff --git a/packages/registries/tsconfig.esm.json b/packages/registries/tsconfig.esm.json new file mode 100644 index 00000000..e1f3b73b --- /dev/null +++ b/packages/registries/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "module": "ES6", + "outDir": "./lib/esm" + } +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c2458527..87cbb979 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -45,6 +45,7 @@ "@cord.network/identifier": "workspace:*", "@cord.network/network": "workspace:*", "@cord.network/network-score": "workspace:*", + "@cord.network/registries": "workspace:*", "@cord.network/schema": "workspace:*", "@cord.network/statement": "workspace:*", "@cord.network/types": "workspace:*", diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index bf841219..33a160f7 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -9,4 +9,5 @@ export * as Statement from '@cord.network/statement' export * as Score from '@cord.network/network-score' //export * as Asset from '@cord.network/asset' export * as Utils from '@cord.network/utils' +export * as Registries from '@cord.network/registries' export * from '@cord.network/types' diff --git a/packages/types/src/Registries.ts b/packages/types/src/Registries.ts new file mode 100644 index 00000000..519de492 --- /dev/null +++ b/packages/types/src/Registries.ts @@ -0,0 +1,58 @@ +import type { DidUri } from './DidDocument' +import { HexString } from './Imported.js' + +export const REGISTRY_IDENT = 9274; +export const REGISTRY_PREFIX = 'registry:cord:'; +export type RegistryUri = `${typeof REGISTRY_PREFIX}${string}`; +export type RegistryId = string; +export type RegistryDigest = HexString; +export const REGISTRYAUTH_IDENT = 10001; +export const REGISTRYAUTH_PREFIX = 'registryauth:cord:'; +export type RegistryAuthorizationUri = `${typeof REGISTRYAUTH_PREFIX}${string}`; +export type RegistryAuthorizationId = string; + +export interface RegistryDetails { + uri: RegistryUri + authorizationUri: RegistryAuthorizationUri +} + +// TODO: Fix schemaId once schema-acc pallet becomes active +// TODO: Handle creatorUri as Did. +export interface IRegistryCreate { + uri: RegistryUri + creatorUri: DidUri + digest: RegistryDigest + blob: string | null + schemaId: string | null + authorizationUri: RegistryAuthorizationUri +} + +export interface IRegistryUpdate { + uri: RegistryUri + creatorUri: DidUri + digest: RegistryDigest + blob: string | null + authorizationUri: RegistryAuthorizationUri +} + +/* eslint-disable no-bitwise */ +export const RegistryPermission = { + ASSERT: 1 << 0, // 0001 + DELEGATE: 1 << 1, // 0010 + ADMIN: 1 << 2, // 0100 +} as const +export type RegistryPermissionType = (typeof RegistryPermission)[keyof typeof RegistryPermission] + +export interface IRegistryAuthorization { + uri: RegistryUri + authorizationUri: RegistryAuthorizationUri + delegateUri: DidUri + permission: RegistryPermissionType + delegatorUri: DidUri +} + +export interface IRegistryAuthorizationDetails { + uri: RegistryUri + delegateUri: DidUri + permission: RegistryPermissionType[] +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b9408d07..99209858 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -22,3 +22,4 @@ export * from './Weight.js' export * from './Imported.js' export * from './Keys.js' export * from './Asset.js' +export * from './Registries.js' diff --git a/tsconfig.docs.json b/tsconfig.docs.json index 5daeb025..2bcb9087 100644 --- a/tsconfig.docs.json +++ b/tsconfig.docs.json @@ -23,7 +23,8 @@ "packages/schema/src/index.ts", "packages/network-score/src/index.ts", "packages/asset/src/index.ts", - "packages/sdk/src/index.ts" + "packages/sdk/src/index.ts", + "packages/registries/src/index.ts", ], "out": "docs", "theme": "default", diff --git a/tsconfig.json b/tsconfig.json index b4d485f2..f15c2db2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -58,7 +58,10 @@ ], "@cord.network/asset": [ "asset/src" - ] + ], + "@cord.network/registries": [ + "registries/src" + ], } } } diff --git a/yarn.lock b/yarn.lock index c8066813..924e18d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1723,6 +1723,20 @@ __metadata: languageName: unknown linkType: soft +"@cord.network/registries@workspace:*, @cord.network/registries@workspace:packages/registries": + version: 0.0.0-use.local + resolution: "@cord.network/registries@workspace:packages/registries" + dependencies: + "@cord.network/config": "workspace:*" + "@cord.network/identifier": "workspace:*" + "@cord.network/network": "workspace:*" + "@cord.network/types": "workspace:*" + "@cord.network/utils": "workspace:*" + rimraf: "npm:^5.0.5" + typescript: "npm:^5.3.3" + languageName: unknown + linkType: soft + "@cord.network/schema@workspace:*, @cord.network/schema@workspace:packages/schema": version: 0.0.0-use.local resolution: "@cord.network/schema@workspace:packages/schema" @@ -1753,6 +1767,7 @@ __metadata: "@cord.network/identifier": "workspace:*" "@cord.network/network": "workspace:*" "@cord.network/network-score": "workspace:*" + "@cord.network/registries": "workspace:*" "@cord.network/schema": "workspace:*" "@cord.network/statement": "workspace:*" "@cord.network/types": "workspace:*"