impl solisting
This commit is contained in:
290
Cargo.lock
generated
290
Cargo.lock
generated
@@ -100,7 +100,7 @@ dependencies = [
|
||||
"solana-svm-measure",
|
||||
"solana-svm-timings",
|
||||
"solana-svm-type-overrides",
|
||||
"solana-sysvar",
|
||||
"solana-sysvar 3.1.1",
|
||||
"solana-sysvar-id",
|
||||
"solana-transaction-context",
|
||||
"thiserror 2.0.18",
|
||||
@@ -224,7 +224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8140a40827bdfd74720f1f3084778fa081262f2f43bd4bdbc350f98ce1b341c6"
|
||||
dependencies = [
|
||||
"anchor-syn",
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
@@ -259,7 +259,7 @@ dependencies = [
|
||||
"anchor-lang-idl",
|
||||
"base64 0.21.7",
|
||||
"bincode",
|
||||
"borsh",
|
||||
"borsh 1.6.1",
|
||||
"bytemuck",
|
||||
"const-crypto",
|
||||
"solana-account-info",
|
||||
@@ -281,7 +281,7 @@ dependencies = [
|
||||
"solana-sdk-ids",
|
||||
"solana-stake-interface",
|
||||
"solana-system-interface 2.0.0",
|
||||
"solana-sysvar",
|
||||
"solana-sysvar 3.1.1",
|
||||
"solana-sysvar-id",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
@@ -696,17 +696,40 @@ dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee"
|
||||
dependencies = [
|
||||
"borsh-derive 0.10.4",
|
||||
"hashbrown 0.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
|
||||
dependencies = [
|
||||
"borsh-derive",
|
||||
"borsh-derive 1.6.1",
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh-derive"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89"
|
||||
dependencies = [
|
||||
"borsh-derive-internal",
|
||||
"borsh-schema-derive-internal",
|
||||
"proc-macro-crate 0.1.5",
|
||||
"proc-macro2",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh-derive"
|
||||
version = "1.6.1"
|
||||
@@ -714,12 +737,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh-derive-internal"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh-schema-derive-internal"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bs58"
|
||||
version = "0.5.1"
|
||||
@@ -784,7 +829,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a98356df42a2eb1bd8f1793ae4ee4de48e384dd974ce5eac8eee802edb7492be"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml",
|
||||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1245,6 +1290,15 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fast-math"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2465292146cdfc2011350fe3b1c616ac83cf0faeedb33463ba1c332ed8948d66"
|
||||
dependencies = [
|
||||
"ieee754",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "feature-probe"
|
||||
version = "0.1.1"
|
||||
@@ -1427,6 +1481,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
@@ -1451,6 +1514,12 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "ieee754"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -1691,7 +1760,7 @@ dependencies = [
|
||||
"solana-svm-transaction",
|
||||
"solana-system-interface 3.2.0",
|
||||
"solana-system-program",
|
||||
"solana-sysvar",
|
||||
"solana-sysvar 3.1.1",
|
||||
"solana-sysvar-id",
|
||||
"solana-transaction",
|
||||
"solana-transaction-context",
|
||||
@@ -1720,6 +1789,15 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "merlin"
|
||||
version = "3.0.0"
|
||||
@@ -1937,6 +2015,15 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
|
||||
dependencies = [
|
||||
"toml 0.5.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
@@ -1955,6 +2042,39 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyth-solana-receiver-sdk"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b7854c4176470c8d86de301dc5b57ac84227dabb9527328b585fc332962d60b"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"hex",
|
||||
"pythnet-sdk",
|
||||
"solana-program",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pythnet-sdk"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "498d20fd330277697aaee92f341bdabdb4695b10e05f054157a18ad8b7746a17"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"bincode",
|
||||
"borsh 0.10.4",
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"fast-math",
|
||||
"hex",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"sha3",
|
||||
"slow_primes",
|
||||
"solana-program",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qstring"
|
||||
version = "0.7.2"
|
||||
@@ -2340,6 +2460,15 @@ version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "slow_primes"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58267dd2fbaa6dceecba9e3e106d2d90a2b02497c0e8b01b8759beccf5113938"
|
||||
dependencies = [
|
||||
"num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -2361,7 +2490,7 @@ dependencies = [
|
||||
"solana-instruction-error",
|
||||
"solana-pubkey 4.2.0",
|
||||
"solana-sdk-ids",
|
||||
"solana-sysvar",
|
||||
"solana-sysvar 3.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2370,6 +2499,8 @@ version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"serde_core",
|
||||
"solana-address 2.6.0",
|
||||
"solana-program-error",
|
||||
"solana-program-memory",
|
||||
@@ -2390,7 +2521,7 @@ version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"borsh 1.6.1",
|
||||
"bytemuck",
|
||||
"bytemuck_derive",
|
||||
"curve25519-dalek",
|
||||
@@ -2488,7 +2619,7 @@ version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c04abbae16f57178a163125805637b8a076175bb5c0002fb04f4792bea901cf7"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"borsh 1.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2608,7 +2739,7 @@ version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8292c436b269ad23cecc8b24f7da3ab07ca111661e25e00ce0e1d22771951ab9"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"borsh 1.6.1",
|
||||
"solana-instruction",
|
||||
"solana-sdk-ids",
|
||||
]
|
||||
@@ -2706,6 +2837,33 @@ dependencies = [
|
||||
"solana-sysvar-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-epoch-stake"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "027e6d0b9e7daac5b2ac7c3f9ca1b727861121d9ef05084cf435ff736051e7c2"
|
||||
dependencies = [
|
||||
"solana-define-syscall 5.1.0",
|
||||
"solana-pubkey 4.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-example-mocks"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eb265ff95e28eceda117e2e3d2d2a611ecbbfe911dfeeeecd1521814540ffab"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"solana-hash 4.3.0",
|
||||
"solana-instruction",
|
||||
"solana-nonce",
|
||||
"solana-pubkey 4.2.0",
|
||||
"solana-sdk-ids",
|
||||
"solana-system-interface 3.2.0",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-feature-gate-interface"
|
||||
version = "3.1.0"
|
||||
@@ -2768,7 +2926,7 @@ version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1b113239362cee7093bfb250467138f079a2a03673181dc15bff6ccd677912d"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"borsh 1.6.1",
|
||||
"bytemuck",
|
||||
"bytemuck_derive",
|
||||
"five8",
|
||||
@@ -2786,6 +2944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37ebb0ffd19263051bc3f683fcc086134b8ff23af894dcb63f7563c7137b42f1"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"borsh 1.6.1",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"solana-define-syscall 5.1.0",
|
||||
@@ -3022,6 +3181,53 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-program"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778f08fb0eaf52c9a3bef2978247f7fab0ccfddc44cfddb936d5ad9f98ede886"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"solana-account-info",
|
||||
"solana-big-mod-exp",
|
||||
"solana-blake3-hasher",
|
||||
"solana-borsh",
|
||||
"solana-clock",
|
||||
"solana-cpi",
|
||||
"solana-define-syscall 5.1.0",
|
||||
"solana-epoch-rewards",
|
||||
"solana-epoch-schedule",
|
||||
"solana-epoch-stake",
|
||||
"solana-example-mocks",
|
||||
"solana-fee-calculator",
|
||||
"solana-hash 4.3.0",
|
||||
"solana-instruction",
|
||||
"solana-instruction-error",
|
||||
"solana-instructions-sysvar",
|
||||
"solana-keccak-hasher",
|
||||
"solana-last-restart-slot",
|
||||
"solana-msg",
|
||||
"solana-native-token",
|
||||
"solana-program-entrypoint",
|
||||
"solana-program-error",
|
||||
"solana-program-memory",
|
||||
"solana-program-option",
|
||||
"solana-program-pack",
|
||||
"solana-pubkey 4.2.0",
|
||||
"solana-rent 4.2.0",
|
||||
"solana-sdk-ids",
|
||||
"solana-secp256k1-recover",
|
||||
"solana-serde-varint",
|
||||
"solana-serialize-utils",
|
||||
"solana-sha256-hasher",
|
||||
"solana-short-vec",
|
||||
"solana-slot-hashes",
|
||||
"solana-slot-history",
|
||||
"solana-stable-layout",
|
||||
"solana-sysvar 4.0.0",
|
||||
"solana-sysvar-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-program-entrypoint"
|
||||
version = "3.1.1"
|
||||
@@ -3040,7 +3246,9 @@ version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f04fa578707b3612b095f0c8e19b66a1233f7c42ca8082fcb3b745afcc0add6"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"borsh 1.6.1",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3106,7 +3314,7 @@ dependencies = [
|
||||
"solana-svm-transaction",
|
||||
"solana-svm-type-overrides",
|
||||
"solana-system-interface 2.0.0",
|
||||
"solana-sysvar",
|
||||
"solana-sysvar 3.1.1",
|
||||
"solana-sysvar-id",
|
||||
"solana-transaction-context",
|
||||
"thiserror 2.0.18",
|
||||
@@ -3149,7 +3357,11 @@ version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9809b081e99bc142ce803bcd7ee18306759ce3b30a96a9da3f6f41c45e50ef0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"solana-sdk-ids",
|
||||
"solana-sdk-macro",
|
||||
"solana-sysvar-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3344,7 +3556,7 @@ dependencies = [
|
||||
"solana-program-error",
|
||||
"solana-pubkey 3.0.0",
|
||||
"solana-system-interface 2.0.0",
|
||||
"solana-sysvar",
|
||||
"solana-sysvar 3.1.1",
|
||||
"solana-sysvar-id",
|
||||
]
|
||||
|
||||
@@ -3467,7 +3679,7 @@ dependencies = [
|
||||
"solana-svm-log-collector",
|
||||
"solana-svm-type-overrides",
|
||||
"solana-system-interface 2.0.0",
|
||||
"solana-sysvar",
|
||||
"solana-sysvar 3.1.1",
|
||||
"solana-transaction-context",
|
||||
]
|
||||
|
||||
@@ -3503,6 +3715,40 @@ dependencies = [
|
||||
"solana-sysvar-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-sysvar"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1632b69b4f72489db5949a10e8308c229dfa003f99ecaa7477b376807c7b81f4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bincode",
|
||||
"bytemuck",
|
||||
"bytemuck_derive",
|
||||
"lazy_static",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"solana-account-info",
|
||||
"solana-clock",
|
||||
"solana-define-syscall 5.1.0",
|
||||
"solana-epoch-rewards",
|
||||
"solana-epoch-schedule",
|
||||
"solana-fee-calculator",
|
||||
"solana-hash 4.3.0",
|
||||
"solana-instruction",
|
||||
"solana-last-restart-slot",
|
||||
"solana-program-entrypoint",
|
||||
"solana-program-error",
|
||||
"solana-program-memory",
|
||||
"solana-pubkey 4.2.0",
|
||||
"solana-rent 4.2.0",
|
||||
"solana-sdk-ids",
|
||||
"solana-sdk-macro",
|
||||
"solana-slot-hashes",
|
||||
"solana-slot-history",
|
||||
"solana-sysvar-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-sysvar-id"
|
||||
version = "3.1.0"
|
||||
@@ -3735,6 +3981,7 @@ dependencies = [
|
||||
"descro",
|
||||
"descro_ext_resolvers",
|
||||
"litesvm",
|
||||
"pyth-solana-receiver-sdk",
|
||||
"solana-keypair",
|
||||
"solana-message",
|
||||
"solana-signer",
|
||||
@@ -3840,6 +4087,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
|
||||
@@ -22,15 +22,14 @@ custom-panic = []
|
||||
anchor-lang = "1.0.2"
|
||||
descro = { path = "../../../descro/programs/descro", features = ["cpi"] }
|
||||
descro_ext_resolvers = { path = "../../../descro/programs/descro_ext_resolvers" }
|
||||
# pyth-solana-receiver-sdk is not used directly — oracle.rs reads PriceUpdateV2 account data
|
||||
# manually (borsh layout matching the SDK) to avoid SDK compilation issues with this Rust toolchain.
|
||||
pyth-solana-receiver-sdk = "1.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
litesvm = "0.12.0"
|
||||
solana-message = "3.0.1"
|
||||
solana-transaction = "3.0.2"
|
||||
solana-signer = "3.0.0"
|
||||
solana-keypair = "3.0.1"
|
||||
solana-message = "4.2.1"
|
||||
solana-transaction = "4.1.3"
|
||||
solana-signer = "3.0.1"
|
||||
solana-keypair = "3.1.2"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
pub mod initialize;
|
||||
#![allow(ambiguous_glob_reexports)]
|
||||
|
||||
pub mod accept_order;
|
||||
pub mod cancel_order;
|
||||
pub mod close_listing;
|
||||
pub mod close_stale_order;
|
||||
pub mod create_listing;
|
||||
pub mod create_order;
|
||||
pub mod initialize;
|
||||
pub mod reject_order;
|
||||
pub mod update_listing;
|
||||
|
||||
pub use accept_order::*;
|
||||
pub use cancel_order::*;
|
||||
pub use close_listing::*;
|
||||
pub use close_stale_order::*;
|
||||
pub use create_listing::*;
|
||||
pub use create_order::*;
|
||||
pub use initialize::*;
|
||||
pub use reject_order::*;
|
||||
pub use update_listing::*;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use crate::state::{ListingAccount, OrderAccount};
|
||||
use crate::error::SolistingError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct AcceptOrder<'info> {
|
||||
#[account(mut)]
|
||||
pub seller: Signer<'info>,
|
||||
|
||||
/// CHECK: Resolver — may need to co-sign if AcceptancePolicy is SignatureGated (descro enforces this)
|
||||
pub resolver: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
|
||||
bump = listing_account.bump,
|
||||
constraint = seller.key() == listing_account.seller @ SolistingError::Unauthorized,
|
||||
)]
|
||||
pub listing_account: Account<'info, ListingAccount>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
|
||||
bump = order_account.bump,
|
||||
constraint = seller.key() == order_account.seller @ SolistingError::Unauthorized,
|
||||
close = seller,
|
||||
)]
|
||||
pub order_account: Account<'info, OrderAccount>,
|
||||
|
||||
/// CHECK: Descro EscrowAccount — state checked by descro.seller_confirm
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"escrow", seller.key().as_ref(), &order_account.escrow_id.to_le_bytes()],
|
||||
bump,
|
||||
seeds::program = descro::id(),
|
||||
constraint = escrow_account.key() == order_account.escrow_account @ SolistingError::EscrowStateUnexpected,
|
||||
)]
|
||||
pub escrow_account: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Optional resolver registry entry — passed through to descro.seller_confirm
|
||||
pub resolver_entry: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Descro program
|
||||
pub descro_program: UncheckedAccount<'info>,
|
||||
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn handler(ctx: Context<AcceptOrder>) -> Result<()> {
|
||||
require!(
|
||||
ctx.accounts.descro_program.key() == descro::id(),
|
||||
SolistingError::InvalidDescroProgram
|
||||
);
|
||||
|
||||
descro::cpi::seller_confirm(
|
||||
CpiContext::new(
|
||||
ctx.accounts.descro_program.to_account_info(),
|
||||
descro::cpi::accounts::SellerConfirm {
|
||||
seller: ctx.accounts.seller.to_account_info(),
|
||||
resolver: ctx.accounts.resolver.to_account_info(),
|
||||
escrow_account: ctx.accounts.escrow_account.to_account_info(),
|
||||
resolver_entry: ctx.accounts.resolver_entry.to_account_info(),
|
||||
},
|
||||
),
|
||||
)?;
|
||||
|
||||
let listing = &mut ctx.accounts.listing_account;
|
||||
listing.quantity_reserved = listing.quantity_reserved.saturating_sub(1);
|
||||
listing.quantity = listing.quantity.saturating_sub(1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::AccountDeserialize;
|
||||
use crate::state::{ListingAccount, OrderAccount};
|
||||
use crate::error::SolistingError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CancelOrder<'info> {
|
||||
#[account(mut)]
|
||||
pub buyer: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
|
||||
bump = listing_account.bump,
|
||||
)]
|
||||
pub listing_account: Account<'info, ListingAccount>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
|
||||
bump = order_account.bump,
|
||||
constraint = buyer.key() == order_account.buyer @ SolistingError::Unauthorized,
|
||||
close = buyer,
|
||||
)]
|
||||
pub order_account: Account<'info, OrderAccount>,
|
||||
|
||||
/// CHECK: Descro EscrowAccount — read defensively before CPI
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"escrow", order_account.seller.as_ref(), &order_account.escrow_id.to_le_bytes()],
|
||||
bump,
|
||||
seeds::program = descro::id(),
|
||||
)]
|
||||
pub escrow_account: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Descro vault
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"vault", escrow_account.key().as_ref()],
|
||||
bump,
|
||||
seeds::program = descro::id(),
|
||||
)]
|
||||
pub vault: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Descro program
|
||||
pub descro_program: UncheckedAccount<'info>,
|
||||
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn handler(ctx: Context<CancelOrder>) -> Result<()> {
|
||||
require!(
|
||||
ctx.accounts.descro_program.key() == descro::id(),
|
||||
SolistingError::InvalidDescroProgram
|
||||
);
|
||||
|
||||
if !ctx.accounts.escrow_account.data_is_empty() {
|
||||
let data = ctx.accounts.escrow_account.try_borrow_data()?;
|
||||
if let Ok(escrow) = descro::EscrowAccount::try_deserialize(&mut data.as_ref()) {
|
||||
if escrow.state == descro::EscrowState::AwaitingSellerConfirm {
|
||||
drop(data);
|
||||
descro::cpi::cancel(
|
||||
CpiContext::new(
|
||||
ctx.accounts.descro_program.to_account_info(),
|
||||
descro::cpi::accounts::Cancel {
|
||||
canceller: ctx.accounts.buyer.to_account_info(),
|
||||
buyer: ctx.accounts.buyer.to_account_info(),
|
||||
escrow_account: ctx.accounts.escrow_account.to_account_info(),
|
||||
vault: ctx.accounts.vault.to_account_info(),
|
||||
system_program: ctx.accounts.system_program.to_account_info(),
|
||||
},
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.accounts.listing_account.quantity_reserved =
|
||||
ctx.accounts.listing_account.quantity_reserved.saturating_sub(1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use crate::state::ListingAccount;
|
||||
use crate::error::SolistingError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CloseListing<'info> {
|
||||
#[account(mut)]
|
||||
pub seller: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
|
||||
bump = listing_account.bump,
|
||||
constraint = seller.key() == listing_account.seller @ SolistingError::Unauthorized,
|
||||
close = seller,
|
||||
)]
|
||||
pub listing_account: Account<'info, ListingAccount>,
|
||||
}
|
||||
|
||||
pub fn handler(_ctx: Context<CloseListing>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::AccountDeserialize;
|
||||
use crate::state::OrderAccount;
|
||||
use crate::error::SolistingError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CloseStaleOrder<'info> {
|
||||
#[account(mut)]
|
||||
pub caller: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
|
||||
bump = order_account.bump,
|
||||
close = caller,
|
||||
)]
|
||||
pub order_account: Account<'info, OrderAccount>,
|
||||
|
||||
/// CHECK: Descro EscrowAccount — must be in terminal state or closed
|
||||
pub escrow_account: UncheckedAccount<'info>,
|
||||
}
|
||||
|
||||
pub fn handler(ctx: Context<CloseStaleOrder>) -> Result<()> {
|
||||
require!(
|
||||
ctx.accounts.escrow_account.key() == ctx.accounts.order_account.escrow_account,
|
||||
SolistingError::EscrowStateUnexpected
|
||||
);
|
||||
|
||||
// Closed (zero data) escrow = already resolved = ok to clean up
|
||||
if !ctx.accounts.escrow_account.data_is_empty() {
|
||||
let data = ctx.accounts.escrow_account.try_borrow_data()?;
|
||||
if let Ok(escrow) = descro::EscrowAccount::try_deserialize(&mut data.as_ref()) {
|
||||
require!(
|
||||
escrow.state == descro::EscrowState::Cancelled
|
||||
|| escrow.state == descro::EscrowState::Complete,
|
||||
SolistingError::EscrowStateUnexpected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use crate::state::{AltCurrencyConfig, Currency, ListingAccount};
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(listing_id: u64)]
|
||||
pub struct CreateListing<'info> {
|
||||
#[account(mut)]
|
||||
pub seller: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
payer = seller,
|
||||
space = 8 + ListingAccount::INIT_SPACE,
|
||||
seeds = [b"listing", seller.key().as_ref(), &listing_id.to_le_bytes()],
|
||||
bump
|
||||
)]
|
||||
pub listing_account: Account<'info, ListingAccount>,
|
||||
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
ctx: Context<CreateListing>,
|
||||
listing_id: u64,
|
||||
canonical_currency: Currency,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
let listing = &mut ctx.accounts.listing_account;
|
||||
listing.seller = ctx.accounts.seller.key();
|
||||
listing.canonical_currency = canonical_currency;
|
||||
listing.price = price;
|
||||
listing.canonical_oracle = canonical_oracle;
|
||||
listing.alt_currencies = alt_currencies;
|
||||
listing.accepted_resolvers = accepted_resolvers;
|
||||
listing.quantity = quantity;
|
||||
listing.quantity_reserved = 0;
|
||||
listing.metadata_uri = metadata_uri;
|
||||
listing.listing_id = listing_id;
|
||||
listing.is_active = true;
|
||||
listing.bump = ctx.bumps.listing_account;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use crate::state::{Currency, ListingAccount, OrderAccount};
|
||||
use crate::error::SolistingError;
|
||||
use crate::oracle;
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(order_id: u64, escrow_id: u64, resolver: Pubkey, payment_currency: Currency)]
|
||||
pub struct CreateOrder<'info> {
|
||||
#[account(mut)]
|
||||
pub buyer: Signer<'info>,
|
||||
|
||||
/// CHECK: Seller — verified against listing.seller via constraint
|
||||
pub seller: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
|
||||
bump = listing_account.bump,
|
||||
constraint = listing_account.is_active @ SolistingError::ListingNotActive,
|
||||
constraint = listing_account.quantity > listing_account.quantity_reserved @ SolistingError::OutOfStock,
|
||||
constraint = seller.key() == listing_account.seller @ SolistingError::Unauthorized,
|
||||
)]
|
||||
pub listing_account: Account<'info, ListingAccount>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
payer = buyer,
|
||||
space = 8 + OrderAccount::INIT_SPACE,
|
||||
seeds = [b"order", listing_account.key().as_ref(), buyer.key().as_ref(), &order_id.to_le_bytes()],
|
||||
bump
|
||||
)]
|
||||
pub order_account: Account<'info, OrderAccount>,
|
||||
|
||||
/// CHECK: Descro EscrowAccount PDA — created by the CPI
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"escrow", seller.key().as_ref(), &escrow_id.to_le_bytes()],
|
||||
bump,
|
||||
seeds::program = descro::id(),
|
||||
)]
|
||||
pub escrow_account: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Descro vault PDA — funded by the CPI
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"vault", escrow_account.key().as_ref()],
|
||||
bump,
|
||||
seeds::program = descro::id(),
|
||||
)]
|
||||
pub descro_vault: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Pyth USD feed for the canonical currency (listing.canonical_oracle).
|
||||
/// Pass SystemProgram as placeholder when canonical_oracle is None (stablecoin).
|
||||
pub canonical_oracle: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Pyth USD feed for the chosen alt currency (alt_cfg.usd_oracle).
|
||||
/// Pass SystemProgram as placeholder when usd_oracle is None or paying canonical.
|
||||
pub target_oracle: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Descro program — verified in handler
|
||||
pub descro_program: UncheckedAccount<'info>,
|
||||
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
ctx: Context<CreateOrder>,
|
||||
order_id: u64,
|
||||
escrow_id: u64,
|
||||
resolver: Pubkey,
|
||||
payment_currency: Currency,
|
||||
expected_amount: u64,
|
||||
max_slippage_bps: u16,
|
||||
) -> Result<()> {
|
||||
require!(
|
||||
ctx.accounts.descro_program.key() == descro::id(),
|
||||
SolistingError::InvalidDescroProgram
|
||||
);
|
||||
|
||||
let listing = &ctx.accounts.listing_account;
|
||||
|
||||
if !listing.accepted_resolvers.is_empty() {
|
||||
require!(
|
||||
listing.accepted_resolvers.contains(&resolver),
|
||||
SolistingError::ResolverNotAccepted
|
||||
);
|
||||
}
|
||||
|
||||
let amount = if payment_currency == listing.canonical_currency {
|
||||
listing.price
|
||||
} else {
|
||||
let alt_cfg = listing
|
||||
.alt_currencies
|
||||
.iter()
|
||||
.find(|c| c.currency == payment_currency)
|
||||
.ok_or(error!(SolistingError::CurrencyNotAccepted))?;
|
||||
|
||||
if let Some(expected_canonical) = listing.canonical_oracle {
|
||||
require!(
|
||||
ctx.accounts.canonical_oracle.key() == expected_canonical,
|
||||
SolistingError::OracleMismatch
|
||||
);
|
||||
}
|
||||
if let Some(expected_target) = alt_cfg.usd_oracle {
|
||||
require!(
|
||||
ctx.accounts.target_oracle.key() == expected_target,
|
||||
SolistingError::OracleMismatch
|
||||
);
|
||||
}
|
||||
|
||||
let c_oracle = listing
|
||||
.canonical_oracle
|
||||
.map(|_| ctx.accounts.canonical_oracle.as_ref());
|
||||
let t_oracle = alt_cfg
|
||||
.usd_oracle
|
||||
.map(|_| ctx.accounts.target_oracle.as_ref());
|
||||
|
||||
oracle::convert_with_slippage(
|
||||
c_oracle,
|
||||
&listing.canonical_currency,
|
||||
listing.price,
|
||||
t_oracle,
|
||||
&payment_currency,
|
||||
expected_amount,
|
||||
max_slippage_bps,
|
||||
)?
|
||||
};
|
||||
|
||||
require!(
|
||||
payment_currency == Currency::Sol,
|
||||
SolistingError::SplNotImplemented
|
||||
);
|
||||
|
||||
descro::cpi::buyer_create_escrow(
|
||||
CpiContext::new(
|
||||
ctx.accounts.descro_program.to_account_info(),
|
||||
descro::cpi::accounts::BuyerCreateEscrow {
|
||||
buyer: ctx.accounts.buyer.to_account_info(),
|
||||
seller: ctx.accounts.seller.to_account_info(),
|
||||
escrow_account: ctx.accounts.escrow_account.to_account_info(),
|
||||
vault: ctx.accounts.descro_vault.to_account_info(),
|
||||
system_program: ctx.accounts.system_program.to_account_info(),
|
||||
},
|
||||
),
|
||||
amount,
|
||||
Some(resolver),
|
||||
escrow_id,
|
||||
)?;
|
||||
|
||||
ctx.accounts.listing_account.quantity_reserved += 1;
|
||||
|
||||
let order = &mut ctx.accounts.order_account;
|
||||
order.listing = ctx.accounts.listing_account.key();
|
||||
order.buyer = ctx.accounts.buyer.key();
|
||||
order.seller = ctx.accounts.seller.key();
|
||||
order.resolver = resolver;
|
||||
order.payment_currency = payment_currency;
|
||||
order.amount = amount;
|
||||
order.escrow_account = ctx.accounts.escrow_account.key();
|
||||
order.escrow_id = escrow_id;
|
||||
order.order_id = order_id;
|
||||
order.created_at = Clock::get()?.unix_timestamp;
|
||||
order.bump = ctx.bumps.order_account;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::AccountDeserialize;
|
||||
use crate::state::{ListingAccount, OrderAccount};
|
||||
use crate::error::SolistingError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct RejectOrder<'info> {
|
||||
#[account(mut)]
|
||||
pub seller: Signer<'info>,
|
||||
|
||||
/// CHECK: Buyer receives vault refund from descro.cancel
|
||||
#[account(mut)]
|
||||
pub buyer: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
|
||||
bump = listing_account.bump,
|
||||
constraint = seller.key() == listing_account.seller @ SolistingError::Unauthorized,
|
||||
)]
|
||||
pub listing_account: Account<'info, ListingAccount>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
|
||||
bump = order_account.bump,
|
||||
constraint = seller.key() == order_account.seller @ SolistingError::Unauthorized,
|
||||
constraint = buyer.key() == order_account.buyer @ SolistingError::Unauthorized,
|
||||
close = seller,
|
||||
)]
|
||||
pub order_account: Account<'info, OrderAccount>,
|
||||
|
||||
/// CHECK: Descro EscrowAccount — read to check current state before CPI
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"escrow", seller.key().as_ref(), &order_account.escrow_id.to_le_bytes()],
|
||||
bump,
|
||||
seeds::program = descro::id(),
|
||||
)]
|
||||
pub escrow_account: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Descro vault — drained to buyer by descro.cancel
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"vault", escrow_account.key().as_ref()],
|
||||
bump,
|
||||
seeds::program = descro::id(),
|
||||
)]
|
||||
pub vault: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Descro program
|
||||
pub descro_program: UncheckedAccount<'info>,
|
||||
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn handler(ctx: Context<RejectOrder>) -> Result<()> {
|
||||
require!(
|
||||
ctx.accounts.descro_program.key() == descro::id(),
|
||||
SolistingError::InvalidDescroProgram
|
||||
);
|
||||
|
||||
// Defensive: skip CPI if escrow already resolved (buyer cancelled directly on descro)
|
||||
if !ctx.accounts.escrow_account.data_is_empty() {
|
||||
let data = ctx.accounts.escrow_account.try_borrow_data()?;
|
||||
if let Ok(escrow) = descro::EscrowAccount::try_deserialize(&mut data.as_ref()) {
|
||||
if escrow.state == descro::EscrowState::AwaitingSellerConfirm {
|
||||
drop(data);
|
||||
descro::cpi::cancel(
|
||||
CpiContext::new(
|
||||
ctx.accounts.descro_program.to_account_info(),
|
||||
descro::cpi::accounts::Cancel {
|
||||
canceller: ctx.accounts.seller.to_account_info(),
|
||||
buyer: ctx.accounts.buyer.to_account_info(),
|
||||
escrow_account: ctx.accounts.escrow_account.to_account_info(),
|
||||
vault: ctx.accounts.vault.to_account_info(),
|
||||
system_program: ctx.accounts.system_program.to_account_info(),
|
||||
},
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.accounts.listing_account.quantity_reserved =
|
||||
ctx.accounts.listing_account.quantity_reserved.saturating_sub(1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use crate::state::{AltCurrencyConfig, Currency, ListingAccount};
|
||||
use crate::error::SolistingError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct UpdateListing<'info> {
|
||||
pub seller: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
|
||||
bump = listing_account.bump,
|
||||
constraint = seller.key() == listing_account.seller @ SolistingError::Unauthorized,
|
||||
constraint = listing_account.is_active @ SolistingError::ListingNotActive,
|
||||
)]
|
||||
pub listing_account: Account<'info, ListingAccount>,
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
ctx: Context<UpdateListing>,
|
||||
canonical_currency: Currency,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
let listing = &mut ctx.accounts.listing_account;
|
||||
listing.canonical_currency = canonical_currency;
|
||||
listing.price = price;
|
||||
listing.canonical_oracle = canonical_oracle;
|
||||
listing.alt_currencies = alt_currencies;
|
||||
listing.accepted_resolvers = accepted_resolvers;
|
||||
listing.quantity = quantity;
|
||||
listing.metadata_uri = metadata_uri;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ pub mod oracle;
|
||||
pub mod state;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
pub use constants::*;
|
||||
pub use error::*;
|
||||
pub use instructions::*;
|
||||
pub use state::*;
|
||||
|
||||
@@ -19,4 +18,69 @@ pub mod solisting {
|
||||
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
|
||||
initialize::handler(ctx)
|
||||
}
|
||||
|
||||
pub fn create_listing(
|
||||
ctx: Context<CreateListing>,
|
||||
listing_id: u64,
|
||||
canonical_currency: state::Currency,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<state::AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
create_listing::handler(
|
||||
ctx, listing_id, canonical_currency, price,
|
||||
canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_listing(
|
||||
ctx: Context<UpdateListing>,
|
||||
canonical_currency: state::Currency,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<state::AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
update_listing::handler(
|
||||
ctx, canonical_currency, price,
|
||||
canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn close_listing(ctx: Context<CloseListing>) -> Result<()> {
|
||||
close_listing::handler(ctx)
|
||||
}
|
||||
|
||||
pub fn create_order(
|
||||
ctx: Context<CreateOrder>,
|
||||
order_id: u64,
|
||||
escrow_id: u64,
|
||||
resolver: Pubkey,
|
||||
payment_currency: state::Currency,
|
||||
expected_amount: u64,
|
||||
max_slippage_bps: u16,
|
||||
) -> Result<()> {
|
||||
create_order::handler(ctx, order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps)
|
||||
}
|
||||
|
||||
pub fn accept_order(ctx: Context<AcceptOrder>) -> Result<()> {
|
||||
accept_order::handler(ctx)
|
||||
}
|
||||
|
||||
pub fn reject_order(ctx: Context<RejectOrder>) -> Result<()> {
|
||||
reject_order::handler(ctx)
|
||||
}
|
||||
|
||||
pub fn cancel_order(ctx: Context<CancelOrder>) -> Result<()> {
|
||||
cancel_order::handler(ctx)
|
||||
}
|
||||
|
||||
pub fn close_stale_order(ctx: Context<CloseStaleOrder>) -> Result<()> {
|
||||
close_stale_order::handler(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,17 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::AnchorDeserialize;
|
||||
use anchor_lang::AccountDeserialize;
|
||||
use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;
|
||||
use crate::error::SolistingError;
|
||||
use crate::state::Currency;
|
||||
|
||||
const ORACLE_MAX_AGE_SECS: i64 = 60;
|
||||
|
||||
/// Mirrors pyth_solana_receiver_sdk::price_update::VerificationLevel (borsh layout).
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
enum VerificationLevel {
|
||||
Partial { num_signatures: u8 },
|
||||
Full,
|
||||
}
|
||||
|
||||
/// Mirrors pythnet_sdk::messages::PriceFeedMessage (borsh layout).
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
struct PriceFeedMessage {
|
||||
pub feed_id: [u8; 32],
|
||||
pub price: i64,
|
||||
pub conf: u64,
|
||||
pub exponent: i32,
|
||||
pub publish_time: i64,
|
||||
pub prev_publish_time: i64,
|
||||
pub ema_price: i64,
|
||||
pub ema_conf: u64,
|
||||
}
|
||||
|
||||
/// Mirrors pyth_solana_receiver_sdk::price_update::PriceUpdateV2 (borsh layout after discriminator).
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
struct PriceUpdateV2Local {
|
||||
pub write_authority: Pubkey,
|
||||
pub verification_level: VerificationLevel,
|
||||
pub price_message: PriceFeedMessage,
|
||||
pub posted_slot: u64,
|
||||
}
|
||||
|
||||
/// Reads a Pyth V2 PriceFeedAccount and returns (price_i64, exponent_i32).
|
||||
/// `price * 10^exponent` is the USD value of 1 whole token.
|
||||
/// The caller must verify the account key matches listing state before calling this.
|
||||
fn read_usd_price(oracle_account: &AccountInfo) -> Result<(i64, i32)> {
|
||||
let data = oracle_account.try_borrow_data()?;
|
||||
require!(data.len() > 8, SolistingError::OraclePriceUnavailable);
|
||||
// Skip the 8-byte Anchor discriminator, then borsh-decode the rest.
|
||||
let price_update = PriceUpdateV2Local::try_from_slice(&data[8..])
|
||||
let price_update = PriceUpdateV2::try_deserialize(&mut data.as_ref())
|
||||
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?;
|
||||
let msg = &price_update.price_message;
|
||||
let now = Clock::get()?.unix_timestamp;
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use anchor_lang::{
|
||||
solana_program::{instruction::Instruction, system_program},
|
||||
AccountDeserialize, InstructionData, ToAccountMetas,
|
||||
};
|
||||
use litesvm::LiteSVM;
|
||||
use solana_keypair::Keypair;
|
||||
use solana_message::{Message, VersionedMessage};
|
||||
use solana_signer::Signer;
|
||||
use solana_transaction::versioned::VersionedTransaction;
|
||||
|
||||
pub use anchor_lang::prelude::Pubkey;
|
||||
pub use solisting::state::{AltCurrencyConfig, Currency, ListingAccount, OrderAccount};
|
||||
|
||||
pub fn setup() -> (LiteSVM, Keypair, Keypair) {
|
||||
let mut svm = LiteSVM::new();
|
||||
|
||||
let solisting_bytes = include_bytes!("../../../../target/deploy/solisting.so");
|
||||
svm.add_program(solisting::id(), solisting_bytes).unwrap();
|
||||
|
||||
let descro_bytes = include_bytes!("../../../../../descro/target/deploy/descro.so");
|
||||
svm.add_program(descro::id(), descro_bytes).unwrap();
|
||||
|
||||
let seller = Keypair::new();
|
||||
let buyer = Keypair::new();
|
||||
svm.airdrop(&seller.pubkey(), 10_000_000_000).unwrap();
|
||||
svm.airdrop(&buyer.pubkey(), 10_000_000_000).unwrap();
|
||||
|
||||
(svm, seller, buyer)
|
||||
}
|
||||
|
||||
pub fn listing_pda(seller: &Pubkey, listing_id: u64) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&[b"listing", seller.as_ref(), &listing_id.to_le_bytes()],
|
||||
&solisting::id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn order_pda(listing: Pubkey, buyer: Pubkey, order_id: u64) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&[b"order", listing.as_ref(), buyer.as_ref(), &order_id.to_le_bytes()],
|
||||
&solisting::id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn escrow_pda(seller: &Pubkey, escrow_id: u64) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&[b"escrow", seller.as_ref(), &escrow_id.to_le_bytes()],
|
||||
&descro::id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn vault_pda(escrow: &Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(&[b"vault", escrow.as_ref()], &descro::id()).0
|
||||
}
|
||||
|
||||
pub fn read_listing(svm: &LiteSVM, seller: &Pubkey, listing_id: u64) -> ListingAccount {
|
||||
let pda = listing_pda(seller, listing_id);
|
||||
let account = svm.get_account(&pda).expect("listing account not found");
|
||||
ListingAccount::try_deserialize(&mut account.data.as_slice()).unwrap()
|
||||
}
|
||||
|
||||
pub fn read_order(svm: &LiteSVM, listing: Pubkey, buyer: Pubkey, order_id: u64) -> OrderAccount {
|
||||
let pda = order_pda(listing, buyer, order_id);
|
||||
let account = svm.get_account(&pda).expect("order account not found");
|
||||
OrderAccount::try_deserialize(&mut account.data.as_slice()).unwrap()
|
||||
}
|
||||
|
||||
pub fn send(svm: &mut LiteSVM, ixs: &[Instruction], signers: &[&Keypair]) {
|
||||
let blockhash = svm.latest_blockhash();
|
||||
let msg = Message::new_with_blockhash(ixs, Some(&signers[0].pubkey()), &blockhash);
|
||||
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), signers).unwrap();
|
||||
svm.send_transaction(tx).expect("transaction failed");
|
||||
}
|
||||
|
||||
pub fn try_send(
|
||||
svm: &mut LiteSVM,
|
||||
ixs: &[Instruction],
|
||||
signers: &[&Keypair],
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let blockhash = svm.latest_blockhash();
|
||||
let msg = Message::new_with_blockhash(ixs, Some(&signers[0].pubkey()), &blockhash);
|
||||
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), signers)
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
||||
svm.send_transaction(tx)
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||
}
|
||||
|
||||
// --- Listing instruction builders ---
|
||||
|
||||
pub fn ix_create_listing(
|
||||
seller: &Pubkey,
|
||||
listing_id: u64,
|
||||
canonical_currency: Currency,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
) -> Instruction {
|
||||
let listing_account = listing_pda(seller, listing_id);
|
||||
Instruction::new_with_bytes(
|
||||
solisting::id(),
|
||||
&solisting::instruction::CreateListing {
|
||||
listing_id,
|
||||
canonical_currency,
|
||||
price,
|
||||
canonical_oracle,
|
||||
alt_currencies,
|
||||
accepted_resolvers,
|
||||
quantity,
|
||||
metadata_uri,
|
||||
}
|
||||
.data(),
|
||||
solisting::accounts::CreateListing {
|
||||
seller: *seller,
|
||||
listing_account,
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ix_update_listing(
|
||||
seller: &Pubkey,
|
||||
listing_id: u64,
|
||||
canonical_currency: Currency,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
) -> Instruction {
|
||||
let listing_account = listing_pda(seller, listing_id);
|
||||
Instruction::new_with_bytes(
|
||||
solisting::id(),
|
||||
&solisting::instruction::UpdateListing {
|
||||
canonical_currency,
|
||||
price,
|
||||
canonical_oracle,
|
||||
alt_currencies,
|
||||
accepted_resolvers,
|
||||
quantity,
|
||||
metadata_uri,
|
||||
}
|
||||
.data(),
|
||||
solisting::accounts::UpdateListing {
|
||||
seller: *seller,
|
||||
listing_account,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ix_close_listing(seller: &Pubkey, listing_id: u64) -> Instruction {
|
||||
let listing_account = listing_pda(seller, listing_id);
|
||||
Instruction::new_with_bytes(
|
||||
solisting::id(),
|
||||
&solisting::instruction::CloseListing {}.data(),
|
||||
solisting::accounts::CloseListing {
|
||||
seller: *seller,
|
||||
listing_account,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
// --- Order instruction builders ---
|
||||
|
||||
/// Build a create_order instruction paying in the canonical currency (no oracle accounts needed).
|
||||
/// For alt-currency orders, use ix_create_order_with_resolver with explicit oracle keys.
|
||||
pub fn ix_create_order(
|
||||
buyer: &Pubkey,
|
||||
seller: &Pubkey,
|
||||
listing_id: u64,
|
||||
order_id: u64,
|
||||
payment_currency: Currency,
|
||||
max_slippage_bps: u16,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
target_oracle: Option<Pubkey>,
|
||||
) -> Instruction {
|
||||
let resolver = Pubkey::new_unique();
|
||||
ix_create_order_with_resolver(
|
||||
buyer,
|
||||
seller,
|
||||
listing_id,
|
||||
order_id,
|
||||
payment_currency,
|
||||
max_slippage_bps,
|
||||
canonical_oracle,
|
||||
target_oracle,
|
||||
resolver,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ix_create_order_with_resolver(
|
||||
buyer: &Pubkey,
|
||||
seller: &Pubkey,
|
||||
listing_id: u64,
|
||||
order_id: u64,
|
||||
payment_currency: Currency,
|
||||
max_slippage_bps: u16,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
target_oracle: Option<Pubkey>,
|
||||
resolver: Pubkey,
|
||||
) -> Instruction {
|
||||
let listing = listing_pda(seller, listing_id);
|
||||
let order = order_pda(listing, *buyer, order_id);
|
||||
// escrow_id is derived from the first 8 bytes of the order PDA to guarantee uniqueness
|
||||
let escrow_id = u64::from_le_bytes(order.to_bytes()[0..8].try_into().unwrap());
|
||||
let escrow_account = escrow_pda(seller, escrow_id);
|
||||
let descro_vault = vault_pda(&escrow_account);
|
||||
let canonical_oracle_key = canonical_oracle.unwrap_or(system_program::ID);
|
||||
let target_oracle_key = target_oracle.unwrap_or(system_program::ID);
|
||||
// expected_amount = 0 disables slippage check (tests compute it externally or don't need it)
|
||||
let expected_amount: u64 = 0;
|
||||
Instruction::new_with_bytes(
|
||||
solisting::id(),
|
||||
&solisting::instruction::CreateOrder {
|
||||
order_id,
|
||||
escrow_id,
|
||||
resolver,
|
||||
payment_currency,
|
||||
expected_amount,
|
||||
max_slippage_bps,
|
||||
}
|
||||
.data(),
|
||||
solisting::accounts::CreateOrder {
|
||||
buyer: *buyer,
|
||||
seller: *seller,
|
||||
listing_account: listing,
|
||||
order_account: order,
|
||||
escrow_account,
|
||||
descro_vault,
|
||||
canonical_oracle: canonical_oracle_key,
|
||||
target_oracle: target_oracle_key,
|
||||
descro_program: descro::id(),
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ix_accept_order(
|
||||
seller: &Pubkey,
|
||||
listing_id: u64,
|
||||
buyer: &Pubkey,
|
||||
order_id: u64,
|
||||
escrow_id: u64,
|
||||
resolver: Pubkey,
|
||||
) -> Instruction {
|
||||
let listing_account = listing_pda(seller, listing_id);
|
||||
let order_account = order_pda(listing_account, *buyer, order_id);
|
||||
let escrow_account = escrow_pda(seller, escrow_id);
|
||||
// Pass a fresh key as resolver_entry — it won't exist, so descro skips the policy check
|
||||
let resolver_entry = Pubkey::new_unique();
|
||||
Instruction::new_with_bytes(
|
||||
solisting::id(),
|
||||
&solisting::instruction::AcceptOrder {}.data(),
|
||||
solisting::accounts::AcceptOrder {
|
||||
seller: *seller,
|
||||
resolver,
|
||||
listing_account,
|
||||
order_account,
|
||||
escrow_account,
|
||||
resolver_entry,
|
||||
descro_program: descro::id(),
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ix_reject_order(
|
||||
seller: &Pubkey,
|
||||
listing_id: u64,
|
||||
buyer: &Pubkey,
|
||||
order_id: u64,
|
||||
escrow_id: u64,
|
||||
) -> Instruction {
|
||||
let listing_account = listing_pda(seller, listing_id);
|
||||
let order_account = order_pda(listing_account, *buyer, order_id);
|
||||
let escrow_account = escrow_pda(seller, escrow_id);
|
||||
let vault = vault_pda(&escrow_account);
|
||||
Instruction::new_with_bytes(
|
||||
solisting::id(),
|
||||
&solisting::instruction::RejectOrder {}.data(),
|
||||
solisting::accounts::RejectOrder {
|
||||
seller: *seller,
|
||||
buyer: *buyer,
|
||||
listing_account,
|
||||
order_account,
|
||||
escrow_account,
|
||||
vault,
|
||||
descro_program: descro::id(),
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ix_cancel_order(
|
||||
buyer: &Pubkey,
|
||||
listing_id: u64,
|
||||
seller: &Pubkey,
|
||||
order_id: u64,
|
||||
escrow_id: u64,
|
||||
) -> Instruction {
|
||||
let listing_account = listing_pda(seller, listing_id);
|
||||
let order_account = order_pda(listing_account, *buyer, order_id);
|
||||
let escrow_account = escrow_pda(seller, escrow_id);
|
||||
let vault = vault_pda(&escrow_account);
|
||||
Instruction::new_with_bytes(
|
||||
solisting::id(),
|
||||
&solisting::instruction::CancelOrder {}.data(),
|
||||
solisting::accounts::CancelOrder {
|
||||
buyer: *buyer,
|
||||
listing_account,
|
||||
order_account,
|
||||
escrow_account,
|
||||
vault,
|
||||
descro_program: descro::id(),
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ix_close_stale_order(
|
||||
caller: &Pubkey,
|
||||
listing: Pubkey,
|
||||
buyer: Pubkey,
|
||||
order_id: u64,
|
||||
escrow_account: Pubkey,
|
||||
) -> Instruction {
|
||||
let order_account = order_pda(listing, buyer, order_id);
|
||||
Instruction::new_with_bytes(
|
||||
solisting::id(),
|
||||
&solisting::instruction::CloseStaleOrder {}.data(),
|
||||
solisting::accounts::CloseStaleOrder {
|
||||
caller: *caller,
|
||||
order_account,
|
||||
escrow_account,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a descro cancel instruction directly (bypasses solisting — used to test defensive paths)
|
||||
pub fn ix_descro_cancel(buyer: &Pubkey, seller: &Pubkey, escrow_id: u64) -> Instruction {
|
||||
let escrow_account = escrow_pda(seller, escrow_id);
|
||||
let vault = vault_pda(&escrow_account);
|
||||
Instruction::new_with_bytes(
|
||||
descro::id(),
|
||||
&descro::instruction::Cancel {}.data(),
|
||||
descro::accounts::Cancel {
|
||||
canceller: *buyer,
|
||||
buyer: *buyer,
|
||||
escrow_account,
|
||||
vault,
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.to_account_metas(None),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
#[test]
|
||||
fn seller_can_create_sol_only_listing() {
|
||||
let (mut svm, seller, _) = setup();
|
||||
let listing_id: u64 = 1;
|
||||
let price = 100_000_000u64; // 0.1 SOL in lamports
|
||||
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(),
|
||||
listing_id,
|
||||
Currency::Sol,
|
||||
price,
|
||||
None,
|
||||
vec![],
|
||||
vec![],
|
||||
10,
|
||||
"ipfs://test".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
|
||||
assert_eq!(listing.canonical_currency, Currency::Sol);
|
||||
assert_eq!(listing.price, price);
|
||||
assert_eq!(listing.alt_currencies.len(), 0);
|
||||
assert_eq!(listing.canonical_oracle, None);
|
||||
assert_eq!(listing.quantity, 10);
|
||||
assert_eq!(listing.quantity_reserved, 0);
|
||||
assert!(listing.is_active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seller_can_create_listing_with_usdc_alt_stablecoin() {
|
||||
let (mut svm, seller, _) = setup();
|
||||
let usdc_mint = Pubkey::new_unique();
|
||||
let sol_usd_feed = Pubkey::new_unique();
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(),
|
||||
10,
|
||||
Currency::Sol,
|
||||
1_000_000_000,
|
||||
Some(sol_usd_feed),
|
||||
vec![AltCurrencyConfig {
|
||||
currency: Currency::Spl { mint: usdc_mint, decimals: 6 },
|
||||
usd_oracle: None,
|
||||
}],
|
||||
vec![],
|
||||
5,
|
||||
"ipfs://x".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
let listing = read_listing(&svm, &seller.pubkey(), 10);
|
||||
assert_eq!(listing.canonical_oracle, Some(sol_usd_feed));
|
||||
assert_eq!(listing.alt_currencies[0].usd_oracle, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seller_can_create_listing_with_bonk_alt_oracle() {
|
||||
let (mut svm, seller, _) = setup();
|
||||
let bonk_mint = Pubkey::new_unique();
|
||||
let sol_usd_feed = Pubkey::new_unique();
|
||||
let bonk_usd_feed = Pubkey::new_unique();
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(),
|
||||
11,
|
||||
Currency::Sol,
|
||||
1_000_000_000,
|
||||
Some(sol_usd_feed),
|
||||
vec![AltCurrencyConfig {
|
||||
currency: Currency::Spl { mint: bonk_mint, decimals: 5 },
|
||||
usd_oracle: Some(bonk_usd_feed),
|
||||
}],
|
||||
vec![],
|
||||
5,
|
||||
"".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
let listing = read_listing(&svm, &seller.pubkey(), 11);
|
||||
assert_eq!(listing.canonical_oracle, Some(sol_usd_feed));
|
||||
assert_eq!(listing.alt_currencies[0].usd_oracle, Some(bonk_usd_feed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seller_can_update_listing() {
|
||||
let (mut svm, seller, _) = setup();
|
||||
let listing_id: u64 = 2;
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
|
||||
None, vec![], vec![], 10, "".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
let ix_update = ix_update_listing(
|
||||
&seller.pubkey(),
|
||||
listing_id,
|
||||
Currency::Sol,
|
||||
2_000_000_000,
|
||||
None,
|
||||
vec![],
|
||||
vec![],
|
||||
20,
|
||||
"ipfs://new".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix_update], &[&seller]);
|
||||
|
||||
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
|
||||
assert_eq!(listing.price, 2_000_000_000);
|
||||
assert_eq!(listing.quantity, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seller_can_close_listing() {
|
||||
let (mut svm, seller, _) = setup();
|
||||
let listing_id: u64 = 3;
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
|
||||
None, vec![], vec![], 5, "".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
|
||||
send(&mut svm, &[ix_close], &[&seller]);
|
||||
|
||||
let pda = listing_pda(&seller.pubkey(), listing_id);
|
||||
assert!(svm.get_account(&pda).is_none());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use solana_keypair::Keypair;
|
||||
use solana_signer::Signer;
|
||||
|
||||
#[test]
|
||||
fn buyer_can_create_order_canonical_sol() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 1u64;
|
||||
let price = 100_000_000u64;
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(), listing_id, Currency::Sol, price,
|
||||
None, vec![], vec![], 5, "".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
let order_id = 1u64;
|
||||
let ix_order = ix_create_order(
|
||||
&buyer.pubkey(),
|
||||
&seller.pubkey(),
|
||||
listing_id,
|
||||
order_id,
|
||||
Currency::Sol,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
send(&mut svm, &[ix_order], &[&buyer]);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
let order = read_order(&svm, listing_key, buyer.pubkey(), order_id);
|
||||
assert_eq!(order.payment_currency, Currency::Sol);
|
||||
assert_eq!(order.amount, price);
|
||||
|
||||
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
|
||||
assert_eq!(listing.quantity_reserved, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_order_fails_if_listing_inactive() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 2u64;
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
|
||||
None, vec![], vec![], 1, "".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
|
||||
send(&mut svm, &[ix_close], &[&seller]);
|
||||
|
||||
let result = try_send(
|
||||
&mut svm,
|
||||
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
|
||||
&[&buyer],
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_order_fails_if_out_of_stock() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 3u64;
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
|
||||
None, vec![], vec![], 1, "".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
|
||||
&[&buyer],
|
||||
);
|
||||
|
||||
let buyer2 = Keypair::new();
|
||||
svm.airdrop(&buyer2.pubkey(), 10_000_000_000).unwrap();
|
||||
let result = try_send(
|
||||
&mut svm,
|
||||
&[ix_create_order(&buyer2.pubkey(), &seller.pubkey(), listing_id, 2, Currency::Sol, 0, None, None)],
|
||||
&[&buyer2],
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_order_fails_if_currency_not_accepted() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 4u64;
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
|
||||
None, vec![], vec![], 5, "".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
let fake_mint = Pubkey::new_unique();
|
||||
let result = try_send(
|
||||
&mut svm,
|
||||
&[ix_create_order(
|
||||
&buyer.pubkey(), &seller.pubkey(), listing_id, 1,
|
||||
Currency::Spl { mint: fake_mint, decimals: 6 }, 0, None, None,
|
||||
)],
|
||||
&[&buyer],
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_order_fails_if_resolver_not_accepted() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 5u64;
|
||||
let allowed_resolver = Pubkey::new_unique();
|
||||
let ix = ix_create_listing(
|
||||
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
|
||||
None, vec![], vec![allowed_resolver], 5, "".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
let result = try_send(
|
||||
&mut svm,
|
||||
&[ix_create_order_with_resolver(
|
||||
&buyer.pubkey(), &seller.pubkey(), listing_id, 1,
|
||||
Currency::Sol, 0, None, None, Pubkey::new_unique(),
|
||||
)],
|
||||
&[&buyer],
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seller_accept_creates_active_descro_escrow() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 10u64;
|
||||
let price = 100_000_000u64;
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())],
|
||||
&[&seller],
|
||||
);
|
||||
let order_id = 1u64;
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, order_id, Currency::Sol, 0, None, None)],
|
||||
&[&buyer],
|
||||
);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
let order = read_order(&svm, listing_key, buyer.pubkey(), order_id);
|
||||
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_accept_order(&seller.pubkey(), listing_id, &buyer.pubkey(), order_id, order.escrow_id, order.resolver)],
|
||||
&[&seller],
|
||||
);
|
||||
|
||||
let order_key = order_pda(listing_key, buyer.pubkey(), order_id);
|
||||
assert!(svm.get_account(&order_key).is_none());
|
||||
|
||||
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
|
||||
assert_eq!(listing.quantity_reserved, 0);
|
||||
assert_eq!(listing.quantity, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seller_can_reject_order() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 20u64;
|
||||
let price = 100_000_000u64;
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())],
|
||||
&[&seller],
|
||||
);
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
|
||||
&[&buyer],
|
||||
);
|
||||
|
||||
let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
|
||||
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_reject_order(&seller.pubkey(), listing_id, &buyer.pubkey(), 1, order.escrow_id)],
|
||||
&[&seller],
|
||||
);
|
||||
|
||||
let buyer_after = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
|
||||
assert!(buyer_after >= buyer_before + price - 10_000);
|
||||
assert_eq!(read_listing(&svm, &seller.pubkey(), listing_id).quantity_reserved, 0);
|
||||
assert!(svm.get_account(&order_pda(listing_key, buyer.pubkey(), 1)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buyer_can_cancel_order() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 21u64;
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
|
||||
&[&seller],
|
||||
);
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
|
||||
&[&buyer],
|
||||
);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_cancel_order(&buyer.pubkey(), listing_id, &seller.pubkey(), 1, order.escrow_id)],
|
||||
&[&buyer],
|
||||
);
|
||||
|
||||
assert_eq!(read_listing(&svm, &seller.pubkey(), listing_id).quantity_reserved, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_handles_already_cancelled_escrow() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 22u64;
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
|
||||
&[&seller],
|
||||
);
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
|
||||
&[&buyer],
|
||||
);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
|
||||
// Buyer cancels directly on descro (bypassing solisting)
|
||||
send(&mut svm, &[ix_descro_cancel(&buyer.pubkey(), &seller.pubkey(), order.escrow_id)], &[&buyer]);
|
||||
// Seller calls solisting reject — should succeed without CPI error
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_reject_order(&seller.pubkey(), listing_id, &buyer.pubkey(), 1, order.escrow_id)],
|
||||
&[&seller],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anyone_can_close_stale_order_after_terminal_escrow() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 30u64;
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
|
||||
&[&seller],
|
||||
);
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
|
||||
&[&buyer],
|
||||
);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
|
||||
// Buyer cancels on descro directly → escrow is Cancelled
|
||||
send(&mut svm, &[ix_descro_cancel(&buyer.pubkey(), &seller.pubkey(), order.escrow_id)], &[&buyer]);
|
||||
|
||||
let janitor = Keypair::new();
|
||||
svm.airdrop(&janitor.pubkey(), 1_000_000).unwrap();
|
||||
let janitor_before = svm.get_account(&janitor.pubkey()).unwrap().lamports;
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_close_stale_order(&janitor.pubkey(), listing_key, buyer.pubkey(), 1, order.escrow_account)],
|
||||
&[&janitor],
|
||||
);
|
||||
|
||||
assert!(svm.get_account(&order_pda(listing_key, buyer.pubkey(), 1)).is_none());
|
||||
let janitor_after = svm.get_account(&janitor.pubkey()).unwrap().lamports;
|
||||
assert!(janitor_after > janitor_before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_stale_order_fails_if_escrow_still_active() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 31u64;
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
|
||||
&[&seller],
|
||||
);
|
||||
send(
|
||||
&mut svm,
|
||||
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
|
||||
&[&buyer],
|
||||
);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
|
||||
|
||||
let janitor = Keypair::new();
|
||||
svm.airdrop(&janitor.pubkey(), 1_000_000).unwrap();
|
||||
let result = try_send(
|
||||
&mut svm,
|
||||
&[ix_close_stale_order(&janitor.pubkey(), listing_key, buyer.pubkey(), 1, order.escrow_account)],
|
||||
&[&janitor],
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user