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"
version=$($formatter --version | grep -o '[0-9\.]*')
if [[ "18.0.0" > "$version" ]]; then
if [[ "19.0.0" > "$version" ]]; then
cat <<EOF
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.
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
# Check some things if we're pushing a branch called "release/"
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 "Making sure you've signed and tagged it."
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 }}
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
uses: ./.github/actions/save_cache
with:
@@ -219,11 +212,6 @@ jobs:
with:
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
run: |
chmod +x ./clio_tests

View File

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

View File

@@ -60,7 +60,7 @@ jobs:
shell: bash
id: run_clang_tidy
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
if: ${{ steps.run_clang_tidy.outcome != 'success' }}
@@ -99,7 +99,7 @@ jobs:
- name: Create PR with fixes
if: ${{ steps.run_clang_tidy.outcome != 'success' }}
uses: peter-evans/create-pull-request@v6
uses: peter-evans/create-pull-request@v7
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}

View File

@@ -71,12 +71,6 @@ jobs:
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}
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
shell: bash
run: |
@@ -130,11 +124,6 @@ jobs:
with:
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
run: |
chmod +x ./clio_tests

View File

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

View File

@@ -21,7 +21,7 @@ git config --local core.hooksPath .githooks
```
## 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.
`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.
@@ -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.
## 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.
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 ()
message(STATUS "Using clang-tidy from CLIO_CLANG_TIDY_BIN")
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 ()
if (NOT _CLANG_TIDY_BIN)

View File

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

View File

@@ -7,7 +7,7 @@ USER root
WORKDIR /root
ENV CCACHE_VERSION=4.10.2 \
LLVM_TOOLS_VERSION=18 \
LLVM_TOOLS_VERSION=19 \
GH_VERSION=2.40.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
"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": {
// Comma-separated list of IPs to exclude from rate limiting
"whitelist": [
@@ -67,7 +70,14 @@
"admin_password": "xrp",
// 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
"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.
"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
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:
@@ -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:
```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(data)
add_subdirectory(etl)
add_subdirectory(etlng)
add_subdirectory(feed)
add_subdirectory(rpc)
add_subdirectory(web)

View File

@@ -1,4 +1,4 @@
add_library(clio_app)
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_);
// Manages clients subscribed to streams
auto subscriptionsRunner = feed::SubscriptionManagerRunner(config_, backend);
auto const subscriptions = subscriptionsRunner.getManager();
auto subscriptions = feed::SubscriptionManager::make_SubscriptionManager(config_, backend);
// Tracks which ledgers have been validated by the network
auto ledgers = etl::NetworkValidatedLedgers::make_ValidatedLedgers();
@@ -123,12 +121,14 @@ ClioApplication::run()
auto const handlerProvider = std::make_shared<rpc::impl::ProductionHandlerProvider const>(
config_, backend, subscriptions, balancer, etl, amendmentCenter, counters
);
using RPCEngineType = rpc::RPCEngine<etl::LoadBalancer, rpc::Counters>;
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
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);
// Blocks until stopped.

View File

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

View File

@@ -364,6 +364,25 @@ public:
boost::asio::yield_context yield
) 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.
*
@@ -617,6 +636,14 @@ public:
virtual void
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.
*

View File

@@ -547,6 +547,45 @@ public:
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>
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
const override
@@ -905,6 +944,17 @@ public:
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
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.
*

View File

@@ -233,6 +233,14 @@ struct NFTsAndCursor {
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.
*/

View File

@@ -257,6 +257,19 @@ public:
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;
}();
@@ -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]() {
return handle_.get().prepare(fmt::format(
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]() {
return handle_.get().prepare(fmt::format(
R"(

View File

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

View File

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

View File

@@ -27,6 +27,7 @@
#include "rpc/Errors.hpp"
#include "util/Assert.hpp"
#include "util/Random.hpp"
#include "util/ResponseExpirationCache.hpp"
#include "util/log/Logger.hpp"
#include <boost/asio/io_context.hpp>
@@ -34,6 +35,7 @@
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <fmt/core.h>
#include <algorithm>
@@ -79,7 +81,10 @@ LoadBalancer::LoadBalancer(
{
auto const forwardingCacheTimeout = config.valueOr<float>("forwarding.cache_timeout", 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;
@@ -111,10 +116,13 @@ LoadBalancer::LoadBalancer(
validatedLedgers,
forwardingTimeout,
[this]() {
if (not hasForwardingSource_)
if (not hasForwardingSource_.lock().get())
chooseForwardingSource();
},
[this](bool wasForwarding) {
if (wasForwarding)
chooseForwardingSource();
},
[this]() { chooseForwardingSource(); },
[this]() {
if (forwardingCache_.has_value())
forwardingCache_->invalidate();
@@ -221,8 +229,12 @@ LoadBalancer::forwardToRippled(
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 (auto cachedResponse = forwardingCache_->get(request); cachedResponse) {
if (auto cachedResponse = forwardingCache_->get(cmd); cachedResponse) {
return std::move(cachedResponse).value();
}
}
@@ -250,7 +262,7 @@ LoadBalancer::forwardToRippled(
if (response) {
if (forwardingCache_ and not response->contains("error"))
forwardingCache_->put(request, *response);
forwardingCache_->put(cmd, *response);
return std::move(response).value();
}
@@ -322,11 +334,13 @@ LoadBalancer::getETLState() noexcept
void
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_) {
if (not hasForwardingSource_ and source->isConnected()) {
if (not hasForwardingSourceLock.get() and source->isConnected()) {
source->setForwarding(true);
hasForwardingSource_ = true;
hasForwardingSourceLock.get() = true;
} else {
source->setForwarding(false);
}

View File

@@ -23,9 +23,10 @@
#include "etl/ETLState.hpp"
#include "etl/NetworkValidatedLedgersInterface.hpp"
#include "etl/Source.hpp"
#include "etl/impl/ForwardingCache.hpp"
#include "feed/SubscriptionManagerInterface.hpp"
#include "rpc/Errors.hpp"
#include "util/Mutex.hpp"
#include "util/ResponseExpirationCache.hpp"
#include "util/config/Config.hpp"
#include "util/log/Logger.hpp"
@@ -39,7 +40,6 @@
#include <org/xrpl/rpc/v1/ledger.pb.h>
#include <xrpl/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <expected>
@@ -69,14 +69,17 @@ private:
util::Logger log_{"ETL"};
// 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::vector<SourcePtr> sources_;
std::optional<ETLState> etlState_;
std::uint32_t downloadRanges_ =
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:
/**

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 {
public:
using OnConnectHook = std::function<void()>;
using OnDisconnectHook = std::function<void()>;
using OnDisconnectHook = std::function<void(bool)>;
using OnLedgerClosedHook = std::function<void()>;
virtual ~SourceBase() = default;

View File

@@ -22,6 +22,7 @@
#include "data/BackendInterface.hpp"
#include "data/Types.hpp"
#include "etl/ETLHelpers.hpp"
#include "etl/MPTHelpers.hpp"
#include "etl/NFTHelpers.hpp"
#include "util/Assert.hpp"
#include "util/log/Logger.hpp"
@@ -154,6 +155,11 @@ public:
backend.writeSuccessor(std::move(lastKey_), request_.ledger().sequence(), std::string{obj.key()});
lastKey_ = obj.key();
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(
std::move(*obj.mutable_key()), request_.ledger().sequence(), std::move(*obj.mutable_data())
);

View File

@@ -47,7 +47,7 @@
namespace etl::impl {
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 {
boost::asio::io_context ctx;

View File

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

View File

@@ -24,6 +24,8 @@
#include "rpc/JS.hpp"
#include "util/Retry.hpp"
#include "util/log/Logger.hpp"
#include "util/prometheus/Label.hpp"
#include "util/prometheus/Prometheus.hpp"
#include "util/requests/Types.hpp"
#include <boost/algorithm/string/classification.hpp>
@@ -66,22 +68,28 @@ SubscriptionSource::SubscriptionSource(
OnConnectHook onConnect,
OnDisconnectHook onDisconnect,
OnLedgerClosedHook onLedgerClosed,
std::chrono::steady_clock::duration const connectionTimeout,
std::chrono::steady_clock::duration const wsTimeout,
std::chrono::steady_clock::duration const retryDelay
)
: log_(fmt::format("GrpcSource[{}:{}]", ip, wsPort))
: log_(fmt::format("SubscriptionSource[{}:{}]", ip, wsPort))
, wsConnectionBuilder_(ip, wsPort)
, validatedLedgers_(std::move(validatedLedgers))
, subscriptions_(std::move(subscriptions))
, strand_(boost::asio::make_strand(ioContext))
, wsTimeout_(wsTimeout)
, retry_(util::makeRetryExponentialBackoff(retryDelay, RETRY_MAX_DELAY, strand_))
, onConnect_(std::move(onConnect))
, onDisconnect_(std::move(onDisconnect))
, 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"})
.addHeader({"X-User", "clio-client"})
.setConnectionTimeout(connectionTimeout);
.setConnectionTimeout(wsTimeout_);
}
SubscriptionSource::~SubscriptionSource()
@@ -133,6 +141,7 @@ void
SubscriptionSource::setForwarding(bool isForwarding)
{
isForwarding_ = isForwarding;
LOG(log_.info()) << "Forwarding set to " << isForwarding_;
}
std::chrono::steady_clock::time_point
@@ -166,20 +175,22 @@ SubscriptionSource::subscribe()
}
wsConnection_ = std::move(connection).value();
isConnected_ = true;
onConnect_();
auto const& subscribeCommand = getSubscribeCommandJson();
auto const writeErrorOpt = wsConnection_->write(subscribeCommand, yield);
auto const writeErrorOpt = wsConnection_->write(subscribeCommand, yield, wsTimeout_);
if (writeErrorOpt) {
handleError(writeErrorOpt.value(), yield);
return;
}
isConnected_ = true;
LOG(log_.info()) << "Connected";
onConnect_();
retry_.reset();
while (!stop_) {
auto const message = wsConnection_->read(yield);
auto const message = wsConnection_->read(yield, wsTimeout_);
if (not message) {
handleError(message.error(), yield);
return;
@@ -224,10 +235,11 @@ SubscriptionSource::handleMessage(std::string const& message)
auto validatedLedgers = boost::json::value_to<std::string>(result.at(JS(validated_ledgers)));
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) {
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))) {
ledgerIndex = object.at(JS(ledger_index)).as_int64();
}
@@ -245,10 +257,13 @@ SubscriptionSource::handleMessage(std::string const& message)
// 2 - Validated transaction
// Only forward proposed transaction, validated transactions are sent by Clio itself
if (object.contains(JS(transaction)) and !object.contains(JS(meta))) {
LOG(log_.debug()) << "Forwarding proposed transaction: " << object;
subscriptions_->forwardProposedTransaction(object);
} else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ValidationReceived) {
LOG(log_.debug()) << "Forwarding validation: " << object;
subscriptions_->forwardValidation(object);
} else if (object.contains(JS(type)) && object.at(JS(type)) == JS_ManifestReceived) {
LOG(log_.debug()) << "Forwarding manifest: " << object;
subscriptions_->forwardManifest(object);
}
}
@@ -270,16 +285,14 @@ void
SubscriptionSource::handleError(util::requests::RequestError const& error, boost::asio::yield_context yield)
{
isConnected_ = false;
isForwarding_ = false;
bool const wasForwarding = isForwarding_.exchange(false);
if (not stop_) {
onDisconnect_();
LOG(log_.info()) << "Disconnected";
onDisconnect_(wasForwarding);
}
if (wsConnection_ != nullptr) {
auto const err = wsConnection_->close(yield);
if (err) {
LOG(log_.error()) << "Error closing websocket connection: " << err->message();
}
wsConnection_->close(yield);
wsConnection_.reset();
}
@@ -306,7 +319,11 @@ SubscriptionSource::logError(util::requests::RequestError const& error) const
void
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

View File

@@ -25,6 +25,7 @@
#include "util/Mutex.hpp"
#include "util/Retry.hpp"
#include "util/log/Logger.hpp"
#include "util/prometheus/Gauge.hpp"
#include "util/requests/Types.hpp"
#include "util/requests/WsConnection.hpp"
@@ -37,6 +38,7 @@
#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include <future>
#include <memory>
#include <optional>
@@ -71,6 +73,8 @@ private:
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
std::chrono::steady_clock::duration wsTimeout_;
util::Retry retry_;
OnConnectHook onConnect_;
@@ -83,9 +87,11 @@ private:
util::Mutex<std::chrono::steady_clock::time_point> lastMessageTime_;
std::reference_wrapper<util::prometheus::GaugeInt> lastMessageTimeSecondsSinceEpoch_;
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_DELAY{1};
@@ -103,7 +109,7 @@ public:
* @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
* 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
*/
SubscriptionSource(
@@ -115,7 +121,7 @@ public:
OnConnectHook onConnect,
OnDisconnectHook onDisconnect,
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
);

View File

@@ -213,6 +213,7 @@ private:
backend_->writeAccountTransactions(std::move(insertTxResultOp->accountTxData));
backend_->writeNFTs(insertTxResultOp->nfTokensData);
backend_->writeNFTTransactions(insertTxResultOp->nfTokenTxData);
backend_->writeMPTHolders(insertTxResultOp->mptHoldersData);
auto [success, duration] =
::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 "util/async/AnyExecutionContext.hpp"
#include "util/async/context/BasicExecutionContext.hpp"
#include "util/config/Config.hpp"
#include "util/log/Logger.hpp"
#include <boost/asio/executor_work_guard.hpp>
@@ -44,6 +45,7 @@
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
/**
@@ -67,16 +69,36 @@ class SubscriptionManager : public SubscriptionManagerInterface {
impl::ProposedTransactionFeed proposedTransactionFeed_;
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
*
* @param executor The executor to use to publish the feeds
* @param backend The backend to use
*/
template <class ExecutorCtx>
SubscriptionManager(ExecutorCtx& executor, std::shared_ptr<data::BackendInterface const> const& backend)
SubscriptionManager(
util::async::AnyExecutionContext&& executor,
std::shared_ptr<data::BackendInterface const> const& backend
)
: backend_(backend)
, ctx_(executor)
, ctx_(std::move(executor))
, manifestFeed_(ctx_, "manifest")
, validationsFeed_(ctx_, "validations")
, ledgerFeed_(ctx_)
@@ -291,41 +313,4 @@ public:
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

View File

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

View File

@@ -6,6 +6,7 @@ target_sources(
Factories.cpp
AMMHelpers.cpp
RPCHelpers.cpp
CredentialHelpers.cpp
Counters.cpp
WorkQueue.cpp
common/Specs.cpp
@@ -33,6 +34,7 @@ target_sources(
handlers/LedgerEntry.cpp
handlers/LedgerIndex.cpp
handlers/LedgerRange.cpp
handlers/MPTHolders.cpp
handlers/NFTsByIssuer.cpp
handlers/NFTBuyOffers.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::rpcFIELD_NOT_FOUND_TRANSACTION, "fieldNotFoundTransaction", "Missing field."},
{ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID, "malformedDocumentID", "Malformed oracle_document_id."},
{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
"malformedAuthorizedCredentials",
"Malformed authorized credentials."},
// special system errors
{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."},

View File

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

View File

@@ -20,19 +20,22 @@
#pragma once
#include "data/BackendInterface.hpp"
#include "rpc/Counters.hpp"
#include "rpc/Errors.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/WorkQueue.hpp"
#include "rpc/common/HandlerProvider.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/impl/ForwardingProxy.hpp"
#include "util/ResponseExpirationCache.hpp"
#include "util/log/Logger.hpp"
#include "web/Context.hpp"
#include "web/dosguard/DOSGuardInterface.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/iterator/transform_iterator.hpp>
#include <boost/json.hpp>
#include <fmt/core.h>
#include <fmt/format.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <chrono>
@@ -41,14 +44,9 @@
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <utility>
// forward declarations
namespace etl {
class LoadBalancer;
class ETLService;
} // namespace etl
/**
* @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.
*/
template <typename LoadBalancerType, typename CountersType>
class RPCEngine {
util::Logger perfLog_{"Performance"};
util::Logger log_{"RPC"};
@@ -64,16 +63,19 @@ class RPCEngine {
std::shared_ptr<BackendInterface> backend_;
std::reference_wrapper<web::dosguard::DOSGuardInterface const> dosGuard_;
std::reference_wrapper<WorkQueue> workQueue_;
std::reference_wrapper<Counters> counters_;
std::reference_wrapper<CountersType> counters_;
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:
/**
* @brief Construct a new RPCEngine object
*
* @param config The config to use
* @param backend The backend to use
* @param balancer The load balancer to use
* @param dosGuard The DOS guard to use
@@ -82,11 +84,12 @@ public:
* @param handlerProvider The handler provider to use
*/
RPCEngine(
util::Config const& config,
std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<etl::LoadBalancer> const& balancer,
std::shared_ptr<LoadBalancerType> const& balancer,
web::dosguard::DOSGuardInterface const& dosGuard,
WorkQueue& workQueue,
Counters& counters,
CountersType& counters,
std::shared_ptr<HandlerProvider const> const& handlerProvider
)
: backend_{backend}
@@ -96,11 +99,22 @@ public:
, handlerProvider_{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.
*
* @param config The config to use
* @param backend The backend to use
* @param balancer The load balancer to use
* @param dosGuard The DOS guard to use
@@ -111,15 +125,16 @@ public:
*/
static std::shared_ptr<RPCEngine>
make_RPCEngine(
util::Config const& config,
std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<etl::LoadBalancer> const& balancer,
std::shared_ptr<LoadBalancerType> const& balancer,
web::dosguard::DOSGuardInterface const& dosGuard,
WorkQueue& workQueue,
Counters& counters,
CountersType& counters,
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
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);
}
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()) {
LOG(log_.error()) << "Database is too busy. Rejecting request";
@@ -154,8 +179,11 @@ public:
LOG(perfLog_.debug()) << ctx.tag() << " finish executing rpc `" << ctx.method << '`';
if (not v)
if (not v) {
notifyErrored(ctx.method);
} else if (not ctx.isAdmin and responseCache_) {
responseCache_->put(ctx.method, v.result->as_object());
}
return Result{std::move(v)};
} catch (data::DatabaseTimeout const& t) {

View File

@@ -36,6 +36,7 @@
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/string.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
@@ -49,6 +50,7 @@
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h>
@@ -79,6 +81,7 @@
#include <algorithm>
#include <array>
#include <cassert>
#include <chrono>
#include <cstddef>
#include <cstdint>
@@ -257,6 +260,7 @@ toExpandedJson(
auto metaJson = toJson(*meta);
insertDeliveredAmount(metaJson, txn, meta, blobs.date);
insertDeliverMaxAlias(txnJson, apiVersion);
insertMPTIssuanceID(metaJson, txn, meta);
if (nftEnabled == NFTokenjson::ENABLE) {
Json::Value nftJson;
@@ -312,6 +316,67 @@ insertDeliveredAmount(
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
insertDeliverMaxAlias(boost::json::object& txJson, std::uint32_t const apiVersion)
{
@@ -428,8 +493,9 @@ ledgerHeaderFromRequest(std::shared_ptr<data::BackendInterface const> const& bac
} else {
ledgerSequence = parseStringAsUInt(stringIndex);
}
} else if (indexValue.is_int64())
} else if (indexValue.is_int64()) {
ledgerSequence = indexValue.as_int64();
}
} else {
ledgerSequence = ctx.range.maxSequence;
}
@@ -947,7 +1013,8 @@ accountHolds(
auto const blob = backend.fetchLedgerObject(key, sequence, yield);
if (!blob) {
amount.clear({currency, issuer});
amount.setIssue(ripple::Issue(currency, issuer));
amount.clear();
return amount;
}
@@ -955,7 +1022,8 @@ accountHolds(
ripple::SLE const sle{it, key};
if (zeroIfFrozen && isFrozen(backend, sequence, account, currency, issuer, yield)) {
amount.clear(ripple::Issue(currency, issuer));
amount.setIssue(ripple::Issue(currency, issuer));
amount.clear();
} else {
amount = sle.getFieldAmount(ripple::sfBalance);
if (account > issuer) {
@@ -1273,6 +1341,31 @@ specifiesCurrentOrClosedLedger(boost::json::object const& request)
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>
getNFTID(boost::json::object const& request)
{

View File

@@ -191,6 +191,21 @@ insertDeliveredAmount(
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
*
@@ -557,6 +572,16 @@ parseIssue(boost::json::object const& issue);
bool
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
*

View File

@@ -34,16 +34,13 @@ static constexpr uint32_t API_VERSION_DEFAULT = 1u;
/**
* @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;
/**
* @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

View File

@@ -29,8 +29,10 @@
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <fmt/core.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/UintTypes.h>
#include <charconv>
@@ -89,16 +91,19 @@ checkIsU32Numeric(std::string_view sv)
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{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_string())
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{};
return makeHexStringValidator<ripple::uint256>(value, key);
}};
CustomValidator CustomValidators::LedgerIndexValidator =
@@ -200,15 +205,13 @@ CustomValidator CustomValidators::SubscribeStreamValidator =
"ledger", "transactions", "transactions_proposed", "book_changes", "manifests", "validations"
};
static std::unordered_set<std::string> const reportingNotSupportStreams = {
"peer_status", "consensus", "server"
};
static std::unordered_set<std::string> const notSupportStreams = {"peer_status", "consensus", "server"};
for (auto const& v : value.as_array()) {
if (!v.is_string())
return Error{Status{RippledError::rpcINVALID_PARAMS, "streamNotString"}};
if (reportingNotSupportStreams.contains(boost::json::value_to<std::string>(v)))
return Error{Status{RippledError::rpcREPORTING_UNSUPPORTED}};
if (notSupportStreams.contains(boost::json::value_to<std::string>(v)))
return Error{Status{RippledError::rpcNOT_SUPPORTED}};
if (not validStreams.contains(boost::json::value_to<std::string>(v)))
return Error{Status{RippledError::rpcSTREAM_MALFORMED}};
@@ -252,4 +255,79 @@ CustomValidator CustomValidators::CurrencyIssueValidator =
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

View File

@@ -27,15 +27,13 @@
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <fmt/core.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <concepts>
#include <cstdint>
#include <ctime>
#include <functional>
#include <initializer_list>
#include <iomanip>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
@@ -153,7 +151,7 @@ struct Type final {
verify(boost::json::value const& value, std::string_view key) const
{
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 convertible = (checkType<Types>(res) || ...);
@@ -458,6 +456,21 @@ public:
[[nodiscard]] bool
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
*/
@@ -492,6 +505,22 @@ struct CustomValidators final {
*/
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.
*
@@ -528,6 +557,51 @@ struct CustomValidators final {
* Used by amm_info.
*/
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

View File

@@ -60,10 +60,6 @@ public:
if (ctx.method == "subscribe" || ctx.method == "unsubscribe")
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))
return false;
@@ -73,6 +69,9 @@ public:
if (specifiesCurrentOrClosedLedger(request))
return true;
if (isForcedForward(ctx))
return true;
auto const checkAccountInfoForward = [&]() {
return ctx.method == "account_info" and request.contains("queue") and request.at("queue").is_bool() and
request.at("queue").as_bool();
@@ -142,6 +141,14 @@ private:
{
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

View File

@@ -45,6 +45,7 @@
#include "rpc/handlers/LedgerEntry.hpp"
#include "rpc/handlers/LedgerIndex.hpp"
#include "rpc/handlers/LedgerRange.hpp"
#include "rpc/handlers/MPTHolders.hpp"
#include "rpc/handlers/NFTBuyOffers.hpp"
#include "rpc/handlers/NFTHistory.hpp"
#include "rpc/handlers/NFTInfo.hpp"
@@ -97,6 +98,7 @@ ProductionHandlerProvider::ProductionHandlerProvider(
{"ledger_entry", {LedgerEntryHandler{backend}}},
{"ledger_index", {LedgerIndexHandler{backend}, true}}, // clio only
{"ledger_range", {LedgerRangeHandler{backend}}},
{"mpt_holders", {MPTHoldersHandler{backend}, true}}, // clio only
{"nfts_by_issuer", {NFTsByIssuerHandler{backend}, true}}, // clio only
{"nft_history", {NFTHistoryHandler{backend}, true}}, // clio only
{"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;
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;
}
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;
while (page) {

View File

@@ -55,10 +55,6 @@ class AccountObjectsHandler {
// dependencies
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:
static auto constexpr LIMIT_MIN = 10;
static auto constexpr LIMIT_MAX = 400;

View File

@@ -19,25 +19,32 @@
#include "rpc/handlers/DepositAuthorized.hpp"
#include "rpc/CredentialHelpers.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/common/Types.hpp"
#include "util/Assert.hpp"
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <memory>
#include <string>
#include <utility>
#include <variant>
namespace rpc {
@@ -71,26 +78,55 @@ DepositAuthorizedHandler::process(DepositAuthorizedHandler::Input input, Context
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.destinationAccount = input.destinationAccount;
response.ledgerHash = ripple::strHex(lgrInfo.hash);
response.ledgerIndex = lgrInfo.seq;
// If the two accounts are the same, then the deposit should be fine.
if (sourceAccountID != destinationAccountID) {
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);
}
}
response.depositAuthorized = depositAuthorized;
if (credentialsPresent)
response.credentials = input.credentials.value();
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;
}
@@ -127,8 +167,10 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, DepositAuthorize
{JS(destination_account), output.destinationAccount},
{JS(ledger_hash), output.ledgerHash},
{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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@
#include "rpc/handlers/LedgerEntry.hpp"
#include "rpc/CredentialHelpers.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
@@ -30,6 +31,8 @@
#include <boost/json/object.hpp>
#include <boost/json/value.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/strHex.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>(
boost::json::value_to<std::string>(input.depositPreauth->at(JS(owner)))
);
// Only one of authorize or authorized_credentials MUST exist;
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."}
};
}
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) {
auto const id =
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) {
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 {
// Must specify 1 of the following fields to indicate what type
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_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID},
{JS(oracle), ripple::ltORACLE},
{JS(credential), ripple::ltCREDENTIAL},
{JS(mptoken), ripple::ltMPTOKEN},
};
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;
};
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 =
std::find_if(indexFieldTypeMap.begin(), indexFieldTypeMap.end(), [&jsonObject](auto const& 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)));
} else if (jsonObject.contains(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
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))) {
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"))

View File

@@ -30,6 +30,7 @@
#include "rpc/common/Validators.hpp"
#include "util/AccountUtils.hpp"
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
@@ -91,6 +92,8 @@ public:
std::optional<std::string> accountRoot;
// account id to address did object
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
std::optional<boost::json::object> directory;
std::optional<boost::json::object> offer;
@@ -99,11 +102,13 @@ public:
std::optional<boost::json::object> depositPreauth;
std::optional<boost::json::object> ticket;
std::optional<boost::json::object> amm;
std::optional<boost::json::object> mptoken;
std::optional<ripple::STXChainBridge> bridge;
std::optional<std::string> bridgeAccount;
std::optional<uint32_t> chainClaimId;
std::optional<uint32_t> createAccountClaimId;
std::optional<ripple::uint256> oracleNode;
std::optional<ripple::uint256> credential;
bool includeDeleted = false;
};
@@ -194,7 +199,8 @@ public:
meta::WithCustomError{
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),
@@ -315,6 +321,59 @@ public:
},
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{}},
{"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
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/tokens.h>
#include <cctype>

View File

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

View File

@@ -19,6 +19,8 @@
#pragma once
#include <algorithm>
#include <array>
#include <type_traits>
namespace util {
@@ -29,4 +31,19 @@ namespace util {
template <typename 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

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(oracle), ripple::ltORACLE),
LedgerTypeAttribute::AccountOwnedLedgerType(JS(credential), ripple::ltCREDENTIAL),
LedgerTypeAttribute::ChainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL),
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mpt_issuance), ripple::ltMPTOKEN_ISSUANCE),
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mptoken), ripple::ltMPTOKEN),
};
public:

View File

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

View File

@@ -30,12 +30,16 @@
#include <unordered_map>
#include <unordered_set>
namespace etl::impl {
namespace util {
/**
* @brief Cache of requests' responses with TTL support and configurable cachable commands
*/
class ResponseExpirationCache {
/**
* @brief A class to store a cache entry.
*/
class CacheEntry {
class Entry {
std::chrono::steady_clock::time_point lastUpdated_;
std::optional<boost::json::object> response_;
@@ -71,49 +75,47 @@ public:
invalidate();
};
/**
* @brief A class to store a cache of forwarding responses
*/
class ForwardingCache {
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:
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 cmds The commands that should be cached
*/
ForwardingCache(std::chrono::steady_clock::duration cacheTimeout);
/**
* @brief Check if a request should be cached
*
* @param request The request to check
* @return true if the request should be cached and false otherwise
*/
[[nodiscard]] static bool
shouldCache(boost::json::object const& request);
ResponseExpirationCache(
std::chrono::steady_clock::duration cacheTimeout,
std::unordered_set<std::string> const& cmds
)
: cacheTimeout_(cacheTimeout)
{
for (auto const& command : cmds) {
cache_.emplace(command, Entry{});
}
}
/**
* @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
*/
[[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
*
* @param request The request to store the response for
* @param cmd The command to store the response for
* @param response The response to store
*/
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
@@ -121,5 +123,4 @@ public:
void
invalidate();
};
} // namespace etl::impl
} // namespace util

View File

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

View File

@@ -24,6 +24,7 @@
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <memory>
@@ -80,6 +81,7 @@ class Retry {
RetryStrategyPtr strategy_;
boost::asio::steady_timer timer_;
size_t attemptNumber_ = 0;
std::shared_ptr<std::atomic_bool> canceled_{std::make_shared<std::atomic_bool>(false)};
public:
/**
@@ -90,6 +92,11 @@ public:
*/
Retry(RetryStrategyPtr strategy, boost::asio::strand<boost::asio::io_context::executor_type> strand);
/**
* @brief Destroy the Retry object
*/
~Retry();
/**
* @brief Schedule a retry
*
@@ -100,15 +107,18 @@ public:
void
retry(Fn&& func)
{
*canceled_ = false;
timer_.expires_after(strategy_->getDelay());
strategy_->increaseDelay();
timer_.async_wait([this, func = std::forward<Fn>(func)](boost::system::error_code const& ec) {
if (ec == boost::asio::error::operation_aborted) {
timer_.async_wait(
[this, canceled = canceled_, func = std::forward<Fn>(func)](boost::system::error_code const& ec) {
if (ec == boost::asio::error::operation_aborted or *canceled) {
return;
}
++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
*/
void
join() noexcept
join() const noexcept
{
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
#include "util/Assert.hpp"
#include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/ObjectView.hpp"
#include "util/newconfig/ValueView.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/Types.hpp"
#include <cstddef>
#include <iterator>
#include <type_traits>
#include <utility>
#include <optional>
#include <string_view>
#include <vector>
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)
* Is constructed with only 1 element which states which type/constraint must every element
* In the array satisfy
*/
class Array {
public:
/**
* @brief Constructs an Array with the provided arguments
* @brief Constructs an Array with provided Arg
*
* @tparam Args Types of the arguments
* @param args Arguments to initialize the elements of the Array
* @param arg Argument to set the type and constraint of ConfigValues in Array
*/
template <typename... Args>
constexpr Array(Args&&... args) : elements_{std::forward<Args>(args)...}
{
}
Array(ConfigValue arg);
/**
* @brief Add ConfigValues to Array class
*
* @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
emplaceBack(ConfigValue value)
{
elements_.push_back(std::move(value));
}
std::optional<Error>
addValue(Value value, std::optional<std::string_view> key = std::nullopt);
/**
* @brief Returns the number of values stored in the Array
@@ -67,10 +62,7 @@ public:
* @return Number of values stored in the Array
*/
[[nodiscard]] size_t
size() const
{
return elements_.size();
}
size() const;
/**
* @brief Returns the ConfigValue at the specified index
@@ -79,13 +71,35 @@ public:
* @return ConfigValue at the specified index
*/
[[nodiscard]] ConfigValue const&
at(std::size_t idx) const
{
ASSERT(idx < elements_.size(), "index is out of scope");
return elements_[idx];
}
at(std::size_t idx) const;
/**
* @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:
ConfigValue itemPattern_;
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/Assert.hpp"
#include "util/OverloadSet.hpp"
#include "util/newconfig/Array.hpp"
#include "util/newconfig/ArrayView.hpp"
#include "util/newconfig/ConfigConstraints.hpp"
#include "util/newconfig/ConfigFileInterface.hpp"
#include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/ObjectView.hpp"
#include "util/newconfig/Types.hpp"
#include "util/newconfig/ValueView.hpp"
#include <fmt/core.h>
@@ -38,6 +43,7 @@
#include <thread>
#include <utility>
#include <variant>
#include <vector>
namespace util::config {
/**
@@ -47,62 +53,76 @@ namespace util::config {
* without default values must be present in the user's config file.
*/
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.port", ConfigValue{ConfigType::Integer}},
{"database.cassandra.port", ConfigValue{ConfigType::Integer}.withConstraint(validatePort)},
{"database.cassandra.keyspace", ConfigValue{ConfigType::String}.defaultValue("clio")},
{"database.cassandra.replication_factor", ConfigValue{ConfigType::Integer}.defaultValue(3u)},
{"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_read_requests_outstanding", ConfigValue{ConfigType::Integer}.defaultValue(100'000)},
{"database.cassandra.max_write_requests_outstanding",
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",
ConfigValue{ConfigType::Integer}.defaultValue(static_cast<uint32_t>(std::thread::hardware_concurrency()))},
{"database.cassandra.core_connections_per_host", ConfigValue{ConfigType::Integer}.defaultValue(1)},
{"database.cassandra.queue_size_io", ConfigValue{ConfigType::Integer}.optional()},
{"database.cassandra.write_batch_size", ConfigValue{ConfigType::Integer}.defaultValue(20)},
{"etl_source.[].ip", Array{ConfigValue{ConfigType::String}.optional()}},
{"etl_source.[].ws_port", Array{ConfigValue{ConfigType::String}.optional().min(1).max(65535)}},
{"etl_source.[].grpc_port", Array{ConfigValue{ConfigType::String}.optional().min(1).max(65535)}},
{"forwarding.cache_timeout", ConfigValue{ConfigType::Double}.defaultValue(0.0)},
{"forwarding.request_timeout", ConfigValue{ConfigType::Double}.defaultValue(10.0)},
ConfigValue{ConfigType::Integer}
.defaultValue(static_cast<uint32_t>(std::thread::hardware_concurrency()))
.withConstraint(validateUint32)},
{"database.cassandra.core_connections_per_host",
ConfigValue{ConfigType::Integer}.defaultValue(1).withConstraint(validateUint16)},
{"database.cassandra.queue_size_io", ConfigValue{ConfigType::Integer}.optional().withConstraint(validateUint16)},
{"database.cassandra.write_batch_size",
ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(validateUint16)},
{"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.max_fetches", ConfigValue{ConfigType::Integer}.defaultValue(1000'000)},
{"dos_guard.max_connections", ConfigValue{ConfigType::Integer}.defaultValue(20)},
{"dos_guard.max_requests", ConfigValue{ConfigType::Integer}.defaultValue(20)},
{"dos_guard.sweep_interval", ConfigValue{ConfigType::Double}.defaultValue(1.0)},
{"cache.peers.[].ip", Array{ConfigValue{ConfigType::String}}},
{"cache.peers.[].port", Array{ConfigValue{ConfigType::String}}},
{"server.ip", ConfigValue{ConfigType::String}},
{"server.port", ConfigValue{ConfigType::Integer}},
{"server.max_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(0)},
{"dos_guard.max_fetches", ConfigValue{ConfigType::Integer}.defaultValue(1000'000).withConstraint(validateUint32)},
{"dos_guard.max_connections", ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(validateUint32)},
{"dos_guard.max_requests", ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(validateUint32)},
{"dos_guard.sweep_interval",
ConfigValue{ConfigType::Double}.defaultValue(1.0).withConstraint(validatePositiveDouble)},
{"cache.peers.[].ip", Array{ConfigValue{ConfigType::String}.withConstraint(validateIP)}},
{"cache.peers.[].port", Array{ConfigValue{ConfigType::String}.withConstraint(validatePort)}},
{"server.ip", ConfigValue{ConfigType::String}.withConstraint(validateIP)},
{"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.admin_password", ConfigValue{ConfigType::String}.optional()},
{"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
{"prometheus.compress_reply", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
{"io_threads", ConfigValue{ConfigType::Integer}.defaultValue(2)},
{"cache.num_diffs", ConfigValue{ConfigType::Integer}.defaultValue(32)},
{"cache.num_markers", ConfigValue{ConfigType::Integer}.defaultValue(48)},
{"cache.num_cursors_from_diff", ConfigValue{ConfigType::Integer}.defaultValue(0)},
{"cache.num_cursors_from_account", ConfigValue{ConfigType::Integer}.defaultValue(0)},
{"cache.page_fetch_size", ConfigValue{ConfigType::Integer}.defaultValue(512)},
{"cache.load", ConfigValue{ConfigType::String}.defaultValue("async")},
{"log_channels.[].channel", Array{ConfigValue{ConfigType::String}.optional()}},
{"log_channels.[].log_level", Array{ConfigValue{ConfigType::String}.optional()}},
{"log_level", ConfigValue{ConfigType::String}.defaultValue("info")},
{"io_threads", ConfigValue{ConfigType::Integer}.defaultValue(2).withConstraint(validateUint16)},
{"cache.num_diffs", ConfigValue{ConfigType::Integer}.defaultValue(32).withConstraint(validateUint16)},
{"cache.num_markers", ConfigValue{ConfigType::Integer}.defaultValue(48).withConstraint(validateUint16)},
{"cache.num_cursors_from_diff", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint16)},
{"cache.num_cursors_from_account", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint16)
},
{"cache.page_fetch_size", ConfigValue{ConfigType::Integer}.defaultValue(512).withConstraint(validateUint16)},
{"cache.load", ConfigValue{ConfigType::String}.defaultValue("async").withConstraint(validateLoadMode)},
{"log_channels.[].channel", Array{ConfigValue{ConfigType::String}.optional().withConstraint(validateChannelName)}},
{"log_channels.[].log_level",
Array{ConfigValue{ConfigType::String}.optional().withConstraint(validateLogLevelName)}},
{"log_level", ConfigValue{ConfigType::String}.defaultValue("info").withConstraint(validateLogLevelName)},
{"log_format",
ConfigValue{ConfigType::String}.defaultValue(
R"(%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%)"
)},
{"log_to_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
{"log_directory", ConfigValue{ConfigType::String}.optional()},
{"log_rotation_size", ConfigValue{ConfigType::Integer}.defaultValue(2048)},
{"log_directory_max_size", ConfigValue{ConfigType::Integer}.defaultValue(50 * 1024)},
{"log_rotation_hour_interval", ConfigValue{ConfigType::Integer}.defaultValue(12)},
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
{"extractor_threads", ConfigValue{ConfigType::Integer}.defaultValue(2u)},
{"log_rotation_size", ConfigValue{ConfigType::Integer}.defaultValue(2048u).withConstraint(validateUint32)},
{"log_directory_max_size",
ConfigValue{ConfigType::Integer}.defaultValue(50u * 1024u).withConstraint(validateUint32)},
{"log_rotation_hour_interval", ConfigValue{ConfigType::Integer}.defaultValue(12).withConstraint(validateUint32)},
{"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)},
{"txn_threshold", ConfigValue{ConfigType::Integer}.defaultValue(0)},
{"start_sequence", ConfigValue{ConfigType::String}.optional()},
{"finish_sequence", ConfigValue{ConfigType::String}.optional()},
{"txn_threshold", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint16)},
{"start_sequence", ConfigValue{ConfigType::Integer}.optional().withConstraint(validateUint32)},
{"finish_sequence", ConfigValue{ConfigType::Integer}.optional().withConstraint(validateUint32)},
{"ssl_cert_file", ConfigValue{ConfigType::String}.optional()},
{"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
{"api_version.min", ConfigValue{ConfigType::Integer}},
@@ -113,7 +133,7 @@ ClioConfigDefinition::ClioConfigDefinition(std::initializer_list<KeyValuePair> p
{
for (auto const& [key, value] : pair) {
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});
}
}
@@ -206,4 +226,51 @@ ClioConfigDefinition::arraySize(std::string_view prefix) const
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

View File

@@ -24,7 +24,7 @@
#include "util/newconfig/ConfigDescription.hpp"
#include "util/newconfig/ConfigFileInterface.hpp"
#include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/Errors.hpp"
#include "util/newconfig/Error.hpp"
#include "util/newconfig/ObjectView.hpp"
#include "util/newconfig/ValueView.hpp"
@@ -41,6 +41,7 @@
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>
namespace util::config {
@@ -66,12 +67,13 @@ public:
/**
* @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
* @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);
/**
@@ -80,9 +82,9 @@ public:
* Should only check for valid values, without populating
*
* @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;
/**

View File

@@ -90,7 +90,9 @@ private:
KV{"server.ip", "IP address 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.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.admin_password", "Password for Clio admin-only APIs."},
KV{"prometheus.enabled", "Enable or disable Prometheus metrics."},
KV{"prometheus.compress_reply", "Enable or disable compression of Prometheus responses."},
KV{"io_threads", "Number of I/O threads."},

View File

@@ -19,9 +19,8 @@
#pragma once
#include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/Types.hpp"
#include <optional>
#include <string_view>
#include <vector>
@@ -36,31 +35,33 @@ namespace util::config {
class ConfigFileInterface {
public:
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.
* @return An optional containing the configuration value if found, otherwise std::nullopt.
* @param key The key of configuration.
* @return the value assosiated with key.
*/
virtual std::optional<ConfigValue>
virtual Value
getValue(std::string_view key) const = 0;
/**
* @brief Retrieves an array of configuration values.
*
* @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;
/**
* @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

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
#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 <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <type_traits>
#include <string_view>
#include <variant>
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
*
@@ -65,8 +45,6 @@ getType()
*/
class ConfigValue {
public:
using Type = std::variant<int64_t, std::string, bool, double>;
/**
* @brief Constructor initializing with the config type
*
@@ -83,12 +61,92 @@ public:
* @return Reference to this 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;
}
/**
* @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
*
@@ -100,32 +158,6 @@ public:
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
*
@@ -165,7 +197,7 @@ public:
*
* @return Config Value
*/
[[nodiscard]] Type const&
[[nodiscard]] Value const&
getValue() const
{
return value_.value();
@@ -178,39 +210,28 @@ private:
* @param type The config type
* @param value The config value
*/
static void
checkTypeConsistency(ConfigType type, Type value)
static std::optional<Error>
checkTypeConsistency(ConfigType type, Value value)
{
if (std::holds_alternative<std::string>(value)) {
ASSERT(type == ConfigType::String, "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::String && !std::holds_alternative<std::string>(value)) {
return Error{"value does not match type string"};
}
if (type == ConfigType::Boolean && !std::holds_alternative<bool>(value)) {
return Error{"value does not match type boolean"};
}
/**
* @brief Sets the value for the config
*
* @param value The value to set
* @return The value that was set
*/
Type
setValue(Type value)
{
checkTypeConsistency(type_, value);
value_ = value;
return value;
if (type == ConfigType::Double && !std::holds_alternative<double>(value)) {
return Error{"value does not match type double"};
}
if (type == ConfigType::Integer && !std::holds_alternative<int64_t>(value)) {
return Error{"value does not match type integer"};
}
return std::nullopt;
}
ConfigType type_{};
bool optional_{false};
std::optional<Type> value_;
std::optional<std::uint32_t> min_;
std::optional<std::uint32_t> max_;
std::optional<Value> value_;
std::optional<std::reference_wrapper<Constraint const>> cons_;
};
} // 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/newconfig/ConfigValue.hpp"
#include "util/newconfig/Types.hpp"
#include <cstdint>
#include <string>
@@ -55,9 +56,9 @@ double
ValueView::asDouble() const
{
if (configVal_.get().hasValue()) {
if (type() == ConfigType::Double) {
if (type() == ConfigType::Double)
return std::get<double>(configVal_.get().getValue());
}
if (type() == ConfigType::Integer)
return static_cast<double>(std::get<int64_t>(configVal_.get().getValue()));
}

View File

@@ -21,6 +21,7 @@
#include "util/Assert.hpp"
#include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/Types.hpp"
#include <fmt/core.h>
@@ -84,7 +85,7 @@ public:
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;
}

View File

@@ -19,6 +19,7 @@
#pragma once
#include "util/WithTimeout.hpp"
#include "util/requests/Types.hpp"
#include "util/requests/WsConnection.hpp"
@@ -39,8 +40,10 @@
#include <boost/beast/websocket/stream_base.hpp>
#include <boost/system/errc.hpp>
#include <atomic>
#include <chrono>
#include <expected>
#include <memory>
#include <optional>
#include <string>
#include <utility>
@@ -65,15 +68,13 @@ public:
auto operation = [&](auto&& token) { ws_.async_read(buffer, token); };
if (timeout) {
withTimeout(operation, yield[errorCode], *timeout);
errorCode = util::withTimeout(operation, yield[errorCode], *timeout);
} else {
operation(yield[errorCode]);
}
if (errorCode) {
errorCode = mapError(errorCode);
if (errorCode)
return std::unexpected{RequestError{"Read error", errorCode}};
}
return boost::beast::buffers_to_string(std::move(buffer).data());
}
@@ -88,15 +89,13 @@ public:
boost::beast::error_code errorCode;
auto operation = [&](auto&& token) { ws_.async_write(boost::asio::buffer(message), token); };
if (timeout) {
withTimeout(operation, yield[errorCode], *timeout);
errorCode = util::withTimeout(operation, yield, *timeout);
} else {
operation(yield[errorCode]);
}
if (errorCode) {
errorCode = mapError(errorCode);
if (errorCode)
return RequestError{"Write error", errorCode};
}
return std::nullopt;
}
@@ -117,31 +116,6 @@ public:
return RequestError{"Close error", errorCode};
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>>;

View File

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

View File

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

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/SslHttpSession.hpp"
#include "web/dosguard/DOSGuardInterface.hpp"
#include "web/impl/ServerSslContext.hpp"
#include "web/interface/Concepts.hpp"
#include "web/ng/impl/ServerSslContext.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address.hpp>
@@ -41,6 +41,7 @@
#include <fmt/core.h>
#include <chrono>
#include <cstdint>
#include <exception>
#include <functional>
#include <memory>
@@ -59,15 +60,6 @@
*/
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.
*
@@ -79,10 +71,8 @@ makeServerSslContext(util::Config const& config);
* @tparam HandlerType The executor to handle the requests
*/
template <
template <typename>
class PlainSessionType,
template <typename>
class SslSessionType,
template <typename> class PlainSessionType,
template <typename> class SslSessionType,
SomeServerHandler 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;
@@ -95,6 +85,7 @@ class Detector : public std::enable_shared_from_this<Detector<PlainSessionType,
std::shared_ptr<HandlerType> const handler_;
boost::beast::flat_buffer buffer_;
std::shared_ptr<impl::AdminVerificationStrategy> const adminVerification_;
std::uint32_t maxWsSendingQueueSize_;
public:
/**
@@ -106,6 +97,7 @@ public:
* @param dosGuard The denial of service guard to use
* @param handler The server handler to use
* @param adminVerification The admin verification strategy to use
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/
Detector(
tcp::socket&& socket,
@@ -113,7 +105,8 @@ public:
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
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))
, ctx_(ctx)
@@ -121,6 +114,7 @@ public:
, dosGuard_(dosGuard)
, handler_(std::move(handler))
, adminVerification_(std::move(adminVerification))
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
{
}
@@ -178,14 +172,22 @@ public:
tagFactory_,
dosGuard_,
handler_,
std::move(buffer_)
std::move(buffer_),
maxWsSendingQueueSize_
)
->run();
return;
}
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();
}
@@ -201,10 +203,8 @@ public:
* @tparam HandlerType The handler to process the request and return response.
*/
template <
template <typename>
class PlainSessionType,
template <typename>
class SslSessionType,
template <typename> class PlainSessionType,
template <typename> class SslSessionType,
SomeServerHandler 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;
@@ -217,6 +217,7 @@ class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslS
std::shared_ptr<HandlerType> handler_;
tcp::acceptor acceptor_;
std::shared_ptr<impl::AdminVerificationStrategy> adminVerification_;
std::uint32_t maxWsSendingQueueSize_;
public:
/**
@@ -229,6 +230,7 @@ public:
* @param dosGuard The denial of service guard to use
* @param handler The server handler to use
* @param adminPassword The optional password to verify admin role in requests
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/
Server(
boost::asio::io_context& ioc,
@@ -237,7 +239,8 @@ public:
util::TagDecoratorFactory tagFactory,
dosguard::DOSGuardInterface& dosGuard,
std::shared_ptr<HandlerType> handler,
std::optional<std::string> adminPassword
std::optional<std::string> adminPassword,
std::uint32_t maxWsSendingQueueSize
)
: ioc_(std::ref(ioc))
, ctx_(std::move(ctx))
@@ -246,6 +249,7 @@ public:
, handler_(std::move(handler))
, acceptor_(boost::asio::make_strand(ioc))
, adminVerification_(impl::make_AdminVerificationStrategy(std::move(adminPassword)))
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
{
boost::beast::error_code ec;
@@ -299,7 +303,13 @@ private:
ctx_ ? std::optional<std::reference_wrapper<boost::asio::ssl::context>>{ctx_.value()} : std::nullopt;
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();
}
@@ -333,7 +343,7 @@ make_HttpServer(
{
static util::Logger const log{"WebServer"};
auto expectedSslContext = makeServerSslContext(config);
auto expectedSslContext = ng::impl::makeServerSslContext(config);
if (not expectedSslContext) {
LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error();
return nullptr;
@@ -361,6 +371,10 @@ make_HttpServer(
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>>(
ioc,
std::move(expectedSslContext).value(),
@@ -368,7 +382,8 @@ make_HttpServer(
util::TagDecoratorFactory(config),
dosGuard,
handler,
std::move(adminPassword)
std::move(adminPassword),
maxWsSendingQueueSize
);
server->run();

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
#include "web/impl/AdminVerificationStrategy.hpp"
#include "util/JsonUtils.hpp"
#include "util/config/Config.hpp"
#include <boost/beast/http/field.hpp>
#include <xrpl/basics/base_uint.h>
@@ -79,4 +80,20 @@ make_AdminVerificationStrategy(std::optional<std::string> password)
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

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