diff --git a/packages/multiversx/typedoc.json b/packages/multiversx/typedoc.json index eeb150a6..41e3f926 100644 --- a/packages/multiversx/typedoc.json +++ b/packages/multiversx/typedoc.json @@ -1,5 +1,5 @@ { - "name": "MultiversX", + "name": "multiversx", "entryPoints": [ "./src/index.ts" ] diff --git a/website/.gitignore b/website/.gitignore index e3e539f3..4a88e6c1 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -8,7 +8,7 @@ .docusaurus .cache-loader -/docs/api +/docs/reference # Misc .DS_Store diff --git a/website/docs/client/_category_.json b/website/docs/client/_category_.json index f69f08cd..f856080a 100644 --- a/website/docs/client/_category_.json +++ b/website/docs/client/_category_.json @@ -1,8 +1,8 @@ { "label": "Client", - "position": 3, + "position": 2, "link": { "type": "generated-index", - "description": "The client to interact with logion network." + "description": "The Logion client" } } diff --git a/website/docs/client/authentication.md b/website/docs/client/authentication.md index 70341550..53b7fd1e 100644 --- a/website/docs/client/authentication.md +++ b/website/docs/client/authentication.md @@ -1,33 +1,26 @@ --- -sidebar_position: 1 +sidebar_position: 2 description: How to authenticate a Polkadot account to logion network. --- # Authentication -## Authenticate with ad-hoc keyring +Below example shows how to use an embedded signer in order to authenticate `client`. This approach is not recommended in production, +a [browser extension](/docs/category/extension) should be used instead. ```typescript -import { LogionClient, KeyringSigner } from '@logion/client'; import { Keyring } from '@polkadot/api'; const keyring = new Keyring(); -keyring.addFromUri("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a"); // Alice +const keypair = keyring.addFromUri("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a"); // Alice const signer = new KeyringSigner(keyring); -const client = await LogionClient.create({ - rpcEndpoints: [ 'wss://rpc01.logion.network' ], // A list of websocket endpoints - directoryEndpoint: 'https://directory.logion.network' // A logion directory -}); - // Authenticate Alice -const authenticatedClient = await client.authenticate([ - "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" ], +let authenticatedClient = await client.authenticate( + [ keypair.address ], signer ); -``` -Now you can use authenticatedClient to interact with the network. -## Authenticate with Polkadot\{.js\} - -In order to connect a webapp to logion-network, it is recommended to use [polkadot\{.js\} app](../extension/polkadot-js.md) extension. +// Later on, refresh the session (session's TTL is 1 hour) +authenticatedClient = await authenticatedClient.refreshTokens(DateTime.now()); +``` diff --git a/website/docs/client/balance-transactions.md b/website/docs/client/balance-transactions.md index 2c0c20e7..b14ccf9a 100644 --- a/website/docs/client/balance-transactions.md +++ b/website/docs/client/balance-transactions.md @@ -1,10 +1,13 @@ --- -sidebar_position: 2 +sidebar_position: 3 description: How to access to the balance and do transactions. --- # Balance and Transactions +The Logion network relies on the LGNT token. This section is about accessing you LGNT balance and transactions. +It also shows how to transfer LGNTs. + ## State :::note diff --git a/website/docs/client/img/identity-loc-state.png b/website/docs/client/img/identity-loc-state.png index 77230fff..b4ff029f 100644 Binary files a/website/docs/client/img/identity-loc-state.png and b/website/docs/client/img/identity-loc-state.png differ diff --git a/website/docs/client/img/identity-loc-state.puml b/website/docs/client/img/identity-loc-state.puml index f3be3703..185d496d 100644 --- a/website/docs/client/img/identity-loc-state.puml +++ b/website/docs/client/img/identity-loc-state.puml @@ -9,8 +9,8 @@ OpenLoc : OPEN RejectedRequest : REVIEW_REJECTED ClosedLoc : CLOSED AcceptedRequest : REVIEW_ACCEPTED -[*] -right-> PendingRequest: requestIdentityLoc\n(draft: false) -[*] --> DraftRequest: requestIdentityLoc\n(draft: true) +[*] -right-> PendingRequest: request___Loc\n(draft: false) +[*] --> DraftRequest: request___Loc\n(draft: true) DraftRequest --> DraftRequest: addMetadata(),\n addFile(),\n deleteMetadata(),\n deleteFile(),\n DraftRequest --> PendingRequest: submit() PendingRequest --> decision diff --git a/website/docs/client/img/protection-state.png b/website/docs/client/img/protection-state.png deleted file mode 100644 index 95e3ffb6..00000000 Binary files a/website/docs/client/img/protection-state.png and /dev/null differ diff --git a/website/docs/client/img/protection-state.puml b/website/docs/client/img/protection-state.puml deleted file mode 100644 index 8af1d132..00000000 --- a/website/docs/client/img/protection-state.puml +++ /dev/null @@ -1,14 +0,0 @@ -@startuml -hide empty description -[*] --> NoProtection -NoProtection --> PendingProtection : requestProtection() -state "Decision" as decision <> -PendingProtection --> decision -decision --> RejectedProtection: (at least) one LO rejects -decision --> AcceptedProtection: both LO's accept -AcceptedProtection --> ActiveProtection: activate() -RejectedProtection --> NoProtection: cancel() -RejectedProtection --> PendingProtection: resubmit() -RejectedProtection --> PendingProtection: changeLegalOfficer() -ActiveProtection --> [*] -@enduml diff --git a/website/docs/client/introduction.md b/website/docs/client/introduction.md new file mode 100644 index 00000000..26afbf51 --- /dev/null +++ b/website/docs/client/introduction.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 1 +description: The basics about the Logion client. +--- + +# Introduction + +Each Logion node runs + +* a Substrate service for block production, +* IPFS/IPFS cluster services running a private IPFS network, +* a private database service, +* a logion off-chain service exposing a REST API. + +In addition to the Logion nodes, a Logion network also includes + +* RPC nodes for accessing the chain, +* a directory service exposing the identity data of the Logion Legal Officer. + +Logion's client interacts with the Logion chain (through RPC nodes), the REST API exposed by each node of the network and the directory. +The other services are publicly accessible. + +The client's purpose is to merge data coming from the chain and the nodes +and expose a unified view. In order to access private data, the client has to be authenticated (see [here](./authentication.md)). + +The client is able to run in several environments: + +- a browser, +- a Node.JS application, +- a React Native based mobile app. + +In function of the environment, the client will be instantiated differently (see below). + +Another key concept is data caching. The client needs to interact with many data sources over the network. In order to +optimize the use of bandwidth, data are cached and retrieved only when needed. The client relies on "states" to handle +the different caches. A state is an immutable object, when the developer calls a mutating method on the state, a new +state instance is returned and the previous state is discarded. + +Examples of how to work with states are given further in the documentation, see for instance [here](../client/loc.md). + +There are several Logion networks: + +- DEV: the network used for testing new developments, very unstable, should not be used unless part of the Logion developers team. +- TEST: a stable sandbox network that can be leveraged by developers to test their own developments using the Logion platform. +- MVP: Logion's production environment. Be sure to test your developments first in TEST before interacting with MVP. + +The network to connect to is chosen at client instantiation, see below for examples. + +## Instantiate the client in the browser + +```typescript +import { Environment } from "@logion/client"; +import { newLogionClient } from '@logion/client-browser'; + +const client = await newLogionClient(Environment.TEST); +``` + +## Instantiate the client in a Node.JS application + +```typescript +import { Environment } from "@logion/client"; +import { newLogionClient } from '@logion/client-node'; + +const client = await newLogionClient(Environment.TEST); +``` + +## Instantiate the client in a React Native mobile app + +The SDK currently supports 2 "frameworks": + +- [Expo](https://expo.dev/) +- [React Native FS](https://github.com/itinance/react-native-fs) + +For Expo: + +```typescript +import { Environment } from "@logion/client"; +import { newLogionClient } from '@logion/client-expo'; + +const client = await newLogionClient(Environment.TEST); +``` + +For React Native FS: + +```typescript +import { Environment } from "@logion/client"; +import { newLogionClient } from '@logion/client-react-native-fs'; + +const client = await newLogionClient(Environment.TEST); +``` + +Make sure to check the README of both packages for further instructions about how to +integrate the Logion SDK in those environments. diff --git a/website/docs/client/loc.md b/website/docs/client/loc.md index 5b74dfd7..b7c011a3 100644 --- a/website/docs/client/loc.md +++ b/website/docs/client/loc.md @@ -1,10 +1,16 @@ --- sidebar_position: 4 -description: How to request and access to LOC. +description: How to request and access to a LOC. --- # Legal Officer Case (LOC) +A LOC is an encrypted and decentralized digital folder containing public and private data. +Its content is reviewed and signed by a Logion Legal Officer (LLO), +an individual operating under a strict legal framework. + +This section is about how to manager your LOCs. + ## State :::note @@ -23,103 +29,232 @@ Always use the most recent state, and discard the former state. In the example above, the var `locsState` must not be used any more as soon as `refreshedState` is available. ::: -## Transaction LOC +## Lifecycle -### Lifecycle +In below diagram, replace `___` by a LOC type. The process is the same for all LOC types. -![Loc State](img/loc-state.png) +![Identity Loc State](img/identity-loc-state.png) + +## Identity LOC ### Request +An Identity LOC is requested this way: + ```typescript -const draftRequest = await locsState.requestTransactionLoc({ +const draftRequest = await locsState.requestIdentityLoc({ legalOfficer: alice, - description: "This is a Transaction LOC", + description: "This is an Identity LOC", + userIdentity: { + email: "john.doe@invalid.domain", + firstName: "John", + lastName: "Doe", + phoneNumber: "+1234", + }, + userPostalAddress: { + line1: "Peace Street", + line2: "2nd floor", + postalCode: "10000", + city: "MyCity", + country: "Wonderland" + }, draft: true, + legalFee: Lgnt.from(160), +}); +``` + +:::warning +The default legal fee for an identity +LOC is 160 LGNTs. Another value of `legalFee` should have been discussed with the LLO beforehand, +otherwise it may reject the LOC. +::: + +### Add items + +You may add metadata (i.e. public name-value pairs), private files and links (to other LOCs) to your LOC. + +Metadata can be added to, or removed from, a draft LOC: + +```typescript title="addMetadata" +draftRequest = await draftRequest.addMetadata({ + name: "Some name", + value: "Some value" +}); +``` + +```typescript title="deleteMetadata" +draftRequest = await draftRequest.deleteMetadata({ + name: "Some name" }); ``` ### Submit -At this stage it's already possible to add [metadata](#metadata) or files, to be published later by the LO. -When done, the request has to be submitted to the LO: + +When done, the request must be submitted to the LLO for review: ```typescript const pendingRequest = await draftRequest.submit(); ``` -Then you have to wait for the LO to open the LOC: +### After review + +If the LLO accepted your LOC, you can proceed and publish it: ```typescript -const openLoc = await pendingRequest.refresh() as OpenLoc; +const acceptedRequest = await pendingRequest.refresh() as AcceptedRequest; +const openLoc = await acceptedRequest.open({ signer, autoPublish: true }); ``` -### Metadata +If you set `autoPublish` to false, you'll have to manually publish each item +manually after the call to open. -Metadata can be added to, or removed from, an open LOC: +After everything has been published, just wait for the LLO to close the LOC: -```typescript title="addMetadata" -openLoc = await openLoc.addMetadata({ - name: "Some name", - value: "Some value" -}); +```typescript +const closedLoc = await openLoc.refresh() as ClosedLoc; ``` -```typescript title="deleteMetadata" -openLoc = await openLoc.deleteMetadata({ - name: "Some name" +## Transaction LOC + +### Request + +```typescript +const draftRequest = await locsState.requestTransactionLoc({ + legalOfficer: alice, + description: "This is a Transaction LOC", + draft: true, + legalFee: Lgnt.from(2000), }); ``` -## Collection LOC - -### Lifecycle +:::warning +The default legal fee for a transaction +LOC is 2000 LGNTs. Another value of `legalFee` should have been discussed with the LLO beforehand, +otherwise the LLO may reject the LOC. +::: -![Collection Loc State](img/collection-state.png) +## Collection LOC ### Request -A Collection LOC can be requested to a given Legal Officer by providing a description. +When requesting a Collection LOC, additional parameters have to be provided. + +Note that at least one of `lastBlockSubmission` and `maxSize` have to be defined. ```typescript const draftRequest = await locsState.requestCollectionLoc({ legalOfficer: alice, description: "This is a Collection LOC", - draft: true + draft: true, + legalFee: Lgnt.from(2000), + valueFee: Lgnt.zero(), + collectionItemFee: Lgnt.zero(), + tokensRecordFee: Lgnt.zero(), + collectionParams: { + lastBlockSubmission: 100000n, + maxSize: 9999, + canUpload: true, + } }); ``` -### Submit -At this stage it's already possible to add [metadata](#metadata) or files, to be published later by the LO. -When done, the request has to be submitted to the LO: +:::warning +The default legal fee for a collection LOC is 2000 LGNTs. Another value +of `legalFee` should be discussed with the LLO beforehands, +this also applies to `valueFee`, `collectionItemFee`, `tokensRecordFee` and the collection +parameters. Otherwise the LLO may reject the LOC. +::: + +## Direct LOC opening + +In order to bypass the review process, one can directly create and publish a LOC and its +items in a single call and signature: ```typescript -const pendingRequest = await draftRequest.submit(); +const openLoc = await locsState.openIdentityLoc({ + description: "This is an Identity LOC", + legalOfficerAddress: alice.address, + files, + metadata, + links, + userIdentity: { + email: "john.doe@invalid.domain", + firstName: "John", + lastName: "Doe", + phoneNumber: "+1234", + }, + userPostalAddress: { + line1: "Peace Street", + line2: "2nd floor", + postalCode: "10000", + city: "MyCity", + country: "Wonderland" + }, + signer +}); ``` -Then you have to wait for the LO to open and close the LOC: +Methods `openTransactionLoc` and `openCollectionLoc` can also be used. `openCollection` will +require the additional fields (see `requestCollectionLoc`). + +:::danger +You have to make sure that the LLO agrees to close the LOCs directly opened this way. +If he doesn't, he may never close or even void your LOC. In that case, you +will "lose" (i.e. there is no refund) the LGNTs paid so far for the creation of the LOC. +::: + +## Queries + +### Querying requests +Pending and rejected requests can be queried: ```typescript -const openLoc = await pendingRequest.refresh() as OpenLoc; -const closedLoc = await openLoc.refresh() as ClosedCollectionLoc; +const type: LocType = 'Transaction'; // could be 'Collection' or 'Identity' +const pendingRequests = locsState.pendingRequests[type]; +const rejectedRequests = locsState.rejectedRequests[type]; ``` -### Collection Item +### Querying LOCs +Similarly, LOC's can be queried according to their state: -When opening the Collection LOC, the LO decides if it supports file upload or not. +```typescript +const type: LocType = 'Transaction'; // could be 'Collection' or 'Identity' +const openLocs = locsState.openLocs[type]; +const closedLocs = locsState.closedLocs[type]; +const voidedLocs = locsState.voidedLocs[type]; +``` -#### Collection WITHOUT upload support +### Accessing a LOC's data + +```typescript +const locData: LocData = locsState.openLocs["Identity"][0].data(); +const userIdentity = locData.userIdentity; // Only set on identity LOCs +console.log("Identity of %s: %s %s %s %s", + locData.requesterAddress, + userIdentity?.firstName, + userIdentity?.lastName, + userIdentity?.email, + userIdentity?.phoneNumber +); +``` + +## Collection Items + +When creating a Collection LOC, the collection parameters tell if it supports file upload or not. + +### Collection WITHOUT upload support Add an item to the collection: ```typescript title="Add Item" -const itemId = "0xc53447c3d4e9d94d6f4ab926378c5b14bd66e28af619d4dcb066c862f8aeb455"; // SHA256 hash of "first-collection-item" (without the quotes) +const itemId = Hash.of("first-collection-item"); const itemDescription = "First collection item"; -const itemFileContent = "test"; -const itemFileHash = hash(itemFileContent); closedLoc = await closedLoc.addCollectionItem({ - itemId, - itemDescription, - signer: state.signer + payload: { + itemId, + itemDescription, + }, + signer, }); ``` @@ -129,7 +264,7 @@ Later on, you can retrieve the item with its ID: const item = await closedLoc.getCollectionItem({ itemId }); ``` -#### Collection WITH upload support +### Collection WITH upload support A collection item may have attached files if the collection permits it. The files are then stored in logion's private IPFS network ensuring their availability over time. If a controlled delivery for attached files is needed, see "Collection with restricted delivery" @@ -142,93 +277,87 @@ There are 2 possibilities when attaching files to an item: See the examples below. ```typescript title="Add Item and provide file content" -const firstItemId = hashString("first-collection-item"); -const firstItemDescription = "First collection item"; -const firstFileContent = "test"; -const firstFileHash = hashString(firstFileContent); +const itemId = Hash.of("first-collection-item"); +const itemDescription = "First collection item"; closedLoc = await closedLoc.addCollectionItem({ - itemId: firstItemId, - itemDescription: firstItemDescription, - signer: state.signer, - itemFiles: [ - new ItemFileWithContent({ - name: "test.txt", - contentType: MimeType.from("text/plain"), - hashOrContent: HashOrContent.fromContent(Buffer.from(firstFileContent)), // Let SDK compute hash and size - }) - ], + payload: { + itemId, + itemDescription, + itemFiles: [ + HashOrContent.fromContent( + new NodeFile("integration/test.txt", "test.txt", MimeType.from("text/plain")) + ), // Let SDK compute hash and size + ], + }, + signer, }); ``` ```typescript title="Add Item and provide hash and size" -const secondItemId = hashString("second-collection-item"); -const secondItemDescription = "Second collection item"; -const secondFileContent = "test2"; -const secondFileHash = hashString(secondFileContent); +const itemId = Hash.of("first-collection-item"); +const itemDescription = "First collection item"; +const file = HashOrContent.fromContentFinalized( + new NodeFile("integration/test.txt", "test.txt", MimeType.from("text/plain")) +); closedLoc = await closedLoc.addCollectionItem({ - itemId: secondItemId, - itemDescription: secondItemDescription, - signer: state.signer, - itemFiles: [ - new ItemFileWithContent({ - name: "test2.txt", - contentType: MimeType.from("text/plain"), - hashOrContent: HashOrContent.fromHash(secondFileHash), // No content, must upload later - size: 5n, // No content, must provide size - }) - ] + payload: { + itemId, + itemDescription, + itemFiles: [ + HashOrContent.fromDescription({ + name: file.name, + hash: file.contentHash, + size: file.size, + mimeType: file.mimeType, + }), + ] + }, + signer, }); ``` ```typescript title="Upload file for an existing item" closedLoc = await closedLoc.uploadCollectionItemFile({ - itemId: secondItemId, - itemFile: new ItemFileWithContent({ - name: "test2.txt", - contentType: MimeType.from("text/plain"), - hashOrContent: HashOrContent.fromContent(Buffer.from(firstFileContent)), - }), + itemId, + itemFile: file, }); ``` -#### Collection with restricted delivery +### Collection with restricted delivery A collection item with restricted delivery requires a token definition i.e. the "address" of the token which opens access to the underlying files when owned. Below an example where the underlying files will be delivered to the owner of an ERC-721 token on Ethereum Mainnet. ```typescript title="Add Item with restricted delivery" -const firstItemId = generateEthereumTokenItemId("202210131750", "4391"); -const firstItemDescription = "First collection item"; -const firstFileContent = "test"; -const firstFileHash = hashString(firstFileContent); -const firstItemToken: ItemTokenWithRestrictedType = { +const itemId = generateEthereumTokenItemId("202210131750", "4391"); +const itemDescription = "First collection item"; +const itemToken: ItemTokenWithRestrictedType = { type: "ethereum_erc721", id: '{"contract":"0x765df6da33c1ec1f83be42db171d7ee334a46df5","id":"4391"}' }; +const file = HashOrContent.fromContent( + new NodeFile("integration/test.txt", "test.txt", MimeType.from("text/plain")) +); closedLoc = await closedLoc.addCollectionItem({ - itemId: firstItemId, - itemDescription: firstItemDescription, - signer: state.signer, - itemFiles: [ - new ItemFileWithContent({ - name: "test.txt", - contentType: MimeType.from("text/plain"), - hashOrContent: HashOrContent.fromContent(Buffer.from(firstFileContent)), - }) - ], - itemToken: firstItemToken, - restrictedDelivery: true, + payload: { + itemId: firstItemId, + itemDescription: firstItemDescription, + itemFiles: [ file ], + itemToken, + restrictedDelivery: true, + }, + signer, }); ``` :::danger In the above example, the item ID is generated using function `generateEthereumTokenItemId`. This ensures that the item ID -matches the one computed by the [logion Smart Contract](https://github.com/logion-network/logion-solidity/blob/main/contracts/Logion.sol). +matches the one computed by the [Logion Smart Contract](https://github.com/logion-network/logion-solidity/blob/main/contracts/Logion.sol). **This is very important because otherwise, the bidirectional link between the item and its token would be broken. The nonce parameter must match the one in the Smart Contract.** -One may consider not using the logion Smart Contract, leaving the choice of the item ID completely open, but this is not recommanded. +One may consider not using the Logion Smart Contract, leaving the choice of the item ID completely open, but this is not recommended. ::: This is the list of supported token types: @@ -236,119 +365,123 @@ This is the list of supported token types: - `ethereum_erc1155` - `goerli_erc721` - `goerli_erc1155` - -With the above types, the `id` field must represent a valid JSON object with 2 fields: `contract` and `id`. Both fields are strings. - -`contract` is the address of the Smart Contract of the token. - -`id` is the token ID as assigned by the Smart Contract. - -#### Terms and Conditions - -Terms and conditions can be added to the collection item, using either the `LogionClassification`, +- `singular_kusama` +- `polygon_erc721` +- `polygon_erc1155` +- `polygon_mumbai_erc721` +- `polygon_mumbai_erc1155` +- `ethereum_erc20` +- `goerli_erc20` +- `polygon_erc20` +- `polygon_mumbai_erc20` +- `multiversx_devnet_esdt` +- `multiversx_testnet_esdt` +- `multiversx_esdt` +- `astar_psp34` +- `astar_shiden_psp34` +- `astar_shibuya_psp34` + +The format of `id` field depends on the token type: + +- With types `astar_*_psp34`, `*_erc721` and `*_erc1155`, the `id` field must be a valid JSON object with 2 fields: `contract` and `id`. Both fields are strings: + - `contract` is the address of the Smart Contract of the token. + - `id` is the token ID as assigned by the Smart Contract. +- With types `*_erc20`, the `id` field must be a valid JSON object with a single `contract` fields: + - `contract` is the address of the Smart Contract of the token. +- With `multiversx_*` types, `id` is a string representing the ID of an ESDT token. + +### Terms and Conditions + +Terms and conditions can be added to the collection item, using either the Logion classification, a set of `SpecificLicense`s, or both. -As a prerequisite, a valid closed Transaction LOC defining the classification or license must exist. -(referred as `logionClassification.locId` and `specificLicense.locId` in the example below). +If using a specific license, a valid closed Transaction LOC defining the license must exist +(referred as `specificLicenseLocId` in the example below). ```typescript -const itemId = "0xc53447c3d4e9d94d6f4ab926378c5b14bd66e28af619d4dcb066c862f8aeb455"; // SHA256 hash of "first-collection-item" (without the quotes) +const itemId = Hash.of("first-collection-item"); const itemDescription = "First collection item"; -const itemFileContent = "test"; -const itemFileHash = hash(itemFileContent); +const file = HashOrContent.fromContent( + new NodeFile("integration/test.txt", "test.txt", MimeType.from("text/plain")) +); closedLoc = await closedLoc.addCollectionItem({ - itemId, - itemDescription, - signer: state.signer, - logionClassification: new LogionClassification(logionClassificationLocRequest.locId, { - regionalLimit: [ "BE", "FR", "LU" ], - transferredRights: [ "PER-PRIV", "REG", "TIME" ], - expiration: "2025-01-01", - }), - specificLicenses: [ new SpecificLicense(specificLicense.locId, "Some details about the license") ] + payload: { + itemId, + itemDescription, + itemFiles: [ file ], + logionClassification: { + regionalLimit: [ "BE", "FR", "LU" ], + transferredRights: [ "PER-PRIV", "REG", "TIME" ], + expiration: "2025-01-01", + }, + specificLicenses: [ + new SpecificLicense(specificLicenseLocId, "Some details about the license"), + ] + }, + signer, }); ``` :::note Logion Classification * The Logion Classification allows to define a set of `transferredRights` to define precisely the scope of the terms and conditions. -All possible transferred rights are available in `logionLicenseItems`. +All possible transferred rights are available in the `logionLicenseItems` array. * With the code `TIME` it is possible to limit the right in time by setting the parameter `expiration`. * With the code `REG` it is possible to limit to some countries/regions with the parameter `regionalLimit`. ::: -## Identity LOC +## Tokens Records -Except for the request, described here, everything else is similar to the [Transaction LOC](#transaction-loc). +A tokens record has attached files. The files are then stored in Logion's private IPFS network +ensuring their availability over time. -### Lifecycle +As for collection items, there are 2 possibilities when attaching files: +- immediate upload of the files on creation or +- creation followed by an explicit upload later on. -![Identity Loc State](img/identity-loc-state.png) - -### Request - -An Identity LOC is requested this way: +See the examples below. -```typescript -const draftRequest = await locsState.requestIdentityLoc({ - legalOfficer: alice, - description: "This is an Identity LOC", - userIdentity: { - email: "john.doe@invalid.domain", - firstName: "John", - lastName: "Doe", - phoneNumber: "+1234", - }, - userPostalAddress: { - line1: "Peace Street", - line2: "2nd floor", - postalCode: "10000", - city: "MyCity", - country: "Wonderland" +```typescript title="Add record and provide file content" +const recordId = Hash.of("record-id"); +const recordDescription = "Some tokens record"; +const file = HashOrContent.fromContent( + new NodeFile("integration/test.txt", "report.txt", MimeType.from("text/plain")) +); +closedLoc = await closedLoc.addTokensRecord({ + payload: { + recordId, + description: recordDescription, + files: [ file ], }, - draft: true + signer, }); ``` -### Submit -At this stage it's already possible to add [metadata](#metadata) or files, to be published later by the LO. -When done, the request has to be submitted to the LO: - -```typescript -const pendingRequest = await draftRequest.submit(); -``` - -## Query - -### Querying requests -Pending and rejected requests can be queried: - -```typescript -const type: LocType = 'Transaction'; // could be 'Collection' or 'Identity' -const pendingRequests = locsState.pendingRequests[type]; -const rejectedRequests = locsState.rejectedRequests[type]; -``` - -### Querying LOCs -Similarly, LOC's can be queried according to their state: - -```typescript -const type: LocType = 'Transaction'; // could be 'Collection' or 'Identity' -const openLocs = locsState.openLocs[type]; -const closedLocs = locsState.closedLocs[type]; -const voidedLocs = locsState.voidedLocs[type]; -``` - -## Accessing LOC data - -```typescript -const locData: LocData = locsState.openLocs["Identity"][0].data(); -const userIdentity = locData.userIdentity; -console.log("Identity of %s: %s %s %s %s", - locData.requesterAddress, - userIdentity?.firstName, - userIdentity?.lastName, - userIdentity?.email, - userIdentity?.phoneNumber +```typescript title="Add record and provide description only" +const recordId = Hash.of("record-id"); +const recordDescription = "Some tokens record"; +const file = HashOrContent.fromContentFinalized( + new NodeFile("integration/test.txt", "report.txt", MimeType.from("text/plain")) ); +closedLoc = await closedLoc.addTokensRecord({ + payload: { + recordId, + description: recordDescription, + files: [ + HashOrContent.fromDescription({ + name: file.name, + hash: file.contentHash, + size: file.size, + mimeType: file.mimeType, + }), + ], + }, + signer, +}); ``` +```typescript title="Upload file for an existing record" +closedLoc = await closedLoc.uploadTokensRecordFile({ + recordId, + file, +}); +``` diff --git a/website/docs/client/protection.md b/website/docs/client/protection.md index 195a35ae..f32fd198 100644 --- a/website/docs/client/protection.md +++ b/website/docs/client/protection.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 5 description: How to protect/recover a Polkadot account, and access the vault. --- @@ -18,99 +18,54 @@ const refreshedState = await protectionState.refresh(); ``` :::caution -All user operations (`requestProtection`, `activate`, `cancel`, etc.), as well as `refresh`, do return a new state. +All user operations (`activateProtection`), as well as `refresh`, do return a new state. Always use the most recent state, and discard the former state. In the example above, the var `protectionState` must not be used any more as soon as `refreshedState` is available. ::: ## Protection -The setup of a Polkadot account protection is a 3-steps process: +The setup of a Polkadot account protection is a 2-steps process: * **Choose 2 officers** among the official list of Logion Legal Officers. -* **Request** the protection towards those 2 LO's. -* Upon acceptance, **activate** the protection on the chain. +* **activate** the protection on-chain given a valid (i.e. closed and non-void) Identity LOC with each LLO. -### Overview of the whole process - -![Protection State Diagram](img/protection-state.png) +### Choose LLOs and LOCs -### Choose legal officers - -The following call accesses the configured [logion-directory](https://github.com/logion-network/logion-directory) -to retrieve the list of available Legal Officers. A protection requires the user to choose 2 distinct Legal Officers -(in this sample Alice and Bob are selected). +For below example to work, you will need a valid Identity LOC with both alice and bob. ```typescript -const legalOfficers: LegalOfficer[] = authenticatedClient.getLegalOfficers(); +const legalOfficers = authenticatedClient.getLegalOfficers(); +const locsState = await authenticatedClient.locsState(); -const alice = legalOfficers[0]; -const bob = legalOfficers[1]; +const alice = authenticatedClient.getLegalOfficer("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"); +const bob = authenticatedClient.getLegalOfficer("5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"); +const aliceLoc = locsState.closedLocs.Identity.find( + loc => loc.data().ownerAddress === alice.address + && loc.data().voidInfo === undefined +); +const bobLoc = locsState.closedLocs.Identity.find( + loc => loc.data().ownerAddress === bob.address + && loc.data().voidInfo === undefined +); ``` -### Request a protection - -You can submit your protection requests to the selected officers: +### Activate your protection ```typescript const noProtection = await authenticatedClient.protectionState() as NoProtection; -const pending = await noProtection.requestProtection({ - legalOfficer1: alice, - legalOfficer2: bob, - userIdentity: { - email: "john.doe@invalid.domain", - firstName: "John", - lastName: "Doe", - phoneNumber: "+1234", +const pending = await noProtection.activateProtection({ + payload: { + legalOfficer1: alice, + legalOfficer2: bob, + requesterIdentityLoc1: aliceLoc, + requesterIdentityLoc2: bobLoc, }, - postalAddress: { - city: "", - country: "", - line1: "", - line2: "", - postalCode: "", - } + signer, }); ``` -### Activate the protection - -You must first wait for both Legal Officers acceptance, and then activate the protection: - -```typescript -const accepted = (await pending.refresh()) as AcceptedProtection; - -accepted.activate(signer); -``` - -In case of refusal of one or both Legal Officers, the refreshed state will be `RejectedProtection`. - -### Rejection Management - -If rejected by one or more Legal Officer, you may either -* Resubmit the request to the officer who rejected, -* Or replace him/her with a new Legal Officer, -* Or completely cancel your protection request. - -```typescript title="Resubmit to LO who rejected" -let rejectedProtection = (await authenticatedClient.protectionState()) as RejectedProtection; -const rejecter = rejectedProtection.protectionParameters.states.find(state => state.status === "REJECTED")!.legalOfficer; -rejectedProtection.resubmit(rejecter); -``` - -```typescript title="Replace LO who rejected with Charlie" -let rejectedProtection = (await authenticatedClient.protectionState()) as RejectedProtection; -const rejecter = rejectedProtection.protectionParameters.states.find(state => state.status === "REJECTED")!.legalOfficer; -const charlie: LegalOfficer = ...; -rejectedProtection.changeLegalOfficer(rejecter, charlie); -``` - -```typescript title="Cancel the protection request" -let rejectedProtection = (await authenticatedClient.protectionState()) as RejectedProtection; -rejectedProtection.cancel(); -``` - ## Vault -Operations require an activated protection (see above [Protection Request](#protection)) +Operations require an activated protection (see above). ### Transfer from vault @@ -162,34 +117,17 @@ One claimed, you can recover the lost assets. Recovery must be requested to the **same Legal Officers** who accepted to protect the lost account (in this case, Alice and Bob). ```typescript -const NEW_ADDRESS = "5GsxAu1XexDATCbDJbWxKSow4gdC6epkajZr7Ht8Ci9VZabV"; - -const authenticatedClient = await LogionClient.create({ - rpcEndpoints: [ 'wss://rpc01.logion.network' ], // A list of websocket endpoints - directoryEndpoint: 'https://directory.logion.network' // A logion directory -}).authenticate([ NEW_ADDRESS ], signer); - const noProtection = await authenticatedClient.protectionState() as NoProtection; const pending = await noProtection.requestRecovery({ - recoveredAddress: REQUESTER_ADDRESS, - signer, - legalOfficer1: alice, - legalOfficer2: bob, - userIdentity: { - email: "john.doe@invalid.domain", - firstName: "John", - lastName: "Doe", - phoneNumber: "+1234", + payload: { + recoveredAddress: REQUESTER_ADDRESS, + legalOfficer1: alice, + legalOfficer2: bob, + requesterIdentityLoc1: aliceLoc, + requesterIdentityLoc2: bobLoc, }, - postalAddress: { - city: "", - country: "", - line1: "", - line2: "", - postalCode: "", - } + signer, }); - ``` ### Activate the new protection diff --git a/website/docs/extension/_category_.json b/website/docs/extension/_category_.json index 69e428bd..05f00ab1 100644 --- a/website/docs/extension/_category_.json +++ b/website/docs/extension/_category_.json @@ -1,8 +1,8 @@ { "label": "Extension", - "position": 2, + "position": 3, "link": { "type": "generated-index", - "description": "Provides some utility classes enabling the use a browser extension with." + "description": "How to use browser extensions with the Logion client." } } diff --git a/website/docs/extension/metamask.md b/website/docs/extension/metamask.md index 377d1bf0..c7e34e67 100644 --- a/website/docs/extension/metamask.md +++ b/website/docs/extension/metamask.md @@ -2,36 +2,30 @@ sidebar_position: 2 --- -# metamask +# MetaMask ![metamask](/img/metamask.png) -Allows to sign a message with an ethereum account using [MetaMask](https://metamask.io/) browser extension (for [chrome](https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn) or [firefox](https://addons.mozilla.org/firefox/addon/ether-metamask/))). +Allows to sign a message with an ethereum account using [MetaMask](https://metamask.io/) browser extension (for [chrome](https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn) or [firefox](https://addons.mozilla.org/firefox/addon/ether-metamask/)). +:::warning +In order to authenticate, `eth_sign` requests must be enabled in MetaMask in advanced settings. +This is considered a dangerous feature as it enables an attacker to make you sign almost anything. +::: ## List all available accounts ```typescript import { enableMetaMask, allMetamaskAccounts } from "@logion/extension"; -const enabled = await enableMetaMask("MyLogionWebApp") - -const accounts = await allMetamaskAccounts(); -accounts.forEach(account => console.log("Detected MetaMask accounts: %s", account.address)); -``` - -## Authenticate the selected account -The selected account can be [authenticated](/docs/client/authentication.md): - -```typescript -const authenticatedClient = await client.authenticate([ accounts[0].address ], signer); -``` - -## Use JWT token - -Once authenticated, the returned JWT token can be used (for instance to claim an asset linked to your ethereum account): - -```typescript -const jwtToken = authenticatedClient.tokens.get(address); +if(await enableMetaMask("MyLogionWebApp")) { + const accounts = await allMetamaskAccounts(); + const authenticated = await client.authenticate(addresses, signer); + ... +} ``` +:::info +If several accounts are available, the call to `authenticate` +will require a signature for each one of them. +::: diff --git a/website/docs/extension/polkadot-js.md b/website/docs/extension/polkadot-js.md index 8237c899..374ddd15 100644 --- a/website/docs/extension/polkadot-js.md +++ b/website/docs/extension/polkadot-js.md @@ -6,29 +6,30 @@ sidebar_position: 1 ![polkadot\{.js\} extension](/img/polkadot.png) -This project provides some utility classes enabling the use the +This project provides some utility classes enabling the use of the [Polkadot JS extension](https://github.com/polkadot-js/extension#readme) -with a [logion client](/docs/category/client). +or other compatible extensions (e.g. [SubWallet](https://www.subwallet.app/)) +with the [Logion client](/docs/category/client). ## Usage Start signing content using the extension. ```typescript -import { enableExtensions, ExtensionSigner } from '@logion/extension'; +import { getAccounts, ExtensionSigner } from '@logion/extension'; -const client = await LogionClient.create({ - rpcEndpoints: [ 'wss://rpc01.logion.network' ], // A list of websocket endpoints - directoryEndpoint: 'https://directory.logion.network' // A logion directory -}); - -const register = await enableExtensions("Your app name"); const signer = new ExtensionSigner(); -register(async (accounts: InjectedAccount[]) => { - const addresses = accounts.map(account => account.address); - const authenticated = await client.authenticate(addresses, signer); // This will enable authentication by signing with the extension -}); +const injectedAccounts = await getAccounts( + "Your app name", + [ "polkadot-js", "subwallet-js" ], // A list of extensions +); +const addresses = injectedAccounts.map(account => account.address); +const authenticated = await client.authenticate(addresses, signer); + +// Pass `signer` as an argument when needed ``` -You can now use authenticated to use logion features. - +:::info +If several accounts are available in the listed extensions, the call to `authenticate` +will require a signature for each one of them. +::: diff --git a/website/docs/intro.md b/website/docs/intro.md index aa5dbba5..969da0bd 100644 --- a/website/docs/intro.md +++ b/website/docs/intro.md @@ -4,23 +4,12 @@ sidebar_position: 1 # Introduction -## Install +The Logion SDK enables the creation of Typescript applications that interact with a Logion network. -Install package [`@logion/extension`](https://www.npmjs.com/package/@logion/extension) with your favorite package manager (recommended). -Note that the extension has [`@logion/client`](https://www.npmjs.com/package/@logion/client) as a dependency. +In order to interact with a Logion network, a [client should be instantiated](./client/introduction.md). +The access to private data and transaction signature require a signer, either +[embedded](./client/authentication.md) or using a [browser extension](/docs/category/extension). -Alternatively if you don't want to use a Polkadot\{.js\} and provide your own keyring instead, you can directly use -[`@logion/client`](https://www.npmjs.com/package/@logion/client) - - -## Architecture - -The logion-sdk allows you to create a Typescript application that interacts with the logion network. -Each logion node runs - -* a Substrate service, -* IPFS/IPFS cluster services, -* a private database service, -* a logion off-chain service. - -![Architecture](/img/architecture.png) +For a more business-oriented description of Logion, please refer to our [white paper](https://docs.logion.network/logion-white-paper). +In particular, [this page](https://docs.logion.network/logion-white-paper/logion-in-a-nutshell) summarizes +Logion's process. diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index e76542cb..a9650d36 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -4,7 +4,7 @@ const darkTheme = themes.dracula; /** @type {import('@docusaurus/types').Config} */ const config = { - title: 'logion SDK', + title: 'Logion SDK', tagline: 'The blockchain infrastructure of safe digital ownership', url: 'https://logion-network.github.io/', baseUrl: '/logion-api/', @@ -32,6 +32,20 @@ const config = { /** @type {import('@docusaurus/preset-classic').Options} */ ({ docs: { + sidebarItemsGenerator: async function({defaultSidebarItemsGenerator, ...args}) { + const sidebarItems = await defaultSidebarItemsGenerator(args); + const reference = sidebarItems.find(item => item.label === "Reference"); + if(reference) { + const packagesIndex = reference.items.findIndex(item => item.type === "doc" && item.id === "reference/modules"); + if(packagesIndex !== -1) { + const packages = reference.items[packagesIndex]; + packages.label = "Packages"; + reference.items.splice(packagesIndex, 1); + reference.items = [ packages ].concat(reference.items); + } + } + return sidebarItems; + }, sidebarPath: require.resolve('./sidebars.js'), editUrl: 'https://github.com/logion-network/logion-api/tree/main/website/', @@ -52,7 +66,7 @@ const config = { /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ navbar: { - title: 'logion SDK', + title: 'Logion SDK', logo: { alt: 'logion Logo', src: 'img/logion218-twitter.png', @@ -75,9 +89,9 @@ const config = { label: 'Client', }, { - to: '/docs/api', + to: '/docs/reference/modules', position: 'left', - label: 'API', + label: 'Reference', }, { href: 'https://github.com/logion-network/logion-api', diff --git a/website/package.json b/website/package.json index 56a1ce10..cb177b62 100644 --- a/website/package.json +++ b/website/package.json @@ -13,12 +13,12 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", - "api-doc": "typedoc", + "api-doc": "typedoc && cp reference_category_.json docs/reference/_category_.json", "clean": "rm -rf build" }, "dependencies": { - "@docusaurus/core": "^3.1.0", - "@docusaurus/preset-classic": "^3.1.0", + "@docusaurus/core": "^3.1.1", + "@docusaurus/preset-classic": "^3.1.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.1.0", "prism-react-renderer": "^2.3.1", @@ -26,9 +26,9 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "^3.1.0", - "@docusaurus/tsconfig": "^3.1.0", - "typedoc": "^0.25.7", + "@docusaurus/module-type-aliases": "^3.1.1", + "@docusaurus/tsconfig": "^3.1.1", + "typedoc": "^0.25.12", "typedoc-plugin-markdown": "^3.17.1", "typescript": "^5.2.2" }, diff --git a/website/reference_category_.json b/website/reference_category_.json new file mode 100644 index 00000000..a9f870af --- /dev/null +++ b/website/reference_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Reference", + "position": 4, + "link": { + "type": "generated-index", + "description": "Reference documentation of Logion SDK's packages." + } + } + \ No newline at end of file diff --git a/website/src/API.md b/website/src/API.md deleted file mode 100644 index 87409a8b..00000000 --- a/website/src/API.md +++ /dev/null @@ -1,14 +0,0 @@ -# API - -## Table of contents - -### Modules - -- [Client](modules/client.md) -- [Client (Browser)](modules/client_browser.md) -- [Client (Node.js)](modules/client_node.md) -- [Client (React Native FS)](modules/client_react_native_fs.md) -- [Crossmint](modules/crossmint.md) -- [Extension](modules/extension.md) -- [MultiversX](modules/MultiversX.md) -- [Node API](modules/node_api.md) diff --git a/website/src/modules.md b/website/src/modules.md new file mode 100644 index 00000000..d247add6 --- /dev/null +++ b/website/src/modules.md @@ -0,0 +1,16 @@ +# Reference + +All modules listed below are available as packages in the `@logion` scope +(i.e. module `client` has package `@logion/client`). + +## Modules + +- [client](modules/client.md) +- [client-browser](modules/client_browser.md) +- [client-node](modules/client_node.md) +- [client-react-native-fs](modules/client_react_native_fs.md) +- [client-expo](modules/client_expo.md) +- [crossmint](modules/crossmint.md) +- [extension](modules/extension.md) +- [multiversx](modules/multiversx.md) +- [node-api](modules/node_api.md) diff --git a/website/static/img/architecture.png b/website/static/img/architecture.png deleted file mode 100644 index 4b103379..00000000 Binary files a/website/static/img/architecture.png and /dev/null differ diff --git a/website/typedoc.json b/website/typedoc.json index 6cd97c67..86fddb94 100644 --- a/website/typedoc.json +++ b/website/typedoc.json @@ -1,6 +1,6 @@ { "$schema": "https://typedoc.org/schema.json", - "out": "docs/api", + "out": "docs/reference", "plugin": ["typedoc-plugin-markdown"], "excludeExternals": false, "entryPointStrategy": "packages", @@ -9,18 +9,20 @@ "../packages/client-browser", "../packages/client-node", "../packages/client-react-native-fs", + "../packages/client-expo", "../packages/crossmint", "../packages/extension", "../packages/multiversx", "../packages/node-api" ], - "readme": "src/API.md", + "readme": "src/modules.md", "skipErrorChecking": true, - "entryDocument": "API.md", + "entryDocument": "modules.md", "hideBreadcrumbs": false, "hideInPageTOC": false, - "name": "API", + "name": "Reference", "excludePrivate": true, + "excludeProtected": true, "excludeInternal": true, "gitRevision": "main" } diff --git a/yarn.lock b/yarn.lock index 4eac7bf7..8746afbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1748,7 +1748,7 @@ __metadata: languageName: node linkType: hard -"@docusaurus/core@npm:3.1.1, @docusaurus/core@npm:^3.1.0": +"@docusaurus/core@npm:3.1.1, @docusaurus/core@npm:^3.1.1": version: 3.1.1 resolution: "@docusaurus/core@npm:3.1.1" dependencies: @@ -1889,7 +1889,7 @@ __metadata: languageName: node linkType: hard -"@docusaurus/module-type-aliases@npm:3.1.1, @docusaurus/module-type-aliases@npm:^3.1.0": +"@docusaurus/module-type-aliases@npm:3.1.1, @docusaurus/module-type-aliases@npm:^3.1.1": version: 3.1.1 resolution: "@docusaurus/module-type-aliases@npm:3.1.1" dependencies: @@ -2064,7 +2064,7 @@ __metadata: languageName: node linkType: hard -"@docusaurus/preset-classic@npm:^3.1.0": +"@docusaurus/preset-classic@npm:^3.1.1": version: 3.1.1 resolution: "@docusaurus/preset-classic@npm:3.1.1" dependencies: @@ -2199,7 +2199,7 @@ __metadata: languageName: node linkType: hard -"@docusaurus/tsconfig@npm:^3.1.0": +"@docusaurus/tsconfig@npm:^3.1.1": version: 3.1.1 resolution: "@docusaurus/tsconfig@npm:3.1.1" checksum: 042a03d5a282b314d3e923d3534b77dc5780e1fb3302a70a929e7e97677d2346e7e9f94452dd68f8d872eef1f02b640a686da43800e3cf0d66ee831acfad91ac @@ -14618,19 +14618,19 @@ __metadata: languageName: node linkType: hard -"typedoc@npm:^0.25.7": - version: 0.25.7 - resolution: "typedoc@npm:0.25.7" +"typedoc@npm:^0.25.12": + version: 0.25.12 + resolution: "typedoc@npm:0.25.12" dependencies: lunr: ^2.3.9 marked: ^4.3.0 minimatch: ^9.0.3 shiki: ^0.14.7 peerDependencies: - typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x bin: typedoc: bin/typedoc - checksum: 49c3bf923a3c9401b549e5843f8efaaac8fa28f8ec6bd8617187b5d9ba9932a3fa63dc3863b82389507ffc7d92908af0dce33780fffb4970cd0833274f6fa0cf + checksum: 6022dd921f78361b62ec9cbb8b847cbcf00e0f2bc45799409d5f6d74edbca8d82d31bf9a7d17a4e1c3c1916ba8265c2266bded50902533faaec53d5cdb558e80 languageName: node linkType: hard @@ -15496,16 +15496,16 @@ __metadata: version: 0.0.0-use.local resolution: "website@workspace:website" dependencies: - "@docusaurus/core": ^3.1.0 - "@docusaurus/module-type-aliases": ^3.1.0 - "@docusaurus/preset-classic": ^3.1.0 - "@docusaurus/tsconfig": ^3.1.0 + "@docusaurus/core": ^3.1.1 + "@docusaurus/module-type-aliases": ^3.1.1 + "@docusaurus/preset-classic": ^3.1.1 + "@docusaurus/tsconfig": ^3.1.1 "@mdx-js/react": ^3.0.0 clsx: ^2.1.0 prism-react-renderer: ^2.3.1 react: ^18.2.0 react-dom: ^18.2.0 - typedoc: ^0.25.7 + typedoc: ^0.25.12 typedoc-plugin-markdown: ^3.17.1 typescript: ^5.2.2 languageName: unknown