forked from Qortal/qortal
Compare commits
543 Commits
Author | SHA1 | Date | |
---|---|---|---|
ddde92fbd2 | |||
208adb0e4c | |||
|
749143e98e | ||
9b20192b30 | |||
8d6830135c | |||
448b536238 | |||
2e989aaa57 | |||
|
8bd293ccd5 | ||
|
a8d73926b3 | ||
|
bd214a0fd3 | ||
|
2347118e59 | ||
|
7fb093e98a | ||
e3a85786e7 | |||
adbba0f947 | |||
61dec0e4b7 | |||
08a2284ce4 | |||
2e3f97b51f | |||
84b973773a | |||
8ffb0625a1 | |||
2ce02faa07 | |||
89999e6b33 | |||
4d28ba692d | |||
cd6d7a3a98 | |||
0a44928e93 | |||
4b037ad13f | |||
|
1f9a2edca4 | ||
c010ab47db | |||
7803d6c8f5 | |||
b0d43a1890 | |||
f277611d31 | |||
d89f7ad41d | |||
39cc56c4d8 | |||
fccd5a7c97 | |||
46395bf4dc | |||
0eb551acc1 | |||
f55efe38c5 | |||
130bb6cf50 | |||
652c902607 | |||
915bb1ded3 | |||
8319193453 | |||
831ed72e56 | |||
885133195e | |||
|
c45d59b389 | ||
30a289baab | |||
d79d64f6b0 | |||
3d83a79014 | |||
82d5d25c59 | |||
1676098abe | |||
0a47ca1462 | |||
0cf9b23142 | |||
0850654519 | |||
|
6648c4be22 | ||
|
07474ab841 | ||
76f5d17f81 | |||
|
50d6e388f0 | ||
|
93c8f78cd3 | ||
|
d42acb788b | ||
2ee5bc5b35 | |||
5e1ad82738 | |||
7fa1180622 | |||
65c637740a | |||
|
a0b4853518 | ||
bb40dcde65 | |||
|
e74a06e031 | ||
|
ea10759bd3 | ||
2c017fc1b0 | |||
497259f652 | |||
38fd0c55a0 | |||
|
3f2fc5c6a2 | ||
|
56db39e190 | ||
|
bcf68ea63d | ||
fd5ba48611 | |||
5e315de213 | |||
aa6c38e631 | |||
a0c7e3d94e | |||
209920b2f8 | |||
|
941847dd98 | ||
|
881d9c9635 | ||
ab78f22b5e | |||
c2d7dfe42e | |||
2629e5f2e8 | |||
fa1e86a46b | |||
85910573a3 | |||
3043d1c2cb | |||
|
e6d13536e0 | ||
|
ec8ddf2b1c | ||
|
e44382a72e | ||
|
1e90d8757b | ||
|
bd942dd8e3 | ||
|
524ed2b301 | ||
|
5c7ffce887 | ||
27cc9e458e | |||
187a360467 | |||
0a5ac37815 | |||
85cf740839 | |||
|
900e2a9e61 | ||
|
252bb84f24 | ||
|
036d9b67ed | ||
7c9d82b780 | |||
d9ad0bd663 | |||
f105af696d | |||
3162a83dc1 | |||
39da7edf5a | |||
2b83c4bbf3 | |||
68a2e65fc7 | |||
639e1df531 | |||
7c4b0bd7f2 | |||
bbf2787ba4 | |||
|
d42148c3a3 | ||
aba4c6000f | |||
5a691762ed | |||
a530b64ae7 | |||
f82533370d | |||
d976904a8e | |||
|
d0139c24dc | ||
|
c58d2b5813 | ||
|
cf591bea4c | ||
|
acd30eddf0 | ||
|
b6272c1b0f | ||
|
885aa10395 | ||
|
3fda19c8b9 | ||
f14bc86b39 | |||
|
51664f0561 | ||
|
b10dfd28c1 | ||
|
9600185365 | ||
|
bbb346a98e | ||
|
f59e6580ab | ||
|
0266248ca6 | ||
|
cbb171f859 | ||
a64e9052dd | |||
acc37cef0e | |||
7f5692eac0 | |||
454c471dfe | |||
|
1a42c52f65 | ||
145191075a | |||
da889f2905 | |||
211fc0d5a4 | |||
51feb96824 | |||
8c1251d716 | |||
4f05b61a8e | |||
a07052161a | |||
780bfe6249 | |||
d4b0d47c90 | |||
da1ea9fe2c | |||
4cf157ba64 | |||
661827f92a | |||
1cd5dccbd6 | |||
f5cd664dde | |||
|
456a2aebca | ||
|
6377b67274 | ||
|
449aab0a11 | ||
|
02346bd9f0 | ||
|
21257065ff | ||
|
708a978de0 | ||
|
e4134a769b | ||
|
a02d1cec75 | ||
|
2d9f1d6d81 | ||
|
c1da495dd1 | ||
|
533df9f2b8 | ||
|
44b4b08117 | ||
|
a140805c36 | ||
|
ea1d4dd962 | ||
|
a6cbdaafd8 | ||
|
5db808e300 | ||
|
cc95106019 | ||
|
64537ad705 | ||
|
806dc6d056 | ||
|
d58fbab1b5 | ||
|
61ede811cd | ||
|
95d42db773 | ||
|
ef07a444d6 | ||
|
5910ceff80 | ||
f916d3581b | |||
498f409346 | |||
|
d54f840265 | ||
|
cc740cceec | ||
|
df39819de0 | ||
e83b2263f0 | |||
5c90c4bc61 | |||
f7793443f3 | |||
|
f6b91df7b6 | ||
|
92d589a1ca | ||
|
c4a7fb3b92 | ||
|
2d27901f9f | ||
|
ce8fb002cc | ||
3f29116b47 | |||
|
d05359dfa9 | ||
|
587b063e6a | ||
|
9e001dfc16 | ||
|
55f941467f | ||
|
4f9a4a2091 | ||
|
d579606d2d | ||
|
dcedcf8685 | ||
f78764880c | |||
|
070f14b3dd | ||
|
2120490f4b | ||
6051b85e52 | |||
9c62740f44 | |||
|
0ed27228b1 | ||
|
b75c2029ac | ||
21c45535be | |||
|
867fe764ca | ||
|
140f14f2f4 | ||
|
9dd61f0e7a | ||
|
747b1a4f9d | ||
15ae32efd9 | |||
03ba36729b | |||
|
425152657a | ||
|
41645ac7b4 | ||
|
83e324c4ad | ||
|
d7f44376be | ||
|
1400e7ae80 | ||
|
3c116ca4f4 | ||
8caf5bf8be | |||
|
687667c8fe | ||
|
feb5564666 | ||
8062cace30 | |||
bac0f01007 | |||
|
995ed6ab2a | ||
|
cc02810f0c | ||
|
84974775b4 | ||
|
677fd7a64f | ||
|
2d070f343b | ||
|
903fa0346a | ||
be3c6d0b25 | |||
21796341f2 | |||
|
97b4db4095 | ||
|
8977dc7933 | ||
|
7dbf532616 | ||
|
d6dec118a9 | ||
|
813f16df11 | ||
aaba6bf4cf | |||
423a1f7bed | |||
|
e118f4a410 | ||
|
8607c30cb6 | ||
46a9075faf | |||
|
6b775cc639 | ||
7e509f27fb | |||
de9c3e551d | |||
72aa7b7a77 | |||
|
7bd570ae71 | ||
|
5afa4e5b1a | ||
|
3f0999e59d | ||
|
87b3b037bd | ||
|
3bf54dbd0a | ||
|
bf8005aa5a | ||
|
79b674d2f2 | ||
|
c989e3c413 | ||
|
f6e398ec0f | ||
|
5054773761 | ||
|
a2ccf0e7da | ||
b266d205b1 | |||
|
6cd722e735 | ||
|
1e4d4e178d | ||
deaef4fc4a | |||
9671c2da61 | |||
14279e58bc | |||
87a7b9df08 | |||
3c307f15b3 | |||
e287fa0ebe | |||
a94ef17883 | |||
94cfcd66cd | |||
dbbe78263d | |||
|
9e8e0df5d6 | ||
|
95948d35ca | ||
9ca2289ed4 | |||
|
e86dab066d | ||
|
debb67a676 | ||
|
6c4ed2496d | ||
|
61d6bd9bca | ||
4e53e6f0ad | |||
183dac5ac4 | |||
3a723ed116 | |||
9f2b44484e | |||
b05c0158b1 | |||
d915dc10f3 | |||
b0f21c2eee | |||
|
975b0a5f3d | ||
0d64e809c7 | |||
d4ef175c4d | |||
|
3190fed3b9 | ||
|
dfcc7bb370 | ||
|
8bd9b41a13 | ||
|
85300178c7 | ||
|
e885a69688 | ||
|
050886a496 | ||
|
1aab7d8d8f | ||
5febfaf3ee | |||
9b64f12b7a | |||
|
6525cac66c | ||
033b6adb72 | |||
53c3f7899a | |||
f7f2d70b77 | |||
|
5cbd4490cc | ||
|
7103f41e36 | ||
|
2d599ec3c5 | ||
|
e40d884c81 | ||
7cbeb00c1a | |||
|
2af8199d9c | ||
|
404c5d0300 | ||
|
db7b17e52e | ||
|
6d202b2b48 | ||
|
499e2ac3f4 | ||
|
15eefa4177 | ||
|
bf270a63ff | ||
|
9454031b48 | ||
|
cae3fdcb06 | ||
|
8b69b65712 | ||
|
1fbb1659a3 | ||
|
9959985a13 | ||
|
15c073edfe | ||
d453e80c6b | |||
4fb799ba38 | |||
29e56158ae | |||
f74c9672f6 | |||
|
a7ca306d1b | ||
|
1a9087984a | ||
|
94e9f86245 | ||
|
bd05578035 | ||
|
c0ed4022a5 | ||
|
12dbff79c9 | ||
|
6be3897fdb | ||
|
43921e6ab8 | ||
|
b92c7cc866 | ||
|
053d56d01d | ||
|
eb6a834fd9 | ||
|
a08f10ece3 | ||
|
ad51073f25 | ||
|
5983e6ccc9 | ||
|
760788e82b | ||
|
d39131ffa9 | ||
|
3fbcc50503 | ||
26ac7e5be5 | |||
b051f9be89 | |||
24ff3ab581 | |||
34382b6e69 | |||
|
086a9afa0e | ||
|
9428f9688f | ||
|
9f4a0b7957 | ||
|
3c8574a466 | ||
|
c428d7ce2e | ||
|
4feb8f46c8 | ||
|
1f7a60dfd8 | ||
|
379b850bbd | ||
|
7bb61ec564 | ||
|
b0224651c2 | ||
|
6bf2b99913 | ||
|
fd9d0c4e51 | ||
|
c2756a5872 | ||
|
ecfb9a7d6d | ||
|
dd9d3fdb22 | ||
|
eea2884112 | ||
|
640a472876 | ||
|
e244a5f882 | ||
|
278dca75e8 | ||
|
897c44ffe8 | ||
|
d9147b3af3 | ||
fe840bbf02 | |||
|
133848ef50 | ||
|
e44b38819e | ||
|
eecd37d6bc | ||
|
a3ab5238d3 | ||
|
e7901a391f | ||
|
9574100a08 | ||
|
528583fe38 | ||
|
33cfd02c49 | ||
|
18e880158f | ||
|
94d3664cb0 | ||
|
f5c8dfe766 | ||
|
a3526d84bc | ||
|
f7e1f2fca8 | ||
|
811b647c88 | ||
|
3215bb638d | ||
|
8ae7a1d65b | ||
|
29dcd53002 | ||
|
62908f867a | ||
|
ff7a87ab18 | ||
|
5f86ecafd9 | ||
|
9694094bbf | ||
|
d8237abde5 | ||
|
fe999a11f4 | ||
|
c14fca5660 | ||
|
cc8cdcd93a | ||
|
537779b152 | ||
|
ac433b1527 | ||
|
fd8d720946 | ||
|
c0eeef546a | ||
|
badd6ad2b0 | ||
|
b4794ada72 | ||
|
d628b3ab2a | ||
|
5928b54a33 | ||
|
4b04b99401 | ||
|
7e872f7800 | ||
91dfc5efd0 | |||
1343a88ee3 | |||
7f7b02f003 | |||
5650923805 | |||
|
5fb2640a3a | ||
|
66c91fd365 | ||
|
bfc03db6a9 | ||
a4bb445f3e | |||
27afcf12bf | |||
|
707176a202 | ||
|
74a914367f | ||
|
eda6ab5701 | ||
13da0e8a7a | |||
d260c0a9a9 | |||
655073c524 | |||
|
c8f3b6918f | ||
|
1565a461ac | ||
|
1f30bef4f8 | ||
|
6f0479c4fc | ||
|
b967800a3e | ||
|
0b50f965cc | ||
|
90f7cee058 | ||
|
947b523e61 | ||
|
95d72866e9 | ||
|
aea1cc62c8 | ||
|
c763445e6e | ||
|
7a6b83aa22 | ||
|
ba555174ba | ||
|
3763035d4a | ||
|
b1a904a3c7 | ||
|
3c4c5a1457 | ||
|
648fa66f6a | ||
|
072aa469e3 | ||
|
2b2d6f4e52 | ||
|
c6456669e2 | ||
|
a74fa15d60 | ||
|
68b99c8643 | ||
|
b9015217de | ||
|
e1043ceacb | ||
|
8b51590844 | ||
|
a8d92805f9 | ||
|
2cc5b90306 | ||
|
f5f82dc3f6 | ||
|
633f73aa86 | ||
|
a49529ad9b | ||
|
f451bccbf6 | ||
|
4cb755a2f1 | ||
|
5ed3237d2f | ||
|
5c7d12f25e | ||
|
92119b5558 | ||
|
23d211836f | ||
|
36a731255a | ||
|
b661d39844 | ||
|
7725c5e21f | ||
|
21f01226e9 | ||
|
8a1bf8b5ec | ||
|
f8233bd05b | ||
|
29480e5664 | ||
|
5a873f9465 | ||
|
dc1289787d | ||
|
ba4866a2e6 | ||
|
2cbc5aabd5 | ||
e3be43a1e6 | |||
1e10bcf3b0 | |||
a575ea4423 | |||
3e45948646 | |||
49c0d45bc6 | |||
cda32a47f1 | |||
|
49063e54ec | ||
|
df3c68679f | ||
|
c210d63c40 | ||
|
0ec661431c | ||
|
8fa344125c | ||
|
2fd5bfb11a | ||
|
cdcb268bd9 | ||
|
d03a2d7da9 | ||
|
961aa9eefd | ||
|
865d3d8aff | ||
|
c0f29f848f | ||
|
94f4c501fa | ||
|
200b0f3412 | ||
|
eb7a29dd2e | ||
|
9dba4b2968 | ||
|
81788610c4 | ||
|
fc10b61193 | ||
|
05b4ecd4ed | ||
|
aba589c0e0 | ||
|
c682fa89fd | ||
|
21d1750779 | ||
|
923e90ebed | ||
|
9490c62242 | ||
|
c941bc6024 | ||
|
0acf0729e9 | ||
|
1f77ee535f | ||
|
b693a514fd | ||
|
b571931127 | ||
|
92b983a16e | ||
|
3f71a63512 | ||
|
86b5bae320 | ||
|
3775135e0c | ||
|
c172a5764b | ||
|
1a5e3b4fb1 | ||
|
f39b6a15da | ||
|
2dfee13d86 | ||
|
b9d81645f8 | ||
|
9547a087b2 | ||
|
e014a207ef | ||
|
611240650e | ||
|
c71dce92b5 | ||
|
34c3adf280 | ||
|
95a1c6bf8b | ||
|
36e944d7e2 | ||
|
f044166b81 | ||
|
aed1823afb | ||
|
6dfaaf0054 | ||
|
45bc2e46d6 | ||
|
46e2e1043d | ||
|
a3518d1f05 | ||
|
0a1ab3d685 | ||
|
5dbacc4db3 | ||
|
1ce2dcfb2b | ||
|
ed6333f82e | ||
|
f27c9193c7 | ||
|
e48529704c | ||
|
53508f9298 | ||
|
33aeec7e87 | ||
8f847d3689 | |||
|
16dc23ddc7 | ||
|
e80494b784 | ||
|
111ec3b483 | ||
|
db4a9ee880 | ||
|
b1ebe1864b | ||
|
3c251c35ea | ||
|
4954a1744b | ||
a7bbad17d7 | |||
8ca9423c52 | |||
|
32b9b7e578 | ||
|
f045e10ada | ||
|
560282dc1d | ||
|
9cd6372161 | ||
|
a4551245cb | ||
|
e4f45c1a70 | ||
|
bc44b998dc | ||
|
b89a35ac69 | ||
|
9566bda279 | ||
|
20d4e88fab | ||
|
a8c27be18a | ||
|
af6be759e7 | ||
|
896d814385 | ||
|
0693e26cda |
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
* Build auto-update download: `tools/build-auto-update.sh` - uploads auto-update file into new git branch
|
||||
* Restart local node
|
||||
* Publish auto-update transaction using *private key* for **non-admin** member of "dev" group:
|
||||
`tools/publish-auto-update.sh non-admin-dev-member-private-key-in-base58`
|
||||
`tools/publish-auto-update.pl non-admin-dev-member-private-key-in-base58`
|
||||
* Wait for auto-update `ARBITRARY` transaction to be confirmed into a block
|
||||
* Have "dev" group admins 'approve' auto-update using `tools/approve-auto-update.sh`
|
||||
This tool will prompt for *private key* of **admin** of "dev" group
|
||||
|
918
Q-Apps.md
918
Q-Apps.md
@ -1,3 +1,919 @@
|
||||
# Qortal Project - Q-Apps Documentation
|
||||
|
||||
The Q-Apps documentation has moved [here](https://github.com/Qortal/qortal/blob/master/Q-Apps.md).
|
||||
## Introduction
|
||||
|
||||
Q-Apps are static web apps written in javascript, HTML, CSS, and other static assets. The key difference between a Q-App and a fully static site is its ability to interact with both the logged-in user and on-chain data. This is achieved using the API described in this document.
|
||||
|
||||
|
||||
# Section 0: Basic QDN concepts
|
||||
|
||||
## Introduction to QDN resources
|
||||
Each published item on QDN (Qortal Data Network) is referred to as a "resource". A resource could contain anything from a few characters of text, to a multi-layered directory structure containing thousands of files.
|
||||
|
||||
Resources are stored on-chain, however the data payload is generally stored off-chain, and verified using an on-chain SHA-256 hash.
|
||||
|
||||
To publish a resource, a user must first register a name, and then include that name when publishing the data. Accounts without a registered name are unable to publish to QDN from a Q-App at this time.
|
||||
|
||||
Owning the name grants update privileges to the data. If that name is later sold or transferred, the permission to update that resource is moved to the new owner.
|
||||
|
||||
|
||||
## Name, service & identifier
|
||||
|
||||
Each QDN resource has 3 important fields:
|
||||
- `name` - the registered name of the account that is publishing the data (which will hold update/edit privileges going forwards).<br /><br />
|
||||
- `service` - the type of content (e.g. IMAGE or JSON). Different services have different validation rules. See [list of available services](#services).<br /><br />
|
||||
- `identifier` - an optional string to allow more than one resource to exist for a given name/service combination. For example, the name `QortalDemo` may wish to publish multiple images. This can be achieved by using a different identifier string for each. The identifier is only unique to the name in question, and so it doesn't matter if another name is using the same service and identifier string.
|
||||
|
||||
|
||||
## Shared identifiers
|
||||
|
||||
Since an identifier can be used by multiple names, this can be used to the advantage of Q-App developers as it allows for data to be stored in a deterministic location.
|
||||
|
||||
An example of this is the user's avatar. This will always be published with service `THUMBNAIL` and identifier `qortal_avatar`, along with the user's name. So, an app can display the avatar of a user just by specifying their name when requesting the data. The same applies when publishing data.
|
||||
|
||||
|
||||
## "Default" resources
|
||||
|
||||
A "default" resource refers to one without an identifier. For example, when a website is published via the UI, it will use the user's name and the service `WEBSITE`. These do not have an identifier, and are therefore the "default" website for this name. When requesting or publishing data without an identifier, apps can either omit the `identifier` key entirely, or include `"identifier": "default"` to indicate that the resource(s) being queried or published do not have an identifier.
|
||||
|
||||
|
||||
<a name="services"></a>
|
||||
## Available service types
|
||||
|
||||
Here is a list of currently available services that can be used in Q-Apps:
|
||||
|
||||
### Public services ###
|
||||
The services below are intended to be used for publicly accessible data.
|
||||
|
||||
IMAGE,
|
||||
THUMBNAIL,
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
PODCAST,
|
||||
VOICE,
|
||||
ARBITRARY_DATA,
|
||||
JSON,
|
||||
DOCUMENT,
|
||||
LIST,
|
||||
PLAYLIST,
|
||||
METADATA,
|
||||
BLOG,
|
||||
BLOG_POST,
|
||||
BLOG_COMMENT,
|
||||
GIF_REPOSITORY,
|
||||
ATTACHMENT,
|
||||
FILE,
|
||||
FILES,
|
||||
CHAIN_DATA,
|
||||
STORE,
|
||||
PRODUCT,
|
||||
OFFER,
|
||||
COUPON,
|
||||
CODE,
|
||||
PLUGIN,
|
||||
EXTENSION,
|
||||
GAME,
|
||||
ITEM,
|
||||
NFT,
|
||||
DATABASE,
|
||||
SNAPSHOT,
|
||||
COMMENT,
|
||||
CHAIN_COMMENT,
|
||||
WEBSITE,
|
||||
APP,
|
||||
QCHAT_ATTACHMENT,
|
||||
QCHAT_IMAGE,
|
||||
QCHAT_AUDIO,
|
||||
QCHAT_VOICE
|
||||
|
||||
### Private services ###
|
||||
For the services below, data is encrypted for a single recipient, and can only be decrypted using the private key of the recipient's wallet.
|
||||
|
||||
QCHAT_ATTACHMENT_PRIVATE,
|
||||
ATTACHMENT_PRIVATE,
|
||||
FILE_PRIVATE,
|
||||
IMAGE_PRIVATE,
|
||||
VIDEO_PRIVATE,
|
||||
AUDIO_PRIVATE,
|
||||
VOICE_PRIVATE,
|
||||
DOCUMENT_PRIVATE,
|
||||
MAIL_PRIVATE,
|
||||
MESSAGE_PRIVATE
|
||||
|
||||
|
||||
## Single vs multi-file resources
|
||||
|
||||
Some resources, such as those published with the `IMAGE` or `JSON` service, consist of a single file or piece of data (the image or the JSON string). This is the most common type of QDN resource, especially in the context of Q-Apps. These can be published by supplying a base64-encoded string containing the data.
|
||||
|
||||
Other resources, such as those published with the `WEBSITE`, `APP`, or `GIF_REPOSITORY` service, can contain multiple files and directories. Publishing these kinds of files is not yet available for Q-Apps, however it is possible to retrieve multi-file resources that are already published. When retrieving this data (via FETCH_QDN_RESOURCE), a `filepath` must be included to indicate the file that you would like to retrieve. There is no need to specify a filepath for single file resources, as these will automatically return the contents of the single file.
|
||||
|
||||
|
||||
## App-specific data
|
||||
|
||||
Some apps may want to make all QDN data for a particular service available. However, others may prefer to only deal with data that has been published by their app (if a specific format/schema is being used for instance).
|
||||
|
||||
Identifiers can be used to allow app developers to locate data that has been published by their app. The recommended approach for this is to use the app name as a prefix on all identifiers when publishing data.
|
||||
|
||||
For instance, an app called `MyApp` could allow users to publish JSON data. The app could choose to prefix all identifiers with the string `myapp_`, and then use a random string for each published resource (resulting in identifiers such as `myapp_qR5ndZ8v`). Then, to locate data that has potentially been published by users of MyApp, it can later search the QDN database for items with `"service": "JSON"` and `"identifier": "myapp_"`. The SEARCH_QDN_RESOURCES action has a `prefix` option in order to match identifiers beginning with the supplied string.
|
||||
|
||||
Note that QDN is a permissionless system, and therefore it's not possible to verify that a resource was actually published by the app. It is recommended that apps validate the contents of the resource to ensure it is formatted correctly, instead of making assumptions.
|
||||
|
||||
|
||||
## Updating a resource
|
||||
|
||||
To update a resource, it can be overwritten by publishing with the same `name`, `service`, and `identifier` combination. Note that the authenticated account must currently own the name in order to publish an update.
|
||||
|
||||
|
||||
## Routing
|
||||
|
||||
If a non-existent `filepath` is accessed, the default behaviour of QDN is to return a `404: File not found` error. This includes anything published using the `WEBSITE` service.
|
||||
|
||||
However, routing is handled differently for anything published using the `APP` service.
|
||||
|
||||
For apps, QDN automatically sends all unhandled requests to the index file (generally index.html). This allows the app to use custom routing, as it is able to listen on any path. If a file exists at a path, the file itself will be served, and so the request won't be sent to the index file.
|
||||
|
||||
It's recommended that all apps return a 404 page if a request isn't able to be routed.
|
||||
|
||||
|
||||
# Section 1: Simple links and image loading via HTML
|
||||
|
||||
## Section 1a: Linking to other QDN websites / resources
|
||||
|
||||
The `qortal://` protocol can be used to access QDN data from within Qortal websites and apps. The basic format is as follows:
|
||||
```
|
||||
<a href="qortal://{service}/{name}/{identifier}/{path}">link text</a>
|
||||
```
|
||||
|
||||
However, the system will support the omission of the `identifier` and/or `path` components to allow for simpler URL formats.
|
||||
|
||||
A simple link to another website can be achieved with this HTML code:
|
||||
```
|
||||
<a href="qortal://WEBSITE/QortalDemo">link text</a>
|
||||
```
|
||||
|
||||
To link to a specific page of another website:
|
||||
```
|
||||
<a href="qortal://WEBSITE/QortalDemo/minting-leveling/index.html">link text</a>
|
||||
```
|
||||
|
||||
To link to a standalone resource, such as an avatar
|
||||
```
|
||||
<a href="qortal://THUMBNAIL/QortalDemo/qortal_avatar">avatar</a>
|
||||
```
|
||||
|
||||
For cases where you would prefer to explicitly include an identifier (to remove ambiguity) you can use the keyword `default` to access a resource that doesn't have an identifier. For instance:
|
||||
```
|
||||
<a href="qortal://WEBSITE/QortalDemo/default">link to root of website</a>
|
||||
<a href="qortal://WEBSITE/QortalDemo/default/minting-leveling/index.html">link to subpage of website</a>
|
||||
```
|
||||
|
||||
|
||||
## Section 1b: Linking to other QDN images
|
||||
|
||||
The same applies for images, such as displaying an avatar:
|
||||
```
|
||||
<img src="qortal://THUMBNAIL/QortalDemo/qortal_avatar" />
|
||||
```
|
||||
|
||||
...or even an image from an entirely different website:
|
||||
```
|
||||
<img src="qortal://WEBSITE/AlphaX/assets/img/logo.png" />
|
||||
```
|
||||
|
||||
|
||||
# Section 2: Integrating a Javascript app
|
||||
|
||||
Javascript apps allow for much more complex integrations with Qortal's blockchain data.
|
||||
|
||||
## Section 2a: Direct API calls
|
||||
|
||||
The standard [Qortal Core API](http://localhost:12391/api-documentation) is available to websites and apps, and can be called directly using a standard AJAX request, such as:
|
||||
```
|
||||
async function getNameInfo(name) {
|
||||
const response = await fetch("/names/" + name);
|
||||
const nameData = await response.json();
|
||||
console.log("nameData: " + JSON.stringify(nameData));
|
||||
}
|
||||
getNameInfo("QortalDemo");
|
||||
```
|
||||
|
||||
However, this only works for read-only data, such as looking up transactions, names, balances, etc. Also, since the address of the logged in account can't be retrieved from the core, apps can't show personalized data with this approach.
|
||||
|
||||
|
||||
## Section 2b: User interaction via qortalRequest()
|
||||
|
||||
To take things a step further, the qortalRequest() function can be used to interact with the user, in order to:
|
||||
|
||||
- Request address and public key of the logged in account
|
||||
- Publish data to QDN
|
||||
- Send chat messages
|
||||
- Join groups
|
||||
- Deploy ATs (smart contracts)
|
||||
- Send QORT or any supported foreign coin
|
||||
- Add/remove items from lists
|
||||
|
||||
In addition to the above, qortalRequest() also supports many read-only functions that are also available via direct core API calls. Using qortalRequest() helps with futureproofing, as the core APIs can be modified without breaking functionality of existing Q-Apps.
|
||||
|
||||
|
||||
### Making a request
|
||||
|
||||
Qortal core will automatically inject the `qortalRequest()` javascript function to all websites/apps, which returns a Promise. This can be used to fetch data or publish data to the Qortal blockchain. This functionality supports async/await, as well as try/catch error handling.
|
||||
|
||||
```
|
||||
async function myfunction() {
|
||||
try {
|
||||
let res = await qortalRequest({
|
||||
action: "GET_ACCOUNT_DATA",
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
console.log(JSON.stringify(res)); // Log the response to the console
|
||||
|
||||
} catch(e) {
|
||||
console.log("Error: " + JSON.stringify(e));
|
||||
}
|
||||
}
|
||||
myfunction();
|
||||
```
|
||||
|
||||
### Timeouts
|
||||
|
||||
Request timeouts are handled automatically when using qortalRequest(). The timeout value will differ based on the action being used - see `getDefaultTimeout()` in [q-apps.js](src/main/resources/q-apps/q-apps.js) for the current values.
|
||||
|
||||
If a request times out it will throw an error - `The request timed out` - which can be handled by the Q-App.
|
||||
|
||||
It is also possible to specify a custom timeout using `qortalRequestWithTimeout(request, timeout)`, however this is discouraged. It's more reliable and futureproof to let the core handle the timeout values.
|
||||
|
||||
|
||||
# Section 3: qortalRequest Documentation
|
||||
|
||||
## Supported actions
|
||||
|
||||
Here is a list of currently supported actions:
|
||||
- GET_USER_ACCOUNT
|
||||
- GET_ACCOUNT_DATA
|
||||
- GET_ACCOUNT_NAMES
|
||||
- SEARCH_NAMES
|
||||
- GET_NAME_DATA
|
||||
- LIST_QDN_RESOURCES
|
||||
- SEARCH_QDN_RESOURCES
|
||||
- GET_QDN_RESOURCE_STATUS
|
||||
- GET_QDN_RESOURCE_PROPERTIES
|
||||
- GET_QDN_RESOURCE_METADATA
|
||||
- GET_QDN_RESOURCE_URL
|
||||
- LINK_TO_QDN_RESOURCE
|
||||
- FETCH_QDN_RESOURCE
|
||||
- PUBLISH_QDN_RESOURCE
|
||||
- PUBLISH_MULTIPLE_QDN_RESOURCES
|
||||
- DECRYPT_DATA
|
||||
- SAVE_FILE
|
||||
- GET_WALLET_BALANCE
|
||||
- GET_BALANCE
|
||||
- SEND_COIN
|
||||
- SEARCH_CHAT_MESSAGES
|
||||
- SEND_CHAT_MESSAGE
|
||||
- LIST_GROUPS
|
||||
- JOIN_GROUP
|
||||
- DEPLOY_AT
|
||||
- GET_AT
|
||||
- GET_AT_DATA
|
||||
- LIST_ATS
|
||||
- FETCH_BLOCK
|
||||
- FETCH_BLOCK_RANGE
|
||||
- SEARCH_TRANSACTIONS
|
||||
- GET_PRICE
|
||||
- GET_LIST_ITEMS
|
||||
- ADD_LIST_ITEMS
|
||||
- DELETE_LIST_ITEM
|
||||
|
||||
More functionality will be added in the future.
|
||||
|
||||
## Example Requests
|
||||
|
||||
Here are some example requests for each of the above:
|
||||
|
||||
### Get address of logged in account
|
||||
_Will likely require user approval_
|
||||
```
|
||||
let account = await qortalRequest({
|
||||
action: "GET_USER_ACCOUNT"
|
||||
});
|
||||
let address = account.address;
|
||||
```
|
||||
|
||||
### Get public key of logged in account
|
||||
_Will likely require user approval_
|
||||
```
|
||||
let pubkey = await qortalRequest({
|
||||
action: "GET_USER_ACCOUNT"
|
||||
});
|
||||
let publicKey = account.publicKey;
|
||||
```
|
||||
|
||||
### Get account data
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_ACCOUNT_DATA",
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
```
|
||||
|
||||
### Get names owned by account
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
```
|
||||
|
||||
### Search names
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEARCH_NAMES",
|
||||
query: "search query goes here",
|
||||
prefix: false, // Optional - if true, only the beginning of the name is matched
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: false
|
||||
});
|
||||
```
|
||||
|
||||
### Get name data
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: "QortalDemo"
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### List QDN resources
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "LIST_QDN_RESOURCES",
|
||||
service: "THUMBNAIL",
|
||||
name: "QortalDemo", // Optional (exact match)
|
||||
identifier: "qortal_avatar", // Optional (exact match)
|
||||
default: true, // Optional
|
||||
includeStatus: false, // Optional - will take time to respond, so only request if necessary
|
||||
includeMetadata: false, // Optional - will take time to respond, so only request if necessary
|
||||
followedOnly: false, // Optional - include followed names only
|
||||
excludeBlocked: false, // Optional - exclude blocked content
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Search QDN resources
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "THUMBNAIL",
|
||||
query: "search query goes here", // Optional - searches both "identifier" and "name" fields
|
||||
identifier: "search query goes here", // Optional - searches only the "identifier" field
|
||||
name: "search query goes here", // Optional - searches only the "name" field
|
||||
prefix: false, // Optional - if true, only the beginning of fields are matched in all of the above filters
|
||||
exactMatchNames: true, // Optional - if true, partial name matches are excluded
|
||||
default: false, // Optional - if true, only resources without identifiers are returned
|
||||
mode: "LATEST", // Optional - whether to return all resources or just the latest for a name/service combination. Possible values: ALL,LATEST. Default: LATEST
|
||||
minLevel: 1, // Optional - whether to filter results by minimum account level
|
||||
includeStatus: false, // Optional - will take time to respond, so only request if necessary
|
||||
includeMetadata: false, // Optional - will take time to respond, so only request if necessary
|
||||
nameListFilter: "QApp1234Subscriptions", // Optional - will only return results if they are from a name included in supplied list
|
||||
followedOnly: false, // Optional - include followed names only
|
||||
excludeBlocked: false, // Optional - exclude blocked content
|
||||
// before: 1683546000000, // Optional - limit to resources created before timestamp
|
||||
// after: 1683546000000, // Optional - limit to resources created after timestamp
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Search QDN resources (multiple names)
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "THUMBNAIL",
|
||||
query: "search query goes here", // Optional - searches both "identifier" and "name" fields
|
||||
identifier: "search query goes here", // Optional - searches only the "identifier" field
|
||||
names: ["QortalDemo", "crowetic", "AlphaX"], // Optional - searches only the "name" field for any of the supplied names
|
||||
prefix: false, // Optional - if true, only the beginning of fields are matched in all of the above filters
|
||||
exactMatchNames: true, // Optional - if true, partial name matches are excluded
|
||||
default: false, // Optional - if true, only resources without identifiers are returned
|
||||
mode: "LATEST", // Optional - whether to return all resources or just the latest for a name/service combination. Possible values: ALL,LATEST. Default: LATEST
|
||||
includeStatus: false, // Optional - will take time to respond, so only request if necessary
|
||||
includeMetadata: false, // Optional - will take time to respond, so only request if necessary
|
||||
nameListFilter: "QApp1234Subscriptions", // Optional - will only return results if they are from a name included in supplied list
|
||||
followedOnly: false, // Optional - include followed names only
|
||||
excludeBlocked: false, // Optional - exclude blocked content
|
||||
// before: 1683546000000, // Optional - limit to resources created before timestamp
|
||||
// after: 1683546000000, // Optional - limit to resources created after timestamp
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Fetch QDN single file resource
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: "QortalDemo",
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar", // Optional. If omitted, the default resource is returned, or you can alternatively use the keyword "default"
|
||||
encoding: "base64", // Optional. If omitted, data is returned in raw form
|
||||
rebuild: false
|
||||
});
|
||||
```
|
||||
|
||||
### Fetch file from multi file QDN resource
|
||||
Data is returned in the base64 format
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: "QortalDemo",
|
||||
service: "WEBSITE",
|
||||
identifier: "default", // Optional. If omitted, the default resource is returned, or you can alternatively request that using the keyword "default", as shown here
|
||||
filepath: "index.html", // Required only for resources containing more than one file
|
||||
rebuild: false
|
||||
});
|
||||
```
|
||||
|
||||
### Get QDN resource status
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_STATUS",
|
||||
name: "QortalDemo",
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar", // Optional
|
||||
build: true // Optional - request that the resource is fetched & built in the background
|
||||
});
|
||||
```
|
||||
|
||||
### Get QDN resource properties
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_PROPERTIES",
|
||||
name: "QortalDemo",
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar" // Optional
|
||||
});
|
||||
// Returns: filename, size, mimeType (where available)
|
||||
```
|
||||
|
||||
### Get QDN resource metadata
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_METADATA",
|
||||
name: "QortalDemo",
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar" // Optional
|
||||
});
|
||||
```
|
||||
|
||||
### Publish a single file to QDN
|
||||
_Requires user approval_.<br />
|
||||
Note: this publishes a single, base64-encoded file. Multi-file resource publishing (such as a WEBSITE or GIF_REPOSITORY) is not yet supported via a Q-App. It will be added in a future update.
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: "Demo", // Publisher must own the registered name - use GET_ACCOUNT_NAMES for a list
|
||||
service: "IMAGE",
|
||||
identifier: "myapp-image1234" // Optional
|
||||
data64: "base64_encoded_data",
|
||||
// filename: "image.jpg", // Optional - to help apps determine the file's type
|
||||
// title: "Title", // Optional
|
||||
// description: "Description", // Optional
|
||||
// category: "TECHNOLOGY", // Optional
|
||||
// tag1: "any", // Optional
|
||||
// tag2: "strings", // Optional
|
||||
// tag3: "can", // Optional
|
||||
// tag4: "go", // Optional
|
||||
// tag5: "here", // Optional
|
||||
// encrypt: true, // Optional - to be used with a private service
|
||||
// recipientPublicKey: "publickeygoeshere" // Only required if `encrypt` is set to true
|
||||
});
|
||||
```
|
||||
|
||||
### Publish multiple resources at once to QDN
|
||||
_Requires user approval_.<br />
|
||||
Note: each resource being published consists of a single, base64-encoded file, each in its own transaction. Useful for publishing two or more related things, such as a video and a video thumbnail.
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
|
||||
resources: [
|
||||
name: "Demo", // Publisher must own the registered name - use GET_ACCOUNT_NAMES for a list
|
||||
service: "IMAGE",
|
||||
identifier: "myapp-image1234" // Optional
|
||||
data64: "base64_encoded_data",
|
||||
// filename: "image.jpg", // Optional - to help apps determine the file's type
|
||||
// title: "Title", // Optional
|
||||
// description: "Description", // Optional
|
||||
// category: "TECHNOLOGY", // Optional
|
||||
// tag1: "any", // Optional
|
||||
// tag2: "strings", // Optional
|
||||
// tag3: "can", // Optional
|
||||
// tag4: "go", // Optional
|
||||
// tag5: "here", // Optional
|
||||
// encrypt: true, // Optional - to be used with a private service
|
||||
// recipientPublicKey: "publickeygoeshere" // Only required if `encrypt` is set to true
|
||||
],
|
||||
[
|
||||
... more resources here if needed ...
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Decrypt encrypted/private data
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "DECRYPT_DATA",
|
||||
encryptedData: 'qortalEncryptedDatabMx4fELNTV+ifJxmv4+GcuOIJOTo+3qAvbWKNY2L1r',
|
||||
publicKey: 'publickeygoeshere'
|
||||
});
|
||||
// Returns base64 encoded string of plaintext data
|
||||
```
|
||||
|
||||
### Prompt user to save a file to disk
|
||||
Note: mimeType not required but recommended. If not specified, saving will fail if the mimeType is unable to be derived from the Blob.
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SAVE_FILE",
|
||||
blob: dataBlob,
|
||||
filename: "myfile.pdf",
|
||||
mimeType: "application/pdf" // Optional but recommended
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Get wallet balance (QORT)
|
||||
_Requires user approval_
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_WALLET_BALANCE",
|
||||
coin: "QORT"
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Get address or asset balance
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_BALANCE",
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
```
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_BALANCE",
|
||||
assetId: 1,
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
```
|
||||
|
||||
### Send QORT to address
|
||||
_Requires user approval_
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEND_COIN",
|
||||
coin: "QORT",
|
||||
destinationAddress: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2",
|
||||
amount: 1.00000000 // 1 QORT
|
||||
});
|
||||
```
|
||||
|
||||
### Send foreign coin to address
|
||||
_Requires user approval_<br />
|
||||
Note: default fees can be found [here](https://github.com/Qortal/qortal-ui/blob/master/plugins/plugins/core/qdn/browser/browser.src.js#L205-L209).
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEND_COIN",
|
||||
coin: "LTC",
|
||||
destinationAddress: "LSdTvMHRm8sScqwCi6x9wzYQae8JeZhx6y",
|
||||
amount: 1.00000000, // 1 LTC
|
||||
fee: 0.00000020 // Optional fee per byte (default fee used if omitted, recommended) - not used for QORT or ARRR
|
||||
});
|
||||
```
|
||||
|
||||
### Search or list chat messages
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEARCH_CHAT_MESSAGES",
|
||||
before: 999999999999999,
|
||||
after: 0,
|
||||
txGroupId: 0, // Optional (must specify either txGroupId or two involving addresses)
|
||||
// involving: ["QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2", "QSefrppsDCsZebcwrqiM1gNbWq7YMDXtG2"], // Optional (must specify either txGroupId or two involving addresses)
|
||||
// reference: "reference", // Optional
|
||||
// chatReference: "chatreference", // Optional
|
||||
// hasChatReference: true, // Optional
|
||||
encoding: "BASE64", // Optional (defaults to BASE58 if omitted)
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Send a group chat message
|
||||
_Requires user approval_
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEND_CHAT_MESSAGE",
|
||||
groupId: 0,
|
||||
message: "Test"
|
||||
});
|
||||
```
|
||||
|
||||
### Send a private chat message
|
||||
_Requires user approval_
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEND_CHAT_MESSAGE",
|
||||
destinationAddress: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2",
|
||||
message: "Test"
|
||||
});
|
||||
```
|
||||
|
||||
### List groups
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "LIST_GROUPS",
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Join a group
|
||||
_Requires user approval_
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "JOIN_GROUP",
|
||||
groupId: 100
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Deploy an AT
|
||||
_Requires user approval_
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "DEPLOY_AT",
|
||||
creationBytes: "12345", // Must be Base58 encoded
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
type: "test type",
|
||||
tags: "test tags",
|
||||
amount: 1.00000000, // 1 QORT
|
||||
assetId: 0,
|
||||
// fee: 0.002 // optional - will use default fee if excluded
|
||||
});
|
||||
```
|
||||
|
||||
### Get AT info
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_AT",
|
||||
atAddress: "ASRUsCjk6fa5bujv3oWYmWaVqNtvxydpPH"
|
||||
});
|
||||
```
|
||||
|
||||
### Get AT data bytes (base58 encoded)
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_AT_DATA",
|
||||
atAddress: "ASRUsCjk6fa5bujv3oWYmWaVqNtvxydpPH"
|
||||
});
|
||||
```
|
||||
|
||||
### List ATs by functionality
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "LIST_ATS",
|
||||
codeHash58: "4KdJETRAdymE7dodDmJbf5d9L1bp4g5Nxky8m47TBkvA",
|
||||
isExecutable: true,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Fetch block by signature
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "FETCH_BLOCK",
|
||||
signature: "875yGFUy1zHV2hmxNWzrhtn9S1zkeD7SQppwdXFysvTXrankCHCz4iyAUgCBM3GjvibbnyRQpriuy1cyu953U1u5uQdzuH3QjQivi9UVwz86z1Akn17MGd5Z5STjpDT7248K6vzMamuqDei57Znonr8GGgn8yyyABn35CbZUCeAuXju"
|
||||
});
|
||||
```
|
||||
|
||||
### Fetch block by height
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "FETCH_BLOCK",
|
||||
height: "1139850"
|
||||
});
|
||||
```
|
||||
|
||||
### Fetch a range of blocks
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "FETCH_BLOCK_RANGE",
|
||||
height: "1139800",
|
||||
count: 20,
|
||||
reverse: false
|
||||
});
|
||||
```
|
||||
|
||||
### Search transactions
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEARCH_TRANSACTIONS",
|
||||
// startBlock: 1139000,
|
||||
// blockLimit: 1000,
|
||||
txGroupId: 0,
|
||||
txType: [
|
||||
"PAYMENT",
|
||||
"REWARD_SHARE"
|
||||
],
|
||||
confirmationStatus: "CONFIRMED",
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
reverse: false
|
||||
});
|
||||
```
|
||||
|
||||
### Get an estimate of the QORT price
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_PRICE",
|
||||
blockchain: "LITECOIN",
|
||||
// maxtrades: 10,
|
||||
inverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Get URL to load a QDN resource
|
||||
Note: this returns a "Resource does not exist" error if a non-existent resource is requested.
|
||||
```
|
||||
let url = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_URL",
|
||||
service: "THUMBNAIL",
|
||||
name: "QortalDemo",
|
||||
identifier: "qortal_avatar"
|
||||
// path: "filename.jpg" // optional - not needed if resource contains only one file
|
||||
});
|
||||
```
|
||||
|
||||
### Get URL to load a QDN website
|
||||
Note: this returns a "Resource does not exist" error if a non-existent resource is requested.
|
||||
```
|
||||
let url = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_URL",
|
||||
service: "WEBSITE",
|
||||
name: "QortalDemo",
|
||||
});
|
||||
```
|
||||
|
||||
### Get URL to load a specific file from a QDN website
|
||||
Note: this returns a "Resource does not exist" error if a non-existent resource is requested.
|
||||
```
|
||||
let url = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_URL",
|
||||
service: "WEBSITE",
|
||||
name: "AlphaX",
|
||||
path: "/assets/img/logo.png"
|
||||
});
|
||||
```
|
||||
|
||||
### Link/redirect to another QDN website
|
||||
Note: an alternate method is to include `<a href="qortal://WEBSITE/QortalDemo">link text</a>` within your HTML code.
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "LINK_TO_QDN_RESOURCE",
|
||||
service: "WEBSITE",
|
||||
name: "QortalDemo",
|
||||
});
|
||||
```
|
||||
|
||||
### Link/redirect to a specific path of another QDN website
|
||||
Note: an alternate method is to include `<a href="qortal://WEBSITE/QortalDemo/minting-leveling/index.html">link text</a>` within your HTML code.
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "LINK_TO_QDN_RESOURCE",
|
||||
service: "WEBSITE",
|
||||
name: "QortalDemo",
|
||||
path: "/minting-leveling/index.html"
|
||||
});
|
||||
```
|
||||
|
||||
### Get the contents of a list
|
||||
_Requires user approval_
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_LIST_ITEMS",
|
||||
list_name: "followedNames"
|
||||
});
|
||||
```
|
||||
|
||||
### Add one or more items to a list
|
||||
_Requires user approval_
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "ADD_LIST_ITEMS",
|
||||
list_name: "blockedNames",
|
||||
items: ["QortalDemo"]
|
||||
});
|
||||
```
|
||||
|
||||
### Delete a single item from a list
|
||||
_Requires user approval_.
|
||||
Items must be deleted one at a time.
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "DELETE_LIST_ITEM",
|
||||
list_name: "blockedNames",
|
||||
item: "QortalDemo"
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
# Section 4: Examples
|
||||
|
||||
Some example projects can be found [here](https://github.com/Qortal/Q-Apps). These can be cloned and modified, or used as a reference when creating a new app.
|
||||
|
||||
|
||||
## Sample App
|
||||
|
||||
Here is a sample application to display the logged-in user's avatar:
|
||||
```
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
async function showAvatar() {
|
||||
try {
|
||||
// Get QORT address of logged in account
|
||||
let account = await qortalRequest({
|
||||
action: "GET_USER_ACCOUNT"
|
||||
});
|
||||
let address = account.address;
|
||||
console.log("address: " + address);
|
||||
|
||||
// Get names owned by this account
|
||||
let names = await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: address
|
||||
});
|
||||
console.log("names: " + JSON.stringify(names));
|
||||
|
||||
if (names.length == 0) {
|
||||
console.log("User has no registered names");
|
||||
return;
|
||||
}
|
||||
|
||||
// Download base64-encoded avatar of the first registered name
|
||||
let avatar = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: names[0].name,
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar",
|
||||
encoding: "base64"
|
||||
});
|
||||
console.log("Avatar size: " + avatar.length + " bytes");
|
||||
|
||||
// Display the avatar image on the screen
|
||||
document.getElementById("avatar").src = "data:image/png;base64," + avatar;
|
||||
|
||||
} catch(e) {
|
||||
console.log("Error: " + JSON.stringify(e));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="showAvatar()">
|
||||
<img width="500" id="avatar" />
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
# Section 5: Testing and Development
|
||||
|
||||
Publishing an in-development app to mainnet isn't recommended. There are several options for developing and testing a Q-app before publishing to mainnet:
|
||||
|
||||
### Preview mode
|
||||
|
||||
Select "Preview" in the UI after choosing the zip. This allows for full Q-App testing without the need to publish any data.
|
||||
|
||||
|
||||
### Testnets
|
||||
|
||||
For an end-to-end test of Q-App publishing, you can use the official testnet, or set up a single node testnet of your own (often referred to as devnet) on your local machine. See [Single Node Testnet Quick Start Guide](testnet/README.md#quick-start).
|
||||
|
||||
|
||||
### Debugging
|
||||
|
||||
It is recommended that you develop and test in a web browser, to allow access to the javascript console. To do this:
|
||||
1. Open the UI app, then minimise it.
|
||||
2. In a Chromium-based web browser, visit: http://localhost:12388/
|
||||
3. Log in to your account and then preview your app/website.
|
||||
4. Go to `View > Developer > JavaScript Console`. Here you can monitor console logs, errors, and network requests from your app, in the same way as any other web-app.
|
||||
|
35
README.md
35
README.md
@ -1,4 +1,19 @@
|
||||
# Qortal Project - Official Repo
|
||||
# Qortal Project - Qortal Core - Primary Repository
|
||||
The Qortal Core is the blockchain and node component of the overall project. It contains the primary API, and ability to make calls to create transactions, and interact with the Qortal Blockchain Network.
|
||||
|
||||
In order to run the Qortal Core, a machine with java 11+ installed is required. Minimum RAM specs will vary depending on settings, but as low as 4GB of RAM should be acceptable in most scenarios.
|
||||
|
||||
Qortal is a complete infrastructure platform with a blockchain backend, it is capable of indefinite web and application hosting with no continual fees, replacement of DNS and centralized name systems and communications systems, and is the foundation of the next generation digital infrastructure of the world. Qortal is unique in nearly every way, and was written from scratch to address as many concerns from both the existing 'blockchain space' and the 'typical internet' as possible, while maintaining a system that is easy to use and able to run on 'any' computer.
|
||||
|
||||
Qortal contains extensive functionality geared toward complete decentralization of the digital world. Removal of 'middlemen' of any kind from all transactions, and ability to publish websites and applications that require no continual fees, on a name that is truly owned by the account that registered it, or purchased it from another. A single name on Qortal is capable of being both a namespace and a 'username'. That single name can have an application, website, public and private data, communications, authentication, the namespace itself and more, which can subsequently be sold to anyone else without the need to change any type of 'hosting' or DNS entries that do not exist, email that doesn't exist, etc. Maintaining the same functionality as those replaced features of web 2.0.
|
||||
|
||||
Over time Qortal has progressed into a fully featured environment catering to any and all types of people and organizations, and will continue to advance as time goes on. Brining more features, capability, device support, and availale replacements for web2.0. Ultimately building a new, completely user-controlled digital world without limits.
|
||||
|
||||
Qortal has no owner, no company on top of it, and is completely community built, run, and funded. A community-established and run group of developers known as the 'dev-group' or Qortal Development Group, make group_approval based decisions for the project's future. If you are a developer interested in assisting with the project, you meay reach out to the Qortal Development Group in any of the available Qortal community locations. Either on the Qortal network itself, or on one of the temporary centralized social media locations.
|
||||
|
||||
Building the future one block at a time. Welcome to Qortal.
|
||||
|
||||
# Building the Qortal Core from Source
|
||||
|
||||
## Build / run
|
||||
|
||||
@ -10,3 +25,21 @@
|
||||
- Run JAR in same working directory as *settings.json*: `java -jar target/qortal-1.0.jar`
|
||||
- Wrap in shell script, add JVM flags, redirection, backgrounding, etc. as necessary.
|
||||
- Or use supplied example shell script: *start.sh*
|
||||
|
||||
# Using a pre-built Qortal 'jar' binary
|
||||
|
||||
If you would prefer to utilize a released version of Qortal, you may do so by downloading one of the available releases from the releases page, that are also linked on https://qortal.org and https://qortal.dev.
|
||||
|
||||
# Learning Q-App Development
|
||||
|
||||
https://qortal.dev contains dev documentation for building JS/React (and other client-side languages) applications or 'Q-Apps' on Qortal. Q-Apps are published on Registered Qortal Names, and aside from a single Name Registration fee, and a fraction of QORT for a publish transaction, require zero continual costs. These applications get more redundant with each new access from a new Qortal Node, making your application faster for the next user to download, and stronger as time goes on. Q-Apps live indefinitely in the history of the blockchain-secured Qortal Data Network (QDN).
|
||||
|
||||
# How to learn more
|
||||
|
||||
If the project interests you, you may learn more from the various web2 and QDN based websites focused on introductory information.
|
||||
|
||||
https://qortal.org - primary internet presence
|
||||
https://qortal.dev - secondary and development focused website with links to many new developments and documentation
|
||||
https://wiki.qortal.org - community built and managed wiki with detailed information regarding the project
|
||||
|
||||
links to telegram and discord communities are at the top of https://qortal.org as well.
|
||||
|
@ -1,7 +1,7 @@
|
||||
rootLogger.level = info
|
||||
# On Windows, uncomment next line to set dirname:
|
||||
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
|
||||
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
|
||||
# property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
|
||||
|
||||
rootLogger.appenderRef.console.ref = stdout
|
||||
rootLogger.appenderRef.rolling.ref = FILE
|
||||
@ -59,11 +59,14 @@ appender.console.filter.threshold.level = error
|
||||
|
||||
appender.rolling.type = RollingFile
|
||||
appender.rolling.name = FILE
|
||||
appender.rolling.fileName = qortal.log
|
||||
appender.rolling.filePattern = qortal.%d{dd-MMM}.log.gz
|
||||
appender.rolling.layout.type = PatternLayout
|
||||
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.rolling.filePattern = ./${filename}.%i
|
||||
appender.rolling.policy.type = SizeBasedTriggeringPolicy
|
||||
appender.rolling.policy.size = 4MB
|
||||
appender.rolling.policy.size = 10MB
|
||||
appender.rolling.strategy.type = DefaultRolloverStrategy
|
||||
appender.rolling.strategy.max = 7
|
||||
# Set the immediate flush to true (default)
|
||||
# appender.rolling.immediateFlush = true
|
||||
# Set the append to true (default), should not overwrite
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,8 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* AdvancedInstaller v16 or better, and enterprise licence if translations are required
|
||||
* Installed AdoptOpenJDK v11 64bit, full JDK *not* JRE
|
||||
* AdvancedInstaller v19.4 or better, and enterprise licence if translations are required
|
||||
* Installed AdoptOpenJDK v17 64bit, full JDK *not* JRE
|
||||
|
||||
## General build instructions
|
||||
|
||||
@ -15,10 +15,8 @@ Typical build procedure:
|
||||
* Place the `qortal.jar` file in `Install-Files\`
|
||||
* Open AdvancedInstaller with qortal.aip file
|
||||
* If releasing a new version, change version number in:
|
||||
+ "Product Information" side menu
|
||||
+ "Product Details" side menu entry
|
||||
+ "Product Details" tab in "Product Details" pane
|
||||
+ "Product Version" entry box
|
||||
* Click away to a different side menu entry, e.g. "Resources" -> "Files and Folders"
|
||||
* You should be prompted whether to generate a new product key, click "Generate New"
|
||||
* Click "Build" button
|
||||
|
BIN
lib/com/dosse/WaifUPnP/1.2/WaifUPnP-1.2.jar
Normal file
BIN
lib/com/dosse/WaifUPnP/1.2/WaifUPnP-1.2.jar
Normal file
Binary file not shown.
9
lib/com/dosse/WaifUPnP/1.2/WaifUPnP-1.2.pom
Normal file
9
lib/com/dosse/WaifUPnP/1.2/WaifUPnP-1.2.pom
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.dosse</groupId>
|
||||
<artifactId>WaifUPnP</artifactId>
|
||||
<version>1.2</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
@ -3,10 +3,11 @@
|
||||
<groupId>com.dosse</groupId>
|
||||
<artifactId>WaifUPnP</artifactId>
|
||||
<versioning>
|
||||
<release>1.1</release>
|
||||
<release>1.2</release>
|
||||
<versions>
|
||||
<version>1.1</version>
|
||||
<version>1.2</version>
|
||||
</versions>
|
||||
<lastUpdated>20220218200127</lastUpdated>
|
||||
<lastUpdated>20231026200127</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
BIN
lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar
Normal file
BIN
lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar
Normal file
Binary file not shown.
123
lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom
Normal file
123
lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom
Normal file
@ -0,0 +1,123 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.4.1</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<skipTests>false</skipTests>
|
||||
<bouncycastle.version>1.69</bouncycastle.version>
|
||||
<junit.version>4.13.2</junit.version>
|
||||
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
|
||||
<maven-jar-plugin.version>3.3.0</maven-jar-plugin.version>
|
||||
<maven-javadoc-plugin.version>3.6.3</maven-javadoc-plugin.version>
|
||||
<maven-source-plugin.version>3.3.0</maven-source-plugin.version>
|
||||
<maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
<testSourceDirectory>src/test/java</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${maven-source-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${maven-javadoc-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${maven-source-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${maven-javadoc-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
BIN
lib/org/ciyam/AT/1.4.2/AT-1.4.2.jar
Normal file
BIN
lib/org/ciyam/AT/1.4.2/AT-1.4.2.jar
Normal file
Binary file not shown.
123
lib/org/ciyam/AT/1.4.2/AT-1.4.2.pom
Normal file
123
lib/org/ciyam/AT/1.4.2/AT-1.4.2.pom
Normal file
@ -0,0 +1,123 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.4.2</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<skipTests>false</skipTests>
|
||||
<bouncycastle.version>1.70</bouncycastle.version>
|
||||
<junit.version>4.13.2</junit.version>
|
||||
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
|
||||
<maven-source-plugin.version>3.3.0</maven-source-plugin.version>
|
||||
<maven-javadoc-plugin.version>3.6.3</maven-javadoc-plugin.version>
|
||||
<maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>
|
||||
<maven-jar-plugin.version>3.4.1</maven-jar-plugin.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
<testSourceDirectory>src/test/java</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${maven-source-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${maven-javadoc-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>${maven-source-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>${maven-javadoc-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -3,15 +3,14 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.4.0</release>
|
||||
<release>1.4.2</release>
|
||||
<versions>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.6</version>
|
||||
<version>1.3.7</version>
|
||||
<version>1.3.8</version>
|
||||
<version>1.4.0</version>
|
||||
<version>1.4.1</version>
|
||||
<version>1.4.2</version>
|
||||
</versions>
|
||||
<lastUpdated>20221105114346</lastUpdated>
|
||||
<lastUpdated>20240426084210</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
@ -1,7 +1,7 @@
|
||||
rootLogger.level = info
|
||||
# On Windows, uncomment next line to set dirname:
|
||||
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
|
||||
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
|
||||
# property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
|
||||
|
||||
rootLogger.appenderRef.console.ref = stdout
|
||||
rootLogger.appenderRef.rolling.ref = FILE
|
||||
@ -59,11 +59,14 @@ appender.console.filter.threshold.level = error
|
||||
|
||||
appender.rolling.type = RollingFile
|
||||
appender.rolling.name = FILE
|
||||
appender.rolling.fileName = qortal.log
|
||||
appender.rolling.filePattern = qortal.%d{dd-MMM}.log.gz
|
||||
appender.rolling.layout.type = PatternLayout
|
||||
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.rolling.filePattern = ./${filename}.%i
|
||||
appender.rolling.policy.type = SizeBasedTriggeringPolicy
|
||||
appender.rolling.policy.size = 4MB
|
||||
appender.rolling.policy.size = 10MB
|
||||
appender.rolling.strategy.type = DefaultRolloverStrategy
|
||||
appender.rolling.strategy.max = 7
|
||||
# Set the immediate flush to true (default)
|
||||
# appender.rolling.immediateFlush = true
|
||||
# Set the append to true (default), should not overwrite
|
||||
|
195
pom.xml
195
pom.xml
@ -3,40 +3,61 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.9.1</version>
|
||||
<version>4.6.6</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<skipTests>true</skipTests>
|
||||
|
||||
<altcoinj.version>7dc8c6f</altcoinj.version>
|
||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||
<bouncycastle.version>1.69</bouncycastle.version>
|
||||
<bouncycastle.version>1.70</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1.4.0</ciyam-at.version>
|
||||
<commons-net.version>3.6</commons-net.version>
|
||||
<commons-text.version>1.8</commons-text.version>
|
||||
<commons-io.version>2.6</commons-io.version>
|
||||
<commons-compress.version>1.21</commons-compress.version>
|
||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||
<xz.version>1.9</xz.version>
|
||||
<ciyam-at.version>1.4.2</ciyam-at.version>
|
||||
<commons-net.version>3.8.0</commons-net.version>
|
||||
<commons-text.version>1.12.0</commons-text.version>
|
||||
<commons-io.version>2.18.0</commons-io.version>
|
||||
<commons-compress.version>1.27.1</commons-compress.version>
|
||||
<commons-lang3.version>3.17.0</commons-lang3.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
<guava.version>28.1-jre</guava.version>
|
||||
<hsqldb.version>2.5.1</hsqldb.version>
|
||||
<extendedset.version>0.12.3</extendedset.version>
|
||||
<git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
|
||||
<grpc.version>1.68.1</grpc.version>
|
||||
<guava.version>33.3.1-jre</guava.version>
|
||||
<hamcrest-library.version>2.2</hamcrest-library.version>
|
||||
<homoglyph.version>1.2.1</homoglyph.version>
|
||||
<icu4j.version>70.1</icu4j.version>
|
||||
<upnp.version>1.1</upnp.version>
|
||||
<jersey.version>2.29.1</jersey.version>
|
||||
<jetty.version>9.4.29.v20200521</jetty.version>
|
||||
<log4j.version>2.17.1</log4j.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<slf4j.version>1.7.12</slf4j.version>
|
||||
<swagger-api.version>2.0.9</swagger-api.version>
|
||||
<swagger-ui.version>3.23.8</swagger-ui.version>
|
||||
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
|
||||
<jsoup.version>1.13.1</jsoup.version>
|
||||
<java-diff-utils.version>4.10</java-diff-utils.version>
|
||||
<grpc.version>1.45.1</grpc.version>
|
||||
<protobuf.version>3.19.4</protobuf.version>
|
||||
<hsqldb.version>2.7.4</hsqldb.version>
|
||||
<icu4j.version>76.1</icu4j.version>
|
||||
<java-diff-utils.version>4.15</java-diff-utils.version>
|
||||
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
|
||||
<jaxb-runtime.version>2.3.9</jaxb-runtime.version>
|
||||
<jersey.version>2.42</jersey.version>
|
||||
<jetty.version>9.4.56.v20240826</jetty.version>
|
||||
<json-simple.version>1.1.1</json-simple.version>
|
||||
<json.version>20240303</json.version>
|
||||
<jsoup.version>1.18.1</jsoup.version>
|
||||
<junit-jupiter-engine.version>5.11.0-M2</junit-jupiter-engine.version>
|
||||
<lifecycle-mapping.version>1.0.0</lifecycle-mapping.version>
|
||||
<log4j.version>2.23.1</log4j.version>
|
||||
<mail.version>1.5.0-b01</mail.version>
|
||||
<maven-build-helper-plugin.version>3.6.0</maven-build-helper-plugin.version>
|
||||
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
|
||||
<maven-dependency-plugin.version>3.6.1</maven-dependency-plugin.version>
|
||||
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
|
||||
<maven-package-info-plugin.version>1.1.0</maven-package-info-plugin.version>
|
||||
<maven-plugin.version>2.18.0</maven-plugin.version>
|
||||
<maven-reproducible-build-plugin.version>0.17</maven-reproducible-build-plugin.version>
|
||||
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
|
||||
<maven-shade-plugin.version>3.6.0</maven-shade-plugin.version>
|
||||
<maven-surefire-plugin.version>3.5.2</maven-surefire-plugin.version>
|
||||
<protobuf.version>3.25.3</protobuf.version>
|
||||
<replacer.version>1.5.3</replacer.version>
|
||||
<simplemagic.version>1.17</simplemagic.version>
|
||||
<slf4j.version>1.7.36</slf4j.version>
|
||||
<swagger-api.version>2.0.10</swagger-api.version>
|
||||
<swagger-ui.version>5.18.2</swagger-ui.version>
|
||||
<upnp.version>1.2</upnp.version>
|
||||
<xz.version>1.10</xz.version>
|
||||
</properties>
|
||||
<build>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
@ -51,14 +72,14 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>versions-maven-plugin</artifactId>
|
||||
<version>2.5</version>
|
||||
<version>${maven-plugin.version}</version>
|
||||
<configuration>
|
||||
<generateBackupPoms>false</generateBackupPoms>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<release>11</release>
|
||||
</configuration>
|
||||
@ -89,7 +110,7 @@
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>${git-commit-id-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>get-the-git-infos</id>
|
||||
@ -121,7 +142,7 @@
|
||||
<plugin>
|
||||
<groupId>com.google.code.maven-replacer-plugin</groupId>
|
||||
<artifactId>replacer</artifactId>
|
||||
<version>1.5.3</version>
|
||||
<version>${replacer.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>replace-swagger-ui</id>
|
||||
@ -132,22 +153,34 @@
|
||||
<inherited>false</inherited>
|
||||
<configuration>
|
||||
<file>${project.build.directory}/swagger-ui.unpacked/META-INF/resources/webjars/swagger-ui/${swagger-ui.version}/index.html</file>
|
||||
<replacements>
|
||||
<replacement>
|
||||
<token>Swagger UI</token>
|
||||
<value>Qortal API Documentation</value>
|
||||
</replacement>
|
||||
</replacements>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>replace-swagger-ui-json</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>replace</goal>
|
||||
</goals>
|
||||
<inherited>false</inherited>
|
||||
<configuration>
|
||||
<file>${project.build.directory}/swagger-ui.unpacked/META-INF/resources/webjars/swagger-ui/${swagger-ui.version}/swagger-initializer.js</file>
|
||||
<replacements>
|
||||
<replacement>
|
||||
<token>https://petstore.swagger.io/v2/swagger.json</token>
|
||||
<value>/openapi.json</value>
|
||||
</replacement>
|
||||
<replacement>
|
||||
<token>Swagger UI</token>
|
||||
<value>API Documentation</value>
|
||||
</replacement>
|
||||
<replacement>
|
||||
<token>deepLinking: true,</token>
|
||||
<value>
|
||||
deepLinking: true,
|
||||
tagsSorter: "alpha",
|
||||
operationsSorter:
|
||||
"alpha",
|
||||
operationsSorter: "alpha",
|
||||
validatorUrl: false,
|
||||
</value>
|
||||
</replacement>
|
||||
@ -164,11 +197,13 @@
|
||||
<configuration>
|
||||
<file>${project.build.outputDirectory}/git.properties</file>
|
||||
<regex>true</regex>
|
||||
<regexFlags><regexFlag>MULTILINE</regexFlag></regexFlags>
|
||||
<regexFlags>
|
||||
<regexFlag>MULTILINE</regexFlag>
|
||||
</regexFlags>
|
||||
<replacements>
|
||||
<replacement>
|
||||
<token>^(#.*$[\n\r]*)</token>
|
||||
<value></value>
|
||||
<value/>
|
||||
</replacement>
|
||||
</replacements>
|
||||
</configuration>
|
||||
@ -178,7 +213,10 @@
|
||||
<!-- add swagger-ui as resource to output package -->
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<version>${maven-resources-plugin.version}</version>
|
||||
<configuration>
|
||||
<propertiesEncoding>ISO-8859-1</propertiesEncoding>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-resources</id>
|
||||
@ -202,7 +240,7 @@
|
||||
<plugin>
|
||||
<groupId>com.github.bohnman</groupId>
|
||||
<artifactId>package-info-maven-plugin</artifactId>
|
||||
<version>${package-info-maven-plugin.version}</version>
|
||||
<version>${maven-package-info-plugin.version}</version>
|
||||
<configuration>
|
||||
<packages>
|
||||
<package>
|
||||
@ -232,7 +270,7 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<version>${maven-build-helper-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-sources</phase>
|
||||
@ -250,7 +288,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
@ -268,13 +306,12 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>2.4.3</version>
|
||||
<version>${maven-shade-plugin.version}</version>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<!-- Don't include original swagger-UI as we're including our own
|
||||
modified version -->
|
||||
<!-- Don't include original swagger-UI as we're including our own modified version -->
|
||||
<exclude>org.webjars:swagger-ui</exclude>
|
||||
<!-- Don't include JUnit as it's for testing only! -->
|
||||
<exclude>junit:junit</exclude>
|
||||
@ -318,7 +355,7 @@
|
||||
<plugin>
|
||||
<groupId>io.github.zlika</groupId>
|
||||
<artifactId>reproducible-build-maven-plugin</artifactId>
|
||||
<version>0.11</version>
|
||||
<version>${maven-reproducible-build-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
@ -335,7 +372,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
@ -347,46 +384,34 @@
|
||||
<plugin>
|
||||
<groupId>org.eclipse.m2e</groupId>
|
||||
<artifactId>lifecycle-mapping</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>${lifecycle-mapping.version}</version>
|
||||
<configuration>
|
||||
<lifecycleMappingMetadata>
|
||||
<pluginExecutions>
|
||||
<pluginExecution>
|
||||
<pluginExecutionFilter>
|
||||
<groupId>
|
||||
org.apache.maven.plugins
|
||||
</groupId>
|
||||
<artifactId>
|
||||
maven-dependency-plugin
|
||||
</artifactId>
|
||||
<versionRange>
|
||||
[2.8,)
|
||||
</versionRange>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>${maven-dependency-plugin.version}</version>
|
||||
<goals>
|
||||
<goal>unpack</goal>
|
||||
</goals>
|
||||
</pluginExecutionFilter>
|
||||
<action>
|
||||
<execute></execute>
|
||||
<execute/>
|
||||
</action>
|
||||
</pluginExecution>
|
||||
<pluginExecution>
|
||||
<pluginExecutionFilter>
|
||||
<groupId>
|
||||
com.google.code.maven-replacer-plugin
|
||||
</groupId>
|
||||
<artifactId>
|
||||
replacer
|
||||
</artifactId>
|
||||
<versionRange>
|
||||
[1.5.3,)
|
||||
</versionRange>
|
||||
<groupId>com.google.code.maven-replacer-plugin</groupId>
|
||||
<artifactId>replacer</artifactId>
|
||||
<version>${replacer.version}</version>
|
||||
<goals>
|
||||
<goal>replace</goal>
|
||||
</goals>
|
||||
</pluginExecutionFilter>
|
||||
<action>
|
||||
<execute></execute>
|
||||
<execute/>
|
||||
</action>
|
||||
</pluginExecution>
|
||||
</pluginExecutions>
|
||||
@ -413,15 +438,17 @@
|
||||
<dependency>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<scope>provided</scope><!-- needed for build, not for runtime -->
|
||||
<version>${maven-build-helper-plugin.version}</version>
|
||||
<scope>provided</scope>
|
||||
<!-- needed for build, not for runtime -->
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.github.bohnman/package-info-maven-plugin -->
|
||||
<dependency>
|
||||
<groupId>com.github.bohnman</groupId>
|
||||
<artifactId>package-info-maven-plugin</artifactId>
|
||||
<version>${package-info-maven-plugin.version}</version>
|
||||
<scope>provided</scope><!-- needed for build, not for runtime -->
|
||||
<version>${maven-package-info-plugin.version}</version>
|
||||
<scope>provided</scope>
|
||||
<!-- needed for build, not for runtime -->
|
||||
</dependency>
|
||||
<!-- HSQLDB for repository -->
|
||||
<dependency>
|
||||
@ -457,12 +484,12 @@
|
||||
<dependency>
|
||||
<groupId>com.googlecode.json-simple</groupId>
|
||||
<artifactId>json-simple</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<version>${json-simple.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20210307</version>
|
||||
<version>${json.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
@ -493,7 +520,7 @@
|
||||
<dependency>
|
||||
<groupId>io.druid</groupId>
|
||||
<artifactId>extendedset</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<version>${extendedset.version}</version>
|
||||
<exclusions>
|
||||
<!-- exclude old versions of jackson-annotations / jackson-core -->
|
||||
<exclusion>
|
||||
@ -564,12 +591,12 @@
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<version>${javax.servlet-api.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.mail</groupId>
|
||||
<artifactId>mail</artifactId>
|
||||
<version>1.5.0-b01</version>
|
||||
<version>${mail.version}</version>
|
||||
</dependency>
|
||||
<!-- Unicode homoglyph utilities -->
|
||||
<dependency>
|
||||
@ -638,7 +665,8 @@
|
||||
<artifactId>jersey-hk2</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
<exclusions>
|
||||
<exclusion><!-- exclude javax.inject-1.jar because other jersey modules include javax.inject v2+ -->
|
||||
<exclusion>
|
||||
<!-- exclude javax.inject-1.jar because other jersey modules include javax.inject v2+ -->
|
||||
<groupId>javax.inject</groupId>
|
||||
<artifactId>javax.inject</artifactId>
|
||||
</exclusion>
|
||||
@ -665,7 +693,8 @@
|
||||
<artifactId>swagger-jaxrs2-servlet-initializer</artifactId>
|
||||
<version>${swagger-api.version}</version>
|
||||
<exclusions>
|
||||
<exclusion><!-- excluded because included in swagger-jaxrs2-servlet-initializer -->
|
||||
<exclusion>
|
||||
<!-- excluded because included in swagger-jaxrs2-servlet-initializer -->
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-integration</artifactId>
|
||||
</exclusion>
|
||||
@ -681,12 +710,12 @@
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.3.1</version>
|
||||
<version>${junit-jupiter-engine.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-library</artifactId>
|
||||
<version>1.3</version>
|
||||
<version>${hamcrest-library.version}</version>
|
||||
</dependency>
|
||||
-->
|
||||
<!-- BouncyCastle for crypto, including TLS secure networking -->
|
||||
@ -735,5 +764,11 @@
|
||||
<artifactId>simplemagic</artifactId>
|
||||
<version>${simplemagic.version}</version>
|
||||
</dependency>
|
||||
<!-- JAXB runtime for WADL support -->
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
<version>${jaxb-runtime.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -5290,7 +5290,7 @@ public final class Service {
|
||||
if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(vendor_)) {
|
||||
com.google.protobuf.GeneratedMessageV3.writeString(output, 2, vendor_);
|
||||
}
|
||||
if (taddrSupport_ != false) {
|
||||
if (taddrSupport_) {
|
||||
output.writeBool(3, taddrSupport_);
|
||||
}
|
||||
if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(chainName_)) {
|
||||
@ -5341,7 +5341,7 @@ public final class Service {
|
||||
if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(vendor_)) {
|
||||
size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, vendor_);
|
||||
}
|
||||
if (taddrSupport_ != false) {
|
||||
if (taddrSupport_) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBoolSize(3, taddrSupport_);
|
||||
}
|
||||
@ -5729,7 +5729,7 @@ public final class Service {
|
||||
vendor_ = other.vendor_;
|
||||
onChanged();
|
||||
}
|
||||
if (other.getTaddrSupport() != false) {
|
||||
if (other.getTaddrSupport()) {
|
||||
setTaddrSupport(other.getTaddrSupport());
|
||||
}
|
||||
if (!other.getChainName().isEmpty()) {
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.hsqldb.jdbc;
|
||||
|
||||
import org.hsqldb.jdbc.pool.JDBCPooledConnection;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.hsqldb.jdbc.pool.JDBCPooledConnection;
|
||||
|
||||
public class HSQLDBPool extends JDBCPool {
|
||||
|
||||
public HSQLDBPool(int poolSize) {
|
||||
|
227
src/main/java/org/qortal/ApplyBootstrap.java
Normal file
227
src/main/java/org/qortal/ApplyBootstrap.java
Normal file
@ -0,0 +1,227 @@
|
||||
package org.qortal;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.api.ApiKey;
|
||||
import org.qortal.api.ApiRequest;
|
||||
import org.qortal.controller.BootstrapNode;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.Security;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.qortal.controller.BootstrapNode.AGENTLIB_JVM_HOLDER_ARG;
|
||||
|
||||
public class ApplyBootstrap {
|
||||
|
||||
static {
|
||||
// This static block will be called before others if using ApplyBootstrap.main()
|
||||
|
||||
// Log into different files for bootstrap - this has to be before LogManger.getLogger() calls
|
||||
System.setProperty("log4j2.filenameTemplate", "log-apply-bootstrap.txt");
|
||||
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ApplyBootstrap.class);
|
||||
private static final String JAR_FILENAME = BootstrapNode.JAR_FILENAME;
|
||||
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
|
||||
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
|
||||
private static final String JAVA_TOOL_OPTIONS_VALUE = "";
|
||||
|
||||
private static final long CHECK_INTERVAL = 15 * 1000L; // ms
|
||||
private static final int MAX_ATTEMPTS = 20;
|
||||
|
||||
public static void main(String[] args) {
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
if (args.length > 0)
|
||||
Settings.fileInstance(args[0]);
|
||||
else
|
||||
Settings.getInstance();
|
||||
|
||||
LOGGER.info("Applying bootstrap...");
|
||||
|
||||
// Shutdown node using API
|
||||
if (!shutdownNode())
|
||||
return;
|
||||
|
||||
// Delete db
|
||||
deleteDB();
|
||||
|
||||
// Restart node
|
||||
restartNode(args);
|
||||
|
||||
LOGGER.info("Bootstrapping...");
|
||||
}
|
||||
|
||||
private static boolean shutdownNode() {
|
||||
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
|
||||
LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri));
|
||||
|
||||
// The /admin/stop endpoint requires an API key, which may or may not be already generated
|
||||
boolean apiKeyNewlyGenerated = false;
|
||||
ApiKey apiKey = null;
|
||||
try {
|
||||
apiKey = new ApiKey();
|
||||
if (!apiKey.generated()) {
|
||||
apiKey.generate();
|
||||
apiKeyNewlyGenerated = true;
|
||||
LOGGER.info("Generated API key");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Error loading API key: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Create GET params
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (apiKey != null) {
|
||||
params.put("apiKey", apiKey.toString());
|
||||
}
|
||||
|
||||
// Attempt to stop the node
|
||||
int attempt;
|
||||
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
||||
final int attemptForLogging = attempt;
|
||||
LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS));
|
||||
String response = ApiRequest.perform(baseUri + "admin/stop", params);
|
||||
if (response == null) {
|
||||
// No response - consider node shut down
|
||||
if (apiKeyNewlyGenerated) {
|
||||
// API key was newly generated for bootstrapping node, so we need to remove it
|
||||
ApplyBootstrap.removeGeneratedApiKey();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGGER.info(() -> String.format("Response from API: %s", response));
|
||||
|
||||
try {
|
||||
Thread.sleep(CHECK_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
// We still need to check...
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (apiKeyNewlyGenerated) {
|
||||
// API key was newly generated for bootstrapping node, so we need to remove it
|
||||
ApplyBootstrap.removeGeneratedApiKey();
|
||||
}
|
||||
|
||||
if (attempt == MAX_ATTEMPTS) {
|
||||
LOGGER.error("Failed to shutdown node - giving up");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void removeGeneratedApiKey() {
|
||||
try {
|
||||
LOGGER.info("Removing newly generated API key...");
|
||||
|
||||
// Delete the API key since it was only generated for bootstrapping node
|
||||
ApiKey apiKey = new ApiKey();
|
||||
apiKey.delete();
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Error loading or deleting API key: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteDB() {
|
||||
// Get the repository path from settings
|
||||
String repositoryPath = Settings.getInstance().getRepositoryPath();
|
||||
LOGGER.debug(String.format("Repository path: %s", repositoryPath));
|
||||
|
||||
try {
|
||||
Path directory = Paths.get(repositoryPath);
|
||||
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Error deleting DB: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void restartNode(String[] args) {
|
||||
String javaHome = System.getProperty("java.home");
|
||||
LOGGER.debug(() -> String.format("Java home: %s", javaHome));
|
||||
|
||||
Path javaBinary = Paths.get(javaHome, "bin", "java");
|
||||
LOGGER.debug(() -> String.format("Java binary: %s", javaBinary));
|
||||
|
||||
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
|
||||
LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher));
|
||||
|
||||
List<String> javaCmd;
|
||||
if (Files.exists(exeLauncher)) {
|
||||
javaCmd = Arrays.asList(exeLauncher.toString());
|
||||
} else {
|
||||
javaCmd = new ArrayList<>();
|
||||
// Java runtime binary itself
|
||||
javaCmd.add(javaBinary.toString());
|
||||
|
||||
// JVM arguments
|
||||
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
||||
|
||||
// Reapply any retained, but disabled, -agentlib JVM arg
|
||||
javaCmd = javaCmd.stream()
|
||||
.map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Call mainClass in JAR
|
||||
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
|
||||
|
||||
// Add saved command-line args
|
||||
javaCmd.addAll(Arrays.asList(args));
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||
|
||||
if (Files.exists(exeLauncher)) {
|
||||
LOGGER.debug(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
|
||||
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
|
||||
}
|
||||
|
||||
// New process will inherit our stdout and stderr
|
||||
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
|
||||
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
|
||||
|
||||
Process process = processBuilder.start();
|
||||
|
||||
// Nothing to pipe to new process, so close output stream (process's stdin)
|
||||
process.getOutputStream().close();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
241
src/main/java/org/qortal/ApplyRestart.java
Normal file
241
src/main/java/org/qortal/ApplyRestart.java
Normal file
@ -0,0 +1,241 @@
|
||||
package org.qortal;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.api.ApiKey;
|
||||
import org.qortal.api.ApiRequest;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.RestartNode;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.Security;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.qortal.controller.RestartNode.AGENTLIB_JVM_HOLDER_ARG;
|
||||
|
||||
public class ApplyRestart {
|
||||
|
||||
static {
|
||||
// This static block will be called before others if using ApplyRestart.main()
|
||||
|
||||
// Log into different files for restart node - this has to be before LogManger.getLogger() calls
|
||||
System.setProperty("log4j2.filenameTemplate", "log-apply-restart.txt");
|
||||
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ApplyRestart.class);
|
||||
private static final String JAR_FILENAME = RestartNode.JAR_FILENAME;
|
||||
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
|
||||
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
|
||||
private static final String JAVA_TOOL_OPTIONS_VALUE = "";
|
||||
|
||||
private static final long CHECK_INTERVAL = 30 * 1000L; // ms
|
||||
private static final int MAX_ATTEMPTS = 12;
|
||||
|
||||
public static void main(String[] args) {
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
if (args.length > 0)
|
||||
Settings.fileInstance(args[0]);
|
||||
else
|
||||
Settings.getInstance();
|
||||
|
||||
LOGGER.info("Applying restart this can take up to 5 minutes...");
|
||||
|
||||
// Shutdown node using API
|
||||
if (!shutdownNode())
|
||||
return;
|
||||
|
||||
try {
|
||||
// Give some time for shutdown
|
||||
TimeUnit.SECONDS.sleep(60);
|
||||
|
||||
// Remove blockchain lock if exist
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (blockchainLock.isLocked())
|
||||
blockchainLock.unlock();
|
||||
|
||||
// Remove blockchain lock file if still exist
|
||||
TimeUnit.SECONDS.sleep(60);
|
||||
deleteLock();
|
||||
|
||||
// Restart node
|
||||
TimeUnit.SECONDS.sleep(15);
|
||||
restartNode(args);
|
||||
|
||||
LOGGER.info("Restarting...");
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.error("Unable to restart", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shutdownNode() {
|
||||
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
|
||||
LOGGER.debug(() -> String.format("Shutting down node using API via %s", baseUri));
|
||||
|
||||
// The /admin/stop endpoint requires an API key, which may or may not be already generated
|
||||
boolean apiKeyNewlyGenerated = false;
|
||||
ApiKey apiKey = null;
|
||||
try {
|
||||
apiKey = new ApiKey();
|
||||
if (!apiKey.generated()) {
|
||||
apiKey.generate();
|
||||
apiKeyNewlyGenerated = true;
|
||||
LOGGER.info("Generated API key");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Error loading API key: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Create GET params
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (apiKey != null) {
|
||||
params.put("apiKey", apiKey.toString());
|
||||
}
|
||||
|
||||
// Attempt to stop the node
|
||||
int attempt;
|
||||
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
||||
final int attemptForLogging = attempt;
|
||||
LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS));
|
||||
String response = ApiRequest.perform(baseUri + "admin/stop", params);
|
||||
if (response == null) {
|
||||
// No response - consider node shut down
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(30);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (apiKeyNewlyGenerated) {
|
||||
// API key was newly generated for restarting node, so we need to remove it
|
||||
ApplyRestart.removeGeneratedApiKey();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGGER.info(() -> String.format("Response from API: %s", response));
|
||||
|
||||
try {
|
||||
Thread.sleep(CHECK_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
// We still need to check...
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (apiKeyNewlyGenerated) {
|
||||
// API key was newly generated for restarting node, so we need to remove it
|
||||
ApplyRestart.removeGeneratedApiKey();
|
||||
}
|
||||
|
||||
if (attempt == MAX_ATTEMPTS) {
|
||||
LOGGER.error("Failed to shutdown node - giving up");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void removeGeneratedApiKey() {
|
||||
try {
|
||||
LOGGER.info("Removing newly generated API key...");
|
||||
|
||||
// Delete the API key since it was only generated for restarting node
|
||||
ApiKey apiKey = new ApiKey();
|
||||
apiKey.delete();
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Error loading or deleting API key: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteLock() {
|
||||
// Get the repository path from settings
|
||||
String repositoryPath = Settings.getInstance().getRepositoryPath();
|
||||
LOGGER.debug(String.format("Repository path is: %s", repositoryPath));
|
||||
|
||||
try {
|
||||
Path root = Paths.get(repositoryPath);
|
||||
File lockFile = new File(root.resolve("blockchain.lck").toUri());
|
||||
LOGGER.debug("Lockfile is: {}", lockFile);
|
||||
FileUtils.forceDelete(FileUtils.getFile(lockFile));
|
||||
} catch (IOException e) {
|
||||
LOGGER.debug("Error deleting blockchain lock file: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void restartNode(String[] args) {
|
||||
String javaHome = System.getProperty("java.home");
|
||||
LOGGER.debug(() -> String.format("Java home: %s", javaHome));
|
||||
|
||||
Path javaBinary = Paths.get(javaHome, "bin", "java");
|
||||
LOGGER.debug(() -> String.format("Java binary: %s", javaBinary));
|
||||
|
||||
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
|
||||
LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher));
|
||||
|
||||
List<String> javaCmd;
|
||||
if (Files.exists(exeLauncher)) {
|
||||
javaCmd = List.of(exeLauncher.toString());
|
||||
} else {
|
||||
javaCmd = new ArrayList<>();
|
||||
|
||||
// Java runtime binary itself
|
||||
javaCmd.add(javaBinary.toString());
|
||||
|
||||
// JVM arguments
|
||||
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
||||
|
||||
// Reapply any retained, but disabled, -agentlib JVM arg
|
||||
javaCmd = javaCmd.stream()
|
||||
.map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Call mainClass in JAR
|
||||
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
|
||||
|
||||
// Add saved command-line args
|
||||
javaCmd.addAll(Arrays.asList(args));
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||
|
||||
if (Files.exists(exeLauncher)) {
|
||||
LOGGER.debug(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
|
||||
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
|
||||
}
|
||||
|
||||
// New process will inherit our stdout and stderr
|
||||
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
|
||||
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
|
||||
|
||||
Process process = processBuilder.start();
|
||||
|
||||
// Nothing to pipe to new process, so close output stream (process's stdin)
|
||||
process.getOutputStream().close();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,14 @@
|
||||
package org.qortal;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.api.ApiKey;
|
||||
import org.qortal.api.ApiRequest;
|
||||
import org.qortal.controller.AutoUpdate;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Files;
|
||||
@ -10,15 +19,6 @@ import java.security.Security;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.api.ApiKey;
|
||||
import org.qortal.api.ApiRequest;
|
||||
import org.qortal.controller.AutoUpdate;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import static org.qortal.controller.AutoUpdate.AGENTLIB_JVM_HOLDER_ARG;
|
||||
|
||||
public class ApplyUpdate {
|
||||
@ -38,7 +38,7 @@ public class ApplyUpdate {
|
||||
private static final String NEW_JAR_FILENAME = AutoUpdate.NEW_JAR_FILENAME;
|
||||
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
|
||||
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
|
||||
private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4";
|
||||
private static final String JAVA_TOOL_OPTIONS_VALUE = "";
|
||||
|
||||
private static final long CHECK_INTERVAL = 30 * 1000L; // ms
|
||||
private static final int MAX_ATTEMPTS = 12;
|
||||
@ -139,7 +139,7 @@ public class ApplyUpdate {
|
||||
apiKey.delete();
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Error loading or deleting API key: {}", e.getMessage());
|
||||
LOGGER.error("Error loading or deleting API key: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,13 +181,13 @@ public class ApplyUpdate {
|
||||
|
||||
private static void restartNode(String[] args) {
|
||||
String javaHome = System.getProperty("java.home");
|
||||
LOGGER.info(() -> String.format("Java home: %s", javaHome));
|
||||
LOGGER.debug(() -> String.format("Java home: %s", javaHome));
|
||||
|
||||
Path javaBinary = Paths.get(javaHome, "bin", "java");
|
||||
LOGGER.info(() -> String.format("Java binary: %s", javaBinary));
|
||||
LOGGER.debug(() -> String.format("Java binary: %s", javaBinary));
|
||||
|
||||
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
|
||||
LOGGER.info(() -> String.format("Windows EXE launcher: %s", exeLauncher));
|
||||
LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher));
|
||||
|
||||
List<String> javaCmd;
|
||||
if (Files.exists(exeLauncher)) {
|
||||
@ -213,12 +213,12 @@ public class ApplyUpdate {
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
LOGGER.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||
|
||||
if (Files.exists(exeLauncher)) {
|
||||
LOGGER.info(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
|
||||
LOGGER.debug(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
|
||||
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
package org.qortal;
|
||||
|
||||
import java.security.Security;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
@ -15,6 +12,9 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.security.Security;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class RepositoryMaintenance {
|
||||
|
||||
static {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.qortal;
|
||||
|
||||
import org.qortal.controller.AutoUpdate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@ -7,8 +9,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.qortal.controller.AutoUpdate;
|
||||
|
||||
public class XorUpdate {
|
||||
|
||||
private static final byte XOR_VALUE = AutoUpdate.XOR_VALUE;
|
||||
|
@ -1,10 +1,5 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import static org.qortal.utils.Amounts.prettyAmount;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.BlockChain;
|
||||
@ -12,11 +7,21 @@ import org.qortal.controller.LiteNode;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.GroupRepository;
|
||||
import org.qortal.repository.NameRepository;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.qortal.utils.Amounts.prettyAmount;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config
|
||||
public class Account {
|
||||
|
||||
@ -193,27 +198,76 @@ public class Account {
|
||||
|
||||
/** Returns whether account can be considered a "minting account".
|
||||
* <p>
|
||||
* To be considered a "minting account", the account needs to pass at least one of these tests:<br>
|
||||
* To be considered a "minting account", the account needs to pass some of these tests:<br>
|
||||
* <ul>
|
||||
* <li>account's level is at least <tt>minAccountLevelToMint</tt> from blockchain config</li>
|
||||
* <li>account has 'founder' flag set</li>
|
||||
* <li>account's address has registered a name</li>
|
||||
* <li>account's address is a member of the minter group</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* @param isGroupValidated true if this account has already been validated for MINTER Group membership
|
||||
* @return true if account can be considered "minting account"
|
||||
* @throws DataException
|
||||
*/
|
||||
public boolean canMint() throws DataException {
|
||||
public boolean canMint(boolean isGroupValidated) throws DataException {
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
if (accountData == null)
|
||||
return false;
|
||||
NameRepository nameRepository = this.repository.getNameRepository();
|
||||
GroupRepository groupRepository = this.repository.getGroupRepository();
|
||||
String myAddress = accountData.getAddress();
|
||||
|
||||
Integer level = accountData.getLevel();
|
||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
|
||||
return true;
|
||||
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
|
||||
int levelToMint = BlockChain.getInstance().getMinAccountLevelToMint();
|
||||
int level = accountData.getLevel();
|
||||
int groupIdToMint = BlockChain.getInstance().getMintingGroupId();
|
||||
int nameCheckHeight = BlockChain.getInstance().getOnlyMintWithNameHeight();
|
||||
int groupCheckHeight = BlockChain.getInstance().getGroupMemberCheckHeight();
|
||||
int removeNameCheckHeight = BlockChain.getInstance().getRemoveOnlyMintWithNameHeight();
|
||||
|
||||
// Founders can always mint, unless they have a penalty
|
||||
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||
return true;
|
||||
// Can only mint if:
|
||||
// Account's level is at least minAccountLevelToMint from blockchain config
|
||||
if (blockchainHeight < nameCheckHeight) {
|
||||
if (Account.isFounder(accountData.getFlags())) {
|
||||
return accountData.getBlocksMintedPenalty() == 0;
|
||||
} else {
|
||||
return level >= levelToMint;
|
||||
}
|
||||
}
|
||||
|
||||
// Can only mint on onlyMintWithNameHeight from blockchain config if:
|
||||
// Account's level is at least minAccountLevelToMint from blockchain config
|
||||
// Account's address has registered a name
|
||||
if (blockchainHeight >= nameCheckHeight && blockchainHeight < groupCheckHeight) {
|
||||
List<NameData> myName = nameRepository.getNamesByOwner(myAddress);
|
||||
if (Account.isFounder(accountData.getFlags())) {
|
||||
return accountData.getBlocksMintedPenalty() == 0 && !myName.isEmpty();
|
||||
} else {
|
||||
return level >= levelToMint && !myName.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// Can only mint on groupMemberCheckHeight from blockchain config if:
|
||||
// Account's level is at least minAccountLevelToMint from blockchain config
|
||||
// Account's address has registered a name
|
||||
// Account's address is a member of the minter group
|
||||
if (blockchainHeight >= groupCheckHeight && blockchainHeight < removeNameCheckHeight) {
|
||||
List<NameData> myName = nameRepository.getNamesByOwner(myAddress);
|
||||
if (Account.isFounder(accountData.getFlags())) {
|
||||
return accountData.getBlocksMintedPenalty() == 0 && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
|
||||
} else {
|
||||
return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
|
||||
}
|
||||
}
|
||||
|
||||
// Can only mint on removeOnlyMintWithNameHeight from blockchain config if:
|
||||
// Account's level is at least minAccountLevelToMint from blockchain config
|
||||
// Account's address is a member of the minter group
|
||||
if (blockchainHeight >= removeNameCheckHeight) {
|
||||
if (Account.isFounder(accountData.getFlags())) {
|
||||
return accountData.getBlocksMintedPenalty() == 0 && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
|
||||
} else {
|
||||
return level >= levelToMint && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -228,7 +282,6 @@ public class Account {
|
||||
return this.repository.getAccountRepository().getBlocksMintedPenaltyCount(this.address);
|
||||
}
|
||||
|
||||
|
||||
/** Returns whether account can build reward-shares.
|
||||
* <p>
|
||||
* To be able to create reward-shares, the account needs to pass at least one of these tests:<br>
|
||||
@ -242,6 +295,7 @@ public class Account {
|
||||
*/
|
||||
public boolean canRewardShare() throws DataException {
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
|
||||
if (accountData == null)
|
||||
return false;
|
||||
|
||||
@ -295,10 +349,28 @@ public class Account {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 'effective' minting level, or zero if reward-share does not exist.
|
||||
* Returns reward-share minting address, or unknown if reward-share does not exist.
|
||||
*
|
||||
* @param repository
|
||||
* @param rewardSharePublicKey
|
||||
* @return address or unknown
|
||||
* @throws DataException
|
||||
*/
|
||||
public static String getRewardShareMintingAddress(Repository repository, byte[] rewardSharePublicKey) throws DataException {
|
||||
// Find actual minter address
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(rewardSharePublicKey);
|
||||
|
||||
if (rewardShareData == null)
|
||||
return "Unknown";
|
||||
|
||||
return rewardShareData.getMinter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 'effective' minting level, or zero if reward-share does not exist.
|
||||
*
|
||||
* @param repository
|
||||
* @param rewardSharePublicKey
|
||||
* @return 0+
|
||||
* @throws DataException
|
||||
*/
|
||||
@ -311,6 +383,7 @@ public class Account {
|
||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||
return rewardShareMinter.getEffectiveMintingLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 'effective' minting level, with a fix for the zero level.
|
||||
* <p>
|
||||
|
@ -1,15 +1,15 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.BinaryOperator;
|
||||
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.BinaryOperator;
|
||||
|
||||
/**
|
||||
* Account lastReference caching
|
||||
* <p>
|
||||
|
@ -340,7 +340,7 @@ public class SelfSponsorshipAlgoV1 {
|
||||
return true;
|
||||
}
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||
return transactionDataList.size() == 0;
|
||||
return transactionDataList.isEmpty();
|
||||
}
|
||||
|
||||
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||
@ -363,5 +363,4 @@ public class SelfSponsorshipAlgoV1 {
|
||||
|
||||
return transactionDataList;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
249
src/main/java/org/qortal/account/SelfSponsorshipAlgoV2.java
Normal file
249
src/main/java/org/qortal/account/SelfSponsorshipAlgoV2.java
Normal file
@ -0,0 +1,249 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class SelfSponsorshipAlgoV2 {
|
||||
|
||||
private final long snapshotTimestampV1 = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
|
||||
private final long snapshotTimestampV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2SnapshotTimestamp();
|
||||
private final long referenceTimestamp = BlockChain.getInstance().getReferenceTimestampBlock();
|
||||
|
||||
private final boolean override;
|
||||
private final Repository repository;
|
||||
private final String address;
|
||||
|
||||
private int recentAssetSendCount = 0;
|
||||
private int recentSponsorshipCount = 0;
|
||||
|
||||
private final Set<String> assetAddresses = new LinkedHashSet<>();
|
||||
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
|
||||
private final Set<String> sponsorAddresses = new LinkedHashSet<>();
|
||||
|
||||
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
private List<TransferAssetTransactionData> transferAssetForAddress = new ArrayList<>();
|
||||
|
||||
public SelfSponsorshipAlgoV2(Repository repository, String address, boolean override) {
|
||||
this.repository = repository;
|
||||
this.address = address;
|
||||
this.override = override;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public Set<String> getPenaltyAddresses() {
|
||||
return this.penaltyAddresses;
|
||||
}
|
||||
|
||||
public void run() throws DataException {
|
||||
if (!override) {
|
||||
this.getAccountPrivs(this.address);
|
||||
}
|
||||
|
||||
if (override) {
|
||||
this.fetchTransferAssetForAddress(this.address);
|
||||
this.findRecentAssetSendCount();
|
||||
|
||||
if (this.recentAssetSendCount >= 6) {
|
||||
this.penaltyAddresses.add(this.address);
|
||||
this.penaltyAddresses.addAll(this.assetAddresses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void getAccountPrivs(String address) throws DataException {
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||
transferPrivsTransactions.removeIf(t -> t.getTimestamp() > this.referenceTimestamp || accountData.getAddress().equals(t.getRecipient()));
|
||||
|
||||
if (transferPrivsTransactions.isEmpty()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
this.penaltyAddresses.add(transferPrivsTransactionData.getRecipient());
|
||||
this.fetchSponsorshipRewardShares(transferPrivsTransactionData.getRecipient());
|
||||
this.findRecentSponsorshipCount();
|
||||
|
||||
if (this.recentSponsorshipCount >= 1) {
|
||||
this.penaltyAddresses.addAll(this.sponsorAddresses);
|
||||
}
|
||||
|
||||
String newAddress = this.getDestinationAccount(transferPrivsTransactionData.getRecipient());
|
||||
|
||||
while (newAddress != null) {
|
||||
// Found destination account
|
||||
this.penaltyAddresses.add(newAddress);
|
||||
this.fetchSponsorshipRewardShares(newAddress);
|
||||
this.findRecentSponsorshipCount();
|
||||
|
||||
if (this.recentSponsorshipCount >= 1) {
|
||||
this.penaltyAddresses.addAll(this.sponsorAddresses);
|
||||
}
|
||||
|
||||
newAddress = this.getDestinationAccount(newAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getDestinationAccount(String address) throws DataException {
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||
transferPrivsTransactions.removeIf(t -> t.getTimestamp() > this.referenceTimestamp || accountData.getAddress().equals(t.getRecipient()));
|
||||
|
||||
if (transferPrivsTransactions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (accountData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
|
||||
return transferPrivsTransactionData.getRecipient();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fetchSponsorshipRewardShares(String address) throws DataException {
|
||||
AccountData accountDataRs = this.repository.getAccountRepository().getAccount(address);
|
||||
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
|
||||
// Define relevant transactions
|
||||
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
|
||||
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, address, false);
|
||||
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
|
||||
|
||||
// Skip removals
|
||||
if (rewardShareTransactionData.getSharePercent() < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not sponsored by this account
|
||||
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountDataRs.getPublicKey())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip self shares
|
||||
if (Objects.equals(rewardShareTransactionData.getRecipient(), address)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean duplicateFound = false;
|
||||
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
|
||||
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
|
||||
// Duplicate
|
||||
duplicateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!duplicateFound) {
|
||||
sponsorshipRewardShares.add(rewardShareTransactionData);
|
||||
this.sponsorAddresses.add(rewardShareTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
|
||||
this.sponsorshipRewardShares = sponsorshipRewardShares;
|
||||
}
|
||||
|
||||
private void fetchTransferAssetForAddress(String address) throws DataException {
|
||||
List<TransferAssetTransactionData> transferAssetForAddress = new ArrayList<>();
|
||||
|
||||
// Define relevant transactions
|
||||
List<TransactionType> txTypes = List.of(TransactionType.TRANSFER_ASSET);
|
||||
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, address, false);
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV2);
|
||||
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getType() != TransactionType.TRANSFER_ASSET) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
|
||||
|
||||
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
|
||||
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
|
||||
// Outgoing transfer asset for this account
|
||||
transferAssetForAddress.add(transferAssetTransactionData);
|
||||
this.assetAddresses.add(transferAssetTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.transferAssetForAddress = transferAssetForAddress;
|
||||
}
|
||||
|
||||
private void findRecentSponsorshipCount() {
|
||||
int recentSponsorshipCount = 0;
|
||||
|
||||
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
|
||||
if (rewardShare.getTimestamp() >= this.snapshotTimestampV1) {
|
||||
recentSponsorshipCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.recentSponsorshipCount = recentSponsorshipCount;
|
||||
}
|
||||
|
||||
private void findRecentAssetSendCount() {
|
||||
int recentAssetSendCount = 0;
|
||||
|
||||
for (TransferAssetTransactionData assetSend : transferAssetForAddress) {
|
||||
if (assetSend.getTimestamp() >= this.snapshotTimestampV1) {
|
||||
recentAssetSendCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.recentAssetSendCount = recentAssetSendCount;
|
||||
}
|
||||
|
||||
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
|
||||
return fetchTransactions(repository,
|
||||
List.of(TransactionType.TRANSFER_PRIVS),
|
||||
address, true);
|
||||
}
|
||||
|
||||
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||
// Fetch all relevant transactions for this account
|
||||
List<byte[]> signatures = repository.getTransactionRepository()
|
||||
.getSignaturesMatchingCriteria(null, null, null, txTypes,
|
||||
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
|
||||
null, null, reverse);
|
||||
|
||||
List<TransactionData> transactionDataList = new ArrayList<>();
|
||||
|
||||
for (byte[] signature : signatures) {
|
||||
// Fetch transaction data
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null) {
|
||||
continue;
|
||||
}
|
||||
transactionDataList.add(transactionData);
|
||||
}
|
||||
|
||||
return transactionDataList;
|
||||
}
|
||||
}
|
370
src/main/java/org/qortal/account/SelfSponsorshipAlgoV3.java
Normal file
370
src/main/java/org/qortal/account/SelfSponsorshipAlgoV3.java
Normal file
@ -0,0 +1,370 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SelfSponsorshipAlgoV3 {
|
||||
|
||||
private final Repository repository;
|
||||
private final String address;
|
||||
private final AccountData accountData;
|
||||
private final long snapshotTimestampV1;
|
||||
private final long snapshotTimestampV3;
|
||||
private final boolean override;
|
||||
|
||||
private int registeredNameCount = 0;
|
||||
private int suspiciousCount = 0;
|
||||
private int suspiciousPercent = 0;
|
||||
private int consolidationCount = 0;
|
||||
private int bulkIssuanceCount = 0;
|
||||
private int recentSponsorshipCount = 0;
|
||||
|
||||
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
private final Map<String, List<TransactionData>> paymentsByAddress = new HashMap<>();
|
||||
private final Set<String> sponsees = new LinkedHashSet<>();
|
||||
private Set<String> consolidatedAddresses = new LinkedHashSet<>();
|
||||
private final Set<String> zeroTransactionAddreses = new LinkedHashSet<>();
|
||||
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
|
||||
|
||||
public SelfSponsorshipAlgoV3(Repository repository, String address, long snapshotTimestampV1, long snapshotTimestampV3, boolean override) throws DataException {
|
||||
this.repository = repository;
|
||||
this.address = address;
|
||||
this.accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
this.snapshotTimestampV1 = snapshotTimestampV1;
|
||||
this.snapshotTimestampV3 = snapshotTimestampV3;
|
||||
this.override = override;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public Set<String> getPenaltyAddresses() {
|
||||
return this.penaltyAddresses;
|
||||
}
|
||||
|
||||
|
||||
public void run() throws DataException {
|
||||
if (this.accountData == null) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchSponsorshipRewardShares();
|
||||
if (this.sponsorshipRewardShares.isEmpty()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
this.findConsolidatedRewards();
|
||||
this.findBulkIssuance();
|
||||
this.findRegisteredNameCount();
|
||||
this.findRecentSponsorshipCount();
|
||||
|
||||
int score = this.calculateScore();
|
||||
if (score <= 0 && !override) {
|
||||
return;
|
||||
}
|
||||
|
||||
String newAddress = this.getDestinationAccount(this.address);
|
||||
while (newAddress != null) {
|
||||
// Found destination account
|
||||
this.penaltyAddresses.add(newAddress);
|
||||
|
||||
// Run algo for this address, but in "override" mode because it has already been flagged
|
||||
SelfSponsorshipAlgoV3 algoV3 = new SelfSponsorshipAlgoV3(this.repository, newAddress, this.snapshotTimestampV1, this.snapshotTimestampV3, true);
|
||||
algoV3.run();
|
||||
this.penaltyAddresses.addAll(algoV3.getPenaltyAddresses());
|
||||
|
||||
newAddress = this.getDestinationAccount(newAddress);
|
||||
}
|
||||
|
||||
this.penaltyAddresses.add(this.address);
|
||||
|
||||
if (this.override || this.recentSponsorshipCount < 20) {
|
||||
this.penaltyAddresses.addAll(this.consolidatedAddresses);
|
||||
this.penaltyAddresses.addAll(this.zeroTransactionAddreses);
|
||||
}
|
||||
else {
|
||||
this.penaltyAddresses.addAll(this.sponsees);
|
||||
}
|
||||
}
|
||||
|
||||
private String getDestinationAccount(String address) throws DataException {
|
||||
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||
if (transferPrivsTransactions.isEmpty()) {
|
||||
// No TRANSFER_PRIVS transactions for this address
|
||||
return null;
|
||||
}
|
||||
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||
if (accountData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
|
||||
return transferPrivsTransactionData.getRecipient();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void findConsolidatedRewards() throws DataException {
|
||||
List<String> sponseesThatSentRewards = new ArrayList<>();
|
||||
Map<String, Integer> paymentRecipients = new HashMap<>();
|
||||
|
||||
// Collect outgoing payments of each sponsee
|
||||
for (String sponseeAddress : this.sponsees) {
|
||||
|
||||
// Firstly fetch all payments for address, since the functions below depend on this data
|
||||
this.fetchPaymentsForAddress(sponseeAddress);
|
||||
|
||||
// Check if the address has zero relevant transactions
|
||||
if (this.hasZeroTransactions(sponseeAddress)) {
|
||||
this.zeroTransactionAddreses.add(sponseeAddress);
|
||||
}
|
||||
|
||||
// Get payment recipients
|
||||
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||
if (allPaymentRecipients.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
sponseesThatSentRewards.add(sponseeAddress);
|
||||
|
||||
List<String> addressesPaidByThisSponsee = new ArrayList<>();
|
||||
for (String paymentRecipient : allPaymentRecipients) {
|
||||
if (addressesPaidByThisSponsee.contains(paymentRecipient)) {
|
||||
// We already tracked this association - don't allow multiple to stack up
|
||||
continue;
|
||||
}
|
||||
addressesPaidByThisSponsee.add(paymentRecipient);
|
||||
|
||||
// Increment count for this recipient, or initialize to 1 if not present
|
||||
if (paymentRecipients.computeIfPresent(paymentRecipient, (k, v) -> v + 1) == null) {
|
||||
paymentRecipients.put(paymentRecipient, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Exclude addresses with a low number of payments
|
||||
Map<String, Integer> filteredPaymentRecipients = paymentRecipients.entrySet().stream()
|
||||
.filter(p -> p.getValue() != null && p.getValue() >= 10)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
// Now check how many sponsees have sent to this subset of addresses
|
||||
Map<String, Integer> sponseesThatConsolidatedRewards = new HashMap<>();
|
||||
for (String sponseeAddress : sponseesThatSentRewards) {
|
||||
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||
// Remove any that aren't to one of the flagged recipients (i.e. consolidation)
|
||||
allPaymentRecipients.removeIf(r -> !filteredPaymentRecipients.containsKey(r));
|
||||
|
||||
int count = allPaymentRecipients.size();
|
||||
if (count == 0) {
|
||||
continue;
|
||||
}
|
||||
if (sponseesThatConsolidatedRewards.computeIfPresent(sponseeAddress, (k, v) -> v + count) == null) {
|
||||
sponseesThatConsolidatedRewards.put(sponseeAddress, count);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove sponsees that have only sent a low number of payments to the filtered addresses
|
||||
Map<String, Integer> filteredSponseesThatConsolidatedRewards = sponseesThatConsolidatedRewards.entrySet().stream()
|
||||
.filter(p -> p.getValue() != null && p.getValue() >= 2)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
this.consolidationCount = sponseesThatConsolidatedRewards.size();
|
||||
this.consolidatedAddresses = new LinkedHashSet<>(filteredSponseesThatConsolidatedRewards.keySet());
|
||||
this.suspiciousCount = this.consolidationCount + this.zeroTransactionAddreses.size();
|
||||
this.suspiciousPercent = (int)(this.suspiciousCount / (float) this.sponsees.size() * 100);
|
||||
}
|
||||
|
||||
private void findBulkIssuance() {
|
||||
Long lastTimestamp = null;
|
||||
for (RewardShareTransactionData rewardShareTransactionData : sponsorshipRewardShares) {
|
||||
long timestamp = rewardShareTransactionData.getTimestamp();
|
||||
if (timestamp >= this.snapshotTimestampV3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastTimestamp != null) {
|
||||
if (timestamp - lastTimestamp < 3*60*1000L) {
|
||||
this.bulkIssuanceCount++;
|
||||
}
|
||||
}
|
||||
lastTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
private void findRegisteredNameCount() throws DataException {
|
||||
int registeredNameCount = 0;
|
||||
for (String sponseeAddress : sponsees) {
|
||||
List<NameData> names = repository.getNameRepository().getNamesByOwner(sponseeAddress);
|
||||
for (NameData name : names) {
|
||||
if (name.getRegistered() < this.snapshotTimestampV3) {
|
||||
registeredNameCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.registeredNameCount = registeredNameCount;
|
||||
}
|
||||
|
||||
private void findRecentSponsorshipCount() {
|
||||
int recentSponsorshipCount = 0;
|
||||
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
|
||||
if (rewardShare.getTimestamp() >= this.snapshotTimestampV1) {
|
||||
recentSponsorshipCount++;
|
||||
}
|
||||
}
|
||||
this.recentSponsorshipCount = recentSponsorshipCount;
|
||||
}
|
||||
|
||||
private int calculateScore() {
|
||||
final int suspiciousMultiplier = (this.suspiciousCount >= 100) ? this.suspiciousPercent : 1;
|
||||
final int nameMultiplier = (this.sponsees.size() >= 25 && this.registeredNameCount <= 1) ? 21 :
|
||||
(this.sponsees.size() >= 15 && this.registeredNameCount <= 1) ? 11 :
|
||||
(this.sponsees.size() >= 5 && this.registeredNameCount <= 1) ? 5 : 1;
|
||||
final int consolidationMultiplier = Math.max(this.consolidationCount, 1);
|
||||
final int bulkIssuanceMultiplier = Math.max(this.bulkIssuanceCount / 2, 1);
|
||||
final int offset = 20;
|
||||
return suspiciousMultiplier * nameMultiplier * consolidationMultiplier * bulkIssuanceMultiplier - offset;
|
||||
}
|
||||
|
||||
private void fetchSponsorshipRewardShares() throws DataException {
|
||||
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
|
||||
// Define relevant transactions
|
||||
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
|
||||
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, this.address, false);
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3);
|
||||
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
|
||||
|
||||
// Skip removals
|
||||
if (rewardShareTransactionData.getSharePercent() < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not sponsored by this account
|
||||
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountData.getPublicKey())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip self shares
|
||||
if (Objects.equals(rewardShareTransactionData.getRecipient(), this.address)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean duplicateFound = false;
|
||||
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
|
||||
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
|
||||
// Duplicate
|
||||
duplicateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!duplicateFound) {
|
||||
sponsorshipRewardShares.add(rewardShareTransactionData);
|
||||
this.sponsees.add(rewardShareTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
|
||||
this.sponsorshipRewardShares = sponsorshipRewardShares;
|
||||
}
|
||||
|
||||
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
|
||||
return fetchTransactions(repository,
|
||||
List.of(TransactionType.TRANSFER_PRIVS),
|
||||
address, true);
|
||||
}
|
||||
|
||||
private void fetchPaymentsForAddress(String address) throws DataException {
|
||||
List<TransactionData> payments = fetchTransactions(repository,
|
||||
Arrays.asList(TransactionType.PAYMENT, TransactionType.TRANSFER_ASSET),
|
||||
address, false);
|
||||
this.paymentsByAddress.put(address, payments);
|
||||
}
|
||||
|
||||
private List<String> fetchOutgoingPaymentRecipientsForAddress(String address) {
|
||||
List<String> outgoingPaymentRecipients = new ArrayList<>();
|
||||
|
||||
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||
if (transactionDataList == null) transactionDataList = new ArrayList<>();
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3);
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
switch (transactionData.getType()) {
|
||||
|
||||
case PAYMENT:
|
||||
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
|
||||
if (!Objects.equals(paymentTransactionData.getRecipient(), address)) {
|
||||
// Outgoing payment from this account
|
||||
outgoingPaymentRecipients.add(paymentTransactionData.getRecipient());
|
||||
}
|
||||
break;
|
||||
|
||||
case TRANSFER_ASSET:
|
||||
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
|
||||
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
|
||||
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
|
||||
// Outgoing payment from this account
|
||||
outgoingPaymentRecipients.add(transferAssetTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return outgoingPaymentRecipients;
|
||||
}
|
||||
|
||||
private boolean hasZeroTransactions(String address) {
|
||||
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||
if (transactionDataList == null) {
|
||||
return true;
|
||||
}
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3);
|
||||
return transactionDataList.isEmpty();
|
||||
}
|
||||
|
||||
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||
// Fetch all relevant transactions for this account
|
||||
List<byte[]> signatures = repository.getTransactionRepository()
|
||||
.getSignaturesMatchingCriteria(null, null, null, txTypes,
|
||||
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
|
||||
null, null, reverse);
|
||||
|
||||
List<TransactionData> transactionDataList = new ArrayList<>();
|
||||
|
||||
for (byte[] signature : signatures) {
|
||||
// Fetch transaction data
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null) {
|
||||
continue;
|
||||
}
|
||||
transactionDataList.add(transactionData);
|
||||
}
|
||||
|
||||
return transactionDataList;
|
||||
}
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
import org.qortal.utils.Amounts;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class AmountTypeAdapter extends XmlAdapter<String, Long> {
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE)
|
||||
@XmlRootElement
|
||||
|
@ -1,18 +1,17 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ApiErrorHandler extends ErrorHandler {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ApiErrorHandler.class);
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.qortal.globalization.Translator;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public enum ApiExceptionFactory {
|
||||
INSTANCE;
|
||||
|
||||
|
@ -1,34 +1,24 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SNIHostName;
|
||||
import javax.net.ssl.SNIServerName;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.xml.bind.*;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
|
||||
import org.eclipse.persistence.exceptions.XMLMarshalException;
|
||||
import org.eclipse.persistence.jaxb.JAXBContextFactory;
|
||||
import org.eclipse.persistence.jaxb.MarshallerProperties;
|
||||
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import javax.xml.bind.*;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
import java.net.*;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ApiRequest {
|
||||
|
||||
private static final Pattern proxyUrlPattern = Pattern.compile("(https://)([^@:/]+)@([0-9.]{7,15})(/.*)");
|
||||
@ -151,7 +141,7 @@ public class ApiRequest {
|
||||
}
|
||||
|
||||
String resultString = result.toString();
|
||||
return resultString.length() > 0 ? resultString.substring(0, resultString.length() - 1) : resultString;
|
||||
return !resultString.isEmpty() ? resultString.substring(0, resultString.length() - 1) : resultString;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,33 +1,10 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
import org.eclipse.jetty.server.CustomRequestLog;
|
||||
import org.eclipse.jetty.server.DetectorConnectionFactory;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.RequestLog;
|
||||
import org.eclipse.jetty.server.RequestLogWriter;
|
||||
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.server.*;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.server.handler.InetAccessHandler;
|
||||
import org.eclipse.jetty.servlet.DefaultServlet;
|
||||
@ -44,6 +21,18 @@ import org.qortal.api.websocket.*;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class ApiService {
|
||||
|
||||
private static ApiService instance;
|
||||
@ -96,7 +85,7 @@ public class ApiService {
|
||||
throw new RuntimeException("Failed to start SSL API due to broken keystore");
|
||||
|
||||
// BouncyCastle-specific SSLContext build
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
|
||||
SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE");
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
public class Base58TypeAdapter extends XmlAdapter<String, byte[]> {
|
||||
|
||||
@Override
|
||||
|
@ -1,8 +1,7 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class BigDecimalTypeAdapter extends XmlAdapter<String, BigDecimal> {
|
||||
|
||||
|
@ -4,9 +4,11 @@ import io.swagger.v3.oas.models.Operation;
|
||||
import io.swagger.v3.oas.models.PathItem;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.responses.ApiResponse;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
class Constants {
|
||||
public static final String APIERROR_CONTEXT_PATH = "/Api";
|
||||
public static final String APIERROR_KEY = "ApiError/%s";
|
||||
|
173
src/main/java/org/qortal/api/DevProxyService.java
Normal file
173
src/main/java/org/qortal/api/DevProxyService.java
Normal file
@ -0,0 +1,173 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
import org.eclipse.jetty.server.*;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.server.handler.InetAccessHandler;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||
import org.qortal.api.resource.ApiDefinition;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class DevProxyService {
|
||||
|
||||
private static DevProxyService instance;
|
||||
|
||||
private final ResourceConfig config;
|
||||
private Server server;
|
||||
|
||||
private DevProxyService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.proxy.resource", "org.qortal.api.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
}
|
||||
|
||||
public static DevProxyService getInstance() {
|
||||
if (instance == null)
|
||||
instance = new DevProxyService();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Iterable<Class<?>> getResources() {
|
||||
return this.config.getClasses();
|
||||
}
|
||||
|
||||
public void start() throws DataException {
|
||||
try {
|
||||
// Create API server
|
||||
|
||||
// SSL support if requested
|
||||
String keystorePathname = Settings.getInstance().getSslKeystorePathname();
|
||||
String keystorePassword = Settings.getInstance().getSslKeystorePassword();
|
||||
|
||||
if (keystorePathname != null && keystorePassword != null) {
|
||||
// SSL version
|
||||
if (!Files.isReadable(Path.of(keystorePathname)))
|
||||
throw new RuntimeException("Failed to start SSL API due to broken keystore");
|
||||
|
||||
// BouncyCastle-specific SSLContext build
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
|
||||
|
||||
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
|
||||
keyStore.load(keystoreStream, keystorePassword.toCharArray());
|
||||
}
|
||||
|
||||
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
|
||||
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
|
||||
|
||||
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setSslContext(sslContext);
|
||||
|
||||
this.server = new Server();
|
||||
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setSecureScheme("https");
|
||||
httpConfig.setSecurePort(Settings.getInstance().getDevProxyPort());
|
||||
|
||||
SecureRequestCustomizer src = new SecureRequestCustomizer();
|
||||
httpConfig.addCustomizer(src);
|
||||
|
||||
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
|
||||
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
|
||||
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Network.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getDevProxyPort());
|
||||
|
||||
this.server.addConnector(portUnifiedConnector);
|
||||
} else {
|
||||
// Non-SSL
|
||||
InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getDevProxyPort());
|
||||
this.server = new Server(endpoint);
|
||||
}
|
||||
|
||||
// Error handler
|
||||
ErrorHandler errorHandler = new ApiErrorHandler();
|
||||
this.server.setErrorHandler(errorHandler);
|
||||
|
||||
// Request logging
|
||||
if (Settings.getInstance().isDevProxyLoggingEnabled()) {
|
||||
RequestLogWriter logWriter = new RequestLogWriter("devproxy-requests.log");
|
||||
logWriter.setAppend(true);
|
||||
logWriter.setTimeZone("UTC");
|
||||
RequestLog requestLog = new CustomRequestLog(logWriter, CustomRequestLog.EXTENDED_NCSA_FORMAT);
|
||||
this.server.setRequestLog(requestLog);
|
||||
}
|
||||
|
||||
// Access handler (currently no whitelist is used)
|
||||
InetAccessHandler accessHandler = new InetAccessHandler();
|
||||
this.server.setHandler(accessHandler);
|
||||
|
||||
// URL rewriting
|
||||
RewriteHandler rewriteHandler = new RewriteHandler();
|
||||
accessHandler.setHandler(rewriteHandler);
|
||||
|
||||
// Context
|
||||
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
|
||||
context.setContextPath("/");
|
||||
rewriteHandler.setHandler(context);
|
||||
|
||||
// Cross-origin resource sharing
|
||||
FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class);
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE");
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
|
||||
context.addFilter(corsFilterHolder, "/*", null);
|
||||
|
||||
// API servlet
|
||||
ServletContainer container = new ServletContainer(this.config);
|
||||
ServletHolder apiServlet = new ServletHolder(container);
|
||||
apiServlet.setInitOrder(1);
|
||||
context.addServlet(apiServlet, "/*");
|
||||
|
||||
// Start server
|
||||
this.server.start();
|
||||
} catch (Exception e) {
|
||||
// Failed to start
|
||||
throw new DataException("Failed to start developer proxy", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
// Stop server
|
||||
this.server.stop();
|
||||
} catch (Exception e) {
|
||||
// Failed to stop
|
||||
}
|
||||
|
||||
this.server = null;
|
||||
instance = null;
|
||||
}
|
||||
|
||||
}
|
@ -69,7 +69,7 @@ public class DomainMapService {
|
||||
throw new RuntimeException("Failed to start SSL API due to broken keystore");
|
||||
|
||||
// BouncyCastle-specific SSLContext build
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
|
||||
SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE");
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
|
||||
|
@ -69,7 +69,7 @@ public class GatewayService {
|
||||
throw new RuntimeException("Failed to start SSL API due to broken keystore");
|
||||
|
||||
// BouncyCastle-specific SSLContext build
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
|
||||
SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE");
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
|
||||
|
@ -13,7 +13,8 @@ public class HTMLParser {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HTMLParser.class);
|
||||
|
||||
private String linkPrefix;
|
||||
private String qdnBase;
|
||||
private String qdnBaseWithPath;
|
||||
private byte[] data;
|
||||
private String qdnContext;
|
||||
private String resourceId;
|
||||
@ -21,10 +22,13 @@ public class HTMLParser {
|
||||
private String identifier;
|
||||
private String path;
|
||||
private String theme;
|
||||
private boolean usingCustomRouting;
|
||||
|
||||
public HTMLParser(String resourceId, String inPath, String prefix, boolean usePrefix, byte[] data,
|
||||
String qdnContext, Service service, String identifier, String theme) {
|
||||
this.linkPrefix = usePrefix ? String.format("%s/%s", prefix, resourceId) : "";
|
||||
public HTMLParser(String resourceId, String inPath, String prefix, boolean includeResourceIdInPrefix, byte[] data,
|
||||
String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting) {
|
||||
String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : String.format("/%s",inPath);
|
||||
this.qdnBase = includeResourceIdInPrefix ? String.format("%s/%s", prefix, resourceId) : prefix;
|
||||
this.qdnBaseWithPath = includeResourceIdInPrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : String.format("%s%s", prefix, inPathWithoutFilename);
|
||||
this.data = data;
|
||||
this.qdnContext = qdnContext;
|
||||
this.resourceId = resourceId;
|
||||
@ -32,12 +36,12 @@ public class HTMLParser {
|
||||
this.identifier = identifier;
|
||||
this.path = inPath;
|
||||
this.theme = theme;
|
||||
this.usingCustomRouting = usingCustomRouting;
|
||||
}
|
||||
|
||||
public void addAdditionalHeaderTags() {
|
||||
String fileContents = new String(data);
|
||||
Document document = Jsoup.parse(fileContents);
|
||||
String baseUrl = this.linkPrefix;
|
||||
Elements head = document.getElementsByTag("head");
|
||||
if (!head.isEmpty()) {
|
||||
// Add q-apps script tag
|
||||
@ -51,16 +55,21 @@ public class HTMLParser {
|
||||
}
|
||||
|
||||
// Escape and add vars
|
||||
String service = this.service.toString().replace("\"","\\\"");
|
||||
String name = this.resourceId != null ? this.resourceId.replace("\"","\\\"") : "";
|
||||
String identifier = this.identifier != null ? this.identifier.replace("\"","\\\"") : "";
|
||||
String path = this.path != null ? this.path.replace("\"","\\\"") : "";
|
||||
String theme = this.theme != null ? this.theme.replace("\"","\\\"") : "";
|
||||
String qdnContextVar = String.format("<script>var _qdnContext=\"%s\"; var _qdnTheme=\"%s\"; var _qdnService=\"%s\"; var _qdnName=\"%s\"; var _qdnIdentifier=\"%s\"; var _qdnPath=\"%s\"; var _qdnBase=\"%s\";</script>", this.qdnContext, theme, service, name, identifier, path, baseUrl);
|
||||
String qdnContext = this.qdnContext != null ? this.qdnContext.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String service = this.service.toString().replace("\\", "").replace("\"","\\\"");
|
||||
String name = this.resourceId != null ? this.resourceId.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String identifier = this.identifier != null ? this.identifier.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String path = this.path != null ? this.path.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String theme = this.theme != null ? this.theme.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String qdnBase = this.qdnBase != null ? this.qdnBase.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String qdnBaseWithPath = this.qdnBaseWithPath != null ? this.qdnBaseWithPath.replace("\\", "").replace("\"","\\\"") : "";
|
||||
String qdnContextVar = String.format("<script>var _qdnContext=\"%s\"; var _qdnTheme=\"%s\"; var _qdnService=\"%s\"; var _qdnName=\"%s\"; var _qdnIdentifier=\"%s\"; var _qdnPath=\"%s\"; var _qdnBase=\"%s\"; var _qdnBaseWithPath=\"%s\";</script>", qdnContext, theme, service, name, identifier, path, qdnBase, qdnBaseWithPath);
|
||||
head.get(0).prepend(qdnContextVar);
|
||||
|
||||
// Add base href tag
|
||||
String baseElement = String.format("<base href=\"%s/\">", baseUrl);
|
||||
// Exclude the path if this request was routed back to the index automatically
|
||||
String baseHref = this.usingCustomRouting ? this.qdnBase : this.qdnBaseWithPath;
|
||||
String baseElement = String.format("<base href=\"%s/\">", baseHref);
|
||||
head.get(0).prepend(baseElement);
|
||||
|
||||
// Add meta charset tag
|
||||
@ -73,7 +82,7 @@ public class HTMLParser {
|
||||
}
|
||||
|
||||
public static boolean isHtmlFile(String path) {
|
||||
if (path.endsWith(".html") || path.endsWith(".htm")) {
|
||||
if (path.endsWith(".html") || path.endsWith(".htm") || path.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -1,8 +1,7 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class RewardSharePercentTypeAdapter extends XmlAdapter<String, Integer> {
|
||||
|
||||
|
6
src/main/java/org/qortal/api/SearchMode.java
Normal file
6
src/main/java/org/qortal/api/SearchMode.java
Normal file
@ -0,0 +1,6 @@
|
||||
package org.qortal.api;
|
||||
|
||||
public enum SearchMode {
|
||||
LATEST,
|
||||
ALL
|
||||
}
|
@ -5,12 +5,11 @@ import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataRenderManager;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public abstract class Security {
|
||||
|
||||
public static final String API_KEY_HEADER = "X-API-KEY";
|
||||
|
@ -1,18 +1,17 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlVariableNode;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import javax.xml.bind.annotation.XmlValue;
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import javax.xml.bind.annotation.XmlValue;
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlVariableNode;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
public class TransactionCountMapXmlAdapter extends XmlAdapter<TransactionCountMapXmlAdapter.StringIntegerMap, Map<TransactionType, Integer>> {
|
||||
|
||||
public static class StringIntegerMap {
|
||||
|
@ -48,10 +48,10 @@ public class DomainMapResource {
|
||||
}
|
||||
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
|
||||
String inPath, String secret58, String prefix, boolean usePrefix, boolean async) {
|
||||
String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async) {
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
|
||||
secret58, prefix, usePrefix, async, "domainMap", request, response, context);
|
||||
secret58, prefix, includeResourceIdInPrefix, async, "domainMap", request, response, context);
|
||||
return renderer.render();
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@ package org.qortal.api.gateway.resource;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
@ -11,11 +13,16 @@ import org.qortal.arbitrary.ArbitraryDataRenderer;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -31,36 +38,12 @@ public class GatewayResource {
|
||||
@Context HttpServletResponse response;
|
||||
@Context ServletContext context;
|
||||
|
||||
/**
|
||||
* We need to allow resource status checking (and building) via the gateway, as the node's API port
|
||||
* may not be forwarded and will almost certainly not be authenticated. Since gateways allow for
|
||||
* all resources to be loaded except those that are blocked, there is no need for authentication.
|
||||
*/
|
||||
@GET
|
||||
@Path("/arbitrary/resource/status/{service}/{name}")
|
||||
public ArbitraryResourceStatus getDefaultResourceStatus(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
return this.getStatus(service, name, null, build);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/arbitrary/resource/status/{service}/{name}/{identifier}")
|
||||
public ArbitraryResourceStatus getResourceStatus(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
return this.getStatus(service, name, identifier, build);
|
||||
}
|
||||
|
||||
private ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) {
|
||||
|
||||
// If "build=true" has been specified in the query string, build the resource before returning its status
|
||||
if (build != null && build == true) {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
||||
if (build != null && build) {
|
||||
try {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
||||
if (!reader.isBuilding()) {
|
||||
reader.loadSynchronously(false);
|
||||
}
|
||||
@ -69,8 +52,13 @@ public class GatewayResource {
|
||||
}
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.getStatus(false);
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.getStatus(repository);
|
||||
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -90,9 +78,9 @@ public class GatewayResource {
|
||||
}
|
||||
|
||||
|
||||
private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean usePrefix, boolean async) {
|
||||
private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean includeResourceIdInPrefix, boolean async) {
|
||||
|
||||
if (inPath == null || inPath.equals("")) {
|
||||
if (inPath == null || inPath.isEmpty()) {
|
||||
// Assume not a real file
|
||||
return ArbitraryDataRenderer.getResponse(response, 404, "Error 404: File Not Found");
|
||||
}
|
||||
@ -152,12 +140,12 @@ public class GatewayResource {
|
||||
}
|
||||
|
||||
String prefix = StringUtils.join(prefixParts, "/");
|
||||
if (prefix != null && prefix.length() > 0) {
|
||||
if (prefix != null && !prefix.isEmpty()) {
|
||||
prefix = "/" + prefix;
|
||||
}
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(name, ResourceIdType.NAME, service, identifier, outPath,
|
||||
secret58, prefix, usePrefix, async, qdnContext, request, response, context);
|
||||
secret58, prefix, includeResourceIdInPrefix, async, qdnContext, request, response, context);
|
||||
return renderer.render();
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,9 @@ package org.qortal.api.model;
|
||||
|
||||
import org.qortal.block.SelfSponsorshipAlgoV1Block;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -1,15 +1,14 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import org.qortal.api.TransactionCountMapXmlAdapter;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.api.TransactionCountMapXmlAdapter;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ActivitySummary {
|
||||
|
@ -1,13 +1,13 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import org.qortal.data.asset.OrderData;
|
||||
|
||||
import javax.xml.bind.Marshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.data.asset.OrderData;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE)
|
||||
public class AggregatedOrder {
|
||||
|
||||
|
@ -1,7 +1,13 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@ -47,4 +53,31 @@ public class ApiOnlineAccount {
|
||||
return this.recipientAddress;
|
||||
}
|
||||
|
||||
public int getMinterLevelFromPublicKey() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return Account.getRewardShareEffectiveMintingLevel(repository, this.rewardSharePublicKey);
|
||||
} catch (DataException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getIsMember() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getGroupRepository().memberExists(694, getMinterAddress());
|
||||
} catch (DataException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// JAXB special
|
||||
|
||||
@XmlElement(name = "minterLevel")
|
||||
protected int getMinterLevel() {
|
||||
return getMinterLevelFromPublicKey();
|
||||
}
|
||||
|
||||
@XmlElement(name = "isMinterMember")
|
||||
protected boolean getMinterMember() {
|
||||
return getIsMember();
|
||||
}
|
||||
}
|
||||
|
101
src/main/java/org/qortal/api/model/AtCreationRequest.java
Normal file
101
src/main/java/org/qortal/api/model/AtCreationRequest.java
Normal file
@ -0,0 +1,101 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.bouncycastle.util.encoders.DecoderException;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class AtCreationRequest {
|
||||
|
||||
@Schema(description = "CIYAM AT version", example = "2")
|
||||
private short ciyamAtVersion;
|
||||
|
||||
@Schema(description = "base64-encoded code bytes", example = "")
|
||||
private String codeBytesBase64;
|
||||
|
||||
@Schema(description = "base64-encoded data bytes", example = "")
|
||||
private String dataBytesBase64;
|
||||
|
||||
private short numCallStackPages;
|
||||
private short numUserStackPages;
|
||||
private long minActivationAmount;
|
||||
|
||||
// Default constructor for JSON deserialization
|
||||
public AtCreationRequest() {}
|
||||
|
||||
// Getters and setters
|
||||
public short getCiyamAtVersion() {
|
||||
return ciyamAtVersion;
|
||||
}
|
||||
|
||||
public void setCiyamAtVersion(short ciyamAtVersion) {
|
||||
this.ciyamAtVersion = ciyamAtVersion;
|
||||
}
|
||||
|
||||
|
||||
public String getCodeBytesBase64() {
|
||||
return this.codeBytesBase64;
|
||||
}
|
||||
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
public byte[] getCodeBytes() {
|
||||
if (this.codeBytesBase64 != null) {
|
||||
try {
|
||||
return Base64.decode(this.codeBytesBase64);
|
||||
}
|
||||
catch (DecoderException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public String getDataBytesBase64() {
|
||||
return this.dataBytesBase64;
|
||||
}
|
||||
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
public byte[] getDataBytes() {
|
||||
if (this.dataBytesBase64 != null) {
|
||||
try {
|
||||
return Base64.decode(this.dataBytesBase64);
|
||||
}
|
||||
catch (DecoderException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public short getNumCallStackPages() {
|
||||
return numCallStackPages;
|
||||
}
|
||||
|
||||
public void setNumCallStackPages(short numCallStackPages) {
|
||||
this.numCallStackPages = numCallStackPages;
|
||||
}
|
||||
|
||||
public short getNumUserStackPages() {
|
||||
return numUserStackPages;
|
||||
}
|
||||
|
||||
public void setNumUserStackPages(short numUserStackPages) {
|
||||
this.numUserStackPages = numUserStackPages;
|
||||
}
|
||||
|
||||
public long getMinActivationAmount() {
|
||||
return minActivationAmount;
|
||||
}
|
||||
|
||||
public void setMinActivationAmount(long minActivationAmount) {
|
||||
this.minActivationAmount = minActivationAmount;
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import java.math.BigInteger;
|
||||
public class BlockMintingInfo {
|
||||
|
||||
public byte[] minterPublicKey;
|
||||
public String minterAddress;
|
||||
public int minterLevel;
|
||||
public int onlineAccountsCount;
|
||||
public BigDecimal maxDistance;
|
||||
@ -19,5 +20,4 @@ public class BlockMintingInfo {
|
||||
|
||||
public BlockMintingInfo() {
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockSignerSummary {
|
||||
|
||||
|
@ -17,7 +17,7 @@ public class ConnectedPeer {
|
||||
|
||||
public enum Direction {
|
||||
INBOUND,
|
||||
OUTBOUND;
|
||||
OUTBOUND
|
||||
}
|
||||
|
||||
public Direction direction;
|
||||
|
@ -1,11 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBitcoinRedeemRequest {
|
||||
|
@ -1,11 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBitcoinRefundRequest {
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBitcoinTemplateRequest {
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBitcoinyHTLCStatus {
|
||||
|
@ -1,11 +1,11 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBuildRequest {
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainCancelRequest {
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainOfferSummary {
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainSecretRequest {
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeRequest {
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeSummary {
|
||||
|
@ -1,12 +1,11 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.util.List;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "Group info, maybe including members")
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
|
@ -1,11 +1,11 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import org.qortal.data.naming.NameData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
import org.qortal.data.naming.NameData;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE)
|
||||
public class NameSummary {
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.network.Network;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class NodeStatus {
|
||||
|
||||
|
83
src/main/java/org/qortal/api/model/PollVotes.java
Normal file
83
src/main/java/org/qortal/api/model/PollVotes.java
Normal file
@ -0,0 +1,83 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.qortal.data.voting.VoteOnPollData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "Poll vote info, including voters")
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class PollVotes {
|
||||
|
||||
@Schema(description = "List of individual votes")
|
||||
@XmlElement(name = "votes")
|
||||
public List<VoteOnPollData> votes;
|
||||
|
||||
@Schema(description = "Total number of votes")
|
||||
public Integer totalVotes;
|
||||
|
||||
@Schema(description = "Total weight of votes")
|
||||
public Integer totalWeight;
|
||||
|
||||
@Schema(description = "List of vote counts for each option")
|
||||
public List<OptionCount> voteCounts;
|
||||
|
||||
@Schema(description = "List of vote weights for each option")
|
||||
public List<OptionWeight> voteWeights;
|
||||
|
||||
// For JAX-RS
|
||||
protected PollVotes() {
|
||||
}
|
||||
|
||||
public PollVotes(List<VoteOnPollData> votes, Integer totalVotes, Integer totalWeight, List<OptionCount> voteCounts, List<OptionWeight> voteWeights) {
|
||||
this.votes = votes;
|
||||
this.totalVotes = totalVotes;
|
||||
this.totalWeight = totalWeight;
|
||||
this.voteCounts = voteCounts;
|
||||
this.voteWeights = voteWeights;
|
||||
}
|
||||
|
||||
@Schema(description = "Vote info")
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class OptionCount {
|
||||
@Schema(description = "Option name")
|
||||
public String optionName;
|
||||
|
||||
@Schema(description = "Vote count")
|
||||
public Integer voteCount;
|
||||
|
||||
// For JAX-RS
|
||||
protected OptionCount() {
|
||||
}
|
||||
|
||||
public OptionCount(String optionName, Integer voteCount) {
|
||||
this.optionName = optionName;
|
||||
this.voteCount = voteCount;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "Vote weights")
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class OptionWeight {
|
||||
@Schema(description = "Option name")
|
||||
public String optionName;
|
||||
|
||||
@Schema(description = "Vote weight")
|
||||
public Integer voteWeight;
|
||||
|
||||
// For JAX-RS
|
||||
protected OptionWeight() {
|
||||
}
|
||||
|
||||
public OptionWeight(String optionName, Integer voteWeight) {
|
||||
this.optionName = optionName;
|
||||
this.voteWeight = voteWeight;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class RewardShareKeyRequest {
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class SimpleForeignTransaction {
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class SimpleTransactionSignRequest {
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.qortal.data.asset.OrderData;
|
||||
import org.qortal.data.asset.TradeData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
import org.qortal.data.asset.OrderData;
|
||||
import org.qortal.data.asset.TradeData;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "Asset trade, including order info")
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
|
@ -0,0 +1,17 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class AddressRequest {
|
||||
|
||||
@Schema(description = "Litecoin BIP32 extended public key", example = "tpub___________________________________________________________________________________________________________")
|
||||
public String xpub58;
|
||||
|
||||
public AddressRequest() {
|
||||
}
|
||||
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BitcoinSendRequest {
|
||||
|
||||
|
@ -0,0 +1,692 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.Arrays;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BitcoinyTBDRequest {
|
||||
|
||||
/**
|
||||
* Target Timespan
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* consensus.nPowTargetTimespan
|
||||
*/
|
||||
private int targetTimespan;
|
||||
|
||||
/**
|
||||
* Target Spacing
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* consensus.nPowTargetSpacing
|
||||
*/
|
||||
private int targetSpacing;
|
||||
|
||||
/**
|
||||
* Packet Magic
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* Concatenate the 4 values in pchMessageStart, then convert the hex to decimal.
|
||||
*
|
||||
* Ex. litecoin
|
||||
* pchMessageStart[0] = 0xfb;
|
||||
* pchMessageStart[1] = 0xc0;
|
||||
* pchMessageStart[2] = 0xb6;
|
||||
* pchMessageStart[3] = 0xdb;
|
||||
* packetMagic = 0xfbc0b6db = 4223710939
|
||||
*/
|
||||
private long packetMagic;
|
||||
|
||||
/**
|
||||
* Port
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* nDefaultPort
|
||||
*/
|
||||
private int port;
|
||||
|
||||
/**
|
||||
* Address Header
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* base58Prefixes[PUBKEY_ADDRESS] from Main Network
|
||||
*/
|
||||
private int addressHeader;
|
||||
|
||||
/**
|
||||
* P2sh Header
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* base58Prefixes[SCRIPT_ADDRESS] from Main Network
|
||||
*/
|
||||
private int p2shHeader;
|
||||
|
||||
/**
|
||||
* Segwit Address Hrp
|
||||
*
|
||||
* HRP -> Human Readable Parts
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* bech32_hrp
|
||||
*/
|
||||
private String segwitAddressHrp;
|
||||
|
||||
/**
|
||||
* Dumped Private Key Header
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* base58Prefixes[SECRET_KEY] from Main Network
|
||||
* This is usually, but not always ... addressHeader + 128
|
||||
*/
|
||||
private int dumpedPrivateKeyHeader;
|
||||
|
||||
/**
|
||||
* Subsidy Decreased Block Count
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* consensus.nSubsidyHalvingInterval
|
||||
*
|
||||
* Digibyte does not support this, because they do halving differently.
|
||||
*/
|
||||
private int subsidyDecreaseBlockCount;
|
||||
|
||||
/**
|
||||
* Expected Genesis Hash
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* consensus.hashGenesisBlock
|
||||
* Remove '0x' prefix
|
||||
*/
|
||||
private String expectedGenesisHash;
|
||||
|
||||
/**
|
||||
* Common Script Pub Key
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* This is the key commonly used to sign alerts for altcoins. Bitcoin and Digibyte are know exceptions.
|
||||
*/
|
||||
public static final String SCRIPT_PUB_KEY = "040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9";
|
||||
|
||||
/**
|
||||
* The Script Pub Key
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* The key to sign alerts.
|
||||
*
|
||||
* const CScript genesisOutputScript = CScript() << ParseHex("040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9") << OP_CHECKSIG;
|
||||
*
|
||||
* ie LTC = 040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9
|
||||
*
|
||||
* this may be the same value as scripHex
|
||||
*/
|
||||
private String pubKey;
|
||||
|
||||
/**
|
||||
* DNS Seeds
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* vSeeds
|
||||
*/
|
||||
private String[] dnsSeeds;
|
||||
|
||||
/**
|
||||
* BIP32 Header P2PKH Pub
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* Concatenate the 4 values in base58Prefixes[EXT_PUBLIC_KEY]
|
||||
* base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x88, 0xB2, 0x1E} = 0x0488B21E
|
||||
*/
|
||||
private int bip32HeaderP2PKHpub;
|
||||
|
||||
/**
|
||||
* BIP32 Header P2PKH Priv
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* Concatenate the 4 values in base58Prefixes[EXT_SECRET_KEY]
|
||||
* base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4} = 0x0488ADE4
|
||||
*/
|
||||
private int bip32HeaderP2PKHpriv;
|
||||
|
||||
/**
|
||||
* Address Header (Testnet)
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* base58Prefixes[PUBKEY_ADDRESS] from Testnet
|
||||
*/
|
||||
private int addressHeaderTestnet;
|
||||
|
||||
/**
|
||||
* BIP32 Header P2PKH Pub (Testnet)
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* Concatenate the 4 values in base58Prefixes[EXT_PUBLIC_KEY]
|
||||
* base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x88, 0xB2, 0x1E} = 0x0488B21E
|
||||
*/
|
||||
private int bip32HeaderP2PKHpubTestnet;
|
||||
|
||||
/**
|
||||
* BIP32 Header P2PKH Priv (Testnet)
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* Concatenate the 4 values in base58Prefixes[EXT_SECRET_KEY]
|
||||
* base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4} = 0x0488ADE4
|
||||
*/
|
||||
private int bip32HeaderP2PKHprivTestnet;
|
||||
|
||||
/**
|
||||
* Id
|
||||
*
|
||||
* "org.litecoin.production" for LTC
|
||||
* I'm guessing this just has to match others for trading purposes.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* Majority Enforce Block Upgrade
|
||||
*
|
||||
* All coins are setting this to 750, except DOGE is setting this to 1500.
|
||||
*/
|
||||
private int majorityEnforceBlockUpgrade;
|
||||
|
||||
/**
|
||||
* Majority Reject Block Outdated
|
||||
*
|
||||
* All coins are setting this to 950, except DOGE is setting this to 1900.
|
||||
*/
|
||||
private int majorityRejectBlockOutdated;
|
||||
|
||||
/**
|
||||
* Majority Window
|
||||
*
|
||||
* All coins are setting this to 1000, except DOGE is setting this to 2000.
|
||||
*/
|
||||
private int majorityWindow;
|
||||
|
||||
/**
|
||||
* Code
|
||||
*
|
||||
* "LITE" for LTC
|
||||
* Currency code for full unit.
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* mCode
|
||||
*
|
||||
* "mLITE" for LTC
|
||||
* Currency code for milli unit.
|
||||
*/
|
||||
private String mCode;
|
||||
|
||||
/**
|
||||
* Base Code
|
||||
*
|
||||
* "Liteoshi" for LTC
|
||||
* Currency code for base unit.
|
||||
*/
|
||||
private String baseCode;
|
||||
|
||||
/**
|
||||
* Min Non Dust Output
|
||||
*
|
||||
* 100000 for LTC, web search for minimum transaction fee per kB
|
||||
*/
|
||||
private int minNonDustOutput;
|
||||
|
||||
/**
|
||||
* URI Scheme
|
||||
*
|
||||
* uriScheme = "litecoin" for LTC
|
||||
* Do a web search to find this value.
|
||||
*/
|
||||
private String uriScheme;
|
||||
|
||||
/**
|
||||
* Protocol Version Minimum
|
||||
*
|
||||
* 70002 for LTC
|
||||
* extracted from /src/protocol.h class
|
||||
*/
|
||||
private int protocolVersionMinimum;
|
||||
|
||||
/**
|
||||
* Protocol Version Current
|
||||
*
|
||||
* 70003 for LTC
|
||||
* extracted from /src/protocol.h class
|
||||
*/
|
||||
private int protocolVersionCurrent;
|
||||
|
||||
/**
|
||||
* Has Max Money
|
||||
*
|
||||
* false for DOGE, true for BTC and LTC
|
||||
*/
|
||||
private boolean hasMaxMoney;
|
||||
|
||||
/**
|
||||
* Max Money
|
||||
*
|
||||
* 84000000 for LTC, 21000000 for BTC
|
||||
* extracted from src/amount.h class
|
||||
*/
|
||||
private long maxMoney;
|
||||
|
||||
/**
|
||||
* Currency Code
|
||||
*
|
||||
* The trading symbol, ie LTC, BTC, DOGE
|
||||
*/
|
||||
private String currencyCode;
|
||||
|
||||
/**
|
||||
* Minimum Order Amount
|
||||
*
|
||||
* web search, LTC minimumOrderAmount = 1000000, 0.01 LTC minimum order to avoid dust errors
|
||||
*/
|
||||
private long minimumOrderAmount;
|
||||
|
||||
/**
|
||||
* Fee Per Kb
|
||||
*
|
||||
* web search, LTC feePerKb = 10000, 0.0001 LTC per 1000 bytes
|
||||
*/
|
||||
private long feePerKb;
|
||||
|
||||
/**
|
||||
* Network Name
|
||||
*
|
||||
* ie Litecoin-MAIN
|
||||
*/
|
||||
private String networkName;
|
||||
|
||||
/**
|
||||
* Fee Ceiling
|
||||
*
|
||||
* web search, LTC fee ceiling = 1000L
|
||||
*/
|
||||
private long feeCeiling;
|
||||
|
||||
/**
|
||||
* Extended Public Key
|
||||
*
|
||||
* xpub for operations that require wallet watching
|
||||
*/
|
||||
private String extendedPublicKey;
|
||||
|
||||
/**
|
||||
* Send Amount
|
||||
*
|
||||
* The amount to send in base units. Also, requires sending fee per byte, receiving address and sender's extended private key.
|
||||
*/
|
||||
private long sendAmount;
|
||||
|
||||
/**
|
||||
* Sending Fee Per Byte
|
||||
*
|
||||
* The fee to include on a send request in base units. Also, requires receiving address, sender's extended private key and send amount.
|
||||
*/
|
||||
private long sendingFeePerByte;
|
||||
|
||||
/**
|
||||
* Receiving Address
|
||||
*
|
||||
* The receiving address for a send request. Also, requires send amount, sender's extended private key and sending fee per byte.
|
||||
*/
|
||||
private String receivingAddress;
|
||||
|
||||
/**
|
||||
* Extended Private Key
|
||||
*
|
||||
* xpriv address for a send request. Also, requires receiving address, send amount and sending fee per byte.
|
||||
*/
|
||||
private String extendedPrivateKey;
|
||||
|
||||
/**
|
||||
* Server Info
|
||||
*
|
||||
* For adding, removing, setting current server requests.
|
||||
*/
|
||||
private ServerInfo serverInfo;
|
||||
|
||||
/**
|
||||
* Script Sig
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* pszTimestamp
|
||||
*
|
||||
* transform this value - https://bitcoin.stackexchange.com/questions/13122/scriptsig-coinbase-structure-of-the-genesis-block
|
||||
* ie LTC = 04ffff001d0104404e592054696d65732030352f4f63742f32303131205374657665204a6f62732c204170706c65e280997320566973696f6e6172792c2044696573206174203536
|
||||
* ie DOGE = 04ffff001d0104084e696e746f6e646f
|
||||
*/
|
||||
private String scriptSig;
|
||||
|
||||
/**
|
||||
* Script Hex
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* genesisOutputScript
|
||||
*
|
||||
* ie LTC = 040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9
|
||||
*
|
||||
* this may be the same value as pubKey
|
||||
*/
|
||||
private String scriptHex;
|
||||
|
||||
/**
|
||||
* Reward
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* CreateGenesisBlock(..., [reward] * COIN)
|
||||
*
|
||||
* ie LTC = 50, BTC = 50, DOGE = 88
|
||||
*/
|
||||
private int reward;
|
||||
|
||||
/**
|
||||
* Genesis Creation Version
|
||||
*/
|
||||
private int genesisCreationVersion;
|
||||
|
||||
/**
|
||||
* Genesis Block Version
|
||||
*/
|
||||
private long genesisBlockVersion;
|
||||
|
||||
/**
|
||||
* Genesis Time
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* CreateGenesisBlock(nTime, ...)
|
||||
*
|
||||
* ie LTC = 1317972665
|
||||
*/
|
||||
private long genesisTime;
|
||||
|
||||
/**
|
||||
* Difficulty Target
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* CreateGenesisBlock(genesisTime, nonce, difficultyTarget, 1, reward * COIN);
|
||||
*
|
||||
* convert from hex to decimal
|
||||
*
|
||||
* ie LTC = 0x1e0ffff0 = 504365040
|
||||
*/
|
||||
private long difficultyTarget;
|
||||
|
||||
/**
|
||||
* Merkle Hex
|
||||
*/
|
||||
private String merkleHex;
|
||||
|
||||
/**
|
||||
* Nonce
|
||||
*
|
||||
* extracted from /src/chainparams.cpp class
|
||||
* CreateGenesisBlock(genesisTime, nonce, difficultyTarget, 1, reward * COIN);
|
||||
*
|
||||
* ie LTC = 2084524493
|
||||
*/
|
||||
private long nonce;
|
||||
|
||||
|
||||
public int getTargetTimespan() {
|
||||
return targetTimespan;
|
||||
}
|
||||
|
||||
public int getTargetSpacing() {
|
||||
return targetSpacing;
|
||||
}
|
||||
|
||||
public long getPacketMagic() {
|
||||
return packetMagic;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public int getAddressHeader() {
|
||||
return addressHeader;
|
||||
}
|
||||
|
||||
public int getP2shHeader() {
|
||||
return p2shHeader;
|
||||
}
|
||||
|
||||
public String getSegwitAddressHrp() {
|
||||
return segwitAddressHrp;
|
||||
}
|
||||
|
||||
public int getDumpedPrivateKeyHeader() {
|
||||
return dumpedPrivateKeyHeader;
|
||||
}
|
||||
|
||||
public int getSubsidyDecreaseBlockCount() {
|
||||
return subsidyDecreaseBlockCount;
|
||||
}
|
||||
|
||||
public String getExpectedGenesisHash() {
|
||||
return expectedGenesisHash;
|
||||
}
|
||||
|
||||
public String getPubKey() {
|
||||
return pubKey;
|
||||
}
|
||||
|
||||
public String[] getDnsSeeds() {
|
||||
return dnsSeeds;
|
||||
}
|
||||
|
||||
public int getBip32HeaderP2PKHpub() {
|
||||
return bip32HeaderP2PKHpub;
|
||||
}
|
||||
|
||||
public int getBip32HeaderP2PKHpriv() {
|
||||
return bip32HeaderP2PKHpriv;
|
||||
}
|
||||
|
||||
public int getAddressHeaderTestnet() {
|
||||
return addressHeaderTestnet;
|
||||
}
|
||||
|
||||
public int getBip32HeaderP2PKHpubTestnet() {
|
||||
return bip32HeaderP2PKHpubTestnet;
|
||||
}
|
||||
|
||||
public int getBip32HeaderP2PKHprivTestnet() {
|
||||
return bip32HeaderP2PKHprivTestnet;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public int getMajorityEnforceBlockUpgrade() {
|
||||
return this.majorityEnforceBlockUpgrade;
|
||||
}
|
||||
|
||||
public int getMajorityRejectBlockOutdated() {
|
||||
return this.majorityRejectBlockOutdated;
|
||||
}
|
||||
|
||||
public int getMajorityWindow() {
|
||||
return this.majorityWindow;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public String getmCode() {
|
||||
return this.mCode;
|
||||
}
|
||||
|
||||
public String getBaseCode() {
|
||||
return this.baseCode;
|
||||
}
|
||||
|
||||
public int getMinNonDustOutput() {
|
||||
return this.minNonDustOutput;
|
||||
}
|
||||
|
||||
public String getUriScheme() {
|
||||
return this.uriScheme;
|
||||
}
|
||||
|
||||
public int getProtocolVersionMinimum() {
|
||||
return this.protocolVersionMinimum;
|
||||
}
|
||||
|
||||
public int getProtocolVersionCurrent() {
|
||||
return this.protocolVersionCurrent;
|
||||
}
|
||||
|
||||
public boolean isHasMaxMoney() {
|
||||
return this.hasMaxMoney;
|
||||
}
|
||||
|
||||
public long getMaxMoney() {
|
||||
return this.maxMoney;
|
||||
}
|
||||
|
||||
public String getCurrencyCode() {
|
||||
return this.currencyCode;
|
||||
}
|
||||
|
||||
public long getMinimumOrderAmount() {
|
||||
return this.minimumOrderAmount;
|
||||
}
|
||||
|
||||
public long getFeePerKb() {
|
||||
return this.feePerKb;
|
||||
}
|
||||
|
||||
public String getNetworkName() {
|
||||
return this.networkName;
|
||||
}
|
||||
|
||||
public long getFeeCeiling() {
|
||||
return this.feeCeiling;
|
||||
}
|
||||
|
||||
public String getExtendedPublicKey() {
|
||||
return this.extendedPublicKey;
|
||||
}
|
||||
|
||||
public long getSendAmount() {
|
||||
return this.sendAmount;
|
||||
}
|
||||
|
||||
public long getSendingFeePerByte() {
|
||||
return this.sendingFeePerByte;
|
||||
}
|
||||
|
||||
public String getReceivingAddress() {
|
||||
return this.receivingAddress;
|
||||
}
|
||||
|
||||
public String getExtendedPrivateKey() {
|
||||
return this.extendedPrivateKey;
|
||||
}
|
||||
|
||||
public ServerInfo getServerInfo() {
|
||||
return this.serverInfo;
|
||||
}
|
||||
|
||||
public String getScriptSig() {
|
||||
return this.scriptSig;
|
||||
}
|
||||
|
||||
public String getScriptHex() {
|
||||
return this.scriptHex;
|
||||
}
|
||||
|
||||
public int getReward() {
|
||||
return this.reward;
|
||||
}
|
||||
|
||||
public int getGenesisCreationVersion() {
|
||||
return this.genesisCreationVersion;
|
||||
}
|
||||
|
||||
public long getGenesisBlockVersion() {
|
||||
return this.genesisBlockVersion;
|
||||
}
|
||||
|
||||
public long getGenesisTime() {
|
||||
return this.genesisTime;
|
||||
}
|
||||
|
||||
public long getDifficultyTarget() {
|
||||
return this.difficultyTarget;
|
||||
}
|
||||
|
||||
public String getMerkleHex() {
|
||||
return this.merkleHex;
|
||||
}
|
||||
|
||||
public long getNonce() {
|
||||
return this.nonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BitcoinyTBDRequest{" +
|
||||
"targetTimespan=" + targetTimespan +
|
||||
", targetSpacing=" + targetSpacing +
|
||||
", packetMagic=" + packetMagic +
|
||||
", port=" + port +
|
||||
", addressHeader=" + addressHeader +
|
||||
", p2shHeader=" + p2shHeader +
|
||||
", segwitAddressHrp='" + segwitAddressHrp + '\'' +
|
||||
", dumpedPrivateKeyHeader=" + dumpedPrivateKeyHeader +
|
||||
", subsidyDecreaseBlockCount=" + subsidyDecreaseBlockCount +
|
||||
", expectedGenesisHash='" + expectedGenesisHash + '\'' +
|
||||
", pubKey='" + pubKey + '\'' +
|
||||
", dnsSeeds=" + Arrays.toString(dnsSeeds) +
|
||||
", bip32HeaderP2PKHpub=" + bip32HeaderP2PKHpub +
|
||||
", bip32HeaderP2PKHpriv=" + bip32HeaderP2PKHpriv +
|
||||
", addressHeaderTestnet=" + addressHeaderTestnet +
|
||||
", bip32HeaderP2PKHpubTestnet=" + bip32HeaderP2PKHpubTestnet +
|
||||
", bip32HeaderP2PKHprivTestnet=" + bip32HeaderP2PKHprivTestnet +
|
||||
", id='" + id + '\'' +
|
||||
", majorityEnforceBlockUpgrade=" + majorityEnforceBlockUpgrade +
|
||||
", majorityRejectBlockOutdated=" + majorityRejectBlockOutdated +
|
||||
", majorityWindow=" + majorityWindow +
|
||||
", code='" + code + '\'' +
|
||||
", mCode='" + mCode + '\'' +
|
||||
", baseCode='" + baseCode + '\'' +
|
||||
", minNonDustOutput=" + minNonDustOutput +
|
||||
", uriScheme='" + uriScheme + '\'' +
|
||||
", protocolVersionMinimum=" + protocolVersionMinimum +
|
||||
", protocolVersionCurrent=" + protocolVersionCurrent +
|
||||
", hasMaxMoney=" + hasMaxMoney +
|
||||
", maxMoney=" + maxMoney +
|
||||
", currencyCode='" + currencyCode + '\'' +
|
||||
", minimumOrderAmount=" + minimumOrderAmount +
|
||||
", feePerKb=" + feePerKb +
|
||||
", networkName='" + networkName + '\'' +
|
||||
", feeCeiling=" + feeCeiling +
|
||||
", extendedPublicKey='" + extendedPublicKey + '\'' +
|
||||
", sendAmount=" + sendAmount +
|
||||
", sendingFeePerByte=" + sendingFeePerByte +
|
||||
", receivingAddress='" + receivingAddress + '\'' +
|
||||
", extendedPrivateKey='" + extendedPrivateKey + '\'' +
|
||||
", serverInfo=" + serverInfo +
|
||||
", scriptSig='" + scriptSig + '\'' +
|
||||
", scriptHex='" + scriptHex + '\'' +
|
||||
", reward=" + reward +
|
||||
", genesisCreationVersion=" + genesisCreationVersion +
|
||||
", genesisBlockVersion=" + genesisBlockVersion +
|
||||
", genesisTime=" + genesisTime +
|
||||
", difficultyTarget=" + difficultyTarget +
|
||||
", merkleHex='" + merkleHex + '\'' +
|
||||
", nonce=" + nonce +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class DigibyteSendRequest {
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class LitecoinSendRequest {
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class RavencoinSendRequest {
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.qortal.crosschain.SupportedBlockchain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crosschain.SupportedBlockchain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotCreateRequest {
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotRespondRequest {
|
||||
|
||||
|
@ -0,0 +1,68 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import java.util.List;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotRespondRequests {
|
||||
|
||||
@Schema(description = "Foreign blockchain private key, e.g. BIP32 'm' key for Bitcoin/Litecoin starting with 'xprv'",
|
||||
example = "xprv___________________________________________________________________________________________________________")
|
||||
public String foreignKey;
|
||||
|
||||
@Schema(description = "List of address matches")
|
||||
@XmlElement(name = "addresses")
|
||||
public List<String> addresses;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT", example = "Qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")
|
||||
public String receivingAddress;
|
||||
|
||||
public TradeBotRespondRequests() {
|
||||
}
|
||||
|
||||
public TradeBotRespondRequests(String foreignKey, List<String> addresses, String receivingAddress) {
|
||||
this.foreignKey = foreignKey;
|
||||
this.addresses = addresses;
|
||||
this.receivingAddress = receivingAddress;
|
||||
}
|
||||
|
||||
@Schema(description = "Address Match")
|
||||
// All properties to be converted to JSON via JAX-RS
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class AddressMatch {
|
||||
@Schema(description = "AT Address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Receiving Address")
|
||||
public String receivingAddress;
|
||||
|
||||
// For JAX-RS
|
||||
protected AddressMatch() {
|
||||
}
|
||||
|
||||
public AddressMatch(String atAddress, String receivingAddress) {
|
||||
this.atAddress = atAddress;
|
||||
this.receivingAddress = receivingAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AddressMatch{" +
|
||||
"atAddress='" + atAddress + '\'' +
|
||||
", receivingAddress='" + receivingAddress + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TradeBotRespondRequests{" +
|
||||
"foreignKey='" + foreignKey + '\'' +
|
||||
", addresses=" + addresses +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
package org.qortal.api.proxy.resource;
|
||||
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.HTMLParser;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.DevProxyManager;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.ConnectException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.util.Enumeration;
|
||||
|
||||
|
||||
@Path("/")
|
||||
public class DevProxyServerResource {
|
||||
|
||||
@Context HttpServletRequest request;
|
||||
@Context HttpServletResponse response;
|
||||
@Context ServletContext context;
|
||||
|
||||
|
||||
@GET
|
||||
public HttpServletResponse getProxyIndex() {
|
||||
return this.proxy("/");
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{path:.*}")
|
||||
public HttpServletResponse getProxyPath(@PathParam("path") String inPath) {
|
||||
return this.proxy(inPath);
|
||||
}
|
||||
|
||||
private HttpServletResponse proxy(String inPath) {
|
||||
try {
|
||||
String source = DevProxyManager.getInstance().getSourceHostAndPort();
|
||||
|
||||
if (!inPath.startsWith("/")) {
|
||||
inPath = "/" + inPath;
|
||||
}
|
||||
|
||||
String queryString = request.getQueryString() != null ? "?" + request.getQueryString() : "";
|
||||
|
||||
// Open URL
|
||||
URL url = new URL(String.format("http://%s%s%s", source, inPath, queryString));
|
||||
HttpURLConnection con = (HttpURLConnection) url.openConnection();
|
||||
|
||||
// Proxy the request data
|
||||
this.proxyRequestToConnection(request, con);
|
||||
|
||||
try {
|
||||
// Make the request and proxy the response code
|
||||
response.setStatus(con.getResponseCode());
|
||||
}
|
||||
catch (ConnectException e) {
|
||||
|
||||
// Tey converting localhost / 127.0.0.1 to IPv6 [::1]
|
||||
if (source.startsWith("localhost") || source.startsWith("127.0.0.1")) {
|
||||
int port = 80;
|
||||
String[] parts = source.split(":");
|
||||
if (parts.length > 1) {
|
||||
port = Integer.parseInt(parts[1]);
|
||||
}
|
||||
source = String.format("[::1]:%d", port);
|
||||
}
|
||||
|
||||
// Retry connection
|
||||
url = new URL(String.format("http://%s%s%s", source, inPath, queryString));
|
||||
con = (HttpURLConnection) url.openConnection();
|
||||
this.proxyRequestToConnection(request, con);
|
||||
response.setStatus(con.getResponseCode());
|
||||
}
|
||||
|
||||
// Proxy the response data back to the caller
|
||||
this.proxyConnectionToResponse(con, response, inPath);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, e.getMessage());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private void proxyRequestToConnection(HttpServletRequest request, HttpURLConnection con) throws ProtocolException {
|
||||
// Proxy the request method
|
||||
con.setRequestMethod(request.getMethod());
|
||||
|
||||
// Proxy the request headers
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String headerName = headerNames.nextElement();
|
||||
String headerValue = request.getHeader(headerName);
|
||||
con.setRequestProperty(headerName, headerValue);
|
||||
}
|
||||
|
||||
// TODO: proxy any POST parameters from "request" to "con"
|
||||
}
|
||||
|
||||
private void proxyConnectionToResponse(HttpURLConnection con, HttpServletResponse response, String inPath) throws IOException {
|
||||
// Proxy the response headers
|
||||
for (int i = 0; ; i++) {
|
||||
String headerKey = con.getHeaderFieldKey(i);
|
||||
String headerValue = con.getHeaderField(i);
|
||||
if (headerKey != null && headerValue != null) {
|
||||
response.addHeader(headerKey, headerValue);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Read the response body
|
||||
InputStream inputStream = con.getInputStream();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
byte[] data = outputStream.toByteArray(); // TODO: limit file size that can be read into memory
|
||||
|
||||
// Close the streams
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
|
||||
// Extract filename
|
||||
String filename = "";
|
||||
if (inPath.contains("/")) {
|
||||
String[] parts = inPath.split("/");
|
||||
if (parts.length > 0) {
|
||||
filename = parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and modify output if needed
|
||||
if (HTMLParser.isHtmlFile(filename)) {
|
||||
// HTML file - needs to be parsed
|
||||
HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, "light", true);
|
||||
htmlParser.addAdditionalHeaderTags();
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' ws:; font-src 'self' data:;");
|
||||
response.setContentType(con.getContentType());
|
||||
response.setContentLength(htmlParser.getData().length);
|
||||
response.getOutputStream().write(htmlParser.getData());
|
||||
}
|
||||
else {
|
||||
// Regular file - can be streamed directly
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self'");
|
||||
response.setContentType(con.getContentType());
|
||||
response.setContentLength(data.length);
|
||||
response.getOutputStream().write(data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
@ -9,25 +10,9 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.api.model.AccountPenaltyStats;
|
||||
import org.qortal.api.model.ApiOnlineAccount;
|
||||
import org.qortal.api.model.RewardShareKeyRequest;
|
||||
@ -35,9 +20,7 @@ import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.AccountPenaltyData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.account.*;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.network.OnlineAccountLevel;
|
||||
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||
@ -59,7 +42,16 @@ import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path("/addresses")
|
||||
@Tag(name = "Addresses")
|
||||
@ -240,8 +232,7 @@ public class AddressesResource {
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by level
|
||||
@ -335,11 +326,8 @@ public class AddressesResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.NON_PRODUCTION, ApiError.REPOSITORY_ISSUE})
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.REPOSITORY_ISSUE})
|
||||
public String fromPublicKey(@PathParam("publickey") String publicKey58) {
|
||||
if (Settings.getInstance().isApiRestricted())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||
|
||||
// Decode public key
|
||||
byte[] publicKey;
|
||||
try {
|
||||
@ -638,4 +626,160 @@ public class AddressesResource {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@GET
|
||||
@Path("/sponsorship/{address}")
|
||||
@Operation(
|
||||
summary = "Returns sponsorship statistics for an account",
|
||||
description = "Returns sponsorship statistics for an account, excluding the recipients that get real reward shares",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the statistics",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SponsorshipReport.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public SponsorshipReport getSponsorshipReport(
|
||||
@PathParam("address") String address,
|
||||
@QueryParam(("realRewardShareRecipient")) String[] realRewardShareRecipients) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(address, realRewardShareRecipients);
|
||||
// Not found?
|
||||
if (report == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
return report;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/sponsorship/{address}/sponsor")
|
||||
@Operation(
|
||||
summary = "Returns sponsorship statistics for an account's sponsor",
|
||||
description = "Returns sponsorship statistics for an account's sponsor, excluding the recipients that get real reward shares",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the statistics",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SponsorshipReport.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public SponsorshipReport getSponsorshipReportForSponsor(
|
||||
@PathParam("address") String address,
|
||||
@QueryParam("realRewardShareRecipient") String[] realRewardShareRecipients) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// get sponsor
|
||||
Optional<String> sponsor = repository.getAccountRepository().getSponsor(address);
|
||||
|
||||
// if there is not sponsor, throw error
|
||||
if(sponsor.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
// get report for sponsor
|
||||
SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(sponsor.get(), realRewardShareRecipients);
|
||||
|
||||
// Not found?
|
||||
if (report == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
return report;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/mintership/{address}")
|
||||
@Operation(
|
||||
summary = "Returns mintership statistics for an account",
|
||||
description = "Returns mintership statistics for an account",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the statistics",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = MintershipReport.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public MintershipReport getMintershipReport(@PathParam("address") String address,
|
||||
@QueryParam("realRewardShareRecipient") String[] realRewardShareRecipients ) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// get sponsorship report for minter, fetch a list of one minter
|
||||
SponsorshipReport report = repository.getAccountRepository().getMintershipReport(address, account -> List.of(account));
|
||||
|
||||
// Not found?
|
||||
if (report == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
// since the report is for one minter, must get sponsee count separately
|
||||
int sponseeCount = repository.getAccountRepository().getSponseeAddresses(address, realRewardShareRecipients).size();
|
||||
|
||||
// since the report is for one minter, must get the first name from a array of names that should be size 1
|
||||
String name = report.getNames().length > 0 ? report.getNames()[0] : null;
|
||||
|
||||
// transform sponsorship report to mintership report
|
||||
MintershipReport mintershipReport
|
||||
= new MintershipReport(
|
||||
report.getAddress(),
|
||||
report.getLevel(),
|
||||
report.getBlocksMinted(),
|
||||
report.getAdjustments(),
|
||||
report.getPenalties(),
|
||||
report.isTransfer(),
|
||||
name,
|
||||
sponseeCount,
|
||||
report.getAvgBalance(),
|
||||
report.getArbitraryCount(),
|
||||
report.getTransferAssetCount(),
|
||||
report.getTransferPrivsCount(),
|
||||
report.getSellCount(),
|
||||
report.getSellAmount(),
|
||||
report.getBuyCount(),
|
||||
report.getBuyAmount()
|
||||
);
|
||||
|
||||
return mintershipReport;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/levels/{minLevel}")
|
||||
@Operation(
|
||||
summary = "Return accounts with levels greater than or equal to input",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "online accounts",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AddressLevelPairing.class)))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
|
||||
public List<AddressLevelPairing> getAddressLevelPairings(@PathParam("minLevel") int minLevel) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// get the level address pairings
|
||||
List<AddressLevelPairing> pairings = repository.getAccountRepository().getAddressLevelPairings(minLevel);
|
||||
|
||||
return pairings;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,12 +13,6 @@ import io.swagger.v3.oas.models.media.MediaType;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.oas.models.parameters.Parameter;
|
||||
import io.swagger.v3.oas.models.responses.ApiResponse;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.ws.rs.Path;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.api.ApiError;
|
||||
@ -27,6 +21,10 @@ import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiService;
|
||||
import org.qortal.globalization.Translator;
|
||||
|
||||
import javax.ws.rs.Path;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Locale;
|
||||
|
||||
public class AnnotationPostProcessor implements ReaderListener {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(AnnotationPostProcessor.class);
|
||||
|
@ -9,7 +9,6 @@ import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.annotations.security.SecuritySchemes;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import org.qortal.api.Security;
|
||||
|
||||
@OpenAPIDefinition(
|
||||
|
@ -1,5 +1,10 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.glassfish.jersey.server.ParamException;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
@ -8,11 +13,6 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.glassfish.jersey.server.ParamException;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
@Provider
|
||||
public class ApiExceptionMapper implements ExceptionMapper<RuntimeException> {
|
||||
|
||||
|
@ -1,18 +1,20 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
|
@ -12,24 +12,6 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.FileNameMap;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -45,11 +27,15 @@ import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataCacheManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataRenderManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryMetadataManager;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.arbitrary.*;
|
||||
import org.qortal.data.arbitrary.ArbitraryCategoryInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceData;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@ -65,10 +51,26 @@ import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.ZipUtils;
|
||||
import org.qortal.utils.*;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.FileNameMap;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Path("/arbitrary")
|
||||
@Tag(name = "Arbitrary")
|
||||
@ -89,12 +91,12 @@ public class ArbitraryResource {
|
||||
"- If default is set to true, only resources without identifiers will be returned.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceInfo.class))
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceData.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<ArbitraryResourceInfo> getResources(
|
||||
public List<ArbitraryResourceData> getResources(
|
||||
@QueryParam("service") Service service,
|
||||
@QueryParam("name") String name,
|
||||
@QueryParam("identifier") String identifier,
|
||||
@ -117,7 +119,7 @@ public class ArbitraryResource {
|
||||
|
||||
// Ensure that "default" and "identifier" parameters cannot coexist
|
||||
boolean defaultRes = Boolean.TRUE.equals(defaultResource);
|
||||
if (defaultRes == true && identifier != null) {
|
||||
if (defaultRes && identifier != null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "identifier cannot be specified when requesting a default resource");
|
||||
}
|
||||
|
||||
@ -136,20 +138,14 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||
.getArbitraryResources(service, identifier, names, defaultRes, followedOnly, excludeBlocked, limit, offset, reverse);
|
||||
List<ArbitraryResourceData> resources = repository.getArbitraryRepository()
|
||||
.getArbitraryResources(service, identifier, names, defaultRes, followedOnly, excludeBlocked,
|
||||
includeMetadata, includeStatus, limit, offset, reverse);
|
||||
|
||||
if (resources == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
|
||||
} catch (DataException e) {
|
||||
@ -164,23 +160,30 @@ public class ArbitraryResource {
|
||||
"If default is set to true, only resources without identifiers will be returned.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceInfo.class))
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceData.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<ArbitraryResourceInfo> searchResources(
|
||||
public List<ArbitraryResourceData> searchResources(
|
||||
@QueryParam("service") Service service,
|
||||
@Parameter(description = "Query (searches both name and identifier fields)") @QueryParam("query") String query,
|
||||
@Parameter(description = "Query (searches name, identifier, title and description fields)") @QueryParam("query") String query,
|
||||
@Parameter(description = "Identifier (searches identifier field only)") @QueryParam("identifier") String identifier,
|
||||
@Parameter(description = "Name (searches name field only)") @QueryParam("name") List<String> names,
|
||||
@Parameter(description = "Title (searches title metadata field only)") @QueryParam("title") String title,
|
||||
@Parameter(description = "Description (searches description metadata field only)") @QueryParam("description") String description,
|
||||
@Parameter(description = "Prefix only (if true, only the beginning of fields are matched)") @QueryParam("prefix") Boolean prefixOnly,
|
||||
@Parameter(description = "Exact match names only (if true, partial name matches are excluded)") @QueryParam("exactmatchnames") Boolean exactMatchNamesOnly,
|
||||
@Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource,
|
||||
@Parameter(description = "Search mode") @QueryParam("mode") SearchMode mode,
|
||||
@Parameter(description = "Min level") @QueryParam("minlevel") Integer minLevel,
|
||||
@Parameter(description = "Filter names by list (exact matches only)") @QueryParam("namefilter") String nameListFilter,
|
||||
@Parameter(description = "Include followed names only") @QueryParam("followedonly") Boolean followedOnly,
|
||||
@Parameter(description = "Exclude blocked content") @QueryParam("excludeblocked") Boolean excludeBlocked,
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata,
|
||||
@Parameter(description = "Creation date before timestamp") @QueryParam("before") Long before,
|
||||
@Parameter(description = "Creation date after timestamp") @QueryParam("after") Long after,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
@ -202,18 +205,62 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||
.searchArbitraryResources(service, query, identifier, names, usePrefixOnly, exactMatchNames, defaultRes, followedOnly, excludeBlocked, limit, offset, reverse);
|
||||
// Move names to exact match list, if requested
|
||||
if (exactMatchNamesOnly != null && exactMatchNamesOnly && names != null) {
|
||||
exactMatchNames.addAll(names);
|
||||
names = null;
|
||||
}
|
||||
|
||||
List<ArbitraryResourceData> resources = repository.getArbitraryRepository()
|
||||
.searchArbitraryResources(service, query, identifier, names, title, description, usePrefixOnly,
|
||||
exactMatchNames, defaultRes, mode, minLevel, followedOnly, excludeBlocked, includeMetadata, includeStatus,
|
||||
before, after, limit, offset, reverse);
|
||||
|
||||
if (resources == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
return resources;
|
||||
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resources/searchsimple")
|
||||
@Operation(
|
||||
summary = "Search arbitrary resources available on chain, optionally filtered by service.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceData.class))
|
||||
)
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<ArbitraryResourceData> searchResourcesSimple(
|
||||
@QueryParam("service") Service service,
|
||||
@Parameter(description = "Identifier (searches identifier field only)") @QueryParam("identifier") String identifier,
|
||||
@Parameter(description = "Name (searches name field only)") @QueryParam("name") List<String> names,
|
||||
@Parameter(description = "Prefix only (if true, only the beginning of fields are matched)") @QueryParam("prefix") Boolean prefixOnly,
|
||||
@Parameter(description = "Case insensitive (ignore leter case on search)") @QueryParam("caseInsensitive") Boolean caseInsensitive,
|
||||
@Parameter(description = "Creation date before timestamp") @QueryParam("before") Long before,
|
||||
@Parameter(description = "Creation date after timestamp") @QueryParam("after") Long after,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
boolean usePrefixOnly = Boolean.TRUE.equals(prefixOnly);
|
||||
boolean ignoreCase = Boolean.TRUE.equals(caseInsensitive);
|
||||
|
||||
List<ArbitraryResourceData> resources = repository.getArbitraryRepository()
|
||||
.searchArbitraryResourcesSimple(service, identifier, names, usePrefixOnly,
|
||||
before, after, limit, offset, reverse, ignoreCase);
|
||||
|
||||
if (resources == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return resources;
|
||||
@ -234,16 +281,14 @@ public class ArbitraryResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ArbitraryResourceStatus getDefaultResourceStatus(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
public ArbitraryResourceStatus getDefaultResourceStatus(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, null, apiKey);
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, null, null);
|
||||
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, null, build);
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, null, build, true);
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -257,14 +302,12 @@ public class ArbitraryResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public FileProperties getResourceProperties(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier) {
|
||||
public FileProperties getResourceProperties(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier) {
|
||||
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier, apiKey);
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier, null);
|
||||
|
||||
return this.getFileProperties(service, name, identifier);
|
||||
}
|
||||
@ -280,17 +323,15 @@ public class ArbitraryResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ArbitraryResourceStatus getResourceStatus(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
public ArbitraryResourceStatus getResourceStatus(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier, apiKey);
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier, null);
|
||||
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, identifier, build);
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, identifier, build, true);
|
||||
}
|
||||
|
||||
|
||||
@ -475,27 +516,25 @@ public class ArbitraryResource {
|
||||
summary = "List arbitrary resources hosted by this node",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceInfo.class))
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceData.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<ArbitraryResourceInfo> getHostedResources(
|
||||
public List<ArbitraryResourceData> getHostedResources(
|
||||
@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@QueryParam("query") String query) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
List<ArbitraryResourceInfo> resources = new ArrayList<>();
|
||||
List<ArbitraryResourceData> resources = new ArrayList<>();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList;
|
||||
|
||||
if (query == null || query.equals("")) {
|
||||
if (query == null || query.isEmpty()) {
|
||||
transactionDataList = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, limit, offset);
|
||||
} else {
|
||||
transactionDataList = ArbitraryDataStorageManager.getInstance().searchHostedTransactions(repository,query, limit, offset);
|
||||
@ -505,22 +544,15 @@ public class ArbitraryResource {
|
||||
if (transactionData.getService() == null) {
|
||||
continue;
|
||||
}
|
||||
ArbitraryResourceInfo arbitraryResourceInfo = new ArbitraryResourceInfo();
|
||||
arbitraryResourceInfo.name = transactionData.getName();
|
||||
arbitraryResourceInfo.service = transactionData.getService();
|
||||
arbitraryResourceInfo.identifier = transactionData.getIdentifier();
|
||||
if (!resources.contains(arbitraryResourceInfo)) {
|
||||
resources.add(arbitraryResourceInfo);
|
||||
ArbitraryResourceData arbitraryResourceData = new ArbitraryResourceData();
|
||||
arbitraryResourceData.name = transactionData.getName();
|
||||
arbitraryResourceData.service = transactionData.getService();
|
||||
arbitraryResourceData.identifier = transactionData.getIdentifier();
|
||||
if (!resources.contains(arbitraryResourceData)) {
|
||||
resources.add(arbitraryResourceData);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
|
||||
} catch (DataException e) {
|
||||
@ -547,8 +579,14 @@ public class ArbitraryResource {
|
||||
@PathParam("identifier") String identifier) {
|
||||
|
||||
Security.checkApiCallAllowed(request);
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.delete(false);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.delete(repository, false);
|
||||
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@ -640,9 +678,7 @@ public class ArbitraryResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse get(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
public HttpServletResponse get(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("filepath") String filepath,
|
||||
@QueryParam("encoding") String encoding,
|
||||
@ -675,9 +711,7 @@ public class ArbitraryResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse get(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
public HttpServletResponse get(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("filepath") String filepath,
|
||||
@ -688,7 +722,7 @@ public class ArbitraryResource {
|
||||
|
||||
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||
Security.checkApiCallAllowed(request, apiKey);
|
||||
Security.checkApiCallAllowed(request, null);
|
||||
}
|
||||
|
||||
return this.download(service, name, identifier, filepath, encoding, rebuild, async, attempts);
|
||||
@ -713,17 +747,13 @@ public class ArbitraryResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ArbitraryResourceMetadata getMetadata(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
public ArbitraryResourceMetadata getMetadata(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
|
||||
try {
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, false);
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, true);
|
||||
if (transactionMetadata != null) {
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, true);
|
||||
if (resourceMetadata != null) {
|
||||
@ -1126,6 +1156,36 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Path("/resources/cache/rebuild")
|
||||
@Operation(
|
||||
summary = "Rebuild arbitrary resources cache from transactions",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true on success",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "boolean"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String rebuildCache(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ArbitraryDataCacheManager.getInstance().buildArbitraryResourcesCache(repository, true);
|
||||
|
||||
return "true";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Shared methods
|
||||
|
||||
private String preview(String directoryPath, Service service) {
|
||||
@ -1172,7 +1232,11 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, error);
|
||||
}
|
||||
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NO_TIME_SYNC);
|
||||
}
|
||||
final Long minLatestBlockTimestamp = now - (60 * 60 * 1000L);
|
||||
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
|
||||
}
|
||||
@ -1230,14 +1294,14 @@ public class ArbitraryResource {
|
||||
// The actual data will be in a randomly-named subfolder of tempDirectory
|
||||
// Remove hidden folders, i.e. starting with "_", as some systems can add them, e.g. "__MACOSX"
|
||||
String[] files = tempDirectory.toFile().list((parent, child) -> !child.startsWith("_"));
|
||||
if (files.length == 1) { // Single directory or file only
|
||||
if (files != null && files.length == 1) { // Single directory or file only
|
||||
path = Paths.get(tempDirectory.toString(), files[0]).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finish here if user has requested a preview
|
||||
if (preview != null && preview == true) {
|
||||
if (preview != null && preview) {
|
||||
return this.preview(path, service);
|
||||
}
|
||||
|
||||
@ -1262,15 +1326,16 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
|
||||
} catch (DataException | IOException e) {
|
||||
} catch (Exception e) {
|
||||
LOGGER.info("Exception when publishing data: ", e);
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private HttpServletResponse download(Service service, String name, String identifier, String filepath, String encoding, boolean rebuild, boolean async, Integer maxAttempts) {
|
||||
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
try {
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
|
||||
int attempts = 0;
|
||||
if (maxAttempts == null) {
|
||||
@ -1310,7 +1375,7 @@ public class ArbitraryResource {
|
||||
if (filepath == null || filepath.isEmpty()) {
|
||||
// No file path supplied - so check if this is a single file resource
|
||||
String[] files = ArrayUtils.removeElement(outputPath.toFile().list(), ".qortal");
|
||||
if (files.length == 1) {
|
||||
if (files != null && files.length == 1) {
|
||||
// This is a single file resource
|
||||
filepath = files[0];
|
||||
}
|
||||
@ -1320,20 +1385,50 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: limit file size that can be read into memory
|
||||
java.nio.file.Path path = Paths.get(outputPath.toString(), filepath);
|
||||
if (!Files.exists(path)) {
|
||||
String message = String.format("No file exists at filepath: %s", filepath);
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, message);
|
||||
}
|
||||
|
||||
byte[] data = Files.readAllBytes(path);
|
||||
byte[] data;
|
||||
int fileSize = (int)path.toFile().length();
|
||||
int length = fileSize;
|
||||
|
||||
// Parse "Range" header
|
||||
Integer rangeStart = null;
|
||||
Integer rangeEnd = null;
|
||||
String range = request.getHeader("Range");
|
||||
if (range != null) {
|
||||
range = range.replace("bytes=", "");
|
||||
String[] parts = range.split("-");
|
||||
rangeStart = (parts != null && parts.length > 0) ? Integer.parseInt(parts[0]) : null;
|
||||
rangeEnd = (parts != null && parts.length > 1) ? Integer.parseInt(parts[1]) : fileSize;
|
||||
}
|
||||
|
||||
if (rangeStart != null && rangeEnd != null) {
|
||||
// We have a range, so update the requested length
|
||||
length = rangeEnd - rangeStart;
|
||||
}
|
||||
|
||||
if (length < fileSize && encoding == null) {
|
||||
// Partial content requested, and not encoding the data
|
||||
response.setStatus(206);
|
||||
response.addHeader("Content-Range", String.format("bytes %d-%d/%d", rangeStart, rangeEnd-1, fileSize));
|
||||
data = FilesystemUtils.readFromFile(path.toString(), rangeStart, length);
|
||||
}
|
||||
else {
|
||||
// Full content requested (or encoded data)
|
||||
response.setStatus(200);
|
||||
data = Files.readAllBytes(path); // TODO: limit file size that can be read into memory
|
||||
}
|
||||
|
||||
// Encode the data if requested
|
||||
if (encoding != null && Objects.equals(encoding.toLowerCase(), "base64")) {
|
||||
data = Base64.encode(data);
|
||||
}
|
||||
|
||||
response.addHeader("Accept-Ranges", "bytes");
|
||||
response.setContentType(context.getMimeType(path.toString()));
|
||||
response.setContentLength(data.length);
|
||||
response.getOutputStream().write(data);
|
||||
@ -1346,8 +1441,8 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
private FileProperties getFileProperties(Service service, String name, String identifier) {
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
try {
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
arbitraryDataReader.loadSynchronously(false);
|
||||
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
||||
if (outputPath == null) {
|
||||
|
@ -8,22 +8,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
@ -39,27 +23,27 @@ import org.qortal.data.asset.AssetData;
|
||||
import org.qortal.data.asset.OrderData;
|
||||
import org.qortal.data.asset.RecentTradeData;
|
||||
import org.qortal.data.asset.TradeData;
|
||||
import org.qortal.data.transaction.CancelAssetOrderTransactionData;
|
||||
import org.qortal.data.transaction.CreateAssetOrderTransactionData;
|
||||
import org.qortal.data.transaction.IssueAssetTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
import org.qortal.data.transaction.UpdateAssetTransactionData;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.repository.AccountRepository.BalanceOrdering;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.AccountRepository.BalanceOrdering;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.CancelAssetOrderTransactionTransformer;
|
||||
import org.qortal.transform.transaction.CreateAssetOrderTransactionTransformer;
|
||||
import org.qortal.transform.transaction.IssueAssetTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransferAssetTransactionTransformer;
|
||||
import org.qortal.transform.transaction.UpdateAssetTransactionTransformer;
|
||||
import org.qortal.transform.transaction.*;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path("/assets")
|
||||
@Tag(name = "Assets")
|
||||
public class AssetsResource {
|
||||
|
@ -8,23 +8,12 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.AtCreationRequest;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
@ -37,10 +26,20 @@ import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Path("/at")
|
||||
@Tag(name = "Automated Transactions")
|
||||
public class AtResource {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AtResource.class);
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
@ -156,6 +155,52 @@ public class AtResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/create")
|
||||
@Operation(
|
||||
summary = "Create base58-encoded AT creation bytes from the provided parameters",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = AtCreationRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "AT creation bytes suitable for use in a DEPLOY_AT transaction",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String create(AtCreationRequest atCreationRequest) {
|
||||
if (atCreationRequest.getCiyamAtVersion() < 2) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "ciyamAtVersion must be at least 2");
|
||||
}
|
||||
if (atCreationRequest.getCodeBytes() == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Valid codeBytesBase64 must be supplied");
|
||||
}
|
||||
if (atCreationRequest.getDataBytes() == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Valid dataBytesBase64 must be supplied");
|
||||
}
|
||||
|
||||
byte[] creationBytes = MachineState.toCreationBytes(
|
||||
atCreationRequest.getCiyamAtVersion(),
|
||||
atCreationRequest.getCodeBytes(),
|
||||
atCreationRequest.getDataBytes(),
|
||||
atCreationRequest.getNumCallStackPages(),
|
||||
atCreationRequest.getNumUserStackPages(),
|
||||
atCreationRequest.getMinActivationAmount()
|
||||
);
|
||||
return Base58.encode(creationBytes);
|
||||
}
|
||||
@POST
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, DEPLOY_AT transaction",
|
||||
|
@ -8,26 +8,6 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
@ -35,7 +15,6 @@ import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.BlockMintingInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
@ -50,6 +29,23 @@ import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
@Path("/blocks")
|
||||
@Tag(name = "Blocks")
|
||||
public class BlocksResource {
|
||||
@ -90,7 +86,7 @@ public class BlocksResource {
|
||||
// Check the database first
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@ -99,7 +95,7 @@ public class BlocksResource {
|
||||
// Not found, so try the block archive
|
||||
blockData = repository.getBlockArchiveRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@ -222,14 +218,25 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Check if the block exists in either the database or archive
|
||||
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0 &&
|
||||
repository.getBlockArchiveRepository().getHeightFromSignature(signature) == 0) {
|
||||
// Not found in either the database or archive
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
// Check if the block exists in either the database or archive
|
||||
int height = repository.getBlockRepository().getHeightFromSignature(signature);
|
||||
if (height == 0) {
|
||||
height = repository.getBlockArchiveRepository().getHeightFromSignature(signature);
|
||||
if (height == 0) {
|
||||
// Not found in either the database or archive
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, height, height);
|
||||
|
||||
// Expand signatures to transactions
|
||||
List<TransactionData> transactions = new ArrayList<>(signatures.size());
|
||||
for (byte[] s : signatures) {
|
||||
transactions.add(repository.getTransactionRepository().fromSignature(s));
|
||||
}
|
||||
|
||||
return transactions;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@ -297,7 +304,7 @@ public class BlocksResource {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
|
||||
@ -467,7 +474,7 @@ public class BlocksResource {
|
||||
// Firstly check the database
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@ -476,7 +483,7 @@ public class BlocksResource {
|
||||
// Not found, so try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@ -535,6 +542,7 @@ public class BlocksResource {
|
||||
}
|
||||
}
|
||||
|
||||
String minterAddress = Account.getRewardShareMintingAddress(repository, blockData.getMinterPublicKey());
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
|
||||
if (minterLevel == 0)
|
||||
// This may be unavailable when requesting a trimmed block
|
||||
@ -547,6 +555,7 @@ public class BlocksResource {
|
||||
|
||||
BlockMintingInfo blockMintingInfo = new BlockMintingInfo();
|
||||
blockMintingInfo.minterPublicKey = blockData.getMinterPublicKey();
|
||||
blockMintingInfo.minterAddress = minterAddress;
|
||||
blockMintingInfo.minterLevel = minterLevel;
|
||||
blockMintingInfo.onlineAccountsCount = blockData.getOnlineAccountsCount();
|
||||
blockMintingInfo.maxDistance = new BigDecimal(block.MAX_DISTANCE);
|
||||
@ -589,7 +598,7 @@ public class BlocksResource {
|
||||
if (height > 1) {
|
||||
// Found match in Blocks table
|
||||
blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
@ -607,7 +616,7 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
|
||||
@ -644,7 +653,7 @@ public class BlocksResource {
|
||||
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<BlockData> blocks = new ArrayList<>();
|
||||
boolean shouldReverse = (reverse != null && reverse == true);
|
||||
boolean shouldReverse = (reverse != null && reverse);
|
||||
|
||||
int i = 0;
|
||||
while (i < count) {
|
||||
@ -657,7 +666,7 @@ public class BlocksResource {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
if (includeOnlineSignatures == null || !includeOnlineSignatures) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
|
||||
@ -880,5 +889,4 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
@ -9,14 +10,6 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
@ -38,7 +31,11 @@ import org.qortal.transform.transaction.ChatTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
@ -119,6 +116,75 @@ public class ChatResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/messages/count")
|
||||
@Operation(
|
||||
summary = "Count chat messages",
|
||||
description = "Returns count of CHAT messages that match criteria. Must provide EITHER 'txGroupId' OR two 'involving' addresses.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "count of messages",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "integer"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public int countChatMessages(@QueryParam("before") Long before, @QueryParam("after") Long after,
|
||||
@QueryParam("txGroupId") Integer txGroupId,
|
||||
@QueryParam("involving") List<String> involvingAddresses,
|
||||
@QueryParam("reference") String reference,
|
||||
@QueryParam("chatreference") String chatReference,
|
||||
@QueryParam("haschatreference") Boolean hasChatReference,
|
||||
@QueryParam("sender") String sender,
|
||||
@QueryParam("encoding") Encoding encoding,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
// Check args meet expectations
|
||||
if ((txGroupId == null && involvingAddresses.size() != 2)
|
||||
|| (txGroupId != null && !involvingAddresses.isEmpty()))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Check any provided addresses are valid
|
||||
if (involvingAddresses.stream().anyMatch(address -> !Crypto.isValidAddress(address)))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (before != null && before < 1500000000000L)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
if (after != null && after < 1500000000000L)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
byte[] referenceBytes = null;
|
||||
if (reference != null)
|
||||
referenceBytes = Base58.decode(reference);
|
||||
|
||||
byte[] chatReferenceBytes = null;
|
||||
if (chatReference != null)
|
||||
chatReferenceBytes = Base58.decode(chatReference);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getChatRepository().getMessagesMatchingCriteria(
|
||||
before,
|
||||
after,
|
||||
txGroupId,
|
||||
referenceBytes,
|
||||
chatReferenceBytes,
|
||||
hasChatReference,
|
||||
involvingAddresses,
|
||||
sender,
|
||||
encoding,
|
||||
limit, offset, reverse).size();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/message/{signature}")
|
||||
@Operation(
|
||||
@ -168,17 +234,21 @@ public class ChatResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public ActiveChats getActiveChats(@PathParam("address") String address, @QueryParam("encoding") Encoding encoding) {
|
||||
public ActiveChats getActiveChats(
|
||||
@PathParam("address") String address,
|
||||
@QueryParam("encoding") Encoding encoding,
|
||||
@QueryParam("haschatreference") Boolean hasChatReference
|
||||
) {
|
||||
if (address == null || !Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getChatRepository().getActiveChats(address, encoding);
|
||||
return repository.getChatRepository().getActiveChats(address, encoding, hasChatReference);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, CHAT transaction",
|
||||
|
@ -7,17 +7,6 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
@ -27,9 +16,9 @@ import org.qortal.api.model.CrossChainBuildRequest;
|
||||
import org.qortal.api.model.CrossChainDualSecretRequest;
|
||||
import org.qortal.api.model.CrossChainTradeRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.crosschain.BitcoinACCTv1;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
@ -53,6 +42,15 @@ import org.qortal.transform.transaction.MessageTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
@Path("/crosschain/BitcoinACCTv1")
|
||||
@Tag(name = "Cross-Chain (BitcoinACCTv1)")
|
||||
public class CrossChainBitcoinACCTv1Resource {
|
||||
|
@ -8,26 +8,31 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.AddressRequest;
|
||||
import org.qortal.api.model.crosschain.BitcoinSendRequest;
|
||||
import org.qortal.crosschain.AddressInfo;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.ChainableServer;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.ServerConnectionInfo;
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
import org.qortal.crosschain.ServerConfigurationInfo;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
|
||||
@Path("/crosschain/btc")
|
||||
@Tag(name = "Cross-Chain (Bitcoin)")
|
||||
@ -151,39 +156,38 @@ public class CrossChainBitcoinResource {
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/unusedaddress")
|
||||
@Path("/addressinfos")
|
||||
@Operation(
|
||||
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "BIP32 'm' private/public key in base58",
|
||||
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||
)
|
||||
}
|
||||
summary = "Returns information for each address for a hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = AddressRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = AddressInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String getUnusedBitcoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||
public List<AddressInfo> getBitcoinAddressInfos(@HeaderParam(Security.API_KEY_HEADER) String apiKey, AddressRequest addressRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
if (!bitcoin.isValidDeterministicKey(key58))
|
||||
if (!bitcoin.isValidDeterministicKey(addressRequest.xpub58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
return bitcoin.getUnusedReceiveAddress(key58);
|
||||
return bitcoin.getWalletAddressInfos(addressRequest.xpub58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
@ -245,4 +249,312 @@ public class CrossChainBitcoinResource {
|
||||
return spendTransaction.getTxId().toString();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverinfos")
|
||||
@Operation(
|
||||
summary = "Returns current Bitcoin server configuration",
|
||||
description = "Returns current Bitcoin server locations and use status",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConfigurationInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public ServerConfigurationInfo getServerConfiguration() {
|
||||
|
||||
return CrossChainUtils.buildServerConfigurationInfo(Bitcoin.getInstance());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverconnectionhistory")
|
||||
@Operation(
|
||||
summary = "Returns Bitcoin server connection history",
|
||||
description = "Returns Bitcoin server connection history",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ServerConnectionInfo> getServerConnectionHistory() {
|
||||
|
||||
return CrossChainUtils.buildServerConnectionHistory(Bitcoin.getInstance());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/addserver")
|
||||
@Operation(
|
||||
summary = "Add server to list of Bitcoin servers",
|
||||
description = "Add server to list of Bitcoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if added, false if not added",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.addServer( Bitcoin.getInstance(), server )) {
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/removeserver")
|
||||
@Operation(
|
||||
summary = "Remove server from list of Bitcoin servers",
|
||||
description = "Remove server from list of Bitcoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if removed, otherwise",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.removeServer( Bitcoin.getInstance(), server ) ) {
|
||||
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/setcurrentserver")
|
||||
@Operation(
|
||||
summary = "Set current Bitcoin server",
|
||||
description = "Set current Bitcoin server",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "connection info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConnectionInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if( serverInfo.getConnectionType() == null ||
|
||||
serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
try {
|
||||
return CrossChainUtils.setCurrentServer( Bitcoin.getInstance(), serverInfo );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ServerConnectionInfo(
|
||||
serverInfo,
|
||||
CrossChainUtils.CORE_API_CALL,
|
||||
true,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
CrossChainUtils.getNotes(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/feekb")
|
||||
@Operation(
|
||||
summary = "Returns Bitcoin fee per Kb.",
|
||||
description = "Returns Bitcoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getBitcoinFeePerKb() {
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
return String.valueOf(bitcoin.getFeePerKb().value);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeekb")
|
||||
@Operation(
|
||||
summary = "Sets Bitcoin fee per Kb.",
|
||||
description = "Sets Bitcoin fee per Kb.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee per Kb",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setBitcoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeePerKb(bitcoin, fee);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "Returns Bitcoin fee per Kb.",
|
||||
description = "Returns Bitcoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getBitcoinFeeCeiling() {
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
return String.valueOf(bitcoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "Sets Bitcoin fee ceiling.",
|
||||
description = "Sets Bitcoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setBitcoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeCeiling(bitcoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,26 +8,31 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.AddressRequest;
|
||||
import org.qortal.api.model.crosschain.DigibyteSendRequest;
|
||||
import org.qortal.crosschain.Digibyte;
|
||||
import org.qortal.crosschain.AddressInfo;
|
||||
import org.qortal.crosschain.ChainableServer;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Digibyte;
|
||||
import org.qortal.crosschain.ServerConnectionInfo;
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
import org.qortal.crosschain.ServerConfigurationInfo;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
|
||||
@Path("/crosschain/dgb")
|
||||
@Tag(name = "Cross-Chain (Digibyte)")
|
||||
@ -151,39 +156,38 @@ public class CrossChainDigibyteResource {
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/unusedaddress")
|
||||
@Path("/addressinfos")
|
||||
@Operation(
|
||||
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "BIP32 'm' private/public key in base58",
|
||||
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||
)
|
||||
}
|
||||
summary = "Returns information for each address for a hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = AddressRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = AddressInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String getUnusedDigibyteReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||
public List<AddressInfo> getDigibyteAddressInfos(@HeaderParam(Security.API_KEY_HEADER) String apiKey, AddressRequest addressRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
if (!digibyte.isValidDeterministicKey(key58))
|
||||
if (!digibyte.isValidDeterministicKey(addressRequest.xpub58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
return digibyte.getUnusedReceiveAddress(key58);
|
||||
return digibyte.getWalletAddressInfos(addressRequest.xpub58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
@ -245,4 +249,312 @@ public class CrossChainDigibyteResource {
|
||||
return spendTransaction.getTxId().toString();
|
||||
}
|
||||
|
||||
}
|
||||
@GET
|
||||
@Path("/serverinfos")
|
||||
@Operation(
|
||||
summary = "Returns current Digibyte server configuration",
|
||||
description = "Returns current Digibyte server locations and use status",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConfigurationInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public ServerConfigurationInfo getServerConfiguration() {
|
||||
|
||||
return CrossChainUtils.buildServerConfigurationInfo(Digibyte.getInstance());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverconnectionhistory")
|
||||
@Operation(
|
||||
summary = "Returns Digibyte server connection history",
|
||||
description = "Returns Digibyte server connection history",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ServerConnectionInfo> getServerConnectionHistory() {
|
||||
|
||||
return CrossChainUtils.buildServerConnectionHistory(Digibyte.getInstance());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/addserver")
|
||||
@Operation(
|
||||
summary = "Add server to list of Digibyte servers",
|
||||
description = "Add server to list of Digibyte servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if added, false if not added",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.addServer( Digibyte.getInstance(), server )) {
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/removeserver")
|
||||
@Operation(
|
||||
summary = "Remove server from list of Digibyte servers",
|
||||
description = "Remove server from list of Digibyte servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if removed, otherwise",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.removeServer( Digibyte.getInstance(), server ) ) {
|
||||
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/setcurrentserver")
|
||||
@Operation(
|
||||
summary = "Set current Digibyte server",
|
||||
description = "Set current Digibyte server",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "connection info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConnectionInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if( serverInfo.getConnectionType() == null ||
|
||||
serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
try {
|
||||
return CrossChainUtils.setCurrentServer( Digibyte.getInstance(), serverInfo );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ServerConnectionInfo(
|
||||
serverInfo,
|
||||
CrossChainUtils.CORE_API_CALL,
|
||||
true,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
CrossChainUtils.getNotes(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/feekb")
|
||||
@Operation(
|
||||
summary = "Returns Digibyte fee per Kb.",
|
||||
description = "Returns Digibyte fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDigibyteFeePerKb() {
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
return String.valueOf(digibyte.getFeePerKb().value);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeekb")
|
||||
@Operation(
|
||||
summary = "Sets Digibyte fee per Kb.",
|
||||
description = "Sets Digibyte fee per Kb.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee per Kb",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDigibyteFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeePerKb(digibyte, fee);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "Returns Digibyte fee per Kb.",
|
||||
description = "Returns Digibyte fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDigibyteFeeCeiling() {
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
return String.valueOf(digibyte.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "Sets Digibyte fee ceiling.",
|
||||
description = "Sets Digibyte fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDigibyteFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeCeiling(digibyte, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,15 +13,22 @@ import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.AddressRequest;
|
||||
import org.qortal.api.model.crosschain.DogecoinSendRequest;
|
||||
import org.qortal.crosschain.AddressInfo;
|
||||
import org.qortal.crosschain.ChainableServer;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Dogecoin;
|
||||
import org.qortal.crosschain.ServerConnectionInfo;
|
||||
import org.qortal.crosschain.ServerInfo;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
import org.qortal.crosschain.ServerConfigurationInfo;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
@ -149,39 +156,38 @@ public class CrossChainDogecoinResource {
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/unusedaddress")
|
||||
@Path("/addressinfos")
|
||||
@Operation(
|
||||
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "BIP32 'm' private/public key in base58",
|
||||
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||
)
|
||||
}
|
||||
summary = "Returns information for each address for a hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = AddressRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = AddressInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String getUnusedDogecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||
public List<AddressInfo> getDogecoinAddressInfos(@HeaderParam(Security.API_KEY_HEADER) String apiKey, AddressRequest addressRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
if (!dogecoin.isValidDeterministicKey(key58))
|
||||
if (!dogecoin.isValidDeterministicKey(addressRequest.xpub58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
return dogecoin.getUnusedReceiveAddress(key58);
|
||||
return dogecoin.getWalletAddressInfos(addressRequest.xpub58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
@ -243,4 +249,312 @@ public class CrossChainDogecoinResource {
|
||||
return spendTransaction.getTxId().toString();
|
||||
}
|
||||
|
||||
}
|
||||
@GET
|
||||
@Path("/serverinfos")
|
||||
@Operation(
|
||||
summary = "Returns current Dogecoin server configuration",
|
||||
description = "Returns current Dogecoin server locations and use status",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConfigurationInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public ServerConfigurationInfo getServerConfiguration() {
|
||||
|
||||
return CrossChainUtils.buildServerConfigurationInfo(Dogecoin.getInstance());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/serverconnectionhistory")
|
||||
@Operation(
|
||||
summary = "Returns Dogecoin server connection history",
|
||||
description = "Returns Dogecoin server connection history",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<ServerConnectionInfo> getServerConnectionHistory() {
|
||||
|
||||
return CrossChainUtils.buildServerConnectionHistory(Dogecoin.getInstance());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/addserver")
|
||||
@Operation(
|
||||
summary = "Add server to list of Dogecoin servers",
|
||||
description = "Add server to list of Dogecoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if added, false if not added",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.addServer( Dogecoin.getInstance(), server )) {
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/removeserver")
|
||||
@Operation(
|
||||
summary = "Remove server from list of Dogecoin servers",
|
||||
description = "Remove server from list of Dogecoin servers",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if removed, otherwise",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
ElectrumX.Server server = new ElectrumX.Server(
|
||||
serverInfo.getHostName(),
|
||||
ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()),
|
||||
serverInfo.getPort()
|
||||
);
|
||||
|
||||
if( CrossChainUtils.removeServer( Dogecoin.getInstance(), server ) ) {
|
||||
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | NullPointerException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/setcurrentserver")
|
||||
@Operation(
|
||||
summary = "Set current Dogecoin server",
|
||||
description = "Set current Dogecoin server",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerInfo.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "connection info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ServerConnectionInfo.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if( serverInfo.getConnectionType() == null ||
|
||||
serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
try {
|
||||
return CrossChainUtils.setCurrentServer( Dogecoin.getInstance(), serverInfo );
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ServerConnectionInfo(
|
||||
serverInfo,
|
||||
CrossChainUtils.CORE_API_CALL,
|
||||
true,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
CrossChainUtils.getNotes(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/feekb")
|
||||
@Operation(
|
||||
summary = "Returns Dogecoin fee per Kb.",
|
||||
description = "Returns Dogecoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDogecoinFeePerKb() {
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
return String.valueOf(dogecoin.getFeePerKb().value);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeekb")
|
||||
@Operation(
|
||||
summary = "Sets Dogecoin fee per Kb.",
|
||||
description = "Sets Dogecoin fee per Kb.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee per Kb",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDogecoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeePerKb(dogecoin, fee);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/feeceiling")
|
||||
@Operation(
|
||||
summary = "Returns Dogecoin fee per Kb.",
|
||||
description = "Returns Dogecoin fee per Kb.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getDogecoinFeeCeiling() {
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
return String.valueOf(dogecoin.getFeeCeiling());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/updatefeeceiling")
|
||||
@Operation(
|
||||
summary = "Sets Dogecoin fee ceiling.",
|
||||
description = "Sets Dogecoin fee ceiling.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number",
|
||||
description = "the fee",
|
||||
example = "100"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
|
||||
public String setDogecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
try {
|
||||
return CrossChainUtils.setFeeCeiling(dogecoin, fee);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,17 +7,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.*;
|
||||
@ -35,6 +24,15 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path("/crosschain/htlc")
|
||||
@Tag(name = "Cross-Chain (Hash time-locked contracts)")
|
||||
public class CrossChainHtlcResource {
|
||||
@ -159,7 +157,7 @@ public class CrossChainHtlcResource {
|
||||
htlcStatus.bitcoinP2shAddress = p2shAddress;
|
||||
htlcStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance, 8);
|
||||
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddress.toString());
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddress.toString(), false);
|
||||
|
||||
if (p2shBalance > 0L && !fundingOutputs.isEmpty()) {
|
||||
htlcStatus.canRedeem = now >= medianBlockTime * 1000L;
|
||||
@ -403,7 +401,7 @@ public class CrossChainHtlcResource {
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(decodedTradePrivateKey);
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoiny.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, decodedSecret, foreignBlockchainReceivingAccountInfo);
|
||||
@ -666,7 +664,7 @@ public class CrossChainHtlcResource {
|
||||
// ElectrumX coins
|
||||
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Validate the destination foreign blockchain address
|
||||
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
|
||||
|
@ -24,11 +24,7 @@ import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
@ -37,7 +33,6 @@ import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
@Path("/crosschain/LitecoinACCTv1")
|
||||
@Tag(name = "Cross-Chain (LitecoinACCTv1)")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user