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..252622352 --- /dev/null +++ b/mkdocs/config/mkdocs.base.yml @@ -0,0 +1,176 @@ +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 + pygments_lang_class: true + 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://docs.nemtest.net + 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..189134f11 --- /dev/null +++ b/mkdocs/config/mkdocs.en.yml @@ -0,0 +1,140 @@ +INHERIT: ./mkdocs.base.yml + +site_name: NEM Docs WIP 🚧 +site_url: https://docs.nemtest.net/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://docs.nemtest.net + 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/mountain + link: https://nemproject.github.io/nem-docs + name: Visit the legacy documentation site +nav: + - index.md + - User Manual: + - userbook/intro.md + - Node Operation: + - userbook/node/install.md + - userbook/node/supernode-program.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 + - devbook/accounts/configure-multisig.md + - Transactions: + - devbook/transactions/transfer-xem.md + - devbook/transactions/transfer-mosaics.md + - devbook/transactions/messages.md + - devbook/transactions/monitoring-status.md + - devbook/transactions/typed-descriptors.md + - devbook/transactions/sign-multisig.md + - Mosaics: + - devbook/mosaics/create-mosaic.md + - devbook/mosaics/mosaic-levy.md + - devbook/mosaics/get-mosaic-info.md + - devbook/mosaics/change-mosaic-supply.md + - devbook/mosaics/modify-mosaic-definition.md + - Namespaces: + - devbook/namespaces/register-root-namespace.md + - devbook/namespaces/register-subnamespace.md + - devbook/namespaces/extend-root-namespace.md + - 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 + - devbook/websockets/listen-multisig-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 + - devbook/reference/whitepaper/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..1128b9e7e --- /dev/null +++ b/mkdocs/config/mkdocs.ja.yml @@ -0,0 +1,89 @@ +INHERIT: ./mkdocs.base.yml + +site_name: NEMドキュメント WIP ⚠ +site_url: https://docs.nemtest.net/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://docs.nemtest.net + 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/mountain + 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 + - devbook/reference/whitepaper/index.md + - 教科書: + - 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/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..023eaba30 --- /dev/null +++ b/mkdocs/lexers/cats_lexer.py @@ -0,0 +1,47 @@ +from pygments.lexer import RegexLexer, words +from pygments.token import * + +__all__ = ['CATSLexer'] + + +class CATSLexer(RegexLexer): + name = 'CATS' + aliases = ['cats'] + filenames = ['*.cats'] + + tokens = { + 'root': [ + # Whitespace and non-code literals. + (r'\s+', Text), + (r'#[^\n]*(?:\r?\n[\t ]*#[^\n]*)*', Comment.Multiline), + (r'"(?:\\.|[^"\\\n])*"', String.Double), + (r'<[^<>\n]+>', Name.Tag), + (r'\[[^\n]+\]', Name.Tag), + + # Grammar keywords and built-in constructors. + (words(('import', 'using', 'enum', 'struct', 'if'), prefix=r'\b', suffix=r'\b'), Keyword), + (words(('abstract', 'inline'), prefix=r'\b', suffix=r'\b'), Keyword.Declaration), + (words(('make_const', 'make_reserved', 'binary_fixed', 'sizeof', 'array'), prefix=r'\b', suffix=r'\b'), Name.Builtin), + + # Conditional and attribute argument operators. + (r'\bnot\s+(?:equals|in)\b', Operator.Word), + (words(('equals', 'in', 'not'), prefix=r'\b', suffix=r'\b'), Operator.Word), + + # Attributes, placeholders and transforms. + (r'@(is_bitwise|is_byte_constrained|alignment|sort_key|sizeref|is_aligned|is_size_implicit|size|initializes|discriminator|comparer)\b', Name.Decorator), + (words(('pad_last', '__FILL__', '__value__'), prefix=r'\b', suffix=r'\b'), Keyword.Pseudo), + (r'\bripemd_keccak_256\b', Name.Function), + + # Primitive values. + (r'\bu?int(?:8|16|32|64)\b', Keyword.Type), + (r'\b0x[A-F0-9]+\b', Number.Hex), + (r'\b\d+\b', Number.Integer), + + # CATS name classes. + (r'\b[A-Z][A-Z0-9_]+\b', Name.Constant), + (r'\b[A-Z][a-z][A-Za-z0-9]*\b', Name.Class), + (r'\b[a-z][a-z0-9_]*\b', Name.Variable), + (r'[=():,!<>]', Punctuation), + (r'[A-Za-z_][A-Za-z0-9_]*', Name), + ], + } 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..e75cacccc --- /dev/null +++ b/mkdocs/overrides/404.html @@ -0,0 +1,22 @@ + + + + + + + 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..00cdd2d23 --- /dev/null +++ b/mkdocs/overrides/assets/stylesheets/extra.css @@ -0,0 +1,890 @@ +@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; +} + +/* Colors for zoomed-in images */ +.gslide-desc a { + color: var(--md-typeset-a-color); +} + +.gslide-desc a:hover { + color: var(--md-accent-fg-color); +} + +.gslide-desc code { + background: var(--md-code-bg-color); +} + +/* 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); +} + +/* CATS documentation placeholders, such as . */ +.md-typeset .language-cats .nt { + font-style: italic; + color: var(--md-code-hl-generic-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..98ea23b66 --- /dev/null +++ b/mkdocs/package-lock.json @@ -0,0 +1,4592 @@ +{ + "name": "nem-docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nem-docs", + "dependencies": { + "@stomp/stompjs": "^7.3.0", + "sockjs-client": "^1.6.1", + "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/@stomp/stompjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.3.0.tgz", + "integrity": "sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==", + "license": "Apache-2.0" + }, + "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/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.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/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "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==", + "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/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "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/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "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/sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://tidelift.com/funding/github/npm/sockjs-client" + } + }, + "node_modules/sockjs-client/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "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/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.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/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "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..12b928d7c --- /dev/null +++ b/mkdocs/package.json @@ -0,0 +1,19 @@ +{ + "name": "nem-docs", + "description": "NEM Docs", + "scripts": { + "lint": "eslint snippets --ext .mjs" + }, + "dependencies": { + "@stomp/stompjs": "^7.3.0", + "sockjs-client": "^1.6.1", + "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/configure-multisig.md b/mkdocs/pages/en/devbook/accounts/configure-multisig.md new file mode 100644 index 000000000..da1d0a3c8 --- /dev/null +++ b/mkdocs/pages/en/devbook/accounts/configure-multisig.md @@ -0,0 +1,305 @@ +--- +title: Configure a Multisig +tutorial_level: advanced +--- + +# Configuring a Multisignature Account + +A , also called _multisig_, cannot initiate transactions on its own. +Instead, it relies on _cosignatory_ accounts to create transactions and sign them on its behalf. + +This tutorial shows how to convert a regular account into a multisig account that requires approval from one of two +cosignatories. +If the account is already multisig, the tutorial instead demonstrates how to remove the cosignatories and revert the +account to a regular account. + +The multisignature structure used in this tutorial is shown below: + +```dot +digraph "Multisignature Tree" { + rankdir="BT"; + node [fontsize=12]; + "Multisignature Account"; + "Cosignatory 0"; + "Cosignatory 1"; + + "Cosignatory 0" -> "Multisignature Account"; + "Cosignatory 1" -> "Multisignature Account"; +} +``` + +## Prerequisites + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). +* Create 3 : one to turn into a multisig, and the other two to act as cosignatories. + You can do this either [from code](./create-from-private-key.md) or + [by using a wallet](../../userbook/wallet/create-account.md). +* Obtain for the account being converted into a multisig to pay for the transaction fees. + See [Getting Testnet Funds from the Faucet](./testnet-faucet.md). + +Additionally, review the [Transfer XEM](../transactions/transfer-xem.md) tutorial to understand how transactions are +announced and confirmed. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/accounts/configure_multisig', ['py', 'js']) }} + +## Code Explanation + +The code defines two helper functions, for announcing a transaction and waiting for its confirmation. +For details on how these work, see the [Transfer XEM](../transactions/transfer-xem.md) tutorial. +The remaining helper functions are described in the sections below. + +The tutorial then proceeds to [set up the required keys](#setting-up-the-accounts), +[fetch the current network time](#fetching-network-time), and +[detect the current configuration](#determining-the-multisig-operation) of the multisig account. + +Depending on whether the account is already configured as a multisig, +transactions are created to [enable](#enabling-the-multisig) or [disable](#disabling-the-multisig) it as appropriate. +Finally, the transactions are [announced and confirmed](#submitting-the-transactions). + +### Setting Up the Accounts + +{{ tutorial.code_snippet_tagged('step-1') }} + +The tutorial requires three separate accounts. +Their can be provided through environment variables. +If not set, default values are used: + +| Environment Variable | Default value | Purpose | +|----------------------------|---------------|----------------------------| +| `MULTISIG_PRIVATE_KEY` | `0000..0001` | Multisig account | +| `COSIGNATORY0_PRIVATE_KEY` | `0000..0002` | First cosignatory account | +| `COSIGNATORY1_PRIVATE_KEY` | `0000..0003` | Second cosignatory account | + +Each private key is a 64-character hexadecimal string. + +The multisig account must hold enough funds to pay the transaction fees. +If the default values are used, this account may already be funded. + +The snippet above derives and stores the and of each account for later use. + +### Fetching Network Time + +{{ tutorial.code_snippet_tagged('step-2') }} + +Network time is fetched from , and the transactions' `timestamp` and `deadline` fields +are derived from it, following the process described in the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +### Determining the Multisig Operation + +{{ tutorial.code_snippet_tagged('step-3') }} + +This helper retrieves the list of current cosignatories for a given address using the endpoint. +If it returns an empty list, the account is not currently configured as a multisig account. + +!!! warning "Check the existing multisig configuration" + + For simplicity, the tutorial assumes that if the list of cosignatories is _not_ empty, then the account is a + multisig configured by the tutorial itself. + + If the configuration is not the expected one, for example, because the cosignatories are different, + the removal transactions will be rejected. + + Applications should always check the current configuration before trying to modify it, including the full list of + cosignatories and the minimum number of signatures required. + +{{ tutorial.code_snippet_tagged('step-4') }} + +The returned cosignatories determine whether the account is configured as a multisig account, and therefore whether to +create the transactions to enable or disable multisig. + +The functions that build them and the delta values they use are described in the next two sections. + +### Enabling the Multisig + +{{ tutorial.code_snippet_tagged('step-5') }} + +All changes to the multisig configuration of an account, including adding or removing cosignatories, +are performed using a . + +The transaction specifies: + +* {{ tutorial.var('type') }}: Multisig configuration changes use the type + . + +* {{ tutorial.var('signer_public_key') }}: of the account whose multisig configuration will be modified. + +* {{ tutorial.var('timestamp') }} and {{ tutorial.var('deadline') }}: The values computed in the network time step. + +* {{ tutorial.var('min_approval_delta') }}: difference between the _desired value_ and the _current value_ of the + number of cosignatures required to approve transactions from the multisig account. + + In this case, the account is initially a regular account, so the current number of required cosignatures is `0`. + To convert it into a multisig account that requires one signature from one of its cosignatories, + the delta is set to `1`. + + The delta value can be negative to _reduce_ the current value, as shown in the next section. + +* {{ tutorial.var('modifications') }}: list of changes to the account's cosignatories. + Each modification adds or removes one cosignatory, identified by its . + + In this case, two `add_cosignatory` modifications add the cosignatories prepared during the + [setup phase](#setting-up-the-accounts). + +!!! note "Safety measures" + + The protocol includes safety mechanisms that help prevent locking an account into an invalid state. + Transactions that would result in an invalid multisig configuration are rejected with an error. + For example, when: + + * The number of cosignatories is lower than the number of required cosignatures + * An account that is already a cosignatory is added + * An account that is not a cosignatory is removed + * More than one cosignatory is removed in a single transaction + * A multisig account is added as a cosignatory + +{{ tutorial.code_snippet_tagged('step-6') }} + +The transaction fee is calculated with and attached to the transaction. +Multisig account modification transactions pay a fixed transaction fee of 0.5 XEM, as shown in the +[fee schedule](../../textbook/transactions.md#fee-schedule). + +{{ tutorial.code_snippet_tagged('step-7') }} + +Finally, the transaction is signed. +In this case, only the signature of the account being converted into a multisig is required. +The cosignatories do not sign the conversion transaction. + +!!! info "From now on, cosignatories must initiate transactions" + + Once an account has multisig enabled, its own signature is no longer accepted. + Any transaction sent from that account, such as a transfer or a further multisig modification, + must instead be initiated and signed by its cosignatories, as shown in the next section. + +### Disabling the Multisig + +Disabling a multisig configuration requires removing all cosignatories. +The process is similar to enabling it, with two key differences: +cosignatories must be removed one by one, and the multisig account itself cannot sign the transactions. + +{{ tutorial.code_snippet_tagged('step-8') }} + +This helper builds a that removes a cosignatory. +It takes the cosignatory to remove and the approval delta to apply as parameters. +{{ tutorial.var('signer_public_key') }} is set to the multisig account's public key because its configuration is being +modified. + +As shown in [Determining the Multisig Operation](#determining-the-multisig-operation), the helper is called twice. + +The first call removes {{ tutorial.var('cosignatory_key_pairs[1]') }} with an approval delta of `0`, because one +cosignatory still remains. + +The second removes the remaining cosignatory with an approval delta of `-1`, reducing the approval requirement from `1` +back to `0`. + +{{ tutorial.code_snippet_tagged('step-9') }} + +Since a multisig account cannot sign transactions on its own, each modification is wrapped in a +. + +The inner modification transaction is converted with so it can be +embedded in the wrapping multisig transaction. + +{{ tutorial.code_snippet_tagged('step-10') }} + +Both the inner transaction and the wrapper pay a transaction fee: 0.5 XEM for the modification and 0.15 XEM for the +multisig wrapper, as shown in the [fee schedule](../../textbook/transactions.md#fee-schedule). +Both fees are deducted from the multisig account. +Cosignatories never pay fees for the transactions they initiate on behalf of a multisig. + +{{ tutorial.code_snippet_tagged('step-11') }} + +Finally, each multisig transaction is signed by the cosignatory that initiates it, the one set as the wrapper's +{{ tutorial.var('signer_public_key') }}. +Here, both removals are initiated and signed by {{ tutorial.var('cosignatory_key_pairs[0]') }}. + +In this case, a single signature is enough because this multisig requires only one cosignature. +In stricter configurations, a removal requires approval from additional cosignatories, like any other transaction, +although the signature of the cosignatory being removed never counts toward the requirement. + +The removal of the last remaining cosignatory is a special case. +Only cosignatories can sign transactions on behalf of the multisig account, so the last cosignatory signs its own +removal, as the second transaction in this tutorial shows. + +The cosignatories could also have been removed in the opposite order. +The only difference would be which cosignatory initiates and signs each transaction. + +!!! note "Disabling other configurations" + + If the removal transactions are rejected, the account may have been configured differently from this tutorial's + default, such as a **2-of-2** multisig instead of a **1-of-2**. + + Check the number of required cosignatures in the `minCosignatories` field returned by , and + adjust the removal transactions as needed. + + For example, to disable a **2-of-2** multisig: + + 1. Remove Cosignatory 1 with `min_approval_delta` set to `-1`, + because the account cannot keep requiring two cosignatures once a single cosignatory remains. + 2. Once confirmed, remove Cosignatory 0 with `min_approval_delta` set to `-1`. + + In this case, Cosignatory 0 can still sign both removals, since the signature of the cosignatory being removed + never counts toward the required approvals. + +### Submitting the Transactions + +{{ tutorial.code_snippet_tagged('step-12') }} + +The final step is to announce the transactions and wait for their confirmation, as described in the +[Transfer XEM](../transactions/transfer-xem.md) tutorial. + +When disabling the multisig, the two multisig transactions are announced sequentially. +The code waits for the first transaction to be confirmed before announcing the second one, because the second removal is +only valid once the first one has been processed. + +## Output + +The output shown below corresponds to two typical runs of the program. + +=== ":material-plus-thick: Enabling the Multisig" + + ```text linenums="1" hl_lines="2-4 8 24 30 34" + --8<-- 'devbook/accounts/configure_multisig_enable.log' + ``` + + Key points in the output: + + * **Lines 2-4**: Addresses and public keys of all involved accounts. + * **Line 8** (`Response: No cosignatories`): No cosignatories are currently configured. + * **Lines 24 and 30** (`cosignatory_public_key`): Public keys of the cosignatories that will be added. + * **Line 34** (`"min_approval_delta": 1`): The number of required cosignatures will be increased by one. + +=== ":material-minus-thick: Disabling the Multisig" + + ```text linenums="1" hl_lines="2-4 8 29-37 61-69" + --8<-- 'devbook/accounts/configure_multisig_disable.log' + ``` + + Key points in the output: + + * **Lines 2-4**: Addresses and public keys of all involved accounts. + * **Line 8** (`Response: [ ... ]`): Existing cosignatories have been detected. + * **Lines 29-37** (First multisig transaction): The number of required cosignatures will remain unchanged and one + existing cosignatory will be removed. + * **Lines 61-69** (Second multisig transaction): The number of required cosignatures will be decreased by one and + the last remaining cosignatory will be removed. + +The transaction hashes shown in the output can be used to look up the transactions in the +[NEM testnet explorer](https://testnet.nem.fyi/). + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +|------------------------------------------------------------------------------------|------------------------------------------------| +| [Retrieve the current multisig configuration](#determining-the-multisig-operation) | | +| [Enable a multisig account](#enabling-the-multisig) | | +| [Disable a multisig account](#disabling-the-multisig) | | +| Wrap a modification in a multisig transaction | | 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..2107b6c52 --- /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 about 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/change-mosaic-supply.md b/mkdocs/pages/en/devbook/mosaics/change-mosaic-supply.md new file mode 100644 index 000000000..93b02484a --- /dev/null +++ b/mkdocs/pages/en/devbook/mosaics/change-mosaic-supply.md @@ -0,0 +1,178 @@ +--- +title: Change Mosaic Supply +tutorial_level: intermediate +--- + +# Changing Mosaic Supply + + created with a [mutable supply](../../textbook/mosaics.md#supply-mutability) can have their total +supply increased or decreased after creation. + +Only the mosaic creator can change the supply. +Supply changes affect only the creator's balance: minted units are added to it, and burned units are removed from it. +The balances of all other accounts that hold the mosaic remain unchanged. + +This tutorial shows how to change a mosaic's supply by minting and burning units. + +## Prerequisites + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). +* Create a mosaic with a [mutable supply](../../textbook/mosaics.md#supply-mutability), + using the that will sign the supply changes. + See the [Creating a Mosaic](./create-mosaic.md) tutorial. +* Keep the that holds the mosaic active. + See [Lifetime](../../textbook/mosaics.md#lifetime) in the Textbook. +* Obtain to pay for the transaction fee. + 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/mosaics/change_mosaic_supply', ['py', 'js']) }} + +## Code Explanation + +Changing a mosaic's supply uses the transaction. +This tutorial announces two of them: one to mint new units and one to burn them. + +Because both transactions are submitted the same way, the snippet defines two helpers, +{{ tutorial.var('announce_transaction') }} and {{ tutorial.var('wait_for_confirmation') }}, which announce a transaction +and then poll the network until it is included in a block. + +A third helper, {{ tutorial.var('fetch_supply') }}, reads the mosaic's current supply from , so +that the effect of each transaction can be observed. + +### Setting Up the Account and the Mosaic + +{{ tutorial.code_snippet_tagged('step-1') }} + +The snippet reads the signer's private key from the `SIGNER_PRIVATE_KEY` environment variable, which defaults to a test +key if not set. +The signer must be the creator of the mosaic. + +The mosaic to update is read from the `NAMESPACE` and `MOSAIC` environment variables, which default to +`my_namespace:token`. + +!!! warning "Use a mosaic created by the signer" + + By default, the code uses the test account referenced by `SIGNER_PRIVATE_KEY` and a mosaic named + `my_namespace:token`. + + If you come from the [Creating a Mosaic](./create-mosaic.md) tutorial, set the `SIGNER_PRIVATE_KEY`, `NAMESPACE`, + and `MOSAIC` environment variables to match the account and mosaic you created there, or any other mosaic that the + signer owns. + +### Fetching Network Time + +{{ tutorial.code_snippet_tagged('step-2') }} + +Network time is fetched from , and the transaction's `timestamp` and `deadline` fields +are derived from it, following the process described in the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +### Increasing Supply (Minting) + +{{ tutorial.code_snippet_tagged('step-3') }} + +The snippet first reads the mosaic's supply, so that the newly minted units can be seen once the transaction is +confirmed. + +To mint new units, the transaction sets: + +* {{ tutorial.var('type') }}: Mosaic supply change transactions use the type . + +* {{ tutorial.var('signer_public_key') }}: The account that signs the transaction and pays the fees. + It must be the creator of the mosaic. + +* {{ tutorial.var('timestamp') }} and {{ tutorial.var('deadline') }}: The values computed in the network time step. + +* {{ tutorial.var('mosaic_id') }}: The [fully qualified name](../../textbook/mosaics.md#fully-qualified-name) of the + mosaic to update. + +* {{ tutorial.var('action') }}: The value `increase` mints new units. + +* {{ tutorial.var('delta') }}: The number of + [whole units](../../textbook/mosaics.md#divisibility) to add. + The resulting total supply cannot exceed the [maximum supply](../../textbook/mosaics.md#initial-supply). + + !!! note "The cap is expressed in atomic units" + + The maximum supply is fixed at $9 \cdot 10^{15}$ **atomic** units, while {{ tutorial.var('delta') }} is + expressed in **whole** units. + + The maximum value of {{ tutorial.var('delta') }} therefore depends on the mosaic's + [divisibility](../../textbook/mosaics.md#divisibility): + + \[ + \text{max\_whole\_units} = \frac{9 \cdot 10^{15}}{10^{\text{divisibility}}} + \] + + The mosaic in this tutorial has a divisibility of `2`, so one whole unit corresponds to $100$ atomic units and + the supply can grow up to $9 \cdot 10^{13}$ whole units. + +The transaction fee is then calculated and the transaction is signed, announced, and confirmed, following the same +process as in the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +Mosaic supply change transactions pay a fixed transaction fee of 0.15 XEM, as shown in the +[fee schedule](../../textbook/transactions.md#fee-schedule). + +Once confirmed, the supply is read again to show the resulting supply. +The minted units are credited to the creator's account. + +### Decreasing Supply (Burning) + +{{ tutorial.code_snippet_tagged('step-4') }} + +To burn existing units, the same transaction type is used with {{ tutorial.var('action') }} set to `decrease` and +{{ tutorial.var('delta') }} set to the number of whole units to remove. + +The burned units are taken from the creator's account, so only units the creator still holds can be burned. +Units already distributed to other accounts remain in their balances, and the transaction fails if the creator's own +balance does not cover {{ tutorial.var('delta') }}. + +Once confirmed, the supply is read again to show the burned units. +Because this tutorial increases and then decreases the supply by the same amount, the final supply matches the value +before the changes. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text linenums="1" hl_lines="8 25-26 35 54-55 64" +--8<-- 'devbook/mosaics/change_mosaic_supply.log' +``` + +Some highlights from the output: + +* **Supply before minting** (line 8): The mosaic starts with a supply of `1000` whole units. + +* **Supply increase** (lines 25-26): The `increase` action with a delta of `500` mints new units into the creator's + balance. + +* **Supply after minting** (line 35): The supply rises to `1500` whole units. + +* **Supply decrease** (lines 54-55): The `decrease` action with the same delta burns those units. + +* **Supply after burning** (line 64): The supply returns to `1000`, because the increase and decrease cancel out. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ------------------------------------------------------------- | ------------------------------------------------------------------------- | +| [Mint mosaic supply](#increasing-supply-minting) | , | +| [Burn mosaic supply](#decreasing-supply-burning) | , | +| [Calculate the transaction fee](#increasing-supply-minting) | | +| [Read the mosaic supply](#increasing-supply-minting) | | + +## Next Steps + +Now that you can change a mosaic's supply, you can: + +* [Send your mosaic with a transfer transaction](../transactions/transfer-mosaics.md) to distribute it to other + accounts +* [Get mosaic information](./get-mosaic-info.md) to inspect the properties and supply of any mosaic diff --git a/mkdocs/pages/en/devbook/mosaics/create-mosaic.md b/mkdocs/pages/en/devbook/mosaics/create-mosaic.md new file mode 100644 index 000000000..d9a66d188 --- /dev/null +++ b/mkdocs/pages/en/devbook/mosaics/create-mosaic.md @@ -0,0 +1,224 @@ +--- +title: Create Mosaic +tutorial_level: intermediate +--- + +# Creating a Mosaic + + represent assets on the NEM blockchain, such as currencies, collectibles, or access rights. +Unlike tokens on other platforms, NEM mosaics are supported directly at the protocol level +and require no additional coding to use. + +Their properties are configurable to support various use cases, from simple currencies to tokens with custom supply +and transfer rules. + +Every mosaic belongs to a registered , which provides the first half of its +[fully qualified name](../../textbook/mosaics.md#fully-qualified-name), such as `my_namespace:token`. +A namespace must therefore be registered before a mosaic can be created. + +This tutorial shows how to create a mosaic under an existing namespace and configure its initial properties. + +## Prerequisites + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). +* Create an to own the mosaic, either + [from code](../accounts/create-from-private-key.md) or + [by using a wallet](../../userbook/wallet/create-account.md). +* Register a to hold the mosaic. + See [Registering a Root Namespace](../namespaces/register-root-namespace.md). +* Obtain to pay for the transaction and creation fees. + See [Getting Testnet Funds from the Faucet](../accounts/testnet-faucet.md). + +Additionally, review the [Transfer XEM](../transactions/transfer-xem.md) tutorial to understand how +transactions are announced and confirmed. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/mosaics/create_mosaic', ['py', 'js']) }} + +## Code Explanation + +### Setting Up the Account + +{{ tutorial.code_snippet_tagged('step-1') }} + +The snippet reads the signer's private key from the `SIGNER_PRIVATE_KEY` environment variable, which defaults to a +test key if not set. +The signer's address is derived from the public key. +This account will own the created mosaic and must also own the namespace that will hold it. + +### Fetching Network Time + +{{ tutorial.code_snippet_tagged('step-2') }} + +Network time is fetched from , and the transaction's `timestamp` and `deadline` fields +are derived from it, following the process described in the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +### Choosing the Mosaic Name + +{{ tutorial.code_snippet_tagged('step-3') }} + +The mosaic ID is assembled from an existing namespace and a mosaic name. +See [Name](../../textbook/mosaics.md#name) in the Textbook for the naming rules. + +To avoid collisions across multiple runs of the tutorial, a timestamp is added to the mosaic name. +In practice, however, programs would use a fixed name for their mosaics. +You can force the tutorial to use fixed names through the `NAMESPACE` and `MOSAIC` environment variables. + +!!! warning "Use a namespace owned by the signer" + + By default, the code uses the test account referenced by `SIGNER_PRIVATE_KEY` and a namespace named + `my_namespace`. + + If you come from the [Registering a Root Namespace](../namespaces/register-root-namespace.md) tutorial, set the + `SIGNER_PRIVATE_KEY` and `NAMESPACE` environment variables to match the account and namespace you created there, + or any other namespace that the signer owns. + +### Defining the Mosaic + +{{ tutorial.code_snippet_tagged('step-4') }} + +The mosaic definition describes the asset itself, separately from the transaction that registers it: + +* {{ tutorial.var('owner_public_key') }}: The of the account creating the mosaic, which must match + {{ tutorial.var('signer_public_key') }}. + The network rejects transactions where the two differ. + +* {{ tutorial.var('id') }}: The mosaic identifier, formed from the namespace and the mosaic name. + +* {{ tutorial.var('description') }}: Text [describing](../../textbook/mosaics.md#description) the mosaic. + +* {{ tutorial.var('properties') }}: A set of key-value pairs that configure the mosaic behavior: + + * {{ tutorial.var('divisibility') }}: The number of decimal places the mosaic supports. + For example, a value of `2` means each whole unit can be divided into 100 (10^2^) atomic units. + See [Divisibility](../../textbook/mosaics.md#divisibility) in the Textbook. + * {{ tutorial.var('initialSupply') }}: The number of whole units minted to the creator when the mosaic is + defined. + See [Initial Supply](../../textbook/mosaics.md#initial-supply) in the Textbook. + * {{ tutorial.var('supplyMutable') }}: Whether the total supply can be changed after creation. + See [Supply Mutability](../../textbook/mosaics.md#supply-mutability) in the Textbook. + * {{ tutorial.var('transferable') }}: Whether the mosaic can be sent between any two accounts other than the + creator. + See [Transferability](../../textbook/mosaics.md#transferability) in the Textbook. + + In this example, the mosaic is divisible to two decimal places and starts with a supply of `1000.00` whole + units. + Its supply can be changed after creation, and its units can be freely transferred between accounts. + +!!! note "Optional levy" + + A mosaic definition can also include an optional [levy](../../textbook/mosaics.md#levy). + For more information, see the [Creating a Mosaic with a Levy](./mosaic-levy.md) tutorial. + +### Building the Mosaic Definition Transaction + +{{ tutorial.code_snippet_tagged('step-5') }} + +The mosaic definition transaction registers the mosaic on the network, specifying: + +* {{ tutorial.var('type') }}: Mosaic definition transactions use the type . + +* {{ tutorial.var('signer_public_key') }}: The account that signs the transaction and pays the fees, which must be the + owner of the namespace that will hold the mosaic. + It becomes the owner of the created mosaic. + +* {{ tutorial.var('timestamp') }} and {{ tutorial.var('deadline') }}: The values computed in the network time step. + +* {{ tutorial.var('rental_fee_sink') }}: The special account that collects mosaic + [creation fees](../../textbook/mosaics.md#creation-fee). + Each network has a fixed sink address: + + * : `NBMOSAICOD4F54EE5CDMR23CCBGOAM2XSIUX6TRS` + * : `TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC` + + The network rejects transactions that send the creation fee to any other address. + +* {{ tutorial.var('rental_fee') }}: The creation fee, which is 10 XEM. + The SDK's helper returns the required amount. + + The network rejects transactions that pay less than this fee. + Larger amounts are accepted, but the entire amount is transferred to the sink account. + +* {{ tutorial.var('mosaic_definition') }}: The mosaic definition built in the previous step. + +{{ tutorial.code_snippet_tagged('step-6') }} + +Finally, the transaction fee is calculated with and attached to the +transaction. +Unlike the creation fee, the transaction fee is paid to the . +Mosaic definition transactions pay a fixed transaction fee of 0.15 XEM, as shown in the +[fee schedule](../../textbook/transactions.md#fee-schedule). + +### Submitting the Mosaic Definition + +{{ tutorial.code_snippet_tagged('step-7') }} + +The mosaic definition transaction is signed and announced following the same process as in the +[Transfer XEM](../transactions/transfer-xem.md#announcing-the-transaction) tutorial. + +{{ tutorial.code_snippet_tagged('step-8') }} + +The code then waits for the transaction to be confirmed by polling the endpoint until the +transaction is included in a block. + +### Retrieving the Mosaic + +{{ tutorial.code_snippet_tagged('step-9') }} + +To verify the mosaic was created successfully, the code retrieves its definition from the +endpoint and displays its properties. + +A successful response confirms the mosaic exists on the network with the expected properties. + +!!! note "Mosaic lifetime" + + A mosaic has no duration of its own and becomes inactive when its parent namespace expires. + [Extending the root namespace](../namespaces/extend-root-namespace.md) keeps its mosaics usable. + See [Lifetime](../../textbook/mosaics.md#lifetime) in the Textbook. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text linenums="1" hl_lines="5 6 7 67 68 69 70" +--8<-- 'devbook/mosaics/create_mosaic.log' +``` + +Some highlights from the output: + +* **Mosaic ID** (line 5): The mosaic is identified by its fully qualified name, combining the namespace + `my_namespace` and a timestamped mosaic name. + Search for this name in the [NEM testnet explorer](https://testnet.nem.fyi/) to view the mosaic details. + +* **Creation fee and transaction fee** (lines 6-7): The creation fee is 10 XEM, while the transaction fee is + 0.15 XEM. + +* **Verified properties** (lines 67-70): The mosaic is retrieved from the network, confirming the expected + divisibility, the initial supply of `1000`, and that the mosaic is both supply mutable and transferable. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| [Define the mosaic](#defining-the-mosaic) | , | +| [Calculate the creation fee](#building-the-mosaic-definition-transaction) | | +| [Retrieve the mosaic](#retrieving-the-mosaic) | | + +## Next Steps + +Now that you have created a mosaic, you can: + +* [Change the mosaic supply](./change-mosaic-supply.md) to mint or burn units if the mosaic was created with a + [mutable supply](../../textbook/mosaics.md#supply-mutability) +* [Send your mosaic with a transfer transaction](../transactions/transfer-mosaics.md) to distribute it to other + accounts +* [Get mosaic information](./get-mosaic-info.md) to inspect the properties and supply of any mosaic +* [Modify the mosaic definition](./modify-mosaic-definition.md) to change its properties before distributing 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..0a227f6dc --- /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:** 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): Text that describes the mosaic, set by its creator. + +* **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/mosaics/modify-mosaic-definition.md b/mkdocs/pages/en/devbook/mosaics/modify-mosaic-definition.md new file mode 100644 index 000000000..603c55ba8 --- /dev/null +++ b/mkdocs/pages/en/devbook/mosaics/modify-mosaic-definition.md @@ -0,0 +1,91 @@ +--- +title: Modify Mosaic Definition +tutorial_level: intermediate +--- + +# Modifying a Mosaic Definition + +After a is created, its creator can modify some of its properties by sending another mosaic definition +transaction that uses the same mosaic identifier. +Instead of creating a new mosaic, the network updates the existing one. + +This tutorial shows how to modify an existing mosaic definition. +To change the mosaic's supply instead, see [Changing Mosaic Supply](./change-mosaic-supply.md). + +## Prerequisites + +Before you start, make sure to: + +* Create a with the that will sign the modification. + Only the creator can modify a mosaic. + See the [Creating a Mosaic](./create-mosaic.md) tutorial. +* Keep the that holds the mosaic active. + See [Lifetime](../../textbook/mosaics.md#lifetime) in the Textbook. +* Obtain to pay for the transaction and creation fees. + See [Getting Testnet Funds from the Faucet](../accounts/testnet-faucet.md). + +## What Can Be Changed + +The [description](../../textbook/mosaics.md#description) can be changed at any time. + +The [transferability](../../textbook/mosaics.md#transferability) and the [name](../../textbook/mosaics.md#name) can +never be changed. + +Changing the [divisibility](../../textbook/mosaics.md#divisibility), +[initial supply](../../textbook/mosaics.md#initial-supply), +[supply mutability](../../textbook/mosaics.md#supply-mutability), or [levy](../../textbook/mosaics.md#levy) requires +the creator to still own the entire mosaic supply. +In practice, most mosaic definitions can only be modified before the mosaic is distributed. + +For the complete rules, see [Modifying a Mosaic](../../textbook/mosaics.md#modifying-a-mosaic) in the Textbook. + +## Procedure + +To modify a mosaic definition, reuse the steps from the [Creating a Mosaic](./create-mosaic.md) tutorial: + +1. Retrieve the current definition from , as described in + [Retrieving the Mosaic](./create-mosaic.md#retrieving-the-mosaic). + +2. Build a new , as described in + [Building the Mosaic Definition Transaction](./create-mosaic.md#building-the-mosaic-definition-transaction), + using the **same mosaic identifier** (namespace name and mosaic name), signed by the **mosaic creator** account. + + The transaction must include the complete mosaic definition, including the + [description](../../textbook/mosaics.md#description), the + [mosaic properties](../../textbook/mosaics.md#properties), and the [levy](../../textbook/mosaics.md#levy). + Resend the definition retrieved in the previous step, changing only the values that should be updated. + + !!! warning "Resend the complete definition" + + The description is replaced with the one in the transaction, and cannot be left empty. + + Mosaic properties omitted from the transaction are reset to their default values: + + * `divisibility`: `0` + * `initialSupply`: `1000` + * `supplyMutable`: `false` + * `transferable`: `true` + + An omitted levy is removed from the mosaic. + +3. Submit the transaction, as described in + [Submitting the Mosaic Definition](./create-mosaic.md#submitting-the-mosaic-definition). + +4. Retrieve the definition again to confirm that the mosaic holds the updated values. + +The transaction pays the full [creation fee](../../textbook/mosaics.md#creation-fee) of 10 XEM, the same amount as +the transaction that created the mosaic, in addition to the fixed transaction fee of 0.15 XEM shown in the +[fee schedule](../../textbook/transactions.md#fee-schedule). +For example, changing only the description costs the same as creating a new mosaic. + +## Outcome + +If only the description changes, the network preserves the existing supply and the balances of all accounts that hold +the mosaic. + +If any other property changes, which is only allowed while the creator owns the entire supply, the network rebuilds the +mosaic from the new definition. +The rebuild resets the total supply to `initialSupply` and assigns the entire supply to the creator account. + +Because the creator is the only holder at that point, no other account balances are affected. +Once the mosaic has been distributed, the network rejects transactions that modify these other properties. diff --git a/mkdocs/pages/en/devbook/mosaics/mosaic-levy.md b/mkdocs/pages/en/devbook/mosaics/mosaic-levy.md new file mode 100644 index 000000000..7178121ed --- /dev/null +++ b/mkdocs/pages/en/devbook/mosaics/mosaic-levy.md @@ -0,0 +1,193 @@ +--- +title: Create Mosaic with Levy +tutorial_level: advanced +--- + +# Creating a Mosaic with a Levy + +A can include an optional , a fee charged to the sender on every transfer and credited to a designated +account. + +A typical use of levies is funding the account behind an asset, for example by charging a commission or a royalty on +every transfer. + +This tutorial shows how to create a mosaic with a levy. + +## Prerequisites + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). +* Create an to own the mosaic, either + [from code](../accounts/create-from-private-key.md) or + [by using a wallet](../../userbook/wallet/create-account.md). +* Register a to hold the mosaic. + See [Registering a Root Namespace](../namespaces/register-root-namespace.md). +* Obtain to pay for the transaction and creation fees. + See [Getting Testnet Funds from the Faucet](../accounts/testnet-faucet.md). + +Additionally, review the [Creating a Mosaic](./create-mosaic.md) tutorial to understand how a mosaic definition is +built, announced, and confirmed. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/mosaics/mosaic_levy', ['py', 'js']) }} + +## Code Explanation + +### Setting Up the Account and the Mosaic + +{{ tutorial.code_snippet_tagged('step-1') }} + +The snippet reads the signer's private key from the `SIGNER_PRIVATE_KEY` environment variable, which defaults to a +test key if not set. +This account signs the transaction and becomes the owner of the mosaic, so it must also own the namespace that will +hold it. +The mosaic identifier is assembled from that namespace and a mosaic name. +See [Name](../../textbook/mosaics.md#name) in the Textbook for the naming rules. + +To avoid collisions across multiple runs of the tutorial, a timestamp is added to the mosaic name. +In practice, however, programs would use a fixed name for their mosaics. +You can force the tutorial to use fixed names through the `NAMESPACE` and `MOSAIC` environment variables. + +!!! warning "Use a namespace owned by the signer" + + By default, the code uses the test account referenced by `SIGNER_PRIVATE_KEY` and a namespace named + `my_namespace`. + + If you come from the [Registering a Root Namespace](../namespaces/register-root-namespace.md) tutorial, set the + `SIGNER_PRIVATE_KEY` and `NAMESPACE` environment variables to match the account and namespace you created there, + or any other namespace that the signer owns. + +{{ tutorial.code_snippet_tagged('step-2') }} + +Network time is fetched from , and the transaction's `timestamp` and `deadline` fields +are derived from it, following the process described in the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +### Describing the Levy + +{{ tutorial.code_snippet_tagged('step-3') }} + +The levy is a structure with four fields: + +* {{ tutorial.var('transfer_fee_type') }}: How the levy amount is calculated: + + * `absolute`: A fixed quantity charged on every transfer, regardless of the amount transferred. + * `percentile`: A quantity proportional to the amount transferred. + + This tutorial uses an `absolute` levy, so every transfer is charged the same amount. + +* {{ tutorial.var('recipient_address') }}: The account credited with the levy on every transfer. + It can be the mosaic creator or any other account. + +* {{ tutorial.var('mosaic_id') }}: The mosaic in which the levy is paid. + This tutorial charges the levy in `nem:xem`, so senders pay in the network currency. + + The levy can also be paid in the mosaic being defined itself. + Any other levy mosaic must already exist on the network and be + [transferable](../../textbook/mosaics.md#transferability). + +* {{ tutorial.var('fee') }}: The levy amount. + For an `absolute` levy, it is expressed in the [atomic units](../../textbook/mosaics.md#divisibility) of the + levy mosaic. + Because `nem:xem` has a of 6, a value of 1'000'000 charges 1 XEM per transfer. + + For a `percentile` levy, the fee is interpreted in basis points instead: a `fee` of `100` charges 1% of the + amount transferred. + See the [percentile levy calculation](../../textbook/mosaics.md#percentile-levy-calculation) in the Textbook for + the full rules. + +### Attaching the Levy to the Mosaic Definition + +{{ tutorial.code_snippet_tagged('step-4') }} + +A levy is part of the mosaic definition, so it is set with the same used in +[Creating a Mosaic](./create-mosaic.md#building-the-mosaic-definition-transaction). +This tutorial reuses the same transaction, with the levy added to the {{ tutorial.var('mosaic_definition') }} field. + +The [creation fee](../../textbook/mosaics.md#creation-fee) is 10 XEM, and the transaction fee is a fixed 0.15 XEM, as +shown in the [fee schedule](../../textbook/transactions.md#fee-schedule). + +### Submitting the Mosaic Definition + +{{ tutorial.code_snippet_tagged('step-5') }} + +The transaction is then signed, announced, and confirmed following the same process as in the +[Transfer XEM](../transactions/transfer-xem.md#announcing-the-transaction) tutorial. + +### Verifying the Levy + +{{ tutorial.code_snippet_tagged('step-6') }} + +To verify the mosaic with the levy was created, the code retrieves the mosaic definition from the + endpoint, which returns the levy alongside the mosaic properties. + +A levy in the response confirms that future transfers of the mosaic will be charged the levy. + +## How the Levy Is Charged + +After the mosaic is created, the levy applies to every +[transfer](../transactions/transfer-mosaics.md) of the mosaic, and no extra field is needed in the transfer +transaction. + +!!! warning "A levy is not a guaranteed charge" + + Levies are not recursive, therefore they can be sidestepped. + See the [example in the Textbook](../../textbook/mosaics.md#limitations). + +The levy is charged on top of the transferred amount, so a sender who transfers 50 units of the mosaic created in this +tutorial is debited: + +* 50 units of the mosaic, credited to the recipient of the transfer. +* 1 XEM, credited to the levy recipient. +* The transaction fee, credited to the . + +The network rejects the transfer if the sender cannot cover both the transferred amount and the levy. +Because this levy is paid in `nem:xem`, the sender must also have enough XEM to cover both the levy and the transaction +fee. +If the levy is paid in another mosaic, the sender must also hold a sufficient balance of that mosaic. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text linenums="1" hl_lines="3 6-10 58-68 82-85" +--8<-- 'devbook/mosaics/mosaic_levy.log' +``` + +Some highlights from the output: + +* **Mosaic ID** (line 3): The mosaic is identified by its fully qualified name, combining the namespace + `my_namespace` and a timestamped mosaic name. + Search for this name in the [NEM testnet explorer](https://testnet.nem.fyi/) to view the mosaic details. + +* **Levy fields** (lines 6-10): The levy to create, an `absolute` fee of 1'000'000 atomic units of `nem:xem` (1 XEM), + paid to the levy recipient on every transfer. + +* **Levy in the transaction** (lines 58-68): The levy is defined inside the mosaic definition. + The recipient address, the levy mosaic name, and the mosaic name are hex-encoded in the payload, while the fee of + this `absolute` levy is expressed in atomic units. + +* **Verified levy** (lines 82-85): The mosaic is retrieved from the network, confirming the levy type, its recipient, + the mosaic in which it is paid, and its amount. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| --------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [Describe the levy](#describing-the-levy) | | +| [Attach the levy to a mosaic](#attaching-the-levy-to-the-mosaic-definition) | , | +| [Verify the levy](#verifying-the-levy) | | + +## Next Steps + +Now that you have created a mosaic with a levy, you can: + +* [Send your mosaic with a transfer transaction](../transactions/transfer-mosaics.md) to see the levy charged to the + sender. +* [Get mosaic information](./get-mosaic-info.md) to inspect the levy of any mosaic. diff --git a/mkdocs/pages/en/devbook/namespaces/extend-root-namespace.md b/mkdocs/pages/en/devbook/namespaces/extend-root-namespace.md new file mode 100644 index 000000000..7cdc89582 --- /dev/null +++ b/mkdocs/pages/en/devbook/namespaces/extend-root-namespace.md @@ -0,0 +1,57 @@ +--- +title: Extend Root Namespace +tutorial_level: beginner +--- + +# Extending a Root Namespace + + are leased for a limited [duration](../../textbook/namespaces.md#duration) of approximately one year +per registration. +If you want to keep a namespace beyond its initial lease, you need to extend it. + +This tutorial shows how to extend a root namespace. + +## Prerequisites + +* An that owns an active root namespace. + See [Registering a Root Namespace](./register-root-namespace.md). +* to pay for the transaction and lease fees. + +## When to Extend + +You can extend a namespace in two situations: + +* **Near the end of the lease:** Renewal is only allowed during the final 43200 blocks (approximately 30 days) before + expiration. + The network rejects renewal attempts made earlier than that. + +* **During the grace period:** The namespace has expired but is still within the + [grace period](../../textbook/namespaces.md#duration), which lasts 43200 blocks (approximately 30 days) after + expiration. + Extending it restores the namespace to active status immediately. + +!!! note "Extending Subnamespaces and Mosaics" + + Only root namespaces need to be extended. + and all associated inherit the root namespace's lifetime. + + Extending a root namespace therefore automatically keeps all of its subnamespaces and mosaics usable. + +## Procedure + +To extend a namespace, repeat the [registration process](./register-root-namespace.md) using the +**same root namespace name** and pay the **100 XEM lease fee** again. + +The account signing the transaction must be the namespace owner. + +The protocol accepts renewals only [near the end of the lease or during the grace period](#when-to-extend). + +## Duration and Limits + +Each renewal extends the lease to one year after the block containing the renewal transaction, not one year after the +previous expiration. +As a consequence, a namespace cannot be prepaid for multiple years in advance. + +To maintain a namespace indefinitely, renew it during the renewal window every year. + +For more details, see [Duration](../../textbook/namespaces.md#duration) in the Textbook. 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/namespaces/register-root-namespace.md b/mkdocs/pages/en/devbook/namespaces/register-root-namespace.md new file mode 100644 index 000000000..479a72ab8 --- /dev/null +++ b/mkdocs/pages/en/devbook/namespaces/register-root-namespace.md @@ -0,0 +1,164 @@ +--- +title: Register Root Namespace +tutorial_level: intermediate +--- + +# Registering a Root Namespace + + provide labels that group related under a meaningful name, like the `nem` prefix +in the native `nem:xem` mosaic. + +Namespaces can be nested under other namespaces, and this tutorial shows how to register a for one +year. + +To learn how to register a instead, read the [Registering a Subnamespace](./register-subnamespace.md) +guide. + +## Prerequisites + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). +* Create an to register the namespace, 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 and lease fees. + See [Getting Testnet Funds from the Faucet](../accounts/testnet-faucet.md). + +Additionally, review the [Transfer XEM](../transactions/transfer-xem.md) tutorial to understand how transactions are +announced and confirmed. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/namespaces/register_root_namespace', ['py', 'js']) }} + +## Code Explanation + +### Setting Up the Account + +{{ tutorial.code_snippet_tagged('step-1') }} + +The snippet reads the signer's private key from the `SIGNER_PRIVATE_KEY` environment variable, which defaults to a test +key if not set. +The signer's address is derived from the public key. +This account will own the registered namespace. + +### Fetching Network Time + +{{ tutorial.code_snippet_tagged('step-2') }} + +Network time is fetched from , and the transaction's `timestamp` and `deadline` fields +are derived from it, following the process described in the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +### Choosing the Namespace Name + +{{ tutorial.code_snippet_tagged('step-3') }} + +A namespace is identified by its name, which the transaction reserves on the network for one year. +See [Name](../../textbook/namespaces.md#name) in the Textbook for the naming rules. + +To avoid collisions across multiple runs of the tutorial, a timestamp is added to the name. +In practice, however, programs would use a fixed name for their namespaces. +You can force the tutorial to use a fixed name through the `ROOT_NAMESPACE` environment variable. + +### Building the Transaction + +{{ tutorial.code_snippet_tagged('step-4') }} + +The namespace registration transaction then registers the namespace on the network, specifying: + +* {{ tutorial.var('type') }}: Namespace registration transactions use the type . + +* {{ tutorial.var('signer_public_key') }}: The account that signs the transaction and pays the fees. + It becomes the owner of the registered namespace. + +* {{ tutorial.var('timestamp') }} and {{ tutorial.var('deadline') }}: The values computed in the network time step. + +* {{ tutorial.var('rental_fee_sink') }}: The special account that collects namespace + [lease fees](../../textbook/namespaces.md#lease-fee). + Each network has a fixed sink address: + + * : `NAMESPACEWH4MKFMBCVFERDPOOP4FK7MTBXDPZZA` + * : `TAMESPACEWH4MKFMBCVFERDPOOP4FK7MTDJEYP35` + + The network rejects transactions that send the lease fee to any other address. + +* {{ tutorial.var('rental_fee') }}: The lease fee, which is 100 XEM for root namespaces. + The SDK's helper returns the required amount. + The {{ tutorial.lit('True') }} argument requests the fee for a root namespace. + + The network rejects transactions that pay less than this fee. + Larger amounts are accepted, but the entire amount is transferred to the sink account. + +* {{ tutorial.var('name') }}: The name of the root namespace. + +{{ tutorial.code_snippet_tagged('step-5') }} + +Finally, the transaction fee is calculated with and attached to the +transaction. +Unlike the lease fee, the transaction fee is paid to the . +Namespace registration transactions pay a fixed transaction fee of 0.15 XEM, as shown in the +[fee schedule](../../textbook/transactions.md#fee-schedule). + +### Submitting the Transaction + +{{ tutorial.code_snippet_tagged('step-6') }} + +The transaction is signed and announced following the same process as in the +[Transfer XEM](../transactions/transfer-xem.md#announcing-the-transaction) tutorial. + +{{ tutorial.code_snippet_tagged('step-7') }} + +The code then waits for the transaction to be confirmed by polling the endpoint until the +transaction is included in a block. + +### Retrieving the Namespace + +{{ tutorial.code_snippet_tagged('step-8') }} + +To verify the namespace was registered, the code retrieves it from the network using the endpoint and +displays its properties. + +A successful response confirms that the namespace is registered and active. + +The response also shows the registration height, which is the block in which the namespace was registered, +marking the start of the one-year lease. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text linenums="1" hl_lines="5 6 7 31-33" +--8<-- 'devbook/namespaces/register_root_namespace.log' +``` + +Some highlights from the output: + +* **Namespace name** (line 5): The chosen name `ns_1783091378` includes a timestamp to ensure uniqueness. + Search for this name in the [NEM testnet explorer](https://testnet.nem.fyi/) to view the namespace details. + +* **Lease fee and transaction fee** (lines 6-7): The lease fee is 100 XEM because this is a root namespace + ( pay 10 XEM instead), while the transaction fee is 0.15 XEM. + +* **Namespace information** (lines 31-33): The registered namespace, its owner (the signer's address), and the + registration height, which is the block at which the lease began. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ----------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| [Build a namespace registration transaction](#building-the-transaction) | , | +| [Calculate the lease fee](#building-the-transaction) | | +| [Retrieve the namespace](#retrieving-the-namespace) | | + +## Next Steps + +Now that you have a root namespace, you can: + +* [Define mosaics](../mosaics/create-mosaic.md) under the namespace to create custom assets +* [Register a subnamespace](./register-subnamespace.md) to create a hierarchical structure +* [Extend the namespace](./extend-root-namespace.md) before it expires to keep it active diff --git a/mkdocs/pages/en/devbook/namespaces/register-subnamespace.md b/mkdocs/pages/en/devbook/namespaces/register-subnamespace.md new file mode 100644 index 000000000..41f4f2fa2 --- /dev/null +++ b/mkdocs/pages/en/devbook/namespaces/register-subnamespace.md @@ -0,0 +1,148 @@ +--- +title: Register Subnamespace +tutorial_level: intermediate +--- + +# Registering a Subnamespace + + (also called "child" namespaces) extend the hierarchical structure of . + +This tutorial shows how to register a subnamespace under an existing . + +To learn how to register a root namespace instead, read the [Registering a Root Namespace](./register-root-namespace.md) +guide. + +## Prerequisites + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). +* Have an with an existing root namespace. + See [Registering a Root Namespace](./register-root-namespace.md). + + !!! note + The examples in this tutorial use a root namespace named `ns_root`. + Make sure to update the code to use your own root namespace name. + +* Obtain to pay for the transaction and lease fees. + See [Getting Testnet Funds from the Faucet](../accounts/testnet-faucet.md). + +Additionally, review the [Transfer XEM](../transactions/transfer-xem.md) tutorial to understand how +transactions are announced and confirmed. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/namespaces/register_subnamespace', ['py', 'js']) }} + +## Code Explanation + +The code follows the same pattern as the [Registering a Root Namespace](./register-root-namespace.md) tutorial. +This section focuses only on the key differences. + +For detailed explanations of the common steps (setting up the account, fetching network time, announcing) and the +transaction descriptor fields shared with a root namespace, +see [Registering a Root Namespace](./register-root-namespace.md). + +### Choosing the Subnamespace Name + +{{ tutorial.code_snippet_tagged('step-1') }} + +A subnamespace is identified by its full name, which joins the parent namespace name and the child name with a dot, +such as `ns_root.product`. +See [Name](../../textbook/namespaces.md#name) in the Textbook for the naming rules. + +To avoid collisions across multiple runs of the tutorial, a timestamp is added to the child name. +In practice, however, programs would use a fixed name for their subnamespaces. +You can force the tutorial to use fixed names through the `ROOT_NAMESPACE` and `SUBNAMESPACE` environment variables. + +!!! warning "Use a parent namespace owned by the signer" + + By default, the code uses the test account referenced by `SIGNER_PRIVATE_KEY` and a parent namespace named + `ns_root`. + + If you come from the [Registering a Root Namespace](./register-root-namespace.md) tutorial, set the + `SIGNER_PRIVATE_KEY` and `ROOT_NAMESPACE` environment variables to match the account and namespace you created + there, or any other namespace that the signer owns. + +### Building the Transaction + +{{ tutorial.code_snippet_tagged('step-2') }} + +The main difference when registering a subnamespace is in the transaction descriptor: + +* {{ tutorial.var('parent_name') }}: The name of the parent namespace, defined in the previous step. + It can be a root namespace or another subnamespace. + +* {{ tutorial.var('name') }}: The name of the subnamespace, chosen in the previous step. + + Note that this is just the name of the subnamespace, not the full path. + For example, to create `company.product`, where `company` is the root, you would set + {{ tutorial.var("`name: 'product'`") }} and {{ tutorial.var("`parent_name: 'company'`") }}. + +* {{ tutorial.var('rental_fee') }}: The lease fee, which is 10 XEM for subnamespaces, paid to the same + [sink account](./register-root-namespace.md#building-the-transaction) as root namespaces. + + The SDK's helper returns the required amount. + The {{ tutorial.lit('False') }} argument requests the fee for a subnamespace. + +The transaction is then signed, announced, and confirmed following the same process as in the +[Registering a Root Namespace](./register-root-namespace.md#submitting-the-transaction) tutorial. + +### Retrieving the Subnamespace + +{{ tutorial.code_snippet_tagged('step-3') }} + +To verify the subnamespace was registered, the code retrieves it from the network using the endpoint +and displays its properties. + +The subnamespace is queried by its full name, which joins the parent and child names with a dot +(for example, `ns_root.sub_1783411728`). + +A successful response confirms the subnamespace is registered and active. + +The response also shows the registration height, which is the block in which the root namespace was registered, +because subnamespaces inherit their root namespace's [lease](../../textbook/namespaces.md#duration). + +!!! note "Subnamespace duration" + + A subnamespace expires when its root namespace expires and cannot be renewed on its own. + [Renewing the root namespace](./extend-root-namespace.md) also renews the subnamespace. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text linenums="1" hl_lines="5 6 7 32-34" +--8<-- 'devbook/namespaces/register_subnamespace.log' +``` + +Some highlights from the output: + +* **Full namespace path** (line 5): `ns_root.sub_1783411728` combines the parent namespace `ns_root` with the + subnamespace name set in the transaction. + +* **Lease fee and transaction fee** (lines 6-7): The lease fee is 10 XEM because this is a subnamespace + (root namespaces pay 100 XEM instead), while the transaction fee is 0.15 XEM. + +* **Namespace information** (lines 32-34): The registered subnamespace, its owner (the signer's address), and the + registration height, which is the block at which the root namespace's lease began, inherited by the subnamespace. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| [Build a subnamespace registration transaction](#building-the-transaction) | , | +| [Calculate the lease fee](#building-the-transaction) | | +| [Retrieve the subnamespace](#retrieving-the-subnamespace) | | + +## Next Steps + +Now that you have a subnamespace, you can: + +* Register additional subnamespaces to expand your hierarchical structure +* [Define mosaics](../mosaics/create-mosaic.md) under the subnamespace to create custom assets 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..737d01ffb --- /dev/null +++ b/mkdocs/pages/en/devbook/network-currency/query-currency-supply.md @@ -0,0 +1,121 @@ +--- +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. + +The balances are added up in atomic units. +They are only converted to whole XEM when printed: dividing by `scale` (1'000'000 for `nem:xem`) gives the whole part, +and the remainder gives the 6 decimal digits. + +Doing this with a regular division instead would produce a float, and these balances are so large that a float can get +the last digit wrong. + +### 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. + +The total supply from is in whole units, so the code multiplies it by `scale` to bring it +to atomic units before subtracting, then prints the result. + +## 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..21f74efbe --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/websockets/index.md @@ -0,0 +1,831 @@ +# 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 each time a new block is added to the chain. + + If multiple blocks are added at once, for example while the node catches up with its peers or after a , + the channel sends a **burst**: one notification per block, delivered in quick succession and in chain order. + + After a rollback, a notification can report a lower block height than a previously received notification because the + new blocks replace blocks that were already reported. + +
+ + +
: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 each time the chain changes, with one notification per chain update that contains the + height of the first block added or replaced. + + Unlike , this channel sends a single notification for each chain update, regardless of how many blocks it + contains. + For example, if five blocks are added at once, sends five notifications while this channel sends only + one, containing the height of the first block. + +
+ + +
: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. + + Every transaction type appears here, including , which are not reported on any of the + [account channels](#account-channels). + A appears as the outer multisig transaction, with the inner transaction nested inside. + +
+ + +
: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 channels send a notification whenever the account is **involved** in a transaction, even if the account +state does not change. + +The accounts considered involved depend on the transaction type: + +| Transaction type | Involved accounts | +| --------------------------------- | --------------------------------------------------------------------------------------- | +| Transfer | The signer and the recipient. | +| Importance transfer | The signer and the remote account. | +| Multisig aggregate modification | The signer and every cosignatory added or removed. | +| Provision namespace | The signer. | +| Mosaic definition creation | The signer, plus the levy recipient if the definition includes a levy. | +| Mosaic supply change | The signer, plus the levy recipient if the mosaic definition includes a levy. | +| Multisig | The initiating cosignatory, the multisig account, and others in the inner transaction. | + +!!! note "Multisig transactions" + + An account that has multiple roles, for example the initiating cosignatory and the recipient of the inner transfer, + receives one notification for each role. + + When a requires multiple signatures, each additional cosignatory approves it by submitting a + separate . + Cosignatures appear only on the global channel. + They never reach account channels, not even the multisig account's or the submitting cosignatory's. + +#### `/account/{address}` + +ws:account/{address} +: Notifies subscribed clients of the account's current state every time a confirmed block involves the address, + either through a transaction it is [involved in](#account-channels) or by harvesting the block. + Requires the address to be [registered](#registration-requests) first. + + Involvement in a transaction does not mean that the account changed. + For example, the recipient of a transfer of zero XEM is notified even though its balance stays the same. + +
+ + +
: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](#account-channels) the account enters the + . + Requires the address to be [registered](#registration-requests) first. + + Since the transaction is not yet included in a block, the `meta.height` field contains the placeholder value + `9007199254740991`, the largest integer that JSON parsers can represent safely. + +
+ + +
: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 every time a confirmed block includes a transaction [involving](#account-channels) + the account. + 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 of the account's mosaics, every time a confirmed block might have changed them. + The account's mosaics are the ones it holds a balance of, together with any it created. + + A notification burst is sent when a block contains a mosaic-related transaction [involving](#account-channels) the + account. + A transfer that carries mosaics notifies the signer, the recipient, and any levy recipients. + Mosaic definition creation and mosaic supply changes instead notify the mosaic's creator, plus any levy recipient. + + The transaction does not need to change the account's mosaics. + For example, a transfer of zero units of a mosaic still notifies both the sender and the recipient. + + Each burst contains the account's full mosaic list, with one notification per mosaic, regardless of which mosaics + changed. + +
+ + +
: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 of the definitions of the account's mosaics, every time a confirmed block might have + changed them. + + This channel is triggered by the same transactions as and covers the same + mosaics. + Each burst contains the account's full list of mosaic definitions, with one notification per definition, regardless + of which mosaics changed. + +
+ + +
: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 of the namespaces the account owns, every time a confirmed block might have + changed them. + + A notification burst is sent when a block contains a provision namespace transaction signed by the account. + Each burst contains the account's full list of owned namespaces, with one notification per namespace. + +
+ + +
: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. + +!!! note "Registrations are shared" + + The node maintains a single list of registered addresses that is shared by all connected clients. + After any client registers an address, all clients subscribed to that account's channels receive notifications for + that address without registering it again. + The registration remains active until the node restarts. + +#### `/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 the account's up to 25 most recent confirmed transactions to + , plus its pending transactions exactly as + does. + +
+ + +
: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 up to 10 of the account's most recent pending transactions to + and the global channel. + +
+ + +
: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 . + + The node processes the block as if it had just been added, so the [account channels](#account-channels) related + to the accounts involved in it are notified again as well. + +
+ + +
: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/reference/whitepaper/index.md b/mkdocs/pages/en/devbook/reference/whitepaper/index.md new file mode 100644 index 000000000..845d710b3 --- /dev/null +++ b/mkdocs/pages/en/devbook/reference/whitepaper/index.md @@ -0,0 +1,17 @@ +--- +title: Whitepaper +hide: +- toc +--- + +# The NEM Whitepaper + + + + 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/messages.md b/mkdocs/pages/en/devbook/transactions/messages.md new file mode 100644 index 000000000..c31bb00a1 --- /dev/null +++ b/mkdocs/pages/en/devbook/transactions/messages.md @@ -0,0 +1,192 @@ +--- +title: Messages +tutorial_level: intermediate +--- + +# Sending Messages with Transfer Transactions + + can include an optional message field, which allows attaching up to 1024 +bytes of data to the transaction. +Messages can be sent as plain text or encrypted using the recipient's public key, ensuring only the intended recipient +can read them. + +This tutorial shows how to send both plain and encrypted messages and how to decode received messages. + +## Prerequisites + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a 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. + See [Getting Testnet Funds from the Faucet](../accounts/testnet-faucet.md). + +Additionally, check the [Transfer transaction](./transfer-xem.md) tutorial to understand how fee +calculation, network time, and transaction confirmation work. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/transactions/messages', ['py', 'js']) }} + +## Code Explanation + +This tutorial focuses on the message-specific aspects of transfer transactions. +The parts about fetching network time, calculating fees, and announcing transactions have been explained in the +[Transfer Transaction](./transfer-xem.md) tutorial and are skipped here for brevity. + +### Setting Up Accounts + +{{ tutorial.code_snippet_tagged('step-1') }} + +To send a message, you need the sender's and the recipient's . +To encrypt a message, you additionally need the recipient's . + +This tutorial uses two accounts (sender and recipient) to demonstrate both sending and receiving plain and encrypted +messages. +The snippet reads their private keys from the `SENDER_PRIVATE_KEY` and `RECIPIENT_PRIVATE_KEY` environment variables, +which default to test keys if not set. +The recipient's public key and address are derived from their private key. + +!!! note "Retrieving public keys" + + When only the address is known, you can retrieve the public key from the network using the + endpoint. + An account's public key becomes available only after it has broadcast at least one transaction. + +### Sending a Plain Text Message + +{{ tutorial.code_snippet_tagged('step-2') }} + +You can combine mosaic transfers with messages by including both the `mosaics` and `message` fields in the transaction +descriptor. + +The transaction is then signed and announced following the same process as in +[Sending XEM with a Transfer Transaction](./transfer-xem.md). + +**Message constraints:** + +* **Maximum size:** 1024 bytes (the network rejects larger messages). +* **Encoding:** UTF-8 by convention, though the protocol does not enforce a standard. +* **Privacy:** All messages are publicly visible on the blockchain unless encrypted. + +!!! tip "Handling larger data" + + For applications requiring more than 1024 bytes of data, common approaches include: + + * **On-chain storage:** Split the data across multiple , allowing you to keep everything on + the blockchain. + * **Off-chain storage:** Store the data off-chain and include a hash and a reference in the message field. + The hash verifies data integrity while the reference enables retrieval. + +### Receiving a Plain Text Message + +{{ tutorial.code_snippet_tagged('step-3') }} + +After announcing the transaction, the {{ tutorial.var('retrieve_confirmed_transaction') }} helper function polls the + endpoint until the transaction is confirmed. + +The confirmed transaction contains the message as a hex string. +To retrieve the original message, it converts the hex string to bytes and decodes it as UTF-8. + +### Sending an Encrypted Message + +{{ tutorial.code_snippet_tagged('step-4') }} + +Encrypted messages provide confidentiality by protecting the message content using a shared secret derived from the +sender's private key and the recipient's public key. +Both the sender and recipient can decrypt the message using their own private key and the other party's public key. + +The class handles message encryption: + +1. A is created with the sender's key pair. +2. The message is encoded using the recipient's public key and the message bytes with . +3. The encrypted payload is attached to the transaction's `message` field. + +The transaction is then signed and announced following the same process as in +[Sending XEM with a Transfer Transaction](./transfer-xem.md). + +!!! note "Message encryption is a convention" + + The NEM protocol does not define a standard for message encryption. + Sender and recipient must agree in advance on whether messages are encrypted and the cipher used. + + implements **AES-GCM**, the convention used by most wallets and applications. + The class also provides for compatibility with legacy AES-CBC messages, + the convention used by the earliest NEM wallets and applications. + automatically detects and decodes both schemes. + + + For more details, see [Optional Messages](../../textbook/transfer_transactions.md#optional-message) in the Textbook. + +### Receiving an Encrypted Message + +{{ tutorial.code_snippet_tagged('step-5') }} + +After announcing the encrypted message transaction, the {{ tutorial.var('retrieve_confirmed_transaction') }} helper +function polls for confirmation. + +To decrypt the message from the confirmed transaction, a is created with the recipient's key pair, +then is called with the sender's public key (obtained from the transaction's +`signer` field) and the encrypted payload. + +The method returns a tuple {{ tutorial.var('(is_decoded, message)') }} indicating whether decryption was successful, +and, if so, contains the original plaintext bytes, which still need to be decoded. + +!!! note "Decryption works both ways" + + Because the encryption uses a shared secret derived from both key pairs, the sender can also decrypt the message + using their own private key and the recipient's public key. + This allows both parties to verify the message content after it has been published on the blockchain. + +If decryption fails, possible causes include: + +* The message was encrypted for a different recipient. +* The message is corrupted or tampered with. +* The message is plain text, not encrypted. +* An incorrect public key was used for the other party. +* The same convention was not used when encrypting and decrypting. + +## Output + +The output shown below corresponds to a typical run of the program. + +```text linenums="1" hl_lines="9 16 19 20 27" +--8<-- 'devbook/transactions/messages.log' +``` + +Some highlights from the output: + +* **Plain message** (line 9): The message attached to the first transaction. + Because it is not encrypted, anyone inspecting the blockchain can read it. + +* **Received plain message** (line 16): The same message, recovered from the confirmed transaction by converting the + hexadecimal payload back to UTF-8. + +* **Original message** (line 19): The secret message before encryption. + +* **Encrypted payload** (line 20): The result of encrypting the previous message with , + shown as a hexadecimal string. + This is what gets stored on the blockchain. + +* **Recipient decrypted message** (line 27): The original message, retrieved from the confirmed transaction and + decrypted by using the recipient's private key and the sender's public key. + +You can view the transactions on the [NEM testnet explorer](https://testnet.nem.fyi/) by searching for the +transaction hashes printed in the output. + +The explorer cannot decrypt encrypted messages because it does not have access to the private keys. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| -------------------------------------------------------------- | ------------------------------------------- | +| [Convert text into UTF-8 bytes](#sending-a-plain-text-message) | `TextEncoder` (JS) and `str.encode`/`bytes.decode` (Python)
System methods, not part of the SDK | +| [Encrypt a message](#sending-an-encrypted-message) | | +| [Decrypt a message](#receiving-an-encrypted-message) | | 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..234e3b4c9 --- /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](./transfer-xem.md) tutorial, 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](./transfer-xem.md) tutorial. + +### 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](./transfer-xem.md#announcing-the-transaction) tutorial. + +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/sign-multisig.md b/mkdocs/pages/en/devbook/transactions/sign-multisig.md new file mode 100644 index 000000000..1708226a6 --- /dev/null +++ b/mkdocs/pages/en/devbook/transactions/sign-multisig.md @@ -0,0 +1,277 @@ +--- +title: Sign a Multisig +tutorial_level: intermediate +--- + +# Signing a Transaction from a Multisignature Account + +This tutorial transfers 1 from an to itself, mirroring the +[Transfer XEM](../transactions/transfer-xem.md) tutorial. + +However, in this case, the source account is a , also called _multisig_, +and therefore it cannot initiate or sign transactions on its own. +Instead, it relies on its cosignatory accounts to create transactions and sign them on its behalf. + +The multisig account used in this tutorial is configured as a **2-of-2** multisig. +It has two cosignatories, and both signatures are required to approve the transfer. + +**Cosignatory 0** initiates the transfer, and **Cosignatory 1** provides the second required cosignature: + +```dot +digraph "Multisignature Tree" { + rankdir="BT"; + node [fontsize=12]; + "Multisignature Account"; + "Cosignatory 0"; + "Cosignatory 1"; + + "Cosignatory 0" -> "Multisignature Account"; + "Cosignatory 1" -> "Multisignature Account"; +} +``` + +!!! note "Alternative: WebSockets" + + In this tutorial, the cosignatory discovers the pending transaction by querying the node. + For a WebSocket-based approach, where the cosignatory is notified in real time, see the + [Listening to Multisig Transaction Flow](../websockets/listen-multisig-transaction-flow.md) tutorial. + +## Prerequisites + +{# Early initialization so we can use the var() macro #} +{% import 'tutorial.jinja2' as tutorial with context %} +{{ tutorial.code_full_tagged('devbook/transactions/sign_multisig', ['py', 'js'], show=false) }} + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). + +* Create a **2-of-2** multisig account. + To create it, run the [configure multisig tutorial](../accounts/configure-multisig.md#enabling-the-multisig), + changing {{ tutorial.var('min_approval_delta') }} from `1` to `2`. + If the account is already a multisig with a different configuration, + [disable it](../accounts/configure-multisig.md#disabling-the-multisig) first. + +Additionally, review the [Transfer XEM](../transactions/transfer-xem.md) tutorial to understand how transactions are +announced and confirmed. + +## Full Code + +{{ tutorial.code_full_tagged('devbook/transactions/sign_multisig', ['py', 'js']) }} + +## Code Explanation + +Signing a transaction on behalf of a multisig account involves wrapping it in a and +collecting the required cosignatures. + +In this tutorial, the wrapped transaction is a transfer, with the multisig account as its signer, +since this is the origin of the funds. +Cosignatory 0 signs and announces the wrapper, and the transaction remains pending until Cosignatory 1 provides the +second required cosignature. + +In practice, each cosignatory would run its own part on a different machine, holding only its own private key. +This tutorial combines both roles in a single program for simplicity. + +The code defines two helper functions for announcing a transaction and waiting for its confirmation. +For details on how these work, see the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +### Setting Up the Accounts + +{{ tutorial.code_snippet_tagged('step-1') }} + +The tutorial requires three separate accounts, configured through environment variables. +If not set, default values are used: + +| Environment Variable | Default value | Purpose | +|----------------------------|---------------|----------------------------------------------| +| `MULTISIG_PUBLIC_KEY` | `D656..ACF2` | 2-of-2 multisig account | +| `COSIGNATORY0_PRIVATE_KEY` | `0000..0002` | First cosignatory account, the **initiator** | +| `COSIGNATORY1_PRIVATE_KEY` | `0000..0003` | Second cosignatory account | + +Each key is a 64-character hexadecimal string. + +Unlike a regular account, the multisig account cannot initiate transactions itself. +Instead, its cosignatories sign on its behalf. +Its is therefore never needed, and its is enough to identify the account. + +The multisig account must hold enough funds to pay the transaction fees. +If the default values are used, this account may already be funded. + +The snippet above derives and stores the of each cosignatory, and the multisig account's , +for later use. + +### Fetching Network Time + +{{ tutorial.code_snippet_tagged('step-2') }} + +Network time is fetched from , and the transactions' `timestamp` and `deadline` fields +are derived from it, following the process described in the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +### Building the Transaction + +The transaction wrapped inside a multisig transaction is called the , and can be any +, such as the transfer used in this tutorial or the modifications used in +[Configuring a Multisignature Account](../accounts/configure-multisig.md). +Multisig transactions cannot be nested. + +{{ tutorial.code_snippet_tagged('step-3') }} + +The inner includes the following fields: + +* {{ tutorial.var('signer_public_key') }}: of the account whose funds are being transferred, that is, + the multisignature account. + +* {{ tutorial.var('recipient_address') }}: in this particular example, the funds are sent back to the sender, so the + recipient is also the multisig account. + +* {{ tutorial.var('amount') }}: 1'000'000 atomic units, corresponding to 1 , + as explained in the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +The inner transaction has its own transaction fee, calculated with . +For the 1 XEM sent here, the fee is 0.05 XEM, as shown in the +[transfer fee schedule](../../textbook/transfer_transactions.md#fees). + +{{ tutorial.code_snippet_tagged('step-4') }} + +The transfer transaction is then wrapped in a . Its most relevant fields are: + +* {{ tutorial.var('signer_public_key') }}: this time, it is the of the cosignatory that initiates the + transaction. + +* {{ tutorial.var('inner_transaction') }}: the wrapped transfer transaction, converted with + so it can be embedded without a signature of its own. + +The multisig wrapper also has its own transaction fee of 0.15 XEM, as shown in the +[fee schedule](../../textbook/transactions.md#fee-schedule). +All fees, and the transferred amount, are deducted from the multisig account once the transaction is confirmed. + +### Initiator: Announcing the Multisig Transaction + +{{ tutorial.code_snippet_tagged('step-5') }} + +In this case, Cosignatory 0 is the initiator of the multisig transaction. +It signs the transaction and announces it to the network. + +If valid, the network accepts the transaction, but it is not confirmed yet. +Since the multisig account requires two cosignatures and only one has been provided, +the transaction waits in the until the missing cosignature arrives. + +!!! note "Simpler configurations" + + In a multisig that requires only one cosignature, such as the 1-of-2 configuration created in the + [Configuring a Multisignature Account](../accounts/configure-multisig.md) tutorial, the initiating + cosignatory's signature is enough. + If valid, the transaction is confirmed without any further steps. + +### Cosignatory: Retrieving the Pending Transaction + +{{ tutorial.code_snippet_tagged('step-6') }} + +At this point, Cosignatory 1 takes over. +Cosignatories can use the endpoint to discover pending multisig transactions +awaiting their signature. + +The metadata of each pending multisig transaction contains the hash of its **inner transaction**, which is the value +that a cosignature must reference. + +A cosignatory can have multiple pending multisig transactions awaiting approval. +In this example, the code selects the transaction issued by the multisig account. +This approach is sufficient for the tutorial because only one pending transaction is expected from that account, and +Cosignatory 1 has no prior knowledge of the transaction to match more precisely. + +Alternatively, the initiator can share the inner transaction hash with the other cosignatories through an off-chain +channel. +The cosignatories can then match this hash to select the exact transaction. + +In real applications, however, filtering by the issuing account is not enough. +Nothing guarantees that a pending transaction is the expected one, so inspect the content of each pending transaction, +such as its type, recipient, and amount, before selecting the one to cosign. + +!!! warning "Verify before cosigning" + + Always verify the contents of a transaction before cosigning it. + Cosignatures are binding and cannot be undone. + +### Cosignatory: Cosigning the Transaction + +{{ tutorial.code_snippet_tagged('step-7') }} + +Cosignatory 1 provides the missing signature by announcing a . +The cosignature specifies: + +* {{ tutorial.var('signer_public_key') }}: of the cosignatory providing the signature. + +* {{ tutorial.var('other_transaction_hash') }}: hash of the inner transfer transaction retrieved in the previous step. + +* {{ tutorial.var('multisig_account_address') }}: of the multisig account the signature refers to. + +The cosignature has a 0.15 XEM fee. +The fee is also deducted from the multisig account once the multisig transaction is confirmed. + +{{ tutorial.code_snippet_tagged('step-8') }} + +Cosignatory 1 then signs the cosignature and announces it to the network. + +The announced cosignature does not appear in the as a separate transaction. +Instead, the network attaches it to the pending multisig transaction. + +In configurations that require additional cosignatures, the transaction remains pending. +The collected signatures can be inspected in the transaction's `signatures` field by querying + again. + +In this tutorial, however, the second cosignature completes the transaction, which leaves the pool and is confirmed +in the next block. + +### Waiting for Confirmation + +{{ tutorial.code_snippet_tagged('step-9') }} + +Once all required cosignatures have been collected, the multisig transaction is confirmed as a single unit. + +Multisig transactions are rejected if they violate protocol constraints. +The following table summarizes the most common error sources: + +| Error message | Probable cause | +|------------------------------------------------|---------------------------------------------------------------------------------------------------------------| +| `FAILURE_TRANSACTION_NOT_ALLOWED_FOR_MULTISIG` | The multisig account tried to announce the transfer itself. | +| `FAILURE_MULTISIG_NOT_A_COSIGNER` | The signer of the multisig transaction is not in the cosignatories list. | +| `FAILURE_MULTISIG_NO_MATCHING_MULTISIG` | The cosignature does not match a pending multisig transaction, or its signer is not a cosignatory. | + +## Output + +The output shown below corresponds to a typical run of the program. + +```text linenums="1" hl_lines="2-4 13 22 35 42 46 51" +--8<-- 'devbook/transactions/sign_multisig.log' +``` + +Key points in the output: + +* **Lines 2-4**: Public keys of all involved accounts. +* **Line 13** (`signer_public_key`): Signer of the multisig transaction. + Note that it matches Cosignatory 0. +* **Line 22** (`signer_public_key`): Signer of the inner transfer transaction. + Note that it matches the multisig account. +* **Line 35** (`Inner transaction hash`): Hash of the pending inner transaction, retrieved from the network. +* **Line 42** (`signer_public_key`): Signer of the cosignature. + Note that it matches Cosignatory 1. +* **Line 46** (`other_transaction_hash`): The inner transaction hash referenced by the cosignature. +* **Line 51**: Hash of the multisig transaction, which uniquely identifies it on the network. + +The multisig transaction hash shown in the output can be used to look up the confirmed transaction in the +[NEM testnet explorer](https://testnet.nem.fyi/). + +## Conclusion + +This tutorial is functionally identical to the [Transfer XEM](../transactions/transfer-xem.md) tutorial, +but using a as the source account. + +In particular, the tutorial showed how to: + +| Step | Related documentation | +|----------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| [Wrap transfer in a multisig transaction](#building-the-transaction) | , | +| [Sign the multisig transaction](#initiator-announcing-the-multisig-transaction) | | +| [Discover pending transactions](#cosignatory-retrieving-the-pending-transaction) | | +| [Cosign a pending multisig transaction](#cosignatory-cosigning-the-transaction) | | 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..83307de1c --- /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..965d636bf --- /dev/null +++ b/mkdocs/pages/en/devbook/transactions/transfer-xem.md @@ -0,0 +1,239 @@ +--- +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. + +The code wraps the seconds value in the SDK's class to obtain the `timestamp`, and derives the +`deadline` from it with the helper. + +### 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-multisig-transaction-flow.md b/mkdocs/pages/en/devbook/websockets/listen-multisig-transaction-flow.md new file mode 100644 index 000000000..a0557d2ae --- /dev/null +++ b/mkdocs/pages/en/devbook/websockets/listen-multisig-transaction-flow.md @@ -0,0 +1,311 @@ +--- +title: Multisig Transaction Flow +tutorial_level: advanced +--- + +# Listening to Multisig Transaction Flow + +A transaction from a follows a richer lifecycle than a regular transaction. +After being announced, it waits in the while the network collects the required cosignatures from +the account's cosignatories. +Only after all cosignatures arrive is the transaction confirmed in a . + +This tutorial recreates the transfer from the +[Signing a Transaction from a Multisignature Account](../transactions/sign-multisig.md) tutorial, but monitors the +full multisig lifecycle using [WebSocket](../reference/websockets/index.md) channels instead of polling. + +The multisig account used in this tutorial is configured as a **2-of-2** multisig. +It has two cosignatories, and both signatures are required to approve the transfer: + +```dot +digraph "Multisignature Tree" { + rankdir="BT"; + node [fontsize=12]; + "Multisignature Account"; + "Cosignatory 0"; + "Cosignatory 1"; + + "Cosignatory 0" -> "Multisignature Account"; + "Cosignatory 1" -> "Multisignature Account"; +} +``` + +Cosignatory 0 builds and announces the multisig transaction, while Cosignatory 1 subscribes to the multisig +account's WebSocket channels, cosigns, and waits for confirmation. + +!!! note "Alternative: Polling" + + For a polling-based approach, where the cosignatory discovers the pending transaction by querying the node, see + the [Signing a Transaction from a Multisignature Account](../transactions/sign-multisig.md) tutorial. + +## Prerequisites + +{# Early initialization so we can use the var() macro #} +{% import 'tutorial.jinja2' as tutorial with context %} +{{ tutorial.code_full_tagged('devbook/transactions/sign_multisig', ['py', 'js'], show=false) }} + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). + +* Create a **2-of-2** multisig account. + To create it, run the [configure multisig tutorial](../accounts/configure-multisig.md#enabling-the-multisig), + changing {{ tutorial.var('min_approval_delta') }} from `1` to `2`. + If the account is already a multisig with a different configuration, + [disable it](../accounts/configure-multisig.md#disabling-the-multisig) first. + +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_multisig_transaction_flow', ['py', 'js']) }} + +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`, the default HTTP API port, with `7778`, the default NIS +WebSocket port. + +!!! note "Python SockJS helpers" + + There is no SockJS client library for Python, so a few small helper methods are defined at the top of the file + for convenience. + +## Code Explanation + +A multisig transaction involves two distinct roles: an **initiator** (Cosignatory 0) that builds, signs, and +announces the multisig transaction, and one or more **cosignatories** (Cosignatory 1 in this tutorial) that monitor +WebSocket channels and cosign after verifying the transaction. +Any cosignatory of the multisig account can take either role. + +In practice, each role runs as a separate program on a separate machine, holding only its own private key. +This tutorial combines both roles in a single script for simplicity. + +### Setting Up the Accounts + +{{ tutorial.code_snippet_tagged('step-1') }} + +The tutorial requires three separate accounts, configured through environment variables. +If not set, default values are used: + +| Environment Variable | Default value | Purpose | +|----------------------------|---------------|----------------------------------------------| +| `MULTISIG_PUBLIC_KEY` | `D656..ACF2` | 2-of-2 multisig account | +| `COSIGNATORY0_PRIVATE_KEY` | `0000..0002` | First cosignatory account, the **initiator** | +| `COSIGNATORY1_PRIVATE_KEY` | `0000..0003` | Second cosignatory account | + +Each key is a 64-character hexadecimal string. + +Unlike a regular account, the multisig account cannot initiate transactions itself. +Instead, its cosignatories sign on its behalf. +Its is therefore never needed, and its is enough to identify the account. + +The multisig account must hold enough funds to pay the transaction fees. +If the default values are used, this account may already be funded. + +The snippet above derives and stores the of each cosignatory, and the multisig account's , +for later use. +The WebSocket channels subscribed later are scoped to this address. + +### Initiator: Building the Multisig Transaction + +{{ tutorial.code_snippet_tagged('step-2') }} + +Cosignatory 0 fetches the network time, builds an transfer of 1 from the multisig +account to itself, wraps it in a , and signs it. +The implementation follows the same pattern described in the +[Signing a Transaction from a Multisignature Account](../transactions/sign-multisig.md#building-the-transaction) +tutorial. + +The transaction is prepared, but it is not [announced](#initiator-announcing-the-multisig-transaction) yet. +The announcement happens after the channel subscriptions are established, ensuring that the resulting notifications are +not missed. + +### Cosignatory: Connecting to the WebSocket + +{{ tutorial.code_snippet_tagged('step-3') }} + +Cosignatory 1 opens a SockJS connection to the `/w/messages` endpoint on `WS_URL` and starts a +over it. + +### Cosignatory: Subscribing to the Channels + +{{ tutorial.code_snippet_tagged('step-4') }} + +Cosignatory 1 subscribes to the same three address-scoped channels used in the +[Listening to Transaction Flow](./listen-transaction-flow.md) tutorial: + +* : Notifies of the account's current state when a involving the account's address is + confirmed. +* : Notifies of a transaction involving the account's address when it enters the + , waiting to be included in a block. +* : Notifies of a transaction involving the account's address when it is included in a + . + +The subscriptions use the IDs `id-0`, `id-1` and `id-2`, which identify them when the code unsubscribes at the end. + +Unlike the non-multisig case, the channels listen to the activity of the **multisig account**, not the cosignatory +account. + +The reason is that the node only notifies the initiating cosignatory and the accounts involved in the inner transaction, +Cosignatory 0 and the multisig account in this example. +Cosignatories waiting to approve the transaction, such as Cosignatory 1, receive no notifications on their own +addresses, so they must subscribe to the multisig account's address instead. + +!!! note "Message handling differences" + + In JavaScript, each channel is subscribed with a dedicated handler function, defined in the + [cosigning](#cosignatory-cosigning-the-pending-transaction) and + [confirmation](#cosignatory-waiting-for-confirmation) steps below. + In Python, messages are instead read sequentially from the connection as they arrive. + +All three channels stay silent until the address is registered, which the next step performs. + +### Cosignatory: Registering the Multisig Account + +{{ tutorial.code_snippet_tagged('step-5') }} + +To receive notifications on an account's channels, the address must first be **registered** with the node. + +The code sends a request to , which registers the multisig address and also forces +the node to send the account's current state on the channel. + +The code waits for this first account notification, which confirms that the registration is active. +The notification follows the [AccountMetaDataPair](../reference/rest/nem.md#model/AccountMetaDataPair) schema. + +### Initiator: Announcing the Multisig Transaction + +{{ tutorial.code_snippet_tagged('step-6') }} + +!!! 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. + + A cosignatory that misses the notification, for example by subscribing only after the announcement, can still + discover the pending transaction by polling . + +Once Cosignatory 1 is subscribed, Cosignatory 0 announces the multisig transaction to the +endpoint and checks the result. +If the node rejects it, the code prints the rejection reason and stops. + +If valid, the network accepts the transaction, but it is not confirmed yet. +Since the multisig account requires two cosignatures and only one has been provided, the transaction waits in the + until the missing cosignature arrives. + +### Cosignatory: Cosigning the Pending Transaction + +{{ tutorial.code_snippet_tagged('step-7') }} + +The pending multisig transaction arrives on the channel as a +[TransactionMetaDataPair](../reference/rest/nem.md#model/TransactionMetaDataPair). +For multisig transactions, the `meta` field contains an additional `innerHash` field, holding the hash of the +**inner transaction**, which is the value that a cosignature must reference. + +A cosignatory can have multiple pending multisig transactions awaiting approval. +In this example, the code selects the transaction issued by the multisig account. +This is enough for the tutorial because only one pending transaction is expected from that account. + +In real applications, however, this filter is not enough. +Nothing guarantees that a pending transaction is the expected one, so inspect the content of each pending transaction, +such as its type, recipient, and amount, before selecting the one to cosign. + +!!! warning "Verify before cosigning" + + Always verify the contents of a transaction before cosigning it. + Cosignatures are binding and cannot be undone. + The full multisig transaction is available in the notification's `transaction` field for inspection. + +{{ tutorial.code_snippet_tagged('step-8') }} + +The code then builds a referencing the inner transaction hash and the multisig account +address, signs it with Cosignatory 1's key, and announces it using the endpoint. + +### Cosignatory: Waiting for Confirmation + +{{ tutorial.code_snippet_tagged('step-9') }} + +The announced cosignature does not appear in the as a separate transaction, so it does not trigger +a notification of its own. +Instead, the network attaches it to the pending multisig transaction, which triggers a new notification on the + channel. +Since this update only reflects the addition of a cosignature, the code ignores it. + +If the multisig transaction requires additional cosignatures, it remains in the unconfirmed pool until all required +cosignatures have been collected. +In this tutorial, the second cosignature satisfies the multisig requirements, so the transaction leaves the unconfirmed +pool and, if valid, is confirmed in the next block. + +The confirmation arrives on the channel. +Since both the sender and the recipient of the inner transfer are the multisig account, this notification is delivered +twice, once for each role. +The code prints both notifications, but reports the confirmation only once. + +The block that includes the transaction also triggers a final notification on the +channel with the account's updated state. +Once this final notification arrives, the program moves on to the cleanup step. + +### Cosignatory: Unsubscribing from Channels + +{{ tutorial.code_snippet_tagged('step-10') }} + +After confirmation, Cosignatory 1 unsubscribes from the three channels and ends the STOMP session before the connection +closes. + +## Output + +```text linenums="1" hl_lines="2-4 5 6 7-9 11 12 14 16 18 19" +--8<-- 'devbook/websockets/listen_multisig_transaction_flow.log' +``` + +The output shows: + +* **Accounts** (lines 2-4): The multisig account address and the public keys of both cosignatories. +* **Build** (line 5): Cosignatory 0 builds and signs the multisig transaction. +* **Connection** (line 6): The STOMP session is established over the node's WebSocket endpoint at port `7778`. +* **Subscriptions** (lines 7-9): The three channels, all scoped to the multisig account's address, are subscribed. +* **Registration** (lines 11): The multisig account's current state arrives on the account channel, confirming + the registration. +* **Announcement** (line 12): Cosignatory 0 announces the multisig transaction. +* **Cosigning** (lines 13-14): The pending multisig transaction arrives on the unconfirmed channel with its inner + transaction hash, and Cosignatory 1 announces the cosignature. +* **Confirmation** (lines 15-17): The completed transaction is confirmed in a block. + The notification arrives twice because the inner transfer's sender and recipient are both the multisig account. +* **Account update** (line 18): The block containing the transaction triggers a final account notification. + The balance is reduced by the 0.35 XEM in [fees](../../textbook/transactions.md#fee-schedule), since the + transferred 1 XEM returns to the sender. +* **Unsubscribe** (line 19): The code unsubscribes from the three channels. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +|------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| [Subscribe to the multisig account's channels](#cosignatory-subscribing-to-the-channels) |

| +| [Register the multisig account](#cosignatory-registering-the-multisig-account) | | +| [Handle pending multisig messages](#cosignatory-cosigning-the-pending-transaction) | [TransactionMetaDataPair](../reference/rest/nem.md#model/TransactionMetaDataPair) | +| [Cosign on an unconfirmed notification](#cosignatory-cosigning-the-pending-transaction) | | 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..c81aa0916 --- /dev/null +++ b/mkdocs/pages/en/devbook/websockets/listen-new-blocks.md @@ -0,0 +1,142 @@ +--- +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']) }} + +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`, the default HTTP API port, with `7778`, the default NIS +WebSocket port. + +The program runs until interrupted with `Ctrl+C`, which triggers the unsubscribe step before closing the connection. + +!!! note "Python SockJS helpers" + + There is no SockJS client library for Python, so a few small helper methods are defined at the top of the file + for convenience. + +## 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). + +Notifications are not always evenly spaced. +When the node adds several blocks at once, for example while catching up with its peers, the channel delivers them in +a quick burst, still one notification per block. + +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. + After a rollback, the channel can even report a block with a lower height than one already received, because the + incoming blocks replace blocks that were reported earlier. + + 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..6efa2f5b1 --- /dev/null +++ b/mkdocs/pages/en/devbook/websockets/listen-transaction-flow.md @@ -0,0 +1,230 @@ +--- +title: Transaction Flow +tutorial_level: intermediate +--- + +# 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']) }} + +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`, the default HTTP API port, with `7778`, the default NIS +WebSocket port. + +!!! note "Python SockJS helpers" + + There is no SockJS client library for Python, so a few small helper methods are defined at the top of the file + for convenience. + +## Code Explanation + +### Setting Up the Monitored Address and Signer + +{{ tutorial.code_snippet_tagged('step-1') }} + +This step sets up the address to monitor and the account that sends a transfer to it. + +`MONITOR_ADDRESS` is the address to watch. +The channels this tutorial subscribes to are scoped to this address and notify whenever it is involved in a transaction, +for example as the sender or recipient of a transfer. +The WebSocket API expects the address uppercase and without hyphens. + +`SIGNER_PRIVATE_KEY` is the private key of the account that sends the transfer, which triggers the notifications. + +If any of these environment variables is not provided, the tutorial provides default values. + +### Building and Signing a Transfer Transaction + +{{ tutorial.code_snippet_tagged('step-2') }} + +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 is built the same way as in the +[Transfer XEM](../transactions/transfer-xem.md) tutorial: fetching the network time, creating the transaction, and +signing it. + +Signing the transaction produces its hash, which uniquely identifies it. +The code stores this hash because transaction channel notifications include the transaction hash. +Later, the code compares each received hash with the stored value to identify notifications for this +transaction. + +The transaction is prepared, but it is not [announced](#announcing-the-transaction) yet. +The announcement happens after the channel subscriptions are established, ensuring that the resulting notifications are +not missed. + +### Connecting to the WebSocket + +{{ tutorial.code_snippet_tagged('step-3') }} + +The code opens a SockJS connection to the `/w/messages` endpoint on `WS_URL` and starts a +over it. + +### Subscribing to the Channels + +{{ tutorial.code_snippet_tagged('step-4') }} + +The code subscribes to three address-scoped channels: + +* : Notifies of the account's current state when a involving the account's address is + confirmed. +* : Notifies of a transaction involving the account's address when it enters the + , waiting to be included in a block. +* : Notifies of a transaction involving the account's address when it is included in a + . + +The subscriptions use the IDs `id-0`, `id-1` and `id-2`, which identify them when the code unsubscribes at the end. + +!!! note "Message handling differences" + + In JavaScript, each channel is subscribed with a dedicated handler function, defined in the + [confirmation](#waiting-for-confirmation) step below. + In Python, messages are instead read sequentially from the connection as they arrive. + +All three channels stay silent until the address is registered, which the next step performs. + +### Registering the Account + +{{ tutorial.code_snippet_tagged('step-5') }} + +To receive notifications on an account's channels, the address must first be **registered** with the node. + +The code sends a request to , which registers the address and also forces the node to +send the account's current state on the channel. + +The code waits for this first account notification, which confirms that the registration is active. +The notification follows the [AccountMetaDataPair](../reference/rest/nem.md#model/AccountMetaDataPair) schema. + +The subscription to the account channel stays open for the rest of the run, so the account notification triggered by +the transaction confirmation also appears in the output. + +### Announcing the Transaction + +{{ tutorial.code_snippet_tagged('step-6') }} + +!!! 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 code announces the transaction to the endpoint and checks the result. +If the node rejects it, the code prints the rejection reason and stops. + +### Waiting for Confirmation + +{{ tutorial.code_snippet_tagged('step-7') }} + +If accepted, the code waits for confirmation, printing each message from the subscribed channels. + +Messages from the transaction channels follow the +[TransactionMetaDataPair](../reference/rest/nem.md#model/TransactionMetaDataPair) schema, whose +`meta.hash.data` field holds the transaction hash. +As each message arrives, the code compares that hash against the stored value to recognize this transaction +among the channel notifications. + +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 . + +The block that includes the transaction also triggers a final notification on the +channel. +Unlike the transaction channels, this notification contains the account's updated state rather than a transaction hash, +so it cannot be matched to a specific transaction. + +Once this final notification arrives, the program moves on to the cleanup step. + +### Unsubscribing from Channels + +{{ tutorial.code_snippet_tagged('step-8') }} + +After confirmation, the code unsubscribes from the three channels and ends the STOMP session before the connection +closes. + +## Output + +```text linenums="1" hl_lines="2-14" +--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`. +* **Subscriptions** (lines 4-6): The account channel and both transaction channels are subscribed. +* **Registration** (lines 7-8): The account's current state arrives on the account channel, confirming the + registration. +* **Announcement** (line 9): The transaction is announced and its hash is printed. +* **Transaction flow** (lines 10-11): The transaction moves from `unconfirmed` to `confirmed`, showing the + confirmation lifecycle. +* **Confirmation** (line 12): The hash from the channel matches the announced + transaction. +* **Account update** (line 13): The block containing the transaction triggers a final account notification. + The balance is unchanged, since the transfer amount is zero. +* **Unsubscribe** (line 14): The code unsubscribes from the three channels. + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| [Subscribe to the account channel](#subscribing-to-the-channels) | | +| [Subscribe to the unconfirmed channel](#subscribing-to-the-channels) | | +| [Subscribe to the transactions channel](#subscribing-to-the-channels) | | +| [Register the account](#registering-the-account) | | +| [Handle transaction messages](#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..b0469d1f5 --- /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](../devbook/reference/whitepaper/index.md), 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..1527602c2 --- /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: `#!cats import`, `#!cats using`, `#!cats enum`, and `#!cats struct`. +Each of these is described in the sections below. + +## `#!cats import` + +CATS files can include other CATS files using the `#!cats 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. + +## `#!cats 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: `#!cats uint8`, `#!cats uint16`, `#!cats uint32`, `#!cats uint64` + * Signed: `#!cats int8`, `#!cats int16`, `#!cats int32`, `#!cats int64` +* **Fixed-size binary buffers**: `#!cats binary_fixed(N)` defines an N-bytes long buffer. + +For example, to define a `#!cats Height` type as an 8-byte unsigned integer: + +```cats +using Height = uint64 +``` + +To define a `#!cats PublicKey` type as a 32-byte binary buffer: + +```cats +using PublicKey = binary_fixed(32) +``` + +Although in the following example both `#!cats Height` and `#!cats Weight` are based on `#!cats uint64`, +they are treated as **distinct types** and cannot be used interchangeably: + +```cats +using Height = uint64 +using Weight = uint64 +``` + +## `#!cats enum` + +The `#!cats 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 `#!cats enum` declaration. +Each member must be assigned a constant integer value. + +For example, to define a `#!cats 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 attribute starts with `@` and must appear on the line above the enum declaration. +Currently, the only supported attribute is: + +* `#!cats @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. + +## `#!cats struct` + +The `#!cats 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 `#!cats 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: + +* `#!cats abstract`: defines a base struct for inheritance. + Generators produce a factory to instantiate the appropriate derived type. + +* `#!cats 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: + +* `#!cats make_const(type, value)`: defines a constant. + This field does not appear in the layout. Instead, it becomes a constant accessible as + `#!cats .` in generated code. + + In this example, `#!cats TRANSPORT_MODE` is not serialized, but results in a constant `#!cats Car.TRANSPORT_MODE` + of type `#!cats TransportMode` with value `#!cats ROAD`. + + ```cats + struct Car + TRANSPORT_MODE = make_const(TransportMode, ROAD) + ``` + +* `#!cats 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 `#!cats wheel_count` is stored as a `#!cats uint8` with the fixed value `#!cats 4`. + + ```cats + struct Car + wheel_count = make_reserved(uint8, 4) + ``` + +* `#!cats 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, `#!cats car_size` is an `#!cats uint16` that always contains the size, in bytes, of the field `#!cats car`, + which has type `#!cats 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: + +* `#!cats equals`: include the field if the selector field exactly matches the constant value. +* `#!cats not equals`: include the field if the selector field does not match the constant value. +* `#!cats in`: include the field if the constant is present in the selector field (for bit flags). +* `#!cats not in`: include the field if the constant is not present in the selector field. + +For example, the field `#!cats buoyancy` is only included when `#!cats transport_mode` is equal to `#!cats 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 `#!cats ` 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 `#!cats vehicles` containing `#!cats vehicles_count` elements of + type `#!cats Vehicle`: + + ```cats + struct Garage + vehicles_count = uint32 + vehicles = array(Vehicle, vehicles_count) + ``` + +* The special keyword `#!cats __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 `#!cats @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 + + `#!cats ` must either be: + + * A fixed-size struct, or + * A variable-size struct annotated with its own `#!cats @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: + +* `#!cats @is_byte_constrained`: interprets the array size as a number of bytes instead of element count. +* `#!cats @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 `#!cats not pad_last` qualifier. + +* `#!cats @sort_key(x)`: ensures the array is sorted by the given property. + + For example, this array of `#!cats 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 `#!cats 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 `#!cats Vehicle` into `#!cats 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 `#!cats 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, `#!cats SizePrefixedString` is inlined into `#!cats Vehicle` as `#!cats 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 `#!cats __value__` is renamed to match the name given to the inline (`#!cats friendly_name`). + All other fields are renamed with a prefix and underscore, such as `#!cats size` becoming `#!cats friendly_name_size`. + +### Struct Attributes + +Structures can include attributes that provide hints to code generators or affect layout behavior. +Attributes appear above the `#!cats struct` declaration, starting with `@`. + +CATS supports the following struct-level attributes: + +* `#!cats @is_aligned`: forces all fields to be aligned to their natural boundaries. +* `#!cats @is_size_implicit`: allows the struct to be referenced by a `#!cats sizeof(type, field)` expression. +* `#!cats @size(x)`: declares that the field `x` contains the full size of the struct in bytes. +* `#!cats @initializes(x, Y)`: initializes field `x` with the constant `Y` defined elsewhere. +* `#!cats @discriminator(x [, y...])`: used with `#!cats abstract` structs to select the appropriate derived type when decoding, + based on the indicated properties. +* `#!cats @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 `#!cats ripemd_keccak_256` for backwards compatibility with NEM. + +For example, this links the field `#!cats transport_mode` in `#!cats 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 `#!cats TRANSPORT_MODE` can be defined in any struct that extends `#!cats Vehicle`. + +### Integer Field Attributes + +Integer fields support one attribute: + +* `#!cats @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 `#!cats vehicle_size` and `#!cats 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 `#!cats 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..17fc5bcac --- /dev/null +++ b/mkdocs/pages/en/textbook/mosaics.md @@ -0,0 +1,251 @@ +# 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 typical use of levies is funding the account behind an asset, for example by charging a commission or a royalty on +every transfer. + +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 charged on every transfer.
  • For absolute levies, this value is an exact quantity, in atomic units.
  • For percentile levies, this value is a percentage of the transferred amount, in [basis points](https://en.wikipedia.org/wiki/Basis_point) (10'000 basis points = 100%)
| + +### How Levies Are Charged + +When a transfer transaction includes a mosaic with a levy, the network automatically 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 (and the transaction fee). + +#### Absolute Levy Calculation + +For an absolute levy, the fee is the exact quantity charged on every transfer, expressed in the atomic units of the +levy mosaic. + +For example, with an absolute levy of `10` atomic units paid in the transferred mosaic itself, 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. + +#### Percentile Levy Calculation + +For a percentile levy, the fee is interpreted in basis points, where one basis point is one hundredth of a percent. +E. g. A fee of `100` charges 1%, and a fee of `10'000` charges 100%. + +The levy is calculated from the transferred amount in **atomic units**: + +$$ +\text{levy} = \left\lfloor \frac{\text{fee} \cdot \text{transferred amount}}{\text{10'000}} \right\rfloor +$$ + +The result is the number of atomic units charged in the levy mosaic. +A levy that rounds down to zero is not charged. + +!!! note "The divisibility of both mosaics affects the charge" + + For percentile levies, the network does not convert between the transferred mosaic and the levy mosaic before + applying the percentage. + It calculates the levy from the transferred amount's atomic-unit count, then treats the result as an atomic-unit + count of the levy mosaic. + + This means the configured percentage is exact only at the atomic-unit level. + If the two mosaics have different , the visible amount charged in whole units can look much larger + or smaller than the same percentage of the visible amount sent. + + For example, consider a mosaic with divisibility 2 whose 1% levy is paid in `nem:xem`, with divisibility 6: + + * Sending 50 whole units transfers `5'000` atomic units of the first mosaic. + * The 1% levy is calculated as 1% of `5'000`, so the result is `50`. + * That result is charged as `50` atomic units of `nem:xem`. + * Because `nem:xem` has divisibility 6, `50` atomic units are only 0.00005 XEM, far less than 1% of 50 XEM. + +### Transfer Requirements + +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. + +The levy mosaic must also still exist on the network when the transfer takes place. +If the levy mosaic is [lost](#lifetime), for example because its namespace expired and was not renewed, transfers of +the mosaic with the levy are rejected. + +### Limitations + +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. +It is transferred to a _sink account_, a network account that collects mosaic creation fees. +Since block 3,481,580 on , the network rejects any transaction from the sink account. +As a result, the collected fees are effectively burned. + +!!! 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 part of a mosaic's **definition**. + +Each field has its own rule: + +* **Description**: can be changed at any time. +* **Transferability** and **name**: cannot be changed once set. +* **Divisibility**, **initial supply**, **supply mutability**, and **levy**: can be changed only while the creator holds + the entire mosaic supply. + +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..69eaf4c76 --- /dev/null +++ b/mkdocs/pages/en/textbook/namespaces.md @@ -0,0 +1,142 @@ +# 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. +It is transferred to a _sink account_, a network account that collects namespace creation fees. +Since block 3,481,580 on , the network rejects any transaction from the sink account. +As a result, the collected fees are effectively burned. + +!!! 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..97fc4b8e8 --- /dev/null +++ b/mkdocs/pages/en/textbook/nodes.md @@ -0,0 +1,227 @@ +# 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 a minimum balance and passing automated checks that confirm it is +in sync, up to date, and reachable by other peers. +These checks are designed to reward nodes that improve the network's reliability, not just nodes that are online. + +Enrollment is optional. +Operational details are available in the [Supernode Program guide](../userbook/node/supernode-program.md). diff --git a/mkdocs/pages/en/textbook/transactions.md b/mkdocs/pages/en/textbook/transactions.md new file mode 100755 index 000000000..e3436a248 --- /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 1'000 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..2f3442807 --- /dev/null +++ b/mkdocs/pages/en/textbook/transfer_transactions.md @@ -0,0 +1,229 @@ +# 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 `1'000` 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. +Learn more about converting between atomic and whole units on the [Mosaics](./mosaics.md#divisibility) page. + +The network rejects the transaction if the sender does not hold enough units of any listed mosaic. + +If a listed mosaic has a , the network charges the levy to the sender on top of the transferred quantity and +credits it to the levy recipient. +See [How Levies Are Charged](./mosaics.md#how-levies-are-charged) on the Mosaics page for the rules. + +!!! 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/en/userbook/node/install.md b/mkdocs/pages/en/userbook/node/install.md new file mode 100644 index 000000000..2ff834418 --- /dev/null +++ b/mkdocs/pages/en/userbook/node/install.md @@ -0,0 +1,224 @@ +--- +title: Node Installation +--- + +# Installing the Node Client + +This guide explains how to deploy a NEM node, either [manually](#manual-installation) or [using Docker](#using-docker). + +## Hardware Requirements + +* A machine connected to the internet with approximately 30 GB of disk space for database and log files, and 16 GB of + RAM as of July 2026. + +* For optimum participation in and , the node must have a publicly reachable IP address + and TCP port **7890** must be open for inbound and outbound connections. + + Without a public IP address, your node can still produce new blocks and submit them to the network, + but peer nodes will not be able to notify your node about new blocks and transactions they discover. + In this case, your node resorts to periodically polling its peers, which is slower. + + Additionally, a public IP address is required to participate in the . + +* For applications like wallets to be able to communicate with the network through your node, + the following TCP ports should be open: + + * **7890** for [REST](../../devbook/reference/rest/nem.md) requests. + * **7778** for [WebSockets](../../devbook/reference/websockets/index.md) requests. + +## Manual Installation + +### Prerequisites + +* Install [Java JRE 11](https://docs.oracle.com/en/java/javase/11/) or [OpenJDK 11](https://openjdk.org/projects/jdk/11/). + +The NIS1 client works on any operating system that supports Java, including Linux, Windows, and macOS. + +### Installation + +* [Download the latest binary](https://github.com/NemProject/nem/releases). + +* Unzip the file in the folder that becomes the NIS1 _installation_ folder. + +### Configuration {: #manual-configuration } + +Create a new `nis/config-user.properties` file with the following content, adapted to your case: + +```ini +nem.folder = %h/nem +nis.bootName = my-server +nis.bootKey = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef +``` + +* Set the `nem.folder` property to point to the NIS1 _home_ folder. + This can be different from the installation folder and will store the program's logs and database files. + + Do not use `~` to refer to your user's home folder, use `%h` instead. + The default location is `%h/nem`. + + Backslashes on Windows need to be doubled: `\\`. + For example, `D:\\NEM\\nis1-home`. + +* Set `nis.bootName` to the name you want for your server. + This is merely informational. + Leading and trailing spaces are removed, but the name can contain spaces in the middle. + Avoid using characters outside the ASCII range, even though UTF8 is supported via escaping + (e.g. `\u3053\u3093\u306B\u3061\u306F`). + +* Set `nis.bootKey` to the of the account managing this node. + If you do not have an account yet, use a such as NEM NanoWallet to create one. + + * When performing , this is the private key of the . + This is the **recommended** setup. + + * When performing , this is directly the private key of your account. + This setup is **not recommended**. + + !!! warning "This key must be kept secret at all times" + +* Set `nis.shouldAutoHarvestOnBoot` to `true` if you want the node to harvest. + +!!! note "Speed Up the First Run of the Node" + + You can optionally download a database snapshot to speed up the first run of the node: + + * Go to [https://bob.nem.ninja/](https://bob.nem.ninja/) and download the most recent `nis5_mainnet-*.mv.db` file. + For example, [nis5_mainnet-5-565-850.mv.db.gz](https://bob.nem.ninja/nis5_mainnet-5-565-850.mv.db.gz). + * Unzip the file inside a folder named `nis/data` inside the NIS1 home folder. + Rename the extracted file to `nis5_mainnet.mv.db`. + +!!! example "Running a Testnet Node" + + If you want to run a node, add the following properties to your `config-user.properties` file: + + ```ini + nem.network = testnet + + nis.treasuryReissuanceForkHeight = 1 + nis.treasuryReissuanceForkTransactionHashes = + nis.treasuryReissuanceForkFallbackTransactionHashes = + nis.multisigMOfNForkHeight = 1 + nis.mosaicsForkHeight = 1 + nis.firstFeeForkHeight = 1 + nis.secondFeeForkHeight = 1 + nis.remoteAccountForkHeight = 1 + nis.mosaicRedefinitionForkHeight = 1 + ``` + +### Launch + +Open a terminal and locate the appropriate launch script for your operating system: + +=== "Windows" + + ```bash + runNis.bat + ``` + +=== "Linux" + + ```bash + nix.runNis.sh + ``` + +!!! note "Out of Memory Issues" + + If you encounter memory issues, edit the launch script and + [increase the `-Xmx` parameter](https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE__GUID-98AC4535-A539-406D-9AC5-390C1AF143F0). + +Launch the script. +The console output indicates that the node is running. + +## Using Docker + +These instructions only work for Linux systems, including the Windows Subsystem for Linux. + +### Prerequisites + +* [Docker](https://docs.docker.com/get-docker/). + +* [Git](https://git-scm.com). + +### Installation {: #docker-installation } + +Clone the [nem-docker](https://github.com/NemProject/nem-docker) repository: + +```bash +git clone https://github.com/NemProject/nem-docker.git +cd nem-docker +``` + +### Configuration + +Upon first run, the client asks for the **boot name** and **boot key** properties (described in the manual +[Configuration](#manual-configuration) section above) and stores them. + +If you want to edit these settings manually, before starting the client create a new file called +`custom-configs/nis.config-user.properties` with the content described above. + +### Controlling the Node + +* To start the node: + + ```bash + ./boot.sh + ``` + +* To stop the node: + + ```bash + ./stop.sh + ``` + +For additional commands, read [the nem-docker GitHub project](https://github.com/NemProject/nem-docker). + +## Synchronization + +When the node first starts, it downloads the whole blockchain from its peers. + +**This is a long process that can take up to 48 hours.** + +If you downloaded the optional database snapshot, the node reads the database first and then downloads the remaining +blocks, significantly reducing the synchronization time. + +Meanwhile, you can check: + +* If you have a public IP, a few minutes after launching your node it should appear in the public list + of nodes at [nodewatch.symbol.tools](https://nodewatch.symbol.tools/nem/nodes). + Its reported chain height increases as the node catches up with the rest of the network. + +* You can also ask your node its current chain height by pointing a browser to + [localhost:7890/chain/height](http://localhost:7890/chain/height). + +## Monitoring the Node + +NIS listens for queries on port 7890, so the first way to monitor your node is to point a browser to +[localhost:7890/node/info](http://localhost:7890/node/info). + +If you get any response, even an error such as `NIS_ILLEGAL_STATE_LOADING_CHAIN`, the node is running. + +For the full list of URLs that can be queried, see the [REST API specification](../../devbook/reference/rest/nem.md). + +## Updating a Node + +Updating the NIS1 client to the latest protocol version is straightforward: + +### Manually + +* Stop the server by pressing `Ctrl+C` or killing the process. + +* Remove the old package. + This means all files in the [NIS1 installation folder](#installation) **except** the `config-user.properties`. + Everything in the NIS1 home folder should remain. + +* [Download the latest binary](https://github.com/NemProject/nem/releases) and extract it in the same folder. + +* Start the server again with [the same command used to launch it](#launch). + +### Using Docker + +* Stop the server with `./stop.sh` + +* Update the repository that you cloned in the [Installation](#docker-installation) step with `git pull` + +* Restart the server with `./boot.sh` diff --git a/mkdocs/pages/en/userbook/node/supernode-program.md b/mkdocs/pages/en/userbook/node/supernode-program.md new file mode 100644 index 000000000..f33310444 --- /dev/null +++ b/mkdocs/pages/en/userbook/node/supernode-program.md @@ -0,0 +1,221 @@ +--- +title: Supernode Program +--- + +# Supernode Program + +The rewards public NEM nodes that help secure the network and provide reliable data access for +applications. +Because all was created in the genesis block, NEM has no block subsidy. +A quantity of XEM was set aside for the Supernode Program. + +Reward eligibility is checked daily in four rounds, one round every six hours. +A participating node must pass every test in every round to qualify for that day's reward. + +Testing is performed by a centrally managed Supernode monitoring service called the _controller_. +Some tests compare the participating node with a trusted _reference node_. + +Participating nodes also run the _Node Servant_, a lightweight application on the same machine as the NIS client. +The Servant performs Supernode Program tests and exposes the `/nr/...` endpoints used by some of those tests. + +## Eligibility Tests + +Each testing round includes the following checks: + +* **Chain height:** The node reports its current height through the API. + The test passes if the height is no more than 4 blocks behind the reference node. + +* **Chain part:** The node serves a random group of 60 to 100 recent blocks through the + API. + The controller verifies the block signatures and compares a composite hash of those blocks with the same hash + calculated from the reference node. + +* **Balance:** The node's main account has at least 10,000 XEM. + The controller checks this through the API, routed through the reference node. + +* **Computing power:** The node completes 10,000 repeated public-key derivations from a random hash. + The test passes if the final public key matches the controller's result and the full round trip takes 5 seconds or + less. + +* **Version:** The node reports a NIS client version through the API. + The test passes if the version is at least as recent as the reference node's version. + +* **Ping:** The controller gives the Servant 5 random partner nodes through the `/nr/task/ping` API. + The Servant calls the `/nr/ping` API on each partner 5 times. + The test passes if no more than one ping fails and the average successful round-trip time is less than 200 ms. + +* **Bandwidth:** The controller gives the Servant the fastest partner from the ping test and a random hash seed through + the `/nr/task/bandwidth` API. + The Servant and partner perform the same 30,000-hash calculation. + The test passes if both hash results match and the measured transfer speed is at least 5 Mbit/s. + +* **Responsiveness:** The controller sends 10 requests to the API. + The test passes if at least 9 requests succeed and all requests complete in 1 second or less. + If only the time limit fails, the controller retries this test up to 4 times. + +## Enrolling in the Program + +Before enrolling, make sure you have: + +* [NEM NanoWallet](https://github.com/NemProject/NanoWallet/releases) installed. + +* A NEM account with at least 10,010 XEM. + The program requires 10,000 XEM to participate, plus approximately 10 XEM for delegated harvesting and enrollment + transaction fees. + +* activated on the main account. + The remote account becomes usable after approximately 360 blocks, or about six hours. + Enrollment can start as soon as the is available. + +* A synchronized public node. + Follow the [node installation guide](install.md) if you do not have one yet. + + Make sure the `nis.shouldAutoHarvestOnBoot` property is set to `true` or not set. + +* A stable public IP address or domain name for the node. + +!!! warning "Use the Delegated Private Key" + + Use the delegated private key for the node and Servant configuration. + Do not use the private key of the main account. + + The main private key controls the account's funds. + The delegated private key only controls delegated harvesting and can be replaced if it is compromised. + +To find the delegated private key in NEM NanoWallet, open **Services**, then **Delegated Harvesting**, then +**Manage Delegated Account**. +Select **Show delegated account keys** and enter the wallet password to reveal the delegated private key. + +### Manually + +#### Configuring the Servant + +1. Download the [Node Servant](https://bob.nem.ninja/servant_0_0_4.zip). + +2. Unzip the file in the folder that becomes the Servant installation folder. + It does not need to be the same as the NIS1 installation folder. + Open the `servant` folder. + +3. Open the Servant `config-user.properties` file. + +4. Set `nem.host` to the node's static IP address or domain name. + This value must remain stable so the controller can test the same node. + +5. Set `servant.key` to the delegated private key. + +6. Save the file. + +7. Open inbound and outbound TCP port **7880** so the Servant can receive Supernode Program tests. + +#### Starting the Servant + +Start NIS and let it synchronize before starting the Servant with: + +=== "Windows" + + ```bash + runservant.bat + ``` + +=== "Linux and macOS" + + ```bash + sh startservant.sh + ``` + + Run the Servant in the background with a tool such as `screen` or `nohup`. + +### Using Docker + +These instructions only work for Linux systems, including the Windows Subsystem for Linux. + +#### Configuring the Servant + +If you installed your node using Docker as explained in the [Installation guide for Docker](./install.md#using-docker), +you already have the Servant installed and you only need to configure it. + +Stop the NIS1 client if it is running, as explained in [Controlling the node](./install.md#controlling-the-node). + +Inside the `custom-configs` folder: + +* Copy the `servant.config.properties.sample` file into `servant.config.properties` and edit it. + Provide values for at least the `nem.host` and `servant.key` properties. +* Copy the `supervisord.conf.sample` file into `supervisord.conf` and edit it. + Set `autostart=true` in both the `[program:nis]` and `[program:servant]` sections. + +#### Starting the Servant + +The Servant now starts every time the NIS1 client is started with: + +```bash +./boot.sh +``` + +You can verify it with: + +```bash +./service.sh status +``` + +### Sending the Enrollment + +The monthly enrollment address is announced through the NEM community channels. + +#### Enrolling with NEM NanoWallet + +1. Open **Services**. + +2. Open **SuperNode Program**. + +3. Open **Check & Enroll in Program**. + +4. Select **Enroll in Program**. + +5. Enter the current enrollment address and the node host. + + The host must match the host returned by the node's endpoint. + +6. Send the enrollment transaction. + +#### Enrolling Manually + +Enrolling manually requires sending a transfer transaction to the current enrollment address with the following +unencrypted message: + +```text +enroll +``` + +* `` as returned by the node's endpoint. +* `` as returned by the following query: + + ```text + https://nem.io/supernode/api/codeword/ + ``` + + !!! warning "Use the Public Key" + + * Use the main in the codeword API URL, not the . + + * Use the
of the account enrolled in the program, not the used to + harvest on the node. + + Please note that returns the remote key. + +## Reviewing Test Results + +Review the node's test results on the [Supernode Program page](https://nem.io/supernode/). +Results do not appear immediately after enrollment. + +## Editing the Supernode Host + +If the node's IP address or domain name changes, update the node and Servant configuration. +Then send a new enrollment transaction with the changed host. + +## Monthly Re-Enrollment + +The Supernode Program requires monthly re-enrollment. +Each month has a new enrollment address, announced through the NEM community channels. + +Enrollment for the next month opens 4 days before the end of the current month. +Repeat the [enrollment process](#sending-the-enrollment) each month with the new enrollment address. 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/devbook/reference/whitepaper/index.md b/mkdocs/pages/ja/devbook/reference/whitepaper/index.md new file mode 100644 index 000000000..6be60a8bc --- /dev/null +++ b/mkdocs/pages/ja/devbook/reference/whitepaper/index.md @@ -0,0 +1,17 @@ +--- +title: ホワイトペーパー +hide: +- toc +--- + +# NEMホワイトペーパー + + + + diff --git a/mkdocs/pages/ja/index.md b/mkdocs/pages/ja/index.md new file mode 100644 index 000000000..679bacc93 --- /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/accounts.md b/mkdocs/pages/ja/textbook/accounts.md new file mode 100644 index 000000000..e5feaff80 --- /dev/null +++ b/mkdocs/pages/ja/textbook/accounts.md @@ -0,0 +1,201 @@ +# アカウント + +アカウント +: 暗号資産や [NFT](default:NFT) などのデジタル資産を安全に保管する場所です。 + 従来の銀行における貸金庫に似た役割を果たします。 + +ブロックチェーンでは、アカウントは [キーペア](default:キーペア) によって保護されます。秘密鍵を使うことでのみアカウントから資産を **送金** でき、公開鍵を共有することで自由に **受け取る** ことができます。 + +公開鍵は利便性のため通常 [アドレス](default:アドレス) として共有され、「アカウント」と「アドレス」は同義語として使われます。 + +アカウントはデジタル資産を管理するだけでなく、秘密鍵の所有権を表し、デジタルアイデンティティとしての役割も果たします。 +ブロックチェーン上では、アカウントはトランザクションの承認、権限設定、[コンセンサス](default:コンセンサス) への参加が可能です。 + +!!! note "アカウントのライフサイクル" + + アカウントは、たとえば資産を受け取るなど、ブロックチェーンと初めてやり取りした時点で有効になります。 + 有効になる前は、アカウントに関する情報はチェーン上に記録されず、ブロックエクスプローラーにも表示されません。 + + 一度有効化されたアカウントは資産をすべて引き出すことはできますが、ブロックチェーンから削除することはできません。 + +## ニーモニック {: #mnemonics } + +ニーモニックフレーズ +: [秘密鍵](default:秘密鍵) を人間が読みやすい形で表したもので、通常は12個または24個のランダムな単語のリストとして表示されます。 + +一般的に「ニーモニック」とも呼ばれ、[HDウォレット](default:HDウォレット) でアカウントを作成または復元する際によく使用されます。 + +NEM は、24個の英単語を必要とする [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) 標準に準拠しています。 + +!!! warning "ニーモニックは秘密鍵と同様に扱ってください" + + ニーモニックフレーズにアクセスできると、そこから生成されたすべてのアカウントに完全にアクセスできます。 + 決して共有せず、暗号化されていないデジタル形式で保存しないでください。 + +## ウォレット {: #wallets } + +ウォレット +: NEM アカウントを管理し、[トランザクション](default:トランザクション) を開始して署名するためのアプリケーションです。 + +[秘密鍵](default:秘密鍵) または [ニーモニックフレーズ](default:ニーモニックフレーズ) を保管し、それらを使ってトランザクションに署名します。 +より広い意味では、ブロックチェーンを探索して操作するためのツールを提供します。 + +ウォレットには次の種類があります。 + +* :material-application-outline: **ソフトウェアウォレット** + + デスクトップまたはモバイル端末にインストールするアプリケーションです。 + + 通常はすべての機能を提供しますが、セキュリティリスクは高くなります。 + ブロックチェーンとやり取りするにはソフトウェアウォレットがオンラインになっている必要があり、パスワードで保護されていても、保存された秘密鍵が漏えいする可能性があります。 + +* :material-integrated-circuit-chip: **ハードウェアウォレット** + + 鍵をオフラインで保管する外部デバイスです。 + + 主に安全なトランザクション署名を目的としており、操作にはソフトウェアウォレットに接続する必要があります。 + + 内部にある秘密鍵は、明示的にバックアップする場合を除いてデバイス外に出ないため、非常に高い安全性を持ちます。 + +ほとんどのウォレットでは、複数アカウントの管理、QR コードのスキャン(署名やトランザクション署名の要求)、[マルチシグアカウント](default:マルチシグアカウント) の設定が可能です。 +アカウントは [秘密鍵](default:秘密鍵) または [ニーモニックフレーズ](default:ニーモニックフレーズ) を使ってインポートまたはエクスポートすることもできます。 + +## HDウォレット {: #hd-wallets } + +HDウォレット +: 階層的決定性(HD)[ウォレット](default:ウォレット) で、単一のシードから複数の [アカウント](default:アカウント) を生成します。 + 複数の [キーペア](default:キーペア) を管理するより便利です。 + +複数アカウントの管理が簡単になりますが、シードが侵害されるとそこから導出されたすべてのアカウントが侵害されるため、シードの保護には特に注意が必要です。 +シードは通常 [ニーモニックフレーズ](default:ニーモニックフレーズ) です。 + +ほとんどのウォレットは HD ウォレットです。 + +NEM は [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 標準を使用して、シードからアカウントを生成します。 + +## マルチシグアカウント {: #multisignature-accounts } + +マルチシグアカウント +: トランザクションを承認するために複数の当事者(**連署人**)からの署名を必要とする [アカウント](default:アカウント)(**マルチシグ**と呼ばれます)です。 + +マルチシグアカウントは次のように設定します。 + +* 連署人の一覧を定義する。 +* トランザクションの承認に必要な、合計 **N** 人の連署人のうちの **最小人数 M** を設定する。 + これは **M-of-N** マルチシグと呼ばれます。 + **M** を **N** と同じにすると(**N-of-N** マルチシグ)、すべての連署人の署名が必要になります。 + +たとえば、**2-of-3** マルチシグには3人の連署人がおり、そのうち任意の2人が署名してトランザクションを承認する必要があります。 + +```dot +digraph "M-of-N Multisignature" { + rankdir="BT"; + node [fontsize=12]; + "Multisig Account" [label="マルチシグアカウント\n2 of 3"]; + + "Cosignatory 1" [label="連署人1", penwidth=2]; + "Cosignatory 2" [label="連署人2", penwidth=2]; + "Cosignatory 3" [label="連署人3"]; + + "Cosignatory 1" -> "Multisig Account" [penwidth=2 minlen=2]; + "Cosignatory 2" -> "Multisig Account" [penwidth=2 minlen=2]; + "Cosignatory 3" -> "Multisig Account" [style=dashed minlen=2]; +} +``` + +上の図では連署人1と2が署名しており、最小値 `M=2` を満たすため、連署人3の署名がなくてもトランザクションは有効です。 + +### 使用例 {: #use-cases } + +* **資金または機能の共同管理** + + 設定された人数の連署人の承認なしには、アカウント上で操作を実行できません。 + + これにより、アカウントの1つが侵害されるリスクも軽減できます。 + +* **多要素承認** + + セキュリティ対策として、複数のデバイスからトランザクションを承認する必要があるマルチシグを作成できます。 + +* **アカウント所有権の移転** + + 秘密鍵を移転してアカウントの所有権を変更する方法は、受信者が送信者による鍵のコピーの削除を確認できないため、実用的ではありません。 + + この問題を解決するには、送信者が移転対象のアカウントを 1-of-1 マルチシグに設定し、受信者アカウントを唯一の連署人に設定します。 + + 必要に応じて、単一の連署人を何度でも変更することで、アカウントを再び移転できます。 + +### 制約 {: #constraints } + +マルチシグの仕組みを設計するときは、次の点に注意してください。 + +* **アカウントの連署人の最大数** + + マルチシグアカウントの連署人は最大 **32** 人です。 + +* **連署人の削除には特別なルールがあります** + + 連署人を削除するために、その連署人自身の署名は必要ありません。 + たとえば、**3-of-5** マルチシグでの削除には、残り4人の連署人から少なくとも3人の署名が必要ですが、**5-of-5** マルチシグでの削除には残り4人全員の署名が必要です。 + + 1つのトランザクションで削除できる連署人は **最大1人** です。 + 複数人を削除するには、別々のトランザクションが必要です。 + + 最後に残った連署人は自分自身を削除でき、その場合マルチシグは解消されます。 + +* **入れ子のマルチシグはありません** + + NEM では、マルチシグアカウントを別のマルチシグの連署人にすることはできず、連署人アカウントをマルチシグに変換することもできません。 + したがって、マルチシグの階層は **1層の深さ** だけです。 + +## インポータンス {: #importance } + +インポータンス +: [アカウント](default:アカウント) の [ベスティング](default:ベスティング) 済み残高と、他のアカウントへの送金に基づく、ネットワークへの貢献度の指標です。 + このスコアは、アカウントがブロックをハーベストする可能性を決定します。 + +インポータンスは、[PoW](default:PoW) システムのハッシュレートや [PoS](default:PoS) システムのステークと似た役割を果たします。 +値が高いほど、ブロックをハーベストして報酬を得る可能性が高くなります。 + +### ベスティング {: #vesting } + +ベスティング +: アカウントの [XEM](default:XEM) 残高が _未ベスティング_ から _ベスティング済み_ へ徐々に成熟するプロセスです。 + ベスティング済みの部分だけがアカウントのインポータンスに加算されるため、新たに資金を受け取ったアカウントはすぐにはハーベストを開始しません。 + +アカウントが初めて XEM を受け取った時点では、全額が未ベスティングです。 +60秒の目標時間では約1日にあたる1440ブロックごとに、未ベスティング残高の10%がベスティング済みになります。 +同じ処理が毎日繰り返され、残高のより多くがベスティング済みになります。 + +たとえば次のようになります。 + +* 1日後には、元の残高の10%がベスティング済みです。 +* 2日後には、19%がベスティング済みです。 +* 7日後には、半分を少し超える量がベスティング済みです。 +* 残高は漸近的に全額ベスティングへ近づきます。 + +保有量が多いほど、ベスティング済み XEM 10'000 のしきい値を早く超えます。 +たとえば XEM を 100'000 保有するアカウントは、最初のベスティングサイクル(約1日後)で 10'000 XEM をベスティングし、その時点でハーベスティング資格を得ます。 + +??? info "インポータンスの計算" + + ベスティング済み残高が XEM 10'000 以上あるすべてのアカウントは、ハーベストとインポータンス計算への参加資格を持ちます。 + + 資格を持つアカウントのインポータンススコアは、次の要素を組み合わせます。 + + * **ベスティング済み残高**。 + * 転送トランザクションのグラフから計算した **[PageRank](https://en.wikipedia.org/wiki/PageRank) に似たスコア**。 + + 次の両方を満たす送金だけが考慮されます。 + + * 過去43200ブロック(約30日)以内に発生した。 + * 受取人自身に参加資格がある(ベスティング済み XEM が10'000以上)。 + + 条件を満たす送金はそれぞれ金額を寄与しますが、古い送金ほど寄与は小さくなります(1日あたり10%減)。 + 2つのアカウントが互いに XEM を送った場合は差額だけがカウントされます。 + その差額が少なくとも 1'000 XEM でなければ、スコアには寄与しません。 + + 完全なアルゴリズムは、[NEM Technical Reference](../devbook/reference/whitepaper/index.md) の7章で説明される _Proof-of-Importance_(PoI)方式を参照してください。 + +!!! note + インポータンススコアは359ブロックごと(約6時間ごと)に再計算され、再計算値は次の再計算までのすべての後続ブロックに適用されます。 diff --git a/mkdocs/pages/ja/textbook/blocks.md b/mkdocs/pages/ja/textbook/blocks.md new file mode 100644 index 000000000..c4820d35b --- /dev/null +++ b/mkdocs/pages/ja/textbook/blocks.md @@ -0,0 +1,93 @@ +# ブロック + +ブロック +: 特定の時点で承認された [トランザクション](default:トランザクション) の集合を記録します。 + +ブロックにはトランザクションに加えて、タイムスタンプ、ブロック高、各ブロックを前のブロックにリンクする _前ブロックハッシュ_ などのメタデータが含まれます。 +このリンクがチェーンを _ブロックチェーン_ にします。どのブロックでも改ざんすると、それ以降のすべてのブロックが無効になります。 + +NEM ネットワークは平均して60秒ごとに新しいブロックを1つ生成します。 + +## ネメシスブロック {: #the-nemesis-block } + +ネメシスブロック +: NEM ブロックチェーンの最初のブロックです。 + ネットワークのコンセンサスによって作成される他のすべてのブロックと異なり、ネットワークの作成者が手動で生成します。 + +```dot +digraph Blockchain { + rankdir=LR; + node [shape=box fontsize=12]; + + Nemesis [label="ネメシス"]; + B2 [label="ブロック2"]; + B3 [label="ブロック3"]; + B4 [label="ブロック4"]; + B5 [label="..." shape=plaintext] + + Nemesis -> B2 -> B3 -> B4 -> B5; +} +``` + +ネメシスブロックはブロックチェーンの初期状態を定義します。 +これには、[XEM](default:XEM) などのモザイクの特定アカウントへの初期配布、ネームスペースの作成、ネットワークの基盤となるその他の構成パラメーターが含まれます。 + +チェーンの根本であるため、ネメシスブロックには前ブロックハッシュがありません。 +他のすべてのブロックは、直接または間接的にネメシスブロックへリンクします。 + +このブロックは、他のブロックチェーンプロトコルでは一般に _ジェネシスブロック_ と呼ばれます。 + +後続のすべてのブロックは、他のブロックチェーンにおけるマイニングに相当する NEM の [ハーベスティング](default:ハーベスティング) と呼ばれるプロセスで作成されます。 +ハーベスターはトランザクションを検証し、ブロックにまとめてチェーンに追加し、報酬としてトランザクション手数料を受け取ります。 + +## ネットワーク時刻 {: #network-time } + +ネットワーク時刻 +: NEM が最初のブロック([ネメシスブロック](default:ネメシスブロック))の作成から経過した秒数として定義する時刻です。 + + すべてのタイムスタンプはこの起点を基準に計算されます。 + +UTC タイムスタンプは、ネットワーク時刻をネメシスブロックの UNIX タイムスタンプに加えることで得られます。 +[メインネット](default:メインネット) では `1427587585`(`2015-03-29T00:06:25Z`)です。 +他のネットワークでは、ネットワークプロパティから取得できます。 + +## ブロック構造 {: #block-structure } + +NEM ブロックチェーンの各ブロックには、メタデータとトランザクションデータの組み合わせが含まれます。 + +| **フィールド** | **説明** | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **高さ** | [ネメシスブロック](default:ネメシスブロック) の `1` から始まる、チェーン内でのブロックの位置です。新しいブロックの高さは前のブロックより1つ大きくなります。 | +| **タイムスタンプ** | ネメシスブロックから経過した秒数です。各ブロックで厳密に増加します。ブロック間の平均時間は60秒に近く保たれます。 | +| **タイプ** | ネメシスブロックは `-1`、通常のブロックは `1` です。 | +| **バージョン** | ブロック形式のバージョンとネットワークをエンコードします(メインネットでは `1744830465`、テストネットでは `-1744830463`)。 | +| **前ブロックハッシュ** | 前のブロックの [ハッシュ](default:ハッシュ) です。内容が改ざんされるとこのハッシュが変わり、チェーンが破壊されて後続のすべてのブロックが無効になります。 | +| **署名** | ハーベスターがブロックの内容に対して生成する暗号学的署名です。すべてのノードがブロックの完全性を検証するために使用します。 | +| **署名者** | ブロックに署名するアカウントです。 _ハーベスター_ とも呼ばれます。トランザクション手数料はそのアカウントに入金されますが、リモートアカウントによる [リモートハーベスティング](default:リモートハーベスティング) または [委任ハーベスティング](default:委任ハーベスティング) で署名された場合はメインアカウントに入金されます。 | +| **トランザクション** | ブロックに含まれる有効なトランザクションの一覧です。各トランザクションはブロックに受け入れられる前に個別に検証されます。 | + +## 派生フィールド {: #derived-fields } + +上記のフィールドに加え、各ノードは各ブロックについて次の値を保持します。 +これらはブロックペイロードの一部ではなく、各ノードが以前のブロックから計算します。 + +| **フィールド** | **説明** | +|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **生成ハッシュ** | ブロックからブロックへ引き継がれるハッシュです。次のブロックをハーベストする資格を持つアカウントの判定に使用します。前のブロックの生成ハッシュとハーベスターの公開鍵から計算されます。 | +| **難易度** | 次のブロックをハーベストする難しさを表すネットワーク全体の指標です。平均ブロック時間を60秒に近づけるため、最近のブロック履歴から動的に調整されます。 | +| **レンタル元** | リモートアカウントによる [リモートハーベスティング](default:リモートハーベスティング) または [委任ハーベスティング](default:委任ハーベスティング) でブロックが署名された場合、インポータンスの裏付けとなり報酬を受け取るメインアカウントです。チェーンの前の部分に記録されたリモートアカウントの委任状態から解決されます。 | + +## ブロックスコア {: #block-score } + +コンセンサスプロセスを支援するため、各ブロックについて次の量が計算されます。 + +ブロックスコア +: [ハーベスティング](default:ハーベスティング) の難しさを反映する、各ブロックに割り当てられた数値です。 + +$$ +\textit{block score} = difficulty − \textit{time elapsed since last block} +$$ + +チェーンスコア +: チェーン内のすべての [ブロックスコア](default:ブロックスコア) の合計です。競合する [フォーク](default:フォーク) の選択に使用します。 + スコアの高いチェーンが勝ちます。 diff --git a/mkdocs/pages/ja/textbook/cats.md b/mkdocs/pages/ja/textbook/cats.md new file mode 100644 index 000000000..d2afc0a19 --- /dev/null +++ b/mkdocs/pages/ja/textbook/cats.md @@ -0,0 +1,410 @@ +# CATS DSL + +CATS +: **CATS DSL**(**CATS** は **Compact Affinitized Transfer Schema** というユーモラスな逆頭字語で、**DSL** は **Domain-Specific Language** の略です)は、構造化データのバイナリレイアウトを定義するためのコンパクトで記述的な言語です。 + +もともと Symbol と NEM のために開発され、両プロトコルのすべてのブロックとトランザクションの仕様に使われていますが、設計は十分に汎用的で、任意のバイナリ形式を記述できます。 + +CATS はサイズ効率、性能、厳密な型付けを優先し、可能な場合はゼロコピーのデシリアライズを目指します。 +固定サイズバッファー、厳密な型エイリアス、インライン構造、条件付きフィールドなどの機能があります。 + +CATS 定義は _ジェネレーター_ で処理されます。ジェネレーターは、CATS で定義したバイナリ構造をネイティブ言語の構造へシリアライズ(書き込み)またはデシリアライズ(読み取り)できるように、特定のプログラミング言語のコードを生成するツールです。 + +現在は Python と JavaScript/TypeScript 用のジェネレーターがあり、Java 用は開発中です(2025年6月時点)。 +これらは NEM SDK で使われ、プラットフォーム間で一貫した効率的なバイナリエンコードを保証します。 + +このページでは CATS DSL の構文と機能を説明します。 +完全な精度が必要な場合は、Symbol のソースリポジトリに [Lark 構文解析言語](https://lark-parser.readthedocs.io) で記述された [正確な文法](https://github.com/symbol/symbol/blob/dev/catbuffer/parser/catparser/grammar/catbuffer.lark) があります。 + +!!! note "空白" + + すべての CATS 文は改行で終わります(セミコロンは使いません)が、それ以外では空白は意味を持ちません。 + + 構文解析器にインデントは必要ありませんが、通常は明確さを加えるために使用します。 + +CATS ファイルは、トップレベルの4つのキーワード `#!cats import`、`#!cats using`、`#!cats enum`、`#!cats struct` で構成されます。 +それぞれについて、以下のセクションで説明します。 + +## `#!cats import` {: #cats-import } + +`#!cats import` 文を使うと、CATS ファイルに他の CATS ファイルを含められます。 +これにより、スキーマ定義をモジュール化して再利用できます。 + +別の CATS ファイルをインポートするには、ファイル名を引用符で指定します。 + +```cats +import "other.cats" +``` + +インポートしたファイル名は、構文解析器に渡されたインクルードパスを基準に解決されます。 + +## `#!cats using` {: #cats-using } + +`using` 文は、組み込みプリミティブ型の **型エイリアス** を定義します。 +これらのエイリアスは構文解析器とジェネレーターでは別の型として扱われるため、2つの型が同じ基礎表現を共有していても厳密な型付けが可能です。 + +```cats +using = +``` + +CATS は組み込み型を次の2カテゴリでエイリアス化できます。 + +* **整数型**: + * 符号なし:`#!cats uint8`、`#!cats uint16`、`#!cats uint32`、`#!cats uint64` + * 符号付き:`#!cats int8`、`#!cats int16`、`#!cats int32`、`#!cats int64` +* **固定サイズバイナリバッファー**:`#!cats binary_fixed(N)` は N バイト長のバッファーを定義します。 + +たとえば、8バイトの符号なし整数として `#!cats Height` 型を定義します。 + +```cats +using Height = uint64 +``` + +32バイトのバイナリバッファーとして `#!cats PublicKey` 型を定義します。 + +```cats +using PublicKey = binary_fixed(32) +``` + +次の例では `#!cats Height` と `#!cats Weight` はどちらも `#!cats uint64` に基づきますが、**別の型** として扱われ、相互に入れ替えて使用できません。 + +```cats +using Height = uint64 +using Weight = uint64 +``` + +## `#!cats enum` {: #cats-enum } + +`#!cats enum` 文は、整数型を基礎とする名前付き定数で構成される型、つまり **列挙型** を定義します。 + +各列挙型では基礎型を明示する必要があり、組み込み整数型のいずれかを使用できます。 + +```cats +enum : + = + ... +``` + +列挙型のメンバーは `#!cats enum` 宣言の下の行に定義します。 +各メンバーには定数の整数値を割り当てる必要があります。 + +たとえば、32ビット符号なし整数を基礎型とする `#!cats TransportMode` 列挙型を定義します。 + +```cats +enum TransportMode : uint32 + ROAD = 0x0001 + SEA = 0x0002 + SKY = 0x0004 +``` + +### 列挙型属性 {: #enum-attributes } + +列挙型は動作を変更する属性をサポートします。 +各属性は `@` で始まり、列挙型宣言の上の行に記述する必要があります。 +現在サポートされている属性は次の1つだけです。 + +* `#!cats @is_bitwise`:列挙型がビットフィールド(フラグの集合)を表し、生成コードでビット演算をサポートすることを示します。 + + 例: + + ```cats + @is_bitwise + enum TransportMode : uint32 + ROAD = 0x0001 + SEA = 0x0002 + SKY = 0x0004 + ``` + + これは、ジェネレーターに列挙値をビット単位の OR で結合でき、個々のフラグをビット単位の AND で確認できることを伝えます。 + +## `#!cats struct` {: #cats-struct } + +`#!cats struct` 文は、名前付きフィールドで構成される **構造化バイナリレイアウト** を定義します。 + +構造体は CATS の最も重要な構成要素です。トランザクション、ブロック、その他すべての複合オブジェクトを記述するために使われます。 + +各構造体宣言は、任意で _修飾子_ が前に付く `#!cats struct` キーワードで始まります。 +宣言の後の行で、フィールド名と型を指定してフィールドを定義します。 + +```cats +[Optional modifier] struct + = + ... +``` + +例: + +```cats +struct Vehicle + weight = uint32 + wheel_count = uint8 +``` + +### 修飾子 {: #modifiers } + +CATS は次の修飾子をサポートします。 + +* `#!cats abstract`:継承用の基底構造体を定義します。 + ジェネレーターは、適切な派生型をインスタンス化するファクトリーを生成します。 + +* `#!cats inline`:構造体が合成にだけ使われ、独立した型として出力されないことを示します。 + +修飾子を指定しなければ、構造体はそのまま生成出力に含まれます。 + +### 特別なフィールドコンストラクター {: #special-field-constructors } + +型の代わりに、特別なコンストラクターを使ってフィールドを宣言することもできます。 + +* `#!cats make_const(type, value)`:定数を定義します。 + このフィールドはレイアウトに現れません。代わりに、生成コードで `#!cats .` としてアクセスできる定数になります。 + + 次の例では `#!cats TRANSPORT_MODE` はシリアライズされませんが、`#!cats ROAD` 値を持つ `#!cats TransportMode` 型の `#!cats Car.TRANSPORT_MODE` 定数になります。 + + ```cats + struct Car + TRANSPORT_MODE = make_const(TransportMode, ROAD) + ``` + +* `#!cats make_reserved(type, value)`:固定値を持つ予約フィールドを定義します。 + このフィールドはレイアウトに保存され、常に指定された値になります。 + + 次の例では、フィールド `#!cats wheel_count` が固定値 `#!cats 4` の `#!cats uint8` として保存されます。 + + ```cats + struct Car + wheel_count = make_reserved(uint8, 4) + ``` + +* `#!cats sizeof(type, reference)`:別のフィールドのサイズ(バイト)で自動的に埋められるフィールドを定義します。 + 参照する型を変更してもサイズフィールドを手動で更新する必要がないため、構造体の保守が簡単になります。 + + ここで `#!cats car_size` は、`#!cats Car` 型のフィールド `#!cats car` のサイズ(バイト)を常に保持する `#!cats uint16` です。 + + ```cats + struct SingleCarGarage + car_size = sizeof(uint16, car) + car = Car + ``` + +### 条件付きフィールド {: #conditional-fields } + +別のフィールドの値に基づいて、条件付きで存在するフィールドを作成できます。 +他の言語の共用体に似た、相互排他的なレイアウトを表せます。 + +条件付きフィールドの構文は次のとおりです。 + +```cats + = if +``` + +CATS は次の条件演算子をサポートします。 + +* `#!cats equals`:セレクターフィールドが定数値と完全に一致する場合にフィールドを含めます。 +* `#!cats not equals`:セレクターフィールドが定数値と一致しない場合にフィールドを含めます。 +* `#!cats in`:セレクターフィールドに定数が含まれる場合にフィールドを含めます(ビットフラグ用)。 +* `#!cats not in`:セレクターフィールドに定数が含まれない場合にフィールドを含めます。 + +たとえば、`#!cats transport_mode` が `#!cats SEA` と等しい場合だけ `#!cats buoyancy` フィールドが含まれます。 + +```cats +struct Vehicle + transport_mode = TransportMode + + buoyancy = uint32 if SEA equals transport_mode +``` + +### 配列フィールド {: #array-fields } + +CATS は、すべての要素が同じ型を持つ、静的サイズと動的サイズの両方の配列をサポートします。 + +構文は次のとおりです。 + +```cats + = array(, ) +``` + +`#!cats ` には次を指定できます。 + +* 要素数を固定する定数。 + + ```cats + struct SmallGarage + vehicles = array(Vehicle, 4) + ``` + +* 別のフィールドへの参照。動的サイズの配列になります。 + + たとえば次の構造体は、`#!cats vehicles_count` 個の `#!cats Vehicle` 型要素を含む `#!cats vehicles` フィールドを定義します。 + + ```cats + struct Garage + vehicles_count = uint32 + vehicles = array(Vehicle, vehicles_count) + ``` + +* 特別なキーワード `#!cats __FILL__`。構造体の末尾まで配列を拡張することを示します。 + + この場合、構造体に [下記](#struct-attributes) の `#!cats @size` 属性を付け、合計サイズ(バイト)を保持するフィールドを参照する必要があります。 + + ```cats + @size(garage_byte_size) + struct Garage + garage_byte_size = uint32 + vehicles = array(Vehicle, __FILL__) + ``` + +!!! note + + `#!cats ` には次のいずれかを指定する必要があります。 + + * 固定サイズ構造体。 + * 独自の `#!cats @size` 属性が付いた可変サイズ構造体。 + + それ以外の場合、構文解析器はバイトストリームから読み取る要素数を判断できません。 + +#### 配列フィールド属性 {: #array-field-attributes } + +配列フィールドには、サイズ、アラインメント、ソート方法を制御する属性を付けられます。 + +サポートされる属性には次があります。 + +* `#!cats @is_byte_constrained`:配列サイズを要素数ではなくバイト数として解釈します。 +* `#!cats @alignment(x [, [not] pad_last])`:要素を x バイト境界に揃え、任意で最後の要素にパディングを付けます。 + + デフォルトでは、アラインメントを使うと最後の要素にパディングが付きます。 + `#!cats not pad_last` 修飾子で無効にできます。 + +* `#!cats @sort_key(x)`:指定したプロパティで配列がソートされるようにします。 + + たとえば、次の `#!cats Vehicle` 構造体の配列は weight でソートされます。 + + ```cats + struct Garage + @sort_key(weight) + @alignment(8, not pad_last) + vehicles = array(Vehicle, __FILL__) + ``` + +### インライン {: #inlines } + +`#!cats inline` 修飾子を使うと、ある構造体を別の構造体の中に **インライン化** できます。 +これにより、入れ子にせずに1つの構造体のフィールドを別の構造体へ直接挿入できます。 + +たとえば、次の定義は `#!cats Vehicle` の内容を `#!cats Car` にインライン化します。 + +```cats +struct Vehicle + weight = uint32 + +struct Car + inline Vehicle + max_clearance = Height + has_left_steering_wheel = uint8 +``` + +インライン化されたフィールドはその場所で展開されるため、`#!cats Car` の最終レイアウトは次と同じです。 + +```cats +struct Car + weight = uint32 + max_clearance = Height + has_left_steering_wheel = uint8 +``` + +!!! note "名前付きインライン" + + 構造体は **名前** を付けてインライン化することもでき、その接頭辞でフィールド名が変更されます。 + + ```cats + = inline + ``` + + 次の例では `#!cats SizePrefixedString` を `#!cats friendly_name` として `#!cats Vehicle` にインライン化します。 + + ```cats + struct SizePrefixedString + size = uint32 + __value__ = array(int8, size) + + struct Vehicle + weight = uint32 + friendly_name = inline SizePrefixedString + year = uint16 + ``` + + 次のように展開されます。 + + ```cats + struct Vehicle + weight = uint32 + friendly_name_size = uint32 + friendly_name = array(int8, friendly_name_size) + year = uint16 + ``` + + 特別なフィールド `#!cats __value__` は、インラインに指定された名前(`#!cats friendly_name`)に変更されます。 + それ以外のフィールドは接頭辞とアンダースコアで変更されます。たとえば `#!cats size` は `#!cats friendly_name_size` になります。 + +### 構造体属性 {: #struct-attributes } + +構造体には、コードジェネレーターへのヒントやレイアウト動作への影響を与える属性を含められます。 +属性は `@` で始まり、`#!cats struct` 宣言の上に記述します。 + +CATS は次の構造体レベル属性をサポートします。 + +* `#!cats @is_aligned`:すべてのフィールドを自然な境界に揃えます。 +* `#!cats @is_size_implicit`:構造体を `#!cats sizeof(type, field)` 式で参照できるようにします。 +* `#!cats @size(x)`:フィールド `x` が構造体全体のサイズ(バイト)を保持することを宣言します。 +* `#!cats @initializes(x, Y)`:別の場所で定義された定数 `Y` でフィールド `x` を初期化します。 +* `#!cats @discriminator(x [, y...])`:`#!cats abstract` 構造体で使い、指定したプロパティに基づいてデコード時に適切な派生型を選択します。 +* `#!cats @comparer(x [!transform] [, y...])`:インスタンスのソートまたは比較に使うプロパティを定義します。 + 任意の変換はプロパティ比較の前に適用されます。 + 現在サポートされている変換は、NEM との後方互換性のための `#!cats ripemd_keccak_256` だけです。 + +たとえば、次は `#!cats Vehicle` のフィールド `#!cats transport_mode` を派生構造体に定義された定数へリンクします。 + +```cats +@initializes(transport_mode, TRANSPORT_MODE) +abstract struct Vehicle + transport_mode = TransportMode + +struct Car + TRANSPORT_MODE = make_const(TransportMode, ROAD) + inline Vehicle +``` + +定数 `#!cats TRANSPORT_MODE` は `#!cats Vehicle` を拡張する任意の構造体で定義できます。 + +### 整数フィールド属性 {: #integer-field-attributes } + +整数フィールドは1つの属性をサポートします。 + +* `#!cats @sizeref(x [, y])`:フィールドの値を `x` のサイズに設定し、任意でオフセット `y` を加えます。 + + たとえば、`#!cats vehicle_size` と `#!cats vehicle` の合計サイズを保存します。 + + ```cats + struct Garage + @sizeref(vehicle, 2) + vehicle_size = uint16 + vehicle = Vehicle + ``` + +## コメント {: #comments } + +`#` で始まる行はコメントとして扱われます。 + +宣言の直上にないコメントは構文解析器に無視されます。 +ただし、宣言またはフィールドの直前にコメントを置くと **ドキュメント** として扱われ、生成出力に保持されることがあります。 + +例: + +```cats +# This comment is ignored + +# This comment is included as documentation +# and will be associated with the `#!cats Height` alias. +using Height = uint64 +``` + +この規約により、バイナリレイアウトに影響を与えずにスキーマへインラインドキュメントを追加できます。 diff --git a/mkdocs/pages/ja/textbook/consensus.md b/mkdocs/pages/ja/textbook/consensus.md new file mode 100644 index 000000000..ca5bc4994 --- /dev/null +++ b/mkdocs/pages/ja/textbook/consensus.md @@ -0,0 +1,58 @@ +# コンセンサス + +コンセンサス +: ネットワーク内のすべての [ノード](default:ノード) が、ブロックチェーンの現在の状態について合意するプロセスです。 + +コンセンサスにより、ネットワークは [ブロック](default:ブロック) とその [トランザクション](default:トランザクション) の一貫した単一の時系列を維持し、すべての [アカウント](default:アカウント) に関連する残高とデータを維持します。 + +コンセンサスは、次の2種類の合意を提供します。 + +* **連結の合意**:各ブロックが前のブロックに正しくリンクし、チェーンの履歴の不変性を保証します。 +* **内容の合意**:ブロック内のすべてのトランザクションがネットワークのルールに従います。たとえば、アカウントからトークンを送るには、その [秘密鍵](default:秘密鍵) による有効な署名と十分な残高が必要です。 + +どちらかの合意に違反するブロックは **無効** とされ、正常に動作するノードから無視されます。 +そのようなブロックはネットワークに伝播されません。 + +## 競合 {: #conflicts } + +NEM のような分散型ネットワークでは、[ノード](default:ノード) が一時的に切断されることがあります。 +遅延、接続の問題、ネットワーク構成の変化などが原因です。 + +_ネットワーク分断_ の間、切断されたノードのグループは、すべて有効であっても、最新のブロックについて一時的に意見が分かれることがあります。 + +その結果、一時的に複数のブロックチェーンが存在することがあります。これを _フォーク_ と呼びます。 + +フォーク +: 2つ以上の競合するチェーンが共通の履歴を持ちながら、最新のブロックが異なる状態です。 + +フォーク中は、照会したノードがそのアカウントに影響するすべてのトランザクションを認識しているかどうかにより、異なるノードへの照会が同じアカウントに対して異なる残高を返すことがあります。 + +接続が復旧すると、ノードは同じ高さにある競合するブロックに遭遇し、競合が発生することがあります。 + +2つのノードが同時に新しいブロックを生成した場合にも、フォークが自然に発生することがあります。 + +## 競合の解決 {: #conflict-resolution } + +ノードがフォークを認識すると、NEM は決定論的なルールで解決します。[チェーンスコア](default:チェーンスコア) が最も高いチェーンを正しいものとみなします。 + +スコアの低いフォーク上のノードは、メインチェーンの一部ではなくなったブロックを _ロールバック_ し、より良いチェーンに切り替える必要があります。 + +ロールバック +: ノードがより良いチェーンに切り替える際、通常はフォークの解決後に、最近追加された1つ以上のブロックを破棄するプロセスです。 + +破棄されたブロック内にあり、メインチェーンにまだ存在しないトランザクションは [未承認トランザクションプール](default:未承認トランザクションプール) に戻され、再びブロックに含める前に再検証する必要があります。 + +NEM のロールバックは通常、最新の数ブロックだけに影響する浅くまれなものです。 + +非常に深いチェーン再編成を防ぐため、NEM は _書き換え制限_ を設けています。 + +書き換え制限 +: NEM でロールバックが到達できる最大深度です。**360ブロック**(約6時間)に設定されています。 + +書き換え制限より深いブロックは、代替チェーンに置き換えられません。 +その結果、新しいブロックが上に追加されるにつれて、トランザクションは徐々に実質的な不可逆状態になります。 + +書き換え制限には別の影響もあります。 +切断中のノードが自分のブロックを追加し続けると、別のチェーンが構築されます。 +そのチェーンが書き換え制限を超えて成長すると、戻るには深すぎるロールバックが必要になるため、ノードは自力で再参加できません。 +2つのチェーンは **解決不能なフォーク** となり、オペレーターはノードをメインチェーンに復元して解消する必要があります。 diff --git a/mkdocs/pages/ja/textbook/cryptography.md b/mkdocs/pages/ja/textbook/cryptography.md new file mode 100644 index 000000000..fcc0b7030 --- /dev/null +++ b/mkdocs/pages/ja/textbook/cryptography.md @@ -0,0 +1,137 @@ +# 暗号の基本 + +ここでは、NEMの技術を支える基本的な暗号技術の概念を解説します。 + +## ハッシュ {: #hashes } + +ハッシュ +: 暗号学的ハッシュは、任意のサイズの入力データを固定長の文字列に変換する数学関数 _(ハッシュ関数)_によって生成される文字列のことです。 + +[Keccak](https://keccak.team/keccak.html) や [RIPEMD-160](https://en.wikipedia.org/wiki/RIPEMD) など、複数の関数が存在しますが、いずれも次の共通する特性を持っています。 + +* **決定性**:同じ入力からは常に同じハッシュを生成します。 +* **衝突耐性**:異なる入力から同じハッシュを作ることは極めて困難です。 +* **不可逆性**:ハッシュから元の入力データを復元することはできません。 + +これらの特性により、データの完全性や、真正性の検証、そしてブロックチェーンにおける [ブロック](default:ブロック) の連結が保証されます。 + +NEM は、鍵導出、アドレス生成、署名、ブロックハッシュに **Keccak-256**、**Keccak-512**、**RIPEMD-160** を使用します。 + +!!! warning "NEM は SHA-3 ではなく Keccak を使用します" + + NEM は SHA-3 として最終決定される前の Keccak を採用しました。 + 2つのアルゴリズムは異なるパディングを使用するため、同じ入力に対して異なる出力を生成します。 + そのため、標準の SHA-3 ライブラリでは NEM の署名を検証したり、NEM のアドレスを再生成することができません。 + 代わりに、[Bouncy Castle](https://www.bouncycastle.org/) などの Keccak 実装が必要です。 + + NEM の Java ソースコードではヘルパーメソッド名に `sha3_256` と `sha3_512` を使用していますが、内部ではどちらも `Keccak-*` を呼び出します。 + `sha3_` 接頭辞は歴史的なものであり、最終版の SHA-3 仕様を指すものではありません。 + +## キー {: #keys } + +秘密鍵 +: 非常に長い数値であり、厳重に秘匿すべき情報です。値そのものに意味はなく、第三者に推測されることは想定されていません。 + 通常はランダムに生成され、同じキーが偶然に生成されることはほぼありません。 + + +NEM の秘密鍵は 32 バイト長で、通常は64文字の16進文字列で表されます。 + +公開鍵 +: [秘密鍵](default:秘密鍵) に対応する公開識別子として機能する長い数値です。広く共有できますが、秘密鍵を明かすことなく + その保有を証明するために利用されます。 + + 秘密鍵から数学的に導出されますが、現在の技術では逆算して秘密鍵を求めることは実質的に不可能です。 + +NEM の公開鍵は 32 バイト長で、通常は64文字の16進文字列で表されます。 + +キーペア +: 1組の[秘密鍵](default:秘密鍵) と対応する [公開鍵](default:公開鍵) のセットです。 + 秘密鍵は所有者のみが保持し、公開鍵は誰でも閲覧できます。 + これにより、デジタル署名や暗号化などの安全な処理が可能になります。 + +NEM は次の2箇所でキーペアを使用します。 + +メインキー +: すべての [アカウント](default:アカウント) に紐付く [キーペア](default:キーペア) です。 + 秘密鍵はアカウントの所有者を識別し、資金の送金やトランザクションのアナウンスを含む、アカウントの完全な制御権を付与します。 + +リモートキー +: すべての [リモートハーベスティング](default:リモートハーベスティング) アカウントに関連付けられた [キーペア](default:キーペア) です。 + アカウントの [メインキー](default:メインキー) を公開せずに、ノードがメインキーに紐付くアカウントに代わってハーベストできるようにします。 + +??? warning "キーの安全性" + + いずれのキーペアでも **秘密鍵** は常に秘密に保つ必要があります。 + + ただし、秘密鍵が漏えいした場合の深刻度は、その鍵の用途によって異なります。 + + | キーの種類 | 重大度 | 影響 | + | --- | --- | --- | + | **メインキー** | 🔴 高 | アカウント内の資産が流出する可能性があります。 | + | **リモートキー** | 🟠 中 | 委任元アカウントの資金には影響しません。攻撃者が多数のリモートキーを集めると、相当なハーベスティング能力を得て、ブロックチェーンに追加されるブロックに影響を与える可能性があります。別のリモートアカウントをリンクすれば簡単に取り消せます。 | + +NEM では、秘密鍵と公開鍵の両方が256ビット(32バイト)の整数です。 +公開鍵は [楕円曲線暗号](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) により、 +[Ed25519](https://ed25519.cr.yp.to) を使用して取得されます。Ed25519 は +[ツイステッド・エドワーズ曲線](https://en.wikipedia.org/wiki/Twisted_Edwards_curve) 上で定義されています。 + +## 署名 {: #signatures } + +署名 +: ある [アカウント](default:アカウント) によって文書が承認されたことを証明するデジタル付加情報です。 + +署名はアカウントの [秘密鍵](default:秘密鍵) を使って文書を処理することで生成されます。 +そのため、対応する公開鍵を使えば誰でも署名が文書と一致することを検証できますが、同一の署名を作成できるのは秘密鍵の所有者だけです。 + +NEM のすべてのトランザクションには署名が付与されますが、必要な署名はトランザクションの種類と参加者によって異なります。 +たとえば、単一所有者のアカウントから別のアカウントへ資産を送る場合、送信元アカウントの秘密鍵の署名だけが必要です。 + +一方、[マルチシグアカウント](default:マルチシグアカウント) から資産を送る場合は、マルチシグのしきい値を満たすだけの連署人の承認が必要です。 +したがって、有効とみなされる前に複数の署名を集める必要があります。 + +NEM の署名は512ビット(64バイト)長で、[Ed25519](https://ed25519.cr.yp.to) アルゴリズムを使用します。 +SHA-512 に依存する標準の Ed25519 とは異なり、NEM は **Keccak-512** ハッシュ関数を使用します([NEM は SHA-3 ではなく Keccak を使用します](#hashes) を参照)。 + +## アドレス {: #addresses } + +アドレス +: [公開鍵](default:公開鍵) を便利に短くした形式です。英字と数字だけを使うため、共有しやすくなっています。 + 通常は [アカウント](default:アカウント) と同義語として使われます。 + +公開鍵と秘密鍵はいずれも印刷や共有が難しいバイナリデータですが、アドレスは英数字のみで構成されます。 + +さらに、NEM の鍵には 32 バイトのバイナリデータ、つまり64文字の16進文字が必要です。 +一方、アドレスは40文字だけで済み、長さと実用性のバランスを保っています。 + +NEM では、公開鍵から次の手順でアドレスを取得します。 + +1. 公開鍵に [Keccak-256](https://keccak.team/keccak.html) を適用し、32バイトのハッシュを生成します。 +2. その結果に [RIPEMD-160](https://en.wikipedia.org/wiki/RIPEMD) を適用し、20バイトのハッシュを生成します。 +3. 次を連結して25バイトの **生アドレス** を生成します。 + + * 1バイトのネットワークバージョン: [メインネット](default:メインネット)(`N`)は `0x68`、[テストネット](default:テストネット)(`T`)は `0x98`、[mijinネット](default:mijinnet)(`M`)は `0x60` です。 + * 手順2で得た20バイトの RIPEMD-160 ハッシュ。 + * 入力ミスを検出する4バイトのチェックサム。直前の21バイト(ネットワークバージョン + RIPEMD-160 ハッシュ)に Keccak-256 を適用した結果の先頭4バイトです。 + +4. 生アドレスを [Base32エンコード](https://en.wikipedia.org/wiki/Base32) して40文字の **エンコード済みアドレス** を生成します。 + + 大文字と数字だけを使用するため、エンコード済みアドレスが最も一般的な共有方法です。 + + 例:`NBHK6WHL5TGBMCLVW4RSFMRO4ZYXCJFRAVO2B4FU` + +5. 読みやすくするため、任意で6文字ごとにハイフンを追加し、46文字の **見やすいアドレス** にできます。 + + 例:`NBHK6W-HL5TGB-MCLVW4-RSFMRO-4ZYXCJ-FRAVO2-B4FU` + +!!! note "アドレスは使用されて初めて追跡されます" + + NEM がアドレスとそれに対応する公開鍵を追跡し始めるのは、それらがトランザクションに初めて登場した時点です。 + +## バニティアドレス {: #vanity-addresses } + +通常、キー及びそれに対応する [アドレス](default:アドレス) はランダムに生成されますが、特定のパターンやプレフィックスを含む **バニティアドレス** を作ることもできます。 + +これは、条件を満たすアドレスが生成されるまで [キーペア](default:キーペア) を繰り返し生成する手法です。 +求める文字列が複雑になるほど、より多くの時間と計算が必要となります。 + +バニティアドレスはブランド名や個人の識別などに便利ですが、セキュリティ上の利点はありません。 diff --git a/mkdocs/pages/ja/textbook/glossary.md b/mkdocs/pages/ja/textbook/glossary.md new file mode 100644 index 000000000..22f43b1d0 --- /dev/null +++ b/mkdocs/pages/ja/textbook/glossary.md @@ -0,0 +1,234 @@ +# 用語集 + +AMA +: Ask Me Anything(何でも聞いてください)。公開質疑応答セッションです。 + +AML +: Anti Money Laundering(マネーロンダリング防止)。 + +APAC +: Asia and Pacific region(アジア太平洋地域)。 + +APR +: Annual Percentage Rate(年利)。 + +アービトラージ +: ある市場で資産を購入し、別の市場で販売して市場間の価格差から利益を得ることです。 + +バックランニング +: すでに保留中の `transactionB` より少し低いガス(または手数料)で `transactionA` をブロードキャストし、同じブロック内で `transactionB` の *直後* に `transactionA` がマイニングされるようにすることです。 + +BLS +: [Boneh–Lynn–Shacham](https://en.wikipedia.org/wiki/BLS_digital_signature) 署名は、署名者が本物であることを利用者が検証できる暗号署名方式です。 + +BTC +: Bitcoin(ビットコイン)。 + +CBDC +: Central Bank Digital Currency(中央銀行デジタル通貨)。 + +CEX +: Centralized Exchange(中央集権型取引所)。分散型取引所([DEX](default:DEX))の対義語です。 + +CLI +: Command-Line Interface(コマンドラインインターフェース)。端末コンソール上でキーボードだけを使って操作するプログラムです。 + +CMC +: Coin Market Cap。暗号資産に関する情報を提供するウェブページです。 + +CSD +: Central Securities Deposit(証券集中保管)。 + +DAO +: Decentralized Autonomous Organization(分散型自律組織)。ガバナンスが完全にブロックチェーン上で行われる組織です。 + +Dapp +: Decentralized Application(分散型アプリケーション)。単一のコンピューターではなくブロックチェーン上で動作するアプリケーションです。 + この用語はやや広く使われるため、より一般的にはブロックチェーンを利用するあらゆるアプリケーションも意味します。 + +DDH +: Decisional [Diffie-Hellman](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange)。 + +DD +: Due Diligence(デューデリジェンス)。 + +DeFi +: Decentralized Finance(分散型金融)。Traditional Finance([TradFi](default:TradFi))の対義語です。 + +DEX +: Decentralized Exchange(分散型取引所)。従来の Centralized Exchange([CEX](default:CEX))の対義語です。 + +DoS +: Denial of Service(サービス拒否)。 + 単一の送信元が過剰なリクエストでサーバーまたはネットワークを氾濫させ、リソースを使い果たして正当なトラフィックに応答できなくする攻撃です。 + + 最も一般的な亜種は DDoS(Distributed Denial of Service)攻撃です。複数の送信元が関与し、多くの場合は所有者の知らないうちに侵害されたデバイスを使います。 + +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 +: Ethereum(イーサリアム)。 + +EVM +: Ethereum Virtual Machine(イーサリアム仮想マシン)。 + +FFT +: [Fast Fourier Transform](https://en.wikipedia.org/wiki/Fast_Fourier_transform)(高速フーリエ変換)。 + +フロントランニング +: すでに保留中の `transactionB` より少し高いガス(または手数料)で `transactionA` をブロードキャストし、同じブロック内で `transactionB` の *直前* に `transactionA` がマイニングされるようにすることです。 + これは、フロントランニングで利益を得られる [DeFi](default:DeFi) 市場で重要です。 + +ハードウェアウォレット +: [秘密鍵](default:秘密鍵) を保管し、それで署名を生成するデバイスです。 + 鍵は暗号化メモリに保存されてデバイス外に出ないため、ハードウェアウォレットはアカウントへアクセスする最も安全な方法の1つとみなされます。 + 通常は署名機能だけを提供するため、トランザクションを作成してアナウンスするソフトウェア [ウォレット](default:ウォレット) またはアプリケーションと組み合わせる必要があります。 + +HTLC +: Hashed Time-Lock Contract(ハッシュタイムロックコントラクト)。 + +ICO +: Initial Coin Offering(新規コイン公開)。 + +インフレーション +: 新しい [ブロック](default:ブロック) ごとに新しく発行され、作成した [ノード](default:ノード) に報酬として与えられる少量の [XYM](default:XYM) です。 + インフレーションは2021年3月のネットワーク開始から48時間後に始まり、1ブロックあたり約200 XYMでした。 + 報酬は緩やかな曲線に従って時間とともに徐々に減少し、30年後に1ブロックあたり1 XYMとなり、105年後に完全に消失します。 + +IP +: Intellectual Property(知的財産)。 + +IRS +: Internal Revenue Service(内国歳入庁)。米国に住んでいる人、または米国市民が税金を支払う相手です。 + +KYC +: Know Your Customer(顧客確認)。[AML](default:AML) に関連します。 + +LATAM +: Latin America(ラテンアメリカ、中央アメリカと南アメリカ)。 + +mainnet +: NEM のメインネットワークです。実価値のあるトランザクションが行われ、[テストネット](default:テストネット) と対比されます。 + +MEV +: Miner-Extractable Value または Maximal-Extractable Value(マイナー抽出可能価値または最大抽出可能価値)。マイナーがブロック内のトランザクションを並べ替えて、何らかの利益を得るプロセスです。 + [フロントランニング](default:フロントランニング)、[バックランニング](default:バックランニング)、[サンドイッチ](default:サンドイッチ) を使います。 + +mijinnet +: もともと企業展開を想定した許可型 NEM ネットワークで、公開の [メインネット](default:メインネット) と [テストネット](default:テストネット) とは異なります。 + +NAM +: North America(北アメリカ)。 + +NEM +: New Economy Movement(新しい経済運動)。 + +NFT +: 個々のエンティティをブロックチェーンベースの資産として表す方法である、非代替性 [トークン](default:トークン) です。 + +NIS1 +: [NEM](default:NEM) のブロックチェーンノードの最初のバージョンです。ネイティブ通貨 [XEM](default:XEM) で公開 [メインネット](default:メインネット) を運用します。 + 2015年3月31日に最初にローンチされました。 + +PoC +: Proof of Concept(概念実証)、つまりプロトタイプです(コンセンサスプロトコルではありません)。 + +PoI +: Proof of Importance(Proof-of-Importance、インポータンスの証明)。 + NEM が使用するコンセンサスプロトコルです。 + [PoS](default:PoS) と似ていますが、ステークに加えてアカウントの活動も測定します。 + +PoS +: Proof of Stake(プルーフ・オブ・ステーク)。たとえば Ethereum で使用されるコンセンサスプロトコルです。 + +PoW +: Proof of Work(プルーフ・オブ・ワーク)。たとえば Bitcoin で使用されるコンセンサスプロトコルです。 + +ラグプル +: 暗号資産の開発者がプロジェクトを放棄し、資金を持ち逃げする悪意のある行為です。 + +サンドイッチ +: [DeFi](default:DeFi) で一般的な [MEV](default:MEV) 技法の一種です。 + ネットワーク内の保留中のトランザクションを見つけ、そのトランザクションの *直前*([フロントランニング](default:フロントランニング))と直後([バックランニング](default:バックランニング))に1つずつ注文を置いて挟み込みます。 + +SDK +: Software Development Kit(ソフトウェア開発キット)。特定のプラットフォーム向けアプリケーションの作成を簡単にするソフトウェアライブラリです。 + +シャーディング +: Ethereum の [スケーリングソリューション](https://ethereum.org/en/developers/docs/scaling/#sharding) です。 + +SXDH +: Symmetric External [Diffie-Hellman](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange)。 + +シビル攻撃 +: 単一の攻撃者が多数の偽のアイデンティティやアカウントを作成し、ネットワークまたはコンセンサスプロセスに対して不釣り合いな影響力を得る攻撃です。 + 一般的な対策には [PoW](default:PoW) や [PoS](default:PoS) があり、影響力を希少なリソースに結び付けます。 + +Symbol +: NEM プロジェクトが作成し、2021年3月に [NIS1](default:NIS1) の発展形としてローンチしたブロックチェーンプラットフォームです。 + +testnet +: 開発を目的とする NEM のテストネットワークです。 + テスト用 [XEM](default:XEM) は [フォーセット(英語ソース)](https://github.com/NemProject/nem/blob/225dad4731d80f622d24ee75b6373b65c0f7db1b/mkdocs/pages/en/devbook/accounts/testnet-faucet.md) から自由に取得できるため、このネットワーク上のトランザクションには [メインネット](default:メインネット) のトランザクションと異なり実価値がありません。 + +TLC +: Tender Loving Care(手厚い配慮)。 + +TLS +: ネットワーク上のピア間の通信を暗号化するセキュリティプロトコルです。 + +トークン +: デジタル資産の表現です。 + NEM では [モザイク](default:モザイク) と呼ばれます。 + +TPS +: Transactions Per Second(1秒あたりのトランザクション数)。 + +TradFi +: Traditional Finance(伝統的金融)。Decentralized Finance([DeFi](default:DeFi))の対義語です。 + +USP +: Unique Selling Proposition または Unique Selling Point(独自の販売提案または独自の販売ポイント)。 + 製品を競合他社と区別するために広告で使える特徴です。 + +VPS +: Virtual Private Server(仮想プライベートサーバー)。 + 通常はデータセンターでホストされ、リモートでアクセスでき、従来の物理マシンとして扱える仮想マシンです。 + +VRF +: Verifiable Random Function(検証可能ランダム関数)。 + +XEM +: NEM ブロックチェーンのネイティブ通貨です。 + +XYM +: [Symbol](default:Symbol) ブロックチェーンのネイティブ通貨です。 + +アカウント +: デジタル資産を管理するブロックチェーン上の単位です。 + +アドレス +: 公開鍵を共有するための短い形式です。 + +ブロック +: 特定の時点で承認されたトランザクションの集合です。 + +ハッシュ +: 入力データから生成される固定長の暗号学的値です。 + +メインネット +: 実価値のあるトランザクションが行われる NEM の公開ネットワークです。 + +テストネット +: 開発用の NEM ネットワークです。 diff --git a/mkdocs/pages/ja/textbook/harvesting.md b/mkdocs/pages/ja/textbook/harvesting.md new file mode 100644 index 000000000..fc4a8fa41 --- /dev/null +++ b/mkdocs/pages/ja/textbook/harvesting.md @@ -0,0 +1,147 @@ +# ハーベスティング + +ハーベスティング +: NEM が新しい [ブロック](default:ブロック) をチェーンに追加し、参加する [アカウント](default:アカウント) に報酬を分配するプロセスです。 + [PoW](default:PoW) の **マイニング** や [PoS](default:PoS) の **ステーキング** と似た役割を果たします。 + +新しいブロックはそれぞれ、1つの [ハーベスターアカウント](default:ハーベスターアカウント) に代わって単一の [ノード](default:ノード) が生成します。 +ノードが次のブロックを生成する確率は、そのノードのハーベスターアカウントのインポータンスの合計によって重み付けされます。 + +ハーベスターアカウント +: ハーベスティングに参加するアカウントです。 + インポータンスがブロックを生成する確率を決め、ハーベストした各ブロックの報酬を受け取ります。 + +ブロックに含まれる [トランザクション](default:トランザクション) の手数料は、そのブロックを生成したインポータンスを持つ単一のハーベスターアカウントに全額支払われます。 + +## 資格 {: #eligibility } + +[PoW](default:PoW) のマイニングとは異なり、ハーベスティングに専用ハードウェアは必要ありません。 + +次の条件を満たす [アカウント](default:アカウント) はハーベスティングに参加できます。 + +* [ベスティング](default:ベスティング) 済み残高が 10'000 XEM 以上。 +* 直接または委任を通じて [ノード](default:ノード) に接続されている。 + +アカウントの [インポータンス](default:インポータンス) スコアが、ハーベストできる頻度を決定します。 + +## ハーベスティングのプロセス {: #harvesting-process } + +NEM には、次のブロックをハーベストするノードを決める中央のコーディネーターは存在しません。 +代わりに、すべての [ノード](default:ノード) が、それぞれの [ハーベスターアカウント](default:ハーベスターアカウント) について同じ決定論的な資格確認を実行し、独立して競争します。 + +このため、主に各アカウントの [インポータンス](default:インポータンス) に基づいて _ターゲット_ 値を計算します。 +インポータンスが高いほど、ターゲットも高くなります。 + +ノードは各ハーベスターアカウントについて、候補ブロックの [生成ハッシュ](./blocks.md#derived-fields) から _ヒット_ と呼ばれる数値を計算します。 + +いずれかのハーベスターアカウントがターゲット値未満のヒットを生成すると、ノードは [未承認トランザクションプール](default:未承認トランザクションプール) から候補ブロックを組み立て、ネットワークの他のノードにアナウンスします。 + +他のノードはブロックを検証し、次を確認します。 + +* ブロックの署名が、主張されたハーベスターによるものである。 +* [トランザクション](default:トランザクション) が有効である。 +* ヒットが実際にターゲットより低い。 + +どれかの確認に失敗すると、他のノードは新しいブロックを無視します。 +[コンセンサス](default:コンセンサス) メカニズムにより、ノードは最終的にネットワークの他のノードが合意するブロックを採用します。 + +ブロックが有効なら、他のノードはそれを受け入れ、自分のチェーンのコピーに含めます。 +次のブロック高でこのサイクルが繰り返されます。 + +!!! info "同時ブロック作成" + 複数のノードが同じ高さでブロックを生成することを防ぐ特別な仕組みはありません。 + この場合、異なるノードがチェーン上の同じ高さで異なるブロックを採用するため、ネットワークが一時的に [フォーク](default:フォーク) することがあります。 + + [コンセンサス](default:コンセンサス) メカニズムは、ノードが競合するブロックを認識すると、これらの競合を解決します。 + +??? abstract "ターゲットとヒットの計算" + + * **ターゲット** は各ノードが独立して計算し、特定のアカウントを使って次のブロックをハーベストする可能性を表します。 + 次の3つの要素に依存します。 + + * アカウントの [インポータンス](default:インポータンス) スコア。活動量が多いアカウントや資金の多いアカウントは、より頻繁にハーベストします。 + * ネットワーク全体の **難易度**。最近のブロック生成時間に応じて動的に調整され、一定のブロック生成レートを維持します。 + * 最後のブロックからの **経過時間**。遅延が長いほど、新しいブロックが生成される可能性が高くなります。 + + * **ヒット** はブロックの [生成ハッシュ](./blocks.md#derived-fields) から決定論的に導出されます。 + この生成ハッシュは、前のブロックの生成ハッシュとハーベスターの [公開鍵](default:公開鍵) を組み合わせて計算されたハッシュです。 + したがってヒットは、過去のハーベスターの完全なチェーンと、現在ハーベストを試みる者に依存します。 + + ブロックを有効にするには、ノードのターゲットがヒットより **大きく** なければなりません。 + インポータンスが高いほど、また遅延が長いほどターゲットは増加し、難易度が高いほどターゲットは減少します。 + +## ハーベスティングの方法 {: #harvesting-methods } + +ノード所有者は、簡単さとセキュリティのどちらを優先するかに応じて、[ローカル](#local-harvesting) または [リモート](#remote-harvesting) ハーベスティングを有効にして参加できます。 +ノードを運用していなくても残高要件を満たすアカウントは、[委任ハーベスティング](#delegated-harvesting) によってノードへリンクしてハーベストできます。 + +### ローカルハーベスティング {: #local-harvesting } + +ローカルハーベスティング +: 報酬がハーベスターアカウントに直接送られる [ハーベスティング](default:ハーベスティング) 方法です。 + [ノード](default:ノード) は、マシンに保存したオペレーターの [メインキー](default:メインキー) を使って生成した [ブロック](default:ブロック) に署名します。 + +!!! warning + ハーベスターアカウントは、高い [インポータンス](default:インポータンス) スコアを維持するために相当な残高を保有する必要があります。 + 秘密鍵を常時オンラインのマシンに保存すると、不正アクセス時に全残高が危険にさらされます。 + +ローカルハーベスティングは設定が簡単ですが、これらのセキュリティリスクにより、公開ノードには適していません。 +ほとんどのオペレーターは代わりにリモートハーベスティングを選びます。 + +### リモートハーベスティング {: #remote-harvesting } + +リモートハーベスティング +: ブロック署名を別の [リモートキー](default:リモートキー)(リモートアカウント)に委任しながら、ノードの [インポータンス](default:インポータンス) スコアと報酬をオペレーターの [メインキー](default:メインキー) に結び付ける [ハーベスティング](default:ハーベスティング) の方法です。 + +リモートアカウントは資金を持たず、ハーベスターのメインアカウントに代わってブロックに署名するためだけに存在します。 +その秘密鍵は常時オンラインのマシン上のノード設定ファイルに保存されるため、使い捨てを前提としています。 + +リモートアカウントは _Account Key Link_ トランザクションに署名することで指定され、メインアカウントの [インポータンス](default:インポータンス) がリモートアカウントへ移ります。 +リモートアカウントは360ブロック(約6時間)の待機期間後にブロックへの署名を開始し、別の _Account Key Link_ トランザクションで削除できますが、同じ待機期間が必要です。 + +メインアカウントは引き続きノードのインポータンスを決定し、すべてのブロック報酬を受け取ります。 +ただし、その鍵はオフラインのままで、侵害から守られます。 +簡単にするため、ブロックにはリモートアカウントが署名していても、メインアカウントをハーベスターアカウントと呼びます。 + +この役割分担はハーベスターの資金を強力に保護するため、ほとんどのオペレーターにとってリモートハーベスティングが推奨される方法です。 + +### 委任ハーベスティング {: #delegated-harvesting } + +委任ハーベスティング +: ノードを運用していない資格のあるアカウントが、第三者のノードにハーベスティングを委任できる [ハーベスティング](default:ハーベスティング) の方法です。 + 委任するアカウントの [インポータンス](default:インポータンス) スコアが使われ、ハーベスト報酬は全額そのアカウントが受け取ります。 + +このようなアカウントを _委任者_ または _委任ハーベスター_ と呼びます。 + +委任者 +: 自身の [インポータンス](default:インポータンス) を保持しながら第三者のノードへ [委任ハーベスティング](default:委任ハーベスティング) を委任し、ハーベスト報酬を受け取るアカウントです。 + _委任ハーベスター_ とも呼ばれます。 + +ノードが作業を行っても、委任者は引き続きハーベスターとみなされ、NEM はブロック報酬を全額委任者に支払います。 +この仕組みにより、アカウントは自分のノードを運用せずに報酬を得られます。 + +委任ハーベスティングはリモートハーベスティングと同じリモートアカウント構成を使います。 +委任者はリモートアカウントの [秘密鍵](default:秘密鍵) を第三者のノードに渡し、ノードはそのアカウントをハーベスト対象に追加して委任者に代わってブロックに署名します。 + +ノードがリモートアカウントを受け入れるかどうかはオペレーターの方針によります。 +委任者はリンクする鍵を変更して、いつでもこの関係を取り消せます。 + +リモートハーベスティングと同様に、ブロック署名は委任者以外のアカウントが行うため、委任者の秘密鍵を安全な保管場所から出す必要はありません。 + +!!! info "リモートハーベスティングと委任ハーベスティングの違い" + + どちらの方法も同じリモートアカウント構成を使います。 + 違いは誰がノードを運用するかだけです。リモートハーベスティングではオペレーターが自身のノードを通じてハーベストし、委任ハーベスティングではアカウントが第三者のノードを通じてハーベストします。 + +## 報酬の分配 {: #reward-distribution } + +[ブロック](default:ブロック) がハーベストされると、ハーベスターはブロック内のすべての [トランザクション](default:トランザクション) の手数料合計を受け取ります。 + +[ローカルハーベスティング](default:ローカルハーベスティング) では、ハーベスターが自分のブロックに署名し、報酬を直接受け取ります。 +[リモートハーベスティング](default:リモートハーベスティング) と [委任ハーベスティング](default:委任ハーベスティング) ではリモートアカウントがブロックに署名しますが、報酬はメインアカウントに流れ、リモートアカウントやそれをホストするノードオペレーターには流れません。 + +NEM はハーベスターとノードオペレーターの間でブロック報酬を分割しません。 +他の人のリモートアカウントをホストするノードは、そのブロックから何も受け取りません。 +プロトコルはハーベスターに全額を支払います。 + +委任ハーベスターのホスティングに対してノードオペレーターが補償を受けるかどうかはプロトコルの範囲外で、当事者間の取り決めに委ねられます。 diff --git a/mkdocs/pages/ja/textbook/intro.md b/mkdocs/pages/ja/textbook/intro.md new file mode 100644 index 000000000..482e6db6e --- /dev/null +++ b/mkdocs/pages/ja/textbook/intro.md @@ -0,0 +1,12 @@ +--- +title: ようこそ +--- + +# テキストブックへようこそ + +このテキストブックでは、NEM ブロックチェーンを支える概念を解説します。 + +[ユーザーマニュアル](../userbook/intro.md) と [開発者マニュアル](../devbook/intro.md) には、必要に応じて +適切なテキストブックのページへのリンクが含まれているため、通常はこの本を最初から最後まで読む必要はありません。 + +ナビゲーションメニューを使って、自由にテキストブックを読み進めてください。 diff --git a/mkdocs/pages/ja/textbook/mosaics.md b/mkdocs/pages/ja/textbook/mosaics.md new file mode 100644 index 000000000..7b8e45272 --- /dev/null +++ b/mkdocs/pages/ja/textbook/mosaics.md @@ -0,0 +1,229 @@ +# モザイク + +モザイク +: NEM ブロックチェーン上の資産を表すもので、他のプロトコルでは一般にトークンと呼ばれます。 + 例として、通貨、ライセンス、コレクション、アクセス権、投票権があります。 + +他のプラットフォームのスマートコントラクトベースのトークンとは異なり、NEM のモザイクはプロトコルレベルで直接サポートされ、追加のコーディングなしで使用できます。 + +各モザイクは新しい種類の資産を定義し、その種類に属する個々のトークンは _モザイク単位_ と呼ばれます。 +モザイクは、各単位を交換できるコインのような代替可能資産や、絵画や [NFT](default:NFT) のような各単位が唯一無二である非代替性資産を表せます。 + +モザイクは登録済みの [ネームスペース](default:ネームスペース) の下に存在し、[完全修飾名](#fully-qualified-name) で識別され、親ネームスペースのレンタルから期間を継承します([ライフタイム](#lifetime) を参照)。 + +## 名前 {: #name } + +名前はネームスペース内でモザイクを識別するもので、その中で一意である必要があります。 +次の形式規則に従います。 + +* 小文字、数字、ハイフン `-`、アンダースコア `_`、アポストロフィ `'` だけを含められます。 +* 英字または数字で始める必要があります。 +* 最大 **32文字** です。 + +一度登録したモザイク名は変更できません。 + +## 完全修飾名 {: #fully-qualified-name } + +モザイクの **完全修飾名**(**モザイク ID** とも呼ばれます)は、ネットワーク上の一意の識別子です。 +ネームスペースとローカルモザイク名をコロンで結合し、`:` の形式にします。 + +| 部分 | 定義 | 長さ制限 | +| --------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| **ネームスペース** | `:` の前にある、ドットで区切られた1つのルートネームスペースと最大2つのサブネームスペース | 16文字(ルート)および64文字(各サブネームスペース) | +| **モザイク名** | `:` の後にあるローカル名。 | 32文字 | + +!!! note "コロンは区切り文字にすぎません" + + コロンはネームスペースとモザイク名を区切るものであり、どちらの名前にも含まれません。 + アプリケーションによっては、代わりに `.` または `!` と表示されます。 + +例: + +* `nem:xem`:ネットワーク固有の通貨。 +* `mycompany.tokens:goldcoin`:架空の企業トークン。 + +## 説明 {: #description } + +各モザイクには、資産の目的、由来、利用条件などを記録する最大 **512文字** の自由記述フィールド **説明** を設定できます。 + +## プロパティ {: #properties } + +モザイクには、転送方法と供給量の変化を制御する動作プロパティがあります。 + +### 可分性 {: #divisibility } + +可分性 +: モザイク数量に設定できる小数点以下の桁数を定義します。 + 可分性 `0` のモザイクは不可分で、整数単位でのみ転送できます。 + より大きい値では小数単位を使用できます。 + +たとえば可分性が `2` の場合、1 _全体単位_ を100 _小数単位_(10^2^)に分けられ、`0.01` 刻みでモザイクを扱えます。 + +小数単位は _原子単位_ とも呼ばれます。この例では、1全体単位は100原子単位で構成されます。 + +他の多くのプロトコルでは、この値は固定されています。たとえば Bitcoin は小数点以下8桁、Ethereum は18桁を使用します。 +NEM では、資産の用途に応じて各モザイクが独自の可分性を定義できます。 + +NEM で許可される可分性の最大値は `6` です。 + +### 初期供給量 {: #initial-supply } + +発行時に作成されるモザイク単位の総数を定義します。 + +可分性に関係なく、モザイクの総供給量は **9 × 10^15^** 原子単位を超えられません。 + +[供給量の可変性](#supply-mutability) を有効にしない限り、供給量は固定されます。 + +### 供給量の可変性 {: #supply-mutability } + +作成後にモザイクの総供給量を増減できるかどうかを示します。 +目的の資産ライフサイクルに応じて、モザイク単位を動的に発行または削除できます。 + +総供給量を変更できるのは、モザイクを作成したアカウントだけです。 +変更が影響するのは作成者の残高だけです。 + +* _ミント_(供給量の増加)では、新しい単位が作成され、作成者のアカウントに追加されます。 +* _バーン_(供給量の減少)では、作成者のアカウントから既存の単位が削除されます。 + アカウントの残高が不足している場合、操作は失敗します。 + +### 転送可能性 {: #transferability } + +モザイクをアカウント間で自由に転送できるかどうかを指定します。 +無効にすると、すべての転送で作成者のアカウントが送信者または受信者のいずれかになる必要があります。 + +```dot +digraph "Transferability" { + rankdir="LR"; + node [fontsize=12]; + "Mosaic Creator" [label="モザイク作成者"]; + "Account A" [label="アカウント A"]; + "Account B" [label="アカウント 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="モザイクが\n転送可能な場合のみ"]; + + { rank = same; "Account A"; "Account B"; } +} +``` + +## 徴収手数料(levy) {: #levy } + +徴収手数料(levy) +: モザイクに付加できる任意の手数料です。そのモザイクを転送するたびに、トランザクション手数料に加えて指定アカウントへ支払われます。 + +徴収手数料は、たとえば転送ごとに手数料やロイヤリティを課して、資産を管理するアカウントに資金を提供するために使われます。 + +徴収手数料には次の4つのフィールドがあります。 + +| フィールド | 説明 | +| ------------- | ----------------------------------------------------------------------------------------- | +| **タイプ** | **Absolute**(固定数量)または **Percentile**(転送量に比例)。 | +| **受取人** | すべての転送で徴収手数料を受け取るアカウント。 | +| **モザイク ID** | 徴収手数料が支払われるモザイク。転送対象のモザイクとは異なる場合があります。 | +| **手数料** | すべての転送で課金される徴収手数料用モザイクの数量です。
  • Absolute 徴収手数料では、原子単位で表す正確な数量です。
  • Percentile 徴収手数料では、[ベーシスポイント](https://en.wikipedia.org/wiki/Basis_point)(10'000ベーシスポイント = 100%)で表す転送量に対する割合です。
| + +### 徴収手数料の請求方法 {: #how-levies-are-charged } + +徴収手数料付きのモザイクを転送トランザクションに含めると、ネットワークは通常のトランザクション手数料に加えて、送信者から徴収手数料を自動的に徴収し、徴収手数料の受取人に入金します。 + +徴収手数料は転送量に上乗せされます。受取人は送信された数量全体を受け取り、送信者は転送量と徴収手数料(およびトランザクション手数料)の両方を引き落とされます。 + +#### 絶対徴収手数料の計算 {: #absolute-levy-calculation } + +絶対徴収手数料では、手数料は転送ごとに課金される正確な数量であり、徴収手数料用モザイクの原子単位で表します。 + +たとえば、転送対象のモザイク自体で原子単位 `10` の絶対徴収手数料を支払う場合、原子単位 `1'000` を送ると送信者から合計 `1'010` が引き落とされます。 +受取人には `1'000` が、徴収手数料の受取人には `10` が入金されます。 + +#### パーセンタイル徴収手数料の計算 {: #percentile-levy-calculation } + +パーセンタイル徴収手数料では、手数料をベーシスポイントで解釈します。1ベーシスポイントは1%の100分の1です。 +たとえば手数料 `100` は1%、手数料 `10'000` は100%を課金します。 + +徴収手数料は、転送量の **原子単位** から計算します。 + +$$ +\text{levy} = \left\lfloor \frac{\text{fee} \cdot \text{transferred amount}}{\text{10'000}} \right\rfloor +$$ + +結果は、徴収手数料用モザイクで課金される原子単位の数量です。 +切り捨て後に0になる徴収手数料は課金されません。 + +!!! note "両方のモザイクの可分性が課金額に影響します" + + パーセンタイル徴収手数料では、ネットワークは割合を適用する前に、転送対象モザイクと徴収手数料用モザイクの間で変換を行いません。 + 転送量の原子単位の数から徴収手数料を計算し、その結果を徴収手数料用モザイクの原子単位の数量として扱います。 + + そのため、設定した割合が正確になるのは原子単位のレベルだけです。 + 2つのモザイクの [可分性](default:可分性) が異なる場合、全体単位で表示される課金額は、表示された送信量に同じ割合を適用した場合よりはるかに大きく見えたり、小さく見えたりします。 + + たとえば、可分性が2のモザイクで、1%の徴収手数料を可分性6の `nem:xem` で支払う場合を考えます。 + + * 全体単位50を送ると、最初のモザイクの原子単位 `5'000` が転送されます。 + * 1%の徴収手数料は `5'000` の1%として計算されるため、結果は `50` です。 + * その結果は `nem:xem` の原子単位 `50` として課金されます。 + * `nem:xem` の可分性は6なので、原子単位 `50` はわずか 0.00005 XEM であり、50 XEM の1%を大きく下回ります。 + +### 転送要件 {: #transfer-requirements } + +送信者は、転送対象モザイクの転送量を賄う残高に加え、徴収手数料用モザイクで徴収手数料を賄う残高も持つ必要があります。徴収手数料用モザイクは、転送対象モザイクと異なる場合があります。 + +転送時点で、徴収手数料用モザイクがネットワーク上に存在している必要もあります。 +たとえばネームスペースが失効して更新されず、その結果、徴収手数料用モザイクが [失われた](#lifetime) 場合、その徴収手数料付きモザイクの転送は拒否されます。 + +### 制限事項 {: #limitations } + +徴収手数料は再帰的ではありません。 +徴収手数料の支払いに使うモザイクが独自の徴収手数料を持っていても、2つ目の徴収手数料は適用されません。 + +!!! warning "徴収手数料は確実な課金ではありません" + + 徴収手数料は再帰的ではないため、回避できます。 + + たとえば、モザイク `A` の徴収手数料がモザイク `B` で支払われ、別のモザイク `C` の徴収手数料が `A` で支払われる場合、`C` を転送すると `C` の徴収手数料として `A` が移動しますが、`A` の徴収手数料は発生しません。 + + したがって徴収手数料は、すべての転送に対して強制できる保証ではなく、最善努力の手数料です。 + +## ライフタイム {: #lifetime } + +モザイク自体には期間がありません。 +そのライフタイムは親ネームスペースのライフタイムに結び付いています。 + +* ネームスペースがアクティブな間は、モザイクを転送でき、供給量を変更できます([プロパティ](#properties) の制限を受けます)。 +* ネームスペースが失効するとモザイクは非アクティブになり、転送と供給量の変更は拒否されますが、既存の残高は保持されます。 +* 元の所有者が [猶予期間](./namespaces.md#duration) 中にネームスペースを更新すると、モザイクとその残高を再び使用できます。 + +!!! warning "猶予期間を過ぎたモザイクの喪失は永続的です" + 猶予期間の後、モザイクは永久に失われます。 + モザイクを保有するアカウントは残高にアクセスできなくなり、モザイクを転送できません。 + + 後から同じ名前でモザイクを登録しても、元の資産の復元ではなく新しい資産が作成されます。 + +[ネームスペースの期間](./namespaces.md#duration) で、ネームスペースのレンタルライフサイクルを説明しています。 + +## 作成手数料 {: #creation-fee } + +新しいモザイクの登録には、一度だけ支払う **10 XEM** の _作成手数料_ が必要です。 + +手数料は作成時に支払う必要があり、返金されません。 +モザイク作成手数料を集めるネットワークアカウントである _シンクアカウント_ に送られます。 +[メインネット](default:メインネット) のブロック3,481,580以降、ネットワークはシンクアカウントからのトランザクションを拒否します。 +その結果、集められた手数料は実質的にバーンされます。 + +!!! note "トランザクション手数料と作成手数料" + モザイクの作成にはトランザクションのアナウンスが必要で、関連する手数料もかかります。 + ただし、このトランザクション手数料は通常、作成手数料と比べてわずかです。 + +## モザイクの変更 {: #modifying-a-mosaic } + +作成後、元の作成者はモザイクの **定義** の一部を変更できます。 + +各フィールドには固有のルールがあります。 + +* **説明**:いつでも変更できます。 +* **転送可能性** と **名前**:一度設定すると変更できません。 +* **可分性**、**初期供給量**、**供給量の可変性**、**徴収手数料**:作成者がモザイクの全供給量を保有している間だけ変更できます。 + +[供給量の可変性](#supply-mutability) が有効なら、総供給量を変更(ミントまたはバーン)できます。 diff --git a/mkdocs/pages/ja/textbook/namespaces.md b/mkdocs/pages/ja/textbook/namespaces.md new file mode 100644 index 000000000..974c7064a --- /dev/null +++ b/mkdocs/pages/ja/textbook/namespaces.md @@ -0,0 +1,135 @@ +# ネームスペース + +ネームスペース +: [アカウント](default:アカウント) にレンタルされる登録済みの名前です。その下に定義された [モザイク](default:モザイク) の接頭辞とグループ化に使用します。 + +ネームスペースを使うと、アカウントは `mycompany.tokens` のような意味のある接頭辞の下に関連するモザイクをまとめられます。 +ネットワーク固有の通貨も同じパターンに従います。一般に [XEM](default:XEM) と呼ばれる `nem:xem` は `nem` ネームスペースに属します。 + +ネームスペースを登録するアカウントを _所有者_ と呼びます。 +所有者はその下に定義できるモザイクを制御するため、ネームスペースは名前の構造と所有権の範囲を提供します。 + +ネームスペースは限られたリソースであるため、永久に所有するのではなく一定期間レンタルされますが、レンタルは更新できます。 + +## サブネームスペース {: #subnamespaces } + +NEM のネームスペースは、インターネットのドメイン名と同様の階層構造に従います。 +各名前はドットで区切られた1〜3個の部分で構成されます。たとえば `foo`、`foo.bar`、`foo.bar.baz` です。 + +最初の部分を _ルートネームスペース_ と呼びます。 +追加の部分は _サブネームスペース_ で、ルートの下に個別に登録する必要があります。 + +ルートネームスペース +: 親を持たないネームスペースです。 + サブネームスペースを階層的にまとめるために使用できます。 + +サブネームスペース +: ルートまたは別のサブネームスペースを親とするネームスペースです。 + _子ネームスペース_ とも呼ばれます。サブネームスペースはルートネームスペースの有効期限が切れると失効します([期間](#duration) を参照)。 + +## 名前 {: #name } + +各ネームスペースには、ネットワーク上で識別する一意の名前があり、特定の形式規則に従う必要があります。 + +* 名前には小文字、数字、ハイフン `-`、アンダースコア `_` だけを使用できます。 +* 名前は英数字で始める必要があります。 +* ルート名は最大16文字です。 +* ルート名 `nem`、`user`、`account`、`org`、`com`、`biz`、`net`、`edu`、`mil`、`gov`、`info` はプロトコルによって予約されており、登録できません。 +* サブネームスペース名は最大64文字です。 + +登録後に名前を変更することはできません。 + +## 期間 {: #duration } + +ルートネームスペースを登録すると、約1年間([メインネット](default:メインネット) で525600ブロック)レンタルされます。 +この期間中、所有者は次の操作を行えます。 + +* ネームスペースの下に [モザイク](default:モザイク) を定義する。 +* サブネームスペースを作成する。 +* ルートネームスペースを更新する。 + サブネームスペースを更新する必要はありません。ルートネームスペースと同じ期間を持つためです。 + +登録中の更新は、**有効期限前の最後の43200ブロック**([メインネット](default:メインネット) で約30日)に制限されます。 +更新するたびに、有効期限は前の有効期限からではなく更新ブロックから1年後に設定されます。ネームスペースを複数年分前払いすることはできません。 + +有効期限前に更新されなければ、約30日間(43200ブロック)の _猶予期間_ に入ります。 +この間、ネームスペースは実質的に無効になります。配下に定義されたモザイクは非アクティブになりますが([モザイクのライフタイム](./mosaics.md#lifetime) を参照)、ネームスペースは他の人が登録できる状態にはまだなりません。 + +猶予期間中に更新できるのは元の所有者だけです。 +猶予期間が終わると、ネームスペースは完全に解放され、他の人が登録できるようになります。 + +```dot +digraph "Namespace registration" { + rankdir="LR"; + fontsize=12; + Available [label="ネームスペース\nが\n利用可能"]; + Registered [label="ネームスペース\nが\n登録済み"]; + "Grace Period" [label="\n猶予期間\n "]; + "Available Again" [label="ネームスペース\nが再び\n利用可能"]; + + Available -> Registered [label="登録"]; + Registered -> "Grace Period" [label="有効期限切れ"]; + Registered -> Registered [label="更新"]; + "Grace Period" -> Registered [label="\n更新" constraint=false]; + "Grace Period" -> "Available Again" [label="解放"]; +} +``` + +ネームスペース登録の状態に応じて、次の操作が許可されます。 + +| 操作 | ネームスペース利用可能 | ネームスペース登録済み | 猶予期間 | +| ----------------------------------- | :-----------------: | :--------------------: | :----------------: | +| ネームスペースを登録 | :white_check_mark: | :material-close: | :material-close: | +| サブネームスペースを登録 | :material-close: | :white_check_mark: | :material-close: | +| ネームスペースの下にモザイクを定義 | :material-close: | :white_check_mark: | :material-close: | +| ネームスペースを更新 | :material-close: | :white_check_mark: | :white_check_mark: | + +!!! note "`nem` ネームスペースは失効しません" + + [XEM](default:XEM) を保持する `nem` ネームスペースは恒久的にアクティブで、レンタルと更新のサイクルから除外されます。 + +## レンタル手数料 {: #lease-fee } + +ネームスペースの登録には、ネットワーク通貨([XEM](default:XEM))によるレンタル手数料が必要です。 + +* **ルートネームスペース**:登録または更新ごとに100 XEM。 +* **サブネームスペース**:登録時に一度だけ10 XEM。 + +これは、ネームスペースが限られたグローバルリソースであることを反映し、名前の占有を防ぐのに役立ちます。 + +手数料は登録または更新時に支払う必要があり、返金されません。 +ネームスペース作成手数料を集めるネットワークアカウントである _シンクアカウント_ に送られます。 +[メインネット](default:メインネット) のブロック3,481,580以降、ネットワークはシンクアカウントからのトランザクションを拒否します。 +その結果、集められた手数料は実質的にバーンされます。 + +!!! note "トランザクション手数料とレンタル手数料" + どの種類のネームスペースを登録または更新する場合も、トランザクションをアナウンスする必要があり、別途トランザクション手数料がかかります。 + ただし、このトランザクション手数料は通常、レンタル手数料と比べてわずかです。 + +## 所有権 {: #ownership } + +ネームスペースは、ルートネームスペースを登録したアカウントが制御します。 + +所有者だけが次の操作を行えます。 + +* ネームスペースの下にモザイクを定義する。 +* サブネームスペースを作成する。 +* ルートネームスペースを更新する。 + +ネームスペースの所有権を直接移転することはできません。 +代わりに、たとえば [マルチシグアカウント](default:マルチシグアカウント) を使って所有者アカウントを引き渡すことで制御を移転します。 + +サブネームスペースは常にルートと同じ所有者を共有し、個別には管理できません。 + +## まとめ {: #summary } + +次の表は、ネームスペースに関する主な数値制限をまとめたものです。 + +| 制限 | 値 | 注記 | +| ------------------------------------------- | ---------------------- | -------------------------------------------------- | +| ネームスペース階層の最大深度 | 3レベル | ルート + 最大2個のサブネームスペース | +| ルートネームスペース名の最大長 | 16文字 | | +| サブネームスペース名の最大長 | 64文字 | 各階層に個別に適用 | +| ネームスペース名で使用できる文字 | `a–z`、`0–9`、`-`、`_` | 英字または数字で始める必要があります | +| ルートネームスペースの標準期間 | 525600ブロック | メインネットで約1年 | +| 有効期限後のネームスペース猶予期間 | 43200ブロック | メインネットで約30日 | diff --git a/mkdocs/pages/ja/textbook/nodes.md b/mkdocs/pages/ja/textbook/nodes.md new file mode 100644 index 000000000..f9f3d0dd7 --- /dev/null +++ b/mkdocs/pages/ja/textbook/nodes.md @@ -0,0 +1,208 @@ +# ノード + +ノード +: NEM ソフトウェアを実行し、ピアノードと情報を共有し、受信した [トランザクション](default:トランザクション) を検証し、[コンセンサス](default:コンセンサス) とブロック作成に参加するコンピューターです。 + +ノードはブロックチェーンの中核を成し、十分な数のノードがアクティブである限りネットワークが機能し続けるようにします。 + +誰でも NEM ノードを実行できます。 +オペレーターは、自分のアカウントでブロックを [ハーベスト](default:ハーベスティング) したり、他の人の [委任ハーベスティング](default:委任ハーベスティング) をホストしたり、[スーパーノードプログラム](default:スーパーノードプログラム) の資格を得たりするために実行します。 + +## ノード構造 {: #node-structure } + +すべての NEM ノードは、_NIS_ と呼ばれる同じアプリケーションを実行します。 + +NIS +: NEM Infrastructure Server です。 + ノードのすべての機能を実装する単一の Java プロセスです。 + +NIS は、[エンジン](#engine)、[REST API](#rest-api)、[WebSocket](#websocket) サービス、組み込み [データベース](#database) の4つのパーツで構成されています。 +エンジンは REST API と WebSocket サービスを通じて公開される中核で、データベースはブロックチェーンを保存します。 + +NIS は他のノードやクライアントとデータを交換します。 + +* _他のノード_ はネットワーク上の NIS ピアです。 +* _クライアント_ はウォレット、エクスプローラー、アプリケーションなどの外部プログラムです。 + +```dot +digraph NemNode { + layout=neato; + splines=ortho; + node [shape=box]; + edge [penwidth=1.5 dir=both]; + + // Layer labels + LblExt [label="外部" shape=plain pos="-1.7,6!"]; + LblInt [label="インターフェース" shape=plain pos="-1.7,4!"]; + LblProc [label="処理" shape=plain pos="-1.7,2!"]; + LblStor [label="ストレージ" shape=plain pos="-1.7,0!"]; + + // External actors + OtherNodes [label="他のノード" style=dashed fixedsize=true width=2 height=0.8 pos="1,6!"]; + Clients [label="クライアント" 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="エンジン" style=filled fixedsize=true width=6 height=0.9 pos="3,2!" URL="#engine"]; + H2 [label="ブロック(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="リクエスト" shape=plain pos="1.6,3.0!"]; + evtLbl [label="イベント" shape=plain pos="4.5,3.0!"]; + rwLbl [label="読み取り / 書き込み" shape=plain pos="4,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 } + +エンジンはブロックチェーンの処理を行います。受信データを検証し、[コンセンサス](default:コンセンサス) と [ハーベスティング](default:ハーベスティング) を実行し、[ピアツーピア通信](#peer-to-peer-communication) を処理し、[未承認トランザクションプール](default:未承認トランザクションプール) を維持します。 + +エンジンは内部コンポーネントであり、直接は公開されません。 +ピアまたはクライアントからのすべての受信リクエストは、下記の REST API を通り、エンジンへ渡されます。 + +### REST API {: #rest-api } + +ピアとクライアントは、単一の HTTP API を通じて NIS にアクセスします。 + +* _ピアリクエスト_ は、ブロック同期、トランザクション中継、ノード検出を処理します。 +* _クライアントリクエスト_ は、ブロックチェーンデータの読み取りと [トランザクション](default:トランザクション) の送信を処理します。 + +API は、JSON とバイナリの2つのエンコードをサポートします。リクエストのコンテンツタイプで選択できます。 +ピアはバイナリでデータを交換し、クライアントは通常 JSON を使います。 + +### WebSocket {: #websocket } + +NIS は組み込みの [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) サービスを通じて、ブロックとトランザクションのイベントを公開します。 +購読したクライアントはポーリングせずにリアルタイムで通知を受け取ります。 + +### データベース {: #database } + +NIS は [H2](https://www.h2database.com) リレーショナルデータベースにブロックチェーンを保存します。_組み込み_ とは、別のデータベースサーバーではなく NIS プロセス内で実行されるという意味です。 + +データベースが保持するのはチェーンの全てのブロックとその中に含まれるトランザクションのみです。 +アカウント残高や [インポータンス](default:インポータンス) スコアなど、現在のブロックチェーン状態は保存しません。 +NIS はその状態をメモリに保持し、起動時に [ネメシスブロック](default:ネメシスブロック) からチェーンを再生して再構築します。そのため、ノードは起動後しばらく利用できません。 + +## ピアツーピア通信 {: #peer-to-peer-communication } + +NEM ノードは分散型のピアツーピア方式で相互に直接通信します。 +中央のコーディネーターはなく、各ノードが他のノードの一部と接続を確立して分散ネットワークを形成します。 + +ノードは既知のピア一覧を共有するため、新しく接続したノードは他のノードを素早く検出してネットワークに統合できます。 +このプロセスは強固な接続性を確保し、個々のノードがオフラインになってもネットワークの回復力を保ちます。 + +```dot +graph P2PNetwork { + layout=circo; + mindist=0.5; + node [style=filled]; + edge [dir=both len=1]; + + N1 [label="ノード1"]; + N2 [label="ノード2"]; + N3 [label="ノード3"]; + N4 [label="ノード4"]; + N5 [label="ノード5"]; + N6 [label="ノード6"]; + N7 [label="ノード7"]; + N8 [label="ノード8"]; + + // Random peer-to-peer connections + N1 -- N2 -- N3 -- N4 -- N5 -- N6 -- N7 -- N8; + N1 -- N5; + N2 -- N6; + N4 -- N1; + N8 -- N3; +} +``` + +ブートストラップを容易にするため、_事前信頼済み_ ピアの初期一覧が [NIS](default:NIS) に組み込まれています。 +これにより、新しいノードは最初の接続を行い、他のノードの検出を始められます。 + +### ノードの評判 {: #node-reputation } + +NEM のような分散型ネットワークでは、ノードはどのピアを信頼し接続を維持するかを決める必要があります。 +静的なホワイトリストや手動で管理した接続に頼る代わりに、NEM ノードは _評判_ システムを使い、時間経過に伴う観測動作に基づいてピアを動的に評価し順位付けします。 + +各ノードは、通信の成功、応答時間、受信データの有効性などの指標を使って、独立して評判を計算します。 +正しく動作し一貫して応答するノードには高いスコアが与えられます。 +無効なデータを送る、応答しない、その他の不正動作をするノードは、ペナルティまたは一時的なブラックリスト登録の対象になります。 + +新しい接続を確立する必要がある場合、ノードは利用可能なピアから、過去のやり取りに基づく評判の高いピアを優先して選択します。 + +組み込みの事前信頼済みピアはこの選択で重く評価されるため、他のピアより頻繁に選ばれ、ネットワークの信頼できる基点として機能します。 +ただし、その動作は他のピアと同じように評価されるため、不正動作をする事前信頼済みピアは評判を失います。 + +評判スコアはローカルです。 +各ノードは直接的な経験だけからネットワークの独自の評価を作り、その評価をメモリだけに保持します。 +再起動後、ノードは獲得した評判を保持せず、新たな相互作用から再構築します。 + +実装は [EigenTrust++](https://en.wikipedia.org/wiki/EigenTrust) アルゴリズムに基づきます。 + +### ノードのローテーション {: #node-rotation } + +孤立したノードグループや停滞したノードグループの形成を防ぐため、ノードは常に同じピアと通信するわけではありません。 +通信するピアを選ぶたびに、評判で重み付けしたランダムな抽出を行います。 +スコアの高いピアほど選ばれやすくなりますが、選択は確率的なままです。 + +このランダム性により、ノードは異なるピアを循環し、ネットワークの分断を避け、長期的な分散化を促進します。 + +## スーパーノード {: #supernodes } + +スーパーノードは _スーパーノードプログラム_ に登録されたノードです。 + +スーパーノードプログラム +: 信頼できる公開ノードに報酬を与える、チェーン外のコミュニティ資金によるプログラムです。 + +NEM にはブロック報酬もインフレーションもありません。 +ノードへの報酬はトランザクション手数料からのみ支払われ、取引量が少ない時期にはその金額が少なくなってしまうことがあります。 +スーパーノードプログラムは、信頼性が証明されたノードに毎日の報酬を支払うことでこれを補います。 + +!!! warning "スーパーノードの報酬は保証されません" + 報酬額はいつでも減額または廃止される可能性があります。 + +プログラムは完全にチェーン外で実行され、NIS 自体は関与しません。_コントローラー_ と呼ばれる別の中央運営サービスが参加ノードをテストし、報酬を支払います。 + +ノードは、最低限の [XEM](default:XEM) 残高を保有し、同期済みで最新状態にあり、他のピアから到達可能であることを確認する自動チェックに合格すると、その日の報酬資格を得ます。 +これらのチェックは、オンラインであるだけでなく、ネットワークの信頼性を高めるノードに報酬を与えることを目的としています。 + +登録は任意です。 +運用の詳細については、 [スーパーノードプログラムガイド](../userbook/node/supernode-program.md) を参照してください。 diff --git a/mkdocs/pages/ja/textbook/transactions.md b/mkdocs/pages/ja/textbook/transactions.md new file mode 100644 index 000000000..f2ccf81c6 --- /dev/null +++ b/mkdocs/pages/ja/textbook/transactions.md @@ -0,0 +1,292 @@ +# トランザクション + +トランザクション +: トランザクションは、ある [アカウント](default:アカウント) から別のアカウントへの資金移動や、新しいモザイクの登録など、NEM ブロックチェーン上で実行する操作を表します。 + +これらの操作は署名済みメッセージで表現され、ネットワークにアナウンスされます。 +ネットワーク内の [ノード](default:ノード) はそれを検証し、受け入れた場合はトランザクションをブロックに含め、ブロックチェーンの状態を更新します。 + +## 基本的なトランザクションの種類 {: #fundamental-transaction-types } + +NEM は、基本トランザクションとマルチシグトランザクションという2つの中核的なトランザクション種類をサポートします。 + +```dot +digraph "Fundamental Transaction Types" { + node [fontsize=12]; + Transaction [label="トランザクション"]; + Basic [label="基本" URL="#basic-transactions"]; + Multisig [label="マルチシグ" URL="#multisig-transactions"]; + + Transaction -> Basic; + Transaction -> Multisig; +} +``` + +### 基本トランザクション {: #basic-transactions } + +基本トランザクション +: 基本 [トランザクション](default:トランザクション) は、単一のアカウントが開始する単一の操作を表し、そのアカウントの [署名](default:署名) だけを必要とします。 + +アカウントからの資金移動や新しい [ネームスペース](default:ネームスペース) の登録などが例です。 + +### マルチシグトランザクション {: #multisig-transactions } + +マルチシグトランザクション +: マルチシグトランザクションは、[マルチシグアカウント](default:マルチシグアカウント) に代わって発行された単一の [内部トランザクション](default:内部トランザクション) を包み、ブロックに含める前に設定された人数の連署人の署名を必要とします。 + +マルチシグトランザクションは1人の連署人が開始しますが、有効にするには他の連署人から追加の署名が必要です。 + +連署 +: トランザクションが複数アカウントの署名を必要とする場合、その追加署名を _連署_ と呼びます。 + +NEM では、各連署は _マルチシグ連署_ トランザクションとして個別に送られ、ハッシュによって [内部トランザクション](default:内部トランザクション) を参照します。 +これにより、連署人は独立して異なるタイミングで署名できます。 +そのため、複数の連携した操作は別々のマルチシグトランザクションとして、内部トランザクションごとに1つずつ発行する必要があります。 + +これらの連署は [未承認トランザクションプール](default:未承認トランザクションプール) 内の保留中のマルチシグトランザクションに蓄積され、必要なしきい値を満たすだけの連署を集めた後にだけブロックに取り込まれることができます。 +マルチシグトランザクションと連署は、単一の単位としてアトミックにまとめて承認されます。 + +マルチシグトランザクションがブロックに含まれると、マルチシグアカウントがトランザクションに関係するすべての手数料を支払います。内部トランザクションの手数料、マルチシグトランザクションの手数料、各連署の手数料です。 +連署人は連署時に自分の残高を使いません。 + +!!! tip "マルチシグトランザクションの例" + + 金庫アカウント `T` は、連署人 `C1`、`C2`、`C3` が管理する 2-of-3 マルチシグです。 + 供給業者 `S` に支払うため、`C1` は `T` から `S` への転送を包むマルチシグトランザクションをアナウンスします。 + `C2` が連署を送信すると、2-of-3 のしきい値を満たします。 + `C3` は署名する必要がありません。 + しきい値に達すると転送が実行され、`S` が資金を受け取ります。 + + ```dot + digraph { + rankdir="LR"; + fontsize=12; + compound=true; + node [fontsize=12]; + + C1 [label="C1"]; + C2 [label="C2"]; + C3 [label="C3"]; + + subgraph clusterMultisig { + label = "マルチシグトランザクション"; + fontsize = 12; + style = dashed; + T [label="T\nマルチシグアカウント\n2 of 3"]; + S [label="S"]; + T -> S [label="転送"]; + } + + C1 -> T [label="署名" lhead=clusterMultisig minlen=2 labelfloat=true]; + C2 -> T [label="連署" lhead=clusterMultisig minlen=2 labelfloat=true]; + C3 -> T [style=dashed lhead=clusterMultisig minlen=2]; + } + ``` + +### 内部トランザクション {: #inner-transactions } + +内部トランザクション +: [マルチシグトランザクション](default:マルチシグトランザクション) に包まれた [基本トランザクション](default:基本トランザクション) を _内部トランザクション_ と呼びます。 + +内部トランザクションは、次の違いを除いて基本トランザクションと同じように動作します。 + +* 個別には署名されません。 + マルチシグトランザクションには開始した連署人が署名し、追加の連署人は別のマルチシグ連署トランザクションを通じて承認します。 + +* それ自体をマルチシグトランザクションにすることはできません。 + マルチシグ階層は1層だけです。 + +* 独自の手数料と期限フィールドを保持します。 + 内部トランザクションの手数料は、マルチシグトランザクションの手数料および各連署の手数料とともにマルチシグアカウントに請求されます。 + +## トランザクションのライフサイクル {: #transaction-lifecycle } + +NEM の各トランザクションは、クライアントによる作成からネットワークによる承認まで、6つの段階を進みます。 + +```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. トランザクションを作成して署名", URL="#1-creation-and-signature"]; + Announcement [label="2. トランザクションをノードにアナウンス", URL="#2-announcement"]; + Validation [label="3. 有効か?", shape=diamond, style="", URL="#3-validation"]; + Propagation [label="4. 他のノードへ伝播", URL="#4-propagation"]; + Harvesting [label="5. ブロックに含める", URL="#5-harvesting"]; + Confirmation [label="6. 承認済みか?", shape=diamond, style="", URL="#6-confirmation"]; + Confirmed [label="承認済み"]; + + Rejection1 [label="拒否" style="rounded,dashed"]; + Rejection2 [label="拒否" style="rounded,dashed"]; + + Creation -> Announcement; + Announcement -> Validation; + Validation -> Propagation [label=" はい", labelfloat=true]; + Propagation -> Harvesting; + Harvesting -> Confirmation; + Confirmation -> Confirmed [label=" はい", labelfloat=true]; + + Validation -> Rejection1 [label="いいえ", style=dashed, minlen=2]; + Confirmation -> Rejection2 [label="いいえ", style=dashed, minlen=2]; + + { rank = same; Validation; Rejection1 } + { rank = same; Confirmation; Rejection2 } +} +``` + +### 1. 作成と署名 {: #1-creation-and-signature } + +通常はアプリであるソフトウェアクライアントがトランザクションを作成し、すべてのパラメーターを入力します。 +たとえば、転送トランザクションには送信元 [アカウント](default:アカウント)、宛先アカウント、金額が必要です。 + +この段階でトランザクションへの署名も行います。 +アカウントの [秘密鍵](default:秘密鍵) の保有者だけが有効な署名を生成できるため、署名は署名アカウントがトランザクションを承認したことを証明します。 + +マルチシグトランザクションでは、開始する連署人が内部トランザクションを包むマルチシグトランザクションに署名します。 +他の連署人は、マルチシグ連署トランザクションを通じて別々に連署を提供します。 + +### 2. アナウンス {: #2-announcement } + +クライアントアプリケーションは、ネットワーク上で接続された [ノード](default:ノード) にトランザクションを送信します。 + +マルチシグトランザクションの場合、連署は別のマルチシグ連署トランザクションとしてアナウンスされ、それぞれの署名者が独立して送信します。 + +### 3. 検証 {: #3-validation } + +ノードはトランザクションの形式が正しく、有効な署名を含むことを確認します。 +マルチシグトランザクションでは、参照された連署がマルチシグアカウントの有効な連署人によるものかどうかも検証します。 + +一部のトランザクション種類には追加の意味検証が必要です。 +たとえば転送トランザクションでは、送信元アカウントに十分な資金があることを確認します。 + +どれかの確認に失敗すると、トランザクションは拒否され、それ以上伝播されません。 +すべての確認に合格すると、処理が続きます。 + +### 4. 伝播 {: #4-propagation } + +ノードがトランザクションを有効と判断すると、ネットワーク内の他のピア [ノード](default:ノード) にブロードキャストし、各ノードの _未承認トランザクションプール_ に追加します。 + +未承認トランザクションプール +: ブロックに含まれるのを待つ検証済みトランザクションの一覧で、ネットワーク内の各ノードが保持します。 + +伝播されたトランザクションをピアが受信すると、他のノードの検証を信頼しないため、自分のプールに追加する前に完全な検証を再実行します。 +トランザクションが検証に合格すれば、ピアは自分のピアへ転送し、伝播がネットワーク全体に広がるまで続きます。 + +!!! warning "未承認トランザクションを信頼しないでください" + + 未承認トランザクションプール内のトランザクションは、まだブロックに含まれる保証がありません。 + [承認済み](#6-confirmation) になり、理想的には [書き換え制限](default:書き換え制限) を超えるまで、最終状態として扱わないでください。 + +マルチシグトランザクションの場合、マルチシグトランザクションと付随するマルチシグ連署トランザクションは独立して伝播します。 + +### 5. ハーベスティング {: #5-harvesting } + +未承認トランザクションプールに入ったトランザクションは、[ハーベスティング](default:ハーベスティング) プロセスによってブロックに含められますが、含まれることは保証されません。 +期限が切れるか、競合するトランザクションが先に承認されると、トランザクションは破棄されます。 + +マルチシグトランザクションの場合、ハーベスターは、マルチシグアカウントの署名しきい値を満たす数の連署が集まるまで、トランザクションをブロックに含めません。 +先に期限が切れると、マルチシグトランザクションと蓄積された連署はプールから破棄されます。 + +### 6. 承認 {: #6-confirmation } + +新しく作成されたブロックは他のノードに伝播され、検証後に受け入れまたは拒否されます。 +[コンセンサス](default:コンセンサス) メカニズムにより、ネットワーク上のすべてのノードが最終的に同じブロックに合意します。 +トランザクションを含むブロックがコンセンサスによって受け入れられると、そのトランザクションは _承認済み_ となります。 + +ノードがすでに受け入れたブロックが後にネットワークの大多数から拒否され、[ロールバック](default:ロールバック) されることがあります。 +この場合、ブロックのトランザクションは元に戻され、未承認トランザクションプールに戻されます。 + +NEM は、ロールバックがどこまで遡れるかを [書き換え制限](default:書き換え制限) で制限します。 + +トランザクションが未承認トランザクションプールにある間に期限切れになると、プールから破棄されます。 +たとえば、提示されたトランザクション手数料が低すぎて、どのハーベスターにも含められない場合に起こります。 + +## 共通トランザクション構造 {: #common-transaction-structure } + +NEM のすべてのトランザクション種類には、次の共通属性があります。 + +| 属性 | 説明 | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **署名者公開鍵** | トランザクションを作成して署名したアカウントの公開鍵。 | +| **署名** | 署名者がトランザクションとその内容を承認したことを示す暗号学的証明。 | +| **タイムスタンプ** | トランザクションが作成された時刻。[ネットワーク時刻](default:ネットワーク時刻) で表します。正確な作成時刻の記録というより、主に期限の起点として機能します。 | +| **期限** | 承認されなかった場合にトランザクションが失効することを示すタイムスタンプ。タイムスタンプから24時間以内です。 | +| **手数料** | トランザクションをブロックに含めるために署名者が支払う手数料。 | +| **タイプ** | トランザクションの種類。存在する追加属性を決定します。 | + +## 検証の詳細 {: #validation-details } + +トランザクションをブロックに含める前に、各ノードは次の確認を独立して行います。 + +| **確認** | **説明** | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| **署名確認** | 署名が有効で、署名者の公開鍵とトランザクションの内容に一致することを確認します。 | +| **手数料確認** | 手数料がネットワークの最小値を満たし、署名者が支払うのに十分な XEM を持つことを確認します。 | +| **期限確認** | 期限がすでに過ぎている場合はトランザクションを破棄します。 | +| **タイムスタンプ確認** | クロック操作を防ぐため、タイムスタンプが未来に離れすぎたトランザクションを拒否します。 | +| **ネットワーク確認** | 異なるネットワークを対象とするトランザクションを拒否します。たとえば、メインネットに送られたテストネットトランザクションです。 | +| **一意性確認** | ハッシュが最近のチェーン履歴にすでに現れるトランザクションを拒否し、リプレイを防ぎます。 | +| **意味確認** | 種類に基づいてトランザクションが論理的に正しいことを検証します。例として、送信者の資金が不足している場合、転送トランザクションは失敗します。 | + +これらの確認のいずれかに失敗したトランザクションは拒否され、それ以上伝播されません。 + +## サポートされるトランザクションの種類 {: #supported-transaction-types } + +NEM は、特定の種類の操作に合わせた次のトランザクション種類をサポートします。 +すべてのトランザクション種類は同じ [共通構造](#common-transaction-structure) を共有し、同じ処理と検証手順に従いますが、目的と必要なフィールドが異なります。 + +
+ +| **トランザクションの種類** | **説明** | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| **[転送トランザクション](default:転送トランザクション)** | | +| `Transfer` | 2つの [アカウント](default:アカウント) 間で XEM または [モザイク](default:モザイク) と任意のメッセージを送る。 | +| **[ハーベスティング](default:ハーベスティング)** | | +| `Account Key Link` | リモートアカウントをリンクして、委任ハーベスティングを有効化または無効化する。 | +| **[マルチシグ](default:マルチシグアカウント)** | | +| `Multisig Account Modification` | マルチシグアカウントを作成し、連署人を追加または削除し、必要な署名の最小数を変更する。 | +| `Multisig Cosignature` | 保留中のマルチシグトランザクションに連署を提供する。 | +| `Multisig` | マルチシグアカウントに代わって発行された内部トランザクションを包む。 | +| **[ネームスペース](default:ネームスペース)** | | +| `Namespace Registration` | ネームスペースを登録または更新する。 | +| **[モザイク](default:モザイク)** | | +| `Mosaic Definition` | 新しいモザイクを作成する。 | +| `Mosaic Supply Change` | モザイクの総供給量を変更する。 | + +
+ +## トランザクション手数料 {: #transaction-fees } + +すべてのトランザクションは、ブロックに含める [ハーベスターアカウント](default:ハーベスターアカウント) に報酬を与える手数料を支払います。 + +NEM の手数料は市場によって決まりません。 +ネットワークが固定スケジュールを公開しているため、ノードに接続しなくてもトランザクションのコストを事前に計算できます。 + +### 手数料スケジュール {: #fee-schedule } + +現在のスケジュールは次のとおりです。 + +| トランザクション | コスト | 注記 | +| --------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------| +| `Transfer` | 0.05 XEM から | XEM 金額、付加モザイク、メッセージ長によって異なります。[手数料](./transfer_transactions.md#fees) を参照してください。 | +| `Account Key Link` | 0.15 XEM | | +| `Multisig Account Modification` | 0.5 XEM | マルチシグアカウントが支払います(通常のアカウントをマルチシグに変換する場合は、そのアカウントが支払います)。 | +| `Multisig Cosignature` | 0.15 XEM | 連署人ではなく、マルチシグアカウントが支払います。 | +| `Multisig`(ラッパー) | 0.15 XEM | 内部トランザクションの手数料に加えて、マルチシグアカウントが支払います。 | +| `Namespace Registration` | 0.15 XEM | ネットワークのシンクアドレスに支払う [レンタル手数料](./namespaces.md#lease-fee) が加算されます。 | +| `Mosaic Definition` | 0.15 XEM | ネットワークのシンクアドレスに支払う [作成手数料](./mosaics.md#creation-fee) が加算されます。 | +| `Mosaic Supply Change` | 0.15 XEM | | + +### 下限と入札 {: #floor-and-bidding } + +スケジュールの金額は最小値です。 +手数料が最小値を下回るトランザクションは、検証者に拒否されます。 + +最小値を超える手数料は受け入れられ、含まれる可能性が高くなります。 + +* ハーベスターがブロックを作成するとき、手数料の高い順にトランザクションを選びます。 +* ネットワークが混雑している間、ノードのスパムフィルターは署名者の [インポータンス](default:インポータンス) と小さな手数料ボーナスの組み合わせで保留中のトランザクションを順位付けします。そのため、手数料の高いトランザクションほど [未承認トランザクションプール](default:未承認トランザクションプール) に入りやすくなります。 + +`Multisig Cosignature` の手数料にはさらに 1'000 XEM の上限があります。これは、1人の連署人が極端な手数料を入札してマルチシグアカウントを枯渇させることを防ぎます。 diff --git a/mkdocs/pages/ja/textbook/transfer_transactions.md b/mkdocs/pages/ja/textbook/transfer_transactions.md new file mode 100644 index 000000000..7392c42fa --- /dev/null +++ b/mkdocs/pages/ja/textbook/transfer_transactions.md @@ -0,0 +1,210 @@ +# 転送トランザクション + +転送トランザクション +: あるアカウントから別のアカウントへ [XEM](default:XEM)、[モザイク](default:モザイク)、任意のメッセージを送る [トランザクション](default:トランザクション) です。 + +転送トランザクションは NEM で最も一般的なトランザクションで、資産の転送と簡単な通信の両方を可能にします。 + +## 主な特徴 {: #key-features } + +* **モザイクの転送** + + 転送トランザクションには1つ以上のモザイクを付加できます。 + 転送トランザクション内のすべてのモザイクは、1人の送信者から1人の受信者へ送られます。 + そのため、転送トランザクションは単純な直接の資産転送に適しています。 + +* **メッセージのサポート** + + 任意でプレーンテキストまたは暗号化メッセージを含められます。 + 転送トランザクションにモザイクは必須ではないため、メッセージだけを送ることもできます。 + これにより、モザイクの転送とともに簡単な通信ができます。 + +* **マルチシグ互換性** + + 他のすべてのトランザクションと同様、転送トランザクションは [マルチシグアカウント](default:マルチシグアカウント) をサポートします。 + これにより、複雑なガバナンス方式など、複数アカウントによる承認を必要にできます。 + +## 構造 {: #structure } + +[共通トランザクション構造](./transactions.md#common-transaction-structure) に加えて、転送トランザクションには次の属性が含まれます。 + +| 属性 | 説明 | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [**受取人のアドレス**](#recipients-address) | 受取アカウントのアドレス。 | +| [**XEM 金額**](#xem-amount) | モザイク一覧が空の場合は受取人に送る XEM、一覧が空でない場合は付加された各モザイクに適用する乗数。 | +| [**転送モザイク一覧**](#list-of-transferred-mosaics) | 転送する0個以上のモザイク。 | +| [**任意のメッセージ**](#optional-message) | 最大1024バイトのプレーンテキストまたは暗号化メッセージ。 | + +### 受取人のアドレス {: #recipients-address } + +受取人は [アドレス](default:アドレス) で指定します。 + +!!! warning "所有者のいないアドレスへ送った資産は失われます" + + これまでチェーン上に現れたことがないアドレスを含め、任意の有効なアドレスに XEM またはモザイクを送れます。 + そのアドレスに対応する [秘密鍵](default:秘密鍵) を誰も持っていなければ、転送された資産を回収できません。 + +### XEM 金額 {: #xem-amount } + +**XEM 金額** はマイクロ XEM で表し、`1 XEM = 1'000'000 micro-XEM` です。 +[モザイク一覧](#list-of-transferred-mosaics) に応じて、次の2つの目的で使われます。 + +* **転送金額** + モザイク一覧が空の場合、XEM 金額は受取人に送る XEM です。 +* **モザイク乗数** + モザイク一覧が空でない場合、XEM 金額自体は XEM を送りません。 + 代わりに、値を `1'000'000` で割って、付加された各モザイクの数量を拡大する **乗数** を得ます。 + 同じ乗数が一覧のすべてのモザイクに適用されます。 + + たとえば `2'000'000` は乗数 `2` になり、一覧内のすべてのモザイク数量を2倍にします。 + 一覧項目が500単位なら、受取人には `1'000` 単位が届きます。 + +!!! note "乗数のルール" + + XEM 金額が乗数として機能する場合は、次のルールがあります。 + + * 結果の乗数が整数になるよう、値は `1'000'000` で割り切れる必要があります。小数の金額は拒否されます。 + * ウォレットは慣例として `1'000'000` に設定し、各モザイク数量を指定どおり転送します。 + * 乗数 `0` は、モザイクを転送しない有効なトランザクションを生成します。 + +### 転送モザイク一覧 {: #list-of-transferred-mosaics } + +転送トランザクションには最大 **10個のモザイク** を含められます。 +一覧を空にすることもでき、その場合は資産を移動せずに送信者がメッセージを付加できます。 + +各項目にはモザイク ID と数量を指定します。 +数量はモザイクの **原子単位** で数える整数です。 + +モザイクの作成時に設定する `divisibility` プロパティは `0` から `6` の範囲で、1全体単位を構成する原子単位の数を定義します。 +たとえば XEM の可分性は `6` なので、1'000'000 原子単位が1 XEMです。 +全体単位と原子単位の変換については、[モザイク](./mosaics.md#divisibility) ページを参照してください。 + +一覧にあるどのモザイクについても、送信者が十分な単位を保有していなければネットワークはトランザクションを拒否します。 + +一覧にあるモザイクに [徴収手数料](default:徴収手数料(levy)) が設定されている場合、ネットワークは転送量に加えて送信者から徴収手数料を徴収し、その徴収手数料の受取人に入金します。 +[徴収手数料の請求方法](./mosaics.md#how-levies-are-charged) の規則を確認してください。 + +!!! tip "他のモザイクと一緒に XEM を送る" + + 他のモザイクと同じトランザクションで XEM を送るには、一覧に XEM を項目として含めます。 + 他のすべてのモザイクと同様に、XEM 金額の乗数によって数量が拡大されます。 + +### 任意のメッセージ {: #optional-message } + +転送トランザクションには、最大 **1024バイト** の任意のメッセージを含められます。 +付加モザイクがなく、XEM 金額が `0` の場合、転送にはメッセージだけが含まれます。 + +すべてのメッセージには `type` フィールドがあり、ペイロードがプレーンテキスト(`0x0001`)かセキュア(`0x0002`)かを識別します。 + +ノードは `type` フィールドと1024バイトのサイズ制限を強制します。 +ペイロードのバイト列は解釈しません。 +プロトコルはプレーンテキストのエンコード方式を定義せず、セキュアペイロードの暗号化方式も標準化しません。 +どちらも送信者と受信者が合意する規約です。 + +#### プレーンテキストの規約 {: #plaintext-conventions } + +プレーンテキストのペイロードはそのまま保存されます。 +送信者と受信者は UTF-8、JSON、16進数などの形式に合意します。 + +NEM のウォレットとアプリケーションは通常 UTF-8 を想定します。 + +#### セキュアメッセージの規約 {: #secure-message-conventions } + +セキュアペイロードは、受信者だけが復号できるよう暗号化されます。 +プロトコルは暗号化方式を標準化していません。 + +既存のウォレットと SDK では、共有鍵を楕円曲線ディフィー・ヘルマン(ECDH)で導出する **AES-CBC** と **AES-GCM** の2方式が広く使われています。 +各方式は1024バイトのペイロードの一部を暗号学的メタデータ用に確保し、AES-CBC では最大 **960バイト**、AES-GCM では **996バイト** のプレーンテキストを使用できます。 + +!!! warning "CBC と GCM は相互運用できません" + + ノードはペイロードを検査せず、同じ `0x0002` フラグの下で両方の方式を受け入れて保存します。 + 受信者がセキュアメッセージを復号できるのは、送信者と同じ方式をツールが実装している場合だけです。 + GCM ツールで作成されたメッセージは CBC だけに対応するツールでは復号できず、その逆も同様です。 + +## 手数料 {: #fees } + +転送トランザクションの手数料は送信内容によって異なります。 +次の2つの要素の合計です。 + +* **転送手数料**:XEM 金額または付加モザイクに基づきます。 +* **メッセージ手数料**:付加メッセージの長さに基づきます。 + +### 転送手数料 {: #transfer-fee } + +XEM だけを転送する場合、手数料は送信金額に応じて変わります。 + +| 送信金額 | コスト | +| ---------------------------- | --------- | +| 最大19'999 XEM | 0.05 XEM | +| 追加10'000 XEM ごと | +0.05 XEM | +| 250'000 XEM 以上 | 1.25 XEM | + +モザイクを転送する場合、手数料は付加された各モザイクの個別手数料の合計です。 +各モザイクは次のように価格付けされます。 + +* **少量で不可分なモザイク**(供給量 ≤ 10'000、可分性 0)は一律 **0.05 XEM** です。 +* **その他すべてのモザイク** は、転送数量とモザイク総供給量から導出した **XEM 換算価値** で価格付けされます。 + この価値は、XEM だけの転送に使う0.05〜1.25 XEMの手数料階層に、モザイク総供給量が少ないほど大きくなる **供給量割引** を適用して対応付けられます。 + +モザイクごとの最小手数料は **0.05 XEM** です。 + +??? info "モザイク手数料の計算" + + 少量でないモザイクの手数料計算は3段階です。転送数量の XEM 換算価値を計算し、その価値を手数料階層で調べて基本手数料を得てから、供給量割引を引きます。 + + **1. XEM 換算価値** + + \[ + \text{xem\_equivalent} = \frac{\text{8'999'999'999} \cdot \text{atomic\_quantity} \cdot \text{multiplier}}{\text{total\_atomic\_supply}} + \] + + ここで、 + + * $\text{8'999'999'999}$ は初期 XEM 供給量(全体単位)です。 + * $\text{atomic\_quantity}$ は転送するモザイク量(原子単位)です。 + * $\text{multiplier}$ は [XEM 金額の乗数](#xem-amount) です(通常は1)。 + * $\text{total\_atomic\_supply}$ はモザイクの総供給量(原子単位)です:$\text{supply} \cdot 10^{\text{divisibility}}$。 + + **2. 基本手数料** + + 得られた価値は、XEM だけの転送と同じ0.05〜1.25 XEMの手数料階層で価格付けされ、モザイクの **基本手数料** になります。 + + **3. 供給量割引** + + 基本手数料から **供給量割引** を引きます。 + + \[ + \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} + \] + + ここで $9 \cdot 10^{15}$ は NEM が許可する最大モザイク数量です。 + + 供給量が少ないほど $\text{xem\_equivalent}$ は大きくなるため、割引がなければ供給量の少ないモザイクは小さな転送でも $1.25$ XEM の上限に達します。 + 割引は同じ供給量の対数でこれを相殺するため、希少なモザイクほど大きな補正を受けます。 + 割引が基本手数料を超えても、最終手数料が **0.05 XEM 未満になることはありません**。 + + **例** + + 供給量 $\text{1'000'000}$、可分性 `0` のモザイクで、乗数1で100単位を送る場合: + + 1. **XEM 換算値**:$\frac{\text{8'999'999'999} \cdot 100 \cdot 1}{\text{1'000'000}} = \text{899'999.9999}$。 + 2. **基本手数料**:上記の XEM だけのスケジュールでは、$\text{10'000}$ XEM の価値ごとに0.05 XEMが加算され、$\text{250'000}$ XEM以上では1.25 XEMが上限です。$\text{899'999.9999}$ は $\text{250'000}$ を超えるため、基本手数料は最大の1.25 XEMです。 + 3. **供給量割引**:$\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。 + + **最終手数料**:$1.25 - 0.90 = 0.35$ XEM。 + +### メッセージ手数料 {: #message-fee } + +空でないメッセージには、基本0.05 XEMに加えて、1024バイトの最大値までペイロード32バイトごとに0.05 XEMがかかります。 + +| メッセージ長 | 追加コスト | +| ---------------------- | ---------- | +| メッセージなし | なし | +| 1〜31バイト | 0.05 XEM | +| 32〜63バイト | 0.10 XEM | +| 64〜95バイト | 0.15 XEM | +| … | … | +| 1024バイト(最大) | 1.65 XEM | + +手数料は保存されるペイロードサイズで計算されます。そのため [セキュアメッセージ](#secure-message-conventions) はプレーンテキストではなく暗号化済みペイロードに対して課金されます。 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..8af113241 --- /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\4\5\6
js:{symbol_name}.\3
\7
\8', + 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/configure_multisig.mjs b/mkdocs/snippets/devbook/accounts/configure_multisig.mjs new file mode 100644 index 000000000..f6cf2c9b1 --- /dev/null +++ b/mkdocs/snippets/devbook/accounts/configure_multisig.mjs @@ -0,0 +1,237 @@ +import { PrivateKey } from 'symbol-sdk'; +import { + NemFacade, + NetworkTimestamp, + calculateTransactionFee, + models +} from 'symbol-sdk/nem'; + +const NODE_URL = process.env.NODE_URL || + 'http://libertalia.nemtest.net:7890'; +console.log('Using node', NODE_URL); + +const facade = new NemFacade('testnet'); +// [>step-1] +const KEY_PREFIX = '0'.repeat(63); + +// Set up the keys for the multisig account and its two cosignatories +const MULTISIG_PRIVATE_KEY = process.env.MULTISIG_PRIVATE_KEY || ( + `${KEY_PREFIX}1`); +const multisigKeyPair = new NemFacade.KeyPair( + new PrivateKey(MULTISIG_PRIVATE_KEY)); +const multisigAddress = facade.network.publicKeyToAddress( + multisigKeyPair.publicKey); +console.log(`Multisig address: ${multisigAddress}`, + `(public key ${multisigKeyPair.publicKey})`); + +const cosignatoryKeyPairs = []; +for (let i = 0; 2 > i; i++) { + const COSIGNATORY_PRIVATE_KEY = + process.env[`COSIGNATORY${i}_PRIVATE_KEY`] || ( + KEY_PREFIX + String(i + 2)); + const keyPair = new NemFacade.KeyPair( + new PrivateKey(COSIGNATORY_PRIVATE_KEY)); + cosignatoryKeyPairs.push(keyPair); + const addr = facade.network.publicKeyToAddress(keyPair.publicKey); + console.log(`Cosignatory ${i} address: ${addr}`, + `(public key ${keyPair.publicKey})`); +} +// [= attempt; ++attempt) { + const response = await fetch(`${NODE_URL}${statusPath}`); + if (response.ok) { + const confirmed = await response.json(); + console.log(`${label} 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(`${label} confirmation took too long.`); +} + +// Returns the cosignatory addresses of the provided multisig [>step-3] +// account, or an empty list if the account is not multisig +async function getMultisigCosignatories(address) { + const accountPath = `/account/get?address=${address}`; + console.log(`Getting cosignatories from ${accountPath}`); + const response = await fetch(`${NODE_URL}${accountPath}`); + const accountInfo = await response.json(); + const foundCosignatories = accountInfo.meta.cosignatories + .map(cosignatory => cosignatory.address); + if (0 === foundCosignatories.length) { + console.log(' Response: No cosignatories'); + return []; + } + console.log(' Response:', JSON.stringify(foundCosignatories)); + return foundCosignatories; +} +// [step-5] +// Returns a transaction that turns a regular account into a multisig +function multisigEnableTransaction(timestamp, deadline, approvalDelta) { + // Create a multisig account modification transaction + // that adds the cosignatories + const modifications = cosignatoryKeyPairs.map(keyPair => ({ + modification: { + modificationType: 'add_cosignatory', + cosignatoryPublicKey: keyPair.publicKey.toString() + } + })); + const transaction = facade.transactionFactory.create({ + type: 'multisig_account_modification_transaction_v2', + // This is the account that will be turned into a multisig + signerPublicKey: multisigKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + // Change of the number of cosignatures + // required to approve transactions + minApprovalDelta: approvalDelta, + modifications + }); + // [step-6] + const fee = calculateTransactionFee(transaction); + transaction.fee = new models.Amount(fee); + console.log(` Transaction fee: ${Number(fee) / 1_000_000} XEM`); + console.log( + 'Enabling the multisig with the modification transaction:'); + console.log(JSON.stringify(transaction.toJson(), null, 2)); + // [step-7] + const signature = facade.signTransaction( + multisigKeyPair, transaction); + facade.transactionFactory.static.attachSignature( + transaction, signature); + return transaction; // [step-8] +// Returns a transaction that removes one cosignatory from the multisig +function multisigRemovalTransaction(timestamp, deadline, + removedKeyPair, approvalDelta) { + // Create a multisig account modification transaction + // that removes a single cosignatory + const innerTransaction = facade.transactionFactory.create({ + type: 'multisig_account_modification_transaction_v2', + // This is the multisig account that will be modified + signerPublicKey: multisigKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + // Change of the number of cosignatures + // required to approve transactions + minApprovalDelta: approvalDelta, + modifications: [ + { + modification: { + modificationType: 'delete_cosignatory', + cosignatoryPublicKey: + removedKeyPair.publicKey.toString() + } + } + ] + }); + // [step-9] + const innerFee = calculateTransactionFee(innerTransaction); + innerTransaction.fee = new models.Amount(innerFee); + const transaction = facade.transactionFactory.create({ + type: 'multisig_transaction_v1', + // This is the cosignatory that initiates the removal + signerPublicKey: cosignatoryKeyPairs[0].publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + innerTransaction: facade.transactionFactory.static + .toNonVerifiableTransaction(innerTransaction) + }); + // [step-10] + const fee = calculateTransactionFee(transaction); + transaction.fee = new models.Amount(fee); + console.log(' Transaction fee:', + `${Number(innerFee + fee) / 1_000_000} XEM`); + console.log( + 'Disabling the multisig with the multisig transaction:'); + console.log(JSON.stringify(transaction.toJson(), null, 2)); + // [step-11] + const signature = facade.signTransaction( + cosignatoryKeyPairs[0], transaction); + facade.transactionFactory.static + .attachSignature(transaction, signature); + return transaction; // [step-2] + 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 = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + // [step-4] + // which operation to perform + const cosignatories = await getMultisigCosignatories(multisigAddress); + let transactions; + if (0 === cosignatories.length) { + // Enable the multisig + transactions = [multisigEnableTransaction( + timestamp, deadline, 1)]; + } else { + // Disable the multisig + transactions = [ + multisigRemovalTransaction( + timestamp, deadline, cosignatoryKeyPairs[1], 0), + multisigRemovalTransaction( + timestamp, deadline, cosignatoryKeyPairs[0], -1) + ]; + } + // [step-12] + for (const signedTransaction of transactions) { + const transactionHash = facade.hashTransaction(signedTransaction) + .toString(); + console.log('Built transaction with hash:', transactionHash); + const jsonPayload = facade.transactionFactory.static + .toJson(signedTransaction); + const result = await announceTransaction( + jsonPayload, 'transaction'); + if ('SUCCESS' !== result) { + console.log('Transaction rejected'); + break; + } + await waitForConfirmation(transactionHash, 'transaction'); + } + // [step-1] +KEY_TEMPLATE = '0' * 63 + '{}' + +# Set up the keys for the multisig account and its two cosignatories +MULTISIG_PRIVATE_KEY = os.getenv( + 'MULTISIG_PRIVATE_KEY', KEY_TEMPLATE.format(1)) +multisig_key_pair = NemFacade.KeyPair(PrivateKey(MULTISIG_PRIVATE_KEY)) +multisig_address = facade.network.public_key_to_address( + multisig_key_pair.public_key) +print(f'Multisig address: {multisig_address} ' + f'(public key {multisig_key_pair.public_key})') + +cosignatory_key_pairs = [] +for i in range(2): + COSIGNATORY_PRIVATE_KEY = os.getenv( + f'COSIGNATORY{i}_PRIVATE_KEY', KEY_TEMPLATE.format(i + 2)) + key_pair = NemFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY)) + cosignatory_key_pairs.append(key_pair) + addr = facade.network.public_key_to_address(key_pair.public_key) + print(f'Cosignatory {i} address: ' + f'{addr} (public key {key_pair.public_key})') # [step-3] +# account, or an empty list if the account is not multisig +def get_multisig_cosignatories(address): + account_path = f'/account/get?address={address}' + print(f'Getting cosignatories from {account_path}') + url = f'{NODE_URL}{account_path}' + with urllib.request.urlopen(url) as account_response: + account_info = json.loads(account_response.read().decode()) + found_cosignatories = [ + cosignatory['address'] + for cosignatory in account_info['meta']['cosignatories'] + ] + if not found_cosignatories: + print(' Response: No cosignatories') + return [] + print(f' Response: {found_cosignatories}') + return found_cosignatories # [step-5] +# Returns a transaction that turns a regular account into a multisig +def multisig_enable_transaction(tx_timestamp, tx_deadline, + approval_delta): + # Create a multisig account modification transaction + # that adds the cosignatories + modifications = [ + {'modification': { + 'modification_type': 'add_cosignatory', + 'cosignatory_public_key': key_pair.public_key + }} + for key_pair in cosignatory_key_pairs + ] + transaction = facade.transaction_factory.create({ + 'type': 'multisig_account_modification_transaction_v2', + # This is the account that will be turned into a multisig + 'signer_public_key': multisig_key_pair.public_key, + 'timestamp': tx_timestamp.timestamp, + 'deadline': tx_deadline.timestamp, + # Change of the number of cosignatures + # required to approve transactions + 'min_approval_delta': approval_delta, + 'modifications': modifications + }) + # [step-6] + fee = calculate_transaction_fee(transaction) + transaction.fee = Amount(fee) + print(f' Transaction fee: {fee / 1_000_000} XEM') + print('Enabling the multisig with the modification transaction:') + print(json.dumps(transaction.to_json(), indent=2)) + # [step-7] + signature = facade.sign_transaction(multisig_key_pair, transaction) + facade.transaction_factory.attach_signature(transaction, signature) + return transaction # [step-8] +# Returns a transaction that removes one cosignatory from the multisig +def multisig_removal_transaction(tx_timestamp, tx_deadline, + removed_key_pair, approval_delta): + # Create a multisig account modification transaction + # that removes a single cosignatory + inner_transaction = facade.transaction_factory.create({ + 'type': 'multisig_account_modification_transaction_v2', + # This is the multisig account that will be modified + 'signer_public_key': multisig_key_pair.public_key, + 'timestamp': tx_timestamp.timestamp, + 'deadline': tx_deadline.timestamp, + # Change of the number of cosignatures + # required to approve transactions + 'min_approval_delta': approval_delta, + 'modifications': [ + {'modification': { + 'modification_type': 'delete_cosignatory', + 'cosignatory_public_key': removed_key_pair.public_key + }} + ] + }) + # [step-9] + inner_fee = calculate_transaction_fee(inner_transaction) + inner_transaction.fee = Amount(inner_fee) + transaction = facade.transaction_factory.create({ + 'type': 'multisig_transaction_v1', + # This is the cosignatory that initiates the removal + 'signer_public_key': cosignatory_key_pairs[0].public_key, + 'timestamp': tx_timestamp.timestamp, + 'deadline': tx_deadline.timestamp, + 'inner_transaction': + facade.transaction_factory.to_non_verifiable_transaction( + inner_transaction) + }) + # [step-10] + fee = calculate_transaction_fee(transaction) + transaction.fee = Amount(fee) + print(f' Transaction fee: {(inner_fee + fee) / 1_000_000} XEM') + print('Disabling the multisig with the multisig transaction:') + print(json.dumps(transaction.to_json(), indent=2)) + # [step-11] + signature = facade.sign_transaction( + cosignatory_key_pairs[0], transaction) + facade.transaction_factory.attach_signature(transaction, signature) + return transaction # [step-2] + 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 = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + # [step-4] + # operation to perform + cosignatories = get_multisig_cosignatories(multisig_address) + if len(cosignatories) == 0: + # Enable the multisig + transactions = [multisig_enable_transaction( + timestamp, deadline, 1)] + else: + # Disable the multisig + transactions = [ + multisig_removal_transaction( + timestamp, deadline, cosignatory_key_pairs[1], 0), + multisig_removal_transaction( + timestamp, deadline, cosignatory_key_pairs[0], -1) + ] + # [step-12] + for signed_transaction in transactions: + transaction_hash = facade.hash_transaction(signed_transaction) + print(f'Built transaction with hash: {transaction_hash}') + json_payload = facade.transaction_factory.to_json( + signed_transaction) + announce_result = announce_transaction( + json_payload, 'transaction') + if 'SUCCESS' != announce_result: + print('Transaction rejected') + break + wait_for_confirmation(transaction_hash, 'transaction') + # [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) + # [= attempt; ++attempt) { + const response = await fetch(`${NODE_URL}${statusPath}`); + if (response.ok) { + const confirmed = await response.json(); + console.log(`${label} 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(`${label} confirmation took too long.`); +} +// [>step-1] +const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY || + '0000000000000000000000000000000000000000000000000000000000000000'; +const signerKeyPair = new NemFacade.KeyPair( + new PrivateKey(SIGNER_PRIVATE_KEY)); + +const facade = new NemFacade('testnet'); +const signerAddress = facade.network.publicKeyToAddress( + signerKeyPair.publicKey); +console.log('Signer address:', signerAddress.toString()); + +const namespaceName = process.env.NAMESPACE || 'my_namespace'; +const mosaicName = process.env.MOSAIC || 'token'; +const mosaicId = `${namespaceName}:${mosaicName}`; +console.log('Mosaic ID:', mosaicId); +// [step-2] + 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 = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + // [step-3] + console.log('Supply before minting:', await fetchSupply(mosaicId)); + + const increaseTx = facade.transactionFactory.create({ + type: 'mosaic_supply_change_transaction_v1', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + mosaicId: { + namespaceId: { name: namespaceName }, + name: mosaicName + }, + action: 'increase', + delta: 500n + }); + increaseTx.fee = new models.Amount( + calculateTransactionFee(increaseTx)); + + const increaseSignature = facade.signTransaction( + signerKeyPair, increaseTx); + const increasePayload = facade.transactionFactory.static + .attachSignature(increaseTx, increaseSignature); + console.log('Built supply increase transaction:'); + console.dir(increaseTx.toJson(), { colors: true }); + const increaseResult = await announceTransaction( + increasePayload, 'supply increase'); + if ('SUCCESS' === increaseResult) { + await waitForConfirmation( + facade.hashTransaction(increaseTx).toString(), + 'supply increase'); + console.log('Supply after minting:', await fetchSupply(mosaicId)); + } else { + console.log('Supply increase rejected'); + } + // [step-4] + const decreaseTx = facade.transactionFactory.create({ + type: 'mosaic_supply_change_transaction_v1', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + mosaicId: { + namespaceId: { name: namespaceName }, + name: mosaicName + }, + action: 'decrease', + delta: 500n + }); + decreaseTx.fee = new models.Amount( + calculateTransactionFee(decreaseTx)); + + const decreaseSignature = facade.signTransaction( + signerKeyPair, decreaseTx); + const decreasePayload = facade.transactionFactory.static + .attachSignature(decreaseTx, decreaseSignature); + console.log('Built supply decrease transaction:'); + console.dir(decreaseTx.toJson(), { colors: true }); + const decreaseResult = await announceTransaction( + decreasePayload, 'supply decrease'); + if ('SUCCESS' === decreaseResult) { + await waitForConfirmation( + facade.hashTransaction(decreaseTx).toString(), + 'supply decrease'); + console.log('Supply after burning:', await fetchSupply(mosaicId)); + } else { + console.log('Supply decrease rejected'); + } + // [step-1] + 'SIGNER_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000000') +signer_key_pair = NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY)) + +facade = NemFacade('testnet') +signer_address = facade.network.public_key_to_address( + signer_key_pair.public_key) +print(f'Signer address: {signer_address}') + +namespace_name = os.getenv('NAMESPACE', 'my_namespace') +mosaic_name = os.getenv('MOSAIC', 'token') +mosaic_id = f'{namespace_name}:{mosaic_name}' +print(f'Mosaic ID: {mosaic_id}') +# [step-2] + 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 = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + # [step-3] + print(f'Supply before minting: {fetch_supply(mosaic_id)}') + + increase_tx = facade.transaction_factory.create({ + 'type': 'mosaic_supply_change_transaction_v1', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'mosaic_id': { + 'namespace_id': {'name': namespace_name}, + 'name': mosaic_name + }, + 'action': 'increase', + 'delta': 500 + }) + increase_tx.fee = Amount(calculate_transaction_fee(increase_tx)) + + signature = facade.sign_transaction(signer_key_pair, increase_tx) + json_payload = facade.transaction_factory.attach_signature( + increase_tx, signature) + print('Built supply increase transaction:') + print(json.dumps(increase_tx.to_json(), indent=2)) + if 'SUCCESS' == announce_transaction(json_payload, 'supply increase'): + wait_for_confirmation( + facade.hash_transaction(increase_tx), 'supply increase') + print(f'Supply after minting: {fetch_supply(mosaic_id)}') + else: + print('Supply increase rejected') + # [step-4] + decrease_tx = facade.transaction_factory.create({ + 'type': 'mosaic_supply_change_transaction_v1', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'mosaic_id': { + 'namespace_id': {'name': namespace_name}, + 'name': mosaic_name + }, + 'action': 'decrease', + 'delta': 500 + }) + decrease_tx.fee = Amount(calculate_transaction_fee(decrease_tx)) + + signature = facade.sign_transaction(signer_key_pair, decrease_tx) + json_payload = facade.transaction_factory.attach_signature( + decrease_tx, signature) + print('Built supply decrease transaction:') + print(json.dumps(decrease_tx.to_json(), indent=2)) + if 'SUCCESS' == announce_transaction(json_payload, 'supply decrease'): + wait_for_confirmation( + facade.hash_transaction(decrease_tx), 'supply decrease') + print(f'Supply after burning: {fetch_supply(mosaic_id)}') + else: + print('Supply decrease rejected') + # [step-1] +const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY || + '0000000000000000000000000000000000000000000000000000000000000000'; +const signerKeyPair = new NemFacade.KeyPair( + new PrivateKey(SIGNER_PRIVATE_KEY)); + +const facade = new NemFacade('testnet'); +const signerAddress = facade.network.publicKeyToAddress( + signerKeyPair.publicKey); +console.log('Signer address:', signerAddress.toString()); +// [step-2] + 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 = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + // [step-3] + const namespaceName = process.env.NAMESPACE || 'my_namespace'; + const mosaicName = process.env.MOSAIC || + `token_${Math.floor(Date.now() / 1000)}`; + const mosaicId = `${namespaceName}:${mosaicName}`; + console.log('Creating mosaic:', mosaicId); + // [step-4] + const mosaicDefinition = { + ownerPublicKey: signerKeyPair.publicKey.toString(), + id: { + namespaceId: { name: namespaceName }, + name: mosaicName + }, + description: 'My tutorial mosaic', + properties: [ + { property: { name: 'divisibility', value: '2' } }, + { property: { name: 'initialSupply', value: '1000' } }, + { property: { name: 'supplyMutable', value: 'true' } }, + { property: { name: 'transferable', value: 'true' } } + ] + }; + // [step-5] + const rentalFee = calculateMosaicRentalFee(); + console.log(' Mosaic creation fee:', + `${Number(rentalFee) / 1_000_000} XEM`); + + const transaction = facade.transactionFactory.create({ + type: 'mosaic_definition_transaction_v1', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + rentalFeeSink: 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC', + rentalFee, + mosaicDefinition + }); + // [step-6] + const fee = calculateTransactionFee(transaction); + 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 mosaic definition transaction:'); + console.dir(transaction.toJson(), { colors: true }); + + // Announce the transaction + const announcePath = '/transaction/announce'; + console.log('Announcing mosaic definition 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-9] + const definitionPath = `/mosaic/definition?mosaicId=${mosaicId}`; + console.log('Fetching mosaic information from', definitionPath); + const definitionResponse = await fetch( + `${NODE_URL}${definitionPath}`); + const mosaicInfo = await definitionResponse.json(); + const properties = Object.fromEntries( + mosaicInfo.properties.map(prop => [prop.name, prop.value])); + console.log('Mosaic information:'); + console.log(' Creator:', mosaicInfo.creator); + console.log(' Divisibility:', properties.divisibility); + console.log(' Initial supply:', properties.initialSupply); + console.log(' Supply mutable:', properties.supplyMutable); + console.log(' Transferable:', properties.transferable); + // [step-1] +SIGNER_PRIVATE_KEY = os.getenv( + 'SIGNER_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000000') +signer_key_pair = NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY)) + +facade = NemFacade('testnet') +signer_address = facade.network.public_key_to_address( + signer_key_pair.public_key) +print(f'Signer address: {signer_address}') +# [step-2] + 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 = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + # [step-3] + namespace_name = os.getenv('NAMESPACE', 'my_namespace') + mosaic_name = os.getenv('MOSAIC', f'token_{int(time.time())}') + mosaic_id = f'{namespace_name}:{mosaic_name}' + print(f'Creating mosaic: {mosaic_id}') + # [step-4] + mosaic_definition = { + 'owner_public_key': signer_key_pair.public_key, + 'id': { + 'namespace_id': {'name': namespace_name}, + 'name': mosaic_name + }, + 'description': 'My tutorial mosaic', + 'properties': [ + {'property_': { + 'name': b'divisibility', 'value': b'2'}}, + {'property_': { + 'name': b'initialSupply', 'value': b'1000'}}, + {'property_': { + 'name': b'supplyMutable', 'value': b'true'}}, + {'property_': { + 'name': b'transferable', 'value': b'true'}} + ] + } + # [step-5] + rental_fee = calculate_mosaic_rental_fee() + print(f' Mosaic creation fee: {rental_fee / 1_000_000} XEM') + + transaction = facade.transaction_factory.create({ + 'type': 'mosaic_definition_transaction_v1', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'rental_fee_sink': 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC', + 'rental_fee': rental_fee, + 'mosaic_definition': mosaic_definition + }) + # [step-6] + fee = calculate_transaction_fee(transaction) + 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 mosaic definition transaction:') + print(json.dumps(transaction.to_json(), indent=2)) + + # Announce the transaction + announce_path = '/transaction/announce' + print(f'Announcing mosaic definition 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']: + transaction_hash = facade.hash_transaction(transaction) + status_path = f'/transaction/get?hash={transaction_hash}' + 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-9] + definition_path = f'/mosaic/definition?mosaicId={mosaic_id}' + print(f'Fetching mosaic information from {definition_path}') + with urllib.request.urlopen( + f'{NODE_URL}{definition_path}' + ) as response: + mosaic_info = json.loads(response.read().decode()) + properties = { + prop['name']: prop['value'] + for prop in mosaic_info['properties'] + } + print('Mosaic information:') + print(f' Creator: {mosaic_info["creator"]}') + print(f' Divisibility: {properties["divisibility"]}') + print(f' Initial supply: {properties["initialSupply"]}') + print(f' Supply mutable: {properties["supplyMutable"]}') + print(f' Transferable: {properties["transferable"]}') + # [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 SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY || + '0000000000000000000000000000000000000000000000000000000000000000'; +const signerKeyPair = new NemFacade.KeyPair( + new PrivateKey(SIGNER_PRIVATE_KEY)); + +const facade = new NemFacade('testnet'); +const signerAddress = facade.network.publicKeyToAddress( + signerKeyPair.publicKey); +console.log('Signer address:', signerAddress.toString()); + +const namespaceName = process.env.NAMESPACE || 'my_namespace'; +const mosaicName = process.env.MOSAIC || + `token_${Math.floor(Date.now() / 1000)}`; +const mosaicId = `${namespaceName}:${mosaicName}`; +console.log('Creating mosaic:', mosaicId); +// [step-2] + 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 = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + // [step-3] + const LEVY_RECIPIENT = process.env.LEVY_RECIPIENT || + 'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4'; + + const levy = { + transferFeeType: 'absolute', + recipientAddress: LEVY_RECIPIENT, + mosaicId: { + namespaceId: { name: 'nem' }, + name: 'xem' + }, + fee: 1_000_000 + }; + console.log('Levy:'); + console.log(' Type:', levy.transferFeeType); + console.log(' Recipient:', levy.recipientAddress); + console.log(' Mosaic:', + `${levy.mosaicId.namespaceId.name}:${levy.mosaicId.name}`); + console.log(' Fee:', levy.fee); + // [step-4] + const rentalFee = calculateMosaicRentalFee(); + console.log(' Mosaic creation fee:', + `${Number(rentalFee) / 1_000_000} XEM`); + + const transaction = facade.transactionFactory.create({ + type: 'mosaic_definition_transaction_v1', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + rentalFeeSink: 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC', + rentalFee, + mosaicDefinition: { + ownerPublicKey: signerKeyPair.publicKey.toString(), + id: { + namespaceId: { name: namespaceName }, + name: mosaicName + }, + description: 'My tutorial mosaic with a levy', + properties: [ + { property: { name: 'divisibility', value: '2' } }, + { property: { name: 'initialSupply', value: '1000' } }, + { property: { name: 'supplyMutable', value: 'true' } }, + { property: { name: 'transferable', value: 'true' } } + ], + levy + } + }); + + // Calculate and attach the transaction fee + const fee = calculateTransactionFee(transaction); + transaction.fee = new models.Amount(fee); + console.log(' Transaction fee:', `${Number(fee) / 1_000_000} XEM`); + // [step-5] + const signature = facade.signTransaction(signerKeyPair, transaction); + const jsonPayload = facade.transactionFactory.static.attachSignature( + transaction, signature); + console.log('Built mosaic definition transaction:'); + console.dir(transaction.toJson(), { colors: true }); + + const announcePath = '/transaction/announce'; + console.log('Announcing mosaic definition 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); + + 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-6] + const definitionPath = `/mosaic/definition?mosaicId=${mosaicId}`; + console.log('Fetching mosaic information from', definitionPath); + const definitionResponse = await fetch( + `${NODE_URL}${definitionPath}`); + const mosaicInfo = await definitionResponse.json(); + const levyInfo = mosaicInfo.levy; + const levyMosaicId = levyInfo.mosaicId; + const levyType = 1 === levyInfo.type ? 'absolute' : 'percentile'; + console.log('Levy information:'); + console.log(' Type:', levyType); + console.log(' Recipient:', levyInfo.recipient); + console.log(' Mosaic:', + `${levyMosaicId.namespaceId}:${levyMosaicId.name}`); + console.log(' Fee:', levyInfo.fee); + // [step-1] +SIGNER_PRIVATE_KEY = os.getenv( + 'SIGNER_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000000') +signer_key_pair = NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY)) + +facade = NemFacade('testnet') +signer_address = facade.network.public_key_to_address( + signer_key_pair.public_key) +print(f'Signer address: {signer_address}') + +namespace_name = os.getenv('NAMESPACE', 'my_namespace') +mosaic_name = os.getenv('MOSAIC', f'token_{int(time.time())}') +mosaic_id = f'{namespace_name}:{mosaic_name}' +print(f'Creating mosaic: {mosaic_id}') +# [step-2] + 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 = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + # [step-3] + LEVY_RECIPIENT = os.getenv( + 'LEVY_RECIPIENT', + 'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4') + + levy = { + 'transfer_fee_type': 'absolute', + 'recipient_address': LEVY_RECIPIENT, + 'mosaic_id': { + 'namespace_id': {'name': 'nem'}, + 'name': 'xem' + }, + 'fee': 1_000_000 + } + levy_mosaic_id = levy['mosaic_id'] + print('Levy:') + print(f' Type: {levy["transfer_fee_type"]}') + print(f' Recipient: {levy["recipient_address"]}') + print(f' Mosaic: {levy_mosaic_id["namespace_id"]["name"]}:' + f'{levy_mosaic_id["name"]}') + print(f' Fee: {levy["fee"]}') + # [step-4] + rental_fee = calculate_mosaic_rental_fee() + print(f' Mosaic creation fee: {rental_fee / 1_000_000} XEM') + + transaction = facade.transaction_factory.create({ + 'type': 'mosaic_definition_transaction_v1', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'rental_fee_sink': 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC', + 'rental_fee': rental_fee, + 'mosaic_definition': { + 'owner_public_key': signer_key_pair.public_key, + 'id': { + 'namespace_id': {'name': namespace_name}, + 'name': mosaic_name + }, + 'description': 'My tutorial mosaic with a levy', + 'properties': [ + {'property_': { + 'name': b'divisibility', 'value': b'2'}}, + {'property_': { + 'name': b'initialSupply', 'value': b'1000'}}, + {'property_': { + 'name': b'supplyMutable', 'value': b'true'}}, + {'property_': { + 'name': b'transferable', 'value': b'true'}} + ], + 'levy': levy + } + }) + + # Calculate and attach the transaction fee + fee = calculate_transaction_fee(transaction) + transaction.fee = Amount(fee) + print(f' Transaction fee: {fee / 1_000_000} XEM') + # [step-5] + signature = facade.sign_transaction(signer_key_pair, transaction) + json_payload = facade.transaction_factory.attach_signature( + transaction, signature) + print('Built mosaic definition transaction:') + print(json.dumps(transaction.to_json(), indent=2)) + + announce_path = '/transaction/announce' + print(f'Announcing mosaic definition 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"]}') + + if 'SUCCESS' == announce_result['message']: + transaction_hash = facade.hash_transaction(transaction) + status_path = f'/transaction/get?hash={transaction_hash}' + 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-6] + definition_path = f'/mosaic/definition?mosaicId={mosaic_id}' + print(f'Fetching mosaic information from {definition_path}') + with urllib.request.urlopen( + f'{NODE_URL}{definition_path}' + ) as response: + mosaic_info = json.loads(response.read().decode()) + levy_info = mosaic_info['levy'] + levy_mosaic_id = levy_info['mosaicId'] + levy_type = 'absolute' if 1 == levy_info['type'] else 'percentile' + print('Levy information:') + print(f' Type: {levy_type}') + print(f' Recipient: {levy_info["recipient"]}') + print(f' Mosaic: ' + f'{levy_mosaic_id["namespaceId"]}:{levy_mosaic_id["name"]}') + print(f' Fee: {levy_info["fee"]}') + # [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"]}') + # [step-1] +const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY || + '0000000000000000000000000000000000000000000000000000000000000000'; +const signerKeyPair = new NemFacade.KeyPair( + new PrivateKey(SIGNER_PRIVATE_KEY)); + +const facade = new NemFacade('testnet'); +const signerAddress = facade.network.publicKeyToAddress( + signerKeyPair.publicKey); +console.log('Signer address:', signerAddress.toString()); +// [step-2] + 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 = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + // [step-3] + const namespaceName = process.env.ROOT_NAMESPACE || + `ns_${Math.floor(Date.now() / 1000)}`; + console.log('Creating root namespace:', namespaceName); + // [step-4] + const rentalFee = calculateNamespaceRentalFee(true); + console.log(' Namespace lease fee:', + `${Number(rentalFee) / 1_000_000} XEM`); + + const transaction = facade.transactionFactory.create({ + type: 'namespace_registration_transaction_v1', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + rentalFeeSink: 'TAMESPACEWH4MKFMBCVFERDPOOP4FK7MTDJEYP35', + rentalFee, + name: namespaceName + }); + + // [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 }); + + // Announce the transaction + const announcePath = '/transaction/announce'; + console.log('Announcing namespace registration 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-7] + 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-8] + const namespacePath = `/namespace?namespace=${namespaceName}`; + console.log('Fetching namespace information from', namespacePath); + const namespaceResponse = await fetch(`${NODE_URL}${namespacePath}`); + const namespaceInfo = await namespaceResponse.json(); + console.log('Namespace information:'); + console.log(' Name:', namespaceInfo.fqn); + console.log(' Owner:', namespaceInfo.owner); + console.log(' Registration height:', namespaceInfo.height); + // [step-1] +SIGNER_PRIVATE_KEY = os.getenv( + 'SIGNER_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000000') +signer_key_pair = NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY)) + +facade = NemFacade('testnet') +signer_address = facade.network.public_key_to_address( + signer_key_pair.public_key) +print(f'Signer address: {signer_address}') +# [step-2] + 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 = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + # [step-3] + namespace_name = os.getenv('ROOT_NAMESPACE', f'ns_{int(time.time())}') + print(f'Creating root namespace: {namespace_name}') + # [step-4] + rental_fee = calculate_namespace_rental_fee(True) + print(f' Namespace lease fee: {rental_fee / 1_000_000} XEM') + + transaction = facade.transaction_factory.create({ + 'type': 'namespace_registration_transaction_v1', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'rental_fee_sink': 'TAMESPACEWH4MKFMBCVFERDPOOP4FK7MTDJEYP35', + 'rental_fee': rental_fee, + 'name': namespace_name + }) + # [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)) + + # Announce the transaction + announce_path = '/transaction/announce' + print(f'Announcing namespace registration 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-7] + 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-8] + 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"]}') + print(f' Registration height: {namespace_info["height"]}') + # [step-1] + const rootNamespaceName = process.env.ROOT_NAMESPACE || 'ns_root'; + const childNamespaceName = process.env.SUBNAMESPACE || + `sub_${Math.floor(Date.now() / 1000)}`; + const fullNamespaceName = + `${rootNamespaceName}.${childNamespaceName}`; + console.log('Creating subnamespace:', fullNamespaceName); + // [step-2] + const rentalFee = calculateNamespaceRentalFee(false); + console.log(' Namespace lease fee:', + `${Number(rentalFee) / 1_000_000} XEM`); + + const transaction = facade.transactionFactory.create({ + type: 'namespace_registration_transaction_v1', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + rentalFeeSink: 'TAMESPACEWH4MKFMBCVFERDPOOP4FK7MTDJEYP35', + rentalFee, + parentName: rootNamespaceName, + name: childNamespaceName + }); + + const fee = calculateTransactionFee(transaction); + transaction.fee = new models.Amount(fee); + console.log(` Transaction fee: ${Number(fee) / 1_000_000} XEM`); + // [= 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); + } + // Retrieve the namespace [>step-3] + const namespacePath = `/namespace?namespace=${fullNamespaceName}`; + console.log('Fetching namespace information from', namespacePath); + const namespaceResponse = await fetch(`${NODE_URL}${namespacePath}`); + const namespaceInfo = await namespaceResponse.json(); + console.log('Namespace information:'); + console.log(' Name:', namespaceInfo.fqn); + console.log(' Owner:', namespaceInfo.owner); + console.log(' Registration height:', namespaceInfo.height); + // [step-1] + root_namespace_name = os.getenv('ROOT_NAMESPACE', 'ns_root') + child_namespace_name = os.getenv( + 'SUBNAMESPACE', f'sub_{int(time.time())}') + full_namespace_name = ( + f'{root_namespace_name}.{child_namespace_name}') + print(f'Creating subnamespace: {full_namespace_name}') + # [step-2] + rental_fee = calculate_namespace_rental_fee(False) + print(f' Namespace lease fee: {rental_fee / 1_000_000} XEM') + + transaction = facade.transaction_factory.create({ + 'type': 'namespace_registration_transaction_v1', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'rental_fee_sink': 'TAMESPACEWH4MKFMBCVFERDPOOP4FK7MTDJEYP35', + 'rental_fee': rental_fee, + 'parent_name': root_namespace_name, + 'name': child_namespace_name + }) + + fee = calculate_transaction_fee(transaction) + transaction.fee = Amount(fee) + print(f' Transaction fee: {fee / 1_000_000} XEM') + # [step-3] + namespace_path = f'/namespace?namespace={full_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"]}') + print(f' Registration height: {namespace_info["height"]}') + # [ (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 scale = 10n ** BigInt(divisibility); + + const fmtAtomic = atomic => + `${(atomic / scale).toLocaleString('en-US')}.` + + `${(atomic % scale).toString().padStart(divisibility, '0')}`; + + const NON_CIRCULATING_ADDRESSES = [ + ['Treasury', 'NCHESTYVD2P6P646AMY7WSNG73PCPZDUQNSD6JAK'], + ['Nemesis', 'NANEMOABLAGR72AZ2RV3V4ZHDCXW25XQ73O7OBT5'], + ['Namespace rental', 'NAMESPACEWH4MKFMBCVFERDPOOP4FK7MTBXDPZZA'], + ['Mosaic rental', 'NBMOSAICOD4F54EE5CDMR23CCBGOAM2XSIUX6TRS'] + ]; + let nonCirculatingSupply = 0n; + 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 = BigInt(accountInfo.account.balance); + nonCirculatingSupply += balance; + console.log(` ${label}: ${fmtAtomic(balance)} ${MOSAIC_ID}`); + } + console.log( + 'Non-circulating supply: ' + + `${fmtAtomic(nonCirculatingSupply)} ${MOSAIC_ID}` + ); // [step-4] + const circulatingSupply = + (BigInt(totalSupply) * scale) - nonCirculatingSupply; + console.log( + 'Circulating supply: ' + + `${fmtAtomic(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] + scale = 10 ** divisibility + + def fmt_atomic(atomic): + return f'{atomic // scale:,}.{atomic % scale:0{divisibility}d}' + + 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'] + non_circulating_supply += balance + print(f' {label}: {fmt_atomic(balance)} {MOSAIC_ID}') + print( + f'Non-circulating supply: ' + f'{fmt_atomic(non_circulating_supply)} {MOSAIC_ID}') # [step-4] + circulating_supply = total_supply * scale - non_circulating_supply + print(f'Circulating supply: {fmt_atomic(circulating_supply)} {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) # [ Sending Plain Text Message +Plain message: Hello, NEM! +Transaction hash: 2F8F20CAF8F6FA42ADE05ED85FED0B4D1DA88051972405C4AE2D181B01FB69C3 +Plain message transaction announced + +<== Receiving Plain Text Message +Polling for Plain message transaction confirmation... + Plain message transaction confirmed! +Received plain message: Hello, NEM! + +==> Sending Encrypted Message +Original message: This is a secret message! +Encrypted payload: 3fefcf6e4f1e5e2165f941a5c15ea66778f82eded50cfbde5fc27eba24f4580ff72fb9d0b45061747be0324a120dc357dd11351c09 +Transaction hash: 76604471D5A345E6F5CE20C65D618BC6F9A600F1DF515A696B2152E0E2D0B427 +Encrypted message transaction announced + +<== Receiving Encrypted Message +Polling for Encrypted message transaction confirmation... + Encrypted message transaction confirmed! +Recipient decrypted message: This is a secret message! diff --git a/mkdocs/snippets/devbook/transactions/messages.mjs b/mkdocs/snippets/devbook/transactions/messages.mjs new file mode 100644 index 000000000..fb2238ff5 --- /dev/null +++ b/mkdocs/snippets/devbook/transactions/messages.mjs @@ -0,0 +1,194 @@ +import { PrivateKey, PublicKey } from 'symbol-sdk'; +import { + MessageEncoder, + NemFacade, + NetworkTimestamp, + calculateTransactionFee, + models +} from 'symbol-sdk/nem'; + +// Configuration +const NODE_URL = process.env.NODE_URL || + 'http://libertalia.nemtest.net:7890'; +console.log('Using node', NODE_URL); + +// Helper function to poll for confirmed transaction +async function retrieveConfirmedTransaction(hash, label) { + console.log(`Polling for ${label} confirmation...`); + let attempts = 0; + const maxAttempts = 120; + + while (attempts < maxAttempts) { + const response = await fetch( + `${NODE_URL}/transaction/get?hash=${hash}`); + if (response.ok) { + console.log(` ${label} confirmed!`); + return response.json(); + } + attempts++; + await new Promise(resolve => { setTimeout(resolve, 2000); }); + } + + throw new Error( + `${label} not confirmed after ${maxAttempts} attempts`); +} + +// Set up sender and recipient accounts [>step-1] +const facade = new NemFacade('testnet'); + +const senderPrivateKeyString = process.env.SENDER_PRIVATE_KEY || + '0000000000000000000000000000000000000000000000000000000000000000'; +const senderKeyPair = new NemFacade.KeyPair( + new PrivateKey(senderPrivateKeyString)); +const senderAddress = facade.network.publicKeyToAddress( + senderKeyPair.publicKey); + +const recipientPrivateKeyString = process.env.RECIPIENT_PRIVATE_KEY || + '1111111111111111111111111111111111111111111111111111111111111111'; +const recipientKeyPair = new NemFacade.KeyPair( + new PrivateKey(recipientPrivateKeyString)); +const recipientAddress = facade.network.publicKeyToAddress( + recipientKeyPair.publicKey); + +console.log('Sender address:', senderAddress.toString()); +console.log('Recipient address:', recipientAddress.toString(), '\n'); +// [ Sending Plain Text Message'); // [>step-2] + +// Create a plain text message +const plainMessage = new TextEncoder().encode('Hello, NEM!'); +console.log('Plain message:', + new TextDecoder().decode(plainMessage)); + +// Build transfer transaction with plain message +const plainTransaction = facade.transactionFactory.create({ + type: 'transfer_transaction_v2', + signerPublicKey: senderKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + recipientAddress: recipientAddress.toString(), + amount: 0n, + message: { + messageType: 'plain', + message: plainMessage + } +}); // [step-3] + +// Wait for confirmation +const plainTxData = await retrieveConfirmedTransaction( + plainTransactionHash, 'Plain message transaction'); + +// Decode plain message from confirmed transaction +const receivedPlainMessage = Buffer.from( + plainTxData.transaction.message.payload, 'hex'); +console.log('Received plain message:', + new TextDecoder().decode(receivedPlainMessage), '\n'); +// [ Sending Encrypted Message'); // [>step-4] + +// Create a message encoder with sender's key pair +const senderMessageEncoder = new MessageEncoder(senderKeyPair); + +// Encrypt the message using recipient's public key +const secretMessage = new TextEncoder().encode( + 'This is a secret message!'); +const encryptedMessage = senderMessageEncoder.encode( + recipientKeyPair.publicKey, secretMessage +); +console.log('Original message:', new TextDecoder().decode(secretMessage)); +console.log('Encrypted payload:', + Buffer.from(encryptedMessage.message).toString('hex')); + +// Build transfer transaction with encrypted message +const encryptedTransaction = facade.transactionFactory.create({ + type: 'transfer_transaction_v2', + signerPublicKey: senderKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + recipientAddress: recipientAddress.toString(), + amount: 0n, + message: { + messageType: 'encrypted', + message: encryptedMessage.message + } +}); // [step-5] + +// Wait for confirmation +const encryptedTxData = await retrieveConfirmedTransaction( + encryptedTransactionHash, 'Encrypted message transaction'); + +// Decode encrypted message using recipient's private key +const recipientMessageEncoder = new MessageEncoder(recipientKeyPair); +const receivedEncryptedMessage = new models.Message(); +receivedEncryptedMessage.messageType = models.MessageType.ENCRYPTED; +receivedEncryptedMessage.message = Buffer.from( + encryptedTxData.transaction.message.payload, 'hex'); + +// Get sender's public key from the transaction +const senderPublicKeyFromTx = new PublicKey( + encryptedTxData.transaction.signer); + +const result = recipientMessageEncoder.tryDecode( + senderPublicKeyFromTx, receivedEncryptedMessage); + +if (result.isDecoded) { + console.log('Recipient decrypted message:', + new TextDecoder().decode(result.message)); +} else { + console.log('Recipient failed to decrypt message'); +} // [step-1] +facade = NemFacade('testnet') + +sender_private_key_string = os.getenv( + 'SENDER_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000000', +) +sender_key_pair = NemFacade.KeyPair( + PrivateKey(sender_private_key_string) +) +sender_address = facade.network.public_key_to_address( + sender_key_pair.public_key +) + +recipient_private_key_string = os.getenv( + 'RECIPIENT_PRIVATE_KEY', + '1111111111111111111111111111111111111111111111111111111111111111', +) +recipient_key_pair = NemFacade.KeyPair( + PrivateKey(recipient_private_key_string) +) +recipient_address = facade.network.public_key_to_address( + recipient_key_pair.public_key +) + +print(f'Sender address: {sender_address}') +print(f'Recipient address: {recipient_address}\n') +# [ Sending Plain Text Message') # [>step-2] + +# Create a plain text message +plain_message = 'Hello, NEM!'.encode('utf-8') +print(f'Plain message: {plain_message.decode("utf-8")}') + +# Build transfer transaction with plain message +plain_transaction = facade.transaction_factory.create( + { + 'type': 'transfer_transaction_v2', + 'signer_public_key': sender_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'recipient_address': recipient_address, + 'amount': 0, + 'message': { + 'message_type': 'plain', + 'message': plain_message, + }, + } +) # [step-3] + +# Wait for confirmation +plain_tx_data = retrieve_confirmed_transaction( + plain_transaction_hash, 'Plain message transaction' +) + +# Decode plain message from confirmed transaction +received_plain_message = bytes.fromhex( + plain_tx_data['transaction']['message']['payload'] +) +print( + f'Received plain message: {received_plain_message.decode("utf-8")}\n' +) +# [ Sending Encrypted Message') # [>step-4] + +# Create a message encoder with sender's key pair +sender_message_encoder = MessageEncoder(sender_key_pair) + +# Encrypt the message using recipient's public key +secret_message = 'This is a secret message!'.encode('utf-8') +encrypted_message = sender_message_encoder.encode( + recipient_key_pair.public_key, secret_message +) +print(f'Original message: {secret_message.decode("utf-8")}') +encrypted_payload = hexlify(encrypted_message.message).decode('utf-8') +print(f'Encrypted payload: {encrypted_payload}') + +# Build transfer transaction with encrypted message +encrypted_transaction = facade.transaction_factory.create( + { + 'type': 'transfer_transaction_v2', + 'signer_public_key': sender_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'recipient_address': recipient_address, + 'amount': 0, + 'message': { + 'message_type': 'encrypted', + 'message': encrypted_message.message, + }, + } +) # [step-5] + +# Wait for confirmation +encrypted_tx_data = retrieve_confirmed_transaction( + encrypted_transaction_hash, 'Encrypted message transaction' +) + +# Decode encrypted message using recipient's private key +recipient_message_encoder = MessageEncoder(recipient_key_pair) +received_encrypted_message = Message() +received_encrypted_message.message_type = MessageType.ENCRYPTED +received_encrypted_message.message = bytes.fromhex( + encrypted_tx_data['transaction']['message']['payload'] +) + +# Get sender's public key from the transaction +sender_public_key_from_tx = PublicKey( + encrypted_tx_data['transaction']['signer'] +) + +(is_decoded, decrypted_message) = recipient_message_encoder.try_decode( + sender_public_key_from_tx, received_encrypted_message +) + +if is_decoded: + message_text = decrypted_message.decode('utf-8') + print(f'Recipient decrypted message: {message_text}') +else: + print('Recipient failed to decrypt message') # [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}") # [= attempt; ++attempt) { + const response = await fetch(`${NODE_URL}${statusPath}`); + if (response.ok) { + const confirmed = await response.json(); + console.log(`${label} 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(`${label} confirmation took too long.`); +} + +const facade = new NemFacade('testnet'); +// [>step-1] +const MULTISIG_PUBLIC_KEY = process.env.MULTISIG_PUBLIC_KEY || ( + 'D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2'); +const multisigPublicKey = new PublicKey(MULTISIG_PUBLIC_KEY); +const multisigAddress = facade.network.publicKeyToAddress( + multisigPublicKey); +console.log(`Multisig public key: ${multisigPublicKey}`); +const COSIGNATORY0_PRIVATE_KEY = process.env.COSIGNATORY0_PRIVATE_KEY || ( + '0000000000000000000000000000000000000000000000000000000000000002'); +const cosignatory0KeyPair = new NemFacade.KeyPair( + new PrivateKey(COSIGNATORY0_PRIVATE_KEY)); +console.log(`Cosignatory 0 public key: ${cosignatory0KeyPair.publicKey}`); +const COSIGNATORY1_PRIVATE_KEY = process.env.COSIGNATORY1_PRIVATE_KEY || ( + '0000000000000000000000000000000000000000000000000000000000000003'); +const cosignatory1KeyPair = new NemFacade.KeyPair( + new PrivateKey(COSIGNATORY1_PRIVATE_KEY)); +console.log(`Cosignatory 1 public key: ${cosignatory1KeyPair.publicKey}`); +// [step-2] + 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 = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + // [step-3] + const transferTransaction = facade.transactionFactory.create({ + type: 'transfer_transaction_v2', + signerPublicKey: multisigPublicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + recipientAddress: multisigAddress.toString(), + amount: 1_000_000n // 1 XEM + }); + transferTransaction.fee = new models.Amount( + calculateTransactionFee(transferTransaction)); + // [step-4] + const transaction = facade.transactionFactory.create({ + type: 'multisig_transaction_v1', + // This is the cosignatory that initiates the transfer + signerPublicKey: cosignatory0KeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + innerTransaction: facade.transactionFactory.static + .toNonVerifiableTransaction(transferTransaction) + }); + transaction.fee = new models.Amount( + calculateTransactionFee(transaction)); + // [step-5] + const signature = facade.signTransaction( + cosignatory0KeyPair, transaction); + const jsonPayload = facade.transactionFactory.static.attachSignature( + transaction, signature); + console.log('Built multisig transaction:'); + console.log(JSON.stringify(transaction.toJson(), null, 2)); + const announceResult = await announceTransaction( + jsonPayload, 'multisig transaction'); + // The transaction is now waiting for the second signature + // [step-6] + if ('SUCCESS' === announceResult) { + const cosignatory1Address = facade.network.publicKeyToAddress( + cosignatory1KeyPair.publicKey); + const unconfirmedPath = '/account/unconfirmedTransactions' + + `?address=${cosignatory1Address}`; + console.log('Fetching pending transactions from', + unconfirmedPath); + const unconfirmedResponse = await fetch( + `${NODE_URL}${unconfirmedPath}`); + const pending = (await unconfirmedResponse.json()).data; + // Select the pending transaction issued by the multisig account + const pendingEntry = pending.find(entry => + multisigPublicKey.toString() === (entry.transaction + .otherTrans?.signer ?? '').toUpperCase()); + const innerTransactionHash = pendingEntry.meta.data; + console.log(' Inner transaction hash:', innerTransactionHash); + // [step-7] + const cosignature = facade.transactionFactory.create({ + type: 'cosignature_v1', + // This is the cosignatory providing the second signature + signerPublicKey: cosignatory1KeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + // Hash of the inner transfer transaction + otherTransactionHash: innerTransactionHash, + // Address of the multisig account + multisigAccountAddress: multisigAddress.toString() + }); + cosignature.fee = new models.Amount( + calculateTransactionFee(cosignature)); + // [step-8] + const cosignatureSignature = facade.signTransaction( + cosignatory1KeyPair, cosignature); + const cosignaturePayload = facade.transactionFactory.static + .attachSignature(cosignature, cosignatureSignature); + console.log('Built cosignature:'); + console.log(JSON.stringify(cosignature.toJson(), null, 2)); + const cosignatureResult = await announceTransaction( + cosignaturePayload, 'cosignature'); + // [step-9] + if ('SUCCESS' === cosignatureResult) { + await waitForConfirmation( + facade.hashTransaction(transaction).toString(), + 'multisig transaction'); + } else { + console.log('Transaction rejected:', cosignatureResult); + } + // [step-1] +MULTISIG_PUBLIC_KEY = os.getenv( + 'MULTISIG_PUBLIC_KEY', + 'D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2') +multisig_public_key = PublicKey(MULTISIG_PUBLIC_KEY) +multisig_address = facade.network.public_key_to_address( + multisig_public_key) +print(f'Multisig public key: {multisig_public_key}') +COSIGNATORY0_PRIVATE_KEY = os.getenv( + 'COSIGNATORY0_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000002') +cosignatory0_key_pair = NemFacade.KeyPair( + PrivateKey(COSIGNATORY0_PRIVATE_KEY)) +print(f'Cosignatory 0 public key: {cosignatory0_key_pair.public_key}') +COSIGNATORY1_PRIVATE_KEY = os.getenv( + 'COSIGNATORY1_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000003') +cosignatory1_key_pair = NemFacade.KeyPair( + PrivateKey(COSIGNATORY1_PRIVATE_KEY)) +print(f'Cosignatory 1 public key: {cosignatory1_key_pair.public_key}') +# [step-2] + 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 = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + # [step-3] + transfer_transaction = facade.transaction_factory.create({ + 'type': 'transfer_transaction_v2', + 'signer_public_key': multisig_public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'recipient_address': multisig_address, + 'amount': 1_000_000 # 1 XEM + }) + transfer_transaction.fee = Amount( + calculate_transaction_fee(transfer_transaction)) + # [step-4] + transaction = facade.transaction_factory.create({ + 'type': 'multisig_transaction_v1', + # This is the cosignatory that initiates the transfer + 'signer_public_key': cosignatory0_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'inner_transaction': + facade.transaction_factory.to_non_verifiable_transaction( + transfer_transaction) + }) + transaction.fee = Amount(calculate_transaction_fee(transaction)) + # [step-5] + signature = facade.sign_transaction( + cosignatory0_key_pair, transaction) + json_payload = facade.transaction_factory.attach_signature( + transaction, signature) + print('Built multisig transaction:') + print(json.dumps(transaction.to_json(), indent=2)) + announce_result = announce_transaction( + json_payload, 'multisig transaction') + # The transaction is now waiting for the second signature + # [step-6] + if 'SUCCESS' == announce_result: + cosignatory1_address = facade.network.public_key_to_address( + cosignatory1_key_pair.public_key) + unconfirmed_path = ('/account/unconfirmedTransactions' + f'?address={cosignatory1_address}') + print(f'Fetching pending transactions from {unconfirmed_path}') + with urllib.request.urlopen( + f'{NODE_URL}{unconfirmed_path}' + ) as response: + pending = json.loads(response.read().decode())['data'] + # Select the pending transaction issued by the multisig account + inner_transaction_hash = next( + entry['meta']['data'] for entry in pending + if entry['transaction'].get('otherTrans', {}).get( + 'signer', '').upper() == str(multisig_public_key)) + print(f' Inner transaction hash: {inner_transaction_hash}') + # [step-7] + cosignature = facade.transaction_factory.create({ + 'type': 'cosignature_v1', + # This is the cosignatory providing the second signature + 'signer_public_key': cosignatory1_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + # Hash of the inner transfer transaction + 'other_transaction_hash': inner_transaction_hash, + # Address of the multisig account + 'multisig_account_address': multisig_address + }) + cosignature.fee = Amount(calculate_transaction_fee(cosignature)) + # [step-8] + cosignature_signature = facade.sign_transaction( + cosignatory1_key_pair, cosignature) + cosignature_payload = facade.transaction_factory.attach_signature( + cosignature, cosignature_signature) + print('Built cosignature:') + print(json.dumps(cosignature.to_json(), indent=2)) + cosignature_result = announce_transaction( + cosignature_payload, 'cosignature') + # [step-9] + if 'SUCCESS' == cosignature_result: + wait_for_confirmation( + facade.hash_transaction(transaction), + 'multisig transaction') + else: + print(f'Transaction rejected: {cosignature_result}') + # [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 = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + // [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: timestamp.timestamp, + deadline: deadline.timestamp, + 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 = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + # [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.timestamp, + 'deadline': deadline.timestamp, + '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 = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + // [step-4] + const transaction = facade.transactionFactory.create({ + type: 'transfer_transaction_v2', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + 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 = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + # [step-4] + transaction = facade.transaction_factory.create({ + 'type': 'transfer_transaction_v2', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + '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_multisig_transaction_flow.log b/mkdocs/snippets/devbook/websockets/listen_multisig_transaction_flow.log new file mode 100644 index 000000000..d18c6a8e7 --- /dev/null +++ b/mkdocs/snippets/devbook/websockets/listen_multisig_transaction_flow.log @@ -0,0 +1,19 @@ +Using node http://libertalia.nemtest.net:7890 +Multisig address: TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6 +Cosignatory 0 public key: AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3 +Cosignatory 1 public key: 26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23 +[Cosignatory 0] Built multisig transaction 844BBBB420167B0D... +[Cosignatory 1] Connected to http://libertalia.nemtest.net:7778 +[Cosignatory 1] Subscribed to /account/TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6 channel +[Cosignatory 1] Subscribed to /unconfirmed/TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6 channel +[Cosignatory 1] Subscribed to /transactions/TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6 channel +Account update: balance=9959750000 +[Cosignatory 1] Multisig account registered +[Cosignatory 0] Announcing multisig transaction 844BBBB420167B0D... +unconfirmed: innerHash=3e1fba4d39d9f053... +[Cosignatory 1] Announced cosignature +confirmed: innerHash=3e1fba4d39d9f053... +Multisig transaction confirmed +confirmed: innerHash=3e1fba4d39d9f053... +Account update: balance=9959400000 +[Cosignatory 1] Unsubscribed from all channels diff --git a/mkdocs/snippets/devbook/websockets/listen_multisig_transaction_flow.mjs b/mkdocs/snippets/devbook/websockets/listen_multisig_transaction_flow.mjs new file mode 100644 index 000000000..96bc660fb --- /dev/null +++ b/mkdocs/snippets/devbook/websockets/listen_multisig_transaction_flow.mjs @@ -0,0 +1,223 @@ +import { Client } from '@stomp/stompjs'; +import SockJS from 'sockjs-client'; +import { PrivateKey, PublicKey } from 'symbol-sdk'; +import { + NemFacade, NetworkTimestamp, calculateTransactionFee, models +} from 'symbol-sdk/nem'; + +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}`); + +const facade = new NemFacade('testnet'); +// Set up the multisig and cosignatory accounts [>step-1] +const MULTISIG_PUBLIC_KEY = process.env.MULTISIG_PUBLIC_KEY || ( + 'D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2'); +const multisigPublicKey = new PublicKey(MULTISIG_PUBLIC_KEY); +const multisigAddress = facade.network.publicKeyToAddress( + multisigPublicKey).toString(); +console.log(`Multisig address: ${multisigAddress}`); +const COSIGNATORY0_PRIVATE_KEY = process.env.COSIGNATORY0_PRIVATE_KEY || ( + '0000000000000000000000000000000000000000000000000000000000000002'); +const cosignatory0KeyPair = new NemFacade.KeyPair( + new PrivateKey(COSIGNATORY0_PRIVATE_KEY)); +console.log(`Cosignatory 0 public key: ${cosignatory0KeyPair.publicKey}`); +const COSIGNATORY1_PRIVATE_KEY = process.env.COSIGNATORY1_PRIVATE_KEY || ( + '0000000000000000000000000000000000000000000000000000000000000003'); +const cosignatory1KeyPair = new NemFacade.KeyPair( + new PrivateKey(COSIGNATORY1_PRIVATE_KEY)); +console.log(`Cosignatory 1 public key: ${cosignatory1KeyPair.publicKey}`); +// [step-2] + const timeResponse = await fetch( + `${NODE_URL}/time-sync/network-time`); + const networkTime = Math.floor( + (await timeResponse.json()).receiveTimeStamp / 1000); + const timestamp = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + + const transferTransaction = facade.transactionFactory.create({ + type: 'transfer_transaction_v2', + signerPublicKey: multisigPublicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + recipientAddress: multisigAddress, + amount: 1_000_000n // 1 XEM + }); + transferTransaction.fee = new models.Amount( + calculateTransactionFee(transferTransaction)); + + const transaction = facade.transactionFactory.create({ + type: 'multisig_transaction_v1', + signerPublicKey: cosignatory0KeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + innerTransaction: facade.transactionFactory.static + .toNonVerifiableTransaction(transferTransaction) + }); + transaction.fee = new models.Amount( + calculateTransactionFee(transaction)); + + const signature = facade.signTransaction( + cosignatory0KeyPair, transaction); + const jsonPayload = facade.transactionFactory.static.attachSignature( + transaction, signature); + const transactionHash = + facade.hashTransaction(transaction).toString().toUpperCase(); + const shortHash = transactionHash.substring(0, 16); + console.log( + `[Cosignatory 0] Built multisig transaction ${shortHash}...`); + // [step-3] + const client = new Client({ + webSocketFactory: () => new SockJS(`${WS_URL}/w/messages`) + }); + await new Promise(resolve => { + client.onConnect = resolve; + client.activate(); + }); + console.log(`[Cosignatory 1] Connected to ${WS_URL}`); + // [step-7] + let innerTransactionHash = null; + let resolveCosigned; + const cosigned = new Promise(resolve => { + resolveCosigned = resolve; + }); + const onUnconfirmed = async message => { + if (null !== innerTransactionHash) + return; + const body = JSON.parse(message.body); + const signer = (body.transaction.otherTrans?.signer ?? '') + .toUpperCase(); + if (multisigPublicKey.toString() !== signer) + return; + innerTransactionHash = body.meta.innerHash.data; + console.log( + 'unconfirmed: innerHash=' + + `${innerTransactionHash.substring(0, 16)}...`); + // [step-8] + const cosignature = facade.transactionFactory.create({ + type: 'cosignature_v1', + // This is the cosignatory providing the second signature + signerPublicKey: cosignatory1KeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + // Hash of the inner transfer transaction + otherTransactionHash: innerTransactionHash, + // Address of the multisig account + multisigAccountAddress: multisigAddress + }); + cosignature.fee = new models.Amount( + calculateTransactionFee(cosignature)); + const cosignatureSignature = facade.signTransaction( + cosignatory1KeyPair, cosignature); + const cosignaturePayload = facade.transactionFactory.static + .attachSignature(cosignature, cosignatureSignature); + const cosignatureResponse = await fetch( + `${NODE_URL}/transaction/announce`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: cosignaturePayload + }); + const cosignatureResult = await cosignatureResponse.json(); + if ('SUCCESS' !== cosignatureResult.message) { + console.log( + `Cosignature rejected: ${cosignatureResult.message}`); + resolveCosigned(false); + return; + } + console.log('[Cosignatory 1] Announced cosignature'); + resolveCosigned(true); + // [step-9] + let confirmed = false; + let resolveRegistered; + let resolveDone; + const registered = new Promise(resolve => { + resolveRegistered = resolve; + }); + const done = new Promise(resolve => { + resolveDone = resolve; + }); + const onConfirmed = message => { + const messageHash = JSON.parse(message.body).meta.innerHash.data; + console.log( + `confirmed: innerHash=${messageHash.substring(0, 16)}...`); + if (messageHash === innerTransactionHash && !confirmed) { + console.log('Multisig transaction confirmed'); + confirmed = true; + } + }; + const onAccountUpdate = message => { + const { balance } = JSON.parse(message.body).account; + console.log(`Account update: balance=${balance}`); + resolveRegistered(); + if (confirmed) + resolveDone(); + }; + // [step-4] + const subscriptions = [ + { + channel: `/account/${multisigAddress}`, + handler: onAccountUpdate, + id: 'id-0' + }, + { + channel: `/unconfirmed/${multisigAddress}`, + handler: onUnconfirmed, + id: 'id-1' + }, + { + channel: `/transactions/${multisigAddress}`, + handler: onConfirmed, + id: 'id-2' + } + ]; + for (const { channel, handler, id } of subscriptions) { + client.subscribe(channel, handler, { id }); + console.log(`[Cosignatory 1] Subscribed to ${channel} channel`); + } + // [step-5] + client.publish({ + destination: '/w/api/account/get', + body: JSON.stringify({ account: multisigAddress }) + }); + await registered; + console.log('[Cosignatory 1] Multisig account registered'); + // [step-6] + console.log( + '[Cosignatory 0] Announcing multisig 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) { + // The transaction is now waiting for the second signature + // [step-10] + for (const { id } of subscriptions) + client.unsubscribe(id); + console.log('[Cosignatory 1] Unsubscribed from all channels'); + client.deactivate(); // [step-1] +MULTISIG_PUBLIC_KEY = os.getenv( + 'MULTISIG_PUBLIC_KEY', + 'D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2') +multisig_public_key = PublicKey(MULTISIG_PUBLIC_KEY) +multisig_address = str(facade.network.public_key_to_address( + multisig_public_key)) +print(f'Multisig address: {multisig_address}') +COSIGNATORY0_PRIVATE_KEY = os.getenv( + 'COSIGNATORY0_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000002') +cosignatory0_key_pair = NemFacade.KeyPair( + PrivateKey(COSIGNATORY0_PRIVATE_KEY)) +print(f'Cosignatory 0 public key: {cosignatory0_key_pair.public_key}') +COSIGNATORY1_PRIVATE_KEY = os.getenv( + 'COSIGNATORY1_PRIVATE_KEY', + '0000000000000000000000000000000000000000000000000000000000000003') +cosignatory1_key_pair = NemFacade.KeyPair( + PrivateKey(COSIGNATORY1_PRIVATE_KEY)) +print(f'Cosignatory 1 public key: {cosignatory1_key_pair.public_key}') +# [step-2] + with urllib.request.urlopen( + f'{NODE_URL}/time-sync/network-time' + ) as resp: + network_time = json.loads( + resp.read().decode())['receiveTimeStamp'] // 1000 + timestamp = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + + transfer_transaction = facade.transaction_factory.create({ + 'type': 'transfer_transaction_v2', + 'signer_public_key': multisig_public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'recipient_address': multisig_address, + 'amount': 1_000_000 # 1 XEM + }) + transfer_transaction.fee = Amount( + calculate_transaction_fee(transfer_transaction)) + + transaction = facade.transaction_factory.create({ + 'type': 'multisig_transaction_v1', + 'signer_public_key': cosignatory0_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + 'inner_transaction': + facade.transaction_factory.to_non_verifiable_transaction( + transfer_transaction) + }) + transaction.fee = Amount(calculate_transaction_fee(transaction)) + + signature = facade.sign_transaction( + cosignatory0_key_pair, transaction) + json_payload = facade.transaction_factory.attach_signature( + transaction, signature) + transaction_hash = str(facade.hash_transaction(transaction)).upper() + print('[Cosignatory 0] Built multisig transaction ' + f'{transaction_hash[:16]}...') + # [step-3] + endpoint = f'{WS_URL}/w/messages' + async with connect(sockjs_url(endpoint)) as websocket: + await stomp_connect(websocket) + print(f'[Cosignatory 1] Connected to {WS_URL}') + frames = stomp_frames(websocket) + # [step-4] + channels = { + f'/account/{multisig_address}': 'id-0', + f'/unconfirmed/{multisig_address}': 'id-1', + f'/transactions/{multisig_address}': 'id-2', + } + for channel, sub_id in channels.items(): + await stomp_subscribe(websocket, channel, sub_id) + print(f'[Cosignatory 1] Subscribed to {channel} channel') + # [step-5] + await stomp_send(websocket, '/w/api/account/get', + json.dumps({'account': multisig_address})) + async for frame in frames: + if '/account/' in frame['headers']['destination']: + balance = json.loads( + frame['body'])['account']['balance'] + print(f'Account update: balance={balance}') + break + print('[Cosignatory 1] Multisig account registered') + # [step-6] + print('[Cosignatory 0] Announcing multisig transaction ' + f'{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']: + print(f'Transaction rejected: {result["message"]}') + return + # The transaction is now waiting for the second signature + # [step-7] + inner_transaction_hash = None + async for frame in frames: + destination = frame['headers']['destination'] + body = json.loads(frame['body']) + if '/unconfirmed/' not in destination: + continue + signer = body['transaction'].get( + 'otherTrans', {}).get('signer', '') + if signer.upper() != str(multisig_public_key): + continue + inner_transaction_hash = body['meta']['innerHash']['data'] + print('unconfirmed: innerHash=' + f'{inner_transaction_hash[:16]}...') + # [step-8] + cosignature = facade.transaction_factory.create({ + 'type': 'cosignature_v1', + # This is the cosignatory providing the second signature + 'signer_public_key': cosignatory1_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + # Hash of the inner transfer transaction + 'other_transaction_hash': inner_transaction_hash, + # Address of the multisig account + 'multisig_account_address': multisig_address + }) + cosignature.fee = Amount( + calculate_transaction_fee(cosignature)) + cosignature_signature = facade.sign_transaction( + cosignatory1_key_pair, cosignature) + cosignature_payload = ( + facade.transaction_factory.attach_signature( + cosignature, cosignature_signature)) + cosignature_request = urllib.request.Request( + f'{NODE_URL}/transaction/announce', + data=cosignature_payload.encode(), + headers={'Content-Type': 'application/json'}, + method='POST' + ) + with urllib.request.urlopen(cosignature_request) as resp: + cosignature_result = json.loads(resp.read().decode()) + if 'SUCCESS' != cosignature_result['message']: + print('Cosignature rejected: ' + f'{cosignature_result["message"]}') + return + print('[Cosignatory 1] Announced cosignature') + break + # [step-9] + confirmed = False + async for frame in frames: + destination = frame['headers']['destination'] + body = json.loads(frame['body']) + if '/account/' in destination: + balance = body['account']['balance'] + print(f'Account update: balance={balance}') + if confirmed: + break + elif '/transactions/' in destination: + message_hash = body['meta']['innerHash']['data'] + print(f'confirmed: innerHash={message_hash[:16]}...') + matched = message_hash == inner_transaction_hash + if matched and not confirmed: + print('Multisig transaction confirmed') + confirmed = True + # [step-10] + for sub_id in channels.values(): + await stomp_unsubscribe(websocket, sub_id) + print('[Cosignatory 1] Unsubscribed from all channels') + await stomp_disconnect(websocket) # [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 timeResponse = await fetch( + `${NODE_URL}/time-sync/network-time`); + const networkTime = Math.floor( + (await timeResponse.json()).receiveTimeStamp / 1000); + const timestamp = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + const transaction = facade.transactionFactory.create({ + type: 'transfer_transaction_v2', + signerPublicKey: signerKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + 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(); + const shortHash = transactionHash.substring(0, 16); + // [step-3] + 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-7] + let confirmed = false; + let resolveRegistered; + let resolveDone; + const registered = new Promise(resolve => { + resolveRegistered = resolve; + }); + const done = new Promise(resolve => { + resolveDone = resolve; + }); + const onUnconfirmed = message => { + const messageHash = JSON.parse(message.body).meta.hash.data; + if (messageHash.toUpperCase() === transactionHash) { + console.log( + `unconfirmed: hash=${messageHash.substring(0, 16)}...`); + } + }; + const onConfirmed = message => { + const messageHash = JSON.parse(message.body).meta.hash.data; + console.log(`confirmed: hash=${messageHash.substring(0, 16)}...`); + if (messageHash.toUpperCase() === transactionHash) { + console.log(`Transaction ${shortHash}... confirmed`); + confirmed = true; + } + }; + const onAccountUpdate = message => { + const { balance } = JSON.parse(message.body).account; + console.log(`Account update: balance=${balance}`); + resolveRegistered(); + if (confirmed) + resolveDone(); + }; + // [step-4] + const accountChannel = `/account/${MONITOR_ADDRESS}`; + const subscriptions = [ + { channel: accountChannel, handler: onAccountUpdate, id: 'id-0' }, + { + channel: `/unconfirmed/${MONITOR_ADDRESS}`, + handler: onUnconfirmed, + id: 'id-1' + }, + { + channel: `/transactions/${MONITOR_ADDRESS}`, + handler: onConfirmed, + id: 'id-2' + } + ]; + for (const { channel, handler, id } of subscriptions) { + client.subscribe(channel, handler, { id }); + console.log(`Subscribed to ${channel} channel`); + } + // [step-5] + client.publish({ + destination: '/w/api/account/get', + body: JSON.stringify({ account: MONITOR_ADDRESS }) + }); + await registered; + console.log('Account registered'); + // [step-6] + 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(); + // [step-8] + for (const { id } of subscriptions) + 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] + with urllib.request.urlopen( + f'{NODE_URL}/time-sync/network-time' + ) as resp: + network_time = json.loads( + resp.read().decode())['receiveTimeStamp'] // 1000 + timestamp = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + transaction = facade.transaction_factory.create({ + 'type': 'transfer_transaction_v2', + 'signer_public_key': signer_key_pair.public_key, + 'timestamp': timestamp.timestamp, + 'deadline': deadline.timestamp, + '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-3] + endpoint = f'{WS_URL}/w/messages' + async with connect(sockjs_url(endpoint)) as websocket: + await stomp_connect(websocket) + print(f'Connected to {WS_URL}') + frames = stomp_frames(websocket) + # [step-4] + account_channel = f'/account/{MONITOR_ADDRESS}' + channels = { + account_channel: 'id-0', + 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] + await stomp_send(websocket, '/w/api/account/get', + json.dumps({'account': MONITOR_ADDRESS})) + async for frame in frames: + if account_channel == frame['headers']['destination']: + balance = json.loads( + frame['body'])['account']['balance'] + print(f'Account update: balance={balance}') + break + print('Account registered') + # [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()) + # [step-7] + if 'SUCCESS' == result['message']: + confirmed = False + async for frame in frames: + destination = frame['headers']['destination'] + body = json.loads(frame['body']) + if account_channel == destination: + balance = body['account']['balance'] + print(f'Account update: balance={balance}') + if confirmed: + break + elif '/transactions/' in destination: + message_hash = body['meta']['hash']['data'] + print(f'confirmed: hash={message_hash[:16]}...') + if message_hash.upper() == transaction_hash: + short_hash = transaction_hash[:16] + print(f'Transaction {short_hash}... confirmed') + confirmed = True + else: + message_hash = body['meta']['hash']['data'] + if message_hash.upper() == transaction_hash: + print(f'unconfirmed: hash={message_hash[:16]}...') + else: + print(f'Transaction rejected: {result["message"]}') + # [step-8] + for sub_id in channels.values(): + await stomp_unsubscribe(websocket, sub_id) + print('Unsubscribed from all channels') + await stomp_disconnect(websocket) # [