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..9f9ad70cc --- /dev/null +++ b/mkdocs/config/mkdocs.ja.yml @@ -0,0 +1,137 @@ +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: + nem: + tutorial_level: チュートリアルの難易度 + tutorial_level_labels: + beginner: 初級 + intermediate: 中級 + advanced: 上級 + 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 + - ノード運用: + - userbook/node/install.md + - userbook/node/supernode-program.md + - 開発者マニュアル: + - devbook/intro.md + - はじめに: + - devbook/start/setup.md + - devbook/start/hello-world.md + - チュートリアル: + - アカウント: + - 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 + - トランザクション: + - 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 + - モザイク: + - 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 + - ネームスペース: + - devbook/namespaces/register-root-namespace.md + - devbook/namespaces/register-subnamespace.md + - devbook/namespaces/extend-root-namespace.md + - devbook/namespaces/get-namespace-info.md + - ネットワーク通貨: + - devbook/network-currency/query-currency-supply.md + - devbook/network-currency/query-block-rewards.md + - チェーン状態: + - devbook/chain/chain-heights.md + - WebSocket: + - devbook/websockets/listen-new-blocks.md + - devbook/websockets/listen-transaction-flow.md + - devbook/websockets/listen-multisig-transaction-flow.md + - リファレンスガイド: + - Python SDK: devbook/reference/py/ + - TypeScript SDK: devbook/reference/ts/ + - NEM REST API: devbook/reference/rest/nem.md + - シリアライズ: devbook/reference/serialization/index.md + - WebSocket: devbook/reference/websockets/index.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/accounts/configure-multisig.md b/mkdocs/pages/ja/devbook/accounts/configure-multisig.md new file mode 100644 index 000000000..3514bb303 --- /dev/null +++ b/mkdocs/pages/ja/devbook/accounts/configure-multisig.md @@ -0,0 +1,271 @@ +--- +title: マルチシグを設定する +tutorial_level: advanced +--- + +# マルチシグアカウントを設定する + +[マルチシグアカウント](default:マルチシグアカウント) は _マルチシグ_ とも呼ばれ、そのアカウントだけではトランザクションを開始することはできません。 +代わりに、_連署人_ アカウントがマルチシグアカウントに代わってトランザクションを作成し、署名します。 + +このチュートリアルでは、通常のアカウントを 2 人の連署人のうち、どちらか一方の承認を必要とするマルチシグアカウントに変換する方法を説明します。 +アカウントがすでにマルチシグの場合は、連署人を削除して通常のアカウントに戻す方法を説明します。 + +このチュートリアルで使用するマルチシグ構成は以下の通りです。 + +```dot +digraph "Multisignature Tree" { + rankdir="BT"; + node [fontsize=12]; + "Multisignature Account" [label="マルチシグアカウント"]; + "Cosignatory 0" [label="連署人 0"]; + "Cosignatory 1" [label="連署人 1"]; + + "Cosignatory 0" -> "Multisignature Account"; + "Cosignatory 1" -> "Multisignature Account"; +} +``` + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 +* 3 つの [アカウント](default:アカウント) を作成する。1 つはマルチシグに変換し、残り 2 つは連署人として使用します。 + [コードから](./create-from-private-key.md) 作成することも、[ウォレットを使って](../../userbook/wallet/create-account.md) 作成することもできます。 +* マルチシグに変換するアカウントに、トランザクション手数料を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](./testnet-faucet.md) を参照してください。 + +さらに、トランザクションのアナウンスと承認の方法を理解するため、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルを確認してください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/accounts/configure_multisig', ['py', 'js']) }} + +## コードの説明 {: #code-explanation } + +コードでは、トランザクションをアナウンスし、その承認を待つ 2 つのヘルパー関数を定義します。 +これらの動作の詳細については、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルを参照してください。 +残りのヘルパー関数については、以下のセクションで説明します。 + +次に、マルチシグアカウントに必要な [キーをセットアップ](#setting-up-the-accounts) し、 +[現在のネットワーク時刻を取得](#fetching-network-time) して、 +マルチシグアカウントの [現在の設定を判定](#determining-the-multisig-operation) します。 + +アカウントがすでにマルチシグとして設定されているかどうかに応じて、 +マルチシグを [有効化](#enabling-the-multisig) または [無効化](#disabling-the-multisig) するトランザクションを作成します。 +最後に、トランザクションを [アナウンスして承認](#submitting-the-transactions) します。 + +### アカウントをセットアップする {: #setting-up-the-accounts } + +{{ tutorial.code_snippet_tagged('step-1') }} + +このチュートリアルでは、3 つのアカウントが必要です。 +各アカウントの [秘密鍵](default:秘密鍵) は環境変数で指定できます。 +設定されていない場合は、デフォルト値を使用します。 + +| 環境変数 | デフォルト値 | 用途 | +| --- | --- | --- | +| `MULTISIG_PRIVATE_KEY` | `0000..0001` | マルチシグアカウント | +| `COSIGNATORY0_PRIVATE_KEY` | `0000..0002` | 1 人目の連署人アカウント | +| `COSIGNATORY1_PRIVATE_KEY` | `0000..0003` | 2 人目の連署人アカウント | + +各秘密鍵は 64 文字の 16 進数文字列です。 + +マルチシグアカウントには、トランザクション手数料を支払うために十分な資金が必要です。 +デフォルト値を使用する場合、このアカウントにはすでに資金がある可能性があります。 + +上記のスニペットでは、後で使用するために各アカウントの [キーペア](default:キーペア) と [アドレス](default:アドレス) を導出して保存します。 + +### ネットワーク時刻を取得する {: #fetching-network-time } + +{{ tutorial.code_snippet_tagged('step-2') }} + +ネットワーク時刻は から取得し、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルで説明されている手順に従って、トランザクションの `timestamp` と `deadline` フィールドを導出します。 + +### マルチシグ操作を判定する {: #determining-the-multisig-operation } + +{{ tutorial.code_snippet_tagged('step-3') }} + +このヘルパーは エンドポイントを使い、指定したアドレスの現在の連署人一覧を取得します。 +空のリストが返された場合、そのアカウントはマルチシグアカウントとして設定されていません。 + +!!! warning "既存のマルチシグ設定を確認してください" + + 分かりやすくするため、このチュートリアルでは、連署人のリストが _空でない_ 場合、そのアカウントはこのチュートリアルで設定されたマルチシグであると仮定します。 + + 連署人が異なる場合など、設定が想定したものではない場合、削除トランザクションは拒否されます。 + + アプリケーションでは、変更を試みる前に、連署人の完全なリストと必要な署名の最小数を含む現在の設定を必ず確認してください。 + +{{ tutorial.code_snippet_tagged('step-4') }} + +返された連署人によって、アカウントがマルチシグとして設定されているかどうかが決まり、マルチシグを有効化または無効化するトランザクションを作成するかどうかも決まります。 + +トランザクションを構築する関数と、それらが使用する差分値については、次の 2 つのセクションで説明します。 + +### マルチシグを有効化する {: #enabling-the-multisig } + +{{ tutorial.code_snippet_tagged('step-5') }} + +連署人の追加や削除を含むアカウントのマルチシグ設定変更は、すべて を使って行います。 + +トランザクションでは、次の項目を指定します。 + +* {{ tutorial.var('type') }}: マルチシグ設定の変更では、タイプ を使用します。 + +* {{ tutorial.var('signer_public_key') }}: マルチシグ設定を変更するアカウントの [公開鍵](default:公開鍵)。 + +* {{ tutorial.var('timestamp') }} と {{ tutorial.var('deadline') }}: ネットワーク時刻の手順で計算した値。 + +* {{ tutorial.var('min_approval_delta') }}: マルチシグアカウントからのトランザクションを承認するために必要な連署数の _希望値_ と _現在値_ の差分。 + + この場合、アカウントは最初は通常のアカウントなので、必要な連署数の現在値は `0` です。 + 連署人の 1 人からの署名を必要とするマルチシグアカウントに変換するため、差分を `1` に設定します。 + + 次のセクションで示すように、現在値を _減らす_ 場合は差分を負の値にします。 + +* {{ tutorial.var('modifications') }}: アカウントの連署人に対する変更のリスト。 + 各変更では、[公開鍵](default:公開鍵) で識別される 1 人の連署人を追加または削除します。 + + この場合、`add_cosignatory` 変更を 2 つ使って、[セットアップ段階](#setting-up-the-accounts) で準備した連署人を追加します。 + +!!! note "安全対策" + + プロトコルには、アカウントが無効な状態に固定されることを防ぐ安全機構が含まれています。 + 無効なマルチシグ設定になるトランザクションはエラーで拒否されます。 + 例えば、次の場合です。 + + * 登録済みの連署人の数が、承認に必要な連署数に達していない + * すでに連署人であるアカウントを追加する + * 連署人ではないアカウントを削除する + * 1 つのトランザクションで複数の連署人を削除する + * マルチシグアカウントを連署人として追加する + +{{ tutorial.code_snippet_tagged('step-6') }} + +トランザクション手数料は で計算し、トランザクションに付加します。 +マルチシグアカウント変更トランザクションの固定手数料は 0.5 XEM で、[手数料表](../../textbook/transactions.md#fee-schedule) に示されています。 + +{{ tutorial.code_snippet_tagged('step-7') }} + +最後に、トランザクションに署名します。 +この場合、マルチシグに変換するアカウントの署名だけが必要です。 +連署人は変換トランザクションに署名しません。 + +!!! info "以降は連署人がトランザクションを開始します" + + アカウントでマルチシグを有効にすると、そのアカウント自身の署名は受け付けられなくなります。 + そのアカウントから送信するトランザクション(送金や、さらにマルチシグを変更するトランザクションなど)は、次のセクションに示すように、連署人が開始して署名する必要があります。 + +### マルチシグを無効化する {: #disabling-the-multisig } + +マルチシグ設定を無効にするには、すべての連署人を削除する必要があります。 +手順は有効化の場合と似ていますが、2 つの重要な違いがあります。 +連署人は 1 人ずつ削除する必要があり、マルチシグアカウント自身はトランザクションに署名できません。 + +{{ tutorial.code_snippet_tagged('step-8') }} + +このヘルパーは、連署人を削除する を構築します。 +削除する連署人と適用する承認差分をパラメーターとして受け取ります。 +設定を変更する対象がマルチシグアカウントであるため、{{ tutorial.var('signer_public_key') }} にはマルチシグアカウントの公開鍵を設定します。 + +[マルチシグ操作を判定する](#determining-the-multisig-operation) で示したように、このヘルパーは 2 回呼び出されます。 + +1 回目の呼び出しでは、{{ tutorial.var('cosignatory_key_pairs[1]') }} を承認差分 `0` で削除します。これは、連署人が 1 人残るためです。 + +2 回目の呼び出しでは、残った連署人を承認差分 `-1` で削除し、必要な連署数を `1` から `0` に減らします。 + +{{ tutorial.code_snippet_tagged('step-9') }} + +マルチシグアカウントは自分でトランザクションに署名できないため、それぞれの変更を でラップします。 + +内部の変更トランザクションは で変換し、ラップ用のマルチシグトランザクションに埋め込めるようにします。 + +{{ tutorial.code_snippet_tagged('step-10') }} + +内部トランザクションとラッパーの両方に手数料がかかります。変更には 0.5 XEM、マルチシグラッパーには 0.15 XEM で、[手数料表](../../textbook/transactions.md#fee-schedule) に示されています。 +どちらの手数料もマルチシグアカウントから差し引かれます。 +マルチシグに代わってトランザクションを開始する連署人が手数料を支払うことはありません。 + +{{ tutorial.code_snippet_tagged('step-11') }} + +最後に、それぞれのマルチシグトランザクションに、それを開始する連署人、つまりラッパーの {{ tutorial.var('signer_public_key') }} に設定された連署人が署名します。 +ここでは、2 つの削除を {{ tutorial.var('cosignatory_key_pairs[0]') }} が開始して署名します。 + +このマルチシグでは必要な署名が 1 つだけなので、署名は 1 つで十分です。 +より厳しい設定では、他のトランザクションと同様に、削除に追加の連署人の承認が必要になります。ただし、削除対象の連署人自身の署名は要件に数えられません。 + +最後に残った連署人の削除は特殊なケースです。 +マルチシグアカウントの代わりに署名できるのは連署人だけなので、最後の連署人が 2 つ目のトランザクションで示すように、自身の削除に署名します。 + +連署人を逆の順序で削除することもできます。 +違いは、各トランザクションを開始して署名する連署人だけです。 + +!!! note "その他の設定を無効化する" + + 削除トランザクションが拒否された場合、このチュートリアルのデフォルトとは異なる設定、例えば **2-of-2** マルチシグになっている可能性があります。 + + が返す `minCosignatories` フィールドで必要な連署数を確認し、必要に応じて削除トランザクションを調整してください。 + + 例えば、**2-of-2** マルチシグを無効にするには、次のようにします。 + + 1. Cosignatory 1 を `min_approval_delta` を `-1` に設定して削除します。連署人が 1 人残った状態で、アカウントが 2 つの署名を必要とし続けることはできないためです。 + 2. 承認されたら、`min_approval_delta` を `-1` に設定して Cosignatory 0 を削除します。 + + この場合、削除対象の連署人自身の署名は必要な承認に数えられないため、Cosignatory 0 は両方の削除に署名できます。 + +### トランザクションを送信する {: #submitting-the-transactions } + +{{ tutorial.code_snippet_tagged('step-12') }} + +最後の手順では、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルで説明したように、トランザクションをアナウンスして承認を待ちます。 + +マルチシグを無効にする場合、2 つのマルチシグトランザクションを順番にアナウンスします。 +2 回目の削除は 1 回目の削除が処理された後でのみ有効になるため、コードは 1 回目のトランザクションが承認されてから 2 回目をアナウンスします。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +=== ":material-plus-thick: マルチシグを有効化する" + + ```text linenums="1" hl_lines="2-4 8 24 30 34" + --8<-- 'devbook/accounts/configure_multisig_enable.log' + ``` + + 出力の要点は次のとおりです。 + + * **2~4 行目**: 関係するすべてのアカウントのアドレスと公開鍵。 + * **8 行目**(`Response: No cosignatories`): 現在、連署人が設定されていない。 + * **24 行目と 30 行目**(`cosignatory_public_key`): 追加される連署人の公開鍵。 + * **34 行目**(`"min_approval_delta": 1`): 必要な署名数が 1 つ増える。 + +=== ":material-minus-thick: マルチシグを無効化する" + + ```text linenums="1" hl_lines="2-4 8 29-37 61-69" + --8<-- 'devbook/accounts/configure_multisig_disable.log' + ``` + + 出力の要点は次のとおりです。 + + * **2~4 行目**: 関係するすべてのアカウントのアドレスと公開鍵。 + * **8 行目**(`Response: [ ... ]`): 既存の連署人が検出された。 + * **29~37 行目**(1 つ目のマルチシグトランザクション): 必要な署名数は変わらず、既存の連署人が 1 人削除される。 + * **61~69 行目**(2 つ目のマルチシグトランザクション): 必要な署名数が 1 つ減り、最後に残った連署人が削除される。 + +出力に表示されたトランザクションハッシュを使って、[NEM テストネットエクスプローラー](https://testnet.nem.fyi/) でトランザクションを検索できます。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [現在のマルチシグ設定を取得する](#determining-the-multisig-operation) | | +| [マルチシグアカウントを有効化する](#enabling-the-multisig) | | +| [マルチシグアカウントを無効化する](#disabling-the-multisig) | | +| 変更をマルチシグトランザクションでラップする | | diff --git a/mkdocs/pages/ja/devbook/accounts/create-from-mnemonic.md b/mkdocs/pages/ja/devbook/accounts/create-from-mnemonic.md new file mode 100644 index 000000000..cae055e8f --- /dev/null +++ b/mkdocs/pages/ja/devbook/accounts/create-from-mnemonic.md @@ -0,0 +1,114 @@ +--- +title: ニーモニックから作成 +tutorial_level: beginner +--- + +# ニーモニックからアカウントを作成する + +このチュートリアルでは、[ニーモニックフレーズ](default:ニーモニックフレーズ)(単に _ニーモニック_ とも呼ばれます)を使って、NEM の [アカウント](default:アカウント) を作成する方法を説明します。 + +この方法は、1 つのシードから複数のアカウントを管理する [HD ウォレット](default:HDウォレット) で一般的に使用されます。 + +## 前提条件 {: #prerequisites } + +まだ準備できていない場合は、まず [開発環境のセットアップ](../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') }} + + は、NEM の暗号処理とネットワークユーティリティへのアクセスを提供します。 +ネットワーク名(`testnet` または `mainnet`)で初期化することで、[アドレス](default:アドレス) などのネットワーク固有の値が正しく生成されるようにします。 + +### ニーモニックを定義する {: #defining-a-mnemonic } + +{{ tutorial.code_snippet_tagged('step-2') }} + +この例では、環境変数 `MNEMONIC` に既存のニーモニックがあるかを確認します。 +環境変数が設定されている場合は、そこからニーモニックを読み込みます。 +設定されていない場合は、 を使って新しいランダムなニーモニックを生成します。 + +NEM は [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) 標準を使用します。 +この標準では、ニーモニックを標準化された単語リストから選ばれた 24 個の英単語で表します。 +これらの単語は、派生するすべての秘密鍵を作成するために使われるエントロピー(ランダム性)を符号化します。 + +!!! warning "ニーモニックフレーズを安全に保管してください" + ニーモニックフレーズを使うと、派生したすべてのアカウントと秘密鍵を再生成できます。 + ニーモニックフレーズにアクセスできる人はアカウントを管理でき、失うと永久にアクセスできなくなります。 + + ニーモニックは決して他人と共有せず、必ず安全な場所に保管してください。 + +### ルートノードを導出する {: #deriving-the-root-node } + +{{ tutorial.code_snippet_tagged('step-3') }} + +ニーモニックを定義した後、 はニーモニックとパスワードをルートノードに変換します。 +ルートノードは、子アカウントを導出する際の開始点になります。 + +パスワードは「25 番目の単語」と呼ばれることもある、ニーモニックシードを拡張するオプションの文字列です。 +空のままにすることも、任意の値を設定することもできます。 +使用すると、セキュリティをさらに高められます。 +同じニーモニックでも、パスワードが異なればまったく異なるアカウントが生成されます。 + +!!! note "パスワードのセキュリティ" + パスワードはアカウントの導出に含まれます。 + アカウントを再生成するには、ニーモニックとパスワードの両方が必要です。 + どちらかを失うと、派生したすべてのアカウントにアクセスできなくなります。 + +この例では、パスワードを環境変数 `PASSWORD` から読み込みます。 +設定されていない場合、スニペットはデフォルト値を使用します。 + +### 子アカウントを導出する {: #deriving-the-child-account } + +{{ tutorial.code_snippet_tagged('step-4') }} + +ルートノードは、それぞれ固有のキーとアドレスを持つ複数のアカウントを生成できます。 +これにより、1 つのニーモニックで多くのアカウントを管理しながら、暗号学的には分離できます。 + +アカウントを導出するには、アカウントインデックスを指定する必要があります。 + は、そのインデックスに対する導出パス(どのアカウントを導出するかを指定する標準化された文字列)を生成し、 はそのパスに従ってアカウントを作成します。 + +この例では、インデックス `0` のアカウントを導出します。 +別のインデックス(例: `1`、`2`、`3`、...)を使えば、追加のアカウントを導出できます。 +各インデックスからは、まったく異なるアカウントが生成されます。 + +### アカウントを作成する {: #creating-the-account } + +{{ tutorial.code_snippet_tagged('step-5') }} + +子ノードを導出したら、使用可能なキーペアとアドレスに変換します。 + +1. **キーペアの作成:** は子ノードから [秘密鍵](default:秘密鍵) と [公開鍵](default:公開鍵) を取り出します。 + 秘密鍵は秘密にしておく必要がありますが、公開鍵は安全に共有できます。 + +2. **アドレスの導出:** は公開鍵を [アドレス](default:アドレス) に変換します。 + アドレスは、アカウントを識別する、より短く人間が読みやすいネットワーク固有の識別子です。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text +--8<-- 'devbook/accounts/create_from_mnemonic.log' +``` + +環境変数なしでコードを実行するたびに、異なるランダムニーモニックとアカウントが生成されます。 +同じニーモニックとパスワードを指定した場合は、指定したアカウントインデックスから常に同じアカウントが導出されます。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [ランダムなニーモニックを作成する](#defining-a-mnemonic) | | +| [ニーモニックからアカウントを導出する](#deriving-the-root-node) | | +| [アカウントのキーペアを取得する](#creating-the-account) | | diff --git a/mkdocs/pages/ja/devbook/accounts/create-from-private-key.md b/mkdocs/pages/ja/devbook/accounts/create-from-private-key.md new file mode 100644 index 000000000..719321a8a --- /dev/null +++ b/mkdocs/pages/ja/devbook/accounts/create-from-private-key.md @@ -0,0 +1,83 @@ +--- +title: 秘密鍵から作成 +tutorial_level: beginner +--- + +# 秘密鍵からアカウントを作成する + +このチュートリアルでは、既存の [秘密鍵](default:秘密鍵) を使うか、新しいランダムアカウントを生成して、NEM の [アカウント](default:アカウント) を作成する方法を説明します。 + +## 前提条件 {: #prerequisites } + +まだ準備できていない場合は、まず [開発環境のセットアップ](../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') }} + + は、NEM の暗号処理とネットワークユーティリティへのアクセスを提供します。 +ネットワーク名(`testnet` または `mainnet`)で初期化することで、[アドレス](default:アドレス) などのネットワーク固有の値が正しく生成されるようにします。 + +### 秘密鍵を定義する {: #defining-a-private-key } + +{{ tutorial.code_snippet_tagged('step-2') }} + +この例では、まず環境変数 `PRIVATE_KEY` から秘密鍵を 16 進数文字列として取得します。 +環境変数が設定されている場合は、その値を オブジェクトに変換します。 +設定されていない場合は、代わりに を使って新しいランダムな秘密鍵を生成します。 + +!!! warning "秘密鍵を安全に保管してください" + 秘密鍵があれば、アカウントとそこに保有されている資産を完全に管理することができます。 + 秘密鍵を失うと、アカウントに永久にアクセスできなくなります。 + 他人が秘密鍵を入手すると、その人物がアカウントを管理できてしまいます。 + + 秘密鍵は決して誰とも共有せず、必ず安全な場所に保管してください。 + +### アカウントを作成する {: #creating-the-account } + +{{ tutorial.code_snippet_tagged('step-3') }} + +秘密鍵を定義した後、公開鍵とアドレスを導出してアカウントを作成します。 + +1. **キーペアの作成:** コンストラクターは秘密鍵を受け取り、対応する [公開鍵](default:公開鍵) を数学的に導出します。 + 秘密鍵は秘密にしておく必要がありますが、公開鍵は安全に誰とでも共有できます。 + +2. **アドレスの導出:** メソッドは公開鍵を [アドレス](default:アドレス) に変換します。 + アドレスは、アカウントを識別する、より短く人間が読みやすいネットワーク固有の識別子です。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text +--8<-- 'devbook/accounts/create_from_private_key.log' +``` + +環境変数なしでプログラムを実行するたびに、異なるランダムなアカウントが生成されます。 +秘密鍵を指定した場合は、常に同じ公開鍵とアドレスが導出されます。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [秘密鍵を読み込む](#defining-a-private-key) | | +| [ランダムな秘密鍵を作成する](#defining-a-private-key) | | +| [公開鍵を取得する](#creating-the-account) | | +| [アドレスを取得する](#creating-the-account) | | + +## 次のステップ {: #next-steps } + +アカウントを作成できたので、次のことを行えます。 + +* [テストネットの資金をフォーセットから取得する](./testnet-faucet.md) +* [最初のトランザクションを送信する](../transactions/transfer-xem.md) diff --git a/mkdocs/pages/ja/devbook/accounts/query-balance.md b/mkdocs/pages/ja/devbook/accounts/query-balance.md new file mode 100644 index 000000000..7f5733391 --- /dev/null +++ b/mkdocs/pages/ja/devbook/accounts/query-balance.md @@ -0,0 +1,94 @@ +--- +title: アカウント残高の照会 +tutorial_level: beginner +--- + +# アカウント残高を照会する + +NEM の [アカウント](default:アカウント) は、ネイティブ通貨である [XEM](default:XEM) を含む [モザイク](default:モザイク)(代替可能トークン)を保有できます。 + +このチュートリアルでは、アカウントのモザイク残高を照会し、NEM の整数で表される [原子単位の数量](../../textbook/mosaics.md#divisibility) を小数形式で表示する方法を説明します。 + +## 前提条件 {: #prerequisites } + +このチュートリアルでは、[NEM REST API](../reference/rest/nem.md) を使用し、[SDK](default:SDK) は必要ありません。 +HTTP リクエストを送信する方法だけが必要です。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/accounts/query_balance', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM API ノードを指定します。 +値が指定されていない場合は、デフォルト値を使用します。 + +チュートリアルでは、次の関数を定義します。 + +* {{ tutorial.var('get_mosaic_balances()') }}: アカウントが所有するすべての [モザイク](default:モザイク) を取得します。 +* {{ tutorial.var('get_mosaic_definitions()') }}: [可分性](default:可分性) を含むモザイク定義を取得します。 +* {{ tutorial.var('format_amount()') }}: モザイクの [可分性](default:可分性) に応じた小数桁数で金額を整形します。 + +## コードの説明 {: #code-explanation } + +### モザイク残高を取得する {: #fetching-mosaic-balances } + +{{ tutorial.code_snippet_tagged('step-2') }} + + エンドポイントは、アカウントが保有するすべてのモザイクと、その数量を _原子単位_ で返します。 + +### モザイク定義を取得する {: #fetching-mosaic-definitions } + +{{ tutorial.code_snippet_tagged('step-3') }} + +モザイク残高を正しく整形するため、スニペットはネットワークからモザイク定義を取得します。 +必要となる主なプロパティは [可分性](default:可分性) で、モザイクがサポートする小数桁数を定義します。 + + エンドポイントは、アカウントが所有するすべてのモザイクの定義を、可分性やその他のプロパティとともに 1 回のリクエストで返します。 + +### 金額を整形する {: #formatting-amounts } + +{{ tutorial.code_snippet_tagged('step-4') }} + +このユーティリティ関数は、_原子単位_ の数量を人が読みやすい形式に変換します。 + +* **原子単位の数量:** ブロックチェーンに保存される整数の生の値。 +* **整形済みの金額:** モザイクの可分性によって決まる小数桁数を使った表示形式。 + +整形では、\(10^{\text{divisibility}}\) に対する除算と剰余によって、原子単位の数量を整数部分と小数部分に分けます。 +その後、小数部分をゼロで埋め、常に正しい小数桁数で表示されるようにします。 + +### すべてを組み合わせる {: #putting-it-all-together } + +{{ tutorial.code_snippet_tagged('step-5') }} + +メインコードは `ADDRESS` 環境変数を読み込み、照会するアカウントを決定します。 +値が指定されていない場合は、デフォルトのサンプルアドレスを使用します。 + +ヘルパー関数を組み合わせて、次の処理を行います。 + +1. アカウントのモザイク残高を取得する。 +2. モザイク定義を取得して、それぞれの可分性を確認する。 +3. 各モザイクを反復処理し、適切な小数桁数で残高を整形する。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text +--8<-- 'devbook/accounts/query_balance.log' +``` + +出力には、アカウントが保有するすべてのモザイクが表示されます。モザイクによって可分性の値が異なることに注目してください。 + +* 1 つ目のモザイクは `nem:xem` で、ネットワークのネイティブ通貨です。可分性は 6 なので、小数点以下 6 桁(`9883.200000`)で表示されます。 +* 2 つ目のモザイクは `company:token` で、ユーザー定義モザイクです。可分性は 0 なので、整数(`1000000`)で表示されます。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [モザイク残高を取得する](#fetching-mosaic-balances) | | +| [モザイク定義を取得する](#fetching-mosaic-definitions) | | diff --git a/mkdocs/pages/ja/devbook/accounts/testnet-faucet.md b/mkdocs/pages/ja/devbook/accounts/testnet-faucet.md new file mode 100644 index 000000000..93168b776 --- /dev/null +++ b/mkdocs/pages/ja/devbook/accounts/testnet-faucet.md @@ -0,0 +1,87 @@ +--- +title: フォーセットで資金を調達する +tutorial_level: beginner +--- + +# フォーセットからテストネットの資金を取得する + +NEM の [テストネット](default:テストネット) には、テスト用に開発者の [アカウント](default:アカウント) へ無料の [XEM](default:XEM) を配布するフォーセットがあります。 +このガイドでは、Web ベースのフォーセットを使ってテストネットの資金を請求する方法を説明します。 + +!!! note + テストネットの XEM に現実世界での価値はありません。 + 現実の通貨を使わずに NEM の機能を試せるようにするためだけに存在します。 + + [メインネット](default:メインネット) の XEM が必要な場合は、[取引所](https://coinmarketcap.com/currencies/nem/#Markets) で購入する必要があります。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 資金を受け取るテストネットの [アカウント](default:アカウント) を作成する。 + [コードから](../accounts/create-from-private-key.md) 作成することも、[ウォレットを使って](../../userbook/wallet/create-account.md) 作成することもできます。 +* フォーセットで本人確認を行うための 𝕏 アカウントを用意する。 + +## テストネットの資金を請求する {: #how-to-claim-testnet-funds } + +{% import 'tutorial.jinja2' as tutorial %} + +{{ tutorial.list_begin() }} + +{{ tutorial.step_begin("faucet-open.jpg") }} +Web ブラウザーを開き、NEM テストネットフォーセット [testnet.nem.tools](https://testnet.nem.tools) に移動します。 +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-sign-in.jpg") }} +**Sign in with Twitter**(現在の 𝕏)をクリックし、認証手順に従います。 + +この手順では、フォーセットの悪用を防ぐため、テスト用資金を 1 アカウントあたり 10'000 XEM に制限します。 +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-authorize.jpg") }} +サインインすると、𝕏 から、フォーセットアプリケーションによるアカウント情報へのアクセスを許可するよう求められます。 + +権限を確認し、**Authorize app** をクリックして続行します。 +許可すると、再びフォーセットへリダイレクトされます。 +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-address.jpg") }} +資金を受け取るアドレスを **Your Testnet Address** フィールドに入力します。 + +アドレスがテストネットのアカウントを意味する `T` で始まっていることを確認してください。 +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-xem.jpg") }} +**XEM Amount** フィールドに、請求する XEM の量を指定します。 +1 回のリクエストの最大量は 10'000 XEM です。 +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-claim.jpg") }} +**Claim** をクリックしてリクエストを送信します。 +リクエストが成功すると、フォーセットは指定した量の XEM をアドレスに送信します。 +{{ tutorial.step_end() }} + +{{ tutorial.step_begin("faucet-view-explorer.jpg") }} +右上に表示される通知の **View in Explorer** をクリックして、トランザクションが処理されたことを確認します。 + +エクスプローラーには、確認状態を含むトランザクションの詳細が表示されます。 +通常のネットワーク状態では、トランザクションは 1 分ほどで承認されます。 + +ウォレットを設定している場合は、[ウォレット](default:ウォレット) から送金を監視することもできます。 +{{ tutorial.step_end() }} + +{{ tutorial.list_end() }} + +## フォーセットに資金を返す {: #returning-funds-to-the-faucet } + +テストが終わったら、使っていない XEM をフォーセットに返すことを検討してください。 +フォーセットのアドレスは、資金を送ってきたアドレスと同じです。 + +[ブロックチェーンエクスプローラー](https://testnet.nem.fyi/) でトランザクションを確認するか、アカウントのトランザクション履歴を検索すると、送信者のアドレスを確認できます。 + +さらによい方法は、テストトランザクションの受取人としてフォーセットのアドレスを使うことです。 +これにより、トランザクションの送信を練習しながら、他の開発者が使えるフォーセットの資金を維持できます。 + +## 次のステップ {: #next-steps } + +次は [転送トランザクションを送信する](../transactions/transfer-xem.md) のはいかがでしょうか。 diff --git a/mkdocs/pages/ja/devbook/chain/chain-heights.md b/mkdocs/pages/ja/devbook/chain/chain-heights.md new file mode 100644 index 000000000..baa989ad1 --- /dev/null +++ b/mkdocs/pages/ja/devbook/chain/chain-heights.md @@ -0,0 +1,105 @@ +--- +title: チェーン高と不可逆高 +tutorial_level: beginner +--- + +# チェーン高と不可逆高を照会する + + エンドポイントは、現在のチェーン高を返します。 + +**不可逆高** は、これ以上ロールバックできない最も高いブロックです。 +NEM では、現在のチェーン高から [書き換え制限](default:書き換え制限) を引いて計算します。 + +このチュートリアルでは、ループでチェーン高をポーリングし、不可逆高を計算して、チェーン高が最後に変化してからどれくらい時間が経過したかを追跡する方法を説明します。 + +## 前提条件 {: #prerequisites } + +このチュートリアルでは、SDK を必要とせずに [NEM REST API](../reference/rest/nem.md) を使用します。 +HTTP リクエストを送信する方法だけが必要です。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/chain/chain_heights', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM API ノードを指定します。 +値が指定されていない場合は、デフォルト値を使用します。 + +プログラムは無限ループで実行され、1 秒ごとにステータス行を表示します。 +キーボード割り込み(`Ctrl+C`)でループを停止できます。 + +## コードの説明 {: #code-explanation } + +### チェーン高を取得する {: #fetching-chain-height } + +{{ tutorial.code_snippet_tagged('step-1') }} + +ループのたびに、コードは エンドポイントへ `GET` リクエストを送信します。 +レスポンスには `height` フィールドが 1 つ含まれ、ノードが認識している最新のブロックである現在のチェーン高を返します。 + +新しいブロックが生成されるたびに、チェーン高は増加します(およそ 60 秒ごとです)。 + +### 不可逆高を計算する {: #calculating-the-irreversible-height } + +{{ tutorial.code_snippet_tagged('step-2') }} + +[書き換え制限](default:書き換え制限) は、NEM でロールバックによって取り消せるブロック数の上限で、**360 ブロック**(およそ 6 時間)に設定されています。 + +現在のチェーン高から書き換え制限を引くと、**不可逆高** が得られます。 + +不可逆高以下のブロックは、これ以上ロールバックできません。 + +ロールバックと書き換え制限の詳細については、テキストブックの [コンセンサス](../../textbook/consensus.md#conflict-resolution) セクションを参照してください。 + +### 高さの変化を追跡する {: #tracking-height-changes } + +{{ tutorial.code_snippet_tagged('step-3') }} + +チェーン高が最後に変化してからの経過時間を表示するため、コードは前回の高さと、最後に更新された時刻を保存します。 + +新しいブロックが到着して高さが変化するたびに、タイムスタンプを更新します。 +その後、経過時間を現在のチェーン高とともに表示します。 + +### ポーリングループ {: #polling-loop } + +{{ tutorial.code_snippet_tagged('step-4') }} + +各ループでは、次の情報を示すステータス行を 1 行表示します。 + +* 現在のチェーン高と、最後に変化してからの経過秒数。 +* 不可逆高。 + +その後、ノードをもう一度照会する前に 1 秒間スリープします。 + +## 出力 {: #output } + +以下の出力は、チェーン高と不可逆高を監視した場合の実行例です。 + +```text linenums="1" hl_lines="5" +--8<-- 'devbook/chain/chain_heights.log' +``` + +出力の要点は次のとおりです。 + +* **新しいブロックの前**(2~4 行目): プログラムがポーリングを続ける間、チェーン高は変わりません。 +* **新しいブロックが到着**(5 行目): チェーン高が `659,471` から `659,472` に進みます。 + 不可逆高も進みます。 + +!!! note "チェーン高と不可逆高の違い" + + 不可逆高は常に [書き換え制限](default:書き換え制限) だけチェーン高より遅れます。 + チェーンの先端付近にあるトランザクションは、まだロールバックされる可能性があります。 + そのブロックの高さが不可逆高以下になると、不可逆になります。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [チェーン高を取得する](#fetching-chain-height) | | + +## 次のステップ {: #next-steps } + +新しいブロックをイベント駆動で監視する方法については、WebSocket チュートリアルの [新しいブロックをリッスンする](../websockets/listen-new-blocks.md) を参照してください。 diff --git a/mkdocs/pages/ja/devbook/intro.md b/mkdocs/pages/ja/devbook/intro.md new file mode 100644 index 000000000..d03dce6d0 --- /dev/null +++ b/mkdocs/pages/ja/devbook/intro.md @@ -0,0 +1,35 @@ +--- +title: 開発者マニュアルへようこそ +--- + +# 開発者マニュアルへようこそ + +開発者マニュアルは、NEM 上でアプリケーションを構築する開発者向けのドキュメントです。 +[SDK](default:SDK) または HTTP API を使って一般的なタスクを実行する方法を、複数のプログラミング言語によるコード例で説明します。 + +マニュアルは次のように構成されています。 + +
+ +* :material-laptop: **はじめに** + + 開発マシンをセットアップし、簡単な `Hello World` サンプルを実行して準備が整っていることを確認します。 + +* :material-school: **チュートリアル** + + 分野ごとにまとめられた、タスクに焦点を当てたチュートリアルに沿って進めます。 + 各チュートリアルには、背景情報が必要な場合に参照できる [テキストブック](../textbook/intro.md) と関連するリファレンスガイドへのリンクがあります。 + +* :material-book-open-page-variant: **リファレンスガイド** + + SDK のメソッド、HTTP および WebSocket エンドポイント、バイナリ構造について網羅的な情報を参照できます。 + +
+ +ナビゲーションメニューを使うか、以下のチュートリアルのいずれかに直接移動してください。 + +チュートリアルは、NEM の概念に関する必要な知識に応じて、初級から上級までのレベルに分かれています。 + +{% import 'tutorials_table.jinja2' as tutorials_table with context %} + +{{ tutorials_table.render() }} diff --git a/mkdocs/pages/ja/devbook/mosaics/change-mosaic-supply.md b/mkdocs/pages/ja/devbook/mosaics/change-mosaic-supply.md new file mode 100644 index 000000000..6b517a8fe --- /dev/null +++ b/mkdocs/pages/ja/devbook/mosaics/change-mosaic-supply.md @@ -0,0 +1,154 @@ +--- +title: モザイクの供給量を変更する +tutorial_level: intermediate +--- + +# モザイクの供給量を変更する + +[供給量が可変](../../textbook/mosaics.md#supply-mutability) として作成された [モザイク](default:モザイク) は、作成後に総供給量を増減できます。 + +供給量を変更できるのはモザイクの作成者だけです。 +供給量の変更が影響するのは作成者の残高だけです。追加発行したモザイクは追加され、焼却したモザイクは削除されます。 +モザイクを保有する他のアカウントの残高は変わりません。 + +このチュートリアルでは、モザイクの追加発行と焼却によってモザイクの供給量を変更する方法を説明します。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 +* 供給量の変更に署名する [アカウント](default:アカウント) を使い、[供給量が可変](../../textbook/mosaics.md#supply-mutability) なモザイクを作成する。 + [モザイクを作成する](./create-mosaic.md) チュートリアルを参照してください。 +* モザイクを保持する [ネームスペース](default:ネームスペース) をアクティブな状態に保つ。 + テキストブックの [有効期間](../../textbook/mosaics.md#lifetime) を参照してください。 +* トランザクション手数料を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](../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 } + +モザイクの供給量を変更するには、 トランザクションを使います。 +このチュートリアルでは、モザイクを追加発行するものと焼却するものの 2 つをアナウンスします。 + +どちらのトランザクションも同じ方法で送信するため、スニペットでは {{ tutorial.var('announce_transaction') }} と {{ tutorial.var('wait_for_confirmation') }} の 2 つのヘルパーを定義します。これらはトランザクションをアナウンスし、ブロックに含まれるまでネットワークをポーリングします。 + +3 つ目のヘルパー {{ tutorial.var('fetch_supply') }} は、 からモザイクの現在の供給量を読み取り、各トランザクションの効果を確認できるようにします。 + +### アカウントとモザイクをセットアップする {: #setting-up-the-account-and-the-mosaic } + +{{ tutorial.code_snippet_tagged('step-1') }} + +スニペットは、署名者の秘密鍵を `SIGNER_PRIVATE_KEY` 環境変数から読み込みます。 +設定されていない場合は、テスト用のデフォルトキーを使用します。 +署名者はモザイクの作成者でなければなりません。 + +更新するモザイクは、`NAMESPACE` と `MOSAIC` 環境変数から読み込みます。デフォルト値は `my_namespace:token` です。 + +!!! warning "署名者が作成したモザイクを使用してください" + + デフォルトでは、コードは `SIGNER_PRIVATE_KEY` が参照するテストアカウントと、`my_namespace:token` という名前のモザイクを使用します。 + + [モザイクを作成する](./create-mosaic.md) チュートリアルから続けている場合は、`SIGNER_PRIVATE_KEY`、`NAMESPACE`、`MOSAIC` 環境変数を、そこで作成したアカウントとモザイク、または署名者が所有する別のモザイクに合わせて設定してください。 + +### ネットワーク時刻を取得する {: #fetching-network-time } + +{{ tutorial.code_snippet_tagged('step-2') }} + +ネットワーク時刻は から取得し、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルで説明されている手順に従って、トランザクションの `timestamp` と `deadline` フィールドを導出します。 + +### 供給量を増やす(追加発行) {: #increasing-supply-minting } + +{{ tutorial.code_snippet_tagged('step-3') }} + +スニペットはまずモザイクの供給量を読み取り、トランザクションが承認されたときに新しく発行されたモザイク数量を確認できるようにします。 + +追加発行するには、トランザクションに次の値を設定します。 + +* {{ tutorial.var('type') }}: モザイク供給量変更トランザクションでは、タイプ を使用します。 + +* {{ tutorial.var('signer_public_key') }}: トランザクションに署名して手数料を支払うアカウント。モザイクの作成者でなければなりません。 + +* {{ tutorial.var('timestamp') }} と {{ tutorial.var('deadline') }}: ネットワーク時刻の手順で計算した値。 + +* {{ tutorial.var('mosaic_id') }}: 更新するモザイクの [完全修飾名](../../textbook/mosaics.md#fully-qualified-name)。 + +* {{ tutorial.var('action') }}: `increase` の値は、モザイクを追加発行します。 + +* {{ tutorial.var('delta') }}: 追加する [全体単位](../../textbook/mosaics.md#divisibility) の数。 + 結果として得られる総供給量は [最大供給量](../../textbook/mosaics.md#initial-supply) を超えられません。 + + !!! note "上限は原子単位で表されます" + + 最大供給量は $9 \cdot 10^{15}$ **原子**単位で固定されていますが、{{ tutorial.var('delta') }} は **全体単位**で表されます。 + + したがって、{{ tutorial.var('delta') }} の最大値はモザイクの [可分性](default:可分性) によって異なります。 + + \[ + \text{max\_whole\_units} = \frac{9 \cdot 10^{15}}{10^{\text{divisibility}}} + \] + + このチュートリアルのモザイクの可分性は `2` なので、1 全体単位は $100$ 原子単位に相当し、供給量は最大 $9 \cdot 10^{13}$ 全体単位まで増やせます。 + +その後、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルと同じ手順で、トランザクション手数料を計算し、トランザクションに署名して、アナウンスし、承認を待ちます。 + +モザイク供給量変更トランザクションの固定手数料は 0.15 XEM で、[手数料表](../../textbook/transactions.md#fee-schedule) に示されています。 + +承認されたら、供給量をもう一度読み取り、結果の供給量を表示します。 +追加発行されたモザイクは作成者のアカウントに加算されます。 + +### 供給量を減らす(焼却) {: #decreasing-supply-burning } + +{{ tutorial.code_snippet_tagged('step-4') }} + +既存のモザイクを焼却するには、同じトランザクションタイプを使い、{{ tutorial.var('action') }} を `decrease` に、{{ tutorial.var('delta') }} を削除する全体単位数に設定します。 + +焼却するモザイクは作成者のアカウントから取得されるため、作成者がまだ保有している量だけを焼却できます。 +他のアカウントにすでに配布されたモザイクは、そのアカウントの残高に残り、作成者自身の残高が {{ tutorial.var('delta') }} をカバーできない場合はトランザクションが失敗します。 + +承認されたら、供給量をもう一度読み取り、焼却された量を表示します。 +このチュートリアルでは供給量を同じ量だけ増やしてから減らすため、最終的な供給量は変更前の値と一致します。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text linenums="1" hl_lines="8 25-26 35 54-55 64" +--8<-- 'devbook/mosaics/change_mosaic_supply.log' +``` + +出力の要点は次のとおりです。 + +* **発行前の供給量**(8 行目): モザイクは 1000 全体単位の供給量で始まります。 + +* **供給量の増加**(25~26 行目): デルタが `500` の `increase` アクションにより、500 全体単位が作成者の残高に追加されます。 + +* **発行後の供給量**(35 行目): 供給量が 1500 全体単位に増えます。 + +* **供給量の減少**(54~55 行目): 同じデルタの `decrease` アクションによって、それらの単位が焼却されます。 + +* **焼却後の供給量**(64 行目): 増加と減少が相殺されるため、供給量は 1000 に戻ります。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [モザイクの供給量を増やす(追加発行)](#increasing-supply-minting) | | +| [モザイクの供給量を減らす(焼却)](#decreasing-supply-burning) | | +| [トランザクション手数料を計算する](#increasing-supply-minting) | | +| [モザイクの供給量を読み取る](#increasing-supply-minting) | | + +## 次のステップ {: #next-steps } + +モザイクの供給量を変更できるようになったので、次のことを行えます。 + +* [転送トランザクションでモザイクを送信する](../transactions/transfer-mosaics.md) ことで、他のアカウントに配布する +* [モザイク情報を取得する](./get-mosaic-info.md) ことで、プロパティと供給量を確認する diff --git a/mkdocs/pages/ja/devbook/mosaics/create-mosaic.md b/mkdocs/pages/ja/devbook/mosaics/create-mosaic.md new file mode 100644 index 000000000..51b6aef98 --- /dev/null +++ b/mkdocs/pages/ja/devbook/mosaics/create-mosaic.md @@ -0,0 +1,199 @@ +--- +title: モザイクを作成する +tutorial_level: intermediate +--- + +# モザイクを作成する + +[モザイク](default:モザイク) は、通貨、コレクティブル、アクセス権など、NEM ブロックチェーン上の資産を表します。 +他のプラットフォームのトークンとは異なり、NEM のモザイクはプロトコルレベルで直接サポートされているため、利用に追加のコーディングは必要ありません。 + +モザイクのプロパティは設定可能で、単純な通貨から、供給量や転送ルールをカスタマイズしたトークンまで、さまざまな用途に対応できます。 + +すべてのモザイクは登録済みの [ネームスペース](default:ネームスペース) に属します。 +ネームスペースは、`my_namespace:token` のような [完全修飾名](../../textbook/mosaics.md#fully-qualified-name) の前半部分を提供します。 +そのため、モザイクを作成する前にネームスペースを登録する必要があります。 + +このチュートリアルでは、既存のネームスペースの下にモザイクを作成し、初期プロパティを設定する方法を説明します。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 +* モザイクを所有する [アカウント](default:アカウント) を、[コード](../accounts/create-from-private-key.md) または [ウォレット](../../userbook/wallet/create-account.md) を使って作成する。 +* モザイクが属する [ネームスペース](default:ネームスペース) を登録する。 + [ルートネームスペースを登録する](../namespaces/register-root-namespace.md) を参照してください。 +* トランザクションと作成手数料を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](../accounts/testnet-faucet.md) を参照してください。 + +さらに、トランザクションのアナウンスと承認の方法を理解するため、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルを確認してください。 + +## 完全なコード {: #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') }} + +スニペットは、署名者の秘密鍵を `SIGNER_PRIVATE_KEY` 環境変数から読み込みます。 +設定されていない場合は、テスト用のデフォルトキーを使用します。 +署名者のアドレスは公開鍵から導出されます。 +このアカウントが作成したモザイクを所有し、モザイクが属するネームスペースも所有していなければなりません。 + +### ネットワーク時刻を取得する {: #fetching-network-time } + +{{ tutorial.code_snippet_tagged('step-2') }} + +ネットワーク時刻は から取得し、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルで説明されている手順に従って、トランザクションの `timestamp` と `deadline` フィールドを導出します。 + +### モザイク名を設定する {: #choosing-the-mosaic-name } + +{{ tutorial.code_snippet_tagged('step-3') }} + +モザイク ID は、既存のネームスペースとモザイク名から組み立てられます。 +命名規則については、テキストブックの [名前](../../textbook/mosaics.md#name) を参照してください。 + +チュートリアルを複数回実行したときの衝突を避けるため、モザイク名にタイムスタンプを追加します。 +ただし、実際のプログラムでは、モザイクに固定名を使用します。 +`NAMESPACE` と `MOSAIC` 環境変数を使うと、チュートリアルで固定名を使用できます。 + +!!! warning "署名者が所有するネームスペースを使用してください" + + デフォルトでは、コードは `SIGNER_PRIVATE_KEY` が参照するテストアカウントと、`my_namespace` という名前のネームスペースを使用します。 + + [ルートネームスペースを登録する](../namespaces/register-root-namespace.md) チュートリアルから続けている場合は、`SIGNER_PRIVATE_KEY` と `NAMESPACE` 環境変数を、そこで作成したアカウントとネームスペース、または署名者が所有する別のネームスペースに合わせて設定してください。 + +### モザイクを定義する {: #defining-the-mosaic } + +{{ tutorial.code_snippet_tagged('step-4') }} + +モザイクの定義では、資産そのものを、それを登録するトランザクションとは区別して記述しています: + +* {{ tutorial.var('owner_public_key') }}: モザイクを作成するアカウントの [公開鍵](default:公開鍵)。{{ tutorial.var('signer_public_key') }} と一致しなければなりません。 + 2 つが異なるトランザクションはネットワークに拒否されます。 + +* {{ tutorial.var('id') }}: ネームスペースとモザイク名から作られるモザイク識別子。 + +* {{ tutorial.var('description') }}: モザイクを [説明する](../../textbook/mosaics.md#description) テキスト。 + +* {{ tutorial.var('properties') }}: モザイクの動作を設定するキーと値の組。 + + * {{ tutorial.var('divisibility') }}: モザイクがサポートする小数桁数(可分性)。 + 例えば `2` は、1 全体単位を 100(10^2^)原子単位に分割できることを意味します。 + テキストブックの [可分性](default:可分性) を参照してください。 + * {{ tutorial.var('initialSupply') }}: モザイク定義時に作成者へ発行される全体単位の数。 + テキストブックの [初期供給量](../../textbook/mosaics.md#initial-supply) を参照してください。 + * {{ tutorial.var('supplyMutable') }}: 作成後に総供給量を変更できるかどうか。 + テキストブックの [供給量の可変性](../../textbook/mosaics.md#supply-mutability) を参照してください。 + * {{ tutorial.var('transferable') }}: 作成者以外の任意の 2 アカウント間でモザイクを送信できるかどうか。 + テキストブックの [転送可能性](../../textbook/mosaics.md#transferability) を参照してください。 + + この例では、モザイクは小数点以下 2 桁まで分割可能で、`1000.00` 全体単位の供給量で始まります。 + 作成後に供給量を変更でき、アカウント間で自由に転送できます。 + +!!! note "オプションの徴収手数料" + + モザイク定義には、オプションの [徴収手数料](default:徴収手数料(levy)) も含められます。 + 詳細については、[徴収手数料付きモザイクを作成する](./mosaic-levy.md) チュートリアルを参照してください。 + +### モザイク定義トランザクションを構築する {: #building-the-mosaic-definition-transaction } + +{{ tutorial.code_snippet_tagged('step-5') }} + +モザイク定義トランザクションでは、次の項目を指定してネットワークにモザイクを登録します。 + +* {{ tutorial.var('type') }}: モザイク定義トランザクションでは、タイプ を使用します。 + +* {{ tutorial.var('signer_public_key') }}: トランザクションに署名して手数料を支払うアカウント。モザイクが属するネームスペースの所有者でなければなりません。 + 作成したモザイクの所有者になります。 + +* {{ tutorial.var('timestamp') }} と {{ tutorial.var('deadline') }}: ネットワーク時刻の手順で計算した値。 + +* {{ tutorial.var('rental_fee_sink') }}: モザイクの [作成手数料](../../textbook/mosaics.md#creation-fee) を集める特別なアカウント。 + 各ネットワークには固定されたシンクアドレスがあります。 + + * [メインネット](default:メインネット): `NBMOSAICOD4F54EE5CDMR23CCBGOAM2XSIUX6TRS` + * [テストネット](default:テストネット): `TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC` + + ネットワークは、作成手数料を他のアドレスへ送るトランザクションを拒否します。 + +* {{ tutorial.var('rental_fee') }}: 10 XEM の作成手数料。 + SDK の ヘルパーは、必要な金額を返します。 + + ネットワークは、この手数料を下回る金額を支払うトランザクションを拒否します。 + より大きい金額は受け付けられますが、全額がシンクアカウントへ送られます。 + +* {{ tutorial.var('mosaic_definition') }}: 前の手順で構築したモザイク定義。 + +{{ tutorial.code_snippet_tagged('step-6') }} + +最後に、 でトランザクション手数料を計算し、トランザクションに付加します。 +作成手数料とは異なり、トランザクション手数料は [ハーベスターアカウント](default:ハーベスターアカウント) に支払われます。 +モザイク定義トランザクションの固定手数料は 0.15 XEM で、[手数料表](../../textbook/transactions.md#fee-schedule) に示されています。 + +### モザイク定義を送信する {: #submitting-the-mosaic-definition } + +{{ tutorial.code_snippet_tagged('step-7') }} + +[XEM を送信する](../transactions/transfer-xem.md#announcing-the-transaction) チュートリアルと同じ手順で、モザイク定義トランザクションに署名してアナウンスします。 + +{{ tutorial.code_snippet_tagged('step-8') }} + +次にコードは、トランザクションがブロックに含まれるまで エンドポイントをポーリングし、承認を待ちます。 + +### モザイクを取得する {: #retrieving-the-mosaic } + +{{ tutorial.code_snippet_tagged('step-9') }} + +モザイクが正常に作成されたことを確認するため、コードは エンドポイントから定義を取得し、そのプロパティを表示します。 + +レスポンスが成功すると、そのモザイクがネットワーク上に存在し、期待したプロパティを持っていることを確認できます。 + +!!! note "モザイクの有効期間" + + モザイク自身に有効期間はなく、親ネームスペースの有効期限が切れると非アクティブになります。 + [ルートネームスペースを延長する](../namespaces/extend-root-namespace.md) と、モザイクが使用可能な状態を維持できます。 + 詳細については、テキストブックの [有効期間](../../textbook/mosaics.md#lifetime) を参照してください。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text linenums="1" hl_lines="5 6 7 67 68 69 70" +--8<-- 'devbook/mosaics/create_mosaic.log' +``` + +出力の要点は次のとおりです。 + +* **モザイク ID**(5 行目): モザイクは、ネームスペース `my_namespace` とタイムスタンプ付きモザイク名を組み合わせた完全修飾名で識別されます。 + [NEM テストネットエクスプローラー](https://testnet.nem.fyi/) でこの名前を検索すると、モザイクの詳細を確認できます。 + +* **作成手数料とトランザクション手数料**(6~7 行目): 作成手数料は 10 XEM、トランザクション手数料は 0.15 XEM です。 + +* **確認されたプロパティ**(67~70 行目): ネットワークからモザイクを取得し、可分性、初期供給量 `1000`、供給量が可変で転送可能であることを確認します。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [モザイクを定義する](#defining-the-mosaic) | | +| [作成手数料を計算する](#building-the-mosaic-definition-transaction) | | +| [モザイクを取得する](#retrieving-the-mosaic) | | + +## 次のステップ {: #next-steps } + +モザイクを作成できたので、次のことを行えます。 + +* [供給量が可変](../../textbook/mosaics.md#supply-mutability) として作成したモザイクを発行または焼却するために、[モザイクの供給量を変更する](./change-mosaic-supply.md) +* [転送トランザクションでモザイクを送信する](../transactions/transfer-mosaics.md) ことで、他のアカウントに配布する +* [モザイク情報を取得する](./get-mosaic-info.md) ことで、任意のモザイクのプロパティと供給量を確認する +* 配布前にプロパティを変更するために、[モザイク定義を変更する](./modify-mosaic-definition.md) diff --git a/mkdocs/pages/ja/devbook/mosaics/get-mosaic-info.md b/mkdocs/pages/ja/devbook/mosaics/get-mosaic-info.md new file mode 100644 index 000000000..4fec24ab6 --- /dev/null +++ b/mkdocs/pages/ja/devbook/mosaics/get-mosaic-info.md @@ -0,0 +1,106 @@ +--- +title: モザイク情報を取得する +tutorial_level: beginner +--- + +# モザイク情報を取得する + +NEM のすべての [モザイク](default:モザイク) には、供給量、可分性、転送ルールなど、オンチェーンのプロパティがあります。 + +このチュートリアルでは、モザイクのプロパティと現在の供給量を取得する方法を説明します。 + +## 前提条件 {: #prerequisites } + +このチュートリアルではネットワークからデータを読み取るだけです。[アカウント](default:アカウント) は必要ありません。 + +始める前に、[開発環境をセットアップ](../start/setup.md) してください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/mosaics/get_mosaic_info', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM API ノードを指定します。 +値が指定されていない場合は、デフォルトの [テストネット](default:テストネット) ノードを使用します。 + +`MOSAIC_ID` 環境変数では、[完全修飾名](../../textbook/mosaics.md#fully-qualified-name) で照会するモザイクを指定します。 +設定されていない場合は、[XEM](default:XEM) モザイク(`nem:xem`)をデフォルト値として使用します。 + +## コードの説明 {: #code-explanation } + +### モザイク情報を取得する {: #fetching-mosaic-information } + +{{ tutorial.code_snippet_tagged('step-1') }} + + エンドポイントは、次の内容を含むモザイク定義を取得します。 + +* **説明:** モザイクを [説明する](../../textbook/mosaics.md#description) テキスト。 +* **作成者:** モザイクを作成したアカウントの [公開鍵](default:公開鍵)。 +* **プロパティ:** モザイクの [動作プロパティ](../../textbook/mosaics.md#properties)。 + * **[可分性](default:可分性):** モザイクがサポートする小数桁数。 + 例えば、XEM の可分性は `6` なので、1 XEM は 1'000'000 原子単位に相当します。 + * **[初期供給量](../../textbook/mosaics.md#initial-supply):** 作成時の供給量で、全体単位で表されます。 + * **[供給量の可変性](../../textbook/mosaics.md#supply-mutability):** 作成後に作成者が供給量を変更できるかどうか。 + * **[転送可能性](../../textbook/mosaics.md#transferability):** モザイクをアカウント間で自由に送信できるか、作成者との間でのみ送信できるか。 +* **徴収手数料(levy):** モザイクを転送するたびに 3 つ目のアカウントへ支払われる、オプションの [徴収手数料](default:徴収手数料(levy))。 + +### 現在の供給量を取得する {: #fetching-the-current-supply } + +{{ tutorial.code_snippet_tagged('step-2') }} + +定義に記録されるのは **初期** 供給量だけです。 +供給量が可変なモザイクでは現在値が異なる可能性があるため、 エンドポイントは、現在流通している供給量を [全体単位](../../textbook/mosaics.md#divisibility) で返します。 + +### 原子単位に変換する {: #converting-to-atomic-units } + +{{ tutorial.code_snippet_tagged('step-3') }} + +エンドポイントは供給量を全体単位で返しますが、トランザクションの数量は [原子単位](../../textbook/mosaics.md#divisibility) で表されます。 +全体単位から原子単位に変換するには、コードで供給量に 10 のモザイクの可分性乗を掛けます。 + +XEM(可分性 `6`)では、`8'999'999'999` 全体単位の供給量は `8'999'999'999'000'000` 原子単位に相当します。 + +## 出力 {: #output } + +以下の出力は、テストネットの XEM モザイクを照会した場合の実行例です。 + +```text linenums="1" hl_lines="5 6 7 8 9 10 11 12 15 17" +--8<-- 'devbook/mosaics/get_mosaic_info.log' +``` + +出力の要点は次のとおりです。 + +* **モザイク ID**(5 行目): XEM モザイクの識別子である完全修飾名 `nem:xem`。 + +* **説明**(6 行目): 作成者が設定した、モザイクを説明するテキスト。 + +* **作成者**(7 行目): モザイクを作成したアカウントの公開鍵。 + +* **可分性**(8 行目): `6` は、1 XEM が 1'000'000(10^6^)原子単位であることを意味します。 + +* **初期供給量**(9 行目): 作成時の供給量で、全体単位で表されます。 + +* **供給量の可変性**(10 行目): `false` は、XEM の供給量が決して変更できないことを意味します。 + +* **転送可能性**(11 行目): `true` は、XEM をアカウント間で自由に送信できることを意味します。 + +* **徴収手数料(levy)**(12 行目): XEM の転送に追加のモザイク手数料はかかりません。 + +* **現在の供給量**(15 行目): 現在流通している供給量。XEM は可変ではないため初期供給量と同じです。 + +* **原子単位の供給量**(17 行目): モザイクの可分性を使って、全体単位から変換した供給量。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [モザイク定義を取得する](#fetching-mosaic-information) | | +| [現在の供給量を取得する](#fetching-the-current-supply) | | + +## 次のステップ {: #next-steps } + +* [モザイクを転送する](../transactions/transfer-mosaics.md) ことで、アカウント間でモザイクを送信する +* [アカウント残高を照会する](../accounts/query-balance.md) ことで、アカウントが保有するモザイクの量を確認する diff --git a/mkdocs/pages/ja/devbook/mosaics/modify-mosaic-definition.md b/mkdocs/pages/ja/devbook/mosaics/modify-mosaic-definition.md new file mode 100644 index 000000000..c2ce408f7 --- /dev/null +++ b/mkdocs/pages/ja/devbook/mosaics/modify-mosaic-definition.md @@ -0,0 +1,76 @@ +--- +title: モザイク定義を変更する +tutorial_level: intermediate +--- + +# モザイク定義を変更する + +[モザイク](default:モザイク) の作成後、作成者は同じモザイク識別子を使う別のモザイク定義トランザクションを送信することで、一部のプロパティを変更できます。 +新しいモザイクを作成するのではなく、ネットワークが既存のモザイクを更新します。 + +このチュートリアルでは、既存のモザイク定義を変更する方法を説明します。 +モザイクの供給量を変更する方法については、[モザイクの供給量を変更する](./change-mosaic-supply.md) を参照してください。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 変更に署名する [アカウント](default:アカウント) で [モザイク](default:モザイク) を作成する。 + モザイクを変更できるのは作成者だけです。 + [モザイクを作成する](./create-mosaic.md) チュートリアルを参照してください。 +* モザイクを保持する [ネームスペース](default:ネームスペース) をアクティブな状態に保つ。 + テキストブックの [有効期間](../../textbook/mosaics.md#lifetime) を参照してください。 +* トランザクションと作成手数料を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](../accounts/testnet-faucet.md) を参照してください。 + +## 変更できる内容 {: #what-can-be-changed } + +[説明](../../textbook/mosaics.md#description) はいつでも変更できます。 + +[転送可能性](../../textbook/mosaics.md#transferability) と [名前](../../textbook/mosaics.md#name) は変更できません。 + +[可分性](default:可分性)、[初期供給量](../../textbook/mosaics.md#initial-supply)、[供給量の可変性](../../textbook/mosaics.md#supply-mutability)、[徴収手数料](default:徴収手数料(levy)) を変更するには、作成者がモザイクの供給量全体を所有している必要があります。 +実際には、ほとんどのモザイク定義は、モザイクを配布する前にしか変更できません。 + +完全なルールについては、テキストブックの [モザイクを変更する](../../textbook/mosaics.md#modifying-a-mosaic) を参照してください。 + +## 手順 {: #procedure } + +モザイク定義を変更するには、[モザイクを作成する](./create-mosaic.md) チュートリアルの手順を再利用します。 + +1. [モザイクを取得する](./create-mosaic.md#retrieving-the-mosaic) で説明されているように、 から現在の定義を取得します。 + +2. [モザイク定義トランザクションを構築する](./create-mosaic.md#building-the-mosaic-definition-transaction) で説明されているように、同じモザイク識別子(ネームスペース名とモザイク名)を使って新しい を構築し、**モザイク作成者**のアカウントで署名します。 + + トランザクションには、[説明](../../textbook/mosaics.md#description)、[モザイクプロパティ](../../textbook/mosaics.md#properties)、[徴収手数料](default:徴収手数料(levy)) を含む完全なモザイク定義を含める必要があります。 + 前の手順で取得した定義を再送し、更新する値だけを変更してください。 + + !!! warning "完全な定義を再送してください" + + 説明はトランザクションに含まれる値で置き換えられるため、空のままにできません。 + + トランザクションから省略されたモザイクプロパティは、デフォルト値にリセットされます。 + + * `divisibility`: `0` + * `initialSupply`: `1000` + * `supplyMutable`: `false` + * `transferable`: `true` + + 省略された徴収手数料はモザイクから削除されます。 + +3. [モザイク定義を送信する](./create-mosaic.md#submitting-the-mosaic-definition) で説明されているように、トランザクションを送信します。 + +4. もう一度定義を取得して、モザイクが更新した値を保持していることを確認します。 + +トランザクションには、固定トランザクション手数料 0.15 XEM に加えて、作成トランザクションと同じ 10 XEM の全額の [作成手数料](../../textbook/mosaics.md#creation-fee) がかかります。固定手数料は [手数料表](../../textbook/transactions.md#fee-schedule) に示されています。 +例えば説明だけを変更する場合でも、新しいモザイクを作成する場合と同じ費用がかかります。 + +## 結果 {: #outcome } + +説明だけを変更する場合、ネットワークは既存の供給量と、モザイクを保有するすべてのアカウントの残高を維持します。 + +作成者が供給量全体を所有している間だけ許可される、その他のプロパティを変更した場合、ネットワークは新しい定義からモザイクを再構築します。 +再構築によって総供給量は `initialSupply` にリセットされ、その全量が作成者アカウントに割り当てられます。 + +その時点では作成者だけが保有者なので、他のアカウントの残高には影響しません。 +モザイクが配布された後は、ネットワークはこれらの他のプロパティを変更するトランザクションを拒否します。 diff --git a/mkdocs/pages/ja/devbook/mosaics/mosaic-levy.md b/mkdocs/pages/ja/devbook/mosaics/mosaic-levy.md new file mode 100644 index 000000000..cdb695547 --- /dev/null +++ b/mkdocs/pages/ja/devbook/mosaics/mosaic-levy.md @@ -0,0 +1,166 @@ +--- +title: 徴収手数料付きモザイクを作成する +tutorial_level: advanced +--- + +# 徴収手数料付きモザイクを作成する + +[モザイク](default:モザイク) にはオプションの [徴収手数料](default:徴収手数料(levy)) を含められます。 +徴収手数料は転送ごとに送信者へ課され、指定されたアカウントへ付与される手数料です。 + +徴収手数料の一般的な用途は、資産の背後にあるアカウントへ資金を提供することです。例えば、すべての転送にコミッションやロイヤリティを課します。 + +このチュートリアルでは、徴収手数料付きモザイクを作成する方法を説明します。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 +* モザイクを所有する [アカウント](default:アカウント) を、[コード](../accounts/create-from-private-key.md) または [ウォレット](../../userbook/wallet/create-account.md) を使って作成する。 +* モザイクが属する [ネームスペース](default:ネームスペース) を登録する。 + [ルートネームスペースを登録する](../namespaces/register-root-namespace.md) を参照してください。 +* トランザクションと作成手数料を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](../accounts/testnet-faucet.md) を参照してください。 + +さらに、モザイク定義の構築、アナウンス、承認の方法を理解するため、[モザイクを作成する](./create-mosaic.md) チュートリアルを確認してください。 + +## 完全なコード {: #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') }} + +スニペットは、署名者の秘密鍵を `SIGNER_PRIVATE_KEY` 環境変数から読み込みます。 +設定されていない場合は、テスト用のデフォルトキーを使用します。 +このアカウントがトランザクションに署名してモザイクの所有者になるため、モザイクを置くネームスペースも所有していなければなりません。 +モザイク識別子は、そのネームスペースとモザイク名から組み立てます。 +命名規則については、テキストブックの [名前](../../textbook/mosaics.md#name) を参照してください。 + +チュートリアルを複数回実行したときの衝突を避けるため、モザイク名にタイムスタンプを追加します。 +ただし、実際のプログラムでは、モザイクに固定名を使用します。 +`NAMESPACE` と `MOSAIC` 環境変数を使うと、チュートリアルで固定名を使用できます。 + +!!! warning "署名者が所有するネームスペースを使用してください" + + デフォルトでは、コードは `SIGNER_PRIVATE_KEY` が参照するテストアカウントと、`my_namespace` という名前のネームスペースを使用します。 + + [ルートネームスペースを登録する](../namespaces/register-root-namespace.md) チュートリアルから続けている場合は、`SIGNER_PRIVATE_KEY` と `NAMESPACE` 環境変数を、そこで作成したアカウントとネームスペース、または署名者が所有する別のネームスペースに合わせて設定してください。 + +{{ tutorial.code_snippet_tagged('step-2') }} + +ネットワーク時刻は から取得し、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルで説明されている手順に従って、トランザクションの `timestamp` と `deadline` フィールドを導出します。 + +### 徴収手数料を記述する {: #describing-the-levy } + +{{ tutorial.code_snippet_tagged('step-3') }} + +徴収手数料は、次の 4 つのフィールドを持つ 構造体です。 + +* {{ tutorial.var('transfer_fee_type') }}: 徴収手数料の金額を計算する方法。 + + * `absolute`: 転送量に関係なく、すべての転送に課す固定量。 + * `percentile`: 転送量に比例する量。 + + このチュートリアルでは `absolute` の徴収手数料を使うため、すべての転送に同じ金額が課されます。 + +* {{ tutorial.var('recipient_address') }}: 転送ごとに徴収手数料を受け取るアカウント。 + モザイクの作成者でも、別のアカウントでも構いません。 + +* {{ tutorial.var('mosaic_id') }}: 徴収手数料を支払うモザイク。 + このチュートリアルでは `nem:xem` で徴収手数料を課すため、送信者はネットワーク通貨で支払います。 + + 定義するモザイク自身で徴収手数料を支払うこともできます。 + その他の徴収手数料用モザイクはネットワーク上にすでに存在し、[転送可能](../../textbook/mosaics.md#transferability) でなければなりません。 + +* {{ tutorial.var('fee') }}: 徴収手数料の金額。 + `absolute` の徴収手数料では、徴収手数料用モザイクの [原子単位](../../textbook/mosaics.md#divisibility) で表します。 + `nem:xem` の [可分性](default:可分性) は 6 なので、1'000'000 という値は転送ごとに 1 XEM を課します。 + + `percentile` の徴収手数料では、手数料は代わりにベーシスポイントで解釈されます。`fee` が `100` なら、転送量の 1% が課されます。 + 完全なルールについては、テキストブックの [パーセンタイル徴収手数料の計算](../../textbook/mosaics.md#percentile-levy-calculation) を参照してください。 + +### モザイク定義に徴収手数料を付加する {: #attaching-the-levy-to-the-mosaic-definition } + +{{ tutorial.code_snippet_tagged('step-4') }} + +徴収手数料はモザイク定義の一部なので、[モザイクを作成する](./create-mosaic.md#building-the-mosaic-definition-transaction) で使う と同じものを使って設定します。 +このチュートリアルでは同じトランザクションを再利用し、{{ tutorial.var('mosaic_definition') }} フィールドに徴収手数料を追加します。 + +[作成手数料](../../textbook/mosaics.md#creation-fee) は 10 XEM、トランザクション手数料は固定の 0.15 XEM で、[手数料表](../../textbook/transactions.md#fee-schedule) に示されています。 + +### モザイク定義を送信する {: #submitting-the-mosaic-definition } + +{{ tutorial.code_snippet_tagged('step-5') }} + +[XEM を送信する](../transactions/transfer-xem.md#announcing-the-transaction) チュートリアルと同じ手順で、トランザクションに署名し、アナウンスして、承認を待ちます。 + +### 徴収手数料を検証する {: #verifying-the-levy } + +{{ tutorial.code_snippet_tagged('step-6') }} + +徴収手数料付きモザイクが作成されたことを確認するため、コードは エンドポイントからモザイク定義を取得します。レスポンスにはモザイクプロパティとともに徴収手数料が返されます。 + +レスポンスに徴収手数料が含まれていれば、今後のモザイク転送に徴収手数料が課されることを確認できます。 + +## 徴収手数料が課される仕組み {: #how-the-levy-is-charged } + +モザイクが作成された後、徴収手数料はモザイクのすべての [転送](../transactions/transfer-mosaics.md) に適用され、転送トランザクションに追加のフィールドは必要ありません。 + +!!! warning "徴収手数料は保証された徴収ではありません" + + 徴収手数料は再帰的ではないため、回避できます。 + テキストブックの [例](../../textbook/mosaics.md#limitations) を参照してください。 + +徴収手数料は転送量に上乗せして課されます。そのため、このチュートリアルで作成したモザイクを 50 単位転送する送信者からは、次の金額が引き落とされます。 + +* モザイク 50 単位。転送の受取人に付与されます。 +* 1 XEM。徴収手数料の受取人に付与されます。 +* トランザクション手数料。[ハーベスターアカウント](default:ハーベスターアカウント) に付与されます。 + +送信者が転送量と徴収手数料の両方を支払えない場合、ネットワークは転送を拒否します。 +この徴収手数料は `nem:xem` で支払うため、送信者は徴収手数料とトランザクション手数料の両方をカバーする十分な XEM も保有しなければなりません。 +徴収手数料を別のモザイクで支払う場合、送信者はそのモザイクも十分な残高を保有しなければなりません。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text linenums="1" hl_lines="3 6-10 58-68 82-85" +--8<-- 'devbook/mosaics/mosaic_levy.log' +``` + +出力の要点は次のとおりです。 + +* **モザイク ID**(3 行目): モザイクは、ネームスペース `my_namespace` とタイムスタンプ付きモザイク名を組み合わせた完全修飾名で識別されます。 + [NEM テストネットエクスプローラー](https://testnet.nem.fyi/) でこの名前を検索すると、モザイクの詳細を確認できます。 + +* **徴収手数料のフィールド**(6~10 行目): 作成する徴収手数料。`nem:xem` の原子単位 1'000'000(1 XEM)の `absolute` 手数料で、転送ごとに徴収手数料の受取人へ支払われます。 + +* **トランザクション内の徴収手数料**(58~68 行目): 徴収手数料はモザイク定義の内部で定義されます。受取人アドレス、徴収手数料のモザイク名、モザイク名はペイロード内で 16 進数にエンコードされ、この `absolute` 徴収手数料は原子単位で表されます。 + +* **検証された徴収手数料**(82~85 行目): ネットワークからモザイクを取得し、徴収手数料のタイプ、受取人、支払いに使うモザイク、その金額を確認します。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [徴収手数料を記述する](#describing-the-levy) | | +| [モザイクに徴収手数料を付加する](#attaching-the-levy-to-the-mosaic-definition) | | +| [徴収手数料を検証する](#verifying-the-levy) | | + +## 次のステップ {: #next-steps } + +徴収手数料付きモザイクを作成できたので、次のことを行えます。 + +* [転送トランザクションでモザイクを送信する](../transactions/transfer-mosaics.md) ことで、送信者に徴収手数料が課されることを確認する +* [モザイク情報を取得する](./get-mosaic-info.md) ことで、任意のモザイクの徴収手数料を確認する diff --git a/mkdocs/pages/ja/devbook/namespaces/extend-root-namespace.md b/mkdocs/pages/ja/devbook/namespaces/extend-root-namespace.md new file mode 100644 index 000000000..0d8f394f5 --- /dev/null +++ b/mkdocs/pages/ja/devbook/namespaces/extend-root-namespace.md @@ -0,0 +1,51 @@ +--- +title: ルートネームスペースを延長する +tutorial_level: beginner +--- + +# ルートネームスペースを延長する + +[ルートネームスペース](default:ルートネームスペース) は、登録 1 回あたりおよそ 1 年という限られた [期間](../../textbook/namespaces.md#duration) のリースを受けます。 +初回のリース期間を超えてネームスペースを維持したい場合は、延長する必要があります。 + +このチュートリアルでは、ルートネームスペースを延長する方法を説明します。 + +## 前提条件 {: #prerequisites } + +* アクティブなルートネームスペースを所有する [アカウント](default:アカウント)。 + [ルートネームスペースを登録する](./register-root-namespace.md) を参照してください。 +* トランザクションとレンタル手数料を支払うための [XEM](default:XEM)。 + +## 延長するタイミング {: #when-to-extend } + +ネームスペースは 2 つの状況で延長できます。 + +* **リース終了間近:** 更新が許可されるのは、有効期限前の最後の 43200 ブロック(およそ 30 日間)だけです。 + それより前に行った更新の試みはネットワークに拒否されます。 + +* **猶予期間中:** ネームスペースの期限は切れているものの、期限切れ後 43200 ブロック(およそ 30 日間)続く [猶予期間](../../textbook/namespaces.md#duration) 内にあります。 + この期間に延長すると、ネームスペースはすぐにアクティブ状態へ戻ります。 + +!!! note "サブネームスペースとモザイクの延長" + + 延長が必要なのはルートネームスペースだけです。 + [サブネームスペース](default:サブネームスペース) と関連するすべての [モザイク](default:モザイク) は、ルートネームスペースの有効期間を継承します。 + + そのため、ルートネームスペースを延長すると、そのすべてのサブネームスペースとモザイクを自動的に使用可能な状態に保てます。 + +## 手順 {: #procedure } + +ネームスペースを延長するには、**同じルートネームスペース名** を使って [登録手順](./register-root-namespace.md) を繰り返し、**100 XEM のレンタル手数料**をもう一度支払います。 + +トランザクションに署名するアカウントは、ネームスペースの所有者でなければなりません。 + +プロトコルは、[リース終了間近または猶予期間中](#when-to-extend) の更新だけを受け付けます。 + +## 期間と制限 {: #duration-and-limits } + +各更新では、更新トランザクションを含むブロックの 1 年後までリースが延長されます。前の有効期限から 1 年後ではありません。 +そのため、ネームスペースを複数年分前払いすることはできません。 + +ネームスペースを無期限に維持するには、毎年、更新期間中に更新してください。 + +詳細については、テキストブックの [期間](../../textbook/namespaces.md#duration) を参照してください。 diff --git a/mkdocs/pages/ja/devbook/namespaces/get-namespace-info.md b/mkdocs/pages/ja/devbook/namespaces/get-namespace-info.md new file mode 100644 index 000000000..18ef52655 --- /dev/null +++ b/mkdocs/pages/ja/devbook/namespaces/get-namespace-info.md @@ -0,0 +1,110 @@ +--- +title: ネームスペース情報を取得する +tutorial_level: beginner +--- + +# ネームスペース情報を取得する + +このチュートリアルでは、[ネームスペース](default:ネームスペース) のプロパティ、その [サブネームスペース](default:サブネームスペース)、およびその下に定義された [モザイク](default:モザイク) を取得する方法を説明します。 + +## 前提条件 {: #prerequisites } + +このチュートリアルではネットワークからデータを読み取るだけです。アカウントは必要ありません。 + +始める前に、[開発環境をセットアップ](../start/setup.md) してください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/namespaces/get_namespace_info', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM API ノードを指定します。 +値が指定されていない場合は、デフォルトの [テストネット](default:テストネット) ノードを使用します。 + +`NAMESPACE_NAME` 環境変数では、`foo` や `foo.bar` のような、ドットで区切られた完全な [名前](../../textbook/namespaces.md#name) で照会するネームスペースを指定します。 +設定されていない場合は、テストネットに登録された [ルートネームスペース](default:ルートネームスペース) である `company` を使用します。 + +## コードの説明 {: #code-explanation } + +### ネームスペース情報を取得する {: #fetching-namespace-information } + +{{ tutorial.code_snippet_tagged('step-1') }} + + エンドポイントは、次の内容を含むネームスペースの現在のプロパティを取得します。 + +* **名前:** ネームスペースの完全なドット区切りの [識別子](../../textbook/namespaces.md#name)。ルートから照会したレベルまでを表します。 + 例えば、`foo` は [ルートネームスペース](default:ルートネームスペース) で、`foo.bar` は `foo` の [サブネームスペース](default:サブネームスペース) です。 + + 返されるデータ構造の `fqn` は _Fully-Qualified Name_(完全修飾名)を表します。 + +* **所有者:** ネームスペースを [登録した](../../textbook/namespaces.md#ownership) アカウントの [アドレス](default:アドレス)。 + +* **高さ:** 現在の所有権が開始した [ブロック](default:ブロック) の高さ。 + +### リースの有効期限を計算する {: #computing-the-lease-expiration } + +{{ tutorial.code_snippet_tagged('step-2') }} + +ネームスペースが永久に所有されることはありません。 +ルートネームスペースは 525600 ブロック(およそ 1 年)[リース](../../textbook/namespaces.md#duration) を受け、期限切れになる前に更新する必要があります。 +サブネームスペースは個別にリースされず、ルートネームスペースと同時に期限切れになります。 + +有効期限の高さは API レスポンスに含まれませんが、ネームスペースの高さにリース期間を加えて導出できます。 +これを が返す現在のチェーン高と比較すると、ネームスペースの期限切れまでに残っているブロック数がわかります。 + +### サブネームスペースを一覧表示する {: #listing-subnamespaces } + +{{ tutorial.code_snippet_tagged('step-3') }} + +ネームスペースの子を直接返すエンドポイントはありません。 +ただし、[サブネームスペースは常にルートネームスペースの所有者を共有する](../../textbook/namespaces.md#ownership) ため、そのアカウントが所有するネームスペースを照会すれば見つけられます。 + + エンドポイントは、アカウントが所有するネームスペースを返します。 +オプションの `parent` パラメーターを使うと、指定したネームスペースのサブネームスペースだけに結果を制限できます。 +前の手順で取得したネームスペース所有者と照会したネームスペースを `parent` の値として使うと、そのサブネームスペースが返されます。 + +### ネームスペースのモザイクを一覧表示する {: #listing-the-namespaces-mosaics } + +{{ tutorial.code_snippet_tagged('step-4') }} + +モザイクは常に [ネームスペースの下に定義](../../textbook/mosaics.md#fully-qualified-name) され、関連するモザイクをまとめるプレフィックスとして機能します。 + + エンドポイントは、照会した名前とネームスペースが完全に一致するモザイクごとに 1 つの定義を返します。 +より深いサブネームスペース(`foo` を照会したときの `foo.bar:baz` など)で定義されたモザイクは含まれません。 +それらも一覧に含めるには、前の手順で見つかった各サブネームスペースについて、この照会を繰り返してください。 + +## 出力 {: #output } + +以下の出力は、テストネットの `company` ネームスペースを照会した場合の実行例です。 + +```text linenums="1" hl_lines="5 6 7 9 10 11 14 15 18 19" +--8<-- 'devbook/namespaces/get_namespace_info.log' +``` + +出力の要点は次のとおりです。 + +* **ネームスペース名**(5 行目): 照会したネームスペース `company`。 + ドットを含まないため、ルートネームスペースです。 + +* **所有者**(6 行目): 現在ネームスペースを所有するアカウント。 + +* **高さ**(7 行目): 現在の所有権期間が開始したブロックの高さ。 + +* **リースの有効期限**(9~11 行目): 有効期限の高さは、所有権の高さにリース期間 525600 ブロックを加えた値です。 + 現在のチェーン高を引くと、有効期限までに残っているブロック数がわかります。 + +* **サブネームスペース**(14~15 行目): `company` の下に `company.division` というサブネームスペースが 1 つ存在します。 + +* **モザイク**(18~19 行目): ネームスペースの直下に `company:token` というモザイクが 1 つ定義されています。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [ネームスペースのプロパティを取得する](#fetching-namespace-information) | | +| [リースの有効期限を計算する](#computing-the-lease-expiration) | | +| [サブネームスペースを一覧表示する](#listing-subnamespaces) | | +| [ネームスペースのモザイクを一覧表示する](#listing-the-namespaces-mosaics) | | diff --git a/mkdocs/pages/ja/devbook/namespaces/register-root-namespace.md b/mkdocs/pages/ja/devbook/namespaces/register-root-namespace.md new file mode 100644 index 000000000..8ef672767 --- /dev/null +++ b/mkdocs/pages/ja/devbook/namespaces/register-root-namespace.md @@ -0,0 +1,149 @@ +--- +title: ルートネームスペースを登録する +tutorial_level: intermediate +--- + +# ルートネームスペースを登録する + +[ネームスペース](default:ネームスペース) は、ネイティブの `nem:xem` モザイクにおける `nem` プレフィックスのように、関連する [モザイク](default:モザイク) を意味のある名前でまとめるラベルを提供します。 + +ネームスペースは他のネームスペースの下にネストできます。このチュートリアルでは、1 年間の [ルートネームスペース](default:ルートネームスペース) を登録する方法を説明します。 + +ルートネームスペースではなく [サブネームスペース](default:サブネームスペース) を登録する方法については、[サブネームスペースを登録する](./register-subnamespace.md) ガイドを参照してください。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 +* ネームスペースを登録する [アカウント](default:アカウント) を、[コード](../accounts/create-from-private-key.md) または [ウォレット](../../userbook/wallet/create-account.md) を使って作成する。 +* トランザクションとレンタル手数料を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](../accounts/testnet-faucet.md) を参照してください。 + +さらに、トランザクションのアナウンスと承認の方法を理解するため、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルを確認してください。 + +## 完全なコード {: #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') }} + +スニペットは、署名者の秘密鍵を `SIGNER_PRIVATE_KEY` 環境変数から読み込みます。 +設定されていない場合は、テスト用のデフォルトキーを使用します。 +署名者のアドレスは公開鍵から導出されます。 +このアカウントが登録したネームスペースを所有します。 + +### ネットワーク時刻を取得する {: #fetching-network-time } + +{{ tutorial.code_snippet_tagged('step-2') }} + +ネットワーク時刻は から取得し、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルで説明されている手順に従って、トランザクションの `timestamp` と `deadline` フィールドを導出します。 + +### ネームスペース名を設定する {: #choosing-the-namespace-name } + +{{ tutorial.code_snippet_tagged('step-3') }} + +ネームスペースは名前で識別され、その名前をトランザクションによってネットワーク上で 1 年間予約します。 +命名規則については、テキストブックの [名前](../../textbook/namespaces.md#name) を参照してください。 + +チュートリアルを複数回実行したときの衝突を避けるため、名前にタイムスタンプを追加します。 +ただし、実際のプログラムでは、ネームスペースに固定名を使用します。 +`ROOT_NAMESPACE` 環境変数を使うと、チュートリアルで固定名を使用できます。 + +### トランザクションを構築する {: #building-the-transaction } + +{{ tutorial.code_snippet_tagged('step-4') }} + +ネームスペース登録トランザクションでは、次の項目を指定してネットワークにネームスペースを登録します。 + +* {{ tutorial.var('type') }}: ネームスペース登録トランザクションでは、タイプ を使用します。 + +* {{ tutorial.var('signer_public_key') }}: トランザクションに署名して手数料を支払うアカウント。 + 登録したネームスペースの所有者になります。 + +* {{ tutorial.var('timestamp') }} と {{ tutorial.var('deadline') }}: ネットワーク時刻の手順で計算した値。 + +* {{ tutorial.var('rental_fee_sink') }}: ネームスペースの [レンタル手数料](../../textbook/namespaces.md#lease-fee) を集める特別なアカウント。 + 各ネットワークには固定されたシンクアドレスがあります。 + + * [メインネット](default:メインネット): `NAMESPACEWH4MKFMBCVFERDPOOP4FK7MTBXDPZZA` + * [テストネット](default:テストネット): `TAMESPACEWH4MKFMBCVFERDPOOP4FK7MTDJEYP35` + + ネットワークは、レンタル手数料を他のアドレスへ送るトランザクションを拒否します。 + +* {{ tutorial.var('rental_fee') }}: ルートネームスペースのレンタル手数料である 100 XEM。 + SDK の ヘルパーは、必要な金額を返します。 + {{ tutorial.lit('True') }} 引数は、ルートネームスペースの手数料を要求します。 + + ネットワークは、この手数料を下回る金額を支払うトランザクションを拒否します。 + より大きい金額は受け付けられますが、全額がシンクアカウントへ送られます。 + +* {{ tutorial.var('name') }}: ルートネームスペースの名前。 + +{{ tutorial.code_snippet_tagged('step-5') }} + +最後に、 でトランザクション手数料を計算し、トランザクションに付加します。 +レンタル手数料とは異なり、トランザクション手数料は [ハーベスターアカウント](default:ハーベスターアカウント) に支払われます。 +ネームスペース登録トランザクションの固定手数料は 0.15 XEM で、[手数料表](../../textbook/transactions.md#fee-schedule) に示されています。 + +### トランザクションを送信する {: #submitting-the-transaction } + +{{ tutorial.code_snippet_tagged('step-6') }} + +[XEM を送信する](../transactions/transfer-xem.md#announcing-the-transaction) チュートリアルと同じ手順で、トランザクションに署名してアナウンスします。 + +{{ tutorial.code_snippet_tagged('step-7') }} + +次にコードは、トランザクションがブロックに含まれるまで エンドポイントをポーリングし、承認を待ちます。 + +### ネームスペースを取得する {: #retrieving-the-namespace } + +{{ tutorial.code_snippet_tagged('step-8') }} + +ネームスペースが登録されたことを確認するため、コードは エンドポイントを使ってネットワークから取得し、そのプロパティを表示します。 + +成功したレスポンスは、ネームスペースが登録されてアクティブになったことを確認します。 + +レスポンスには登録時の高さも表示されます。これはネームスペースが登録されたブロックで、1 年間のリースの開始を示します。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text linenums="1" hl_lines="5 6 7 31-33" +--8<-- 'devbook/namespaces/register_root_namespace.log' +``` + +出力の要点は次のとおりです。 + +* **ネームスペース名**(5 行目): 選択した名前 `ns_1783091378` には、一意性を確保するためのタイムスタンプが含まれています。 + この名前を [NEM テストネットエクスプローラー](https://testnet.nem.fyi/) で検索すると、ネームスペースの詳細を確認できます。 + +* **レンタル手数料とトランザクション手数料**(6~7 行目): ルートネームスペースなのでレンタル手数料は 100 XEM です([サブネームスペース](default:サブネームスペース) は代わりに 10 XEM を支払います)。トランザクション手数料は 0.15 XEM です。 + +* **ネームスペース情報**(31~33 行目): 登録されたネームスペース、その所有者(署名者のアドレス)、登録時の高さ。リースが開始したブロックの高さです。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [ネームスペース登録トランザクションを構築する](#building-the-transaction) | | +| [レンタル手数料を計算する](#building-the-transaction) | | +| [ネームスペースを取得する](#retrieving-the-namespace) | | + +## 次のステップ {: #next-steps } + +ルートネームスペースを取得できたので、次のことを行えます。 + +* [モザイクを定義する](../mosaics/create-mosaic.md) ことで、ネームスペースの下にカスタム資産を作成する +* [サブネームスペースを登録する](./register-subnamespace.md) ことで、階層構造を作成する +* 有効期限前に [ネームスペースを延長する](./extend-root-namespace.md) ことで、アクティブな状態を維持する diff --git a/mkdocs/pages/ja/devbook/namespaces/register-subnamespace.md b/mkdocs/pages/ja/devbook/namespaces/register-subnamespace.md new file mode 100644 index 000000000..91032d6ef --- /dev/null +++ b/mkdocs/pages/ja/devbook/namespaces/register-subnamespace.md @@ -0,0 +1,131 @@ +--- +title: サブネームスペースを登録する +tutorial_level: intermediate +--- + +# サブネームスペースを登録する + +[サブネームスペース](default:サブネームスペース)(「子」ネームスペースとも呼ばれます)は、[ネームスペース](default:ネームスペース) の階層構造を拡張します。 + +このチュートリアルでは、既存の [ルートネームスペース](default:ルートネームスペース) の下にサブネームスペースを登録する方法を説明します。 + +ルートネームスペースを登録する方法については、[ルートネームスペースを登録する](./register-root-namespace.md) ガイドを参照してください。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 +* 既存のルートネームスペースを持つ [アカウント](default:アカウント) を用意する。 + [ルートネームスペースを登録する](./register-root-namespace.md) を参照してください。 + + !!! note + このチュートリアルの例では、`ns_root` という名前のルートネームスペースを使用します。 + コードを自分のルートネームスペース名に更新してください。 + +* トランザクションとレンタル手数料を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](../accounts/testnet-faucet.md) を参照してください。 + +さらに、トランザクションのアナウンスと承認の方法を理解するため、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルを確認してください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/namespaces/register_subnamespace', ['py', 'js']) }} + +## コードの説明 {: #code-explanation } + +コードは [ルートネームスペースを登録する](./register-root-namespace.md) チュートリアルと同じパターンに従います。 +このセクションでは、主な違いだけに焦点を当てます。 + +共通手順(アカウントのセットアップ、ネットワーク時刻の取得、アナウンス)とルートネームスペースで共有されるトランザクションディスクリプタフィールドの詳細な説明については、[ルートネームスペースを登録する](./register-root-namespace.md) を参照してください。 + +### サブネームスペース名を設定する {: #choosing-the-subnamespace-name } + +{{ tutorial.code_snippet_tagged('step-1') }} + +サブネームスペースは、親ネームスペース名と子名をドットでつないだ完全な名前で識別します(`ns_root.product` など)。 +命名規則については、テキストブックの [名前](../../textbook/namespaces.md#name) を参照してください。 + +チュートリアルを複数回実行したときの衝突を避けるため、子名にタイムスタンプを追加します。 +ただし、実際のプログラムでは、サブネームスペースに固定名を使用します。 +`ROOT_NAMESPACE` と `SUBNAMESPACE` 環境変数を使うと、チュートリアルで固定名を使用できます。 + +!!! warning "署名者が所有する親ネームスペースを使用してください" + + デフォルトでは、コードは `SIGNER_PRIVATE_KEY` が参照するテストアカウントと、`ns_root` という親ネームスペースを使用します。 + + [ルートネームスペースを登録する](./register-root-namespace.md) チュートリアルから続けている場合は、`SIGNER_PRIVATE_KEY` と `ROOT_NAMESPACE` 環境変数を、そこで作成したアカウントとネームスペース、または署名者が所有する別のネームスペースに合わせて設定してください。 + +### トランザクションを構築する {: #building-the-transaction } + +{{ tutorial.code_snippet_tagged('step-2') }} + +サブネームスペースを登録する場合の主な違いは、トランザクションディスクリプタにあります。 + +* {{ tutorial.var('parent_name') }}: 前の手順で定義した親ネームスペースの名前。 + ルートネームスペースでも、別のサブネームスペースでも構いません。 + +* {{ tutorial.var('name') }}: 前の手順で設定したサブネームスペースの名前。 + + これはサブネームスペースの名前だけで、完全なパスではないことに注意してください。 + 例えば、ルートが `company` の `company.product` を作成する場合は、{{ tutorial.var("`name: 'product'`") }} と {{ tutorial.var("`parent_name: 'company'`") }} を設定します。 + +* {{ tutorial.var('rental_fee') }}: サブネームスペースのレンタル手数料である 10 XEM。ルートネームスペースと同じ [シンクアカウント](./register-root-namespace.md#building-the-transaction) に支払います。 + + SDK の ヘルパーは、必要な金額を返します。 + {{ tutorial.lit('False') }} 引数は、サブネームスペースの手数料を要求します。 + +その後、[ルートネームスペースを登録する](./register-root-namespace.md#submitting-the-transaction) チュートリアルと同じ手順で、トランザクションに署名し、アナウンスして、承認を待ちます。 + +### サブネームスペースを取得する {: #retrieving-the-subnamespace } + +{{ tutorial.code_snippet_tagged('step-3') }} + +サブネームスペースが登録されたことを確認するため、コードは エンドポイントを使ってネットワークから取得し、そのプロパティを表示します。 + +サブネームスペースは、親名と子名をドットでつないだ完全な名前で照会します(例: `ns_root.sub_1783411728`)。 + +成功したレスポンスは、サブネームスペースが登録されてアクティブになったことを確認します。 + +レスポンスには登録時の高さも表示されます。サブネームスペースはルートネームスペースの [リース](../../textbook/namespaces.md#duration) を継承するため、これはルートネームスペースが登録されたブロックの高さです。 + +!!! note "サブネームスペースの期間" + + サブネームスペースはルートネームスペースの期限切れと同時に期限切れになり、単独で更新することはできません。 + [ルートネームスペースを更新する](./extend-root-namespace.md) と、サブネームスペースも更新されます。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text linenums="1" hl_lines="5 6 7 32-34" +--8<-- 'devbook/namespaces/register_subnamespace.log' +``` + +出力の要点は次のとおりです。 + +* **完全なネームスペースパス**(5 行目): `ns_root.sub_1783411728` は、親ネームスペース `ns_root` とトランザクションで設定したサブネームスペース名を組み合わせたものです。 + +* **レンタル手数料とトランザクション手数料**(6~7 行目): サブネームスペースなのでレンタル手数料は 10 XEM です(ルートネームスペースは代わりに 100 XEM を支払います)。トランザクション手数料は 0.15 XEM です。 + +* **ネームスペース情報**(32~34 行目): 登録されたサブネームスペース、その所有者(署名者のアドレス)、登録時の高さ。ルートネームスペースのリースが開始したブロックの高さをサブネームスペースが継承しています。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [サブネームスペース登録トランザクションを構築する](#building-the-transaction) | | +| [レンタル手数料を計算する](#building-the-transaction) | | +| [サブネームスペースを取得する](#retrieving-the-subnamespace) | | + +## 次のステップ {: #next-steps } + +サブネームスペースを取得できたので、次のことを行えます。 + +* 追加のサブネームスペースを登録して、階層構造を拡張する +* [モザイクを定義する](../mosaics/create-mosaic.md) ことで、サブネームスペースの下にカスタム資産を作成する diff --git a/mkdocs/pages/ja/devbook/network-currency/query-block-rewards.md b/mkdocs/pages/ja/devbook/network-currency/query-block-rewards.md new file mode 100644 index 000000000..ff0237474 --- /dev/null +++ b/mkdocs/pages/ja/devbook/network-currency/query-block-rewards.md @@ -0,0 +1,104 @@ +--- +title: ブロック報酬を照会する +tutorial_level: beginner +--- + +# ブロック報酬を照会する + +NEM の各 [ブロック](default:ブロック) は、1 つの [ハーベスターアカウント](default:ハーベスターアカウント) によって生成されます。 +ブロックのハーベスティングに対する報酬はすべて、そのブロックに含まれる [トランザクション](default:トランザクション) の手数料から生じ、これらの手数料はブロックを生成したハーベスターに全額支払われます。 + +このチュートリアルでは、任意のブロックを照会し、そのハーベスターを特定して、報酬を構成するトランザクション手数料を合計する方法を説明します。 + +## 前提条件 {: #prerequisites } + +始める前に、[開発環境をセットアップ](../start/setup.md) してください。 + +このチュートリアルではネットワークからデータを読み取るだけです。[アカウント](default:アカウント) や [XEM](default:XEM) の残高は必要ありません。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/network-currency/query_block_rewards', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM API ノードを指定します。 +値が指定されていない場合は、デフォルトの [テストネット](default:テストネット) ノードを使用します。 + +`BLOCK_HEIGHT` 環境変数で照会するブロックを選択します。 +設定されていない場合は、テストネットでハーベスティングされたブロック `661258` をデフォルト値として使用します。 + +## コードの説明 {: #code-explanation } + +コードはブロック高を指定してブロックを取得し、ブロックの署名者公開鍵からハーベスターのアドレスを導出します。 +次に、ブロック内のすべてのトランザクションの手数料を合計して、合計報酬を求めます。 + +### ブロック情報を取得する {: #fetching-block-information } + +{{ tutorial.code_snippet_tagged('step-1') }} + + エンドポイントは、指定した高さのブロックに関する情報を返します。 +これには、ブロックに含まれるトランザクションの一覧も含まれます。 + +### ハーベスターを特定する {: #identifying-the-harvester } + +{{ tutorial.code_snippet_tagged('step-2') }} + +`signer` フィールドには、ブロックをハーベスティングしたアカウントの [公開鍵](default:公開鍵) が格納されています。 + メソッドは、この公開鍵を対応するテストネットの [アドレス](default:アドレス) に変換します。 + +!!! info "署名者が常に報酬を得るアカウントとは限りません" + + [ローカルハーベスティング](default:ローカルハーベスティング) では、`signer` が [ハーベスターアカウント](default:ハーベスターアカウント) であり、報酬を受け取ります。 + + [リモートハーベスティング](default:リモートハーベスティング) または [委任ハーベスティング](default:委任ハーベスティング) では、`signer` は [リモートアカウント](default:リモートキー) ですが、報酬は [メインアカウント](default:メインキー) に支払われます。 + +### トランザクション手数料を合計する {: #summing-the-transaction-fees } + +{{ tutorial.code_snippet_tagged('step-3') }} + +ブロック内の各トランザクションには、原子単位で表された `fee` フィールドがあります。 +XEM の [可分性](default:可分性) は 6 なので、`350000` 原子単位は `0.350000` XEM を表します。 + +すべてのトランザクションの手数料を加算すると、ブロックの合計報酬になります。 + +### 合計報酬を計算する {: #calculating-the-total-reward } + +{{ tutorial.code_snippet_tagged('step-4') }} + +ブロックの合計報酬は、すべてのトランザクション手数料の合計であり、ハーベスターに全額支払われます。 +空のブロックには手数料がないため、報酬もありません。 + +!!! note "別の方法: アカウントごとに報酬を照会する" + + このチュートリアルでは、特定のブロックに含まれるトランザクション手数料を合計して、そのブロックの報酬を計算します。 + + 特定のアカウントが得た報酬に関心がある場合は、 エンドポイントを使用してください。 + ハーベスティングされたブロックごとに 1 件のエントリを返し、そのブロックで得た報酬を示す `totalFee` フィールドも含みます。 + + このエンドポイントは、報酬を得るアカウントである [ハーベスターアカウント](default:ハーベスターアカウント) のアドレスを受け取ります。 + [リモートハーベスティング](default:リモートハーベスティング) または [委任ハーベスティング](default:委任ハーベスティング) の場合、署名したリモートアカウントではなくメインアカウントです。 + + リモートアカウントのアドレスを指定しても、メインアカウントに代わって署名したブロックが返されます。 + +## 出力 {: #output } + +以下の出力は、ブロック 661,258 の報酬を照会した場合の実行例です。 + +```text linenums="1" hl_lines="4 7-8 10" +--8<-- 'devbook/network-currency/query_block_rewards.log' +``` + +出力の要点は次のとおりです。 + +* **ハーベスター**(4 行目): ブロックの `signer` 公開鍵から導出されたアドレス。 +* **トランザクション手数料**(7~8 行目): ブロックに含まれる各トランザクションが支払った手数料。 +* **ブロックの合計報酬**(10 行目): すべてのトランザクション手数料の合計で、ハーベスターに全額支払われる金額。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [ブロック情報を取得する](#fetching-block-information) | | diff --git a/mkdocs/pages/ja/devbook/network-currency/query-currency-supply.md b/mkdocs/pages/ja/devbook/network-currency/query-currency-supply.md new file mode 100644 index 000000000..e2b2a74d8 --- /dev/null +++ b/mkdocs/pages/ja/devbook/network-currency/query-currency-supply.md @@ -0,0 +1,114 @@ +--- +title: 通貨供給量を照会する +tutorial_level: beginner +--- + +# 通貨供給量を照会する + +取引所や市場データアグリゲーターは、時価総額やトークン指標を表示するために、正確な供給量を必要とします。 + +NEM は、ネイティブ通貨である [XEM](default:XEM) の供給量を REST API で公開しています。 +このチュートリアルでは、総供給量を照会し、そこから流通供給量を導出する方法を説明します。 + +## 前提条件 {: #prerequisites } + +このチュートリアルでは、SDK を必要とせずに [NEM REST API](../reference/rest/nem.md) を使用します。 +HTTP リクエストを送信する方法だけが必要です。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/network-currency/query_currency_supply', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM [メインネット](default:メインネット) ノードを指定します。 + +!!! note "なぜメインネットなのか" + 他のチュートリアルは、通常、実際の資金を使わないように [テストネット](default:テストネット) に対して実行します。 + このチュートリアルは、流通供給量を計算するために固定されたメインネットアカウントのアドレスを照会する点で異なります。そのため、`NODE_URL` はメインネットノードを指す必要があります。 + +## コードの説明 {: #code-explanation } + +### 総供給量を取得する {: #fetching-the-total-supply } + +{{ tutorial.code_snippet_tagged('step-1') }} + +[XEM](default:XEM) の総供給量は固定されています。 +すべての 8'999'999'999 XEM は [ネメシスブロック](default:ネメシスブロック) で作成され、新しい XEM が発行されることはありません。 + +このチュートリアルでは、供給量や可分性などの値をハードコードせず API から読み取ります。 +そのため、同じ方法を、供給量を変更できるものを含む他の [モザイク](default:モザイク) にも使用できます。 + +コードは、XEM モザイク識別子 `nem:xem` を `mosaicId` クエリパラメーターとして渡し、 エンドポイントへ `GET` リクエストを送信します。 + +レスポンスは、モザイク識別子と現在の `supply` を含む JSON オブジェクトです。 +`supply` は [全体単位](../../textbook/mosaics.md#divisibility) で表されます。 + +### モザイクの可分性を読み取る {: #reading-the-mosaics-divisibility } + +{{ tutorial.code_snippet_tagged('step-2') }} + +前の手順で取得した供給量はすでに全体単位ですが、次の手順で読み取るアカウント残高は [原子単位](../../textbook/mosaics.md#divisibility) で報告されます。 + +値を同じ単位に変換するため、この手順ではまずモザイクの [可分性](default:可分性) を取得します。 +その値を使って、残高を原子単位から全体単位へ変換します。 + + エンドポイントは、可分性を含むモザイク定義を返します。 +`nem:xem` の可分性は 6 です。 + +### 非流通供給量を取得する {: #fetching-the-non-circulating-supply } + +{{ tutorial.code_snippet_tagged('step-3') }} + +総供給量の一部は、公開市場に含まれないアカウントによって保有されています。 + +* **トレジャリー**: チームが管理する XEM を保有する準備金アカウント。 +* **ネメシス**: ネメシスブロックに署名したアカウント。 + ネメシスブロックの後はトランザクションを送信できないため、このアカウントが保有する XEM は実質的に流通していません。 +* **ネームスペースレンタルシンク**: [ネームスペース](default:ネームスペース) の登録に支払われた手数料を集めます。 +* **モザイクレンタルシンク**: [モザイク](default:モザイク) の作成に支払われた手数料を集めます。 + +コードは エンドポイントで各アカウントを照会し、残高を合計します。 + +残高は原子単位で合計します。 +表示するときだけ全 XEM に変換します。`scale`(`nem:xem` では 1'000'000)で割った商が整数部分で、余りが小数点以下 6 桁になります。 + +これを通常の除算で行うと浮動小数点数になり、残高が非常に大きいため、最後の桁を誤る可能性があります。 + +### 流通供給量を導出する {: #deriving-the-circulating-supply } + +{{ tutorial.code_snippet_tagged('step-4') }} + +流通供給量は、総供給量から非流通残高を引いた値です。 + +これは、公開市場で自由に利用できる XEM の量です。 + + の総供給量は全体単位なので、コードは差し引く前に `scale` を掛けて原子単位に変換し、その結果を表示します。 + +## 出力 {: #output } + +以下の出力は、通貨供給量を照会した場合の実行例です。 + +```text linenums="1" hl_lines="2 7 8" +--8<-- 'devbook/network-currency/query_currency_supply.log' +``` + +出力には、XEM 供給量の内訳が表示されます。 + +* **総供給量**(2 行目): 存在するすべての XEM。 +* **非流通供給量**(7 行目): トレジャリー、ネメシス、レンタルシンクの残高の合計。 +* **流通供給量**(8 行目): 実際に流通して利用できる XEM。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [総供給量を取得する](#fetching-the-total-supply) | | +| [モザイクの可分性を読み取る](#reading-the-mosaics-divisibility) | | +| [非流通供給量を取得する](#fetching-the-non-circulating-supply) | | + +## 次のステップ {: #next-steps } + +特定のアカウントの XEM 残高を確認するには、[アカウント残高を照会する](../accounts/query-balance.md) チュートリアルを参照してください。 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/serialization/index.md b/mkdocs/pages/ja/devbook/reference/serialization/index.md new file mode 100644 index 000000000..3d7486c60 --- /dev/null +++ b/mkdocs/pages/ja/devbook/reference/serialization/index.md @@ -0,0 +1,278 @@ +--- +hide: + - toc +--- + +# シリアライズ + +## 基本型 {: #basic-types } + +
+
Amount
+
8 ubytes
+

モザイクの数量を絶対単位で表します。正の値または 0 だけを指定できます。

+
Height
+
8 ubytes
+

ブロックチェーン上のブロックのインデックスです。最初のブロック(ネメシスブロック)の高さは 1 で、その後の各ブロックで 1 ずつ増えます。

+
Timestamp
+
4 ubytes
+

ネメシスブロックの作成から経過した秒数です。

+
Address
+
40 ubytes
+

アドレスはアカウントを識別し、その PublicKey から導出されます。40 バイトは Base32 エンコード形式に対応します。

+
Hash256
+
32 ubytes
+

32 バイト(256 ビット)のハッシュです。使用箇所によって変更される可能性があるため、正確なアルゴリズムは指定されていません。

+
PublicKey
+
32 ubytes
+

秘密鍵から導出される 32 バイト(256 ビット)の整数です。キーペアの公開識別子として広く配布でき、対応する秘密鍵でエンティティが署名したことを証明するために使われます。

+
Signature
+
64 ubytes
+

署名済みデータが変更されていないことを証明する 64 バイト(512 ビット)の配列です。NEM は Keccak-512 ハッシュ関数を使う Ed25519 署名を使用します。

+
+ +## 列挙型 {: #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/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/websockets/index.md b/mkdocs/pages/ja/devbook/reference/websockets/index.md new file mode 100644 index 000000000..08058a03b --- /dev/null +++ b/mkdocs/pages/ja/devbook/reference/websockets/index.md @@ -0,0 +1,785 @@ +# WebSocket {: #websockets } + +NEM は [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) でブロックチェーンイベントを公開します。 +そのため、[REST API](../rest/nem.md) を常にポーリングしなくても、アプリケーションでライブ更新を受信できます。 + +クライアントアプリケーションは、ネットワーク内の任意の [ノード](default:ノード) へ WebSocket 接続を開き、監視したい [チャネル](#channels) をサブスクライブします。 +チャネルでイベントが発生すると、ノードはサブスクライブしているすべてのクライアントへリアルタイムで通知します。 + +一部のチャネルは、REST API と同様に、即時データを取得する [リクエスト](#requests) も受け付けます。 +これにより、WebSocket だけを API として使い、ライブ通知とオンデマンド更新の両方を処理するアプリケーションを簡単にできます。 + +## 接続 {: #connection } + +NEM は、HTTP API のポートとは別の専用ポート(デフォルト `7778`)で、[SockJS](https://github.com/sockjs/sockjs-client) 上の [STOMP](https://stomp.github.io/) メッセージングプロトコルを使って WebSocket を提供します。 + +SockJS エンドポイントは `/w/messages` です。例えば `http://localhost:7778/w/messages` です。 + +クライアントは通常、SockJS クライアントライブラリまたはネイティブ WebSocket API と、メッセージングを処理する STOMP クライアントライブラリを組み合わせて接続します。 + +??? note "ネイティブ WebSocket で接続する" + + SockJS は WebSocket に似たトランスポートを提供し、クロスブラウザー対応と、ネイティブ WebSocket が利用できない場合の HTTP ベースのフォールバックを備えています。 + + ネイティブ WebSocket をサポートするクライアントは、SockJS の WebSocket トランスポートエンドポイント `/w/messages/websocket` に直接接続できます。例えば `ws://localhost:7778/w/messages/websocket` です。 + + これは SockJS サーバーを利用しながら、SockJS クライアントライブラリなしで SockJS の WebSocket トランスポートを使う方法です。 + +## STOMP セッション {: #stomp-session } + +STOMP セッション +: [STOMP](https://stomp.github.io/) メッセージングプロトコルに従い、WebSocket 接続上で行うクライアントとノードの会話です。 + +クライアントは、[STOMP フレーム]() をノードと交換してセッションを制御します。 + +1. `CONNECT` フレームを送信して STOMP セッションを開始する。 +2. 監視する各 [WebSocket チャネル]() に `SUBSCRIBE` フレームを送信する。 + 各サブスクリプションには、クライアントが定義する `id` が必要です。 +3. 明示的な登録が必要なチャネルの通知を有効にするため、オプションの [登録リクエスト](#registration-requests) を `SEND` フレームで送信する。 +4. サブスクライブしたチャネルのイベントごとに、ノードから `MESSAGE` フレームを受信する。 +5. 通知の受信を停止するため、サブスクライブした各チャネルに `UNSUBSCRIBE` フレームを送信する。 +6. セッションを終了するため、`DISCONNECT` フレームを送信する。 + +!!! warning "接続が静かに切断される可能性があります" + + WebSocket 接続は、例えば長時間アイドル状態だった後など、通知なしに切断される可能性があります。 + ほとんどの STOMP クライアントは接続終了コールバックでこれを報告するため、再接続に適した場所です。 + + 再接続すると新しいセッションが開始されるため、すべてのチャネルをもう一度サブスクライブする必要があります。 + +## STOMP フレーム {: #stomp-frames } + +STOMP フレーム +: [STOMP](https://stomp.github.io/) プロトコルに従うプレーンテキストメッセージです。 + コマンド、オプションの `header:value` 行、オプションの本文で構成されます。 + +クライアントとノードは、次のフレームタイプを交換します。 + +### `CONNECT` {: #connect } + +STOMP セッションを開始します。 +接続が開いた直後に、クライアントはこのフレームを 1 回送信する必要があります。 +詳細は [STOMP 仕様](https://stomp.github.io/stomp-specification-1.2.html#CONNECT_or_STOMP_Frame) を参照してください。 + +```stomp title="Example" +CONNECT +accept-version:1.2 +heart-beat:0,0 +``` + +### `SUBSCRIBE` {: #subscribe } + +クライアントが選んだ `id` と `destination` で [WebSocket チャネル]() をサブスクライブします。 +詳細は [STOMP 仕様](https://stomp.github.io/stomp-specification-1.2.html#SUBSCRIBE) を参照してください。 + +```stomp title="Example" +SUBSCRIBE +id:sub-0 +destination:/blocks +``` + +* `id` は 1 つの接続内でのみ一意です(他のクライアントは同じ値を再利用できます)。 + すべてのメッセージで `subscription` ヘッダーとして返され、後で `UNSUBSCRIBE` に使われます。 +* `destination` はチャネルを識別するため、同じ接続で複数のチャネルを監視できます。 + +### `MESSAGE` {: #message } + +ノードからチャネルデータを配信します。 +ノードが送信する唯一のフレームタイプです。 +詳細は [STOMP 仕様](https://stomp.github.io/stomp-specification-1.2.html#MESSAGE) を参照してください。 + +```stomp title="Example" +MESSAGE +destination:/blocks +subscription:sub-0 +message-id:befkedjj-6247 + +{ ... } +``` + +* `destination` は `SUBSCRIBE` フレームのチャネルと一致します。 +* `subscription` は `SUBSCRIBE` フレームの `id` と一致します。 +* `message-id` はサーバーが各メッセージに割り当てる一意の識別子です。 +* `{ ... }` は本文で、形状は [チャネル](#channels) によって異なる JSON オブジェクトです。 + 下の **メッセージ本文** タブを参照してください。 + +### `SEND` {: #send } + +`/w/api` 宛先へ [WebSocket リクエスト]() を送信します。 +詳細は [STOMP 仕様](https://stomp.github.io/stomp-specification-1.2.html#SEND) を参照してください。 + +```stomp title="Example" +SEND +destination:/w/api/account/subscribe + +{ "account": "{address}" } +``` + +### `UNSUBSCRIBE` {: #unsubscribe } + +`id` でサブスクリプションをキャンセルします。 +詳細は [STOMP 仕様](https://stomp.github.io/stomp-specification-1.2.html#UNSUBSCRIBE) を参照してください。 + +```stomp title="Example" +UNSUBSCRIBE +id:sub-0 +``` + +### `DISCONNECT` {: #disconnect } + +セッションを終了します。 +詳細は [STOMP 仕様](https://stomp.github.io/stomp-specification-1.2.html#DISCONNECT) を参照してください。 + +```stomp title="Example" +DISCONNECT +``` + +## チャネル {: #channels } + +WebSocket チャネル +: ノードの通知をチャネルにまとめます。 + クライアントは受信したい通知を持つ各チャネルをサブスクライブします。 + +すべてのチャネルは [`SUBSCRIBE`](#subscribe) フレームでサブスクライブします。 + +ここでは、報告するイベントの種類ごとに利用可能なチャネルをまとめます。 + +### ブロックチャネル {: #block-channels } + +チェーンに追加された新しいブロックを報告するチャネルです。 + +#### `/blocks` {: #blocks } + +ws:blocks +: 新しいブロックがチェーンに追加されるたびに、サブスクライブしたクライアントへ通知します。 + + ノードがピアに追いついている場合や [ロールバック](default:ロールバック) の後など、一度に複数のブロックが追加されると、チャネルは **バースト** を送信します。ブロックごとに 1 通知を、チェーン順に短い間隔で配信します。 + + ロールバック後は、新しいブロックがすでに報告したブロックを置き換えるため、以前の通知より低いブロック高さを通知することがあります。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/blocks +``` + + + +```stomp +MESSAGE +destination:/blocks +subscription:sub-0 +message-id:... + +{ ... } +``` + +[Block](../rest/nem.md#model/Block) JSON 本文が続きます。 +
+ +#### `/blocks/new` {: #blocksnew } + +ws:blocks/new +: チェーンが変更されるたびに、追加または置き換えられた最初のブロックの高さを含む通知を、更新ごとに 1 つ送信します。 + + とは異なり、含まれるブロック数に関係なく、チェーン更新ごとに 1 通知だけ送信します。 + 例えば 5 ブロックが一度に追加された場合、 は 5 通知を送信しますが、このチャネルは最初のブロックの高さを含む 1 通知だけを送信します。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/blocks/new +``` + + + +```stomp +MESSAGE +destination:/blocks/new +subscription:sub-0 +message-id:... + +{ "height": 1234567 } +``` + +
+ +### トランザクションチャネル {: #transaction-channels } + +関係するアカウントにかかわらず、トランザクションの活動を報告するチャネルです。 + +#### `/unconfirmed` {: #unconfirmed } + +ws:unconfirmed +: 関係するアカウントにかかわらず、トランザクションが [未承認トランザクションプール](default:未承認トランザクションプール) に入るたびにサブスクライブしたクライアントへ通知します。 + + ここには [連署](default:連署) を含むすべてのトランザクションタイプが現れます。連署は [アカウントチャネル](#account-channels) では報告されません。 + [マルチシグトランザクション](default:マルチシグトランザクション) は外側のマルチシグトランザクションとして現れ、その内部に内部トランザクションがネストされます。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/unconfirmed +``` + + + +```stomp +MESSAGE +destination:/unconfirmed +subscription:sub-0 +message-id:... + +{ ... } +``` + +[Transaction](../rest/nem.md#model/Transaction) JSON 本文が続きます。 +
+ +### アカウントチャネル {: #account-channels } + +残高、トランザクション、所有するモザイクやネームスペースなど、特定のアカウントの活動を報告するチャネルです。 + +!!! note "アドレス形式" + + チャネルの `destination` またはリクエスト本文にアドレスが現れる場合は、[エンコードされたアドレス](../../../textbook/cryptography.md#addresses) 形式を使います。 + 大文字と数字のみで、ハイフンはありません。 + + **例:** `TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4`。 + +アカウントチャネルは、アカウントの状態が変わらない場合でも、アカウントがトランザクションに **関係する** たびに通知します。 + +関係するとみなされるアカウントは、トランザクションタイプによって異なります。 + +| トランザクションタイプ | 関係するアカウント | +| --- | --- | +| 転送 | 署名者と受取人。 | +| インポータンス転送 | 署名者とリモートアカウント。 | +| マルチシグアカウント変更 | 追加または削除されたすべての連署人と署名者。 | +| ネームスペース登録 | 署名者。 | +| モザイク定義の作成 | 署名者、および定義に徴収手数料が含まれる場合は徴収手数料の受取人。 | +| モザイク供給量の変更 | 署名者、およびモザイク定義に徴収手数料が含まれる場合は徴収手数料の受取人。 | +| マルチシグ | 開始した連署人、マルチシグアカウント、内部トランザクションに含まれるその他のアカウント。 | + +!!! note "マルチシグトランザクション" + + 開始した連署人と内部送金の受取人など、複数の役割を持つアカウントには、役割ごとに 1 通知が届きます。 + + [マルチシグトランザクション](default:マルチシグトランザクション) に複数の署名が必要な場合、追加の各連署人は別の [連署](default:連署) を送信して承認します。 + 連署はグローバルな チャネルにだけ現れます。 + マルチシグアカウントや送信した連署人のチャネルを含め、アカウントチャネルには届きません。 + +#### `/account/{address}` {: #accountaddress } + +ws:account/{address} +: アドレスが関係する承認済みブロックごとに、アカウントの現在の状態を通知します。トランザクションで [関係する](#account-channels) 場合と、ブロックをハーベスティングした場合が含まれます。 + 先にアドレスを [登録](#registration-requests) する必要があります。 + + トランザクションに関係しても、アカウントが変化したとは限りません。 + 例えば XEM を 0 転送した受取人は、残高が変わらなくても通知されます。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/account/{address} +``` + + + +```stomp +MESSAGE +destination:/account/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` + +[AccountMetaDataPair](../rest/nem.md#model/AccountMetaDataPair) JSON 本文が続きます。 +
+ +#### `/unconfirmed/{address}` {: #unconfirmedaddress } + +ws:unconfirmed/{address} +: アカウントに [関係する](#account-channels) トランザクションが [未承認トランザクションプール](default:未承認トランザクションプール) に入るたびに、サブスクライブしたクライアントへ通知します。 + 先にアドレスを [登録](#registration-requests) する必要があります。 + + トランザクションはまだブロックに含まれていないため、`meta.height` フィールドには JSON パーサーが安全に表現できる最大の整数であるプレースホルダー `9007199254740991` が含まれます。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/unconfirmed/{address} +``` + + + +```stomp +MESSAGE +destination:/unconfirmed/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` + +[TransactionMetaDataPair](../rest/nem.md#model/TransactionMetaDataPair) JSON 本文が続きます。 +
+ +#### `/transactions/{address}` {: #transactionsaddress } + +ws:transactions/{address} +: アカウントに [関係する](#account-channels) トランザクションを承認済みブロックが含むたびに、サブスクライブしたクライアントへ通知します。 + 先にアドレスを [登録](#registration-requests) する必要があります。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/transactions/{address} +``` + + + +```stomp +MESSAGE +destination:/transactions/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` + +[TransactionMetaDataPair](../rest/nem.md#model/TransactionMetaDataPair) JSON 本文が続きます。 +
+ +#### `/account/mosaic/owned/{address}` {: #accountmosaicownedaddress } + +ws:account/mosaic/owned/{address} +: 承認済みブロックによってアカウントのモザイクが変わった可能性があるたびに、アカウントのモザイクを通知します。 + アカウントのモザイクとは、残高を保有するモザイクと、作成したモザイクです。 + + モザイク関連のトランザクションがアカウントに [関係する](#account-channels) ブロックに含まれると、通知バーストを送信します。 + モザイクを運ぶ転送は、署名者、受取人、徴収手数料の受取人へ通知します。 + モザイク定義の作成とモザイク供給量の変更は、モザイク作成者と徴収手数料の受取人へ通知します。 + + トランザクションがアカウントのモザイクを変更する必要はありません。 + 例えばモザイクを 0 単位転送しても、送信者と受取人の両方へ通知します。 + + 各バーストにはアカウントのモザイク一覧全体が含まれ、変更されたモザイクにかかわらずモザイクごとに 1 通知が届きます。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/account/mosaic/owned/{address} +``` + + + +```stomp +MESSAGE +destination:/account/mosaic/owned/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` + +[Mosaic](../rest/nem.md#model/Mosaic) JSON 本文が続きます。 +
+ +#### `/account/mosaic/owned/definition/{address}` {: #accountmosaicowneddefinitionaddress } + +ws:account/mosaic/owned/definition/{address} +: 承認済みブロックによって変わった可能性があるたびに、アカウントのモザイク定義を通知します。 + + と同じトランザクションによって発生し、同じモザイクを対象とします。 + 各バーストにはアカウントのモザイク定義一覧全体が含まれ、変更されたモザイクにかかわらず定義ごとに 1 通知が届きます。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/account/mosaic/owned/definition/{address} +``` + + + +```stomp +MESSAGE +destination:/account/mosaic/owned/definition/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` + +[MosaicDefinitionSupplyTuple](../rest/nem.md#model/MosaicDefinitionSupplyTuple) JSON 本文が続きます。 +
+ +#### `/account/namespace/owned/{address}` {: #accountnamespaceownedaddress } + +ws:account/namespace/owned/{address} +: 承認済みブロックによって変わった可能性があるたびに、アカウントが所有するネームスペースを通知します。 + + アカウントが署名したネームスペース提供トランザクションをブロックが含むと、通知バーストを送信します。 + 各バーストには所有するネームスペース一覧全体が含まれ、ネームスペースごとに 1 通知が届きます。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/account/namespace/owned/{address} +``` + + + +```stomp +MESSAGE +destination:/account/namespace/owned/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` + +[Namespace](../rest/nem.md#model/Namespace) JSON 本文が続きます。 +
+ +#### `/recenttransactions/{address}` {: #recenttransactionsaddress } + +ws:recenttransactions/{address} +: への応答としてのみ、アカウントの最新 25 件の承認済みトランザクションを通知します。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/recenttransactions/{address} +``` + + + +```stomp +MESSAGE +destination:/recenttransactions/{address} +subscription:sub-0 +message-id:... + +{ ... } +``` + +`data` フィールドにラップされた [TransactionMetaDataPair](../rest/nem.md#model/TransactionMetaDataPair) の一覧が続きます。 +
+ +### システムチャネル {: #system-channels } + +ブロックチェーンイベントではなく、ノードの状態とリクエストエラーを報告するチャネルです。 + +#### `/node/info` {: #nodeinfo } + +ws:node/info +: への応答としてのみ、ノードの情報をサブスクライブしたクライアントへ通知します。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```stomp +SUBSCRIBE +id:sub-0 +destination:/node/info +``` + + + +```stomp +MESSAGE +destination:/node/info +subscription:sub-0 +message-id:... + +{ ... } +``` + +[Node](../rest/nem.md#model/Node) JSON 本文が続きます。 +
+ +#### `/errors` {: #errors } + +ws:errors +: `/w/api` の [WebSocket リクエスト]() が失敗したとき、例えばアドレスのペイロードが無効なときに、サブスクライブしたクライアントへ通知します。 + クライアントは接続直後にこのチャネルをサブスクライブできるため、問題が静かに破棄されず、ここに現れます。 + +
+ + +
:material-arrow-up-bold: サブスクリプションフレーム:material-arrow-down-bold: 通知フレーム
+ +```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 リクエスト +: クライアントから送信するメッセージで、ノードに次のいずれかを配信させます。 + **登録** が必要なチャネルの通知、またはブロックチェーンの状態の **スナップショット** を含む即時通知です。 + +リクエストは **読み取り専用** で、チェーンの状態を変更しません。 + +すべてのリクエストは [`SEND`](#send) フレームで、`/w/api/` から始まる宛先へ送信します。 +応答を返さないリクエストもあれば、上の [チャネル](#channels) のいずれかを通して結果を返すリクエストもあります。 + +### 登録リクエスト {: #registration-requests } + +一部の [アカウントチャネル](#account-channels) は、アドレスを登録するまで何も送信しません。 +これらのリクエストで登録すると、チャネルが通知を配信し始めます。 + +!!! note "登録は共有されます" + + ノードは、接続しているすべてのクライアントで共有する登録アドレスの一覧を 1 つ保持します。 + どのクライアントかがアドレスを登録すると、そのアカウントのチャネルをサブスクライブしているすべてのクライアントが、再登録なしでそのアドレスの通知を受け取ります。 + 登録はノードが再起動するまで有効です。 + +#### `/w/api/account/subscribe` {: #wapiaccountsubscribe } + +req:w/api/account/subscribe +: アドレスを登録し、ノードが [アカウントチャネル](#account-channels) に通知を送信し始めるようにします。 + +
+ + +
:material-arrow-up-bold: リクエストフレーム
+ +```stomp +SEND +destination:/w/api/account/subscribe + +{ "account": "{address}" } +``` + +
+ +#### `/w/api/account/get` {: #wapiaccountget } + +req:w/api/account/get +: と同じようにアドレスを登録し、アカウントの現在の状態を含む通知を に送信するようノードに要求します。 + +
+ + +
:material-arrow-up-bold: リクエストフレーム
+ +```stomp +SEND +destination:/w/api/account/get + +{ "account": "{address}" } +``` + +
+ +### スナップショットリクエスト {: #snapshot-requests } + +各リクエストは、新しいイベントを待たずに、現在のデータのスナップショットをノードからチャネルへ即時送信させます。 + +これにより、[REST API](../rest/nem.md) をポーリングする代わりに、ライブ更新と同じチャネルを通してオンデマンドで現在のデータを取得できます。 + +#### `/w/api/account/transfers/all` {: #wapiaccounttransfersall } + +req:w/api/account/transfers/all +: アカウントの最新 25 件までの承認済みトランザクションを に送信し、 と同じ方法で承認待ちのトランザクションも送信するようノードに要求します。 + +
+ + +
:material-arrow-up-bold: リクエストフレーム
+ +```stomp +SEND +destination:/w/api/account/transfers/all + +{ "account": "{address}" } +``` + +
+ +#### `/w/api/account/transfers/unconfirmed` {: #wapiaccounttransfersunconfirmed } + +req:w/api/account/transfers/unconfirmed +: アカウントの最新の承認待ちトランザクションを最大 10 件、 とグローバルな チャネルに送信するようノードに要求します。 + +
+ + +
:material-arrow-up-bold: リクエストフレーム
+ +```stomp +SEND +destination:/w/api/account/transfers/unconfirmed + +{ "account": "{address}" } +``` + +
+ +#### `/w/api/account/mosaic/owned` {: #wapiaccountmosaicowned } + +req:w/api/account/mosaic/owned +: アカウントが所有するモザイクを含む通知を に送信するようノードに要求します。 + +
+ + +
:material-arrow-up-bold: リクエストフレーム
+ +```stomp +SEND +destination:/w/api/account/mosaic/owned + +{ "account": "{address}" } +``` + +
+ +#### `/w/api/account/mosaic/owned/definition` {: #wapiaccountmosaicowneddefinition } + +req:w/api/account/mosaic/owned/definition +: アカウントが所有するモザイク定義を含む通知を に送信するようノードに要求します。 + +
+ + +
:material-arrow-up-bold: リクエストフレーム
+ +```stomp +SEND +destination:/w/api/account/mosaic/owned/definition + +{ "account": "{address}" } +``` + +
+ +#### `/w/api/account/namespace/owned` {: #wapiaccountnamespaceowned } + +req:w/api/account/namespace/owned +: アカウントが所有するネームスペースを含む通知を に送信するようノードに要求します。 + +
+ + +
:material-arrow-up-bold: リクエストフレーム
+ +```stomp +SEND +destination:/w/api/account/namespace/owned + +{ "account": "{address}" } +``` + +
+ +#### `/w/api/block/last` {: #wapiblocklast } + +req:w/api/block/last +: 最新ブロックを含む通知を に送信するようノードに要求します。 + + ノードはブロックが今追加されたかのように処理するため、そのブロックに関係するアカウントの [アカウントチャネル](#account-channels) にも再び通知します。 + +
+ + +
:material-arrow-up-bold: リクエストフレーム
+ +```stomp +SEND +destination:/w/api/block/last +``` + +
+ +#### `/w/api/node/info` {: #wapinodeinfo } + +req:w/api/node/info +: ノード自身の情報を含む通知を に送信するようノードに要求します。 + +
+ + +
:material-arrow-up-bold: リクエストフレーム
+ +```stomp +SEND +destination:/w/api/node/info +``` + +
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/devbook/start/hello-world.md b/mkdocs/pages/ja/devbook/start/hello-world.md new file mode 100644 index 000000000..6802ed89f --- /dev/null +++ b/mkdocs/pages/ja/devbook/start/hello-world.md @@ -0,0 +1,64 @@ +--- +title: Hello World +tutorial_level: beginner +--- + +# Hello World + +このチュートリアルでは、最小限のプログラムを作成して Symbol SDK が正しく動作することを確認します。 +プログラムは次の処理を行います。 + +* SDK を使ってネットワーク名と起動日を取得する。 +* [ノード](default:ノード) に接続し、現在のチェーン高を表示する。 + +必要なのは基本的な SDK 呼び出しと REST リクエストだけで、アカウント、キー、トランザクションは必要ありません。 + +## 前提条件 {: #prerequisites } + +まだ準備できていない場合は、まず [開発環境のセットアップ](../start/setup.md) を行ってください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/start/hello_world', ['py', 'js']) }} + +### SDK の呼び出し {: #making-sdk-calls } + +{{ tutorial.code_snippet_tagged('step-1') }} + + クラスは、NEM ブロックチェーンで Symbol SDK を使用する際の主なエントリーポイントです。 +トランザクションの構築と署名からネットワーク関連情報の取得まで、必要になるほとんどのメソッドを提供します。 + +ファサードを作成するには、`mainnet` または `testnet` のいずれかの、使用するネットワーク名を指定します。 + +この例では、ネットワークの起動日も取得します。 + メソッドは、ネットワークタイムスタンプを UTC の日時に変換します。 +ジェネシスタイムスタンプである `0` を渡すと、ジェネシスブロックが生成された時点、つまりネットワークの起動日を取得できます。 + +### ノードから情報を取得する {: #retrieving-information-from-a-node } + +{{ tutorial.code_snippet_tagged('step-2') }} + +NEM ブロックチェーンとのやりとりは、[ノード](default:ノード) を通じて行われます。このノードは、ネットワークの状態を照会したり、トランザクションを送信するための REST インターフェースを提供しています。 + +この例では、テストネットのノードに接続し、 エンドポイントから現在のチェーン高を取得します。 + +このリクエストに秘密鍵や認証の必要はなく、環境が正しくセットアップされ、ネットワークに接続できることを確認するための簡単で効果的なテストになります。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text +--8<-- 'devbook/start/hello_world.log' +``` + +## まとめ {: #conclusion } + +上記の出力が表示されたら、準備は完了です。 +Symbol SDK にアクセスでき、NEM ノードへの接続にも成功しています。 + +これで NEM の冒険を始めるための準備は万端です。 + +次は [アカウントを作成](../accounts/create-from-private-key.md) してみてはいかがでしょうか。 diff --git a/mkdocs/pages/ja/devbook/start/setup.md b/mkdocs/pages/ja/devbook/start/setup.md new file mode 100644 index 000000000..428c01359 --- /dev/null +++ b/mkdocs/pages/ja/devbook/start/setup.md @@ -0,0 +1,90 @@ +--- +title: セットアップ +--- + +# 開発環境のセットアップ + +このページでは、このドキュメントのチュートリアルを実行するために必要な依存関係と、実行方法を説明します。 + +ほとんどのチュートリアルでは、[NEM](default:NEM) と [Symbol](default:Symbol) の両方のネットワークをサポートする Symbol SDK を使用します。 +NEM アプリケーションの構築に推奨されるライブラリなので、以下の手順はすべてのチュートリアルに適用できます。 + +使用する言語を選択してください。 + +=== ":simple-python: Python" + + + + +
前提条件[Python](https://www.python.org/downloads/) 3.10 以降
インストール + 次のコマンドで Symbol SDK バージョン 3.3.1 をインストールします。 + ```bash + pip install symbol-sdk-python --upgrade + ``` +
サンプルコードの実行 + サンプルをダウンロードして、次のコマンドで実行します。 + ```bash + python hello-world.py + ``` +
+ + ??? warning "トラブルシューティング" + + システムによっては、Python の依存関係の一部がソースからビルドされるため、SDK のインストールに追加のシステムパッケージが必要になる場合があります。 + + ヘッダー、ライブラリ、コンパイラツールが見つからないというエラーでインストールに失敗した場合は、使用しているシステムに必要な **開発用パッケージ** をインストールして、もう一度インストールを実行してください。 + + よくある症状として、`gcc` または `pysha3` に関するメッセージが表示されます。 + + Ubuntu と Debian では、通常、次のコマンドで十分です。 + + ```bash + sudo apt install python3-dev build-essential + ``` + + その後、Symbol SDK のインストールをもう一度実行してください。 + +=== ":simple-javascript: JavaScript" + + + + +
前提条件[Node.js](https://nodejs.org/) の現在サポートされているバージョン
インストール + プロジェクトフォルダーを作成し、依存関係として Symbol SDK バージョン 3.3.1 をインストールします。 + ```bash + mkdir nem-dev && cd nem-dev + npm init -y + npm install symbol-sdk + ``` +
サンプルコードの実行 + サンプルをダウンロードして、次のコマンドで実行します。 + ```bash + node hello-world.mjs + ``` +
+ +## 次のステップ {: #next-steps } + +* [Hello World](./hello-world.md) に進む + + diff --git a/mkdocs/pages/ja/devbook/transactions/messages.md b/mkdocs/pages/ja/devbook/transactions/messages.md new file mode 100644 index 000000000..a8a9f8e7a --- /dev/null +++ b/mkdocs/pages/ja/devbook/transactions/messages.md @@ -0,0 +1,159 @@ +--- +title: メッセージ +tutorial_level: intermediate +--- + +# 転送トランザクションでメッセージを送信する + +[転送トランザクション](default:転送トランザクション) には、最大 1024 バイトのデータをトランザクションに付加できるオプションのメッセージフィールドを含められます。 +メッセージは平文で送信することも、受取人の公開鍵で暗号化することもできます。暗号化すれば、意図した受取人だけが読めます。 + +このチュートリアルでは、平文メッセージと暗号化メッセージの両方を送信し、受信したメッセージをデコードする方法を説明します。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 +* 転送トランザクションを送信する [アカウント](default:アカウント) を、[コード](../accounts/create-from-private-key.md) または [ウォレット](../../userbook/wallet/create-account.md) を使って作成する。 +* トランザクション手数料を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](../accounts/testnet-faucet.md) を参照してください。 + +さらに、手数料の計算、ネットワーク時刻、トランザクションの承認の仕組みを理解するため、[転送トランザクション](./transfer-xem.md) チュートリアルを確認してください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/transactions/messages', ['py', 'js']) }} + +## コードの説明 {: #code-explanation } + +このチュートリアルでは、転送トランザクションのメッセージ固有の部分に焦点を当てます。 +ネットワーク時刻の取得、手数料の計算、トランザクションのアナウンスについては [転送トランザクション](./transfer-xem.md) チュートリアルで説明しているため、ここでは省略します。 + +### アカウントをセットアップする {: #setting-up-accounts } + +{{ tutorial.code_snippet_tagged('step-1') }} + +メッセージを送信するには、送信者の [秘密鍵](default:秘密鍵) と受取人の [アドレス](default:アドレス) が必要です。 +メッセージを暗号化するには、さらに受取人の [公開鍵](default:公開鍵) が必要です。 + +このチュートリアルでは、平文メッセージと暗号化メッセージの送受信を示すため、送信者と受取人の 2 つのアカウントを使います。 +スニペットは、`SENDER_PRIVATE_KEY` と `RECIPIENT_PRIVATE_KEY` 環境変数からそれぞれの秘密鍵を読み込み、設定されていない場合はテスト用キーをデフォルト値として使用します。 +受取人の公開鍵とアドレスは、秘密鍵から導出されます。 + +!!! note "公開鍵を取得する" + + アドレスだけがわかっている場合は、 エンドポイントを使ってネットワークから公開鍵を取得できます。 + アカウントの公開鍵が利用可能になるのは、少なくとも 1 回トランザクションを送信した後です。 + +### 平文メッセージを送信する {: #sending-a-plain-text-message } + +{{ tutorial.code_snippet_tagged('step-2') }} + +トランザクションディスクリプタに `mosaics` フィールドと `message` フィールドの両方を含めることで、モザイクの転送とメッセージを組み合わせられます。 + +その後、[転送トランザクションで XEM を送信する](./transfer-xem.md) と同じ手順でトランザクションに署名してアナウンスします。 + +**メッセージの制約:** + +* **最大サイズ:** 1024 バイト(ネットワークはそれより大きいメッセージを拒否します)。 +* **エンコーディング:** 慣例では UTF-8 ですが、プロトコルは標準を強制しません。 +* **プライバシー:** 暗号化しない限り、すべてのメッセージはブロックチェーン上で公開されます。 + +!!! tip "大きなデータを扱う" + + 1024 バイトを超えるデータが必要なアプリケーションでは、一般的に次の方法を使います。 + + * **オンチェーンストレージ:** データを複数の [転送トランザクション](default:転送トランザクション) に分割して、すべてをブロックチェーン上に保持する。 + * **オフチェーンストレージ:** データをオフチェーンに保存し、メッセージフィールドにハッシュと参照を含める。 + ハッシュでデータの完全性を検証し、参照で取得できるようにします。 + +### 平文メッセージを受信する {: #receiving-a-plain-text-message } + +{{ tutorial.code_snippet_tagged('step-3') }} + +トランザクションをアナウンスした後、{{ tutorial.var('retrieve_confirmed_transaction') }} ヘルパー関数はトランザクションが承認されるまで エンドポイントをポーリングします。 + +承認されたトランザクションには、メッセージが 16 進数文字列で含まれています。 +元のメッセージを取得するには、16 進数文字列をバイト列に変換し、UTF-8 としてデコードします。 + +### 暗号化メッセージを送信する {: #sending-an-encrypted-message } + +{{ tutorial.code_snippet_tagged('step-4') }} + +暗号化メッセージは、送信者の秘密鍵と受取人の公開鍵から導出した共有秘密を使ってメッセージの内容を保護し、機密性を提供します。 +送信者と受取人は、自分の秘密鍵と相手の公開鍵を使ってメッセージを復号できます。 + + クラスがメッセージの暗号化を処理します。 + +1. 送信者のキーペアで を作成する。 +2. 受取人の公開鍵とメッセージバイトを使い、 でメッセージをエンコードする。 +3. 暗号化したペイロードをトランザクションの `message` フィールドに付加する。 + +その後、[転送トランザクションで XEM を送信する](./transfer-xem.md) と同じ手順でトランザクションに署名してアナウンスします。 + +!!! note "メッセージの暗号化は慣例です" + + NEM プロトコルは、メッセージ暗号化の標準を定義していません。 + 送信者と受取人は、メッセージを暗号化するかどうか、および使用する暗号方式について事前に合意する必要があります。 + + は **AES-GCM** を実装します。これは、ほとんどのウォレットとアプリケーションが使用する慣例です。 + このクラスには、初期の NEM ウォレットとアプリケーションが使用した従来の AES-CBC メッセージと互換性を保つため、 もあります。 + は両方の方式を自動的に検出してデコードします。 + + 詳細については、テキストブックの [オプションメッセージ](../../textbook/transfer_transactions.md#optional-message) を参照してください。 + +### 暗号化メッセージを受信する {: #receiving-an-encrypted-message } + +{{ tutorial.code_snippet_tagged('step-5') }} + +暗号化メッセージのトランザクションをアナウンスした後、{{ tutorial.var('retrieve_confirmed_transaction') }} ヘルパー関数は承認を待ちます。 + +承認されたトランザクションのメッセージを復号するには、受取人のキーペアで を作成し、送信者の公開鍵(トランザクションの `signer` フィールドから取得)と暗号化ペイロードを指定して を呼び出します。 + +メソッドは、復号に成功したかどうかを示す {{ tutorial.var('(is_decoded, message)') }} タプルを返します。成功した場合は元の平文バイトが含まれ、さらにデコードする必要があります。 + +!!! note "復号はどちらの方向でも機能します" + + 暗号化では両方のキーペアから導出した共有秘密を使うため、送信者も自分の秘密鍵と受取人の公開鍵でメッセージを復号できます。 + これにより、ブロックチェーン上に公開された後で、双方がメッセージの内容を検証できます。 + +復号に失敗する原因には、次のものがあります。 + +* メッセージが別の受取人向けに暗号化されている。 +* メッセージが破損している、または改ざんされている。 +* メッセージが暗号化されておらず、平文である。 +* 相手に対して誤った公開鍵を使った。 +* 暗号化と復号で同じ慣例を使っていない。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text linenums="1" hl_lines="9 16 19 20 27" +--8<-- 'devbook/transactions/messages.log' +``` + +出力の要点は次のとおりです。 + +* **平文メッセージ**(9 行目): 1 つ目のトランザクションに付加されたメッセージ。暗号化されていないため、ブロックチェーンを調べる人は誰でも読めます。 +* **受信した平文メッセージ**(16 行目): 16 進数ペイロードを UTF-8 に戻し、承認されたトランザクションから復元した同じメッセージ。 +* **元のメッセージ**(19 行目): 暗号化前の秘密メッセージ。 +* **暗号化ペイロード**(20 行目): で前のメッセージを暗号化した結果で、16 進数文字列として表示されます。これがブロックチェーンに保存される内容です。 +* **受取人が復号したメッセージ**(27 行目): 承認されたトランザクションから取得し、受取人の秘密鍵と送信者の公開鍵を使って で復号した元のメッセージ。 + +出力に表示されたトランザクションハッシュを [NEM テストネットエクスプローラー](https://testnet.nem.fyi/) で検索すれば、トランザクションを確認できます。 +エクスプローラーは秘密鍵にアクセスできないため、暗号化メッセージを復号できません。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [テキストを UTF-8 バイトに変換する](#sending-a-plain-text-message) | `TextEncoder`(JS)と `str.encode`/`bytes.decode`(Python)
SDK の一部ではないシステムメソッド | +| [メッセージを暗号化する](#sending-an-encrypted-message) | | +| [メッセージを復号する](#receiving-an-encrypted-message) | | diff --git a/mkdocs/pages/ja/devbook/transactions/monitoring-status.md b/mkdocs/pages/ja/devbook/transactions/monitoring-status.md new file mode 100644 index 000000000..fecd13f8d --- /dev/null +++ b/mkdocs/pages/ja/devbook/transactions/monitoring-status.md @@ -0,0 +1,186 @@ +--- +title: トランザクションのステータス +tutorial_level: beginner +--- + +# トランザクションのステータスを監視する + +NEM ネットワークへ [トランザクション](default:トランザクション) をアナウンスした後、トランザクションは [ブロック](default:ブロック) に含まれるまで未承認のままです。 + +ステータスの変化を監視することは、トランザクションの承認または失敗に応答できる、応答性の高いアプリケーションを構築するために重要です。 + +このチュートリアルでは、承認されるまでトランザクションのステータスをポーリングする方法、[未承認トランザクションプール](default:未承認トランザクションプール) で承認を待っているか確認する方法、そしていつまでも承認されないと判断する方法を説明します。 + +このような監視は通常、[XEM を送信する](./transfer-xem.md) チュートリアルに示すように、トランザクションが承認されることを確認するため、アナウンス直後に行います。 + +!!! note "本番環境ではポーリングは推奨されません" + + このチュートリアルでは説明のためにポーリングでトランザクションのステータスを確認しますが、本番アプリケーションに推奨される方法ではありません。 + + [WebSocket](../reference/websockets/index.md) を使えば、API を繰り返し呼び出すオーバーヘッドなしに、より応答性の高い解決策を実現できます。 + +## 前提条件 {: #prerequisites } + +このチュートリアルでは、SDK を必要とせずに [NEM REST API](../reference/rest/nem.md) を使用します。 +HTTP リクエストを送信する方法だけが必要です。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/transactions/monitoring_status', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM API ノードを指定します。 +値が指定されていない場合は、デフォルト値を使用します。 + +チュートリアルでは、まず次の再利用可能な関数を定義します。 + +* {{ tutorial.var('get_confirmation_height()') }}: トランザクションが [ハーベスティング](default:ハーベスティング) によって承認され、すでにブロックチェーンの一部になっているか確認します。 +* {{ tutorial.var('is_in_unconfirmed_pool()') }}: トランザクションが [未承認トランザクションプール](default:未承認トランザクションプール) で承認を待っているかを返します。 +* {{ tutorial.var('wait_for_confirmation()') }}: トランザクションが承認されるか試行回数に達するまで、承認の確認を繰り返します。 + +その後、[すべてを組み合わせる](#putting-it-all-together) に示すように、これらを一緒に呼び出してトランザクションを監視します。 + +## コードの説明 {: #code-explanation } + +### トランザクションハッシュ、アドレス、署名を取得する {: #finding-the-transaction-hash-address-and-signature } + +{{ tutorial.code_snippet_tagged('step-1') }} + +トランザクションを監視するには、署名後に生成されるハッシュが必要です。 +ハッシュは NEM ネットワーク上でトランザクションを一意に識別します。 + +スニペットでは、**署名者のアドレス** と **トランザクションの署名** も読み取ります。 +承認を検出するためにどちらも必要ではありませんが、[未承認トランザクションプール](default:未承認トランザクションプール) でトランザクションが承認を待っているか確認するために使います。 + +スニペットではサンプル値を使用します。 +`TRANSACTION_HASH`、`SIGNER_ADDRESS`、`TRANSACTION_SIGNATURE` 環境変数を設定すれば、値を上書きできます。 +3 つの値はすべて、[XEM を送信する](./transfer-xem.md) チュートリアルに示すように、トランザクションへの署名時に生成されます。 + +### 承認を確認する {: #checking-for-confirmation } + +{{ tutorial.code_snippet_tagged('step-2') }} + +{{ tutorial.var('get_confirmation_height') }} 関数は、トランザクションハッシュを指定して を照会し、トランザクションが承認されたか確認します。 + +トランザクションがブロックに含まれると、このエンドポイントは `meta.height` のブロックの高さとともに内容を返し、関数はその高さを返します。 + +それ以外の場合、エンドポイントは HTTP `400`(「Hash was not found in cache」)を返し、関数は高さを返しません。これはトランザクションが承認されていないことを意味します。 +承認されていないトランザクションは [未承認トランザクションプール](default:未承認トランザクションプール) で承認を待っている可能性があり、次の関数が確認します。 + +!!! warning "ハッシュ検索の有効期間は短いです" + + はデフォルトで 36 時間保持されるキャッシュから読み取ります。 + 検索はデフォルトで有効ですが、ノード運用者は無効化したり保持期間を変更したりできます。 + + 保持期間より古いトランザクションハッシュを照会すると、トランザクションが実際には承認済みでも HTTP `400` エラーが返ります。 + + そのため、この保持期間より後に検索する可能性のあるトランザクションをアナウンスするときは、ハッシュとともに承認ブロックの高さを保存してください。 + そうすれば、 エンドポイントでブロックからトランザクションを直接取得できます。 + + それ以外の場合は、 で署名者の完全な履歴をページングするか、ブロックチェーンをブロックごとに検索してトランザクションを見つける必要があります。 + +### 未承認プールを確認する {: #inspecting-the-unconfirmed-pool } + +{{ tutorial.code_snippet_tagged('step-3') }} + +{{ tutorial.var('is_in_unconfirmed_pool') }} は署名者のアドレスを使って を照会し、監視対象のトランザクションが承認待ちのリストに含まれているか報告します。 + +!!! note "無効なトランザクションはプールに入りません" + + [検証](../../textbook/transactions.md#3-validation) に失敗したトランザクションは未承認プールに入りません。受信ノードは、[XEM を送信する](./transfer-xem.md#announcing-the-transaction) チュートリアルに示すように、アナウンス時にすぐ拒否します。 + +上記のエンドポイントレスポンスには各エントリのハッシュがないため、関数は **署名** で照合します。 +トランザクションの署名は一意で、すべてのプールエントリの `transaction.signature` に含まれます。 +マルチシグトランザクションでは、内部トランザクションではなく、アナウンスされたラッパーの署名です。 + +関数は次を返します。 + +* {{ tutorial.lit('True') }}: トランザクションはまだ未承認プールにあり、ブロックに含まれるのを待っています。 +* {{ tutorial.lit('False') }}: トランザクションはレスポンスにありません。 + 署名者が使用したノードにまだ到着していない、すでに承認された、プールから削除された、レスポンスから除外されたなどの原因が考えられます。 + + !!! warning "レスポンスは 25 トランザクションに制限されます" + + エンドポイントは、アドレスに関係する最新 25 件までのトランザクションを返します。 + 受信トランザクションもこの上限に数えられるため、混雑したアカウントでは、監視対象のトランザクションが未承認プールに存在していてもレスポンスに含まれない可能性があります。 + +### 承認を待つ {: #waiting-for-confirmation } + +{{ tutorial.code_snippet_tagged('step-4') }} + +{{ tutorial.var('wait_for_confirmation') }} 関数は、トランザクションが承認されるか、2 分が経過する(タイムアウトは設定可能)まで、毎秒 {{ tutorial.var('get_confirmation_height') }} を呼び出します。 + +確認で承認が報告されると、関数はすぐ {{ tutorial.lit('True') }} を返します。 +試行回数を使い切ると、代わりに {{ tutorial.lit('False') }} を返します。 +これは、ポーリング期間内に承認されなかったことを意味するだけで、失敗したことを意味しません。ただし、このケースはまれです。 + +### すべてを組み合わせる {: #putting-it-all-together } + +{{ tutorial.code_snippet_tagged('step-5') }} + +スニペットは {{ tutorial.var('get_confirmation_height') }} を呼び出して、トランザクションがすでに承認されているか確認することから始めます。 + +!!! warning "承認済みトランザクションも取り消される可能性があります" + + 承認済みトランザクションはブロックに含まれていますが、まだ不可逆ではありません。 + 後続ブロックが [書き換え制限](default:書き換え制限) を超えるのに十分な数だけ追加されるまでは、[ロールバック](default:ロールバック) が発生する可能性があります。 + +トランザクションがまだブロックの一部でなければ、{{ tutorial.var('is_in_unconfirmed_pool') }} が未承認プールを検索します。 +承認済みでもプール内でもないトランザクションは、見つからないと報告されます。 + +トランザクションが未承認プールで待機している場合にだけ、スニペットは {{ tutorial.var('wait_for_confirmation') }} を呼び出し、承認されるかポーリング期間が終わるまで確認します。 + +!!! note "拒否または期限切れだけが失敗を意味します" + + トランザクションが決して承認されないと知る方法は 2 つだけです。アナウンス時に拒否されたか、**deadline** が過ぎた場合です。 + + あるノードの未承認プールからトランザクションが消えたからといって、失敗したとは限りません。 + 各ノードは独自のプールを管理しており、別のピアがまだ保持して最終的に承認する可能性があります。 + 例えば、ノードが空のプールで再起動したり、プール容量を管理するときに古いトランザクションを削除したりすることがあります。 + + アナウンスの拒否はすぐ返されるため、最終的な判定は **deadline** です。 + [ネットワーク時刻](default:ネットワーク時刻) が deadline を過ぎると、トランザクションをブロックに含めることはできなくなり、代わりのトランザクションをアナウンスしても安全です。 + + [トランザクションの構築](./transfer-xem.md#fetching-network-time) で選択した deadline と、 が返すネットワーク時刻を比較してください。 + +## 出力 {: #output } + +以下の出力は、新しくアナウンスしたトランザクションを監視した場合の実行例です。 + +```text linenums="1" hl_lines="2 4 10 12" +--8<-- 'devbook/transactions/monitoring_status.log' +``` + +出力の要点は次のとおりです。 + +* **トランザクションハッシュ**(2 行目): 監視するトランザクションのハッシュで、ネットワーク上でそのトランザクションを一意に識別します。 +* **ポーリング開始**(4 行目): トランザクションがまだブロックチェーンに存在せず、未承認プールで待機していることが見つかったため、ポーリングを開始します。 +* **ポーリングの試行**(5~9 行目): トランザクションがブロックに含まれるのを待つ間、各試行で `pending` と表示します。 +* **承認**(10 行目): 最終的にポーリングでブロック `652601` に含まれたことが報告されます。 +* **最終結果**(12 行目): トランザクションが承認され、監視が終了します。 + +試行回数と時間は、ネットワーク状態とブロック生成速度によって変わります。 + +ネットワーク側からトランザクションを確認するには、[NEM テストネットエクスプローラー](https://testnet.nem.fyi/) にアクセスしてトランザクションハッシュを検索してください。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [承認を確認する](#checking-for-confirmation) | | +| [未承認プールを確認する](#inspecting-the-unconfirmed-pool) | | +| [承認を待つ](#waiting-for-confirmation) | | +| [トランザクションが決して承認されない時点を検出する](#putting-it-all-together) | | + +## 次のステップ {: #next-steps } + +本番アプリケーションでは、次の改善を検討してください。 + +* **書き換え制限を過ぎるまで待つ。** 後続ブロックが十分に追加されるまで、承認済みトランザクションもロールバックされる可能性があります。 + 実用上のしきい値については [書き換え制限](default:書き換え制限) を参照してください。 +* **複数ノードを照会する。** 複数の [ノード](default:ノード) でステータスを確認し、信頼性を高めて単一ノードの問題からシステムを保護します。 +* **WebSocket を使う:** ポーリングを WebSocket サブスクリプションに置き換え、API を繰り返し呼び出さずにリアルタイムで更新します。 + [トランザクションフローをリッスンする](../websockets/listen-transaction-flow.md) WebSocket チュートリアルを参照してください。 diff --git a/mkdocs/pages/ja/devbook/transactions/sign-multisig.md b/mkdocs/pages/ja/devbook/transactions/sign-multisig.md new file mode 100644 index 000000000..2d04c70d3 --- /dev/null +++ b/mkdocs/pages/ja/devbook/transactions/sign-multisig.md @@ -0,0 +1,245 @@ +--- +title: マルチシグに署名する +tutorial_level: intermediate +--- + +# マルチシグアカウントからトランザクションに署名する + +このチュートリアルでは、1 [XEM](default:XEM) を、[アカウント](default:アカウント) からそのアカウント自身へ送信します。[XEM を送信する](../transactions/transfer-xem.md) チュートリアルと同じ流れです。 + +ただし今回は、送信元アカウントが [マルチシグアカウント](default:マルチシグアカウント)(_マルチシグ_ とも呼ばれます)なので、自分でトランザクションを開始したり署名したりすることができません。 +代わりに、連署人アカウントを使って、マルチシグアカウントに代わってトランザクションを作成し、署名します。 + +このチュートリアルで使うマルチシグアカウントは **2-of-2** マルチシグとして設定されています。 +連署人が 2 人いて、送金を承認するには両方の署名が必要です。 + +**連署人 0** が送金を開始し、**連署人 1** が必要な 2 つ目の連署を提供します。 + +```dot +digraph "Multisignature Tree" { + rankdir="BT"; + node [fontsize=12]; + "Multisignature Account" [label="マルチシグアカウント"]; + "Cosignatory 0" [label="連署人 0"]; + "Cosignatory 1" [label="連署人 1"]; + + "Cosignatory 0" -> "Multisignature Account"; + "Cosignatory 1" -> "Multisignature Account"; +} +``` + +!!! note "別の方法: WebSocket" + + このチュートリアルでは、連署人がノードを照会して承認待ちのトランザクションを見つけます。 + 連署人にリアルタイムで通知する WebSocket ベースの方法については、[マルチシグトランザクションフローをリッスンする](../websockets/listen-multisig-transaction-flow.md) チュートリアルを参照してください。 + +## 前提条件 {: #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) }} + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 + +* **2-of-2** マルチシグアカウントを作成する。 + 作成するには、[マルチシグを設定する](../accounts/configure-multisig.md#enabling-the-multisig) チュートリアルで {{ tutorial.var('min_approval_delta') }} を `1` から `2` に変更して実行します。 + アカウントがすでに別の設定のマルチシグである場合は、まず [無効化](../accounts/configure-multisig.md#disabling-the-multisig) してください。 + +さらに、トランザクションのアナウンスと承認の方法を理解するため、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルを確認してください。 + +## 完全なコード {: #full-code } + +{{ tutorial.code_full_tagged('devbook/transactions/sign_multisig', ['py', 'js']) }} + +## コードの説明 {: #code-explanation } + +マルチシグアカウントに代わってトランザクションに署名するには、 にラップし、必要な連署を集めます。 + +このチュートリアルでは、ラップされるトランザクションは送金で、資金の出所であるマルチシグアカウントを署名者とします。 +連署人 0 がラッパーに署名してアナウンスし、連署人 1 が 2 つ目の必要な連署をするまで、トランザクションは保留されます。 + +実際には、各連署人がそれぞれの秘密鍵だけを保持して、別のマシンで自分の処理を実行します。 +このチュートリアルでは簡単にするため、両方の役割を 1 つのプログラムにまとめています。 + +コードでは、トランザクションをアナウンスして承認を待つ 2 つのヘルパー関数を定義します。 +これらの動作の詳細については、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルを参照してください。 + +### アカウントをセットアップする {: #setting-up-the-accounts } + +{{ tutorial.code_snippet_tagged('step-1') }} + +このチュートリアルでは、環境変数で設定する 3 つのアカウントが必要です。 +設定されていない場合は、デフォルト値を使用します。 + +| 環境変数 | デフォルト値 | 用途 | +| --- | --- | --- | +| `MULTISIG_PUBLIC_KEY` | `D656..ACF2` | 2-of-2 マルチシグアカウント | +| `COSIGNATORY0_PRIVATE_KEY` | `0000..0002` | 1 人目の連署人アカウント、**開始者** | +| `COSIGNATORY1_PRIVATE_KEY` | `0000..0003` | 2 人目の連署人アカウント | + +各キーは 64 文字の 16 進数文字列です。 + +通常のアカウントとは異なり、マルチシグアカウントは自分でトランザクションを開始できません。 +代わりに、連署人がアカウントに代わって署名します。 +そのため、マルチシグアカウントの [秘密鍵](default:秘密鍵) は必要なく、アカウントの識別には [公開鍵](default:公開鍵) だけで十分です。 + +マルチシグアカウントには、トランザクション手数料を支払うために十分な資金が必要です。 +デフォルト値を使用する場合、このアカウントにはすでに資金がある可能性があります。 + +上記のスニペットでは、後で使用するために各連署人の [キーペア](default:キーペア) と、マルチシグアカウントの [アドレス](default:アドレス) を導出して保存します。 + +### ネットワーク時刻を取得する {: #fetching-network-time } + +{{ tutorial.code_snippet_tagged('step-2') }} + +ネットワーク時刻は から取得し、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルで説明されている手順に従って、トランザクションの `timestamp` と `deadline` フィールドを導出します。 + +### トランザクションを構築する {: #building-the-transaction } + +マルチシグトランザクションの内部にラップするトランザクションは [内部トランザクション](default:内部トランザクション) と呼ばれ、このチュートリアルで使う転送や、[マルチシグアカウントを設定する](../accounts/configure-multisig.md) で使う変更など、任意の [基本トランザクション](default:基本トランザクション) を指定できます。 +マルチシグトランザクションを入れ子にすることはできません。 + +{{ tutorial.code_snippet_tagged('step-3') }} + +内部の [転送トランザクション](default:転送トランザクション) には次のフィールドが含まれます。 + +* {{ tutorial.var('signer_public_key') }}: 資金を送るアカウント、つまりマルチシグアカウントの [公開鍵](default:公開鍵)。 + +* {{ tutorial.var('recipient_address') }}: この例では資金を送信者へ戻すため、受取人もマルチシグアカウントです。 + +* {{ tutorial.var('amount') }}: 1 [XEM](default:XEM) に相当する 1'000'000 原子単位。[XEM を送信する](../transactions/transfer-xem.md) チュートリアルで説明しています。 + +内部トランザクションには固有のトランザクション手数料があり、 で計算します。 +ここで送る 1 XEM の手数料は 0.05 XEM で、[送金手数料表](../../textbook/transfer_transactions.md#fees) に示されています。 + +{{ tutorial.code_snippet_tagged('step-4') }} + +次に転送トランザクションを にラップします。主なフィールドは次のとおりです。 + +* {{ tutorial.var('signer_public_key') }}: 今回は、トランザクションを開始する連署人の [公開鍵](default:公開鍵) です。 + +* {{ tutorial.var('inner_transaction') }}: ラップした転送トランザクション。 で変換し、独自の署名なしで埋め込めるようにします。 + +マルチシグラッパーにも 0.15 XEM の固有のトランザクション手数料があり、[手数料表](../../textbook/transactions.md#fee-schedule) に示されています。 +トランザクションが承認されると、すべての手数料と送金額がマルチシグアカウントから差し引かれます。 + +### 開始者: マルチシグトランザクションをアナウンスする {: #initiator-announcing-the-multisig-transaction } + +{{ tutorial.code_snippet_tagged('step-5') }} + +この場合、連署人 0 がマルチシグトランザクションの開始者です。 +トランザクションに署名してネットワークにアナウンスします。 + +有効であればネットワークはトランザクションを受け付けますが、まだ承認されていません。 +マルチシグアカウントには 2 つの連署が必要なのに対し 1 つしかないため、不足している連署が追加されるまでトランザクションは [未承認トランザクションプール](default:未承認トランザクションプール) で待機します。 + +!!! note "より簡単な設定" + + [マルチシグアカウントを設定する](../accounts/configure-multisig.md) チュートリアルで作成する 1-of-2 のように、連署が 1 つだけ必要なマルチシグでは、開始した連署人の署名だけで十分です。 + 有効であれば、追加の手順なしでトランザクションが承認されます。 + +### 連署人: 承認待ちのトランザクションを取得する {: #cosignatory-retrieving-the-pending-transaction } + +{{ tutorial.code_snippet_tagged('step-6') }} + +ここで連署人 1 が処理を引き継ぎます。 +連署人は エンドポイントを使って、署名を待っている承認待ちのマルチシグトランザクションを見つけられます。 + +承認待ちの各マルチシグトランザクションのメタデータには **内部トランザクション** のハッシュが含まれ、連署時に参照する値になります。 + +連署人は、承認待ちのマルチシグトランザクションを複数持つ可能性があります。 +この例では、マルチシグアカウントが発行したトランザクションを選択します。 +そのアカウントからの承認待ちトランザクションは 1 件だけであると想定し、連署人 1 はより正確に照合するための事前情報を持たないため、このチュートリアルにおいてはこの方法で十分です。 + +別の方法として、開始者がオフチェーンの経路で内部トランザクションのハッシュを他の連署人と共有できます。 +連署人はそのハッシュを照合して、正確なトランザクションを選択できます。 + +ただし、実際のアプリケーションでは、発行アカウントだけでの絞り込みは不十分です。 +承認待ちのトランザクションが期待したものだと保証するものはないため、連署するものを選ぶ前に、タイプ、受取人、金額など、承認待ちの各トランザクションの内容を確認してください。 + +!!! warning "連署する前に確認してください" + + 連署する前に、必ずトランザクションの内容を確認してください。 + 連署は拘束力があり、取り消すことはできません。 + +### 連署人: トランザクションに連署する {: #cosignatory-cosigning-the-transaction } + +{{ tutorial.code_snippet_tagged('step-7') }} + +連署人 1 は をアナウンスして、不足している署名を提供します。 +連署には次の内容を指定します。 + +* {{ tutorial.var('signer_public_key') }}: 署名を提供する連署人の [公開鍵](default:公開鍵)。 + +* {{ tutorial.var('other_transaction_hash') }}: 前の手順で取得した内部転送トランザクションのハッシュ。 + +* {{ tutorial.var('multisig_account_address') }}: 署名が対象とするマルチシグアカウントの [アドレス](default:アドレス)。 + +連署の手数料は 0.15 XEM です。 +マルチシグトランザクションが承認されると、この手数料もマルチシグアカウントから差し引かれます。 + +{{ tutorial.code_snippet_tagged('step-8') }} + +次に連署人 1 が連署に署名してネットワークにアナウンスします。 + +アナウンスされた連署は、独立したトランザクションとして [未承認トランザクションプール](default:未承認トランザクションプール) に表示されません。 +代わりに、ネットワークが承認待ちのマルチシグトランザクションへ付加します。 + +追加の連署が必要な設定では、トランザクションは保留されたままです。 +もう一度 を照会すれば、トランザクションの `signatures` フィールドで集まった署名を確認できます。 + +ただしこのチュートリアルでは、2 つ目の連署でトランザクションが完了します。トランザクションはプールを離れ、次のブロックで承認されます。 + +### 承認を待つ {: #waiting-for-confirmation } + +{{ tutorial.code_snippet_tagged('step-9') }} + +必要な連署がすべて集まると、マルチシグトランザクションは 1 つの単位として承認されます。 + +マルチシグトランザクションは、プロトコルの制約に違反すると拒否されます。 +次の表に、よくあるエラーの原因をまとめます。 + +| エラーメッセージ | 考えられる原因 | +| --- | --- | +| `FAILURE_TRANSACTION_NOT_ALLOWED_FOR_MULTISIG` | マルチシグアカウント自身が送金をアナウンスしようとした。 | +| `FAILURE_MULTISIG_NOT_A_COSIGNER` | マルチシグトランザクションの署名者が連署人リストにない。 | +| `FAILURE_MULTISIG_NO_MATCHING_MULTISIG` | 連署が承認待ちのマルチシグトランザクションと一致しない、または署名者が連署人ではない。 | + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text linenums="1" hl_lines="2-4 13 22 35 42 46 51" +--8<-- 'devbook/transactions/sign_multisig.log' +``` + +出力の要点は次のとおりです。 + +* **2~4 行目**: 関係するすべてのアカウントの公開鍵。 +* **13 行目**(`signer_public_key`): マルチシグトランザクションの署名者。 + 連署人 0 と一致することに注意してください。 +* **22 行目**(`signer_public_key`): 内部転送トランザクションの署名者。 + マルチシグアカウントと一致することに注意してください。 +* **35 行目**(`Inner transaction hash`): ネットワークから取得した承認待ちの内部トランザクションのハッシュ。 +* **42 行目**(`signer_public_key`): 連署の署名者。 + 連署人 1 と一致することに注意してください。 +* **46 行目**(`other_transaction_hash`): 連署が参照する内部トランザクションのハッシュ。 +* **51 行目**: ネットワーク上でマルチシグトランザクションを一意に識別するハッシュ。 + +出力に表示されたマルチシグトランザクションのハッシュを使って、[NEM テストネットエクスプローラー](https://testnet.nem.fyi/) で承認済みトランザクションを検索できます。 + +## まとめ {: #conclusion } + +このチュートリアルは、送信元アカウントとして [マルチシグアカウント](default:マルチシグアカウント) を使う点を除けば、[XEM を送信する](../transactions/transfer-xem.md) チュートリアルと機能的に同じです。 + +具体的には、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [送金をマルチシグトランザクションでラップする](#building-the-transaction) | | +| [マルチシグトランザクションに署名する](#initiator-announcing-the-multisig-transaction) | | +| [承認待ちのトランザクションを見つける](#cosignatory-retrieving-the-pending-transaction) | | +| [承認待ちのマルチシグトランザクションに連署する](#cosignatory-cosigning-the-transaction) | | diff --git a/mkdocs/pages/ja/devbook/transactions/transfer-mosaics.md b/mkdocs/pages/ja/devbook/transactions/transfer-mosaics.md new file mode 100644 index 000000000..c1471fad7 --- /dev/null +++ b/mkdocs/pages/ja/devbook/transactions/transfer-mosaics.md @@ -0,0 +1,181 @@ +--- +title: モザイクを転送する +tutorial_level: intermediate +--- + +# 転送トランザクションでモザイクを送信する + +[転送トランザクション](default:転送トランザクション) には、[XEM](default:XEM) の代わりに、または XEM と一緒に、他の [モザイク](default:モザイク) を含められます。 + +このチュートリアルでは、通常の [XEM 送信](./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"]; +} +``` + +この例では、テストネットに存在する `company:token` モザイクを 100 単位送信します。すでにいくらか保有しているデフォルトのテストアカウントを使います。 +署名アカウントが十分な量を所有していれば、他のモザイクでも同じ流れを使えます。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* [開発環境をセットアップ](../start/setup.md) する。 +* 転送トランザクションを送信する [アカウント](default:アカウント) を、[コード](../accounts/create-from-private-key.md) または [ウォレット](../../userbook/wallet/create-account.md) を使って作成する。 +* トランザクション手数料と転送額を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](../accounts/testnet-faucet.md) を参照してください。 +* 送信する [モザイク](default:モザイク) を十分に保有する。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/transactions/transfer_mosaics', ['py', 'js']) }} + +## コードの説明 {: #code-explanation } + +署名、アナウンス、承認待ちは [XEM を送信する](./transfer-xem.md) チュートリアルと同じなので、ここでは説明しません。 +以下では、異なる手順だけを説明します。 + +### アカウントをセットアップする {: #setting-up-the-accounts } + +{{ tutorial.code_snippet_tagged('step-1') }} + +すべてのモザイク転送には、**送信者** と **受取人** の 2 つのアカウントが関係します。 + +**送信者** は、トランザクションに署名して手数料を支払い、送信するモザイクを保有する [アカウント](default:アカウント) です。 +秘密鍵は `SIGNER_PRIVATE_KEY` 環境変数から読み込みます。 +指定されていない場合は、テスト用のキーをデフォルト値として使います。 + +**受取人** はモザイクを受け取るアカウントです。 +その [アドレス](default:アドレス) は `RECIPIENT_ADDRESS` 環境変数から読み込みます。 +指定されていない場合は、テスト用のアドレスをデフォルト値として使います。 + +デフォルトのテストアカウントには `company:token` があらかじめ資金提供されていますが、残高はチュートリアルの実行間で共有され、使い切る可能性があります。 +残高不足で転送が失敗する場合は、`SIGNER_PRIVATE_KEY` をモザイクを保有するアカウントに設定してください。 + +### モザイクをセットアップする {: #setting-up-the-mosaic } + +{{ tutorial.code_snippet_tagged('step-2') }} + +送信するモザイクは `MOSAIC_ID` で指定します。これは `:` 形式の [完全修飾名](../../textbook/mosaics.md#fully-qualified-name) で、デフォルト値は `company:token` です。 +設定すれば、署名アカウントが所有する他のモザイクをチュートリアルで指定できます。 + +`QUANTITY` は、送信するモザイクの量を [全体単位](../../textbook/mosaics.md#divisibility) で指定する値で、デフォルトは 100 です。 + +### ネットワーク時刻を取得する {: #fetching-network-time } + +{{ tutorial.code_snippet_tagged('step-3') }} + +NEM のすべてのトランザクションには、作成時点を示す `timestamp` と、ネットワークが承認を試み続ける期間を示す `deadline` が必要です。どちらも [ネットワーク時刻](default:ネットワーク時刻) で表します。 + +スニペットは から現在のネットワーク時刻を取得し、`timestamp` に設定して、2 時間後を `deadline` に設定します。 + +エンドポイントは時刻をミリ秒で返すため、コードでは 1000 で割って、トランザクションが必要とする秒数を取得します。 + +キャッシュ戦略を含む詳細については、XEM 送信の [ネットワーク時刻を取得する](./transfer-xem.md#fetching-network-time) を参照してください。 + +### モザイク定義と供給量を取得する {: #fetching-the-mosaic-definition-and-supply } + +{{ tutorial.code_snippet_tagged('step-4') }} + +モザイクの [可分性](default:可分性) を使って転送量を [原子単位](../../textbook/mosaics.md#divisibility) に変換し、現在の供給量とともに手数料を決定します。 +トランザクションを構築する前に、両方をノードから取得します。 + +* エンドポイントは、`divisibility` などのプロパティを含むモザイク定義を返します。 +* エンドポイントは、現在の総供給量を [全体単位](../../textbook/mosaics.md#divisibility) で返します。 + +ネットワーク時刻と同じく、転送ごとにこれらの値を取得する必要はありません。 +モザイクの可分性は作成時に固定され、供給量は供給量を可変として作成された場合だけ変化するため、アプリケーションは両方を一度取得してキャッシュし、必要に応じて供給量を更新できます。 + +### トランザクションを構築する {: #building-the-transaction } + +{{ tutorial.code_snippet_tagged('step-5') }} + +スニペットはまず、前の手順で取得した可分性を使って、`QUANTITY` を全体単位から原子単位へ変換します。 + +!!! note "全体単位から原子単位へ" + + 作成時に設定され 0~6 の範囲を取るモザイクの `divisibility` は、次の変換を定義します。 + 1 全体単位は 10^divisibility^ [原子単位](../../textbook/mosaics.md#divisibility) に相当します。 + + ここで使う `company:token` モザイクの可分性は 0 なので、10^0^ = 1 であり、`QUANTITY` 100 は 100 原子単位としてエンコードされます。 + + 一方、可分性が 2 のモザイクでは、1 全体単位あたり 10^2^ = 100 原子単位です。そのため同じ `QUANTITY` 100 は 10'000 原子単位としてエンコードされます。 + +次にスニペットは、追加の `mosaics` フィールドを持つ ディスクリプタを使って を呼び出します。`mosaics` フィールドには最大 10 件を指定できます。 +各エントリでは、モザイクと送信量を指定します。 + +* モザイクを所有する [ネームスペース](default:ネームスペース)。 +* モザイク名。 +* モザイクの **原子単位** で指定する数量(上で計算済み)。 + +モザイクを付加すると、最上位の `amount` は [XEM の金額](../../textbook/transfer_transactions.md#xem-amount) ではなくなります。 +列挙したすべてのモザイクに適用する乗数になり、`1_000_000` は係数 1 を表します。 +`amount` を `1_000_000` に設定すると、各モザイクをエントリに指定した数量で送信します。 + +!!! info "他のモザイクと一緒に XEM を送信する" + + 最上位の `amount` は XEM を送るのではなく、乗数として機能します。 + 別のモザイクと同時に XEM を送るには、`mosaics` 配列に `nem:xem` エントリを追加します。 + その数量は、送信する XEM の原子単位で指定します。 + +### トランザクション手数料を計算する {: #calculating-the-transaction-fee } + +{{ tutorial.code_snippet_tagged('step-6') }} + +モザイク転送の手数料は、各モザイクの供給量、可分性、転送する数量によって決まります。 +NEM の固定手数料表を手動で実装する代わりに、スニペットは SDK の ヘルパーを呼び出します。 + +ヘルパーは前の手順で構築したトランザクションから転送数量を直接読み取り、トランザクションに保存されない値なので、各モザイクの `supply`([全体単位](../../textbook/mosaics.md#divisibility))と `divisibility` を 2 つ目の引数として受け取ります。 + +返された手数料は、署名前に `transaction.fee` へ割り当てます。 + +計算の完全なルールについては、[手数料](../../textbook/transfer_transactions.md#fees) セクションを参照してください。 + +### 署名、アナウンス、承認待ち {: #signing-announcing-and-waiting-for-confirmation } + +これらの手順は [XEM を送信する](./transfer-xem.md) 場合と同じなので、ここでは詳しく説明しません。 + +!!! warning "徴収手数料付きモザイク" + + 一部のモザイクには [徴収手数料](default:徴収手数料(levy)) があり、そのモザイクを転送するたびに、トランザクション手数料に加えて第三者アカウントへ追加手数料が支払われます。 + 送信者のアカウントは、徴収手数料用モザイク(転送するモザイクとは異なる場合があります)を支払えるだけ保有していなければ、トランザクションが拒否されます。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text linenums="1" hl_lines="8 18 21 22-29" +--8<-- 'devbook/transactions/transfer_mosaics.log' +``` + +通常の XEM 転送と異なる部分に焦点を当てた、出力の要点は次のとおりです。 + +* **定義と供給量**(8 行目): 可分性(`0`)と現在の供給量(`1000000`)を持つ `company:token` モザイク。数量を原子単位へ変換し、手数料を計算するために取得します。 +* **トランザクション手数料**(18 行目): モザイクの供給量、可分性、転送量から導出した `350000` 原子単位(`0.35` XEM)。 +* **乗数としての amount**(21 行目): モザイクを付加すると、`amount` は XEM の金額ではなく `1000000`(係数 1)になります。 +* **モザイク配列**(22~29 行目): 転送に含まれるモザイク。ここでは数量 `100` のエントリ 1 件です。 + ネームスペースと名前はアドレスと同じように 16 進数でエンコードされるため、`636F6D70616E79` と `746F6B656E` は `company` と `token` にデコードされます。 + +ネットワーク側からトランザクションを確認するには、[NEM テストネットエクスプローラー](https://testnet.nem.fyi/) でトランザクションハッシュを検索できます。 +ハッシュは `Waiting for confirmation from /transaction/get?hash=...` と表示される行に出力されます。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [モザイクの可分性を取得する](#fetching-the-mosaic-definition-and-supply) | | +| [モザイクの供給量を取得する](#fetching-the-mosaic-definition-and-supply) | | +| [モザイク転送を構築する](#building-the-transaction) | | +| [トランザクション手数料を計算する](#calculating-the-transaction-fee) | | diff --git a/mkdocs/pages/ja/devbook/transactions/transfer-xem.md b/mkdocs/pages/ja/devbook/transactions/transfer-xem.md new file mode 100644 index 000000000..2e9353aa2 --- /dev/null +++ b/mkdocs/pages/ja/devbook/transactions/transfer-xem.md @@ -0,0 +1,207 @@ +--- +title: XEM を送信する +tutorial_level: beginner +--- + +# 転送トランザクションで XEM を送信する + +ある [アカウント](default:アカウント) から別のアカウントへ [XEM](default:XEM) を送信することは、NEM ブロックチェーンで最も基本的な操作です。他の種類の [トランザクション](default:トランザクション) も、同じ一般的なパターンに従います。 + +```dot +digraph "Transfer XEM" { + rankdir="LR"; + node [fontsize=12]; + + A [label="A"]; + B [label="B"]; + + A -> B [label="1 XEM"]; +} +``` + +このチュートリアルでは、2 つのアカウント間で 1 XEM を送信する [転送トランザクション](default:転送トランザクション) を作成、署名、アナウンスし、承認されるまでトランザクションのステータスをポーリングする方法を説明します。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* [開発環境をセットアップ](../start/setup.md) する。 +* 転送トランザクションを送信する [アカウント](default:アカウント) を、[コード](../accounts/create-from-private-key.md) または [ウォレット](../../userbook/wallet/create-account.md) を使って作成する。 +* トランザクション手数料と転送額を支払うための [XEM](default:XEM) を用意する。 + [フォーセットからテストネットの資金を取得する](../accounts/testnet-faucet.md) を参照してください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/transactions/transfer_xem', ['py', 'js']) }} + +コード全体を 1 つの `try` ブロックでラップして簡単なエラー処理を行いますが、アプリケーションではより細かな制御が必要になる場合があります。 + +## コードの説明 {: #code-explanation } + +### アカウントをセットアップする {: #setting-up-the-accounts } + +{{ tutorial.code_snippet_tagged('step-1') }} + +すべての転送トランザクションには、**送信者** と **受取人** の 2 つのアカウントが関係します。 + +**送信者** は、トランザクションに署名して手数料を支払う [アカウント](default:アカウント) です。 +秘密鍵は `SIGNER_PRIVATE_KEY` 環境変数から読み込みます。 +指定されていない場合は、テスト用のキーをデフォルト値として使います。 + +**受取人** は XEM を受け取るアカウントです。 +その [アドレス](default:アドレス) は `RECIPIENT_ADDRESS` 環境変数から読み込みます。 +指定されていない場合は、テスト用のアドレスをデフォルト値として使います。 + +### 送金額を定義する {: #defining-the-transfer-amount } + +{{ tutorial.code_snippet_tagged('step-2') }} + +スニペットでは、`XEM_AMOUNT` 環境変数から数値として読み込んだ送金額を、`xem` 変数に定義します。 +指定されていない場合は、デフォルト値として 1 XEM を使います。 + +トランザクションの `amount` フィールドには、XEM 単位ではなく [原子単位](../../textbook/mosaics.md#divisibility) が必要です。 +XEM の [可分性](default:可分性) は 6 なので、1 XEM は 100 万原子単位です。 +スニペットでは `xem` に 1'000'000 を掛けて `amount` を導出します。 + +### ネットワーク時刻を取得する {: #fetching-network-time } + +{{ tutorial.code_snippet_tagged('step-3') }} + +NEM のすべてのトランザクションには 2 つの時刻フィールドがあり、どちらも [ネットワーク時刻](default:ネットワーク時刻) で表します。これは NEM のネメシスブロックからの経過秒数です。 + +* `timestamp`: トランザクションが作成された時点。ここでは現在のネットワーク時刻を設定します。 +* `deadline`: トランザクションを破棄する前に、ネットワークが承認を試み続ける期間。 + タイムスタンプより後の時間で、[24 時間](../../textbook/transactions.md#common-transaction-structure) 以内でなければなりません。 + 範囲外の時間を指定した場合、ノードはトランザクションを拒否します。 + この例では、範囲内であるタイムスタンプの 2 時間後に設定します。 + +送金のトランザクションを構築するには正確なネットワーク時刻が必要です。 + エンドポイントは、ノードの現在のネットワーク時刻を返します。 +ノードはこの値をミリ秒で返すため、コードでは 1000 で割って、トランザクションが必要とする秒数を取得します。 + +ただし、アプリケーションがトランザクションごとにネットワーク時刻を照会する必要はありません。 +一度取得した後、必要に応じてローカルシステムの時計を使って調整できます。 +これにより、正確さと性能のバランスが取れます。 + +コードは秒数を SDK の クラスでラップして `timestamp` を取得し、 ヘルパーで `deadline` を導出します。 + +### トランザクションを構築する {: #building-the-transaction } + +{{ tutorial.code_snippet_tagged('step-4') }} + +スニペットは、転送トランザクションのプロパティを指定するディスクリプタを使って を呼び出します。 + +* {{ tutorial.var('type') }}: このチュートリアルでは、現在の送金バージョンである を使用します。XEM と他の [モザイク](default:モザイク) の両方を送信できます。 + ここではモザイクを付加しないため、トランザクションは XEM だけを送信します。 + +* {{ tutorial.var('signer_public_key') }}: 署名者は手数料を支払うアカウントです。 + 転送トランザクションでは、送られる XEM の送信元でもあります。 + +* {{ tutorial.var('timestamp') }} と {{ tutorial.var('deadline') }}: ネットワーク時刻の手順で計算した値。 + +* {{ tutorial.var('recipient_address') }}: XEM を受け取るアドレス。 + +* {{ tutorial.var('amount') }}: 前の手順で計算した原子単位の数量。1 XEM の場合は `1_000_000` です。 + +!!! info "モザイクまたはメッセージを送信する" + + は、XEM の代わりに他の [モザイク](default:モザイク) を送信したり、メッセージを含めたりできます。それぞれの場合で手数料の計算方法は異なります。 + 詳細については、[モザイクを送信する](./transfer-mosaics.md) と [メッセージ付きで送信する](./messages.md) チュートリアルを参照してください。 + +### トランザクション手数料を計算する {: #calculating-the-transaction-fee } + +{{ tutorial.code_snippet_tagged('step-5') }} + +すべてのトランザクションは、ブロックに含める [ハーベスターアカウント](default:ハーベスターアカウント) に手数料を支払います。 + +NEM の [固定手数料表](../../textbook/transfer_transactions.md#fees) を手動で実装する代わりに、スニペットは SDK の ヘルパーを呼び出します。このヘルパーは、前の手順で構築したトランザクションから XEM の金額を直接読み取ります。 + +返された手数料は、署名前に `transaction.fee` へ割り当てます。 +手数料は少額の場合 0.05 XEM から始まり、送信する XEM に応じて増加し、最大 1.25 XEM で上限になります。 + +### 署名してシリアライズする {: #signing-and-serializing } + +{{ tutorial.code_snippet_tagged('step-6') }} + +トランザクションを作成したら、署名アカウントの秘密鍵で署名する必要があります。 +署名により、トランザクションが本物であり、送信者によって承認されたことを保証します。 + + は [署名](default:署名) を返します。 + は署名をトランザクションに追加し、ノードへ直接送信してアナウンスできる JSON ペイロードにシリアライズします。 + +### トランザクションをアナウンスする {: #announcing-the-transaction } + +{{ tutorial.code_snippet_tagged('step-7') }} + +署名済みペイロードは、任意の NEM [ノード](default:ノード) の エンドポイントに送信します。 + +ノードはアナウンスされるとすぐにトランザクションを検証し、結果をレスポンスで返します。 +`SUCCESS` はトランザクションが最初のチェックに合格し、[未承認トランザクションプール](default:未承認トランザクションプール) に追加されたことを意味します。 +それ以外の結果はノードが受け付けなかったことを意味し、例えばアカウントが金額と手数料をカバーできる XEM を保有していないなど、その理由をレスポンスメッセージで説明します。 + +!!! warning "未承認トランザクションを信頼しないでください" + + `SUCCESS` の結果は、トランザクションが未承認プールに到達したことだけを意味します。 + ブロックに含まれることはまだ保証されていません。 + [承認](#waiting-for-confirmation) されるまで、できれば [書き換え制限](default:書き換え制限) を過ぎるまで待ってから信頼してください。 + +### 承認を待つ {: #waiting-for-confirmation } + +{{ tutorial.code_snippet_tagged('step-8') }} + +上記のスニペットは、アナウンスしたトランザクションのハッシュを使って エンドポイントを繰り返し照会します。 + +!!! note "ポーリングと WebSocket" + + この手順ではポーリングでトランザクションが承認されたか確認します。 + ポーリングは説明のために使用していますが、実際のアプリケーションに推奨される方法ではありません。 + + [WebSocket](../websockets/listen-transaction-flow.md) を使えば、API を繰り返し呼び出すオーバーヘッドなしに、より応答性の高い解決策を実現できます。 + +トランザクションが未承認の間、エンドポイントはエラーを返します。コードは 1 秒待ってから再試行し、最大 120 回(約 2 分)繰り返します。 + +トランザクションがブロックに含まれると、エンドポイントはブロックの高さとともにトランザクションを返し、ループは終了します。 + +NEM はおよそ 1 分に 1 ブロックを生成するため、通常、承認には数秒から数分かかります。 + +## 出力 {: #output } + +以下は、プログラムの実行時の出力例です。 + +```text linenums="1" hl_lines="11 13 15 16 17 20 21 34" +--8<-- 'devbook/transactions/transfer_xem.log' +``` + +出力の要点は次のとおりです。 + +* **署名者の公開鍵**(11 行目): トランザクションに署名して XEM を送るアカウント。 +* **トランザクション手数料**(13 行目): `50000` 原子単位(`0.05` XEM)。デフォルトの 1 XEM を送る手数料です。 +* **受取人アドレス**(15 行目): XEM を受け取るアカウント。 + これは同じ `RECIPIENT_ADDRESS` ですが、NEM のトランザクション形式では Base32 テキストの各文字を 16 進数の ASCII コードとしてエンコードするため、見た目が異なります。そのため `5442...` は `TBUL...` にデコードされます(`54` は `T`、`42` は `B` など)。 +* **転送額**(16 行目): `1000000` 原子単位で、1 XEM に相当します。 +* **モザイクなし**(17 行目): モザイク配列が空なので、トランザクションは XEM だけを送信します。 +* **アナウンス結果**(20 行目): `SUCCESS` はノードがトランザクションを未承認プールに受け入れたことを意味します。 +* **トランザクションハッシュ**(21 行目): ネットワーク上でトランザクションを一意に識別するハッシュ。 +* **承認**(34 行目): トランザクションがブロック `626588` に含まれています。 + +`pending` のチェック回数は、次のブロックがハーベスティングされるまでの時間によって変わるため、実行ごとに異なります。 + +ネットワーク側からトランザクションを確認するには、[NEM テストネットエクスプローラー](https://testnet.nem.fyi/) でトランザクションハッシュを検索できます。 +ハッシュは `Waiting for confirmation from /transaction/get?hash=...` と表示される行に出力されます。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [ネットワーク時刻を取得する](#fetching-network-time) | | +| [トランザクションを構築する](#building-the-transaction) | | +| [トランザクション手数料を計算する](#calculating-the-transaction-fee) | | +| [トランザクションに署名する](#signing-and-serializing) |
| +| [トランザクションをアナウンスする](#announcing-the-transaction) | | +| [承認を待つ](#waiting-for-confirmation) | | + +他のほとんどの NEM トランザクションタイプも、同じ方法で作成、署名、アナウンスします。 diff --git a/mkdocs/pages/ja/devbook/transactions/typed-descriptors.md b/mkdocs/pages/ja/devbook/transactions/typed-descriptors.md new file mode 100644 index 000000000..60cf596b2 --- /dev/null +++ b/mkdocs/pages/ja/devbook/transactions/typed-descriptors.md @@ -0,0 +1,58 @@ +--- +title: 型付きディスクリプタ +tutorial_level: beginner +--- + +# JavaScript で型付きディスクリプタを使ってトランザクションを作成する + +{% import 'tutorial.jinja2' as tutorial with context %} +{{ tutorial.code_full_tagged('devbook/transactions/transfer_xem.typed', ['js'], show=false) }} + +トランザクションは NEM ブロックチェーンの基本的な要素です。ネットワークとのやり取りの多くがトランザクションを通じて行われるためです。 + +チュートリアル全体の JavaScript 例では、簡潔な構文でトランザクションを作成できる メソッドを使用しています。 +しかし、このメソッドは型安全ではありません。汎用オブジェクトを受け取り、正しいフィールドが含まれていることを前提とします。 + +このページでは、代わりに を使う方法を説明します。 +この方法は定義済みのパラメーターを受け取るため、型安全性が高く、IDE のサポートも向上します。 + +ここで示すコードは、[転送トランザクションを作成する](./transfer-xem.md) チュートリアルと同じですが、トランザクションの作成手順だけが異なります。 +簡潔にするため、そのセクションだけを示します。署名やアナウンスを含む残りの手順は変わりません。 + +{{ tutorial.code_snippet_tagged('step-1') }} + +[完全なチュートリアルコードをダウンロードする。]({{ config.repo_url }}/raw/refs/heads/{{config.extra.nem.branch}}/mkdocs/snippets/devbook/transactions/transfer_xem.typed.mjs){ .source-link } + +## 作成手順 {: #creation-process } + +トランザクションは、型安全な方法で次の 2 段階により作成します。トランザクションディスクリプタを作成し、そのディスクリプタからトランザクション自体を作成します。 + +### ディスクリプタを作成する {: #creating-the-descriptor } + +{{ tutorial.code_snippet_tagged('step-2') }} + +JavaScript でトランザクションを構築するとき、構造化されたパラメーターを持つコンストラクターによって型安全性を提供するのが型付きディスクリプタです。 + +例えば、コードで使われている を参照してください。 + +### トランザクションを作成する {: #creating-the-transaction } + +{{ tutorial.code_snippet_tagged('step-3') }} + +ディスクリプタの準備ができたら、トランザクションの作成は簡単です。ディスクリプタを メソッドに渡し、必要な手数料と期限を指定します。 + +[転送トランザクションを作成する](./transfer-xem.md#calculating-the-transaction-fee) チュートリアルと同じく、トランザクションの内容によって手数料が決まるため、構築後に手数料を計算する必要があります。 + +!!! warning "型付き版と型なし版では期限の指定方法が異なります" + + に渡す期限は秒数で指定し、_ネットワーク時刻_ を基準にします。 + 一方、 に渡す期限も秒数で指定しますが、_システム時刻_、つまりコードを実行するマシンのローカル時計を基準にします。 + + この方法は、現在のネットワーク時刻を取得する必要がないため便利です。例えばトランザクションを 2 時間後に期限切れにするには、上のコードのように `#!js 2 * 60 * 60` 秒の期限を指定するだけです。 + + ただし、システム時計がネットワーク時刻と正しく同期していない場合、想定より早くトランザクションが期限切れになったり、指定した期限がネットワークの許可する最大オフセットである 24 時間を超えて完全に拒否されたりする可能性があります。 + + **したがって、型安全なメソッドを使うアプリケーションでは、システム時計が正しく同期していることを確認するため、定期的にネットワーク時刻を確認する必要があります。** + +トランザクションを作成したら、通常どおり使用できます。 +型付きメソッドで作成したトランザクションと型なしメソッドで作成したトランザクションに違いはありません。 diff --git a/mkdocs/pages/ja/devbook/websockets/listen-multisig-transaction-flow.md b/mkdocs/pages/ja/devbook/websockets/listen-multisig-transaction-flow.md new file mode 100644 index 000000000..924c8ad48 --- /dev/null +++ b/mkdocs/pages/ja/devbook/websockets/listen-multisig-transaction-flow.md @@ -0,0 +1,264 @@ +--- +title: マルチシグトランザクションフロー +tutorial_level: advanced +--- + +# マルチシグトランザクションフローをリッスンする + +[マルチシグアカウント](default:マルチシグアカウント) からのトランザクションは、通常のトランザクションより複雑なライフサイクルをたどります。 +アナウンスされた後、ネットワークがアカウントの連署人から必要な連署を集める間、[未承認トランザクションプール](default:未承認トランザクションプール) で待機します。 +すべての連署が届いた後で初めて、トランザクションは [ブロック](default:ブロック) で承認されます。 + +このチュートリアルでは、[マルチシグアカウントからトランザクションに署名する](../transactions/sign-multisig.md) チュートリアルの送金を再現しますが、ポーリングの代わりに [WebSocket](../reference/websockets/index.md) チャネルを使って、マルチシグのライフサイクル全体を監視します。 + +このチュートリアルで使うマルチシグアカウントは **2-of-2** マルチシグとして設定されています。 +連署人が 2 人いて、送金を承認するには両方の署名が必要です。 + +```dot +digraph "Multisignature Tree" { + rankdir="BT"; + node [fontsize=12]; + "Multisignature Account" [label="マルチシグアカウント"]; + "Cosignatory 0" [label="連署人 0"]; + "Cosignatory 1" [label="連署人 1"]; + + "Cosignatory 0" -> "Multisignature Account"; + "Cosignatory 1" -> "Multisignature Account"; +} +``` + +連署人 0 がマルチシグトランザクションを構築してアナウンスし、連署人 1 がマルチシグアカウントの WebSocket チャネルをサブスクライブして連署し、承認を待ちます。 + +!!! note "別の方法: ポーリング" + + 連署人がノードを照会して承認待ちのトランザクションを見つけるポーリングの方法については、[マルチシグアカウントからトランザクションに署名する](../transactions/sign-multisig.md) チュートリアルを参照してください。 + +## 前提条件 {: #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) }} + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 + +* **2-of-2** マルチシグアカウントを作成する。 + 作成するには、[マルチシグを設定する](../accounts/configure-multisig.md#enabling-the-multisig) チュートリアルで {{ tutorial.var('min_approval_delta') }} を `1` から `2` に変更して実行します。 + アカウントがすでに別の設定のマルチシグである場合は、まず [無効化](../accounts/configure-multisig.md#disabling-the-multisig) してください。 + +さらに、NEM は [SockJS](https://github.com/sockjs/sockjs-client) 上で [STOMP](https://stomp.github.io/) メッセージングプロトコルを使って WebSocket を提供するため、STOMP クライアントと WebSocket トランスポートが必要です。 + +=== ":simple-python: Python" + + `stomper` と `websockets` ライブラリをインストールします。 + + ```bash + pip install stomper websockets + ``` + +=== ":simple-javascript: JavaScript" + + `@stomp/stompjs` と `sockjs-client` ライブラリをインストールします。 + + ```bash + npm install @stomp/stompjs sockjs-client + ``` + +接続プロトコルの詳細については、[WebSocket リファレンス](../reference/websockets/index.md) を参照してください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/websockets/listen_multisig_transaction_flow', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM [ノード](default:ノード) を指定します。 +値が指定されていない場合は、デフォルト値を使用します。 + +`WS_URL` は同じノードの WebSocket エンドポイントを定義します。 +`NODE_URL` のポート `7890`(デフォルトの HTTP API ポート)を `7778`(デフォルトの NIS WebSocket ポート)に置き換えて導出します。 + +!!! note "Python の SockJS ヘルパー" + + Python 用の SockJS クライアントライブラリはないため、便宜上、ファイルの先頭に小さなヘルパーメソッドをいくつか定義しています。 + +## コードの説明 {: #code-explanation } + +マルチシグトランザクションには 2 つの異なる役割があります。マルチシグトランザクションを構築、署名、アナウンスする **開始者**(連署人 0)と、WebSocket チャネルを監視してトランザクションを確認した後に連署する 1 人以上の **連署人**(このチュートリアルでは連署人 1)です。 +マルチシグアカウントの連署人なら、どちらの役割も担当できます。 + +実際には、各役割を別々のマシン上の別々のプログラムで実行し、それぞれ自分の秘密鍵だけを保持します。 +このチュートリアルでは簡単にするため、両方の役割を 1 つのスクリプトにまとめています。 + +### アカウントをセットアップする {: #setting-up-the-accounts } + +{{ tutorial.code_snippet_tagged('step-1') }} + +このチュートリアルでは、環境変数で設定する別々の 3 アカウントが必要です。 +設定されていない場合は、デフォルト値を使用します。 + +| 環境変数 | デフォルト値 | 用途 | +| --- | --- | --- | +| `MULTISIG_PUBLIC_KEY` | `D656..ACF2` | 2-of-2 マルチシグアカウント | +| `COSIGNATORY0_PRIVATE_KEY` | `0000..0002` | 1 人目の連署人、**開始者** | +| `COSIGNATORY1_PRIVATE_KEY` | `0000..0003` | 2 人目の連署人 | + +各キーは 64 文字の 16 進数文字列です。 + +通常のアカウントとは異なり、マルチシグアカウントは自分でトランザクションを開始できません。 +代わりに、連署人がアカウントに代わって署名します。 +そのため、マルチシグアカウントの [秘密鍵](default:秘密鍵) は必要なく、アカウントの識別には [公開鍵](default:公開鍵) だけで十分です。 + +マルチシグアカウントには、トランザクション手数料を支払うために十分な資金が必要です。 +デフォルト値を使用する場合、このアカウントにはすでに資金がある可能性があります。 + +上記のスニペットでは、後で使用するために各連署人の [キーペア](default:キーペア) とマルチシグアカウントの [アドレス](default:アドレス) を導出して保存します。 +後でサブスクライブする WebSocket チャネルは、このアドレスに対応付けられます。 + +### 開始者: マルチシグトランザクションを構築する {: #initiator-building-the-multisig-transaction } + +{{ tutorial.code_snippet_tagged('step-2') }} + +連署人 0 はネットワーク時刻を取得し、マルチシグアカウントから自身へ 1 [XEM](default:XEM) を送る [内部トランザクション](default:内部トランザクション) を構築し、 にラップして署名します。 +実装は、[マルチシグアカウントからトランザクションに署名する](../transactions/sign-multisig.md#building-the-transaction) チュートリアルで説明したパターンに従います。 + +トランザクションは準備されますが、まだ [アナウンス](#initiator-announcing-the-multisig-transaction) されません。 +チャネルのサブスクリプションを確立した後でアナウンスするため、結果の通知を取り逃しません。 + +### 連署人: WebSocket に接続する {: #cosignatory-connecting-to-the-websocket } + +{{ tutorial.code_snippet_tagged('step-3') }} + +連署人 1 は `WS_URL` の `/w/messages` エンドポイントへ SockJS 接続を開き、その上で [STOMP セッション]() を開始します。 + +### 連署人: チャネルをサブスクライブする {: #cosignatory-subscribing-to-the-channels } + +{{ tutorial.code_snippet_tagged('step-4') }} + +連署人 1 は、[トランザクションフローをリッスンする](./listen-transaction-flow.md) チュートリアルで使った、アドレスに対応付けられた同じ 3 チャネルをサブスクライブします。 + +* : アドレスを含む [ブロック](default:ブロック) が承認されると、アカウントの現在の状態を通知します。 +* : アドレスに関係するトランザクションが [未承認トランザクションプール](default:未承認トランザクションプール) に入り、ブロックへの包含を待つ状態になると通知します。 +* : アドレスに関係するトランザクションが [ブロック](default:ブロック) に含まれると通知します。 + +サブスクリプションには `id-0`、`id-1`、`id-2` を使います。プログラムが最後にサブスクライブを解除するときに、それぞれを識別します。 + +マルチシグではない場合と異なり、チャネルは **連署人アカウント** ではなく **マルチシグアカウント** の活動を監視します。 + +ノードが通知するのは、開始した連署人と内部トランザクションに関係するアカウントだけだからです。この例では連署人 0 とマルチシグアカウントです。 +連署人 1 のように承認を待つ連署人は、自分のアドレスでは通知を受け取らないため、代わりにマルチシグアカウントのアドレスをサブスクライブする必要があります。 + +!!! note "メッセージ処理の違い" + + JavaScript では、各チャネルを専用のハンドラー関数でサブスクライブします。関数は下の [連署](#cosignatory-cosigning-the-pending-transaction) と [承認](#cosignatory-waiting-for-confirmation) の手順で定義します。 + Python では、接続から到着したメッセージを順番に読み取ります。 + +3 つのチャネルは、次の手順でアドレスを登録するまで何も送信しません。 + +### 連署人: マルチシグアカウントを登録する {: #cosignatory-registering-the-multisig-account } + +{{ tutorial.code_snippet_tagged('step-5') }} + +アカウントのチャネルから通知を受け取るには、まずアドレスをノードに **登録** する必要があります。 + +コードは にリクエストを送信します。マルチシグアドレスを登録するとともに、 チャネルでアカウントの現在の状態を送信するようノードに要求します。 + +コードは最初のアカウント通知を待ち、登録が有効になったことを確認します。 +通知は [AccountMetaDataPair](../reference/rest/nem.md#model/AccountMetaDataPair) スキーマに従います。 + +### 開始者: マルチシグトランザクションをアナウンスする {: #initiator-announcing-the-multisig-transaction } + +{{ tutorial.code_snippet_tagged('step-6') }} + +!!! warning "チャネルをサブスクライブしてからアナウンスしてください" + + リスナーの準備ができていることを確実にするため、トランザクションは必ず WebSocket チャネルをサブスクライブした**後**にアナウンスしてください。 + そうしないと、WebSocket がリッスンする前に通知が届く可能性があります。 + + 例えばアナウンス後にサブスクライブしたため通知を取り逃した連署人でも、 をポーリングして承認待ちのトランザクションを見つけることはできます。 + +連署人 1 がサブスクライブしたら、連署人 0 は エンドポイントへマルチシグトランザクションをアナウンスし、結果を確認します。 +ノードが拒否した場合は、拒否理由を表示して停止します。 + +有効であればネットワークはトランザクションを受け付けますが、まだ承認されていません。 +マルチシグアカウントには 2 つの連署が必要なのに 1 つしか提供されていないため、不足している連署が到着するまでトランザクションは [未承認トランザクションプール](default:未承認トランザクションプール) で待機します。 + +### 連署人: 承認待ちのトランザクションに連署する {: #cosignatory-cosigning-the-pending-transaction } + +{{ tutorial.code_snippet_tagged('step-7') }} + +承認待ちのマルチシグトランザクションは、[TransactionMetaDataPair](../reference/rest/nem.md#model/TransactionMetaDataPair) として チャネルに届きます。 +マルチシグトランザクションでは、`meta` フィールドに追加の `innerHash` フィールドが含まれ、**内部トランザクション** のハッシュ、つまり連署が参照する値を保持します。 + +連署人は、承認待ちのマルチシグトランザクションを複数持つ可能性があります。 +この例では、マルチシグアカウントが発行したトランザクションを選択します。 +そのアカウントからの承認待ちトランザクションは 1 件だけであると想定するため、チュートリアルにはこれで十分です。 + +ただし、実際のアプリケーションでは、このフィルターだけでは不十分です。 +承認待ちのトランザクションが期待したものだと保証するものはないため、連署するものを選ぶ前に、タイプ、受取人、金額など、承認待ちの各トランザクションの内容を確認してください。 + +!!! warning "連署する前に確認してください" + + 連署する前に、必ずトランザクションの内容を確認してください。 + 連署は拘束力があり、取り消すことはできません。 + 完全なマルチシグトランザクションは通知の `transaction` フィールドで確認できます。 + +{{ tutorial.code_snippet_tagged('step-8') }} + +コードは次に、内部トランザクションのハッシュとマルチシグアカウントのアドレスを参照する を構築し、連署人 1 のキーで署名して、 エンドポイントを使ってアナウンスします。 + +### 連署人: 承認を待つ {: #cosignatory-waiting-for-confirmation } + +{{ tutorial.code_snippet_tagged('step-9') }} + +アナウンスした連署は独立したトランザクションとして [未承認トランザクションプール](default:未承認トランザクションプール) に表示されないため、独自の通知は発生しません。 +代わりにネットワークが承認待ちのマルチシグトランザクションに付加し、 チャネルで新しい通知が発生します。 +この更新は連署が追加されたことだけを示すため、コードは無視します。 + +マルチシグトランザクションに追加の連署が必要なら、必要な連署がすべて集まるまで未承認プールに残ります。 +このチュートリアルでは 2 つ目の連署で要件を満たすため、トランザクションは未承認プールを離れ、有効であれば次のブロックで承認されます。 + +承認は チャネルに届きます。 +内部送金の送信者と受取人がどちらもマルチシグアカウントなので、この通知は役割ごとに 1 回、合計 2 回配信されます。 +コードは両方の通知を表示しますが、承認は 1 回だけ報告します。 + +トランザクションを含むブロックは、アカウントの更新後の状態を持つ チャネルの最後の通知も発生させます。 +この最後の通知が届くと、プログラムは後片付けの手順へ進みます。 + +### 連署人: チャネルのサブスクライブを解除する {: #cosignatory-unsubscribing-from-channels } + +{{ tutorial.code_snippet_tagged('step-10') }} + +承認後、連署人 1 は 3 つのチャネルのサブスクライブを解除し、接続を閉じる前に STOMP セッションを終了します。 + +## 出力 {: #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' +``` + +出力には次の内容が表示されます。 + +* **アカウント**(2~4 行目): マルチシグアカウントのアドレスと、両方の連署人の公開鍵。 +* **構築**(5 行目): 連署人 0 がマルチシグトランザクションを構築して署名します。 +* **接続**(6 行目): ノードのポート `7778` の WebSocket エンドポイント上で STOMP セッションが確立されます。 +* **サブスクリプション**(7~9 行目): マルチシグアカウントのアドレスに対応付けられた 3 チャネルをサブスクライブします。 +* **登録**(11 行目): マルチシグアカウントの現在の状態がアカウントチャネルに届き、登録を確認します。 +* **アナウンス**(12 行目): 連署人 0 がマルチシグトランザクションをアナウンスします。 +* **連署**(13~14 行目): 内部トランザクションハッシュを含む承認待ちのマルチシグトランザクションが未承認チャネルに届き、連署人 1 が連署をアナウンスします。 +* **承認**(15~17 行目): 完了したトランザクションがブロックで承認されます。内部送金の送信者と受取人がどちらもマルチシグアカウントなので、通知は 2 回届きます。 +* **アカウント更新**(18 行目): トランザクションを含むブロックが最後のアカウント通知を発生させます。送信した 1 XEM が送信者へ戻るため、残高は [手数料](../../textbook/transactions.md#fee-schedule) の 0.35 XEM だけ減ります。 +* **サブスクライブ解除**(19 行目): コードが 3 つのチャネルのサブスクライブを解除します。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [マルチシグアカウントのチャネルをサブスクライブする](#cosignatory-subscribing-to-the-channels) |

| +| [マルチシグアカウントを登録する](#cosignatory-registering-the-multisig-account) | | +| [承認待ちのマルチシグメッセージを処理する](#cosignatory-cosigning-the-pending-transaction) | [TransactionMetaDataPair](../reference/rest/nem.md#model/TransactionMetaDataPair) | +| [未承認通知に対して連署する](#cosignatory-cosigning-the-pending-transaction) | | diff --git a/mkdocs/pages/ja/devbook/websockets/listen-new-blocks.md b/mkdocs/pages/ja/devbook/websockets/listen-new-blocks.md new file mode 100644 index 000000000..06ea17949 --- /dev/null +++ b/mkdocs/pages/ja/devbook/websockets/listen-new-blocks.md @@ -0,0 +1,129 @@ +--- +title: 新しいブロック +tutorial_level: beginner +--- + +# 新しいブロックをリッスンする + + [WebSocket チャネル]() は、新しい [ブロック](default:ブロック) がチェーンに追加されるたびにリアルタイム通知を送信します。 + エンドポイントをポーリングする場合と比べ、WebSocket は API を繰り返し呼び出すオーバーヘッドなしに、発生した更新をプッシュします。 + +このチュートリアルでは、チャネルをサブスクライブして、受け取った更新を表示する方法を説明します。 + +!!! note "ポーリングによる方法" + + ポーリングを使う方法については、[チェーン高と不可逆高を照会する](../chain/chain-heights.md) チュートリアルを参照してください。 + +## 前提条件 {: #prerequisites } + +NEM は [SockJS](https://github.com/sockjs/sockjs-client) 上で [STOMP](https://stomp.github.io/) メッセージングプロトコルを使って WebSocket を提供するため、STOMP クライアントと WebSocket トランスポートが必要です。 + +=== ":simple-python: Python" + + `stomper` と `websockets` ライブラリをインストールします。 + + ```bash + pip install stomper websockets + ``` + +=== ":simple-javascript: JavaScript" + + `@stomp/stompjs` と `sockjs-client` ライブラリをインストールします。 + + ```bash + npm install @stomp/stompjs sockjs-client + ``` + +接続プロトコルの詳細については、[WebSocket リファレンス](../reference/websockets/index.md) を参照してください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/websockets/listen_new_blocks', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM [ノード](default:ノード) を指定します。 +値が指定されていない場合は、デフォルト値を使用します。 + +`WS_URL` は同じノードの WebSocket エンドポイントを定義します。 +`NODE_URL` のポート `7890`(デフォルトの HTTP API ポート)を `7778`(デフォルトの NIS WebSocket ポート)に置き換えて導出します。 + +プログラムは `Ctrl+C` で割り込みを受けるまで実行され、接続を閉じる前にサブスクライブ解除の手順が実行されます。 + +!!! note "Python の SockJS ヘルパー" + + Python 用の SockJS クライアントライブラリはないため、便宜上、ファイルの先頭に小さなヘルパーメソッドをいくつか定義しています。 + +## コードの説明 {: #code-explanation } + +### WebSocket に接続する {: #connecting-to-the-websocket } + +{{ tutorial.code_snippet_tagged('step-1') }} + +最初の手順では、ノードの `/w/messages` エンドポイントへの接続を開き、その上で [STOMP セッション]() を開始します。 + +### チャネルをサブスクライブする {: #subscribing-to-the-channel } + +{{ tutorial.code_snippet_tagged('step-2') }} + +コードは チャネルをサブスクライブします。 +ノードは新しいブロックがチェーンに追加されるたびに、サブスクライバーへ通知します(およそ 1 分ごとです)。 + +通知は常に均等な間隔で届くとは限りません。 +例えば、ノードがピアに追いつくために一度に複数のブロックを追加すると、チャネルはブロックごとに 1 通知を、短い間隔でまとめて配信します。 + +サブスクリプションには `id`(`id-0`)が付与されます。終了時のサブスクライブ解除に使います。 + +受信した各メッセージは、以下の整形処理に渡されます。 + +### メッセージを整形する {: #formatting-the-message } + +{{ tutorial.code_snippet_tagged('step-3') }} + +受信する各メッセージの本文は、ブロック情報です。[Block](../reference/rest/nem.md#model/Block) スキーマに従います。 + +スニペットでは、各メッセージから次の 2 フィールドを表示します。 + +* `height`: 新しいブロックの高さ。 +* `signer`: ブロックを生成した [ハーベスターアカウント](default:ハーベスターアカウント) の公開鍵。 + +NEM のブロックは、このペイロードに自身のハッシュを含まないため、このチュートリアルでは各ブロックを `height` で識別し、ハーベスターの `signer` も表示します。 + +!!! warning "新しいブロックはまだ確定していません" + + 新しいブロックはすでにチェーンの一部ですが、まだ不可逆ではありません。 + 後続ブロックが [書き換え制限](default:書き換え制限) を超えるのに十分な数だけ追加されるまでは、[ロールバック](default:ロールバック) が発生する可能性があります。 + ロールバック後は、受信済みのブロックを置き換えるブロックが届くため、チャネルがすでに受け取ったブロックより低い高さのブロックを報告することもあります。 + + [チェーン高と不可逆高を照会する](../chain/chain-heights.md) チュートリアルでは、不可逆高の計算方法を説明しています。 + +### 終了時にサブスクライブ解除する {: #unsubscribing-on-exit } + +{{ tutorial.code_snippet_tagged('step-4') }} + +プログラムが割り込み(`Ctrl+C`)を受けると、コードはチャネルのサブスクライブを解除し、接続を閉じる前に STOMP セッションを終了します。 +これにより、ノードからきれいに切断できます。 + +## 出力 {: #output } + +以下の出力は、新しいブロックをリッスンした場合の実行例です。 + +```text linenums="1" hl_lines="2 3 4 9" +--8<-- 'devbook/websockets/listen_new_blocks.log' +``` + +出力には次の内容が表示されます。 + +* **接続**(2 行目): ノードのポート `7778` の WebSocket エンドポイント上で STOMP セッションが確立されます。 +* **サブスクリプション**(3 行目): `/blocks` チャネルをサブスクライブします。 +* **新しいブロック**(4~8 行目): およそ 1 分ごとに新しいブロック通知が届きます。 +* **サブスクライブ解除**(9 行目): `Ctrl+C` でコードがサブスクライブを解除して切断します。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [ブロックチャネルをサブスクライブする](#subscribing-to-the-channel) | | +| [ブロックメッセージを整形する](#formatting-the-message) | [Block](../reference/rest/nem.md#model/Block) | diff --git a/mkdocs/pages/ja/devbook/websockets/listen-transaction-flow.md b/mkdocs/pages/ja/devbook/websockets/listen-transaction-flow.md new file mode 100644 index 000000000..29ccce577 --- /dev/null +++ b/mkdocs/pages/ja/devbook/websockets/listen-transaction-flow.md @@ -0,0 +1,198 @@ +--- +title: トランザクションフロー +tutorial_level: intermediate +--- + +# トランザクションフローをリッスンする + +NEM は、特定の [アカウント](default:アカウント) の [トランザクション](default:トランザクション) が承認プロセスを進むとき、リアルタイム通知を送信する [WebSocket チャネル]() を提供します。 + エンドポイントをポーリングする場合と比べ、WebSocket は API を繰り返し呼び出すオーバーヘッドなしに、発生した更新をプッシュします。 + +このチュートリアルでは、トランザクションチャネルをサブスクライブし、最小限の [転送トランザクション](../transactions/transfer-xem.md) をアナウンスして、WebSocket で承認を待つ方法を説明します。 + +!!! note "別の方法: ポーリング" + + ポーリングを使う方法については、[トランザクションのステータスを監視する](../transactions/monitoring-status.md) チュートリアルを参照してください。 + +## 前提条件 {: #prerequisites } + +始める前に、次の準備をしてください。 + +* 開発環境をセットアップする。 + [開発環境のセットアップ](../start/setup.md) を参照してください。 +* 監視するアカウントのアドレスを用意する。 +* トランザクション手数料を支払える残高を持つアカウントを用意する。 + [秘密鍵からアカウントを作成する](../accounts/create-from-private-key.md) または [ウォレットでアカウントを作成する](../../userbook/wallet/create-account.md) を参照してください。 + +さらに、NEM は [SockJS](https://github.com/sockjs/sockjs-client) 上で [STOMP](https://stomp.github.io/) メッセージングプロトコルを使って WebSocket を提供するため、STOMP クライアントと WebSocket トランスポートが必要です。 + +=== ":simple-python: Python" + + `stomper` と `websockets` ライブラリをインストールします。 + + ```bash + pip install stomper websockets + ``` + +=== ":simple-javascript: JavaScript" + + `@stomp/stompjs` と `sockjs-client` ライブラリをインストールします。 + + ```bash + npm install @stomp/stompjs sockjs-client + ``` + +接続プロトコルの詳細については、[WebSocket リファレンス](../reference/websockets/index.md) を参照してください。 + +## 完全なコード {: #full-code } + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/websockets/listen_transaction_flow', ['py', 'js']) }} + +スニペットでは、`NODE_URL` 環境変数を使って NEM [ノード](default:ノード) を指定します。 +値が指定されていない場合は、デフォルト値を使用します。 + +`WS_URL` は同じノードの WebSocket エンドポイントを定義します。 +`NODE_URL` のポート `7890`(デフォルトの HTTP API ポート)を `7778`(デフォルトの NIS WebSocket ポート)に置き換えて導出します。 + +!!! note "Python の SockJS ヘルパー" + + Python 用の SockJS クライアントライブラリはないため、便宜上、ファイルの先頭に小さなヘルパーメソッドをいくつか定義しています。 + +## コードの説明 {: #code-explanation } + +### 監視対象アドレスと署名者をセットアップする {: #setting-up-the-monitored-address-and-signer } + +{{ tutorial.code_snippet_tagged('step-1') }} + +この手順では、監視するアドレスと、そこへ送金するアカウントをセットアップします。 + +`MONITOR_ADDRESS` は監視するアドレスです。 +このチュートリアルがサブスクライブするチャネルはこのアドレスに対応付けられ、送金の送信者や受取人など、トランザクションに関係するたびに通知します。 +WebSocket API では、アドレスを大文字かつハイフンなしで指定します。 + +`SIGNER_PRIVATE_KEY` は送金を送信するアカウントの秘密鍵で、通知を発生させます。 + +これらの環境変数が設定されていない場合は、チュートリアルがデフォルト値を用意します。 + +### 転送トランザクションを構築して署名する {: #building-and-signing-a-transfer-transaction } + +{{ tutorial.code_snippet_tagged('step-2') }} + +このチュートリアルでは、監視対象アドレスへ、金額 0、モザイクなし、メッセージなしの最小限の [転送トランザクション](default:転送トランザクション) を構築します。 +簡単にするため送金を使いますが、どのトランザクションタイプでも同じ WebSocket 通知を発生させます。 + +トランザクションは [XEM を送信する](../transactions/transfer-xem.md) チュートリアルと同じ方法で構築します。ネットワーク時刻を取得し、トランザクションを作成して、署名します。 + +トランザクションに署名するとハッシュが生成され、一意に識別できるようになります。 +コードはこのハッシュを保存します。トランザクションチャネルの通知にはトランザクションハッシュが含まれるためです。 +後で、受信した各ハッシュを保存値と比較して、このトランザクションの通知を特定します。 + +トランザクションは準備されますが、まだ [アナウンス](#announcing-the-transaction) されません。 +チャネルのサブスクリプションを確立した後でアナウンスするため、結果の通知を取り逃しません。 + +### WebSocket に接続する {: #connecting-to-the-websocket } + +{{ tutorial.code_snippet_tagged('step-3') }} + +コードは `WS_URL` の `/w/messages` エンドポイントへ SockJS 接続を開き、その上で [STOMP セッション]() を開始します。 + +### チャネルをサブスクライブする {: #subscribing-to-the-channels } + +{{ tutorial.code_snippet_tagged('step-4') }} + +コードは、アドレスに対応付けられた次の 3 チャネルをサブスクライブします。 + +* : アドレスを含む [ブロック](default:ブロック) が承認されると、アカウントの現在の状態を通知します。 +* : アドレスに関係するトランザクションが [未承認トランザクションプール](default:未承認トランザクションプール) に入り、ブロックへの包含を待つ状態になると通知します。 +* : アドレスに関係するトランザクションが [ブロック](default:ブロック) に含まれると通知します。 + +サブスクリプションには `id-0`、`id-1`、`id-2` を使います。プログラムが最後にサブスクライブを解除するときに、それぞれを識別します。 + +!!! note "メッセージ処理の違い" + + JavaScript では、各チャネルを専用のハンドラー関数でサブスクライブします。関数は下の [承認を待つ](#waiting-for-confirmation) 手順で定義します。 + Python では、接続から到着したメッセージを順番に読み取ります。 + +3 つのチャネルは、次の手順でアドレスを登録するまで何も送信しません。 + +### アカウントを登録する {: #registering-the-account } + +{{ tutorial.code_snippet_tagged('step-5') }} + +アカウントのチャネルから通知を受け取るには、まずアドレスをノードに **登録** する必要があります。 + +コードは にリクエストを送信します。アドレスを登録するとともに、 チャネルでアカウントの現在の状態を送信するようノードに要求します。 + +コードは最初のアカウント通知を待ち、登録が有効になったことを確認します。 +通知は [AccountMetaDataPair](../reference/rest/nem.md#model/AccountMetaDataPair) スキーマに従います。 + +アカウントチャネルのサブスクリプションは実行中ずっと開いたままなので、トランザクションの承認で発生するアカウント通知も出力に表示されます。 + +### トランザクションをアナウンスする {: #announcing-the-transaction } + +{{ tutorial.code_snippet_tagged('step-6') }} + +!!! warning "チャネルをサブスクライブしてからアナウンスしてください" + + リスナーの準備ができていることを確実にするため、トランザクションは必ず WebSocket チャネルをサブスクライブした**後**にアナウンスしてください。 + そうしないと、WebSocket がリッスンする前に通知が届く可能性があります。 + +コードは エンドポイントへトランザクションをアナウンスし、結果を確認します。 +ノードが拒否した場合は、拒否理由を表示して停止します。 + +### 承認を待つ {: #waiting-for-confirmation } + +{{ tutorial.code_snippet_tagged('step-7') }} + +受け付けられた場合、コードは承認を待ち、サブスクライブしたチャネルからの各メッセージを表示します。 + +トランザクションチャネルからのメッセージは [TransactionMetaDataPair](../reference/rest/nem.md#model/TransactionMetaDataPair) スキーマに従い、`meta.hash.data` フィールドにトランザクションハッシュを持ちます。 +メッセージが届くたびに、コードはそのハッシュを保存値と比較して、チャネル通知の中からこのトランザクションを認識します。 + +成功したトランザクションで想定される順序は、[トランザクションのライフサイクル](../../textbook/transactions.md#transaction-lifecycle) に説明されています。 + +1. `unconfirmed`: トランザクションが [未承認トランザクションプール](default:未承認トランザクションプール) に入る。 +2. `confirmed`: トランザクションが [ブロック](default:ブロック) に含まれる。 + +トランザクションを含むブロックは、 チャネルでも最後の通知を発生させます。 +トランザクションチャネルとは異なり、この通知にはトランザクションハッシュではなくアカウントの更新後の状態が含まれるため、特定のトランザクションとは照合できません。 + +この最後の通知が届くと、プログラムは後片付けの手順へ進みます。 + +### チャネルのサブスクライブを解除する {: #unsubscribing-from-channels } + +{{ tutorial.code_snippet_tagged('step-8') }} + +承認後、コードは 3 つのチャネルのサブスクライブを解除し、接続を閉じる前に STOMP セッションを終了します。 + +## 出力 {: #output } + +```text linenums="1" hl_lines="2-14" +--8<-- 'devbook/websockets/listen_transaction_flow.log' +``` + +出力には次の内容が表示されます。 + +* **アドレス**(2 行目): 監視対象のアドレス。 +* **接続**(3 行目): ノードのポート `7778` の WebSocket エンドポイント上で STOMP セッションが確立されます。 +* **サブスクリプション**(4~6 行目): アカウントチャネルと 2 つのトランザクションチャネルがサブスクライブされます。 +* **登録**(7~8 行目): アカウントの現在の状態がアカウントチャネルに届き、登録を確認します。 +* **アナウンス**(9 行目): トランザクションがアナウンスされ、そのハッシュが表示されます。 +* **トランザクションフロー**(10~11 行目): トランザクションが `unconfirmed` から `confirmed` へ進み、承認のライフサイクルが表示されます。 +* **承認**(12 行目): チャネルのハッシュが、アナウンスしたトランザクションと一致します。 +* **アカウント更新**(13 行目): トランザクションを含むブロックが最後のアカウント通知を発生させます。送金額が 0 なので残高は変わりません。 +* **サブスクライブ解除**(14 行目): コードが 3 つのチャネルのサブスクライブを解除します。 + +## まとめ {: #conclusion } + +このチュートリアルでは、次の方法を説明しました。 + +| 手順 | 関連ドキュメント | +| --- | --- | +| [アカウントチャネルをサブスクライブする](#subscribing-to-the-channels) | | +| [未承認チャネルをサブスクライブする](#subscribing-to-the-channels) | | +| [トランザクションチャネルをサブスクライブする](#subscribing-to-the-channels) | | +| [アカウントを登録する](#registering-the-account) | | +| [トランザクションメッセージを処理する](#waiting-for-confirmation) | [TransactionMetaDataPair](../reference/rest/nem.md#model/TransactionMetaDataPair) | 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..b28a8a529 --- /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/pages/ja/userbook/node/install.md b/mkdocs/pages/ja/userbook/node/install.md new file mode 100644 index 000000000..010af5dd7 --- /dev/null +++ b/mkdocs/pages/ja/userbook/node/install.md @@ -0,0 +1,215 @@ +--- +title: ノードのインストール +--- + +# ノードクライアントのインストール + +このガイドでは、[手動インストール](#manual-installation)または[Docker を使用](#using-docker)して NEM ノードをデプロイする方法を説明します。 + +## ハードウェア要件 {: #hardware-requirements } + +* インターネットに接続されたマシン。データベースとログファイル用に約 30 GB のディスク容量と、16 GB の RAM が必要です(2026 年 7 月時点)。 + +* [コンセンサス](default:コンセンサス) と [ハーベスティング](default:ハーベスティング) に円滑に参加するには、ノードにパブリックに到達可能な IP アドレスが必要です。また、TCP ポート **7890** を受信と送信の双方に対して開く必要があります。 + + パブリック IP アドレスがなくても、ノードは新しいブロックを生成してネットワークに送信できます。 + ただし、ピアノードは、検出した新しいブロックやトランザクションをノードに通知できません。 + この場合、ノードは定期的にピアへポーリングするため、新しいブロックやトランザクションの把握が遅くなります。 + + また、[スーパーノードプログラム](default:スーパーノードプログラム) に参加するには、パブリック IP アドレスが必要です。 + +* ウォレットなどのアプリケーションがノードを介してネットワークと通信できるようにするには、次の TCP ポートを開く必要があります。 + + * **7890**([REST](../../devbook/reference/rest/nem.md) リクエスト用)。 + * **7778**([WebSockets](../../devbook/reference/websockets/index.md) リクエスト用)。 + +## 手動インストール {: #manual-installation } + +### 前提条件 {: #prerequisites } + +* [Java JRE 11](https://docs.oracle.com/en/java/javase/11/) または [OpenJDK 11](https://openjdk.org/projects/jdk/11/) をインストールします。 + +NIS1 クライアントは、Linux、Windows、macOS など、Java をサポートするすべてのオペレーティングシステムで動作します。 + +### インストール {: #installation } + +* [最新のバイナリをダウンロード](https://github.com/NemProject/nem/releases)します。 + +* NIS1 の _インストール_ フォルダーとなるフォルダーにファイルを解凍します。 + +### 設定 {: #manual-configuration } + +新しい `nis/config-user.properties` ファイルを作成し、ケースに合わせて次の内容を変更します。 + +```ini +nem.folder = %h/nem +nis.bootName = my-server +nis.bootKey = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef +``` + +* `nem.folder` は NIS1 の _ホーム_ フォルダーを設定します。 + このフォルダーはインストールフォルダーと異なっていてもよく、プログラムのログとデータベースファイルを保存します。 + + ユーザーのホームフォルダーを示すために `~` を使わず、`%h` を使います。 + デフォルトの場所は `%h/nem` です。 + + Windows ではバックスラッシュを二重にする必要があります: `\\`。 + たとえば `D:\\NEM\\nis1-home` です。 + +* `nis.bootName` をサーバーに付ける名前に設定します。 + これは情報を示すだけの値です。 + 先頭と末尾の空白は削除されますが、名前の途中には空白を含められます。 + UTF8 はエスケープによってサポートされていますが、ASCII 範囲外の文字は使わないでください + (例: `\u3053\u3093\u306B\u3061\u306F`)。 + +* `nis.bootKey` をこのノードを管理するアカウントの[秘密鍵](default:秘密鍵)に設定します。 + まだアカウントを持っていない場合は、NEM NanoWallet などの[ウォレット](default:ウォレット)を使って作成します。 + + * [委任ハーベスティング](default:委任ハーベスティング)を行う場合、これは[リモートアカウント](default:リモートキー)の秘密鍵です。 + この設定を**推奨**します。 + + * [ローカルハーベスティング](default:ローカルハーベスティング)を行う場合、これはアカウント自体の秘密鍵です。 + この設定は**推奨しません**。 + + !!! warning "このキーは常に秘密にしてください" + +* ノードでハーベストする場合は、`nis.shouldAutoHarvestOnBoot` を `true` に設定します。 + +!!! note "ノードの初回起動を高速化する" + + ノードの初回起動を高速化するため、データベーススナップショットを任意でダウンロードできます。 + + * [https://bob.nem.ninja/](https://bob.nem.ninja/) に移動し、最新の `nis5_mainnet-*.mv.db` ファイルをダウンロードします。 + 例: [nis5_mainnet-5-565-850.mv.db.gz](https://bob.nem.ninja/nis5_mainnet-5-565-850.mv.db.gz)。 + * NIS1 のホームフォルダー内に `nis/data` という名前のフォルダーを作り、その中にファイルを解凍します。 + 解凍したファイルの名前を `nis5_mainnet.mv.db` に変更します。 + +!!! example "テストネットノードを実行する" + + [テストネット](default:テストネット)ノードを実行する場合は、`config-user.properties` ファイルに次のプロパティを追加します。 + + ```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 } + +ターミナルを開き、オペレーティングシステムに対応する起動スクリプトを確認します。 + +=== "Windows" + + ```bash + runNis.bat + ``` + +=== "Linux" + + ```bash + nix.runNis.sh + ``` + +!!! note "メモリ不足の問題" + + メモリ不足が発生した場合は、起動スクリプトを編集して[`-Xmx` パラメーターを増やします](https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE__GUID-98AC4535-A539-406D-9AC5-390C1AF143F0)。 + +スクリプトを起動します。 +コンソール出力にノードが実行中であることが示されます。 + +## Docker を使用する {: #using-docker } + +これらの手順は、Windows Subsystem for Linux を含む Linux システムでのみ動作します。 + +### 前提条件 {: #prerequisites_1 } + +* [Docker](https://docs.docker.com/get-docker/)。 + +* [Git](https://git-scm.com)。 + +### インストール {: #docker-installation } + +次のコマンドで [nem-docker](https://github.com/NemProject/nem-docker) リポジトリをクローンします。 + +```bash +git clone https://github.com/NemProject/nem-docker.git +cd nem-docker +``` + +### 設定 {: #configuration } + +初回実行時に、クライアントは(上の手動[設定](#manual-configuration)セクションで説明した)**boot name** と **boot key** のプロパティを尋ね、それらを保存します。 + +設定を手動で編集する場合は、クライアントを起動する前に、上記で説明した内容を使って `custom-configs/nis.config-user.properties` という新しいファイルを作成します。 + +### ノードを制御する {: #controlling-the-node } + +* ノードを起動するには、次を実行します。 + + ```bash + ./boot.sh + ``` + +* ノードを停止するには、次を実行します。 + + ```bash + ./stop.sh + ``` + +他のコマンドについては、[nem-docker GitHub プロジェクト](https://github.com/NemProject/nem-docker)を参照してください。 + +## 同期 {: #synchronization } + +ノードは初回起動時に、ピアからブロックチェーン全体をダウンロードします。 + +**この処理には最大 48 時間かかることがあります。** + +任意のデータベーススナップショットをダウンロードした場合、ノードはまずデータベースを読み込み、その後、残りのブロックをダウンロードします。 +これにより、同期時間が大幅に短縮されます。 + +同期中も、次のことを確認できます。 + +* パブリック IP がある場合、ノードを起動して数分後には、[nodewatch.symbol.tools](https://nodewatch.symbol.tools/nem/nodes) の公開ノード一覧に表示されるはずです。 + 報告されるチェーン高は、ネットワークの他のノードに追いつくにつれて増加します。 + +* ブラウザで[localhost:7890/chain/height](http://localhost:7890/chain/height)を開いて、ノードに現在のチェーン高を尋ねることもできます。 + +## ノードを監視する {: #monitoring-the-node } + +NIS はポート 7890 でクエリを待ち受けるため、ノードを監視する最初の方法は、ブラウザで[localhost:7890/node/info](http://localhost:7890/node/info)を開くことです。 + +`NIS_ILLEGAL_STATE_LOADING_CHAIN` のようなエラーを含め、何らかの応答があれば、ノードは実行中です。 + +クエリ可能な URL の一覧については、[REST API 仕様](../../devbook/reference/rest/nem.md)を参照してください。 + +## ノードを更新する {: #updating-a-node } + +NIS1 クライアントを最新のプロトコルバージョンに更新するのは簡単です。 + +### 手動で行う {: #manually } + +* `Ctrl+C` を押すかプロセスを終了して、サーバーを停止します。 + +* 古いパッケージを削除します。 + これは、`config-user.properties` を除く [NIS1 インストールフォルダー](#installation)内のすべてのファイルを削除することを意味します。 + NIS1 のホームフォルダー内のすべてのファイルは残します。 + +* [最新のバイナリ](https://github.com/NemProject/nem/releases)をダウンロードし、同じフォルダーに解凍します。 + +* [起動時と同じコマンド](#launch)で、サーバーを再び起動します。 + +### Docker を使用する {: #using-docker_1 } + +* `./stop.sh` でサーバーを停止します。 + +* [インストール](#docker-installation)手順でクローンしたリポジトリを `git pull` で更新します。 + +* `./boot.sh` でサーバーを再起動します。 diff --git a/mkdocs/pages/ja/userbook/node/supernode-program.md b/mkdocs/pages/ja/userbook/node/supernode-program.md new file mode 100644 index 000000000..16e9b2a98 --- /dev/null +++ b/mkdocs/pages/ja/userbook/node/supernode-program.md @@ -0,0 +1,212 @@ +--- +title: スーパーノードプログラム +--- + +# スーパーノードプログラム + +[スーパーノードプログラム](default:スーパーノードプログラム)は、ネットワークの安全性確保とアプリケーションへの信頼性の高いデータアクセス提供に貢献する、公開 NEM ノードに報酬を与えます。 +すべての [XEM](default:XEM) はネメシスブロックで発行されたため、NEM には新規発行によるブロック報酬がありません。 +スーパーノードプログラム用に XEM の一部が確保されています。 + +報酬の資格は毎日 4 ラウンドで確認され、6 時間ごとに 1 ラウンド実施されます。 +その日の報酬を受け取るには、参加ノードが各ラウンドのすべてのテストに合格する必要があります。 + +テストは、中央で管理される _コントローラー_ というスーパーノード監視サービスによって実施されます。 +一部のテストでは、参加ノードと信頼できる _参照ノード_ を比較します。 + +参加ノードでは、NIS1 クライアントと同じマシン上で軽量アプリケーションの _Node Servant_ も実行します。 +Servant はスーパーノードプログラムのテストを実行し、その一部で使われる `/nr/...` エンドポイントを公開します。 + +## 資格テスト {: #eligibility-tests } + +各テストラウンドには、次の確認が含まれます。 + +* **チェーン高**:ノードは API を通じて現在の高さを報告します。 + 高さが参照ノードより 4 ブロック以内の遅れであれば、テストに合格します。 + +* **チェーン部分**:ノードは、最近のブロック 60 ~ 100 個からなるランダムなグループを + API を通じて提供します。 + コントローラーはブロックの署名を検証し、そのブロックから計算した複合ハッシュを、参照ノードから同じ方法で計算したハッシュと比較します。 + +* **残高**:ノードのメインアカウントには少なくとも 10,000 XEM が必要です。 + コントローラーは参照ノードを経由して、 API でこれを確認します。 + +* **計算能力**:ノードは、ランダムなハッシュから公開鍵導出を 10,000 回繰り返し実行します。 + 最終的な公開鍵がコントローラーの結果と一致し、ラウンドトリップ全体が 5 秒以内であれば、テストに合格します。 + +* **バージョン**:ノードは API を通じて NIS クライアントのバージョンを報告します。 + バージョンが参照ノードのバージョンと同じか、それより新しければ、テストに合格します。 + +* **Ping**:コントローラーは `/nr/task/ping` API を通じて、ランダムなパートナーノードを 5 つ Servant に渡します。 + Servant は各パートナーの `/nr/ping` API を 5 回呼び出します。 + 失敗する Ping が 1 回以下で、成功したラウンドトリップ時間の平均が 200 ms 未満であれば、テストに合格します。 + +* **帯域幅**:コントローラーは Ping テストで最速だったパートナーとランダムなハッシュシードを、`/nr/task/bandwidth` API を通じて Servant に渡します。 + Servant とパートナーは同じ 30,000 ハッシュ計算を実行します。 + 両方のハッシュ結果が一致し、測定された転送速度が少なくとも 5 Mbit/s であれば、テストに合格します。 + +* **応答性**:コントローラーは API に 10 件のリクエストを送信します。 + 少なくとも 9 件のリクエストが成功し、すべてのリクエストが 1 秒以内に完了すれば、テストに合格します。 + 時間制限だけが不合格の理由である場合、コントローラーはこのテストを最大 4 回再試行します。 + +## プログラムへの登録 {: #enrolling-in-the-program } + +登録する前に、次の準備ができていることを確認してください。 + +* [NEM NanoWallet](https://github.com/NemProject/NanoWallet/releases) がインストールされていること。 + +* 少なくとも 10,010 XEM がある NEM アカウント。 + プログラムへの参加には 10,000 XEM が必要で、これに加えて委任ハーベスティングと登録トランザクションの手数料として約 10 XEM が必要です。 + +* メインアカウントで[委任ハーベスティング](default:委任ハーベスティング)が有効になっていること。 + リモートアカウントは約 360 ブロック、つまり約 6 時間後に使用可能になります。 + [リモートアカウントの秘密鍵](default:リモートキー)が使用可能になり次第、登録を開始できます。 + +* 同期済みのパブリックノード。 + まだ持っていない場合は、[ノードのインストールガイド](install.md)に従ってください。 + + `nis.shouldAutoHarvestOnBoot` プロパティが `true` に設定されているか、設定されていないことを確認してください。 + +* 安定したパブリック IP アドレスまたはドメイン名がノードに付与されていること。 + +!!! warning "リモートアカウントの秘密鍵を使用する" + + ノードと Servant の設定にはリモートアカウントの秘密鍵を使用します。 + メインアカウントの秘密鍵は使用しないでください。 + + メイン秘密鍵はアカウントの資金を管理します。 + リモートアカウントの秘密鍵は委任ハーベスティングだけを管理し、漏えいした場合は置き換えられます。 + +NEM NanoWallet でリモートアカウントの秘密鍵を見つけるには、**Services**、**Delegated Harvesting**、**Manage Delegated Account** の順に開きます。 +**Show delegated account keys** を選択し、ウォレットのパスワードを入力してリモートアカウントの秘密鍵を表示します。 + +### 手動で行う {: #manually } + +#### Servant を設定する {: #configuring-the-servant } + +1. [Node Servant](https://bob.nem.ninja/servant_0_0_4.zip) をダウンロードします。 + +2. Servant のインストールフォルダーになるフォルダーにファイルを解凍します。 + NIS1 のインストールフォルダーと同じである必要はありません。 + `servant` フォルダーを開きます。 + +3. Servant の `config-user.properties` ファイルを開きます。 + +4. `nem.host` をノードの静的 IP アドレスまたはドメイン名に設定します。 + コントローラーが同じノードをテストできるよう、この値は安定していなければなりません。 + +5. `servant.key` をリモートアカウントの秘密鍵に設定します。 + +6. ファイルを保存します。 + +7. Servant がスーパーノードプログラムのテストを受信できるよう、受信接続と送信接続用に TCP ポート **7880** を開きます。 + +#### Servant を起動する {: #starting-the-servant } + +NIS を起動して同期させてから、次のコマンドで Servant を起動します。 + +=== "Windows" + + ```bash + runservant.bat + ``` + +=== "Linux and macOS" + + ```bash + sh startservant.sh + ``` + + `screen` や `nohup` などのツールを使って、Servant をバックグラウンドで実行します。 + +### Docker を使用する {: #using-docker } + +これらの手順は、Windows Subsystem for Linux を含む Linux システムでのみ動作します。 + +#### Servant を設定する {: #configuring-the-servant_1 } + +ノードを[Docker 用のインストールガイド](./install.md#using-docker)の説明どおりに Docker でインストールした場合、Servant はすでにインストールされているため、設定するだけで済みます。 + +[ノードを制御する](./install.md#controlling-the-node)の説明どおりに、実行中であれば NIS1 クライアントを停止します。 + +`custom-configs` フォルダー内で、次の操作を行います。 + +* `servant.config.properties.sample` ファイルを `servant.config.properties` にコピーして編集します。 + 少なくとも `nem.host` と `servant.key` プロパティの値を指定します。 +* `supervisord.conf.sample` ファイルを `supervisord.conf` にコピーして編集します。 + `[program:nis]` セクションと `[program:servant]` セクションの両方で `autostart=true` を設定します。 + +#### Servant を起動する {: #starting-the-servant_1 } + +これで、NIS1 クライアントを起動するたびに Servant も起動します。 + +```bash +./boot.sh +``` + +次のコマンドで確認できます。 + +```bash +./service.sh status +``` + +### 登録を送信する {: #sending-the-enrollment } + +毎月の登録アドレスは、NEM コミュニティの各種チャンネルで告知されます。 + +#### NEM NanoWallet で登録する {: #enrolling-with-nem-nanowallet } + +1. **Services** を開きます。 + +2. **SuperNode Program** を開きます。 + +3. **Check & Enroll in Program** を開きます。 + +4. **Enroll in Program** を選択します。 + +5. 現在の登録アドレスとノードホストを入力します。 + + ホストは、ノードの エンドポイントが返すホストと一致していなければなりません。 + +6. 登録トランザクションを送信します。 + +#### 手動で登録する {: #enrolling-manually } + +手動で登録するには、現在の登録アドレスへ、次の暗号化されていないメッセージを含む転送トランザクションを送信します。 + +```text +enroll +``` + +* `` はノードの エンドポイントが返す値です。 +* `` は次のクエリが返す値です。 + + ```text + https://nem.io/supernode/api/codeword/ + ``` + + !!! warning "公開鍵を使用する" + + * コードワード API の URL には、[秘密鍵](default:秘密鍵) ではなくメインの [公開鍵](default:公開鍵) を使用します。 + + * ノードでハーベストするために使う [リモートキー](default:リモートキー) ではなく、プログラムに登録したアカウントの [メイン公開鍵](default:メインキー) を使用します。 + + が返すのはリモートキーであることに注意してください。 + +## テスト結果を確認する {: #reviewing-test-results } + +[スーパーノードプログラムのページ](https://nem.io/supernode/)でノードのテスト結果を確認します。 +登録直後には結果が表示されません。 + +## スーパーノードホストを編集する {: #editing-the-supernode-host } + +ノードの IP アドレスまたはドメイン名が変わった場合は、ノードと Servant の設定を更新します。 +その後、変更したホストで新しい登録トランザクションを送信します。 + +## 毎月再登録する {: #monthly-re-enrollment } + +スーパーノードプログラムでは、毎月の再登録が必要です。 +毎月新しい登録アドレスがあり、NEM コミュニティの各種チャンネルで告知されます。 + +次の月の登録は、現在の月の終了 4 日前に開始されます。 +新しい登録アドレスを使って、毎月[登録プロセス](#sending-the-enrollment)を繰り返します。 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) # [