mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-22 09:11:59 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac83827fca | ||
|
|
0811024030 | ||
|
|
737d35463d | ||
|
|
2ed1eaba9c | ||
|
|
6dddfbdcb6 | ||
|
|
fc2d7b4b9b | ||
|
|
c877898ae7 | ||
|
|
88e27f47f5 | ||
|
|
9681def3e0 | ||
|
|
0057007b4c | ||
|
|
8aad93e572 | ||
|
|
fa0ec3ece2 | ||
|
|
4f9f2f8370 | ||
|
|
0fb93be2cd | ||
|
|
26dcd6072e | ||
|
|
42f228af0e | ||
|
|
8be70baff2 | ||
|
|
7e5c314000 | ||
|
|
9669112d69 | ||
|
|
9a71763f79 | ||
|
|
1a96eac8c9 | ||
|
|
a2acdef539 | ||
|
|
58ddbbcc4e | ||
|
|
5ff33ff19f | ||
|
|
682f599261 | ||
|
|
7c26e33a8c | ||
|
|
2595ee8295 | ||
|
|
531685c0e4 | ||
|
|
476fdcd0aa | ||
|
|
b6762b215e | ||
|
|
4ac7f59378 | ||
|
|
3cdf49bb56 | ||
|
|
28615403b8 | ||
|
|
bb10bd819a | ||
|
|
3882217e6a | ||
|
|
8cd2896115 | ||
|
|
e570519ec8 | ||
|
|
2f74b74110 | ||
|
|
fddb5499f0 | ||
|
|
fed98d51be | ||
|
|
8e3ee3a0fb | ||
|
|
e57faf8f26 | ||
|
|
725c1c39a5 | ||
|
|
61fa6b0ed6 | ||
|
|
99843061ab | ||
|
|
3a23763800 | ||
|
|
3f4d49a228 | ||
|
|
88cfadbc17 | ||
|
|
6c1da41cb3 | ||
|
|
0e6a411012 | ||
|
|
55e9e35c1f | ||
|
|
84603356ef | ||
|
|
2f6b4dfc09 | ||
|
|
b19af253ae | ||
|
|
b6c6c186a8 | ||
|
|
ae6e708f51 | ||
|
|
442f4ed752 | ||
|
|
8617cf5fd0 | ||
|
|
2e00230f79 | ||
|
|
44dcce6c45 | ||
|
|
8d43192a1c | ||
|
|
59e2bb473b | ||
|
|
b4d2506e81 | ||
|
|
7290c8db30 | ||
|
|
2af50ecf02 | ||
|
|
f88a24c1b5 | ||
|
|
a4b4ff13d2 | ||
|
|
2bf4f9df28 | ||
|
|
19ba38e8ce | ||
|
|
373a52ebf1 | ||
|
|
fe82478dc4 | ||
|
|
bed281c8c3 | ||
|
|
01484b4e03 | ||
|
|
c86512254d | ||
|
|
7df9b9b8a4 | ||
|
|
36e1ec1f8e | ||
|
|
5f91395766 | ||
|
|
dda6cb2aa4 | ||
|
|
578aade39b | ||
|
|
b09890a527 | ||
|
|
baa574a93e | ||
|
|
a32bd840b9 | ||
|
|
0bdc232938 | ||
|
|
8333563d98 | ||
|
|
f543cfce73 | ||
|
|
0f5e694682 | ||
|
|
615638d705 | ||
|
|
9b46bbd7bb | ||
|
|
1b58bda5a7 | ||
|
|
c1f15a4079 | ||
|
|
18ea03a4d6 | ||
|
|
3d911d1062 | ||
|
|
99fd116b6d | ||
|
|
935ab94159 | ||
|
|
18dad062f6 | ||
|
|
1e8fc1825c | ||
|
|
f64795706d | ||
|
|
67a9914a8b | ||
|
|
b01d15a67d | ||
|
|
e4c9d15e60 | ||
|
|
ccae89ad01 | ||
|
|
3c00ce6644 | ||
|
|
e9d1b763fd | ||
|
|
cf88fadfdc | ||
|
|
4977ae7003 | ||
|
|
f0f6970a08 | ||
|
|
fcb4acb11d | ||
|
|
0ecae58be2 | ||
|
|
98b5e300c0 | ||
|
|
2ea062ae2b | ||
|
|
c6e5eb9dc9 | ||
|
|
46a19b0fb1 | ||
|
|
40cf8cc8de | ||
|
|
ddc70d6fbc | ||
|
|
625b8895b0 | ||
|
|
5fd8d8fcd1 | ||
|
|
28e012feb6 | ||
|
|
958c637c24 | ||
|
|
34381e6e93 | ||
|
|
8843b3e2b0 | ||
|
|
3f91653d23 | ||
|
|
5f442835e5 | ||
|
|
aa8fd89227 | ||
|
|
153ebc204a | ||
|
|
589c16550c | ||
|
|
49063456b6 | ||
|
|
87eb804cd1 | ||
|
|
23d5118b52 | ||
|
|
94eb342435 | ||
|
|
ac326b8e34 | ||
|
|
164a38ad20 | ||
|
|
3300e0fc80 | ||
|
|
3a4569c33f | ||
|
|
05a0620663 | ||
|
|
af83c06b43 | ||
|
|
e6317c3282 | ||
|
|
3da7432273 | ||
|
|
61c1dd5878 | ||
|
|
c095e7af39 | ||
|
|
ddce40bb52 | ||
|
|
746f9a3488 | ||
|
|
fb9612bb65 | ||
|
|
a611580f48 | ||
|
|
e4a37b1c10 | ||
|
|
07c671ede4 | ||
|
|
ba4f847e57 | ||
|
|
7c8d73810d | ||
|
|
9829e8581c | ||
|
|
a12aa5609d | ||
|
|
abf834c5a8 | ||
|
|
7ae557d143 | ||
|
|
5f4b0c8da3 | ||
|
|
6ef197dae3 | ||
|
|
fafa7eecd9 | ||
|
|
5ede934be5 | ||
|
|
8d036ea9ba | ||
|
|
bc6acc846a | ||
|
|
afa050261f | ||
|
|
1715d064fb | ||
|
|
cf2f2af90f | ||
|
|
354321c956 | ||
|
|
af2572a493 | ||
|
|
1a5dadbf35 | ||
|
|
ced7a60f14 | ||
|
|
043c4a8b2b | ||
|
|
f9b4bf06ab | ||
|
|
8048182f46 | ||
|
|
cb4d1d3eba | ||
|
|
81e623e35f | ||
|
|
3209f96e7f | ||
|
|
c65d662630 | ||
|
|
1b99f8afa1 | ||
|
|
c6f0a1317c | ||
|
|
fad5f8e23d | ||
|
|
ec6e0f5e46 | ||
|
|
f1caef2c0d | ||
|
|
55bf9424f9 | ||
|
|
aa100c97ed | ||
|
|
9fa5eafe95 | ||
|
|
4d9e948e30 | ||
|
|
e5cdf28e34 | ||
|
|
1d82b36105 | ||
|
|
b910cd1e6d | ||
|
|
536861ba99 | ||
|
|
ef030e3dc6 | ||
|
|
3da46c3d6f | ||
|
|
82f5552836 | ||
|
|
4d25681c51 | ||
|
|
98a1631498 | ||
|
|
8dce8cbf8d | ||
|
|
0b3049138b | ||
|
|
d7136583b8 | ||
|
|
db1da9172c | ||
|
|
19b13e5d6e | ||
|
|
c92ffac645 | ||
|
|
216f0b6910 | ||
|
|
c31bf96986 | ||
|
|
7f8ef994a7 | ||
|
|
72ba1c48c2 | ||
|
|
c721da1562 | ||
|
|
3606a2d31b | ||
|
|
764905b041 | ||
|
|
556c6ec522 | ||
|
|
2a78145170 | ||
|
|
04395b70a6 | ||
|
|
d15fee7c7a | ||
|
|
9437b8fb80 | ||
|
|
b611663379 | ||
|
|
03b7ed980a | ||
|
|
bf74d50bb0 | ||
|
|
003480bcb5 | ||
|
|
9dbd18d0d6 | ||
|
|
b3ba7f03e4 | ||
|
|
0a8bb52241 | ||
|
|
5eeafd7ead | ||
|
|
df146382cb | ||
|
|
af8a7c3ff3 | ||
|
|
ed6d2af23c | ||
|
|
4f0ad71fff | ||
|
|
6871380288 | ||
|
|
285456656c | ||
|
|
179d8f1656 | ||
|
|
94241c08ce | ||
|
|
abd4e872bf | ||
|
|
81521151f4 | ||
|
|
5a819c1d9c | ||
|
|
f2f3208fda | ||
|
|
7fe888cdda | ||
|
|
db3f1bf2cf | ||
|
|
5f64257dca | ||
|
|
2fbdccb883 | ||
|
|
ecc10afac3 | ||
|
|
4fc54aa9c5 | ||
|
|
2bd7f0c45c | ||
|
|
900423339e | ||
|
|
d907afa6bb | ||
|
|
9b116d82b7 | ||
|
|
1d1bf50ee3 | ||
|
|
5002004697 | ||
|
|
f311a3b9c6 | ||
|
|
fe91f4b875 | ||
|
|
2d9a2b811c | ||
|
|
514b65ed24 | ||
|
|
0c7f3088a8 | ||
|
|
12cab02300 | ||
|
|
04a908261a | ||
|
|
83898742f1 | ||
|
|
1795918719 | ||
|
|
6894217078 | ||
|
|
3ffbed27b5 | ||
|
|
b372e3d260 | ||
|
|
707b385b8a | ||
|
|
6cb140a491 | ||
|
|
c43ee8c25a | ||
|
|
0a7ec11510 | ||
|
|
f7aaba71d1 | ||
|
|
418a2e2ad2 | ||
|
|
a1023e8224 | ||
|
|
a32ed63d47 | ||
|
|
934fb46ffc | ||
|
|
b27136d03b | ||
|
|
064ac36dc7 | ||
|
|
5e2cf86f4b | ||
|
|
425d2ada65 | ||
|
|
60a3d67c0b | ||
|
|
a03c5d8c91 | ||
|
|
bcc90da49b | ||
|
|
b473a3803c | ||
|
|
9614f186b4 | ||
|
|
0390f2d956 | ||
|
|
ba5ead998d | ||
|
|
409a0f1049 | ||
|
|
bd2f27bb64 | ||
|
|
55d8d131cf | ||
|
|
5a6e3247e8 | ||
|
|
2a522775eb | ||
|
|
d677c83795 | ||
|
|
7cffd9f377 | ||
|
|
0a406cb9a4 | ||
|
|
f172a51016 | ||
|
|
00d53659b1 | ||
|
|
b0797d92f5 | ||
|
|
48484ed7af | ||
|
|
6a4aadde8a | ||
|
|
79e3aba26e | ||
|
|
bf61359bc7 | ||
|
|
a79d152993 | ||
|
|
ce7c3a0e2f | ||
|
|
e6b0580209 | ||
|
|
1f3c639100 | ||
|
|
e6efc4f49b | ||
|
|
dbbd321029 | ||
|
|
439dbc54ae | ||
|
|
547e59f52f | ||
|
|
a0d385de73 | ||
|
|
bec136009f | ||
|
|
98b7fa7734 | ||
|
|
d7bb476858 | ||
|
|
18c71c383f | ||
|
|
de85a933c4 | ||
|
|
16247372f1 | ||
|
|
c6c8b6418c | ||
|
|
10c4e7380f | ||
|
|
4933dce472 | ||
|
|
80c2822716 | ||
|
|
f48ff9a67e | ||
|
|
4bd2385359 | ||
|
|
be71492773 | ||
|
|
bcb70edf2b | ||
|
|
07071032bc | ||
|
|
f476e119a2 | ||
|
|
0fed232428 | ||
|
|
0273510485 | ||
|
|
a62dee37f4 | ||
|
|
8d6af3e1b7 | ||
|
|
ee0f2da641 | ||
|
|
44499d9652 | ||
|
|
365a5dcd4b | ||
|
|
7f5b456018 | ||
|
|
c7217486bd | ||
|
|
050141d31e | ||
|
|
b697f37199 | ||
|
|
ceb58caba1 | ||
|
|
2724b500ee | ||
|
|
1b84e7beb8 | ||
|
|
383900d8b5 | ||
|
|
ba806d86b5 | ||
|
|
4d7c7350a0 | ||
|
|
adc6500aed | ||
|
|
eb9fce73eb | ||
|
|
ec3ed165e4 | ||
|
|
d41d20cc81 | ||
|
|
5f965e7146 | ||
|
|
f382bb4578 | ||
|
|
ba37f87f80 | ||
|
|
eaa9c23f55 | ||
|
|
ac3730dbd9 | ||
|
|
581b8eb626 | ||
|
|
cd4ec2711e | ||
|
|
e9e3dbad0d | ||
|
|
295b361431 | ||
|
|
ad60df9d51 | ||
|
|
6ae635c21d | ||
|
|
61807a2f3d | ||
|
|
16ba5a8980 | ||
|
|
99b817549e | ||
|
|
60a8c335c8 | ||
|
|
1783768c32 | ||
|
|
b3630eaa06 | ||
|
|
d691016911 | ||
|
|
6506aead8c | ||
|
|
ddfc83d026 | ||
|
|
fe42c1e0e3 | ||
|
|
928a211a63 | ||
|
|
8bff69a4ca | ||
|
|
5fced7d501 | ||
|
|
7cdf622ad6 | ||
|
|
afd8ceac7b | ||
|
|
5d63db3662 | ||
|
|
9f6cd7dbf1 | ||
|
|
0725aa3ada | ||
|
|
3eb88d4076 | ||
|
|
1c61f150b1 | ||
|
|
4c74b287d8 | ||
|
|
264d762f74 | ||
|
|
3c02461c6b | ||
|
|
a30f2be51f | ||
|
|
0690a9fb65 | ||
|
|
4fa103765e | ||
|
|
b0d6ce40a9 | ||
|
|
c57a3d935b | ||
|
|
82b5943601 | ||
|
|
41b76967f5 | ||
|
|
e4c2ee44c1 | ||
|
|
c05f022995 | ||
|
|
7723c3a201 | ||
|
|
8e338818ca | ||
|
|
ea51aa420c | ||
|
|
bc410678b4 | ||
|
|
a42c15c969 | ||
|
|
a3102a0278 | ||
|
|
f4f142dc48 | ||
|
|
e03a70c9a3 | ||
|
|
d47b8797d4 | ||
|
|
56ba0aad02 | ||
|
|
825bc6d2de | ||
|
|
18fe63a5ad | ||
|
|
d6e0e7b903 | ||
|
|
8759e1af5e | ||
|
|
b78f3fb08b | ||
|
|
5fa0b2e1c8 | ||
|
|
6a88d0559a | ||
|
|
314fdc2bb0 | ||
|
|
acd7154d41 | ||
|
|
63a1bea60d | ||
|
|
077c60bd34 | ||
|
|
6485537207 | ||
|
|
286f079679 | ||
|
|
075818d551 | ||
|
|
b0f42d613b | ||
|
|
8e5b87f142 | ||
|
|
92483ee5e1 | ||
|
|
a80b82da7a | ||
|
|
41d2aaa16d | ||
|
|
9de2caba94 | ||
|
|
a45fb3ad08 | ||
|
|
666f954529 | ||
|
|
7d2e02fe64 | ||
|
|
9912ed7af7 | ||
|
|
441d898ae7 | ||
|
|
553a7ff86a | ||
|
|
349d070273 | ||
|
|
b6dcb9c177 | ||
|
|
8aff3a6568 | ||
|
|
9e885b8bc3 | ||
|
|
d6b924a5f4 | ||
|
|
97691a0dd9 | ||
|
|
3781af6305 | ||
|
|
cb5d5a603d | ||
|
|
81713bfebe | ||
|
|
b9aeb8faa0 | ||
|
|
a4e75cdccc | ||
|
|
f74c94ef50 | ||
|
|
bfb51fdcc6 | ||
|
|
25aaf4146f | ||
|
|
8ee10f2f8c | ||
|
|
bf96821c2c | ||
|
|
9787806427 | ||
|
|
9043aab432 | ||
|
|
58989a81dc | ||
|
|
82acd623d8 | ||
|
|
e4fe8cc492 | ||
|
|
0f8a6b9299 | ||
|
|
4f2c87bbd4 | ||
|
|
66b8e6b263 | ||
|
|
cabff83222 | ||
|
|
d94b527780 | ||
|
|
f0e8a05f08 | ||
|
|
278b4127e1 | ||
|
|
87b3d7519c | ||
|
|
ed4e75c9ad | ||
|
|
b0cc5a6e0b | ||
|
|
6b4139a29e | ||
|
|
925242262a | ||
|
|
74fe77009c | ||
|
|
51f7905dad | ||
|
|
0b1d3e63f6 | ||
|
|
549fd4fd5a | ||
|
|
846ee21c48 | ||
|
|
6654836440 | ||
|
|
a25a13673e | ||
|
|
dd7db3814a | ||
|
|
fdb3b9c3ac | ||
|
|
30eed1ffb1 | ||
|
|
7884cffdc6 | ||
|
|
c1a8210fa7 | ||
|
|
9f3b482f57 | ||
|
|
9487027852 | ||
|
|
9065900608 | ||
|
|
2da319f896 | ||
|
|
449a98d80c | ||
|
|
2343120fcf | ||
|
|
2d4fa523cd | ||
|
|
c8cc3adbc5 | ||
|
|
169e0ad3d2 | ||
|
|
e20a0a5d27 | ||
|
|
6dda29bb14 | ||
|
|
dd662e9fe9 | ||
|
|
9b775c5bf6 | ||
|
|
2047673862 | ||
|
|
5b545aebeb | ||
|
|
1e75816125 | ||
|
|
455fd807a4 | ||
|
|
8b75ac0f32 | ||
|
|
f1a5ae70e2 | ||
|
|
46bd532eeb | ||
|
|
6f99b215cd | ||
|
|
3482540fc8 | ||
|
|
fb999c7bd4 |
@@ -69,5 +69,9 @@ ipfs-service-provider.sh
|
|||||||
run-dev.sh
|
run-dev.sh
|
||||||
wallet.json
|
wallet.json
|
||||||
data/
|
data/
|
||||||
|
run-dev.sh
|
||||||
|
*.zip
|
||||||
|
|
||||||
!README.md
|
!README.md
|
||||||
|
|
||||||
|
wallet.json
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Permissionless Software Foundation Community Contributing Guide 1.0
|
||||||
|
|
||||||
|
This document describes a very simple process suitable for most projects under
|
||||||
|
the PSF umbrella. It is based on [this Medium article](https://medium.com/the-node-js-collection/healthy-open-source-967fa8be7951) and the [Node.js Community Contribution Guide](https://github.com/nodejs/TSC/blob/master/BasePolicies/CONTRIBUTING.md).
|
||||||
|
Projects are encouraged to adopt this whether they
|
||||||
|
are hosted under the PSF or not.
|
||||||
|
|
||||||
|
The goal of this document is to create a contribution process that:
|
||||||
|
|
||||||
|
* Encourages new contributions.
|
||||||
|
* Encourages contributors to remain involved.
|
||||||
|
* Avoids unnecessary processes and bureaucracy whenever possible.
|
||||||
|
* Creates a transparent decision making process which makes it clear how
|
||||||
|
contributors can be involved in decision making.
|
||||||
|
|
||||||
|
This document is based on much prior art in the Node.js community, io.js,
|
||||||
|
and the Node.js project.
|
||||||
|
|
||||||
|
Additional guidance can be found at the [Permissionless Software Foundation Telegram Channel](https://t.me/permissionless_software).
|
||||||
|
|
||||||
|
## Vocabulary
|
||||||
|
|
||||||
|
* A **Contributor** is any individual creating or commenting on an issue or pull request.
|
||||||
|
* A **Committer** is a subset of contributors who have been given write access to the repository.
|
||||||
|
* A **TC (Technical Committee)** is a group of committers representing the required technical
|
||||||
|
expertise to resolve rare disputes.
|
||||||
|
|
||||||
|
# Logging Issues
|
||||||
|
|
||||||
|
Log an issue for any question or problem you might have. When in doubt, log an issue,
|
||||||
|
any additional policies about what to include will be provided in the responses. The only
|
||||||
|
exception is security disclosures which should be sent privately.
|
||||||
|
|
||||||
|
Committers may direct you to another repository, ask for additional clarifications, and
|
||||||
|
add appropriate metadata before the issue is addressed.
|
||||||
|
|
||||||
|
Please be courteous, respectful, and every participant is expected to follow the
|
||||||
|
project's Code of Conduct.
|
||||||
|
|
||||||
|
# Contributions
|
||||||
|
|
||||||
|
Any change to resources in this repository must be through pull requests. This applies to all changes
|
||||||
|
to documentation, code, binary files, etc. Even long term committers and TC members must use
|
||||||
|
pull requests.
|
||||||
|
|
||||||
|
No pull request can be merged without being reviewed.
|
||||||
|
|
||||||
|
For non-trivial contributions, pull requests should sit for at least 36 hours to ensure that
|
||||||
|
contributors in other timezones have time to review. Consideration should also be given to
|
||||||
|
weekends and other holiday periods to ensure active committers all have reasonable time to
|
||||||
|
become involved in the discussion and review process if they wish.
|
||||||
|
|
||||||
|
The default for each contribution is that it is accepted once no committer has an objection.
|
||||||
|
During review committers may also request that a specific contributor who is most versed in a
|
||||||
|
particular area gives a "LGTM" before the PR can be merged. There is no additional "sign off"
|
||||||
|
process for contributions to land. Once all issues brought by committers are addressed it can
|
||||||
|
be landed by any committer.
|
||||||
|
|
||||||
|
In the case of an objection being raised in a pull request by another committer, all involved
|
||||||
|
committers should seek to arrive at a consensus by way of addressing concerns being expressed
|
||||||
|
by discussion, compromise on the proposed change, or withdrawal of the proposed change.
|
||||||
|
|
||||||
|
If a contribution is controversial and committers cannot agree about how to get it to land
|
||||||
|
or if it should land then it should be escalated to the TC. TC members should regularly
|
||||||
|
discuss pending contributions in order to find a resolution. It is expected that only a
|
||||||
|
small minority of issues be brought to the TC for resolution and that discussion and
|
||||||
|
compromise among committers be the default resolution mechanism.
|
||||||
|
|
||||||
|
# Becoming a Committer
|
||||||
|
|
||||||
|
All contributors who land a non-trivial contribution should be on-boarded in a timely manner,
|
||||||
|
and added as a committer, and be given write access to the repository.
|
||||||
|
|
||||||
|
Committers are expected to follow this policy and continue to send pull requests, go through
|
||||||
|
proper review, and have other committers merge their pull requests.
|
||||||
|
|
||||||
|
# TC Process
|
||||||
|
|
||||||
|
The TC uses a "consensus seeking" process for issues that are escalated to the TC.
|
||||||
|
The group tries to find a resolution that has no open objections among TC members.
|
||||||
|
If a consensus cannot be reached that has no objections then a majority wins vote
|
||||||
|
is called. It is also expected that the majority of decisions made by the TC are via
|
||||||
|
a consensus seeking process and that voting is only used as a last-resort.
|
||||||
|
|
||||||
|
Resolution may involve returning the issue to committers with suggestions on how to
|
||||||
|
move forward towards a consensus. It is not expected that a meeting of the TC
|
||||||
|
will resolve all issues on its agenda during that meeting and may prefer to continue
|
||||||
|
the discussion happening among the committers.
|
||||||
|
|
||||||
|
Members can be added to the TC at any time. Any committer can nominate another committer
|
||||||
|
to the TC and the TC uses its standard consensus seeking process to evaluate whether or
|
||||||
|
not to add this new member. Members who do not participate consistently at the level of
|
||||||
|
a majority of the other members are expected to resign.
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
Copyright (c) 2022 Permissionless Software Foundation
|
Copyright (c) 2021-2024 Permissionless Software Foundation
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Pedigree
|
||||||
|
|
||||||
|
This repository was forked from [koa-api-boilerplate](https://github.com/christroutner/koa-api-boilerplate). Code changes from that upstream repository is frequently pulled in and merged to this repository.
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
# bch-dex
|
# bch-dex
|
||||||
|
|
||||||
[](http://standardjs.com) [](https://github.com/semantic-release/semantic-release) [](https://gitter.im/Permissionless-Software-Foundation/psf-dex-dev?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](http://standardjs.com) [](https://github.com/semantic-release/semantic-release)
|
||||||
|
|
||||||
`bch-dex` is a decentralized exchange for permissionless trading of [SLP tokens](https://github.com/simpleledger/slp-specifications/blob/master/slp-token-type-1.md) and [NFTs](https://github.com/simpleledger/slp-specifications/blob/master/slp-nft-1.md). Trading fees are incredibly small, compared to other DEX platforms. The software is based entirely on open protocols and open source software. It's not a '.com' website that you visit, but software you run on your own computer, in the confort of your own home. So long as you have an internet connection and the technical knowledge to run this software, no one can stop you from trading tokens. `bch-dex` software is maintained by the [Permissionless Software Foundation](https://psfoundation.cash/).
|
`bch-dex` is a decentralized exchange *protocol* for permissionless trading of [SLP tokens](https://github.com/simpleledger/slp-specifications/blob/master/slp-token-type-1.md) and [NFTs](https://github.com/simpleledger/slp-specifications/blob/master/slp-nft-1.md). Trading fees are incredibly small, compared to other DEX platforms. The software is based entirely on open protocols and open source software. `bch-dex` software is maintained by the [Permissionless Software Foundation](https://psfoundation.info).
|
||||||
|
|
||||||
- [High-level Overview of bch-dex](https://youtu.be/LVX8CLi4sHw) (Video)
|
- [High-level Overview of bch-dex](https://youtu.be/LVX8CLi4sHw) (Video)
|
||||||
|
|
||||||
This is a prototype web service that monitors the [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) for trading signals, to trade BCH and SLP tokens. It's inspired by the [SWaP Protocol](https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md).
|
This is a prototype web service that monitors one or more [Nostr relays](https://nostr.com) for trading signals, to trade BCH and SLP tokens. It's inspired by the [SWaP Protocol](https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md).
|
||||||
|
|
||||||
This repository contains the back end code. The user interface is contained in the [bch-dex-ui](https://github.com/Permissionless-Software-Foundation/bch-dex-ui) repository.
|
This repository contains the back end code. The user interface is contained in the [bch-dex-ui-v3](https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3) repository.
|
||||||
|
|
||||||
**Warning**: This repository is under active development. Things will be constantly changing and breaking.
|
**Warning**: This repository is under active development. Things will be constantly changing and breaking.
|
||||||
|
|
||||||
@@ -19,57 +19,43 @@ This repository contains the back end code. The user interface is contained in t
|
|||||||
|
|
||||||
## Participate
|
## Participate
|
||||||
This is an open source project, and we encourage other JavaScript developers to participate in its creation and maintenance. We have two chat rooms for the community:
|
This is an open source project, and we encourage other JavaScript developers to participate in its creation and maintenance. We have two chat rooms for the community:
|
||||||
- [Telegram Channel](https://t.me/psf_dex_dev)
|
- [Telegram Channel](https://t.me/bch_js_toolkit)
|
||||||
- [Gitter Channel](https://gitter.im/Permissionless-Software-Foundation/psf-dex-dev)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Running the DEX requires composition of these different software packages:
|
Running the DEX requires composition of these different software packages:
|
||||||
- bch-dex - This repository is the back end software that tracks trade data on the network, generates [Offers and Counter Offers](https://github.com/Permissionless-Software-Foundation/bch-dex/tree/ct-unstable/dev-docs#definitions), and finalizes trades by accepting Counter Offers.
|
- bch-dex - This repository is the back end software that tracks trade data on the network, generates [Offers and Counter Offers](https://github.com/Permissionless-Software-Foundation/bch-dex/tree/ct-unstable/dev-docs#definitions), and finalizes trades by accepting Counter Offers.
|
||||||
- [bch-dex-ui](https://github.com/Permissionless-Software-Foundation/bch-dex-ui) is a [Gatsby](https://www.gatsbyjs.com/) web app and user interface (UI) for bch-dex.
|
- [bch-dex-ui-v3](https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3) web app and user interface (UI) for bch-dex focused on Sellers of tokens.
|
||||||
- [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) is a censorship-resistant database used to communicate trade data between peers running bch-dex.
|
- [MongoDB](https://www.mongodb.com/) is a database used by bch-dex to store and manage local data, user accounts, etc.
|
||||||
- [IPFS](https://ipfs.io/) is a censorship-resistant network for communicating data over the internet.
|
|
||||||
- [MongoDB](https://www.mongodb.com/) is a database used by both P2WDB and bch-dex to store and manage local data.
|
|
||||||
|
|
||||||
The above software is orchestrated using [Docker](https://www.docker.com/) and Docker Compose. The target operating system is Ubuntu 20+, and the target hardware is amd64 (normal desktop PCs) and the arm64 (Raspberry Pi 4). Trying to operate this software on other operating systems or hardware is possible, but not supported.
|
The above software is orchestrated using [Docker](https://www.docker.com/) and Docker Compose. The target operating system is Ubuntu 20+, and the target hardware is amd64 (normal desktop PCs). Trying to operate this software on other operating systems or hardware is possible, but not supported.
|
||||||
|
|
||||||
The Raspberry Pi 4 is the primary hardware target, because this software functions best when it is always running. The Pi 4 makes a great appliance for monitoring trade signals and finalizing trades.
|
Setup instructions:
|
||||||
|
|
||||||
Instructions for setting up Node.js, Docker, and Docker Compose can be found in [this Gist](https://gist.github.com/christroutner/a39f656850dc022b60f25c9663dd1cdd). Walk-through videos can also be found on the [PSF Videos page](https://psfoundation.cash/video/).
|
- Follow the direction in [this Gist](https://gist.github.com/christroutner/a39f656850dc022b60f25c9663dd1cdd) to install Node.js, Docker, and Docker Compose.
|
||||||
|
- Clone the repository with `git clone https://github.com/Permissionless-Software-Foundation/bch-dex` and enter it with `cd bch-dex`.
|
||||||
Here are the steps involved in setting up bch-dex:
|
- Install dependencies by running `npm install`
|
||||||
|
- Create a wallet:
|
||||||
1. Follow the direction in [this Gist](https://gist.github.com/christroutner/a39f656850dc022b60f25c9663dd1cdd) to install Node.js, Docker, and Docker Compose.
|
|
||||||
1. Clone the repository with `git clone https://github.com/Permissionless-Software-Foundation/bch-dex` and enter it with `cd bch-dex`.
|
|
||||||
1. Install dependencies by running `npm install`
|
|
||||||
1. Create a wallet:
|
|
||||||
- `cd production/scripts`
|
- `cd production/scripts`
|
||||||
- `node create-wallet.js`
|
- `node create-wallet.js`
|
||||||
1. Change directory to the `production/docker` or `production/rpi-docker` folder depending on your hardware target.
|
- Change directory to the `production/docker` folder.
|
||||||
1. Pull the Docker images down from Docker Hub: `docker-compose pull`
|
- Pull the Docker images down from Docker Hub: `docker-compose pull`
|
||||||
1. Start the Docker containers with `docker-compose up -d`
|
- Build the back end: `docker-compose build --no-cache bch-dex`
|
||||||
1. Wait for the P2WDB to sync and populate bch-dex with trade data. You can monitor it with `docker logs --tail 20 -f p2wdb`.
|
- Build the front end: `docker-compose build --no-cache dex-ui`
|
||||||
1. Open a web browser and navigate the `http://localhost:4500`. You'll be able to see new Offers as they come in and are detected by bch-dex.
|
- Start the Docker containers with `docker-compose up -d`
|
||||||
1. To take the other side of the trade, click the `Take` button in the UI.
|
- It will take about 10 minutes for the bch-dex to sync with the network and display tokens for sale.
|
||||||
1. You can add the 12-word mnemonic from the `wallet.json` file to the the web wallet, which will mirror your wallet in the UI, and allow you to perform basic wallet functions (send and receive BCH and tokens).
|
- Open a web browser and navigate to `http://localhost:4500`.
|
||||||
|
- You will be presented with a login screen. Click the sign-up tab, enter an email and password to create an account. This information is stored locally on bch-dex, it is not set to a third party.
|
||||||
|
- Once logged in, you can retrieve your BCH and SLP addresses, to fund your wallet.
|
||||||
|
|
||||||
|
|
||||||
## Blockchain Infrastructure
|
## Blockchain Infrastructure
|
||||||
|
|
||||||
The P2WDB and bch-dex containers requires a 'Global Back End' (as described in [CashStack.info](https://cashstack.info)) in order to connect to the blockchain. By default, the Docker containers connect to [free-bch.fullstack.cash](https://free-bch.fullstack.cash/). Several community-provided Global Back End servers are provided and can be [viewed here](https://gist.github.com/christroutner/63c5513782181f8b8ea3eb89f7cadeb6). The back end can be changed by setting the `CONSUMER_URL` environment variable in the `docker-compose.yml` file.
|
bch-dex requires a [Cash Stack](https://cashstack.info) back end in order to connect to the blockchain. By default, the Docker containers connect to [free-bch.fullstack.cash](https://free-bch.fullstack.cash/). Several community-provided servers are provided and can be [viewed here](https://consumers.psfoundation.info/consumers.json). The back end can be changed by setting the `CONSUMER_URL` environment variable in the `docker-compose.yml` file.
|
||||||
|
|
||||||
### Applying Software Updates
|
## Environment Variables
|
||||||
As this is an active project, software updates will happen frequently. To apply a software update, perform these steps.
|
|
||||||
|
|
||||||
|
- `DISABLE_NEW_ACCOUNTS` - If set, only the admin user can create new accounts. The sign-up tab becomes non-functional. Useful for controlling access to your instance of bch-dex.
|
||||||
1. Enter the `docker` or `rpi-docker` folder, depending on your hardware target.
|
- `ADMIN_PASSWORD` - Sets the password for the admin user. This can be used with the [createUser.js script](/util/users/production/createUser.js) to add new users to the system, if the `DISABLE_NEW_ACCOUNTS` env var is set.
|
||||||
1. Bring down the Docker containers with `docker-compose down`.
|
|
||||||
1. Pull in the software updates with `git pull`
|
|
||||||
1. Update dependencies with `npm install`
|
|
||||||
1. Pull new Docker images with `docker-compose pull`
|
|
||||||
1. Clean up disk space by deleting old Docker images with `./cleanup-images.sh`
|
|
||||||
1. Start the Docker containers with `docker-compose up -d`
|
|
||||||
|
|
||||||
|
|
||||||
Sometimes it may be necessary to delete the databases before applying a software update. This can be done by stopping the Docker containers and deleting the `production/data` directory. When the Docker containers are restarted, they will recreate that directory. The P2WDB will re-sync and bch-dex will be populated with fresh trade data.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+22
-14
@@ -25,6 +25,7 @@ import config from '../config/index.js' // this first.
|
|||||||
|
|
||||||
import AdminLib from '../src/adapters/admin.js'
|
import AdminLib from '../src/adapters/admin.js'
|
||||||
import errorMiddleware from '../src/controllers/rest-api/middleware/error.js'
|
import errorMiddleware from '../src/controllers/rest-api/middleware/error.js'
|
||||||
|
import { usageMiddleware } from '../src/use-cases/usage-use-cases.js'
|
||||||
// import wlogger from '../src/adapters/wlogger.js'
|
// import wlogger from '../src/adapters/wlogger.js'
|
||||||
import Controllers from '../src/controllers/index.js'
|
import Controllers from '../src/controllers/index.js'
|
||||||
import { applyPassportMods } from '../config/passport.js'
|
import { applyPassportMods } from '../config/passport.js'
|
||||||
@@ -45,16 +46,18 @@ class Server {
|
|||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
app.keys = [this.config.session]
|
app.keys = [this.config.session]
|
||||||
|
|
||||||
// Connect to the Mongo Database.
|
if (!this.config.noMongo) {
|
||||||
this.mongoose.Promise = global.Promise
|
// Connect to the Mongo Database.
|
||||||
this.mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
this.mongoose.Promise = global.Promise
|
||||||
console.log(
|
this.mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||||
`Connecting to MongoDB with this connection string: ${this.config.database}`
|
console.log(
|
||||||
)
|
`Connecting to MongoDB with this connection string: ${this.config.database}`
|
||||||
await this.mongoose.connect(this.config.database, {
|
)
|
||||||
useUnifiedTopology: true,
|
await this.mongoose.connect(this.config.database, {
|
||||||
useNewUrlParser: true
|
useUnifiedTopology: true,
|
||||||
})
|
useNewUrlParser: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Starting environment: ${this.config.env}`)
|
console.log(`Starting environment: ${this.config.env}`)
|
||||||
console.log(`Debug level: ${this.config.debugLevel}\n`)
|
console.log(`Debug level: ${this.config.debugLevel}\n`)
|
||||||
@@ -68,6 +71,7 @@ class Server {
|
|||||||
app.use(bodyParser())
|
app.use(bodyParser())
|
||||||
app.use(session())
|
app.use(session())
|
||||||
app.use(errorMiddleware())
|
app.use(errorMiddleware())
|
||||||
|
app.use(usageMiddleware())
|
||||||
|
|
||||||
// Used to generate the docs.
|
// Used to generate the docs.
|
||||||
app.use(mount('/', serve(`${process.cwd()}/docs`)))
|
app.use(mount('/', serve(`${process.cwd()}/docs`)))
|
||||||
@@ -124,9 +128,11 @@ class Server {
|
|||||||
this.server = await app.listen(config.port)
|
this.server = await app.listen(config.port)
|
||||||
console.log(`Server started on ${config.port}`)
|
console.log(`Server started on ${config.port}`)
|
||||||
|
|
||||||
// Create the system admin user.
|
if (!this.config.noMongo) {
|
||||||
const success = await this.adminLib.createSystemUser()
|
// Create the system admin user.
|
||||||
if (success) console.log('System admin user created.')
|
const success = await this.adminLib.createSystemUser()
|
||||||
|
if (success) console.log('System admin user created.')
|
||||||
|
}
|
||||||
|
|
||||||
// Attach the other IPFS controllers.
|
// Attach the other IPFS controllers.
|
||||||
// Skip if this is a test environment.
|
// Skip if this is a test environment.
|
||||||
@@ -138,7 +144,9 @@ class Server {
|
|||||||
console.log('\nConfiguration:')
|
console.log('\nConfiguration:')
|
||||||
console.log(`Circuit Relay: ${this.config.isCircuitRelay}`)
|
console.log(`Circuit Relay: ${this.config.isCircuitRelay}`)
|
||||||
console.log(`IPFS TCP port: ${this.config.ipfsTcpPort}`)
|
console.log(`IPFS TCP port: ${this.config.ipfsTcpPort}`)
|
||||||
console.log(`IPFS WS port: ${this.config.ipfsWsPort}\n`)
|
console.log(`IPFS WS port: ${this.config.ipfsWsPort}`)
|
||||||
|
console.log(`IPFS WebRTC port: ${this.config.ipfsWebRtcPort}`)
|
||||||
|
console.log(`Connection preference: ${this.config.connectPref}\n`)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Vendored
+91
-12
@@ -1,6 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
This file is used to store unsecure, application-specific data common to all
|
This file is used to store unsecure, application-specific data common to all
|
||||||
environments.
|
environments.
|
||||||
|
|
||||||
|
Additional Environent Variables:
|
||||||
|
- CONNECT_PREF: should have a value of 'cr' (default), or 'direct'. This is
|
||||||
|
used by helia-coord to select a connection preference between peers. Servers
|
||||||
|
with an ip4 or ip6 address should use 'direct'.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint no-unneeded-ternary:0 */
|
/* eslint no-unneeded-ternary:0 */
|
||||||
@@ -43,27 +48,75 @@ export default {
|
|||||||
consumerUrl: process.env.CONSUMER_URL
|
consumerUrl: process.env.CONSUMER_URL
|
||||||
? process.env.CONSUMER_URL
|
? process.env.CONSUMER_URL
|
||||||
: 'https://free-bch.fullstack.cash',
|
: 'https://free-bch.fullstack.cash',
|
||||||
|
// : 'https://dev-consumer.psfoundation.info',
|
||||||
// : 'https://wa-usa-bch-consumer.fullstackcash.nl',
|
// : 'https://wa-usa-bch-consumer.fullstackcash.nl',
|
||||||
|
|
||||||
// P2WDB URL that will accept API calls from the p2wdb npm library.
|
// P2WDB URL that will accept API calls from the p2wdb npm library.
|
||||||
p2wdbUrl: process.env.P2WDB_URL ? process.env.P2WDB_URL : 'https://p2wdb.fullstack.cash',
|
p2wdbUrl: process.env.P2WDB_URL ? process.env.P2WDB_URL : 'https://p2wdb.fullstack.cash',
|
||||||
|
// p2wdbUrl: process.env.P2WDB_URL ? process.env.P2WDB_URL : 'http://localhost:5010',
|
||||||
|
|
||||||
|
// Enable or Disable the usage of Mongo DB.
|
||||||
|
noMongo: process.env.NO_MONGO ? true : false,
|
||||||
|
|
||||||
|
// BEGIN WALLET CONFIGURATION
|
||||||
|
|
||||||
|
// BCH Mnemonic for generating encryption keys and payment address
|
||||||
|
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
|
||||||
|
|
||||||
|
// (Optional) use a wallet file generated by psf-bch-wallet.
|
||||||
|
walletFile: process.env.WALLET_FILE
|
||||||
|
? process.env.WALLET_FILE
|
||||||
|
: `${__dirname.toString()}/../../wallet.json`,
|
||||||
|
|
||||||
|
// Wallet Configuration
|
||||||
|
// Default value: web3 - connect to bch-api through web 3 JSON RPC over IPFS.
|
||||||
|
// Alternate value: web2 - connect to bch-api through a web 2 REST API over HTTP.
|
||||||
|
walletInterface: process.env.WALLET_INTERFACE ? process.env.WALLET_INTERFACE : 'web3',
|
||||||
|
|
||||||
|
// Wallet API Server
|
||||||
|
// Default web2 server: https://api.fullstack.cash/v5/
|
||||||
|
// Default web3 server: https://free-bch.fullstack.cash/
|
||||||
|
apiServer: process.env.WALLET_INTERFACE === 'web2'
|
||||||
|
? (
|
||||||
|
process.env.APISERVER
|
||||||
|
? process.env.APISERVER
|
||||||
|
: 'https://api.fullstack.cash/v5/'
|
||||||
|
)
|
||||||
|
: 'https://free-bch.fullstack.cash/',
|
||||||
|
|
||||||
|
// Basic Authentication Password for connecting to bch-api through a web 2
|
||||||
|
// REST API over HTTP.
|
||||||
|
authPass: process.env.WALLET_AUTH_PASS
|
||||||
|
? process.env.WALLET_AUTH_PASS
|
||||||
|
: '',
|
||||||
|
|
||||||
// FullStack.cash account information, used for automatic JWT handling.
|
// FullStack.cash account information, used for automatic JWT handling.
|
||||||
getJwtAtStartup: process.env.GET_JWT_AT_STARTUP ? true : false,
|
getJwtAtStartup: process.env.GET_JWT_AT_STARTUP ? true : false,
|
||||||
authServer: process.env.AUTHSERVER
|
authServer: process.env.AUTHSERVER
|
||||||
? process.env.AUTHSERVER
|
? process.env.AUTHSERVER
|
||||||
: 'https://auth.fullstack.cash',
|
: 'https://auth.fullstack.cash',
|
||||||
apiServer: process.env.APISERVER
|
|
||||||
? process.env.APISERVER
|
|
||||||
: 'https://api.fullstack.cash/v5/',
|
|
||||||
fullstackLogin: process.env.FULLSTACKLOGIN
|
fullstackLogin: process.env.FULLSTACKLOGIN
|
||||||
? process.env.FULLSTACKLOGIN
|
? process.env.FULLSTACKLOGIN
|
||||||
: 'demo@demo.com',
|
: 'demo@demo.com',
|
||||||
fullstackPassword: process.env.FULLSTACKPASS
|
fullstackPassword: process.env.FULLSTACKPASS
|
||||||
? process.env.FULLSTACKPASS
|
? process.env.FULLSTACKPASS
|
||||||
: 'demo',
|
: 'demo',
|
||||||
|
// authPass: process.env.FULLSTACK_AUTH_PASS
|
||||||
|
// ? process.env.FULLSTACK_AUTH_PASS
|
||||||
|
// : '',
|
||||||
|
|
||||||
|
// END WALLET CONFIGURATION
|
||||||
|
|
||||||
|
// BEGIN IPFS CONFIGURATION
|
||||||
|
|
||||||
|
// Debug verbosity level of helia-coord.
|
||||||
|
// 0 = no debug logs. 3 = maximum logs.
|
||||||
|
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 2,
|
||||||
|
|
||||||
|
// Enable/Disable the IPFS node at startup. Enabled by default.
|
||||||
|
// useIpfs: process.env.DISABLE_IPFS ? false : true,
|
||||||
|
useIpfs: false,
|
||||||
|
|
||||||
// IPFS settings.
|
|
||||||
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
|
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
|
||||||
// SSL domain used for websocket connection via browsers.
|
// SSL domain used for websocket connection via browsers.
|
||||||
crDomain: process.env.CR_DOMAIN ? process.env.CR_DOMAIN : '',
|
crDomain: process.env.CR_DOMAIN ? process.env.CR_DOMAIN : '',
|
||||||
@@ -84,7 +137,10 @@ export default {
|
|||||||
'@type': 'Organization',
|
'@type': 'Organization',
|
||||||
name: 'Permissionless Software Foundation',
|
name: 'Permissionless Software Foundation',
|
||||||
url: 'https://PSFoundation.cash'
|
url: 'https://PSFoundation.cash'
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// If this node has an IP4 address or domain name used to provide a REST API.
|
||||||
|
web2Api: process.env.WEB2_API ? process.env.WEB2_API : null
|
||||||
},
|
},
|
||||||
|
|
||||||
// P2WDB webhook endpoint
|
// P2WDB webhook endpoint
|
||||||
@@ -100,14 +156,11 @@ export default {
|
|||||||
// IPFS Ports
|
// IPFS Ports
|
||||||
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4001,
|
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4001,
|
||||||
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 4003,
|
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 4003,
|
||||||
|
ipfsWebRtcPort: process.env.IPFS_WEB_RTC_PORT ? process.env.IPFS_WEB_RTC_PORT : 4005,
|
||||||
// BCH Mnemonic for generating encryption keys and payment address
|
connectPref: process.env.CONNECT_PREF, // Used in helia-coord to select connection preference.
|
||||||
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
|
|
||||||
|
|
||||||
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 1,
|
|
||||||
|
|
||||||
// Settings for production, using external go-ipfs node.
|
// Settings for production, using external go-ipfs node.
|
||||||
isProduction: process.env.SVC_ENV === 'production' ? true : false,
|
isProduction: process.env.SVC_ENV === 'prod' ? true : false,
|
||||||
ipfsHost: process.env.IPFS_HOST ? process.env.IPFS_HOST : 'localhost',
|
ipfsHost: process.env.IPFS_HOST ? process.env.IPFS_HOST : 'localhost',
|
||||||
ipfsApiPort: process.env.IPFS_API_PORT
|
ipfsApiPort: process.env.IPFS_API_PORT
|
||||||
? parseInt(process.env.IPFS_API_PORT)
|
? parseInt(process.env.IPFS_API_PORT)
|
||||||
@@ -116,5 +169,31 @@ export default {
|
|||||||
chatPubSubChan: 'psf-ipfs-chat-001',
|
chatPubSubChan: 'psf-ipfs-chat-001',
|
||||||
|
|
||||||
// IPFS gateway
|
// IPFS gateway
|
||||||
ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://p2wdb-gateway.fullstack.cash/ipfs/'
|
ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://pin.fullstack.cash/ipfs/download/',
|
||||||
|
|
||||||
|
// This can add specific Circuit Relay v2 servers to connect to.
|
||||||
|
bootstrapRelays: [
|
||||||
|
// v2 Circuit Relay (Token Tiger)
|
||||||
|
// '/ip4/137.184.93.145/tcp/8001/p2p/12D3KooWGMEKkdJfyZbwdH9EafZbRTtMn7FnhWPrE4MhRty2763g',
|
||||||
|
|
||||||
|
// v2 Circuit Relay server (FullStack.cash)
|
||||||
|
// '/ip4/78.46.129.7/tcp/4001/p2p/12D3KooWFQ11GQ5NubsJGhYZ4X3wrAGimLevxfm6HPExCrMYhpSL'
|
||||||
|
],
|
||||||
|
|
||||||
|
// END IPFS CONFIGURATION
|
||||||
|
|
||||||
|
// Nostr
|
||||||
|
nostrRelay: process.env.NOSTR_RELAY ? process.env.NOSTR_RELAY : 'wss://nostr-relay.psfoundation.info',
|
||||||
|
nostrTopic: process.env.NOSTR_TOPIC ? process.env.NOSTR_TOPIC : 'bch-dex-test-topic-02',
|
||||||
|
nostrGlobalFeed: process.env.NOSTR_GLOBAL_FEED ? process.env.NOSTR_GLOBAL_FEED : 'slpdex-socialmedia',
|
||||||
|
|
||||||
|
// Account Configuration
|
||||||
|
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
|
||||||
|
|
||||||
|
// Admin password
|
||||||
|
adminPassword: process.env.ADMIN_PASSWORD,
|
||||||
|
|
||||||
|
// The Operator of BCH DEX can elect to receive a percentage of each sale.
|
||||||
|
operatorAddress: process.env.OPERATOR_ADDRESS ? process.env.OPERATOR_ADDRESS : 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
|
||||||
|
operatorPercentage: process.env.OPERATOR_PERCENTAGE ? parseFloat(process.env.OPERATOR_PERCENTAGE) : 2.0
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
-1
@@ -8,5 +8,6 @@ export default {
|
|||||||
session: 'secret-boilerplate-token',
|
session: 'secret-boilerplate-token',
|
||||||
token: 'secret-jwt-token',
|
token: 'secret-jwt-token',
|
||||||
database: 'mongodb://localhost:27017/bch-swap-service-test',
|
database: 'mongodb://localhost:27017/bch-swap-service-test',
|
||||||
env: 'test'
|
env: 'test',
|
||||||
|
nostrRelay: 'wss://'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,4 @@ if (env === 'test') {
|
|||||||
config = production
|
config = production
|
||||||
}
|
}
|
||||||
|
|
||||||
// const importStr = `./env/${env}.js`
|
|
||||||
// console.log('importStr: ', importStr)
|
|
||||||
// import config from importStr
|
|
||||||
|
|
||||||
export default Object.assign({}, common, config)
|
export default Object.assign({}, common, config)
|
||||||
|
|||||||
@@ -53,7 +53,4 @@ function applyPassportMods (passport) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// For testing
|
|
||||||
// export default { passport, passportCallback };
|
|
||||||
// export default applyPassportMods
|
|
||||||
export { applyPassportMods, passportCallback }
|
export { applyPassportMods, passportCallback }
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -1,4 +1,8 @@
|
|||||||
import Server from './bin/server.js'
|
import Server from './bin/server.js'
|
||||||
const server = new Server()
|
const server = new Server()
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.log(`Handling ${reason.code} error. stack: `, reason)
|
||||||
|
})
|
||||||
|
|
||||||
server.startServer()
|
server.startServer()
|
||||||
|
|||||||
+26701
File diff suppressed because it is too large
Load Diff
Generated
+13283
-14804
File diff suppressed because it is too large
Load Diff
+34
-12
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "bch-dex",
|
"name": "bch-dex",
|
||||||
"version": "2.0.2",
|
"version": "3.0.0",
|
||||||
"description": "A DEX on BCH for trading SLP tokens.",
|
"description": "A DEX on BCH for trading SLP tokens.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"test": "npm run test:all",
|
"test": "npm run test:unit",
|
||||||
"test:all": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
|
"test:all": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
|
||||||
"test:unit": "export BCH_DEX=test && mocha --exit --timeout 15000 --recursive test/unit/",
|
"test:unit": "export BCH_DEX=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit/",
|
||||||
"test:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 30000 test/e2e/automated/",
|
"test:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 30000 test/e2e/automated/",
|
||||||
|
"start:e2e:server": "export BCH_DEX=test && node index.js",
|
||||||
"test:integration": "export BCH_DEX=test && mocha --exit --timeout 45000 --recursive test/integration",
|
"test:integration": "export BCH_DEX=test && mocha --exit --timeout 45000 --recursive test/integration",
|
||||||
"test:temp": "export BCH_DEX=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
|
"test:temp": "export BCH_DEX=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
|
||||||
"lint": "standard --env mocha --fix",
|
"lint": "standard --env mocha --fix",
|
||||||
@@ -28,14 +29,34 @@
|
|||||||
},
|
},
|
||||||
"repository": "Permissionless-Software-Foundation/bch-dex",
|
"repository": "Permissionless-Software-Foundation/bch-dex",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chris.troutner/retry-queue": "1.0.5",
|
"@chainsafe/libp2p-gossipsub": "14.1.0",
|
||||||
"@psf/bch-js": "^6.4.5",
|
"@chainsafe/libp2p-noise": "16.0.1",
|
||||||
|
"@chainsafe/libp2p-yamux": "7.0.1",
|
||||||
|
"@chris.troutner/retry-queue": "1.0.8",
|
||||||
|
"@helia/unixfs": "4.0.2",
|
||||||
|
"@libp2p/bootstrap": "11.0.26",
|
||||||
|
"@libp2p/circuit-relay-v2": "3.2.2",
|
||||||
|
"@libp2p/config": "1.1.0",
|
||||||
|
"@libp2p/identify": "3.0.22",
|
||||||
|
"@libp2p/keychain": "5.0.14",
|
||||||
|
"@libp2p/logger": "5.1.8",
|
||||||
|
"@libp2p/ping": "2.0.22",
|
||||||
|
"@libp2p/tcp": "10.1.2",
|
||||||
|
"@libp2p/webrtc": "5.2.2",
|
||||||
|
"@libp2p/websockets": "9.2.2",
|
||||||
|
"@multiformats/multiaddr": "12.3.5",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"bch-message-lib": "2.2.1",
|
"bch-message-lib": "2.2.1",
|
||||||
|
"bch-nostr": "1.3.3",
|
||||||
|
"bch-token-sweep": "2.2.1",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"bitcoincashjs-lib": "3.3.3",
|
"bitcoincashjs-lib": "3.3.3",
|
||||||
|
"blockstore-fs": "2.0.2",
|
||||||
|
"datastore-fs": "10.0.2",
|
||||||
"glob": "7.1.6",
|
"glob": "7.1.6",
|
||||||
"ipfs-coord-esm": "9.1.3",
|
"helia": "5.2.1",
|
||||||
|
"helia-coord": "1.7.2",
|
||||||
|
"ipfs-coord-esm": "9.1.7",
|
||||||
"ipfs-http-client": "58.0.0",
|
"ipfs-http-client": "58.0.0",
|
||||||
"jsonrpc-lite": "2.2.0",
|
"jsonrpc-lite": "2.2.0",
|
||||||
"jsonwebtoken": "8.5.1",
|
"jsonwebtoken": "8.5.1",
|
||||||
@@ -51,25 +72,26 @@
|
|||||||
"koa-router": "10.0.0",
|
"koa-router": "10.0.0",
|
||||||
"koa-static": "5.0.0",
|
"koa-static": "5.0.0",
|
||||||
"koa2-ratelimit": "0.9.1",
|
"koa2-ratelimit": "0.9.1",
|
||||||
"libp2p": "^0.36.2",
|
"libp2p": "2.7.2",
|
||||||
"line-reader": "0.4.0",
|
"line-reader": "0.4.0",
|
||||||
"minimal-slp-wallet": "5.2.1",
|
"minimal-slp-wallet": "5.13.2",
|
||||||
"mongoose": "5.13.14",
|
"mongoose": "5.13.14",
|
||||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||||
"nodemailer": "6.7.5",
|
"nodemailer": "6.7.5",
|
||||||
"p2wdb": "2.2.3",
|
"nostr-tools": "2.10.4",
|
||||||
|
"p2wdb-esm": "2.2.9",
|
||||||
"passport-local": "1.0.0",
|
"passport-local": "1.0.0",
|
||||||
"public-ip": "^4.0.4",
|
"public-ip": "6.0.1",
|
||||||
"winston": "3.3.3",
|
"winston": "3.3.3",
|
||||||
"winston-daily-rotate-file": "4.5.0"
|
"winston-daily-rotate-file": "4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"apidoc": "0.51.1",
|
"apidoc": "0.51.1",
|
||||||
"bch-token-sweep": "2.1.2",
|
|
||||||
"c8": "7.12.0",
|
"c8": "7.12.0",
|
||||||
"chai": "4.3.0",
|
"chai": "4.3.0",
|
||||||
"coveralls": "2.11.4",
|
"coveralls": "2.11.4",
|
||||||
"husky": "4.3.8",
|
"husky": "4.3.8",
|
||||||
|
"lodash.clonedeep": "4.5.0",
|
||||||
"mocha": "10.0.0",
|
"mocha": "10.0.0",
|
||||||
"semantic-release": "19.0.3",
|
"semantic-release": "19.0.3",
|
||||||
"sinon": "9.2.4",
|
"sinon": "9.2.4",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
COMPOSE_PROJECT_NAME=bch-dex
|
||||||
@@ -12,7 +12,7 @@ RUN apt-get update
|
|||||||
RUN apt-get install -y sudo git curl nano gnupg wget
|
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||||
|
|
||||||
#Install Node and NPM
|
#Install Node and NPM
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
|
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
|
||||||
RUN bash nodesource_setup.sh
|
RUN bash nodesource_setup.sh
|
||||||
RUN apt-get install -y nodejs build-essential
|
RUN apt-get install -y nodejs build-essential
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ WORKDIR /home/safeuser
|
|||||||
|
|
||||||
# Update to the latest version of npm.
|
# Update to the latest version of npm.
|
||||||
#RUN npm install -g npm@7.23.0
|
#RUN npm install -g npm@7.23.0
|
||||||
RUN npm install -g npm
|
#RUN npm install -g npm
|
||||||
|
|
||||||
# npm mirror to prevent direct dependency on npm.
|
# npm mirror to prevent direct dependency on npm.
|
||||||
#RUN npm set registry http://94.130.170.209:4873/
|
#RUN npm set registry http://94.130.170.209:4873/
|
||||||
@@ -46,14 +46,14 @@ RUN npm install -g npm
|
|||||||
|
|
||||||
# Clone the rest.bitcoin.com repository
|
# Clone the rest.bitcoin.com repository
|
||||||
WORKDIR /home/safeuser
|
WORKDIR /home/safeuser
|
||||||
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v2
|
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3
|
||||||
|
|
||||||
# Switch to the desired branch. `master` is usually stable,
|
# Switch to the desired branch. `master` is usually stable,
|
||||||
# and `stage` has the most up-to-date changes.
|
# and `stage` has the most up-to-date changes.
|
||||||
WORKDIR /home/safeuser/bch-dex-ui-v2
|
WORKDIR /home/safeuser/bch-dex-ui-v3
|
||||||
|
|
||||||
# For development: switch to unstable branch
|
# For development: switch to unstable branch
|
||||||
RUN git checkout ct-unstable
|
RUN git checkout psfoundation
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
#RUN mkdir .ipfsdata
|
#RUN mkdir .ipfsdata
|
||||||
@@ -61,13 +61,16 @@ RUN git checkout ct-unstable
|
|||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Build the site
|
# Build the site
|
||||||
RUN CI=true npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Load the NGINX image.
|
# Load the NGINX image.
|
||||||
FROM nginx
|
FROM nginx
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
# Copy the files built in the first container to the new NGINX container.
|
# Copy the files built in the first container to the new NGINX container.
|
||||||
COPY --from=builder /home/safeuser/bch-dex-ui-v2/build /usr/share/nginx/html
|
COPY --from=builder /home/safeuser/bch-dex-ui-v3/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy the NGINX configuration file.
|
||||||
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
#USER safeuser
|
#USER safeuser
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
#access_log /var/log/nginx/host.access.log main;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error_page 404 /404.html;
|
||||||
|
|
||||||
|
# redirect server error pages to the static page /50x.html
|
||||||
|
#
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ RUN apt-get update
|
|||||||
RUN apt-get install -y sudo git curl nano gnupg wget
|
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||||
|
|
||||||
#Install Node and NPM
|
#Install Node and NPM
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
|
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
|
||||||
RUN bash nodesource_setup.sh
|
RUN bash nodesource_setup.sh
|
||||||
RUN apt-get install -y nodejs build-essential
|
RUN apt-get install -y nodejs build-essential
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ WORKDIR /home/safeuser
|
|||||||
|
|
||||||
# Update to the latest version of npm.
|
# Update to the latest version of npm.
|
||||||
#RUN npm install -g npm@7.23.0
|
#RUN npm install -g npm@7.23.0
|
||||||
RUN npm install -g npm
|
#RUN npm install -g npm
|
||||||
|
|
||||||
# npm mirror to prevent direct dependency on npm.
|
# npm mirror to prevent direct dependency on npm.
|
||||||
RUN npm set registry http://94.130.170.209:4873/
|
RUN npm set registry http://94.130.170.209:4873/
|
||||||
@@ -72,7 +72,10 @@ RUN npm run docs
|
|||||||
#EXPOSE 5010
|
#EXPOSE 5010
|
||||||
|
|
||||||
# Start the application.
|
# Start the application.
|
||||||
#COPY start-production.sh start-production.sh
|
|
||||||
CMD ["./start-production.sh"]
|
CMD ["./start-production.sh"]
|
||||||
|
|
||||||
#CMD ["npm", "start"]
|
#CMD ["npm", "start"]
|
||||||
|
|
||||||
|
# Used for debugging
|
||||||
|
#COPY dummy-app.js dummy-app.js
|
||||||
|
#CMD ["node", "dummy-app.js"]
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
setInterval(function () {
|
||||||
|
const now = new Date()
|
||||||
|
console.log(`timestamp: ${now.toLocaleString()}`)
|
||||||
|
}, 5000)
|
||||||
@@ -24,9 +24,14 @@ export DBURL=mongodb://172.17.0.1:5666/bch-swap-service-prod
|
|||||||
export PORT=5700
|
export PORT=5700
|
||||||
|
|
||||||
# bch-dex specific env vars
|
# bch-dex specific env vars
|
||||||
export BCH_DEX=production
|
export BCH_DEX=prod
|
||||||
export WEBHOOKSERVICE=http://172.17.0.1:5667/webhook
|
export WEBHOOKSERVICE=http://172.17.0.1:5667/webhook
|
||||||
export WEBHOOKTARGET=http://172.17.0.1:5700/p2wdb
|
export WEBHOOKTARGET=http://172.17.0.1:5700/p2wdb
|
||||||
export APP_ID=bch-dex-001
|
export APP_ID=bch-dex-001
|
||||||
|
|
||||||
|
#export P2WDB_URL=http://172.17.0.1:5667
|
||||||
|
|
||||||
|
export DISABLE_NEW_ACCOUNTS=true
|
||||||
|
export ADMIN_PASSWORD=decatur
|
||||||
|
|
||||||
npm start
|
npm start
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
version: '3.9'
|
version: '3.9'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mongo-p2wdb-service:
|
mongo-dex:
|
||||||
image: mongo:4.2.0
|
image: mongo:4.2.0
|
||||||
container_name: mongo-p2wdb-service
|
container_name: mongo-dex
|
||||||
ports:
|
ports:
|
||||||
- '5666:27017' # <host port>:<container port>
|
- '5666:27017' # <host port>:<container port>
|
||||||
volumes:
|
volumes:
|
||||||
@@ -13,67 +13,11 @@ services:
|
|||||||
command: mongod --logpath=/dev/null # -- quiet
|
command: mongod --logpath=/dev/null # -- quiet
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
ipfs:
|
|
||||||
# https://github.com/christroutner/trickle-ipfs
|
|
||||||
image: christroutner/trickle-ipfs:v1.0.1
|
|
||||||
#build:
|
|
||||||
# context: ./
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
container_name: ipfs
|
|
||||||
environment:
|
|
||||||
IPFS_DAEMON_ARGUMENTS: '--enable-pubsub-experiment --migrate=true --agent-version-suffix=docker --routing=dhtclient'
|
|
||||||
UPLOAD_KBPS: '1000'
|
|
||||||
DOWNLOAD_KBPS: '1000'
|
|
||||||
logging:
|
|
||||||
driver: 'json-file'
|
|
||||||
options:
|
|
||||||
max-size: '10m'
|
|
||||||
max-file: '10'
|
|
||||||
mem_limit: 2000mb
|
|
||||||
ports:
|
|
||||||
- 4001:4001
|
|
||||||
- 4003:4003
|
|
||||||
- 172.17.0.1:5001:5001
|
|
||||||
- 172.17.0.1:8080:8080
|
|
||||||
command: [
|
|
||||||
'./start-ipfs.sh'
|
|
||||||
]
|
|
||||||
volumes:
|
|
||||||
- ../data/go-ipfs/data:/root/.ipfs
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
p2wdb:
|
|
||||||
#build:
|
|
||||||
# context: ./p2wdb/
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
image: christroutner/p2wdb:v3.1.8
|
|
||||||
container_name: p2wdb
|
|
||||||
environment:
|
|
||||||
CONSUMER_URL: 'https://free-bch.fullstack.cash'
|
|
||||||
DEBUG_LEVEL: 1
|
|
||||||
logging:
|
|
||||||
driver: 'json-file'
|
|
||||||
options:
|
|
||||||
max-size: '10m'
|
|
||||||
max-file: '10'
|
|
||||||
mem_limit: 1gb
|
|
||||||
links:
|
|
||||||
- mongo-p2wdb-service
|
|
||||||
ports:
|
|
||||||
# <host port>:<container port>
|
|
||||||
- 5667:5667 # REST API
|
|
||||||
#- 5668:5668 # IPFS TCP port
|
|
||||||
#- 5669:5669 # IPFS WS Port
|
|
||||||
volumes:
|
|
||||||
- ../data/ipfsdata/p2wdb:/home/safeuser/ipfs-p2wdb-service/.ipfsdata/p2wdb
|
|
||||||
- ./p2wdb/start-production.sh:/home/safeuser/ipfs-p2wdb-service/start-production.sh
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
bch-dex:
|
bch-dex:
|
||||||
#build:
|
build:
|
||||||
# context: ./bch-dex/
|
context: ./bch-dex/
|
||||||
# dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: christroutner/bch-dex:v2.0.6
|
#image: christroutner/bch-dex:v2.2.4
|
||||||
container_name: bch-dex
|
container_name: bch-dex
|
||||||
environment:
|
environment:
|
||||||
CONSUMER_URL: 'https://free-bch.fullstack.cash'
|
CONSUMER_URL: 'https://free-bch.fullstack.cash'
|
||||||
@@ -84,29 +28,34 @@ services:
|
|||||||
max-file: '10'
|
max-file: '10'
|
||||||
mem_limit: 1000mb
|
mem_limit: 1000mb
|
||||||
links:
|
links:
|
||||||
- mongo-p2wdb-service
|
- mongo-dex
|
||||||
- p2wdb
|
#- p2wdb
|
||||||
ports:
|
ports:
|
||||||
- '5700:5700' # <host port>:<container port>
|
# <host port>:<container port>
|
||||||
|
- '5700:5700' # REST API
|
||||||
|
- '4001:4001' # TCP
|
||||||
|
- '4003:4003' # Websockets
|
||||||
|
- '4005:4005' # WebRTC
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ../scripts/wallet.json:/home/safeuser/bch-dex/wallet.json
|
- ../scripts/wallet.json:/home/safeuser/bch-dex/wallet.json
|
||||||
- ./bch-dex/start-production.sh:/home/safeuser/bch-dex/start-production.sh
|
- ./bch-dex/start-production.sh:/home/safeuser/bch-dex/start-production.sh
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
dex-ui:
|
dex-ui:
|
||||||
#build:
|
build:
|
||||||
# context: ./bch-dex-ui/
|
context: ./bch-dex-ui/
|
||||||
# dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: christroutner/bch-dex-ui:v1.12.0
|
#image: christroutner/bch-dex-ui:v1.2.4
|
||||||
container_name: dex-ui
|
container_name: dex-ui
|
||||||
environment:
|
environment:
|
||||||
SERVER: 'http://192.168.2.3'
|
SERVER: 'http://172.17.0.1'
|
||||||
logging:
|
logging:
|
||||||
driver: 'json-file'
|
driver: 'json-file'
|
||||||
options:
|
options:
|
||||||
max-size: '10m'
|
max-size: '10m'
|
||||||
max-file: '10'
|
max-file: '10'
|
||||||
mem_limit: 500mb
|
mem_limit: 1000mb
|
||||||
links:
|
links:
|
||||||
- bch-dex
|
- bch-dex
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
# Create a Docker container for the P2WDB
|
|
||||||
#
|
|
||||||
|
|
||||||
#IMAGE BUILD COMMANDS
|
|
||||||
FROM ubuntu:20.04
|
|
||||||
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
|
||||||
|
|
||||||
#Update the OS and install any OS packages needed.
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get install -y sudo git curl nano gnupg wget
|
|
||||||
|
|
||||||
#Install Node and NPM
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
|
|
||||||
RUN bash nodesource_setup.sh
|
|
||||||
RUN apt-get install -y nodejs build-essential
|
|
||||||
|
|
||||||
#Create the user 'safeuser' and add them to the sudo group.
|
|
||||||
RUN useradd -ms /bin/bash safeuser
|
|
||||||
RUN adduser safeuser sudo
|
|
||||||
|
|
||||||
#Set password to 'abcd8765' change value below if you want a different password
|
|
||||||
RUN echo safeuser:abcd8765 | chpasswd
|
|
||||||
|
|
||||||
#Set the working directory to be the users home directory
|
|
||||||
WORKDIR /home/safeuser
|
|
||||||
|
|
||||||
#Setup NPM for non-root global install (like on a mac)
|
|
||||||
RUN mkdir /home/safeuser/.npm-global
|
|
||||||
RUN chown -R safeuser .npm-global
|
|
||||||
RUN echo "export PATH=~/.npm-global/bin:$PATH" >> /home/safeuser/.profile
|
|
||||||
RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
|
|
||||||
|
|
||||||
# Update to the latest version of npm.
|
|
||||||
#RUN npm install -g npm@7.23.0
|
|
||||||
RUN npm install -g npm
|
|
||||||
|
|
||||||
# npm mirror to prevent direct dependency on npm.
|
|
||||||
RUN npm set registry http://94.130.170.209:4873/
|
|
||||||
|
|
||||||
# Switch to user account.
|
|
||||||
#USER safeuser
|
|
||||||
# Prep 'sudo' commands.
|
|
||||||
#RUN echo 'abcd8765' | sudo -S pwd
|
|
||||||
|
|
||||||
# Clone the rest.bitcoin.com repository
|
|
||||||
RUN git clone https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service
|
|
||||||
WORKDIR /home/safeuser/ipfs-p2wdb-service
|
|
||||||
RUN mkdir .ipfsdata
|
|
||||||
RUN git checkout ct-unstable
|
|
||||||
|
|
||||||
#RUN npm install fsevents
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
#RUN mkdir .ipfsdata
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Generate the API docs
|
|
||||||
RUN npm run docs
|
|
||||||
|
|
||||||
# Expose the port the API will be served on.
|
|
||||||
#EXPOSE 5010
|
|
||||||
|
|
||||||
# Start the application.
|
|
||||||
#COPY start-production.sh start-production.sh
|
|
||||||
CMD ["./start-production.sh"]
|
|
||||||
|
|
||||||
#CMD ["npm", "start"]
|
|
||||||
|
|
||||||
# Used for debugging
|
|
||||||
#COPY dummyapp.js dummyapp.js
|
|
||||||
#CMD ["node", "dummyapp.js"]
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
setInterval(function () {
|
|
||||||
const now = new Date()
|
|
||||||
console.log(`ping ${now.toLocaleString()}`)
|
|
||||||
}, 10000)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# BEGIN: Optional configuration settings
|
|
||||||
|
|
||||||
# This mnemonic is used to set up persistent public key for e2ee
|
|
||||||
# Replace this with your own 12-word mnemonic.
|
|
||||||
export MNEMONIC="mass response fiscal world message exact series swallow forward confirm canoe festival"
|
|
||||||
|
|
||||||
# The human readable name this IPFS node identifies as.
|
|
||||||
export COORD_NAME=generic-p2wdb-production
|
|
||||||
|
|
||||||
# Allow this node to function as a circuit relay. It must not be behind a firewall.
|
|
||||||
#export ENABLE_CIRCUIT_RELAY=true
|
|
||||||
# For browsers to use your circuit realy, you must set up a domain, SSL certificate,
|
|
||||||
# and you must forward that subdomain to the IPFS_WS_PORT.
|
|
||||||
#export CR_DOMAIN=subdomain.yourdomain.com
|
|
||||||
|
|
||||||
# Debug level. 0 = minimal info. 2 = max info.
|
|
||||||
# This value set by docker-compose.yml
|
|
||||||
#export DEBUG_LEVEL=1
|
|
||||||
|
|
||||||
# Log-in information for retrieving a JWT token from FullStack.cash.
|
|
||||||
export FULLSTACKLOGIN=demo@demo.com
|
|
||||||
export FULLSTACKPASS=demo
|
|
||||||
|
|
||||||
# END: Optional configuration settings
|
|
||||||
|
|
||||||
|
|
||||||
# Production database connection string.
|
|
||||||
export DBURL=mongodb://172.17.0.1:5666/p2wdb-service-dev
|
|
||||||
|
|
||||||
# Configure IPFS ports
|
|
||||||
#export IPFS_TCP_PORT=5668
|
|
||||||
#export IPFS_WS_PORT=5669
|
|
||||||
|
|
||||||
# Configure REST API port
|
|
||||||
export PORT=5667
|
|
||||||
|
|
||||||
export P2W_ENV=production
|
|
||||||
export IPFS_HOST=172.17.0.1
|
|
||||||
export IPFS_API_PORT=5001
|
|
||||||
export IPFS_TCP_PORT=4001
|
|
||||||
|
|
||||||
# P2WDB specific env vars
|
|
||||||
#export ORBITDB_NAME=/orbitdb/zdpuAm4fPafpjs5mgbBUJV8Az7kd8Xj1ttD6M1JaAxF8RMRrw/psf-bch-p2wdb-keyvalue-v1.0.0-0004
|
|
||||||
|
|
||||||
npm start
|
|
||||||
@@ -12,9 +12,12 @@ RUN apt-get update
|
|||||||
RUN apt-get install -y sudo git curl nano gnupg wget
|
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||||
|
|
||||||
#Install Node and NPM
|
#Install Node and NPM
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
|
RUN sudo apt-get install -y ca-certificates curl gnupg
|
||||||
RUN bash nodesource_setup.sh
|
RUN sudo mkdir -p /etc/apt/keyrings
|
||||||
RUN apt-get install -y nodejs build-essential
|
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||||
|
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||||
|
RUN sudo apt-get update
|
||||||
|
RUN sudo apt-get install -y nodejs build-essential
|
||||||
|
|
||||||
#Create the user 'safeuser' and add them to the sudo group.
|
#Create the user 'safeuser' and add them to the sudo group.
|
||||||
#RUN useradd -ms /bin/bash safeuser
|
#RUN useradd -ms /bin/bash safeuser
|
||||||
@@ -34,7 +37,7 @@ WORKDIR /home/safeuser
|
|||||||
|
|
||||||
# Update to the latest version of npm.
|
# Update to the latest version of npm.
|
||||||
#RUN npm install -g npm@7.23.0
|
#RUN npm install -g npm@7.23.0
|
||||||
RUN npm install -g npm
|
#RUN npm install -g npm
|
||||||
|
|
||||||
# npm mirror to prevent direct dependency on npm.
|
# npm mirror to prevent direct dependency on npm.
|
||||||
RUN npm set registry http://94.130.170.209:4873/
|
RUN npm set registry http://94.130.170.209:4873/
|
||||||
@@ -53,7 +56,7 @@ RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex
|
|||||||
WORKDIR /home/safeuser/bch-dex
|
WORKDIR /home/safeuser/bch-dex
|
||||||
|
|
||||||
# For development: switch to unstable branch
|
# For development: switch to unstable branch
|
||||||
RUN git checkout ct-unstable
|
RUN git checkout master
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
#RUN mkdir .ipfsdata
|
#RUN mkdir .ipfsdata
|
||||||
@@ -63,14 +66,6 @@ RUN npm install
|
|||||||
# Generate the API docs
|
# Generate the API docs
|
||||||
RUN npm run docs
|
RUN npm run docs
|
||||||
|
|
||||||
# Copy the wallet files
|
|
||||||
#COPY wallet.json wallet.json
|
|
||||||
|
|
||||||
#VOLUME /home/safeuser/keys
|
|
||||||
|
|
||||||
# Expose the port the API will be served on.
|
|
||||||
#EXPOSE 5010
|
|
||||||
|
|
||||||
# Start the application.
|
# Start the application.
|
||||||
#COPY start-production.sh start-production.sh
|
#COPY start-production.sh start-production.sh
|
||||||
CMD ["./start-production.sh"]
|
CMD ["./start-production.sh"]
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ services:
|
|||||||
#build:
|
#build:
|
||||||
# context: ./bch-dex/
|
# context: ./bch-dex/
|
||||||
# dockerfile: Dockerfile
|
# dockerfile: Dockerfile
|
||||||
image: christroutner/bch-dex-rpi:v2.0.6
|
image: christroutner/bch-dex-rpi:v2.0.8
|
||||||
container_name: bch-dex
|
container_name: bch-dex
|
||||||
environment:
|
environment:
|
||||||
#CONSUMER_URL: 'https://free-bch.fullstack.cash'
|
#CONSUMER_URL: 'https://free-bch.fullstack.cash'
|
||||||
@@ -95,7 +95,7 @@ services:
|
|||||||
#build:
|
#build:
|
||||||
# context: ./bch-dex-ui/
|
# context: ./bch-dex-ui/
|
||||||
# dockerfile: Dockerfile
|
# dockerfile: Dockerfile
|
||||||
image: christroutner/bch-dex-ui-rpi:v1.2.2
|
image: christroutner/bch-dex-ui-rpi:v1.2.4
|
||||||
container_name: dex-ui
|
container_name: dex-ui
|
||||||
logging:
|
logging:
|
||||||
driver: 'json-file'
|
driver: 'json-file'
|
||||||
|
|||||||
@@ -41,8 +41,14 @@ export IPFS_HOST=172.17.0.1
|
|||||||
export IPFS_API_PORT=5001
|
export IPFS_API_PORT=5001
|
||||||
export IPFS_TCP_PORT=4001
|
export IPFS_TCP_PORT=4001
|
||||||
export IPFS_WS_PORT=4003
|
export IPFS_WS_PORT=4003
|
||||||
|
export IPFS_WEB_RTC_PORT=4005
|
||||||
|
# Set the debug level for helia-coord. 0-3.
|
||||||
|
# 0 = no debug logs. 3 = maximum debug logs.
|
||||||
|
export DEBUG_LEVEL=0
|
||||||
|
|
||||||
# P2WDB specific env vars
|
# Use this if the IPFS node has a publically accessible IP address.
|
||||||
export ORBITDB_NAME=/orbitdb/zdpuAqNiwLiJBfbRK7uihV2hAbNSXj78ufzv5VyQb8GuvRwDh/psf-bch-p2wdb-keyvalue-v3.0.0-0001
|
#export CONNECT_PREF=direct
|
||||||
|
# Use this if the IPFS node is behind a NAT or Firewall and cannot be accessed directly.
|
||||||
|
export CONNECT_PREF=cr
|
||||||
|
|
||||||
npm start
|
npm start
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
This script is similar ot sweep-wallet.js, but it can do a much faster (bulk)
|
||||||
|
scanning of addresses. However, this requires a subscription to a FullStack.cash
|
||||||
|
or access to a web2 Cash Stack.
|
||||||
|
|
||||||
|
This script will scan 20 addresses at a time. If a balance is found in any
|
||||||
|
of the addresses, it will sweep the funds from that address.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
// Public npm libraries
|
||||||
|
import BchTokenSweep from 'bch-token-sweep/index.js'
|
||||||
|
|
||||||
|
// Local libraries
|
||||||
|
import WalletAdapter from '../../src/adapters/wallet.js'
|
||||||
|
import BCHJS from '@psf/bch-js'
|
||||||
|
import JwtLib from 'jwt-bch-lib'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
// const EMTPY_ADDR_CUTOFF = 100
|
||||||
|
const LAST_ADDR_INDEX = 100
|
||||||
|
|
||||||
|
if (!process.env.BCHJSTOKEN) {
|
||||||
|
console.log('You will need a JWT token from FullStack.cash to execute this script. Export it to the BCHJSTOKEN environment variable and try again.')
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sweepFunds () {
|
||||||
|
try {
|
||||||
|
// Configure the wallet library to use a FullStack.cash or a local Cash Stack
|
||||||
|
const walletLib = new WalletAdapter()
|
||||||
|
walletLib.config.useFullStackCash = true
|
||||||
|
walletLib.config.apiServer = 'http://192.168.2.129:3000/v5/'
|
||||||
|
|
||||||
|
// const walletInfo = await walletLib.openWallet()
|
||||||
|
const walletInfo = await openWallet(walletLib)
|
||||||
|
const wallet = await walletLib.instanceWallet(walletInfo)
|
||||||
|
console.log('walletInfo: ', walletInfo)
|
||||||
|
|
||||||
|
const rootAddr = walletInfo.cashAddress
|
||||||
|
const rootWif = walletInfo.privateKey
|
||||||
|
console.log(`Sweeping all funds into root address ${rootAddr}...`)
|
||||||
|
|
||||||
|
// Instantiate bch-js, and use the JWT token saved to environment variable.
|
||||||
|
|
||||||
|
// Generate an HD tree
|
||||||
|
// const bchjs = wallet.bchjs
|
||||||
|
const bchjs = new BCHJS()
|
||||||
|
const rootSeed = await bchjs.Mnemonic.toSeed(walletInfo.mnemonic)
|
||||||
|
const masterHDNode = bchjs.HDNode.fromSeed(rootSeed)
|
||||||
|
|
||||||
|
const emptyAddrCnt = 0
|
||||||
|
const hdIndex = 1
|
||||||
|
|
||||||
|
// Divide all the addresses by 20, since that's how many will be checked at once.
|
||||||
|
const numOfLoops = Math.ceil(LAST_ADDR_INDEX / 20)
|
||||||
|
|
||||||
|
for (let i = 1; i < numOfLoops; i++) {
|
||||||
|
const startIndex = i
|
||||||
|
const stopIndex = i * 20
|
||||||
|
console.log(`startIndex: ${startIndex}, stopIndex: ${stopIndex}`)
|
||||||
|
|
||||||
|
// Generate the next block of 20 key pairs to scan.
|
||||||
|
const addrsToScan = []
|
||||||
|
const wifsToScan = []
|
||||||
|
for (let j = startIndex; j < stopIndex; j++) {
|
||||||
|
// Generate a keypair from the HD wallet.
|
||||||
|
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${j}`)
|
||||||
|
const cashAddress = bchjs.HDNode.toCashAddress(childNode)
|
||||||
|
const wifToSweep = bchjs.HDNode.toWIF(childNode)
|
||||||
|
|
||||||
|
addrsToScan.push(cashAddress)
|
||||||
|
wifsToScan.push(wifToSweep)
|
||||||
|
}
|
||||||
|
console.log(`Scanning these addresses for a balance: ${JSON.stringify(addrsToScan, null, 2)}`)
|
||||||
|
|
||||||
|
// Scan a block of 20 addresses for a balance.
|
||||||
|
const balances = await bchjs.Electrumx.balance(addrsToScan)
|
||||||
|
console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
|
||||||
|
|
||||||
|
// Scan through the results and look for a balance.
|
||||||
|
for (let j = 0; j < balances.balances.length; j++) {
|
||||||
|
const thisBalance = balances.balances[j]
|
||||||
|
|
||||||
|
// const combinedBal = thisBalance.balance.confirmed + thisBalance.balance.
|
||||||
|
|
||||||
|
// if(thisBalance.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do {
|
||||||
|
// // Generate a keypair from the HD wallet.
|
||||||
|
// const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
||||||
|
// const cashAddress = bchjs.HDNode.toCashAddress(childNode)
|
||||||
|
// const wifToSweep = bchjs.HDNode.toWIF(childNode)
|
||||||
|
//
|
||||||
|
// console.log(`\nSweeping ${cashAddress} with private key ${wifToSweep}`)
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
// // Sweep tokens from address
|
||||||
|
// const sweeper = new BchTokenSweep(
|
||||||
|
// wifToSweep,
|
||||||
|
// rootWif,
|
||||||
|
// wallet,
|
||||||
|
// 550,
|
||||||
|
// rootAddr
|
||||||
|
// )
|
||||||
|
// await sweeper.populateObjectFromNetwork()
|
||||||
|
//
|
||||||
|
// const hex = await sweeper.sweepTo(rootAddr)
|
||||||
|
// // console.log(`hex: ${hex}`)
|
||||||
|
//
|
||||||
|
// const txid = await sweeper.blockchain.broadcast(hex)
|
||||||
|
//
|
||||||
|
// // console.log('Transaction ID', txid)
|
||||||
|
// console.log(`Swept HD index ${hdIndex}. TXID: ${txid}`)
|
||||||
|
//
|
||||||
|
// emptyAddrCnt = 0
|
||||||
|
//
|
||||||
|
// // Wait between loop iterations.
|
||||||
|
// await bchjs.Util.sleep(3000)
|
||||||
|
// } catch (err) {
|
||||||
|
// console.log(`error message with index ${hdIndex}: ${err.message}`)
|
||||||
|
// // console.log(err)
|
||||||
|
// emptyAddrCnt++
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// hdIndex++
|
||||||
|
// } while (emptyAddrCnt < EMTPY_ADDR_CUTOFF)
|
||||||
|
//
|
||||||
|
// console.log(`${EMTPY_ADDR_CUTOFF} empty addresses detected. Exiting.`)
|
||||||
|
//
|
||||||
|
// console.log('\n\nDo not forget to reset the nextAddress property in the wallet.json file!\n\n')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in sweepFunds(): ', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sweepFunds()
|
||||||
|
|
||||||
|
// Open the wallet file, or create one if the file doesn't exist.
|
||||||
|
// Does not instance the wallet. The output of this function is expected to
|
||||||
|
// be passed to instanceWallet().
|
||||||
|
async function openWallet (walletLib) {
|
||||||
|
try {
|
||||||
|
// console.log('this.WALLET_FILE: ', this.WALLET_FILE)
|
||||||
|
const walletData = await walletLib.jsonFiles.readJSON('./wallet.json')
|
||||||
|
|
||||||
|
return walletData
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in openWallet()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Public NPM libraries
|
// Public NPM libraries
|
||||||
import BchWallet from 'minimal-slp-wallet/index'
|
import BchWallet from 'minimal-slp-wallet'
|
||||||
|
|
||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@ async function createWallet () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the wallet to be created.
|
// Wait for the wallet to be created.
|
||||||
this.bchWallet = new BchWallet(undefined, advancedConfig)
|
const bchWallet = new BchWallet(undefined, advancedConfig)
|
||||||
await this.bchWallet.walletInfoPromise
|
await bchWallet.walletInfoPromise
|
||||||
const walletData = this.bchWallet.walletInfo
|
const walletData = bchWallet.walletInfo
|
||||||
walletData.nextAddress = 1
|
walletData.nextAddress = 1
|
||||||
|
|
||||||
// Save the wallet file to disk.
|
// Save the wallet file to disk.
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
This script is the same as sweep-wallet.js, but it requires a FullStack.cash
|
||||||
|
account, which makes the scanning much faster.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public npm libraries
|
||||||
|
// const BCHJS = require('@psf/bch-js')
|
||||||
|
import BchTokenSweep from 'bch-token-sweep/index.js'
|
||||||
|
|
||||||
|
// Local libraries
|
||||||
|
import WalletAdapter from '../../src/adapters/wallet.js'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
// const EMTPY_ADDR_CUTOFF = 10
|
||||||
|
const START_INDEX = 1
|
||||||
|
const LAST_ADDR_INDEX = 100
|
||||||
|
|
||||||
|
if (!process.env.BCHJSTOKEN) {
|
||||||
|
console.log('You will need a JWT token from FullStack.cash to execute this script. Export it to the BCHJSTOKEN environment variable and try again.')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sweepFunds () {
|
||||||
|
try {
|
||||||
|
// Configure the wallet library to use a FullStack.cash or a local Cash Stack
|
||||||
|
const walletLib = new WalletAdapter()
|
||||||
|
walletLib.config.useFullStackCash = true
|
||||||
|
// walletLib.config.apiServer = 'http://192.168.2.129:3000/v5/'
|
||||||
|
walletLib.config.apiServer = 'https://api.fullstack.cash/v5/'
|
||||||
|
|
||||||
|
// Open the wallet files.
|
||||||
|
const walletInfo = await openWallet(walletLib)
|
||||||
|
const wallet = await walletLib.instanceWallet(walletInfo)
|
||||||
|
console.log('walletInfo: ', walletInfo)
|
||||||
|
|
||||||
|
const rootAddr = walletInfo.cashAddress
|
||||||
|
const rootWif = walletInfo.privateKey
|
||||||
|
console.log(`Sweeping all funds into root address ${rootAddr}...`)
|
||||||
|
|
||||||
|
// Generate an HD tree
|
||||||
|
const bchjs = wallet.bchjs
|
||||||
|
const rootSeed = await bchjs.Mnemonic.toSeed(walletInfo.mnemonic)
|
||||||
|
const masterHDNode = bchjs.HDNode.fromSeed(rootSeed)
|
||||||
|
|
||||||
|
let emptyAddrCnt = 0
|
||||||
|
let hdIndex = START_INDEX
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Generate a keypair from the HD wallet.
|
||||||
|
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
||||||
|
const cashAddress = bchjs.HDNode.toCashAddress(childNode)
|
||||||
|
const wifToSweep = bchjs.HDNode.toWIF(childNode)
|
||||||
|
|
||||||
|
console.log(`\nSweeping ${cashAddress} with private key ${wifToSweep}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Sweep tokens from address
|
||||||
|
const sweeper = new BchTokenSweep(
|
||||||
|
wifToSweep,
|
||||||
|
rootWif,
|
||||||
|
wallet,
|
||||||
|
550,
|
||||||
|
rootAddr
|
||||||
|
)
|
||||||
|
await sweeper.populateObjectFromNetwork()
|
||||||
|
|
||||||
|
const hex = await sweeper.sweepTo(rootAddr)
|
||||||
|
// console.log(`hex: ${hex}`)
|
||||||
|
|
||||||
|
const txid = await sweeper.blockchain.broadcast(hex)
|
||||||
|
|
||||||
|
// console.log('Transaction ID', txid)
|
||||||
|
console.log(`Swept HD index ${hdIndex}. TXID: ${txid}`)
|
||||||
|
|
||||||
|
emptyAddrCnt = 0
|
||||||
|
|
||||||
|
// Wait between loop iterations.
|
||||||
|
await bchjs.Util.sleep(3000)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error message with index ${hdIndex}: ${err.message}`)
|
||||||
|
// console.log(err)
|
||||||
|
emptyAddrCnt++
|
||||||
|
}
|
||||||
|
|
||||||
|
hdIndex++
|
||||||
|
// } while (emptyAddrCnt < EMTPY_ADDR_CUTOFF)
|
||||||
|
// console.log(`${EMTPY_ADDR_CUTOFF} empty addresses detected. Exiting.`)
|
||||||
|
} while (hdIndex < LAST_ADDR_INDEX)
|
||||||
|
console.log(`Last index of ${LAST_ADDR_INDEX} reached. emptyAddrCnt: ${emptyAddrCnt}`)
|
||||||
|
|
||||||
|
console.log('\n\nDo not forget to reset the nextAddress property in the wallet.json file!\n\n')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in sweepFunds(): ', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sweepFunds()
|
||||||
|
|
||||||
|
// Open the wallet file, or create one if the file doesn't exist.
|
||||||
|
// Does not instance the wallet. The output of this function is expected to
|
||||||
|
// be passed to instanceWallet().
|
||||||
|
async function openWallet (walletLib) {
|
||||||
|
try {
|
||||||
|
// console.log('this.WALLET_FILE: ', this.WALLET_FILE)
|
||||||
|
const walletData = await walletLib.jsonFiles.readJSON('./wallet.json')
|
||||||
|
|
||||||
|
return walletData
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in openWallet()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
This script is the same as sweep-wallet.js, but instead of sweeping the keys,
|
||||||
|
it simply lists out the addresses in the HD keychain. This is hany when trying
|
||||||
|
to figure out which address a token was sent to.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public npm libraries
|
||||||
|
|
||||||
|
// Local libraries
|
||||||
|
import WalletAdapter from '../../src/adapters/wallet.js'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const START_INDEX = 1
|
||||||
|
const LAST_ADDR_INDEX = 100
|
||||||
|
|
||||||
|
async function listAddrs () {
|
||||||
|
try {
|
||||||
|
// Configure the wallet library to use a FullStack.cash or a local Cash Stack
|
||||||
|
const walletLib = new WalletAdapter()
|
||||||
|
|
||||||
|
// Open the wallet files.
|
||||||
|
const walletInfo = await openWallet(walletLib)
|
||||||
|
const wallet = await walletLib.instanceWallet(walletInfo)
|
||||||
|
console.log('walletInfo: ', walletInfo)
|
||||||
|
|
||||||
|
const rootAddr = walletInfo.cashAddress
|
||||||
|
// const rootWif = walletInfo.privateKey
|
||||||
|
console.log(`Sweeping all funds into root address ${rootAddr}...`)
|
||||||
|
|
||||||
|
// Generate an HD tree
|
||||||
|
const bchjs = wallet.bchjs
|
||||||
|
const rootSeed = await bchjs.Mnemonic.toSeed(walletInfo.mnemonic)
|
||||||
|
const masterHDNode = bchjs.HDNode.fromSeed(rootSeed)
|
||||||
|
|
||||||
|
let hdIndex = START_INDEX
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Generate a keypair from the HD wallet.
|
||||||
|
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
||||||
|
const cashAddress = bchjs.HDNode.toCashAddress(childNode)
|
||||||
|
// const wifToSweep = bchjs.HDNode.toWIF(childNode)
|
||||||
|
|
||||||
|
console.log(`HD Index ${hdIndex}: ${cashAddress}`)
|
||||||
|
|
||||||
|
hdIndex++
|
||||||
|
} while (hdIndex < LAST_ADDR_INDEX)
|
||||||
|
console.log(`Last index of ${LAST_ADDR_INDEX} reached.`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in sweepFunds(): ', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listAddrs()
|
||||||
|
|
||||||
|
// Open the wallet file, or create one if the file doesn't exist.
|
||||||
|
// Does not instance the wallet. The output of this function is expected to
|
||||||
|
// be passed to instanceWallet().
|
||||||
|
async function openWallet (walletLib) {
|
||||||
|
try {
|
||||||
|
// console.log('this.WALLET_FILE: ', this.WALLET_FILE)
|
||||||
|
const walletData = await walletLib.jsonFiles.readJSON('./wallet.json')
|
||||||
|
|
||||||
|
return walletData
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in openWallet()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Global npm libraries
|
// Global npm libraries
|
||||||
import BchWallet from 'minimal-slp-wallet/index.js'
|
import BchWallet from 'minimal-slp-wallet'
|
||||||
|
|
||||||
// Local libraries
|
// Local libraries
|
||||||
import WalletAdapter from '../../src/adapters/wallet.js'
|
import WalletAdapter from '../../src/adapters/wallet.js'
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
This script will travers the HD wallet and sweep funds and tokens back
|
This script will travers the HD wallet and sweep funds and tokens back
|
||||||
into the root address (index 0). That root address needs to have funds
|
into the root address (index 0). That root address needs to have funds
|
||||||
to pay for the transactions.
|
to pay for the transactions.
|
||||||
|
|
||||||
This script will sweep BCH and fungible (Type 1) tokens, but not NFTs.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Public npm libraries
|
// Public npm libraries
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"privateKey": "L4VyBDiWYYeeZhkM3xE3nsZigXJYmvRM29YBxoC7da4FzvubjxFo",
|
||||||
|
"publicKey": "028d6c77d865e6140db4b879a9bdf69ee4846593c0b6c48a82c448d5792806aab1",
|
||||||
|
"mnemonic": "split title trigger soda execute frost brush labor vague protect bicycle fiscal",
|
||||||
|
"cashAddress": "bitcoincash:qprrdk7en7m774gsafkycxcymc9ry209jus5v8wx5z",
|
||||||
|
"address": "bitcoincash:qprrdk7en7m774gsafkycxcymc9ry209jus5v8wx5z",
|
||||||
|
"legacyAddress": "17QG25rNUfnfj2NtdaFiyiqKSJatvEnMYd",
|
||||||
|
"hdPath": "m/44'/245'/0'/0/0",
|
||||||
|
"slpAddress": "simpleledger:qprrdk7en7m774gsafkycxcymc9ry209juu08umx2u",
|
||||||
|
"nextAddress": 1
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export COORD_NAME=ipfs-service-provider-generic
|
|||||||
export MNEMONIC="churn aisle shield silver ladder swear hunt slim pen demand spoil veteran"
|
export MNEMONIC="churn aisle shield silver ladder swear hunt slim pen demand spoil veteran"
|
||||||
|
|
||||||
# 0 = less verbose. 3 = most verbose
|
# 0 = less verbose. 3 = most verbose
|
||||||
export DEBUG_LEVEL=2
|
export DEBUG_LEVEL=0
|
||||||
|
|
||||||
# Production settings that use external IPFS node.
|
# Production settings that use external IPFS node.
|
||||||
# https://github.com/christroutner/docker-ipfs
|
# https://github.com/christroutner/docker-ipfs
|
||||||
|
|||||||
+27
-58
@@ -51,70 +51,38 @@ class Admin {
|
|||||||
// used by the Listing Manager and test scripts, in order access private API
|
// used by the Listing Manager and test scripts, in order access private API
|
||||||
// functions.
|
// functions.
|
||||||
async createSystemUser () {
|
async createSystemUser () {
|
||||||
// Create the system user.
|
|
||||||
try {
|
try {
|
||||||
context.password = _this._randomString(20)
|
const context = {
|
||||||
|
email: 'system@system.com',
|
||||||
const options = {
|
name: 'admin',
|
||||||
method: 'POST',
|
password: _this.config.adminPassword || _this._randomString(20),
|
||||||
url: `${LOCALHOST}/users`,
|
type: 'admin',
|
||||||
data: {
|
mnemonic: 'spread ivory oval pioneer flash gap merit nerve simple image entire trouble'
|
||||||
user: {
|
|
||||||
email: 'system@system.com',
|
|
||||||
password: context.password,
|
|
||||||
name: 'admin'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const result = await _this.axios.request(options)
|
// Check if the user already exists
|
||||||
// console.log('admin.data: ', result.data)
|
let adminUser = await _this.User.findOne({ email: context.email })
|
||||||
|
|
||||||
context.email = result.data.user.email
|
if (adminUser) {
|
||||||
context.id = result.data.user._id
|
// Update the password
|
||||||
context.token = result.data.token
|
adminUser.password = context.password
|
||||||
|
} else {
|
||||||
|
// Create a new admin user
|
||||||
|
adminUser = new _this.User(context)
|
||||||
|
}
|
||||||
|
// Update context with the new user id and token
|
||||||
|
context.id = adminUser._id
|
||||||
|
context.token = await adminUser.generateToken()
|
||||||
|
|
||||||
// Get the mongoDB entry
|
// Save the user
|
||||||
const user = await _this.User.findById(context.id)
|
await adminUser.save()
|
||||||
|
|
||||||
// Change the user type to admin
|
// Write the user data to the JSON file
|
||||||
user.type = 'admin'
|
await _this.jsonFiles.writeJSON(context, JSON_PATH)
|
||||||
// console.log(`user created: ${JSON.stringify(user, null, 2)}`)
|
|
||||||
|
|
||||||
// Save the user model.
|
|
||||||
await user.save()
|
|
||||||
|
|
||||||
// console.log(`admin user created: ${JSON.stringify(result.body, null, 2)}`)
|
|
||||||
// console.log(`with password: ${context.password}`)
|
|
||||||
|
|
||||||
// Write out the system user information to a JSON file that external
|
|
||||||
// applications like the Task Manager and the test scripts can access.
|
|
||||||
|
|
||||||
await jsonFiles.writeJSON(context, JSON_PATH)
|
|
||||||
// console.log('context: ', context)
|
|
||||||
// console.log('JSON_PATH: ', JSON_PATH)
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
// Handle existing system user.
|
console.log('Error in admin.js/createSystemUser()')
|
||||||
if (err.response.status === 422) {
|
throw error
|
||||||
try {
|
|
||||||
// Delete the existing user
|
|
||||||
await _this.deleteExistingSystemUser()
|
|
||||||
|
|
||||||
// Call this function again.
|
|
||||||
return _this.createSystemUser()
|
|
||||||
} catch (err2) {
|
|
||||||
console.error(
|
|
||||||
'Error in admin.js/createSystemUser() while trying generate new system user.'
|
|
||||||
)
|
|
||||||
// process.end(1)
|
|
||||||
throw err2
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Error in admin.js/createSystemUser: ')
|
|
||||||
// process.end(1)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +97,7 @@ class Admin {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await _this.User.deleteOne({ email: 'system@system.com' })
|
await _this.User.deleteOne({ email: 'system@system.com' })
|
||||||
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Error in admin.js/deleteExistingSystemUser()')
|
console.log('Error in admin.js/deleteExistingSystemUser()')
|
||||||
throw err
|
throw err
|
||||||
@@ -152,7 +121,7 @@ class Admin {
|
|||||||
Accept: 'application/json'
|
Accept: 'application/json'
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
email: 'system@system.com',
|
email: existingUser.email,
|
||||||
password: existingUser.password
|
password: existingUser.password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -6,7 +6,7 @@
|
|||||||
import BCHJS from '@psf/bch-js'
|
import BCHJS from '@psf/bch-js'
|
||||||
|
|
||||||
import MsgLib from 'bch-message-lib'
|
import MsgLib from 'bch-message-lib'
|
||||||
import BchWallet from 'minimal-slp-wallet/index.js'
|
import BchWallet from 'minimal-slp-wallet'
|
||||||
|
|
||||||
class Bch {
|
class Bch {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -66,6 +66,9 @@ class Bch {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/** DEV NOTE: This function are deprecated.?
|
||||||
|
* https://github.com/Permissionless-Software-Foundation/bch-message-lib/pull/41
|
||||||
|
*/
|
||||||
async getMerit (slpAddr) {
|
async getMerit (slpAddr) {
|
||||||
try {
|
try {
|
||||||
if (!slpAddr || typeof slpAddr !== 'string') {
|
if (!slpAddr || typeof slpAddr !== 'string') {
|
||||||
|
|||||||
+24
-5
@@ -9,22 +9,21 @@ import BCHJS from '@psf/bch-js'
|
|||||||
|
|
||||||
// Load individual adapter libraries.
|
// Load individual adapter libraries.
|
||||||
import IPFSAdapter from './ipfs/index.js'
|
import IPFSAdapter from './ipfs/index.js'
|
||||||
|
|
||||||
import LocalDB from './localdb/index.js'
|
import LocalDB from './localdb/index.js'
|
||||||
import LogsAPI from './logapi.js'
|
import LogsAPI from './logapi.js'
|
||||||
import Passport from './passport.js'
|
import Passport from './passport.js'
|
||||||
import Nodemailer from './nodemailer.js'
|
import Nodemailer from './nodemailer.js'
|
||||||
|
|
||||||
// const { wlogger } = require('./wlogger')
|
// const { wlogger } = require('./wlogger')
|
||||||
import JSONFiles from './json-files.js'
|
import JSONFiles from './json-files.js'
|
||||||
|
|
||||||
import FullStackJWT from './fullstack-jwt.js'
|
import FullStackJWT from './fullstack-jwt.js'
|
||||||
import config from '../../config/index.js'
|
import config from '../../config/index.js'
|
||||||
|
// import Wallet from './wallet.adapter.js'
|
||||||
|
|
||||||
import BCHAdapter from './bch.js'
|
import BCHAdapter from './bch.js'
|
||||||
import WalletAdapter from './wallet.js'
|
import WalletAdapter from './wallet.js'
|
||||||
import P2wdbAdapter from './p2wdb-adapter.js'
|
import P2wdbAdapter from './p2wdb-adapter.js'
|
||||||
import Webhook from './webhook.js'
|
import Webhook from './webhook.js'
|
||||||
|
import NostrAdapter from './nostr.js'
|
||||||
|
|
||||||
class Adapters {
|
class Adapters {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
@@ -43,6 +42,9 @@ class Adapters {
|
|||||||
localConfig.bchWallet = this.wallet.bchWallet
|
localConfig.bchWallet = this.wallet.bchWallet
|
||||||
this.p2wdb = new P2wdbAdapter(localConfig)
|
this.p2wdb = new P2wdbAdapter(localConfig)
|
||||||
this.webhook = new Webhook()
|
this.webhook = new Webhook()
|
||||||
|
this.nostr = new NostrAdapter(this.config)
|
||||||
|
|
||||||
|
// this.wallet = new Wallet(localConfig)
|
||||||
|
|
||||||
// Get a valid JWT API key and instance bch-js.
|
// Get a valid JWT API key and instance bch-js.
|
||||||
this.fullStackJwt = new FullStackJWT(config)
|
this.fullStackJwt = new FullStackJWT(config)
|
||||||
@@ -50,14 +52,22 @@ class Adapters {
|
|||||||
|
|
||||||
async start () {
|
async start () {
|
||||||
try {
|
try {
|
||||||
|
// let apiToken
|
||||||
if (this.config.getJwtAtStartup) {
|
if (this.config.getJwtAtStartup) {
|
||||||
// Get a JWT token and instantiate bch-js with it. Then pass that instance
|
// Get a JWT token and instantiate bch-js with it. Then pass that instance
|
||||||
// to all the rest of the apps controllers and adapters.
|
// to all the rest of the apps controllers and adapters.
|
||||||
await this.fullStackJwt.getJWT()
|
// apiToken = await this.fullStackJwt.getJWT()
|
||||||
// Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js.
|
// Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js.
|
||||||
this.bchjs = await this.fullStackJwt.instanceBchjs()
|
this.bchjs = await this.fullStackJwt.instanceBchjs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a default instance of minimal-slp-wallet without initializing it
|
||||||
|
// (without retrieving the wallets UTXOs). This instance will be overwritten
|
||||||
|
// if the operator has configured BCH payments.
|
||||||
|
console.log('\nCreating default startup wallet. This wallet may be overwritten.')
|
||||||
|
// await this.wallet.instanceWalletWithoutInitialization({}, { apiToken })
|
||||||
|
this.bchjs = this.wallet.bchWallet.bchjs
|
||||||
|
|
||||||
// Start the IPFS node.
|
// Start the IPFS node.
|
||||||
// Do not start these adapters if this is an e2e test.
|
// Do not start these adapters if this is an e2e test.
|
||||||
// if (this.config.env !== 'test') {
|
// if (this.config.env !== 'test') {
|
||||||
@@ -66,6 +76,8 @@ class Adapters {
|
|||||||
|
|
||||||
// Open the wallet file
|
// Open the wallet file
|
||||||
const walletData = await this.wallet.openWallet()
|
const walletData = await this.wallet.openWallet()
|
||||||
|
await this.nostr.start(walletData.privateKey)
|
||||||
|
|
||||||
// console.log('walletData: ', walletData)
|
// console.log('walletData: ', walletData)
|
||||||
|
|
||||||
if (this.config.env !== 'test') {
|
if (this.config.env !== 'test') {
|
||||||
@@ -73,11 +85,18 @@ class Adapters {
|
|||||||
await this.wallet.instanceWallet(walletData, this.bchjs)
|
await this.wallet.instanceWallet(walletData, this.bchjs)
|
||||||
|
|
||||||
// Wait until a webhook is established with the P2WDB
|
// Wait until a webhook is established with the P2WDB
|
||||||
await this.webhook.waitUntilSuccess(this.config.webhookTarget)
|
// await this.webhook.waitUntilSuccess(this.config.webhookTarget)
|
||||||
|
|
||||||
// Overwrite instances of wallet used by P2WDB lib.
|
// Overwrite instances of wallet used by P2WDB lib.
|
||||||
this.p2wdb.bchWallet = this.wallet.bchWallet
|
this.p2wdb.bchWallet = this.wallet.bchWallet
|
||||||
this.p2wdb.bchjs = this.wallet.bchWallet.bchjs
|
this.p2wdb.bchjs = this.wallet.bchWallet.bchjs
|
||||||
|
|
||||||
|
if (this.config.useIpfs) {
|
||||||
|
await this.ipfs.start()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// These lines are here to ensure code coverage hits 100%.
|
||||||
|
console.log('Not starting IPFS node since this is an e2e test.')
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Async Adapters have been started.')
|
console.log('Async Adapters have been started.')
|
||||||
|
|||||||
@@ -58,6 +58,140 @@ class IPFS {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the status of this IPFS node.
|
||||||
|
// Create an object with different metrics about this IPFS node, then return
|
||||||
|
// the object.
|
||||||
|
getStatus () {
|
||||||
|
try {
|
||||||
|
const statusObj = {
|
||||||
|
ipfsId: this.ipfsCoordAdapter.ipfsCoord.thisNode.ipfsId,
|
||||||
|
multiAddrs: this.ipfsCoordAdapter.ipfsCoord.thisNode.ipfsMultiaddrs,
|
||||||
|
bchAddr: this.ipfsCoordAdapter.ipfsCoord.thisNode.bchAddr,
|
||||||
|
slpAddr: this.ipfsCoordAdapter.ipfsCoord.thisNode.slpAddr,
|
||||||
|
pubKey: this.ipfsCoordAdapter.ipfsCoord.thisNode.pubKey,
|
||||||
|
peers: this.ipfsCoordAdapter.ipfsCoord.thisNode.peerList.length,
|
||||||
|
relays: this.ipfsCoordAdapter.ipfsCoord.thisNode.relayData.length
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusObj
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in ipfs-coord.js/getStatus()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get details on the other peers this node is connected to.
|
||||||
|
async getPeers (showAll) {
|
||||||
|
try {
|
||||||
|
const peerData = this.ipfsCoordAdapter.ipfsCoord.thisNode.peerData
|
||||||
|
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
|
||||||
|
|
||||||
|
let ipfsPeers =
|
||||||
|
await this.ipfsCoordAdapter.ipfsCoord.adapters.ipfs.getPeers()
|
||||||
|
// console.log('ipfsPeers: ', ipfsPeers)
|
||||||
|
|
||||||
|
ipfsPeers = this._removeDuplicatePeers(ipfsPeers)
|
||||||
|
// console.log('filtered ipfsPeers: ', ipfsPeers)
|
||||||
|
|
||||||
|
const peerDataOut = []
|
||||||
|
|
||||||
|
// Loop through each IPFS peer and hydrate it with data from the peerData.
|
||||||
|
for (let i = 0; i < ipfsPeers.length; i++) {
|
||||||
|
const thisPeer = {
|
||||||
|
peer: ipfsPeers[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (!showAll) {
|
||||||
|
// // Delete properties that don't contain good info.
|
||||||
|
// delete thisPeer.muxer
|
||||||
|
// delete thisPeer.latency
|
||||||
|
// delete thisPeer.streams
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Get the ipfs-coord peer data for this peer.
|
||||||
|
let thisPeerData = peerData.filter((x) =>
|
||||||
|
x.from.includes(thisPeer.peer)
|
||||||
|
)
|
||||||
|
thisPeerData = thisPeerData[0]
|
||||||
|
// console.log('thisPeerData: ', thisPeerData)
|
||||||
|
|
||||||
|
// Skip if peerData for this IPFS peer could not be found.
|
||||||
|
// if (!thisPeerData) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
// console.log('thisPeerData: ', thisPeerData)
|
||||||
|
|
||||||
|
// Add data to the IPFS peer data.
|
||||||
|
thisPeer.name = thisPeerData.data.jsonLd.name
|
||||||
|
thisPeer.protocol = thisPeerData.data.jsonLd.protocol
|
||||||
|
thisPeer.version = thisPeerData.data.jsonLd.version
|
||||||
|
thisPeer.connectionAddr = thisPeerData.data.connectionAddr
|
||||||
|
|
||||||
|
if (showAll) {
|
||||||
|
// Add all the peer data.
|
||||||
|
thisPeer.peerData = thisPeerData
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`warning: ${thisPeer.peer}: ${err.message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerDataOut.push(thisPeer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerDataOut
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in getPeers(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expects an array of peers and returns an array of peers with duplicates
|
||||||
|
// removed.
|
||||||
|
// This function is used by getPeers()
|
||||||
|
_removeDuplicatePeers (arr) {
|
||||||
|
// https://stackoverflow.com/questions/2218999/how-to-remove-all-duplicates-from-an-array-of-objects
|
||||||
|
return arr.filter((v, i, a) => a.findIndex((t) => t === v) === i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get data about the known Circuit Relays. Hydrate with data from peers list.
|
||||||
|
getRelays () {
|
||||||
|
try {
|
||||||
|
const relayData = this.ipfsCoordAdapter.ipfsCoord.thisNode.relayData
|
||||||
|
const peerData = this.ipfsCoordAdapter.ipfsCoord.thisNode.peerData
|
||||||
|
// console.log(`relayData: ${JSON.stringify(relayData, null, 2)}`)
|
||||||
|
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
|
||||||
|
|
||||||
|
for (let i = 0; i < relayData.length; i++) {
|
||||||
|
const thisRelay = relayData[i]
|
||||||
|
|
||||||
|
// Find the peer that corresponds to this relay.
|
||||||
|
const thisPeer = peerData.filter((x) =>
|
||||||
|
x.from.includes(thisRelay.ipfsId)
|
||||||
|
)
|
||||||
|
// console.log('thisPeer: ', thisPeer)
|
||||||
|
|
||||||
|
// If the peer couldn't be found, skip.
|
||||||
|
if (!thisPeer.length) {
|
||||||
|
thisRelay.name = ''
|
||||||
|
thisRelay.description = ''
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
thisRelay.name = thisPeer[0].data.jsonLd.name
|
||||||
|
thisRelay.description = thisPeer[0].data.jsonLd.description
|
||||||
|
}
|
||||||
|
|
||||||
|
const v1Relays = this.config.v1Relays
|
||||||
|
|
||||||
|
return { v2Relays: relayData, v1Relays }
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in getRelays(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IPFS
|
export default IPFS
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Global npm libraries
|
// Global npm libraries
|
||||||
import IpfsCoord from 'ipfs-coord-esm'
|
// import IpfsCoord from 'ipfs-coord-esm'
|
||||||
|
import IpfsCoord from 'helia-coord'
|
||||||
|
|
||||||
// import BCHJS from '@psf/bch-js';
|
// import BCHJS from '@psf/bch-js';
|
||||||
import SlpWallet from 'minimal-slp-wallet/index.js'
|
import SlpWallet from 'minimal-slp-wallet'
|
||||||
import publicIp from 'public-ip'
|
import { publicIpv4 } from 'public-ip'
|
||||||
|
|
||||||
// Local libraries
|
// Local libraries
|
||||||
import config from '../../../config/index.js'
|
import config from '../../../config/index.js'
|
||||||
@@ -34,7 +35,7 @@ class IpfsCoordAdapter {
|
|||||||
// this.bchjs = new BCHJS()
|
// this.bchjs = new BCHJS()
|
||||||
this.wallet = new SlpWallet()
|
this.wallet = new SlpWallet()
|
||||||
this.config = config
|
this.config = config
|
||||||
this.publicIp = publicIp
|
this.publicIp = publicIpv4
|
||||||
|
|
||||||
// Properties of this class instance.
|
// Properties of this class instance.
|
||||||
this.isReady = false
|
this.isReady = false
|
||||||
@@ -51,7 +52,7 @@ class IpfsCoordAdapter {
|
|||||||
// If configured as a Circuit Relay, get the public IP addresses for this node.
|
// If configured as a Circuit Relay, get the public IP addresses for this node.
|
||||||
if (this.config.isCircuitRelay) {
|
if (this.config.isCircuitRelay) {
|
||||||
try {
|
try {
|
||||||
const ip4 = await this.publicIp.v4()
|
const ip4 = await this.publicIp()
|
||||||
// const ip6 = await publicIp.v6()
|
// const ip6 = await publicIp.v6()
|
||||||
|
|
||||||
circuitRelayInfo.ip4 = ip4
|
circuitRelayInfo.ip4 = ip4
|
||||||
@@ -64,23 +65,28 @@ class IpfsCoordAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nullLog = () => {}
|
||||||
|
|
||||||
const ipfsCoordOptions = {
|
const ipfsCoordOptions = {
|
||||||
ipfs: this.ipfs,
|
ipfs: this.ipfs,
|
||||||
type: 'node.js',
|
type: 'node.js',
|
||||||
// type: 'browser',
|
// type: 'browser',
|
||||||
wallet: this.wallet,
|
wallet: this.wallet,
|
||||||
privateLog: console.log, // Default to console.log
|
// privateLog: console.log, // Default to console.log
|
||||||
|
privateLog: nullLog,
|
||||||
isCircuitRelay: this.config.isCircuitRelay,
|
isCircuitRelay: this.config.isCircuitRelay,
|
||||||
circuitRelayInfo,
|
circuitRelayInfo,
|
||||||
apiInfo: this.config.apiInfo,
|
apiInfo: this.config.apiInfo,
|
||||||
announceJsonLd: this.config.announceJsonLd,
|
announceJsonLd: this.config.announceJsonLd,
|
||||||
debugLevel: this.config.debugLevel
|
debugLevel: this.config.debugLevel,
|
||||||
|
v1Relays: this.config.v1Relays,
|
||||||
|
tcpPort: this.config.ipfsTcpPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Production env uses external go-ipfs node.
|
// Production env uses external go-ipfs node.
|
||||||
if (this.config.isProduction) {
|
// if (this.config.isProduction) {
|
||||||
ipfsCoordOptions.nodeType = 'external'
|
// ipfsCoordOptions.nodeType = 'external'
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.ipfsCoord = new this.IpfsCoord(ipfsCoordOptions)
|
this.ipfsCoord = new this.IpfsCoord(ipfsCoordOptions)
|
||||||
|
|
||||||
@@ -107,9 +113,12 @@ class IpfsCoordAdapter {
|
|||||||
|
|
||||||
// Subscribe to the chat pubsub channel
|
// Subscribe to the chat pubsub channel
|
||||||
async subscribeToChat () {
|
async subscribeToChat () {
|
||||||
|
// TODO: Allow user to replace nullog with their own log handler at startup.
|
||||||
|
const nullLog = () => {}
|
||||||
|
|
||||||
await this.ipfsCoord.adapters.pubsub.subscribeToPubsubChannel(
|
await this.ipfsCoord.adapters.pubsub.subscribeToPubsubChannel(
|
||||||
this.config.chatPubSubChan,
|
this.config.chatPubSubChan,
|
||||||
console.log,
|
nullLog,
|
||||||
this.ipfsCoord.thisNode
|
this.ipfsCoord.thisNode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+254
-79
@@ -9,95 +9,98 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Global npm libraries
|
// Global npm libraries
|
||||||
// const IPFS = require('ipfs')
|
import { createHelia } from 'helia'
|
||||||
// const IPFS = require('@chris.troutner/ipfs')
|
|
||||||
// import IPFSembedded from 'ipfs';
|
|
||||||
|
|
||||||
import { create } from 'ipfs-http-client'
|
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import http from 'http'
|
import { FsBlockstore } from 'blockstore-fs'
|
||||||
|
import { FsDatastore } from 'datastore-fs'
|
||||||
|
import { createLibp2p } from 'libp2p'
|
||||||
|
import { tcp } from '@libp2p/tcp'
|
||||||
|
import { noise } from '@chainsafe/libp2p-noise'
|
||||||
|
import { yamux } from '@chainsafe/libp2p-yamux'
|
||||||
|
// import { bootstrap } from '@libp2p/bootstrap'
|
||||||
|
// import { identifyService } from 'libp2p/identify'
|
||||||
|
import { identify, identifyPush } from '@libp2p/identify'
|
||||||
|
import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
||||||
|
// import { circuitRelayServer } from '@libp2p/circuit-relay-v2'
|
||||||
|
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
|
||||||
|
import { webSockets } from '@libp2p/websockets'
|
||||||
|
import { publicIpv4 } from 'public-ip'
|
||||||
|
import { multiaddr } from '@multiformats/multiaddr'
|
||||||
|
import { webRTC, webRTCDirect } from '@libp2p/webrtc'
|
||||||
|
import { keychain } from '@libp2p/keychain'
|
||||||
|
import { unixfs } from '@helia/unixfs'
|
||||||
|
// import { generateKeyPairFromSeed } from '@libp2p/crypto/keys'
|
||||||
|
// import crypto from 'crypto'
|
||||||
|
import { ping } from '@libp2p/ping'
|
||||||
|
import { loadOrCreateSelfKey } from '@libp2p/config'
|
||||||
|
|
||||||
// Local libraries
|
// Local libraries
|
||||||
import config from '../../../config/index.js'
|
import config from '../../../config/index.js'
|
||||||
|
import JsonFiles from '../json-files.js'
|
||||||
|
|
||||||
const IPFS_DIR = './.ipfsdata/ipfs'
|
// Hack to get __dirname back.
|
||||||
|
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
|
||||||
|
import * as url from 'url'
|
||||||
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||||
|
// console.log('__dirname: ', __dirname)
|
||||||
|
|
||||||
|
const ROOT_DIR = `${__dirname}../../../`
|
||||||
|
const IPFS_DIR = `${__dirname}../../../.ipfsdata/ipfs`
|
||||||
|
|
||||||
class IpfsAdapter {
|
class IpfsAdapter {
|
||||||
constructor (localConfig) {
|
constructor (localConfig) {
|
||||||
// Encapsulate dependencies
|
// Encapsulate dependencies
|
||||||
this.config = config
|
this.config = config
|
||||||
this.fs = fs
|
this.fs = fs
|
||||||
this.create = create
|
this.createLibp2p = createLibp2p
|
||||||
|
this.publicIp = publicIpv4
|
||||||
// Choose the IPFS constructor based on the config settings.
|
this.multiaddr = multiaddr
|
||||||
// this.IPFS = IPFSembedded // default
|
this.jsonFiles = new JsonFiles()
|
||||||
// if (this.config.isProduction) {
|
this.keychain = keychain
|
||||||
// this.IPFS = IPFSexternal
|
this.createHelia = createHelia
|
||||||
// }
|
|
||||||
|
|
||||||
// Properties of this class instance.
|
// Properties of this class instance.
|
||||||
this.isReady = false
|
this.isReady = false
|
||||||
|
|
||||||
|
// Bind 'this' object to all subfunctions
|
||||||
|
this.start = this.start.bind(this)
|
||||||
|
this.createNode = this.createNode.bind(this)
|
||||||
|
this.stop = this.stop.bind(this)
|
||||||
|
this.ensureBlocksDir = this.ensureBlocksDir.bind(this)
|
||||||
|
// this.getPrivateKey = this.getPrivateKey.bind(this)
|
||||||
|
this.getSeed = this.getSeed.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start an IPFS node.
|
// Start an IPFS node.
|
||||||
async start () {
|
async start () {
|
||||||
try {
|
try {
|
||||||
// Ipfs Options
|
// Ensure the directory structure exists that is needed by the IPFS node to store data.
|
||||||
const ipfsOptionsEmbedded = {
|
this.ensureBlocksDir()
|
||||||
repo: IPFS_DIR,
|
|
||||||
start: true,
|
|
||||||
config: {
|
|
||||||
relay: {
|
|
||||||
enabled: true, // enable circuit relay dialer and listener
|
|
||||||
hop: {
|
|
||||||
enabled: config.isCircuitRelay // enable circuit relay HOP (make this node a relay)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pubsub: true, // enable pubsub
|
|
||||||
Swarm: {
|
|
||||||
ConnMgr: {
|
|
||||||
HighWater: 30,
|
|
||||||
LowWater: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Addresses: {
|
|
||||||
Swarm: [
|
|
||||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
|
|
||||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
Datastore: {
|
|
||||||
StorageMax: '2GB',
|
|
||||||
StorageGCWatermark: 50,
|
|
||||||
GCPeriod: '15m'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipfsOptionsExternal = {
|
// Create an IPFS node
|
||||||
host: this.config.ipfsHost,
|
const ipfs = await this.createNode()
|
||||||
port: this.config.ipfsApiPort,
|
// console.log('ipfs: ', ipfs)
|
||||||
agent: http.Agent({ keepAlive: true, maxSockets: 2000 })
|
|
||||||
}
|
|
||||||
|
|
||||||
let ipfsOptions = ipfsOptionsEmbedded
|
this.id = ipfs.libp2p.peerId.toString()
|
||||||
if (this.config.isProduction) {
|
console.log('IPFS ID: ', this.id)
|
||||||
ipfsOptions = ipfsOptionsExternal
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new IPFS node.
|
// Attempt to guess our ip4 IP address.
|
||||||
this.ipfs = await this.create(ipfsOptions)
|
const ip4 = await this.publicIp()
|
||||||
|
let detectedMultiaddr = `/ip4/${ip4}/tcp/${this.config.ipfsTcpPort}/p2p/${this.id}`
|
||||||
|
detectedMultiaddr = this.multiaddr(detectedMultiaddr)
|
||||||
|
|
||||||
// Set the 'server' profile so the node does not scan private networks.
|
// Get the multiaddrs for the node.
|
||||||
await this.ipfs.config.profiles.apply('server')
|
const multiaddrs = ipfs.libp2p.getMultiaddrs()
|
||||||
|
multiaddrs.push(detectedMultiaddr)
|
||||||
|
console.log('Multiaddrs: ', multiaddrs)
|
||||||
|
|
||||||
// Debugging: Display IPFS config settings.
|
this.multiaddrs = multiaddrs
|
||||||
// const configSettings = await this.ipfs.config.getAll()
|
|
||||||
// console.log(`configSettings: ${JSON.stringify(configSettings, null, 2)}`)
|
|
||||||
|
|
||||||
// Signal that this adapter is ready.
|
// Signal that this adapter is ready.
|
||||||
this.isReady = true
|
this.isReady = true
|
||||||
|
|
||||||
|
this.ipfs = ipfs
|
||||||
|
|
||||||
return this.ipfs
|
return this.ipfs
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in ipfs.js/start()')
|
console.error('Error in ipfs.js/start()')
|
||||||
@@ -111,30 +114,202 @@ class IpfsAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function creates an IPFS node using Helia.
|
||||||
|
// It returns the node as an object.
|
||||||
|
async createNode () {
|
||||||
|
try {
|
||||||
|
// Create block and data stores.
|
||||||
|
const blockstore = new FsBlockstore(`${IPFS_DIR}/blockstore`)
|
||||||
|
const datastore = new FsDatastore(`${IPFS_DIR}/datastore`)
|
||||||
|
|
||||||
|
// Get the private key.
|
||||||
|
// const privateKey = await this.getPrivateKey()
|
||||||
|
// console.log('privateKey: ', privateKey)
|
||||||
|
|
||||||
|
// Create an identity
|
||||||
|
const keychainInit = {
|
||||||
|
selfKey: 'myKey',
|
||||||
|
pass: await this.getSeed()
|
||||||
|
}
|
||||||
|
const privateKey = await loadOrCreateSelfKey(datastore, keychainInit)
|
||||||
|
|
||||||
|
// Configure services
|
||||||
|
const services = {
|
||||||
|
identify: identify(),
|
||||||
|
identifyPush: identifyPush(),
|
||||||
|
pubsub: gossipsub({ allowPublishToZeroTopicPeers: true }),
|
||||||
|
ping: ping(),
|
||||||
|
keychain: keychain(keychainInit)
|
||||||
|
}
|
||||||
|
if (this.config.isCircuitRelay) {
|
||||||
|
console.log('Helia (IPFS) node IS configured as Circuit Relay')
|
||||||
|
services.relay = circuitRelayServer({ // makes the node function as a relay server
|
||||||
|
hopTimeout: 30 * 1000, // incoming relay requests must be resolved within this time limit
|
||||||
|
reservations: {
|
||||||
|
maxReservations: 15, // how many peers are allowed to reserve relay slots on this server
|
||||||
|
reservationClearInterval: 300 * 1000, // how often to reclaim stale reservations
|
||||||
|
applyDefaultLimit: true, // whether to apply default data/duration limits to each relayed connection
|
||||||
|
defaultDurationLimit: 2 * 60 * 1000, // the default maximum amount of time a relayed connection can be open for
|
||||||
|
defaultDataLimit: BigInt(2 << 7) // the default maximum number of bytes that can be transferred over a relayed connection
|
||||||
|
},
|
||||||
|
maxInboundHopStreams: 32, // how many inbound HOP streams are allow simultaneously
|
||||||
|
maxOutboundHopStreams: 64 // how many outbound HOP streams are allow simultaneously
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('Helia (IPFS) node IS NOT configured as Circuit Relay')
|
||||||
|
}
|
||||||
|
|
||||||
|
const transports = [
|
||||||
|
tcp(),
|
||||||
|
webSockets(),
|
||||||
|
circuitRelayTransport({
|
||||||
|
discoverRelays: 3,
|
||||||
|
reservationConcurrency: 3
|
||||||
|
}),
|
||||||
|
webRTC(),
|
||||||
|
webRTCDirect()
|
||||||
|
]
|
||||||
|
|
||||||
|
// libp2p is the networking layer that underpins Helia
|
||||||
|
const libp2p = await this.createLibp2p({
|
||||||
|
// peerId,
|
||||||
|
privateKey,
|
||||||
|
datastore,
|
||||||
|
addresses: {
|
||||||
|
listen: [
|
||||||
|
'/ip4/127.0.0.1/tcp/0',
|
||||||
|
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
|
||||||
|
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`,
|
||||||
|
'/webrtc',
|
||||||
|
'/p2p-circuit'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
transports,
|
||||||
|
connectionEncrypters: [
|
||||||
|
noise()
|
||||||
|
],
|
||||||
|
streamMuxers: [
|
||||||
|
yamux()
|
||||||
|
],
|
||||||
|
services
|
||||||
|
})
|
||||||
|
|
||||||
|
// create a Helia node
|
||||||
|
const helia = await this.createHelia({
|
||||||
|
blockstore,
|
||||||
|
datastore,
|
||||||
|
libp2p
|
||||||
|
})
|
||||||
|
|
||||||
|
// Attach IPFS file system.
|
||||||
|
const fs = unixfs(helia)
|
||||||
|
helia.fs = fs
|
||||||
|
|
||||||
|
return helia
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating Helia node: ', err)
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async stop () {
|
async stop () {
|
||||||
await this.ipfs.stop()
|
await this.ipfs.stop()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the '/blocks' directory that is used to store IPFS data.
|
// Ensure that the directories exist to store blocks from the IPFS network.
|
||||||
// Dev Note: It's assumed this node is not pinning any data and that
|
// This function is called at startup, before the IPFS node is started.
|
||||||
// everything in this directory is transient. This folder will regularly
|
ensureBlocksDir () {
|
||||||
// fill up and prevent IPFS from starting.
|
try {
|
||||||
// rmBlocksDir () {
|
!this.fs.existsSync(`${ROOT_DIR}.ipfsdata`) && this.fs.mkdirSync(`${ROOT_DIR}.ipfsdata`)
|
||||||
// try {
|
|
||||||
// const dir = `${IPFS_DIR}/blocks`
|
!this.fs.existsSync(`${IPFS_DIR}`) && this.fs.mkdirSync(`${IPFS_DIR}`)
|
||||||
// console.log(`Deleting ${dir} directory...`)
|
|
||||||
//
|
!this.fs.existsSync(`${IPFS_DIR}/blockstore`) && this.fs.mkdirSync(`${IPFS_DIR}/blockstore`)
|
||||||
// this.fs.rmdirSync(dir, { recursive: true })
|
|
||||||
//
|
!this.fs.existsSync(`${IPFS_DIR}/datastore`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore`)
|
||||||
// console.log(`${dir} directory is deleted!`)
|
|
||||||
//
|
!this.fs.existsSync(`${IPFS_DIR}/datastore/pkcs8`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore/pkcs8`)
|
||||||
// return true // Signal successful execution.
|
|
||||||
// } catch (err) {
|
// !fs.existsSync(`${IPFS_DIR}/datastore/peers`) && fs.mkdirSync(`${IPFS_DIR}/datastore/peers`)
|
||||||
// console.log('Error in rmBlocksDir()')
|
|
||||||
// throw err
|
return true
|
||||||
// }
|
} catch (err) {
|
||||||
|
console.error('Error in adapters/ipfs.js/ensureBlocksDir(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function opens the seed used to generate the key for this IPFS peer.
|
||||||
|
// The seed is stored in a JSON file. If it doesn't exist, a new one is created.
|
||||||
|
async getSeed () {
|
||||||
|
try {
|
||||||
|
let seed
|
||||||
|
|
||||||
|
const filename = `${IPFS_DIR}/seed.json`
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to read the JSON file containing the seed.
|
||||||
|
seed = await this.jsonFiles.readJSON(filename)
|
||||||
|
} catch (err) {
|
||||||
|
const seedNum = Math.floor(Math.random() * 1000000000000000000000)
|
||||||
|
seed = seedNum.toString()
|
||||||
|
|
||||||
|
// Save the newly generated seed
|
||||||
|
await this.jsonFiles.writeJSON(seed, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('getSeed() seed: ', seed)
|
||||||
|
|
||||||
|
return seed
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in adapters/ipfs/ipfs.js/getSeed()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the private key from disk, or generate a new one and save it,
|
||||||
|
// if it doesn't exist.
|
||||||
|
// async getPrivateKey () {
|
||||||
|
// try {
|
||||||
|
// const filename = `${IPFS_DIR}/privkey.json`
|
||||||
|
|
||||||
|
// let privKeyHex
|
||||||
|
// try {
|
||||||
|
// // Try to read the JSON file containing the seed.
|
||||||
|
// privKeyHex = await this.jsonFiles.readJSON(filename)
|
||||||
|
// // console.log('saved privKeyHex: ', privKeyHex)
|
||||||
|
// } catch (err) {
|
||||||
|
// // Generate a new private key and save it to disk.
|
||||||
|
|
||||||
|
// // Generate a new private key and save it to disk.
|
||||||
|
// const randomBuffer = crypto.randomBytes(32)
|
||||||
|
// // console.log('randomBuffer: ', randomBuffer)
|
||||||
|
|
||||||
|
// privKeyHex = randomBuffer.toString('hex')
|
||||||
|
// // console.log('new privKeyHex: ', privKeyHex)
|
||||||
|
|
||||||
|
// // Save the newly generated seed
|
||||||
|
// await this.jsonFiles.writeJSON(privKeyHex, filename)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Convert the saved hex string to a buffer.
|
||||||
|
// const privKeyBuf = Buffer.from(privKeyHex, 'hex')
|
||||||
|
|
||||||
|
// // Convert the buffer to a Uint8Array 'seed'
|
||||||
|
// const seed = new Uint8Array(privKeyBuf.buffer, privKeyBuf.byteOffset, privKeyBuf.byteLength)
|
||||||
|
// // console.log('seed: ', seed)
|
||||||
|
|
||||||
|
// // Generate a ED25519 key pair.
|
||||||
|
// const privKey = await generateKeyPairFromSeed('Ed25519', seed)
|
||||||
|
|
||||||
|
// return privKey
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Error in adapters/ipfs/ipfs.js/getPrivateKey(): ', err)
|
||||||
|
// throw err
|
||||||
|
// }
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import Users from './models/users.js'
|
|||||||
import Entries from './models/entry.js'
|
import Entries from './models/entry.js'
|
||||||
import Order from './models/order.js'
|
import Order from './models/order.js'
|
||||||
import Offer from './models/offer.js'
|
import Offer from './models/offer.js'
|
||||||
|
import Usage from './models/usage.js'
|
||||||
|
import SmAccount from './models/smAccount.js'
|
||||||
|
import DeletedChat from './models/deletedChat.js'
|
||||||
|
import DeletedPost from './models/deletedPost.js'
|
||||||
|
import CounterOffer from './models/counter-offer.js'
|
||||||
|
|
||||||
class LocalDB {
|
class LocalDB {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -15,6 +20,11 @@ class LocalDB {
|
|||||||
this.Entry = Entries
|
this.Entry = Entries
|
||||||
this.Order = Order
|
this.Order = Order
|
||||||
this.Offer = Offer
|
this.Offer = Offer
|
||||||
|
this.Usage = Usage
|
||||||
|
this.SmAccount = SmAccount
|
||||||
|
this.DeletedChat = DeletedChat
|
||||||
|
this.DeletedPost = DeletedPost
|
||||||
|
this.CounterOffer = CounterOffer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
const CounterOffer = new mongoose.Schema({
|
||||||
|
nostrEventId: { type: String, required: true }, // Nostr Event Id.
|
||||||
|
takerAddr: { type: String, default: null },
|
||||||
|
takerNpub: { type: String, default: null },
|
||||||
|
makerNpub: { type: String, default: null },
|
||||||
|
counterOfferAddr: { type: String, default: null },
|
||||||
|
counterOfferUtxo: { type: String, default: null },
|
||||||
|
takerOfferUtxo: { type: String, default: null },
|
||||||
|
tokenId: { type: String, default: null }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mongoose.model('counterOffer', CounterOffer)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Database model for tracking deleted Nostr chat messages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
const DeletedChat = new mongoose.Schema({
|
||||||
|
eventId: { type: String, required: true, default: null }, // Event ID of the deleted message.
|
||||||
|
npub: { type: String, default: null }, // Nostr public key of the user who created the message.
|
||||||
|
bchAddr: { type: String, default: null }, // bch address of the user who created the message.
|
||||||
|
pubkey: { type: String, default: null }, // Nostr public key of the user who created the message.
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
updatedAt: { type: Date, default: Date.now }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mongoose.model('deletedChat', DeletedChat)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Database model for tracking deleted Nostr posts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
const DeletedPost = new mongoose.Schema({
|
||||||
|
eventId: { type: String, required: true, default: null }, // Event ID of the deleted post.
|
||||||
|
npub: { type: String, default: null }, // Nostr public key of the user who created the post.
|
||||||
|
bchAddr: { type: String, default: null }, // bch address of the user who created the post.
|
||||||
|
pubkey: { type: String, default: null }, // Nostr public key of the user who created the post.
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
updatedAt: { type: Date, default: Date.now }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mongoose.model('deletedPost', DeletedPost)
|
||||||
@@ -6,7 +6,7 @@ const Offer = new mongoose.Schema({
|
|||||||
tokenId: { type: String },
|
tokenId: { type: String },
|
||||||
utxoTxid: { type: String },
|
utxoTxid: { type: String },
|
||||||
utxoVout: { type: Number },
|
utxoVout: { type: Number },
|
||||||
ticker: { type: String },
|
ticker: { type: String, default: '' },
|
||||||
tokenType: { type: Number },
|
tokenType: { type: Number },
|
||||||
|
|
||||||
// Trade data
|
// Trade data
|
||||||
@@ -18,6 +18,7 @@ const Offer = new mongoose.Schema({
|
|||||||
p2wdbHash: { type: String },
|
p2wdbHash: { type: String },
|
||||||
offerStatus: { type: String },
|
offerStatus: { type: String },
|
||||||
makerAddr: { type: String },
|
makerAddr: { type: String },
|
||||||
|
makerNpub: { type: String, default: null },
|
||||||
|
|
||||||
// Authentication data
|
// Authentication data
|
||||||
signature: { type: String },
|
signature: { type: String },
|
||||||
@@ -34,8 +35,22 @@ const Offer = new mongoose.Schema({
|
|||||||
// SWaP Protocol Properties
|
// SWaP Protocol Properties
|
||||||
lokadId: { type: String },
|
lokadId: { type: String },
|
||||||
messageType: { type: Number },
|
messageType: { type: Number },
|
||||||
messageClass: { type: Number }
|
messageClass: { type: Number },
|
||||||
|
nostrEventId: { type: String }, // Nostr Event Id.
|
||||||
|
|
||||||
|
// Operator fee and address
|
||||||
|
operatorAddress: { type: String },
|
||||||
|
operatorPercentage: { type: Number },
|
||||||
|
|
||||||
|
// Token Data
|
||||||
|
immutableDataCid: { type: String, default: null },
|
||||||
|
mutableDataCid: { type: String, default: null },
|
||||||
|
tokenIconUrl: { type: String, default: null },
|
||||||
|
tokenData: { type: Object },
|
||||||
|
tokenCategories: { type: Array, default: [] },
|
||||||
|
tokenTags: { type: Array, default: [] },
|
||||||
|
userDataStr: { type: String }, // Token user data
|
||||||
|
lastUpdatedTokenData: { type: String, default: null } // ISO timestamp of the last time token data was updated.
|
||||||
})
|
})
|
||||||
|
|
||||||
export default mongoose.model('offer', Offer)
|
export default mongoose.model('offer', Offer)
|
||||||
|
|||||||
@@ -10,20 +10,21 @@ import mongoose from 'mongoose'
|
|||||||
|
|
||||||
const Order = new mongoose.Schema({
|
const Order = new mongoose.Schema({
|
||||||
// Token data
|
// Token data
|
||||||
tokenId: { type: String },
|
tokenId: { type: String, required: true },
|
||||||
utxoTxid: { type: String },
|
utxoTxid: { type: String, required: true },
|
||||||
utxoVout: { type: Number },
|
utxoVout: { type: Number, required: true },
|
||||||
ticker: { type: String },
|
ticker: { type: String, required: true },
|
||||||
tokenType: { type: Number },
|
tokenType: { type: Number, required: true },
|
||||||
|
|
||||||
// Trade data
|
// Trade data
|
||||||
buyOrSell: { type: String },
|
buyOrSell: { type: String, required: true },
|
||||||
numTokens: { type: Number },
|
numTokens: { type: Number, required: true },
|
||||||
rateInBaseUnit: { type: String },
|
rateInBaseUnit: { type: String, required: true },
|
||||||
minUnitsToExchange: { type: String },
|
minUnitsToExchange: { type: String },
|
||||||
p2wdbTxid: { type: String },
|
p2wdbTxid: { type: String },
|
||||||
p2wdbHash: { type: String },
|
p2wdbHash: { type: String },
|
||||||
makerAddr: { type: String },
|
makerAddr: { type: String, required: true },
|
||||||
|
makerNpub: { type: String, required: true },
|
||||||
|
|
||||||
// Authentication data
|
// Authentication data
|
||||||
signature: { type: String },
|
signature: { type: String },
|
||||||
@@ -32,12 +33,22 @@ const Order = new mongoose.Schema({
|
|||||||
offerPubKey: { type: String },
|
offerPubKey: { type: String },
|
||||||
|
|
||||||
// Wallet Data
|
// Wallet Data
|
||||||
hdIndex: { type: Number }, // HD index address holding the UTXO for this offer.
|
hdIndex: { type: Number, required: true }, // HD index address holding the UTXO for this offer.
|
||||||
|
|
||||||
// SWaP Protocol Properties
|
// SWaP Protocol Properties
|
||||||
lokadId: { type: String },
|
lokadId: { type: String },
|
||||||
messageType: { type: Number },
|
messageType: { type: Number },
|
||||||
messageClass: { type: Number }
|
messageClass: { type: Number },
|
||||||
|
nostrEventId: { type: String, required: true }, // Nostr Event Id.
|
||||||
|
|
||||||
|
// Additional properties found in createOrder
|
||||||
|
dataType: { type: String, required: true },
|
||||||
|
|
||||||
|
userId: { type: String, required: true },
|
||||||
|
|
||||||
|
// Operator fee and address
|
||||||
|
operatorAddress: { type: String },
|
||||||
|
operatorPercentage: { type: Number }
|
||||||
})
|
})
|
||||||
|
|
||||||
export default mongoose.model('order', Order)
|
export default mongoose.model('order', Order)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Social Media Account Model.
|
||||||
|
|
||||||
|
This model is used to track content creators using the DEX
|
||||||
|
Nostr social media channel.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
const SmAccount = new mongoose.Schema({
|
||||||
|
npub: { type: String, required: true },
|
||||||
|
bchAddr: { type: String, required: true, default: '' },
|
||||||
|
pubkey: { type: String, required: true },
|
||||||
|
pfp: { type: String, default: '' },
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
updatedAt: { type: Date, default: Date.now },
|
||||||
|
followerCnt: { type: Number, default: 0 },
|
||||||
|
followingCnt: { type: Number, default: 0 },
|
||||||
|
posts: { type: Number, default: 0 },
|
||||||
|
likes: { type: Number, default: 0 },
|
||||||
|
comments: { type: Number, default: 0 },
|
||||||
|
reposts: { type: Number, default: 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mongoose.model('smAccount', SmAccount)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
Usage data model
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Global npm libraries
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
const Usage = new mongoose.Schema({
|
||||||
|
ip: { type: String },
|
||||||
|
url: { type: String },
|
||||||
|
method: { type: String },
|
||||||
|
timestamp: { type: Date }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mongoose.model('usage', Usage)
|
||||||
@@ -11,6 +11,7 @@ const User = new mongoose.Schema({
|
|||||||
name: { type: String },
|
name: { type: String },
|
||||||
username: { type: String },
|
username: { type: String },
|
||||||
password: { type: String, required: true },
|
password: { type: String, required: true },
|
||||||
|
mnemonic: { type: String, required: true },
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
/*
|
||||||
|
Adapter library for working with Nostr.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Global npm libraries
|
||||||
|
import BchNostr from 'bch-nostr'
|
||||||
|
import { RelayPool } from 'nostr'
|
||||||
|
import RetryQueue from '@chris.troutner/retry-queue'
|
||||||
|
import * as nip19 from 'nostr-tools/nip19'
|
||||||
|
import { base58_to_binary as base58Tobinary } from 'base58-js'
|
||||||
|
import { getPublicKey } from 'nostr-tools/pure'
|
||||||
|
|
||||||
|
class NostrAdapter {
|
||||||
|
constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) {
|
||||||
|
this.relayWs = localConfig.nostrRelay //
|
||||||
|
this.topic = localConfig.nostrTopic
|
||||||
|
this.globalFeed = localConfig.nostrGlobalFeed
|
||||||
|
|
||||||
|
if (!this.relayWs) {
|
||||||
|
throw new Error(
|
||||||
|
'"nostrRelay" must be passed when instantiate NostrAdapter'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.topic) {
|
||||||
|
throw new Error(
|
||||||
|
'"nostrTopic" must be passed when instantiate NostrAdapter'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate dependencies
|
||||||
|
this.bchNostr = new BchNostr()
|
||||||
|
this.RelayPool = RelayPool
|
||||||
|
this.retryQueue = new RetryQueue({
|
||||||
|
concurrency: 1,
|
||||||
|
attempts: 5,
|
||||||
|
retryPeriod: 1000
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bind the 'this' object
|
||||||
|
this.start = this.start.bind(this)
|
||||||
|
this.post = this.post.bind(this)
|
||||||
|
this.read = this.read.bind(this)
|
||||||
|
this.eventId2note = this.eventId2note.bind(this)
|
||||||
|
this.pubkey2npub = this.pubkey2npub.bind(this)
|
||||||
|
this.npub2pubkey = this.npub2pubkey.bind(this)
|
||||||
|
this.readGlobalFeed = this.readGlobalFeed.bind(this)
|
||||||
|
this.getFollowers = this.getFollowers.bind(this)
|
||||||
|
this.privKeyToNpub = this.privKeyToNpub.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create nostr keys.
|
||||||
|
// NOTE: WIF must be provided in this function instead the constructor function
|
||||||
|
// we can provide it after app wallet creation.
|
||||||
|
async start (WIF) {
|
||||||
|
try {
|
||||||
|
if (!WIF || typeof WIF !== 'string') {
|
||||||
|
throw new Error('WIF must be a string!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { privKeyBuf, nostrPubKey } =
|
||||||
|
this.bchNostr.keys.createNostrPubKeyFromWif({ wif: WIF })
|
||||||
|
|
||||||
|
this.privKeyBuf = privKeyBuf
|
||||||
|
this.nostrPubKey = nostrPubKey
|
||||||
|
|
||||||
|
console.log(`Nostr Pub Key : ${nostrPubKey}`)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error in nostr.js/start() ${error.message} `)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post a message over the instance-topic.
|
||||||
|
async post (msg = '') {
|
||||||
|
try {
|
||||||
|
if (!msg || typeof msg !== 'string') {
|
||||||
|
throw new Error('msg must be a string!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const inObj = {
|
||||||
|
kind: 867,
|
||||||
|
privKeyBuf: this.privKeyBuf,
|
||||||
|
nostrPubKey: this.nostrPubKey,
|
||||||
|
relayWs: this.relayWs,
|
||||||
|
msg,
|
||||||
|
tags: [['t', this.topic]]
|
||||||
|
}
|
||||||
|
const eventId = await this.retryQueue.addToQueue(this.bchNostr.post.uploadToNostr, inObj)
|
||||||
|
return eventId
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error in nostr.js/post() ${error.message} `)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a message over the instance-topic.
|
||||||
|
async read (limit = 10) {
|
||||||
|
try {
|
||||||
|
if (typeof limit !== 'number' || limit < 1) {
|
||||||
|
throw new Error('Limit must be greater than 0')
|
||||||
|
}
|
||||||
|
|
||||||
|
const relays = [this.relayWs]
|
||||||
|
const pool = this.RelayPool(relays)
|
||||||
|
|
||||||
|
const nostrData = new Promise((resolve, reject) => {
|
||||||
|
const messages = []
|
||||||
|
|
||||||
|
pool.on('open', (relay) => {
|
||||||
|
// relay.subscribe('REQ', { ids: [eventId] })
|
||||||
|
relay.subscribe('REQ', { limit, kinds: [867], '#t': [this.topic] })
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('eose', (relay) => {
|
||||||
|
relay.close()
|
||||||
|
resolve(messages)
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('event', (relay, subId, ev) => {
|
||||||
|
// console.log('ev: ', ev)
|
||||||
|
messages.push({ content: ev.content, eventId: ev.id })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const messages = await nostrData
|
||||||
|
|
||||||
|
return messages
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error in nostr.js/read() ${error.message} `)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert an Event ID into a `noteabc..` syntax that Astral expects.
|
||||||
|
// This can be used to generate a link to Astral to display the post.
|
||||||
|
eventId2note (eventId) {
|
||||||
|
return nip19.noteEncode(eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a pubkey into a `npubabc..` syntax that Astral expects.
|
||||||
|
pubkey2npub (pubkey) {
|
||||||
|
return nip19.npubEncode(pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
npub2pubkey (npub) {
|
||||||
|
return nip19.decode(npub)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the global feed.
|
||||||
|
async readGlobalFeed (inObj = {}) {
|
||||||
|
try {
|
||||||
|
const { limit = 10 } = inObj
|
||||||
|
|
||||||
|
if (typeof limit !== 'number' || limit < 1) {
|
||||||
|
throw new Error('Limit must be greater than 0')
|
||||||
|
}
|
||||||
|
|
||||||
|
const relays = [this.relayWs]
|
||||||
|
const pool = this.RelayPool(relays)
|
||||||
|
|
||||||
|
const nostrData = new Promise((resolve, reject) => {
|
||||||
|
const messages = []
|
||||||
|
|
||||||
|
pool.on('open', (relay) => {
|
||||||
|
// relay.subscribe('REQ', { ids: [eventId] })
|
||||||
|
relay.subscribe('REQ', { limit, kinds: [1], '#t': [this.globalFeed] })
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('eose', (relay) => {
|
||||||
|
relay.close()
|
||||||
|
resolve(messages)
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('event', (relay, subId, ev) => {
|
||||||
|
// console.log('ev: ', ev)
|
||||||
|
|
||||||
|
const { content, id, tags, pubkey } = ev
|
||||||
|
|
||||||
|
const msg = {
|
||||||
|
content,
|
||||||
|
id,
|
||||||
|
tags,
|
||||||
|
npub: this.pubkey2npub(pubkey),
|
||||||
|
pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.push(msg)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const messages = await nostrData
|
||||||
|
|
||||||
|
return messages
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error in nostr.js/readGlobalFeed() ${error.message} `)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFollowers (inObj = {}) {
|
||||||
|
try {
|
||||||
|
const { pubkey } = inObj
|
||||||
|
|
||||||
|
if (!pubkey || typeof pubkey !== 'string') {
|
||||||
|
throw new Error('pubkey must be a string!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const relays = [this.relayWs]
|
||||||
|
const pool = this.RelayPool(relays)
|
||||||
|
|
||||||
|
const nostrData = new Promise((resolve, reject) => {
|
||||||
|
const followers = []
|
||||||
|
|
||||||
|
pool.on('open', (relay) => {
|
||||||
|
// Query for kind 3 events where the target pubkey appears in tags
|
||||||
|
// This finds all contact lists that include the target pubkey
|
||||||
|
relay.subscribe('subid', {
|
||||||
|
limit: 100,
|
||||||
|
kinds: [3],
|
||||||
|
'#p': [pubkey] // Filter by tag 'p' containing the target pubkey
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('eose', relay => {
|
||||||
|
relay.close()
|
||||||
|
resolve(followers)
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('event', (relay, subId, ev) => {
|
||||||
|
// Each event represents a contact list from someone who follows the target
|
||||||
|
// The author of this event is a follower
|
||||||
|
const follower = {
|
||||||
|
pubkey: ev.pubkey,
|
||||||
|
npub: this.pubkey2npub(ev.pubkey),
|
||||||
|
contactList: ev.tags,
|
||||||
|
createdAt: ev.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
followers.push(follower)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const followers = await nostrData
|
||||||
|
return followers
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error in nostr.js/getFollowers() ${error.message} `)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a BCH private key (WIF) to a Nostr npub.
|
||||||
|
privKeyToNpub (privKey) {
|
||||||
|
try {
|
||||||
|
if (!privKey || typeof privKey !== 'string') {
|
||||||
|
throw new Error('privKey must be a string!')
|
||||||
|
}
|
||||||
|
// Extract the privaty key from the WIF, using this guide:
|
||||||
|
// https://learnmeabitcoin.com/technical/keys/private-key/wif/
|
||||||
|
const wifBuf = base58Tobinary(privKey)
|
||||||
|
const privBuf = wifBuf.slice(1, 33)
|
||||||
|
// console.log('privBuf: ', privBuf)
|
||||||
|
|
||||||
|
const nostrPubKey = getPublicKey(privBuf)
|
||||||
|
// console.log('nostrPubKey: ', nostrPubKey)
|
||||||
|
// Convert pubkey to npub
|
||||||
|
const nostrNpub = nip19.npubEncode(nostrPubKey)
|
||||||
|
// console.log('nostrNpub: ', nostrNpub)
|
||||||
|
|
||||||
|
return nostrNpub
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error in nostr.js/privKeyToNpub() ${error.message} `)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NostrAdapter
|
||||||
@@ -5,10 +5,11 @@
|
|||||||
// Public npm libraries.
|
// Public npm libraries.
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import { Write, Read } from 'p2wdb/index.js'
|
import P2WDB from 'p2wdb-esm'
|
||||||
|
|
||||||
// Local libraries
|
// Local libraries
|
||||||
import config from '../../config/index.js'
|
import config from '../../config/index.js'
|
||||||
|
const { Write, Read } = P2WDB
|
||||||
|
|
||||||
class P2wdbAdapter {
|
class P2wdbAdapter {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
/*
|
||||||
|
Wallet adapter. This library instantiates minimal-slp-wallet based on different
|
||||||
|
configuration settings. That single instance of minimal-slp-wallet is then
|
||||||
|
easily passed around the rest of this application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Global npm libraries
|
||||||
|
import BchWallet from 'minimal-slp-wallet'
|
||||||
|
|
||||||
|
// Local libraries
|
||||||
|
import config from '../../config/index.js'
|
||||||
|
import JsonFiles from './json-files.js'
|
||||||
|
|
||||||
|
class Wallet {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// Encapsulate dependencies
|
||||||
|
this.config = config
|
||||||
|
this.BchWallet = BchWallet
|
||||||
|
this.jsonFiles = new JsonFiles()
|
||||||
|
|
||||||
|
// Bind 'this' object to all subfunctions
|
||||||
|
this.instanceWalletWithoutInitialization = this.instanceWalletWithoutInitialization.bind(this)
|
||||||
|
this._instanceWallet = this._instanceWallet.bind(this)
|
||||||
|
this.openWallet = this.openWallet.bind(this)
|
||||||
|
this.instanceWallet = this.instanceWallet.bind(this)
|
||||||
|
this.incrementNextAddress = this.incrementNextAddress.bind(this)
|
||||||
|
this.getKeyPair = this.getKeyPair.bind(this)
|
||||||
|
this.optimize = this.optimize.bind(this)
|
||||||
|
this.getBalance = this.getBalance.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used for initializing the wallet, without waiting to update the wallet
|
||||||
|
// UTXOs from the blockchain.
|
||||||
|
// This is useful when the wallet is simply needed to make calls to the blockchain,
|
||||||
|
// and there is no need to hydrate it with UTXO data.
|
||||||
|
async instanceWalletWithoutInitialization (walletData = {}, advancedConfig = {}) {
|
||||||
|
try {
|
||||||
|
// Use the apiToken from the config settings, if one is not passed-in at
|
||||||
|
// run-time.
|
||||||
|
if (!advancedConfig.apiToken) {
|
||||||
|
advancedConfig.apiToken = this.config.apiToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an authentication password is passed in the config, add it.
|
||||||
|
if (this.config.authPass) {
|
||||||
|
advancedConfig.authPass = this.config.authPass
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect and configure different blockchain infrastructure settings.
|
||||||
|
if (this.config.walletInterface === 'web2') {
|
||||||
|
advancedConfig.interface = 'rest-api'
|
||||||
|
advancedConfig.restURL = this.config.apiServer
|
||||||
|
} else {
|
||||||
|
// By default use the web3 Cash Stack (https://cashstack.info)
|
||||||
|
advancedConfig.interface = 'consumer-api'
|
||||||
|
advancedConfig.restURL = this.config.apiServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('advancedConfig setting when creating wallet: ', advancedConfig)
|
||||||
|
|
||||||
|
// Instantiate minimal-slp-wallet.
|
||||||
|
if (walletData.mnemonic) {
|
||||||
|
// Instance the wallet using the mnemonic passed in to this function.
|
||||||
|
console.log('Mnemonic provided to wallet library from calling function.')
|
||||||
|
this.bchWallet = await this._instanceWallet(walletData.mnemonic, advancedConfig)
|
||||||
|
} else if (this.config.mnemonic) {
|
||||||
|
// Otherwise use the mnemonic in the config setting or passed as an environment variable.
|
||||||
|
console.log('Mnemonic retrieved from config file or environment variable.')
|
||||||
|
this.bchWallet = await this._instanceWallet(this.config.mnemonic, advancedConfig)
|
||||||
|
} else {
|
||||||
|
// If no mnemonic is provided, then generate a new mnemonic to create the wallet.
|
||||||
|
console.log('New mnemonic generated.')
|
||||||
|
this.bchWallet = await this._instanceWallet(undefined, advancedConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for wallet to initialize.
|
||||||
|
await this.bchWallet.walletInfoPromise
|
||||||
|
console.log('BCH wallet initialized.')
|
||||||
|
console.log(`Wallet address: ${this.bchWallet.walletInfo.cashAddress}`)
|
||||||
|
console.log(`Wallet mnemonic: ${this.bchWallet.walletInfo.mnemonic}`)
|
||||||
|
|
||||||
|
return this.bchWallet
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in instanceWalletWithoutInitialization()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is used for easier mocking of unit tests.
|
||||||
|
async _instanceWallet (mnemonic, config) {
|
||||||
|
const wallet = new this.BchWallet(mnemonic, config)
|
||||||
|
await wallet.walletInfoPromise
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the wallet file, or create one if the file doesn't exist.
|
||||||
|
// Does not instance the wallet. The output of this function is expected to
|
||||||
|
// be passed to instanceWallet().
|
||||||
|
async openWallet (advancedConfig = {}) {
|
||||||
|
try {
|
||||||
|
let walletData
|
||||||
|
|
||||||
|
// Try to open the wallet.json file.
|
||||||
|
try {
|
||||||
|
walletData = await this.jsonFiles.readJSON(this.config.walletFile)
|
||||||
|
walletData = walletData.wallet
|
||||||
|
} catch (err) {
|
||||||
|
// Create a new wallet file if one does not already exist.
|
||||||
|
console.log('Wallet file not found. Creating new wallet.json file.')
|
||||||
|
|
||||||
|
// Create a new wallet.
|
||||||
|
// No-Update flag creates wallet without making any network calls.
|
||||||
|
const walletInstance = await this.instanceWalletWithoutInitialization({}, advancedConfig)
|
||||||
|
|
||||||
|
walletData = walletInstance.walletInfo
|
||||||
|
|
||||||
|
// Add the nextAddress property
|
||||||
|
walletData.nextAddress = 1
|
||||||
|
|
||||||
|
// Write the wallet data to the JSON file.
|
||||||
|
await this.jsonFiles.writeJSON({ wallet: walletData }, this.config.walletFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('walletData: ', walletData)
|
||||||
|
return walletData
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in openWallet()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an instance of minimal-slp-wallet. Same as
|
||||||
|
// instanceWalletWithoutInitialization(), but waits for the wallet to initialize
|
||||||
|
// its UTXOs (wallet balance and tokens).
|
||||||
|
async instanceWallet (walletData = {}, advancedConfig = {}) {
|
||||||
|
try {
|
||||||
|
// Instance the wallet without initialization.
|
||||||
|
await this.instanceWalletWithoutInitialization(walletData, advancedConfig)
|
||||||
|
|
||||||
|
// Initialize the wallet
|
||||||
|
await this.bchWallet.initialize()
|
||||||
|
|
||||||
|
return this.bchWallet
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in wallet.js/instanceWallet()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increments the 'nextAddress' property in the wallet file. This property
|
||||||
|
// indicates the HD index that should be used to generate a key pair for
|
||||||
|
// storing funds for Offers.
|
||||||
|
// This function opens the wallet file, increments the nextAddress property,
|
||||||
|
// then saves the change to the wallet file.
|
||||||
|
async incrementNextAddress () {
|
||||||
|
try {
|
||||||
|
const walletData = await this.openWallet()
|
||||||
|
|
||||||
|
await this.instanceWalletWithoutInitialization(walletData)
|
||||||
|
|
||||||
|
walletData.nextAddress++
|
||||||
|
// console.log('walletData finish: ', walletData)
|
||||||
|
|
||||||
|
await this.jsonFiles.writeJSON({ wallet: walletData }, this.config.walletFile)
|
||||||
|
|
||||||
|
// Update the working instance of the wallet.
|
||||||
|
this.bchWallet.walletInfo.nextAddress++
|
||||||
|
// console.log('this.bchWallet.walletInfo: ', this.bchWallet.walletInfo)
|
||||||
|
|
||||||
|
return walletData.nextAddress
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in incrementNextAddress()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method returns an object that contains a private key WIF, public address,
|
||||||
|
// and the index of the HD wallet that the key pair was generated from.
|
||||||
|
// TODO: Allow input integer. If input is used, use that as the index. If no
|
||||||
|
// input is provided, then call incrementNextAddress().
|
||||||
|
async getKeyPair (hdIndex = 0) {
|
||||||
|
try {
|
||||||
|
if (!hdIndex) {
|
||||||
|
// Increment the HD index and generate a new key pair.
|
||||||
|
hdIndex = await this.incrementNextAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
const mnemonic = this.bchWallet.walletInfo.mnemonic
|
||||||
|
|
||||||
|
// root seed buffer
|
||||||
|
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
|
||||||
|
const masterHDNode = this.bchWallet.bchjs.HDNode.fromSeed(rootSeed)
|
||||||
|
|
||||||
|
// HDNode of BIP44 account
|
||||||
|
// const account = this.bchWallet.bchjs.HDNode.derivePath(masterHDNode, "m/44'/245'/0'")
|
||||||
|
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
||||||
|
const cashAddress = this.bchWallet.bchjs.HDNode.toCashAddress(childNode)
|
||||||
|
console.log('Generating a new key pair for cashAddress: ', cashAddress)
|
||||||
|
|
||||||
|
const wif = this.bchWallet.bchjs.HDNode.toWIF(childNode)
|
||||||
|
|
||||||
|
const outObj = {
|
||||||
|
cashAddress,
|
||||||
|
wif,
|
||||||
|
hdIndex
|
||||||
|
}
|
||||||
|
return outObj
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in getKeyPair()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize the wallet by consolidating the UTXOs.
|
||||||
|
async optimize () {
|
||||||
|
const UTXO_THREASHOLD = 7
|
||||||
|
// Do a dry-run first to see if there are enough UTXOs worth consolidating.
|
||||||
|
const dryRunOut = await this.bchWallet.optimize(true)
|
||||||
|
if (dryRunOut.bchUtxoCnt > UTXO_THREASHOLD) {
|
||||||
|
// Consolidate BCH UTXOs if the count is above the threashold.
|
||||||
|
const txids = await this.bchWallet.optimize()
|
||||||
|
console.log(`Wallet optimized with these return values: ${JSON.stringify(txids, null, 2)}`)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the balance of the wallet in sats and PSF tokens.
|
||||||
|
// This function is called by the GET /entry/balance controller.
|
||||||
|
async getBalance () {
|
||||||
|
const balance = await this.bchWallet.getBalance()
|
||||||
|
// console.log('balance: ', balance)
|
||||||
|
const tokens = await this.bchWallet.listTokens()
|
||||||
|
// console.log('tokens: ', tokens)
|
||||||
|
// Find the array entry for the PSF token
|
||||||
|
const psfTokens = tokens.find(x => x.tokenId === '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0')
|
||||||
|
// console.log('psfTokens: ', psfTokens)
|
||||||
|
let psfBalance = 0
|
||||||
|
if (psfTokens) {
|
||||||
|
psfBalance = psfTokens.qty
|
||||||
|
}
|
||||||
|
const outObj = {
|
||||||
|
satBalance: balance,
|
||||||
|
psfBalance,
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
return outObj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Wallet
|
||||||
+199
-13
@@ -1,15 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
Adapter library for working with a wallet.
|
Adapter library for working with a wallet.
|
||||||
|
This library is specific to bch-dex. The other wallet adapter library is
|
||||||
|
inherited from ipfs-service-provider.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Public npm libraries
|
// Public npm libraries
|
||||||
import BchWallet from 'minimal-slp-wallet/index.js'
|
import BchWallet from 'minimal-slp-wallet'
|
||||||
|
import BchTokenSweep from 'bch-token-sweep/index.js'
|
||||||
import bitcoinJs from 'bitcoincashjs-lib'
|
import bitcoinJs from 'bitcoincashjs-lib'
|
||||||
|
import RetryQueue from '@chris.troutner/retry-queue'
|
||||||
|
|
||||||
// Local libraries
|
// Local libraries
|
||||||
import JsonFiles from './json-files.js'
|
import JsonFiles from './json-files.js'
|
||||||
|
|
||||||
import config from '../../config/index.js'
|
import config from '../../config/index.js'
|
||||||
|
|
||||||
// Hack to get __dirname back.
|
// Hack to get __dirname back.
|
||||||
@@ -22,6 +24,8 @@ const WALLET_FILE = `${__dirname.toString()}/../../wallet.json`
|
|||||||
// const P2WDB_TOKEN_ID =
|
// const P2WDB_TOKEN_ID =
|
||||||
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
|
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
|
||||||
|
|
||||||
|
const advancedConfig = {}
|
||||||
|
|
||||||
class WalletAdapter {
|
class WalletAdapter {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
// Encapsulate dependencies
|
// Encapsulate dependencies
|
||||||
@@ -30,10 +34,29 @@ class WalletAdapter {
|
|||||||
this.config = config
|
this.config = config
|
||||||
this.bitcoinJs = bitcoinJs
|
this.bitcoinJs = bitcoinJs
|
||||||
this.BchWallet = BchWallet
|
this.BchWallet = BchWallet
|
||||||
|
this.BchTokenSweep = BchTokenSweep
|
||||||
this.bchWallet = {} // Will be replaced when initialized.
|
this.bchWallet = {} // Will be replaced when initialized.
|
||||||
|
// this.advancedConfig = localConfig.advancedConfig
|
||||||
|
this.retryQueue = new RetryQueue({
|
||||||
|
concurrency: 1,
|
||||||
|
attempts: 5,
|
||||||
|
retryPeriod: 5000
|
||||||
|
})
|
||||||
|
|
||||||
// Bind the 'this' object
|
// Bind the 'this' object
|
||||||
|
this.openWallet = this.openWallet.bind(this)
|
||||||
|
this.instanceWallet = this.instanceWallet.bind(this)
|
||||||
|
this.incrementNextAddress = this.incrementNextAddress.bind(this)
|
||||||
|
this.getKeyPair = this.getKeyPair.bind(this)
|
||||||
|
this.generateSignature = this.generateSignature.bind(this)
|
||||||
|
this.generatePartialTx = this.generatePartialTx.bind(this)
|
||||||
this.moveTokens = this.moveTokens.bind(this)
|
this.moveTokens = this.moveTokens.bind(this)
|
||||||
|
this.moveBch = this.moveBch.bind(this)
|
||||||
|
this.deseralizeTx = this.deseralizeTx.bind(this)
|
||||||
|
this.completeTx = this.completeTx.bind(this)
|
||||||
|
this.reclaimTokens = this.reclaimTokens.bind(this)
|
||||||
|
this.moveTokensFromCustomWallet = this.moveTokensFromCustomWallet.bind(this)
|
||||||
|
this.cid2json = this.cid2json.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the wallet file, or create one if the file doesn't exist.
|
// Open the wallet file, or create one if the file doesn't exist.
|
||||||
@@ -87,7 +110,8 @@ class WalletAdapter {
|
|||||||
throw new Error('Wallet data is not formatted correctly. Can not read mnemonic in wallet file!')
|
throw new Error('Wallet data is not formatted correctly. Can not read mnemonic in wallet file!')
|
||||||
}
|
}
|
||||||
|
|
||||||
const advancedConfig = {}
|
// const advancedConfig = {}
|
||||||
|
console.log(`Using FullStack.cash: ${this.config.useFullStackCash}`)
|
||||||
if (this.config.useFullStackCash) {
|
if (this.config.useFullStackCash) {
|
||||||
advancedConfig.interface = 'rest-api'
|
advancedConfig.interface = 'rest-api'
|
||||||
advancedConfig.restURL = this.config.apiServer
|
advancedConfig.restURL = this.config.apiServer
|
||||||
@@ -139,10 +163,9 @@ class WalletAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: This method uses the system wallet, not the user or admin wallet.
|
||||||
// This method returns an object that contains a private key WIF, public address,
|
// This method returns an object that contains a private key WIF, public address,
|
||||||
// and the index of the HD wallet that the key pair was generated from.
|
// and the index of the HD wallet that the key pair was generated from.
|
||||||
// TODO: Allow input integer. If input is used, use that as the index. If no
|
|
||||||
// input is provided, then call incrementNextAddress().
|
|
||||||
async getKeyPair (hdIndex = 0) {
|
async getKeyPair (hdIndex = 0) {
|
||||||
try {
|
try {
|
||||||
if (!hdIndex) {
|
if (!hdIndex) {
|
||||||
@@ -151,6 +174,7 @@ class WalletAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mnemonic = this.bchWallet.walletInfo.mnemonic
|
const mnemonic = this.bchWallet.walletInfo.mnemonic
|
||||||
|
console.log(`getKeyPair() for HD index: ${hdIndex} called on mnemonic: ${mnemonic}`)
|
||||||
|
|
||||||
// root seed buffer
|
// root seed buffer
|
||||||
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
|
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
|
||||||
@@ -183,7 +207,9 @@ class WalletAdapter {
|
|||||||
// Generate a cryptographic signature, required to write to the P2WDB.
|
// Generate a cryptographic signature, required to write to the P2WDB.
|
||||||
async generateSignature (message) {
|
async generateSignature (message) {
|
||||||
try {
|
try {
|
||||||
// TODO: Add input validation for message.
|
if (!message || typeof message !== 'string') {
|
||||||
|
throw new Error('message is required!')
|
||||||
|
}
|
||||||
|
|
||||||
const privKey = this.bchWallet.walletInfo.privateKey
|
const privKey = this.bchWallet.walletInfo.privateKey
|
||||||
|
|
||||||
@@ -340,7 +366,7 @@ class WalletAdapter {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
// return true
|
// return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move tokens to an address controlled by the HD wallet, to generate a
|
// Move tokens to an address controlled by the HD wallet, to generate a
|
||||||
@@ -348,6 +374,12 @@ class WalletAdapter {
|
|||||||
async moveTokens (inObj = {}) {
|
async moveTokens (inObj = {}) {
|
||||||
try {
|
try {
|
||||||
const { tokenId, qty } = inObj
|
const { tokenId, qty } = inObj
|
||||||
|
if (!tokenId || typeof tokenId !== 'string') {
|
||||||
|
throw new Error('tokenId must be a string!')
|
||||||
|
}
|
||||||
|
if (!qty || typeof qty !== 'number') {
|
||||||
|
throw new Error('qty must be a number!')
|
||||||
|
}
|
||||||
|
|
||||||
const keyPair = await this.getKeyPair()
|
const keyPair = await this.getKeyPair()
|
||||||
console.log('keyPair: ', keyPair)
|
console.log('keyPair: ', keyPair)
|
||||||
@@ -370,6 +402,10 @@ class WalletAdapter {
|
|||||||
x => x.tokenId === tokenId
|
x => x.tokenId === tokenId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (tokenUtxos.length === 0) {
|
||||||
|
throw new Error('tokenId not found!')
|
||||||
|
}
|
||||||
|
|
||||||
const txid = await this.bchWallet.sendTokens(receiver, 3)
|
const txid = await this.bchWallet.sendTokens(receiver, 3)
|
||||||
|
|
||||||
const utxoInfo = {
|
const utxoInfo = {
|
||||||
@@ -390,6 +426,10 @@ class WalletAdapter {
|
|||||||
// generate a segregated UTXO.
|
// generate a segregated UTXO.
|
||||||
async moveBch (amountSat) {
|
async moveBch (amountSat) {
|
||||||
try {
|
try {
|
||||||
|
if (!amountSat) {
|
||||||
|
throw new Error('amountSat is required!')
|
||||||
|
}
|
||||||
|
|
||||||
const keyPair = await this.getKeyPair()
|
const keyPair = await this.getKeyPair()
|
||||||
console.log('keyPair: ', keyPair)
|
console.log('keyPair: ', keyPair)
|
||||||
|
|
||||||
@@ -422,6 +462,10 @@ class WalletAdapter {
|
|||||||
// representing the transaction.
|
// representing the transaction.
|
||||||
async deseralizeTx (txHex) {
|
async deseralizeTx (txHex) {
|
||||||
try {
|
try {
|
||||||
|
// Input validation
|
||||||
|
if (!txHex || typeof txHex !== 'string') {
|
||||||
|
throw new Error('hex must be a string!')
|
||||||
|
}
|
||||||
// Ensure the URL points at FullStack.cash, since the web 3 infra does not
|
// Ensure the URL points at FullStack.cash, since the web 3 infra does not
|
||||||
// yet support this call.
|
// yet support this call.
|
||||||
const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL
|
const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL
|
||||||
@@ -436,18 +480,33 @@ class WalletAdapter {
|
|||||||
|
|
||||||
return txObj2
|
return txObj2
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in wallet.js/deserializePartialTx()')
|
console.error('Error in wallet.js/deserializeTx()')
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete the partially signed transaction by signing the first input,
|
// Complete the partially signed transaction by signing the first input,
|
||||||
// then broadcasting the transaction to the network.
|
// then broadcasting the transaction to the network.
|
||||||
async completeTx (hex, hdIndex) {
|
async completeTx (hex, hdIndex, mnemonic) {
|
||||||
try {
|
try {
|
||||||
|
// Input validation
|
||||||
|
if (!hex || typeof hex !== 'string') {
|
||||||
|
throw new Error('hex must be a string!')
|
||||||
|
}
|
||||||
|
if (typeof hdIndex !== 'number' || hdIndex < 0) {
|
||||||
|
throw new Error('hdIndex must be a non-negative number!')
|
||||||
|
}
|
||||||
|
if (!mnemonic || typeof mnemonic !== 'string') {
|
||||||
|
throw new Error('mnemonic must be a string!')
|
||||||
|
}
|
||||||
// console.log('hex: ', hex)
|
// console.log('hex: ', hex)
|
||||||
|
console.log('completeTx() hdIndex: ', hdIndex)
|
||||||
|
console.log('completeTx() mnemonic: ', mnemonic)
|
||||||
|
|
||||||
const bchjs = this.bchWallet.bchjs
|
// Instantiate the user wallet
|
||||||
|
const userWallet = await this.instanceWallet({ mnemonic })
|
||||||
|
|
||||||
|
const bchjs = userWallet.bchjs
|
||||||
|
|
||||||
// instance of transaction builder
|
// instance of transaction builder
|
||||||
// const transactionBuilder = new bchjs.TransactionBuilder()
|
// const transactionBuilder = new bchjs.TransactionBuilder()
|
||||||
@@ -470,8 +529,10 @@ class WalletAdapter {
|
|||||||
// 'mainnet'
|
// 'mainnet'
|
||||||
// )
|
// )
|
||||||
|
|
||||||
|
console.log('completeTx() userWallet.walletInfo: ', userWallet.walletInfo)
|
||||||
|
|
||||||
// Get the keypair for the address used in the Order
|
// Get the keypair for the address used in the Order
|
||||||
const keyPair = await this.getKeyPair(hdIndex)
|
const keyPair = await userWallet.getKeyPair(hdIndex)
|
||||||
console.log(`maker keyPair: ${JSON.stringify(keyPair, null, 2)}`)
|
console.log(`maker keyPair: ${JSON.stringify(keyPair, null, 2)}`)
|
||||||
const makerECPair = bchjs.ECPair.fromWIF(keyPair.wif)
|
const makerECPair = bchjs.ECPair.fromWIF(keyPair.wif)
|
||||||
|
|
||||||
@@ -503,7 +564,9 @@ class WalletAdapter {
|
|||||||
// return csTxHex
|
// return csTxHex
|
||||||
|
|
||||||
// Broadcast transaction to the network
|
// Broadcast transaction to the network
|
||||||
const txid = await this.bchWallet.ar.sendTx(csTxHex)
|
// const txid = await this.bchWallet.ar.sendTx(csTxHex)
|
||||||
|
const txid = await this.retryQueue.addToQueue(this.bchWallet.broadcast, { hex: csTxHex })
|
||||||
|
console.log('completeTx() txid: ', txid)
|
||||||
|
|
||||||
return txid
|
return txid
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -511,6 +574,129 @@ class WalletAdapter {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given an Order database object, this function sends the tokens from the
|
||||||
|
// HD index where they are stored, back to the root address of the wallet.
|
||||||
|
async reclaimTokens (orderData = {}) {
|
||||||
|
try {
|
||||||
|
if (!orderData.hdIndex) {
|
||||||
|
throw new Error('orderData.hdIndex is required!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the key pair from the Order.
|
||||||
|
const keyPair = await this.getKeyPair(orderData.hdIndex)
|
||||||
|
console.log('keyPair: ', keyPair)
|
||||||
|
|
||||||
|
// Instantiate the wallet using the private key of the Order.
|
||||||
|
// const tempWallet = new this.BchWallet(keyPair.wif, advancedConfig)
|
||||||
|
// await tempWallet.initialize()
|
||||||
|
|
||||||
|
// Send the tokens from the Order to the root address.
|
||||||
|
// const receiver = {
|
||||||
|
// address: this.bchWallet.walletInfo.address,
|
||||||
|
// tokenId: orderData.tokenId,
|
||||||
|
// qty: orderData.numTokens
|
||||||
|
// }
|
||||||
|
// console.log('receiver: ', receiver)
|
||||||
|
//
|
||||||
|
// const txid = await tempWallet.sendTokens(receiver)
|
||||||
|
|
||||||
|
// Sweep the tokens to the root address.
|
||||||
|
const sweeper = new this.BchTokenSweep(
|
||||||
|
keyPair.wif,
|
||||||
|
this.bchWallet.walletInfo.privateKey,
|
||||||
|
this.bchWallet,
|
||||||
|
550,
|
||||||
|
this.bchWallet.walletInfo.cashAddress
|
||||||
|
)
|
||||||
|
await sweeper.populateObjectFromNetwork()
|
||||||
|
const hex = await sweeper.sweepTo(this.bchWallet.walletInfo.cashAddress)
|
||||||
|
const txid = await this.bchWallet.ar.sendTx(hex)
|
||||||
|
|
||||||
|
return txid
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in wallet.js/reclaimTokens()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is consumed by the use-cases/order.js/createOrder() function.
|
||||||
|
// Tokens are moved from the primary address to a holding address to protect the Order UTXO.
|
||||||
|
async moveTokensFromCustomWallet (inObj = {}) {
|
||||||
|
try {
|
||||||
|
const { tokenId, qty, wallet } = inObj
|
||||||
|
// Input validation
|
||||||
|
if (!tokenId || typeof tokenId !== 'string') {
|
||||||
|
throw new Error('tokenId must be a string!')
|
||||||
|
}
|
||||||
|
if (!qty) {
|
||||||
|
throw new Error('qty must be a number!')
|
||||||
|
}
|
||||||
|
if (!wallet) {
|
||||||
|
throw new Error('wallet is required!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the system wallet with the user wallet.
|
||||||
|
// this.bchWallet = wallet
|
||||||
|
|
||||||
|
// DEV Note: Storing all token Offers in index 2 of the HD wallet.
|
||||||
|
// This makes them easy to sweep, and can be uniquely identified by their UTXO.
|
||||||
|
// Note: use walle.getKeyPair() to use the user wallet. this.getKeyPair uses the system wallet.
|
||||||
|
const keyPair = await wallet.getKeyPair(2)
|
||||||
|
console.log('keyPair: ', keyPair)
|
||||||
|
|
||||||
|
const receiver = {
|
||||||
|
address: keyPair.cashAddress,
|
||||||
|
tokenId,
|
||||||
|
qty
|
||||||
|
}
|
||||||
|
console.log('receiver: ', receiver)
|
||||||
|
|
||||||
|
// Update the UTXO store of the wallet.
|
||||||
|
await wallet.initialize()
|
||||||
|
|
||||||
|
// Get the token type of the token being moved.
|
||||||
|
// Combine Fungible and NFT token UTXOs.
|
||||||
|
let tokenUtxos = wallet.utxos.utxoStore.slpUtxos.type1.tokens.concat(
|
||||||
|
wallet.utxos.utxoStore.slpUtxos.nft.tokens)
|
||||||
|
// Get token UTXOs that match the token in the order.
|
||||||
|
tokenUtxos = tokenUtxos.filter(
|
||||||
|
x => x.tokenId === tokenId
|
||||||
|
)
|
||||||
|
|
||||||
|
const txid = await wallet.sendTokens(receiver, 3)
|
||||||
|
const utxoInfo = {
|
||||||
|
txid,
|
||||||
|
vout: 1,
|
||||||
|
hdIndex: keyPair.hdIndex,
|
||||||
|
tokenType: tokenUtxos[0].tokenType
|
||||||
|
}
|
||||||
|
|
||||||
|
return utxoInfo
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in wallet.js/moveTokensFromCustomWallet()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cid2json (urlOrCid) {
|
||||||
|
try {
|
||||||
|
// Input validation
|
||||||
|
if (!urlOrCid || typeof urlOrCid !== 'string') {
|
||||||
|
throw new Error('urlOrCid must be a string!')
|
||||||
|
}
|
||||||
|
// Extract the cid from the url or cid.
|
||||||
|
const cid = urlOrCid.split('/').pop()
|
||||||
|
console.log('cid to json: ', cid)
|
||||||
|
const jsonRes = await this.bchWallet.cid2json({ cid })
|
||||||
|
const json = jsonRes.json
|
||||||
|
// console.log('json: ', json)
|
||||||
|
return json
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in wallet.js/cid2json()', err.message)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WalletAdapter
|
export default WalletAdapter
|
||||||
|
|||||||
+15
-13
@@ -6,13 +6,15 @@ import config from '../../config/index.js'
|
|||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class WebHook {
|
class WebHook {
|
||||||
constructor () {
|
constructor () {
|
||||||
_this = this
|
this.config = config
|
||||||
_this.config = config
|
this.axios = axios
|
||||||
_this.axios = axios
|
this.sleepTime = 2000
|
||||||
|
this.createWebhook = this.createWebhook.bind(this)
|
||||||
|
this.deleteWebhook = this.deleteWebhook.bind(this)
|
||||||
|
this.waitUntilSuccess = this.waitUntilSuccess.bind(this)
|
||||||
|
this.sleep = this.sleep.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// REST petition to create a webhook in p2wdb-service
|
// REST petition to create a webhook in p2wdb-service
|
||||||
@@ -22,14 +24,14 @@ class WebHook {
|
|||||||
throw new Error('url must be a string')
|
throw new Error('url must be a string')
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = _this.config.webhookService
|
const endpoint = this.config.webhookService
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
appId: this.config.p2wdbAppId,
|
appId: this.config.p2wdbAppId,
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await axios.post(endpoint, obj)
|
const result = await this.axios.post(endpoint, obj)
|
||||||
|
|
||||||
return result.data
|
return result.data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -45,14 +47,14 @@ class WebHook {
|
|||||||
throw new Error('url must be a string')
|
throw new Error('url must be a string')
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = _this.config.webhookService
|
const endpoint = this.config.webhookService
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
appId: this.config.p2wdbAppId,
|
appId: this.config.p2wdbAppId,
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await axios.delete(endpoint, { data: obj })
|
const result = await this.axios.delete(endpoint, { data: obj })
|
||||||
|
|
||||||
return result.data
|
return result.data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -86,7 +88,7 @@ class WebHook {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
console.log(`${now.toLocaleString()}: Error trying to create webhook with P2WDB. Trying again...`)
|
console.log(`${now.toLocaleString()}: Error trying to create webhook with P2WDB. Trying again...`)
|
||||||
await sleep(2000)
|
await this.sleep(this.sleepTime)
|
||||||
}
|
}
|
||||||
} while (!success)
|
} while (!success)
|
||||||
|
|
||||||
@@ -96,10 +98,10 @@ class WebHook {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function sleep (ms) {
|
async sleep (ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms))
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WebHook
|
export default WebHook
|
||||||
|
|||||||
+17
-12
@@ -6,28 +6,28 @@
|
|||||||
|
|
||||||
// Public npm libraries.
|
// Public npm libraries.
|
||||||
|
|
||||||
// Load the Clean Architecture Adapters library
|
// Local libraries
|
||||||
import Adapters from '../adapters/index.js'
|
import Adapters from '../adapters/index.js'
|
||||||
|
|
||||||
// Load the JSON RPC Controller.
|
|
||||||
import JSONRPC from './json-rpc/index.js'
|
import JSONRPC from './json-rpc/index.js'
|
||||||
|
|
||||||
// Load the Clean Architecture Use Case libraries.
|
|
||||||
import UseCases from '../use-cases/index.js'
|
import UseCases from '../use-cases/index.js'
|
||||||
|
|
||||||
// const useCases = new UseCases({ adapters })
|
|
||||||
|
|
||||||
// Load the REST API Controllers.
|
|
||||||
import RESTControllers from './rest-api/index.js'
|
import RESTControllers from './rest-api/index.js'
|
||||||
|
|
||||||
import TimerControllers from './timer-controllers.js'
|
import TimerControllers from './timer-controllers.js'
|
||||||
|
import config from '../../config/index.js'
|
||||||
|
|
||||||
class Controllers {
|
class Controllers {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
|
// Encapsulate dependencies
|
||||||
this.adapters = new Adapters()
|
this.adapters = new Adapters()
|
||||||
this.useCases = new UseCases({ adapters: this.adapters })
|
this.useCases = new UseCases({ adapters: this.adapters })
|
||||||
|
|
||||||
this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases })
|
this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases })
|
||||||
|
this.config = config
|
||||||
|
|
||||||
|
// Bind 'this' object to all subfunction
|
||||||
|
this.initAdapters = this.initAdapters.bind(this)
|
||||||
|
this.initUseCases = this.initUseCases.bind(this)
|
||||||
|
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
||||||
|
this.attachControllers = this.attachControllers.bind(this)
|
||||||
|
this.attachRPCControllers = this.attachRPCControllers.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spin up any adapter libraries that have async startup needs.
|
// Spin up any adapter libraries that have async startup needs.
|
||||||
@@ -54,8 +54,13 @@ class Controllers {
|
|||||||
|
|
||||||
// Attach any other controllers other than REST API controllers.
|
// Attach any other controllers other than REST API controllers.
|
||||||
async attachControllers (app) {
|
async attachControllers (app) {
|
||||||
|
if (this.config.useIpfs) {
|
||||||
|
// Attach JSON RPC controllers
|
||||||
|
this.attachRPCControllers()
|
||||||
|
}
|
||||||
|
|
||||||
// this.attachRPCControllers()
|
// Attach and start the timer controllers
|
||||||
|
this.timerControllers.startTimers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the JSON RPC router to the ipfs-coord adapter.
|
// Add the JSON RPC router to the ipfs-coord adapter.
|
||||||
|
|||||||
@@ -29,7 +29,36 @@ class EntryRESTControllerLib {
|
|||||||
_this = this
|
_this = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /entry Create an entry
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName CreateEntry
|
||||||
|
* @apiGroup REST Entry
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST -d '{"entry": "1234567890", "description": "1234567890", "slpAddress": "1234567890", "signature": "1234567890", "category": "1234567890"}' localhost:5001/entry
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} entry Entry object
|
||||||
|
* @apiSuccess {String} entry.id Entry ID
|
||||||
|
* @apiSuccess {String} entry.description Entry description
|
||||||
|
* @apiSuccess {String} entry.slpAddress Entry slp address
|
||||||
|
* @apiSuccess {String} entry.signature Entry signature
|
||||||
|
* @apiSuccess {String} entry.category Entry category
|
||||||
|
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "entry": {
|
||||||
|
* "id": "1234567890",
|
||||||
|
* "description": "1234567890",
|
||||||
|
* "slpAddress": "1234567890",
|
||||||
|
* "signature": "1234567890",
|
||||||
|
* "category": "1234567890"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
// No api-doc documentation because this wont be a public endpoint
|
// No api-doc documentation because this wont be a public endpoint
|
||||||
|
// Note: this is a private endpoint, needs to be authenticated
|
||||||
async createEntry (ctx) {
|
async createEntry (ctx) {
|
||||||
try {
|
try {
|
||||||
console.log('body: ', ctx.request.body)
|
console.log('body: ', ctx.request.body)
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ import AuthRESTController from './auth/index.js'
|
|||||||
import UserRouter from './users/index.js'
|
import UserRouter from './users/index.js'
|
||||||
import ContactRESTController from './contact/index.js'
|
import ContactRESTController from './contact/index.js'
|
||||||
import LogsRESTController from './logs/index.js'
|
import LogsRESTController from './logs/index.js'
|
||||||
|
import IpfsRESTController from './ipfs/index.js'
|
||||||
|
import config from '../../../config/index.js'
|
||||||
import EntryRouter from './entry/index.js'
|
import EntryRouter from './entry/index.js'
|
||||||
import OfferRouter from './offer/index.js'
|
import OfferRouter from './offer/index.js'
|
||||||
import OrderRouter from './order/index.js'
|
import OrderRouter from './order/index.js'
|
||||||
import P2WDBRouter from './p2wdb/index.js'
|
import P2WDBRouter from './p2wdb/index.js'
|
||||||
|
import UsageRESTController from './usage/index.js'
|
||||||
|
import SmAccountRouter from './smAccount/index.js'
|
||||||
|
import NostrRouter from './nostr/index.js'
|
||||||
|
|
||||||
class RESTControllers {
|
class RESTControllers {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
@@ -32,7 +37,11 @@ class RESTControllers {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('Controllers localConfig: ', localConfig)
|
// Bind 'this' object to all subfunctions.
|
||||||
|
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
||||||
|
|
||||||
|
// Encapsulate dependencies
|
||||||
|
this.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
attachRESTControllers (app) {
|
attachRESTControllers (app) {
|
||||||
@@ -41,13 +50,15 @@ class RESTControllers {
|
|||||||
useCases: this.useCases
|
useCases: this.useCases
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach the REST API Controllers associated with the /auth route
|
if (!this.config.noMongo) {
|
||||||
const authRESTController = new AuthRESTController(dependencies)
|
// Attach the REST API Controllers associated with the /auth route
|
||||||
authRESTController.attach(app)
|
const authRESTController = new AuthRESTController(dependencies)
|
||||||
|
authRESTController.attach(app)
|
||||||
|
|
||||||
// Attach the REST API Controllers associated with the /user route
|
// Attach the REST API Controllers associated with the /user route
|
||||||
const userRouter = new UserRouter(dependencies)
|
const userRouter = new UserRouter(dependencies)
|
||||||
userRouter.attach(app)
|
userRouter.attach(app)
|
||||||
|
}
|
||||||
|
|
||||||
// Attach the REST API Controllers associated with the /contact route
|
// Attach the REST API Controllers associated with the /contact route
|
||||||
const contactRESTController = new ContactRESTController(dependencies)
|
const contactRESTController = new ContactRESTController(dependencies)
|
||||||
@@ -57,6 +68,10 @@ class RESTControllers {
|
|||||||
const logsRESTController = new LogsRESTController(dependencies)
|
const logsRESTController = new LogsRESTController(dependencies)
|
||||||
logsRESTController.attach(app)
|
logsRESTController.attach(app)
|
||||||
|
|
||||||
|
// Attach the REST API Controllers associated with the /ipfs route
|
||||||
|
const ipfsRESTController = new IpfsRESTController(dependencies)
|
||||||
|
ipfsRESTController.attach(app)
|
||||||
|
|
||||||
// Attach the REST API Controllers associated with the /entry route
|
// Attach the REST API Controllers associated with the /entry route
|
||||||
const entryRouter = new EntryRouter(dependencies)
|
const entryRouter = new EntryRouter(dependencies)
|
||||||
entryRouter.attach(app)
|
entryRouter.attach(app)
|
||||||
@@ -69,6 +84,17 @@ class RESTControllers {
|
|||||||
|
|
||||||
const p2wdbRouter = new P2WDBRouter(dependencies)
|
const p2wdbRouter = new P2WDBRouter(dependencies)
|
||||||
p2wdbRouter.attach(app)
|
p2wdbRouter.attach(app)
|
||||||
|
|
||||||
|
// Attach the REST API Controllers associated with the /usage route
|
||||||
|
const usageRESTController = new UsageRESTController(dependencies)
|
||||||
|
usageRESTController.attach(app)
|
||||||
|
|
||||||
|
const smAccountRouter = new SmAccountRouter(dependencies)
|
||||||
|
smAccountRouter.attach(app)
|
||||||
|
|
||||||
|
// Attach the REST API Controllers associated with the /nostr route
|
||||||
|
const nostrRouter = new NostrRouter(dependencies)
|
||||||
|
nostrRouter.attach(app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/*
|
||||||
|
REST API Controller library for the /ipfs route
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Global npm libraries
|
||||||
|
|
||||||
|
// Local libraries
|
||||||
|
import wlogger from '../../../adapters/wlogger.js'
|
||||||
|
|
||||||
|
class IpfsRESTControllerLib {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// Dependency Injection.
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating /ipfs REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Use Cases library required when instantiating /ipfs REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate dependencies
|
||||||
|
// this.UserModel = this.adapters.localdb.Users
|
||||||
|
// this.userUseCases = this.useCases.user
|
||||||
|
|
||||||
|
// Bind 'this' object to all subfunctions
|
||||||
|
this.getStatus = this.getStatus.bind(this)
|
||||||
|
this.getPeers = this.getPeers.bind(this)
|
||||||
|
this.getRelays = this.getRelays.bind(this)
|
||||||
|
this.handleError = this.handleError.bind(this)
|
||||||
|
this.connect = this.connect.bind(this)
|
||||||
|
this.getThisNode = this.getThisNode.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /ipfs Get status on IPFS infrastructure
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetIpfsStatus
|
||||||
|
* @apiGroup REST IPFS
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} status IPFS status object
|
||||||
|
* @apiSuccess {String} status.ipfsId IPFS ID
|
||||||
|
* @apiSuccess {String} status.multiaddrs IPFS multiaddrs
|
||||||
|
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "status": {
|
||||||
|
* "ipfsId": "1234567890",
|
||||||
|
* "multiaddrs": ["/ip4/123.45.67.89/tcp/4001/p2p/1234567890"]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
async getStatus (ctx) {
|
||||||
|
try {
|
||||||
|
const status = await this.adapters.ipfs.getStatus()
|
||||||
|
|
||||||
|
ctx.body = { status }
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in ipfs/controller.js/getStatus(): ')
|
||||||
|
// ctx.throw(422, err.message)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /ipfs/peers Get information on IPFS peers this node is connected to
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetIpfsPeers
|
||||||
|
* @apiGroup REST IPFS
|
||||||
|
*
|
||||||
|
* @apiParam {Boolean} [showAll=false] Whether to include detailed peer data
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/peers \
|
||||||
|
* -d '{"showAll": false}'
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object[]} peers Array of peer objects
|
||||||
|
* @apiSuccess {String} peers[].peer Peer ID
|
||||||
|
* @apiSuccess {String} peers[].name Peer name
|
||||||
|
* @apiSuccess {String} peers[].protocol Protocol used by the peer
|
||||||
|
* @apiSuccess {String} peers[].version Peer version
|
||||||
|
* @apiSuccess {String} peers[].connectionAddr Connection address
|
||||||
|
* @apiSuccess {Object} [peers[].peerData] Detailed peer data (when showAll=true)
|
||||||
|
*/
|
||||||
|
async getPeers (ctx) {
|
||||||
|
try {
|
||||||
|
const showAll = ctx.request.body.showAll
|
||||||
|
|
||||||
|
const peers = await this.adapters.ipfs.getPeers(showAll)
|
||||||
|
|
||||||
|
ctx.body = { peers }
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in ipfs/controller.js/getPeers(): ')
|
||||||
|
// ctx.throw(422, err.message)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /ipfs/relays Get data about the known Circuit Relays
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetIpfsRelays
|
||||||
|
* @apiGroup REST IPFS
|
||||||
|
*
|
||||||
|
* @apiDescription Returns information about Circuit Relays, both v1 and v2, that this node knows about. V2 relays are hydrated with peer data from the connected peers list.
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/relays
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} relays Object containing relay information
|
||||||
|
* @apiSuccess {Object[]} relays.v2Relays Array of v2 Circuit Relay objects
|
||||||
|
* @apiSuccess {String} relays.v2Relays[].ipfsId IPFS ID of the relay
|
||||||
|
* @apiSuccess {String} relays.v2Relays[].name Name of the relay (hydrated from peer data)
|
||||||
|
* @apiSuccess {String} relays.v2Relays[].description Description of the relay (hydrated from peer data)
|
||||||
|
* @apiSuccess {Object[]} relays.v1Relays Array of v1 Circuit Relay configurations
|
||||||
|
*/
|
||||||
|
// Get data about the known Circuit Relays. Hydrate with data from peers list.
|
||||||
|
async getRelays (ctx) {
|
||||||
|
try {
|
||||||
|
const relays = await this.adapters.ipfs.getRelays()
|
||||||
|
|
||||||
|
ctx.body = { relays }
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in ipfs/controller.js/getRelays(): ')
|
||||||
|
// ctx.throw(422, err.message)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /ipfs/connect Connect to a specific IPFS peer
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName ConnectToIpfsPeer
|
||||||
|
* @apiGroup REST IPFS
|
||||||
|
*
|
||||||
|
* @apiDescription Attempts to establish a connection to a specific IPFS peer using the provided multiaddr. Optionally returns detailed information about the connection.
|
||||||
|
*
|
||||||
|
* @apiParam {String} multiaddr Multiaddress of the peer to connect to (required)
|
||||||
|
* @apiParam {Boolean} [getDetails=false] Whether to return detailed connection information
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/connect \
|
||||||
|
* -d '{"multiaddr": "/ip4/161.35.99.207/tcp/4001/p2p/12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu", "getDetails": false}'
|
||||||
|
*
|
||||||
|
* @apiSuccess {Boolean} success Indicates whether the connection attempt was successful
|
||||||
|
* @apiSuccess {Object} [details] Additional connection details (when getDetails=true)
|
||||||
|
*/
|
||||||
|
async connect (ctx) {
|
||||||
|
try {
|
||||||
|
const multiaddr = ctx.request.body.multiaddr
|
||||||
|
const getDetails = ctx.request.body.getDetails
|
||||||
|
|
||||||
|
// console.log('this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.adapters.ipfs: ', this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.adapters.ipfs)
|
||||||
|
const result = await this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.adapters.ipfs.connectToPeer({ multiaddr, getDetails })
|
||||||
|
// console.log('result: ', result)
|
||||||
|
|
||||||
|
ctx.body = result
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in ipfs/controller.js/connect():', err)
|
||||||
|
// ctx.throw(422, err.message)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /ipfs/node Get a copy of the thisNode object from helia-coord
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetThisNode
|
||||||
|
* @apiGroup REST IPFS
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getThisNode (ctx) {
|
||||||
|
try {
|
||||||
|
const thisNode = this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode
|
||||||
|
|
||||||
|
ctx.body = { thisNode }
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in ipfs/controller.js/getThisNode(): ')
|
||||||
|
// ctx.throw(422, err.message)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRY error handler
|
||||||
|
handleError (ctx, err) {
|
||||||
|
// If an HTTP status is specified by the buisiness logic, use that.
|
||||||
|
if (err.status) {
|
||||||
|
if (err.message) {
|
||||||
|
ctx.throw(err.status, err.message)
|
||||||
|
} else {
|
||||||
|
ctx.throw(err.status)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// By default use a 422 error if the HTTP status is not specified.
|
||||||
|
ctx.throw(422, err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// module.exports = IpfsRESTControllerLib
|
||||||
|
export default IpfsRESTControllerLib
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
REST API library for the /ipfs route.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public npm libraries.
|
||||||
|
import Router from 'koa-router'
|
||||||
|
|
||||||
|
// Local libraries.
|
||||||
|
import IPFSRESTControllerLib from './controller.js'
|
||||||
|
import Validators from '../middleware/validators.js'
|
||||||
|
|
||||||
|
// let _this
|
||||||
|
|
||||||
|
class IpfsRouter {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// Dependency Injection.
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating IPFS REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencies = {
|
||||||
|
adapters: this.adapters,
|
||||||
|
useCases: this.useCases
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate dependencies.
|
||||||
|
this.ipfsRESTController = new IPFSRESTControllerLib(dependencies)
|
||||||
|
this.validators = new Validators()
|
||||||
|
|
||||||
|
// Instantiate the router and set the base route.
|
||||||
|
const baseUrl = '/ipfs'
|
||||||
|
this.router = new Router({ prefix: baseUrl })
|
||||||
|
|
||||||
|
// _this = this
|
||||||
|
}
|
||||||
|
|
||||||
|
attach (app) {
|
||||||
|
if (!app) {
|
||||||
|
throw new Error(
|
||||||
|
'Must pass app object when attaching REST API controllers.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the routes and attach the controller.
|
||||||
|
this.router.get('/', this.ipfsRESTController.getStatus)
|
||||||
|
this.router.post('/peers', this.ipfsRESTController.getPeers)
|
||||||
|
this.router.post('/relays', this.ipfsRESTController.getRelays)
|
||||||
|
this.router.post('/connect', this.ipfsRESTController.connect)
|
||||||
|
this.router.get('/node', this.ipfsRESTController.getThisNode)
|
||||||
|
|
||||||
|
// Attach the Controller routes to the Koa app.
|
||||||
|
app.use(this.router.routes())
|
||||||
|
app.use(this.router.allowedMethods())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// module.exports = BchRouter
|
||||||
|
export default IpfsRouter
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
/*
|
||||||
|
REST API Controller library for the /nostr route
|
||||||
|
*/
|
||||||
|
import wlogger from '../../../adapters/wlogger.js'
|
||||||
|
|
||||||
|
class NostrRESTControllerLib {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// Dependency Injection.
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating /nostr REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Use Cases library required when instantiating /nostr REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createDeletedChat = this.createDeletedChat.bind(this)
|
||||||
|
this.getDeletedChats = this.getDeletedChats.bind(this)
|
||||||
|
this.createDeletedPost = this.createDeletedPost.bind(this)
|
||||||
|
this.getDeletedPosts = this.getDeletedPosts.bind(this)
|
||||||
|
this.handleError = this.handleError.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /nostr/deletedChat Create Deleted Chat.
|
||||||
|
* @apiPermission admin
|
||||||
|
* @apiName CreateDeletedChat
|
||||||
|
* @apiGroup REST Nostr
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -H "Authorization: Bearer <token>" -X POST -d '{"eventId": "note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q" , "npub" : "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8" , "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", "pubkey":"6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1"}' localhost:5700/nostr/deletedChat
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} _id DB Model id
|
||||||
|
* @apiSuccess {String} eventId Event id
|
||||||
|
* @apiSuccess {String} bchAddr BCH Address
|
||||||
|
* @apiSuccess {String} npub Nostr Npub
|
||||||
|
* @apiSuccess {String} pubkey Nostr pubkey
|
||||||
|
* @apiSuccess {Date} createdAt Created at date.
|
||||||
|
* @apiSuccess {Date} updatedAt Updated at date.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "_id": "6900f87f9cafee38028ea27e",
|
||||||
|
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
|
||||||
|
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
|
||||||
|
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
|
||||||
|
* "createdAt":"2025-10-28T17:08:15.409Z",
|
||||||
|
* "updatedAt": "2025-10-28T17:08:15.409Z"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
|
async createDeletedChat (ctx) {
|
||||||
|
try {
|
||||||
|
const chat = ctx.request.body
|
||||||
|
|
||||||
|
const result = await this.useCases.nostr.createDeletedChat(chat)
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
deletedChat: result
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in createDeletedChat() REST API handler.')
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /nostr/deletedChat Get Deleted Chats.
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetDeletedChats
|
||||||
|
* @apiGroup REST Nostr
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5700/nostr/deletedChat
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} _id DB Model id
|
||||||
|
* @apiSuccess {String} eventId Event id
|
||||||
|
* @apiSuccess {String} bchAddr BCH Address
|
||||||
|
* @apiSuccess {String} npub Nostr Npub
|
||||||
|
* @apiSuccess {String} pubkey Nostr pubkey
|
||||||
|
* @apiSuccess {Date} createdAt Created at date.
|
||||||
|
* @apiSuccess {Date} updatedAt Updated at date.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* [{
|
||||||
|
* "_id": "6900f87f9cafee38028ea27e",
|
||||||
|
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
|
||||||
|
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
|
||||||
|
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
|
||||||
|
* "createdAt":"2025-10-28T17:08:15.409Z",
|
||||||
|
* "updatedAt": "2025-10-28T17:08:15.409Z"
|
||||||
|
* }]
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
async getDeletedChats (ctx) {
|
||||||
|
try {
|
||||||
|
const deletedChats = await this.useCases.nostr.getDeletedChats()
|
||||||
|
|
||||||
|
ctx.body = { deletedChats }
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in getDeletedChats() REST API handler.')
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /nostr/deletedPost Create Deleted Post.
|
||||||
|
* @apiPermission admin
|
||||||
|
* @apiName CreateDeletedPost
|
||||||
|
* @apiGroup REST Nostr
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -H "Authorization: Bearer <token>" -X POST -d '{"eventId": "note19e93faw4ffqepsqsrwrnstd3ee00nmzakwwuyfjm43dankgummfqms4p6q" , "npub" : "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8" , "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", "pubkey":"6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1"}' localhost:5700/nostr/deletedPost
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} _id DB Model id
|
||||||
|
* @apiSuccess {String} eventId Event id
|
||||||
|
* @apiSuccess {String} bchAddr BCH Address
|
||||||
|
* @apiSuccess {String} npub Nostr Npub
|
||||||
|
* @apiSuccess {String} pubkey Nostr pubkey
|
||||||
|
* @apiSuccess {Date} createdAt Created at date.
|
||||||
|
* @apiSuccess {Date} updatedAt Updated at date.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "_id": "6900f87f9cafee38028ea27e",
|
||||||
|
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
|
||||||
|
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
|
||||||
|
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
|
||||||
|
* "createdAt":"2025-10-28T17:08:15.409Z",
|
||||||
|
* "updatedAt": "2025-10-28T17:08:15.409Z"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
|
async createDeletedPost (ctx) {
|
||||||
|
try {
|
||||||
|
const post = ctx.request.body
|
||||||
|
|
||||||
|
const result = await this.useCases.nostr.createDeletedPost(post)
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
deletedPost: result
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in createDeletedPost() REST API handler.')
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /nostr/deletedPost Get Deleted Posts.
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetDeletedPosts
|
||||||
|
* @apiGroup REST Nostr
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5700/nostr/deletedPost
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} _id DB Model id
|
||||||
|
* @apiSuccess {String} eventId Event id
|
||||||
|
* @apiSuccess {String} bchAddr BCH Address
|
||||||
|
* @apiSuccess {String} npub Nostr Npub
|
||||||
|
* @apiSuccess {String} pubkey Nostr pubkey
|
||||||
|
* @apiSuccess {Date} createdAt Created at date.
|
||||||
|
* @apiSuccess {Date} updatedAt Updated at date.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* [{
|
||||||
|
* "_id": "6900f87f9cafee38028ea27e",
|
||||||
|
* "npub": "npub1d4ed5x49d7p24xn63flj4985dc4gpfngdhtqcxpth0ywhm6czxcscfpcq8",
|
||||||
|
* "bchAddr" : "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
|
||||||
|
* "pubKey": "6d72da1aa56f82aa9a7a8a7f2a94f46e2a80a6686dd60c182bbbc8ebef5811b1",
|
||||||
|
* "createdAt":"2025-10-28T17:08:15.409Z",
|
||||||
|
* "updatedAt": "2025-10-28T17:08:15.409Z"
|
||||||
|
* }]
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
async getDeletedPosts (ctx) {
|
||||||
|
try {
|
||||||
|
const deletedPosts = await this.useCases.nostr.getDeletedPosts()
|
||||||
|
|
||||||
|
ctx.body = { deletedPosts }
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in getDeletedPosts() REST API handler.')
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRY error handler
|
||||||
|
handleError (ctx, err) {
|
||||||
|
// If an HTTP status is specified by the buisiness logic, use that.
|
||||||
|
if (err.status) {
|
||||||
|
if (err.message) {
|
||||||
|
ctx.throw(err.status, err.message)
|
||||||
|
} else {
|
||||||
|
ctx.throw(err.status)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// By default use a 422 error if the HTTP status is not specified.
|
||||||
|
ctx.throw(422, err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NostrRESTControllerLib
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
REST API library for /nostr route.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public npm libraries.
|
||||||
|
import Router from 'koa-router'
|
||||||
|
|
||||||
|
// Local libraries.
|
||||||
|
import NostrRESTControllerLib from './controller.js'
|
||||||
|
|
||||||
|
import Validators from '../middleware/validators.js'
|
||||||
|
|
||||||
|
import config from '../../../../config/index.js'
|
||||||
|
|
||||||
|
class NostrRouter {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// Dependency Injection.
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating NostrRouter REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Use Cases library required when instantiating NostrRouter REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencies = {
|
||||||
|
adapters: this.adapters,
|
||||||
|
useCases: this.useCases
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate dependencies.
|
||||||
|
this.config = config
|
||||||
|
this.nostrRESTController = new NostrRESTControllerLib(dependencies)
|
||||||
|
this.validators = new Validators()
|
||||||
|
|
||||||
|
// Instantiate the router and set the base route.
|
||||||
|
const baseUrl = '/nostr'
|
||||||
|
this.router = new Router({ prefix: baseUrl })
|
||||||
|
|
||||||
|
this.attach = this.attach.bind(this)
|
||||||
|
this.createDeletedChat = this.createDeletedChat.bind(this)
|
||||||
|
this.createDeletedPost = this.createDeletedPost.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
attach (app) {
|
||||||
|
if (!app) {
|
||||||
|
throw new Error(
|
||||||
|
'Must pass app object when attaching REST API controllers.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the routes and attach the controller.
|
||||||
|
this.router.post('/deletedChat', this.createDeletedChat)
|
||||||
|
this.router.get('/deletedChat', this.nostrRESTController.getDeletedChats)
|
||||||
|
this.router.post('/deletedPost', this.createDeletedPost)
|
||||||
|
this.router.get('/deletedPost', this.nostrRESTController.getDeletedPosts)
|
||||||
|
|
||||||
|
// Attach the Controller routes to the Koa app.
|
||||||
|
app.use(this.router.routes())
|
||||||
|
app.use(this.router.allowedMethods())
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDeletedChat (ctx, next) {
|
||||||
|
await this.validators.ensureAdmin(ctx, next)
|
||||||
|
await this.nostrRESTController.createDeletedChat(ctx, next)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDeletedPost (ctx, next) {
|
||||||
|
await this.validators.ensureAdmin(ctx, next)
|
||||||
|
await this.nostrRESTController.createDeletedPost(ctx, next)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NostrRouter
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
REST API Controller library for the /offer route
|
REST API Controller library for the /offer route
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- Add api-doc documentation for all endpoints in this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import wlogger from '../../../adapters/wlogger.js'
|
import wlogger from '../../../adapters/wlogger.js'
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class OfferRESTControllerLib {
|
class OfferRESTControllerLib {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
// Dependency Injection.
|
// Dependency Injection.
|
||||||
@@ -26,17 +27,27 @@ class OfferRESTControllerLib {
|
|||||||
this.OfferModel = this.adapters.localdb.Offer
|
this.OfferModel = this.adapters.localdb.Offer
|
||||||
// this.userUseCases = this.useCases.user
|
// this.userUseCases = this.useCases.user
|
||||||
|
|
||||||
_this = this
|
// Bind 'this' object to all subfunctions.
|
||||||
|
this.createOffer = this.createOffer.bind(this)
|
||||||
|
this.listOffers = this.listOffers.bind(this)
|
||||||
|
this.listNftOffers = this.listNftOffers.bind(this)
|
||||||
|
this.listFungibleOffers = this.listFungibleOffers.bind(this)
|
||||||
|
this.takeOffer = this.takeOffer.bind(this)
|
||||||
|
this.listOffersByAddress = this.listOffersByAddress.bind(this)
|
||||||
|
this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
|
||||||
|
this.listCounterOffersByAddress = this.listCounterOffersByAddress.bind(this)
|
||||||
|
this.handleError = this.handleError.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No api-doc documentation because this wont be a public endpoint
|
// No api-doc documentation because this wont be a public endpoint
|
||||||
|
// This function has no endpoint
|
||||||
async createOffer (ctx) {
|
async createOffer (ctx) {
|
||||||
try {
|
try {
|
||||||
console.log('body: ', ctx.request.body)
|
console.log('body: ', ctx.request.body)
|
||||||
|
|
||||||
const offerObj = ctx.request.body
|
const offerObj = ctx.request.body
|
||||||
|
|
||||||
await _this.useCases.offer.createOffer(offerObj)
|
await this.useCases.offer.createOffer(offerObj)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
success: true
|
success: true
|
||||||
@@ -45,9 +56,68 @@ class OfferRESTControllerLib {
|
|||||||
// console.log(`err.message: ${err.message}`)
|
// console.log(`err.message: ${err.message}`)
|
||||||
// console.log('err: ', err)
|
// console.log('err: ', err)
|
||||||
// ctx.throw(422, err.message)
|
// ctx.throw(422, err.message)
|
||||||
_this.handleError(ctx, err)
|
this.handleError(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @api {get} /offer/list/all/:page Get all offers
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName ListOffers
|
||||||
|
* @apiGroup REST Offer
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/all/0
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object[]} offers Array of offer objects
|
||||||
|
* @apiSuccess {ObjectId} offers._id Offer id
|
||||||
|
* @apiSuccess {String} offers.ticker Ticker
|
||||||
|
* @apiSuccess {String} offers.tokenId Token id
|
||||||
|
* @apiSuccess {String} offers.utxoTxid Utxo transaction id
|
||||||
|
* @apiSuccess {Number} offers.utxoVout Utxo output index
|
||||||
|
* @apiSuccess {String} offers.buyOrSell Buy or sell
|
||||||
|
* @apiSuccess {Number} offers.numTokens Number of tokens
|
||||||
|
* @apiSuccess {String} offers.rateInBaseUnit Rate in base unit
|
||||||
|
* @apiSuccess {String} offers.minUnitsToExchange Minimum units to exchange
|
||||||
|
* @apiSuccess {String} offers.offerStatus Offer status
|
||||||
|
* @apiSuccess {String} offers.makerAddr Maker address
|
||||||
|
* @apiSuccess {Number} offers.timestamp Timestamp
|
||||||
|
* @apiSuccess {String} offers.displayCategory Display category
|
||||||
|
* @apiSuccess {Boolean} offers.nsfw Nsfw
|
||||||
|
* @apiSuccess {Array} offers.flags Flags
|
||||||
|
* @apiSuccess {Number} offers.messageType Message type
|
||||||
|
* @apiSuccess {Number} offers.messageClass Message class
|
||||||
|
* @apiSuccess {String} offers.nostrEventId Nostr event id
|
||||||
|
* @apiSuccess {String} offers.operatorAddress Operator address
|
||||||
|
* @apiSuccess {Number} offers.operatorPercentage Operator percentage
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "offers": [{
|
||||||
|
* "_id": "56bd1da600a526986cf65c80",
|
||||||
|
* "tokenId": "1234567890",
|
||||||
|
* "utxoTxid": "1234567890",
|
||||||
|
* "utxoVout": 0,
|
||||||
|
* "buyOrSell": "buy",
|
||||||
|
* "numTokens": 1,
|
||||||
|
* "rateInBaseUnit": "100000000",
|
||||||
|
* "minUnitsToExchange": "100000000",
|
||||||
|
* "offerStatus": "active",
|
||||||
|
* "makerAddr": "1234567890",
|
||||||
|
* "timestamp": 1716883200,
|
||||||
|
* "displayCategory": "nft",
|
||||||
|
* "nsfw": false,
|
||||||
|
* "flags": [],
|
||||||
|
* "messageType": 1,
|
||||||
|
* "messageClass": 1,
|
||||||
|
* "nostrEventId": "1234567890",
|
||||||
|
* "operatorAddress": "1234567890",
|
||||||
|
* "operatorPercentage": 100
|
||||||
|
* }]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
|
|
||||||
// curl -X GET http://localhost:5700/offer/list/all/0
|
// curl -X GET http://localhost:5700/offer/list/all/0
|
||||||
async listOffers (ctx) {
|
async listOffers (ctx) {
|
||||||
@@ -55,59 +125,333 @@ class OfferRESTControllerLib {
|
|||||||
let page = ctx.params.page
|
let page = ctx.params.page
|
||||||
if (!page) page = 0
|
if (!page) page = 0
|
||||||
|
|
||||||
const offers = await _this.useCases.offer.listOffers(page)
|
const offers = await this.useCases.offer.listOffers(page)
|
||||||
|
|
||||||
ctx.body = offers
|
ctx.body = offers
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Error in listOffers REST API handler: ', err)
|
console.log('Error in listOffers REST API handler: ', err)
|
||||||
_this.handleError(ctx, err)
|
this.handleError(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /offer/list/nft/:page Get all nft offers
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName ListNftOffers
|
||||||
|
* @apiGroup REST Offer
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/nft/0
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object[]} offers Array of offer objects
|
||||||
|
* @apiSuccess {ObjectId} offers._id Offer id
|
||||||
|
* @apiSuccess {String} offers.ticker Ticker
|
||||||
|
* @apiSuccess {String} offers.tokenId Token id
|
||||||
|
* @apiSuccess {String} offers.utxoTxid Utxo transaction id
|
||||||
|
* @apiSuccess {Number} offers.utxoVout Utxo output index
|
||||||
|
* @apiSuccess {String} offers.buyOrSell Buy or sell
|
||||||
|
* @apiSuccess {Number} offers.numTokens Number of tokens
|
||||||
|
* @apiSuccess {String} offers.rateInBaseUnit Rate in base unit
|
||||||
|
* @apiSuccess {String} offers.minUnitsToExchange Minimum units to exchange
|
||||||
|
* @apiSuccess {String} offers.offerStatus Offer status
|
||||||
|
* @apiSuccess {String} offers.makerAddr Maker address
|
||||||
|
* @apiSuccess {Number} offers.timestamp Timestamp
|
||||||
|
* @apiSuccess {String} offers.displayCategory Display category
|
||||||
|
* @apiSuccess {Boolean} offers.nsfw Nsfw
|
||||||
|
* @apiSuccess {Array} offers.flags Flags
|
||||||
|
* @apiSuccess {Number} offers.messageType Message type
|
||||||
|
* @apiSuccess {Number} offers.messageClass Message class
|
||||||
|
* @apiSuccess {String} offers.nostrEventId Nostr event id
|
||||||
|
* @apiSuccess {String} offers.operatorAddress Operator address
|
||||||
|
* @apiSuccess {Number} offers.operatorPercentage Operator percentage
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "offers": [{
|
||||||
|
* "_id": "56bd1da600a526986cf65c80",
|
||||||
|
* "tokenId": "1234567890",
|
||||||
|
* "utxoTxid": "1234567890",
|
||||||
|
* "utxoVout": 0,
|
||||||
|
* "buyOrSell": "buy",
|
||||||
|
* "numTokens": 1,
|
||||||
|
* "rateInBaseUnit": "100000000",
|
||||||
|
* "minUnitsToExchange": "100000000",
|
||||||
|
* "offerStatus": "active",
|
||||||
|
* "makerAddr": "1234567890",
|
||||||
|
* "timestamp": 1716883200,
|
||||||
|
* "displayCategory": "nft",
|
||||||
|
* "nsfw": false,
|
||||||
|
* "flags": [],
|
||||||
|
* "messageType": 1,
|
||||||
|
* "messageClass": 1,
|
||||||
|
* "nostrEventId": "1234567890",
|
||||||
|
* "operatorAddress": "1234567890",
|
||||||
|
* "operatorPercentage": 100
|
||||||
|
* }]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
|
|
||||||
// curl -X GET http://localhost:5700/offer/list/nft/0
|
// curl -X GET http://localhost:5700/offer/list/nft/0
|
||||||
async listNftOffers (ctx) {
|
async listNftOffers (ctx) {
|
||||||
try {
|
try {
|
||||||
let page = ctx.params.page
|
let page = ctx.params.page
|
||||||
if (!page) page = 0
|
if (!page) page = 0
|
||||||
|
|
||||||
const offers = await _this.useCases.offer.listNftOffers(page)
|
const offers = await this.useCases.offer.listNftOffers(page)
|
||||||
|
|
||||||
ctx.body = offers
|
ctx.body = offers
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Error in listNftOffers REST API handler: ', err)
|
console.log('Error in listNftOffers REST API handler: ', err)
|
||||||
_this.handleError(ctx, err)
|
this.handleError(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /offer/list/fungible/:page Get all fungible offers
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName ListFungibleOffers
|
||||||
|
* @apiGroup REST Offer
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/fungible/0
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object[]} offers Array of offer objects
|
||||||
|
* @apiSuccess {ObjectId} offers._id Offer id
|
||||||
|
* @apiSuccess {String} offers.ticker Ticker
|
||||||
|
* @apiSuccess {String} offers.tokenId Token id
|
||||||
|
* @apiSuccess {String} offers.utxoTxid Utxo transaction id
|
||||||
|
* @apiSuccess {Number} offers.utxoVout Utxo output index
|
||||||
|
* @apiSuccess {String} offers.buyOrSell Buy or sell
|
||||||
|
* @apiSuccess {Number} offers.numTokens Number of tokens
|
||||||
|
* @apiSuccess {String} offers.rateInBaseUnit Rate in base unit
|
||||||
|
* @apiSuccess {String} offers.minUnitsToExchange Minimum units to exchange
|
||||||
|
* @apiSuccess {String} offers.offerStatus Offer status
|
||||||
|
* @apiSuccess {String} offers.makerAddr Maker address
|
||||||
|
* @apiSuccess {Number} offers.timestamp Timestamp
|
||||||
|
* @apiSuccess {String} offers.displayCategory Display category
|
||||||
|
* @apiSuccess {Boolean} offers.nsfw Nsfw
|
||||||
|
* @apiSuccess {Array} offers.flags Flags
|
||||||
|
* @apiSuccess {Number} offers.messageType Message type
|
||||||
|
* @apiSuccess {Number} offers.messageClass Message class
|
||||||
|
* @apiSuccess {String} offers.nostrEventId Nostr event id
|
||||||
|
* @apiSuccess {String} offers.operatorAddress Operator address
|
||||||
|
* @apiSuccess {Number} offers.operatorPercentage Operator percentage
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "offers": [{
|
||||||
|
* "_id": "56bd1da600a526986cf65c80",
|
||||||
|
* "tokenId": "1234567890",
|
||||||
|
* "utxoTxid": "1234567890",
|
||||||
|
* "utxoVout": 0,
|
||||||
|
* "buyOrSell": "buy",
|
||||||
|
* "numTokens": 1,
|
||||||
|
* "rateInBaseUnit": "100000000",
|
||||||
|
* "minUnitsToExchange": "100000000",
|
||||||
|
* "offerStatus": "active",
|
||||||
|
* "makerAddr": "1234567890",
|
||||||
|
* "timestamp": 1716883200,
|
||||||
|
* "displayCategory": "fungible",
|
||||||
|
* "nsfw": false,
|
||||||
|
* "flags": [],
|
||||||
|
* "messageType": 1,
|
||||||
|
* "messageClass": 1,
|
||||||
|
* "nostrEventId": "1234567890",
|
||||||
|
* "operatorAddress": "1234567890",
|
||||||
|
* "operatorPercentage": 100
|
||||||
|
* }]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
// curl -X GET http://localhost:5700/offer/list/fungible/0
|
// curl -X GET http://localhost:5700/offer/list/fungible/0
|
||||||
async listFungibleOffers (ctx) {
|
async listFungibleOffers (ctx) {
|
||||||
try {
|
try {
|
||||||
let page = ctx.params.page
|
let page = ctx.params.page
|
||||||
if (!page) page = 0
|
if (!page) page = 0
|
||||||
|
|
||||||
const offers = await _this.useCases.offer.listFungibleOffers(page)
|
const offers = await this.useCases.offer.listFungibleOffers(page)
|
||||||
|
|
||||||
ctx.body = offers
|
ctx.body = offers
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Error in Fungible REST API handler: ', err)
|
console.log('Error in Fungible REST API handler: ', err)
|
||||||
_this.handleError(ctx, err)
|
this.handleError(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /offer/take Take an offer
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName TakeOffer
|
||||||
|
* @apiGroup REST Offer
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST -d '{ "nostrEventId": "1234567890" }' localhost:5010/offer/take
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} eventId Event id
|
||||||
|
* @apiSuccess {String} noteId Note id
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "eventId": "1234567890",
|
||||||
|
* "noteId": "1234567890"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
// Currently only supports 'sell' offers, and will only buy the 'numTokens'
|
// Currently only supports 'sell' offers, and will only buy the 'numTokens'
|
||||||
// listed in the offer.
|
// listed in the offer.
|
||||||
async takeOffer (ctx) {
|
async takeOffer (ctx) {
|
||||||
try {
|
try {
|
||||||
console.log('REST API controller, body: ', ctx.request.body)
|
console.log('REST API controller, body: ', ctx.request.body)
|
||||||
|
|
||||||
const offerCid = ctx.request.body.offerCid
|
const nostrEventId = ctx.request.body.nostrEventId
|
||||||
|
|
||||||
const hash = await _this.useCases.offer.takeOffer(offerCid)
|
const { eventId, noteId } = await this.useCases.offer.takeOffer(nostrEventId)
|
||||||
|
|
||||||
ctx.body = { hash }
|
ctx.body = { eventId, noteId }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
wlogger.error('Error in takeOffer() REST API handler.')
|
wlogger.error('Error in takeOffer() REST API handler.')
|
||||||
_this.handleError(ctx, err)
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /offer/list/addr/:addr List all offers being made by a given address
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName ListOffersByAddress
|
||||||
|
* @apiGroup REST Offer
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/addr/bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object[]} offers Array of offer objects
|
||||||
|
* @apiSuccess {ObjectId} offers._id Offer id
|
||||||
|
* @apiSuccess {String} offers.ticker Ticker
|
||||||
|
* @apiSuccess {String} offers.tokenId Token id
|
||||||
|
* @apiSuccess {String} offers.utxoTxid Utxo transaction id
|
||||||
|
* @apiSuccess {Number} offers.utxoVout Utxo output index
|
||||||
|
* @apiSuccess {String} offers.buyOrSell Buy or sell
|
||||||
|
* @apiSuccess {Number} offers.numTokens Number of tokens
|
||||||
|
* @apiSuccess {String} offers.rateInBaseUnit Rate in base unit
|
||||||
|
* @apiSuccess {String} offers.minUnitsToExchange Minimum units to exchange
|
||||||
|
* @apiSuccess {String} offers.offerStatus Offer status
|
||||||
|
* @apiSuccess {String} offers.makerAddr Maker address
|
||||||
|
* @apiSuccess {Number} offers.timestamp Timestamp
|
||||||
|
* @apiSuccess {String} offers.displayCategory Display category
|
||||||
|
* @apiSuccess {Boolean} offers.nsfw Nsfw
|
||||||
|
* @apiSuccess {Array} offers.flags Flags
|
||||||
|
* @apiSuccess {Number} offers.messageType Message type
|
||||||
|
* @apiSuccess {Number} offers.messageClass Message class
|
||||||
|
* @apiSuccess {String} offers.nostrEventId Nostr event id
|
||||||
|
* @apiSuccess {String} offers.operatorAddress Operator address
|
||||||
|
* @apiSuccess {Number} offers.operatorPercentage Operator percentage
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "offers": [{
|
||||||
|
* "_id": "56bd1da600a526986cf65c80",
|
||||||
|
* "tokenId": "1234567890",
|
||||||
|
* "utxoTxid": "1234567890",
|
||||||
|
* "utxoVout": 0,
|
||||||
|
* "buyOrSell": "buy",
|
||||||
|
* "numTokens": 1,
|
||||||
|
* "rateInBaseUnit": "100000000",
|
||||||
|
* "minUnitsToExchange": "100000000",
|
||||||
|
* "offerStatus": "active",
|
||||||
|
* "makerAddr": "bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2",
|
||||||
|
* "timestamp": 1716883200,
|
||||||
|
* "displayCategory": "fungible",
|
||||||
|
* "nsfw": false,
|
||||||
|
* "flags": [],
|
||||||
|
* "messageType": 1,
|
||||||
|
* "messageClass": 1,
|
||||||
|
* "nostrEventId": "1234567890",
|
||||||
|
* "operatorAddress": "1234567890",
|
||||||
|
* "operatorPercentage": 100
|
||||||
|
* }]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
|
|
||||||
|
// List all offers being made by a given address.
|
||||||
|
// curl -X GET http://localhost:5700/offer/list/addr/bitcoincash:qrpxtnrlhfz9wsuuse7z5k2mxmvw0r3pu5qepyhmq2
|
||||||
|
async listOffersByAddress (ctx) {
|
||||||
|
try {
|
||||||
|
const addr = ctx.params.addr
|
||||||
|
|
||||||
|
const offers = await this.useCases.offer.listOffersByAddress(addr)
|
||||||
|
|
||||||
|
ctx.body = offers
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error in listOffersByAddress REST API handler: ', err)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncOfferMutableData (ctx) {
|
||||||
|
try {
|
||||||
|
const tokenId = ctx.request.body.tokenId
|
||||||
|
const offer = await this.useCases.offer.syncOfferMutableData(tokenId)
|
||||||
|
|
||||||
|
ctx.body = offer
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error in syncOfferMutableData REST API handler: ', err)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /offer/list/counter-offer/:addr List all counter offers being made by a given address
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName listCounterOffersByAddress
|
||||||
|
* @apiGroup REST Offer
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5000/offer/list/counter-offer/bitcoincash:qq54a3xyyptty63ztlcavzzh64gy4d247qs4ueaupe
|
||||||
|
*
|
||||||
|
*
|
||||||
|
|
||||||
|
* @apiSuccess {String} offers.nostrEventId Nostr event id
|
||||||
|
* @apiSuccess {String} offers.takerNpub Nostr Npub.
|
||||||
|
* @apiSuccess {String} offers.takerAddr BCH Address.
|
||||||
|
* @apiSuccess {String} offers.counterOfferAddr BCH Address
|
||||||
|
* @apiSuccess {String} offers.counterOfferUtxo Utxo txid.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "counterOffers": [{
|
||||||
|
* "_id": "56bd1da600a526986cf65c80",
|
||||||
|
* "nostrEventId": "1234567890",
|
||||||
|
* "takerAddr": "bitcoincash:qq54a3xyyptty63ztlcavzzh64gy4d247qs4ueaupe",
|
||||||
|
* "takerNpub": "npub1nhc47z2tf42a4ps7plzcyax2j76gdausve763tqw3xuw7jjd7ceqppquly",
|
||||||
|
* "counterOfferAddr": "bitcoincash:qzltx40ldxr53edchprppj5lpg4q7vl20usx25ga44",
|
||||||
|
* "counterOfferUtxo": "71dab5b81cf06f3495aeebcc70fb351109e5e88bc85da67d0d144c0e5ca0bd6a",
|
||||||
|
* }]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
|
async listCounterOffersByAddress (ctx) {
|
||||||
|
try {
|
||||||
|
const addr = ctx.params.addr
|
||||||
|
|
||||||
|
const counterOffers = await this.useCases.offer.listCounterOffersByAddress(addr)
|
||||||
|
|
||||||
|
ctx.body = { counterOffers }
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error in listCounterOffersByAddress REST API handler: ', err)
|
||||||
|
this.handleError(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import Router from 'koa-router'
|
|||||||
// Local libraries.
|
// Local libraries.
|
||||||
import OfferRESTControllerLib from './controller.js'
|
import OfferRESTControllerLib from './controller.js'
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class OfferRouter {
|
class OfferRouter {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
// Dependency Injection.
|
// Dependency Injection.
|
||||||
@@ -38,7 +36,8 @@ class OfferRouter {
|
|||||||
const baseUrl = '/offer'
|
const baseUrl = '/offer'
|
||||||
this.router = new Router({ prefix: baseUrl })
|
this.router = new Router({ prefix: baseUrl })
|
||||||
|
|
||||||
_this = this
|
// Bind 'this' object to all subfunctions.
|
||||||
|
this.attach = this.attach.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
attach (app) {
|
attach (app) {
|
||||||
@@ -48,16 +47,24 @@ class OfferRouter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 12/3/24 CT:
|
||||||
|
// Note: The createOffer() path was used by a P2WDB webhook to generate an
|
||||||
|
// Offer from an Order. This has been deprecated and Offers are now created
|
||||||
|
// by a Timer Controller monitoring a Nostr topic.
|
||||||
|
|
||||||
// Define the routes and attach the controller.
|
// Define the routes and attach the controller.
|
||||||
this.router.post('/', _this.offerRESTController.createOffer)
|
// this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
|
||||||
this.router.post('/take', _this.offerRESTController.takeOffer)
|
this.router.post('/take', this.offerRESTController.takeOffer)
|
||||||
this.router.get('/list/all/:page', _this.offerRESTController.listOffers)
|
this.router.post('/mutable/sync/', this.offerRESTController.syncOfferMutableData)
|
||||||
this.router.get('/list/nft/:page', _this.offerRESTController.listNftOffers)
|
this.router.get('/list/all/:page', this.offerRESTController.listOffers)
|
||||||
this.router.get('/list/fungible/:page', _this.offerRESTController.listFungibleOffers)
|
this.router.get('/list/nft/:page', this.offerRESTController.listNftOffers)
|
||||||
|
this.router.get('/list/fungible/:page', this.offerRESTController.listFungibleOffers)
|
||||||
|
this.router.get('/list/addr/:addr', this.offerRESTController.listOffersByAddress)
|
||||||
|
this.router.get('/list/counter-offer/:addr', this.offerRESTController.listCounterOffersByAddress)
|
||||||
|
|
||||||
// Attach the Controller routes to the Koa app.
|
// Attach the Controller routes to the Koa app.
|
||||||
app.use(_this.router.routes())
|
app.use(this.router.routes())
|
||||||
app.use(_this.router.allowedMethods())
|
app.use(this.router.allowedMethods())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,16 +29,159 @@ class OrderRESTControllerLib {
|
|||||||
_this = this
|
_this = this
|
||||||
}
|
}
|
||||||
|
|
||||||
// No api-doc documentation because this wont be a public endpoint
|
/**
|
||||||
|
* @api {post} /order Create an order
|
||||||
|
* @apiPermission User
|
||||||
|
* @apiName CreateOrder
|
||||||
|
* @apiGroup REST Order
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST -d '{"messageType": "1", "messageClass": "1", "tokenId": "1234567890", "buyOrSell": "buy", "rateInBaseUnit": "100000000", "minUnitsToExchange": "100000000", "numTokens": "1", "makerAddr": "1234567890"}' localhost:5001/order
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} eventId Event id
|
||||||
|
* @apiSuccess {String} noteId Note id
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "eventId": "1234567890",
|
||||||
|
* "noteId": "1234567890"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
|
|
||||||
async createOrder (ctx) {
|
async createOrder (ctx) {
|
||||||
try {
|
try {
|
||||||
// console.log('body: ', ctx.request.body)
|
// console.log('body: ', ctx.request.body)
|
||||||
|
|
||||||
const orderObj = ctx.request.body.order
|
const orderObj = ctx.request.body.order
|
||||||
|
const user = ctx.state.user
|
||||||
|
orderObj.userId = user._id
|
||||||
|
console.log('orderObj: ', orderObj)
|
||||||
|
|
||||||
const hash = await _this.useCases.order.createOrder(orderObj)
|
const { eventId, noteId } = await _this.useCases.order.createOrder(orderObj)
|
||||||
|
|
||||||
ctx.body = { hash }
|
ctx.body = { eventId, noteId }
|
||||||
|
} catch (err) {
|
||||||
|
// console.log(`err.message: ${err.message}`)
|
||||||
|
// console.log('err: ', err)
|
||||||
|
// ctx.throw(422, err.message)
|
||||||
|
_this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @api {get} /order/list/all/:page List all orders
|
||||||
|
* @apiPermission user
|
||||||
|
* @apiName ListOrders
|
||||||
|
* @apiGroup REST Order
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5000/order/list/all/0
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object[]} orders Array of order objects
|
||||||
|
* @apiSuccess {ObjectId} orders._id Order id
|
||||||
|
* @apiSuccess {String} orders.tokenId Token id
|
||||||
|
* @apiSuccess {String} orders.ticker Ticker
|
||||||
|
* @apiSuccess {String} orders.dataType Data type
|
||||||
|
* @apiSuccess {String} orders.utxoTxid Utxo transaction id
|
||||||
|
* @apiSuccess {Number} orders.utxoVout Utxo output index
|
||||||
|
* @apiSuccess {String} orders.buyOrSell Buy or sell
|
||||||
|
* @apiSuccess {Number} orders.numTokens Number of tokens
|
||||||
|
* @apiSuccess {String} orders.rateInBaseUnit Rate in base unit
|
||||||
|
* @apiSuccess {String} orders.minUnitsToExchange Minimum units to exchange
|
||||||
|
* @apiSuccess {String} orders.offerStatus Offer status
|
||||||
|
* @apiSuccess {String} orders.makerAddr Maker address
|
||||||
|
* @apiSuccess {Number} orders.timestamp Timestamp
|
||||||
|
* @apiSuccess {String} orders.displayCategory Display category
|
||||||
|
* @apiSuccess {Boolean} orders.nsfw Nsfw
|
||||||
|
* @apiSuccess {Array} orders.flags Flags
|
||||||
|
* @apiSuccess {Number} orders.messageType Message type
|
||||||
|
* @apiSuccess {Number} orders.messageClass Message class
|
||||||
|
* @apiSuccess {String} orders.nostrEventId Nostr event id
|
||||||
|
* @apiSuccess {String} orders.operatorAddress Operator address
|
||||||
|
* @apiSuccess {Number} orders.operatorPercentage Operator percentage
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "orders": [{
|
||||||
|
* "_id": "56bd1da600a526986cf65c80",
|
||||||
|
* "tokenId": "1234567890",
|
||||||
|
* "ticker": "BTC",
|
||||||
|
* "dataType": "offer",
|
||||||
|
* "utxoTxid": "1234567890",
|
||||||
|
* "utxoVout": 0,
|
||||||
|
* "buyOrSell": "buy",
|
||||||
|
* "numTokens": 1,
|
||||||
|
* "rateInBaseUnit": "100000000",
|
||||||
|
* "minUnitsToExchange": "100000000",
|
||||||
|
* "offerStatus": "active",
|
||||||
|
* "makerAddr": "1234567890",
|
||||||
|
* "timestamp": 1716883200,
|
||||||
|
* "displayCategory": "nft",
|
||||||
|
* "nsfw": false,
|
||||||
|
* "flags": [],
|
||||||
|
* "messageType": 1,
|
||||||
|
* "messageClass": 1,
|
||||||
|
* "nostrEventId": "1234567890",
|
||||||
|
* "operatorAddress": "1234567890",
|
||||||
|
* "operatorPercentage": 100
|
||||||
|
* }]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
|
|
||||||
|
// curl -X GET http://localhost:5700/order/list/all/0
|
||||||
|
async listOrders (ctx) {
|
||||||
|
try {
|
||||||
|
let page = ctx.params.page
|
||||||
|
if (!page) page = 0
|
||||||
|
|
||||||
|
const offers = await _this.useCases.order.listOrders(page)
|
||||||
|
|
||||||
|
ctx.body = offers
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error in listOrders REST API handler: ', err)
|
||||||
|
_this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete an existing order by returning the token to the root address of
|
||||||
|
// the DEX wallet.
|
||||||
|
/**
|
||||||
|
* @api {post} /order/delete Delete an order
|
||||||
|
* @apiPermission user
|
||||||
|
* @apiName DeleteOrder
|
||||||
|
* @apiGroup REST Order
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST -d '{ "nostrEventId": "1234567890" }' localhost:5000/order/delete
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} txid Transaction id
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "txid": "1234567890"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse TokenError
|
||||||
|
*/
|
||||||
|
|
||||||
|
// curl -X POST http://localhost:5700/order/delete -d '{ "nostrEventId": "1234567890" }'
|
||||||
|
|
||||||
|
async deleteOrder (ctx) {
|
||||||
|
try {
|
||||||
|
// console.log('body: ', ctx.request.body)
|
||||||
|
|
||||||
|
const nostrEventId = ctx.request.body.nostrEventId
|
||||||
|
// console.log('p2wdbHash: ', p2wdbHash)
|
||||||
|
|
||||||
|
const txid = await _this.useCases.order.deleteOrder(nostrEventId)
|
||||||
|
|
||||||
|
ctx.body = { txid }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.log(`err.message: ${err.message}`)
|
// console.log(`err.message: ${err.message}`)
|
||||||
// console.log('err: ', err)
|
// console.log('err: ', err)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Router from 'koa-router'
|
|||||||
|
|
||||||
// Local libraries.
|
// Local libraries.
|
||||||
import OrderRESTControllerLib from './controller.js'
|
import OrderRESTControllerLib from './controller.js'
|
||||||
|
import Validators from '../middleware/validators.js'
|
||||||
|
|
||||||
let _this
|
let _this
|
||||||
|
|
||||||
@@ -32,12 +33,14 @@ class OrderRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encapsulate dependencies.
|
// Encapsulate dependencies.
|
||||||
|
this.validators = new Validators()
|
||||||
this.orderRESTController = new OrderRESTControllerLib(dependencies)
|
this.orderRESTController = new OrderRESTControllerLib(dependencies)
|
||||||
|
|
||||||
// Instantiate the router and set the base route.
|
// Instantiate the router and set the base route.
|
||||||
const baseUrl = '/order'
|
const baseUrl = '/order'
|
||||||
this.router = new Router({ prefix: baseUrl })
|
this.router = new Router({ prefix: baseUrl })
|
||||||
|
|
||||||
|
this.createOrder = this.createOrder.bind(this)
|
||||||
_this = this
|
_this = this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,12 +52,24 @@ class OrderRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define the routes and attach the controller.
|
// Define the routes and attach the controller.
|
||||||
this.router.post('/', _this.orderRESTController.createOrder)
|
this.router.post('/', this.createOrder)
|
||||||
|
this.router.get('/list/all/:page', _this.orderRESTController.listOrders)
|
||||||
|
this.router.post('/delete', this.deleteOrder)
|
||||||
|
|
||||||
// Attach the Controller routes to the Koa app.
|
// Attach the Controller routes to the Koa app.
|
||||||
app.use(_this.router.routes())
|
app.use(_this.router.routes())
|
||||||
app.use(_this.router.allowedMethods())
|
app.use(_this.router.allowedMethods())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createOrder (ctx, next) {
|
||||||
|
await _this.validators.ensureUser(ctx, next)
|
||||||
|
await _this.orderRESTController.createOrder(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOrder (ctx, next) {
|
||||||
|
await _this.validators.ensureUser(ctx, next)
|
||||||
|
await _this.orderRESTController.deleteOrder(ctx, next)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrderRouter
|
export default OrderRouter
|
||||||
|
|||||||
@@ -31,7 +31,25 @@ class P2WDBRESTControllerLib {
|
|||||||
_this = this
|
_this = this
|
||||||
}
|
}
|
||||||
|
|
||||||
// No api-doc documentation because this wont be a public endpoint
|
/**
|
||||||
|
* @api {post} /p2wdb Route a P2WDB webhook
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName RouteWebhook
|
||||||
|
* @apiGroup REST P2WDB
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST -d '{"data": {"dataType": "offer"} } ' localhost:5001/p2wdb
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} success Success message
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "success": true
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Note: verify if this is a private endpoint, needs to be authenticated
|
||||||
async routeWebhook (ctx) {
|
async routeWebhook (ctx) {
|
||||||
try {
|
try {
|
||||||
console.log('p2wdb REST API handler: body: ', ctx.request.body)
|
console.log('p2wdb REST API handler: body: ', ctx.request.body)
|
||||||
@@ -64,6 +82,7 @@ class P2WDBRESTControllerLib {
|
|||||||
await _this.useCases.offer.flagOffer(flagData)
|
await _this.useCases.offer.flagOffer(flagData)
|
||||||
} else {
|
} else {
|
||||||
console.log('Could not route P2WDB webhook data.')
|
console.log('Could not route P2WDB webhook data.')
|
||||||
|
// NOTE: Should we throw an error here?
|
||||||
}
|
}
|
||||||
|
|
||||||
// const orderObj = ctx.request.body.order
|
// const orderObj = ctx.request.body.order
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
REST API Controller library for the /order route
|
||||||
|
*/
|
||||||
|
|
||||||
|
// const { wlogger } = require('../../../adapters/wlogger')
|
||||||
|
|
||||||
|
let _this
|
||||||
|
|
||||||
|
class SmAccountRESTControllerLib {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// Dependency Injection.
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating /sm REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Use Cases library required when instantiating /sm REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate dependencies
|
||||||
|
this.SmAccountModel = this.adapters.localdb.SmAccount
|
||||||
|
// this.userUseCases = this.useCases.user
|
||||||
|
|
||||||
|
_this = this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /sm/list/all/{page} List all Social Media Accounts
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName ListAccounts
|
||||||
|
* @apiGroup REST Social Media Accounts
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/list/all/0
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
// curl -X GET http://localhost:5700/sm/list/all/0
|
||||||
|
async listAccounts (ctx) {
|
||||||
|
try {
|
||||||
|
let page = ctx.params.page
|
||||||
|
if (!page) page = 0
|
||||||
|
|
||||||
|
const offers = await _this.useCases.smAccount.listAccounts(page)
|
||||||
|
|
||||||
|
ctx.body = offers
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error in listAccounts REST API handler: ', err)
|
||||||
|
_this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /sm/npub/{npub} Get Social Media Account by Npub
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetAccountByNpub
|
||||||
|
* @apiGroup REST Social Media Accounts
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
// curl -X GET http://localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
|
||||||
|
async getAccountByNpub (ctx) {
|
||||||
|
try {
|
||||||
|
const npub = ctx.params.npub
|
||||||
|
if (!npub) {
|
||||||
|
ctx.throw(400, 'Npub is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await _this.useCases.smAccount.getAccountByNpub(npub)
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
ctx.throw(404, 'Account not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = account
|
||||||
|
} catch (err) {
|
||||||
|
// console.log('Error in getAccountByNpub REST API handler: ', err)
|
||||||
|
_this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /sm/bchAddr/{bchAddr} Get Social Media Account by BCH address
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetAccountByBchAddr
|
||||||
|
* @apiGroup REST Social Media Accounts
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
// curl -X GET http://localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
|
||||||
|
async getAccountByBchAddr (ctx) {
|
||||||
|
try {
|
||||||
|
const bchAddr = ctx.params.bchAddr
|
||||||
|
if (!bchAddr) {
|
||||||
|
ctx.throw(400, 'BCH address is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await _this.useCases.smAccount.getAccountByBchAddr(bchAddr)
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
ctx.throw(404, 'Account not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = account
|
||||||
|
} catch (err) {
|
||||||
|
// console.log('Error in getAccountByBchAddr REST API handler: ', err)
|
||||||
|
_this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRY error handler
|
||||||
|
handleError (ctx, err) {
|
||||||
|
console.log('err', err)
|
||||||
|
|
||||||
|
// If an HTTP status is specified by the buisiness logic, use that.
|
||||||
|
if (err.status) {
|
||||||
|
if (err.message) {
|
||||||
|
ctx.throw(err.status, err.message)
|
||||||
|
} else {
|
||||||
|
ctx.throw(err.status)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// By default use a 422 error if the HTTP status is not specified.
|
||||||
|
ctx.throw(422, err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SmAccountRESTControllerLib
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
REST API library for /order route.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public npm libraries.
|
||||||
|
import Router from 'koa-router'
|
||||||
|
|
||||||
|
// Local libraries.
|
||||||
|
import SmAccountRESTControllerLib from './controller.js'
|
||||||
|
import Validators from '../middleware/validators.js'
|
||||||
|
|
||||||
|
let _this
|
||||||
|
|
||||||
|
class SmAccountRouter {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// Dependency Injection.
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating /sm REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Use Cases library required when instantiating /sm REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencies = {
|
||||||
|
adapters: this.adapters,
|
||||||
|
useCases: this.useCases
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate dependencies.
|
||||||
|
this.validators = new Validators()
|
||||||
|
this.smAccountRESTController = new SmAccountRESTControllerLib(dependencies)
|
||||||
|
|
||||||
|
// Instantiate the router and set the base route.
|
||||||
|
const baseUrl = '/sm'
|
||||||
|
this.router = new Router({ prefix: baseUrl })
|
||||||
|
|
||||||
|
_this = this
|
||||||
|
}
|
||||||
|
|
||||||
|
attach (app) {
|
||||||
|
if (!app) {
|
||||||
|
throw new Error(
|
||||||
|
'Must pass app object when attached REST API controllers.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the routes and attach the controller.
|
||||||
|
this.router.get('/list/all/:page', _this.smAccountRESTController.listAccounts)
|
||||||
|
this.router.get('/npub/:npub', _this.smAccountRESTController.getAccountByNpub)
|
||||||
|
this.router.get('/bchAddr/:bchAddr', _this.smAccountRESTController.getAccountByBchAddr)
|
||||||
|
|
||||||
|
// Attach the Controller routes to the Koa app.
|
||||||
|
app.use(_this.router.routes())
|
||||||
|
app.use(_this.router.allowedMethods())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SmAccountRouter
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
REST API Controller library for the /usage route
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Global npm libraries
|
||||||
|
|
||||||
|
// Local libraries
|
||||||
|
import wlogger from '../../../adapters/wlogger.js'
|
||||||
|
|
||||||
|
class UsageRESTControllerLib {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// Dependency Injection.
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating /usage REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Use Cases library required when instantiating /usage REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate dependencies
|
||||||
|
|
||||||
|
// Bind 'this' object to all subfunctions
|
||||||
|
this.getStatus = this.getStatus.bind(this)
|
||||||
|
this.getTopIps = this.getTopIps.bind(this)
|
||||||
|
this.getTopEndpoints = this.getTopEndpoints.bind(this)
|
||||||
|
this.handleError = this.handleError.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /usage Get status on IPFS infrastructure
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetUsageStatus
|
||||||
|
* @apiGroup REST Usage
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
getStatus (ctx) {
|
||||||
|
try {
|
||||||
|
// const status = await this.adapters.ipfs.getStatus()
|
||||||
|
const status = this.useCases.usage.getRestSummary()
|
||||||
|
|
||||||
|
ctx.body = { status }
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in usage/controller.js/getStatus(): ')
|
||||||
|
// ctx.throw(422, err.message)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /usage/ips Get top IP addresses consuming the REST API
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetUsageIPs
|
||||||
|
* @apiGroup REST Usage
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/ips
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
getTopIps (ctx) {
|
||||||
|
try {
|
||||||
|
const ips = this.useCases.usage.getTopIps()
|
||||||
|
|
||||||
|
ctx.body = { ips }
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in usage/controller.js/getTopIps(): ')
|
||||||
|
// ctx.throw(422, err.message)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /usage/endpoints Get top endpoints consumed from the REST API
|
||||||
|
* @apiPermission public
|
||||||
|
* @apiName GetUsageEndpoints
|
||||||
|
* @apiGroup REST Usage
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/endpoints
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
getTopEndpoints (ctx) {
|
||||||
|
try {
|
||||||
|
const endpoints = this.useCases.usage.getTopEndpoints()
|
||||||
|
|
||||||
|
ctx.body = { endpoints }
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in usage/controller.js/getTopEndpoints(): ')
|
||||||
|
// ctx.throw(422, err.message)
|
||||||
|
this.handleError(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRY error handler
|
||||||
|
handleError (ctx, err) {
|
||||||
|
// If an HTTP status is specified by the buisiness logic, use that.
|
||||||
|
if (err.status) {
|
||||||
|
if (err.message) {
|
||||||
|
ctx.throw(err.status, err.message)
|
||||||
|
} else {
|
||||||
|
ctx.throw(err.status)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// By default use a 422 error if the HTTP status is not specified.
|
||||||
|
ctx.throw(422, err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// module.exports = IpfsRESTControllerLib
|
||||||
|
export default UsageRESTControllerLib
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
REST API library for the /usage route.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public npm libraries.
|
||||||
|
import Router from 'koa-router'
|
||||||
|
|
||||||
|
// Local libraries.
|
||||||
|
import UsageRESTControllerLib from './controller.js'
|
||||||
|
import Validators from '../middleware/validators.js'
|
||||||
|
|
||||||
|
// let _this
|
||||||
|
|
||||||
|
class UsageRouter {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// Dependency Injection.
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating IPFS REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencies = {
|
||||||
|
adapters: this.adapters,
|
||||||
|
useCases: this.useCases
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate dependencies.
|
||||||
|
this.usageRESTController = new UsageRESTControllerLib(dependencies)
|
||||||
|
this.validators = new Validators()
|
||||||
|
|
||||||
|
// Instantiate the router and set the base route.
|
||||||
|
const baseUrl = '/usage'
|
||||||
|
this.router = new Router({ prefix: baseUrl })
|
||||||
|
|
||||||
|
// _this = this
|
||||||
|
}
|
||||||
|
|
||||||
|
attach (app) {
|
||||||
|
if (!app) {
|
||||||
|
throw new Error(
|
||||||
|
'Must pass app object when attaching REST API controllers.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the routes and attach the controller.
|
||||||
|
this.router.get('/', this.usageRESTController.getStatus)
|
||||||
|
this.router.get('/ips', this.usageRESTController.getTopIps)
|
||||||
|
this.router.get('/endpoints', this.usageRESTController.getTopEndpoints)
|
||||||
|
|
||||||
|
// Attach the Controller routes to the Koa app.
|
||||||
|
app.use(this.router.routes())
|
||||||
|
app.use(this.router.allowedMethods())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// module.exports = BchRouter
|
||||||
|
export default UsageRouter
|
||||||
@@ -124,7 +124,7 @@ class UserRESTControllerLib {
|
|||||||
|
|
||||||
ctx.body = { users }
|
ctx.body = { users }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
wlogger.error('Error in users/controller.js/getUsers(): '.err)
|
wlogger.error('Error in users/controller.js/getUsers(): ', err)
|
||||||
ctx.throw(422, err.message)
|
ctx.throw(422, err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import UserRESTControllerLib from './controller.js'
|
|||||||
|
|
||||||
import Validators from '../middleware/validators.js'
|
import Validators from '../middleware/validators.js'
|
||||||
|
|
||||||
|
import config from '../../../../config/index.js'
|
||||||
|
|
||||||
let _this
|
let _this
|
||||||
|
|
||||||
class UserRouter {
|
class UserRouter {
|
||||||
@@ -34,6 +36,7 @@ class UserRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encapsulate dependencies.
|
// Encapsulate dependencies.
|
||||||
|
this.config = config
|
||||||
this.userRESTController = new UserRESTControllerLib(dependencies)
|
this.userRESTController = new UserRESTControllerLib(dependencies)
|
||||||
this.validators = new Validators()
|
this.validators = new Validators()
|
||||||
|
|
||||||
@@ -52,7 +55,7 @@ class UserRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define the routes and attach the controller.
|
// Define the routes and attach the controller.
|
||||||
this.router.post('/', this.userRESTController.createUser)
|
this.router.post('/', this.createUser)
|
||||||
this.router.get('/', this.getAll)
|
this.router.get('/', this.getAll)
|
||||||
this.router.get('/:id', this.getById)
|
this.router.get('/:id', this.getById)
|
||||||
this.router.put('/:id', this.updateUser)
|
this.router.put('/:id', this.updateUser)
|
||||||
@@ -63,13 +66,21 @@ class UserRouter {
|
|||||||
app.use(this.router.allowedMethods())
|
app.use(this.router.allowedMethods())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createUser (ctx, next) {
|
||||||
|
if (process.env.DISABLE_NEW_ACCOUNTS) {
|
||||||
|
await _this.validators.ensureAdmin(ctx, next)
|
||||||
|
}
|
||||||
|
await _this.userRESTController.createUser(ctx, next)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
async getAll (ctx, next) {
|
async getAll (ctx, next) {
|
||||||
await _this.validators.ensureUser(ctx, next)
|
await _this.validators.ensureAdmin(ctx, next)
|
||||||
await _this.userRESTController.getUsers(ctx, next)
|
await _this.userRESTController.getUsers(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById (ctx, next) {
|
async getById (ctx, next) {
|
||||||
await _this.validators.ensureUser(ctx, next)
|
await _this.validators.ensureTargetUserOrAdmin(ctx, next)
|
||||||
await _this.userRESTController.getUser(ctx, next)
|
await _this.userRESTController.getUser(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
/*
|
/*
|
||||||
This Controller library is concerned with timer-based functions that are
|
This Controller library is concerned with timer-based functions that are
|
||||||
kicked off periodicially.
|
kicked off periodically.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Used to retain scope of 'this', when the scope is lost.
|
import config from '../../config/index.js'
|
||||||
let _this
|
|
||||||
|
|
||||||
class TimerControllers {
|
class TimerControllers {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
@@ -24,45 +23,195 @@ class TimerControllers {
|
|||||||
|
|
||||||
this.debugLevel = localConfig.debugLevel
|
this.debugLevel = localConfig.debugLevel
|
||||||
|
|
||||||
_this = this
|
// Constants
|
||||||
|
this.cleanUsageInterval = 60000 * 60 // 1 hour
|
||||||
|
this.backupUsageInterval = 60000 * 10 // 10 minutes
|
||||||
|
this.newSmAccountsInterval = 60000 * 60 // 60 minutes
|
||||||
|
this.updateSmAccountsInterval = 60000 * 10 // 10 minutes
|
||||||
|
this.removeOlderDeletedChatsAndPostsInterval = 60000 * 60 * 1 // 1 hour
|
||||||
|
|
||||||
this.startTimers()
|
// Encapsulate dependencies
|
||||||
|
this.config = config
|
||||||
|
|
||||||
|
// Bind 'this' object to all subfunctions
|
||||||
|
this.startTimers = this.startTimers.bind(this)
|
||||||
|
this.gcOrders = this.gcOrders.bind(this)
|
||||||
|
this.gcOffers = this.gcOffers.bind(this)
|
||||||
|
this.checkDupOffers = this.checkDupOffers.bind(this)
|
||||||
|
this.loadOffers = this.loadOffers.bind(this)
|
||||||
|
this.cleanUsage = this.cleanUsage.bind(this)
|
||||||
|
this.backupUsage = this.backupUsage.bind(this)
|
||||||
|
this.newSmAccounts = this.newSmAccounts.bind(this)
|
||||||
|
this.updateSmAccounts = this.updateSmAccounts.bind(this)
|
||||||
|
this.removeOlderDeletedChatsAndPosts = this.removeOlderDeletedChatsAndPosts.bind(this)
|
||||||
|
// State
|
||||||
|
this.gcOrdersInt = null
|
||||||
|
this.gcOffersInt = null
|
||||||
|
this.checkDupOffersInt = null
|
||||||
|
this.loadOffersInt = null
|
||||||
|
this.newSmAccountsInt = null
|
||||||
|
this.updateSmAccountsInt = null
|
||||||
|
this.removeOlderDeletedChatsAndPostsInt = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start all the time-based controllers.
|
// Start all the time-based controllers.
|
||||||
startTimers () {
|
startTimers () {
|
||||||
setInterval(this.gcOrders, 60000 * 5)
|
this.gcOrdersInt = setInterval(this.gcOrders, 60000 * 5)
|
||||||
setInterval(this.gcOffers, 60000 * 5)
|
this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5)
|
||||||
setInterval(this.checkDupOffers, 60000 * 4.5)
|
// this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
|
||||||
|
this.loadOffersInt = setInterval(this.loadOffers, 60000 * 2)
|
||||||
|
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
|
||||||
|
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
|
||||||
|
this.newSmAccountsInt = setInterval(this.newSmAccounts, this.newSmAccountsInterval)
|
||||||
|
this.updateSmAccountsInt = setInterval(this.updateSmAccounts, this.updateSmAccountsInterval)
|
||||||
|
this.removeOlderDeletedChatsAndPostsInt = setInterval(this.removeOlderDeletedChatsAndPosts, this.removeOlderDeletedChatsAndPostsInterval)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTimers () {
|
||||||
|
clearInterval(this.gcOrdersInt)
|
||||||
|
clearInterval(this.gcOffersInt)
|
||||||
|
// clearInterval(this.checkDupOffers)
|
||||||
|
clearInterval(this.optimizeWalletHandle)
|
||||||
|
clearInterval(this.cleanUsageHandle)
|
||||||
|
clearInterval(this.backupUsageHandle)
|
||||||
|
clearInterval(this.newSmAccountsInt)
|
||||||
|
clearInterval(this.updateSmAccountsInt)
|
||||||
|
clearInterval(this.removeOlderDeletedChatsAndPostsInt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Garbage Collect the Orders.
|
// Garbage Collect the Orders.
|
||||||
gcOrders () {
|
gcOrders () {
|
||||||
try {
|
try {
|
||||||
_this.useCases.order.removeStaleOrders()
|
this.useCases.order.removeStaleOrders()
|
||||||
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Do not throw an error. This is a top-level function.
|
// Do not throw an error. This is a top-level function.
|
||||||
console.log('Error in timer-controllers.js/gcOrders(): ', err)
|
console.log('Error in timer-controllers.js/gcOrders(): ', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the usage state so that stats reflect the last 24 hours.
|
||||||
|
cleanUsage () {
|
||||||
|
try {
|
||||||
|
clearInterval(this.cleanUsageHandle)
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
console.log(`cleanUsage() Timer Controller executing at ${now.toLocaleString()}`)
|
||||||
|
|
||||||
|
this.useCases.usage.cleanUsage()
|
||||||
|
|
||||||
|
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in time-controller.js/cleanUsage(): ', err)
|
||||||
|
|
||||||
|
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Garbage Collect the Offers.
|
// Garbage Collect the Offers.
|
||||||
gcOffers () {
|
gcOffers () {
|
||||||
try {
|
try {
|
||||||
_this.useCases.offer.removeStaleOffers()
|
this.useCases.offer.removeStaleOffers()
|
||||||
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Do not throw an error. This is a top-level function.
|
// Do not throw an error. This is a top-level function.
|
||||||
console.log('Error in timer-controllers.js/gcOffers(): ', err)
|
console.log('Error in timer-controllers.js/gcOffers(): ', err)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove duplicate Offers
|
// Remove duplicate Offers
|
||||||
checkDupOffers () {
|
checkDupOffers () {
|
||||||
try {
|
try {
|
||||||
_this.useCases.offer.removeDuplicateOffers()
|
this.useCases.offer.removeDuplicateOffers()
|
||||||
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Do not throw an error. This is a top-level function.
|
// Do not throw an error. This is a top-level function.
|
||||||
console.log('Error in timer-controllers.js/checkDupOffers(): ', err)
|
console.log('Error in timer-controllers.js/checkDupOffers(): ', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load offers From nostr .
|
||||||
|
async loadOffers () {
|
||||||
|
try {
|
||||||
|
await this.useCases.offer.loadOffers()
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
// Do not throw an error. This is a top-level function.
|
||||||
|
console.log('Error in timer-controllers.js/loadOffers(): ', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup the usage stats to the database
|
||||||
|
async backupUsage () {
|
||||||
|
try {
|
||||||
|
clearInterval(this.backupUsageHandle)
|
||||||
|
|
||||||
|
console.log('backupUsage() Timer Controller executing at ', new Date().toLocaleString())
|
||||||
|
|
||||||
|
// Clear the database of old usage data.
|
||||||
|
await this.useCases.usage.clearUsage()
|
||||||
|
|
||||||
|
// Save the current usage snapshot to the database.
|
||||||
|
await this.useCases.usage.saveUsage()
|
||||||
|
|
||||||
|
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in time-controller.js/backupUsage(): ', err)
|
||||||
|
|
||||||
|
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
|
||||||
|
|
||||||
|
// Note: Do not throw an error. This is a top-level function.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for new Social Media Accounts.
|
||||||
|
async newSmAccounts () {
|
||||||
|
try {
|
||||||
|
await this.useCases.smAccount.checkForNewSmAccounts()
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in time-controller.js/newSmAccounts(): ', err)
|
||||||
|
|
||||||
|
// Note: Do not throw an error. This is a top-level function.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the Social Media Accounts.
|
||||||
|
async updateSmAccounts () {
|
||||||
|
try {
|
||||||
|
await this.useCases.smAccount.updateSmAccounts()
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in time-controller.js/updateSmAccounts(): ', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove older deleted chats and posts.
|
||||||
|
async removeOlderDeletedChatsAndPosts () {
|
||||||
|
try {
|
||||||
|
// console.log('removeOlderDeletedChatsAndPosts() Timer Controller executing at ', new Date().toLocaleString())
|
||||||
|
clearInterval(this.removeOlderDeletedChatsAndPostsInt)
|
||||||
|
await this.useCases.nostr.removeOlderDeletedChats()
|
||||||
|
await this.useCases.nostr.removeOlderDeletedPosts()
|
||||||
|
this.removeOlderDeletedChatsAndPostsInt = setInterval(this.removeOlderDeletedChatsAndPosts, this.removeOlderDeletedChatsAndPostsInterval)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in time-controller.js/removeOlderDeletedChatsAndPosts(): ', err)
|
||||||
|
this.removeOlderDeletedChatsAndPostsInt = setInterval(this.removeOlderDeletedChatsAndPosts, this.removeOlderDeletedChatsAndPostsInterval)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Counter Offer Entity
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CounterOfferEntity {
|
||||||
|
validate (offerData = {}) {
|
||||||
|
console.log('counter offer data input validation ', offerData)
|
||||||
|
// Throw an error if input object does not have a data property
|
||||||
|
if (!offerData.data) {
|
||||||
|
throw new Error(
|
||||||
|
'Input to counterOffer.validate() must be an object with a data property.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
nostrEventId,
|
||||||
|
takerAddr,
|
||||||
|
takerNpub,
|
||||||
|
makerNpub,
|
||||||
|
counterOfferAddr,
|
||||||
|
counterOfferUtxo,
|
||||||
|
takerOfferUtxo,
|
||||||
|
tokenId
|
||||||
|
} = offerData.data
|
||||||
|
|
||||||
|
// Input Validation
|
||||||
|
if (!nostrEventId || typeof nostrEventId !== 'string') {
|
||||||
|
throw new Error("Property 'nostrEventId' must be a string.")
|
||||||
|
}
|
||||||
|
// Next validations are optional, to allow old counter offers without the data.
|
||||||
|
if (takerAddr && typeof takerAddr !== 'string') {
|
||||||
|
throw new Error("Property 'takerAddr' must be a string.")
|
||||||
|
}
|
||||||
|
if (takerNpub && typeof takerNpub !== 'string') {
|
||||||
|
throw new Error("Property 'takerNpub' must be a string.")
|
||||||
|
}
|
||||||
|
if (counterOfferAddr && typeof counterOfferAddr !== 'string') {
|
||||||
|
throw new Error("Property 'counterOfferAddr' must be a string.")
|
||||||
|
}
|
||||||
|
if (counterOfferUtxo && typeof counterOfferUtxo !== 'string') {
|
||||||
|
throw new Error("Property 'counterOfferUtxo' must be a string.")
|
||||||
|
}
|
||||||
|
if (takerOfferUtxo && typeof takerOfferUtxo !== 'string') {
|
||||||
|
throw new Error("Property 'takerOfferUtxo' must be a string.")
|
||||||
|
}
|
||||||
|
if (tokenId && typeof tokenId !== 'string') {
|
||||||
|
throw new Error("Property 'tokenId' must be a string.")
|
||||||
|
}
|
||||||
|
if (makerNpub && typeof makerNpub !== 'string') {
|
||||||
|
throw new Error("Property 'makerNpub' must be a string.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatedCounterOfferData = {
|
||||||
|
nostrEventId,
|
||||||
|
takerAddr,
|
||||||
|
takerNpub,
|
||||||
|
counterOfferAddr,
|
||||||
|
counterOfferUtxo,
|
||||||
|
takerOfferUtxo,
|
||||||
|
tokenId,
|
||||||
|
makerNpub
|
||||||
|
}
|
||||||
|
|
||||||
|
return validatedCounterOfferData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CounterOfferEntity
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
DeletedChat Entity
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DeletedChat {
|
||||||
|
validate (data = {}) {
|
||||||
|
const { eventId } = data
|
||||||
|
// Input Validation
|
||||||
|
if (!eventId || typeof eventId !== 'string') {
|
||||||
|
throw new Error("Property 'eventId' must be a string!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeletedChat
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
DeletedPost Entity
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DeletedPost {
|
||||||
|
validate (data = {}) {
|
||||||
|
const { eventId } = data
|
||||||
|
// Input Validation
|
||||||
|
if (!eventId || typeof eventId !== 'string') {
|
||||||
|
throw new Error("Property 'eventId' must be a string!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeletedPost
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
/*
|
/*
|
||||||
Entry Entity
|
Entry Entity
|
||||||
|
|
||||||
|
TODO: Explain what an entry is and how it works.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Entry {
|
class Entry {
|
||||||
validate ({
|
validate ({
|
||||||
entry,
|
entry,
|
||||||
|
|||||||
+29
-5
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Offer Entity
|
Offer Entity
|
||||||
An ffer is created when a new Signal is detected via the P2WDB webhook.
|
An offer is created when a new Signal is detected via the P2WDB webhook.
|
||||||
It's destroyed when the UTXO described in the Signal has been detected as spent.
|
It's destroyed when the UTXO described in the Signal has been detected as spent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -17,6 +17,8 @@ class OfferEntity {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log('offer entity validate() offerData: ', offerData)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messageType,
|
messageType,
|
||||||
messageClass,
|
messageClass,
|
||||||
@@ -29,8 +31,12 @@ class OfferEntity {
|
|||||||
utxoVout,
|
utxoVout,
|
||||||
offerStatus,
|
offerStatus,
|
||||||
makerAddr,
|
makerAddr,
|
||||||
// ticker,
|
makerNpub,
|
||||||
tokenType
|
ticker,
|
||||||
|
tokenType,
|
||||||
|
nostrEventId,
|
||||||
|
operatorAddress,
|
||||||
|
operatorPercentage
|
||||||
} = offerData.data
|
} = offerData.data
|
||||||
|
|
||||||
// Input Validation
|
// Input Validation
|
||||||
@@ -73,6 +79,21 @@ class OfferEntity {
|
|||||||
if (!tokenType || typeof tokenType !== 'number') {
|
if (!tokenType || typeof tokenType !== 'number') {
|
||||||
throw new Error("Property 'tokenType' must be a number.")
|
throw new Error("Property 'tokenType' must be a number.")
|
||||||
}
|
}
|
||||||
|
if (!nostrEventId || typeof nostrEventId !== 'string') {
|
||||||
|
throw new Error("Property 'nostrEventId' must be a string.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the operator address and percentage to the offer.
|
||||||
|
if (!operatorAddress || typeof operatorAddress !== 'string') {
|
||||||
|
throw new Error("Property 'operatorAddress' must be a string.")
|
||||||
|
}
|
||||||
|
if (!operatorPercentage || typeof operatorPercentage !== 'number') {
|
||||||
|
throw new Error("Property 'operatorPercentage' must be a number.")
|
||||||
|
}
|
||||||
|
// Validate npub type if it exist
|
||||||
|
if (makerNpub && typeof makerNpub !== 'string') {
|
||||||
|
throw new Error("Property 'makerNpub' must be a string.")
|
||||||
|
}
|
||||||
|
|
||||||
// Convert the timestamp to a number.
|
// Convert the timestamp to a number.
|
||||||
let timestamp = new Date(offerData.timestamp)
|
let timestamp = new Date(offerData.timestamp)
|
||||||
@@ -95,9 +116,12 @@ class OfferEntity {
|
|||||||
p2wdbHash: offerData.hash,
|
p2wdbHash: offerData.hash,
|
||||||
offerStatus: offerStatus || this.offerStatus[0],
|
offerStatus: offerStatus || this.offerStatus[0],
|
||||||
makerAddr,
|
makerAddr,
|
||||||
// ticker,
|
ticker,
|
||||||
tokenType
|
tokenType,
|
||||||
|
nostrEventId,
|
||||||
|
makerNpub
|
||||||
}
|
}
|
||||||
|
// console.log('offer entity validatedOfferData: ', validatedOfferData)
|
||||||
|
|
||||||
return validatedOfferData
|
return validatedOfferData
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Entry use-case library.
|
||||||
|
|
||||||
|
TODO: Explain what an entry is and how it works.
|
||||||
|
*/
|
||||||
|
|
||||||
// local libraries
|
// local libraries
|
||||||
import wlogger from '../adapters/wlogger.js'
|
import wlogger from '../adapters/wlogger.js'
|
||||||
@@ -30,11 +35,12 @@ class EntryLib {
|
|||||||
if (!isValidSignature) {
|
if (!isValidSignature) {
|
||||||
throw new Error('Invalid signature')
|
throw new Error('Invalid signature')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify psf tokens balance
|
// Verify psf tokens balance
|
||||||
// const psfBalance = await this.bch.getPSFTokenBalance(
|
// const psfBalance = await this.bch.getPSFTokenBalance(
|
||||||
// entryEntity.slpAddress
|
// entryEntity.slpAddress
|
||||||
// )
|
// )
|
||||||
|
// NOTE: this.bch.getPSFTokenBalance , does not exist , should be created?
|
||||||
|
|
||||||
const psfBalance = 1
|
const psfBalance = 1
|
||||||
|
|
||||||
if (psfBalance < 10) {
|
if (psfBalance < 10) {
|
||||||
|
|||||||
+10
-6
@@ -4,10 +4,14 @@
|
|||||||
https://troutsblog.com/blog/clean-architecture
|
https://troutsblog.com/blog/clean-architecture
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Local libraries
|
||||||
import UserUseCases from './user.js'
|
import UserUseCases from './user.js'
|
||||||
import EntryUseCases from './entry.js'
|
import EntryUseCases from './entry.js'
|
||||||
import OfferUseCases from './offer/index.js'
|
import OfferUseCases from './offer/index.js'
|
||||||
import OrderUseCases from './order.js'
|
import OrderUseCases from './order.js'
|
||||||
|
import { UsageUseCases } from './usage-use-cases.js'
|
||||||
|
import SmAccountUseCases from './smAccount-use-cases.js'
|
||||||
|
import NostrUseCases from './nostr-use-cases.js'
|
||||||
|
|
||||||
class UseCases {
|
class UseCases {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
@@ -24,19 +28,19 @@ class UseCases {
|
|||||||
this.order = new OrderUseCases(localConfig)
|
this.order = new OrderUseCases(localConfig)
|
||||||
localConfig.order = this.order
|
localConfig.order = this.order
|
||||||
this.offer = new OfferUseCases(localConfig)
|
this.offer = new OfferUseCases(localConfig)
|
||||||
|
this.usage = new UsageUseCases(localConfig)
|
||||||
|
this.smAccount = new SmAccountUseCases(localConfig)
|
||||||
|
this.nostr = new NostrUseCases(localConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run any startup Use Cases at the start of the app.
|
// Run any startup Use Cases at the start of the app.
|
||||||
async start () {
|
async start () {
|
||||||
// try {
|
// Load the usage stats from the database
|
||||||
|
await this.usage.loadUsage()
|
||||||
|
|
||||||
console.log('Async Use Cases have been started.')
|
console.log('Async Use Cases have been started.')
|
||||||
|
|
||||||
return true
|
return true
|
||||||
// } catch (err) {
|
|
||||||
// console.error('Error in use-cases/index.js/start()')
|
|
||||||
// // console.log(err)
|
|
||||||
// throw err
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
This library contains business-logic for dealing with deleted chats. Most of these
|
||||||
|
functions are called by the /nostr REST API endpoints.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import wlogger from '../adapters/wlogger.js'
|
||||||
|
import DeletedChatEntity from '../entities/deletedChat.js'
|
||||||
|
import DeletedPostEntity from '../entities/deletedPost.js'
|
||||||
|
|
||||||
|
class NostrUseCases {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of adapters must be passed in when instantiating Nostr Use Cases library.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Encapsulate dependencies
|
||||||
|
this.DeletedChatEntity = new DeletedChatEntity()
|
||||||
|
this.DeletedPostEntity = new DeletedPostEntity()
|
||||||
|
this.DeletedChatModel = this.adapters.localdb.DeletedChat
|
||||||
|
this.DeletedPostModel = this.adapters.localdb.DeletedPost
|
||||||
|
|
||||||
|
this.createDeletedChat = this.createDeletedChat.bind(this)
|
||||||
|
this.getDeletedChats = this.getDeletedChats.bind(this)
|
||||||
|
this.createDeletedPost = this.createDeletedPost.bind(this)
|
||||||
|
this.getDeletedPosts = this.getDeletedPosts.bind(this)
|
||||||
|
this.removeOlderDeletedChats = this.removeOlderDeletedChats.bind(this)
|
||||||
|
this.removeOlderDeletedPosts = this.removeOlderDeletedPosts.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new deleted chat model and add it to the Mongo database.
|
||||||
|
async createDeletedChat (deletedChatObj) {
|
||||||
|
try {
|
||||||
|
// Input Validation
|
||||||
|
|
||||||
|
const deletedChatEntity = this.DeletedChatEntity.validate(deletedChatObj)
|
||||||
|
const deletedChat = new this.DeletedChatModel(deletedChatEntity)
|
||||||
|
|
||||||
|
// Save the new model to the database.
|
||||||
|
await deletedChat.save()
|
||||||
|
return deletedChat
|
||||||
|
} catch (err) {
|
||||||
|
// console.log('createDeletedChat() error: ', err)
|
||||||
|
wlogger.error('Error in lib/nostr.js/createDeletedChat()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns an array of all deleted chats models in the Mongo database.
|
||||||
|
async getDeletedChats () {
|
||||||
|
try {
|
||||||
|
// Get deleted chats models.
|
||||||
|
const deletedChats = await this.DeletedChatModel.find({})
|
||||||
|
|
||||||
|
return deletedChats
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in lib/users.js/getAllDeletedChat()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new deleted post model and add it to the Mongo database.
|
||||||
|
async createDeletedPost (deletedPostObj) {
|
||||||
|
try {
|
||||||
|
// Input Validation
|
||||||
|
|
||||||
|
const deletedPostEntity = this.DeletedPostEntity.validate(deletedPostObj)
|
||||||
|
const deletedPost = new this.DeletedPostModel(deletedPostEntity)
|
||||||
|
|
||||||
|
// Save the new model to the database.
|
||||||
|
await deletedPost.save()
|
||||||
|
return deletedPost
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in lib/users.js/createDeletedPost()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns an array of all deleted posts models in the Mongo database.
|
||||||
|
async getDeletedPosts () {
|
||||||
|
try {
|
||||||
|
// Get deleted posts models.
|
||||||
|
const deletedPosts = await this.DeletedPostModel.find({})
|
||||||
|
|
||||||
|
return deletedPosts
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in lib/users.js/getDeletedPosts()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deleted chats that are older than 3 months.
|
||||||
|
async removeOlderDeletedChats () {
|
||||||
|
try {
|
||||||
|
const deletedChats = await this.DeletedChatModel.find({})
|
||||||
|
const threeMonthsAgo = new Date()
|
||||||
|
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
|
||||||
|
for (let i = 0; i < deletedChats.length; i++) {
|
||||||
|
const deletedChat = deletedChats[i]
|
||||||
|
if (deletedChat.createdAt.getTime() < threeMonthsAgo.getTime()) {
|
||||||
|
await deletedChat.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in lib/users.js/removeOlderDeletedChats()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deleted posts that are older than 3 months.
|
||||||
|
async removeOlderDeletedPosts () {
|
||||||
|
try {
|
||||||
|
const deletedPosts = await this.DeletedPostModel.find({})
|
||||||
|
const threeMonthsAgo = new Date()
|
||||||
|
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
|
||||||
|
for (let i = 0; i < deletedPosts.length; i++) {
|
||||||
|
const deletedPost = deletedPosts[i]
|
||||||
|
if (deletedPost.createdAt.getTime() < threeMonthsAgo.getTime()) {
|
||||||
|
await deletedPost.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in lib/users.js/removeOlderDeletedPosts()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NostrUseCases
|
||||||
+533
-126
@@ -19,6 +19,7 @@ import RetryQueue from '@chris.troutner/retry-queue'
|
|||||||
|
|
||||||
// Local libraries
|
// Local libraries
|
||||||
import OfferEntity from '../../entities/offer.js'
|
import OfferEntity from '../../entities/offer.js'
|
||||||
|
import CounterOfferEntity from '../../entities/counterOffer.js'
|
||||||
import config from '../../../config/index.js'
|
import config from '../../../config/index.js'
|
||||||
|
|
||||||
const DEFAULT_ENTRIES_PER_PAGE = 20
|
const DEFAULT_ENTRIES_PER_PAGE = 20
|
||||||
@@ -37,7 +38,7 @@ class OfferUseCases {
|
|||||||
this.orderUseCase = localConfig.order
|
this.orderUseCase = localConfig.order
|
||||||
if (!this.orderUseCase) {
|
if (!this.orderUseCase) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Instance of Offer Use Cases must be passed in when instantiating Offer Use Cases library.'
|
'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,24 +46,46 @@ class OfferUseCases {
|
|||||||
this.config = config
|
this.config = config
|
||||||
this.axios = axios
|
this.axios = axios
|
||||||
this.offerEntity = new OfferEntity()
|
this.offerEntity = new OfferEntity()
|
||||||
|
this.counterOfferEntity = new CounterOfferEntity()
|
||||||
this.OfferModel = this.adapters.localdb.Offer
|
this.OfferModel = this.adapters.localdb.Offer
|
||||||
|
this.CounterOfferModel = this.adapters.localdb.CounterOffer
|
||||||
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
|
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
|
||||||
|
|
||||||
// Bind 'this' object to functions
|
// Bind 'this' object to functions
|
||||||
|
this.createOffer = this.createOffer.bind(this)
|
||||||
this.detectNsfw = this.detectNsfw.bind(this)
|
this.detectNsfw = this.detectNsfw.bind(this)
|
||||||
|
this.categorizeToken = this.categorizeToken.bind(this)
|
||||||
|
this.listOffers = this.listOffers.bind(this)
|
||||||
|
this.listNftOffers = this.listNftOffers.bind(this)
|
||||||
|
this.listFungibleOffers = this.listFungibleOffers.bind(this)
|
||||||
|
this.takeOffer = this.takeOffer.bind(this)
|
||||||
|
this.ensureFunds = this.ensureFunds.bind(this)
|
||||||
|
this.findOfferByEvent = this.findOfferByEvent.bind(this)
|
||||||
|
this.findOfferByTxid = this.findOfferByTxid.bind(this)
|
||||||
|
this.acceptCounterOffer = this.acceptCounterOffer.bind(this)
|
||||||
|
this.removeDuplicateOffers = this.removeDuplicateOffers.bind(this)
|
||||||
|
this.removeStaleOffers = this.removeStaleOffers.bind(this)
|
||||||
|
this.flagOffer = this.flagOffer.bind(this)
|
||||||
|
this.loadOffers = this.loadOffers.bind(this)
|
||||||
|
this.listOffersByAddress = this.listOffersByAddress.bind(this)
|
||||||
|
this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
|
||||||
|
this.listCounterOffersByAddress = this.listCounterOffersByAddress.bind(this)
|
||||||
|
|
||||||
|
// State
|
||||||
|
this.seenOffers = []
|
||||||
|
this.seenCounterOffers = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method is called by the POST /offer REST API controller, which is
|
// This method is called by timer controller to load offers from a Nostr topic.
|
||||||
// triggered by a P2WDB webhook.
|
|
||||||
async createOffer (offerObj) {
|
async createOffer (offerObj) {
|
||||||
try {
|
try {
|
||||||
console.log('Use Case createOffer(offerObj): ', offerObj)
|
// console.log('Use Case createOffer(offerObj): ', offerObj)
|
||||||
|
|
||||||
// Return if Offer already exists in database with the same P2WDB CID.
|
// Return if Offer already exists in database with the same utxo transaction id.
|
||||||
try {
|
try {
|
||||||
await this.findOfferByHash(offerObj.hash)
|
await this.findOfferByTxid(offerObj.data.utxoTxid)
|
||||||
|
|
||||||
console.log('Offer already found in local database.')
|
// console.log('Offer already found in local database.')
|
||||||
return false
|
return false
|
||||||
} catch (err) { /* exit quietly */ }
|
} catch (err) { /* exit quietly */ }
|
||||||
|
|
||||||
@@ -71,53 +94,98 @@ class OfferUseCases {
|
|||||||
// Input Validation
|
// Input Validation
|
||||||
// TODO: This is a hack. Find a better way to protect against the corner-
|
// TODO: This is a hack. Find a better way to protect against the corner-
|
||||||
// case of counter-offers getting routed here.
|
// case of counter-offers getting routed here.
|
||||||
if (offerObj.data.dataType === 'counter-offer') {
|
// if (offerObj.data.dataType === 'counter-offer') {
|
||||||
console.log('WARN: Counter Offer innappropriately routed to createOffer()')
|
// console.log('WARN: Counter Offer innappropriately routed to createOffer()')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Quickly skip over offers that have already been processed.
|
||||||
|
const eventId = offerObj.data.nostrEventId
|
||||||
|
if (this.seenOffers.includes(eventId)) {
|
||||||
|
// console.log(`Offer with event ID ${eventId} already processed. Skipping.`)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
this.seenOffers.push(eventId)
|
||||||
|
|
||||||
// Verify that UTXO in offer is unspent. If it is spent, then ignore the
|
// Verify that UTXO in offer is unspent. If it is spent, then ignore the
|
||||||
// offer.
|
// offer.
|
||||||
// const txid = offerObj.data.utxoTxid
|
|
||||||
// const vout = offerObj.data.utxoVout
|
|
||||||
// const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
|
||||||
// txid,
|
|
||||||
// vout
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
const utxo = {
|
const utxo = {
|
||||||
tx_hash: offerObj.data.utxoTxid,
|
tx_hash: offerObj.data.utxoTxid,
|
||||||
tx_pos: offerObj.data.utxoVout
|
tx_pos: offerObj.data.utxoVout
|
||||||
}
|
}
|
||||||
// const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
// const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||||
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
||||||
|
// console.log('utxoStatus: ', utxoStatus)
|
||||||
console.log('utxoStatus: ', utxoStatus)
|
|
||||||
// if (utxoStatus === null) return false
|
// if (utxoStatus === null) return false
|
||||||
if (!utxoStatus) return false
|
if (!utxoStatus) {
|
||||||
|
console.log(`UTXO txid: ${offerObj.data.utxoTxid}, vout: ${offerObj.data.utxoVout} has been spent. Skipping.`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// A new offer gets a status of 'posted'
|
// A new offer gets a status of 'posted'
|
||||||
offerObj.data.offerStatus = 'posted'
|
offerObj.data.offerStatus = 'posted'
|
||||||
|
|
||||||
|
// Set timestamp
|
||||||
|
offerObj.timestamp = new Date().getTime()
|
||||||
|
|
||||||
const offerEntity = this.offerEntity.validate(offerObj)
|
const offerEntity = this.offerEntity.validate(offerObj)
|
||||||
console.log('offerEntity: ', offerEntity)
|
// console.log('offerEntity: ', offerEntity)
|
||||||
|
|
||||||
|
console.log(`New Offer for token ID ${offerEntity.tokenId} detected from Nostr post ${eventId}`)
|
||||||
|
|
||||||
// Get data about the token.
|
// Get data about the token.
|
||||||
const tokenId = offerEntity.tokenId
|
const tokenId = offerEntity.tokenId
|
||||||
// const tokenData = await this.adapters.wallet.bchWallet.getTokenData(tokenId)
|
let tokenData = null
|
||||||
const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
|
try {
|
||||||
console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in OfferUseCases/createOffer() getting token data: ', err.message)
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a 'display category' for the token. This will allow the
|
// Do additional data analysis if the token data was successfully retrieved.
|
||||||
// front end UI to figure out how to display the token.
|
if (tokenData) {
|
||||||
const displayCategory = this.categorizeToken(offerEntity, tokenData)
|
// Store the mutable and immutable data cids.
|
||||||
console.log('displayCategory: ', displayCategory)
|
const mutableDataCid = tokenData.mutableData
|
||||||
offerEntity.displayCategory = displayCategory
|
const immutableDataCid = tokenData.immutableData
|
||||||
|
offerEntity.mutableDataCid = mutableDataCid
|
||||||
|
offerEntity.immutableDataCid = immutableDataCid
|
||||||
|
offerEntity.tokenData = tokenData
|
||||||
|
|
||||||
// Detect if user set the NSFW flag.
|
// Get the mutable data from the cid if it exists.
|
||||||
let nsfw = false
|
if (mutableDataCid && typeof mutableDataCid === 'string') {
|
||||||
// nsfw = await this.detectNsfw(tokenData)
|
let mutableData = null
|
||||||
nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData)
|
try {
|
||||||
offerEntity.nsfw = nsfw
|
mutableData = await this.retryQueue.addToQueue(this.adapters.wallet.cid2json, mutableDataCid)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in OfferUseCases/createOffer() getting mutable data: ', err.message)
|
||||||
|
}
|
||||||
|
console.log('mutableData: ', mutableData)
|
||||||
|
|
||||||
|
if (mutableData) {
|
||||||
|
try {
|
||||||
|
offerEntity.tokenIconUrl = mutableData.tokenIcon
|
||||||
|
offerEntity.tokenCategories = mutableData.category
|
||||||
|
offerEntity.tokenTags = mutableData.tags
|
||||||
|
offerEntity.lastUpdatedTokenData = new Date().getTime()
|
||||||
|
|
||||||
|
offerEntity.userDataStr = JSON.stringify(mutableData.userData)
|
||||||
|
} catch (error) {
|
||||||
|
// skip error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('offerEntity: ', offerEntity)
|
||||||
|
|
||||||
|
// Generate a 'display category' for the token. This will allow the
|
||||||
|
// front end UI to figure out how to display the token.
|
||||||
|
const displayCategory = this.categorizeToken(offerEntity, tokenData)
|
||||||
|
console.log('displayCategory: ', displayCategory)
|
||||||
|
offerEntity.displayCategory = displayCategory
|
||||||
|
|
||||||
|
// Detect if user set the NSFW flag.
|
||||||
|
// const nsfw = false
|
||||||
|
// nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData)
|
||||||
|
// offerEntity.nsfw = nsfw
|
||||||
|
}
|
||||||
|
|
||||||
// Add offer to the local database.
|
// Add offer to the local database.
|
||||||
const offerModel = new this.OfferModel(offerEntity)
|
const offerModel = new this.OfferModel(offerEntity)
|
||||||
@@ -125,7 +193,7 @@ class OfferUseCases {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in createOffer()')
|
console.error('\n\nError in createOffer()', err.message, '\n\n', err)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,16 +215,29 @@ class OfferUseCases {
|
|||||||
|
|
||||||
// Retrieve the mutable data from Filecoin/IPFS.
|
// Retrieve the mutable data from Filecoin/IPFS.
|
||||||
// const url = `https://${cid}.ipfs.w3s.link/data.json`
|
// const url = `https://${cid}.ipfs.w3s.link/data.json`
|
||||||
const url = `${this.config.ipfsGateway}${cid}/data.json`
|
let mutableData = {}
|
||||||
const result = await axios.get(url)
|
|
||||||
const mutableData = result.data
|
try {
|
||||||
console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
|
// Try the conventional data URL.
|
||||||
|
const url = `${this.config.ipfsGateway}${cid}/data.json`
|
||||||
|
const result = await this.axios.get(url)
|
||||||
|
mutableData = result.data
|
||||||
|
// console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
|
||||||
|
} catch (err) {
|
||||||
|
// Try the newer data URL.
|
||||||
|
const url = `${this.config.ipfsGateway}${cid}`
|
||||||
|
const result = await this.axios.get(url)
|
||||||
|
mutableData = result.data
|
||||||
|
// console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
|
||||||
|
}
|
||||||
|
console.log('mutableData: ', mutableData)
|
||||||
|
|
||||||
// Logical tests
|
// Logical tests
|
||||||
const hasNsfw = !!mutableData.nsfw
|
const hasNsfw = !!mutableData.nsfw
|
||||||
const nsfwSetTrue = mutableData.nsfw === true
|
const nsfwSetTrue = mutableData.nsfw === true
|
||||||
const nsfwStringTrue = mutableData.nsfw === 'true'
|
const nsfwStringTrue = mutableData.nsfw === 'true'
|
||||||
const nsfwDetected = hasNsfw && (nsfwSetTrue || nsfwStringTrue)
|
const nsfwDetected = hasNsfw && (nsfwSetTrue || nsfwStringTrue)
|
||||||
|
// console.log(`hasNsfw: ${hasNsfw}, nsfwSetTrue: ${nsfwSetTrue}, nsfwStringTrue: ${nsfwStringTrue}, nsfwDetected: ${nsfwDetected}`)
|
||||||
|
|
||||||
if (nsfwDetected) {
|
if (nsfwDetected) {
|
||||||
console.log('NSFW flag set as true')
|
console.log('NSFW flag set as true')
|
||||||
@@ -215,11 +296,11 @@ class OfferUseCases {
|
|||||||
async listOffers (page = 0) {
|
async listOffers (page = 0) {
|
||||||
try {
|
try {
|
||||||
const data = await this.OfferModel.find({})
|
const data = await this.OfferModel.find({})
|
||||||
// Sort entries so newest entries show first.
|
// Sort entries so newest entries show first.
|
||||||
.sort('-timestamp')
|
.sort('-timestamp')
|
||||||
// Skip to the start of the selected page.
|
// Skip to the start of the selected page.
|
||||||
.skip(page * DEFAULT_ENTRIES_PER_PAGE)
|
.skip(page * DEFAULT_ENTRIES_PER_PAGE)
|
||||||
// Only return 20 results.
|
// Only return 20 results.
|
||||||
.limit(DEFAULT_ENTRIES_PER_PAGE)
|
.limit(DEFAULT_ENTRIES_PER_PAGE)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -229,17 +310,17 @@ class OfferUseCases {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listNftOffers (page = 0, nsfw = false) {
|
/* async listNftOffers (page = 0, nsfw = false) {
|
||||||
try {
|
try {
|
||||||
const data = await this.OfferModel.find({
|
const data = await this.OfferModel.find({
|
||||||
displayCategory: { $ne: 'fungible' },
|
displayCategory: { $ne: 'fungible' },
|
||||||
nsfw
|
nsfw
|
||||||
})
|
})
|
||||||
// Sort entries so newest entries show first.
|
// Sort entries so newest entries show first.
|
||||||
.sort('-timestamp')
|
.sort('-timestamp')
|
||||||
// Skip to the start of the selected page.
|
// Skip to the start of the selected page.
|
||||||
.skip(page * NFT_ENTRIES_PER_PAGE)
|
.skip(page * NFT_ENTRIES_PER_PAGE)
|
||||||
// Only return 20 results.
|
// Only return 20 results.
|
||||||
.limit(NFT_ENTRIES_PER_PAGE)
|
.limit(NFT_ENTRIES_PER_PAGE)
|
||||||
|
|
||||||
// console.log('listNftOffers() returning this data: ', data)
|
// console.log('listNftOffers() returning this data: ', data)
|
||||||
@@ -249,16 +330,52 @@ class OfferUseCases {
|
|||||||
console.error('Error in use-cases/offer/listNftOffers()')
|
console.error('Error in use-cases/offer/listNftOffers()')
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
async listNftOffers (page = 0, nsfw = false) {
|
||||||
|
try {
|
||||||
|
const query = {
|
||||||
|
displayCategory: { $ne: 'fungible' },
|
||||||
|
nsfw
|
||||||
|
}
|
||||||
|
// Total query offers
|
||||||
|
const totalOffers = await this.OfferModel.countDocuments(query)
|
||||||
|
// Total pages
|
||||||
|
const totalPages = Math.ceil(totalOffers / NFT_ENTRIES_PER_PAGE)
|
||||||
|
|
||||||
|
const data = await this.OfferModel.find(query)
|
||||||
|
// Sort entries so newest entries show first.
|
||||||
|
.sort('-timestamp')
|
||||||
|
// Skip to the start of the selected page.
|
||||||
|
.skip(page * NFT_ENTRIES_PER_PAGE)
|
||||||
|
// Only return 20 results.
|
||||||
|
.limit(NFT_ENTRIES_PER_PAGE)
|
||||||
|
|
||||||
|
// console.log('listNftOffers() returning this data: ', data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
pagination: {
|
||||||
|
currentPage: page,
|
||||||
|
totalPages,
|
||||||
|
totalOffers,
|
||||||
|
pageSize: NFT_ENTRIES_PER_PAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in use-cases/offer/listNftOffers()')
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listFungibleOffers (page = 0) {
|
async listFungibleOffers (page = 0) {
|
||||||
try {
|
try {
|
||||||
const data = await this.OfferModel.find({ displayCategory: 'fungible' })
|
const data = await this.OfferModel.find({ displayCategory: 'fungible' })
|
||||||
// Sort entries so newest entries show first.
|
// Sort entries so newest entries show first.
|
||||||
.sort('-timestamp')
|
.sort('-timestamp')
|
||||||
// Skip to the start of the selected page.
|
// Skip to the start of the selected page.
|
||||||
.skip(page * FUNGIBLE_ENTRIES_PER_PAGE)
|
.skip(page * FUNGIBLE_ENTRIES_PER_PAGE)
|
||||||
// Only return 20 results.
|
// Only return 20 results.
|
||||||
.limit(FUNGIBLE_ENTRIES_PER_PAGE)
|
.limit(FUNGIBLE_ENTRIES_PER_PAGE)
|
||||||
|
|
||||||
// console.log('listFungibleOffers() returning this data: ', data)
|
// console.log('listFungibleOffers() returning this data: ', data)
|
||||||
@@ -278,12 +395,12 @@ class OfferUseCases {
|
|||||||
// provide functionality to take less than the total amount of tokens offered
|
// provide functionality to take less than the total amount of tokens offered
|
||||||
// (offerInfo.numTokens). Taking less than the offered amount will be added
|
// (offerInfo.numTokens). Taking less than the offered amount will be added
|
||||||
// in the future.
|
// in the future.
|
||||||
async takeOffer (offerCid) {
|
async takeOffer (eventId) {
|
||||||
try {
|
try {
|
||||||
console.log('offerCid: ', offerCid)
|
if (!eventId || typeof eventId !== 'string') throw new Error('eventId must be a string')
|
||||||
|
|
||||||
// Get the Offer information
|
// Get the Offer information
|
||||||
const offerInfo = await this.findOfferByHash(offerCid)
|
const offerInfo = await this.findOfferByEvent(eventId)
|
||||||
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
|
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
|
||||||
|
|
||||||
// Ensure the offer is in a 'posted' state and not already 'taken'
|
// Ensure the offer is in a 'posted' state and not already 'taken'
|
||||||
@@ -292,19 +409,14 @@ class OfferUseCases {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify that UTXO for sale is unspent. Abort if it's been spent.
|
// Verify that UTXO for sale is unspent. Abort if it's been spent.
|
||||||
// const txid = offerInfo.utxoTxid
|
|
||||||
// const vout = offerInfo.utxoVout
|
|
||||||
|
|
||||||
const utxo = {
|
const utxo = {
|
||||||
tx_hash: offerInfo.utxoTxid,
|
tx_hash: offerInfo.utxoTxid,
|
||||||
tx_pos: offerInfo.utxoVout
|
tx_pos: offerInfo.utxoVout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note : should be added to retry-queue?
|
||||||
const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||||
// const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
// console.log('utxoStatus: ', utxoStatus)
|
||||||
// txid,
|
|
||||||
// vout
|
|
||||||
// )
|
|
||||||
console.log('utxoStatus: ', utxoStatus)
|
|
||||||
if (!utxoStatus) {
|
if (!utxoStatus) {
|
||||||
console.log(`utxo txid: ${offerInfo.utxoTxid}, vout: ${offerInfo.utxoVout}`)
|
console.log(`utxo txid: ${offerInfo.utxoTxid}, vout: ${offerInfo.utxoVout}`)
|
||||||
|
|
||||||
@@ -317,11 +429,12 @@ class OfferUseCases {
|
|||||||
await this.ensureFunds(offerInfo)
|
await this.ensureFunds(offerInfo)
|
||||||
|
|
||||||
// Get UTXOs.
|
// Get UTXOs.
|
||||||
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
|
// const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
|
||||||
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||||
|
|
||||||
// Calculate amount of sats to generate a counter offer.
|
// Calculate amount of sats to generate a counter offer.
|
||||||
let satsToMove = offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)
|
let satsToMove = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit))
|
||||||
|
console.log('satsToMove', satsToMove, offerInfo)
|
||||||
if (isNaN(satsToMove)) {
|
if (isNaN(satsToMove)) {
|
||||||
throw new Error('Could not calculate the amount of BCH to generate counter offer')
|
throw new Error('Could not calculate the amount of BCH to generate counter offer')
|
||||||
}
|
}
|
||||||
@@ -347,9 +460,9 @@ class OfferUseCases {
|
|||||||
// Create valid Offer object
|
// Create valid Offer object
|
||||||
const takenOfferInfo = Object.assign({}, offerInfo)
|
const takenOfferInfo = Object.assign({}, offerInfo)
|
||||||
takenOfferInfo.partialTxHex = partialTxHex
|
takenOfferInfo.partialTxHex = partialTxHex
|
||||||
delete takenOfferInfo.p2wdbHash
|
delete takenOfferInfo.nostrEventId
|
||||||
delete takenOfferInfo._id
|
delete takenOfferInfo._id
|
||||||
takenOfferInfo.offerHash = offerInfo.p2wdbHash
|
takenOfferInfo.offerHash = offerInfo.nostrEventId
|
||||||
|
|
||||||
// Add P2WDB specific flag for signaling that this is a new offer.
|
// Add P2WDB specific flag for signaling that this is a new offer.
|
||||||
takenOfferInfo.dataType = 'counter-offer'
|
takenOfferInfo.dataType = 'counter-offer'
|
||||||
@@ -357,19 +470,21 @@ class OfferUseCases {
|
|||||||
// Write offer info to the P2WDB
|
// Write offer info to the P2WDB
|
||||||
// TODO: This will trigger the webhook. Find some way of triggering the
|
// TODO: This will trigger the webhook. Find some way of triggering the
|
||||||
// webhook on new offers, but not on counteroffers
|
// webhook on new offers, but not on counteroffers
|
||||||
const p2wdbObj = {
|
const nostrData = {
|
||||||
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
|
// wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
|
||||||
data: takenOfferInfo,
|
data: takenOfferInfo
|
||||||
appId: this.config.p2wdbAppId
|
// appId: this.config.p2wdbAppId
|
||||||
}
|
}
|
||||||
const hash = await this.adapters.p2wdb.write(p2wdbObj)
|
const resultEventId = await this.adapters.nostr.post(JSON.stringify(nostrData))
|
||||||
|
|
||||||
|
const noteId = this.adapters.nostr.eventId2note(resultEventId)
|
||||||
|
|
||||||
// Delete the Offer from the database, so that the user doesn't attempt
|
// Delete the Offer from the database, so that the user doesn't attempt
|
||||||
// to take the offer more than once.
|
// to take the offer more than once.
|
||||||
offerInfo.remove()
|
// offerInfo.remove()
|
||||||
|
|
||||||
// Return the P2WDB CID
|
// Return the P2WDB CID
|
||||||
return hash
|
return { eventId: resultEventId, noteId }
|
||||||
|
|
||||||
// return 'fake-hash'
|
// return 'fake-hash'
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -392,15 +507,16 @@ class OfferUseCases {
|
|||||||
await this.adapters.wallet.bchWallet.initialize()
|
await this.adapters.wallet.bchWallet.initialize()
|
||||||
|
|
||||||
// Ensure the app wallet has enough funds to write to the P2WDB.
|
// Ensure the app wallet has enough funds to write to the P2WDB.
|
||||||
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
|
// Note : this validation should be deprecated for nostr functionality?
|
||||||
const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
|
// const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
|
||||||
if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
|
// const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
|
||||||
|
// if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
|
||||||
|
|
||||||
if (offerEntity.buyOrSell.includes('sell')) {
|
if (offerEntity.buyOrSell.includes('sell')) {
|
||||||
// Sell Offer
|
// Sell Offer
|
||||||
|
|
||||||
// Calculate the sats needed
|
// Calculate the sats needed
|
||||||
const satsNeeded = offerEntity.numTokens * parseInt(offerEntity.rateInBaseUnit)
|
const satsNeeded = Math.ceil(offerEntity.numTokens * parseInt(offerEntity.rateInBaseUnit))
|
||||||
if (isNaN(satsNeeded)) {
|
if (isNaN(satsNeeded)) {
|
||||||
throw new Error('Could not calculate sats needed!')
|
throw new Error('Could not calculate sats needed!')
|
||||||
}
|
}
|
||||||
@@ -413,7 +529,7 @@ class OfferUseCases {
|
|||||||
throw new Error('App wallet does not control enough BCH to purchase the tokens.')
|
throw new Error('App wallet does not control enough BCH to purchase the tokens.')
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
} else {
|
} else {
|
||||||
// Buy Offer
|
// Buy Offer
|
||||||
throw new Error('Buy offers are not supported yet.')
|
throw new Error('Buy offers are not supported yet.')
|
||||||
@@ -432,82 +548,217 @@ class OfferUseCases {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOfferByHash (p2wdbHash) {
|
// Retrieve an Order model from the database. Find it by its event Id.
|
||||||
|
async findOfferByEvent (nostrEventId) {
|
||||||
|
try {
|
||||||
|
if (typeof nostrEventId !== 'string' || !nostrEventId) {
|
||||||
|
throw new Error('nostrEventId must be a string')
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = await this.OfferModel.findOne({ nostrEventId })
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
throw new Error('offer not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderObject = order.toObject()
|
||||||
|
// return this.offerEntity.validateFromModel(offerObject)
|
||||||
|
|
||||||
|
return orderObject
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in findOfferByEvent()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOfferByTxid (utxoTxid) {
|
||||||
// try {
|
// try {
|
||||||
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
|
// try {
|
||||||
throw new Error('p2wdbHash must be a string')
|
if (typeof utxoTxid !== 'string' || !utxoTxid) {
|
||||||
|
throw new Error('utxoTxid must be a string')
|
||||||
}
|
}
|
||||||
|
|
||||||
const offer = await this.OfferModel.findOne({ p2wdbHash })
|
const offer = await this.OfferModel.findOne({ utxoTxid })
|
||||||
|
|
||||||
|
// TODO: Offer should be found by TXID, then if there is more than one
|
||||||
|
// result, they should be filtered by the vout property. That will leave
|
||||||
|
// one remaining UTXO.
|
||||||
|
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
throw new Error('offer not found')
|
throw new Error('offer not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
return offer
|
return offer
|
||||||
|
// } catch (error) {
|
||||||
// const offerObject = offer.toObject()
|
// // console.error('Error in use-cases/offer/findOfferByTxid(): ', error.message)
|
||||||
// return this.offerEntity.validateFromModel(offerObject)
|
// throw error
|
||||||
|
|
||||||
// return offerObject
|
|
||||||
// } catch (err) {
|
|
||||||
// // console.error('Error in findOffer(): ', err)
|
|
||||||
// throw err
|
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is called by the P2WDB webhook REST API handler. When a
|
// This function is called by loadOffers().
|
||||||
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
|
// It does due diligence on the Counter Offer, then signs
|
||||||
// to this function. It does due dilligence on the Counter Offer, then signs
|
|
||||||
// and broadcasts the transaction to accept the Counter Offer.
|
// and broadcasts the transaction to accept the Counter Offer.
|
||||||
async acceptCounterOffer (p2wdbData) {
|
async acceptCounterOffer (offerData) {
|
||||||
try {
|
try {
|
||||||
console.log(`acceptCounterOffer() p2wdbData: ${JSON.stringify(p2wdbData, null, 2)}`)
|
console.log('offerData', offerData)
|
||||||
|
// console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`)
|
||||||
|
|
||||||
|
// Quickly skip over offers that have already been processed.
|
||||||
|
const eventId = offerData.data.nostrEventId
|
||||||
|
|
||||||
|
// Create counter offer entity for nostr event id if it not exist
|
||||||
|
const existingCounterOffer = await this.CounterOfferModel.findOne(({ nostrEventId: eventId }))
|
||||||
|
if (!existingCounterOffer) {
|
||||||
|
// Add taker offer utxo to counter offer data.
|
||||||
|
offerData.data.takerOfferUtxo = offerData.data.utxoTxid
|
||||||
|
const counterOEntity = this.counterOfferEntity.validate(offerData)
|
||||||
|
// Create new counter offfer model and save it.
|
||||||
|
const counterOffer = new this.CounterOfferModel(counterOEntity)
|
||||||
|
await counterOffer.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.seenOffers.includes(eventId)) {
|
||||||
|
// console.log(`Offer with event ID ${eventId} already processed. Skipping.`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`New counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerData.data.nostrEventId)}`)
|
||||||
|
|
||||||
// See if this instance of bch-dex is managing the Order associated with
|
// See if this instance of bch-dex is managing the Order associated with
|
||||||
// the incoming Counter Offer.
|
// the incoming Counter Offer.
|
||||||
const orderHash = p2wdbData.data.offerHash
|
|
||||||
|
// Note : this should be handled by nostrEvent id or UtxoId?
|
||||||
|
// const orderHash = offerData.data.nostrEventId
|
||||||
let orderData = {}
|
let orderData = {}
|
||||||
try {
|
try {
|
||||||
orderData = await this.orderUseCase.findOrderByHash(orderHash)
|
orderData = await this.orderUseCase.findOrderByUtxo(offerData)
|
||||||
console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`)
|
console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Exiting.')
|
console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Skipping.\n')
|
||||||
|
|
||||||
|
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
|
||||||
|
this.seenOffers.push(eventId)
|
||||||
|
|
||||||
|
return 'N/A'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the UTXO for sale is valid.
|
||||||
|
let utxoStatus = null
|
||||||
|
try {
|
||||||
|
// Get the status of the UTXO associate with this Offer.
|
||||||
|
const utxo = {
|
||||||
|
tx_hash: offerData.data.utxoTxid,
|
||||||
|
tx_pos: offerData.data.utxoVout
|
||||||
|
}
|
||||||
|
// console.log(`Checking this UTXO: ${JSON.stringify(utxo, null, 2)}`)
|
||||||
|
|
||||||
|
// utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||||
|
utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
||||||
|
console.log('utxoStatus: ', utxoStatus)
|
||||||
|
} catch (err) {
|
||||||
|
console.log('acceptCounterOffer() err validating UTXO: ', err)
|
||||||
|
|
||||||
|
// Handle corner case of bad-data in the Offer model.
|
||||||
|
if (err.isAxiosError) {
|
||||||
|
console.log('Error trying to contact wallet service: ', err)
|
||||||
|
return 'N/A'
|
||||||
|
} else {
|
||||||
|
return 'N/A'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the Offer UTXO is spent, exit.
|
||||||
|
if (utxoStatus === false) {
|
||||||
|
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
|
||||||
|
this.seenOffers.push(eventId)
|
||||||
|
|
||||||
|
console.log(`Aborting Counter Offer from Event ID ${eventId}`)
|
||||||
|
console.log(`https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(eventId)}`)
|
||||||
|
|
||||||
return 'N/A'
|
return 'N/A'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deserialize the partially signed transaction.
|
// Deserialize the partially signed transaction.
|
||||||
const txHex = p2wdbData.data.partialTxHex
|
const txHex = offerData.data.partialTxHex
|
||||||
const txObj = await this.adapters.wallet.deseralizeTx(txHex)
|
const txObj = await this.adapters.wallet.deseralizeTx(txHex)
|
||||||
console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`)
|
console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`)
|
||||||
|
|
||||||
// Ensure the 3rd output (vout=2) contains the required amount of BCH.
|
// Ensure the 3rd output (vout=2) contains the required amount of BCH.
|
||||||
const satsToReceive = orderData.numTokens * parseInt(orderData.rateInBaseUnit)
|
const satsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit))
|
||||||
|
console.log('Ceil', satsToReceive)
|
||||||
if (isNaN(satsToReceive)) {
|
if (isNaN(satsToReceive)) {
|
||||||
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
|
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
|
||||||
}
|
}
|
||||||
const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value)
|
const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value)
|
||||||
|
|
||||||
const hasRequiredAmount = satsOut === satsToReceive
|
const hasRequiredAmount = satsOut === satsToReceive
|
||||||
|
|
||||||
if (!hasRequiredAmount) {
|
if (!hasRequiredAmount) {
|
||||||
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
|
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the Counter Offer has an output for the Operator of bch-dex.
|
||||||
|
if (!txObj.vout[3]) {
|
||||||
|
console.log('The Counter Offer does not have an output for the Operator.')
|
||||||
|
|
||||||
|
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
|
||||||
|
this.seenOffers.push(eventId)
|
||||||
|
|
||||||
|
return 'N/A'
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the 3rd output (vout=2) is going to the maker address specified
|
// Ensure the 3rd output (vout=2) is going to the maker address specified
|
||||||
// in the Offer.
|
// in the Offer.
|
||||||
|
|
||||||
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
|
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
|
||||||
const makerAddr = orderData.makerAddr
|
const makerAddr = orderData.makerAddr
|
||||||
const hasCorrectAddr = makerAddr === addrInCounterOffer
|
const hasCorrectAddr = makerAddr === addrInCounterOffer
|
||||||
|
|
||||||
if (!hasCorrectAddr) {
|
if (!hasCorrectAddr) {
|
||||||
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
|
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the 4th output (vout=3) is going to the operator address specified in the Offer.
|
||||||
|
// NOTE: This is the operator of the bch-dex intance, NOT the seller of the token.
|
||||||
|
const operatorAddr = orderData.operatorAddress
|
||||||
|
|
||||||
|
const hasCorrectOperatorAddr = operatorAddr === txObj.vout[3].scriptPubKey.addresses[0]
|
||||||
|
if (!hasCorrectOperatorAddr) {
|
||||||
|
throw new Error(`The Counter Offer has an output address of ${txObj.vout[3].scriptPubKey.addresses[0]}, which does not match the Operator address of ${operatorAddr} in the Offer.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the 4th output (vout=3) contains the required amount of BCH.
|
||||||
|
const operatorSatsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[3].value)
|
||||||
|
let estimatedOperatorFee = Math.floor(txObj.vout[3].value * orderData.operatorPercentage / 100)
|
||||||
|
if (estimatedOperatorFee < 546) estimatedOperatorFee = 546
|
||||||
|
console.log('operatorSatsOut: ', operatorSatsOut, 'estimatedOperatorFee: ', estimatedOperatorFee)
|
||||||
|
if (operatorSatsOut < estimatedOperatorFee) {
|
||||||
|
console.log(`Skipping: The Counter Offer has an output of ${operatorSatsOut}, which is less than the estimated operator fee of ${estimatedOperatorFee}.`)
|
||||||
|
|
||||||
|
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
|
||||||
|
this.seenOffers.push(eventId)
|
||||||
|
|
||||||
|
return 'N/A'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the User ID from the Order model.
|
||||||
|
const userId = orderData.userId
|
||||||
|
|
||||||
|
// Get the User model from the database.
|
||||||
|
this.UserModel = this.adapters.localdb.Users
|
||||||
|
const user = await this.UserModel.findById(userId)
|
||||||
|
console.log('user: ', user)
|
||||||
|
|
||||||
|
// Get the HD index from the Order model.
|
||||||
|
const hdIndex = orderData.hdIndex
|
||||||
|
|
||||||
// Sign and broadcast the transaction.
|
// Sign and broadcast the transaction.
|
||||||
const txid = await this.adapters.wallet.completeTx(txHex, orderData.hdIndex)
|
const txid = await this.adapters.wallet.completeTx(txHex, hdIndex, user.mnemonic)
|
||||||
console.log('txid: ', txid)
|
console.log('txid: ', txid)
|
||||||
|
|
||||||
return txid
|
return txid
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in acceptCounterOffer()')
|
console.error('Error in acceptCounterOffer(): ', err)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -569,12 +820,6 @@ class OfferUseCases {
|
|||||||
|
|
||||||
let utxoStatus = null
|
let utxoStatus = null
|
||||||
try {
|
try {
|
||||||
// Get the status of the UTXO associate with this Offer.
|
|
||||||
// utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
|
||||||
// thisOffer.utxoTxid,
|
|
||||||
// thisOffer.utxoVout
|
|
||||||
// )
|
|
||||||
|
|
||||||
// Get the status of the UTXO associate with this Offer.
|
// Get the status of the UTXO associate with this Offer.
|
||||||
const utxo = {
|
const utxo = {
|
||||||
tx_hash: thisOffer.utxoTxid,
|
tx_hash: thisOffer.utxoTxid,
|
||||||
@@ -582,7 +827,8 @@ class OfferUseCases {
|
|||||||
}
|
}
|
||||||
// console.log(`Checking this UTXO: ${JSON.stringify(utxo, null, 2)}`)
|
// console.log(`Checking this UTXO: ${JSON.stringify(utxo, null, 2)}`)
|
||||||
|
|
||||||
utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
// utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||||
|
utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
||||||
// console.log('utxoStatus: ', utxoStatus)
|
// console.log('utxoStatus: ', utxoStatus)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Handle corner case of bad-data in the Offer model.
|
// Handle corner case of bad-data in the Offer model.
|
||||||
@@ -599,12 +845,14 @@ class OfferUseCases {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the Offer UTXO is spent, delete the Offer model.
|
// If the Offer UTXO is spent, delete the Offer model.
|
||||||
if (!utxoStatus) {
|
if (utxoStatus === false) {
|
||||||
|
// console.log('utxoStatus: ', utxoStatus)
|
||||||
console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`)
|
console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`)
|
||||||
await thisOffer.remove()
|
await thisOffer.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the Offer is older than 7 days, delete it.
|
// TODO: Instead of deleting the offer, send the token back to the Makers wallet.
|
||||||
|
// If the Offer is older than a threshold, delete it.
|
||||||
const nowMs = now.getTime()
|
const nowMs = now.getTime()
|
||||||
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
|
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
|
||||||
const eightWeeksAgo = nowMs - eightWeeks
|
const eightWeeksAgo = nowMs - eightWeeks
|
||||||
@@ -619,32 +867,191 @@ class OfferUseCases {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async flagOffer (flagData) {
|
// Flag offers as NSFW.
|
||||||
console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`)
|
async flagOffer (flagData = {}) {
|
||||||
|
try {
|
||||||
|
if (!flagData.data) throw new Error('"data" property is required')
|
||||||
|
|
||||||
const p2wdbHash = flagData.data.p2wdbHash
|
console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`)
|
||||||
|
|
||||||
// Get the offer from the database.
|
const eventId = flagData.data.nostrEventId
|
||||||
const offer = await this.findOfferByHash(p2wdbHash)
|
|
||||||
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
|
|
||||||
|
|
||||||
if (!offer) {
|
// Get the offer from the database.
|
||||||
throw new Error(`Offer ${p2wdbHash} not found in the database.`)
|
const offer = await this.findOfferByEvent(eventId)
|
||||||
|
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
|
||||||
|
|
||||||
|
if (!offer) {
|
||||||
|
throw new Error(`Offer ${eventId} not found in the database.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the raw flag data to the database model.
|
||||||
|
offer.flags.push(flagData)
|
||||||
|
|
||||||
|
// If flag count is 3 or more, mark the Offer as NSFW
|
||||||
|
const flagCnt = offer.flags.length
|
||||||
|
if (flagCnt >= 3) {
|
||||||
|
offer.nsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated offer data to the database.
|
||||||
|
await offer.save()
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in flagOffer(): ', error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add the raw flag data to the database model.
|
// Get offers data from nostr.
|
||||||
offer.flags.push(flagData)
|
async loadOffers () {
|
||||||
|
try {
|
||||||
|
// Retrieve offers array.
|
||||||
|
const offers = await this.adapters.nostr.read()
|
||||||
|
// console.log('offers: ', offers)
|
||||||
|
|
||||||
// If flag count is 3 or more, mark the Offer as NSFW
|
for (let i = 0; i < offers.length; i++) {
|
||||||
const flagCnt = offer.flags.length
|
try {
|
||||||
if (flagCnt >= 3) {
|
const offer = offers[i]
|
||||||
offer.nsfw = true
|
|
||||||
|
// offer data
|
||||||
|
const offerObj = JSON.parse(offer.content)
|
||||||
|
|
||||||
|
// Append the Nostr Event ID to the offer object
|
||||||
|
offerObj.data.nostrEventId = offer.eventId
|
||||||
|
// console.log('loadOffers() offerObj: ', offerObj)
|
||||||
|
|
||||||
|
if (offerObj.data.dataType === 'offer') {
|
||||||
|
// Try to create new offer
|
||||||
|
await this.createOffer(offerObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offerObj.data.dataType === 'counter-offer') {
|
||||||
|
// console.log('Counter offer detected: ', offerObj)
|
||||||
|
// console.log('Counter offer detected: ', offerObj.data.nostrEventId)
|
||||||
|
// console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
|
||||||
|
await this.acceptCounterOffer(offerObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore the post if it doesn't fit the above filters.
|
||||||
|
} catch (error) {
|
||||||
|
/* exit quietly */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in loadOffers(): ', error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save the updated offer data to the database.
|
// List all offers being made by a given address.
|
||||||
await offer.save()
|
async listOffersByAddress (addr) {
|
||||||
|
try {
|
||||||
|
const offers = await this.OfferModel.find({ makerAddr: addr })
|
||||||
|
return offers
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in listOffersByAddress(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
// update offer model mutable data
|
||||||
|
async syncOfferMutableData (tokenId) {
|
||||||
|
try {
|
||||||
|
// validate input
|
||||||
|
if (!tokenId || typeof tokenId !== 'string') {
|
||||||
|
throw new Error('tokenId must be a string!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate existing offer with the associated tokenId
|
||||||
|
const offer = await this.OfferModel.findOne({ tokenId })
|
||||||
|
if (!offer) throw new Error('Associated offer not found!')
|
||||||
|
|
||||||
|
// Verify last update timestamp. This prevents users from spamming the API with requests.
|
||||||
|
// It only updates the mutable data if it has been more than 5 minutes since the last update.
|
||||||
|
const lastUpdateTs = Number(offer.lastUpdatedTokenData)
|
||||||
|
const now = new Date().getTime()
|
||||||
|
const period = 5
|
||||||
|
if (lastUpdateTs) {
|
||||||
|
// add 5 minutes to the last update
|
||||||
|
const lastUpdate = new Date(lastUpdateTs)
|
||||||
|
console.log('lastUpdate', lastUpdate)
|
||||||
|
lastUpdate.setMinutes(lastUpdate.getMinutes() + period)
|
||||||
|
|
||||||
|
console.log(new Date().getMinutes() + ' ' + lastUpdate.getMinutes())
|
||||||
|
// if now is less than the lastUpdate + 5 minutos , them skip.
|
||||||
|
if (now < lastUpdate.getTime()) {
|
||||||
|
console.log('Skipping , token lastUpdate is less than 5 minutes.')
|
||||||
|
return offer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenData = null
|
||||||
|
try {
|
||||||
|
tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
|
||||||
|
} catch (err) {
|
||||||
|
// Dev Note: If getTokenData() fails, the code below will
|
||||||
|
console.error('Error in OfferUseCases/createOffer() getting token data: ', err.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do additional data analysis if the token data was successfully retrieved.
|
||||||
|
if (tokenData) {
|
||||||
|
// Store the mutable and immutable data cids.
|
||||||
|
const mutableDataCid = tokenData.mutableData
|
||||||
|
const immutableDataCid = tokenData.immutableData
|
||||||
|
offer.mutableDataCid = mutableDataCid
|
||||||
|
offer.immutableDataCid = immutableDataCid
|
||||||
|
offer.tokenData = tokenData
|
||||||
|
|
||||||
|
// Get the mutable data from the cid if it exists.
|
||||||
|
if (mutableDataCid && typeof mutableDataCid === 'string') {
|
||||||
|
let mutableData = null
|
||||||
|
try {
|
||||||
|
mutableData = await this.retryQueue.addToQueue(this.adapters.wallet.cid2json, mutableDataCid)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in OfferUseCases/createOffer() getting mutable data: ', err.message)
|
||||||
|
}
|
||||||
|
console.log('mutableData: ', mutableData)
|
||||||
|
|
||||||
|
if (mutableData) {
|
||||||
|
try {
|
||||||
|
offer.tokenIconUrl = mutableData.tokenIcon
|
||||||
|
offer.tokenCategories = mutableData.category
|
||||||
|
offer.tokenTags = mutableData.tags
|
||||||
|
|
||||||
|
offer.userDataStr = JSON.stringify(mutableData.userData)
|
||||||
|
} catch (error) {
|
||||||
|
// skip error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Save update time stamp
|
||||||
|
offer.lastUpdatedTokenData = new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated offer data to the database.
|
||||||
|
await offer.save()
|
||||||
|
|
||||||
|
// Return the updated offer data.
|
||||||
|
return offer
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in syncOfferMutableData(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listCounterOffersByAddress (takerAddr) {
|
||||||
|
try {
|
||||||
|
if (!takerAddr || typeof takerAddr !== 'string') {
|
||||||
|
throw new Error('takerAddr must be a string')
|
||||||
|
}
|
||||||
|
|
||||||
|
const counterOffers = await this.CounterOfferModel.find({ takerAddr })
|
||||||
|
return counterOffers
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in listCounterOffersByAddress(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+145
-36
@@ -10,6 +10,8 @@ import wlogger from '../adapters/wlogger.js'
|
|||||||
import OrderEntity from '../entities/order.js'
|
import OrderEntity from '../entities/order.js'
|
||||||
import config from '../../config/index.js'
|
import config from '../../config/index.js'
|
||||||
|
|
||||||
|
const DEFAULT_ENTRIES_PER_PAGE = 20
|
||||||
|
|
||||||
class OrderLib {
|
class OrderLib {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
// console.log('User localConfig: ', localConfig)
|
// console.log('User localConfig: ', localConfig)
|
||||||
@@ -23,84 +25,123 @@ class OrderLib {
|
|||||||
// Encapsulate dependencies
|
// Encapsulate dependencies
|
||||||
this.orderEntity = new OrderEntity()
|
this.orderEntity = new OrderEntity()
|
||||||
this.OrderModel = this.adapters.localdb.Order
|
this.OrderModel = this.adapters.localdb.Order
|
||||||
|
this.UserModel = this.adapters.localdb.Users
|
||||||
this.bch = this.adapters.bch
|
this.bch = this.adapters.bch
|
||||||
this.config = config
|
this.config = config
|
||||||
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
|
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
|
||||||
|
|
||||||
// Bind subfunctions to the 'this' object.
|
// Bind subfunctions to the 'this' object.
|
||||||
this.ensureFunds = this.ensureFunds.bind(this)
|
this.ensureFunds = this.ensureFunds.bind(this)
|
||||||
|
this.createOrder = this.createOrder.bind(this)
|
||||||
|
this.findOrderByUtxo = this.findOrderByUtxo.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new order model and add it to the Mongo database.
|
// Create a new order model and add it to the Mongo database.
|
||||||
async createOrder (entryObj) {
|
async createOrder (entryObj) {
|
||||||
try {
|
try {
|
||||||
console.log('createOrder(entryObj): ', entryObj)
|
// console.log('createOrder(entryObj): ', entryObj)
|
||||||
if (!entryObj) return false
|
if (!entryObj) return false
|
||||||
|
|
||||||
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
|
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
|
||||||
|
|
||||||
|
// Get the user data from the database, for the user creating the new Order.
|
||||||
|
const user = await this.UserModel.findById(entryObj.userId)
|
||||||
|
if (!user) throw new Error('user not found')
|
||||||
|
|
||||||
|
console.log(`Using FullStack.cash: ${this.config.useFullStackCash}`)
|
||||||
|
const advancedConfig = {}
|
||||||
|
if (this.config.useFullStackCash) {
|
||||||
|
advancedConfig.interface = 'rest-api'
|
||||||
|
advancedConfig.restURL = this.config.apiServer
|
||||||
|
advancedConfig.apiToken = this.config.apiToken
|
||||||
|
} else {
|
||||||
|
advancedConfig.interface = 'consumer-api'
|
||||||
|
advancedConfig.restURL = this.config.consumerUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instantiate minimal-slp-wallet with the user's mnemonic.
|
||||||
|
const BchWallet = this.adapters.wallet.BchWallet
|
||||||
|
const userWallet = new BchWallet(user.mnemonic, advancedConfig)
|
||||||
|
|
||||||
|
// Wait for wallet to initialize.
|
||||||
|
await userWallet.walletInfoPromise
|
||||||
|
await userWallet.initialize()
|
||||||
|
|
||||||
// Specify the address to send payment.
|
// Specify the address to send payment.
|
||||||
entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress
|
console.log('userWallet.walletInfo: ', userWallet.walletInfo)
|
||||||
|
entryObj.makerAddr = userWallet.walletInfo.cashAddress
|
||||||
console.log('entryObj.makerAddr: ', entryObj.makerAddr)
|
console.log('entryObj.makerAddr: ', entryObj.makerAddr)
|
||||||
|
// console.log('entryObj.makerAddr: ', entryObj.makerAddr)
|
||||||
|
|
||||||
// Input Validation
|
// Input Validation
|
||||||
const orderEntity = this.orderEntity.inputValidate(entryObj)
|
const orderEntity = this.orderEntity.inputValidate(entryObj)
|
||||||
console.log('orderEntity: ', orderEntity)
|
// console.log('orderEntity: ', orderEntity)
|
||||||
|
|
||||||
// Ensure sufficient tokens exist to create the order.
|
// Optimize the wallet to speed up working with it.
|
||||||
// await this.ensureFunds(orderEntity)
|
console.log('Optimizing wallet before creating new order.')
|
||||||
await this.retryQueue.addToQueue(this.ensureFunds, orderEntity)
|
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
|
||||||
|
await userWallet.optimize()
|
||||||
|
|
||||||
// Get Ticker for token ID.
|
// Get Ticker for token ID.
|
||||||
// const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
|
const tokenData = await userWallet.getTxData([entryObj.tokenId])
|
||||||
const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
|
|
||||||
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
||||||
entryObj.ticker = tokenData[0].tokenTicker
|
orderEntity.ticker = tokenData[0].tokenTicker
|
||||||
|
|
||||||
// Move the tokens to holding address.
|
// Move the tokens to holding address.
|
||||||
const moveObj = {
|
const moveObj = {
|
||||||
tokenId: orderEntity.tokenId,
|
tokenId: orderEntity.tokenId,
|
||||||
qty: orderEntity.numTokens
|
qty: orderEntity.numTokens,
|
||||||
|
wallet: userWallet
|
||||||
}
|
}
|
||||||
// const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
|
// const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
|
||||||
const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokens, moveObj)
|
const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokensFromCustomWallet, moveObj)
|
||||||
console.log('utxoInfo: ', utxoInfo)
|
// console.log('utxoInfo: ', utxoInfo)
|
||||||
|
|
||||||
// Update the UTXO store for the wallet.
|
// Update the UTXO store for the wallet.
|
||||||
await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000)
|
await userWallet.bchjs.Util.sleep(3000)
|
||||||
// await this.adapters.wallet.bchWallet.getUtxos()
|
// await this.adapters.wallet.bchWallet.getUtxos()
|
||||||
await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
|
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
|
||||||
|
await userWallet.initialize()
|
||||||
|
|
||||||
// Update the order with the new UTXO information.
|
// Update the order with the new UTXO information.
|
||||||
orderEntity.utxoTxid = utxoInfo.txid
|
orderEntity.utxoTxid = utxoInfo.txid
|
||||||
orderEntity.utxoVout = utxoInfo.vout
|
orderEntity.utxoVout = utxoInfo.vout
|
||||||
orderEntity.tokenType = utxoInfo.tokenType
|
orderEntity.tokenType = utxoInfo.tokenType
|
||||||
|
|
||||||
// Add P2WDB specific flag for signaling that this is a new offer.
|
// Add flag to signal that this is a new offer.
|
||||||
orderEntity.dataType = 'offer'
|
orderEntity.dataType = 'offer'
|
||||||
|
orderEntity.userId = user._id
|
||||||
|
|
||||||
// Add order to P2WDB.
|
// Add the operator address and percentage to the order.
|
||||||
const p2wdbObj = {
|
orderEntity.operatorAddress = this.config.operatorAddress
|
||||||
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
|
orderEntity.operatorPercentage = this.config.operatorPercentage
|
||||||
data: orderEntity,
|
|
||||||
appId: this.config.p2wdbAppId
|
// Convert the Seller's BCH private key to a Nostr npub. This is used to identify the seller in Nostr.
|
||||||
|
orderEntity.makerNpub = this.adapters.nostr.privKeyToNpub(userWallet.walletInfo.privateKey)
|
||||||
|
|
||||||
|
// Post the new Order information to Nostr under the topic set in the
|
||||||
|
// config file.
|
||||||
|
const postObj = {
|
||||||
|
data: orderEntity
|
||||||
}
|
}
|
||||||
// const hash = await this.adapters.p2wdb.write(p2wdbObj)
|
const postMsg = JSON.stringify(postObj)
|
||||||
const hash = await this.retryQueue.addToQueue(this.adapters.p2wdb.write, p2wdbObj)
|
const eventId = await this.adapters.nostr.post(postMsg)
|
||||||
// console.log('hash: ', hash)
|
// console.log('hash: ', hash)
|
||||||
|
|
||||||
// Create a MongoDB model to hold the Order
|
// Create a MongoDB model to hold the Order
|
||||||
orderEntity.hdIndex = utxoInfo.hdIndex
|
orderEntity.hdIndex = utxoInfo.hdIndex
|
||||||
orderEntity.p2wdbHash = hash
|
orderEntity.nostrEventId = eventId
|
||||||
|
|
||||||
console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`)
|
console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`)
|
||||||
const order = new this.OrderModel(orderEntity)
|
const order = new this.OrderModel(orderEntity)
|
||||||
await order.save()
|
await order.save()
|
||||||
|
|
||||||
return hash
|
const noteId = this.adapters.nostr.eventId2note(eventId)
|
||||||
|
|
||||||
|
return { eventId, noteId }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
|
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
|
||||||
wlogger.error('Error in use-cases/order.js/createOrder())')
|
wlogger.error('Error in use-cases/order.js/createOrder(): ', err)
|
||||||
console.log('error entryObj: ', entryObj)
|
console.log('error entryObj: ', entryObj)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
@@ -115,6 +156,7 @@ class OrderLib {
|
|||||||
|
|
||||||
// Update the wallet balance
|
// Update the wallet balance
|
||||||
await this.adapters.wallet.bchWallet.getUtxos()
|
await this.adapters.wallet.bchWallet.getUtxos()
|
||||||
|
// console.log(`wallet UTXOs: ${JSON.stringify(this.adapters.wallet.bchWallet.utxos.utxoStore, null, 2)}`)
|
||||||
|
|
||||||
// Ensure the app wallet has enough funds to write to the P2WDB.
|
// Ensure the app wallet has enough funds to write to the P2WDB.
|
||||||
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
|
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
|
||||||
@@ -130,12 +172,10 @@ class OrderLib {
|
|||||||
|
|
||||||
// Combine Fungible and NFT token UTXOs.
|
// Combine Fungible and NFT token UTXOs.
|
||||||
let tokenUtxos = utxos.slpUtxos.type1.tokens.concat(utxos.slpUtxos.nft.tokens)
|
let tokenUtxos = utxos.slpUtxos.type1.tokens.concat(utxos.slpUtxos.nft.tokens)
|
||||||
|
|
||||||
// Get token UTXOs that match the token in the order.
|
// Get token UTXOs that match the token in the order.
|
||||||
tokenUtxos = tokenUtxos.filter(
|
tokenUtxos = tokenUtxos.filter(
|
||||||
x => x.tokenId === orderEntity.tokenId
|
x => x.tokenId === orderEntity.tokenId
|
||||||
)
|
)
|
||||||
console.log('tokenUtxos: ', tokenUtxos)
|
|
||||||
|
|
||||||
// Get the total amount of tokens in the wallet that match the token
|
// Get the total amount of tokens in the wallet that match the token
|
||||||
// in the order.
|
// in the order.
|
||||||
@@ -151,7 +191,7 @@ class OrderLib {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
} else {
|
} else {
|
||||||
// Buy Order
|
// Buy Order
|
||||||
throw new Error('Buy orders are not supported yet.')
|
throw new Error('Buy orders are not supported yet.')
|
||||||
@@ -164,14 +204,14 @@ class OrderLib {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve an Order model from the database. Find it by its P2WDB CID.
|
// Retrieve an Order model from the database. Find it by its event Id.
|
||||||
async findOrderByHash (p2wdbHash) {
|
async findOrderByEvent (nostrEventId) {
|
||||||
try {
|
try {
|
||||||
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
|
if (typeof nostrEventId !== 'string' || !nostrEventId) {
|
||||||
throw new Error('p2wdbHash must be a string')
|
throw new Error('nostrEventId must be a string')
|
||||||
}
|
}
|
||||||
|
|
||||||
const order = await this.OrderModel.findOne({ p2wdbHash })
|
const order = await this.OrderModel.findOne({ nostrEventId })
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new Error('order not found')
|
throw new Error('order not found')
|
||||||
@@ -182,7 +222,38 @@ class OrderLib {
|
|||||||
|
|
||||||
return orderObject
|
return orderObject
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in findOrder()')
|
console.error('Error in findOrderByEvent()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve an Order model from the database. Find it by its UTXO (TXID & Vout)
|
||||||
|
async findOrderByUtxo (offerData = {}) {
|
||||||
|
try {
|
||||||
|
// console.log('findOrderByUtxo() offerData: ', offerData)
|
||||||
|
|
||||||
|
// const order = await this.OrderModel.findOne({ nostrEventId })
|
||||||
|
const order = await this.OrderModel.findOne({ utxoTxid: offerData.data.utxoTxid, utxoVout: offerData.data.utxoVout })
|
||||||
|
// const order = await this.OrderModel.findOne({ utxoTxid: offerData.data.utxoTxid })
|
||||||
|
// const order = await this.OrderModel.findOne({ tokenId: offerData.data.tokenId })
|
||||||
|
console.log('\nfindOrderByUtxo() order: ', order)
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
throw new Error('order not found')
|
||||||
|
}
|
||||||
|
if (!order.nostrEventId) {
|
||||||
|
throw new Error('order not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderObject = order.toObject()
|
||||||
|
// return this.offerEntity.validateFromModel(offerObject)
|
||||||
|
|
||||||
|
return orderObject
|
||||||
|
} catch (err) {
|
||||||
|
if (!err.message.toString().includes('order not found')) {
|
||||||
|
console.error('Error in findOrderByUtxo(): ', err)
|
||||||
|
}
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +287,8 @@ class OrderLib {
|
|||||||
tx_hash: thisOrder.utxoTxid,
|
tx_hash: thisOrder.utxoTxid,
|
||||||
tx_pos: thisOrder.utxoVout
|
tx_pos: thisOrder.utxoVout
|
||||||
}
|
}
|
||||||
utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
// utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||||
|
utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
||||||
// console.log('utxoStatus: ', utxoStatus)
|
// console.log('utxoStatus: ', utxoStatus)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Handle corner case of bad-data in the Order model.
|
// Handle corner case of bad-data in the Order model.
|
||||||
@@ -233,7 +305,8 @@ class OrderLib {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the Order UTXO is spent, delete the Order model.
|
// If the Order UTXO is spent, delete the Order model.
|
||||||
if (!utxoStatus) {
|
if (utxoStatus === false) {
|
||||||
|
// console.log('utxoStatus: ', utxoStatus)
|
||||||
console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`)
|
console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`)
|
||||||
await thisOrder.remove()
|
await thisOrder.remove()
|
||||||
}
|
}
|
||||||
@@ -243,6 +316,42 @@ class OrderLib {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listOrders (page = 0) {
|
||||||
|
try {
|
||||||
|
const data = await this.OrderModel.find({})
|
||||||
|
|
||||||
|
// Sort entries so newest entries show first.
|
||||||
|
.sort('-timestamp')
|
||||||
|
// Skip to the start of the selected page.
|
||||||
|
.skip(page * DEFAULT_ENTRIES_PER_PAGE)
|
||||||
|
// Only return 20 results.
|
||||||
|
.limit(DEFAULT_ENTRIES_PER_PAGE)
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in use-cases/order/listOrders()')
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete an Order model by sending the token back to the root address. This
|
||||||
|
// will allow the garbage collectors to delete the Order and the Offer.
|
||||||
|
async deleteOrder (nostrEventId) {
|
||||||
|
try {
|
||||||
|
// Find the order by the given hash.
|
||||||
|
const order = await this.findOrderByEvent(nostrEventId)
|
||||||
|
console.log('order: ', order)
|
||||||
|
|
||||||
|
// Reclaim the tokens
|
||||||
|
const txid = await this.adapters.wallet.reclaimTokens(order)
|
||||||
|
|
||||||
|
return txid
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in use-cases/order/deleteOrder()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrderLib
|
export default OrderLib
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
Social Media Account Use Cases.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class smAccountUseCases {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// console.log('User localConfig: ', localConfig)
|
||||||
|
|
||||||
|
// Dependency injection.
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of adapters must be passed in when instantiating SM Account Use Cases library.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind 'this' object to all methods
|
||||||
|
this.checkForNewSmAccounts = this.checkForNewSmAccounts.bind(this)
|
||||||
|
this.listAccounts = this.listAccounts.bind(this)
|
||||||
|
this.getAccountByNpub = this.getAccountByNpub.bind(this)
|
||||||
|
this.getAccountByBchAddr = this.getAccountByBchAddr.bind(this)
|
||||||
|
this.updateSmAccounts = this.updateSmAccounts.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for new Social Media Accounts.
|
||||||
|
async checkForNewSmAccounts () {
|
||||||
|
try {
|
||||||
|
console.log('Checking for new Social Media Accounts...')
|
||||||
|
|
||||||
|
const messages = await this.adapters.nostr.readGlobalFeed()
|
||||||
|
console.log('messages: ', messages)
|
||||||
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
const msg = messages[i]
|
||||||
|
// const { content, id, tags, npub, pubkey } = msg
|
||||||
|
const { tags, npub, pubkey } = msg
|
||||||
|
let bchAddr = null
|
||||||
|
// console.log('msg: ', JSON.stringify(msg, null, 2))
|
||||||
|
|
||||||
|
// Find the 'u' tag if it exists
|
||||||
|
const uTag = tags.find(tag => tag[0] === 'u')
|
||||||
|
|
||||||
|
// If 'u' tag exists and contains a BCH address
|
||||||
|
if (uTag && uTag[1].includes('bitcoincash:')) {
|
||||||
|
bchAddr = uTag[1]
|
||||||
|
// console.log('BCH address found:', bchAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SmAccount = await this.adapters.localdb.SmAccount
|
||||||
|
|
||||||
|
if (bchAddr) {
|
||||||
|
const smAccount = await SmAccount.findOne({ npub })
|
||||||
|
if (!smAccount) {
|
||||||
|
console.log(`Creating new Social Media Account with npub: ${npub} and bchAddr: ${bchAddr}`)
|
||||||
|
const newSmAccount = new SmAccount({ npub, bchAddr, pubkey })
|
||||||
|
await newSmAccount.save()
|
||||||
|
} else {
|
||||||
|
console.log(`Social Media Account with npub: ${npub} already exists`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in smAccount-use-cases.js/checkForNewSmAccounts(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Social Media Accounts.
|
||||||
|
async listAccounts (page = 0) {
|
||||||
|
try {
|
||||||
|
const SmAccount = await this.adapters.localdb.SmAccount
|
||||||
|
const accounts = await SmAccount.find({}).skip(page * 10).limit(10)
|
||||||
|
return accounts
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in smAccount-use-cases.js/listAccounts(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a Social Media Account by Npub.
|
||||||
|
async getAccountByNpub (npub) {
|
||||||
|
try {
|
||||||
|
const SmAccount = await this.adapters.localdb.SmAccount
|
||||||
|
const account = await SmAccount.findOne({ npub })
|
||||||
|
return account
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in smAccount-use-cases.js/getAccountByNpub(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a Social Media Account by BCH address.
|
||||||
|
async getAccountByBchAddr (bchAddr) {
|
||||||
|
try {
|
||||||
|
const SmAccount = await this.adapters.localdb.SmAccount
|
||||||
|
const account = await SmAccount.findOne({ bchAddr })
|
||||||
|
console.log('Account found: ', account)
|
||||||
|
|
||||||
|
return account
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in smAccount-use-cases.js/getAccountByBchAddr(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update metadata about social media accounts being tracked.
|
||||||
|
async updateSmAccounts () {
|
||||||
|
try {
|
||||||
|
console.log('Updating Social Media Accounts...')
|
||||||
|
const SmAccount = await this.adapters.localdb.SmAccount
|
||||||
|
const accounts = await SmAccount.find({})
|
||||||
|
for (let i = 0; i < accounts.length; i++) {
|
||||||
|
const account = accounts[i]
|
||||||
|
// console.log(`Account ${i}: `, account)
|
||||||
|
|
||||||
|
// Get the number of accounts following this account
|
||||||
|
const pubkey = account.pubkey
|
||||||
|
const followList = await this.adapters.nostr.getFollowers({ pubkey })
|
||||||
|
// console.log(`Follow list ${i}: `, JSON.stringify(followList, null, 2))
|
||||||
|
|
||||||
|
// Update the follower count for this account.
|
||||||
|
const followerCnt = followList.length
|
||||||
|
await SmAccount.updateOne({ _id: account._id }, { $set: { followerCnt } })
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in smAccount-use-cases.js/updateSmAccounts(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default smAccountUseCases
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
/*
|
||||||
|
Use Case library for tracking usage. This library contains business logic
|
||||||
|
for tracking the usage of REST API and JSON RPC calls. This library is used
|
||||||
|
by admins to keep an eye on how many API calls were made in a 24-hour and
|
||||||
|
1-hour time period.
|
||||||
|
|
||||||
|
Usage stats are held in memory. But they are periodically backed up to the
|
||||||
|
Mongo database. On startup, the usage stats are loaded from the database.
|
||||||
|
This allows the usage stats to be persisted across restarts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This global variable is used to share data between the REST middleware and
|
||||||
|
// the Usage Use Case class instance.
|
||||||
|
let restCalls = []
|
||||||
|
|
||||||
|
class UsageUseCases {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
// console.log('User localConfig: ', localConfig)
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of adapters must be passed in when instantiating Usage Use Cases library.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate dependencies
|
||||||
|
this.UsageModel = this.adapters.localdb.Usage
|
||||||
|
|
||||||
|
// Bind 'this' object to all subfunctions
|
||||||
|
this.cleanUsage = this.cleanUsage.bind(this)
|
||||||
|
this.getRestSummary = this.getRestSummary.bind(this)
|
||||||
|
this.getTopIps = this.getTopIps.bind(this)
|
||||||
|
this.getTopEndpoints = this.getTopEndpoints.bind(this)
|
||||||
|
this.clearUsage = this.clearUsage.bind(this)
|
||||||
|
this.saveUsage = this.saveUsage.bind(this)
|
||||||
|
this.loadUsage = this.loadUsage.bind(this)
|
||||||
|
|
||||||
|
// State
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the state by removing entries that are older than 24 hours. This
|
||||||
|
// ensures stats reflect only the last 24 hours.
|
||||||
|
// This function is called by a Timer Controller.
|
||||||
|
cleanUsage () {
|
||||||
|
try {
|
||||||
|
const now = new Date()
|
||||||
|
const twentyFourHoursAgo = now.getTime() - (60000 * 60 * 24)
|
||||||
|
|
||||||
|
console.log('cleanUsage() now: ', now)
|
||||||
|
console.log('cleanUsage() restCalls.length before filtering: ', restCalls.length)
|
||||||
|
restCalls = restCalls.filter(x => x.timestamp > twentyFourHoursAgo)
|
||||||
|
console.log('cleanUsage() restCalls.length after filtering: ', restCalls.length)
|
||||||
|
|
||||||
|
return restCalls
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in usage-use-cases.js/cleanUsage()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the calls to a REST API
|
||||||
|
getRestSummary (inObj = {}) {
|
||||||
|
try {
|
||||||
|
console.log(`getRestSummary(): There have been ${restCalls.length} REST calls`)
|
||||||
|
|
||||||
|
return restCalls.length
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in usage-use-cases.js/getRestSummary()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the top 20 IP addresses from the stats.
|
||||||
|
getTopIps () {
|
||||||
|
try {
|
||||||
|
const ips = restCalls.map(x => x.ip)
|
||||||
|
// Create a Map to count occurrences of each IP address string
|
||||||
|
const countMap = new Map()
|
||||||
|
ips.forEach(ip => {
|
||||||
|
countMap.set(ip, (countMap.get(ip) || 0) + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert the Map into an array of objects with `str` and `cnt` properties
|
||||||
|
const result = Array.from(countMap, ([ip, cnt]) => ({ ip, cnt }))
|
||||||
|
|
||||||
|
// Sort the results by the `cnt` property in descending order
|
||||||
|
result.sort((a, b) => b.cnt - a.cnt)
|
||||||
|
|
||||||
|
// Ensure the result has at most 20 elements
|
||||||
|
return result.slice(0, 20)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in usage-use-cases.js/getTopIps()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the top 20 most consumed endpoints.
|
||||||
|
getTopEndpoints () {
|
||||||
|
try {
|
||||||
|
const endpoints = restCalls.map(x => `${x.method} ${x.url}`)
|
||||||
|
|
||||||
|
// Create a Map to count occurrences of each IP address string
|
||||||
|
const countMap = new Map()
|
||||||
|
endpoints.forEach(endpoint => {
|
||||||
|
countMap.set(endpoint, (countMap.get(endpoint) || 0) + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert the Map into an array of objects with `str` and `cnt` properties
|
||||||
|
const result = Array.from(countMap, ([endpoint, cnt]) => ({ endpoint, cnt }))
|
||||||
|
|
||||||
|
// Sort the results by the `cnt` property in descending order
|
||||||
|
result.sort((a, b) => b.cnt - a.cnt)
|
||||||
|
|
||||||
|
// Ensure the result has at most 20 elements
|
||||||
|
return result.slice(0, 20)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in usage-use-cases.js/getTopEndpoints()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the usage database data
|
||||||
|
async clearUsage () {
|
||||||
|
try {
|
||||||
|
await this.UsageModel.deleteMany({})
|
||||||
|
|
||||||
|
// Debugging: verify the database is empty
|
||||||
|
// Delete this code after debugging
|
||||||
|
const usage = await this.UsageModel.find({})
|
||||||
|
console.log('clearUsage() usage: ', usage)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in usage-use-cases.js/clearUsage()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the usage data to the database
|
||||||
|
async saveUsage (inObj = {}) {
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < restCalls.length; i++) {
|
||||||
|
const thisRestCall = restCalls[i]
|
||||||
|
|
||||||
|
// Debugging: delete this code after debugging
|
||||||
|
// if (i === 5) {
|
||||||
|
// console.log('saveUsage() thisRestCall: ', thisRestCall)
|
||||||
|
// }
|
||||||
|
|
||||||
|
const usageData = {
|
||||||
|
ip: thisRestCall.ip,
|
||||||
|
url: thisRestCall.url,
|
||||||
|
method: thisRestCall.method,
|
||||||
|
timestamp: thisRestCall.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
const usage = new this.UsageModel(usageData)
|
||||||
|
await usage.save()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in usage-use-cases.js/saveUsage()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load usage data from the database
|
||||||
|
async loadUsage () {
|
||||||
|
try {
|
||||||
|
const usage = await this.UsageModel.find({})
|
||||||
|
// console.log('usage: ', usage)
|
||||||
|
|
||||||
|
// Debugging: delete this code after debugging
|
||||||
|
if (usage[5]) {
|
||||||
|
console.log('loadUsage() usage[5]: ', usage[5])
|
||||||
|
}
|
||||||
|
|
||||||
|
restCalls = usage
|
||||||
|
return usage
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in usage-use-cases.js/loadUsage(): ', err)
|
||||||
|
return false
|
||||||
|
// throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This Koa middleware is called any time there is a REST API. It logs the
|
||||||
|
// details from the request object.
|
||||||
|
function usageMiddleware () {
|
||||||
|
return async (ctx, next) => {
|
||||||
|
try {
|
||||||
|
await next()
|
||||||
|
|
||||||
|
// console.log('ctx.request: ', ctx.request)
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
const reqObj = {
|
||||||
|
ip: ctx.request.ip,
|
||||||
|
url: ctx.request.url,
|
||||||
|
method: ctx.request.method,
|
||||||
|
timestamp: now.getTime()
|
||||||
|
}
|
||||||
|
// console.log('reqObj: ', reqObj)
|
||||||
|
|
||||||
|
restCalls.push(reqObj)
|
||||||
|
} catch (err) {
|
||||||
|
ctx.status = err.status || 500
|
||||||
|
ctx.body = err.message
|
||||||
|
ctx.app.emit('error', err, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsageUseCases, usageMiddleware, restCalls }
|
||||||
+10
-1
@@ -16,7 +16,7 @@ class UserLib {
|
|||||||
'Instance of adapters must be passed in when instantiating User Use Cases library.'
|
'Instance of adapters must be passed in when instantiating User Use Cases library.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
this.BchWallet = this.adapters.wallet.BchWallet
|
||||||
// Encapsulate dependencies
|
// Encapsulate dependencies
|
||||||
this.UserEntity = new UserEntity()
|
this.UserEntity = new UserEntity()
|
||||||
this.UserModel = this.adapters.localdb.Users
|
this.UserModel = this.adapters.localdb.Users
|
||||||
@@ -30,6 +30,11 @@ class UserLib {
|
|||||||
const userEntity = this.UserEntity.validate(userObj)
|
const userEntity = this.UserEntity.validate(userObj)
|
||||||
const user = new this.UserModel(userEntity)
|
const user = new this.UserModel(userEntity)
|
||||||
|
|
||||||
|
const wallet = new this.BchWallet()
|
||||||
|
const walletInfo = await wallet.walletInfoPromise
|
||||||
|
const mnemonic = walletInfo.mnemonic
|
||||||
|
user.mnemonic = mnemonic
|
||||||
|
|
||||||
// Enforce default value of 'user'
|
// Enforce default value of 'user'
|
||||||
user.type = 'user'
|
user.type = 'user'
|
||||||
// console.log('user: ', user)
|
// console.log('user: ', user)
|
||||||
@@ -102,6 +107,7 @@ class UserLib {
|
|||||||
|
|
||||||
// Input Validation
|
// Input Validation
|
||||||
// Optional inputs, but they must be strings if included.
|
// Optional inputs, but they must be strings if included.
|
||||||
|
|
||||||
if (newData.email && typeof newData.email !== 'string') {
|
if (newData.email && typeof newData.email !== 'string') {
|
||||||
throw new Error("Property 'email' must be a string!")
|
throw new Error("Property 'email' must be a string!")
|
||||||
}
|
}
|
||||||
@@ -111,6 +117,9 @@ class UserLib {
|
|||||||
if (newData.password && typeof newData.password !== 'string') {
|
if (newData.password && typeof newData.password !== 'string') {
|
||||||
throw new Error("Property 'password' must be a string!")
|
throw new Error("Property 'password' must be a string!")
|
||||||
}
|
}
|
||||||
|
if (newData.mnemonic) {
|
||||||
|
throw new Error("Property 'mnemonic' cannot be updated!")
|
||||||
|
}
|
||||||
|
|
||||||
// Save a copy of the original user type.
|
// Save a copy of the original user type.
|
||||||
const userType = existingUser.type
|
const userType = existingUser.type
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
/key/swarm/psk/1.0.0/
|
|
||||||
/base16/
|
|
||||||
bbd935b70105b03ebd0c6a3c2d2730cd22fc0d18c490ccf689b6c8a22e1bed2a
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
Liveness test runs before all other tests to ensure the server is running.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public npm libraries
|
||||||
|
import { assert } from 'chai'
|
||||||
|
import axios from 'axios'
|
||||||
|
import testUtils from '../../utils/test-utils.js'
|
||||||
|
|
||||||
|
// const sinon = require('sinon')
|
||||||
|
|
||||||
|
// Local support libraries
|
||||||
|
import config from '../../../config/index.js'
|
||||||
|
// import Server from '../../../bin/server.js'
|
||||||
|
// import testUtils from '../../utils/test-utils.js'
|
||||||
|
// const adminLib = new AdminLib()
|
||||||
|
|
||||||
|
const LOCALHOST = `http://localhost:${config.port}`
|
||||||
|
|
||||||
|
describe('#Check Server Liveness', () => {
|
||||||
|
// before(async () => {
|
||||||
|
it('should confirm the server is running', async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${LOCALHOST}/`)
|
||||||
|
assert(response.status === 200, 'Server is running, continuing with E2E tests.')
|
||||||
|
} catch (err) {
|
||||||
|
console.log('\nServer is not running, exiting tests.')
|
||||||
|
console.log('Start the server with `npm run start:e2e:server` before running E2E tests.\n')
|
||||||
|
console.log('Ensure running npm run docs before running the test server')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should confirm the server is running over test enviroment', async () => {
|
||||||
|
try {
|
||||||
|
const res = await testUtils.loginAdminUser()
|
||||||
|
assert.property(res, 'user')
|
||||||
|
assert.property(res, 'token')
|
||||||
|
assert.property(res, 'id')
|
||||||
|
} catch (err) {
|
||||||
|
console.log('\nServer is not running over test enviroment, exiting tests.')
|
||||||
|
console.log('Start the server with `npm run start:e2e:server` before running E2E tests.\n')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -13,116 +13,114 @@ import axios from 'axios'
|
|||||||
|
|
||||||
// Local support libraries
|
// Local support libraries
|
||||||
import config from '../../../config/index.js'
|
import config from '../../../config/index.js'
|
||||||
|
// import Server from '../../../bin/server.js'
|
||||||
import Server from '../../../bin/server.js'
|
|
||||||
import testUtils from '../../utils/test-utils.js'
|
import testUtils from '../../utils/test-utils.js'
|
||||||
import AdminLib from '../../../src/adapters/admin.js'
|
|
||||||
const adminLib = new AdminLib()
|
|
||||||
|
|
||||||
// const request = supertest.agent(app.listen())
|
// const request = supertest.agent(app.listen())
|
||||||
const context = {}
|
const context = {}
|
||||||
|
|
||||||
const LOCALHOST = `http://localhost:${config.port}`
|
const LOCALHOST = `http://localhost:${config.port}`
|
||||||
|
|
||||||
describe('Auth', () => {
|
if (!config.noMongo) {
|
||||||
before(async () => {
|
describe('Auth', () => {
|
||||||
const app = new Server()
|
before(async () => {
|
||||||
|
// const app = new Server()
|
||||||
|
|
||||||
// This should be the first instruction. It starts the REST API server.
|
// This should be the first instruction. It starts the REST API server.
|
||||||
await app.startServer()
|
// await app.startServer()
|
||||||
|
|
||||||
// Stop the IPFS node for the rest of the e2e tests.
|
// TODO:
|
||||||
// await app.controllers.adapters.ipfs.stop()
|
|
||||||
|
|
||||||
// Delete all previous users in the database.
|
// Stop the IPFS node for the rest of the e2e tests.
|
||||||
await testUtils.deleteAllUsers()
|
// await app.controllers.adapters.ipfs.stop()
|
||||||
|
|
||||||
// Create a new admin user.
|
// Delete all previous users in the database.
|
||||||
await adminLib.createSystemUser()
|
await testUtils.deleteAllUsers()
|
||||||
|
|
||||||
const userObj = {
|
const userObj = {
|
||||||
email: 'test@test.com',
|
email: 'test@test.com',
|
||||||
password: 'pass',
|
password: 'pass',
|
||||||
name: 'test'
|
name: 'test'
|
||||||
}
|
|
||||||
const testUser = await testUtils.createUser(userObj)
|
|
||||||
// console.log('TestUser: ', testUser)
|
|
||||||
|
|
||||||
context.user = testUser.user
|
|
||||||
context.token = testUser.token
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('POST /auth', () => {
|
|
||||||
it('should throw 401 if credentials are incorrect', async () => {
|
|
||||||
try {
|
|
||||||
const options = {
|
|
||||||
method: 'post',
|
|
||||||
url: `${LOCALHOST}/auth`,
|
|
||||||
data: {
|
|
||||||
email: 'test@test.com',
|
|
||||||
password: 'wrongpassword'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await axios(options)
|
|
||||||
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
|
||||||
)
|
|
||||||
assert(false, 'Unexpected result')
|
|
||||||
} catch (err) {
|
|
||||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
|
||||||
}
|
}
|
||||||
|
const testUser = await testUtils.createUser(userObj)
|
||||||
|
// console.log('TestUser: ', testUser)
|
||||||
|
|
||||||
|
context.user = testUser.user
|
||||||
|
context.token = testUser.token
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw 401 if email is wrong format', async () => {
|
describe('POST /auth', () => {
|
||||||
try {
|
it('should throw 401 if credentials are incorrect', async () => {
|
||||||
const options = {
|
try {
|
||||||
method: 'post',
|
const options = {
|
||||||
url: `${LOCALHOST}/auth`,
|
method: 'post',
|
||||||
data: {
|
url: `${LOCALHOST}/auth`,
|
||||||
email: 'wrongEmail',
|
data: {
|
||||||
password: 'wrongpassword'
|
email: 'test@test.com',
|
||||||
|
password: 'wrongpassword'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await axios(options)
|
||||||
|
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||||
|
)
|
||||||
|
assert(false, 'Unexpected result')
|
||||||
|
} catch (err) {
|
||||||
|
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
await axios(options)
|
it('should throw 401 if email is wrong format', async () => {
|
||||||
assert(false, 'Unexpected result')
|
try {
|
||||||
} catch (err) {
|
const options = {
|
||||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
method: 'post',
|
||||||
}
|
url: `${LOCALHOST}/auth`,
|
||||||
})
|
data: {
|
||||||
|
email: 'wrongEmail',
|
||||||
it('should auth user', async () => {
|
password: 'wrongpassword'
|
||||||
try {
|
}
|
||||||
const options = {
|
|
||||||
method: 'post',
|
|
||||||
url: `${LOCALHOST}/auth`,
|
|
||||||
data: {
|
|
||||||
email: 'test@test.com',
|
|
||||||
password: 'pass'
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const result = await axios(options)
|
|
||||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
|
||||||
|
|
||||||
assert(result.status === 200, 'Status Code 200 expected.')
|
await axios(options)
|
||||||
assert(
|
assert(false, 'Unexpected result')
|
||||||
result.data.user.email === 'test@test.com',
|
} catch (err) {
|
||||||
'Email of test expected'
|
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||||
)
|
}
|
||||||
assert(
|
})
|
||||||
result.data.user.password === undefined,
|
|
||||||
'Password expected to be omited'
|
it('should auth user', async () => {
|
||||||
)
|
try {
|
||||||
} catch (err) {
|
const options = {
|
||||||
console.log(
|
method: 'post',
|
||||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
url: `${LOCALHOST}/auth`,
|
||||||
)
|
data: {
|
||||||
throw err
|
email: 'test@test.com',
|
||||||
}
|
password: 'pass'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = await axios(options)
|
||||||
|
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||||
|
|
||||||
|
assert(result.status === 200, 'Status Code 200 expected.')
|
||||||
|
assert(
|
||||||
|
result.data.user.email === 'test@test.com',
|
||||||
|
'Email of test expected'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
result.data.user.password === undefined,
|
||||||
|
'Password expected to be omited'
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(
|
||||||
|
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||||
|
)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,115 +0,0 @@
|
|||||||
import { assert } from 'chai'
|
|
||||||
import Admin from '../../../src/adapters/admin.js'
|
|
||||||
import sinon from 'sinon'
|
|
||||||
import util from 'util'
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
let sandbox
|
|
||||||
let uut
|
|
||||||
describe('Admin', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
uut = new Admin()
|
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => sandbox.restore())
|
|
||||||
|
|
||||||
describe('loginAdmin()', () => {
|
|
||||||
it('should logind admin', async () => {
|
|
||||||
try {
|
|
||||||
const error = new Error('test error')
|
|
||||||
error.response = {
|
|
||||||
status: 422
|
|
||||||
}
|
|
||||||
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
|
|
||||||
|
|
||||||
const result = await uut.loginAdmin()
|
|
||||||
const user = result.data.user
|
|
||||||
|
|
||||||
assert.property(user, '_id')
|
|
||||||
assert.property(user, 'email')
|
|
||||||
assert.property(user, 'type')
|
|
||||||
|
|
||||||
assert.isString(user._id)
|
|
||||||
assert.isString(user.email)
|
|
||||||
assert.isString(user.type)
|
|
||||||
|
|
||||||
assert.equal(user.type, 'admin')
|
|
||||||
} catch (err) {
|
|
||||||
assert(false, 'Unexpected result')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle axios error', async () => {
|
|
||||||
try {
|
|
||||||
// Returns an erroneous password to force
|
|
||||||
// an auth error
|
|
||||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
|
||||||
|
|
||||||
await uut.loginAdmin()
|
|
||||||
assert(false, 'Unexpected result')
|
|
||||||
} catch (err) {
|
|
||||||
assert.equal(err.response.status, 401)
|
|
||||||
assert.include(err.response.data, 'Unauthorized')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('createSystemUser()', () => {
|
|
||||||
it('should create admin', async () => {
|
|
||||||
try {
|
|
||||||
const result = await uut.createSystemUser()
|
|
||||||
|
|
||||||
assert.property(result, 'email')
|
|
||||||
assert.property(result, 'password')
|
|
||||||
assert.property(result, 'id')
|
|
||||||
assert.property(result, 'token')
|
|
||||||
} catch (err) {
|
|
||||||
assert(false, 'Unexpected result')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle axios error', async () => {
|
|
||||||
try {
|
|
||||||
const error1 = new Error('test error')
|
|
||||||
error1.response = {
|
|
||||||
status: 422
|
|
||||||
}
|
|
||||||
const error2 = new Error('test error')
|
|
||||||
error1.response = {
|
|
||||||
status: 500
|
|
||||||
}
|
|
||||||
// The loginAdmin() function in some use cases is recursive
|
|
||||||
// after handling the 422 error, it gets called again
|
|
||||||
sandbox
|
|
||||||
.stub(uut.axios, 'request')
|
|
||||||
.onFirstCall()
|
|
||||||
.throws(error1)
|
|
||||||
.onSecondCall()
|
|
||||||
.throws(error2)
|
|
||||||
|
|
||||||
await uut.createSystemUser()
|
|
||||||
assert(false, 'Unexpected result')
|
|
||||||
} catch (err) {
|
|
||||||
assert.include(err.message, 'test error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle errors when remove user', async () => {
|
|
||||||
try {
|
|
||||||
const error1 = new Error('test error')
|
|
||||||
error1.response = {
|
|
||||||
status: 422
|
|
||||||
}
|
|
||||||
sandbox.stub(uut.axios, 'request').throws(error1)
|
|
||||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
|
||||||
|
|
||||||
await uut.createSystemUser()
|
|
||||||
assert(false, 'Unexpected result')
|
|
||||||
} catch (err) {
|
|
||||||
assert.include(err.message, 'test error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
End-to-end tests for /usage endpoints.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import config from '../../../config/index.js'
|
||||||
|
import { assert } from 'chai'
|
||||||
|
import axios from 'axios'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import util from 'util'
|
||||||
|
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
const LOCALHOST = `http://localhost:${config.port}`
|
||||||
|
|
||||||
|
let sandbox
|
||||||
|
|
||||||
|
describe('Usage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => sandbox.restore())
|
||||||
|
|
||||||
|
describe('GET /usage', () => {
|
||||||
|
it('should return usage status', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'get',
|
||||||
|
url: `${LOCALHOST}/usage`
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await axios(options)
|
||||||
|
|
||||||
|
assert.property(result.data, 'status')
|
||||||
|
} catch (err) {
|
||||||
|
assert(false, 'Unexpected result')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /usage/ips', () => {
|
||||||
|
it('should return ips', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'get',
|
||||||
|
url: `${LOCALHOST}/usage/ips`
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await axios(options)
|
||||||
|
|
||||||
|
assert.property(result.data, 'ips')
|
||||||
|
} catch (err) {
|
||||||
|
assert(false, 'Unexpected result')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /usage/endpoints', () => {
|
||||||
|
it('should return ips', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'get',
|
||||||
|
url: `${LOCALHOST}/usage/endpoints`
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await axios(options)
|
||||||
|
|
||||||
|
assert.property(result.data, 'endpoints')
|
||||||
|
} catch (err) {
|
||||||
|
assert(false, 'Unexpected result')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import testUtils from '../../utils/test-utils.js'
|
||||||
|
import { assert } from 'chai'
|
||||||
|
import config from '../../../config/index.js'
|
||||||
|
import axios from 'axios'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import util from 'util'
|
||||||
|
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
const LOCALHOST = `http://localhost:${config.port}`
|
||||||
|
|
||||||
|
const context = {}
|
||||||
|
let sandbox
|
||||||
|
|
||||||
|
// const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||||
|
|
||||||
|
if (!config.noMongo) {
|
||||||
|
describe('Nostr', () => {
|
||||||
|
before(async () => {
|
||||||
|
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||||
|
|
||||||
|
// Create a second test user.
|
||||||
|
const userObj = {
|
||||||
|
email: 'test2@test.com',
|
||||||
|
password: 'pass2',
|
||||||
|
name: 'test2'
|
||||||
|
}
|
||||||
|
const testUser = await testUtils.createUser(userObj)
|
||||||
|
// console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`)
|
||||||
|
|
||||||
|
context.user2 = testUser.user
|
||||||
|
context.token2 = testUser.token
|
||||||
|
context.id2 = testUser.user._id
|
||||||
|
|
||||||
|
// Get the JWT used to log in as the admin 'system' user.
|
||||||
|
const adminJWT = await testUtils.getAdminJWT()
|
||||||
|
console.log(`adminJWT: ${adminJWT}`)
|
||||||
|
context.adminJWT = adminJWT
|
||||||
|
|
||||||
|
// const admin = await testUtils.loginAdminUser()
|
||||||
|
// context.adminJWT = admin.token
|
||||||
|
|
||||||
|
// const admin = await adminLib.loginAdmin()
|
||||||
|
// console.log(`admin: ${JSON.stringify(admin, null, 2)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// const useCases = new UseCases({ adapters })
|
||||||
|
// uut = new UserController({ adapters, useCases })
|
||||||
|
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => sandbox.restore())
|
||||||
|
|
||||||
|
describe('POST /nostr/deletedChat - Create Deleted Chat', () => {
|
||||||
|
it('should not create deleted chat if the authorization header is missing', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `${LOCALHOST}/nostr/deletedChat`
|
||||||
|
}
|
||||||
|
await axios(options)
|
||||||
|
|
||||||
|
assert.equal(true, false, 'Unexpected behavior')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.response.status, 401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should not create deleted chat if the authorization header is invalid scheme', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `${LOCALHOST}/nostr/deletedChat`,
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer invalid'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios(options)
|
||||||
|
|
||||||
|
assert.equal(true, false, 'Unexpected behavior')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.response.status, 401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should not create deleted chat if the authorization header is invalid token', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `${LOCALHOST}/nostr/deletedChat`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${context.token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios(options)
|
||||||
|
|
||||||
|
assert.equal(true, false, 'Unexpected behavior')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.response.status, 401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should not create deleted chat if token is not admin', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `${LOCALHOST}/nostr/deletedChat`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${context.token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios(options)
|
||||||
|
|
||||||
|
assert.equal(true, false, 'Unexpected behavior')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.response.status, 401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('POST /nostr/deletedPost - Create Deleted Post', () => {
|
||||||
|
it('should not create deleted post if the authorization header is missing', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `${LOCALHOST}/nostr/deletedPost`
|
||||||
|
}
|
||||||
|
await axios(options)
|
||||||
|
|
||||||
|
assert.equal(true, false, 'Unexpected behavior')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.response.status, 401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create deleted post if the authorization header is invalid scheme', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `${LOCALHOST}/nostr/deletedPost`,
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer invalid'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await axios(options)
|
||||||
|
|
||||||
|
assert.equal(true, false, 'Unexpected behavior')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.response.status, 401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should not create deleted post if the authorization header is invalid token', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `${LOCALHOST}/nostr/deletedPost`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${context.token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await axios(options)
|
||||||
|
|
||||||
|
assert.equal(true, false, 'Unexpected behavior')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.response.status, 401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should not create deleted post if token is not admin', async () => {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `${LOCALHOST}/nostr/deletedPost`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${context.token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await axios(options)
|
||||||
|
|
||||||
|
assert.equal(true, false, 'Unexpected behavior')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.response.status, 401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -27,8 +27,9 @@ describe('#adapters', () => {
|
|||||||
it('should start the async adapters', async () => {
|
it('should start the async adapters', async () => {
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
uut.config.getJwtAtStartup = true
|
uut.config.getJwtAtStartup = true
|
||||||
sandbox.stub(uut.fullStackJwt, 'getJWT').resolves()
|
uut.config.useIpfs = true
|
||||||
sandbox.stub(uut.fullStackJwt, 'instanceBchjs').resolves()
|
uut.config.env = 'not-a-test'
|
||||||
|
sandbox.stub(uut.wallet, 'instanceWallet').resolves()
|
||||||
sandbox.stub(uut.ipfs, 'start').resolves()
|
sandbox.stub(uut.ipfs, 'start').resolves()
|
||||||
|
|
||||||
const result = await uut.start()
|
const result = await uut.start()
|
||||||
@@ -36,6 +37,22 @@ describe('#adapters', () => {
|
|||||||
assert.equal(result, true)
|
assert.equal(result, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not start ipfs on test enviroment', async () => {
|
||||||
|
// Mock dependencies
|
||||||
|
uut.config.getJwtAtStartup = true
|
||||||
|
uut.config.useIpfs = true
|
||||||
|
uut.config.env = 'test'
|
||||||
|
|
||||||
|
sandbox.stub(uut.fullStackJwt, 'getJWT').resolves()
|
||||||
|
sandbox.stub(uut.fullStackJwt, 'instanceBchjs').resolves()
|
||||||
|
const ipfsSpy = sandbox.stub(uut.ipfs, 'start').resolves(null)
|
||||||
|
|
||||||
|
const result = await uut.start()
|
||||||
|
|
||||||
|
assert.isTrue(ipfsSpy.notCalled)
|
||||||
|
assert.equal(result, true)
|
||||||
|
})
|
||||||
|
|
||||||
it('should catch and throw an error', async () => {
|
it('should catch and throw an error', async () => {
|
||||||
try {
|
try {
|
||||||
// Force an error
|
// Force an error
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { assert } from 'chai'
|
||||||
|
import Admin from '../../../src/adapters/admin.js'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import util from 'util'
|
||||||
|
import config from '../../../config/index.js'
|
||||||
|
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
let sandbox
|
||||||
|
let uut
|
||||||
|
describe('Admin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
uut = new Admin()
|
||||||
|
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => sandbox.restore())
|
||||||
|
|
||||||
|
if (!config.noMongo) {
|
||||||
|
describe('loginAdmin()', () => {
|
||||||
|
it('should login admin', async () => {
|
||||||
|
try {
|
||||||
|
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'pass' })
|
||||||
|
sandbox.stub(uut.axios, 'request').resolves(true)
|
||||||
|
|
||||||
|
const result = await uut.loginAdmin()
|
||||||
|
assert.isTrue(result)
|
||||||
|
} catch (err) {
|
||||||
|
assert(false, 'Unexpected result')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle axios error', async () => {
|
||||||
|
try {
|
||||||
|
// Returns an erroneous password to force
|
||||||
|
// an auth error
|
||||||
|
sandbox.stub(uut.axios, 'request').throws(new Error('test error'))
|
||||||
|
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||||
|
|
||||||
|
await uut.loginAdmin()
|
||||||
|
assert(false, 'Unexpected result')
|
||||||
|
} catch (err) {
|
||||||
|
assert.include(err.message, 'test error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('createSystemUser()', () => {
|
||||||
|
it('should create admin', async () => {
|
||||||
|
try {
|
||||||
|
await uut.deleteExistingSystemUser()
|
||||||
|
const result = await uut.createSystemUser()
|
||||||
|
|
||||||
|
assert.property(result, 'email')
|
||||||
|
assert.property(result, 'password')
|
||||||
|
assert.property(result, 'id')
|
||||||
|
assert.property(result, 'token')
|
||||||
|
} catch (err) {
|
||||||
|
assert(false, 'Unexpected result')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should update admin password', async () => {
|
||||||
|
try {
|
||||||
|
uut.config.adminPassword = 'newpassword'
|
||||||
|
|
||||||
|
const fakeUser = {
|
||||||
|
password: 'oldpassword',
|
||||||
|
save: () => { return 'token' },
|
||||||
|
generateToken: () => { return 'token' }
|
||||||
|
}
|
||||||
|
|
||||||
|
sandbox.stub(uut.User, 'findOne').resolves(fakeUser)
|
||||||
|
sandbox.stub(uut.jsonFiles, 'writeJSON').resolves(true)
|
||||||
|
const result = await uut.createSystemUser()
|
||||||
|
|
||||||
|
assert.property(result, 'email')
|
||||||
|
assert.property(result, 'password')
|
||||||
|
assert.property(result, 'id')
|
||||||
|
assert.property(result, 'token')
|
||||||
|
|
||||||
|
assert.equal(fakeUser.password, 'newpassword', 'password should be updated')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
assert(false, 'Unexpected result')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle error', async () => {
|
||||||
|
try {
|
||||||
|
sandbox.stub(uut.User, 'findOne').throws(new Error('test error'))
|
||||||
|
await uut.createSystemUser()
|
||||||
|
assert.fail('Unexpected result')
|
||||||
|
} catch (err) {
|
||||||
|
assert.include(err.message, 'test error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deleteExistingSystemUser()', () => {
|
||||||
|
it('should delete admin', async () => {
|
||||||
|
try {
|
||||||
|
sandbox.stub(uut.User, 'deleteOne').resolves(true)
|
||||||
|
const result = await uut.deleteExistingSystemUser()
|
||||||
|
assert.isTrue(result)
|
||||||
|
} catch (err) {
|
||||||
|
assert(false, 'Unexpected result')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle error when deleting admin', async () => {
|
||||||
|
try {
|
||||||
|
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||||
|
await uut.deleteExistingSystemUser()
|
||||||
|
assert.fail('Unexpected result')
|
||||||
|
} catch (err) {
|
||||||
|
assert.include(err.message, 'test error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -135,18 +135,19 @@ describe('bch', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// it('should return the merit ', async () => {
|
/* it('should return the merit ', async () => {
|
||||||
// try {
|
try {
|
||||||
// // Mock live network calls.
|
// Mock live network calls.
|
||||||
// sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
|
sandbox.stub(uut.msgLib.merit, 'agMerit').resolves(100)
|
||||||
//
|
|
||||||
// const slpAddr =
|
const slpAddr =
|
||||||
// 'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
|
'simpleledger:qqgnksc6zr4nzxrye69fq625wu2myxey6uh9kzjy96'
|
||||||
// const merit = await uut.getMerit(slpAddr)
|
const merit = await uut.getMerit(slpAddr)
|
||||||
// assert.isNumber(merit)
|
assert.isNumber(merit)
|
||||||
// } catch (err) {
|
} catch (err) {
|
||||||
// assert.fail('Unexpected result')
|
console.log('err: ', err)
|
||||||
// }
|
assert.fail('Unexpected result')
|
||||||
// })
|
}
|
||||||
|
}) */
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user