diff --git a/shine/AGENTS.md b/shine/AGENTS.md index f8b669a..f6e8869 100644 --- a/shine/AGENTS.md +++ b/shine/AGENTS.md @@ -7,3 +7,19 @@ - `doc/SHINE_USER_PDA_V1.md` Если меняется формат записи, сериализация, правила подписи, `prev_hash`, экономика лимитов или связанные ограничения create/update, этот документ нужно обновлять в том же изменении. + +## Language Rule + +Во всем проекте использовать русский язык: + +- комментарии в коде; +- тексты в файлах настроек и справочных файлах; +- сообщения и описания в коммитах; +- сопроводительные технические заметки. + +## Rule: Logic and Docs + +Если меняется бизнес-логика смарт-контрактов, сериализация PDA, правила переводов или экономика: + +1. Обновить соответствующий документ в `doc/` в том же изменении. +2. Если документ сразу обновить нельзя, обязательно явно согласовать это с пользователем в чате и зафиксировать план обновления. diff --git a/shine/Anchor.toml b/shine/Anchor.toml index 3494b7c..7d19493 100644 --- a/shine/Anchor.toml +++ b/shine/Anchor.toml @@ -5,35 +5,23 @@ package_manager = "yarn" resolution = true skip-lint = false -[programs.localnet] -shine_users = "5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t" #тут надо если что обновлять -shine_payments = "92sgkgx7KHpbhQu81mNGHaKa7skJB7esArVdPM7paDSW" #тут надо если что обновлять - - [programs.devnet] -shine_users = "5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t" #тут надо если что обновлять -shine_payments = "92sgkgx7KHpbhQu81mNGHaKa7skJB7esArVdPM7paDSW" #тут надо если что обновлять +shine_payments = "4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE" +shine_users = "8Z3HQizFRhyVu5cNBwWNBXZHTpu89VMkn7Wuk1oCtkeJ" - -[workspace] -members = [ - "programs/shine_users", - "programs/shine_payments", -] +[programs.localnet] +shine_payments = "4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE" +shine_users = "5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t" [registry] url = "https://api.apr.dev" [provider] -cluster = "devnet"#"http://127.0.0.1:8899" # это в какую сеть деплоит по умолчанию -wallet = "~/.config/solana/id.json" # а это с какого кошелько спишутся средства за деплой +cluster = "devnet" +wallet = "~/.config/solana/id.json" + +[workspace] +members = ["programs/shine_users", "programs/shine_payments"] [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" - - - - - - - diff --git a/shine/Cargo.lock b/shine/Cargo.lock index 2ff187b..24d4b95 100644 --- a/shine/Cargo.lock +++ b/shine/Cargo.lock @@ -2,42 +2,6 @@ # It is not intended for manual editing. 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]] name = "ahash" version = "0.8.12" @@ -59,12 +23,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "anchor-attribute-access-control" version = "0.31.1" @@ -225,21 +183,6 @@ dependencies = [ "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]] name = "anchor-syn" version = "0.31.1" @@ -446,9 +389,6 @@ name = "bytemuck" version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" -dependencies = [ - "bytemuck_derive", -] [[package]] name = "bytemuck_derive" @@ -461,12 +401,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "cargo_toml" version = "0.19.2" @@ -498,16 +432,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "common" version = "0.1.0" @@ -563,32 +487,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", "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]] name = "curve25519-dalek" version = "4.1.3" @@ -602,7 +503,6 @@ dependencies = [ "fiat-crypto", "rand_core 0.6.4", "rustc_version", - "serde", "subtle", "zeroize", ] @@ -618,12 +518,6 @@ dependencies = [ "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]] name = "digest" version = "0.9.0" @@ -644,33 +538,6 @@ dependencies = [ "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]] name = "equivalent" version = "1.0.2" @@ -713,18 +580,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "generic-array" version = "0.14.7" @@ -773,11 +628,6 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] [[package]] name = "heck" @@ -788,15 +638,6 @@ dependencies = [ "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]] name = "indexmap" version = "2.9.0" @@ -807,24 +648,6 @@ dependencies = [ "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]] name = "itoa" version = "1.0.15" @@ -939,31 +762,6 @@ dependencies = [ "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]] name = "num-bigint" version = "0.4.6" @@ -974,17 +772,6 @@ dependencies = [ "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]] name = "num-derive" version = "0.4.2" @@ -1014,28 +801,6 @@ dependencies = [ "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]] name = "once_cell" version = "1.21.3" @@ -1071,33 +836,6 @@ dependencies = [ "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]] name = "ppv-lite86" version = "0.2.21" @@ -1134,15 +872,6 @@ dependencies = [ "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]] name = "quote" version = "1.0.40" @@ -1380,13 +1109,10 @@ dependencies = [ [[package]] name = "shine_payments" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anchor-lang", - "anchor-spl", "common", - "mpl-token-metadata", - "spl-token 4.0.2", ] [[package]] @@ -1395,8 +1121,6 @@ version = "0.1.0" dependencies = [ "anchor-lang", "common", - "ed25519-dalek", - "sha2 0.10.9", ] [[package]] @@ -1405,12 +1129,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" - [[package]] name = "smallvec" version = "1.15.0" @@ -1540,20 +1258,6 @@ dependencies = [ "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]] name = "solana-decode-error" version = "2.3.0" @@ -1569,17 +1273,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "solana-epoch-rewards" version = "2.2.1" @@ -1851,7 +1544,7 @@ dependencies = [ "log", "memoffset", "num-bigint", - "num-derive 0.4.2", + "num-derive", "num-traits", "rand 0.8.5", "serde", @@ -1975,7 +1668,7 @@ dependencies = [ "borsh 1.5.7", "bytemuck", "bytemuck_derive", - "curve25519-dalek 4.1.3", + "curve25519-dalek", "five8", "five8_const", "getrandom 0.2.16", @@ -2042,35 +1735,6 @@ dependencies = [ "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]] name = "solana-serde-varint" version = "2.2.2" @@ -2111,27 +1775,6 @@ dependencies = [ "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]] name = "solana-slot-hashes" version = "2.2.1" @@ -2269,7 +1912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4f08746f154458f28b98330c0d55cb431e2de64ee4b8efc98dcbe292e0672b" dependencies = [ "bincode", - "num-derive 0.4.2", + "num-derive", "num-traits", "serde", "serde_derive", @@ -2286,376 +1929,6 @@ dependencies = [ "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]] name = "subtle" version = "2.6.1" @@ -2807,26 +2080,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "version_check" version = "0.9.5" @@ -3011,17 +2264,3 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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", -] diff --git a/shine/doc/SHINE_PAYMENTS_V2.md b/shine/doc/SHINE_PAYMENTS_V2.md new file mode 100644 index 0000000..9ab88dc --- /dev/null +++ b/shine/doc/SHINE_PAYMENTS_V2.md @@ -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` diff --git a/shine/doc/SHINE_USER_PDA_V1.md b/shine/doc/SHINE_USER_PDA_V1.md index bda69c1..1d4e0cd 100644 --- a/shine/doc/SHINE_USER_PDA_V1.md +++ b/shine/doc/SHINE_USER_PDA_V1.md @@ -71,6 +71,8 @@ 3. `signature = Ed25519.sign(root_private_key, msg_hash)`. 4. Проверка: - `Ed25519.verify(root_key, msg_hash, signature)`. + - В текущей реализации проверка выполняется через встроенную Solana-инструкцию `Ed25519Program` + (инструкция должна идти в транзакции перед вызовом `create_user_pda` / `update_user_pda`). ## 5. Что входит в `prev_hash` diff --git a/shine/programs/shine_payments/Cargo.toml b/shine/programs/shine_payments/Cargo.toml index add95e4..fda7863 100644 --- a/shine/programs/shine_payments/Cargo.toml +++ b/shine/programs/shine_payments/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "shine_payments" -version = "0.1.0" -description = "Payments and investments smart contract" +version = "0.2.0" +description = "Shine Payments v2 (очереди выплат)" edition = "2021" [lib] @@ -15,12 +15,6 @@ bench = false 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 = [] diff --git a/shine/programs/shine_payments/src/lib.rs b/shine/programs/shine_payments/src/lib.rs index aa1bfee..869ca8d 100644 --- a/shine/programs/shine_payments/src/lib.rs +++ b/shine/programs/shine_payments/src/lib.rs @@ -1,158 +1,605 @@ 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; - -/// Подключаем модуль с полной реализацией. -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; // просто сделал с запасом - -// ============================================== -// Программа -// ============================================== +declare_id!("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE"); #[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) -> 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 invest(ctx: Context, amount: u64) -> Result<()> { - inv_invest(ctx, amount) - } - - /// add_bonus — начисление бонусов (обычно от DAO). - /// Для NFT используем расширенный контекст AddBonusCtx (с аккаунтами коллекции и т.п.). - pub fn add_bonus(ctx: Context, investor: Pubkey, amount: u64) -> Result<()> { - inv_add_bonus(ctx, investor, amount) - } - - /// claim — выплата. - pub fn claim(ctx: Context) -> Result<()> { - inv_claim(ctx) - } - - /// ВРЕМЕННАЯ ФУНКЦИЯ только для тестов (в итоговой версии её не будет): - /// deleteInit — удалить PDA из init и вернуть ренту подписанту. - pub fn delete_init(ctx: Context) -> Result<()> { - let program_id = ctx.program_id; - - // PDA по тем же сид/бамп, что и в init - let (expected_pda, _bump) = Pubkey::find_program_address(&[PDA_SEED_PREFIX], program_id); + pub fn update_coef_limit(ctx: Context, args: UpdateCoefLimitArgs) -> Result<()> { + let config = read_state::(&ctx.accounts.config_pda)?; require_keys_eq!( - expected_pda, - ctx.accounts.state_pda.key(), + config.manager_wallet, + 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::(&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, args: BuyTicketArgs) -> Result<()> { + require!(args.amount_lamports > 0, PaymentsError::InvalidAmount); + let config = read_state::(&ctx.accounts.config_pda)?; + let coef_limit = read_state::(&ctx.accounts.coef_limit_pda)?; + let mut queues = read_state::(&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) -> Result<()> { + let config = read_state::(&ctx.accounts.config_pda)?; + let mut queues = read_state::(&ctx.accounts.queues_pda)?; + let _vault_state = read_state::(&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 ); - // Рента уйдёт на счёт подписанта (signer) - common::utils::delete_pda_return_rent( - &ctx.accounts.state_pda.to_account_info(), - &ctx.accounts.signer.to_account_info(), - program_id, - ) + let mut ticket = read_state::(&ctx.accounts.next_ticket_pda)?; + require!(ticket.queue_id == 1, PaymentsError::InvalidTicketQueue); + require!(ticket.index == next_index, PaymentsError::InvalidTicketIndex); + require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid); + 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)] -pub struct DeleteInit<'info> { - /// Подписант транзакции — ПОЛУЧАТЕЛЬ ренты +pub struct Init<'info> { + /// CHECK: подписант и плательщик, проверяется атрибутом `signer`. + #[account(mut, signer)] + pub payer: AccountInfo<'info>, + /// CHECK: PDA конфига, адрес проверяется вручную в `ensure_expected_pdas`. #[account(mut)] - pub signer: Signer<'info>, - - /// Тот самый PDA из init - /// CHECK: адрес валидируем в хендлере по сид-у + pub config_pda: AccountInfo<'info>, + /// CHECK: PDA коэффициента/лимита, адрес проверяется вручную в `ensure_expected_pdas`. #[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>, } -/// Контекст для add_bonus: полный набор аккаунтов для операций с NFT и коллекцией. -/// (Комменты по стилю проекта оставлены.) #[derive(Accounts)] -pub struct AddBonusCtx<'info> { - /// Любой платящий/подписант (в реальном коде — свои проверки). +pub struct UpdateCoefLimit<'info> { + /// CHECK: подписант-менеджер, проверяется атрибутом `signer` и сверкой адреса в коде. + #[account(mut, signer)] + pub signer: AccountInfo<'info>, + /// CHECK: PDA конфига, читается и валидируется вручную. #[account(mut)] - pub signer: Signer<'info>, + pub config_pda: AccountInfo<'info>, + /// CHECK: PDA коэффициента/лимита, читается и валидируется вручную. + #[account(mut)] + pub coef_limit_pda: AccountInfo<'info>, +} - /// Тот же PDA с состоянием (должен уже существовать). - /// CHECK: проверяется вручную по адресу +#[derive(Accounts)] +pub struct BuyTicket<'info> { + /// CHECK: подписант-покупатель, проверяется атрибутом `signer`. + #[account(mut, signer)] + pub signer: AccountInfo<'info>, + /// CHECK: PDA конфига, читается и валидируется вручную. #[account(mut)] - pub state_pda: UncheckedAccount<'info>, - - // --- аккаунты минтимого NFT --- - /// Mint создаваемого NFT (должен быть создан заранее: decimals=0, mint_authority=signer, freeze_authority=signer) - /// CHECK + pub config_pda: AccountInfo<'info>, + /// CHECK: PDA коэффициента/лимита, читается и валидируется вручную. #[account(mut)] - pub mint_pda: UncheckedAccount<'info>, - - /// ATA получателя (может быть предсоздан тестом) - /// CHECK + pub coef_limit_pda: AccountInfo<'info>, + /// CHECK: PDA очередей, читается и валидируется вручную. #[account(mut)] - pub recipient_ata: UncheckedAccount<'info>, - /// Владелец ATA (инвестор) - /// CHECK - pub recipient_owner: UncheckedAccount<'info>, - - // --- аккаунты коллекции (уже созданной заранее) --- - /// CHECK - pub collection_mint: UncheckedAccount<'info>, - /// CHECK + pub queues_pda: AccountInfo<'info>, + /// CHECK: PDA тикета, адрес и состояние (должен быть пустым) проверяются вручную. #[account(mut)] - pub collection_metadata_pda: UncheckedAccount<'info>, - /// CHECK + pub ticket_pda: AccountInfo<'info>, + /// CHECK: DAO-кошелек, адрес сверяется с конфигом вручную. #[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 dao_wallet: AccountInfo<'info>, 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(pda: &AccountInfo, state: &T) -> Result<()> { + let bytes = state + .try_to_vec() + .map_err(|_| error!(ErrCode::DeserializationError))?; + write_to_pda(pda, &bytes) +} + +fn read_state(pda: &AccountInfo) -> Result { + 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 { + 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) +} diff --git a/shine/programs/shine_payments/src/settings.rs b/shine/programs/shine_payments/src/settings.rs new file mode 100644 index 0000000..e97aa92 --- /dev/null +++ b/shine/programs/shine_payments/src/settings.rs @@ -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"; diff --git a/shine/programs/shine_payments/web/admin_tools.html b/shine/programs/shine_payments/web/admin_tools.html new file mode 100644 index 0000000..ab273dc --- /dev/null +++ b/shine/programs/shine_payments/web/admin_tools.html @@ -0,0 +1,385 @@ + + + + + + Тех. инструменты — Shine Payments Devnet + + + +

Техническая страница (Devnet)

+
Программа:
+ +
+
+ + + +
+
+
+
+ +
+

Коэффициент и лимит

+
Право изменения: загрузка...
+
+ + + +
+
+
+ +
+

Адреса и балансы

+
Загрузка...
+
+ +
+

Очередь 1 (все билеты)

+
+ +
+
+
+ + + + + diff --git a/shine/programs/shine_payments/web/buy_ticket.html b/shine/programs/shine_payments/web/buy_ticket.html new file mode 100644 index 0000000..6394e6d --- /dev/null +++ b/shine/programs/shine_payments/web/buy_ticket.html @@ -0,0 +1,260 @@ + + + + + + Покупка билета — Shine Payments Devnet + + + +

Покупка билета (Devnet)

+
Программа:
+ +
+
+ + +
+
+
+ +
+

Текущее состояние

+
Загрузка...
+
+ +
+

Покупка билета в 1-й очереди

+
+ + +
+
+ +
+
+
+ + + + + diff --git a/shine/programs/shine_payments/web/index.html b/shine/programs/shine_payments/web/index.html new file mode 100644 index 0000000..e0eb0da --- /dev/null +++ b/shine/programs/shine_payments/web/index.html @@ -0,0 +1,44 @@ + + + + + + Главная — Shine Payments Devnet + + + +

Shine Payments Devnet

+
+
Выберите страницу:
+
+ + +

Покупка билета

+
Создание нового билета в 1-й очереди.
+
+ + +

Отслеживание билета

+
Проверка позиции в очереди, статуса и шаг выплат.
+
+ + +

Тех. инструменты

+
Init, просмотр всех билетов, коэффициент/лимит, адреса и балансы.
+
+ + diff --git a/shine/programs/shine_payments/web/track_ticket.html b/shine/programs/shine_payments/web/track_ticket.html new file mode 100644 index 0000000..1896cfa --- /dev/null +++ b/shine/programs/shine_payments/web/track_ticket.html @@ -0,0 +1,361 @@ + + + + + + Отслеживание билета — Shine Payments Devnet + + + +

Отслеживание билета (Devnet)

+
Программа:
+ +
+
+ + +
+
+
+ +
+

Поиск билета

+
+ + + +
+
+
+ +
+

Состояние шага выплат

+
Загрузка...
+
+ +
+
+
+ + + + + diff --git a/shine/programs/shine_payments_legacy/Cargo.toml b/shine/programs/shine_payments_legacy/Cargo.toml new file mode 100644 index 0000000..add95e4 --- /dev/null +++ b/shine/programs/shine_payments_legacy/Cargo.toml @@ -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"] diff --git a/shine/programs/shine_payments_legacy/LEGACY_NOTICE.md b/shine/programs/shine_payments_legacy/LEGACY_NOTICE.md new file mode 100644 index 0000000..e9b68fb --- /dev/null +++ b/shine/programs/shine_payments_legacy/LEGACY_NOTICE.md @@ -0,0 +1,6 @@ +# Важно + +Эта папка содержит устаревшую версию `shine_payments`. + +- Не использовать для новых доработок. +- Актуальная реализация находится в `programs/shine_payments`. diff --git a/shine/programs/shine_payments/dApp/copyToServer.sh b/shine/programs/shine_payments_legacy/dApp/copyToServer.sh similarity index 100% rename from shine/programs/shine_payments/dApp/copyToServer.sh rename to shine/programs/shine_payments_legacy/dApp/copyToServer.sh diff --git a/shine/programs/shine_payments/dApp/init.html b/shine/programs/shine_payments_legacy/dApp/init.html similarity index 100% rename from shine/programs/shine_payments/dApp/init.html rename to shine/programs/shine_payments_legacy/dApp/init.html diff --git a/shine/programs/shine_payments/src/investments.rs b/shine/programs/shine_payments_legacy/src/investments.rs similarity index 100% rename from shine/programs/shine_payments/src/investments.rs rename to shine/programs/shine_payments_legacy/src/investments.rs diff --git a/shine/programs/shine_payments_legacy/src/lib.rs b/shine/programs/shine_payments_legacy/src/lib.rs new file mode 100644 index 0000000..8aace15 --- /dev/null +++ b/shine/programs/shine_payments_legacy/src/lib.rs @@ -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) -> Result<()> { + inv_init(ctx) + } + + /// invest — в начале читает состояние, в конце сохраняет (логика внутри модуля). + pub fn invest(ctx: Context, amount: u64) -> Result<()> { + inv_invest(ctx, amount) + } + + /// add_bonus — начисление бонусов (обычно от DAO). + /// Для NFT используем расширенный контекст AddBonusCtx (с аккаунтами коллекции и т.п.). + pub fn add_bonus(ctx: Context, investor: Pubkey, amount: u64) -> Result<()> { + inv_add_bonus(ctx, investor, amount) + } + + /// claim — выплата. + pub fn claim(ctx: Context) -> Result<()> { + inv_claim(ctx) + } + + /// ВРЕМЕННАЯ ФУНКЦИЯ только для тестов (в итоговой версии её не будет): + /// deleteInit — удалить PDA из init и вернуть ренту подписанту. + pub fn delete_init(ctx: Context) -> 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>, +} diff --git a/shine/programs/shine_payments/src/nft.rs b/shine/programs/shine_payments_legacy/src/nft.rs similarity index 100% rename from shine/programs/shine_payments/src/nft.rs rename to shine/programs/shine_payments_legacy/src/nft.rs diff --git a/shine/programs/shine_users/Cargo.toml b/shine/programs/shine_users/Cargo.toml index 0406bdc..a7c8d13 100644 --- a/shine/programs/shine_users/Cargo.toml +++ b/shine/programs/shine_users/Cargo.toml @@ -14,8 +14,6 @@ bench = false [dependencies] anchor-lang = "0.31.1" common = { path = "../common" } -ed25519-dalek = { version = "1.0.1", default-features = false, features = ["u64_backend"] } -sha2 = "0.10" [features] diff --git a/shine/programs/shine_users/src/lib.rs b/shine/programs/shine_users/src/lib.rs index d9829b5..38b749c 100644 --- a/shine/programs/shine_users/src/lib.rs +++ b/shine/programs/shine_users/src/lib.rs @@ -6,7 +6,7 @@ pub mod settings; use users::*; -declare_id!("5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t"); +declare_id!("8Z3HQizFRhyVu5cNBwWNBXZHTpu89VMkn7Wuk1oCtkeJ"); #[program] diff --git a/shine/programs/shine_users/src/users.rs b/shine/programs/shine_users/src/users.rs index 7f0585f..7178dd2 100644 --- a/shine/programs/shine_users/src/users.rs +++ b/shine/programs/shine_users/src/users.rs @@ -1,9 +1,14 @@ use crate::settings; 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 ed25519_dalek::{PublicKey, Signature, Verifier}; -use sha2::{Digest, Sha256}; use std::str::FromStr; const MAGIC: &[u8; 5] = b"SHiNE"; @@ -78,6 +83,8 @@ pub struct CreateUserPda<'info> { /// CHECK: адрес получателя комиссии проверяется вручную с константой settings. #[account(mut)] pub fee_receiver: AccountInfo<'info>, + /// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции. + pub instructions: AccountInfo<'info>, } #[derive(Accounts)] @@ -92,6 +99,8 @@ pub struct UpdateUserPda<'info> { /// CHECK: адрес получателя комиссии проверяется вручную с константой settings. #[account(mut)] pub fee_receiver: AccountInfo<'info>, + /// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции. + pub instructions: AccountInfo<'info>, } pub fn create_user_pda(ctx: Context, args: CreateUserPdaArgs) -> Result<()> { @@ -138,8 +147,12 @@ pub fn create_user_pda(ctx: Context, args: CreateUserPdaArgs) -> }; let unsigned = serialize_unsigned_record(&record)?; - verify_record_signature(&record.root_key, &args.signature, &unsigned)?; - record.signature = vec_to_signature(&args.signature)?; + record.signature = verify_record_signature( + &ctx.accounts.instructions, + &record.root_key, + &args.signature, + &unsigned, + )?; let serialized = serialize_full_record(&record)?; require!( @@ -247,8 +260,12 @@ pub fn update_user_pda(ctx: Context, args: UpdateUserPdaArgs) -> }; let unsigned = serialize_unsigned_record(&new_record)?; - verify_record_signature(&new_record.root_key, &args.signature, &unsigned)?; - new_record.signature = vec_to_signature(&args.signature)?; + new_record.signature = verify_record_signature( + &ctx.accounts.instructions, + &new_record.root_key, + &args.signature, + &unsigned, + )?; let serialized = serialize_full_record(&new_record)?; require!( @@ -418,22 +435,110 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result { fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32]> { let unsigned = serialize_unsigned_record(record)?; - let digest = Sha256::digest(unsigned); + let digest = hashv(&[&unsigned]); let mut out = [0u8; 32]; - out.copy_from_slice(&digest); + out.copy_from_slice(digest.as_ref()); Ok(out) } -fn verify_record_signature(root_key: &Pubkey, signature: &[u8], unsigned: &[u8]) -> Result<()> { - let sig_arr = vec_to_signature(signature)?; - let hash = Sha256::digest(unsigned); - let verify_key = - PublicKey::from_bytes(root_key.as_ref()).map_err(|_| error!(ErrCode::InvalidSignature))?; - let sig = Signature::from_bytes(&sig_arr).map_err(|_| error!(ErrCode::InvalidSignature))?; - verify_key - .verify(hash.as_slice(), &sig) - .map_err(|_| error!(ErrCode::InvalidSignature))?; - Ok(()) +fn verify_record_signature( + instructions_sysvar: &AccountInfo, + root_key: &Pubkey, + signature: &[u8], + unsigned: &[u8], +) -> Result<[u8; 64]> { + require_keys_eq!( + *instructions_sysvar.key, + anchor_lang::solana_program::sysvar::instructions::id(), + 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, +} + +fn parse_ed25519_ix(ix: &Instruction) -> Result { + 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 { + 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<()> { diff --git a/КЛЮЧИ_И_ДЕПЛОЙ_ТЕСТОВОЕ.md b/КЛЮЧИ_И_ДЕПЛОЙ_ТЕСТОВОЕ.md new file mode 100644 index 0000000..6f9821b --- /dev/null +++ b/КЛЮЧИ_И_ДЕПЛОЙ_ТЕСТОВОЕ.md @@ -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`