From fcd469d279bfefe843c200c2069c0179c732477f Mon Sep 17 00:00:00 2001 From: Adi Bhagavath Date: Mon, 9 Oct 2023 11:12:14 +0530 Subject: [PATCH] Add score module to sdk --- demo/src/demo-score.ts | 203 ++++++++++++------ demo/src/demo.ts | 2 +- demo/src/utils/updateScore.ts | 56 +++++ packages/did/src/DidDetails/FullDidDetails.ts | 1 + packages/modules/src/index.ts | 1 + packages/modules/src/scoring/Scoring.chain.ts | 65 ++++++ packages/modules/src/scoring/Scoring.ts | 55 +++++ packages/modules/src/scoring/index.ts | 2 + packages/types/src/Score.ts | 44 ++-- packages/utils/src/SDKErrors.ts | 2 + 10 files changed, 349 insertions(+), 82 deletions(-) create mode 100644 demo/src/utils/updateScore.ts create mode 100644 packages/modules/src/scoring/Scoring.chain.ts create mode 100644 packages/modules/src/scoring/Scoring.ts create mode 100644 packages/modules/src/scoring/index.ts diff --git a/demo/src/demo-score.ts b/demo/src/demo-score.ts index 2d66a3c3..43cafb6f 100644 --- a/demo/src/demo-score.ts +++ b/demo/src/demo-score.ts @@ -1,93 +1,164 @@ import * as Cord from '@cord.network/sdk' -import { ScoreType } from '@cord.network/types' -import { UUID } from '@cord.network/utils' +import { UUID, Crypto } from '@cord.network/utils' +import { generateKeypairs } from './utils/generateKeypairs' +import { createDid } from './utils/generateDid' +import { addRegistryAdminDelegate } from './utils/generateRegistry' +import { randomUUID } from 'crypto' +import { addAuthority } from './utils/createAuthorities' +import { createAccount } from './utils/createAccount' +import { updateScore } from './utils/updateScore' +import { ScoreType, IJournalContent, EntryType } from '@cord.network/types' async function main() { - await Cord.init({ address: 'ws://127.0.0.1:9944' }) - - // Step 1: Setup Org Identity - console.log(`\nā„ļø Demo Identities (KeyRing)`) - //3x4DHc1rxVAEqKWSx1DAAA8wZxLB4VhiRbMV997niBckUwSi - const sellerIdentity = Cord.Identity.buildFromURI('//Entity', { - signingKeyPairType: 'sr25519', - }) - console.log( - `šŸ› Seller Entity (${sellerIdentity.signingKeyType}): ${sellerIdentity.address}` + const networkAddress = 'ws://127.0.0.1:63554' + Cord.ConfigService.set({ submitTxResolveOn: Cord.Chain.IS_IN_BLOCK }) + await Cord.connect(networkAddress) + + const api = Cord.ConfigService.get('api') + + console.log(`\nā„ļø New Member`) + const authorityAuthorIdentity = Crypto.makeKeypairFromUri( + '//Alice', + 'sr25519' + ) + // Setup author authority account. + const { account: authorIdentity } = await createAccount() + console.log(`šŸ¦ Member (${authorIdentity.type}): ${authorIdentity.address}`) + await addAuthority(authorityAuthorIdentity, authorIdentity.address) + console.log(`šŸ” Member permissions updated`) + console.log('āœ… Network Member added!') + + // Step 2: Setup Identities + console.log(`\nā„ļø Demo Identities (KeyRing)`) + const { mnemonic: issuerMnemonic, document: issuerDid } = await createDid( + authorIdentity ) - const deliveryIdentity = Cord.Identity.buildFromURI('//Delivery', { - signingKeyPairType: 'sr25519', - }) + const issuerKeys = generateKeypairs(issuerMnemonic) console.log( - `šŸ› Delivery Entity (${deliveryIdentity.signingKeyType}): ${deliveryIdentity.address}` + `šŸ› Issuer (${issuerDid?.assertionMethod![0].type}): ${issuerDid.uri}` ) - const collectorIdentity = Cord.Identity.buildFromURI('//BuyerApp', { - signingKeyPairType: 'ed25519', - }) + + // Create Delegate One DID + const { mnemonic: delegateOneMnemonic, document: delegateOneDid } = + await createDid(authorIdentity) + + const delegateOneKeys = generateKeypairs(delegateOneMnemonic) + console.log( - `šŸ§‘šŸ»ā€šŸ’¼ Score Collector (${collectorIdentity.signingKeyType}): ${collectorIdentity.address}` + `šŸ› Delegate (${delegateOneDid?.assertionMethod![0].type}): ${ + delegateOneDid.uri + }` ) - const requestorIdentity = Cord.Identity.buildFromURI('//SellerApp', { - signingKeyPairType: 'ed25519', - }) + + console.log('āœ… Identities created!') + + // Entities + console.log(`\nā„ļø Demo Entities`) + const sellerIdentity = Crypto.makeKeypairFromUri('//Entity', 'sr25519') console.log( - `šŸ‘©ā€āš•ļø Score Requestor (${requestorIdentity.signingKeyType}): ${requestorIdentity.address}` + `šŸ› Seller Entity (${sellerIdentity.type}): ${sellerIdentity.address}` + ) + await addAuthority(authorityAuthorIdentity, sellerIdentity.address) + + const { mnemonic: sellerMnemonic, document: sellerDid } = await createDid( + sellerIdentity ) - const transactionAuthor = Cord.Identity.buildFromURI('//Bob', { - signingKeyPairType: 'sr25519', - }) + + const collectorIdentity = Crypto.makeKeypairFromUri('//BuyerApp', 'sr25519') console.log( - `šŸ¢ Transaction Author (${transactionAuthor.signingKeyType}): ${transactionAuthor.address}` + `šŸ§‘šŸ»ā€šŸ’¼ Score Collector (${collectorIdentity.type}): ${collectorIdentity.address}` ) - console.log('āœ… Identities created!') + await addAuthority(authorityAuthorIdentity, collectorIdentity.address) - // Step 2: Create a jounal entry - console.log(`\nā„ļø Journal Entry `) - let journalContent = { - entity: sellerIdentity.address, - uid: UUID.generate().toString(), - tid: UUID.generate().toString(), - collector: collectorIdentity.address, - requestor: requestorIdentity.address, - scoreType: ScoreType.overall, - score: 3.7, + const { mnemonic: collectorMnemonic, document: collectorDid } = + await createDid(collectorIdentity) + + console.log('āœ… Entities created!') + + console.log(`\nā„ļø Registry Creation `) + + const registryTitle = `Registry v3.${randomUUID().substring(0, 4)}` + const registryDetails: Cord.IContents = { + title: registryTitle, + description: 'Registry for for scoring', } - console.dir(journalContent, { depth: null, colors: true }) - let newJournalEntry = Cord.Score.fromJournalProperties( - journalContent, - sellerIdentity - ) + const registryType: Cord.IRegistryType = { + details: registryDetails, + creator: issuerDid.uri, + } - let journalCreationExtrinsic = await Cord.Score.entries(newJournalEntry) - console.log(`\nā„ļø Transformed Journal Entry `) - console.dir(newJournalEntry, { depth: null, colors: true }) + const txRegistry: Cord.IRegistry = + Cord.Registry.fromRegistryProperties(registryType) + let registry try { - await Cord.Chain.signAndSubmitTx( - journalCreationExtrinsic, - transactionAuthor, - { - resolveOn: Cord.Chain.IS_IN_BLOCK, - rejectOn: Cord.Chain.IS_ERROR, - } + await Cord.Registry.verifyStored(txRegistry) + console.log('Registry already stored. Skipping creation') + } catch { + console.log('Regisrty not present. Creating it now...') + // Authorize the tx. + const tx = api.tx.registry.create(txRegistry.details, null) + const extrinsic = await Cord.Did.authorizeTx( + issuerDid.uri, + tx, + async ({ data }) => ({ + signature: issuerKeys.assertionMethod.sign(data), + keyType: issuerKeys.assertionMethod.type, + }), + authorIdentity.address ) - console.log('āœ… Journal Entry added!') - } catch (e: any) { - console.log(e.errorCode, '-', e.message) + console.log('\n', txRegistry) + // Write to chain then return the Schema. + await Cord.Chain.signAndSubmitTx(extrinsic, authorIdentity) + registry = txRegistry } + console.log('\nāœ… Registry created!') - console.log(`\nā„ļø Query Chain Scores `) - const chainScore = await Cord.Score.query( - journalContent.entity, - journalContent.scoreType + // Step 4: Add Delelegate One as Registry Admin + console.log(`\nā„ļø Registry Admin Delegate Authorization `) + const registryAuthority = await addRegistryAdminDelegate( + authorIdentity, + issuerDid.uri, + registry['identifier'], + delegateOneDid.uri, + async ({ data }) => ({ + signature: issuerKeys.capabilityDelegation.sign(data), + keyType: issuerKeys.capabilityDelegation.type, + }) ) - console.dir(chainScore, { depth: null, colors: true }) + console.log(`\nāœ… Registry Authorization - ${registryAuthority} - created!`) - const chainAvgScore = await Cord.Score.queryAverage( - journalContent.entity, - journalContent.scoreType + console.log(`\nā„ļø Journal Entry `) + let journalContent: IJournalContent = { + entity: sellerDid.uri.replace('did:cord:', ''), + tid: UUID.generatev4().toString(), + collector: collectorDid.uri.replace('did:cord:', ''), + rating_type: ScoreType.overall, + rating: 12.116, + entry_type: EntryType.debit, + count: 5, + } + console.dir(journalContent, { depth: null, colors: true }) + console.log('\nāœ… Journal Entry created!') + + console.log('\nAnchoring the score on the blockchain...') + const scoreIdentifier = await updateScore( + journalContent, + registryAuthority, + authorIdentity, + delegateOneDid.uri, + delegateOneKeys + ) + + console.log( + '\nāœ… The score has been successfully anchored on the blockchain \nIdentifier:', + scoreIdentifier ) - console.dir(chainAvgScore, { depth: null, colors: true }) + let x = await Cord.Scoring.fetchAverageScore(sellerDid.uri,'Overall') + console.log('x',x) + let y = await Cord.Scoring.fetchJournalFromChain('score:cord:Y8ARa4Djnve7csqRDMtNn2D3LjY4mbyiUyV7zKYEv99LPYgY9','Overall') + console.log(y) } main() diff --git a/demo/src/demo.ts b/demo/src/demo.ts index f81e58e7..63242437 100644 --- a/demo/src/demo.ts +++ b/demo/src/demo.ts @@ -34,7 +34,7 @@ function getChallenge(): string { } async function main() { - const networkAddress = 'ws://127.0.0.1:9944' + const networkAddress = 'ws://127.0.0.1:63554' Cord.ConfigService.set({ submitTxResolveOn: Cord.Chain.IS_IN_BLOCK }) await Cord.connect(networkAddress) diff --git a/demo/src/utils/updateScore.ts b/demo/src/utils/updateScore.ts new file mode 100644 index 00000000..43bb6343 --- /dev/null +++ b/demo/src/utils/updateScore.ts @@ -0,0 +1,56 @@ +import * as Cord from '@cord.network/sdk' +import { IJournalContent, IRatingInput } from '@cord.network/types' + +/** + * This function anchors the score on the blockchain + * @param journalContent - Score entry details + * @param registryAuthority - Registry authority + * @param authorIdentity - The account that will be used to sign and submit the extrinsic. + * @param authorDid - DID of the entity which anchors the transaction. + * @param authorKeys - Keys which are used to sign. + * @returns the hash of the score entry if the operation is executed successfully. + */ + +export async function updateScore( + journalContent: IJournalContent, + registryAuthority: String, + authorIdentity: Cord.CordKeyringPair, + authorDid: Cord.DidUri, + authorKeys: Cord.CordKeyringPair +) { + const api = Cord.ConfigService.get('api') + + journalContent.rating = await Cord.Scoring.adjustAndRoundRating( + journalContent.rating + ) + const digest = await Cord.Scoring.generateDigestFromJournalContent( + journalContent + ) + const authorization = registryAuthority.replace('auth:cord:', '') + const ratingInput: IRatingInput = { + entry: journalContent, + digest: digest, + creator: authorIdentity.address, + } + const journalCreationExtrinsic = await api.tx.score.addRating( + ratingInput, + authorization + ) + + const authorizedStreamTx = await Cord.Did.authorizeTx( + authorDid, + journalCreationExtrinsic, + async ({ data }) => ({ + signature: authorKeys.assertionMethod.sign(data), + keyType: authorKeys.assertionMethod.type, + }), + authorIdentity.address + ) + + try { + await Cord.Chain.signAndSubmitTx(authorizedStreamTx, authorIdentity) + return Cord.Scoring.getUriForScore(journalContent) + } catch (error) { + return error.message + } +} diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index facc3480..cf509326 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -34,6 +34,7 @@ const methodMapping: Record = { 'did.submitDidCall': undefined, didLookup: 'authentication', didName: 'authentication', + score:'authentication', } function getKeyRelationshipForMethod( diff --git a/packages/modules/src/index.ts b/packages/modules/src/index.ts index 50411f08..57fc28cb 100644 --- a/packages/modules/src/index.ts +++ b/packages/modules/src/index.ts @@ -4,6 +4,7 @@ export * as Registry from './registry/index.js' export * as Content from './content/index.js' export * as Stream from './stream/index.js' export * as Document from './document/index.js' +export * as Scoring from './scoring/index.js' export { connect, disconnect, init } from './cordconfig/index.js' export { SDKErrors } from '@cord.network/utils' // export { Identity, PublicIdentity } from './identity/index.js' diff --git a/packages/modules/src/scoring/Scoring.chain.ts b/packages/modules/src/scoring/Scoring.chain.ts new file mode 100644 index 00000000..2ffae16a --- /dev/null +++ b/packages/modules/src/scoring/Scoring.chain.ts @@ -0,0 +1,65 @@ +import { + ScoreType, + IScoreDetails, + SCORE_MODULUS, + IJournalContent, + } from '@cord.network/types' + import { ConfigService } from '@cord.network/config' + import { Identifier, SDKErrors } from '@cord.network/utils' + import * as Did from '@cord.network/did' + + export async function fetchJournalFromChain( + scoreId: string, + scoreType: ScoreType + ): Promise { + const api = ConfigService.get('api') + const cordScoreId = Identifier.uriToIdentifier(scoreId) + const encodedScoreEntry = await api.query.score.journal( + cordScoreId, + scoreType + ) + const decodedScoreEntry = fromChain(encodedScoreEntry) + if (decodedScoreEntry === null) { + throw new SDKErrors.ScoreMissingError( + `There is not a Score of type ${scoreType} with the provided ID "${scoreId}" on chain.` + ) + } else return decodedScoreEntry + } + + export function fromChain( + encodedEntry: any + ): IJournalContent | null { + if (encodedEntry.isSome) { + const unwrapped = encodedEntry.unwrap() + return { + entity: Did.fromChain(unwrapped.entry.entity), + tid: JSON.stringify(unwrapped.entry.tid.toHuman()), + collector: Did.fromChain(unwrapped.entry.collector), + rating_type: unwrapped.entry.ratingType.toString(), + rating: parseInt(unwrapped.entry.rating.toString()) / SCORE_MODULUS, + entry_type: unwrapped.entry.entryType.toString(), + count: parseInt(unwrapped.entry.count.toString()), + } + } else { + return null + } + } + + export async function fetchScore( + entityUri: string, + scoreType: ScoreType + ): Promise { + const api = ConfigService.get('api') + const encoded = await api.query.score.scores(entityUri, scoreType) + if (encoded.isSome) { + const decoded = encoded.unwrap() + return { + rating: JSON.parse(decoded.rating.toString()), + count: JSON.parse(decoded.count.toString()), + } + } else + throw new SDKErrors.ScoreMissingError( + `There is not a Score of type ${scoreType} with the provided ID "${entityUri}" on chain.` + ) + } + \ No newline at end of file diff --git a/packages/modules/src/scoring/Scoring.ts b/packages/modules/src/scoring/Scoring.ts new file mode 100644 index 00000000..9476acc6 --- /dev/null +++ b/packages/modules/src/scoring/Scoring.ts @@ -0,0 +1,55 @@ +import { SCORE_MODULUS, IJournalContent } from '@cord.network/types' +import { Crypto } from '@cord.network/utils' +import { + SCORE_IDENT, + SCORE_PREFIX, + ScoreType, + IScoreAverageDetails, +} from '@cord.network/types' +import { Identifier } from '@cord.network/utils' +import { fetchScore } from './Scoring.chain' + +export function adjustAndRoundRating(rating: number): number { + rating = Math.round(rating * SCORE_MODULUS) + return rating +} + +export function ComputeActualRating(rating: number): number { + return rating / SCORE_MODULUS +} + +export function ComputeAverageRating(rating: number, count: number): number { + return rating / count +} + +export function generateDigestFromJournalContent( + journalContent: IJournalContent +) { + const digest = Crypto.hash(JSON.stringify(journalContent)) + const hexDigest = Crypto.u8aToHex(digest) + return hexDigest +} + +export function getUriForScore(journalContent: IJournalContent) { + const scoreDigest = generateDigestFromJournalContent(journalContent) + return Identifier.hashToUri(scoreDigest, SCORE_IDENT, SCORE_PREFIX) +} + +export async function fetchAverageScore( + entityUri: string, + scoreType: ScoreType +): Promise { + const pertialEntityUri = entityUri.split('did:cord:').join('') + const decodedScoreEntry = await fetchScore(pertialEntityUri, scoreType) + + const actualRating = ComputeActualRating(decodedScoreEntry.rating) + const averageRating = ComputeAverageRating( + actualRating, + decodedScoreEntry.count + ) + return { + rating: actualRating, + count: decodedScoreEntry.count, + average: averageRating, + } +} diff --git a/packages/modules/src/scoring/index.ts b/packages/modules/src/scoring/index.ts new file mode 100644 index 00000000..00af3458 --- /dev/null +++ b/packages/modules/src/scoring/index.ts @@ -0,0 +1,2 @@ +export * from './Scoring.js' +export * from './Scoring.chain.js' \ No newline at end of file diff --git a/packages/types/src/Score.ts b/packages/types/src/Score.ts index 1ea1fc44..c0368af2 100644 --- a/packages/types/src/Score.ts +++ b/packages/types/src/Score.ts @@ -1,22 +1,33 @@ import { HexString } from '@polkadot/util/types.js' -import type { IPublicIdentity } from './PublicIdentity.js' export const SCORE_IDENTIFIER: number = 101 export const SCORE_PREFIX: string = 'score:cord:' +export const SCORE_MODULUS: number = 10 +export const SCORE_IDENT: number = 11034 export enum ScoreType { - overall = 'overall', - delivery = 'delivery', + overall = 'Overall', + delivery = 'Delivery', +} +export enum EntryType { + credit = 'Credit', + debit = 'Debit', } export interface IJournalContent { - entity: IPublicIdentity['address'] - uid: string + entity: string tid: string - collector: IPublicIdentity['address'] - requestor: IPublicIdentity['address'] - scoreType: ScoreType - score: number + collector: string + rating_type: string + rating: number + entry_type: string + count: number +} + +export interface IRatingInput { + entry: IJournalContent + digest: string + creator: string } export interface IJournal { @@ -40,11 +51,14 @@ export interface IScoreAggregateDetails { score: number } } + export interface IScoreAverageDetails { - entity: IJournalContent['entity'] - scoreType: ScoreType - average: { - count: number - score: number - } + rating: number + count: number + average: number +} + +export interface IScoreDetails { + rating: number + count: number } diff --git a/packages/utils/src/SDKErrors.ts b/packages/utils/src/SDKErrors.ts index 558c20a1..e12ac082 100644 --- a/packages/utils/src/SDKErrors.ts +++ b/packages/utils/src/SDKErrors.ts @@ -210,3 +210,5 @@ export class DecodingMessageError extends SDKError {} export class TimeoutError extends SDKError {} export class CodecMismatchError extends SDKError {} + +export class ScoreMissingError extends SDKError {}