mirror of
https://github.com/logos-blockchain/logos-blockchain-module.git
synced 2026-08-09 15:53:14 +00:00
Compare commits
6 Commits
0.2.0-rc.2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d911d1105e | ||
|
|
098927c20a | ||
|
|
df845c65a5 | ||
|
|
581323e9e5 | ||
|
|
2946e0f853 | ||
|
|
1b0b5a45b4 |
265
.github/workflows/doctests.yml
vendored
Normal file
265
.github/workflows/doctests.yml
vendored
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
name: blockchain-module Doc-Tests
|
||||||
|
|
||||||
|
# Runs the executable blockchain-module doc-tests end-to-end via the shared
|
||||||
|
# doctest CLI:
|
||||||
|
# - blockchain-module-config.test.yaml: packages and installs THIS commit of
|
||||||
|
# the blockchain module as an .lgx, starts a logoscore daemon, loads the
|
||||||
|
# module, and generates node configs (with bootstrap peers, and with
|
||||||
|
# skip_ibd for a peerless run).
|
||||||
|
# - blockchain-module-runtime.test.yaml: takes that config and drives a real
|
||||||
|
# node lifecycle — start, chain state, wallet addresses, stop.
|
||||||
|
# Both run in one invocation so they share a single HTML report.
|
||||||
|
#
|
||||||
|
# Both specs run the node with NO bootstrap peers (skip_ibd). That is
|
||||||
|
# deliberate: a node configured with peers spends startup trying to fetch a
|
||||||
|
# chain tip from each, and aborts bootstrap if none answer — so a CI job that
|
||||||
|
# depends on a live network fails whenever the network moves, the peers rotate,
|
||||||
|
# or the node's chainsync protocol version changes. With IBD skipped the node
|
||||||
|
# comes up against its own genesis in under a second and every assertion is
|
||||||
|
# deterministic.
|
||||||
|
#
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# One-time setup required for the clickable report links to work:
|
||||||
|
#
|
||||||
|
# 1. Repo Settings → Pages → "Build and deployment" → Source: "Deploy from a
|
||||||
|
# branch", Branch: `gh-pages` / `(root)`. (The publish-report job creates
|
||||||
|
# the gh-pages branch on its first run.)
|
||||||
|
# 2. Nothing else — GITHUB_TOKEN already has the permissions granted below.
|
||||||
|
#
|
||||||
|
# Each run publishes the two-column HTML report to:
|
||||||
|
# https://<owner>.github.io/<repo>/pr-<N>/<os>/ (pull requests)
|
||||||
|
# https://<owner>.github.io/<repo>/main/<os>/ (pushes to master)
|
||||||
|
# and (for PRs) posts/updates a comment with the links.
|
||||||
|
#
|
||||||
|
# Note: pull requests opened from forks get a read-only GITHUB_TOKEN, so the
|
||||||
|
# Pages push and PR comment are skipped for them — the downloadable artifact is
|
||||||
|
# still produced.
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
push:
|
||||||
|
branches: [master, main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: doctests-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
doctests:
|
||||||
|
name: blockchain-module doc-tests (${{ matrix.os }})
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Linux only, unlike the other repos' doc-tests. The specs build the
|
||||||
|
# module's .lgx, which compiles the bundled logos_blockchain native
|
||||||
|
# library — and this repo builds for Linux exclusively (ci.yml is
|
||||||
|
# ubuntu-latest; release.yml is ubuntu-24.04 + ubuntu-24.04-arm). With no
|
||||||
|
# darwin job anywhere there is no darwin binary cache to substitute from,
|
||||||
|
# so a macOS runner compiles the whole Rust node from source every time:
|
||||||
|
# measured at ~4.5 min on ubuntu against >15 min and still going on macOS.
|
||||||
|
#
|
||||||
|
# Add macos-latest back once this repo ships a darwin build that
|
||||||
|
# populates the cache — at that point the doctest gets it nearly free.
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 90
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Setup Cachix
|
||||||
|
uses: cachix/cachix-action@v15
|
||||||
|
with:
|
||||||
|
name: logos-co
|
||||||
|
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||||
|
|
||||||
|
# Resolve the commit under test. For pull requests this is the PR's head
|
||||||
|
# commit (not the synthetic merge commit); for pushes it's the pushed
|
||||||
|
# commit. Passed to --release-for below so the doc-test packages THIS
|
||||||
|
# commit of logos-blockchain-module instead of the latest published flake.
|
||||||
|
#
|
||||||
|
# Fork PRs are the exception: their head commit lives in the fork, not in
|
||||||
|
# logos-blockchain/logos-blockchain-module, so nix could not fetch
|
||||||
|
# `github:logos-blockchain/logos-blockchain-module/<sha>`. We blank the SHA
|
||||||
|
# for forks (--release-for repo= → pins that repo to latest), so the
|
||||||
|
# doc-test still runs for fork PRs, just against master.
|
||||||
|
- name: Resolve commit under test
|
||||||
|
id: commit
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ] && \
|
||||||
|
[ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
|
||||||
|
echo "sha=" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Fork PR detected — doc-test will run against latest master."
|
||||||
|
else
|
||||||
|
echo "sha=${{ github.event.pull_request.head.sha || github.sha }}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The runner is the shared `doctest` CLI, invoked directly via its flake
|
||||||
|
# (github:logos-co/logos-doctest). The flake bundles Python + PyYAML
|
||||||
|
# (+ rich), so no pip install step is needed.
|
||||||
|
#
|
||||||
|
# --release-for pins the {release} placeholder for logos-blockchain-module
|
||||||
|
# to the commit under test, so
|
||||||
|
# `github:logos-blockchain/logos-blockchain-module{release}` in the spec
|
||||||
|
# becomes `.../<sha>`. Every other repo URL still resolves to latest.
|
||||||
|
- name: Run blockchain-module doc-tests
|
||||||
|
run: |
|
||||||
|
# Both specs run in one invocation so they share a single --report.
|
||||||
|
# --continue-on-fail so the run walks every step and the published
|
||||||
|
# report is complete. The job still fails (non-zero exit) if any step
|
||||||
|
# failed; this only changes whether we stop early.
|
||||||
|
#
|
||||||
|
# No --output-dir: it only supports a single spec. Without it the specs
|
||||||
|
# run in per-spec temp workdirs and still land in one --report.
|
||||||
|
|
||||||
|
nix run github:logos-co/logos-doctest -- run \
|
||||||
|
doctests/blockchain-module-config.test.yaml \
|
||||||
|
doctests/blockchain-module-runtime.test.yaml \
|
||||||
|
--verbose \
|
||||||
|
--continue-on-fail \
|
||||||
|
--release-for logos-blockchain-module=${{ steps.commit.outputs.sha }} \
|
||||||
|
--report "${{ runner.temp }}/blockchain-doctest-report.html"
|
||||||
|
|
||||||
|
- name: Stage report for upload
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p report-out
|
||||||
|
# Name it index.html so the published directory URL renders directly.
|
||||||
|
if [ -f "${{ runner.temp }}/blockchain-doctest-report.html" ]; then
|
||||||
|
cp "${{ runner.temp }}/blockchain-doctest-report.html" report-out/index.html
|
||||||
|
else
|
||||||
|
echo "<h1>No report produced</h1>" > report-out/index.html
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload blockchain-module execution report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: blockchain-doctest-report-${{ matrix.os }}
|
||||||
|
path: report-out/index.html
|
||||||
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
- name: Verify markdown generation
|
||||||
|
run: |
|
||||||
|
for spec in blockchain-module-config blockchain-module-runtime; do
|
||||||
|
nix run github:logos-co/logos-doctest -- generate \
|
||||||
|
"doctests/$spec.test.yaml" \
|
||||||
|
--release-for logos-blockchain-module=${{ steps.commit.outputs.sha }} \
|
||||||
|
-o "/tmp/$spec.md"
|
||||||
|
test -s "/tmp/$spec.md"
|
||||||
|
done
|
||||||
|
echo "Generated markdown successfully"
|
||||||
|
|
||||||
|
publish-report:
|
||||||
|
name: Publish report to GitHub Pages
|
||||||
|
needs: doctests
|
||||||
|
# Run even when tests fail — a failing run is exactly when you want to open
|
||||||
|
# the report. Skip on forks, where GITHUB_TOKEN can't push or comment.
|
||||||
|
if: ${{ always() && github.event.pull_request.head.repo.fork != true }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # push to the gh-pages branch
|
||||||
|
pull-requests: write # post/update the PR comment
|
||||||
|
|
||||||
|
# Serialize Pages pushes so two refs can't race on the gh-pages branch.
|
||||||
|
concurrency:
|
||||||
|
group: gh-pages-publish
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all reports
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
# No `name:` → downloads every artifact into artifacts/<name>/...
|
||||||
|
|
||||||
|
- name: Arrange site directory
|
||||||
|
id: arrange
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
BASE="pr-${{ github.event.pull_request.number }}"
|
||||||
|
else
|
||||||
|
BASE="main"
|
||||||
|
fi
|
||||||
|
echo "base=$BASE" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
mkdir -p "site/$BASE"
|
||||||
|
found=""
|
||||||
|
for os in ubuntu-latest; do
|
||||||
|
src="artifacts/blockchain-doctest-report-$os/index.html"
|
||||||
|
if [ -f "$src" ]; then
|
||||||
|
mkdir -p "site/$BASE/$os"
|
||||||
|
cp "$src" "site/$BASE/$os/index.html"
|
||||||
|
found="$found $os"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "found=$found" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Landing page for this ref linking to each OS report.
|
||||||
|
{
|
||||||
|
echo "<!doctype html><meta charset=utf-8>"
|
||||||
|
echo "<title>blockchain-module doc-test reports — $BASE</title>"
|
||||||
|
echo "<style>body{font:16px system-ui;margin:40px;max-width:640px}a{color:#2563eb}</style>"
|
||||||
|
echo "<h1>blockchain-module doc-test reports</h1>"
|
||||||
|
echo "<p><strong>$BASE</strong> · commit <code>${GITHUB_SHA::7}</code></p><ul>"
|
||||||
|
for os in ubuntu-latest; do
|
||||||
|
if [ -d "site/$BASE/$os" ]; then
|
||||||
|
echo "<li><a href=\"./$os/\">$os</a></li>"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "</ul>"
|
||||||
|
} > "site/$BASE/index.html"
|
||||||
|
|
||||||
|
- name: Deploy to gh-pages
|
||||||
|
if: steps.arrange.outputs.found != ''
|
||||||
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./site
|
||||||
|
keep_files: true # don't wipe other PRs' directories
|
||||||
|
commit_message: "Publish blockchain-module doc-test report for ${{ steps.arrange.outputs.base }} (${{ github.sha }})"
|
||||||
|
|
||||||
|
- name: Comment on PR with report links
|
||||||
|
if: ${{ github.event_name == 'pull_request' && steps.arrange.outputs.found != '' }}
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const base = "${{ steps.arrange.outputs.base }}";
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
const root = `https://${owner}.github.io/${repo}/${base}`;
|
||||||
|
const oses = "${{ steps.arrange.outputs.found }}".trim().split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
|
const links = oses.map(os => `- [\`${os}\` report](${root}/${os}/)`).join("\n");
|
||||||
|
const marker = "<!-- blockchain-doctest-report-links -->";
|
||||||
|
const body =
|
||||||
|
`${marker}\n` +
|
||||||
|
`### 📊 blockchain-module doc-test report\n\n` +
|
||||||
|
`This commit of the blockchain module, packaged as an \`.lgx\` and ` +
|
||||||
|
`driven through a logoscore daemon — config generation and a full ` +
|
||||||
|
`node lifecycle — rendered alongside the commands actually run and ` +
|
||||||
|
`their output (updated each run, commit \`${context.sha.slice(0,7)}\`):\n\n` +
|
||||||
|
`${links}\n\n` +
|
||||||
|
`_Pages can take a minute to update after the run finishes._`;
|
||||||
|
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner, repo, issue_number: context.issue.number, per_page: 100,
|
||||||
|
});
|
||||||
|
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number: context.issue.number, body });
|
||||||
|
}
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -16,3 +16,11 @@ state/
|
|||||||
# AI
|
# AI
|
||||||
.codex
|
.codex
|
||||||
|
|
||||||
|
# Doc-test output: generated artifacts are disposable, but keep the rendered
|
||||||
|
# .md that the docs embed.
|
||||||
|
doctests/outputs/*
|
||||||
|
!doctests/outputs/*.md
|
||||||
|
# Disposable scratch dir for doc-test previews (kept separate so it never
|
||||||
|
# clobbers the committed .md in doctests/outputs).
|
||||||
|
doctests/preview-outputs
|
||||||
|
doctest-local
|
||||||
|
|||||||
209
doctests/blockchain-module-config.test.yaml
Normal file
209
doctests/blockchain-module-config.test.yaml
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
name: "Generating and Inspecting a Logos Blockchain Node Config"
|
||||||
|
output: blockchain-module-config.md
|
||||||
|
release: ""
|
||||||
|
|
||||||
|
intro: |
|
||||||
|
`blockchain_module` wraps the [`logos-blockchain`](https://github.com/logos-blockchain/logos-blockchain)
|
||||||
|
node as a Logos module. Before a node can run it needs a **user config** — a YAML
|
||||||
|
file describing the keystore, the network ports, the bootstrap peers to sync
|
||||||
|
from, and where the node keeps its state, database and logs.
|
||||||
|
|
||||||
|
The module generates that file for you through `generate_user_config`, so the
|
||||||
|
operator never has to hand-write YAML. This doc-test drives that call through
|
||||||
|
the headless `logoscore` daemon and inspects what comes out, then shows what
|
||||||
|
happens when you try to generate a second config into the same instance.
|
||||||
|
|
||||||
|
> No node is started here — config generation is entirely local and needs no
|
||||||
|
> network. For starting and stopping a node see the companion
|
||||||
|
> [`blockchain-module-runtime`](./blockchain-module-runtime.md) doc-test.
|
||||||
|
|
||||||
|
what_you_build: "A generated `user_config.yaml` for a Logos blockchain node, produced through the module's `generate_user_config` call and inspected for the values that were asked for."
|
||||||
|
|
||||||
|
what_you_learn:
|
||||||
|
- How a Logos module is packaged as `.lgx` and installed with `lgpm`
|
||||||
|
- How to call a module's methods headlessly through the `logoscore` daemon
|
||||||
|
- What `generate_user_config` writes, and which knobs it accepts
|
||||||
|
- Why a second generation into the same instance is refused, and what to use instead
|
||||||
|
- Why the generated config's paths land under the module's own per-instance directory
|
||||||
|
|
||||||
|
prerequisites:
|
||||||
|
- |
|
||||||
|
**Nix** with flakes enabled (see [nixos.org](https://nixos.org/download.html)).
|
||||||
|
- "**A Linux or macOS machine.** Nothing here needs a display or a network peer."
|
||||||
|
|
||||||
|
sections:
|
||||||
|
- title: "Build the Logos daemon"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
Build the Logos runtime CLI from its published flake. The result outputs a
|
||||||
|
binary named `logoscore` under a symlinked directory named `./logos`.
|
||||||
|
`logoscore` is the headless frontend for
|
||||||
|
[`logos-liblogos`](https://github.com/logos-co/logos-liblogos) — it brings in
|
||||||
|
the whole module-runtime stack we need to load and call the module.
|
||||||
|
steps:
|
||||||
|
- title: "Build the CLI"
|
||||||
|
run: "nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos"
|
||||||
|
code_block: |
|
||||||
|
nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos
|
||||||
|
check_file: "logos/bin/logoscore"
|
||||||
|
post_text: |
|
||||||
|
The build produces `logos/bin/logoscore` plus bundled runtime libraries
|
||||||
|
and a `logos/modules/` directory containing the built-in
|
||||||
|
`capability_module` (required for the auth handshake when loading
|
||||||
|
modules).
|
||||||
|
|
||||||
|
- title: "Build the lgpm package manager"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
`lgpm` installs `.lgx` packages into a modules directory so the daemon can
|
||||||
|
discover them.
|
||||||
|
steps:
|
||||||
|
- title: "Build lgpm"
|
||||||
|
run: "nix build 'github:logos-co/logos-package-manager' --out-link ./lgpm"
|
||||||
|
code_block: |
|
||||||
|
nix build 'github:logos-co/logos-package-manager' --out-link ./lgpm
|
||||||
|
check_file: "lgpm/bin/lgpm"
|
||||||
|
|
||||||
|
- title: "Build and install the blockchain module"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
The module's flake exposes an `.lgx` package — the Logos distribution format.
|
||||||
|
Building it compiles the Qt plugin *and* the bundled `logos_blockchain`
|
||||||
|
native library it links against, so this step is the slow one.
|
||||||
|
steps:
|
||||||
|
- title: "Build the .lgx package"
|
||||||
|
run: "nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' --out-link ./blockchain-lgx"
|
||||||
|
code_block: |
|
||||||
|
nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' --out-link ./blockchain-lgx
|
||||||
|
|
||||||
|
- title: "Install it into a modules directory"
|
||||||
|
text: "Install the package so the daemon can find it:"
|
||||||
|
run: "./lgpm/bin/lgpm --modules-dir ./modules install --dir ./blockchain-lgx"
|
||||||
|
code_block: |
|
||||||
|
lgpm --modules-dir ./modules install --dir ./blockchain-lgx
|
||||||
|
expect_contains:
|
||||||
|
- "blockchain_module"
|
||||||
|
check_file: "modules/blockchain_module/manifest.json"
|
||||||
|
|
||||||
|
- title: "Load the module in the daemon"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
Start the daemon pointed at the modules directory, then load
|
||||||
|
`blockchain_module`. Loading only brings the plugin into the process — it
|
||||||
|
does **not** start a blockchain node.
|
||||||
|
steps:
|
||||||
|
- title: "Start the daemon"
|
||||||
|
run: "./logos/bin/logoscore stop >/dev/null 2>&1; sleep 2; ./logos/bin/logoscore daemon --modules-dir ./modules --persistence-path ./data >/dev/null 2>&1 &"
|
||||||
|
code_block: |
|
||||||
|
logoscore daemon --modules-dir ./modules --persistence-path ./data
|
||||||
|
|
||||||
|
- title: "Load the module"
|
||||||
|
run: "sleep 8; ./logos/bin/logoscore load-module blockchain_module"
|
||||||
|
code_block: |
|
||||||
|
logoscore load-module blockchain_module
|
||||||
|
expect_contains:
|
||||||
|
- "blockchain_module"
|
||||||
|
|
||||||
|
- title: "Inspect the methods it exposes"
|
||||||
|
text: |
|
||||||
|
`module-info` lists the `Q_INVOKABLE` methods — the same names you can
|
||||||
|
`call`. Note `generate_user_config`, `start`, `stop`, and the
|
||||||
|
`wallet_*` family:
|
||||||
|
run: "./logos/bin/logoscore module-info blockchain_module"
|
||||||
|
code_block: |
|
||||||
|
logoscore module-info blockchain_module
|
||||||
|
expect_contains:
|
||||||
|
- "generate_user_config"
|
||||||
|
- "start"
|
||||||
|
- "stop"
|
||||||
|
|
||||||
|
- title: "Generate a config with bootstrap peers"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
`generate_user_config` takes a JSON object of arguments. Every field is
|
||||||
|
optional — an empty object produces a working default config. Here we pass
|
||||||
|
the two that matter most for joining a network: the bootstrap peers to sync
|
||||||
|
from, and the ports to listen on.
|
||||||
|
|
||||||
|
`use_persistence_paths` routes the config — and the node's `state`, `db`
|
||||||
|
and `logs` directories — under the module's own per-instance directory,
|
||||||
|
which is always writable. That is the safe default.
|
||||||
|
steps:
|
||||||
|
- title: "Write the arguments"
|
||||||
|
file:
|
||||||
|
path: gen-args.json
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"initial_peers": [
|
||||||
|
"/ip4/65.109.51.37/udp/3000/quic-v1/p2p/12D3KooWFrouXfmrR4nsLMtE7wu15DoMJ6VtoUtHinREZCvbWHar"
|
||||||
|
],
|
||||||
|
"net_port": 3000,
|
||||||
|
"blend_port": 3001,
|
||||||
|
"output": "user_config.yaml",
|
||||||
|
"use_persistence_paths": true
|
||||||
|
}
|
||||||
|
|
||||||
|
- title: "Generate the config"
|
||||||
|
run: "./logos/bin/logoscore call blockchain_module generate_user_config @gen-args.json"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module generate_user_config @gen-args.json
|
||||||
|
expect_contains:
|
||||||
|
- '"success":true'
|
||||||
|
- "user_config.yaml"
|
||||||
|
post_text: |
|
||||||
|
The call returns the absolute path of the file it wrote — that path is
|
||||||
|
what you hand to `start`, as the runtime doc-test does.
|
||||||
|
|
||||||
|
`output` is worth passing even though it is optional. Omit it and the
|
||||||
|
call still reports success but returns an **empty** path, leaving you
|
||||||
|
nothing to start the node with.
|
||||||
|
|
||||||
|
- title: "Generating a second config into the same instance"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
A natural next move is to generate a *second* config — say a peerless one
|
||||||
|
with `skip_ibd: true` — alongside the first. That does **not** work, and the
|
||||||
|
error is worth seeing because it is the same one operators hit when they
|
||||||
|
re-run generation over an existing setup.
|
||||||
|
|
||||||
|
The keystore is created once per instance directory. A second
|
||||||
|
`generate_user_config` against the same instance refuses rather than
|
||||||
|
overwriting it:
|
||||||
|
steps:
|
||||||
|
- title: "Write the arguments"
|
||||||
|
file:
|
||||||
|
path: gen-args-solo.json
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"skip_ibd": true,
|
||||||
|
"net_port": 3100,
|
||||||
|
"blend_port": 3101,
|
||||||
|
"output": "solo-user-config.yaml",
|
||||||
|
"use_persistence_paths": true
|
||||||
|
}
|
||||||
|
|
||||||
|
- title: "Try to generate a second config"
|
||||||
|
run: "./logos/bin/logoscore call blockchain_module generate_user_config @gen-args-solo.json"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module generate_user_config @gen-args-solo.json
|
||||||
|
expect_contains:
|
||||||
|
- "Keystore file exists. Use `update` command."
|
||||||
|
post_text: |
|
||||||
|
Refusing is the right behaviour — silently regenerating a keystore would
|
||||||
|
discard the keys the previous config's node identity depends on. To
|
||||||
|
change an existing setup use `update_user_config`; to get a genuinely
|
||||||
|
separate configuration, use a separate instance directory.
|
||||||
|
|
||||||
|
This is also why `skip_ibd` is demonstrated in the companion
|
||||||
|
[`blockchain-module-runtime`](./blockchain-module-runtime.md) doc-test
|
||||||
|
rather than here: that spec generates exactly one config, with
|
||||||
|
`skip_ibd: true`, and then starts a node from it.
|
||||||
|
|
||||||
|
- title: "Shut down"
|
||||||
|
step: true
|
||||||
|
text: "Stop the daemon. No node was ever started, so there is nothing else to clean up."
|
||||||
|
steps:
|
||||||
|
- title: "Stop the daemon"
|
||||||
|
run: "./logos/bin/logoscore stop"
|
||||||
|
code_block: |
|
||||||
|
logoscore stop
|
||||||
321
doctests/blockchain-module-runtime.test.yaml
Normal file
321
doctests/blockchain-module-runtime.test.yaml
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
name: "Starting and Stopping a Logos Blockchain Node"
|
||||||
|
output: blockchain-module-runtime.md
|
||||||
|
release: ""
|
||||||
|
|
||||||
|
intro: |
|
||||||
|
This doc-test takes the config produced in
|
||||||
|
[`blockchain-module-config`](./blockchain-module-config.md) and actually runs a
|
||||||
|
node with it: start, query the chain state, read the wallet's known addresses,
|
||||||
|
and stop again.
|
||||||
|
|
||||||
|
It runs the node **with no bootstrap peers** (`skip_ibd: true`). That is
|
||||||
|
deliberate. A node configured with peers spends its startup trying to fetch a
|
||||||
|
chain tip from each one, and if none answer it aborts bootstrap — so a test
|
||||||
|
that depends on a live network is a test that fails whenever the network moves,
|
||||||
|
the peers rotate, or the node's chainsync protocol version changes. With IBD
|
||||||
|
skipped the node comes up against its own genesis state in well under a second
|
||||||
|
and every assertion below is deterministic.
|
||||||
|
|
||||||
|
What that buys you is coverage of the whole local lifecycle: every service
|
||||||
|
reaching ready, the wallet initialising, the chain state being queryable, and a
|
||||||
|
clean shutdown. What it does not cover is syncing from a real network — see the
|
||||||
|
note at the end.
|
||||||
|
|
||||||
|
what_you_build: "A running Logos blockchain node, started headlessly from a generated config, queried for its chain and wallet state, and shut down cleanly."
|
||||||
|
|
||||||
|
what_you_learn:
|
||||||
|
- How to start and stop a blockchain node through the module's API
|
||||||
|
- Which services the node brings up, and what "ready" looks like for each
|
||||||
|
- How to read chain state with `get_cryptarchia_info`
|
||||||
|
- How the wallet exposes its known addresses and their balances
|
||||||
|
- Why skipping IBD is the right choice for a reproducible test, and what it leaves untested
|
||||||
|
|
||||||
|
prerequisites:
|
||||||
|
- |
|
||||||
|
**Nix** with flakes enabled (see [nixos.org](https://nixos.org/download.html)).
|
||||||
|
- "**A Linux or macOS machine.** No display and no network peers are required."
|
||||||
|
|
||||||
|
sections:
|
||||||
|
- title: "Build the tools and install the module"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
Same preparation as the [config doc-test](./blockchain-module-config.md):
|
||||||
|
the runtime CLI, the package manager, and this commit of the module
|
||||||
|
packaged as an `.lgx`. Building the `.lgx` compiles the bundled
|
||||||
|
`logos_blockchain` native library, so it is the slow step.
|
||||||
|
steps:
|
||||||
|
- title: "Build the Logos daemon CLI"
|
||||||
|
run: "nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos"
|
||||||
|
code_block: |
|
||||||
|
nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos
|
||||||
|
check_file: "logos/bin/logoscore"
|
||||||
|
|
||||||
|
- title: "Build lgpm"
|
||||||
|
run: "nix build 'github:logos-co/logos-package-manager' --out-link ./lgpm"
|
||||||
|
code_block: |
|
||||||
|
nix build 'github:logos-co/logos-package-manager' --out-link ./lgpm
|
||||||
|
check_file: "lgpm/bin/lgpm"
|
||||||
|
|
||||||
|
- title: "Build the module's .lgx package"
|
||||||
|
run: "nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' --out-link ./blockchain-lgx"
|
||||||
|
code_block: |
|
||||||
|
nix build 'github:logos-blockchain/logos-blockchain-module{release}#lgx' --out-link ./blockchain-lgx
|
||||||
|
|
||||||
|
- title: "Install it into a modules directory"
|
||||||
|
run: "./lgpm/bin/lgpm --modules-dir ./modules install --dir ./blockchain-lgx"
|
||||||
|
code_block: |
|
||||||
|
lgpm --modules-dir ./modules install --dir ./blockchain-lgx
|
||||||
|
expect_contains:
|
||||||
|
- "blockchain_module"
|
||||||
|
check_file: "modules/blockchain_module/manifest.json"
|
||||||
|
|
||||||
|
- title: "Start the daemon and load the module"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
Run the daemon and load the plugin. Loading brings the plugin into the
|
||||||
|
process — it does **not** start a blockchain node.
|
||||||
|
steps:
|
||||||
|
- title: "Start the daemon"
|
||||||
|
run: "./logos/bin/logoscore stop >/dev/null 2>&1; sleep 2; ./logos/bin/logoscore daemon --modules-dir ./modules --persistence-path ./data >/dev/null 2>&1 &"
|
||||||
|
code_block: |
|
||||||
|
logoscore daemon --modules-dir ./modules --persistence-path ./data
|
||||||
|
|
||||||
|
- title: "Load the module"
|
||||||
|
run: "sleep 8; ./logos/bin/logoscore load-module blockchain_module"
|
||||||
|
code_block: |
|
||||||
|
logoscore load-module blockchain_module
|
||||||
|
expect_contains:
|
||||||
|
- "blockchain_module"
|
||||||
|
|
||||||
|
- title: "Confirm the node is not running yet"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
Every node-dependent call guards on a running node and reports so plainly.
|
||||||
|
This is worth seeing before starting, because it is the error you will meet
|
||||||
|
most often in practice:
|
||||||
|
steps:
|
||||||
|
- title: "Ask for chain info with no node"
|
||||||
|
run: "./logos/bin/logoscore call blockchain_module get_cryptarchia_info"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module get_cryptarchia_info
|
||||||
|
expect_contains:
|
||||||
|
- "The node is not running."
|
||||||
|
|
||||||
|
- title: "Generate a solo config"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
Generate a config with no bootstrap peers. `skip_ibd` empties the IBD peer
|
||||||
|
list, so the node will skip Initial Block Download rather than attempt to
|
||||||
|
sync from anyone.
|
||||||
|
steps:
|
||||||
|
- title: "Write the arguments"
|
||||||
|
file:
|
||||||
|
path: runtime-args.json
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"skip_ibd": true,
|
||||||
|
"net_port": 3200,
|
||||||
|
"blend_port": 3201,
|
||||||
|
"output": "user_config.yaml",
|
||||||
|
"use_persistence_paths": true
|
||||||
|
}
|
||||||
|
|
||||||
|
- title: "Generate it"
|
||||||
|
run: "./logos/bin/logoscore call blockchain_module generate_user_config @runtime-args.json"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module generate_user_config @runtime-args.json
|
||||||
|
expect_contains:
|
||||||
|
- '"success":true'
|
||||||
|
- "user_config.yaml"
|
||||||
|
post_text: |
|
||||||
|
`output` matters here. Omit it and the call still succeeds but returns an
|
||||||
|
empty path, leaving you nothing to hand to `start`. Give it a relative
|
||||||
|
path and the module resolves it under the instance's persistence
|
||||||
|
directory and returns the absolute path it wrote.
|
||||||
|
|
||||||
|
- title: "Confirm skip_ibd emptied the peer list"
|
||||||
|
text: |
|
||||||
|
`skip_ibd` writes an empty IBD peer list into the config, which is what
|
||||||
|
makes the node skip bootstrap rather than try to sync:
|
||||||
|
run: "CFG=$(find . $HOME/.logoscore -name user_config.yaml -path '*blockchain_module*' 2>/dev/null | head -1); echo using $CFG; grep -A1 'ibd:' $CFG"
|
||||||
|
code_block: |
|
||||||
|
grep -A1 'ibd:' <generated>/user_config.yaml
|
||||||
|
expect_contains:
|
||||||
|
- "peers: []"
|
||||||
|
|
||||||
|
- title: "Shorten the bootstrap period"
|
||||||
|
text: |
|
||||||
|
A generated config carries `prolonged_bootstrap_period: '3600.000000000'`
|
||||||
|
— the node stays in `Bootstrapping` mode for a full hour before
|
||||||
|
switching to `Online`. That is sensible for joining a real network and
|
||||||
|
useless for a test, so patch it down. `generate_user_config` does not
|
||||||
|
expose this knob, so we edit the YAML directly.
|
||||||
|
run: "CFG=$(find . $HOME/.logoscore -name user_config.yaml -path '*blockchain_module*' 2>/dev/null | head -1); sed \"s/prolonged_bootstrap_period: '3600.000000000'/prolonged_bootstrap_period: '5.000000000'/\" $CFG > $CFG.tmp && mv $CFG.tmp $CFG && grep -n prolonged_bootstrap_period $CFG"
|
||||||
|
code_block: |
|
||||||
|
# sed -i differs between GNU and BSD/macOS, so write and move instead.
|
||||||
|
sed "s/prolonged_bootstrap_period: '3600.000000000'/prolonged_bootstrap_period: '5.000000000'/" \
|
||||||
|
<generated>/user_config.yaml > tmp && mv tmp <generated>/user_config.yaml
|
||||||
|
expect_contains:
|
||||||
|
- "prolonged_bootstrap_period: '5.000000000'"
|
||||||
|
|
||||||
|
- title: "Start the node"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
`start` takes the config path and a deployment name. Passing an empty
|
||||||
|
deployment uses the built-in default.
|
||||||
|
|
||||||
|
The node brings its services up in order — tracing, storage, network,
|
||||||
|
cryptarchia, wallet, mempool. Because IBD is skipped there is no bootstrap
|
||||||
|
phase to wait through, so `start` returns once the runtime is up.
|
||||||
|
steps:
|
||||||
|
- title: "Start it"
|
||||||
|
run: "CFG=$(find . $HOME/.logoscore -name user_config.yaml -path '*blockchain_module*' 2>/dev/null | head -1); ./logos/bin/logoscore call blockchain_module start $CFG ''"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module start <generated>/user_config.yaml ""
|
||||||
|
expect_contains:
|
||||||
|
- '"success":true'
|
||||||
|
post_text: |
|
||||||
|
The daemon's output records the startup sequence. Three lines are worth
|
||||||
|
knowing, because they are the ones that distinguish a healthy peerless
|
||||||
|
start from the failures you meet in practice:
|
||||||
|
|
||||||
|
```
|
||||||
|
chain::service: genesis time is already in the past: finishing AwaitingGenesisTime phase with no-op
|
||||||
|
bootstrap::ibd: Skipping IBD as no peers are configured
|
||||||
|
chain_network_service: Initial Block Download completed successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
The first confirms the generated config's genesis is already live — a
|
||||||
|
config whose genesis is in the *future* parks the node in
|
||||||
|
`AwaitingGenesisTime` indefinitely, with no error to explain it. The
|
||||||
|
second and third are `skip_ibd` doing its job: bootstrap is bypassed
|
||||||
|
rather than attempted and failed.
|
||||||
|
|
||||||
|
- title: "Query the chain"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
With the node up, `get_cryptarchia_info` reports the consensus state: the
|
||||||
|
current tip, the last immutable block, and the slot. On a freshly started
|
||||||
|
node with nothing synced, tip and LIB are both genesis.
|
||||||
|
steps:
|
||||||
|
- title: "Read chain state"
|
||||||
|
run: "./logos/bin/logoscore call blockchain_module get_cryptarchia_info"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module get_cryptarchia_info
|
||||||
|
expect_contains:
|
||||||
|
- '"success":true'
|
||||||
|
- '"mode\":\"Bootstrapping'
|
||||||
|
post_text: |
|
||||||
|
The reply carries `height`, `slot`, `tip`, `lib` and `mode`. Immediately
|
||||||
|
after start the mode is `Bootstrapping`.
|
||||||
|
|
||||||
|
- title: "Wait for the chain to come Online"
|
||||||
|
text: |
|
||||||
|
After the (shortened) prolonged-bootstrap period the mode flips to
|
||||||
|
`Online`. This is the transition the blend service is waiting on when it
|
||||||
|
logs *"Waiting for chain to become Online mode"*.
|
||||||
|
run: "sleep 15 && ./logos/bin/logoscore call blockchain_module get_cryptarchia_info"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module get_cryptarchia_info
|
||||||
|
expect_contains:
|
||||||
|
- '"mode\":\"Online'
|
||||||
|
post_text: |
|
||||||
|
Note `height` and `slot` are still `0`. A node with no peers reaches
|
||||||
|
`Online` but does not by itself start producing blocks — see the closing
|
||||||
|
section.
|
||||||
|
|
||||||
|
- title: "Read the wallet"
|
||||||
|
step: true
|
||||||
|
text: |
|
||||||
|
The wallet is initialised from the keystore in the generated config, so it
|
||||||
|
knows its addresses immediately — no sync required. Balances are a separate
|
||||||
|
lookup against the ledger, and on an unsynced chain they reflect genesis
|
||||||
|
state only.
|
||||||
|
steps:
|
||||||
|
- title: "List known addresses"
|
||||||
|
run: "./logos/bin/logoscore call blockchain_module wallet_get_known_addresses"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module wallet_get_known_addresses
|
||||||
|
expect_contains:
|
||||||
|
- '"success":true'
|
||||||
|
post_text: |
|
||||||
|
Five addresses, returned as a JSON array. These are what the module's UI
|
||||||
|
lists in its Accounts panel, and the ones it then queries balances for
|
||||||
|
one at a time.
|
||||||
|
|
||||||
|
- title: "Ask for a balance before the chain has synced"
|
||||||
|
text: |
|
||||||
|
Feed one of those addresses straight back into `wallet_get_balance`.
|
||||||
|
On a node that has not synced, the call **fails** — even though the
|
||||||
|
address came from the module's own list a moment earlier:
|
||||||
|
run: "./logos/bin/logoscore call blockchain_module wallet_get_balance \"$(./logos/bin/logoscore call blockchain_module wallet_get_known_addresses | grep -o '[0-9a-f]\\{64\\}' | head -1)\""
|
||||||
|
code_block: |
|
||||||
|
ADDR=$(logoscore call blockchain_module wallet_get_known_addresses | grep -o '[0-9a-f]\{64\}' | head -1)
|
||||||
|
logoscore call blockchain_module wallet_get_balance "$ADDR"
|
||||||
|
expect_contains:
|
||||||
|
- "Unknown wallet address."
|
||||||
|
post_text: |
|
||||||
|
Two different notions of "known" are in play: the wallet holds the
|
||||||
|
*key*, but the *ledger* has no entry for that address until the chain
|
||||||
|
has progressed far enough to contain one. The error wording describes
|
||||||
|
the second and reads like the first, which is why it looks — wrongly —
|
||||||
|
like a malformed address.
|
||||||
|
|
||||||
|
This is asserted here deliberately. It is the current behaviour, it is
|
||||||
|
reachable in seconds without a network, and it is the single most
|
||||||
|
confusing thing a new operator meets. If the node later returns `0` or a
|
||||||
|
distinct "not synced" error instead, this assertion should fail and be
|
||||||
|
updated — which is exactly what a doc-test is for.
|
||||||
|
|
||||||
|
- title: "Stop the node"
|
||||||
|
step: true
|
||||||
|
text: "Shut the node down. `stop` is idempotent in the sense that stopping an already-stopped node reports plainly rather than failing hard."
|
||||||
|
steps:
|
||||||
|
- title: "Stop it"
|
||||||
|
run: "./logos/bin/logoscore call blockchain_module stop"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module stop
|
||||||
|
expect_contains:
|
||||||
|
- '"success":true'
|
||||||
|
|
||||||
|
- title: "Confirm it is down"
|
||||||
|
run: "./logos/bin/logoscore call blockchain_module get_cryptarchia_info"
|
||||||
|
code_block: |
|
||||||
|
logoscore call blockchain_module get_cryptarchia_info
|
||||||
|
expect_contains:
|
||||||
|
- "The node is not running."
|
||||||
|
|
||||||
|
- title: "Stop the daemon"
|
||||||
|
run: "./logos/bin/logoscore stop"
|
||||||
|
code_block: |
|
||||||
|
logoscore stop
|
||||||
|
|
||||||
|
- title: "What this does not cover"
|
||||||
|
text: |
|
||||||
|
Skipping IBD keeps this doc-test fast and deterministic. What it leaves out
|
||||||
|
was established by running it, not assumed:
|
||||||
|
|
||||||
|
- **The chain does not advance.** A peerless node reaches `Online` and then
|
||||||
|
stays at `height: 0`, `slot: 0` indefinitely — observed over several
|
||||||
|
minutes. Genesis being in the past and bootstrap completing are not
|
||||||
|
sufficient to make a lone node produce blocks. So **block streams,
|
||||||
|
non-zero balances and transfers cannot be asserted here**, and this
|
||||||
|
doc-test does not pretend to.
|
||||||
|
- **Syncing from peers.** Bootstrap, block download and the chainsync
|
||||||
|
protocol handshake are never exercised. A node whose peers speak a
|
||||||
|
different chainsync version fails with `AllPeersFailed` — a real and
|
||||||
|
common failure, reachable only against a live network.
|
||||||
|
|
||||||
|
Covering transactions therefore needs either a multi-node fixture or
|
||||||
|
whatever the node uses to make a single node leader-eligible. The node's own
|
||||||
|
cucumber suite (`tests/src/cucumber`) already runs manual-node scenarios
|
||||||
|
with deployment overrides such as
|
||||||
|
`time.chain_start_time = now_plus_seconds(0)`, and is the better place to
|
||||||
|
look before attempting it here.
|
||||||
|
|
||||||
|
Two knobs that are *not* reachable through `generate_user_config` and had to
|
||||||
|
be patched into the YAML directly — worth exposing on the FFI if this
|
||||||
|
pattern is repeated:
|
||||||
|
|
||||||
|
- `prolonged_bootstrap_period` (the hour-long `Bootstrapping` hold)
|
||||||
|
- `time.chain_start_time` (deployment-side, controls genesis)
|
||||||
28
doctests/run.sh
Executable file
28
doctests/run.sh
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Run this repo's doc-tests end-to-end and regenerate their Markdown into
|
||||||
|
# ./outputs/. The runner is the shared `doctest` CLI
|
||||||
|
# (https://github.com/logos-co/logos-doctest), invoked via its flake.
|
||||||
|
#
|
||||||
|
# Override the runner with DOCTEST, e.g. to use a local checkout:
|
||||||
|
# DOCTEST="nix run path:../../logos-doctest --" ./run.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
read -r -a DOCTEST <<< "${DOCTEST:-nix run github:logos-co/logos-doctest --}"
|
||||||
|
|
||||||
|
if [ -e outputs ]; then chmod -R u+w outputs 2>/dev/null || true; fi
|
||||||
|
rm -rf outputs && mkdir -p outputs
|
||||||
|
|
||||||
|
for spec in *.test.yaml; do
|
||||||
|
name="$(basename "${spec%.test.yaml}")"
|
||||||
|
echo "==> Running ${spec}"
|
||||||
|
"${DOCTEST[@]}" run "${spec}" --verbose --output-dir ./outputs/
|
||||||
|
echo "==> Generating outputs/${name}.md"
|
||||||
|
"${DOCTEST[@]}" generate "${spec}" -o "outputs/${name}.md"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Cleaning build artifacts from outputs/ (keeps .md and images/)"
|
||||||
|
"${DOCTEST[@]}" clean ./outputs --verbose 2>/dev/null || true
|
||||||
|
echo "==> Done."
|
||||||
41150
flake.lock
generated
41150
flake.lock
generated
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,8 @@
|
|||||||
description = "Logos Blockchain Module - Qt6 Plugin";
|
description = "Logos Blockchain Module - Qt6 Plugin";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
logos-module-builder.url = "github:logos-co/logos-module-builder?ref=38ddf92c1f240f4e420d300a1fbabb1609d5db01";
|
logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.6";
|
||||||
logos-blockchain.url = "github:logos-blockchain/logos-blockchain?ref=0.2.0";
|
logos-blockchain.url = "github:logos-blockchain/logos-blockchain?ref=0.2.1";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs@{ logos-module-builder, ... }:
|
outputs = inputs@{ logos-module-builder, ... }:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "blockchain_module",
|
"name": "blockchain_module",
|
||||||
"display_name": "Blockchain Module",
|
"display_name": "Blockchain Module",
|
||||||
"version": "0.2.0",
|
"version": "0.0.999",
|
||||||
"description": "Logos blockchain node for logos-core",
|
"description": "Logos blockchain node for logos-core",
|
||||||
"author": "Logos Blockchain Team",
|
"author": "Logos Blockchain Team",
|
||||||
"type": "core",
|
"type": "core",
|
||||||
|
|||||||
@ -144,7 +144,7 @@ namespace {
|
|||||||
std::string state_path_data;
|
std::string state_path_data;
|
||||||
std::string storage_path_data;
|
std::string storage_path_data;
|
||||||
std::string logs_path_data;
|
std::string logs_path_data;
|
||||||
bool ibd_val;
|
bool skip_ibd_val;
|
||||||
std::string log_filter_data;
|
std::string log_filter_data;
|
||||||
std::string kms_file_data;
|
std::string kms_file_data;
|
||||||
|
|
||||||
@ -235,12 +235,12 @@ namespace {
|
|||||||
ffi_args.logs_path = nullptr;
|
ffi_args.logs_path = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ibd (bool -> const bool*)
|
// skip_ibd (bool -> const bool*)
|
||||||
if (args.contains("ibd") && args["ibd"].is_boolean()) {
|
if (args.contains("skip_ibd") && args["skip_ibd"].is_boolean()) {
|
||||||
ibd_val = args["ibd"].get<bool>();
|
skip_ibd_val = args["skip_ibd"].get<bool>();
|
||||||
ffi_args.ibd = &ibd_val;
|
ffi_args.skip_ibd = &skip_ibd_val;
|
||||||
} else {
|
} else {
|
||||||
ffi_args.ibd = nullptr;
|
ffi_args.skip_ibd = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// log_filter (string -> const char*)
|
// log_filter (string -> const char*)
|
||||||
@ -263,15 +263,45 @@ namespace {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void LogosBlockchainModule::on_new_block_callback(const char* block) {
|
void LogosBlockchainModule::on_new_block_callback(const char* block) {
|
||||||
if (s_instance) {
|
if (!s_instance || !block) {
|
||||||
fprintf(stderr, "Received new block: %s\n", block);
|
return;
|
||||||
json j;
|
|
||||||
j["block"] = std::string(block);
|
|
||||||
s_instance->newBlock(j.dump());
|
|
||||||
// SAFETY:
|
|
||||||
// We are getting an owned pointer here which is freed after this callback is called, so there is no need to
|
|
||||||
// free the resource here as we are copying the data!
|
|
||||||
}
|
}
|
||||||
|
fprintf(stderr, "Received new block: %s\n", block);
|
||||||
|
json j;
|
||||||
|
j["block"] = std::string(block);
|
||||||
|
s_instance->newBlock(j.dump());
|
||||||
|
// SAFETY:
|
||||||
|
// We are getting an owned pointer here which is freed after this callback is called, so there is no need to
|
||||||
|
// free the resource here as we are copying the data!
|
||||||
|
}
|
||||||
|
|
||||||
|
// The stream callbacks pass the FFI's JSON through unwrapped (it is already a
|
||||||
|
// complete JSON document with the node's HTTP stream schema). A NULL pointer
|
||||||
|
// means the stream ended; it is forwarded as the JSON literal `null` so
|
||||||
|
// termination stays in-band on the same event.
|
||||||
|
|
||||||
|
void LogosBlockchainModule::on_processed_block_callback(const char* event) {
|
||||||
|
if (!s_instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event) {
|
||||||
|
fprintf(stderr, "Processed block stream ended.\n");
|
||||||
|
s_instance->processedBlock("null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_instance->processedBlock(std::string(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogosBlockchainModule::on_lib_block_callback(const char* event) {
|
||||||
|
if (!s_instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event) {
|
||||||
|
fprintf(stderr, "LIB block stream ended.\n");
|
||||||
|
s_instance->libBlock("null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_instance->libBlock(std::string(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
LogosBlockchainModule::LogosBlockchainModule() {
|
LogosBlockchainModule::LogosBlockchainModule() {
|
||||||
@ -414,7 +444,15 @@ StdLogosResult LogosBlockchainModule::start(const std::string& config_path, cons
|
|||||||
|
|
||||||
s_instance = this;
|
s_instance = this;
|
||||||
OperationStatus subscribe_status = subscribe_to_new_blocks(node, on_new_block_callback);
|
OperationStatus subscribe_status = subscribe_to_new_blocks(node, on_new_block_callback);
|
||||||
return result::from_operation_status(subscribe_status);
|
if (!is_ok(&subscribe_status)) {
|
||||||
|
return result::err(operation_status::take_message(subscribe_status));
|
||||||
|
}
|
||||||
|
OperationStatus processed_status = subscribe_to_processed_blocks(node, on_processed_block_callback);
|
||||||
|
if (!is_ok(&processed_status)) {
|
||||||
|
return result::err(operation_status::take_message(processed_status));
|
||||||
|
}
|
||||||
|
OperationStatus lib_status = subscribe_to_lib_blocks(node, on_lib_block_callback);
|
||||||
|
return result::from_operation_status(lib_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
StdLogosResult LogosBlockchainModule::stop() {
|
StdLogosResult LogosBlockchainModule::stop() {
|
||||||
@ -425,7 +463,7 @@ StdLogosResult LogosBlockchainModule::stop() {
|
|||||||
|
|
||||||
s_instance = nullptr;
|
s_instance = nullptr;
|
||||||
|
|
||||||
OperationStatus status = stop_node(node);
|
OperationStatus status = shutdown_node(node);
|
||||||
if (!is_ok(&status)) {
|
if (!is_ok(&status)) {
|
||||||
fprintf(stderr, "Could not stop the node: %s\n", operation_status::take_message(status).c_str());
|
fprintf(stderr, "Could not stop the node: %s\n", operation_status::take_message(status).c_str());
|
||||||
}
|
}
|
||||||
@ -906,6 +944,31 @@ StdLogosResult LogosBlockchainModule::channel_deposit_with_notes(
|
|||||||
return result::ok(bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), ADDRESS_BYTES));
|
return result::ok(bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), ADDRESS_BYTES));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StdLogosResult LogosBlockchainModule::get_channel_state(const std::string& channel_id_hex) const {
|
||||||
|
if (!node) {
|
||||||
|
return result::err("The node is not running.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<uint8_t> bytes = parse_address_hex(channel_id_hex);
|
||||||
|
if (bytes.empty() || static_cast<int>(bytes.size()) != ADDRESS_BYTES) {
|
||||||
|
return result::err("Invalid channel_id (64 hex characters required).");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [value, error] = ::get_channel_state(node, bytes.data());
|
||||||
|
if (!is_ok(&error)) {
|
||||||
|
return result::err(operation_status::take_message(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out(value);
|
||||||
|
OperationStatus free_status = free_cstring(value);
|
||||||
|
if (!is_ok(&free_status)) {
|
||||||
|
fprintf(
|
||||||
|
stderr, "Failed to free channel state string: %s\n", operation_status::take_message(free_status).c_str()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result::ok(std::move(out));
|
||||||
|
}
|
||||||
|
|
||||||
StdLogosResult LogosBlockchainModule::wallet_get_claimable_vouchers() const {
|
StdLogosResult LogosBlockchainModule::wallet_get_claimable_vouchers() const {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return result::err("The node is not running.");
|
return result::err("The node is not running.");
|
||||||
@ -936,26 +999,50 @@ StdLogosResult LogosBlockchainModule::wallet_get_claimable_vouchers() const {
|
|||||||
return result::ok(obj.dump());
|
return result::ok(obj.dump());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StdLogosResult LogosBlockchainModule::wallet_fund_tx(const std::string& request_json) const {
|
||||||
|
if (!node) {
|
||||||
|
return result::err("The node is not running.");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [value, error] = ::wallet_fund_tx(node, request_json.c_str());
|
||||||
|
if (!is_ok(&error)) {
|
||||||
|
return result::err(operation_status::take_message(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out(value);
|
||||||
|
OperationStatus free_status = free_cstring(value);
|
||||||
|
if (!is_ok(&free_status)) {
|
||||||
|
fprintf(stderr, "Failed to free funded tx string: %s\n", operation_status::take_message(free_status).c_str());
|
||||||
|
}
|
||||||
|
return result::ok(std::move(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
|
||||||
|
StdLogosResult LogosBlockchainModule::submit_signed_transaction(const std::string& signed_tx_json) const {
|
||||||
|
if (!node) {
|
||||||
|
return result::err("The node is not running.");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [value, error] = ::submit_signed_transaction(node, signed_tx_json.c_str());
|
||||||
|
if (!is_ok(&error)) {
|
||||||
|
return result::err(operation_status::take_message(error));
|
||||||
|
}
|
||||||
|
return result::ok(bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), TX_HASH_BYTES));
|
||||||
|
}
|
||||||
|
|
||||||
// Blend
|
// Blend
|
||||||
|
|
||||||
StdLogosResult LogosBlockchainModule::blend_join_as_core_node(
|
StdLogosResult LogosBlockchainModule::blend_join_as_core_node(
|
||||||
const std::string& provider_id_hex,
|
const std::string& locator,
|
||||||
const std::string& zk_id_hex,
|
const std::string& locked_note_id_hex
|
||||||
const std::string& locked_note_id_hex,
|
|
||||||
const std::vector<std::string>& locators
|
|
||||||
) const {
|
) const {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return result::err("The node is not running.");
|
return result::err("The node is not running.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<uint8_t> provider_id_bytes = parse_address_hex(provider_id_hex);
|
if (locator.empty()) {
|
||||||
if (provider_id_bytes.empty() || static_cast<int>(provider_id_bytes.size()) != ADDRESS_BYTES) {
|
return result::err("Invalid locator (must not be empty).");
|
||||||
return result::err("Invalid provider_id_hex (64 hex characters required).");
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<uint8_t> zk_id_bytes = parse_address_hex(zk_id_hex);
|
|
||||||
if (zk_id_bytes.empty() || static_cast<int>(zk_id_bytes.size()) != ADDRESS_BYTES) {
|
|
||||||
return result::err("Invalid zk_id_hex (64 hex characters required).");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<uint8_t> locked_note_id_bytes = parse_address_hex(locked_note_id_hex);
|
const std::vector<uint8_t> locked_note_id_bytes = parse_address_hex(locked_note_id_hex);
|
||||||
@ -963,21 +1050,7 @@ StdLogosResult LogosBlockchainModule::blend_join_as_core_node(
|
|||||||
return result::err("Invalid locked_note_id_hex (64 hex characters required).");
|
return result::err("Invalid locked_note_id_hex (64 hex characters required).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// locators_ptrs holds raw pointers into the std::strings (valid as long as `locators` lives).
|
auto [value, error] = ::blend_join_as_core_node(node, locator.c_str(), locked_note_id_bytes.data());
|
||||||
std::vector<const char*> locators_ptrs;
|
|
||||||
locators_ptrs.reserve(locators.size());
|
|
||||||
for (const std::string& locator : locators) {
|
|
||||||
locators_ptrs.push_back(locator.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto [value, error] = ::blend_join_as_core_node(
|
|
||||||
node,
|
|
||||||
provider_id_bytes.data(),
|
|
||||||
zk_id_bytes.data(),
|
|
||||||
locked_note_id_bytes.data(),
|
|
||||||
locators_ptrs.data(),
|
|
||||||
locators_ptrs.size()
|
|
||||||
);
|
|
||||||
if (!is_ok(&error)) {
|
if (!is_ok(&error)) {
|
||||||
return result::err(operation_status::take_message(error));
|
return result::err(operation_status::take_message(error));
|
||||||
}
|
}
|
||||||
@ -1067,10 +1140,21 @@ StdLogosResult LogosBlockchainModule::get_cryptarchia_info() const {
|
|||||||
|
|
||||||
json obj;
|
json obj;
|
||||||
obj["lib"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value->lib), ADDRESS_BYTES);
|
obj["lib"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value->lib), ADDRESS_BYTES);
|
||||||
|
obj["lib_slot"] = static_cast<int64_t>(value->lib_slot);
|
||||||
obj["tip"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value->tip), ADDRESS_BYTES);
|
obj["tip"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value->tip), ADDRESS_BYTES);
|
||||||
obj["slot"] = static_cast<int64_t>(value->slot);
|
obj["slot"] = static_cast<int64_t>(value->slot);
|
||||||
obj["height"] = static_cast<int64_t>(value->height);
|
obj["height"] = static_cast<int64_t>(value->height);
|
||||||
obj["mode"] = (value->mode == State::Online) ? "Online" : "Bootstrapping";
|
switch (value->mode) {
|
||||||
|
case State::Online:
|
||||||
|
obj["mode"] = "Online";
|
||||||
|
break;
|
||||||
|
case State::NotStarted:
|
||||||
|
obj["mode"] = "NotStarted";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
obj["mode"] = "Bootstrapping";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
OperationStatus free_status = free_cryptarchia_info(value);
|
OperationStatus free_status = free_cryptarchia_info(value);
|
||||||
if (!is_ok(&free_status)) {
|
if (!is_ok(&free_status)) {
|
||||||
@ -1078,3 +1162,53 @@ StdLogosResult LogosBlockchainModule::get_cryptarchia_info() const {
|
|||||||
}
|
}
|
||||||
return result::ok(obj.dump());
|
return result::ok(obj.dump());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StdLogosResult LogosBlockchainModule::get_block_events(const std::string& header_id_hex) const {
|
||||||
|
if (!node) {
|
||||||
|
return result::err("The node is not running.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<uint8_t> bytes = parse_address_hex(header_id_hex);
|
||||||
|
if (bytes.empty() || static_cast<int>(bytes.size()) != ADDRESS_BYTES) {
|
||||||
|
return result::err("Header ID must be 64 hex characters (32 bytes).");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [value, error] = ::get_block_events(node, reinterpret_cast<const HeaderId*>(bytes.data()));
|
||||||
|
if (!is_ok(&error)) {
|
||||||
|
return result::err(operation_status::take_message(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out(value);
|
||||||
|
OperationStatus free_status = free_cstring(value);
|
||||||
|
if (!is_ok(&free_status)) {
|
||||||
|
fprintf(
|
||||||
|
stderr, "Failed to free block events string: %s\n", operation_status::take_message(free_status).c_str()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result::ok(std::move(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time
|
||||||
|
|
||||||
|
StdLogosResult LogosBlockchainModule::get_time_info() const {
|
||||||
|
if (!node) {
|
||||||
|
return result::err("The node is not running.");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [value, error] = ::get_time_info(node);
|
||||||
|
if (!is_ok(&error)) {
|
||||||
|
return result::err(operation_status::take_message(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
json obj;
|
||||||
|
obj["slot_duration_ms"] = static_cast<int64_t>(value->slot_duration_ms);
|
||||||
|
obj["genesis_time_unix_ms"] = value->genesis_time_unix_ms;
|
||||||
|
obj["current_slot"] = static_cast<int64_t>(value->current_slot);
|
||||||
|
obj["current_epoch"] = value->current_epoch;
|
||||||
|
|
||||||
|
OperationStatus free_status = free_time_info(value);
|
||||||
|
if (!is_ok(&free_status)) {
|
||||||
|
fprintf(stderr, "Failed to free time info: %s\n", operation_status::take_message(free_status).c_str());
|
||||||
|
}
|
||||||
|
return result::ok(obj.dump());
|
||||||
|
}
|
||||||
|
|||||||
@ -99,6 +99,16 @@ public:
|
|||||||
) const;
|
) const;
|
||||||
[[nodiscard]] StdLogosResult leader_claim() const;
|
[[nodiscard]] StdLogosResult leader_claim() const;
|
||||||
[[nodiscard]] StdLogosResult wallet_get_claimable_vouchers() const;
|
[[nodiscard]] StdLogosResult wallet_get_claimable_vouchers() const;
|
||||||
|
// Funds an unsigned transaction: request_json is passed through to the
|
||||||
|
// node's wallet fund endpoint (same JSON schema as the HTTP `/wallet/fund`
|
||||||
|
// request body); returns the funded transaction as JSON.
|
||||||
|
[[nodiscard]] StdLogosResult wallet_fund_tx(const std::string& request_json) const;
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
// Submits a signed transaction: signed_tx_json is passed through to the
|
||||||
|
// node (same JSON schema as the HTTP `/mantle/transact` request body);
|
||||||
|
// returns the transaction hash hex on success.
|
||||||
|
[[nodiscard]] StdLogosResult submit_signed_transaction(const std::string& signed_tx_json) const;
|
||||||
|
|
||||||
// Channel
|
// Channel
|
||||||
// Amount-based deposit: the binding selects funding notes itself (splitting a
|
// Amount-based deposit: the binding selects funding notes itself (splitting a
|
||||||
@ -128,12 +138,15 @@ public:
|
|||||||
const std::string& optional_tip_hex
|
const std::string& optional_tip_hex
|
||||||
) const;
|
) const;
|
||||||
|
|
||||||
|
// State of the channel with the given 32-byte channel ID, as JSON (same
|
||||||
|
// schema as the node's `/mantle/channel/{id}` HTTP endpoint). Fails with a
|
||||||
|
// not-found error when the channel does not exist yet.
|
||||||
|
[[nodiscard]] StdLogosResult get_channel_state(const std::string& channel_id_hex) const;
|
||||||
|
|
||||||
// Blend
|
// Blend
|
||||||
[[nodiscard]] StdLogosResult blend_join_as_core_node(
|
[[nodiscard]] StdLogosResult blend_join_as_core_node(
|
||||||
const std::string& provider_id_hex,
|
const std::string& locator,
|
||||||
const std::string& zk_id_hex,
|
const std::string& locked_note_id_hex
|
||||||
const std::string& locked_note_id_hex,
|
|
||||||
const std::vector<std::string>& locators
|
|
||||||
) const;
|
) const;
|
||||||
|
|
||||||
// Explorer
|
// Explorer
|
||||||
@ -143,6 +156,13 @@ public:
|
|||||||
|
|
||||||
// Cryptarchia
|
// Cryptarchia
|
||||||
[[nodiscard]] StdLogosResult get_cryptarchia_info() const;
|
[[nodiscard]] StdLogosResult get_cryptarchia_info() const;
|
||||||
|
// Events emitted by the block with the given 32-byte header ID, as JSON.
|
||||||
|
[[nodiscard]] StdLogosResult get_block_events(const std::string& header_id_hex) const;
|
||||||
|
|
||||||
|
// Time
|
||||||
|
// Consensus time info as JSON:
|
||||||
|
// { slot_duration_ms, genesis_time_unix_ms, current_slot, current_epoch }
|
||||||
|
[[nodiscard]] StdLogosResult get_time_info() const;
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
// Clang-format only handles public/private/protected, so it miss-indents this section.
|
// Clang-format only handles public/private/protected, so it miss-indents this section.
|
||||||
@ -152,6 +172,19 @@ logos_events:
|
|||||||
// blockJson is the full block serialized as JSON.
|
// blockJson is the full block serialized as JSON.
|
||||||
// ReSharper disable once CppFunctionIsNotImplemented
|
// ReSharper disable once CppFunctionIsNotImplemented
|
||||||
void newBlock(const std::string& blockJson);
|
void newBlock(const std::string& blockJson);
|
||||||
|
// Fired per processed block. eventJson carries the block plus the chain
|
||||||
|
// state after processing it (same schema as the node's
|
||||||
|
// `/cryptarchia/blocks/stream` HTTP endpoint; transaction ids at
|
||||||
|
// `transactions[].mantle_tx.hash`). When the stream ends, fired exactly
|
||||||
|
// once with the JSON literal `null` — restart the node subscription (via
|
||||||
|
// stop/start) to keep receiving events.
|
||||||
|
// ReSharper disable once CppFunctionIsNotImplemented
|
||||||
|
void processedBlock(const std::string& eventJson);
|
||||||
|
// Fired per newly finalized (LIB) block. blockInfoJson uses the same
|
||||||
|
// schema as the node's `/cryptarchia/lib/stream` HTTP endpoint. When the
|
||||||
|
// stream ends, fired exactly once with the JSON literal `null`.
|
||||||
|
// ReSharper disable once CppFunctionIsNotImplemented
|
||||||
|
void libBlock(const std::string& blockInfoJson);
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -160,6 +193,10 @@ private:
|
|||||||
// Static instance for C callback (C API doesn't support user data)
|
// Static instance for C callback (C API doesn't support user data)
|
||||||
static LogosBlockchainModule* s_instance;
|
static LogosBlockchainModule* s_instance;
|
||||||
|
|
||||||
// C-compatible callback function
|
// C-compatible callback functions. The stream callbacks receive NULL
|
||||||
|
// exactly once when their stream ends; that is forwarded as the JSON
|
||||||
|
// literal `null` on the corresponding event.
|
||||||
static void on_new_block_callback(const char* block);
|
static void on_new_block_callback(const char* block);
|
||||||
|
static void on_processed_block_callback(const char* event);
|
||||||
|
static void on_lib_block_callback(const char* event);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,23 @@
|
|||||||
|
|
||||||
#include "logos_blockchain_module.h"
|
#include "logos_blockchain_module.h"
|
||||||
|
|
||||||
|
// Recorded payloads of the most recent stream events, so tests can assert what
|
||||||
|
// the trampolines forwarded (including the `null` end-of-stream sentinel).
|
||||||
|
std::string g_lastNewBlockEventJson;
|
||||||
|
std::string g_lastProcessedBlockEventJson;
|
||||||
|
std::string g_lastLibBlockEventJson;
|
||||||
|
|
||||||
void LogosBlockchainModule::newBlock(const std::string& blockJson) {
|
void LogosBlockchainModule::newBlock(const std::string& blockJson) {
|
||||||
|
g_lastNewBlockEventJson = blockJson;
|
||||||
emitEventImpl_("newBlock", nullptr);
|
emitEventImpl_("newBlock", nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LogosBlockchainModule::processedBlock(const std::string& eventJson) {
|
||||||
|
g_lastProcessedBlockEventJson = eventJson;
|
||||||
|
emitEventImpl_("processedBlock", nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogosBlockchainModule::libBlock(const std::string& blockInfoJson) {
|
||||||
|
g_lastLibBlockEventJson = blockInfoJson;
|
||||||
|
emitEventImpl_("libBlock", nullptr);
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,12 @@ std::string g_lastGeneratedStatePath;
|
|||||||
std::string g_lastGeneratedStoragePath;
|
std::string g_lastGeneratedStoragePath;
|
||||||
std::string g_lastGeneratedLogsPath;
|
std::string g_lastGeneratedLogsPath;
|
||||||
|
|
||||||
|
// Captures the callbacks passed to the subscription calls so tests can drive
|
||||||
|
// the streams (including the NULL end-of-stream sentinel).
|
||||||
|
BlockCallback g_lastNewBlockCallback = nullptr;
|
||||||
|
BlockCallback g_lastProcessedBlockCallback = nullptr;
|
||||||
|
BlockCallback g_lastLibBlockCallback = nullptr;
|
||||||
|
|
||||||
static char s_fakeNode = 0;
|
static char s_fakeNode = 0;
|
||||||
static CryptarchiaInfo s_fakeCryptarchiaInfo = {};
|
static CryptarchiaInfo s_fakeCryptarchiaInfo = {};
|
||||||
|
|
||||||
@ -58,8 +64,8 @@ NodeResult start_lb_node(const char* config_path, const char* deployment) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
OperationStatus stop_node(LogosBlockchainNode* node) {
|
OperationStatus shutdown_node(LogosBlockchainNode* node) {
|
||||||
LOGOS_CMOCK_RECORD("stop_node");
|
LOGOS_CMOCK_RECORD("shutdown_node");
|
||||||
return make_status(0);
|
return make_status(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,9 +143,22 @@ StringResult get_peer_id(const char* config_path) {
|
|||||||
|
|
||||||
OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback) {
|
OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback) {
|
||||||
LOGOS_CMOCK_RECORD("subscribe_to_new_blocks");
|
LOGOS_CMOCK_RECORD("subscribe_to_new_blocks");
|
||||||
|
g_lastNewBlockCallback = callback;
|
||||||
return make_status(LOGOS_CMOCK_RETURN(int, "subscribe_to_new_blocks"));
|
return make_status(LOGOS_CMOCK_RETURN(int, "subscribe_to_new_blocks"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OperationStatus subscribe_to_processed_blocks(LogosBlockchainNode* node, BlockCallback callback) {
|
||||||
|
LOGOS_CMOCK_RECORD("subscribe_to_processed_blocks");
|
||||||
|
g_lastProcessedBlockCallback = callback;
|
||||||
|
return make_status(LOGOS_CMOCK_RETURN(int, "subscribe_to_processed_blocks"));
|
||||||
|
}
|
||||||
|
|
||||||
|
OperationStatus subscribe_to_lib_blocks(LogosBlockchainNode* node, BlockCallback callback) {
|
||||||
|
LOGOS_CMOCK_RECORD("subscribe_to_lib_blocks");
|
||||||
|
g_lastLibBlockCallback = callback;
|
||||||
|
return make_status(LOGOS_CMOCK_RETURN(int, "subscribe_to_lib_blocks"));
|
||||||
|
}
|
||||||
|
|
||||||
BalanceResult get_balance(LogosBlockchainNode* node, const uint8_t* address, const void* reserved) {
|
BalanceResult get_balance(LogosBlockchainNode* node, const uint8_t* address, const void* reserved) {
|
||||||
LOGOS_CMOCK_RECORD("get_balance");
|
LOGOS_CMOCK_RECORD("get_balance");
|
||||||
BalanceResult result;
|
BalanceResult result;
|
||||||
@ -218,6 +237,15 @@ OperationStatus free_claimable_vouchers(ClaimableVouchers vouchers) {
|
|||||||
return make_status(0);
|
return make_status(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StringResult get_channel_state(LogosBlockchainNode* node, const uint8_t* channel_id) {
|
||||||
|
LOGOS_CMOCK_RECORD("get_channel_state");
|
||||||
|
StringResult result;
|
||||||
|
const char* json = LOGOS_CMOCK_RETURN_STRING("get_channel_state");
|
||||||
|
result.value = json ? strdup(json) : nullptr;
|
||||||
|
result.error = make_status(LOGOS_CMOCK_RETURN(int, "get_channel_state_error"));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Wallet-notes mock storage (up to 4 notes)
|
// Wallet-notes mock storage (up to 4 notes)
|
||||||
static WalletNote s_mockNotes[4];
|
static WalletNote s_mockNotes[4];
|
||||||
|
|
||||||
@ -251,6 +279,23 @@ OperationStatus free_wallet_notes(WalletNotes notes) {
|
|||||||
return make_status(0);
|
return make_status(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StringResult wallet_fund_tx(LogosBlockchainNode* node, const char* request_json) {
|
||||||
|
LOGOS_CMOCK_RECORD("wallet_fund_tx");
|
||||||
|
StringResult result;
|
||||||
|
const char* json = LOGOS_CMOCK_RETURN_STRING("wallet_fund_tx");
|
||||||
|
result.value = json ? strdup(json) : nullptr;
|
||||||
|
result.error = make_status(LOGOS_CMOCK_RETURN(int, "wallet_fund_tx_error"));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubmitTransactionResult submit_signed_transaction(LogosBlockchainNode* node, const char* signed_tx_json) {
|
||||||
|
LOGOS_CMOCK_RECORD("submit_signed_transaction");
|
||||||
|
SubmitTransactionResult result;
|
||||||
|
memset(result.value, 0xFA, sizeof(Hash));
|
||||||
|
result.error = make_status(LOGOS_CMOCK_RETURN(int, "submit_signed_transaction_error"));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
FfiChannelDepositResult channel_deposit(LogosBlockchainNode* node, const ChannelDepositArguments* arguments) {
|
FfiChannelDepositResult channel_deposit(LogosBlockchainNode* node, const ChannelDepositArguments* arguments) {
|
||||||
LOGOS_CMOCK_RECORD("channel_deposit");
|
LOGOS_CMOCK_RECORD("channel_deposit");
|
||||||
FfiChannelDepositResult result;
|
FfiChannelDepositResult result;
|
||||||
@ -272,11 +317,8 @@ FfiChannelDepositResult channel_deposit_with_notes(
|
|||||||
|
|
||||||
BlendHashResult blend_join_as_core_node(
|
BlendHashResult blend_join_as_core_node(
|
||||||
LogosBlockchainNode* node,
|
LogosBlockchainNode* node,
|
||||||
const uint8_t* provider_id,
|
const char* locator,
|
||||||
const uint8_t* zk_id,
|
const uint8_t* locked_note_id)
|
||||||
const uint8_t* locked_note_id,
|
|
||||||
const char** locators,
|
|
||||||
size_t locators_count)
|
|
||||||
{
|
{
|
||||||
LOGOS_CMOCK_RECORD("blend_join_as_core_node");
|
LOGOS_CMOCK_RECORD("blend_join_as_core_node");
|
||||||
BlendHashResult result;
|
BlendHashResult result;
|
||||||
@ -316,6 +358,7 @@ CryptarchiaInfoResult get_cryptarchia_info(LogosBlockchainNode* node) {
|
|||||||
LOGOS_CMOCK_RECORD("get_cryptarchia_info");
|
LOGOS_CMOCK_RECORD("get_cryptarchia_info");
|
||||||
CryptarchiaInfoResult result;
|
CryptarchiaInfoResult result;
|
||||||
memset(&s_fakeCryptarchiaInfo, 0, sizeof(s_fakeCryptarchiaInfo));
|
memset(&s_fakeCryptarchiaInfo, 0, sizeof(s_fakeCryptarchiaInfo));
|
||||||
|
s_fakeCryptarchiaInfo.lib_slot = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_lib_slot"));
|
||||||
s_fakeCryptarchiaInfo.slot = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_slot"));
|
s_fakeCryptarchiaInfo.slot = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_slot"));
|
||||||
s_fakeCryptarchiaInfo.height = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_height"));
|
s_fakeCryptarchiaInfo.height = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_height"));
|
||||||
s_fakeCryptarchiaInfo.mode = static_cast<State>(LOGOS_CMOCK_RETURN(int, "cryptarchia_mode"));
|
s_fakeCryptarchiaInfo.mode = static_cast<State>(LOGOS_CMOCK_RETURN(int, "cryptarchia_mode"));
|
||||||
@ -331,6 +374,34 @@ OperationStatus free_cryptarchia_info(CryptarchiaInfo* info) {
|
|||||||
return make_status(0);
|
return make_status(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StringResult get_block_events(LogosBlockchainNode* node, const HeaderId* header_id) {
|
||||||
|
LOGOS_CMOCK_RECORD("get_block_events");
|
||||||
|
StringResult result;
|
||||||
|
const char* json = LOGOS_CMOCK_RETURN_STRING("get_block_events");
|
||||||
|
result.value = json ? strdup(json) : nullptr;
|
||||||
|
result.error = make_status(LOGOS_CMOCK_RETURN(int, "get_block_events_error"));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static TimeInfo s_fakeTimeInfo = {};
|
||||||
|
|
||||||
|
TimeInfoResult get_time_info(LogosBlockchainNode* node) {
|
||||||
|
LOGOS_CMOCK_RECORD("get_time_info");
|
||||||
|
TimeInfoResult result;
|
||||||
|
s_fakeTimeInfo.slot_duration_ms = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "time_slot_duration_ms"));
|
||||||
|
s_fakeTimeInfo.genesis_time_unix_ms = static_cast<int64_t>(LOGOS_CMOCK_RETURN(int, "time_genesis_time_unix_ms"));
|
||||||
|
s_fakeTimeInfo.current_slot = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "time_current_slot"));
|
||||||
|
s_fakeTimeInfo.current_epoch = static_cast<uint32_t>(LOGOS_CMOCK_RETURN(int, "time_current_epoch"));
|
||||||
|
result.value = &s_fakeTimeInfo;
|
||||||
|
result.error = make_status(LOGOS_CMOCK_RETURN(int, "get_time_info_error"));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
OperationStatus free_time_info(TimeInfo* info) {
|
||||||
|
LOGOS_CMOCK_RECORD("free_time_info");
|
||||||
|
return make_status(0);
|
||||||
|
}
|
||||||
|
|
||||||
OperationStatus free_cstring(char* s) {
|
OperationStatus free_cstring(char* s) {
|
||||||
LOGOS_CMOCK_RECORD("free_cstring");
|
LOGOS_CMOCK_RECORD("free_cstring");
|
||||||
free(s);
|
free(s);
|
||||||
|
|||||||
@ -45,7 +45,7 @@ typedef struct {
|
|||||||
} OperationStatus;
|
} OperationStatus;
|
||||||
|
|
||||||
// Consensus state enum
|
// Consensus state enum
|
||||||
typedef enum { Bootstrapping, Online } State;
|
typedef enum { Bootstrapping, Online, NotStarted } State;
|
||||||
|
|
||||||
// Key type for generate_key / add_key
|
// Key type for generate_key / add_key
|
||||||
typedef enum { Ed25519, Zk } KeyType;
|
typedef enum { Ed25519, Zk } KeyType;
|
||||||
@ -62,7 +62,7 @@ typedef struct {
|
|||||||
const char* state_path;
|
const char* state_path;
|
||||||
const char* storage_path;
|
const char* storage_path;
|
||||||
const char* logs_path;
|
const char* logs_path;
|
||||||
const bool* ibd;
|
const bool* skip_ibd;
|
||||||
const char* log_filter;
|
const char* log_filter;
|
||||||
const char* kms_file;
|
const char* kms_file;
|
||||||
} GenerateConfigArgs;
|
} GenerateConfigArgs;
|
||||||
@ -134,12 +134,21 @@ typedef struct {
|
|||||||
// Cryptarchia consensus info
|
// Cryptarchia consensus info
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t lib[32];
|
uint8_t lib[32];
|
||||||
|
uint64_t lib_slot;
|
||||||
uint8_t tip[32];
|
uint8_t tip[32];
|
||||||
uint64_t slot;
|
uint64_t slot;
|
||||||
uint64_t height;
|
uint64_t height;
|
||||||
State mode;
|
State mode;
|
||||||
} CryptarchiaInfo;
|
} CryptarchiaInfo;
|
||||||
|
|
||||||
|
// Time service info
|
||||||
|
typedef struct {
|
||||||
|
uint64_t slot_duration_ms;
|
||||||
|
int64_t genesis_time_unix_ms;
|
||||||
|
uint64_t current_slot;
|
||||||
|
uint32_t current_epoch;
|
||||||
|
} TimeInfo;
|
||||||
|
|
||||||
// Result types (C++ structured bindings decompose these)
|
// Result types (C++ structured bindings decompose these)
|
||||||
typedef struct { LogosBlockchainNode* value; OperationStatus error; } NodeResult;
|
typedef struct { LogosBlockchainNode* value; OperationStatus error; } NodeResult;
|
||||||
typedef struct { uint64_t value; OperationStatus error; } BalanceResult;
|
typedef struct { uint64_t value; OperationStatus error; } BalanceResult;
|
||||||
@ -152,6 +161,8 @@ typedef struct { ClaimableVouchers value; OperationStatus error; } FfiClaimableV
|
|||||||
typedef struct { Hash value; OperationStatus error; } BlendHashResult;
|
typedef struct { Hash value; OperationStatus error; } BlendHashResult;
|
||||||
typedef struct { char* value; OperationStatus error; } StringResult;
|
typedef struct { char* value; OperationStatus error; } StringResult;
|
||||||
typedef struct { CryptarchiaInfo* value; OperationStatus error; } CryptarchiaInfoResult;
|
typedef struct { CryptarchiaInfo* value; OperationStatus error; } CryptarchiaInfoResult;
|
||||||
|
typedef struct { TimeInfo* value; OperationStatus error; } TimeInfoResult;
|
||||||
|
typedef struct { Hash value; OperationStatus error; } SubmitTransactionResult;
|
||||||
|
|
||||||
// Block event callback
|
// Block event callback
|
||||||
typedef void (*BlockCallback)(const char* block_json);
|
typedef void (*BlockCallback)(const char* block_json);
|
||||||
@ -162,8 +173,12 @@ bool is_ok(const OperationStatus* status);
|
|||||||
// Lifecycle
|
// Lifecycle
|
||||||
OperationStatus generate_user_config(GenerateConfigArgs args);
|
OperationStatus generate_user_config(GenerateConfigArgs args);
|
||||||
NodeResult start_lb_node(const char* config_path, const char* deployment);
|
NodeResult start_lb_node(const char* config_path, const char* deployment);
|
||||||
OperationStatus stop_node(LogosBlockchainNode* node);
|
OperationStatus shutdown_node(LogosBlockchainNode* node);
|
||||||
OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback);
|
OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback);
|
||||||
|
// Streams: each event is a JSON C string; the callback is invoked exactly once
|
||||||
|
// with NULL when the stream ends.
|
||||||
|
OperationStatus subscribe_to_processed_blocks(LogosBlockchainNode* node, BlockCallback callback);
|
||||||
|
OperationStatus subscribe_to_lib_blocks(LogosBlockchainNode* node, BlockCallback callback);
|
||||||
|
|
||||||
// Config management
|
// Config management
|
||||||
OperationStatus update_user_config(const char* user_config_path, const char* keystore_path);
|
OperationStatus update_user_config(const char* user_config_path, const char* keystore_path);
|
||||||
@ -209,6 +224,10 @@ FfiWalletNotesResult get_wallet_notes(
|
|||||||
const uint8_t* wallet_address,
|
const uint8_t* wallet_address,
|
||||||
const HeaderId* optional_tip);
|
const HeaderId* optional_tip);
|
||||||
OperationStatus free_wallet_notes(WalletNotes notes);
|
OperationStatus free_wallet_notes(WalletNotes notes);
|
||||||
|
StringResult wallet_fund_tx(LogosBlockchainNode* node, const char* request_json);
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
SubmitTransactionResult submit_signed_transaction(LogosBlockchainNode* node, const char* signed_tx_json);
|
||||||
|
|
||||||
// Channel
|
// Channel
|
||||||
FfiChannelDepositResult channel_deposit(LogosBlockchainNode* node, const ChannelDepositArguments* arguments);
|
FfiChannelDepositResult channel_deposit(LogosBlockchainNode* node, const ChannelDepositArguments* arguments);
|
||||||
@ -217,15 +236,13 @@ FfiChannelDepositResult channel_deposit_with_notes(
|
|||||||
const ChannelDepositWithNotesArguments* arguments);
|
const ChannelDepositWithNotesArguments* arguments);
|
||||||
FfiClaimableVouchersResult get_claimable_vouchers(LogosBlockchainNode* node, const HeaderId* optional_tip);
|
FfiClaimableVouchersResult get_claimable_vouchers(LogosBlockchainNode* node, const HeaderId* optional_tip);
|
||||||
OperationStatus free_claimable_vouchers(ClaimableVouchers vouchers);
|
OperationStatus free_claimable_vouchers(ClaimableVouchers vouchers);
|
||||||
|
StringResult get_channel_state(LogosBlockchainNode* node, const uint8_t* channel_id);
|
||||||
|
|
||||||
// Blend
|
// Blend
|
||||||
BlendHashResult blend_join_as_core_node(
|
BlendHashResult blend_join_as_core_node(
|
||||||
LogosBlockchainNode* node,
|
LogosBlockchainNode* node,
|
||||||
const uint8_t* provider_id,
|
const char* locator,
|
||||||
const uint8_t* zk_id,
|
const uint8_t* locked_note_id);
|
||||||
const uint8_t* locked_note_id,
|
|
||||||
const char** locators,
|
|
||||||
size_t locators_count);
|
|
||||||
|
|
||||||
// Explorer
|
// Explorer
|
||||||
StringResult get_block(LogosBlockchainNode* node, const HeaderId* header_id);
|
StringResult get_block(LogosBlockchainNode* node, const HeaderId* header_id);
|
||||||
@ -235,6 +252,11 @@ StringResult get_transaction(LogosBlockchainNode* node, const TxHash* tx_hash);
|
|||||||
// Cryptarchia
|
// Cryptarchia
|
||||||
CryptarchiaInfoResult get_cryptarchia_info(LogosBlockchainNode* node);
|
CryptarchiaInfoResult get_cryptarchia_info(LogosBlockchainNode* node);
|
||||||
OperationStatus free_cryptarchia_info(CryptarchiaInfo* info);
|
OperationStatus free_cryptarchia_info(CryptarchiaInfo* info);
|
||||||
|
StringResult get_block_events(LogosBlockchainNode* node, const HeaderId* header_id);
|
||||||
|
|
||||||
|
// Time
|
||||||
|
TimeInfoResult get_time_info(LogosBlockchainNode* node);
|
||||||
|
OperationStatus free_time_info(TimeInfo* info);
|
||||||
|
|
||||||
// Memory management
|
// Memory management
|
||||||
OperationStatus free_cstring(char* s);
|
OperationStatus free_cstring(char* s);
|
||||||
|
|||||||
@ -292,7 +292,7 @@ LOGOS_TEST(wallet_get_known_addresses_without_node_returns_error) {
|
|||||||
LOGOS_TEST(blend_join_as_core_node_without_node_returns_error) {
|
LOGOS_TEST(blend_join_as_core_node_without_node_returns_error) {
|
||||||
auto t = LogosTestContext("blockchain_module");
|
auto t = LogosTestContext("blockchain_module");
|
||||||
LogosBlockchainModule module;
|
LogosBlockchainModule module;
|
||||||
StdLogosResult result = module.blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, {"locator1"});
|
StdLogosResult result = module.blend_join_as_core_node("locator1", VALID_HEX);
|
||||||
LOGOS_ASSERT_FALSE(result.success);
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
LOGOS_ASSERT_TRUE(contains(result.error, "not running"));
|
LOGOS_ASSERT_TRUE(contains(result.error, "not running"));
|
||||||
}
|
}
|
||||||
@ -321,6 +321,42 @@ LOGOS_TEST(get_cryptarchia_info_without_node_returns_error) {
|
|||||||
LOGOS_ASSERT_FALSE(module.get_cryptarchia_info().success);
|
LOGOS_ASSERT_FALSE(module.get_cryptarchia_info().success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_block_events_without_node_returns_error) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
LogosBlockchainModule module;
|
||||||
|
LOGOS_ASSERT_FALSE(module.get_block_events(VALID_HEX).success);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_time_info_without_node_returns_error) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
LogosBlockchainModule module;
|
||||||
|
LOGOS_ASSERT_FALSE(module.get_time_info().success);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_channel_state_without_node_returns_error) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
LogosBlockchainModule module;
|
||||||
|
StdLogosResult result = module.get_channel_state(VALID_HEX);
|
||||||
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.error, "not running"));
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(wallet_fund_tx_without_node_returns_error) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
LogosBlockchainModule module;
|
||||||
|
StdLogosResult result = module.wallet_fund_tx("{}");
|
||||||
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.error, "not running"));
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(submit_signed_transaction_without_node_returns_error) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
LogosBlockchainModule module;
|
||||||
|
StdLogosResult result = module.submit_signed_transaction("{}");
|
||||||
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.error, "not running"));
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Node lifecycle (start / stop)
|
// Node lifecycle (start / stop)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -334,9 +370,36 @@ LOGOS_TEST(start_succeeds_with_mocked_dependencies) {
|
|||||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
LOGOS_ASSERT(t.cFunctionCalled("start_lb_node"));
|
LOGOS_ASSERT(t.cFunctionCalled("start_lb_node"));
|
||||||
LOGOS_ASSERT(t.cFunctionCalled("subscribe_to_new_blocks"));
|
LOGOS_ASSERT(t.cFunctionCalled("subscribe_to_new_blocks"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("subscribe_to_processed_blocks"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("subscribe_to_lib_blocks"));
|
||||||
delete module;
|
delete module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(start_fails_when_processed_blocks_subscription_fails) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
LogosBlockchainModule module;
|
||||||
|
|
||||||
|
t.mockCFunction("start_lb_node").returns(1);
|
||||||
|
t.mockCFunction("subscribe_to_new_blocks").returns(0);
|
||||||
|
t.mockCFunction("subscribe_to_processed_blocks").returns(1);
|
||||||
|
|
||||||
|
LOGOS_ASSERT_FALSE(module.start(tmpDir.filePath("config.json"), "").success);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(start_fails_when_lib_blocks_subscription_fails) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
LogosBlockchainModule module;
|
||||||
|
|
||||||
|
t.mockCFunction("start_lb_node").returns(1);
|
||||||
|
t.mockCFunction("subscribe_to_new_blocks").returns(0);
|
||||||
|
t.mockCFunction("subscribe_to_processed_blocks").returns(0);
|
||||||
|
t.mockCFunction("subscribe_to_lib_blocks").returns(1);
|
||||||
|
|
||||||
|
LOGOS_ASSERT_FALSE(module.start(tmpDir.filePath("config.json"), "").success);
|
||||||
|
}
|
||||||
|
|
||||||
LOGOS_TEST(start_returns_1_when_already_running) {
|
LOGOS_TEST(start_returns_1_when_already_running) {
|
||||||
auto t = LogosTestContext("blockchain_module");
|
auto t = LogosTestContext("blockchain_module");
|
||||||
TempDir tmpDir;
|
TempDir tmpDir;
|
||||||
@ -354,7 +417,7 @@ LOGOS_TEST(stop_succeeds_with_running_node) {
|
|||||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
LOGOS_ASSERT_TRUE(module->stop().success);
|
LOGOS_ASSERT_TRUE(module->stop().success);
|
||||||
LOGOS_ASSERT(t.cFunctionCalled("stop_node"));
|
LOGOS_ASSERT(t.cFunctionCalled("shutdown_node"));
|
||||||
delete module;
|
delete module;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,27 +538,15 @@ LOGOS_TEST(wallet_transfer_funds_rejects_invalid_optional_tip) {
|
|||||||
|
|
||||||
// blend_join_as_core_node validation
|
// blend_join_as_core_node validation
|
||||||
|
|
||||||
LOGOS_TEST(blend_join_rejects_invalid_provider_id) {
|
LOGOS_TEST(blend_join_rejects_empty_locator) {
|
||||||
auto t = LogosTestContext("blockchain_module");
|
auto t = LogosTestContext("blockchain_module");
|
||||||
TempDir tmpDir;
|
TempDir tmpDir;
|
||||||
auto* module = createStartedModule(t, tmpDir);
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
StdLogosResult result = module->blend_join_as_core_node("short", VALID_HEX, VALID_HEX, {});
|
StdLogosResult result = module->blend_join_as_core_node("", VALID_HEX);
|
||||||
LOGOS_ASSERT_FALSE(result.success);
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
LOGOS_ASSERT_TRUE(contains(result.error, "provider_id"));
|
LOGOS_ASSERT_TRUE(contains(result.error, "locator"));
|
||||||
delete module;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGOS_TEST(blend_join_rejects_invalid_zk_id) {
|
|
||||||
auto t = LogosTestContext("blockchain_module");
|
|
||||||
TempDir tmpDir;
|
|
||||||
auto* module = createStartedModule(t, tmpDir);
|
|
||||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
|
||||||
|
|
||||||
StdLogosResult result = module->blend_join_as_core_node(VALID_HEX, "short", VALID_HEX, {});
|
|
||||||
LOGOS_ASSERT_FALSE(result.success);
|
|
||||||
LOGOS_ASSERT_TRUE(contains(result.error, "zk_id"));
|
|
||||||
delete module;
|
delete module;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -505,7 +556,7 @@ LOGOS_TEST(blend_join_rejects_invalid_locked_note_id) {
|
|||||||
auto* module = createStartedModule(t, tmpDir);
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
StdLogosResult result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, "short", {});
|
StdLogosResult result = module->blend_join_as_core_node("locator1", "short");
|
||||||
LOGOS_ASSERT_FALSE(result.success);
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
LOGOS_ASSERT_TRUE(contains(result.error, "locked_note_id"));
|
LOGOS_ASSERT_TRUE(contains(result.error, "locked_note_id"));
|
||||||
delete module;
|
delete module;
|
||||||
@ -537,6 +588,30 @@ LOGOS_TEST(get_transaction_rejects_invalid_hex) {
|
|||||||
delete module;
|
delete module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_block_events_rejects_invalid_hex) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
StdLogosResult result = module->get_block_events("tooshort");
|
||||||
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.error, "64 hex"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_channel_state_rejects_invalid_hex) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
StdLogosResult result = module->get_channel_state("bad");
|
||||||
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.error, "channel_id"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 0x prefix handling
|
// 0x prefix handling
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -1025,6 +1100,104 @@ LOGOS_TEST(wallet_get_claimable_vouchers_returns_error_on_ffi_failure) {
|
|||||||
delete module;
|
delete module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(wallet_fund_tx_returns_json_on_success) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("wallet_fund_tx").returns(R"({"mantle_tx":{"ops":[]}})");
|
||||||
|
t.mockCFunction("wallet_fund_tx_error").returns(0);
|
||||||
|
|
||||||
|
StdLogosResult result = module->wallet_fund_tx(R"({"tx":{},"funding_keys":[]})");
|
||||||
|
LOGOS_ASSERT_TRUE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.value.get<std::string>(), "mantle_tx"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("wallet_fund_tx"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(wallet_fund_tx_returns_error_on_ffi_failure) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("wallet_fund_tx_error").returns(1);
|
||||||
|
|
||||||
|
StdLogosResult result = module->wallet_fund_tx("{}");
|
||||||
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.error, "mock error"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
|
||||||
|
LOGOS_TEST(submit_signed_transaction_returns_tx_hash) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("submit_signed_transaction_error").returns(0);
|
||||||
|
|
||||||
|
StdLogosResult result = module->submit_signed_transaction("{}");
|
||||||
|
LOGOS_ASSERT_TRUE(result.success);
|
||||||
|
// Mock fills hash with 0xFA -> hex "fafa...fa" (64 chars)
|
||||||
|
std::string hash = result.value.get<std::string>();
|
||||||
|
LOGOS_ASSERT_EQ(static_cast<int>(hash.length()), 64);
|
||||||
|
LOGOS_ASSERT_TRUE(hash.substr(0, 2) == "fa");
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("submit_signed_transaction"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(submit_signed_transaction_returns_error_on_ffi_failure) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("submit_signed_transaction_error").returns(1);
|
||||||
|
|
||||||
|
StdLogosResult result = module->submit_signed_transaction("{}");
|
||||||
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.error, "mock error"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel state
|
||||||
|
|
||||||
|
LOGOS_TEST(get_channel_state_returns_json_on_success) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("get_channel_state").returns(R"({"tip":"abc","inscriptions":[]})");
|
||||||
|
t.mockCFunction("get_channel_state_error").returns(0);
|
||||||
|
|
||||||
|
StdLogosResult result = module->get_channel_state(VALID_HEX);
|
||||||
|
LOGOS_ASSERT_TRUE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.value.get<std::string>(), "inscriptions"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("get_channel_state"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_channel_state_returns_error_on_ffi_failure) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("get_channel_state_error").returns(1);
|
||||||
|
|
||||||
|
StdLogosResult result = module->get_channel_state(VALID_HEX);
|
||||||
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.error, "mock error"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
// Blend
|
// Blend
|
||||||
|
|
||||||
LOGOS_TEST(blend_join_as_core_node_returns_declaration_id) {
|
LOGOS_TEST(blend_join_as_core_node_returns_declaration_id) {
|
||||||
@ -1035,8 +1208,7 @@ LOGOS_TEST(blend_join_as_core_node_returns_declaration_id) {
|
|||||||
|
|
||||||
t.mockCFunction("blend_join_as_core_node_error").returns(0);
|
t.mockCFunction("blend_join_as_core_node_error").returns(0);
|
||||||
|
|
||||||
std::vector<std::string> locators = {"locator1", "locator2"};
|
StdLogosResult result = module->blend_join_as_core_node("locator1", VALID_HEX);
|
||||||
StdLogosResult result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, locators);
|
|
||||||
LOGOS_ASSERT_TRUE(result.success);
|
LOGOS_ASSERT_TRUE(result.success);
|
||||||
// Mock fills hash with 0xCD -> hex "cdcd...cd" (64 chars)
|
// Mock fills hash with 0xCD -> hex "cdcd...cd" (64 chars)
|
||||||
std::string declarationId = result.value.get<std::string>();
|
std::string declarationId = result.value.get<std::string>();
|
||||||
@ -1054,7 +1226,7 @@ LOGOS_TEST(blend_join_as_core_node_returns_error_on_ffi_failure) {
|
|||||||
|
|
||||||
t.mockCFunction("blend_join_as_core_node_error").returns(1);
|
t.mockCFunction("blend_join_as_core_node_error").returns(1);
|
||||||
|
|
||||||
StdLogosResult result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, {});
|
StdLogosResult result = module->blend_join_as_core_node("locator1", VALID_HEX);
|
||||||
LOGOS_ASSERT_FALSE(result.success);
|
LOGOS_ASSERT_FALSE(result.success);
|
||||||
delete module;
|
delete module;
|
||||||
}
|
}
|
||||||
@ -1159,6 +1331,7 @@ LOGOS_TEST(get_cryptarchia_info_returns_json_on_success) {
|
|||||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
t.mockCFunction("get_cryptarchia_info_error").returns(0);
|
t.mockCFunction("get_cryptarchia_info_error").returns(0);
|
||||||
|
t.mockCFunction("cryptarchia_lib_slot").returns(90);
|
||||||
t.mockCFunction("cryptarchia_slot").returns(100);
|
t.mockCFunction("cryptarchia_slot").returns(100);
|
||||||
t.mockCFunction("cryptarchia_height").returns(50);
|
t.mockCFunction("cryptarchia_height").returns(50);
|
||||||
t.mockCFunction("cryptarchia_mode").returns(1); // Online
|
t.mockCFunction("cryptarchia_mode").returns(1); // Online
|
||||||
@ -1173,11 +1346,26 @@ LOGOS_TEST(get_cryptarchia_info_returns_json_on_success) {
|
|||||||
LOGOS_ASSERT_TRUE(contains(json, "Online"));
|
LOGOS_ASSERT_TRUE(contains(json, "Online"));
|
||||||
LOGOS_ASSERT_TRUE(contains(json, "lib"));
|
LOGOS_ASSERT_TRUE(contains(json, "lib"));
|
||||||
LOGOS_ASSERT_TRUE(contains(json, "tip"));
|
LOGOS_ASSERT_TRUE(contains(json, "tip"));
|
||||||
|
LOGOS_ASSERT_TRUE(contains(json, "\"lib_slot\":90"));
|
||||||
LOGOS_ASSERT(t.cFunctionCalled("get_cryptarchia_info"));
|
LOGOS_ASSERT(t.cFunctionCalled("get_cryptarchia_info"));
|
||||||
LOGOS_ASSERT(t.cFunctionCalled("free_cryptarchia_info"));
|
LOGOS_ASSERT(t.cFunctionCalled("free_cryptarchia_info"));
|
||||||
delete module;
|
delete module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_cryptarchia_info_not_started_mode) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("get_cryptarchia_info_error").returns(0);
|
||||||
|
t.mockCFunction("cryptarchia_mode").returns(2); // NotStarted
|
||||||
|
|
||||||
|
StdLogosResult result = module->get_cryptarchia_info();
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.value.get<std::string>(), "NotStarted"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
LOGOS_TEST(get_cryptarchia_info_bootstrapping_mode) {
|
LOGOS_TEST(get_cryptarchia_info_bootstrapping_mode) {
|
||||||
auto t = LogosTestContext("blockchain_module");
|
auto t = LogosTestContext("blockchain_module");
|
||||||
TempDir tmpDir;
|
TempDir tmpDir;
|
||||||
@ -1204,6 +1392,147 @@ LOGOS_TEST(get_cryptarchia_info_returns_error_on_ffi_failure) {
|
|||||||
delete module;
|
delete module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_block_events_returns_json_on_success) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("get_block_events").returns(R"([{"type":"inscription"}])");
|
||||||
|
t.mockCFunction("get_block_events_error").returns(0);
|
||||||
|
|
||||||
|
StdLogosResult result = module->get_block_events(VALID_HEX);
|
||||||
|
LOGOS_ASSERT_TRUE(result.success);
|
||||||
|
LOGOS_ASSERT_TRUE(contains(result.value.get<std::string>(), "inscription"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("get_block_events"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_block_events_returns_error_on_ffi_failure) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("get_block_events_error").returns(1);
|
||||||
|
|
||||||
|
LOGOS_ASSERT_FALSE(module->get_block_events(VALID_HEX).success);
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time
|
||||||
|
|
||||||
|
LOGOS_TEST(get_time_info_returns_json_on_success) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("get_time_info_error").returns(0);
|
||||||
|
t.mockCFunction("time_slot_duration_ms").returns(2000);
|
||||||
|
t.mockCFunction("time_genesis_time_unix_ms").returns(1700000);
|
||||||
|
t.mockCFunction("time_current_slot").returns(1234);
|
||||||
|
t.mockCFunction("time_current_epoch").returns(7);
|
||||||
|
|
||||||
|
StdLogosResult result = module->get_time_info();
|
||||||
|
LOGOS_ASSERT_TRUE(result.success);
|
||||||
|
std::string json = result.value.get<std::string>();
|
||||||
|
LOGOS_ASSERT_TRUE(contains(json, "\"slot_duration_ms\":2000"));
|
||||||
|
LOGOS_ASSERT_TRUE(contains(json, "\"genesis_time_unix_ms\":1700000"));
|
||||||
|
LOGOS_ASSERT_TRUE(contains(json, "\"current_slot\":1234"));
|
||||||
|
LOGOS_ASSERT_TRUE(contains(json, "\"current_epoch\":7"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("get_time_info"));
|
||||||
|
LOGOS_ASSERT(t.cFunctionCalled("free_time_info"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(get_time_info_returns_error_on_ffi_failure) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
|
||||||
|
t.mockCFunction("get_time_info_error").returns(1);
|
||||||
|
|
||||||
|
LOGOS_ASSERT_FALSE(module->get_time_info().success);
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Stream events (trampolines drive logos_events; end of stream = JSON null)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Captured by the mock subscribe calls (mock_logos_blockchain.cpp).
|
||||||
|
extern BlockCallback g_lastNewBlockCallback;
|
||||||
|
extern BlockCallback g_lastProcessedBlockCallback;
|
||||||
|
extern BlockCallback g_lastLibBlockCallback;
|
||||||
|
// Recorded by the event stubs (event_stubs.cpp).
|
||||||
|
extern std::string g_lastNewBlockEventJson;
|
||||||
|
extern std::string g_lastProcessedBlockEventJson;
|
||||||
|
extern std::string g_lastLibBlockEventJson;
|
||||||
|
|
||||||
|
LOGOS_TEST(processed_block_stream_forwards_json_and_null_sentinel) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
LOGOS_ASSERT_TRUE(g_lastProcessedBlockCallback != nullptr);
|
||||||
|
|
||||||
|
g_lastProcessedBlockEventJson.clear();
|
||||||
|
g_lastProcessedBlockCallback(R"({"header_id":"abc","transactions":[]})");
|
||||||
|
LOGOS_ASSERT_EQ(g_lastProcessedBlockEventJson, std::string(R"({"header_id":"abc","transactions":[]})"));
|
||||||
|
|
||||||
|
g_lastProcessedBlockCallback(nullptr);
|
||||||
|
LOGOS_ASSERT_EQ(g_lastProcessedBlockEventJson, std::string("null"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGOS_TEST(lib_block_stream_forwards_json_and_null_sentinel) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
LOGOS_ASSERT_TRUE(g_lastLibBlockCallback != nullptr);
|
||||||
|
|
||||||
|
g_lastLibBlockEventJson.clear();
|
||||||
|
g_lastLibBlockCallback(R"({"header_id":"def","slot":9})");
|
||||||
|
LOGOS_ASSERT_EQ(g_lastLibBlockEventJson, std::string(R"({"header_id":"def","slot":9})"));
|
||||||
|
|
||||||
|
g_lastLibBlockCallback(nullptr);
|
||||||
|
LOGOS_ASSERT_EQ(g_lastLibBlockEventJson, std::string("null"));
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The legacy new-block stream never sends NULL, but the trampoline must not
|
||||||
|
// crash if it ever does (regression test for the added guard).
|
||||||
|
LOGOS_TEST(new_block_callback_ignores_null_pointer) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
LOGOS_ASSERT_TRUE(g_lastNewBlockCallback != nullptr);
|
||||||
|
|
||||||
|
g_lastNewBlockEventJson.clear();
|
||||||
|
g_lastNewBlockCallback(nullptr);
|
||||||
|
LOGOS_ASSERT_EQ(g_lastNewBlockEventJson, std::string());
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream events are not delivered after the module stops (s_instance cleared).
|
||||||
|
LOGOS_TEST(stream_events_not_delivered_after_stop) {
|
||||||
|
auto t = LogosTestContext("blockchain_module");
|
||||||
|
TempDir tmpDir;
|
||||||
|
auto* module = createStartedModule(t, tmpDir);
|
||||||
|
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||||
|
LOGOS_ASSERT_TRUE(module->stop().success);
|
||||||
|
|
||||||
|
g_lastProcessedBlockEventJson.clear();
|
||||||
|
g_lastProcessedBlockCallback(R"({"header_id":"abc"})");
|
||||||
|
LOGOS_ASSERT_EQ(g_lastProcessedBlockEventJson, std::string());
|
||||||
|
delete module;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Config management (operate on file paths, no running node required)
|
// Config management (operate on file paths, no running node required)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user