diff --git a/.gitignore b/.gitignore index 1a5aefe14..4a53da293 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ # Log file *.log +# ...except tutorial sample output +!mkdocs/**/*.log # BlueJ files *.ctxt @@ -26,3 +28,18 @@ hs_err_pid* .vscode/ .idea/ *.iml + +# MacOS +.DS_Store + +# js +node_modules/ +npm-*.log* +ts/ + +# python +.python-version +__pycache__/ +*.egg-info/ +build/ +dist/ diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..87cc27253 --- /dev/null +++ b/.mailmap @@ -0,0 +1,6 @@ +segfaultxavi +segfaultxavi Xavi Artigas +daoka +daoka +daoka +zero <234838951+zero4862@users.noreply.github.com> diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..8636a4435 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +en +ja diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..8b6f6cb5a --- /dev/null +++ b/docs/index.html @@ -0,0 +1,23 @@ + + + + + + + + + Redirecting to your language... +
+ Use these links if redirection does not work: English 日本語 + + \ No newline at end of file diff --git a/init.sh b/init.sh index 7cf7f45a9..603cb8d08 100755 --- a/init.sh +++ b/init.sh @@ -7,4 +7,5 @@ git -C _symbol config core.sparseCheckout true echo 'jenkins/*' >> .git/modules/_symbol/info/sparse-checkout echo 'linters/*' >> .git/modules/_symbol/info/sparse-checkout echo 'tests/*' >> .git/modules/_symbol/info/sparse-checkout +echo 'sdk/*' >> .git/modules/_symbol/info/sparse-checkout git submodule update --force --checkout _symbol diff --git a/mkdocs/.eslintrc b/mkdocs/.eslintrc new file mode 100644 index 000000000..d92a47261 --- /dev/null +++ b/mkdocs/.eslintrc @@ -0,0 +1,45 @@ +--- +extends: + - airbnb + - plugin:jsdoc/recommended-error + - ../linters/javascript/default.eslintrc +globals: + WebSocket: readonly +rules: + import/extensions: + - error + - ignorePackages + # Tutorials make heavy use of the console for output + no-console: off + max-len: + - error + - code: 88 + ignoreTrailingComments: true + function-paren-newline: + - off + # This rule requires some pretty ugly constructs some times + prefer-destructuring: + - off + # Allow some simple for loops + no-restricted-syntax: + - error + - ForInStatement + # No cumbersome JSDocs in tutorial code + jsdoc/require-jsdoc: + - off + # Operators are the only logical place to break some long lines + operator-linebreak: + - error + - after + # Prefer old-style function declarations for clarity + func-style: + - error + - declaration + - allowArrowFunctions: true + # Polling loops do active waiting inside loops + no-await-in-loop: + - off + # The 'ethers' dependency is only used by one tutorial, we don't want to + # force it on every user. Specially because it's a heavy dependency. + import/no-extraneous-dependencies: + - off diff --git a/mkdocs/.gitignore b/mkdocs/.gitignore new file mode 100644 index 000000000..da16a5215 --- /dev/null +++ b/mkdocs/.gitignore @@ -0,0 +1,5 @@ +# Doxygen temporary folder +.doxy + +.venv +__pycache__ diff --git a/mkdocs/.pycodestyle b/mkdocs/.pycodestyle new file mode 100644 index 000000000..0593b46f2 --- /dev/null +++ b/mkdocs/.pycodestyle @@ -0,0 +1,3 @@ +[pycodestyle] +max-line-length = 88 +ignore = W191, E128, W503, W504 diff --git a/mkdocs/CONTRIBUTING.md b/mkdocs/CONTRIBUTING.md new file mode 100644 index 000000000..e78cba963 --- /dev/null +++ b/mkdocs/CONTRIBUTING.md @@ -0,0 +1,207 @@ +# Documentation Guidelines + +These are some guidelines for writing *technical documentation*. + +The goal of technical docs is to *teach*: there is something *we* know and the *reader* does not, and needs to learn. +Therefore, tech docs need to be clear, unambiguous, and concise. +Compare with *marketing material* which has different goals and uses different techniques. + +A good document structure helps the reader find what they need quickly without having to read too much. +That said, if understanding a document requires previous knowledge, you must always state so in the introduction and provide links. + +**Always put yourself in the shoes of the reader.** + +## General + +* **Keep the scope of the document in mind**. + + A document should precisely fulfill its purpose, nothing more, nothing less. + It is a common pitfall to end up going into rabbit holes and spending half a document explaining irrelevant details. + +* **Keep the audience in mind**. + + Always think whether your intended audience will understand what you are writing. + Do they have all the necessary context? Education? Data? + +* **Try to write short sentences.** + + Avoid complex grammar, complex use of tenses, ambiguous pronouns and so on. + A good guideline when it comes to technical writing is to aim for 20-30 words per sentence. + Keeping sentences short should however never come at the expense of clarity, syntactic cues and important information. + +* **Consistency is key**. + + Be consistent in your use of formatting, words and expressions, as it makes the text easier to understand. + +* **USE A SPELL CHECKER**. + + Seriously, I’m ready to use physical violence to enforce this one. + +* **Use a Markdown checker when writing Markdown**. + + It will get rid of the most common (and annoying) markdown issues, like trailing white space, unnecessary blank lines around blocks, etc. + At some point this might even be enforced. + +## Structure + +* Document and section titles should follow the [Chicago Title Capitalization](https://en.wikipedia.org/wiki/Title_case#Chicago_Manual_of_Style) standard. +* Documents should start with a level one heading and should ideally be the same as the file name. +* Sections should be ordered hierarchically. Each document starts with a level one heading (`#`), which can contain one or more level two headings (`##`), which can contain one or more level threes (`###`) and so on. + + You cannot skip levels, e.g., you cannot add a level 6 right after the title because it looks nice *in a particular app*. + +## Markdown Formatting + +* Lists should use the `*` character rather than the `-` character, always start capitalized and end with a full stop. +* Paragraphs that include multiple sentences should have the sentences on separate lines, so that updating one sentence results in a clear diff where only one line changes. +* For long documents, it is good to have a table of contents at the end of the introduction of the level one heading section. +* Always specify the language for code blocks so that neither the syntax highlighter nor the text editor must guess. + If no specific type makes sense, just use `text`. + +## Additional Formatting and Macros + +Some plugins enable additional formatting. On top of them, a few macros have been created to simplify repeated process +like tutorial steps and multi-language code snippets. + +### Glossary Links + +Define glossary terms using: + +```markdown +category:glossary_term +: Definition. +``` + +If no category is used (and no colon after it), the default category is used. +The default category can also be used explicitly by using `_`. + +Link to glossary terms using `` and you'll get a popup with the definition when hovering +over the term in the text. + +Link to glossary terms in the default category using ``. + +You can provide an alternate text instead of the glossary term using a pipe `|`: +``. +The glossary plugin takes care of plurals, though, so they don't typically require the alternate text. + +Every API class and method defines a term, so they can be linked to using, for example: ``. +The available categories are `java`, `get`, `post`, `ser`, and `ws`. + +### Tutorial Steps + +These macros create a table with each row beginning with a big-numbered description and a floating screenshot on the right. +When clicked, the image is zoomed while the description is still shown. +Steps can be navigated while the image is zoomed. + +```jinja +{% import 'tutorial.jinja2' as tutorial %} + +{{ tutorial.list_begin() }} +{{ tutorial.step_begin("screenshots/create-profile-0.jpg") }} +Write here the description for this step. +{{ tutorial.step_end() }} +{{ tutorial.list_end() }} +``` + +[Usage example](./pages/en/userbook/wallet/create-profile.md). + +Add as many `step_begin()` / `step_end()` pairs as required. + +**Lists do not work correctly in the description**, because they are an HTML block element and do not flow around the floating picture. + +### Multi-Language Code Snippets + +These macros create a tab group with a code block and optional caption. + +There are two versions: + +The simplified one accepts a list of strings, describing the language and line range, and optionally a caption. + +```jinja +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full("devbook/hello-world", ["py", "js"]) }} +{{ tutorial.code_snippet(["py:4:4", "js:4:4"])}} +{{ tutorial.code_snippet(["py:6:16", "js:6:16:The constructor only accepts parameters of the right type, \ +making it easier to use during development. We can do almost any markdown here:\n +* One **black**\n +* Two"]) }} +``` + +The extended syntax accepts a list of objects, keyed by language code: + +```jinja +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_snippet({ + 'py': { 'range': [41, 54] }, + 'js': { + 'range': [40, 52], + 'descriptor': 'TransferTransactionV1Descriptor' + } +}) }} +``` + +Available parameters are: + +* `range`: List of two values indicating the start and end lines of the code snippet. +* `descriptor`: If present, includes an admonition about typed descriptors including a link to this descriptor. +* `caption`: Free text to add below the snippet. + +`code_snippet` uses the filename of the previous `code_full`. + +[Usage example](./pages/en/devbook/start/hello-world.md). + +`code_full` inserts the whole source file, for all the listed languages, and sets the file name to be used by the snippet macros. +Each language tab can have an optional caption, separated from the language code by a colon. + +`code_snippet` inserts a range of lines, with an optional caption. + +**Captions allow complex markdown like lists and term links, but they are formatted differently.** +Lines must be continued by escaping the line break, and line breaks are inserted with \n. +See the example above. + +The only supported language is Java (`java`). +See [`tutorial.jinja2`](./templates/macros/tutorial.jinja2) for details. + +## Technical Writing + +* Use American English (`organize` instead of `organise`, `behavior` instead of `behaviour`, etc.) +* Use the American format for dates with long month names: `January 9, 2023`. 3-letter short month names can be used when space is at a premium, for example on narrow table columns. In this case, use the Day-Month-Year format: `9-Jan-2023`. +* Do not use gendered pronouns when talking about users/consumers/whatever but always `they/their` instead. +* Avoid talking about `us`, or `we`, even if it means resorting to passive voice. +* Use active voice when there is no specific need to use passive. +* Do not use the future tense but use present simple for expressing general truths instead. +* Abbreviations and acronyms should be spelled out the first time they appear in any technical document with the shortened form appearing in parentheses immediately after the term. + The abbreviation or acronym can then be used throughout the document. +* Avoid ambiguous and abstract language (e.g. `really`, `quite`, `very`), imprecise or subjective terms (i.e. `fast`, `slow`, `tall`, `small`) and words that have no precise meaning (i.e. `a bit`, `thing`, `stuff`). +* Avoid contractions (e.g. `don't`, `you'll`, etc.) as they are meant for informal contexts. +* Avoid generalized statements, because they are difficult to substantiate and too broad to be supported. +* Avoid story-telling, remain factual and concise. +* Avoid jargon. +* Humor is allowed, as long as it is not distracting. I.e., do not go out of your way for the sake of a pun. +* Avoid em-dashes `—`. Putting non-restrictive relative clauses into separate sentences leads to simpler, clearer writing. + If em-dashes are needed, make sure to use the right character: `—` (alt code: `ALT+0151`). + + Most of the time what you really want is a colon `:`. +* When referring to something in a certain way (i.e. `FBAS` for *Federated Byzantine Agreement System*) make sure to consistently use only FBAS after the term is introduced. +* Use digits when the number is mostly meant to be used in a program. + Spell out numbers when they are not (e.g., when a number can be a pronoun, such as in *that's the one I used*). + +## Links + +* Use informative link titles. + For example, instead of naming your links `link` or `here`, wrap part of the sentence that is meant to be linked as a title. +* Links to external sources should be: + * Clear, concise, factual (not tips & tricks-type articles, or blog posts). + * Reliable to stand the test of time (will not start to 404 because it's a personal blog and the person decided to get rid of it, for example). + * From reliable sources (this is where Wikipedia isn't always perfect, but fine for technical subjects). +* Whenever possible, use internal links instead of external ones: if something has been described in our documents somewhere, link to it instead of externally. + +## Official Spellings + +* dapp +* mainnet (or main network) +* smart contracts +* testnet (or test network) +* web3 diff --git a/mkdocs/Jenkinsfile b/mkdocs/Jenkinsfile new file mode 100644 index 000000000..b520cb8e2 --- /dev/null +++ b/mkdocs/Jenkinsfile @@ -0,0 +1,7 @@ +defaultCiPipeline { + operatingSystem = ['ubuntu'] + instanceSize = 'medium' + environment = 'docs' + packageId = 'docs' + publisher = 'gh-pages' +} diff --git a/mkdocs/README.md b/mkdocs/README.md new file mode 100644 index 000000000..07b22cee5 --- /dev/null +++ b/mkdocs/README.md @@ -0,0 +1,95 @@ +# Documentation + +Congratulations! You found the secret documentation README file, which explains: + +* The build process for the new NEM docs site (new as of 2026). +* The tools used. +* The rationale behind some of the decisions. +* How to use these tools in the production on new content pages. + +This file is mostly addressed to two audiences: + +* Content writers that want to know how to add content and what special tools they have at their disposal. + Move to the [Content Writers](#content-writers) section. + +* Doc-ops guys that need to maintain the docs' pipeline running smoothly. + Move to the [Doc-ops](#doc-ops) section. + +## Content Writers + +There are two kinds of documentation pages: original and autogenerated from source code. + +### Original Pages + +Original pages are created from [Markdown](https://www.markdownguide.org/) text files in the `pages` folder next to this file. +The User Manual, the Textbook and all the tutorials in the Developer Manual are original pages. + +A copy of each page exist in each of the supported languages. +For now, these are English and Japanese, and reside in the `pages/en` and `pages/ja` folders. + +If you add a new page, remember to add it to the navigation sidebar. +You need to add an entry to the `nav` section in `config/mkdocs.en.yml` and `config/mkdocs.ja.yml`. + +You must use Markdown syntax in these files, with some additions brought by the different tools we are using: + +* [Basic Markdown syntax](https://www.markdownguide.org/basic-syntax/). +* [Material theme features](https://squidfunk.github.io/mkdocs-material/reference/). +* [Glossary links](https://realtimeprojects.github.io/mkdocs-ezglossary/usage/definition/): + +See the [CONTRIBUTING](./CONTRIBUTING.md) guide for more information. + +### Autogenerated Reference Pages + +Developers usually add comments to their source code in a format that can be read by developers, +but also extracted automatically and used to build exhaustive API reference guides. + +This is the method used to generate the reference pages in the NEM Developer Manual, +so tech writers also need to take care of the source code comments in: + +* Java: [`/core`](/core), [`/nis`](/nis), [`/peer`](/peer) +* REST: [`/openapi`](/openapi9) + +However, the features available for autogenerated pages are limited to the +[basic Markdown syntax](https://www.markdownguide.org/basic-syntax/). + +## Doc-ops + +### Dependencies + +* Python: Install `requirements.txt` in this folder. +* [Doxygen](https://www.doxygen.nl/): Tested with 1.13.2 +* [GraphViz](https://www.graphviz.org): Tested with 2.43.0 + +### Build Commands + +Once all dependencies are installed: + +* Run `/scripts/ci/build.sh` from the `mkdocs` folder to generate the static site in the `/docs/` folder. +* Run `/scripts/ci/gh_pages_publish.sh` to publish the site to GitHub pages. + +### Build Flow + +All MkDocs hooks are in the `scripts/hooks.py` file. + +Plugins: + +* MkDoxy: Plugin that uses Doxygen to generate the Java API docs. + * Doxygen is an external C++ tool. + * Uses templates in `templates/mkdoxy` to add term definitions. + * The `on_files` hook removes unwanted files, configured in the `extra/nem/java-sdk/include-prefixes/` section of `config/mkdocs.base.yml`. +* swagger-ui-tag: Allows embedding OpenAPI specs in docs. + * The `on_pre_build` hook copies the YAML spec file from `/openapi` to `devbook/reference/rest`. +* gen_files: Plugin that executes Python scripts that can create new files and add them to navigation. + * `scripts/gen_ref_pages_java.py`: Creates `devbook/reference/java/links.md` to add generated Java API files to navigation. +* ezglossary: Plugin that adds tooltips to every term link. + * Uses templates in `templates/ezglossary`. +* literate-nav: Embeds `links.md` files into navigation. + +Overrides: + +These are MkDocs templates to customize the pages. + +* `main.html`: Defines the `styles` block that sets colors depending on the section (`userbook`, `devbook` or `textbook`). +* `partials/source.html`: Empty file to remove the GitHub info on the corner. +* `partials/copyright.html`: Add cookie settings link in the footer. +* `partials/javascripts/content.html`: Script to handle dynamic links that change depending on selected programming language (EXPERIMENTAL). diff --git a/mkdocs/config/mkdocs.base.yml b/mkdocs/config/mkdocs.base.yml new file mode 100644 index 000000000..815f712d3 --- /dev/null +++ b/mkdocs/config/mkdocs.base.yml @@ -0,0 +1,175 @@ +repo_url: https://github.com/NemProject/nem +repo_name: nem +plugins: + search: {} + meta-manager: {} # Add recursive metadata to md files + ezglossary: # Automatic glossary tooltips + templates: ../templates/ezglossary + use_default: true + inline_refs: none + markdown_links: true + tooltip: full + ignore_case: true + strict: true + plurals: en + sections: + - py + - js + - java + - ws + - req + - ser + - _ + mkdoxy: # Generate API reference docs for Java + enabled: !ENV [NEM_DOCS_JAVA, false] + save-api: .doxy + projects: + JavaSDK: # name of project must be alphanumeric + numbers (without spaces) + src-dirs: ../core/src/main/java ../nis/src/main/java ../peer/src/main/java # path to source code (support multiple paths separated by space) => INPUT + template-dir: templates/mkdoxy + full-doc: !ENV [NEM_DOCS_FULL, true] # if you want to generate full documentation + api-path: devbook/reference/java + doxy-cfg: # standard doxygen configuration (key: value) + FILE_PATTERNS: "*.java" # specify file patterns to filter out + OPTIMIZE_OUTPUT_JAVA: true + JAVADOC_AUTOBRIEF: true + STRIP_FROM_PATH: !relative $config_dir/../.. + SOURCE_BROWSER: true + SOURCE_TOOLTIPS: true + INLINE_SOURCES: true + gen-files: # Generate stub files for API reference docs and link.md files for literate-nav + scripts: + - ../scripts/gen_ref_pages_java.py + - ../scripts/gen_ref_pages_py.py + - ../scripts/gen_ref_pages_ts.py + mkdocstrings: # Generate API reference docs for Python + default_handler: python + custom_templates: ../templates/mkdocstrings + handlers: + python: + paths: [../../_symbol/sdk/python] + options: + show_source: false + merge_init_into_class: false + show_symbol_type_heading: true + show_symbol_type_toc: true + show_root_toc_entry: false + show_object_full_path: false + literate-nav: # Allow navigation to be specified in the md files created by gen-files + nav_file: links.md + git-revision-date-localized: # git dates in page footers + enable_creation_date: true + exclude: + - devbook/reference/* + git-authors: # git authors in page footers + sort_authors_by: contribution + exclude: + - devbook/reference/* + glightbox: + auto_caption: false + caption_position: left + background: none + shadow: false + macros: + include_dir: templates/macros + site-urls: {} +hooks: + - ../scripts/hooks.py + - ../scripts/typedoc-plugin.py + - ../scripts/register_lexers.py +markdown_extensions: + attr_list: {} + admonition: {} + pymdownx.caret: {} + pymdownx.mark: {} + pymdownx.tilde: {} + pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pymdownx.inlinehilite: {} + pymdownx.snippets: + base_path: !relative $config_dir/../snippets + check_paths: true + pymdownx.superfences: {} + pymdownx.tabbed: + alternate_style: true + pymdownx.details: {} + def_list: {} + md_in_html: {} + toc: + permalink: ⚓︎ + toc_depth: 3 + mkdocs_graphviz: {} + pymdownx.arithmatex: + generic: true +extra_javascript: + - assets/javascripts/katex.js + - https://unpkg.com/katex@0/dist/katex.min.js + - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js +theme: + name: material + custom_dir: ../overrides + favicon: assets/images/favicon.ico + icon: + repo: fontawesome/brands/github + logo: assets/images/nem-logo.svg + features: + - content.tooltips # Material tooltips instead of browser + # - navigation.instant # Single-page application + # - navigation.prune # Navigation only holds visible items + - navigation.tracking # URL follows the current section in the page + - navigation.tabs # Top navbar + - toc.follow # Current section in TOC is always visible + - content.tabs.link # Linked content tabs + - content.code.copy # Copy code button in code blocks + - content.action.edit # Edit on GitHub button + - content.action.view # View on GitHub button + - search.suggest # Suggest search terms + - search.highlight # Highlight searched term + - search.share # Add share button for deep-linking to search results +extra: + homepage: https://nemproject.github.io/nem-docs + alternate: + - name: English + link: /en/ + lang: en + - name: 日本語 + link: /ja/ + lang: ja + nem: + branch: new-docs + java-sdk: + include-prefixes: [classorg, interfaceorg] + py-sdk: # Symbol SDK is shared with NEM; exclude the Symbol-only parts + ignore-files: + - __main__ + - symbolchain + - nem + - sc + - nc + - SymbolFacade + - facade + - external + - Ordered + - SharedKey + ignore-folders: + - symbol + - impl + ts-sdk: + output_dir: devbook/reference/ts + tsconfig: ../_symbol/sdk/javascript/tsconfig/check-bindings.json + options: config/typedoc.json + disabled: !ENV [NEM_DOCS_DISABLE_TS, false] + class-remaps: {} + global-namespaces: + - FeeCalculator +copyright: > + Copyright © 2026 The Symbol Syndicate +extra_css: + - assets/stylesheets/extra.css + - https://unpkg.com/katex@0/dist/katex.min.css +not_in_nav: | + /404.md diff --git a/mkdocs/config/mkdocs.en.yml b/mkdocs/config/mkdocs.en.yml new file mode 100644 index 000000000..2a1212b23 --- /dev/null +++ b/mkdocs/config/mkdocs.en.yml @@ -0,0 +1,125 @@ +INHERIT: ./mkdocs.base.yml + +site_name: NEM Docs WIP 🚧 +site_url: https://nemproject.github.io/nem-docs/en +docs_dir: ../pages/en +site_dir: ../../docs/en +edit_uri: blob/new-docs/mkdocs/pages/en +plugins: + search: + lang: en + mkdoxy: # Generate API reference docs for Java + projects: + JavaSDK: # name of project must be alphanumeric + numbers (without spaces) + doxy-cfg: # standard doxygen configuration (key: value) + OUTPUT_LANGUAGE: English +markdown_extensions: + toc: + permalink_title: Anchor link to this section for reference + title: On this page +theme: + language: en + font: + text: Nunito Sans + palette: + # Palette toggle for automatic mode + - media: (prefers-color-scheme) + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference +extra: + nem: + tutorial_level: Tutorial level + tutorial_level_labels: + beginner: BEGINNER + intermediate: INTERMEDIATE + advanced: ADVANCED + consent: + title: Cookie consent + description: >- + We use cookies to recognize your repeated visits and preferences, as well + as to measure the effectiveness of our documentation and whether users + find what they're searching for. With your consent, you're helping us to + make our documentation better. + social: + - icon: fontawesome/solid/house + link: https://nemproject.github.io/nem-docs + name: Go to the home page + - icon: fontawesome/brands/x-twitter + link: https://x.com/SymbolSyndicate + name: Follow us on X + - icon: fontawesome/brands/github + link: https://github.com/NemProject + name: Explore our repos + - icon: fontawesome/brands/discord + link: https://discord.gg/J38KwW5ZuG + name: Join our Discord server + - icon: fontawesome/solid/handshake-simple + link: https://nemproject.github.io/nem-docs + name: Visit the legacy documentation site +nav: + - index.md + - User Manual: + - userbook/intro.md + - Developer Manual: + - devbook/intro.md + - Getting Started: + - devbook/start/setup.md + - devbook/start/hello-world.md + - Tutorials: + - Accounts: + - devbook/accounts/create-from-private-key.md + - devbook/accounts/create-from-mnemonic.md + - devbook/accounts/testnet-faucet.md + - devbook/accounts/query-balance.md + - Transactions: + - devbook/transactions/transfer-xem.md + - devbook/transactions/transfer-mosaics.md + - devbook/transactions/monitoring-status.md + - devbook/transactions/typed-descriptors.md + - Mosaics: + - devbook/mosaics/get-mosaic-info.md + - Namespaces: + - devbook/namespaces/get-namespace-info.md + - Network Currency: + - devbook/network-currency/query-currency-supply.md + - devbook/network-currency/query-block-rewards.md + - Chain State: + - devbook/chain/chain-heights.md + - WebSockets: + - devbook/websockets/listen-new-blocks.md + - devbook/websockets/listen-transaction-flow.md + - Reference Guides: + - Python SDK: devbook/reference/py/ + - TypeScript SDK: devbook/reference/ts/ + - NEM REST API: devbook/reference/rest/nem.md + - Serialization: devbook/reference/serialization/index.md + - WebSockets: devbook/reference/websockets/index.md + - Textbook: + - textbook/intro.md + - textbook/cryptography.md + - textbook/accounts.md + - textbook/consensus.md + - textbook/transactions.md + - textbook/transfer_transactions.md + - textbook/mosaics.md + - textbook/namespaces.md + - textbook/blocks.md + - textbook/nodes.md + - textbook/harvesting.md + - textbook/cats.md + - textbook/glossary.md diff --git a/mkdocs/config/mkdocs.ja.yml b/mkdocs/config/mkdocs.ja.yml new file mode 100644 index 000000000..f8932a1c8 --- /dev/null +++ b/mkdocs/config/mkdocs.ja.yml @@ -0,0 +1,77 @@ +INHERIT: ./mkdocs.base.yml + +site_name: NEMドキュメント WIP ⚠ +site_url: https://nemproject.github.io/nem-docs/ja +docs_dir: ../pages/ja +site_dir: ../../docs/ja +edit_uri: blob/new-docs/mkdocs/pages/ja +plugins: + search: + lang: ja + mkdoxy: # Generate API reference docs for Java + projects: + JavaSDK: # name of project must be alphanumeric + numbers (without spaces) + doxy-cfg: # standard doxygen configuration (key: value) + OUTPUT_LANGUAGE: Japanese +markdown_extensions: + toc: + permalink_title: 参照用にこのセクションへのアンカーリンク + title: このページに +theme: + language: ja + palette: + # Palette toggle for automatic mode + - media: (prefers-color-scheme) + toggle: + icon: material/brightness-auto + name: ライトモードに切り替える + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: ダークモードに切り替える + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: システム設定に切り替える +extra: + consent: + title: クッキーの同意 + description: >- + 当社では、お客様の繰り返しのアクセスや好みを認識するため、 + また、当社のドキュメントの有効性やユーザーが探しているものを見つけられるかどうかを測定するために Cookie を使用しています。 + お客様の同意により、当社はドキュメントの改善に協力することになります。 + social: + - icon: fontawesome/solid/house + link: https://nemproject.github.io/nem-docs + name: ホームページ + - icon: fontawesome/brands/x-twitter + link: https://x.com/SymbolSyndicate + name: X (旧Twitter) + - icon: fontawesome/brands/github + link: https://github.com/NemProject + name: リポジトリ + - icon: fontawesome/brands/discord + link: https://discord.gg/J38KwW5ZuG + name: Discordサーバー + - icon: fontawesome/solid/handshake-simple + link: https://nemproject.github.io/nem-docs + name: 旧ドキュメントサイト +nav: + - index.md + - ユーザーマニュアル: + - userbook/intro.md + - 開発者マニュアル: + - devbook/intro.md + - リファレンスガイド: + - Python SDK: devbook/reference/py/ + - TypeScript SDK: devbook/reference/ts/ + - NEM REST API: devbook/reference/rest/nem.md + - 教科書: + - textbook/intro.md + - textbook/glossary.md diff --git a/mkdocs/config/typedoc.json b/mkdocs/config/typedoc.json new file mode 100644 index 000000000..feb660375 --- /dev/null +++ b/mkdocs/config/typedoc.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://typedoc-plugin-markdown.org/schema.json", + + // The Symbol SDK is shared with NEM. Document only the NEM and shared + // entry points and exclude the Symbol-only modules. + "entryPoints": [ + "../../_symbol/sdk/javascript/ts/src/index.d.ts", + "../../_symbol/sdk/javascript/ts/src/Bip32.d.ts", + "../../_symbol/sdk/javascript/ts/src/Network.d.ts", + "../../_symbol/sdk/javascript/ts/src/NetworkTimestamp.d.ts", + "../../_symbol/sdk/javascript/ts/src/nem/index.d.ts", + "../../_symbol/sdk/javascript/ts/src/nem/MessageEncoder.d.ts" + ], + "plugin": [ + "typedoc-plugin-markdown" + ], + + "router": "kind-structure", + "includeVersion": true, + "excludePrivate": true, + "excludeExternals": true, + "sortEntryPoints": false, + "cleanOutputDir": true, + "readme": "none", + "disableSources": true, + "gitRemote": "upstream", + "exclude": ["**/symbol/*"], + "disableGit": true, + + // Markdown plugin options + "hidePageHeader": true, + "hidePageTitle": false, + "hideBreadcrumbs": true, + "hideGroupHeadings": true, + "useCodeBlocks": true, + "excludeScopesInPaths": true, + "parametersFormat": "table", + "propertiesFormat": "table", + "enumMembersFormat": "table", + "classPropertiesFormat": "table", + "propertyMembersFormat": "table", + "typeDeclarationFormat": "table", + "interfacePropertiesFormat": "table", + "useHTMLEncodedBrackets": true +} diff --git a/mkdocs/lexers/cats_lexer.py b/mkdocs/lexers/cats_lexer.py new file mode 100644 index 000000000..a0180f4ec --- /dev/null +++ b/mkdocs/lexers/cats_lexer.py @@ -0,0 +1,22 @@ +from pygments.lexer import RegexLexer +from pygments.token import * + +__all__ = ['CATSLexer'] + +class CATSLexer(RegexLexer): + name = 'CATS' + aliases = ['cats'] + filenames = ['*.cats'] + + tokens = { + 'root': [ + (r'\b(import|using|enum|struct|make_const|make_reserved|binary_fixed|sizeof|if|equals|not equals|has|not has|array|__FILL__|__value__|abstract|inline|pad_last|not pad_last)\b', Keyword), + (r'\b(__FILL__|__value__)\b', Keyword.Reserved), + (r'\b(int8|int16|int32|int64|uint8|uint16|uint32|uint64)\b', Keyword.Type), + (r'@(is_bitwise|is_aligned|is_size_implicit|size|initializes|discriminator|comparer|alignment|is_byte_constrained|sort_key|sizeref)\b', Keyword), + (r'(0x)?\d+', Number), + (r'[a-zA-Z_][a-zA-Z0-9_]*', Name), + (r'#.*', Comment.Single), + (r'\s+', Text), + ], + } diff --git a/mkdocs/lexers/stomp_lexer.py b/mkdocs/lexers/stomp_lexer.py new file mode 100644 index 000000000..f819e3641 --- /dev/null +++ b/mkdocs/lexers/stomp_lexer.py @@ -0,0 +1,27 @@ +from pygments.lexer import RegexLexer, bygroups +from pygments.token import * + +__all__ = ['STOMPLexer'] + + +class STOMPLexer(RegexLexer): + name = 'STOMP' + aliases = ['stomp'] + filenames = ['*.stomp'] + + tokens = { + 'root': [ + (r'[A-Z][A-Z0-9_-]*', Keyword, 'headers'), + (r'.+', Text, 'headers'), + ], + 'headers': [ + (r'\n\n', Text, 'body'), + (r'\n', Text), + (r'([^:\s]+)(:)([^\n]*)', bygroups(Name.Attribute, Punctuation, String)), + (r'.+', Text), + ], + 'body': [ + (r'.+', Text), + (r'\n', Text), + ], + } diff --git a/mkdocs/overrides/404.html b/mkdocs/overrides/404.html new file mode 100644 index 000000000..c36dc78cd --- /dev/null +++ b/mkdocs/overrides/404.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/mkdocs/overrides/assets/images/confused.webp b/mkdocs/overrides/assets/images/confused.webp new file mode 100644 index 000000000..8cdc58db0 Binary files /dev/null and b/mkdocs/overrides/assets/images/confused.webp differ diff --git a/mkdocs/overrides/assets/images/devbook-selected.svg b/mkdocs/overrides/assets/images/devbook-selected.svg new file mode 100755 index 000000000..1416aea2b --- /dev/null +++ b/mkdocs/overrides/assets/images/devbook-selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/mkdocs/overrides/assets/images/devbook.svg b/mkdocs/overrides/assets/images/devbook.svg new file mode 100755 index 000000000..abe50ad3e --- /dev/null +++ b/mkdocs/overrides/assets/images/devbook.svg @@ -0,0 +1,3 @@ + + + diff --git a/mkdocs/overrides/assets/images/favicon.ico b/mkdocs/overrides/assets/images/favicon.ico new file mode 100755 index 000000000..37c29355d Binary files /dev/null and b/mkdocs/overrides/assets/images/favicon.ico differ diff --git a/mkdocs/overrides/assets/images/nem-logo.svg b/mkdocs/overrides/assets/images/nem-logo.svg new file mode 100755 index 000000000..a5773bb82 --- /dev/null +++ b/mkdocs/overrides/assets/images/nem-logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/mkdocs/overrides/assets/images/textbook-selected.svg b/mkdocs/overrides/assets/images/textbook-selected.svg new file mode 100755 index 000000000..444c63a25 --- /dev/null +++ b/mkdocs/overrides/assets/images/textbook-selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/mkdocs/overrides/assets/images/textbook.svg b/mkdocs/overrides/assets/images/textbook.svg new file mode 100755 index 000000000..65a959c2e --- /dev/null +++ b/mkdocs/overrides/assets/images/textbook.svg @@ -0,0 +1,3 @@ + + + diff --git a/mkdocs/overrides/assets/images/userbook-selected.svg b/mkdocs/overrides/assets/images/userbook-selected.svg new file mode 100755 index 000000000..b402000a3 --- /dev/null +++ b/mkdocs/overrides/assets/images/userbook-selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/mkdocs/overrides/assets/images/userbook.svg b/mkdocs/overrides/assets/images/userbook.svg new file mode 100755 index 000000000..0fa11759b --- /dev/null +++ b/mkdocs/overrides/assets/images/userbook.svg @@ -0,0 +1,3 @@ + + + diff --git a/mkdocs/overrides/assets/images/watercolor-dark.webp b/mkdocs/overrides/assets/images/watercolor-dark.webp new file mode 100755 index 000000000..7d84d3ee5 Binary files /dev/null and b/mkdocs/overrides/assets/images/watercolor-dark.webp differ diff --git a/mkdocs/overrides/assets/images/watercolor-light.webp b/mkdocs/overrides/assets/images/watercolor-light.webp new file mode 100755 index 000000000..36d991a8e Binary files /dev/null and b/mkdocs/overrides/assets/images/watercolor-light.webp differ diff --git a/mkdocs/overrides/assets/javascripts/katex.js b/mkdocs/overrides/assets/javascripts/katex.js new file mode 100644 index 000000000..8786759af --- /dev/null +++ b/mkdocs/overrides/assets/javascripts/katex.js @@ -0,0 +1,10 @@ +document$.subscribe(({ body }) => { + renderMathInElement(body, { + delimiters: [ + { left: "$$", right: "$$", display: true }, + { left: "$", right: "$", display: false }, + { left: "\\(", right: "\\)", display: false }, + { left: "\\[", right: "\\]", display: true } + ], + }) +}) diff --git a/mkdocs/overrides/assets/pdfs/NEM_techRef.pdf b/mkdocs/overrides/assets/pdfs/NEM_techRef.pdf new file mode 100644 index 000000000..19af8c6be Binary files /dev/null and b/mkdocs/overrides/assets/pdfs/NEM_techRef.pdf differ diff --git a/mkdocs/overrides/assets/stylesheets/extra.css b/mkdocs/overrides/assets/stylesheets/extra.css new file mode 100644 index 000000000..1ce93f946 --- /dev/null +++ b/mkdocs/overrides/assets/stylesheets/extra.css @@ -0,0 +1,871 @@ +@font-face { + font-family: "Nunito Sans"; + font-style: normal; + font-weight: 200 1000; + font-stretch: 100%; + font-display: swap; + src: url(https://fonts.gstatic.com/s/nunitosans/v19/pe0AMImSLYBIv1o4X1M8ce2xCx3yop4tQpF_MeTm0lfUVwoNnq4CLz0_kJ3xzA.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Colors */ +:root { + --userbook-color: #FAB600; + --userbook-color--dark: #997000; + --userbook-color--light: #FFD462; + --userbook-color--transparent: #FAB60020; + --devbook-color: #67B8E8; + --devbook-color--dark: #2389C5; + --devbook-color--light: #C7E8FB; + --devbook-color--transparent: #67B8E820; + --textbook-color: #4FBAAF; + --textbook-color--dark: #159083; + --textbook-color--light: #B0ECE6; + --textbook-color--transparent: #4FBAAF20; + --example-admonition-color: #4CAF50; + --example-admonition-color--transparent: #4CAF5020; + + /* Scalar REST API reference */ + --scalar-font-size: 0.8rem; + --scalar-small-font-size: 0.7rem; + --scalar-font: var(--scalar-font-size); + --scalar-font-size-3: var(--scalar-small-font-size); + --scalar-font-size-4: var(--scalar-small-font-size); + --scalar-small: var(--scalar-small-font-size); + --scalar-micro: var(--scalar-small-font-size); + --scalar-mini: var(--scalar-small-font-size); + +} + +[data-md-color-scheme=slate][data-md-color-primary=indigo] { + --md-typeset-a-color: var(--md-primary-fg-color--light); + --md-accent-fg-color: var(--md-primary-fg-color); + --md-accent-fg-color--light: var(--md-primary-fg-color--dark); + --md-accent-fg-color--dark: var(--md-primary-fg-color--light); + --md-default-bg-color--light: #333; + --md-footer-bg-color--dark: var(--md-primary-fg-color--dark); + --md-footer-fg-color: var(--md-primary-fg-color); + --md-blanket-color: #000C; + --md-code-bg-color: var(--md-accent-fg-color--transparent); + --md-typeset-mark-color: inherit; + --md-code-hl-string-color: #d59781; + --md-code-hl-comment-color: green; + --md-typeset-table-color: var(--md-primary-fg-color); + --md-typeset-table-color--light: var(--md-accent-fg-color--transparent); + --userbook-a-color: var(--userbook-color--light); + --userbook-a-hl-color: var(--userbook-color); + --devbook-a-color: var(--devbook-color--light); + --devbook-a-hl-color: var(--devbook-color); + --textbook-a-color: var(--textbook-color--light); + --textbook-a-hl-color: var(--textbook-color); +} + +[data-md-color-scheme=default][data-md-color-primary=indigo] { + --md-typeset-a-color: var(--md-primary-fg-color); + --md-accent-fg-color: var(--md-primary-fg-color--dark); + --md-accent-fg-color--light: var(--md-primary-fg-color--light); + --md-accent-fg-color--dark: var(--md-primary-fg-color--dark); + --md-default-bg-color--light: #DDD; + --md-footer-bg-color--dark: var(--md-primary-fg-color--dark); + --md-footer-fg-color: var(--md-primary-fg-color); + --md-blanket-color: #CCCE; + --md-code-bg-color: var(--md-accent-fg-color--transparent); + --md-code-hl-string-color: #d59781; + --md-code-hl-comment-color: green; + --md-typeset-table-color: var(--md-primary-fg-color); + --md-typeset-table-color--light: var(--md-accent-fg-color--transparent); + --userbook-a-color: var(--userbook-color); + --userbook-a-hl-color: var(--userbook-color--dark); + --devbook-a-color: var(--devbook-color); + --devbook-a-hl-color: var(--devbook-color--dark); + --textbook-a-color: var(--textbook-color); + --textbook-a-hl-color: var(--textbook-color--dark); +} + +/* Home page cards */ +.userbook div { + min-width: 300px; + min-height: 300px; + background: url("../images/userbook.svg") no-repeat center / contain; +} + +.devbook div { + min-width: 300px; + min-height: 300px; + background: url("../images/devbook.svg") no-repeat center / contain; +} + +.textbook div { + min-width: 300px; + min-height: 300px; + background: url("../images/textbook.svg") no-repeat center / contain; +} + +.userbook:hover div { + background: url("../images/userbook-selected.svg") no-repeat center / contain; +} + +.devbook:hover div { + background: url("../images/devbook-selected.svg") no-repeat center / contain; +} + +.textbook:hover div { + background: url("../images/textbook-selected.svg") no-repeat center / contain; +} + +.md-typeset .grid .card { + text-align: center; + padding: 0; + border: 0; +} + +.md-typeset .grid .card:hover { + box-shadow: none; +} + +.md-typeset .grid a.userbook:hover h2, +.md-typeset .grid a.userbook:hover p { + color: var(--userbook-a-hl-color); +} +.md-typeset .grid a.devbook:hover h2, +.md-typeset .grid a.devbook:hover p { + color: var(--devbook-a-hl-color); +} +.md-typeset .grid a.textbook:hover h2, +.md-typeset .grid a.textbook:hover p { + color: var(--textbook-a-hl-color); +} + +.md-typeset .grid .card a { + display: block; + padding: 10px; + height: 100%; +} + +.md-typeset .grid .card h2 { + margin: 10px 0 0 0; + font-size: 1.5rem; + font-family: "Nunito Sans"; + color: var(--md-default-fg-color); +} + +.md-typeset .grid .card p { + margin: 0; + font-size: 1rem; + color: var(--md-default-fg-color--light); +} + +/* Top navigation tab style: centered, with icons and custom font*/ +.md-tabs .md-tabs__item:first-child { + display: none; +} + +.md-tabs .md-tabs__item a { + padding-left: 2rem; + font-size: 2em; + font-family: "Nunito Sans"; + line-height: 2.4em; + margin-top: 0; +} + +.md-tabs .md-tabs__item:nth-child(2) { + background: url("../images/userbook.svg") no-repeat; + background-size: 1.4rem; + background-position: left center; + filter: opacity(50%); +} + +.md-tabs .md-tabs__item:nth-child(3) { + background: url("../images/devbook.svg") no-repeat; + background-size: 1.4rem; + background-position: left center; + filter: opacity(50%); +} + +.md-tabs .md-tabs__item:nth-child(4) { + background: url("../images/textbook.svg") no-repeat; + background-size: 1.4rem; + background-position: left center; + filter: opacity(50%); +} + +.md-tabs .md-tabs__item:nth-child(2):hover, +.md-tabs .md-tabs__item:nth-child(3):hover, +.md-tabs .md-tabs__item:nth-child(4):hover { + filter: opacity(75%); +} + +.md-tabs .md-tabs__item--active:nth-child(2) { + background: url("../images/userbook-selected.svg") no-repeat; + background-size: 2.4rem; + background-position: left center; + filter: none; +} + +.md-tabs .md-tabs__item--active:nth-child(3) { + background: url("../images/devbook-selected.svg") no-repeat; + background-size: 2.4rem; + background-position: left center; + filter: none; +} + +.md-tabs .md-tabs__item--active:nth-child(4) { + background: url("../images/textbook-selected.svg") no-repeat; + background-size: 2.4rem; + background-position: left center; + filter: none; +} + +.md-tabs[hidden] .md-tabs__item { + background: none; +} + +.md-tabs__list { + justify-content: center; + gap: 30px; + margin-bottom: 10px; +} + +/* Bigger logo */ +[dir=ltr] .md-header__title { + margin-left: 0; +} + +.md-header__button.md-logo { + margin: 0; + padding: .4rem; +} + +.md-header__button.md-logo img { + height: 2rem; +} + +/* Page background */ +[data-md-color-scheme=default] .md-main, +[data-md-color-scheme=slate] .md-main { + background: transparent; +} + +/* Header & Footer*/ +.md-tabs, +.md-header, +.md-footer { + background: url("../images/watercolor-dark.webp") repeat; + background-attachment: fixed; + background-size: contain; +} + +.md-footer-meta { + background: url("../images/watercolor-dark.webp") repeat; + background-size: contain; +} + +.md-header--shadow { + box-shadow: none; +} + +.md-header__source { + display: none; +} + +.md-header__button:hover { + color: var(--md-primary-fg-color); + opacity: 1; +} + +/* Language logo in the header for sections that have it */ +.md-header__topic svg { + height: 1.5rem; + vertical-align: middle; + fill: var(--md-primary-bg-color); + margin-right: 0.5rem; +} + +/* Separator for source file facts at page bottom */ +aside.md-source-file { + border-top: 1px solid var(--md-primary-fg-color); + padding-top: 0.5rem; + margin-top: 2rem; +} + +/* Footer */ +.md-copyright { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-right: 36px; +} + +/* Links to other sections use the color of that section */ +.md-content a[href*="userbook"][href^='..'], +.gslide-desc a[href*="userbook"][href^='..'] { + color: var(--userbook-a-color); +} + +.md-content a[href*="userbook"][href^='..']:hover, +.gslide-desc a[href*="userbook"][href^='..']:hover { + color: var(--userbook-a-hl-color); +} + +.md-content a[href*="devbook"][href^='..'], +.gslide-desc a[href*="devbook"][href^='..'] { + color: var(--devbook-a-color); +} + +.md-content a[href*="devbook"][href^='..']:hover, +.gslide-desc a[href*="devbook"][href^='..']:hover { + color: var(--devbook-a-hl-color); +} + +.md-content a[href*="textbook"][href^='..'], +.gslide-desc a[href*="textbook"][href^='..'] { + color: var(--textbook-a-color); +} + +.md-content a[href*="textbook"][href^='..']:hover, +.gslide-desc a[href*="textbook"][href^='..']:hover { + color: var(--textbook-a-hl-color); +} + +/* Anchors (links without href target) have no color */ +.md-content a:not([href]) { + color: var(--md-default-fg-color); +} + +/* Add icon indicating external links */ +.md-content .md-typeset a:not(.md-icon) { + &[href^="//"]::after, + &[href^="http://"]::after, + &[href^="https://"]::after { + content: "↗"; + font-size: smaller; + margin-left: .2em; + vertical-align: top; + } +} + +/* Tooltips */ +.md-tooltip__inner, +[role=tooltip]>.md-tooltip2__inner { + font-size: 0.75rem; + font-weight: normal; + background-color: var(--md-default-bg-color--light); + border-radius: 4px; +} + +/* Search box */ +.md-search__form { + background-color: rgba(0, 0, 0, 0.5); + border: 2px solid var(--md-primary-fg-color); + border-radius: 0.4rem; +} + +.md-search__icon svg { + fill: var(--md-primary-fg-color); +} + +.md-search__input::placeholder { + color: var(--md-primary-fg-color); + opacity: 1; +} + +/* Hide definition list terms generated automatically for the reference guide */ +.automatic-reference-term dt { + display: none; +} + +/* Glossaries */ +.md-content dl:not(.automatic-reference-term) { + border: 1px solid var(--md-primary-fg-color); + border-radius: 4px; + overflow: hidden; +} + +.md-content dt { + background-color: var(--md-accent-fg-color--transparent); + padding: 8px; +} + +.md-content dd { + margin-right: 1.875em; +} + +.md-content dt:has(a:target) { + background-color: var(--md-primary-fg-color--light); + display: block; +} + +.md-content dt a:target { + color: var(--md-default-bg-color); + font-weight: bold; +} + +/* Dynamic links */ +.md-typeset .dylink input, +.md-typeset .dylink label { + display: none; +} + +.md-typeset .dylink input[type=radio]:checked+.dylink-option { + display: inline; +} + +/* TypeScript reference guide */ + +#extended-by+ul, #extends+ul { + display: inline; + list-style: none; + margin: 0; +} + +#extended-by+ul li, #extends+ul li { + display: inline; + margin: 0; +} + +/* Java reference decorators inherited from MkDocStrings */ +:root, :host, +[data-md-color-scheme="default"] { + --doc-symbol-parameter-fg-color: #df50af; + --doc-symbol-attribute-fg-color: #953800; + --doc-symbol-function-fg-color: #8250df; + --doc-symbol-method-fg-color: #8250df; + --doc-symbol-class-fg-color: #0550ae; + --doc-symbol-module-fg-color: #5cad0f; + + --doc-symbol-parameter-bg-color: #df50af1a; + --doc-symbol-attribute-bg-color: #9538001a; + --doc-symbol-function-bg-color: #8250df1a; + --doc-symbol-method-bg-color: #8250df1a; + --doc-symbol-class-bg-color: #0550ae1a; + --doc-symbol-module-bg-color: #5cad0f1a; +} + +[data-md-color-scheme="slate"] { + --doc-symbol-parameter-fg-color: #ffa8cc; + --doc-symbol-attribute-fg-color: #ffa657; + --doc-symbol-function-fg-color: #d2a8ff; + --doc-symbol-method-fg-color: #d2a8ff; + --doc-symbol-class-fg-color: #79c0ff; + --doc-symbol-module-fg-color: #baff79; + + --doc-symbol-parameter-bg-color: #ffa8cc1a; + --doc-symbol-attribute-bg-color: #ffa6571a; + --doc-symbol-function-bg-color: #d2a8ff1a; + --doc-symbol-method-bg-color: #d2a8ff1a; + --doc-symbol-class-bg-color: #79c0ff1a; + --doc-symbol-module-bg-color: #baff791a; +} + +code.doc-symbol { + border-radius: .1rem; + font-size: .85em; + padding: 0 .3em; + font-weight: bold; +} + +code.doc-symbol-parameter, +a code.doc-symbol-parameter { + color: var(--doc-symbol-parameter-fg-color); + background-color: var(--doc-symbol-parameter-bg-color); +} + +code.doc-symbol-parameter::after { + content: "param"; +} + +code.doc-symbol-attribute, +a code.doc-symbol-attribute, +code.doc-symbol-variable, +a code.doc-symbol-variable { + color: var(--doc-symbol-attribute-fg-color); + background-color: var(--doc-symbol-attribute-bg-color); +} + +code.doc-symbol-attribute::after, +code.doc-symbol-variable::after { + content: "attr"; +} + +code.doc-symbol-function, +a code.doc-symbol-function { + color: var(--doc-symbol-function-fg-color); + background-color: var(--doc-symbol-function-bg-color); +} + +code.doc-symbol-function::after { + content: "func"; +} + +code.doc-symbol-method, +a code.doc-symbol-method { + color: var(--doc-symbol-method-fg-color); + background-color: var(--doc-symbol-method-bg-color); +} + +code.doc-symbol-method::after { + content: "meth"; +} + +code.doc-symbol-class, +a code.doc-symbol-class { + color: var(--doc-symbol-class-fg-color); + background-color: var(--doc-symbol-class-bg-color); +} + +code.doc-symbol-class::after { + content: "class"; +} + +code.doc-symbol-module, +a code.doc-symbol-module { + color: var(--doc-symbol-module-fg-color); + background-color: var(--doc-symbol-module-bg-color); +} + +code.doc-symbol-module::after { + content: "mod"; +} + +code.doc-symbol-interface, +a code.doc-symbol-interface { + color: var(--doc-symbol-module-fg-color); + background-color: var(--doc-symbol-module-bg-color); +} + +code.doc-symbol-interface::after { + content: "interface"; +} + +.md-code__nav, +:hover>.md-code__nav { + background-color: transparent; +} + +/* Reset admonition text size to normal */ +.md-typeset .admonition, +.md-typeset details { + font-size: inherit; +} + +/* Different color for the Example admonition, because the default is too close + to the textbook's purple. */ +.md-typeset .admonition.example, +.md-typeset details.example { + border-color: var(--example-admonition-color); +} + +.md-typeset .example>.admonition-title, +.md-typeset .example>summary { + background-color: var(--example-admonition-color--transparent); +} + +.md-typeset .example>.admonition-title::before, +.md-typeset .example>summary::before { + background-color: var(--example-admonition-color); +} + +/* Custom borderless admonition, just to help position images. Use with: */ +/* !!! image inline end "" */ +.md-typeset .admonition.image, +.md-typeset details.image { + border: 0; + box-shadow: none; + background-color: transparent; +} + +/* Invert black images so they are visible in dark mode. */ +[data-md-color-scheme=slate] img.invertible { + filter: invert(1) brightness(0.8); +} + +/* GLightBox images and descriptions */ +.md-typeset a.glightbox img { + transition: box-shadow 0.25s; + box-shadow: none; + border-radius: 0.4rem; +} + +.md-typeset a.glightbox img:hover { + transition: box-shadow 0.25s; + box-shadow: 0 0 0 2px var(--md-accent-fg-color); +} + +.glightbox-clean .gcontainer .gslide-description { + background: var(--md-default-bg-color); +} + +.glightbox-clean .gcontainer .gslide-desc { + font-family: "Nunito Sans"; + font-size: inherit; +} + +.glightbox-container .goverlay { + background: var(--md-blanket-color); +} + +.glightbox-container .twemoji svg { + fill: currentcolor; + width: 1.125em; + vertical-align: text-bottom; +} + +/* Tutorial steps */ +.big-number { + font-size: x-large; + color: var(--md-primary-fg-color); +} + +.md-typeset table.tutorial { + border: 0; + font-size: inherit; +} + +.md-typeset table.tutorial tr:hover { + background-color: var(--md-typeset-table-color--light); +} + +.md-typeset .tutorial img { + float: right; + width: 50%; + margin-left: 1em; +} + +.md-typeset .tutorial td { + padding: 1em; + border-top: 1px solid var(--md-default-bg-color--light); + border-bottom: 1px solid var(--md-default-bg-color--light); +} + +/* Tutorial level badge */ +.md-typeset .tutorial_level { + border-radius: 0.5rem; + font-size: initial; + font-weight: normal; + padding: 0 10px; +} + +.md-typeset h1:has(+ .tutorial_level-container) { + margin-bottom: 0.5rem; +} +.md-typeset .tutorial_level-container { + margin: 0 0 1.2em; +} + +[data-md-color-scheme=slate] .md-typeset .tutorial_level-beginner { + background-color: #1F7A3E; + color: #B6F2C8; +} +.md-typeset .tutorial_level-beginner { + background-color: #DFF5E6; + color: #1F7A3E; +} +[data-md-color-scheme=slate] .md-typeset .tutorial_level-intermediate { + background-color: #8A6D1A; + color: #FFF1B8; +} +.md-typeset .tutorial_level-intermediate { + background-color: #FFF4CC; + color: #8A6D1A; +} + +[data-md-color-scheme=slate] .md-typeset .tutorial_level-advanced { + background-color: #8C2F39; + color: #FFD1D6; +} + +.md-typeset .tutorial_level-advanced { + background-color: #FDE2E4; + color: #8C2F39; +} + +/* Links around code blocks */ +.md-typeset p:has(.source-link) { + margin-top: 0; + font-size: small; + float: right; +} + +/* Decorators for links to REST API */ +.md-typeset code.rest-method { + color: white; + border-radius: 0.25rem; + font-size: .5rem; + font-family: sans-serif; + font-weight: bold; + padding: 2px 6px; + line-height: 1; + vertical-align: middle; +} + +[data-md-color-scheme=slate] .md-typeset a code.rest-method-get { + background-color: #2a69a7; +} + +[data-md-color-scheme=slate] .md-typeset a code.rest-method-put { + background-color: #d59d58 +} + +[data-md-color-scheme=slate] .md-typeset a code.rest-method-post { + background-color: #48cb90 +} + +[data-md-color-scheme=default] .md-typeset a code.rest-method-get { + background-color: #61affe; +} + +[data-md-color-scheme=default] .md-typeset a code.rest-method-put { + background-color: #fca130 +} + +[data-md-color-scheme=default] .md-typeset a code.rest-method-post { + background-color: #49cc90 +} + +.md-typeset code.rest-method-ws { + background-color: #b50505; +} + +.md-typeset code.rest-method-req { + background-color: #8e6fc9; +} + +/* Graphviz diagrams */ +.graphviz { + display: block; + margin: auto; +} + +.graphviz text { + fill: var(--md-default-fg-color); + font-family: "Nunito Sans"; +} + +.graphviz .edge text { + fill: var(--md-default-fg-color--light); +} + +.graphviz .edge path { + stroke: var(--md-primary-fg-color--light); +} + +.graphviz .edge polygon { + stroke: var(--md-primary-fg-color--light); + fill: var(--md-primary-fg-color--light); +} + +.graphviz .node path:not([fill="transparent"]), +.graphviz .node polygon:not([fill="transparent"]), +.graphviz .node ellipse:not([fill="transparent"]) { + stroke: var(--md-accent-fg-color--dark); + fill: var(--md-accent-fg-color--transparent); + transition: fill 0.25s; +} + +.graphviz .node:hover a:any-link path, +.graphviz .node:hover a:any-link polygon, +.graphviz .node:hover a:any-link ellipse, +.graphviz .edge:hover a:any-link text { + /* Highlight on hover only if there's a link target */ + fill: var(--md-primary-fg-color--light); + transition: fill 0.25s; +} + +.graphviz .node:hover a:any-link text { + fill: var(--md-primary-fg-color--dark); +} + +.graphviz .cluster polygon { + stroke: var(--md-primary-fg-color--light); +} + +.graphviz .node.metadata polygon { + fill: var(--md-accent-fg-color--light); +} + +/* General tables customization */ +.md-typeset table:not([class]) th { + color: var(--md-primary-fg-color--light); + background: var(--md-accent-fg-color--transparent); +} + +[data-md-color-scheme=default] .md-typeset table:not([class]) th { + color: var(--md-typeset-color); +} + +.centered .md-typeset__table { + display: table; + margin: 0 auto; +} + +.md-typeset .frame-table table:not([class]) { + display: table; + width: 100%; + table-layout: fixed; +} + +/* Tables with subsections */ +.md-typeset .subsections table td:not(:has(strong)):first-child { + padding-left: 2rem; + border-style: hidden; + white-space: nowrap; +} + +/* Tables with an unbroken first column */ + +.md-typeset .keyed-table table td:first-child code { + white-space: nowrap; +} + +.md-typeset .subsections table td:not(:has(strong)):first-child+td { + border-style: hidden; +} + +/* Font for book title in nav bar */ +.md-nav--lifted>.md-nav__list>.md-nav__item>[for] { + font-size: 1rem; + font-family: "Nunito Sans"; + font-weight: normal; +} + +/* Lists representing operations */ +.operation-item { + list-style-type: "⟹"; + padding-left: 0.5rem; +} + +/* Lists that use big icons instead of bullets */ +.md-typeset .icon-list>ul { + list-style: none; + margin-left: 0; +} + +.md-typeset .icon-list>ul>li { + padding-left: 3em; + margin-bottom: 1rem; + position: relative; +} + +.md-typeset .icon-list>ul>li>p:first-child>.twemoji { + position: absolute; + left: 0px; + height: 2em; +} + +.md-typeset .icon-list>ul>li>p:first-child>.twemoji>svg { + width: 2em; +} + +/* Scalar REST API reference */ +.scalar-app { + font-size: 0.8rem; +} + +.light-mode:has(.scalar-app) { + --scalar-color-accent: var(--devbook-color--dark); +} + +.dark-mode:has(.scalar-app) { + --scalar-background-1: var(--md-default-bg-color); + --scalar-color-accent: var(--devbook-color); +} diff --git a/mkdocs/overrides/devbook/accounts/faucet-address.jpg b/mkdocs/overrides/devbook/accounts/faucet-address.jpg new file mode 100644 index 000000000..f34646e46 Binary files /dev/null and b/mkdocs/overrides/devbook/accounts/faucet-address.jpg differ diff --git a/mkdocs/overrides/devbook/accounts/faucet-authorize.jpg b/mkdocs/overrides/devbook/accounts/faucet-authorize.jpg new file mode 100644 index 000000000..1f73d745b Binary files /dev/null and b/mkdocs/overrides/devbook/accounts/faucet-authorize.jpg differ diff --git a/mkdocs/overrides/devbook/accounts/faucet-claim.jpg b/mkdocs/overrides/devbook/accounts/faucet-claim.jpg new file mode 100644 index 000000000..eaeee9beb Binary files /dev/null and b/mkdocs/overrides/devbook/accounts/faucet-claim.jpg differ diff --git a/mkdocs/overrides/devbook/accounts/faucet-open.jpg b/mkdocs/overrides/devbook/accounts/faucet-open.jpg new file mode 100644 index 000000000..eb0908a6d Binary files /dev/null and b/mkdocs/overrides/devbook/accounts/faucet-open.jpg differ diff --git a/mkdocs/overrides/devbook/accounts/faucet-sign-in.jpg b/mkdocs/overrides/devbook/accounts/faucet-sign-in.jpg new file mode 100644 index 000000000..9c13f7688 Binary files /dev/null and b/mkdocs/overrides/devbook/accounts/faucet-sign-in.jpg differ diff --git a/mkdocs/overrides/devbook/accounts/faucet-view-explorer.jpg b/mkdocs/overrides/devbook/accounts/faucet-view-explorer.jpg new file mode 100644 index 000000000..9c7e9fc18 Binary files /dev/null and b/mkdocs/overrides/devbook/accounts/faucet-view-explorer.jpg differ diff --git a/mkdocs/overrides/devbook/accounts/faucet-xem.jpg b/mkdocs/overrides/devbook/accounts/faucet-xem.jpg new file mode 100644 index 000000000..1fb94b25d Binary files /dev/null and b/mkdocs/overrides/devbook/accounts/faucet-xem.jpg differ diff --git a/mkdocs/overrides/main.html b/mkdocs/overrides/main.html new file mode 100644 index 000000000..9f27e4052 --- /dev/null +++ b/mkdocs/overrides/main.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block styles %} +{{ super() }} +{% if page and page.meta and page.meta.section_name %} + +{% else %} + +{% endif %} +{% endblock %} diff --git a/mkdocs/overrides/partials/actions.html b/mkdocs/overrides/partials/actions.html new file mode 100644 index 000000000..dd4453375 --- /dev/null +++ b/mkdocs/overrides/partials/actions.html @@ -0,0 +1,19 @@ +{% if page.edit_url and not page.meta.disable_actions %} + {% if "content.action.edit" in features %} + + {% set icon = config.theme.icon.edit or "material/file-edit-outline" %} + {% include ".icons/" ~ icon ~ ".svg" %} + + {% endif %} + {% if "content.action.view" in features %} + {% if "/blob/" in page.edit_url %} + {% set part = "blob" %} + {% else %} + {% set part = "edit" %} + {% endif %} + + {% set icon = config.theme.icon.view or "material/file-eye-outline" %} + {% include ".icons/" ~ icon ~ ".svg" %} + + {% endif %} +{% endif %} diff --git a/mkdocs/overrides/partials/alternate.html b/mkdocs/overrides/partials/alternate.html new file mode 100644 index 000000000..f26d371f3 --- /dev/null +++ b/mkdocs/overrides/partials/alternate.html @@ -0,0 +1,25 @@ +{#- + Custom language switcher template + Preserve the current page path when switching languages +-#} +
+
+ {% set icon = config.theme.icon.alternate or "material/translate" %} + +
+
    + {% for alt in config.extra.alternate %} +
  • + {% set current_path = page.url | default('', true) %} + {% set new_path = '/' ~ alt.lang ~ '/' ~ current_path %} + + {{ alt.name }} + +
  • + {% endfor %} +
+
+
+
diff --git a/mkdocs/overrides/partials/copyright.html b/mkdocs/overrides/partials/copyright.html new file mode 100644 index 000000000..6b92aa5e0 --- /dev/null +++ b/mkdocs/overrides/partials/copyright.html @@ -0,0 +1,41 @@ + + + \ No newline at end of file diff --git a/mkdocs/overrides/partials/header.html b/mkdocs/overrides/partials/header.html new file mode 100644 index 000000000..2203c0db6 --- /dev/null +++ b/mkdocs/overrides/partials/header.html @@ -0,0 +1,69 @@ +{% set class = "md-header" %} +{% if "navigation.tabs.sticky" in features %} + {% set class = class ~ " md-header--shadow md-header--lifted" %} +{% elif "navigation.tabs" not in features %} + {% set class = class ~ " md-header--shadow" %} +{% endif %} +
+ + {% if "navigation.tabs.sticky" in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} +
diff --git a/mkdocs/overrides/partials/javascripts/content.html b/mkdocs/overrides/partials/javascripts/content.html new file mode 100644 index 000000000..668d8842c --- /dev/null +++ b/mkdocs/overrides/partials/javascripts/content.html @@ -0,0 +1,51 @@ + +{% if "content.tabs.link" in features %} + +{% endif %} + + + diff --git a/mkdocs/overrides/partials/source.html b/mkdocs/overrides/partials/source.html new file mode 100644 index 000000000..e69de29bb diff --git a/mkdocs/package-lock.json b/mkdocs/package-lock.json new file mode 100644 index 000000000..7ef1ca365 --- /dev/null +++ b/mkdocs/package-lock.json @@ -0,0 +1,4485 @@ +{ + "name": "nem-docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nem-docs", + "dependencies": { + "symbol-sdk": "^3.3.2", + "typedoc": "0.28.0", + "typedoc-plugin-markdown": "4.5.0" + }, + "devDependencies": { + "eslint": "^8.50.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-plugin-jsdoc": "^62.0.0" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz", + "integrity": "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.58.0", + "comment-parser": "1.4.6", + "esquery": "^1.7.0", + "jsdoc-type-pratt-parser": "~7.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@es-joy/resolve.exports": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", + "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz", + "integrity": "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.23.0", + "@shikijs/langs": "^3.23.0", + "@shikijs/themes": "^3.23.0", + "@shikijs/types": "^3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/base62": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", + "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, + "node_modules/bitcore-lib": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-11.5.1.tgz", + "integrity": "sha512-/DjKIY6GzCm8YB3HDGdhP6UtNHGC8KjT7tt7eMz2UNk/Z6AQgMZsyhogSiEq4qpWanP8XeJaRcpbccOJX96MRg==", + "license": "MIT", + "dependencies": { + "bech32": "=2.0.0", + "bn.js": "=4.11.8", + "bs58": "^4.0.1", + "buffer-compare": "=1.1.1", + "elliptic": "^6.5.3", + "inherits": "=2.0.1", + "lodash": "^4.17.20" + } + }, + "node_modules/bitcore-lib/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", + "license": "ISC" + }, + "node_modules/bitcore-mnemonic": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/bitcore-mnemonic/-/bitcore-mnemonic-11.5.1.tgz", + "integrity": "sha512-fNSuqDVplJdThD413Y+5RW3ZqhWEZ7ZbVOlIyqPAKg6i2/SkL0d96HbKqjBzmbTERIfetB/fufx5clt87ozi6A==", + "license": "MIT", + "dependencies": { + "bitcore-lib": "^11.5.1", + "unorm": "^1.4.1" + }, + "peerDependencies": { + "bitcore-lib": "*" + } + }, + "node_modules/bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/buffer-compare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-compare/-/buffer-compare-1.1.1.tgz", + "integrity": "sha512-O6NvNiHZMd3mlIeMDjP6t/gPG75OqGPeiRZXoMQZJ6iy9GofCls4Ijs5YkPZZwoysizLiedhticmdyx/GyHghA==" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comment-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz", + "integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + }, + "engines": { + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "62.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.9.0.tgz", + "integrity": "sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.86.0", + "@es-joy/resolve.exports": "1.2.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.6", + "debug": "^4.4.3", + "escape-string-regexp": "^4.0.0", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "html-entities": "^2.6.0", + "object-deep-merge": "^2.0.0", + "parse-imports-exports": "^0.2.4", + "semver": "^7.7.4", + "spdx-expression-parse": "^4.0.0", + "to-valid-identifier": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz", + "integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0", + "peer": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "license": "MIT" + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-deep-merge": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.1.tgz", + "integrity": "sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-crypto-wasm-node": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/symbol-crypto-wasm-node/-/symbol-crypto-wasm-node-0.1.1.tgz", + "integrity": "sha512-gASOhy8+uITSZh5bCYKve5GFtQQ2yQjTFqYNRO4Wq5mH29Ai+3TyKTAq9wZ6bwQfgm7j8U4aqmwFn9WdsXU4eQ==", + "optional": true + }, + "node_modules/symbol-sdk": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/symbol-sdk/-/symbol-sdk-3.3.2.tgz", + "integrity": "sha512-QV8QI4u+tnLHyi1iwsrTbRc5jwKbx4bCAHtdCFZxIzWcH6YggO8OzPrJSUnv8Mkhqt/zpoKYF8HJwF9JmM++XQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~2.2.0", + "@types/node": "^25.0.3", + "bitcore-mnemonic": "~11.5.1", + "ripemd160": "~2.0.2" + }, + "optionalDependencies": { + "symbol-crypto-wasm-node": "^0.1.1" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-valid-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", + "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/base62": "^1.0.0", + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedoc": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.0.tgz", + "integrity": "sha512-UU+xxZXrpnUhEulBYRwY2afoYFC24J2fTFovOs3llj2foGShCoKVQL6cQCfQ+sBAOdiFn2dETpZ9xhah+CL3RQ==", + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.2.1", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.7.0 " + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.5.0.tgz", + "integrity": "sha512-SZ3Nhkl8WE46W2/9OrjHIkXeSi4ZuceQeGxw2kGHyaaooRHYiiHlOWJx6SM0WKjqRUTfhz9T7wSwm3zwPEZndA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", + "license": "MIT or GPL-2.0", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/mkdocs/package.json b/mkdocs/package.json new file mode 100644 index 000000000..96283d235 --- /dev/null +++ b/mkdocs/package.json @@ -0,0 +1,17 @@ +{ + "name": "nem-docs", + "description": "NEM Docs", + "scripts": { + "lint": "eslint snippets --ext .mjs" + }, + "dependencies": { + "symbol-sdk": "^3.3.2", + "typedoc": "0.28.0", + "typedoc-plugin-markdown": "4.5.0" + }, + "devDependencies": { + "eslint": "^8.50.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-plugin-jsdoc": "^62.0.0" + } +} diff --git a/mkdocs/pages/en/404.md b/mkdocs/pages/en/404.md new file mode 100644 index 000000000..cc54608d0 --- /dev/null +++ b/mkdocs/pages/en/404.md @@ -0,0 +1,18 @@ +--- +hide: + - navigation + - toc +disable_actions: true +--- + +
+ +# 404: Page Not Found + +**There are {{ config.extra.nem.page_count }} pages on this site.** + +Congratulations on missing all of them and confusing the wizard. + +![Page not found](site:/assets/images/confused.webp){.off-glb} + +
diff --git a/mkdocs/pages/en/devbook/.meta.yml b/mkdocs/pages/en/devbook/.meta.yml new file mode 100644 index 000000000..cefaa8af0 --- /dev/null +++ b/mkdocs/pages/en/devbook/.meta.yml @@ -0,0 +1 @@ +section_name: devbook diff --git a/mkdocs/pages/en/devbook/accounts/create-from-mnemonic.md b/mkdocs/pages/en/devbook/accounts/create-from-mnemonic.md new file mode 100644 index 000000000..e5da0502a --- /dev/null +++ b/mkdocs/pages/en/devbook/accounts/create-from-mnemonic.md @@ -0,0 +1,119 @@ +--- +title: Create from Mnemonic +tutorial_level: beginner +--- + +# Creating Accounts from Mnemonics + +This tutorial shows how to create for the NEM blockchain using a , +also known simply as _mnemonic_. + +This approach is commonly used by to manage multiple accounts from a single seed. + +## Prerequisites + +If you have not done so already, start with [Setting Up a Development Environment](../start/setup.md). + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/accounts/create_from_mnemonic', ['py', 'js']) }} + +## Code Explanation + +### Initializing the Facade + +{{ tutorial.code_snippet_tagged('step-1') }} + +The provides access to NEM's cryptographic operations and network utilities. +It is initialized with a network name (`testnet` or `mainnet`) to ensure that network-specific values, +such as , are generated correctly. + +### Defining a Mnemonic + +{{ tutorial.code_snippet_tagged('step-2') }} + +The example checks for an existing mnemonic in the `MNEMONIC` environment variable. +If the variable is set, the mnemonic is loaded from it. +Otherwise, a new random mnemonic is generated using . + +NEM uses the [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) standard, which represents +mnemonics as 24 English words selected from a standardized word list. +These words encode the entropy (randomness) used to create all derived private keys. + +!!! warning "Store your mnemonic phrase securely" + The mnemonic phrase can be used to regenerate all derived accounts and private keys. + Anyone with access to it can control your accounts, and losing it means losing access permanently. + + Never share your mnemonic with anyone, and always store it in a secure location. + +### Deriving the Root Node + +{{ tutorial.code_snippet_tagged('step-3') }} + +After defining the mnemonic, converts the mnemonic and a password into a root node, +which serves as the starting point for deriving child accounts. + +The password (sometimes called a "25th word") is an optional string that extends the mnemonic seed. +It can be left empty or set to any value. +When used, it adds another layer of security. +Different passwords with the same mnemonic produce completely different accounts. + +!!! note "Password security" + The password is part of the account derivation. + Both the mnemonic and password are required to regenerate the accounts. + If you lose either one, you lose access to all derived accounts. + +In this example, the password is loaded from the `PASSWORD` environment variable. +If not set, the snippet uses a default one. + +### Deriving the Child Account + +{{ tutorial.code_snippet_tagged('step-4') }} + +The root node can generate multiple accounts, each with its own unique keys and address. +This allows a single mnemonic to manage many accounts while keeping them cryptographically isolated. + +Deriving an account requires specifying an account index. + generates the derivation path +(a standardized string that specifies which account to derive) for that index, +and follows that path to create the account. + +In this example, the account at index `0` is derived. +Additional accounts can be derived by using different indices (e.g., `1`, `2`, `3`, ...). +Each index produces a completely different account. + +### Creating the Account + +{{ tutorial.code_snippet_tagged('step-5') }} + +Once the child node is derived, it is converted into a usable key pair and address. + +1. **Key pair creation:** extracts the and from the + child node. + The private key must remain secret, while the public key can be safely shared. + +2. **Address derivation:** converts the public key into an , a shorter, + human-readable, network-specific identifier for the account. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text +--8<-- 'devbook/accounts/create_from_mnemonic.log' +``` + +Each time the code runs without environment variables, it generates a different random mnemonic and account. +If the same mnemonic and password are provided, the same account is always derived for the given account index. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| [Create a random mnemonic](#defining-a-mnemonic) | | +| [Derive an account from a mnemonic](#deriving-the-root-node) | , , and | +| [Get the key pair of the account](#creating-the-account) | , | diff --git a/mkdocs/pages/en/devbook/accounts/create-from-private-key.md b/mkdocs/pages/en/devbook/accounts/create-from-private-key.md new file mode 100644 index 000000000..b619e008e --- /dev/null +++ b/mkdocs/pages/en/devbook/accounts/create-from-private-key.md @@ -0,0 +1,86 @@ +--- +title: Create from Private Keys +tutorial_level: beginner +--- + +# Creating Accounts from Private Keys + +This tutorial shows how to create for the NEM blockchain, either by using an existing +or by generating a new random account. + +## Prerequisites + +If you have not done so already, start with [Setting Up a Development Environment](../start/setup.md). + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/accounts/create_from_private_key', ['py', 'js']) }} + +## Code Explanation + +### Initializing the Facade + +{{ tutorial.code_snippet_tagged('step-1') }} + +The provides access to NEM's cryptographic operations and network utilities. +It is initialized with a network name (`testnet` or `mainnet`) to ensure that network-specific values, +such as , are generated correctly. + +### Defining a Private Key + +{{ tutorial.code_snippet_tagged('step-2') }} + +The example starts by retrieving a private key from the environment variable `PRIVATE_KEY` as a hexadecimal string. +If the variable is set, the value is converted into a object. +Otherwise, a new random private key is generated using instead. + +!!! warning "Store your private key securely" + The private key gives full control over the account and any assets it holds. + If you lose the private key, you lose access to the account permanently. + If someone else obtains the private key, they can control the account. + + Never share your private key with anyone, and always store it in a secure location. + +### Creating the Account + +{{ tutorial.code_snippet_tagged('step-3') }} + +After defining the private key, an account is created by deriving its public key and address. + +1. **Key pair creation:** The constructor takes the private key and mathematically derives the + corresponding . + While the private key must remain secret, the public key can be safely shared with anyone. + +2. **Address derivation:** The method converts the public key into an + , a shorter, human-readable, network-specific identifier for the account. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text +--8<-- 'devbook/accounts/create_from_private_key.log' +``` + +Each time the program runs without the environment variable, it generates a different random account. +If a private key is provided, the same public key and address are always derived. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ------------------------------------------------------------- | ---------------------------------------- | +| [Load a private key](#defining-a-private-key) | | +| [Create a random private key](#defining-a-private-key) | | +| [Get the public key](#creating-the-account) | | +| [Get the address](#creating-the-account) | | + +## Next Steps + +Now that you have an account, you can: + +* [Get testnet funds from the faucet](./testnet-faucet.md) +* [Send your first transaction](../transactions/transfer-xem.md) diff --git a/mkdocs/pages/en/devbook/accounts/query-balance.md b/mkdocs/pages/en/devbook/accounts/query-balance.md new file mode 100644 index 000000000..11f750bb3 --- /dev/null +++ b/mkdocs/pages/en/devbook/accounts/query-balance.md @@ -0,0 +1,100 @@ +--- +title: Query Account Balance +tutorial_level: beginner +--- + +# Querying an Account Balance + + on NEM can hold (fungible tokens), including the native currency . + +This tutorial shows how to query an account's mosaic balances and display NEM's whole-number +[atomic amounts](../../textbook/mosaics.md#divisibility) in decimal form. + +## Prerequisites + +This tutorial uses the [NEM REST API](../reference/rest/nem.md) without requiring an . +You only need a way to make HTTP requests. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/accounts/query_balance', ['py', 'js']) }} + +The snippet uses the `NODE_URL` environment variable to set the NEM API node. +If no value is provided, a default one is used. + +The tutorial defines the following functions: + +* {{ tutorial.var('get_mosaic_balances()') }}: Fetches all owned by an account. +* {{ tutorial.var('get_mosaic_definitions()') }}: Fetches mosaic definitions, including . +* {{ tutorial.var('format_amount()') }}: Formats amounts with the appropriate number of decimal places, according to + their . + +## Code Explanation + +### Fetching Mosaic Balances + +{{ tutorial.code_snippet_tagged('step-2') }} + +The endpoint returns every mosaic the account holds, together with its quantity in +_atomic units_. + +### Fetching Mosaic Definitions + +{{ tutorial.code_snippet_tagged('step-3') }} + +To format mosaic balances correctly, the snippet fetches their definitions from the network. +The key property required is , which defines how many decimal places a mosaic supports. + +The endpoint returns the definition for every mosaic owned by the account in a +single request, including divisibility and other properties. + +### Formatting Amounts + +{{ tutorial.code_snippet_tagged('step-4') }} + +This utility function converts _atomic_ amounts into human-friendly representations: + +* **Atomic amount:** The raw value stored on the blockchain, expressed as an integer. +* **Formatted amount:** The display format with decimal places determined by the mosaic's divisibility. + +The formatting splits the atomic amount into whole and fractional parts by dividing and taking the remainder with +respect to \(10^{\text{divisibility}}\). +The fractional part is then zero-padded to ensure it always displays the correct number of decimal places. + +### Putting It All Together + +{{ tutorial.code_snippet_tagged('step-5') }} + +The main code reads the `ADDRESS` environment variable to determine which account to query. +If no value is provided, it uses a default sample address. + +It orchestrates the helper functions to: + +1. Fetch the mosaic balances for the account. +2. Retrieve the mosaic definitions to determine each mosaic's divisibility. +3. Iterate through each mosaic and format its balance with the appropriate number of decimal places. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text +--8<-- 'devbook/accounts/query_balance.log' +``` + +The output displays all mosaics the account holds. Notice how different mosaics have different divisibility values: + +* The first mosaic is `nem:xem`, the network's native currency, which has divisibility 6 and is therefore displayed with + six decimal places (`9883.200000`). +* The second mosaic is `company:token`, a user-defined mosaic with divisibility 0, displayed as an integer (`1000000`). + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| -------------------------------------------------------- | ------------------------------------------------------------- | +| [Fetch mosaic balances](#fetching-mosaic-balances) | | +| [Fetch mosaic definitions](#fetching-mosaic-definitions) | | diff --git a/mkdocs/pages/en/devbook/accounts/testnet-faucet.md b/mkdocs/pages/en/devbook/accounts/testnet-faucet.md new file mode 100644 index 000000000..a6d96a8ab --- /dev/null +++ b/mkdocs/pages/en/devbook/accounts/testnet-faucet.md @@ -0,0 +1,90 @@ +--- +title: Fund via Faucet +tutorial_level: beginner +--- + +# Getting Testnet Funds from the Faucet + +The NEM provides a faucet that distributes free to developer for testing purposes. +This guide explains how to claim testnet funds using the web-based faucet. + +!!! note + Testnet XEM has no real-world value. + It exists only to let you experiment with NEM features without using real currency. + + If you need XEM, you will need to buy it through an + [exchange](https://coinmarketcap.com/currencies/nem/#Markets). + +## Prerequisites + +Before you start, make sure to: + +* Create a testnet to receive funds, either + [from code](../accounts/create-from-private-key.md) or + [by using a wallet](../../userbook/wallet/create-account.md). +* Have an 𝕏 account to verify your identity with the faucet. + +## How to Claim Testnet Funds + +{% import 'tutorial.jinja2' as tutorial %} + +{{ tutorial.list_begin() }} + +{{ tutorial.step_begin("faucet-open.jpg") }} +Open your web browser and navigate to the NEM testnet faucet at [testnet.nem.tools](https://testnet.nem.tools). +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-sign-in.jpg") }} +Click **Sign in with Twitter** (now 𝕏) and follow the authentication flow. + +This step limits the amount of test funds to 10,000 XEM per account, to help prevent abuse of the faucet. +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-authorize.jpg") }} +After signing in, 𝕏 will ask you to authorize the faucet application to access your account information. + +Review the permissions and click **Authorize app** to continue. +Once authorized, you will be redirected again to the faucet. +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-address.jpg") }} +Enter the address where you want to receive the funds in the **Your Testnet Address** field. + +Make sure the address starts with `T`, meaning it is a testnet account. +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-xem.jpg") }} +In the **XEM Amount** field, specify how much XEM you want to claim. +The maximum amount per request is 10,000 XEM. +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-claim.jpg") }} +Click **Claim** to submit your request. +If the request is successful, the faucet will transfer the specified amount of XEM to your address. +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-view-explorer.jpg") }} +Click **View in Explorer** in the top-right corner notification to verify that the transaction was processed. + +The explorer will display the transaction details, including its confirmation status. +The transaction should confirm in less than a minute under normal network conditions. + +You can also monitor the transfer from your if you have one set up. +{{ tutorial.step_end() }} + +{{ tutorial.list_end() }} + +## Returning Funds to the Faucet + +When you are done testing, consider returning unused XEM back to the faucet. +The faucet address is the same address that sent you the funds. + +You can find the sender address by checking the transaction in the [blockchain explorer](https://testnet.nem.fyi/) +or by searching your account transaction history. + +Better yet, use the faucet address as the recipient for your test transactions. +This way, you practice sending transactions while helping keep the faucet stocked for other developers. + +## Next Steps + +Why not try [sending a transfer transaction](../transactions/transfer-xem.md)? diff --git a/mkdocs/pages/en/devbook/chain/chain-heights.md b/mkdocs/pages/en/devbook/chain/chain-heights.md new file mode 100644 index 000000000..916af2c86 --- /dev/null +++ b/mkdocs/pages/en/devbook/chain/chain-heights.md @@ -0,0 +1,110 @@ +--- +title: Chain and Irreversible Height +tutorial_level: beginner +--- + +# Querying Chain and Irreversible Height + +The endpoint returns the current chain height. + +The **irreversible height** is the highest block that can no longer be rolled back. +On NEM, it is calculated by subtracting the from the current chain height. + +This tutorial shows how to poll the chain height in a loop, calculate the irreversible height, and track how long ago +the chain height last changed. + +## Prerequisites + +This tutorial uses the [NEM REST API](../reference/rest/nem.md) without requiring an SDK. +You only need a way to make HTTP requests. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/chain/chain_heights', ['py', 'js']) }} + +The snippet uses the `NODE_URL` environment variable to set the NEM API node. +If no value is provided, a default one is used. + +The program runs in an infinite loop, printing a status line every second. +A keyboard interrupt (`Ctrl+C`) stops the loop. + +## Code Explanation + +### Fetching Chain Height + +{{ tutorial.code_snippet_tagged('step-1') }} + +On each iteration, the code sends a `GET` request to the endpoint. +The response contains a single `height` field with the current chain height, the latest block known to the node. + +The chain height increases each time a new block is produced (approximately every 60 seconds). + +### Calculating the Irreversible Height + +{{ tutorial.code_snippet_tagged('step-2') }} + +The is the maximum number of blocks a rollback can undo on NEM, set to **360 blocks** (approximately +six hours). + +Subtracting the rewrite limit from the current chain height gives the **irreversible height**. + +Any block at or below the irreversible height can no longer be rolled back. + +See the [Consensus](../../textbook/consensus.md#conflict-resolution) textbook section for details on rollbacks and the +rewrite limit. + +### Tracking Height Changes + +{{ tutorial.code_snippet_tagged('step-3') }} + +To show how long ago the chain height last changed, the code stores the previous height and the time at which it was +last updated. + +Whenever a new block arrives and the height changes, the timestamp is refreshed. +The elapsed time is then displayed alongside the current chain height. + +### Polling Loop + +{{ tutorial.code_snippet_tagged('step-4') }} + +Each iteration prints a single status line showing: + +* The current chain height and how many seconds have elapsed since it last changed. +* The irreversible height. + +The loop then sleeps for one second before querying the node again. + +## Output + +The following output shows a typical run monitoring the chain height and the irreversible height: + +```text linenums="1" hl_lines="5" +--8<-- 'devbook/chain/chain_heights.log' +``` + +Some highlights from the output: + +* **Before a new block** (lines 2 to 4): The chain height remains unchanged while the program continues polling. +* **A new block arrives** (line 5): The chain height advances from `659,471` to `659,472`. + The irreversible height advances as well. + +!!! note "Chain height vs. irreversible height" + + The irreversible height always trails the chain height by the . + Transactions near the chain tip may still be rolled back. + Once their block falls below the rewrite limit, they become irreversible. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| -------------------------------------------- | --------------------- | +| [Fetch chain height](#fetching-chain-height) | | + +## Next Steps + +For an event-driven approach to monitoring new blocks, see the +[Listening to New Blocks](../websockets/listen-new-blocks.md) WebSocket tutorial. diff --git a/mkdocs/pages/en/devbook/intro.md b/mkdocs/pages/en/devbook/intro.md new file mode 100644 index 000000000..0f477b237 --- /dev/null +++ b/mkdocs/pages/en/devbook/intro.md @@ -0,0 +1,37 @@ +--- +title: Welcome +--- + +# Welcome to the Developer Manual + +The developer manual is for developers building applications on NEM. +It provides code examples in multiple programming languages, +showing how to perform common tasks with the or the HTTP API. + +The manual is structured as follows: + +
+ +* :material-laptop: **Getting Started** + + Set up your development machine and run a quick `Hello World` sample to check that everything is ready. + +* :material-school: **Tutorials** + + Follow task-focused tutorials grouped by area. + Each tutorial links to the [textbook](../textbook/intro.md) and the relevant reference guides when background + information is useful. + +* :material-book-open-page-variant: **Reference Guides** + + Consult exhaustive information about SDK methods, HTTP and WebSockets endpoints, and binary structures. + +
+ +Use the navigation menu, or jump directly into one of the tutorials below. + +Tutorials are grouped by level, from beginner to advanced, based on the required familiarity with NEM concepts. + +{% import 'tutorials_table.jinja2' as tutorials_table with context %} + +{{ tutorials_table.render() }} diff --git a/mkdocs/pages/en/devbook/mosaics/get-mosaic-info.md b/mkdocs/pages/en/devbook/mosaics/get-mosaic-info.md new file mode 100644 index 000000000..f2730a5b3 --- /dev/null +++ b/mkdocs/pages/en/devbook/mosaics/get-mosaic-info.md @@ -0,0 +1,115 @@ +--- +title: Get Mosaic Information +tutorial_level: beginner +--- + +# Getting Mosaic Information + +Every on NEM has a set of on-chain properties such as supply, divisibility, and transfer rules. + +This tutorial shows how to retrieve a mosaic's properties and its current supply. + +## Prerequisites + +This tutorial only reads data from the network. No is required. + +Before you start, make sure to [set up your development environment](../start/setup.md). + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/mosaics/get_mosaic_info', ['py', 'js']) }} + +The snippet uses the `NODE_URL` environment variable to set the NEM API node. +If no value is provided, a default node is used. + +The `MOSAIC_ID` environment variable specifies which mosaic to query, given as its +[fully qualified name](../../textbook/mosaics.md#fully-qualified-name). +If not set, it defaults to the mosaic (`nem:xem`). + +## Code Explanation + +### Fetching Mosaic Information + +{{ tutorial.code_snippet_tagged('step-1') }} + +The endpoint retrieves the definition of a mosaic, including: + +* **Description:** Human-readable text [describing](../../textbook/mosaics.md#description) the mosaic. +* **Creator:** The of the account that created the mosaic. +* **Properties:** The mosaic's [behavioral properties](../../textbook/mosaics.md#properties): + * **[Divisibility](../../textbook/mosaics.md#divisibility):** The number of decimal places the mosaic supports. + For example, XEM has a divisibility of `6`, meaning 1 XEM equals 1'000'000 atomic units. + * **[Initial supply](../../textbook/mosaics.md#initial-supply):** The supply at creation time, expressed in + whole units. + * **[Supply mutability](../../textbook/mosaics.md#supply-mutability):** Whether the creator can change the + supply after creation. + * **[Transferability](../../textbook/mosaics.md#transferability):** Whether the mosaic can be freely sent + between accounts or only to and from the creator. +* **Levy:** An optional [extra fee](../../textbook/mosaics.md#levy) paid to a third account whenever the mosaic is + transferred. + +### Fetching the Current Supply + +{{ tutorial.code_snippet_tagged('step-2') }} + +The definition only records the **initial** supply. +For mosaics with mutable supply, the current value can differ, so the endpoint returns the supply +currently in circulation, expressed in [whole units](../../textbook/mosaics.md#divisibility). + +### Converting to Atomic Units + +{{ tutorial.code_snippet_tagged('step-3') }} + +The endpoint reports supply in whole units, but transaction quantities are expressed in +[atomic units](../../textbook/mosaics.md#divisibility). +To convert from whole to atomic units, the code multiplies the supply by 10 raised to the mosaic's divisibility. + +For XEM (divisibility `6`), a supply of `8'999'999'999` whole units equals `8'999'999'999'000'000'` atomic units. + +## Output + +The output shown below corresponds to a typical run of the program, querying the XEM mosaic on testnet. + +```text linenums="1" hl_lines="5 6 7 8 9 10 11 12 15 17" +--8<-- 'devbook/mosaics/get_mosaic_info.log' +``` + +Some highlights from the output: + +* **Mosaic ID** (line 5): The XEM mosaic identifier, the fully qualified name `nem:xem`. + +* **Description** (line 6): A human-readable label set when the mosaic was created. + +* **Creator** (line 7): The public key of the account that created the mosaic. + +* **Divisibility** (line 8): The value `6` means 1 XEM = 1,000,000 (10^6^) atomic units. + +* **Initial supply** (line 9): The supply at creation time, in whole units. + +* **Supply mutable** (line 10): The value `false` means the XEM supply can never change. + +* **Transferable** (line 11): The value `true` means XEM can be freely sent between accounts. + +* **Levy** (line 12): XEM transfers carry no additional mosaic fee. + +* **Current supply** (line 15): The supply currently in circulation, identical to the initial supply because XEM + is not mutable. + +* **Supply in atomic units** (line 17): The supply converted from whole units to atomic units using the mosaic's + divisibility. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| -------------------------------------------------------------- | ------------------------ | +| [Fetch the mosaic definition](#fetching-mosaic-information) | | +| [Fetch the current supply](#fetching-the-current-supply) | | + +## Next Steps + +* [Transfer mosaics](../transactions/transfer-mosaics.md) to send a mosaic between accounts +* [Query an account balance](../accounts/query-balance.md) to see how much of a mosaic an account holds diff --git a/mkdocs/pages/en/devbook/namespaces/get-namespace-info.md b/mkdocs/pages/en/devbook/namespaces/get-namespace-info.md new file mode 100644 index 000000000..50d409b4b --- /dev/null +++ b/mkdocs/pages/en/devbook/namespaces/get-namespace-info.md @@ -0,0 +1,121 @@ +--- +title: Get Namespace Information +tutorial_level: beginner +--- + +# Getting Namespace Information + +This tutorial shows how to retrieve a 's properties, its , and the defined under +it. + +## Prerequisites + +This tutorial only reads data from the network. No account is required. + +Before you start, make sure to [set up your development environment](../start/setup.md). + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/namespaces/get_namespace_info', ['py', 'js']) }} + +The snippet uses the `NODE_URL` environment variable to set the NEM API node. +If no value is provided, a default node is used. + +The `NAMESPACE_NAME` environment variable specifies which namespace to query, given as its full dot-separated +[name](../../textbook/namespaces.md#name) like `foo` or `foo.bar`. +If not set, it defaults to `company`, a registered on testnet. + +## Code Explanation + +### Fetching Namespace Information + +{{ tutorial.code_snippet_tagged('step-1') }} + +The endpoint retrieves the current properties of a namespace, including: + +* **Name:** The complete dot-separated [identifier](../../textbook/namespaces.md#name) of the namespace, + from the root down to the queried level. + For example, `foo` is a and `foo.bar` is a of `foo`. + + `fqn` in the returned data structure stands for _Fully-Qualified Name_. + +* **Owner:** The of the account that [registered the namespace](../../textbook/namespaces.md#ownership). + +* **Height:** The height at which the current ownership began. + +### Computing the Lease Expiration + +{{ tutorial.code_snippet_tagged('step-2') }} + +Namespaces are not owned permanently. +A root namespace is [leased](../../textbook/namespaces.md#duration) for 525600 blocks (approximately one year) +and must be renewed before it expires. +Subnamespaces are not leased individually, as they expire together with their root namespace. + +The expiration height is not part of the API response, but it can be derived by adding the lease duration to the +namespace's height. +Comparing it with the current chain height, returned by , gives the number of blocks remaining before +the namespace expires. + +### Listing Subnamespaces + +{{ tutorial.code_snippet_tagged('step-3') }} + +There is no endpoint that returns the children of a namespace directly. +However, because [subnamespaces always share the owner](../../textbook/namespaces.md#ownership) of their root +namespace, they can be found by querying the namespaces owned by that account. + +The endpoint returns the namespaces owned by an account, and its optional `parent` +parameter restricts the results to subnamespaces of a given namespace. +Using the namespace owner obtained in the previous step and the queried namespace as the `parent` value returns its +subnamespaces. + +### Listing the Namespace's Mosaics + +{{ tutorial.code_snippet_tagged('step-4') }} + +Mosaics are always [defined under a namespace](../../textbook/mosaics.md#fully-qualified-name), which acts as a prefix +grouping related mosaics together. + +The endpoint returns one definition for each mosaic whose namespace matches +the queried name exactly. +Mosaics defined under deeper subnamespaces (such as `foo.bar:baz` when querying `foo`) are not included. +To list those as well, repeat this query for each subnamespace found in the previous step. + +## Output + +The output shown below corresponds to a typical run of the program, querying the `company` namespace on testnet. + +```text linenums="1" hl_lines="5 6 7 9 10 11 14 15 18 19" +--8<-- 'devbook/namespaces/get_namespace_info.log' +``` + +Some highlights from the output: + +* **Namespace name** (line 5): The queried namespace, `company`. + Because it contains no dots, it is a root namespace. + +* **Owner** (line 6): The account that currently owns the namespace. + +* **Height** (line 7): The block height at which the current ownership period began. + +* **Lease expiration** (lines 9-11): The expiration height is the ownership height plus the lease duration of + 525600 blocks. + Subtracting the current chain height shows how many blocks remain before expiration. + +* **Subnamespaces** (lines 14-15): One subnamespace exists under `company`: `company.division`. + +* **Mosaics** (lines 18-19): One mosaic is defined directly under the namespace: `company:token`. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ---------------------------------------------------------------- | ---------------------------------------- | +| [Fetch namespace properties](#fetching-namespace-information) | | +| [Compute the lease expiration](#computing-the-lease-expiration) | | +| [List subnamespaces](#listing-subnamespaces) | | +| [List the namespace's mosaics](#listing-the-namespaces-mosaics) | | diff --git a/mkdocs/pages/en/devbook/network-currency/query-block-rewards.md b/mkdocs/pages/en/devbook/network-currency/query-block-rewards.md new file mode 100644 index 000000000..fd69f8ea9 --- /dev/null +++ b/mkdocs/pages/en/devbook/network-currency/query-block-rewards.md @@ -0,0 +1,107 @@ +--- +title: Query Block Rewards +tutorial_level: beginner +--- + +# Querying Block Rewards + +Each on NEM is produced by a single . +The entire reward for harvesting a block comes from the fees collected in that block, and these fees are +paid in full to the harvester that produced it. + +This tutorial shows how to query any block, identify its harvester, and sum the transaction fees that form the reward. + +## Prerequisites + +Before you start, [set up your development environment](../start/setup.md). + +This tutorial only reads data from the network. No or balance is required. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/network-currency/query_block_rewards', ['py', 'js']) }} + +The snippet uses the `NODE_URL` environment variable to set the NEM API node. +If no value is provided, a default node is used. + +The `BLOCK_HEIGHT` environment variable selects which block to query. +If not set, it defaults to `661258`, a block harvested on testnet. + +## Code Explanation + +The code fetches a block by height and derives the harvester address from the block's signer public key. +It then sums the fees of every transaction in the block to obtain the total reward. + +### Fetching Block Information + +{{ tutorial.code_snippet_tagged('step-1') }} + +The endpoint returns information about the block at the requested height, +including the list of transactions present in the block. + +### Identifying the Harvester + +{{ tutorial.code_snippet_tagged('step-2') }} + +The `signer` field holds the of the account that harvested the block. +The method converts this public key into the corresponding testnet . + +!!! info "The signer is not always the account that earns the reward" + + With , the `signer` is the and receives the reward. + + With or , the `signer` is a , while the + reward is paid to the
. + +### Summing the Transaction Fees + +{{ tutorial.code_snippet_tagged('step-3') }} + +Each transaction in the block has a `fee` field expressed in atomic units. +XEM has a of 6, so `350000` atomic units represent `0.350000` XEM. + +Adding the fees of every transaction gives the total reward for the block. + +### Calculating the Total Reward + +{{ tutorial.code_snippet_tagged('step-4') }} + +The total block reward equals the sum of all transaction fees, paid in full to the harvester. +An empty block has no fees, and therefore no reward. + +!!! note "Alternative: Query rewards by account" + + This tutorial calculates the reward for a specific block by summing the transaction fees it contains. + + If you are interested in the rewards earned by a particular account instead, use the + endpoint. + It returns one entry per harvested block, including a `totalFee` field with the reward earned for that block. + + The endpoint accepts the address of the , the account that earns the reward. + With or , that is the main account, not the remote account that signed. + + A remote account address also returns the blocks it signed on behalf of the main account. + +## Output + +The following output shows a typical run querying the rewards for block 661,258: + +```text linenums="1" hl_lines="4 7-8 10" +--8<-- 'devbook/network-currency/query_block_rewards.log' +``` + +Some highlights from the output: + +* **Harvester** (line 4): The address derived from the block's `signer` public key. +* **Transaction fees** (lines 7 to 8): The fee paid by each transaction included in the block. +* **Total block reward** (line 10): The sum of all transaction fees paid in full to the harvester. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| --------------------------------------------------------- | ------------------------------------------ | +| [Fetch block information](#fetching-block-information) | | diff --git a/mkdocs/pages/en/devbook/network-currency/query-currency-supply.md b/mkdocs/pages/en/devbook/network-currency/query-currency-supply.md new file mode 100644 index 000000000..85fed54c2 --- /dev/null +++ b/mkdocs/pages/en/devbook/network-currency/query-currency-supply.md @@ -0,0 +1,113 @@ +--- +title: Query Currency Supply +tutorial_level: beginner +--- + +# Querying Currency Supply + +Exchanges and market data aggregators need accurate supply figures to display market capitalization and token metrics. + +NEM exposes the supply of , the native currency, through the REST API. +This tutorial shows how to query the total supply and derive the circulating supply from it. + +## Prerequisites + +This tutorial uses the [NEM REST API](../reference/rest/nem.md) without requiring an SDK. +You only need a way to make HTTP requests. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/network-currency/query_currency_supply', ['py', 'js']) }} + +The snippet uses the `NODE_URL` environment variable to set a NEM node. + +!!! note "Why mainnet?" + Other tutorials typically run against to avoid spending real funds. + This one is different because it queries fixed mainnet account addresses to calculate the circulating supply, so + `NODE_URL` must point to a mainnet node. + +## Code Explanation + +### Fetching the Total Supply + +{{ tutorial.code_snippet_tagged('step-1') }} + +The total supply of is fixed. +All 8'999'999'999 XEM were created in the and no new XEM is ever minted. + +This tutorial reads values like the supply and divisibility from the API rather than hard-coding them, so the same +approach also works for other , including those whose supply can change. + +The code sends a `GET` request to the endpoint, passing the XEM mosaic identifier `nem:xem` as +the `mosaicId` query parameter. + +The response is a JSON object with the mosaic identifier and its current `supply`, expressed in +[whole units](../../textbook/mosaics.md#divisibility). + +### Reading the Mosaic's Divisibility + +{{ tutorial.code_snippet_tagged('step-2') }} + +The supply fetched in the previous step is already in whole units, but the account balances read in the next steps are +reported in [atomic units](../../textbook/mosaics.md#divisibility). + +To convert values to the same units, this step first fetches the mosaic's . +This value is then used to convert the balances from atomic units to whole units. + +The endpoint returns the mosaic's definition, which includes the divisibility. +For `nem:xem`, the divisibility is 6. + +### Fetching the Non-Circulating Supply + +{{ tutorial.code_snippet_tagged('step-3') }} + +A portion of the total supply is held by accounts that are not part of the open market: + +* **Treasury:** A reserve account that holds team-controlled XEM. +* **Nemesis:** The account that signed the nemesis block. + It cannot send transactions after the nemesis block, so any XEM held by this account is effectively out of + circulation. +* **Namespace rental sink:** Collects the fees paid to register . +* **Mosaic rental sink:** Collects the fees paid to create . + +The code queries each account with the endpoint and sums their balances. +Each balance is divided by 10^divisibility^, using the divisibility value fetched in the previous step, to convert it +from atomic units to whole units. +For `nem:xem`, this divisor is 1'000'000. + +### Deriving the Circulating Supply + +{{ tutorial.code_snippet_tagged('step-4') }} + +The circulating supply is the total supply minus the non-circulating balances. +This is the amount of XEM that is freely available on the open market. + +## Output + +The following output shows a typical run querying the currency supply: + +```text linenums="1" hl_lines="2 7 8" +--8<-- 'devbook/network-currency/query_currency_supply.log' +``` + +The output shows the full breakdown of the XEM supply: + +* **Total supply** (line 2): All the XEM that exists. +* **Non-circulating supply** (line 7): The sum of the treasury, nemesis, and rental sink balances. +* **Circulating supply** (line 8): The XEM actually available in circulation. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| -------------------------------------------------------------------- | ------------------------ | +| [Fetch total supply](#fetching-the-total-supply) | | +| [Read the mosaic divisibility](#reading-the-mosaics-divisibility) | | +| [Fetch non-circulating supply](#fetching-the-non-circulating-supply) | | + +## Next Steps + +To check a specific account's XEM balance, see the [Query Account Balance](../accounts/query-balance.md) tutorial. diff --git a/mkdocs/pages/en/devbook/reference/.meta.yml b/mkdocs/pages/en/devbook/reference/.meta.yml new file mode 100644 index 000000000..21fca99fd --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/.meta.yml @@ -0,0 +1 @@ +disable_actions: true diff --git a/mkdocs/pages/en/devbook/reference/java/.meta.yml b/mkdocs/pages/en/devbook/reference/java/.meta.yml new file mode 100644 index 000000000..b00c993b7 --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/java/.meta.yml @@ -0,0 +1 @@ +language_icon: fontawesome/brands/java diff --git a/mkdocs/pages/en/devbook/reference/py/.meta.yml b/mkdocs/pages/en/devbook/reference/py/.meta.yml new file mode 100644 index 000000000..c574be72a --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/py/.meta.yml @@ -0,0 +1 @@ +language_icon: simple/python diff --git a/mkdocs/pages/en/devbook/reference/rest/.gitignore b/mkdocs/pages/en/devbook/reference/rest/.gitignore new file mode 100644 index 000000000..1cda54be9 --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/rest/.gitignore @@ -0,0 +1 @@ +*.yml diff --git a/mkdocs/pages/en/devbook/reference/rest/nem.md b/mkdocs/pages/en/devbook/reference/rest/nem.md new file mode 100644 index 000000000..0ef4a4b20 --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/rest/nem.md @@ -0,0 +1,65 @@ +--- +hide: + - toc +--- + +
+ + + + + diff --git a/mkdocs/pages/en/devbook/reference/serialization/index.md b/mkdocs/pages/en/devbook/reference/serialization/index.md new file mode 100644 index 000000000..7d855c579 --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/serialization/index.md @@ -0,0 +1,278 @@ +--- +hide: + - toc +--- + +# Serialization + +## Basic Types + +
+
Amount
+
8 ubytes
+

A quantity of mosaics in absolute units. It can only be positive or zero.

+
Height
+
8 ubytes
+

Index of a block in the blockchain. The first block (the Nemesis block) has height 1 and each subsequent block increases height by 1.

+
Timestamp
+
4 ubytes
+

Number of seconds elapsed since the creation of the Nemesis block.

+
Address
+
40 ubytes
+

An address identifies an account and is derived from its PublicKey. The 40 bytes correspond to its Base32-encoded form.

+
Hash256
+
32 ubytes
+

A 32-byte (256 bit) hash. The exact algorithm is unspecified as it can change depending on where it is used.

+
PublicKey
+
32 ubytes
+

A 32-byte (256 bit) integer derived from a private key. It serves as the public identifier of the key pair and can be disseminated widely. It is used to prove that an entity was signed with the paired private key.

+
Signature
+
64 ubytes
+

A 64-byte (512 bit) array certifying that the signed data has not been modified. NEM uses Ed25519 signatures with the Keccak-512 hash function.

+
+ +## Enumerations + + + +--8<-- 'devbook/reference/serialization/NetworkType.html' + + + +--8<-- 'devbook/reference/serialization/TransactionType.html' + + + +--8<-- 'devbook/reference/serialization/LinkAction.html' + + + +--8<-- 'devbook/reference/serialization/MosaicTransferFeeType.html' + + + +--8<-- 'devbook/reference/serialization/MosaicSupplyChangeAction.html' + + + +--8<-- 'devbook/reference/serialization/MultisigAccountModificationType.html' + + + +--8<-- 'devbook/reference/serialization/MessageType.html' + + + +--8<-- 'devbook/reference/serialization/BlockType.html' + +## Structures + + + +--8<-- 'devbook/reference/serialization/Transaction.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableTransaction.html' + + + +--8<-- 'devbook/reference/serialization/AccountKeyLinkTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableAccountKeyLinkTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/NamespaceId.html' + + + +--8<-- 'devbook/reference/serialization/MosaicId.html' + + + +--8<-- 'devbook/reference/serialization/Mosaic.html' + + + +--8<-- 'devbook/reference/serialization/SizePrefixedMosaic.html' + + + +--8<-- 'devbook/reference/serialization/MosaicLevy.html' + + + +--8<-- 'devbook/reference/serialization/MosaicProperty.html' + + + +--8<-- 'devbook/reference/serialization/SizePrefixedMosaicProperty.html' + + + +--8<-- 'devbook/reference/serialization/MosaicDefinition.html' + + + +--8<-- 'devbook/reference/serialization/MosaicDefinitionTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableMosaicDefinitionTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/MosaicSupplyChangeTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableMosaicSupplyChangeTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/MultisigAccountModification.html' + + + +--8<-- 'devbook/reference/serialization/SizePrefixedMultisigAccountModification.html' + + + +--8<-- 'devbook/reference/serialization/MultisigAccountModificationTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableMultisigAccountModificationTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/MultisigAccountModificationTransactionV2.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableMultisigAccountModificationTransactionV2.html' + + + +--8<-- 'devbook/reference/serialization/CosignatureV1Body.html' + + + +--8<-- 'devbook/reference/serialization/CosignatureV1.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableCosignatureV1.html' + + + +--8<-- 'devbook/reference/serialization/SizePrefixedCosignatureV1.html' + + + +--8<-- 'devbook/reference/serialization/MultisigTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableMultisigTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/NamespaceRegistrationTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableNamespaceRegistrationTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/Message.html' + + + +--8<-- 'devbook/reference/serialization/TransferTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableTransferTransactionV1.html' + + + +--8<-- 'devbook/reference/serialization/TransferTransactionV2.html' + + + +--8<-- 'devbook/reference/serialization/NonVerifiableTransferTransactionV2.html' + + + +--8<-- 'devbook/reference/serialization/Block.html' + + diff --git a/mkdocs/pages/en/devbook/reference/ts/.meta.yml b/mkdocs/pages/en/devbook/reference/ts/.meta.yml new file mode 100644 index 000000000..a140a65ca --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/ts/.meta.yml @@ -0,0 +1 @@ +language_icon: simple/javascript diff --git a/mkdocs/pages/en/devbook/reference/websockets/index.md b/mkdocs/pages/en/devbook/reference/websockets/index.md new file mode 100644 index 000000000..54caabf0b --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/websockets/index.md @@ -0,0 +1,682 @@ +# WebSockets + +NEM publishes blockchain events over +[WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API), so applications can receive live updates +without constantly polling the [REST API](../rest/nem.md). + +Client applications open a WebSocket connection to any in the network and subscribe to the [channels](#channels) +they want to monitor. +When an event occurs on a channel, the node notifies every subscribed client in real time. + +Some channels also accept [requests](#requests) for immediate data, similar to the REST API. +This can simplify applications that use WebSockets as their only API for both live notifications and on-demand updates. + +## Connection + +NEM serves WebSockets using the [STOMP](https://stomp.github.io/) messaging protocol over +[SockJS](https://github.com/sockjs/sockjs-client), on a dedicated port (`7778` by default) separate from the HTTP API +port. + +The SockJS endpoint is `/w/messages`, for example `http://localhost:7778/w/messages`. + +Clients typically connect using either a SockJS client library or the native WebSocket API, together with a STOMP client +library to handle messaging. + +??? note "Connecting using native WebSockets" + + SockJS provides a WebSocket-like transport with cross-browser support and HTTP-based fallback options when native + WebSockets are unavailable. + + Clients with native WebSocket support can connect directly to the SockJS WebSocket transport endpoint at + `/w/messages/websocket`, for example: `ws://localhost:7778/w/messages/websocket`. + + This uses the WebSocket transport of SockJS without requiring the SockJS client library, + while still relying on the SockJS server. + +## STOMP Session + +STOMP Session +: A client-node conversation over a WebSocket connection, following the [STOMP](https://stomp.github.io/) messaging + protocol. + +A client controls the session by exchanging with the node: + +1. Send a `CONNECT` frame to start the STOMP session. +2. Send a `SUBSCRIBE` frame for each to monitor. + Each subscription requires a client-defined `id`. +3. Send an optional [registration request](#registration-requests) with a `SEND` frame to enable notifications for + channels that require explicit registration. +4. Receive a `MESSAGE` frame from the node for every event on a subscribed channel. +5. Send an `UNSUBSCRIBE` frame for each subscribed channel to stop receiving its notifications. +6. Send a `DISCONNECT` frame to end the session. + +!!! warning "Connections can drop silently" + + The WebSocket connection can drop without notice, for example after being idle for too long. + Most STOMP clients report this through a connection-closed callback, which is a good place to reconnect. + + Reconnection starts a fresh session, so every channel must be subscribed again. + +## STOMP Frames + +STOMP Frame +: A plain-text message adhering to the [STOMP](https://stomp.github.io/) protocol, + made of a command, optional `header:value` lines, and an optional body. + +The client and node exchange the following frame types. + +### `CONNECT` + +Starts the STOMP session. +The client must send this frame once, right after the connection opens. +See the [STOMP specification](https://stomp.github.io/stomp-specification-1.2.html#CONNECT_or_STOMP_Frame) for full details. + +```stomp title="Example" +CONNECT +accept-version:1.2 +heart-beat:0,0 +``` + +### `SUBSCRIBE` + +Subscribes to a , with a client-chosen `id` and `destination`. +See the [STOMP specification](https://stomp.github.io/stomp-specification-1.2.html#SUBSCRIBE) for full details. + +```stomp title="Example" +SUBSCRIBE +id:sub-0 +destination:/blocks +``` + +* `id` is unique only within a single connection (other clients can reuse the same value). + It is echoed back as the `subscription` header on every message and used to `UNSUBSCRIBE` later. +* `destination` identifies the channel, so the same connection can monitor multiple channels. + +### `MESSAGE` + +Delivers channel data from the node. +It is the only frame type the node sends. +See the [STOMP specification](https://stomp.github.io/stomp-specification-1.2.html#MESSAGE) for full details. + +```stomp title="Example" +MESSAGE +destination:/blocks +subscription:sub-0 +message-id:befkedjj-6247 + +{ ... } +``` + +* `destination` matches the channel from the `SUBSCRIBE` frame. +* `subscription` matches the `id` from the `SUBSCRIBE` frame. +* `message-id` is a unique identifier the server assigns to each message. +* `{ ... }` is the body, a JSON object whose shape depends on the [channel](#channels). + See the **Message body** tabs below. + +### `SEND` + +Sends a to a `/w/api` destination. +See the [STOMP specification](https://stomp.github.io/stomp-specification-1.2.html#SEND) for full details. + +```stomp title="Example" +SEND +destination:/w/api/account/subscribe + +{ "account": "{address}" } +``` + +### `UNSUBSCRIBE` + +Cancels a subscription by its `id`. +See the [STOMP specification](https://stomp.github.io/stomp-specification-1.2.html#UNSUBSCRIBE) for full details. + +```stomp title="Example" +UNSUBSCRIBE +id:sub-0 +``` + +### `DISCONNECT` + +Ends the session. +See the [STOMP specification](https://stomp.github.io/stomp-specification-1.2.html#DISCONNECT) for full details. + +```stomp title="Example" +DISCONNECT +``` + +## Channels + +WebSocket Channel +: Node notifications are grouped into channels. + Clients subscribe to each channel whose notifications they want to receive. + +Every channel is subscribed to with a [`SUBSCRIBE`](#subscribe) frame. + +The available channels are grouped here by the type of event they report. + +### Block Channels + +These channels report new blocks as they are added to the chain. + +#### `/blocks` + +ws:blocks +: Notifies subscribed clients every time a new block is added to the chain. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/blocks +``` + +```stomp +MESSAGE +destination:/blocks +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by a [Block](../rest/nem.md#model/Block) JSON body. +
+ +#### `/blocks/new` + +ws:blocks/new +: Notifies subscribed clients of the new chain height every time a new block is added. + A lighter alternative to `/blocks` when only the height is needed. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/blocks/new +``` + +```stomp +MESSAGE +destination:/blocks/new +subscription:sub-0 +message-id:... + +{ "height": 1234567 } +``` +
+ +### Transaction Channels + +These channels report transaction activity, regardless of the accounts involved. + +#### `/unconfirmed` + +ws:unconfirmed +: Notifies subscribed clients every time a transaction enters the , regardless of the accounts + involved. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/unconfirmed +``` + +```stomp +MESSAGE +destination:/unconfirmed +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by a [Transaction](../rest/nem.md#model/Transaction) JSON body. +
+ +### Account Channels + +These channels report activity for a specific account, such as its balance, transactions, and the mosaics and +namespaces it owns. + +!!! note "Address format" + + Wherever an address appears, either in a channel `destination` or a request body, it uses the + [encoded address](../../../textbook/cryptography.md#addresses) format: + uppercase letters and digits, without hyphens. + + **Example:** `TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4`. + +#### `/account/{address}` + +ws:account/{address} +: Notifies subscribed clients when the account's state, such as its balance, changes. + Requires the address to be [registered](#registration-requests) first. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/account/{address} +``` + +```stomp +MESSAGE +destination:/account/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by an [AccountMetaDataPair](../rest/nem.md#model/AccountMetaDataPair) JSON body. +
+ +#### `/unconfirmed/{address}` + +ws:unconfirmed/{address} +: Notifies subscribed clients every time a transaction involving the account enters the . + Requires the address to be [registered](#registration-requests) first. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/unconfirmed/{address} +``` + +```stomp +MESSAGE +destination:/unconfirmed/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by a [TransactionMetaDataPair](../rest/nem.md#model/TransactionMetaDataPair) JSON body. +
+ +#### `/transactions/{address}` + +ws:transactions/{address} +: Notifies subscribed clients when a transaction involving the account is confirmed. + Requires the address to be [registered](#registration-requests) first. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/transactions/{address} +``` + +```stomp +MESSAGE +destination:/transactions/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by a [TransactionMetaDataPair](../rest/nem.md#model/TransactionMetaDataPair) JSON body. +
+ +#### `/account/mosaic/owned/{address}` + +ws:account/mosaic/owned/{address} +: Notifies subscribed clients when a block changes the mosaics the account owns. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/account/mosaic/owned/{address} +``` + +```stomp +MESSAGE +destination:/account/mosaic/owned/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by a [Mosaic](../rest/nem.md#model/Mosaic) JSON body. +
+ +#### `/account/mosaic/owned/definition/{address}` + +ws:account/mosaic/owned/definition/{address} +: Notifies subscribed clients when a block changes the mosaic definitions the account owns. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/account/mosaic/owned/definition/{address} +``` + +```stomp +MESSAGE +destination:/account/mosaic/owned/definition/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by a [MosaicDefinitionSupplyTuple](../rest/nem.md#model/MosaicDefinitionSupplyTuple) JSON body. +
+ +#### `/account/namespace/owned/{address}` + +ws:account/namespace/owned/{address} +: Notifies subscribed clients when a block changes the namespaces the account owns. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/account/namespace/owned/{address} +``` + +```stomp +MESSAGE +destination:/account/namespace/owned/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by a [Namespace](../rest/nem.md#model/Namespace) JSON body. +
+ +#### `/recenttransactions/{address}` + +ws:recenttransactions/{address} +: Notifies subscribed clients of the account's 25 most recent confirmed transactions, only in response to + . + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/recenttransactions/{address} +``` + +```stomp +MESSAGE +destination:/recenttransactions/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by a list of [TransactionMetaDataPair](../rest/nem.md#model/TransactionMetaDataPair) wrapped in a `data` field. +
+ +### System Channels + +These channels report node status and request errors, rather than blockchain events. + +#### `/node/info` + +ws:node/info +: Notifies subscribed clients of the node's information, only in response to . + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/node/info +``` + +```stomp +MESSAGE +destination:/node/info +subscription:sub-0 +message-id:... + +{ ... } +``` +Followed by a [Node](../rest/nem.md#model/Node) JSON body. +
+ +#### `/errors` + +ws:errors +: Notifies subscribed clients when a `/w/api` fails, for example when its address payload is invalid. + A client can subscribe to this channel right after connecting, so problems surface here instead of being silently + dropped. + +
+ + +
:material-arrow-up-bold: Subscription frame:material-arrow-down-bold: Notification frame
+```stomp +SUBSCRIBE +id:sub-0 +destination:/errors +``` + +```stomp +MESSAGE +destination:/errors +subscription:sub-0 +message-id:... + +{ + "timeStamp": 67191609, + "status": 400, + "error": "Bad Request", + "message": "account is not valid" +} +``` +
+ +## Requests + +WebSocket Request +: A message sent by the client to make the node deliver: + either notifications on channels that require **registration**, or an immediate notification containing a + **snapshot** of the state of the blockchain. + +Requests are **read-only** and do not modify the chain state. + +All requests are sent with a [`SEND`](#send) frame to a destination that begins with `/w/api/`. +Some requests return no answer, while others return results through one of the [channels](#channels) above. + +### Registration Requests + +Some [account channels](#account-channels) stay silent until the address is registered. +These requests perform that registration, so those channels begin delivering notifications. + +#### `/w/api/account/subscribe` + +req:w/api/account/subscribe +: Registers the address so the node starts sending notifications on [account channels](#account-channels). + +
+ + +
:material-arrow-up-bold: Request frame
+```stomp +SEND +destination:/w/api/account/subscribe + +{ "account": "{address}" } +``` +
+ +#### `/w/api/account/get` + +req:w/api/account/get +: Registers the address like , and forces the node to send a + notification containing the account's current state to . + +
+ + +
:material-arrow-up-bold: Request frame
+```stomp +SEND +destination:/w/api/account/get + +{ "account": "{address}" } +``` +
+ +### Snapshot Requests + +Each request forces the node to send an immediate snapshot of current data to a channel, without waiting for a new +event. + +This allows applications to fetch current data on demand through the same channels used for live updates, instead of polling the [REST API](../rest/nem.md). + +#### `/w/api/account/transfers/all` + +req:w/api/account/transfers/all +: Forces the node to send a notification containing the account's 25 most recent confirmed transactions + to , and another with up to 10 pending ones to . + +
+ + +
:material-arrow-up-bold: Request frame
+```stomp +SEND +destination:/w/api/account/transfers/all + +{ "account": "{address}" } +``` +
+ +#### `/w/api/account/transfers/unconfirmed` + +req:w/api/account/transfers/unconfirmed +: Forces the node to send a notification containing up to 10 of the account's most recent pending + transactions to . + +
+ + +
:material-arrow-up-bold: Request frame
+```stomp +SEND +destination:/w/api/account/transfers/unconfirmed + +{ "account": "{address}" } +``` +
+ +#### `/w/api/account/mosaic/owned` + +req:w/api/account/mosaic/owned +: Forces the node to send a notification containing the mosaics the account owns to + . + +
+ + +
:material-arrow-up-bold: Request frame
+```stomp +SEND +destination:/w/api/account/mosaic/owned + +{ "account": "{address}" } +``` +
+ +#### `/w/api/account/mosaic/owned/definition` + +req:w/api/account/mosaic/owned/definition +: Forces the node to send a notification containing the mosaic definitions the account owns to + . + +
+ + +
:material-arrow-up-bold: Request frame
+```stomp +SEND +destination:/w/api/account/mosaic/owned/definition + +{ "account": "{address}" } +``` +
+ +#### `/w/api/account/namespace/owned` + +req:w/api/account/namespace/owned +: Forces the node to send a notification containing the namespaces the account owns to + . + +
+ + +
:material-arrow-up-bold: Request frame
+```stomp +SEND +destination:/w/api/account/namespace/owned + +{ "account": "{address}" } +``` +
+ +#### `/w/api/block/last` + +req:w/api/block/last +: Forces the node to send a notification containing the latest block to . + +
+ + +
:material-arrow-up-bold: Request frame
+```stomp +SEND +destination:/w/api/block/last +``` +
+ +#### `/w/api/node/info` + +req:w/api/node/info +: Forces the node to send a notification containing its own information to . + +
+ + +
:material-arrow-up-bold: Request frame
+```stomp +SEND +destination:/w/api/node/info +``` +
diff --git a/mkdocs/pages/en/devbook/start/hello-world.md b/mkdocs/pages/en/devbook/start/hello-world.md new file mode 100644 index 000000000..383ff8d15 --- /dev/null +++ b/mkdocs/pages/en/devbook/start/hello-world.md @@ -0,0 +1,69 @@ +--- +title: Hello World +tutorial_level: beginner +--- + +# Hello World + +This tutorial shows how to verify that your Symbol SDK installation is working correctly by writing a minimal program +that: + +* Retrieves the network name and launch date using the SDK. +* Connects to a and prints the current blockchain height. + +No accounts, keys, or transactions are required, just a basic SDK call and a REST request. + +## Prerequisites + +If you have not done so already, start with [Setting Up a Development Environment](../start/setup.md). + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/start/hello_world', ['py', 'js']) }} + +### Making SDK Calls + +{{ tutorial.code_snippet_tagged('step-1') }} + +The class is the main entry point to the Symbol SDK when working with the NEM blockchain. +It provides most of the methods you will need, from building and signing transactions to retrieving network-related +information. + +To create a facade, simply specify the name of the network you want to work with, either `mainnet` or `testnet`. + +This example then demonstrates how to retrieve the network launch date. +The method converts a network timestamp into a UTC datetime. +By passing `0` (the genesis timestamp) you can obtain the moment the genesis block was produced, that is, the network's +launch date. + +### Retrieving Information From a Node + +{{ tutorial.code_snippet_tagged('step-2') }} + +Interaction with the NEM blockchain happens through a , which exposes a REST interface for querying network +state and submitting transactions. + +This example connects to a testnet node and retrieves the current blockchain height from the +endpoint. + +This request does not require any private keys or authorization, making it a simple and effective test to confirm that +the environment is set up correctly and can reach the network. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text +--8<-- 'devbook/start/hello_world.log' +``` + +## Conclusion + +If you got the output shown above, you're all set! +You have access to the Symbol SDK and successfully reached a NEM node. + +That's all you need to start your NEM adventure. + +Why not try [creating an account next](../accounts/create-from-private-key.md)? diff --git a/mkdocs/pages/en/devbook/start/setup.md b/mkdocs/pages/en/devbook/start/setup.md new file mode 100644 index 000000000..963d7b1c1 --- /dev/null +++ b/mkdocs/pages/en/devbook/start/setup.md @@ -0,0 +1,92 @@ +--- +title: Setup +--- + +# Setting Up a Development Environment + +This page lists the dependencies required to run the tutorials in this documentation and explains how to run them. + +Most tutorials use the Symbol SDK, which supports both the and networks. +It is the recommended library for building NEM applications, so the steps below apply to every tutorial. + +Select the language you prefer: + +=== ":simple-python: Python" + + + + +
Prerequisites[Python](https://www.python.org/downloads/) 3.10 or later
Installation + Install the Symbol SDK version 3.3.1 with: + ```bash + pip install symbol-sdk-python --upgrade + ``` +
Running the Sample Code + Download a sample and run it with: + ```bash + python hello-world.py + ``` +
+ + ??? warning "Troubleshooting" + + On some systems, installing the SDK may require additional system packages, + because some Python dependencies are built from source. + + If installation fails with errors related to missing headers, libraries, or compiler tools, + install the required **development packages** for your system and run the installation again. + + Common symptoms include errors mentioning `gcc` or `pysha3`. + + On Ubuntu and Debian, it is typically enough to install: + + ```bash + sudo apt install python3-dev build-essential + ``` + + Then run the Symbol SDK installation again. + +=== ":simple-javascript: JavaScript" + + + + +
PrerequisitesAny actively supported version of [Node.js](https://nodejs.org/)
Installation + Create a project folder and install the Symbol SDK version 3.3.1 as a dependency: + ```bash + mkdir nem-dev && cd nem-dev + npm init -y + npm install symbol-sdk + ``` +
Running the Sample Code + Download a sample and run it with: + ```bash + node hello-world.mjs + ``` +
+ +## Next Steps + +* Proceed to [Hello World](./hello-world.md) + + diff --git a/mkdocs/pages/en/devbook/transactions/monitoring-status.md b/mkdocs/pages/en/devbook/transactions/monitoring-status.md new file mode 100644 index 000000000..d197ac3b5 --- /dev/null +++ b/mkdocs/pages/en/devbook/transactions/monitoring-status.md @@ -0,0 +1,223 @@ +--- +title: Transaction Status +tutorial_level: beginner +--- + +# Monitoring Transaction Status + +After announcing a to the NEM network, it remains unconfirmed until it is included in a . + +Monitoring status changes is essential for building responsive applications that can react to transaction confirmation +or failure. + +This tutorial shows how to poll a transaction's status until it is confirmed, how to check whether it is still +waiting in the , and how to decide when it will never confirm. + +This kind of monitoring typically happens right after announcing a transaction, as shown in the +[Transfer XEM tutorial](./transfer-xem.md), to make sure it gets confirmed. + +!!! note "Polling is not recommended for production" + + This tutorial uses polling to check the transaction status for illustration purposes, + but it is not the recommended approach for production applications. + + [WebSockets](../reference/websockets/index.md) provide a more responsive solution without the overhead of repeated + API calls. + +## Prerequisites + +This tutorial uses the [NEM REST API](../reference/rest/nem.md) without requiring an SDK. +You only need a way to make HTTP requests. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/transactions/monitoring_status', ['py', 'js']) }} + +The snippet uses the `NODE_URL` environment variable to set the NEM API node. +If no value is provided, a default one is used. + +The tutorial first defines the following reusable functions: + +* {{ tutorial.var('get_confirmation_height()') }}: Checks whether the transaction has been confirmed by + and is already part of the blockchain. +* {{ tutorial.var('is_in_unconfirmed_pool()') }}: Reports whether the transaction is waiting to be confirmed in the + . +* {{ tutorial.var('wait_for_confirmation()') }}: Repeats the confirmation check until the transaction is confirmed or + the attempts run out. + +The tutorial then calls them together to monitor the transaction, +as shown in [Putting It All Together](#putting-it-all-together). + +## Code Explanation + +### Finding the Transaction Hash, Address, and Signature + +{{ tutorial.code_snippet_tagged('step-1') }} + +To monitor a transaction, you need its hash, which is generated after signing. +The hash uniquely identifies the transaction on the NEM network. + +The snippet also reads a **signer's address** and the **transaction signature**. +Neither is needed to detect confirmation, but both are used to check whether a transaction is still waiting in the +. + +The snippet uses sample values. +Set `TRANSACTION_HASH`, `SIGNER_ADDRESS`, and `TRANSACTION_SIGNATURE` environment variables to override them. +All three values are produced when signing a transaction, as shown in the [Transfer XEM tutorial](./transfer-xem.md). + +### Checking for Confirmation + +{{ tutorial.code_snippet_tagged('step-2') }} + +The {{ tutorial.var('get_confirmation_height') }} function checks whether the transaction is confirmed by querying + with the transaction hash. + +When a transaction has been included in a block, this endpoint returns its contents together with the block height +in `meta.height`, which the function returns. + +Otherwise, the endpoint responds with HTTP `400` ("Hash was not found in cache") and the function returns no height, +meaning the transaction is not confirmed. +A transaction that is not confirmed may still be waiting in the , which the next function inspects. + +!!! warning "Hash lookup is short-lived" + + reads from a cache with a default retention of 36 hours. + The lookup is enabled by default, but node operators can disable it or change the retention. + + Querying for a transaction hash older than the retention period returns an HTTP `400` error, even if the + transaction is in fact confirmed. + + Therefore, when announcing a transaction that might need to be looked up later than this retention period, + store its confirmation block height along with its hash. + In this way, the transaction can be directly retrieved from the block via the endpoint. + + Otherwise, the transaction needs to be located by paging through the signer's full history with + , or by searching the blockchain block by block. + +### Inspecting the Unconfirmed Pool + +{{ tutorial.code_snippet_tagged('step-3') }} + +{{ tutorial.var('is_in_unconfirmed_pool') }} queries for the signer's address and +reports whether the monitored transaction is among the pending list. + +!!! note "Invalid transactions never enter the pool" + + A transaction that fails [validation](../../textbook/transactions.md#3-validation) does not reach the unconfirmed + pool: the receiving rejects it immediately when announced, as shown in the + [Transfer XEM tutorial](./transfer-xem.md#announcing-the-transaction). + +The above endpoint response omits each entry's hash, so the function matches by **signature** instead. +A transaction's signature is unique and appears under `transaction.signature` in every pool entry. +For multisig transactions, this is the signature of the announced wrapper, not of the inner transaction. + +The function returns: + +* {{ tutorial.lit('True') }}: the transaction is still in the unconfirmed pool, waiting to be included in a block. +* {{ tutorial.lit('False') }}: the transaction is not in the response. + Possible causes include: it has not arrived at this node yet, it has already been confirmed, + it was dropped from the pool, or it was left out of the response. + + !!! warning "The response is limited to 25 transactions" + + The endpoint returns at most the 25 most recent transactions involving the address. + Incoming transactions count toward this limit too, so on a busy account the monitored transaction can be missing + from the response while it is still in the unconfirmed pool. + +### Waiting for Confirmation + +{{ tutorial.code_snippet_tagged('step-4') }} + +The {{ tutorial.var('wait_for_confirmation') }} function calls {{ tutorial.var('get_confirmation_height') }} every +second until the transaction is confirmed, or two minutes ellapse (configurable timeout). + +The function returns {{ tutorial.lit('True') }} as soon as a check reports a confirmation. +When the attempts run out, it returns {{ tutorial.lit('False') }} instead. +This means the transaction was not confirmed within the polling window, not that it failed, but this is a rare case. + +### Putting It All Together + +{{ tutorial.code_snippet_tagged('step-5') }} + +The snippet starts with a call to {{ tutorial.var('get_confirmation_height') }} to check if the transaction is already +confirmed. + +!!! warning "Confirmed transactions can still be reversed" + + A confirmed transaction has been included in a block but is not yet irreversible. + Until enough subsequent blocks are added to surpass the , are still possible. + +If the transaction is not already part of a block, {{ tutorial.var('is_in_unconfirmed_pool') }} looks for it in the +unconfirmed pool. +A transaction that is neither confirmed nor in this pool is reported as not found. + +Only when the transaction is waiting in the unconfirmed pool does the snippet call +{{ tutorial.var('wait_for_confirmation') }} to poll until the transaction is confirmed or the polling window ends. + +!!! note "Only rejection or a passed deadline means failure" + + There are only two ways to know a transaction will never confirm: it was rejected when announced, or its + **deadline** has passed. + + A transaction disappearing from one node's unconfirmed pool does **not** mean it has failed. + Each node maintains its own pool, and another peer may still hold and eventually confirm the transaction. + For example, a node may restart with an empty pool or remove older transactions while managing pool capacity. + + Because announcement rejections are returned immediately, the **deadline** provides the final verdict. + Once passes the deadline, the transaction can no longer be included in a block and it is safe + to announce a replacement transaction. + + Compare the deadline chosen when [building the transaction](./transfer-xem.md#fetching-network-time) against the + network time returned by . + +## Output + +The following output shows a typical run monitoring a freshly-announced transaction: + +```text linenums="1" hl_lines="2 4 10 12" +--8<-- 'devbook/transactions/monitoring_status.log' +``` + +Some highlights from the output: + +* **Transaction hash** (line 2): The hash of the transaction to monitor, which uniquely identifies it on the network. + +* **Polling start** (line 4): Polling starts because the transaction was not already present in the blockchain and was + found waiting in the unconfirmed pool. + +* **Polling attempts** (lines 5-9): Each attempt reports `pending` while the transaction waits to be included in a + block. + +* **Confirmation** (line 10): A polling attempt finally reports the inclusion in block `652601`. + +* **Final outcome** (line 12): The transaction is confirmed and monitoring ends. + +The number of attempts and timing vary depending on network conditions and block production rate. + +To see the transaction from the network's perspective, visit the [NEM Testnet Explorer](https://testnet.nem.fyi/) and +search for the transaction hash. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ------------------------------------------------------------------------- | --------------------------------------- | +| [Check for confirmation](#checking-for-confirmation) | | +| [Inspect the unconfirmed pool](#inspecting-the-unconfirmed-pool) | | +| [Wait for confirmation](#waiting-for-confirmation) | | +| [Detect when a transaction will never confirm](#putting-it-all-together) | | + +## Next Steps + +For production applications, consider these improvements: + +* **Wait past the rewrite limit.** A confirmed transaction can still be rolled back until enough subsequent + blocks have been added. + See the for the practical threshold. +* **Query multiple nodes.** Check status across several for greater reliability and protection against + single-node issues. +* **Use WebSockets:** Replace polling with WebSocket subscriptions for real-time updates without repeated API calls. + See the [Listening to Transaction Flow](../websockets/listen-transaction-flow.md) WebSocket tutorial. diff --git a/mkdocs/pages/en/devbook/transactions/transfer-mosaics.md b/mkdocs/pages/en/devbook/transactions/transfer-mosaics.md new file mode 100644 index 000000000..b13f67ea8 --- /dev/null +++ b/mkdocs/pages/en/devbook/transactions/transfer-mosaics.md @@ -0,0 +1,217 @@ +--- +title: Transfer Mosaics +tutorial_level: intermediate +--- + +# Sending Mosaics with a Transfer Transaction + +A can carry other instead of, or alongside, . + +This tutorial shows how to send a mosaic, focusing on the parts that differ from a plain +[XEM Transfer](./transfer-xem.md). + +```dot +digraph "Transfer company:token" { + rankdir="LR"; + node [fontsize=12]; + + A [label="A"]; + B [label="B"]; + + A -> B [label="100 company:token"]; +} +``` + +The example sends 100 units of a `company:token` mosaic that exists on testnet, using the default test account that +already owns some. +The same flow works for any other mosaic, as long as the signing account owns enough of it. + +## Prerequisites + +Before you start, make sure to: + +* [Set Up your Development Environment](../start/setup.md). +* Create an to send the transfer transaction, either + [from code](../accounts/create-from-private-key.md) or + [by using a wallet](../../userbook/wallet/create-account.md). +* Obtain to pay for the transaction fee and transfer amount. + See [Getting Testnet Funds from the Faucet](../accounts/testnet-faucet.md). +* Own enough of the you want to transfer. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/transactions/transfer_mosaics', ['py', 'js']) }} + +## Code Explanation + +Signing, announcing, and waiting for confirmation work the same as in the [Transfer XEM](./transfer-xem.md) tutorial and +are not repeated here. +Only the steps that differ are explained below. + +### Setting Up the Accounts + +{{ tutorial.code_snippet_tagged('step-1') }} + +Every mosaic transfer involves two accounts: a **sender** and a **recipient**. + +The **sender** is the that signs the transaction, pays the fee, and holds the mosaic being sent. +Its private key is loaded from the `SIGNER_PRIVATE_KEY` environment variable. +If not provided, a test key is used as default. + +The **recipient** is the account that receives the mosaic. +Its is loaded from the `RECIPIENT_ADDRESS` environment variable. +If not provided, a test address is used as default. + +The default test account is pre-funded with `company:token`, though its balance is shared across tutorial runs and can +be exhausted. +If your transfer fails with insufficient balance, set `SIGNER_PRIVATE_KEY` to an account that holds the mosaic. + +### Setting Up the Mosaic + +{{ tutorial.code_snippet_tagged('step-2') }} + +The mosaic to send is identified by `MOSAIC_ID`, its +[fully qualified name](../../textbook/mosaics.md#fully-qualified-name) in `:` form, +defaulting to `company:token`. +Setting it lets you point the tutorial at any other mosaic the signing account owns. + +`QUANTITY` is how much of the mosaic to transfer, in [whole units](../../textbook/mosaics.md#divisibility), defaulting +to 100. + +### Fetching Network Time + +{{ tutorial.code_snippet_tagged('step-3') }} + +Every NEM transaction needs a `timestamp` (when it was created) and a `deadline` (how long the network keeps trying to +confirm it), both in . + +The snippet fetches the current network time from , sets `timestamp` to it, and sets +`deadline` two hours later. + +The endpoint returns the time in milliseconds, so the code divides by 1000 to obtain the seconds that transactions +expect. + +See [Fetching Network Time](./transfer-xem.md#fetching-network-time) in Transfer XEM for more detail, including caching +strategies. + +### Fetching the Mosaic Definition and Supply + +{{ tutorial.code_snippet_tagged('step-4') }} + +The mosaic's is used to convert the transfer quantity to +[atomic units](../../textbook/mosaics.md#divisibility), and together with the +current supply it determines the fee. +Both are fetched from the node before building the transaction. + +* The endpoint returns the mosaic's definition, including its properties such as + `divisibility`. +* The endpoint returns the current total supply, in + [whole units](../../textbook/mosaics.md#divisibility). + +As with the network time, these values do not need to be fetched before every transfer. +A mosaic's divisibility is fixed at creation, and its supply changes only if the mosaic was created with a mutable +supply, so applications can fetch both values once and cache them, refreshing the supply when needed. + +### Building the Transaction + +{{ tutorial.code_snippet_tagged('step-5') }} + +The snippet first converts `QUANTITY` from whole units to atomic units using the divisibility fetched in the +previous step. + +!!! note "Whole to atomic units" + + A mosaic's `divisibility`, set at creation and ranging from 0 to 6, defines the conversion: + 1 whole unit equals 10^divisibility^ [atomic units](../../textbook/mosaics.md#divisibility). + + The `company:token` mosaic used here has divisibility 0, so 10^0^ = 1 and a `QUANTITY` of 100 is encoded as 100 + atomic units. + + A mosaic with divisibility 2, by contrast, would have 10^2^ = 100 atomic units per whole unit, so the same + `QUANTITY` of 100 would be encoded as 10'000 atomic units. + +The snippet then calls with a descriptor that has an extra +`mosaics` field, which accepts up to 10 entries. +Each entry identifies a mosaic and how much of it to send: + +* The that owns the mosaic. +* The mosaic's name. +* The quantity, given in the mosaic's **atomic units** (calculated above). + +When mosaics are attached, the top-level `amount` is no longer the +[XEM amount](../../textbook/transfer_transactions.md#xem-amount). +It becomes a multiplier applied to every listed mosaic, where `1_000_000` represents a factor of one. +Setting `amount` to `1_000_000` sends each mosaic at the quantity given in its entry. + +!!! info "Sending XEM alongside other mosaics" + + The top-level `amount` now acts as a multiplier rather than sending XEM. + To send XEM at the same time as another mosaic, add a `nem:xem` entry to the `mosaics` array. + Its quantity is the amount of XEM to send, in atomic units. + +### Calculating the Transaction Fee + +{{ tutorial.code_snippet_tagged('step-6') }} + +The fee for a mosaic transfer depends on each mosaic's supply, divisibility, and the quantity transferred. +Rather than implement NEM's fixed fee schedule by hand, the snippet calls the SDK's + helper. + +The helper reads the transferred quantities directly from the transaction built in the previous step, and takes +each mosaic's `supply` (in [whole units](../../textbook/mosaics.md#divisibility)) and `divisibility` as a second +argument, since those values are not stored in the transaction. + +The returned fee is assigned to `transaction.fee` before signing. + +See the [Fees](../../textbook/transfer_transactions.md#fees) section for the full rules behind the calculation. + +### Signing, Announcing, and Waiting for Confirmation + +These steps work the same as in [Transfer XEM](./transfer-xem.md) and are not detailed here. + +!!! warning "Mosaics with a levy" + + Some mosaics carry a : an additional fee paid to a third-party account on every transfer of that mosaic, on + top of the transaction fee. + The sender's account must hold enough of the levy mosaic (which may differ from the mosaic being transferred) + to cover it, or the transaction is rejected. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text linenums="1" hl_lines="8 18 21 22-29" +--8<-- 'devbook/transactions/transfer_mosaics.log' +``` + +Some highlights from the output, focusing on the parts that differ from a plain XEM transfer: + +* **Definition and supply** (line 8): The mosaic `company:token`, with its divisibility (`0`) and current supply + (`1000000`), fetched to convert the quantity to atomic units and to calculate the fee. + +* **Transaction fee** (line 18): `350000` atomic units (`0.35` XEM), derived from the mosaic's supply, divisibility, and + the quantity sent. + +* **Amount as multiplier** (line 21): With mosaics attached, `amount` is `1000000` (a factor of one) rather than an + amount of XEM. + +* **Mosaics array** (lines 22-29): The mosaics included in the transfer, here a single entry with a quantity of `100`. + The namespace and name are hex-encoded like the address, so `636F6D70616E79` and `746F6B656E` decode back to + `company` and `token`. + +To see the transaction from the network's perspective, you can search for the transaction hash on the +[NEM testnet explorer](https://testnet.nem.fyi/). +The hash is printed in the line that says `Waiting for confirmation from /transaction/get?hash=...`. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| --------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [Fetch a mosaic's divisibility](#fetching-the-mosaic-definition-and-supply) | | +| [Fetch a mosaic's supply](#fetching-the-mosaic-definition-and-supply) | | +| [Build a mosaic transfer](#building-the-transaction) | | +| [Calculate the transaction fee](#calculating-the-transaction-fee) | | diff --git a/mkdocs/pages/en/devbook/transactions/transfer-xem.md b/mkdocs/pages/en/devbook/transactions/transfer-xem.md new file mode 100644 index 000000000..00bdae0a9 --- /dev/null +++ b/mkdocs/pages/en/devbook/transactions/transfer-xem.md @@ -0,0 +1,237 @@ +--- +title: Transfer XEM +tutorial_level: beginner +--- + +# Sending XEM with a Transfer Transaction + +Sending from one to another is the most basic action on the NEM blockchain, and every other type of + follows the same general pattern. + +```dot +digraph "Transfer XEM" { + rankdir="LR"; + node [fontsize=12]; + + A [label="A"]; + B [label="B"]; + + A -> B [label="1 XEM"]; +} +``` + +This tutorial shows how to create, sign, and announce a that sends 1 XEM between two accounts, +and then poll the transaction's status until it is confirmed. + +## Prerequisites + +Before you start, make sure to: + +* [Set Up your Development Environment](../start/setup.md). +* Create an to send the transfer transaction, either + [from code](../accounts/create-from-private-key.md) or + [by using a wallet](../../userbook/wallet/create-account.md). +* Obtain to pay for the transaction fee and transfer amount. + See [Getting Testnet Funds from the Faucet](../accounts/testnet-faucet.md). + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/transactions/transfer_xem', ['py', 'js']) }} + +The whole code is wrapped in a single `try` block to provide simple error handling, +but applications will probably want to use more fine-grained control. + +## Code Explanation + +### Setting Up the Accounts + +{{ tutorial.code_snippet_tagged('step-1') }} + +Every transfer transaction involves two accounts: a **sender** and a **recipient**. + +The **sender** is the that signs the transaction and pays the fee. +Its private key is loaded from the `SIGNER_PRIVATE_KEY` environment variable. +If not provided, a test key is used as default. + +The **recipient** is the account that receives the XEM. +Its is loaded from the `RECIPIENT_ADDRESS` environment variable. +If not provided, a test address is used as default. + +### Defining the Transfer Amount + +{{ tutorial.code_snippet_tagged('step-2') }} + +The snippet defines the transfer amount in the `xem` variable, loaded as a number from the `XEM_AMOUNT` environment +variable. +If not provided, a default of 1 XEM is used. + +The transaction's `amount` field requires [atomic units](../../textbook/mosaics.md#divisibility), not whole XEM. +XEM has a of 6, so one XEM equals one million atomic units. +The snippet derives `amount` by multiplying `xem` by 1'000'000. + +### Fetching Network Time + +{{ tutorial.code_snippet_tagged('step-3') }} + +Every NEM transaction contains two time fields, both expressed in , +the number of seconds since the NEM nemesis block: + +* `timestamp`: The moment the transaction is created, set here to the current network time. +* `deadline`: How long the network keeps trying to confirm the transaction before discarding it. + It must be after the timestamp and no more than + [24 hours](../../textbook/transactions.md#common-transaction-structure) later. + Otherwise, the node rejects the transaction. + This example sets it two hours after the timestamp, well within the limit. + +Building a transfer therefore needs an accurate network time. + +The endpoint reports the node's current network time. +The node returns this value in milliseconds, so the code divides it by 1000 to obtain the seconds that transactions +expect. + +However, applications do not need to query the network time before every transaction. +It can be fetched once and then adjusted using the local system clock when needed. +This provides a good balance between accuracy and performance. + +### Building the Transaction + +{{ tutorial.code_snippet_tagged('step-4') }} + +The snippet calls with a descriptor that supplies the transfer transaction's +properties: + +* {{ tutorial.var('type') }}: This tutorial uses , the current transfer version, which can carry both XEM and + other . + No mosaics are attached here, so the transaction sends XEM only. + +* {{ tutorial.var('signer_public_key') }}: The signer is the account that will pay the fee. + In a transfer transaction, it is also the source of the transferred XEM. + +* {{ tutorial.var('timestamp') }} and {{ tutorial.var('deadline') }}: The values computed in the network time step. + +* {{ tutorial.var('recipient_address') }}: The address that will receive the XEM. + +* {{ tutorial.var('amount') }}: The atomic-unit value computed earlier. For 1 XEM, this is `1_000_000`. + +!!! info "Sending a mosaic or a message" + + A can also carry other instead of XEM, or include a message, with the fee + calculated differently in each case. + See the [Transfer Mosaics](./transfer-mosaics.md) and [Transfer with a Message](./messages.md) tutorials. + +### Calculating the Transaction Fee + +{{ tutorial.code_snippet_tagged('step-5') }} + +Every transaction pays a fee to the that includes it in a block. + +Rather than implement NEM's [fixed fee schedule](../../textbook/transfer_transactions.md#fees) by hand, +the snippet calls the SDK's helper, which reads the XEM amount directly from +the transaction built in the previous step. + +The returned fee is assigned to `transaction.fee` before signing. +The fee starts at 0.05 XEM for small amounts and grows with the XEM sent, up to a cap of 1.25 XEM. + +### Signing and Serializing + +{{ tutorial.code_snippet_tagged('step-6') }} + +Once the transaction is created, it must be signed with the signing account's private key. +Signing ensures the transaction is authentic and authorized by the sender. + + returns a . + adds the signature to the transaction and serializes it into a JSON payload +ready to be submitted directly to a node for announcement. + +### Announcing the Transaction + +{{ tutorial.code_snippet_tagged('step-7') }} + +The signed payload is submitted to the endpoint of any NEM . + +The node validates the transaction as soon as it is announced and reports the outcome in the response. +A result of `SUCCESS` means the transaction passed this first check and was added to the . +Any other result means the node did not accept it, and the response message explains why, for example that the +account does not hold enough XEM to cover the amount and the fee. + +!!! warning "Do not rely on unconfirmed transactions" + + A `SUCCESS` result only means the transaction reached the unconfirmed pool. + It is not yet guaranteed to be included in a block. + Wait until it is [confirmed](#waiting-for-confirmation), and ideally past the , before relying + on it. + +### Waiting for Confirmation + +{{ tutorial.code_snippet_tagged('step-8') }} + +The snippet above repeatedly queries the endpoint using the hash of the announced transaction. + +!!! note "Polling vs WebSockets" + + This step uses polling to check whether the transaction has been confirmed. + Polling is used here for illustration purposes, but it is not the recommended approach for real applications. + + [WebSockets](../websockets/listen-transaction-flow.md) provide a more responsive solution without the overhead of + repeated API calls. + +While the transaction is still unconfirmed, the endpoint responds with an error, and the code waits one second +before retrying, for up to 120 attempts (about two minutes). + +Once the transaction is included in a block, the endpoint returns it together with the block height, and the loop +ends. + +NEM produces a block roughly once per minute, so confirmation usually takes from a few seconds to a couple of minutes. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text linenums="1" hl_lines="11 13 15 16 17 20 21 34" +--8<-- 'devbook/transactions/transfer_xem.log' +``` + +Some highlights from the output: + +* **Signer public key** (line 11): The account that signs the transaction and sends the XEM. + +* **Transaction fee** (line 13): `50000` atomic units (`0.05` XEM), the fee for sending the default amount of 1 XEM. + +* **Recipient address** (line 15): The account that receives the XEM. + This is the same `RECIPIENT_ADDRESS`, but it looks different because NEM's transaction format encodes each character + of its Base32 text as an ASCII code in hexadecimal, so `5442...` decodes back to `TBUL...` + (`54` is `T`, `42` is `B`, and so on). + +* **Transfer amount** (line 16): `1000000` atomic units, equal to 1 XEM. + +* **No mosaics** (line 17): An empty mosaics array means the transaction sends XEM only. + +* **Announcement result** (line 20): A result of `SUCCESS` means the node accepted the transaction into the unconfirmed + pool. + +* **Transaction hash** (line 21): The hash that uniquely identifies the transaction on the network. + +* **Confirmation** (line 34): The transaction is included in block `626588`. + +The number of `pending` checks depends on how soon the next block is harvested, so it varies between runs. + +To see the transaction from the network's perspective, you can search for the transaction hash on the +[NEM testnet explorer](https://testnet.nem.fyi/). +The hash is printed in the line that says `Waiting for confirmation from /transaction/get?hash=...`. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ----------------------------------------------------------------- | -------------------------------------------------------------------------- | +| [Obtain the network time](#fetching-network-time) | | +| [Build the transaction](#building-the-transaction) | | +| [Calculate the transaction fee](#calculating-the-transaction-fee) | | +| [Sign the transaction](#signing-and-serializing) |
| +| [Announce the transaction](#announcing-the-transaction) | | +| [Wait for confirmation](#waiting-for-confirmation) | | + +Most other NEM transaction types are created, signed, and announced in the same way. diff --git a/mkdocs/pages/en/devbook/transactions/typed-descriptors.md b/mkdocs/pages/en/devbook/transactions/typed-descriptors.md new file mode 100755 index 000000000..86e967da7 --- /dev/null +++ b/mkdocs/pages/en/devbook/transactions/typed-descriptors.md @@ -0,0 +1,73 @@ +--- +title: Typed Descriptors +tutorial_level: beginner +--- + +# Creating Transactions Using Typed Descriptors in JavaScript + +{% import 'tutorial.jinja2' as tutorial with context %} +{{ tutorial.code_full_tagged('devbook/transactions/transfer_xem.typed', ['js'], show=false) }} + +Transactions are a fundamental part of the NEM blockchain, because most interactions with the network happen +through them. + +All JavaScript examples throughout the tutorials use the method +to create transactions, due to its compact syntax. +However, this method is not type-safe: it accepts a generic object and depends on it having the correct fields. + +This page shows how to use instead. +This alternative accepts well-defined parameters, offering better type safety and improved IDE support. + +The code presented here is the same as in the [Creating a Transfer Transaction](./transfer-xem.md) tutorial, +with the only difference being the transaction creation step. +For brevity, only that section is shown here. +The rest of the process, including signing and announcing the transaction, remains unchanged. + +{{ tutorial.code_snippet_tagged('step-1') }} + +[Download the full tutorial code.]({{ config.repo_url }}/raw/refs/heads/{{config.extra.nem.branch}}/mkdocs/snippets/devbook/transactions/transfer_xem.typed.mjs){ .source-link } + +## Creation Process + +Transactions are created in a type-safe manner in two steps: creating a transaction descriptor and creating the +transaction itself. + +### Creating the Descriptor + +{{ tutorial.code_snippet_tagged('step-2') }} + +Typed descriptors are what provide type safety when building transactions in JavaScript, +because of their constructors with structured parameters. + +See for example the used in the code. + +### Creating the Transaction + +{{ tutorial.code_snippet_tagged('step-3') }} + +Once the descriptor is ready, creating the transaction is straightforward: it simply involves passing the descriptor to +the method and provide the desired fees and deadline. + +Note that, as in the [Creating a Transfer Transaction](./transfer-xem.md#calculating-the-transaction-fee) tutorial, +the transaction's fee must be calculated after construction because it depends on the transaction's contents. + +!!! warning "Deadlines are provided differently in the typed and untyped versions" + + Deadlines passed to are specified in seconds and are relative to the + _network time_. + In contrast, deadlines passed to are specified in seconds + and are relative to the _system time_, that is, the local clock of the machine running the code. + + This approach is convenient because it removes the need to fetch the current network time: for example, + to make a transaction expire in two hours, you only need to provide a deadline of `#!js 2 * 60 * 60` seconds as in + the code above. + + However, if the system clock is not properly synchronized with the network time, transactions may expire earlier + than expected, or be rejected entirely if the provided deadline exceeds the network's maximum allowed offset of 24 + hours. + + **Therefore, applications using the type-safe method should periodically check the network time to ensure the + system clock is properly synchronized.** + +Once the transaction has been created, you can use it normally. +There is no difference between transactions created using the typed and untyped methods. diff --git a/mkdocs/pages/en/devbook/websockets/listen-new-blocks.md b/mkdocs/pages/en/devbook/websockets/listen-new-blocks.md new file mode 100644 index 000000000..8f0f98985 --- /dev/null +++ b/mkdocs/pages/en/devbook/websockets/listen-new-blocks.md @@ -0,0 +1,134 @@ +--- +title: New Blocks +tutorial_level: beginner +--- + +# Listening to New Blocks + +The sends a real-time notification every time a new is +added to the chain. +Compared to polling the endpoint, WebSockets push updates as they happen without the overhead of +repeated API calls. + +This tutorial shows how to subscribe to the channel and display each update as it arrives. + +!!! note "Polling alternative" + + For a polling-based approach, see the + [Querying Chain and Irreversible Height](../chain/chain-heights.md) tutorial. + +## Prerequisites + +NEM serves WebSockets using the [STOMP](https://stomp.github.io/) messaging protocol over +[SockJS](https://github.com/sockjs/sockjs-client), so a STOMP client and a WebSocket transport are required. + +=== ":simple-python: Python" + + Install the `stomper` and `websockets` libraries: + + ```bash + pip install stomper websockets + ``` + +=== ":simple-javascript: JavaScript" + + Install the `@stomp/stompjs` and `sockjs-client` libraries: + + ```bash + npm install @stomp/stompjs sockjs-client + ``` + +See the [WebSocket reference](../reference/websockets/index.md) for details on the connection protocol. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/websockets/listen_new_blocks', ['py', 'js']) }} + +!!! note + + There is no SockJS client library for Python, so a few small helper methods are defined at the top of the file + for convenience. + +The snippet uses the `NODE_URL` environment variable to set the NEM . +If no value is provided, a default one is used. + +`WS_URL` defines the WebSocket endpoint for the same node. +It is derived from `NODE_URL` by replacing port `7890` with `7778`. + +The program runs until interrupted with `Ctrl+C`, which triggers the unsubscribe step before closing the connection. + +## Code Explanation + +### Connecting to the WebSocket + +{{ tutorial.code_snippet_tagged('step-1') }} + +The first step is to open a connection to the node's `/w/messages` endpoint and start a +over it. + +### Subscribing to the Channel + +{{ tutorial.code_snippet_tagged('step-2') }} + +The code subscribes to the channel. +The node then notifies subscribers every time a new block is added to the chain (approximately every minute). + +The subscription is given an `id` (`id-0`) which is used to unsubscribe on exit. + +Each incoming message is then passed to the formatting logic below. + +### Formatting the Message + +{{ tutorial.code_snippet_tagged('step-3') }} + +The body of each incoming message is the new block, following the [Block](../reference/rest/nem.md#model/Block) +schema. + +For each message, the snippet prints two of its fields: + +* `height`: The height of the new block. +* `signer`: The public key of the that produced the block. + +A NEM block does not include its own hash in this payload, so this tutorial identifies each block by its `height` +and also prints the harvester's `signer`. + +!!! warning "New blocks are not yet final" + + New blocks are already part of the chain but not yet irreversible. + Until enough subsequent blocks surpass the , are still possible. + The [Querying Chain and Irreversible Height](../chain/chain-heights.md) tutorial shows how to calculate the + irreversible height. + +### Unsubscribing on Exit + +{{ tutorial.code_snippet_tagged('step-4') }} + +When the program is interrupted (`Ctrl+C`), the code unsubscribes from the channel and ends the STOMP session before +closing the connection. +This ensures a clean disconnection from the node. + +## Output + +The following output shows a typical run listening to new blocks: + +```text linenums="1" hl_lines="2 3 4 9" +--8<-- 'devbook/websockets/listen_new_blocks.log' +``` + +The output shows: + +* **Connection** (line 2): The STOMP session is established over the node's WebSocket endpoint at port `7778`. +* **Subscription** (line 3): The `/blocks` channel is subscribed. +* **New blocks** (lines 4-8): New block notifications arrive approximately every minute. +* **Unsubscribe** (line 9): On `Ctrl+C`, the code unsubscribes and disconnects. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ------------------------------------------------------------- | --------------------------------------------- | +| [Subscribe to the block channel](#subscribing-to-the-channel) | | +| [Format block messages](#formatting-the-message) | [Block](../reference/rest/nem.md#model/Block) | diff --git a/mkdocs/pages/en/devbook/websockets/listen-transaction-flow.md b/mkdocs/pages/en/devbook/websockets/listen-transaction-flow.md new file mode 100644 index 000000000..433d3f168 --- /dev/null +++ b/mkdocs/pages/en/devbook/websockets/listen-transaction-flow.md @@ -0,0 +1,192 @@ +--- +title: Transaction Flow +tutorial_level: beginner +--- + +# Listening to Transaction Flow + +NEM provides that send real-time notifications as a moves +through the confirmation process for a specific . +Compared to polling the endpoint, WebSockets push updates as they happen without the overhead +of repeated API calls. + +This tutorial shows how to subscribe to transaction channels, announce a minimal +[Transfer Transaction](../transactions/transfer-xem.md), and wait for its confirmation using WebSockets. + +!!! note "Alternative: Polling" + + For a polling-based approach, see the + [Monitoring Transaction Status](../transactions/monitoring-status.md) tutorial. + +## Prerequisites + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). +* Have the address of the account to monitor. +* Have an account with enough balance for transaction fees. + See [Creating an Account from a Private Key](../accounts/create-from-private-key.md) or + [Creating an Account by Using a Wallet](../../userbook/wallet/create-account.md). + +Additionally, NEM serves WebSockets using the [STOMP](https://stomp.github.io/) messaging protocol over +[SockJS](https://github.com/sockjs/sockjs-client), so a STOMP client and a WebSocket transport are required: + +=== ":simple-python: Python" + + Install the `stomper` and `websockets` libraries: + + ```bash + pip install stomper websockets + ``` + +=== ":simple-javascript: JavaScript" + + Install the `@stomp/stompjs` and `sockjs-client` libraries: + + ```bash + npm install @stomp/stompjs sockjs-client + ``` + +See the [WebSocket reference](../reference/websockets/index.md) for details on the connection protocol. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/websockets/listen_transaction_flow', ['py', 'js']) }} + +!!! note + + There is no SockJS client library for Python, so a few small helper methods are defined at the top of the file + for convenience. + +The snippet uses the `NODE_URL` environment variable to set the NEM . +If no value is provided, a default one is used. + +`WS_URL` defines the WebSocket endpoint for the same node. +It is derived from `NODE_URL` by replacing port `7890` with `7778`. + +## Code Explanation + +### Setting Up the Monitored Address and Signer + +{{ tutorial.code_snippet_tagged('step-1') }} + +Each transaction channel is scoped to a specific address. +The channels send a notification whenever this address is involved in a transaction, as sender or recipient. +The `MONITOR_ADDRESS` environment variable sets the address to watch. +The WebSocket API expects it uppercase and without hyphens. + +To trigger notifications, this tutorial sends a transfer transaction to the monitored address. +The sender's private key is read from `SIGNER_PRIVATE_KEY`. + +If any of these environment variables is not provided, the tutorial provides default values. + +### Connecting to the WebSocket + +{{ tutorial.code_snippet_tagged('step-2') }} + +The code opens a SockJS connection to the `/w/messages` endpoint on `WS_URL` and starts a +over it. + +### Registering the Account + +{{ tutorial.code_snippet_tagged('step-3') }} + +To receive notifications on an account's transaction channels, the address must first be **registered** with the node. + +Before sending the registration request, the code temporarily subscribes to the channel +using `id-0`, so it can capture the notification that confirms registration. + +The code then sends a request to register the address. + +Once the address is registered, the node sends the account’s current state on the same +channel, which serves as the registration confirmation. + +When the notification arrives, the temporary subscription to the account messages channel is dropped using the same +`id-0` identifier. + +### Subscribing to the Channels + +{{ tutorial.code_snippet_tagged('step-4') }} + +With the account's address registered, the code subscribes to two address-scoped channels: + +* : Notifies when a transaction involving the address enters the , + waiting to be included in a block. +* : Notifies when a transaction involving the address is included in a . + +The subscriptions use IDs `id-1` and `id-2`, which identify them when the code unsubscribes after confirmation. + +### Building and Signing a Transfer Transaction + +{{ tutorial.code_snippet_tagged('step-5') }} + +This tutorial builds a minimal to the monitored address, with a zero amount, no mosaics, and no message. +A transfer is used for simplicity, but any transaction type triggers the same WebSocket notifications. + +The transaction follows the same process described in the +[Transfer Transaction tutorial](../transactions/transfer-xem.md): fetching the network time, creating the transaction, +and signing it. +The hash is computed locally so it can be matched against incoming messages later. + +### Announcing and Waiting for Confirmation + +{{ tutorial.code_snippet_tagged('step-6') }} + +The code announces the transaction to the endpoint and checks the result. +If the node rejected it, the code prints the rejection reason and stops. + +Otherwise, the code waits for confirmation, printing each message from the two subscribed channels. +Each message follows the [TransactionMetaDataPair](../reference/rest/nem.md#model/TransactionMetaDataPair) schema, whose +`meta.hash.data` field holds the transaction hash. + +When a message from the channel arrives whose hash matches the announced transaction, +the program prints a confirmation message and exits. + +!!! warning "Announce after subscribing to channels" + + Always announce the transaction **after** subscribing to the WebSocket channels to ensure the listener is ready. + Otherwise, notifications could arrive before the WebSocket is listening. + +The expected sequence for a successful transaction is described in the +[Transaction Lifecycle](../../textbook/transactions.md#transaction-lifecycle) section: + +1. `unconfirmed`: The transaction enters the . +2. `confirmed`: The transaction is included in a . + +### Unsubscribing from Channels + +{{ tutorial.code_snippet_tagged('step-7') }} + +After confirmation, the code unsubscribes from both channels and ends the STOMP session before the connection closes. + +## Output + +```text linenums="1" hl_lines="2 3 4 5 6 7 8 9 10 11" +--8<-- 'devbook/websockets/listen_transaction_flow.log' +``` + +The output shows: + +* **Address** (line 2): The monitored address. +* **Connection** (line 3): The STOMP session is established over the node's WebSocket endpoint at port `7778`. +* **Registration** (line 4): The account registration is confirmed before subscribing. +* **Subscriptions** (lines 5-6): Both transaction channels are subscribed. +* **Announcement** (line 7): The transaction is announced and its hash is printed. +* **Transaction flow** (lines 8-9): The transaction moves from `unconfirmed` to `confirmed`, showing the confirmation + lifecycle. +* **Confirmation** (line 10): The hash from the `/transactions/{address}` channel matches the announced transaction. +* **Unsubscribe** (line 11): The code unsubscribes from both channels. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| [Register the account](#registering-the-account) | , | +| [Subscribe to the unconfirmed channel](#subscribing-to-the-channels) | | +| [Subscribe to the transactions channel](#subscribing-to-the-channels) | | +| [Handle transaction messages](#announcing-and-waiting-for-confirmation) | [TransactionMetaDataPair](../reference/rest/nem.md#model/TransactionMetaDataPair) | diff --git a/mkdocs/pages/en/index.md b/mkdocs/pages/en/index.md new file mode 100644 index 000000000..c6404232e --- /dev/null +++ b/mkdocs/pages/en/index.md @@ -0,0 +1,49 @@ +--- +hide: + - navigation + - toc +section_name: textbook +disable_actions: true +--- + +# Welcome to the NEM documentation pages + + + + diff --git a/mkdocs/pages/en/textbook/.meta.yml b/mkdocs/pages/en/textbook/.meta.yml new file mode 100644 index 000000000..2a164617d --- /dev/null +++ b/mkdocs/pages/en/textbook/.meta.yml @@ -0,0 +1 @@ +section_name: textbook diff --git a/mkdocs/pages/en/textbook/accounts.md b/mkdocs/pages/en/textbook/accounts.md new file mode 100644 index 000000000..ad90b3e54 --- /dev/null +++ b/mkdocs/pages/en/textbook/accounts.md @@ -0,0 +1,222 @@ +# Accounts + +Account +: A secure place where digital assets like cryptocurrencies or can be stored. + It functions similarly to a safe deposit box in traditional banking. + +In the case of blockchain, accounts are secured by a : assets can only be **transferred out** of an account +by using its private key, but the public key can be shared freely in order to **receive** assets. + +Public keys are commonly shared as an for convenience, so the terms "account" and "address" are used as +synonyms. + +Besides managing digital assets, accounts also represent the ownership of a private key, and act as a form of digital +identity. +On a blockchain, accounts can authorize transactions, configure permissions, and participate in mechanisms. + +!!! note "Account Lifecycle" + + Accounts become active the first time they interact with the blockchain, for example, by receiving assets. + Prior to activation, no information about them is recorded on-chain and they do not appear in block explorers. + + Once activated, an account can be emptied of assets, but it cannot be deleted from the blockchain. + +## Mnemonics + +Mnemonic Phrase +: A human-readable representation of a , typically shown as a list of 12 or 24 random words. + +It is also called just mnemonic, and often used when creating or restoring accounts in . + +NEM uses the [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) standard that requires +24 English words. + +!!! warning "Treat mnemonics as if they were private keys" + + Access to a mnemonic phrase provides full access to all accounts generated from it. + Never share it, and avoid storing it unencrypted in digital form. + +## Wallets + +Wallet +: An application used to manage NEM accounts, initiate and sign them. + +It stores or , and uses them to sign transactions. +More broadly, wallets provide tools for exploring and interacting with the blockchain. + +Wallets can be: + +* :material-application-outline: **Software wallets** + + Applications installed on desktop or mobile devices. + + These typically offer the full range of functionality, at an increased security risk: + the software wallet must be online in order to interact with the blockchain, exposing the stored private keys to + potential compromise, even if protected by a password. + +* :material-integrated-circuit-chip: **Hardware wallets** + + External physical devices that store keys offline. + + These are designed primarily for secure transaction signing and must be connected to a software wallet to operate. + + The private keys they contain never leave the device except when explicitly backed up, making hardware wallets + significantly more secure. + +Most wallets allow managing multiple accounts, QR code scanning (for signing and requesting transaction signatures), +and configuration. +Accounts can be also imported or exported using either or . + +## HD Wallets + +HD Wallet +: A Hierarchical Deterministic (HD) derives a series of from a single seed, + which is more convenient than having to manage multiple . + +This greatly simplifies the management of multiple accounts, but extra caution must be taken to keep the seed safe +because compromising the seed compromises all the accounts derived from it. +The seed is typically a . + +Most wallets are HD wallets. + +NEM uses the [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) standard to generate accounts from +the seed. + +## Multisignature Accounts + +Multisignature Account +: An (called **multisig**) requiring signature from multiple parties (called **cosignatories**) to + approve transactions. + +Multisig accounts are configured by: + +* Defining the list of cosignatories. +* Setting the **minimum number of cosignatories** (**M**) out of the total (**N**) required to authorize a transaction. + This is known as an **M-of-N** multisig. + Setting **M** equal to **N** (an **N-of-N** multisig) requires every cosignatory to sign. + +For example, a **2-of-3** multisig has three cosignatories, any two of which must sign to authorize a transaction: + +```dot +digraph "M-of-N Multisignature" { + rankdir="BT"; + node [fontsize=12]; + "Multisig Account" [label="Multisig Account\n2 of 3"]; + + "Cosignatory 1" [penwidth=2]; + "Cosignatory 2" [penwidth=2]; + + "Cosignatory 1" -> "Multisig Account" [penwidth=2 minlen=2]; + "Cosignatory 2" -> "Multisig Account" [penwidth=2 minlen=2]; + "Cosignatory 3" -> "Multisig Account" [style=dashed minlen=2]; +} +``` + +In the previous diagram, Cosignatories 1 and 2 sign, which meets the minimum of `M=2`, so the transaction is valid +without Cosignatory 3. + +### Use Cases + +* **Shared control over funds or functionality**. + + No action can be performed on the account without approval from the configured number of cosignatories. + + This also mitigates the risk of one of the accounts being compromised. + +* **Multifactor authorization**. + + As a security measure, users can create a multisig so that they need to approve transactions from multiple devices. + +* **Account ownership transfer**. + + Transferring private keys is not a viable mechanism to change ownership of an account, + because the receiver can never be sure that the sender has deleted their copy of the keys. + + To solve this issue, the sender can configure the transferred account as a 1-of-1 multisig, + and set the receiver account as the only cosignatory. + + The account can be transferred again by changing the single cosignatory as many times as needed. + +### Constraints + +Bear in mind the following when designing multisignature solutions: + +* **Maximum number of cosignatories for an account**. + + A multisig account can have at most **32** cosignatories. + +* **Removing cosignatories has special rules**. + + Removing a cosignatory does not require their own signature. + For example, a removal in a **3-of-5** multisig needs at least 3 signatures from the 4 remaining cosignatories, + but a removal in a **5-of-5** multisig needs all 4 remaining signatures. + + A single transaction can remove **at most one** cosignatory. + Removing several requires separate transactions. + + The last remaining cosignatory can remove themselves, which dissolves the multisig. + +* **No nested multisigs**. + + In NEM, a multisig account cannot itself be a cosignatory of another multisig, and a cosignatory account cannot + be converted into a multisig. + Multisig hierarchies are therefore **one layer deep**. + +## Importance + +Importance +: A measure of an 's contribution to the network, based on its balance and its outgoing + transfers to other accounts. + This score determines the account's chances of harvesting a . + +Importance serves a role similar to hashrate in systems or stake in systems: +the higher the value, the greater the chance to harvest a block and earn rewards. + +### Vesting + +Vesting +: The process by which an account's balance gradually matures from _unvested_ to _vested_. + Only the vested portion counts toward the account's importance, so newly funded accounts do not + start harvesting immediately. + +When an account first receives XEM, the full amount is unvested. +Every 1440 blocks (about one day at the 60-second target), 10% of the unvested balance migrates to the vested portion. +The same step repeats each day, gradually moving more of the balance to vested. + +For example: + +* After day 1, 10% of the original balance is vested. +* After day 2, 19% is vested. +* After day 7, just over half is vested. +* The balance asymptotically approaches fully vested. + +Larger holdings cross the 10'000 XEM vested threshold sooner. +An account holding 100'000 XEM, for instance, vests 10'000 XEM at its first vesting cycle (after about one day) +and becomes harvesting-eligible at that point. + +??? info "Importance Calculation" + + All accounts that have at least 10'000 XEM in vested balance are eligible to harvest and participate in the + importance calculation. + + The importance score for an eligible account combines: + + * Its **vested balance**. + * A **[PageRank](https://en.wikipedia.org/wiki/PageRank)-like score** computed over the graph of outgoing transfer + transactions. + + Only transfers that meet both of the following are considered: + + * The transfer occurred within the last 43200 blocks (about 30 days). + * The recipient is itself eligible (has at least 10'000 vested XEM). + + Each qualifying transfer contributes its amount, with older transfers counting for less (10% less per day). + If two accounts sent XEM to each other, only the difference counts. + That difference must be at least 1'000 XEM to contribute to the score. + + The full algorithm is the _Proof-of-Importance_ (PoI) scheme described in the + [NEM Technical Reference](site:/assets/pdfs/NEM_techRef.pdf), section 7. + +!!! note + Importance scores are recalculated every 359 blocks (roughly 6 hours), and the recalculated value applies to all + subsequent blocks until the next recalculation. diff --git a/mkdocs/pages/en/textbook/blocks.md b/mkdocs/pages/en/textbook/blocks.md new file mode 100644 index 000000000..1723b9197 --- /dev/null +++ b/mkdocs/pages/en/textbook/blocks.md @@ -0,0 +1,99 @@ +# Blocks + +Block +: A block records a set of confirmed at a specific point in time. + +In addition to transactions, blocks contain metadata such as a timestamp, the block's height, and the +_previous block hash_ that links each block to its predecessor. +This linkage is what makes the chain a _blockchain_: tampering with any block invalidates every block that follows. + +The NEM network produces one new block every 60 seconds on average. + +## The Nemesis Block + +Nemesis block +: The first block in the NEM blockchain. + Unlike all other blocks, which are created through network consensus, the nemesis block is manually generated by the + network creators. + +```dot +digraph Blockchain { + rankdir=LR; + node [shape=box fontsize=12]; + + Nemesis [label="Nemesis"]; + B2 [label="Block 2"]; + B3 [label="Block 3"]; + B4 [label="Block 4"]; + B5 [label="..." shape=plaintext] + + Nemesis -> B2 -> B3 -> B4 -> B5; +} +``` + +It defines the initial state of the blockchain. +This includes the initial distribution of mosaics, such as , to specific accounts, the creation of namespaces, +and other configuration parameters that set the foundation for the network. + +Because it is the root of the chain, the nemesis block has no previous block hash. +All other blocks are linked back to it directly or indirectly. + +This block is commonly called the _genesis block_ in other blockchain protocols. + +All blocks that follow are created through a process called , NEM's equivalent of mining in other +blockchains. +Harvesters validate transactions, group them into blocks, and add the blocks to the chain, receiving transaction fees as +a reward. + +## Network Time + +Network Time +: NEM defines time as the number of seconds elapsed since the creation of its first block, + known as the . + + All timestamps are calculated relative to this origin. + +UTC timestamps are obtained by adding the network time to the Nemesis block's UNIX timestamp, +which is `1427587585` (`2015-03-29T00:06:25Z`) on . +For other networks, it can be retrieved from the network properties. + +## Block Structure + +Each block in the NEM blockchain contains a combination of metadata and transaction data, including: + +| **Field** | **Description** | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Height** | The block's position in the chain, starting from `1` for the . Each new block has a height one greater than its predecessor. | +| **Timestamp** | Seconds elapsed since the nemesis block, strictly increasing for each block. Average time between blocks is kept close to 60s. | +| **Type** | `-1` for the nemesis block, `1` for regular blocks. | +| **Version** | Encodes the block format version and the network (`1744830465` on mainnet, `-1744830463` on testnet). | +| **Previous block hash** | of the previous block. If its contents were tampered with, this hash would change, breaking the chain and invalidating every successor. | +| **Signature** | Cryptographic signature produced by the harvester over the block's contents. Used by every node to verify block integrity. | +| **Signer** | The account that signs the block, also referred to as the _harvester_. Transaction fees are credited to it, or to the main account when the block is signed by a remote account through or . | +| **Transactions** | A list of valid transactions included in the block. Each transaction is independently verified before being accepted into the block. | + +## Derived Fields + +In addition to the fields above, each node keeps the following values for every block. +They are not part of the block payload: every node computes them from earlier blocks. + +| **Field** | **Description** | +|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Generation hash** | A hash carried forward from block to block, used to determine which accounts are eligible to harvest the next block. Computed from the previous block's generation hash and the harvester's public key. | +| **Difficulty** | A network-wide measure of how hard it is to harvest the next block. Adjusted dynamically from the recent block history to keep the average block time close to 60s. | +| **Lessor** | When the block is signed by a remote account through or , the main account whose backed it and which receives the reward. Resolved from the remote account's delegation state recorded earlier in the chain. | + +## Block Score + +The following quantity is computed for each block, to aid in the process. + +Block Score +: A numerical value assigned to each block that reflects how hard it was to . + +$$ +\textit{block score} = difficulty − \textit{time elapsed since last block} +$$ + +Chain Score +: Sum of the of all blocks in a chain, used to choose between competing . + The chain with the higher score wins. diff --git a/mkdocs/pages/en/textbook/cats.md b/mkdocs/pages/en/textbook/cats.md new file mode 100644 index 000000000..9a958dbd8 --- /dev/null +++ b/mkdocs/pages/en/textbook/cats.md @@ -0,0 +1,429 @@ +# CATS DSL + +CATS +: The **CATS DSL** (humorously backronymed as **Compact Affinitized Transfer Schema**, + and short for **Domain-Specific Language**) is a compact, descriptive language for defining the binary layout of + structured data. + +Originally developed for Symbol and NEM, it is used to specify all blocks and transactions in both protocols, +but its design is general enough to describe any binary format. + +CATS prioritizes size efficiency, performance, and strict typing, aiming at zero-copy deserialization where possible. +Features include fixed-size buffers, strict type aliases, inline structures, and conditionally present fields. + +CATS definitions are processed by _generators_: tools that produce code in a specific programming language to enable +applications to serialize (write) and deserialize (read) CATS-defined binary structures into native language constructs. + +Generators currently exist for Python and JavaScript/TypeScript, with one for Java under development (as of June 2025). +These are used by the NEM SDKs to ensure consistent and efficient binary encoding across platforms. + +This page describes the syntax and features of the CATS DSL. +For full precision, the Symbol source repository contains +[the exact grammar](https://github.com/symbol/symbol/blob/dev/catbuffer/parser/catparser/grammar/catbuffer.lark) +written using the [Lark parsing language](https://lark-parser.readthedocs.io). + +!!! note "Whitespace" + + All CATS statements end with a line feed (semicolons are not used), but whitespace is otherwise not significant. + + Indentation is not required by the parsers, but is conventionally used to add clarity. + +A CATS file is composed of four top-level keywords: `import`, `using`, `enum`, and `struct`. +Each of these is described in the sections below. + +## `import` + +CATS files can include other CATS files using the `import` statement. +This allows schema definitions to be modular and reusable. + +To import another CATS file, specify its filename in quotes: + +```cats +import "other.cats" +``` + +Imported filenames are resolved relative to the include path passed to the parser. + +## `using` + +The `using` statement defines a **type alias** for a built-in primitive type. +These aliases are treated as distinct types by the parser and generators, +enabling strict typing even when two types share the same underlying representation. + +```cats +using = +``` + +CATS supports aliases for two categories of built-in types: + +* **Integer types**: + * Unsigned: `uint8`, `uint16`, `uint32`, `uint64` + * Signed: `int8`, `int16`, `int32`, `int64` +* **Fixed-size binary buffers**: `binary_fixed(N)` defines an N-bytes long buffer. + +For example, to define a `Height` type as an 8-byte unsigned integer: + +```cats +using Height = uint64 +``` + +To define a `PublicKey` type as a 32-byte binary buffer: + +```cats +using PublicKey = binary_fixed(32) +``` + +Although in the following example both `Height` and `Weight` are based on `uint64`, +they are treated as **distinct types** and cannot be used interchangeably: + +```cats +using Height = uint64 +using Weight = uint64 +``` + +## `enum` + +The `enum` statement defines an **enumeration**, a type consisting of named constants backed by an integer type. + +Each enumeration must specify its backing type explicitly, and any of the built-in integer types can be used. + +```cats +enum : + = + ... +``` + +Enumeration members are defined on the lines below the `enum` declaration. +Each member must be assigned a constant integer value. + +For example, to define a `TransportMode` enum backed by a 32-bit unsigned integer: + +```cats +enum TransportMode : uint32 + ROAD = 0x0001 + SEA = 0x0002 + SKY = 0x0004 +``` + +### Enum Attributes + +Enumerations support attributes that modify their behavior. +Each attributes starts with `@` and must appear on the line above the enum declaration. +Currently, the only supported attribute is: + +* `@is_bitwise`: indicates that the enumeration represents a bit field (i.e. a set of flags) + and should support bitwise operations in the generated code. + + For example: + + ```cats + @is_bitwise + enum TransportMode : uint32 + ROAD = 0x0001 + SEA = 0x0002 + SKY = 0x0004 + ``` + + This tells the generator that enum values can be combined using bitwise OR, + and that individual flags may be checked using bitwise AND. + +## `struct` + +The `struct` statement defines a **structured binary layout** composed of named fields. + +Structures are the most important building block in CATS: they are used to describe transactions, blocks, +and all other composite objects. + +Each structure declaration starts with the `struct` keyword, optionally preceded by a _modifier_. +Fields are then defined on the lines following the declaration, giving them a name and a type: + +```cats +[Optional modifier] struct + = + ... +``` + +For example: + +```cats +struct Vehicle + weight = uint32 + wheel_count = uint8 +``` + +### Modifiers + +CATS supports the following modifiers: + +* `abstract`: defines a base struct for inheritance. + Generators produce a factory to instantiate the appropriate derived type. + +* `inline`: indicates that the struct is used only for composition and should not be emitted as a standalone type. + +If no modifier is specified, the struct is included in the generated output as-is. + +### Special Field Constructors + +Fields may also be declared using special constructors instead of a type: + +* `make_const(type, value)`: defines a constant. + This field does not appear in the layout. Instead, it becomes a constant accessible as + `.` in generated code. + + In this example, `TRANSPORT_MODE` is not serialized, but results in a constant `Car.TRANSPORT_MODE` + of type `TransportMode` with value `ROAD`. + + ```cats + struct Car + TRANSPORT_MODE = make_const(TransportMode, ROAD) + ``` + +* `make_reserved(type, value)`: defines a reserved field with a fixed value. + This field is stored in the layout, and always has the provided value. + + In the example below, the field `wheel_count` is stored as a `uint8` with the fixed value `4`. + + ```cats + struct Car + wheel_count = make_reserved(uint8, 4) + ``` + +* `sizeof(type, reference)`: defines a field automatically filled with the size (in bytes) of another field. + This makes structures easier to maintain, since changing a referenced type does not require manually updating + size fields. + + Here, `car_size` is an `uint16` that always contains the size, in bytes, of the field `car`, + which has type `Car`. + + ```cats + struct SingleCarGarage + car_size = sizeof(uint16, car) + car = Car + ``` + +### Conditional Fields + +Fields can be made **conditionally present** based on the value of another field. +This can be used to represent mutually exclusive layouts, similar to unions in other languages. + +Conditional fields use the following syntax: + +```cats + = if +``` + +CATS supports the following conditional operators: + +* `equals`: include the field if the selector field exactly matches the constant value. +* `not equals`: include the field if the selector field does not match the constant value. +* `has`: include the field if all bits in the constant value are set in the selector field (for bit flags). +* `not has`: include the field if any bits in the constant value are **not** set in the selector field. + +For example, the field `buoyancy` is only included when `transport_mode` is equal to `SEA`: + +```cats +struct Vehicle + transport_mode = TransportMode + + buoyancy = uint32 if SEA equals transport_mode +``` + +### Array Fields + +CATS supports both static and dynamically sized arrays, where all elements have the same type. + +The syntax is: + +```cats + = array(, ) +``` + +Where `NumberOfElements` can be: + +* A constant, producing a statically-sized array. + + ```cats + struct SmallGarage + vehicles = array(Vehicle, 4) + ``` + +* A reference to another field, producing a dynamically-sized array. + + For example, the following struct defines a field `vehicles` containing `vehicles_count` elements of type `Vehicle`: + + ```cats + struct Garage + vehicles_count = uint32 + vehicles = array(Vehicle, vehicles_count) + ``` + +* The special keyword `__FILL__` can be used to indicate that the array should extend until the end of the structure. + + In that case, the struct must be annotated with the `@size` attribute ([see below](#struct-attributes)), + referencing a field that holds the total size in bytes. + + ```cats + @size(garage_byte_size) struct Garage + garage_byte_size = uint32 + vehicles = array(Vehicle, __FILL__) + ``` + +!!! note + + `ElementType` must either be: + + * A fixed-size struct, or + * A variable-size struct annotated with its own `@size` attribute + + Otherwise, the parser cannot determine how many elements to read from the byte stream. + +#### Array Field Attributes + +Array fields can be annotated with attributes to control how they are sized, aligned, or sorted. + +Supported attributes include: + +* `@is_byte_constrained`: interprets the array size as a number of bytes instead of element count. +* `@alignment(x [, [not] pad_last])`: aligns elements to `x`-byte boundaries; optionally pads the last element. + + By default, when alignment is used, the final element is padded. + This can be disabled using the `not pad_last` qualifier. + +* `@sort_key(x)`: ensures the array is sorted by the given property. + + For example, this array of `Vehicle` structs is sorted by weight: + + ```cats + struct Garage + @sort_key(weight) + @alignment(8, not pad_last) + vehicles = array(Vehicle, __FILL__) + ``` + +### Inlines + +A structure can be **inlined** within another using the `inline` modifier. +This allows the fields of one struct to be inserted directly into another without nesting. + +For example, the following definition inlines the contents of `Vehicle` into `Car`: + +```cats +struct Vehicle + weight = uint32 + +struct Car + inline Vehicle + max_clearance = Height + has_left_steering_wheel = uint8 +``` + +Since the inlined fields are expanded in place the final layout of `Car` is equivalent to: + +```cats +struct Car + weight = uint32 + max_clearance = Height + has_left_steering_wheel = uint8 +``` + +!!! note "Named inlines" + + A struct can also be inlined with a **name**, which causes its fields to be renamed with that prefix: + + ```cats + = inline + ``` + + In this example, `SizePrefixedString` is inlined into `Vehicle` as `friendly_name`: + + ```cats + struct SizePrefixedString + size = uint32 + __value__ = array(int8, size) + + struct Vehicle + weight = uint32 + friendly_name = inline SizePrefixedString + year = uint16 + ``` + + This expands to: + + ```cats + struct Vehicle + weight = uint32 + friendly_name_size = uint32 + friendly_name = array(int8, friendly_name_size) + year = uint16 + ``` + + The special field `__value__` is renamed to match the name given to the inline (`friendly_name`). + All other fields are renamed with a prefix and underscore, such as `size` becoming `friendly_name_size`. + +### Struct Attributes + +Structures can include attributes that provide hints to code generators or affect layout behavior. +Attributes appear above the `struct` declaration, starting with `@`. + +CATS supports the following struct-level attributes: + +* `@is_aligned`: forces all fields to be aligned to their natural boundaries. +* `@is_size_implicit`: allows the struct to be referenced in a `sizeof(x)` expression. +* `@size(x)`: declares that the field `x` contains the full size of the struct in bytes. +* `@initializes(x, Y)`: initializes field `x` with the constant `Y` defined elsewhere. +* `@discriminator(x [, y...])`: used with `abstract` structs to select the appropriate derived type when decoding, + based on the indicated properties. +* `@comparer(x [!transform] [, y...])`: defines which properties to use to sort or compare instances. + The optional transforms are applied prior to property comparison. + Currently, the only transform supported is `ripemd_keccak_256` for backwards compatibility with NEM. + +For example, this links the field `transport_mode` in `Vehicle` to a constant defined in a derived struct: + +```cats +@initializes(transport_mode, TRANSPORT_MODE) +abstract struct Vehicle + transport_mode = TransportMode + +struct Car + TRANSPORT_MODE = make_const(TransportMode, ROAD) + inline Vehicle +``` + +The constant `TRANSPORT_MODE` can be defined in any struct that extends `Vehicle`. + +* * * + +### Integer Field Attributes + +Integer fields support one attribute: + +* `@sizeref(x [, y])`: sets the value of the field to the size of `x`, optionally adjusted by an offset `y`. + + For example, to store the combined size of `vehicle_size` and `vehicle`: + + ```cats + struct Garage + @sizeref(vehicle, 2) + vehicle_size = uint16 + vehicle = Vehicle + ``` + +## Comments + +Any line that begins with `#` is treated as a comment. + +Comments not directly above a declaration are ignored by the parser. +However, if a comment is placed immediately before a declaration or field, it is treated as **documentation** +and may be preserved in the generated output. + +For example: + +```cats +# This comment is ignored + +# This comment is included as documentation +# and will be associated with the `Height` alias. +using Height = uint64 +``` + +This convention allows adding inline documentation to schemas without affecting the binary layout. diff --git a/mkdocs/pages/en/textbook/consensus.md b/mkdocs/pages/en/textbook/consensus.md new file mode 100644 index 000000000..258847b22 --- /dev/null +++ b/mkdocs/pages/en/textbook/consensus.md @@ -0,0 +1,71 @@ +# Consensus + +Consensus +: The process by which all in the network agree on the current state of the blockchain. + +With consensus, the network preserves a single, consistent timeline of and their , +and therefore the balance and data associated with every . + +Consensus provides two forms of agreement: + +* **Sealing agreement**: + each block correctly links to its predecessor, ensuring the immutability of the chain's history. + +* **Content agreement**: + all transactions in a block comply with the network's rules. + For example, transferring tokens from an account requires a valid signature from its + and a sufficient balance. + +Blocks that violate either form of agreement are _invalid_ and ignored by well-behaved nodes. +Such blocks are not propagated through the network. + +## Conflicts + +In a decentralized network like NEM, may temporarily become disconnected. +This can happen due to latency, connectivity issues, or changes in network topology. + +During _network partitions_, disconnected groups of nodes may temporarily disagree on the most recent blocks, +even though they might all be valid. + +As a result, more than one version of the blockchain may exist for a short time, a situation known as a _fork_. + +Fork +: State where two or more competing chains share a common history but differ in their latest blocks. + +During a fork, for example, queries to different nodes might return different balances for the same account, +depending on whether the queried nodes have seen all the transactions that affect that account. + +When connectivity is restored, nodes might encounter competing blocks for the same height, resulting in a conflict. + +Forks might also occur naturally when two nodes produce a new block at the same time. + +## Conflict Resolution + +When a node becomes aware of a fork, NEM resolves it using a deterministic rule: +the chain with the highest is considered the correct one. + +Nodes on the lower-scoring fork need to _roll back_ any blocks that are no longer part of the +main chain and switch to the better one. + +Rollback +: The process of discarding one or more recently added blocks when a node switches to a better chain, + typically after a fork is resolved. + +Any transactions in the discarded blocks that are not already present in the main chain +return to the and must be re-verified before they can be included in a block again. + +Rollbacks on NEM are usually shallow and rare, affecting only the most recent blocks. + +To prevent very deep chain reorganizations, NEM enforces a _rewrite limit_. + +Rewrite limit +: The maximum depth a rollback can reach on NEM, set to **360 blocks** (approximately six hours). + +Blocks deeper than the rewrite limit cannot be replaced by an alternative chain. +As a result, transactions gradually become effectively irreversible as new blocks are added on top of them. + +The rewrite limit has a second consequence. +A node that keeps adding its own blocks while disconnected builds a separate chain. +If that chain grows past the rewrite limit, switching back would require too deep a rollback, so the node cannot rejoin +on its own. +The two chains form an **unresolvable fork** that an operator must clear by restoring the node to the main chain. diff --git a/mkdocs/pages/en/textbook/cryptography.md b/mkdocs/pages/en/textbook/cryptography.md new file mode 100644 index 000000000..0315e3736 --- /dev/null +++ b/mkdocs/pages/en/textbook/cryptography.md @@ -0,0 +1,154 @@ +# Basic Cryptography + +These are the basic cryptography concepts that underpin NEM's technology. + +## Hashes + +Hash +: A cryptographic hash is a fixed-size string of characters produced by a mathematical function + (called a _hash function_) that maps input data of any size to a unique output. + +Several such functions exist, such as [Keccak](https://keccak.team/keccak.html) or +[RIPEMD-160](https://en.wikipedia.org/wiki/RIPEMD), but they all share the same essential properties: + +* **Determinism**: The same input always produces the same hash. +* **Collision resistance**: It is extremely difficult to find two different inputs that produce the same hash. +* **Irreversibility**: The original input cannot be reconstructed from the hash. + +These properties are critical for ensuring data integrity, verifying authenticity, +and linking together in a blockchain. + +NEM uses **Keccak-256**, **Keccak-512**, and **RIPEMD-160** across key derivation, address generation, signing, and +block hashing. + +!!! warning "NEM uses Keccak, not SHA-3" + + NEM adopted Keccak before it was finalized as SHA-3. + The two algorithms use different padding and therefore produce different outputs for the same input. + As a result, a standard SHA-3 library cannot verify NEM signatures or regenerate NEM addresses. + A Keccak implementation such as [Bouncy Castle](https://www.bouncycastle.org/) is required instead. + + NEM's Java source names its helper methods `sha3_256` and `sha3_512`, but both internally call + `Keccak-*`. + The `sha3_` prefix is historical and does not refer to the final SHA-3 specification. + +## Keys + +Private Key +: A very long, secret number. + The actual value of the private key is meaningless, and it is meant to be kept secret. + It should be impossible to guess by unauthorized parties, and, although it is commonly randomly-generated, + it is extremely unlikely that the same number is generated twice by chance. + +NEM private keys are 32 bytes long, typically represented as 64-character hexadecimal strings. + +Public Key +: A very long number that serves as the public identifier of a and can be disseminated widely. + It can be used to prove that the private key is known without revealing it. + + Although mathematically derived from the private key, the reverse operation is practically impossible with + current technology. + +NEM public keys are 32 bytes long, typically represented as 64-character hexadecimal strings. + +Key Pair +: A matched set consisting of a and its corresponding . + The private key is kept secret by the owner, while the public key is distributed openly. + Together, they enable secure cryptographic operations such as digital signatures and encryption. + +NEM uses key pairs in two places: + +Main Key +: associated with every . + Its private key identifies the account owner and grants full control over the account, including the ability to + transfer funds and announce transactions. + +Remote Key +: associated with every account. + It allows a node to harvest on behalf of another account without exposing the account's
. + +??? warning "Key Security" + + The **private key** in any key pair should be kept secret at all times. + + However, the severity of having a secret key revealed depends on the purpose of that key: + + | Key | Severity | Impact | + | ---------- | -------- | ------ | + | **Main** | 🔴 HIGH | Assets can be transferred out of the account. | + | **Remote** | 🟠 MED | Harmless to the delegating account's funds. An attacker gathering a large number of remote keys could gain substantial harvesting power and influence which blocks are added to the blockchain. Easily reverted by linking another remote account. | + +On NEM, both the private and the public key are 256-bit (32-byte) integers. +The public key is obtained via [Elliptic Curve Cryptography](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) +using [Ed25519](https://ed25519.cr.yp.to), which is defined over a +[twisted Edwards curve](https://en.wikipedia.org/wiki/Twisted_Edwards_curve). + +## Signatures + +Signature +: A digital attachment to a document that certifies that the document is approved by a given . + +The signature is obtained by processing the document with the of the account, +so that anybody can use the associated public key to verify that the signature matches the document, +but only the owner of the private key can produce an identical signature. + +All transactions on NEM are signed, but the signatures required depend on the transaction type and its participants. +For example, transferring assets from a single-owner account to another only requires the signature of the +source account's private key. + +However, transferring assets from a requires the approval of enough +cosignatories to meet the multisig threshold, and must therefore gather multiple signatures before it is considered +valid. + +Signatures on NEM are 512-bit (64-byte) long and use the [Ed25519](https://ed25519.cr.yp.to) algorithm. +Unlike standard Ed25519, which relies on SHA-512, NEM uses the **Keccak-512** hash function +(see [NEM uses Keccak, not SHA-3](#hashes)). + +## Addresses + +Address +: A convenient, shorter form of a , that simplifies sharing it by requiring only + letters and numbers. It is typically a synonym for . + +Keys, both public and private, are binary data which is hard to print and share, whereas addresses are made up of +only latin letters and numbers. + +Moreover, NEM keys require 32 bytes of binary data, or 64 hexadecimal characters. +Addresses, on the other hand, only require 40 characters, reaching a compromise between length and practicality. + +On NEM, addresses are obtained from public keys by: + +1. Applying [Keccak-256](https://keccak.team/keccak.html) to the public key to produce a 32-byte hash. +2. Applying [RIPEMD-160](https://en.wikipedia.org/wiki/RIPEMD) to the result to produce a 20-byte hash. +3. Generating a 25-byte **raw address** by joining: + + * A 1-byte network version: `0x68` for (`N`), `0x98` for (`T`), or `0x60` for + (`M`). + * The 20-byte RIPEMD-160 hash from step 2. + * A 4-byte checksum to detect mistyped addresses, computed as the first 4 bytes of the `Keccak-256` hash of the + previous 21 bytes (network version + RIPEMD-160 hash). + +4. Generating a 40-character **encoded address** by [Base32-encoding](https://en.wikipedia.org/wiki/Base32) the raw + address. + + The encoded address is the most common way of sharing addresses because it only uses uppercase letters and digits. + + Example: `NBHK6WHL5TGBMCLVW4RSFMRO4ZYXCJFRAVO2B4FU` + +5. Optionally, for easier reading, hyphens can be added every 6 characters to create a 46-character **pretty address**. + + Example: `NBHK6W-HL5TGB-MCLVW4-RSFMRO-4ZYXCJ-FRAVO2-B4FU` + +!!! note "Addresses are tracked only once used" + + NEM only starts tracking an address and its associated public key when they first appear in a transaction. + +## Vanity Addresses + +While keys, and therefore too, are normally generated randomly, it is possible to create +**vanity addresses** that include specific patterns or prefixes. + +This involves generating repeatedly until one produces an address that meets the desired criteria. +The process usually requires substantial time and computation depending on the complexity of the pattern. + +Vanity addresses can be useful for branding, visibility, or personal preference, but they offer no security advantage. diff --git a/mkdocs/pages/en/textbook/glossary.md b/mkdocs/pages/en/textbook/glossary.md new file mode 100644 index 000000000..73aa23f6c --- /dev/null +++ b/mkdocs/pages/en/textbook/glossary.md @@ -0,0 +1,234 @@ +# Glossary + +AMA +: Ask Me Anything. + An open questions session. + +AML +: Anti Money Laundering. + +APAC +: Asia and Pacific region. + +APR +: Annual Percentage Rate. + +Arbitrage +: When a trader purchases an asset in a market and sells it in a different one, to profit from a deviation in prices + between markets. + +Backrunning +: To broadcast ``transactionA`` with slightly lower gas (or fees) than an already pending ``transactionB`` so that + ``transactionA`` gets mined *right after* ``transactionB`` in the same block. + +BLS +: A [Boneh–Lynn–Shacham](https://en.wikipedia.org/wiki/BLS_digital_signature) signature is a cryptographic signature + scheme which allows a user to verify that a signer is authentic. + +BTC +: Bitcoin. + +CBDC +: Central Bank Digital Currency. + +CEX +: Centralized Exchange, as opposed to Decentralized Exchanges (). + +CLI +: Command-Line Interface. A Program which is entirely used from a terminal console, using only the keyboard. + +CMC +: Coin Market Cap. A web page with cryptocurrency information. + +CSD +: Central Securities Deposit. + +DAO +: Decentralized Autonomous Organization. + An organization whose governance happens completely on a blockchain. + +Dapp +: Decentralized Application. An application that runs on a blockchain instead of a single computer. + The term is slightly abused so, in a more general sense, it also means any application which makes use of a + blockchain. + +DDH +: Decisional [Diffie-Hellman](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange). + +DD +: Due Diligence. + +DeFi +: Decentralized Finance, as opposed to Traditional Finance (). + +DEX +: Decentralized Exchange, as opposed to traditional Centralized Exchanges (). + +DoS +: Denial of Service. + An attack in which a single source floods a server or network with excessive requests, + overwhelming its resources and rendering it unable to respond to legitimate traffic. + + The most common variant is the DDoS (Distributed Denial of Service) attack that involves multiple sources, + often using compromised devices without the owners' knowledge. + +DTC +: Direct To Consumer, i.e. mass market. + +E2E +: End-To-End. + +EMEA +: Europe, Middle-East and Africa. + +ERC +: Ethereum Request for Comment. + Commonly utilized to refer to a token standard on the EVM + (such as ERC-20, ERC-721, or ERC-1155). + +ETH +: Ethereum. + +EVM +: Ethereum Virtual Machine. + +FFT +: [Fast Fourier Transform](https://en.wikipedia.org/wiki/Fast_Fourier_transform). + +Frontrunning +: To broadcast ``transactionA`` with slightly higher gas (or fees) than an already pending + ``transactionB`` so that ``transactionA`` gets mined *right before* ``transactionB`` in the same block. + This is important in case of markets, where gains can be made from frontrunning. + +Hardware wallet +: A device designed to store and produce signatures with them. + The keys are stored in an encrypted memory and never leave the device, so hardware wallets are deemed one of + the most secure ways to access an account. + They typically only provide signing functionality, so they must be paired with a software or + application that creates the and announces them. + +HTLC +: Hashed Time-Lock Contract. + +ICO +: Initial Coin Offering. + +Inflation +: A small amount of that is freshly minted with each new to reward the that creates it. + Inflation began 48 hours after network launch in March 2021, starting at approximately 200 XYM per block. + The reward decreases gradually over time following a slow curve, reaching 1 XYM per block after 30 years, + and disappearing entirely after 105 years. + +IP +: Intellectual Property. + +IRS +: Internal Revenue Service. Who you pay your taxes to if you live in the United States or are an American citizen. + +KYC +: Know Your Customer. Related to . + +LATAM +: Latin America (Central and South America). + +mainnet +: NEM's Main Network, where transactions with real value happen, as opposed to the . + +MEV +: Miner-Extractable Value, or Maximal-Extractable Value, is the process of reorganizing transactions inside a block + by miners, to gain *something*. Uses , , or . + +mijinnet +: A permissioned NEM network originally intended for enterprise deployments, distinct from the public + and . + +NAM +: North America. + +NEM +: The New Economy Movement. + +NFT +: A non-fungible , a way to represent individual entities as a blockchain-based asset. + +NIS1 +: The first version of 's blockchain node that operates the public with the native currency . + First launched on March 31, 2015. + +PoC +: Proof of Concept, i.e., a prototype (not a consensus protocol). + +PoI +: Proof of Importance. + The consensus protocol used by NEM. + Similar to but measuring an account's activity besides its stake. + +PoS +: Proof of Stake. A consensus protocol, used, for example, by Ethereum. + +PoW +: Proof of Work. A consensus protocol, used, for example, by Bitcoin. + +Rug Pull +: A malicious maneuver where cryptocurrency developers abandon a project and run off with the funds. + +Sandwich +: A type of technique that is popular in . + To make a sandwich, you find a pending transaction in the network and then try to surround it by placing one order + *just* before the transaction () and one order just after it (). + +SDK +: Software Development Kit. A Software library used to simplify creating applications for a given platform. + +Sharding +: An Ethereum [scaling solution](https://ethereum.org/en/developers/docs/scaling/#sharding). + +SXDH +: Symmetric External [Diffie-Hellman](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange). + +Sybil Attack +: An attack in which a single adversary creates many fake identities or accounts to gain disproportionate influence + over a network or consensus process. + Common countermeasures include or , which tie influence to scarce resources. + +Symbol +: A blockchain platform created by the NEM project, launched in March 2021 as an evolution of . + +testnet +: NEM's Test Network, intended for development. + Test can be freely obtained from a [faucet](../devbook/accounts/testnet-faucet.md), so transactions on this + network do not have real value, as opposed to transactions on the . + +TLC +: Tender Loving Care. + +TLS +: Security protocol used to encrypt communication between peers on a network. + +Token +: A representation of a digital asset. + On NEM they are called . + +TPS +: Transactions Per Second. + +TradFi +: Traditional Finance, as opposed to Decentralized Finance (). + +USP +: Unique Selling Proposition or Unique Selling Point. + A characteristic of a product that can be used in advertising to differentiate it from its competitors. + +VPS +: Virtual Private Server. + A virtual machine typically hosted on a data center which can be accessed remotely and treated as if it was a + conventional physical machine. + +VRF +: Verifiable Random Function. + +XEM +: The native currency of the NEM blockchain. + +XYM +: The native currency of the blockchain. diff --git a/mkdocs/pages/en/textbook/harvesting.md b/mkdocs/pages/en/textbook/harvesting.md new file mode 100644 index 000000000..ea72bd0b9 --- /dev/null +++ b/mkdocs/pages/en/textbook/harvesting.md @@ -0,0 +1,172 @@ +# Harvesting + +Harvesting +: The process by which NEM adds new to the chain and distributes rewards to participating . + It plays a similar role to **mining** in or **staking** in . + +Each new block is produced by a single on behalf of one of its . +A node's chance of producing the next block is weighted by the combined of its harvester accounts. + +Harvester account +: An account participating in harvesting. + Its importance determines its chance of producing blocks, and it receives the rewards from each block it harvests. + +The fees from the included in the block are paid in full to a single harvester account, the one whose +importance backed the block. + +## Eligibility + +Unlike mining in , harvesting does not require specialized hardware. + +Participation in harvesting is open to any that: + +* Holds at least 10'000 in balance. +* Is connected to a , either directly or through delegation. + +The account's score determines how often it can harvest. + +## Harvesting Process + +NEM has no central coordinator to determine which node will harvest the next block. +Instead, every independently competes by running the same deterministic eligibility check with each of its +. + +To do this, a _target_ value is calculated based primarily on each account's . +The higher the importance, the higher its target will be. + +For each of its harvester accounts, the node computes a number called the _hit_ from the candidate block's +[generation hash](./blocks.md#derived-fields). + +If any of its harvester accounts produces a hit below the target, the node assembles a candidate block +from the and announces it to the rest of the network. + +Other nodes then verify the block, ensuring: + +* The block signature comes from the claimed harvester. +* The are valid. +* The hit is indeed lower than the target. + +If any of these checks fail, other nodes simply ignore the new block. +The mechanism makes sure that the node eventually adopts a block that the rest of the network agrees on. + +If the block is valid, it is accepted by other nodes that include it in their copies of the chain. +The cycle repeats at the next block height. + +!!! info "Simultaneous Block Creation" + Note that no special measures are in place to prevent multiple nodes from generating blocks at the same height. + When this occurs, the network may temporarily as different nodes adopt different blocks for the same + position in the chain. + + The mechanism resolves these conflicts as nodes become aware of the competing blocks. + +??? abstract "Target and Hit Calculation" + + * The **target** is calculated independently by each node and reflects the likelihood of harvesting the next block + using a specific account. + It depends on three factors: + + * The account's score: more active or better-funded accounts will harvest more often. + * The network-wide **difficulty**, which adjusts dynamically based on recent block production times, + to maintain a constant rate. + * The **time elapsed** since the last block: longer delays increase the chance of a new block being produced. + + * The **hit** is derived deterministically from the block's [generation hash](blocks.md#derived-fields), which is + itself the hash of the previous block's generation hash combined with the harvester's . + The hit therefore depends on the full chain of past harvesters and on who is attempting to + harvest now. + + For the block to be valid, the node's target must be **greater than** its hit. + A higher importance or a longer delay increases the target, while a higher difficulty decreases it. + +## Harvesting Methods + +Node owners can participate in harvesting by enabling [local](#local-harvesting) or [remote](#remote-harvesting) +harvesting, depending on their preferred balance between simplicity and security. +Accounts that do not operate a node but meet the balance requirements can still harvest by linking to a node through +[delegated harvesting](#delegated-harvesting). + +### Local Harvesting + +Local Harvesting +: A type of where the rewards are sent directly to the harvester account. + The signs produced using the operator's
, which must be stored on the machine. + +!!! warning + The harvester account must hold a significant balance to maintain a high score. + Storing its private key on a machine that is permanently online puts the entire balance at risk + in case of unauthorized access. + +While local harvesting offers a straightforward setup, these security risks make it unsuitable for public nodes. +Most operators instead prefer remote harvesting. + +### Remote Harvesting + +Remote Harvesting +: A type of that delegates block signing to a separate , while the node's + score and rewards remain tied to the operator's
. + +The remote account holds no funds and exists only to sign blocks on behalf of the harvester's main account. +Because its is stored in the node's configuration files, hosted on a permanently-online machine, it is +designed to be expendable. + +The remote account is designated by signing an _Account Key Link_ transaction, which transfers the main account's + to it. +The remote account begins signing blocks after a settling period of 360 blocks (approximately six hours), and a second +_Account Key Link_ transaction removes it, subject to the same delay. + +The main account still determines the node's importance and receives all block rewards. +However, its key remains offline, safe from compromise. +For simplicity, the main account is still called the harvester account, even though blocks are signed by the remote +account. + +This separation of duties offers strong protection for the harvester's funds and makes remote harvesting the preferred +option for most operators. + +### Delegated Harvesting + +Delegated Harvesting +: A form of that lets an eligible account that does not operate a node delegate harvesting duties to a + third-party node. + The delegating account's score is used, and it receives the harvested rewards in full. + +Such an account is called a _delegator_, or _delegated harvester_. + +Delegator +: An account that harvesting to a third-party node while retaining its + and receiving the harvested rewards. + Also called a _delegated harvester_. + +Although the node performs the work, the delegator is still considered the harvester, and NEM pays it the block rewards +in full. +The arrangement lets an account earn rewards without running a node of its own. + +Delegated harvesting uses the same remote account setup as remote harvesting. +The delegator provides the remote account's to the third-party node, which adds the account to the set it +harvests for and signs blocks on the delegator's behalf. + +Whether the node accepts the remote account depends on the operator's policy, and the delegator can revoke the +arrangement at any time by changing its linked key. + +As with remote harvesting, block signing is performed by an account other than the delegator, so its +never needs to leave secure storage. + +!!! info "Remote vs. Delegated Harvesting" + + Both methods use the same remote account setup. + They differ only in who runs the node: in remote harvesting the operator harvests through their own node, while in + delegated harvesting the account harvests through a third party's node. + +## Reward Distribution + +When a is harvested, the harvester receives the sum of fees from every in the block. + +With , the harvester signs its own blocks and receives the rewards directly. +With and , the remote account signs the blocks, but the rewards still +flow to the main account, never to the remote account or to the node operator hosting it. + +NEM does not split block rewards between the harvester and the node operator. +A node that hosts a remote account for someone else receives nothing from those blocks. +The protocol pays the harvester in full. + +How node operators are compensated for hosting delegated harvesters, if at all, falls outside the protocol and is left +to arrangements between the parties involved. diff --git a/mkdocs/pages/en/textbook/intro.md b/mkdocs/pages/en/textbook/intro.md new file mode 100644 index 000000000..7b3506c66 --- /dev/null +++ b/mkdocs/pages/en/textbook/intro.md @@ -0,0 +1,11 @@ +--- +title: Welcome +--- + +# Welcome to the Textbook + +This book explains the concepts that power the NEM blockchain. + +The [User Manual](../userbook/intro.md) and the [Developer Manual](../devbook/intro.md) already provide links to the appropriate textbook pages when needed, so there is typically no need to read this book cover to cover. + +Anyway, feel free to browse the textbook using the navigation menu! diff --git a/mkdocs/pages/en/textbook/mosaics.md b/mkdocs/pages/en/textbook/mosaics.md new file mode 100644 index 000000000..5a4fc5587 --- /dev/null +++ b/mkdocs/pages/en/textbook/mosaics.md @@ -0,0 +1,197 @@ +# Mosaics + +Mosaic +: A representation of an asset on the NEM blockchain, commonly called tokens on other protocols. + For example: currencies, licenses, collectibles, access rights, or voting power. + +Unlike smart contract-based tokens on other platforms, NEM mosaics are supported directly at the protocol level, +and require no additional coding to use. + +Each mosaic defines a new type of asset, and the individual tokens that belong to this type are called _mosaic units_. +Mosaics can represent fungible assets, such as coins, where each unit is interchangeable, and non-fungible assets, +such as paintings or , where each unit is unique. + +A mosaic lives under a registered , is identified by a [fully qualified name](#fully-qualified-name), +and inherits its duration from its parent namespace's lease (see [Lifetime](#lifetime)). + +## Name + +The name identifies a mosaic within its namespace and must be unique within it. +It follows specific formatting rules: + +* It can only contain lowercase letters, numbers, hyphens `-`, underscores `_`, and apostrophes `'`. +* It must start with a letter or a number. +* It can be at most **32 characters** long. + +Once registered, a mosaic name cannot be changed. + +## Fully Qualified Name + +A mosaic's **fully qualified name** (also called its **mosaic ID**) is the unique identifier on the network. +It joins the namespace and the local mosaic name with a colon, in the form `:`: + +| Part | Definition | Length limit | +| --------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| **Namespace** | One root namespace and up to two subnamespaces, separated by dots, before the `:`. | 16 characters (root) and 64 characters (each subnamespace). | +| **Mosaic name** | The local name after the `:`. | 32 characters. | + +!!! note "The colon is just a separator" + + The colon separates the namespace from the mosaic name and is not a character in either. + Some applications display it as `.` or `!` instead. + +Examples: + +* `nem:xem`: The native network currency. +* `mycompany.tokens:goldcoin`: A hypothetical company token. + +## Description + +Each mosaic can have a **description**: a free-text field of up to **512 characters** documenting the asset, for example +its purpose, origin, or terms of use. + +## Properties + +Mosaics expose behavioral properties that govern how they can be transferred and how their supply evolves. + +### Divisibility + +Divisibility +: Defines how many decimal places a mosaic quantity can have. + A mosaic with divisibility `0` is indivisible: it can only be transferred in whole units. + Higher values allow fractional units. + +For example, a divisibility of `2` means each _whole unit_ can be divided into 100 _fractional units_ (10^2^), +allowing the mosaic to be handled in increments of `0.01`. + +Fractional units are also called _atomic units_, so in this example, 1 whole unit consists of 100 atomic units. + +In many other protocols, this value is hardcoded. +For example, Bitcoin uses 8 decimal places, and Ethereum uses 18. +NEM allows each mosaic to define its own divisibility, depending on the needs of the asset it represents. + +The maximum allowed divisibility in NEM is `6`. + +### Initial Supply + +Defines the total number of mosaic units created at issuance. + +A mosaic's total supply cannot exceed **9 × 10^15^** atomic units, regardless of divisibility. + +The supply is fixed unless [supply mutability](#supply-mutability) is enabled. + +### Supply Mutability + +Indicates whether the mosaic's total supply can be increased or decreased after creation. +It allows for dynamic issuance or removal of mosaic units, depending on the desired asset lifecycle. + +Only the account that created the mosaic can modify its total supply. +These changes affect only the creator's balance: + +* When _minting_ (increasing the supply), new units are created and added to the creator's account. +* When _burning_ (decreasing the supply), existing units are removed from the creator's account. + If the account does not have enough balance, the operation fails. + +### Transferability + +Specifies whether the mosaic can be freely transferred between accounts. +If disabled, every transfer must involve the creator's account as either sender or recipient. + +```dot +digraph "Transferability" { + rankdir="LR"; + node [fontsize=12]; + "Mosaic Creator"; + "Account A"; + "Account B"; + + "Mosaic Creator" -> "Account A" [dir=both]; + "Mosaic Creator" -> "Account B" [dir=both]; + "Account A" -> "Account B" [dir=both style=dashed labeldistance=7 labelangle=-60 + minlen=4 headlabel="Only if mosaic\nis transferable"]; + + { rank = same; "Account A"; "Account B"; } +} +``` + +## Levy + +Levy +: An optional fee attached to a mosaic, paid to a designated account on every transfer of that mosaic, on top of the + transaction fee. + +A levy specifies four fields: + +| Field | Description | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Type** | `Absolute` (fixed quantity) or `Percentile` (proportional to the amount transferred). | +| **Recipient** | Account that receives the levy on every transfer. | +| **Mosaic ID** | The mosaic in which the levy is paid. It may differ from the mosaic being transferred. | +| **Fee** | Quantity of the levy mosaic. For `Absolute`, it is the exact quantity charged on every transfer in atomic units. For `Percentile`, it is interpreted in **basis points**: the levy equals `Fee / 10000` of the transferred amount. | + +When a transfer transaction includes a mosaic with a levy, the network charges the levy to the sender and credits it +to the levy recipient, in addition to the regular transaction fee. + +The levy is paid on top of the transferred amount: the recipient receives the full quantity sent, and the sender is +debited for both the transfer and the levy. + +For example, a `Percentile` levy with `Fee = 100` (100 basis points) charges 1% of the transferred amount. +Sending `1 000` atomic units debits the sender `1 010` in total: `1 000` credited to the recipient and `10` credited +to the levy recipient. + +The sender must hold enough balance to cover the transferred amount and the levy in the levy mosaic, which may differ +from the one being transferred. + +Levies are not recursive. +If the mosaic used to pay the levy carries its own levy, that second levy is not applied. + +!!! warning "A levy is not a guaranteed charge" + + Because levies are not recursive, they can be sidestepped. + For example, if mosaic `A`'s levy is paid in mosaic `B`, and another mosaic `C`'s levy is paid in `A`, transferring + `C` moves `A` as `C`'s levy without triggering `A`'s levy. + A levy is therefore a best-effort fee, not an enforceable guarantee on every transfer. + +## Lifetime + +A mosaic has no duration of its own. +Its lifetime is tied to the lifetime of its parent namespace: + +* While the namespace is active, the mosaic can be transferred and its supply can be changed (subject to its +[properties](#properties)). +* When the namespace expires, the mosaic becomes inactive: transfers and supply changes are rejected, but existing + balances are preserved. +* If the original owner renews the namespace during the [grace period](./namespaces.md#duration), the mosaic and its + balances become usable again. + +!!! warning "Mosaic loss past the grace period is permanent" + After the grace period, the mosaic is permanently lost. + Accounts holding the mosaic lose access to their balances and the mosaic cannot be transferred. + + Registering a mosaic with the same name later creates a new asset, not a recovery of the original. + +See [Namespace duration](./namespaces.md#duration) for the namespace lease lifecycle. + +## Creation Fee + +Registering a new mosaic requires paying a one-time _creation fee_ of **10 XEM**. + +The fee must be paid at the time of creation and is non-refundable. + +!!! note "Transaction fee vs. creation fee" + Creating a mosaic requires announcing a transaction, which also has an associated fee. + However, this transaction fee is typically negligible compared to the creation fee. + +## Modifying a Mosaic + +After creation, the original creator can modify a mosaic's **definition**. + +Each field has its own rule: + +* **Description**: can be changed at any time. +* **Divisibility**, **initial supply**, **supply mutability**, and **levy**: can be changed only while the creator holds + the entire mosaic supply. +* **Transferability**: cannot be changed once set. +* **Name**: cannot be changed. + +The total supply can be changed (mint or burn) if [supply mutability](#supply-mutability) is enabled. diff --git a/mkdocs/pages/en/textbook/namespaces.md b/mkdocs/pages/en/textbook/namespaces.md new file mode 100644 index 000000000..59039b325 --- /dev/null +++ b/mkdocs/pages/en/textbook/namespaces.md @@ -0,0 +1,139 @@ +# Namespaces + +Namespace +: A registered name leased to an , used to prefix and group defined under it. + +Namespaces let an account group related mosaics under a meaningful prefix like `mycompany.tokens`. +The network's own currency follows the same pattern: `nem:xem`, commonly known as , lives in the `nem` namespace. + +The account that registers a namespace is called its _owner_. +The owner controls which mosaics can be defined under it, so namespaces provide both naming structure and +ownership scoping. + +Because namespaces are a limited resource, they are leased for a fixed period rather than owned permanently, +but leases can be renewed. + +## Subnamespaces + +Namespaces in NEM follow a hierarchical structure, similar to internet domain names. +Each name consists of one to three parts separated by dots, for example, `foo`, `foo.bar`, or `foo.bar.baz`. + +The first part is called the _root namespace_. +Any additional parts are _subnamespaces_, which must be registered separately under the root. + +Root namespace +: A namespace that has no parent. + It can be used to group subnamespaces together in a hierarchical manner. + +Subnamespace +: A namespace that belongs to a parent namespace, either the root or another subnamespace. + It is also called a _child namespace_. + Subnamespaces expire when the root namespace expires (see [Duration](#duration)). + +## Name + +Each namespace has a unique name that identifies it on the network, and must follow specific formatting rules: + +* Names can only contain lowercase letters, numbers, hyphens `-`, and underscores `_`. +* Names must start with a letter or number. +* Root names can be at most 16 characters long. +* Root names `nem`, `user`, `account`, `org`, `com`, `biz`, `net`, `edu`, `mil`, `gov`, and `info` are reserved by + the protocol and cannot be registered. +* Subnamespace names can be at most 64 characters long. + +Once registered, a name cannot be changed. + +## Duration + +When a root namespace is registered, it is leased for approximately one year (525600 blocks on ). +During this time, the owner can perform operations such as: + +* Define under the namespace. +* Create subnamespaces. +* Renew the root namespace. + Subnamespaces do not need to be renewed, as they have the same duration as their root namespace. + +Renewal while the namespace is registered is restricted to the **last 43200 blocks before expiration** +(approximately 30 days on ). +Each renewal sets the new expiry one year from the renewal block, not from the previous expiry, so a namespace cannot be +prepaid for multiple years in advance. + +If the namespace is not renewed before expiration, it enters an approximately 30-day _grace period_ (43200 blocks). +During this time, the namespace is effectively disabled: mosaics defined under it become inactive +(see [Mosaic lifetime](./mosaics.md#lifetime)) and the namespace is not yet available for others to register. + +Only the original owner can renew the namespace during the grace period. +Once the grace period ends, the namespace is fully released and becomes available for others to register. + +```dot +digraph "Namespace registration" { + rankdir="LR"; + fontsize=12; + Available [label="Namespace\nis\navailable"]; + Registered [label="Namespace\nis\nregistered"]; + "Grace Period" [label="\nGrace Period\n "]; + "Available Again" [label="Namespace\nis\navailable again"]; + + Available -> Registered [label="Registration"]; + Registered -> "Grace Period" [label="Expiration"]; + Registered -> Registered [label="Renewal"]; + "Grace Period" -> Registered [label="\nRenewal" constraint=false]; + "Grace Period" -> "Available Again" [label="Release"]; +} +``` + +The following operations are permitted depending on the state of the namespace registration: + +| Operation | Namespace Available | Namespace Registered | Grace Period | +| ----------------------------------- | :-----------------: | :--------------------: | :----------------: | +| Register the namespace | :white_check_mark: | :material-close: | :material-close: | +| Register a subnamespace | :material-close: | :white_check_mark: | :material-close: | +| Define a mosaic under the namespace | :material-close: | :white_check_mark: | :material-close: | +| Renew the namespace | :material-close: | :white_check_mark: | :white_check_mark: | + +!!! note "The `nem` namespace never expires" + + The `nem` namespace, which holds , is permanently active and is exempt from the lease and renewal cycle. + +## Lease Fee + +Registering a namespace requires paying a lease fee in the network currency (): + +* **Root namespace:** 100 XEM per registration or renewal. +* **Subnamespace:** 10 XEM, paid once at registration. + +This reflects the fact that namespaces are a limited global resource and helps prevent name squatting. + +The fee must be paid at the time of registration or renewal, and is non-refundable. + +!!! note "Transaction fee vs. lease fee" + Registering or renewing any kind of namespace requires announcing a transaction, which also has an associated fee. + However, this transaction fee is typically negligible compared to the lease fee. + +## Ownership + +A namespace is controlled by the account that registered the root namespace. + +Only the owner can: + +* Define mosaics under the namespace. +* Create subnamespaces. +* Renew the root namespace. + +Namespace ownership cannot be transferred directly. +Instead, control must be transferred by handing over the owner account, for example, using a . + +Subnamespaces always share the same owner as the root and cannot be managed separately. + +## Summary + +The following table summarizes the main numerical limits related to namespaces. + +| Limit | Value | Notes | +| ------------------------------------------- | ---------------------- | -------------------------------------------------- | +| Maximum depth of namespace hierarchy | 3 levels | Root + up to 2 subnamespaces | +| Maximum length of a root namespace name | 16 characters | | +| Maximum length of a subnamespace name | 64 characters | Applies to each sublevel individually | +| Allowed characters in namespace names | `a–z`, `0–9`, `-`, `_` | Must start with a letter or number | +| Default root namespace duration | 525600 blocks | Approximately 1 year on mainnet | +| Namespace grace period after expiration | 43200 blocks | Approximately 30 days on mainnet | diff --git a/mkdocs/pages/en/textbook/nodes.md b/mkdocs/pages/en/textbook/nodes.md new file mode 100644 index 000000000..1fe42b658 --- /dev/null +++ b/mkdocs/pages/en/textbook/nodes.md @@ -0,0 +1,248 @@ +# Nodes + +Node +: A computer running the NEM software which shares information with peer nodes, validates incoming , + and participates in and block creation. + +Nodes form the backbone of the blockchain, ensuring the network remains functional as long as enough nodes are active. + +Anyone can run a NEM node. +Operators do so to blocks with their own account, to host for others, +or to qualify for the . + +## Node Structure + +Every NEM node runs the same application, called _NIS_. + +NIS +: NEM Infrastructure Server. + A single Java process that implements all node functionality. + +NIS has four parts: an [engine](#engine), a [REST API](#rest-api), a [WebSocket](#websocket) service, and an embedded +[database](#database). +The engine is the core, exposed through the REST API and WebSocket service, while the database stores the blockchain. + +NIS exchanges data with other nodes and with clients: + +* _Other nodes_ are NIS peers on the network. +* _Clients_ are external programs such as wallets, explorers, and applications. + +```dot +digraph NemNode { + layout=neato; + splines=ortho; + node [shape=box]; + edge [penwidth=1.5 dir=both]; + + // Layer labels + LblExt [label="External" shape=plain pos="-1.7,6!"]; + LblInt [label="Interface" shape=plain pos="-1.7,4!"]; + LblProc [label="Processing" shape=plain pos="-1.7,2!"]; + LblStor [label="Storage" shape=plain pos="-1.7,0!"]; + + // External actors + OtherNodes [label="Other nodes" style=dashed fixedsize=true width=2 height=0.8 pos="1,6!"]; + Clients [label="Clients" style=dashed fixedsize=true width=2 height=0.8 pos="5,6!"]; + + subgraph cluster_nis { + label=""; + style="rounded,dashed"; + + // Core components + REST [label="REST API" style=filled fixedsize=true width=2 height=0.8 pos="1,4!" URL="#rest-api"]; + WebSocket [label="WebSocket" style=filled fixedsize=true width=2 height=0.8 pos="5,4!" URL="#websocket"]; + Engine [label="Engine" style=filled fixedsize=true width=6 height=0.9 pos="3,2!" URL="#engine"]; + H2 [label="Blocks (H2)" style=filled shape=cylinder fixedsize=true width=2.6 height=0.95 pos="3,0!" URL="#database"]; + NISLabel [label="NIS" shape=plain pos="3,-1.1!"]; + + // Invisible spacers so the NIS box fully encloses REST and WebSocket + spcL [shape=point style=invis pos="-0.3,4.85!"]; + spcR [shape=point style=invis pos="6.3,4.85!"]; + } + + // Midpoint waypoints pin the three Engine arrows to straight verticals, + // so the labels beside them cannot deflect the arrows off-centre + pR [shape=point width=0.01 style=invis pos="1,3.0!"]; + pW [shape=point width=0.01 style=invis pos="5,3.0!"]; + pB [shape=point width=0.01 style=invis pos="3,1.0!"]; + + // Waypoints for the squared Clients <-> REST route + cw1 [shape=point width=0 style=invis pos="3,4!"]; + cw2 [shape=point width=0 style=invis pos="3,6!"]; + + // Labels sit right beside their arrows + reqLbl [label="requests" shape=plain pos="1.6,3.0!"]; + evtLbl [label="events" shape=plain pos="4.5,3.0!"]; + rwLbl [label="read / write" shape=plain pos="3.75,1.0!"]; + + // External connections + OtherNodes -> REST; + Clients -> WebSocket; + + // Internal connections, pinned straight through the waypoints + REST -> pR [dir=back headclip=false]; + pR -> Engine [dir=forward tailclip=false]; + WebSocket -> pW [dir=back headclip=false]; + pW -> Engine [dir=none tailclip=false]; + Engine -> pB [dir=back headclip=false]; + pB -> H2 [dir=forward tailclip=false]; + + // Clients reach the REST API too: out of REST's right side, into the left of Clients + REST:e -> cw1 [dir=back]; + cw1 -> cw2 [dir=none]; + cw2 -> Clients:w [dir=forward]; +} +``` + +### Engine + +The engine performs the blockchain work: it validates incoming data, runs and , handles +[peer-to-peer networking](#peer-to-peer-communication), and maintains the +. + +The engine is an internal component and is not exposed directly. +Every inbound request, from a peer or from a client, arrives through the REST API described below and is then handed +to the engine. + +### REST API + +Both peers and clients reach NIS through a single HTTP API: + +* _Peer requests_ handle block synchronization, transaction relay, and node discovery. +* _Client requests_ handle reading blockchain data and submitting . + +The API supports two encodings, selected by the request's content type: JSON and binary. +Peers exchange data in binary, while clients typically use JSON. + +### WebSocket + +NIS publishes block and transaction events through a built-in +[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) service. +Subscribed clients receive notifications in real time without polling. + +### Database + +NIS stores the blockchain in an [H2](https://www.h2database.com) relational database that is _embedded_, meaning it runs +inside the NIS process rather than as a separate database server. + +The database holds only the chain itself: every block and the transactions inside it. +It does not store the current blockchain state, such as account balances and scores. +NIS keeps that state in memory and rebuilds it at startup by replaying the chain from the onward, +which is why a node stays unavailable for a while after it starts. + +## Peer-to-Peer Communication + +NEM nodes communicate directly with one another in a decentralized, peer-to-peer fashion. +There is no central coordinator: instead, each node establishes connections with a subset of other nodes, forming a +distributed network. + +Nodes share their lists of known peers, allowing a newly connected node to quickly discover others and integrate into +the network. +This process ensures robust connectivity and helps the network remain resilient, even if individual nodes go offline. + +```dot +graph P2PNetwork { + layout=circo; + mindist=0.5; + node [style=filled]; + edge [dir=both len=1]; + + N1 [label="Node 1"]; + N2 [label="Node 2"]; + N3 [label="Node 3"]; + N4 [label="Node 4"]; + N5 [label="Node 5"]; + N6 [label="Node 6"]; + N7 [label="Node 7"]; + N8 [label="Node 8"]; + + // Random peer-to-peer connections + N1 -- N2 -- N3 -- N4 -- N5 -- N6 -- N7 -- N8; + N1 -- N5; + N2 -- N6; + N4 -- N1; + N8 -- N3; +} +``` + +To facilitate bootstrapping, an initial list of _pre-trusted_ peers is bundled with . +This allows a new node to make its first connections and begin discovering others. + +### Node Reputation + +In a decentralized network like NEM, nodes must decide which peers to trust and maintain connections with. +Rather than relying on static whitelists or manually curated connections, NEM nodes use a _reputation_ system +to dynamically score and rank their peers based on observed behavior over time. + +Each node calculates reputation independently, using metrics such as communication success, response time, +and the validity of received data. +Nodes that behave correctly and respond consistently are given higher scores. +Those that send invalid data, fail to respond, or otherwise misbehave may be penalized or temporarily blacklisted. + +When a node needs to establish a new connection, it selects from the available peers, prioritizing those with higher +reputation based on past interactions. + +The bundled pre-trusted peers are weighted more heavily in this selection, +so they are chosen more often than other peers and act as reliable anchors for the network. +Their behavior is still scored like any other peer, so a misbehaving pre-trusted peer loses reputation accordingly. + +Reputation scores are local. +Each node builds its own view of the network from its direct experience alone, and it holds that view only in memory. +After a restart, a node keeps no earned reputation and rebuilds it from new interactions. + +The implementation is based on the [EigenTrust++](https://en.wikipedia.org/wiki/EigenTrust) algorithm. + +### Node Rotation + +To prevent the formation of isolated or stagnant node groups, a node does not always communicate with the same +peers. +Every time it selects peers to communicate with, it draws them at random, weighted by reputation. +Higher-scoring peers are more likely to be chosen, but the choice stays probabilistic. + +This randomness keeps nodes cycling through different peers, avoiding network fragmentation and promoting +long-term decentralization. + +## Supernodes + +A supernode is a node enrolled in the _Supernode Program_. + +Supernode Program +: An off-chain, community-funded program that rewards reliable public nodes. + +NEM has no block subsidy or inflation. +Nodes are paid exclusively from transaction fees, which can be small in periods of low activity. +The Supernode Program offsets this by paying daily rewards to nodes that prove themselves reliable. + +!!! warning "Supernode rewards are not guaranteed" + Reward amounts may be reduced or discontinued at any time. + +The program runs entirely off-chain, and NIS itself plays no part: a separate, centrally operated service called the +_controller_ tests participating nodes and pays out the rewards. + +A node qualifies for a day's reward by holding at least 10'000 and passing a battery of automated tests that +confirm it is in sync, up to date, and running on hardware and a network connection capable of serving peers reliably. + +Testing happens in four rounds spaced six hours apart, and a node must pass every test in every round to be eligible. +Each round runs the eight tests below, several of them measured against a trusted _reference node_ that the controller +knows to be healthy: + +| Test | A node passes when | +| ------------------- | -------------------------------------------------------------------------------------------------------| +| **Chain height** | Its chain is no more than 4 blocks behind the reference node. | +| **Chain part** | A random sequence of 60 to 100 recent blocks matches the reference node exactly, including signatures. | +| **Balance** | Its main account holds at least 10'000 XEM. | +| **Computing power** | It completes 10'000 successive key derivations, round trip included, in 5 seconds or less. | +| **Version** | It runs a client version at least as recent as the reference node's. | +| **Ping** | Across pings to 5 random peers, at most one ping fails and the average round trip stays under 200 ms. | +| **Bandwidth** | A bulk hashing exchange with a peer runs at an effective transfer speed of at least 5 Mbit/s. | +| **Responsiveness** | It answers 10 chain-height requests in 1 second or less, with at least 9 succeeding. | + +The ping and bandwidth tests reach out to other peers on the network, so a node's score reflects how it performs with +the wider network rather than just its exchange with the reference node. + +Rewards come from a fixed daily pool of 25'500 XEM, and each qualifying node earns at most 500 XEM per day. +When 51 or fewer nodes qualify, each receives the full 500 XEM. +Beyond that, the pool is divided equally, so per-node rewards fall as the network grows. + +Enrollment is optional. +Step-by-step instructions are available on the [NEM Supernode page](https://nem.io/supernode/). diff --git a/mkdocs/pages/en/textbook/transactions.md b/mkdocs/pages/en/textbook/transactions.md new file mode 100755 index 000000000..8e1a369c3 --- /dev/null +++ b/mkdocs/pages/en/textbook/transactions.md @@ -0,0 +1,317 @@ +# Transactions + +Transaction +: A transaction represents an action to perform on the NEM blockchain, + like moving funds from one to another, or registering a new mosaic. + +These actions are expressed in a signed message, which is then announced to the network. + in the network validate it and, if accepted, include the transaction in a block, updating the state of +the blockchain. + +## Fundamental Transaction Types + +NEM supports two core transaction types: basic and multisig. + +```dot +digraph "Fundamental Transaction Types" { + node [fontsize=12]; + Transaction; + Basic [URL="#basic-transactions"]; + Multisig [URL="#multisig-transactions"]; + + Transaction -> Basic; + Transaction -> Multisig; +} +``` + +### Basic Transactions + +Basic Transaction +: A basic represents a single action, initiated by a single account, + requiring only that account's . + +Examples include transferring funds from an account or registering a new . + +### Multisig Transactions + +Multisig Transaction +: A multisig transaction wraps a single issued on behalf of a + , and requires signatures from the configured number of cosignatories + before it can be included in a block. + +Multisig transactions are initiated by one cosignatory, but require additional signatures from other cosignatories +to be valid. + +Cosignature +: When a transaction requires signatures from multiple accounts, the additional signatures are called _cosignatures_. + +On NEM, each cosignature is delivered as its own _Multisig Cosignature_ transaction that references the + by hash, allowing cosignatories to sign independently and at different times. +Multiple coordinated actions must therefore be issued as separate multisig transactions, one per inner transaction. + +These cosignatures accumulate on the pending multisig transaction in the , and the transaction +can be included in a block only after it has collected enough cosignatures to meet its required threshold. +The multisig transaction and its cosignatures are then confirmed together atomically as a single unit. + +When a multisig transaction is included in a block, the multisig account pays all fees associated with the +transaction: the inner transaction's fee, the multisig transaction's fee, and every cosignature's fee. +Cosignatories never spend from their own balance when cosigning. + +!!! tip "Multisig Transaction Example" + + A treasury account `T` is a 2-of-3 multisig controlled by cosignatories `C1`, `C2`, and `C3`. + To pay a supplier `S`, `C1` announces a multisig transaction wrapping a transfer from `T` to `S`. + `C2` then submits a cosignature, meeting the 2-of-3 threshold. + `C3` does not need to sign. + Once the threshold is reached, the transfer executes, and `S` receives the funds. + + ```dot + digraph { + rankdir="LR"; + fontsize=12; + compound=true; + node [fontsize=12]; + + C1 [label="C1"]; + C2 [label="C2"]; + C3 [label="C3"]; + + subgraph clusterMultisig { + label = "Multisig Transaction"; + fontsize = 12; + style = dashed; + T [label="T\nMultisig Account\n2 of 3"]; + S [label="S"]; + T -> S [label="Transfer"]; + } + + C1 -> T [label="signature" lhead=clusterMultisig minlen=2 labelfloat=true]; + C2 -> T [label="cosignature" lhead=clusterMultisig minlen=2 labelfloat=true]; + C3 -> T [style=dashed lhead=clusterMultisig minlen=2]; + } + ``` + +### Inner Transactions + +Inner Transaction +: The wrapped inside a is called the _inner transaction_. + +Inner transactions behave like basic transactions, with the following differences: + +* They are not individually signed. + The multisig transaction is signed by the initiating cosignatory, and additional cosignatories provide + their approvals through separate multisig cosignature transactions. + +* They cannot themselves be multisig transactions. + Multisig hierarchies are only one layer deep. + +* They retain their own fee and deadline fields. + Inner transaction fees are billed to the multisig account along with the multisig transaction's fee + and each cosignature's fee. + +## Transaction Lifecycle + +Each NEM transaction moves through six stages, from creation by a client to confirmation by the network: + +```dot +digraph "Transaction Lifecycle" { + node [shape=box, style=rounded, fontsize=12, margin="0.2,0.1"]; + edge [fontsize=12]; + nodesep=0.3; + ranksep=0.3; + + Creation [label="1. Transaction is created and signed", URL="#1-creation-and-signature"]; + Announcement [label="2. Transaction is announced to a node", URL="#2-announcement"]; + Validation [label="3. Is it +valid?", shape=diamond, style="", URL="#3-validation"]; + Propagation [label="4. Propagate to other nodes", URL="#4-propagation"]; + Harvesting [label="5. Inclusion in a block", URL="#5-harvesting"]; + Confirmation [label="6. Confirmed?", shape=diamond, style="", URL="#6-confirmation"]; + Confirmed [label="Confirmed"]; + + Rejection1 [label="Rejected" style="rounded,dashed"]; + Rejection2 [label="Rejected" style="rounded,dashed"]; + + Creation -> Announcement; + Announcement -> Validation; + Validation -> Propagation [label=" Yes", labelfloat=true]; + Propagation -> Harvesting; + Harvesting -> Confirmation; + Confirmation -> Confirmed [label=" Yes", labelfloat=true]; + + Validation -> Rejection1 [label=No, style=dashed, minlen=2]; + Confirmation -> Rejection2 [label=No, style=dashed, minlen=2]; + + { rank = same; Validation; Rejection1 } + { rank = same; Confirmation; Rejection2 } +} +``` + +### 1. Creation and Signature + +A software client, typically an app, creates the transaction and fills in all its parameters. +For example, a transfer transaction requires the source , destination account, and amount. + +This step also involves signing the transaction. +Signatures prove that the signing account has authorized the transaction, since only the holder of an account's + can produce a valid signature. + +For multisig transactions, the initiating cosignatory signs the multisig transaction that wraps the inner transaction. +Other cosignatories provide their cosignatures separately, via multisig cosignature transactions. + +### 2. Announcement + +The client application submits the transaction to a connected on the network. + +For multisig transactions, cosignatures are announced as separate multisig cosignature transactions, each submitted +independently by its signer. + +### 3. Validation + +The node checks that the transaction is well-formed and includes a valid signature. +For multisig transactions, the node also verifies that any referenced cosignatures come from valid +cosignatories of the multisig account. + +Some transaction types require additional semantic checks. +For example, a transfer transaction verifies that the source account has enough funds. + +If any of these checks fail, the transaction is rejected and not propagated further. +If all checks pass, the process continues. + +### 4. Propagation + +Once the node considers the transaction to be valid, it is broadcast to other peer in the network, +and added to every node's _unconfirmed pool_. + +Unconfirmed pool +: A list of validated transactions awaiting inclusion in a block, maintained by each node in the network. + +When a peer receives a propagated transaction, it runs the full validation again before adding the transaction +to its own pool, because no node trusts another's validation. +If the transaction passes, the peer forwards it to its own peers, and propagation continues until the transaction +is distributed across the network. + +!!! warning "Do not rely on unconfirmed transactions" + + A transaction in the unconfirmed pool is not yet guaranteed to be included in a block. + Wait until it is [confirmed](#6-confirmation), and ideally past the , before treating it as final. + +For multisig transactions, the multisig transaction and its accompanying multisig cosignature transactions propagate +independently. + +### 5. Harvesting + +Once in the unconfirmed pool, the transaction can be included in a block by the process, though inclusion +is not guaranteed. +The transaction is dropped if its deadline passes or a conflicting transaction is confirmed first. + +For multisig transactions, harvesters do not include the transaction in a block until enough cosignatures have +been collected to meet the multisig account's signature threshold. +If the deadline expires first, the multisig transaction and its accumulated cosignatures are dropped from the pool. + +### 6. Confirmation + +Newly created blocks are propagated to other nodes that validate them and either accept or reject them. +The mechanism ensures that all nodes on the network ultimately agree on the same blocks. +Once the block containing a transaction is accepted by consensus, the transaction is _confirmed_. + +Occasionally, a block already accepted by a node is later rejected by the majority of the network and must be +. +In this case, the block's transactions are reverted and returned to the unconfirmed pool. + +NEM bounds how far back a rollback can reach with the . + +If a transaction's deadline expires while it is still in the unconfirmed pool, it is dropped from the pool. +This may happen, for example, if the transaction fee offered is too low to be included by any harvester. + +## Common Transaction Structure + +All transaction types in NEM share a set of common attributes: + +| Attribute | Description | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Signer public key** | Public key of the account that created and signed the transaction. | +| **Signature** | Cryptographic proof that the signer authorized the transaction and its content. | +| **Timestamp** | When the transaction was created, expressed in . It mainly serves to anchor the deadline rather than as a precise record of creation time. | +| **Deadline** | Timestamp indicating when the transaction expires if not confirmed, no later than 24 hours after the timestamp. | +| **Fee** | Fee the signer pays to have the transaction included in a block. | +| **Type** | Transaction type, which determines which additional attributes, if any, are present. | + +## Validation Details + +Before a transaction is included in a block, each node independently validates it using the following checks: + +| **Check** | **Description** | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Signature check** | Verifies the signature is valid and matches the signer's public key and the transaction's contents. | +| **Fee check** | Confirms the fee meets the network minimum and that the signer has enough XEM to pay it. | +| **Deadline check** | Discards the transaction if its deadline has already passed. | +| **Timestamp check** | Rejects transactions whose timestamp lies too far in the future, protecting against clock manipulation. | +| **Network check** | Rejects transactions that target a different network, for example a testnet transaction sent to mainnet. | +| **Uniqueness check** | Rejects transactions whose hash already appears in the recent chain history, preventing replay. | +| **Semantic checks** | Validates that the transaction is logically correct based on its type. Example: a transfer transaction fails if the sender lacks sufficient funds. | + +Transactions that fail any of these checks are rejected and not propagated further. + +## Supported Transaction Types + +NEM supports the following transaction types, each tailored to a specific kind of operation. +All transaction types share the same [common structure](#common-transaction-structure) and follow the same processing +and validation steps, but differ in purpose and required fields. + +
+ +| **Transaction Type** | **Description** | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| **[Transfer Transactions](default:transfer transaction)** | | +| `Transfer` | Send XEM or and an optional message between two . | +| **[Harvesting](default:harvesting)** | | +| `Account Key Link` | Activate or deactivate delegated harvesting by linking a remote account. | +| **[Multisig](default:multisignature account)** | | +| `Multisig Account Modification` | Create a multisig account, add or remove cosignatories, and change the minimum number of required signatures. | +| `Multisig Cosignature` | Provide a cosignature for a pending multisig transaction. | +| `Multisig` | Wrap an inner transaction issued on behalf of a multisig account. | +| **[Namespaces](default:namespace)** | | +| `Namespace Registration` | Register or renew a namespace. | +| **[Mosaics](default:mosaic)** | | +| `Mosaic Definition` | Create a new mosaic. | +| `Mosaic Supply Change` | Change the total supply of a mosaic. | + +
+ +## Transaction Fees + +Every transaction pays a fee that compensates the that includes it in a block. + +NEM fees are not market-driven. +The network publishes a fixed schedule, so the cost of any transaction can be calculated up front without contacting a +node. + +### Fee Schedule + +The current schedule is: + +| Transaction | Cost | Notes | +| --------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------| +| `Transfer` | From 0.05 XEM | Depends on the XEM amount, attached mosaics, and message length. See [Fees](./transfer_transactions.md#fees). | +| `Account Key Link` | 0.15 XEM | | +| `Multisig Account Modification` | 0.5 XEM | Paid by the multisig account (or, when converting a regular account into a multisig, by that account). | +| `Multisig Cosignature` | 0.15 XEM | Paid by the multisig account, not the cosignatory. | +| `Multisig` (wrapper) | 0.15 XEM | Paid by the multisig account, on top of the inner transaction's fee. | +| `Namespace Registration` | 0.15 XEM | Plus a [lease fee](./namespaces.md#lease-fee) paid to a network sink address. | +| `Mosaic Definition` | 0.15 XEM | Plus a [creation fee](./mosaics.md#creation-fee) paid to a network sink address. | +| `Mosaic Supply Change` | 0.15 XEM | | + +### Floor and Bidding + +The amounts in the schedule are minimums. +A transaction whose fee is below the minimum is rejected by validators. + +A higher fee than the minimum is accepted and increases the chance of inclusion: + +* When a harvester builds a block, it picks transactions sorted by fee, highest first. +* During network congestion, a node's spam filter ranks pending transactions by a combination of the signer's + and a small fee bonus, so higher-fee transactions are more likely to enter the . + +`Multisig Cosignature` fees are additionally capped at 1000 XEM, which protects the multisig account from being drained +by a single cosignatory bidding an extreme fee. diff --git a/mkdocs/pages/en/textbook/transfer_transactions.md b/mkdocs/pages/en/textbook/transfer_transactions.md new file mode 100644 index 000000000..9f9a8a87c --- /dev/null +++ b/mkdocs/pages/en/textbook/transfer_transactions.md @@ -0,0 +1,226 @@ +# Transfer Transactions + +Transfer Transaction +: A that allows sending , , and optional messages from one account to another. + +Transfer transactions are the most common type of transaction on NEM, enabling both asset transfers and simple +communication. + +## Key Features + +* **Mosaic Transfer** + + You can attach one or more mosaics to a transfer transaction. + All mosaics in a transfer transaction are sent from one sender to one recipient. + This makes transfer transactions ideal for simple, direct asset transfers. + +* **Message Support** + + Optional plaintext or encrypted messages can be included. + A transfer transaction does not require mosaics, so messages can also be sent on their own. + This allows for simple communication alongside the mosaic transfers. + +* **Multisig Compatibility** + + Transfer transactions, like all other transactions, support . + This allows them to require authorization from more than one account, allowing complex governance schemes. + +## Structure + +Besides the [common transaction structure](./transactions.md#common-transaction-structure), +a transfer transaction contains the following attributes: + +| Attribute | Description | +| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [**Recipient's address**](#recipients-address) | Address of the receiving account. | +| [**XEM amount**](#xem-amount) | Either the XEM sent to the recipient (when the mosaic list is empty) or a multiplier applied to each attached mosaic (when the mosaic list is non-empty). | +| [**List of transferred mosaics**](#list-of-transferred-mosaics) | Zero or more mosaics to transfer. | +| [**Optional message**](#optional-message) | Plaintext or encrypted message, up to 1024 bytes. | + +### Recipient's Address + +The recipient is specified as an . + +!!! warning "Assets sent to an unowned address are lost" + + It is possible to send XEM or mosaics to any valid address, including one that has never appeared on-chain. + If no one holds the corresponding to that address, the transferred assets are unrecoverable. + +### XEM Amount + +The **XEM amount** is expressed in micro-XEM, where `1 XEM = 1'000'000 micro-XEM`. +It serves two purposes depending on the [mosaic list](#list-of-transferred-mosaics): + +* **Transfer amount.** + When the mosaic list is empty, the XEM amount is the XEM sent to the recipient. +* **Mosaic multiplier.** + When the mosaic list is non-empty, the XEM amount transfers no XEM on its own. + Instead, the value is divided by `1'000'000` to obtain a **multiplier** that scales every attached mosaic's + quantity. + The same multiplier applies to all mosaics in the list. + + For example, `2'000'000` yields a multiplier of `2`, doubling every mosaic quantity in the list. + A list entry of `500` units is delivered as `1000` units to the recipient. + +!!! note "Multiplier rules" + + When the XEM amount acts as a multiplier: + + * The value must be divisible by `1'000'000` so the resulting multiplier is an integer. + Fractional amounts are rejected. + * Wallets set it to `1'000'000` by convention, transferring each mosaic quantity as specified. + * A multiplier of `0` produces a valid transaction that transfers no mosaics. + +### List of Transferred Mosaics + +A transfer transaction can include up to **10 mosaics**. +The list can also be empty, which lets the sender attach a message without moving any assets. + +Each entry specifies a mosaic ID and a quantity. +Quantities are integers counted in the mosaic's **atomic units**. + +A mosaic's `divisibility` property, set when the mosaic is created and ranging from `0` to `6`, +defines how many atomic units make one whole unit. +XEM, for example, has divisibility `6`, so `1'000'000` atomic units equal one whole XEM. + +Full details appear in the [Mosaics](./mosaics.md) page. + +The network rejects the transaction if the sender does not hold enough of any listed mosaic. + +!!! tip "Sending XEM alongside other mosaics" + + To transfer XEM in the same transaction as other mosaics, include XEM as an entry in this list. + Its quantity is then scaled by the XEM-amount multiplier along with every other mosaic in the list. + +### Optional Message + +A transfer transaction may carry an optional message of up to **1024 bytes**. +With no attached mosaics and a `0` XEM amount, the transfer carries only the message. + +Every message contains a **type** field identifying its payload as plaintext (`0x0001`) or secure (`0x0002`). + +Nodes enforce the type field and the 1024-byte size limit. +They do not interpret the payload bytes. +The protocol does not define a plaintext encoding, and it does not standardize an encryption scheme for secure +payloads. +Both are conventions agreed between sender and recipient. + +#### Plaintext Conventions + +Plaintext payloads are stored as-is. +Sender and recipient agree on a format such as UTF-8, JSON, or hex. + +NEM wallets and applications typically assume UTF-8. + +#### Secure Message Conventions + +Secure payloads are encrypted so that only the recipient can decrypt them. +The protocol does not standardize an encryption scheme. + +Two schemes are widely used in existing wallets and SDKs: +**AES-CBC** and **AES-GCM**, both with a shared key derived via Elliptic Curve Diffie-Hellman (ECDH). +Each scheme reserves part of the 1024-byte payload for cryptographic metadata, leaving up to **960 bytes** of usable +plaintext under AES-CBC and **996 bytes** under AES-GCM. + +!!! warning "CBC and GCM are not interoperable" + + Nodes accept and store both schemes under the same `0x0002` flag without inspecting the payload. + A recipient can only decrypt a secure message when its tooling implements the same scheme the sender used. + Messages produced by GCM tooling cannot be decrypted by CBC-only tooling, and vice versa. + +## Fees + +A transfer transaction's fee depends on what is sent. +It is the sum of two components: + +* The **transfer fee**, based on the XEM amount or the attached mosaics. +* The **message fee**, based on the length of any attached message. + +### Transfer Fee + +For a XEM-only transfer, the fee scales with the amount sent: + +| Amount sent | Cost | +| ---------------------------- | --------- | +| Up to 19'999 XEM | 0.05 XEM | +| Each additional 10'000 XEM | +0.05 XEM | +| 250'000 XEM or more | 1.25 XEM | + +For a mosaic transfer, the fee is the sum of every attached mosaic's individual fee. +Each mosaic is priced as follows: + +* **Tiny, indivisible mosaics** (supply ≤ 10'000 and divisibility 0) pay a flat **0.05 XEM**. +* **All other mosaics** are priced from their **XEM-equivalent value**, derived from the transferred quantity and the + mosaic's total supply. + This value maps to the same 0.05-to-1.25 XEM fee tiers used for XEM-only transfers, with a **supply discount** + that grows as the mosaic's total supply shrinks. + +The minimum per-mosaic fee is **0.05 XEM**. + +??? info "Mosaic Fee Calculation" + + Computing a non-tiny mosaic's fee takes three steps: compute the transferred quantity's XEM-equivalent value, + look that value up on the fee tiers to get a base fee, then subtract the supply discount. + + **1. XEM-equivalent value** + + \[ + \text{xem\_equivalent} = \frac{\text{8'999'999'999} \cdot \text{atomic\_quantity} \cdot \text{multiplier}}{\text{total\_atomic\_supply}} + \] + + where: + + * $\text{8'999'999'999}$ is the initial XEM supply, in whole units. + * $\text{atomic\_quantity}$ is the amount of the mosaic being transferred, in atomic units. + * $\text{multiplier}$ is the [XEM-amount multiplier](#xem-amount) (typically 1). + * $\text{total\_atomic\_supply}$ is the mosaic's total supply, in atomic units: + $\text{supply} \cdot 10^{\text{divisibility}}$. + + **2. Base fee** + + The resulting value is then priced on the same 0.05-to-1.25 XEM fee tiers as a XEM-only transfer, + yielding the mosaic's **base fee**. + + **3. Supply discount** + + A **supply discount** is then subtracted from that base fee: + + \[ + \text{discount} = \left\lfloor 0.8 \cdot \ln \!\left( \frac{9 \cdot 10^{15}}{\text{total\_atomic\_supply}} \right) \right\rfloor \cdot 0.05 \text{ XEM} + \] + + where $9 \cdot 10^{15}$ is the largest mosaic quantity NEM allows. + + $\text{xem\_equivalent}$ grows as the mosaic's supply shrinks, so without the discount low-supply mosaics would hit + the $1.25$ XEM cap on tiny transfers. + The discount counteracts this with a logarithm of that same supply, so scarcer mosaics get a larger correction. + The final fee is **never less than 0.05 XEM**, even when the discount exceeds the base fee. + + **Example** + + A mosaic with supply $\text{1'000'000}$ and divisibility $0$, sending $100$ units with multiplier $1$: + + 1. **XEM-equivalent**: $\frac{\text{8'999'999'999} \cdot 100 \cdot 1}{\text{1'000'000}} = \text{899'999.9999}$. + 2. **Base fee**: The XEM-only schedule above adds $0.05$ XEM per $\text{10'000}$ XEM of value, + with a $1.25$ XEM cap at $\text{250'000}$ XEM or more. + Since $\text{899'999.9999}$ exceeds $\text{250'000}$, the base fee is the maximum: $1.25$ XEM. + 3. **Supply discount**: $\left\lfloor 0.8 \cdot \ln \!\left( \frac{9 \cdot 10^{15}}{\text{1'000'000}} \right) \right\rfloor \cdot 0.05 = 0.90$ XEM. + + **Final fee**: $1.25 - 0.90 = 0.35$ XEM. + +### Message Fee + +A non-empty message costs **0.05 XEM** as a base, plus **0.05 XEM** for every additional 32 bytes of +payload, up to the 1024-byte maximum: + +| Message length | Added cost | +| ---------------------- | ---------- | +| No message | None | +| 1 to 31 bytes | 0.05 XEM | +| 32 to 63 bytes | 0.10 XEM | +| 64 to 95 bytes | 0.15 XEM | +| … | … | +| 1024 bytes (maximum) | 1.65 XEM | + +The fee is calculated on the stored payload size, so [secure messages](#secure-message-conventions) are billed on their +encrypted payload, not the plaintext. diff --git a/mkdocs/pages/en/userbook/.meta.yml b/mkdocs/pages/en/userbook/.meta.yml new file mode 100644 index 000000000..d94c701d2 --- /dev/null +++ b/mkdocs/pages/en/userbook/.meta.yml @@ -0,0 +1 @@ +section_name: userbook diff --git a/mkdocs/pages/en/userbook/intro.md b/mkdocs/pages/en/userbook/intro.md new file mode 100644 index 000000000..14edf5f38 --- /dev/null +++ b/mkdocs/pages/en/userbook/intro.md @@ -0,0 +1,15 @@ +--- +title: Welcome +--- + +# Welcome to the User Manual + +This manual explains how to perform various actions on the NEM blockchain using the applications maintained by The Symbol Syndicate. +No coding required! + +The topics range from basic tasks, such as creating an account and funding it, to more advanced ones, such as restricting an account to only send transactions to a selected list of addresses. + +Each page in this manual describes how to complete a single task. +Instructions are given in numbered steps, with screenshots and links to the [textbook](../textbook/intro.md) for background information when needed. + +Select a topic from the navigation menu to get started! diff --git a/mkdocs/pages/ja/404.md b/mkdocs/pages/ja/404.md new file mode 100644 index 000000000..9fb4cf829 --- /dev/null +++ b/mkdocs/pages/ja/404.md @@ -0,0 +1,18 @@ +--- +hide: + - navigation + - toc +disable_actions: true +--- + +
+ +# 404: ページが見つかりません + +**このサイトには {{ config.extra.nem.page_count }} 件のページがあります。** + +そのすべてを見逃し、この魔法使いを混乱させてしまったことをお祝いします。 + +![Page not found](site:/assets/images/confused.webp){.off-glb} + +
diff --git a/mkdocs/pages/ja/devbook/.meta.yml b/mkdocs/pages/ja/devbook/.meta.yml new file mode 100644 index 000000000..cefaa8af0 --- /dev/null +++ b/mkdocs/pages/ja/devbook/.meta.yml @@ -0,0 +1 @@ +section_name: devbook diff --git a/mkdocs/pages/ja/devbook/intro.md b/mkdocs/pages/ja/devbook/intro.md new file mode 100644 index 000000000..c305b37f6 --- /dev/null +++ b/mkdocs/pages/ja/devbook/intro.md @@ -0,0 +1,3 @@ +# 序章 + +Welcome to the Developer Manual. diff --git a/mkdocs/pages/ja/devbook/reference/java/.meta.yml b/mkdocs/pages/ja/devbook/reference/java/.meta.yml new file mode 100644 index 000000000..b00c993b7 --- /dev/null +++ b/mkdocs/pages/ja/devbook/reference/java/.meta.yml @@ -0,0 +1 @@ +language_icon: fontawesome/brands/java diff --git a/mkdocs/pages/ja/devbook/reference/py/.meta.yml b/mkdocs/pages/ja/devbook/reference/py/.meta.yml new file mode 100644 index 000000000..c574be72a --- /dev/null +++ b/mkdocs/pages/ja/devbook/reference/py/.meta.yml @@ -0,0 +1 @@ +language_icon: simple/python diff --git a/mkdocs/pages/ja/devbook/reference/rest/.gitignore b/mkdocs/pages/ja/devbook/reference/rest/.gitignore new file mode 100644 index 000000000..1cda54be9 --- /dev/null +++ b/mkdocs/pages/ja/devbook/reference/rest/.gitignore @@ -0,0 +1 @@ +*.yml diff --git a/mkdocs/pages/ja/devbook/reference/rest/nem.md b/mkdocs/pages/ja/devbook/reference/rest/nem.md new file mode 100644 index 000000000..0ef4a4b20 --- /dev/null +++ b/mkdocs/pages/ja/devbook/reference/rest/nem.md @@ -0,0 +1,65 @@ +--- +hide: + - toc +--- + +
+ + + + + diff --git a/mkdocs/pages/ja/devbook/reference/ts/.meta.yml b/mkdocs/pages/ja/devbook/reference/ts/.meta.yml new file mode 100644 index 000000000..a140a65ca --- /dev/null +++ b/mkdocs/pages/ja/devbook/reference/ts/.meta.yml @@ -0,0 +1 @@ +language_icon: simple/javascript diff --git a/mkdocs/pages/ja/index.md b/mkdocs/pages/ja/index.md new file mode 100644 index 000000000..70d9d5d2a --- /dev/null +++ b/mkdocs/pages/ja/index.md @@ -0,0 +1,49 @@ +--- +hide: + - navigation + - toc +section_name: textbook +disable_actions: true +--- + +# NEMドキュメントページへようこそ + + + + diff --git a/mkdocs/pages/ja/textbook/.meta.yml b/mkdocs/pages/ja/textbook/.meta.yml new file mode 100644 index 000000000..2a164617d --- /dev/null +++ b/mkdocs/pages/ja/textbook/.meta.yml @@ -0,0 +1 @@ +section_name: textbook diff --git a/mkdocs/pages/ja/textbook/glossary.md b/mkdocs/pages/ja/textbook/glossary.md new file mode 100644 index 000000000..b3b305165 --- /dev/null +++ b/mkdocs/pages/ja/textbook/glossary.md @@ -0,0 +1,228 @@ +# 用語集 + +AMA +: Ask Me Anything(何でも質問会)。オープンな質疑応答セッション。 + +AML +: (Anti Money Laundering)マネーロンダリング防止。 + +APAC +: (Asia and Pacific region)アジア太平洋地域。 + +APR +: (Annual Percentage Rate)年利。 + +アービトラージ +: (Arbitrage)ある市場で資産を購入し、別の市場で販売して、価格差から利益を得る取引手法。 + +バックランニング +: (Backrunning)すでに未処理の `transactionB` が存在する状態で、やや低い手数料(またはガス)で `transactionA` を送信し、 + 同一ブロック内で `transactionB` の*直後*に`transactionA`がマイニングされるように狙う行為。 + +BLS +: [Boneh–Lynn–Shacham](https://en.wikipedia.org/wiki/BLS_digital_signature) 署名方式。 + 署名者の真正性を検証できる暗号署名スキーム。 + +BTC +: ビットコイン。 + +CBDC +: (Central Bank Digital Currency)中央銀行デジタル通貨。 + +CEX +: (Centralized Exchange)中央集権型取引所。分散型取引所()の対義語。 + +CLI +: (Command-Line Interface)コマンドラインインターフェイス。 + 端末コンソール上でキーボードのみを使って操作するプログラム。 + Symbol にはブロックチェーンと対話する CLI ツールがある。 + +CMC +: Coin Market Cap。暗号資産に関する情報を提供するウェブサイト。 + +CSD +: (Central Securities Deposit)証券集中保管機関。 + +DAO +: (Decentralized Autonomous Organization)自律分散型組織。 + ガバナンスが完全にブロックチェーン上で行われる組織。 + +Dapp +: (Decentralized Application)分散型アプリケーション。 + 単一のコンピュータではなくブロックチェーン上で動作するアプリケーション。 + 広義には、ブロックチェーンを利用するあらゆるアプリケーションも指す。 + +DDH +: (Decisional Diffie-Hellman)判定型 [Diffie–Hellman](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) 仮定。 + +DD +: (Due Diligence)デューデリジェンス。 + +DeFi +: (Decentralized Finance)分散型金融。従来型金融()の対義語。 + +DEX +: (Decentralized Exchange)分散型取引所。中央集権型取引所()の対義語。 + +DoS +: (Denial of Service)サービス拒否攻撃。 + 単一の送信元がサーバーやネットワークに大量のリクエストを送りつけ、 + リソースを使い果たさせて正規の通信を妨害する攻撃。 + + 最も一般的な変種は DDoS(分散型サービス拒否攻撃)で、 + 多数の送信元(しばしば乗っ取られた端末)を利用して実行される。 + +DTC +: (Direct To Consumer)消費者直販。マスマーケット向け販売。 + +E2E +: (End-To-End)エンドツーエンド。 + +EMEA +: (Europe, Middle-East and Africa)欧州・中東・アフリカ地域。 + +ERC +: Ethereum Request for Comment。 + EVM 上のトークン標準(例:ERC-20、ERC-721、ERC-1155)を指すのに一般的に用いられる。 + +ETH +: イーサリアム。 + +EVM +: Ethereum Virtual Machine(イーサリアム仮想マシン)。 + +FFT +: [高速フーリエ変換(Fast Fourier Transform)](https://en.wikipedia.org/wiki/Fast_Fourier_transform)。 + +フロントランニング +: (Frontrunning)すでに未処理の `transactionB` が存在する状態で、やや高い手数料(またはガス)で `transactionA` を送信し、 + 同一ブロック内で `transactionB` の*直前*にマイニングされるように狙う行為。 + 市場ではこの方法で利益を得るケースがある。 + +ハードウェアウォレット +: (Hardware wallet)[秘密鍵](default:秘密鍵) を安全に保管し、署名を生成するための専用デバイス。 + 鍵は暗号化されたメモリに保存され、デバイス外に出ることはないため、 + ハードウェアウォレットは最も安全なアクセス手段のひとつとされる。 + 通常は署名機能のみを持ち、[ウォレット](default:ウォレット) アプリケーションと組み合わせて + [トランザクション](default:トランザクション) を作成・送信する。 + +HTLC +: (Hashed Time-Lock Contract)ハッシュタイムロックコントラクト。 + +ICO +: (Initial Coin Offering)イニシャルコインオファリング。 + +インフレーション +: (Inflation)新しい [ブロック](default:ブロック) が生成されるたびに、報酬として少量の が新規発行される仕組み。 + 2021年3月のネットワークローンチの48時間後に開始し、当初は1ブロックあたり約200 XYM。 + 報酬は緩やかに減少し、30年後には1 XYM、105年後には完全に終了する。 + +IP +: (Intellectual Property)知的財産。 + +IRS +: (Internal Revenue Service)アメリカ合衆国内国歳入庁。 + 米国居住者または米国市民が税金を納める機関。 + +KYC +: (Know Your Customer)顧客確認。 に関連。 + +LATAM +: ラテンアメリカ(中南米地域)。 + +MEV +: マイナー抽出可能価値(Miner/Maximal Extractable Value)。 + マイナーがブロック内のトランザクション順序を操作し、利益を得る手法。 + [フロントランニング](default:フロントランニング)、[バックランニング](default:バックランニング)、[サンドイッチ](default:サンドイッチ)> などが含まれる。 + +NAM +: (North America)北米地域。 + +NEM +: New Economy Movement(新経済運動)。 + +NFT +: 非代替性 [トークン](default:トークン)。 + ブロックチェーン上で固有のデジタル資産を表現する手段。 + +NIS1 +: の最初のパブリックメインネット用ノード実装。 + ネイティブ通貨 を扱い、2015年3月31日にローンチされた。 + +PoC +: (Proof of Concept)概念実証。プロトタイプの意。 + +PoI +: (Proof of Importance)プルーフ・オブ・インポータンス。 + で使用されるコンセンサス方式。 + に似ているが、ステーク量に加えてアカウントの活動度も評価する。 + +PoS +: (Proof of Stake)プルーフ・オブ・ステーク。 + 例えばイーサリアムなどで採用されているコンセンサス方式。 + +PoS+ +: (Proof of Stake Plus)プルーフ・オブ・ステーク・プラス。 + Symbol のコンセンサスメカニズム。 + を拡張し、ステーク量に加えてネットワーク上の活動も考慮する。 + 各アカウントが [ブロックを収穫](default:ハーベスティング) する確率は、 + その [インポータンス](default:インポータンス) スコアに基づいて計算される。 + +PoW +: (Proof of Work)プルーフ・オブ・ワーク。 + 例えばビットコインで採用されているコンセンサス方式。 + +ラグプル +: (Rug Pull)開発者がプロジェクトを放棄し、資金を持ち逃げする詐欺的行為。 + +サンドイッチ +: (Sandwich) で一般的な 手法の一種。 + 未処理トランザクションを見つけ、その直前に注文を置き([フロントランニング](default:フロントランニング))、 + 直後にもう一つの注文を置く([バックランニング](default:バックランニング))ことで利益を得る。 + +SDK +: (Software Development Kit)ソフトウェア開発キット。 + 特定プラットフォーム向けアプリケーションを簡単に作成できるライブラリ群。 + +シャーディング +: (Sharding)イーサリアムの[スケーリングソリューション](https://ethereum.org/en/developers/docs/scaling/#sharding)。 + +SXDH +: (Symmetric External Diffie–Hellman)対称外部 [Diffie–Hellman](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) 仮定。 + +シビル攻撃 +: 攻撃者が多数の偽アカウントやアイデンティティを作成し、 + ネットワークやコンセンサス過程に過剰な影響力を与えようとする攻撃。 + 一般的な対策として、 のように影響力を希少資源に結びつける方法がある。 + +TLC +: Tender Loving Care(愛情を込めた丁寧な対応)。 + +TLS +: 通信を暗号化するセキュリティプロトコル。ネットワーク上のピア間通信に使用される。 + +トークン +: (Token)デジタル資産を表す単位。Symbol では [モザイク](default:モザイク) と呼ばれる。 + +TPS +: (Transactions Per Second)1秒あたりのトランザクション数。 + +TradFi +: (Traditional Finance)従来型金融。 の対義語。 + +USP +: (Unique Selling Proposition / Point)独自の販売提案またはセールスポイント。 + 製品を競合他社と差別化する特徴。 + +VPS +: (Virtual Private Server)仮想専用サーバー。 + データセンター上にホストされ、リモートで物理サーバーのように扱える仮想マシン。 + +VRF +: (Verifiable Random Function)検証可能なランダム関数。 + +XEM +: ブロックチェーンのネイティブ通貨。 + +XYM +: Symbol ブロックチェーンのネイティブ通貨。 diff --git a/mkdocs/pages/ja/textbook/intro.md b/mkdocs/pages/ja/textbook/intro.md new file mode 100644 index 000000000..76ca6903d --- /dev/null +++ b/mkdocs/pages/ja/textbook/intro.md @@ -0,0 +1,13 @@ +--- +title: 序章 +--- + +# テキストブックへようこそ + +このテキストブックでは、NEM ブロックチェーンを支える基本的な概念を解説します。 + +[ユーザーマニュアル](../userbook/intro.md) や [開発者マニュアル](../devbook/intro.md) には、必要に応じて +関連するテキストブックのページへのリンクが含まれているため、 +この本を最初から最後まで読む必要は基本的にありません。 + +もちろん、ナビゲーションメニューを使って自由に読み進めていただいてかまいません。 diff --git a/mkdocs/pages/ja/userbook/.meta.yml b/mkdocs/pages/ja/userbook/.meta.yml new file mode 100644 index 000000000..d94c701d2 --- /dev/null +++ b/mkdocs/pages/ja/userbook/.meta.yml @@ -0,0 +1 @@ +section_name: userbook diff --git a/mkdocs/pages/ja/userbook/intro.md b/mkdocs/pages/ja/userbook/intro.md new file mode 100644 index 000000000..fc7e1b375 --- /dev/null +++ b/mkdocs/pages/ja/userbook/intro.md @@ -0,0 +1,17 @@ +--- +title: はじめに +--- + +# ユーザーマニュアルへようこそ + +このマニュアルでは、Symbol Syndicate が提供するアプリケーションを使って NEM ブロックチェーン上で +さまざまな操作を行う方法を解説します。プログラミングは不要です! + +内容は、アカウントの作成や入金といった基本操作から、 +特定のアドレス宛のみにトランザクションを送信できるようにアカウントを制限する高度な操作まで幅広く扱います。 + +各ページでは、1 つの操作手順に絞って説明しています。 +手順は番号付きのステップで示され、必要に応じてスクリーンショットや +背景知識の参考として [テキストブック](../textbook/intro.md) へのリンクも掲載されています。 + +ナビゲーションメニューからトピックを選び、使い方を始めましょう! diff --git a/mkdocs/requirements.txt b/mkdocs/requirements.txt new file mode 100644 index 000000000..b7ed8a52d --- /dev/null +++ b/mkdocs/requirements.txt @@ -0,0 +1,17 @@ +mkdocs==1.6.1 +mkdocs-ezglossary-plugin==2.1.0 +mkdocs-material==9.7.6 +mkdocs-material-extensions==1.3.1 +mkdocs-meta-manager==1.1.0 +mkdocs-git-revision-date-localized-plugin==1.3.0 +mkdocs-git-authors-plugin==0.9.2 +mkdocs-gen-files==0.5.0 +mkdocs-literate-nav==0.6.1 +mkdocstrings==0.29.0 +mkdocstrings-python==1.16.7 +mkdoxy==1.2.7 +mkdocs-glightbox==0.4.0 +mkdocs-macros-plugin==1.3.7 +mkdocs-site-urls==0.2.0 +mkdocs-graphviz==1.5.3 +symbol-sdk-python==3.3.2 diff --git a/mkdocs/scripts/ci/build.sh b/mkdocs/scripts/ci/build.sh new file mode 100755 index 000000000..aa1f76138 --- /dev/null +++ b/mkdocs/scripts/ci/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -ex + +# Run from the mkdocs folder +mkdocs build -f config/mkdocs.en.yml +mkdocs build -f config/mkdocs.ja.yml diff --git a/mkdocs/scripts/ci/ezglossary.patch b/mkdocs/scripts/ci/ezglossary.patch new file mode 100644 index 000000000..d55636b77 --- /dev/null +++ b/mkdocs/scripts/ci/ezglossary.patch @@ -0,0 +1,12 @@ +--- a/src/mkdocs_ezglossary_plugin/plugin.py ++++ b/src/mkdocs_ezglossary_plugin/plugin.py +@@ -239,6 +239,9 @@ + sec = f"{td.section}:" if td.section != "_" else "" + return f'{text}' + ++ entry.definition = re.sub(r'<([^:<]*):>', r'\1', entry.definition) ++ entry.definition = re.sub(r'<[^:<]*:\|([^>]*)>', r'\1', entry.definition) ++ + # Preserve visible text from nested glossary links/anchors before html2text + entry.definition = _preserve_visible_text_for_tooltip(entry.definition) + entry.definition = _html2text(entry.definition) diff --git a/mkdocs/scripts/ci/gh_pages_publish.sh b/mkdocs/scripts/ci/gh_pages_publish.sh new file mode 100755 index 000000000..6b6d64e7a --- /dev/null +++ b/mkdocs/scripts/ci/gh_pages_publish.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -ex + +git commit -m "[docs]: release new docs" +git remote set-url origin https://github.com/NemProject/nem +git remote -v +git push -f origin main:gh-pages diff --git a/mkdocs/scripts/ci/lint.sh b/mkdocs/scripts/ci/lint.sh new file mode 100755 index 000000000..3669f731e --- /dev/null +++ b/mkdocs/scripts/ci/lint.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -ex + +npm run lint +bash scripts/ci/lint_python.sh diff --git a/mkdocs/scripts/ci/lint_python.sh b/mkdocs/scripts/ci/lint_python.sh new file mode 100755 index 000000000..8d5e75035 --- /dev/null +++ b/mkdocs/scripts/ci/lint_python.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -ex + +find . \( -name node_modules -o -name .venv \) -prune -o -type f -name "*.sh" -print0 | xargs -0 shellcheck +find snippets \( -name node_modules -o -name .venv \) -prune -o -type f -name "*.py" -print0 | PYTHONPATH=. xargs -0 python3 -m isort \ + --line-length 75 \ + --indent " " \ + --multi-line 3 \ + --check-only +find snippets \( -name node_modules -o -name .venv \) -prune -o -type f -name "*.py" -print0 | PYTHONPATH=. xargs -0 python3 -m pycodestyle \ + --config=.pycodestyle + +# Build a custom .pylintrc file based on the global one +TMP_RC_FILE=/tmp/nem-docs.pylintrc +cp "$(git rev-parse --show-toplevel)/linters/python/.pylintrc" "$TMP_RC_FILE" +{ + # Allow lowercase "constants" (actually, top-level regular variables) + echo "const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(t_[A-Z0-9_]+)|(__.*__))$" + # Disable some warnings we accept for tutorial code + echo "disable=missing-docstring,broad-exception-caught,duplicate-code,use-maxsplit-arg,too-many-locals,too-many-branches,too-many-statements" + # Do not check these modules, as we do not install them to build the docs + echo "ignored-modules=web3,websockets" +} >> $TMP_RC_FILE +find snippets \( -name node_modules -o -name .venv \) -prune -o -type f -name "*.py" -print0 | PYTHONPATH=. xargs -0 python3 -m pylint \ + --rcfile $TMP_RC_FILE diff --git a/mkdocs/scripts/ci/mkdoxy.patch b/mkdocs/scripts/ci/mkdoxy.patch new file mode 100644 index 000000000..b0315d7e9 --- /dev/null +++ b/mkdocs/scripts/ci/mkdoxy.patch @@ -0,0 +1,11 @@ +--- generatorBase.py 2026-04-07 18:46:54.307700752 +0200 ++++ generatorBase.py.ORG 2026-04-07 18:46:48.875973948 +0200 +@@ -21,7 +21,7 @@ + log: logging.Logger = logging.getLogger("mkdocs") + + +-LETTERS = string.ascii_lowercase + "~_@\\" ++LETTERS = string.ascii_lowercase + "~_@\\[" + + + class GeneratorBase: diff --git a/mkdocs/scripts/ci/setup_build.sh b/mkdocs/scripts/ci/setup_build.sh new file mode 100755 index 000000000..f6097907d --- /dev/null +++ b/mkdocs/scripts/ci/setup_build.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -ex + +# Install Typedoc +npm install + +# Build Typescript SDK +pushd ../_symbol/sdk/javascript +npm install +npx tsc -p ./tsconfig/build-bindings.json +popd + +# Build OpenAPI spec +pushd ../openapi +npm install +npm run build +popd + +# Patch the ezglossary plugin, ignoring errors if it was already patched +ez_root="$(pip show mkdocs-ezglossary-plugin | sed -n 's/Location: \(.*\)/\1/p')" +ez_plugin="${ez_root}/mkdocs_ezglossary_plugin/plugin.py" +patch "${ez_plugin}" scripts/ci/ezglossary.patch --force --ignore-whitespace --fuzz 0 || true diff --git a/mkdocs/scripts/ci/setup_lint.sh b/mkdocs/scripts/ci/setup_lint.sh new file mode 100755 index 000000000..5745a46a2 --- /dev/null +++ b/mkdocs/scripts/ci/setup_lint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -ex + +npm install + +python3 -m pip install -r "$(git rev-parse --show-toplevel)/linters/python/lint_requirements.txt" +python3 -m pip install -r requirements.txt diff --git a/mkdocs/scripts/ci/test.sh b/mkdocs/scripts/ci/test.sh new file mode 100755 index 000000000..becdb9ab6 --- /dev/null +++ b/mkdocs/scripts/ci/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "no tests for docs (for now)" diff --git a/mkdocs/scripts/deploy-gh-pages.sh b/mkdocs/scripts/deploy-gh-pages.sh new file mode 100755 index 000000000..ece0dcee9 --- /dev/null +++ b/mkdocs/scripts/deploy-gh-pages.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -ex + +NEM_DOCS_DISABLE_TS=false mkdocs build -f config/mkdocs.en.yml +NEM_DOCS_DISABLE_TS=false mkdocs build -f config/mkdocs.ja.yml +cd ../docs +mv en en2 +mv ja ja2 +git checkout gh-pages +rm -rf en ja +mv en2 en +mv ja2 ja +git add -f en ja +git commit -m "[docs] Update" +git push +git checkout new-docs +cd ../mkdocs diff --git a/mkdocs/scripts/gen_ref_pages_java.py b/mkdocs/scripts/gen_ref_pages_java.py new file mode 100644 index 000000000..c1c2e2d32 --- /dev/null +++ b/mkdocs/scripts/gen_ref_pages_java.py @@ -0,0 +1,32 @@ +import re +from pathlib import Path + +import mkdocs_gen_files + +mkdoxy = mkdocs_gen_files.config["plugins"].get("mkdoxy") +if mkdoxy and mkdoxy.config.get("enabled"): + nav = mkdocs_gen_files.Nav() + + # Include in navigation all Java files generated by Doxygen that start with configured prefixes + prefixes = tuple(mkdocs_gen_files.config["extra"]["nem"]["java-sdk"]["include-prefixes"]) + for f in mkdocs_gen_files.editor.FilesEditor.current().files: + if f.src_uri.startswith("devbook/reference/java/"): + if not f.name.startswith(prefixes): + continue + + path = Path(f.src_uri.removeprefix("devbook/reference/java/")) + module_path = path.relative_to(".").with_suffix("") + doc_path = path + + match = re.search(r'\n# ([^\s]*)', f.content_bytes.decode("utf-8")) + if match: + title = match.group(1).replace("::", "/") + module_path = Path(title) + + parts = tuple(module_path.parts) + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open("devbook/reference/java/links.md", "w") as nav_file: + # Exclude the index file from being indexed by the search plugin + nav_file.writelines(["---\n", "search:\n", " exclude: true\n", "---\n\n"]) + nav_file.writelines(nav.build_literate_nav()) diff --git a/mkdocs/scripts/gen_ref_pages_py.py b/mkdocs/scripts/gen_ref_pages_py.py new file mode 100644 index 000000000..6ec195223 --- /dev/null +++ b/mkdocs/scripts/gen_ref_pages_py.py @@ -0,0 +1,38 @@ +from pathlib import Path + +import mkdocs_gen_files + +nav = mkdocs_gen_files.Nav() +ignored_files = mkdocs_gen_files.config['extra']['nem']['py-sdk']['ignore-files'] +ignored_folders = mkdocs_gen_files.config['extra']['nem']['py-sdk']['ignore-folders'] + +root = Path(__file__).parent.parent.parent +src = root / "_symbol/sdk/python/symbolchain" +paths = sorted(src.rglob("*.py")) + +for path in paths: + module_path = path.relative_to(src.parent).with_suffix("") + doc_path = path.relative_to(src).with_suffix(".md") + full_doc_path = Path("devbook/reference/py", doc_path) + + parts = tuple(module_path.parts) + + if parts[-1] == "__init__": + parts = parts[:-1] + if parts[-1] in ignored_files: + continue + if any(e in parts for e in ignored_folders): + continue + + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + identifier = ".".join(parts) + print(f'# :simple-python: {parts[-1]}', file=fd) + print('', file=fd) + print("::: " + identifier, file=fd) + +with mkdocs_gen_files.open("devbook/reference/py/links.md", "w") as nav_file: + # Exclude the index file from being indexed by the search plugin + nav_file.writelines(["---\n", "search:\n", " exclude: true\n", "---\n\n"]) + nav_file.writelines(nav.build_literate_nav()) diff --git a/mkdocs/scripts/gen_ref_pages_ts.py b/mkdocs/scripts/gen_ref_pages_ts.py new file mode 100644 index 000000000..4df6838b6 --- /dev/null +++ b/mkdocs/scripts/gen_ref_pages_ts.py @@ -0,0 +1,27 @@ +from pathlib import Path + +import mkdocs_gen_files + +nav = mkdocs_gen_files.Nav() + +# Generate index file for the TypeScript API ref +for f in mkdocs_gen_files.editor.FilesEditor.current().files: + if not f.src_uri.startswith("devbook/reference/ts/"): + continue + path = Path(f.src_uri.removeprefix("devbook/reference/ts/")) + if path.stem == "README" or path.stem == ".meta": + continue + if "symbol" in path.parts: + continue + + module_path = path.relative_to(".").with_suffix("") + + p = [i for i in module_path.parts if i not in ["namespaces", "classes", "functions"]] + parts = tuple(p) + + nav[parts] = path.as_posix() + +with mkdocs_gen_files.open("devbook/reference/ts/links.md", "w") as nav_file: + # Exclude the index file from being indexed by the search plugin + nav_file.writelines(["---\n", "search:\n", " exclude: true\n", "---\n\n"]) + nav_file.writelines(nav.build_literate_nav()) diff --git a/mkdocs/scripts/hooks.py b/mkdocs/scripts/hooks.py new file mode 100644 index 000000000..7c344495d --- /dev/null +++ b/mkdocs/scripts/hooks.py @@ -0,0 +1,521 @@ +import logging +import os +import re +import shutil +import sys +from pathlib import Path + +import mkdocs.plugins +import yaml +from mkdocs.structure import files + +from mkdocs.config import Config, base + +log = logging.getLogger('mkdocs') + + +def build_nav_order_and_section(config): + order = {} + section = {} + counter = 0 + + def walk(items, current_section): + nonlocal counter + + for item in items: + if isinstance(item, str): + order[item] = counter + section[item] = current_section + counter += 1 + + elif isinstance(item, dict): + for name, value in item.items(): + if isinstance(value, str): + order[value] = counter + section[value] = current_section + counter += 1 + elif isinstance(value, list): + walk(value, name) + + walk(config.get("nav", []), 'None') + return order, section + + +def parse_page_header(text: str) -> tuple[dict, str | None]: + """ + Return the YAML frontmatter of a page and its first level 1 header + """ + lines = text.splitlines() + meta = {} + start = 0 + + if lines and lines[0].strip() == "---": + for i in range(1, len(lines)): + if lines[i].strip() == "---": + raw_yaml = "\n".join(lines[1:i]) + meta = yaml.safe_load(raw_yaml) or {} + start = i + 1 + break + + for line in lines[start:]: + stripped = line.strip() + if stripped.startswith("# "): + return meta, stripped[2:] + + return meta, None + + +@mkdocs.plugins.event_priority(-50) +def on_files(in_files: files.Files, config: base.Config) -> files.Files: + """ + Exclude from processing files we don't care about: + Doxygen-generated: We only keep filenames starting with configured prefixes. + Parse frontmatter of developer tutorials to find their level and store it for later. + """ + out_files: list[File] = [] + prefixes = tuple(config["extra"]["nem"]["java-sdk"]["include-prefixes"] + ["links"]) + config['extra']['nem']['tutorials'] = {} + nav_order, nav_section = build_nav_order_and_section(config) + section_order = {} + for f in in_files: + if f.src_uri.startswith("devbook/reference/java"): + if not f.name.startswith(prefixes): + log.debug(f"Custom hook: Removing {f.name}") + continue + out_files.append(f) + + if not f.src_path.startswith("devbook/") or not f.src_path.endswith(".md"): + continue + + src = Path(config["docs_dir"]) / f.src_path + if not src.is_file(): + continue + text = src.read_text(encoding="utf-8") + meta, title = parse_page_header(text) + + if "tutorial_level" not in meta: + continue + + section = nav_section[f.src_path] + level = meta["tutorial_level"] + if section not in config['extra']['nem']['tutorials']: + config['extra']['nem']['tutorials'][section] = {} + if level not in config['extra']['nem']['tutorials'][section]: + config['extra']['nem']['tutorials'][section][level] = [] + config['extra']['nem']['tutorials'][section][level].append({ + "title": meta.get("title") or title, + "url": '/'.join(f.url.split('/')[1:-1]) + '.md', + "order": nav_order[f.url[:-1] + '.md'] + }) + section_order[section] = min(section_order.get(section, 999999), nav_order.get(f.src_path, 999999)) + + for section in config['extra']['nem']['tutorials'].values(): + for items in section.values(): + items.sort(key=lambda t: t["order"]) + + tutorials = config['extra']['nem']['tutorials'] + config['extra']['nem']['tutorials'] = dict( + sorted( + tutorials.items(), + key=lambda item: section_order.get(item[0], 999999), + ) + ) + return files.Files(out_files) + + +TAG_RE = re.compile(r"\[(?P[<>])(?P[A-Za-z0-9_.:-]+)\]") +COMMENT_RE = re.compile(r"(?P[ \t]*(#|//))\s*(?P.*)$") + + +def extract_tutorial_code(config: base.Config) -> None: + """ + Scans all .py and .mjs files under snippets/devbook and reads all tutorial code, separating it into + sections using [>start] and [": { + "full": str, # Full source with snippet tags removed (lines preserved) + "snippets": { + "": { + "code": str, # Extracted snippet (tags removed) + "start_line": int, # 1-based line number in original file where snippet starts + "end_line": int # 1-based line number where snippet ends (exclusive of closing tag) + }, + ... + } + }, + ... + } + + Notes: + - Paths are relative to the `snippets` folder (POSIX-style). + - Tag-only comment lines are replaced with blank lines in "full.code" to preserve line numbers. + - Inline tag comments are stripped, preserving the code before the tag. + """ + def _comment_contains_only_tags(body: str) -> bool: + without_tags = TAG_RE.sub("", body).strip() + return without_tags == "" + + def _remove_tags_from_comment(body: str) -> str: + return TAG_RE.sub("", body) + + def _process_file(path: Path) -> dict: + lines = path.read_text(encoding="utf-8").splitlines() + + clean_full_lines: list[str] = [] + snippets: dict[str, dict] = {} + + open_sections: dict[str, dict] = {} + + for index, line in enumerate(lines, start=1): + comment_match = COMMENT_RE.search(line) + tags = [] + + if comment_match: + tags = list(TAG_RE.finditer(comment_match.group("body"))) + + has_tags = bool(comment_match and tags) + comment_only_line = has_tags and (line[:comment_match.start()].strip() == "") + is_tag_only_comment = comment_only_line and _comment_contains_only_tags(comment_match.group("body")) + + clean_line = line + + if has_tags: + before_comment = line[:comment_match.start()].rstrip() + after_tags = _remove_tags_from_comment(comment_match.group("body")).strip() + + if before_comment and after_tags: + clean_line = f"{before_comment} {comment_match.group('prefix')} {after_tags}" + elif before_comment: + clean_line = before_comment + elif after_tags: + clean_line = f"{line[:comment_match.start()]}{comment_match.group('prefix')} {after_tags}" + else: + clean_line = "" + + # First process closing tags on this line. + # + # This allows: + # + # // [second] + # + # to close `first` before opening `second`. + for tag in tags: + if tag.group("kind") != "<": + continue + + name = tag.group("name") + + if name not in open_sections: + raise ValueError(f"{path}:{index}: closing unopened snippet section '{name}'") + + section = open_sections.pop(name) + if clean_line: + section["lines"].append(clean_line) + snippets[name] = { + "code": "\n".join(section["lines"]), + "start_line": section["start_line"], + } + + # Then process opening tags on this line. + for tag in tags: + if tag.group("kind") != ">": + continue + + name = tag.group("name") + + if name in open_sections: + raise ValueError(f"{path}:{index}: snippet section '{name}' is already open") + + if name in snippets: + raise ValueError(f"{path}:{index}: duplicate snippet section '{name}'") + + open_sections[name] = { + "start_line": index if clean_line else index + 1, + "lines": [], + } + + # Remove tag-only comments from rendered full code, but preserve line numbers. + clean_full_lines.append(clean_line) + + # Marker comments are not included in snippets. + # If the line had code before the marker comment, keep that code. + # If it was only a marker comment, preserve the line only in the full listing, + # not in extracted snippets. + if clean_line or not has_tags: + for section in open_sections.values(): + section["lines"].append(clean_line) + + if open_sections: + still_open = ", ".join(sorted(open_sections)) + raise ValueError(f"{path}: unclosed snippet section(s): {still_open}") + + return { + "full": "\n".join(clean_full_lines), + "snippets": snippets, + } + + root = Path(__file__).parent.parent.joinpath("snippets").resolve() + examples_dir = root / "devbook" + + config["extra"]["nem"]["tutorial_code"] = {} + + for path in examples_dir.rglob("*"): + if not path.is_file(): + continue + + if path.suffix not in {".py", ".mjs"}: + continue + + rel_path = path.relative_to(root).as_posix() + result = _process_file(path) + config["extra"]["nem"]["tutorial_code"][rel_path] = result + + +@mkdocs.plugins.event_priority(50) +def on_pre_build(config: base.Config): + """ + Copy the OpenAPI spec file next to its markdown, and load it into the config. + Load all tutorial sample code into memory and parse sections. + """ + spec_path = Path(__file__).parent.parent.parent.joinpath("openapi", "_build").resolve() + md_path = Path(config.docs_dir).joinpath("devbook", "reference", "rest").resolve() + spec_fname = 'openapi3.yml' + shutil.copy2(spec_path / spec_fname, md_path / spec_fname) + with open(spec_path / spec_fname, 'r', encoding='utf-8') as f: + config['extra']['nem']['openapi'] = yaml.safe_load(f) + extract_tutorial_code(config) + + +def page_markdown_js_typedoc(content, page, config, files): + """ + Customize markdown for JS API pages. The Typedoc-markdown plugin does not + support templates so we need this workaround. + """ + if not page.url.startswith("devbook/reference/ts"): + return content + + symbol_name = '' + parent_name = page.parent.title if page.parent else '' + + def symbol_type_repl(m): + dict = {"Class": "class", "Function": "method"} + nonlocal symbol_name + symbol_name = m.group(2).removesuffix('()') + if symbol_name == "default": + nonlocal parent_name + symbol_name = parent_name + # Insert manual word breaks in camel-case titles, in case they are very long + symbol_name_wbr = re.sub(r'([a-z])([A-Z])', r'\1\2', symbol_name) + if m.group(1) not in dict: + return f'# {m.group(1)}: {symbol_name_wbr}' + return f'# :simple-javascript: {symbol_name_wbr}' + + # Add object type icon at the header + content = re.sub(r'^# ([^:]*): ([^\n]*)', symbol_type_repl, content, count=1) + + # Add glossary definition to page title + # Documentation MUST NOT start with # so we can tell it apart from the next markdown heading + content = re.sub( + r'^(.*?)\n\n([^#].*?)\n\n', + rf'\1\n\n
js:{symbol_name}
\2
\n\n', content, count=1) + + # Add glossary definition to accessors + # Documentation MUST NOT start with # so we can tell it apart from the next markdown heading + m = re.search(r'\n## Accessors\n', content) + if m: + content = content[:m.start()] + re.sub( + r"(\n### )([^\n]*?)(\n\n#### Get Signature\n\n```.*?```\n\n)([^#].*?)(\n\n#####)", + rf'\1\2\3
js:{symbol_name}.\2
\4
\5', + content[m.start():], flags=re.DOTALL) + + # Add glossary definition to methods + # Documentation MUST NOT start with # so we can tell it apart from the next markdown heading + m = re.search(r'\n## Methods\n', content) + if m: + content = content[:m.start()] + re.sub( + r"(\n### )([^(]*?)(\(\)\n\n```.*?```\n\n)([^#].*?)(\n\n####)", + rf'\1\2\3
js:{symbol_name}.\2
\4
\5', + content[m.start():], flags=re.DOTALL) + + # Add glossary definition to global functions + content = re.sub( + r"(```ts\nfunction .*?)\n\n(.*?)(\n\n## )", + rf'
js:{symbol_name}
\2
\1\2\3', + content, flags=re.DOTALL) + + # Add special anchor because the typedoc-md plugin forgot to add it? + content = re.sub(r'(\n## Constructors)', r'\1', content, count=1) + + # Replace \c with code tags + content = re.sub(r'\\c ([^ ]*?) ', r'`\1` ', content) + + # Remove absolute markdown hyperlinks + content = re.sub(r'\[([^]]*)\]\(/[^)]*\)', r'\1', content) + + # Remove \note tags. They've been mangled when moved from CATS to JS and are barely usable. + content = re.sub(r'\\note ', '', content) + + return content + + +def camel_to_snake(name): + s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +def page_markdown_dylinks(content, page, config, files): + """ + The Dynamic Links (dylinks) parser. + Turn expressions like and into links to the reference pages that change + text and href depending on the selected language. + Accepts JavaScript.camelCase and reformats to Python.snake_case. + Settings in mkdocs.base.yml: + - The dictionary extra.nem.class-remaps translates from Python names to JS names, because sometimes they're different. + - The array extra.nem.global-namespaces lists class names which do not exist in JS and must be removed. + """ + langs = ['py', 'js'] + lang_names = ['Python', 'JavaScript'] + class_remaps = config['extra']['nem']['class-remaps'] + global_namespaces = config['extra']['nem']['global-namespaces'] + rgroup_id = 999 + + def class_formatter(m): + nonlocal rgroup_id + r = '' + for ndx, l in enumerate(langs): + class_name = m.group(1) + if l == 'py' and class_name in class_remaps: + class_name = class_remaps[class_name] + r += ( + f'' + f'' + ) + r += '' + rgroup_id += 1 + return r + + def method_formatter(m): + nonlocal rgroup_id + r = '' + for ndx, l in enumerate(langs): + class_name = m.group(1) + method_name = m.group(2) + if l == 'py': + if class_name in class_remaps: + class_name = class_remaps[class_name] + method_name = camel_to_snake(method_name) + if l == 'js' and class_name in global_namespaces: + class_name = "" + r += ( + f'' + ) + r += '' + rgroup_id += 1 + return r + + content = re.sub(r'', class_formatter, content) + content = re.sub(r'', method_formatter, content) + + return content + + +def page_markdown_rest(content, page, config, files): + def path_formatter(m): + method = m.group(1) + path = m.group(2) + spec = config['extra']['nem']['openapi']['paths'] + if path not in spec: + log.warning(f'Page {page.file.src_path} has invalid path {path}') + return f'**INVALID PATH `{path}`**' + spec = spec[path] + if method not in spec: + log.warning(f'Page {page.file.src_path} has invalid method `{method}` in path {path}') + return f'**INVALID PATH `{method}:{path}`**' + spec = spec[method] + summary = spec['summary'] + r = ( + f'[`{path}` `{method.upper()}`{{.rest-method .rest-method-{method}}}]' + f'(site:/devbook/reference/rest/nem#tag/{spec['tags'][0].replace(' ', '_')}/{spec['operationId']} "{summary}")' + ) + return r + + content = re.sub(r'<(get|put|post):([^>]*)>', path_formatter, content) + return content + + +def page_markdown_ws(content, page, config, files): + content = re.sub(r'(]*>)', r'\1 WS', content) + return content + + +def page_markdown_req(content, page, config, files): + content = re.sub(r'(]*>)', r'\1 REQ', content) + return content + + +def page_markdown_tutorial_complexity_tags(content, page, config, files): + if 'tutorial_level' in page.meta: + level = page.meta['tutorial_level'] + tag = ( + f'
' + f'' + f'{config.extra['nem']['tutorial_level_labels'][level]}' + f'' + f'
' + ) + content = re.sub(r'(^# [^\n]*)', rf'\g<1>\n\n{tag}', content) + return content + + +@mkdocs.plugins.event_priority(0) +def on_page_markdown(content, page, config, files): + content = page_markdown_js_typedoc(content, page, config, files) + content = page_markdown_dylinks(content, page, config, files) + content = page_markdown_rest(content, page, config, files) + content = page_markdown_ws(content, page, config, files) + content = page_markdown_req(content, page, config, files) + content = page_markdown_tutorial_complexity_tags(content, page, config, files) + return content + + +class ignoreRESTAnchors(logging.Filter): + def filter(self, record): + return not re.search(r'reference/rest/nem.md.*does not contain an anchor', record.msg) + + +def on_startup(*args, **kwargs): + """ + Add the mkdocs folder to PYTHONPATH, so custom modules like the CATS lexer are found. + Customize the log level of individual plugins. + """ + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + if project_root not in sys.path: + sys.path.insert(0, project_root) + # Make this noisy plugin shut up a bit + mkdocs.plugins.get_plugin_logger('mkdocs_site_urls').setLevel(logging.WARNING) + # Silence messages about missing anchors in links to the REST reference guide because that's a dynamic page + pagesLog = logging.getLogger('mkdocs.structure.pages') + pagesLog.addFilter(ignoreRESTAnchors()) + + +def on_nav(nav, config, files): + """ + Counts the total number of pages on the site, after autogeneration, and stores it for later use. + """ + def count_pages(items): + count = 0 + for item in items: + if hasattr(item, 'children') and item.children: + count += count_pages(item.children) + else: + count += 1 + return count + + num_pages = count_pages(nav) + config['extra']['nem']['page_count'] = num_pages + log.info(f"Custom hook: Counted {num_pages} pages") diff --git a/mkdocs/scripts/ptl.py b/mkdocs/scripts/ptl.py new file mode 100644 index 000000000..56b61c9bf --- /dev/null +++ b/mkdocs/scripts/ptl.py @@ -0,0 +1,67 @@ +# Prepare Tasks List summary +# +# Takes a hierarchy of tasks with statuses, and calculates the completion +# percentage of each task. Useful to generate plots. +# +# 1. Download the task list database from Notion as a CSV file called all.csv, +# 2. Import to Google sheet called `Task list progress`, on tab Raw. +# 3. Export tab Filtered to filtered.csv +# Fields must be: Name, Status, Label, ID, Parent ID +# 4. Run this script +# 5. Copy stdout to the `Task list chart.ods` to generate plot. + +import math + +import pandas as pd + + +def iterate_count(id): + prog = 0 + if id in ch: + num_total = 0 + num_done = 0 + for c in ch[id]: + num_total = num_total + 1 + num_done = num_done + iterate_count(c) + prog = num_done / num_total + else: + prog = 1 if status[id] == 'Done' or status[id] == 'Archived' else 0 + percentage[id] = prog + return prog + + +def iterate_print(id, indent): + if id in ch: + print("--" * (indent - 1) + ("->" if indent > 0 else ""), names[id], ",", percentage[id]) + for c in ch[id]: + iterate_print(c, indent + 1) + + +d = pd.read_csv('filtered.csv') + +ch = {} +names = {} +status = {} +percentage = {} +for v in d.values: + id = v[3] + if not math.isnan(v[4]): + pid = int(v[4]) + else: + pid = -1 + if pid not in ch: + ch[pid] = [] + ch[pid].append(id) + names[id] = v[0] + status[id] = v[1] + percentage[id] = 0 + +for v in d.values: + if not math.isnan(v[4]): + continue + iterate_count(v[3]) + +for v in d.values: + if not math.isnan(v[4]): + continue + iterate_print(v[3], 0) diff --git a/mkdocs/scripts/register_lexers.py b/mkdocs/scripts/register_lexers.py new file mode 100644 index 000000000..17715204e --- /dev/null +++ b/mkdocs/scripts/register_lexers.py @@ -0,0 +1,17 @@ +from pygments.lexers._mapping import LEXERS + +LEXERS['CATSLexer'] = ( + 'lexers.cats_lexer', # module name + 'CATS', # display name + ('cats',), # aliases + ('*.cats',), # file extensions + ('text/x-cats',) # mimetypes +) + +LEXERS['STOMPLexer'] = ( + 'lexers.stomp_lexer', # module name + 'STOMP', # display name + ('stomp',), # aliases + ('*.stomp',), # file extensions + ('text/x-stomp',) # mimetypes +) diff --git a/mkdocs/scripts/serve.bat b/mkdocs/scripts/serve.bat new file mode 100644 index 000000000..10ae22487 --- /dev/null +++ b/mkdocs/scripts/serve.bat @@ -0,0 +1 @@ +mkdocs serve -f config\mkdocs.en.yml --dirtyreload --watch templates --watch overrides --watch config diff --git a/mkdocs/scripts/typedoc-plugin.py b/mkdocs/scripts/typedoc-plugin.py new file mode 100644 index 000000000..080088984 --- /dev/null +++ b/mkdocs/scripts/typedoc-plugin.py @@ -0,0 +1,131 @@ +# Author: Jakub Andrýsek +# Email: email@kubaandrysek.cz +# Website: https://kubaandrysek.cz +# License: MIT +# GitHub: https://github.com/JakubAndrysek/mkdocs-typedoc +# PyPI: https://pypi.org/project/mkdocs-typedoc/ + +import logging +import os +import subprocess + +import mkdocs.plugins +from mkdocs.structure.files import File + +log: logging.Logger = logging.getLogger("mkdocs") + + +@mkdocs.plugins.event_priority(10) +def on_files(files, config): + plugin_config = config["extra"]["nem"]["ts-sdk"] + # Check if the Typedoc generation is enabled + if plugin_config["disabled"]: + return files + + # Path to the typedoc.json options file + typedoc_options = plugin_config["options"] + + output_dir = plugin_config["output_dir"] + + # Path to the generated documentation + doc_path = os.path.join(config["site_dir"], output_dir) + + if not os.path.exists(doc_path): + os.makedirs(doc_path) + + # Path to the tsconfig file + tsconfig_path = plugin_config["tsconfig"] + + if not os.path.exists(tsconfig_path): + log.error( + "tsconfig.json file does not exist. Please create it or change the path in mkdocs.yml." + ) + return files + + # Check if Node.js is installed + if not is_node_installed(): + log.error( + "Node.js is not installed. Please install it from https://nodejs.org/en/download/." + ) + return files + + # Check if TypeDoc is installed + if not is_typedoc_installed(): + log.error( + """TypeDoc is not installed. Please install it with `npm install typedoc --save-dev`. + See https://typedoc.kubaandrysek.cz for more information.""" + ) + return files + + # Build TypeDoc documentation + try: + typedoc_config = [ + ("--out", doc_path), + ("--tsconfig", tsconfig_path), + ] + + if typedoc_options: + typedoc_config.insert(2, ("--options", typedoc_options)) + + # Flattening the list of pairs to pass into subprocess.run + flattened_config = [item for pair in typedoc_config for item in pair] + + command = [get_npx_filename(), "typedoc", *flattened_config] + subprocess.run(command, check=True) + except subprocess.CalledProcessError as e: + log.error("TypeDoc failed with error code %d" % e.returncode) + return files + except Exception as e: + log.error(f"TypeDoc failed with error: {e}") + return files + + # Add generated TypeDoc documentation to MkDocs + for dirpath, dirnames, filenames in os.walk(doc_path): + for filename in filenames: + if filename.endswith("README.md"): + continue + abs_src_path = os.path.join(dirpath, filename) + doc_rel_path = os.path.relpath(abs_src_path, config["site_dir"]) + files.append( + File( + doc_rel_path, + config["site_dir"], + config["site_dir"], + config["use_directory_urls"], + ) + ) + + return files + + +def get_npx_filename(): + return "npx.cmd" if os.name == "nt" else "npx" + + +def is_node_installed(): + try: + result = subprocess.run( + ["node", "--version"], check=True, capture_output=True, text=True + ) + return result.returncode == 0 + except subprocess.CalledProcessError: + return False + except Exception as e: + log.error(f"TypeDoc: Node.js failed with error: {e}") + return False + + +def is_typedoc_installed(): + try: + result = subprocess.run( + [get_npx_filename(), "typedoc", "--version"], + check=True, + capture_output=True, + text=True, + ) + return result.returncode == 0 + except subprocess.CalledProcessError: + return False + except Exception as e: + log.error(f"TypeDoc: TypeDoc failed with error: {e}") + return False diff --git a/mkdocs/snippets/devbook/accounts/create_from_mnemonic.log b/mkdocs/snippets/devbook/accounts/create_from_mnemonic.log new file mode 100644 index 000000000..68c0da271 --- /dev/null +++ b/mkdocs/snippets/devbook/accounts/create_from_mnemonic.log @@ -0,0 +1,6 @@ +Generating random mnemonic phrase... +Mnemonic phrase: hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay ***** +Password: correcthorsebatterystaple +Address: TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP +Public key: 462EE976890916E54FA825D26BDD0235F5EB5B6A143C199AB0AE5EE9328E08CE +Private key: 0000000000000000000000000000000000000000000000000000000000000000 diff --git a/mkdocs/snippets/devbook/accounts/create_from_mnemonic.mjs b/mkdocs/snippets/devbook/accounts/create_from_mnemonic.mjs new file mode 100644 index 000000000..ff0599105 --- /dev/null +++ b/mkdocs/snippets/devbook/accounts/create_from_mnemonic.mjs @@ -0,0 +1,39 @@ +import { Bip32 } from 'symbol-sdk'; +import { NemFacade } from 'symbol-sdk/nem'; +import process from 'process'; + +// Initialize the facade for the testnet network [>step-1] +const facade = new NemFacade('testnet'); +// [step-2] +const bip32 = new Bip32(NemFacade.BIP32_CURVE_NAME); +let mnemonic = process.env.MNEMONIC; +if (mnemonic) { + console.log('Loading mnemonic phrase from environment variable...'); +} else { + console.log('Generating random mnemonic phrase...'); + mnemonic = bip32.random(); +} +console.log('Mnemonic phrase:', mnemonic); +// [step-3] +const password = process.env.PASSWORD || 'correcthorsebatterystaple'; +console.log('Password:', password); + +// Derive a root Bip32 node from the mnemonic and a password +const rootNode = bip32.fromMnemonic(mnemonic, password); +// [step-4] +const accountIndex = 0; +const childNode = rootNode.derivePath(facade.bip32Path(accountIndex)); +// [step-5] +const keyPair = NemFacade.bip32NodeToKeyPair(childNode); + +// Derive the address from the public key +const address = facade.network.publicKeyToAddress(keyPair.publicKey); + +// Output the account details +console.log('Address:', address.toString()); +console.log('Public key:', keyPair.publicKey.toString()); +console.log('Private key:', keyPair.privateKey.toString()); // [step-1] +facade = NemFacade('testnet') +# [step-2] +bip32 = Bip32(NemFacade.BIP32_CURVE_NAME) +mnemonic = os.getenv('MNEMONIC') +if mnemonic: + print('Loading mnemonic phrase from environment variable...') +else: + print('Generating random mnemonic phrase...') + mnemonic = bip32.random() +print(f'Mnemonic phrase: {mnemonic}') +# [step-3] +password = os.getenv('PASSWORD', 'correcthorsebatterystaple') +print(f'Password: {password}') + +# Derive a root Bip32 node from the mnemonic and a password +root_node = bip32.from_mnemonic(mnemonic, password) +# [step-4] +account_index = 0 +child_node = root_node.derive_path(facade.bip32_path(account_index)) +# [step-5] +key_pair = facade.bip32_node_to_key_pair(child_node) + +# Derive the address from the public key +address = facade.network.public_key_to_address(key_pair.public_key) + +# Output the account details +print(f'Address: {address}') +print(f'Public key: {key_pair.public_key}') +print(f'Private key: {key_pair.private_key}') # [step-1] +const facade = new NemFacade('testnet'); +// [step-2] +// Otherwise generate a random one. +const privateKeyString = process.env.PRIVATE_KEY; +let privateKey; +if (privateKeyString) { + console.log('Loading account from environment variable...'); + privateKey = new PrivateKey(privateKeyString); +} else { + console.log('Generating random account...'); + privateKey = PrivateKey.random(); +} // [step-3] +// Create a key pair from the private key +const keyPair = new NemFacade.KeyPair(privateKey); + +// Derive the public key from the private key +const publicKey = keyPair.publicKey; + +// Derive the address from the public key +const address = facade.network.publicKeyToAddress(publicKey); + +// Output the account details +console.log('Address:', address.toString()); +console.log('Public key:', publicKey.toString()); +console.log('Private key:', privateKey.toString()); // [step-1] +facade = NemFacade('testnet') +# [step-2] +# Otherwise generate a random one. +private_key_string = os.getenv('PRIVATE_KEY') +if private_key_string: + print('Loading account from environment variable...') + private_key = PrivateKey(private_key_string) +else: + print('Generating random account...') + private_key = PrivateKey.random() +# [step-3] +key_pair = facade.KeyPair(private_key) + +# Derive the public key from the private key +public_key = key_pair.public_key + +# Derive the address from the public key +address = facade.network.public_key_to_address(public_key) + +# Output the account details +print(f'Address: {address}') +print(f'Public key: {public_key}') +print(f'Private key: {private_key}') # [step-2] +/** + * Fetch all mosaic balances owned by an account. + * @param {string} address - Account address + * @returns {Promise} List of mosaics with id and quantity + */ +async function getMosaicBalances(address) { + const path = `/account/mosaic/owned?address=${address}`; + const response = await fetch(`${NODE_URL}${path}`); + const info = await response.json(); + return info.data; +} // [step-3] +/** + * Fetch mosaic definitions for every mosaic owned by an account. + * @param {string} address - Account address + * @returns {Promise} Map of "namespace:name" to mosaic definition + */ +async function getMosaicDefinitions(address) { + const path = `/account/mosaic/owned/definition?address=${address}`; + const response = await fetch(`${NODE_URL}${path}`); + const info = await response.json(); + // Build a map from "namespace:name" to mosaic definition + const definitionsMap = new Map(); + for (const entry of info.data) { + const key = `${entry.id.namespaceId}:${entry.id.name}`; + definitionsMap.set(key, entry); + } + return definitionsMap; +} // [step-4] +/** + * Format an atomic amount with decimal places. + * @param {bigint} amount - The atomic amount + * @param {number} divisibility - Number of decimal places + * @returns {string} The formatted amount + */ +function formatAmount(amount, divisibility) { + if (0 === divisibility) + return amount.toString(); + + const divisor = 10n ** BigInt(divisibility); + const wholePart = amount / divisor; + const fractionalPart = amount % divisor; + const fractionalStr = fractionalPart.toString() + .padStart(divisibility, '0'); + return `${wholePart}.${fractionalStr}`; +} +// [step-5] +const ADDRESS = process.env.ADDRESS || + 'TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP'; +console.log('Fetching balances for', ADDRESS); + +try { + // Fetch mosaic balances and definitions for the account + const accountMosaics = await getMosaicBalances(ADDRESS); + const mosaicDefinitions = await getMosaicDefinitions(ADDRESS); + + if (0 === accountMosaics.length) { + console.log('Account holds no mosaics'); + } else { + console.log(`Account holds ${accountMosaics.length} mosaic(s):`); + + for (const mosaicEntry of accountMosaics) { + const { mosaicId } = mosaicEntry; + const key = `${mosaicId.namespaceId}:${mosaicId.name}`; + const balance = BigInt(mosaicEntry.quantity); + + // Get mosaic divisibility from the definition + const definition = mosaicDefinitions.get(key); + const properties = Object.fromEntries( + definition.properties.map(p => [p.name, p.value]) + ); + const divisibility = parseInt( + properties.divisibility || '0', 10); + + // Format and display the balance + const formattedBalance = formatAmount(balance, divisibility); + console.log(`- Mosaic ${key}`); + console.log(` Balance: ${formattedBalance}`); + console.log(` Balance (atomic): ${balance.toString()}`); + console.log(` Divisibility: ${divisibility}`); + } + } +} catch (e) { + console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown'); +} // [step-2] + """ + Fetch all mosaic balances owned by an account. + + Args: + address: The account address + + Returns: + List of mosaics, each with a structured mosaicId and quantity + """ + balances_path = f'/account/mosaic/owned?address={address}' + with urllib.request.urlopen(f'{NODE_URL}{balances_path}') as response: + balances_info = json.loads(response.read().decode()) + return balances_info['data'] # [step-3] + """ + Fetch mosaic definitions for every mosaic owned by an account. + + Args: + address: The account address + + Returns: + Dictionary mapping "namespace:name" to the mosaic definition + """ + definitions_path = '/account/mosaic/owned/definition' + with urllib.request.urlopen( + f'{NODE_URL}{definitions_path}?address={address}' + ) as response: + definitions_info = json.loads(response.read().decode()) + # Build a dictionary mapping "namespace:name" to its definition + definitions_map = {} + for entry in definitions_info['data']: + entry_id = entry['id'] + entry_key = f'{entry_id["namespaceId"]}:{entry_id["name"]}' + definitions_map[entry_key] = entry + return definitions_map # [step-4] + """ + Format an atomic amount with decimal places. + + Args: + amount: The atomic amount as an integer + divisibility: Number of decimal places + + Returns: + Formatted amount as a string + """ + if divisibility == 0: + return str(amount) + whole_part = amount // (10 ** divisibility) + fractional_part = amount % (10 ** divisibility) + return f'{whole_part}.{fractional_part:0{divisibility}d}' # [step-5] +ADDRESS = os.getenv('ADDRESS', 'TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP') +print(f'Fetching balances for {ADDRESS}') + +try: + # Fetch mosaic balances and definitions for the account + account_mosaics = get_mosaic_balances(ADDRESS) + mosaic_definitions = get_mosaic_definitions(ADDRESS) + + if not account_mosaics: + print('Account holds no mosaics') + else: + print(f'Account holds {len(account_mosaics)} mosaic(s):') + + for mosaic_entry in account_mosaics: + mosaic_id = mosaic_entry['mosaicId'] + key = f'{mosaic_id["namespaceId"]}:{mosaic_id["name"]}' + balance = int(mosaic_entry['quantity']) + + # Get mosaic divisibility from the definition + definition = mosaic_definitions[key] + properties = { + p['name']: p['value'] + for p in definition['properties'] + } + mosaic_divisibility = int( + properties.get('divisibility', '0')) + + # Format and display the balance + formatted_balance = format_amount( + balance, mosaic_divisibility) + print(f'- Mosaic {key}') + print(f' Balance: {formatted_balance}') + print(f' Balance (atomic): {balance}') + print(f' Divisibility: {mosaic_divisibility}') +except urllib.error.URLError as e: + print(e.reason) # [step-1] + if (!response.ok) + throw new Error(`HTTP error! status: ${response.status}`); + + const chainHeight = await response.json(); + + const height = parseInt(chainHeight.height, 10); // [step-2] + const irreversibleHeight = Math.max(0, height - REWRITE_LIMIT); + // [step-3] + if (null !== prevHeight && height !== prevHeight) + heightChangedAt = now; + + const heightAgo = null !== heightChangedAt ? + `${Math.floor((now - heightChangedAt) / 1000)}s ago` : + '-'; // [step-4] + const heightLabel = height.toLocaleString().padStart(10); + const irreversibleLabel = + irreversibleHeight.toLocaleString().padStart(10); + console.log( + `Height: ${heightLabel} (changed ${heightAgo})` + + ` | Irreversible: ${irreversibleLabel}` + ); + + prevHeight = height; + await new Promise(resolve => { setTimeout(resolve, 1000); }); + // [step-1] + f'{NODE_URL}/chain/height' + ) as response: + chain_height = json.loads(response.read().decode()) + + height = int(chain_height['height']) # [step-2] + irreversible_height = max(0, height - REWRITE_LIMIT) # [step-3] + now = time.time() + if prev_height is not None and height != prev_height: + height_changed_at = now + + if height_changed_at is not None: + height_ago = f'{int(now - height_changed_at)}s ago' + else: + height_ago = '-' # [step-4] + print( + f'Height: {height:>10,} (changed {height_ago})' + f' | Irreversible: {irreversible_height:>10,}' + ) + + prev_height = height + time.sleep(1) + # [step-1] + const mosaicPath = `/mosaic/definition?mosaicId=${MOSAIC_ID}`; + console.log('Fetching mosaic information from', mosaicPath); + const mosaicResponse = await fetch(`${NODE_URL}${mosaicPath}`); + if (!mosaicResponse.ok) + throw new Error(`HTTP error! status: ${mosaicResponse.status}`); + + const mosaicJSON = await mosaicResponse.json(); + const fullName = `${mosaicJSON.id.namespaceId}:${mosaicJSON.id.name}`; + console.log('Mosaic information:'); + console.log(` Mosaic ID: ${fullName}`); + console.log(' Description:', mosaicJSON.description); + console.log(' Creator:', mosaicJSON.creator); + const properties = Object.fromEntries(mosaicJSON.properties + .map(property => [property.name, property.value])); + const divisibility = parseInt(properties.divisibility, 10); + console.log(' Divisibility:', divisibility); + console.log(' Initial supply:', properties.initialSupply); + console.log(' Supply mutable:', properties.supplyMutable); + console.log(' Transferable:', properties.transferable); + const hasLevy = 0 !== Object.keys(mosaicJSON.levy).length; + console.log(' Levy:', hasLevy ? mosaicJSON.levy : 'none'); + // [step-2] + const supplyPath = `/mosaic/supply?mosaicId=${MOSAIC_ID}`; + console.log('\nFetching current supply from', supplyPath); + const supplyResponse = await fetch(`${NODE_URL}${supplyPath}`); + const supplyInfo = await supplyResponse.json(); + const supply = supplyInfo.supply; + console.log(' Current supply:', supply); + // [step-3] + const atomic = BigInt(supply) * (10n ** BigInt(divisibility)); + console.log(`\nSupply in atomic units: ${atomic}`); + // [step-1] + mosaic_path = f'/mosaic/definition?mosaicId={MOSAIC_ID}' + print(f'Fetching mosaic information from {mosaic_path}') + with urllib.request.urlopen(f'{NODE_URL}{mosaic_path}') as response: + response_json = json.loads(response.read().decode()) + mosaic_id = response_json['id'] + full_name = f'{mosaic_id["namespaceId"]}:{mosaic_id["name"]}' + print('Mosaic information:') + print(f' Mosaic ID: {full_name}') + print(f' Description: {response_json["description"]}') + print(f' Creator: {response_json["creator"]}') + properties = { + prop['name']: prop['value'] + for prop in response_json['properties'] + } + divisibility = int(properties['divisibility']) + print(f' Divisibility: {divisibility}') + print(f' Initial supply: {properties["initialSupply"]}') + print(f' Supply mutable: {properties["supplyMutable"]}') + print(f' Transferable: {properties["transferable"]}') + levy = response_json['levy'] + print(f' Levy: {levy if levy else "none"}') + # [step-2] + supply_path = f'/mosaic/supply?mosaicId={MOSAIC_ID}' + print(f'\nFetching current supply from {supply_path}') + with urllib.request.urlopen(f'{NODE_URL}{supply_path}') as response: + supply_info = json.loads(response.read().decode()) + supply = supply_info['supply'] + print(f' Current supply: {supply}') + # [step-3] + atomic = supply * 10 ** divisibility + print(f'\nSupply in atomic units: {atomic}') + # [step-1] + const namespacePath = `/namespace?namespace=${NAMESPACE_NAME}`; + console.log('Fetching namespace information from', namespacePath); + const namespaceResponse = await fetch(`${NODE_URL}${namespacePath}`); + if (!namespaceResponse.ok) + throw new Error(`HTTP error! status: ${namespaceResponse.status}`); + + const namespaceInfo = await namespaceResponse.json(); + console.log('Namespace information:'); + console.log(' Name:', namespaceInfo.fqn); + console.log(' Owner:', namespaceInfo.owner); + const leaseHeight = namespaceInfo.height; + console.log(' Height:', leaseHeight); + // [step-2] + const LEASE_DURATION = 525600; // approximately one year of blocks + const chainResponse = await fetch(`${NODE_URL}/chain/height`); + const currentHeight = (await chainResponse.json()).height; + const expirationHeight = leaseHeight + LEASE_DURATION; + console.log('\nCurrent chain height:', currentHeight); + console.log('Lease expiration height:', expirationHeight); + console.log('Blocks until expiration:', + expirationHeight - currentHeight); + // [step-3] + const owner = namespaceInfo.owner; + const subnamespacesPath = '/account/namespace/page' + + `?address=${owner}&parent=${NAMESPACE_NAME}`; + console.log('\nFetching subnamespaces from', subnamespacesPath); + const subnamespacesResponse = + await fetch(`${NODE_URL}${subnamespacesPath}`); + const subnamespaces = (await subnamespacesResponse.json()).data; + console.log(`Subnamespaces of ${NAMESPACE_NAME}:`, + subnamespaces.length); + for (const subnamespace of subnamespaces) + console.log(` ${subnamespace.fqn}`); + // [step-4] + const mosaicsPath = + `/namespace/mosaic/definition/page?namespace=${NAMESPACE_NAME}`; + console.log('\nFetching mosaic definitions from', mosaicsPath); + const mosaicsResponse = await fetch(`${NODE_URL}${mosaicsPath}`); + const mosaics = (await mosaicsResponse.json()).data; + console.log(`Mosaics defined under ${NAMESPACE_NAME}:`, + mosaics.length); + for (const entry of mosaics) { + const mosaicId = entry.mosaic.id; + console.log(` ${mosaicId.namespaceId}:${mosaicId.name}`); + } + // [step-1] + namespace_path = f'/namespace?namespace={NAMESPACE_NAME}' + print(f'Fetching namespace information from {namespace_path}') + with urllib.request.urlopen(f'{NODE_URL}{namespace_path}') as response: + namespace_info = json.loads(response.read().decode()) + print('Namespace information:') + print(f' Name: {namespace_info["fqn"]}') + print(f' Owner: {namespace_info["owner"]}') + lease_height = namespace_info['height'] + print(f' Height: {lease_height}') + # [step-2] + LEASE_DURATION = 525600 # approximately one year of blocks + with urllib.request.urlopen(f'{NODE_URL}/chain/height') as response: + current_height = json.loads(response.read().decode())['height'] + expiration_height = lease_height + LEASE_DURATION + print(f'\nCurrent chain height: {current_height}') + print(f'Lease expiration height: {expiration_height}') + print(f'Blocks until expiration: {expiration_height - current_height}') + # [step-3] + owner = namespace_info['owner'] + subnamespaces_path = ( + f'/account/namespace/page' + f'?address={owner}&parent={NAMESPACE_NAME}') + print(f'\nFetching subnamespaces from {subnamespaces_path}') + with urllib.request.urlopen( + f'{NODE_URL}{subnamespaces_path}' + ) as response: + subnamespaces = json.loads(response.read().decode())['data'] + print(f'Subnamespaces of {NAMESPACE_NAME}: {len(subnamespaces)}') + for subnamespace in subnamespaces: + print(f' {subnamespace["fqn"]}') + # [step-4] + mosaics_path = ( + f'/namespace/mosaic/definition/page?namespace={NAMESPACE_NAME}') + print(f'\nFetching mosaic definitions from {mosaics_path}') + with urllib.request.urlopen(f'{NODE_URL}{mosaics_path}') as response: + mosaics = json.loads(response.read().decode())['data'] + print(f'Mosaics defined under {NAMESPACE_NAME}: {len(mosaics)}') + for entry in mosaics: + mosaic_id = entry['mosaic']['id'] + print(f' {mosaic_id["namespaceId"]}:{mosaic_id["name"]}') + # [ (Number(v) / 1e6).toLocaleString( + 'en-US', { minimumFractionDigits: 6 }); + +const NODE_URL = process.env.NODE_URL || + 'http://libertalia.nemtest.net:7890'; +console.log(`Using node ${NODE_URL}`); + +const BLOCK_HEIGHT = process.env.BLOCK_HEIGHT || '661258'; + +const facade = new NemFacade('testnet'); + +try { + // Fetch the block at the given height [>step-1] + const blockUrl = `${NODE_URL}/block/at/public`; + const response = await fetch(blockUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ height: parseInt(BLOCK_HEIGHT, 10) }) + }); + if (!response.ok) + throw new Error(`HTTP error! status: ${response.status}`); + const block = await response.json(); + const transactions = block.transactions; + console.log(`Block height: ${BLOCK_HEIGHT}`); + console.log(`Transactions: ${transactions.length}`); + // [step-2] + const harvester = facade.network.publicKeyToAddress( + new PublicKey(block.signer)); + console.log(`Harvester: ${harvester}`); + // [step-3] + let totalReward = 0n; + console.log('\nTransaction fees:'); + for (const transaction of transactions) { + const fee = BigInt(transaction.fee); + totalReward += fee; + console.log(` Fee: ${fmt(fee)} XEM`); + } + // [step-4] + console.log(`\nTotal block reward: ${fmt(totalReward)} XEM`); + // [step-1] + block_url = f'{NODE_URL}/block/at/public' + request = urllib.request.Request( + block_url, + data=json.dumps({'height': int(BLOCK_HEIGHT)}).encode(), + headers={'Content-Type': 'application/json'}) + with urllib.request.urlopen(request) as response: + block = json.loads(response.read()) + transactions = block['transactions'] + print(f'Block height: {BLOCK_HEIGHT}') + print(f'Transactions: {len(transactions)}') + # [step-2] + harvester = facade.network.public_key_to_address( + PublicKey(block['signer'])) + print(f'Harvester: {harvester}') + # [step-3] + total_reward = 0 + print('\nTransaction fees:') + for transaction in transactions: + fee = int(transaction['fee']) + total_reward += fee + print(f' Fee: {fee / 1e6:,.6f} XEM') + # [step-4] + print(f'\nTotal block reward: {total_reward / 1e6:,.6f} XEM') + # [ + xem.toLocaleString('en-US', { minimumFractionDigits: 6 }); + + const MOSAIC_ID = 'nem:xem'; // [>step-1] + const supplyPath = `/mosaic/supply?mosaicId=${MOSAIC_ID}`; + const response = await fetch(`${NODE_URL}${supplyPath}`); + const supplyInfo = await response.json(); + const totalSupply = supplyInfo.supply; + console.log(`Total supply: ${fmt(totalSupply)} ${MOSAIC_ID}`); // [step-2] + const definitionPath = `/mosaic/definition?mosaicId=${MOSAIC_ID}`; + const definitionResponse = + await fetch(`${NODE_URL}${definitionPath}`); + const definition = await definitionResponse.json(); + const properties = Object.fromEntries( + definition.properties.map( + property => [property.name, property.value])); + const divisibility = parseInt(properties.divisibility, 10); + // [step-3] + const NON_CIRCULATING_ADDRESSES = [ + ['Treasury', 'NCHESTYVD2P6P646AMY7WSNG73PCPZDUQNSD6JAK'], + ['Nemesis', 'NANEMOABLAGR72AZ2RV3V4ZHDCXW25XQ73O7OBT5'], + ['Namespace rental', 'NAMESPACEWH4MKFMBCVFERDPOOP4FK7MTBXDPZZA'], + ['Mosaic rental', 'NBMOSAICOD4F54EE5CDMR23CCBGOAM2XSIUX6TRS'] + ]; + let nonCirculatingSupply = 0; + for (const [label, address] of NON_CIRCULATING_ADDRESSES) { + const accountPath = `/account/get?address=${address}`; + const accountResponse = await fetch(`${NODE_URL}${accountPath}`); + const accountInfo = await accountResponse.json(); + const balance = + accountInfo.account.balance / (10 ** divisibility); + nonCirculatingSupply += balance; + console.log(` ${label}: ${fmt(balance)} ${MOSAIC_ID}`); + } + console.log( + `Non-circulating supply: ` + + `${fmt(nonCirculatingSupply)} ${MOSAIC_ID}` + ); // [step-4] + const circulatingSupply = totalSupply - nonCirculatingSupply; + console.log( + `Circulating supply: ` + + `${fmt(circulatingSupply)} ${MOSAIC_ID}` + ); // [step-1] + supply_path = f'/mosaic/supply?mosaicId={MOSAIC_ID}' + with urllib.request.urlopen(f'{NODE_URL}{supply_path}') as response: + supply_info = json.loads(response.read().decode()) + total_supply = supply_info['supply'] + print(f'Total supply: {total_supply:,.6f} {MOSAIC_ID}') # [step-2] + definition_path = f'/mosaic/definition?mosaicId={MOSAIC_ID}' + with urllib.request.urlopen( + f'{NODE_URL}{definition_path}' + ) as response: + definition = json.loads(response.read().decode()) + properties = { + prop['name']: prop['value'] + for prop in definition['properties'] + } + divisibility = int(properties['divisibility']) + # [step-3] + NON_CIRCULATING_ADDRESSES = [ + ('Treasury', 'NCHESTYVD2P6P646AMY7WSNG73PCPZDUQNSD6JAK'), + ('Nemesis', 'NANEMOABLAGR72AZ2RV3V4ZHDCXW25XQ73O7OBT5'), + ('Namespace rental', 'NAMESPACEWH4MKFMBCVFERDPOOP4FK7MTBXDPZZA'), + ('Mosaic rental', 'NBMOSAICOD4F54EE5CDMR23CCBGOAM2XSIUX6TRS'), + ] + non_circulating_supply = 0 + for label, address in NON_CIRCULATING_ADDRESSES: + account_path = f'/account/get?address={address}' + with urllib.request.urlopen( + f'{NODE_URL}{account_path}' + ) as response: + account_info = json.loads(response.read().decode()) + balance = (account_info['account']['balance'] + / (10 ** divisibility)) + non_circulating_supply += balance + print(f' {label}: {balance:,.6f} {MOSAIC_ID}') + print( + f'Non-circulating supply: ' + f'{non_circulating_supply:,.6f} {MOSAIC_ID}') # [step-4] + circulating_supply = total_supply - non_circulating_supply + print(f'Circulating supply: {circulating_supply:,.6f} {MOSAIC_ID}') # [ +
+ + +
Size: 173 bytes = 0xad
schema
+
ser:AccountKeyLinkTransactionV1
binary layout for an account key link transaction (V1, latest)
+ + +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const ACCOUNT_KEY_LINK (0x801)
+
 
+
 
+
 
+
link_action
+ +

link action

+
 
+
 
+
 
+
remote_public_key_size
+
byte[4]
+
reserved 32

remote account public key size

+
 
+
 
+
 
+
remote_public_key
+ +

public key of remote account to which importance should be transferred

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/Block.html b/mkdocs/snippets/devbook/reference/serialization/Block.html new file mode 100644 index 000000000..b2e9b81dc --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/Block.html @@ -0,0 +1,100 @@ +
+
+ + +
Size: 168+ bytes = 0xa8+ (variable)
schema
+
ser:Block
binary layout for a block
+
+ +
+
 
+
 
+
 
+
type
+ +

block type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
previous_block_hash_outer_size
+
byte[4]
+
reserved 36

previous block hash outer size

+
 
+
 
+
 
+
previous_block_hash_size
+
byte[4]
+
reserved 32
+
 
+
 
+
 
+
previous_block_hash
+ +
+
 
+
 
+
 
+
height
+ +

block height

+
 
+
 
+
 
+
transactions_count
+
byte[4]
+

transactions count

+
 
+
 
+
 
+
transactions
+
Transaction​[transactions_count]
+

transactions

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/BlockType.html b/mkdocs/snippets/devbook/reference/serialization/BlockType.html new file mode 100644 index 000000000..509a9f461 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/BlockType.html @@ -0,0 +1,16 @@ +
+
+ + +
Size: 4 bytes = 0x4
schema
+
ser:BlockType
enumeration of block types
+
+ +
+
0xffffffff
+
NEMESIS
+

nemesis block

+
0x1
+
NORMAL
+

normal block

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/CosignatureV1.html b/mkdocs/snippets/devbook/reference/serialization/CosignatureV1.html new file mode 100644 index 000000000..50ccfbb45 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/CosignatureV1.html @@ -0,0 +1,118 @@ +
+
+ + +
Size: 217 bytes = 0xd9
schema
+
ser:CosignatureV1
binary layout for a cosignature transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MULTISIG_COSIGNATURE (0x1002)
+
 
+
 
+
 
+
other_​transaction_​hash_​outer_​size
+
byte[4]
+
reserved 36

other transaction hash outer size

+
 
+
 
+
 
+
other_transaction_hash_size
+
byte[4]
+
reserved 32

other transaction hash size

+
 
+
 
+
 
+
other_transaction_hash
+ +

other transaction hash

+
 
+
 
+
 
+
multisig_account_address_size
+
byte[4]
+
reserved 40

multisig account address size

+
 
+
 
+
 
+
multisig_account_address
+ +

multisig account address

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/CosignatureV1Body.html b/mkdocs/snippets/devbook/reference/serialization/CosignatureV1Body.html new file mode 100644 index 000000000..d005825ab --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/CosignatureV1Body.html @@ -0,0 +1,52 @@ +
+
+ + +
Size: 89 bytes = 0x59
schema
+
ser:CosignatureV1Body
shared content between V1 verifiable and non-verifiable cosignature transactions
+
+ +
+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MULTISIG_COSIGNATURE (0x1002)
+
 
+
 
+
 
+
other_​transaction_​hash_​outer_​size
+
byte[4]
+
reserved 36

other transaction hash outer size

+
 
+
 
+
 
+
other_transaction_hash_size
+
byte[4]
+
reserved 32

other transaction hash size

+
 
+
 
+
 
+
other_transaction_hash
+ +

other transaction hash

+
 
+
 
+
 
+
multisig_account_address_size
+
byte[4]
+
reserved 40

multisig account address size

+
 
+
 
+
 
+
multisig_account_address
+ +

multisig account address

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/LinkAction.html b/mkdocs/snippets/devbook/reference/serialization/LinkAction.html new file mode 100644 index 000000000..4e76c91a9 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/LinkAction.html @@ -0,0 +1,16 @@ +
+
+ + +
Size: 4 bytes = 0x4
schema
+
ser:LinkAction
enumeration of link actions
+
+ +
+
0x1
+
LINK
+

unlink account

+
0x2
+
UNLINK
+

link account

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/Message.html b/mkdocs/snippets/devbook/reference/serialization/Message.html new file mode 100644 index 000000000..30a0163db --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/Message.html @@ -0,0 +1,28 @@ +
+
+ + +
Size: 8+ bytes = 0x8+ (variable)
schema
+
ser:Message
binary layout for a message
+
+ +
+
 
+
 
+
 
+
message_type
+ +

message type

+
 
+
 
+
 
+
message_size
+
byte[4]
+

message size

+
 
+
 
+
 
+
message
+
byte[message_size]
+

message payload

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MessageType.html b/mkdocs/snippets/devbook/reference/serialization/MessageType.html new file mode 100644 index 000000000..c485df1bf --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MessageType.html @@ -0,0 +1,16 @@ +
+
+ + +
Size: 4 bytes = 0x4
schema
+
ser:MessageType
enumeration of message types this is a hint used by the client but ignored by the server
+
+ +
+
0x1
+
PLAIN
+

plain message

+
0x2
+
ENCRYPTED
+

encrypted message

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/Mosaic.html b/mkdocs/snippets/devbook/reference/serialization/Mosaic.html new file mode 100644 index 000000000..6220b17ab --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/Mosaic.html @@ -0,0 +1,28 @@ +
+
+ + +
Size: 20+ bytes = 0x14+ (variable)
schema
+
ser:Mosaic
binary layout for a mosaic
+
+ +
+
 
+
 
+
 
+
mosaic_id_size
+
byte[4]
+

mosaic id size

+
 
+
 
+
 
+
mosaic_id
+ +

mosaic id

+
 
+
 
+
 
+
amount
+ +

quantity

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MosaicDefinition.html b/mkdocs/snippets/devbook/reference/serialization/MosaicDefinition.html new file mode 100644 index 000000000..0fc8914d2 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MosaicDefinition.html @@ -0,0 +1,70 @@ +
+
+ + +
Size: 60+ bytes = 0x3c+ (variable)
schema
+
ser:MosaicDefinition
binary layout for a mosaic definition
+
+ +
+
 
+
 
+
 
+
owner_public_key_size
+
byte[4]
+
reserved 32

owner public key size

+
 
+
 
+
 
+
owner_public_key
+ +

owner public key

+
 
+
 
+
 
+
id_size
+
byte[4]
+

mosaic id size

+
 
+
 
+
 
+
id
+ +

mosaic id referenced by this definition

+
 
+
 
+
 
+
description_size
+
byte[4]
+

description size

+
 
+
 
+
 
+
description
+
byte[description_size]
+

description

+
 
+
 
+
 
+
properties_count
+
byte[4]
+

number of properties

+
 
+
 
+
 
+
properties
+
SizePrefixedMosaicProperty​[properties_count]
+

properties

+
 
+
 
+
 
+
levy_size
+
byte[4]
+

size of the serialized levy

+
 
+
 
+
 
+
levy
+ +

optional levy that is applied to transfers of this mosaic

This field is only present if:
levy_size not equals 0
+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MosaicDefinitionTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/MosaicDefinitionTransactionV1.html new file mode 100644 index 000000000..af692cb70 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MosaicDefinitionTransactionV1.html @@ -0,0 +1,118 @@ +
+
+ + +
Size: 249+ bytes = 0xf9+ (variable)
schema
+
ser:MosaicDefinitionTransactionV1
binary layout for a mosaic definition transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MOSAIC_DEFINITION (0x4001)
+
 
+
 
+
 
+
mosaic_definition_size
+
byte[4]
+

mosaic definition size

+
 
+
 
+
 
+
mosaic_definition
+ +

mosaic definition

+
 
+
 
+
 
+
rental_fee_sink_size
+
byte[4]
+
reserved 40

mosaic rental fee sink public key size

+
 
+
 
+
 
+
rental_fee_sink
+ +

mosaic rental fee sink public key

+
 
+
 
+
 
+
rental_fee
+ +

mosaic rental fee

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MosaicId.html b/mkdocs/snippets/devbook/reference/serialization/MosaicId.html new file mode 100644 index 000000000..46b91afe2 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MosaicId.html @@ -0,0 +1,28 @@ +
+
+ + +
Size: 8+ bytes = 0x8+ (variable)
schema
+
ser:MosaicId
binary layout for a mosaic id
+
+ +
+
 
+
 
+
 
+
namespace_id
+ +

namespace id

+
 
+
 
+
 
+
name_size
+
byte[4]
+

name size

+
 
+
 
+
 
+
name
+
byte[name_size]
+

name

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MosaicLevy.html b/mkdocs/snippets/devbook/reference/serialization/MosaicLevy.html new file mode 100644 index 000000000..327c37cd8 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MosaicLevy.html @@ -0,0 +1,46 @@ +
+
+ + +
Size: 68+ bytes = 0x44+ (variable)
schema
+
ser:MosaicLevy
binary layout for a mosaic levy
+
+ +
+
 
+
 
+
 
+
transfer_fee_type
+ +

mosaic fee type

+
 
+
 
+
 
+
recipient_address_size
+
byte[4]
+
reserved 40

recipient address size

+
 
+
 
+
 
+
recipient_address
+ +

recipient address

+
 
+
 
+
 
+
mosaic_id_size
+
byte[4]
+

levy mosaic id size

+
 
+
 
+
 
+
mosaic_id
+ +

levy mosaic id

+
 
+
 
+
 
+
fee
+ +

amount of levy mosaic to transfer

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MosaicProperty.html b/mkdocs/snippets/devbook/reference/serialization/MosaicProperty.html new file mode 100644 index 000000000..af6017898 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MosaicProperty.html @@ -0,0 +1,34 @@ +
+
+ + +
Size: 8+ bytes = 0x8+ (variable)
schema
+
ser:MosaicProperty
binary layout for a mosaic property supported property names are: divisibility, initialSupply, supplyMutable, transferable
+
+ +
+
 
+
 
+
 
+
name_size
+
byte[4]
+

property name size

+
 
+
 
+
 
+
name
+
byte[name_size]
+

property name

+
 
+
 
+
 
+
value_size
+
byte[4]
+

property value size

+
 
+
 
+
 
+
value
+
byte[value_size]
+

property value

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MosaicSupplyChangeAction.html b/mkdocs/snippets/devbook/reference/serialization/MosaicSupplyChangeAction.html new file mode 100644 index 000000000..582fc2fea --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MosaicSupplyChangeAction.html @@ -0,0 +1,16 @@ +
+
+ + +
Size: 4 bytes = 0x4
schema
+
ser:MosaicSupplyChangeAction
enumeration of mosaic supply change actions
+
+ +
+
0x1
+
INCREASE
+

increases the supply

+
0x2
+
DECREASE
+

decreases the supply

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MosaicSupplyChangeTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/MosaicSupplyChangeTransactionV1.html new file mode 100644 index 000000000..31154acd9 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MosaicSupplyChangeTransactionV1.html @@ -0,0 +1,112 @@ +
+
+ + +
Size: 157+ bytes = 0x9d+ (variable)
schema
+
ser:MosaicSupplyChangeTransactionV1
binary layout for a mosaic supply change transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MOSAIC_SUPPLY_CHANGE (0x4002)
+
 
+
 
+
 
+
mosaic_id_size
+
byte[4]
+

mosaic id size

+
 
+
 
+
 
+
mosaic_id
+ +

mosaic id

+
 
+
 
+
 
+
action
+ +

supply change action

+
 
+
 
+
 
+
delta
+ +

change amount

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MosaicTransferFeeType.html b/mkdocs/snippets/devbook/reference/serialization/MosaicTransferFeeType.html new file mode 100644 index 000000000..e0ceac7bc --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MosaicTransferFeeType.html @@ -0,0 +1,16 @@ +
+
+ + +
Size: 4 bytes = 0x4
schema
+
ser:MosaicTransferFeeType
enumeration of mosaic transfer fee types
+
+ +
+
0x1
+
ABSOLUTE
+

fee represents an absolute value

+
0x2
+
PERCENTILE
+

fee is proportional to a percentile of the transferred mosaic

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModification.html b/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModification.html new file mode 100644 index 000000000..f02fe707a --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModification.html @@ -0,0 +1,28 @@ +
+
+ + +
Size: 40 bytes = 0x28
schema
+
ser:MultisigAccountModification
binary layout for a multisig account modification
+
+ +
+
 
+
 
+
 
+
modification_type
+ +

modification type

+
 
+
 
+
 
+
cosignatory_public_key_size
+
byte[4]
+
reserved 32

cosignatory public key size

+
 
+
 
+
 
+
cosignatory_public_key
+ +

cosignatory public key

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModificationTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModificationTransactionV1.html new file mode 100644 index 000000000..01916ca77 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModificationTransactionV1.html @@ -0,0 +1,100 @@ +
+
+ + +
Size: 137+ bytes = 0x89+ (variable)
schema
+
ser:MultisigAccountModificationTransactionV1
binary layout for a multisig account modification transaction (V1)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MULTISIG_ACCOUNT_MODIFICATION (0x1001)
+
 
+
 
+
 
+
modifications_count
+
byte[4]
+

number of modifications

+
 
+
 
+
 
+
modifications
+ +

multisig account modifications

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModificationTransactionV2.html b/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModificationTransactionV2.html new file mode 100644 index 000000000..7d0d07a00 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModificationTransactionV2.html @@ -0,0 +1,112 @@ +
+
+ + +
Size: 145+ bytes = 0x91+ (variable)
schema
+
ser:MultisigAccountModificationTransactionV2
binary layout for a multisig account modification transaction (V2, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 2
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MULTISIG_ACCOUNT_MODIFICATION (0x1001)
+
 
+
 
+
 
+
modifications_count
+
byte[4]
+

number of modifications

+
 
+
 
+
 
+
modifications
+ +

multisig account modifications

+
 
+
 
+
 
+
min_approval_delta_size
+
byte[4]
+
reserved 4

the size of the min_approval_delta

+
 
+
 
+
 
+
min_approval_delta
+
byte[4]
+

relative change of the minimal number of cosignatories required when approving a transaction

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModificationType.html b/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModificationType.html new file mode 100644 index 000000000..c8a02213d --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MultisigAccountModificationType.html @@ -0,0 +1,16 @@ +
+
+ + +
Size: 4 bytes = 0x4
schema
+
ser:MultisigAccountModificationType
enumeration of multisig account modification types
+
+ +
+
0x1
+
ADD_COSIGNATORY
+

add a new cosignatory

+
0x2
+
DELETE_COSIGNATORY
+

delete an existing cosignatory

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/MultisigTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/MultisigTransactionV1.html new file mode 100644 index 000000000..1508ed4a5 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/MultisigTransactionV1.html @@ -0,0 +1,112 @@ +
+
+ + +
Size: 201+ bytes = 0xc9+ (variable)
schema
+
ser:MultisigTransactionV1
binary layout for a multisig transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MULTISIG (0x1004)
+
 
+
 
+
 
+
inner_transaction_size
+
byte[4]
+

inner transaction size

+
 
+
 
+
 
+
inner_transaction
+ +

inner transaction

+
 
+
 
+
 
+
cosignatures_count
+
byte[4]
+

number of attached cosignatures

+
 
+
 
+
 
+
cosignatures
+
SizePrefixedCosignatureV1​[cosignatures_count]
+

cosignatures

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NamespaceId.html b/mkdocs/snippets/devbook/reference/serialization/NamespaceId.html new file mode 100644 index 000000000..e0a5a9205 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NamespaceId.html @@ -0,0 +1,22 @@ +
+
+ + +
Size: 4+ bytes = 0x4+ (variable)
schema
+
ser:NamespaceId
binary layout for a namespace id
+
+ +
+
 
+
 
+
 
+
name_size
+
byte[4]
+

name size

+
 
+
 
+
 
+
name
+
byte[name_size]
+

name

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NamespaceRegistrationTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/NamespaceRegistrationTransactionV1.html new file mode 100644 index 000000000..2c31a4a5e --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NamespaceRegistrationTransactionV1.html @@ -0,0 +1,130 @@ +
+
+ + +
Size: 193+ bytes = 0xc1+ (variable)
schema
+
ser:NamespaceRegistrationTransactionV1
binary layout for a namespace registration transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const NAMESPACE_REGISTRATION (0x2001)
+
 
+
 
+
 
+
rental_fee_sink_size
+
byte[4]
+
reserved 40

mosaic rental fee sink public key size

+
 
+
 
+
 
+
rental_fee_sink
+ +

mosaic rental fee sink public key

+
 
+
 
+
 
+
rental_fee
+ +

mosaic rental fee

+
 
+
 
+
 
+
name_size
+
byte[4]
+

new namespace name size

+
 
+
 
+
 
+
name
+
byte[name_size]
+

new namespace name

+
 
+
 
+
 
+
parent_name_size
+
byte[4]
+

size of the parent namespace name

+
 
+
 
+
 
+
parent_name
+
byte[parent_name_size]
+

parent namespace name

This field is only present if:
parent_name_size not equals 4294967295
+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NetworkType.html b/mkdocs/snippets/devbook/reference/serialization/NetworkType.html new file mode 100644 index 000000000..6cecbb3e0 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NetworkType.html @@ -0,0 +1,16 @@ +
+
+ + +
Size: 1 byte = 0x1
schema
+
ser:NetworkType
enumeration of network types
+
+ +
+
0x68
+
MAINNET
+

main network

+
0x98
+
TESTNET
+

test network

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableAccountKeyLinkTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableAccountKeyLinkTransactionV1.html new file mode 100644 index 000000000..ae43c841e --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableAccountKeyLinkTransactionV1.html @@ -0,0 +1,94 @@ +
+
+ + +
Size: 105 bytes = 0x69
schema
+
ser:NonVerifiableAccountKeyLinkTransactionV1
binary layout for a non-verifiable account key link transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const ACCOUNT_KEY_LINK (0x801)
+
 
+
 
+
 
+
link_action
+ +

link action

+
 
+
 
+
 
+
remote_public_key_size
+
byte[4]
+
reserved 32

remote account public key size

+
 
+
 
+
 
+
remote_public_key
+ +

public key of remote account to which importance should be transferred

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableCosignatureV1.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableCosignatureV1.html new file mode 100644 index 000000000..87947c4ad --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableCosignatureV1.html @@ -0,0 +1,106 @@ +
+
+ + +
Size: 149 bytes = 0x95
schema
+
ser:NonVerifiableCosignatureV1
binary layout for a non-verifiable cosignature transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MULTISIG_COSIGNATURE (0x1002)
+
 
+
 
+
 
+
other_​transaction_​hash_​outer_​size
+
byte[4]
+
reserved 36

other transaction hash outer size

+
 
+
 
+
 
+
other_transaction_hash_size
+
byte[4]
+
reserved 32

other transaction hash size

+
 
+
 
+
 
+
other_transaction_hash
+ +

other transaction hash

+
 
+
 
+
 
+
multisig_account_address_size
+
byte[4]
+
reserved 40

multisig account address size

+
 
+
 
+
 
+
multisig_account_address
+ +

multisig account address

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMosaicDefinitionTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMosaicDefinitionTransactionV1.html new file mode 100644 index 000000000..3df8ccdab --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMosaicDefinitionTransactionV1.html @@ -0,0 +1,106 @@ +
+
+ + +
Size: 181+ bytes = 0xb5+ (variable)
schema
+
ser:NonVerifiableMosaicDefinitionTransactionV1
binary layout for a non-verifiable mosaic definition transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MOSAIC_DEFINITION (0x4001)
+
 
+
 
+
 
+
mosaic_definition_size
+
byte[4]
+

mosaic definition size

+
 
+
 
+
 
+
mosaic_definition
+ +

mosaic definition

+
 
+
 
+
 
+
rental_fee_sink_size
+
byte[4]
+
reserved 40

mosaic rental fee sink public key size

+
 
+
 
+
 
+
rental_fee_sink
+ +

mosaic rental fee sink public key

+
 
+
 
+
 
+
rental_fee
+ +

mosaic rental fee

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMosaicSupplyChangeTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMosaicSupplyChangeTransactionV1.html new file mode 100644 index 000000000..076c4394d --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMosaicSupplyChangeTransactionV1.html @@ -0,0 +1,100 @@ +
+
+ + +
Size: 89+ bytes = 0x59+ (variable)
schema
+
ser:NonVerifiableMosaicSupplyChangeTransactionV1
binary layout for a non-verifiable mosaic supply change transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MOSAIC_SUPPLY_CHANGE (0x4002)
+
 
+
 
+
 
+
mosaic_id_size
+
byte[4]
+

mosaic id size

+
 
+
 
+
 
+
mosaic_id
+ +

mosaic id

+
 
+
 
+
 
+
action
+ +

supply change action

+
 
+
 
+
 
+
delta
+ +

change amount

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMultisigAccountModificationTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMultisigAccountModificationTransactionV1.html new file mode 100644 index 000000000..3f63aa8ad --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMultisigAccountModificationTransactionV1.html @@ -0,0 +1,88 @@ +
+
+ + +
Size: 69+ bytes = 0x45+ (variable)
schema
+
ser:NonVerifiableMultisigAccountModificationTransactionV1
binary layout for a non-verifiable multisig account modification transaction (V1)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MULTISIG_ACCOUNT_MODIFICATION (0x1001)
+
 
+
 
+
 
+
modifications_count
+
byte[4]
+

number of modifications

+
 
+
 
+
 
+
modifications
+ +

multisig account modifications

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMultisigAccountModificationTransactionV2.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMultisigAccountModificationTransactionV2.html new file mode 100644 index 000000000..502566b36 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMultisigAccountModificationTransactionV2.html @@ -0,0 +1,100 @@ +
+
+ + +
Size: 77+ bytes = 0x4d+ (variable)
schema
+
ser:NonVerifiableMultisigAccountModificationTransactionV2
binary layout for a non-verifiable multisig account modification transaction (V2, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 2
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MULTISIG_ACCOUNT_MODIFICATION (0x1001)
+
 
+
 
+
 
+
modifications_count
+
byte[4]
+

number of modifications

+
 
+
 
+
 
+
modifications
+ +

multisig account modifications

+
 
+
 
+
 
+
min_approval_delta_size
+
byte[4]
+
reserved 4

the size of the min_approval_delta

+
 
+
 
+
 
+
min_approval_delta
+
byte[4]
+

relative change of the minimal number of cosignatories required when approving a transaction

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMultisigTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMultisigTransactionV1.html new file mode 100644 index 000000000..c2f62438a --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableMultisigTransactionV1.html @@ -0,0 +1,88 @@ +
+
+ + +
Size: 129 bytes = 0x81
schema
+
ser:NonVerifiableMultisigTransactionV1
binary layout for a non-verifiable multisig transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const MULTISIG (0x1004)
+
 
+
 
+
 
+
inner_transaction_size
+
byte[4]
+

inner transaction size

+
 
+
 
+
 
+
inner_transaction
+ +

inner transaction

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableNamespaceRegistrationTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableNamespaceRegistrationTransactionV1.html new file mode 100644 index 000000000..063bb7248 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableNamespaceRegistrationTransactionV1.html @@ -0,0 +1,118 @@ +
+
+ + +
Size: 125+ bytes = 0x7d+ (variable)
schema
+
ser:NonVerifiableNamespaceRegistrationTransactionV1
binary layout for a non-verifiable namespace registration transaction (V1, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const NAMESPACE_REGISTRATION (0x2001)
+
 
+
 
+
 
+
rental_fee_sink_size
+
byte[4]
+
reserved 40

mosaic rental fee sink public key size

+
 
+
 
+
 
+
rental_fee_sink
+ +

mosaic rental fee sink public key

+
 
+
 
+
 
+
rental_fee
+ +

mosaic rental fee

+
 
+
 
+
 
+
name_size
+
byte[4]
+

new namespace name size

+
 
+
 
+
 
+
name
+
byte[name_size]
+

new namespace name

+
 
+
 
+
 
+
parent_name_size
+
byte[4]
+

size of the parent namespace name

+
 
+
 
+
 
+
parent_name
+
byte[parent_name_size]
+

parent namespace name

This field is only present if:
parent_name_size not equals 4294967295
+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableTransaction.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableTransaction.html new file mode 100644 index 000000000..3cd4a2d18 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableTransaction.html @@ -0,0 +1,63 @@ +
+
+ +
Size: 60 bytes = 0x3c
+
ser:NonVerifiableTransaction
binary layout for a non-verifiable transaction
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableTransferTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableTransferTransactionV1.html new file mode 100644 index 000000000..94cf52bd6 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableTransferTransactionV1.html @@ -0,0 +1,106 @@ +
+
+ + +
Size: 121+ bytes = 0x79+ (variable)
schema
+
ser:NonVerifiableTransferTransactionV1
binary layout for a non-verifiable transfer transaction (V1)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const TRANSFER (0x101)
+
 
+
 
+
 
+
recipient_address_size
+
byte[4]
+
reserved 40

recipient address size

+
 
+
 
+
 
+
recipient_address
+ +

recipient address

+
 
+
 
+
 
+
amount
+ +

XEM amount

+
 
+
 
+
 
+
message_envelope_size
+
byte[4]
+

message envelope size

+
 
+
 
+
 
+
message
+ +

optional message

This field is only present if:
message_envelope_size not equals 0
+
diff --git a/mkdocs/snippets/devbook/reference/serialization/NonVerifiableTransferTransactionV2.html b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableTransferTransactionV2.html new file mode 100644 index 000000000..8611192c2 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/NonVerifiableTransferTransactionV2.html @@ -0,0 +1,118 @@ +
+
+ + +
Size: 125+ bytes = 0x7d+ (variable)
schema
+
ser:NonVerifiableTransferTransactionV2
binary layout for a non-verifiable transfer transaction (V2, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 2
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const TRANSFER (0x101)
+
 
+
 
+
 
+
recipient_address_size
+
byte[4]
+
reserved 40

recipient address size

+
 
+
 
+
 
+
recipient_address
+ +

recipient address

+
 
+
 
+
 
+
amount
+ +

XEM amount

+
 
+
 
+
 
+
message_envelope_size
+
byte[4]
+

message envelope size

+
 
+
 
+
 
+
message
+ +

optional message

This field is only present if:
message_envelope_size not equals 0
+
 
+
 
+
 
+
mosaics_count
+
byte[4]
+

number of attached mosaics

+
 
+
 
+
 
+
mosaics
+
SizePrefixedMosaic​[mosaics_count]
+

attached mosaics notice that mosaic amount is multipled by transfer amount to get effective amount

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/SizePrefixedCosignatureV1.html b/mkdocs/snippets/devbook/reference/serialization/SizePrefixedCosignatureV1.html new file mode 100644 index 000000000..52778f9db --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/SizePrefixedCosignatureV1.html @@ -0,0 +1,22 @@ +
+
+ + +
Size: 221 bytes = 0xdd
schema
+
ser:SizePrefixedCosignatureV1
cosignature attached to a multisig transaction with prefixed size
+
+ +
+
 
+
 
+
 
+
cosignature_size
+
byte[4]
+

cosignature size

+
 
+
 
+
 
+
cosignature
+ +

cosignature

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/SizePrefixedMosaic.html b/mkdocs/snippets/devbook/reference/serialization/SizePrefixedMosaic.html new file mode 100644 index 000000000..c4ac18930 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/SizePrefixedMosaic.html @@ -0,0 +1,22 @@ +
+
+ + +
Size: 24+ bytes = 0x18+ (variable)
schema
+
ser:SizePrefixedMosaic
binary layout for a mosaic with a size prefixed size
+
+ +
+
 
+
 
+
 
+
mosaic_size
+
byte[4]
+

mosaic size

+
 
+
 
+
 
+
mosaic
+ +

mosaic

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/SizePrefixedMosaicProperty.html b/mkdocs/snippets/devbook/reference/serialization/SizePrefixedMosaicProperty.html new file mode 100644 index 000000000..ed1aca576 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/SizePrefixedMosaicProperty.html @@ -0,0 +1,22 @@ +
+
+ + +
Size: 12+ bytes = 0xc+ (variable)
schema
+
ser:SizePrefixedMosaicProperty
binary layout for a size prefixed mosaic property
+
+ +
+
 
+
 
+
 
+
property_size
+
byte[4]
+

property size

+
 
+
 
+
 
+
property
+ +

property value

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/SizePrefixedMultisigAccountModification.html b/mkdocs/snippets/devbook/reference/serialization/SizePrefixedMultisigAccountModification.html new file mode 100644 index 000000000..c9c4a8faf --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/SizePrefixedMultisigAccountModification.html @@ -0,0 +1,22 @@ +
+
+ + +
Size: 44 bytes = 0x2c
schema
+
ser:SizePrefixedMultisigAccountModification
binary layout for a multisig account modification prefixed with size
+
+ +
+
 
+
 
+
 
+
modification_size
+
byte[4]
+

modification size

+
 
+
 
+
 
+
modification
+ +

modification

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/Transaction.html b/mkdocs/snippets/devbook/reference/serialization/Transaction.html new file mode 100644 index 000000000..5f757ed78 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/Transaction.html @@ -0,0 +1,75 @@ +
+
+ +
Size: 128 bytes = 0x80
+
ser:Transaction
binary layout for a transaction
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/TransactionType.html b/mkdocs/snippets/devbook/reference/serialization/TransactionType.html new file mode 100644 index 000000000..d74b3fc66 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/TransactionType.html @@ -0,0 +1,34 @@ +
+
+ + +
Size: 4 bytes = 0x4
schema
+
ser:TransactionType
enumeration of transaction types
+
+ +
+
0x101
+
TRANSFER
+

transfer transaction

+
0x801
+
ACCOUNT_KEY_LINK
+

account key link trasaction alternatively called importance transfer transaction

+
0x1001
+
MULTISIG_ACCOUNT_MODIFICATION
+

multisig account modification transaction alternatively called multisig consignatory modification transaction

+
0x1002
+
MULTISIG_COSIGNATURE
+

multisig cosignature transaction alternatively called multisig signature transaction

+
0x1004
+
MULTISIG
+

multisig transaction

+
0x2001
+
NAMESPACE_REGISTRATION
+

namespace registration transaction alternatively called provision namespace transaction

+
0x4001
+
MOSAIC_DEFINITION
+

mosaic definition transaction alternatively called mosaic definition creation transaction

+
0x4002
+
MOSAIC_SUPPLY_CHANGE
+

mosaic supply change transaction

+
diff --git a/mkdocs/snippets/devbook/reference/serialization/TransferTransactionV1.html b/mkdocs/snippets/devbook/reference/serialization/TransferTransactionV1.html new file mode 100644 index 000000000..68b052172 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/TransferTransactionV1.html @@ -0,0 +1,118 @@ +
+
+ + +
Size: 189+ bytes = 0xbd+ (variable)
schema
+
ser:TransferTransactionV1
binary layout for a transfer transaction (V1)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 1
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const TRANSFER (0x101)
+
 
+
 
+
 
+
recipient_address_size
+
byte[4]
+
reserved 40

recipient address size

+
 
+
 
+
 
+
recipient_address
+ +

recipient address

+
 
+
 
+
 
+
amount
+ +

XEM amount

+
 
+
 
+
 
+
message_envelope_size
+
byte[4]
+

message envelope size

+
 
+
 
+
 
+
message
+ +

optional message

This field is only present if:
message_envelope_size not equals 0
+
diff --git a/mkdocs/snippets/devbook/reference/serialization/TransferTransactionV2.html b/mkdocs/snippets/devbook/reference/serialization/TransferTransactionV2.html new file mode 100644 index 000000000..cd38de429 --- /dev/null +++ b/mkdocs/snippets/devbook/reference/serialization/TransferTransactionV2.html @@ -0,0 +1,130 @@ +
+
+ + +
Size: 193+ bytes = 0xc1+ (variable)
schema
+
ser:TransferTransactionV2
binary layout for a transfer transaction (V2, latest)
+
+ +
+
 
+
 
+
 
+
type
+ +

transaction type

+
 
+
 
+
 
+
version
+
byte[1]
+

entity version

+
 
+
 
+
 
+
entity_body_reserved_1
+
byte[2]
+
reserved 0

reserved padding between version and network type

+
 
+
 
+
 
+
network
+ +

entity network

+
 
+
 
+
 
+
timestamp
+ +

entity timestamp

+
 
+
 
+
 
+
signer_public_key_size
+
byte[4]
+
reserved 32

entity signer public key size

+
 
+
 
+
 
+
signer_public_key
+ +

entity signer public key

+
 
+
 
+
 
+
signature_size
+
byte[4]
+
reserved 64

entity signature size

+
 
+
 
+
 
+
signature
+ +

entity signature

+
 
+
 
+
 
+
fee
+ +

transaction fee

+
 
+
 
+
 
+
deadline
+ +

transaction deadline

+
 
+
 
+
 
+
TRANSACTION_VERSION
+
byte[1]
+
const 2
+
 
+
 
+
 
+
TRANSACTION_TYPE
+ +
const TRANSFER (0x101)
+
 
+
 
+
 
+
recipient_address_size
+
byte[4]
+
reserved 40

recipient address size

+
 
+
 
+
 
+
recipient_address
+ +

recipient address

+
 
+
 
+
 
+
amount
+ +

XEM amount

+
 
+
 
+
 
+
message_envelope_size
+
byte[4]
+

message envelope size

+
 
+
 
+
 
+
message
+ +

optional message

This field is only present if:
message_envelope_size not equals 0
+
 
+
 
+
 
+
mosaics_count
+
byte[4]
+

number of attached mosaics

+
 
+
 
+
 
+
mosaics
+
SizePrefixedMosaic​[mosaics_count]
+

attached mosaics notice that mosaic amount is multipled by transfer amount to get effective amount

+
diff --git a/mkdocs/snippets/devbook/start/hello_world.log b/mkdocs/snippets/devbook/start/hello_world.log new file mode 100644 index 000000000..162321934 --- /dev/null +++ b/mkdocs/snippets/devbook/start/hello_world.log @@ -0,0 +1,5 @@ +Network name: testnet +Network launch date: 2015-03-29 00:06:25+00:00 +Using node http://libertalia.nemtest.net:7890 +Fetching chain height from /chain/height + Blockchain height: 625,079 blocks diff --git a/mkdocs/snippets/devbook/start/hello_world.mjs b/mkdocs/snippets/devbook/start/hello_world.mjs new file mode 100644 index 000000000..9869e1e21 --- /dev/null +++ b/mkdocs/snippets/devbook/start/hello_world.mjs @@ -0,0 +1,27 @@ +import { + NemFacade, + NetworkTimestamp +} from 'symbol-sdk/nem'; +// [>step-1] +const facade = new NemFacade('testnet'); +console.log(`Network name: ${facade.network.name}`); +// NetworkTimestamp(0) is the genesis block timestamp (network launch) +const launchDate = facade.network.toDatetime(new NetworkTimestamp(0)); +console.log(`Network launch date: ${launchDate.toISOString()}`); // [step-2] +const NODE_URL = 'http://libertalia.nemtest.net:7890'; +console.log(`Using node ${NODE_URL}`); +try { + // Fetch current chain height + const heightPath = '/chain/height'; + console.log(`Fetching chain height from ${heightPath}`); + const response = await fetch(`${NODE_URL}${heightPath}`, + { timeout: 10000 }); + if (!response.ok) + throw new Error(`HTTP error! status: ${response.status}`); + const responseJson = await response.json(); + const height = parseInt(responseJson.height, 10); + console.log(` Blockchain height: ${height.toLocaleString()} blocks`); +} catch (e) { + console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown'); +} // [step-1] +facade = NemFacade('testnet') +print(f"Network name: {facade.network.name}") +# NetworkTimestamp(0) is the genesis block timestamp +launch_date = facade.network.to_datetime(NetworkTimestamp(0)) +print(f"Network launch date: {launch_date}") # [step-2] +NODE_URL = 'http://libertalia.nemtest.net:7890' +print(f'Using node {NODE_URL}') +try: + # Fetch current chain height + height_path = '/chain/height' + print(f'Fetching chain height from {height_path}') + with urllib.request.urlopen( + f'{NODE_URL}{height_path}', timeout=10 + ) as response: + response_json = json.loads(response.read().decode()) + height = int(response_json['height']) + print(f" Blockchain height: {height:,} blocks") + +except urllib.error.URLError as e: + print(e.reason) # [step-1] +// Transaction hash to monitor. +const transactionHash = process.env.TRANSACTION_HASH || + 'AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703C0B'; +// Signer's address. +const signerAddress = process.env.SIGNER_ADDRESS || + 'TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP'; +// Transaction signature. +const transactionSignature = process.env.TRANSACTION_SIGNATURE || + '99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70F52F089BD651DF' + + 'A7D4991639A48B810EFD98C45060D7AD9AE57FDA37F58561459DCE8D0A747F02'; +// [step-2] +/** + * Query /transaction/get once to check for confirmation. + * @param {string} txHash - hash of the transaction to check + * @returns {number|null} height of the block containing the + * transaction, or null if it is not confirmed yet + */ +async function getConfirmationHeight(txHash) { + const url = `${NODE_URL}/transaction/get?hash=${txHash}`; + const response = await fetch(url); + if (response.ok) { + const confirmed = await response.json(); + return confirmed.meta.height; + } + if (400 !== response.status) + throw new Error(`Unexpected status: ${response.status}`); + return null; +} // [step-3] +/** + * Check whether a transaction with the given signature is in the + * address's unconfirmed pool. + * @param {string} signature - hex signature of the monitored transaction + * @param {string} address - signer's address + * @returns {boolean} true if the signature is in the signer's pool + */ +async function isInUnconfirmedPool(signature, address) { + const path = `/account/unconfirmedTransactions?address=${address}`; + const response = await fetch(`${NODE_URL}${path}`); + const pool = (await response.json()).data; + + const target = signature.toLowerCase(); + return pool.some( + entry => entry.transaction.signature.toLowerCase() === target + ); +} // [step-4] +/** + * Check for confirmation repeatedly until the transaction is + * confirmed or the attempts run out. + * @param {string} txHash - hash of the transaction to monitor + * @param {number} maxAttempts - maximum polling attempts + * @param {number} waitSeconds - seconds to wait between attempts + * @returns {boolean} true if the transaction was confirmed + */ +async function waitForConfirmation( + txHash, + maxAttempts = 120, + waitSeconds = 1 +) { + console.log('\nWaiting for transaction confirmation'); + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + await new Promise(resolve => { + setTimeout(resolve, waitSeconds * 1000); + }); + const height = await getConfirmationHeight(txHash); + const status = + height ? `confirmed in block ${height}` : 'pending'; + console.log(` Attempt ${attempt}: ${status}`); + if (height) + return true; + } + return false; +} // [step-5] +try { + const blockHeight = await getConfirmationHeight(transactionHash); + if (blockHeight) + console.log(`\nTransaction confirmed in block ${blockHeight}`); + else if (!(await isInUnconfirmedPool(transactionSignature, + signerAddress))) + console.log('\nTransaction not found'); + else if (await waitForConfirmation(transactionHash)) + console.log('\nTransaction confirmed!'); + else + console.log('\nConfirmation timed out'); +} catch (error) { + console.log(`\nCould not reach the node: ${error.message}`); +} +// [step-1] +# Transaction hash to monitor +transaction_hash = os.getenv( + "TRANSACTION_HASH", + "AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703C0B") +# Signer's address +signer_address = os.getenv( + "SIGNER_ADDRESS", + "TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP") +# Transaction signature +transaction_signature = os.getenv( + "TRANSACTION_SIGNATURE", + "99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70F52F089BD651DF" + "A7D4991639A48B810EFD98C45060D7AD9AE57FDA37F58561459DCE8D0A747F02") +# [step-2] + """ + Query /transaction/get once to check for confirmation. + + Args: + tx_hash: hash of the transaction to check + + Returns: + The height of the block containing the transaction, or None + if the transaction is not confirmed yet + """ + url = f"{NODE_URL}/transaction/get?hash={tx_hash}" + try: + with urllib.request.urlopen(url) as response: + confirmed = json.loads(response.read().decode()) + return confirmed["meta"]["height"] + except urllib.error.HTTPError as err: + if err.status != 400: + raise + return None # [step-3] + """ + Check whether a transaction with the given signature is in the + address's unconfirmed pool. + """ + path = f"/account/unconfirmedTransactions?address={address}" + with urllib.request.urlopen(f"{NODE_URL}{path}") as response: + pool = json.loads(response.read().decode())["data"] + + target = signature.lower() + return any( + entry["transaction"]["signature"].lower() == target + for entry in pool + ) # [step-4] + tx_hash, max_attempts=120, wait_seconds=1 +): + """ + Check for confirmation repeatedly until the transaction is confirmed + or the attempts run out. + + Args: + tx_hash: hash of the transaction to monitor + max_attempts: maximum polling attempts + wait_seconds: seconds to wait between attempts + + Returns: + True if the transaction was confirmed, False otherwise + """ + print("\nWaiting for transaction confirmation") + for attempt in range(1, max_attempts + 1): + time.sleep(wait_seconds) + height = get_confirmation_height(tx_hash) + status = f"confirmed in block {height}" if height else "pending" + print(f" Attempt {attempt}: {status}") + if height: + return True + return False # [step-5] + block_height = get_confirmation_height(transaction_hash) + if block_height: + print(f"\nTransaction confirmed in block {block_height}") + elif not is_in_unconfirmed_pool(transaction_signature, + signer_address): + print("\nTransaction not found") + elif wait_for_confirmation(transaction_hash): + print("\nTransaction confirmed!") + else: + print("\nTransaction not confirmed within the polling window") +except urllib.error.URLError as err: + print(f"\nCould not reach the node: {err.reason}") # [step-1] +const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY || + '0000000000000000000000000000000000000000000000000000000000000000'; +const signerKeyPair = new NemFacade.KeyPair( + new PrivateKey(SIGNER_PRIVATE_KEY)); + +const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS || + 'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4'; +// [step-2] +const MOSAIC_ID = process.env.MOSAIC_ID || 'company:token'; +const [MOSAIC_NAMESPACE, MOSAIC_NAME] = MOSAIC_ID.split(':'); +const QUANTITY = parseInt(process.env.QUANTITY || '100', 10); +console.log('Sending mosaic', MOSAIC_ID); +console.log(` Amount: ${QUANTITY} units`); +// [step-3] + const timePath = '/time-sync/network-time'; + console.log('Fetching current network time from', timePath); + const timeResponse = await fetch(`${NODE_URL}${timePath}`); + const timeJSON = await timeResponse.json(); + const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000); + console.log(' Network time:', networkTime, + 's since the nemesis block'); + + // Derived fields from network time + const timestamp = networkTime; + const deadline = networkTime + (2 * 60 * 60); + // [step-4] + const definitionPath = `/mosaic/definition?mosaicId=${MOSAIC_ID}`; + console.log('Fetching mosaic definition from', definitionPath); + const definitionResponse = + await fetch(`${NODE_URL}${definitionPath}`); + const definition = await definitionResponse.json(); + const properties = Object.fromEntries( + definition.properties.map( + property => [property.name, property.value])); + const divisibility = parseInt(properties.divisibility, 10); + + const supplyPath = `/mosaic/supply?mosaicId=${MOSAIC_ID}`; + console.log('Fetching mosaic supply from', supplyPath); + const supplyResponse = await fetch(`${NODE_URL}${supplyPath}`); + const { supply } = await supplyResponse.json(); + console.log(` ${MOSAIC_ID}: divisibility ${divisibility},`, + `supply ${supply}`); + // [step-5] + const atomicQuantity = QUANTITY * (10 ** divisibility); + const multiplier = 1; + const scaledMultiplier = multiplier * 1_000_000; + const transaction = facade.transactionFactory.create({ + type: 'transfer_transaction_v2', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp, + deadline, + recipientAddress: RECIPIENT_ADDRESS, + amount: BigInt(scaledMultiplier), + mosaics: [{ + mosaic: { + mosaicId: { + namespaceId: { + name: MOSAIC_NAMESPACE + }, + name: MOSAIC_NAME + }, + amount: BigInt(atomicQuantity) + } + }] + }); + // [step-6] + const fee = calculateTransactionFee(transaction, { + [MOSAIC_ID]: { supply: BigInt(supply), divisibility } + }); + transaction.fee = new models.Amount(fee); + console.log(` Transaction fee: ${Number(fee) / 1_000_000} XEM`); + // [step-7] + const signature = facade.signTransaction(signerKeyPair, transaction); + const jsonPayload = facade.transactionFactory.static.attachSignature( + transaction, signature); + console.log('Built transaction:'); + console.dir(transaction.toJson(), { colors: true, depth: null }); + // [step-8] + const announcePath = '/transaction/announce'; + console.log('Announcing transaction to', announcePath); + const announceResponse = await fetch(`${NODE_URL}${announcePath}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: jsonPayload + }); + const announceResult = await announceResponse.json(); + console.log(' Result:', announceResult.message); + // [step-9] + if ('SUCCESS' === announceResult.message) { + const transactionHash = facade.hashTransaction(transaction) + .toString(); + const statusPath = `/transaction/get?hash=${transactionHash}`; + console.log('Waiting for confirmation from', statusPath); + + let isConfirmed = false; + for (let attempt = 1; 120 >= attempt; ++attempt) { + const response = await fetch(`${NODE_URL}${statusPath}`); + + if (response.ok) { + const confirmed = await response.json(); + console.log('Transaction confirmed in block', + confirmed.meta.height); + isConfirmed = true; + break; + } + console.log(' Transaction status: pending'); + await new Promise(resolve => { setTimeout(resolve, 1000); }); + } + if (!isConfirmed) + console.warn('Confirmation took too long.'); + } else { + console.log('Transaction rejected:', announceResult.message); + } + // [step-1] +SIGNER_PRIVATE_KEY = os.getenv( + 'SIGNER_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000000') +signer_key_pair = NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY)) + +RECIPIENT_ADDRESS = os.getenv( + 'RECIPIENT_ADDRESS', + 'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4') +# [step-2] +MOSAIC_ID = os.getenv('MOSAIC_ID', 'company:token') +MOSAIC_NAMESPACE, MOSAIC_NAME = MOSAIC_ID.split(':') +QUANTITY = int(os.getenv('QUANTITY', '100')) +print(f'Sending mosaic {MOSAIC_ID}') +print(f' Amount: {QUANTITY} units') +# [step-3] + time_path = '/time-sync/network-time' + print(f'Fetching current network time from {time_path}') + with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response: + response_json = json.loads(response.read().decode()) + network_time = response_json['receiveTimeStamp'] // 1000 + print(f' Network time: {network_time} s since the nemesis block') + + # Derived fields from network time + timestamp = network_time + deadline = network_time + 2 * 60 * 60 + # [step-4] + definition_path = f'/mosaic/definition?mosaicId={MOSAIC_ID}' + print(f'Fetching mosaic definition from {definition_path}') + with urllib.request.urlopen( + f'{NODE_URL}{definition_path}') as response: + definition = json.loads(response.read().decode()) + properties = { + prop['name']: prop['value'] + for prop in definition['properties'] + } + divisibility = int(properties['divisibility']) + + supply_path = f'/mosaic/supply?mosaicId={MOSAIC_ID}' + print(f'Fetching mosaic supply from {supply_path}') + with urllib.request.urlopen(f'{NODE_URL}{supply_path}') as response: + supply = json.loads(response.read().decode())['supply'] + print(f' {MOSAIC_ID}: divisibility {divisibility}, supply {supply}') + # [step-5] + atomic_quantity = QUANTITY * (10 ** divisibility) + multiplier = 1 + scaled_multiplier = multiplier * 1_000_000 + transaction = facade.transaction_factory.create({ + 'type': 'transfer_transaction_v2', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp, + 'deadline': deadline, + 'recipient_address': RECIPIENT_ADDRESS, + 'amount': scaled_multiplier, + 'mosaics': [{ + 'mosaic': { + 'mosaic_id': { + 'namespace_id': {'name': MOSAIC_NAMESPACE}, + 'name': MOSAIC_NAME + }, + 'amount': atomic_quantity + } + }] + }) + # [step-6] + fee = calculate_transaction_fee( + transaction, + {MOSAIC_ID: {'supply': supply, 'divisibility': divisibility}}) + transaction.fee = Amount(fee) + print(f' Transaction fee: {fee / 1_000_000} XEM') + # [step-7] + signature = facade.sign_transaction(signer_key_pair, transaction) + json_payload = facade.transaction_factory.attach_signature( + transaction, signature) + print('Built transaction:') + print(json.dumps(transaction.to_json(), indent=2)) + # [step-8] + announce_path = '/transaction/announce' + print(f'Announcing transaction to {announce_path}') + announce_request = urllib.request.Request( + f'{NODE_URL}{announce_path}', + data=json_payload.encode(), + headers={'Content-Type': 'application/json'}, + method='POST' + ) + with urllib.request.urlopen(announce_request) as response: + announce_result = json.loads(response.read().decode()) + print(f' Result: {announce_result['message']}') + # [step-9] + if 'SUCCESS' == announce_result['message']: + status_path = ( + f'/transaction/get?hash={ + facade.hash_transaction(transaction)}') + print(f'Waiting for confirmation from {status_path}') + is_confirmed = False + for attempt in range(120): + try: + with urllib.request.urlopen( + f'{NODE_URL}{status_path}' + ) as response: + confirmed = json.loads(response.read().decode()) + height = confirmed['meta']['height'] + print(f'Transaction confirmed in block {height}') + is_confirmed = True + break + except urllib.error.HTTPError: + print(' Transaction status: pending') + time.sleep(1) + if not is_confirmed: + print('Confirmation took too long.') + else: + print(f'Transaction rejected: {announce_result['message']}') + # [step-1] +const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY || + '0000000000000000000000000000000000000000000000000000000000000000'; +const signerKeyPair = new NemFacade.KeyPair( + new PrivateKey(SIGNER_PRIVATE_KEY)); + +const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS || + 'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4'; +// [step-2] +const xem = parseFloat(process.env.XEM_AMOUNT || '1'); +const amount = BigInt(Math.round(xem * 1_000_000)); +// [step-3] + const timePath = '/time-sync/network-time'; + console.log('Fetching current network time from', timePath); + const timeResponse = await fetch(`${NODE_URL}${timePath}`); + const timeJSON = await timeResponse.json(); + const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000); + console.log(' Network time:', networkTime, + 's since the nemesis block'); + + // Derived fields from network time + const timestamp = networkTime; + const deadline = networkTime + (2 * 60 * 60); + // [step-4] + const transaction = facade.transactionFactory.create({ + type: 'transfer_transaction_v2', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp, + deadline, + recipientAddress: RECIPIENT_ADDRESS, + amount + }); + // [step-5] + const fee = calculateTransactionFee(transaction); + transaction.fee = new models.Amount(fee); + console.log(` Transaction fee: ${Number(fee) / 1_000_000} XEM`); + // [step-6] + const signature = facade.signTransaction(signerKeyPair, transaction); + const jsonPayload = facade.transactionFactory.static.attachSignature( + transaction, signature); + console.log('Built transaction:'); + console.dir(transaction.toJson(), { colors: true }); + // [step-7] + const announcePath = '/transaction/announce'; + console.log('Announcing transaction to', announcePath); + const announceResponse = await fetch(`${NODE_URL}${announcePath}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: jsonPayload + }); + const announceResult = await announceResponse.json(); + console.log(' Result:', announceResult.message); + // [step-8] + if ('SUCCESS' === announceResult.message) { + const transactionHash = facade.hashTransaction(transaction) + .toString(); + const statusPath = `/transaction/get?hash=${transactionHash}`; + console.log('Waiting for confirmation from', statusPath); + + let isConfirmed = false; + for (let attempt = 1; 120 >= attempt; ++attempt) { + const response = await fetch(`${NODE_URL}${statusPath}`); + + if (response.ok) { + const confirmed = await response.json(); + console.log('Transaction confirmed in block', + confirmed.meta.height); + isConfirmed = true; + break; + } + console.log(' Transaction status: pending'); + await new Promise(resolve => { setTimeout(resolve, 1000); }); + } + if (!isConfirmed) + console.warn('Confirmation took too long.'); + } else { + console.log('Transaction rejected:', announceResult.message); + } + // [step-1] +SIGNER_PRIVATE_KEY = os.getenv( + 'SIGNER_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000000') +signer_key_pair = NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY)) + +RECIPIENT_ADDRESS = os.getenv( + 'RECIPIENT_ADDRESS', + 'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4') +# [step-2] +xem = float(os.getenv('XEM_AMOUNT', '1')) +amount = round(xem * 1_000_000) +# [step-3] + time_path = '/time-sync/network-time' + print(f'Fetching current network time from {time_path}') + with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response: + response_json = json.loads(response.read().decode()) + network_time = response_json['receiveTimeStamp'] // 1000 + print(f' Network time: {network_time} s since the nemesis block') + + # Derived fields from network time + timestamp = network_time + deadline = network_time + 2 * 60 * 60 + # [step-4] + transaction = facade.transaction_factory.create({ + 'type': 'transfer_transaction_v2', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp, + 'deadline': deadline, + 'recipient_address': RECIPIENT_ADDRESS, + 'amount': amount + }) + # [step-5] + fee = calculate_transaction_fee(transaction) + transaction.fee = Amount(fee) + print(f' Transaction fee: {fee / 1_000_000} XEM') + # [step-6] + signature = facade.sign_transaction(signer_key_pair, transaction) + json_payload = facade.transaction_factory.attach_signature( + transaction, signature) + print('Built transaction:') + print(json.dumps(transaction.to_json(), indent=2)) + # [step-7] + announce_path = '/transaction/announce' + print(f'Announcing transaction to {announce_path}') + announce_request = urllib.request.Request( + f'{NODE_URL}{announce_path}', + data=json_payload.encode(), + headers={'Content-Type': 'application/json'}, + method='POST' + ) + with urllib.request.urlopen(announce_request) as response: + announce_result = json.loads(response.read().decode()) + print(f' Result: {announce_result['message']}') + # [step-8] + if 'SUCCESS' == announce_result['message']: + status_path = ( + f'/transaction/get?hash={ + facade.hash_transaction(transaction)}') + print(f'Waiting for confirmation from {status_path}') + is_confirmed = False + for attempt in range(120): + try: + with urllib.request.urlopen( + f'{NODE_URL}{status_path}' + ) as response: + confirmed = json.loads(response.read().decode()) + height = confirmed['meta']['height'] + print(f'Transaction confirmed in block {height}') + is_confirmed = True + break + except urllib.error.HTTPError: + print(' Transaction status: pending') + time.sleep(1) + if not is_confirmed: + print('Confirmation took too long.') + else: + print(f'Transaction rejected: {announce_result['message']}') + # [step-1] + // Build the transaction [>step-2] + const typedDescriptor = + new descriptors.TransferTransactionV2Descriptor( + new Address(RECIPIENT_ADDRESS), + new models.Amount(amount) + ); + // [step-3] + const transaction = facade.createTransactionFromTypedDescriptor( + typedDescriptor, signerKeyPair.publicKey, 0n, 2 * 60 * 60); + transaction.fee = new models.Amount( + calculateTransactionFee(transaction)); // [= attempt; ++attempt) { + const response = await fetch(`${NODE_URL}${statusPath}`); + + if (response.ok) { + const confirmed = await response.json(); + console.log('Transaction confirmed in block', + confirmed.meta.height); + isConfirmed = true; + break; + } + console.log(' Transaction status: pending'); + await new Promise(resolve => { setTimeout(resolve, 1000); }); + } + if (!isConfirmed) + console.warn('Confirmation took too long.'); + } else { + console.log('Transaction rejected:', announceResult.message); + } +} catch (e) { + console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown'); +} diff --git a/mkdocs/snippets/devbook/websockets/listen_new_blocks.log b/mkdocs/snippets/devbook/websockets/listen_new_blocks.log new file mode 100644 index 000000000..1d7b5b3b5 --- /dev/null +++ b/mkdocs/snippets/devbook/websockets/listen_new_blocks.log @@ -0,0 +1,9 @@ +Using node http://libertalia.nemtest.net:7890 +Connected to http://libertalia.nemtest.net:7778 +Subscribed to /blocks channel +New block: height=682,352 harvester=95BA0E864CD5EDB3... +New block: height=682,353 harvester=95BA0E864CD5EDB3... +New block: height=682,354 harvester=7C814B3B44B1A98B... +New block: height=682,355 harvester=95BA0E864CD5EDB3... +New block: height=682,356 harvester=95BA0E864CD5EDB3... +Unsubscribed and disconnected diff --git a/mkdocs/snippets/devbook/websockets/listen_new_blocks.mjs b/mkdocs/snippets/devbook/websockets/listen_new_blocks.mjs new file mode 100644 index 000000000..75d40b8db --- /dev/null +++ b/mkdocs/snippets/devbook/websockets/listen_new_blocks.mjs @@ -0,0 +1,42 @@ +import { Client } from '@stomp/stompjs'; +import SockJS from 'sockjs-client'; + +const NODE_URL = process.env.NODE_URL || + 'http://libertalia.nemtest.net:7890'; +const WS_URL = NODE_URL.replace(':7890', ':7778'); +console.log(`Using node ${NODE_URL}`); + +// Open connection [>step-1] +const client = new Client({ + webSocketFactory: () => new SockJS(`${WS_URL}/w/messages`) +}); +await new Promise(resolve => { + client.onConnect = resolve; + client.activate(); +}); +console.log(`Connected to ${WS_URL}`); +// [step-3] +function formatBlock(message) { + const block = JSON.parse(message.body); + console.log( + `New block: height=${block.height.toLocaleString()}` + + ` harvester=${block.signer.substring(0, 16).toUpperCase()}...` + ); +} +// [step-2] +const destination = '/blocks'; +const subscription = client.subscribe(destination, formatBlock, { + id: 'id-0' +}); +console.log(`Subscribed to ${destination} channel`); +// [step-4] +process.on('SIGINT', () => { + subscription.unsubscribe(); + client.deactivate(); + console.log('Unsubscribed and disconnected'); + process.exit(0); +}); +// [step-1] + async with connect(sockjs_url(f'{WS_URL}/w/messages')) as websocket: + await stomp_connect(websocket) + print(f'Connected to {WS_URL}') + # [step-2] + destination = '/blocks' + await stomp_subscribe(websocket, destination, 'id-0') + print(f'Subscribed to {destination} channel') + # [step-3] + try: + async for raw_frame in websocket: + for block in stomp_messages(raw_frame): + print( + f'New block: height={block["height"]:,}' + f' harvester={block["signer"][:16].upper()}...' + ) + # [step-4] + finally: + await stomp_unsubscribe(websocket, 'id-0') + await stomp_disconnect(websocket) + print('Unsubscribed and disconnected') + # [step-1] +const MONITOR_ADDRESS = process.env.MONITOR_ADDRESS || + 'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4'; +console.log(`Monitoring address: ${MONITOR_ADDRESS}`); + +const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY || + '0000000000000000000000000000000000000000000000000000000000000000'; +const facade = new NemFacade('testnet'); +const signerKeyPair = new NemFacade.KeyPair( + new PrivateKey(SIGNER_PRIVATE_KEY)); // [step-2] + const client = new Client({ + webSocketFactory: () => new SockJS(`${WS_URL}/w/messages`) + }); + await new Promise(resolve => { + client.onConnect = resolve; + client.activate(); + }); + console.log(`Connected to ${WS_URL}`); + // [step-3] + const destination = `/account/${MONITOR_ADDRESS}`; + await new Promise(resolve => { + client.subscribe(destination, () => { + client.unsubscribe('id-0'); + resolve(); + }, { id: 'id-0' }); + client.publish({ + destination: '/w/api/account/get', + body: JSON.stringify({ account: MONITOR_ADDRESS }) + }); + }); + console.log('Account registered'); + // [step-4] + const channels = { + [`/unconfirmed/${MONITOR_ADDRESS}`]: 'id-1', + [`/transactions/${MONITOR_ADDRESS}`]: 'id-2' + }; + // The message handler is set later, when waiting for confirmation + let handleMessage; + const onMessage = message => handleMessage?.(message); + for (const [channel, id] of Object.entries(channels)) { + client.subscribe(channel, onMessage, { id }); + console.log(`Subscribed to ${channel} channel`); + } + // [step-5] + const timeResponse = await fetch( + `${NODE_URL}/time-sync/network-time`); + const networkTime = Math.floor( + (await timeResponse.json()).receiveTimeStamp / 1000); + const transaction = facade.transactionFactory.create({ + type: 'transfer_transaction_v2', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp: networkTime, + deadline: networkTime + (2 * 60 * 60), + recipientAddress: MONITOR_ADDRESS, + amount: 0n + }); + transaction.fee = new models.Amount( + calculateTransactionFee(transaction)); + const signature = facade.signTransaction(signerKeyPair, transaction); + const jsonPayload = facade.transactionFactory.static.attachSignature( + transaction, signature); + const transactionHash = + facade.hashTransaction(transaction).toString().toUpperCase(); + // [step-6] + const shortHash = transactionHash.substring(0, 16); + const confirmed = new Promise(resolve => { + handleMessage = message => { + const pair = JSON.parse(message.body); + const messageHash = pair.meta.hash.data; + const messageShort = messageHash.substring(0, 16); + const status = message.headers.destination + .includes('/transactions/') ? 'confirmed' : 'unconfirmed'; + console.log(`${status}: hash=${messageShort}...`); + if ('confirmed' === status && + messageHash.toUpperCase() === transactionHash) { + console.log(`Transaction ${shortHash}... confirmed`); + resolve(); + } + }; + }); + console.log(`Announcing transaction ${shortHash}...`); + const response = await fetch(`${NODE_URL}/transaction/announce`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: jsonPayload + }); + const announceResult = await response.json(); + if ('SUCCESS' === announceResult.message) + await confirmed; + else + console.log(`Transaction rejected: ${announceResult.message}`); + // [step-7] + for (const id of Object.values(channels)) + client.unsubscribe(id); + console.log('Unsubscribed from all channels'); + client.deactivate(); // [step-1] +MONITOR_ADDRESS = os.getenv( + 'MONITOR_ADDRESS', + 'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4' +) +print(f'Monitoring address: {MONITOR_ADDRESS}') + +SIGNER_PRIVATE_KEY = os.getenv( + 'SIGNER_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000000' +) +facade = NemFacade('testnet') +signer_key_pair = NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY)) # [step-2] + endpoint = f'{WS_URL}/w/messages' + async with connect(sockjs_url(endpoint)) as websocket: + await stomp_connect(websocket) + print(f'Connected to {WS_URL}') + # [step-3] + destination = f'/account/{MONITOR_ADDRESS}' + await stomp_subscribe(websocket, destination, 'id-0') + await stomp_send(websocket, '/w/api/account/get', + json.dumps({'account': MONITOR_ADDRESS})) + async for raw_frame in websocket: + if any(f['headers']['destination'] == destination + for f in stomp_messages(raw_frame)): + break + await stomp_unsubscribe(websocket, 'id-0') + print('Account registered') + # [step-4] + channels = { + f'/unconfirmed/{MONITOR_ADDRESS}': 'id-1', + f'/transactions/{MONITOR_ADDRESS}': 'id-2', + } + for channel, sub_id in channels.items(): + await stomp_subscribe(websocket, channel, sub_id) + print(f'Subscribed to {channel} channel') + # [step-5] + with urllib.request.urlopen( + f'{NODE_URL}/time-sync/network-time' + ) as resp: + network_time = json.loads( + resp.read().decode())['receiveTimeStamp'] // 1000 + transaction = facade.transaction_factory.create({ + 'type': 'transfer_transaction_v2', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': network_time, + 'deadline': network_time + 2 * 60 * 60, + 'recipient_address': MONITOR_ADDRESS, + 'amount': 0, + }) + transaction.fee = Amount(calculate_transaction_fee(transaction)) + signature = facade.sign_transaction(signer_key_pair, transaction) + json_payload = facade.transaction_factory.attach_signature( + transaction, signature) + transaction_hash = str( + facade.hash_transaction(transaction)).upper() + # [step-6] + print(f'Announcing transaction {transaction_hash[:16]}...') + announce_request = urllib.request.Request( + f'{NODE_URL}/transaction/announce', + data=json_payload.encode(), + headers={'Content-Type': 'application/json'}, + method='POST' + ) + with urllib.request.urlopen(announce_request) as resp: + result = json.loads(resp.read().decode()) + + if 'SUCCESS' == result['message']: + confirmed = False + async for raw_frame in websocket: + for frame in stomp_messages(raw_frame): + destination = frame['headers']['destination'] + pair = json.loads(frame['body']) + message_hash = pair['meta']['hash']['data'] + status = ( + 'confirmed' if '/transactions/' in destination + else 'unconfirmed') + print(f'{status}: hash={message_hash[:16]}...') + is_match = message_hash.upper() == transaction_hash + if status == 'confirmed' and is_match: + short_hash = transaction_hash[:16] + print(f'Transaction {short_hash}... confirmed') + confirmed = True + if confirmed: + break + else: + print(f'Transaction rejected: {result["message"]}') + # [step-7] + for sub_id in channels.values(): + await stomp_unsubscribe(websocket, sub_id) + print('Unsubscribed from all channels') + await stomp_disconnect(websocket) # [