Новый shine_payments v2, legacy-перенос и обновление Devnet UI
This commit is contained in:
parent
09dea46948
commit
0a9b76055f
@ -7,3 +7,19 @@
|
|||||||
- `doc/SHINE_USER_PDA_V1.md`
|
- `doc/SHINE_USER_PDA_V1.md`
|
||||||
|
|
||||||
Если меняется формат записи, сериализация, правила подписи, `prev_hash`, экономика лимитов или связанные ограничения create/update, этот документ нужно обновлять в том же изменении.
|
Если меняется формат записи, сериализация, правила подписи, `prev_hash`, экономика лимитов или связанные ограничения create/update, этот документ нужно обновлять в том же изменении.
|
||||||
|
|
||||||
|
## Language Rule
|
||||||
|
|
||||||
|
Во всем проекте использовать русский язык:
|
||||||
|
|
||||||
|
- комментарии в коде;
|
||||||
|
- тексты в файлах настроек и справочных файлах;
|
||||||
|
- сообщения и описания в коммитах;
|
||||||
|
- сопроводительные технические заметки.
|
||||||
|
|
||||||
|
## Rule: Logic and Docs
|
||||||
|
|
||||||
|
Если меняется бизнес-логика смарт-контрактов, сериализация PDA, правила переводов или экономика:
|
||||||
|
|
||||||
|
1. Обновить соответствующий документ в `doc/` в том же изменении.
|
||||||
|
2. Если документ сразу обновить нельзя, обязательно явно согласовать это с пользователем в чате и зафиксировать план обновления.
|
||||||
|
|||||||
@ -5,35 +5,23 @@ package_manager = "yarn"
|
|||||||
resolution = true
|
resolution = true
|
||||||
skip-lint = false
|
skip-lint = false
|
||||||
|
|
||||||
[programs.localnet]
|
|
||||||
shine_users = "5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t" #тут надо если что обновлять
|
|
||||||
shine_payments = "92sgkgx7KHpbhQu81mNGHaKa7skJB7esArVdPM7paDSW" #тут надо если что обновлять
|
|
||||||
|
|
||||||
|
|
||||||
[programs.devnet]
|
[programs.devnet]
|
||||||
shine_users = "5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t" #тут надо если что обновлять
|
shine_payments = "4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE"
|
||||||
shine_payments = "92sgkgx7KHpbhQu81mNGHaKa7skJB7esArVdPM7paDSW" #тут надо если что обновлять
|
shine_users = "8Z3HQizFRhyVu5cNBwWNBXZHTpu89VMkn7Wuk1oCtkeJ"
|
||||||
|
|
||||||
|
[programs.localnet]
|
||||||
[workspace]
|
shine_payments = "4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE"
|
||||||
members = [
|
shine_users = "5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t"
|
||||||
"programs/shine_users",
|
|
||||||
"programs/shine_payments",
|
|
||||||
]
|
|
||||||
|
|
||||||
[registry]
|
[registry]
|
||||||
url = "https://api.apr.dev"
|
url = "https://api.apr.dev"
|
||||||
|
|
||||||
[provider]
|
[provider]
|
||||||
cluster = "devnet"#"http://127.0.0.1:8899" # это в какую сеть деплоит по умолчанию
|
cluster = "devnet"
|
||||||
wallet = "~/.config/solana/id.json" # а это с какого кошелько спишутся средства за деплой
|
wallet = "~/.config/solana/id.json"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["programs/shine_users", "programs/shine_payments"]
|
||||||
|
|
||||||
[scripts]
|
[scripts]
|
||||||
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
769
shine/Cargo.lock
generated
769
shine/Cargo.lock
generated
@ -2,42 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aead"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-common",
|
|
||||||
"generic-array",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aes"
|
|
||||||
version = "0.8.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cipher",
|
|
||||||
"cpufeatures",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aes-gcm-siv"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d"
|
|
||||||
dependencies = [
|
|
||||||
"aead",
|
|
||||||
"aes",
|
|
||||||
"cipher",
|
|
||||||
"ctr",
|
|
||||||
"polyval",
|
|
||||||
"subtle",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.12"
|
version = "0.8.12"
|
||||||
@ -59,12 +23,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "allocator-api2"
|
|
||||||
version = "0.2.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anchor-attribute-access-control"
|
name = "anchor-attribute-access-control"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
@ -225,21 +183,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anchor-spl"
|
|
||||||
version = "0.31.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3c08cb5d762c0694f74bd02c9a5b04ea53cefc496e2c27b3234acffca5cd076b"
|
|
||||||
dependencies = [
|
|
||||||
"anchor-lang",
|
|
||||||
"spl-associated-token-account",
|
|
||||||
"spl-pod",
|
|
||||||
"spl-token 7.0.0",
|
|
||||||
"spl-token-2022",
|
|
||||||
"spl-token-group-interface",
|
|
||||||
"spl-token-metadata-interface",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anchor-syn"
|
name = "anchor-syn"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
@ -446,9 +389,6 @@ name = "bytemuck"
|
|||||||
version = "1.23.0"
|
version = "1.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
|
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
|
||||||
dependencies = [
|
|
||||||
"bytemuck_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck_derive"
|
name = "bytemuck_derive"
|
||||||
@ -461,12 +401,6 @@ dependencies = [
|
|||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byteorder"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cargo_toml"
|
name = "cargo_toml"
|
||||||
version = "0.19.2"
|
version = "0.19.2"
|
||||||
@ -498,16 +432,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cipher"
|
|
||||||
version = "0.4.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-common",
|
|
||||||
"inout",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -563,32 +487,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
"rand_core 0.6.4",
|
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ctr"
|
|
||||||
version = "0.9.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
|
||||||
dependencies = [
|
|
||||||
"cipher",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "curve25519-dalek"
|
|
||||||
version = "3.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
"digest 0.9.0",
|
|
||||||
"rand_core 0.5.1",
|
|
||||||
"subtle",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curve25519-dalek"
|
name = "curve25519-dalek"
|
||||||
version = "4.1.3"
|
version = "4.1.3"
|
||||||
@ -602,7 +503,6 @@ dependencies = [
|
|||||||
"fiat-crypto",
|
"fiat-crypto",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"serde",
|
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@ -618,12 +518,6 @@ dependencies = [
|
|||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "derivation-path"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@ -644,33 +538,6 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ed25519"
|
|
||||||
version = "1.5.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
|
|
||||||
dependencies = [
|
|
||||||
"signature",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ed25519-dalek"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
|
|
||||||
dependencies = [
|
|
||||||
"curve25519-dalek 3.2.0",
|
|
||||||
"ed25519",
|
|
||||||
"sha2 0.9.9",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "either"
|
|
||||||
version = "1.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@ -713,18 +580,6 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5"
|
checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fnv"
|
|
||||||
version = "1.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foldhash"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@ -773,11 +628,6 @@ name = "hashbrown"
|
|||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
dependencies = [
|
|
||||||
"allocator-api2",
|
|
||||||
"equivalent",
|
|
||||||
"foldhash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@ -788,15 +638,6 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hmac"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
|
||||||
dependencies = [
|
|
||||||
"digest 0.10.7",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@ -807,24 +648,6 @@ dependencies = [
|
|||||||
"hashbrown 0.15.2",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inout"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@ -939,31 +762,6 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "merlin"
|
|
||||||
version = "3.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
"keccak",
|
|
||||||
"rand_core 0.6.4",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mpl-token-metadata"
|
|
||||||
version = "5.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "046f0779684ec348e2759661361c8798d79021707b1392cb49f3b5eb911340ff"
|
|
||||||
dependencies = [
|
|
||||||
"borsh 0.10.4",
|
|
||||||
"num-derive 0.3.3",
|
|
||||||
"num-traits",
|
|
||||||
"solana-program",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@ -974,17 +772,6 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-derive"
|
|
||||||
version = "0.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@ -1014,28 +801,6 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num_enum"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
|
||||||
dependencies = [
|
|
||||||
"num_enum_derive",
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num_enum_derive"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro-crate 3.3.0",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.101",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@ -1071,33 +836,6 @@ dependencies = [
|
|||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pbkdf2"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
|
||||||
dependencies = [
|
|
||||||
"digest 0.10.7",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "percent-encoding"
|
|
||||||
version = "2.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "polyval"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"opaque-debug",
|
|
||||||
"universal-hash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@ -1134,15 +872,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "qstring"
|
|
||||||
version = "0.7.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e"
|
|
||||||
dependencies = [
|
|
||||||
"percent-encoding",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@ -1380,13 +1109,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shine_payments"
|
name = "shine_payments"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anchor-lang",
|
"anchor-lang",
|
||||||
"anchor-spl",
|
|
||||||
"common",
|
"common",
|
||||||
"mpl-token-metadata",
|
|
||||||
"spl-token 4.0.2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1395,8 +1121,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anchor-lang",
|
"anchor-lang",
|
||||||
"common",
|
"common",
|
||||||
"ed25519-dalek",
|
|
||||||
"sha2 0.10.9",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1405,12 +1129,6 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signature"
|
|
||||||
version = "1.6.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@ -1540,20 +1258,6 @@ dependencies = [
|
|||||||
"solana-stable-layout",
|
"solana-stable-layout",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-curve25519"
|
|
||||||
version = "2.3.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eae4261b9a8613d10e77ac831a8fa60b6fa52b9b103df46d641deff9f9812a23"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"bytemuck_derive",
|
|
||||||
"curve25519-dalek 4.1.3",
|
|
||||||
"solana-define-syscall",
|
|
||||||
"subtle",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solana-decode-error"
|
name = "solana-decode-error"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@ -1569,17 +1273,6 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2"
|
checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-derivation-path"
|
|
||||||
version = "2.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "939756d798b25c5ec3cca10e06212bdca3b1443cb9bb740a38124f58b258737b"
|
|
||||||
dependencies = [
|
|
||||||
"derivation-path",
|
|
||||||
"qstring",
|
|
||||||
"uriparse",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solana-epoch-rewards"
|
name = "solana-epoch-rewards"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@ -1851,7 +1544,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"num-derive 0.4.2",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
@ -1975,7 +1668,7 @@ dependencies = [
|
|||||||
"borsh 1.5.7",
|
"borsh 1.5.7",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"bytemuck_derive",
|
"bytemuck_derive",
|
||||||
"curve25519-dalek 4.1.3",
|
"curve25519-dalek",
|
||||||
"five8",
|
"five8",
|
||||||
"five8_const",
|
"five8_const",
|
||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
@ -2042,35 +1735,6 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-security-txt"
|
|
||||||
version = "1.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "156bb61a96c605fa124e052d630dba2f6fb57e08c7d15b757e1e958b3ed7b3fe"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown 0.15.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-seed-derivable"
|
|
||||||
version = "2.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3beb82b5adb266c6ea90e5cf3967235644848eac476c5a1f2f9283a143b7c97f"
|
|
||||||
dependencies = [
|
|
||||||
"solana-derivation-path",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-seed-phrase"
|
|
||||||
version = "2.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15"
|
|
||||||
dependencies = [
|
|
||||||
"hmac",
|
|
||||||
"pbkdf2",
|
|
||||||
"sha2 0.10.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solana-serde-varint"
|
name = "solana-serde-varint"
|
||||||
version = "2.2.2"
|
version = "2.2.2"
|
||||||
@ -2111,27 +1775,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-signature"
|
|
||||||
version = "2.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "64c8ec8e657aecfc187522fc67495142c12f35e55ddeca8698edbb738b8dbd8c"
|
|
||||||
dependencies = [
|
|
||||||
"five8",
|
|
||||||
"solana-sanitize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-signer"
|
|
||||||
version = "2.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7c41991508a4b02f021c1342ba00bcfa098630b213726ceadc7cb032e051975b"
|
|
||||||
dependencies = [
|
|
||||||
"solana-pubkey",
|
|
||||||
"solana-signature",
|
|
||||||
"solana-transaction-error",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solana-slot-hashes"
|
name = "solana-slot-hashes"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@ -2269,7 +1912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ef4f08746f154458f28b98330c0d55cb431e2de64ee4b8efc98dcbe292e0672b"
|
checksum = "ef4f08746f154458f28b98330c0d55cb431e2de64ee4b8efc98dcbe292e0672b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"num-derive 0.4.2",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@ -2286,376 +1929,6 @@ dependencies = [
|
|||||||
"solana-system-interface",
|
"solana-system-interface",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-zk-sdk"
|
|
||||||
version = "2.3.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97b9fc6ec37d16d0dccff708ed1dd6ea9ba61796700c3bb7c3b401973f10f63b"
|
|
||||||
dependencies = [
|
|
||||||
"aes-gcm-siv",
|
|
||||||
"base64 0.22.1",
|
|
||||||
"bincode",
|
|
||||||
"bytemuck",
|
|
||||||
"bytemuck_derive",
|
|
||||||
"curve25519-dalek 4.1.3",
|
|
||||||
"itertools",
|
|
||||||
"js-sys",
|
|
||||||
"merlin",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"sha3",
|
|
||||||
"solana-derivation-path",
|
|
||||||
"solana-instruction",
|
|
||||||
"solana-pubkey",
|
|
||||||
"solana-sdk-ids",
|
|
||||||
"solana-seed-derivable",
|
|
||||||
"solana-seed-phrase",
|
|
||||||
"solana-signature",
|
|
||||||
"solana-signer",
|
|
||||||
"subtle",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-associated-token-account"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3"
|
|
||||||
dependencies = [
|
|
||||||
"borsh 1.5.7",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"solana-program",
|
|
||||||
"spl-associated-token-account-client",
|
|
||||||
"spl-token 7.0.0",
|
|
||||||
"spl-token-2022",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-associated-token-account-client"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6f8349dbcbe575f354f9a533a21f272f3eb3808a49e2fdc1c34393b88ba76cb"
|
|
||||||
dependencies = [
|
|
||||||
"solana-instruction",
|
|
||||||
"solana-pubkey",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-discriminator"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"solana-program-error",
|
|
||||||
"solana-sha256-hasher",
|
|
||||||
"spl-discriminator-derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-discriminator-derive"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"spl-discriminator-syn",
|
|
||||||
"syn 2.0.101",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-discriminator-syn"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5d1dbc82ab91422345b6df40a79e2b78c7bce1ebb366da323572dd60b7076b67"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"sha2 0.10.9",
|
|
||||||
"syn 2.0.101",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-elgamal-registry"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce0f668975d2b0536e8a8fd60e56a05c467f06021dae037f1d0cfed0de2e231d"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"solana-program",
|
|
||||||
"solana-zk-sdk",
|
|
||||||
"spl-pod",
|
|
||||||
"spl-token-confidential-transfer-proof-extraction",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-memo"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9f09647c0974e33366efeb83b8e2daebb329f0420149e74d3a4bd2c08cf9f7cb"
|
|
||||||
dependencies = [
|
|
||||||
"solana-account-info",
|
|
||||||
"solana-instruction",
|
|
||||||
"solana-msg",
|
|
||||||
"solana-program-entrypoint",
|
|
||||||
"solana-program-error",
|
|
||||||
"solana-pubkey",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-pod"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d994afaf86b779104b4a95ba9ca75b8ced3fdb17ee934e38cb69e72afbe17799"
|
|
||||||
dependencies = [
|
|
||||||
"borsh 1.5.7",
|
|
||||||
"bytemuck",
|
|
||||||
"bytemuck_derive",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"solana-decode-error",
|
|
||||||
"solana-msg",
|
|
||||||
"solana-program-error",
|
|
||||||
"solana-program-option",
|
|
||||||
"solana-pubkey",
|
|
||||||
"solana-zk-sdk",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-program-error"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1"
|
|
||||||
dependencies = [
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"solana-program",
|
|
||||||
"spl-program-error-derive",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-program-error-derive"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"sha2 0.10.9",
|
|
||||||
"syn 2.0.101",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-tlv-account-resolution"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"solana-account-info",
|
|
||||||
"solana-decode-error",
|
|
||||||
"solana-instruction",
|
|
||||||
"solana-msg",
|
|
||||||
"solana-program-error",
|
|
||||||
"solana-pubkey",
|
|
||||||
"spl-discriminator",
|
|
||||||
"spl-pod",
|
|
||||||
"spl-program-error",
|
|
||||||
"spl-type-length-value",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-token"
|
|
||||||
version = "4.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5e9e171cbcb4b1f72f6d78ed1e975cb467f56825c27d09b8dd2608e4e7fc8b3b"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"bytemuck",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"num_enum",
|
|
||||||
"solana-program",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-token"
|
|
||||||
version = "7.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"bytemuck",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"num_enum",
|
|
||||||
"solana-program",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-token-2022"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"bytemuck",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"num_enum",
|
|
||||||
"solana-program",
|
|
||||||
"solana-security-txt",
|
|
||||||
"solana-zk-sdk",
|
|
||||||
"spl-elgamal-registry",
|
|
||||||
"spl-memo",
|
|
||||||
"spl-pod",
|
|
||||||
"spl-token 7.0.0",
|
|
||||||
"spl-token-confidential-transfer-ciphertext-arithmetic",
|
|
||||||
"spl-token-confidential-transfer-proof-extraction",
|
|
||||||
"spl-token-confidential-transfer-proof-generation",
|
|
||||||
"spl-token-group-interface",
|
|
||||||
"spl-token-metadata-interface",
|
|
||||||
"spl-transfer-hook-interface",
|
|
||||||
"spl-type-length-value",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-token-confidential-transfer-ciphertext-arithmetic"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "170378693c5516090f6d37ae9bad2b9b6125069be68d9acd4865bbe9fc8499fd"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.22.1",
|
|
||||||
"bytemuck",
|
|
||||||
"solana-curve25519",
|
|
||||||
"solana-zk-sdk",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-token-confidential-transfer-proof-extraction"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"solana-curve25519",
|
|
||||||
"solana-program",
|
|
||||||
"solana-zk-sdk",
|
|
||||||
"spl-pod",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-token-confidential-transfer-proof-generation"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8627184782eec1894de8ea26129c61303f1f0adeed65c20e0b10bc584f09356d"
|
|
||||||
dependencies = [
|
|
||||||
"curve25519-dalek 4.1.3",
|
|
||||||
"solana-zk-sdk",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-token-group-interface"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"solana-decode-error",
|
|
||||||
"solana-instruction",
|
|
||||||
"solana-msg",
|
|
||||||
"solana-program-error",
|
|
||||||
"solana-pubkey",
|
|
||||||
"spl-discriminator",
|
|
||||||
"spl-pod",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-token-metadata-interface"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb"
|
|
||||||
dependencies = [
|
|
||||||
"borsh 1.5.7",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"solana-borsh",
|
|
||||||
"solana-decode-error",
|
|
||||||
"solana-instruction",
|
|
||||||
"solana-msg",
|
|
||||||
"solana-program-error",
|
|
||||||
"solana-pubkey",
|
|
||||||
"spl-discriminator",
|
|
||||||
"spl-pod",
|
|
||||||
"spl-type-length-value",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-transfer-hook-interface"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"bytemuck",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"solana-account-info",
|
|
||||||
"solana-cpi",
|
|
||||||
"solana-decode-error",
|
|
||||||
"solana-instruction",
|
|
||||||
"solana-msg",
|
|
||||||
"solana-program-error",
|
|
||||||
"solana-pubkey",
|
|
||||||
"spl-discriminator",
|
|
||||||
"spl-pod",
|
|
||||||
"spl-program-error",
|
|
||||||
"spl-tlv-account-resolution",
|
|
||||||
"spl-type-length-value",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spl-type-length-value"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"num-derive 0.4.2",
|
|
||||||
"num-traits",
|
|
||||||
"solana-account-info",
|
|
||||||
"solana-decode-error",
|
|
||||||
"solana-msg",
|
|
||||||
"solana-program-error",
|
|
||||||
"spl-discriminator",
|
|
||||||
"spl-pod",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@ -2807,26 +2080,6 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "universal-hash"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-common",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "uriparse"
|
|
||||||
version = "0.6.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff"
|
|
||||||
dependencies = [
|
|
||||||
"fnv",
|
|
||||||
"lazy_static",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@ -3011,17 +2264,3 @@ name = "zeroize"
|
|||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||||
dependencies = [
|
|
||||||
"zeroize_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zeroize_derive"
|
|
||||||
version = "1.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.101",
|
|
||||||
]
|
|
||||||
|
|||||||
73
shine/doc/SHINE_PAYMENTS_V2.md
Normal file
73
shine/doc/SHINE_PAYMENTS_V2.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# SHINE Payments v2 (краткое описание)
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
`shine_payments` v2 — контракт очереди выплат:
|
||||||
|
|
||||||
|
1. Покупка билета в очередь 1:
|
||||||
|
- пользователь переводит SOL в DAO;
|
||||||
|
- получает тикет с суммой будущей выплаты `input * coef`.
|
||||||
|
2. Шаг выплат:
|
||||||
|
- из inflow-вольта платится следующий тикет;
|
||||||
|
- такая же сумма отправляется в DAO;
|
||||||
|
- фиксированная награда отправляется вызвавшему шаг.
|
||||||
|
|
||||||
|
## PDA
|
||||||
|
|
||||||
|
1. `config_pda` (`shine_payments_v2_config`)
|
||||||
|
- `dao_wallet`
|
||||||
|
- `manager_wallet`
|
||||||
|
- `inflow_vault`
|
||||||
|
- `call_reward_lamports`
|
||||||
|
|
||||||
|
2. `coef_limit_pda` (`shine_payments_v2_coef_limit`)
|
||||||
|
- `coef_ppm` (коэффициент в fixed-point, scale = 1_000_000)
|
||||||
|
- `limit_lamports`
|
||||||
|
|
||||||
|
3. `queues_pda` (`shine_payments_v2_queues`)
|
||||||
|
- очередь 1: `tickets_total`, `tickets_paid`, `sum_total`, `sum_paid`
|
||||||
|
- очередь 2: `tickets_total`, `tickets_paid`, `sum_total`, `sum_paid`
|
||||||
|
|
||||||
|
4. `inflow_vault_pda` (`shine_payments_v2_inflow_vault`)
|
||||||
|
- PDA-вольт программы для выплат.
|
||||||
|
|
||||||
|
5. `q1_ticket_pda` (`shine_payments_v2_q1_ticket + index_le_u64`)
|
||||||
|
- `is_paid`
|
||||||
|
- `recipient_wallet`
|
||||||
|
- `payout_lamports`
|
||||||
|
- `debt_before_lamports`
|
||||||
|
- `index`
|
||||||
|
|
||||||
|
## Методы
|
||||||
|
|
||||||
|
1. `init`
|
||||||
|
- вызывается один раз (кто угодно);
|
||||||
|
- создает `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`.
|
||||||
|
|
||||||
|
2. `update_coef_limit` (только manager)
|
||||||
|
- меняет коэффициент и лимит.
|
||||||
|
|
||||||
|
3. `buy_ticket`
|
||||||
|
- проверяет, что текущий долг очереди 1 меньше лимита;
|
||||||
|
- переводит входную сумму в DAO;
|
||||||
|
- создает тикет в очереди 1;
|
||||||
|
- увеличивает агрегаты очереди.
|
||||||
|
|
||||||
|
4. `step_payout`
|
||||||
|
- если есть тикеты в очереди 1:
|
||||||
|
- платит `X` получателю тикета,
|
||||||
|
- платит `X` в DAO,
|
||||||
|
- платит reward вызвавшему;
|
||||||
|
- помечает тикет выплаченным, обновляет агрегаты.
|
||||||
|
- если обе очереди пусты/полностью выплачены:
|
||||||
|
- переводит из inflow-вольта в DAO весь доступный остаток (сверх ренты),
|
||||||
|
- reward не платится.
|
||||||
|
|
||||||
|
## Стартовые настройки
|
||||||
|
|
||||||
|
См. `programs/shine_payments/src/settings.rs`:
|
||||||
|
|
||||||
|
- `START_COEF_PPM = 5_000_000` (коэффициент 5.0)
|
||||||
|
- `START_LIMIT_LAMPORTS = 100 SOL`
|
||||||
|
- `START_CALL_REWARD_LAMPORTS = 0.008 SOL`
|
||||||
|
- `DAO_WALLET`, `MANAGER_WALLET`
|
||||||
@ -71,6 +71,8 @@
|
|||||||
3. `signature = Ed25519.sign(root_private_key, msg_hash)`.
|
3. `signature = Ed25519.sign(root_private_key, msg_hash)`.
|
||||||
4. Проверка:
|
4. Проверка:
|
||||||
- `Ed25519.verify(root_key, msg_hash, signature)`.
|
- `Ed25519.verify(root_key, msg_hash, signature)`.
|
||||||
|
- В текущей реализации проверка выполняется через встроенную Solana-инструкцию `Ed25519Program`
|
||||||
|
(инструкция должна идти в транзакции перед вызовом `create_user_pda` / `update_user_pda`).
|
||||||
|
|
||||||
## 5. Что входит в `prev_hash`
|
## 5. Что входит в `prev_hash`
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "shine_payments"
|
name = "shine_payments"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Payments and investments smart contract"
|
description = "Shine Payments v2 (очереди выплат)"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@ -15,12 +15,6 @@ bench = false
|
|||||||
anchor-lang = "0.31.1"
|
anchor-lang = "0.31.1"
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|
||||||
# ==== добавлено для NFT-функционала ====
|
|
||||||
anchor-spl = { version = "0.31.1", features = ["associated_token", "token"] }
|
|
||||||
mpl-token-metadata = "5.1.1"
|
|
||||||
spl-token = { version = "4.0.0", features = ["no-entrypoint"] }
|
|
||||||
# ======================================
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
no-entrypoint = []
|
no-entrypoint = []
|
||||||
|
|||||||
@ -1,158 +1,605 @@
|
|||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
use anchor_lang::solana_program::{program::invoke, system_instruction};
|
||||||
|
use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
declare_id!("6Hes38UKFGF8cfQDQDVWoMGcSzGMUAgamWG31hCVhyPY");
|
pub mod settings;
|
||||||
|
|
||||||
|
declare_id!("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE");
|
||||||
/// Подключаем модуль с полной реализацией.
|
|
||||||
pub mod investments;
|
|
||||||
use investments::*; // импортируем всё в корень
|
|
||||||
|
|
||||||
// === модуль NFT ===
|
|
||||||
pub mod nft;
|
|
||||||
|
|
||||||
// ==============================================
|
|
||||||
// Константы формата / сидов / размеров
|
|
||||||
// ==============================================
|
|
||||||
|
|
||||||
/// Префикс (seed) для PDA, где храним глобальное состояние выплат.
|
|
||||||
/// Важно: сид — это просто набор байт; здесь он фиксированный.
|
|
||||||
pub const PDA_SEED_PREFIX: &[u8] = b"shine_investments_state";
|
|
||||||
|
|
||||||
/// Значение коэффициента «по умолчанию» при инициализации.
|
|
||||||
pub const DEFAULT_COEF: u32 = 10; // ← «коэффициент» = 10 при init
|
|
||||||
|
|
||||||
/// Ровно столько байт резервируем под PDA-данные.
|
|
||||||
/// (Можно добавить запас на будущее, но по заданию — только 28.)
|
|
||||||
pub const PAY_STATE_SPACE: u64 = 50; // просто сделал с запасом
|
|
||||||
|
|
||||||
// ==============================================
|
|
||||||
// Программа
|
|
||||||
// ==============================================
|
|
||||||
|
|
||||||
#[program]
|
#[program]
|
||||||
pub mod shine_payments {
|
pub mod shine_payments {
|
||||||
use super::*;
|
use super::*;
|
||||||
// Явно подтягиваем типы и функции, чтобы не было путаницы после предыдущих ошибок парсера
|
|
||||||
use crate::investments::{Init, UseState};
|
|
||||||
use crate::investments::{
|
|
||||||
add_bonus as inv_add_bonus, claim as inv_claim, init as inv_init, invest as inv_invest,
|
|
||||||
ErrCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// init — создаёт PDA и кладёт дефолтное состояние.
|
|
||||||
pub fn init(ctx: Context<Init>) -> Result<()> {
|
pub fn init(ctx: Context<Init>) -> Result<()> {
|
||||||
inv_init(ctx)
|
ensure_expected_pdas(ctx.program_id, &ctx.accounts)?;
|
||||||
|
require!(
|
||||||
|
ctx.accounts.config_pda.owner == &Pubkey::default(),
|
||||||
|
ErrCode::SystemAlreadyInitialized
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
ctx.accounts.coef_limit_pda.owner == &Pubkey::default(),
|
||||||
|
ErrCode::SystemAlreadyInitialized
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
ctx.accounts.queues_pda.owner == &Pubkey::default(),
|
||||||
|
ErrCode::SystemAlreadyInitialized
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
ctx.accounts.inflow_vault_pda.owner == &Pubkey::default(),
|
||||||
|
ErrCode::SystemAlreadyInitialized
|
||||||
|
);
|
||||||
|
|
||||||
|
let dao_wallet = Pubkey::from_str(settings::DAO_WALLET)
|
||||||
|
.map_err(|_| error!(PaymentsError::InvalidSettingsWallet))?;
|
||||||
|
let manager_wallet = Pubkey::from_str(settings::MANAGER_WALLET)
|
||||||
|
.map_err(|_| error!(PaymentsError::InvalidSettingsWallet))?;
|
||||||
|
|
||||||
|
let system_program_ai = ctx.accounts.system_program.to_account_info();
|
||||||
|
|
||||||
|
let config = ConfigState {
|
||||||
|
version: 1,
|
||||||
|
dao_wallet,
|
||||||
|
manager_wallet,
|
||||||
|
inflow_vault: ctx.accounts.inflow_vault_pda.key(),
|
||||||
|
call_reward_lamports: settings::START_CALL_REWARD_LAMPORTS,
|
||||||
|
};
|
||||||
|
create_and_store_state(
|
||||||
|
ctx.program_id,
|
||||||
|
&ctx.accounts.payer,
|
||||||
|
&system_program_ai,
|
||||||
|
&ctx.accounts.config_pda,
|
||||||
|
settings::CONFIG_SEED,
|
||||||
|
settings::CONFIG_SPACE,
|
||||||
|
&config,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let coef_limit = CoefLimitState {
|
||||||
|
version: 1,
|
||||||
|
coef_ppm: settings::START_COEF_PPM,
|
||||||
|
limit_lamports: settings::START_LIMIT_LAMPORTS,
|
||||||
|
};
|
||||||
|
create_and_store_state(
|
||||||
|
ctx.program_id,
|
||||||
|
&ctx.accounts.payer,
|
||||||
|
&system_program_ai,
|
||||||
|
&ctx.accounts.coef_limit_pda,
|
||||||
|
settings::COEF_LIMIT_SEED,
|
||||||
|
settings::COEF_LIMIT_SPACE,
|
||||||
|
&coef_limit,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let queues = QueuesState {
|
||||||
|
version: 1,
|
||||||
|
q1_tickets_total: 0,
|
||||||
|
q1_tickets_paid: 0,
|
||||||
|
q1_sum_total: 0,
|
||||||
|
q1_sum_paid: 0,
|
||||||
|
q2_tickets_total: 0,
|
||||||
|
q2_tickets_paid: 0,
|
||||||
|
q2_sum_total: 0,
|
||||||
|
q2_sum_paid: 0,
|
||||||
|
};
|
||||||
|
create_and_store_state(
|
||||||
|
ctx.program_id,
|
||||||
|
&ctx.accounts.payer,
|
||||||
|
&system_program_ai,
|
||||||
|
&ctx.accounts.queues_pda,
|
||||||
|
settings::QUEUES_SEED,
|
||||||
|
settings::QUEUES_SPACE,
|
||||||
|
&queues,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let vault = VaultState { version: 1 };
|
||||||
|
create_and_store_state(
|
||||||
|
ctx.program_id,
|
||||||
|
&ctx.accounts.payer,
|
||||||
|
&system_program_ai,
|
||||||
|
&ctx.accounts.inflow_vault_pda,
|
||||||
|
settings::INFLOW_VAULT_SEED,
|
||||||
|
settings::INFLOW_VAULT_SPACE,
|
||||||
|
&vault,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// invest — в начале читает состояние, в конце сохраняет (логика внутри модуля).
|
pub fn update_coef_limit(ctx: Context<UpdateCoefLimit>, args: UpdateCoefLimitArgs) -> Result<()> {
|
||||||
pub fn invest(ctx: Context<UseState>, amount: u64) -> Result<()> {
|
let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?;
|
||||||
inv_invest(ctx, amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// add_bonus — начисление бонусов (обычно от DAO).
|
|
||||||
/// Для NFT используем расширенный контекст AddBonusCtx (с аккаунтами коллекции и т.п.).
|
|
||||||
pub fn add_bonus(ctx: Context<AddBonusCtx>, investor: Pubkey, amount: u64) -> Result<()> {
|
|
||||||
inv_add_bonus(ctx, investor, amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// claim — выплата.
|
|
||||||
pub fn claim(ctx: Context<UseState>) -> Result<()> {
|
|
||||||
inv_claim(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ВРЕМЕННАЯ ФУНКЦИЯ только для тестов (в итоговой версии её не будет):
|
|
||||||
/// deleteInit — удалить PDA из init и вернуть ренту подписанту.
|
|
||||||
pub fn delete_init(ctx: Context<DeleteInit>) -> Result<()> {
|
|
||||||
let program_id = ctx.program_id;
|
|
||||||
|
|
||||||
// PDA по тем же сид/бамп, что и в init
|
|
||||||
let (expected_pda, _bump) = Pubkey::find_program_address(&[PDA_SEED_PREFIX], program_id);
|
|
||||||
require_keys_eq!(
|
require_keys_eq!(
|
||||||
expected_pda,
|
config.manager_wallet,
|
||||||
ctx.accounts.state_pda.key(),
|
ctx.accounts.signer.key(),
|
||||||
|
PaymentsError::UnauthorizedManager
|
||||||
|
);
|
||||||
|
require!(args.coef_ppm > 0, PaymentsError::InvalidCoefficient);
|
||||||
|
require!(args.limit_lamports > 0, PaymentsError::InvalidLimit);
|
||||||
|
|
||||||
|
let mut coef_limit = read_state::<CoefLimitState>(&ctx.accounts.coef_limit_pda)?;
|
||||||
|
coef_limit.coef_ppm = args.coef_ppm;
|
||||||
|
coef_limit.limit_lamports = args.limit_lamports;
|
||||||
|
write_state(&ctx.accounts.coef_limit_pda, &coef_limit)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buy_ticket(ctx: Context<BuyTicket>, args: BuyTicketArgs) -> Result<()> {
|
||||||
|
require!(args.amount_lamports > 0, PaymentsError::InvalidAmount);
|
||||||
|
let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?;
|
||||||
|
let coef_limit = read_state::<CoefLimitState>(&ctx.accounts.coef_limit_pda)?;
|
||||||
|
let mut queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?;
|
||||||
|
|
||||||
|
require_keys_eq!(
|
||||||
|
ctx.accounts.dao_wallet.key(),
|
||||||
|
config.dao_wallet,
|
||||||
|
PaymentsError::InvalidDaoWallet
|
||||||
|
);
|
||||||
|
|
||||||
|
let current_debt = queues
|
||||||
|
.q1_sum_total
|
||||||
|
.checked_sub(queues.q1_sum_paid)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
require!(
|
||||||
|
current_debt < coef_limit.limit_lamports,
|
||||||
|
PaymentsError::QueueTemporarilyPaused
|
||||||
|
);
|
||||||
|
|
||||||
|
let ticket_index = queues
|
||||||
|
.q1_tickets_total
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
let (expected_ticket_pda, ticket_bump) = find_ticket_pda(ctx.program_id, 1, ticket_index);
|
||||||
|
require_keys_eq!(
|
||||||
|
expected_ticket_pda,
|
||||||
|
ctx.accounts.ticket_pda.key(),
|
||||||
|
ErrCode::InvalidPdaAddress
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
ctx.accounts.ticket_pda.owner == &Pubkey::default(),
|
||||||
|
ErrCode::PdaAlreadyExists
|
||||||
|
);
|
||||||
|
|
||||||
|
let payout_lamports = args
|
||||||
|
.amount_lamports
|
||||||
|
.checked_mul(coef_limit.coef_ppm)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?
|
||||||
|
/ settings::COEF_SCALE_PPM;
|
||||||
|
require!(payout_lamports > 0, PaymentsError::InvalidPayoutAmount);
|
||||||
|
|
||||||
|
let ix = system_instruction::transfer(
|
||||||
|
ctx.accounts.signer.key,
|
||||||
|
ctx.accounts.dao_wallet.key,
|
||||||
|
args.amount_lamports,
|
||||||
|
);
|
||||||
|
invoke(
|
||||||
|
&ix,
|
||||||
|
&[
|
||||||
|
ctx.accounts.signer.clone(),
|
||||||
|
ctx.accounts.dao_wallet.clone(),
|
||||||
|
ctx.accounts.system_program.to_account_info(),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let ticket = TicketState {
|
||||||
|
version: 1,
|
||||||
|
queue_id: 1,
|
||||||
|
index: ticket_index,
|
||||||
|
is_paid: false,
|
||||||
|
recipient_wallet: args.recipient_wallet,
|
||||||
|
payout_lamports,
|
||||||
|
debt_before_lamports: current_debt,
|
||||||
|
};
|
||||||
|
create_state_with_seeds(
|
||||||
|
ctx.program_id,
|
||||||
|
&ctx.accounts.signer,
|
||||||
|
&ctx.accounts.system_program.to_account_info(),
|
||||||
|
&ctx.accounts.ticket_pda,
|
||||||
|
&[
|
||||||
|
settings::Q1_TICKET_SEED,
|
||||||
|
&ticket_index.to_le_bytes(),
|
||||||
|
&[ticket_bump],
|
||||||
|
],
|
||||||
|
settings::TICKET_SPACE,
|
||||||
|
&ticket,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
queues.q1_tickets_total = ticket_index;
|
||||||
|
queues.q1_sum_total = queues
|
||||||
|
.q1_sum_total
|
||||||
|
.checked_add(payout_lamports)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
write_state(&ctx.accounts.queues_pda, &queues)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step_payout(ctx: Context<StepPayout>) -> Result<()> {
|
||||||
|
let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?;
|
||||||
|
let mut queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?;
|
||||||
|
let _vault_state = read_state::<VaultState>(&ctx.accounts.inflow_vault_pda)?;
|
||||||
|
|
||||||
|
require_keys_eq!(
|
||||||
|
ctx.accounts.dao_wallet.key(),
|
||||||
|
config.dao_wallet,
|
||||||
|
PaymentsError::InvalidDaoWallet
|
||||||
|
);
|
||||||
|
require_keys_eq!(
|
||||||
|
ctx.accounts.inflow_vault_pda.key(),
|
||||||
|
config.inflow_vault,
|
||||||
|
PaymentsError::InvalidInflowVault
|
||||||
|
);
|
||||||
|
|
||||||
|
let q1_pending = queues
|
||||||
|
.q1_tickets_total
|
||||||
|
.checked_sub(queues.q1_tickets_paid)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
let q2_pending = queues
|
||||||
|
.q2_tickets_total
|
||||||
|
.checked_sub(queues.q2_tickets_paid)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
|
||||||
|
if q1_pending == 0 {
|
||||||
|
if q2_pending > 0 {
|
||||||
|
return Err(error!(PaymentsError::SecondQueueNotImplemented));
|
||||||
|
}
|
||||||
|
transfer_all_available_to_dao(
|
||||||
|
&ctx.accounts.inflow_vault_pda,
|
||||||
|
&ctx.accounts.dao_wallet,
|
||||||
|
)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_index = queues
|
||||||
|
.q1_tickets_paid
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
let (expected_ticket_pda, _) = find_ticket_pda(ctx.program_id, 1, next_index);
|
||||||
|
require_keys_eq!(
|
||||||
|
expected_ticket_pda,
|
||||||
|
ctx.accounts.next_ticket_pda.key(),
|
||||||
ErrCode::InvalidPdaAddress
|
ErrCode::InvalidPdaAddress
|
||||||
);
|
);
|
||||||
|
|
||||||
// Рента уйдёт на счёт подписанта (signer)
|
let mut ticket = read_state::<TicketState>(&ctx.accounts.next_ticket_pda)?;
|
||||||
common::utils::delete_pda_return_rent(
|
require!(ticket.queue_id == 1, PaymentsError::InvalidTicketQueue);
|
||||||
&ctx.accounts.state_pda.to_account_info(),
|
require!(ticket.index == next_index, PaymentsError::InvalidTicketIndex);
|
||||||
&ctx.accounts.signer.to_account_info(),
|
require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid);
|
||||||
program_id,
|
require_keys_eq!(
|
||||||
)
|
ctx.accounts.ticket_recipient_wallet.key(),
|
||||||
|
ticket.recipient_wallet,
|
||||||
|
PaymentsError::InvalidTicketRecipient
|
||||||
|
);
|
||||||
|
|
||||||
|
let needed = ticket
|
||||||
|
.payout_lamports
|
||||||
|
.checked_add(ticket.payout_lamports)
|
||||||
|
.and_then(|v| v.checked_add(config.call_reward_lamports))
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
require!(
|
||||||
|
available_vault_lamports(&ctx.accounts.inflow_vault_pda)? >= needed,
|
||||||
|
PaymentsError::NotEnoughInflowForStep
|
||||||
|
);
|
||||||
|
|
||||||
|
transfer_from_vault(
|
||||||
|
&ctx.accounts.inflow_vault_pda,
|
||||||
|
&ctx.accounts.ticket_recipient_wallet,
|
||||||
|
ticket.payout_lamports,
|
||||||
|
)?;
|
||||||
|
transfer_from_vault(
|
||||||
|
&ctx.accounts.inflow_vault_pda,
|
||||||
|
&ctx.accounts.dao_wallet,
|
||||||
|
ticket.payout_lamports,
|
||||||
|
)?;
|
||||||
|
transfer_from_vault(
|
||||||
|
&ctx.accounts.inflow_vault_pda,
|
||||||
|
&ctx.accounts.signer,
|
||||||
|
config.call_reward_lamports,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
ticket.is_paid = true;
|
||||||
|
write_state(&ctx.accounts.next_ticket_pda, &ticket)?;
|
||||||
|
|
||||||
|
queues.q1_tickets_paid = queues
|
||||||
|
.q1_tickets_paid
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
queues.q1_sum_paid = queues
|
||||||
|
.q1_sum_paid
|
||||||
|
.checked_add(ticket.payout_lamports)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
write_state(&ctx.accounts.queues_pda, &queues)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============================================
|
|
||||||
// Контексты вне #[program]
|
|
||||||
// ==============================================
|
|
||||||
|
|
||||||
/// Контекст для deleteInit (временный для тестов)
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct DeleteInit<'info> {
|
pub struct Init<'info> {
|
||||||
/// Подписант транзакции — ПОЛУЧАТЕЛЬ ренты
|
/// CHECK: подписант и плательщик, проверяется атрибутом `signer`.
|
||||||
|
#[account(mut, signer)]
|
||||||
|
pub payer: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA конфига, адрес проверяется вручную в `ensure_expected_pdas`.
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub signer: Signer<'info>,
|
pub config_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA коэффициента/лимита, адрес проверяется вручную в `ensure_expected_pdas`.
|
||||||
/// Тот самый PDA из init
|
|
||||||
/// CHECK: адрес валидируем в хендлере по сид-у
|
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub state_pda: UncheckedAccount<'info>,
|
pub coef_limit_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA состояния очередей, адрес проверяется вручную в `ensure_expected_pdas`.
|
||||||
/// Системная программа
|
#[account(mut)]
|
||||||
|
pub queues_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA inflow-вольта, адрес проверяется вручную в `ensure_expected_pdas`.
|
||||||
|
#[account(mut)]
|
||||||
|
pub inflow_vault_pda: AccountInfo<'info>,
|
||||||
pub system_program: Program<'info, System>,
|
pub system_program: Program<'info, System>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Контекст для add_bonus: полный набор аккаунтов для операций с NFT и коллекцией.
|
|
||||||
/// (Комменты по стилю проекта оставлены.)
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct AddBonusCtx<'info> {
|
pub struct UpdateCoefLimit<'info> {
|
||||||
/// Любой платящий/подписант (в реальном коде — свои проверки).
|
/// CHECK: подписант-менеджер, проверяется атрибутом `signer` и сверкой адреса в коде.
|
||||||
|
#[account(mut, signer)]
|
||||||
|
pub signer: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA конфига, читается и валидируется вручную.
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub signer: Signer<'info>,
|
pub config_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA коэффициента/лимита, читается и валидируется вручную.
|
||||||
|
#[account(mut)]
|
||||||
|
pub coef_limit_pda: AccountInfo<'info>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Тот же PDA с состоянием (должен уже существовать).
|
#[derive(Accounts)]
|
||||||
/// CHECK: проверяется вручную по адресу
|
pub struct BuyTicket<'info> {
|
||||||
|
/// CHECK: подписант-покупатель, проверяется атрибутом `signer`.
|
||||||
|
#[account(mut, signer)]
|
||||||
|
pub signer: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA конфига, читается и валидируется вручную.
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub state_pda: UncheckedAccount<'info>,
|
pub config_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA коэффициента/лимита, читается и валидируется вручную.
|
||||||
// --- аккаунты минтимого NFT ---
|
|
||||||
/// Mint создаваемого NFT (должен быть создан заранее: decimals=0, mint_authority=signer, freeze_authority=signer)
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub mint_pda: UncheckedAccount<'info>,
|
pub coef_limit_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA очередей, читается и валидируется вручную.
|
||||||
/// ATA получателя (может быть предсоздан тестом)
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub recipient_ata: UncheckedAccount<'info>,
|
pub queues_pda: AccountInfo<'info>,
|
||||||
/// Владелец ATA (инвестор)
|
/// CHECK: PDA тикета, адрес и состояние (должен быть пустым) проверяются вручную.
|
||||||
/// CHECK
|
|
||||||
pub recipient_owner: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
// --- аккаунты коллекции (уже созданной заранее) ---
|
|
||||||
/// CHECK
|
|
||||||
pub collection_mint: UncheckedAccount<'info>,
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub collection_metadata_pda: UncheckedAccount<'info>,
|
pub ticket_pda: AccountInfo<'info>,
|
||||||
/// CHECK
|
/// CHECK: DAO-кошелек, адрес сверяется с конфигом вручную.
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub collection_master_edition_pda: UncheckedAccount<'info>,
|
pub dao_wallet: AccountInfo<'info>,
|
||||||
/// Апдейтер коллекции (update authority)
|
|
||||||
pub collection_update_authority: Signer<'info>,
|
|
||||||
|
|
||||||
// --- metadata + master edition для создаваемого NFT ---
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
|
||||||
pub metadata_pda: UncheckedAccount<'info>,
|
|
||||||
/// CHECK
|
|
||||||
#[account(mut)]
|
|
||||||
pub master_edition_pda: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
// --- программы ---
|
|
||||||
/// CHECK: проверяется по ID внутри nft.rs
|
|
||||||
pub token_metadata_program: UncheckedAccount<'info>,
|
|
||||||
pub token_program: Program<'info, anchor_spl::token::Token>,
|
|
||||||
pub associated_token_program: Program<'info, anchor_spl::associated_token::AssociatedToken>,
|
|
||||||
pub system_program: Program<'info, System>,
|
pub system_program: Program<'info, System>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct StepPayout<'info> {
|
||||||
|
/// CHECK: подписант-вызвавший шаг выплат, проверяется атрибутом `signer`.
|
||||||
|
#[account(mut, signer)]
|
||||||
|
pub signer: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA конфига, читается и валидируется вручную.
|
||||||
|
#[account(mut)]
|
||||||
|
pub config_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA очередей, читается и валидируется вручную.
|
||||||
|
#[account(mut)]
|
||||||
|
pub queues_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA inflow-вольта, адрес сверяется с конфигом вручную.
|
||||||
|
#[account(mut)]
|
||||||
|
pub inflow_vault_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: PDA следующего тикета, адрес и содержимое валидируются вручную.
|
||||||
|
#[account(mut)]
|
||||||
|
pub next_ticket_pda: AccountInfo<'info>,
|
||||||
|
/// CHECK: кошелек получателя тикета, адрес сверяется с полем тикета вручную.
|
||||||
|
#[account(mut)]
|
||||||
|
pub ticket_recipient_wallet: AccountInfo<'info>,
|
||||||
|
/// CHECK: DAO-кошелек, адрес сверяется с конфигом вручную.
|
||||||
|
#[account(mut)]
|
||||||
|
pub dao_wallet: AccountInfo<'info>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||||
|
pub struct UpdateCoefLimitArgs {
|
||||||
|
pub coef_ppm: u64,
|
||||||
|
pub limit_lamports: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||||
|
pub struct BuyTicketArgs {
|
||||||
|
pub amount_lamports: u64,
|
||||||
|
pub recipient_wallet: Pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||||
|
pub struct ConfigState {
|
||||||
|
pub version: u8,
|
||||||
|
pub dao_wallet: Pubkey,
|
||||||
|
pub manager_wallet: Pubkey,
|
||||||
|
pub inflow_vault: Pubkey,
|
||||||
|
pub call_reward_lamports: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||||
|
pub struct CoefLimitState {
|
||||||
|
pub version: u8,
|
||||||
|
pub coef_ppm: u64,
|
||||||
|
pub limit_lamports: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||||
|
pub struct QueuesState {
|
||||||
|
pub version: u8,
|
||||||
|
pub q1_tickets_total: u64,
|
||||||
|
pub q1_tickets_paid: u64,
|
||||||
|
pub q1_sum_total: u64,
|
||||||
|
pub q1_sum_paid: u64,
|
||||||
|
pub q2_tickets_total: u64,
|
||||||
|
pub q2_tickets_paid: u64,
|
||||||
|
pub q2_sum_total: u64,
|
||||||
|
pub q2_sum_paid: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||||
|
pub struct TicketState {
|
||||||
|
pub version: u8,
|
||||||
|
pub queue_id: u8,
|
||||||
|
pub index: u64,
|
||||||
|
pub is_paid: bool,
|
||||||
|
pub recipient_wallet: Pubkey,
|
||||||
|
pub payout_lamports: u64,
|
||||||
|
pub debt_before_lamports: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||||
|
pub struct VaultState {
|
||||||
|
pub version: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[error_code]
|
||||||
|
pub enum PaymentsError {
|
||||||
|
#[msg("Ошибка в адресах кошельков из настроек программы")]
|
||||||
|
InvalidSettingsWallet,
|
||||||
|
#[msg("Недостаточно данных PDA")]
|
||||||
|
EmptyState,
|
||||||
|
#[msg("Неверный inflow vault")]
|
||||||
|
InvalidInflowVault,
|
||||||
|
#[msg("Неверный DAO кошелек")]
|
||||||
|
InvalidDaoWallet,
|
||||||
|
#[msg("Управляющий кошелек не авторизован")]
|
||||||
|
UnauthorizedManager,
|
||||||
|
#[msg("Некорректный коэффициент")]
|
||||||
|
InvalidCoefficient,
|
||||||
|
#[msg("Некорректный лимит")]
|
||||||
|
InvalidLimit,
|
||||||
|
#[msg("Некорректная сумма")]
|
||||||
|
InvalidAmount,
|
||||||
|
#[msg("Очередь временно приостановлена: достигнут лимит")]
|
||||||
|
QueueTemporarilyPaused,
|
||||||
|
#[msg("Некорректная сумма выплаты")]
|
||||||
|
InvalidPayoutAmount,
|
||||||
|
#[msg("Недостаточно средств на inflow vault для шага выплаты")]
|
||||||
|
NotEnoughInflowForStep,
|
||||||
|
#[msg("Тикет уже выплачен")]
|
||||||
|
TicketAlreadyPaid,
|
||||||
|
#[msg("Неверный получатель тикета")]
|
||||||
|
InvalidTicketRecipient,
|
||||||
|
#[msg("Неверный номер тикета")]
|
||||||
|
InvalidTicketIndex,
|
||||||
|
#[msg("Неверный тип очереди у тикета")]
|
||||||
|
InvalidTicketQueue,
|
||||||
|
#[msg("Вторая очередь пока не реализована для выплат")]
|
||||||
|
SecondQueueNotImplemented,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_expected_pdas(program_id: &Pubkey, accounts: &Init) -> Result<()> {
|
||||||
|
let (config, _) = find_single_pda(program_id, settings::CONFIG_SEED);
|
||||||
|
let (coef, _) = find_single_pda(program_id, settings::COEF_LIMIT_SEED);
|
||||||
|
let (queues, _) = find_single_pda(program_id, settings::QUEUES_SEED);
|
||||||
|
let (inflow, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED);
|
||||||
|
require_keys_eq!(config, accounts.config_pda.key(), ErrCode::InvalidPdaAddress);
|
||||||
|
require_keys_eq!(
|
||||||
|
coef,
|
||||||
|
accounts.coef_limit_pda.key(),
|
||||||
|
ErrCode::InvalidPdaAddress
|
||||||
|
);
|
||||||
|
require_keys_eq!(
|
||||||
|
queues,
|
||||||
|
accounts.queues_pda.key(),
|
||||||
|
ErrCode::InvalidPdaAddress
|
||||||
|
);
|
||||||
|
require_keys_eq!(
|
||||||
|
inflow,
|
||||||
|
accounts.inflow_vault_pda.key(),
|
||||||
|
ErrCode::InvalidPdaAddress
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_single_pda(program_id: &Pubkey, seed: &[u8]) -> (Pubkey, u8) {
|
||||||
|
Pubkey::find_program_address(&[seed], program_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_ticket_pda(program_id: &Pubkey, queue_id: u8, index: u64) -> (Pubkey, u8) {
|
||||||
|
let idx = index.to_le_bytes();
|
||||||
|
let seed = if queue_id == 1 {
|
||||||
|
settings::Q1_TICKET_SEED
|
||||||
|
} else {
|
||||||
|
settings::Q2_TICKET_SEED
|
||||||
|
};
|
||||||
|
Pubkey::find_program_address(&[seed, &idx], program_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_and_store_state<'info, T: AnchorSerialize>(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
payer: &AccountInfo<'info>,
|
||||||
|
system_program: &AccountInfo<'info>,
|
||||||
|
pda: &AccountInfo<'info>,
|
||||||
|
seed: &[u8],
|
||||||
|
space: usize,
|
||||||
|
state: &T,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (_, bump) = find_single_pda(program_id, seed);
|
||||||
|
create_state_with_seeds(
|
||||||
|
program_id,
|
||||||
|
payer,
|
||||||
|
system_program,
|
||||||
|
pda,
|
||||||
|
&[seed, &[bump]],
|
||||||
|
space,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_state_with_seeds<'info, T: AnchorSerialize>(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
payer: &AccountInfo<'info>,
|
||||||
|
system_program: &AccountInfo<'info>,
|
||||||
|
pda: &AccountInfo<'info>,
|
||||||
|
seeds: &[&[u8]],
|
||||||
|
space: usize,
|
||||||
|
state: &T,
|
||||||
|
) -> Result<()> {
|
||||||
|
create_pda(
|
||||||
|
pda,
|
||||||
|
payer,
|
||||||
|
system_program,
|
||||||
|
program_id,
|
||||||
|
seeds,
|
||||||
|
space as u64,
|
||||||
|
)?;
|
||||||
|
write_state(pda, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_state<T: AnchorSerialize>(pda: &AccountInfo, state: &T) -> Result<()> {
|
||||||
|
let bytes = state
|
||||||
|
.try_to_vec()
|
||||||
|
.map_err(|_| error!(ErrCode::DeserializationError))?;
|
||||||
|
write_to_pda(pda, &bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_state<T: AnchorDeserialize>(pda: &AccountInfo) -> Result<T> {
|
||||||
|
let raw = safe_read_pda(pda);
|
||||||
|
require!(!raw.is_empty(), PaymentsError::EmptyState);
|
||||||
|
let mut slice: &[u8] = &raw;
|
||||||
|
T::deserialize(&mut slice).map_err(|_| error!(ErrCode::DeserializationError))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn available_vault_lamports(vault: &AccountInfo) -> Result<u64> {
|
||||||
|
let total = vault.lamports();
|
||||||
|
let rent_min = Rent::get()?.minimum_balance(vault.data_len());
|
||||||
|
Ok(total.saturating_sub(rent_min))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer_from_vault(vault: &AccountInfo, recipient: &AccountInfo, amount: u64) -> Result<()> {
|
||||||
|
if amount == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut vault_lamports = vault.try_borrow_mut_lamports()?;
|
||||||
|
let mut recipient_lamports = recipient.try_borrow_mut_lamports()?;
|
||||||
|
require!(**vault_lamports >= amount, PaymentsError::NotEnoughInflowForStep);
|
||||||
|
**vault_lamports = vault_lamports
|
||||||
|
.checked_sub(amount)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
**recipient_lamports = recipient_lamports
|
||||||
|
.checked_add(amount)
|
||||||
|
.ok_or(error!(ErrCode::MathOverflow))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer_all_available_to_dao(vault: &AccountInfo, dao_wallet: &AccountInfo) -> Result<()> {
|
||||||
|
let available = available_vault_lamports(vault)?;
|
||||||
|
transfer_from_vault(vault, dao_wallet, available)
|
||||||
|
}
|
||||||
|
|||||||
20
shine/programs/shine_payments/src/settings.rs
Normal file
20
shine/programs/shine_payments/src/settings.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
pub const CONFIG_SEED: &[u8] = b"shine_payments_v2_config";
|
||||||
|
pub const COEF_LIMIT_SEED: &[u8] = b"shine_payments_v2_coef_limit";
|
||||||
|
pub const QUEUES_SEED: &[u8] = b"shine_payments_v2_queues";
|
||||||
|
pub const INFLOW_VAULT_SEED: &[u8] = b"shine_payments_v2_inflow_vault";
|
||||||
|
pub const Q1_TICKET_SEED: &[u8] = b"shine_payments_v2_q1_ticket";
|
||||||
|
pub const Q2_TICKET_SEED: &[u8] = b"shine_payments_v2_q2_ticket";
|
||||||
|
|
||||||
|
pub const CONFIG_SPACE: usize = 8 + 160;
|
||||||
|
pub const COEF_LIMIT_SPACE: usize = 8 + 96;
|
||||||
|
pub const QUEUES_SPACE: usize = 8 + 192;
|
||||||
|
pub const INFLOW_VAULT_SPACE: usize = 8 + 32;
|
||||||
|
pub const TICKET_SPACE: usize = 8 + 160;
|
||||||
|
|
||||||
|
pub const COEF_SCALE_PPM: u64 = 1_000_000;
|
||||||
|
pub const START_COEF_PPM: u64 = 5_000_000; // 5.0
|
||||||
|
pub const START_LIMIT_LAMPORTS: u64 = 100 * 1_000_000_000; // 100 SOL
|
||||||
|
pub const START_CALL_REWARD_LAMPORTS: u64 = 8_000_000; // 0.008 SOL
|
||||||
|
|
||||||
|
pub const DAO_WALLET: &str = "6bFc5Gz5qF172GQhK5HpDbWs8F6qcSxdHn5XqAstf1fY";
|
||||||
|
pub const MANAGER_WALLET: &str = "4yzHKs2zFXpyqqCETe8KpAs4xhEo4QhJ2ybyTgRZphZv";
|
||||||
385
shine/programs/shine_payments/web/admin_tools.html
Normal file
385
shine/programs/shine_payments/web/admin_tools.html
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Тех. инструменты — Shine Payments Devnet</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; }
|
||||||
|
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; }
|
||||||
|
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
|
||||||
|
input { padding: 8px; min-width: 180px; }
|
||||||
|
button { padding: 8px 12px; cursor: pointer; }
|
||||||
|
.muted { color: #666; }
|
||||||
|
.ok { color: #0a7a3c; }
|
||||||
|
.warn { color: #9f5f00; }
|
||||||
|
.err { color: #b30000; white-space: pre-wrap; }
|
||||||
|
.paid { color: #0a7a3c; font-weight: 700; }
|
||||||
|
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; }
|
||||||
|
table { border-collapse: collapse; width: 100%; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 6px; text-align: left; font-size: 14px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Техническая страница (Devnet)</h1>
|
||||||
|
<div class="muted">Программа: <code id="programId"></code></div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="row">
|
||||||
|
<button id="connectBtn">Подключить кошелек</button>
|
||||||
|
<button id="refreshBtn">Обновить всё</button>
|
||||||
|
<button id="initBtn">Init (один раз)</button>
|
||||||
|
</div>
|
||||||
|
<div id="walletInfo" class="muted"></div>
|
||||||
|
<div id="initResult" class="muted"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h3>Коэффициент и лимит</h3>
|
||||||
|
<div class="muted">Право изменения: <code id="managerAllowed">загрузка...</code></div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Коэффициент (например 5): <input id="coefInput" value="5" /></label>
|
||||||
|
<label>Лимит (SOL): <input id="limitInput" value="100" /></label>
|
||||||
|
<button id="updateCoefBtn">Обновить</button>
|
||||||
|
</div>
|
||||||
|
<div id="updateResult" class="muted"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h3>Адреса и балансы</h3>
|
||||||
|
<div id="balances" class="muted">Загрузка...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h3>Очередь 1 (все билеты)</h3>
|
||||||
|
<div class="row">
|
||||||
|
<button id="loadQueueBtn">Показать всю очередь</button>
|
||||||
|
</div>
|
||||||
|
<div id="queueTable" class="muted"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const PROGRAM_ID = new solanaWeb3.PublicKey("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE");
|
||||||
|
const RPC_URL = "https://api.devnet.solana.com";
|
||||||
|
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
|
||||||
|
const SEEDS = {
|
||||||
|
config: "shine_payments_v2_config",
|
||||||
|
coef: "shine_payments_v2_coef_limit",
|
||||||
|
queues: "shine_payments_v2_queues",
|
||||||
|
inflow: "shine_payments_v2_inflow_vault",
|
||||||
|
ticketQ1: "shine_payments_v2_q1_ticket",
|
||||||
|
};
|
||||||
|
|
||||||
|
let walletPubkey = null;
|
||||||
|
let cache = null;
|
||||||
|
document.getElementById("programId").textContent = PROGRAM_ID.toBase58();
|
||||||
|
|
||||||
|
function utf8(s) { return new TextEncoder().encode(s); }
|
||||||
|
function u64ToBytes(v) {
|
||||||
|
let x = BigInt(v);
|
||||||
|
const out = new Uint8Array(8);
|
||||||
|
for (let i = 0; i < 8; i++) { out[i] = Number(x & 255n); x >>= 8n; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function readU64(data, offset) {
|
||||||
|
let x = 0n;
|
||||||
|
for (let i = 0; i < 8; i++) x |= BigInt(data[offset + i]) << (8n * BigInt(i));
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
function concat(...parts) {
|
||||||
|
const len = parts.reduce((n, p) => n + p.length, 0);
|
||||||
|
const out = new Uint8Array(len);
|
||||||
|
let o = 0;
|
||||||
|
for (const p of parts) { out.set(p, o); o += p.length; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function trimZeros(v) {
|
||||||
|
return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, "");
|
||||||
|
}
|
||||||
|
function lamportsToSolStr(l) {
|
||||||
|
return trimZeros((Number(l) / 1_000_000_000).toFixed(9));
|
||||||
|
}
|
||||||
|
function solToLamports(solStr) {
|
||||||
|
const v = Number(solStr);
|
||||||
|
if (!Number.isFinite(v) || v <= 0) throw new Error("Некорректная сумма SOL");
|
||||||
|
return BigInt(Math.round(v * 1_000_000_000));
|
||||||
|
}
|
||||||
|
async function ixDiscriminator(name) {
|
||||||
|
const msg = utf8("global:" + name);
|
||||||
|
const hash = await crypto.subtle.digest("SHA-256", msg);
|
||||||
|
return new Uint8Array(hash).slice(0, 8);
|
||||||
|
}
|
||||||
|
function isUnauthorizedManager(msg) {
|
||||||
|
const s = String(msg || "").toLowerCase();
|
||||||
|
return s.includes("unauthorizedmanager") || s.includes("0x1774");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfig(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
|
||||||
|
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
|
||||||
|
const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
|
||||||
|
const reward = readU64(data, o); o += 8;
|
||||||
|
return { version, dao, manager, inflow, reward };
|
||||||
|
}
|
||||||
|
function parseCoef(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const coefPpm = readU64(data, o); o += 8;
|
||||||
|
const limit = readU64(data, o); o += 8;
|
||||||
|
return { version, coefPpm, limit };
|
||||||
|
}
|
||||||
|
function parseQueues(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const q1Total = readU64(data, o); o += 8;
|
||||||
|
const q1Paid = readU64(data, o); o += 8;
|
||||||
|
const q1SumTotal = readU64(data, o); o += 8;
|
||||||
|
const q1SumPaid = readU64(data, o); o += 8;
|
||||||
|
const q2Total = readU64(data, o); o += 8;
|
||||||
|
const q2Paid = readU64(data, o); o += 8;
|
||||||
|
const q2SumTotal = readU64(data, o); o += 8;
|
||||||
|
const q2SumPaid = readU64(data, o); o += 8;
|
||||||
|
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid };
|
||||||
|
}
|
||||||
|
function parseTicket(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const queueId = data[o++];
|
||||||
|
const index = readU64(data, o); o += 8;
|
||||||
|
const isPaid = data[o++] === 1;
|
||||||
|
const recipient = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
|
||||||
|
const payout = readU64(data, o); o += 8;
|
||||||
|
const debtBefore = readU64(data, o); o += 8;
|
||||||
|
return { version, queueId, index, isPaid, recipient, payout, debtBefore };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProvider() {
|
||||||
|
if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден");
|
||||||
|
return window.solana;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWallet() {
|
||||||
|
const provider = getProvider();
|
||||||
|
const r = await provider.connect();
|
||||||
|
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
|
||||||
|
document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||||
|
await refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendInstruction(ix) {
|
||||||
|
const provider = getProvider();
|
||||||
|
if (!walletPubkey) await connectWallet();
|
||||||
|
const tx = new solanaWeb3.Transaction().add(ix);
|
||||||
|
tx.feePayer = walletPubkey;
|
||||||
|
const bh = await connection.getLatestBlockhash("confirmed");
|
||||||
|
tx.recentBlockhash = bh.blockhash;
|
||||||
|
const signed = await provider.signTransaction(tx);
|
||||||
|
const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: false });
|
||||||
|
await connection.confirmTransaction({ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight }, "confirmed");
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function derivePdas() {
|
||||||
|
const [configPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.config)], PROGRAM_ID);
|
||||||
|
const [coefPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.coef)], PROGRAM_ID);
|
||||||
|
const [queuesPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID);
|
||||||
|
const [inflowPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.inflow)], PROGRAM_ID);
|
||||||
|
return { configPda, coefPda, queuesPda, inflowPda };
|
||||||
|
}
|
||||||
|
|
||||||
|
function q1TicketPda(index) {
|
||||||
|
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.ticketQ1), u64ToBytes(index)], PROGRAM_ID);
|
||||||
|
return pda;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCore() {
|
||||||
|
const pdas = derivePdas();
|
||||||
|
const [cfgAi, coefAi, qAi, inflowAi] = await Promise.all([
|
||||||
|
connection.getAccountInfo(pdas.configPda, "confirmed"),
|
||||||
|
connection.getAccountInfo(pdas.coefPda, "confirmed"),
|
||||||
|
connection.getAccountInfo(pdas.queuesPda, "confirmed"),
|
||||||
|
connection.getAccountInfo(pdas.inflowPda, "confirmed"),
|
||||||
|
]);
|
||||||
|
if (!cfgAi || !coefAi || !qAi || !inflowAi) {
|
||||||
|
cache = { pdas, notInited: true };
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
const config = parseConfig(cfgAi.data);
|
||||||
|
const coef = parseCoef(coefAi.data);
|
||||||
|
const queues = parseQueues(qAi.data);
|
||||||
|
const [daoBal, inflowRent] = await Promise.all([
|
||||||
|
connection.getBalance(config.dao, "confirmed"),
|
||||||
|
connection.getMinimumBalanceForRentExemption(inflowAi.data.length, "confirmed"),
|
||||||
|
]);
|
||||||
|
cache = {
|
||||||
|
pdas,
|
||||||
|
config,
|
||||||
|
coef,
|
||||||
|
queues,
|
||||||
|
inflowLamports: BigInt(inflowAi.lamports),
|
||||||
|
inflowAvailable: BigInt(Math.max(0, inflowAi.lamports - inflowRent)),
|
||||||
|
daoBalance: BigInt(daoBal),
|
||||||
|
};
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshBalances() {
|
||||||
|
const el = document.getElementById("balances");
|
||||||
|
try {
|
||||||
|
const core = await loadCore();
|
||||||
|
if (core.notInited) {
|
||||||
|
el.innerHTML = `<span class="warn">PDA ещё не инициализированы.</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const coefText = trimZeros((Number(core.coef.coefPpm) / 1_000_000).toFixed(6));
|
||||||
|
document.getElementById("managerAllowed").textContent = core.config.manager.toBase58();
|
||||||
|
el.innerHTML = `
|
||||||
|
<div>DAO: <code>${core.config.dao.toBase58()}</code></div>
|
||||||
|
<div>Inflow vault: <code>${core.config.inflow.toBase58()}</code></div>
|
||||||
|
<div class="muted">Inflow vault — это входящий PDA-кошелек выплат программы.</div>
|
||||||
|
<div>Manager: <code>${core.config.manager.toBase58()}</code></div>
|
||||||
|
<div>Награда за шаг: <b>${lamportsToSolStr(core.config.reward)} SOL</b></div>
|
||||||
|
<div>Коэффициент: <b>${coefText}</b>, лимит: <b>${lamportsToSolStr(core.coef.limit)} SOL</b></div>
|
||||||
|
<div>Баланс DAO: <b>${lamportsToSolStr(core.daoBalance)} SOL</b></div>
|
||||||
|
<div>Баланс inflow vault: <b>${lamportsToSolStr(core.inflowLamports)} SOL</b></div>
|
||||||
|
<div>Доступно в inflow (сверх ренты): <b>${lamportsToSolStr(core.inflowAvailable)} SOL</b></div>
|
||||||
|
<div>Q1: total=${core.queues.q1Total}, paid=${core.queues.q1Paid}, sum_total=${lamportsToSolStr(core.queues.q1SumTotal)} SOL, sum_paid=${lamportsToSolStr(core.queues.q1SumPaid)} SOL</div>
|
||||||
|
<div>Q2: total=${core.queues.q2Total}, paid=${core.queues.q2Paid}, sum_total=${lamportsToSolStr(core.queues.q2SumTotal)} SOL, sum_paid=${lamportsToSolStr(core.queues.q2SumPaid)} SOL</div>
|
||||||
|
`;
|
||||||
|
} catch (e) {
|
||||||
|
el.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
|
||||||
|
document.getElementById("managerAllowed").textContent = "не определен";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runInit() {
|
||||||
|
const out = document.getElementById("initResult");
|
||||||
|
out.textContent = "";
|
||||||
|
try {
|
||||||
|
const provider = getProvider();
|
||||||
|
if (!walletPubkey) {
|
||||||
|
await connectWallet();
|
||||||
|
} else if (!provider.isConnected) {
|
||||||
|
await provider.connect();
|
||||||
|
}
|
||||||
|
const pdas = derivePdas();
|
||||||
|
const disc = await ixDiscriminator("init");
|
||||||
|
const keys = [
|
||||||
|
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
|
||||||
|
{ pubkey: pdas.configPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: pdas.coefPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: pdas.queuesPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: pdas.inflowPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
|
||||||
|
];
|
||||||
|
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data: disc });
|
||||||
|
const sig = await sendInstruction(ix);
|
||||||
|
out.innerHTML = `<span class="ok">Init выполнен. Tx: <code>${sig}</code></span>`;
|
||||||
|
await refreshAll();
|
||||||
|
} catch (e) {
|
||||||
|
const raw = String(e.message || e);
|
||||||
|
if (isUnauthorizedManager(raw)) {
|
||||||
|
const mgr = document.getElementById("managerAllowed").textContent;
|
||||||
|
out.innerHTML = `<span class="warn">Вы подключены не под тем аккаунтом. Изменение доступно только управляющему кошельку: <code>${mgr}</code>.</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.innerHTML = `<span class="err">${raw}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCoefLimit() {
|
||||||
|
const out = document.getElementById("updateResult");
|
||||||
|
out.textContent = "";
|
||||||
|
try {
|
||||||
|
const provider = getProvider();
|
||||||
|
if (!walletPubkey) {
|
||||||
|
await connectWallet();
|
||||||
|
} else if (!provider.isConnected) {
|
||||||
|
await provider.connect();
|
||||||
|
}
|
||||||
|
const core = await loadCore();
|
||||||
|
if (core.notInited) throw new Error("Сначала выполните init");
|
||||||
|
|
||||||
|
const coef = Number(document.getElementById("coefInput").value.trim());
|
||||||
|
if (!Number.isFinite(coef) || coef <= 0) throw new Error("Некорректный коэффициент");
|
||||||
|
const coefPpm = BigInt(Math.round(coef * 1_000_000));
|
||||||
|
const limitLamports = solToLamports(document.getElementById("limitInput").value.trim());
|
||||||
|
|
||||||
|
const disc = await ixDiscriminator("update_coef_limit");
|
||||||
|
const data = concat(disc, u64ToBytes(coefPpm), u64ToBytes(limitLamports));
|
||||||
|
const keys = [
|
||||||
|
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
|
||||||
|
{ pubkey: core.pdas.configPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: core.pdas.coefPda, isSigner: false, isWritable: true },
|
||||||
|
];
|
||||||
|
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data });
|
||||||
|
const sig = await sendInstruction(ix);
|
||||||
|
out.innerHTML = `<span class="ok">Обновлено. Tx: <code>${sig}</code></span>`;
|
||||||
|
await refreshAll();
|
||||||
|
} catch (e) {
|
||||||
|
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showQueue() {
|
||||||
|
const out = document.getElementById("queueTable");
|
||||||
|
out.textContent = "Загрузка...";
|
||||||
|
try {
|
||||||
|
const core = await loadCore();
|
||||||
|
if (core.notInited) throw new Error("Сначала выполните init");
|
||||||
|
if (core.queues.q1Total === 0n) {
|
||||||
|
out.innerHTML = `<span class="muted">Очередь 1 пока пустая.</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rows = [];
|
||||||
|
for (let i = 1n; i <= core.queues.q1Total; i++) {
|
||||||
|
const pda = q1TicketPda(i);
|
||||||
|
const ai = await connection.getAccountInfo(pda, "confirmed");
|
||||||
|
if (!ai) {
|
||||||
|
rows.push(`<tr><td>${i.toString()}</td><td colspan="6" class="err">PDA не найден</td></tr>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const t = parseTicket(ai.data);
|
||||||
|
rows.push(`
|
||||||
|
<tr>
|
||||||
|
<td>${t.index.toString()}</td>
|
||||||
|
<td>${t.isPaid ? '<span class="paid">да</span>' : "нет"}</td>
|
||||||
|
<td><code>${pda.toBase58()}</code></td>
|
||||||
|
<td><code>${t.recipient.toBase58()}</code></td>
|
||||||
|
<td>${lamportsToSolStr(t.payout)} SOL</td>
|
||||||
|
<td>${lamportsToSolStr(t.debtBefore)} SOL</td>
|
||||||
|
<td>${t.queueId}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
out.innerHTML = `
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th><th>Выплачен</th><th>PDA</th><th>Получатель</th><th>Сумма</th><th>Debt Before</th><th>Очередь</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows.join("")}</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
} catch (e) {
|
||||||
|
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAll() {
|
||||||
|
await refreshBalances();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("connectBtn").addEventListener("click", connectWallet);
|
||||||
|
document.getElementById("refreshBtn").addEventListener("click", refreshAll);
|
||||||
|
document.getElementById("initBtn").addEventListener("click", runInit);
|
||||||
|
document.getElementById("updateCoefBtn").addEventListener("click", updateCoefLimit);
|
||||||
|
document.getElementById("loadQueueBtn").addEventListener("click", showQueue);
|
||||||
|
refreshAll();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
260
shine/programs/shine_payments/web/buy_ticket.html
Normal file
260
shine/programs/shine_payments/web/buy_ticket.html
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Покупка билета — Shine Payments Devnet</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; }
|
||||||
|
h1 { margin-bottom: 8px; }
|
||||||
|
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; }
|
||||||
|
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
|
||||||
|
input { padding: 8px; min-width: 220px; }
|
||||||
|
button { padding: 8px 12px; cursor: pointer; }
|
||||||
|
.muted { color: #666; }
|
||||||
|
.ok { color: #0a7a3c; }
|
||||||
|
.warn { color: #9f5f00; }
|
||||||
|
.err { color: #b30000; white-space: pre-wrap; }
|
||||||
|
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Покупка билета (Devnet)</h1>
|
||||||
|
<div class="muted">Программа: <code id="programId"></code></div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="row">
|
||||||
|
<button id="connectBtn">Подключить кошелек</button>
|
||||||
|
<button id="refreshBtn">Обновить состояние</button>
|
||||||
|
</div>
|
||||||
|
<div id="walletInfo" class="muted"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h3>Текущее состояние</h3>
|
||||||
|
<div id="stateInfo" class="muted">Загрузка...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h3>Покупка билета в 1-й очереди</h3>
|
||||||
|
<div class="row">
|
||||||
|
<label>Сумма (SOL): <input id="amountSol" value="0.1" /></label>
|
||||||
|
<label>Кошелек для выплаты: <input id="recipient" placeholder="по умолчанию ваш кошелек" /></label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="buyBtn">Купить билет</button>
|
||||||
|
</div>
|
||||||
|
<div id="buyResult" class="muted"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const PROGRAM_ID = new solanaWeb3.PublicKey("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE");
|
||||||
|
const RPC_URL = "https://api.devnet.solana.com";
|
||||||
|
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
|
||||||
|
|
||||||
|
const SEEDS = {
|
||||||
|
config: "shine_payments_v2_config",
|
||||||
|
coef: "shine_payments_v2_coef_limit",
|
||||||
|
queues: "shine_payments_v2_queues",
|
||||||
|
ticketQ1: "shine_payments_v2_q1_ticket",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COEF_SCALE = 1_000_000n;
|
||||||
|
let walletPubkey = null;
|
||||||
|
|
||||||
|
document.getElementById("programId").textContent = PROGRAM_ID.toBase58();
|
||||||
|
|
||||||
|
function utf8(s) { return new TextEncoder().encode(s); }
|
||||||
|
function u64ToBytes(v) {
|
||||||
|
let x = BigInt(v);
|
||||||
|
const out = new Uint8Array(8);
|
||||||
|
for (let i = 0; i < 8; i++) { out[i] = Number(x & 255n); x >>= 8n; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function readU64(data, offset) {
|
||||||
|
let x = 0n;
|
||||||
|
for (let i = 0; i < 8; i++) x |= BigInt(data[offset + i]) << (8n * BigInt(i));
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
function concat(...parts) {
|
||||||
|
const len = parts.reduce((n, p) => n + p.length, 0);
|
||||||
|
const out = new Uint8Array(len);
|
||||||
|
let o = 0;
|
||||||
|
for (const p of parts) { out.set(p, o); o += p.length; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function trimZeros(v) {
|
||||||
|
return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, "");
|
||||||
|
}
|
||||||
|
function lamportsToSolStr(l) {
|
||||||
|
const sol = Number(l) / 1_000_000_000;
|
||||||
|
return trimZeros(sol.toFixed(9));
|
||||||
|
}
|
||||||
|
function solToLamports(solStr) {
|
||||||
|
const v = Number(solStr);
|
||||||
|
if (!Number.isFinite(v) || v <= 0) throw new Error("Некорректная сумма SOL");
|
||||||
|
return BigInt(Math.round(v * 1_000_000_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ixDiscriminator(name) {
|
||||||
|
const msg = utf8("global:" + name);
|
||||||
|
const hash = await crypto.subtle.digest("SHA-256", msg);
|
||||||
|
return new Uint8Array(hash).slice(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfig(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)).toBase58(); o += 32;
|
||||||
|
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)).toBase58(); o += 32;
|
||||||
|
const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)).toBase58(); o += 32;
|
||||||
|
const reward = readU64(data, o); o += 8;
|
||||||
|
return { version, dao, manager, inflow, reward };
|
||||||
|
}
|
||||||
|
function parseCoef(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const coefPpm = readU64(data, o); o += 8;
|
||||||
|
const limit = readU64(data, o); o += 8;
|
||||||
|
return { version, coefPpm, limit };
|
||||||
|
}
|
||||||
|
function parseQueues(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const q1Total = readU64(data, o); o += 8;
|
||||||
|
const q1Paid = readU64(data, o); o += 8;
|
||||||
|
const q1SumTotal = readU64(data, o); o += 8;
|
||||||
|
const q1SumPaid = readU64(data, o); o += 8;
|
||||||
|
const q2Total = readU64(data, o); o += 8;
|
||||||
|
const q2Paid = readU64(data, o); o += 8;
|
||||||
|
const q2SumTotal = readU64(data, o); o += 8;
|
||||||
|
const q2SumPaid = readU64(data, o); o += 8;
|
||||||
|
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProvider() {
|
||||||
|
if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден");
|
||||||
|
return window.solana;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWallet() {
|
||||||
|
const provider = getProvider();
|
||||||
|
const r = await provider.connect();
|
||||||
|
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
|
||||||
|
document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||||
|
await refreshState();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendInstruction(ix) {
|
||||||
|
const provider = getProvider();
|
||||||
|
if (!walletPubkey) await connectWallet();
|
||||||
|
const tx = new solanaWeb3.Transaction().add(ix);
|
||||||
|
tx.feePayer = walletPubkey;
|
||||||
|
const bh = await connection.getLatestBlockhash("confirmed");
|
||||||
|
tx.recentBlockhash = bh.blockhash;
|
||||||
|
const signed = await provider.signTransaction(tx);
|
||||||
|
const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: false });
|
||||||
|
await connection.confirmTransaction({ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight }, "confirmed");
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function derivePdas() {
|
||||||
|
const [configPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.config)], PROGRAM_ID);
|
||||||
|
const [coefPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.coef)], PROGRAM_ID);
|
||||||
|
const [queuesPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID);
|
||||||
|
return { configPda, coefPda, queuesPda };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCoreState() {
|
||||||
|
const pdas = await derivePdas();
|
||||||
|
const [cfgAi, coefAi, queuesAi] = await Promise.all([
|
||||||
|
connection.getAccountInfo(pdas.configPda, "confirmed"),
|
||||||
|
connection.getAccountInfo(pdas.coefPda, "confirmed"),
|
||||||
|
connection.getAccountInfo(pdas.queuesPda, "confirmed"),
|
||||||
|
]);
|
||||||
|
if (!cfgAi || !coefAi || !queuesAi) throw new Error("PDA не инициализированы. Сначала выполните init на странице админки.");
|
||||||
|
const config = parseConfig(cfgAi.data);
|
||||||
|
const coef = parseCoef(coefAi.data);
|
||||||
|
const queues = parseQueues(queuesAi.data);
|
||||||
|
return { pdas, config, coef, queues };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshState() {
|
||||||
|
const el = document.getElementById("stateInfo");
|
||||||
|
try {
|
||||||
|
const { config, coef, queues } = await loadCoreState();
|
||||||
|
const currentDebt = queues.q1SumTotal - queues.q1SumPaid;
|
||||||
|
const pendingTickets = queues.q1Total - queues.q1Paid;
|
||||||
|
const remaining = coef.limit > currentDebt ? coef.limit - currentDebt : 0n;
|
||||||
|
const coefText = trimZeros((Number(coef.coefPpm) / Number(COEF_SCALE)).toFixed(6));
|
||||||
|
const paused = currentDebt >= coef.limit;
|
||||||
|
el.innerHTML = `
|
||||||
|
<div>DAO: <code>${config.dao}</code></div>
|
||||||
|
<div>Inflow vault: <code>${config.inflow}</code></div>
|
||||||
|
<div class="muted">Inflow vault — это входящий PDA-кошелек выплат программы.</div>
|
||||||
|
<div>Коэффициент: <b>${coefText}</b></div>
|
||||||
|
<div>Лимит очереди (1): <b>${lamportsToSolStr(coef.limit)} SOL</b></div>
|
||||||
|
<div>Текущий долг очереди (1): <b>${lamportsToSolStr(currentDebt)} SOL</b></div>
|
||||||
|
<div>Билетов в ожидании до вас сейчас: <b>${pendingTickets.toString()}</b></div>
|
||||||
|
<div>Осталось до изменения коэффициента/лимита: <b>${lamportsToSolStr(remaining)} SOL</b></div>
|
||||||
|
<div class="${paused ? "warn" : "ok"}">${paused ? "Покупка временно приостановлена: очередь заполнена." : "Покупка доступна."}</div>
|
||||||
|
`;
|
||||||
|
} catch (e) {
|
||||||
|
el.innerHTML = `<div class="err">${String(e.message || e)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buyTicket() {
|
||||||
|
const out = document.getElementById("buyResult");
|
||||||
|
out.textContent = "";
|
||||||
|
try {
|
||||||
|
const provider = getProvider();
|
||||||
|
if (!walletPubkey) {
|
||||||
|
await connectWallet();
|
||||||
|
} else if (!provider.isConnected) {
|
||||||
|
await provider.connect();
|
||||||
|
}
|
||||||
|
const { pdas, config, coef, queues } = await loadCoreState();
|
||||||
|
const currentDebt = queues.q1SumTotal - queues.q1SumPaid;
|
||||||
|
if (currentDebt >= coef.limit) {
|
||||||
|
out.innerHTML = `<span class="warn">Пока временно приостановлено: очередь заполнена. После изменения коэффициента/лимита покупка снова заработает.</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const amountLamports = solToLamports(document.getElementById("amountSol").value.trim());
|
||||||
|
const recipientRaw = document.getElementById("recipient").value.trim();
|
||||||
|
const recipient = recipientRaw ? new solanaWeb3.PublicKey(recipientRaw) : walletPubkey;
|
||||||
|
|
||||||
|
const nextIndex = queues.q1Total + 1n;
|
||||||
|
const [ticketPda] = solanaWeb3.PublicKey.findProgramAddressSync(
|
||||||
|
[utf8(SEEDS.ticketQ1), u64ToBytes(nextIndex)],
|
||||||
|
PROGRAM_ID
|
||||||
|
);
|
||||||
|
const disc = await ixDiscriminator("buy_ticket");
|
||||||
|
const data = concat(disc, u64ToBytes(amountLamports), recipient.toBytes());
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
|
||||||
|
{ pubkey: pdas.configPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: pdas.coefPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: pdas.queuesPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: ticketPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: new solanaWeb3.PublicKey(config.dao), isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
|
||||||
|
];
|
||||||
|
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data });
|
||||||
|
const sig = await sendInstruction(ix);
|
||||||
|
out.innerHTML = `<span class="ok">Готово. Tx: <code>${sig}</code></span>`;
|
||||||
|
await refreshState();
|
||||||
|
} catch (e) {
|
||||||
|
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("connectBtn").addEventListener("click", connectWallet);
|
||||||
|
document.getElementById("refreshBtn").addEventListener("click", refreshState);
|
||||||
|
document.getElementById("buyBtn").addEventListener("click", buyTicket);
|
||||||
|
refreshState();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
shine/programs/shine_payments/web/index.html
Normal file
44
shine/programs/shine_payments/web/index.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Главная — Shine Payments Devnet</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; }
|
||||||
|
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; }
|
||||||
|
a.card {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
a.card:hover { background: #fafafa; }
|
||||||
|
.muted { color: #666; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Shine Payments Devnet</h1>
|
||||||
|
<div class="panel">
|
||||||
|
<div>Выберите страницу:</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="card" href="./buy_ticket.html">
|
||||||
|
<h3>Покупка билета</h3>
|
||||||
|
<div class="muted">Создание нового билета в 1-й очереди.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="./track_ticket.html">
|
||||||
|
<h3>Отслеживание билета</h3>
|
||||||
|
<div class="muted">Проверка позиции в очереди, статуса и шаг выплат.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="./admin_tools.html">
|
||||||
|
<h3>Тех. инструменты</h3>
|
||||||
|
<div class="muted">Init, просмотр всех билетов, коэффициент/лимит, адреса и балансы.</div>
|
||||||
|
</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
361
shine/programs/shine_payments/web/track_ticket.html
Normal file
361
shine/programs/shine_payments/web/track_ticket.html
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Отслеживание билета — Shine Payments Devnet</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; }
|
||||||
|
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; }
|
||||||
|
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
|
||||||
|
input { padding: 8px; min-width: 260px; }
|
||||||
|
button { padding: 8px 12px; cursor: pointer; }
|
||||||
|
.muted { color: #666; }
|
||||||
|
.ok { color: #0a7a3c; }
|
||||||
|
.warn { color: #9f5f00; }
|
||||||
|
.err { color: #b30000; white-space: pre-wrap; }
|
||||||
|
.paid { color: #0a7a3c; font-weight: 700; }
|
||||||
|
.waiting { color: #666; }
|
||||||
|
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Отслеживание билета (Devnet)</h1>
|
||||||
|
<div class="muted">Программа: <code id="programId"></code></div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="row">
|
||||||
|
<button id="connectBtn">Подключить кошелек</button>
|
||||||
|
<button id="refreshBtn">Обновить</button>
|
||||||
|
</div>
|
||||||
|
<div id="walletInfo" class="muted"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h3>Поиск билета</h3>
|
||||||
|
<div class="row">
|
||||||
|
<label>Номер билета: <input id="ticketIndex" placeholder="например 1" /></label>
|
||||||
|
<label>или кошелек получателя: <input id="recipientWallet" placeholder="Base58 адрес" /></label>
|
||||||
|
<button id="findBtn">Найти</button>
|
||||||
|
</div>
|
||||||
|
<div id="ticketResult" class="muted"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h3>Состояние шага выплат</h3>
|
||||||
|
<div id="payoutInfo" class="muted">Загрузка...</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="stepBtn">Сделать шаг выплат</button>
|
||||||
|
</div>
|
||||||
|
<div id="stepResult" class="muted"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const PROGRAM_ID = new solanaWeb3.PublicKey("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE");
|
||||||
|
const RPC_URL = "https://api.devnet.solana.com";
|
||||||
|
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
|
||||||
|
|
||||||
|
const SEEDS = {
|
||||||
|
config: "shine_payments_v2_config",
|
||||||
|
coef: "shine_payments_v2_coef_limit",
|
||||||
|
queues: "shine_payments_v2_queues",
|
||||||
|
ticketQ1: "shine_payments_v2_q1_ticket",
|
||||||
|
};
|
||||||
|
|
||||||
|
let walletPubkey = null;
|
||||||
|
let cachedCore = null;
|
||||||
|
|
||||||
|
document.getElementById("programId").textContent = PROGRAM_ID.toBase58();
|
||||||
|
|
||||||
|
function utf8(s) { return new TextEncoder().encode(s); }
|
||||||
|
function u64ToBytes(v) {
|
||||||
|
let x = BigInt(v);
|
||||||
|
const out = new Uint8Array(8);
|
||||||
|
for (let i = 0; i < 8; i++) { out[i] = Number(x & 255n); x >>= 8n; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function readU64(data, offset) {
|
||||||
|
let x = 0n;
|
||||||
|
for (let i = 0; i < 8; i++) x |= BigInt(data[offset + i]) << (8n * BigInt(i));
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
function concat(...parts) {
|
||||||
|
const len = parts.reduce((n, p) => n + p.length, 0);
|
||||||
|
const out = new Uint8Array(len);
|
||||||
|
let o = 0;
|
||||||
|
for (const p of parts) { out.set(p, o); o += p.length; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function trimZeros(v) {
|
||||||
|
return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, "");
|
||||||
|
}
|
||||||
|
function lamportsToSolStr(l) {
|
||||||
|
const sol = Number(l) / 1_000_000_000;
|
||||||
|
return trimZeros(sol.toFixed(9));
|
||||||
|
}
|
||||||
|
async function ixDiscriminator(name) {
|
||||||
|
const msg = utf8("global:" + name);
|
||||||
|
const hash = await crypto.subtle.digest("SHA-256", msg);
|
||||||
|
return new Uint8Array(hash).slice(0, 8);
|
||||||
|
}
|
||||||
|
function isNotEnoughForStep(msg) {
|
||||||
|
const s = String(msg || "").toLowerCase();
|
||||||
|
return s.includes("notenoughinflowforstep") || s.includes("0x177a");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfig(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
|
||||||
|
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
|
||||||
|
const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
|
||||||
|
const reward = readU64(data, o); o += 8;
|
||||||
|
return { version, dao, manager, inflow, reward };
|
||||||
|
}
|
||||||
|
function parseQueues(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const q1Total = readU64(data, o); o += 8;
|
||||||
|
const q1Paid = readU64(data, o); o += 8;
|
||||||
|
const q1SumTotal = readU64(data, o); o += 8;
|
||||||
|
const q1SumPaid = readU64(data, o); o += 8;
|
||||||
|
const q2Total = readU64(data, o); o += 8;
|
||||||
|
const q2Paid = readU64(data, o); o += 8;
|
||||||
|
const q2SumTotal = readU64(data, o); o += 8;
|
||||||
|
const q2SumPaid = readU64(data, o); o += 8;
|
||||||
|
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid };
|
||||||
|
}
|
||||||
|
function parseTicket(data) {
|
||||||
|
let o = 0;
|
||||||
|
const version = data[o++];
|
||||||
|
const queueId = data[o++];
|
||||||
|
const index = readU64(data, o); o += 8;
|
||||||
|
const isPaid = data[o++] === 1;
|
||||||
|
const recipient = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
|
||||||
|
const payout = readU64(data, o); o += 8;
|
||||||
|
const debtBefore = readU64(data, o); o += 8;
|
||||||
|
return { version, queueId, index, isPaid, recipient, payout, debtBefore };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProvider() {
|
||||||
|
if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден");
|
||||||
|
return window.solana;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWallet() {
|
||||||
|
const provider = getProvider();
|
||||||
|
const r = await provider.connect();
|
||||||
|
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
|
||||||
|
document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||||
|
await refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendInstruction(ix) {
|
||||||
|
const provider = getProvider();
|
||||||
|
if (!walletPubkey) await connectWallet();
|
||||||
|
const tx = new solanaWeb3.Transaction().add(ix);
|
||||||
|
tx.feePayer = walletPubkey;
|
||||||
|
const bh = await connection.getLatestBlockhash("confirmed");
|
||||||
|
tx.recentBlockhash = bh.blockhash;
|
||||||
|
const signed = await provider.signTransaction(tx);
|
||||||
|
const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: false });
|
||||||
|
await connection.confirmTransaction({ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight }, "confirmed");
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveCorePdas() {
|
||||||
|
const [configPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.config)], PROGRAM_ID);
|
||||||
|
const [queuesPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID);
|
||||||
|
return { configPda, queuesPda };
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveQ1TicketPda(index) {
|
||||||
|
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.ticketQ1), u64ToBytes(index)], PROGRAM_ID);
|
||||||
|
return pda;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCoreState() {
|
||||||
|
const pdas = deriveCorePdas();
|
||||||
|
const [cfgAi, qAi] = await Promise.all([
|
||||||
|
connection.getAccountInfo(pdas.configPda, "confirmed"),
|
||||||
|
connection.getAccountInfo(pdas.queuesPda, "confirmed"),
|
||||||
|
]);
|
||||||
|
if (!cfgAi || !qAi) throw new Error("PDA не инициализированы. Запустите init на странице админки.");
|
||||||
|
const config = parseConfig(cfgAi.data);
|
||||||
|
const queues = parseQueues(qAi.data);
|
||||||
|
const inflowAi = await connection.getAccountInfo(config.inflow, "confirmed");
|
||||||
|
if (!inflowAi) throw new Error("Inflow vault отсутствует");
|
||||||
|
const rentMin = await connection.getMinimumBalanceForRentExemption(inflowAi.data.length, "confirmed");
|
||||||
|
const available = BigInt(Math.max(0, inflowAi.lamports - rentMin));
|
||||||
|
cachedCore = { pdas, config, queues, inflowAi, available };
|
||||||
|
return cachedCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshPayoutInfo() {
|
||||||
|
const el = document.getElementById("payoutInfo");
|
||||||
|
try {
|
||||||
|
const core = await loadCoreState();
|
||||||
|
const q1Pending = core.queues.q1Total - core.queues.q1Paid;
|
||||||
|
const q2Pending = core.queues.q2Total - core.queues.q2Paid;
|
||||||
|
|
||||||
|
if (q1Pending === 0n && q2Pending === 0n) {
|
||||||
|
el.innerHTML = `
|
||||||
|
<div>Обе очереди пусты/полностью выплачены.</div>
|
||||||
|
<div>На inflow vault доступно (сверх ренты): <b>${lamportsToSolStr(core.available)} SOL</b></div>
|
||||||
|
<div class="warn">При шаге выплат эта сумма будет переведена в DAO, награда вызывающему не начисляется.</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (q1Pending === 0n && q2Pending > 0n) {
|
||||||
|
el.innerHTML = `<div class="warn">Во 2-й очереди есть ожидание, но её выплаты пока не реализованы.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIndex = core.queues.q1Paid + 1n;
|
||||||
|
const nextPda = deriveQ1TicketPda(nextIndex);
|
||||||
|
const nextAi = await connection.getAccountInfo(nextPda, "confirmed");
|
||||||
|
if (!nextAi) {
|
||||||
|
el.innerHTML = `<div class="err">Не найден следующий тикет #${nextIndex.toString()}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = parseTicket(nextAi.data);
|
||||||
|
const need = next.payout * 2n + core.config.reward;
|
||||||
|
const missing = core.available >= need ? 0n : (need - core.available);
|
||||||
|
el.innerHTML = `
|
||||||
|
<div>Следующий тикет: <b>#${next.index.toString()}</b></div>
|
||||||
|
<div>Выплата по тикету: <b>${lamportsToSolStr(next.payout)} SOL</b></div>
|
||||||
|
<div>Нужно для шага (выплата + DAO + награда): <b>${lamportsToSolStr(need)} SOL</b></div>
|
||||||
|
<div>Доступно в inflow vault: <b>${lamportsToSolStr(core.available)} SOL</b></div>
|
||||||
|
<div>${missing === 0n
|
||||||
|
? '<span class="ok">Хватает для шага выплаты.</span>'
|
||||||
|
: `<span class="warn">Не хватает: ${lamportsToSolStr(missing)} SOL</span>`
|
||||||
|
}</div>
|
||||||
|
`;
|
||||||
|
} catch (e) {
|
||||||
|
el.innerHTML = `<div class="err">${String(e.message || e)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findTickets() {
|
||||||
|
const out = document.getElementById("ticketResult");
|
||||||
|
out.textContent = "";
|
||||||
|
try {
|
||||||
|
const core = await loadCoreState();
|
||||||
|
const idxRaw = document.getElementById("ticketIndex").value.trim();
|
||||||
|
const walletRaw = document.getElementById("recipientWallet").value.trim();
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
if (idxRaw) {
|
||||||
|
const idx = BigInt(idxRaw);
|
||||||
|
const pda = deriveQ1TicketPda(idx);
|
||||||
|
const ai = await connection.getAccountInfo(pda, "confirmed");
|
||||||
|
if (!ai) throw new Error(`Тикет #${idx.toString()} не найден`);
|
||||||
|
const t = parseTicket(ai.data);
|
||||||
|
results.push({ pda, t });
|
||||||
|
} else if (walletRaw) {
|
||||||
|
const recipient = new solanaWeb3.PublicKey(walletRaw);
|
||||||
|
for (let i = 1n; i <= core.queues.q1Total; i++) {
|
||||||
|
const pda = deriveQ1TicketPda(i);
|
||||||
|
const ai = await connection.getAccountInfo(pda, "confirmed");
|
||||||
|
if (!ai) continue;
|
||||||
|
const t = parseTicket(ai.data);
|
||||||
|
if (t.recipient.toBase58() === recipient.toBase58()) {
|
||||||
|
results.push({ pda, t });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (results.length === 0) throw new Error("По этому кошельку тикеты не найдены");
|
||||||
|
} else {
|
||||||
|
throw new Error("Введите номер тикета или кошелек");
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIndex = core.queues.q1Paid + 1n;
|
||||||
|
const lines = results.map(({ pda, t }) => {
|
||||||
|
const isCurrent = !t.isPaid && t.index === nextIndex;
|
||||||
|
const inFront = t.index > nextIndex ? (t.index - nextIndex) : 0n;
|
||||||
|
const remainingToThis = t.debtBefore > core.queues.q1SumPaid ? (t.debtBefore - core.queues.q1SumPaid) : 0n;
|
||||||
|
const missingInsideCurrentTicket = isCurrent && core.available < t.payout ? (t.payout - core.available) : 0n;
|
||||||
|
return `
|
||||||
|
<div class="panel">
|
||||||
|
<div>Тикет #<b>${t.index.toString()}</b> (<span class="${t.isPaid ? "paid" : "waiting"}">${t.isPaid ? "выплачен" : "ожидание"}</span>)</div>
|
||||||
|
<div>PDA: <code>${pda.toBase58()}</code></div>
|
||||||
|
<div>Получатель: <code>${t.recipient.toBase58()}</code></div>
|
||||||
|
<div>Сумма выплаты: <b>${lamportsToSolStr(t.payout)} SOL</b></div>
|
||||||
|
<div>Билетов перед ним сейчас: <b>${inFront.toString()}</b></div>
|
||||||
|
<div>До его выплаты по сумме в предыдущих билетах осталось: <b>${lamportsToSolStr(remainingToThis)} SOL</b></div>
|
||||||
|
${isCurrent ? `<div class="warn">Это текущий билет к выплате.</div>` : ``}
|
||||||
|
${isCurrent && missingInsideCurrentTicket > 0n
|
||||||
|
? `<div class="warn">Для выплаты именно этого билета внутри его суммы не хватает: <b>${lamportsToSolStr(missingInsideCurrentTicket)} SOL</b>.</div>`
|
||||||
|
: ``}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
out.innerHTML = lines.join("");
|
||||||
|
} catch (e) {
|
||||||
|
out.innerHTML = `<div class="err">${String(e.message || e)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stepPayout() {
|
||||||
|
const out = document.getElementById("stepResult");
|
||||||
|
out.textContent = "";
|
||||||
|
try {
|
||||||
|
const provider = getProvider();
|
||||||
|
if (!walletPubkey) {
|
||||||
|
await connectWallet();
|
||||||
|
} else if (!provider.isConnected) {
|
||||||
|
await provider.connect();
|
||||||
|
}
|
||||||
|
const core = cachedCore || await loadCoreState();
|
||||||
|
const q1Pending = core.queues.q1Total - core.queues.q1Paid;
|
||||||
|
|
||||||
|
let nextTicketPda;
|
||||||
|
let recipient;
|
||||||
|
if (q1Pending > 0n) {
|
||||||
|
const nextIndex = core.queues.q1Paid + 1n;
|
||||||
|
nextTicketPda = deriveQ1TicketPda(nextIndex);
|
||||||
|
const ai = await connection.getAccountInfo(nextTicketPda, "confirmed");
|
||||||
|
if (!ai) throw new Error(`Следующий тикет #${nextIndex.toString()} не найден`);
|
||||||
|
recipient = parseTicket(ai.data).recipient;
|
||||||
|
} else {
|
||||||
|
nextTicketPda = deriveQ1TicketPda(core.queues.q1Paid + 1n);
|
||||||
|
recipient = walletPubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const disc = await ixDiscriminator("step_payout");
|
||||||
|
const data = concat(disc);
|
||||||
|
const keys = [
|
||||||
|
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
|
||||||
|
{ pubkey: core.pdas.configPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: core.pdas.queuesPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: core.config.inflow, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: nextTicketPda, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: recipient, isSigner: false, isWritable: true },
|
||||||
|
{ pubkey: core.config.dao, isSigner: false, isWritable: true },
|
||||||
|
];
|
||||||
|
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data });
|
||||||
|
const sig = await sendInstruction(ix);
|
||||||
|
out.innerHTML = `<span class="ok">Шаг выполнен. Tx: <code>${sig}</code></span>`;
|
||||||
|
await refreshAll();
|
||||||
|
} catch (e) {
|
||||||
|
const raw = String(e.message || e);
|
||||||
|
if (isNotEnoughForStep(raw)) {
|
||||||
|
out.innerHTML = `<span class="warn">Недостаточно средств для шага выплаты. Это нормальная обработанная ошибка.</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.innerHTML = `<span class="err">${raw}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAll() {
|
||||||
|
await refreshPayoutInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("connectBtn").addEventListener("click", connectWallet);
|
||||||
|
document.getElementById("refreshBtn").addEventListener("click", refreshAll);
|
||||||
|
document.getElementById("findBtn").addEventListener("click", findTickets);
|
||||||
|
document.getElementById("stepBtn").addEventListener("click", stepPayout);
|
||||||
|
refreshAll();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
shine/programs/shine_payments_legacy/Cargo.toml
Normal file
33
shine/programs/shine_payments_legacy/Cargo.toml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "shine_payments"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Payments and investments smart contract"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "lib"]
|
||||||
|
name = "shine_payments"
|
||||||
|
test = false
|
||||||
|
doctest = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anchor-lang = "0.31.1"
|
||||||
|
common = { path = "../common" }
|
||||||
|
|
||||||
|
# ==== добавлено для NFT-функционала ====
|
||||||
|
anchor-spl = { version = "0.31.1", features = ["associated_token", "token"] }
|
||||||
|
mpl-token-metadata = "5.1.1"
|
||||||
|
spl-token = { version = "4.0.0", features = ["no-entrypoint"] }
|
||||||
|
# ======================================
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
no-entrypoint = []
|
||||||
|
no-idl = []
|
||||||
|
no-log-ix-name = []
|
||||||
|
anchor-debug = []
|
||||||
|
custom-heap = []
|
||||||
|
custom-panic = []
|
||||||
|
cpi = []
|
||||||
|
idl-build = ["anchor-lang/idl-build"]
|
||||||
6
shine/programs/shine_payments_legacy/LEGACY_NOTICE.md
Normal file
6
shine/programs/shine_payments_legacy/LEGACY_NOTICE.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Важно
|
||||||
|
|
||||||
|
Эта папка содержит устаревшую версию `shine_payments`.
|
||||||
|
|
||||||
|
- Не использовать для новых доработок.
|
||||||
|
- Актуальная реализация находится в `programs/shine_payments`.
|
||||||
158
shine/programs/shine_payments_legacy/src/lib.rs
Normal file
158
shine/programs/shine_payments_legacy/src/lib.rs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
declare_id!("qpgnAKhsXgPPaqQWfXhpme7UnG8GyStssuoSjF6Fzy3");
|
||||||
|
|
||||||
|
|
||||||
|
/// Подключаем модуль с полной реализацией.
|
||||||
|
pub mod investments;
|
||||||
|
use investments::*; // импортируем всё в корень
|
||||||
|
|
||||||
|
// === модуль NFT ===
|
||||||
|
pub mod nft;
|
||||||
|
|
||||||
|
// ==============================================
|
||||||
|
// Константы формата / сидов / размеров
|
||||||
|
// ==============================================
|
||||||
|
|
||||||
|
/// Префикс (seed) для PDA, где храним глобальное состояние выплат.
|
||||||
|
/// Важно: сид — это просто набор байт; здесь он фиксированный.
|
||||||
|
pub const PDA_SEED_PREFIX: &[u8] = b"shine_investments_state";
|
||||||
|
|
||||||
|
/// Значение коэффициента «по умолчанию» при инициализации.
|
||||||
|
pub const DEFAULT_COEF: u32 = 10; // ← «коэффициент» = 10 при init
|
||||||
|
|
||||||
|
/// Ровно столько байт резервируем под PDA-данные.
|
||||||
|
/// (Можно добавить запас на будущее, но по заданию — только 28.)
|
||||||
|
pub const PAY_STATE_SPACE: u64 = 50; // просто сделал с запасом
|
||||||
|
|
||||||
|
// ==============================================
|
||||||
|
// Программа
|
||||||
|
// ==============================================
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
pub mod shine_payments {
|
||||||
|
use super::*;
|
||||||
|
// Явно подтягиваем типы и функции, чтобы не было путаницы после предыдущих ошибок парсера
|
||||||
|
use crate::investments::{Init, UseState};
|
||||||
|
use crate::investments::{
|
||||||
|
add_bonus as inv_add_bonus, claim as inv_claim, init as inv_init, invest as inv_invest,
|
||||||
|
ErrCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// init — создаёт PDA и кладёт дефолтное состояние.
|
||||||
|
pub fn init(ctx: Context<Init>) -> Result<()> {
|
||||||
|
inv_init(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// invest — в начале читает состояние, в конце сохраняет (логика внутри модуля).
|
||||||
|
pub fn invest(ctx: Context<UseState>, amount: u64) -> Result<()> {
|
||||||
|
inv_invest(ctx, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// add_bonus — начисление бонусов (обычно от DAO).
|
||||||
|
/// Для NFT используем расширенный контекст AddBonusCtx (с аккаунтами коллекции и т.п.).
|
||||||
|
pub fn add_bonus(ctx: Context<AddBonusCtx>, investor: Pubkey, amount: u64) -> Result<()> {
|
||||||
|
inv_add_bonus(ctx, investor, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// claim — выплата.
|
||||||
|
pub fn claim(ctx: Context<UseState>) -> Result<()> {
|
||||||
|
inv_claim(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ВРЕМЕННАЯ ФУНКЦИЯ только для тестов (в итоговой версии её не будет):
|
||||||
|
/// deleteInit — удалить PDA из init и вернуть ренту подписанту.
|
||||||
|
pub fn delete_init(ctx: Context<DeleteInit>) -> Result<()> {
|
||||||
|
let program_id = ctx.program_id;
|
||||||
|
|
||||||
|
// PDA по тем же сид/бамп, что и в init
|
||||||
|
let (expected_pda, _bump) = Pubkey::find_program_address(&[PDA_SEED_PREFIX], program_id);
|
||||||
|
require_keys_eq!(
|
||||||
|
expected_pda,
|
||||||
|
ctx.accounts.state_pda.key(),
|
||||||
|
ErrCode::InvalidPdaAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
// Рента уйдёт на счёт подписанта (signer)
|
||||||
|
common::utils::delete_pda_return_rent(
|
||||||
|
&ctx.accounts.state_pda.to_account_info(),
|
||||||
|
&ctx.accounts.signer.to_account_info(),
|
||||||
|
program_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================
|
||||||
|
// Контексты вне #[program]
|
||||||
|
// ==============================================
|
||||||
|
|
||||||
|
/// Контекст для deleteInit (временный для тестов)
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct DeleteInit<'info> {
|
||||||
|
/// Подписант транзакции — ПОЛУЧАТЕЛЬ ренты
|
||||||
|
#[account(mut)]
|
||||||
|
pub signer: Signer<'info>,
|
||||||
|
|
||||||
|
/// Тот самый PDA из init
|
||||||
|
/// CHECK: адрес валидируем в хендлере по сид-у
|
||||||
|
#[account(mut)]
|
||||||
|
pub state_pda: UncheckedAccount<'info>,
|
||||||
|
|
||||||
|
/// Системная программа
|
||||||
|
pub system_program: Program<'info, System>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Контекст для add_bonus: полный набор аккаунтов для операций с NFT и коллекцией.
|
||||||
|
/// (Комменты по стилю проекта оставлены.)
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct AddBonusCtx<'info> {
|
||||||
|
/// Любой платящий/подписант (в реальном коде — свои проверки).
|
||||||
|
#[account(mut)]
|
||||||
|
pub signer: Signer<'info>,
|
||||||
|
|
||||||
|
/// Тот же PDA с состоянием (должен уже существовать).
|
||||||
|
/// CHECK: проверяется вручную по адресу
|
||||||
|
#[account(mut)]
|
||||||
|
pub state_pda: UncheckedAccount<'info>,
|
||||||
|
|
||||||
|
// --- аккаунты минтимого NFT ---
|
||||||
|
/// Mint создаваемого NFT (должен быть создан заранее: decimals=0, mint_authority=signer, freeze_authority=signer)
|
||||||
|
/// CHECK
|
||||||
|
#[account(mut)]
|
||||||
|
pub mint_pda: UncheckedAccount<'info>,
|
||||||
|
|
||||||
|
/// ATA получателя (может быть предсоздан тестом)
|
||||||
|
/// CHECK
|
||||||
|
#[account(mut)]
|
||||||
|
pub recipient_ata: UncheckedAccount<'info>,
|
||||||
|
/// Владелец ATA (инвестор)
|
||||||
|
/// CHECK
|
||||||
|
pub recipient_owner: UncheckedAccount<'info>,
|
||||||
|
|
||||||
|
// --- аккаунты коллекции (уже созданной заранее) ---
|
||||||
|
/// CHECK
|
||||||
|
pub collection_mint: UncheckedAccount<'info>,
|
||||||
|
/// CHECK
|
||||||
|
#[account(mut)]
|
||||||
|
pub collection_metadata_pda: UncheckedAccount<'info>,
|
||||||
|
/// CHECK
|
||||||
|
#[account(mut)]
|
||||||
|
pub collection_master_edition_pda: UncheckedAccount<'info>,
|
||||||
|
/// Апдейтер коллекции (update authority)
|
||||||
|
pub collection_update_authority: Signer<'info>,
|
||||||
|
|
||||||
|
// --- metadata + master edition для создаваемого NFT ---
|
||||||
|
/// CHECK
|
||||||
|
#[account(mut)]
|
||||||
|
pub metadata_pda: UncheckedAccount<'info>,
|
||||||
|
/// CHECK
|
||||||
|
#[account(mut)]
|
||||||
|
pub master_edition_pda: UncheckedAccount<'info>,
|
||||||
|
|
||||||
|
// --- программы ---
|
||||||
|
/// CHECK: проверяется по ID внутри nft.rs
|
||||||
|
pub token_metadata_program: UncheckedAccount<'info>,
|
||||||
|
pub token_program: Program<'info, anchor_spl::token::Token>,
|
||||||
|
pub associated_token_program: Program<'info, anchor_spl::associated_token::AssociatedToken>,
|
||||||
|
pub system_program: Program<'info, System>,
|
||||||
|
}
|
||||||
@ -14,8 +14,6 @@ bench = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anchor-lang = "0.31.1"
|
anchor-lang = "0.31.1"
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
ed25519-dalek = { version = "1.0.1", default-features = false, features = ["u64_backend"] }
|
|
||||||
sha2 = "0.10"
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@ -6,7 +6,7 @@ pub mod settings;
|
|||||||
use users::*;
|
use users::*;
|
||||||
|
|
||||||
|
|
||||||
declare_id!("5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t");
|
declare_id!("8Z3HQizFRhyVu5cNBwWNBXZHTpu89VMkn7Wuk1oCtkeJ");
|
||||||
|
|
||||||
|
|
||||||
#[program]
|
#[program]
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
use crate::settings;
|
use crate::settings;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use anchor_lang::solana_program::{program::invoke, system_instruction};
|
use anchor_lang::solana_program::{
|
||||||
|
ed25519_program,
|
||||||
|
hash::hashv,
|
||||||
|
instruction::Instruction,
|
||||||
|
program::invoke,
|
||||||
|
sysvar::instructions::{load_current_index_checked, load_instruction_at_checked},
|
||||||
|
system_instruction,
|
||||||
|
};
|
||||||
use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode};
|
use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode};
|
||||||
use ed25519_dalek::{PublicKey, Signature, Verifier};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
const MAGIC: &[u8; 5] = b"SHiNE";
|
const MAGIC: &[u8; 5] = b"SHiNE";
|
||||||
@ -78,6 +83,8 @@ pub struct CreateUserPda<'info> {
|
|||||||
/// CHECK: адрес получателя комиссии проверяется вручную с константой settings.
|
/// CHECK: адрес получателя комиссии проверяется вручную с константой settings.
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub fee_receiver: AccountInfo<'info>,
|
pub fee_receiver: AccountInfo<'info>,
|
||||||
|
/// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции.
|
||||||
|
pub instructions: AccountInfo<'info>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
@ -92,6 +99,8 @@ pub struct UpdateUserPda<'info> {
|
|||||||
/// CHECK: адрес получателя комиссии проверяется вручную с константой settings.
|
/// CHECK: адрес получателя комиссии проверяется вручную с константой settings.
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub fee_receiver: AccountInfo<'info>,
|
pub fee_receiver: AccountInfo<'info>,
|
||||||
|
/// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции.
|
||||||
|
pub instructions: AccountInfo<'info>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> {
|
pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> {
|
||||||
@ -138,8 +147,12 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
|
|||||||
};
|
};
|
||||||
|
|
||||||
let unsigned = serialize_unsigned_record(&record)?;
|
let unsigned = serialize_unsigned_record(&record)?;
|
||||||
verify_record_signature(&record.root_key, &args.signature, &unsigned)?;
|
record.signature = verify_record_signature(
|
||||||
record.signature = vec_to_signature(&args.signature)?;
|
&ctx.accounts.instructions,
|
||||||
|
&record.root_key,
|
||||||
|
&args.signature,
|
||||||
|
&unsigned,
|
||||||
|
)?;
|
||||||
|
|
||||||
let serialized = serialize_full_record(&record)?;
|
let serialized = serialize_full_record(&record)?;
|
||||||
require!(
|
require!(
|
||||||
@ -247,8 +260,12 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
|
|||||||
};
|
};
|
||||||
|
|
||||||
let unsigned = serialize_unsigned_record(&new_record)?;
|
let unsigned = serialize_unsigned_record(&new_record)?;
|
||||||
verify_record_signature(&new_record.root_key, &args.signature, &unsigned)?;
|
new_record.signature = verify_record_signature(
|
||||||
new_record.signature = vec_to_signature(&args.signature)?;
|
&ctx.accounts.instructions,
|
||||||
|
&new_record.root_key,
|
||||||
|
&args.signature,
|
||||||
|
&unsigned,
|
||||||
|
)?;
|
||||||
|
|
||||||
let serialized = serialize_full_record(&new_record)?;
|
let serialized = serialize_full_record(&new_record)?;
|
||||||
require!(
|
require!(
|
||||||
@ -418,22 +435,110 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
|
|||||||
|
|
||||||
fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32]> {
|
fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32]> {
|
||||||
let unsigned = serialize_unsigned_record(record)?;
|
let unsigned = serialize_unsigned_record(record)?;
|
||||||
let digest = Sha256::digest(unsigned);
|
let digest = hashv(&[&unsigned]);
|
||||||
let mut out = [0u8; 32];
|
let mut out = [0u8; 32];
|
||||||
out.copy_from_slice(&digest);
|
out.copy_from_slice(digest.as_ref());
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_record_signature(root_key: &Pubkey, signature: &[u8], unsigned: &[u8]) -> Result<()> {
|
fn verify_record_signature(
|
||||||
let sig_arr = vec_to_signature(signature)?;
|
instructions_sysvar: &AccountInfo,
|
||||||
let hash = Sha256::digest(unsigned);
|
root_key: &Pubkey,
|
||||||
let verify_key =
|
signature: &[u8],
|
||||||
PublicKey::from_bytes(root_key.as_ref()).map_err(|_| error!(ErrCode::InvalidSignature))?;
|
unsigned: &[u8],
|
||||||
let sig = Signature::from_bytes(&sig_arr).map_err(|_| error!(ErrCode::InvalidSignature))?;
|
) -> Result<[u8; 64]> {
|
||||||
verify_key
|
require_keys_eq!(
|
||||||
.verify(hash.as_slice(), &sig)
|
*instructions_sysvar.key,
|
||||||
.map_err(|_| error!(ErrCode::InvalidSignature))?;
|
anchor_lang::solana_program::sysvar::instructions::id(),
|
||||||
Ok(())
|
ErrCode::InvalidSignature
|
||||||
|
);
|
||||||
|
let provided_sig = vec_to_signature(signature)?;
|
||||||
|
let msg_hash = hashv(&[unsigned]);
|
||||||
|
|
||||||
|
let current_ix_index =
|
||||||
|
load_current_index_checked(instructions_sysvar).map_err(|_| error!(ErrCode::InvalidSignature))?;
|
||||||
|
require!(current_ix_index > 0, ErrCode::InvalidSignature);
|
||||||
|
let ed_ix = load_instruction_at_checked(
|
||||||
|
(current_ix_index - 1) as usize,
|
||||||
|
instructions_sysvar,
|
||||||
|
)
|
||||||
|
.map_err(|_| error!(ErrCode::InvalidSignature))?;
|
||||||
|
|
||||||
|
let parsed = parse_ed25519_ix(&ed_ix)?;
|
||||||
|
require_keys_eq!(parsed.pubkey, *root_key, ErrCode::InvalidSignature);
|
||||||
|
require!(parsed.message == msg_hash.as_ref(), ErrCode::InvalidSignature);
|
||||||
|
require!(parsed.signature == provided_sig, ErrCode::InvalidSignature);
|
||||||
|
|
||||||
|
Ok(parsed.signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParsedEd25519 {
|
||||||
|
pub pubkey: Pubkey,
|
||||||
|
pub signature: [u8; 64],
|
||||||
|
pub message: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ed25519_ix(ix: &Instruction) -> Result<ParsedEd25519> {
|
||||||
|
require_keys_eq!(ix.program_id, ed25519_program::id(), ErrCode::InvalidSignature);
|
||||||
|
|
||||||
|
let data = &ix.data;
|
||||||
|
require!(data.len() >= 16, ErrCode::InvalidSignature);
|
||||||
|
require!(data[0] == 1, ErrCode::InvalidSignature); // одна подпись
|
||||||
|
|
||||||
|
let signature_offset = le_u16(data, 2)? as usize;
|
||||||
|
let signature_ix_index = le_u16(data, 4)?;
|
||||||
|
let pubkey_offset = le_u16(data, 6)? as usize;
|
||||||
|
let pubkey_ix_index = le_u16(data, 8)?;
|
||||||
|
let message_offset = le_u16(data, 10)? as usize;
|
||||||
|
let message_size = le_u16(data, 12)? as usize;
|
||||||
|
let message_ix_index = le_u16(data, 14)?;
|
||||||
|
|
||||||
|
require!(signature_ix_index == u16::MAX, ErrCode::InvalidSignature);
|
||||||
|
require!(pubkey_ix_index == u16::MAX, ErrCode::InvalidSignature);
|
||||||
|
require!(message_ix_index == u16::MAX, ErrCode::InvalidSignature);
|
||||||
|
|
||||||
|
let signature_end = signature_offset
|
||||||
|
.checked_add(64)
|
||||||
|
.ok_or(error!(ErrCode::InvalidSignature))?;
|
||||||
|
let pubkey_end = pubkey_offset
|
||||||
|
.checked_add(32)
|
||||||
|
.ok_or(error!(ErrCode::InvalidSignature))?;
|
||||||
|
let message_end = message_offset
|
||||||
|
.checked_add(message_size)
|
||||||
|
.ok_or(error!(ErrCode::InvalidSignature))?;
|
||||||
|
|
||||||
|
let signature_slice = data
|
||||||
|
.get(signature_offset..signature_end)
|
||||||
|
.ok_or(error!(ErrCode::InvalidSignature))?;
|
||||||
|
let pubkey_slice = data
|
||||||
|
.get(pubkey_offset..pubkey_end)
|
||||||
|
.ok_or(error!(ErrCode::InvalidSignature))?;
|
||||||
|
let message = data
|
||||||
|
.get(message_offset..message_end)
|
||||||
|
.ok_or(error!(ErrCode::InvalidSignature))?
|
||||||
|
.to_vec();
|
||||||
|
|
||||||
|
let mut signature = [0u8; 64];
|
||||||
|
signature.copy_from_slice(signature_slice);
|
||||||
|
let pubkey = Pubkey::new_from_array(
|
||||||
|
<[u8; 32]>::try_from(pubkey_slice).map_err(|_| error!(ErrCode::InvalidSignature))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(ParsedEd25519 {
|
||||||
|
pubkey,
|
||||||
|
signature,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn le_u16(data: &[u8], offset: usize) -> Result<u16> {
|
||||||
|
let end = offset
|
||||||
|
.checked_add(2)
|
||||||
|
.ok_or(error!(ErrCode::InvalidSignature))?;
|
||||||
|
let s = data
|
||||||
|
.get(offset..end)
|
||||||
|
.ok_or(error!(ErrCode::InvalidSignature))?;
|
||||||
|
Ok(u16::from_le_bytes([s[0], s[1]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_login(login: &str) -> Result<()> {
|
fn validate_login(login: &str) -> Result<()> {
|
||||||
|
|||||||
55
КЛЮЧИ_И_ДЕПЛОЙ_ТЕСТОВОЕ.md
Normal file
55
КЛЮЧИ_И_ДЕПЛОЙ_ТЕСТОВОЕ.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Ключи и деплой (тестовое пояснение)
|
||||||
|
|
||||||
|
## 1) Какие адреса участвуют
|
||||||
|
|
||||||
|
В проекте есть **2 программы**, поэтому у них **2 разных Program ID**:
|
||||||
|
|
||||||
|
1. `shine_users` -> отдельный адрес программы
|
||||||
|
2. `shine_payments` -> отдельный адрес программы
|
||||||
|
|
||||||
|
Это нормальная схема Solana: одна программа = один Program ID.
|
||||||
|
|
||||||
|
Отдельно есть адрес кошелька-деплоера (upgrade authority), сейчас это:
|
||||||
|
|
||||||
|
- keypair: `~/.config/solana/id.json`
|
||||||
|
- адрес: `4yzHKs2zFXpyqqCETe8KpAs4xhEo4QhJ2ybyTgRZphZv`
|
||||||
|
|
||||||
|
Именно этот кошелек:
|
||||||
|
|
||||||
|
- платит комиссии/ренту при деплое;
|
||||||
|
- владеет правом апгрейда программ;
|
||||||
|
- получает обратно SOL при `solana program close`.
|
||||||
|
|
||||||
|
## 2) Почему раньше "плавали" адреса программ
|
||||||
|
|
||||||
|
`anchor deploy` берет адрес программы из program keypair файла (`target/deploy/*-keypair.json`).
|
||||||
|
Если keypair другой, Program ID тоже будет другой.
|
||||||
|
|
||||||
|
Чтобы этого не было, нужно держать синхронно:
|
||||||
|
|
||||||
|
1. `declare_id!` в `programs/*/src/lib.rs`
|
||||||
|
2. `[programs.devnet]` и `[programs.localnet]` в `Anchor.toml`
|
||||||
|
3. соответствующие `*-keypair.json` для программ
|
||||||
|
|
||||||
|
Сделано:
|
||||||
|
|
||||||
|
- выполнен `anchor keys sync`;
|
||||||
|
- keypair CLI по умолчанию переключен на `~/.config/solana/id.json`;
|
||||||
|
- сохранены копии program keypair в `shine/keys/`.
|
||||||
|
|
||||||
|
## 3) Сколько SOL занимали программы раньше (до закрытия)
|
||||||
|
|
||||||
|
Перед очисткой были закрыты 4 программы с такими возвратами:
|
||||||
|
|
||||||
|
1. `8Z3HQizFRhyVu5cNBwWNBXZHTpu89VMkn7Wuk1oCtkeJ` -> `3.38059032 SOL`
|
||||||
|
2. `qpgnAKhsXgPPaqQWfXhpme7UnG8GyStssuoSjF6Fzy3` -> `2.11208856 SOL`
|
||||||
|
3. `5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t` -> `1.76425560 SOL`
|
||||||
|
4. `92sgkgx7KHpbhQu81mNGHaKa7skJB7esArVdPM7paDSW` -> `1.66820760 SOL`
|
||||||
|
|
||||||
|
Итого было занято программами:
|
||||||
|
|
||||||
|
- `8.92514208 SOL`
|
||||||
|
|
||||||
|
Из них "актуальная пара" (2 программы последнего деплоя) занимала:
|
||||||
|
|
||||||
|
- `3.38059032 + 2.11208856 = 5.49267888 SOL`
|
||||||
Loading…
Reference in New Issue
Block a user