Compare commits
584 Commits
master
...
feature/pd
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
5720f1cb50 | ||
|
|
9324da5cb7 | ||
|
|
408b0eeb39 | ||
|
|
ed83b1f906 | ||
|
|
3068c3e2b8 | ||
|
|
93c6f247f7 | ||
|
|
05a9441493 | ||
|
|
aa02e92e4d | ||
|
|
c397c28acb | ||
|
|
c93cc6c522 | ||
|
|
0cdcc77606 | ||
|
|
87eec7e5c9 | ||
|
|
44a1ba01f3 | ||
|
|
d49661fa29 | ||
|
|
71fdee0cfd | ||
|
|
1ced351ea2 | ||
|
|
c048347f2e | ||
|
|
be4f76834a | ||
|
|
23edad416c | ||
|
|
f3e4233285 | ||
|
|
84e0f039cb | ||
|
|
1f8b20a7d1 | ||
|
|
f0e1ab3af8 | ||
|
|
112ab4d5d5 | ||
|
|
8768e142e3 | ||
|
|
827d2e9c3e | ||
|
|
0f3c4a621d | ||
|
|
e60475f351 | ||
|
|
0f63f7dae6 | ||
|
|
f9a15ab192 | ||
|
|
77f5759d60 | ||
|
|
684f3237cf | ||
|
|
23e61cc182 | ||
|
|
d2f45ff67a | ||
|
|
06e12e9103 | ||
|
|
29dddeff4f | ||
|
|
017d568aea | ||
|
|
c91b52cfd2 | ||
|
|
2bd38d8d78 | ||
|
|
7d9db68d80 | ||
|
|
4b94303d67 | ||
|
|
08628704c7 | ||
|
|
f1c1132690 | ||
|
|
d2426c473c | ||
|
|
66986b804c | ||
|
|
95daa230bb | ||
|
|
365b22d778 | ||
|
|
cf2b54464e | ||
|
|
4e60c1274a | ||
|
|
2f65e63fbe | ||
|
|
b461431197 | ||
|
|
5c92b6a734 | ||
|
|
ba348dafb3 | ||
|
|
ce2d310e8c | ||
|
|
475db28095 | ||
|
|
823a41c027 | ||
|
|
2a834f1b14 | ||
|
|
c8ffb6cf29 | ||
|
|
ecc9efd434 | ||
|
|
dd35e56029 | ||
|
|
d0e7998650 | ||
|
|
fec5e49304 | ||
|
|
3b12e14e71 | ||
|
|
86eaf2139d | ||
|
|
65fad993ad | ||
|
|
55e6e477be | ||
| ba5efcc152 | |||
| 26253564d5 | |||
| 92791c77a9 | |||
| 465792b2ab | |||
| de269fd828 | |||
| 8c91484f37 | |||
| 6904ac8b7c | |||
| aea6bbcb0e | |||
|
|
a788d8bcf5 | ||
|
|
cc074a941f | ||
|
|
47574100f9 | ||
| 7ad74942e0 | |||
| ac1cc04637 | |||
| 722d055e2d | |||
|
|
a9a55da8e0 | ||
|
|
f2b23ace8b | ||
|
|
1f2048e270 | ||
|
|
b16a23243e | ||
|
|
653f1268a6 | ||
|
|
56db6d0add | ||
|
|
cf2152dcfc | ||
|
|
a95bd245cf | ||
|
|
92fd315505 | ||
|
|
2225c2d173 | ||
|
|
f8a76bcd7f | ||
|
|
3efa8bb7ee | ||
|
|
5c155ef503 | ||
|
|
41d199e24a | ||
|
|
e1f2b54de3 | ||
|
|
d6c5757dfa | ||
|
|
9a489801c5 | ||
|
|
9fcdcd087b | ||
|
|
af1304022e | ||
|
|
7972676eb8 | ||
|
|
bef205aec7 | ||
|
|
49fdbbf7ae | ||
|
|
dd69a52273 | ||
|
|
c681b4d684 | ||
|
|
b166013707 | ||
|
|
3e04727022 | ||
|
|
5d13112b00 | ||
|
|
373f88086e | ||
|
|
05492306c0 | ||
|
|
423d490939 | ||
|
|
7edc0ba901 | ||
|
|
0ebb71daf1 | ||
| b4480d89cf | |||
| ff584ba5d1 | |||
|
|
4b15cabd4f | ||
|
|
be4a2d135a | ||
| 69f0fdf120 | |||
| e3bebff618 | |||
| f19f7b0ec4 | |||
|
|
ca4cfd9d8d | ||
|
|
96d292074b | ||
|
|
0536a018c6 | ||
|
|
81d1b84a7d | ||
|
|
61c21b245e | ||
|
|
919387f581 | ||
|
|
3b8ea70d3c | ||
|
|
477ab3b580 | ||
|
|
a1da814030 | ||
|
|
19fd5611b2 | ||
|
|
556004a557 | ||
|
|
fba6d6bba0 | ||
|
|
04252e006b | ||
|
|
436e1f0c53 | ||
|
|
21030b1d51 | ||
|
|
b583a86ade | ||
|
|
3262ec9b4a | ||
|
|
0c9afea67a | ||
|
|
b83543d018 | ||
|
|
d4a0185507 | ||
|
|
42dcf6970d | ||
| 0b4374141e | |||
| 652ddc9d88 | |||
|
|
cf6a2830c8 | ||
|
|
01d9553db4 | ||
|
|
578b526f96 | ||
|
|
e3061b46f9 | ||
| 2559f1e66b | |||
|
|
9ca469a075 | ||
| 519bce6b78 | |||
| 557ea96be0 | |||
| 9a49cc67f0 | |||
| 3012f0799b | |||
| 7a8852f64b | |||
| f92e6c3cf1 | |||
| 72dc83daff | |||
|
|
5981d3f871 | ||
|
|
fb0c5ad3f8 | ||
| 04d9d588e8 | |||
|
|
e5fe925023 | ||
| 345a21a211 | |||
|
|
b83e8c3979 | ||
|
|
0744ac3163 | ||
| 3de992d251 | |||
| 369ef61cab | |||
| e6e96c4b0d | |||
| dc96033cb1 | |||
| 9ee6bf4380 | |||
| e0f0726e68 | |||
|
|
105a56499d | ||
|
|
2bd27cd73b | ||
|
|
aed64e76a7 | ||
|
|
abfd073de8 | ||
|
|
41edd1423c | ||
|
|
471fde78c1 | ||
|
|
3e4759a0c9 | ||
|
|
b5276890fb | ||
|
|
1488bc3d6d | ||
|
|
f4e7210a40 | ||
|
|
e385bb6bf9 | ||
|
|
f56e531384 | ||
|
|
32606fe1c2 | ||
|
|
a8734846a0 | ||
|
|
ad0edf3c88 | ||
|
|
66975862f7 | ||
|
|
b9185e761b | ||
|
|
885cf463a7 | ||
|
|
689f35fea2 | ||
|
|
6bf5d1d5ed | ||
|
|
b0b8c7a445 | ||
|
|
d25c19cdaa | ||
|
|
89d06d317b | ||
|
|
c5ec32f87a | ||
|
|
ce5c348023 | ||
|
|
832eea5889 | ||
|
|
60049442f1 | ||
|
|
624557ebfd | ||
|
|
6b0379bfdc | ||
|
|
a9510a6d36 | ||
|
|
59e4156bb9 | ||
|
|
de9606519a | ||
|
|
eeb115584d | ||
|
|
ee3721dfa4 | ||
|
|
239cc231ea | ||
|
|
4bd4df7b09 | ||
|
|
d12371b84f | ||
|
|
c97b3e3ec3 | ||
|
|
2c2aad1355 | ||
|
|
9949935bcc | ||
|
|
35fc6ebf62 | ||
|
|
d2205648e6 | ||
|
|
68ed93dd24 | ||
|
|
a06b76b800 | ||
|
|
67f882b9bc | ||
|
|
17dc4981c6 | ||
|
|
0179b25d12 | ||
|
|
e3c1cbf1c0 | ||
|
|
5899bd2f77 | ||
|
|
1b0e1cf1d4 | ||
|
|
134e877b7c | ||
|
|
b75ac46781 | ||
|
|
edc94d3700 | ||
|
|
b13efa92fd | ||
|
|
6f796c98f7 | ||
|
|
3a5856c7f0 | ||
|
|
775b655aac | ||
|
|
101fd2eaa4 | ||
|
|
6f0bb01b61 | ||
|
|
b345900459 | ||
|
|
f1cfe9b6aa | ||
|
|
8941582d54 | ||
|
|
8c5de781ea | ||
|
|
baef264bd0 | ||
|
|
74df7e2645 | ||
|
|
56cd90a197 | ||
|
|
abdce05136 | ||
|
|
35565845ca | ||
|
|
a83ec2c971 | ||
|
|
4b371e142d | ||
|
|
aa2644d812 | ||
|
|
fd99250882 | ||
|
|
5344c42ceb | ||
|
|
21413268f3 | ||
|
|
aa35d87885 | ||
|
|
a53444b863 | ||
|
|
4b0031fb08 | ||
|
|
62b8534769 | ||
|
|
f3262c2d64 | ||
|
|
7986184111 | ||
|
|
9c35567389 | ||
|
|
8325cbec84 | ||
|
|
c6d310184b | ||
|
|
83892d5093 | ||
|
|
1e1cdd9e76 | ||
|
|
3e62a2a01c | ||
|
|
90d10086d7 | ||
|
|
db2d9a666b | ||
|
|
3a0899bcfe | ||
|
|
d13c60fca1 | ||
|
|
580bd6fbeb | ||
|
|
49ebf1605a | ||
|
|
f1fbb35296 | ||
|
|
b85643ca33 | ||
|
|
c27da63a3e | ||
|
|
a332ddc828 | ||
|
|
ab31ccf6d8 | ||
|
|
01b38952e5 | ||
|
|
0fdb5b245c | ||
|
|
94263a46bd | ||
|
|
56a69ab683 | ||
|
|
e73e103ac4 | ||
|
|
b8b33696ec | ||
|
|
c0b0c99f53 | ||
|
|
a2954071bd | ||
|
|
76e4a6cba0 | ||
|
|
b55fd1571e | ||
|
|
8de4e95c6a | ||
|
|
ddeaf82bfd | ||
|
|
e95f65ac78 | ||
|
|
97d37a2eb6 | ||
|
|
a23d090bc1 | ||
|
|
4956ba7352 | ||
|
|
acdd6c928b | ||
|
|
6774c26ea1 | ||
|
|
ef0cd2cb7d | ||
|
|
e921b06826 | ||
|
|
c0c29b74ab | ||
|
|
310863faec | ||
|
|
b05da86197 | ||
|
|
c44d755ce0 | ||
|
|
b7e6cf7437 | ||
|
|
e73328461e | ||
|
|
db93eace30 | ||
|
|
c5dfa47903 | ||
|
|
d96985303d | ||
|
|
27bd47dbe0 | ||
|
|
e3377a48b3 | ||
|
|
a2ed41514d | ||
|
|
78ee5a60fa | ||
|
|
bff403ea04 | ||
|
|
3061bf3d1e | ||
|
|
9b03273055 | ||
|
|
bdcab5ee05 | ||
|
|
14cc3be620 | ||
|
|
50da3e868d | ||
|
|
2c68dedea2 | ||
|
|
da12521517 | ||
|
|
28bbdb8b7c | ||
|
|
1e8e2915f9 | ||
|
|
2350745e61 | ||
|
|
1fec6c7b54 | ||
|
|
3e10407afd | ||
|
|
3d5b5b2214 | ||
|
|
c094af920e | ||
|
|
dafdae5276 | ||
|
|
df7f38bd0a | ||
|
|
4c1aeeeac8 | ||
|
|
667c5310bf | ||
|
|
126cf2f5c3 | ||
|
|
c8fa4a01a1 | ||
|
|
e764a713c4 | ||
|
|
93aef6e18b | ||
|
|
f213e9aa43 | ||
|
|
630ba30c27 | ||
|
|
b2d3061796 | ||
|
|
ecb018b32c | ||
|
|
e9f58ca004 | ||
|
|
c0dfa6c7ab | ||
|
|
78d6124f2a | ||
|
|
1a8d1c70fd | ||
|
|
e0333a9c32 | ||
|
|
9a3bc9e488 | ||
|
|
a905822515 | ||
|
|
d7c7bb3c23 | ||
|
|
97a2bee81a | ||
|
|
58bbf063ca | ||
|
|
0159dd9074 | ||
|
|
29a07a9a8b | ||
|
|
c824fb5e9b | ||
|
|
6a9746c17a | ||
|
|
af0e9e51e0 | ||
|
|
5713287c84 | ||
|
|
bd0c3dba50 | ||
|
|
e63c53a855 | ||
|
|
8be56192cb | ||
|
|
d07602b0a9 | ||
|
|
185ba5b1d3 | ||
|
|
2d48ae7a16 | ||
|
|
bec1d08757 | ||
|
|
cc59bd18ee | ||
|
|
f0b560ec06 | ||
|
|
ba3ee4290f | ||
|
|
30fcde5744 | ||
|
|
4a92a7fa22 | ||
|
|
9b188d56e9 | ||
|
|
c7bf8051b9 | ||
|
|
7591fbdace | ||
|
|
5d4d451943 | ||
|
|
828820b6e4 | ||
|
|
eaad476bf5 | ||
|
|
0b7691bdea | ||
|
|
21fbc8ffa0 | ||
|
|
24be1d0c1f | ||
|
|
2830f75f65 | ||
|
|
c4a3ff6f68 | ||
|
|
cfc92beec0 | ||
|
|
cdfc416d02 | ||
|
|
e17a6765ec | ||
|
|
7bdb3118ae | ||
|
|
126b4ba3a1 | ||
|
|
07e57b8563 | ||
|
|
a9c69e5947 | ||
|
|
1340c8e9c6 | ||
|
|
62e55dbaec | ||
|
|
1ee2a1cf62 | ||
|
|
ad45e005f5 | ||
|
|
0c7d8fac02 | ||
|
|
0b7ad79032 | ||
|
|
e2a9caa07d | ||
|
|
619d2145f9 | ||
|
|
3016d25f73 | ||
|
|
4deaedf79f | ||
|
|
3a412fcd51 | ||
|
|
d9e61e7c5b | ||
|
|
f3e4651bd5 | ||
|
|
9cbff47194 | ||
|
|
525627c972 | ||
|
|
6d8777da83 | ||
|
|
8fb428ef0d | ||
|
|
1bfe742278 | ||
|
|
76b1131d95 | ||
|
|
670e7e9743 | ||
|
|
ff911636c7 | ||
|
|
61f4c1e115 | ||
|
|
3ded1da707 | ||
|
|
c9e4b8dfbf | ||
|
|
c08826e848 | ||
|
|
bc8f4a0582 | ||
|
|
ad323d17a2 | ||
|
|
09566fdfde | ||
|
|
91ed444c90 | ||
|
|
32c046233b | ||
|
|
cf5460c5c7 | ||
|
|
c25393e3b6 | ||
|
|
8a83ac85d9 | ||
|
|
78e62997d1 | ||
|
|
c0fba4af94 | ||
|
|
9723696b2c | ||
|
|
eb5593c7be | ||
|
|
089146a137 | ||
|
|
d4c8201a88 | ||
|
|
538ec8ec73 | ||
|
|
52fa631733 | ||
|
|
284e962910 | ||
|
|
6ba7a54921 | ||
|
|
ecd059ced2 | ||
|
|
4f825e2a86 | ||
|
|
1bf1c768dd | ||
|
|
b33fa4aeaa | ||
|
|
889ce0d921 | ||
|
|
1c9841b4a6 | ||
|
|
99cf000f24 | ||
|
|
3d780a2605 | ||
|
|
1aabcf4d80 | ||
|
|
51de9779e3 | ||
|
|
2f9cf2bff1 | ||
|
|
6d3719ba71 | ||
|
|
dabda362e6 | ||
|
|
b23ecdfdf2 | ||
|
|
18bf5d65d7 | ||
|
|
37c36ffdba | ||
|
|
c7440e2b5c | ||
|
|
6949fd8a2f | ||
|
|
ef72719502 | ||
|
|
a647091a3f | ||
|
|
b5706d3ed5 | ||
|
|
0b2bee0a3d | ||
|
|
4cee326a25 | ||
|
|
bf4cecde05 | ||
|
|
84bef3365e | ||
|
|
922c18db4b | ||
|
|
22fb35d1d4 | ||
|
|
ebf7c9f18e | ||
|
|
43b0efb4d3 | ||
|
|
4430615117 | ||
|
|
e84c63c3d1 | ||
|
|
580695b486 | ||
|
|
9f1ca37977 | ||
|
|
c1964adb58 | ||
|
|
ad5525d88b | ||
|
|
98d478531b | ||
|
|
a2495afa44 | ||
|
|
3f5f94a53f | ||
|
|
97840a45d6 | ||
|
|
69cd33479b | ||
|
|
376d42cd79 | ||
|
|
b69075cbac | ||
|
|
bbca821dcd | ||
|
|
d9fe1f02b8 | ||
|
|
5fe41c7656 | ||
|
|
9cf6fabe64 | ||
|
|
cd0352f904 | ||
|
|
fa30bd2a49 | ||
|
|
e9e05c1192 | ||
|
|
b7025dde59 | ||
|
|
973a632b85 | ||
|
|
9d0da4b39f | ||
|
|
aba86fc687 | ||
|
|
4c87207129 | ||
|
|
7a167b470a | ||
|
|
1ea5390771 | ||
|
|
a218f6586d | ||
|
|
4753b83831 | ||
|
|
a2626dfdd0 | ||
|
|
8e19486cf5 | ||
|
|
e2b89da2fa | ||
|
|
f1af2bd4d4 | ||
|
|
1c94bb25a6 | ||
|
|
06c77b1c1f | ||
|
|
8bcaa192c5 | ||
|
|
8fd7f4676b | ||
|
|
93c007b2b9 | ||
|
|
eb922d918b | ||
|
|
7ba333bf6c | ||
|
|
94777c58c6 | ||
|
|
a6a5089379 | ||
|
|
eb122456ab | ||
|
|
55d34e2a87 | ||
|
|
bfffe44c4a | ||
|
|
dd49c4de00 | ||
|
|
eef760d776 | ||
|
|
fa019bcb4f | ||
|
|
432b574592 | ||
|
|
c3d20ba338 | ||
|
|
be7a3ab7a6 | ||
|
|
272d7ca1be | ||
|
|
05a4714fb1 | ||
|
|
59e5df0dd3 | ||
|
|
771758c831 | ||
|
|
ca55bfca93 | ||
|
|
71f1a6179c | ||
|
|
c13940216b | ||
|
|
f17d077f25 | ||
|
|
62ea49d1fc | ||
|
|
f653689112 | ||
|
|
df03f3f4ba | ||
|
|
34e8640e78 | ||
|
|
b6b50557a7 | ||
|
|
08d90b6e8e | ||
|
|
43a26007d6 | ||
|
|
7fdc890a85 | ||
|
|
ae3838ccf2 | ||
|
|
783b5b08e3 | ||
|
|
015caec01c | ||
|
|
526e2d9cc4 | ||
|
|
3f374f48e1 | ||
|
|
795341dd8d | ||
|
|
c523816cdf | ||
|
|
b26e09904a | ||
|
|
1526392ca5 | ||
|
|
809d897da6 | ||
|
|
eeb8ee9069 | ||
|
|
25aa57dc5e | ||
|
|
6c2449f623 | ||
|
|
d8057807a3 | ||
|
|
e532401a75 | ||
|
|
f8cc12560e | ||
|
|
c8ee9925a1 | ||
|
|
d460ea2952 | ||
|
|
e1b2c62231 | ||
|
|
bead78b372 | ||
|
|
4e14f300f9 | ||
|
|
4759521176 | ||
|
|
a309b6f3ef | ||
|
|
834cf98ef9 | ||
|
|
80ea016687 | ||
|
|
5ecaf67bcb | ||
|
|
33635886e0 | ||
|
|
bba4b7fb41 | ||
|
|
26afcb892a | ||
|
|
9633e3528d | ||
|
|
62e4338e88 | ||
|
|
d949895fec | ||
|
|
b5fa05a660 | ||
|
|
c515d5287e | ||
|
|
ae63a653c8 | ||
|
|
03b6ff3c32 | ||
|
|
935ffecbb0 | ||
|
|
c140e3aae4 | ||
|
|
7f92dc5f51 | ||
|
|
627321d4ae | ||
|
|
0c49cae055 | ||
|
|
3cafd29ee5 | ||
|
|
6c4d8cd51b | ||
|
|
d6d2bfeb73 | ||
|
|
1b1da19d3d | ||
|
|
4fb6b10a97 | ||
|
|
e9c11d6b75 | ||
|
|
2037ebaa8b | ||
|
|
45a862b11f | ||
|
|
29c6e5a0f6 | ||
|
|
aa2caf1f10 | ||
|
|
8188b91f86 | ||
|
|
eb37b43de4 | ||
|
|
eaf1affb27 | ||
|
|
ab44cc5282 | ||
|
|
19c4fd6cd1 | ||
|
|
096246542d | ||
|
|
a6be7b75aa | ||
|
|
80ffba545a | ||
|
|
dbf1f22bac | ||
|
|
7f91c60d26 | ||
|
|
7072882b0b | ||
|
|
00fc9e3926 | ||
|
|
95ec6ba037 | ||
|
|
87da6efbfb | ||
|
|
2ab1bbc02c | ||
|
|
47c53c1a14 | ||
|
|
888bb1595f | ||
|
|
2ed4f6d666 | ||
|
|
2b5fa16824 | ||
|
|
199769cac0 | ||
|
|
c9bfa2d01a | ||
|
|
fc748a744c | ||
|
|
5d8dd86c96 |
71
.gitignore
vendored
71
.gitignore
vendored
@ -1,4 +1,11 @@
|
|||||||
|
## папки с данными создавайемыми при работе сервера
|
||||||
|
data/
|
||||||
|
logs/
|
||||||
|
logs
|
||||||
|
.understand-anything/
|
||||||
|
|
||||||
.gradle
|
.gradle
|
||||||
|
.gradle-home/
|
||||||
build/
|
build/
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
!**/src/main/**/build/
|
!**/src/main/**/build/
|
||||||
@ -40,4 +47,66 @@ bin/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# временный debug token
|
||||||
|
.debug-token
|
||||||
|
|
||||||
|
# Локальные артефакты и секреты Solana-модуля
|
||||||
|
shine-solana/.git/
|
||||||
|
shine-solana/.git-local-backup/
|
||||||
|
shine-solana/.idea/
|
||||||
|
shine-solana/shine/.idea/
|
||||||
|
shine-solana/shine/.gradle/
|
||||||
|
shine-solana/shine/.anchor/
|
||||||
|
shine-solana/shine/.yarn/
|
||||||
|
shine-solana/shine/.vendor/
|
||||||
|
shine-solana/shine/node_modules/
|
||||||
|
shine-solana/shine/target/
|
||||||
|
shine-solana/shine/test-ledger/
|
||||||
|
shine-solana/shine/old_vers/
|
||||||
|
shine-solana/shine/program-keypair.json
|
||||||
|
shine-solana/shine/keys/
|
||||||
|
shine-solana/shine/validator.log
|
||||||
|
shine-solana/shine/doc/КОШЕЛЬКИ_DEVNET_ТЕСТ.md
|
||||||
|
shine-solana/shine/scripts/del/
|
||||||
|
shine-solana/shine/scripts/**/keypairs/
|
||||||
|
shine-solana/shine/scripts/**/runs/
|
||||||
|
shine-solana/shine/scripts/**/*.env
|
||||||
|
shine-solana/shine/scripts/**/TEMP_*.md
|
||||||
|
|
||||||
|
# Локальные артефакты и внешние материалы ESP32-подпроекта
|
||||||
|
ESP32/esp32-config-tool/
|
||||||
|
ESP32/**/.git/
|
||||||
|
ESP32/**/.idea/
|
||||||
|
ESP32-wallet/.idea/
|
||||||
|
ESP32/**/.arduino-build/
|
||||||
|
ESP32/**/official-demo/
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/
|
||||||
|
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/**
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/
|
||||||
|
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/**
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/
|
||||||
|
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/**
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/
|
||||||
|
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/**
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/lv_conf.h
|
||||||
|
ESP32/**/original-firmware/*.bin
|
||||||
|
ESP32/**/original-firmware/*.bin.sha256
|
||||||
|
ESP32/**/*.elf
|
||||||
|
ESP32/**/*.map
|
||||||
|
ESP32/**/*.merged.bin
|
||||||
|
ESP32/**/*.uf2
|
||||||
|
ESP32/**/*.o
|
||||||
|
ESP32/**/*.d
|
||||||
|
ESP32/**/*.a
|
||||||
|
|
||||||
|
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
|
||||||
|
server-backup/archive/**
|
||||||
|
!server-backup/archive/.gitkeep
|
||||||
|
|
||||||
|
# Локальная дев-обвязка Claude (дев-сервер shine-UI, сессии, планы) — не коммитим
|
||||||
|
.claude/
|
||||||
|
# Рабочие бэкапы/превью-ассеты UI — не для репозитория
|
||||||
|
*.bak.png
|
||||||
|
shine-UI/assets/navbar_preview.png
|
||||||
|
|||||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
shine-server-server
|
||||||
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@ -13,6 +13,8 @@
|
|||||||
<option value="$PROJECT_DIR$/shine-server-config" />
|
<option value="$PROJECT_DIR$/shine-server-config" />
|
||||||
<option value="$PROJECT_DIR$/shine-server-crypto" />
|
<option value="$PROJECT_DIR$/shine-server-crypto" />
|
||||||
<option value="$PROJECT_DIR$/shine-server-db" />
|
<option value="$PROJECT_DIR$/shine-server-db" />
|
||||||
|
<option value="$PROJECT_DIR$/shine-server-geo" />
|
||||||
|
<option value="$PROJECT_DIR$/shine-server-log" />
|
||||||
<option value="$PROJECT_DIR$/shine-server-net-protocol" />
|
<option value="$PROJECT_DIR$/shine-server-net-protocol" />
|
||||||
<option value="$PROJECT_DIR$/shine-server-net-server" />
|
<option value="$PROJECT_DIR$/shine-server-net-server" />
|
||||||
</set>
|
</set>
|
||||||
|
|||||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
157
AGENTS.md
Normal file
157
AGENTS.md
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# AGENTS
|
||||||
|
|
||||||
|
## Язык проекта
|
||||||
|
- По умолчанию использовать русский язык во всех пользовательских текстах и технических пояснениях.
|
||||||
|
- Пояснения к коммитам, PR и merge-запросам писать на русском языке.
|
||||||
|
- Комментарии в коде, встроенные справки, документацию и инструкции писать по возможности на русском языке.
|
||||||
|
|
||||||
|
## Примечание
|
||||||
|
- Если внешний инструмент/интеграция требует английский формат, допускается английский, но рядом желательно дать краткое пояснение на русском.
|
||||||
|
|
||||||
|
## Структура проекта (кратко)
|
||||||
|
- Серверный код SHiNE находится в папке `SHiNE-server/`.
|
||||||
|
- Код клиентского UI SHiNE находится в папке `shine-UI/`.
|
||||||
|
- Веб-панель администратора сервера (управление Solana PDA сервера) находится в `shine-UI/`:
|
||||||
|
- точка входа `shine-UI/server-ui.html`;
|
||||||
|
- остальные файлы серверного UI — в `shine-UI/server-ui/`.
|
||||||
|
- Локальный Telegram-бот агента-кодера находится в папке `SHiNE-agent-bot-coder/` и не является кодом основного серверного приложения.
|
||||||
|
- Solana/Anchor-модуль находится в папке `shine-solana/shine/` и ведётся отдельно от основного server/UI деплоя.
|
||||||
|
|
||||||
|
## Сервис агента-кодера
|
||||||
|
- В проекте есть локальный Telegram-бот-сервис агента-кодера в папке `SHiNE-agent-bot-coder/`.
|
||||||
|
- Сервис принимает сообщения из Telegram, ведёт историю диалога, ставит задачи в очередь и вызывает Codex CLI для обработки запросов по проекту.
|
||||||
|
- Автоматически читаемые инструкции для Codex внутри сервиса держать в `SHiNE-agent-bot-coder/AGENTS.md`.
|
||||||
|
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`.
|
||||||
|
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
|
||||||
|
|
||||||
|
## ESP32 UI homeserver
|
||||||
|
- Для UI-скетча устройства `ESP32-S3-Touch-AMOLED-2.16` документ-спецификация и Arduino-скетч должны всегда оставаться синхронными.
|
||||||
|
- Актуальный документ по экранной логике, состояниям, кнопкам, полям, статусам и ограничениям UI считать источником истины для скетча.
|
||||||
|
- При изменении документа обязательно в том же наборе изменений приводить в соответствие скетч.
|
||||||
|
- При изменении скетча обязательно в том же наборе изменений обновлять документ, если поменялись экраны, тексты, переходы, статусы, кнопки, поля или поведение.
|
||||||
|
- Для нового ESP32 UI-прототипа homeserver использовать русский язык как основной и отдельно следить, чтобы текст реально отображался на устройстве, а не только логически присутствовал в коде.
|
||||||
|
|
||||||
|
## Solana-модуль
|
||||||
|
- В проекте есть отдельный Solana/Anchor-модуль в папке `shine-solana/shine/`.
|
||||||
|
- Модуль логически связан с SHiNE, но не должен автоматически подключаться к сборке или деплою основного сервера без отдельного решения.
|
||||||
|
- В Solana-модуле действуют локальные инструкции `shine-solana/shine/AGENTS.md`; при изменениях внутри модуля сначала читать их.
|
||||||
|
- В git добавлять исходники, lock-файлы, настройки проекта и документацию Solana-модуля, но не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
|
||||||
|
- Для Solana deploy/push использовать правила из локального `shine-solana/shine/AGENTS.md`; не смешивать deploy Solana-модуля с `deployServer`/`deployUI` основного проекта.
|
||||||
|
- Для регистрации пользователей в Solana (программа `shine_users`) единая актуальная инструкция по деплою/инициализации, адресам программ, и куда их прописывать в UI/сервере находится в:
|
||||||
|
- `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
||||||
|
- Этот файл считать основной справкой (single source of truth) по деплою и первичной инициализации Solana-регистрации в текущем проекте.
|
||||||
|
- Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в:
|
||||||
|
- `Dev_Docs/Solana_Architecture/README.md`
|
||||||
|
- Документ формата пользовательской PDA-записи `shine_users` находится в:
|
||||||
|
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
||||||
|
|
||||||
|
## Документация блокчейна
|
||||||
|
- Актуальная документация по форматам блокчейна находится в `Dev_Docs/Blockchain/README.md`.
|
||||||
|
- Это точка входа (оглавление), рядом расположены детальные файлы по форматам, типам каналов и командным сообщениям.
|
||||||
|
- При любом изменении кода, связанного с блокчейном (формат блока, типы каналов, правила чтения/записи, команды), обязательно обновлять соответствующие документы в `Dev_Docs/Blockchain/`.
|
||||||
|
- Дополнительно обязательно вести `Dev_Docs/Blockchain/CHANGELOG.md`: дописывать изменения построчно с указанием даты/времени и хэша коммита, после которого внесено изменение.
|
||||||
|
- Перед любым изменением формата блокчейна обязательно заранее предупреждать пользователя, что формат будет изменён.
|
||||||
|
- Изменять формат блокчейна можно только после явного подтверждения пользователя (без подтверждения формат не менять).
|
||||||
|
- Добавление любых данных в блокчейн выполнять только через операцию `AddBlock`.
|
||||||
|
- Перед каждым `AddBlock` обязательно проверять/актуализировать текущее состояние вершины блокчейна (`last global number/hash`) и использовать его при формировании блока.
|
||||||
|
|
||||||
|
## Документация личных сообщений (DM)
|
||||||
|
- Актуальная документация по логике личных сообщений находится в `Dev_Docs/Personal_Messages/README.md`.
|
||||||
|
- При любом изменении кода, связанного с личными сообщениями (формат подписанного DM-блока, типы DM-сообщений, правила доставки/ACK/read-receipt, роутинг по сессиям, UI-логика чатов), обязательно обновлять `Dev_Docs/Personal_Messages/README.md`.
|
||||||
|
- Логика личных сообщений в коде должна всегда соответствовать `Dev_Docs/Personal_Messages/README.md`.
|
||||||
|
- Документ по личным сообщениям обязан поддерживаться в актуальном состоянии.
|
||||||
|
|
||||||
|
## Документация API сервера
|
||||||
|
- Актуальная документация по публичному JSON/WebSocket API сервера находится в `Dev_Docs/API/`.
|
||||||
|
- При любом изменении серверного API/эндпоинтов/операций `op` обязательно обновлять соответствующие документы в `Dev_Docs/API/`.
|
||||||
|
- Перед изменением самого серверного API обязательно явно предупредить пользователя, какие операции, поля запросов/ответов или коды ошибок будут изменены, и запросить отдельное подтверждение.
|
||||||
|
- Без явного подтверждения пользователя формат серверного API не менять; допускается только приведение документации в соответствие уже существующему коду.
|
||||||
|
- Если добавляется новая операция `op`, нужно обновить общий список операций в `Dev_Docs/API/09_Operations_Index.md` или создать его, если файла ещё нет.
|
||||||
|
|
||||||
|
## Документация Figma
|
||||||
|
- Актуальная документация по переносу экранов SHiNE в Figma и обратному переносу из Figma в код находится в `Dev_Docs/Figma/`.
|
||||||
|
- Точка входа: `Dev_Docs/Figma/README.md`.
|
||||||
|
- Подробный рабочий регламент: `Dev_Docs/Figma/TRANSFER_UI_SCREENS.md`.
|
||||||
|
- Для экранов регистрации, входа и других чувствительных UI-flow по умолчанию переносить экраны в Figma по одному, а не пачкой, если пользователь отдельно не подтвердил иной способ.
|
||||||
|
|
||||||
|
## Версионирование
|
||||||
|
- Единый файл версий проекта: `VERSION.properties` (в корне репозитория).
|
||||||
|
- Перед каждым новым коммитом обязательно увеличивать версии в `VERSION.properties`:
|
||||||
|
- `client.version` — версия клиентского UI.
|
||||||
|
- `server.version` — версия серверной части.
|
||||||
|
- Базовое правило инкремента: `+1` по последнему числовому сегменту (patch), если не оговорено иное.
|
||||||
|
- Обычные коммиты делать стандартным `git commit`; переменная `$GITEA_TOKEN` для коммитов не нужна и не используется.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
- Все документы и заметки по деплою хранить в папке `Dev_Docs/deploy/`.
|
||||||
|
- Production-хост SHiNE: `player@shineup.me` (`185.229.109.118`).
|
||||||
|
- Основной test-хост SHiNE: `player@193.8.215.70` (`t.shineup.me`).
|
||||||
|
- Резервный test-хост SHiNE: `player@93.170.12.154` (`test.shineup.me`).
|
||||||
|
- Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`).
|
||||||
|
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
|
||||||
|
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
|
||||||
|
- Любые изменения и любой деплой на production `shineup.me` выполнять только после отдельного явного подтверждения пользователя.
|
||||||
|
- Если пользователь пишет просто `задеплой` без уточнения production/test, по умолчанию деплоить на `t.shineup.me`.
|
||||||
|
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`.
|
||||||
|
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`.
|
||||||
|
- Production server deploy: `./gradlew deployServerProduction`.
|
||||||
|
- Production UI deploy: `./gradlew deployUIProduction`.
|
||||||
|
- Резервный test deploy на `test.shineup.me`: `./gradlew deployServerTest` и `./gradlew deployUITest`, но пока их не использовать без отдельной причины.
|
||||||
|
- Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`).
|
||||||
|
- Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя.
|
||||||
|
- Для временной бесплатной загрузки аватаров в Arweave секретный JWK нельзя хранить в git и нельзя прописывать в репозиторный `application.properties`.
|
||||||
|
- Для продовой настройки тестового Arweave-кошелька JWK-файл нужно хранить только на сервере, например: `/home/player/SHiNE/secrets/test-free-avatar-wallet.json`.
|
||||||
|
- Для этой временной фичи на проде должны быть заданы параметры `test.freeAvatar.walletJwkPath` и `test.freeAvatar.walletAddress` через серверный override-конфиг/секреты на хосте.
|
||||||
|
- После изменения продовых значений `test.freeAvatar.*` нужно заново выполнить серверный деплой или перезапуск сервера, чтобы настройки были перечитаны приложением.
|
||||||
|
- При таких изменениях в git допускается коммитить только документацию и код чтения настроек, но не сам JWK, не содержимое секрета и не реальные приватные ключи.
|
||||||
|
|
||||||
|
## Логи звонков (установка соединения)
|
||||||
|
- Специальный поток диагностики установки звонков идёт через `CallDeliveryReport` (клиент → сервер).
|
||||||
|
- На проде специальный файл для звонков:
|
||||||
|
- `/home/player/SHiNE/shine-server/logs/call-delivery-events.log`
|
||||||
|
- Общий серверный лог (и ротации) на проде:
|
||||||
|
- `/home/player/SHiNE/shine-server/logs/app.log`
|
||||||
|
- `/home/player/SHiNE/shine-server/logs/app.YYYY-MM-DD.log`
|
||||||
|
- Для анализа причин недозвона в первую очередь фильтровать записи по ключам:
|
||||||
|
- `CallDeliveryReport`
|
||||||
|
- `call_connected`
|
||||||
|
- `outgoing_failed`
|
||||||
|
- `incoming_failed`
|
||||||
|
- `call_busy`
|
||||||
|
- `call_declined`
|
||||||
|
- `unknown_error`
|
||||||
|
- В этих записях искать поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`.
|
||||||
|
|
||||||
|
## Недопроверенные фичи (обязательно)
|
||||||
|
- Папка для учёта недопроверенных фич: `Dev_Docs/Pending_Features/`.
|
||||||
|
- По каждой новой доработке, которая требует ручной проверки, добавлять отдельный markdown-файл в `Dev_Docs/Pending_Features/`.
|
||||||
|
- Рекомендуемый формат имени файла: `YYYY-MM-DD_HHMM_<short-feature-name>.md`.
|
||||||
|
- Имена новых файлов и краткие описания фич по возможности писать на русском языке.
|
||||||
|
- Внутри файла обязательно указывать:
|
||||||
|
- краткое описание фичи;
|
||||||
|
- что именно проверять;
|
||||||
|
- ожидаемый результат;
|
||||||
|
- статус (например: `pending`, `in_progress`, `done`).
|
||||||
|
- После подтверждения, что фича проверена и работает корректно, соответствующий файл удалять.
|
||||||
|
- В `Dev_Docs/Pending_Features/README.md` вести краткий регламент и поддерживать актуальность.
|
||||||
|
|
||||||
|
## Будущие фичи
|
||||||
|
- Папка для задач, сознательно отложенных на будущее: `Dev_Docs/Future_Features/`.
|
||||||
|
- Точка входа по планам: `Dev_Docs/Future_Features/README.md`.
|
||||||
|
- Внутри планы разделены по горизонтам: `near/`, `medium/`, `far/`.
|
||||||
|
- Если пользователь спрашивает, какие есть планы или что можно продолжить, сначала читать `Dev_Docs/Future_Features/README.md`, затем при необходимости конкретные файлы из горизонтов.
|
||||||
|
- Файлы из этой папки не считать активными задачами и не начинать реализацию без явной просьбы пользователя.
|
||||||
|
- Если часть кода временно отключена или закомментирована, в файле будущей фичи подробно описывать:
|
||||||
|
- какие файлы и участки отключены;
|
||||||
|
- что осталось в коде как заготовка;
|
||||||
|
- какие документы нужно обновить при возврате;
|
||||||
|
- с какого сценария продолжать разработку.
|
||||||
|
|
||||||
|
## Коммуникация по новым задачам (обязательно)
|
||||||
|
- При получении нового задания сначала кратко пересказать задачу своими словами.
|
||||||
|
- До начала реализации задать недостающие уточняющие вопросы (если они есть).
|
||||||
|
- Если есть уместные идеи/улучшения — кратко предложить их; если полезных идей нет, ничего дополнительно не предлагать.
|
||||||
|
- Добавлять краткую оценку фичи (насколько это полезно/удачно по мнению исполнителя).
|
||||||
|
- После этого обязательно запросить подтверждение от пользователя, что задача понята верно, и только после подтверждения переходить к реализации.
|
||||||
|
- Если вопросов нет, явно написать в формате: «Я всё понял, начинаю делать?» и ждать подтверждения.
|
||||||
|
- Без подтверждения пользователя реализацию не начинать.
|
||||||
18
AGENT_DEBUG_RUNBOOK.md
Normal file
18
AGENT_DEBUG_RUNBOOK.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Runbook для агента: тест сетевого соединения
|
||||||
|
|
||||||
|
## Быстрый цикл
|
||||||
|
1. Убедись, что сервер запущен.
|
||||||
|
2. Скажи пользователю: «Запусти двух клиентов и напиши “продолжай”».
|
||||||
|
3. По команде «продолжай»:
|
||||||
|
- вызови `GET /debug/ws/clients`,
|
||||||
|
- выбери 2 активные сессии (предпочтительно разных логинов),
|
||||||
|
- вызови `POST /debug/ws/connect`,
|
||||||
|
- получи `runId`.
|
||||||
|
4. Читай `GET /debug/ws/logs?limit=200&runId=<runId>` и сообщай прогресс.
|
||||||
|
5. Если неуспех — покажи ошибки, предложи перезапуск 2 клиентов и повтори.
|
||||||
|
|
||||||
|
## Формат взаимодействия с пользователем
|
||||||
|
- Старт: «Сервер готов. Запусти двух клиентов и скажи “продолжай”.»
|
||||||
|
- После старта run: «Тест запущен, runId=..., проверяю логи.»
|
||||||
|
- Успех: «Соединение установлено, вижу connected.»
|
||||||
|
- Неуспех: «Соединение не поднялось, причины: ... Предлагаю перезапустить клиентов и повторить.»
|
||||||
11
CLAUDE.md
Normal file
11
CLAUDE.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
@AGENTS.md
|
||||||
|
@AGENT_DEBUG_RUNBOOK.md
|
||||||
|
|
||||||
|
## Обязательно читать при работе с UI
|
||||||
|
@shine-UI/AGENTS.md
|
||||||
|
|
||||||
|
## Справка по подпроектам
|
||||||
|
- При работе внутри `SHiNE-agent-bot-coder/` — читать `SHiNE-agent-bot-coder/AGENTS.md` и `SHiNE-agent-bot-coder/AGENT.md`.
|
||||||
|
- При работе внутри `shine-solana/shine/` — читать `shine-solana/shine/AGENTS.md`.
|
||||||
|
- При работе внутри `shine-UI/server-ui/` — читать `shine-UI/AGENTS.md`.
|
||||||
|
- При работе внутри `SHiNE-server/` — читать `SHiNE-server/AGENTS.md`.
|
||||||
92
DEBUG_CONNECTION_TESTING.md
Normal file
92
DEBUG_CONNECTION_TESTING.md
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# DEBUG: тестирование сетевого соединения между двумя клиентами
|
||||||
|
|
||||||
|
Документ описывает временный debug-контур для проверки WebRTC соединения между двумя активными WS-сессиями.
|
||||||
|
|
||||||
|
## 1) Подготовка
|
||||||
|
|
||||||
|
|
||||||
|
0. Убедись, что в `application.properties` включен параметр:
|
||||||
|
`debug.tempApi.enabled=true`
|
||||||
|
1. Создай файл `.debug-token` в корне проекта на основе `debug-token.example`.
|
||||||
|
2. В `.debug-token` должна быть одна строка: секретный токен.
|
||||||
|
3. Перезапусти сервер.
|
||||||
|
|
||||||
|
## 2) API debug
|
||||||
|
|
||||||
|
Базовый заголовок для всех запросов:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1 Получить список живых клиентов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s \
|
||||||
|
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>" \
|
||||||
|
http://localhost:7070/debug/ws/clients | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Ответ содержит `sessionId`, `login`, `ip`, `userAgent`, и клиентскую информацию.
|
||||||
|
|
||||||
|
### 2.2 Запустить debug-соединение между двумя сессиями
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"initiatorSessionId": "SESSION_ID_A",
|
||||||
|
"responderSessionId": "SESSION_ID_B",
|
||||||
|
"clearDebugLog": false
|
||||||
|
}' \
|
||||||
|
http://localhost:7070/debug/ws/connect | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
В ответе придёт `runId`. Его используй для фильтра логов.
|
||||||
|
|
||||||
|
### 2.3 Читать последние N debug-логов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s \
|
||||||
|
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>" \
|
||||||
|
"http://localhost:7070/debug/ws/logs?limit=200&runId=<RUN_ID>" | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Операционный сценарий “Codex + пользователь”
|
||||||
|
|
||||||
|
1. Codex поднимает сервер и сообщает пользователю ссылку на UI.
|
||||||
|
2. Codex пишет пользователю: **«Запусти двух клиентов и скажи “продолжай”»**.
|
||||||
|
3. Пользователь запускает два клиента (лучше под разными логинами).
|
||||||
|
4. Пользователь пишет: **«продолжай»**.
|
||||||
|
5. Codex:
|
||||||
|
- вызывает `/debug/ws/clients`,
|
||||||
|
- выбирает 2 сессии,
|
||||||
|
- вызывает `/debug/ws/connect`,
|
||||||
|
- получает `runId`,
|
||||||
|
- читает `/debug/ws/logs?runId=...` и сообщает прогресс.
|
||||||
|
6. Если соединение не удалось:
|
||||||
|
- Codex сообщает ошибки по логам,
|
||||||
|
- при необходимости просит перезапустить 2 клиента,
|
||||||
|
- повторяет запуск debug-run.
|
||||||
|
|
||||||
|
## 4) Какие сообщения считать успехом
|
||||||
|
|
||||||
|
- `peer_connection_connected`
|
||||||
|
- `debug_connection_success`
|
||||||
|
- `signal_sent_200/210/220` без ошибок
|
||||||
|
|
||||||
|
## 5) Что говорить пользователю в ходе прогона (через «колонку»/чат)
|
||||||
|
|
||||||
|
Рекомендуемые фразы:
|
||||||
|
|
||||||
|
- «Сервер запущен. Запусти двух клиентов и напиши “продолжай”.»
|
||||||
|
- «Вижу 2 активные сессии, запускаю тест соединения.»
|
||||||
|
- «Тест запущен, runId=... Сейчас проверяю логи.»
|
||||||
|
- «Соединение установлено / не установлено. Ниже причины и следующий шаг.»
|
||||||
|
|
||||||
|
## 6) Ограничения
|
||||||
|
|
||||||
|
- Механизм временный, не для production-эксплуатации.
|
||||||
|
- Доступ к debug API имеет любой, кто знает токен.
|
||||||
|
- Рекомендуется тестить между разными логинами.
|
||||||
17
DOC/libs/shine-server-bd/DOC.md
Normal file
17
DOC/libs/shine-server-bd/DOC.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
shine-server-bd — это библиотека реалезующая всю работу с БД:
|
||||||
|
|
||||||
|
хранит пользователей/сессии/параметры/кэш IP→гео и данные блокчейна (состояние + блоки), предоставляя единый SqliteDbController для соединений, набор DAO под каждую таблицу (Singleton, методы с Connection для транзакций и без Connection — сами открывают/закрывают), и простые entity-модели как контейнеры данных для маппинга ResultSet↔Java.
|
||||||
|
|
||||||
|
Логика структуры классов (в двух словах):
|
||||||
|
|
||||||
|
shine.db.SqliteDbController — один вход в БД: читает db.path, при отсутствии файла создаёт БД, выдаёт новые Connection и настраивает PRAGMA.
|
||||||
|
shine.db.DatabaseInitializer — разовая сборка схемы (таблицы + индексы).
|
||||||
|
|
||||||
|
|
||||||
|
shine.db.entities.* — POJO-модели строк таблиц (без логики, только поля/геттеры/сеттеры + иногда удобные методы вроде getClientKeyByte()).
|
||||||
|
shine.db.dao.* — DAO по таблицам: ActiveSessionsDAO, SolanaUsersDAO, UserParamsDAO, IpGeoCacheDAO, BlockchainStateDAO, BlocksDAO; плюс “сервисные” DAO:
|
||||||
|
|
||||||
|
UserCreateDAO — атомарная регистрация пользователя в транзакции (BEGIN IMMEDIATE + rollback/commit).
|
||||||
|
// Временное решение позволяющее регистрировать новых пользователей
|
||||||
|
// атомарно и добавляет запись и в solana_users и в BlockchainState
|
||||||
|
|
||||||
85
DOC/libs/shine-server-blockchain/Doc.md
Normal file
85
DOC/libs/shine-server-blockchain/Doc.md
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
shine-server-blockchain — это библиотека, которая задаёт формат блока, правила парсинга/валидации тела, крипто-проверку (hash+Ed25519) и безопасную работу с файлами блокчейна (data/<name>.bch через временный .tmp_bch).
|
||||||
|
|
||||||
|
Как устроена структура и логика работы
|
||||||
|
|
||||||
|
1) “Блок” как центральный объект (ядро)
|
||||||
|
BchBlockEntry — единая модель блока “как лежит на диске/в сети”:
|
||||||
|
читает/собирает байты в формате RAW + signature64 + hash32
|
||||||
|
сразу парсит body через BodyRecordParser
|
||||||
|
сразу проверяет что lineIndex совпадает с тем, что ожидает конкретный тип body (expectedLineIndex())
|
||||||
|
То есть: всё, что считается “блоком”, обязано быть самодостаточно валидным уже на этапе создания объекта.
|
||||||
|
|
||||||
|
2) “Body” как плагины по типам (расширяемая часть) ! <-- Новые типы записей добавлть сюда !
|
||||||
|
BodyRecord — интерфейс контракта для всех тел:
|
||||||
|
type/version — идентификаторы формата
|
||||||
|
expectedLineIndex() — жёсткое правило “в какой линии может жить”
|
||||||
|
check() — логическая валидация содержимого
|
||||||
|
toBytes() — сериализация обратно в бинарь
|
||||||
|
|
||||||
|
BodyRecordParser — диспетчер: читает первые 4 байта (type+ver) и выбирает нужный класс:
|
||||||
|
HeaderBody (lineIndex=0)
|
||||||
|
TextBody (lineIndex=1)
|
||||||
|
ReactionBody (lineIndex=2)
|
||||||
|
|
||||||
|
Добавление нового типа = добавить новый класс XxxBody + кейс в BodyRecordParser.
|
||||||
|
|
||||||
|
3) Криптография как отдельный слой проверки
|
||||||
|
BchCryptoVerifier отвечает за “как получить хэш и как проверить подпись”:
|
||||||
|
строит preimage = "SHiNE" + login + prevGlobalHash32 + prevLineHash32 + rawBytes
|
||||||
|
считает sha256(preimage) и сравнивает с hash32 внутри блока
|
||||||
|
проверяет Ed25519 подпись над hash32
|
||||||
|
Важно: BchBlockEntry не проверяет подпись — он проверяет структуру блока и правильность body/lineIndex, а криптопроверка вынесена отдельно.
|
||||||
|
|
||||||
|
4) Утилиты вокруг имени и файлов
|
||||||
|
|
||||||
|
BlockchainNameUtil — извлекает login из blockchainName (отрезает 3 символа суффикса).
|
||||||
|
FileStoreUtil — безопасное файловое хранилище:
|
||||||
|
|
||||||
|
|
||||||
|
5) Объяснение структуры работы
|
||||||
|
|
||||||
|
Типичный сценарий: пришёл блок → проверить → принять
|
||||||
|
Шаг 0. Контекст (что у нас уже есть снаружи)
|
||||||
|
|
||||||
|
Снаружи библиотеки (в сервере) у тебя уже известны:
|
||||||
|
userLogin — владелец блокчейна
|
||||||
|
publicKey32 — публичный ключ пользователя
|
||||||
|
prevGlobalHash32 — хэш предыдущего блока по глобальной цепи
|
||||||
|
prevLineHash32 — хэш предыдущего блока по текущей линии
|
||||||
|
|
||||||
|
Библиотека не хранит это сама, она ожидает, что сервер это передаст.
|
||||||
|
|
||||||
|
Шаг 1. Парсинг блока (структура + логика)
|
||||||
|
BchBlockEntry block = new BchBlockEntry(fullBytes);
|
||||||
|
|
||||||
|
|
||||||
|
Что происходит здесь автоматически:
|
||||||
|
проверяется длина блока
|
||||||
|
проверяется recordSize
|
||||||
|
парсится RAW-заголовок
|
||||||
|
парсится body через BodyRecordParser
|
||||||
|
проверяется, что lineIndex соответствует типу body
|
||||||
|
(HEADER → line 0, TEXT → line 1, REACTION → line 2 и т.д.)
|
||||||
|
❗ На этом шаге никакой криптографии ещё нет — только структура и логика формата.
|
||||||
|
|
||||||
|
Если тут не упало исключение → блок структурно корректен.
|
||||||
|
|
||||||
|
Шаг 2. Подготовка данных для криптопроверки (они получаются просто из частей байтов полного блокас подписью)
|
||||||
|
byte[] rawBytes = block.getRawBytes();
|
||||||
|
byte[] signature64 = block.getSignature64();
|
||||||
|
byte[] hash32FromTail = block.getHash32();
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
rawBytes — это ровно те байты, которые участвовали в хэшировании
|
||||||
|
hash32FromTail — это то, что автор блока положил внутрь блока
|
||||||
|
|
||||||
|
Шаг 3. Криптографическая проверка (ключевой вызов)
|
||||||
|
boolean ok = BchCryptoVerifier.verifyAll(
|
||||||
|
userLogin,
|
||||||
|
prevGlobalHash32,
|
||||||
|
prevLineHash32,
|
||||||
|
rawBytes,
|
||||||
|
signature64,
|
||||||
|
publicKey32,
|
||||||
|
hash32FromTail
|
||||||
|
);
|
||||||
48
DOC/libs/shine-server-blockchain/Общая структура блока.md
Normal file
48
DOC/libs/shine-server-blockchain/Общая структура блока.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
Общая структура блока
|
||||||
|
|
||||||
|
Блок — это бинарная запись фиксированного формата:
|
||||||
|
|
||||||
|
[ RAW ][ signature64 ][ hash32 ]
|
||||||
|
|
||||||
|
RAW — данные блока (участвуют в хэшировании и подписи)
|
||||||
|
signature64 — Ed25519-подпись над hash32
|
||||||
|
hash32 — SHA-256 от preimage (привязан к цепочке и владельцу)
|
||||||
|
|
||||||
|
RAW-часть (BigEndian)
|
||||||
|
recordSize int32 — размер RAW (без signature+hash)
|
||||||
|
recordNumber int32 — глобальный номер блока
|
||||||
|
timestamp int64 — unix time (seconds)
|
||||||
|
lineIndex int16 — индекс линии
|
||||||
|
lineNumber int32 — номер блока внутри линии
|
||||||
|
bodyBytes bytes — тело блока (type+version+payload)
|
||||||
|
|
||||||
|
Общая структура блокчейна
|
||||||
|
|
||||||
|
Блокчейн — это:
|
||||||
|
линейная цепочка блоков (по recordNumber)
|
||||||
|
внутри неё — параллельные логические линии (lineIndex)
|
||||||
|
каждая линия имеет собственную нумерацию (lineNumber) и prevLineHash
|
||||||
|
вся цепочка связана ещё и prevGlobalHash
|
||||||
|
|
||||||
|
👉 Таким образом, каждый блок:
|
||||||
|
связан с предыдущим глобальным блоком
|
||||||
|
и с предыдущим блоком своей линии
|
||||||
|
|
||||||
|
Это даёт:
|
||||||
|
строгий порядок всей истории
|
||||||
|
и независимую валидацию логических потоков
|
||||||
|
|
||||||
|
Криптографический смысл блока
|
||||||
|
|
||||||
|
Хэш блока считается от:
|
||||||
|
"SHiNE" +
|
||||||
|
login +
|
||||||
|
prevGlobalHash32 +
|
||||||
|
prevLineHash32 +
|
||||||
|
RAW
|
||||||
|
|
||||||
|
Это означает:
|
||||||
|
блок жёстко привязан к владельцу (login)
|
||||||
|
блок невозможно перенести в другую цепочку
|
||||||
|
подмена предыдущего блока ломает всю цепь
|
||||||
|
Подпись Ed25519 делается над этим хэшем.
|
||||||
36
DOC/libs/shine-server-blockchain/Формат существующих Body.md
Normal file
36
DOC/libs/shine-server-blockchain/Формат существующих Body.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
Формат и смысл существующих Body
|
||||||
|
|
||||||
|
1) HeaderBody (type=0, ver=1)
|
||||||
|
|
||||||
|
Линия: lineIndex = 0
|
||||||
|
Смысл:
|
||||||
|
Генезис-блок блокчейна, объявляет формат и владельца.
|
||||||
|
|
||||||
|
Содержит:
|
||||||
|
сигнатуру формата "SHiNE"
|
||||||
|
login владельца блокчейна
|
||||||
|
👉 Всегда первый блок, всегда в линии 0.
|
||||||
|
|
||||||
|
2) TextBody (type=1, ver=1)
|
||||||
|
|
||||||
|
Линия: lineIndex = 1
|
||||||
|
Смысл:
|
||||||
|
Основной контент — текстовые записи (посты, сообщения, дневник).
|
||||||
|
|
||||||
|
Содержит:
|
||||||
|
UTF-8 текст произвольной длины
|
||||||
|
👉 Это “основная история” блокчейна пользователя.
|
||||||
|
|
||||||
|
3) ReactionBody (type=2, ver=1)
|
||||||
|
|
||||||
|
Линия: lineIndex = 2
|
||||||
|
Смысл:
|
||||||
|
Связь с другим блокчейном или блоком (реакция, ответ, лайк, ссылка).
|
||||||
|
|
||||||
|
Содержит:
|
||||||
|
код реакции
|
||||||
|
имя целевого блокчейна
|
||||||
|
globalNumber целевого блока
|
||||||
|
hash32 целевого блока
|
||||||
|
👉 Это механизм межблокчейн-связей без изменения чужих цепочек.
|
||||||
|
|
||||||
7
DOC/libs/shine-server-config/doc.md
Normal file
7
DOC/libs/shine-server-config/doc.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
shine-server-config
|
||||||
|
|
||||||
|
Минимальная библиотека конфигурации, предоставляющая потокобезопасный singleton-доступ к параметрам из application.properties.
|
||||||
|
|
||||||
|
Настройки:
|
||||||
|
server.port=7070 — порт запуска сервера
|
||||||
|
db.path=data/shine.sqlite — путь к SQLite базе данных
|
||||||
8
DOC/libs/shine-server-crypto/DOC.md
Normal file
8
DOC/libs/shine-server-crypto/DOC.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
shine-server-crypto
|
||||||
|
|
||||||
|
О чём: базовые крипто-утилиты для SHA-256 и Ed25519 (BouncyCastle) + проверка подписи/хэша для .bch сущностей и маленький self-test.
|
||||||
|
Внешние методы, которые вызываются:
|
||||||
|
BchCryptoVerifier.verifyAll(), BchCryptoVerifier.buildPreimage(), Ed25519Util.generatePrivateKey(), Ed25519Util.generatePrivateKeyFromString(), Ed25519Util.derivePublicKey(), Ed25519Util.sign(), Ed25519Util.verify(), Ed25519Util.keyToBase64(), Ed25519Util.keyFromBase64(),
|
||||||
|
HashSHA256Util.sha256()
|
||||||
|
|
||||||
|
HashSHA256Util.loginToLoginId(), HashSHA256Util.loginIdFromLogin(),
|
||||||
19
DOC/libs/shine-server-geo/DOC.md
Normal file
19
DOC/libs/shine-server-geo/DOC.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
shine-server-geo
|
||||||
|
|
||||||
|
Назначение: утилиты для получения “кто подключился” (IP/UA/язык) и геолокации по IP (с опциональным кэшем в БД).
|
||||||
|
|
||||||
|
Классы:
|
||||||
|
|
||||||
|
ClientInfoService — собирает строку UA/ch-ua/platform/mobile/remoteIP, вытаскивает реальный IP (приоритет: X-Forwarded-For → X-Real-IP → remoteAddress), парсит первый Accept-Language.
|
||||||
|
GeoLookupService — геолокация по IP через внешний API (ip-api.com), умеет вариант без кэша и с кэшем в таблице ip_geo_cache через IpGeoCacheDAO (пишет даже unknown), плюс метод получения внешнего IP через api.ipify.org.
|
||||||
|
GeoLookupTestMain — консольный тест: берёт IP из аргумента или определяет внешний, вызывает геолокацию и печатает результат + время.
|
||||||
|
|
||||||
|
|
||||||
|
Внешние (публично используемые) методы:
|
||||||
|
|
||||||
|
ClientInfoService.buildClientInfoString(Session) — формирует строку с User-Agent, client-hints и реальным IP клиента
|
||||||
|
ClientInfoService.extractClientIp(Session) — извлекает реальный IP (X-Forwarded-For / X-Real-IP / remoteAddress)
|
||||||
|
ClientInfoService.extractPreferredLanguageTag(Session) — возвращает основной язык клиента из Accept-Language
|
||||||
|
GeoLookupService.resolveCountryCityOrIp(String ip) — геолокация по IP без кэша (Country, City или unknown)
|
||||||
|
GeoLookupService.resolveCountryCityOrIpWithCache(String ip) — геолокация по IP с кэшированием в БД
|
||||||
|
GeoLookupService.fetchPublicIpOrDefault(String fallbackIp) — получение внешнего IP текущей машины
|
||||||
10
DOC/libs/shine-server-log/DOC.md
Normal file
10
DOC/libs/shine-server-log/DOC.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
shine-log (BlockchainAdminNotifier)
|
||||||
|
|
||||||
|
Суть (1 предложение): единая точка для “красного” оповещения админа о критических проблемах консистентности, сейчас — через максимально заметный log.error, позже — через Telegram/email/webhook и т.п.
|
||||||
|
|
||||||
|
Структура (очень кратко):
|
||||||
|
|
||||||
|
BlockchainAdminNotifier (final utility)
|
||||||
|
|
||||||
|
BlockchainAdminNotifier.critical(String message)
|
||||||
|
BlockchainAdminNotifier.critical(String message, Throwable t)
|
||||||
52
DOC/libs/shine-server-protocol/doc.md
Normal file
52
DOC/libs/shine-server-protocol/doc.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
shine-server-protocol
|
||||||
|
Библиотека JSON-протокол поверх WebSocket для взаимодействия с клиентами.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Всё общение — JSON поверх WebSocket
|
||||||
|
Формат всегда один:
|
||||||
|
|
||||||
|
request: op + requestId + payload
|
||||||
|
response: op + requestId + status + payload
|
||||||
|
|
||||||
|
|
||||||
|
Net_Event / Net_Request / Net_Response
|
||||||
|
Базовые классы протокола.
|
||||||
|
requestId связывает запрос и ответ, status = результат.
|
||||||
|
|
||||||
|
Хэндлер = логика операции
|
||||||
|
Каждый op обрабатывается своим JsonMessageHandler.
|
||||||
|
|
||||||
|
Entities (Request / Response)
|
||||||
|
DTO-классы для Jackson:
|
||||||
|
|
||||||
|
Net_Xxx_Request — что приходит от клиента
|
||||||
|
|
||||||
|
Net_Xxx_Response — что уходит клиенту
|
||||||
|
|
||||||
|
JsonHandlerRegistry
|
||||||
|
Связывает:
|
||||||
|
|
||||||
|
op → RequestClass
|
||||||
|
op → Handler
|
||||||
|
|
||||||
|
|
||||||
|
JsonInboundProcessor
|
||||||
|
Единая точка входа:
|
||||||
|
парсит JSON → маппит payload → вызывает handler → собирает ответ JSON.
|
||||||
|
|
||||||
|
Папки по темам
|
||||||
|
|
||||||
|
auth/ — авторизация и сессии
|
||||||
|
(AuthChallenge → CreateAuthSession → Refresh / List / Close)
|
||||||
|
|
||||||
|
blockchain/ — AddBlock
|
||||||
|
|
||||||
|
tempToTest/ — AddUser (временный, потом уйдёт в блокчейн-логику)
|
||||||
|
|
||||||
|
ConnectionContext
|
||||||
|
Состояние одного WebSocket-подключения (login, session, authStatus).
|
||||||
|
|
||||||
|
ActiveConnectionsRegistry
|
||||||
|
Глобальный реестр активных авторизованных соединений
|
||||||
|
(нужно для закрытия других сессий).
|
||||||
140
Dev_Docs/API/00_Common_API_Format.md
Normal file
140
Dev_Docs/API/00_Common_API_Format.md
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# API для разработчиков: Общий формат запросов и ответов
|
||||||
|
|
||||||
|
Этот файл описывает не конкретные операции, а общий wire-контракт всего API сервера.
|
||||||
|
|
||||||
|
Здесь зафиксировано:
|
||||||
|
|
||||||
|
- как выглядит любой запрос;
|
||||||
|
- как выглядит любой успешный ответ;
|
||||||
|
- как выглядит любой ответ с ошибкой;
|
||||||
|
- какие поля являются обязательными для всех операций;
|
||||||
|
- как клиент должен интерпретировать `status`, `ok` и `payload`.
|
||||||
|
|
||||||
|
Логика простая: сначала клиент и сервер договариваются о едином формате конверта, и только потом в остальных документах уже описываются конкретные методы и их поля.
|
||||||
|
|
||||||
|
## 1. Общий формат запроса
|
||||||
|
|
||||||
|
Все запросы по WebSocket используют один и тот же JSON-конверт:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "OperationName",
|
||||||
|
"requestId": "req-001",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поля
|
||||||
|
|
||||||
|
- `op` — имя операции.
|
||||||
|
- `requestId` — клиентский идентификатор запроса.
|
||||||
|
- `payload` — объект параметров операции.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Общий формат успешного ответа
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "OperationName",
|
||||||
|
"requestId": "req-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Общий формат ответа с ошибкой
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "OperationName",
|
||||||
|
"requestId": "req-001",
|
||||||
|
"status": 400,
|
||||||
|
"ok": false,
|
||||||
|
"error": "BAD_REQUEST",
|
||||||
|
"message": "Human readable description",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Обязательные правила
|
||||||
|
|
||||||
|
- Сервер возвращает `op` в каждом ответе.
|
||||||
|
- Сервер возвращает `requestId` в каждом ответе без изменений.
|
||||||
|
- Сервер возвращает `status` в каждом ответе.
|
||||||
|
- Сервер возвращает `ok` в каждом ответе.
|
||||||
|
- Сервер всегда возвращает `payload` как объект.
|
||||||
|
- Даже при отсутствии данных сервер возвращает `payload: {}`.
|
||||||
|
- `ok` находится на верхнем уровне ответа, а не внутри `payload`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Правило интерпретации
|
||||||
|
|
||||||
|
Источник истины — `status`.
|
||||||
|
|
||||||
|
- если `status` в диапазоне `200..299`, то ответ успешный и `ok` должен быть `true`;
|
||||||
|
- если `status` вне диапазона `200..299`, то ответ ошибочный и `ok` должен быть `false`.
|
||||||
|
|
||||||
|
Запрещённые состояния:
|
||||||
|
|
||||||
|
- `status = 200` и `ok = false`;
|
||||||
|
- `status = 400` и `ok = true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Общие правила формата
|
||||||
|
|
||||||
|
- Все строки подписи и challenge собираются в UTF-8.
|
||||||
|
- Временные метки передаются как Unix time в миллисекундах.
|
||||||
|
- Бинарные поля передаются строками Base64.
|
||||||
|
- При ошибке `error` — это машинный код причины.
|
||||||
|
- При ошибке `message` — человекочитаемое описание причины.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Общие коды ошибок
|
||||||
|
|
||||||
|
Ниже перечислены коды ошибок, которые не привязаны к одной конкретной операции и могут встречаться в разных местах API.
|
||||||
|
|
||||||
|
- `400 / EMPTY_JSON` — клиент отправил пустое или полностью отсутствующее JSON-сообщение.
|
||||||
|
- `400 / NO_OP` — в корневом объекте не передано поле `op`.
|
||||||
|
- `400 / UNKNOWN_OP` — сервер не знает такую операцию.
|
||||||
|
- `400 / NO_PAYLOAD` — в корневом объекте отсутствует `payload`.
|
||||||
|
- `400 / BAD_PAYLOAD` — `payload` передан, но это не JSON-объект.
|
||||||
|
- `400 / BAD_REQUEST_FORMAT` — JSON-конверт формально валиден, но поля операции не удалось распарсить в ожидаемый формат.
|
||||||
|
- `500 / INTERNAL_HANDLER_ERROR` — в handler конкретной операции случилась непредвиденная серверная ошибка.
|
||||||
|
- `500 / INTERNAL_ERROR` — произошла внутренняя ошибка на уровне общего JSON-процессора или другого серверного слоя.
|
||||||
|
|
||||||
|
Общее правило для dev/test этапа:
|
||||||
|
|
||||||
|
- `message` в таких ошибках должен быть коротким, но полезным;
|
||||||
|
- по возможности сервер добавляет тип исключения и краткую деталь причины;
|
||||||
|
- это сделано для упрощения интеграционных тестов и отладки;
|
||||||
|
- позже для production этот уровень детализации может быть уменьшен.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Источник истины по списку операций
|
||||||
|
|
||||||
|
Фактический список публичных WebSocket-операций берётся из:
|
||||||
|
|
||||||
|
- `shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java`.
|
||||||
|
|
||||||
|
Если операция зарегистрирована в `HANDLERS` и `REQUEST_TYPES`, она считается доступной через JSON/WebSocket API. Общий актуальный индекс таких операций поддерживается в `Dev_Docs/API/09_Operations_Index.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Короткое резюме
|
||||||
|
|
||||||
|
- Запросы всегда идут как `op + requestId + payload`.
|
||||||
|
- Ответы всегда идут как `op + requestId + status + ok + payload`.
|
||||||
|
- Ошибки всегда возвращают `ok: false`, `error`, `message`, `payload: {}`.
|
||||||
200
Dev_Docs/API/01_User_Registration_API.md
Normal file
200
Dev_Docs/API/01_User_Registration_API.md
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
# API для разработчиков: Регистрация пользователя
|
||||||
|
|
||||||
|
Этот файл описывает раздел API, связанный с проверкой наличия пользователя на сервере и dev/test операциями.
|
||||||
|
|
||||||
|
Сейчас здесь три метода:
|
||||||
|
|
||||||
|
- `AddUser` — операция отключена (регистрация только через Solana);
|
||||||
|
- `GetUser` — временная серверная проверка существования пользователя и чтение его базовых данных;
|
||||||
|
- `SearchUsers` — dev/test поиск логинов по префиксу.
|
||||||
|
|
||||||
|
Регистрация выполняется через Solana (`shine_users`). Сервер при входе может лениво импортировать пользователя из Solana PDA в локальную БД, если записи ещё нет.
|
||||||
|
|
||||||
|
## Статус документа
|
||||||
|
|
||||||
|
Это временная глава API.
|
||||||
|
|
||||||
|
Текущая регистрация пользователя и текущая проверка, существует пользователь или нет, пока реализованы как серверные dev/test операции. В будущем и регистрация, и проверка identity должны идти напрямую через Solana.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Операция `AddUser`
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Операция отключена. Используется только как явный ответ клиентам старых версий.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AddUser",
|
||||||
|
"requestId": "reg-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "anya",
|
||||||
|
"blockchainName": "anya-001",
|
||||||
|
"solanaKey": "BASE64_32_PUBLIC_KEY",
|
||||||
|
"blockchainKey": "BASE64_32_PUBLIC_KEY",
|
||||||
|
"clientKey": "BASE64_32_PUBLIC_KEY",
|
||||||
|
"bchLimit": 1000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример ответа
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AddUser",
|
||||||
|
"requestId": "reg-001",
|
||||||
|
"status": 410,
|
||||||
|
"ok": false,
|
||||||
|
"error": "ADD_USER_DISABLED",
|
||||||
|
"message": "Серверная регистрация AddUser отключена. Используйте регистрацию через Solana.",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `AddUser`
|
||||||
|
|
||||||
|
- `410 / ADD_USER_DISABLED` — серверная регистрация отключена, используйте Solana-first flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Операция `GetUser`
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Временная серверная проверка, существует пользователь или нет.
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
- это server-side existence-check;
|
||||||
|
- если пользователя нет в локальной БД, он может быть импортирован при авторизации из Solana PDA.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetUser",
|
||||||
|
"requestId": "user-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "anya"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ: пользователь существует
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetUser",
|
||||||
|
"requestId": "user-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"exists": true,
|
||||||
|
"login": "Anya",
|
||||||
|
"blockchainName": "anya-001",
|
||||||
|
"solanaKey": "BASE64_32_PUBLIC_KEY",
|
||||||
|
"blockchainKey": "BASE64_32_PUBLIC_KEY",
|
||||||
|
"clientKey": "BASE64_32_PUBLIC_KEY",
|
||||||
|
"serverLastGlobalNumber": 128,
|
||||||
|
"serverLastGlobalHash": "4f...ab",
|
||||||
|
"serverBlockchainSizeBytes": 45212,
|
||||||
|
"serverBlockchainSizeLimitBytes": 100000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Дополнительные серверные поля в `GetUser`:
|
||||||
|
|
||||||
|
- `serverLastGlobalNumber` — номер последнего блока в пользовательском блокчейне на сервере;
|
||||||
|
- `serverLastGlobalHash` — hash последнего блока (hex-строка 64 символа);
|
||||||
|
- `serverBlockchainSizeBytes` — текущий размер пользовательского блокчейна на сервере в байтах;
|
||||||
|
- `serverBlockchainSizeLimitBytes` — текущий лимит размера блокчейна на сервере в байтах;
|
||||||
|
|
||||||
|
### Успешный ответ: пользователя нет
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetUser",
|
||||||
|
"requestId": "user-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"exists": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример ошибки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetUser",
|
||||||
|
"requestId": "user-001",
|
||||||
|
"status": 400,
|
||||||
|
"ok": false,
|
||||||
|
"error": "BAD_FIELDS",
|
||||||
|
"message": "Некорректные поля: login",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `GetUser`
|
||||||
|
|
||||||
|
- `400 / BAD_FIELDS` — не передан или пуст `login`.
|
||||||
|
- `501 / DB_ERROR` — ошибка БД при поиске пользователя.
|
||||||
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Операция `SearchUsers`
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Поиск пользователей по префиксу логина. Операция зарегистрирована в серверном API и используется как вспомогательная dev/test операция.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SearchUsers",
|
||||||
|
"requestId": "search-001",
|
||||||
|
"payload": {
|
||||||
|
"prefix": "an"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SearchUsers",
|
||||||
|
"requestId": "search-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"logins": ["anya", "andrey"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `SearchUsers`
|
||||||
|
|
||||||
|
- `400 / BAD_FIELDS` — некорректный или пустой `prefix`.
|
||||||
|
- `501 / DB_ERROR` — ошибка БД при поиске.
|
||||||
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Короткое резюме
|
||||||
|
|
||||||
|
- `AddUser` — отключен (`410 / ADD_USER_DISABLED`).
|
||||||
|
- `GetUser` — проверка существования пользователя на сервере.
|
||||||
|
- `SearchUsers` — временный поиск пользователей по префиксу.
|
||||||
|
- Регистрация выполняется только через Solana.
|
||||||
341
Dev_Docs/API/02_Authentication_API.md
Normal file
341
Dev_Docs/API/02_Authentication_API.md
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
# API для разработчиков: Авторизация
|
||||||
|
|
||||||
|
Этот файл описывает именно этапы авторизации клиента, то есть как создать новую сессию и как войти в уже существующую.
|
||||||
|
|
||||||
|
Здесь четыре базовых метода обычной авторизации:
|
||||||
|
|
||||||
|
- `AuthChallenge`
|
||||||
|
- `CreateAuthSession`
|
||||||
|
- `SessionChallenge`
|
||||||
|
- `SessionLogin`
|
||||||
|
|
||||||
|
Логика раздела такая:
|
||||||
|
|
||||||
|
- сначала клиент либо начинает создание новой сессии через `clientKey`;
|
||||||
|
- либо начинает вход в уже созданную сессию через `sessionKey`;
|
||||||
|
- сервер на первом шаге выдаёт challenge/nonce;
|
||||||
|
- на втором шаге клиент присылает подписанный ответ;
|
||||||
|
- сервер сверяет актуальные публичные ключи и только потом проверяет подпись.
|
||||||
|
|
||||||
|
Новые поля этого раздела:
|
||||||
|
|
||||||
|
- `sessionType` — числовой код типа сессии;
|
||||||
|
- `clientPlatform` — свободная строка платформы клиента.
|
||||||
|
|
||||||
|
Текущие поддерживаемые коды `sessionType`:
|
||||||
|
|
||||||
|
- `1` — обычный клиент;
|
||||||
|
- `50` — кошелёк;
|
||||||
|
- `100` — homeserver.
|
||||||
|
|
||||||
|
Правило проверки `sessionType`:
|
||||||
|
|
||||||
|
1. если в `Solana PDA` нет записи для `sessionKey`, сервер принимает `sessionType`, присланный клиентом;
|
||||||
|
2. если запись в `PDA` есть, `sessionType` в запросе должен совпадать с `session_type` из `PDA`;
|
||||||
|
3. при несовпадении сервер возвращает `460 / SESSION_TYPE_MISMATCH`.
|
||||||
|
|
||||||
|
Ниже в документе сначала описан сценарий, а потом зафиксированы точные форматы запросов и ответов.
|
||||||
|
|
||||||
|
Отдельно появился новый серверный сценарий pairing через доверенный homeserver/ESP. Он не заменяет обычный вход и описан в:
|
||||||
|
|
||||||
|
- `Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md`
|
||||||
|
|
||||||
|
Кратко:
|
||||||
|
|
||||||
|
- `AuthChallenge/CreateAuthSession` и `SessionChallenge/SessionLogin` остаются каноническими потоками обычной авторизации;
|
||||||
|
- pairing через ESP идёт отдельными `op` и только подготавливает безопасное добавление новой сессии;
|
||||||
|
- решение об одобрении pairing принимает любая уже авторизованная доверенная сессия пользователя.
|
||||||
|
|
||||||
|
## 1. Поток авторизации
|
||||||
|
|
||||||
|
Поддерживаются два сценария:
|
||||||
|
|
||||||
|
1. Создание новой сессии:
|
||||||
|
`AuthChallenge` -> `CreateAuthSession`
|
||||||
|
2. Вход в существующую сессию:
|
||||||
|
`SessionChallenge` -> `SessionLogin`
|
||||||
|
|
||||||
|
`clientKey` используется для создания новой сессии.
|
||||||
|
|
||||||
|
`sessionKey` используется для входа в уже созданную сессию.
|
||||||
|
|
||||||
|
`sessionKey` передаётся и хранится целиком одной строкой, например:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ed25519/BASE64_PUBLIC_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `AuthChallenge`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AuthChallenge",
|
||||||
|
"requestId": "auth-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AuthChallenge",
|
||||||
|
"requestId": "auth-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"authNonce": "8f2f0f71-0b1c-4ab2-8f5d-0bc5d6f6aa11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `AuthChallenge`
|
||||||
|
|
||||||
|
- `400 / EMPTY_LOGIN` — пустой `login`.
|
||||||
|
- `400 / ALREADY_AUTHED` — по текущему соединению уже выполнена авторизация.
|
||||||
|
- `422 / UNKNOWN_USER` — пользователь с таким `login` не найден.
|
||||||
|
- `501 / SOLANA_IMPORT_FAILED` — сервер не смог проверить/импортировать пользователя из Solana при lazy-import.
|
||||||
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера, если появится вне штатного сценария.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `CreateAuthSession`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CreateAuthSession",
|
||||||
|
"requestId": "create-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice",
|
||||||
|
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||||||
|
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET",
|
||||||
|
"timeMs": 1774600000123,
|
||||||
|
"authNonce": "nonce",
|
||||||
|
"clientKey": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
|
"signatureB64": "BASE64_SIGNATURE",
|
||||||
|
"sessionType": 1,
|
||||||
|
"clientPlatform": "Web",
|
||||||
|
"clientInfo": "Android 15; Pixel 9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Строка для подписи
|
||||||
|
|
||||||
|
```text
|
||||||
|
AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Дополнительная проверка ключа
|
||||||
|
|
||||||
|
Перед проверкой подписи сервер должен:
|
||||||
|
|
||||||
|
1. взять актуальный `solana_users.client_key`;
|
||||||
|
2. сравнить его с `payload.clientKey`;
|
||||||
|
3. только потом проверять подпись.
|
||||||
|
|
||||||
|
Если `clientKey` не совпадает, сервер возвращает ошибку `DEVICE_KEY_NOT_ACTUAL`.
|
||||||
|
|
||||||
|
На будущее:
|
||||||
|
|
||||||
|
- для ротации `client_key` желательно добавить перепроверку через Solana.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CreateAuthSession",
|
||||||
|
"requestId": "create-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"sessionId": "sess_7c5e5c4b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `CreateAuthSession`
|
||||||
|
|
||||||
|
- `400 / NO_STEP1_CONTEXT` — для данного соединения не был корректно выполнен `AuthChallenge`.
|
||||||
|
- `400 / EMPTY_LOGIN` — пустой `login`.
|
||||||
|
- `400 / LOGIN_MISMATCH` — `login` не совпадает с тем, для кого был выдан `authNonce`.
|
||||||
|
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при повторном чтении пользователя.
|
||||||
|
- `422 / USER_NOT_FOUND` — пользователь не найден.
|
||||||
|
- `501 / NO_LOGIN` — у пользователя на сервере не заполнен `login`.
|
||||||
|
- `400 / EMPTY_STORAGE_PWD` — пустой `storagePwd`.
|
||||||
|
- `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`.
|
||||||
|
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` или `clientKey` не поддерживается текущим сервером.
|
||||||
|
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey`, `clientKey` или `signatureB64`.
|
||||||
|
- `400 / EMPTY_SIGNATURE` — пустая подпись.
|
||||||
|
- `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна.
|
||||||
|
- `400 / NO_DEVICE_KEY` — у пользователя в БД отсутствует `clientKey`.
|
||||||
|
- `400 / EMPTY_AUTH_NONCE` — пустой `authNonce`.
|
||||||
|
- `400 / AUTH_NONCE_MISMATCH` — `authNonce` не соответствует значению из `AuthChallenge`.
|
||||||
|
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `clientKey`.
|
||||||
|
- `422 / DEVICE_KEY_NOT_ACTUAL` — `clientKey` не совпадает с актуальной версией на сервере.
|
||||||
|
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||||||
|
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
||||||
|
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
|
||||||
|
- `501 / DB_ERROR_SESSION_CREATE` — ошибка БД при создании записи активной сессии.
|
||||||
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `SessionChallenge`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SessionChallenge",
|
||||||
|
"requestId": "sch-001",
|
||||||
|
"payload": {
|
||||||
|
"sessionId": "sess_7c5e5c4b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SessionChallenge",
|
||||||
|
"requestId": "sch-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"nonce": "0e5bb0f4-c7d8-4efb-b44d-bf31a6126c66"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `SessionChallenge`
|
||||||
|
|
||||||
|
- `400 / EMPTY_SESSION_ID` — пустой `sessionId`.
|
||||||
|
- `501 / DB_ERROR` — ошибка БД при чтении сессии.
|
||||||
|
- `422 / SESSION_NOT_FOUND` — сессия не найдена.
|
||||||
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. `SessionLogin`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SessionLogin",
|
||||||
|
"requestId": "slogin-001",
|
||||||
|
"payload": {
|
||||||
|
"sessionId": "sess_7c5e5c4b",
|
||||||
|
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||||||
|
"timeMs": 1774600010456,
|
||||||
|
"signatureB64": "BASE64_SIGNATURE",
|
||||||
|
"sessionType": 1,
|
||||||
|
"clientPlatform": "Web",
|
||||||
|
"clientInfo": "Android 15; Pixel 9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Строка для подписи
|
||||||
|
|
||||||
|
```text
|
||||||
|
SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Дополнительная проверка ключа
|
||||||
|
|
||||||
|
Перед проверкой подписи сервер должен:
|
||||||
|
|
||||||
|
1. взять `active_sessions.session_key`;
|
||||||
|
2. сравнить его с `payload.sessionKey`;
|
||||||
|
3. только потом проверять подпись.
|
||||||
|
|
||||||
|
Если ключ не совпадает, сервер возвращает ошибку `SESSION_KEY_NOT_ACTUAL`.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SessionLogin",
|
||||||
|
"requestId": "slogin-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `SessionLogin`
|
||||||
|
|
||||||
|
- `400 / EMPTY_SESSION_ID` — пустой `sessionId`.
|
||||||
|
- `400 / NO_CHALLENGE` — перед `SessionLogin` не был успешно выполнен `SessionChallenge` либо nonce уже истёк.
|
||||||
|
- `400 / SESSION_ID_MISMATCH` — nonce был выдан для другого `sessionId`.
|
||||||
|
- `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна.
|
||||||
|
- `400 / EMPTY_SIGNATURE` — пустая подпись.
|
||||||
|
- `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`.
|
||||||
|
- `501 / DB_ERROR` — ошибка БД при чтении сессии.
|
||||||
|
- `422 / SESSION_NOT_FOUND` — сессия не найдена.
|
||||||
|
- `501 / NO_SESSION_KEY` — у сессии отсутствует `session_key`.
|
||||||
|
- `422 / SESSION_KEY_NOT_ACTUAL` — переданный `sessionKey` не совпадает с актуальной версией на сервере.
|
||||||
|
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` не поддерживается текущим сервером.
|
||||||
|
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey` или `signatureB64`.
|
||||||
|
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||||||
|
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
||||||
|
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
|
||||||
|
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при чтении пользователя для этой сессии.
|
||||||
|
- `422 / USER_NOT_FOUND_FOR_SESSION` — пользователь, которому принадлежит сессия, не найден.
|
||||||
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Pairing через homeserver/ESP
|
||||||
|
|
||||||
|
Новые `op`, относящиеся к этому сценарию:
|
||||||
|
|
||||||
|
- `GetTrustedDeviceLoginSettings`
|
||||||
|
- `UpsertTrustedDeviceLoginSettings`
|
||||||
|
- `StartTrustedDeviceLogin`
|
||||||
|
- `ListTrustedDeviceLoginRequests`
|
||||||
|
- `ApproveTrustedDeviceLogin`
|
||||||
|
- `RejectTrustedDeviceLogin`
|
||||||
|
- `CancelTrustedDeviceLogin`
|
||||||
|
- `GetTrustedDeviceLoginStatus`
|
||||||
|
|
||||||
|
В этом потоке:
|
||||||
|
|
||||||
|
- новое устройство не владеет `clientKey` и не проходит обычный `CreateAuthSession`;
|
||||||
|
- пароль проверяется сервером только как фильтр;
|
||||||
|
- решение об одобрении принимает уже авторизованная доверенная сессия пользователя;
|
||||||
|
- сервер не расшифровывает `encryptedPayload` и не становится источником приватных ключей.
|
||||||
|
|
||||||
|
Точные форматы этих операций см. в `03_Session_Management_API.md` и в протокольном документе:
|
||||||
|
|
||||||
|
- `Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Пример ошибки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SessionLogin",
|
||||||
|
"requestId": "slogin-001",
|
||||||
|
"status": 403,
|
||||||
|
"ok": false,
|
||||||
|
"error": "SESSION_KEY_NOT_ACTUAL",
|
||||||
|
"message": "session_key не соответствует актуальной версии",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
485
Dev_Docs/API/03_Session_Management_API.md
Normal file
485
Dev_Docs/API/03_Session_Management_API.md
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
# API для разработчиков: Управление сессиями
|
||||||
|
|
||||||
|
Этот файл описывает методы, которые используются уже после успешной авторизации пользователя в сессию.
|
||||||
|
|
||||||
|
Здесь два метода:
|
||||||
|
|
||||||
|
- `ListSessions` — получить список активных сессий пользователя;
|
||||||
|
- `CloseActiveSession` — закрыть одну из активных сессий.
|
||||||
|
|
||||||
|
Дополнительно в этом же слое управления сессиями появился сценарий pairing через доверенную уже авторизованную сессию пользователя:
|
||||||
|
|
||||||
|
- `GetTrustedDeviceLoginSettings`
|
||||||
|
- `UpsertTrustedDeviceLoginSettings`
|
||||||
|
- `ListTrustedDeviceLoginRequests`
|
||||||
|
- `ApproveTrustedDeviceLogin`
|
||||||
|
- `RejectTrustedDeviceLogin`
|
||||||
|
- `CancelTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Анонимное новое устройство работает с двумя связанными операциями:
|
||||||
|
|
||||||
|
- `StartTrustedDeviceLogin`
|
||||||
|
- `GetTrustedDeviceLoginStatus`
|
||||||
|
|
||||||
|
Логика раздела такая:
|
||||||
|
|
||||||
|
- сначала пользователь проходит `SessionLogin`;
|
||||||
|
- после этого сервер считает соединение авторизованным;
|
||||||
|
- уже в этом состоянии клиент может читать список сессий и управлять ими.
|
||||||
|
|
||||||
|
То есть это не этап создания или входа в сессию, а этап последующего контроля уже существующих активных сессий.
|
||||||
|
|
||||||
|
## 1. `ListSessions`
|
||||||
|
|
||||||
|
Доступно только после успешного `SessionLogin`.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListSessions",
|
||||||
|
"requestId": "list-001",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListSessions",
|
||||||
|
"requestId": "list-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"sessions": [
|
||||||
|
{
|
||||||
|
"sessionId": "sess_7c5e5c4b",
|
||||||
|
"sessionType": 1,
|
||||||
|
"clientPlatform": "Web",
|
||||||
|
"onlineOnThisServer": true,
|
||||||
|
"clientInfoFromClient": "Android 15; Pixel 9",
|
||||||
|
"clientInfoFromRequest": "UA=Java-http-client/17.0.18; remote=127.0.0.1",
|
||||||
|
"geo": "RU/Moscow",
|
||||||
|
"lastAuthenticatedAtMs": 1774600010500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `ListSessions`
|
||||||
|
|
||||||
|
- `422 / NOT_AUTHENTICATED` — запрос доступен только после успешного `SessionLogin`.
|
||||||
|
- `501 / DB_ERROR_LIST_SESSIONS` — ошибка БД при чтении списка активных сессий.
|
||||||
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
### Поля одной сессии в `ListSessions`
|
||||||
|
|
||||||
|
- `sessionId` — идентификатор активной сессии;
|
||||||
|
- `sessionType` — числовой код типа сессии:
|
||||||
|
- `1` — клиент;
|
||||||
|
- `50` — кошелёк;
|
||||||
|
- `100` — homeserver;
|
||||||
|
- `clientPlatform` — строка платформы, как её прислал клиент;
|
||||||
|
- `onlineOnThisServer` — `true`, если эта сессия сейчас держит живое WebSocket-подключение именно к данному серверу;
|
||||||
|
- `clientInfoFromClient` — краткая строка клиента;
|
||||||
|
- `clientInfoFromRequest` — строка, собранная сервером из запроса;
|
||||||
|
- `geo` — страна/город или fallback-строка;
|
||||||
|
- `lastAuthenticatedAtMs` — время последней успешной авторизации этой сессии.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `CloseActiveSession`
|
||||||
|
|
||||||
|
Доступно только после успешного `SessionLogin`.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CloseActiveSession",
|
||||||
|
"requestId": "close-001",
|
||||||
|
"payload": {
|
||||||
|
"sessionId": "sess_7c5e5c4b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CloseActiveSession",
|
||||||
|
"requestId": "close-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `CloseActiveSession`
|
||||||
|
|
||||||
|
- `422 / NOT_AUTHENTICATED` — запрос доступен только после успешного `SessionLogin`.
|
||||||
|
- `400 / NO_SESSION_TO_CLOSE` — сервер не смог определить, какую сессию нужно закрыть.
|
||||||
|
- `501 / DB_ERROR` — ошибка БД при поиске сессии или её удалении.
|
||||||
|
- `422 / SESSION_NOT_FOUND` — целевая сессия не найдена.
|
||||||
|
- `422 / SESSION_OF_ANOTHER_USER` — нельзя закрывать сессию другого пользователя.
|
||||||
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Пример ошибки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CloseActiveSession",
|
||||||
|
"requestId": "close-001",
|
||||||
|
"status": 403,
|
||||||
|
"ok": false,
|
||||||
|
"error": "NOT_AUTHENTICATED",
|
||||||
|
"message": "Операция доступна только для авторизованных пользователей",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 4. Формат `sessionId`
|
||||||
|
|
||||||
|
Текущее серверное значение `sessionId` генерируется как:
|
||||||
|
|
||||||
|
- случайные **32 байта** (`SecureRandom`),
|
||||||
|
- кодирование в **стандартный Base64 RFC 4648** (алфавит `A-Z a-z 0-9 + /`),
|
||||||
|
- **без padding** `=`.
|
||||||
|
|
||||||
|
Практически это строка длиной около **43 символов** (для 32 байт без `=`).
|
||||||
|
|
||||||
|
Пример реального формата:
|
||||||
|
|
||||||
|
```
|
||||||
|
K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M
|
||||||
|
```
|
||||||
|
|
||||||
|
Важно: это **не человеко-читаемое имя**, а непрозрачный идентификатор.
|
||||||
|
Нужно передавать его как есть, без нормализации регистра и без URL-экранирования внутри JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. TrustedDeviceLogin через доверенную сессию
|
||||||
|
|
||||||
|
Этот блок относится к сценарию добавления новой сессии через доверенное устройство пользователя.
|
||||||
|
|
||||||
|
### 5.1. `GetTrustedDeviceLoginSettings`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetTrustedDeviceLoginSettings",
|
||||||
|
"requestId": "trusted-login-get-001",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetTrustedDeviceLoginSettings",
|
||||||
|
"requestId": "trusted-login-get-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"enabled": true,
|
||||||
|
"hasPassword": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если отдельной записи настроек на сервере ещё нет, сервер считает состояние по умолчанию таким:
|
||||||
|
|
||||||
|
- `enabled = true`
|
||||||
|
- `hasPassword = false`
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### 5.2. `UpsertTrustedDeviceLoginSettings`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "UpsertTrustedDeviceLoginSettings",
|
||||||
|
"requestId": "esp-set-001",
|
||||||
|
"payload": {
|
||||||
|
"enabled": true,
|
||||||
|
"passwordHash": "sha256$0123abcd..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если вход через доверенное устройство должен работать **без доп. пароля**, клиент включает его с пустым `passwordHash`.
|
||||||
|
|
||||||
|
Если `enabled = false`, сервер автоматически удаляет пароль и запрещает вход через другое устройство.
|
||||||
|
|
||||||
|
Формат непустого `passwordHash`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sha256$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "UpsertTrustedDeviceLoginSettings",
|
||||||
|
"requestId": "esp-set-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"enabled": true,
|
||||||
|
"hasPassword": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### 5.3. `StartTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Эта операция доступна без уже существующей пользовательской сессии.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "StartTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-start-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice",
|
||||||
|
"passwordHash": "sha256$0123abcd...",
|
||||||
|
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||||||
|
"requesterSessionType": 1,
|
||||||
|
"requesterClientPlatform": "Android",
|
||||||
|
"payloadType": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если на доверённом устройстве вход включён **без доп. пароля**, новое устройство может отправить пустой `passwordHash`.
|
||||||
|
|
||||||
|
Поле `trustedSessionOnline` показывает, что у пользователя сейчас есть хотя бы одна онлайн доверенная сессия, способная принять pairing-заявку.
|
||||||
|
Поле `shortCode` теперь содержит `10` цифр. В UI его рекомендуется показывать как `5` пар, например: `49 20 70 91 23`.
|
||||||
|
|
||||||
|
TTL заявки фиксирован на сервере и сейчас всегда равен `300` секундам.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "StartTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-start-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "created",
|
||||||
|
"shortCode": "4920709123",
|
||||||
|
"fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA",
|
||||||
|
"expiresAtMs": 1781441990538,
|
||||||
|
"trustedSessionOnline": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / EMPTY_LOGIN`
|
||||||
|
- `400 / EMPTY_REQUESTER_SESSION_KEY`
|
||||||
|
- `400 / BAD_REQUESTER_SESSION_KEY`
|
||||||
|
- `400 / BAD_SESSION_TYPE`
|
||||||
|
- `400 / BAD_PAYLOAD_TYPE`
|
||||||
|
- `422 / PAIRING_NOT_AVAILABLE`
|
||||||
|
- `422 / PAIRING_PASSWORD_INVALID` — pairing-пароль не подходит. Та же ошибка возвращается и если новое устройство ввело пароль, а у пользователя режим pairing включён без пароля.
|
||||||
|
- `422 / PAIRING_NO_TRUSTED_SESSION_ONLINE` — сейчас нет ни одной онлайн доверённой сессии пользователя, поэтому код не создаётся.
|
||||||
|
- `429 / PAIRING_RATE_LIMITED`
|
||||||
|
|
||||||
|
### 5.4. `ListTrustedDeviceLoginRequests`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||||||
|
Возвращает только реально активные pending-заявки со `state = created`. Уже `approved` и `rejected` заявки в этот список больше не попадают.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListTrustedDeviceLoginRequests",
|
||||||
|
"requestId": "esp-list-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "created",
|
||||||
|
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||||||
|
"requesterSessionType": 1,
|
||||||
|
"requesterClientPlatform": "Android",
|
||||||
|
"payloadType": 1,
|
||||||
|
"shortCode": "4920709123",
|
||||||
|
"fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA",
|
||||||
|
"createdAtMs": 1781441810538,
|
||||||
|
"expiresAtMs": 1781441990538,
|
||||||
|
"deliveredToHomeserver": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `463 / PAIRING_REQUIRES_AUTH_SESSION`
|
||||||
|
|
||||||
|
### 5.5. `ApproveTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ApproveTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-approve-001",
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"encryptedPayload": "BASE64_OR_OTHER_OPAQUE_PAYLOAD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ApproveTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-approve-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "approved"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / EMPTY_PAIRING_ID`
|
||||||
|
- `400 / EMPTY_ENCRYPTED_PAYLOAD`
|
||||||
|
- `404 / PAIRING_NOT_FOUND`
|
||||||
|
- `422 / PAIRING_OF_ANOTHER_USER`
|
||||||
|
- `422 / PAIRING_NOT_PENDING`
|
||||||
|
- `422 / PAIRING_EXPIRED`
|
||||||
|
- `463 / PAIRING_REQUIRES_AUTH_SESSION`
|
||||||
|
|
||||||
|
### 5.6. `RejectTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя. Похоже на approve, но переводит заявку в `state=rejected`.
|
||||||
|
|
||||||
|
### 5.7. `GetTrustedDeviceLoginStatus`
|
||||||
|
|
||||||
|
Операция для нового устройства.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetTrustedDeviceLoginStatus",
|
||||||
|
"requestId": "esp-status-001",
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ после approve
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetTrustedDeviceLoginStatus",
|
||||||
|
"requestId": "esp-status-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "approved",
|
||||||
|
"shortCode": "4920709123",
|
||||||
|
"fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA",
|
||||||
|
"payloadType": 1,
|
||||||
|
"encryptedPayload": "AQIDBA==",
|
||||||
|
"expiresAtMs": 1781441990538
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Возможные `state`
|
||||||
|
|
||||||
|
- `created`
|
||||||
|
- `approved`
|
||||||
|
- `rejected`
|
||||||
|
- `canceled`
|
||||||
|
- `expired`
|
||||||
|
|
||||||
|
### 5.8. `CancelTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Операция для нового устройства, которое уже создало pairing-заявку и хочет принудительно снять ожидание до истечения TTL.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CancelTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-cancel-001",
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CancelTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-cancel-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "canceled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / EMPTY_PAIRING_ID`
|
||||||
|
- `400 / EMPTY_REQUESTER_SESSION_KEY`
|
||||||
|
- `400 / BAD_REQUESTER_SESSION_KEY`
|
||||||
|
- `404 / PAIRING_NOT_FOUND`
|
||||||
|
- `422 / PAIRING_OF_ANOTHER_REQUESTER`
|
||||||
|
- `422 / PAIRING_NOT_PENDING`
|
||||||
239
Dev_Docs/API/04_Add_Block_to_Blockchain_API.md
Normal file
239
Dev_Docs/API/04_Add_Block_to_Blockchain_API.md
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
# API для разработчиков: 04 — Запись и чтение блока блокчейна
|
||||||
|
|
||||||
|
Документ описывает **текущий рабочий формат** сетевых вызовов:
|
||||||
|
|
||||||
|
- `AddBlock` — запись любого блока в блокчейн пользователя;
|
||||||
|
- `GetBlockchainBlock` — публичное чтение одного конкретного блока по имени цепочки и номеру.
|
||||||
|
|
||||||
|
`GetBlockchainBlock` нужен в том числе для межсерверной синхронизации и для открытого чтения публичного блокчейна по одному блоку.
|
||||||
|
|
||||||
|
> Важный принцип: на уровне JSON API сейчас есть **один универсальный метод** записи — `AddBlock`.
|
||||||
|
> Конкретный смысл записи задаётся типом самого бинарного блока (`type/subType/version` в заголовке блока).
|
||||||
|
|
||||||
|
## 1. Что делает `AddBlock`
|
||||||
|
|
||||||
|
`AddBlock`:
|
||||||
|
- принимает имя блокчейна и base64 бинарного блока;
|
||||||
|
- проверяет непрерывность цепочки (`blockNumber`, `prevHash`);
|
||||||
|
- проверяет формат и подпись Ed25519;
|
||||||
|
- валидирует `body` по правилам типа блока;
|
||||||
|
- сохраняет блок и обновляет состояние цепочки.
|
||||||
|
|
||||||
|
## 2. JSON формат запроса
|
||||||
|
|
||||||
|
`op = "AddBlock"`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AddBlock",
|
||||||
|
"requestId": "req-1001",
|
||||||
|
"payload": {
|
||||||
|
"blockchainName": "alice-001",
|
||||||
|
"blockNumber": 12,
|
||||||
|
"prevBlockHash": "ab12...ff",
|
||||||
|
"blockBytesB64": "AAAB..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Поля `payload`:
|
||||||
|
- `blockchainName` — обязательно, формат `login-NNN`.
|
||||||
|
- `blockNumber` — обязательно (временное legacy-поле для совместимости; должно совпасть с номером внутри бинарного блока).
|
||||||
|
- `prevBlockHash` — legacy-поле, сейчас сервер использует `prevHash` из бинарного блока и состояние цепочки.
|
||||||
|
- `blockBytesB64` — обязательно: **полный бинарный блок** (`preimage + sigMarker + signature`) в Base64.
|
||||||
|
|
||||||
|
## 3. Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AddBlock",
|
||||||
|
"requestId": "req-1001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"reasonCode": null,
|
||||||
|
"serverLastGlobalNumber": 12,
|
||||||
|
"serverLastGlobalHash": "9f0e...a1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Ошибка (единый формат)
|
||||||
|
|
||||||
|
При ошибках сервер отдаёт `Net_Exception_Response` со стандартными полями и дополнительно с состоянием сервера для ресинка:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AddBlock",
|
||||||
|
"requestId": "req-1001",
|
||||||
|
"status": 400,
|
||||||
|
"ok": false,
|
||||||
|
"error": "bad_prev_hash",
|
||||||
|
"message": "Некорректный prevHash (цепочка не совпадает)",
|
||||||
|
"payload": {
|
||||||
|
"serverLastGlobalNumber": 11,
|
||||||
|
"serverLastGlobalHash": "c3d4...98"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Основные `reasonCode`
|
||||||
|
|
||||||
|
- `empty_blockchain_name`, `bad_blockchain_name`
|
||||||
|
- `blockchain_state_not_found`
|
||||||
|
- `bad_block_base64`, `bad_block_format`, `bad_block_body`
|
||||||
|
- `bad_block_number`, `req_global_mismatch`, `bad_prev_hash`
|
||||||
|
- `bad_signature`, `signature_verify_failed`
|
||||||
|
- `prev_line_block_not_found`, `bad_prev_line_hash`
|
||||||
|
- `limit_exceeded`
|
||||||
|
- `chain_resync_in_progress` — цепочка временно заблокирована полным resync
|
||||||
|
- `repost_disabled` — репосты временно отключены до будущей реализации
|
||||||
|
- `internal_error`
|
||||||
|
|
||||||
|
## 5. Какие блоки реально можно добавлять через `AddBlock`
|
||||||
|
|
||||||
|
Через `AddBlock` можно писать поддержанные форматы, кроме явно отключённых временных фич:
|
||||||
|
|
||||||
|
1. **TECH (type=0)**
|
||||||
|
- `HEADER_COMPAT (subType=0)`
|
||||||
|
- `TECH_CREATE_CHANNEL (subType=1)`
|
||||||
|
|
||||||
|
2. **TEXT (type=1)**
|
||||||
|
- `TEXT_POST (10)`
|
||||||
|
- `TEXT_EDIT_POST (11)`
|
||||||
|
- `TEXT_REPLY (20)`
|
||||||
|
- `TEXT_EDIT_REPLY (21)`
|
||||||
|
- `TEXT_REPOST (30)` — формат зарезервирован, но новые блоки временно отклоняются с `repost_disabled`
|
||||||
|
|
||||||
|
3. **REACTION (type=2)**
|
||||||
|
- `REACTION_LIKE (1)`
|
||||||
|
|
||||||
|
4. **CONNECTION (type=3)**
|
||||||
|
- `CONNECTION_FRIEND (10)`
|
||||||
|
- `CONNECTION_UNFRIEND (11)`
|
||||||
|
- `CONNECTION_CONTACT (20)`
|
||||||
|
- `CONNECTION_UNCONTACT (21)`
|
||||||
|
- `CONNECTION_FOLLOW (30)`
|
||||||
|
- `CONNECTION_UNFOLLOW (31)`
|
||||||
|
- `CONNECTION_SPOUSE (40)`
|
||||||
|
- `CONNECTION_UNSPOUSE (41)`
|
||||||
|
- `CONNECTION_PARENT (50)`
|
||||||
|
- `CONNECTION_UNPARENT (51)`
|
||||||
|
- `CONNECTION_CHILD (52)`
|
||||||
|
- `CONNECTION_UNCHILD (53)`
|
||||||
|
- `CONNECTION_SIBLING (54)`
|
||||||
|
- `CONNECTION_UNSIBLING (55)`
|
||||||
|
- `CONNECTION_KNOWN_PERSON (60)`
|
||||||
|
- `CONNECTION_UNKNOWN_PERSON (61)`
|
||||||
|
- `CONNECTION_SHINE_CONFIRMED (70)`
|
||||||
|
- `CONNECTION_SHINE_UNCONFIRMED (71)`
|
||||||
|
- `CONNECTION_SHINE_SEEN (74)`
|
||||||
|
- `CONNECTION_SHINE_UNSEEN (75)`
|
||||||
|
|
||||||
|
5. **USER_PARAM (type=4)**
|
||||||
|
- `USER_PARAM_TEXT_TEXT (1)`
|
||||||
|
|
||||||
|
## 6. Хватает ли функций сейчас
|
||||||
|
|
||||||
|
Коротко: **для записи событий в блокчейн — хватает**, для полноценного клиентского чтения — **пока не хватает**.
|
||||||
|
|
||||||
|
Что есть:
|
||||||
|
- единый надёжный write-путь `AddBlock`;
|
||||||
|
- есть `GetFriendsLists` и API по `UserParam`;
|
||||||
|
- есть унифицированные коды ошибок и поля для ресинхронизации.
|
||||||
|
|
||||||
|
Что пока ограничивает продукт:
|
||||||
|
- нет полноценного read API для каналов/постов/тредов;
|
||||||
|
- нет API списка подписок с серверными счётчиками непрочитанного;
|
||||||
|
- нет ленты событий (новые ответы/лайки/подписки) как отдельного RPC.
|
||||||
|
|
||||||
|
## 7. Рекомендации по клиенту при записи блоков
|
||||||
|
|
||||||
|
1. Перед отправкой держать локальный `lastNumber/lastHash`.
|
||||||
|
2. При `bad_prev_hash` или `bad_block_number`:
|
||||||
|
- взять `serverLastGlobalNumber/serverLastGlobalHash` из ошибки,
|
||||||
|
- пересобрать следующий блок на актуальной вершине.
|
||||||
|
3. Для edit-блоков всегда ссылаться на **оригинальный** блок, а не на предыдущий edit.
|
||||||
|
4. Для связей/подписок использовать target на **root** (HEADER или CREATE_CHANNEL), а не на произвольный пост.
|
||||||
|
|
||||||
|
|
||||||
|
## 8. USER_PARAM для «личных данных»
|
||||||
|
|
||||||
|
Да, на текущем API это можно добавить **без изменения серверного кода**:
|
||||||
|
|
||||||
|
- в `UserParam` поле `param` сейчас не ограничено фиксированным справочником;
|
||||||
|
- сервер хранит пары `param -> value` как строки (при наличии корректной подписи и `time_ms`);
|
||||||
|
- чтение уже есть через `GetUserParam` и `ListUserParams`.
|
||||||
|
|
||||||
|
Рекомендуемый стартовый набор ключей для профиля (MVP):
|
||||||
|
|
||||||
|
- `name`
|
||||||
|
- `last_name`
|
||||||
|
- `address_physical`
|
||||||
|
- `address_web`
|
||||||
|
- `phone`
|
||||||
|
|
||||||
|
Практическая рекомендация: заранее зафиксировать единый словарь ключей в клиенте/документации, чтобы избежать дублей вида `lastname` vs `last_name`, `site` vs `address_web` и т.д.
|
||||||
|
|
||||||
|
Ограничения, которые важно учесть:
|
||||||
|
|
||||||
|
- сейчас нет серверной ACL-политики чтения параметров (в MVP их может читать любой клиент, который знает `login`);
|
||||||
|
- нет валидации формата значений для конкретных ключей (телефон, URL и т.д. проверяются только на стороне клиента);
|
||||||
|
- нет отдельного индекса/поиска по этим полям — только точечное чтение и listing по `login`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. `GetBlockchainBlock`
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Публичное чтение одного конкретного блока из цепочки.
|
||||||
|
|
||||||
|
Нужно для:
|
||||||
|
|
||||||
|
- открытого чтения блокчейна по одному блоку;
|
||||||
|
- межсерверной синхронизации;
|
||||||
|
- восстановления/докачки отсутствующего хвоста цепочки.
|
||||||
|
|
||||||
|
### JSON формат запроса
|
||||||
|
|
||||||
|
`op = "GetBlockchainBlock"`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetBlockchainBlock",
|
||||||
|
"requestId": "req-2001",
|
||||||
|
"payload": {
|
||||||
|
"blockchainName": "alice-001",
|
||||||
|
"blockNumber": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Поля `payload`:
|
||||||
|
|
||||||
|
- `blockchainName` — обязательно, формат `login-NNN`.
|
||||||
|
- `blockNumber` — обязательно, номер блока в цепочке, `>= 0`.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetBlockchainBlock",
|
||||||
|
"requestId": "req-2001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"blockchainName": "alice-001",
|
||||||
|
"blockNumber": 12,
|
||||||
|
"blockHash": "9f0eaabbccddeeff00112233445566778899aabbccddeeff0011223344556677",
|
||||||
|
"blockBytesB64": "AAAB..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / BAD_FIELDS` — некорректные `blockchainName` или `blockNumber`.
|
||||||
|
- `404 / BLOCK_NOT_FOUND` — такого блока нет.
|
||||||
|
- `500 / INTERNAL_ERROR` — внутренняя ошибка сервера.
|
||||||
615
Dev_Docs/API/05_Technical_Requests_API.md
Normal file
615
Dev_Docs/API/05_Technical_Requests_API.md
Normal file
@ -0,0 +1,615 @@
|
|||||||
|
# API для разработчиков: Технические запросы
|
||||||
|
|
||||||
|
Этот файл описывает технические WebSocket-запросы, которые нужны для служебной работы клиента с сервером. Часть операций доступна без авторизации, часть требует успешной авторизованной сессии.
|
||||||
|
|
||||||
|
Сейчас здесь девять методов:
|
||||||
|
|
||||||
|
- `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения;
|
||||||
|
- `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети;
|
||||||
|
- `ListBlockchainHeads` — краткая сводка по всем локальным блокчейнам сервера для межсерверной синхронизации;
|
||||||
|
- `GetSyncUserProfile` — межсерверный профиль пользователя для создания локальной цепочки без Solana RPC;
|
||||||
|
- `SendSignal` — общий межсессионный технический сигнал в одну конкретную сессию или сразу во все активные сессии пользователя;
|
||||||
|
- `GetCallIceConfig` — выдача STUN/TURN конфигурации для звонков;
|
||||||
|
- `ClientErrorLog` — отправка клиентской ошибки в серверный лог;
|
||||||
|
- `ClientDebugLog` — отправка клиентского debug-события в серверный буфер;
|
||||||
|
- `CallDeliveryReport` — диагностический отчёт клиента о доставке/установке звонка.
|
||||||
|
|
||||||
|
Логика раздела такая:
|
||||||
|
|
||||||
|
- `Ping` нужен для регулярной проверки, что соединение всё ещё живо;
|
||||||
|
- `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла.
|
||||||
|
- `ListBlockchainHeads` нужен для сервер-сервер сверки: партнёр получает список heads по всем цепочкам, сравнивает его со своим состоянием и затем добирает недостающие блоки по диапазону.
|
||||||
|
- `GetSyncUserProfile` нужен для server-to-server режима, когда принимающий сервер хочет создать у себя локальные `solana_users + blockchain_state` без прямого обращения в Solana. Это используется как временный обход ограничений внешнего Solana RPC.
|
||||||
|
- `SendSignal` нужен для доверенных межсессионных команд одного пользователя. Первое практическое применение — `remote AddBlock via homeserver session`, но формат задуман как общий transport на вырост.
|
||||||
|
|
||||||
|
Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов.
|
||||||
|
|
||||||
|
## 1. `Ping`
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Служебный keep-alive запрос.
|
||||||
|
|
||||||
|
Клиент может отправлять его периодически, чтобы:
|
||||||
|
|
||||||
|
- поддерживать активное WebSocket-соединение;
|
||||||
|
- понимать, что сервер отвечает;
|
||||||
|
- при необходимости получать текущее серверное время.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "Ping",
|
||||||
|
"requestId": "ping-001",
|
||||||
|
"payload": {
|
||||||
|
"ts": 1774700000123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Поле `ts` в запросе необязательно для логики сервера. Сервер его не валидирует и не использует для принятия решения.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "Ping",
|
||||||
|
"requestId": "ping-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"ts": 1774700000456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `Ping`
|
||||||
|
|
||||||
|
- У `Ping` нет специальных прикладных ошибок.
|
||||||
|
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `GetServerInfo`
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Запрос публичной информации о сервере.
|
||||||
|
|
||||||
|
Он нужен клиенту для выбора сервера в децентрализованной сети. По этому запросу клиент может:
|
||||||
|
|
||||||
|
- проверить, что сервер вообще доступен;
|
||||||
|
- показать URL и версию сервера;
|
||||||
|
- показать физический регион или адрес размещения;
|
||||||
|
- показать описание сервера;
|
||||||
|
- показать поле `origin` как комментарий о природе этого узла;
|
||||||
|
- показать дополнительную текстовую информацию.
|
||||||
|
|
||||||
|
Этот запрос доступен без авторизации.
|
||||||
|
|
||||||
|
### Источник данных
|
||||||
|
|
||||||
|
- `version` берётся из Gradle build и подставляется в `application.properties`;
|
||||||
|
- остальные поля читаются из настроек сервера;
|
||||||
|
- если значение в конфиге не задано, сервер возвращает пустую строку.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetServerInfo",
|
||||||
|
"requestId": "srv-001",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetServerInfo",
|
||||||
|
"requestId": "srv-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"url": "wss://node.example.org/ws",
|
||||||
|
"version": "1.0",
|
||||||
|
"physicalRegion": "Грузия, Тбилиси",
|
||||||
|
"description": "Public community SHiNE node",
|
||||||
|
"origin": "Community-operated node",
|
||||||
|
"extraInfo": "IPv4 + IPv6; test federation enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поля ответа
|
||||||
|
|
||||||
|
- `url` — публичный URL сервера.
|
||||||
|
- `version` — версия сервера из Gradle build.
|
||||||
|
- `physicalRegion` — физический регион или адрес размещения сервера.
|
||||||
|
- `description` — человекочитаемое описание сервера.
|
||||||
|
- `origin` — комментарий о том, какой это сервер.
|
||||||
|
- `extraInfo` — любая дополнительная информация о сервере.
|
||||||
|
|
||||||
|
### Специфические коды ошибок `GetServerInfo`
|
||||||
|
|
||||||
|
- У `GetServerInfo` нет специальных прикладных ошибок при штатной работе.
|
||||||
|
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `ListBlockchainHeads`
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Запрос краткой сводки по всем локальным блокчейнам сервера.
|
||||||
|
|
||||||
|
Нужен для межсерверной синхронизации. Партнёр может:
|
||||||
|
|
||||||
|
- получить список всех блокчейнов;
|
||||||
|
- сравнить `lastBlockNumber` и `lastBlockHash` со своими значениями;
|
||||||
|
- понять, какие цепочки нужно догонять;
|
||||||
|
- затем отдельно запросить недостающие блоки по диапазону.
|
||||||
|
|
||||||
|
Этот запрос доступен без авторизации.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListBlockchainHeads",
|
||||||
|
"requestId": "heads-001",
|
||||||
|
"payload": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListBlockchainHeads",
|
||||||
|
"requestId": "heads-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"blockchains": [
|
||||||
|
{
|
||||||
|
"blockchainName": "alice_main",
|
||||||
|
"lastBlockNumber": 124,
|
||||||
|
"lastBlockHash": "aabbccdd00112233445566778899aabbccddeeff00112233445566778899aabb",
|
||||||
|
"fileSizeBytes": 58720
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поля ответа
|
||||||
|
|
||||||
|
- `blockchains` — массив текущих heads всех цепочек сервера.
|
||||||
|
- `blockchainName` — имя блокчейна.
|
||||||
|
- `lastBlockNumber` — последний номер блока в этой цепочке.
|
||||||
|
- `lastBlockHash` — последний хэш блока в HEX-формате `64` символа.
|
||||||
|
- `fileSizeBytes` — текущий размер файла блокчейна в байтах.
|
||||||
|
|
||||||
|
### Специфические коды ошибок `ListBlockchainHeads`
|
||||||
|
|
||||||
|
- У `ListBlockchainHeads` нет специальных прикладных ошибок при штатной работе.
|
||||||
|
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `GetSyncUserProfile`
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Запрос минимального профиля пользователя для межсерверной синхронизации.
|
||||||
|
|
||||||
|
Нужен в сценарии, когда сервер во время periodic sync увидел чужой блокчейн, которого у него локально ещё нет. Вместо обращения в Solana PDA он может запросить у партнёра:
|
||||||
|
|
||||||
|
- `login`
|
||||||
|
- `blockchainName`
|
||||||
|
- `solanaKey`
|
||||||
|
- `blockchainKey`
|
||||||
|
- `clientKey`
|
||||||
|
- `blockchainSizeLimitBytes`
|
||||||
|
|
||||||
|
После этого принимающий сервер может локально создать записи в `solana_users` и `blockchain_state`, а затем уже докачивать блоки через `GetBlockchainBlock`.
|
||||||
|
|
||||||
|
Этот запрос доступен без авторизации и предназначен именно для server-to-server sync.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetSyncUserProfile",
|
||||||
|
"requestId": "sync-user-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ: пользователь не найден
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetSyncUserProfile",
|
||||||
|
"requestId": "sync-user-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"exists": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ: пользователь найден
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetSyncUserProfile",
|
||||||
|
"requestId": "sync-user-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"exists": true,
|
||||||
|
"login": "alice",
|
||||||
|
"blockchainName": "alice-001",
|
||||||
|
"solanaKey": "BASE64_32",
|
||||||
|
"blockchainKey": "BASE64_32",
|
||||||
|
"clientKey": "BASE64_32",
|
||||||
|
"blockchainSizeLimitBytes": 100000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поля ответа
|
||||||
|
|
||||||
|
- `exists` — найден ли пользователь на сервере-партнёре.
|
||||||
|
- `login` — канонический login из БД сервера-партнёра.
|
||||||
|
- `blockchainName` — имя основной цепочки пользователя.
|
||||||
|
- `solanaKey` — публичный ключ логина.
|
||||||
|
- `blockchainKey` — публичный ключ блокчейна.
|
||||||
|
- `clientKey` — публичный клиентский ключ, который в текущей модели используется при создании локальной записи.
|
||||||
|
- `blockchainSizeLimitBytes` — лимит размера файла блокчейна, который будет записан в локальный `blockchain_state`.
|
||||||
|
|
||||||
|
### Специфические коды ошибок `GetSyncUserProfile`
|
||||||
|
|
||||||
|
- `400 / BAD_FIELDS` — пустой или некорректный `login`.
|
||||||
|
- `404 / BLOCKCHAIN_STATE_NOT_FOUND` — пользователь найден, но на сервере-партнёре отсутствует `blockchain_state` для его цепочки.
|
||||||
|
- При непредвиденной ошибке сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. `SendSignal`
|
||||||
|
|
||||||
|
Доступно только после успешной авторизации.
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Общий межсессионный технический сигнал.
|
||||||
|
|
||||||
|
Этот метод нужен для случаев, когда одна активная сессия пользователя должна быстро передать служебную команду другой сессии того же пользователя или сразу всем его активным сессиям.
|
||||||
|
|
||||||
|
Первый целевой сценарий:
|
||||||
|
|
||||||
|
- `remote AddBlock via homeserver session`
|
||||||
|
|
||||||
|
То есть телефон без локального `blockchain.key` может:
|
||||||
|
|
||||||
|
- подготовить только сырой payload операции без текущей вершины цепочки;
|
||||||
|
- подписать сам `SendSignal` своим `session key`;
|
||||||
|
- дополнительно подписать его `client key`, чтобы homeserver/ESP32 точно видел, что запрос пришёл от доверенного клиента этого же логина;
|
||||||
|
- отправить запрос в выбранную `homeserver`-сессию;
|
||||||
|
- получить от неё ответ после настоящего `AddBlock`, который homeserver соберёт и подпишет уже сама.
|
||||||
|
|
||||||
|
### Режимы доставки
|
||||||
|
|
||||||
|
- `targetMode = "single_session"` — доставка в одну конкретную `targetSessionId`.
|
||||||
|
- `targetMode = "all_sessions"` — доставка во все активные сессии указанного логина.
|
||||||
|
|
||||||
|
### Важное правило подписи
|
||||||
|
|
||||||
|
Сам `SendSignal` не подписывает поле `data` отдельной вложенной подписью. Вместо этого сервер проверяет подписи по общему preimage сигнала, в который входит:
|
||||||
|
|
||||||
|
- `fromLogin`
|
||||||
|
- `fromSessionId`
|
||||||
|
- `toLogin`
|
||||||
|
- `targetMode`
|
||||||
|
- `targetSessionId`
|
||||||
|
- `signalType`
|
||||||
|
- `signalRequestId`
|
||||||
|
- `timeMs`
|
||||||
|
- `sha256(data)`
|
||||||
|
|
||||||
|
Поддерживаются две подписи:
|
||||||
|
|
||||||
|
- `sessionSignatureB64` — обязательная подпись текущей авторизованной `session key`;
|
||||||
|
- `clientSignatureB64` — необязательная подпись `client key`.
|
||||||
|
|
||||||
|
Для сценария `remote AddBlock via homeserver` текущая договорённость такая:
|
||||||
|
|
||||||
|
- запрос должен идти только своему же логину;
|
||||||
|
- запрос должен быть подписан и `session key`, и `client key`;
|
||||||
|
- в будущем для отдельных wallet-сценариев `clientSignatureB64` может быть пустой.
|
||||||
|
|
||||||
|
### Запрос в одну сессию
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SendSignal",
|
||||||
|
"requestId": "ws-req-001",
|
||||||
|
"payload": {
|
||||||
|
"toLogin": "alice",
|
||||||
|
"targetMode": "single_session",
|
||||||
|
"targetSessionId": "sess-hs-001",
|
||||||
|
"signalType": "remote_addblock_request",
|
||||||
|
"signalRequestId": "remote-addblock-001",
|
||||||
|
"data": "{\"operation\":\"remote_addblock_request\",\"signalRequestId\":\"remote-addblock-001\",\"blockchainName\":\"alice_main\",\"blockBodyB64\":\"...\"}",
|
||||||
|
"timeMs": 1774700000123,
|
||||||
|
"sessionSignatureB64": "BASE64_64",
|
||||||
|
"clientSignatureB64": "BASE64_64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SendSignal",
|
||||||
|
"requestId": "ws-req-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"deliveredCount": 1,
|
||||||
|
"deliveredSessionIds": ["sess-hs-001"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Событие на принимающей стороне
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "IncomingSignal",
|
||||||
|
"eventId": "evt-001",
|
||||||
|
"payload": {
|
||||||
|
"fromLogin": "alice",
|
||||||
|
"fromSessionId": "sess-phone-001",
|
||||||
|
"toLogin": "alice",
|
||||||
|
"targetMode": "single_session",
|
||||||
|
"targetSessionId": "sess-hs-001",
|
||||||
|
"signalType": "remote_addblock_request",
|
||||||
|
"signalRequestId": "remote-addblock-001",
|
||||||
|
"data": "{\"operation\":\"remote_addblock_request\",\"signalRequestId\":\"remote-addblock-001\",\"blockchainName\":\"alice_main\",\"blockBodyB64\":\"...\"}",
|
||||||
|
"timeMs": 1774700000123,
|
||||||
|
"sessionSignatureB64": "BASE64_64",
|
||||||
|
"clientSignatureB64": "BASE64_64",
|
||||||
|
"dataSha256B64": "BASE64_32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфика `remote AddBlock`
|
||||||
|
|
||||||
|
Для `remote_addblock_request` поле `data` теперь содержит:
|
||||||
|
|
||||||
|
- `blockchainName`
|
||||||
|
- `blockBodyB64`
|
||||||
|
|
||||||
|
Где `blockBodyB64` — это не финальный блок и не почти готовый preimage, а компактный бинарный контейнер:
|
||||||
|
|
||||||
|
- `msgType` (`u16`)
|
||||||
|
- `msgSubType` (`u16`)
|
||||||
|
- `msgVersion` (`u16`)
|
||||||
|
- `bodyBytes`
|
||||||
|
|
||||||
|
После этого homeserver сама:
|
||||||
|
|
||||||
|
- вызывает `GetUser(login)` и получает `serverLastGlobalNumber/serverLastGlobalHash`;
|
||||||
|
- вычисляет новый `blockNumber = last + 1`;
|
||||||
|
- подставляет актуальный `prevBlockHash`;
|
||||||
|
- ставит текущее время;
|
||||||
|
- досчитывает полный preimage;
|
||||||
|
- подписывает его своим `blockchain key`;
|
||||||
|
- и только потом делает настоящий `AddBlock`.
|
||||||
|
|
||||||
|
### Специфические коды ошибок `SendSignal`
|
||||||
|
|
||||||
|
- `422 / NOT_AUTHENTICATED` — требуется авторизация.
|
||||||
|
- `400 / BAD_FIELDS` — не хватает обязательных полей или нарушено правило `single_session/all_sessions`.
|
||||||
|
- `400 / BAD_TARGET_MODE` — передан неизвестный `targetMode`.
|
||||||
|
- `400 / TIME_SKEW` — `timeMs` отличается от серверного более чем на 30 секунд.
|
||||||
|
- `500 / NO_CLIENT_KEY` — для текущего пользователя не найден `client key`.
|
||||||
|
- `404 / USER_NOT_FOUND` — логин адресата не найден.
|
||||||
|
- `400 / BAD_DATA` — сервер не смог обработать `data`.
|
||||||
|
- `400 / BAD_SESSION_SIGNATURE` — некорректная подпись `session key`.
|
||||||
|
- `400 / BAD_CLIENT_SIGNATURE` — некорректная подпись `client key`.
|
||||||
|
- `404 / SESSION_NOT_FOUND` — при `single_session` целевая сессия не найдена или не онлайн.
|
||||||
|
- `404 / NO_TARGET_SESSIONS` — при `all_sessions` у пользователя сейчас нет активных онлайн-сессий.
|
||||||
|
- `404 / DELIVERY_FAILED` — сервер не смог отправить событие ни в одну из целевых сессий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. `GetCallIceConfig`
|
||||||
|
|
||||||
|
Доступно только после успешной авторизации.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetCallIceConfig",
|
||||||
|
"requestId": "ice-001",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetCallIceConfig",
|
||||||
|
"requestId": "ice-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"stunUrls": ["stun:stun.example.org:3478"],
|
||||||
|
"turnUrls": ["turn:turn.example.org:3478?transport=udp"],
|
||||||
|
"turnUsername": "user",
|
||||||
|
"turnPassword": "password",
|
||||||
|
"turnServers": [
|
||||||
|
{
|
||||||
|
"id": "primary",
|
||||||
|
"urls": ["turn:turn.example.org:3478?transport=udp"],
|
||||||
|
"username": "user",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"turnEnabled": true,
|
||||||
|
"generatedAtMs": 1774700000123,
|
||||||
|
"expiresAtMs": 1774700300123,
|
||||||
|
"ttlSec": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `GetCallIceConfig`
|
||||||
|
|
||||||
|
- `422 / NOT_AUTHENTICATED` — требуется авторизация.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. `ClientErrorLog`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ClientErrorLog",
|
||||||
|
"requestId": "err-001",
|
||||||
|
"payload": {
|
||||||
|
"kind": "global_error",
|
||||||
|
"message": "TypeError: failed",
|
||||||
|
"stack": "...",
|
||||||
|
"sourceUrl": "https://shineup.me/app.js",
|
||||||
|
"lineNumber": 10,
|
||||||
|
"columnNumber": 20,
|
||||||
|
"route": "#/channel-view/own-0",
|
||||||
|
"href": "https://shineup.me/#/channel-view/own-0",
|
||||||
|
"userAgent": "...",
|
||||||
|
"clientTs": 1774700000123,
|
||||||
|
"requestOp": "GetChannelMessages",
|
||||||
|
"requestIdRef": "GetChannelMessages-123",
|
||||||
|
"contextJson": "{\"screen\":\"channels\"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ClientErrorLog",
|
||||||
|
"requestId": "err-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"serverTs": 1774700000456,
|
||||||
|
"accepted": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `ClientErrorLog`
|
||||||
|
|
||||||
|
- `400 / BAD_FIELDS` — обязательные поля ошибки не заполнены.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. `ClientDebugLog`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ClientDebugLog",
|
||||||
|
"requestId": "dbg-001",
|
||||||
|
"payload": {
|
||||||
|
"runId": "ui-run-1",
|
||||||
|
"level": "info",
|
||||||
|
"message": "opened channels tab",
|
||||||
|
"details": "{\"route\":\"#/channels\"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ClientDebugLog",
|
||||||
|
"requestId": "dbg-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"accepted": true,
|
||||||
|
"serverTs": 1774700000456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `ClientDebugLog`
|
||||||
|
|
||||||
|
- `400 / BAD_FIELDS` — поле `message` не заполнено.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. `CallDeliveryReport`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CallDeliveryReport",
|
||||||
|
"requestId": "call-report-001",
|
||||||
|
"payload": {
|
||||||
|
"type": "outgoing_failed",
|
||||||
|
"value": "{\"reason\":\"ice_failed\",\"callId\":\"call-1\"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CallDeliveryReport",
|
||||||
|
"requestId": "call-report-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"serverTs": 1774700000456,
|
||||||
|
"accepted": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Специфические коды ошибок `CallDeliveryReport`
|
||||||
|
|
||||||
|
- `400 / BAD_FIELDS` — поле `type` не заполнено.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Короткое резюме
|
||||||
|
|
||||||
|
- `Ping` нужен для keep-alive и проверки, что WebSocket-соединение живо.
|
||||||
|
- `GetServerInfo` нужен для выбора сервера в сети и показа публичной информации об узле.
|
||||||
|
- `SendSignal` нужен для доверенных межсессионных сигналов одного пользователя, включая `remote AddBlock via homeserver session`.
|
||||||
|
- `GetCallIceConfig` нужен для WebRTC-звонков и требует авторизации.
|
||||||
|
- `ClientErrorLog`, `ClientDebugLog`, `CallDeliveryReport` используются для диагностики клиента и звонков.
|
||||||
341
Dev_Docs/API/06_Channels_Read_API.md
Normal file
341
Dev_Docs/API/06_Channels_Read_API.md
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
# 06. Channels Read API
|
||||||
|
|
||||||
|
## Человеко-читаемое объяснение
|
||||||
|
Эти функции — это **чтение данных каналов** для UI:
|
||||||
|
|
||||||
|
1. `ListSubscriptionsFeed` — отдает данные для экрана списка каналов:
|
||||||
|
- ваши каналы (личный + созданные вами),
|
||||||
|
- каналы пользователей, на кого вы подписаны,
|
||||||
|
- отдельные каналы, на которые вы подписаны напрямую.
|
||||||
|
|
||||||
|
2. `GetChannelMessages` — отдает полную ленту одного канала (пока без курсоров, загружается сразу целиком),
|
||||||
|
включая версии сообщений, лайки и ответы.
|
||||||
|
|
||||||
|
3. `GetMessageThread` — отдает дерево обсуждения вокруг конкретного сообщения:
|
||||||
|
предки, фокус-сообщение, потомки.
|
||||||
|
|
||||||
|
4. `GetChannelsCounters` — отдает счетчики разделов каналов для пользователя.
|
||||||
|
|
||||||
|
5. `ListGroupChats200` — отдает список групповых чатов типа `200`.
|
||||||
|
|
||||||
|
6. `GetGroupDialog` — отдает сообщения конкретного группового чата типа `200`.
|
||||||
|
|
||||||
|
> На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) ListSubscriptionsFeed
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListSubscriptionsFeed",
|
||||||
|
"requestId": "req-1",
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice",
|
||||||
|
"limit": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (success)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListSubscriptionsFeed",
|
||||||
|
"requestId": "req-1",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice",
|
||||||
|
"ownedChannels": [
|
||||||
|
{
|
||||||
|
"channel": {
|
||||||
|
"ownerLogin": "Alice",
|
||||||
|
"ownerBlockchainName": "alice-001",
|
||||||
|
"channelName": "0",
|
||||||
|
"personal": true,
|
||||||
|
"channelRoot": { "blockNumber": 0, "blockHash": "..." }
|
||||||
|
},
|
||||||
|
"messagesCount": 120,
|
||||||
|
"lastMessage": {
|
||||||
|
"messageRef": { "blockNumber": 921, "blockHash": "..." },
|
||||||
|
"text": "последняя версия текста",
|
||||||
|
"createdAtMs": 1760000000000,
|
||||||
|
"authorLogin": "Alice",
|
||||||
|
"authorBlockchainName": "alice-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"followedUsersChannels": [
|
||||||
|
{
|
||||||
|
"channel": {
|
||||||
|
"ownerLogin": "Bob",
|
||||||
|
"ownerBlockchainName": "bob-001",
|
||||||
|
"channelName": "0",
|
||||||
|
"personal": true,
|
||||||
|
"channelRoot": { "blockNumber": 0, "blockHash": "..." }
|
||||||
|
},
|
||||||
|
"messagesCount": 540,
|
||||||
|
"lastMessage": {
|
||||||
|
"messageRef": { "blockNumber": 922, "blockHash": "..." },
|
||||||
|
"text": "последняя версия текста",
|
||||||
|
"createdAtMs": 1760000100000,
|
||||||
|
"authorLogin": "Bob",
|
||||||
|
"authorBlockchainName": "bob-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"followedChannels": [
|
||||||
|
{
|
||||||
|
"channel": {
|
||||||
|
"ownerLogin": "Carl",
|
||||||
|
"ownerBlockchainName": "carl-001",
|
||||||
|
"channelName": "market",
|
||||||
|
"personal": false,
|
||||||
|
"channelRoot": { "blockNumber": 456, "blockHash": "..." }
|
||||||
|
},
|
||||||
|
"messagesCount": 90,
|
||||||
|
"lastMessage": {
|
||||||
|
"messageRef": { "blockNumber": 1002, "blockHash": "..." },
|
||||||
|
"text": "актуальный текст",
|
||||||
|
"createdAtMs": 1760001000000,
|
||||||
|
"authorLogin": "Carl",
|
||||||
|
"authorBlockchainName": "carl-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) GetChannelMessages
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetChannelMessages",
|
||||||
|
"requestId": "req-2",
|
||||||
|
"payload": {
|
||||||
|
"channel": {
|
||||||
|
"ownerBlockchainName": "bob-001",
|
||||||
|
"channelRootBlockNumber": 123,
|
||||||
|
"channelRootBlockHash": "..."
|
||||||
|
},
|
||||||
|
"limit": 200,
|
||||||
|
"sort": "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (success)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetChannelMessages",
|
||||||
|
"requestId": "req-2",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"channel": {
|
||||||
|
"ownerLogin": "Bob",
|
||||||
|
"ownerBlockchainName": "bob-001",
|
||||||
|
"channelName": "news",
|
||||||
|
"channelRoot": { "blockNumber": 123, "blockHash": "..." }
|
||||||
|
},
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"messageRef": { "blockNumber": 140, "blockHash": "..." },
|
||||||
|
"authorLogin": "Bob",
|
||||||
|
"authorBlockchainName": "bob-001",
|
||||||
|
"createdAtMs": 1760000000000,
|
||||||
|
"text": "текущая версия",
|
||||||
|
"likesCount": 12,
|
||||||
|
"repliesCount": 3,
|
||||||
|
"versionsTotal": 4,
|
||||||
|
"versions": [
|
||||||
|
{ "versionIndex": 1, "blockNumber": 140, "blockHash": "...", "text": "v1", "createdAtMs": 1760000000000 },
|
||||||
|
{ "versionIndex": 2, "blockNumber": 155, "blockHash": "...", "text": "v2", "createdAtMs": 1760001000000 },
|
||||||
|
{ "versionIndex": 3, "blockNumber": 170, "blockHash": "...", "text": "v3", "createdAtMs": 1760002000000 },
|
||||||
|
{ "versionIndex": 4, "blockNumber": 199, "blockHash": "...", "text": "v4", "createdAtMs": 1760003000000 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) GetMessageThread
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetMessageThread",
|
||||||
|
"requestId": "req-3",
|
||||||
|
"payload": {
|
||||||
|
"message": {
|
||||||
|
"blockchainName": "bob-001",
|
||||||
|
"blockNumber": 333,
|
||||||
|
"blockHash": "..."
|
||||||
|
},
|
||||||
|
"depthUp": 20,
|
||||||
|
"depthDown": 2,
|
||||||
|
"limitChildrenPerNode": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (success)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetMessageThread",
|
||||||
|
"requestId": "req-3",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"ancestors": [MessageNode],
|
||||||
|
"focus": MessageNode,
|
||||||
|
"descendants": [MessageNodeTree]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MessageNode (дополнение)
|
||||||
|
- `MessageNode` расширяет формат сообщения из `GetChannelMessages` и дополнительно содержит:
|
||||||
|
- `channelInfo` — мета-информация о канале (если применимо);
|
||||||
|
- `rawBlockB64` — сырой `block_bytes` текущего блока в Base64.
|
||||||
|
- Поле `rawBlockB64` присутствует у узлов во всех частях ответа `GetMessageThread`: `focus`, `ancestors[]`, `descendants[]`.
|
||||||
|
- В `GetChannelMessages` поле `rawBlockB64` **не добавляется** (лента канала без сырого блока, чтобы не раздувать ответ).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) GetChannelsCounters
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetChannelsCounters",
|
||||||
|
"requestId": "req-4",
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (success)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetChannelsCounters",
|
||||||
|
"requestId": "req-4",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice",
|
||||||
|
"feedCount": 12,
|
||||||
|
"dialogs100Count": 3,
|
||||||
|
"groupChats200Count": 4,
|
||||||
|
"myChannelsCount": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) ListGroupChats200
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListGroupChats200",
|
||||||
|
"requestId": "req-5",
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (success)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListGroupChats200",
|
||||||
|
"requestId": "req-5",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice",
|
||||||
|
"chats": [
|
||||||
|
{
|
||||||
|
"ownerLogin": "Alice",
|
||||||
|
"ownerBlockchainName": "alice-001",
|
||||||
|
"channelRootBlockNumber": 123,
|
||||||
|
"channelRootBlockHash": "...",
|
||||||
|
"channelName": "team",
|
||||||
|
"chatTitle": "Team chat",
|
||||||
|
"membersCount": 3,
|
||||||
|
"updatedAtMs": 1760000000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) GetGroupDialog
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetGroupDialog",
|
||||||
|
"requestId": "req-6",
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice",
|
||||||
|
"group": {
|
||||||
|
"ownerBlockchainName": "alice-001",
|
||||||
|
"channelRootBlockNumber": 123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (success)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetGroupDialog",
|
||||||
|
"requestId": "req-6",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"group": {
|
||||||
|
"ownerLogin": "Alice",
|
||||||
|
"ownerBlockchainName": "alice-001",
|
||||||
|
"channelRootBlockNumber": 123,
|
||||||
|
"channelName": "team",
|
||||||
|
"chatTitle": "Team chat"
|
||||||
|
},
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"authorLogin": "Bob",
|
||||||
|
"authorBlockchainName": "bob-001",
|
||||||
|
"blockNumber": 140,
|
||||||
|
"blockHash": "...",
|
||||||
|
"createdAtMs": 1760000000000,
|
||||||
|
"text": "Привет"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reason codes
|
||||||
|
- `bad_fields`
|
||||||
|
- `user_not_found`
|
||||||
|
- `channel_not_found`
|
||||||
|
- `message_not_found`
|
||||||
|
- `limit_too_large`
|
||||||
|
- `channel_name_already_exists`
|
||||||
|
- `internal_error`
|
||||||
145
Dev_Docs/API/07_Channels_Feature_Runbook.md
Normal file
145
Dev_Docs/API/07_Channels_Feature_Runbook.md
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# 07. Channels Feature Runbook (человеческое описание + диагностика)
|
||||||
|
|
||||||
|
## 1) Что уже сделано простыми словами
|
||||||
|
|
||||||
|
Сейчас реализован полный минимальный контур для каналов:
|
||||||
|
|
||||||
|
1. **Серверные read API**:
|
||||||
|
- `ListSubscriptionsFeed` — экран списка каналов.
|
||||||
|
- `GetChannelMessages` — сообщения конкретного канала.
|
||||||
|
- `GetMessageThread` — дерево обсуждения для сообщения.
|
||||||
|
|
||||||
|
2. **UI вкладки Каналы**:
|
||||||
|
- при открытии пытается загрузить реальный feed с сервера;
|
||||||
|
- если сервер недоступен — fallback на мок-данные;
|
||||||
|
- группы каналов выводятся в нужном порядке;
|
||||||
|
- есть кнопка «Добавить канал», модалки подписки, переход в канал.
|
||||||
|
|
||||||
|
3. **Проверка уникальности имени канала на сервере**
|
||||||
|
- в `AddBlock` при `CreateChannelBody` добавлена проверка;
|
||||||
|
- при дубле возвращается `409 channel_name_already_exists`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Что тестировать в первую очередь (быстрый чеклист)
|
||||||
|
|
||||||
|
### Базовый smoke
|
||||||
|
1. Авторизоваться в UI.
|
||||||
|
2. Открыть вкладку «Каналы».
|
||||||
|
3. Убедиться, что данные загрузились с сервера (или виден fallback-баннер).
|
||||||
|
4. Нажать любой канал — должен открыться экран канала с сообщениями.
|
||||||
|
|
||||||
|
### API smoke
|
||||||
|
1. Вызвать `ListSubscriptionsFeed`.
|
||||||
|
2. Для канала `ownedChannels[0]` вызвать `GetChannelMessages`.
|
||||||
|
3. Для первого `messages[0]` вызвать `GetMessageThread`.
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
1. `ListSubscriptionsFeed` с пустым login -> `bad_fields`.
|
||||||
|
2. `GetChannelMessages` с битым channel payload -> `bad_fields`.
|
||||||
|
3. `GetMessageThread` с несуществующим block -> `message_not_found`.
|
||||||
|
4. `AddBlock(CreateChannel)` с уже существующим именем -> `channel_name_already_exists`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Готовые JSON-запросы для ручной диагностики
|
||||||
|
|
||||||
|
## 3.1 ListSubscriptionsFeed
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListSubscriptionsFeed",
|
||||||
|
"requestId": "debug-feed-1",
|
||||||
|
"payload": {
|
||||||
|
"login": "TestUser1",
|
||||||
|
"limit": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.2 GetChannelMessages
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetChannelMessages",
|
||||||
|
"requestId": "debug-ch-1",
|
||||||
|
"payload": {
|
||||||
|
"channel": {
|
||||||
|
"ownerBlockchainName": "TestUser1-001",
|
||||||
|
"channelRootBlockNumber": 0,
|
||||||
|
"channelRootBlockHash": ""
|
||||||
|
},
|
||||||
|
"limit": 200,
|
||||||
|
"sort": "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.3 GetMessageThread
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetMessageThread",
|
||||||
|
"requestId": "debug-thread-1",
|
||||||
|
"payload": {
|
||||||
|
"message": {
|
||||||
|
"blockchainName": "TestUser1-001",
|
||||||
|
"blockNumber": 123,
|
||||||
|
"blockHash": "<hash-from-GetChannelMessages>"
|
||||||
|
},
|
||||||
|
"depthUp": 20,
|
||||||
|
"depthDown": 2,
|
||||||
|
"limitChildrenPerNode": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Что смотреть в ответах
|
||||||
|
|
||||||
|
### ListSubscriptionsFeed
|
||||||
|
- `payload.login` — канонический login.
|
||||||
|
- `ownedChannels / followedUsersChannels / followedChannels` — массивы.
|
||||||
|
- у каждой записи есть:
|
||||||
|
- `channel.channelRoot.blockNumber`,
|
||||||
|
- `messagesCount`,
|
||||||
|
- `lastMessage` (может быть null, если сообщений нет).
|
||||||
|
|
||||||
|
### GetChannelMessages
|
||||||
|
- `payload.channel` заполнен;
|
||||||
|
- `payload.messages[]` содержит:
|
||||||
|
- `likesCount`, `repliesCount`,
|
||||||
|
- `versionsTotal`, `versions[]`,
|
||||||
|
- `text` должен быть текущей (последней) версией.
|
||||||
|
|
||||||
|
### GetMessageThread
|
||||||
|
- `payload.ancestors[]`, `payload.focus`, `payload.descendants[]`.
|
||||||
|
- у узлов должны быть версии и счетчики.
|
||||||
|
- у каждого узла дополнительно может приходить `rawBlockB64` (Base64 сырого `block_bytes`).
|
||||||
|
|
||||||
|
### Важно по совместимости
|
||||||
|
- `rawBlockB64` добавлен только в `GetMessageThread`.
|
||||||
|
- `GetChannelMessages` не содержит `rawBlockB64` (без изменений формата ленты).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Частые проблемы и как быстро локализовать
|
||||||
|
|
||||||
|
1. **`status != 200`, code=bad_fields**
|
||||||
|
- проверить вложенность payload и обязательные поля.
|
||||||
|
|
||||||
|
2. **`message_not_found` в GetMessageThread**
|
||||||
|
- обычно передали blockNumber/hash не из `messageRef`.
|
||||||
|
|
||||||
|
3. **Пустой список сообщений в GetChannelMessages**
|
||||||
|
- проверить `ownerBlockchainName` и `channelRootBlockNumber`.
|
||||||
|
|
||||||
|
4. **`channel_name_already_exists` при AddBlock**
|
||||||
|
- это ожидаемо: в этой цепочке уже есть канал с таким именем.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) Для будущей доработки
|
||||||
|
|
||||||
|
1. Добавить курсоры (пагинацию) для больших каналов.
|
||||||
|
2. Перевести «Подписаться»/«Добавить канал» в UI с демо-заглушек на реальные write RPC.
|
||||||
|
3. Добавить batch-агрегации для thread/versions (оптимизация).
|
||||||
|
4. Добавить полноценные интеграционные тесты на негативные кейсы и нагрузку.
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
# MCP: чтение и дозапись персонального публичного чата (type=100)
|
||||||
|
|
||||||
|
Документ для реализации MCP-инструмента, который:
|
||||||
|
- читает переписку между двумя логинами (`from`, `to`);
|
||||||
|
- добавляет новое сообщение от отправителя через серверный `AddBlock`.
|
||||||
|
|
||||||
|
Важно: речь про **персональные публичные** каналы (`channelTypeCode=100`), а не приватные DM.
|
||||||
|
|
||||||
|
## 1. Базовые предпосылки
|
||||||
|
|
||||||
|
1. У каждого пользователя свой блокчейн (`<login>-001`).
|
||||||
|
2. Персональный публичный чат хранится как канал типа `100`:
|
||||||
|
- у `A` канал с `channelName = B`;
|
||||||
|
- у `B` зеркальный канал с `channelName = A`.
|
||||||
|
3. Сообщения канала — `TEXT_POST` и `TEXT_REPOST` в линии `line_code = rootBlockNumber` канала.
|
||||||
|
4. Запись блока возможна только при валидной подписи blockchain-ключом владельца цепочки.
|
||||||
|
|
||||||
|
## 2. Что должен уметь MCP-инструмент
|
||||||
|
|
||||||
|
Минимальный набор операций:
|
||||||
|
|
||||||
|
1. `read_personal_public_dialog(fromLogin, toLogin, limitPerSide=200)`
|
||||||
|
2. `append_personal_public_message(fromLogin, toLogin, text)`
|
||||||
|
|
||||||
|
## 3. Алгоритм чтения переписки
|
||||||
|
|
||||||
|
### 3.1 Найти оба канала (прямой и зеркальный)
|
||||||
|
|
||||||
|
Для `fromLogin = A`, `toLogin = B`:
|
||||||
|
|
||||||
|
1. Запросить `ListSubscriptionsFeed` для `A` и найти owned-канал:
|
||||||
|
- `ownerLogin == A`
|
||||||
|
- `channelName == B`
|
||||||
|
- `channelTypeCode == 100`
|
||||||
|
2. Запросить `ListSubscriptionsFeed` для `B` и найти owned-канал:
|
||||||
|
- `ownerLogin == B`
|
||||||
|
- `channelName == A`
|
||||||
|
- `channelTypeCode == 100`
|
||||||
|
|
||||||
|
Если какой-то из каналов не найден — вернуть частичный результат + флаг отсутствия зеркала.
|
||||||
|
|
||||||
|
### 3.2 Вычитать сообщения из каналов
|
||||||
|
|
||||||
|
Для каждого найденного канала вызвать `GetChannelMessages`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetChannelMessages",
|
||||||
|
"payload": {
|
||||||
|
"login": "<текущий-login-сессии>",
|
||||||
|
"channel": {
|
||||||
|
"ownerBlockchainName": "...",
|
||||||
|
"channelRootBlockNumber": 123,
|
||||||
|
"channelRootBlockHash": "..."
|
||||||
|
},
|
||||||
|
"limit": 200,
|
||||||
|
"sort": "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Склеить в единый диалог
|
||||||
|
|
||||||
|
1. Объединить массивы сообщений из `A->B` и `B->A`.
|
||||||
|
2. Отсортировать по `createdAtMs`, при равенстве — по `messageRef.blockNumber`.
|
||||||
|
3. Вернуть структуру:
|
||||||
|
- `messages[]`
|
||||||
|
- `directChannelFound` / `reverseChannelFound`
|
||||||
|
- метаданные обоих каналов.
|
||||||
|
|
||||||
|
## 4. Алгоритм дозаписи сообщения
|
||||||
|
|
||||||
|
Цель: добавить сообщение **от имени `fromLogin`** в его канал `fromLogin -> toLogin`.
|
||||||
|
|
||||||
|
### 4.1 Найти канал отправителя
|
||||||
|
|
||||||
|
Через `ListSubscriptionsFeed(fromLogin)` найти owned-канал:
|
||||||
|
- `channelName == toLogin`
|
||||||
|
- `channelTypeCode == 100`
|
||||||
|
|
||||||
|
Если канал не найден — вернуть ошибку `channel_not_found`.
|
||||||
|
|
||||||
|
### 4.2 Отправить `AddBlock` с `TEXT_POST`
|
||||||
|
|
||||||
|
Использовать клиентский/серверный helper формирования `TEXT_POST` body:
|
||||||
|
- `lineCode = channelRootBlockNumber`;
|
||||||
|
- `prevLineNumber/prevLineHash` берутся из последнего сообщения линии;
|
||||||
|
- подпись — blockchain private key пользователя `fromLogin`.
|
||||||
|
|
||||||
|
Если у вас в MCP нет приватного ключа пользователя, дозапись невозможна.
|
||||||
|
|
||||||
|
## 5. Контракт MCP (рекомендуемый)
|
||||||
|
|
||||||
|
## `read_personal_public_dialog`
|
||||||
|
|
||||||
|
Вход:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fromLogin": "alice",
|
||||||
|
"toLogin": "bob",
|
||||||
|
"limitPerSide": 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Выход:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"directChannelFound": true,
|
||||||
|
"reverseChannelFound": true,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"authorLogin": "alice",
|
||||||
|
"text": "Привет",
|
||||||
|
"createdAtMs": 1760000000000,
|
||||||
|
"channelSide": "alice->bob",
|
||||||
|
"messageRef": { "blockNumber": 11, "blockHash": "..." }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `append_personal_public_message`
|
||||||
|
|
||||||
|
Вход:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fromLogin": "alice",
|
||||||
|
"toLogin": "bob",
|
||||||
|
"text": "Тест"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Выход:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"serverLastGlobalNumber": 321,
|
||||||
|
"serverLastGlobalHash": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Ограничения и безопасность
|
||||||
|
|
||||||
|
1. Персональный канал типа `100` сейчас публичный по модели чтения (не E2E DM).
|
||||||
|
2. Нельзя дозаписать блок в чужой блокчейн без приватного ключа владельца (проверка подписи сервером).
|
||||||
|
3. Для прод-инструмента нужно:
|
||||||
|
- строгая авторизация MCP-вызовов;
|
||||||
|
- аудит, кто и от чьего имени запрашивал чтение/запись;
|
||||||
|
- лимиты/квоты на запись.
|
||||||
|
|
||||||
|
## 7. Мини-чеклист для реализации MCP
|
||||||
|
|
||||||
|
1. Реализовать helper поиска канала `findOwnedPersonalChannel(ownerLogin, peerLogin)`.
|
||||||
|
2. Реализовать чтение двух сторон и merge/sort.
|
||||||
|
3. Реализовать отправку `TEXT_POST` в найденный канал отправителя.
|
||||||
|
4. Добавить понятные ошибки:
|
||||||
|
- `user_not_found`
|
||||||
|
- `channel_not_found`
|
||||||
|
- `reverse_channel_not_found`
|
||||||
|
- `signature_required`
|
||||||
|
- `add_block_failed`
|
||||||
73
Dev_Docs/API/09_Operations_Index.md
Normal file
73
Dev_Docs/API/09_Operations_Index.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# API для разработчиков: индекс операций
|
||||||
|
|
||||||
|
Этот файл фиксирует полный список публичных JSON/WebSocket операций, зарегистрированных в коде сервера.
|
||||||
|
|
||||||
|
Источник истины на момент актуализации:
|
||||||
|
|
||||||
|
- `shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java`.
|
||||||
|
|
||||||
|
Если операция есть в `HANDLERS` и `REQUEST_TYPES`, клиент может отправлять её как `op` в общем JSON-конверте из `00_Common_API_Format.md`.
|
||||||
|
|
||||||
|
## Актуальные операции
|
||||||
|
|
||||||
|
| Операция | Раздел документации | Кратко |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AddUser` | `01_User_Registration_API.md` | отключено (`410 / ADD_USER_DISABLED`) |
|
||||||
|
| `GetUser` | `01_User_Registration_API.md` | чтение/проверка пользователя + server-состояние его блокчейна |
|
||||||
|
| `SearchUsers` | `01_User_Registration_API.md` | поиск логинов по префиксу |
|
||||||
|
| `TestGetFreeAvatarQuota` | `14_Test_Free_Avatar_Upload_API.md` | временный тестовый просмотр остатка бесплатных загрузок аватара |
|
||||||
|
| `TestUploadFreeAvatar` | `14_Test_Free_Avatar_Upload_API.md` | временная тестовая бесплатная загрузка маленького аватара в Arweave |
|
||||||
|
| `AuthChallenge` | `02_Authentication_API.md` | challenge для создания новой сессии |
|
||||||
|
| `CreateAuthSession` | `02_Authentication_API.md` | создание новой авторизованной сессии |
|
||||||
|
| `SessionChallenge` | `02_Authentication_API.md` | challenge для входа в существующую сессию |
|
||||||
|
| `SessionLogin` | `02_Authentication_API.md` | вход в существующую сессию |
|
||||||
|
| `GetTrustedDeviceLoginSettings` | `03_Session_Management_API.md` | чтение текущего режима входа через доверенное устройство |
|
||||||
|
| `UpsertTrustedDeviceLoginSettings` | `03_Session_Management_API.md` | включение/обновление pairing-настроек доверенной сессией |
|
||||||
|
| `StartTrustedDeviceLogin` | `03_Session_Management_API.md` | создание pairing-заявки для нового устройства |
|
||||||
|
| `ListTrustedDeviceLoginRequests` | `03_Session_Management_API.md` | список активных pairing-заявок для доверенной сессии |
|
||||||
|
| `ApproveTrustedDeviceLogin` | `03_Session_Management_API.md` | подтверждение pairing-заявки доверенной сессией |
|
||||||
|
| `RejectTrustedDeviceLogin` | `03_Session_Management_API.md` | отклонение pairing-заявки доверенной сессией |
|
||||||
|
| `CancelTrustedDeviceLogin` | `03_Session_Management_API.md` | отмена pairing-заявки со стороны нового ожидающего устройства |
|
||||||
|
| `GetTrustedDeviceLoginStatus` | `03_Session_Management_API.md` | чтение статуса и результата pairing-заявки |
|
||||||
|
| `ListSessions` | `03_Session_Management_API.md` | список активных сессий |
|
||||||
|
| `CloseActiveSession` | `03_Session_Management_API.md` | закрытие активной сессии |
|
||||||
|
| `AddBlock` | `04_Add_Block_to_Blockchain_API.md` | добавление блока в блокчейн |
|
||||||
|
| `GetBlockchainBlock` | `04_Add_Block_to_Blockchain_API.md` | чтение одного блока блокчейна |
|
||||||
|
| `Ping` | `05_Technical_Requests_API.md` | keep-alive |
|
||||||
|
| `GetServerInfo` | `05_Technical_Requests_API.md` | публичная информация о сервере |
|
||||||
|
| `ListBlockchainHeads` | `05_Technical_Requests_API.md` | список heads всех локальных блокчейнов |
|
||||||
|
| `GetSyncUserProfile` | `05_Technical_Requests_API.md` | межсерверный профиль пользователя для синхронизации |
|
||||||
|
| `SendSignal` | `05_Technical_Requests_API.md` | общий межсессионный технический сигнал в одну или все сессии пользователя |
|
||||||
|
| `GetCallIceConfig` | `05_Technical_Requests_API.md` | STUN/TURN конфигурация звонков |
|
||||||
|
| `ClientErrorLog` | `05_Technical_Requests_API.md` | логирование клиентской ошибки |
|
||||||
|
| `ClientDebugLog` | `05_Technical_Requests_API.md` | клиентский debug-лог |
|
||||||
|
| `CallDeliveryReport` | `05_Technical_Requests_API.md` | диагностика доставки/установки звонков |
|
||||||
|
| `ListSubscriptionsFeed` | `06_Channels_Read_API.md` | лента каналов/подписок |
|
||||||
|
| `GetChannelMessages` | `06_Channels_Read_API.md` | сообщения канала |
|
||||||
|
| `GetMessageThread` | `06_Channels_Read_API.md` | тред сообщения |
|
||||||
|
| `GetChannelsCounters` | `06_Channels_Read_API.md` | счетчики разделов каналов |
|
||||||
|
| `ListGroupChats200` | `06_Channels_Read_API.md` | список групповых чатов типа `200` |
|
||||||
|
| `GetGroupDialog` | `06_Channels_Read_API.md` | сообщения группового чата типа `200` |
|
||||||
|
| `UpsertUserParam` | `10_User_Params_API.md` | запись параметра пользователя |
|
||||||
|
| `GetUserParam` | `10_User_Params_API.md` | чтение одного параметра пользователя |
|
||||||
|
| `ListUserParams` | `10_User_Params_API.md` | список параметров пользователя |
|
||||||
|
| `GetFriendsLists` | `11_Connections_API.md` | входящие/исходящие друзья |
|
||||||
|
| `ListContacts` | `11_Connections_API.md` | контакты текущего пользователя |
|
||||||
|
| `GetUserConnectionsGraph` | `11_Connections_API.md` | граф связей пользователя |
|
||||||
|
| `AddCloseFriend` | `11_Connections_API.md` | добавить близкого друга |
|
||||||
|
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
|
||||||
|
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
|
||||||
|
| `SendDirectMessage` | `12_Direct_Messages_Push_Calls_API.md` | отправка подписанного DM-пакета |
|
||||||
|
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
|
||||||
|
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | алиас `SendMessagePair` |
|
||||||
|
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
|
||||||
|
| `AckSessionDelivery` | `12_Direct_Messages_Push_Calls_API.md` | подтверждение доставки в сессию |
|
||||||
|
| `CallInviteBroadcast` | `12_Direct_Messages_Push_Calls_API.md` | broadcast приглашения к звонку |
|
||||||
|
| `CallSignalToSession` | `12_Direct_Messages_Push_Calls_API.md` | сигнал звонка в конкретную сессию |
|
||||||
|
|
||||||
|
## Важные замечания
|
||||||
|
|
||||||
|
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
|
||||||
|
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
||||||
|
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
|
||||||
|
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
|
||||||
129
Dev_Docs/API/10_User_Params_API.md
Normal file
129
Dev_Docs/API/10_User_Params_API.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# API для разработчиков: параметры пользователя
|
||||||
|
|
||||||
|
Документ описывает операции для записи и чтения пользовательских параметров.
|
||||||
|
|
||||||
|
Текущие операции:
|
||||||
|
|
||||||
|
- `UpsertUserParam`
|
||||||
|
- `GetUserParam`
|
||||||
|
- `ListUserParams`
|
||||||
|
|
||||||
|
## 1. `UpsertUserParam`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "UpsertUserParam",
|
||||||
|
"requestId": "param-upsert-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice",
|
||||||
|
"param": "display_name",
|
||||||
|
"time_ms": 1774700000123,
|
||||||
|
"value": "Alice",
|
||||||
|
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
|
"signature": "BASE64_SIGNATURE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "UpsertUserParam",
|
||||||
|
"requestId": "param-upsert-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Типовые ошибки
|
||||||
|
|
||||||
|
- `400 / BAD_FIELDS` — некорректные обязательные поля.
|
||||||
|
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||||||
|
- `501 / DB_ERROR` — ошибка БД.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `GetUserParam`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetUserParam",
|
||||||
|
"requestId": "param-get-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice",
|
||||||
|
"param": "display_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetUserParam",
|
||||||
|
"requestId": "param-get-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"login": "alice",
|
||||||
|
"param": "display_name",
|
||||||
|
"time_ms": 1774700000123,
|
||||||
|
"value": "Alice",
|
||||||
|
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
|
"signature": "BASE64_SIGNATURE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если параметр не найден, сервер возвращает `404` с пустым `payload`; отдельный прикладной код ошибки текущий handler не задаёт.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `ListUserParams`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListUserParams",
|
||||||
|
"requestId": "param-list-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListUserParams",
|
||||||
|
"requestId": "param-list-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"login": "alice",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"login": "alice",
|
||||||
|
"param": "display_name",
|
||||||
|
"time_ms": 1774700000123,
|
||||||
|
"value": "Alice",
|
||||||
|
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
|
"signature": "BASE64_SIGNATURE"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примечание
|
||||||
|
|
||||||
|
Имена JSON-полей `time_ms` и `client_key` сейчас соответствуют Java-модели ответа/запроса и должны передаваться именно в таком виде.
|
||||||
174
Dev_Docs/API/11_Connections_API.md
Normal file
174
Dev_Docs/API/11_Connections_API.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# API для разработчиков: связи пользователей
|
||||||
|
|
||||||
|
Документ описывает операции чтения и записи пользовательских связей.
|
||||||
|
|
||||||
|
Текущие операции:
|
||||||
|
|
||||||
|
- `GetFriendsLists`
|
||||||
|
- `ListContacts`
|
||||||
|
- `GetUserConnectionsGraph`
|
||||||
|
- `AddCloseFriend`
|
||||||
|
|
||||||
|
## 1. `GetFriendsLists`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetFriendsLists",
|
||||||
|
"requestId": "friends-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetFriendsLists",
|
||||||
|
"requestId": "friends-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice",
|
||||||
|
"out_friends": ["Bob"],
|
||||||
|
"in_friends": ["Kate"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `ListContacts`
|
||||||
|
|
||||||
|
`ListContacts` использует текущую авторизованную сессию. В payload нет дополнительных полей.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListContacts",
|
||||||
|
"requestId": "contacts-001",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListContacts",
|
||||||
|
"requestId": "contacts-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice",
|
||||||
|
"contacts": ["Bob", "Kate"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `GetUserConnectionsGraph`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetUserConnectionsGraph",
|
||||||
|
"requestId": "graph-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetUserConnectionsGraph",
|
||||||
|
"requestId": "graph-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice",
|
||||||
|
"outFriends": ["Bob"],
|
||||||
|
"inFriends": ["Kate"],
|
||||||
|
"outContacts": [],
|
||||||
|
"inContacts": [],
|
||||||
|
"outFollows": [],
|
||||||
|
"inFollows": [],
|
||||||
|
"outSpouses": [],
|
||||||
|
"inSpouses": [],
|
||||||
|
"outParents": [],
|
||||||
|
"inParents": [],
|
||||||
|
"outChildren": [],
|
||||||
|
"inChildren": [],
|
||||||
|
"outSiblings": [],
|
||||||
|
"inSiblings": [],
|
||||||
|
"outKnownPersons": [],
|
||||||
|
"inKnownPersons": [],
|
||||||
|
"outShineConfirmed": [],
|
||||||
|
"inShineConfirmed": [],
|
||||||
|
"outShineSeen": [],
|
||||||
|
"inShineSeen": [],
|
||||||
|
"parents": [],
|
||||||
|
"children": [],
|
||||||
|
"siblings": [],
|
||||||
|
"spouses": [],
|
||||||
|
"allUsers": [
|
||||||
|
{
|
||||||
|
"login": "Bob",
|
||||||
|
"official": false,
|
||||||
|
"shine": true,
|
||||||
|
"officialLabel": "",
|
||||||
|
"shineLabel": "shine",
|
||||||
|
"avatar": { "ar": "..." }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Примечание
|
||||||
|
|
||||||
|
Поля `known_person`, `shine_confirmed`, `shine_seen` в UI считаются недопроверенной зоной проекта; при изменениях этой логики нужна ручная end-to-end проверка.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `AddCloseFriend`
|
||||||
|
|
||||||
|
`AddCloseFriend` использует текущую авторизованную сессию как источник `login`.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AddCloseFriend",
|
||||||
|
"requestId": "close-friend-001",
|
||||||
|
"payload": {
|
||||||
|
"toLogin": "bob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AddCloseFriend",
|
||||||
|
"requestId": "close-friend-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"login": "Alice",
|
||||||
|
"toLogin": "Bob",
|
||||||
|
"relation": "close_friend"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
190
Dev_Docs/API/12_Direct_Messages_Push_Calls_API.md
Normal file
190
Dev_Docs/API/12_Direct_Messages_Push_Calls_API.md
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
# API для разработчиков: DM, push и сигналы звонков
|
||||||
|
|
||||||
|
Документ описывает публичные операции, связанные с личными сообщениями, WebPush и сигналами звонков.
|
||||||
|
|
||||||
|
Подробная логика DM и бинарного формата:
|
||||||
|
|
||||||
|
- `Dev_Docs/Personal_Messages/README.md`
|
||||||
|
|
||||||
|
## 1. `UpsertPushToken`
|
||||||
|
|
||||||
|
Требует авторизации.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "UpsertPushToken",
|
||||||
|
"requestId": "push-upsert-001",
|
||||||
|
"payload": {
|
||||||
|
"sessionId": "SESSION_ID",
|
||||||
|
"endpoint": "https://push.example/...",
|
||||||
|
"p256dhKey": "BASE64",
|
||||||
|
"authKey": "BASE64",
|
||||||
|
"platform": "web",
|
||||||
|
"userAgent": "Mozilla/5.0 ..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "UpsertPushToken",
|
||||||
|
"requestId": "push-upsert-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"tokenId": "token-1",
|
||||||
|
"updatedAtMs": 1774700000123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. `SendTestWebPush`
|
||||||
|
|
||||||
|
Требует авторизации.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SendTestWebPush",
|
||||||
|
"requestId": "push-test-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice",
|
||||||
|
"sessionId": "SESSION_ID",
|
||||||
|
"title": "Test",
|
||||||
|
"text": "Push body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. `SendMessagePair` и `ReceiveOutcomingMessage`
|
||||||
|
|
||||||
|
`ReceiveOutcomingMessage` — алиас `SendMessagePair`.
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Передаёт пару signed DM-блоков:
|
||||||
|
|
||||||
|
- `incomingBlobB64` — блок `type=1` или `type=3`
|
||||||
|
- `outgoingBlobB64` — блок `type=2` или `type=4`
|
||||||
|
|
||||||
|
Для контентных сообщений `type=1/2` внутри base64 лежит бинарный формат `SHiNE_DM`.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SendMessagePair",
|
||||||
|
"requestId": "dm-pair-001",
|
||||||
|
"payload": {
|
||||||
|
"incomingBlobB64": "BASE64_INCOMING_SIGNED_BLOCK",
|
||||||
|
"outgoingBlobB64": "BASE64_OUTGOING_SIGNED_BLOCK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "SendMessagePair",
|
||||||
|
"requestId": "dm-pair-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"baseKey": "from|to|time|nonce",
|
||||||
|
"incomingKey": "from|to|time|nonce|1",
|
||||||
|
"outgoingKey": "from|to|time|nonce|2",
|
||||||
|
"deliveredWsSessions": 1,
|
||||||
|
"deliveredWebPushSessions": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
|
||||||
|
- `400 / BAD_BLOCK_FORMAT` — base64 или бинарный контейнер повреждён
|
||||||
|
- `400 / BAD_CONTENT_FORMAT` — для контентного сообщения пришёл не `SHiNE_DM`
|
||||||
|
- `400 / ATTACHMENTS_DISABLED` — в `SHiNE_DM` пришёл `attachmentsCount != 0`
|
||||||
|
- `404 / USER_NOT_FOUND` — один из логинов не найден
|
||||||
|
- `460 / BAD_SIGNATURE` — подпись блока не прошла проверку
|
||||||
|
|
||||||
|
## 4. `ReceiveIncomingMessage`
|
||||||
|
|
||||||
|
Принимает только один входящий signed DM-блок.
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Используется там, где нужно принять только incoming-вариант сообщения.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ReceiveIncomingMessage",
|
||||||
|
"requestId": "dm-in-001",
|
||||||
|
"payload": {
|
||||||
|
"incomingBlobB64": "BASE64_INCOMING_SIGNED_BLOCK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. `AckSessionDelivery`
|
||||||
|
|
||||||
|
Требует авторизации. Подтверждает доставку в текущую сессию.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "AckSessionDelivery",
|
||||||
|
"requestId": "ack-001",
|
||||||
|
"payload": {
|
||||||
|
"messageKey": "from|to|time|nonce|1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Событие `SignedMessageArrived`
|
||||||
|
|
||||||
|
Сервер присылает его по WebSocket в активные сессии адресата.
|
||||||
|
|
||||||
|
### Payload события
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"messageKey": "from|to|time|nonce|1",
|
||||||
|
"baseKey": "from|to|time|nonce",
|
||||||
|
"fromLogin": "alice",
|
||||||
|
"toLogin": "bob",
|
||||||
|
"targetLogin": "bob",
|
||||||
|
"messageType": 1,
|
||||||
|
"timeMs": 1774700000123,
|
||||||
|
"nonce": 123456789,
|
||||||
|
"blobB64": "BASE64_SIGNED_BLOCK",
|
||||||
|
"backlog": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если это новая ревизия того же письма, `messageKey` остаётся тем же, а `revisionTimeMs` меняется внутри бинарного блока.
|
||||||
|
|
||||||
|
## 7. `CallInviteBroadcast`
|
||||||
|
|
||||||
|
Требует авторизации. Шлёт приглашение к звонку в активные сессии `toLogin`.
|
||||||
|
|
||||||
|
## 8. `CallSignalToSession`
|
||||||
|
|
||||||
|
Требует авторизации. Шлёт сигнал звонка в конкретную сессию.
|
||||||
|
|
||||||
|
## 9. Замечания
|
||||||
|
|
||||||
|
- read-receipt `type=3/4` пока остаются в legacy-формате `SHiNE_dm2`
|
||||||
|
- контентные DM `type=1/2` используют `SHiNE_DM`
|
||||||
|
- сервер хранит только последнюю версию контентного сообщения по `messageKey`
|
||||||
|
- удаление сообщения реализуется новой ревизией с пустым телом и `attachmentsCount = 0`
|
||||||
|
- HTTP endpoints для DM-файлов сейчас отсутствуют
|
||||||
190
Dev_Docs/API/13_HTTP_Debug_API.md
Normal file
190
Dev_Docs/API/13_HTTP_Debug_API.md
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
# API для разработчиков: HTTP debug endpoints
|
||||||
|
|
||||||
|
Этот файл описывает отдельный HTTP debug API сервера. Он не использует WebSocket-конверт `op/requestId/payload` и включается только настройкой:
|
||||||
|
|
||||||
|
- `debug.tempApi.enabled=true`
|
||||||
|
|
||||||
|
Источник истины:
|
||||||
|
|
||||||
|
- `src/main/java/server/debug/DebugApiConfigurator.java`
|
||||||
|
- `src/main/java/server/debug/*Servlet.java`
|
||||||
|
|
||||||
|
Если `.debug-token` отсутствует или пуст, endpoints возвращают `503 / DEBUG_DISABLED`.
|
||||||
|
|
||||||
|
## Авторизация
|
||||||
|
|
||||||
|
Для большинства debug endpoints используется Bearer token из `.debug-token`:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Для `POST /debug/ws/ui-reload-all` поддерживается заголовок:
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-Debug-Token: <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
При неверном токене сервер возвращает `401 / UNAUTHORIZED`.
|
||||||
|
|
||||||
|
## Формат успешного HTTP-ответа
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Формат HTTP-ошибки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": false,
|
||||||
|
"code": "BAD_JSON",
|
||||||
|
"message": "Тело запроса должно быть JSON"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. `GET /debug/ws/clients`
|
||||||
|
|
||||||
|
Возвращает список активных WebSocket-клиентов.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"count": 1,
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"sessionId": "SESSION_ID",
|
||||||
|
"login": "alice",
|
||||||
|
"authStatus": 2,
|
||||||
|
"wsOpen": true,
|
||||||
|
"remoteAddress": "127.0.0.1",
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"userAgent": "Mozilla/5.0 ...",
|
||||||
|
"clientInfoFromClient": "...",
|
||||||
|
"clientInfoFromRequest": "...",
|
||||||
|
"userLanguage": "ru",
|
||||||
|
"sessionCreatedAtMs": 1774700000123
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. `POST /debug/ws/connect`
|
||||||
|
|
||||||
|
Запускает debug-сценарий соединения двух активных WS-сессий и отправляет им события:
|
||||||
|
|
||||||
|
- `DebugConnectPrepareResponder`
|
||||||
|
- `DebugConnectStartInitiator`
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"initiatorSessionId": "SESSION_ID_1",
|
||||||
|
"responderSessionId": "SESSION_ID_2",
|
||||||
|
"clearDebugLog": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"runId": "dbg-...",
|
||||||
|
"callId": "debug-call-...",
|
||||||
|
"accepted": true,
|
||||||
|
"initiatorSessionId": "SESSION_ID_1",
|
||||||
|
"responderSessionId": "SESSION_ID_2",
|
||||||
|
"initiatorLogin": "alice",
|
||||||
|
"responderLogin": "bob",
|
||||||
|
"mode": "cross-login"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / BAD_JSON` — тело запроса не JSON.
|
||||||
|
- `400 / BAD_FIELDS` — не заполнены sessionId или переданы одинаковые sessionId.
|
||||||
|
- `404 / INITIATOR_NOT_FOUND` — сессия инициатора не найдена или неактивна.
|
||||||
|
- `404 / RESPONDER_NOT_FOUND` — сессия получателя не найдена или неактивна.
|
||||||
|
|
||||||
|
## 3. `GET /debug/ws/logs`
|
||||||
|
|
||||||
|
Возвращает tail debug-логов из `DebugRunLogBuffer`.
|
||||||
|
|
||||||
|
### Query-параметры
|
||||||
|
|
||||||
|
- `limit` — количество записей.
|
||||||
|
- `runId` — фильтр по runId.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"count": 1,
|
||||||
|
"limit": 100,
|
||||||
|
"runIdFilter": "ui-run-1",
|
||||||
|
"logs": [
|
||||||
|
{
|
||||||
|
"ts": 1774700000123,
|
||||||
|
"level": "info",
|
||||||
|
"runId": "ui-run-1",
|
||||||
|
"source": "debug-connect",
|
||||||
|
"sessionId": "SESSION_ID",
|
||||||
|
"login": "alice",
|
||||||
|
"message": "opened channels tab",
|
||||||
|
"details": "{}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. `POST /debug/ws/ui-reload-all`
|
||||||
|
|
||||||
|
Рассылает активным UI-сессиям debug-событие на reload.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reason": "manual_debug_api",
|
||||||
|
"reloadAfterMs": 700
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `reason` не передан или пустой, сервер использует `manual_debug_api`. `reloadAfterMs` ограничивается диапазоном `100..15000`.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"accepted": true,
|
||||||
|
"reason": "manual_debug_api",
|
||||||
|
"reloadAfterMs": 700,
|
||||||
|
"issuedAtMs": 1774700000123,
|
||||||
|
"totalConnections": 2,
|
||||||
|
"sentCount": 2,
|
||||||
|
"skippedCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / BAD_JSON` — тело запроса не JSON.
|
||||||
176
Dev_Docs/API/14_Test_Free_Avatar_Upload_API.md
Normal file
176
Dev_Docs/API/14_Test_Free_Avatar_Upload_API.md
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
# Временное Test API для бесплатной загрузки аватаров в Arweave
|
||||||
|
|
||||||
|
> Статус: **временное тестовое решение**.
|
||||||
|
> Все операции из этого файла начинаются с `Test...`, чтобы это было видно сразу и в коде, и в UI.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Этот временный API даёт пользователю ограниченную бесплатную загрузку маленьких аватаров в Arweave:
|
||||||
|
|
||||||
|
- загрузка идёт через **серверный Arweave-кошелёк**;
|
||||||
|
- лимит на пользователя: по умолчанию `3` загрузки за всё время;
|
||||||
|
- лимит хранится в SQLite-таблице `test_free_avatar_uploads`;
|
||||||
|
- если лимит исчерпан, сервер возвращает понятную ошибку;
|
||||||
|
- загружать можно только маленький итоговый файл аватара, по умолчанию до `128 KB`.
|
||||||
|
|
||||||
|
## Настройки сервера
|
||||||
|
|
||||||
|
В `application.properties`:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
test.freeAvatar.enabled=true
|
||||||
|
test.freeAvatar.gateway=https://arweave.net
|
||||||
|
test.freeAvatar.limitPerUser=3
|
||||||
|
test.freeAvatar.maxBytes=131072
|
||||||
|
test.freeAvatar.walletAddress=
|
||||||
|
test.freeAvatar.walletJwkPath=
|
||||||
|
```
|
||||||
|
|
||||||
|
Пояснения:
|
||||||
|
|
||||||
|
- `test.freeAvatar.enabled` - включить или выключить временный API;
|
||||||
|
- `test.freeAvatar.gateway` - Arweave gateway для `price/tx/wallet`;
|
||||||
|
- `test.freeAvatar.limitPerUser` - пожизненный бесплатный лимит на пользователя;
|
||||||
|
- `test.freeAvatar.maxBytes` - максимальный размер итогового файла;
|
||||||
|
- `test.freeAvatar.walletAddress` - публичный адрес серверного Arweave-кошелька;
|
||||||
|
- `test.freeAvatar.walletJwkPath` - путь к приватному JWK-файлу серверного кошелька.
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
- приватный JWK хранится вне кода;
|
||||||
|
- если `walletAddress` указан и не совпадает с адресом, вычисленным из JWK, сервер вернёт ошибку настройки.
|
||||||
|
|
||||||
|
## `TestGetFreeAvatarQuota`
|
||||||
|
|
||||||
|
Возвращает остаток бесплатных загрузок для текущего авторизованного пользователя.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "TestGetFreeAvatarQuota",
|
||||||
|
"requestId": "req-test-avatar-quota-1",
|
||||||
|
"payload": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "TestGetFreeAvatarQuota",
|
||||||
|
"requestId": "req-test-avatar-quota-1",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"enabled": true,
|
||||||
|
"limit": 3,
|
||||||
|
"usedCount": 1,
|
||||||
|
"remainingCount": 2,
|
||||||
|
"maxBytes": 131072
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поля ответа
|
||||||
|
|
||||||
|
- `enabled` - временный API сейчас включён на сервере или нет;
|
||||||
|
- `limit` - полный лимит бесплатных загрузок;
|
||||||
|
- `usedCount` - сколько уже израсходовано;
|
||||||
|
- `remainingCount` - сколько ещё осталось;
|
||||||
|
- `maxBytes` - максимальный размер итогового файла.
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `422 NOT_AUTHENTICATED` - требуется авторизация.
|
||||||
|
|
||||||
|
## `TestUploadFreeAvatar`
|
||||||
|
|
||||||
|
Временная бесплатная загрузка маленькой аватарки в Arweave через серверный кошелёк.
|
||||||
|
|
||||||
|
### Правила
|
||||||
|
|
||||||
|
- операция требует авторизованную сессию;
|
||||||
|
- сервер использует текущий login из сессии;
|
||||||
|
- сервер принимает только:
|
||||||
|
- `image/jpeg`
|
||||||
|
- `image/png`
|
||||||
|
- `image/webp`
|
||||||
|
- размер итогового файла должен быть не больше `maxBytes` из квоты;
|
||||||
|
- если пользователь уже сделал `limit` бесплатных загрузок, операция запрещена.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
`fileBytesBase64` - это обычный Base64 байт итогового подготовленного файла.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "TestUploadFreeAvatar",
|
||||||
|
"requestId": "req-test-avatar-upload-1",
|
||||||
|
"payload": {
|
||||||
|
"contentType": "image/webp",
|
||||||
|
"fileBytesBase64": "UklGRiQAAABXRUJQVlA4WAoAAAAQAAAA...",
|
||||||
|
"sha256Hex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "TestUploadFreeAvatar",
|
||||||
|
"requestId": "req-test-avatar-upload-1",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"txId": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"sha256Hex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||||
|
"usedCount": 2,
|
||||||
|
"remainingCount": 1,
|
||||||
|
"limit": 3,
|
||||||
|
"gateway": "https://arweave.net"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поля ответа
|
||||||
|
|
||||||
|
- `txId` - Arweave Transaction ID загруженного файла;
|
||||||
|
- `sha256Hex` - SHA-256 загруженного файла;
|
||||||
|
- `usedCount` - сколько бесплатных загрузок уже израсходовано после этой операции;
|
||||||
|
- `remainingCount` - сколько бесплатных загрузок осталось;
|
||||||
|
- `limit` - общий лимит;
|
||||||
|
- `gateway` - gateway, через который сервер отправлял транзакцию.
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `422 NOT_AUTHENTICATED` - требуется авторизация;
|
||||||
|
- `400 BAD_FIELDS` - не переданы `contentType` или `fileBytesBase64`;
|
||||||
|
- `400 BAD_BASE64` - `fileBytesBase64` не декодируется;
|
||||||
|
- `400 BAD_AVATAR_FILE` - файл не проходит ограничения сервера;
|
||||||
|
- `400 FREE_AVATAR_LIMIT_EXHAUSTED` - бесплатный лимит аватарок исчерпан;
|
||||||
|
- `501 FREE_AVATAR_TEMP_DISABLED` - временная функция выключена или сервер не настроен;
|
||||||
|
- `500 INTERNAL_ERROR` - внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
## Как это используется в UI
|
||||||
|
|
||||||
|
На экране редактирования профиля в мастере смены аватара есть временный сценарий:
|
||||||
|
|
||||||
|
- `Залить аватар бесплатно`
|
||||||
|
|
||||||
|
UI:
|
||||||
|
|
||||||
|
1. вызывает `TestGetFreeAvatarQuota`;
|
||||||
|
2. показывает остаток лимита;
|
||||||
|
3. локально подготавливает уменьшенный файл аватара;
|
||||||
|
4. проверяет, что итоговый файл не превышает `maxBytes`;
|
||||||
|
5. вызывает `TestUploadFreeAvatar`;
|
||||||
|
6. после получения `txId` обычным путём записывает `avatar.ar` в профиль через `AddBlock`.
|
||||||
|
|
||||||
|
## Почему решение временное
|
||||||
|
|
||||||
|
- используется общий серверный Arweave-кошелёк;
|
||||||
|
- лимит хранится отдельной технической таблицей;
|
||||||
|
- операции имеют префикс `Test...`;
|
||||||
|
- сценарий нужен как переходный бесплатный путь для маленьких аватаров.
|
||||||
25
Dev_Docs/Blockchain/00_Blockchain_Formats_and_Block_Types.md
Normal file
25
Dev_Docs/Blockchain/00_Blockchain_Formats_and_Block_Types.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Форматы блокчейнов и блоков (индекс раздела)
|
||||||
|
|
||||||
|
Этот раздел разбит на несколько файлов, чтобы формат добавляемых блоков было проще читать и сопровождать.
|
||||||
|
|
||||||
|
## Структура раздела
|
||||||
|
|
||||||
|
- `01_Common_Block_Format.md` — общий бинарный формат блока (Frame v0), правила подписи и проверки.
|
||||||
|
- `02_Blockchain_Kinds_and_Lines.md` — виды блокчейнов и логические линии внутри цепочки.
|
||||||
|
- `10_TECH_Blocks.md` — системные блоки (`type=0`).
|
||||||
|
- `11_TEXT_Blocks.md` — текстовые блоки (`type=1`).
|
||||||
|
- `12_REACTION_Blocks.md` — реакции (`type=2`).
|
||||||
|
- `13_CONNECTION_Blocks.md` — связи/подписки (`type=3`).
|
||||||
|
- `14_USER_PARAM_Blocks.md` — пользовательские параметры (`type=4`).
|
||||||
|
|
||||||
|
## Быстрая карта типов
|
||||||
|
|
||||||
|
- `type=0` — TECH: HEADER, CREATE_CHANNEL.
|
||||||
|
- `type=1` — TEXT: POST/EDIT_POST/REPLY/EDIT_REPLY/REPOST.
|
||||||
|
- `type=2` — REACTION: LIKE/UNLIKE.
|
||||||
|
- `type=3` — CONNECTION: FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING и обратные операции.
|
||||||
|
- `type=4` — USER_PARAM: key/value-параметры пользователя.
|
||||||
|
|
||||||
|
## Примечание
|
||||||
|
|
||||||
|
Если нужно добавить новый тип или подтип блока, сначала обновляйте профильный файл этого раздела, затем API-документацию в `Dev_Docs/API`.
|
||||||
40
Dev_Docs/Blockchain/01_Channel_Types_and_CreateChannel.md
Normal file
40
Dev_Docs/Blockchain/01_Channel_Types_and_CreateChannel.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Типы каналов и CreateChannel
|
||||||
|
|
||||||
|
## 1. Формат `CreateChannelBody` (`msg_type=0`, `subType=1`, `version=1`)
|
||||||
|
Payload включает:
|
||||||
|
|
||||||
|
1. line-поля канала (`lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`);
|
||||||
|
2. `channelName`;
|
||||||
|
3. `channelDescription`;
|
||||||
|
4. `channelType` (`uint16`, 2 байта);
|
||||||
|
5. `channelTypeVersion` (`uint16`, 2 байта).
|
||||||
|
|
||||||
|
## 2. Типы каналов
|
||||||
|
- `0` — `stories` (root-канал пользователя).
|
||||||
|
- `1` — публичный канал.
|
||||||
|
- `100` — персональный канал.
|
||||||
|
- `200` — групповой/чатовый канал.
|
||||||
|
|
||||||
|
Версия типа (`channelTypeVersion`) сейчас используется со значением `1`.
|
||||||
|
|
||||||
|
Важно для MVP:
|
||||||
|
- `100` и `200` в формате поддерживаются, но в текущем UI не используются.
|
||||||
|
- В MVP рабочий UI-флоу — каналы `0` и `1`.
|
||||||
|
|
||||||
|
## 3. Имя root-канала
|
||||||
|
- Root-канал (`line_code = 0`) в API/чтении отображается как `stories`.
|
||||||
|
- Публикации в `stories` разрешены владельцу собственного блокчейна.
|
||||||
|
|
||||||
|
## 4. Уникальность имени канала
|
||||||
|
Проверка уникальности выполняется на сервере по ключу:
|
||||||
|
|
||||||
|
`owner_bch_name + channel_type_code + slug(channel_name)`
|
||||||
|
|
||||||
|
Это означает:
|
||||||
|
- одно и то же имя допустимо у одного владельца для разных типов (`1`, `100`, `200`);
|
||||||
|
- в рамках одного типа и одного владельца имя уникально.
|
||||||
|
|
||||||
|
## 5. Персональные каналы (`type=100`)
|
||||||
|
- Сервер не проверяет бизнес-валидность собеседника по имени канала.
|
||||||
|
- Проверка существования login для персонального канала выполняется на UI при создании.
|
||||||
|
- При чтении сервер пытается собрать парный поток `A->B` + `B->A` (если обратный канал существует).
|
||||||
49
Dev_Docs/Blockchain/01_Common_Block_Format.md
Normal file
49
Dev_Docs/Blockchain/01_Common_Block_Format.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Общий формат добавляемого блока (Frame v0)
|
||||||
|
|
||||||
|
Этот файл описывает **единый бинарный формат** блока, который клиент отправляет через `AddBlock` в поле `blockBytesB64`.
|
||||||
|
|
||||||
|
## 1. Полная структура блока
|
||||||
|
|
||||||
|
Блок состоит из двух частей:
|
||||||
|
|
||||||
|
1. **PREIMAGE** (подписывается)
|
||||||
|
2. **TAIL** (маркер подписи + подпись)
|
||||||
|
|
||||||
|
### PREIMAGE
|
||||||
|
|
||||||
|
- `frameCode (uint16)`
|
||||||
|
- `prevHash32 (32 bytes)`
|
||||||
|
- `blockSize (int32)` — размер PREIMAGE
|
||||||
|
- `blockNumber (int32)`
|
||||||
|
- `timestamp (int64)`
|
||||||
|
- `type (uint16)`
|
||||||
|
- `subType (uint16)`
|
||||||
|
- `version (uint16)`
|
||||||
|
- `bodyBytes (N)`
|
||||||
|
|
||||||
|
### TAIL
|
||||||
|
|
||||||
|
- `sigMarker (uint16)`
|
||||||
|
- `signature64 (64 bytes, Ed25519)`
|
||||||
|
|
||||||
|
## 2. Что проверяет сервер при AddBlock
|
||||||
|
|
||||||
|
- `frameCode` должен быть `0x0000`.
|
||||||
|
- `sigMarker` должен быть `0x0100`.
|
||||||
|
- `blockNumber` должен идти строго по порядку (`last + 1`).
|
||||||
|
- `prevHash32` должен совпасть с вершиной цепочки на сервере.
|
||||||
|
- `body` должен пройти `check()` для конкретного типа.
|
||||||
|
- подпись должна валидироваться публичным ключом блокчейна.
|
||||||
|
|
||||||
|
## 3. Ограничения
|
||||||
|
|
||||||
|
- максимальный полный размер блока: до 4 MiB;
|
||||||
|
- timestamp не должен сильно уходить в будущее;
|
||||||
|
- `bodyBytes` парсится по `type/subType/version` из заголовка блока.
|
||||||
|
|
||||||
|
## 4. Почему это важно
|
||||||
|
|
||||||
|
Одинаковый общий формат позволяет:
|
||||||
|
- передавать разные виды записей через один RPC `AddBlock`;
|
||||||
|
- валидировать блоки единообразно;
|
||||||
|
- расширять типы `body`, не ломая каркас блока.
|
||||||
47
Dev_Docs/Blockchain/02_Blockchain_Kinds_and_Lines.md
Normal file
47
Dev_Docs/Blockchain/02_Blockchain_Kinds_and_Lines.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Виды блокчейнов и логических линий
|
||||||
|
|
||||||
|
## 1. Именованный блокчейн
|
||||||
|
|
||||||
|
Базовый идентификатор цепочки пользователя:
|
||||||
|
|
||||||
|
- `blockchainName = <login>-<NNN>`
|
||||||
|
- пример: `alice-001`
|
||||||
|
|
||||||
|
Обычно это одна основная цепочка пользователя.
|
||||||
|
|
||||||
|
## 2. Логические линии внутри одной цепочки
|
||||||
|
|
||||||
|
Физически цепочка одна, но внутри есть независимые логические последовательности (линии), которые ведутся через поля:
|
||||||
|
|
||||||
|
- `lineCode`
|
||||||
|
- `prevLineNumber`
|
||||||
|
- `prevLineHash32`
|
||||||
|
- `thisLineNumber`
|
||||||
|
|
||||||
|
Линии используются для:
|
||||||
|
- TECH-событий;
|
||||||
|
- каналов с текстовыми постами;
|
||||||
|
- связей и подписок;
|
||||||
|
- пользовательских параметров.
|
||||||
|
|
||||||
|
## 3. Правила line-полей (фактическая серверная валидация)
|
||||||
|
|
||||||
|
Line-поля: `lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`.
|
||||||
|
|
||||||
|
- Line-поля разрешены только для `msg_type`: `0`, `1`, `3`, `4`.
|
||||||
|
- Если передано хотя бы одно line-поле, должны быть переданы все 4.
|
||||||
|
- `prevLineNumber/prevLineHash32` должны указывать на существующий блок этой же цепочки.
|
||||||
|
- Для первого шага после root (`prevLineNumber == lineCode`):
|
||||||
|
- `TEXT (msg_type=1)`: `thisLineNumber = 0`;
|
||||||
|
- `TECH/CONNECTION/USER_PARAM (0/3/4)`: `thisLineNumber = 1`.
|
||||||
|
- Для обычного шага:
|
||||||
|
- `TEXT`: `thisLineNumber` допускает `same` или `+1` от предыдущего блока линии;
|
||||||
|
- `TECH/CONNECTION/USER_PARAM`: строго `+1`.
|
||||||
|
|
||||||
|
## 4. Root-идея для каналов и подписок
|
||||||
|
|
||||||
|
Для ссылок вида follow/friend/contact принято ссылаться на корневые блоки:
|
||||||
|
- `HEADER` для базовой сущности пользователя/канала `0`;
|
||||||
|
- `CREATE_CHANNEL` для пользовательских каналов.
|
||||||
|
|
||||||
|
Так ссылки остаются стабильными, даже когда в канале появляются новые сообщения.
|
||||||
33
Dev_Docs/Blockchain/02_Channel_Commands.md
Normal file
33
Dev_Docs/Blockchain/02_Channel_Commands.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Командные сообщения каналов
|
||||||
|
|
||||||
|
## 1. Общий префикс
|
||||||
|
Командные сообщения распознаются по префиксу:
|
||||||
|
|
||||||
|
`/.`
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
- `/.desc Новый комментарий канала`
|
||||||
|
|
||||||
|
## 2. Поддерживаемые команды
|
||||||
|
|
||||||
|
### Для всех типов каналов (`0`, `1`, `100`, `200`)
|
||||||
|
- `/.desc <text>` — смена описания канала.
|
||||||
|
|
||||||
|
Примечание:
|
||||||
|
- Описание канала в чтении определяется последней командой `/.desc` в линии канала.
|
||||||
|
- Если `/.desc` не было, используется описание из `CreateChannel`.
|
||||||
|
|
||||||
|
### Дополнительно для `type=200`
|
||||||
|
- `/.add <login> <channelName>`
|
||||||
|
- `/.remove <login> <channelName>`
|
||||||
|
|
||||||
|
Формат аргументов фиксирован: через пробел.
|
||||||
|
|
||||||
|
## 3. Текущая модель применения
|
||||||
|
- Команды передаются как обычные `TEXT_POST` сообщения.
|
||||||
|
- Сервер уже применяет `/.desc` при вычислении актуального описания канала.
|
||||||
|
- Команды `/.add` и `/.remove` зарезервированы под расширенную модель участников `type=200` на уровне UI/агрегации.
|
||||||
|
|
||||||
|
## 4. Статус для MVP
|
||||||
|
- В текущем UI каналы `type=100` и `type=200` не используются.
|
||||||
|
- Соответственно, `/.add` и `/.remove` считаются запланированными и пока не участвуют в рабочем UI-сценарии.
|
||||||
18
Dev_Docs/Blockchain/10_TECH_Blocks.md
Normal file
18
Dev_Docs/Blockchain/10_TECH_Blocks.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# TECH блоки (`type=0`, `version=1`)
|
||||||
|
|
||||||
|
TECH-тип покрывает системные записи цепочки.
|
||||||
|
|
||||||
|
## Подтипы
|
||||||
|
|
||||||
|
1. `subType=0` — `HEADER_COMPAT`
|
||||||
|
- стартовый блок цепочки;
|
||||||
|
- payload: tag `SHiNE` + login владельца.
|
||||||
|
|
||||||
|
2. `subType=1` — `TECH_CREATE_CHANNEL`
|
||||||
|
- создание нового канала;
|
||||||
|
- хранит line-поля + `channelName` + `channelDescription` + `channelType` + `channelTypeVersion`.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
- инициализация блокчейна;
|
||||||
|
- управление набором каналов пользователя.
|
||||||
38
Dev_Docs/Blockchain/11_TEXT_Blocks.md
Normal file
38
Dev_Docs/Blockchain/11_TEXT_Blocks.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# TEXT блоки (`type=1`, `version=1`)
|
||||||
|
|
||||||
|
TEXT-тип хранит сообщения и редактирования.
|
||||||
|
|
||||||
|
## Подтипы
|
||||||
|
|
||||||
|
1. `subType=10` — `TEXT_POST`
|
||||||
|
- пост в линии канала;
|
||||||
|
- содержит line-поля + текст.
|
||||||
|
|
||||||
|
2. `subType=11` — `TEXT_EDIT_POST`
|
||||||
|
- редактирование поста;
|
||||||
|
- line-поля + target на оригинальный POST + новый текст.
|
||||||
|
|
||||||
|
3. `subType=20` — `TEXT_REPLY`
|
||||||
|
- ответ на сообщение;
|
||||||
|
- target (`toBlockchainName`, `toBlockGlobalNumber`, `toBlockHash32`) + текст.
|
||||||
|
|
||||||
|
4. `subType=21` — `TEXT_EDIT_REPLY`
|
||||||
|
- редактирование ответа;
|
||||||
|
- target на исходный REPLY + новый текст.
|
||||||
|
- допускается пустой `text` для логического удаления сообщения (без физического удаления блока).
|
||||||
|
|
||||||
|
5. `subType=30` — `TEXT_REPOST`
|
||||||
|
- репост сообщения в линию канала;
|
||||||
|
- содержит line-поля + target на оригинальное сообщение + текст комментария;
|
||||||
|
- на текущем этапе продуктовой логики репост не редактируется (версии не накапливаются);
|
||||||
|
- временно отключён для записи через `AddBlock` до будущей реализации репостов.
|
||||||
|
|
||||||
|
## Правило для edit
|
||||||
|
|
||||||
|
`EDIT_POST` и `EDIT_REPLY` должны ссылаться на **оригинальный** блок, а не на предыдущий edit.
|
||||||
|
|
||||||
|
## Пустой text в edit
|
||||||
|
|
||||||
|
- Для `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` допустим `textLen=0`.
|
||||||
|
- Такой edit трактуется как логическое удаление содержимого сообщения.
|
||||||
|
- Для удаления используется именно edit-блок; отдельного `DELETE`-подтипа нет.
|
||||||
14
Dev_Docs/Blockchain/12_REACTION_Blocks.md
Normal file
14
Dev_Docs/Blockchain/12_REACTION_Blocks.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# REACTION блоки (`type=2`, `version=1`)
|
||||||
|
|
||||||
|
## Подтипы
|
||||||
|
|
||||||
|
1. `subType=1` — `REACTION_LIKE`
|
||||||
|
- лайк на целевой блок;
|
||||||
|
- хранит target: `toBlockchainName`, `toBlockGlobalNumber`, `toBlockHash32`.
|
||||||
|
2. `subType=2` — `REACTION_UNLIKE`
|
||||||
|
- снятие лайка с целевого блока;
|
||||||
|
- хранит target: `toBlockchainName`, `toBlockGlobalNumber`, `toBlockHash32`.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
- реакция на текстовые сообщения (и потенциально другие target-блоки, если это разрешено бизнес-логикой).
|
||||||
29
Dev_Docs/Blockchain/13_CONNECTION_Blocks.md
Normal file
29
Dev_Docs/Blockchain/13_CONNECTION_Blocks.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# CONNECTION блоки (`type=3`, `version=1`)
|
||||||
|
|
||||||
|
CONNECTION-тип описывает социальные связи и подписки.
|
||||||
|
|
||||||
|
## Подтипы
|
||||||
|
|
||||||
|
• `10/11` — `close_friend / unclose_friend` (близкий друг)
|
||||||
|
• `20/21` — `contact / uncontact` (контакт)
|
||||||
|
• `30/31` — `follow / unfollow` (подписан)
|
||||||
|
• `40/41` — `spouse / unspouse` (супруг/супруга)
|
||||||
|
• `50/51` — `parent / unparent` (родитель)
|
||||||
|
• `52/53` — `child / unchild` (ребёнок)
|
||||||
|
• `54/55` — `sibling / unsibling` (брат/сестра)
|
||||||
|
• `60/61` — `known_person / unknown_person` (знаю этого человека)
|
||||||
|
• `70/71` — `shine_confirmed / shine_unconfirmed` (точно уверен, что сияющий)
|
||||||
|
• `74/75` — `shine_seen / shine_unseen` (мало знаком, но видел сияющим)
|
||||||
|
|
||||||
|
## Общий формат payload
|
||||||
|
|
||||||
|
- line-поля (`lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`)
|
||||||
|
- target (`toBlockchainName`, `toBlockGlobalNumber`, `toBlockHash32`)
|
||||||
|
|
||||||
|
## Правила target
|
||||||
|
|
||||||
|
- FRIEND/CONTACT обычно указывают на `HEADER` цели (`block 0`).
|
||||||
|
- FOLLOW указывает на root канала:
|
||||||
|
- `HEADER` для канала `0`;
|
||||||
|
- `CREATE_CHANNEL` для пользовательского канала.
|
||||||
|
- Для остальных типов связи (`SPOUSE/PARENT/CHILD/SIBLING`) используется тот же target-формат.
|
||||||
14
Dev_Docs/Blockchain/14_USER_PARAM_Blocks.md
Normal file
14
Dev_Docs/Blockchain/14_USER_PARAM_Blocks.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# USER_PARAM блоки (`type=4`, `version=1`)
|
||||||
|
|
||||||
|
## Подтипы
|
||||||
|
|
||||||
|
1. `subType=1` — `USER_PARAM_TEXT_TEXT`
|
||||||
|
- хранит line-поля + `paramKey` + `paramValue`.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
- сохранение пользовательского состояния (настройки клиента, синк-метки, курсоры чтения и т.д.).
|
||||||
|
|
||||||
|
## Практика
|
||||||
|
|
||||||
|
Для сложных структур удобно хранить JSON-строку в `paramValue` с версией схемы.
|
||||||
64
Dev_Docs/Blockchain/CHANGELOG.md
Normal file
64
Dev_Docs/Blockchain/CHANGELOG.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# История изменений документации блокчейна
|
||||||
|
|
||||||
|
# 2026-06-26 17:45:18 +0400
|
||||||
|
- Базовый коммит-ориентир: `44a1ba0`.
|
||||||
|
- На `t.shineup.me` подтверждена рабочая схема startup sync и full-resync:
|
||||||
|
- после рестарта сервер добивает `BlockchainTmpRecovery` и `BlockchainResyncRecovery`;
|
||||||
|
- `aidartest-001` успешно подтягивается с `shineup.me`;
|
||||||
|
- итоговое локальное состояние по `aidartest-001` дошло до `last_block_number=13`.
|
||||||
|
- В `Dev_Docs/Blockchain/sync-between-servers.md` добавлен практический результат ручной проверки на тестовом сервере.
|
||||||
|
|
||||||
|
## 2026-06-26 17:03:22 +0400
|
||||||
|
- Базовый коммит-ориентир: `71fdee0`.
|
||||||
|
- Обычный `AddBlock` переведён на crash-safe схему через временный кандидат `<blockchainName>.tmp_bch`, sidecar `<blockchainName>.write_check` и marker `<blockchainName>.write_pending`.
|
||||||
|
- `BlockchainTmpRecoveryOnStartup` теперь разбирает marker-driven recovery для обычной записи блока:
|
||||||
|
- если marker есть, recovery либо завершает swap tmp -> main, либо удаляет мусор;
|
||||||
|
- если marker нет, временные артефакты считаются мусором и удаляются.
|
||||||
|
- В `Dev_Docs/Blockchain/sync-between-servers.md` добавлено описание обычного `AddBlock` recovery и разделение между `write_pending` и `resync_pending`.
|
||||||
|
|
||||||
|
## 2026-05-24 11:40:00 +0300
|
||||||
|
- Базовый коммит-ориентир: `abdce05`.
|
||||||
|
- `TEXT_REPOST (subType=30)` оставлен как зарезервированный формат, но новые блоки репоста временно отключены на уровне `AddBlock`.
|
||||||
|
- В `11_TEXT_Blocks.md` зафиксировано, что запись `TEXT_REPOST` временно не используется до будущей реализации.
|
||||||
|
- В `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md` добавлен код отказа `repost_disabled`.
|
||||||
|
|
||||||
|
## 2026-05-21 19:05:00 +0300
|
||||||
|
- Базовый коммит-ориентир: `5344c42`.
|
||||||
|
- Добавлен новый TEXT-подтип `TEXT_REPOST (subType=30)`:
|
||||||
|
- обновлён перечень типов в `11_TEXT_Blocks.md`;
|
||||||
|
- обновлена быстрая карта типов в `00_Blockchain_Formats_and_Block_Types.md`.
|
||||||
|
- Уточнено API-описание поддержанных подтипов в `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md`.
|
||||||
|
- В документе `Dev_Docs/API/08_MCP_Чтение_и_дозапись_персонального_публичного_чата.md` зафиксировано, что чтение канала учитывает `TEXT_POST` и `TEXT_REPOST`.
|
||||||
|
|
||||||
|
## 2026-05-20 11:34:17 +0300
|
||||||
|
- Базовый коммит-ориентир: `a53444b`.
|
||||||
|
- В `13_CONNECTION_Blocks.md` добавлены новые CONNECTION подтипы:
|
||||||
|
- `60/61` — `known_person / unknown_person` (знаю этого человека);
|
||||||
|
- `70/71` — `shine_confirmed / shine_unconfirmed` (точно уверен, что сияющий);
|
||||||
|
- `74/75` — `shine_seen / shine_unseen` (мало знаком, но видел сияющим).
|
||||||
|
- Обновлён список CONNECTION-подтипов в `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md`.
|
||||||
|
|
||||||
|
## 2026-05-19 20:30:21 +0300
|
||||||
|
- Базовый коммит-ориентир: `7986184`.
|
||||||
|
- Уточнён документ `11_TEXT_Blocks.md`: для `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` зафиксировано, что `textLen=0` допустим и трактуется как логическое удаление сообщения.
|
||||||
|
- Явно закреплено, что отдельного `DELETE`-подтипа нет, удаление выполняется edit-блоком.
|
||||||
|
|
||||||
|
## 2026-05-19 00:22:46 +0300
|
||||||
|
- Базовый коммит-ориентир: `c27da63a3e65`.
|
||||||
|
- Актуализирован `README.md` как точка входа для MVP-документации по протоколу.
|
||||||
|
- В документации явно зафиксировано, что `channelType=100` и `channelType=200` присутствуют в формате, но пока не используются в UI.
|
||||||
|
- Актуализирован перечень REACTION-подтипов: добавлен `REACTION_UNLIKE (subType=2)`.
|
||||||
|
- Актуализирован перечень CONNECTION-подтипов: добавлены `SPOUSE/PARENT/CHILD/SIBLING` и обратные операции.
|
||||||
|
- В документ `02_Blockchain_Kinds_and_Lines.md` добавлены фактические серверные правила валидации line-полей.
|
||||||
|
- Обновлён корневой `AGENTS.md`: формат блокчейна менять только после явного подтверждения пользователя и с предварительным предупреждением.
|
||||||
|
|
||||||
|
## 2026-05-13 00:02:32 +0300
|
||||||
|
- Базовый коммит-ориентир: `f63f40f1eb2f`.
|
||||||
|
- Добавлен текущий формат `CreateChannelBody` с полями `channelType (2 байта)` и `channelTypeVersion (2 байта)`.
|
||||||
|
- Зафиксированы типы каналов: `0=stories`, `1=public`, `100=personal`, `200=group`.
|
||||||
|
- Серверная уникальность имени канала изменена на `owner + type + name(slug)`.
|
||||||
|
- Root-канал `0` переименован в `stories` на уровне API-чтения.
|
||||||
|
- Для персонального канала (`type=100`) включена сборка парного потока при чтении (`A->B` + `B->A`, если существует).
|
||||||
|
- Добавлена поддержка командного префикса `/.` и команды `/.desc` для актуализации описания канала при чтении.
|
||||||
|
- Зафиксированы команды `/.add` и `/.remove` для каналов `type=200` (зарезервировано под расширение участниками).
|
||||||
|
- В `AGENTS.md` добавлено обязательное правило актуализации документации в `Dev_Docs/Blockchain/`.
|
||||||
34
Dev_Docs/Blockchain/README.md
Normal file
34
Dev_Docs/Blockchain/README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Документация блокчейна SHiNE (MVP)
|
||||||
|
|
||||||
|
Этот каталог описывает только текущий рабочий формат протокола для MVP.
|
||||||
|
|
||||||
|
## Основные документы
|
||||||
|
1. [01_Common_Block_Format.md](./01_Common_Block_Format.md)
|
||||||
|
Единый бинарный формат блока (Frame v0), подпись, базовые проверки.
|
||||||
|
2. [02_Blockchain_Kinds_and_Lines.md](./02_Blockchain_Kinds_and_Lines.md)
|
||||||
|
Виды цепочек и правила line-полей.
|
||||||
|
3. [10_TECH_Blocks.md](./10_TECH_Blocks.md)
|
||||||
|
Системные блоки (`msg_type=0`).
|
||||||
|
4. [11_TEXT_Blocks.md](./11_TEXT_Blocks.md)
|
||||||
|
Текстовые блоки (`msg_type=1`).
|
||||||
|
5. [12_REACTION_Blocks.md](./12_REACTION_Blocks.md)
|
||||||
|
Реакции (`msg_type=2`).
|
||||||
|
6. [13_CONNECTION_Blocks.md](./13_CONNECTION_Blocks.md)
|
||||||
|
Социальные связи (`msg_type=3`).
|
||||||
|
7. [14_USER_PARAM_Blocks.md](./14_USER_PARAM_Blocks.md)
|
||||||
|
Параметры пользователя (`msg_type=4`).
|
||||||
|
8. [01_Channel_Types_and_CreateChannel.md](./01_Channel_Types_and_CreateChannel.md)
|
||||||
|
Типы каналов и формат `CreateChannelBody`.
|
||||||
|
9. [02_Channel_Commands.md](./02_Channel_Commands.md)
|
||||||
|
Команды в текстовых сообщениях каналов.
|
||||||
|
10. [CHANGELOG.md](./CHANGELOG.md)
|
||||||
|
Журнал изменений документации.
|
||||||
|
|
||||||
|
## Важные ограничения MVP
|
||||||
|
- Каналы `type=100` и `type=200` присутствуют в формате, но сейчас не используются в UI.
|
||||||
|
- Поддерживаемый рабочий сценарий UI на текущем этапе: `stories (type=0)` и `public (type=1)`.
|
||||||
|
|
||||||
|
## Обязательное сопровождение
|
||||||
|
- При любом изменении формата/правил блокчейна в коде документы этого каталога обновляются в том же наборе изменений.
|
||||||
|
- Обычный `AddBlock` сейчас пишет через `<blockchainName>.tmp_bch`, `<blockchainName>.write_check` и `<blockchainName>.write_pending`; эта схема и `BlockchainTmpRecoveryOnStartup` должны быть описаны в актуальной документации по синхронизации и recovery.
|
||||||
|
- Каждое обновление документов фиксируется в `CHANGELOG.md` с датой/временем и хэшем коммита-основания.
|
||||||
256
Dev_Docs/Blockchain/sync-between-servers.md
Normal file
256
Dev_Docs/Blockchain/sync-between-servers.md
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
# Синхронизация блоков и DM между серверами SHiNE
|
||||||
|
|
||||||
|
Документ описывает архитектуру и протокол синхронизации данных между партнёрскими серверами SHiNE.
|
||||||
|
|
||||||
|
## 1. Зачем нужна синхронизация
|
||||||
|
|
||||||
|
Пользователи SHiNE могут быть «приписаны» к разным серверам.
|
||||||
|
Когда пользователь A (на сервере X) пишет пользователю B (на сервере Y):
|
||||||
|
|
||||||
|
1. Сервер X принимает сообщение;
|
||||||
|
2. Сервер X должен переслать DM-блок серверу Y;
|
||||||
|
3. Сервер Y сохраняет блок и доставляет в активные сессии пользователя B.
|
||||||
|
|
||||||
|
Аналогично, блоки пользовательского блокчейна (записи `AddBlock`) должны синхронизироваться,
|
||||||
|
чтобы любой партнёрский сервер мог отдать полную историю пользователя.
|
||||||
|
|
||||||
|
## 2. Список серверов синхронизации (`sync_servers`)
|
||||||
|
|
||||||
|
Каждый сервер регистрирует в своей Solana PDA список `sync_servers` —
|
||||||
|
логины SHiNE-аккаунтов партнёрских серверов, с которыми он синхронизируется.
|
||||||
|
|
||||||
|
- Список хранится в блоке `ServerProfileBlock` внутри `user_pda` сервера.
|
||||||
|
- Адрес каждого партнёрского сервера читается из его PDA на Solana.
|
||||||
|
- Синхронизация двусторонняя: оба сервера должны иметь друг друга в `sync_servers`.
|
||||||
|
|
||||||
|
## 3. Что синхронизируется
|
||||||
|
|
||||||
|
### 3.1 Личные сообщения (DM)
|
||||||
|
|
||||||
|
- Все DM-блоки форматов типов `1/2` (текст) и `3/4` (read-receipt).
|
||||||
|
- Сервер-отправитель: при получении пары блоков от клиента перенаправляет их серверу получателя.
|
||||||
|
- Сервер-получатель: сохраняет блоки в `signed_messages_v2`, доставляет в активные сессии.
|
||||||
|
- Дедупликация по уникальному `message_key = from|to|timeMs|nonce|type`.
|
||||||
|
|
||||||
|
### 3.2 Блоки пользовательского блокчейна
|
||||||
|
|
||||||
|
- Все блоки `AddBlock` пользователей, зарегистрированных на сервере или синхронизирующихся через него.
|
||||||
|
- Синхронизируются в обе стороны между всеми партнёрами из `sync_servers`.
|
||||||
|
- Порядок блоков сохраняется (по глобальному номеру блока и хэшу).
|
||||||
|
- Дедупликация по глобальному номеру блока и хэшу.
|
||||||
|
|
||||||
|
## 4. Текущая реализованная схема
|
||||||
|
|
||||||
|
На текущем этапе сервер уже умеет базовую межсерверную синхронизацию пользовательских блокчейнов.
|
||||||
|
|
||||||
|
### 4.1 Что уже сделано
|
||||||
|
|
||||||
|
1. При старте сервер читает свой `server.SHiNE.login`.
|
||||||
|
2. По этому логину он загружает из Solana свою server PDA.
|
||||||
|
3. Из неё вытаскивает список `sync_servers`.
|
||||||
|
4. Для каждого логина партнёра сервер читает его PDA и сохраняет локально:
|
||||||
|
- `login`
|
||||||
|
- `server_address`
|
||||||
|
5. После этого:
|
||||||
|
- новые локальные `AddBlock` рассылаются партнёрам в фоне;
|
||||||
|
- при старте запускается periodic sync;
|
||||||
|
- periodic sync повторяется каждые `12` часов после старта.
|
||||||
|
|
||||||
|
### 4.2 Какие server-to-server API уже используются
|
||||||
|
|
||||||
|
- `ListBlockchainHeads` — список heads всех локальных цепочек партнёра;
|
||||||
|
- `GetBlockchainBlock` — чтение одного конкретного блока партнёра;
|
||||||
|
- `GetSyncUserProfile` — минимальный профиль пользователя для локального создания `solana_users + blockchain_state` без обращения в Solana RPC.
|
||||||
|
|
||||||
|
### 4.3 Как сейчас работает periodic sync
|
||||||
|
|
||||||
|
Для каждого сервера из локальной таблицы `sync_servers`:
|
||||||
|
|
||||||
|
1. запрашивается `ListBlockchainHeads`;
|
||||||
|
2. для каждой удалённой цепочки сравниваются:
|
||||||
|
- `lastBlockNumber`
|
||||||
|
- `lastBlockHash`
|
||||||
|
- локальное состояние;
|
||||||
|
3. если локальная цепочка слабее, сервер по одному блоку вызывает `GetBlockchainBlock`;
|
||||||
|
4. каждый скачанный блок локально применяется через существующий `AddBlock`;
|
||||||
|
5. если у сервера ещё нет локальной записи пользователя/цепочки, перед этим подготавливается локальный `solana_users + blockchain_state`.
|
||||||
|
6. если во время replay обнаруживается рассинхрон или на одинаковой высоте удалённая цепочка сильнее, запускается полный resync:
|
||||||
|
- цепочка помечается in-memory как `resync in progress`;
|
||||||
|
- создаётся marker-file в `data/`;
|
||||||
|
- в одной SQL-транзакции очищаются локальные данные цепочки и корректируются чужие счётчики;
|
||||||
|
- удаляются `.bch` и `.tmp_bch`;
|
||||||
|
- цепочка подтягивается заново с `0` через `GetBlockchainBlock`.
|
||||||
|
- обычный `AddBlock` на эту цепочку в этот момент возвращает `chain_resync_in_progress`.
|
||||||
|
|
||||||
|
### 4.4 Как именно работает full resync
|
||||||
|
|
||||||
|
Full resync запускается только тогда, когда:
|
||||||
|
|
||||||
|
- локальная chain отстаёт и обычная докачка хвоста упирается в `bad_prev_hash` или `bad_block_number`;
|
||||||
|
- либо высота цепочек одинаковая, но удалённая версия сильнее по правилу:
|
||||||
|
- `lastBlockNumber`;
|
||||||
|
- `fileSizeBytes`;
|
||||||
|
- `lastBlockHash`.
|
||||||
|
|
||||||
|
Порядок действий:
|
||||||
|
|
||||||
|
1. Ставится in-memory guard на `blockchainName`.
|
||||||
|
2. Создаётся marker-file `<blockchainName>.resync_pending`.
|
||||||
|
3. Обычный `AddBlock` на эту chain временно получает `chain_resync_in_progress`.
|
||||||
|
4. Вызывается атомарный SQL cleanup одной chain:
|
||||||
|
- уменьшаются чужие `likes_count` и `replies_count`;
|
||||||
|
- удаляются локальные derived-state записи этой chain;
|
||||||
|
- удаляются `blocks` и `blockchain_state` этой chain.
|
||||||
|
5. Удаляются файлы `<blockchainName>.bch` и `<blockchainName>.tmp_bch`.
|
||||||
|
6. Локальная chain создаётся заново через `GetSyncUserProfile` или через Solana import, если `sync.importUserProfileFromPartner.enabled=false`.
|
||||||
|
7. Chain replay-ится с `0` через `GetBlockchainBlock`.
|
||||||
|
8. Если всё прошло успешно, marker-file удаляется.
|
||||||
|
9. Если на любом шаге произошёл сбой, marker-file остаётся на диске, и сервер добивает эту chain при следующем старте.
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
- full resync не делает умный rollback по одному блоку;
|
||||||
|
- full resync не трогает DM-таблицы и `solana_users`;
|
||||||
|
- висячие cross-chain ссылки считаются допустимым поведением системы.
|
||||||
|
|
||||||
|
### 4.5 Как работает обычный `AddBlock` и его recovery
|
||||||
|
|
||||||
|
Обычная запись блока теперь тоже идёт через временные артефакты:
|
||||||
|
|
||||||
|
1. собирается `<blockchainName>.tmp_bch` как полный кандидат на замену основного файла;
|
||||||
|
2. пишется маленький sidecar `<blockchainName>.write_check` с `blockNumber` и `blockHash`;
|
||||||
|
3. только после этого создаётся пустой marker `<blockchainName>.write_pending`;
|
||||||
|
4. выполняется SQL-транзакция;
|
||||||
|
5. после `commit` tmp атомарно ставится на место основного `.bch`;
|
||||||
|
6. marker и sidecar удаляются.
|
||||||
|
|
||||||
|
На старте `BlockchainTmpRecoveryOnStartup` смотрит именно на эту пару:
|
||||||
|
|
||||||
|
- если `write_pending` есть, recovery проверяет sidecar и БД, а затем либо завершает swap, либо чистит временные файлы;
|
||||||
|
- если `write_pending` нет, а `tmp_bch` или `write_check` остались, это мусор и он удаляется;
|
||||||
|
- `resync_pending` сюда не относится, это отдельный recovery-поток.
|
||||||
|
|
||||||
|
### 4.6 Startup recovery по marker-file
|
||||||
|
|
||||||
|
При старте сервер идёт в таком порядке:
|
||||||
|
|
||||||
|
1. `BlockchainTmpRecoveryOnStartup` для `*.write_pending` и orphan `*.tmp_bch` / `*.write_check`;
|
||||||
|
2. `BlockchainResyncRecoveryOnStartup` для `*.resync_pending`;
|
||||||
|
3. только потом поднимается обычный сервер и запускается `PeriodicBlockchainSyncService`.
|
||||||
|
|
||||||
|
Если marker-file существует:
|
||||||
|
|
||||||
|
- сервер не должен начинать обычную работу поверх этой chain;
|
||||||
|
- recovery снова выполняет cleanup и replay с нуля;
|
||||||
|
- если recovery не завершился, marker остаётся, и сервер не переходит к обычному режиму для этой chain.
|
||||||
|
|
||||||
|
### 4.7 Зачем понадобился `GetSyncUserProfile`
|
||||||
|
|
||||||
|
Изначально подготовка локальной цепочки делалась через Solana:
|
||||||
|
|
||||||
|
- из `blockchainName` извлекался `login`;
|
||||||
|
- сервер вызывал import пользователя из Solana PDA;
|
||||||
|
- по данным PDA локально создавались `solana_users + blockchain_state`.
|
||||||
|
|
||||||
|
На практике это упёрлось в ограничение внешнего Solana RPC: при чистом старте и массовой подтяжке чужих цепочек сервер мог получать `HTTP 429`.
|
||||||
|
|
||||||
|
Поэтому добавлен отдельный обходной режим:
|
||||||
|
|
||||||
|
- настройка `sync.importUserProfileFromPartner.enabled=true`
|
||||||
|
- в этом режиме сервер **не ходит в Solana RPC** для создания локальной цепочки во время sync;
|
||||||
|
- вместо этого он запрашивает у сервера-партнёра `GetSyncUserProfile` и создаёт локальную запись по данным партнёра.
|
||||||
|
- если локальный `solana_users` уже существует, sync восстанавливает только `blockchain_state` и не трогает identity-слой.
|
||||||
|
|
||||||
|
Это временная практическая заплатка, чтобы clean-start sync не зависел от rate limit внешнего Solana endpoint.
|
||||||
|
|
||||||
|
### 4.8 Что делает настройка `sync.importUserProfileFromPartner.enabled`
|
||||||
|
|
||||||
|
- `false` — стандартный режим, подготовка локального пользователя идёт через Solana PDA;
|
||||||
|
- `true` — sync-режим обхода Solana, локальный пользователь создаётся по server-to-server `GetSyncUserProfile`.
|
||||||
|
|
||||||
|
Настройка влияет именно на этап подготовки отсутствующей локальной цепочки во время periodic sync.
|
||||||
|
|
||||||
|
## 5. Целевой протокол следующего этапа
|
||||||
|
|
||||||
|
### 5.1 Межсерверное соединение
|
||||||
|
|
||||||
|
- Серверы устанавливают постоянное WebSocket-соединение друг с другом.
|
||||||
|
- Адрес партнёра определяется по `server_address` из его Solana PDA.
|
||||||
|
- Аутентификация: подпись Ed25519 корневым ключом сервера (`root_key` из PDA).
|
||||||
|
- При разрыве — переподключение с экспоненциальным backoff.
|
||||||
|
|
||||||
|
### 5.2 Доставка новых данных (push)
|
||||||
|
|
||||||
|
- При получении нового блока или DM сервер немедленно пушит его всем подключённым партнёрам.
|
||||||
|
- Партнёр подтверждает приём (ACK). Без ACK — повтор с backoff.
|
||||||
|
|
||||||
|
### 5.3 Начальная синхронизация (backfill)
|
||||||
|
|
||||||
|
- При первом подключении к партнёру серверы обмениваются «курсорами» состояния:
|
||||||
|
последний глобальный номер блока, последний известный DM-ключ.
|
||||||
|
- Сервер с более полной историей досылает недостающее партнёру.
|
||||||
|
|
||||||
|
### 5.4 Разрешение конфликтов
|
||||||
|
|
||||||
|
- Блоки пользовательского блокчейна: порядок определяется глобальным номером блока.
|
||||||
|
Конфликтующие ветки (fork) разрешаются по правилам `AddBlock` (см. `Dev_Docs/Blockchain/README.md`).
|
||||||
|
- DM: конфликтов нет, `message_key` уникален.
|
||||||
|
|
||||||
|
## 6. Маршрутизация DM между серверами
|
||||||
|
|
||||||
|
При отправке DM от пользователя A к пользователю B:
|
||||||
|
|
||||||
|
1. Клиент A отправляет пару блоков на свой сервер X.
|
||||||
|
2. Сервер X определяет, на каком сервере зарегистрирован пользователь B.
|
||||||
|
- Сначала проверяет локально (если B зарегистрирован на X).
|
||||||
|
- Иначе читает PDA пользователя B из Solana и смотрит `access_servers`.
|
||||||
|
- Выбирает первый доступный сервер из `access_servers` и перенаправляет туда DM.
|
||||||
|
3. Сервер Y (из `access_servers` B) сохраняет и доставляет блоки.
|
||||||
|
|
||||||
|
Кэш адресов серверов: обновляется раз в сессию (при ошибке соединения).
|
||||||
|
|
||||||
|
## 7. Безопасность
|
||||||
|
|
||||||
|
- Все блоки подписаны ключами пользователя на клиенте — сервер не может подделать содержимое.
|
||||||
|
- Серверы не расшифровывают DM-контент (шифрование — задача следующего этапа).
|
||||||
|
- При синхронизации каждый блок проходит валидацию подписи на принимающем сервере.
|
||||||
|
|
||||||
|
## 8. Статус реализации
|
||||||
|
|
||||||
|
| Компонент | Статус |
|
||||||
|
|-----------|--------|
|
||||||
|
| Регистрация серверной PDA в Solana | ✅ Реализовано |
|
||||||
|
| Чтение `sync_servers` из PDA | ✅ Реализовано |
|
||||||
|
| Локальная таблица `sync_servers` | ✅ Реализовано |
|
||||||
|
| Публичный `ListBlockchainHeads` | ✅ Реализовано |
|
||||||
|
| Публичный `GetBlockchainBlock` | ✅ Реализовано |
|
||||||
|
| Публичный `GetSyncUserProfile` | ✅ Реализовано |
|
||||||
|
| Плановый blockchain sync при старте + каждые 12 часов | ✅ Реализовано |
|
||||||
|
| Обход Solana RPC через `sync.importUserProfileFromPartner.enabled` | ✅ Реализовано |
|
||||||
|
| Обычный `AddBlock` через `tmp_bch`/`write_check`/`write_pending` | ✅ Реализовано |
|
||||||
|
| Межсерверный постоянный WebSocket-канал | Нужна реализация |
|
||||||
|
| Push новых DM партнёрам | Нужна реализация |
|
||||||
|
| Push блоков блокчейна партнёрам | ✅ Реализована базовая one-shot версия |
|
||||||
|
| Periodic backfill отсутствующего хвоста | ✅ Реализовано |
|
||||||
|
| Разрешение рассинхрона / divergence | ✅ Реализована базовая full-resync схема во время periodic sync |
|
||||||
|
| Startup recovery по `*.resync_pending` marker-file | ✅ Реализовано |
|
||||||
|
| Маршрутизация DM через access_servers | Нужна реализация (заглушка) |
|
||||||
|
|
||||||
|
Текущая версия сервера уже умеет базовую синхронизацию блокчейнов между партнёрами.
|
||||||
|
Не реализованы ещё DM-sync и постоянные server-to-server соединения.
|
||||||
|
|
||||||
|
Следующие отдельные шаги после текущего этапа:
|
||||||
|
- отдельно проверить full-resync и startup-recovery на реальном тестовом прогоне после ручного удаления БД/файлов.
|
||||||
|
|
||||||
|
### 8.1 Практическая проверка на тестовом сервере
|
||||||
|
|
||||||
|
Проверка на `t.shineup.me` показала, что текущая схема действительно поднимает цепочку при старте:
|
||||||
|
|
||||||
|
- после рестарта сервер сначала проходит `BlockchainTmpRecovery`;
|
||||||
|
- затем обрабатывает `BlockchainResyncRecovery`;
|
||||||
|
- после этого сам догружает цепочку `aidartest-001` с `shineup.me`;
|
||||||
|
- итоговое состояние на тестовом сервере:
|
||||||
|
- `blockchain_state.last_block_number = 13`
|
||||||
|
- `blocks` по `aidartest-001` = `14` записей
|
||||||
|
|
||||||
|
Это подтверждает, что startup sync и full-resync flow работают в живом сценарии, а не только в коде.
|
||||||
33
Dev_Docs/Figma/README.md
Normal file
33
Dev_Docs/Figma/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Figma
|
||||||
|
|
||||||
|
Эта папка хранит рабочие инструкции по переносу экранов SHiNE в Figma и по обратному переносу изменений из Figma в код.
|
||||||
|
|
||||||
|
## Что здесь лежит
|
||||||
|
|
||||||
|
- `README.md` — точка входа и краткий регламент.
|
||||||
|
- `TRANSFER_UI_SCREENS.md` — подробная инструкция по переносу экранов UI в Figma и обратно.
|
||||||
|
|
||||||
|
## Когда читать
|
||||||
|
|
||||||
|
Читать перед любыми задачами вида:
|
||||||
|
- перенести экран из `shine-UI` в Figma;
|
||||||
|
- собрать новый Figma-файл для экранов SHiNE;
|
||||||
|
- перенести изменения из Figma обратно в код;
|
||||||
|
- уточнить, каким способом переносить экраны: по одному или пачкой.
|
||||||
|
|
||||||
|
## Ключевое правило
|
||||||
|
|
||||||
|
Для экранов SHiNE безопасный рабочий способ на текущий момент:
|
||||||
|
- переносить экраны в Figma по одному;
|
||||||
|
- не пытаться сразу переносить длинный auth-flow пачкой;
|
||||||
|
- после каждого переноса визуально проверять результат в самой Figma;
|
||||||
|
- только после удачного одного экрана переходить к следующему.
|
||||||
|
|
||||||
|
## Про Miro
|
||||||
|
|
||||||
|
Отдельной папки `Miro` пока нет.
|
||||||
|
|
||||||
|
Причина:
|
||||||
|
- практики по Miro в проекте пока мало;
|
||||||
|
- устойчивого процесса ещё нет;
|
||||||
|
- как только появится стабильный сценарий работы с Miro, его нужно будет оформить аналогично Figma.
|
||||||
224
Dev_Docs/Figma/TRANSFER_UI_SCREENS.md
Normal file
224
Dev_Docs/Figma/TRANSFER_UI_SCREENS.md
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# Перенос экранов UI в Figma и обратно
|
||||||
|
|
||||||
|
## Зачем нужен этот документ
|
||||||
|
|
||||||
|
Этот документ фиксирует практический опыт, который уже был получен на переносе стартового экрана, экрана регистрации и дальнейших попытках.
|
||||||
|
|
||||||
|
Главная цель:
|
||||||
|
- чтобы агент не повторял неудачные попытки;
|
||||||
|
- чтобы переносы делались одинаково;
|
||||||
|
- чтобы изменения из Figma можно было уверенно переносить назад в `shine-UI`.
|
||||||
|
|
||||||
|
## Где находится основной UI
|
||||||
|
|
||||||
|
- основной клиентский UI: `shine-UI/`
|
||||||
|
- маршруты и список pre-auth экранов: `shine-UI/js/router.js`
|
||||||
|
- экраны: `shine-UI/js/pages/`
|
||||||
|
- общие стили: `shine-UI/styles/main.css`, `shine-UI/styles/layout.css`, `shine-UI/styles/components.css`
|
||||||
|
|
||||||
|
## Что считать успешным переносом в Figma
|
||||||
|
|
||||||
|
Успешный перенос экрана в Figma — это не просто фон и прямоугольники.
|
||||||
|
|
||||||
|
Нужно, чтобы:
|
||||||
|
- были видны все ключевые текстовые элементы;
|
||||||
|
- кнопки были перенесены как отдельные элементы;
|
||||||
|
- поля ввода были явно видны;
|
||||||
|
- экран был узнаваем визуально;
|
||||||
|
- пользователь мог вручную подправить макет в Figma;
|
||||||
|
- после правок можно было понять, что именно переносить обратно в код.
|
||||||
|
|
||||||
|
## Текущий рабочий способ
|
||||||
|
|
||||||
|
На текущем проекте лучший практический способ такой:
|
||||||
|
|
||||||
|
1. Переносить только один экран за раз.
|
||||||
|
2. Сначала читать конкретный `js/pages/<screen>.js`.
|
||||||
|
3. Затем читать связанные стили из `styles/components.css` и `styles/layout.css`.
|
||||||
|
4. После этого вручную собирать экран в Figma как отдельный frame с явными элементами.
|
||||||
|
5. Проверять в Figma, что не получился только фон без текста и контролов.
|
||||||
|
6. Только после успешной проверки переходить к следующему экрану.
|
||||||
|
|
||||||
|
## Почему нельзя переносить пачкой
|
||||||
|
|
||||||
|
Был получен негативный опыт:
|
||||||
|
- при переносе сразу многих экранов в Figma часть экранов отображалась как фон без нормальных надписей и элементов;
|
||||||
|
- длинные экраны с большим количеством текста и форм разваливались;
|
||||||
|
- автогенерация давала внешний вид, непригодный для ручной доработки.
|
||||||
|
|
||||||
|
Поэтому правило такое:
|
||||||
|
- auth-flow, регистрация, вход, onboarding — переносить по одному экрану;
|
||||||
|
- после каждого экрана ждать визуального подтверждения пользователя;
|
||||||
|
- не объединять 5-10 экранов в один проход без отдельного разрешения и без промежуточной проверки.
|
||||||
|
|
||||||
|
## Рекомендуемый порядок переноса в Figma
|
||||||
|
|
||||||
|
### Вперёд: код -> Figma
|
||||||
|
|
||||||
|
1. Определить точный экран.
|
||||||
|
2. Найти файл экрана в `shine-UI/js/pages/`.
|
||||||
|
3. Найти используемые CSS-классы через поиск по файлу экрана.
|
||||||
|
4. Вытащить:
|
||||||
|
- тексты;
|
||||||
|
- состав кнопок;
|
||||||
|
- поля ввода;
|
||||||
|
- карточки;
|
||||||
|
- блоки статуса;
|
||||||
|
- последовательность секций.
|
||||||
|
5. Если экран длинный, всё равно переносить его как один frame, но собирать блоками сверху вниз.
|
||||||
|
6. В Figma создавать отдельный экран рядом с уже существующими экранами, а не смешивать всё в одну кучу.
|
||||||
|
7. После создания экрана проверить метаданные/скриншот Figma, если инструмент это позволяет.
|
||||||
|
|
||||||
|
### Назад: Figma -> код
|
||||||
|
|
||||||
|
1. Снять актуальный скриншот изменённого Figma-экрана.
|
||||||
|
2. Получить метаданные узла, если это помогает понять структуру.
|
||||||
|
3. Сравнить Figma с текущим кодом экрана.
|
||||||
|
4. Переносить обратно в код только реальные изменения:
|
||||||
|
- порядок блоков;
|
||||||
|
- тексты;
|
||||||
|
- размеры/отступы;
|
||||||
|
- наличие или отсутствие карточек;
|
||||||
|
- подписи кнопок;
|
||||||
|
- видимость блоков.
|
||||||
|
5. Не придумывать новые UX-решения без отдельного подтверждения пользователя, если их нет в Figma.
|
||||||
|
6. После правок проверять экран локально или как минимум по коду и зависимостям.
|
||||||
|
|
||||||
|
## Что переносить вручную
|
||||||
|
|
||||||
|
Вручную, а не автогенерацией, нужно переносить:
|
||||||
|
- экраны регистрации;
|
||||||
|
- экраны входа;
|
||||||
|
- длинные формы;
|
||||||
|
- экраны с несколькими карточками;
|
||||||
|
- экраны с длинными объясняющими текстами;
|
||||||
|
- экраны, где важен порядок блоков.
|
||||||
|
|
||||||
|
Причина:
|
||||||
|
- именно они чаще всего ломаются при слишком автоматическом переносе.
|
||||||
|
|
||||||
|
## Какие ошибки уже были
|
||||||
|
|
||||||
|
### Ошибка 1. Перенос пачкой
|
||||||
|
|
||||||
|
Проблема:
|
||||||
|
- несколько экранов были добавлены сразу;
|
||||||
|
- пользователь увидел, что на экранах в Figma «какая-то ерунда».
|
||||||
|
|
||||||
|
Вывод:
|
||||||
|
- переносить по одному.
|
||||||
|
|
||||||
|
### Ошибка 2. Видно только фон
|
||||||
|
|
||||||
|
Проблема:
|
||||||
|
- frame создавался, фон и свечения были видны;
|
||||||
|
- тексты и элементы либо не появлялись, либо получались непригодными.
|
||||||
|
|
||||||
|
Вывод:
|
||||||
|
- при сложных экранах собирать элементы вручную и явно.
|
||||||
|
|
||||||
|
### Ошибка 3. Слишком вольная реконструкция
|
||||||
|
|
||||||
|
Проблема:
|
||||||
|
- экран формально был перенесён, но визуально не соответствовал ожиданию пользователя.
|
||||||
|
|
||||||
|
Вывод:
|
||||||
|
- для SHiNE важнее узнаваемый и редактируемый экран, чем «формально похожий» экран.
|
||||||
|
|
||||||
|
## Обязательные проверки после переноса в Figma
|
||||||
|
|
||||||
|
После каждого нового экрана агент должен проверить:
|
||||||
|
- виден ли заголовок;
|
||||||
|
- видны ли кнопки;
|
||||||
|
- видны ли поля ввода;
|
||||||
|
- не исчезли ли длинные тексты;
|
||||||
|
- не сломан ли порядок секций;
|
||||||
|
- не оказался ли на холсте только фон и пустые прямоугольники.
|
||||||
|
|
||||||
|
Если хотя бы один пункт не выполнен:
|
||||||
|
- не считать перенос завершённым;
|
||||||
|
- либо переделать экран сразу;
|
||||||
|
- либо остановиться и показать пользователю только после исправления.
|
||||||
|
|
||||||
|
## Правила для длинных экранов
|
||||||
|
|
||||||
|
Если экран длинный, например регистрация:
|
||||||
|
- высота frame может быть больше стандартной мобильной высоты;
|
||||||
|
- секции должны идти в правильном вертикальном порядке;
|
||||||
|
- отдельные карточки должны быть вынесены в отдельные блоки;
|
||||||
|
- тексты лучше упрощённо располагать вручную, чем терять их совсем.
|
||||||
|
|
||||||
|
## Правила для экрана регистрации
|
||||||
|
|
||||||
|
Экран `register-view` особенно чувствительный.
|
||||||
|
|
||||||
|
При переносе нужно отдельно учитывать:
|
||||||
|
- заголовок и стрелку назад;
|
||||||
|
- поля логина и пароля;
|
||||||
|
- переключатель режима 12 слов;
|
||||||
|
- сетку слов;
|
||||||
|
- строку статуса длины пароля;
|
||||||
|
- строку статуса проверки логина;
|
||||||
|
- кнопку проверки логина;
|
||||||
|
- отдельную карточку первого сервера;
|
||||||
|
- отдельную карточку FAQ;
|
||||||
|
- нижние кнопки `Назад` и `Далее`.
|
||||||
|
|
||||||
|
## Правила для экрана входа
|
||||||
|
|
||||||
|
Для экранов входа важно не смешивать:
|
||||||
|
- экран выбора способа входа;
|
||||||
|
- вход по логину/паролю;
|
||||||
|
- вход через другое устройство;
|
||||||
|
- вход по QR.
|
||||||
|
|
||||||
|
Каждый из них переносить отдельно.
|
||||||
|
|
||||||
|
## Что делать после правок пользователя в Figma
|
||||||
|
|
||||||
|
Если пользователь изменил экран в Figma:
|
||||||
|
|
||||||
|
1. Считать Figma источником визуальной правки.
|
||||||
|
2. Сначала понять, что именно изменено:
|
||||||
|
- тексты;
|
||||||
|
- порядок блоков;
|
||||||
|
- наличие блоков;
|
||||||
|
- размеры;
|
||||||
|
- отступы;
|
||||||
|
- логика flow.
|
||||||
|
3. Переносить эти изменения назад в код минимально необходимыми правками.
|
||||||
|
4. Если из Figma следует уже не только визуальная, но и UX-логическая правка, отдельно проверить, что она согласована пользователем.
|
||||||
|
|
||||||
|
## Когда нужно добавить заметку в Pending_Features
|
||||||
|
|
||||||
|
Если после изменения по Figma:
|
||||||
|
- поменялась логика flow;
|
||||||
|
- поменялась регистрация/вход;
|
||||||
|
- нужен реальный прогон на test2;
|
||||||
|
- затронута интеграция с Solana;
|
||||||
|
|
||||||
|
тогда нужно добавить файл в `Dev_Docs/Pending_Features/`.
|
||||||
|
|
||||||
|
## Что пока не оформлено для Miro
|
||||||
|
|
||||||
|
По Miro пока нет устойчивого процесса.
|
||||||
|
|
||||||
|
Из того, что уже понятно:
|
||||||
|
- пока не стоит обещать такой же отлаженный перенос, как для Figma;
|
||||||
|
- сначала нужно накопить хотя бы 2-3 реальных сценария работы;
|
||||||
|
- только после этого оформлять отдельную папку и регламент.
|
||||||
|
|
||||||
|
## Краткая памятка для агента
|
||||||
|
|
||||||
|
Если задача звучит как:
|
||||||
|
- «перенеси экран в Figma»;
|
||||||
|
- «добавь экран в Figma»;
|
||||||
|
- «я поправил экран в Figma, перенеси назад»;
|
||||||
|
|
||||||
|
то агент должен:
|
||||||
|
|
||||||
|
1. Прочитать этот документ.
|
||||||
|
2. Работать по одному экрану.
|
||||||
|
3. Не переносить auth-flow пачкой.
|
||||||
|
4. Проверять результат после каждого экрана.
|
||||||
|
5. При переносе обратно в код не гадать, а опираться на Figma-правки.
|
||||||
154
Dev_Docs/Keys/DERIVATION.md
Normal file
154
Dev_Docs/Keys/DERIVATION.md
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
# Деривация секрета и ключей SHiNE (формулы)
|
||||||
|
|
||||||
|
> **Статус: ИСТОЧНИК ИСТИНЫ (single source of truth) по конкретной деривации.**
|
||||||
|
> Этот файл описывает, как из пароля получается секрет и как из секрета выводятся
|
||||||
|
> все ключи (root, blockchain, device/Solana, homeserver) — формулами, байт-в-байт.
|
||||||
|
> Если в коде меняется деривация (формула секрета, параметры Argon2id, соль, формула
|
||||||
|
> ключа, разделитель `|`, набор/имена суффиксов, формат homeserver-ключа, связь
|
||||||
|
> dev-ключ ↔ Solana-адрес) — **в том же изменении обязательно править этот документ**.
|
||||||
|
> Роли и назначение ключей описаны отдельно в `Dev_Docs/Keys/README.md` (архитектура).
|
||||||
|
> Здесь — только механика. Документ намеренно краткий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Секрет (masterSecret)
|
||||||
|
|
||||||
|
`masterSecret` — 32 байта. Два источника:
|
||||||
|
|
||||||
|
**А. Из пароля пользователя (основной путь, UI).**
|
||||||
|
|
||||||
|
```
|
||||||
|
login = trim(lowercase(login))
|
||||||
|
salt = SHA-256("shine-auth-v2|login=" + login + "|suffix=master.secret")[0..16) // первые 16 байт
|
||||||
|
material = utf8(login + "\n" + password)
|
||||||
|
masterSecret(32) = Argon2id(material, salt, t=2, m=65536 KiB, p=1, dkLen=32)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Параметры Argon2id фиксированы: `t=2`, `m=65536` (64 МиБ), `p=1`, `dkLen=32`.
|
||||||
|
- Логин входит и в соль, и в начало `material` (склейка через `\n`).
|
||||||
|
- Пустой пароль **запрещён**: легаси-fallback без Argon2 удалён, `deriveMasterSecretFromPassword` бросает ошибку на пустом пароле, а форма регистрации в UI блокирует пустой пароль (`register-view.js`).
|
||||||
|
|
||||||
|
**Б. Случайный (прошивка ESP32, новый аккаунт без пароля).**
|
||||||
|
|
||||||
|
```
|
||||||
|
masterSecret(32) = 32 случайных байта (esp_random) // хранится на устройстве как base58
|
||||||
|
```
|
||||||
|
|
||||||
|
Дальше деривация ключей одинакова независимо от источника секрета.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Производные ключи
|
||||||
|
|
||||||
|
Все ключи выводятся из `masterSecret` по **одной формуле**, отличается только суффикс:
|
||||||
|
|
||||||
|
```
|
||||||
|
material = base64_std(masterSecret) + "|" + <суффикс>
|
||||||
|
seed(32) = SHA-256(material)
|
||||||
|
(pub, priv) = Ed25519_keypair_from_seed(seed)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `base64_std` — стандартный base64 (не url-safe).
|
||||||
|
- Разделитель — символ `|`.
|
||||||
|
- Суффиксы значимы байт-в-байт (регистр и точки важны).
|
||||||
|
|
||||||
|
| Ключ | Суффикс | Назначение (кратко) |
|
||||||
|
|------|---------|---------------------|
|
||||||
|
| root | `root.key` | Личность. Подписывает unsigned-часть PDA-записи (`RootKeyBlock`). |
|
||||||
|
| blockchain | `bch.key` | Подписывает `LastBlockState` персонального блокчейна (`blockchain_public_key`). |
|
||||||
|
| device / **Solana** | `client.key` | Ключ устройства = Solana-ключ. Fee payer и подпись Solana-транзакций; адрес кошелька = `base58(clientPub)`. См. §3. |
|
||||||
|
| homeserver | `homeserver.key:<имя>` | Ключ homeserver-устройства, по одному на каждый homeserver (различитель — имя). См. §4. |
|
||||||
|
|
||||||
|
Полные роли каждого ключа — в `Dev_Docs/Keys/README.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Solana-ключ
|
||||||
|
|
||||||
|
Отдельного «солана-ключа» нет. На Solana работают два ключа:
|
||||||
|
|
||||||
|
- **`client.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(clientPub)`.
|
||||||
|
Этим ключом оплачиваются и подписываются `create_user_pda` / `update_user_pda`.
|
||||||
|
Пополнять SOL нужно именно на этот адрес.
|
||||||
|
- **`root.key` — авторитет записи**, подписывает unsigned-часть PDA через Ed25519-инструкцию, но **не** является fee payer.
|
||||||
|
|
||||||
|
Соответствует формату PDA `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` §2.1
|
||||||
|
(«create/update оплачиваются с `client_key`», «root_key — не fee payer»).
|
||||||
|
|
||||||
|
Кратко про роли на Solana: `root.key` — это **главный (master) ключ**: им управляют PDA-записью
|
||||||
|
(`create/update`) и через это можно заменить все остальные ключи; `client.key` — это **пополняемый
|
||||||
|
кошелёк и плательщик комиссий**. Полное описание ролей — `Dev_Docs/Keys/README.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ключи homeserver
|
||||||
|
|
||||||
|
У пользователя может быть несколько homeserver-ов. Каждый имеет **своё имя** и **свой приватный ключ**,
|
||||||
|
выведенный из секрета по той же формуле с именованным суффиксом:
|
||||||
|
|
||||||
|
```
|
||||||
|
suffix = "homeserver.key:" + <имя homeserver> // имя по умолчанию: "homeserver1"
|
||||||
|
material = base64_std(masterSecret) + "|" + suffix
|
||||||
|
seed(32) = SHA-256(material)
|
||||||
|
(pub, priv) = Ed25519_keypair_from_seed(seed)
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример для двух homeserver-ов:
|
||||||
|
|
||||||
|
```
|
||||||
|
homeserver.key:home-a -> ключ A
|
||||||
|
homeserver.key:home-b -> ключ B
|
||||||
|
```
|
||||||
|
|
||||||
|
Публичный ключ homeserver-а публикуется в `SessionsBlock` пользовательской PDA как
|
||||||
|
`session_pub_key` с `session_type = 100`, имя — в `session_name` (формат PDA §13).
|
||||||
|
|
||||||
|
> Это переименование прежней схемы `subserver.key:<имя>` → `homeserver.key:<имя>`.
|
||||||
|
> Термин «саб-сервер» по проекту заменяется на «homeserver».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Где это в коде
|
||||||
|
|
||||||
|
### Деривация секрета и ключей (UI, каноническая)
|
||||||
|
- `shine-UI/js/services/crypto-utils.js`
|
||||||
|
- секрет из пароля: `makeArgon2Salt`, `deriveMasterSecretArgon2id`, `deriveMasterSecretFromPassword` (~129–218);
|
||||||
|
- ключ из секрета: `deriveEd25519FromMasterSecret` (~220).
|
||||||
|
- `shine-UI/js/services/auth-service.js` — набор root/bch/dev из `masterSecret` (~732–758).
|
||||||
|
- `shine-UI/server-ui/js/server-ui-shared.js` — те же root/bch/dev для серверного UI (~147–160).
|
||||||
|
|
||||||
|
### Solana-ключ / адрес кошелька (UI)
|
||||||
|
- `shine-UI/js/pages/registration-payment-view.js` — `deriveUserWalletAddress`: адрес = `base58(clientPub)` (~113).
|
||||||
|
- `shine-UI/js/pages/topup-view.js` — `clientWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.clientPair`.
|
||||||
|
Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `client.key`, мимо `masterSecret`) удалён.
|
||||||
|
|
||||||
|
### Деривация ключей (прошивка ESP32)
|
||||||
|
- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino`
|
||||||
|
- основной скетч ESP32-проекта `SHiNE`; `deriveKeysFromMasterSecret` (~782), `restoreDerivedKeysFromSecret` (~806), `deriveFreshSecretAndWallet` (~829);
|
||||||
|
- регистрация/подпись Solana: `registerHomeserverOnSolana` (~1182), `signMessageEd25519` (~1147).
|
||||||
|
- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_ui/shine_homeserver_ui.ino`
|
||||||
|
- старый тестовый вариант; оставлен как legacy-скетч для сравнения и диагностики.
|
||||||
|
|
||||||
|
### Формат PDA (куда попадают ключи)
|
||||||
|
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
||||||
|
— `RootKeyBlock` §6, `ClientKeyBlock` §7, `blockchain_public_key` §9, `SessionsBlock`/`session_type=100` §13, оплата §2.1.
|
||||||
|
|
||||||
|
### Сервер (тестовый seed)
|
||||||
|
- `SHiNE-server/src/test/java/test/it/cases/SeedDataPopulationHelper.java` `deriveKeysFromPassword` (~246) —
|
||||||
|
выводит ключи как `Ed25519(SHA-256(base64(SHA-256(password)) + suffix))`, **без** Argon2 и **без** разделителя `|`.
|
||||||
|
Это **не баг**, а точное повторение легаси-пути UI `derivePasswordSeed` (для пустого пароля), у которого тоже нет `|`.
|
||||||
|
С современным путём `masterSecret`-bundle (Argon2 + `base64(secret)|suffix`) он **не совпадает** by design.
|
||||||
|
Если потребуется, чтобы seed совпадал с реальными клиентами на Argon2 — нужно отдельно портировать
|
||||||
|
Argon2id+masterSecret в Java (на сервере Argon2 сейчас нет). Простое добавление `|` было бы **неверным**:
|
||||||
|
сломало бы совпадение с легаси-путём и всё равно не дало бы совпадения с Argon2-путём.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Правило синхронизации (обязательно)
|
||||||
|
|
||||||
|
1. Этот документ — источник истины по деривации секрета и ключей.
|
||||||
|
2. Любое изменение кода, затрагивающее формулу секрета, параметры Argon2id, соль, формулу ключа,
|
||||||
|
разделитель `|`, набор/имена суффиксов, формат homeserver-ключа или связь dev-ключ ↔ Solana-адрес —
|
||||||
|
**обязательно** отражать здесь в том же изменении.
|
||||||
|
3. Пункты, помеченные ⚠️, — это долг к устранению, а не норма.
|
||||||
|
4. Нельзя сознательно оставлять код и этот документ в рассинхроне без отдельной явной договорённости.
|
||||||
176
Dev_Docs/Keys/README.md
Normal file
176
Dev_Docs/Keys/README.md
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
# Ключи SHiNE
|
||||||
|
|
||||||
|
Этот документ описывает роли ключей в SHiNE и их связь с Solana, персональным блокчейном, личными сообщениями, сессиями и будущими аппаратными устройствами.
|
||||||
|
|
||||||
|
Документ является архитектурной справкой. Он не меняет текущие форматы API, DM-блоков или блокчейна сам по себе.
|
||||||
|
|
||||||
|
## Коротко
|
||||||
|
|
||||||
|
В SHiNE у пользователя есть несколько уровней ключей:
|
||||||
|
|
||||||
|
- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `client key`).
|
||||||
|
- `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
|
||||||
|
- `client key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
|
||||||
|
- `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере.
|
||||||
|
|
||||||
|
Главная идея: самые важные ключи можно держать на доверенном серверном или аппаратном устройстве, а обычные клиентские устройства получают только ключи, нужные для текущей работы.
|
||||||
|
|
||||||
|
## `root key`
|
||||||
|
|
||||||
|
`root key` - главный ключ пользователя.
|
||||||
|
|
||||||
|
Назначение:
|
||||||
|
|
||||||
|
- регистрация пользователя в Solana;
|
||||||
|
- создание и обновление пользовательской PDA-записи;
|
||||||
|
- вызов критически важных Solana-функций;
|
||||||
|
- изменение главных настроек пользователя;
|
||||||
|
- управление остальными ключами;
|
||||||
|
- подтверждение операций, которые должны иметь максимальный уровень доверия.
|
||||||
|
|
||||||
|
`root key` — это **главный (master) ключ** в следующем смысле: зная `root key`, можно управлять пользовательской PDA-записью в Solana (`create_user_pda` / `update_user_pda`) и тем самым **заменить все остальные ключи** пользователя (device, blockchain, homeserver). Поэтому компрометация `root key` равносильна компрометации всей личности пользователя.
|
||||||
|
|
||||||
|
Важно не путать авторитет и кошелёк: `root key` — это авторитет над PDA-записью, а **SOL-комиссии за create/update платит `client key`** (он же fee payer и адрес для пополнения). Подробнее о том, какой ключ за что отвечает на Solana, — в `Dev_Docs/Keys/DERIVATION.md`, §3.
|
||||||
|
|
||||||
|
## `blockchain key`
|
||||||
|
|
||||||
|
`blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
|
||||||
|
|
||||||
|
Назначение:
|
||||||
|
|
||||||
|
- подпись записей в персональном блокчейне пользователя;
|
||||||
|
- подтверждение действий, которые должны попасть в SHiNE-блокчейн;
|
||||||
|
- разделение полномочий между главным Solana-ключом и ключом ежедневной записи.
|
||||||
|
|
||||||
|
У пользователя может быть несколько персональных блокчейнов или веток. При смене `blockchain key` фактически создаётся новая ветка записи:
|
||||||
|
|
||||||
|
- `username-001` - первая ветка;
|
||||||
|
- `username-002` - вторая ветка;
|
||||||
|
- `username-003` - третья ветка.
|
||||||
|
|
||||||
|
Рабочая логика по умолчанию должна использовать последнюю актуальную ветку. Старые ветки остаются читаемыми и показывают историю смены ключей.
|
||||||
|
|
||||||
|
## `client key`
|
||||||
|
|
||||||
|
`client key` - общий ключ, который знают доверенные устройства пользователя.
|
||||||
|
|
||||||
|
Назначение:
|
||||||
|
|
||||||
|
- повседневные входящие и исходящие личные сообщения;
|
||||||
|
- звонки и связанные с ними сообщения;
|
||||||
|
- self-messages, то есть внутренние сообщения пользователя самому себе;
|
||||||
|
- мелкие Solana-расходы на текущие операции;
|
||||||
|
- derivation Arweave-кошелька;
|
||||||
|
- оплата или подготовка добавления данных в Arweave-кошелек по отдельному протоколу.
|
||||||
|
|
||||||
|
Arweave-кошелёк должен выводиться из `client key` по протоколу:
|
||||||
|
|
||||||
|
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md`
|
||||||
|
|
||||||
|
Если пользователь теряет только `client key`, в худшем случае ломается повседневная переписка и доступ конкретных устройств к ежедневным операциям. `root key` и `blockchain key` при правильной архитектуре остаются отдельно защищёнными.
|
||||||
|
|
||||||
|
## `session key`
|
||||||
|
|
||||||
|
`session key` - уникальный ключ конкретной сессии или устройства.
|
||||||
|
|
||||||
|
Возможные форматы:
|
||||||
|
|
||||||
|
- `Ed25519` - предпочтительный современный вариант;
|
||||||
|
- `RSA` - legacy-вариант, полезный для устройств, где системное защищённое хранилище хорошо поддерживает RSA-ключи и не позволяет извлекать приватный ключ.
|
||||||
|
|
||||||
|
Назначение:
|
||||||
|
|
||||||
|
- авторизация сессии на сервере;
|
||||||
|
- привязка устройства к пользователю;
|
||||||
|
- подтверждение запросов от конкретной сессии;
|
||||||
|
- доступ к зашифрованному `client key` после успешной авторизации.
|
||||||
|
|
||||||
|
Одна и та же сессия может быть пригодна для подключения к нескольким серверам пользователя, если архитектура конкретного пользователя это допускает.
|
||||||
|
|
||||||
|
У сессии должны быть:
|
||||||
|
|
||||||
|
- имя сессии;
|
||||||
|
- тип сессии;
|
||||||
|
- публичная часть ключа;
|
||||||
|
- ссылка на пользователя;
|
||||||
|
- информация о сервере или серверах, которым эта сессия доверена.
|
||||||
|
|
||||||
|
Имя сессии может создаваться автоматически из названия устройства и короткого случайного идентификатора, например `Android-a1b2c3`, `Ubuntu-f47a90`. Пользователь может переименовать сессию.
|
||||||
|
|
||||||
|
## Типы сессий
|
||||||
|
|
||||||
|
Базовые типы:
|
||||||
|
|
||||||
|
- обычная пользовательская сессия;
|
||||||
|
- серверная сессия;
|
||||||
|
- аппаратная или доверенная сессия с доступом к расширенным ключам.
|
||||||
|
|
||||||
|
Обычное устройство обычно имеет:
|
||||||
|
|
||||||
|
- собственный `session key`;
|
||||||
|
- зашифрованный `client key`, который открывается после авторизации;
|
||||||
|
- доступ к DM, звонкам и обычным пользовательским операциям.
|
||||||
|
|
||||||
|
Доверенное серверное или аппаратное устройство может иметь:
|
||||||
|
|
||||||
|
- `root key`;
|
||||||
|
- `blockchain key`;
|
||||||
|
- `client key`;
|
||||||
|
- собственный `session key`.
|
||||||
|
|
||||||
|
Такая сессия может подписывать операции повышенной важности по запросам пользователя.
|
||||||
|
|
||||||
|
## Внутренние self-messages
|
||||||
|
|
||||||
|
Self-message - это сообщение пользователя самому себе.
|
||||||
|
|
||||||
|
Такие сообщения нужны, чтобы обычное устройство могло попросить доверенное устройство выполнить действие:
|
||||||
|
|
||||||
|
- подписать запись `blockchain key` и передать её в SHiNE-блокчейн;
|
||||||
|
- подписать изменение настройки через `root key`;
|
||||||
|
- обновить ключи;
|
||||||
|
- сохранить внутреннюю команду или настройку;
|
||||||
|
- отправить сообщение другому пользователю с сохранением копии себе;
|
||||||
|
- сохранить сообщение только себе.
|
||||||
|
|
||||||
|
Важно: self-message не является публичной командой сервера. Это пользовательская внутренняя команда, которую сервер или доверенное устройство обрабатывает в рамках прав конкретного пользователя.
|
||||||
|
|
||||||
|
## Шифрование входящих сообщений
|
||||||
|
|
||||||
|
Входящее сообщение может быть зашифровано:
|
||||||
|
|
||||||
|
- `client key`;
|
||||||
|
- `session key`;
|
||||||
|
- отдельным ключом конкретного чата;
|
||||||
|
- другим ключом, который уже известен клиенту.
|
||||||
|
|
||||||
|
В сообщении не должно быть лишнего раскрытия того, каким именно ключом оно зашифровано. Клиент пробует расшифровать сообщение доступными ключами по порядку. Если расшифровка не удалась, сообщение остаётся непонятным для этого устройства.
|
||||||
|
|
||||||
|
## Копии сообщений
|
||||||
|
|
||||||
|
Для отправки сообщений нужны несколько режимов:
|
||||||
|
|
||||||
|
- сообщение другому пользователю с исходящей копией себе;
|
||||||
|
- сообщение другому пользователю без локальной исходящей копии;
|
||||||
|
- сообщение только себе.
|
||||||
|
|
||||||
|
Это должно позволить строить обычные DM, внутренние команды, личные заметки и зашифрованные пользовательские чаты поверх одной общей модели сообщений.
|
||||||
|
|
||||||
|
## Связанные документы
|
||||||
|
|
||||||
|
- `Dev_Docs/Keys/DERIVATION.md` - **источник истины по конкретной деривации** секрета и ключей (формулы Argon2id, `base64|suffix→SHA-256→Ed25519`, суффиксы `root.key`/`bch.key`/`client.key`/`homeserver.key:<имя>`, Solana-ключ, ссылки на код).
|
||||||
|
- `Dev_Docs/Personal_Messages/README.md` - текущая документация личных сообщений.
|
||||||
|
- `Dev_Docs/Blockchain/README.md` - точка входа по форматам SHiNE-блокчейна.
|
||||||
|
- `Dev_Docs/Solana_Architecture/README.md` - архитектура Solana-программ, PDA-счетов, DAO и движения средств.
|
||||||
|
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` - деплой и первичная инициализация Solana-регистрации.
|
||||||
|
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md` - derivation Arweave-кошелька из `client key`.
|
||||||
|
|
||||||
|
## Что нужно уточнить перед реализацией
|
||||||
|
|
||||||
|
- точный формат записи списка ключей в Solana PDA;
|
||||||
|
- как именно обозначать активную ветку персонального блокчейна;
|
||||||
|
- какие операции требуют `root key`, а какие достаточно подписывать `blockchain key`;
|
||||||
|
- формат self-message-команд;
|
||||||
|
- порядок перебора ключей при расшифровке входящих сообщений;
|
||||||
|
- правила ротации `client key` и восстановления доступа после потери устройства;
|
||||||
|
- какие типы серверных и аппаратных сессий нужны в первой реализации.
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
# Поддержать проект Сияние
|
||||||
|
|
||||||
|
Статус: `pending`
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
В `shine-UI/js/pages/wallet-view.js` добавлен новый раздел `Поддержать проект Сияние` с тремя входами:
|
||||||
|
|
||||||
|
1. купить билет;
|
||||||
|
2. посмотреть билет по номеру;
|
||||||
|
3. сгенерировать новую пару ключей.
|
||||||
|
|
||||||
|
## Что проверить
|
||||||
|
|
||||||
|
1. Открыть `Кошелёк`.
|
||||||
|
2. Перейти в `Поддержать проект Сияние`.
|
||||||
|
3. Проверить экран покупки:
|
||||||
|
- виден коэффициент;
|
||||||
|
- виден остаток лимита очереди 1;
|
||||||
|
- виден расчет в SOL;
|
||||||
|
- кнопка `Справка` открывает отдельный экран;
|
||||||
|
- покупка блокируется, если сумма больше остатка лимита.
|
||||||
|
4. Проверить экран просмотра:
|
||||||
|
- `12` ищется как билет очереди 1;
|
||||||
|
- `2-5` и `3 8` ищутся как билеты очередей 2 и 3;
|
||||||
|
- показываются статус, количество билетов до него и уже выплаченные значения.
|
||||||
|
5. Проверить генератор ключей:
|
||||||
|
- генерируется новая пара ключей;
|
||||||
|
- публичный и секретный ключи показываются;
|
||||||
|
- можно скопировать и скачать результат;
|
||||||
|
- дополнительный текст в поле необязателен.
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
|
||||||
|
- Экран раздела поддержки открывается из `wallet-view`.
|
||||||
|
- Покупка билета выполняется по текущему курсу и с допуском 3%.
|
||||||
|
- По номеру билета показывается понятная сводка по очереди.
|
||||||
|
- Генерация ключей использует безопасный браузерный рандом и не требует сохранения секретного ключа.
|
||||||
20
Dev_Docs/Pending_Features/README.md
Normal file
20
Dev_Docs/Pending_Features/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Недопроверенные фичи
|
||||||
|
|
||||||
|
Эта папка хранит список доработок, которые уже реализованы, но ещё не подтверждены ручной проверкой.
|
||||||
|
|
||||||
|
## Как использовать
|
||||||
|
|
||||||
|
1. При каждом коммите с новыми пользовательскими фичами (если нужна ручная проверка) добавить новый файл:
|
||||||
|
- формат: `YYYY-MM-DD_HHMM_<short-feature-name>.md`
|
||||||
|
- название `<short-feature-name>` и текст файла по возможности писать на русском языке
|
||||||
|
2. В файле указать:
|
||||||
|
- что сделано;
|
||||||
|
- как проверять;
|
||||||
|
- ожидаемый результат;
|
||||||
|
- текущий статус (`pending` / `in_progress` / `done`).
|
||||||
|
3. После подтверждения работоспособности — удалить файл фичи из этой папки.
|
||||||
|
|
||||||
|
## Важно
|
||||||
|
|
||||||
|
- `README.md` не удаляется.
|
||||||
|
- Количество недопроверенных фич = число файлов `*.md` в этой папке, кроме `README.md`.
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
# Регистрация: FAQ и режим пароля из 12 слов
|
||||||
|
|
||||||
|
- краткое описание:
|
||||||
|
- на экране регистрации добавлен блок частых вопросов с переходом на отдельный экран справки;
|
||||||
|
- добавлен альтернативный режим ввода пароля через 12 полей-слов в кошелёчном формате, которые склеиваются в одну строку без изменения API;
|
||||||
|
- такой же режим добавлен и на экран входа по логину и паролю.
|
||||||
|
|
||||||
|
- что проверять:
|
||||||
|
- на стартовом экране открыть `Зарегистрироваться`;
|
||||||
|
- убедиться, что внизу экрана есть кнопки FAQ;
|
||||||
|
- открыть несколько вопросов и проверить возврат обратно на регистрацию;
|
||||||
|
- включить галочку `Представить пароль в виде 12 слов`;
|
||||||
|
- убедиться, что появляется сетка с нумерованными полями в 3 колонки;
|
||||||
|
- ввести часть слов, перейти дальше и проверить, что шаг подтверждения и генерация ключей работают;
|
||||||
|
- выключить галочку и проверить, что пароль остаётся собранным в одном поле;
|
||||||
|
- открыть экран входа по паролю и повторить те же проверки для режима `12 слов`;
|
||||||
|
- пройти регистрацию до шага оплаты без ошибок интерфейса.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- FAQ открывается отдельным экраном и содержит понятные ответы;
|
||||||
|
- режим `12 слов` не ломает регистрацию и вход и даёт тот же поток, что и обычный пароль;
|
||||||
|
- пароль не отправляется в новом формате, а продолжает использоваться как одна строка.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
# Временная бесплатная загрузка аватара в Arweave
|
||||||
|
|
||||||
|
- краткое описание фичи:
|
||||||
|
Добавлены два временных `Test...` API для бесплатной загрузки маленьких аватаров в Arweave через серверный кошелёк с лимитом `3` загрузки на пользователя. В UI мастера смены аватара добавлен пункт `Залить аватар бесплатно`.
|
||||||
|
|
||||||
|
- что именно проверять:
|
||||||
|
1. Пользователь с активной сессией открывает редактирование профиля.
|
||||||
|
2. По нажатию на аватар открывается мастер `Сменить аватар`.
|
||||||
|
3. В мастере есть пункт `Залить аватар бесплатно`.
|
||||||
|
4. До первой загрузки UI показывает остаток `3 из 3`.
|
||||||
|
5. Маленький JPEG/PNG/WebP после уменьшения до файла <= `128 KB` успешно уходит через `TestUploadFreeAvatar`.
|
||||||
|
6. После загрузки приходит `txId`, и аватар сохраняется в профиль как `avatar.ar`.
|
||||||
|
7. Остаток уменьшается: `2`, `1`, `0`.
|
||||||
|
8. На четвёртой попытке сервер отвечает понятной ошибкой про исчерпанный бесплатный лимит.
|
||||||
|
9. Если итоговый уменьшенный файл всё ещё > `128 KB`, UI не отправляет его и показывает понятную ошибку.
|
||||||
|
10. Если серверный Arweave JWK/path не настроен, UI получает понятную ошибку временной функции.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- первые 3 маленькие аватарки загружаются через серверный Arweave-кошелёк;
|
||||||
|
- после каждой успешной загрузки `ava` в профиле указывает на новый `txId`;
|
||||||
|
- после исчерпания лимита дальнейшая бесплатная загрузка блокируется без записи в профиль;
|
||||||
|
- обычная загрузка через свой Arweave-кошелёк продолжает работать отдельно.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
pending
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
# Общий список каналов без stories
|
||||||
|
|
||||||
|
- Краткое описание:
|
||||||
|
вкладка `Каналы` переведена на единый список без разделения на "мои" и "подписки".
|
||||||
|
Название канала в списке теперь показывается как `login_владельца/название_канала`.
|
||||||
|
Служебный канал `stories` скрыт из списка каналов, поиска, подписки и связанных UI-сценариев.
|
||||||
|
|
||||||
|
- Что проверять:
|
||||||
|
1. Открыть вкладку `Каналы`.
|
||||||
|
2. Убедиться, что сразу показывается один общий список.
|
||||||
|
3. Проверить, что свои и чужие каналы отображаются вместе.
|
||||||
|
4. Проверить формат названий: `ownerLogin/channelName`.
|
||||||
|
5. Открыть свой канал и убедиться, что внутри сохраняется UI владельца.
|
||||||
|
6. Открыть чужой канал и убедиться, что внутри сохраняется UI подписчика.
|
||||||
|
7. Проверить, что `stories` не отображается:
|
||||||
|
- в общем списке;
|
||||||
|
- в поиске каналов;
|
||||||
|
- в подписке на канал;
|
||||||
|
- в списках выбора канала для репоста.
|
||||||
|
|
||||||
|
- Ожидаемый результат:
|
||||||
|
- вкладка `Каналы` больше не делится на два режима;
|
||||||
|
- все видимые каналы идут единым списком;
|
||||||
|
- `stories` нигде не виден и не предлагается пользователю;
|
||||||
|
- переход в канал сохраняет корректный UI в зависимости от владельца.
|
||||||
|
|
||||||
|
- Статус:
|
||||||
|
`pending`
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
# Crash-safe запись обычного `AddBlock` через `tmp_bch`
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
Обычный `AddBlock` переведён на схему:
|
||||||
|
|
||||||
|
1. сборка `<blockchainName>.tmp_bch`;
|
||||||
|
2. запись sidecar `<blockchainName>.write_check` с `blockNumber` и `blockHash`;
|
||||||
|
3. создание пустого marker `<blockchainName>.write_pending`;
|
||||||
|
4. SQL-транзакция;
|
||||||
|
5. атомарная подмена `tmp -> main`;
|
||||||
|
6. удаление временных файлов.
|
||||||
|
|
||||||
|
## Что проверить
|
||||||
|
|
||||||
|
1. Обычный `AddBlock` на свежей цепочке.
|
||||||
|
2. Падение до SQL-commit:
|
||||||
|
- должны остаться только временные файлы;
|
||||||
|
- на старте они должны быть удалены.
|
||||||
|
3. Падение после SQL-commit, но до `atomicReplaceBlockchainFile(...)`:
|
||||||
|
- на старте recovery должен довести swap до конца.
|
||||||
|
4. Падение после `atomicReplaceBlockchainFile(...)`, но до удаления marker/sidecar:
|
||||||
|
- на старте recovery должен просто подчистить хвост.
|
||||||
|
5. Сценарий без marker:
|
||||||
|
- `tmp_bch` / `write_check` считаются мусором и удаляются.
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
|
||||||
|
- БД и файловая версия цепочки остаются согласованными.
|
||||||
|
- Повторный старт сервера не ломает chain и не требует ручной правки файлов.
|
||||||
|
- `BlockchainTmpRecoveryOnStartup` корректно обрабатывает и живые остатки, и мусор.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
`pending`
|
||||||
|
|
||||||
|
## Что уже сделано
|
||||||
|
|
||||||
|
- В коде есть `tmp_bch`, `write_check` и `write_pending`.
|
||||||
|
- `BlockchainWriter` пишет обычный `AddBlock` через временные артефакты.
|
||||||
|
- `BlockchainTmpRecoveryOnStartup` умеет добивать или чистить незавершённую запись.
|
||||||
|
|
||||||
|
## Что ещё перепроверить
|
||||||
|
|
||||||
|
- ручной crash-test на тестовом сервере;
|
||||||
|
- совместимость с уже существующими `resync_pending` marker-файлами;
|
||||||
|
- отсутствие ложных срабатываний на старых временных файлах.
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
# Проверка аварийных остановок на разных этапах
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
Нужно отдельно проверить, как сервер восстанавливается после внезапной остановки:
|
||||||
|
|
||||||
|
1. во время обычного `AddBlock` / `tmp_bch`-pipeline;
|
||||||
|
2. во время `full resync` цепочки;
|
||||||
|
3. во время startup recovery, если остановка произошла на предыдущем запуске;
|
||||||
|
4. при обычном апгрейде сервиса без явного crash-сценария.
|
||||||
|
|
||||||
|
## Что проверять
|
||||||
|
|
||||||
|
1. Остановка сервиса до `commit` БД.
|
||||||
|
2. Остановка сервиса после `commit`, но до замены `main.bch`.
|
||||||
|
3. Остановка сервиса во время `BlockchainResyncCleanupDAO`.
|
||||||
|
4. Остановка сервиса во время повторной загрузки цепочки по `GetBlockchainBlock`.
|
||||||
|
5. Поведение при обычном `systemctl restart`, когда сервер сам должен добить recovery.
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
|
||||||
|
- после старта сервер либо дочищает временные артефакты, либо завершает незаконченный `resync`;
|
||||||
|
- не остаётся битых `.tmp_bch`, `.write_check`, `.write_pending`, `.resync_pending`;
|
||||||
|
- БД и файлы цепочки остаются согласованными;
|
||||||
|
- обычная работа сервера не стартует поверх незавершённого recovery.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
`pending`
|
||||||
18
Dev_Docs/Personal_Messages/AGENTS.md
Normal file
18
Dev_Docs/Personal_Messages/AGENTS.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# AGENTS
|
||||||
|
|
||||||
|
## Документация DM в этой папке
|
||||||
|
|
||||||
|
- Основной актуальный документ по личным сообщениям:
|
||||||
|
- `README.md`
|
||||||
|
- Его считать единственным источником истины по текущей реализованной логике DM.
|
||||||
|
|
||||||
|
## Черновик будущих вложений
|
||||||
|
|
||||||
|
- Файл `Черновик_будущих_DM_вложений.md` не является актуальной спецификацией.
|
||||||
|
- В нём описан только ранний черновик того, как когда-то планировались:
|
||||||
|
- формат вложений в DM;
|
||||||
|
- внешние и внутренние поля вложения;
|
||||||
|
- предполагаемая механика загрузки файлов.
|
||||||
|
- Эта схема не была реализована в таком виде и может существенно измениться в будущем.
|
||||||
|
- Любые решения по текущему коду, протоколу и UI нельзя принимать по этому черновику.
|
||||||
|
- Если есть расхождение между `README.md` и черновиком вложений, верным всегда считается `README.md`.
|
||||||
214
Dev_Docs/Personal_Messages/README.md
Normal file
214
Dev_Docs/Personal_Messages/README.md
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
# Личные сообщения (DM)
|
||||||
|
|
||||||
|
## Текущее состояние
|
||||||
|
|
||||||
|
Сейчас в проекте реализованы:
|
||||||
|
|
||||||
|
- новый формат контентных личных сообщений `SHiNE_DM`;
|
||||||
|
- ревизии сообщений через `revisionTimeMs`;
|
||||||
|
- редактирование сообщения через повторную отправку той же логической пары;
|
||||||
|
- удаление сообщения через пустую ревизию;
|
||||||
|
- `upsert` последней версии сообщения на сервере.
|
||||||
|
|
||||||
|
Сейчас в проекте **не реализованы**:
|
||||||
|
|
||||||
|
- вложения в DM;
|
||||||
|
- upload/download файлов для DM;
|
||||||
|
- UI-кнопка прикрепления файла;
|
||||||
|
- серверное хранение файловых связей для DM.
|
||||||
|
|
||||||
|
Черновик будущих вложений вынесен отдельно:
|
||||||
|
|
||||||
|
- `Dev_Docs/Personal_Messages/Черновик_будущих_DM_вложений.md`
|
||||||
|
|
||||||
|
## Общая схема
|
||||||
|
|
||||||
|
Личное сообщение по-прежнему отправляется парой signed-блоков:
|
||||||
|
|
||||||
|
- `type=1` — входящий блок для получателя;
|
||||||
|
- `type=2` — исходящая копия для отправителя.
|
||||||
|
|
||||||
|
Read-receipt пока остаются в legacy-формате:
|
||||||
|
|
||||||
|
- `type=3` — входящее подтверждение прочтения;
|
||||||
|
- `type=4` — исходящая копия подтверждения.
|
||||||
|
|
||||||
|
Ключи сообщения:
|
||||||
|
|
||||||
|
- `baseKey = fromLogin|toLogin|timeMs|nonce`
|
||||||
|
- `messageKey = baseKey|messageType`
|
||||||
|
|
||||||
|
Логический идентификатор письма задаётся парой:
|
||||||
|
|
||||||
|
- `timeMs`
|
||||||
|
- `nonce`
|
||||||
|
|
||||||
|
Эти поля не меняются при редактировании или удалении. Меняется только:
|
||||||
|
|
||||||
|
- `revisionTimeMs`
|
||||||
|
- содержимое `encryptedBody`
|
||||||
|
|
||||||
|
Сервер хранит только последнюю версию записи для каждого `messageKey`.
|
||||||
|
|
||||||
|
## Формат контентного DM: `SHiNE_DM`
|
||||||
|
|
||||||
|
Префикс бинарного блока:
|
||||||
|
|
||||||
|
- `SHiNE_DM`
|
||||||
|
|
||||||
|
Поля идут в big-endian порядке:
|
||||||
|
|
||||||
|
1. `formatVersionMajor` (`u8`) = `1`
|
||||||
|
2. `formatVersionMinor` (`u8`) = `0`
|
||||||
|
3. `toLoginLen` (`u8`) + `toLogin` (ASCII, `1..60`)
|
||||||
|
4. `fromLoginLen` (`u8`) + `fromLogin` (ASCII, `1..60`)
|
||||||
|
5. `timeMs` (`u64`)
|
||||||
|
6. `nonce` (`u32`)
|
||||||
|
7. `messageType` (`u16`) — только `1` или `2`
|
||||||
|
8. `revisionTimeMs` (`u64`)
|
||||||
|
9. `attachmentsCount` (`u8`)
|
||||||
|
10. `encryptedBodyLen` (`u32`)
|
||||||
|
11. `encryptedBody` (`bytes`)
|
||||||
|
12. `signature` (`64 bytes`, Ed25519)
|
||||||
|
|
||||||
|
### Ограничения
|
||||||
|
|
||||||
|
- `attachmentsCount` сейчас всегда должен быть `0`
|
||||||
|
- `encryptedBodyLen` сейчас ограничен сервером до `16384` байт
|
||||||
|
- `revisionTimeMs` не может быть отрицательным
|
||||||
|
|
||||||
|
Если приходит `attachmentsCount != 0`, сервер отклоняет такой DM как:
|
||||||
|
|
||||||
|
- `ATTACHMENTS_DISABLED`
|
||||||
|
|
||||||
|
## Legacy read-receipt: `SHiNE_dm2`
|
||||||
|
|
||||||
|
Подтверждения прочтения `type=3/4` пока используют старый контейнер `SHiNE_dm2`:
|
||||||
|
|
||||||
|
1. `toLoginLen` (`u8`) + `toLogin`
|
||||||
|
2. `fromLoginLen` (`u8`) + `fromLogin`
|
||||||
|
3. `timeMs` (`u64`)
|
||||||
|
4. `nonce` (`u32`)
|
||||||
|
5. `messageType` (`u16`) — `3` или `4`
|
||||||
|
6. `payloadLen` (`u16`)
|
||||||
|
7. `payloadBytes`
|
||||||
|
8. `signature`
|
||||||
|
|
||||||
|
## Редактирование
|
||||||
|
|
||||||
|
Редактирование делается новой отправкой той же логической пары сообщения:
|
||||||
|
|
||||||
|
- `timeMs` и `nonce` остаются теми же;
|
||||||
|
- `messageType` остаётся `1/2`;
|
||||||
|
- `revisionTimeMs` становится больше;
|
||||||
|
- `encryptedBody` содержит новую версию текста.
|
||||||
|
|
||||||
|
Если на сервер приходит более старая ревизия, она игнорируется.
|
||||||
|
|
||||||
|
Если приходит та же ревизия и тот же бинарный блок, сервер тоже её не применяет повторно.
|
||||||
|
|
||||||
|
## Удаление
|
||||||
|
|
||||||
|
Удаление личного сообщения делается как новая ревизия того же сообщения:
|
||||||
|
|
||||||
|
- `timeMs` и `nonce` остаются прежними;
|
||||||
|
- `revisionTimeMs` увеличивается;
|
||||||
|
- `attachmentsCount = 0`;
|
||||||
|
- `encryptedBodyLen = 0`;
|
||||||
|
- `encryptedBody` пустой.
|
||||||
|
|
||||||
|
В UI такое сообщение не показывается.
|
||||||
|
|
||||||
|
На сервере это не отдельный тип сообщения, а просто последняя пустая ревизия того же `messageKey`.
|
||||||
|
|
||||||
|
## Поведение сервера
|
||||||
|
|
||||||
|
Для контентных DM сервер:
|
||||||
|
|
||||||
|
1. принимает пару signed-блоков `type=1/2`;
|
||||||
|
2. валидирует формат, подпись и совпадение ключевых полей пары;
|
||||||
|
3. проверяет, что для обеих сторон пары совпадают:
|
||||||
|
- `fromLogin`
|
||||||
|
- `toLogin`
|
||||||
|
- `timeMs`
|
||||||
|
- `nonce`
|
||||||
|
- `revisionTimeMs`
|
||||||
|
- `encryptedBody`
|
||||||
|
4. делает `upsert` последней версии в `signed_messages_v2`;
|
||||||
|
5. сбрасывает pending-доставку по сессиям для новой ревизии;
|
||||||
|
6. рассылает актуальную версию адресатам через `SignedMessageArrived`.
|
||||||
|
|
||||||
|
История старых ревизий сейчас не хранится отдельно: в таблице остаётся только последняя версия по каждому `messageKey`.
|
||||||
|
|
||||||
|
## Хранение в БД
|
||||||
|
|
||||||
|
Основная таблица:
|
||||||
|
|
||||||
|
- `signed_messages_v2`
|
||||||
|
|
||||||
|
Для контентных DM в ней используются:
|
||||||
|
|
||||||
|
- `message_key`
|
||||||
|
- `base_key`
|
||||||
|
- `target_login`
|
||||||
|
- `from_login`
|
||||||
|
- `to_login`
|
||||||
|
- `time_ms`
|
||||||
|
- `nonce`
|
||||||
|
- `message_type`
|
||||||
|
- `revision_time_ms`
|
||||||
|
- `raw_block`
|
||||||
|
- `created_at_ms`
|
||||||
|
|
||||||
|
Отдельных таблиц файлов для DM сейчас нет.
|
||||||
|
|
||||||
|
## События и доставка
|
||||||
|
|
||||||
|
Запрос на отправку по WebSocket остаётся прежним:
|
||||||
|
|
||||||
|
- `SendMessagePair`
|
||||||
|
- `ReceiveOutcomingMessage` как алиас
|
||||||
|
|
||||||
|
Клиент отправляет:
|
||||||
|
|
||||||
|
- `incomingBlobB64`
|
||||||
|
- `outgoingBlobB64`
|
||||||
|
|
||||||
|
Событие в активные сессии:
|
||||||
|
|
||||||
|
- `SignedMessageArrived`
|
||||||
|
|
||||||
|
Если пришла новая ревизия того же сообщения, `messageKey` остаётся прежним, а внутри `blobB64` будет более новый `revisionTimeMs`.
|
||||||
|
|
||||||
|
Подтверждение доставки в сессию:
|
||||||
|
|
||||||
|
- `AckSessionDelivery`
|
||||||
|
|
||||||
|
WebPush и локальные уведомления сейчас работают так:
|
||||||
|
|
||||||
|
- для активной онлайн-сессии приоритет у доставки по WebSocket через `SignedMessageArrived`;
|
||||||
|
- если целевая сессия не онлайн по WebSocket, сервер может отправить WebPush с `kind=new_message`;
|
||||||
|
- если вкладка/приложение живы, но страница скрыта (`document.visibilityState !== visible`), UI дополнительно пытается показать системное уведомление через `service worker`;
|
||||||
|
- для активной видимой страницы UI проигрывает короткий локальный сигнал на каждое новое входящее DM, если браузер ранее разрешил аудио-контекст после пользовательского жеста;
|
||||||
|
- для скрытой, но живой страницы UI также делает `best effort` сигнал через `vibrate()` и более длинный локальный звук;
|
||||||
|
- эти локальные сигналы не гарантируются браузером: на мобильных устройствах они зависят от политики Chrome/Android/iOS.
|
||||||
|
|
||||||
|
## Правила UI
|
||||||
|
|
||||||
|
UI сейчас работает так:
|
||||||
|
|
||||||
|
- показывает только текст `encryptedBody`;
|
||||||
|
- умеет обновлять уже существующее сообщение по тому же `messageKey`;
|
||||||
|
- не показывает удалённые сообщения;
|
||||||
|
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
|
||||||
|
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
|
||||||
|
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
|
||||||
|
- на видимом экране чата/приложения проигрывает короткий локальный звук на новое входящее DM;
|
||||||
|
- при входящем DM для скрытой, но ещё живой страницы пытается поднять системное уведомление через `service worker`;
|
||||||
|
- не показывает и не принимает вложения.
|
||||||
|
|
||||||
|
## Что обязательно помнить
|
||||||
|
|
||||||
|
- вложения в DM сейчас отключены на уровне протокола и UI;
|
||||||
|
- любые старые описания `/f/...`, `/upload` и файловых таблиц для DM больше не актуальны;
|
||||||
|
- если позже вложения вернутся, их формат и серверная логика могут быть другими.
|
||||||
73
Dev_Docs/Personal_Messages/Черновик_будущих_DM_вложений.md
Normal file
73
Dev_Docs/Personal_Messages/Черновик_будущих_DM_вложений.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Черновик будущих вложений в DM
|
||||||
|
|
||||||
|
## Важно
|
||||||
|
|
||||||
|
Этот документ описывает только ранний черновик идеи.
|
||||||
|
|
||||||
|
Сейчас в проекте **нет** поддержки вложений в личных сообщениях:
|
||||||
|
|
||||||
|
- в реализованном формате `SHiNE_DM` поле `attachmentsCount` пока всегда должно быть `0`;
|
||||||
|
- UI не показывает кнопку прикрепления файлов;
|
||||||
|
- сервер не принимает upload файлов для DM;
|
||||||
|
- сервер не раздаёт специальные DM-файлы по отдельным endpoints;
|
||||||
|
- сервер не хранит отдельные файловые связи для личных сообщений.
|
||||||
|
|
||||||
|
Этот документ нужен только для того, чтобы рядом с актуальной документацией было явно видно:
|
||||||
|
|
||||||
|
- какие идеи обсуждались;
|
||||||
|
- что это **не реализовано**;
|
||||||
|
- что формат, хранение и способ загрузки потом могут сильно измениться.
|
||||||
|
|
||||||
|
## Что обсуждалось
|
||||||
|
|
||||||
|
Рассматривался такой общий подход:
|
||||||
|
|
||||||
|
- у контентного DM есть внешний список вложений;
|
||||||
|
- во внешнем формате лежат только технические данные;
|
||||||
|
- человекочитаемые данные о файле живут внутри зашифрованного тела сообщения;
|
||||||
|
- один и тот же blob-файл теоретически мог бы переиспользоваться в нескольких сообщениях.
|
||||||
|
|
||||||
|
Черновой вариант внешнего списка:
|
||||||
|
|
||||||
|
- `attachmentsCount`
|
||||||
|
- далее для каждого вложения:
|
||||||
|
- `encFileHashSHA256` (`32 bytes`)
|
||||||
|
- `encFileSize` (`u64`)
|
||||||
|
|
||||||
|
Черновой вариант внутреннего маркера в тексте:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<<file:file-format(1.0):type|fileName|origSize|origHashB64u|encHashB64u|encSize|keyB64u|nonceB64u>>
|
||||||
|
```
|
||||||
|
|
||||||
|
Где обсуждались поля:
|
||||||
|
|
||||||
|
- `type`
|
||||||
|
- `fileName`
|
||||||
|
- `origSize`
|
||||||
|
- `origHashB64u`
|
||||||
|
- `encHashB64u`
|
||||||
|
- `encSize`
|
||||||
|
- `keyB64u`
|
||||||
|
- `nonceB64u`
|
||||||
|
|
||||||
|
## Что может измениться
|
||||||
|
|
||||||
|
В будущем могут измениться любые части идеи:
|
||||||
|
|
||||||
|
- сам бинарный формат;
|
||||||
|
- способ привязки файлов к сообщению;
|
||||||
|
- момент загрузки файла относительно отправки сообщения;
|
||||||
|
- серверное хранение blob-файлов;
|
||||||
|
- права доступа к скачиванию;
|
||||||
|
- способ рендера вложения в UI.
|
||||||
|
|
||||||
|
Именно поэтому этот файл не надо воспринимать как актуальную спецификацию.
|
||||||
|
|
||||||
|
## Источник истины на сейчас
|
||||||
|
|
||||||
|
Актуальное состояние личных сообщений описано только в:
|
||||||
|
|
||||||
|
- `Dev_Docs/Personal_Messages/README.md`
|
||||||
|
|
||||||
|
Если между этим черновиком и основным README есть расхождение, верным считается `README.md`.
|
||||||
482
Dev_Docs/Solana/user_pda/README.md
Normal file
482
Dev_Docs/Solana/user_pda/README.md
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
# Solana user_pda: итоговый целевой формат пользовательской записи
|
||||||
|
|
||||||
|
Документ описывает целевой формат пользовательской PDA-записи `user_pda` для Solana-программы `shine_users`.
|
||||||
|
|
||||||
|
Это не формат основного блокчейна SHiNE и не документация по `AddBlock`. Основной блокчейн SHiNE описан отдельно в `Dev_Docs/Blockchain/`.
|
||||||
|
|
||||||
|
Статус документа: итоговый согласованный формат, к которому приведены `create_user_pda`, `update_user_pda` и тестовый сериализатор Solana-модуля.
|
||||||
|
|
||||||
|
## 1. Назначение user_pda
|
||||||
|
|
||||||
|
`user_pda` хранит публичное состояние пользователя в Solana:
|
||||||
|
|
||||||
|
- логин пользователя;
|
||||||
|
- неизменяемые параметры создания записи;
|
||||||
|
- публичный recovery-ключ пользователя;
|
||||||
|
- корневой публичный ключ пользователя;
|
||||||
|
- клиентский публичный ключ пользователя;
|
||||||
|
- данные одного или нескольких пользовательских блокчейнов SHiNE;
|
||||||
|
- серверные данные пользователя, если пользователь выступает сервером;
|
||||||
|
- серверы доступа пользователя;
|
||||||
|
- счетчики/лимиты;
|
||||||
|
- подпись записи.
|
||||||
|
|
||||||
|
На первом этапе поддерживается один пользовательский блокчейн SHiNE, но формат блока блокчейна сразу допускает повторение таких блоков в будущем.
|
||||||
|
|
||||||
|
## 2. Адрес PDA
|
||||||
|
|
||||||
|
Адрес пользовательской PDA вычисляется по логину:
|
||||||
|
|
||||||
|
- seed prefix: `user_login=`;
|
||||||
|
- второй seed: нормализованный логин в нижнем регистре;
|
||||||
|
- program id: программа `shine_users`.
|
||||||
|
|
||||||
|
Один логин соответствует одной `user_pda`.
|
||||||
|
|
||||||
|
## 2.1. Кто оплачивает create/update PDA
|
||||||
|
|
||||||
|
- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `client_key`.
|
||||||
|
- `root_key` используется для подписи unsigned части записи через Ed25519 instruction и не является fee payer.
|
||||||
|
- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `client_key`.
|
||||||
|
|
||||||
|
## 3. Общие правила кодирования
|
||||||
|
|
||||||
|
- Числа кодируются в Little Endian.
|
||||||
|
- `u8`, `u16`, `u32`, `u64` имеют обычный фиксированный размер.
|
||||||
|
- Публичный ключ Solana/Ed25519: 32 байта.
|
||||||
|
- Ed25519-подпись: 64 байта.
|
||||||
|
- SHA-256/Solana hash: 32 байта.
|
||||||
|
- Строка переменной длины: `len: u8` + `bytes[len]` в UTF-8.
|
||||||
|
- Arweave `tx_id`: строка переменной длины. Ожидаемая практическая длина base64url tx id - 43 байта, но формат хранит длину явно.
|
||||||
|
- Все типизированные блоки после фиксированного заголовка начинаются с `block_type: u8` и `block_version: u8`.
|
||||||
|
- Отдельный `block_len` у типизированных блоков не хранится: блоки парсятся по известным полям, счетчикам и строкам с `len: u8`.
|
||||||
|
|
||||||
|
## 4. Верхний формат записи
|
||||||
|
|
||||||
|
Первые 9 полей фиксированы и идут строго в указанном порядке. Это общий заголовок записи.
|
||||||
|
|
||||||
|
| N | Поле | Тип | Размер | Правило |
|
||||||
|
|---|------|-----|--------|---------|
|
||||||
|
| 1 | `magic` | bytes | 5 | Всегда `SHiNE`. |
|
||||||
|
| 2 | `format_major` | `u8` | 1 | Для первого формата: `1`. |
|
||||||
|
| 3 | `format_minor` | `u8` | 1 | Для первой версии нового формата: `0`. |
|
||||||
|
| 4 | `record_len` | `u16` | 2 | Длина полезной записи от `magic` до `signature` включительно, без padding. |
|
||||||
|
| 5 | `created_at_ms` | `u64` | 8 | Время создания записи, Unix time в миллисекундах. Не меняется. |
|
||||||
|
| 6 | `updated_at_ms` | `u64` | 8 | Время последнего обновления записи. |
|
||||||
|
| 7 | `record_number` | `u32` | 4 | Номер версии записи пользователя. При создании `0`, при обновлении +1. |
|
||||||
|
| 8 | `prev_record_hash` | bytes | 32 | Хэш unsigned-части предыдущей записи. При создании 32 нулевых байта. |
|
||||||
|
| 9 | `login` | string | `1 + len` | Логин пользователя. Не меняется. |
|
||||||
|
|
||||||
|
После первых 9 полей идет набор типизированных блоков:
|
||||||
|
|
||||||
|
```text
|
||||||
|
UserPdaRecordV1
|
||||||
|
- fixed_header: поля 1..9
|
||||||
|
- blocks_count: u8
|
||||||
|
- blocks: TypedBlock[blocks_count]
|
||||||
|
- signature: [u8; 64]
|
||||||
|
- padding: bytes до размера PDA, если нужен
|
||||||
|
```
|
||||||
|
|
||||||
|
`blocks_count` входит в unsigned-часть записи и подписывается.
|
||||||
|
|
||||||
|
## 5. Типы блоков
|
||||||
|
|
||||||
|
Зарезервированные значения `block_type`:
|
||||||
|
|
||||||
|
| block_type | Блок | Назначение |
|
||||||
|
|------------|------|------------|
|
||||||
|
| `0` | `RecoveryKeyBlock` | Ключ восстановления пользователя. |
|
||||||
|
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
|
||||||
|
| `2` | `ClientKeyBlock` | Клиентский ключ пользователя. |
|
||||||
|
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
||||||
|
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
||||||
|
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
||||||
|
| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и homeserver-ы. |
|
||||||
|
| `70` | `TrustedStateBlock` | Счетчик trusted-связей. |
|
||||||
|
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
|
||||||
|
- обязательные блоки: `RecoveryKeyBlock`, `RootKeyBlock`, `ClientKeyBlock`, `BlockchainRegistryBlock`;
|
||||||
|
- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `SessionsBlock`, `TrustedStateBlock`;
|
||||||
|
- каждый обязательный блок должен встречаться ровно один раз;
|
||||||
|
- порядок блоков в записи фиксируется для простоты проверки:
|
||||||
|
`RecoveryKey`, `RootKey`, `ClientKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`.
|
||||||
|
|
||||||
|
## 6. RecoveryKeyBlock
|
||||||
|
|
||||||
|
Recovery-ключ нужен для будущих сценариев восстановления и ротации остальных ключей. В текущей версии он только публикуется в записи и не меняется через обычный `update_user_pda`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
RecoveryKeyBlock
|
||||||
|
- block_type: u8 = 0
|
||||||
|
- block_version: u8 = 0
|
||||||
|
- recovery_key: [u8; 32]
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- при создании задается публичный recovery-ключ пользователя;
|
||||||
|
- при обновлении `recovery_key` должен совпадать с предыдущей записью;
|
||||||
|
- приватный `recovery.key` в PDA не хранится;
|
||||||
|
- отдельная ротация recovery-ключа будет отдельным форматом/сценарием в будущем.
|
||||||
|
|
||||||
|
## 7. RootKeyBlock
|
||||||
|
|
||||||
|
Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
RootKeyBlock
|
||||||
|
- block_type: u8 = 1
|
||||||
|
- block_version: u8 = 0
|
||||||
|
- root_key: [u8; 32]
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- при создании задается корневой публичный ключ пользователя;
|
||||||
|
- при обновлении `root_key` должен совпадать с предыдущей записью;
|
||||||
|
- ротация root-key будет отдельным форматом/сценарием в будущем.
|
||||||
|
|
||||||
|
## 8. ClientKeyBlock
|
||||||
|
|
||||||
|
Смена `client_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один клиентский ключ пользователя.
|
||||||
|
|
||||||
|
```text
|
||||||
|
ClientKeyBlock
|
||||||
|
- block_type: u8 = 2
|
||||||
|
- block_version: u8 = 0
|
||||||
|
- client_key: [u8; 32]
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- при создании задается текущий клиентский публичный ключ пользователя;
|
||||||
|
- при обновлении `client_key` должен совпадать с предыдущей записью;
|
||||||
|
- история устройств и несколько клиентских ключей в этом формате не хранятся.
|
||||||
|
|
||||||
|
## 9. BlockchainRegistryBlock
|
||||||
|
|
||||||
|
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
|
||||||
|
|
||||||
|
```text
|
||||||
|
BlockchainRegistryBlock
|
||||||
|
- block_type: u8 = 3
|
||||||
|
- block_version: u8 = 0
|
||||||
|
- blockchain_count: u8
|
||||||
|
- blockchain_records: BlockchainRecord[blockchain_count]
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- на первом этапе `blockchain_count = 1`;
|
||||||
|
- в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`;
|
||||||
|
- каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн.
|
||||||
|
|
||||||
|
## 10. BlockchainRecord
|
||||||
|
|
||||||
|
```text
|
||||||
|
BlockchainRecord
|
||||||
|
- blockchain_type: u8
|
||||||
|
- blockchain_name: string
|
||||||
|
- blockchain_public_key: [u8; 32]
|
||||||
|
- paid_limit_bytes: u64
|
||||||
|
- used_bytes: u64
|
||||||
|
- last_block_number: u32
|
||||||
|
- last_block_hash: [u8; 32]
|
||||||
|
- last_block_signature: [u8; 64]
|
||||||
|
- arweave_present: u8
|
||||||
|
- arweave_tx_id: string, только если arweave_present = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
`blockchain_type`:
|
||||||
|
|
||||||
|
| Значение | Смысл |
|
||||||
|
|----------|-------|
|
||||||
|
| `1` | Основной пользовательский SHiNE-блокчейн. |
|
||||||
|
|
||||||
|
Поля:
|
||||||
|
|
||||||
|
- `blockchain_name` - строковое имя пользовательского блокчейна, например `login-001`. На первом этапе для основного блокчейна пользователя используется имя вида `<login>-001`, потому что это первый блокчейн этого пользователя.
|
||||||
|
- `blockchain_public_key` - публичный ключ блокчейна пользователя.
|
||||||
|
- `paid_limit_bytes` - оплаченный лимит хранения/записей в байтах.
|
||||||
|
- `used_bytes` - сколько байт уже занято в пользовательском SHiNE-блокчейне.
|
||||||
|
- `last_block_number` - номер последнего известного блока пользовательского блокчейна.
|
||||||
|
- `last_block_hash` - хэш последнего известного блока.
|
||||||
|
- `last_block_signature` - подпись хэша специального сообщения о вершине блокчейна ключом `blockchain_public_key`.
|
||||||
|
- `arweave_present` - `0`, если ссылки нет; `1`, если ссылка есть.
|
||||||
|
- `arweave_tx_id` - Arweave transaction id, где лежит выгруженный пользовательский канал/состояние.
|
||||||
|
|
||||||
|
Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
|
||||||
|
|
||||||
|
## 11. Правила обновления BlockchainRecord
|
||||||
|
|
||||||
|
При обновлении записи:
|
||||||
|
|
||||||
|
- `blockchain_type` для существующей записи не меняется;
|
||||||
|
- `blockchain_public_key` пока не ротируется автоматически; смена ключа требует отдельного согласованного сценария;
|
||||||
|
- `paid_limit_bytes` может только увеличиваться или оставаться прежним;
|
||||||
|
- при увеличении `paid_limit_bytes` пользователь платит комиссию в Solana по тарифам программы;
|
||||||
|
- `used_bytes` может только увеличиваться или оставаться прежним;
|
||||||
|
- `last_block_number` может только увеличиваться или оставаться прежним;
|
||||||
|
- `used_bytes <= paid_limit_bytes`;
|
||||||
|
- если `last_block_number` увеличился, то должны быть переданы новый `last_block_hash` и новая `last_block_signature`;
|
||||||
|
- `last_block_signature` проверяется через Ed25519-инструкцию Solana: подпись должна соответствовать хэшу сообщения `LastBlockState` и `blockchain_public_key`;
|
||||||
|
- в транзакции `create_user_pda` / `update_user_pda` две Ed25519-инструкции должны идти непосредственно перед вызовом `shine_users`: сначала подпись `root_key`, затем подпись `blockchain_public_key`;
|
||||||
|
- `arweave_tx_id` можно добавить или заменить на новый, если пользователь выгрузил более актуальное состояние в Arweave;
|
||||||
|
- уменьшать лимит, число блоков или занятый размер нельзя.
|
||||||
|
|
||||||
|
Сообщение `LastBlockState`, которое хэшируется и подписывается ключом `blockchain_public_key`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LastBlockState
|
||||||
|
- constant: bytes = "SHiNE_LAST_BLOCK"
|
||||||
|
- login: string
|
||||||
|
- blockchain_name: string
|
||||||
|
- last_block_number: u32
|
||||||
|
- last_block_hash: [u8; 32]
|
||||||
|
- used_bytes: u64
|
||||||
|
```
|
||||||
|
|
||||||
|
Алгоритм:
|
||||||
|
|
||||||
|
```text
|
||||||
|
message = SHA-256(LastBlockState bytes)
|
||||||
|
last_block_signature = Ed25519(blockchain_public_key, message)
|
||||||
|
```
|
||||||
|
|
||||||
|
Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
|
||||||
|
|
||||||
|
## 12. ServerProfileBlock
|
||||||
|
|
||||||
|
Блок присутствует, если пользователь выступает сервером.
|
||||||
|
|
||||||
|
```text
|
||||||
|
ServerProfileBlock
|
||||||
|
- block_type: u8 = 30
|
||||||
|
- block_version: u8 = 0
|
||||||
|
- is_server: u8
|
||||||
|
- address_format_type: u8, только если is_server = 1
|
||||||
|
- address_format_version: u8, только если is_server = 1
|
||||||
|
- server_address: string, только если is_server = 1
|
||||||
|
- sync_servers_count: u8, только если is_server = 1
|
||||||
|
- sync_servers: string[sync_servers_count], только если is_server = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- `is_server = 0` означает, что серверных данных нет;
|
||||||
|
- `is_server = 1` означает, что пользователь публикует серверный профиль;
|
||||||
|
- `address_format_type` — тип формата адреса сервера: `1` = URL-строка (например `https://shineup.me/ws`);
|
||||||
|
- `address_format_version` — версия формата адреса, сейчас `0`;
|
||||||
|
- `sync_servers_count` максимум `32`;
|
||||||
|
- `server_address` - строковый адрес сервера в соответствии с `address_format_type`;
|
||||||
|
- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
|
||||||
|
|
||||||
|
## 13. AccessServersBlock
|
||||||
|
|
||||||
|
Блок хранит серверы доступа/relay для пользователя.
|
||||||
|
|
||||||
|
```text
|
||||||
|
AccessServersBlock
|
||||||
|
- block_type: u8 = 40
|
||||||
|
- block_version: u8 = 0
|
||||||
|
- access_servers_count: u8
|
||||||
|
- access_servers: string[access_servers_count]
|
||||||
|
```
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- блок может отсутствовать, если серверы доступа не заданы;
|
||||||
|
- список может обновляться при изменении маршрутизации пользователя;
|
||||||
|
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
|
||||||
|
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
|
||||||
|
|
||||||
|
## 14. SessionsBlock
|
||||||
|
|
||||||
|
Блок хранит опубликованные пользовательские сессии. На текущем этапе регистрация пользователя не добавляет туда записи автоматически, поэтому стандартный create/update продолжает работать с пустым списком.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SessionsBlock
|
||||||
|
- block_type: u8 = 50
|
||||||
|
- block_version: u8 = 0
|
||||||
|
- sessions_mode: u8
|
||||||
|
- sessions_count: u8
|
||||||
|
- sessions: SessionRecord[sessions_count]
|
||||||
|
```
|
||||||
|
|
||||||
|
`sessions_mode`:
|
||||||
|
|
||||||
|
| Значение | Смысл |
|
||||||
|
|----------|-------|
|
||||||
|
| `1` | Можно использовать и сессии, зарегистрированные в PDA, и сессии, созданные вне PDA. |
|
||||||
|
| `10` | Зарезервировано на будущее: можно использовать только сессии, опубликованные в PDA. |
|
||||||
|
|
||||||
|
Сейчас рабочий режим по умолчанию: `sessions_mode = 1`. Серверная логика пока не реализует особое поведение для `10`; это задел под будущее расширение.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SessionRecord
|
||||||
|
- session_type: u8
|
||||||
|
- session_version: u8
|
||||||
|
- session_name: string
|
||||||
|
- session_pub_key: [u8; 32]
|
||||||
|
```
|
||||||
|
|
||||||
|
`session_type`:
|
||||||
|
|
||||||
|
| Значение | Смысл |
|
||||||
|
|----------|-------|
|
||||||
|
| `1` | Обычная пользовательская сессия. |
|
||||||
|
| `50` | Кошелёк пользователя. |
|
||||||
|
| `100` | Homeserver пользователя. |
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- максимум `64` записей на пользователя;
|
||||||
|
- `session_name` не пустой, максимум `64` байта;
|
||||||
|
- `session_name` может содержать только символы `[A-Za-z0-9_]`;
|
||||||
|
- `session_version` сейчас должна быть равна `1`;
|
||||||
|
- внутри одного блока должны быть уникальны и `session_name`, и `session_pub_key`;
|
||||||
|
- на текущем этапе UI и регистрация не обязаны добавлять туда записи автоматически.
|
||||||
|
|
||||||
|
## 15. TrustedStateBlock
|
||||||
|
|
||||||
|
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
|
||||||
|
|
||||||
|
```text
|
||||||
|
TrustedStateBlock
|
||||||
|
- block_type: u8 = 70
|
||||||
|
- block_version: u8 = 0
|
||||||
|
- trusted_count: u8 = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
|
||||||
|
|
||||||
|
## 16. Подпись user_pda
|
||||||
|
|
||||||
|
Подписывается не вся PDA целиком, а unsigned-часть записи:
|
||||||
|
|
||||||
|
- от `magic` до последнего байта последнего типизированного блока включительно;
|
||||||
|
- включая `record_len`, `blocks_count`, все заголовки блоков и тела блоков;
|
||||||
|
- без поля `signature`;
|
||||||
|
- без padding.
|
||||||
|
|
||||||
|
Алгоритм:
|
||||||
|
|
||||||
|
```text
|
||||||
|
message = hash(unsigned_record_bytes)
|
||||||
|
signature = Ed25519(root_key, message)
|
||||||
|
```
|
||||||
|
|
||||||
|
Solana-программа проверяет подпись через встроенную Ed25519-инструкцию. Подписантом должен быть `root_key` из `RootKeyBlock`.
|
||||||
|
Для `shine_users` эта инструкция должна стоять в транзакции сразу перед Ed25519-инструкцией `last_block_signature` и непосредственно перед самой `create/update`-инструкцией программы.
|
||||||
|
|
||||||
|
Смену формата подписи сейчас не трогаем.
|
||||||
|
|
||||||
|
## 17. Регистрация пользователя
|
||||||
|
|
||||||
|
При регистрации:
|
||||||
|
|
||||||
|
- PDA еще не должна существовать;
|
||||||
|
- логин проходит проверку формата и login guard;
|
||||||
|
- `record_number = 0`;
|
||||||
|
- `prev_record_hash = 0x00...00`;
|
||||||
|
- `created_at_ms = updated_at_ms`;
|
||||||
|
- обязательные блоки присутствуют;
|
||||||
|
- создается минимум один `BlockchainRecord`;
|
||||||
|
- новый `SessionsBlock` может присутствовать, но при обычной регистрации сейчас записывается пустой список с `sessions_mode = 1`;
|
||||||
|
- стартовый `paid_limit_bytes` равен стартовому бонусу плюс оплаченный дополнительный лимит;
|
||||||
|
- `used_bytes <= paid_limit_bytes`;
|
||||||
|
- пользователь платит регистрационную комиссию;
|
||||||
|
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
|
||||||
|
- вся unsigned-часть записи подписана `root_key`.
|
||||||
|
|
||||||
|
## 18. Обновление пользователя
|
||||||
|
|
||||||
|
При обновлении:
|
||||||
|
|
||||||
|
- PDA должна существовать;
|
||||||
|
- `login`, `created_at_ms`, `recovery_key`, `root_key`, `client_key` не меняются;
|
||||||
|
- `record_number = previous_record_number + 1`;
|
||||||
|
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
|
||||||
|
- `updated_at_ms` обновляется;
|
||||||
|
- unsigned-часть новой записи подписана `root_key`;
|
||||||
|
- лимиты блокчейнов могут только увеличиваться;
|
||||||
|
- занятый размер и номер последнего блока не могут уменьшаться;
|
||||||
|
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
|
||||||
|
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
|
||||||
|
|
||||||
|
## 19. Отличия от старого линейного формата
|
||||||
|
|
||||||
|
Старый формат после `login` хранил поля линейно:
|
||||||
|
|
||||||
|
- `root_key_status`;
|
||||||
|
- `root_key`;
|
||||||
|
- `blockchain_key_status`;
|
||||||
|
- `blockchain_key`;
|
||||||
|
- `client_key_status`;
|
||||||
|
- `client_key`;
|
||||||
|
- `chain_number`;
|
||||||
|
- `balance`;
|
||||||
|
- серверные поля;
|
||||||
|
- access-серверы;
|
||||||
|
- `trusted_count`;
|
||||||
|
- `reserved`;
|
||||||
|
- `signature`.
|
||||||
|
|
||||||
|
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
|
||||||
|
|
||||||
|
- recovery-ключ становится отдельным обязательным блоком;
|
||||||
|
- ключи становятся отдельными блоками;
|
||||||
|
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
|
||||||
|
- серверные данные и access-серверы отделяются от данных блокчейна;
|
||||||
|
- расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи.
|
||||||
|
|
||||||
|
## 20. Деривация ключей из master secret
|
||||||
|
|
||||||
|
Сама Solana-программа не вычисляет ключи из секрета и не хранит приватные ключи. Но текущая согласованная клиентская схема деривации для публичной версии формата фиксируется здесь как reference для UI/ESP32/внешних клиентов.
|
||||||
|
|
||||||
|
Базовая формула:
|
||||||
|
|
||||||
|
```text
|
||||||
|
seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || suffix_utf8)
|
||||||
|
```
|
||||||
|
|
||||||
|
Где:
|
||||||
|
|
||||||
|
- `master_secret32` — 32-байтовый master secret пользователя;
|
||||||
|
- `suffix_utf8` — строка назначения ключа.
|
||||||
|
|
||||||
|
Согласованные suffix:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"recovery.key"
|
||||||
|
"root.key"
|
||||||
|
"blockchain.key"
|
||||||
|
"client.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
Соответствие:
|
||||||
|
|
||||||
|
```text
|
||||||
|
recovery.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "recovery.key")
|
||||||
|
root.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "root.key")
|
||||||
|
blockchain.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "blockchain.key")
|
||||||
|
client.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "client.key")
|
||||||
|
```
|
||||||
|
|
||||||
|
Далее каждая строка `seed` интерпретируется off-chain как `seed32` для отдельной пары Ed25519.
|
||||||
|
|
||||||
|
## 21. Что пока не входит в формат
|
||||||
|
|
||||||
|
Пока не проектируем:
|
||||||
|
|
||||||
|
- ротацию `recovery_key`;
|
||||||
|
- ротацию `root_key`;
|
||||||
|
- сложную ротацию `client_key`;
|
||||||
|
- ротацию `blockchain_public_key`;
|
||||||
|
- проверку содержимого Arweave transaction;
|
||||||
|
- хранение полной истории пользовательского блокчейна внутри Solana;
|
||||||
|
- подключение Solana-модуля к сборке/деплою основного сервера SHiNE.
|
||||||
166
Dev_Docs/Solana_Architecture/README.md
Normal file
166
Dev_Docs/Solana_Architecture/README.md
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
# Архитектура Solana-программ SHiNE
|
||||||
|
|
||||||
|
Документ описывает рабочую архитектуру Solana-части SHiNE: три Anchor-программы, DAO, ключи управления, PDA-счета и движение денег.
|
||||||
|
|
||||||
|
Это архитектурная справка. Она не меняет код, формат PDA-записи пользователя, серверный API или формат блокчейна SHiNE.
|
||||||
|
|
||||||
|
Статус: актуализировано по коду `shine-solana/shine/programs/*` на 2026-05-25.
|
||||||
|
|
||||||
|
Связанные документы:
|
||||||
|
|
||||||
|
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` — single source of truth по деплою и первичной инициализации регистрации пользователей.
|
||||||
|
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` — точный формат `user_pda` для `shine_users`.
|
||||||
|
- `shine-solana/shine/doc/FUNDS_FLOW.md` — короткая справка по денежным потокам внутри Solana-модуля.
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
В Solana-модуле сейчас три основные программы:
|
||||||
|
|
||||||
|
1. `shine_login_guard` — проверяет логин и возвращает класс логина: обычный, premium или trademark.
|
||||||
|
2. `shine_users` — создает и обновляет пользовательскую PDA-запись, проверяет подписи и берет оплату за регистрацию/увеличение лимита.
|
||||||
|
3. `shine_payments` — принимает входящий поток средств в `inflow_vault`, ведет очереди тикетов, позволяет DAO выдавать лимиты менеджерам и выполняет выплаты.
|
||||||
|
|
||||||
|
DAO в текущем виде не является отдельной Anchor-программой SHiNE внутри `programs/`. Это управляющая модель поверх кошельков, governance-скриптов и authority-адресов. Для проектирования ее удобно считать отдельным управляющим блоком: DAO голосует, назначает управляющие ключи, управляет казной и вызывает защищенные методы второй и третьей программ.
|
||||||
|
|
||||||
|
## Общая схема
|
||||||
|
|
||||||
|
Редактируемая Mermaid-схема находится в [schemes/architecture.mmd](schemes/architecture.mmd).
|
||||||
|
|
||||||
|
Картинки:
|
||||||
|
|
||||||
|
- [schemes/architecture.svg](schemes/architecture.svg)
|
||||||
|
- [schemes/architecture.png](schemes/architecture.png)
|
||||||
|
|
||||||
|
## Программы и функции
|
||||||
|
|
||||||
|
| Блок | Папка/имя | Текущие функции из кода | Основной смысл |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | `shine_login_guard` | `classify_login` | Проверка логина перед регистрацией. |
|
||||||
|
| 2 | `shine_users` | `init_users_economy_config`, `update_users_economy_config`, `create_user_pda`, `update_user_pda` | Регистрация пользователя, обновление записи, экономика лимита. |
|
||||||
|
| 3 | `shine_payments` | `init`, `update_coef_limit`, `grant_manager_limits`, `buy_ticket`, `buy_ticket_usd`, `buy_ticket_sol`, `manager_add_ticket`, `step_payout`, `change_ticket_recipient` | Vault, билеты, очереди, выплаты, DAO-настройки, лимиты менеджеров. |
|
||||||
|
| DAO | governance/authority | Вызовы через governance и управляющие ключи | Управление правами, казной, настройками и будущими обновлениями программ. |
|
||||||
|
|
||||||
|
## Актуальные program id
|
||||||
|
|
||||||
|
Актуальные адреса заданы одновременно в `Anchor.toml`, `declare_id!` программ и `programs/common/src/deploy_config.rs`:
|
||||||
|
|
||||||
|
| Программа | Program ID |
|
||||||
|
| --- | --- |
|
||||||
|
| `shine_login_guard` | `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` |
|
||||||
|
| `shine_users` | `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` |
|
||||||
|
| `shine_payments` | `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW` |
|
||||||
|
|
||||||
|
Если эти адреса меняются, нужно синхронно обновить:
|
||||||
|
|
||||||
|
1. `shine-solana/shine/Anchor.toml`
|
||||||
|
2. `declare_id!` в `programs/*/src/lib.rs`
|
||||||
|
3. `programs/common/src/deploy_config.rs`
|
||||||
|
4. UI/серверные константы, перечисленные в `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
||||||
|
|
||||||
|
## Ключи и authority
|
||||||
|
|
||||||
|
Для удобного понимания на старте можно считать, что есть четыре группы ключей:
|
||||||
|
|
||||||
|
1. `key_1` / authority программы `shine_login_guard`.
|
||||||
|
- Сейчас программа только классифицирует логин.
|
||||||
|
- На первом этапе ее можно оставить под отдельным ключом.
|
||||||
|
- В будущем право обновления можно передать DAO.
|
||||||
|
|
||||||
|
2. `key_2` / authority программы `shine_users`.
|
||||||
|
- Отвечает за деплой/upgrade второй программы.
|
||||||
|
- Защищенное обновление economy-конфига в коде уже проверяет `DAO_AUTHORITY`.
|
||||||
|
- В целевой модели upgrade-authority второй программы нужно передать DAO.
|
||||||
|
|
||||||
|
3. `key_3` / authority программы `shine_payments`.
|
||||||
|
- Отвечает за деплой/upgrade третьей программы.
|
||||||
|
- Защищенные методы `update_coef_limit` и `grant_manager_limits` проверяют `dao_wallet` из `ConfigState`.
|
||||||
|
- В целевой модели upgrade-authority третьей программы нужно передать DAO.
|
||||||
|
|
||||||
|
4. DAO-ключи.
|
||||||
|
- Это управляющие кошельки/токены/realm governance.
|
||||||
|
- DAO может добавлять и отзывать управляющие ключи по голосованию.
|
||||||
|
- DAO-казна получает деньги от покупки тикетов и DAO-часть выплат из `inflow_vault`.
|
||||||
|
|
||||||
|
Адреса program id сейчас берутся из `programs/common/src/deploy_config.rs`. Для production/devnet можно подбирать vanity-адреса с понятным началом вроде `SHi...`, но это отдельная операция генерации ключей и деплоя.
|
||||||
|
|
||||||
|
## Счета и PDA
|
||||||
|
|
||||||
|
Постоянные PDA и счета:
|
||||||
|
|
||||||
|
1. `shine_users`
|
||||||
|
- `user_pda` — пользовательская запись по seed `login=<login>`, создается для каждого логина.
|
||||||
|
- `users_economy_config_pda` — общие параметры экономики регистрации и лимита.
|
||||||
|
|
||||||
|
2. `shine_payments`
|
||||||
|
- `config_pda` — хранит `dao_wallet` и адрес `inflow_vault`.
|
||||||
|
- `coef_limit_pda` — хранит коэффициент выплат, лимит очереди и награду вызывающему `step_payout`.
|
||||||
|
- `queues_pda` — агрегаты очередей выплат.
|
||||||
|
- `inflow_vault_pda` — PDA-вольт, куда `shine_users` переводит оплату регистрации и увеличения лимита.
|
||||||
|
- `ticket_pda` — отдельная PDA-запись тикета на каждую покупку/менеджерскую выдачу.
|
||||||
|
- `manager_allowance_pda` — PDA лимитов конкретного менеджера.
|
||||||
|
|
||||||
|
3. DAO
|
||||||
|
- `dao_wallet` / treasury — казна DAO.
|
||||||
|
- governance-аккаунты DAO — realm, governance, proposal/vote records и связанные аккаунты SPL Governance, если используется эта модель.
|
||||||
|
|
||||||
|
## Правило разделения с основным сервером
|
||||||
|
|
||||||
|
Solana-модуль лежит в основном репозитории как отдельная папка `shine-solana/shine/`, но не подключается автоматически к сборке или деплою основного сервера SHiNE. Команды `deployServer` и `deployUI` не должны деплоить Anchor-программы. Solana build/deploy выполняется отдельно из папки `shine-solana/shine/` по локальным правилам модуля.
|
||||||
|
|
||||||
|
## Движение денег
|
||||||
|
|
||||||
|
Основные потоки:
|
||||||
|
|
||||||
|
1. Регистрация пользователя через `shine_users::create_user_pda`.
|
||||||
|
- Платит `signer`.
|
||||||
|
- Деньги идут в `shine_payments::inflow_vault_pda`.
|
||||||
|
- Сумма состоит из регистрационной комиссии и оплаты дополнительного лимита.
|
||||||
|
|
||||||
|
2. Увеличение лимита через `shine_users::update_user_pda`.
|
||||||
|
- Платит `signer`.
|
||||||
|
- Деньги идут в тот же `inflow_vault_pda`.
|
||||||
|
- Сумма равна оплате дополнительного лимита.
|
||||||
|
|
||||||
|
3. Покупка тикета через `shine_payments::buy_ticket*`.
|
||||||
|
- Платит покупатель.
|
||||||
|
- Деньги сразу идут в `dao_wallet`.
|
||||||
|
- Одновременно создается тикет на выплату.
|
||||||
|
|
||||||
|
4. Выплата через `shine_payments::step_payout`.
|
||||||
|
- Вызвать может любой подписант.
|
||||||
|
- Деньги берутся из `inflow_vault_pda`.
|
||||||
|
- Часть идет получателю тикета.
|
||||||
|
- Часть идет в `dao_wallet`.
|
||||||
|
- Небольшая награда идет вызвавшему шаг выплат.
|
||||||
|
- Если очереди пустые, весь доступный остаток `inflow_vault_pda` переводится в DAO.
|
||||||
|
|
||||||
|
## Передача прав DAO
|
||||||
|
|
||||||
|
Минимальная целевая модель:
|
||||||
|
|
||||||
|
1. `shine_login_guard`
|
||||||
|
- Пока оставить на отдельном ключе `key_1`.
|
||||||
|
- Передачу DAO сделать позже, когда логика premium/trademark стабилизируется.
|
||||||
|
|
||||||
|
2. `shine_users`
|
||||||
|
- Economy-настройки уже должны обновляться DAO-authority.
|
||||||
|
- Upgrade-authority программы после проверки можно передать DAO.
|
||||||
|
|
||||||
|
3. `shine_payments`
|
||||||
|
- DAO уже управляет настройками выплат и лимитами менеджеров через `dao_wallet`.
|
||||||
|
- Upgrade-authority программы после проверки можно передать DAO.
|
||||||
|
|
||||||
|
4. DAO
|
||||||
|
- Управляет казной.
|
||||||
|
- Принимает решения голосованием.
|
||||||
|
- Добавляет/отзывает управляющие ключи.
|
||||||
|
- Вызывает защищенные методы второй и третьей программ.
|
||||||
|
- В будущем может принять управление первой программой.
|
||||||
|
|
||||||
|
## Детальные файлы
|
||||||
|
|
||||||
|
- [details/shine_login_guard.md](details/shine_login_guard.md)
|
||||||
|
- [details/shine_users.md](details/shine_users.md)
|
||||||
|
- [details/shine_payments.md](details/shine_payments.md)
|
||||||
|
- [details/shine_dao.md](details/shine_dao.md)
|
||||||
|
- [details/accounts_and_money_flow.md](details/accounts_and_money_flow.md)
|
||||||
110
Dev_Docs/Solana_Architecture/details/accounts_and_money_flow.md
Normal file
110
Dev_Docs/Solana_Architecture/details/accounts_and_money_flow.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Счета, ключи и движение денег
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
В архитектуре есть три типа объектов:
|
||||||
|
|
||||||
|
1. Ключи программ и DAO.
|
||||||
|
2. PDA-счета состояния.
|
||||||
|
3. Денежные счета, через которые проходят SOL/lamports.
|
||||||
|
|
||||||
|
## Ключи
|
||||||
|
|
||||||
|
Минимальный набор для понимания:
|
||||||
|
|
||||||
|
1. `key_1` — deploy/upgrade authority `shine_login_guard`.
|
||||||
|
2. `key_2` — deploy/upgrade authority `shine_users`.
|
||||||
|
3. `key_3` — deploy/upgrade authority `shine_payments`.
|
||||||
|
4. `DAO_AUTHORITY` — адрес, который имеет право менять защищенные настройки.
|
||||||
|
5. `DAO_TREASURY_WALLET` / `dao_wallet` — казна DAO.
|
||||||
|
6. `manager_wallet` — кошелек менеджера, которому DAO выдает лимиты на создание тикетов.
|
||||||
|
7. `user root_key` — корневой ключ пользователя для подписи пользовательской записи.
|
||||||
|
8. `user client_key` — ключ устройства пользователя.
|
||||||
|
9. `server_key` — ключ сервера пользователя, если пользователь является сервером.
|
||||||
|
|
||||||
|
Текущие адреса из `programs/common/src/deploy_config.rs`:
|
||||||
|
|
||||||
|
| Роль | Адрес |
|
||||||
|
| --- | --- |
|
||||||
|
| `SHINE_LOGIN_GUARD_PROGRAM_ID` | `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` |
|
||||||
|
| `SHINE_USERS_PROGRAM_ID` | `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` |
|
||||||
|
| `SHINE_PAYMENTS_PROGRAM_ID` | `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW` |
|
||||||
|
| `DAO_AUTHORITY` | `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P` |
|
||||||
|
| `DAO_TREASURY_WALLET` | `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P` |
|
||||||
|
|
||||||
|
## Постоянные PDA
|
||||||
|
|
||||||
|
`shine_users`:
|
||||||
|
|
||||||
|
- `user_pda` — создается для каждого логина, seed `login=` + normalized login.
|
||||||
|
- `users_economy_config_pda` — один PDA с экономикой регистрации, seed `shine_users_economy_config`.
|
||||||
|
|
||||||
|
`shine_payments`:
|
||||||
|
|
||||||
|
- `config_pda` — один PDA конфига, seed `shine_payments_config`.
|
||||||
|
- `coef_limit_pda` — один PDA коэффициента/лимита/награды, seed `shine_payments_coef_limit`.
|
||||||
|
- `queues_pda` — один PDA агрегатов очередей, seed `shine_payments_queues`.
|
||||||
|
- `inflow_vault_pda` — один PDA-вольт входящих средств, seed `shine_payments_inflow_vault`.
|
||||||
|
- `ticket_pda` — много PDA, по одному на тикет, seed `shine_payments_q1_ticket` или `shine_payments_q2_ticket` + индекс.
|
||||||
|
- `manager_allowance_pda` — много PDA, по одному на менеджера, seed `shine_p_manager_allow` + адрес менеджера.
|
||||||
|
|
||||||
|
## Денежные потоки
|
||||||
|
|
||||||
|
### Регистрация
|
||||||
|
|
||||||
|
```text
|
||||||
|
user signer -> shine_users::create_user_pda -> shine_payments::inflow_vault_pda
|
||||||
|
```
|
||||||
|
|
||||||
|
Состав платежа:
|
||||||
|
|
||||||
|
- регистрационная комиссия;
|
||||||
|
- оплата `additional_limit`.
|
||||||
|
|
||||||
|
### Увеличение лимита
|
||||||
|
|
||||||
|
```text
|
||||||
|
user signer -> shine_users::update_user_pda -> shine_payments::inflow_vault_pda
|
||||||
|
```
|
||||||
|
|
||||||
|
Состав платежа:
|
||||||
|
|
||||||
|
- только оплата `additional_limit`.
|
||||||
|
|
||||||
|
### Покупка тикета
|
||||||
|
|
||||||
|
```text
|
||||||
|
buyer signer -> shine_payments::buy_ticket* -> dao_wallet
|
||||||
|
```
|
||||||
|
|
||||||
|
При этом создается `ticket_pda`, но деньги в `inflow_vault_pda` на этом шаге не идут.
|
||||||
|
|
||||||
|
### Выплата
|
||||||
|
|
||||||
|
```text
|
||||||
|
shine_payments::inflow_vault_pda -> ticket_recipient_wallet
|
||||||
|
shine_payments::inflow_vault_pda -> dao_wallet
|
||||||
|
shine_payments::inflow_vault_pda -> step_payout caller
|
||||||
|
```
|
||||||
|
|
||||||
|
Если очереди пустые:
|
||||||
|
|
||||||
|
```text
|
||||||
|
shine_payments::inflow_vault_pda -> dao_wallet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что нужно создать на старте
|
||||||
|
|
||||||
|
Минимально:
|
||||||
|
|
||||||
|
1. Три program id для `shine_login_guard`, `shine_users`, `shine_payments`.
|
||||||
|
2. Три upgrade-authority ключа или один временный deploy-ключ с четким планом передачи прав.
|
||||||
|
3. DAO authority/treasury.
|
||||||
|
4. `users_economy_config_pda`.
|
||||||
|
5. `shine_payments` PDA: `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`.
|
||||||
|
|
||||||
|
Динамически будут создаваться:
|
||||||
|
|
||||||
|
- `user_pda` на каждого пользователя;
|
||||||
|
- `ticket_pda` на каждый тикет;
|
||||||
|
- `manager_allowance_pda` на каждого менеджера.
|
||||||
74
Dev_Docs/Solana_Architecture/details/shine_dao.md
Normal file
74
Dev_Docs/Solana_Architecture/details/shine_dao.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# SHiNE DAO
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
DAO — управляющий слой Solana-части SHiNE. В текущем коде это не отдельная Anchor-программа в `programs/`, а модель управления через DAO-кошелек, DAO-authority, governance-скрипты и будущую передачу upgrade-authority программ.
|
||||||
|
|
||||||
|
## Что DAO должно уметь
|
||||||
|
|
||||||
|
1. Управлять казной.
|
||||||
|
- Принимать средства на `dao_wallet`.
|
||||||
|
- Выплачивать средства со счета DAO по решениям голосования.
|
||||||
|
|
||||||
|
2. Управлять настройками `shine_users`.
|
||||||
|
- Обновлять регистрационную комиссию.
|
||||||
|
- Обновлять цену шага лимита.
|
||||||
|
- Обновлять стартовый бонус лимита.
|
||||||
|
|
||||||
|
3. Управлять настройками `shine_payments`.
|
||||||
|
- Обновлять коэффициент выплат.
|
||||||
|
- Обновлять лимит очереди.
|
||||||
|
- Обновлять награду за вызов `step_payout`.
|
||||||
|
|
||||||
|
4. Управлять менеджерами.
|
||||||
|
- Выдавать менеджеру лимит на добавление тикетов.
|
||||||
|
- Отдельно учитывать лимиты Q1 и Q2.
|
||||||
|
|
||||||
|
5. Управлять правами программ.
|
||||||
|
- Принять upgrade-authority `shine_users`.
|
||||||
|
- Принять upgrade-authority `shine_payments`.
|
||||||
|
- Позже принять upgrade-authority `shine_login_guard`, если это потребуется.
|
||||||
|
|
||||||
|
6. Управлять ключами DAO.
|
||||||
|
- Добавлять управляющие ключи.
|
||||||
|
- Отзывать или сжигать управляющие ключи.
|
||||||
|
- Делать это через голосование, а не вручную одним админом.
|
||||||
|
|
||||||
|
7. Фиксировать решения.
|
||||||
|
- Делать заявления/решения через governance-механику.
|
||||||
|
- Привязывать важные изменения к proposal/vote/execute.
|
||||||
|
|
||||||
|
## Текущие адреса управления
|
||||||
|
|
||||||
|
В общем deploy-конфиге сейчас есть два важных адреса:
|
||||||
|
|
||||||
|
- `DAO_AUTHORITY` — используется `shine_users` для проверки права менять economy-конфиг.
|
||||||
|
- `DAO_TREASURY_WALLET` — используется `shine_payments` как `dao_wallet`.
|
||||||
|
|
||||||
|
Сейчас они могут совпадать. В целевой DAO-модели их лучше рассматривать как разные роли:
|
||||||
|
|
||||||
|
- authority/governance signer — кто имеет право исполнять управленческие инструкции;
|
||||||
|
- treasury wallet — счет, куда приходят деньги DAO.
|
||||||
|
|
||||||
|
## Передача прав
|
||||||
|
|
||||||
|
Рекомендуемый порядок:
|
||||||
|
|
||||||
|
1. Сначала стабилизировать и проверить `shine_users` и `shine_payments`.
|
||||||
|
2. Передать DAO право обновлять настройки, если оно еще не передано.
|
||||||
|
3. Передать DAO upgrade-authority второй и третьей программ.
|
||||||
|
4. Оставить `shine_login_guard` на отдельном ключе до стабилизации словарей и правил логинов.
|
||||||
|
5. После стабилизации решить отдельным голосованием, передавать ли первую программу DAO.
|
||||||
|
|
||||||
|
## Важное разделение
|
||||||
|
|
||||||
|
Есть два разных типа прав:
|
||||||
|
|
||||||
|
1. Право вызвать защищенную функцию программы.
|
||||||
|
- Например, `update_coef_limit` или `grant_manager_limits`.
|
||||||
|
- Проверяется внутри программы по `dao_wallet` или `DAO_AUTHORITY`.
|
||||||
|
|
||||||
|
2. Право обновить саму программу.
|
||||||
|
- Это upgrade-authority Solana ProgramData.
|
||||||
|
- Оно передается отдельной Solana-командой/DAO-транзакцией и не равно обычному PDA-счету.
|
||||||
|
|
||||||
58
Dev_Docs/Solana_Architecture/details/shine_login_guard.md
Normal file
58
Dev_Docs/Solana_Architecture/details/shine_login_guard.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# `shine_login_guard`
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
`shine_login_guard` — первая программа Solana-модуля SHiNE. Она проверяет логин перед регистрацией пользователя и возвращает класс логина.
|
||||||
|
|
||||||
|
Папка программы: `shine-solana/shine/programs/shine_login_guard/`.
|
||||||
|
|
||||||
|
## Текущая функция
|
||||||
|
|
||||||
|
1. `classify_login(login: String)`
|
||||||
|
- Нормализует логин.
|
||||||
|
- Проверяет длину и допустимые символы.
|
||||||
|
- Сравнивает части логина со словарями premium/trademark.
|
||||||
|
- Возвращает результат через `set_return_data`.
|
||||||
|
|
||||||
|
Классы результата:
|
||||||
|
|
||||||
|
- `0` — обычный логин, регистрацию можно продолжать.
|
||||||
|
- `1` — premium-логин.
|
||||||
|
- `2` — trademark-логин, нужна отдельная проверка/разрешение.
|
||||||
|
|
||||||
|
## Правила нормализации и классификации
|
||||||
|
|
||||||
|
Текущая логика из `programs/shine_login_guard/src/lib.rs`:
|
||||||
|
|
||||||
|
- пустой логин или логин длиннее 20 символов получает класс `premium`;
|
||||||
|
- `_` при нормализации удаляется;
|
||||||
|
- допустимы только ASCII-буквы и цифры, остальные символы дают класс `premium`;
|
||||||
|
- после удаления `_` результат приводится к нижнему регистру;
|
||||||
|
- логины длиной 7 символов или меньше считаются `premium`;
|
||||||
|
- логин разбивается максимум на 3 словарных фрагмента;
|
||||||
|
- если среди найденных фрагментов есть trademark-слово, результат `trademark`;
|
||||||
|
- если найдены только premium-слова, результат `premium`;
|
||||||
|
- если разбиение по словарям не найдено, результат `free`.
|
||||||
|
|
||||||
|
Словари собираются на этапе build из файлов:
|
||||||
|
|
||||||
|
- `programs/shine_login_guard/src/dictionaries/premium/*.txt`
|
||||||
|
- `programs/shine_login_guard/src/dictionaries/trademarks/*.txt`
|
||||||
|
|
||||||
|
## Роль в общей схеме
|
||||||
|
|
||||||
|
`shine_users::create_user_pda` вызывает `shine_login_guard` через CPI и продолжает регистрацию только если логин получил класс `0`.
|
||||||
|
|
||||||
|
## Ключи и управление
|
||||||
|
|
||||||
|
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_1`.
|
||||||
|
|
||||||
|
Текущая рекомендация:
|
||||||
|
|
||||||
|
- пока оставить `shine_login_guard` под отдельным ключом;
|
||||||
|
- не передавать ее DAO до стабилизации правил premium/trademark;
|
||||||
|
- позже можно передать upgrade-authority DAO, чтобы изменения словарей и правил проходили через голосование.
|
||||||
|
|
||||||
|
## Счета
|
||||||
|
|
||||||
|
Собственных постоянных PDA-счетов у программы сейчас нет. Для проверки нужен только подписант транзакции в `ClassifyLogin`.
|
||||||
173
Dev_Docs/Solana_Architecture/details/shine_payments.md
Normal file
173
Dev_Docs/Solana_Architecture/details/shine_payments.md
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# `shine_payments`
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
`shine_payments` — третья программа Solana-модуля SHiNE. Она отвечает за vault входящих средств, DAO-казну, покупку тикетов, менеджерские лимиты, очереди выплат и пошаговое исполнение выплат.
|
||||||
|
|
||||||
|
Папка программы: `shine-solana/shine/programs/shine_payments/`.
|
||||||
|
|
||||||
|
## Текущие функции
|
||||||
|
|
||||||
|
1. `init`
|
||||||
|
- Создает основные PDA: `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`.
|
||||||
|
- Записывает `dao_wallet` и стартовые параметры выплат.
|
||||||
|
|
||||||
|
2. `update_coef_limit`
|
||||||
|
- Обновляет коэффициент выплаты, лимит очереди и награду вызвавшему `step_payout`.
|
||||||
|
- Требует подпись DAO-кошелька из `ConfigState`.
|
||||||
|
|
||||||
|
3. `grant_manager_limits`
|
||||||
|
- DAO выдает менеджеру лимиты на создание тикетов в очередях Q1/Q2.
|
||||||
|
- Создает или обновляет `manager_allowance_pda`.
|
||||||
|
|
||||||
|
4. `buy_ticket`
|
||||||
|
- Покупка тикета с суммой в lamports, пересчетом через Pyth SOL/USD.
|
||||||
|
|
||||||
|
5. `buy_ticket_usd`
|
||||||
|
- Покупка тикета от USD-центов с защитой по максимальному платежу в lamports.
|
||||||
|
|
||||||
|
6. `buy_ticket_sol`
|
||||||
|
- Покупка тикета в lamports с проверкой минимального ожидаемого USD-эквивалента.
|
||||||
|
|
||||||
|
7. `manager_add_ticket`
|
||||||
|
- Менеджер создает тикет за счет выданного ему DAO-лимита.
|
||||||
|
|
||||||
|
8. `step_payout`
|
||||||
|
- Любой подписант может вызвать шаг выплат.
|
||||||
|
- Программа выплачивает следующий тикет, DAO-часть и награду вызывающему.
|
||||||
|
|
||||||
|
9. `change_ticket_recipient`
|
||||||
|
- Текущий получатель тикета может поменять адрес получателя, если тикет еще не следующий на выплату.
|
||||||
|
|
||||||
|
## Аргументы инструкций
|
||||||
|
|
||||||
|
`init` аргументов не принимает.
|
||||||
|
|
||||||
|
`update_coef_limit`:
|
||||||
|
|
||||||
|
- `coef_ppm: u64`
|
||||||
|
- `limit_usd_cents: u64`
|
||||||
|
- `call_reward_lamports: u64`
|
||||||
|
|
||||||
|
`grant_manager_limits`:
|
||||||
|
|
||||||
|
- `manager_wallet: Pubkey`
|
||||||
|
- `add_q1_usd_cents: u64`
|
||||||
|
- `add_q2_usd_cents: u64`
|
||||||
|
|
||||||
|
`buy_ticket`:
|
||||||
|
|
||||||
|
- `amount_lamports: u64`
|
||||||
|
- `recipient_wallet: Pubkey`
|
||||||
|
|
||||||
|
`buy_ticket_usd`:
|
||||||
|
|
||||||
|
- `amount_usd_cents: u64`
|
||||||
|
- `max_pay_lamports: u64`
|
||||||
|
- `recipient_wallet: Pubkey`
|
||||||
|
|
||||||
|
`buy_ticket_sol`:
|
||||||
|
|
||||||
|
- `amount_lamports: u64`
|
||||||
|
- `min_expected_usd_cents: u64`
|
||||||
|
- `recipient_wallet: Pubkey`
|
||||||
|
|
||||||
|
`manager_add_ticket`:
|
||||||
|
|
||||||
|
- `queue_id: u8` — только `1` или `2`
|
||||||
|
- `recipient_wallet: Pubkey`
|
||||||
|
- `payout_usd_cents: u64`
|
||||||
|
|
||||||
|
`change_ticket_recipient`:
|
||||||
|
|
||||||
|
- `new_recipient_wallet: Pubkey`
|
||||||
|
|
||||||
|
## Главные PDA
|
||||||
|
|
||||||
|
1. `config_pda`
|
||||||
|
- Seed: `shine_payments_config`.
|
||||||
|
- Хранит `dao_wallet` и `inflow_vault`.
|
||||||
|
- Размер PDA: `8 + 160` байт.
|
||||||
|
|
||||||
|
2. `coef_limit_pda`
|
||||||
|
- Seed: `shine_payments_coef_limit`.
|
||||||
|
- Хранит коэффициент выплат, лимит и награду `step_payout`.
|
||||||
|
- Размер PDA: `8 + 96` байт.
|
||||||
|
|
||||||
|
3. `queues_pda`
|
||||||
|
- Seed: `shine_payments_queues`.
|
||||||
|
- Хранит агрегаты очередей Q1/Q2.
|
||||||
|
- Размер PDA: `8 + 192` байт.
|
||||||
|
|
||||||
|
4. `inflow_vault_pda`
|
||||||
|
- Seed: `shine_payments_inflow_vault`.
|
||||||
|
- Принимает деньги от `shine_users`.
|
||||||
|
- Из него выполняются выплаты тикетам, DAO и вызывающему `step_payout`.
|
||||||
|
- Размер PDA: `8 + 32` байт.
|
||||||
|
|
||||||
|
5. `ticket_pda`
|
||||||
|
- Seed зависит от очереди и индекса тикета.
|
||||||
|
- Отдельная PDA-запись на каждый тикет.
|
||||||
|
- Q1 seed: `shine_payments_q1_ticket` + `ticket_index`.
|
||||||
|
- Q2 seed: `shine_payments_q2_ticket` + `ticket_index`.
|
||||||
|
- Размер PDA: `8 + 160` байт.
|
||||||
|
|
||||||
|
6. `manager_allowance_pda`
|
||||||
|
- Seed: `shine_p_manager_allow` + адрес менеджера.
|
||||||
|
- Хранит доступный лимит менеджера по Q1/Q2.
|
||||||
|
- Размер PDA: `8 + 128` байт.
|
||||||
|
|
||||||
|
## Текущие параметры
|
||||||
|
|
||||||
|
Параметры initial config из `programs/shine_payments/src/settings.rs`:
|
||||||
|
|
||||||
|
| Поле | Значение | Смысл |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `START_COEF_PPM` | `5_000_000` | коэффициент 5.0x в ppm-масштабе |
|
||||||
|
| `START_LIMIT_USD_CENTS` | `1_000_000` | стартовый лимит Q1: 10_000 USD |
|
||||||
|
| `START_CALL_REWARD_LAMPORTS` | `8_000_000` | награда вызвавшему `step_payout`, 0.008 SOL |
|
||||||
|
| `MAX_CALL_REWARD_LAMPORTS` | `10_000_000` | максимум награды, 0.01 SOL |
|
||||||
|
| `ORACLE_MAX_AGE_SECS` | `120` | максимальный возраст цены Pyth |
|
||||||
|
|
||||||
|
Для расчетов используется Pyth SOL/USD:
|
||||||
|
|
||||||
|
- feed id: `0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d`
|
||||||
|
- price update account: `7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE`
|
||||||
|
|
||||||
|
## Деньги
|
||||||
|
|
||||||
|
Входы:
|
||||||
|
|
||||||
|
- из `shine_users` в `inflow_vault_pda` при регистрации и увеличении лимита;
|
||||||
|
- от покупателя тикета сразу в `dao_wallet` при `buy_ticket*`.
|
||||||
|
|
||||||
|
Выходы:
|
||||||
|
|
||||||
|
- из `inflow_vault_pda` получателю тикета;
|
||||||
|
- из `inflow_vault_pda` в `dao_wallet`;
|
||||||
|
- из `inflow_vault_pda` вызвавшему `step_payout`;
|
||||||
|
- если очереди пустые, весь доступный остаток `inflow_vault_pda` переводится в DAO.
|
||||||
|
|
||||||
|
## Очереди и выплаты
|
||||||
|
|
||||||
|
Выплаты идут строго пошагово:
|
||||||
|
|
||||||
|
- если есть невыплаченные Q1-тикеты, `step_payout` берет следующий Q1;
|
||||||
|
- если Q1 пустая, берется следующий Q2;
|
||||||
|
- для Q1 DAO-часть равна сумме тикета в USD;
|
||||||
|
- для Q2 DAO-часть равна двойной сумме тикета в USD;
|
||||||
|
- перед выплатой суммы пересчитываются из USD-центов в lamports по Pyth SOL/USD;
|
||||||
|
- если в `inflow_vault_pda` не хватает средств на тикет, DAO-часть и награду вызвавшему, шаг отклоняется.
|
||||||
|
|
||||||
|
`change_ticket_recipient` запрещает менять получателя у тикета, который является следующим на выплату.
|
||||||
|
|
||||||
|
## Ключи и управление
|
||||||
|
|
||||||
|
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_3`.
|
||||||
|
|
||||||
|
Целевая модель:
|
||||||
|
|
||||||
|
- `update_coef_limit` вызывает DAO;
|
||||||
|
- `grant_manager_limits` вызывает DAO;
|
||||||
|
- upgrade-authority программы после проверки передается DAO;
|
||||||
|
- `step_payout` остается открытым для любого подписанта, чтобы выплаты не зависели от одного оператора.
|
||||||
136
Dev_Docs/Solana_Architecture/details/shine_users.md
Normal file
136
Dev_Docs/Solana_Architecture/details/shine_users.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# `shine_users`
|
||||||
|
|
||||||
|
## Кратко
|
||||||
|
|
||||||
|
`shine_users` — вторая программа Solana-модуля SHiNE. Она отвечает за создание и обновление пользовательской PDA-записи, проверку подписи записи, проверку логина через `shine_login_guard` и оплату регистрации/дополнительного лимита.
|
||||||
|
|
||||||
|
Папка программы: `shine-solana/shine/programs/shine_users/`.
|
||||||
|
|
||||||
|
## Текущие функции
|
||||||
|
|
||||||
|
1. `init_users_economy_config`
|
||||||
|
- Создает PDA с экономическими настройками пользователей.
|
||||||
|
- Записывает стартовую регистрационную комиссию, цену шага лимита и стартовый бонус лимита.
|
||||||
|
|
||||||
|
2. `update_users_economy_config`
|
||||||
|
- Обновляет экономические настройки.
|
||||||
|
- Требует подпись `DAO_AUTHORITY` из общего deploy-конфига.
|
||||||
|
|
||||||
|
3. `create_user_pda`
|
||||||
|
- Проверяет логин через `shine_login_guard`.
|
||||||
|
- Проверяет структуру полей пользователя.
|
||||||
|
- Проверяет подпись записи root-ключом пользователя.
|
||||||
|
- Создает `user_pda` по seed `login=<normalized_login>`.
|
||||||
|
- Переводит оплату регистрации и дополнительного лимита в `shine_payments::inflow_vault_pda`.
|
||||||
|
|
||||||
|
4. `update_user_pda`
|
||||||
|
- Проверяет неизменяемые поля пользователя.
|
||||||
|
- Проверяет `prev_hash`, новую подпись и новое состояние последнего блока.
|
||||||
|
- При необходимости расширяет PDA.
|
||||||
|
- Переводит оплату дополнительного лимита в `shine_payments::inflow_vault_pda`.
|
||||||
|
|
||||||
|
## Аргументы инструкций
|
||||||
|
|
||||||
|
`init_users_economy_config` аргументов не принимает.
|
||||||
|
|
||||||
|
`update_users_economy_config`:
|
||||||
|
|
||||||
|
- `registration_fee_lamports: u64`
|
||||||
|
- `lamports_per_limit_step: u64`
|
||||||
|
- `start_bonus_limit: u64`
|
||||||
|
|
||||||
|
`create_user_pda`:
|
||||||
|
|
||||||
|
- `login: String`
|
||||||
|
- `root_key: Pubkey`
|
||||||
|
- `created_at_ms: u64`
|
||||||
|
- `additional_limit: u64`
|
||||||
|
- `fields: UserMutableFields`
|
||||||
|
- `signature: Vec<u8>`
|
||||||
|
|
||||||
|
`update_user_pda`:
|
||||||
|
|
||||||
|
- `login: String`
|
||||||
|
- `root_key: Pubkey`
|
||||||
|
- `created_at_ms: u64`
|
||||||
|
- `updated_at_ms: u64`
|
||||||
|
- `version: u32`
|
||||||
|
- `prev_hash: Vec<u8>`
|
||||||
|
- `additional_limit: u64`
|
||||||
|
- `fields: UserMutableFields`
|
||||||
|
- `signature: Vec<u8>`
|
||||||
|
|
||||||
|
`UserMutableFields`:
|
||||||
|
|
||||||
|
- `client_key: Pubkey`
|
||||||
|
- `blockchain_public_key: Pubkey`
|
||||||
|
- `blockchain_name: String`
|
||||||
|
- `used_bytes: u64`
|
||||||
|
- `last_block_number: u32`
|
||||||
|
- `last_block_hash: Vec<u8>` — ровно 32 байта
|
||||||
|
- `last_block_signature: Vec<u8>` — ровно 64 байта
|
||||||
|
- `arweave_tx_id: String`
|
||||||
|
- `is_server: bool`
|
||||||
|
- `server_key: Pubkey`
|
||||||
|
- `server_address: String`
|
||||||
|
- `sync_servers: Vec<String>`
|
||||||
|
- `access_servers: Vec<String>`
|
||||||
|
- `trusted_count: u8`
|
||||||
|
|
||||||
|
## Главные PDA
|
||||||
|
|
||||||
|
1. `user_pda`
|
||||||
|
- PDA записи пользователя.
|
||||||
|
- Seed: `login=<normalized_login>`.
|
||||||
|
- Создается отдельно для каждого логина.
|
||||||
|
- Стартовый размер: `768` байт.
|
||||||
|
- При обновлении может расширяться через `realloc`, но один auto-realloc ограничен `10_000` байт.
|
||||||
|
|
||||||
|
2. `users_economy_config_pda`
|
||||||
|
- PDA с настройками экономики.
|
||||||
|
- Seed: `shine_users_economy_config`.
|
||||||
|
- Хранит регистрационную комиссию, цену шага лимита и стартовый бонус.
|
||||||
|
- Размер PDA: `8 + 96` байт.
|
||||||
|
|
||||||
|
## Текущие параметры экономики
|
||||||
|
|
||||||
|
Параметры initial config из `programs/shine_users/src/settings.rs`:
|
||||||
|
|
||||||
|
| Поле | Значение | Смысл |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `START_REGISTRATION_FEE_LAMPORTS` | `10_000_000` | стартовая комиссия регистрации, 0.01 SOL |
|
||||||
|
| `LIMIT_STEP` | `10_000` | шаг `additional_limit` |
|
||||||
|
| `START_LAMPORTS_PER_LIMIT_STEP` | `100_000` | 0.0001 SOL за один шаг лимита |
|
||||||
|
| `START_BONUS_LIMIT` | `100_000` | стартовый бесплатный лимит при регистрации |
|
||||||
|
|
||||||
|
`additional_limit` в create/update должен быть кратен `LIMIT_STEP`.
|
||||||
|
|
||||||
|
## Связь с другими программами
|
||||||
|
|
||||||
|
`shine_users` зависит от:
|
||||||
|
|
||||||
|
- `shine_login_guard` — для проверки логина при создании пользователя;
|
||||||
|
- `shine_payments` — для вычисления и проверки `inflow_vault_pda`, куда уходят платежи.
|
||||||
|
|
||||||
|
`create_user_pda` делает CPI-вызов `shine_login_guard::classify_login` и принимает только результат `0`. Premium/trademark логины сейчас отклоняются ошибками `PremiumLogin` или `TrademarkLoginRequiresReview`.
|
||||||
|
|
||||||
|
Подпись `user_pda` и подпись состояния последнего блока проверяются через встроенную Solana Ed25519-инструкцию, которая должна идти раньше инструкции `shine_users` в той же транзакции.
|
||||||
|
|
||||||
|
## Деньги
|
||||||
|
|
||||||
|
Деньги из `shine_users` идут только в `inflow_vault_pda` программы `shine_payments`.
|
||||||
|
|
||||||
|
Потоки:
|
||||||
|
|
||||||
|
- `create_user_pda`: регистрационная комиссия + оплата `additional_limit`;
|
||||||
|
- `update_user_pda`: оплата `additional_limit`, если она больше нуля.
|
||||||
|
|
||||||
|
## Ключи и управление
|
||||||
|
|
||||||
|
На старте удобно считать, что у программы есть отдельный управляющий ключ `key_2`.
|
||||||
|
|
||||||
|
Целевая модель:
|
||||||
|
|
||||||
|
- economy-настройки меняет DAO-authority;
|
||||||
|
- upgrade-authority программы после проверки передается DAO;
|
||||||
|
- пользовательские операции `create_user_pda` и `update_user_pda` остаются доступными обычным пользователям при корректных подписях и оплате.
|
||||||
54
Dev_Docs/Solana_Architecture/schemes/architecture.mmd
Normal file
54
Dev_Docs/Solana_Architecture/schemes/architecture.mmd
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
flowchart LR
|
||||||
|
U[Пользователь / signer]
|
||||||
|
B[Покупатель тикета]
|
||||||
|
M[Менеджер]
|
||||||
|
C[Любой caller step_payout]
|
||||||
|
|
||||||
|
LG[1. shine_login_guard<br/>classify_login]
|
||||||
|
USERS[2. shine_users<br/>create_user_pda / update_user_pda]
|
||||||
|
PAY[3. shine_payments<br/>vault / tickets / payouts]
|
||||||
|
DAO[SHiNE DAO<br/>governance / authority / treasury]
|
||||||
|
|
||||||
|
USERPDA[(user_pda<br/>по login)]
|
||||||
|
ECON[(users_economy_config_pda)]
|
||||||
|
CONFIG[(config_pda)]
|
||||||
|
COEF[(coef_limit_pda)]
|
||||||
|
QUEUES[(queues_pda)]
|
||||||
|
VAULT[(inflow_vault_pda)]
|
||||||
|
TICKET[(ticket_pda)]
|
||||||
|
ALLOW[(manager_allowance_pda)]
|
||||||
|
|
||||||
|
U -->|логин| USERS
|
||||||
|
USERS -->|CPI проверка| LG
|
||||||
|
USERS -->|создает/обновляет| USERPDA
|
||||||
|
USERS -->|читает экономику| ECON
|
||||||
|
U -->|регистрация / лимит| VAULT
|
||||||
|
|
||||||
|
DAO -->|update economy| USERS
|
||||||
|
DAO -->|update coef/limit| PAY
|
||||||
|
DAO -->|grant manager limits| PAY
|
||||||
|
DAO -->|создает/отзывает ключи| DAO
|
||||||
|
|
||||||
|
PAY --> CONFIG
|
||||||
|
PAY --> COEF
|
||||||
|
PAY --> QUEUES
|
||||||
|
PAY --> VAULT
|
||||||
|
PAY --> TICKET
|
||||||
|
PAY --> ALLOW
|
||||||
|
|
||||||
|
B -->|buy_ticket*| PAY
|
||||||
|
B -->|оплата покупки тикета| DAO
|
||||||
|
PAY -->|создает тикет| TICKET
|
||||||
|
|
||||||
|
M -->|manager_add_ticket| PAY
|
||||||
|
ALLOW -->|лимиты Q1/Q2| M
|
||||||
|
|
||||||
|
C -->|step_payout| PAY
|
||||||
|
VAULT -->|выплата тикета| U
|
||||||
|
VAULT -->|DAO-часть| DAO
|
||||||
|
VAULT -->|call reward| C
|
||||||
|
|
||||||
|
DAO -. upgrade authority после передачи .-> USERS
|
||||||
|
DAO -. upgrade authority после передачи .-> PAY
|
||||||
|
DAO -. позже возможно .-> LG
|
||||||
|
|
||||||
BIN
Dev_Docs/Solana_Architecture/schemes/architecture.png
Normal file
BIN
Dev_Docs/Solana_Architecture/schemes/architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
139
Dev_Docs/Solana_Architecture/schemes/architecture.svg
Normal file
139
Dev_Docs/Solana_Architecture/schemes/architecture.svg
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="900" viewBox="0 0 1400 900" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Архитектура Solana-программ SHiNE</title>
|
||||||
|
<desc id="desc">Схема трех программ, DAO, PDA-счетов и движения денег.</desc>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||||
|
<path d="M0,0 L0,6 L9,3 z" fill="#2f3a45"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="moneyArrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||||
|
<path d="M0,0 L0,6 L9,3 z" fill="#0a7f62"/>
|
||||||
|
</marker>
|
||||||
|
<style>
|
||||||
|
.bg { fill: #f7f8fa; }
|
||||||
|
.title { font: 700 30px Arial, sans-serif; fill: #1f2933; }
|
||||||
|
.subtitle { font: 400 16px Arial, sans-serif; fill: #52606d; }
|
||||||
|
.box { fill: #ffffff; stroke: #9aa5b1; stroke-width: 2; rx: 8; }
|
||||||
|
.program { fill: #e8f1ff; stroke: #3465a4; }
|
||||||
|
.dao { fill: #fff3d6; stroke: #b7791f; }
|
||||||
|
.pda { fill: #edf7ed; stroke: #2f855a; }
|
||||||
|
.actor { fill: #f3e8ff; stroke: #805ad5; }
|
||||||
|
.txt { font: 700 17px Arial, sans-serif; fill: #1f2933; }
|
||||||
|
.small { font: 400 13px Arial, sans-serif; fill: #3e4c59; }
|
||||||
|
.line { stroke: #2f3a45; stroke-width: 2.2; fill: none; marker-end: url(#arrow); }
|
||||||
|
.money { stroke: #0a7f62; stroke-width: 3; fill: none; marker-end: url(#moneyArrow); }
|
||||||
|
.dashed { stroke-dasharray: 8 7; }
|
||||||
|
.legend { font: 400 14px Arial, sans-serif; fill: #3e4c59; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect class="bg" x="0" y="0" width="1400" height="900"/>
|
||||||
|
<text class="title" x="52" y="54">SHiNE Solana: программы, DAO, счета и движение денег</text>
|
||||||
|
<text class="subtitle" x="52" y="82">Текущая модель: три Anchor-программы, DAO/authority как управляющий слой, inflow vault и DAO treasury.</text>
|
||||||
|
|
||||||
|
<rect class="box actor" x="52" y="150" width="210" height="78"/>
|
||||||
|
<text class="txt" x="72" y="181">Пользователь</text>
|
||||||
|
<text class="small" x="72" y="206">signer, root_key, client_key</text>
|
||||||
|
|
||||||
|
<rect class="box actor" x="52" y="310" width="210" height="78"/>
|
||||||
|
<text class="txt" x="72" y="341">Покупатель тикета</text>
|
||||||
|
<text class="small" x="72" y="366">buy_ticket*</text>
|
||||||
|
|
||||||
|
<rect class="box actor" x="52" y="470" width="210" height="78"/>
|
||||||
|
<text class="txt" x="72" y="501">Менеджер</text>
|
||||||
|
<text class="small" x="72" y="526">manager_add_ticket</text>
|
||||||
|
|
||||||
|
<rect class="box actor" x="52" y="630" width="210" height="78"/>
|
||||||
|
<text class="txt" x="72" y="661">Любой caller</text>
|
||||||
|
<text class="small" x="72" y="686">step_payout</text>
|
||||||
|
|
||||||
|
<rect class="box program" x="360" y="126" width="270" height="96"/>
|
||||||
|
<text class="txt" x="382" y="160">1. shine_login_guard</text>
|
||||||
|
<text class="small" x="382" y="186">classify_login</text>
|
||||||
|
<text class="small" x="382" y="205">free / premium / trademark</text>
|
||||||
|
|
||||||
|
<rect class="box program" x="360" y="286" width="270" height="112"/>
|
||||||
|
<text class="txt" x="382" y="320">2. shine_users</text>
|
||||||
|
<text class="small" x="382" y="346">create_user_pda</text>
|
||||||
|
<text class="small" x="382" y="365">update_user_pda</text>
|
||||||
|
<text class="small" x="382" y="384">economy config</text>
|
||||||
|
|
||||||
|
<rect class="box program" x="360" y="518" width="270" height="122"/>
|
||||||
|
<text class="txt" x="382" y="552">3. shine_payments</text>
|
||||||
|
<text class="small" x="382" y="578">vault, tickets, queues</text>
|
||||||
|
<text class="small" x="382" y="597">grant_manager_limits</text>
|
||||||
|
<text class="small" x="382" y="616">step_payout</text>
|
||||||
|
|
||||||
|
<rect class="box dao" x="776" y="126" width="270" height="122"/>
|
||||||
|
<text class="txt" x="798" y="160">SHiNE DAO</text>
|
||||||
|
<text class="small" x="798" y="186">governance / authority</text>
|
||||||
|
<text class="small" x="798" y="205">treasury dao_wallet</text>
|
||||||
|
<text class="small" x="798" y="224">ключи через голосование</text>
|
||||||
|
|
||||||
|
<rect class="box pda" x="776" y="306" width="270" height="84"/>
|
||||||
|
<text class="txt" x="798" y="340">shine_users PDA</text>
|
||||||
|
<text class="small" x="798" y="365">user_pda, economy_config</text>
|
||||||
|
|
||||||
|
<rect class="box pda" x="776" y="500" width="270" height="150"/>
|
||||||
|
<text class="txt" x="798" y="534">shine_payments PDA</text>
|
||||||
|
<text class="small" x="798" y="560">config_pda, coef_limit_pda</text>
|
||||||
|
<text class="small" x="798" y="579">queues_pda</text>
|
||||||
|
<text class="small" x="798" y="598">inflow_vault_pda</text>
|
||||||
|
<text class="small" x="798" y="617">ticket_pda, manager_allowance</text>
|
||||||
|
|
||||||
|
<rect class="box pda" x="1134" y="500" width="214" height="88"/>
|
||||||
|
<text class="txt" x="1156" y="534">inflow_vault</text>
|
||||||
|
<text class="small" x="1156" y="560">деньги регистрации</text>
|
||||||
|
|
||||||
|
<rect class="box dao" x="1134" y="170" width="214" height="88"/>
|
||||||
|
<text class="txt" x="1156" y="204">DAO treasury</text>
|
||||||
|
<text class="small" x="1156" y="230">dao_wallet</text>
|
||||||
|
|
||||||
|
<path class="line" d="M262 189 C300 189, 318 334, 360 334"/>
|
||||||
|
<text class="small" x="270" y="286">регистрация / update</text>
|
||||||
|
|
||||||
|
<path class="line" d="M360 314 C322 250, 320 176, 360 174"/>
|
||||||
|
<text class="small" x="330" y="250">CPI login</text>
|
||||||
|
|
||||||
|
<path class="line" d="M630 342 L776 342"/>
|
||||||
|
<text class="small" x="646" y="329">создает/обновляет</text>
|
||||||
|
|
||||||
|
<path class="money" d="M262 205 C438 432, 1010 390, 1134 530"/>
|
||||||
|
<text class="small" x="430" y="430">регистрация и лимит -> inflow_vault</text>
|
||||||
|
|
||||||
|
<path class="money" d="M262 349 C540 260, 870 244, 1134 214"/>
|
||||||
|
<text class="small" x="538" y="270">покупка тикета -> DAO treasury</text>
|
||||||
|
|
||||||
|
<path class="line" d="M262 509 L360 579"/>
|
||||||
|
<text class="small" x="276" y="540">создать тикет</text>
|
||||||
|
|
||||||
|
<path class="line" d="M630 579 L776 575"/>
|
||||||
|
<text class="small" x="648" y="562">PDA состояния</text>
|
||||||
|
|
||||||
|
<path class="line" d="M1046 575 L1134 548"/>
|
||||||
|
|
||||||
|
<path class="money" d="M1134 560 C970 700, 580 728, 262 669"/>
|
||||||
|
<text class="small" x="650" y="720">call reward caller</text>
|
||||||
|
|
||||||
|
<path class="money" d="M1134 536 C860 754, 426 238, 262 194"/>
|
||||||
|
<text class="small" x="632" y="760">выплата получателю тикета</text>
|
||||||
|
|
||||||
|
<path class="money" d="M1241 500 L1241 258"/>
|
||||||
|
<text class="small" x="1254" y="380">DAO-часть выплат</text>
|
||||||
|
|
||||||
|
<path class="line" d="M776 188 L630 342"/>
|
||||||
|
<text class="small" x="642" y="250">update economy</text>
|
||||||
|
|
||||||
|
<path class="line" d="M776 216 C690 290, 666 516, 630 558"/>
|
||||||
|
<text class="small" x="654" y="438">settings / managers</text>
|
||||||
|
|
||||||
|
<path class="line dashed" d="M910 248 C850 702, 620 720, 520 640"/>
|
||||||
|
<text class="small" x="690" y="690">upgrade-authority: users/payments; login_guard позже</text>
|
||||||
|
|
||||||
|
<rect class="box" x="52" y="808" width="1296" height="54"/>
|
||||||
|
<line x1="74" y1="835" x2="132" y2="835" class="line"/>
|
||||||
|
<text class="legend" x="146" y="840">логические вызовы и управление</text>
|
||||||
|
<line x1="374" y1="835" x2="432" y2="835" class="money"/>
|
||||||
|
<text class="legend" x="446" y="840">движение SOL/lamports</text>
|
||||||
|
<line x1="682" y1="835" x2="740" y2="835" class="line dashed"/>
|
||||||
|
<text class="legend" x="754" y="840">будущая передача upgrade-authority DAO</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.1 KiB |
177
Dev_Docs/audit/Solana-audit-2-by-Claude-11июня2026.md
Normal file
177
Dev_Docs/audit/Solana-audit-2-by-Claude-11июня2026.md
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
# Аудит безопасности Solana-программ SHiNE — выпуск 2 (11.06.2026)
|
||||||
|
|
||||||
|
Повторный независимый аудит после исправления всех 4 находок первого отчёта
|
||||||
|
(`Solana-audit-by-Claude-File5-9июня2026.md`). Код перечитан целиком:
|
||||||
|
|
||||||
|
- `shine_login_guard` (183 строки) — stateless-классификатор логинов;
|
||||||
|
- `shine_users` (1069 строк) — реестр пользователей, PDA-записи, подписи, экономика лимитов;
|
||||||
|
- `shine_payments` (1381 строка) — очереди тикетов, выплаты из вольта, оракул Pyth.
|
||||||
|
|
||||||
|
Перебраны классы атак: подмена аккаунтов/PDA, авторизация и подписи, арифметика и
|
||||||
|
переполнения, валидация оракула, экономика, реентранси, griefing/DoS, **алиасинг
|
||||||
|
аккаунтов (передача одного аккаунта в несколько слотов инструкции)**.
|
||||||
|
|
||||||
|
## Статус прошлых находок (все закрыты)
|
||||||
|
|
||||||
|
- 🔴 Critical #1 (economy-config PDA в `shine_users`) — закрыто: `validate_users_economy_config_pda` проверяет и адрес, и `owner == program_id`, и вызывается перед чтением и в create, и в update.
|
||||||
|
- 🔴 Critical #2 (singleton-PDA в `shine_payments`) — закрыто: `validate_singleton_state_pda` проверяет точный адрес + `owner == id()` во всех инструкциях (`update_coef_limit`, `grant_manager_limits`, `buy_ticket*`, `manager_add_ticket`, `step_payout`, `change_ticket_recipient`).
|
||||||
|
- 🟠 Medium (валидация Pyth) — закрыто: пин адреса аккаунта `PYTH_SOL_USD_ACCOUNT`, проверка `owner == pyth_receiver`, разбор официальным `PriceUpdateV2`, `get_price_no_older_than` с проверкой `feed_id`, проверка возраста и доверительного интервала (`ORACLE_MAX_CONFIDENCE_PPM`).
|
||||||
|
- 🟡 Low (griefing на предсказуемых адресах) — закрыто: `create_pda_account` в обеих программах переведён на «создание поверх предзаполненного» (allocate + assign + добор ренты).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 HIGH (НОВОЕ) — `shine_payments`: тикет с `recipient_wallet == inflow_vault` навсегда замораживает все выплаты — ✅ ИСПРАВЛЕНО (11.06.2026)
|
||||||
|
|
||||||
|
Закрыто: равенство `recipient == inflow_vault` запрещено во всех точках задания
|
||||||
|
получателя — `buy_ticket_by_purchase_usd` (через `config.inflow_vault`),
|
||||||
|
`process_manager_add_ticket` и `process_change_ticket_recipient` (через
|
||||||
|
`find_single_pda(INFLOW_VAULT_SEED)`). Дополнительно в `transfer_from_vault` добавлена
|
||||||
|
защита по умолчанию `require!(vault.key != recipient.key)`. Документация —
|
||||||
|
`doc/programs/shine_payments.md` §10.1. Историческое описание находки ниже.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Где
|
||||||
|
`transfer_from_vault` (строки 1258–1268) переводит лампорты из вольта прямой
|
||||||
|
манипуляцией балансами (вольт — PDA без приватного ключа, обычный system-перевод
|
||||||
|
невозможен):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn transfer_from_vault(vault: &AccountInfo, recipient: &AccountInfo, amount: u64) -> ProgramResult {
|
||||||
|
if amount == 0 { return Ok(()); }
|
||||||
|
let mut vault_lamports = vault.try_borrow_mut_lamports()?; // займ #1
|
||||||
|
let mut recipient_lamports = recipient.try_borrow_mut_lamports()?; // займ #2
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
В `step_payout` (строка 849) получатель — это `ticket.recipient_wallet`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
transfer_from_vault(inflow_vault_pda, ticket_recipient_wallet, ticket_lamports)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
А `recipient_wallet` нигде не валидируется при создании тикета:
|
||||||
|
`buy_ticket*` (строки 696/711/725 → 1031), `manager_add_ticket` (строка 765),
|
||||||
|
`change_ticket_recipient` (строка 900) — берут его «как есть» из аргументов.
|
||||||
|
|
||||||
|
### Суть атаки (алиасинг аккаунта)
|
||||||
|
В Solana, если один и тот же аккаунт передан в инструкцию в нескольких слотах,
|
||||||
|
рантайм отдаёт для всех слотов **один и тот же** `RefCell` (механизм дублей).
|
||||||
|
Поэтому если `ticket.recipient_wallet` равен адресу `inflow_vault` PDA, то в
|
||||||
|
`step_payout` аккаунт вольта попадает и в слот `inflow_vault_pda`, и в слот
|
||||||
|
`ticket_recipient_wallet`. Тогда внутри `transfer_from_vault`:
|
||||||
|
|
||||||
|
- `vault.try_borrow_mut_lamports()` — берёт mutable-займ (успех);
|
||||||
|
- `recipient.try_borrow_mut_lamports()` — это **тот же** аккаунт → второй
|
||||||
|
mutable-займ → `Err(AccountBorrowFailed)` → `?` возвращает ошибку → инструкция
|
||||||
|
падает.
|
||||||
|
|
||||||
|
### Почему это «заморозка всего», а не один тикет
|
||||||
|
Выплаты идут строго по возрастанию индекса. `step_payout` всегда обслуживает
|
||||||
|
сначала очередь Q1 (если в ней есть pending), затем Q2, затем Q3, и в каждой —
|
||||||
|
ровно «следующий неоплаченный» тикет (`paid + 1`). Тикет с `recipient == vault`:
|
||||||
|
|
||||||
|
- не может быть оплачен (`step_payout` всегда падает на нём);
|
||||||
|
- не может быть пропущен (нет механизма «skip»);
|
||||||
|
- блокирует все тикеты после него в своей очереди;
|
||||||
|
- если он в Q1 — блокирует обслуживание Q2 и Q3 (до них очередь не доходит);
|
||||||
|
- лампорты вольта (накопленные регистрационные комиссии) перестают выплачиваться
|
||||||
|
и не уходят в DAO (слив в DAO происходит только когда `pending == 0` по всем
|
||||||
|
очередям, а это состояние недостижимо).
|
||||||
|
|
||||||
|
### Эксплуатация (тривиальная, перестановочная)
|
||||||
|
Q1 — публичная очередь (`buy_ticket` доступен любому). Атакующий покупает **один**
|
||||||
|
дешёвый тикет Q1, указав `recipient_wallet = <адрес inflow_vault PDA>`. Адрес вольта
|
||||||
|
детерминирован и публичен (`find_single_pda(INFLOW_VAULT_SEED)`). С этого момента вся
|
||||||
|
подсистема выплат и средства вольта заморожены за стоимость одного тикета + ренты.
|
||||||
|
|
||||||
|
Дополнительно: даже при защите на этапе покупки остаётся вектор через
|
||||||
|
`change_ticket_recipient` (строка 900) — владелец любого своего неоплаченного тикета
|
||||||
|
может выставить `new_recipient_wallet = vault` позже.
|
||||||
|
|
||||||
|
### Класс и серьёзность
|
||||||
|
Класс: «account aliasing / duplicate-account mutable borrow» + отсутствие
|
||||||
|
валидации адреса получателя. Прямой кражи средств нет, но это перманентный
|
||||||
|
отказ в обслуживании (availability) с блокировкой средств вольта, триггер —
|
||||||
|
копеечный и доступен анонимно. Оценка: **HIGH**.
|
||||||
|
|
||||||
|
### Рекомендуемый фикс
|
||||||
|
Запретить `recipient`, равный адресу вольта, во всех точках, где он задаётся, чтобы
|
||||||
|
тикет с таким получателем вообще не мог появиться:
|
||||||
|
|
||||||
|
1. в `buy_ticket_by_purchase_usd` — `require!(recipient_wallet != config.inflow_vault, …)`
|
||||||
|
(config уже прочитан);
|
||||||
|
2. в `process_manager_add_ticket` — сверять с `find_single_pda(INFLOW_VAULT_SEED).0`;
|
||||||
|
3. в `process_change_ticket_recipient` — то же для `new_recipient_wallet`.
|
||||||
|
|
||||||
|
Дополнительно (defense-in-depth) — в `transfer_from_vault` явно
|
||||||
|
`require!(vault.key != recipient.key, …)` с понятной ошибкой, чтобы любой будущий
|
||||||
|
вызов был защищён от алиасинга. Этого `require` недостаточно как единственной меры
|
||||||
|
(тикет всё равно застрял бы), поэтому основная защита — на входе.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 LOW / INFO — наблюдения без прямой эксплуатации
|
||||||
|
|
||||||
|
### L1. `change_ticket_recipient` и `buy_ticket` не проверяют получателя на «опасные» адреса
|
||||||
|
Связано с HIGH выше; после фикса основной проблемы стоит заодно зафиксировать
|
||||||
|
правило «получатель не должен совпадать с системными PDA программы».
|
||||||
|
|
||||||
|
### L2. Гонка за логином (first-come) в `shine_users`
|
||||||
|
Адрес `user_pda` выводится из логина. После закрытия griefing-подсева остаётся
|
||||||
|
обычное состязание: увидев в мемпуле регистрацию `alice`, атакующий может
|
||||||
|
зарегистрировать `alice` со своим `root_key` первым. On-chain это решается только
|
||||||
|
commit-reveal; для текущей модели — приемлемый риск, отметить как известный.
|
||||||
|
|
||||||
|
### L3. `step_payout` без slippage-параметра
|
||||||
|
Выплата считается по текущей цене оракула без верхней границы лампортов. Цена
|
||||||
|
ограничена возрастом (120с) и доверительным интервалом (10%), аккаунт оракула
|
||||||
|
запинен — манипуляция маловероятна, но при резком движении цены SOL объём выплаты
|
||||||
|
в лампортах плавает. Риск низкий; при желании добавить верхнюю границу на шаг.
|
||||||
|
|
||||||
|
### L4. Экономическая устойчивость вольта (дизайн, не баг)
|
||||||
|
Деньги за покупку тикетов (`buy_ticket`) уходят на `dao_wallet`, а выплаты в
|
||||||
|
`step_payout` идут из `inflow_vault`, который наполняется **регистрационными
|
||||||
|
комиссиями** `shine_users`. Если поток регистраций меньше обязательств по выплатам,
|
||||||
|
вольт истощается и выплаты останавливаются (без потери средств, но с остановкой
|
||||||
|
сервиса). Это свойство экономической модели — стоит явно держать в уме и
|
||||||
|
мониторить баланс вольта/обязательств.
|
||||||
|
|
||||||
|
### L5. Заполнение Q1 до лимита как мягкий DoS
|
||||||
|
`buy_ticket` блокируется при `q1_sum_total >= limit_usd_cents`. Атакующий может
|
||||||
|
наполнить Q1 своими тикетами и приостановить покупки. Дорого (тратит SOL в DAO и
|
||||||
|
ренту) и его же тикеты потом оплачиваются из вольта, поэтому это скорее
|
||||||
|
экономический, а не дешёвый griefing. Риск низкий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Проверено и подтверждено как корректное
|
||||||
|
|
||||||
|
- **Подмена singleton-PDA** невозможна: везде сверяется точный адрес и владелец.
|
||||||
|
- **Авторизация**: `update_coef_limit`/`grant_manager_limits` требуют `signer == config.dao_wallet`; `manager_add_ticket` — `signer == allowance.manager_wallet`; `change_ticket_recipient` — `signer == ticket.recipient_wallet`; обновление economy-config — `signer == DAO_AUTHORITY`.
|
||||||
|
- **Ed25519 в `shine_users`**: строгие относительные индексы (−1/−2), `num_signatures == 1`, все три `ix_index == u16::MAX` (данные внутри самой ed25519-инструкции), сверка pubkey/signature/message по хэшу. Подмена и указание на чужую инструкцию исключены.
|
||||||
|
- **Цепочка версий записи** (`version == record_number+1`, `prev_hash == hash(old)`) — корректная защита от replay; сигнатура записи завязана на `root_key`, а не на плательщика.
|
||||||
|
- **Монотонность** `used_bytes`/`last_block_number` и `used_bytes <= paid_limit_bytes`.
|
||||||
|
- **Арифметика**: повсеместные `checked_*`, `overflow-checks = true`, расчёты оракула в `u128` с `u64::try_from` на сужении.
|
||||||
|
- **Оракул Pyth**: пин аккаунта + owner + feed_id + возраст + confidence через официальный SDK.
|
||||||
|
- **Рент-экземпт вольта** сохраняется: `available_vault_lamports` вычитает `minimum_balance`, а суммарная проверка `available >= needed` гарантирует, что после выплат вольт не опустится ниже ренты.
|
||||||
|
- **Двойная оплата тикета** исключена: `is_paid` + инкремент `*_tickets_paid`, следующий шаг адресует следующий индекс.
|
||||||
|
- **Реентранси отсутствует**: CPI только в System Program (transfer/allocate/assign) и в stateless `shine_login_guard` (с проверкой возвращённого `program_id`); обратных вызовов в наши программы нет.
|
||||||
|
- **create_pda_account (новый)**: устойчив к подсеву лампортов; атакующий не может ни выделить данные, ни сменить владельца PDA (нет ключа/seeds), поэтому ветка allocate+assign безопасна.
|
||||||
|
- **shine_login_guard**: stateless, без аккаунтов и средств; DFS-классификация ограничена (`MAX_WORDS_PER_LOGIN = 3`, длина ≤ 20) — без compute-DoS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приоритет действий
|
||||||
|
|
||||||
|
1. **HIGH** — запретить `recipient == inflow_vault` в `buy_ticket*`, `manager_add_ticket`,
|
||||||
|
`change_ticket_recipient`; добавить `require!(vault.key != recipient.key)` в
|
||||||
|
`transfer_from_vault` как защиту по умолчанию. Закрыть до mainnet.
|
||||||
|
2. **LOW** — зафиксировать правило «получатель ≠ системные PDA» (L1), оценить
|
||||||
|
добавление верхней границы выплаты на шаг (L3).
|
||||||
|
3. **INFO** — формально задокументировать экономику вольта (L4) и known-issue
|
||||||
|
гонки за логином (L5/L2).
|
||||||
|
|
||||||
|
Изменений в код в рамках этого аудита не вносил — это анализ. Готов подготовить патч
|
||||||
|
по пункту 1, если подтвердите.
|
||||||
134
Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md
Normal file
134
Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# Аудит безопасности Solana-программ SHiNE — выпуск 3 (12.06.2026)
|
||||||
|
|
||||||
|
Тематический аудит с фокусом на **полноту проверок входных аккаунтов**
|
||||||
|
(signer / owner / каноничный PDA-адрес / system-program / sysvar инструкций /
|
||||||
|
аккаунт оракула) — отвечает на вопрос «точно ли хватает всех проверок входных
|
||||||
|
аккаунтов». Код перечитан целиком после исправлений аудита №2
|
||||||
|
(`Solana-audit-2-by-Claude-11июня2026.md`):
|
||||||
|
|
||||||
|
- `shine_login_guard` (183 строки) — stateless-классификатор логинов, аккаунтами не пользуется;
|
||||||
|
- `shine_users` (1068 строк) — реестр пользователей, PDA-записи, ed25519-подписи, экономика лимитов;
|
||||||
|
- `shine_payments` (1398 строк) — очереди тикетов, выплаты из вольта, оракул Pyth.
|
||||||
|
|
||||||
|
Это ручная (не-Anchor `#[derive(Accounts)]`) реализация на `solana_program`, поэтому
|
||||||
|
каждая проверка аккаунта выполняется явно в коде handler-а. Перебраны: подмена
|
||||||
|
аккаунтов/PDA, подмена владельца, bump-seed атаки, отсутствие signer/authority,
|
||||||
|
подмена system-program и sysvar, подмена аккаунта оракула, неинициализированные/
|
||||||
|
повторно инициализируемые PDA, «лишние» аккаунты.
|
||||||
|
|
||||||
|
## Итоговый вердикт
|
||||||
|
|
||||||
|
**Проверок входных аккаунтов достаточно во всех трёх программах.** По каждому
|
||||||
|
handler присутствуют все требуемые классы проверок; грубых дыр (подмена PDA на
|
||||||
|
чужой аккаунт, отсутствие owner/signer-проверки, использование пользовательского
|
||||||
|
bump, подмена аккаунта оракула) не найдено. Все Critical/HIGH из аудитов №1 и №2
|
||||||
|
закрыты и в этом проходе подтверждены в коде. Новых эксплуатируемых пробелов в
|
||||||
|
валидации аккаунтов нет; есть несколько LOW/INFO-замечаний «by design».
|
||||||
|
|
||||||
|
## Статус прошлых находок (подтверждено в коде на 12.06.2026)
|
||||||
|
|
||||||
|
- 🔴 Critical #1 (economy-config PDA, `shine_users`) — закрыто: `validate_users_economy_config_pda` (адрес + `owner == program_id`) вызывается и в create, и в update перед чтением.
|
||||||
|
- 🔴 Critical #2 (singleton-PDA, `shine_payments`) — закрыто: `validate_singleton_state_pda` (адрес + `owner == id()`) во всех инструкциях.
|
||||||
|
- 🟠 Medium (валидация Pyth) — закрыто: пин адреса `PYTH_SOL_USD_ACCOUNT`, `owner == pyth_receiver`, `PriceUpdateV2`, `feed_id`, возраст, доверительный интервал.
|
||||||
|
- 🟡 Low (griefing на предсказуемых адресах) — закрыто: `create_pda_account` создаёт «поверх предзаполненного» в обеих программах.
|
||||||
|
- 🔴 HIGH аудита №2 (`recipient_wallet == inflow_vault` замораживает выплаты) — закрыто: запрет `recipient == inflow_vault` в `buy_ticket_by_purchase_usd` (стр. 1026), `process_manager_add_ticket` (стр. 747), `process_change_ticket_recipient` (стр. 878) + защита по умолчанию `require!(vault.key != recipient.key)` в `transfer_from_vault` (стр. 1278).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Матрица проверок входных аккаунтов
|
||||||
|
|
||||||
|
### shine_users
|
||||||
|
|
||||||
|
| Инструкция | signer | owner PDA | адрес/seed PDA | system | sysvar / подпись | прочее |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `init_users_economy_config` | ✓ | `owner == system` + `data_is_empty` (анти-reinit) | деривация + сверка | ✓ | — | значения из `settings`, не из ввода |
|
||||||
|
| `update_users_economy_config` | ✓ + `signer == DAO_AUTHORITY` | `owner == program_id` | деривация + сверка | — | — | `lamports_per_limit_step > 0` |
|
||||||
|
| `create_user_pda` | ✓ + `signer == client_key` | user_pda `owner == system` + empty; econ_config `owner == program_id` | user_pda, econ_config, inflow_vault, login_guard — все сверены | ✓ | ed25519 (record sig idx −2, last_block idx −1) | `inflow_vault` сверен с PDA `shine_payments`; login_guard сверен дважды |
|
||||||
|
| `update_user_pda` | ✓ + `signer == client_key` | user_pda `owner == program_id`; econ_config `owner == program_id` | деривация + сверка | ✓ | ed25519 + `version == old+1` + `prev_hash == hash(old)` | immutable-поля сверены с прежней записью |
|
||||||
|
|
||||||
|
### shine_payments
|
||||||
|
|
||||||
|
| Инструкция | signer | owner / валидация PDA | адрес PDA | system | прочее |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `init` | ✓ payer | все 4 PDA `is_uninitialized` | деривация + сверка | ✓ | `dao_wallet` из `settings`, нет лишних аккаунтов |
|
||||||
|
| `update_coef_limit` | ✓ + `signer == config.dao_wallet` | config/coef `owner == id()` | деривация + сверка | — | границы coef/limit/reward; нет лишних аккаунтов |
|
||||||
|
| `grant_manager_limits` | ✓ + `signer == config.dao_wallet` | config `owner == id()`; allowance create/read | allowance из `manager_wallet` | ✓ | `state.manager_wallet == args.manager_wallet` |
|
||||||
|
| `buy_ticket` / `_usd` / `_sol` | ✓ | config/coef/queues `owner == id()` | ticket деривация + сверка + `is_uninitialized` | ✓ | oracle (key+owner+возраст+confidence), `dao_wallet == config.dao_wallet`, `recipient != inflow_vault`, slippage |
|
||||||
|
| `manager_add_ticket` | ✓ | allowance/queues `owner == id()` | allowance из `signer`; ticket деривация + сверка + uninit | ✓ | `allowance.manager_wallet == signer`, `queue_id ∈ {1,2,3}`, `recipient != inflow_vault` |
|
||||||
|
| `step_payout` | ✓ | все singleton-PDA `owner == id()` | ticket деривация + сверка | — | `dao_wallet == config.dao_wallet`, `inflow == config.inflow_vault`, ticket `queue/index/!is_paid/recipient`, oracle |
|
||||||
|
| `change_ticket_recipient` | ✓ + `signer == ticket.recipient_wallet` | queues + ticket `owner == id()` (через `read_state`) | ticket деривация из своих `queue_id/index` + сверка | — | `!is_paid`, запрет менять «следующий к выплате», `recipient != inflow_vault` |
|
||||||
|
|
||||||
|
### shine_login_guard
|
||||||
|
|
||||||
|
Аккаунты не используются (`_accounts`); программа stateless, средствами не владеет.
|
||||||
|
Защита со стороны вызова реализована в `shine_users`: сверяется и адрес вызываемой
|
||||||
|
программы (`login_guard_program.key == SHINE_LOGIN_GUARD_PROGRAM_ID`), и `program_id`
|
||||||
|
в `get_return_data`. Подмена/подделка ответа исключены. Отдельных проверок входных
|
||||||
|
аккаунтов внутри программы не требуется.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 LOW / INFO — наблюдения без прямой эксплуатации
|
||||||
|
|
||||||
|
### L1. Permissionless `init` в обеих программах
|
||||||
|
`shine_payments::init` и `shine_users::init_users_economy_config` может вызвать кто
|
||||||
|
угодно первым. Практического эксплойта нет: все значения (включая `dao_wallet` и
|
||||||
|
`DAO_AUTHORITY`) берутся из констант `settings`, а не из ввода, повторная
|
||||||
|
инициализация заблокирована проверками `is_uninitialized` / `data_is_empty`. Риск
|
||||||
|
низкий; при желании привязать init к ожидаемому деплой-кошельку. Совпадает с моделью
|
||||||
|
«первый init = деплой».
|
||||||
|
|
||||||
|
### L2. В `shine_users` нет явной проверки «лишних аккаунтов» — ✅ ИСПРАВЛЕНО (12.06.2026)
|
||||||
|
`shine_payments` в каждом handler делает `require!(account_iter.next().is_none())`.
|
||||||
|
В `shine_users` такой проверки не было — лишние аккаунты в конце списка просто
|
||||||
|
игнорировались (читается строго нужное количество через `next_account_info`). Это
|
||||||
|
безвредно (на безопасность не влияло), но для симметрии и явности добавлено.
|
||||||
|
Класс: гигиена, не уязвимость.
|
||||||
|
|
||||||
|
Закрыто: во все 4 инструкции `shine_users` (`init_users_economy_config`,
|
||||||
|
`update_users_economy_config`, `create_user_pda`, `update_user_pda`) после чтения
|
||||||
|
фиксированного набора аккаунтов добавлено `require!(it.next().is_none(),
|
||||||
|
ShineUsersError::InvalidInstruction)`. Документация — `doc/programs/shine_users.md` §3.4.
|
||||||
|
|
||||||
|
### L3. Гонка за логином (first-come) в `shine_users` — known issue
|
||||||
|
Адрес `user_pda` детерминирован из логина; после закрытия griefing-подсева остаётся
|
||||||
|
обычное состязание за регистрацию (front-run в мемпуле). On-chain решается только
|
||||||
|
commit-reveal; для текущей модели — приемлемый риск, ранее зафиксирован в аудите №2
|
||||||
|
(L2). К проверкам аккаунтов не относится.
|
||||||
|
|
||||||
|
### L4. Экономическая устойчивость вольта (дизайн, не баг)
|
||||||
|
Деньги за покупку тикетов уходят на `dao_wallet`, а выплаты `step_payout` идут из
|
||||||
|
`inflow_vault`, наполняемого регистрационными комиссиями `shine_users` (коэффициент
|
||||||
|
по умолчанию `START_COEF_PPM = 5x`). При недостаточном притоке регистраций вольт
|
||||||
|
истощается и выплаты останавливаются (без потери средств). Это свойство
|
||||||
|
экономической модели «очередь/билеты», а не дефект валидации аккаунтов — отмечено
|
||||||
|
для полноты (ранее L4 в аудите №2). Мониторить баланс вольта vs обязательств.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Проверено и подтверждено как корректное (по входным аккаунтам)
|
||||||
|
|
||||||
|
- **Подмена PDA** невозможна нигде: всюду пара «деривация `find_program_address` + сверка полного адреса». Пользовательский bump не принимается, `create_program_address` с внешним bump не используется — bump-seed атаки исключены.
|
||||||
|
- **Проверка владельца** при каждом чтении PDA: `read_state` и `validate_singleton_state_pda` (`shine_payments`) требуют `owner == id()`; `validate_users_economy_config_pda` и проверка `user_pda.owner == program_id` (`shine_users`) — перед десериализацией данных.
|
||||||
|
- **Создаваемые PDA**: проверка `is_uninitialized` / `owner == system && data_is_empty` исключает повторную инициализацию и перезапись чужого аккаунта.
|
||||||
|
- **signer / authority**: все handler начинают с обязательного `is_signer`; привилегированные операции дополнительно сверяют ключ с авторитетом (`config.dao_wallet`, `DAO_AUTHORITY`, `allowance.manager_wallet`, `ticket.recipient_wallet`, `client_key`).
|
||||||
|
- **system-program** сверяется с `system_program::ID` там, где идёт создание аккаунта/перевод; **sysvar инструкций** сверяется с `sysvar::instructions::id()` перед ed25519-интроспекцией.
|
||||||
|
- **Аккаунт оракула**: пин адреса `PYTH_SOL_USD_ACCOUNT` + `owner == pyth_receiver` + `feed_id` + возраст (120 с) + доверительный интервал (10%).
|
||||||
|
- **Ed25519 в `shine_users`**: относительные индексы −1/−2, `num_signatures == 1`, все три `ix_index == u16::MAX` (offset-данные внутри самой ed25519-инструкции), сверка `program_id == ed25519_program` и pubkey/signature/message по хэшу — указать на чужую инструкцию нельзя.
|
||||||
|
- **Алиасинг аккаунтов**: `recipient != inflow_vault` запрещён на входе во всех точках задания получателя + `vault.key != recipient.key` в `transfer_from_vault`.
|
||||||
|
- **`inflow_vault` в `shine_users`** сверяется с PDA, выведенным из `SHINE_PAYMENTS_PROGRAM_ID` и `SHINE_PAYMENTS_INFLOW_VAULT_SEED` — комиссия не может уйти на чужой адрес.
|
||||||
|
- **Реентранси** отсутствует: CPI только в System Program и в stateless `shine_login_guard` (с проверкой возвращённого `program_id`); обратных вызовов в наши программы нет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приоритет действий
|
||||||
|
|
||||||
|
1. **LOW** — ✅ выполнено 12.06.2026: добавлено `require!(it.next().is_none(), …)` во
|
||||||
|
все инструкции `shine_users` для симметрии с `shine_payments` (L2).
|
||||||
|
2. **INFO** — зафиксировать в эксплуатационной документации known-issue гонки за
|
||||||
|
логином (L3) и экономику вольта (L4); рассмотреть привязку `init` к ожидаемому
|
||||||
|
деплой-кошельку (L1).
|
||||||
|
|
||||||
|
Критичных и высоких находок по полноте проверок входных аккаунтов в этом проходе
|
||||||
|
нет. Единственная LOW-правка (L2) применена в рамках этого же изменения; код
|
||||||
|
`shine_users` собирается успешно (`cargo build -p shine_users`).
|
||||||
114
Dev_Docs/audit/Solana-audit-by-Claude-File5-9июня2026.md
Normal file
114
Dev_Docs/audit/Solana-audit-by-Claude-File5-9июня2026.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
Аудит безопасности Solana-программ SHiNE
|
||||||
|
|
||||||
|
Проверены три программы в shine-solana/shine/programs/:
|
||||||
|
- shine_login_guard (183 строки) — stateless-классификатор логинов
|
||||||
|
- shine_users (1035 строк) — реестр пользователей, PDA-записи, подписи, экономика лимитов
|
||||||
|
- shine_payments (1330 строк) — очереди тикетов, выплаты, оракул Pyth
|
||||||
|
|
||||||
|
Общая инженерная культура высокая: везде checked_*-арифметика, overflow-checks = true, ручная верификация ed25519 через sysvar инструкций, аккуратный bounds-checked парсинг. Но есть две критические дыры одного класса — отсутствие проверки адреса PDA, которые позволяют обойти всю экономику и украсть средства из вольта.
|
||||||
|
|
||||||
|
---
|
||||||
|
🔴 CRITICAL #1 — shine_users: economy-config PDA не валидируется → бесплатная регистрация и бесконечный лимит
|
||||||
|
|
||||||
|
В process_create_user_pda (строка 448) и process_update_user_pda (строка 525):
|
||||||
|
|
||||||
|
let economy = read_users_economy_config(users_economy_config_pda)?;
|
||||||
|
|
||||||
|
А read_users_economy_config (строки 633–643) не проверяет ни адрес, ни владельца аккаунта — просто читает байты:
|
||||||
|
|
||||||
|
fn read_users_economy_config(pda: &AccountInfo) -> Result<...> {
|
||||||
|
let raw = read_pda_all(pda)?; // try_borrow_data, без проверок
|
||||||
|
require!(raw.len() >= 25, ...);
|
||||||
|
Ok(UsersEconomyConfigState { version: raw[0], registration_fee_lamports: ..., ... })
|
||||||
|
}
|
||||||
|
|
||||||
|
Сравните: в init/update_economy_config адрес проверяется через find_users_economy_config_pda (строки 382, 414), а в create/update — нет.
|
||||||
|
|
||||||
|
Эксплуатация. Атакующий создаёт любой свой аккаунт с 25 байтами произвольного содержимого и передаёт его как users_economy_config_pda:
|
||||||
|
- registration_fee_lamports = 0 → регистрация без оплаты;
|
||||||
|
- start_bonus_limit = u64::MAX → запись пользователя сразу получает гигантский paid_limit_bytes (бесплатная безлимитная квота хранилища/блокчейна);
|
||||||
|
- lamports_per_limit_step = 0 → бесплатное пополнение лимита на любую величину.
|
||||||
|
|
||||||
|
Комиссия (когда она ненулевая) уходит в правильный вольт — validate_inflow_vault это проверяет — но атакующему достаточно обнулить комиссию и накрутить лимит. Это полный обход экономической модели программы.
|
||||||
|
|
||||||
|
Фикс: в обеих функциях перед чтением добавить
|
||||||
|
require_keys_eq!(find_users_economy_config_pda(program_id).0, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress);
|
||||||
|
require!(users_economy_config_pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
|
||||||
|
|
||||||
|
---
|
||||||
|
🔴 CRITICAL #2 — shine_payments: singleton-PDA не привязаны к адресу → кража из вольта в step_payout
|
||||||
|
|
||||||
|
ensure_expected_pdas вызывается только в process_init (строка 519). Во всех остальных инструкциях config_pda, coef_limit_pda, queues_pda читаются через read_state, который проверяет только владельца (*pda.owner == id()), но не адрес:
|
||||||
|
|
||||||
|
fn read_state<T>(pda) -> ... {
|
||||||
|
require!(!is_uninitialized_account(pda), ...);
|
||||||
|
require_keys_eq!(*pda.owner, id(), ...); // только owner, адрес НЕ проверяется
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
Программа владеет аккаунтами нескольких типов (config, coef_limit, queues, vault, tickets, manager_allowances), и часть их содержимого атакующий контролирует напрямую. В TicketState поле recipient_wallet (32 байта по смещению 11) — полностью произвольные байты из BuyTicketArgs, это не обязан быть валидный ключ.
|
||||||
|
|
||||||
|
Эксплуатация (конкретная, практически реализуемая). В process_step_payout (строки 783–846) coef_limit_pda не проверяется по адресу. Раскладка CoefLimitState при декодировании: version=byte0, coef_ppm=[1..9], limit=[9..17], call_reward_lamports=[17..25]. Байты [17..25] тикета попадают на recipient_wallet[6..14] — их атакующий задаёт сам при покупке тикета. То есть:
|
||||||
|
|
||||||
|
1. Атакующий покупает тикет с recipient_wallet, чьи байты 6..14 кодируют огромный call_reward_lamports.
|
||||||
|
2. В step_payout подставляет этот тикет как coef_limit_pda.
|
||||||
|
3. transfer_from_vault(inflow_vault_pda, signer, coef_limit.call_reward_lamports) (строка 840) переводит подписанту (атакующему) почти весь доступный баланс вольта, который должен был накопиться для DAO.
|
||||||
|
|
||||||
|
version тикета = 1, decode значение версии не проверяет, поэтому подстановка проходит.
|
||||||
|
|
||||||
|
Подстановка config_pda для обхода DAO-авторизации в update_coef_limit/grant_manager_limits теоретически тоже возможна, но непрактична: там нужный dao_wallet пересекается со структурными полями тикета, и подбор потребовал бы грайндинга ~80 бит ключа. А вот путь через coef_limit/step_payout — реальная кража.
|
||||||
|
|
||||||
|
Фикс: во всех инструкциях, принимающих singleton-PDA, проверять адрес, например вызывать ensure_expected_pdas-подобную проверку (require_keys_eq!(find_single_pda(program_id, SEED).0, *pda.key, …)) для config/coef_limit/queues/inflow_vault, а для queues_pda в change_ticket_recipient — тоже.
|
||||||
|
|
||||||
|
---
|
||||||
|
🟠 MEDIUM — shine_payments: слабая валидация оракула Pyth
|
||||||
|
|
||||||
|
read_sol_usd_price / parse_pyth_price_update_v2 (строки 1038–1075):
|
||||||
|
|
||||||
|
1. Не проверяется владелец аккаунта цены (что он принадлежит Pyth receiver). Спасает только то, что адрес жёстко закреплён константой PYTH_SOL_USD_ACCOUNT. Это делает подмену невозможной, но защита держится на одном инварианте.
|
||||||
|
2. feed_id не проверяется. Константа PYTH_SOL_USD_FEED_ID объявлена в settings.rs, но в коде нигде не используется — программа доверяет, что в закреплённом аккаунте лежит именно SOL/USD.
|
||||||
|
3. Фиксированные смещения (73/89/93) предполагают VerificationLevel::Full. Borsh сериализует enum переменной длиной: Partial{num_signatures} занимает 2 байта вместо 1, что сдвигает все поля на 1 байт и приведёт к чтению мусорной цены. Уровень верификации не проверяется.
|
||||||
|
4. Confidence (conf) игнорируется — нет защиты от широкого ценового интервала.
|
||||||
|
|
||||||
|
Проверка возраста цены (ORACLE_MAX_AGE_SECS = 120) есть и сделана корректно. Рекомендация: проверять владельца аккаунта, сверять feed_id с константой и валидировать verification_level == Full (или парсить через официальный pyth_solana_receiver_sdk, который уже завендорен в .vendor/).
|
||||||
|
|
||||||
|
---
|
||||||
|
🟡 LOW — DoS через предсказуемые адреса тикетов — ✅ ИСПРАВЛЕНО (11.06.2026)
|
||||||
|
|
||||||
|
Закрыто: `create_pda_account` в `shine_payments` и `shine_users` переведён на паттерн
|
||||||
|
«создание поверх предзаполненного» (allocate + assign + добор ренты вместо строгого
|
||||||
|
`system_instruction::create_account`). «Подсев» лампортов на заранее известный адрес
|
||||||
|
тикета или пользовательской записи больше не блокирует создание PDA. Проверка
|
||||||
|
`is_uninitialized_account` в payments перестала зависеть от нулевого баланса. Тот же фикс
|
||||||
|
закрывает аналогичный сквоттинг логинов в `shine_users` (адрес выводится из логина).
|
||||||
|
Подробности — в `doc/programs/shine_payments.md` §3.4 и `doc/programs/shine_users.md` §3.3.
|
||||||
|
|
||||||
|
Историческое описание находки ниже.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
is_uninitialized_account (строка 1195) считает аккаунт неинициализированным только если lamports() == 0. Адреса тикетов детерминированы (queue_seed + index), а индекс последователен и предсказуем. Любой может заранее перевести немного лампортов на адрес следующего тикета — тогда create_pda_account упадёт (PdaAlreadyExists / ошибка create_account), заблокировав покупку/добавление тикета. Это griefing-DoS, не кража. Митигировать можно паттерном «create поверх предзаполненного» (allocate + assign + добор ренты) вместо system_instruction::create_account.
|
||||||
|
|
||||||
|
---
|
||||||
|
✅ Что проверено и сделано корректно
|
||||||
|
|
||||||
|
- Верификация подписей ed25519 в shine_users (строки 885–922): строго пинятся относительные индексы инструкций (−1/−2), требуется num_signatures == 1, все три ix-index == u16::MAX (данные внутри самой ed25519-инструкции — нельзя указать на чужую инструкцию), и сверяются pubkey/signature/message. Сделано грамотно.
|
||||||
|
- Цепочка версий записи (version == record_number+1, prev_hash == hash(old)) — корректная защита от replay (строки 535–536).
|
||||||
|
- Авторизация обновления записи завязана на ed25519-подпись root_key, а не на подписанта-плательщика — случайный аккаунт обновить чужую запись не может.
|
||||||
|
- Монотонность used_bytes/last_block_number и used_bytes <= paid_limit_bytes (строки 979–986).
|
||||||
|
- inflow_vault валидируется по derive из программы payments (строки 988–993).
|
||||||
|
- transfer_from_vault сохраняет рент-экземпт (вычитает minimum_balance через available_vault_lamports).
|
||||||
|
- init обеих программ безопасен к front-run: значения берутся из констант, а не от вызывающего; повторная инициализация заблокирована проверкой «uninitialized».
|
||||||
|
- Арифметика: overflow-checks = true в release-профиле + повсеместные checked_add/sub/mul. Парсинг везде с проверкой границ.
|
||||||
|
- manager_allowance PDA — единственный из payments, чей адрес проверяется корректно во всех путях (строки 645–646, 739–740).
|
||||||
|
- shine_login_guard — stateless, без аккаунтов и средств; рисков безопасности не несёт.
|
||||||
|
|
||||||
|
---
|
||||||
|
Приоритет действий
|
||||||
|
|
||||||
|
1. Critical #1 — добавить проверку адреса+владельца economy-config в create/update shine_users. Тривиальный фикс, помощник find_users_economy_config_pda уже есть.
|
||||||
|
2. Critical #2 — добавить проверку адресов всех singleton-PDA во все инструкции shine_payments (минимум coef_limit_pda/config_pda/queues_pda в step_payout и change_ticket_recipient).
|
||||||
|
3. Medium — ужесточить парсинг Pyth (owner + feed_id + verification_level), либо перейти на завендоренный SDK.
|
||||||
|
4. Low — учесть griefing-DoS на предсказуемых адресах тикетов.
|
||||||
|
|
||||||
|
Обе критические находки относятся к одному классу (Solana «missing ownership/address check» — самая частая категория эксплойтов), их стоит закрыть до любого деплоя в mainnet. Изменений в код я не вносил — это только анализ; готов подготовить патчи на оба критических пункта, если подтвердите.
|
||||||
104
Dev_Docs/deploy/README.md
Normal file
104
Dev_Docs/deploy/README.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Деплой SHiNE (шаблон)
|
||||||
|
|
||||||
|
Этот раздел хранит актуальные инструкции по деплою.
|
||||||
|
|
||||||
|
## Базовый сервер
|
||||||
|
|
||||||
|
- SSH: `player@shineup.me`
|
||||||
|
- Домен: `shineup.me`
|
||||||
|
- Базовый путь: `/home/player`
|
||||||
|
|
||||||
|
Для всех рабочих инструкций и скриптов использовать доменное имя `shineup.me`, а не фиксированный IP:
|
||||||
|
|
||||||
|
- актуальный IP должен браться через DNS-резолв на момент подключения;
|
||||||
|
- ручное дублирование IP в документации и deploy-скриптах не поддерживать.
|
||||||
|
|
||||||
|
## Контуры деплоя
|
||||||
|
|
||||||
|
- Production:
|
||||||
|
- SSH: `player@shineup.me`
|
||||||
|
- Домен: `shineup.me`
|
||||||
|
- IP: `185.229.109.118`
|
||||||
|
- Main test:
|
||||||
|
- SSH: `player@193.8.215.70`
|
||||||
|
- Домен: `t.shineup.me`
|
||||||
|
- IP: `193.8.215.70`
|
||||||
|
- Reserve test:
|
||||||
|
- SSH: `player@93.170.12.154`
|
||||||
|
- Домен: `test.shineup.me`
|
||||||
|
- IP: `93.170.12.154`
|
||||||
|
|
||||||
|
## Локальные команды
|
||||||
|
|
||||||
|
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`
|
||||||
|
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`
|
||||||
|
- Production server deploy: `./gradlew deployServerProduction`
|
||||||
|
- Production UI deploy: `./gradlew deployUIProduction`
|
||||||
|
- Reserve test server deploy: `./gradlew deployServerTest`
|
||||||
|
- Reserve test UI deploy: `./gradlew deployUITest`
|
||||||
|
- Локальный запуск: `./gradlew startLocal`
|
||||||
|
|
||||||
|
## Политика подтверждений
|
||||||
|
|
||||||
|
- `shineup.me` — production.
|
||||||
|
- Любые изменения на `shineup.me`, включая deploy сервера, deploy UI, конфиги, перезапуски и миграции, делать только после отдельного явного подтверждения пользователя.
|
||||||
|
- Если пользователь пишет просто `задеплой` без уточнения, по умолчанию это означает deploy на `t.shineup.me`.
|
||||||
|
|
||||||
|
## Main test deploy (`t.shineup.me`)
|
||||||
|
|
||||||
|
- Это основной сервер для тестов.
|
||||||
|
- `deployServer` и `deployUI` по умолчанию направлены именно сюда.
|
||||||
|
- Серверный deploy не запускает JUnit/IT-тесты на удалённом сервере.
|
||||||
|
- `deployServer` / `deployServerTest2` делают:
|
||||||
|
- сборку fat-jar локально;
|
||||||
|
- синхронизацию `data/` и `shine.sqlite` с production `shineup.me`;
|
||||||
|
- перенос `application.properties` с production с поправкой `server.ui.indexPath` на `/home/player/SHiNE/shine-ui/index.html`;
|
||||||
|
- установку `systemd` unit на `193.8.215.70`;
|
||||||
|
- перезапуск `shine-server.service`;
|
||||||
|
- установку/проверку Caddy для `t.shineup.me`.
|
||||||
|
- `deployUI` / `deployUITest2` публикуют UI в `/home/player/SHiNE/shine-ui` на `193.8.215.70`.
|
||||||
|
|
||||||
|
## Reserve test deploy (`test.shineup.me`)
|
||||||
|
|
||||||
|
- `test.shineup.me` считается резервным тестовым сервером.
|
||||||
|
- Его настройки и адрес не менять без отдельной задачи.
|
||||||
|
- Задачи `deployServerTest` и `deployUITest` считаются резервными и требуют отдельной причины.
|
||||||
|
|
||||||
|
## UI-деплой и Caddy (обязательно)
|
||||||
|
|
||||||
|
- Целевая директория UI-деплоя: `/home/player/SHiNE/shine-ui`.
|
||||||
|
- `Caddyfile` на сервере должен смотреть в ту же директорию через `root * /home/player/SHiNE/shine-ui`.
|
||||||
|
- В `deploy_shine-PWA.sh` добавлена проверка: скрипт ищет блок `shineup.me { ... }` (или значение `EXPECTED_CADDY_SITE`) и проверяет `root` внутри этого блока.
|
||||||
|
- Если `root` внутри целевого блока не совпадает, деплой прерывается с ошибкой.
|
||||||
|
- Для ручного обхода проверки (только осознанно): `ALLOW_CADDY_MISMATCH=1 ./gradlew deployUI`.
|
||||||
|
- При необходимости можно явно переопределить путь деплоя:
|
||||||
|
- `REMOTE_UI_DIR=/нужный/путь ./gradlew deployUI`
|
||||||
|
- `EXPECTED_CADDY_UI_ROOT=/нужный/путь ./gradlew deployUI`
|
||||||
|
- `EXPECTED_CADDY_SITE=example.com ./gradlew deployUI`
|
||||||
|
|
||||||
|
## Временные тестовые сайты Solana tickets
|
||||||
|
|
||||||
|
- Для HTML UI программы `shine_payments` используется отдельный временный тестовый сайт.
|
||||||
|
- Основной каталог публикации:
|
||||||
|
- `/home/player/sites/test-solana-tickets.shineup.me`
|
||||||
|
- Рабочие домены:
|
||||||
|
- `https://test-solana-tickets.shineup.me`
|
||||||
|
- `https://test-solana-tickets.shiningpeople.ru`
|
||||||
|
- Назначение:
|
||||||
|
- ручная проверка сценариев покупки билетов;
|
||||||
|
- проверка DAO-инструментов и лимитов менеджеров;
|
||||||
|
- проверка ручного добавления билетов и `step_payout`.
|
||||||
|
- Эти сайты не считать основным UI SHiNE; это отдельная тестовая публикация под Solana-часть.
|
||||||
|
|
||||||
|
### Важно для локального UI (history-router / Ctrl+F5)
|
||||||
|
|
||||||
|
- Локальный UI **обязательно** поднимать только через `./gradlew startLocal`.
|
||||||
|
- Эта задача запускает `scripts/local_spa_server.py`, который делает SPA fallback: любой неизвестный путь (`/m/...`, `/channel/...`) возвращает `index.html`.
|
||||||
|
- Это обязательно для корректной работы `Ctrl+F5` на внутренних роутов без `404`.
|
||||||
|
- Рабочий URL выводится задачей в консоль в формате: `http://localhost:<WEB_PORT>/?localWsPort=<WS_PORT>`.
|
||||||
|
|
||||||
|
## Обязательные правила
|
||||||
|
|
||||||
|
1. Перед серверным деплоем проверить локально.
|
||||||
|
2. При нестандартном деплое (другой хост, другая структура, ручные шаги) обязательно уточнить у пользователя, нужно ли обновить этот шаблон.
|
||||||
|
3. Если деплой-процесс изменился, этот файл и файлы в `servers/` обновлять в том же коммите.
|
||||||
36
Dev_Docs/deploy/agent-bot-coder-local-systemd.md
Normal file
36
Dev_Docs/deploy/agent-bot-coder-local-systemd.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Локальный деплой SHiNE-agent-bot-coder (systemd, пользователь ai)
|
||||||
|
|
||||||
|
## Где находится сервис
|
||||||
|
- Папка сервиса: `SHiNE-agent-bot-coder/`
|
||||||
|
- Systemd unit: `SHiNE-agent-bot-coder/scripts/systemd/shine-agent-bot-coder.service`
|
||||||
|
- Скрипт установки: `SHiNE-agent-bot-coder/scripts/systemd/install-local-systemd.sh`
|
||||||
|
|
||||||
|
## Предусловия
|
||||||
|
1. Заполнен `.env` на основе `.env.example`.
|
||||||
|
2. Доступен рабочий Codex CLI:
|
||||||
|
- `/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl`
|
||||||
|
3. На машине установлен `systemd --user`.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
Из корня репозитория:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash SHiNE-agent-bot-coder/scripts/systemd/install-local-systemd.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
1. проверяет наличие `python3`;
|
||||||
|
2. копирует unit в `~/.config/systemd/user/`;
|
||||||
|
3. делает `systemctl --user daemon-reload`;
|
||||||
|
4. включает автозапуск и стартует сервис.
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
```bash
|
||||||
|
systemctl --user status shine-agent-bot-coder --no-pager
|
||||||
|
journalctl --user -u shine-agent-bot-coder -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Перезапуск после изменений
|
||||||
|
```bash
|
||||||
|
systemctl --user restart shine-agent-bot-coder
|
||||||
|
```
|
||||||
43
Dev_Docs/deploy/servers/193.8.215.70_test2_main.md
Normal file
43
Dev_Docs/deploy/servers/193.8.215.70_test2_main.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Сервер `193.8.215.70` — основной test (`t.shineup.me`)
|
||||||
|
|
||||||
|
- Пользователь: `player`
|
||||||
|
- Домен: `t.shineup.me`
|
||||||
|
- Логин сервера: `tshineupme`
|
||||||
|
- Каталог SHiNE: `/home/player/SHiNE`
|
||||||
|
- UI публикация для Caddy: `/home/player/SHiNE/shine-ui`
|
||||||
|
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||||
|
- Данные: `/home/player/SHiNE/shine-server/data/`
|
||||||
|
- `shine.sqlite`
|
||||||
|
- `*.bch`
|
||||||
|
- Логи сервера: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||||
|
|
||||||
|
## Сервисы
|
||||||
|
|
||||||
|
- `shine-server.service` (systemd)
|
||||||
|
- `caddy.service` (systemd)
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
- Это основной сервер для тестов SHiNE.
|
||||||
|
- Default deploy по умолчанию должен идти сюда.
|
||||||
|
- Источник данных для тестовой БД: production `shineup.me`.
|
||||||
|
|
||||||
|
## Caddy
|
||||||
|
|
||||||
|
- Конфиг: `/etc/caddy/Caddyfile`
|
||||||
|
- Сайты:
|
||||||
|
- `t.shineup.me`
|
||||||
|
- `agent.shiningpeople.ru`
|
||||||
|
- Для `t.shineup.me`:
|
||||||
|
- `root * /home/player/SHiNE/shine-ui`
|
||||||
|
- `try_files {path} /index.html`
|
||||||
|
- `reverse_proxy /ws* -> 127.0.0.1:7070`
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
- Default server deploy:
|
||||||
|
- `./gradlew deployServer`
|
||||||
|
- `./gradlew deployServerTest2`
|
||||||
|
- Default UI deploy:
|
||||||
|
- `./gradlew deployUI`
|
||||||
|
- `./gradlew deployUITest2`
|
||||||
40
Dev_Docs/deploy/servers/93.170.12.154_rapsberry.md
Normal file
40
Dev_Docs/deploy/servers/93.170.12.154_rapsberry.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Сервер `93.170.12.154` — резервный test (`test.shineup.me`)
|
||||||
|
|
||||||
|
- Пользователь: `player`
|
||||||
|
- Каталог SHiNE: `/home/player/SHiNE`
|
||||||
|
- Домен: `test.shineup.me`
|
||||||
|
- Роль: резервный тестовый сервер
|
||||||
|
- UI публикация для Caddy: `/home/player/SHiNE/shine-ui`
|
||||||
|
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||||
|
- Данные: `/home/player/SHiNE/shine-server/data/`
|
||||||
|
- `shine.sqlite`
|
||||||
|
- `*.bch`
|
||||||
|
- Логи сервера: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||||
|
|
||||||
|
## Сервисы
|
||||||
|
|
||||||
|
- `shine-server.service` (systemd)
|
||||||
|
- `caddy.service` (systemd)
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
- Резервный тестовый сервер для SHiNE.
|
||||||
|
- Источник данных для тестовой БД: production `shineup.me`.
|
||||||
|
- Пока не использовать для обычного deploy.
|
||||||
|
- Основной прод-сервер: `shineup.me` (`185.229.109.118`).
|
||||||
|
|
||||||
|
## Caddy
|
||||||
|
|
||||||
|
- Конфиг: `/etc/caddy/Caddyfile`
|
||||||
|
- Настройки:
|
||||||
|
- `no-store/no-cache` заголовки;
|
||||||
|
- `try_files {path} /index.html` (SPA fallback);
|
||||||
|
- `reverse_proxy /ws* -> 127.0.0.1:7070`;
|
||||||
|
- целевой сайт: `test.shineup.me`.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
- Резервные задачи:
|
||||||
|
- `./gradlew deployServerTest`
|
||||||
|
- `./gradlew deployUITest`
|
||||||
|
- Эти задачи пока не использовать без отдельной причины.
|
||||||
47
Dev_Docs/deploy/servers/shineup.me_main.md
Normal file
47
Dev_Docs/deploy/servers/shineup.me_main.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Сервер `shineup.me` — основной
|
||||||
|
|
||||||
|
- SSH: `player@shineup.me`
|
||||||
|
- Определение IP: через DNS-резолв домена `shineup.me` на момент подключения
|
||||||
|
- Пользователь: `player`
|
||||||
|
- Базовый путь: `/home/player`
|
||||||
|
- Каталог SHiNE: `/home/player/SHiNE`
|
||||||
|
- UI публикация: `/home/player/SHiNE/shine-ui`
|
||||||
|
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||||
|
- Данные: `/home/player/SHiNE/shine-server/data/`
|
||||||
|
- Логи сервера: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||||
|
|
||||||
|
## Сервисы
|
||||||
|
|
||||||
|
- `shine-server.service` (systemd)
|
||||||
|
- `caddy.service` (systemd)
|
||||||
|
|
||||||
|
## Caddy
|
||||||
|
|
||||||
|
- Активный конфиг (через systemd `ExecStart`): `/home/player/SHiNE/caddy/Caddyfile`
|
||||||
|
- Для UI:
|
||||||
|
- `root * /home/player/SHiNE/shine-ui`
|
||||||
|
- `try_files {path} /index.html` (SPA fallback)
|
||||||
|
- no-cache заголовки
|
||||||
|
- `reverse_proxy /ws* -> 127.0.0.1:7070`
|
||||||
|
|
||||||
|
## Дополнительно
|
||||||
|
|
||||||
|
- Для отдельной админки `shine_payments` используется каталог:
|
||||||
|
- `/home/player/sites/test-solana-tickets.shineup.me`
|
||||||
|
- Эта публикация используется как временный тестовый сайт для сценариев покупки билетов и выплат `shine_payments`.
|
||||||
|
- Домены этой публикации:
|
||||||
|
- `https://test-solana-tickets.shineup.me`
|
||||||
|
- `https://test-solana-tickets.shiningpeople.ru`
|
||||||
|
- Для всех deploy-скриптов и инструкций использовать именно `player@shineup.me`, без жёсткой фиксации IP.
|
||||||
|
|
||||||
|
## Правило изменений
|
||||||
|
|
||||||
|
- `shineup.me` — production.
|
||||||
|
- Любые изменения на этом сервере делать только после отдельного явного подтверждения пользователя.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
- Production deploy-задачи:
|
||||||
|
- `./gradlew deployServerProduction`
|
||||||
|
- `./gradlew deployUIProduction`
|
||||||
|
- Default deploy-задачи `./gradlew deployServer` и `./gradlew deployUI` сюда больше не относятся.
|
||||||
125
Dev_Docs/Инициализация_Solana_регистрации/README.md
Normal file
125
Dev_Docs/Инициализация_Solana_регистрации/README.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# Деплой и инициализация Solana-регистрации (две обязательные программы)
|
||||||
|
|
||||||
|
## Коротко
|
||||||
|
|
||||||
|
Для рабочей регистрации пользователя нужны **обе** программы:
|
||||||
|
|
||||||
|
1. `shine_users` — хранение и обновление `user_pda`, economy-конфиг, логика регистрации.
|
||||||
|
2. `shine_login_guard` — проверка/классификация логина (CPI из `shine_users`).
|
||||||
|
|
||||||
|
Если задеплоена только одна из них — регистрация неработоспособна.
|
||||||
|
|
||||||
|
## Актуальные адреса (devnet)
|
||||||
|
|
||||||
|
- `shine_users` (регистрация пользователей):
|
||||||
|
`3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ`
|
||||||
|
- `shine_login_guard`:
|
||||||
|
`3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo`
|
||||||
|
- `shine_payments`:
|
||||||
|
`c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW`
|
||||||
|
|
||||||
|
## Подтверждение деплоя
|
||||||
|
|
||||||
|
- Сеть: `https://api.devnet.solana.com`
|
||||||
|
- `shine_users`:
|
||||||
|
- `Program ID`: `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ`
|
||||||
|
- TX deploy: `5VzfpSirFCRqPUZfvAt3eADY9KnowW79PKZ1pCQAa2DJGiztj4dUYYXrSQNmWEhPVu6mPSDfcuHzFyEVmoKLa9DM`
|
||||||
|
- `shine_login_guard`:
|
||||||
|
- `Program ID`: `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo`
|
||||||
|
- TX deploy: `5iptngPYrLLjPE3Xby24zyNW3edVUnBNLBx785vjojMoq5JNLFNQvLNAm3jNYHbpf2B36qtbpTNzcvUNyRDqm1Mf`
|
||||||
|
|
||||||
|
## Порядок деплоя (devnet)
|
||||||
|
|
||||||
|
1. Убедиться, что CLI смотрит в devnet и у кошелька есть SOL.
|
||||||
|
2. Собрать и задеплоить `shine_login_guard`.
|
||||||
|
3. Собрать и задеплоить `shine_users`.
|
||||||
|
4. Проверить, что адреса совпадают между:
|
||||||
|
- `Anchor.toml`
|
||||||
|
- `declare_id!` в `programs/*/src/lib.rs`
|
||||||
|
- UI/серверными константами.
|
||||||
|
5. Выполнить `init_users_economy_config` (один раз на программу `shine_users`).
|
||||||
|
|
||||||
|
Пример команд:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd shine-solana/shine
|
||||||
|
solana config get
|
||||||
|
solana balance
|
||||||
|
|
||||||
|
anchor build -p shine_login_guard
|
||||||
|
anchor deploy -p shine_login_guard
|
||||||
|
|
||||||
|
anchor build -p shine_users
|
||||||
|
anchor deploy -p shine_users
|
||||||
|
```
|
||||||
|
|
||||||
|
## Куда вписаны адреса в проекте
|
||||||
|
|
||||||
|
### UI
|
||||||
|
|
||||||
|
- Общие Solana-константы:
|
||||||
|
- `shine-UI/js/solana-programs.js`
|
||||||
|
- Страница инициализации:
|
||||||
|
- `shine-UI/js/pages/solana-users-init-view.js`
|
||||||
|
- Переход на страницу:
|
||||||
|
- `shine-UI/js/pages/developer-settings-view.js`
|
||||||
|
|
||||||
|
### Browser plugin wallet
|
||||||
|
|
||||||
|
- Резолвер PDA и проверка адресов:
|
||||||
|
- `SHiNE-browser-plugin-wallet/js/lib/shine-server-resolver.js`
|
||||||
|
- Проверка текущего wallet по PDA:
|
||||||
|
- `SHiNE-browser-plugin-wallet/background.js`
|
||||||
|
- Отображение состояния в popup:
|
||||||
|
- `SHiNE-browser-plugin-wallet/popup.js`
|
||||||
|
|
||||||
|
### Сервер
|
||||||
|
|
||||||
|
- Серверные константы Solana:
|
||||||
|
- `shine-server-config/src/main/java/utils/config/SolanaProgramsConfig.java`
|
||||||
|
|
||||||
|
### Solana / Anchor
|
||||||
|
|
||||||
|
- `shine-solana/shine/Anchor.toml`
|
||||||
|
- `shine-solana/shine/programs/shine_users/src/lib.rs` (`declare_id!`)
|
||||||
|
- `shine-solana/shine/programs/shine_login_guard/src/lib.rs` (`declare_id!`)
|
||||||
|
- `shine-solana/shine/programs/shine_payments/src/lib.rs` (`declare_id!`)
|
||||||
|
|
||||||
|
### Где ещё нужно синхронизировать адреса после нового deploy
|
||||||
|
|
||||||
|
- UI-константы и все потребители в `shine-UI/js/*`
|
||||||
|
- browser-plugin-wallet
|
||||||
|
- серверный `SolanaProgramsConfig.java`
|
||||||
|
- Anchor-конфиг и `declare_id!` в Solana-модуле
|
||||||
|
- документы:
|
||||||
|
- `Dev_Docs/Solana/user_pda/README.md`
|
||||||
|
- `shine-solana/shine/doc/programs/shine_users.md`
|
||||||
|
- `shine-solana/shine/doc/devnet_keys_and_deploy.md`
|
||||||
|
|
||||||
|
## Как запустить инициализацию economy PDA
|
||||||
|
|
||||||
|
1. Открыть UI.
|
||||||
|
2. Перейти: `Профиль -> Настройки -> Настройки разработчика -> Solana: init регистрации`.
|
||||||
|
3. Подключить кошелёк (Phantom, devnet).
|
||||||
|
4. Нажать `Запустить init_users_economy_config`.
|
||||||
|
5. Дождаться статуса `Успешно`.
|
||||||
|
|
||||||
|
Страница сама вычисляет PDA `users_economy_config` по seed:
|
||||||
|
|
||||||
|
- seed: `shine_users_economy_config`
|
||||||
|
- program: `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ`
|
||||||
|
|
||||||
|
## Кто оплачивает create/update user_pda
|
||||||
|
|
||||||
|
- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `clientKey`.
|
||||||
|
- В UI это означает, что Solana fee payer всегда берётся из `device`-ключа пользователя/сервера.
|
||||||
|
- `rootKey` нужен для подписи unsigned PDA-записи, но не оплачивает транзакцию.
|
||||||
|
- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `clientKey`.
|
||||||
|
|
||||||
|
## Важно
|
||||||
|
|
||||||
|
- `init_users_economy_config` выполняется один раз на программу.
|
||||||
|
Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение).
|
||||||
|
- Серверные приватные ключи для Solana не используются как отдельный backend-wallet: в UI/server UI транзакцию оплачивает именно `clientKey`, а содержимое записи подписывает `rootKey`.
|
||||||
|
- `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина.
|
||||||
|
Несовпадение адреса приведёт к ошибке регистрации.
|
||||||
118
Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md
Normal file
118
Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# ESP Pairing и режимы подключения
|
||||||
|
|
||||||
|
Этот документ фиксирует актуальные режимы входа/подключения в SHiNE. Он нужен как отдельная точка входа по сценариям подключения, чтобы не смешивать обычную авторизацию и серверный pairing через доверенное уже авторизованное устройство пользователя.
|
||||||
|
|
||||||
|
## 1. Текущие режимы
|
||||||
|
|
||||||
|
### 1. Создание новой сессии через `clientKey`
|
||||||
|
|
||||||
|
Поток:
|
||||||
|
|
||||||
|
`AuthChallenge -> CreateAuthSession`
|
||||||
|
|
||||||
|
Смысл:
|
||||||
|
|
||||||
|
- новое устройство уже владеет приватным `clientKey`;
|
||||||
|
- сервер проверяет подпись `clientKey`;
|
||||||
|
- создаётся обычная активная сессия пользователя;
|
||||||
|
- этот поток остаётся без изменений.
|
||||||
|
|
||||||
|
### 2. Повторный вход в существующую сессию через `sessionKey`
|
||||||
|
|
||||||
|
Поток:
|
||||||
|
|
||||||
|
`SessionChallenge -> SessionLogin`
|
||||||
|
|
||||||
|
Смысл:
|
||||||
|
|
||||||
|
- устройство уже владеет приватным `sessionKey`;
|
||||||
|
- сервер проверяет подпись `sessionKey`;
|
||||||
|
- соединение снова входит в существующую сессию;
|
||||||
|
- этот поток тоже остаётся без изменений.
|
||||||
|
|
||||||
|
## 2. Добавление сессии через доверенное устройство пользователя
|
||||||
|
|
||||||
|
Новый поток не заменяет обычный логин, а живёт рядом с ним.
|
||||||
|
|
||||||
|
Цель:
|
||||||
|
|
||||||
|
- новое устройство знает `login`, а `pairing password` используется только если он включён на доверённом устройстве;
|
||||||
|
- сервер использует пароль только как фильтр от мусора;
|
||||||
|
- реальное доверие даёт любая уже онлайн доверенная сессия пользователя;
|
||||||
|
- сервер не выдаёт приватные ключи сам от себя.
|
||||||
|
|
||||||
|
Текущий поток:
|
||||||
|
|
||||||
|
1. Любая доверенная сессия пользователя создаёт на сервере pairing-настройку:
|
||||||
|
`UpsertEspPairingSettings`
|
||||||
|
2. Новое устройство создаёт pending-заявку:
|
||||||
|
`StartEspPairing`
|
||||||
|
3. Онлайн доверенная сессия видит список активных заявок:
|
||||||
|
`ListEspPairingRequests`
|
||||||
|
4. Доверенная сессия либо подтверждает заявку:
|
||||||
|
`ApproveEspPairing`
|
||||||
|
5. Либо отклоняет:
|
||||||
|
`RejectEspPairing`
|
||||||
|
6. Новое устройство читает результат:
|
||||||
|
`GetEspPairingStatus`
|
||||||
|
|
||||||
|
## 3. Что именно делает сервер
|
||||||
|
|
||||||
|
- хранит включённость pairing и optional `passwordHash` в формате `sha256$<hex>`;
|
||||||
|
- хранит pairing-заявки всех статусов, но в список активных для доверённого устройства отдаёт только pending `created`;
|
||||||
|
- рассчитывает короткий код `shortCode` из `10` цифр;
|
||||||
|
- рассчитывает длинный `fingerprintB58` из `SHA-256` заявки;
|
||||||
|
- уведомляет онлайн доверенные сессии событием `IncomingEspPairingRequest`, если такие сессии подключены;
|
||||||
|
- хранит переданный `encryptedPayload` как непрозрачную строку и не анализирует его содержимое.
|
||||||
|
|
||||||
|
## 4. Чего сервер в этом режиме не делает
|
||||||
|
|
||||||
|
- не передаёт приватный `clientKey`;
|
||||||
|
- не расшифровывает `encryptedPayload`;
|
||||||
|
- не проверяет криптографию содержимого payload;
|
||||||
|
- не делает клиентский UI;
|
||||||
|
- не навязывает конкретную схему `Ed25519 -> X25519` в коде сервера.
|
||||||
|
|
||||||
|
Это намеренно: сервер остаётся безопасным каркасом маршрутизации и состояния, а E2E-логика упаковки ключей живёт на клиентах и ESP-устройствах.
|
||||||
|
|
||||||
|
## 5. Роли и ограничения
|
||||||
|
|
||||||
|
- любая уже авторизованная доверенная сессия пользователя может вызывать:
|
||||||
|
- `UpsertEspPairingSettings`
|
||||||
|
- `ListEspPairingRequests`
|
||||||
|
- `ApproveEspPairing`
|
||||||
|
- `RejectEspPairing`
|
||||||
|
- новое устройство может вызвать `StartEspPairing` и `GetEspPairingStatus` без уже существующей авторизованной сессии;
|
||||||
|
- `payloadType` поддерживается в вариантах:
|
||||||
|
- `1` — минимальный пакет
|
||||||
|
- `2` — расширенный пакет
|
||||||
|
- `3` — полный пакет
|
||||||
|
|
||||||
|
Сервер не интерпретирует эти три типа глубже, а только фиксирует их в состоянии заявки.
|
||||||
|
|
||||||
|
## 6. Статусы pairing-заявки
|
||||||
|
|
||||||
|
- `created` — заявка создана и ждёт решения доверенной сессии;
|
||||||
|
- `approved` — доверенная сессия подтвердила и приложила `encryptedPayload`;
|
||||||
|
- `rejected` — доверенная сессия отклонила заявку;
|
||||||
|
- `expired` — TTL заявки истёк до подтверждения.
|
||||||
|
|
||||||
|
## 7. Практический смысл
|
||||||
|
|
||||||
|
Эта схема даёт нужное разделение доверия:
|
||||||
|
|
||||||
|
- пароль на сервере, если он включён, только отсеивает лишних;
|
||||||
|
- онлайн доверенная сессия решает, добавлять ли новую сессию;
|
||||||
|
- сервер остаётся маршрутизатором и хранилищем состояния, а не владельцем секретов.
|
||||||
|
|
||||||
|
Текущий формат pairing-пароля:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sha256$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Связанный документ по внешнему кошельку
|
||||||
|
|
||||||
|
Для отдельного RPC-взаимодействия между браузерным wallet-расширением и ESP32 см. документ:
|
||||||
|
|
||||||
|
- [Формат_взаимодействия_внешнего_кошелька_и_ESP32.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Dev_Docs/Протоколы/Формат_взаимодействия_внешнего_кошелька_и_ESP32.md)
|
||||||
104
Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md
Normal file
104
Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# SHiNE Arweave Wallet Derivation v1
|
||||||
|
|
||||||
|
Сокращение: **SAWD-v1**.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
Из 32-байтного `clientKey32` пользователя получить один и тот же нативный Arweave RSA-4096 JWK wallet и один и тот же Arweave address.
|
||||||
|
|
||||||
|
## Вход
|
||||||
|
- `clientKey32`: ровно 32 байта.
|
||||||
|
- Если исходный `client.key` хранится как Ed25519 PKCS8 base64, нужно извлечь последние 32 байта из PKCS8.
|
||||||
|
- Если используется Solana keypair JSON на 64 байта, используются только `bytes[0..31]`.
|
||||||
|
|
||||||
|
## Выход
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"derivation": "SAWD-v1",
|
||||||
|
"jwk": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"e": "AQAB",
|
||||||
|
"n": "...",
|
||||||
|
"d": "...",
|
||||||
|
"p": "...",
|
||||||
|
"q": "...",
|
||||||
|
"dp": "...",
|
||||||
|
"dq": "...",
|
||||||
|
"qi": "..."
|
||||||
|
},
|
||||||
|
"owner": "...",
|
||||||
|
"address": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Где:
|
||||||
|
- `owner = jwk.n`
|
||||||
|
- `address = base64url_no_padding(SHA-256(unsigned_big_endian_bytes(n)))`
|
||||||
|
|
||||||
|
## Константы
|
||||||
|
- `DERIVATION_NAME = "SAWD-v1"`
|
||||||
|
- `MASTER_LABEL = "SHINE/ARWEAVE/RSA4096/SAWD-v1/MASTER"`
|
||||||
|
- `STREAM_LABEL = "SHINE/ARWEAVE/RSA4096/SAWD-v1/STREAM"`
|
||||||
|
- `MR_LABEL = "SHINE/ARWEAVE/RSA4096/SAWD-v1/MILLER-RABIN"`
|
||||||
|
- `RSA_BITS = 4096`
|
||||||
|
- `PRIME_BITS = 2048`
|
||||||
|
- `PUBLIC_EXPONENT = 65537`
|
||||||
|
- `MILLER_RABIN_ROUNDS = 64`
|
||||||
|
- `SMALL_PRIME_LIMIT = 10000`
|
||||||
|
|
||||||
|
## Алгоритм
|
||||||
|
1. Проверить `clientKey32.length == 32`.
|
||||||
|
2. `masterSeed32 = HMAC-SHA256(key = UTF8(MASTER_LABEL), message = clientKey32)`.
|
||||||
|
3. Реализовать `deriveBytes(label, length)`:
|
||||||
|
- `output = empty`
|
||||||
|
- `counter = 0`
|
||||||
|
- while `output.length < length`:
|
||||||
|
- `block = HMAC-SHA256(key = masterSeed32, message = UTF8(STREAM_LABEL) || UTF8("/") || UTF8(label) || UTF8("/") || uint64_be(counter))`
|
||||||
|
- `output = output || block`
|
||||||
|
- `counter++`
|
||||||
|
- вернуть первые `length` байт.
|
||||||
|
4. Для `p` и `q`:
|
||||||
|
- `raw = deriveBytes(label + "/" + index, 256)`
|
||||||
|
- `candidate = unsigned_big_endian_integer(raw)`
|
||||||
|
- `candidate = candidate OR 2^2047`
|
||||||
|
- `candidate = candidate OR 1`
|
||||||
|
- Проверить:
|
||||||
|
- `bitLength(candidate) == 2048`
|
||||||
|
- `candidate odd`
|
||||||
|
- не делится на малые простые `<= 10000`
|
||||||
|
- `gcd(candidate - 1, 65537) == 1`
|
||||||
|
- проходит Miller-Rabin `64 rounds`
|
||||||
|
5. Базы Miller-Rabin детерминированные:
|
||||||
|
- `baseBytes = HMAC-SHA256(key = masterSeed32, message = UTF8(MR_LABEL) || UTF8("/") || UTF8(label) || UTF8("/") || uint64_be(index) || UTF8("/") || uint32_be(round))`
|
||||||
|
- `a = 2 + (unsigned_big_endian_integer(baseBytes) mod (candidate - 3))`
|
||||||
|
6. `p = derivePrime("p")`, `q = derivePrime("q")`.
|
||||||
|
7. Если `p == q`, продолжить поиск `q`.
|
||||||
|
8. Если `p > q`, поменять местами. В SAWD-v1 всегда `p < q`.
|
||||||
|
9. `n = p * q`
|
||||||
|
10. `e = 65537`
|
||||||
|
11. `lambda = lcm(p - 1, q - 1)`
|
||||||
|
12. `d = modular_inverse(e, lambda)`
|
||||||
|
13. `dp = d mod (p - 1)`
|
||||||
|
14. `dq = d mod (q - 1)`
|
||||||
|
15. `qi = modular_inverse(q, p)`
|
||||||
|
16. Сформировать JWK:
|
||||||
|
- `kty = "RSA"`
|
||||||
|
- `e = "AQAB"`
|
||||||
|
- `n,d,p,q,dp,dq,qi = base64url unsigned big-endian integer without padding`
|
||||||
|
17. `owner = jwk.n`
|
||||||
|
18. `address = base64url_no_padding(SHA-256(unsigned_big_endian_bytes(n)))`
|
||||||
|
|
||||||
|
## Запрещено
|
||||||
|
- `crypto.generateKeyPair`
|
||||||
|
- `WebCrypto generateKey`
|
||||||
|
- `KeyPairGenerator`
|
||||||
|
- `SecureRandom(seed)`
|
||||||
|
- `Math.random`
|
||||||
|
- системный `random`
|
||||||
|
- ArDrive CLI
|
||||||
|
- Turbo
|
||||||
|
- внешний API для генерации ключа
|
||||||
|
- сохранение приватного JWK
|
||||||
|
|
||||||
|
## Версионирование стандарта
|
||||||
|
Если меняется любая константа или шаг алгоритма — это уже **SAWD-v2**.
|
||||||
|
Пользователи, созданные на SAWD-v1, должны продолжать восстанавливаться через SAWD-v1.
|
||||||
@ -0,0 +1,311 @@
|
|||||||
|
# Формат взаимодействия внешнего кошелька и ESP32
|
||||||
|
|
||||||
|
Этот документ фиксирует актуальный формат взаимодействия между внешним браузерным wallet-расширением SHiNE и устройством `ESP32-S3-Touch-AMOLED-2.16`.
|
||||||
|
|
||||||
|
Документ описывает:
|
||||||
|
|
||||||
|
- как расширение получает текущий активный публичный ключ кошелька с ESP32;
|
||||||
|
- как расширение отправляет на ESP32 запрос подписи транзакции;
|
||||||
|
- что именно считается активным кошельком на ESP32;
|
||||||
|
- какие проверки и UI-реакции ожидаются в браузерном расширении и на устройстве;
|
||||||
|
- какие ограничения действуют в текущей версии протокола.
|
||||||
|
|
||||||
|
## 1. Общая идея
|
||||||
|
|
||||||
|
Устройство ESP32 хранит `master secret` пользователя и локально умеет выводить несколько кошельков из одного секрета.
|
||||||
|
|
||||||
|
На устройстве в UI пользователь выбирает текущий активный кошелёк:
|
||||||
|
|
||||||
|
- `client.key`
|
||||||
|
- `root.key`
|
||||||
|
- `custom`
|
||||||
|
|
||||||
|
Для `custom` используется derivation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sha256(base64(secret32) + "|wallet." + customName)
|
||||||
|
```
|
||||||
|
|
||||||
|
Браузерное расширение не указывает ESP32, какой кошелёк нужно вернуть в первом запросе. Оно просто спрашивает:
|
||||||
|
|
||||||
|
```text
|
||||||
|
какой кошелёк сейчас активен на устройстве
|
||||||
|
```
|
||||||
|
|
||||||
|
ESP32 возвращает:
|
||||||
|
|
||||||
|
- тип текущего активного кошелька;
|
||||||
|
- его публичный ключ `Base58`.
|
||||||
|
|
||||||
|
## 2. Транспорт и маршрут
|
||||||
|
|
||||||
|
Текущий формат использует уже существующую `wallet-session` браузерного расширения.
|
||||||
|
|
||||||
|
Схема маршрута:
|
||||||
|
|
||||||
|
`browser extension -> SHiNE server -> homeserver session on ESP32 -> SHiNE server -> browser extension`
|
||||||
|
|
||||||
|
В текущем формате:
|
||||||
|
|
||||||
|
- отдельная цифровая подпись payload не добавляется;
|
||||||
|
- отдельное E2E-шифрование для wallet RPC не добавляется;
|
||||||
|
- используется существующая авторизованная `wallet-session`, транспорт `WSS` и server-side маршрут через уже существующую операцию `CallSignalToSession`.
|
||||||
|
|
||||||
|
## 3. Запрос текущего публичного ключа кошелька
|
||||||
|
|
||||||
|
### 3.1. Назначение
|
||||||
|
|
||||||
|
Операция нужна, чтобы браузерное расширение могло узнать, какой кошелёк сейчас выбран на ESP32, и показать его пользователю перед дальнейшими действиями.
|
||||||
|
|
||||||
|
### 3.2. Формат запроса
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"operation": "get_wallet_public_key",
|
||||||
|
"requestId": "1718998123456-482193",
|
||||||
|
"timeMs": 1718998123456
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3. Поля запроса
|
||||||
|
|
||||||
|
- `v` — версия формата wallet RPC. Для текущего варианта: `1`.
|
||||||
|
- `operation` — строка операции. Для текущего запроса: `get_wallet_public_key`.
|
||||||
|
- `requestId` — идентификатор запроса, уникальный в пределах сеанса расширения. Рекомендуемый формат:
|
||||||
|
`timeMs-random`.
|
||||||
|
- `timeMs` — локальное время отправителя в миллисекундах.
|
||||||
|
|
||||||
|
### 3.4. Поведение ESP32
|
||||||
|
|
||||||
|
При получении такого запроса ESP32:
|
||||||
|
|
||||||
|
1. смотрит, какой кошелёк сейчас выбран в локальном UI;
|
||||||
|
2. вычисляет или берёт уже подготовленный публичный ключ именно этого активного кошелька;
|
||||||
|
3. возвращает тип кошелька и его `publicKeyBase58`.
|
||||||
|
|
||||||
|
Запрос не содержит:
|
||||||
|
|
||||||
|
- `walletSelector`;
|
||||||
|
- `customName`;
|
||||||
|
- `targetSessionName`.
|
||||||
|
|
||||||
|
Они намеренно не входят в текущий формат этого запроса.
|
||||||
|
|
||||||
|
## 4. Формат ответа
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"op": "get_wallet_public_key_result",
|
||||||
|
"requestId": "1718998123456-482193",
|
||||||
|
"ok": true,
|
||||||
|
"wallet": {
|
||||||
|
"type": "custom",
|
||||||
|
"publicKeyBase58": "...."
|
||||||
|
},
|
||||||
|
"timeMs": 1718998123999
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Поля ответа
|
||||||
|
|
||||||
|
- `v` — версия формата ответа. Сейчас `1`.
|
||||||
|
- `op` — строка результата операции. Сейчас `get_wallet_public_key_result`.
|
||||||
|
- `requestId` — должен совпадать с `requestId` исходного запроса.
|
||||||
|
- `ok` — признак успешного результата.
|
||||||
|
- `wallet.type` — тип активного кошелька:
|
||||||
|
- `client.key`
|
||||||
|
- `root.key`
|
||||||
|
- `custom`
|
||||||
|
- `wallet.publicKeyBase58` — публичный ключ активного кошелька в `Base58`.
|
||||||
|
- `timeMs` — время формирования ответа на стороне ESP32 в миллисекундах.
|
||||||
|
|
||||||
|
## 6. Ошибки текущего формата
|
||||||
|
|
||||||
|
Минимальный формат ошибки допускается таким:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"op": "get_wallet_public_key_result",
|
||||||
|
"requestId": "1718998123456-482193",
|
||||||
|
"ok": false,
|
||||||
|
"error": "wallet_unavailable",
|
||||||
|
"timeMs": 1718998123999
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Рекомендуемые коды ошибок:
|
||||||
|
|
||||||
|
- `wallet_unavailable` — на устройстве нельзя получить текущий кошелёк;
|
||||||
|
- `secret_not_configured` — на устройстве ещё нет корректно сохранённого секрета;
|
||||||
|
- `wallet_type_unknown` — выбранный локальный тип кошелька не распознан;
|
||||||
|
- `internal_error` — прочая локальная ошибка устройства.
|
||||||
|
|
||||||
|
## 7. Правила для браузерного расширения
|
||||||
|
|
||||||
|
После ответа `ok=true` расширение должно:
|
||||||
|
|
||||||
|
1. показать пользователю тип кошелька;
|
||||||
|
2. показать полный `publicKeyBase58`;
|
||||||
|
3. дать кнопку копирования ключа в буфер;
|
||||||
|
4. сохранить этот ключ как текущий ключ устройства для следующей операции подписи.
|
||||||
|
|
||||||
|
### 7.1. Проверка через PDA Solana
|
||||||
|
|
||||||
|
Расширение уже знает публичные ключи пользователя из Solana PDA. Поэтому оно может дополнительно проверить ответ ESP32:
|
||||||
|
|
||||||
|
- если `wallet.type = client.key`, то `publicKeyBase58` должен совпасть с `clientKey`, прочитанным из PDA;
|
||||||
|
- если `wallet.type = root.key`, то `publicKeyBase58` должен совпасть с `rootKey`, прочитанным из PDA;
|
||||||
|
- если `wallet.type = custom`, такой проверки по PDA пока нет.
|
||||||
|
|
||||||
|
При несовпадении для `client.key` или `root.key` расширение должно показать пользователю предупреждение, что возвращённый ключ не совпал с ожидаемым ключом из PDA.
|
||||||
|
|
||||||
|
## 8. Ожидаемое поведение UI расширения
|
||||||
|
|
||||||
|
### 8.1. Общий вид popup
|
||||||
|
|
||||||
|
Popup браузерного расширения должен быть узким и вытянутым по вертикали.
|
||||||
|
|
||||||
|
### 8.2. Состояние без подключения
|
||||||
|
|
||||||
|
Если `wallet-session` ещё не подключена:
|
||||||
|
|
||||||
|
- показывается кнопка `Подключить`;
|
||||||
|
- по нажатию открывается экран подключения, близкий по смыслу к сценарию `Войти через другое устройство`;
|
||||||
|
- пользователь вводит логин устройства и получает код подключения.
|
||||||
|
|
||||||
|
### 8.3. Состояние после подключения
|
||||||
|
|
||||||
|
Если `wallet-session` уже подключена:
|
||||||
|
|
||||||
|
- показывается статус `Подключено`;
|
||||||
|
- остаётся выбор homeserver;
|
||||||
|
- появляется кнопка запроса текущего кошелька;
|
||||||
|
- появляется кнопка `Отключить`.
|
||||||
|
|
||||||
|
### 8.4. Подключение кошелька с сайта
|
||||||
|
|
||||||
|
Когда сайт просит подключить кошелёк через расширение, расширение должно вести себя как обычный wallet extension:
|
||||||
|
|
||||||
|
1. показать пользователю подтверждение подключения;
|
||||||
|
2. показать, какой именно кошелёк будет подключён;
|
||||||
|
3. после подтверждения пользователя завершить подключение;
|
||||||
|
4. если пользователь отказался, не подключать кошелёк к сайту.
|
||||||
|
|
||||||
|
## 9. Запрос подписи транзакции
|
||||||
|
|
||||||
|
### 9.1. Назначение
|
||||||
|
|
||||||
|
Операция нужна, чтобы браузерное расширение могло запросить у ESP32 подпись Solana-транзакции текущим активным кошельком.
|
||||||
|
|
||||||
|
Расширение передаёт:
|
||||||
|
|
||||||
|
- публичный ключ, которым ожидается подпись;
|
||||||
|
- сериализованную транзакцию;
|
||||||
|
- комментарий, который должен быть показан на экране ESP32.
|
||||||
|
|
||||||
|
ESP32:
|
||||||
|
|
||||||
|
1. показывает пользователю запрос подтверждения;
|
||||||
|
2. показывает комментарий к подписи;
|
||||||
|
3. после нажатия `APPROVE` или `REJECT` возвращает ответ в расширение.
|
||||||
|
|
||||||
|
### 9.2. Формат запроса
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"operation": "sign_transaction",
|
||||||
|
"requestId": "1718998123456-482193",
|
||||||
|
"timeMs": 1718998123456,
|
||||||
|
"publicKeyBase58": "....",
|
||||||
|
"transactionBase64": "....",
|
||||||
|
"comment": "Site https://example.com requested transaction signature"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3. Поля запроса
|
||||||
|
|
||||||
|
- `v` — версия wallet RPC. Сейчас `1`.
|
||||||
|
- `operation` — строка операции: `sign_transaction`.
|
||||||
|
- `requestId` — идентификатор запроса.
|
||||||
|
- `timeMs` — время отправки на стороне расширения.
|
||||||
|
- `publicKeyBase58` — публичный ключ, от которого ожидается подпись.
|
||||||
|
- `transactionBase64` — сериализованная Solana transaction в `base64`.
|
||||||
|
- `comment` — короткое текстовое описание, которое ESP32 показывает пользователю при запросе подписи.
|
||||||
|
|
||||||
|
### 9.4. Поведение ESP32
|
||||||
|
|
||||||
|
При получении такого запроса ESP32:
|
||||||
|
|
||||||
|
1. сравнивает `publicKeyBase58` с публичным ключом текущего активного выбранного кошелька;
|
||||||
|
2. если ключ не совпадает, сразу возвращает ошибку `wallet_mismatch`;
|
||||||
|
3. если ключ совпадает, показывает отдельный экран подтверждения подписи;
|
||||||
|
4. на экране показывает:
|
||||||
|
- каким кошельком будет выполнена подпись;
|
||||||
|
- комментарий `comment`;
|
||||||
|
- кнопки `APPROVE` и `REJECT`;
|
||||||
|
5. если пользователь подтверждает подпись, ESP32 подписывает транзакцию и возвращает результат;
|
||||||
|
6. если пользователь отклоняет, ESP32 возвращает `rejected_by_user`.
|
||||||
|
|
||||||
|
## 10. Формат ответа на подпись
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"op": "sign_transaction_result",
|
||||||
|
"requestId": "1718998123456-482193",
|
||||||
|
"ok": true,
|
||||||
|
"publicKeyBase58": "....",
|
||||||
|
"signatureBase58": "....",
|
||||||
|
"signedTransactionBase64": "....",
|
||||||
|
"timeMs": 1718998123999
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если пользователь отклонил запрос:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"op": "sign_transaction_result",
|
||||||
|
"requestId": "1718998123456-482193",
|
||||||
|
"ok": false,
|
||||||
|
"error": "rejected_by_user",
|
||||||
|
"timeMs": 1718998123999
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Рекомендуемые ошибки для `sign_transaction`:
|
||||||
|
|
||||||
|
- `rejected_by_user`
|
||||||
|
- `wallet_unavailable`
|
||||||
|
- `wallet_mismatch`
|
||||||
|
- `transaction_base64_invalid`
|
||||||
|
- `transaction_sign_failed`
|
||||||
|
- `bad_request`
|
||||||
|
|
||||||
|
## 11. Подключение кошелька с сайта
|
||||||
|
|
||||||
|
При вызове сайта `connect wallet` расширение должно вести себя как обычный wallet extension:
|
||||||
|
|
||||||
|
1. запросить подтверждение у пользователя в браузере;
|
||||||
|
2. получить текущий публичный ключ с ESP32;
|
||||||
|
3. вернуть сайту `publicKey` текущего активного кошелька.
|
||||||
|
|
||||||
|
Для `signTransaction` расширение:
|
||||||
|
|
||||||
|
1. получает транзакцию от сайта;
|
||||||
|
2. пересылает её на ESP32 через `sign_transaction`;
|
||||||
|
3. ждёт решение пользователя на устройстве;
|
||||||
|
4. возвращает браузеру уже подписанную транзакцию.
|
||||||
|
|
||||||
|
## 12. Ограничения текущей версии
|
||||||
|
|
||||||
|
- запрос возвращает только текущий активный кошелёк, а не список всех кошельков;
|
||||||
|
- выбор типа кошелька делается только на самом ESP32;
|
||||||
|
- отдельная цифровая подпись ответа пока не используется;
|
||||||
|
- отдельное E2E-шифрование wallet RPC пока не используется;
|
||||||
|
- `custom`-кошельки пока не сверяются с PDA.
|
||||||
28
ESP32/AGENTS.md
Normal file
28
ESP32/AGENTS.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# AGENTS for ESP32
|
||||||
|
|
||||||
|
## Язык UI
|
||||||
|
|
||||||
|
- Для ESP32-скетчей и экранного UI использовать английский язык.
|
||||||
|
- Русский текст на экране ESP32 пока не поддерживается корректно: шрифтовой путь для кириллицы не считается рабочим.
|
||||||
|
- Если меняется UI-скетч, все пользовательские строки на экране должны оставаться английскими, пока ограничение не снято отдельной задачей.
|
||||||
|
|
||||||
|
## Синхронизация со спецификацией
|
||||||
|
|
||||||
|
- При изменении экранов, кнопок, переходов, статусов или текстов обязательно обновлять соответствующую спецификацию в `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/`.
|
||||||
|
|
||||||
|
## Сборка ESP32
|
||||||
|
|
||||||
|
- Основной способ проверки и прошивки скетчей для `ESP32-S3-Touch-AMOLED-2.16` - `main-device/burn.sh`.
|
||||||
|
- Не собирать эти скетчи напрямую через `arduino-cli compile` без `burn.sh`, потому что скрипт добавляет нужные локальные библиотеки и конфиги из `official-demo/examples/Arduino-v3.3.5/libraries`.
|
||||||
|
- Если сборка падает по `lv_conf.h` или `TouchDrvCSTXXX.hpp`, сначала проверять именно `burn.sh` и его `--library` пути, а не считать, что файл пропал из репозитория.
|
||||||
|
|
||||||
|
## Диагностика ESP32
|
||||||
|
|
||||||
|
- Последнюю сохранённую ошибку или диагностическую запись читать с устройства через USB serial monitor на `115200`.
|
||||||
|
- Базовая команда:
|
||||||
|
`arduino-cli monitor -p /dev/ttyACM0 --config baudrate=115200`
|
||||||
|
- После подключения отправлять одну из команд:
|
||||||
|
`last_error`, `last_diag` или `reg_diag`
|
||||||
|
- Для очистки сохранённой диагностики использовать:
|
||||||
|
`clear_error` или `clear_diag`
|
||||||
|
- При падениях в ветках регистрации и обновления PDA сначала читать именно `last_error`: запись хранится в NVS и может пережить перезагрузку устройства.
|
||||||
119
ESP32/CODEX_PORTING_GUIDE.md
Normal file
119
ESP32/CODEX_PORTING_GUIDE.md
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# ESP32-S3-Touch-AMOLED-2.16 Codex Guide
|
||||||
|
|
||||||
|
Этот файл переносится в другие проекты как готовая инструкция для Codex по этой плате.
|
||||||
|
|
||||||
|
## 1) Что это за плата
|
||||||
|
|
||||||
|
- Модель: `Waveshare ESP32-S3-Touch-AMOLED-2.16`
|
||||||
|
- MCU: `ESP32-S3` (flash 16MB, PSRAM 8MB)
|
||||||
|
- Экран: AMOLED, физически 480x480, углы скруглены (часть крайних пикселей может быть невидима)
|
||||||
|
- Touch: CST92xx
|
||||||
|
- IMU: QMI8658
|
||||||
|
- Аудио:
|
||||||
|
- DAC/вывод (динамик): ES8311
|
||||||
|
- ADC/вход (микрофоны): ES7210
|
||||||
|
|
||||||
|
## 2) Что уже установлено в этой среде
|
||||||
|
|
||||||
|
- Ubuntu
|
||||||
|
- `arduino-cli 1.4.0`
|
||||||
|
- `esp32:esp32` core `3.3.5`
|
||||||
|
- `esptool` из `~/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool`
|
||||||
|
- USB порт платы: обычно `/dev/ttyACM0`
|
||||||
|
|
||||||
|
Проверка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
arduino-cli version
|
||||||
|
arduino-cli core list
|
||||||
|
arduino-cli board list
|
||||||
|
ls -l /dev/ttyACM0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Структура подпроекта (эталон)
|
||||||
|
|
||||||
|
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
|
||||||
|
- `original-firmware/` — backup/restore заводской прошивки
|
||||||
|
- `main-device/` — прошивки и `burn.sh`
|
||||||
|
- `reference/` — заметки и ссылки
|
||||||
|
|
||||||
|
## 4) Бэкап перед любыми экспериментами
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
|
||||||
|
./backup_factory.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемый результат:
|
||||||
|
- `factory-full-16mb.bin`
|
||||||
|
- `factory-full-16mb.bin.sha256`
|
||||||
|
|
||||||
|
Восстановление:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./restore_factory_backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5) Деплой (прошивка) — стандарт
|
||||||
|
|
||||||
|
Главный скрипт:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ESP32-S3-Touch-AMOLED-2.16/main-device
|
||||||
|
./burn.sh <mode>
|
||||||
|
```
|
||||||
|
|
||||||
|
Режимы:
|
||||||
|
- `hello` — базовый экран
|
||||||
|
- `widgets` — экран+touch+IMU (официальный пример)
|
||||||
|
- `audio` — тест аудио тракта
|
||||||
|
- `simple` — кастомный интеграционный тест (экран, touch, запись/воспроизведение, VU, tilt)
|
||||||
|
|
||||||
|
## 6) Как писать код под эту плату (важно)
|
||||||
|
|
||||||
|
1. **Экран**
|
||||||
|
- Рабочее разрешение использовать `480x480`.
|
||||||
|
- Не рисовать критичный текст/кнопки впритык к краю; держать safe margin (`~20px+`) из-за скругленных углов.
|
||||||
|
- Не делать полный `fillScreen` в каждом loop: только частичные обновления (`fillRect`/локальные перерисовки), иначе мерцание.
|
||||||
|
|
||||||
|
2. **Touch**
|
||||||
|
- Настройка CST:
|
||||||
|
- `setMaxCoordinates(480, 480)`
|
||||||
|
- `setSwapXY(true)`
|
||||||
|
- `setMirrorXY(true, false)`
|
||||||
|
- Обрабатывать touch по IRQ + `getPoint`.
|
||||||
|
- После смещения UI обязательно пересчитывать hitbox кнопок.
|
||||||
|
|
||||||
|
3. **Аудио**
|
||||||
|
- Для динамика инициализировать `ES8311`.
|
||||||
|
- Для микрофона обязательно инициализировать `ES7210`; без этого запись может быть пустой.
|
||||||
|
- Для отладки записи показывать VU/peak на экране во время `RECORD`.
|
||||||
|
- Для быстрой проверки тракта всегда держать кнопку `BEEP` (тон), чтобы отделить проблему динамика от проблемы микрофона.
|
||||||
|
|
||||||
|
4. **IMU**
|
||||||
|
- QMI8658 обновлять с ограниченной частотой (например 80–150 мс для UI-строки), чтобы не шуметь перерисовками.
|
||||||
|
|
||||||
|
5. **Стабильность UI**
|
||||||
|
- Статика: рисуется один раз в setup.
|
||||||
|
- Динамика: отдельная зона, перерисовывать только по изменению данных.
|
||||||
|
|
||||||
|
## 7) Рекомендуемый workflow для Codex
|
||||||
|
|
||||||
|
1. Проверить порт и инструменты.
|
||||||
|
2. Если новая плата/первый запуск — сделать backup flash.
|
||||||
|
3. Собрать и залить `simple`.
|
||||||
|
4. Пройти ручной чек:
|
||||||
|
- экран отображает текст без обрезки,
|
||||||
|
- touch срабатывает по кнопкам,
|
||||||
|
- `BEEP` слышно,
|
||||||
|
- VU двигается во время записи,
|
||||||
|
- `PLAY` воспроизводит записанное,
|
||||||
|
- `Tilt` меняется при повороте.
|
||||||
|
5. Только после этого усложнять приложение.
|
||||||
|
|
||||||
|
## 8) Ссылки
|
||||||
|
|
||||||
|
- Product page: https://www.waveshare.com/product/arduino/boards-kits/esp32-s3/esp32-s3-touch-amoled-2.16.htm
|
||||||
|
- Docs: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16
|
||||||
|
- Arduino setup: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16/Development-Environment-Setup-Arduino
|
||||||
|
- Official examples: https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16
|
||||||
119
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/CODEX_PORTING_GUIDE.md
Normal file
119
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/CODEX_PORTING_GUIDE.md
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# ESP32-S3-Touch-AMOLED-2.16 Codex Guide
|
||||||
|
|
||||||
|
Этот файл переносится в другие проекты как готовая инструкция для Codex по этой плате.
|
||||||
|
|
||||||
|
## 1) Что это за плата
|
||||||
|
|
||||||
|
- Модель: `Waveshare ESP32-S3-Touch-AMOLED-2.16`
|
||||||
|
- MCU: `ESP32-S3` (flash 16MB, PSRAM 8MB)
|
||||||
|
- Экран: AMOLED, физически 480x480, углы скруглены (часть крайних пикселей может быть невидима)
|
||||||
|
- Touch: CST92xx
|
||||||
|
- IMU: QMI8658
|
||||||
|
- Аудио:
|
||||||
|
- DAC/вывод (динамик): ES8311
|
||||||
|
- ADC/вход (микрофоны): ES7210
|
||||||
|
|
||||||
|
## 2) Что уже установлено в этой среде
|
||||||
|
|
||||||
|
- Ubuntu
|
||||||
|
- `arduino-cli 1.4.0`
|
||||||
|
- `esp32:esp32` core `3.3.5`
|
||||||
|
- `esptool` из `~/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool`
|
||||||
|
- USB порт платы: обычно `/dev/ttyACM0`
|
||||||
|
|
||||||
|
Проверка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
arduino-cli version
|
||||||
|
arduino-cli core list
|
||||||
|
arduino-cli board list
|
||||||
|
ls -l /dev/ttyACM0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Структура подпроекта (эталон)
|
||||||
|
|
||||||
|
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
|
||||||
|
- `original-firmware/` — backup/restore заводской прошивки
|
||||||
|
- `main-device/` — прошивки и `burn.sh`
|
||||||
|
- `reference/` — заметки и ссылки
|
||||||
|
|
||||||
|
## 4) Бэкап перед любыми экспериментами
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
|
||||||
|
./backup_factory.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемый результат:
|
||||||
|
- `factory-full-16mb.bin`
|
||||||
|
- `factory-full-16mb.bin.sha256`
|
||||||
|
|
||||||
|
Восстановление:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./restore_factory_backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5) Деплой (прошивка) — стандарт
|
||||||
|
|
||||||
|
Главный скрипт:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ESP32-S3-Touch-AMOLED-2.16/main-device
|
||||||
|
./burn.sh <mode>
|
||||||
|
```
|
||||||
|
|
||||||
|
Режимы:
|
||||||
|
- `hello` — базовый экран
|
||||||
|
- `widgets` — экран+touch+IMU (официальный пример)
|
||||||
|
- `audio` — тест аудио тракта
|
||||||
|
- `simple` — кастомный интеграционный тест (экран, touch, запись/воспроизведение, VU, tilt)
|
||||||
|
|
||||||
|
## 6) Как писать код под эту плату (важно)
|
||||||
|
|
||||||
|
1. **Экран**
|
||||||
|
- Рабочее разрешение использовать `480x480`.
|
||||||
|
- Не рисовать критичный текст/кнопки впритык к краю; держать safe margin (`~20px+`) из-за скругленных углов.
|
||||||
|
- Не делать полный `fillScreen` в каждом loop: только частичные обновления (`fillRect`/локальные перерисовки), иначе мерцание.
|
||||||
|
|
||||||
|
2. **Touch**
|
||||||
|
- Настройка CST:
|
||||||
|
- `setMaxCoordinates(480, 480)`
|
||||||
|
- `setSwapXY(true)`
|
||||||
|
- `setMirrorXY(true, false)`
|
||||||
|
- Обрабатывать touch по IRQ + `getPoint`.
|
||||||
|
- После смещения UI обязательно пересчитывать hitbox кнопок.
|
||||||
|
|
||||||
|
3. **Аудио**
|
||||||
|
- Для динамика инициализировать `ES8311`.
|
||||||
|
- Для микрофона обязательно инициализировать `ES7210`; без этого запись может быть пустой.
|
||||||
|
- Для отладки записи показывать VU/peak на экране во время `RECORD`.
|
||||||
|
- Для быстрой проверки тракта всегда держать кнопку `BEEP` (тон), чтобы отделить проблему динамика от проблемы микрофона.
|
||||||
|
|
||||||
|
4. **IMU**
|
||||||
|
- QMI8658 обновлять с ограниченной частотой (например 80–150 мс для UI-строки), чтобы не шуметь перерисовками.
|
||||||
|
|
||||||
|
5. **Стабильность UI**
|
||||||
|
- Статика: рисуется один раз в setup.
|
||||||
|
- Динамика: отдельная зона, перерисовывать только по изменению данных.
|
||||||
|
|
||||||
|
## 7) Рекомендуемый workflow для Codex
|
||||||
|
|
||||||
|
1. Проверить порт и инструменты.
|
||||||
|
2. Если новая плата/первый запуск — сделать backup flash.
|
||||||
|
3. Собрать и залить `simple`.
|
||||||
|
4. Пройти ручной чек:
|
||||||
|
- экран отображает текст без обрезки,
|
||||||
|
- touch срабатывает по кнопкам,
|
||||||
|
- `BEEP` слышно,
|
||||||
|
- VU двигается во время записи,
|
||||||
|
- `PLAY` воспроизводит записанное,
|
||||||
|
- `Tilt` меняется при повороте.
|
||||||
|
5. Только после этого усложнять приложение.
|
||||||
|
|
||||||
|
## 8) Ссылки
|
||||||
|
|
||||||
|
- Product page: https://www.waveshare.com/product/arduino/boards-kits/esp32-s3/esp32-s3-touch-amoled-2.16.htm
|
||||||
|
- Docs: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16
|
||||||
|
- Arduino setup: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16/Development-Environment-Setup-Arduino
|
||||||
|
- Official examples: https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16
|
||||||
28
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/README.md
Normal file
28
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# ESP32-S3-Touch-AMOLED-2.16
|
||||||
|
|
||||||
|
Подпроект для Waveshare `ESP32-S3-Touch-AMOLED-2.16`.
|
||||||
|
|
||||||
|
Структура:
|
||||||
|
|
||||||
|
- `official-demo/` — официальный репозиторий примеров Waveshare
|
||||||
|
- `original-firmware/` — резервная копия заводской прошивки
|
||||||
|
- `main-device/` — скрипты быстрой проверки устройства и основной скетч `shine_homeserver_main/`
|
||||||
|
- `reference/` — локальные заметки по документации и железу
|
||||||
|
- `main-device/shine_homeserver_main/` — основной рабочий скетч ESP32-проекта `SHiNE`
|
||||||
|
|
||||||
|
Примечание по git:
|
||||||
|
|
||||||
|
- `official-demo/` держать как локальный внешний checkout из `https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16`, в основной git его не добавлять.
|
||||||
|
- `original-firmware/*.bin` — локальный дамп конкретной платы, в git не добавлять.
|
||||||
|
- `.arduino-build/` и готовые `.bin/.elf/.map` — сборочные артефакты, в git не добавлять.
|
||||||
|
|
||||||
|
Быстрый старт:
|
||||||
|
|
||||||
|
1. Сделать backup текущей прошивки:
|
||||||
|
- `cd original-firmware && ./backup_factory.sh`
|
||||||
|
2. Залить тест экрана/тача:
|
||||||
|
- `cd ../main-device && ./burn.sh widgets`
|
||||||
|
3. Залить тест динамика:
|
||||||
|
- `cd ../main-device && ./burn.sh audio`
|
||||||
|
4. Залить основной UI:
|
||||||
|
- `cd ../main-device && ./burn.sh shine-homeserver-main`
|
||||||
52
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/README.md
Normal file
52
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/README.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Main Device
|
||||||
|
|
||||||
|
Основной скетч homeserver и старые тестовые скетчи для быстрой проверки платы.
|
||||||
|
`burn.sh` теперь:
|
||||||
|
- сам пытается найти USB-порт ESP32;
|
||||||
|
- сначала делает быструю инкрементальную сборку;
|
||||||
|
- если быстрая сборка не удалась, автоматически повторяет полную `clean`-сборку.
|
||||||
|
|
||||||
|
Для режимов `widgets`, `audio` и `hello` рядом должен лежать локальный checkout `official-demo/` из официального репозитория Waveshare. В основной git он не добавляется, потому что это большой внешний набор примеров, библиотек, прошивок и артефактов.
|
||||||
|
|
||||||
|
Режимы:
|
||||||
|
- `widgets` — экран + touch + IMU (пример `05_LVGL_Widgets`)
|
||||||
|
- `audio` — динамик/аудио-кодек (пример `07_ES8311`)
|
||||||
|
- `hello` — базовый тест экрана (пример `01_HelloWorld`)
|
||||||
|
- `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU)
|
||||||
|
- `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости)
|
||||||
|
- `homeserver-ui` — совместимый алиас, указывает на `shine_homeserver_main/`
|
||||||
|
- `shine-homeserver-main` — основной скетч проекта `SHiNE` для ESP32, текущая рабочая версия UI
|
||||||
|
- `shine-homeserver-ui-main` — старое имя основного скетча, оставлено как совместимый алиас
|
||||||
|
- `legacy-homeserver-ui` — старый UI-прототип `shine_homeserver_ui/`, оставлен как тестовый и не является основным
|
||||||
|
- `text-test` — диагностический экран рендера текста: default font, U8g2 ASCII, U8g2 кириллица, кнопки с подписями
|
||||||
|
- `gfx-text-test` — тот же тест рендера текста, но уже внутри новой папки `test_sketches/`
|
||||||
|
- `gfx-layout-test` — тест геометрии и нижних рядов кнопок
|
||||||
|
- `lvgl-basic-test` — минимальный экран на `LVGL` с текстом и кнопками
|
||||||
|
- `lvgl-interaction-test` — экран на `LVGL` с большим числом кнопок и сообщением о нажатой кнопке
|
||||||
|
- `lvgl-touch-debug-test` — точечная диагностика touch: сырые координаты, маркер точки и большая тест-кнопка `LVGL`
|
||||||
|
- `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации
|
||||||
|
- `lvgl-subserver-touch-test` — старый гибридный тест: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из старого `shine_homeserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет
|
||||||
|
- `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы
|
||||||
|
- `lvgl-nav-minimal-test` — старое имя основного скетча, теперь ведёт на `shine_homeserver_main/` для совместимости
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
|
||||||
|
- `./burn.sh widgets`
|
||||||
|
- `./burn.sh audio`
|
||||||
|
- `./burn.sh hello`
|
||||||
|
- `./burn.sh simple`
|
||||||
|
- `./burn.sh homeserver-ui`
|
||||||
|
- `./burn.sh shine-homeserver-main`
|
||||||
|
- `./burn.sh shine-homeserver-ui-main`
|
||||||
|
- `./burn.sh legacy-homeserver-ui`
|
||||||
|
- `./burn.sh text-test`
|
||||||
|
- `./burn.sh gfx-text-test`
|
||||||
|
- `./burn.sh gfx-layout-test`
|
||||||
|
- `./burn.sh lvgl-basic-test`
|
||||||
|
- `./burn.sh lvgl-interaction-test`
|
||||||
|
- `./burn.sh lvgl-touch-debug-test`
|
||||||
|
- `./burn.sh lvgl-official-based-test`
|
||||||
|
- `./burn.sh lvgl-subserver-touch-test`
|
||||||
|
- `./burn.sh lvgl-russian-font-test`
|
||||||
|
- `./burn.sh lvgl-nav-minimal-test`
|
||||||
|
- `./flash_shine_homeserver_main.sh` - автоматически находит USB-порт и заливает `shine_homeserver_main`
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// BLAKE2b state
|
||||||
|
struct B2State {
|
||||||
|
uint64_t h[8], t[2], f[2];
|
||||||
|
uint8_t buf[128];
|
||||||
|
size_t buflen, outlen;
|
||||||
|
};
|
||||||
97
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/burn.sh
Executable file
97
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/burn.sh
Executable file
@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BOARD_DIR="$(cd "${ROOT_DIR}/.." && pwd)"
|
||||||
|
DEMO_BASE="${BOARD_DIR}/official-demo/examples/Arduino-v3.3.5"
|
||||||
|
MODE="${1:-widgets}"
|
||||||
|
PORT="${PORT:-}"
|
||||||
|
FQBN="${FQBN:-esp32:esp32:esp32s3:USBMode=hwcdc,CDCOnBoot=cdc,UploadSpeed=921600,CPUFreq=240,FlashMode=dio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,PSRAM=opi}"
|
||||||
|
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/.arduino-build/build-${MODE}}"
|
||||||
|
OUT_DIR="${OUT_DIR:-${ROOT_DIR}/.arduino-build/out-${MODE}}"
|
||||||
|
|
||||||
|
detect_port() {
|
||||||
|
local detected
|
||||||
|
detected="$(arduino-cli board list 2>/dev/null | awk '/\/dev\/tty(ACM|USB)/ {print $1; exit}')"
|
||||||
|
if [[ -n "${detected}" ]]; then
|
||||||
|
echo "${detected}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for candidate in /dev/ttyACM* /dev/ttyUSB*; do
|
||||||
|
if [[ -e "${candidate}" ]]; then
|
||||||
|
echo "${candidate}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${MODE}" in
|
||||||
|
hello) SKETCH_DIR="${DEMO_BASE}/examples/01_HelloWorld" ;;
|
||||||
|
widgets) SKETCH_DIR="${DEMO_BASE}/examples/05_LVGL_Widgets" ;;
|
||||||
|
audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;;
|
||||||
|
simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;;
|
||||||
|
argon2) SKETCH_DIR="${ROOT_DIR}/argon2_sd_test" ;;
|
||||||
|
homeserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||||
|
shine-homeserver-main) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||||
|
shine-homeserver-ui-main) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||||
|
legacy-homeserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_ui" ;;
|
||||||
|
text-test) SKETCH_DIR="${ROOT_DIR}/text_render_test" ;;
|
||||||
|
gfx-text-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_text_render_test" ;;
|
||||||
|
gfx-layout-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_button_layout_test" ;;
|
||||||
|
lvgl-basic-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_basic_test" ;;
|
||||||
|
lvgl-interaction-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_interaction_test" ;;
|
||||||
|
lvgl-touch-debug-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_touch_debug_test" ;;
|
||||||
|
lvgl-official-based-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_official_based_test" ;;
|
||||||
|
lvgl-subserver-touch-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_subserver_touch_test" ;;
|
||||||
|
lvgl-russian-font-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_russian_font_test" ;;
|
||||||
|
lvgl-nav-minimal-test) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||||
|
*)
|
||||||
|
echo "Unknown mode: ${MODE}" >&2
|
||||||
|
echo "Use one of: hello, widgets, audio, simple, argon2, homeserver-ui, shine-homeserver-main, shine-homeserver-ui-main, legacy-homeserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test, lvgl-russian-font-test" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -z "${PORT}" ]]; then
|
||||||
|
if ! PORT="$(detect_port)"; then
|
||||||
|
echo "Failed to auto-detect ESP32 port. Set PORT=/dev/ttyACM0 ./burn.sh ${MODE}" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== Mode: ${MODE}"
|
||||||
|
echo "== Sketch: ${SKETCH_DIR}"
|
||||||
|
echo "== Port: ${PORT}"
|
||||||
|
echo "== FQBN: ${FQBN}"
|
||||||
|
|
||||||
|
mkdir -p "${BUILD_DIR}" "${OUT_DIR}"
|
||||||
|
|
||||||
|
compile_args=(
|
||||||
|
--fqbn "${FQBN}"
|
||||||
|
--build-path "${BUILD_DIR}"
|
||||||
|
--output-dir "${OUT_DIR}"
|
||||||
|
--library "${DEMO_BASE}/libraries/GFX_Library_for_Arduino"
|
||||||
|
--library "${DEMO_BASE}/libraries/SensorLib"
|
||||||
|
--library "${DEMO_BASE}/libraries/XPowersLib"
|
||||||
|
--library "${DEMO_BASE}/libraries/lvgl"
|
||||||
|
--library "${DEMO_BASE}/libraries/Mylibrary"
|
||||||
|
"${SKETCH_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "== Compile: fast incremental build"
|
||||||
|
if ! arduino-cli compile "${compile_args[@]}"; then
|
||||||
|
echo "== Compile: fast build failed, retrying clean build"
|
||||||
|
arduino-cli compile --clean "${compile_args[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
arduino-cli upload \
|
||||||
|
-p "${PORT}" \
|
||||||
|
--fqbn "${FQBN}" \
|
||||||
|
--input-dir "${OUT_DIR}" \
|
||||||
|
"${SKETCH_DIR}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Done."
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
detect_port_from_arduino_cli() {
|
||||||
|
local line
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "${line}" ]] && continue
|
||||||
|
[[ "${line}" == Port* ]] && continue
|
||||||
|
if [[ "${line}" == /dev/* ]]; then
|
||||||
|
awk '{print $1}' <<<"${line}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < <(arduino-cli board list 2>/dev/null || true)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_port_from_dev() {
|
||||||
|
local candidates=()
|
||||||
|
local path
|
||||||
|
for path in /dev/ttyACM* /dev/ttyUSB*; do
|
||||||
|
[[ -e "${path}" ]] || continue
|
||||||
|
candidates+=("${path}")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${#candidates[@]}" -eq 1 ]]; then
|
||||||
|
printf '%s\n' "${candidates[0]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
PORT="${PORT:-}"
|
||||||
|
if [[ -z "${PORT}" ]]; then
|
||||||
|
PORT="$(detect_port_from_arduino_cli || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "${PORT}" ]]; then
|
||||||
|
PORT="$(detect_port_from_dev || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${PORT}" ]]; then
|
||||||
|
echo "Не удалось автоматически найти USB-порт ESP32." >&2
|
||||||
|
echo "Подключите плату и проверьте 'arduino-cli board list'." >&2
|
||||||
|
echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_homeserver_main.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== Найден порт: ${PORT}"
|
||||||
|
PORT="${PORT}" "${ROOT_DIR}/burn.sh" shine-homeserver-main
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
# SHiNE Homeserver UI Main
|
||||||
|
|
||||||
|
Это основной рабочий скетч ESP32-проекта `SHiNE`.
|
||||||
|
|
||||||
|
Текущая каноническая точка запуска:
|
||||||
|
|
||||||
|
- `./burn.sh shine-homeserver-main`
|
||||||
|
- `./burn.sh homeserver-ui`
|
||||||
|
|
||||||
|
Историческое имя этого скетча:
|
||||||
|
|
||||||
|
- `lvgl-nav-minimal-test`
|
||||||
|
|
||||||
|
Прежние тестовые варианты для этой платы остаются в `main-device/test_sketches/` и должны восприниматься как старые диагностические сборки, а не как основной UI.
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,591 @@
|
|||||||
|
#include "shine_secret_generation.h"
|
||||||
|
|
||||||
|
#include <SD_MMC.h>
|
||||||
|
#include <mbedtls/sha256.h>
|
||||||
|
#include <mbedtls/base64.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <driver/gpio.h>
|
||||||
|
|
||||||
|
#define PIN_SD_CLK 2
|
||||||
|
#define PIN_SD_CMD 1
|
||||||
|
#define PIN_SD_D0 3
|
||||||
|
#define SD_MEM_FILE "/argon2.bin"
|
||||||
|
|
||||||
|
#define A2_T 2u
|
||||||
|
#define A2_M 65536u
|
||||||
|
#define A2_P 1u
|
||||||
|
#define A2_DKLEN 32u
|
||||||
|
#define A2_SYNC 4u
|
||||||
|
#define A2_SEG (A2_M / A2_SYNC)
|
||||||
|
#define A2_VERSION 0x13u
|
||||||
|
#define A2_TYPE 2u
|
||||||
|
#define A2_BLKSZ 1024u
|
||||||
|
#define TOTAL_FILLS (A2_T * A2_M - 2u)
|
||||||
|
|
||||||
|
struct B2State {
|
||||||
|
uint64_t h[8], t[2], f[2];
|
||||||
|
uint8_t buf[128];
|
||||||
|
size_t buflen, outlen;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool gSdReady = false;
|
||||||
|
static bool gRunning = false;
|
||||||
|
static bool gDone = false;
|
||||||
|
static bool gError = false;
|
||||||
|
static char gMessage[96] = {};
|
||||||
|
static uint8_t gSecret[32] = {};
|
||||||
|
static char gSecretB58[64] = {};
|
||||||
|
static uint32_t gDoneBlocks = 0;
|
||||||
|
static uint32_t gStartMs = 0;
|
||||||
|
static uint32_t gCurPass = 0;
|
||||||
|
static uint32_t gCurBlock = 2;
|
||||||
|
static bool gInitDone = false;
|
||||||
|
static File gSdFile;
|
||||||
|
static uint8_t *gBufPrev = nullptr;
|
||||||
|
static uint8_t *gBufRef = nullptr;
|
||||||
|
static uint8_t *gBufOut = nullptr;
|
||||||
|
static uint8_t *gBufZero = nullptr;
|
||||||
|
static uint8_t *gBufAddr = nullptr;
|
||||||
|
static uint32_t *gJ1Seg = nullptr;
|
||||||
|
static uint8_t gH0[64] = {};
|
||||||
|
static B2State gB2S;
|
||||||
|
|
||||||
|
#define ROTR64(x,n) (((x)>>(n))|((x)<<(64-(n))))
|
||||||
|
static const uint64_t B2IV[8] = {
|
||||||
|
0x6A09E667F3BCC908ULL,0xBB67AE8584CAA73BULL,
|
||||||
|
0x3C6EF372FE94F82BULL,0xA54FF53A5F1D36F1ULL,
|
||||||
|
0x510E527FADE682D1ULL,0x9B05688C2B3E6C1FULL,
|
||||||
|
0x1F83D9ABFB41BD6BULL,0x5BE0CD19137E2179ULL
|
||||||
|
};
|
||||||
|
static const uint8_t B2SIGMA[12][16] = {
|
||||||
|
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
|
||||||
|
{14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3},
|
||||||
|
{11,8,12,0,5,2,15,13,10,14,3,6,7,1,9,4},
|
||||||
|
{7,9,3,1,13,12,11,14,2,6,5,10,4,0,15,8},
|
||||||
|
{9,0,5,7,2,4,10,15,14,1,11,12,6,8,3,13},
|
||||||
|
{2,12,6,10,0,11,8,3,4,13,7,5,15,14,1,9},
|
||||||
|
{12,5,1,15,14,13,4,10,0,7,6,3,9,2,8,11},
|
||||||
|
{13,11,7,14,12,1,3,9,5,0,15,4,8,6,2,10},
|
||||||
|
{6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5},
|
||||||
|
{10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0},
|
||||||
|
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
|
||||||
|
{14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3}
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char B58_ALPHA[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
static void setMessage(const char *message) {
|
||||||
|
snprintf(gMessage, sizeof(gMessage), "%s", message ? message : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sha256calc(const uint8_t *in, size_t len, uint8_t *out32);
|
||||||
|
|
||||||
|
static void finishSecretFromBytes(const uint8_t secret32[32], const char *message) {
|
||||||
|
memcpy(gSecret, secret32, 32);
|
||||||
|
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
|
||||||
|
gDone = true;
|
||||||
|
gRunning = false;
|
||||||
|
gError = false;
|
||||||
|
gInitDone = false;
|
||||||
|
gDoneBlocks = TOTAL_FILLS;
|
||||||
|
setMessage(message ? message : "Secret generated");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void b2_compress(B2State *S, const uint8_t *blk) {
|
||||||
|
uint64_t m[16], v[16];
|
||||||
|
for (int i = 0; i < 16; i++) m[i] = ((const uint64_t *)blk)[i];
|
||||||
|
for (int i = 0; i < 8; i++) v[i] = S->h[i];
|
||||||
|
v[8]=B2IV[0];v[9]=B2IV[1];v[10]=B2IV[2];v[11]=B2IV[3];
|
||||||
|
v[12]=B2IV[4]^S->t[0];v[13]=B2IV[5]^S->t[1];
|
||||||
|
v[14]=B2IV[6]^S->f[0];v[15]=B2IV[7]^S->f[1];
|
||||||
|
#define BG(r,i,a,b,c,d) \
|
||||||
|
v[a]+=v[b]+m[B2SIGMA[r][2*(i)]]; v[d]=ROTR64(v[d]^v[a],32); \
|
||||||
|
v[c]+=v[d]; v[b]=ROTR64(v[b]^v[c],24); \
|
||||||
|
v[a]+=v[b]+m[B2SIGMA[r][2*(i)+1]]; v[d]=ROTR64(v[d]^v[a],16); \
|
||||||
|
v[c]+=v[d]; v[b]=ROTR64(v[b]^v[c],63)
|
||||||
|
for (int r = 0; r < 12; r++) {
|
||||||
|
BG(r,0,0,4,8,12);BG(r,1,1,5,9,13);BG(r,2,2,6,10,14);BG(r,3,3,7,11,15);
|
||||||
|
BG(r,4,0,5,10,15);BG(r,5,1,6,11,12);BG(r,6,2,7,8,13);BG(r,7,3,4,9,14);
|
||||||
|
}
|
||||||
|
#undef BG
|
||||||
|
for (int i = 0; i < 8; i++) S->h[i] ^= v[i] ^ v[i + 8];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void b2_init(B2State *S, size_t outlen) {
|
||||||
|
memset(S, 0, sizeof(*S));
|
||||||
|
memcpy(S->h, B2IV, 64);
|
||||||
|
S->h[0] ^= 0x01010000ULL | outlen;
|
||||||
|
S->outlen = outlen;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void b2_update(B2State *S, const uint8_t *in, size_t inlen) {
|
||||||
|
while (inlen > 0) {
|
||||||
|
size_t fill = 128 - S->buflen;
|
||||||
|
size_t use = inlen < fill ? inlen : fill;
|
||||||
|
memcpy(S->buf + S->buflen, in, use);
|
||||||
|
S->buflen += use;
|
||||||
|
in += use;
|
||||||
|
inlen -= use;
|
||||||
|
if (S->buflen == 128 && inlen > 0) {
|
||||||
|
S->t[0] += 128;
|
||||||
|
if (S->t[0] < 128) S->t[1]++;
|
||||||
|
b2_compress(S, S->buf);
|
||||||
|
S->buflen = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void b2_final(B2State *S, uint8_t *digest) {
|
||||||
|
S->f[0] = ~0ULL;
|
||||||
|
S->t[0] += S->buflen;
|
||||||
|
if (S->t[0] < S->buflen) S->t[1]++;
|
||||||
|
memset(S->buf + S->buflen, 0, 128 - S->buflen);
|
||||||
|
b2_compress(S, S->buf);
|
||||||
|
uint8_t tmp[64];
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
uint64_t w = S->h[i];
|
||||||
|
for (int j = 0; j < 8; j++) tmp[i * 8 + j] = (uint8_t)(w >> (j * 8));
|
||||||
|
}
|
||||||
|
memcpy(digest, tmp, S->outlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void b2hash(const uint8_t *in, size_t inlen, uint8_t *digest, size_t outlen) {
|
||||||
|
b2_init(&gB2S, outlen);
|
||||||
|
b2_update(&gB2S, in, inlen);
|
||||||
|
b2_final(&gB2S, digest);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void b2long(const uint8_t *in, size_t inlen, uint8_t *digest, uint32_t outlen) {
|
||||||
|
uint8_t lenle[4] = {(uint8_t)outlen, (uint8_t)(outlen >> 8), (uint8_t)(outlen >> 16), (uint8_t)(outlen >> 24)};
|
||||||
|
if (outlen <= 64) {
|
||||||
|
b2_init(&gB2S, outlen);
|
||||||
|
b2_update(&gB2S, lenle, 4);
|
||||||
|
b2_update(&gB2S, in, inlen);
|
||||||
|
b2_final(&gB2S, digest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint8_t tmp[64];
|
||||||
|
b2_init(&gB2S, 64);
|
||||||
|
b2_update(&gB2S, lenle, 4);
|
||||||
|
b2_update(&gB2S, in, inlen);
|
||||||
|
b2_final(&gB2S, tmp);
|
||||||
|
memcpy(digest, tmp, 32);
|
||||||
|
digest += 32;
|
||||||
|
uint32_t rem = outlen - 32;
|
||||||
|
while (rem > 64) {
|
||||||
|
b2hash(tmp, 64, tmp, 64);
|
||||||
|
memcpy(digest, tmp, 32);
|
||||||
|
digest += 32;
|
||||||
|
rem -= 32;
|
||||||
|
}
|
||||||
|
b2hash(tmp, 64, tmp, rem);
|
||||||
|
memcpy(digest, tmp, rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define A2G(a,b,c,d) do{ \
|
||||||
|
(a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
|
||||||
|
(d)=ROTR64((d)^(a),32); \
|
||||||
|
(c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
|
||||||
|
(b)=ROTR64((b)^(c),24); \
|
||||||
|
(a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
|
||||||
|
(d)=ROTR64((d)^(a),16); \
|
||||||
|
(c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
|
||||||
|
(b)=ROTR64((b)^(c),63); \
|
||||||
|
}while(0)
|
||||||
|
#define A2P(v) do{ \
|
||||||
|
A2G((v)[0],(v)[4],(v)[8],(v)[12]);A2G((v)[1],(v)[5],(v)[9],(v)[13]); \
|
||||||
|
A2G((v)[2],(v)[6],(v)[10],(v)[14]);A2G((v)[3],(v)[7],(v)[11],(v)[15]); \
|
||||||
|
A2G((v)[0],(v)[5],(v)[10],(v)[15]);A2G((v)[1],(v)[6],(v)[11],(v)[12]); \
|
||||||
|
A2G((v)[2],(v)[7],(v)[8],(v)[13]);A2G((v)[3],(v)[4],(v)[9],(v)[14]); \
|
||||||
|
}while(0)
|
||||||
|
|
||||||
|
static void fillBlock(uint32_t prevIdx, uint32_t refIdx, uint32_t outIdx, bool xorMode) {
|
||||||
|
gSdFile.seek((uint64_t)prevIdx * A2_BLKSZ); gSdFile.read(gBufPrev, A2_BLKSZ);
|
||||||
|
gSdFile.seek((uint64_t)refIdx * A2_BLKSZ); gSdFile.read(gBufRef, A2_BLKSZ);
|
||||||
|
uint64_t *R = (uint64_t *)gBufOut, *P = (uint64_t *)gBufPrev, *F = (uint64_t *)gBufRef;
|
||||||
|
for (int i = 0; i < 128; i++) R[i] = P[i] ^ F[i];
|
||||||
|
uint64_t *Q = (uint64_t *)gBufRef; memcpy(Q, R, A2_BLKSZ);
|
||||||
|
for (int j = 0; j < 8; j++) A2P(&Q[16 * j]);
|
||||||
|
uint64_t row[16];
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
for (int k = 0; k < 8; k++) { row[2 * k] = Q[2 * i + 16 * k]; row[2 * k + 1] = Q[2 * i + 16 * k + 1]; }
|
||||||
|
A2P(row);
|
||||||
|
for (int k = 0; k < 8; k++) { Q[2 * i + 16 * k] = row[2 * k]; Q[2 * i + 16 * k + 1] = row[2 * k + 1]; }
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 128; i++) R[i] = Q[i] ^ R[i];
|
||||||
|
if (xorMode) {
|
||||||
|
gSdFile.seek((uint64_t)outIdx * A2_BLKSZ); gSdFile.read(gBufPrev, A2_BLKSZ);
|
||||||
|
uint64_t *O = (uint64_t *)gBufPrev; for (int i = 0; i < 128; i++) R[i] ^= O[i];
|
||||||
|
}
|
||||||
|
gSdFile.seek((uint64_t)outIdx * A2_BLKSZ); gSdFile.write(gBufOut, A2_BLKSZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void generateAddresses(uint32_t pass, uint32_t slice) {
|
||||||
|
memset(gBufZero, 0, A2_BLKSZ);
|
||||||
|
uint8_t inputBlk[A2_BLKSZ]; memset(inputBlk, 0, A2_BLKSZ);
|
||||||
|
uint64_t *iv = (uint64_t *)inputBlk;
|
||||||
|
iv[0]=pass;iv[1]=0;iv[2]=slice;iv[3]=A2_M;iv[4]=A2_T;iv[5]=A2_TYPE;
|
||||||
|
uint32_t count = 0;
|
||||||
|
for (uint32_t b = 0; b < A2_SEG; b += 128) {
|
||||||
|
iv[6] = ++count;
|
||||||
|
{
|
||||||
|
uint64_t *R=(uint64_t*)gBufAddr,*Z=(uint64_t*)gBufZero,*I=iv;
|
||||||
|
for(int i=0;i<128;i++)R[i]=Z[i]^I[i];
|
||||||
|
uint64_t *Q=R,row[16];
|
||||||
|
for(int j=0;j<8;j++)A2P(&Q[16*j]);
|
||||||
|
for(int i=0;i<8;i++){
|
||||||
|
for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
|
||||||
|
A2P(row);
|
||||||
|
for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
|
||||||
|
}
|
||||||
|
for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^I[i]);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
uint64_t *R=(uint64_t*)gBufPrev,*Z=(uint64_t*)gBufZero,*A=(uint64_t*)gBufAddr;
|
||||||
|
for(int i=0;i<128;i++)R[i]=Z[i]^A[i];
|
||||||
|
uint64_t *Q=R,row[16];
|
||||||
|
for(int j=0;j<8;j++)A2P(&Q[16*j]);
|
||||||
|
for(int i=0;i<8;i++){
|
||||||
|
for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
|
||||||
|
A2P(row);
|
||||||
|
for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
|
||||||
|
}
|
||||||
|
for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^A[i]);
|
||||||
|
memcpy(gBufAddr,gBufPrev,A2_BLKSZ);
|
||||||
|
}
|
||||||
|
uint64_t *addr=(uint64_t*)gBufAddr;
|
||||||
|
uint32_t cnt=(b+128<=A2_SEG)?128:(A2_SEG-b);
|
||||||
|
for(uint32_t j=0;j<cnt;j++)gJ1Seg[b+j]=(uint32_t)(addr[j]&0xFFFFFFFFULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t indexAlpha(uint32_t pass, uint32_t slice, uint32_t posInSlice, uint32_t J1) {
|
||||||
|
uint32_t refAreaSize;
|
||||||
|
if (pass == 0) {
|
||||||
|
if (slice == 0) {
|
||||||
|
uint32_t pos = posInSlice;
|
||||||
|
refAreaSize = (pos < 2) ? 0 : pos - 1;
|
||||||
|
} else {
|
||||||
|
refAreaSize = slice * A2_SEG + posInSlice - 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
refAreaSize = A2_M - A2_SEG + posInSlice - 1;
|
||||||
|
}
|
||||||
|
if (refAreaSize == 0) refAreaSize = 1;
|
||||||
|
uint64_t relPos = (uint64_t)J1 * J1 >> 32;
|
||||||
|
relPos = refAreaSize - 1 - ((uint64_t)refAreaSize * relPos >> 32);
|
||||||
|
uint32_t startPos = 0;
|
||||||
|
if (pass != 0 && slice != A2_SYNC - 1) startPos = (slice + 1) * A2_SEG;
|
||||||
|
return (startPos + relPos) % A2_M;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sha256calc(const uint8_t *in, size_t len, uint8_t *out32) {
|
||||||
|
mbedtls_sha256_context ctx;
|
||||||
|
mbedtls_sha256_init(&ctx);
|
||||||
|
mbedtls_sha256_starts(&ctx, 0);
|
||||||
|
mbedtls_sha256_update(&ctx, in, len);
|
||||||
|
mbedtls_sha256_finish(&ctx, out32);
|
||||||
|
mbedtls_sha256_free(&ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void trimInPlace(char *text) {
|
||||||
|
if (!text) return;
|
||||||
|
size_t len = strlen(text);
|
||||||
|
size_t start = 0;
|
||||||
|
while (start < len && (text[start] == ' ' || text[start] == '\t' || text[start] == '\n' || text[start] == '\r')) start++;
|
||||||
|
size_t end = len;
|
||||||
|
while (end > start && (text[end - 1] == ' ' || text[end - 1] == '\t' || text[end - 1] == '\n' || text[end - 1] == '\r')) end--;
|
||||||
|
if (start > 0 && end > start) memmove(text, text + start, end - start);
|
||||||
|
if (end <= start) { text[0] = '\0'; return; }
|
||||||
|
text[end - start] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void lowercaseAsciiInPlace(char *text) {
|
||||||
|
if (!text) return;
|
||||||
|
for (int i = 0; text[i]; i++) if (text[i] >= 'A' && text[i] <= 'Z') text[i] += 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void normalizeLoginInPlace(char *text) {
|
||||||
|
trimInPlace(text);
|
||||||
|
lowercaseAsciiInPlace(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void bytesToBase64Std(const uint8_t *data, size_t len, char *out, size_t outSz) {
|
||||||
|
size_t b64len = 0;
|
||||||
|
if (outSz == 0) return;
|
||||||
|
if (mbedtls_base64_encode((uint8_t*)out, outSz, &b64len, data, len) != 0) {
|
||||||
|
out[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (b64len >= outSz) b64len = outSz - 1;
|
||||||
|
out[b64len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void deriveLegacyMasterSecret(const char *password, uint8_t *out32) {
|
||||||
|
uint8_t baseHash[32];
|
||||||
|
sha256calc((const uint8_t*)password, strlen(password), baseHash);
|
||||||
|
char baseB64[64];
|
||||||
|
bytesToBase64Std(baseHash, 32, baseB64, sizeof(baseB64));
|
||||||
|
char material[96];
|
||||||
|
snprintf(material, sizeof(material), "%smaster.secret", baseB64);
|
||||||
|
sha256calc((const uint8_t*)material, strlen(material), out32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void shineSecretBase58Encode(const uint8_t *data, size_t len, char *out, size_t outSz) {
|
||||||
|
int zeros = 0;
|
||||||
|
while (zeros < (int)len && data[zeros] == 0) zeros++;
|
||||||
|
uint8_t tmp[128] = {};
|
||||||
|
int tmpLen = 0;
|
||||||
|
for (int i = zeros; i < (int)len; i++) {
|
||||||
|
int carry = data[i];
|
||||||
|
for (int j = 0; j < tmpLen; j++) { carry += 256 * tmp[j]; tmp[j] = carry % 58; carry /= 58; }
|
||||||
|
while (carry) { tmp[tmpLen++] = carry % 58; carry /= 58; }
|
||||||
|
}
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < zeros && n < (int)outSz - 1; i++) out[n++] = '1';
|
||||||
|
for (int i = tmpLen - 1; i >= 0 && n < (int)outSz - 1; i--) out[n++] = B58_ALPHA[(int)tmp[i]];
|
||||||
|
out[n] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shineSecretBase58Decode(const char *input, uint8_t *out, size_t *outLen, size_t maxOutLen, String &error) {
|
||||||
|
error = "";
|
||||||
|
if (!input || !*input) {
|
||||||
|
error = "secret too short";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t inLen = strlen(input);
|
||||||
|
uint8_t tmp[256] = {};
|
||||||
|
size_t tmpLen = 0;
|
||||||
|
size_t zeros = 0;
|
||||||
|
while (zeros < inLen && input[zeros] == '1') zeros++;
|
||||||
|
|
||||||
|
for (size_t i = zeros; i < inLen; ++i) {
|
||||||
|
const char *pos = strchr(B58_ALPHA, input[i]);
|
||||||
|
if (!pos) {
|
||||||
|
error = "invalid base58";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int carry = (int)(pos - B58_ALPHA);
|
||||||
|
for (size_t j = 0; j < tmpLen; ++j) {
|
||||||
|
carry += 58 * tmp[j];
|
||||||
|
tmp[j] = carry & 0xFF;
|
||||||
|
carry >>= 8;
|
||||||
|
}
|
||||||
|
while (carry > 0) {
|
||||||
|
tmp[tmpLen++] = carry & 0xFF;
|
||||||
|
carry >>= 8;
|
||||||
|
if (tmpLen >= sizeof(tmp)) {
|
||||||
|
error = "secret too long";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t decodedLen = zeros + tmpLen;
|
||||||
|
if (decodedLen > maxOutLen) {
|
||||||
|
error = "secret too long";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memset(out, 0, maxOutLen);
|
||||||
|
size_t offset = 0;
|
||||||
|
for (size_t i = 0; i < zeros; ++i) out[offset++] = 0;
|
||||||
|
for (size_t i = 0; i < tmpLen; ++i) out[offset + i] = tmp[tmpLen - 1 - i];
|
||||||
|
*outLen = decodedLen;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void argon2Init(const char *login, const char *password) {
|
||||||
|
char saltSrc[256];
|
||||||
|
snprintf(saltSrc, sizeof(saltSrc), "shine-auth-v2|login=%s|suffix=master.secret", login);
|
||||||
|
uint8_t saltHash[32]; sha256calc((uint8_t*)saltSrc, strlen(saltSrc), saltHash);
|
||||||
|
uint8_t salt[16]; memcpy(salt, saltHash, 16);
|
||||||
|
|
||||||
|
char passBytes[128];
|
||||||
|
snprintf(passBytes, sizeof(passBytes), "%s\n%s", login, password);
|
||||||
|
uint32_t passLen = strlen(passBytes), saltLen = 16;
|
||||||
|
|
||||||
|
b2_init(&gB2S, 64);
|
||||||
|
uint8_t le4[4];
|
||||||
|
#define B2LE32(val) do{uint32_t _v=(val);le4[0]=_v;le4[1]=_v>>8;le4[2]=_v>>16;le4[3]=_v>>24;b2_update(&gB2S,le4,4);}while(0)
|
||||||
|
B2LE32(A2_P);B2LE32(A2_DKLEN);B2LE32(A2_M);B2LE32(A2_T);
|
||||||
|
B2LE32(A2_VERSION);B2LE32(A2_TYPE);
|
||||||
|
B2LE32(passLen);b2_update(&gB2S,(const uint8_t*)passBytes,passLen);
|
||||||
|
B2LE32(saltLen);b2_update(&gB2S,salt,saltLen);
|
||||||
|
B2LE32(0);B2LE32(0);
|
||||||
|
#undef B2LE32
|
||||||
|
b2_final(&gB2S, gH0);
|
||||||
|
|
||||||
|
uint8_t input[72]; memcpy(input, gH0, 64);
|
||||||
|
for (uint32_t i = 0; i < 2; i++) {
|
||||||
|
input[64]=i;input[65]=0;input[66]=0;input[67]=0;
|
||||||
|
input[68]=0;input[69]=0;input[70]=0;input[71]=0;
|
||||||
|
b2long(input, 72, gBufOut, A2_BLKSZ);
|
||||||
|
gSdFile.seek((uint64_t)i * A2_BLKSZ); gSdFile.write(gBufOut, A2_BLKSZ);
|
||||||
|
}
|
||||||
|
gCurPass = 0;
|
||||||
|
gCurBlock = 2;
|
||||||
|
gDoneBlocks = 0;
|
||||||
|
gStartMs = millis();
|
||||||
|
gInitDone = true;
|
||||||
|
generateAddresses(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shineSecretInitSd(String &error) {
|
||||||
|
error = "";
|
||||||
|
if (gSdReady) return true;
|
||||||
|
gpio_reset_pin(GPIO_NUM_2);
|
||||||
|
pinMode(PIN_SD_CMD, INPUT_PULLUP);
|
||||||
|
pinMode(PIN_SD_D0, INPUT_PULLUP);
|
||||||
|
delay(20);
|
||||||
|
SD_MMC.setPins(PIN_SD_CLK, PIN_SD_CMD, PIN_SD_D0);
|
||||||
|
gSdReady = SD_MMC.begin("/sdcard", true);
|
||||||
|
if (!gSdReady) {
|
||||||
|
error = "SD card error";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!gBufPrev) {
|
||||||
|
gBufPrev=(uint8_t*)ps_malloc(A2_BLKSZ);
|
||||||
|
gBufRef=(uint8_t*)ps_malloc(A2_BLKSZ);
|
||||||
|
gBufOut=(uint8_t*)ps_malloc(A2_BLKSZ);
|
||||||
|
gBufZero=(uint8_t*)ps_calloc(1, A2_BLKSZ);
|
||||||
|
gBufAddr=(uint8_t*)ps_malloc(A2_BLKSZ);
|
||||||
|
gJ1Seg=(uint32_t*)ps_malloc(A2_SEG * sizeof(uint32_t));
|
||||||
|
}
|
||||||
|
if (!gBufPrev || !gBufRef || !gBufOut || !gBufZero || !gBufAddr || !gJ1Seg) {
|
||||||
|
error = "PSRAM alloc failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error) {
|
||||||
|
error = "";
|
||||||
|
if (!normalizedLogin || !*normalizedLogin) {
|
||||||
|
error = "login not set";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char loginBuf[64];
|
||||||
|
snprintf(loginBuf, sizeof(loginBuf), "%s", normalizedLogin);
|
||||||
|
normalizeLoginInPlace(loginBuf);
|
||||||
|
if (!loginBuf[0]) {
|
||||||
|
error = "login not set";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shineSecretInitSd(error)) return false;
|
||||||
|
|
||||||
|
if (gSdFile) gSdFile.close();
|
||||||
|
SD_MMC.remove(SD_MEM_FILE);
|
||||||
|
gSdFile = SD_MMC.open(SD_MEM_FILE, "w+");
|
||||||
|
if (!gSdFile) {
|
||||||
|
error = "SD open failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(gSecret, 0, sizeof(gSecret));
|
||||||
|
gSecretB58[0] = '\0';
|
||||||
|
gDone = false;
|
||||||
|
gError = false;
|
||||||
|
gRunning = true;
|
||||||
|
gInitDone = false;
|
||||||
|
setMessage("Generating secret...");
|
||||||
|
|
||||||
|
if (!password || !password[0]) {
|
||||||
|
deriveLegacyMasterSecret(password ? password : "", gSecret);
|
||||||
|
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
|
||||||
|
gDone = true;
|
||||||
|
gRunning = false;
|
||||||
|
setMessage("Secret generated");
|
||||||
|
gSdFile.close();
|
||||||
|
SD_MMC.remove(SD_MEM_FILE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
argon2Init(loginBuf, password);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shineSecretStep(uint32_t blocksPerTick) {
|
||||||
|
if (!gRunning || !gInitDone) return gDone;
|
||||||
|
bool done = false;
|
||||||
|
for (uint32_t i = 0; i < blocksPerTick && !done; i++) {
|
||||||
|
if (gCurPass >= A2_T) {
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint32_t blk = gCurBlock;
|
||||||
|
uint32_t slice = blk / A2_SEG;
|
||||||
|
uint32_t posInSlice = blk - slice * A2_SEG;
|
||||||
|
bool xorMode = (gCurPass > 0);
|
||||||
|
uint32_t J1;
|
||||||
|
bool useArgon2i = (gCurPass == 0 && slice < 2);
|
||||||
|
if (useArgon2i) {
|
||||||
|
J1 = gJ1Seg[posInSlice];
|
||||||
|
} else {
|
||||||
|
uint32_t prevIdx = (blk == 0) ? A2_M - 1 : blk - 1;
|
||||||
|
gSdFile.seek((uint64_t)prevIdx * A2_BLKSZ);
|
||||||
|
gSdFile.read(gBufRef, 8);
|
||||||
|
J1 = ((uint32_t*)gBufRef)[0];
|
||||||
|
}
|
||||||
|
uint32_t prevIdx = (blk == 0) ? A2_M - 1 : blk - 1;
|
||||||
|
uint32_t refIdx = indexAlpha(gCurPass, slice, posInSlice, J1);
|
||||||
|
fillBlock(prevIdx, refIdx, blk, xorMode);
|
||||||
|
gDoneBlocks++;
|
||||||
|
gCurBlock++;
|
||||||
|
if (gCurBlock >= A2_M) {
|
||||||
|
gCurBlock = 0;
|
||||||
|
gCurPass++;
|
||||||
|
} else {
|
||||||
|
uint32_t newSlice = gCurBlock / A2_SEG;
|
||||||
|
if (newSlice != slice && gCurPass == 0 && newSlice < 2) generateAddresses(gCurPass, newSlice);
|
||||||
|
}
|
||||||
|
done = (gCurPass >= A2_T);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
gSdFile.seek((uint64_t)(A2_M - 1) * A2_BLKSZ);
|
||||||
|
gSdFile.read(gBufOut, A2_BLKSZ);
|
||||||
|
b2long(gBufOut, A2_BLKSZ, gSecret, A2_DKLEN);
|
||||||
|
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
|
||||||
|
gDone = true;
|
||||||
|
gRunning = false;
|
||||||
|
setMessage("Secret generated");
|
||||||
|
gSdFile.close();
|
||||||
|
SD_MMC.remove(SD_MEM_FILE);
|
||||||
|
}
|
||||||
|
return gDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shineSecretAbort() {
|
||||||
|
gRunning = false;
|
||||||
|
gDone = false;
|
||||||
|
gError = false;
|
||||||
|
gInitDone = false;
|
||||||
|
gDoneBlocks = 0;
|
||||||
|
if (gSdFile) gSdFile.close();
|
||||||
|
if (gSdReady) SD_MMC.remove(SD_MEM_FILE);
|
||||||
|
setMessage("Generation cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
ShineSecretGenerationStatus shineSecretStatus() {
|
||||||
|
ShineSecretGenerationStatus status = {};
|
||||||
|
status.running = gRunning;
|
||||||
|
status.done = gDone;
|
||||||
|
status.error = gError;
|
||||||
|
status.doneBlocks = gDoneBlocks;
|
||||||
|
status.totalBlocks = TOTAL_FILLS;
|
||||||
|
status.elapsedSec = gStartMs == 0 ? 0 : (millis() - gStartMs) / 1000;
|
||||||
|
snprintf(status.message, sizeof(status.message), "%s", gMessage);
|
||||||
|
snprintf(status.secretBase58, sizeof(status.secretBase58), "%s", gSecretB58);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *shineSecretBytes() {
|
||||||
|
return gSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *shineSecretBase58() {
|
||||||
|
return gSecretB58;
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
struct ShineSecretGenerationStatus {
|
||||||
|
bool running;
|
||||||
|
bool done;
|
||||||
|
bool error;
|
||||||
|
uint32_t doneBlocks;
|
||||||
|
uint32_t totalBlocks;
|
||||||
|
uint32_t elapsedSec;
|
||||||
|
char message[96];
|
||||||
|
char secretBase58[64];
|
||||||
|
};
|
||||||
|
|
||||||
|
bool shineSecretInitSd(String &error);
|
||||||
|
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error);
|
||||||
|
bool shineSecretStep(uint32_t blocksPerTick = 4);
|
||||||
|
void shineSecretAbort();
|
||||||
|
ShineSecretGenerationStatus shineSecretStatus();
|
||||||
|
const uint8_t *shineSecretBytes();
|
||||||
|
const char *shineSecretBase58();
|
||||||
|
void shineSecretBase58Encode(const uint8_t *data, size_t len, char *out, size_t outSz);
|
||||||
|
bool shineSecretBase58Decode(const char *input, uint8_t *out, size_t *outLen, size_t maxOutLen, String &error);
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
# SHiNE Homeserver UI Legacy
|
||||||
|
|
||||||
|
Это старый тестовый вариант UI для ESP32-платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
||||||
|
|
||||||
|
Не использовать как основной скетч проекта.
|
||||||
|
Основной рабочий скетч сейчас лежит в `../shine_homeserver_main/`.
|
||||||
@ -0,0 +1 @@
|
|||||||
|
#include "../../official-demo/examples/Arduino-v3.3.5/libraries/lvgl/src/extra/libs/qrcode/qrcodegen.c"
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* ESPRESSIF MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2018 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
|
||||||
|
* it is 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:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AUDIO_HAL_H_
|
||||||
|
#define _AUDIO_HAL_H_
|
||||||
|
|
||||||
|
#define AUDIO_HAL_VOL_DEFAULT 60
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select media hal codec mode
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_HAL_CODEC_MODE_ENCODE = 1, /*!< select adc */
|
||||||
|
AUDIO_HAL_CODEC_MODE_DECODE, /*!< select dac */
|
||||||
|
AUDIO_HAL_CODEC_MODE_BOTH, /*!< select both adc and dac */
|
||||||
|
AUDIO_HAL_CODEC_MODE_LINE_IN, /*!< set adc channel */
|
||||||
|
} audio_hal_codec_mode_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select adc channel for input mic signal
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_HAL_ADC_INPUT_LINE1 = 0x00, /*!< mic input to adc channel 1 */
|
||||||
|
AUDIO_HAL_ADC_INPUT_LINE2, /*!< mic input to adc channel 2 */
|
||||||
|
AUDIO_HAL_ADC_INPUT_ALL, /*!< mic input to both channels of adc */
|
||||||
|
AUDIO_HAL_ADC_INPUT_DIFFERENCE, /*!< mic input to adc difference channel */
|
||||||
|
} audio_hal_adc_input_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select channel for dac output
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_HAL_DAC_OUTPUT_LINE1 = 0x00, /*!< dac output signal to channel 1 */
|
||||||
|
AUDIO_HAL_DAC_OUTPUT_LINE2, /*!< dac output signal to channel 2 */
|
||||||
|
AUDIO_HAL_DAC_OUTPUT_ALL, /*!< dac output signal to both channels */
|
||||||
|
} audio_hal_dac_output_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select operating mode i.e. start or stop for audio codec chip
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_HAL_CTRL_STOP = 0x00, /*!< set stop mode */
|
||||||
|
AUDIO_HAL_CTRL_START = 0x01, /*!< set start mode */
|
||||||
|
} audio_hal_ctrl_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select I2S interface operating mode i.e. master or slave for audio codec chip
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_HAL_MODE_SLAVE = 0x00, /*!< set slave mode */
|
||||||
|
AUDIO_HAL_MODE_MASTER = 0x01, /*!< set master mode */
|
||||||
|
} audio_hal_iface_mode_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select I2S interface samples per second
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_HAL_08K_SAMPLES, /*!< set to 8k samples per second */
|
||||||
|
AUDIO_HAL_11K_SAMPLES, /*!< set to 11.025k samples per second */
|
||||||
|
AUDIO_HAL_16K_SAMPLES, /*!< set to 16k samples in per second */
|
||||||
|
AUDIO_HAL_22K_SAMPLES, /*!< set to 22.050k samples per second */
|
||||||
|
AUDIO_HAL_24K_SAMPLES, /*!< set to 24k samples in per second */
|
||||||
|
AUDIO_HAL_32K_SAMPLES, /*!< set to 32k samples in per second */
|
||||||
|
AUDIO_HAL_44K_SAMPLES, /*!< set to 44.1k samples per second */
|
||||||
|
AUDIO_HAL_48K_SAMPLES, /*!< set to 48k samples per second */
|
||||||
|
} audio_hal_iface_samples_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select I2S interface number of bits per sample
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_HAL_BIT_LENGTH_16BITS = 1, /*!< set 16 bits per sample */
|
||||||
|
AUDIO_HAL_BIT_LENGTH_24BITS, /*!< set 24 bits per sample */
|
||||||
|
AUDIO_HAL_BIT_LENGTH_32BITS, /*!< set 32 bits per sample */
|
||||||
|
} audio_hal_iface_bits_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select I2S interface format for audio codec chip
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_HAL_I2S_NORMAL = 0, /*!< set normal I2S format */
|
||||||
|
AUDIO_HAL_I2S_LEFT, /*!< set all left format */
|
||||||
|
AUDIO_HAL_I2S_RIGHT, /*!< set all right format */
|
||||||
|
AUDIO_HAL_I2S_DSP, /*!< set dsp/pcm format */
|
||||||
|
} audio_hal_iface_format_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief I2s interface configuration for audio codec chip
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
audio_hal_iface_mode_t mode; /*!< audio codec chip mode */
|
||||||
|
audio_hal_iface_format_t fmt; /*!< I2S interface format */
|
||||||
|
audio_hal_iface_samples_t samples; /*!< I2S interface samples per second */
|
||||||
|
audio_hal_iface_bits_t bits; /*!< i2s interface number of bits per sample */
|
||||||
|
} audio_hal_codec_i2s_iface_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Configure media hal for initialization of audio codec chip
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
audio_hal_adc_input_t adc_input; /*!< set adc channel */
|
||||||
|
audio_hal_dac_output_t dac_output; /*!< set dac channel */
|
||||||
|
audio_hal_codec_mode_t codec_mode; /*!< select codec mode: adc, dac or both */
|
||||||
|
audio_hal_codec_i2s_iface_t i2s_iface; /*!< set I2S interface configuration */
|
||||||
|
} audio_hal_codec_config_t;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif //__AUDIO_HAL_H__
|
||||||
@ -0,0 +1,549 @@
|
|||||||
|
/*
|
||||||
|
* ESPRESSIF MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021 <ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
|
||||||
|
* it is 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:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "es7210.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define I2S_DSP_MODE_A 0
|
||||||
|
#define MCLK_DIV_FRE 256
|
||||||
|
|
||||||
|
|
||||||
|
#define ES7210_MCLK_SOURCE FROM_CLOCK_DOUBLE_PIN /* In master mode, 0 : MCLK from pad 1 : MCLK from clock doubler */
|
||||||
|
#define FROM_PAD_PIN 0
|
||||||
|
#define FROM_CLOCK_DOUBLE_PIN 1
|
||||||
|
|
||||||
|
|
||||||
|
static TwoWire *es7210wire;
|
||||||
|
static es7210_gain_value_t gain;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Clock coefficient structer
|
||||||
|
*/
|
||||||
|
struct _coeff_div_es7210 {
|
||||||
|
uint32_t mclk; /* mclk frequency */
|
||||||
|
uint32_t lrck; /* lrck */
|
||||||
|
uint8_t ss_ds;
|
||||||
|
uint8_t adc_div; /* adcclk divider */
|
||||||
|
uint8_t dll; /* dll_bypass */
|
||||||
|
uint8_t doubler; /* doubler enable */
|
||||||
|
uint8_t osr; /* adc osr */
|
||||||
|
uint8_t mclk_src; /* select mclk source */
|
||||||
|
uint32_t lrck_h; /* The high 4 bits of lrck */
|
||||||
|
uint32_t lrck_l; /* The low 8 bits of lrck */
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *TAG = "ES7210";
|
||||||
|
|
||||||
|
static es7210_input_mics_t mic_select = (es7210_input_mics_t)(ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2 | ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4);
|
||||||
|
|
||||||
|
/* Codec hifi mclk clock divider coefficients
|
||||||
|
* MEMBER REG
|
||||||
|
* mclk: 0x03
|
||||||
|
* lrck: standard
|
||||||
|
* ss_ds: --
|
||||||
|
* adc_div: 0x02
|
||||||
|
* dll: 0x06
|
||||||
|
* doubler: 0x02
|
||||||
|
* osr: 0x07
|
||||||
|
* mclk_src: 0x03
|
||||||
|
* lrckh: 0x04
|
||||||
|
* lrckl: 0x05
|
||||||
|
*/
|
||||||
|
static const struct _coeff_div_es7210 coeff_div[] = {
|
||||||
|
//mclk lrck ss_ds adc_div dll doubler osr mclk_src lrckh lrckl
|
||||||
|
/* 8k */
|
||||||
|
{12288000, 8000 , 0x00, 0x03, 0x01, 0x00, 0x20, 0x00, 0x06, 0x00},
|
||||||
|
{16384000, 8000 , 0x00, 0x04, 0x01, 0x00, 0x20, 0x00, 0x08, 0x00},
|
||||||
|
{19200000, 8000 , 0x00, 0x1e, 0x00, 0x01, 0x28, 0x00, 0x09, 0x60},
|
||||||
|
{4096000, 8000 , 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||||
|
|
||||||
|
/* 11.025k */
|
||||||
|
{11289600, 11025, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
|
||||||
|
|
||||||
|
/* 12k */
|
||||||
|
{12288000, 12000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
|
||||||
|
{19200000, 12000, 0x00, 0x14, 0x00, 0x01, 0x28, 0x00, 0x06, 0x40},
|
||||||
|
|
||||||
|
/* 16k */
|
||||||
|
{4096000, 16000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||||
|
{19200000, 16000, 0x00, 0x0a, 0x00, 0x00, 0x1e, 0x00, 0x04, 0x80},
|
||||||
|
{16384000, 16000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
|
||||||
|
{12288000, 16000, 0x00, 0x03, 0x01, 0x01, 0x20, 0x00, 0x03, 0x00},
|
||||||
|
|
||||||
|
/* 22.05k */
|
||||||
|
{11289600, 22050, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||||
|
|
||||||
|
/* 24k */
|
||||||
|
{12288000, 24000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||||
|
{19200000, 24000, 0x00, 0x0a, 0x00, 0x01, 0x28, 0x00, 0x03, 0x20},
|
||||||
|
|
||||||
|
/* 32k */
|
||||||
|
{12288000, 32000, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80},
|
||||||
|
{16384000, 32000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||||
|
{19200000, 32000, 0x00, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x02, 0x58},
|
||||||
|
|
||||||
|
/* 44.1k */
|
||||||
|
{11289600, 44100, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||||
|
|
||||||
|
/* 48k */
|
||||||
|
{12288000, 48000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||||
|
{19200000, 48000, 0x00, 0x05, 0x00, 0x01, 0x28, 0x00, 0x01, 0x90},
|
||||||
|
|
||||||
|
/* 64k */
|
||||||
|
{16384000, 64000, 0x01, 0x01, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
|
||||||
|
{19200000, 64000, 0x00, 0x05, 0x00, 0x01, 0x1e, 0x00, 0x01, 0x2c},
|
||||||
|
|
||||||
|
/* 88.2k */
|
||||||
|
{11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
|
||||||
|
|
||||||
|
/* 96k */
|
||||||
|
{12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
|
||||||
|
{19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8},
|
||||||
|
};
|
||||||
|
|
||||||
|
static esp_err_t es7210_write_reg(uint8_t reg_addr, uint8_t data)
|
||||||
|
{
|
||||||
|
|
||||||
|
es7210wire->beginTransmission(ES7210_ADDR);
|
||||||
|
es7210wire->write(reg_addr);
|
||||||
|
es7210wire->write(data);
|
||||||
|
return es7210wire->endTransmission();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t es7210_update_reg_bit(uint8_t reg_addr, uint8_t update_bits, uint8_t data)
|
||||||
|
{
|
||||||
|
uint8_t regv;
|
||||||
|
regv = es7210_read_reg(reg_addr);
|
||||||
|
regv = (regv & (~update_bits)) | (update_bits & data);
|
||||||
|
return es7210_write_reg(reg_addr, regv);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_coeff(uint32_t mclk, uint32_t lrck)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < (sizeof(coeff_div) / sizeof(coeff_div[0])); i++) {
|
||||||
|
if (coeff_div[i].lrck == lrck && coeff_div[i].mclk == mclk)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t get_es7210_mclk_src(void)
|
||||||
|
{
|
||||||
|
return ES7210_MCLK_SOURCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int es7210_read_reg(uint8_t reg_addr)
|
||||||
|
{
|
||||||
|
uint8_t data;
|
||||||
|
es7210wire->beginTransmission(ES7210_ADDR);
|
||||||
|
es7210wire->write(reg_addr);
|
||||||
|
es7210wire->endTransmission(false);
|
||||||
|
es7210wire->requestFrom(ES7210_ADDR, (size_t)1);
|
||||||
|
data = es7210wire->read();
|
||||||
|
return (int)data;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_config_sample(audio_hal_iface_samples_t sample)
|
||||||
|
{
|
||||||
|
uint8_t regv;
|
||||||
|
int coeff;
|
||||||
|
int sample_fre = 0;
|
||||||
|
int mclk_fre = 0;
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
switch (sample) {
|
||||||
|
case AUDIO_HAL_08K_SAMPLES:
|
||||||
|
sample_fre = 8000;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_11K_SAMPLES:
|
||||||
|
sample_fre = 11025;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_16K_SAMPLES:
|
||||||
|
sample_fre = 16000;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_22K_SAMPLES:
|
||||||
|
sample_fre = 22050;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_24K_SAMPLES:
|
||||||
|
sample_fre = 24000;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_32K_SAMPLES:
|
||||||
|
sample_fre = 32000;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_44K_SAMPLES:
|
||||||
|
sample_fre = 44100;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_48K_SAMPLES:
|
||||||
|
sample_fre = 48000;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE(TAG, "Unable to configure sample rate %dHz", sample_fre);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mclk_fre = sample_fre * MCLK_DIV_FRE;
|
||||||
|
coeff = get_coeff(mclk_fre, sample_fre);
|
||||||
|
if (coeff < 0) {
|
||||||
|
ESP_LOGE(TAG, "Unable to configure sample rate %dHz with %dHz MCLK", sample_fre, mclk_fre);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
/* Set clock parammeters */
|
||||||
|
if (coeff >= 0) {
|
||||||
|
/* Set adc_div & doubler & dll */
|
||||||
|
regv = es7210_read_reg(ES7210_MAINCLK_REG02) & 0x00;
|
||||||
|
regv |= coeff_div[coeff].adc_div;
|
||||||
|
regv |= coeff_div[coeff].doubler << 6;
|
||||||
|
regv |= coeff_div[coeff].dll << 7;
|
||||||
|
ret |= es7210_write_reg(ES7210_MAINCLK_REG02, regv);
|
||||||
|
/* Set osr */
|
||||||
|
regv = coeff_div[coeff].osr;
|
||||||
|
ret |= es7210_write_reg(ES7210_OSR_REG07, regv);
|
||||||
|
/* Set lrck */
|
||||||
|
regv = coeff_div[coeff].lrck_h;
|
||||||
|
ret |= es7210_write_reg(ES7210_LRCK_DIVH_REG04, regv);
|
||||||
|
regv = coeff_div[coeff].lrck_l;
|
||||||
|
ret |= es7210_write_reg(ES7210_LRCK_DIVL_REG05, regv);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_mic_select(es7210_input_mics_t mic)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
mic_select = mic;
|
||||||
|
if (mic_select & (ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2 | ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4)) {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00);
|
||||||
|
}
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0xff);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0xff);
|
||||||
|
if (mic_select & ES7210_INPUT_MIC1) {
|
||||||
|
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC1");
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0x00);
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x10, 0x10);
|
||||||
|
}
|
||||||
|
if (mic_select & ES7210_INPUT_MIC2) {
|
||||||
|
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC2");
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0x00);
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x10, 0x10);
|
||||||
|
}
|
||||||
|
if (mic_select & ES7210_INPUT_MIC3) {
|
||||||
|
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC3");
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x15, 0x00);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0x00);
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x10, 0x10);
|
||||||
|
}
|
||||||
|
if (mic_select & ES7210_INPUT_MIC4) {
|
||||||
|
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC4");
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x15, 0x00);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0x00);
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x10, 0x10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Microphone selection error");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_adc_init(TwoWire *tw, audio_hal_codec_config_t *codec_cfg)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
|
||||||
|
es7210wire = tw;
|
||||||
|
|
||||||
|
ret |= es7210_write_reg(ES7210_RESET_REG00, 0xff);
|
||||||
|
ret |= es7210_write_reg(ES7210_RESET_REG00, 0x41);
|
||||||
|
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, 0x1f);
|
||||||
|
ret |= es7210_write_reg(ES7210_TIME_CONTROL0_REG09, 0x30); /* Set chip state cycle */
|
||||||
|
ret |= es7210_write_reg(ES7210_TIME_CONTROL1_REG0A, 0x30); /* Set power on state cycle */
|
||||||
|
// ret |= es7210_write_reg(ES7210_ADC12_HPF2_REG23, 0x2a); /* Quick setup */
|
||||||
|
// ret |= es7210_write_reg(ES7210_ADC12_HPF1_REG22, 0x0a);
|
||||||
|
// ret |= es7210_write_reg(ES7210_ADC34_HPF2_REG20, 0x0a);
|
||||||
|
// ret |= es7210_write_reg(ES7210_ADC34_HPF1_REG21, 0x2a);
|
||||||
|
/* Set master/slave audio interface */
|
||||||
|
audio_hal_codec_i2s_iface_t *i2s_cfg = & (codec_cfg->i2s_iface);
|
||||||
|
switch (i2s_cfg->mode) {
|
||||||
|
case AUDIO_HAL_MODE_MASTER: /* MASTER MODE */
|
||||||
|
ESP_LOGI(TAG, "ES7210 in Master mode");
|
||||||
|
// ret |= es7210_update_reg_bit(ES7210_MODE_CONFIG_REG08, 0x01, 0x01);
|
||||||
|
ret |= es7210_write_reg(ES7210_MODE_CONFIG_REG08, 0x20);
|
||||||
|
/* Select clock source for internal mclk */
|
||||||
|
switch (get_es7210_mclk_src()) {
|
||||||
|
case FROM_PAD_PIN:
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x00);
|
||||||
|
break;
|
||||||
|
case FROM_CLOCK_DOUBLE_PIN:
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x80);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x00);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_MODE_SLAVE: /* SLAVE MODE */
|
||||||
|
ESP_LOGI(TAG, "ES7210 in Slave mode");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0xC3); /* Select power off analog, vdda = 3.3V, close vx20ff, VMID select 5KΩ start */
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC12_BIAS_REG41, 0x70); /* Select 2.87v */
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC34_BIAS_REG42, 0x70); /* Select 2.87v */
|
||||||
|
ret |= es7210_write_reg(ES7210_OSR_REG07, 0x20);
|
||||||
|
ret |= es7210_write_reg(ES7210_MAINCLK_REG02, 0xc1); /* Set the frequency division coefficient and use dll except clock doubler, and need to set 0xc1 to clear the state */
|
||||||
|
ret |= es7210_config_sample(i2s_cfg->samples);
|
||||||
|
ret |= es7210_mic_select(mic_select);
|
||||||
|
ret |= es7210_adc_set_gain_all(GAIN_0DB);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_adc_deinit()
|
||||||
|
{
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_config_fmt(audio_hal_iface_format_t fmt)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
uint8_t adc_iface = 0;
|
||||||
|
adc_iface = es7210_read_reg(ES7210_SDP_INTERFACE1_REG11);
|
||||||
|
adc_iface &= 0xfc;
|
||||||
|
switch (fmt) {
|
||||||
|
case AUDIO_HAL_I2S_NORMAL:
|
||||||
|
ESP_LOGD(TAG, "ES7210 in I2S Format");
|
||||||
|
adc_iface |= 0x00;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_I2S_LEFT:
|
||||||
|
case AUDIO_HAL_I2S_RIGHT:
|
||||||
|
ESP_LOGD(TAG, "ES7210 in LJ Format");
|
||||||
|
adc_iface |= 0x01;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_I2S_DSP:
|
||||||
|
if (I2S_DSP_MODE_A) {
|
||||||
|
ESP_LOGD(TAG, "ES7210 in DSP-A Format");
|
||||||
|
adc_iface |= 0x03;
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "ES7210 in DSP-B Format");
|
||||||
|
adc_iface |= 0x13;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
adc_iface &= 0xfc;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ret |= es7210_write_reg(ES7210_SDP_INTERFACE1_REG11, adc_iface);
|
||||||
|
/* Force ADC1/2 output to SDOUT1 and ADC3/4 output to SDOUT2 */
|
||||||
|
ret |= es7210_write_reg(ES7210_SDP_INTERFACE2_REG12, 0x00);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_set_bits(audio_hal_iface_bits_t bits)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
uint8_t adc_iface = 0;
|
||||||
|
adc_iface = es7210_read_reg(ES7210_SDP_INTERFACE1_REG11);
|
||||||
|
adc_iface &= 0x1f;
|
||||||
|
switch (bits) {
|
||||||
|
case AUDIO_HAL_BIT_LENGTH_16BITS:
|
||||||
|
adc_iface |= 0x60;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_BIT_LENGTH_24BITS:
|
||||||
|
adc_iface |= 0x00;
|
||||||
|
break;
|
||||||
|
case AUDIO_HAL_BIT_LENGTH_32BITS:
|
||||||
|
adc_iface |= 0x80;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
adc_iface |= 0x60;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ret |= es7210_write_reg(ES7210_SDP_INTERFACE1_REG11, adc_iface);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_adc_config_i2s(audio_hal_codec_mode_t mode, audio_hal_codec_i2s_iface_t *iface)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
ret |= es7210_set_bits(iface->bits);
|
||||||
|
ret |= es7210_config_fmt(iface->fmt);
|
||||||
|
ret |= es7210_config_sample(iface->samples);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_start(uint8_t clock_reg_value)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, clock_reg_value);
|
||||||
|
ret |= es7210_write_reg(ES7210_POWER_DOWN_REG06, 0x00);
|
||||||
|
// ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0x40);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC1_POWER_REG47, 0x00);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC2_POWER_REG48, 0x00);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC3_POWER_REG49, 0x00);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC4_POWER_REG4A, 0x00);
|
||||||
|
ret |= es7210_mic_select(mic_select);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_stop(void)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC1_POWER_REG47, 0xff);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC2_POWER_REG48, 0xff);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC3_POWER_REG49, 0xff);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC4_POWER_REG4A, 0xff);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B,0xff);
|
||||||
|
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0xff);
|
||||||
|
// ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0xc0);
|
||||||
|
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, 0x7f);
|
||||||
|
ret |= es7210_write_reg(ES7210_POWER_DOWN_REG06, 0x07);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_adc_ctrl_state(audio_hal_codec_mode_t mode, audio_hal_ctrl_t ctrl_state)
|
||||||
|
{
|
||||||
|
static uint8_t regv;
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
// ESP_LOGW(TAG, "ES7210 only supports ADC mode");
|
||||||
|
ret = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
|
||||||
|
if ((ret != 0x7f) && (ret != 0xff)) {
|
||||||
|
regv = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
|
||||||
|
}
|
||||||
|
if (ctrl_state == AUDIO_HAL_CTRL_START) {
|
||||||
|
ESP_LOGI(TAG, "The ES7210_CLOCK_OFF_REG01 value before stop is %x",regv);
|
||||||
|
ret |= es7210_start(regv);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "The codec is about to stop");
|
||||||
|
regv = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
|
||||||
|
ret |= es7210_stop();
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_adc_set_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t gain)
|
||||||
|
{
|
||||||
|
esp_err_t ret_val = ESP_OK;
|
||||||
|
|
||||||
|
if (gain < GAIN_0DB) {
|
||||||
|
gain = GAIN_0DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gain > GAIN_37_5DB) {
|
||||||
|
gain = GAIN_37_5DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mic_mask & ES7210_INPUT_MIC1) {
|
||||||
|
ret_val |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x0f, gain);
|
||||||
|
}
|
||||||
|
if (mic_mask & ES7210_INPUT_MIC2) {
|
||||||
|
ret_val |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x0f, gain);
|
||||||
|
}
|
||||||
|
if (mic_mask & ES7210_INPUT_MIC3) {
|
||||||
|
ret_val |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x0f, gain);
|
||||||
|
}
|
||||||
|
if (mic_mask & ES7210_INPUT_MIC4) {
|
||||||
|
ret_val |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x0f, gain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_adc_set_gain_all(es7210_gain_value_t gain)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
uint32_t max_gain_vaule = 14;
|
||||||
|
if (gain < 0) {
|
||||||
|
gain = (es7210_gain_value_t) 0;
|
||||||
|
} else if (gain > max_gain_vaule) {
|
||||||
|
gain = (es7210_gain_value_t) max_gain_vaule;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "SET: gain:%d", gain);
|
||||||
|
if (mic_select & ES7210_INPUT_MIC1) {
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x0f, gain);
|
||||||
|
}
|
||||||
|
if (mic_select & ES7210_INPUT_MIC2) {
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x0f, gain);
|
||||||
|
}
|
||||||
|
if (mic_select & ES7210_INPUT_MIC3) {
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x0f, gain);
|
||||||
|
}
|
||||||
|
if (mic_select & ES7210_INPUT_MIC4) {
|
||||||
|
ret |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x0f, gain);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_adc_get_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t *gain)
|
||||||
|
{
|
||||||
|
int regv = 0;
|
||||||
|
uint8_t gain_value;
|
||||||
|
if (mic_mask & ES7210_INPUT_MIC1) {
|
||||||
|
regv = es7210_read_reg(ES7210_MIC1_GAIN_REG43);
|
||||||
|
} else if (mic_mask & ES7210_INPUT_MIC2) {
|
||||||
|
regv = es7210_read_reg(ES7210_MIC2_GAIN_REG44);
|
||||||
|
} else if (mic_mask & ES7210_INPUT_MIC3) {
|
||||||
|
regv = es7210_read_reg(ES7210_MIC3_GAIN_REG45);
|
||||||
|
} else if (mic_mask & ES7210_INPUT_MIC4) {
|
||||||
|
regv = es7210_read_reg(ES7210_MIC4_GAIN_REG46);
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "No MIC selected");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
if (regv == ESP_FAIL) {
|
||||||
|
return regv;
|
||||||
|
}
|
||||||
|
gain_value = (regv & 0x0f); /* Retain the last four bits for gain */
|
||||||
|
*gain = (es7210_gain_value_t) gain_value;
|
||||||
|
ESP_LOGI(TAG, "GET: gain_value:%d", gain_value);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_adc_set_volume(int volume)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
ESP_LOGD(TAG, "ADC can adjust gain");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t es7210_set_mute(bool enable)
|
||||||
|
{
|
||||||
|
ESP_LOGD(TAG, "ES7210 SetMute :%d", enable);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void es7210_read_all(void)
|
||||||
|
{
|
||||||
|
for (int i = 0; i <= 0x4E; i++) {
|
||||||
|
uint8_t reg = es7210_read_reg(i);
|
||||||
|
ets_printf("REG:%02x, %02x\n", reg, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -0,0 +1,260 @@
|
|||||||
|
/*
|
||||||
|
* ESPRESSIF MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021 <ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
|
||||||
|
* it is 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:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _ES7210_H
|
||||||
|
#define _ES7210_H
|
||||||
|
|
||||||
|
#include "audio_hal.h"
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ES7210_AD1_AD0_00 = 0x40,
|
||||||
|
ES7210_AD1_AD0_01 = 0x41,
|
||||||
|
ES7210_AD1_AD0_10 = 0x42,
|
||||||
|
ES7210_AD1_AD0_11 = 0x43,
|
||||||
|
} es7210_address_t;
|
||||||
|
|
||||||
|
/* ES7210 address*/
|
||||||
|
#define ES7210_ADDR ES7210_AD1_AD0_00
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define ES7210_RESET_REG00 0x00 /* Reset control */
|
||||||
|
#define ES7210_CLOCK_OFF_REG01 0x01 /* Used to turn off the ADC clock */
|
||||||
|
#define ES7210_MAINCLK_REG02 0x02 /* Set ADC clock frequency division */
|
||||||
|
#define ES7210_MASTER_CLK_REG03 0x03 /* MCLK source $ SCLK division */
|
||||||
|
#define ES7210_LRCK_DIVH_REG04 0x04 /* lrck_divh */
|
||||||
|
#define ES7210_LRCK_DIVL_REG05 0x05 /* lrck_divl */
|
||||||
|
#define ES7210_POWER_DOWN_REG06 0x06 /* power down */
|
||||||
|
#define ES7210_OSR_REG07 0x07
|
||||||
|
#define ES7210_MODE_CONFIG_REG08 0x08 /* Set master/slave & channels */
|
||||||
|
#define ES7210_TIME_CONTROL0_REG09 0x09 /* Set Chip intial state period*/
|
||||||
|
#define ES7210_TIME_CONTROL1_REG0A 0x0A /* Set Power up state period */
|
||||||
|
#define ES7210_SDP_INTERFACE1_REG11 0x11 /* Set sample & fmt */
|
||||||
|
#define ES7210_SDP_INTERFACE2_REG12 0x12 /* Pins state */
|
||||||
|
#define ES7210_ADC_AUTOMUTE_REG13 0x13 /* Set mute */
|
||||||
|
#define ES7210_ADC34_MUTERANGE_REG14 0x14 /* Set mute range */
|
||||||
|
#define ES7210_ADC34_HPF2_REG20 0x20 /* HPF */
|
||||||
|
#define ES7210_ADC34_HPF1_REG21 0x21
|
||||||
|
#define ES7210_ADC12_HPF1_REG22 0x22
|
||||||
|
#define ES7210_ADC12_HPF2_REG23 0x23
|
||||||
|
#define ES7210_ANALOG_REG40 0x40 /* ANALOG Power */
|
||||||
|
#define ES7210_MIC12_BIAS_REG41 0x41
|
||||||
|
#define ES7210_MIC34_BIAS_REG42 0x42
|
||||||
|
#define ES7210_MIC1_GAIN_REG43 0x43
|
||||||
|
#define ES7210_MIC2_GAIN_REG44 0x44
|
||||||
|
#define ES7210_MIC3_GAIN_REG45 0x45
|
||||||
|
#define ES7210_MIC4_GAIN_REG46 0x46
|
||||||
|
#define ES7210_MIC1_POWER_REG47 0x47
|
||||||
|
#define ES7210_MIC2_POWER_REG48 0x48
|
||||||
|
#define ES7210_MIC3_POWER_REG49 0x49
|
||||||
|
#define ES7210_MIC4_POWER_REG4A 0x4A
|
||||||
|
#define ES7210_MIC12_POWER_REG4B 0x4B /* MICBias & ADC & PGA Power */
|
||||||
|
#define ES7210_MIC34_POWER_REG4C 0x4C
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ES7210_INPUT_MIC1 = 0x01,
|
||||||
|
ES7210_INPUT_MIC2 = 0x02,
|
||||||
|
ES7210_INPUT_MIC3 = 0x04,
|
||||||
|
ES7210_INPUT_MIC4 = 0x08
|
||||||
|
} es7210_input_mics_t;
|
||||||
|
|
||||||
|
typedef enum gain_value{
|
||||||
|
GAIN_0DB = 0,
|
||||||
|
GAIN_3DB,
|
||||||
|
GAIN_6DB,
|
||||||
|
GAIN_9DB,
|
||||||
|
GAIN_12DB,
|
||||||
|
GAIN_15DB,
|
||||||
|
GAIN_18DB,
|
||||||
|
GAIN_21DB,
|
||||||
|
GAIN_24DB,
|
||||||
|
GAIN_27DB,
|
||||||
|
GAIN_30DB,
|
||||||
|
GAIN_33DB,
|
||||||
|
GAIN_34_5DB,
|
||||||
|
GAIN_36DB,
|
||||||
|
GAIN_37_5DB,
|
||||||
|
} es7210_gain_value_t;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @brief Initialize ES7210 ADC chip
|
||||||
|
*
|
||||||
|
* @param[in] codec_cfg: configuration of ES7210
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_OK
|
||||||
|
* - ESP_FAIL
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_adc_init(TwoWire *tw, audio_hal_codec_config_t *codec_cfg);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Deinitialize ES7210 ADC chip
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_OK
|
||||||
|
* - ESP_FAIL
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_adc_deinit();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Configure ES7210 ADC mode and I2S interface
|
||||||
|
*
|
||||||
|
* @param[in] mode: codec mode
|
||||||
|
* @param[in] iface: I2S config
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_FAIL Parameter error
|
||||||
|
* - ESP_OK Success
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_adc_config_i2s(audio_hal_codec_mode_t mode, audio_hal_codec_i2s_iface_t *iface);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Control ES7210 ADC chip
|
||||||
|
*
|
||||||
|
* @param[in] mode: codec mode
|
||||||
|
* @param[in] ctrl_state: start or stop progress
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_FAIL Parameter error
|
||||||
|
* - ESP_OK Success
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_adc_ctrl_state(audio_hal_codec_mode_t mode, audio_hal_ctrl_t ctrl_state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set gain of given mask
|
||||||
|
*
|
||||||
|
* @param[in] mic_mask Mask of MIC channel
|
||||||
|
*
|
||||||
|
* @param[in] gain: gain
|
||||||
|
*
|
||||||
|
* gain : value
|
||||||
|
* GAIN_0DB : 1
|
||||||
|
* GAIN_3DB : 2
|
||||||
|
* GAIN_6DB : 3
|
||||||
|
* ·
|
||||||
|
* ·
|
||||||
|
* ·
|
||||||
|
* GAIN_30DB : 10
|
||||||
|
* GAIN_33DB : 11
|
||||||
|
* GAIN_34_5DB : 12
|
||||||
|
* GAIN_36DB : 13
|
||||||
|
* GAIN_37_5DB : 14
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_OK
|
||||||
|
* - ESP_FAIL
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_adc_set_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t gain);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set gain (Note: the enabled microphone sets the same gain)
|
||||||
|
*
|
||||||
|
* @param[in] gain: gain
|
||||||
|
*
|
||||||
|
* gain : value
|
||||||
|
* GAIN_0DB : 1
|
||||||
|
* GAIN_3DB : 2
|
||||||
|
* GAIN_6DB : 3
|
||||||
|
* ·
|
||||||
|
* ·
|
||||||
|
* ·
|
||||||
|
* GAIN_30DB : 10
|
||||||
|
* GAIN_33DB : 11
|
||||||
|
* GAIN_34_5DB : 12
|
||||||
|
* GAIN_36DB : 13
|
||||||
|
* GAIN_37_5DB : 14
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_OK
|
||||||
|
* - ESP_FAIL
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_adc_set_gain_all(es7210_gain_value_t gain);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get MIC gain
|
||||||
|
*
|
||||||
|
* @param mic_mask Selected MIC
|
||||||
|
* @param gain Pointer to `es7210_gain_value_t`
|
||||||
|
* @return
|
||||||
|
* - ESP_OK
|
||||||
|
* - ESP_FAIL
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_adc_get_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t *gain);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set volume
|
||||||
|
*
|
||||||
|
* @param[in] volume: volume
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_OK
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_adc_set_volume(int volume);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set ES7210 ADC mute status
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_FAIL
|
||||||
|
* - ESP_OK
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_set_mute(bool enable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Select ES7210 mic
|
||||||
|
*
|
||||||
|
* @param[in] mic: mics
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_FAIL
|
||||||
|
* - ESP_OK
|
||||||
|
*/
|
||||||
|
esp_err_t es7210_mic_select(es7210_input_mics_t mic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read regs of ES7210
|
||||||
|
*
|
||||||
|
* @param[in] reg_addr: reg_addr
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* - ESP_FAIL
|
||||||
|
* - ESP_OK
|
||||||
|
*/
|
||||||
|
int es7210_read_reg(uint8_t reg_addr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read all regs of ES7210
|
||||||
|
*/
|
||||||
|
void es7210_read_all(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* _ES7210_H_ */
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user