Compare commits

...

59 Commits

Author SHA1 Message Date
Alex Kremer
a316486741 feat: Add v3 support (#1754) 2024-11-27 18:29:57 +00:00
Alex Kremer
e2caf577e0 feat: Healthcheck endpoint (#1751)
Fixes #1759
2024-11-27 18:29:32 +00:00
Alex Kremer
b23693e403 feat: Upgrade to libxrpl 2.3.0 (#1756) 2024-11-27 18:24:54 +00:00
Peter Chen
d001e35427 fix: authorized_credential elements in array not objects bug (#1744) (#1747)
fixes: #1743
2024-11-21 12:05:15 -05:00
Peter Chen
592af70f03 fix: Credential error message (#1738)
fixes #1737
2024-11-18 16:15:11 +00:00
Alex Kremer
33cf336964 feat: Upgrade to libxrpl 2.3.0-rc2 (#1736) 2024-11-18 16:13:59 +00:00
github-actions[bot]
16e07b90db style: clang-tidy auto fixes (#1735)
Fixes #1734. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-18 16:13:47 +00:00
Peter Chen
39419c8b58 feat: Add Support Credentials for Clio (#1712)
Rippled PR: [here](https://github.com/XRPLF/rippled/pull/5103)
2024-11-18 16:13:09 +00:00
github-actions[bot]
e38658a0d6 style: clang-tidy auto fixes (#1730)
Fixes #1729. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-18 16:12:40 +00:00
Shawn Xie
fb98a6a394 feat: Implement MPT changes (#1147)
Implements https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0033d-multi-purpose-tokens
2024-11-11 17:27:04 +00:00
dependabot[bot]
b8a8248c42 ci: Bump wandalen/wretry.action from 3.7.0 to 3.7.2 (#1723)
Bumps
[wandalen/wretry.action](https://github.com/wandalen/wretry.action) from
3.7.0 to 3.7.2.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8ceaefd717"><code>8ceaefd</code></a>
version 3.7.2</li>
<li><a
href="ce976ac9e7"><code>ce976ac</code></a>
version 3.7.1</li>
<li><a
href="7a8f8d4bf2"><code>7a8f8d4</code></a>
Merge pull request <a
href="https://redirect.github.com/wandalen/wretry.action/issues/174">#174</a>
from dmvict/master</li>
<li><a
href="2103bce855"><code>2103bce</code></a>
Fix action, add option <code>pre_retry_command</code> to call of
subaction</li>
<li>See full diff in <a
href="https://github.com/wandalen/wretry.action/compare/v3.7.0...v3.7.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=wandalen/wretry.action&package-manager=github_actions&previous-version=3.7.0&new-version=3.7.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alex Kremer <akremer@ripple.com>
2024-11-11 17:27:04 +00:00
Alex Kremer
a092b7ae08 feat: Upgrade to libxrpl 2.3.0-rc1 (#1718)
Fixes #1717
2024-11-11 17:27:04 +00:00
github-actions[bot]
07438a2e02 style: clang-tidy auto fixes (#1720)
Fixes #1719. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-11 17:27:04 +00:00
Alex Kremer
09aa688de4 feat: ETLng Registry (#1713)
For #1597
2024-11-11 17:27:03 +00:00
dependabot[bot]
a7bff26fd6 ci: Bump wandalen/wretry.action from 3.5.0 to 3.7.0 (#1714)
Bumps
[wandalen/wretry.action](https://github.com/wandalen/wretry.action) from
3.5.0 to 3.7.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f8754f7974"><code>f8754f7</code></a>
version 3.7.0</li>
<li><a
href="03db9837ed"><code>03db983</code></a>
Merge pull request <a
href="https://redirect.github.com/wandalen/wretry.action/issues/171">#171</a>
from dmvict/docker_readme</li>
<li><a
href="d80901cd5c"><code>d80901c</code></a>
Sync readme for new feature</li>
<li><a
href="e00d406ade"><code>e00d406</code></a>
version 3.6.0</li>
<li><a
href="e00deaa9ba"><code>e00deaa</code></a>
Merge pull request <a
href="https://redirect.github.com/wandalen/wretry.action/issues/170">#170</a>
from dmvict/pre_retry_action</li>
<li><a
href="8b50f3152e"><code>8b50f31</code></a>
Update action, add option <code>pre_retry_command</code> to run command
between retries</li>
<li><a
href="990f16983d"><code>990f169</code></a>
Merge pull request <a
href="https://redirect.github.com/wandalen/wretry.action/issues/167">#167</a>
from Vampire/add-typing</li>
<li><a
href="aeb34f4d13"><code>aeb34f4</code></a>
Add action typing</li>
<li>See full diff in <a
href="https://github.com/wandalen/wretry.action/compare/v3.5.0...v3.7.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=wandalen/wretry.action&package-manager=github_actions&previous-version=3.5.0&new-version=3.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alex Kremer <akremer@ripple.com>
2024-11-11 17:27:03 +00:00
Peter Chen
081adf1cae fix: Support Delete NFT (#1695)
Fixes #1677
2024-11-11 17:27:03 +00:00
cyan317
ffc9deb0f8 fix: Add queue size limit for websocket (#1701)
For slow clients, we will disconnect with it if the message queue is too
long.

---------

Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
2024-11-11 17:27:02 +00:00
github-actions[bot]
717a29ecdf style: clang-tidy auto fixes (#1711)
Fixes #1710.

---------

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
2024-11-11 17:27:02 +00:00
Sergey Kuznetsov
e8db74456a ci: Fix nightly build (#1709)
Fixes #1703.
2024-11-11 17:27:02 +00:00
Sergey Kuznetsov
4947a83696 fix: Fix issues clang-tidy found (#1708)
Fixes #1706.
2024-11-11 17:27:01 +00:00
github-actions[bot]
164387cab0 style: clang-tidy auto fixes (#1705)
Fixes #1704. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-11 17:27:01 +00:00
Sergey Kuznetsov
b8f1deb90f refactor: Coroutine based webserver (#1699)
Code of new coroutine-based web server. The new server is not connected
to Clio and not ready to use yet.
For #919.
2024-11-11 17:27:01 +00:00
Sergey Kuznetsov
5c77e59374 fix: Fix timer spurious calls (#1700)
Fixes #1634.
I also checked other timers and they don't have the issue.
2024-11-11 17:27:00 +00:00
Peter Chen
6d070132c7 fix: example config syntax (#1696) 2024-11-11 17:27:00 +00:00
cyan317
d2dda69448 fix: Remove log (#1694) 2024-11-11 17:27:00 +00:00
cyan317
e2aeaa0956 chore: Add counter for total messages waiting to be sent (#1691) 2024-11-11 17:27:00 +00:00
Sergey Kuznetsov
2951b4aaa0 style: Fix include (#1687)
Fixes #1686
2024-11-11 17:26:59 +00:00
cyan317
6c3c761dd1 chore: Remove unused static variables (#1683) 2024-11-11 17:26:59 +00:00
github-actions[bot]
527020680a style: clang-tidy auto fixes (#1685)
Fixes #1684. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-11 17:26:59 +00:00
Alex Kremer
401448f771 style: Update code formatting (#1682)
For #1664
2024-11-11 17:26:58 +00:00
Alex Kremer
0f12a6d7f2 chore: Upgrade to llvm 19 tooling (#1681)
For #1664
2024-11-11 17:26:58 +00:00
Peter Chen
5c8fc939f2 fix: deletion script will not OOM (#1679)
fixes #1676 and #1678
2024-11-11 17:26:58 +00:00
github-actions[bot]
b1be848098 style: clang-tidy auto fixes (#1674)
Fixes #1673. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-11-11 17:26:58 +00:00
cyan317
41aabbfcce feat: server info cache (#1671)
fix: #1181
2024-11-11 17:26:57 +00:00
dependabot[bot]
c00d25aa6b chore: Bump ytanikin/PRConventionalCommits from 1.2.0 to 1.3.0 (#1670)
Bumps
[ytanikin/PRConventionalCommits](https://github.com/ytanikin/prconventionalcommits)
from 1.2.0 to 1.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ytanikin/prconventionalcommits/releases">ytanikin/PRConventionalCommits's
releases</a>.</em></p>
<blockquote>
<h2>1.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: Set breaking changes regex by <a
href="https://github.com/alexangas"><code>@​alexangas</code></a> in <a
href="https://redirect.github.com/ytanikin/PRConventionalCommits/pull/24">ytanikin/PRConventionalCommits#24</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/alexangas"><code>@​alexangas</code></a>
made their first contribution in <a
href="https://redirect.github.com/ytanikin/PRConventionalCommits/pull/24">ytanikin/PRConventionalCommits#24</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ytanikin/PRConventionalCommits/compare/1.2.0...1.3.0">https://github.com/ytanikin/PRConventionalCommits/compare/1.2.0...1.3.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b628c5a234"><code>b628c5a</code></a>
test: enable &quot;breaking change&quot; test</li>
<li><a
href="e1b5683aa4"><code>e1b5683</code></a>
fix: Set breaking changes regex (<a
href="https://redirect.github.com/ytanikin/prconventionalcommits/issues/24">#24</a>)</li>
<li><a
href="92a7ab7dc6"><code>92a7ab7</code></a>
fix: upgrade dependencies (<a
href="https://redirect.github.com/ytanikin/prconventionalcommits/issues/26">#26</a>)</li>
<li><a
href="cc6cc0dddb"><code>cc6cc0d</code></a>
test: fix tests (<a
href="https://redirect.github.com/ytanikin/prconventionalcommits/issues/25">#25</a>)</li>
<li>See full diff in <a
href="https://github.com/ytanikin/prconventionalcommits/compare/1.2.0...1.3.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ytanikin/PRConventionalCommits&package-manager=github_actions&previous-version=1.2.0&new-version=1.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 17:26:57 +00:00
Alex Kremer
8d5c588e35 chore: Apply commits for 2.3.0-b4 (#1725) 2024-11-11 14:37:31 +00:00
Sergey Kuznetsov
9df3e936cc chore: Update libxrpl to 2.3.0-b4 (#1667) 2024-09-25 14:44:03 +01:00
Alex Kremer
4166c46820 fix: Workaround for gcc12 bug with defaulted destructors (#1666)
Fixes #1662
2024-09-25 14:44:03 +01:00
github-actions[bot]
f75cbd456b style: clang-tidy auto fixes (#1663)
Fixes #1662.

---------

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
Co-authored-by: Peter Chen <ychen@ripple.com>
2024-09-25 14:44:03 +01:00
Peter Chen
d189651821 fix: add no lint to ignore clang-tidy (#1660)
Fixes build for
[#1659](https://github.com/XRPLF/clio/actions/runs/10956058143/job/30421296417)
2024-09-25 14:44:02 +01:00
github-actions[bot]
3f791c1315 style: clang-tidy auto fixes (#1659)
Fixes #1658. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-09-25 14:44:02 +01:00
Peter Chen
418511332e chore: Revert Cassandra driver upgrade (#1656)
Reverts XRPLF/clio#1646
2024-09-25 14:44:02 +01:00
Peter Chen
e5a0477352 refactor: Clio Config (#1593)
Add constraint + parse json into Config
Second part of refactoring Clio Config; First PR found
[here](https://github.com/XRPLF/clio/pull/1544)

Steps that are left to implement:
- Replacing all the places where we fetch config values (by using
config.valueOr/MaybeValue) to instead get it from Config Definition
- Generate markdown file using Clio Config Description
2024-09-25 14:44:02 +01:00
cyan317
3118110eb8 feat: add 'force_forward' field to request (#1647)
Fix #1141
2024-09-25 14:44:01 +01:00
Alex Kremer
6d20f39f67 feat: Delete-before support in data removal tool (#1649)
Fixes #1650
2024-09-25 14:44:01 +01:00
Peter Chen
9cb1e06c8e fix: Upgrade Cassandra driver (#1646)
Fixes #1296
2024-09-25 14:44:01 +01:00
Peter Chen
423244eb4b fix: pre-push tag (#1614)
Fix issue of git was verifying incorrect Tag
2024-09-25 14:44:01 +01:00
cyan317
7aaba1cbad fix: no restriction on type field (#1644)
'type' should not matter if 'full' or 'accounts' is false. Relax the
restriction for 'type'
2024-09-25 14:44:00 +01:00
cyan317
b7c50fd73d fix: Add more restrictions to admin fields (#1643) 2024-09-25 14:44:00 +01:00
dependabot[bot]
442ee874d5 ci: Bump peter-evans/create-pull-request from 6 to 7 (#1636)
Bumps
[peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request)
from 6 to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/peter-evans/create-pull-request/releases">peter-evans/create-pull-request's
releases</a>.</em></p>
<blockquote>
<h2>Create Pull Request v7.0.0</h2>
<p> Now supports commit signing with bot-generated tokens! See
&quot;What's new&quot; below. ✍️🤖</p>
<h3>Behaviour changes</h3>
<ul>
<li>Action input <code>git-token</code> has been renamed
<code>branch-token</code>, to be more clear about its purpose. The
<code>branch-token</code> is the token that the action will use to
create and update the branch.</li>
<li>The action now handles requests that have been rate-limited by
GitHub. Requests hitting a primary rate limit will retry twice, for a
total of three attempts. Requests hitting a secondary rate limit will
not be retried.</li>
<li>The <code>pull-request-operation</code> output now returns
<code>none</code> when no operation was executed.</li>
<li>Removed deprecated output environment variable
<code>PULL_REQUEST_NUMBER</code>. Please use the
<code>pull-request-number</code> action output instead.</li>
</ul>
<h3>What's new</h3>
<ul>
<li>The action can now sign commits as <code>github-actions[bot]</code>
when using <code>GITHUB_TOKEN</code>, or your own bot when using <a
href="https://github.com/peter-evans/create-pull-request/blob/HEAD/docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens">GitHub
App tokens</a>. See <a
href="https://github.com/peter-evans/create-pull-request/blob/HEAD/docs/concepts-guidelines.md#commit-signature-verification-for-bots">commit
signing</a> for details.</li>
<li>Action input <code>draft</code> now accepts a new value
<code>always-true</code>. This will set the pull request to draft status
when the pull request is updated, as well as on creation.</li>
<li>A new action input <code>maintainer-can-modify</code> indicates
whether <a
href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork">maintainers
can modify</a> the pull request. The default is <code>true</code>, which
retains the existing behaviour of the action.</li>
<li>A new output <code>pull-request-commits-verified</code> returns
<code>true</code> or <code>false</code>, indicating whether GitHub
considers the signature of the branch's commits to be verified.</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.36 to
18.19.39 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3000">peter-evans/create-pull-request#3000</a></li>
<li>build(deps-dev): bump ts-jest from 29.1.5 to 29.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3008">peter-evans/create-pull-request#3008</a></li>
<li>build(deps-dev): bump prettier from 3.3.2 to 3.3.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3018">peter-evans/create-pull-request#3018</a></li>
<li>build(deps-dev): bump ts-jest from 29.2.0 to 29.2.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3019">peter-evans/create-pull-request#3019</a></li>
<li>build(deps-dev): bump eslint-plugin-prettier from 5.1.3 to 5.2.1 by
<a href="https://github.com/dependabot"><code>@​dependabot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3035">peter-evans/create-pull-request#3035</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.39 to
18.19.41 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3037">peter-evans/create-pull-request#3037</a></li>
<li>build(deps): bump undici from 6.19.2 to 6.19.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3036">peter-evans/create-pull-request#3036</a></li>
<li>build(deps-dev): bump ts-jest from 29.2.2 to 29.2.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3038">peter-evans/create-pull-request#3038</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.41 to
18.19.42 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3070">peter-evans/create-pull-request#3070</a></li>
<li>build(deps): bump undici from 6.19.4 to 6.19.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3086">peter-evans/create-pull-request#3086</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.42 to
18.19.43 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3087">peter-evans/create-pull-request#3087</a></li>
<li>build(deps-dev): bump ts-jest from 29.2.3 to 29.2.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3088">peter-evans/create-pull-request#3088</a></li>
<li>build(deps): bump undici from 6.19.5 to 6.19.7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3145">peter-evans/create-pull-request#3145</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.43 to
18.19.44 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3144">peter-evans/create-pull-request#3144</a></li>
<li>Update distribution by <a
href="https://github.com/actions-bot"><code>@​actions-bot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3154">peter-evans/create-pull-request#3154</a></li>
<li>build(deps): bump undici from 6.19.7 to 6.19.8 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3213">peter-evans/create-pull-request#3213</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.44 to
18.19.45 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3214">peter-evans/create-pull-request#3214</a></li>
<li>Update distribution by <a
href="https://github.com/actions-bot"><code>@​actions-bot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3221">peter-evans/create-pull-request#3221</a></li>
<li>build(deps-dev): bump eslint-import-resolver-typescript from 3.6.1
to 3.6.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3255">peter-evans/create-pull-request#3255</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.45 to
18.19.46 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3254">peter-evans/create-pull-request#3254</a></li>
<li>build(deps-dev): bump ts-jest from 29.2.4 to 29.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3256">peter-evans/create-pull-request#3256</a></li>
<li>v7 - signed commits by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3057">peter-evans/create-pull-request#3057</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/rustycl0ck"><code>@​rustycl0ck</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3057">peter-evans/create-pull-request#3057</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/create-pull-request/compare/v6.1.0...v7.0.0">https://github.com/peter-evans/create-pull-request/compare/v6.1.0...v7.0.0</a></p>
<h2>Create Pull Request v6.1.0</h2>
<p> Adds <code>pull-request-branch</code> as an action output.</p>
<h2>What's Changed</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8867c4aba1"><code>8867c4a</code></a>
fix: handle ambiguous argument failure on diff stat (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3312">#3312</a>)</li>
<li><a
href="6073f5434b"><code>6073f54</code></a>
build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3291">#3291</a>)</li>
<li><a
href="6d01b5601c"><code>6d01b56</code></a>
build(deps-dev): bump eslint-plugin-import from 2.29.1 to 2.30.0 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3290">#3290</a>)</li>
<li><a
href="25cf8451c3"><code>25cf845</code></a>
build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
7.17.0 to 7.18.0 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3289">#3289</a>)</li>
<li><a
href="d87b980a0e"><code>d87b980</code></a>
build(deps-dev): bump <code>@​types/node</code> from 18.19.46 to
18.19.48 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3288">#3288</a>)</li>
<li><a
href="119d131ea9"><code>119d131</code></a>
build(deps): bump peter-evans/create-pull-request from 6 to 7 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3283">#3283</a>)</li>
<li><a
href="73e6230af4"><code>73e6230</code></a>
docs: update readme</li>
<li><a
href="c0348e860f"><code>c0348e8</code></a>
ci: add v7 to workflow</li>
<li><a
href="4320041ed3"><code>4320041</code></a>
feat: signed commits (v7) (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3057">#3057</a>)</li>
<li><a
href="0c2a66fe4a"><code>0c2a66f</code></a>
build(deps-dev): bump ts-jest from 29.2.4 to 29.2.5 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3256">#3256</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/peter-evans/create-pull-request/compare/v6...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/create-pull-request&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 14:44:00 +01:00
cyan317
0679034978 fix: Don't forward ledger API if 'full' is a string (#1640)
Fix #1635
2024-09-25 14:43:59 +01:00
github-actions[bot]
b41ea34212 style: clang-tidy auto fixes (#1639)
Fixes #1638. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-09-25 14:43:59 +01:00
Sergey Kuznetsov
4e147deafa fix: Subscription source bugs fix (#1626) (#1633)
Fixes #1620.
Cherry pick of #1626 into develop.

- Add timeouts for websocket operations for connections to rippled.
Without these timeouts if connection hangs for some reason, clio
wouldn't know the connection is hanging.
- Fix potential data race in choosing new subscription source which will
forward messages to users.
- Optimise switching between subscription sources.
2024-09-25 14:43:59 +01:00
Sergey Kuznetsov
b08447e8e0 fix: Fix logging in SubscriptionSource (#1617) (#1632)
Fixes #1616. 
Cherry pick of #1617 into develop.
2024-09-25 14:43:59 +01:00
cyan317
9432165ace refactor: Remove SubscriptionManagerRunner (#1623) 2024-09-25 14:43:58 +01:00
github-actions[bot]
3b6a87249c style: clang-tidy auto fixes (#1631)
Fixes #1630. Please review and commit clang-tidy fixes.

Co-authored-by: kuznetsss <15742918+kuznetsss@users.noreply.github.com>
2024-09-25 14:43:58 +01:00
Sergey Kuznetsov
b7449f72b7 test: Add test for WsConnection for ping response (#1619) 2024-09-25 14:43:58 +01:00
cyan317
443c74436e fix: not forward admin API (#1628) 2024-09-25 14:43:57 +01:00
Peter Chen
7b5e02731d fix: AccountNFT with invalid marker (#1589)
Fixes [#1497](https://github.com/XRPLF/clio/issues/1497)
Mimics the behavior of the [fix on Rippled
side](https://github.com/XRPLF/rippled/pull/5045)
2024-08-27 15:38:19 -04:00
196 changed files with 15038 additions and 2394 deletions

View File

@@ -26,12 +26,12 @@ sources="src tests"
formatter="clang-format -i" formatter="clang-format -i"
version=$($formatter --version | grep -o '[0-9\.]*') version=$($formatter --version | grep -o '[0-9\.]*')
if [[ "18.0.0" > "$version" ]]; then if [[ "19.0.0" > "$version" ]]; then
cat <<EOF cat <<EOF
ERROR ERROR
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
A minimum of version 18 of `which clang-format` is required. A minimum of version 19 of `which clang-format` is required.
Your version is $version. Your version is $version.
Please fix paths and run again. Please fix paths and run again.
----------------------------------------------------------------------------- -----------------------------------------------------------------------------

View File

@@ -42,7 +42,7 @@ verify_tag_signed() {
while read local_ref local_oid remote_ref remote_oid; do while read local_ref local_oid remote_ref remote_oid; do
# Check some things if we're pushing a branch called "release/" # Check some things if we're pushing a branch called "release/"
if echo "$remote_ref" | grep ^refs\/heads\/release\/ &> /dev/null ; then if echo "$remote_ref" | grep ^refs\/heads\/release\/ &> /dev/null ; then
version=$(echo $remote_ref | awk -F/ '{print $NF}') version=$(git tag --points-at HEAD)
echo "Looks like you're trying to push a $version release..." echo "Looks like you're trying to push a $version release..."
echo "Making sure you've signed and tagged it." echo "Making sure you've signed and tagged it."
if verify_commit_signed && verify_tag && verify_tag_signed ; then if verify_commit_signed && verify_tag && verify_tag_signed ; then

View File

@@ -149,13 +149,6 @@ jobs:
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }} name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }}
path: build/clio_*tests path: build/clio_*tests
- name: Upload test data
if: ${{ !matrix.code_coverage }}
uses: actions/upload-artifact@v4
with:
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }}
path: build/tests/unit/test_data
- name: Save cache - name: Save cache
uses: ./.github/actions/save_cache uses: ./.github/actions/save_cache
with: with:
@@ -219,11 +212,6 @@ jobs:
with: with:
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }} name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }}
- uses: actions/download-artifact@v4
with:
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }}
path: tests/unit/test_data
- name: Run clio_tests - name: Run clio_tests
run: | run: |
chmod +x ./clio_tests chmod +x ./clio_tests

View File

@@ -10,7 +10,7 @@ jobs:
# permissions: # permissions:
# pull-requests: write # pull-requests: write
steps: steps:
- uses: ytanikin/PRConventionalCommits@1.2.0 - uses: ytanikin/PRConventionalCommits@1.3.0
with: with:
task_types: '["build","feat","fix","docs","test","ci","style","refactor","perf","chore"]' task_types: '["build","feat","fix","docs","test","ci","style","refactor","perf","chore"]'
add_label: false add_label: false

View File

@@ -60,7 +60,7 @@ jobs:
shell: bash shell: bash
id: run_clang_tidy id: run_clang_tidy
run: | run: |
run-clang-tidy-18 -p build -j ${{ steps.number_of_threads.outputs.threads_number }} -fix -quiet 1>output.txt run-clang-tidy-19 -p build -j ${{ steps.number_of_threads.outputs.threads_number }} -fix -quiet 1>output.txt
- name: Check format - name: Check format
if: ${{ steps.run_clang_tidy.outcome != 'success' }} if: ${{ steps.run_clang_tidy.outcome != 'success' }}
@@ -99,7 +99,7 @@ jobs:
- name: Create PR with fixes - name: Create PR with fixes
if: ${{ steps.run_clang_tidy.outcome != 'success' }} if: ${{ steps.run_clang_tidy.outcome != 'success' }}
uses: peter-evans/create-pull-request@v6 uses: peter-evans/create-pull-request@v7
env: env:
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}

View File

@@ -71,12 +71,6 @@ jobs:
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }} name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}
path: build/clio_*tests path: build/clio_*tests
- name: Upload test data
uses: actions/upload-artifact@v4
with:
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}
path: build/tests/unit/test_data
- name: Compress clio_server - name: Compress clio_server
shell: bash shell: bash
run: | run: |
@@ -130,11 +124,6 @@ jobs:
with: with:
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }} name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}
- uses: actions/download-artifact@v4
with:
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}
path: tests/unit/test_data
- name: Run clio_tests - name: Run clio_tests
run: | run: |
chmod +x ./clio_tests chmod +x ./clio_tests

View File

@@ -23,7 +23,7 @@ jobs:
- name: Upload coverage report - name: Upload coverage report
if: ${{ hashFiles('build/coverage_report.xml') != '' }} if: ${{ hashFiles('build/coverage_report.xml') != '' }}
uses: wandalen/wretry.action@v3.5.0 uses: wandalen/wretry.action@v3.7.2
with: with:
action: codecov/codecov-action@v4 action: codecov/codecov-action@v4
with: | with: |

View File

@@ -21,7 +21,7 @@ git config --local core.hooksPath .githooks
``` ```
## Git hooks dependencies ## Git hooks dependencies
The pre-commit hook requires `clang-format >= 18.0.0` and `cmake-format` to be installed on your machine. The pre-commit hook requires `clang-format >= 19.0.0` and `cmake-format` to be installed on your machine.
`clang-format` can be installed using `brew` on macOS and default package manager on Linux. `clang-format` can be installed using `brew` on macOS and default package manager on Linux.
`cmake-format` can be installed using `pip`. `cmake-format` can be installed using `pip`.
The hook will also attempt to automatically use `doxygen` to verify that everything public in the codebase is covered by doc comments. If `doxygen` is not installed, the hook will raise a warning suggesting to install `doxygen` for future commits. The hook will also attempt to automatically use `doxygen` to verify that everything public in the codebase is covered by doc comments. If `doxygen` is not installed, the hook will raise a warning suggesting to install `doxygen` for future commits.
@@ -105,7 +105,7 @@ The button for that is near the bottom of the PR's page on GitHub.
This is a non-exhaustive list of recommended style guidelines. These are not always strictly enforced and serve as a way to keep the codebase coherent. This is a non-exhaustive list of recommended style guidelines. These are not always strictly enforced and serve as a way to keep the codebase coherent.
## Formatting ## Formatting
Code must conform to `clang-format` version 18, unless the result would be unreasonably difficult to read or maintain. Code must conform to `clang-format` version 19, unless the result would be unreasonably difficult to read or maintain.
In most cases the pre-commit hook will take care of formatting and will fix any issues automatically. In most cases the pre-commit hook will take care of formatting and will fix any issues automatically.
To manually format your code, use `clang-format -i <your changed files>` for C++ files and `cmake-format -i <your changed files>` for CMake files. To manually format your code, use `clang-format -i <your changed files>` for C++ files and `cmake-format -i <your changed files>` for CMake files.

View File

@@ -8,7 +8,7 @@ if (lint)
endif () endif ()
message(STATUS "Using clang-tidy from CLIO_CLANG_TIDY_BIN") message(STATUS "Using clang-tidy from CLIO_CLANG_TIDY_BIN")
else () else ()
find_program(_CLANG_TIDY_BIN NAMES "clang-tidy-18" "clang-tidy" REQUIRED) find_program(_CLANG_TIDY_BIN NAMES "clang-tidy-19" "clang-tidy" REQUIRED)
endif () endif ()
if (NOT _CLANG_TIDY_BIN) if (NOT _CLANG_TIDY_BIN)

View File

@@ -28,7 +28,8 @@ class Clio(ConanFile):
'protobuf/3.21.9', 'protobuf/3.21.9',
'grpc/1.50.1', 'grpc/1.50.1',
'openssl/1.1.1u', 'openssl/1.1.1u',
'xrpl/2.3.0-b1', 'xrpl/2.3.0',
'zlib/1.3.1',
'libbacktrace/cci.20210118' 'libbacktrace/cci.20210118'
] ]

View File

@@ -7,7 +7,7 @@ USER root
WORKDIR /root WORKDIR /root
ENV CCACHE_VERSION=4.10.2 \ ENV CCACHE_VERSION=4.10.2 \
LLVM_TOOLS_VERSION=18 \ LLVM_TOOLS_VERSION=19 \
GH_VERSION=2.40.0 \ GH_VERSION=2.40.0 \
DOXYGEN_VERSION=1.12.0 DOXYGEN_VERSION=1.12.0

View File

@@ -39,6 +39,9 @@
"cache_timeout": 0.250, // in seconds, could be 0, which means no cache "cache_timeout": 0.250, // in seconds, could be 0, which means no cache
"request_timeout": 10.0 // time for Clio to wait for rippled to reply on a forwarded request (default is 10 seconds) "request_timeout": 10.0 // time for Clio to wait for rippled to reply on a forwarded request (default is 10 seconds)
}, },
"rpc": {
"cache_timeout": 0.5 // in seconds, could be 0, which means no cache for rpc
},
"dos_guard": { "dos_guard": {
// Comma-separated list of IPs to exclude from rate limiting // Comma-separated list of IPs to exclude from rate limiting
"whitelist": [ "whitelist": [
@@ -67,7 +70,14 @@
"admin_password": "xrp", "admin_password": "xrp",
// If local_admin is true, Clio will consider requests come from 127.0.0.1 as admin requests // If local_admin is true, Clio will consider requests come from 127.0.0.1 as admin requests
// It's true by default unless admin_password is set,'local_admin' : true and 'admin_password' can not be set at the same time // It's true by default unless admin_password is set,'local_admin' : true and 'admin_password' can not be set at the same time
"local_admin": false "local_admin": false,
"processing_policy": "parallel", // Could be "sequent" or "parallel".
// For sequent policy request from one client connection will be processed one by one and the next one will not be read before
// the previous one is processed. For parallel policy Clio will take all requests and process them in parallel and
// send a reply for each request whenever it is ready.
"parallel_requests_limit": 10, // Optional parameter, used only if "processing_strategy" is "parallel". It limits the number of requests for one client connection processed in parallel. Infinite if not specified.
// Max number of responses to queue up before sent successfully. If a client's waiting queue is too long, the server will close the connection.
"ws_max_sending_queue_size": 1500
}, },
// Time in seconds for graceful shutdown. Defaults to 10 seconds. Not fully implemented yet. // Time in seconds for graceful shutdown. Defaults to 10 seconds. Not fully implemented yet.
"graceful_period": 10.0, "graceful_period": 10.0,

View File

@@ -14,7 +14,7 @@ You can find an example docker-compose file, with Prometheus and Grafana configs
## Using `clang-tidy` for static analysis ## Using `clang-tidy` for static analysis
The minimum [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) version required is 17.0. The minimum [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) version required is 19.0.
Clang-tidy can be run by Cmake when building the project. To achieve this, you just need to provide the option `-o lint=True` for the `conan install` command: Clang-tidy can be run by Cmake when building the project. To achieve this, you just need to provide the option `-o lint=True` for the `conan install` command:
@@ -26,5 +26,5 @@ By default Cmake will try to find `clang-tidy` automatically in your system.
To force Cmake to use your desired binary, set the `CLIO_CLANG_TIDY_BIN` environment variable to the path of the `clang-tidy` binary. For example: To force Cmake to use your desired binary, set the `CLIO_CLANG_TIDY_BIN` environment variable to the path of the `clang-tidy` binary. For example:
```sh ```sh
export CLIO_CLANG_TIDY_BIN=/opt/homebrew/opt/llvm@17/bin/clang-tidy export CLIO_CLANG_TIDY_BIN=/opt/homebrew/opt/llvm@19/bin/clang-tidy
``` ```

View File

@@ -1,6 +1,7 @@
add_subdirectory(util) add_subdirectory(util)
add_subdirectory(data) add_subdirectory(data)
add_subdirectory(etl) add_subdirectory(etl)
add_subdirectory(etlng)
add_subdirectory(feed) add_subdirectory(feed)
add_subdirectory(rpc) add_subdirectory(rpc)
add_subdirectory(web) add_subdirectory(web)

View File

@@ -1,4 +1,4 @@
add_library(clio_app) add_library(clio_app)
target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp) target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp)
target_link_libraries(clio_app PUBLIC clio_etl clio_feed clio_web clio_rpc) target_link_libraries(clio_app PUBLIC clio_etl clio_etlng clio_feed clio_web clio_rpc)

View File

@@ -101,9 +101,7 @@ ClioApplication::run()
auto backend = data::make_Backend(config_); auto backend = data::make_Backend(config_);
// Manages clients subscribed to streams // Manages clients subscribed to streams
auto subscriptionsRunner = feed::SubscriptionManagerRunner(config_, backend); auto subscriptions = feed::SubscriptionManager::make_SubscriptionManager(config_, backend);
auto const subscriptions = subscriptionsRunner.getManager();
// Tracks which ledgers have been validated by the network // Tracks which ledgers have been validated by the network
auto ledgers = etl::NetworkValidatedLedgers::make_ValidatedLedgers(); auto ledgers = etl::NetworkValidatedLedgers::make_ValidatedLedgers();
@@ -123,12 +121,14 @@ ClioApplication::run()
auto const handlerProvider = std::make_shared<rpc::impl::ProductionHandlerProvider const>( auto const handlerProvider = std::make_shared<rpc::impl::ProductionHandlerProvider const>(
config_, backend, subscriptions, balancer, etl, amendmentCenter, counters config_, backend, subscriptions, balancer, etl, amendmentCenter, counters
); );
using RPCEngineType = rpc::RPCEngine<etl::LoadBalancer, rpc::Counters>;
auto const rpcEngine = auto const rpcEngine =
rpc::RPCEngine::make_RPCEngine(backend, balancer, dosGuard, workQueue, counters, handlerProvider); RPCEngineType::make_RPCEngine(config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider);
// Init the web server // Init the web server
auto handler = auto handler =
std::make_shared<web::RPCServerHandler<rpc::RPCEngine, etl::ETLService>>(config_, backend, rpcEngine, etl); std::make_shared<web::RPCServerHandler<RPCEngineType, etl::ETLService>>(config_, backend, rpcEngine, etl);
auto const httpServer = web::make_HttpServer(config_, ioc, dosGuard, handler); auto const httpServer = web::make_HttpServer(config_, ioc, dosGuard, handler);
// Blocks until stopped. // Blocks until stopped.

View File

@@ -124,6 +124,13 @@ struct Amendments {
REGISTER(NFTokenMintOffer); REGISTER(NFTokenMintOffer);
REGISTER(fixReducedOffersV2); REGISTER(fixReducedOffersV2);
REGISTER(fixEnforceNFTokenTrustline); REGISTER(fixEnforceNFTokenTrustline);
REGISTER(fixInnerObjTemplate2);
REGISTER(fixNFTokenPageLinks);
REGISTER(InvariantsV1_1);
REGISTER(MPTokensV1);
REGISTER(fixAMMv1_2);
REGISTER(AMMClawback);
REGISTER(Credentials);
// Obsolete but supported by libxrpl // Obsolete but supported by libxrpl
REGISTER(CryptoConditionsSuite); REGISTER(CryptoConditionsSuite);

View File

@@ -364,6 +364,25 @@ public:
boost::asio::yield_context yield boost::asio::yield_context yield
) const = 0; ) const = 0;
/**
* @brief Fetches all holders' balances for a MPTIssuanceID
*
* @param mptID MPTIssuanceID you wish you query.
* @param limit Paging limit.
* @param cursorIn Optional cursor to allow us to pick up from where we last left off.
* @param ledgerSequence The ledger sequence to fetch for
* @param yield Currently executing coroutine.
* @return std::vector<Blob> of MPToken balances and an optional marker
*/
virtual MPTHoldersAndCursor
fetchMPTHolders(
ripple::uint192 const& mptID,
std::uint32_t const limit,
std::optional<ripple::AccountID> const& cursorIn,
std::uint32_t const ledgerSequence,
boost::asio::yield_context yield
) const = 0;
/** /**
* @brief Fetches a specific ledger object. * @brief Fetches a specific ledger object.
* *
@@ -617,6 +636,14 @@ public:
virtual void virtual void
writeNFTTransactions(std::vector<NFTTransactionsData> const& data) = 0; writeNFTTransactions(std::vector<NFTTransactionsData> const& data) = 0;
/**
* @brief Write accounts that started holding onto a MPT.
*
* @param data A vector of MPT ID and account pairs
*/
virtual void
writeMPTHolders(std::vector<MPTHolderData> const& data) = 0;
/** /**
* @brief Write a new successor. * @brief Write a new successor.
* *

View File

@@ -547,6 +547,45 @@ public:
return ret; return ret;
} }
MPTHoldersAndCursor
fetchMPTHolders(
ripple::uint192 const& mptID,
std::uint32_t const limit,
std::optional<ripple::AccountID> const& cursorIn,
std::uint32_t const ledgerSequence,
boost::asio::yield_context yield
) const override
{
auto const holderEntries = executor_.read(
yield, schema_->selectMPTHolders, mptID, cursorIn.value_or(ripple::AccountID(0)), Limit{limit}
);
auto const& holderResults = holderEntries.value();
if (not holderResults.hasRows()) {
LOG(log_.debug()) << "No rows returned";
return {};
}
std::vector<ripple::uint256> mptKeys;
std::optional<ripple::AccountID> cursor;
for (auto const [holder] : extract<ripple::AccountID>(holderResults)) {
mptKeys.push_back(ripple::keylet::mptoken(mptID, holder).key);
cursor = holder;
}
auto mptObjects = doFetchLedgerObjects(mptKeys, ledgerSequence, yield);
auto it = std::remove_if(mptObjects.begin(), mptObjects.end(), [](Blob const& mpt) { return mpt.empty(); });
mptObjects.erase(it, mptObjects.end());
ASSERT(mptKeys.size() <= limit, "Number of keys can't exceed the limit");
if (mptKeys.size() == limit)
return {mptObjects, cursor};
return {mptObjects, {}};
}
std::optional<Blob> std::optional<Blob>
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield) doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
const override const override
@@ -905,6 +944,17 @@ public:
executor_.write(std::move(statements)); executor_.write(std::move(statements));
} }
void
writeMPTHolders(std::vector<MPTHolderData> const& data) override
{
std::vector<Statement> statements;
statements.reserve(data.size());
for (auto [mptId, holder] : data)
statements.push_back(schema_->insertMPTHolder.bind(std::move(mptId), std::move(holder)));
executor_.write(std::move(statements));
}
void void
startWrites() const override startWrites() const override
{ {

View File

@@ -172,6 +172,14 @@ struct NFTsData {
} }
}; };
/**
* @brief Represents an MPT and holder pair
*/
struct MPTHolderData {
ripple::uint192 mptID;
ripple::AccountID holder;
};
/** /**
* @brief Check whether the supplied object is an offer. * @brief Check whether the supplied object is an offer.
* *

View File

@@ -233,6 +233,14 @@ struct NFTsAndCursor {
std::optional<ripple::uint256> cursor; std::optional<ripple::uint256> cursor;
}; };
/**
* @brief Represents an array of MPTokens
*/
struct MPTHoldersAndCursor {
std::vector<Blob> mptokens;
std::optional<ripple::AccountID> cursor;
};
/** /**
* @brief Stores a range of sequences as a min and max pair. * @brief Stores a range of sequences as a min and max pair.
*/ */

View File

@@ -257,6 +257,19 @@ public:
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions") qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")
)); ));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
mpt_id blob,
holder blob,
PRIMARY KEY (mpt_id, holder)
)
WITH CLUSTERING ORDER BY (holder ASC)
)",
qualifiedTableName(settingsProvider_.get(), "mp_token_holders")
));
return statements; return statements;
}(); }();
@@ -393,6 +406,17 @@ public:
)); ));
}(); }();
PreparedStatement insertMPTHolder = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(mpt_id, holder)
VALUES (?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "mp_token_holders")
));
}();
PreparedStatement insertLedgerHeader = [this]() { PreparedStatement insertLedgerHeader = [this]() {
return handle_.get().prepare(fmt::format( return handle_.get().prepare(fmt::format(
R"( R"(
@@ -687,6 +711,20 @@ public:
)); ));
}(); }();
PreparedStatement selectMPTHolders = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT holder
FROM {}
WHERE mpt_id = ?
AND holder > ?
ORDER BY holder ASC
LIMIT ?
)",
qualifiedTableName(settingsProvider_.get(), "mp_token_holders")
));
}();
PreparedStatement selectLedgerByHash = [this]() { PreparedStatement selectLedgerByHash = [this]() {
return handle_.get().prepare(fmt::format( return handle_.get().prepare(fmt::format(
R"( R"(

View File

@@ -106,9 +106,9 @@ public:
using UintByteTupleType = std::tuple<uint32_t, ripple::uint256>; using UintByteTupleType = std::tuple<uint32_t, ripple::uint256>;
using ByteVectorType = std::vector<ripple::uint256>; using ByteVectorType = std::vector<ripple::uint256>;
if constexpr (std::is_same_v<DecayedType, ripple::uint256>) { if constexpr (std::is_same_v<DecayedType, ripple::uint256> || std::is_same_v<DecayedType, ripple::uint192>) {
auto const rc = bindBytes(value.data(), value.size()); auto const rc = bindBytes(value.data(), value.size());
throwErrorIfNeeded(rc, "Bind ripple::uint256"); throwErrorIfNeeded(rc, "Bind ripple::base_uint");
} else if constexpr (std::is_same_v<DecayedType, ripple::AccountID>) { } else if constexpr (std::is_same_v<DecayedType, ripple::AccountID>) {
auto const rc = bindBytes(value.data(), value.size()); auto const rc = bindBytes(value.data(), value.size());
throwErrorIfNeeded(rc, "Bind ripple::AccountID"); throwErrorIfNeeded(rc, "Bind ripple::AccountID");

View File

@@ -10,8 +10,8 @@ target_sources(
NetworkValidatedLedgers.cpp NetworkValidatedLedgers.cpp
NFTHelpers.cpp NFTHelpers.cpp
Source.cpp Source.cpp
MPTHelpers.cpp
impl/AmendmentBlockHandler.cpp impl/AmendmentBlockHandler.cpp
impl/ForwardingCache.cpp
impl/ForwardingSource.cpp impl/ForwardingSource.cpp
impl/GrpcSource.cpp impl/GrpcSource.cpp
impl/SubscriptionSource.cpp impl/SubscriptionSource.cpp

View File

@@ -27,6 +27,7 @@
#include "rpc/Errors.hpp" #include "rpc/Errors.hpp"
#include "util/Assert.hpp" #include "util/Assert.hpp"
#include "util/Random.hpp" #include "util/Random.hpp"
#include "util/ResponseExpirationCache.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
@@ -34,6 +35,7 @@
#include <boost/json/array.hpp> #include <boost/json/array.hpp>
#include <boost/json/object.hpp> #include <boost/json/object.hpp>
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <fmt/core.h> #include <fmt/core.h>
#include <algorithm> #include <algorithm>
@@ -79,7 +81,10 @@ LoadBalancer::LoadBalancer(
{ {
auto const forwardingCacheTimeout = config.valueOr<float>("forwarding.cache_timeout", 0.f); auto const forwardingCacheTimeout = config.valueOr<float>("forwarding.cache_timeout", 0.f);
if (forwardingCacheTimeout > 0.f) { if (forwardingCacheTimeout > 0.f) {
forwardingCache_ = impl::ForwardingCache{Config::toMilliseconds(forwardingCacheTimeout)}; forwardingCache_ = util::ResponseExpirationCache{
Config::toMilliseconds(forwardingCacheTimeout),
{"server_info", "server_state", "server_definitions", "fee", "ledger_closed"}
};
} }
static constexpr std::uint32_t MAX_DOWNLOAD = 256; static constexpr std::uint32_t MAX_DOWNLOAD = 256;
@@ -111,10 +116,13 @@ LoadBalancer::LoadBalancer(
validatedLedgers, validatedLedgers,
forwardingTimeout, forwardingTimeout,
[this]() { [this]() {
if (not hasForwardingSource_) if (not hasForwardingSource_.lock().get())
chooseForwardingSource();
},
[this](bool wasForwarding) {
if (wasForwarding)
chooseForwardingSource(); chooseForwardingSource();
}, },
[this]() { chooseForwardingSource(); },
[this]() { [this]() {
if (forwardingCache_.has_value()) if (forwardingCache_.has_value())
forwardingCache_->invalidate(); forwardingCache_->invalidate();
@@ -221,8 +229,12 @@ LoadBalancer::forwardToRippled(
boost::asio::yield_context yield boost::asio::yield_context yield
) )
{ {
if (not request.contains("command"))
return std::unexpected{rpc::ClioError::rpcCOMMAND_IS_MISSING};
auto const cmd = boost::json::value_to<std::string>(request.at("command"));
if (forwardingCache_) { if (forwardingCache_) {
if (auto cachedResponse = forwardingCache_->get(request); cachedResponse) { if (auto cachedResponse = forwardingCache_->get(cmd); cachedResponse) {
return std::move(cachedResponse).value(); return std::move(cachedResponse).value();
} }
} }
@@ -250,7 +262,7 @@ LoadBalancer::forwardToRippled(
if (response) { if (response) {
if (forwardingCache_ and not response->contains("error")) if (forwardingCache_ and not response->contains("error"))
forwardingCache_->put(request, *response); forwardingCache_->put(cmd, *response);
return std::move(response).value(); return std::move(response).value();
} }
@@ -322,11 +334,13 @@ LoadBalancer::getETLState() noexcept
void void
LoadBalancer::chooseForwardingSource() LoadBalancer::chooseForwardingSource()
{ {
hasForwardingSource_ = false; LOG(log_.info()) << "Choosing a new source to forward subscriptions";
auto hasForwardingSourceLock = hasForwardingSource_.lock();
hasForwardingSourceLock.get() = false;
for (auto& source : sources_) { for (auto& source : sources_) {
if (not hasForwardingSource_ and source->isConnected()) { if (not hasForwardingSourceLock.get() and source->isConnected()) {
source->setForwarding(true); source->setForwarding(true);
hasForwardingSource_ = true; hasForwardingSourceLock.get() = true;
} else { } else {
source->setForwarding(false); source->setForwarding(false);
} }

View File

@@ -23,9 +23,10 @@
#include "etl/ETLState.hpp" #include "etl/ETLState.hpp"
#include "etl/NetworkValidatedLedgersInterface.hpp" #include "etl/NetworkValidatedLedgersInterface.hpp"
#include "etl/Source.hpp" #include "etl/Source.hpp"
#include "etl/impl/ForwardingCache.hpp"
#include "feed/SubscriptionManagerInterface.hpp" #include "feed/SubscriptionManagerInterface.hpp"
#include "rpc/Errors.hpp" #include "rpc/Errors.hpp"
#include "util/Mutex.hpp"
#include "util/ResponseExpirationCache.hpp"
#include "util/config/Config.hpp" #include "util/config/Config.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
@@ -39,7 +40,6 @@
#include <org/xrpl/rpc/v1/ledger.pb.h> #include <org/xrpl/rpc/v1/ledger.pb.h>
#include <xrpl/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h> #include <xrpl/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
#include <atomic>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <expected> #include <expected>
@@ -69,14 +69,17 @@ private:
util::Logger log_{"ETL"}; util::Logger log_{"ETL"};
// Forwarding cache must be destroyed after sources because sources have a callback to invalidate cache // Forwarding cache must be destroyed after sources because sources have a callback to invalidate cache
std::optional<impl::ForwardingCache> forwardingCache_; std::optional<util::ResponseExpirationCache> forwardingCache_;
std::optional<std::string> forwardingXUserValue_; std::optional<std::string> forwardingXUserValue_;
std::vector<SourcePtr> sources_; std::vector<SourcePtr> sources_;
std::optional<ETLState> etlState_; std::optional<ETLState> etlState_;
std::uint32_t downloadRanges_ = std::uint32_t downloadRanges_ =
DEFAULT_DOWNLOAD_RANGES; /*< The number of markers to use when downloading initial ledger */ DEFAULT_DOWNLOAD_RANGES; /*< The number of markers to use when downloading initial ledger */
std::atomic_bool hasForwardingSource_{false};
// Using mutext instead of atomic_bool because choosing a new source to
// forward messages should be done with a mutual exclusion otherwise there will be a race condition
util::Mutex<bool> hasForwardingSource_{false};
public: public:
/** /**

83
src/etl/MPTHelpers.cpp Normal file
View File

@@ -0,0 +1,83 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "data/DBHelpers.hpp"
#include <ripple/protocol/STBase.h>
#include <ripple/protocol/STTx.h>
#include <ripple/protocol/TxMeta.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <optional>
#include <string>
namespace etl {
/**
* @brief Get the MPToken created from a transaction
*
* @param txMeta Transaction metadata
* @return MPT and holder account pair
*/
static std::optional<MPTHolderData>
getMPTokenAuthorize(ripple::TxMeta const& txMeta)
{
for (ripple::STObject const& node : txMeta.getNodes()) {
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltMPTOKEN)
continue;
if (node.getFName() == ripple::sfCreatedNode) {
auto const& newMPT = node.peekAtField(ripple::sfNewFields).downcast<ripple::STObject>();
return MPTHolderData{newMPT[ripple::sfMPTokenIssuanceID], newMPT.getAccountID(ripple::sfAccount)};
}
}
return {};
}
std::optional<MPTHolderData>
getMPTHolderFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx)
{
if (txMeta.getResultTER() != ripple::tesSUCCESS || sttx.getTxnType() != ripple::TxType::ttMPTOKEN_AUTHORIZE)
return {};
return getMPTokenAuthorize(txMeta);
}
std::optional<MPTHolderData>
getMPTHolderFromObj(std::string const& key, std::string const& blob)
{
ripple::STLedgerEntry const sle =
ripple::STLedgerEntry(ripple::SerialIter{blob.data(), blob.size()}, ripple::uint256::fromVoid(key.data()));
if (sle.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltMPTOKEN)
return {};
auto const mptIssuanceID = sle[ripple::sfMPTokenIssuanceID];
auto const holder = sle.getAccountID(ripple::sfAccount);
return MPTHolderData{mptIssuanceID, holder};
}
} // namespace etl

50
src/etl/MPTHelpers.hpp Normal file
View File

@@ -0,0 +1,50 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
/** @file */
#pragma once
#include "data/DBHelpers.hpp"
#include <ripple/protocol/STTx.h>
#include <ripple/protocol/TxMeta.h>
namespace etl {
/**
* @brief Pull MPT data from TX via ETLService.
*
* @param txMeta Transaction metadata
* @param sttx The transaction
* @return The MPTIssuanceID and holder pair as a optional
*/
std::optional<MPTHolderData>
getMPTHolderFromTx(ripple::TxMeta const& txMeta, ripple::STTx const& sttx);
/**
* @brief Pull MPT data from ledger object via loadInitialLedger.
*
* @param key The owner key
* @param blob Object data as blob
* @return The MPTIssuanceID and holder pair as a optional
*/
std::optional<MPTHolderData>
getMPTHolderFromObj(std::string const& key, std::string const& blob);
} // namespace etl

View File

@@ -53,7 +53,7 @@ namespace etl {
class SourceBase { class SourceBase {
public: public:
using OnConnectHook = std::function<void()>; using OnConnectHook = std::function<void()>;
using OnDisconnectHook = std::function<void()>; using OnDisconnectHook = std::function<void(bool)>;
using OnLedgerClosedHook = std::function<void()>; using OnLedgerClosedHook = std::function<void()>;
virtual ~SourceBase() = default; virtual ~SourceBase() = default;

View File

@@ -22,6 +22,7 @@
#include "data/BackendInterface.hpp" #include "data/BackendInterface.hpp"
#include "data/Types.hpp" #include "data/Types.hpp"
#include "etl/ETLHelpers.hpp" #include "etl/ETLHelpers.hpp"
#include "etl/MPTHelpers.hpp"
#include "etl/NFTHelpers.hpp" #include "etl/NFTHelpers.hpp"
#include "util/Assert.hpp" #include "util/Assert.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
@@ -154,6 +155,11 @@ public:
backend.writeSuccessor(std::move(lastKey_), request_.ledger().sequence(), std::string{obj.key()}); backend.writeSuccessor(std::move(lastKey_), request_.ledger().sequence(), std::string{obj.key()});
lastKey_ = obj.key(); lastKey_ = obj.key();
backend.writeNFTs(getNFTDataFromObj(request_.ledger().sequence(), obj.key(), obj.data())); backend.writeNFTs(getNFTDataFromObj(request_.ledger().sequence(), obj.key(), obj.data()));
auto const maybeMPTHolder = getMPTHolderFromObj(obj.key(), obj.data());
if (maybeMPTHolder)
backend.writeMPTHolders({*maybeMPTHolder});
backend.writeLedgerObject( backend.writeLedgerObject(
std::move(*obj.mutable_key()), request_.ledger().sequence(), std::move(*obj.mutable_data()) std::move(*obj.mutable_key()), request_.ledger().sequence(), std::move(*obj.mutable_data())
); );

View File

@@ -47,7 +47,7 @@
namespace etl::impl { namespace etl::impl {
GrpcSource::GrpcSource(std::string const& ip, std::string const& grpcPort, std::shared_ptr<BackendInterface> backend) GrpcSource::GrpcSource(std::string const& ip, std::string const& grpcPort, std::shared_ptr<BackendInterface> backend)
: log_(fmt::format("ETL_Grpc[{}:{}]", ip, grpcPort)), backend_(std::move(backend)) : log_(fmt::format("GrpcSource[{}:{}]", ip, grpcPort)), backend_(std::move(backend))
{ {
try { try {
boost::asio::io_context ctx; boost::asio::io_context ctx;

View File

@@ -22,6 +22,7 @@
#include "data/BackendInterface.hpp" #include "data/BackendInterface.hpp"
#include "data/DBHelpers.hpp" #include "data/DBHelpers.hpp"
#include "data/Types.hpp" #include "data/Types.hpp"
#include "etl/MPTHelpers.hpp"
#include "etl/NFTHelpers.hpp" #include "etl/NFTHelpers.hpp"
#include "etl/SystemState.hpp" #include "etl/SystemState.hpp"
#include "etl/impl/LedgerFetcher.hpp" #include "etl/impl/LedgerFetcher.hpp"
@@ -55,6 +56,7 @@ struct FormattedTransactionsData {
std::vector<AccountTransactionsData> accountTxData; std::vector<AccountTransactionsData> accountTxData;
std::vector<NFTTransactionsData> nfTokenTxData; std::vector<NFTTransactionsData> nfTokenTxData;
std::vector<NFTsData> nfTokensData; std::vector<NFTsData> nfTokensData;
std::vector<MPTHolderData> mptHoldersData;
}; };
namespace etl::impl { namespace etl::impl {
@@ -124,6 +126,10 @@ public:
if (maybeNFT) if (maybeNFT)
result.nfTokensData.push_back(*maybeNFT); result.nfTokensData.push_back(*maybeNFT);
auto const maybeMPTHolder = getMPTHolderFromTx(txMeta, sttx);
if (maybeMPTHolder)
result.mptHoldersData.push_back(*maybeMPTHolder);
result.accountTxData.emplace_back(txMeta, sttx.getTransactionID()); result.accountTxData.emplace_back(txMeta, sttx.getTransactionID());
static constexpr std::size_t KEY_SIZE = 32; static constexpr std::size_t KEY_SIZE = 32;
std::string keyStr{reinterpret_cast<char const*>(sttx.getTransactionID().data()), KEY_SIZE}; std::string keyStr{reinterpret_cast<char const*>(sttx.getTransactionID().data()), KEY_SIZE};
@@ -240,6 +246,7 @@ public:
backend_->writeAccountTransactions(std::move(insertTxResult.accountTxData)); backend_->writeAccountTransactions(std::move(insertTxResult.accountTxData));
backend_->writeNFTs(insertTxResult.nfTokensData); backend_->writeNFTs(insertTxResult.nfTokensData);
backend_->writeNFTTransactions(insertTxResult.nfTokenTxData); backend_->writeNFTTransactions(insertTxResult.nfTokenTxData);
backend_->writeMPTHolders(insertTxResult.mptHoldersData);
} }
backend_->finishWrites(sequence); backend_->finishWrites(sequence);

View File

@@ -24,6 +24,8 @@
#include "rpc/JS.hpp" #include "rpc/JS.hpp"
#include "util/Retry.hpp" #include "util/Retry.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
#include "util/prometheus/Label.hpp"
#include "util/prometheus/Prometheus.hpp"
#include "util/requests/Types.hpp" #include "util/requests/Types.hpp"
#include <boost/algorithm/string/classification.hpp> #include <boost/algorithm/string/classification.hpp>
@@ -66,22 +68,28 @@ SubscriptionSource::SubscriptionSource(
OnConnectHook onConnect, OnConnectHook onConnect,
OnDisconnectHook onDisconnect, OnDisconnectHook onDisconnect,
OnLedgerClosedHook onLedgerClosed, OnLedgerClosedHook onLedgerClosed,
std::chrono::steady_clock::duration const connectionTimeout, std::chrono::steady_clock::duration const wsTimeout,
std::chrono::steady_clock::duration const retryDelay std::chrono::steady_clock::duration const retryDelay
) )
: log_(fmt::format("GrpcSource[{}:{}]", ip, wsPort)) : log_(fmt::format("SubscriptionSource[{}:{}]", ip, wsPort))
, wsConnectionBuilder_(ip, wsPort) , wsConnectionBuilder_(ip, wsPort)
, validatedLedgers_(std::move(validatedLedgers)) , validatedLedgers_(std::move(validatedLedgers))
, subscriptions_(std::move(subscriptions)) , subscriptions_(std::move(subscriptions))
, strand_(boost::asio::make_strand(ioContext)) , strand_(boost::asio::make_strand(ioContext))
, wsTimeout_(wsTimeout)
, retry_(util::makeRetryExponentialBackoff(retryDelay, RETRY_MAX_DELAY, strand_)) , retry_(util::makeRetryExponentialBackoff(retryDelay, RETRY_MAX_DELAY, strand_))
, onConnect_(std::move(onConnect)) , onConnect_(std::move(onConnect))
, onDisconnect_(std::move(onDisconnect)) , onDisconnect_(std::move(onDisconnect))
, onLedgerClosed_(std::move(onLedgerClosed)) , onLedgerClosed_(std::move(onLedgerClosed))
, lastMessageTimeSecondsSinceEpoch_(PrometheusService::gaugeInt(
"subscription_source_last_message_time",
util::prometheus::Labels({{"source", fmt::format("{}:{}", ip, wsPort)}}),
"Seconds since epoch of the last message received from rippled subscription streams"
))
{ {
wsConnectionBuilder_.addHeader({boost::beast::http::field::user_agent, "clio-client"}) wsConnectionBuilder_.addHeader({boost::beast::http::field::user_agent, "clio-client"})
.addHeader({"X-User", "clio-client"}) .addHeader({"X-User", "clio-client"})
.setConnectionTimeout(connectionTimeout); .setConnectionTimeout(wsTimeout_);
} }
SubscriptionSource::~SubscriptionSource() SubscriptionSource::~SubscriptionSource()
@@ -133,6 +141,7 @@ void
SubscriptionSource::setForwarding(bool isForwarding) SubscriptionSource::setForwarding(bool isForwarding)
{ {
isForwarding_ = isForwarding; isForwarding_ = isForwarding;
LOG(log_.info()) << "Forwarding set to " << isForwarding_;
} }
std::chrono::steady_clock::time_point std::chrono::steady_clock::time_point
@@ -166,20 +175,22 @@ SubscriptionSource::subscribe()
} }
wsConnection_ = std::move(connection).value(); wsConnection_ = std::move(connection).value();
isConnected_ = true;
onConnect_();
auto const& subscribeCommand = getSubscribeCommandJson(); auto const& subscribeCommand = getSubscribeCommandJson();
auto const writeErrorOpt = wsConnection_->write(subscribeCommand, yield); auto const writeErrorOpt = wsConnection_->write(subscribeCommand, yield, wsTimeout_);
if (writeErrorOpt) { if (writeErrorOpt) {
handleError(writeErrorOpt.value(), yield); handleError(writeErrorOpt.value(), yield);
return; return;
} }
isConnected_ = true;
LOG(log_.info()) << "Connected";
onConnect_();
retry_.reset(); retry_.reset();
while (!stop_) { while (!stop_) {
auto const message = wsConnection_->read(yield); auto const message = wsConnection_->read(yield, wsTimeout_);
if (not message) { if (not message) {
handleError(message.error(), yield); handleError(message.error(), yield);
return; return;
@@ -224,10 +235,11 @@ SubscriptionSource::handleMessage(std::string const& message)
auto validatedLedgers = boost::json::value_to<std::string>(result.at(JS(validated_ledgers))); auto validatedLedgers = boost::json::value_to<std::string>(result.at(JS(validated_ledgers)));
setValidatedRange(std::move(validatedLedgers)); setValidatedRange(std::move(validatedLedgers));
} }
LOG(log_.info()) << "Received a message on ledger subscription stream. Message : " << object; LOG(log_.debug()) << "Received a message on ledger subscription stream. Message: " << object;
} else if (object.contains(JS(type)) && object.at(JS(type)) == JS_LedgerClosed) { } else if (object.contains(JS(type)) && object.at(JS(type)) == JS_LedgerClosed) {
LOG(log_.info()) << "Received a message on ledger subscription stream. Message : " << object; LOG(log_.debug()) << "Received a message of type 'ledgerClosed' on ledger subscription stream. Message: "
<< object;
if (object.contains(JS(ledger_index))) { if (object.contains(JS(ledger_index))) {
ledgerIndex = object.at(JS(ledger_index)).as_int64(); ledgerIndex = object.at(JS(ledger_index)).as_int64();
} }
@@ -245,10 +257,13 @@ SubscriptionSource::handleMessage(std::string const& message)
// 2 - Validated transaction // 2 - Validated transaction
// Only forward proposed transaction, validated transactions are sent by Clio itself // Only forward proposed transaction, validated transactions are sent by Clio itself
if (object.contains(JS(transaction)) and !object.contains(JS(meta))) { if (object.contains(JS(transaction)) and !object.contains(JS(meta))) {
LOG(log_.debug()) << "Forwarding proposed transaction: " << object;
subscriptions_->forwardProposedTransaction(object); subscriptions_->forwardProposedTransaction(object);
} else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ValidationReceived) { } else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ValidationReceived) {
LOG(log_.debug()) << "Forwarding validation: " << object;
subscriptions_->forwardValidation(object); subscriptions_->forwardValidation(object);
} else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ManifestReceived) { } else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ManifestReceived) {
LOG(log_.debug()) << "Forwarding manifest: " << object;
subscriptions_->forwardManifest(object); subscriptions_->forwardManifest(object);
} }
} }
@@ -261,7 +276,7 @@ SubscriptionSource::handleMessage(std::string const& message)
return std::nullopt; return std::nullopt;
} catch (std::exception const& e) { } catch (std::exception const& e) {
LOG(log_.error()) << "Exception in handleMessage : " << e.what(); LOG(log_.error()) << "Exception in handleMessage: " << e.what();
return util::requests::RequestError{fmt::format("Error handling message: {}", e.what())}; return util::requests::RequestError{fmt::format("Error handling message: {}", e.what())};
} }
} }
@@ -270,16 +285,14 @@ void
SubscriptionSource::handleError(util::requests::RequestError const& error, boost::asio::yield_context yield) SubscriptionSource::handleError(util::requests::RequestError const& error, boost::asio::yield_context yield)
{ {
isConnected_ = false; isConnected_ = false;
isForwarding_ = false; bool const wasForwarding = isForwarding_.exchange(false);
if (not stop_) { if (not stop_) {
onDisconnect_(); LOG(log_.info()) << "Disconnected";
onDisconnect_(wasForwarding);
} }
if (wsConnection_ != nullptr) { if (wsConnection_ != nullptr) {
auto const err = wsConnection_->close(yield); wsConnection_->close(yield);
if (err) {
LOG(log_.error()) << "Error closing websocket connection: " << err->message();
}
wsConnection_.reset(); wsConnection_.reset();
} }
@@ -306,7 +319,11 @@ SubscriptionSource::logError(util::requests::RequestError const& error) const
void void
SubscriptionSource::setLastMessageTime() SubscriptionSource::setLastMessageTime()
{ {
lastMessageTime_.lock().get() = std::chrono::steady_clock::now(); lastMessageTimeSecondsSinceEpoch_.get().set(
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count()
);
auto lock = lastMessageTime_.lock();
lock.get() = std::chrono::steady_clock::now();
} }
void void

View File

@@ -25,6 +25,7 @@
#include "util/Mutex.hpp" #include "util/Mutex.hpp"
#include "util/Retry.hpp" #include "util/Retry.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
#include "util/prometheus/Gauge.hpp"
#include "util/requests/Types.hpp" #include "util/requests/Types.hpp"
#include "util/requests/WsConnection.hpp" #include "util/requests/WsConnection.hpp"
@@ -37,6 +38,7 @@
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <functional>
#include <future> #include <future>
#include <memory> #include <memory>
#include <optional> #include <optional>
@@ -71,6 +73,8 @@ private:
boost::asio::strand<boost::asio::io_context::executor_type> strand_; boost::asio::strand<boost::asio::io_context::executor_type> strand_;
std::chrono::steady_clock::duration wsTimeout_;
util::Retry retry_; util::Retry retry_;
OnConnectHook onConnect_; OnConnectHook onConnect_;
@@ -83,9 +87,11 @@ private:
util::Mutex<std::chrono::steady_clock::time_point> lastMessageTime_; util::Mutex<std::chrono::steady_clock::time_point> lastMessageTime_;
std::reference_wrapper<util::prometheus::GaugeInt> lastMessageTimeSecondsSinceEpoch_;
std::future<void> runFuture_; std::future<void> runFuture_;
static constexpr std::chrono::seconds CONNECTION_TIMEOUT{30}; static constexpr std::chrono::seconds WS_TIMEOUT{30};
static constexpr std::chrono::seconds RETRY_MAX_DELAY{30}; static constexpr std::chrono::seconds RETRY_MAX_DELAY{30};
static constexpr std::chrono::seconds RETRY_DELAY{1}; static constexpr std::chrono::seconds RETRY_DELAY{1};
@@ -103,7 +109,7 @@ public:
* @param onDisconnect The onDisconnect hook. Called when the connection is lost * @param onDisconnect The onDisconnect hook. Called when the connection is lost
* @param onLedgerClosed The onLedgerClosed hook. Called when the ledger is closed but only if the source is * @param onLedgerClosed The onLedgerClosed hook. Called when the ledger is closed but only if the source is
* forwarding * forwarding
* @param connectionTimeout The connection timeout. Defaults to 30 seconds * @param wsTimeout A timeout for websocket operations. Defaults to 30 seconds
* @param retryDelay The retry delay. Defaults to 1 second * @param retryDelay The retry delay. Defaults to 1 second
*/ */
SubscriptionSource( SubscriptionSource(
@@ -115,7 +121,7 @@ public:
OnConnectHook onConnect, OnConnectHook onConnect,
OnDisconnectHook onDisconnect, OnDisconnectHook onDisconnect,
OnLedgerClosedHook onLedgerClosed, OnLedgerClosedHook onLedgerClosed,
std::chrono::steady_clock::duration const connectionTimeout = CONNECTION_TIMEOUT, std::chrono::steady_clock::duration const wsTimeout = WS_TIMEOUT,
std::chrono::steady_clock::duration const retryDelay = RETRY_DELAY std::chrono::steady_clock::duration const retryDelay = RETRY_DELAY
); );

View File

@@ -213,6 +213,7 @@ private:
backend_->writeAccountTransactions(std::move(insertTxResultOp->accountTxData)); backend_->writeAccountTransactions(std::move(insertTxResultOp->accountTxData));
backend_->writeNFTs(insertTxResultOp->nfTokensData); backend_->writeNFTs(insertTxResultOp->nfTokensData);
backend_->writeNFTTransactions(insertTxResultOp->nfTokenTxData); backend_->writeNFTTransactions(insertTxResultOp->nfTokenTxData);
backend_->writeMPTHolders(insertTxResultOp->mptHoldersData);
auto [success, duration] = auto [success, duration] =
::util::timed<std::chrono::duration<double>>([&]() { return backend_->finishWrites(lgrInfo.seq); }); ::util::timed<std::chrono::duration<double>>([&]() { return backend_->finishWrites(lgrInfo.seq); });

5
src/etlng/CMakeLists.txt Normal file
View File

@@ -0,0 +1,5 @@
add_library(clio_etlng INTERFACE)
# target_sources(clio_etlng PRIVATE )
target_link_libraries(clio_etlng INTERFACE clio_data)

129
src/etlng/Models.hpp Normal file
View File

@@ -0,0 +1,129 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/Concepts.hpp"
#include <boost/json/object.hpp>
#include <fmt/core.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/proto/org/xrpl/rpc/v1/get_ledger.pb.h>
#include <xrpl/proto/org/xrpl/rpc/v1/ledger.pb.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxMeta.h>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace etlng::model {
/**
* @brief A specification for the Registry.
*
* This specification simply defines the transaction types that are to be filtered out from the incoming transactions by
* the Registry for its `onTransaction` and `onInitialTransaction` hooks.
* It's a compilation error to list the same transaction type more than once.
*/
template <ripple::TxType... Types>
requires(util::hasNoDuplicates(Types...))
struct Spec {
static constexpr bool SpecTag = true;
/**
* @brief Checks if the transaction type was requested.
*
* @param type The transaction type
* @return true if the transaction was requested; false otherwise
*/
[[nodiscard]] constexpr static bool
wants(ripple::TxType type) noexcept
{
return ((Types == type) || ...);
}
};
/**
* @brief Represents a single transaction on the ledger.
*/
struct Transaction {
std::string raw; // raw binary blob
std::string metaRaw;
// unpacked blob and meta
ripple::STTx sttx;
ripple::TxMeta meta;
// commonly used stuff
ripple::uint256 id;
std::string key; // key is the above id as a string of 32 characters
ripple::TxType type;
};
/**
* @brief Represents a single object on the ledger.
*/
struct Object {
/**
* @brief Modification type for the object.
*/
enum class ModType : int {
Unspecified = 0,
Created = 1,
Modified = 2,
Deleted = 3,
};
ripple::uint256 key;
std::string keyRaw;
ripple::Blob data;
std::string dataRaw;
std::string successor;
std::string predecessor;
ModType type;
};
/**
* @brief Represents a book successor.
*/
struct BookSuccessor {
std::string firstBook;
std::string bookBase;
};
/**
* @brief Represents an entire ledger diff worth of transactions and objects.
*/
struct LedgerData {
std::vector<Transaction> transactions;
std::vector<Object> objects;
std::optional<std::vector<BookSuccessor>> successors;
ripple::LedgerHeader header;
std::string rawHeader;
uint32_t seq;
};
} // namespace etlng::model

View File

@@ -0,0 +1,108 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "etlng/Models.hpp"
#include <cstdint>
#include <string>
#include <vector>
namespace etlng {
/**
* @brief The interface for a registry that can dispatch transactions and objects to extensions.
*
* This class defines the interface for dispatching data through to extensions.
*
* @note
* The registry itself consists of Extensions.
* Each extension must define at least one valid hook:
* - for ongoing ETL dispatch:
* - void onLedgerData(etlng::model::LedgerData const&)
* - void onTransaction(uint32_t, etlng::model::Transaction const&)
* - void onObject(uint32_t, etlng::model::Object const&)
* - for initial ledger load
* - void onInitialData(etlng::model::LedgerData const&)
* - void onInitialTransaction(uint32_t, etlng::model::Transaction const&)
* - for initial objects (called for each downloaded batch)
* - void onInitialObjects(uint32_t, std::vector<etlng::model::Object> const&, std::string)
* - void onInitialObject(uint32_t, etlng::model::Object const&)
*
* When the registry dispatches (initial)data or objects, each of the above hooks will be called in order on each
* registered extension.
* This means that the order of execution is from left to right (hooks) and top to bottom (registered extensions).
*
* If either `onTransaction` or `onInitialTransaction` are defined, the extension will have to additionally define a
* Specification. The specification lists transaction types to filter from the incoming data such that `onTransaction`
* and `onInitialTransaction` are only called for the transactions that are of interest for the given extension.
*
* The specification is setup like so:
* @code{.cpp}
* struct Ext {
* using spec = etlng::model::Spec<
* ripple::TxType::ttNFTOKEN_BURN,
* ripple::TxType::ttNFTOKEN_ACCEPT_OFFER,
* ripple::TxType::ttNFTOKEN_CREATE_OFFER,
* ripple::TxType::ttNFTOKEN_CANCEL_OFFER,
* ripple::TxType::ttNFTOKEN_MINT>;
*
* static void
* onInitialTransaction(uint32_t, etlng::model::Transaction const&);
* };
* @endcode
*/
struct RegistryInterface {
virtual ~RegistryInterface() = default;
/**
* @brief Dispatch initial objects.
*
* These objects are received during initial ledger load.
*
* @param seq The sequence
* @param data The objects to dispatch
* @param lastKey The predcessor of the first object in data if known; an empty string otherwise
*/
virtual void
dispatchInitialObjects(uint32_t seq, std::vector<model::Object> const& data, std::string lastKey) = 0;
/**
* @brief Dispatch initial ledger data.
*
* The transactions, header and edge keys are received during initial ledger load.
*
* @param data The data to dispatch
*/
virtual void
dispatchInitialData(model::LedgerData const& data) = 0;
/**
* @brief Dispatch an entire ledger diff.
*
* This is used to dispatch incoming diffs through the extensions.
*
* @param data The data to dispatch
*/
virtual void
dispatch(model::LedgerData const& data) = 0;
};
} // namespace etlng

219
src/etlng/impl/Registry.hpp Normal file
View File

@@ -0,0 +1,219 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "etlng/Models.hpp"
#include "etlng/RegistryInterface.hpp"
#include <xrpl/protocol/TxFormats.h>
#include <concepts>
#include <cstdint>
#include <string>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
namespace etlng::impl {
template <typename T>
concept HasLedgerDataHook = requires(T p) {
{ p.onLedgerData(std::declval<etlng::model::LedgerData>()) } -> std::same_as<void>;
};
template <typename T>
concept HasInitialDataHook = requires(T p) {
{ p.onInitialData(std::declval<etlng::model::LedgerData>()) } -> std::same_as<void>;
};
template <typename T>
concept HasTransactionHook = requires(T p) {
{ p.onTransaction(uint32_t{}, std::declval<etlng::model::Transaction>()) } -> std::same_as<void>;
};
template <typename T>
concept HasObjectHook = requires(T p) {
{ p.onObject(uint32_t{}, std::declval<etlng::model::Object>()) } -> std::same_as<void>;
};
template <typename T>
concept HasInitialTransactionHook = requires(T p) {
{ p.onInitialTransaction(uint32_t{}, std::declval<etlng::model::Transaction>()) } -> std::same_as<void>;
};
template <typename T>
concept HasInitialObjectsHook = requires(T p) {
{
p.onInitialObjects(uint32_t{}, std::declval<std::vector<etlng::model::Object>>(), std::string{})
} -> std::same_as<void>;
};
template <typename T>
concept HasInitialObjectHook = requires(T p) {
{ p.onInitialObject(uint32_t{}, std::declval<etlng::model::Object>()) } -> std::same_as<void>;
};
template <typename T>
concept ContainsSpec = std::decay_t<T>::spec::SpecTag;
template <typename T>
concept ContainsValidHook = HasLedgerDataHook<T> or HasInitialDataHook<T> or
(HasTransactionHook<T> and ContainsSpec<T>) or (HasInitialTransactionHook<T> and ContainsSpec<T>) or
HasObjectHook<T> or HasInitialObjectsHook<T> or HasInitialObjectHook<T>;
template <typename T>
concept NoTwoOfKind = not(HasLedgerDataHook<T> and HasTransactionHook<T>) and
not(HasInitialDataHook<T> and HasInitialTransactionHook<T>) and not(HasInitialDataHook<T> and HasObjectHook<T>) and
not(HasInitialObjectsHook<T> and HasInitialObjectHook<T>);
template <typename T>
concept SomeExtension = NoTwoOfKind<T> and ContainsValidHook<T>;
template <SomeExtension... Ps>
class Registry : public RegistryInterface {
std::tuple<Ps...> store_;
static_assert(
(((not HasTransactionHook<std::decay_t<Ps>>) or ContainsSpec<std::decay_t<Ps>>) and ...),
"Spec must be specified when 'onTransaction' function exists."
);
static_assert(
(((not HasInitialTransactionHook<std::decay_t<Ps>>) or ContainsSpec<std::decay_t<Ps>>) and ...),
"Spec must be specified when 'onInitialTransaction' function exists."
);
public:
explicit constexpr Registry(SomeExtension auto&&... exts)
requires(std::is_same_v<std::decay_t<decltype(exts)>, std::decay_t<Ps>> and ...)
: store_(std::forward<Ps>(exts)...)
{
}
~Registry() override = default;
Registry(Registry const&) = delete;
Registry(Registry&&) = default;
Registry&
operator=(Registry const&) = delete;
Registry&
operator=(Registry&&) = default;
constexpr void
dispatch(model::LedgerData const& data) override
{
// send entire batch of data at once
{
auto const expand = [&](auto& p) {
if constexpr (requires { p.onLedgerData(data); }) {
p.onLedgerData(data);
}
};
std::apply([&expand](auto&&... xs) { (expand(xs), ...); }, store_);
}
// send filtered transactions
{
auto const expand = [&]<typename P>(P& p, model::Transaction const& t) {
if constexpr (requires { p.onTransaction(data.seq, t); }) {
if (std::decay_t<P>::spec::wants(t.type))
p.onTransaction(data.seq, t);
}
};
for (auto const& t : data.transactions) {
std::apply([&expand, &t](auto&&... xs) { (expand(xs, t), ...); }, store_);
}
}
// send per object path
{
auto const expand = [&]<typename P>(P&& p, model::Object const& o) {
if constexpr (requires { p.onObject(data.seq, o); }) {
p.onObject(data.seq, o);
}
};
for (auto const& obj : data.objects) {
std::apply([&expand, &obj](auto&&... xs) { (expand(xs, obj), ...); }, store_);
}
}
}
constexpr void
dispatchInitialObjects(uint32_t seq, std::vector<model::Object> const& data, std::string lastKey) override
{
// send entire vector path
{
auto const expand = [&](auto&& p) {
if constexpr (requires { p.onInitialObjects(seq, data, lastKey); }) {
p.onInitialObjects(seq, data, lastKey);
}
};
std::apply([&expand](auto&&... xs) { (expand(xs), ...); }, store_);
}
// send per object path
{
auto const expand = [&]<typename P>(P&& p, model::Object const& o) {
if constexpr (requires { p.onInitialObject(seq, o); }) {
p.onInitialObject(seq, o);
}
};
for (auto const& obj : data) {
std::apply([&expand, &obj](auto&&... xs) { (expand(xs, obj), ...); }, store_);
}
}
}
constexpr void
dispatchInitialData(model::LedgerData const& data) override
{
// send entire batch path
{
auto const expand = [&](auto&& p) {
if constexpr (requires { p.onInitialData(data); }) {
p.onInitialData(data);
}
};
std::apply([&expand](auto&&... xs) { (expand(xs), ...); }, store_);
}
// send per tx path
{
auto const expand = [&]<typename P>(P&& p, model::Transaction const& tx) {
if constexpr (requires { p.onInitialTransaction(data.seq, tx); }) {
if (std::decay_t<P>::spec::wants(tx.type))
p.onInitialTransaction(data.seq, tx);
}
};
for (auto const& tx : data.transactions) {
std::apply([&expand, &tx](auto&&... xs) { (expand(xs, tx), ...); }, store_);
}
}
}
};
} // namespace etlng::impl

View File

@@ -30,6 +30,7 @@
#include "feed/impl/TransactionFeed.hpp" #include "feed/impl/TransactionFeed.hpp"
#include "util/async/AnyExecutionContext.hpp" #include "util/async/AnyExecutionContext.hpp"
#include "util/async/context/BasicExecutionContext.hpp" #include "util/async/context/BasicExecutionContext.hpp"
#include "util/config/Config.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
#include <boost/asio/executor_work_guard.hpp> #include <boost/asio/executor_work_guard.hpp>
@@ -44,6 +45,7 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
/** /**
@@ -67,16 +69,36 @@ class SubscriptionManager : public SubscriptionManagerInterface {
impl::ProposedTransactionFeed proposedTransactionFeed_; impl::ProposedTransactionFeed proposedTransactionFeed_;
public: public:
/**
* @brief Factory function to create a new SubscriptionManager with a PoolExecutionContext.
*
* @param config The configuration to use
* @param backend The backend to use
* @return A shared pointer to a new instance of SubscriptionManager
*/
static std::shared_ptr<SubscriptionManager>
make_SubscriptionManager(util::Config const& config, std::shared_ptr<data::BackendInterface const> const& backend)
{
auto const workersNum = config.valueOr<std::uint64_t>("subscription_workers", 1);
util::Logger const logger{"Subscriptions"};
LOG(logger.info()) << "Starting subscription manager with " << workersNum << " workers";
return std::make_shared<feed::SubscriptionManager>(util::async::PoolExecutionContext(workersNum), backend);
}
/** /**
* @brief Construct a new Subscription Manager object * @brief Construct a new Subscription Manager object
* *
* @param executor The executor to use to publish the feeds * @param executor The executor to use to publish the feeds
* @param backend The backend to use * @param backend The backend to use
*/ */
template <class ExecutorCtx> SubscriptionManager(
SubscriptionManager(ExecutorCtx& executor, std::shared_ptr<data::BackendInterface const> const& backend) util::async::AnyExecutionContext&& executor,
std::shared_ptr<data::BackendInterface const> const& backend
)
: backend_(backend) : backend_(backend)
, ctx_(executor) , ctx_(std::move(executor))
, manifestFeed_(ctx_, "manifest") , manifestFeed_(ctx_, "manifest")
, validationsFeed_(ctx_, "validations") , validationsFeed_(ctx_, "validations")
, ledgerFeed_(ctx_) , ledgerFeed_(ctx_)
@@ -291,41 +313,4 @@ public:
report() const final; report() const final;
}; };
/**
* @brief The help class to run the subscription manager. The container of PoolExecutionContext which is used to publish
* the feeds.
*/
class SubscriptionManagerRunner {
std::uint64_t workersNum_;
using ActualExecutionCtx = util::async::PoolExecutionContext;
ActualExecutionCtx ctx_;
std::shared_ptr<SubscriptionManager> subscriptionManager_;
util::Logger logger_{"Subscriptions"};
public:
/**
* @brief Construct a new Subscription Manager Runner object
*
* @param config The configuration
* @param backend The backend to use
*/
SubscriptionManagerRunner(util::Config const& config, std::shared_ptr<data::BackendInterface> const& backend)
: workersNum_(config.valueOr<std::uint64_t>("subscription_workers", 1))
, ctx_(workersNum_)
, subscriptionManager_(std::make_shared<SubscriptionManager>(ctx_, backend))
{
LOG(logger_.info()) << "Starting subscription manager with " << workersNum_ << " workers";
}
/**
* @brief Get the subscription manager
*
* @return The subscription manager
*/
std::shared_ptr<SubscriptionManager>
getManager()
{
return subscriptionManager_;
}
};
} // namespace feed } // namespace feed

View File

@@ -203,6 +203,7 @@ TransactionFeed::pub(
pubObj[JS(meta)] = rpc::toJson(*meta); pubObj[JS(meta)] = rpc::toJson(*meta);
rpc::insertDeliveredAmount(pubObj[JS(meta)].as_object(), tx, meta, txMeta.date); rpc::insertDeliveredAmount(pubObj[JS(meta)].as_object(), tx, meta, txMeta.date);
rpc::insertDeliverMaxAlias(pubObj[txKey].as_object(), version); rpc::insertDeliverMaxAlias(pubObj[txKey].as_object(), version);
rpc::insertMPTIssuanceID(pubObj[JS(meta)].as_object(), tx, meta);
pubObj[JS(type)] = "transaction"; pubObj[JS(type)] = "transaction";
pubObj[JS(validated)] = true; pubObj[JS(validated)] = true;

View File

@@ -6,6 +6,7 @@ target_sources(
Factories.cpp Factories.cpp
AMMHelpers.cpp AMMHelpers.cpp
RPCHelpers.cpp RPCHelpers.cpp
CredentialHelpers.cpp
Counters.cpp Counters.cpp
WorkQueue.cpp WorkQueue.cpp
common/Specs.cpp common/Specs.cpp
@@ -33,6 +34,7 @@ target_sources(
handlers/LedgerEntry.cpp handlers/LedgerEntry.cpp
handlers/LedgerIndex.cpp handlers/LedgerIndex.cpp
handlers/LedgerRange.cpp handlers/LedgerRange.cpp
handlers/MPTHolders.cpp
handlers/NFTsByIssuer.cpp handlers/NFTsByIssuer.cpp
handlers/NFTBuyOffers.cpp handlers/NFTBuyOffers.cpp
handlers/NFTHistory.cpp handlers/NFTHistory.cpp

View File

@@ -0,0 +1,161 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "data/BackendInterface.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/common/Types.hpp"
#include "util/Assert.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <boost/json/value_to.hpp>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <expected>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
namespace rpc::credentials {
bool
checkExpired(ripple::SLE const& sleCred, ripple::LedgerHeader const& ledger)
{
if (sleCred.isFieldPresent(ripple::sfExpiration)) {
std::uint32_t const exp = sleCred.getFieldU32(ripple::sfExpiration);
std::uint32_t const now = ledger.parentCloseTime.time_since_epoch().count();
return now > exp;
}
return false;
}
std::set<std::pair<ripple::AccountID, ripple::Slice>>
createAuthCredentials(ripple::STArray const& in)
{
std::set<std::pair<ripple::AccountID, ripple::Slice>> out;
for (auto const& cred : in)
out.insert({cred[ripple::sfIssuer], cred[ripple::sfCredentialType]});
return out;
}
ripple::STArray
parseAuthorizeCredentials(boost::json::array const& jv)
{
ripple::STArray arr;
for (auto const& jo : jv) {
ASSERT(
jo.at(JS(issuer)).is_string(),
"issuer must be string, should already be checked in AuthorizeCredentialValidator"
);
auto const issuer =
ripple::parseBase58<ripple::AccountID>(static_cast<std::string>(jo.at(JS(issuer)).as_string()));
ASSERT(
issuer.has_value(), "issuer must be present, should already be checked in AuthorizeCredentialValidator."
);
ASSERT(
jo.at(JS(credential_type)).is_string(),
"credential_type must be string, should already be checked in AuthorizeCredentialValidator"
);
auto const credentialType = ripple::strUnHex(static_cast<std::string>(jo.at(JS(credential_type)).as_string()));
ASSERT(
credentialType.has_value(),
"credential_type must be present, should already be checked in AuthorizeCredentialValidator."
);
auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential);
credential.setAccountID(ripple::sfIssuer, *issuer);
credential.setFieldVL(ripple::sfCredentialType, *credentialType);
arr.push_back(std::move(credential));
}
return arr;
}
std::expected<ripple::STArray, Status>
fetchCredentialArray(
std::optional<boost::json::array> const& credID,
ripple::AccountID const& srcAcc,
BackendInterface const& backend,
ripple::LedgerHeader const& info,
boost::asio::yield_context const& yield
)
{
ripple::STArray authCreds;
std::unordered_set<std::string_view> elems;
for (auto const& elem : credID.value()) {
ASSERT(elem.is_string(), "should already be checked in validators.hpp that elem is a string.");
if (elems.contains(elem.as_string()))
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "duplicates in credentials."}};
elems.insert(elem.as_string());
ripple::uint256 credHash;
ASSERT(
credHash.parseHex(boost::json::value_to<std::string>(elem)),
"should already be checked in validators.hpp that elem is a uint256 hex"
);
auto const credKeylet = ripple::keylet::credential(credHash).key;
auto const credLedgerObject = backend.fetchLedgerObject(credKeylet, info.seq, yield);
if (!credLedgerObject)
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials don't exist."}};
auto credIt = ripple::SerialIter{credLedgerObject->data(), credLedgerObject->size()};
auto const sleCred = ripple::SLE{credIt, credKeylet};
if ((sleCred.getType() != ripple::ltCREDENTIAL) ||
((sleCred.getFieldU32(ripple::sfFlags) & ripple::lsfAccepted) == 0u))
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials aren't accepted"}};
if (credentials::checkExpired(sleCred, info))
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials are expired"}};
if (sleCred.getAccountID(ripple::sfSubject) != srcAcc)
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials don't belong to the root account"}};
auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential);
credential.setAccountID(ripple::sfIssuer, sleCred.getAccountID(ripple::sfIssuer));
credential.setFieldVL(ripple::sfCredentialType, sleCred.getFieldVL(ripple::sfCredentialType));
authCreds.push_back(std::move(credential));
}
return authCreds;
}
} // namespace rpc::credentials

View File

@@ -0,0 +1,89 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "data/BackendInterface.hpp"
#include "rpc/Errors.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <expected>
#include <optional>
#include <set>
#include <utility>
namespace rpc::credentials {
/**
* @brief Check if credential is expired
*
* @param sleCred The credential to check
* @param ledger The ledger to check the closed time of
* @return true if credential not expired, false otherwise
*/
bool
checkExpired(ripple::SLE const& sleCred, ripple::LedgerHeader const& ledger);
/**
* @brief Creates authentication credential field (which is a set of pairs of AccountID and Credential ID)
*
* @param in The array of Credential objects to check
* @return Auth Credential array
*/
std::set<std::pair<ripple::AccountID, ripple::Slice>>
createAuthCredentials(ripple::STArray const& in);
/**
* @brief Parses each credential object and makes sure the credential type and values are correct
*
* @param jv The boost json array of credentials to parse
* @return Array of credentials after parsing
*/
ripple::STArray
parseAuthorizeCredentials(boost::json::array const& jv);
/**
* @brief Get Array of Credential objects
*
* @param credID Array of CredentialID's to parse
* @param srcAcc The Source Account
* @param backend backend interface
* @param info The ledger header
* @param yield The coroutine context
* @return Array of credential objects, error if failed otherwise
*/
std::expected<ripple::STArray, Status>
fetchCredentialArray(
std::optional<boost::json::array> const& credID,
ripple::AccountID const& srcAcc,
BackendInterface const& backend,
ripple::LedgerHeader const& info,
boost::asio::yield_context const& yield
);
} // namespace rpc::credentials

View File

@@ -83,6 +83,9 @@ getErrorInfo(ClioError code)
{ClioError::rpcUNKNOWN_OPTION, "unknownOption", "Unknown option."}, {ClioError::rpcUNKNOWN_OPTION, "unknownOption", "Unknown option."},
{ClioError::rpcFIELD_NOT_FOUND_TRANSACTION, "fieldNotFoundTransaction", "Missing field."}, {ClioError::rpcFIELD_NOT_FOUND_TRANSACTION, "fieldNotFoundTransaction", "Missing field."},
{ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID, "malformedDocumentID", "Malformed oracle_document_id."}, {ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID, "malformedDocumentID", "Malformed oracle_document_id."},
{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
"malformedAuthorizedCredentials",
"Malformed authorized credentials."},
// special system errors // special system errors
{ClioError::rpcINVALID_API_VERSION, JS(invalid_API_version), "Invalid API version."}, {ClioError::rpcINVALID_API_VERSION, JS(invalid_API_version), "Invalid API version."},
{ClioError::rpcCOMMAND_IS_MISSING, JS(missingCommand), "Method is not specified or is not a string."}, {ClioError::rpcCOMMAND_IS_MISSING, JS(missingCommand), "Method is not specified or is not a string."},

View File

@@ -43,6 +43,7 @@ enum class ClioError {
rpcUNKNOWN_OPTION = 5005, rpcUNKNOWN_OPTION = 5005,
rpcFIELD_NOT_FOUND_TRANSACTION = 5006, rpcFIELD_NOT_FOUND_TRANSACTION = 5006,
rpcMALFORMED_ORACLE_DOCUMENT_ID = 5007, rpcMALFORMED_ORACLE_DOCUMENT_ID = 5007,
rpcMALFORMED_AUTHORIZED_CREDENTIALS = 5008,
// special system errors start with 6000 // special system errors start with 6000
rpcINVALID_API_VERSION = 6000, rpcINVALID_API_VERSION = 6000,

View File

@@ -20,19 +20,22 @@
#pragma once #pragma once
#include "data/BackendInterface.hpp" #include "data/BackendInterface.hpp"
#include "rpc/Counters.hpp"
#include "rpc/Errors.hpp" #include "rpc/Errors.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/WorkQueue.hpp" #include "rpc/WorkQueue.hpp"
#include "rpc/common/HandlerProvider.hpp" #include "rpc/common/HandlerProvider.hpp"
#include "rpc/common/Types.hpp" #include "rpc/common/Types.hpp"
#include "rpc/common/impl/ForwardingProxy.hpp" #include "rpc/common/impl/ForwardingProxy.hpp"
#include "util/ResponseExpirationCache.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
#include "web/Context.hpp" #include "web/Context.hpp"
#include "web/dosguard/DOSGuardInterface.hpp" #include "web/dosguard/DOSGuardInterface.hpp"
#include <boost/asio/spawn.hpp> #include <boost/asio/spawn.hpp>
#include <boost/iterator/transform_iterator.hpp>
#include <boost/json.hpp> #include <boost/json.hpp>
#include <fmt/core.h> #include <fmt/core.h>
#include <fmt/format.h>
#include <xrpl/protocol/ErrorCodes.h> #include <xrpl/protocol/ErrorCodes.h>
#include <chrono> #include <chrono>
@@ -41,14 +44,9 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <unordered_set>
#include <utility> #include <utility>
// forward declarations
namespace etl {
class LoadBalancer;
class ETLService;
} // namespace etl
/** /**
* @brief This namespace contains all the RPC logic and handlers. * @brief This namespace contains all the RPC logic and handlers.
*/ */
@@ -57,6 +55,7 @@ namespace rpc {
/** /**
* @brief The RPC engine that ties all RPC-related functionality together. * @brief The RPC engine that ties all RPC-related functionality together.
*/ */
template <typename LoadBalancerType, typename CountersType>
class RPCEngine { class RPCEngine {
util::Logger perfLog_{"Performance"}; util::Logger perfLog_{"Performance"};
util::Logger log_{"RPC"}; util::Logger log_{"RPC"};
@@ -64,16 +63,19 @@ class RPCEngine {
std::shared_ptr<BackendInterface> backend_; std::shared_ptr<BackendInterface> backend_;
std::reference_wrapper<web::dosguard::DOSGuardInterface const> dosGuard_; std::reference_wrapper<web::dosguard::DOSGuardInterface const> dosGuard_;
std::reference_wrapper<WorkQueue> workQueue_; std::reference_wrapper<WorkQueue> workQueue_;
std::reference_wrapper<Counters> counters_; std::reference_wrapper<CountersType> counters_;
std::shared_ptr<HandlerProvider const> handlerProvider_; std::shared_ptr<HandlerProvider const> handlerProvider_;
impl::ForwardingProxy<etl::LoadBalancer, Counters, HandlerProvider> forwardingProxy_; impl::ForwardingProxy<LoadBalancerType, CountersType, HandlerProvider> forwardingProxy_;
std::optional<util::ResponseExpirationCache> responseCache_;
public: public:
/** /**
* @brief Construct a new RPCEngine object * @brief Construct a new RPCEngine object
* *
* @param config The config to use
* @param backend The backend to use * @param backend The backend to use
* @param balancer The load balancer to use * @param balancer The load balancer to use
* @param dosGuard The DOS guard to use * @param dosGuard The DOS guard to use
@@ -82,11 +84,12 @@ public:
* @param handlerProvider The handler provider to use * @param handlerProvider The handler provider to use
*/ */
RPCEngine( RPCEngine(
util::Config const& config,
std::shared_ptr<BackendInterface> const& backend, std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<etl::LoadBalancer> const& balancer, std::shared_ptr<LoadBalancerType> const& balancer,
web::dosguard::DOSGuardInterface const& dosGuard, web::dosguard::DOSGuardInterface const& dosGuard,
WorkQueue& workQueue, WorkQueue& workQueue,
Counters& counters, CountersType& counters,
std::shared_ptr<HandlerProvider const> const& handlerProvider std::shared_ptr<HandlerProvider const> const& handlerProvider
) )
: backend_{backend} : backend_{backend}
@@ -96,11 +99,22 @@ public:
, handlerProvider_{handlerProvider} , handlerProvider_{handlerProvider}
, forwardingProxy_{balancer, counters, handlerProvider} , forwardingProxy_{balancer, counters, handlerProvider}
{ {
// Let main thread catch the exception if config type is wrong
auto const cacheTimeout = config.valueOr<float>("rpc.cache_timeout", 0.f);
if (cacheTimeout > 0.f) {
LOG(log_.info()) << fmt::format("Init RPC Cache, timeout: {} seconds", cacheTimeout);
responseCache_.emplace(
util::Config::toMilliseconds(cacheTimeout), std::unordered_set<std::string>{"server_info"}
);
}
} }
/** /**
* @brief Factory function to create a new instance of the RPC engine. * @brief Factory function to create a new instance of the RPC engine.
* *
* @param config The config to use
* @param backend The backend to use * @param backend The backend to use
* @param balancer The load balancer to use * @param balancer The load balancer to use
* @param dosGuard The DOS guard to use * @param dosGuard The DOS guard to use
@@ -111,15 +125,16 @@ public:
*/ */
static std::shared_ptr<RPCEngine> static std::shared_ptr<RPCEngine>
make_RPCEngine( make_RPCEngine(
util::Config const& config,
std::shared_ptr<BackendInterface> const& backend, std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<etl::LoadBalancer> const& balancer, std::shared_ptr<LoadBalancerType> const& balancer,
web::dosguard::DOSGuardInterface const& dosGuard, web::dosguard::DOSGuardInterface const& dosGuard,
WorkQueue& workQueue, WorkQueue& workQueue,
Counters& counters, CountersType& counters,
std::shared_ptr<HandlerProvider const> const& handlerProvider std::shared_ptr<HandlerProvider const> const& handlerProvider
) )
{ {
return std::make_shared<RPCEngine>(backend, balancer, dosGuard, workQueue, counters, handlerProvider); return std::make_shared<RPCEngine>(config, backend, balancer, dosGuard, workQueue, counters, handlerProvider);
} }
/** /**
@@ -131,8 +146,18 @@ public:
Result Result
buildResponse(web::Context const& ctx) buildResponse(web::Context const& ctx)
{ {
if (forwardingProxy_.shouldForward(ctx)) if (forwardingProxy_.shouldForward(ctx)) {
// Disallow forwarding of the admin api, only user api is allowed for security reasons.
if (isAdminCmd(ctx.method, ctx.params))
return Result{Status{RippledError::rpcNO_PERMISSION}};
return forwardingProxy_.forward(ctx); return forwardingProxy_.forward(ctx);
}
if (not ctx.isAdmin and responseCache_) {
if (auto res = responseCache_->get(ctx.method); res.has_value())
return Result{std::move(res).value()};
}
if (backend_->isTooBusy()) { if (backend_->isTooBusy()) {
LOG(log_.error()) << "Database is too busy. Rejecting request"; LOG(log_.error()) << "Database is too busy. Rejecting request";
@@ -154,8 +179,11 @@ public:
LOG(perfLog_.debug()) << ctx.tag() << " finish executing rpc `" << ctx.method << '`'; LOG(perfLog_.debug()) << ctx.tag() << " finish executing rpc `" << ctx.method << '`';
if (not v) if (not v) {
notifyErrored(ctx.method); notifyErrored(ctx.method);
} else if (not ctx.isAdmin and responseCache_) {
responseCache_->put(ctx.method, v.result->as_object());
}
return Result{std::move(v)}; return Result{std::move(v)};
} catch (data::DatabaseTimeout const& t) { } catch (data::DatabaseTimeout const& t) {

View File

@@ -36,6 +36,7 @@
#include <boost/json/array.hpp> #include <boost/json/array.hpp>
#include <boost/json/object.hpp> #include <boost/json/object.hpp>
#include <boost/json/parse.hpp> #include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/string.hpp> #include <boost/json/string.hpp>
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <boost/json/value_to.hpp> #include <boost/json/value_to.hpp>
@@ -49,6 +50,7 @@
#include <xrpl/basics/chrono.h> #include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h> #include <xrpl/basics/strHex.h>
#include <xrpl/beast/utility/Zero.h> #include <xrpl/beast/utility/Zero.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h> #include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h> #include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h> #include <xrpl/protocol/Book.h>
@@ -79,6 +81,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cassert>
#include <chrono> #include <chrono>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@@ -257,6 +260,7 @@ toExpandedJson(
auto metaJson = toJson(*meta); auto metaJson = toJson(*meta);
insertDeliveredAmount(metaJson, txn, meta, blobs.date); insertDeliveredAmount(metaJson, txn, meta, blobs.date);
insertDeliverMaxAlias(txnJson, apiVersion); insertDeliverMaxAlias(txnJson, apiVersion);
insertMPTIssuanceID(metaJson, txn, meta);
if (nftEnabled == NFTokenjson::ENABLE) { if (nftEnabled == NFTokenjson::ENABLE) {
Json::Value nftJson; Json::Value nftJson;
@@ -312,6 +316,67 @@ insertDeliveredAmount(
return false; return false;
} }
/**
* @brief Get the delivered amount
*
* @param meta The metadata
* @return The mpt_issuance_id or std::nullopt if not available
*/
static std::optional<ripple::uint192>
getMPTIssuanceID(std::shared_ptr<ripple::TxMeta const> const& meta)
{
ripple::TxMeta const& transactionMeta = *meta;
for (ripple::STObject const& node : transactionMeta.getNodes()) {
if (node.getFieldU16(ripple::sfLedgerEntryType) != ripple::ltMPTOKEN_ISSUANCE ||
node.getFName() != ripple::sfCreatedNode)
continue;
auto const& mptNode = node.peekAtField(ripple::sfNewFields).downcast<ripple::STObject>();
return ripple::makeMptID(mptNode[ripple::sfSequence], mptNode[ripple::sfIssuer]);
}
return {};
}
/**
* @brief Check if transaction has a new MPToken created
*
* @param txn The transaction
* @param meta The metadata
* @return true if the transaction can have a mpt_issuance_id
*/
static bool
canHaveMPTIssuanceID(std::shared_ptr<ripple::STTx const> const& txn, std::shared_ptr<ripple::TxMeta const> const& meta)
{
if (txn->getTxnType() != ripple::ttMPTOKEN_ISSUANCE_CREATE)
return false;
if (meta->getResultTER() != ripple::tesSUCCESS)
return false;
return true;
}
bool
insertMPTIssuanceID(
boost::json::object& metaJson,
std::shared_ptr<ripple::STTx const> const& txn,
std::shared_ptr<ripple::TxMeta const> const& meta
)
{
if (!canHaveMPTIssuanceID(txn, meta))
return false;
if (auto const id = getMPTIssuanceID(meta)) {
metaJson[JS(mpt_issuance_id)] = ripple::to_string(*id);
return true;
}
assert(false);
return false;
}
void void
insertDeliverMaxAlias(boost::json::object& txJson, std::uint32_t const apiVersion) insertDeliverMaxAlias(boost::json::object& txJson, std::uint32_t const apiVersion)
{ {
@@ -428,8 +493,9 @@ ledgerHeaderFromRequest(std::shared_ptr<data::BackendInterface const> const& bac
} else { } else {
ledgerSequence = parseStringAsUInt(stringIndex); ledgerSequence = parseStringAsUInt(stringIndex);
} }
} else if (indexValue.is_int64()) } else if (indexValue.is_int64()) {
ledgerSequence = indexValue.as_int64(); ledgerSequence = indexValue.as_int64();
}
} else { } else {
ledgerSequence = ctx.range.maxSequence; ledgerSequence = ctx.range.maxSequence;
} }
@@ -947,7 +1013,8 @@ accountHolds(
auto const blob = backend.fetchLedgerObject(key, sequence, yield); auto const blob = backend.fetchLedgerObject(key, sequence, yield);
if (!blob) { if (!blob) {
amount.clear({currency, issuer}); amount.setIssue(ripple::Issue(currency, issuer));
amount.clear();
return amount; return amount;
} }
@@ -955,7 +1022,8 @@ accountHolds(
ripple::SLE const sle{it, key}; ripple::SLE const sle{it, key};
if (zeroIfFrozen && isFrozen(backend, sequence, account, currency, issuer, yield)) { if (zeroIfFrozen && isFrozen(backend, sequence, account, currency, issuer, yield)) {
amount.clear(ripple::Issue(currency, issuer)); amount.setIssue(ripple::Issue(currency, issuer));
amount.clear();
} else { } else {
amount = sle.getFieldAmount(ripple::sfBalance); amount = sle.getFieldAmount(ripple::sfBalance);
if (account > issuer) { if (account > issuer) {
@@ -1273,6 +1341,31 @@ specifiesCurrentOrClosedLedger(boost::json::object const& request)
return false; return false;
} }
bool
isAdminCmd(std::string const& method, boost::json::object const& request)
{
if (method == JS(ledger)) {
auto const requestStr = boost::json::serialize(request);
Json::Value jv;
Json::Reader{}.parse(requestStr, jv);
// rippled considers string/non-zero int/non-empty array/ non-empty json as true.
// Use rippled's API asBool to get the same result.
// https://github.com/XRPLF/rippled/issues/5119
auto const isFieldSet = [&jv](auto const field) { return jv.isMember(field) and jv[field].asBool(); };
// According to doc
// https://xrpl.org/docs/references/http-websocket-apis/public-api-methods/ledger-methods/ledger,
// full/accounts/type are admin only, but type only works when full/accounts are set, so we don't need to check
// type.
if (isFieldSet(JS(full)) or isFieldSet(JS(accounts)))
return true;
}
if (method == JS(feature) and request.contains(JS(vetoed)))
return true;
return false;
}
std::variant<ripple::uint256, Status> std::variant<ripple::uint256, Status>
getNFTID(boost::json::object const& request) getNFTID(boost::json::object const& request)
{ {

View File

@@ -191,6 +191,21 @@ insertDeliveredAmount(
uint32_t date uint32_t date
); );
/**
* @brief Add "mpt_issuance_id" into MPTokenIssuanceCreate transaction json.
*
* @param metaJson The metadata json object to add "MPTokenIssuanceID"
* @param txn The transaction object
* @param meta The metadata object
* @return true if the "mpt_issuance_id" is added to the metadata json object
*/
bool
insertMPTIssuanceID(
boost::json::object& metaJson,
std::shared_ptr<ripple::STTx const> const& txn,
std::shared_ptr<ripple::TxMeta const> const& meta
);
/** /**
* @brief Convert STBase object to JSON * @brief Convert STBase object to JSON
* *
@@ -557,6 +572,16 @@ parseIssue(boost::json::object const& issue);
bool bool
specifiesCurrentOrClosedLedger(boost::json::object const& request); specifiesCurrentOrClosedLedger(boost::json::object const& request);
/**
* @brief Check whether a request requires administrative privileges on rippled side.
*
* @param method The method name to check
* @param request The request to check
* @return true if the request requires ADMIN role
*/
bool
isAdminCmd(std::string const& method, boost::json::object const& request);
/** /**
* @brief Get the NFTID from the request * @brief Get the NFTID from the request
* *

View File

@@ -34,16 +34,13 @@ static constexpr uint32_t API_VERSION_DEFAULT = 1u;
/** /**
* @brief Minimum API version supported by this build * @brief Minimum API version supported by this build
*
* Note: Clio does not natively support v1 and only supports v2 or newer.
* However, Clio will forward all v1 requests to rippled for backward compatibility.
*/ */
static constexpr uint32_t API_VERSION_MIN = 1u; static constexpr uint32_t API_VERSION_MIN = 1u;
/** /**
* @brief Maximum API version supported by this build * @brief Maximum API version supported by this build
*/ */
static constexpr uint32_t API_VERSION_MAX = 2u; static constexpr uint32_t API_VERSION_MAX = 3u;
/** /**
* @brief A baseclass for API version helper * @brief A baseclass for API version helper

View File

@@ -29,8 +29,10 @@
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <boost/json/value_to.hpp> #include <boost/json/value_to.hpp>
#include <fmt/core.h> #include <fmt/core.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h> #include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h> #include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/UintTypes.h> #include <xrpl/protocol/UintTypes.h>
#include <charconv> #include <charconv>
@@ -89,16 +91,19 @@ checkIsU32Numeric(std::string_view sv)
return ec == std::errc(); return ec == std::errc();
} }
CustomValidator CustomValidators::Uint160HexStringValidator =
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
return makeHexStringValidator<ripple::uint160>(value, key);
}};
CustomValidator CustomValidators::Uint192HexStringValidator =
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
return makeHexStringValidator<ripple::uint192>(value, key);
}};
CustomValidator CustomValidators::Uint256HexStringValidator = CustomValidator CustomValidators::Uint256HexStringValidator =
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_string()) return makeHexStringValidator<ripple::uint256>(value, key);
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
ripple::uint256 ledgerHash;
if (!ledgerHash.parseHex(boost::json::value_to<std::string>(value)))
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
return MaybeError{};
}}; }};
CustomValidator CustomValidators::LedgerIndexValidator = CustomValidator CustomValidators::LedgerIndexValidator =
@@ -200,15 +205,13 @@ CustomValidator CustomValidators::SubscribeStreamValidator =
"ledger", "transactions", "transactions_proposed", "book_changes", "manifests", "validations" "ledger", "transactions", "transactions_proposed", "book_changes", "manifests", "validations"
}; };
static std::unordered_set<std::string> const reportingNotSupportStreams = { static std::unordered_set<std::string> const notSupportStreams = {"peer_status", "consensus", "server"};
"peer_status", "consensus", "server"
};
for (auto const& v : value.as_array()) { for (auto const& v : value.as_array()) {
if (!v.is_string()) if (!v.is_string())
return Error{Status{RippledError::rpcINVALID_PARAMS, "streamNotString"}}; return Error{Status{RippledError::rpcINVALID_PARAMS, "streamNotString"}};
if (reportingNotSupportStreams.contains(boost::json::value_to<std::string>(v))) if (notSupportStreams.contains(boost::json::value_to<std::string>(v)))
return Error{Status{RippledError::rpcREPORTING_UNSUPPORTED}}; return Error{Status{RippledError::rpcNOT_SUPPORTED}};
if (not validStreams.contains(boost::json::value_to<std::string>(v))) if (not validStreams.contains(boost::json::value_to<std::string>(v)))
return Error{Status{RippledError::rpcSTREAM_MALFORMED}}; return Error{Status{RippledError::rpcSTREAM_MALFORMED}};
@@ -252,4 +255,79 @@ CustomValidator CustomValidators::CurrencyIssueValidator =
return MaybeError{}; return MaybeError{};
}}; }};
CustomValidator CustomValidators::CredentialTypeValidator =
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (not value.is_string())
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " NotString"}};
auto const& credTypeHex = ripple::strViewUnHex(value.as_string());
if (!credTypeHex.has_value())
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " NotHexString"}};
if (credTypeHex->empty())
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " is empty"}};
if (credTypeHex->size() > ripple::maxCredentialTypeLength) {
return Error{
Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " greater than max length"}
};
}
return MaybeError{};
}};
CustomValidator CustomValidators::AuthorizeCredentialValidator =
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (not value.is_array())
return Error{Status{ClioError::rpcMALFORMED_REQUEST, std::string(key) + " not array"}};
auto const& authCred = value.as_array();
if (authCred.empty()) {
return Error{Status{
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
fmt::format("Requires at least one element in authorized_credentials array.")
}};
}
if (authCred.size() > ripple::maxCredentialsArraySize) {
return Error{Status{
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
fmt::format(
"Max {} number of credentials in authorized_credentials array", ripple::maxCredentialsArraySize
)
}};
}
for (auto const& credObj : value.as_array()) {
if (!credObj.is_object()) {
return Error{Status{
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
"authorized_credentials elements in array are not objects."
}};
}
auto const& obj = credObj.as_object();
if (!obj.contains("issuer")) {
return Error{
Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "Field 'Issuer' is required but missing."}
};
}
// don't want to change issuer error message to be about credentials
if (!IssuerValidator.verify(credObj, "issuer"))
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "issuer NotString"}};
if (!obj.contains("credential_type")) {
return Error{Status{
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "Field 'CredentialType' is required but missing."
}};
}
if (auto const err = CredentialTypeValidator.verify(credObj, "credential_type"); !err)
return err;
}
return MaybeError{};
}};
} // namespace rpc::validation } // namespace rpc::validation

View File

@@ -27,15 +27,13 @@
#include <boost/json/object.hpp> #include <boost/json/object.hpp>
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <fmt/core.h> #include <fmt/core.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/ErrorCodes.h> #include <xrpl/protocol/ErrorCodes.h>
#include <concepts> #include <concepts>
#include <cstdint>
#include <ctime> #include <ctime>
#include <functional> #include <functional>
#include <initializer_list> #include <initializer_list>
#include <iomanip>
#include <sstream>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <utility> #include <utility>
@@ -153,7 +151,7 @@ struct Type final {
verify(boost::json::value const& value, std::string_view key) const verify(boost::json::value const& value, std::string_view key) const
{ {
if (not value.is_object() or not value.as_object().contains(key.data())) if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. field does not exist, let 'required' fail instead return {}; // ignore. If field is supposed to exist, let 'required' fail instead
auto const& res = value.as_object().at(key.data()); auto const& res = value.as_object().at(key.data());
auto const convertible = (checkType<Types>(res) || ...); auto const convertible = (checkType<Types>(res) || ...);
@@ -458,6 +456,21 @@ public:
[[nodiscard]] bool [[nodiscard]] bool
checkIsU32Numeric(std::string_view sv); checkIsU32Numeric(std::string_view sv);
template <class HexType>
requires(std::is_same_v<HexType, ripple::uint160> || std::is_same_v<HexType, ripple::uint192> || std::is_same_v<HexType, ripple::uint256>)
MaybeError
makeHexStringValidator(boost::json::value const& value, std::string_view key)
{
if (!value.is_string())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
HexType parsedInt;
if (!parsedInt.parseHex(value.as_string().c_str()))
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
return MaybeError{};
}
/** /**
* @brief A group of custom validation functions * @brief A group of custom validation functions
*/ */
@@ -492,6 +505,22 @@ struct CustomValidators final {
*/ */
static CustomValidator AccountMarkerValidator; static CustomValidator AccountMarkerValidator;
/**
* @brief Provides a commonly used validator for uint160(AccountID) hex string.
*
* It must be a string and also a decodable hex.
* AccountID uses this validator.
*/
static CustomValidator Uint160HexStringValidator;
/**
* @brief Provides a commonly used validator for uint192 hex string.
*
* It must be a string and also a decodable hex.
* MPTIssuanceID uses this validator.
*/
static CustomValidator Uint192HexStringValidator;
/** /**
* @brief Provides a commonly used validator for uint256 hex string. * @brief Provides a commonly used validator for uint256 hex string.
* *
@@ -528,6 +557,51 @@ struct CustomValidators final {
* Used by amm_info. * Used by amm_info.
*/ */
static CustomValidator CurrencyIssueValidator; static CustomValidator CurrencyIssueValidator;
/**
* @brief Provides a validator for validating authorized_credentials json array.
*
* Used by deposit_preauth.
*/
static CustomValidator AuthorizeCredentialValidator;
/**
* @brief Provides a validator for validating credential_type.
*
* Used by AuthorizeCredentialValidator in deposit_preauth.
*/
static CustomValidator CredentialTypeValidator;
};
/**
* @brief Validates that the elements of the array is of type Hex256 uint
*/
struct Hex256ItemType final {
/**
* @brief Validates given the prerequisite that the type of the json value is an array,
* verifies all values within the array is of uint256 hash
*
* @param value the value to verify
* @param key The key used to retrieve the tested value from the outer object
* @return `RippledError::rpcINVALID_PARAMS` if validation failed; otherwise no error is returned
*/
[[nodiscard]] static MaybeError
verify(boost::json::value const& value, std::string_view key)
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. If field is supposed to exist, let 'required' fail instead
auto const& res = value.as_object().at(key.data());
// loop through each item in the array and make sure it is uint256 hex string
for (auto const& elem : res.as_array()) {
ripple::uint256 num;
if (!elem.is_string() || !num.parseHex(elem.as_string())) {
return Error{Status{RippledError::rpcINVALID_PARAMS, "Item is not a valid uint256 type."}};
}
}
return {};
}
}; };
} // namespace rpc::validation } // namespace rpc::validation

View File

@@ -60,10 +60,6 @@ public:
if (ctx.method == "subscribe" || ctx.method == "unsubscribe") if (ctx.method == "subscribe" || ctx.method == "unsubscribe")
return false; return false;
// Disallow forwarding of the admin api, only user api is allowed for security reasons.
if (ctx.method == "feature" and request.contains("vetoed"))
return false;
if (handlerProvider_->isClioOnly(ctx.method)) if (handlerProvider_->isClioOnly(ctx.method))
return false; return false;
@@ -73,6 +69,9 @@ public:
if (specifiesCurrentOrClosedLedger(request)) if (specifiesCurrentOrClosedLedger(request))
return true; return true;
if (isForcedForward(ctx))
return true;
auto const checkAccountInfoForward = [&]() { auto const checkAccountInfoForward = [&]() {
return ctx.method == "account_info" and request.contains("queue") and request.at("queue").is_bool() and return ctx.method == "account_info" and request.contains("queue") and request.at("queue").is_bool() and
request.at("queue").as_bool(); request.at("queue").as_bool();
@@ -142,6 +141,14 @@ private:
{ {
return handlerProvider_->contains(method) || isProxied(method); return handlerProvider_->contains(method) || isProxied(method);
} }
bool
isForcedForward(web::Context const& ctx) const
{
static constexpr auto FORCE_FORWARD = "force_forward";
return ctx.isAdmin and ctx.params.contains(FORCE_FORWARD) and ctx.params.at(FORCE_FORWARD).is_bool() and
ctx.params.at(FORCE_FORWARD).as_bool();
}
}; };
} // namespace rpc::impl } // namespace rpc::impl

View File

@@ -45,6 +45,7 @@
#include "rpc/handlers/LedgerEntry.hpp" #include "rpc/handlers/LedgerEntry.hpp"
#include "rpc/handlers/LedgerIndex.hpp" #include "rpc/handlers/LedgerIndex.hpp"
#include "rpc/handlers/LedgerRange.hpp" #include "rpc/handlers/LedgerRange.hpp"
#include "rpc/handlers/MPTHolders.hpp"
#include "rpc/handlers/NFTBuyOffers.hpp" #include "rpc/handlers/NFTBuyOffers.hpp"
#include "rpc/handlers/NFTHistory.hpp" #include "rpc/handlers/NFTHistory.hpp"
#include "rpc/handlers/NFTInfo.hpp" #include "rpc/handlers/NFTInfo.hpp"
@@ -97,6 +98,7 @@ ProductionHandlerProvider::ProductionHandlerProvider(
{"ledger_entry", {LedgerEntryHandler{backend}}}, {"ledger_entry", {LedgerEntryHandler{backend}}},
{"ledger_index", {LedgerIndexHandler{backend}, true}}, // clio only {"ledger_index", {LedgerIndexHandler{backend}, true}}, // clio only
{"ledger_range", {LedgerRangeHandler{backend}}}, {"ledger_range", {LedgerRangeHandler{backend}}},
{"mpt_holders", {MPTHoldersHandler{backend}, true}}, // clio only
{"nfts_by_issuer", {NFTsByIssuerHandler{backend}, true}}, // clio only {"nfts_by_issuer", {NFTsByIssuerHandler{backend}, true}}, // clio only
{"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only {"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only
{"nft_buy_offers", {NFTBuyOffersHandler{backend}}}, {"nft_buy_offers", {NFTBuyOffersHandler{backend}}},

View File

@@ -78,10 +78,17 @@ AccountNFTsHandler::process(AccountNFTsHandler::Input input, Context const& ctx)
input.marker ? ripple::uint256{input.marker->c_str()} : ripple::keylet::nftpage_max(*accountID).key; input.marker ? ripple::uint256{input.marker->c_str()} : ripple::keylet::nftpage_max(*accountID).key;
auto const blob = sharedPtrBackend_->fetchLedgerObject(pageKey, lgrInfo.seq, ctx.yield); auto const blob = sharedPtrBackend_->fetchLedgerObject(pageKey, lgrInfo.seq, ctx.yield);
if (!blob) if (!blob) {
if (input.marker.has_value())
return Error{Status{RippledError::rpcINVALID_PARAMS, "Marker field does not match any valid Page ID"}};
return response; return response;
}
std::optional<ripple::SLE const> page{ripple::SLE{ripple::SerialIter{blob->data(), blob->size()}, pageKey}}; std::optional<ripple::SLE const> page{ripple::SLE{ripple::SerialIter{blob->data(), blob->size()}, pageKey}};
if (page->getType() != ripple::ltNFTOKEN_PAGE)
return Error{Status{RippledError::rpcINVALID_PARAMS, "Marker matches Page ID from another Account"}};
auto numPages = 0u; auto numPages = 0u;
while (page) { while (page) {

View File

@@ -55,10 +55,6 @@ class AccountObjectsHandler {
// dependencies // dependencies
std::shared_ptr<BackendInterface> sharedPtrBackend_; std::shared_ptr<BackendInterface> sharedPtrBackend_;
// constants
static std::unordered_map<std::string, ripple::LedgerEntryType> const TYPES_MAP;
static std::unordered_set<std::string> const TYPES_KEYS;
public: public:
static auto constexpr LIMIT_MIN = 10; static auto constexpr LIMIT_MIN = 10;
static auto constexpr LIMIT_MAX = 400; static auto constexpr LIMIT_MAX = 400;

View File

@@ -19,25 +19,32 @@
#include "rpc/handlers/DepositAuthorized.hpp" #include "rpc/handlers/DepositAuthorized.hpp"
#include "rpc/CredentialHelpers.hpp"
#include "rpc/Errors.hpp" #include "rpc/Errors.hpp"
#include "rpc/JS.hpp" #include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp" #include "rpc/RPCHelpers.hpp"
#include "rpc/common/Types.hpp" #include "rpc/common/Types.hpp"
#include "util/Assert.hpp"
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp> #include <boost/json/conversion.hpp>
#include <boost/json/object.hpp> #include <boost/json/object.hpp>
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <boost/json/value_to.hpp> #include <boost/json/value_to.hpp>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h> #include <xrpl/basics/strHex.h>
#include <xrpl/protocol/Indexes.h> #include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h> #include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h> #include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/SField.h> #include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STLedgerEntry.h> #include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h> #include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h> #include <xrpl/protocol/jss.h>
#include <memory>
#include <string> #include <string>
#include <utility>
#include <variant> #include <variant>
namespace rpc { namespace rpc {
@@ -71,26 +78,55 @@ DepositAuthorizedHandler::process(DepositAuthorizedHandler::Input input, Context
Output response; Output response;
auto it = ripple::SerialIter{dstAccountLedgerObject->data(), dstAccountLedgerObject->size()};
auto const sleDest = ripple::SLE{it, dstKeylet};
bool const reqAuth = sleDest.isFlag(ripple::lsfDepositAuth) && (sourceAccountID != destinationAccountID);
auto const& creds = input.credentials;
bool const credentialsPresent = creds.has_value();
ripple::STArray authCreds;
if (credentialsPresent) {
if (creds.value().empty()) {
return Error{Status{RippledError::rpcINVALID_PARAMS, "credential array has no elements."}};
}
if (creds.value().size() > ripple::maxCredentialsArraySize) {
return Error{Status{RippledError::rpcINVALID_PARAMS, "credential array too long."}};
}
auto const credArray = credentials::fetchCredentialArray(
input.credentials, *sourceAccountID, *sharedPtrBackend_, lgrInfo, ctx.yield
);
if (!credArray.has_value())
return Error{std::move(credArray).error()};
authCreds = std::move(credArray).value();
}
// If the two accounts are the same OR if that flag is
// not set, then the deposit should be fine.
bool depositAuthorized = true;
if (reqAuth) {
ripple::uint256 hashKey;
if (credentialsPresent) {
auto const sortedAuthCreds = credentials::createAuthCredentials(authCreds);
ASSERT(
sortedAuthCreds.size() == authCreds.size(), "should already be checked above that there is no duplicate"
);
hashKey = ripple::keylet::depositPreauth(*destinationAccountID, sortedAuthCreds).key;
} else {
hashKey = ripple::keylet::depositPreauth(*destinationAccountID, *sourceAccountID).key;
}
depositAuthorized = sharedPtrBackend_->fetchLedgerObject(hashKey, lgrInfo.seq, ctx.yield).has_value();
}
response.sourceAccount = input.sourceAccount; response.sourceAccount = input.sourceAccount;
response.destinationAccount = input.destinationAccount; response.destinationAccount = input.destinationAccount;
response.ledgerHash = ripple::strHex(lgrInfo.hash); response.ledgerHash = ripple::strHex(lgrInfo.hash);
response.ledgerIndex = lgrInfo.seq; response.ledgerIndex = lgrInfo.seq;
response.depositAuthorized = depositAuthorized;
// If the two accounts are the same, then the deposit should be fine. if (credentialsPresent)
if (sourceAccountID != destinationAccountID) { response.credentials = input.credentials.value();
auto it = ripple::SerialIter{dstAccountLedgerObject->data(), dstAccountLedgerObject->size()};
auto sle = ripple::SLE{it, dstKeylet};
// Check destination for the DepositAuth flag.
// If that flag is not set then a deposit should be just fine.
if ((sle.getFieldU32(ripple::sfFlags) & ripple::lsfDepositAuth) != 0u) {
// See if a preauthorization entry is in the ledger.
auto const depositPreauthKeylet = ripple::keylet::depositPreauth(*destinationAccountID, *sourceAccountID);
auto const sleDepositAuth =
sharedPtrBackend_->fetchLedgerObject(depositPreauthKeylet.key, lgrInfo.seq, ctx.yield);
response.depositAuthorized = static_cast<bool>(sleDepositAuth);
}
}
return response; return response;
} }
@@ -115,6 +151,10 @@ tag_invoke(boost::json::value_to_tag<DepositAuthorizedHandler::Input>, boost::js
} }
} }
if (jsonObject.contains(JS(credentials))) {
input.credentials = boost::json::value_to<boost::json::array>(jv.at(JS(credentials)));
}
return input; return input;
} }
@@ -127,8 +167,10 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, DepositAuthorize
{JS(destination_account), output.destinationAccount}, {JS(destination_account), output.destinationAccount},
{JS(ledger_hash), output.ledgerHash}, {JS(ledger_hash), output.ledgerHash},
{JS(ledger_index), output.ledgerIndex}, {JS(ledger_index), output.ledgerIndex},
{JS(validated), output.validated}, {JS(validated), output.validated}
}; };
if (output.credentials)
jv.as_object()[JS(credentials)] = *output.credentials;
} }
} // namespace rpc } // namespace rpc

View File

@@ -25,8 +25,10 @@
#include "rpc/common/Types.hpp" #include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp" #include "rpc/common/Validators.hpp"
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp> #include <boost/json/conversion.hpp>
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/jss.h> #include <xrpl/protocol/jss.h>
#include <cstdint> #include <cstdint>
@@ -59,6 +61,8 @@ public:
std::string destinationAccount; std::string destinationAccount;
std::string ledgerHash; std::string ledgerHash;
uint32_t ledgerIndex{}; uint32_t ledgerIndex{};
std::optional<boost::json::array> credentials;
// validated should be sent via framework // validated should be sent via framework
bool validated = true; bool validated = true;
}; };
@@ -71,6 +75,7 @@ public:
std::string destinationAccount; std::string destinationAccount;
std::optional<std::string> ledgerHash; std::optional<std::string> ledgerHash;
std::optional<uint32_t> ledgerIndex; std::optional<uint32_t> ledgerIndex;
std::optional<boost::json::array> credentials;
}; };
using Result = HandlerReturnType<Output>; using Result = HandlerReturnType<Output>;
@@ -99,6 +104,7 @@ public:
{JS(destination_account), validation::Required{}, validation::CustomValidators::AccountValidator}, {JS(destination_account), validation::Required{}, validation::CustomValidators::AccountValidator},
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator}, {JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator}, {JS(ledger_index), validation::CustomValidators::LedgerIndexValidator},
{JS(credentials), validation::Type<boost::json::array>{}, validation::Hex256ItemType()}
}; };
return rpcSpec; return rpcSpec;

View File

@@ -43,7 +43,6 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <unordered_set>
namespace rpc { namespace rpc {

View File

@@ -19,6 +19,7 @@
#include "rpc/handlers/LedgerEntry.hpp" #include "rpc/handlers/LedgerEntry.hpp"
#include "rpc/CredentialHelpers.hpp"
#include "rpc/Errors.hpp" #include "rpc/Errors.hpp"
#include "rpc/JS.hpp" #include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp" #include "rpc/RPCHelpers.hpp"
@@ -30,6 +31,8 @@
#include <boost/json/object.hpp> #include <boost/json/object.hpp>
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <boost/json/value_to.hpp> #include <boost/json/value_to.hpp>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h> #include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h> #include <xrpl/basics/strHex.h>
#include <xrpl/json/json_value.h> #include <xrpl/json/json_value.h>
@@ -97,11 +100,30 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
auto const owner = util::parseBase58Wrapper<ripple::AccountID>( auto const owner = util::parseBase58Wrapper<ripple::AccountID>(
boost::json::value_to<std::string>(input.depositPreauth->at(JS(owner))) boost::json::value_to<std::string>(input.depositPreauth->at(JS(owner)))
); );
auto const authorized = util::parseBase58Wrapper<ripple::AccountID>( // Only one of authorize or authorized_credentials MUST exist;
boost::json::value_to<std::string>(input.depositPreauth->at(JS(authorized))) if (input.depositPreauth->contains(JS(authorized)) ==
); input.depositPreauth->contains(JS(authorized_credentials))) {
return Error{
Status{ClioError::rpcMALFORMED_REQUEST, "Must have one of authorized or authorized_credentials."}
};
}
key = ripple::keylet::depositPreauth(*owner, *authorized).key; if (input.depositPreauth->contains(JS(authorized))) {
auto const authorized = util::parseBase58Wrapper<ripple::AccountID>(
boost::json::value_to<std::string>(input.depositPreauth->at(JS(authorized)))
);
key = ripple::keylet::depositPreauth(*owner, *authorized).key;
} else {
auto const authorizedCredentials = rpc::credentials::parseAuthorizeCredentials(
input.depositPreauth->at(JS(authorized_credentials)).as_array()
);
auto const authCreds = credentials::createAuthCredentials(authorizedCredentials);
if (authCreds.size() != authorizedCredentials.size())
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "duplicates in credentials."}};
key = ripple::keylet::depositPreauth(owner.value(), authCreds).key;
}
} else if (input.ticket) { } else if (input.ticket) {
auto const id = auto const id =
util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(input.ticket->at(JS(account)) util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(input.ticket->at(JS(account))
@@ -145,6 +167,18 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
} }
} else if (input.oracleNode) { } else if (input.oracleNode) {
key = input.oracleNode.value(); key = input.oracleNode.value();
} else if (input.credential) {
key = input.credential.value();
} else if (input.mptIssuance) {
auto const mptIssuanceID = ripple::uint192{std::string_view(*(input.mptIssuance))};
key = ripple::keylet::mptIssuance(mptIssuanceID).key;
} else if (input.mptoken) {
auto const holder =
ripple::parseBase58<ripple::AccountID>(boost::json::value_to<std::string>(input.mptoken->at(JS(account))));
auto const mptIssuanceID =
ripple::uint192{std::string_view(boost::json::value_to<std::string>(input.mptoken->at(JS(mpt_issuance_id))))
};
key = ripple::keylet::mptoken(mptIssuanceID, *holder).key;
} else { } else {
// Must specify 1 of the following fields to indicate what type // Must specify 1 of the following fields to indicate what type
if (ctx.apiVersion == 1) if (ctx.apiVersion == 1)
@@ -277,6 +311,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
{JS(xchain_owned_create_account_claim_id), ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, {JS(xchain_owned_create_account_claim_id), ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
{JS(xchain_owned_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID}, {JS(xchain_owned_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID},
{JS(oracle), ripple::ltORACLE}, {JS(oracle), ripple::ltORACLE},
{JS(credential), ripple::ltCREDENTIAL},
{JS(mptoken), ripple::ltMPTOKEN},
}; };
auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) { auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) {
@@ -302,6 +338,16 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
return ripple::keylet::oracle(*account, documentId).key; return ripple::keylet::oracle(*account, documentId).key;
}; };
auto const parseCredentialFromJson = [](boost::json::value const& json) {
auto const subject =
util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(json.at(JS(subject))));
auto const issuer =
util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(json.at(JS(issuer))));
auto const credType = ripple::strUnHex(boost::json::value_to<std::string>(json.at(JS(credential_type))));
return ripple::keylet::credential(*subject, *issuer, ripple::Slice(credType->data(), credType->size())).key;
};
auto const indexFieldType = auto const indexFieldType =
std::find_if(indexFieldTypeMap.begin(), indexFieldTypeMap.end(), [&jsonObject](auto const& pair) { std::find_if(indexFieldTypeMap.begin(), indexFieldTypeMap.end(), [&jsonObject](auto const& pair) {
auto const& [field, _] = pair; auto const& [field, _] = pair;
@@ -317,6 +363,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
input.accountRoot = boost::json::value_to<std::string>(jv.at(JS(account_root))); input.accountRoot = boost::json::value_to<std::string>(jv.at(JS(account_root)));
} else if (jsonObject.contains(JS(did))) { } else if (jsonObject.contains(JS(did))) {
input.did = boost::json::value_to<std::string>(jv.at(JS(did))); input.did = boost::json::value_to<std::string>(jv.at(JS(did)));
} else if (jsonObject.contains(JS(mpt_issuance))) {
input.mptIssuance = boost::json::value_to<std::string>(jv.at(JS(mpt_issuance)));
} }
// no need to check if_object again, validator only allows string or object // no need to check if_object again, validator only allows string or object
else if (jsonObject.contains(JS(directory))) { else if (jsonObject.contains(JS(directory))) {
@@ -348,6 +396,10 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
); );
} else if (jsonObject.contains(JS(oracle))) { } else if (jsonObject.contains(JS(oracle))) {
input.oracleNode = parseOracleFromJson(jv.at(JS(oracle))); input.oracleNode = parseOracleFromJson(jv.at(JS(oracle)));
} else if (jsonObject.contains(JS(credential))) {
input.credential = parseCredentialFromJson(jv.at(JS(credential)));
} else if (jsonObject.contains(JS(mptoken))) {
input.mptoken = jv.at(JS(mptoken)).as_object();
} }
if (jsonObject.contains("include_deleted")) if (jsonObject.contains("include_deleted"))

View File

@@ -30,6 +30,7 @@
#include "rpc/common/Validators.hpp" #include "rpc/common/Validators.hpp"
#include "util/AccountUtils.hpp" #include "util/AccountUtils.hpp"
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp> #include <boost/json/conversion.hpp>
#include <boost/json/object.hpp> #include <boost/json/object.hpp>
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
@@ -91,6 +92,8 @@ public:
std::optional<std::string> accountRoot; std::optional<std::string> accountRoot;
// account id to address did object // account id to address did object
std::optional<std::string> did; std::optional<std::string> did;
// mpt issuance id to address mptIssuance object
std::optional<std::string> mptIssuance;
// TODO: extract into custom objects, remove json from Input // TODO: extract into custom objects, remove json from Input
std::optional<boost::json::object> directory; std::optional<boost::json::object> directory;
std::optional<boost::json::object> offer; std::optional<boost::json::object> offer;
@@ -99,11 +102,13 @@ public:
std::optional<boost::json::object> depositPreauth; std::optional<boost::json::object> depositPreauth;
std::optional<boost::json::object> ticket; std::optional<boost::json::object> ticket;
std::optional<boost::json::object> amm; std::optional<boost::json::object> amm;
std::optional<boost::json::object> mptoken;
std::optional<ripple::STXChainBridge> bridge; std::optional<ripple::STXChainBridge> bridge;
std::optional<std::string> bridgeAccount; std::optional<std::string> bridgeAccount;
std::optional<uint32_t> chainClaimId; std::optional<uint32_t> chainClaimId;
std::optional<uint32_t> createAccountClaimId; std::optional<uint32_t> createAccountClaimId;
std::optional<ripple::uint256> oracleNode; std::optional<ripple::uint256> oracleNode;
std::optional<ripple::uint256> credential;
bool includeDeleted = false; bool includeDeleted = false;
}; };
@@ -194,7 +199,8 @@ public:
meta::WithCustomError{ meta::WithCustomError{
validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_OWNER) validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_OWNER)
}}, }},
{JS(authorized), validation::Required{}, validation::CustomValidators::AccountBase58Validator}, {JS(authorized), validation::CustomValidators::AccountBase58Validator},
{JS(authorized_credentials), validation::CustomValidators::AuthorizeCredentialValidator}
}, },
}}, }},
{JS(directory), {JS(directory),
@@ -315,6 +321,59 @@ public:
}, },
meta::WithCustomError{modifiers::ToNumber{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)}}, meta::WithCustomError{modifiers::ToNumber{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)}},
}}}, }}},
{JS(credential),
meta::WithCustomError{
validation::Type<std::string, boost::json::object>{}, Status(ClioError::rpcMALFORMED_REQUEST)
},
meta::IfType<std::string>{
meta::WithCustomError{malformedRequestHexStringValidator, Status(ClioError::rpcMALFORMED_ADDRESS)}
},
meta::IfType<boost::json::object>{meta::Section{
{JS(subject),
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
meta::WithCustomError{
validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_ADDRESS)
}},
{JS(issuer),
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
meta::WithCustomError{
validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_ADDRESS)
}},
{
JS(credential_type),
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
meta::WithCustomError{validation::Type<std::string>{}, Status(ClioError::rpcMALFORMED_REQUEST)},
},
}}},
{JS(mpt_issuance),
meta::WithCustomError{
validation::CustomValidators::Uint192HexStringValidator, Status(ClioError::rpcMALFORMED_REQUEST)
}},
{JS(mptoken),
meta::WithCustomError{
validation::Type<std::string, boost::json::object>{}, Status(ClioError::rpcMALFORMED_REQUEST)
},
meta::IfType<std::string>{malformedRequestHexStringValidator},
meta::IfType<boost::json::object>{
meta::Section{
{
JS(account),
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
meta::WithCustomError{
validation::CustomValidators::AccountBase58Validator,
Status(ClioError::rpcMALFORMED_ADDRESS)
},
},
{
JS(mpt_issuance_id),
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
meta::WithCustomError{
validation::CustomValidators::Uint192HexStringValidator,
Status(ClioError::rpcMALFORMED_REQUEST)
},
},
},
}},
{JS(ledger), check::Deprecated{}}, {JS(ledger), check::Deprecated{}},
{"include_deleted", validation::Type<bool>{}}, {"include_deleted", validation::Type<bool>{}},
}; };

View File

@@ -0,0 +1,139 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "rpc/handlers/MPTHolders.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/common/Types.hpp"
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <ripple/basics/base_uint.h>
#include <ripple/basics/strHex.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/LedgerHeader.h>
#include <ripple/protocol/jss.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <optional>
#include <string>
#include <variant>
using namespace ripple;
namespace rpc {
MPTHoldersHandler::Result
MPTHoldersHandler::process(MPTHoldersHandler::Input input, Context const& ctx) const
{
auto const range = sharedPtrBackend_->fetchLedgerRange();
auto const lgrInfoOrStatus = getLedgerHeaderFromHashOrSeq(
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence
);
if (auto const status = std::get_if<Status>(&lgrInfoOrStatus))
return Error{*status};
auto const lgrInfo = std::get<LedgerInfo>(lgrInfoOrStatus);
auto const limit = input.limit.value_or(MPTHoldersHandler::LIMIT_DEFAULT);
auto const mptID = ripple::uint192{input.mptID.c_str()};
auto const issuanceLedgerObject =
sharedPtrBackend_->fetchLedgerObject(ripple::keylet::mptIssuance(mptID).key, lgrInfo.seq, ctx.yield);
if (!issuanceLedgerObject)
return Error{Status{RippledError::rpcOBJECT_NOT_FOUND, "objectNotFound"}};
std::optional<ripple::AccountID> cursor;
if (input.marker)
cursor = ripple::AccountID{input.marker->c_str()};
auto const dbResponse = sharedPtrBackend_->fetchMPTHolders(mptID, limit, cursor, lgrInfo.seq, ctx.yield);
auto output = MPTHoldersHandler::Output{};
output.mptID = to_string(mptID);
output.limit = limit;
output.ledgerIndex = lgrInfo.seq;
boost::json::array const mpts;
for (auto const& mpt : dbResponse.mptokens) {
ripple::STLedgerEntry const sle{ripple::SerialIter{mpt.data(), mpt.size()}, keylet::mptIssuance(mptID).key};
boost::json::object mptJson;
mptJson[JS(account)] = toBase58(sle[ripple::sfAccount]);
mptJson[JS(flags)] = sle.getFlags();
mptJson["mpt_amount"] =
toBoostJson(ripple::STUInt64{ripple::sfMPTAmount, sle[ripple::sfMPTAmount]}.getJson(JsonOptions::none));
mptJson["mptoken_index"] = ripple::to_string(ripple::keylet::mptoken(mptID, sle[ripple::sfAccount]).key);
output.mpts.push_back(mptJson);
}
if (dbResponse.cursor.has_value())
output.marker = strHex(*dbResponse.cursor);
return output;
}
void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, MPTHoldersHandler::Output const& output)
{
jv = {
{JS(mpt_issuance_id), output.mptID},
{JS(limit), output.limit},
{JS(ledger_index), output.ledgerIndex},
{"mptokens", output.mpts},
{JS(validated), output.validated},
};
if (output.marker.has_value())
jv.as_object()[JS(marker)] = *(output.marker);
}
MPTHoldersHandler::Input
tag_invoke(boost::json::value_to_tag<MPTHoldersHandler::Input>, boost::json::value const& jv)
{
auto const& jsonObject = jv.as_object();
MPTHoldersHandler::Input input;
input.mptID = jsonObject.at(JS(mpt_issuance_id)).as_string().c_str();
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = jsonObject.at(JS(ledger_hash)).as_string().c_str();
if (jsonObject.contains(JS(ledger_index))) {
if (!jsonObject.at(JS(ledger_index)).is_string()) {
input.ledgerIndex = jsonObject.at(JS(ledger_index)).as_int64();
} else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") {
input.ledgerIndex = std::stoi(jsonObject.at(JS(ledger_index)).as_string().c_str());
}
}
if (jsonObject.contains(JS(limit)))
input.limit = jsonObject.at(JS(limit)).as_int64();
if (jsonObject.contains(JS(marker)))
input.marker = jsonObject.at(JS(marker)).as_string().c_str();
return input;
}
} // namespace rpc

View File

@@ -0,0 +1,128 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "data/BackendInterface.hpp"
#include "rpc/JS.hpp"
#include "rpc/common/Modifiers.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
namespace rpc {
/**
* @brief The mpt_holders command asks the Clio server for all holders of a particular MPTokenIssuance.
*/
class MPTHoldersHandler {
std::shared_ptr<BackendInterface> sharedPtrBackend_;
public:
static auto constexpr LIMIT_MIN = 1;
static auto constexpr LIMIT_MAX = 100;
static auto constexpr LIMIT_DEFAULT = 50;
/**
* @brief A struct to hold the output data of the command
*/
struct Output {
boost::json::array mpts;
uint32_t ledgerIndex;
std::string mptID;
bool validated = true;
uint32_t limit;
std::optional<std::string> marker;
};
/**
* @brief A struct to hold the input data for the command
*/
struct Input {
std::string mptID;
std::optional<std::string> ledgerHash;
std::optional<uint32_t> ledgerIndex;
std::optional<std::string> marker;
std::optional<uint32_t> limit;
};
using Result = HandlerReturnType<Output>;
/**
* @brief Construct a new MPTHoldersHandler object
*
* @param sharedPtrBackend The backend to use
*/
MPTHoldersHandler(std::shared_ptr<BackendInterface> const& sharedPtrBackend) : sharedPtrBackend_(sharedPtrBackend)
{
}
/**
* @brief Returns the API specification for the command
*
* @param apiVersion The api version to return the spec for
* @return The spec for the given apiVersion
*/
static RpcSpecConstRef
spec([[maybe_unused]] uint32_t apiVersion)
{
static auto const rpcSpec = RpcSpec{
{JS(mpt_issuance_id), validation::Required{}, validation::CustomValidators::Uint192HexStringValidator},
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator},
{JS(limit),
validation::Type<uint32_t>{},
validation::Min(1u),
modifiers::Clamp<int32_t>{LIMIT_MIN, LIMIT_MAX}},
{JS(marker), validation::CustomValidators::Uint160HexStringValidator},
};
return rpcSpec;
}
/**
* @brief Process the MPTHolders command
*
* @param input The input data for the command
* @param ctx The context of the request
* @return The result of the operation
*/
Result
process(Input input, Context const& ctx) const;
private:
/**
* @brief Convert the Output to a JSON object
*
* @param [out] jv The JSON object to convert to
* @param output The output to convert
*/
friend void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output);
/**
* @brief Convert a JSON object to Input type
*
* @param jv The JSON object to convert
* @return Input parsed from the JSON object
*/
friend Input
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
};
} // namespace rpc

View File

@@ -19,6 +19,8 @@
#pragma once #pragma once
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/tokens.h> #include <xrpl/protocol/tokens.h>
#include <cctype> #include <cctype>

View File

@@ -4,6 +4,7 @@ target_sources(
clio_util clio_util
PRIVATE build/Build.cpp PRIVATE build/Build.cpp
config/Config.cpp config/Config.cpp
CoroutineGroup.cpp
log/Logger.cpp log/Logger.cpp
prometheus/Http.cpp prometheus/Http.cpp
prometheus/Label.cpp prometheus/Label.cpp
@@ -19,15 +20,19 @@ target_sources(
requests/Types.cpp requests/Types.cpp
requests/WsConnection.cpp requests/WsConnection.cpp
requests/impl/SslContext.cpp requests/impl/SslContext.cpp
ResponseExpirationCache.cpp
SignalsHandler.cpp SignalsHandler.cpp
Taggable.cpp Taggable.cpp
TerminationHandler.cpp TerminationHandler.cpp
TimeUtils.cpp TimeUtils.cpp
TxUtils.cpp TxUtils.cpp
LedgerUtils.cpp LedgerUtils.cpp
newconfig/ConfigDefinition.cpp newconfig/Array.cpp
newconfig/ObjectView.cpp
newconfig/ArrayView.cpp newconfig/ArrayView.cpp
newconfig/ConfigConstraints.cpp
newconfig/ConfigDefinition.cpp
newconfig/ConfigFileJson.cpp
newconfig/ObjectView.cpp
newconfig/ValueView.cpp newconfig/ValueView.cpp
) )

View File

@@ -19,6 +19,8 @@
#pragma once #pragma once
#include <algorithm>
#include <array>
#include <type_traits> #include <type_traits>
namespace util { namespace util {
@@ -29,4 +31,19 @@ namespace util {
template <typename T> template <typename T>
concept SomeNumberType = std::is_arithmetic_v<T> && !std::is_same_v<T, bool> && !std::is_const_v<T>; concept SomeNumberType = std::is_arithmetic_v<T> && !std::is_same_v<T, bool> && !std::is_const_v<T>;
/**
* @brief Checks that the list of given values contains no duplicates
*
* @param values The list of values to check
* @returns true if no duplicates exist; false otherwise
*/
static consteval auto
hasNoDuplicates(auto&&... values)
{
auto store = std::array{values...};
auto end = store.end();
std::ranges::sort(store);
return (std::unique(std::begin(store), end) == end);
}
} // namespace util } // namespace util

View File

@@ -0,0 +1,82 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/CoroutineGroup.hpp"
#include "util/Assert.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <cstddef>
#include <functional>
#include <optional>
#include <utility>
namespace util {
CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren)
: timer_{yield.get_executor(), boost::asio::steady_timer::duration::max()}, maxChildren_{maxChildren}
{
}
CoroutineGroup::~CoroutineGroup()
{
ASSERT(childrenCounter_ == 0, "CoroutineGroup is destroyed without waiting for child coroutines to finish");
}
bool
CoroutineGroup::canSpawn() const
{
return not maxChildren_.has_value() or childrenCounter_ < *maxChildren_;
}
bool
CoroutineGroup::spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn)
{
if (not canSpawn())
return false;
++childrenCounter_;
boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) {
fn(yield);
--childrenCounter_;
if (childrenCounter_ == 0)
timer_.cancel();
});
return true;
}
void
CoroutineGroup::asyncWait(boost::asio::yield_context yield)
{
if (childrenCounter_ == 0)
return;
boost::system::error_code error;
timer_.async_wait(yield[error]);
}
size_t
CoroutineGroup::size() const
{
return childrenCounter_;
}
} // namespace util

View File

@@ -0,0 +1,96 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <cstddef>
#include <functional>
#include <optional>
namespace util {
/**
* @brief CoroutineGroup is a helper class to manage a group of coroutines. It allows to spawn multiple coroutines and
* wait for all of them to finish.
*/
class CoroutineGroup {
boost::asio::steady_timer timer_;
std::optional<int> maxChildren_;
int childrenCounter_{0};
public:
/**
* @brief Construct a new Coroutine Group object
*
* @param yield The yield context to use for the internal timer
* @param maxChildren The maximum number of coroutines that can be spawned at the same time. If not provided, there
* is no limit
*/
CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren = std::nullopt);
/**
* @brief Destroy the Coroutine Group object
*
* @note asyncWait() must be called before the object is destroyed
*/
~CoroutineGroup();
/**
* @brief Check if a new coroutine can be spawned (i.e. there is space for a new coroutine in the group)
*
* @return true If a new coroutine can be spawned. false if the maximum number of coroutines has been reached
*/
bool
canSpawn() const;
/**
* @brief Spawn a new coroutine in the group
*
* @param yield The yield context to use for the coroutine (it should be the same as the one used in the
* constructor)
* @param fn The function to execute
* @return true If the coroutine was spawned successfully. false if the maximum number of coroutines has been
* reached
*/
bool
spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn);
/**
* @brief Wait for all the coroutines in the group to finish
*
* @note This method must be called before the object is destroyed
*
* @param yield The yield context to use for the internal timer
*/
void
asyncWait(boost::asio::yield_context yield);
/**
* @brief Get the number of coroutines in the group
*
* @return size_t The number of coroutines in the group
*/
size_t
size() const;
};
} // namespace util

View File

@@ -112,7 +112,10 @@ class LedgerTypes {
), ),
LedgerTypeAttribute::AccountOwnedLedgerType(JS(did), ripple::ltDID), LedgerTypeAttribute::AccountOwnedLedgerType(JS(did), ripple::ltDID),
LedgerTypeAttribute::AccountOwnedLedgerType(JS(oracle), ripple::ltORACLE), LedgerTypeAttribute::AccountOwnedLedgerType(JS(oracle), ripple::ltORACLE),
LedgerTypeAttribute::AccountOwnedLedgerType(JS(credential), ripple::ltCREDENTIAL),
LedgerTypeAttribute::ChainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL), LedgerTypeAttribute::ChainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL),
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mpt_issuance), ripple::ltMPTOKEN_ISSUANCE),
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mptoken), ripple::ltMPTOKEN),
}; };
public: public:

View File

@@ -17,88 +17,56 @@
*/ */
//============================================================================== //==============================================================================
#include "etl/impl/ForwardingCache.hpp" #include "util/ResponseExpirationCache.hpp"
#include "util/Assert.hpp" #include "util/Assert.hpp"
#include <boost/json/object.hpp> #include <boost/json/object.hpp>
#include <boost/json/value_to.hpp>
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <shared_mutex> #include <shared_mutex>
#include <string> #include <string>
#include <unordered_set>
#include <utility> #include <utility>
namespace etl::impl { namespace util {
namespace {
std::optional<std::string>
getCommand(boost::json::object const& request)
{
if (not request.contains("command")) {
return std::nullopt;
}
return boost::json::value_to<std::string>(request.at("command"));
}
} // namespace
void void
CacheEntry::put(boost::json::object response) ResponseExpirationCache::Entry::put(boost::json::object response)
{ {
response_ = std::move(response); response_ = std::move(response);
lastUpdated_ = std::chrono::steady_clock::now(); lastUpdated_ = std::chrono::steady_clock::now();
} }
std::optional<boost::json::object> std::optional<boost::json::object>
CacheEntry::get() const ResponseExpirationCache::Entry::get() const
{ {
return response_; return response_;
} }
std::chrono::steady_clock::time_point std::chrono::steady_clock::time_point
CacheEntry::lastUpdated() const ResponseExpirationCache::Entry::lastUpdated() const
{ {
return lastUpdated_; return lastUpdated_;
} }
void void
CacheEntry::invalidate() ResponseExpirationCache::Entry::invalidate()
{ {
response_.reset(); response_.reset();
} }
std::unordered_set<std::string> const
ForwardingCache::CACHEABLE_COMMANDS{"server_info", "server_state", "server_definitions", "fee", "ledger_closed"};
ForwardingCache::ForwardingCache(std::chrono::steady_clock::duration const cacheTimeout) : cacheTimeout_{cacheTimeout}
{
for (auto const& command : CACHEABLE_COMMANDS) {
cache_.emplace(command, CacheEntry{});
}
}
bool bool
ForwardingCache::shouldCache(boost::json::object const& request) ResponseExpirationCache::shouldCache(std::string const& cmd)
{ {
auto const command = getCommand(request); return cache_.contains(cmd);
return command.has_value() and CACHEABLE_COMMANDS.contains(*command);
} }
std::optional<boost::json::object> std::optional<boost::json::object>
ForwardingCache::get(boost::json::object const& request) const ResponseExpirationCache::get(std::string const& cmd) const
{ {
auto const command = getCommand(request); auto it = cache_.find(cmd);
if (not command.has_value()) {
return std::nullopt;
}
auto it = cache_.find(*command);
if (it == cache_.end()) if (it == cache_.end())
return std::nullopt; return std::nullopt;
@@ -110,20 +78,19 @@ ForwardingCache::get(boost::json::object const& request) const
} }
void void
ForwardingCache::put(boost::json::object const& request, boost::json::object const& response) ResponseExpirationCache::put(std::string const& cmd, boost::json::object const& response)
{ {
auto const command = getCommand(request); if (not shouldCache(cmd))
if (not command.has_value() or not shouldCache(request))
return; return;
ASSERT(cache_.contains(*command), "Command is not in the cache: {}", *command); ASSERT(cache_.contains(cmd), "Command is not in the cache: {}", cmd);
auto entry = cache_[*command].lock<std::unique_lock>(); auto entry = cache_[cmd].lock<std::unique_lock>();
entry->put(response); entry->put(response);
} }
void void
ForwardingCache::invalidate() ResponseExpirationCache::invalidate()
{ {
for (auto& [_, entry] : cache_) { for (auto& [_, entry] : cache_) {
auto entryLock = entry.lock<std::unique_lock>(); auto entryLock = entry.lock<std::unique_lock>();
@@ -131,4 +98,4 @@ ForwardingCache::invalidate()
} }
} }
} // namespace etl::impl } // namespace util

View File

@@ -30,90 +30,92 @@
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
namespace etl::impl { namespace util {
/** /**
* @brief A class to store a cache entry. * @brief Cache of requests' responses with TTL support and configurable cachable commands
*/ */
class CacheEntry { class ResponseExpirationCache {
std::chrono::steady_clock::time_point lastUpdated_;
std::optional<boost::json::object> response_;
public:
/** /**
* @brief Put a response into the cache * @brief A class to store a cache entry.
*
* @param response The response to store
*/ */
void class Entry {
put(boost::json::object response); std::chrono::steady_clock::time_point lastUpdated_;
std::optional<boost::json::object> response_;
/** public:
* @brief Get the response from the cache /**
* * @brief Put a response into the cache
* @return The response *
*/ * @param response The response to store
std::optional<boost::json::object> */
get() const; void
put(boost::json::object response);
/** /**
* @brief Get the last time the cache was updated * @brief Get the response from the cache
* *
* @return The last time the cache was updated * @return The response
*/ */
std::chrono::steady_clock::time_point std::optional<boost::json::object>
lastUpdated() const; get() const;
/** /**
* @brief Invalidate the cache entry * @brief Get the last time the cache was updated
*/ *
void * @return The last time the cache was updated
invalidate(); */
}; std::chrono::steady_clock::time_point
lastUpdated() const;
/**
* @brief Invalidate the cache entry
*/
void
invalidate();
};
/**
* @brief A class to store a cache of forwarding responses
*/
class ForwardingCache {
std::chrono::steady_clock::duration cacheTimeout_; std::chrono::steady_clock::duration cacheTimeout_;
std::unordered_map<std::string, util::Mutex<CacheEntry, std::shared_mutex>> cache_; std::unordered_map<std::string, util::Mutex<Entry, std::shared_mutex>> cache_;
bool
shouldCache(std::string const& cmd);
public: public:
static std::unordered_set<std::string> const CACHEABLE_COMMANDS;
/** /**
* @brief Construct a new Forwarding Cache object * @brief Construct a new Cache object
* *
* @param cacheTimeout The time for cache entries to expire * @param cacheTimeout The time for cache entries to expire
* @param cmds The commands that should be cached
*/ */
ForwardingCache(std::chrono::steady_clock::duration cacheTimeout); ResponseExpirationCache(
std::chrono::steady_clock::duration cacheTimeout,
/** std::unordered_set<std::string> const& cmds
* @brief Check if a request should be cached )
* : cacheTimeout_(cacheTimeout)
* @param request The request to check {
* @return true if the request should be cached and false otherwise for (auto const& command : cmds) {
*/ cache_.emplace(command, Entry{});
[[nodiscard]] static bool }
shouldCache(boost::json::object const& request); }
/** /**
* @brief Get a response from the cache * @brief Get a response from the cache
* *
* @param request The request to get the response for * @param cmd The command to get the response for
* @return The response if it exists or std::nullopt otherwise * @return The response if it exists or std::nullopt otherwise
*/ */
[[nodiscard]] std::optional<boost::json::object> [[nodiscard]] std::optional<boost::json::object>
get(boost::json::object const& request) const; get(std::string const& cmd) const;
/** /**
* @brief Put a response into the cache if the request should be cached * @brief Put a response into the cache if the request should be cached
* *
* @param request The request to store the response for * @param cmd The command to store the response for
* @param response The response to store * @param response The response to store
*/ */
void void
put(boost::json::object const& request, boost::json::object const& response); put(std::string const& cmd, boost::json::object const& response);
/** /**
* @brief Invalidate all entries in the cache * @brief Invalidate all entries in the cache
@@ -121,5 +123,4 @@ public:
void void
invalidate(); invalidate();
}; };
} // namespace util
} // namespace etl::impl

View File

@@ -57,10 +57,16 @@ Retry::Retry(RetryStrategyPtr strategy, boost::asio::strand<boost::asio::io_cont
{ {
} }
Retry::~Retry()
{
*canceled_ = true;
}
void void
Retry::cancel() Retry::cancel()
{ {
timer_.cancel(); timer_.cancel();
*canceled_ = true;
} }
size_t size_t

View File

@@ -24,6 +24,7 @@
#include <boost/asio/steady_timer.hpp> #include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp> #include <boost/asio/strand.hpp>
#include <atomic>
#include <chrono> #include <chrono>
#include <cstddef> #include <cstddef>
#include <memory> #include <memory>
@@ -80,6 +81,7 @@ class Retry {
RetryStrategyPtr strategy_; RetryStrategyPtr strategy_;
boost::asio::steady_timer timer_; boost::asio::steady_timer timer_;
size_t attemptNumber_ = 0; size_t attemptNumber_ = 0;
std::shared_ptr<std::atomic_bool> canceled_{std::make_shared<std::atomic_bool>(false)};
public: public:
/** /**
@@ -90,6 +92,11 @@ public:
*/ */
Retry(RetryStrategyPtr strategy, boost::asio::strand<boost::asio::io_context::executor_type> strand); Retry(RetryStrategyPtr strategy, boost::asio::strand<boost::asio::io_context::executor_type> strand);
/**
* @brief Destroy the Retry object
*/
~Retry();
/** /**
* @brief Schedule a retry * @brief Schedule a retry
* *
@@ -100,15 +107,18 @@ public:
void void
retry(Fn&& func) retry(Fn&& func)
{ {
*canceled_ = false;
timer_.expires_after(strategy_->getDelay()); timer_.expires_after(strategy_->getDelay());
strategy_->increaseDelay(); strategy_->increaseDelay();
timer_.async_wait([this, func = std::forward<Fn>(func)](boost::system::error_code const& ec) { timer_.async_wait(
if (ec == boost::asio::error::operation_aborted) { [this, canceled = canceled_, func = std::forward<Fn>(func)](boost::system::error_code const& ec) {
return; if (ec == boost::asio::error::operation_aborted or *canceled) {
return;
}
++attemptNumber_;
func();
} }
++attemptNumber_; );
func();
});
} }
/** /**

71
src/util/WithTimeout.hpp Normal file
View File

@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio/associated_executor.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/system/detail/error_code.hpp>
#include <boost/system/errc.hpp>
#include <chrono>
#include <ctime>
#include <memory>
namespace util {
/**
* @brief Perform a coroutine operation with a timeout.
*
* @tparam Operation The operation type to perform. Must be a callable accepting yield context with bound cancellation
* token.
* @param operation The operation to perform.
* @param yield The yield context.
* @param timeout The timeout duration.
* @return The error code of the operation.
*/
template <typename Operation>
boost::system::error_code
withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::system::error_code error;
auto operationCompleted = std::make_shared<bool>(false);
boost::asio::cancellation_signal cancellationSignal;
auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield[error]);
boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout};
timer.async_wait([&cancellationSignal, operationCompleted](boost::system::error_code errorCode) {
if (!errorCode and !*operationCompleted)
cancellationSignal.emit(boost::asio::cancellation_type::terminal);
});
operation(cyield);
*operationCompleted = true;
// Map error code to timeout
if (error == boost::system::errc::operation_canceled) {
return boost::system::errc::make_error_code(boost::system::errc::timed_out);
}
return error;
}
} // namespace util

View File

@@ -369,7 +369,7 @@ public:
* @brief Block until all operations are completed * @brief Block until all operations are completed
*/ */
void void
join() noexcept join() const noexcept
{ {
context_.join(); context_.join();
} }

View File

@@ -0,0 +1,84 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/newconfig/Array.hpp"
#include "util/Assert.hpp"
#include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/Types.hpp"
#include <cstddef>
#include <optional>
#include <string_view>
#include <utility>
#include <vector>
namespace util::config {
Array::Array(ConfigValue arg) : itemPattern_{std::move(arg)}
{
}
std::optional<Error>
Array::addValue(Value value, std::optional<std::string_view> key)
{
auto const& configValPattern = itemPattern_;
auto const constraint = configValPattern.getConstraint();
auto newElem = constraint.has_value() ? ConfigValue{configValPattern.type()}.withConstraint(constraint->get())
: ConfigValue{configValPattern.type()};
if (auto const maybeError = newElem.setValue(value, key); maybeError.has_value())
return maybeError;
elements_.emplace_back(std::move(newElem));
return std::nullopt;
}
size_t
Array::size() const
{
return elements_.size();
}
ConfigValue const&
Array::at(std::size_t idx) const
{
ASSERT(idx < elements_.size(), "Index is out of scope");
return elements_[idx];
}
ConfigValue const&
Array::getArrayPattern() const
{
return itemPattern_;
}
std::vector<ConfigValue>::const_iterator
Array::begin() const
{
return elements_.begin();
}
std::vector<ConfigValue>::const_iterator
Array::end() const
{
return elements_.end();
}
} // namespace util::config

View File

@@ -19,47 +19,42 @@
#pragma once #pragma once
#include "util/Assert.hpp"
#include "util/newconfig/ConfigValue.hpp" #include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/ObjectView.hpp" #include "util/newconfig/Error.hpp"
#include "util/newconfig/ValueView.hpp" #include "util/newconfig/Types.hpp"
#include <cstddef> #include <cstddef>
#include <iterator> #include <optional>
#include <type_traits> #include <string_view>
#include <utility>
#include <vector> #include <vector>
namespace util::config { namespace util::config {
/** /**
* @brief Array definition for Json/Yaml config * @brief Array definition to store multiple values provided by the user from Json/Yaml
* *
* Used in ClioConfigDefinition to represent multiple potential values (like whitelist) * Used in ClioConfigDefinition to represent multiple potential values (like whitelist)
* Is constructed with only 1 element which states which type/constraint must every element
* In the array satisfy
*/ */
class Array { class Array {
public: public:
/** /**
* @brief Constructs an Array with the provided arguments * @brief Constructs an Array with provided Arg
* *
* @tparam Args Types of the arguments * @param arg Argument to set the type and constraint of ConfigValues in Array
* @param args Arguments to initialize the elements of the Array
*/ */
template <typename... Args> Array(ConfigValue arg);
constexpr Array(Args&&... args) : elements_{std::forward<Args>(args)...}
{
}
/** /**
* @brief Add ConfigValues to Array class * @brief Add ConfigValues to Array class
* *
* @param value The ConfigValue to add * @param value The ConfigValue to add
* @param key optional string key to include that will show in error message
* @return optional error if adding config value to array fails. nullopt otherwise
*/ */
void std::optional<Error>
emplaceBack(ConfigValue value) addValue(Value value, std::optional<std::string_view> key = std::nullopt);
{
elements_.push_back(std::move(value));
}
/** /**
* @brief Returns the number of values stored in the Array * @brief Returns the number of values stored in the Array
@@ -67,10 +62,7 @@ public:
* @return Number of values stored in the Array * @return Number of values stored in the Array
*/ */
[[nodiscard]] size_t [[nodiscard]] size_t
size() const size() const;
{
return elements_.size();
}
/** /**
* @brief Returns the ConfigValue at the specified index * @brief Returns the ConfigValue at the specified index
@@ -79,13 +71,35 @@ public:
* @return ConfigValue at the specified index * @return ConfigValue at the specified index
*/ */
[[nodiscard]] ConfigValue const& [[nodiscard]] ConfigValue const&
at(std::size_t idx) const at(std::size_t idx) const;
{
ASSERT(idx < elements_.size(), "index is out of scope"); /**
return elements_[idx]; * @brief Returns the ConfigValue that defines the type/constraint every
} * ConfigValue must follow in Array
*
* @return The item_pattern
*/
[[nodiscard]] ConfigValue const&
getArrayPattern() const;
/**
* @brief Returns an iterator to the beginning of the ConfigValue vector.
*
* @return A constant iterator to the beginning of the vector.
*/
[[nodiscard]] std::vector<ConfigValue>::const_iterator
begin() const;
/**
* @brief Returns an iterator to the end of the ConfigValue vector.
*
* @return A constant iterator to the end of the vector.
*/
[[nodiscard]] std::vector<ConfigValue>::const_iterator
end() const;
private: private:
ConfigValue itemPattern_;
std::vector<ConfigValue> elements_; std::vector<ConfigValue> elements_;
}; };

View File

@@ -0,0 +1,103 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/newconfig/ConfigConstraints.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/Types.hpp"
#include <cstdint>
#include <optional>
#include <regex>
#include <stdexcept>
#include <string>
#include <variant>
namespace util::config {
std::optional<Error>
PortConstraint::checkTypeImpl(Value const& port) const
{
if (!(std::holds_alternative<int64_t>(port) || std::holds_alternative<std::string>(port)))
return Error{"Port must be a string or integer"};
return std::nullopt;
}
std::optional<Error>
PortConstraint::checkValueImpl(Value const& port) const
{
uint32_t p = 0;
if (std::holds_alternative<std::string>(port)) {
try {
p = static_cast<uint32_t>(std::stoi(std::get<std::string>(port)));
} catch (std::invalid_argument const& e) {
return Error{"Port string must be an integer."};
}
} else {
p = static_cast<uint32_t>(std::get<int64_t>(port));
}
if (p >= portMin && p <= portMax)
return std::nullopt;
return Error{"Port does not satisfy the constraint bounds"};
}
std::optional<Error>
ValidIPConstraint::checkTypeImpl(Value const& ip) const
{
if (!std::holds_alternative<std::string>(ip))
return Error{"Ip value must be a string"};
return std::nullopt;
}
std::optional<Error>
ValidIPConstraint::checkValueImpl(Value const& ip) const
{
if (std::get<std::string>(ip) == "localhost")
return std::nullopt;
static std::regex const ipv4(
R"(^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$)"
);
static std::regex const ip_url(
R"(^((http|https):\/\/)?((([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6})|(((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])))(:\d{1,5})?(\/[^\s]*)?$)"
);
if (std::regex_match(std::get<std::string>(ip), ipv4) || std::regex_match(std::get<std::string>(ip), ip_url))
return std::nullopt;
return Error{"Ip is not a valid ip address"};
}
std::optional<Error>
PositiveDouble::checkTypeImpl(Value const& num) const
{
if (!(std::holds_alternative<double>(num) || std::holds_alternative<int64_t>(num)))
return Error{"Double number must be of type int or double"};
return std::nullopt;
}
std::optional<Error>
PositiveDouble::checkValueImpl(Value const& num) const
{
if (std::get<double>(num) >= 0)
return std::nullopt;
return Error{"Double number must be greater than 0"};
}
} // namespace util::config

View File

@@ -0,0 +1,362 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "rpc/common/APIVersion.hpp"
#include "util/log/Logger.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/Types.hpp"
#include <fmt/core.h>
#include <fmt/format.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
namespace util::config {
class ValueView;
class ConfigValue;
/**
* @brief specific values that are accepted for logger levels in config.
*/
static constexpr std::array<char const*, 7> LOG_LEVELS = {
"trace",
"debug",
"info",
"warning",
"error",
"fatal",
"count",
};
/**
* @brief specific values that are accepted for logger tag style in config.
*/
static constexpr std::array<char const*, 5> LOG_TAGS = {
"int",
"uint",
"null",
"none",
"uuid",
};
/**
* @brief specific values that are accepted for cache loading in config.
*/
static constexpr std::array<char const*, 3> LOAD_CACHE_MODE = {
"sync",
"async",
"none",
};
/**
* @brief specific values that are accepted for database type in config.
*/
static constexpr std::array<char const*, 1> DATABASE_TYPE = {"cassandra"};
/**
* @brief An interface to enforce constraints on certain values within ClioConfigDefinition.
*/
class Constraint {
public:
constexpr virtual ~Constraint() noexcept = default;
/**
* @brief Check if the value meets the specific constraint.
*
* @param val The value to be checked
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]]
std::optional<Error>
checkConstraint(Value const& val) const
{
if (auto const maybeError = checkTypeImpl(val); maybeError.has_value())
return maybeError;
return checkValueImpl(val);
}
protected:
/**
* @brief Creates an error message for all constraints that must satisfy certain hard-coded values.
*
* @tparam arrSize, the size of the array of hardcoded values
* @param key The key to the value
* @param value The value the user provided
* @param arr The array with hard-coded values to add to error message
* @return The error message specifying what the value of key must be
*/
template <std::size_t arrSize>
constexpr std::string
makeErrorMsg(std::string_view key, Value const& value, std::array<char const*, arrSize> arr) const
{
// Extract the value from the variant
auto const valueStr = std::visit([](auto const& v) { return fmt::format("{}", v); }, value);
// Create the error message
return fmt::format(
R"(You provided value "{}". Key "{}"'s value must be one of the following: {})",
valueStr,
key,
fmt::join(arr, ", ")
);
}
/**
* @brief Check if the value is of a correct type for the constraint.
*
* @param val The value type to be checked
* @return An Error object if the constraint is not met, nullopt otherwise
*/
virtual std::optional<Error>
checkTypeImpl(Value const& val) const = 0;
/**
* @brief Check if the value is within the constraint.
*
* @param val The value type to be checked
* @return An Error object if the constraint is not met, nullopt otherwise
*/
virtual std::optional<Error>
checkValueImpl(Value const& val) const = 0;
};
/**
* @brief A constraint to ensure the port number is within a valid range.
*/
class PortConstraint final : public Constraint {
public:
constexpr ~PortConstraint() noexcept override = default;
private:
/**
* @brief Check if the type of the value is correct for this specific constraint.
*
* @param port The type to be checked
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkTypeImpl(Value const& port) const override;
/**
* @brief Check if the value is within the constraint.
*
* @param port The value to be checked
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkValueImpl(Value const& port) const override;
static constexpr uint32_t portMin = 1;
static constexpr uint32_t portMax = 65535;
};
/**
* @brief A constraint to ensure the IP address is valid.
*/
class ValidIPConstraint final : public Constraint {
public:
constexpr ~ValidIPConstraint() noexcept override = default;
private:
/**
* @brief Check if the type of the value is correct for this specific constraint.
*
* @param ip The type to be checked.
* @return An optional Error if the constraint is not met, std::nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkTypeImpl(Value const& ip) const override;
/**
* @brief Check if the value is within the constraint.
*
* @param ip The value to be checked
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkValueImpl(Value const& ip) const override;
};
/**
* @brief A constraint class to ensure the provided value is one of the specified values in an array.
*
* @tparam arrSize The size of the array containing the valid values for the constraint
*/
template <std::size_t arrSize>
class OneOf final : public Constraint {
public:
/**
* @brief Constructs a constraint where the value must be one of the values in the provided array.
*
* @param key The key of the ConfigValue that has this constraint
* @param arr The value that has this constraint must be of the values in arr
*/
constexpr OneOf(std::string_view key, std::array<char const*, arrSize> arr) : key_{key}, arr_{arr}
{
}
constexpr ~OneOf() noexcept override = default;
private:
/**
* @brief Check if the type of the value is correct for this specific constraint.
*
* @param val The type to be checked
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkTypeImpl(Value const& val) const override
{
if (!std::holds_alternative<std::string>(val))
return Error{fmt::format(R"(Key "{}"'s value must be a string)", key_)};
return std::nullopt;
}
/**
* @brief Check if the value matches one of the value in the provided array
*
* @param val The value to check
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkValueImpl(Value const& val) const override
{
namespace rg = std::ranges;
auto const check = [&val](std::string_view name) { return std::get<std::string>(val) == name; };
if (rg::any_of(arr_, check))
return std::nullopt;
return Error{makeErrorMsg(key_, val, arr_)};
}
std::string_view key_;
std::array<char const*, arrSize> arr_;
};
/**
* @brief A constraint class to ensure an integer value is between two numbers (inclusive)
*/
template <typename numType>
class NumberValueConstraint final : public Constraint {
public:
/**
* @brief Constructs a constraint where the number must be between min_ and max_.
*
* @param min the minimum number it can be to satisfy this constraint
* @param max the maximum number it can be to satisfy this constraint
*/
constexpr NumberValueConstraint(numType min, numType max) : min_{min}, max_{max}
{
}
constexpr ~NumberValueConstraint() noexcept override = default;
private:
/**
* @brief Check if the type of the value is correct for this specific constraint.
*
* @param num The type to be checked
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkTypeImpl(Value const& num) const override
{
if (!std::holds_alternative<int64_t>(num))
return Error{"Number must be of type integer"};
return std::nullopt;
}
/**
* @brief Check if the number is positive.
*
* @param num The number to check
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkValueImpl(Value const& num) const override
{
auto const numValue = std::get<int64_t>(num);
if (numValue >= static_cast<int64_t>(min_) && numValue <= static_cast<int64_t>(max_))
return std::nullopt;
return Error{fmt::format("Number must be between {} and {}", min_, max_)};
}
numType min_;
numType max_;
};
/**
* @brief A constraint to ensure a double number is positive
*/
class PositiveDouble final : public Constraint {
public:
constexpr ~PositiveDouble() noexcept override = default;
private:
/**
* @brief Check if the type of the value is correct for this specific constraint.
*
* @param num The type to be checked
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkTypeImpl(Value const& num) const override;
/**
* @brief Check if the number is positive.
*
* @param num The number to check
* @return An Error object if the constraint is not met, nullopt otherwise
*/
[[nodiscard]] std::optional<Error>
checkValueImpl(Value const& num) const override;
};
static constinit PortConstraint validatePort{};
static constinit ValidIPConstraint validateIP{};
static constinit OneOf validateChannelName{"channel", Logger::CHANNELS};
static constinit OneOf validateLogLevelName{"log_level", LOG_LEVELS};
static constinit OneOf validateCassandraName{"database.type", DATABASE_TYPE};
static constinit OneOf validateLoadMode{"cache.load", LOAD_CACHE_MODE};
static constinit OneOf validateLogTag{"log_tag_style", LOG_TAGS};
static constinit PositiveDouble validatePositiveDouble{};
static constinit NumberValueConstraint<uint16_t> validateUint16{
std::numeric_limits<uint16_t>::min(),
std::numeric_limits<uint16_t>::max()
};
static constinit NumberValueConstraint<uint32_t> validateUint32{
std::numeric_limits<uint32_t>::min(),
std::numeric_limits<uint32_t>::max()
};
static constinit NumberValueConstraint<uint32_t> validateApiVersion{rpc::API_VERSION_MIN, rpc::API_VERSION_MAX};
} // namespace util::config

View File

@@ -20,10 +20,15 @@
#include "util/newconfig/ConfigDefinition.hpp" #include "util/newconfig/ConfigDefinition.hpp"
#include "util/Assert.hpp" #include "util/Assert.hpp"
#include "util/OverloadSet.hpp"
#include "util/newconfig/Array.hpp" #include "util/newconfig/Array.hpp"
#include "util/newconfig/ArrayView.hpp" #include "util/newconfig/ArrayView.hpp"
#include "util/newconfig/ConfigConstraints.hpp"
#include "util/newconfig/ConfigFileInterface.hpp"
#include "util/newconfig/ConfigValue.hpp" #include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/ObjectView.hpp" #include "util/newconfig/ObjectView.hpp"
#include "util/newconfig/Types.hpp"
#include "util/newconfig/ValueView.hpp" #include "util/newconfig/ValueView.hpp"
#include <fmt/core.h> #include <fmt/core.h>
@@ -38,6 +43,7 @@
#include <thread> #include <thread>
#include <utility> #include <utility>
#include <variant> #include <variant>
#include <vector>
namespace util::config { namespace util::config {
/** /**
@@ -47,62 +53,76 @@ namespace util::config {
* without default values must be present in the user's config file. * without default values must be present in the user's config file.
*/ */
static ClioConfigDefinition ClioConfig = ClioConfigDefinition{ static ClioConfigDefinition ClioConfig = ClioConfigDefinition{
{{"database.type", ConfigValue{ConfigType::String}.defaultValue("cassandra")}, {{"database.type", ConfigValue{ConfigType::String}.defaultValue("cassandra").withConstraint(validateCassandraName)},
{"database.cassandra.contact_points", ConfigValue{ConfigType::String}.defaultValue("localhost")}, {"database.cassandra.contact_points", ConfigValue{ConfigType::String}.defaultValue("localhost")},
{"database.cassandra.port", ConfigValue{ConfigType::Integer}}, {"database.cassandra.port", ConfigValue{ConfigType::Integer}.withConstraint(validatePort)},
{"database.cassandra.keyspace", ConfigValue{ConfigType::String}.defaultValue("clio")}, {"database.cassandra.keyspace", ConfigValue{ConfigType::String}.defaultValue("clio")},
{"database.cassandra.replication_factor", ConfigValue{ConfigType::Integer}.defaultValue(3u)}, {"database.cassandra.replication_factor", ConfigValue{ConfigType::Integer}.defaultValue(3u)},
{"database.cassandra.table_prefix", ConfigValue{ConfigType::String}.defaultValue("table_prefix")}, {"database.cassandra.table_prefix", ConfigValue{ConfigType::String}.defaultValue("table_prefix")},
{"database.cassandra.max_write_requests_outstanding", ConfigValue{ConfigType::Integer}.defaultValue(10'000)}, {"database.cassandra.max_write_requests_outstanding",
{"database.cassandra.max_read_requests_outstanding", ConfigValue{ConfigType::Integer}.defaultValue(100'000)}, ConfigValue{ConfigType::Integer}.defaultValue(10'000).withConstraint(validateUint32)},
{"database.cassandra.max_read_requests_outstanding",
ConfigValue{ConfigType::Integer}.defaultValue(100'000).withConstraint(validateUint32)},
{"database.cassandra.threads", {"database.cassandra.threads",
ConfigValue{ConfigType::Integer}.defaultValue(static_cast<uint32_t>(std::thread::hardware_concurrency()))}, ConfigValue{ConfigType::Integer}
{"database.cassandra.core_connections_per_host", ConfigValue{ConfigType::Integer}.defaultValue(1)}, .defaultValue(static_cast<uint32_t>(std::thread::hardware_concurrency()))
{"database.cassandra.queue_size_io", ConfigValue{ConfigType::Integer}.optional()}, .withConstraint(validateUint32)},
{"database.cassandra.write_batch_size", ConfigValue{ConfigType::Integer}.defaultValue(20)}, {"database.cassandra.core_connections_per_host",
{"etl_source.[].ip", Array{ConfigValue{ConfigType::String}.optional()}}, ConfigValue{ConfigType::Integer}.defaultValue(1).withConstraint(validateUint16)},
{"etl_source.[].ws_port", Array{ConfigValue{ConfigType::String}.optional().min(1).max(65535)}}, {"database.cassandra.queue_size_io", ConfigValue{ConfigType::Integer}.optional().withConstraint(validateUint16)},
{"etl_source.[].grpc_port", Array{ConfigValue{ConfigType::String}.optional().min(1).max(65535)}}, {"database.cassandra.write_batch_size",
{"forwarding.cache_timeout", ConfigValue{ConfigType::Double}.defaultValue(0.0)}, ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(validateUint16)},
{"forwarding.request_timeout", ConfigValue{ConfigType::Double}.defaultValue(10.0)}, {"etl_source.[].ip", Array{ConfigValue{ConfigType::String}.withConstraint(validateIP)}},
{"etl_source.[].ws_port", Array{ConfigValue{ConfigType::String}.withConstraint(validatePort)}},
{"etl_source.[].grpc_port", Array{ConfigValue{ConfigType::String}.withConstraint(validatePort)}},
{"forwarding.cache_timeout",
ConfigValue{ConfigType::Double}.defaultValue(0.0).withConstraint(validatePositiveDouble)},
{"forwarding.request_timeout",
ConfigValue{ConfigType::Double}.defaultValue(10.0).withConstraint(validatePositiveDouble)},
{"dos_guard.whitelist.[]", Array{ConfigValue{ConfigType::String}}}, {"dos_guard.whitelist.[]", Array{ConfigValue{ConfigType::String}}},
{"dos_guard.max_fetches", ConfigValue{ConfigType::Integer}.defaultValue(1000'000)}, {"dos_guard.max_fetches", ConfigValue{ConfigType::Integer}.defaultValue(1000'000).withConstraint(validateUint32)},
{"dos_guard.max_connections", ConfigValue{ConfigType::Integer}.defaultValue(20)}, {"dos_guard.max_connections", ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(validateUint32)},
{"dos_guard.max_requests", ConfigValue{ConfigType::Integer}.defaultValue(20)}, {"dos_guard.max_requests", ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(validateUint32)},
{"dos_guard.sweep_interval", ConfigValue{ConfigType::Double}.defaultValue(1.0)}, {"dos_guard.sweep_interval",
{"cache.peers.[].ip", Array{ConfigValue{ConfigType::String}}}, ConfigValue{ConfigType::Double}.defaultValue(1.0).withConstraint(validatePositiveDouble)},
{"cache.peers.[].port", Array{ConfigValue{ConfigType::String}}}, {"cache.peers.[].ip", Array{ConfigValue{ConfigType::String}.withConstraint(validateIP)}},
{"server.ip", ConfigValue{ConfigType::String}}, {"cache.peers.[].port", Array{ConfigValue{ConfigType::String}.withConstraint(validatePort)}},
{"server.port", ConfigValue{ConfigType::Integer}}, {"server.ip", ConfigValue{ConfigType::String}.withConstraint(validateIP)},
{"server.max_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(0)}, {"server.port", ConfigValue{ConfigType::Integer}.withConstraint(validatePort)},
{"server.workers", ConfigValue{ConfigType::Integer}.withConstraint(validateUint32)},
{"server.max_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint32)},
{"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()}, {"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()},
{"server.admin_password", ConfigValue{ConfigType::String}.optional()},
{"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)}, {"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
{"prometheus.compress_reply", ConfigValue{ConfigType::Boolean}.defaultValue(true)}, {"prometheus.compress_reply", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
{"io_threads", ConfigValue{ConfigType::Integer}.defaultValue(2)}, {"io_threads", ConfigValue{ConfigType::Integer}.defaultValue(2).withConstraint(validateUint16)},
{"cache.num_diffs", ConfigValue{ConfigType::Integer}.defaultValue(32)}, {"cache.num_diffs", ConfigValue{ConfigType::Integer}.defaultValue(32).withConstraint(validateUint16)},
{"cache.num_markers", ConfigValue{ConfigType::Integer}.defaultValue(48)}, {"cache.num_markers", ConfigValue{ConfigType::Integer}.defaultValue(48).withConstraint(validateUint16)},
{"cache.num_cursors_from_diff", ConfigValue{ConfigType::Integer}.defaultValue(0)}, {"cache.num_cursors_from_diff", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint16)},
{"cache.num_cursors_from_account", ConfigValue{ConfigType::Integer}.defaultValue(0)}, {"cache.num_cursors_from_account", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint16)
{"cache.page_fetch_size", ConfigValue{ConfigType::Integer}.defaultValue(512)}, },
{"cache.load", ConfigValue{ConfigType::String}.defaultValue("async")}, {"cache.page_fetch_size", ConfigValue{ConfigType::Integer}.defaultValue(512).withConstraint(validateUint16)},
{"log_channels.[].channel", Array{ConfigValue{ConfigType::String}.optional()}}, {"cache.load", ConfigValue{ConfigType::String}.defaultValue("async").withConstraint(validateLoadMode)},
{"log_channels.[].log_level", Array{ConfigValue{ConfigType::String}.optional()}}, {"log_channels.[].channel", Array{ConfigValue{ConfigType::String}.optional().withConstraint(validateChannelName)}},
{"log_level", ConfigValue{ConfigType::String}.defaultValue("info")}, {"log_channels.[].log_level",
Array{ConfigValue{ConfigType::String}.optional().withConstraint(validateLogLevelName)}},
{"log_level", ConfigValue{ConfigType::String}.defaultValue("info").withConstraint(validateLogLevelName)},
{"log_format", {"log_format",
ConfigValue{ConfigType::String}.defaultValue( ConfigValue{ConfigType::String}.defaultValue(
R"(%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%)" R"(%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%)"
)}, )},
{"log_to_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)}, {"log_to_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
{"log_directory", ConfigValue{ConfigType::String}.optional()}, {"log_directory", ConfigValue{ConfigType::String}.optional()},
{"log_rotation_size", ConfigValue{ConfigType::Integer}.defaultValue(2048)}, {"log_rotation_size", ConfigValue{ConfigType::Integer}.defaultValue(2048u).withConstraint(validateUint32)},
{"log_directory_max_size", ConfigValue{ConfigType::Integer}.defaultValue(50 * 1024)}, {"log_directory_max_size",
{"log_rotation_hour_interval", ConfigValue{ConfigType::Integer}.defaultValue(12)}, ConfigValue{ConfigType::Integer}.defaultValue(50u * 1024u).withConstraint(validateUint32)},
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}, {"log_rotation_hour_interval", ConfigValue{ConfigType::Integer}.defaultValue(12).withConstraint(validateUint32)},
{"extractor_threads", ConfigValue{ConfigType::Integer}.defaultValue(2u)}, {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint").withConstraint(validateLogTag)},
{"extractor_threads", ConfigValue{ConfigType::Integer}.defaultValue(2u).withConstraint(validateUint32)},
{"read_only", ConfigValue{ConfigType::Boolean}.defaultValue(false)}, {"read_only", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
{"txn_threshold", ConfigValue{ConfigType::Integer}.defaultValue(0)}, {"txn_threshold", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint16)},
{"start_sequence", ConfigValue{ConfigType::String}.optional()}, {"start_sequence", ConfigValue{ConfigType::Integer}.optional().withConstraint(validateUint32)},
{"finish_sequence", ConfigValue{ConfigType::String}.optional()}, {"finish_sequence", ConfigValue{ConfigType::Integer}.optional().withConstraint(validateUint32)},
{"ssl_cert_file", ConfigValue{ConfigType::String}.optional()}, {"ssl_cert_file", ConfigValue{ConfigType::String}.optional()},
{"ssl_key_file", ConfigValue{ConfigType::String}.optional()}, {"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
{"api_version.min", ConfigValue{ConfigType::Integer}}, {"api_version.min", ConfigValue{ConfigType::Integer}},
@@ -113,7 +133,7 @@ ClioConfigDefinition::ClioConfigDefinition(std::initializer_list<KeyValuePair> p
{ {
for (auto const& [key, value] : pair) { for (auto const& [key, value] : pair) {
if (key.contains("[]")) if (key.contains("[]"))
ASSERT(std::holds_alternative<Array>(value), "Value must be array if key has \"[]\""); ASSERT(std::holds_alternative<Array>(value), R"(Value must be array if key has "[]")");
map_.insert({key, value}); map_.insert({key, value});
} }
} }
@@ -206,4 +226,51 @@ ClioConfigDefinition::arraySize(std::string_view prefix) const
std::unreachable(); std::unreachable();
} }
std::optional<std::vector<Error>>
ClioConfigDefinition::parse(ConfigFileInterface const& config)
{
std::vector<Error> listOfErrors;
for (auto& [key, value] : map_) {
// if key doesn't exist in user config, makes sure it is marked as ".optional()" or has ".defaultValue()"" in
// ClioConfigDefitinion above
if (!config.containsKey(key)) {
if (std::holds_alternative<ConfigValue>(value)) {
if (!(std::get<ConfigValue>(value).isOptional() || std::get<ConfigValue>(value).hasValue()))
listOfErrors.emplace_back(key, "key is required in user Config");
} else if (std::holds_alternative<Array>(value)) {
if (!(std::get<Array>(value).getArrayPattern().isOptional()))
listOfErrors.emplace_back(key, "key is required in user Config");
}
continue;
}
ASSERT(
std::holds_alternative<ConfigValue>(value) || std::holds_alternative<Array>(value),
"Value must be of type ConfigValue or Array"
);
std::visit(
util::OverloadSet{// handle the case where the config value is a single element.
// attempt to set the value from the configuration for the specified key.
[&key, &config, &listOfErrors](ConfigValue& val) {
if (auto const maybeError = val.setValue(config.getValue(key), key);
maybeError.has_value())
listOfErrors.emplace_back(maybeError.value());
},
// handle the case where the config value is an array.
// iterate over each provided value in the array and attempt to set it for the key.
[&key, &config, &listOfErrors](Array& arr) {
for (auto const& val : config.getArray(key)) {
if (auto const maybeError = arr.addValue(val, key); maybeError.has_value())
listOfErrors.emplace_back(maybeError.value());
}
}
},
value
);
}
if (!listOfErrors.empty())
return listOfErrors;
return std::nullopt;
}
} // namespace util::config } // namespace util::config

View File

@@ -24,7 +24,7 @@
#include "util/newconfig/ConfigDescription.hpp" #include "util/newconfig/ConfigDescription.hpp"
#include "util/newconfig/ConfigFileInterface.hpp" #include "util/newconfig/ConfigFileInterface.hpp"
#include "util/newconfig/ConfigValue.hpp" #include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/Errors.hpp" #include "util/newconfig/Error.hpp"
#include "util/newconfig/ObjectView.hpp" #include "util/newconfig/ObjectView.hpp"
#include "util/newconfig/ValueView.hpp" #include "util/newconfig/ValueView.hpp"
@@ -41,6 +41,7 @@
#include <unordered_map> #include <unordered_map>
#include <utility> #include <utility>
#include <variant> #include <variant>
#include <vector>
namespace util::config { namespace util::config {
@@ -66,12 +67,13 @@ public:
/** /**
* @brief Parses the configuration file * @brief Parses the configuration file
* *
* Should also check that no extra configuration key/value pairs are present * Also checks that no extra configuration key/value pairs are present. Adds to list of Errors
* if it does
* *
* @param config The configuration file interface * @param config The configuration file interface
* @return An optional Error object if parsing fails * @return An optional vector of Error objects stating all the failures if parsing fails
*/ */
[[nodiscard]] std::optional<Error> [[nodiscard]] std::optional<std::vector<Error>>
parse(ConfigFileInterface const& config); parse(ConfigFileInterface const& config);
/** /**
@@ -80,9 +82,9 @@ public:
* Should only check for valid values, without populating * Should only check for valid values, without populating
* *
* @param config The configuration file interface * @param config The configuration file interface
* @return An optional Error object if validation fails * @return An optional vector of Error objects stating all the failures if validation fails
*/ */
[[nodiscard]] std::optional<Error> [[nodiscard]] std::optional<std::vector<Error>>
validate(ConfigFileInterface const& config) const; validate(ConfigFileInterface const& config) const;
/** /**

View File

@@ -90,7 +90,9 @@ private:
KV{"server.ip", "IP address of the Clio HTTP server."}, KV{"server.ip", "IP address of the Clio HTTP server."},
KV{"server.port", "Port number of the Clio HTTP server."}, KV{"server.port", "Port number of the Clio HTTP server."},
KV{"server.max_queue_size", "Maximum size of the server's request queue."}, KV{"server.max_queue_size", "Maximum size of the server's request queue."},
KV{"server.workers", "Maximum number of threads for server to run with."},
KV{"server.local_admin", "Indicates if the server should run with admin privileges."}, KV{"server.local_admin", "Indicates if the server should run with admin privileges."},
KV{"server.admin_password", "Password for Clio admin-only APIs."},
KV{"prometheus.enabled", "Enable or disable Prometheus metrics."}, KV{"prometheus.enabled", "Enable or disable Prometheus metrics."},
KV{"prometheus.compress_reply", "Enable or disable compression of Prometheus responses."}, KV{"prometheus.compress_reply", "Enable or disable compression of Prometheus responses."},
KV{"io_threads", "Number of I/O threads."}, KV{"io_threads", "Number of I/O threads."},

View File

@@ -19,9 +19,8 @@
#pragma once #pragma once
#include "util/newconfig/ConfigValue.hpp" #include "util/newconfig/Types.hpp"
#include <optional>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
@@ -36,31 +35,33 @@ namespace util::config {
class ConfigFileInterface { class ConfigFileInterface {
public: public:
virtual ~ConfigFileInterface() = default; virtual ~ConfigFileInterface() = default;
/**
* @brief Parses the provided path of user clio configuration data
*
* @param filePath The path to the Clio Config data
*/
virtual void
parse(std::string_view filePath) = 0;
/** /**
* @brief Retrieves a configuration value. * @brief Retrieves the value of configValue.
* *
* @param key The key of the configuration value. * @param key The key of configuration.
* @return An optional containing the configuration value if found, otherwise std::nullopt. * @return the value assosiated with key.
*/ */
virtual std::optional<ConfigValue> virtual Value
getValue(std::string_view key) const = 0; getValue(std::string_view key) const = 0;
/** /**
* @brief Retrieves an array of configuration values. * @brief Retrieves an array of configuration values.
* *
* @param key The key of the configuration array. * @param key The key of the configuration array.
* @return An optional containing a vector of configuration values if found, otherwise std::nullopt. * @return A vector of configuration values if found, otherwise std::nullopt.
*/ */
virtual std::optional<std::vector<ConfigValue>> virtual std::vector<Value>
getArray(std::string_view key) const = 0; getArray(std::string_view key) const = 0;
/**
* @brief Checks if key exist in configuration file.
*
* @param key The key to search for.
* @return true if key exists in configuration file, false otherwise.
*/
virtual bool
containsKey(std::string_view key) const = 0;
}; };
} // namespace util::config } // namespace util::config

View File

@@ -0,0 +1,166 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/newconfig/ConfigFileJson.hpp"
#include "util/Assert.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/Types.hpp"
#include <boost/filesystem/path.hpp>
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/parse_options.hpp>
#include <boost/json/value.hpp>
#include <fmt/core.h>
#include <cstddef>
#include <exception>
#include <fstream>
#include <ios>
#include <iostream>
#include <ostream>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace util::config {
namespace {
/**
* @brief Extracts the value from a JSON object and converts it into the corresponding type.
*
* @param jsonValue The JSON value to extract.
* @return A variant containing the same type corresponding to the extracted value.
*/
[[nodiscard]] Value
extractJsonValue(boost::json::value const& jsonValue)
{
if (jsonValue.is_int64()) {
return jsonValue.as_int64();
}
if (jsonValue.is_string()) {
return jsonValue.as_string().c_str();
}
if (jsonValue.is_bool()) {
return jsonValue.as_bool();
}
if (jsonValue.is_double()) {
return jsonValue.as_double();
}
ASSERT(false, "Json is not of type int, string, bool or double");
std::unreachable();
}
} // namespace
ConfigFileJson::ConfigFileJson(boost::json::object jsonObj)
{
flattenJson(jsonObj, "");
}
std::expected<ConfigFileJson, Error>
ConfigFileJson::make_ConfigFileJson(boost::filesystem::path configFilePath)
{
try {
if (auto const in = std::ifstream(configFilePath.string(), std::ios::in | std::ios::binary); in) {
std::stringstream contents;
contents << in.rdbuf();
auto opts = boost::json::parse_options{};
opts.allow_comments = true;
auto const tempObj = boost::json::parse(contents.str(), {}, opts).as_object();
return ConfigFileJson{tempObj};
}
return std::unexpected<Error>(
Error{fmt::format("Could not open configuration file '{}'", configFilePath.string())}
);
} catch (std::exception const& e) {
return std::unexpected<Error>(Error{fmt::format(
"An error occurred while processing configuration file '{}': {}", configFilePath.string(), e.what()
)});
}
}
Value
ConfigFileJson::getValue(std::string_view key) const
{
auto const jsonValue = jsonObject_.at(key);
auto const value = extractJsonValue(jsonValue);
return value;
}
std::vector<Value>
ConfigFileJson::getArray(std::string_view key) const
{
ASSERT(jsonObject_.at(key).is_array(), "Key {} has value that is not an array", key);
std::vector<Value> configValues;
auto const arr = jsonObject_.at(key).as_array();
for (auto const& item : arr) {
auto const value = extractJsonValue(item);
configValues.emplace_back(value);
}
return configValues;
}
bool
ConfigFileJson::containsKey(std::string_view key) const
{
return jsonObject_.contains(key);
}
void
ConfigFileJson::flattenJson(boost::json::object const& obj, std::string const& prefix)
{
for (auto const& [key, value] : obj) {
std::string const fullKey = prefix.empty() ? std::string(key) : fmt::format("{}.{}", prefix, std::string(key));
// In ClioConfigDefinition, value must be a primitive or array
if (value.is_object()) {
flattenJson(value.as_object(), fullKey);
} else if (value.is_array()) {
auto const& arr = value.as_array();
for (std::size_t i = 0; i < arr.size(); ++i) {
std::string const arrayPrefix = fullKey + ".[]";
if (arr[i].is_object()) {
flattenJson(arr[i].as_object(), arrayPrefix);
} else {
jsonObject_[arrayPrefix] = arr;
}
}
} else {
// if "[]" is present in key, then value must be an array instead of primitive
if (fullKey.contains(".[]") && !jsonObject_.contains(fullKey)) {
boost::json::array newArray;
newArray.emplace_back(value);
jsonObject_[fullKey] = newArray;
} else if (fullKey.contains(".[]") && jsonObject_.contains(fullKey)) {
jsonObject_[fullKey].as_array().emplace_back(value);
} else {
jsonObject_[fullKey] = value;
}
}
}
}
} // namespace util::config

View File

@@ -0,0 +1,100 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/newconfig/ConfigFileInterface.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/Types.hpp"
#include <boost/filesystem/path.hpp>
#include <boost/json/object.hpp>
#include <expected>
#include <string>
#include <string_view>
#include <vector>
namespace util::config {
/** @brief Json representation of config */
class ConfigFileJson final : public ConfigFileInterface {
public:
/**
* @brief Construct a new ConfigJson object and stores the values from
* user's config into a json object.
*
* @param jsonObj the Json object to parse; represents user's config
*/
ConfigFileJson(boost::json::object jsonObj);
/**
* @brief Retrieves a configuration value by its key.
*
* @param key The key of the configuration value to retrieve.
* @return A variant containing the same type corresponding to the extracted value.
*/
[[nodiscard]] Value
getValue(std::string_view key) const override;
/**
* @brief Retrieves an array of configuration values by its key.
*
* @param key The key of the configuration array to retrieve.
* @return A vector of variants holding the config values specified by user.
*/
[[nodiscard]] std::vector<Value>
getArray(std::string_view key) const override;
/**
* @brief Checks if the configuration contains a specific key.
*
* @param key The key to check for.
* @return True if the key exists, false otherwise.
*/
[[nodiscard]] bool
containsKey(std::string_view key) const override;
/**
* @brief Creates a new ConfigFileJson by parsing the provided JSON file and
* stores the values in the object.
*
* @param configFilePath The path to the JSON file to be parsed.
* @return A ConfigFileJson object if parsing user file is successful. Error otherwise
*/
[[nodiscard]] static std::expected<ConfigFileJson, Error>
make_ConfigFileJson(boost::filesystem::path configFilePath);
private:
/**
* @brief Recursive function to flatten a JSON object into the same structure as the Clio Config.
*
* The keys will end up having the same naming convensions in Clio Config.
* Other than the keys specified in user Config file, no new keys are created.
*
* @param obj The JSON object to flatten.
* @param prefix The prefix to use for the keys in the flattened object.
*/
void
flattenJson(boost::json::object const& obj, std::string const& prefix);
boost::json::object jsonObject_;
};
} // namespace util::config

View File

@@ -0,0 +1,49 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/newconfig/ConfigFileInterface.hpp"
#include "util/newconfig/Types.hpp"
#include <boost/filesystem/path.hpp>
#include <string_view>
#include <vector>
// TODO: implement when we support yaml
namespace util::config {
/** @brief Yaml representation of config */
class ConfigFileYaml final : public ConfigFileInterface {
public:
ConfigFileYaml() = default;
Value
getValue(std::string_view key) const override;
std::vector<Value>
getArray(std::string_view key) const override;
bool
containsKey(std::string_view key) const override;
};
} // namespace util::config

View File

@@ -20,43 +20,23 @@
#pragma once #pragma once
#include "util/Assert.hpp" #include "util/Assert.hpp"
#include "util/UnsupportedType.hpp" #include "util/OverloadSet.hpp"
#include "util/newconfig/ConfigConstraints.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/Types.hpp"
#include <fmt/core.h>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <functional>
#include <optional> #include <optional>
#include <string> #include <string>
#include <type_traits> #include <string_view>
#include <variant> #include <variant>
namespace util::config { namespace util::config {
/** @brief Custom clio config types */
enum class ConfigType { Integer, String, Double, Boolean };
/**
* @brief Get the corresponding clio config type
*
* @tparam Type The type to get the corresponding ConfigType for
* @return The corresponding ConfigType
*/
template <typename Type>
constexpr ConfigType
getType()
{
if constexpr (std::is_same_v<Type, int64_t>) {
return ConfigType::Integer;
} else if constexpr (std::is_same_v<Type, std::string>) {
return ConfigType::String;
} else if constexpr (std::is_same_v<Type, double>) {
return ConfigType::Double;
} else if constexpr (std::is_same_v<Type, bool>) {
return ConfigType::Boolean;
} else {
static_assert(util::Unsupported<Type>, "Wrong config type");
}
}
/** /**
* @brief Represents the config values for Json/Yaml config * @brief Represents the config values for Json/Yaml config
* *
@@ -65,8 +45,6 @@ getType()
*/ */
class ConfigValue { class ConfigValue {
public: public:
using Type = std::variant<int64_t, std::string, bool, double>;
/** /**
* @brief Constructor initializing with the config type * @brief Constructor initializing with the config type
* *
@@ -83,12 +61,92 @@ public:
* @return Reference to this ConfigValue * @return Reference to this ConfigValue
*/ */
[[nodiscard]] ConfigValue& [[nodiscard]] ConfigValue&
defaultValue(Type value) defaultValue(Value value)
{ {
setValue(value); auto const err = checkTypeConsistency(type_, value);
ASSERT(!err.has_value(), "{}", err->error);
value_ = value;
return *this; return *this;
} }
/**
* @brief Sets the value current ConfigValue given by the User's defined value
*
* @param value The value to set
* @param key The Config key associated with the value. Optional to include; Used for debugging message to user.
* @return optional Error if user tries to set a value of wrong type or not within a constraint
*/
[[nodiscard]] std::optional<Error>
setValue(Value value, std::optional<std::string_view> key = std::nullopt)
{
auto err = checkTypeConsistency(type_, value);
if (err.has_value()) {
if (key.has_value())
err->error = fmt::format("{} {}", key.value(), err->error);
return err;
}
if (cons_.has_value()) {
auto constraintCheck = cons_->get().checkConstraint(value);
if (constraintCheck.has_value()) {
if (key.has_value())
constraintCheck->error = fmt::format("{} {}", key.value(), constraintCheck->error);
return constraintCheck;
}
}
value_ = value;
return std::nullopt;
}
/**
* @brief Assigns a constraint to the ConfigValue.
*
* This method associates a specific constraint with the ConfigValue.
* If the ConfigValue already holds a value, the method will check whether
* the value satisfies the given constraint. If the constraint is not satisfied,
* an assertion failure will occur with a detailed error message.
*
* @param cons The constraint to be applied to the ConfigValue.
* @return A reference to the modified ConfigValue object.
*/
[[nodiscard]] constexpr ConfigValue&
withConstraint(Constraint const& cons)
{
cons_ = std::reference_wrapper<Constraint const>(cons);
ASSERT(cons_.has_value(), "Constraint must be defined");
if (value_.has_value()) {
auto const& temp = cons_.value().get();
auto const& result = temp.checkConstraint(value_.value());
if (result.has_value()) {
// useful for specifying clear Error message
std::string type;
std::visit(
util::OverloadSet{
[&type](bool tmp) { type = fmt::format("bool {}", tmp); },
[&type](std::string const& tmp) { type = fmt::format("string {}", tmp); },
[&type](double tmp) { type = fmt::format("double {}", tmp); },
[&type](int64_t tmp) { type = fmt::format("int {}", tmp); }
},
value_.value()
);
ASSERT(false, "Value {} ConfigValue does not satisfy the set Constraint", type);
}
}
return *this;
}
/**
* @brief Retrieves the constraint associated with this ConfigValue, if any.
*
* @return An optional reference to the associated Constraint.
*/
[[nodiscard]] std::optional<std::reference_wrapper<Constraint const>>
getConstraint() const
{
return cons_;
}
/** /**
* @brief Gets the config type * @brief Gets the config type
* *
@@ -100,32 +158,6 @@ public:
return type_; return type_;
} }
/**
* @brief Sets the minimum value for the config
*
* @param min The minimum value
* @return Reference to this ConfigValue
*/
[[nodiscard]] constexpr ConfigValue&
min(std::uint32_t min)
{
min_ = min;
return *this;
}
/**
* @brief Sets the maximum value for the config
*
* @param max The maximum value
* @return Reference to this ConfigValue
*/
[[nodiscard]] constexpr ConfigValue&
max(std::uint32_t max)
{
max_ = max;
return *this;
}
/** /**
* @brief Sets the config value as optional, meaning the user doesn't have to provide the value in their config * @brief Sets the config value as optional, meaning the user doesn't have to provide the value in their config
* *
@@ -165,7 +197,7 @@ public:
* *
* @return Config Value * @return Config Value
*/ */
[[nodiscard]] Type const& [[nodiscard]] Value const&
getValue() const getValue() const
{ {
return value_.value(); return value_.value();
@@ -178,39 +210,28 @@ private:
* @param type The config type * @param type The config type
* @param value The config value * @param value The config value
*/ */
static void static std::optional<Error>
checkTypeConsistency(ConfigType type, Type value) checkTypeConsistency(ConfigType type, Value value)
{ {
if (std::holds_alternative<std::string>(value)) { if (type == ConfigType::String && !std::holds_alternative<std::string>(value)) {
ASSERT(type == ConfigType::String, "Value does not match type string"); return Error{"value does not match type string"};
} else if (std::holds_alternative<bool>(value)) {
ASSERT(type == ConfigType::Boolean, "Value does not match type boolean");
} else if (std::holds_alternative<double>(value)) {
ASSERT(type == ConfigType::Double, "Value does not match type double");
} else if (std::holds_alternative<int64_t>(value)) {
ASSERT(type == ConfigType::Integer, "Value does not match type integer");
} }
} if (type == ConfigType::Boolean && !std::holds_alternative<bool>(value)) {
return Error{"value does not match type boolean"};
/** }
* @brief Sets the value for the config if (type == ConfigType::Double && !std::holds_alternative<double>(value)) {
* return Error{"value does not match type double"};
* @param value The value to set }
* @return The value that was set if (type == ConfigType::Integer && !std::holds_alternative<int64_t>(value)) {
*/ return Error{"value does not match type integer"};
Type }
setValue(Type value) return std::nullopt;
{
checkTypeConsistency(type_, value);
value_ = value;
return value;
} }
ConfigType type_{}; ConfigType type_{};
bool optional_{false}; bool optional_{false};
std::optional<Type> value_; std::optional<Value> value_;
std::optional<std::uint32_t> min_; std::optional<std::reference_wrapper<Constraint const>> cons_;
std::optional<std::uint32_t> max_;
}; };
} // namespace util::config } // namespace util::config

View File

@@ -0,0 +1,57 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <fmt/core.h>
#include <string>
#include <string_view>
#include <utility>
namespace util::config {
/** @brief Displays the different errors when parsing user config */
struct Error {
/**
* @brief Constructs an Error with a custom error message.
*
* @param err the error message to display to users.
*/
Error(std::string err) : error{std::move(err)}
{
}
/**
* @brief Constructs an Error with a custom error message.
*
* @param key the key associated with the error.
* @param err the error message to display to users.
*/
Error(std::string_view key, std::string_view err)
: error{
fmt::format("{} {}", key, err),
}
{
}
std::string error;
};
} // namespace util::config

View File

@@ -0,0 +1,60 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/UnsupportedType.hpp"
#include <cstdint>
#include <string>
#include <type_traits>
#include <variant>
namespace util::config {
/** @brief Custom clio config types */
enum class ConfigType { Integer, String, Double, Boolean };
/** @brief Represents the supported Config Values */
using Value = std::variant<int64_t, std::string, bool, double>;
/**
* @brief Get the corresponding clio config type
*
* @tparam Type The type to get the corresponding ConfigType for
* @return The corresponding ConfigType
*/
template <typename Type>
constexpr ConfigType
getType()
{
if constexpr (std::is_same_v<Type, int64_t>) {
return ConfigType::Integer;
} else if constexpr (std::is_same_v<Type, std::string>) {
return ConfigType::String;
} else if constexpr (std::is_same_v<Type, double>) {
return ConfigType::Double;
} else if constexpr (std::is_same_v<Type, bool>) {
return ConfigType::Boolean;
} else {
static_assert(util::Unsupported<Type>, "Wrong config type");
}
}
} // namespace util::config

View File

@@ -21,6 +21,7 @@
#include "util/Assert.hpp" #include "util/Assert.hpp"
#include "util/newconfig/ConfigValue.hpp" #include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/Types.hpp"
#include <cstdint> #include <cstdint>
#include <string> #include <string>
@@ -55,9 +56,9 @@ double
ValueView::asDouble() const ValueView::asDouble() const
{ {
if (configVal_.get().hasValue()) { if (configVal_.get().hasValue()) {
if (type() == ConfigType::Double) { if (type() == ConfigType::Double)
return std::get<double>(configVal_.get().getValue()); return std::get<double>(configVal_.get().getValue());
}
if (type() == ConfigType::Integer) if (type() == ConfigType::Integer)
return static_cast<double>(std::get<int64_t>(configVal_.get().getValue())); return static_cast<double>(std::get<int64_t>(configVal_.get().getValue()));
} }

View File

@@ -21,6 +21,7 @@
#include "util/Assert.hpp" #include "util/Assert.hpp"
#include "util/newconfig/ConfigValue.hpp" #include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/Types.hpp"
#include <fmt/core.h> #include <fmt/core.h>
@@ -84,7 +85,7 @@ public:
return static_cast<T>(val); return static_cast<T>(val);
} }
} }
ASSERT(false, "Value view is not of any Int type"); ASSERT(false, "Value view is not of Int type");
return 0; return 0;
} }

View File

@@ -19,6 +19,7 @@
#pragma once #pragma once
#include "util/WithTimeout.hpp"
#include "util/requests/Types.hpp" #include "util/requests/Types.hpp"
#include "util/requests/WsConnection.hpp" #include "util/requests/WsConnection.hpp"
@@ -39,8 +40,10 @@
#include <boost/beast/websocket/stream_base.hpp> #include <boost/beast/websocket/stream_base.hpp>
#include <boost/system/errc.hpp> #include <boost/system/errc.hpp>
#include <atomic>
#include <chrono> #include <chrono>
#include <expected> #include <expected>
#include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -65,15 +68,13 @@ public:
auto operation = [&](auto&& token) { ws_.async_read(buffer, token); }; auto operation = [&](auto&& token) { ws_.async_read(buffer, token); };
if (timeout) { if (timeout) {
withTimeout(operation, yield[errorCode], *timeout); errorCode = util::withTimeout(operation, yield[errorCode], *timeout);
} else { } else {
operation(yield[errorCode]); operation(yield[errorCode]);
} }
if (errorCode) { if (errorCode)
errorCode = mapError(errorCode);
return std::unexpected{RequestError{"Read error", errorCode}}; return std::unexpected{RequestError{"Read error", errorCode}};
}
return boost::beast::buffers_to_string(std::move(buffer).data()); return boost::beast::buffers_to_string(std::move(buffer).data());
} }
@@ -88,15 +89,13 @@ public:
boost::beast::error_code errorCode; boost::beast::error_code errorCode;
auto operation = [&](auto&& token) { ws_.async_write(boost::asio::buffer(message), token); }; auto operation = [&](auto&& token) { ws_.async_write(boost::asio::buffer(message), token); };
if (timeout) { if (timeout) {
withTimeout(operation, yield[errorCode], *timeout); errorCode = util::withTimeout(operation, yield, *timeout);
} else { } else {
operation(yield[errorCode]); operation(yield[errorCode]);
} }
if (errorCode) { if (errorCode)
errorCode = mapError(errorCode);
return RequestError{"Write error", errorCode}; return RequestError{"Write error", errorCode};
}
return std::nullopt; return std::nullopt;
} }
@@ -117,31 +116,6 @@ public:
return RequestError{"Close error", errorCode}; return RequestError{"Close error", errorCode};
return std::nullopt; return std::nullopt;
} }
private:
template <typename Operation>
static void
withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::asio::cancellation_signal cancellationSignal;
auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield);
boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout};
timer.async_wait([&cancellationSignal](boost::system::error_code errorCode) {
if (!errorCode)
cancellationSignal.emit(boost::asio::cancellation_type::terminal);
});
operation(cyield);
}
static boost::system::error_code
mapError(boost::system::error_code const ec)
{
if (ec == boost::system::errc::operation_canceled) {
return boost::system::errc::make_error_code(boost::system::errc::timed_out);
}
return ec;
}
}; };
using PlainWsConnection = WsConnectionImpl<boost::beast::websocket::stream<boost::beast::tcp_stream>>; using PlainWsConnection = WsConnectionImpl<boost::beast::websocket::stream<boost::beast::tcp_stream>>;

View File

@@ -3,13 +3,17 @@ add_library(clio_web)
target_sources( target_sources(
clio_web clio_web
PRIVATE Resolver.cpp PRIVATE Resolver.cpp
Server.cpp
dosguard/DOSGuard.cpp dosguard/DOSGuard.cpp
dosguard/IntervalSweepHandler.cpp dosguard/IntervalSweepHandler.cpp
dosguard/WhitelistHandler.cpp dosguard/WhitelistHandler.cpp
impl/AdminVerificationStrategy.cpp impl/AdminVerificationStrategy.cpp
impl/ServerSslContext.cpp ng/Connection.cpp
ng/impl/ConnectionHandler.cpp
ng/impl/ServerSslContext.cpp
ng/impl/WsConnection.cpp
ng/Server.cpp ng/Server.cpp
ng/Request.cpp
ng/Response.cpp
) )
target_link_libraries(clio_web PUBLIC clio_util) target_link_libraries(clio_web PUBLIC clio_util)

View File

@@ -30,6 +30,7 @@
#include <boost/beast/core/flat_buffer.hpp> #include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/tcp_stream.hpp> #include <boost/beast/core/tcp_stream.hpp>
#include <cstdint>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -52,6 +53,7 @@ class HttpSession : public impl::HttpBase<HttpSession, HandlerType>,
public std::enable_shared_from_this<HttpSession<HandlerType>> { public std::enable_shared_from_this<HttpSession<HandlerType>> {
boost::beast::tcp_stream stream_; boost::beast::tcp_stream stream_;
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_; std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
std::uint32_t maxWsSendingQueueSize_;
public: public:
/** /**
@@ -64,6 +66,7 @@ public:
* @param dosGuard The denial of service guard to use * @param dosGuard The denial of service guard to use
* @param handler The server handler to use * @param handler The server handler to use
* @param buffer Buffer with initial data received from the peer * @param buffer Buffer with initial data received from the peer
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/ */
explicit HttpSession( explicit HttpSession(
tcp::socket&& socket, tcp::socket&& socket,
@@ -72,7 +75,8 @@ public:
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory, std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard, std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
std::shared_ptr<HandlerType> const& handler, std::shared_ptr<HandlerType> const& handler,
boost::beast::flat_buffer buffer boost::beast::flat_buffer buffer,
std::uint32_t maxWsSendingQueueSize
) )
: impl::HttpBase<HttpSession, HandlerType>( : impl::HttpBase<HttpSession, HandlerType>(
ip, ip,
@@ -84,6 +88,7 @@ public:
) )
, stream_(std::move(socket)) , stream_(std::move(socket))
, tagFactory_(tagFactory) , tagFactory_(tagFactory)
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
{ {
} }
@@ -128,7 +133,8 @@ public:
this->handler_, this->handler_,
std::move(this->buffer_), std::move(this->buffer_),
std::move(this->req_), std::move(this->req_),
ConnectionBase::isAdmin() ConnectionBase::isAdmin(),
maxWsSendingQueueSize_
) )
->run(); ->run();
} }

View File

@@ -62,7 +62,8 @@ public:
* @param dosGuard The denial of service guard to use * @param dosGuard The denial of service guard to use
* @param handler The server handler to use * @param handler The server handler to use
* @param buffer Buffer with initial data received from the peer * @param buffer Buffer with initial data received from the peer
* @param isAdmin Whether the connection has admin privileges * @param isAdmin Whether the connection has admin privileges,
* @param maxSendingQueueSize The maximum size of the sending queue for websocket
*/ */
explicit PlainWsSession( explicit PlainWsSession(
boost::asio::ip::tcp::socket&& socket, boost::asio::ip::tcp::socket&& socket,
@@ -71,9 +72,17 @@ public:
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard, std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
std::shared_ptr<HandlerType> const& handler, std::shared_ptr<HandlerType> const& handler,
boost::beast::flat_buffer&& buffer, boost::beast::flat_buffer&& buffer,
bool isAdmin bool isAdmin,
std::uint32_t maxSendingQueueSize
) )
: impl::WsBase<PlainWsSession, HandlerType>(ip, tagFactory, dosGuard, handler, std::move(buffer)) : impl::WsBase<PlainWsSession, HandlerType>(
ip,
tagFactory,
dosGuard,
handler,
std::move(buffer),
maxSendingQueueSize
)
, ws_(std::move(socket)) , ws_(std::move(socket))
{ {
ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer) ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer)
@@ -107,6 +116,7 @@ class WsUpgrader : public std::enable_shared_from_this<WsUpgrader<HandlerType>>
std::string ip_; std::string ip_;
std::shared_ptr<HandlerType> const handler_; std::shared_ptr<HandlerType> const handler_;
bool isAdmin_; bool isAdmin_;
std::uint32_t maxWsSendingQueueSize_;
public: public:
/** /**
@@ -120,6 +130,7 @@ public:
* @param buffer Buffer with initial data received from the peer. Ownership is transferred * @param buffer Buffer with initial data received from the peer. Ownership is transferred
* @param request The request. Ownership is transferred * @param request The request. Ownership is transferred
* @param isAdmin Whether the connection has admin privileges * @param isAdmin Whether the connection has admin privileges
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/ */
WsUpgrader( WsUpgrader(
boost::beast::tcp_stream&& stream, boost::beast::tcp_stream&& stream,
@@ -129,7 +140,8 @@ public:
std::shared_ptr<HandlerType> const& handler, std::shared_ptr<HandlerType> const& handler,
boost::beast::flat_buffer&& buffer, boost::beast::flat_buffer&& buffer,
http::request<http::string_body> request, http::request<http::string_body> request,
bool isAdmin bool isAdmin,
std::uint32_t maxWsSendingQueueSize
) )
: http_(std::move(stream)) : http_(std::move(stream))
, buffer_(std::move(buffer)) , buffer_(std::move(buffer))
@@ -139,6 +151,7 @@ public:
, ip_(std::move(ip)) , ip_(std::move(ip))
, handler_(handler) , handler_(handler)
, isAdmin_(isAdmin) , isAdmin_(isAdmin)
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
{ {
} }
@@ -175,7 +188,14 @@ private:
boost::beast::get_lowest_layer(http_).expires_never(); boost::beast::get_lowest_layer(http_).expires_never();
std::make_shared<PlainWsSession<HandlerType>>( std::make_shared<PlainWsSession<HandlerType>>(
http_.release_socket(), ip_, tagFactory_, dosGuard_, handler_, std::move(buffer_), isAdmin_ http_.release_socket(),
ip_,
tagFactory_,
dosGuard_,
handler_,
std::move(buffer_),
isAdmin_,
maxWsSendingQueueSize_
) )
->run(std::move(req_)); ->run(std::move(req_));
} }

View File

@@ -1,48 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "web/Server.hpp"
#include "util/config/Config.hpp"
#include <boost/asio/ssl/context.hpp>
#include <optional>
#include <string>
namespace web {
std::expected<std::optional<boost::asio::ssl::context>, std::string>
makeServerSslContext(util::Config const& config)
{
bool const configHasCertFile = config.contains("ssl_cert_file");
bool const configHasKeyFile = config.contains("ssl_key_file");
if (configHasCertFile != configHasKeyFile)
return std::unexpected{"Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together."};
if (not configHasCertFile)
return std::nullopt;
auto const certFilename = config.value<std::string>("ssl_cert_file");
auto const keyFilename = config.value<std::string>("ssl_key_file");
return impl::makeServerSslContext(certFilename, keyFilename);
}
} // namespace web

View File

@@ -24,8 +24,8 @@
#include "web/HttpSession.hpp" #include "web/HttpSession.hpp"
#include "web/SslHttpSession.hpp" #include "web/SslHttpSession.hpp"
#include "web/dosguard/DOSGuardInterface.hpp" #include "web/dosguard/DOSGuardInterface.hpp"
#include "web/impl/ServerSslContext.hpp"
#include "web/interface/Concepts.hpp" #include "web/interface/Concepts.hpp"
#include "web/ng/impl/ServerSslContext.hpp"
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address.hpp> #include <boost/asio/ip/address.hpp>
@@ -41,6 +41,7 @@
#include <fmt/core.h> #include <fmt/core.h>
#include <chrono> #include <chrono>
#include <cstdint>
#include <exception> #include <exception>
#include <functional> #include <functional>
#include <memory> #include <memory>
@@ -59,15 +60,6 @@
*/ */
namespace web { namespace web {
/**
* @brief A helper function to create a server SSL context.
*
* @param config The config to create the context
* @return Optional SSL context or error message if any
*/
std::expected<std::optional<boost::asio::ssl::context>, std::string>
makeServerSslContext(util::Config const& config);
/** /**
* @brief The Detector class to detect if the connection is a ssl or not. * @brief The Detector class to detect if the connection is a ssl or not.
* *
@@ -79,10 +71,8 @@ makeServerSslContext(util::Config const& config);
* @tparam HandlerType The executor to handle the requests * @tparam HandlerType The executor to handle the requests
*/ */
template < template <
template <typename> template <typename> class PlainSessionType,
class PlainSessionType, template <typename> class SslSessionType,
template <typename>
class SslSessionType,
SomeServerHandler HandlerType> SomeServerHandler HandlerType>
class Detector : public std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>> { class Detector : public std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>> {
using std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this; using std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this;
@@ -95,6 +85,7 @@ class Detector : public std::enable_shared_from_this<Detector<PlainSessionType,
std::shared_ptr<HandlerType> const handler_; std::shared_ptr<HandlerType> const handler_;
boost::beast::flat_buffer buffer_; boost::beast::flat_buffer buffer_;
std::shared_ptr<impl::AdminVerificationStrategy> const adminVerification_; std::shared_ptr<impl::AdminVerificationStrategy> const adminVerification_;
std::uint32_t maxWsSendingQueueSize_;
public: public:
/** /**
@@ -106,6 +97,7 @@ public:
* @param dosGuard The denial of service guard to use * @param dosGuard The denial of service guard to use
* @param handler The server handler to use * @param handler The server handler to use
* @param adminVerification The admin verification strategy to use * @param adminVerification The admin verification strategy to use
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/ */
Detector( Detector(
tcp::socket&& socket, tcp::socket&& socket,
@@ -113,7 +105,8 @@ public:
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory, std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard, std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
std::shared_ptr<HandlerType> handler, std::shared_ptr<HandlerType> handler,
std::shared_ptr<impl::AdminVerificationStrategy> adminVerification std::shared_ptr<impl::AdminVerificationStrategy> adminVerification,
std::uint32_t maxWsSendingQueueSize
) )
: stream_(std::move(socket)) : stream_(std::move(socket))
, ctx_(ctx) , ctx_(ctx)
@@ -121,6 +114,7 @@ public:
, dosGuard_(dosGuard) , dosGuard_(dosGuard)
, handler_(std::move(handler)) , handler_(std::move(handler))
, adminVerification_(std::move(adminVerification)) , adminVerification_(std::move(adminVerification))
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
{ {
} }
@@ -178,14 +172,22 @@ public:
tagFactory_, tagFactory_,
dosGuard_, dosGuard_,
handler_, handler_,
std::move(buffer_) std::move(buffer_),
maxWsSendingQueueSize_
) )
->run(); ->run();
return; return;
} }
std::make_shared<PlainSessionType<HandlerType>>( std::make_shared<PlainSessionType<HandlerType>>(
stream_.release_socket(), ip, adminVerification_, tagFactory_, dosGuard_, handler_, std::move(buffer_) stream_.release_socket(),
ip,
adminVerification_,
tagFactory_,
dosGuard_,
handler_,
std::move(buffer_),
maxWsSendingQueueSize_
) )
->run(); ->run();
} }
@@ -201,10 +203,8 @@ public:
* @tparam HandlerType The handler to process the request and return response. * @tparam HandlerType The handler to process the request and return response.
*/ */
template < template <
template <typename> template <typename> class PlainSessionType,
class PlainSessionType, template <typename> class SslSessionType,
template <typename>
class SslSessionType,
SomeServerHandler HandlerType> SomeServerHandler HandlerType>
class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>> { class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>> {
using std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this; using std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this;
@@ -217,6 +217,7 @@ class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslS
std::shared_ptr<HandlerType> handler_; std::shared_ptr<HandlerType> handler_;
tcp::acceptor acceptor_; tcp::acceptor acceptor_;
std::shared_ptr<impl::AdminVerificationStrategy> adminVerification_; std::shared_ptr<impl::AdminVerificationStrategy> adminVerification_;
std::uint32_t maxWsSendingQueueSize_;
public: public:
/** /**
@@ -229,6 +230,7 @@ public:
* @param dosGuard The denial of service guard to use * @param dosGuard The denial of service guard to use
* @param handler The server handler to use * @param handler The server handler to use
* @param adminPassword The optional password to verify admin role in requests * @param adminPassword The optional password to verify admin role in requests
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/ */
Server( Server(
boost::asio::io_context& ioc, boost::asio::io_context& ioc,
@@ -237,7 +239,8 @@ public:
util::TagDecoratorFactory tagFactory, util::TagDecoratorFactory tagFactory,
dosguard::DOSGuardInterface& dosGuard, dosguard::DOSGuardInterface& dosGuard,
std::shared_ptr<HandlerType> handler, std::shared_ptr<HandlerType> handler,
std::optional<std::string> adminPassword std::optional<std::string> adminPassword,
std::uint32_t maxWsSendingQueueSize
) )
: ioc_(std::ref(ioc)) : ioc_(std::ref(ioc))
, ctx_(std::move(ctx)) , ctx_(std::move(ctx))
@@ -246,6 +249,7 @@ public:
, handler_(std::move(handler)) , handler_(std::move(handler))
, acceptor_(boost::asio::make_strand(ioc)) , acceptor_(boost::asio::make_strand(ioc))
, adminVerification_(impl::make_AdminVerificationStrategy(std::move(adminPassword))) , adminVerification_(impl::make_AdminVerificationStrategy(std::move(adminPassword)))
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
{ {
boost::beast::error_code ec; boost::beast::error_code ec;
@@ -299,7 +303,13 @@ private:
ctx_ ? std::optional<std::reference_wrapper<boost::asio::ssl::context>>{ctx_.value()} : std::nullopt; ctx_ ? std::optional<std::reference_wrapper<boost::asio::ssl::context>>{ctx_.value()} : std::nullopt;
std::make_shared<Detector<PlainSessionType, SslSessionType, HandlerType>>( std::make_shared<Detector<PlainSessionType, SslSessionType, HandlerType>>(
std::move(socket), ctxRef, std::cref(tagFactory_), dosGuard_, handler_, adminVerification_ std::move(socket),
ctxRef,
std::cref(tagFactory_),
dosGuard_,
handler_,
adminVerification_,
maxWsSendingQueueSize_
) )
->run(); ->run();
} }
@@ -333,7 +343,7 @@ make_HttpServer(
{ {
static util::Logger const log{"WebServer"}; static util::Logger const log{"WebServer"};
auto expectedSslContext = makeServerSslContext(config); auto expectedSslContext = ng::impl::makeServerSslContext(config);
if (not expectedSslContext) { if (not expectedSslContext) {
LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error(); LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error();
return nullptr; return nullptr;
@@ -361,6 +371,10 @@ make_HttpServer(
throw std::logic_error("Admin config error, one method must be specified to authorize admin."); throw std::logic_error("Admin config error, one method must be specified to authorize admin.");
} }
// If the transactions number is 200 per ledger, A client which subscribes everything will send 400+ feeds for
// each ledger. we allow user delay 3 ledgers by default
auto const maxWsSendingQueueSize = serverConfig.valueOr("ws_max_sending_queue_size", 1500);
auto server = std::make_shared<HttpServer<HandlerType>>( auto server = std::make_shared<HttpServer<HandlerType>>(
ioc, ioc,
std::move(expectedSslContext).value(), std::move(expectedSslContext).value(),
@@ -368,7 +382,8 @@ make_HttpServer(
util::TagDecoratorFactory(config), util::TagDecoratorFactory(config),
dosGuard, dosGuard,
handler, handler,
std::move(adminPassword) std::move(adminPassword),
maxWsSendingQueueSize
); );
server->run(); server->run();

View File

@@ -37,6 +37,7 @@
#include <chrono> #include <chrono>
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -59,6 +60,7 @@ class SslHttpSession : public impl::HttpBase<SslHttpSession, HandlerType>,
public std::enable_shared_from_this<SslHttpSession<HandlerType>> { public std::enable_shared_from_this<SslHttpSession<HandlerType>> {
boost::beast::ssl_stream<boost::beast::tcp_stream> stream_; boost::beast::ssl_stream<boost::beast::tcp_stream> stream_;
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_; std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
std::uint32_t maxWsSendingQueueSize_;
public: public:
/** /**
@@ -72,6 +74,7 @@ public:
* @param dosGuard The denial of service guard to use * @param dosGuard The denial of service guard to use
* @param handler The server handler to use * @param handler The server handler to use
* @param buffer Buffer with initial data received from the peer * @param buffer Buffer with initial data received from the peer
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/ */
explicit SslHttpSession( explicit SslHttpSession(
tcp::socket&& socket, tcp::socket&& socket,
@@ -81,7 +84,8 @@ public:
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory, std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard, std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
std::shared_ptr<HandlerType> const& handler, std::shared_ptr<HandlerType> const& handler,
boost::beast::flat_buffer buffer boost::beast::flat_buffer buffer,
std::uint32_t maxWsSendingQueueSize
) )
: impl::HttpBase<SslHttpSession, HandlerType>( : impl::HttpBase<SslHttpSession, HandlerType>(
ip, ip,
@@ -93,6 +97,7 @@ public:
) )
, stream_(std::move(socket), ctx) , stream_(std::move(socket), ctx)
, tagFactory_(tagFactory) , tagFactory_(tagFactory)
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
{ {
} }
@@ -173,7 +178,8 @@ public:
this->handler_, this->handler_,
std::move(this->buffer_), std::move(this->buffer_),
std::move(this->req_), std::move(this->req_),
ConnectionBase::isAdmin() ConnectionBase::isAdmin(),
maxWsSendingQueueSize_
) )
->run(); ->run();
} }

View File

@@ -36,6 +36,7 @@
#include <boost/optional/optional.hpp> #include <boost/optional/optional.hpp>
#include <chrono> #include <chrono>
#include <cstdint>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -64,6 +65,7 @@ public:
* @param handler The server handler to use * @param handler The server handler to use
* @param buffer Buffer with initial data received from the peer * @param buffer Buffer with initial data received from the peer
* @param isAdmin Whether the connection has admin privileges * @param isAdmin Whether the connection has admin privileges
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/ */
explicit SslWsSession( explicit SslWsSession(
boost::beast::ssl_stream<boost::beast::tcp_stream>&& stream, boost::beast::ssl_stream<boost::beast::tcp_stream>&& stream,
@@ -72,9 +74,17 @@ public:
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard, std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
std::shared_ptr<HandlerType> const& handler, std::shared_ptr<HandlerType> const& handler,
boost::beast::flat_buffer&& buffer, boost::beast::flat_buffer&& buffer,
bool isAdmin bool isAdmin,
std::uint32_t maxWsSendingQueueSize
) )
: impl::WsBase<SslWsSession, HandlerType>(ip, tagFactory, dosGuard, handler, std::move(buffer)) : impl::WsBase<SslWsSession, HandlerType>(
ip,
tagFactory,
dosGuard,
handler,
std::move(buffer),
maxWsSendingQueueSize
)
, ws_(std::move(stream)) , ws_(std::move(stream))
{ {
ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer) ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer)
@@ -106,6 +116,7 @@ class SslWsUpgrader : public std::enable_shared_from_this<SslWsUpgrader<HandlerT
std::shared_ptr<HandlerType> const handler_; std::shared_ptr<HandlerType> const handler_;
http::request<http::string_body> req_; http::request<http::string_body> req_;
bool isAdmin_; bool isAdmin_;
std::uint32_t maxWsSendingQueueSize_;
public: public:
/** /**
@@ -119,6 +130,7 @@ public:
* @param buffer Buffer with initial data received from the peer. Ownership is transferred * @param buffer Buffer with initial data received from the peer. Ownership is transferred
* @param request The request. Ownership is transferred * @param request The request. Ownership is transferred
* @param isAdmin Whether the connection has admin privileges * @param isAdmin Whether the connection has admin privileges
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/ */
SslWsUpgrader( SslWsUpgrader(
boost::beast::ssl_stream<boost::beast::tcp_stream> stream, boost::beast::ssl_stream<boost::beast::tcp_stream> stream,
@@ -128,7 +140,8 @@ public:
std::shared_ptr<HandlerType> handler, std::shared_ptr<HandlerType> handler,
boost::beast::flat_buffer&& buffer, boost::beast::flat_buffer&& buffer,
http::request<http::string_body> request, http::request<http::string_body> request,
bool isAdmin bool isAdmin,
std::uint32_t maxWsSendingQueueSize
) )
: https_(std::move(stream)) : https_(std::move(stream))
, buffer_(std::move(buffer)) , buffer_(std::move(buffer))
@@ -138,6 +151,7 @@ public:
, handler_(std::move(handler)) , handler_(std::move(handler))
, req_(std::move(request)) , req_(std::move(request))
, isAdmin_(isAdmin) , isAdmin_(isAdmin)
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
{ {
} }
@@ -179,7 +193,14 @@ private:
boost::beast::get_lowest_layer(https_).expires_never(); boost::beast::get_lowest_layer(https_).expires_never();
std::make_shared<SslWsSession<HandlerType>>( std::make_shared<SslWsSession<HandlerType>>(
std::move(https_), ip_, tagFactory_, dosGuard_, handler_, std::move(buffer_), isAdmin_ std::move(https_),
ip_,
tagFactory_,
dosGuard_,
handler_,
std::move(buffer_),
isAdmin_,
maxWsSendingQueueSize_
) )
->run(std::move(req_)); ->run(std::move(req_));
} }

View File

@@ -20,6 +20,7 @@
#include "web/impl/AdminVerificationStrategy.hpp" #include "web/impl/AdminVerificationStrategy.hpp"
#include "util/JsonUtils.hpp" #include "util/JsonUtils.hpp"
#include "util/config/Config.hpp"
#include <boost/beast/http/field.hpp> #include <boost/beast/http/field.hpp>
#include <xrpl/basics/base_uint.h> #include <xrpl/basics/base_uint.h>
@@ -79,4 +80,20 @@ make_AdminVerificationStrategy(std::optional<std::string> password)
return std::make_shared<IPAdminVerificationStrategy>(); return std::make_shared<IPAdminVerificationStrategy>();
} }
std::expected<std::shared_ptr<AdminVerificationStrategy>, std::string>
make_AdminVerificationStrategy(util::Config const& serverConfig)
{
auto adminPassword = serverConfig.maybeValue<std::string>("admin_password");
auto const localAdmin = serverConfig.maybeValue<bool>("local_admin");
bool const localAdminEnabled = localAdmin && localAdmin.value();
if (localAdminEnabled == adminPassword.has_value()) {
if (adminPassword.has_value())
return std::unexpected{"Admin config error, local_admin and admin_password can not be set together."};
return std::unexpected{"Admin config error, either local_admin and admin_password must be specified."};
}
return make_AdminVerificationStrategy(std::move(adminPassword));
}
} // namespace web::impl } // namespace web::impl

Some files were not shown because too many files have changed in this diff Show More