From 67d1d295bc40cf9fa9df6911fd910b8040ec8470 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 28 Oct 2025 17:53:21 -0300 Subject: [PATCH 01/41] Add sample Anchor program and set up LiteSVM --- packages/svm/settler/.gitignore | 7 + packages/svm/settler/.prettierignore | 7 + packages/svm/settler/Anchor.toml | 19 + packages/svm/settler/Cargo.lock | 2226 +++++++++++++++++ packages/svm/settler/Cargo.toml | 14 + packages/svm/settler/package.json | 30 + .../svm/settler/programs/settler/Cargo.toml | 21 + .../svm/settler/programs/settler/Xargo.toml | 2 + .../svm/settler/programs/settler/src/lib.rs | 18 + packages/svm/settler/tests/global-hooks.ts | 0 packages/svm/settler/tests/settler.test.ts | 46 + packages/svm/settler/tests/utils.ts | 95 + packages/svm/settler/tsconfig.json | 13 + yarn.lock | 362 ++- 14 files changed, 2850 insertions(+), 10 deletions(-) create mode 100644 packages/svm/settler/.gitignore create mode 100644 packages/svm/settler/.prettierignore create mode 100644 packages/svm/settler/Anchor.toml create mode 100644 packages/svm/settler/Cargo.lock create mode 100644 packages/svm/settler/Cargo.toml create mode 100644 packages/svm/settler/package.json create mode 100644 packages/svm/settler/programs/settler/Cargo.toml create mode 100644 packages/svm/settler/programs/settler/Xargo.toml create mode 100644 packages/svm/settler/programs/settler/src/lib.rs create mode 100644 packages/svm/settler/tests/global-hooks.ts create mode 100644 packages/svm/settler/tests/settler.test.ts create mode 100644 packages/svm/settler/tests/utils.ts create mode 100644 packages/svm/settler/tsconfig.json diff --git a/packages/svm/settler/.gitignore b/packages/svm/settler/.gitignore new file mode 100644 index 0000000..2e0446b --- /dev/null +++ b/packages/svm/settler/.gitignore @@ -0,0 +1,7 @@ +.anchor +.DS_Store +target +**/*.rs.bk +node_modules +test-ledger +.yarn diff --git a/packages/svm/settler/.prettierignore b/packages/svm/settler/.prettierignore new file mode 100644 index 0000000..4142583 --- /dev/null +++ b/packages/svm/settler/.prettierignore @@ -0,0 +1,7 @@ +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger diff --git a/packages/svm/settler/Anchor.toml b/packages/svm/settler/Anchor.toml new file mode 100644 index 0000000..c93c0e0 --- /dev/null +++ b/packages/svm/settler/Anchor.toml @@ -0,0 +1,19 @@ +[toolchain] +package_manager = "yarn" + +[features] +resolution = true +skip-lint = false + +[programs.localnet] +settler = "HbNt35Ng8aM4NUy39evpCQqXEC4Nmaq16ewY8dyNF6NF" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "node --test --experimental-strip-types tests/settler.test.ts" diff --git a/packages/svm/settler/Cargo.lock b/packages/svm/settler/Cargo.lock new file mode 100644 index 0000000..e1452ea --- /dev/null +++ b/packages/svm/settler/Cargo.lock @@ -0,0 +1,2226 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anchor-attribute-access-control" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f70fd141a4d18adf11253026b32504f885447048c7494faf5fa83b01af9c0cf" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715a261c57c7679581e06f07a74fa2af874ac30f86bd8ea07cca4a7e5388a064" +dependencies = [ + "anchor-syn", + "bs58", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730d6df8ae120321c5c25e0779e61789e4b70dc8297102248902022f286102e4" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e6e449cc3a37b2880b74dcafb8e5a17b954c0e58e376432d7adc646fb333ef" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7710e4c54adf485affcd9be9adec5ef8846d9c71d7f31e16ba86ff9fc1dd49f" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ecfd49b2aeadeb32f35262230db402abed76ce87e27562b34f61318b2ec83c" +dependencies = [ + "anchor-lang-idl", + "anchor-syn", + "anyhow", + "bs58", + "heck", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be89d160793a88495af462a7010b3978e48e30a630c91de47ce2c1d3cb7a6149" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc6ee78acb7bfe0c2dd2abc677aaa4789c0281a0c0ef01dbf6fe85e0fd9e6e4" +dependencies = [ + "anchor-syn", + "borsh-derive-internal", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-space" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134a01c0703f6fd355a0e472c033f6f3e41fac1ef6e370b20c50f4c8d022cea7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-lang" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6bab117055905e930f762c196e08f861f8dfe7241b92cee46677a3b15561a0a" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-constant", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-program", + "anchor-derive-accounts", + "anchor-derive-serde", + "anchor-derive-space", + "anchor-lang-idl", + "base64 0.21.7", + "bincode", + "borsh 0.10.4", + "bytemuck", + "solana-program", + "thiserror 1.0.69", +] + +[[package]] +name = "anchor-lang-idl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e8599d21995f68e296265aa5ab0c3cef582fd58afec014d01bd0bce18a4418" +dependencies = [ + "anchor-lang-idl-spec", + "anyhow", + "heck", + "regex", + "serde", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "anchor-lang-idl-spec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdf143115440fe621bdac3a29a1f7472e09f6cd82b2aa569429a0c13f103838" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "anchor-syn" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc7a6d90cc643df0ed2744862cdf180587d1e5d28936538c18fc8908489ed67" +dependencies = [ + "anyhow", + "bs58", + "cargo_toml", + "heck", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 1.0.109", + "thiserror 1.0.69", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-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.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive 1.5.7", + "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.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "cargo_toml" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a98356df42a2eb1bd8f1793ae4ee4de48e384dd974ce5eac8eee802edb7492be" +dependencies = [ + "serde", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.2.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "five8" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.9", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "settler" +version = "0.1.0" +dependencies = [ + "anchor-lang", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "solana-account" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f949fe4edaeaea78c844023bfc1c898e0b1f5a100f8a8d2d0f85d0a7b090258" +dependencies = [ + "solana-account-info", + "solana-clock", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-account-info" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" +dependencies = [ + "bincode", + "serde", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", +] + +[[package]] +name = "solana-address-lookup-table-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" +dependencies = [ + "bincode", + "bytemuck", + "serde", + "serde_derive", + "solana-clock", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-slot-hashes", +] + +[[package]] +name = "solana-atomic-u64" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52e52720efe60465b052b9e7445a01c17550666beec855cce66f44766697bc2" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-big-mod-exp" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" +dependencies = [ + "num-bigint", + "num-traits", + "solana-define-syscall", +] + +[[package]] +name = "solana-bincode" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" +dependencies = [ + "bincode", + "serde", + "solana-instruction", +] + +[[package]] +name = "solana-blake3-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" +dependencies = [ + "blake3", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + +[[package]] +name = "solana-borsh" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" +dependencies = [ + "borsh 0.10.4", + "borsh 1.5.7", +] + +[[package]] +name = "solana-clock" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb482ab70fced82ad3d7d3d87be33d466a3498eb8aa856434ff3c0dfc2e2e31" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-cpi" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-stable-layout", +] + +[[package]] +name = "solana-decode-error" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35" +dependencies = [ + "num-traits", +] + +[[package]] +name = "solana-define-syscall" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" + +[[package]] +name = "solana-epoch-rewards" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b575d3dd323b9ea10bb6fe89bf6bf93e249b215ba8ed7f68f1a3633f384db7" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fce071fbddecc55d727b1d7ed16a629afe4f6e4c217bc8d00af3b785f6f67ed" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-example-mocks" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" +dependencies = [ + "serde", + "serde_derive", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-hash", + "solana-instruction", + "solana-keccak-hasher", + "solana-message", + "solana-nonce", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "thiserror 2.0.17", +] + +[[package]] +name = "solana-feature-gate-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" +dependencies = [ + "bincode", + "serde", + "serde_derive", + "solana-account", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-fee-calculator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89bc408da0fb3812bc3008189d148b4d3e08252c79ad810b245482a3f70cd8d" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hash" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" +dependencies = [ + "borsh 1.5.7", + "bytemuck", + "bytemuck_derive", + "five8", + "js-sys", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", + "wasm-bindgen", +] + +[[package]] +name = "solana-instruction" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47298e2ce82876b64f71e9d13a46bc4b9056194e7f9937ad3084385befa50885" +dependencies = [ + "bincode", + "borsh 1.5.7", + "getrandom 0.2.16", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-define-syscall", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e85a6fad5c2d0c4f5b91d34b8ca47118fc593af706e523cdbedf846a954f57" +dependencies = [ + "bitflags", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-keccak-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" +dependencies = [ + "sha3", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + +[[package]] +name = "solana-last-restart-slot" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6360ac2fdc72e7463565cd256eedcf10d7ef0c28a1249d261ec168c1b55cdd" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-loader-v2-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-loader-v4-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-message" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" +dependencies = [ + "bincode", + "blake3", + "lazy_static", + "serde", + "serde_derive", + "solana-bincode", + "solana-hash", + "solana-instruction", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-system-interface", + "solana-transaction-error", + "wasm-bindgen", +] + +[[package]] +name = "solana-msg" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-native-token" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" + +[[package]] +name = "solana-nonce" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" +dependencies = [ + "serde", + "serde_derive", + "solana-fee-calculator", + "solana-hash", + "solana-pubkey", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-program" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" +dependencies = [ + "bincode", + "blake3", + "borsh 0.10.4", + "borsh 1.5.7", + "bs58", + "bytemuck", + "console_error_panic_hook", + "console_log", + "getrandom 0.2.16", + "lazy_static", + "log", + "memoffset", + "num-bigint", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info", + "solana-address-lookup-table-interface", + "solana-atomic-u64", + "solana-big-mod-exp", + "solana-bincode", + "solana-blake3-hasher", + "solana-borsh", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-example-mocks", + "solana-feature-gate-interface", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-keccak-hasher", + "solana-last-restart-slot", + "solana-loader-v2-interface", + "solana-loader-v3-interface", + "solana-loader-v4-interface", + "solana-message", + "solana-msg", + "solana-native-token", + "solana-nonce", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "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-stake-interface", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", + "solana-vote-interface", + "thiserror 2.0.17", + "wasm-bindgen", +] + +[[package]] +name = "solana-program-entrypoint" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ce041b1a0ed275290a5008ee1a4a6c48f5054c8a3d78d313c08958a06aedbd" +dependencies = [ + "solana-account-info", + "solana-msg", + "solana-program-error", + "solana-pubkey", +] + +[[package]] +name = "solana-program-error" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" +dependencies = [ + "borsh 1.5.7", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-pubkey", +] + +[[package]] +name = "solana-program-memory" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5426090c6f3fd6cfdc10685322fede9ca8e5af43cd6a59e98bfe4e91671712" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-program-option" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" + +[[package]] +name = "solana-program-pack" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-pubkey" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" +dependencies = [ + "borsh 0.10.4", + "borsh 1.5.7", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8", + "five8_const", + "getrandom 0.2.16", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-decode-error", + "solana-define-syscall", + "solana-sanitize", + "solana-sha256-hasher", + "wasm-bindgen", +] + +[[package]] +name = "solana-rent" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sanitize" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" + +[[package]] +name = "solana-sdk-ids" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +dependencies = [ + "solana-pubkey", +] + +[[package]] +name = "solana-sdk-macro" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "solana-secp256k1-recover" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" +dependencies = [ + "libsecp256k1", + "solana-define-syscall", + "thiserror 2.0.17", +] + +[[package]] +name = "solana-serde-varint" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7e155eba458ecfb0107b98236088c3764a09ddf0201ec29e52a0be40857113" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serialize-utils" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +dependencies = [ + "solana-instruction", + "solana-pubkey", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" +dependencies = [ + "sha2 0.10.9", + "solana-define-syscall", + "solana-hash", +] + +[[package]] +name = "solana-short-vec" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c54c66f19b9766a56fa0057d060de8378676cb64987533fa088861858fc5a69" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-slot-hashes" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + +[[package]] +name = "solana-stake-interface" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +dependencies = [ + "borsh 0.10.4", + "borsh 1.5.7", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-system-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-system-interface" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +dependencies = [ + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-sysvar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +dependencies = [ + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" +dependencies = [ + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-transaction-error" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" +dependencies = [ + "solana-instruction", + "solana-sanitize", +] + +[[package]] +name = "solana-vote-interface" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" +dependencies = [ + "bincode", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-decode-error", + "solana-hash", + "solana-instruction", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-serde-varint", + "solana-serialize-utils", + "solana-short-vec", + "solana-system-interface", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime 0.7.3", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.108", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/packages/svm/settler/Cargo.toml b/packages/svm/settler/Cargo.toml new file mode 100644 index 0000000..f397704 --- /dev/null +++ b/packages/svm/settler/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = [ + "programs/*" +] +resolver = "2" + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/packages/svm/settler/package.json b/packages/svm/settler/package.json new file mode 100644 index 0000000..a74470d --- /dev/null +++ b/packages/svm/settler/package.json @@ -0,0 +1,30 @@ +{ + "name": "@mimicprotocol/settler", + "private": true, + "version": "0.0.1", + "license": "GPL-3.0", + "type": "module", + "scripts": { + "build": "anchor build", + "test": "NODE_ENV=test ts-mocha -r ./tests/global-hooks.ts ./tests --recursive --extension .test.ts --exit", + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@solana/spl-token": "^0.4.13", + "anchor-litesvm": "=0.1.0", + "litesvm": "=0.1.0" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.20", + "@types/mocha": "^10.0.10", + "@types/node": "^22.15.18", + "chai": "^5.2.0", + "mocha": "^11.2.2", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "typescript": "~5.5.0" + } +} diff --git a/packages/svm/settler/programs/settler/Cargo.toml b/packages/svm/settler/programs/settler/Cargo.toml new file mode 100644 index 0000000..8c46639 --- /dev/null +++ b/packages/svm/settler/programs/settler/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "settler" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "settler" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] + + +[dependencies] +anchor-lang = "0.31.1" diff --git a/packages/svm/settler/programs/settler/Xargo.toml b/packages/svm/settler/programs/settler/Xargo.toml new file mode 100644 index 0000000..475fb71 --- /dev/null +++ b/packages/svm/settler/programs/settler/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/packages/svm/settler/programs/settler/src/lib.rs b/packages/svm/settler/programs/settler/src/lib.rs new file mode 100644 index 0000000..59630fe --- /dev/null +++ b/packages/svm/settler/programs/settler/src/lib.rs @@ -0,0 +1,18 @@ +#![allow(unexpected_cfgs)] + +use anchor_lang::prelude::*; + +declare_id!("HbNt35Ng8aM4NUy39evpCQqXEC4Nmaq16ewY8dyNF6NF"); + +#[program] +pub mod settler { + use super::*; + + pub fn initialize(ctx: Context) -> Result<()> { + msg!("Greetings from: {:?}", ctx.program_id); + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize {} diff --git a/packages/svm/settler/tests/global-hooks.ts b/packages/svm/settler/tests/global-hooks.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/svm/settler/tests/settler.test.ts b/packages/svm/settler/tests/settler.test.ts new file mode 100644 index 0000000..1b9c1cd --- /dev/null +++ b/packages/svm/settler/tests/settler.test.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { Program, Wallet } from '@coral-xyz/anchor' +import { Keypair } from '@solana/web3.js' +import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' +import { expect } from 'chai' +import { Settler } from '../target/types/settler' +import * as SettlerIDL from '../target/idl/settler.json' +import path from 'path' + +import { extractLogs } from './utils' + +describe('Settler Program', () => { + let client: any + let provider: LiteSVMProvider + let admin: Keypair + let malicious: Keypair + let program: Program + + before(async () => { + admin = Keypair.generate() + malicious = Keypair.generate() + + client = fromWorkspace(path.join(__dirname, '../')).withBuiltins() + + provider = new LiteSVMProvider(client, new Wallet(admin)) + program = new Program(SettlerIDL as any, provider) + + // Airdrop initial lamports + provider.client.airdrop(admin.publicKey, BigInt(100_000_000_000)) + provider.client.airdrop(malicious.publicKey, BigInt(100_000_000_000)) + }) + + describe('Settler', () => { + it('should call initialize', async () => { + const tx = await program.methods.initialize().transaction() + tx.recentBlockhash = provider.client.latestBlockhash() + tx.feePayer = admin.publicKey + tx.sign(admin) + const res = provider.client.sendTransaction(tx) + + expect(extractLogs(res.toString()).join('').includes(`Greetings from: ${program.programId.toString()}`)).to.be.ok + }) + }) +}) diff --git a/packages/svm/settler/tests/utils.ts b/packages/svm/settler/tests/utils.ts new file mode 100644 index 0000000..60c2f50 --- /dev/null +++ b/packages/svm/settler/tests/utils.ts @@ -0,0 +1,95 @@ +import { + Account as SPLAccount, + createAssociatedTokenAccountIdempotentInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + MINT_SIZE, + TOKEN_PROGRAM_ID, + unpackAccount, +} from '@solana/spl-token' +import { AccountInfo, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction } from '@solana/web3.js' +import { LiteSVMProvider } from 'anchor-litesvm' +import { AccountInfoBytes, Clock } from 'litesvm' + +export function extractLogs(liteSvmTxMetadataString: string): string[] { + const logsMatch = liteSvmTxMetadataString.match(/logs: \[(.*?)\],/s) + if (!logsMatch) return [] + + return logsMatch[1].split('", "') +} + +export function getAtaBalance(provider: LiteSVMProvider, address: PublicKey): number { + const ata = getAndUnpackAta(provider, address) + return Number(ata?.amount ?? 0) +} + +export function getAndUnpackAta(provider: LiteSVMProvider, address: PublicKey): SPLAccount | null { + const account = provider.client.getAccount(address) + if (!account) return null + return unpackAccount(address, { + ...account, + data: Buffer.from(account?.data), + }) +} + +export function toAccountInfo(stuff: AccountInfoBytes | null): AccountInfo | null { + return stuff + ? { + executable: stuff.executable, + data: Buffer.from(stuff.data), + lamports: stuff.lamports, + owner: stuff.owner, + rentEpoch: stuff.rentEpoch, + } + : null +} + +export async function createMint(provider: LiteSVMProvider, mintAuthority: Keypair, decimals = 9): Promise { + const mint = Keypair.generate() + const lamports = provider.client.minimumBalanceForRentExemption(BigInt(MINT_SIZE)) + const initAccountIx = SystemProgram.createAccount({ + fromPubkey: mintAuthority.publicKey, + newAccountPubkey: mint.publicKey, + space: MINT_SIZE, + lamports: Number(lamports), + programId: TOKEN_PROGRAM_ID, + }) + const mintIx = createInitializeMint2Instruction(mint.publicKey, decimals, mintAuthority.publicKey, null) + const tx = new Transaction().add(initAccountIx).add(mintIx) + tx.recentBlockhash = provider.client.latestBlockhash() + tx.feePayer = mintAuthority.publicKey + tx.sign(mintAuthority, mint) + provider.client.sendTransaction(tx) + + return mint.publicKey +} + +export function createMintTokensToIxs( + mintAuthority: PublicKey, + mints: { mint: PublicKey; authority: PublicKey }[], + amount: number +): TransactionInstruction[] { + return mints + .map(({ mint, authority }) => { + const ata = getAssociatedTokenAddressSync(mint, authority, true) + return [ + createAssociatedTokenAccountIdempotentInstruction(mintAuthority, ata, authority, mint), + createMintToInstruction(mint, ata, mintAuthority, amount), + ] + }) + .flat() +} + +export function warpSeconds(provider: LiteSVMProvider, seconds: number): void { + const clock = provider.client.getClock() + provider.client.setClock( + new Clock( + clock.slot, + clock.epochStartTimestamp, + clock.epoch, + clock.leaderScheduleEpoch, + clock.unixTimestamp + BigInt(seconds) + ) + ) +} diff --git a/packages/svm/settler/tsconfig.json b/packages/svm/settler/tsconfig.json new file mode 100644 index 0000000..3c9280f --- /dev/null +++ b/packages/svm/settler/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "target": "es2019", + "module": "commonjs", + "esModuleInterop": true, + "moduleResolution": "node", + "sourceMap": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "types": ["node", "mocha", "chai"] + } +} diff --git a/yarn.lock b/yarn.lock index cdce89e..4c486cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43,6 +43,38 @@ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== +"@coral-xyz/anchor-errors@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.31.1.tgz#d635cbac2533973ae6bfb5d3ba1de89ce5aece2d" + integrity sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ== + +"@coral-xyz/anchor@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.31.1.tgz#0fdeebf45a3cb2e47e8ebbb815ca98542152962c" + integrity sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA== + dependencies: + "@coral-xyz/anchor-errors" "^0.31.1" + "@coral-xyz/borsh" "^0.31.1" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.69.0" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + eventemitter3 "^4.0.7" + pako "^2.0.3" + superstruct "^0.15.4" + toml "^3.0.0" + +"@coral-xyz/borsh@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.31.1.tgz#5328e1e0921b75d7f4a62dd3f61885a938bc7241" + integrity sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + "@esbuild/aix-ppc64@0.25.4": version "0.25.4" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz#830d6476cbbca0c005136af07303646b419f1162" @@ -385,6 +417,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@iarna/toml@^2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" + integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== + "@ignored/edr-optimism-darwin-arm64@0.13.0-alpha.6": version "0.13.0-alpha.6" resolved "https://registry.yarnpkg.com/@ignored/edr-optimism-darwin-arm64/-/edr-optimism-darwin-arm64-0.13.0-alpha.6.tgz#3a388062840ea6f7e8c16152c0a9624b1eddedeb" @@ -509,7 +546,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.2.tgz#d53c65a21658fb02f3303e7ee3ba89d6754c64b4" integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ== -"@noble/hashes@1.8.0", "@noble/hashes@^1.4.0": +"@noble/hashes@1.8.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== @@ -789,13 +826,30 @@ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.19.0.tgz#b0c291494abd4fdefb8f77d200e381c3f7167c3f" integrity sha512-I41rKpMJHHZb0z0Nja+Lxto6IkEEmX3uWjnECypF8Z1HIjeJB0+PXl8p/7TeaKYqw2J2GYcRTg7jQZDmvKle1w== -"@solana/buffer-layout@^4.0.1": +"@solana/buffer-layout-utils@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" + integrity sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/web3.js" "^1.32.0" + bigint-buffer "^1.1.5" + bignumber.js "^9.0.1" + +"@solana/buffer-layout@^4.0.0", "@solana/buffer-layout@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== dependencies: buffer "~6.0.3" +"@solana/codecs-core@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz#1a2d76b9c7b9e7b7aeb3bd78be81c2ba21e3ce22" + integrity sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ== + dependencies: + "@solana/errors" "2.0.0-rc.1" + "@solana/codecs-core@2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.3.0.tgz#6bf2bb565cb1ae880f8018635c92f751465d8695" @@ -803,6 +857,23 @@ dependencies: "@solana/errors" "2.3.0" +"@solana/codecs-data-structures@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz#d47b2363d99fb3d643f5677c97d64a812982b888" + integrity sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-numbers@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz#f34978ddf7ea4016af3aaed5f7577c1d9869a614" + integrity sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + "@solana/codecs-numbers@^2.1.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz#ac7e7f38aaf7fcd22ce2061fbdcd625e73828dc6" @@ -811,6 +882,34 @@ "@solana/codecs-core" "2.3.0" "@solana/errors" "2.3.0" +"@solana/codecs-strings@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz#e1d9167075b8c5b0b60849f8add69c0f24307018" + integrity sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-rc.1.tgz#146dc5db58bd3c28e04b4c805e6096c2d2a0a875" + integrity sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/options" "2.0.0-rc.1" + +"@solana/errors@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-rc.1.tgz#3882120886eab98a37a595b85f81558861b29d62" + integrity sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ== + dependencies: + chalk "^5.3.0" + commander "^12.1.0" + "@solana/errors@2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.3.0.tgz#4ac9380343dbeffb9dffbcb77c28d0e457c5fa31" @@ -819,7 +918,43 @@ chalk "^5.4.1" commander "^14.0.0" -"@solana/web3.js@^1.98.4": +"@solana/options@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-rc.1.tgz#06924ba316dc85791fc46726a51403144a85fc4d" + integrity sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/spl-token-group@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz#83c00f0cd0bda33115468cd28b89d94f8ec1fee4" + integrity sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token-metadata@^0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz#d240947aed6e7318d637238022a7b0981b32ae80" + integrity sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token@^0.4.13": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.14.tgz#b86bc8a17f50e9680137b585eca5f5eb9d55c025" + integrity sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-group" "^0.0.7" + "@solana/spl-token-metadata" "^0.1.6" + buffer "^6.0.3" + +"@solana/web3.js@^1.32.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.69.0", "@solana/web3.js@^1.98.4": version "1.98.4" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.4.tgz#df51d78be9d865181ec5138b4e699d48e6895bbe" integrity sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw== @@ -881,6 +1016,13 @@ lodash "^4.17.15" ts-essentials "^7.0.1" +"@types/bn.js@^5.1.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.2.0.tgz#4349b9710e98f9ab3cdc50f1c5e4dcbd8ef29c80" + integrity sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q== + dependencies: + "@types/node" "*" + "@types/chai-as-promised@^8.0.1", "@types/chai-as-promised@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-8.0.2.tgz#5ac957d346fec5b686b62606baa5704787669704" @@ -1094,6 +1236,13 @@ ajv@^8.0.1: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" +anchor-litesvm@=0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/anchor-litesvm/-/anchor-litesvm-0.1.0.tgz#4ff90aa95f770e1af37eb134553f1049c43b6a3b" + integrity sha512-KOKNe6EjeErV367eU2w6eI/6wwZry0EbtnsmDs3gis98A5uH6CRYBMmLzOPDP75QNIztXKt3jeDNF42K3B6nhg== + dependencies: + "@iarna/toml" "^2.2.5" + ansi-colors@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -1231,6 +1380,11 @@ arraybuffer.prototype.slice@^1.0.4: get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== + assertion-error@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" @@ -1275,12 +1429,31 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +bigint-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" + integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== + dependencies: + bindings "^1.3.0" + +bignumber.js@^9.0.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== + +bindings@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + bn.js@^4.11.9: version "4.12.2" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.2.tgz#3d8fed6796c24e177737f7cc5172ee04ef39ec99" integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== -bn.js@^5.2.0, bn.js@^5.2.1: +bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: version "5.2.2" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.2.tgz#82c09f9ebbb17107cd72cb7fd39bd1f9d0aaa566" integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== @@ -1338,6 +1511,16 @@ bs58@^4.0.0, bs58@^4.0.1: dependencies: base-x "^3.0.2" +buffer-from@^1.0.0, buffer-from@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-layout@^1.2.0, buffer-layout@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" + integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== + buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -1384,7 +1567,7 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^6.0.0: +camelcase@^6.0.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -1509,6 +1692,11 @@ commander@^10.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@^14.0.0: version "14.0.1" resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.1.tgz#2f9225c19e6ebd0dc4404dd45821b2caa17ea09b" @@ -1541,6 +1729,13 @@ cron-parser@^5.3.1: dependencies: luxon "^3.7.1" +cross-fetch@^3.1.5: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3" + integrity sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q== + dependencies: + node-fetch "^2.7.0" + cross-spawn@^7.0.2, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1634,6 +1829,11 @@ delay@^5.0.0: resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + diff@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" @@ -2129,6 +2329,11 @@ ethers@^6.14.1: tslib "2.7.0" ws "8.17.1" +eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + eventemitter3@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" @@ -2199,6 +2404,11 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -2936,6 +3146,45 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +litesvm-darwin-arm64@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/litesvm-darwin-arm64/-/litesvm-darwin-arm64-0.1.0.tgz#8e0dc55ba2e0a8a4db2cbb418de5160105c31fb2" + integrity sha512-GwBph2fNaR9UP1nhFmQYk7UMcI9+ogrzpbDl2en70FUnPc+FDlchOts2X8IL5XFCU+m4ZGoQl/N9b+FywJN5lQ== + +litesvm-darwin-universal@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/litesvm-darwin-universal/-/litesvm-darwin-universal-0.1.0.tgz#176d6d083d526ea0681cf8505581b06896dccfbe" + integrity sha512-GjGpz77ei+RfWiMtHiES0X8GP+TafFHWu5fAQXgXEoGia7RG32Z12iJBhwfvk0T4EKbm8bqlyqhe5Me/nARm/w== + +litesvm-darwin-x64@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/litesvm-darwin-x64/-/litesvm-darwin-x64-0.1.0.tgz#3d2e45dc168460a16cb58db560f7e366eb35e88d" + integrity sha512-1N/IPfoT+gcpkvG9Wm7+vcaxgl/LB8hbm85cHDQRq4O1wyiRRiR6ByeNRamj+OFEDSfI7a/1NshhMobnnr2MGQ== + +litesvm-linux-x64-gnu@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/litesvm-linux-x64-gnu/-/litesvm-linux-x64-gnu-0.1.0.tgz#479a7bbffd105ab30ef52269de0b31f98f64f585" + integrity sha512-S6krvRz6BXxVZIkap5XkWwulSG4KsbFrxjyqP1X0zANa8jWMHEd09Zy9pPgfOetni2exg67fBmSlWq4sRfzlCw== + +litesvm-linux-x64-musl@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/litesvm-linux-x64-musl/-/litesvm-linux-x64-musl-0.1.0.tgz#73e2486c6272025b8c576de25ddbffb5a3155eb7" + integrity sha512-3E9gC5HRCEHFsfUNDhAY9JKx6ou6JazlnYMhT89JgbAN/YsJmyEgkTfY6W7TVV90g4EAJ/K4smSmg+vV413mYQ== + +litesvm@=0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/litesvm/-/litesvm-0.1.0.tgz#0b0a444bb65d7068a7d09c3b05536d40e93db2aa" + integrity sha512-XfpvWgYxFQUZxwFzWJTsuHRc1y34q8WtC60lhvH1xb6YX1/RBLqzIClp2JkxtlqGnDi/WPlThsXGmC3SinuPMQ== + dependencies: + "@solana/web3.js" "^1.68.0" + bs58 "^4.0.1" + optionalDependencies: + litesvm-darwin-arm64 "0.1.0" + litesvm-darwin-universal "0.1.0" + litesvm-darwin-x64 "0.1.0" + litesvm-linux-x64-gnu "0.1.0" + litesvm-linux-x64-musl "0.1.0" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -2991,6 +3240,11 @@ luxon@^3.7.1: resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" @@ -3066,6 +3320,13 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -3233,6 +3494,11 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== +pako@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -3331,7 +3597,7 @@ prettier-plugin-solidity@lkndjrt/prettier-plugin-solidity: solidity-comments-extractor "^0.0.7" string-width "^4.2.2" -prettier@^2.2.1, prettier@^2.3.1, prettier@^2.8.3: +prettier@^2.2.1, prettier@^2.3.1, prettier@^2.6.2, prettier@^2.8.3: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -3701,6 +3967,19 @@ solidity-comments-extractor@^0.0.7: resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz#99d8f1361438f84019795d928b931f4e5c39ca19" integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== +source-map-support@^0.5.6: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + split2@^3.0.0: version "3.2.2" resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" @@ -3730,7 +4009,16 @@ string-format@^2.0.0: resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3787,7 +4075,14 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -3811,6 +4106,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +superstruct@^0.15.4: + version "0.15.5" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab" + integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ== + superstruct@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" @@ -3887,6 +4187,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -3907,7 +4212,30 @@ ts-essentials@^7.0.1: resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== -tsconfig-paths@^3.15.0: +ts-mocha@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-10.1.0.tgz#17a1c055f5f7733fd82447c4420740db87221bc8" + integrity sha512-T0C0Xm3/WqCuF2tpa0GNGESTBoKZaiqdUP8guNv4ZY316AFXlyidnrzQ1LUrCT0Wb1i3J0zFTgOh/55Un44WdA== + dependencies: + ts-node "7.0.1" + optionalDependencies: + tsconfig-paths "^3.5.0" + +ts-node@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" + integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== + dependencies: + arrify "^1.0.0" + buffer-from "^1.1.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.6" + yn "^2.0.0" + +tsconfig-paths@^3.15.0, tsconfig-paths@^3.5.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== @@ -4192,7 +4520,16 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -4268,6 +4605,11 @@ yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" + integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From ba26d0ea006151c4fbe9bca57b8c9434bb5f360d Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 29 Oct 2025 13:04:06 -0300 Subject: [PATCH 02/41] Add CI file, rename svm/settler to svm/ --- .github/workflows/ci-svm.yml | 63 +++++++++++++++++++ packages/svm/{settler => }/.gitignore | 0 packages/svm/{settler => }/.prettierignore | 0 packages/svm/{settler => }/Anchor.toml | 0 packages/svm/{settler => }/Cargo.lock | 0 packages/svm/{settler => }/Cargo.toml | 0 packages/svm/{settler => }/package.json | 0 .../{settler => }/programs/settler/Cargo.toml | 0 .../{settler => }/programs/settler/Xargo.toml | 0 .../{settler => }/programs/settler/src/lib.rs | 0 .../svm/{settler => }/tests/global-hooks.ts | 0 .../svm/{settler => }/tests/settler.test.ts | 0 packages/svm/{settler => }/tests/utils.ts | 0 packages/svm/{settler => }/tsconfig.json | 0 14 files changed, 63 insertions(+) create mode 100644 .github/workflows/ci-svm.yml rename packages/svm/{settler => }/.gitignore (100%) rename packages/svm/{settler => }/.prettierignore (100%) rename packages/svm/{settler => }/Anchor.toml (100%) rename packages/svm/{settler => }/Cargo.lock (100%) rename packages/svm/{settler => }/Cargo.toml (100%) rename packages/svm/{settler => }/package.json (100%) rename packages/svm/{settler => }/programs/settler/Cargo.toml (100%) rename packages/svm/{settler => }/programs/settler/Xargo.toml (100%) rename packages/svm/{settler => }/programs/settler/src/lib.rs (100%) rename packages/svm/{settler => }/tests/global-hooks.ts (100%) rename packages/svm/{settler => }/tests/settler.test.ts (100%) rename packages/svm/{settler => }/tests/utils.ts (100%) rename packages/svm/{settler => }/tsconfig.json (100%) diff --git a/.github/workflows/ci-svm.yml b/.github/workflows/ci-svm.yml new file mode 100644 index 0000000..d7e6a6e --- /dev/null +++ b/.github/workflows/ci-svm.yml @@ -0,0 +1,63 @@ +name: CI SVM + +env: + CI: true + +on: + push: + branches: "*" + paths: + - packages/svm/** + pull_request: + branches: "*" + paths: + - packages/svm/** + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + - name: Install + working-directory: packages/svm + run: yarn + - name: Build + working-directory: packages/svm + run: yarn build + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + - name: Install + working-directory: packages/svm + run: yarn + - name: Build + working-directory: packages/svm + run: yarn lint + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + - name: Install + working-directory: packages/svm + run: yarn + - name: Build + working-directory: packages/svm + run: yarn build + - name: Test + working-directory: packages/svm + run: yarn test diff --git a/packages/svm/settler/.gitignore b/packages/svm/.gitignore similarity index 100% rename from packages/svm/settler/.gitignore rename to packages/svm/.gitignore diff --git a/packages/svm/settler/.prettierignore b/packages/svm/.prettierignore similarity index 100% rename from packages/svm/settler/.prettierignore rename to packages/svm/.prettierignore diff --git a/packages/svm/settler/Anchor.toml b/packages/svm/Anchor.toml similarity index 100% rename from packages/svm/settler/Anchor.toml rename to packages/svm/Anchor.toml diff --git a/packages/svm/settler/Cargo.lock b/packages/svm/Cargo.lock similarity index 100% rename from packages/svm/settler/Cargo.lock rename to packages/svm/Cargo.lock diff --git a/packages/svm/settler/Cargo.toml b/packages/svm/Cargo.toml similarity index 100% rename from packages/svm/settler/Cargo.toml rename to packages/svm/Cargo.toml diff --git a/packages/svm/settler/package.json b/packages/svm/package.json similarity index 100% rename from packages/svm/settler/package.json rename to packages/svm/package.json diff --git a/packages/svm/settler/programs/settler/Cargo.toml b/packages/svm/programs/settler/Cargo.toml similarity index 100% rename from packages/svm/settler/programs/settler/Cargo.toml rename to packages/svm/programs/settler/Cargo.toml diff --git a/packages/svm/settler/programs/settler/Xargo.toml b/packages/svm/programs/settler/Xargo.toml similarity index 100% rename from packages/svm/settler/programs/settler/Xargo.toml rename to packages/svm/programs/settler/Xargo.toml diff --git a/packages/svm/settler/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs similarity index 100% rename from packages/svm/settler/programs/settler/src/lib.rs rename to packages/svm/programs/settler/src/lib.rs diff --git a/packages/svm/settler/tests/global-hooks.ts b/packages/svm/tests/global-hooks.ts similarity index 100% rename from packages/svm/settler/tests/global-hooks.ts rename to packages/svm/tests/global-hooks.ts diff --git a/packages/svm/settler/tests/settler.test.ts b/packages/svm/tests/settler.test.ts similarity index 100% rename from packages/svm/settler/tests/settler.test.ts rename to packages/svm/tests/settler.test.ts diff --git a/packages/svm/settler/tests/utils.ts b/packages/svm/tests/utils.ts similarity index 100% rename from packages/svm/settler/tests/utils.ts rename to packages/svm/tests/utils.ts diff --git a/packages/svm/settler/tsconfig.json b/packages/svm/tsconfig.json similarity index 100% rename from packages/svm/settler/tsconfig.json rename to packages/svm/tsconfig.json From 6df2013d4cce2eeabaaff99b33695d070625f009 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 30 Oct 2025 12:12:47 -0300 Subject: [PATCH 03/41] Configure CI --- .github/workflows/ci-evm.yml | 63 -- .github/workflows/ci-svm.yml | 63 -- .github/workflows/ci.yml | 109 +++ packages/svm/.prettierrc.cjs | 7 + packages/svm/Cargo.lock | 805 +++-------------------- packages/svm/package.json | 21 +- packages/svm/programs/settler/Cargo.toml | 2 +- packages/svm/tests/settler.test.ts | 8 +- yarn.lock | 8 +- 9 files changed, 215 insertions(+), 871 deletions(-) delete mode 100644 .github/workflows/ci-evm.yml delete mode 100644 .github/workflows/ci-svm.yml create mode 100644 .github/workflows/ci.yml create mode 100644 packages/svm/.prettierrc.cjs diff --git a/.github/workflows/ci-evm.yml b/.github/workflows/ci-evm.yml deleted file mode 100644 index 8964dba..0000000 --- a/.github/workflows/ci-evm.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: CI EVM - -env: - CI: true - -on: - push: - branches: "*" - paths: - - packages/evm/** - pull_request: - branches: "*" - paths: - - packages/evm/** - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version-file: '.nvmrc' - - name: Install - working-directory: packages/evm - run: yarn - - name: Build - working-directory: packages/evm - run: yarn build - - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version-file: '.nvmrc' - - name: Install - working-directory: packages/evm - run: yarn - - name: Build - working-directory: packages/evm - run: yarn lint - - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version-file: '.nvmrc' - - name: Install - working-directory: packages/evm - run: yarn - - name: Build - working-directory: packages/evm - run: yarn build - - name: Test - working-directory: packages/evm - run: yarn test diff --git a/.github/workflows/ci-svm.yml b/.github/workflows/ci-svm.yml deleted file mode 100644 index d7e6a6e..0000000 --- a/.github/workflows/ci-svm.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: CI SVM - -env: - CI: true - -on: - push: - branches: "*" - paths: - - packages/svm/** - pull_request: - branches: "*" - paths: - - packages/svm/** - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version-file: ".nvmrc" - - name: Install - working-directory: packages/svm - run: yarn - - name: Build - working-directory: packages/svm - run: yarn build - - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version-file: ".nvmrc" - - name: Install - working-directory: packages/svm - run: yarn - - name: Build - working-directory: packages/svm - run: yarn lint - - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version-file: ".nvmrc" - - name: Install - working-directory: packages/svm - run: yarn - - name: Build - working-directory: packages/svm - run: yarn build - - name: Test - working-directory: packages/svm - run: yarn test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f2ed729 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,109 @@ +name: CI + +env: + CI: true + +on: + push: + branches: "*" + paths: + - packages/** + pull_request: + types: [opened, synchronize, reopened] + paths: + - packages/** + +jobs: + find-changed-packages: + runs-on: ubuntu-22.04 + outputs: + packages: ${{ steps.write-output.outputs.packages }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Find changed packages + id: changed-packages + uses: tj-actions/changed-files@v39 + with: + files: packages/** + dir_names: true + - name: Write output + id: write-output + run: | + PACKAGES=$(echo ${{ steps.changed-packages.outputs.all_changed_files }} | tr -s ' ' '\n' | grep -oP '(?<=packages/)([\w-]*)' | sort --unique | paste -sd ' ' | sed 's/\//-/g') + LIST="[\"$(echo ${PACKAGES// /\", \"})\"]" + echo "List: $LIST" + echo "packages={\"package\":$LIST}" >> "$GITHUB_OUTPUT" + + lint: + runs-on: ubuntu-22.04 + needs: find-changed-packages + strategy: + matrix: ${{fromJson(needs.find-changed-packages.outputs.packages)}} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + - name: Install + run: yarn workspace @mimicprotocol/contracts-${{ matrix.package }} install + - name: Lint + run: yarn workspace @mimicprotocol/contracts-${{ matrix.package }} lint + + test-evm: + runs-on: ubuntu-latest + needs: find-changed-packages + if: ${{ contains(needs.find-changed-packages.outputs.packages, 'evm') }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + - name: Install + run: yarn workspace @mimicprotocol/contracts-evm install + - name: Build + run: yarn workspace @mimicprotocol/contracts-evm build + - name: Test + run: yarn workspace @mimicprotocol/contracts-evm test + + test-solana: + runs-on: ubuntu-22.04 + needs: find-changed-packages + if: ${{ contains(needs.find-changed-packages.outputs.packages, 'svm') }} + container: solanafoundation/anchor:v0.32.1 + steps: + - name: Print versions + run: | + echo "Node.js version: $(node -v)" + echo "Yarn version: $(yarn -v)" + echo "Solana version: $(solana --version)" + echo "Anchor version: $(anchor --version)" + echo "LiteSVM version: $(litesvm --version)" + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.15.1" + - name: Set Up Solana Test Keypair + run: solana-keygen new --no-bip39-passphrase --silent + - name: Install + working-directory: packages/svm + run: yarn + - name: Pre-clean caches + working-directory: packages/svm + run: | + yarn cache clean || true + rm -rf ~/.cargo/registry ~/.cargo/git ~/.cache || true + - name: Build Anchor Program + working-directory: packages/svm + run: | + rustup default stable + DEPLOYER_KEY=$(solana-keygen pubkey) anchor build + - name: Test + working-directory: packages/svm + run: | + yarn test diff --git a/packages/svm/.prettierrc.cjs b/packages/svm/.prettierrc.cjs new file mode 100644 index 0000000..8907c02 --- /dev/null +++ b/packages/svm/.prettierrc.cjs @@ -0,0 +1,7 @@ +const ts = require('eslint-config-mimic/prettier') + +module.exports = { + overrides: [ + ts, + ] +} diff --git a/packages/svm/Cargo.lock b/packages/svm/Cargo.lock index e1452ea..66d20c5 100644 --- a/packages/svm/Cargo.lock +++ b/packages/svm/Cargo.lock @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "anchor-attribute-access-control" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f70fd141a4d18adf11253026b32504f885447048c7494faf5fa83b01af9c0cf" +checksum = "7a883ca44ef14b2113615fc6d3a85fefc68b5002034e88db37f7f1f802f88aa9" dependencies = [ "anchor-syn", "proc-macro2", @@ -37,9 +37,9 @@ dependencies = [ [[package]] name = "anchor-attribute-account" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715a261c57c7679581e06f07a74fa2af874ac30f86bd8ea07cca4a7e5388a064" +checksum = "61c4d97763b29030412b4b80715076377edc9cc63bc3c9e667297778384b9fd2" dependencies = [ "anchor-syn", "bs58", @@ -50,9 +50,9 @@ dependencies = [ [[package]] name = "anchor-attribute-constant" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730d6df8ae120321c5c25e0779e61789e4b70dc8297102248902022f286102e4" +checksum = "aae3328bbf9bbd517a51621b1ba6cbec06cbbc25e8cfc7403bddf69bcf088206" dependencies = [ "anchor-syn", "quote", @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "anchor-attribute-error" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e6e449cc3a37b2880b74dcafb8e5a17b954c0e58e376432d7adc646fb333ef" +checksum = "cf2398a6d9e16df1ee9d7d37d970a8246756de898c8dd16ef6bdbe4da20cf39a" dependencies = [ "anchor-syn", "quote", @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "anchor-attribute-event" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7710e4c54adf485affcd9be9adec5ef8846d9c71d7f31e16ba86ff9fc1dd49f" +checksum = "f12758f4ec2f0e98d4d56916c6fe95cb23d74b8723dd902c762c5ef46ebe7b65" dependencies = [ "anchor-syn", "proc-macro2", @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "anchor-attribute-program" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ecfd49b2aeadeb32f35262230db402abed76ce87e27562b34f61318b2ec83c" +checksum = "8c7193b5af2649813584aae6e3569c46fd59616a96af2083c556b13136c3830f" dependencies = [ "anchor-lang-idl", "anchor-syn", @@ -101,9 +101,9 @@ dependencies = [ [[package]] name = "anchor-derive-accounts" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be89d160793a88495af462a7010b3978e48e30a630c91de47ce2c1d3cb7a6149" +checksum = "d332d1a13c0fca1a446de140b656e66110a5e8406977dcb6a41e5d6f323760b0" dependencies = [ "anchor-syn", "quote", @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "anchor-derive-serde" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abc6ee78acb7bfe0c2dd2abc677aaa4789c0281a0c0ef01dbf6fe85e0fd9e6e4" +checksum = "8656e4af182edaeae665fa2d2d7ee81148518b5bd0be9a67f2a381bb17da7d46" dependencies = [ "anchor-syn", "borsh-derive-internal", @@ -125,9 +125,9 @@ dependencies = [ [[package]] name = "anchor-derive-space" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134a01c0703f6fd355a0e472c033f6f3e41fac1ef6e370b20c50f4c8d022cea7" +checksum = "dcff2a083560cd79817db07d89a4de39a2c4b2eaa00c1742cf0df49b25ff2bed" dependencies = [ "proc-macro2", "quote", @@ -136,9 +136,9 @@ dependencies = [ [[package]] name = "anchor-lang" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6bab117055905e930f762c196e08f861f8dfe7241b92cee46677a3b15561a0a" +checksum = "e67d85d5376578f12d840c29ff323190f6eecd65b00a0b5f2b2f232751d049cc" dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", @@ -154,8 +154,27 @@ dependencies = [ "bincode", "borsh 0.10.4", "bytemuck", - "solana-program", - "thiserror 1.0.69", + "solana-account-info", + "solana-clock", + "solana-cpi", + "solana-define-syscall", + "solana-feature-gate-interface", + "solana-instruction", + "solana-instructions-sysvar", + "solana-invoke", + "solana-loader-v3-interface", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", + "thiserror", ] [[package]] @@ -170,7 +189,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha2 0.10.9", + "sha2", ] [[package]] @@ -185,9 +204,9 @@ dependencies = [ [[package]] name = "anchor-syn" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc7a6d90cc643df0ed2744862cdf180587d1e5d28936538c18fc8908489ed67" +checksum = "b93b69aa7d099b59378433f6d7e20e1008fc10c69e48b220270e5b3f2ec4c8be" dependencies = [ "anyhow", "bs58", @@ -197,9 +216,9 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "syn 1.0.109", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -208,30 +227,12 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.21.7" @@ -259,29 +260,6 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "blake3" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "digest 0.10.7", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -389,6 +367,9 @@ name = "bytemuck" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] [[package]] name = "bytemuck_derive" @@ -411,16 +392,6 @@ dependencies = [ "toml 0.8.23", ] -[[package]] -name = "cc" -version = "1.2.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" -dependencies = [ - "find-msvc-tools", - "shlex", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -433,32 +404,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "console_log" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" -dependencies = [ - "log", - "web-sys", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -468,12 +413,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.6" @@ -493,9 +432,9 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest 0.10.7", + "digest", "fiat-crypto", - "rand_core 0.6.4", + "rand_core", "rustc_version", "subtle", "zeroize", @@ -512,24 +451,14 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -550,12 +479,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "find-msvc-tools" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" - [[package]] name = "five8" version = "0.2.1" @@ -590,17 +513,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.16" @@ -610,7 +522,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -625,9 +537,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "heck" @@ -640,12 +552,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.15.5", ] [[package]] @@ -664,15 +576,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keccak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" -dependencies = [ - "cpufeatures", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -685,52 +588,6 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" -[[package]] -name = "libsecp256k1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" -dependencies = [ - "arrayref", - "base64 0.12.3", - "digest 0.9.0", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", - "rand 0.7.3", - "serde", - "sha2 0.9.9", -] - -[[package]] -name = "libsecp256k1-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" -dependencies = [ - "libsecp256k1-core", -] - [[package]] name = "lock_api" version = "0.4.14" @@ -752,45 +609,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -806,12 +624,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "parking_lot" version = "0.12.5" @@ -835,15 +647,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -859,7 +662,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.7", + "toml_edit 0.23.2", ] [[package]] @@ -880,76 +683,11 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] [[package]] name = "redox_syscall" @@ -1091,19 +829,6 @@ dependencies = [ "anchor-lang", ] -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" version = "0.10.9" @@ -1112,74 +837,26 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest 0.10.7", - "keccak", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "solana-account" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f949fe4edaeaea78c844023bfc1c898e0b1f5a100f8a8d2d0f85d0a7b090258" -dependencies = [ - "solana-account-info", - "solana-clock", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", -] - [[package]] name = "solana-account-info" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" dependencies = [ - "bincode", - "serde", "solana-program-error", "solana-program-memory", "solana-pubkey", ] -[[package]] -name = "solana-address-lookup-table-interface" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" -dependencies = [ - "bincode", - "bytemuck", - "serde", - "serde_derive", - "solana-clock", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", - "solana-slot-hashes", -] - [[package]] name = "solana-atomic-u64" version = "2.2.1" @@ -1189,50 +866,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "solana-big-mod-exp" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" -dependencies = [ - "num-bigint", - "num-traits", - "solana-define-syscall", -] - -[[package]] -name = "solana-bincode" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" -dependencies = [ - "bincode", - "serde", - "solana-instruction", -] - -[[package]] -name = "solana-blake3-hasher" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" -dependencies = [ - "blake3", - "solana-define-syscall", - "solana-hash", - "solana-sanitize", -] - -[[package]] -name = "solana-borsh" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" -dependencies = [ - "borsh 0.10.4", - "borsh 1.5.7", -] - [[package]] name = "solana-clock" version = "2.2.2" @@ -1302,44 +935,14 @@ dependencies = [ "solana-sysvar-id", ] -[[package]] -name = "solana-example-mocks" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" -dependencies = [ - "serde", - "serde_derive", - "solana-address-lookup-table-interface", - "solana-clock", - "solana-hash", - "solana-instruction", - "solana-keccak-hasher", - "solana-message", - "solana-nonce", - "solana-pubkey", - "solana-sdk-ids", - "solana-system-interface", - "thiserror 2.0.17", -] - [[package]] name = "solana-feature-gate-interface" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" dependencies = [ - "bincode", - "serde", - "serde_derive", - "solana-account", - "solana-account-info", - "solana-instruction", - "solana-program-error", "solana-pubkey", - "solana-rent", "solana-sdk-ids", - "solana-system-interface", ] [[package]] @@ -1359,7 +962,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" dependencies = [ - "borsh 1.5.7", "bytemuck", "bytemuck_derive", "five8", @@ -1378,8 +980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47298e2ce82876b64f71e9d13a46bc4b9056194e7f9937ad3084385befa50885" dependencies = [ "bincode", - "borsh 1.5.7", - "getrandom 0.2.16", + "getrandom", "js-sys", "num-traits", "serde", @@ -1407,15 +1008,16 @@ dependencies = [ ] [[package]] -name = "solana-keccak-hasher" -version = "2.2.1" +name = "solana-invoke" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" +checksum = "58f5693c6de226b3626658377168b0184e94e8292ff16e3d31d4766e65627565" dependencies = [ - "sha3", + "solana-account-info", "solana-define-syscall", - "solana-hash", - "solana-sanitize", + "solana-instruction", + "solana-program-entrypoint", + "solana-stable-layout", ] [[package]] @@ -1431,25 +1033,11 @@ dependencies = [ "solana-sysvar-id", ] -[[package]] -name = "solana-loader-v2-interface" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" -dependencies = [ - "serde", - "serde_bytes", - "serde_derive", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", -] - [[package]] name = "solana-loader-v3-interface" -version = "5.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" +checksum = "fa4be76cfa9afd84ca2f35ebc09f0da0f0092935ccdac0595d98447f259538c2" dependencies = [ "serde", "serde_bytes", @@ -1460,44 +1048,6 @@ dependencies = [ "solana-system-interface", ] -[[package]] -name = "solana-loader-v4-interface" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" -dependencies = [ - "serde", - "serde_bytes", - "serde_derive", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", - "solana-system-interface", -] - -[[package]] -name = "solana-message" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" -dependencies = [ - "bincode", - "blake3", - "lazy_static", - "serde", - "serde_derive", - "solana-bincode", - "solana-hash", - "solana-instruction", - "solana-pubkey", - "solana-sanitize", - "solana-sdk-ids", - "solana-short-vec", - "solana-system-interface", - "solana-transaction-error", - "wasm-bindgen", -] - [[package]] name = "solana-msg" version = "2.2.1" @@ -1507,106 +1057,6 @@ dependencies = [ "solana-define-syscall", ] -[[package]] -name = "solana-native-token" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" - -[[package]] -name = "solana-nonce" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" -dependencies = [ - "serde", - "serde_derive", - "solana-fee-calculator", - "solana-hash", - "solana-pubkey", - "solana-sha256-hasher", -] - -[[package]] -name = "solana-program" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" -dependencies = [ - "bincode", - "blake3", - "borsh 0.10.4", - "borsh 1.5.7", - "bs58", - "bytemuck", - "console_error_panic_hook", - "console_log", - "getrandom 0.2.16", - "lazy_static", - "log", - "memoffset", - "num-bigint", - "num-derive", - "num-traits", - "rand 0.8.5", - "serde", - "serde_bytes", - "serde_derive", - "solana-account-info", - "solana-address-lookup-table-interface", - "solana-atomic-u64", - "solana-big-mod-exp", - "solana-bincode", - "solana-blake3-hasher", - "solana-borsh", - "solana-clock", - "solana-cpi", - "solana-decode-error", - "solana-define-syscall", - "solana-epoch-rewards", - "solana-epoch-schedule", - "solana-example-mocks", - "solana-feature-gate-interface", - "solana-fee-calculator", - "solana-hash", - "solana-instruction", - "solana-instructions-sysvar", - "solana-keccak-hasher", - "solana-last-restart-slot", - "solana-loader-v2-interface", - "solana-loader-v3-interface", - "solana-loader-v4-interface", - "solana-message", - "solana-msg", - "solana-native-token", - "solana-nonce", - "solana-program-entrypoint", - "solana-program-error", - "solana-program-memory", - "solana-program-option", - "solana-program-pack", - "solana-pubkey", - "solana-rent", - "solana-sanitize", - "solana-sdk-ids", - "solana-sdk-macro", - "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-stake-interface", - "solana-system-interface", - "solana-sysvar", - "solana-sysvar-id", - "solana-vote-interface", - "thiserror 2.0.17", - "wasm-bindgen", -] - [[package]] name = "solana-program-entrypoint" version = "2.3.0" @@ -1627,8 +1077,6 @@ checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" dependencies = [ "borsh 1.5.7", "num-traits", - "serde", - "serde_derive", "solana-decode-error", "solana-instruction", "solana-msg", @@ -1672,7 +1120,7 @@ dependencies = [ "curve25519-dalek", "five8", "five8_const", - "getrandom 0.2.16", + "getrandom", "js-sys", "num-traits", "serde", @@ -1725,26 +1173,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "solana-secp256k1-recover" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" -dependencies = [ - "libsecp256k1", - "solana-define-syscall", - "thiserror 2.0.17", -] - -[[package]] -name = "solana-serde-varint" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a7e155eba458ecfb0107b98236088c3764a09ddf0201ec29e52a0be40857113" -dependencies = [ - "serde", -] - [[package]] name = "solana-serialize-utils" version = "2.2.1" @@ -1762,20 +1190,11 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" dependencies = [ - "sha2 0.10.9", + "sha2", "solana-define-syscall", "solana-hash", ] -[[package]] -name = "solana-short-vec" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c54c66f19b9766a56fa0057d060de8378676cb64987533fa088861858fc5a69" -dependencies = [ - "serde", -] - [[package]] name = "solana-slot-hashes" version = "2.2.1" @@ -1818,8 +1237,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" dependencies = [ - "borsh 0.10.4", - "borsh 1.5.7", "num-traits", "serde", "serde_derive", @@ -1857,8 +1274,6 @@ checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" dependencies = [ "base64 0.22.1", "bincode", - "bytemuck", - "bytemuck_derive", "lazy_static", "serde", "serde_derive", @@ -1896,40 +1311,6 @@ dependencies = [ "solana-sdk-ids", ] -[[package]] -name = "solana-transaction-error" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" -dependencies = [ - "solana-instruction", - "solana-sanitize", -] - -[[package]] -name = "solana-vote-interface" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" -dependencies = [ - "bincode", - "num-derive", - "num-traits", - "serde", - "serde_derive", - "solana-clock", - "solana-decode-error", - "solana-hash", - "solana-instruction", - "solana-pubkey", - "solana-rent", - "solana-sdk-ids", - "solana-serde-varint", - "solana-serialize-utils", - "solana-short-vec", - "solana-system-interface", -] - [[package]] name = "subtle" version = "2.6.1" @@ -1964,16 +1345,7 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl", ] [[package]] @@ -1987,17 +1359,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "tinyvec" version = "1.10.0" @@ -2068,9 +1429,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "d1dee9dc43ac2aaf7d3b774e2fba5148212bf2bd9374f4e50152ebe9afd03d42" dependencies = [ "indexmap", "toml_datetime 0.7.3", @@ -2117,12 +1478,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2174,16 +1529,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "web-sys" -version = "0.3.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "windows-link" version = "0.2.1" diff --git a/packages/svm/package.json b/packages/svm/package.json index a74470d..baeb65c 100644 --- a/packages/svm/package.json +++ b/packages/svm/package.json @@ -1,5 +1,5 @@ { - "name": "@mimicprotocol/settler", + "name": "@mimicprotocol/contracts-svm", "private": true, "version": "0.0.1", "license": "GPL-3.0", @@ -7,11 +7,11 @@ "scripts": { "build": "anchor build", "test": "NODE_ENV=test ts-mocha -r ./tests/global-hooks.ts ./tests --recursive --extension .test.ts --exit", - "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", - "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + "lint": "eslint . --ext .ts", + "lint:fix": "yarn lint --fix" }, "dependencies": { - "@coral-xyz/anchor": "^0.31.1", + "@coral-xyz/anchor": "^0.32.1", "@solana/spl-token": "^0.4.13", "anchor-litesvm": "=0.1.0", "litesvm": "=0.1.0" @@ -25,6 +25,15 @@ "mocha": "^11.2.2", "prettier": "^2.6.2", "ts-mocha": "^10.0.0", - "typescript": "~5.5.0" - } + "typescript": "~5.5.0", + "eslint": "^7.9.0", + "eslint-config-mimic": "^0.0.2" + }, + "eslintConfig": { + "extends": "eslint-config-mimic" + }, + "eslintIgnore": [ + "dist", + "**/target/*" + ] } diff --git a/packages/svm/programs/settler/Cargo.toml b/packages/svm/programs/settler/Cargo.toml index 8c46639..9ebb8c8 100644 --- a/packages/svm/programs/settler/Cargo.toml +++ b/packages/svm/programs/settler/Cargo.toml @@ -18,4 +18,4 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] -anchor-lang = "0.31.1" +anchor-lang = { version = "0.32.1", features = [ "init-if-needed" ] } diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 1b9c1cd..d5ff4ce 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -5,10 +5,10 @@ import { Program, Wallet } from '@coral-xyz/anchor' import { Keypair } from '@solana/web3.js' import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' -import { Settler } from '../target/types/settler' -import * as SettlerIDL from '../target/idl/settler.json' import path from 'path' +import * as SettlerIDL from '../target/idl/settler.json' +import { Settler } from '../target/types/settler' import { extractLogs } from './utils' describe('Settler Program', () => { @@ -21,9 +21,9 @@ describe('Settler Program', () => { before(async () => { admin = Keypair.generate() malicious = Keypair.generate() - + client = fromWorkspace(path.join(__dirname, '../')).withBuiltins() - + provider = new LiteSVMProvider(client, new Wallet(admin)) program = new Program(SettlerIDL as any, provider) diff --git a/yarn.lock b/yarn.lock index 4c486cb..154406d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -48,10 +48,10 @@ resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.31.1.tgz#d635cbac2533973ae6bfb5d3ba1de89ce5aece2d" integrity sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ== -"@coral-xyz/anchor@^0.31.1": - version "0.31.1" - resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.31.1.tgz#0fdeebf45a3cb2e47e8ebbb815ca98542152962c" - integrity sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA== +"@coral-xyz/anchor@^0.32.1": + version "0.32.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.32.1.tgz#a07440d9d267840f4f99f1493bd8ce7d7f128e57" + integrity sha512-zAyxFtfeje2FbMA1wzgcdVs7Hng/MijPKpRijoySPCicnvcTQs/+dnPZ/cR+LcXM9v9UYSyW81uRNYZtN5G4yg== dependencies: "@coral-xyz/anchor-errors" "^0.31.1" "@coral-xyz/borsh" "^0.31.1" From a9c9b1a374855a4181dfeec79092d4ba0ad6e07f Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 30 Oct 2025 12:29:03 -0300 Subject: [PATCH 04/41] Remove old commands --- packages/svm/Anchor.toml | 2 +- packages/svm/tests/utils.ts | 89 ------------------------------------- 2 files changed, 1 insertion(+), 90 deletions(-) diff --git a/packages/svm/Anchor.toml b/packages/svm/Anchor.toml index c93c0e0..4b0987b 100644 --- a/packages/svm/Anchor.toml +++ b/packages/svm/Anchor.toml @@ -16,4 +16,4 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] -test = "node --test --experimental-strip-types tests/settler.test.ts" +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/packages/svm/tests/utils.ts b/packages/svm/tests/utils.ts index 60c2f50..54c2529 100644 --- a/packages/svm/tests/utils.ts +++ b/packages/svm/tests/utils.ts @@ -1,95 +1,6 @@ -import { - Account as SPLAccount, - createAssociatedTokenAccountIdempotentInstruction, - createInitializeMint2Instruction, - createMintToInstruction, - getAssociatedTokenAddressSync, - MINT_SIZE, - TOKEN_PROGRAM_ID, - unpackAccount, -} from '@solana/spl-token' -import { AccountInfo, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction } from '@solana/web3.js' -import { LiteSVMProvider } from 'anchor-litesvm' -import { AccountInfoBytes, Clock } from 'litesvm' - export function extractLogs(liteSvmTxMetadataString: string): string[] { const logsMatch = liteSvmTxMetadataString.match(/logs: \[(.*?)\],/s) if (!logsMatch) return [] return logsMatch[1].split('", "') } - -export function getAtaBalance(provider: LiteSVMProvider, address: PublicKey): number { - const ata = getAndUnpackAta(provider, address) - return Number(ata?.amount ?? 0) -} - -export function getAndUnpackAta(provider: LiteSVMProvider, address: PublicKey): SPLAccount | null { - const account = provider.client.getAccount(address) - if (!account) return null - return unpackAccount(address, { - ...account, - data: Buffer.from(account?.data), - }) -} - -export function toAccountInfo(stuff: AccountInfoBytes | null): AccountInfo | null { - return stuff - ? { - executable: stuff.executable, - data: Buffer.from(stuff.data), - lamports: stuff.lamports, - owner: stuff.owner, - rentEpoch: stuff.rentEpoch, - } - : null -} - -export async function createMint(provider: LiteSVMProvider, mintAuthority: Keypair, decimals = 9): Promise { - const mint = Keypair.generate() - const lamports = provider.client.minimumBalanceForRentExemption(BigInt(MINT_SIZE)) - const initAccountIx = SystemProgram.createAccount({ - fromPubkey: mintAuthority.publicKey, - newAccountPubkey: mint.publicKey, - space: MINT_SIZE, - lamports: Number(lamports), - programId: TOKEN_PROGRAM_ID, - }) - const mintIx = createInitializeMint2Instruction(mint.publicKey, decimals, mintAuthority.publicKey, null) - const tx = new Transaction().add(initAccountIx).add(mintIx) - tx.recentBlockhash = provider.client.latestBlockhash() - tx.feePayer = mintAuthority.publicKey - tx.sign(mintAuthority, mint) - provider.client.sendTransaction(tx) - - return mint.publicKey -} - -export function createMintTokensToIxs( - mintAuthority: PublicKey, - mints: { mint: PublicKey; authority: PublicKey }[], - amount: number -): TransactionInstruction[] { - return mints - .map(({ mint, authority }) => { - const ata = getAssociatedTokenAddressSync(mint, authority, true) - return [ - createAssociatedTokenAccountIdempotentInstruction(mintAuthority, ata, authority, mint), - createMintToInstruction(mint, ata, mintAuthority, amount), - ] - }) - .flat() -} - -export function warpSeconds(provider: LiteSVMProvider, seconds: number): void { - const clock = provider.client.getClock() - provider.client.setClock( - new Clock( - clock.slot, - clock.epochStartTimestamp, - clock.epoch, - clock.leaderScheduleEpoch, - clock.unixTimestamp + BigInt(seconds) - ) - ) -} From e92718d06c0f0d99a6b254fff006fa4c12d92c8e Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 30 Oct 2025 12:29:51 -0300 Subject: [PATCH 05/41] Rm empty line --- packages/svm/programs/settler/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svm/programs/settler/Cargo.toml b/packages/svm/programs/settler/Cargo.toml index 9ebb8c8..899599c 100644 --- a/packages/svm/programs/settler/Cargo.toml +++ b/packages/svm/programs/settler/Cargo.toml @@ -16,6 +16,5 @@ no-idl = [] no-log-ix-name = [] idl-build = ["anchor-lang/idl-build"] - [dependencies] anchor-lang = { version = "0.32.1", features = [ "init-if-needed" ] } From c5cbdf77080e9c97d4f04ee994c988c90ee8b90a Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 30 Oct 2025 12:38:45 -0300 Subject: [PATCH 06/41] Use anchor test on package.json --- packages/svm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svm/package.json b/packages/svm/package.json index baeb65c..bf28823 100644 --- a/packages/svm/package.json +++ b/packages/svm/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "build": "anchor build", - "test": "NODE_ENV=test ts-mocha -r ./tests/global-hooks.ts ./tests --recursive --extension .test.ts --exit", + "test": "anchor test", "lint": "eslint . --ext .ts", "lint:fix": "yarn lint --fix" }, From 7f31d8f5f56f62545f1b91d6816b124df82ae278 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 30 Oct 2025 13:39:49 -0300 Subject: [PATCH 07/41] Simplify Solana CI --- .github/workflows/ci.yml | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2ed729..61efa14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,13 +75,6 @@ jobs: if: ${{ contains(needs.find-changed-packages.outputs.packages, 'svm') }} container: solanafoundation/anchor:v0.32.1 steps: - - name: Print versions - run: | - echo "Node.js version: $(node -v)" - echo "Yarn version: $(yarn -v)" - echo "Solana version: $(solana --version)" - echo "Anchor version: $(anchor --version)" - echo "LiteSVM version: $(litesvm --version)" - name: Checkout uses: actions/checkout@v3 - name: Set up Node.js @@ -90,20 +83,16 @@ jobs: node-version: "22.15.1" - name: Set Up Solana Test Keypair run: solana-keygen new --no-bip39-passphrase --silent - - name: Install - working-directory: packages/svm - run: yarn - name: Pre-clean caches working-directory: packages/svm run: | yarn cache clean || true rm -rf ~/.cargo/registry ~/.cargo/git ~/.cache || true + - name: Install + run: yarn workspace @mimicprotocol/contracts-svm install - name: Build Anchor Program - working-directory: packages/svm run: | rustup default stable - DEPLOYER_KEY=$(solana-keygen pubkey) anchor build + yarn workspace @mimicprotocol/contracts-svm build - name: Test - working-directory: packages/svm - run: | - yarn test + run: yarn workspace @mimicprotocol/contracts-svm test From e637e3b369d636f134c8977d4bf20427ed7f7eb7 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 5 Nov 2025 16:50:41 -0300 Subject: [PATCH 08/41] Address code suggestions --- packages/svm/.gitignore | 1 - packages/svm/.prettierignore | 1 - packages/svm/Anchor.toml | 2 +- packages/svm/tests/global-hooks.ts | 0 4 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 packages/svm/tests/global-hooks.ts diff --git a/packages/svm/.gitignore b/packages/svm/.gitignore index 2e0446b..ee87279 100644 --- a/packages/svm/.gitignore +++ b/packages/svm/.gitignore @@ -1,5 +1,4 @@ .anchor -.DS_Store target **/*.rs.bk node_modules diff --git a/packages/svm/.prettierignore b/packages/svm/.prettierignore index 4142583..e2ebda9 100644 --- a/packages/svm/.prettierignore +++ b/packages/svm/.prettierignore @@ -1,5 +1,4 @@ .anchor -.DS_Store target node_modules dist diff --git a/packages/svm/Anchor.toml b/packages/svm/Anchor.toml index 4b0987b..1bb4fa9 100644 --- a/packages/svm/Anchor.toml +++ b/packages/svm/Anchor.toml @@ -16,4 +16,4 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] -test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" +test = "yarn run ts-mocha -p ./tsconfig.json tests/**/*.ts" diff --git a/packages/svm/tests/global-hooks.ts b/packages/svm/tests/global-hooks.ts deleted file mode 100644 index e69de29..0000000 From de1c9984f2244b764fc4eef99d2660514853e2b7 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Fri, 31 Oct 2025 12:30:44 -0300 Subject: [PATCH 09/41] Scaffold Settler structure --- packages/svm/programs/settler/src/errors.rs | 0 .../settler/src/instructions/add_axia_sig.rs | 1 + .../add_instructions_to_proposal.rs | 1 + .../src/instructions/add_validator_sigs.rs | 1 + .../instructions/change_whitelist_program.rs | 1 + .../src/instructions/claim_stale_proposal.rs | 1 + .../settler/src/instructions/create_intent.rs | 1 + .../settler/src/instructions/create_proposal.rs | 1 + .../src/instructions/execute_proposal.rs | 1 + .../programs/settler/src/instructions/mod.rs | 17 +++++++++++++++++ packages/svm/programs/settler/src/lib.rs | 13 +++++-------- .../settler/src/state/fulfilled_intent.rs | 1 + .../svm/programs/settler/src/state/intent.rs | 1 + packages/svm/programs/settler/src/state/mod.rs | 9 +++++++++ .../svm/programs/settler/src/state/proposal.rs | 1 + .../settler/src/state/settler_settings.rs | 1 + .../programs/settler/src/types/intent_event.rs | 0 .../svm/programs/settler/src/types/max_fee.rs | 0 packages/svm/programs/settler/src/types/mod.rs | 11 +++++++++++ .../svm/programs/settler/src/types/op_type.rs | 0 .../settler/src/types/proposal_instruction.rs | 0 .../types/proposal_instruction_account_meta.rs | 0 22 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 packages/svm/programs/settler/src/errors.rs create mode 100644 packages/svm/programs/settler/src/instructions/add_axia_sig.rs create mode 100644 packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs create mode 100644 packages/svm/programs/settler/src/instructions/add_validator_sigs.rs create mode 100644 packages/svm/programs/settler/src/instructions/change_whitelist_program.rs create mode 100644 packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs create mode 100644 packages/svm/programs/settler/src/instructions/create_intent.rs create mode 100644 packages/svm/programs/settler/src/instructions/create_proposal.rs create mode 100644 packages/svm/programs/settler/src/instructions/execute_proposal.rs create mode 100644 packages/svm/programs/settler/src/instructions/mod.rs create mode 100644 packages/svm/programs/settler/src/state/fulfilled_intent.rs create mode 100644 packages/svm/programs/settler/src/state/intent.rs create mode 100644 packages/svm/programs/settler/src/state/mod.rs create mode 100644 packages/svm/programs/settler/src/state/proposal.rs create mode 100644 packages/svm/programs/settler/src/state/settler_settings.rs create mode 100644 packages/svm/programs/settler/src/types/intent_event.rs create mode 100644 packages/svm/programs/settler/src/types/max_fee.rs create mode 100644 packages/svm/programs/settler/src/types/mod.rs create mode 100644 packages/svm/programs/settler/src/types/op_type.rs create mode 100644 packages/svm/programs/settler/src/types/proposal_instruction.rs create mode 100644 packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs new file mode 100644 index 0000000..e69de29 diff --git a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs b/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/instructions/create_proposal.rs b/packages/svm/programs/settler/src/instructions/create_proposal.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/create_proposal.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/instructions/mod.rs b/packages/svm/programs/settler/src/instructions/mod.rs new file mode 100644 index 0000000..d968cf1 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/mod.rs @@ -0,0 +1,17 @@ +pub mod add_axia_sig; +pub mod add_instructions_to_proposal; +pub mod add_validator_sigs; +pub mod change_whitelist_program; +pub mod claim_stale_proposal; +pub mod create_intent; +pub mod create_proposal; +pub mod execute_proposal; + +pub use add_axia_sig::*; +pub use add_instructions_to_proposal::*; +pub use add_validator_sigs::*; +pub use change_whitelist_program::*; +pub use claim_stale_proposal::*; +pub use create_intent::*; +pub use create_proposal::*; +pub use execute_proposal::*; diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index 59630fe..d7b3908 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -4,15 +4,12 @@ use anchor_lang::prelude::*; declare_id!("HbNt35Ng8aM4NUy39evpCQqXEC4Nmaq16ewY8dyNF6NF"); +pub mod instructions; +pub mod state; +pub mod errors; +pub mod types; + #[program] pub mod settler { use super::*; - - pub fn initialize(ctx: Context) -> Result<()> { - msg!("Greetings from: {:?}", ctx.program_id); - Ok(()) - } } - -#[derive(Accounts)] -pub struct Initialize {} diff --git a/packages/svm/programs/settler/src/state/fulfilled_intent.rs b/packages/svm/programs/settler/src/state/fulfilled_intent.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/state/fulfilled_intent.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/state/mod.rs b/packages/svm/programs/settler/src/state/mod.rs new file mode 100644 index 0000000..fb0b3ba --- /dev/null +++ b/packages/svm/programs/settler/src/state/mod.rs @@ -0,0 +1,9 @@ +pub mod fulfilled_intent; +pub mod intent; +pub mod proposal; +pub mod settler_settings; + +pub use fulfilled_intent::*; +pub use intent::*; +pub use proposal::*; +pub use settler_settings::*; diff --git a/packages/svm/programs/settler/src/state/proposal.rs b/packages/svm/programs/settler/src/state/proposal.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/state/proposal.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/state/settler_settings.rs b/packages/svm/programs/settler/src/state/settler_settings.rs new file mode 100644 index 0000000..473b2c7 --- /dev/null +++ b/packages/svm/programs/settler/src/state/settler_settings.rs @@ -0,0 +1 @@ +use anchor_lang::prelude::*; diff --git a/packages/svm/programs/settler/src/types/intent_event.rs b/packages/svm/programs/settler/src/types/intent_event.rs new file mode 100644 index 0000000..e69de29 diff --git a/packages/svm/programs/settler/src/types/max_fee.rs b/packages/svm/programs/settler/src/types/max_fee.rs new file mode 100644 index 0000000..e69de29 diff --git a/packages/svm/programs/settler/src/types/mod.rs b/packages/svm/programs/settler/src/types/mod.rs new file mode 100644 index 0000000..0f9e229 --- /dev/null +++ b/packages/svm/programs/settler/src/types/mod.rs @@ -0,0 +1,11 @@ +pub mod intent_event; +pub mod max_fee; +pub mod op_type; +pub mod proposal_instruction; +pub mod proposal_instruction_account_meta; + +pub use intent_event::*; +pub use max_fee::*; +pub use op_type::*; +pub use proposal_instruction::*; +pub use proposal_instruction_account_meta::*; diff --git a/packages/svm/programs/settler/src/types/op_type.rs b/packages/svm/programs/settler/src/types/op_type.rs new file mode 100644 index 0000000..e69de29 diff --git a/packages/svm/programs/settler/src/types/proposal_instruction.rs b/packages/svm/programs/settler/src/types/proposal_instruction.rs new file mode 100644 index 0000000..e69de29 diff --git a/packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs b/packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs new file mode 100644 index 0000000..e69de29 From d9342c610f604b27be4762357254b8b56220577b Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Fri, 31 Oct 2025 12:33:09 -0300 Subject: [PATCH 10/41] Boilerplate --- .../programs/settler/src/instructions/add_axia_sig.rs | 9 +++++++++ .../src/instructions/add_instructions_to_proposal.rs | 9 +++++++++ .../settler/src/instructions/add_validator_sigs.rs | 9 +++++++++ .../settler/src/instructions/change_whitelist_program.rs | 9 +++++++++ .../settler/src/instructions/claim_stale_proposal.rs | 9 +++++++++ .../programs/settler/src/instructions/create_intent.rs | 9 +++++++++ .../programs/settler/src/instructions/create_proposal.rs | 9 +++++++++ .../settler/src/instructions/execute_proposal.rs | 9 +++++++++ .../svm/programs/settler/src/state/fulfilled_intent.rs | 3 +++ packages/svm/programs/settler/src/state/intent.rs | 3 +++ packages/svm/programs/settler/src/state/proposal.rs | 3 +++ .../svm/programs/settler/src/state/settler_settings.rs | 3 +++ 12 files changed, 84 insertions(+) diff --git a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs index 473b2c7..6528024 100644 --- a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs +++ b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs @@ -1 +1,10 @@ use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct AddAxiaSig { + +} + +pub fn add_axia_sig(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs index 473b2c7..fbe164d 100644 --- a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs @@ -1 +1,10 @@ use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct AddInstructionsToProposal { + +} + +pub fn add_axia_sig(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs b/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs index 473b2c7..81f1d70 100644 --- a/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs +++ b/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs @@ -1 +1,10 @@ use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct AddValidatorSigs { + +} + +pub fn add_axia_sig(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs index 473b2c7..2dc434d 100644 --- a/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs +++ b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs @@ -1 +1,10 @@ use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ChangeWhitelistProgram { + +} + +pub fn add_axia_sig(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs index 473b2c7..070eaec 100644 --- a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs @@ -1 +1,10 @@ use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ClaimStaleProposal { + +} + +pub fn add_axia_sig(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index 473b2c7..d9faaa6 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -1 +1,10 @@ use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CreateIntent { + +} + +pub fn add_axia_sig(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/create_proposal.rs b/packages/svm/programs/settler/src/instructions/create_proposal.rs index 473b2c7..eb1b581 100644 --- a/packages/svm/programs/settler/src/instructions/create_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/create_proposal.rs @@ -1 +1,10 @@ use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CreateProposal { + +} + +pub fn add_axia_sig(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index 473b2c7..a1adedf 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -1 +1,10 @@ use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ExecuteProposal { + +} + +pub fn add_axia_sig(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/state/fulfilled_intent.rs b/packages/svm/programs/settler/src/state/fulfilled_intent.rs index 473b2c7..11dea17 100644 --- a/packages/svm/programs/settler/src/state/fulfilled_intent.rs +++ b/packages/svm/programs/settler/src/state/fulfilled_intent.rs @@ -1 +1,4 @@ use anchor_lang::prelude::*; + +#[account] +pub struct FulfilledIntent {} diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs index 473b2c7..3ff9532 100644 --- a/packages/svm/programs/settler/src/state/intent.rs +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -1 +1,4 @@ use anchor_lang::prelude::*; + +#[account] +pub struct Intent {} diff --git a/packages/svm/programs/settler/src/state/proposal.rs b/packages/svm/programs/settler/src/state/proposal.rs index 473b2c7..4d7664e 100644 --- a/packages/svm/programs/settler/src/state/proposal.rs +++ b/packages/svm/programs/settler/src/state/proposal.rs @@ -1 +1,4 @@ use anchor_lang::prelude::*; + +#[account] +pub struct Proposal {} diff --git a/packages/svm/programs/settler/src/state/settler_settings.rs b/packages/svm/programs/settler/src/state/settler_settings.rs index 473b2c7..add5cdb 100644 --- a/packages/svm/programs/settler/src/state/settler_settings.rs +++ b/packages/svm/programs/settler/src/state/settler_settings.rs @@ -1 +1,4 @@ use anchor_lang::prelude::*; + +#[account] +pub struct SettlerSettings {} From a68a860a6913bd05002500fe1a014e76e1a14892 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Fri, 31 Oct 2025 12:36:38 -0300 Subject: [PATCH 11/41] Add types --- packages/svm/programs/settler/src/types/intent_event.rs | 4 ++++ packages/svm/programs/settler/src/types/max_fee.rs | 6 ++++++ packages/svm/programs/settler/src/types/op_type.rs | 6 ++++++ .../programs/settler/src/types/proposal_instruction.rs | 9 +++++++++ .../src/types/proposal_instruction_account_meta.rs | 7 +++++++ 5 files changed, 32 insertions(+) diff --git a/packages/svm/programs/settler/src/types/intent_event.rs b/packages/svm/programs/settler/src/types/intent_event.rs index e69de29..9777df2 100644 --- a/packages/svm/programs/settler/src/types/intent_event.rs +++ b/packages/svm/programs/settler/src/types/intent_event.rs @@ -0,0 +1,4 @@ +pub struct IntentEvent { + pub topic: [u8; 32], + pub data: Vec, +} diff --git a/packages/svm/programs/settler/src/types/max_fee.rs b/packages/svm/programs/settler/src/types/max_fee.rs index e69de29..e181540 100644 --- a/packages/svm/programs/settler/src/types/max_fee.rs +++ b/packages/svm/programs/settler/src/types/max_fee.rs @@ -0,0 +1,6 @@ +use anchor_lang::prelude::Pubkey; + +pub struct MaxFee { + pub mint: Pubkey, + pub amount: u64, +} diff --git a/packages/svm/programs/settler/src/types/op_type.rs b/packages/svm/programs/settler/src/types/op_type.rs index e69de29..cc36418 100644 --- a/packages/svm/programs/settler/src/types/op_type.rs +++ b/packages/svm/programs/settler/src/types/op_type.rs @@ -0,0 +1,6 @@ +#[repr(u8)] +pub enum OpType { + Swap = 1, + Transfer = 2, + Call = 3, +} diff --git a/packages/svm/programs/settler/src/types/proposal_instruction.rs b/packages/svm/programs/settler/src/types/proposal_instruction.rs index e69de29..5d972a4 100644 --- a/packages/svm/programs/settler/src/types/proposal_instruction.rs +++ b/packages/svm/programs/settler/src/types/proposal_instruction.rs @@ -0,0 +1,9 @@ +use anchor_lang::prelude::Pubkey; + +use crate::types::ProposalInstructionAccountMeta; + +pub struct ProposalInstruction { + pub program_id: Pubkey, + pub accounts: Vec, + pub data: Vec, +} diff --git a/packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs b/packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs index e69de29..5a2b55f 100644 --- a/packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs +++ b/packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs @@ -0,0 +1,7 @@ +use anchor_lang::prelude::Pubkey; + +pub struct ProposalInstructionAccountMeta { + pub pubkey: Pubkey, + pub is_signer: bool, + pub is_writable: bool, +} From 2ab3a7f553b34329dd870e233a69b0324f07f9ae Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Fri, 31 Oct 2025 12:41:54 -0300 Subject: [PATCH 12/41] lib.rs boilerplate --- .../add_instructions_to_proposal.rs | 2 +- .../src/instructions/add_validator_sigs.rs | 2 +- .../instructions/change_whitelist_program.rs | 2 +- .../src/instructions/claim_stale_proposal.rs | 2 +- .../settler/src/instructions/create_intent.rs | 2 +- .../src/instructions/create_proposal.rs | 2 +- .../src/instructions/execute_proposal.rs | 2 +- packages/svm/programs/settler/src/lib.rs | 34 +++++++++++++++++++ 8 files changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs index fbe164d..19a190e 100644 --- a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs @@ -5,6 +5,6 @@ pub struct AddInstructionsToProposal { } -pub fn add_axia_sig(ctx: Context) -> Result<()> { +pub fn add_instructions_to_proposal(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs b/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs index 81f1d70..85c8b17 100644 --- a/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs +++ b/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs @@ -5,6 +5,6 @@ pub struct AddValidatorSigs { } -pub fn add_axia_sig(ctx: Context) -> Result<()> { +pub fn add_validator_sigs(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs index 2dc434d..0c88519 100644 --- a/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs +++ b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs @@ -5,6 +5,6 @@ pub struct ChangeWhitelistProgram { } -pub fn add_axia_sig(ctx: Context) -> Result<()> { +pub fn change_whitelist_program(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs index 070eaec..da8b0d3 100644 --- a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs @@ -5,6 +5,6 @@ pub struct ClaimStaleProposal { } -pub fn add_axia_sig(ctx: Context) -> Result<()> { +pub fn claim_stale_proposal(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index d9faaa6..07ccd56 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -5,6 +5,6 @@ pub struct CreateIntent { } -pub fn add_axia_sig(ctx: Context) -> Result<()> { +pub fn create_intent(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/create_proposal.rs b/packages/svm/programs/settler/src/instructions/create_proposal.rs index eb1b581..5c69604 100644 --- a/packages/svm/programs/settler/src/instructions/create_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/create_proposal.rs @@ -5,6 +5,6 @@ pub struct CreateProposal { } -pub fn add_axia_sig(ctx: Context) -> Result<()> { +pub fn create_proposal(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index a1adedf..49ad9fc 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -5,6 +5,6 @@ pub struct ExecuteProposal { } -pub fn add_axia_sig(ctx: Context) -> Result<()> { +pub fn execute_proposal(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index d7b3908..2efa668 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -9,7 +9,41 @@ pub mod state; pub mod errors; pub mod types; +use crate::instructions::*; + #[program] pub mod settler { use super::*; + + pub fn add_axia_sig(ctx: Context) -> Result<()> { + instructions::add_axia_sig(ctx) + } + + pub fn add_instructions_to_proposal(ctx: Context) -> Result<()> { + instructions::add_instructions_to_proposal(ctx) + } + + pub fn add_validator_sigs(ctx: Context) -> Result<()> { + instructions::add_validator_sigs(ctx) + } + + pub fn change_whitelist_program(ctx: Context) -> Result<()> { + instructions::change_whitelist_program(ctx) + } + + pub fn claim_stale_proposal(ctx: Context) -> Result<()> { + instructions::claim_stale_proposal(ctx) + } + + pub fn create_intent(ctx: Context) -> Result<()> { + instructions::create_intent(ctx) + } + + pub fn create_proposal(ctx: Context) -> Result<()> { + instructions::create_proposal(ctx) + } + + pub fn execute_proposal(ctx: Context) -> Result<()> { + instructions::execute_proposal(ctx) + } } From 95cf128602e6280dfc3ed32c897a2bf16a4c6c47 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Fri, 31 Oct 2025 12:42:37 -0300 Subject: [PATCH 13/41] Cargo fmt --- packages/svm/programs/settler/src/errors.rs | 1 + .../svm/programs/settler/src/instructions/add_axia_sig.rs | 4 +--- .../settler/src/instructions/add_instructions_to_proposal.rs | 4 +--- .../programs/settler/src/instructions/add_validator_sigs.rs | 4 +--- .../settler/src/instructions/change_whitelist_program.rs | 4 +--- .../programs/settler/src/instructions/claim_stale_proposal.rs | 4 +--- .../svm/programs/settler/src/instructions/create_intent.rs | 4 +--- .../svm/programs/settler/src/instructions/create_proposal.rs | 4 +--- .../svm/programs/settler/src/instructions/execute_proposal.rs | 4 +--- packages/svm/programs/settler/src/lib.rs | 2 +- 10 files changed, 10 insertions(+), 25 deletions(-) diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index e69de29..8b13789 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -0,0 +1 @@ + diff --git a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs index 6528024..2a626c7 100644 --- a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs +++ b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs @@ -1,9 +1,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct AddAxiaSig { - -} +pub struct AddAxiaSig {} pub fn add_axia_sig(ctx: Context) -> Result<()> { Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs index 19a190e..477daa8 100644 --- a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs @@ -1,9 +1,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct AddInstructionsToProposal { - -} +pub struct AddInstructionsToProposal {} pub fn add_instructions_to_proposal(ctx: Context) -> Result<()> { Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs b/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs index 85c8b17..8737d49 100644 --- a/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs +++ b/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs @@ -1,9 +1,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct AddValidatorSigs { - -} +pub struct AddValidatorSigs {} pub fn add_validator_sigs(ctx: Context) -> Result<()> { Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs index 0c88519..c7be3d8 100644 --- a/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs +++ b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs @@ -1,9 +1,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct ChangeWhitelistProgram { - -} +pub struct ChangeWhitelistProgram {} pub fn change_whitelist_program(ctx: Context) -> Result<()> { Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs index da8b0d3..aaa5b01 100644 --- a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs @@ -1,9 +1,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct ClaimStaleProposal { - -} +pub struct ClaimStaleProposal {} pub fn claim_stale_proposal(ctx: Context) -> Result<()> { Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index 07ccd56..59ff6f6 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -1,9 +1,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct CreateIntent { - -} +pub struct CreateIntent {} pub fn create_intent(ctx: Context) -> Result<()> { Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/create_proposal.rs b/packages/svm/programs/settler/src/instructions/create_proposal.rs index 5c69604..2f7bc41 100644 --- a/packages/svm/programs/settler/src/instructions/create_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/create_proposal.rs @@ -1,9 +1,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct CreateProposal { - -} +pub struct CreateProposal {} pub fn create_proposal(ctx: Context) -> Result<()> { Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index 49ad9fc..6f24c0e 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -1,9 +1,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] -pub struct ExecuteProposal { - -} +pub struct ExecuteProposal {} pub fn execute_proposal(ctx: Context) -> Result<()> { Ok(()) diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index 2efa668..8768fe0 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -4,9 +4,9 @@ use anchor_lang::prelude::*; declare_id!("HbNt35Ng8aM4NUy39evpCQqXEC4Nmaq16ewY8dyNF6NF"); +pub mod errors; pub mod instructions; pub mod state; -pub mod errors; pub mod types; use crate::instructions::*; From b1ee2bfdf8ae6c8a672249dbebb9e4eaf5018d54 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Fri, 31 Oct 2025 12:49:20 -0300 Subject: [PATCH 14/41] Add initialize and pause ixs --- .../svm/programs/settler/src/instructions/initialize.rs | 8 ++++++++ packages/svm/programs/settler/src/instructions/mod.rs | 4 ++++ packages/svm/programs/settler/src/instructions/pause.rs | 8 ++++++++ packages/svm/programs/settler/src/lib.rs | 8 ++++++++ 4 files changed, 28 insertions(+) create mode 100644 packages/svm/programs/settler/src/instructions/initialize.rs create mode 100644 packages/svm/programs/settler/src/instructions/pause.rs diff --git a/packages/svm/programs/settler/src/instructions/initialize.rs b/packages/svm/programs/settler/src/instructions/initialize.rs new file mode 100644 index 0000000..02cd122 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/initialize.rs @@ -0,0 +1,8 @@ +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct Initialize {} + +pub fn initialize(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/mod.rs b/packages/svm/programs/settler/src/instructions/mod.rs index d968cf1..1a59e8a 100644 --- a/packages/svm/programs/settler/src/instructions/mod.rs +++ b/packages/svm/programs/settler/src/instructions/mod.rs @@ -6,6 +6,8 @@ pub mod claim_stale_proposal; pub mod create_intent; pub mod create_proposal; pub mod execute_proposal; +pub mod initialize; +pub mod pause; pub use add_axia_sig::*; pub use add_instructions_to_proposal::*; @@ -15,3 +17,5 @@ pub use claim_stale_proposal::*; pub use create_intent::*; pub use create_proposal::*; pub use execute_proposal::*; +pub use initialize::*; +pub use pause::*; diff --git a/packages/svm/programs/settler/src/instructions/pause.rs b/packages/svm/programs/settler/src/instructions/pause.rs new file mode 100644 index 0000000..8e2caa3 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/pause.rs @@ -0,0 +1,8 @@ +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct Pause {} + +pub fn pause(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index 8768fe0..1f1cecd 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -46,4 +46,12 @@ pub mod settler { pub fn execute_proposal(ctx: Context) -> Result<()> { instructions::execute_proposal(ctx) } + + pub fn initialize(ctx: Context) -> Result<()> { + instructions::initialize(ctx) + } + + pub fn pause(ctx: Context) -> Result<()> { + instructions::pause(ctx) + } } From 5862acf563a08d71c96a1b2b0e9c52dccdcb5bf2 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Fri, 31 Oct 2025 18:25:04 -0300 Subject: [PATCH 15/41] Add initialize ix --- packages/svm/package.json | 4 +- .../svm/programs/settler/src/constants.rs | 4 ++ packages/svm/programs/settler/src/errors.rs | 6 ++ .../settler/src/instructions/initialize.rs | 33 +++++++++- packages/svm/programs/settler/src/lib.rs | 5 +- .../settler/src/state/settler_settings.rs | 7 ++- packages/svm/sdks/settler/Settler.ts | 21 +++++++ packages/svm/tests/settler.test.ts | 63 +++++++++++++++---- packages/svm/tests/utils.ts | 26 ++++++-- 9 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 packages/svm/programs/settler/src/constants.rs create mode 100644 packages/svm/sdks/settler/Settler.ts diff --git a/packages/svm/package.json b/packages/svm/package.json index bf28823..2d06e9b 100644 --- a/packages/svm/package.json +++ b/packages/svm/package.json @@ -5,8 +5,8 @@ "license": "GPL-3.0", "type": "module", "scripts": { - "build": "anchor build", - "test": "anchor test", + "build": "DEPLOYER_KEY=$(solana-keygen pubkey) anchor build", + "test": "anchor run test", "lint": "eslint . --ext .ts", "lint:fix": "yarn lint --fix" }, diff --git a/packages/svm/programs/settler/src/constants.rs b/packages/svm/programs/settler/src/constants.rs new file mode 100644 index 0000000..756ec67 --- /dev/null +++ b/packages/svm/programs/settler/src/constants.rs @@ -0,0 +1,4 @@ +pub const DEPLOYER_KEY: &'static str = env!( + "DEPLOYER_KEY", + "Please set the DEPLOYER_KEY env variable before compiling." +); diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index 8b13789..4b93bbd 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -1 +1,7 @@ +use anchor_lang::prelude::*; +#[error_code] +pub enum SettlerError { + #[msg("Only Deployer can call this instruction")] + OnlyDeployer, +} diff --git a/packages/svm/programs/settler/src/instructions/initialize.rs b/packages/svm/programs/settler/src/instructions/initialize.rs index 02cd122..539200f 100644 --- a/packages/svm/programs/settler/src/instructions/initialize.rs +++ b/packages/svm/programs/settler/src/instructions/initialize.rs @@ -1,8 +1,37 @@ use anchor_lang::prelude::*; +use std::str::FromStr; + +use crate::{constants::DEPLOYER_KEY, errors::SettlerError, state::SettlerSettings}; #[derive(Accounts)] -pub struct Initialize {} +pub struct Initialize<'info> { + #[account(mut)] + pub deployer: Signer<'info>, + + #[account( + init, + seeds = [b"settler-settings"], + bump, + payer = deployer, + space = 8 + SettlerSettings::INIT_SPACE, + )] + pub settler_settings: Box>, + + pub system_program: Program<'info, System>, +} + +pub fn initialize(ctx: Context, whitelist_program: Pubkey) -> Result<()> { + require_keys_eq!( + ctx.accounts.deployer.key(), + Pubkey::from_str(DEPLOYER_KEY).unwrap(), + SettlerError::OnlyDeployer, + ); + + let settler_settings = &mut ctx.accounts.settler_settings; + + settler_settings.whitelist_program = whitelist_program; + settler_settings.is_paused = false; + settler_settings.bump = ctx.bumps.settler_settings; -pub fn initialize(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index 1f1cecd..67c7576 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -4,6 +4,7 @@ use anchor_lang::prelude::*; declare_id!("HbNt35Ng8aM4NUy39evpCQqXEC4Nmaq16ewY8dyNF6NF"); +pub mod constants; pub mod errors; pub mod instructions; pub mod state; @@ -47,8 +48,8 @@ pub mod settler { instructions::execute_proposal(ctx) } - pub fn initialize(ctx: Context) -> Result<()> { - instructions::initialize(ctx) + pub fn initialize(ctx: Context, whitelist_program: Pubkey) -> Result<()> { + instructions::initialize(ctx, whitelist_program) } pub fn pause(ctx: Context) -> Result<()> { diff --git a/packages/svm/programs/settler/src/state/settler_settings.rs b/packages/svm/programs/settler/src/state/settler_settings.rs index add5cdb..01a3942 100644 --- a/packages/svm/programs/settler/src/state/settler_settings.rs +++ b/packages/svm/programs/settler/src/state/settler_settings.rs @@ -1,4 +1,9 @@ use anchor_lang::prelude::*; #[account] -pub struct SettlerSettings {} +#[derive(InitSpace)] +pub struct SettlerSettings { + pub whitelist_program: Pubkey, + pub is_paused: bool, + pub bump: u8, +} diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts new file mode 100644 index 0000000..bce3ef8 --- /dev/null +++ b/packages/svm/sdks/settler/Settler.ts @@ -0,0 +1,21 @@ +import { Program, Provider, web3 } from '@coral-xyz/anchor' + +import * as SettlerIDL from '../../target/idl/settler.json' +import { Settler } from '../../target/types/settler' + +export default class SettlerSDK { + protected program: Program + + constructor(provider: Provider) { + this.program = new Program(SettlerIDL, provider) + } + + async initializeIx(whitelistProgram: web3.PublicKey): Promise { + const ix = await this.program.methods.initialize(whitelistProgram).instruction() + return ix + } + + getSettlerSettingsPubkey(): web3.PublicKey { + return web3.PublicKey.findProgramAddressSync([Buffer.from('settler-settings')], this.program.programId)[0] + } +} diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index d5ff4ce..47b87a5 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -5,42 +5,83 @@ import { Program, Wallet } from '@coral-xyz/anchor' import { Keypair } from '@solana/web3.js' import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' +import fs from 'fs' +import { LiteSVM } from 'litesvm' +import os from 'os' import path from 'path' +import SettlerSDK from '../sdks/settler/Settler' import * as SettlerIDL from '../target/idl/settler.json' import { Settler } from '../target/types/settler' -import { extractLogs } from './utils' +import { makeTxSignAndSend } from './utils' describe('Settler Program', () => { - let client: any + let client: LiteSVM let provider: LiteSVMProvider + let maliciousProvider: LiteSVMProvider let admin: Keypair let malicious: Keypair let program: Program + let sdk: SettlerSDK + let maliciousSdk: SettlerSDK before(async () => { - admin = Keypair.generate() + admin = Keypair.fromSecretKey( + Uint8Array.from(JSON.parse(fs.readFileSync(path.join(os.homedir(), '.config', 'solana', 'id.json'), 'utf8'))) + ) malicious = Keypair.generate() client = fromWorkspace(path.join(__dirname, '../')).withBuiltins() provider = new LiteSVMProvider(client, new Wallet(admin)) + maliciousProvider = new LiteSVMProvider(client, new Wallet(malicious)) + program = new Program(SettlerIDL as any, provider) + sdk = new SettlerSDK(provider) + maliciousSdk = new SettlerSDK(maliciousProvider) + // Airdrop initial lamports provider.client.airdrop(admin.publicKey, BigInt(100_000_000_000)) provider.client.airdrop(malicious.publicKey, BigInt(100_000_000_000)) }) + beforeEach(() => { + client.expireBlockhash() + }) + describe('Settler', () => { - it('should call initialize', async () => { - const tx = await program.methods.initialize().transaction() - tx.recentBlockhash = provider.client.latestBlockhash() - tx.feePayer = admin.publicKey - tx.sign(admin) - const res = provider.client.sendTransaction(tx) - - expect(extractLogs(res.toString()).join('').includes(`Greetings from: ${program.programId.toString()}`)).to.be.ok + describe('initialize', () => { + it('cant initialize if not deployer', async () => { + const whitelistProgram = Keypair.generate().publicKey + + const ix = await maliciousSdk.initializeIx(whitelistProgram) + const res = await makeTxSignAndSend(maliciousProvider, ix) + + expect(res.toString()).to.include(`Only Deployer can call this instruction.`) + }) + + it('should call initialize', async () => { + const whitelistProgram = Keypair.generate().publicKey + + const ix = await sdk.initializeIx(whitelistProgram) + await makeTxSignAndSend(provider, ix) + + const settings = await program.account.settlerSettings.fetch(sdk.getSettlerSettingsPubkey()) + expect(settings.whitelistProgram.toString()).to.be.eq(whitelistProgram.toString()) + expect(settings.isPaused).to.be.false + }) + + it('cant call initialize again', async () => { + const whitelistProgram = Keypair.generate().publicKey + + const ix = await sdk.initializeIx(whitelistProgram) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include( + `Allocate: account Address { address: ${sdk.getSettlerSettingsPubkey()}, base: None } already in use` + ) + }) }) }) }) diff --git a/packages/svm/tests/utils.ts b/packages/svm/tests/utils.ts index 54c2529..bcf0463 100644 --- a/packages/svm/tests/utils.ts +++ b/packages/svm/tests/utils.ts @@ -1,6 +1,24 @@ -export function extractLogs(liteSvmTxMetadataString: string): string[] { - const logsMatch = liteSvmTxMetadataString.match(/logs: \[(.*?)\],/s) - if (!logsMatch) return [] +import { web3 } from '@coral-xyz/anchor' +import { LiteSVMProvider } from 'anchor-litesvm' +import { FailedTransactionMetadata, TransactionMetadata } from 'litesvm' - return logsMatch[1].split('", "') +export async function signAndSendTx( + provider: LiteSVMProvider, + tx: web3.Transaction +): Promise { + tx.recentBlockhash = provider.client.latestBlockhash() + tx.feePayer = provider.wallet.publicKey + const stx = await provider.wallet.signTransaction(tx) + return provider.client.sendTransaction(stx) +} + +export function makeTx(...ixs: web3.TransactionInstruction[]): web3.Transaction { + return new web3.Transaction().add(...ixs) +} + +export async function makeTxSignAndSend( + provider: LiteSVMProvider, + ...ixs: web3.TransactionInstruction[] +): Promise { + return signAndSendTx(provider, makeTx(...ixs)) } From 4d37b7d1dca861fb1415328ce55b88f9bb042979 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Fri, 31 Oct 2025 22:02:05 -0300 Subject: [PATCH 16/41] Add Whitelist program (almost done) --- packages/svm/Anchor.toml | 1 + packages/svm/Cargo.lock | 7 ++ packages/svm/programs/settler/Cargo.toml | 6 ++ packages/svm/programs/settler/Xargo.toml | 2 - packages/svm/programs/settler/src/lib.rs | 2 - packages/svm/programs/whitelist/Cargo.toml | 26 +++++++ .../svm/programs/whitelist/src/constants.rs | 4 ++ packages/svm/programs/whitelist/src/errors.rs | 19 +++++ .../whitelist/src/instructions/initialize.rs | 43 +++++++++++ .../whitelist/src/instructions/mod.rs | 9 +++ .../src/instructions/propose_admin.rs | 32 +++++++++ .../set_entity_whitelist_status.rs | 72 +++++++++++++++++++ .../src/instructions/set_proposed_admin.rs | 47 ++++++++++++ packages/svm/programs/whitelist/src/lib.rs | 41 +++++++++++ .../whitelist/src/state/entity_registry.rs | 14 ++++ .../whitelist/src/state/global_settings.rs | 11 +++ .../svm/programs/whitelist/src/state/mod.rs | 5 ++ .../whitelist/src/types/entity_type.rs | 13 ++++ .../svm/programs/whitelist/src/types/mod.rs | 5 ++ .../whitelist/src/types/whitelist_status.rs | 12 ++++ packages/svm/rust-toolchain.toml | 4 ++ 21 files changed, 371 insertions(+), 4 deletions(-) delete mode 100644 packages/svm/programs/settler/Xargo.toml create mode 100644 packages/svm/programs/whitelist/Cargo.toml create mode 100644 packages/svm/programs/whitelist/src/constants.rs create mode 100644 packages/svm/programs/whitelist/src/errors.rs create mode 100644 packages/svm/programs/whitelist/src/instructions/initialize.rs create mode 100644 packages/svm/programs/whitelist/src/instructions/mod.rs create mode 100644 packages/svm/programs/whitelist/src/instructions/propose_admin.rs create mode 100644 packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs create mode 100644 packages/svm/programs/whitelist/src/instructions/set_proposed_admin.rs create mode 100644 packages/svm/programs/whitelist/src/lib.rs create mode 100644 packages/svm/programs/whitelist/src/state/entity_registry.rs create mode 100644 packages/svm/programs/whitelist/src/state/global_settings.rs create mode 100644 packages/svm/programs/whitelist/src/state/mod.rs create mode 100644 packages/svm/programs/whitelist/src/types/entity_type.rs create mode 100644 packages/svm/programs/whitelist/src/types/mod.rs create mode 100644 packages/svm/programs/whitelist/src/types/whitelist_status.rs create mode 100644 packages/svm/rust-toolchain.toml diff --git a/packages/svm/Anchor.toml b/packages/svm/Anchor.toml index 1bb4fa9..a651c4c 100644 --- a/packages/svm/Anchor.toml +++ b/packages/svm/Anchor.toml @@ -7,6 +7,7 @@ skip-lint = false [programs.localnet] settler = "HbNt35Ng8aM4NUy39evpCQqXEC4Nmaq16ewY8dyNF6NF" +whitelist = "7PwVkjnnapxytWFW69WFDLhfVZZgKhBE9m3zwcDZmncr" [registry] url = "https://api.apr.dev" diff --git a/packages/svm/Cargo.lock b/packages/svm/Cargo.lock index 66d20c5..18046f0 100644 --- a/packages/svm/Cargo.lock +++ b/packages/svm/Cargo.lock @@ -1529,6 +1529,13 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "whitelist" +version = "0.1.0" +dependencies = [ + "anchor-lang", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/packages/svm/programs/settler/Cargo.toml b/packages/svm/programs/settler/Cargo.toml index 899599c..e20a186 100644 --- a/packages/svm/programs/settler/Cargo.toml +++ b/packages/svm/programs/settler/Cargo.toml @@ -15,6 +15,12 @@ no-entrypoint = [] no-idl = [] no-log-ix-name = [] idl-build = ["anchor-lang/idl-build"] +anchor-debug = [] +custom-heap = [] +custom-panic = [] [dependencies] anchor-lang = { version = "0.32.1", features = [ "init-if-needed" ] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/packages/svm/programs/settler/Xargo.toml b/packages/svm/programs/settler/Xargo.toml deleted file mode 100644 index 475fb71..0000000 --- a/packages/svm/programs/settler/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index 67c7576..e3675d8 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(unexpected_cfgs)] - use anchor_lang::prelude::*; declare_id!("HbNt35Ng8aM4NUy39evpCQqXEC4Nmaq16ewY8dyNF6NF"); diff --git a/packages/svm/programs/whitelist/Cargo.toml b/packages/svm/programs/whitelist/Cargo.toml new file mode 100644 index 0000000..199f4d5 --- /dev/null +++ b/packages/svm/programs/whitelist/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "whitelist" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "whitelist" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] +anchor-debug = [] +custom-heap = [] +custom-panic = [] + +[dependencies] +anchor-lang = { version = "0.32.1", features = [ "init-if-needed" ] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/packages/svm/programs/whitelist/src/constants.rs b/packages/svm/programs/whitelist/src/constants.rs new file mode 100644 index 0000000..756ec67 --- /dev/null +++ b/packages/svm/programs/whitelist/src/constants.rs @@ -0,0 +1,4 @@ +pub const DEPLOYER_KEY: &'static str = env!( + "DEPLOYER_KEY", + "Please set the DEPLOYER_KEY env variable before compiling." +); diff --git a/packages/svm/programs/whitelist/src/errors.rs b/packages/svm/programs/whitelist/src/errors.rs new file mode 100644 index 0000000..45d5a12 --- /dev/null +++ b/packages/svm/programs/whitelist/src/errors.rs @@ -0,0 +1,19 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum WhitelistError { + #[msg("Only deployer can call this instruction")] + OnlyDeployer, + + #[msg("Only admin can call this instruction")] + OnlyAdmin, + + #[msg("Only proposed admin can call this instruction")] + OnlyProposedAdmin, + + #[msg("Proposed admin is already set")] + ProposedAdminIsAlreadySet, + + #[msg("Can't set proposed admin - either no next admin is proposed or cooldown period is not over yet")] + SetProposedAdminError, +} diff --git a/packages/svm/programs/whitelist/src/instructions/initialize.rs b/packages/svm/programs/whitelist/src/instructions/initialize.rs new file mode 100644 index 0000000..83646ab --- /dev/null +++ b/packages/svm/programs/whitelist/src/instructions/initialize.rs @@ -0,0 +1,43 @@ +use anchor_lang::prelude::*; +use std::str::FromStr; + +use crate::{constants::DEPLOYER_KEY, errors::WhitelistError, state::GlobalSettings}; + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account(mut)] + pub deployer: Signer<'info>, + + #[account( + init, + seeds = [b"global-settings"], + bump, + payer = deployer, + space = 8 + GlobalSettings::INIT_SPACE + )] + pub global_settings: Box>, + + pub system_program: Program<'info, System>, +} + +pub fn initialize( + ctx: Context, + admin: Pubkey, + proposed_admin_cooldown: u64, +) -> Result<()> { + require_keys_eq!( + ctx.accounts.deployer.key(), + Pubkey::from_str(DEPLOYER_KEY).unwrap(), + WhitelistError::OnlyDeployer, + ); + + let global_settings = &mut ctx.accounts.global_settings; + + global_settings.admin = admin; + global_settings.proposed_admin = None; + global_settings.proposed_admin_cooldown = proposed_admin_cooldown; + global_settings.proposed_admin_next_change_timestamp = u64::MAX; + global_settings.bump = ctx.bumps.global_settings; + + Ok(()) +} diff --git a/packages/svm/programs/whitelist/src/instructions/mod.rs b/packages/svm/programs/whitelist/src/instructions/mod.rs new file mode 100644 index 0000000..751c01b --- /dev/null +++ b/packages/svm/programs/whitelist/src/instructions/mod.rs @@ -0,0 +1,9 @@ +pub mod initialize; +pub mod propose_admin; +pub mod set_entity_whitelist_status; +pub mod set_proposed_admin; + +pub use initialize::*; +pub use propose_admin::*; +pub use set_entity_whitelist_status::*; +pub use set_proposed_admin::*; diff --git a/packages/svm/programs/whitelist/src/instructions/propose_admin.rs b/packages/svm/programs/whitelist/src/instructions/propose_admin.rs new file mode 100644 index 0000000..b95d5b0 --- /dev/null +++ b/packages/svm/programs/whitelist/src/instructions/propose_admin.rs @@ -0,0 +1,32 @@ +use anchor_lang::prelude::*; + +use crate::{errors::WhitelistError, state::GlobalSettings}; + +#[derive(Accounts)] +pub struct ProposeAdmin<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + #[account( + mut, + seeds = [b"global-settings"], + bump = global_settings.bump, + has_one = admin @ WhitelistError::OnlyAdmin + )] + pub global_settings: Box>, +} + +pub fn propose_admin(ctx: Context, proposed_admin: Pubkey) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + let global_settings = &mut ctx.accounts.global_settings; + + if global_settings.proposed_admin != None { + err!(WhitelistError::ProposedAdminIsAlreadySet)?; + } + + global_settings.proposed_admin = Some(proposed_admin); + global_settings.proposed_admin_next_change_timestamp = + now + global_settings.proposed_admin_cooldown; + + Ok(()) +} diff --git a/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs b/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs new file mode 100644 index 0000000..1766356 --- /dev/null +++ b/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs @@ -0,0 +1,72 @@ +use anchor_lang::prelude::*; + +use crate::{ + errors::WhitelistError, + state::{EntityRegistry, GlobalSettings}, + types::{EntityType, WhitelistStatus}, +}; + +#[derive(Accounts)] +#[instruction(entity_type: EntityType, entity_pubkey: Pubkey)] +pub struct SetEntityWhitelistStatus<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + #[account( + init_if_needed, + seeds = [b"entity-registry", [entity_type as u8].as_ref(), entity_pubkey.as_ref()], + bump, + payer = admin, + space = 8 + EntityRegistry::INIT_SPACE + )] + pub entity_registry: Box>, + + #[account( + seeds = [b"global-settings"], + bump = global_settings.bump, + has_one = admin @ WhitelistError::OnlyAdmin + )] + pub global_settings: Box>, + + pub system_program: Program<'info, System>, +} + +pub fn set_entity_whitelist_status( + ctx: Context, + entity_type: EntityType, + entity_pubkey: Pubkey, + status: WhitelistStatus, +) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + let entity_registry = &mut ctx.accounts.entity_registry; + + // TODO: fix PDA check + + if entity_registry.last_update == 0 { + entity_registry.entity_type = entity_type; + entity_registry.entity_pubkey = entity_pubkey; + entity_registry.bump = ctx.bumps.entity_registry; + } + entity_registry.status = status; + entity_registry.last_update = now; + entity_registry.updated_by = ctx.accounts.admin.key(); + + emit!(SetEntityWhitelistStatusEvent { + entity_type, + entity_pubkey, + status, + timestamp: now, + updated_by: entity_registry.updated_by, + }); + + Ok(()) +} + +#[event] +pub struct SetEntityWhitelistStatusEvent { + pub entity_type: EntityType, + pub entity_pubkey: Pubkey, + pub status: WhitelistStatus, + pub timestamp: u64, + pub updated_by: Pubkey, +} diff --git a/packages/svm/programs/whitelist/src/instructions/set_proposed_admin.rs b/packages/svm/programs/whitelist/src/instructions/set_proposed_admin.rs new file mode 100644 index 0000000..48c28fc --- /dev/null +++ b/packages/svm/programs/whitelist/src/instructions/set_proposed_admin.rs @@ -0,0 +1,47 @@ +use anchor_lang::prelude::*; + +use crate::{errors::WhitelistError, state::GlobalSettings}; + +#[derive(Accounts)] +pub struct SetProposedAdmin<'info> { + #[account(mut)] + pub proposed_admin: Signer<'info>, + + #[account( + mut, + seeds = [b"global-settings"], + bump = global_settings.bump, + constraint = + global_settings.proposed_admin == Some(proposed_admin.key()) @ WhitelistError::OnlyProposedAdmin + )] + pub global_settings: Box>, +} + +pub fn set_proposed_admin(ctx: Context) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + let global_settings = &mut ctx.accounts.global_settings; + + let can_change = global_settings.proposed_admin_next_change_timestamp < now; + + match (global_settings.proposed_admin, can_change) { + (Some(_proposed_admin), true) => { + emit!(SetProposedAdminEvent { + old_admin: global_settings.admin, + new_admin: _proposed_admin, + }); + + global_settings.admin = _proposed_admin; + global_settings.proposed_admin = None; + global_settings.proposed_admin_next_change_timestamp = u64::MAX; + } + _ => err!(WhitelistError::SetProposedAdminError)?, + } + + Ok(()) +} + +#[event] +pub struct SetProposedAdminEvent { + pub old_admin: Pubkey, + pub new_admin: Pubkey, +} diff --git a/packages/svm/programs/whitelist/src/lib.rs b/packages/svm/programs/whitelist/src/lib.rs new file mode 100644 index 0000000..057866b --- /dev/null +++ b/packages/svm/programs/whitelist/src/lib.rs @@ -0,0 +1,41 @@ +use anchor_lang::prelude::*; + +declare_id!("7PwVkjnnapxytWFW69WFDLhfVZZgKhBE9m3zwcDZmncr"); + +pub mod constants; +pub mod errors; +pub mod instructions; +pub mod state; +pub mod types; + +use crate::{instructions::*, types::*}; + +#[program] +pub mod whitelist { + use super::*; + + pub fn initialize( + ctx: Context, + admin: Pubkey, + proposed_admin_cooldown: u64, + ) -> Result<()> { + instructions::initialize(ctx, admin, proposed_admin_cooldown) + } + + pub fn propose_admin(ctx: Context, proposed_admin: Pubkey) -> Result<()> { + instructions::propose_admin(ctx, proposed_admin) + } + + pub fn set_entity_whitelist_status( + ctx: Context, + entity_type: EntityType, + entity_pubkey: Pubkey, + status: WhitelistStatus, + ) -> Result<()> { + instructions::set_entity_whitelist_status(ctx, entity_type, entity_pubkey, status) + } + + pub fn set_proposed_admin(ctx: Context) -> Result<()> { + instructions::set_proposed_admin(ctx) + } +} diff --git a/packages/svm/programs/whitelist/src/state/entity_registry.rs b/packages/svm/programs/whitelist/src/state/entity_registry.rs new file mode 100644 index 0000000..a404175 --- /dev/null +++ b/packages/svm/programs/whitelist/src/state/entity_registry.rs @@ -0,0 +1,14 @@ +use anchor_lang::prelude::*; + +use crate::types::{EntityType, WhitelistStatus}; + +#[account] +#[derive(InitSpace)] +pub struct EntityRegistry { + pub entity_type: EntityType, + pub entity_pubkey: Pubkey, + pub status: WhitelistStatus, + pub last_update: u64, + pub updated_by: Pubkey, + pub bump: u8, +} diff --git a/packages/svm/programs/whitelist/src/state/global_settings.rs b/packages/svm/programs/whitelist/src/state/global_settings.rs new file mode 100644 index 0000000..0d8f2e2 --- /dev/null +++ b/packages/svm/programs/whitelist/src/state/global_settings.rs @@ -0,0 +1,11 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(InitSpace)] +pub struct GlobalSettings { + pub admin: Pubkey, + pub proposed_admin: Option, + pub proposed_admin_cooldown: u64, + pub proposed_admin_next_change_timestamp: u64, + pub bump: u8, +} diff --git a/packages/svm/programs/whitelist/src/state/mod.rs b/packages/svm/programs/whitelist/src/state/mod.rs new file mode 100644 index 0000000..e0bef0c --- /dev/null +++ b/packages/svm/programs/whitelist/src/state/mod.rs @@ -0,0 +1,5 @@ +pub mod entity_registry; +pub mod global_settings; + +pub use entity_registry::*; +pub use global_settings::*; diff --git a/packages/svm/programs/whitelist/src/types/entity_type.rs b/packages/svm/programs/whitelist/src/types/entity_type.rs new file mode 100644 index 0000000..1d798cc --- /dev/null +++ b/packages/svm/programs/whitelist/src/types/entity_type.rs @@ -0,0 +1,13 @@ +use anchor_lang::prelude::*; + +#[repr(u8)] +#[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize)] +pub enum EntityType { + Validator = 1, + Axia = 2, + Solver = 3, +} + +impl anchor_lang::Space for EntityType { + const INIT_SPACE: usize = 1; +} diff --git a/packages/svm/programs/whitelist/src/types/mod.rs b/packages/svm/programs/whitelist/src/types/mod.rs new file mode 100644 index 0000000..7e7c4e8 --- /dev/null +++ b/packages/svm/programs/whitelist/src/types/mod.rs @@ -0,0 +1,5 @@ +pub mod entity_type; +pub mod whitelist_status; + +pub use entity_type::*; +pub use whitelist_status::*; diff --git a/packages/svm/programs/whitelist/src/types/whitelist_status.rs b/packages/svm/programs/whitelist/src/types/whitelist_status.rs new file mode 100644 index 0000000..a4e916b --- /dev/null +++ b/packages/svm/programs/whitelist/src/types/whitelist_status.rs @@ -0,0 +1,12 @@ +use anchor_lang::prelude::*; + +#[repr(u8)] +#[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize)] +pub enum WhitelistStatus { + Whitelisted = 1, + Blacklisted = 2, +} + +impl anchor_lang::Space for WhitelistStatus { + const INIT_SPACE: usize = 1; +} diff --git a/packages/svm/rust-toolchain.toml b/packages/svm/rust-toolchain.toml new file mode 100644 index 0000000..cb684c0 --- /dev/null +++ b/packages/svm/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.89.0" +components = ["rustfmt","clippy"] +profile = "minimal" From 48c1f00bcb59a05ab97049d0192805d692e447b0 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 3 Nov 2025 12:06:39 -0300 Subject: [PATCH 17/41] Add WhitelistSDK --- .../set_entity_whitelist_status.rs | 6 +- packages/svm/sdks/whitelist/Whitelist.ts | 128 ++++++++++++++++++ 2 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 packages/svm/sdks/whitelist/Whitelist.ts diff --git a/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs b/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs index 1766356..2a3dacf 100644 --- a/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs +++ b/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs @@ -7,14 +7,14 @@ use crate::{ }; #[derive(Accounts)] -#[instruction(entity_type: EntityType, entity_pubkey: Pubkey)] +#[instruction(entity_type: u8, entity_pubkey: Pubkey)] pub struct SetEntityWhitelistStatus<'info> { #[account(mut)] pub admin: Signer<'info>, #[account( init_if_needed, - seeds = [b"entity-registry", [entity_type as u8].as_ref(), entity_pubkey.as_ref()], + seeds = [b"entity-registry", entity_type.to_le_bytes().as_ref(), entity_pubkey.as_ref()], bump, payer = admin, space = 8 + EntityRegistry::INIT_SPACE @@ -40,8 +40,6 @@ pub fn set_entity_whitelist_status( let now = Clock::get()?.unix_timestamp as u64; let entity_registry = &mut ctx.accounts.entity_registry; - // TODO: fix PDA check - if entity_registry.last_update == 0 { entity_registry.entity_type = entity_type; entity_registry.entity_pubkey = entity_pubkey; diff --git a/packages/svm/sdks/whitelist/Whitelist.ts b/packages/svm/sdks/whitelist/Whitelist.ts new file mode 100644 index 0000000..57fe9da --- /dev/null +++ b/packages/svm/sdks/whitelist/Whitelist.ts @@ -0,0 +1,128 @@ +import { Program, Provider, web3, BN, IdlTypes } from '@coral-xyz/anchor' + +import * as WhitelistIDL from '../../target/idl/whitelist.json' +import { Whitelist } from '../../target/types/whitelist' + +export enum EntityType { + Validator = 1, + Axia = 2, + Solver = 3 +} + +export enum WhitelistStatus { + Whitelisted = 1, + Blacklisted = 2 +} + +export default class WhitelistSDK { + protected program: Program + + constructor(provider: Provider) { + this.program = new Program(WhitelistIDL, provider) + } + + async initializeIx( + deployer: web3.PublicKey, + admin: web3.PublicKey, + proposedAdminCooldown: number + ): Promise { + const globalSettings = this.getGlobalSettingsPubkey() + const ix = await this.program.methods + .initialize(admin, new BN(proposedAdminCooldown)) + .accountsPartial({ + deployer, + globalSettings, + }) + .instruction() + return ix + } + + async proposeAdminIx( + admin: web3.PublicKey, + proposedAdmin: web3.PublicKey + ): Promise { + const globalSettings = this.getGlobalSettingsPubkey() + const ix = await this.program.methods + .proposeAdmin(proposedAdmin) + .accountsPartial({ + admin, + globalSettings, + }) + .instruction() + return ix + } + + async setEntityWhitelistStatusIx( + admin: web3.PublicKey, + entityType: EntityType, + entityPubkey: web3.PublicKey, + status: WhitelistStatus + ): Promise { + const entityRegistry = this.getEntityRegistryPubkey(entityType, entityPubkey) + const globalSettings = this.getGlobalSettingsPubkey() + const ix = await this.program.methods + .setEntityWhitelistStatus(this.entityTypeToAnchorEnum(entityType), entityPubkey, this.whitelistStatusToAnchorEnum(status)) + .accountsPartial({ + admin, + entityRegistry, + globalSettings, + }) + .instruction() + return ix + } + + async setProposedAdminIx( + proposedAdmin: web3.PublicKey + ): Promise { + const globalSettings = this.getGlobalSettingsPubkey() + const ix = await this.program.methods + .setProposedAdmin() + .accountsPartial({ + proposedAdmin, + globalSettings, + }) + .instruction() + return ix + } + + getGlobalSettingsPubkey(): web3.PublicKey { + return web3.PublicKey.findProgramAddressSync( + [Buffer.from('global-settings')], + this.program.programId + )[0] + } + + getEntityRegistryPubkey( + entityType: EntityType, + entityPubkey: web3.PublicKey + ): web3.PublicKey { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('entity-registry'), + Buffer.from([entityType]), + entityPubkey.toBuffer(), + ], + this.program.programId + )[0] + } + + entityTypeToAnchorEnum( + entityType: EntityType + ): IdlTypes['entityType'] { + if (entityType === EntityType.Validator) return { validator: {} } + if (entityType === EntityType.Axia) return { axia: {} } + if (entityType === EntityType.Solver) return { solver: {} } + + throw new Error(`Unsupported entity type ${entityType}`) + } + + whitelistStatusToAnchorEnum( + status: WhitelistStatus + ): IdlTypes['whitelistStatus'] { + if (status === WhitelistStatus.Whitelisted) return { whitelisted: {} } + if (status === WhitelistStatus.Blacklisted) return { blacklisted: {} } + + throw new Error(`Unsupported whitelist status ${status}`) + } +} + From 93133469098b1c9fd32bad07f33b912f4a36ee93 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 3 Nov 2025 13:18:54 -0300 Subject: [PATCH 18/41] Add Whitelist tests --- .../set_entity_whitelist_status.rs | 4 +- packages/svm/sdks/whitelist/Whitelist.ts | 17 +- packages/svm/tests/utils.ts | 15 +- packages/svm/tests/whitelist.test.ts | 406 ++++++++++++++++++ 4 files changed, 431 insertions(+), 11 deletions(-) create mode 100644 packages/svm/tests/whitelist.test.ts diff --git a/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs b/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs index 2a3dacf..8d2b234 100644 --- a/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs +++ b/packages/svm/programs/whitelist/src/instructions/set_entity_whitelist_status.rs @@ -7,14 +7,14 @@ use crate::{ }; #[derive(Accounts)] -#[instruction(entity_type: u8, entity_pubkey: Pubkey)] +#[instruction(entity_type: EntityType, entity_pubkey: Pubkey)] pub struct SetEntityWhitelistStatus<'info> { #[account(mut)] pub admin: Signer<'info>, #[account( init_if_needed, - seeds = [b"entity-registry", entity_type.to_le_bytes().as_ref(), entity_pubkey.as_ref()], + seeds = [b"entity-registry".as_ref(), &[entity_type as u8], entity_pubkey.as_ref()], bump, payer = admin, space = 8 + EntityRegistry::INIT_SPACE diff --git a/packages/svm/sdks/whitelist/Whitelist.ts b/packages/svm/sdks/whitelist/Whitelist.ts index 57fe9da..36f0d23 100644 --- a/packages/svm/sdks/whitelist/Whitelist.ts +++ b/packages/svm/sdks/whitelist/Whitelist.ts @@ -22,7 +22,6 @@ export default class WhitelistSDK { } async initializeIx( - deployer: web3.PublicKey, admin: web3.PublicKey, proposedAdminCooldown: number ): Promise { @@ -30,7 +29,7 @@ export default class WhitelistSDK { const ix = await this.program.methods .initialize(admin, new BN(proposedAdminCooldown)) .accountsPartial({ - deployer, + deployer: this.getSignerKey(), globalSettings, }) .instruction() @@ -38,14 +37,13 @@ export default class WhitelistSDK { } async proposeAdminIx( - admin: web3.PublicKey, proposedAdmin: web3.PublicKey ): Promise { const globalSettings = this.getGlobalSettingsPubkey() const ix = await this.program.methods .proposeAdmin(proposedAdmin) .accountsPartial({ - admin, + admin: this.getSignerKey(), globalSettings, }) .instruction() @@ -53,7 +51,6 @@ export default class WhitelistSDK { } async setEntityWhitelistStatusIx( - admin: web3.PublicKey, entityType: EntityType, entityPubkey: web3.PublicKey, status: WhitelistStatus @@ -63,7 +60,7 @@ export default class WhitelistSDK { const ix = await this.program.methods .setEntityWhitelistStatus(this.entityTypeToAnchorEnum(entityType), entityPubkey, this.whitelistStatusToAnchorEnum(status)) .accountsPartial({ - admin, + admin: this.getSignerKey(), entityRegistry, globalSettings, }) @@ -72,19 +69,23 @@ export default class WhitelistSDK { } async setProposedAdminIx( - proposedAdmin: web3.PublicKey ): Promise { const globalSettings = this.getGlobalSettingsPubkey() const ix = await this.program.methods .setProposedAdmin() .accountsPartial({ - proposedAdmin, + proposedAdmin: this.getSignerKey(), globalSettings, }) .instruction() return ix } + getSignerKey(): web3.PublicKey { + if (!this.program.provider.wallet) throw new Error('Must set program provider wallet') + return this.program.provider.wallet?.publicKey + } + getGlobalSettingsPubkey(): web3.PublicKey { return web3.PublicKey.findProgramAddressSync( [Buffer.from('global-settings')], diff --git a/packages/svm/tests/utils.ts b/packages/svm/tests/utils.ts index bcf0463..75bdbad 100644 --- a/packages/svm/tests/utils.ts +++ b/packages/svm/tests/utils.ts @@ -1,6 +1,6 @@ import { web3 } from '@coral-xyz/anchor' import { LiteSVMProvider } from 'anchor-litesvm' -import { FailedTransactionMetadata, TransactionMetadata } from 'litesvm' +import { Clock, FailedTransactionMetadata, TransactionMetadata } from 'litesvm' export async function signAndSendTx( provider: LiteSVMProvider, @@ -22,3 +22,16 @@ export async function makeTxSignAndSend( ): Promise { return signAndSendTx(provider, makeTx(...ixs)) } + +export function warpSeconds(provider: LiteSVMProvider, seconds: number): void { + const clock = provider.client.getClock() + provider.client.setClock( + new Clock( + clock.slot, + clock.epochStartTimestamp, + clock.epoch, + clock.leaderScheduleEpoch, + clock.unixTimestamp + BigInt(seconds) + ) + ) +} diff --git a/packages/svm/tests/whitelist.test.ts b/packages/svm/tests/whitelist.test.ts new file mode 100644 index 0000000..9bef18c --- /dev/null +++ b/packages/svm/tests/whitelist.test.ts @@ -0,0 +1,406 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { Program, Wallet, web3 } from '@coral-xyz/anchor' +import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' +import { expect } from 'chai' +import fs from 'fs' +import { LiteSVM } from 'litesvm' +import os from 'os' +import path from 'path' + +import WhitelistSDK, { EntityType, WhitelistStatus } from '../sdks/whitelist/Whitelist' +import * as WhitelistIDL from '../target/idl/whitelist.json' +import { Whitelist } from '../target/types/whitelist' +import { makeTxSignAndSend, warpSeconds } from './utils' + +describe('Whitelist Program', () => { + let client: LiteSVM + + let deployer: web3.Keypair + let admin: web3.Keypair + let proposedAdmin: web3.Keypair + let malicious: web3.Keypair + + let deployerProvider: LiteSVMProvider + let adminProvider: LiteSVMProvider + let proposedAdminProvider: LiteSVMProvider + let maliciousProvider: LiteSVMProvider + + let program: Program + + let deployerSdk: WhitelistSDK + let adminSdk: WhitelistSDK + let proposedAdminSdk: WhitelistSDK + let maliciousSdk: WhitelistSDK + + before(async () => { + deployer = web3.Keypair.fromSecretKey( + Uint8Array.from(JSON.parse(fs.readFileSync(path.join(os.homedir(), '.config', 'solana', 'id.json'), 'utf8'))) + ) + admin = web3.Keypair.generate() + proposedAdmin = web3.Keypair.generate() + malicious = web3.Keypair.generate() + + client = fromWorkspace(path.join(__dirname, '../')).withBuiltins() + + deployerProvider = new LiteSVMProvider(client, new Wallet(deployer)) + adminProvider = new LiteSVMProvider(client, new Wallet(admin)) + proposedAdminProvider = new LiteSVMProvider(client, new Wallet(proposedAdmin)) + maliciousProvider = new LiteSVMProvider(client, new Wallet(malicious)) + + program = new Program(WhitelistIDL as any, deployerProvider) + + deployerSdk = new WhitelistSDK(deployerProvider) + adminSdk = new WhitelistSDK(adminProvider) + proposedAdminSdk = new WhitelistSDK(proposedAdminProvider) + maliciousSdk = new WhitelistSDK(maliciousProvider) + + deployerProvider.client.airdrop(deployer.publicKey, BigInt(100_000_000_000)) + deployerProvider.client.airdrop(admin.publicKey, BigInt(100_000_000_000)) + deployerProvider.client.airdrop(proposedAdmin.publicKey, BigInt(100_000_000_000)) + deployerProvider.client.airdrop(malicious.publicKey, BigInt(100_000_000_000)) + }) + + beforeEach(() => { + client.expireBlockhash() + }) + + describe('Whitelist', () => { + describe('initialize', () => { + it('cant initialize if not deployer', async () => { + const newAdmin = web3.Keypair.generate().publicKey + const proposedAdminCooldown = 3600 + + const ix = await maliciousSdk.initializeIx(newAdmin, proposedAdminCooldown) + const res = await makeTxSignAndSend(maliciousProvider, ix) + + expect(res.toString()).to.include(`Only deployer can call this instruction`) + }) + + it('should call initialize', async () => { + const proposedAdminCooldown = 3600 + + const ix = await deployerSdk.initializeIx(admin.publicKey, proposedAdminCooldown) + await makeTxSignAndSend(deployerProvider, ix) + + const settings = await program.account.globalSettings.fetch(deployerSdk.getGlobalSettingsPubkey()) + expect(settings.proposedAdmin).to.be.null + expect(settings.proposedAdminCooldown.toNumber()).to.be.eq(3600) + expect(settings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX + }) + + it('cant call initialize again', async () => { + const proposedAdminCooldown = 3600 + + const ix = await deployerSdk.initializeIx(admin.publicKey, proposedAdminCooldown) + const res = await makeTxSignAndSend(deployerProvider, ix) + + expect(res.toString()).to.include( + `Allocate: account Address { address: ${deployerSdk.getGlobalSettingsPubkey()}, base: None } already in use` + ) + }) + }) + + describe('propose_admin and set_proposed_admin', () => { + it('cant propose admin if not admin', async () => { + const ix = await maliciousSdk.proposeAdminIx(proposedAdmin.publicKey) + const res = await makeTxSignAndSend(maliciousProvider, ix) + + expect(res.toString()).to.include(`Only admin can call this instruction`) + }) + + it('cant set proposed admin if no next admin was proposed yet', async () => { + const ix = await adminSdk.setProposedAdminIx() + const res = await makeTxSignAndSend(adminProvider, ix) + + expect(res.toString()).to.include( + `Only proposed admin can call this instruction` + ) + }) + + it('should propose admin successfully', async () => { + const ix = await adminSdk.proposeAdminIx(proposedAdmin.publicKey) + await makeTxSignAndSend(adminProvider, ix) + + const updatedSettings = await program.account.globalSettings.fetch(adminSdk.getGlobalSettingsPubkey()) + expect(updatedSettings.proposedAdmin?.toString()).to.be.eq(proposedAdmin.publicKey.toString()) + expect(updatedSettings.proposedAdminNextChangeTimestamp.toNumber()).to.be.greaterThan(0) + }) + + it('cant propose admin if one is already proposed', async () => { + const proposedAdmin2 = web3.Keypair.generate().publicKey + + const ix = await adminSdk.proposeAdminIx(proposedAdmin2) + const res = await makeTxSignAndSend(adminProvider, ix) + + expect(res.toString()).to.include(`Proposed admin is already set`) + }) + + it('cant set proposed admin if not proposed admin', async () => { + const ix = await maliciousSdk.setProposedAdminIx() + const res = await makeTxSignAndSend(maliciousProvider, ix) + + expect(res.toString()).to.include(`Only proposed admin can call this instruction`) + }) + + it('cant set proposed admin if cooldown hasnt passed', async () => { + const ix = await proposedAdminSdk.setProposedAdminIx() + const res = await makeTxSignAndSend(proposedAdminProvider, ix) + + expect(res.toString()).to.include( + `Can't set proposed admin - either no next admin is proposed or cooldown period is not over yet` + ) + }) + + it('should set proposed admin successfully after cooldown', async () => { + warpSeconds(deployerProvider, 3601) + + const ix = await proposedAdminSdk.setProposedAdminIx() + await makeTxSignAndSend(proposedAdminProvider, ix) + + const updatedSettings = await program.account.globalSettings.fetch(deployerSdk.getGlobalSettingsPubkey()) + expect(updatedSettings.admin.toString()).to.be.eq(proposedAdmin.publicKey.toString()) + expect(updatedSettings.proposedAdmin).to.be.null + expect(updatedSettings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX + }) + + it('resets admin to original one for next tests', async () => { + const ix = await proposedAdminSdk.proposeAdminIx(admin.publicKey) + await makeTxSignAndSend(proposedAdminProvider, ix) + + warpSeconds(deployerProvider, 3601) + + const ix2 = await adminSdk.setProposedAdminIx() + await makeTxSignAndSend(adminProvider, ix2) + + const updatedSettings = await program.account.globalSettings.fetch(deployerSdk.getGlobalSettingsPubkey()) + expect(updatedSettings.admin.toString()).to.be.eq(admin.publicKey.toString()) + expect(updatedSettings.proposedAdmin).to.be.null + expect(updatedSettings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX + + }) + }) + + describe('set_entity_whitelist_status', () => { + let validator: web3.PublicKey + let axia: web3.PublicKey + let solver: web3.PublicKey + let validator2: web3.PublicKey + let axia2: web3.PublicKey + let solver2: web3.PublicKey + + before(() => { + validator = web3.Keypair.generate().publicKey + axia = web3.Keypair.generate().publicKey + solver = web3.Keypair.generate().publicKey + validator2 = web3.Keypair.generate().publicKey + axia2 = web3.Keypair.generate().publicKey + solver2 = web3.Keypair.generate().publicKey + }) + + it('cant set status if not admin', async () => { + const ix = await maliciousSdk.setEntityWhitelistStatusIx( + EntityType.Validator, + validator, + WhitelistStatus.Whitelisted + ) + const res = await makeTxSignAndSend(maliciousProvider, ix) + + expect(res.toString()).to.include(`Only admin can call this instruction`) + }) + + it('should set whitelist status successfully (validator)', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Validator, + validator, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Validator, validator) + ) + expect(entityRegistry.entityType).to.deep.include({ validator: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(validator.toString()) + expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) + expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) + }) + + it('should set whitelist status successfully (axia)', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Axia, + axia, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) + ) + expect(entityRegistry.entityType).to.deep.include({ axia: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(axia.toString()) + expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) + expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) + }) + + it('should set whitelist status successfully (solver)', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Solver, + solver, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Solver, solver) + ) + expect(entityRegistry.entityType).to.deep.include({ solver: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(solver.toString()) + expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) + expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) + }) + + it('should update status correctly (whitelist to blacklist transition) (validator)', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Validator, + validator, + WhitelistStatus.Blacklisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Validator, validator) + ) + expect(entityRegistry.status).to.deep.include({ blacklisted: {} }) + }) + + it('should update status correctly (whitelist to blacklist transition) (axia)', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Axia, + axia, + WhitelistStatus.Blacklisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) + ) + expect(entityRegistry.status).to.deep.include({ blacklisted: {} }) + }) + + it('should update status correctly (whitelist to blacklist transition) (solver)', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Solver, + solver, + WhitelistStatus.Blacklisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Solver, solver) + ) + expect(entityRegistry.status).to.deep.include({ blacklisted: {} }) + }) + + it('should update status correctly (blacklist to whitelist transition) (validator)', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Validator, + validator, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Validator, validator) + ) + expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + }) + + it('should update status correctly (blacklist to whitelist transition) (axia)', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Axia, + axia, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) + ) + expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + }) + + it('should update status correctly (blacklist to whitelist transition) (solver)', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Solver, + solver, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Solver, solver) + ) + expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + }) + + it('can whitelist another validator', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Validator, + validator2, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Validator, validator2) + ) + expect(entityRegistry.entityType).to.deep.include({ validator: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(validator2.toString()) + expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) + expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) + }) + + it('can whitelist another axia', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Axia, + axia2, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Axia, axia2) + ) + expect(entityRegistry.entityType).to.deep.include({ axia: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(axia2.toString()) + expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) + expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) + }) + + it('can whitelist another solver', async () => { + const ix = await adminSdk.setEntityWhitelistStatusIx( + EntityType.Solver, + solver2, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(adminProvider, ix) + + const entityRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Solver, solver2) + ) + expect(entityRegistry.entityType).to.deep.include({ solver: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(solver2.toString()) + expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) + expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) + }) + }) + }) +}) From 5423cea9adb88055d8657dd70f32e74db9e3d876 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 3 Nov 2025 13:31:57 -0300 Subject: [PATCH 19/41] Fix lint issues --- packages/svm/sdks/whitelist/Whitelist.ts | 54 +++++++++--------------- packages/svm/tests/whitelist.test.ts | 53 ++++------------------- 2 files changed, 30 insertions(+), 77 deletions(-) diff --git a/packages/svm/sdks/whitelist/Whitelist.ts b/packages/svm/sdks/whitelist/Whitelist.ts index 36f0d23..1cc2f72 100644 --- a/packages/svm/sdks/whitelist/Whitelist.ts +++ b/packages/svm/sdks/whitelist/Whitelist.ts @@ -1,17 +1,22 @@ -import { Program, Provider, web3, BN, IdlTypes } from '@coral-xyz/anchor' +import { BN, IdlTypes, Program, Provider, web3 } from '@coral-xyz/anchor' import * as WhitelistIDL from '../../target/idl/whitelist.json' import { Whitelist } from '../../target/types/whitelist' export enum EntityType { + // eslint-disable-next-line no-unused-vars Validator = 1, + // eslint-disable-next-line no-unused-vars Axia = 2, - Solver = 3 + // eslint-disable-next-line no-unused-vars + Solver = 3, } export enum WhitelistStatus { + // eslint-disable-next-line no-unused-vars Whitelisted = 1, - Blacklisted = 2 + // eslint-disable-next-line no-unused-vars + Blacklisted = 2, } export default class WhitelistSDK { @@ -21,10 +26,7 @@ export default class WhitelistSDK { this.program = new Program(WhitelistIDL, provider) } - async initializeIx( - admin: web3.PublicKey, - proposedAdminCooldown: number - ): Promise { + async initializeIx(admin: web3.PublicKey, proposedAdminCooldown: number): Promise { const globalSettings = this.getGlobalSettingsPubkey() const ix = await this.program.methods .initialize(admin, new BN(proposedAdminCooldown)) @@ -36,9 +38,7 @@ export default class WhitelistSDK { return ix } - async proposeAdminIx( - proposedAdmin: web3.PublicKey - ): Promise { + async proposeAdminIx(proposedAdmin: web3.PublicKey): Promise { const globalSettings = this.getGlobalSettingsPubkey() const ix = await this.program.methods .proposeAdmin(proposedAdmin) @@ -58,7 +58,11 @@ export default class WhitelistSDK { const entityRegistry = this.getEntityRegistryPubkey(entityType, entityPubkey) const globalSettings = this.getGlobalSettingsPubkey() const ix = await this.program.methods - .setEntityWhitelistStatus(this.entityTypeToAnchorEnum(entityType), entityPubkey, this.whitelistStatusToAnchorEnum(status)) + .setEntityWhitelistStatus( + this.entityTypeToAnchorEnum(entityType), + entityPubkey, + this.whitelistStatusToAnchorEnum(status) + ) .accountsPartial({ admin: this.getSignerKey(), entityRegistry, @@ -68,8 +72,7 @@ export default class WhitelistSDK { return ix } - async setProposedAdminIx( - ): Promise { + async setProposedAdminIx(): Promise { const globalSettings = this.getGlobalSettingsPubkey() const ix = await this.program.methods .setProposedAdmin() @@ -87,29 +90,17 @@ export default class WhitelistSDK { } getGlobalSettingsPubkey(): web3.PublicKey { - return web3.PublicKey.findProgramAddressSync( - [Buffer.from('global-settings')], - this.program.programId - )[0] + return web3.PublicKey.findProgramAddressSync([Buffer.from('global-settings')], this.program.programId)[0] } - getEntityRegistryPubkey( - entityType: EntityType, - entityPubkey: web3.PublicKey - ): web3.PublicKey { + getEntityRegistryPubkey(entityType: EntityType, entityPubkey: web3.PublicKey): web3.PublicKey { return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('entity-registry'), - Buffer.from([entityType]), - entityPubkey.toBuffer(), - ], + [Buffer.from('entity-registry'), Buffer.from([entityType]), entityPubkey.toBuffer()], this.program.programId )[0] } - entityTypeToAnchorEnum( - entityType: EntityType - ): IdlTypes['entityType'] { + entityTypeToAnchorEnum(entityType: EntityType): IdlTypes['entityType'] { if (entityType === EntityType.Validator) return { validator: {} } if (entityType === EntityType.Axia) return { axia: {} } if (entityType === EntityType.Solver) return { solver: {} } @@ -117,13 +108,10 @@ export default class WhitelistSDK { throw new Error(`Unsupported entity type ${entityType}`) } - whitelistStatusToAnchorEnum( - status: WhitelistStatus - ): IdlTypes['whitelistStatus'] { + whitelistStatusToAnchorEnum(status: WhitelistStatus): IdlTypes['whitelistStatus'] { if (status === WhitelistStatus.Whitelisted) return { whitelisted: {} } if (status === WhitelistStatus.Blacklisted) return { blacklisted: {} } throw new Error(`Unsupported whitelist status ${status}`) } } - diff --git a/packages/svm/tests/whitelist.test.ts b/packages/svm/tests/whitelist.test.ts index 9bef18c..ef75f1c 100644 --- a/packages/svm/tests/whitelist.test.ts +++ b/packages/svm/tests/whitelist.test.ts @@ -114,9 +114,7 @@ describe('Whitelist Program', () => { const ix = await adminSdk.setProposedAdminIx() const res = await makeTxSignAndSend(adminProvider, ix) - expect(res.toString()).to.include( - `Only proposed admin can call this instruction` - ) + expect(res.toString()).to.include(`Only proposed admin can call this instruction`) }) it('should propose admin successfully', async () => { @@ -178,7 +176,6 @@ describe('Whitelist Program', () => { expect(updatedSettings.admin.toString()).to.be.eq(admin.publicKey.toString()) expect(updatedSettings.proposedAdmin).to.be.null expect(updatedSettings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX - }) }) @@ -229,11 +226,7 @@ describe('Whitelist Program', () => { }) it('should set whitelist status successfully (axia)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( - EntityType.Axia, - axia, - WhitelistStatus.Whitelisted - ) + const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Axia, axia, WhitelistStatus.Whitelisted) await makeTxSignAndSend(adminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( @@ -247,11 +240,7 @@ describe('Whitelist Program', () => { }) it('should set whitelist status successfully (solver)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( - EntityType.Solver, - solver, - WhitelistStatus.Whitelisted - ) + const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Solver, solver, WhitelistStatus.Whitelisted) await makeTxSignAndSend(adminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( @@ -279,11 +268,7 @@ describe('Whitelist Program', () => { }) it('should update status correctly (whitelist to blacklist transition) (axia)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( - EntityType.Axia, - axia, - WhitelistStatus.Blacklisted - ) + const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Axia, axia, WhitelistStatus.Blacklisted) await makeTxSignAndSend(adminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( @@ -293,11 +278,7 @@ describe('Whitelist Program', () => { }) it('should update status correctly (whitelist to blacklist transition) (solver)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( - EntityType.Solver, - solver, - WhitelistStatus.Blacklisted - ) + const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Solver, solver, WhitelistStatus.Blacklisted) await makeTxSignAndSend(adminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( @@ -321,11 +302,7 @@ describe('Whitelist Program', () => { }) it('should update status correctly (blacklist to whitelist transition) (axia)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( - EntityType.Axia, - axia, - WhitelistStatus.Whitelisted - ) + const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Axia, axia, WhitelistStatus.Whitelisted) await makeTxSignAndSend(adminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( @@ -335,11 +312,7 @@ describe('Whitelist Program', () => { }) it('should update status correctly (blacklist to whitelist transition) (solver)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( - EntityType.Solver, - solver, - WhitelistStatus.Whitelisted - ) + const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Solver, solver, WhitelistStatus.Whitelisted) await makeTxSignAndSend(adminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( @@ -367,11 +340,7 @@ describe('Whitelist Program', () => { }) it('can whitelist another axia', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( - EntityType.Axia, - axia2, - WhitelistStatus.Whitelisted - ) + const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Axia, axia2, WhitelistStatus.Whitelisted) await makeTxSignAndSend(adminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( @@ -385,11 +354,7 @@ describe('Whitelist Program', () => { }) it('can whitelist another solver', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( - EntityType.Solver, - solver2, - WhitelistStatus.Whitelisted - ) + const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Solver, solver2, WhitelistStatus.Whitelisted) await makeTxSignAndSend(adminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( From be5807e22bba8f1f236de78c7a3124a95bc8da52 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 3 Nov 2025 17:21:36 -0300 Subject: [PATCH 20/41] Minor fixes in Whitelist Program --- packages/svm/programs/whitelist/src/constants.rs | 2 ++ packages/svm/programs/whitelist/src/errors.rs | 9 +++++++++ .../whitelist/src/instructions/initialize.rs | 16 +++++++++++++++- .../whitelist/src/instructions/propose_admin.rs | 5 +++-- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/svm/programs/whitelist/src/constants.rs b/packages/svm/programs/whitelist/src/constants.rs index 756ec67..79c805d 100644 --- a/packages/svm/programs/whitelist/src/constants.rs +++ b/packages/svm/programs/whitelist/src/constants.rs @@ -2,3 +2,5 @@ pub const DEPLOYER_KEY: &'static str = env!( "DEPLOYER_KEY", "Please set the DEPLOYER_KEY env variable before compiling." ); + +pub const MAX_COOLDOWN: u64 = 3600 * 24 * 30; diff --git a/packages/svm/programs/whitelist/src/errors.rs b/packages/svm/programs/whitelist/src/errors.rs index 45d5a12..8df3897 100644 --- a/packages/svm/programs/whitelist/src/errors.rs +++ b/packages/svm/programs/whitelist/src/errors.rs @@ -16,4 +16,13 @@ pub enum WhitelistError { #[msg("Can't set proposed admin - either no next admin is proposed or cooldown period is not over yet")] SetProposedAdminError, + + #[msg("Cooldown too large")] + CooldownTooLarge, + + #[msg("Cooldown can't be zero")] + CooldownCantBeZero, + + #[msg("Math error")] + MathError, } diff --git a/packages/svm/programs/whitelist/src/instructions/initialize.rs b/packages/svm/programs/whitelist/src/instructions/initialize.rs index 83646ab..7aa835d 100644 --- a/packages/svm/programs/whitelist/src/instructions/initialize.rs +++ b/packages/svm/programs/whitelist/src/instructions/initialize.rs @@ -1,7 +1,11 @@ use anchor_lang::prelude::*; use std::str::FromStr; -use crate::{constants::DEPLOYER_KEY, errors::WhitelistError, state::GlobalSettings}; +use crate::{ + constants::{DEPLOYER_KEY, MAX_COOLDOWN}, + errors::WhitelistError, + state::GlobalSettings, +}; #[derive(Accounts)] pub struct Initialize<'info> { @@ -31,6 +35,16 @@ pub fn initialize( WhitelistError::OnlyDeployer, ); + require!( + proposed_admin_cooldown > 0, + WhitelistError::CooldownCantBeZero, + ); + + require!( + proposed_admin_cooldown <= MAX_COOLDOWN, + WhitelistError::CooldownTooLarge, + ); + let global_settings = &mut ctx.accounts.global_settings; global_settings.admin = admin; diff --git a/packages/svm/programs/whitelist/src/instructions/propose_admin.rs b/packages/svm/programs/whitelist/src/instructions/propose_admin.rs index b95d5b0..75d2f61 100644 --- a/packages/svm/programs/whitelist/src/instructions/propose_admin.rs +++ b/packages/svm/programs/whitelist/src/instructions/propose_admin.rs @@ -25,8 +25,9 @@ pub fn propose_admin(ctx: Context, proposed_admin: Pubkey) -> Resu } global_settings.proposed_admin = Some(proposed_admin); - global_settings.proposed_admin_next_change_timestamp = - now + global_settings.proposed_admin_cooldown; + global_settings.proposed_admin_next_change_timestamp = now + .checked_add(global_settings.proposed_admin_cooldown) + .ok_or(WhitelistError::MathError)?; Ok(()) } From c6a22bb65212147ebb0a008ab3fd46e1d5f61d85 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 3 Nov 2025 17:51:24 -0300 Subject: [PATCH 21/41] Add more tests for Whitelist --- packages/svm/tests/whitelist.test.ts | 198 ++++++++++++++++++++++++--- 1 file changed, 176 insertions(+), 22 deletions(-) diff --git a/packages/svm/tests/whitelist.test.ts b/packages/svm/tests/whitelist.test.ts index ef75f1c..ea4facd 100644 --- a/packages/svm/tests/whitelist.test.ts +++ b/packages/svm/tests/whitelist.test.ts @@ -78,13 +78,32 @@ describe('Whitelist Program', () => { expect(res.toString()).to.include(`Only deployer can call this instruction`) }) - it('should call initialize', async () => { + it('cant initialize with cooldown = 0', async () => { + const proposedAdminCooldown = 0 + + const ix = await deployerSdk.initializeIx(admin.publicKey, proposedAdminCooldown) + const res = await makeTxSignAndSend(deployerProvider, ix) + + expect(res.toString()).to.include(`Cooldown can't be zero`) + }) + + it('cant initialize with cooldown > MAX_COOLDOWN', async () => { + const proposedAdminCooldown = 3600 * 24 * 30 + 1 + + const ix = await deployerSdk.initializeIx(admin.publicKey, proposedAdminCooldown) + const res = await makeTxSignAndSend(deployerProvider, ix) + + expect(res.toString()).to.include(`Cooldown too large`) + }) + + it('should initialize', async () => { const proposedAdminCooldown = 3600 const ix = await deployerSdk.initializeIx(admin.publicKey, proposedAdminCooldown) await makeTxSignAndSend(deployerProvider, ix) const settings = await program.account.globalSettings.fetch(deployerSdk.getGlobalSettingsPubkey()) + expect(settings.admin.toString()).to.be.eq(admin.publicKey.toString()) expect(settings.proposedAdmin).to.be.null expect(settings.proposedAdminCooldown.toNumber()).to.be.eq(3600) expect(settings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX @@ -177,6 +196,39 @@ describe('Whitelist Program', () => { expect(updatedSettings.proposedAdmin).to.be.null expect(updatedSettings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX }) + + it('can propose same admin as current admin', async () => { + const settings = await program.account.globalSettings.fetch(adminSdk.getGlobalSettingsPubkey()) + const currentAdmin = settings.admin + + const ix = await adminSdk.proposeAdminIx(currentAdmin) + await makeTxSignAndSend(adminProvider, ix) + + const updatedSettings = await program.account.globalSettings.fetch(adminSdk.getGlobalSettingsPubkey()) + expect(updatedSettings.proposedAdmin?.toString()).to.be.eq(currentAdmin.toString()) + + warpSeconds(deployerProvider, 3601) + + await makeTxSignAndSend(adminProvider, await adminSdk.setProposedAdminIx()) + const updatedSettings2 = await program.account.globalSettings.fetch(adminSdk.getGlobalSettingsPubkey()) + expect(updatedSettings2.admin.toString()).to.be.eq(admin.publicKey.toString()) + expect(updatedSettings2.proposedAdmin).to.be.null + }) + + it('should calculate proposed_admin_next_change_timestamp correctly', async () => { + const settingsBefore = await program.account.globalSettings.fetch(adminSdk.getGlobalSettingsPubkey()) + const cooldown = settingsBefore.proposedAdminCooldown.toNumber() + + const clockBefore = deployerProvider.client.getClock() + const nowBefore = Number(clockBefore.unixTimestamp) + + const ix = await adminSdk.proposeAdminIx(proposedAdmin.publicKey) + await makeTxSignAndSend(adminProvider, ix) + + const settingsAfter = await program.account.globalSettings.fetch(adminSdk.getGlobalSettingsPubkey()) + const expectedTimestamp = nowBefore + cooldown + expect(settingsAfter.proposedAdminNextChangeTimestamp.toNumber()).to.be.eq(expectedTimestamp) + }) }) describe('set_entity_whitelist_status', () => { @@ -218,10 +270,12 @@ describe('Whitelist Program', () => { const entityRegistry = await program.account.entityRegistry.fetch( adminSdk.getEntityRegistryPubkey(EntityType.Validator, validator) ) + const now = Number(client.getClock().unixTimestamp) + expect(entityRegistry.entityType).to.deep.include({ validator: {} }) expect(entityRegistry.entityPubkey.toString()).to.be.eq(validator.toString()) expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) - expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) + expect(entityRegistry.lastUpdate.toNumber()).to.be.eq(now) expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) }) @@ -232,10 +286,12 @@ describe('Whitelist Program', () => { const entityRegistry = await program.account.entityRegistry.fetch( adminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) ) + const now = Number(client.getClock().unixTimestamp) + expect(entityRegistry.entityType).to.deep.include({ axia: {} }) expect(entityRegistry.entityPubkey.toString()).to.be.eq(axia.toString()) expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) - expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) + expect(entityRegistry.lastUpdate.toNumber()).to.be.eq(now) expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) }) @@ -246,79 +302,156 @@ describe('Whitelist Program', () => { const entityRegistry = await program.account.entityRegistry.fetch( adminSdk.getEntityRegistryPubkey(EntityType.Solver, solver) ) + const now = Number(client.getClock().unixTimestamp) + expect(entityRegistry.entityType).to.deep.include({ solver: {} }) expect(entityRegistry.entityPubkey.toString()).to.be.eq(solver.toString()) expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) - expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) + expect(entityRegistry.lastUpdate.toNumber()).to.be.eq(now) expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) }) + it('warps some seconds and changes admin for next tests', async () => { + const diff = 3601 + + const then = Number(client.getClock().unixTimestamp) + const ix = await adminSdk.proposeAdminIx(proposedAdmin.publicKey) + await makeTxSignAndSend(adminProvider, ix) + + warpSeconds(deployerProvider, diff) + + const now = Number(client.getClock().unixTimestamp) + expect(now - then).to.be.eq(diff) + + const ix2 = await proposedAdminSdk.setProposedAdminIx() + await makeTxSignAndSend(proposedAdminProvider, ix2) + }) + it('should update status correctly (whitelist to blacklist transition) (validator)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( + const ix = await proposedAdminSdk.setEntityWhitelistStatusIx( EntityType.Validator, validator, WhitelistStatus.Blacklisted ) - await makeTxSignAndSend(adminProvider, ix) + await makeTxSignAndSend(proposedAdminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( - adminSdk.getEntityRegistryPubkey(EntityType.Validator, validator) + proposedAdminSdk.getEntityRegistryPubkey(EntityType.Validator, validator) ) + const now = Number(client.getClock().unixTimestamp) + + expect(entityRegistry.entityType).to.deep.include({ validator: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(validator.toString()) expect(entityRegistry.status).to.deep.include({ blacklisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.eq(now) + expect(entityRegistry.updatedBy.toString()).to.be.eq(proposedAdmin.publicKey.toString()) }) it('should update status correctly (whitelist to blacklist transition) (axia)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Axia, axia, WhitelistStatus.Blacklisted) - await makeTxSignAndSend(adminProvider, ix) + const ix = await proposedAdminSdk.setEntityWhitelistStatusIx(EntityType.Axia, axia, WhitelistStatus.Blacklisted) + await makeTxSignAndSend(proposedAdminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( - adminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) + proposedAdminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) ) + const now = Number(client.getClock().unixTimestamp) + + expect(entityRegistry.entityType).to.deep.include({ axia: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(axia.toString()) expect(entityRegistry.status).to.deep.include({ blacklisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.eq(now) + expect(entityRegistry.updatedBy.toString()).to.be.eq(proposedAdmin.publicKey.toString()) }) it('should update status correctly (whitelist to blacklist transition) (solver)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Solver, solver, WhitelistStatus.Blacklisted) - await makeTxSignAndSend(adminProvider, ix) + const ix = await proposedAdminSdk.setEntityWhitelistStatusIx( + EntityType.Solver, + solver, + WhitelistStatus.Blacklisted + ) + await makeTxSignAndSend(proposedAdminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( - adminSdk.getEntityRegistryPubkey(EntityType.Solver, solver) + proposedAdminSdk.getEntityRegistryPubkey(EntityType.Solver, solver) ) + const now = Number(client.getClock().unixTimestamp) + + expect(entityRegistry.entityType).to.deep.include({ solver: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(solver.toString()) expect(entityRegistry.status).to.deep.include({ blacklisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.eq(now) + expect(entityRegistry.updatedBy.toString()).to.be.eq(proposedAdmin.publicKey.toString()) }) it('should update status correctly (blacklist to whitelist transition) (validator)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx( + const ix = await proposedAdminSdk.setEntityWhitelistStatusIx( EntityType.Validator, validator, WhitelistStatus.Whitelisted ) - await makeTxSignAndSend(adminProvider, ix) + await makeTxSignAndSend(proposedAdminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( - adminSdk.getEntityRegistryPubkey(EntityType.Validator, validator) + proposedAdminSdk.getEntityRegistryPubkey(EntityType.Validator, validator) ) + const now = Number(client.getClock().unixTimestamp) + + expect(entityRegistry.entityType).to.deep.include({ validator: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(validator.toString()) expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.eq(now) + expect(entityRegistry.updatedBy.toString()).to.be.eq(proposedAdmin.publicKey.toString()) }) it('should update status correctly (blacklist to whitelist transition) (axia)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Axia, axia, WhitelistStatus.Whitelisted) - await makeTxSignAndSend(adminProvider, ix) + const ix = await proposedAdminSdk.setEntityWhitelistStatusIx(EntityType.Axia, axia, WhitelistStatus.Whitelisted) + await makeTxSignAndSend(proposedAdminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( - adminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) + proposedAdminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) ) + const now = Number(client.getClock().unixTimestamp) + + expect(entityRegistry.entityType).to.deep.include({ axia: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(axia.toString()) expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.eq(now) + expect(entityRegistry.updatedBy.toString()).to.be.eq(proposedAdmin.publicKey.toString()) }) it('should update status correctly (blacklist to whitelist transition) (solver)', async () => { - const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Solver, solver, WhitelistStatus.Whitelisted) - await makeTxSignAndSend(adminProvider, ix) + const ix = await proposedAdminSdk.setEntityWhitelistStatusIx( + EntityType.Solver, + solver, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(proposedAdminProvider, ix) const entityRegistry = await program.account.entityRegistry.fetch( - adminSdk.getEntityRegistryPubkey(EntityType.Solver, solver) + proposedAdminSdk.getEntityRegistryPubkey(EntityType.Solver, solver) ) + const now = Number(client.getClock().unixTimestamp) + + expect(entityRegistry.entityType).to.deep.include({ solver: {} }) + expect(entityRegistry.entityPubkey.toString()).to.be.eq(solver.toString()) expect(entityRegistry.status).to.deep.include({ whitelisted: {} }) + expect(entityRegistry.lastUpdate.toNumber()).to.be.eq(now) + expect(entityRegistry.updatedBy.toString()).to.be.eq(proposedAdmin.publicKey.toString()) + }) + + it('resets admin to original one for next tests', async () => { + const ix = await proposedAdminSdk.proposeAdminIx(admin.publicKey) + await makeTxSignAndSend(proposedAdminProvider, ix) + + warpSeconds(deployerProvider, 3601) + + const ix2 = await adminSdk.setProposedAdminIx() + await makeTxSignAndSend(adminProvider, ix2) + + const updatedSettings = await program.account.globalSettings.fetch(deployerSdk.getGlobalSettingsPubkey()) + expect(updatedSettings.admin.toString()).to.be.eq(admin.publicKey.toString()) + expect(updatedSettings.proposedAdmin).to.be.null + expect(updatedSettings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX }) it('can whitelist another validator', async () => { @@ -366,6 +499,27 @@ describe('Whitelist Program', () => { expect(entityRegistry.lastUpdate.toNumber()).to.be.greaterThan(0) expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) }) + + it('should create separate accounts for same pubkey with different entity types', async () => { + const ix1 = await adminSdk.setEntityWhitelistStatusIx(EntityType.Validator, axia, WhitelistStatus.Whitelisted) + await makeTxSignAndSend(adminProvider, ix1) + + const validatorRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Validator, axia) + ) + const axiaRegistry = await program.account.entityRegistry.fetch( + adminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) + ) + + expect(validatorRegistry.entityType).to.deep.include({ validator: {} }) + expect(validatorRegistry.status).to.deep.include({ whitelisted: {} }) + expect(axiaRegistry.entityType).to.deep.include({ axia: {} }) + expect(axiaRegistry.status).to.deep.include({ whitelisted: {} }) + + const validatorPda = adminSdk.getEntityRegistryPubkey(EntityType.Validator, axia) + const axiaPda = adminSdk.getEntityRegistryPubkey(EntityType.Axia, axia) + expect(validatorPda.toString()).to.not.eq(axiaPda.toString()) + }) }) }) }) From 84250e4b5a9ed8061390b3e33bbf519b7e385431 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 3 Nov 2025 18:10:09 -0300 Subject: [PATCH 22/41] Add idls/, implement set_paused_state --- packages/svm/idls/whitelist.json | 518 ++++++++++++++++++ .../instructions/change_whitelist_program.rs | 1 + .../settler/src/instructions/initialize.rs | 4 +- .../programs/settler/src/instructions/mod.rs | 4 +- .../settler/src/instructions/pause.rs | 8 - .../src/instructions/set_paused_state.rs | 46 ++ packages/svm/programs/settler/src/lib.rs | 9 +- packages/svm/sdks/settler/Settler.ts | 4 +- 8 files changed, 576 insertions(+), 18 deletions(-) create mode 100644 packages/svm/idls/whitelist.json delete mode 100644 packages/svm/programs/settler/src/instructions/pause.rs create mode 100644 packages/svm/programs/settler/src/instructions/set_paused_state.rs diff --git a/packages/svm/idls/whitelist.json b/packages/svm/idls/whitelist.json new file mode 100644 index 0000000..8bfadb2 --- /dev/null +++ b/packages/svm/idls/whitelist.json @@ -0,0 +1,518 @@ +{ + "address": "7PwVkjnnapxytWFW69WFDLhfVZZgKhBE9m3zwcDZmncr", + "metadata": { + "name": "whitelist", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "initialize", + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], + "accounts": [ + { + "name": "deployer", + "writable": true, + "signer": true + }, + { + "name": "global_settings", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 45, + 115, + 101, + 116, + 116, + 105, + 110, + 103, + 115 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "proposed_admin_cooldown", + "type": "u64" + } + ] + }, + { + "name": "propose_admin", + "discriminator": [ + 121, + 214, + 199, + 212, + 87, + 39, + 117, + 234 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true, + "relations": [ + "global_settings" + ] + }, + { + "name": "global_settings", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 45, + 115, + 101, + 116, + 116, + 105, + 110, + 103, + 115 + ] + } + ] + } + } + ], + "args": [ + { + "name": "proposed_admin", + "type": "pubkey" + } + ] + }, + { + "name": "set_entity_whitelist_status", + "discriminator": [ + 100, + 20, + 23, + 73, + 220, + 118, + 179, + 50 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true, + "relations": [ + "global_settings" + ] + }, + { + "name": "entity_registry", + "writable": true + }, + { + "name": "global_settings", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 45, + 115, + 101, + 116, + 116, + 105, + 110, + 103, + 115 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "entity_type", + "type": { + "defined": { + "name": "EntityType" + } + } + }, + { + "name": "entity_pubkey", + "type": "pubkey" + }, + { + "name": "status", + "type": { + "defined": { + "name": "WhitelistStatus" + } + } + } + ] + }, + { + "name": "set_proposed_admin", + "discriminator": [ + 160, + 170, + 199, + 240, + 246, + 244, + 199, + 2 + ], + "accounts": [ + { + "name": "proposed_admin", + "writable": true, + "signer": true + }, + { + "name": "global_settings", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 45, + 115, + 101, + 116, + 116, + 105, + 110, + 103, + 115 + ] + } + ] + } + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "EntityRegistry", + "discriminator": [ + 206, + 167, + 4, + 107, + 132, + 162, + 158, + 163 + ] + }, + { + "name": "GlobalSettings", + "discriminator": [ + 109, + 67, + 50, + 55, + 2, + 20, + 148, + 62 + ] + } + ], + "events": [ + { + "name": "SetEntityWhitelistStatusEvent", + "discriminator": [ + 137, + 194, + 109, + 101, + 80, + 30, + 4, + 114 + ] + }, + { + "name": "SetProposedAdminEvent", + "discriminator": [ + 153, + 83, + 248, + 103, + 132, + 126, + 171, + 96 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "OnlyDeployer", + "msg": "Only deployer can call this instruction" + }, + { + "code": 6001, + "name": "OnlyAdmin", + "msg": "Only admin can call this instruction" + }, + { + "code": 6002, + "name": "OnlyProposedAdmin", + "msg": "Only proposed admin can call this instruction" + }, + { + "code": 6003, + "name": "ProposedAdminIsAlreadySet", + "msg": "Proposed admin is already set" + }, + { + "code": 6004, + "name": "SetProposedAdminError", + "msg": "Can't set proposed admin - either no next admin is proposed or cooldown period is not over yet" + }, + { + "code": 6005, + "name": "CooldownTooLarge", + "msg": "Cooldown too large" + }, + { + "code": 6006, + "name": "CooldownCantBeZero", + "msg": "Cooldown can't be zero" + }, + { + "code": 6007, + "name": "MathError", + "msg": "Math error" + } + ], + "types": [ + { + "name": "EntityRegistry", + "type": { + "kind": "struct", + "fields": [ + { + "name": "entity_type", + "type": { + "defined": { + "name": "EntityType" + } + } + }, + { + "name": "entity_pubkey", + "type": "pubkey" + }, + { + "name": "status", + "type": { + "defined": { + "name": "WhitelistStatus" + } + } + }, + { + "name": "last_update", + "type": "u64" + }, + { + "name": "updated_by", + "type": "pubkey" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "EntityType", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "Validator" + }, + { + "name": "Axia" + }, + { + "name": "Solver" + } + ] + } + }, + { + "name": "GlobalSettings", + "type": { + "kind": "struct", + "fields": [ + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "proposed_admin", + "type": { + "option": "pubkey" + } + }, + { + "name": "proposed_admin_cooldown", + "type": "u64" + }, + { + "name": "proposed_admin_next_change_timestamp", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "SetEntityWhitelistStatusEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "entity_type", + "type": { + "defined": { + "name": "EntityType" + } + } + }, + { + "name": "entity_pubkey", + "type": "pubkey" + }, + { + "name": "status", + "type": { + "defined": { + "name": "WhitelistStatus" + } + } + }, + { + "name": "timestamp", + "type": "u64" + }, + { + "name": "updated_by", + "type": "pubkey" + } + ] + } + }, + { + "name": "SetProposedAdminEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "old_admin", + "type": "pubkey" + }, + { + "name": "new_admin", + "type": "pubkey" + } + ] + } + }, + { + "name": "WhitelistStatus", + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "Whitelisted" + }, + { + "name": "Blacklisted" + } + ] + } + } + ] +} diff --git a/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs index c7be3d8..ca70398 100644 --- a/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs +++ b/packages/svm/programs/settler/src/instructions/change_whitelist_program.rs @@ -4,5 +4,6 @@ use anchor_lang::prelude::*; pub struct ChangeWhitelistProgram {} pub fn change_whitelist_program(ctx: Context) -> Result<()> { + // TODO: check against crate::whitelist::ID Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/initialize.rs b/packages/svm/programs/settler/src/instructions/initialize.rs index 539200f..7aa463c 100644 --- a/packages/svm/programs/settler/src/instructions/initialize.rs +++ b/packages/svm/programs/settler/src/instructions/initialize.rs @@ -20,7 +20,7 @@ pub struct Initialize<'info> { pub system_program: Program<'info, System>, } -pub fn initialize(ctx: Context, whitelist_program: Pubkey) -> Result<()> { +pub fn initialize(ctx: Context) -> Result<()> { require_keys_eq!( ctx.accounts.deployer.key(), Pubkey::from_str(DEPLOYER_KEY).unwrap(), @@ -29,7 +29,7 @@ pub fn initialize(ctx: Context, whitelist_program: Pubkey) -> Result let settler_settings = &mut ctx.accounts.settler_settings; - settler_settings.whitelist_program = whitelist_program; + settler_settings.whitelist_program = crate::whitelist::ID; settler_settings.is_paused = false; settler_settings.bump = ctx.bumps.settler_settings; diff --git a/packages/svm/programs/settler/src/instructions/mod.rs b/packages/svm/programs/settler/src/instructions/mod.rs index 1a59e8a..b3732e6 100644 --- a/packages/svm/programs/settler/src/instructions/mod.rs +++ b/packages/svm/programs/settler/src/instructions/mod.rs @@ -7,7 +7,7 @@ pub mod create_intent; pub mod create_proposal; pub mod execute_proposal; pub mod initialize; -pub mod pause; +pub mod set_paused_state; pub use add_axia_sig::*; pub use add_instructions_to_proposal::*; @@ -18,4 +18,4 @@ pub use create_intent::*; pub use create_proposal::*; pub use execute_proposal::*; pub use initialize::*; -pub use pause::*; +pub use set_paused_state::*; diff --git a/packages/svm/programs/settler/src/instructions/pause.rs b/packages/svm/programs/settler/src/instructions/pause.rs deleted file mode 100644 index 8e2caa3..0000000 --- a/packages/svm/programs/settler/src/instructions/pause.rs +++ /dev/null @@ -1,8 +0,0 @@ -use anchor_lang::prelude::*; - -#[derive(Accounts)] -pub struct Pause {} - -pub fn pause(ctx: Context) -> Result<()> { - Ok(()) -} diff --git a/packages/svm/programs/settler/src/instructions/set_paused_state.rs b/packages/svm/programs/settler/src/instructions/set_paused_state.rs new file mode 100644 index 0000000..ca8b52e --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/set_paused_state.rs @@ -0,0 +1,46 @@ +use anchor_lang::prelude::*; + +use crate::state::SettlerSettings; + +#[derive(Accounts)] +pub struct SetPausedState<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + #[account( + seeds = [b"global-settings"], + bump = whitelist_program_global_settings.bump, + seeds::program = settler_settings.whitelist_program, + has_one = admin @ crate::whitelist::errors::ProgramError::OnlyAdmin + )] + pub whitelist_program_global_settings: + Box>, + + #[account( + mut, + seeds = [b"settler-settings"], + bump = settler_settings.bump + )] + pub settler_settings: Box>, +} + +pub fn set_paused_state(ctx: Context, is_paused: bool) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + + ctx.accounts.settler_settings.is_paused = is_paused; + + emit!(SetPausedStateEvent { + changed_at: now, + changed_by: ctx.accounts.admin.key(), + is_paused, + }); + + Ok(()) +} + +#[event] +pub struct SetPausedStateEvent { + changed_at: u64, + changed_by: Pubkey, + is_paused: bool, +} diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index e3675d8..a2bb537 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; declare_id!("HbNt35Ng8aM4NUy39evpCQqXEC4Nmaq16ewY8dyNF6NF"); +declare_program!(whitelist); pub mod constants; pub mod errors; @@ -46,11 +47,11 @@ pub mod settler { instructions::execute_proposal(ctx) } - pub fn initialize(ctx: Context, whitelist_program: Pubkey) -> Result<()> { - instructions::initialize(ctx, whitelist_program) + pub fn initialize(ctx: Context) -> Result<()> { + instructions::initialize(ctx) } - pub fn pause(ctx: Context) -> Result<()> { - instructions::pause(ctx) + pub fn set_paused_state(ctx: Context, is_paused: bool) -> Result<()> { + instructions::set_paused_state(ctx, is_paused) } } diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index bce3ef8..b4881f7 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -10,8 +10,8 @@ export default class SettlerSDK { this.program = new Program(SettlerIDL, provider) } - async initializeIx(whitelistProgram: web3.PublicKey): Promise { - const ix = await this.program.methods.initialize(whitelistProgram).instruction() + async initializeIx(): Promise { + const ix = await this.program.methods.initialize().instruction() return ix } From daee1534b40ad276f992089b188ac179e73b1c2e Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 3 Nov 2025 21:25:31 -0300 Subject: [PATCH 23/41] Add create_intent, extend_intent, claim_stale_intent (partially implemented) --- packages/svm/programs/settler/src/errors.rs | 24 ++++ .../src/instructions/claim_stale_intent.rs | 27 ++++ .../settler/src/instructions/create_intent.rs | 79 +++++++++- .../settler/src/instructions/extend_intent.rs | 55 +++++++ .../programs/settler/src/instructions/mod.rs | 4 + packages/svm/programs/settler/src/lib.rs | 44 +++++- .../svm/programs/settler/src/state/intent.rs | 66 ++++++++- .../programs/settler/src/state/proposal.rs | 34 ++++- .../settler/src/types/intent_event.rs | 9 ++ .../svm/programs/settler/src/types/max_fee.rs | 7 +- .../svm/programs/settler/src/types/mod.rs | 4 - .../svm/programs/settler/src/types/op_type.rs | 3 + .../settler/src/types/proposal_instruction.rs | 9 -- .../proposal_instruction_account_meta.rs | 7 - packages/svm/sdks/settler/Settler.ts | 136 +++++++++++++++++- packages/svm/sdks/settler/types.ts | 37 +++++ packages/svm/tests/settler.test.ts | 25 ++-- 17 files changed, 528 insertions(+), 42 deletions(-) create mode 100644 packages/svm/programs/settler/src/instructions/claim_stale_intent.rs create mode 100644 packages/svm/programs/settler/src/instructions/extend_intent.rs delete mode 100644 packages/svm/programs/settler/src/types/proposal_instruction.rs delete mode 100644 packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs create mode 100644 packages/svm/sdks/settler/types.ts diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index 4b93bbd..fa68add 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -4,4 +4,28 @@ use anchor_lang::prelude::*; pub enum SettlerError { #[msg("Only Deployer can call this instruction")] OnlyDeployer, + + #[msg("Only a whitelisted solver can call this instruction")] + OnlySolver, + + #[msg("Only a whitelisted Axia address can call this instruction")] + OnlyAxia, + + #[msg("Only a whitelisted validator can call this instruction")] + OnlyValidator, + + #[msg("Signer must be intent creator")] + IncorrectIntentCreator, + + #[msg("Intent is already final")] + IntentIsFinal, + + #[msg("Proposal is already final")] + ProposalIsFinal, + + #[msg("Intent not yet expired. Please wait for the deadline to pass")] + IntentNotYetExpired, + + #[msg("Deadline can't be in the past")] + DeadlineIsInThePast, } diff --git a/packages/svm/programs/settler/src/instructions/claim_stale_intent.rs b/packages/svm/programs/settler/src/instructions/claim_stale_intent.rs new file mode 100644 index 0000000..bd0b2cd --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/claim_stale_intent.rs @@ -0,0 +1,27 @@ +use anchor_lang::prelude::*; + +use crate::{errors::SettlerError, state::Intent}; + +#[derive(Accounts)] +pub struct ClaimStaleIntent<'info> { + #[account(mut)] + pub intent_creator: Signer<'info>, + + #[account( + mut, + close = intent_creator, + has_one = intent_creator @ SettlerError::IncorrectIntentCreator, + )] + pub intent: Box>, +} + +pub fn claim_stale_intent(ctx: Context) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + + require!( + ctx.accounts.intent.deadline < now, + SettlerError::IntentNotYetExpired + ); + + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index 59ff6f6..3baf365 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -1,8 +1,83 @@ use anchor_lang::prelude::*; +use crate::{ + errors::SettlerError, + state::Intent, + types::{IntentEvent, MaxFee, OpType}, + whitelist::{ + accounts::EntityRegistry, + types::{EntityType, WhitelistStatus}, + }, +}; + #[derive(Accounts)] -pub struct CreateIntent {} +// TODO: can we optimize this deser? we just need the three Vec for their length +#[instruction(intent_hash: [u8; 32], data: Vec, max_fees: Vec, events: Vec)] +pub struct CreateIntent<'info> { + #[account(mut)] + pub solver: Signer<'info>, + + // #[account( + // seeds = [b"entity-registry".as_ref(), &[EntityType::Solver as u8], solver.key().as_ref()], + // bump = solver_registry.bump, + // seeds::program = crate::whitelist::ID, + // constraint = + // solver_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::OnlySolver + // )] + // pub solver_registry: Box>, + #[account( + init, + seeds = [b"intent", intent_hash.as_ref()], + bump, + payer = solver, + space = 8 + Intent::BASE_LEN + Intent::data_size(data.len()) + Intent::max_fees_size(max_fees.len()) + Intent::events_size(&events) + )] + // TODO: change to AccountLoader? + // TODO: init within the handler body to save compute? + pub intent: Box>, + + #[account( + seeds = [b"fulfilled-intent", intent_hash.as_ref()], + bump + )] + /// This PDA must be uninitialized + pub fulfilled_intent: SystemAccount<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn create_intent( + ctx: Context, + intent_hash: [u8; 32], + data: Vec, + max_fees: Vec, + events: Vec, + op: OpType, + user: Pubkey, + nonce: [u8; 32], + deadline: u64, + min_validations: u16, + is_final: bool, +) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + require!(deadline > now, SettlerError::DeadlineIsInThePast); + + // TODO: check hash + + let intent = &mut ctx.accounts.intent; + + intent.op = op; + intent.user = user; + intent.intent_creator = ctx.accounts.solver.key(); + intent.nonce = nonce; + intent.deadline = deadline; + intent.min_validations = min_validations; + intent.validations = 0; + intent.is_final = is_final; + intent.data = data; + intent.max_fees = max_fees; + intent.events = events; + intent.bump = ctx.bumps.intent; -pub fn create_intent(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/extend_intent.rs b/packages/svm/programs/settler/src/instructions/extend_intent.rs new file mode 100644 index 0000000..8f7b2f0 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/extend_intent.rs @@ -0,0 +1,55 @@ +use anchor_lang::prelude::*; + +use crate::{ + errors::SettlerError, + state::Intent, + types::{IntentEvent, MaxFee}, +}; + +#[derive(Accounts)] +#[instruction(more_data: Option>, more_max_fees: Option>, more_events: Option>)] +pub struct ExtendIntent<'info> { + #[account(mut)] + pub intent_creator: Signer<'info>, + + #[account( + mut, + has_one = intent_creator @ SettlerError::IncorrectIntentCreator, + constraint = !intent.is_final @ SettlerError::IntentIsFinal, + realloc = + intent.extended_size(intent.to_account_info().data_len(), &more_data, &more_max_fees, &more_events)?, + realloc::payer = intent_creator, + realloc::zero = true + )] + pub intent: Box>, + + pub system_program: Program<'info, System>, +} + +pub fn extend_intent( + ctx: Context, + more_data: Option>, + more_max_fees: Option>, + more_events: Option>, + finalize: bool, +) -> Result<()> { + let intent = &mut ctx.accounts.intent; + + if let Some(_more_data) = more_data { + intent.data.extend_from_slice(&_more_data); + } + + if let Some(_more_max_fees) = more_max_fees { + intent.max_fees.extend_from_slice(&_more_max_fees); + } + + if let Some(_more_events) = more_events { + intent.events.extend_from_slice(&_more_events); + } + + if finalize { + intent.is_final = true; + } + + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/mod.rs b/packages/svm/programs/settler/src/instructions/mod.rs index b3732e6..1c45098 100644 --- a/packages/svm/programs/settler/src/instructions/mod.rs +++ b/packages/svm/programs/settler/src/instructions/mod.rs @@ -2,10 +2,12 @@ pub mod add_axia_sig; pub mod add_instructions_to_proposal; pub mod add_validator_sigs; pub mod change_whitelist_program; +pub mod claim_stale_intent; pub mod claim_stale_proposal; pub mod create_intent; pub mod create_proposal; pub mod execute_proposal; +pub mod extend_intent; pub mod initialize; pub mod set_paused_state; @@ -13,9 +15,11 @@ pub use add_axia_sig::*; pub use add_instructions_to_proposal::*; pub use add_validator_sigs::*; pub use change_whitelist_program::*; +pub use claim_stale_intent::*; pub use claim_stale_proposal::*; pub use create_intent::*; pub use create_proposal::*; pub use execute_proposal::*; +pub use extend_intent::*; pub use initialize::*; pub use set_paused_state::*; diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index a2bb537..e644b82 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -9,7 +9,7 @@ pub mod instructions; pub mod state; pub mod types; -use crate::instructions::*; +use crate::{instructions::*, types::*}; #[program] pub mod settler { @@ -31,12 +31,40 @@ pub mod settler { instructions::change_whitelist_program(ctx) } + pub fn claim_stale_intent(ctx: Context) -> Result<()> { + instructions::claim_stale_intent(ctx) + } + pub fn claim_stale_proposal(ctx: Context) -> Result<()> { instructions::claim_stale_proposal(ctx) } - pub fn create_intent(ctx: Context) -> Result<()> { - instructions::create_intent(ctx) + pub fn create_intent( + ctx: Context, + intent_hash: [u8; 32], + data: Vec, + max_fees: Vec, + events: Vec, + op: OpType, + user: Pubkey, + nonce: [u8; 32], + deadline: u64, + min_validations: u16, + is_final: bool, + ) -> Result<()> { + instructions::create_intent( + ctx, + intent_hash, + data, + max_fees, + events, + op, + user, + nonce, + deadline, + min_validations, + is_final, + ) } pub fn create_proposal(ctx: Context) -> Result<()> { @@ -47,6 +75,16 @@ pub mod settler { instructions::execute_proposal(ctx) } + pub fn extend_intent( + ctx: Context, + more_data: Option>, + more_max_fees: Option>, + more_events: Option>, + finalize: bool, + ) -> Result<()> { + instructions::extend_intent(ctx, more_data, more_max_fees, more_events, finalize) + } + pub fn initialize(ctx: Context) -> Result<()> { instructions::initialize(ctx) } diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs index 3ff9532..37904f5 100644 --- a/packages/svm/programs/settler/src/state/intent.rs +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -1,4 +1,68 @@ use anchor_lang::prelude::*; +use crate::types::{IntentEvent, MaxFee, OpType}; + #[account] -pub struct Intent {} +pub struct Intent { + pub op: OpType, + pub user: Pubkey, + pub intent_creator: Pubkey, + pub nonce: [u8; 32], + pub deadline: u64, + pub min_validations: u16, + pub validations: u16, + pub is_final: bool, + pub data: Vec, + pub max_fees: Vec, + pub events: Vec, + pub bump: u8, +} + +impl Intent { + /// Doesn't take into account size of variable fields + pub const BASE_LEN: usize = + 1 + // op + 32 + // user + 32 + // intent_creator + 32 + // nonce + 8 + // deadline + 2 + // min_validations + 2 + // validations + 1 + // is_final + 1 // bump + ; + + pub fn data_size(len: usize) -> usize { + 4 + len + } + + pub fn max_fees_size(len: usize) -> usize { + 4 + MaxFee::INIT_SPACE * len + } + + pub fn events_size(events: &Vec) -> usize { + 4 + events.iter().map(|event| event.size()).sum::() + } + + pub fn extended_size( + &self, + mut size: usize, + more_data: &Option>, + more_max_fees: &Option>, + more_events: &Option>, + ) -> Result { + if let Some(_more_data) = more_data { + size += Intent::data_size(_more_data.len()) - 4; + } + + if let Some(_more_max_fees) = more_max_fees { + size += Intent::max_fees_size(_more_max_fees.len()) - 4; + } + + if let Some(_more_events) = more_events { + size += Intent::events_size(&_more_events) - 4; + } + + Ok(size) + } +} diff --git a/packages/svm/programs/settler/src/state/proposal.rs b/packages/svm/programs/settler/src/state/proposal.rs index 4d7664e..b17d041 100644 --- a/packages/svm/programs/settler/src/state/proposal.rs +++ b/packages/svm/programs/settler/src/state/proposal.rs @@ -1,4 +1,36 @@ use anchor_lang::prelude::*; #[account] -pub struct Proposal {} +pub struct Proposal { + pub intent: Pubkey, + pub solver: Pubkey, + pub deadline: u64, + pub is_final: bool, + pub instructions: Vec, + pub bump: u8, +} + +impl Proposal { + /// Doesn't take into account size of variable fields + pub const BASE_LEN: usize = + 32 + // intent + 32 + // solver + 8 + // deadline + 1 + // is_final + 1 // bump + ; +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct ProposalInstruction { + pub program_id: Pubkey, + pub accounts: Vec, + pub data: Vec, +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct ProposalInstructionAccountMeta { + pub pubkey: Pubkey, + pub is_signer: bool, + pub is_writable: bool, +} diff --git a/packages/svm/programs/settler/src/types/intent_event.rs b/packages/svm/programs/settler/src/types/intent_event.rs index 9777df2..0eb00c4 100644 --- a/packages/svm/programs/settler/src/types/intent_event.rs +++ b/packages/svm/programs/settler/src/types/intent_event.rs @@ -1,4 +1,13 @@ +use anchor_lang::prelude::*; + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct IntentEvent { pub topic: [u8; 32], pub data: Vec, } + +impl IntentEvent { + pub fn size(&self) -> usize { + 32 + 4 + self.data.len() + } +} diff --git a/packages/svm/programs/settler/src/types/max_fee.rs b/packages/svm/programs/settler/src/types/max_fee.rs index e181540..a5d5fe6 100644 --- a/packages/svm/programs/settler/src/types/max_fee.rs +++ b/packages/svm/programs/settler/src/types/max_fee.rs @@ -1,6 +1,11 @@ -use anchor_lang::prelude::Pubkey; +use anchor_lang::prelude::*; +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct MaxFee { pub mint: Pubkey, pub amount: u64, } + +impl Space for MaxFee { + const INIT_SPACE: usize = 32 + 8; +} diff --git a/packages/svm/programs/settler/src/types/mod.rs b/packages/svm/programs/settler/src/types/mod.rs index 0f9e229..76aa716 100644 --- a/packages/svm/programs/settler/src/types/mod.rs +++ b/packages/svm/programs/settler/src/types/mod.rs @@ -1,11 +1,7 @@ pub mod intent_event; pub mod max_fee; pub mod op_type; -pub mod proposal_instruction; -pub mod proposal_instruction_account_meta; pub use intent_event::*; pub use max_fee::*; pub use op_type::*; -pub use proposal_instruction::*; -pub use proposal_instruction_account_meta::*; diff --git a/packages/svm/programs/settler/src/types/op_type.rs b/packages/svm/programs/settler/src/types/op_type.rs index cc36418..620ab7e 100644 --- a/packages/svm/programs/settler/src/types/op_type.rs +++ b/packages/svm/programs/settler/src/types/op_type.rs @@ -1,4 +1,7 @@ +use anchor_lang::prelude::*; + #[repr(u8)] +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub enum OpType { Swap = 1, Transfer = 2, diff --git a/packages/svm/programs/settler/src/types/proposal_instruction.rs b/packages/svm/programs/settler/src/types/proposal_instruction.rs deleted file mode 100644 index 5d972a4..0000000 --- a/packages/svm/programs/settler/src/types/proposal_instruction.rs +++ /dev/null @@ -1,9 +0,0 @@ -use anchor_lang::prelude::Pubkey; - -use crate::types::ProposalInstructionAccountMeta; - -pub struct ProposalInstruction { - pub program_id: Pubkey, - pub accounts: Vec, - pub data: Vec, -} diff --git a/packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs b/packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs deleted file mode 100644 index 5a2b55f..0000000 --- a/packages/svm/programs/settler/src/types/proposal_instruction_account_meta.rs +++ /dev/null @@ -1,7 +0,0 @@ -use anchor_lang::prelude::Pubkey; - -pub struct ProposalInstructionAccountMeta { - pub pubkey: Pubkey, - pub is_signer: bool, - pub is_writable: bool, -} diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index b4881f7..6af0635 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -1,7 +1,18 @@ -import { Program, Provider, web3 } from '@coral-xyz/anchor' +import { BN, IdlTypes, Program, Provider, web3 } from '@coral-xyz/anchor' import * as SettlerIDL from '../../target/idl/settler.json' import { Settler } from '../../target/types/settler' +import { CreateIntentParams, ExtendIntentParams, IntentEvent, MaxFee, OpType } from './types' + +type MaxFeeAnchor = { + mint: web3.PublicKey + amount: BN +} + +type IntentEventAnchor = { + topic: number[] + data: Buffer +} export default class SettlerSDK { protected program: Program @@ -15,7 +26,130 @@ export default class SettlerSDK { return ix } + async createIntentIx( + intentHashHex: string, + params: CreateIntentParams, + isFinal = true + ): Promise { + const { + op, + user, + nonceHex, + deadline, + minValidations, + dataHex, + maxFees, + eventsHex, + } = params + + const intentHash = this.parseIntentHashHex(intentHashHex) + const nonce = this.parseIntentNonceHex(nonceHex) + const data = Buffer.from(dataHex, 'hex') + const maxFeesBn = this.parseIntentMaxFees(maxFees) + const events = this.parseIntentEventsHex(eventsHex) + + const ix = await this.program.methods + .createIntent( + intentHash, + data, + maxFeesBn, + events, + this.opTypeToAnchorEnum(op), + user, + nonce, + new BN(deadline), + minValidations, + isFinal + ) + .accountsPartial({ solver: this.getSignerKey() }) + .instruction() + + return ix + } + + async extendIntentIx( + intentHashHex: string, + params: ExtendIntentParams, + isFinal = true + ): Promise { + const { moreDataHex = '', moreMaxFees = [], moreEventsHex = [] } = params + + const moreData = Buffer.from(moreDataHex, 'hex') + const moreMaxFeesBn = this.parseIntentMaxFees(moreMaxFees) + const moreEvents = this.parseIntentEventsHex(moreEventsHex) + + const ix = await this.program.methods + .extendIntent(moreData, moreMaxFeesBn, moreEvents, isFinal) + .accountsPartial({ + intentCreator: this.getSignerKey(), + intent: this.getIntentKey(intentHashHex), + }) + .instruction() + + return ix + } + + async claimStaleIntentIx(intentHashHex: string): Promise { + const ix = await this.program.methods + .claimStaleIntent() + .accountsPartial({ + intentCreator: this.getSignerKey(), + intent: this.getIntentKey(intentHashHex), + }) + .instruction() + + return ix + } + getSettlerSettingsPubkey(): web3.PublicKey { return web3.PublicKey.findProgramAddressSync([Buffer.from('settler-settings')], this.program.programId)[0] } + + getIntentKey(intentHashHex: string): web3.PublicKey { + const intentHash = Buffer.from(intentHashHex, 'hex') + if (intentHash.length != 32) throw new Error(`Intent hash must be 32 bytes: ${intentHashHex}`) + + return web3.PublicKey.findProgramAddressSync([Buffer.from('intent'), intentHash], this.program.programId)[0] + } + + getSignerKey(): web3.PublicKey { + if (!this.program.provider.wallet) throw new Error('Must set program provider wallet') + return this.program.provider.wallet?.publicKey + } + + opTypeToAnchorEnum(op: OpType): IdlTypes['opType'] { + if (op === OpType.Transfer) return { transfer: {} } + if (op === OpType.Swap) return { swap: {} } + if (op === OpType.Call) return { call: {} } + + throw new Error(`Unsupported op ${op}`) + } + + private parseIntentHashHex(intentHashHex: string): number[] { + const intentHash = Buffer.from(intentHashHex, 'hex') + if (intentHash.length != 32) throw new Error(`Intent hash must be 32 bytes: ${intentHashHex}`) + return Array.from(intentHash) + } + + private parseIntentNonceHex(nonceHex: string): number[] { + const nonce = Buffer.from(nonceHex, 'hex') + if (nonce.length != 32) throw new Error(`Nonce must be 32 bytes: ${nonceHex}`) + return Array.from(nonce) + } + + private parseIntentEventsHex(eventsHex: IntentEvent[]): IntentEventAnchor[] { + const events = eventsHex.map((eventHex) => ({ + topic: Array.from(Uint8Array.from(Buffer.from(eventHex.topicHex, 'hex'))), + data: Buffer.from(eventHex.dataHex, 'hex'), + })) + if (events.some((event) => event.topic.length != 32)) throw new Error(`Event topics must be 32 bytes`) + return events + } + + private parseIntentMaxFees(maxFees: MaxFee[]): MaxFeeAnchor[] { + return maxFees.map((maxFee) => ({ + ...maxFee, + amount: new BN(maxFee.amount), + })) + } } diff --git a/packages/svm/sdks/settler/types.ts b/packages/svm/sdks/settler/types.ts new file mode 100644 index 0000000..b446d1f --- /dev/null +++ b/packages/svm/sdks/settler/types.ts @@ -0,0 +1,37 @@ +import { web3 } from '@coral-xyz/anchor' + +export type MaxFee = { + mint: web3.PublicKey + amount: number +} + +export type IntentEvent = { + topicHex: string + dataHex: string +} + +export enum OpType { + // eslint-disable-next-line no-unused-vars + Transfer = 1, + // eslint-disable-next-line no-unused-vars + Swap = 2, + // eslint-disable-next-line no-unused-vars + Call = 3, +} + +export type CreateIntentParams = { + op: OpType, + user: web3.PublicKey, + nonceHex: string, + deadline: number, + minValidations: number, + dataHex: string, + maxFees: MaxFee[], + eventsHex: IntentEvent[], +} + +export type ExtendIntentParams = { + moreDataHex?: string + moreMaxFees?: MaxFee[] + moreEventsHex?: IntentEvent[] +} diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 47b87a5..ada0d82 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { Program, Wallet } from '@coral-xyz/anchor' -import { Keypair } from '@solana/web3.js' +import { BN, Program, Wallet } from '@coral-xyz/anchor' +import { Keypair, PublicKey } from '@solana/web3.js' import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' import fs from 'fs' @@ -12,8 +12,10 @@ import path from 'path' import SettlerSDK from '../sdks/settler/Settler' import * as SettlerIDL from '../target/idl/settler.json' +import * as WhitelistIDL from '../target/idl/whitelist.json' import { Settler } from '../target/types/settler' -import { makeTxSignAndSend } from './utils' +import { makeTxSignAndSend, warpSeconds } from './utils' +import { OpType } from '../sdks/settler/types' describe('Settler Program', () => { let client: LiteSVM @@ -53,29 +55,23 @@ describe('Settler Program', () => { describe('Settler', () => { describe('initialize', () => { it('cant initialize if not deployer', async () => { - const whitelistProgram = Keypair.generate().publicKey - - const ix = await maliciousSdk.initializeIx(whitelistProgram) + const ix = await maliciousSdk.initializeIx() const res = await makeTxSignAndSend(maliciousProvider, ix) expect(res.toString()).to.include(`Only Deployer can call this instruction.`) }) it('should call initialize', async () => { - const whitelistProgram = Keypair.generate().publicKey - - const ix = await sdk.initializeIx(whitelistProgram) + const ix = await sdk.initializeIx() await makeTxSignAndSend(provider, ix) const settings = await program.account.settlerSettings.fetch(sdk.getSettlerSettingsPubkey()) - expect(settings.whitelistProgram.toString()).to.be.eq(whitelistProgram.toString()) + expect(settings.whitelistProgram.toString()).to.be.eq(WhitelistIDL.address) expect(settings.isPaused).to.be.false }) it('cant call initialize again', async () => { - const whitelistProgram = Keypair.generate().publicKey - - const ix = await sdk.initializeIx(whitelistProgram) + const ix = await sdk.initializeIx() const res = await makeTxSignAndSend(provider, ix) expect(res.toString()).to.include( @@ -83,5 +79,8 @@ describe('Settler Program', () => { ) }) }) + + describe('create_intent', () => { + }) }) }) From ab064dc1a50e73136a2e949d7985e8360496d1a1 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 4 Nov 2025 19:43:33 -0300 Subject: [PATCH 24/41] Add intent creation tests --- .../settler/src/state/fulfilled_intent.rs | 4 +- packages/svm/sdks/settler/Settler.ts | 21 +- packages/svm/sdks/settler/types.ts | 16 +- packages/svm/tests/settler.test.ts | 722 +++++++++++++++++- 4 files changed, 741 insertions(+), 22 deletions(-) diff --git a/packages/svm/programs/settler/src/state/fulfilled_intent.rs b/packages/svm/programs/settler/src/state/fulfilled_intent.rs index 11dea17..0556f84 100644 --- a/packages/svm/programs/settler/src/state/fulfilled_intent.rs +++ b/packages/svm/programs/settler/src/state/fulfilled_intent.rs @@ -1,4 +1,6 @@ use anchor_lang::prelude::*; #[account] -pub struct FulfilledIntent {} +pub struct FulfilledIntent { + pub fulfilled_at: u64, +} diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index 6af0635..1e88358 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -31,16 +31,7 @@ export default class SettlerSDK { params: CreateIntentParams, isFinal = true ): Promise { - const { - op, - user, - nonceHex, - deadline, - minValidations, - dataHex, - maxFees, - eventsHex, - } = params + const { op, user, nonceHex, deadline, minValidations, dataHex, maxFees, eventsHex } = params const intentHash = this.parseIntentHashHex(intentHashHex) const nonce = this.parseIntentNonceHex(nonceHex) @@ -112,6 +103,16 @@ export default class SettlerSDK { return web3.PublicKey.findProgramAddressSync([Buffer.from('intent'), intentHash], this.program.programId)[0] } + getFulfilledIntentKey(intentHashHex: string): web3.PublicKey { + const intentHash = Buffer.from(intentHashHex, 'hex') + if (intentHash.length != 32) throw new Error(`Intent hash must be 32 bytes: ${intentHashHex}`) + + return web3.PublicKey.findProgramAddressSync( + [Buffer.from('fulfilled-intent'), intentHash], + this.program.programId + )[0] + } + getSignerKey(): web3.PublicKey { if (!this.program.provider.wallet) throw new Error('Must set program provider wallet') return this.program.provider.wallet?.publicKey diff --git a/packages/svm/sdks/settler/types.ts b/packages/svm/sdks/settler/types.ts index b446d1f..227ebce 100644 --- a/packages/svm/sdks/settler/types.ts +++ b/packages/svm/sdks/settler/types.ts @@ -20,14 +20,14 @@ export enum OpType { } export type CreateIntentParams = { - op: OpType, - user: web3.PublicKey, - nonceHex: string, - deadline: number, - minValidations: number, - dataHex: string, - maxFees: MaxFee[], - eventsHex: IntentEvent[], + op: OpType + user: web3.PublicKey + nonceHex: string + deadline: number + minValidations: number + dataHex: string + maxFees: MaxFee[] + eventsHex: IntentEvent[] } export type ExtendIntentParams = { diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index ada0d82..215a5d5 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { BN, Program, Wallet } from '@coral-xyz/anchor' -import { Keypair, PublicKey } from '@solana/web3.js' +import { Program, Wallet } from '@coral-xyz/anchor' +import { Keypair } from '@solana/web3.js' import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' import fs from 'fs' @@ -11,11 +11,11 @@ import os from 'os' import path from 'path' import SettlerSDK from '../sdks/settler/Settler' +import { OpType } from '../sdks/settler/types' import * as SettlerIDL from '../target/idl/settler.json' import * as WhitelistIDL from '../target/idl/whitelist.json' import { Settler } from '../target/types/settler' import { makeTxSignAndSend, warpSeconds } from './utils' -import { OpType } from '../sdks/settler/types' describe('Settler Program', () => { let client: LiteSVM @@ -81,6 +81,722 @@ describe('Settler Program', () => { }) describe('create_intent', () => { + const generateIntentHash = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const generateNonce = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + it('should create an intent', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], + eventsHex: [ + { + topicHex: Buffer.from(Array(32).fill(1)).toString('hex'), + dataHex: '040506', + }, + ], + } + + const ix = await sdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.op).to.deep.include({ transfer: {} }) + expect(intent.user.toString()).to.be.eq(user.toString()) + expect(intent.intentCreator.toString()).to.be.eq(admin.publicKey.toString()) + expect(Buffer.from(intent.nonce).toString('hex')).to.be.eq(nonce) + expect(intent.deadline.toNumber()).to.be.eq(deadline) + expect(intent.minValidations).to.be.eq(1) + expect(intent.validations).to.be.eq(0) + expect(intent.isFinal).to.be.false + expect(Buffer.from(intent.data).toString('hex')).to.be.eq('010203') + expect(intent.maxFees.length).to.be.eq(1) + expect(intent.maxFees[0].mint.toString()).to.be.eq(params.maxFees[0].mint.toString()) + expect(intent.maxFees[0].amount.toNumber()).to.be.eq(1000) + expect(intent.events.length).to.be.eq(1) + expect(Buffer.from(intent.events[0].topic).toString('hex')).to.be.eq(params.eventsHex[0].topicHex) + expect(Buffer.from(intent.events[0].data).toString('hex')).to.be.eq('040506') + }) + + it('should create an intent with empty data', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Swap, + user, + nonceHex: nonce, + deadline, + minValidations: 2, + dataHex: '', + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 2000, + }, + ], + eventsHex: [], + } + + const ix = await sdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.op).to.deep.include({ swap: {} }) + expect(Buffer.from(intent.data).toString('hex')).to.be.eq('') + expect(intent.isFinal).to.be.true + }) + + it('should create an intent with empty max_fees', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Call, + user, + nonceHex: nonce, + deadline, + minValidations: 3, + dataHex: '070809', + maxFees: [], + eventsHex: [], + } + + const ix = await sdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.op).to.deep.include({ call: {} }) + expect(intent.maxFees.length).to.be.eq(0) + }) + + it('should create an intent with empty events', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '0a0b0c', + maxFees: [], + eventsHex: [], + } + + const ix = await sdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.events.length).to.be.eq(0) + }) + + it('should create an intent with is_final true', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '', + maxFees: [], + eventsHex: [], + } + + const ix = await sdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.isFinal).to.be.true + }) + + it('should create an intent with is_final false', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '', + maxFees: [], + eventsHex: [], + } + + const ix = await sdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.isFinal).to.be.false + }) + + xit('cant create intent if not whitelisted solver', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '', + maxFees: [], + eventsHex: [], + } + + const ix = await maliciousSdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(maliciousProvider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.intentCreator.toString()).to.be.eq(malicious.publicKey.toString()) + }) + + it('cant create intent with deadline in the past', async () => { + warpSeconds(provider, 500) + + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now - 100 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '', + maxFees: [], + eventsHex: [], + } + + const ix = await sdk.createIntentIx(intentHash, params, false) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include(`Deadline can't be in the past`) + }) + + it('cant create intent with deadline equal to now', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '', + maxFees: [], + eventsHex: [], + } + + const ix = await sdk.createIntentIx(intentHash, params, false) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include(`Deadline can't be in the past`) + }) + + it('cant create intent if fulfilled_intent PDA already exists', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '', + maxFees: [], + eventsHex: [], + } + + // Mock FulfilledIntent + const fulfilledIntent = sdk.getFulfilledIntentKey(intentHash) + client.setAccount(fulfilledIntent, { + executable: false, + lamports: 1002240, + owner: program.programId, + data: Buffer.from('595168911b9267f7' + '010000000000000000', 'hex'), + }) + + const ix = await sdk.createIntentIx(intentHash, params, false) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include( + `AnchorError caused by account: fulfilled_intent. Error Code: AccountNotSystemOwned. Error Number: 3011. Error Message: The given account is not owned by the system program` + ) + }) + + it('cant create intent with same intent_hash twice', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '', + maxFees: [], + eventsHex: [], + } + + const ix = await sdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(provider, ix) + + client.expireBlockhash() + const ix2 = await sdk.createIntentIx(intentHash, params, false) + const res = await makeTxSignAndSend(provider, ix2) + + expect(res.toString()).to.include(`already in use`) + }) + + it('cant create intent with invalid intent_hash', async () => { + const invalidIntentHash = '123456' + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '', + maxFees: [], + eventsHex: [], + } + + try { + const ix = await sdk.createIntentIx(invalidIntentHash, params, false) + await makeTxSignAndSend(provider, ix) + expect.fail('Should have thrown an error') + } catch (error: any) { + expect(error.message).to.include(`Intent hash must be 32 bytes`) + } + }) + }) + + describe('extend_intent', () => { + const generateIntentHash = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const generateNonce = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const createTestIntent = async (isFinal = false): Promise => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], + eventsHex: [ + { + topicHex: Buffer.from(Array(32).fill(1)).toString('hex'), + dataHex: '040506', + }, + ], + } + + const ix = await sdk.createIntentIx(intentHash, params, isFinal) + await makeTxSignAndSend(provider, ix) + return intentHash + } + + it('should extend an intent with more data', async () => { + const intentHash = await createTestIntent(false) + + const extendParams = { + moreDataHex: '070809', + } + + const ix = await sdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(Buffer.from(intent.data).toString('hex')).to.be.eq('010203070809') + expect(intent.isFinal).to.be.false + }) + + it('should extend an intent with more max_fees', async () => { + const intentHash = await createTestIntent(false) + + const newMint = Keypair.generate().publicKey + const extendParams = { + moreMaxFees: [ + { + mint: newMint, + amount: 2000, + }, + ], + } + + const ix = await sdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.maxFees.length).to.be.eq(2) + expect(intent.maxFees[0].amount.toNumber()).to.be.eq(1000) + expect(intent.maxFees[1].mint.toString()).to.be.eq(newMint.toString()) + expect(intent.maxFees[1].amount.toNumber()).to.be.eq(2000) + }) + + it('should extend an intent with more events', async () => { + const intentHash = await createTestIntent(false) + + const newTopic = Buffer.from(Array(32).fill(2)).toString('hex') + const extendParams = { + moreEventsHex: [ + { + topicHex: newTopic, + dataHex: '0a0b0c', + }, + ], + } + + const ix = await sdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.events.length).to.be.eq(2) + expect(Buffer.from(intent.events[0].topic).toString('hex')).to.be.eq( + Buffer.from(Array(32).fill(1)).toString('hex') + ) + expect(Buffer.from(intent.events[1].topic).toString('hex')).to.be.eq(newTopic) + expect(Buffer.from(intent.events[1].data).toString('hex')).to.be.eq('0a0b0c') + }) + + it('should extend an intent with all optional fields', async () => { + const intentHash = await createTestIntent(false) + + const newMint = Keypair.generate().publicKey + const newTopic = Buffer.from(Array(32).fill(3)).toString('hex') + const extendParams = { + moreDataHex: '0d0e0f', + moreMaxFees: [ + { + mint: newMint, + amount: 3000, + }, + ], + moreEventsHex: [ + { + topicHex: newTopic, + dataHex: '101112', + }, + ], + } + + const ix = await sdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(Buffer.from(intent.data).toString('hex')).to.be.eq('0102030d0e0f') + expect(intent.maxFees.length).to.be.eq(2) + expect(intent.maxFees[1].amount.toNumber()).to.be.eq(3000) + expect(intent.events.length).to.be.eq(2) + expect(Buffer.from(intent.events[1].data).toString('hex')).to.be.eq('101112') + }) + + it('should finalize an intent', async () => { + const intentHash = await createTestIntent(false) + + const extendParams = {} + + const ix = await sdk.extendIntentIx(intentHash, extendParams, true) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.isFinal).to.be.true + }) + + it('should extend and finalize an intent in one call', async () => { + const intentHash = await createTestIntent(false) + + const extendParams = { + moreDataHex: '191a1b', + } + + const ix = await sdk.extendIntentIx(intentHash, extendParams, true) + await makeTxSignAndSend(provider, ix) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(Buffer.from(intent.data).toString('hex')).to.be.eq('010203191a1b') + expect(intent.isFinal).to.be.true + }) + + it('should extend an intent multiple times', async () => { + const intentHash = await createTestIntent(false) + + const extendParams1 = { + moreDataHex: '1c1d1e', + } + const ix1 = await sdk.extendIntentIx(intentHash, extendParams1, false) + await makeTxSignAndSend(provider, ix1) + + const extendParams2 = { + moreDataHex: '1f2021', + } + const ix2 = await sdk.extendIntentIx(intentHash, extendParams2, false) + await makeTxSignAndSend(provider, ix2) + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(Buffer.from(intent.data).toString('hex')).to.be.eq('0102031c1d1e1f2021') + expect(intent.isFinal).to.be.false + }) + + it('cant extend intent if not intent creator', async () => { + const intentHash = await createTestIntent(false) + + const extendParams = { + moreDataHex: '222324', + } + + const ix = await maliciousSdk.extendIntentIx(intentHash, extendParams, false) + const res = await makeTxSignAndSend(maliciousProvider, ix) + + expect(res.toString()).to.include(`Signer must be intent creator`) + }) + + it('cant extend non-existent intent', async () => { + const intentHash = generateIntentHash() + + const extendParams = { + moreDataHex: '252627', + } + + const ix = await sdk.extendIntentIx(intentHash, extendParams, false) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include(`AccountNotInitialized`) + }) + + it('cant extend intent if already finalized', async () => { + const intentHash = await createTestIntent(true) + + const extendParams = { + moreDataHex: '28292a', + } + + const ix = await sdk.extendIntentIx(intentHash, extendParams, false) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include(`Intent is already final`) + }) + + it('cant finalize already finalized intent', async () => { + const intentHash = await createTestIntent(true) + + const extendParams = {} + + const ix = await sdk.extendIntentIx(intentHash, extendParams, true) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include(`Intent is already final`) + }) + }) + + describe('claim_stale_intent', () => { + const generateIntentHash = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const generateNonce = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const createTestIntentWithDeadline = async (deadline: number, isFinal = false): Promise => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [], + eventsHex: [], + } + + const ix = await sdk.createIntentIx(intentHash, params, isFinal) + await makeTxSignAndSend(provider, ix) + return intentHash + } + + it('should claim stale intent', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 50 + const intentHash = await createTestIntentWithDeadline(deadline, false) + + const intentBefore = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intentBefore).to.not.be.null + + warpSeconds(provider, 51) + + const intentBalanceBefore = Number(provider.client.getBalance(sdk.getIntentKey(intentHash))) || 0 + const intentCreatorBalanceBefore = Number(provider.client.getBalance(intentBefore.intentCreator)) || 0 + + const ix = await sdk.claimStaleIntentIx(intentHash) + await makeTxSignAndSend(provider, ix) + + const intentBalanceAfter = Number(provider.client.getBalance(sdk.getIntentKey(intentHash))) || 0 + const intentCreatorBalanceAfter = Number(provider.client.getBalance(intentBefore.intentCreator)) || 0 + + try { + await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect.fail('Intent account should be closed') + } catch (error: any) { + expect(error.message).to.include(`Account does not exist`) + } + + expect(intentCreatorBalanceAfter).to.be.eq(intentCreatorBalanceBefore + intentBalanceBefore - 5000) + expect(intentBalanceAfter).to.be.eq(0) + }) + + it('cant claim intent if deadline has not passed', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 500 + const intentHash = await createTestIntentWithDeadline(deadline, false) + + warpSeconds(provider, 100) + + const ix = await sdk.claimStaleIntentIx(intentHash) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include(`Intent not yet expired`) + }) + + it('cant claim intent if deadline equals now', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 300 + const intentHash = await createTestIntentWithDeadline(deadline, false) + + warpSeconds(provider, 300) + + const ix = await sdk.claimStaleIntentIx(intentHash) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include(`Intent not yet expired`) + }) + + it('cant claim stale intent if not intent creator', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 80 + const intentHash = await createTestIntentWithDeadline(deadline, false) + + warpSeconds(provider, 81) + + const ix = await maliciousSdk.claimStaleIntentIx(intentHash) + const res = await makeTxSignAndSend(maliciousProvider, ix) + + expect(res.toString()).to.include(`Signer must be intent creator`) + }) + + it('cant claim non-existent intent', async () => { + const intentHash = generateIntentHash() + + const ix = await sdk.claimStaleIntentIx(intentHash) + const res = await makeTxSignAndSend(provider, ix) + + expect(res.toString()).to.include(`AccountNotInitialized`) + }) + + it('cant claim intent twice', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 90 + const intentHash = await createTestIntentWithDeadline(deadline, false) + + warpSeconds(provider, 91) + + const ix = await sdk.claimStaleIntentIx(intentHash) + await makeTxSignAndSend(provider, ix) + + client.expireBlockhash() + const ix2 = await sdk.claimStaleIntentIx(intentHash) + const res = await makeTxSignAndSend(provider, ix2) + + const errorMsg = res.toString() + expect(errorMsg.includes(`AccountNotInitialized`)).to.be.true + }) }) }) }) From 10e2025b861c8b16ec19f7e41fcbedab7bce7511 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 4 Nov 2025 22:08:08 -0300 Subject: [PATCH 25/41] Add more tests, fix TODOs, finish intent cycle instructions and tests --- .../settler/src/instructions/create_intent.rs | 19 +- .../settler/src/instructions/extend_intent.rs | 2 +- .../svm/programs/settler/src/state/intent.rs | 2 +- packages/svm/sdks/settler/Settler.ts | 14 +- packages/svm/tests/settler.test.ts | 212 ++++++++++++------ 5 files changed, 165 insertions(+), 84 deletions(-) diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index 3baf365..1ce9cf3 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -17,14 +17,15 @@ pub struct CreateIntent<'info> { #[account(mut)] pub solver: Signer<'info>, - // #[account( - // seeds = [b"entity-registry".as_ref(), &[EntityType::Solver as u8], solver.key().as_ref()], - // bump = solver_registry.bump, - // seeds::program = crate::whitelist::ID, - // constraint = - // solver_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::OnlySolver - // )] - // pub solver_registry: Box>, + #[account( + seeds = [b"entity-registry".as_ref(), &[EntityType::Solver as u8 + 1], solver.key().as_ref()], + bump = solver_registry.bump, + seeds::program = crate::whitelist::ID, + constraint = + solver_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::OnlySolver + )] + pub solver_registry: Box>, + #[account( init, seeds = [b"intent", intent_hash.as_ref()], @@ -74,7 +75,7 @@ pub fn create_intent( intent.min_validations = min_validations; intent.validations = 0; intent.is_final = is_final; - intent.data = data; + intent.intent_data = data; intent.max_fees = max_fees; intent.events = events; intent.bump = ctx.bumps.intent; diff --git a/packages/svm/programs/settler/src/instructions/extend_intent.rs b/packages/svm/programs/settler/src/instructions/extend_intent.rs index 8f7b2f0..59fcb55 100644 --- a/packages/svm/programs/settler/src/instructions/extend_intent.rs +++ b/packages/svm/programs/settler/src/instructions/extend_intent.rs @@ -36,7 +36,7 @@ pub fn extend_intent( let intent = &mut ctx.accounts.intent; if let Some(_more_data) = more_data { - intent.data.extend_from_slice(&_more_data); + intent.intent_data.extend_from_slice(&_more_data); } if let Some(_more_max_fees) = more_max_fees { diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs index 37904f5..bc30689 100644 --- a/packages/svm/programs/settler/src/state/intent.rs +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -12,7 +12,7 @@ pub struct Intent { pub min_validations: u16, pub validations: u16, pub is_final: bool, - pub data: Vec, + pub intent_data: Vec, pub max_fees: Vec, pub events: Vec, pub bump: u8, diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index 1e88358..67bf3d5 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -1,7 +1,9 @@ import { BN, IdlTypes, Program, Provider, web3 } from '@coral-xyz/anchor' import * as SettlerIDL from '../../target/idl/settler.json' +import * as WhitelistIDL from '../../target/idl/whitelist.json' import { Settler } from '../../target/types/settler' +import { EntityType } from '../whitelist/Whitelist' import { CreateIntentParams, ExtendIntentParams, IntentEvent, MaxFee, OpType } from './types' type MaxFeeAnchor = { @@ -52,7 +54,10 @@ export default class SettlerSDK { minValidations, isFinal ) - .accountsPartial({ solver: this.getSignerKey() }) + .accountsPartial({ + solver: this.getSignerKey(), + solverRegistry: this.getEntityRegistryPubkey(EntityType.Solver, this.getSignerKey()), + }) .instruction() return ix @@ -113,6 +118,13 @@ export default class SettlerSDK { )[0] } + getEntityRegistryPubkey(entityType: EntityType, entityPubkey: web3.PublicKey): web3.PublicKey { + return web3.PublicKey.findProgramAddressSync( + [Buffer.from('entity-registry'), Buffer.from([entityType]), entityPubkey.toBuffer()], + new web3.PublicKey(WhitelistIDL.address) + )[0] + } + getSignerKey(): web3.PublicKey { if (!this.program.provider.wallet) throw new Error('Must set program provider wallet') return this.program.provider.wallet?.publicKey diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 215a5d5..12161f8 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -12,6 +12,7 @@ import path from 'path' import SettlerSDK from '../sdks/settler/Settler' import { OpType } from '../sdks/settler/types' +import WhitelistSDK, { EntityType, WhitelistStatus } from '../sdks/whitelist/Whitelist' import * as SettlerIDL from '../target/idl/settler.json' import * as WhitelistIDL from '../target/idl/whitelist.json' import { Settler } from '../target/types/settler' @@ -19,33 +20,53 @@ import { makeTxSignAndSend, warpSeconds } from './utils' describe('Settler Program', () => { let client: LiteSVM + let provider: LiteSVMProvider let maliciousProvider: LiteSVMProvider + let solverProvider: LiteSVMProvider + let admin: Keypair let malicious: Keypair + let solver: Keypair + let program: Program + let sdk: SettlerSDK let maliciousSdk: SettlerSDK + let solverSdk: SettlerSDK + + let whitelistSdk: WhitelistSDK before(async () => { admin = Keypair.fromSecretKey( Uint8Array.from(JSON.parse(fs.readFileSync(path.join(os.homedir(), '.config', 'solana', 'id.json'), 'utf8'))) ) malicious = Keypair.generate() + solver = Keypair.generate() client = fromWorkspace(path.join(__dirname, '../')).withBuiltins() provider = new LiteSVMProvider(client, new Wallet(admin)) maliciousProvider = new LiteSVMProvider(client, new Wallet(malicious)) + solverProvider = new LiteSVMProvider(client, new Wallet(solver)) program = new Program(SettlerIDL as any, provider) sdk = new SettlerSDK(provider) maliciousSdk = new SettlerSDK(maliciousProvider) + solverSdk = new SettlerSDK(solverProvider) - // Airdrop initial lamports provider.client.airdrop(admin.publicKey, BigInt(100_000_000_000)) provider.client.airdrop(malicious.publicKey, BigInt(100_000_000_000)) + provider.client.airdrop(solver.publicKey, BigInt(100_000_000_000)) + + // Initialize Whitelist and whitelist Solver + whitelistSdk = new WhitelistSDK(provider) + await makeTxSignAndSend(provider, await whitelistSdk.initializeIx(admin.publicKey, 1)) + await makeTxSignAndSend( + provider, + await whitelistSdk.setEntityWhitelistStatusIx(EntityType.Solver, solver.publicKey, WhitelistStatus.Whitelisted) + ) }) beforeEach(() => { @@ -117,19 +138,19 @@ describe('Settler Program', () => { ], } - const ix = await sdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.op).to.deep.include({ transfer: {} }) expect(intent.user.toString()).to.be.eq(user.toString()) - expect(intent.intentCreator.toString()).to.be.eq(admin.publicKey.toString()) + expect(intent.intentCreator.toString()).to.be.eq(solver.publicKey.toString()) expect(Buffer.from(intent.nonce).toString('hex')).to.be.eq(nonce) expect(intent.deadline.toNumber()).to.be.eq(deadline) expect(intent.minValidations).to.be.eq(1) expect(intent.validations).to.be.eq(0) expect(intent.isFinal).to.be.false - expect(Buffer.from(intent.data).toString('hex')).to.be.eq('010203') + expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq('010203') expect(intent.maxFees.length).to.be.eq(1) expect(intent.maxFees[0].mint.toString()).to.be.eq(params.maxFees[0].mint.toString()) expect(intent.maxFees[0].amount.toNumber()).to.be.eq(1000) @@ -161,12 +182,12 @@ describe('Settler Program', () => { eventsHex: [], } - const ix = await sdk.createIntentIx(intentHash, params, true) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.op).to.deep.include({ swap: {} }) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq('') + expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq('') expect(intent.isFinal).to.be.true }) @@ -188,8 +209,8 @@ describe('Settler Program', () => { eventsHex: [], } - const ix = await sdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.op).to.deep.include({ call: {} }) @@ -214,8 +235,8 @@ describe('Settler Program', () => { eventsHex: [], } - const ix = await sdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.events.length).to.be.eq(0) @@ -239,8 +260,8 @@ describe('Settler Program', () => { eventsHex: [], } - const ix = await sdk.createIntentIx(intentHash, params, true) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.isFinal).to.be.true @@ -264,14 +285,14 @@ describe('Settler Program', () => { eventsHex: [], } - const ix = await sdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.isFinal).to.be.false }) - xit('cant create intent if not whitelisted solver', async () => { + it('cant create intent if not whitelisted solver', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey @@ -290,10 +311,13 @@ describe('Settler Program', () => { } const ix = await maliciousSdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(maliciousProvider, ix) + const res = await makeTxSignAndSend(maliciousProvider, ix) + expect(res.toString()).to.include( + 'AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized' + ) - const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(intent.intentCreator.toString()).to.be.eq(malicious.publicKey.toString()) + const intent = client.getAccount(sdk.getIntentKey(intentHash)) + expect(intent).to.be.null }) it('cant create intent with deadline in the past', async () => { @@ -316,8 +340,8 @@ describe('Settler Program', () => { eventsHex: [], } - const ix = await sdk.createIntentIx(intentHash, params, false) - const res = await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, false) + const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Deadline can't be in the past`) }) @@ -340,8 +364,8 @@ describe('Settler Program', () => { eventsHex: [], } - const ix = await sdk.createIntentIx(intentHash, params, false) - const res = await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, false) + const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Deadline can't be in the past`) }) @@ -373,8 +397,8 @@ describe('Settler Program', () => { data: Buffer.from('595168911b9267f7' + '010000000000000000', 'hex'), }) - const ix = await sdk.createIntentIx(intentHash, params, false) - const res = await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, false) + const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include( `AnchorError caused by account: fulfilled_intent. Error Code: AccountNotSystemOwned. Error Number: 3011. Error Message: The given account is not owned by the system program` @@ -399,12 +423,12 @@ describe('Settler Program', () => { eventsHex: [], } - const ix = await sdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, false) + await makeTxSignAndSend(solverProvider, ix) client.expireBlockhash() - const ix2 = await sdk.createIntentIx(intentHash, params, false) - const res = await makeTxSignAndSend(provider, ix2) + const ix2 = await solverSdk.createIntentIx(intentHash, params, false) + const res = await makeTxSignAndSend(solverProvider, ix2) expect(res.toString()).to.include(`already in use`) }) @@ -428,8 +452,8 @@ describe('Settler Program', () => { } try { - const ix = await sdk.createIntentIx(invalidIntentHash, params, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(invalidIntentHash, params, false) + await makeTxSignAndSend(solverProvider, ix) expect.fail('Should have thrown an error') } catch (error: any) { expect(error.message).to.include(`Intent hash must be 32 bytes`) @@ -474,8 +498,8 @@ describe('Settler Program', () => { ], } - const ix = await sdk.createIntentIx(intentHash, params, isFinal) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) + await makeTxSignAndSend(solverProvider, ix) return intentHash } @@ -486,11 +510,11 @@ describe('Settler Program', () => { moreDataHex: '070809', } - const ix = await sdk.extendIntentIx(intentHash, extendParams, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq('010203070809') + expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq('010203070809') expect(intent.isFinal).to.be.false }) @@ -507,8 +531,8 @@ describe('Settler Program', () => { ], } - const ix = await sdk.extendIntentIx(intentHash, extendParams, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.maxFees.length).to.be.eq(2) @@ -530,8 +554,8 @@ describe('Settler Program', () => { ], } - const ix = await sdk.extendIntentIx(intentHash, extendParams, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.events.length).to.be.eq(2) @@ -563,24 +587,68 @@ describe('Settler Program', () => { ], } - const ix = await sdk.extendIntentIx(intentHash, extendParams, false) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq('0102030d0e0f') + expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq('0102030d0e0f') expect(intent.maxFees.length).to.be.eq(2) expect(intent.maxFees[1].amount.toNumber()).to.be.eq(3000) expect(intent.events.length).to.be.eq(2) expect(Buffer.from(intent.events[1].data).toString('hex')).to.be.eq('101112') }) + it('should extend an intent to a large size', async () => { + const intentHash = await createTestIntent(false) + const intentKey = sdk.getIntentKey(intentHash) + + for (let i = 0; i < 100; i++) { + const ix = await solverSdk.extendIntentIx(intentHash, { moreDataHex: 'f'.repeat(100) }, false) + await makeTxSignAndSend(solverProvider, ix) + client.expireBlockhash() + } + + for (let i = 0; i < 25; i++) { + const extendParams = { + moreEventsHex: [ + { topicHex: 'e'.repeat(64), dataHex: 'beef'.repeat(100) }, + { topicHex: 'd'.repeat(64), dataHex: 'beef'.repeat(100) }, + ], + } + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(solverProvider, ix) + client.expireBlockhash() + } + + for (let i = 0; i < 19; i++) { + const extendParams = { + moreMaxFees: [ + { mint: Keypair.generate().publicKey, amount: i }, + { mint: Keypair.generate().publicKey, amount: i + 1000 }, + { mint: Keypair.generate().publicKey, amount: i + 2000 }, + ], + } + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) + await makeTxSignAndSend(solverProvider, ix) + client.expireBlockhash() + } + + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + const intentAcc = client.getAccount(intentKey) + expect(intent.intentData.length).to.be.eq(3 + 5000) + expect(intent.maxFees.length).to.be.eq(58) + expect(intent.events.length).to.be.eq(51) + expect(intent.isFinal).to.be.false + expect(intentAcc?.data.length).to.be.eq(19293) + }) + it('should finalize an intent', async () => { const intentHash = await createTestIntent(false) const extendParams = {} - const ix = await sdk.extendIntentIx(intentHash, extendParams, true) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, true) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.isFinal).to.be.true @@ -593,11 +661,11 @@ describe('Settler Program', () => { moreDataHex: '191a1b', } - const ix = await sdk.extendIntentIx(intentHash, extendParams, true) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, true) + await makeTxSignAndSend(solverProvider, ix) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq('010203191a1b') + expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq('010203191a1b') expect(intent.isFinal).to.be.true }) @@ -607,17 +675,17 @@ describe('Settler Program', () => { const extendParams1 = { moreDataHex: '1c1d1e', } - const ix1 = await sdk.extendIntentIx(intentHash, extendParams1, false) - await makeTxSignAndSend(provider, ix1) + const ix1 = await solverSdk.extendIntentIx(intentHash, extendParams1, false) + await makeTxSignAndSend(solverProvider, ix1) const extendParams2 = { moreDataHex: '1f2021', } - const ix2 = await sdk.extendIntentIx(intentHash, extendParams2, false) - await makeTxSignAndSend(provider, ix2) + const ix2 = await solverSdk.extendIntentIx(intentHash, extendParams2, false) + await makeTxSignAndSend(solverProvider, ix2) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq('0102031c1d1e1f2021') + expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq('0102031c1d1e1f2021') expect(intent.isFinal).to.be.false }) @@ -654,8 +722,8 @@ describe('Settler Program', () => { moreDataHex: '28292a', } - const ix = await sdk.extendIntentIx(intentHash, extendParams, false) - const res = await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) + const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Intent is already final`) }) @@ -665,8 +733,8 @@ describe('Settler Program', () => { const extendParams = {} - const ix = await sdk.extendIntentIx(intentHash, extendParams, true) - const res = await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.extendIntentIx(intentHash, extendParams, true) + const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Intent is already final`) }) @@ -697,8 +765,8 @@ describe('Settler Program', () => { eventsHex: [], } - const ix = await sdk.createIntentIx(intentHash, params, isFinal) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) + await makeTxSignAndSend(solverProvider, ix) return intentHash } @@ -715,8 +783,8 @@ describe('Settler Program', () => { const intentBalanceBefore = Number(provider.client.getBalance(sdk.getIntentKey(intentHash))) || 0 const intentCreatorBalanceBefore = Number(provider.client.getBalance(intentBefore.intentCreator)) || 0 - const ix = await sdk.claimStaleIntentIx(intentHash) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.claimStaleIntentIx(intentHash) + await makeTxSignAndSend(solverProvider, ix) const intentBalanceAfter = Number(provider.client.getBalance(sdk.getIntentKey(intentHash))) || 0 const intentCreatorBalanceAfter = Number(provider.client.getBalance(intentBefore.intentCreator)) || 0 @@ -739,8 +807,8 @@ describe('Settler Program', () => { warpSeconds(provider, 100) - const ix = await sdk.claimStaleIntentIx(intentHash) - const res = await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.claimStaleIntentIx(intentHash) + const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Intent not yet expired`) }) @@ -752,8 +820,8 @@ describe('Settler Program', () => { warpSeconds(provider, 300) - const ix = await sdk.claimStaleIntentIx(intentHash) - const res = await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.claimStaleIntentIx(intentHash) + const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Intent not yet expired`) }) @@ -774,8 +842,8 @@ describe('Settler Program', () => { it('cant claim non-existent intent', async () => { const intentHash = generateIntentHash() - const ix = await sdk.claimStaleIntentIx(intentHash) - const res = await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.claimStaleIntentIx(intentHash) + const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`AccountNotInitialized`) }) @@ -787,12 +855,12 @@ describe('Settler Program', () => { warpSeconds(provider, 91) - const ix = await sdk.claimStaleIntentIx(intentHash) - await makeTxSignAndSend(provider, ix) + const ix = await solverSdk.claimStaleIntentIx(intentHash) + await makeTxSignAndSend(solverProvider, ix) client.expireBlockhash() - const ix2 = await sdk.claimStaleIntentIx(intentHash) - const res = await makeTxSignAndSend(provider, ix2) + const ix2 = await solverSdk.claimStaleIntentIx(intentHash) + const res = await makeTxSignAndSend(solverProvider, ix2) const errorMsg = res.toString() expect(errorMsg.includes(`AccountNotInitialized`)).to.be.true From 6ca040893e40c5bc7ade61bd1716491b7109dc86 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 5 Nov 2025 18:04:57 -0300 Subject: [PATCH 26/41] Add Proposal lifecycle ixs --- packages/svm/programs/settler/src/errors.rs | 23 +++++- .../add_instructions_to_proposal.rs | 36 ++++++++- .../src/instructions/claim_stale_proposal.rs | 44 ++++++++++- .../settler/src/instructions/create_intent.rs | 3 +- .../src/instructions/create_proposal.rs | 76 ++++++++++++++++++- .../settler/src/instructions/extend_intent.rs | 2 +- packages/svm/programs/settler/src/lib.rs | 22 ++++-- .../svm/programs/settler/src/state/intent.rs | 7 +- .../programs/settler/src/state/proposal.rs | 24 +++++- packages/svm/tests/settler.test.ts | 6 +- 10 files changed, 220 insertions(+), 23 deletions(-) diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index fa68add..0f907ab 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -17,15 +17,36 @@ pub enum SettlerError { #[msg("Signer must be intent creator")] IncorrectIntentCreator, + #[msg("Signer must be proposal creator")] + IncorrectProposalCreator, + #[msg("Intent is already final")] IntentIsFinal, + #[msg("Intent is not final")] + IntentIsNotFinal, + #[msg("Proposal is already final")] ProposalIsFinal, #[msg("Intent not yet expired. Please wait for the deadline to pass")] IntentNotYetExpired, - #[msg("Deadline can't be in the past")] + #[msg("Intent has already expired")] + IntentIsExpired, + + #[msg("Proposal not yet expired. Please wait for the deadline to pass")] + ProposalNotYetExpired, + + #[msg("Proposal has already expired")] + ProposalIsExpired, + + #[msg("Deadline must be in the future")] DeadlineIsInThePast, + + #[msg("Proposal deadline can't be after the Intent's deadline")] + ProposalDeadlineExceedsIntentDeadline, + + #[msg("Intent has insufficient validations")] + InsufficientIntentValidations, } diff --git a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs index 477daa8..ecf2688 100644 --- a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs @@ -1,8 +1,40 @@ use anchor_lang::prelude::*; +use crate::{ + errors::SettlerError, + state::{Proposal, ProposalInstruction}, +}; + #[derive(Accounts)] -pub struct AddInstructionsToProposal {} +#[instruction(more_instructions: Vec)] +pub struct AddInstructionsToProposal<'info> { + #[account(mut)] + pub proposal_creator: Signer<'info>, + + #[account( + mut, + realloc = Proposal::extended_size(proposal.to_account_info().data_len(), &more_instructions), + realloc::payer = proposal_creator, + realloc::zero = true, + has_one = proposal_creator @ SettlerError::IncorrectProposalCreator + )] + // Any proposal + pub proposal: Box>, + + pub system_program: Program<'info, System>, +} + +pub fn add_instructions_to_proposal( + ctx: Context, + more_instructions: Vec, +) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + let proposal = &mut ctx.accounts.proposal; + + require!(proposal.deadline > now, SettlerError::ProposalIsExpired); + require!(!proposal.is_final, SettlerError::ProposalIsFinal); + + proposal.instructions.extend_from_slice(&more_instructions); -pub fn add_instructions_to_proposal(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs index aaa5b01..6a3a6a8 100644 --- a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs @@ -1,8 +1,48 @@ use anchor_lang::prelude::*; +use crate::{errors::SettlerError, state::Proposal}; + #[derive(Accounts)] -pub struct ClaimStaleProposal {} +pub struct ClaimStaleProposal<'info> { + #[account(mut)] + pub proposal_creator: Signer<'info>, + + #[account( + mut, + close = proposal_creator, + has_one = proposal_creator @ SettlerError::IncorrectProposalCreator + )] + pub proposal: Box>, + // + // remaining_accounts (N): + // + // #[account( + // mut, + // close = proposal_creator, + // has_one = proposal_creator @ SettlerError::IncorrectProposalCreator + // )] + // pub proposal_n: Box>, +} + +pub fn claim_stale_proposal<'info>( + ctx: Context<'_, '_, 'info, 'info, ClaimStaleProposal<'info>>, +) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + let proposal_creator = ctx.accounts.proposal_creator.to_account_info(); + + for account_info in ctx.remaining_accounts { + let proposal: Box> = + Box::new(Account::::try_from(account_info)?); + + require_keys_eq!( + proposal.proposal_creator, + proposal_creator.key(), + SettlerError::IncorrectProposalCreator + ); + require!(now > proposal.deadline, SettlerError::ProposalNotYetExpired); + + proposal.close(ctx.accounts.proposal_creator.to_account_info())?; + } -pub fn claim_stale_proposal(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index 1ce9cf3..2da26a6 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -18,7 +18,7 @@ pub struct CreateIntent<'info> { pub solver: Signer<'info>, #[account( - seeds = [b"entity-registry".as_ref(), &[EntityType::Solver as u8 + 1], solver.key().as_ref()], + seeds = [b"entity-registry", &[EntityType::Solver as u8 + 1], solver.key().as_ref()], bump = solver_registry.bump, seeds::program = crate::whitelist::ID, constraint = @@ -70,6 +70,7 @@ pub fn create_intent( intent.op = op; intent.user = user; intent.intent_creator = ctx.accounts.solver.key(); + intent.intent_hash = intent_hash; intent.nonce = nonce; intent.deadline = deadline; intent.min_validations = min_validations; diff --git a/packages/svm/programs/settler/src/instructions/create_proposal.rs b/packages/svm/programs/settler/src/instructions/create_proposal.rs index 2f7bc41..d7fda37 100644 --- a/packages/svm/programs/settler/src/instructions/create_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/create_proposal.rs @@ -1,8 +1,80 @@ use anchor_lang::prelude::*; +use crate::{ + errors::SettlerError, + state::{Intent, Proposal, ProposalInstruction}, + whitelist::{ + accounts::EntityRegistry, + types::{EntityType, WhitelistStatus}, + }, +}; + #[derive(Accounts)] -pub struct CreateProposal {} +#[instruction(instructions: Vec)] +pub struct CreateProposal<'info> { + #[account(mut)] + pub solver: Signer<'info>, + + #[account( + seeds = [b"entity-registry", &[EntityType::Solver as u8 + 1], solver.key().as_ref()], + bump = solver_registry.bump, + seeds::program = crate::whitelist::ID, + constraint = + solver_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::OnlySolver + )] + pub solver_registry: Box>, + + /// Any intent + pub intent: Box>, + + #[account( + seeds = [b"fulfilled-intent", intent.intent_hash.as_ref()], + bump + )] + /// This PDA must be uninitialized + pub fulfilled_intent: SystemAccount<'info>, + + #[account( + init, + seeds = [b"proposal", intent.key().as_ref(), solver.key().as_ref()], + bump, + payer = solver, + space = 8 + Proposal::BASE_LEN + Proposal::instructions_size(&instructions) + )] + pub proposal: Box>, + + pub system_program: Program<'info, System>, +} + +pub fn create_proposal( + ctx: Context, + instructions: Vec, + deadline: u64, + is_final: bool, +) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + let intent = &ctx.accounts.intent; + + require!(deadline > now, SettlerError::DeadlineIsInThePast); + require!(intent.deadline > now, SettlerError::IntentIsExpired); + require!( + deadline <= intent.deadline, + SettlerError::ProposalDeadlineExceedsIntentDeadline + ); + require!( + intent.validations >= intent.min_validations, + SettlerError::InsufficientIntentValidations + ); + require!(intent.is_final, SettlerError::IntentIsNotFinal); + + let proposal = &mut ctx.accounts.proposal; + + proposal.intent = intent.key(); + proposal.proposal_creator = ctx.accounts.solver.key(); + proposal.deadline = deadline; + proposal.is_final = is_final; + proposal.instructions = instructions; + proposal.bump = ctx.bumps.proposal; -pub fn create_proposal(ctx: Context) -> Result<()> { Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/extend_intent.rs b/packages/svm/programs/settler/src/instructions/extend_intent.rs index 59fcb55..ab74148 100644 --- a/packages/svm/programs/settler/src/instructions/extend_intent.rs +++ b/packages/svm/programs/settler/src/instructions/extend_intent.rs @@ -17,7 +17,7 @@ pub struct ExtendIntent<'info> { has_one = intent_creator @ SettlerError::IncorrectIntentCreator, constraint = !intent.is_final @ SettlerError::IntentIsFinal, realloc = - intent.extended_size(intent.to_account_info().data_len(), &more_data, &more_max_fees, &more_events)?, + Intent::extended_size(intent.to_account_info().data_len(), &more_data, &more_max_fees, &more_events), realloc::payer = intent_creator, realloc::zero = true )] diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index e644b82..31f4c12 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -9,7 +9,7 @@ pub mod instructions; pub mod state; pub mod types; -use crate::{instructions::*, types::*}; +use crate::{instructions::*, state::*, types::*}; #[program] pub mod settler { @@ -19,8 +19,11 @@ pub mod settler { instructions::add_axia_sig(ctx) } - pub fn add_instructions_to_proposal(ctx: Context) -> Result<()> { - instructions::add_instructions_to_proposal(ctx) + pub fn add_instructions_to_proposal( + ctx: Context, + more_instructions: Vec, + ) -> Result<()> { + instructions::add_instructions_to_proposal(ctx, more_instructions) } pub fn add_validator_sigs(ctx: Context) -> Result<()> { @@ -35,7 +38,9 @@ pub mod settler { instructions::claim_stale_intent(ctx) } - pub fn claim_stale_proposal(ctx: Context) -> Result<()> { + pub fn claim_stale_proposal<'info>( + ctx: Context<'_, '_, 'info, 'info, ClaimStaleProposal<'info>>, + ) -> Result<()> { instructions::claim_stale_proposal(ctx) } @@ -67,8 +72,13 @@ pub mod settler { ) } - pub fn create_proposal(ctx: Context) -> Result<()> { - instructions::create_proposal(ctx) + pub fn create_proposal( + ctx: Context, + instructions: Vec, + deadline: u64, + is_final: bool, + ) -> Result<()> { + instructions::create_proposal(ctx, instructions, deadline, is_final) } pub fn execute_proposal(ctx: Context) -> Result<()> { diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs index bc30689..2b7bdd1 100644 --- a/packages/svm/programs/settler/src/state/intent.rs +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -7,6 +7,7 @@ pub struct Intent { pub op: OpType, pub user: Pubkey, pub intent_creator: Pubkey, + pub intent_hash: [u8; 32], pub nonce: [u8; 32], pub deadline: u64, pub min_validations: u16, @@ -24,6 +25,7 @@ impl Intent { 1 + // op 32 + // user 32 + // intent_creator + 32 + // intent_hash 32 + // nonce 8 + // deadline 2 + // min_validations @@ -45,12 +47,11 @@ impl Intent { } pub fn extended_size( - &self, mut size: usize, more_data: &Option>, more_max_fees: &Option>, more_events: &Option>, - ) -> Result { + ) -> usize { if let Some(_more_data) = more_data { size += Intent::data_size(_more_data.len()) - 4; } @@ -63,6 +64,6 @@ impl Intent { size += Intent::events_size(&_more_events) - 4; } - Ok(size) + size } } diff --git a/packages/svm/programs/settler/src/state/proposal.rs b/packages/svm/programs/settler/src/state/proposal.rs index b17d041..894bc05 100644 --- a/packages/svm/programs/settler/src/state/proposal.rs +++ b/packages/svm/programs/settler/src/state/proposal.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; #[account] pub struct Proposal { pub intent: Pubkey, - pub solver: Pubkey, + pub proposal_creator: Pubkey, pub deadline: u64, pub is_final: bool, pub instructions: Vec, @@ -14,11 +14,22 @@ impl Proposal { /// Doesn't take into account size of variable fields pub const BASE_LEN: usize = 32 + // intent - 32 + // solver + 32 + // proposal_creator 8 + // deadline 1 + // is_final 1 // bump ; + + pub fn instructions_size(instructions: &Vec) -> usize { + 4 + instructions + .iter() + .map(|instruction| instruction.size()) + .sum::() + } + + pub fn extended_size(size: usize, more_instructions: &Vec) -> usize { + size + Proposal::instructions_size(more_instructions) - 4 + } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] @@ -28,6 +39,15 @@ pub struct ProposalInstruction { pub data: Vec, } +impl ProposalInstruction { + pub fn size(&self) -> usize { + let accounts_size = 4 + self.accounts.len() * (32 + 1 + 1); + let data_size = 4 + self.data.len(); + + 32 + accounts_size + data_size + } +} + #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct ProposalInstructionAccountMeta { pub pubkey: Pubkey, diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 12161f8..456c395 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -343,7 +343,7 @@ describe('Settler Program', () => { const ix = await solverSdk.createIntentIx(intentHash, params, false) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Deadline can't be in the past`) + expect(res.toString()).to.include(`Deadline must be in the future`) }) it('cant create intent with deadline equal to now', async () => { @@ -367,7 +367,7 @@ describe('Settler Program', () => { const ix = await solverSdk.createIntentIx(intentHash, params, false) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Deadline can't be in the past`) + expect(res.toString()).to.include(`Deadline must be in the future`) }) it('cant create intent if fulfilled_intent PDA already exists', async () => { @@ -639,7 +639,7 @@ describe('Settler Program', () => { expect(intent.maxFees.length).to.be.eq(58) expect(intent.events.length).to.be.eq(51) expect(intent.isFinal).to.be.false - expect(intentAcc?.data.length).to.be.eq(19293) + expect(intentAcc?.data.length).to.be.eq(19325) }) it('should finalize an intent', async () => { From 3431bc5fed80bc9d5284bf9582e2c38e5fd7a9b7 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 5 Nov 2025 18:12:21 -0300 Subject: [PATCH 27/41] Add SDK methods --- packages/svm/sdks/settler/Settler.ts | 93 +++++++++++++++++++++++++++- packages/svm/sdks/settler/types.ts | 12 ++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index 67bf3d5..00c4c22 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -4,7 +4,15 @@ import * as SettlerIDL from '../../target/idl/settler.json' import * as WhitelistIDL from '../../target/idl/whitelist.json' import { Settler } from '../../target/types/settler' import { EntityType } from '../whitelist/Whitelist' -import { CreateIntentParams, ExtendIntentParams, IntentEvent, MaxFee, OpType } from './types' +import { + CreateIntentParams, + ExtendIntentParams, + IntentEvent, + MaxFee, + OpType, + ProposalInstruction, + ProposalInstructionAccountMeta, +} from './types' type MaxFeeAnchor = { mint: web3.PublicKey @@ -16,6 +24,12 @@ type IntentEventAnchor = { data: Buffer } +type ProposalInstructionAnchor = { + programId: web3.PublicKey + accounts: ProposalInstructionAccountMeta[] + data: Buffer +} + export default class SettlerSDK { protected program: Program @@ -97,6 +111,63 @@ export default class SettlerSDK { return ix } + async createProposalIx( + intentHashHex: string, + instructions: ProposalInstruction[], + deadline: number, + isFinal = true + ): Promise { + const parsedInstructions = this.parseProposalInstructions(instructions) + + const ix = await this.program.methods + .createProposal(parsedInstructions, new BN(deadline), isFinal) + .accountsPartial({ + solver: this.getSignerKey(), + solverRegistry: this.getEntityRegistryPubkey(EntityType.Solver, this.getSignerKey()), + intent: this.getIntentKey(intentHashHex), + fulfilledIntent: this.getFulfilledIntentKey(intentHashHex), + }) + .instruction() + + return ix + } + + async addInstructionsToProposalIx( + intentHashHex: string, + moreInstructions: ProposalInstruction[], + solverPubkey?: web3.PublicKey + ): Promise { + const parsedInstructions = this.parseProposalInstructions(moreInstructions) + const solver = solverPubkey || this.getSignerKey() + + const ix = await this.program.methods + .addInstructionsToProposal(parsedInstructions) + .accountsPartial({ + proposalCreator: this.getSignerKey(), + proposal: this.getProposalKey(intentHashHex, solver), + }) + .instruction() + + return ix + } + + async claimStaleProposalIx( + intentHashHex: string, + solverPubkey?: web3.PublicKey + ): Promise { + const solver = solverPubkey || this.getSignerKey() + + const ix = await this.program.methods + .claimStaleProposal() + .accountsPartial({ + proposalCreator: this.getSignerKey(), + proposal: this.getProposalKey(intentHashHex, solver), + }) + .instruction() + + return ix + } + getSettlerSettingsPubkey(): web3.PublicKey { return web3.PublicKey.findProgramAddressSync([Buffer.from('settler-settings')], this.program.programId)[0] } @@ -118,6 +189,19 @@ export default class SettlerSDK { )[0] } + getProposalKey(intentHashHex: string, solverPubkey?: web3.PublicKey): web3.PublicKey { + const intentHash = Buffer.from(intentHashHex, 'hex') + if (intentHash.length != 32) throw new Error(`Intent hash must be 32 bytes: ${intentHashHex}`) + + const intentKey = this.getIntentKey(intentHashHex) + const solver = solverPubkey || this.getSignerKey() + + return web3.PublicKey.findProgramAddressSync( + [Buffer.from('proposal'), intentKey.toBuffer(), solver.toBuffer()], + this.program.programId + )[0] + } + getEntityRegistryPubkey(entityType: EntityType, entityPubkey: web3.PublicKey): web3.PublicKey { return web3.PublicKey.findProgramAddressSync( [Buffer.from('entity-registry'), Buffer.from([entityType]), entityPubkey.toBuffer()], @@ -165,4 +249,11 @@ export default class SettlerSDK { amount: new BN(maxFee.amount), })) } + + private parseProposalInstructions(instructions: ProposalInstruction[]): ProposalInstructionAnchor[] { + return instructions.map((instruction) => ({ + ...instruction, + data: typeof instruction.data === 'string' ? Buffer.from(instruction.data, 'hex') : instruction.data, + })) + } } diff --git a/packages/svm/sdks/settler/types.ts b/packages/svm/sdks/settler/types.ts index 227ebce..8f21267 100644 --- a/packages/svm/sdks/settler/types.ts +++ b/packages/svm/sdks/settler/types.ts @@ -35,3 +35,15 @@ export type ExtendIntentParams = { moreMaxFees?: MaxFee[] moreEventsHex?: IntentEvent[] } + +export type ProposalInstructionAccountMeta = { + pubkey: web3.PublicKey + isSigner: boolean + isWritable: boolean +} + +export type ProposalInstruction = { + programId: web3.PublicKey + accounts: ProposalInstructionAccountMeta[] + data: Buffer | string +} From b0726aa8e744ac2f889bf289a6783a53b745e562 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 5 Nov 2025 19:07:14 -0300 Subject: [PATCH 28/41] Add Proposal cycle tests --- .../add_instructions_to_proposal.rs | 5 + .../src/instructions/claim_stale_proposal.rs | 7 - packages/svm/programs/settler/src/lib.rs | 3 +- packages/svm/sdks/settler/Settler.ts | 19 +- packages/svm/tests/settler.test.ts | 966 ++++++++++++++++++ 5 files changed, 985 insertions(+), 15 deletions(-) diff --git a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs index ecf2688..8c9cc91 100644 --- a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs @@ -27,6 +27,7 @@ pub struct AddInstructionsToProposal<'info> { pub fn add_instructions_to_proposal( ctx: Context, more_instructions: Vec, + finalize: bool, ) -> Result<()> { let now = Clock::get()?.unix_timestamp as u64; let proposal = &mut ctx.accounts.proposal; @@ -36,5 +37,9 @@ pub fn add_instructions_to_proposal( proposal.instructions.extend_from_slice(&more_instructions); + if finalize { + proposal.is_final = true; + } + Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs index 6a3a6a8..25a7b9c 100644 --- a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs @@ -6,13 +6,6 @@ use crate::{errors::SettlerError, state::Proposal}; pub struct ClaimStaleProposal<'info> { #[account(mut)] pub proposal_creator: Signer<'info>, - - #[account( - mut, - close = proposal_creator, - has_one = proposal_creator @ SettlerError::IncorrectProposalCreator - )] - pub proposal: Box>, // // remaining_accounts (N): // diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index 31f4c12..93993fb 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -22,8 +22,9 @@ pub mod settler { pub fn add_instructions_to_proposal( ctx: Context, more_instructions: Vec, + finalize: bool, ) -> Result<()> { - instructions::add_instructions_to_proposal(ctx, more_instructions) + instructions::add_instructions_to_proposal(ctx, more_instructions, finalize) } pub fn add_validator_sigs(ctx: Context) -> Result<()> { diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index 00c4c22..2a3116c 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -80,7 +80,7 @@ export default class SettlerSDK { async extendIntentIx( intentHashHex: string, params: ExtendIntentParams, - isFinal = true + finalize = true ): Promise { const { moreDataHex = '', moreMaxFees = [], moreEventsHex = [] } = params @@ -89,7 +89,7 @@ export default class SettlerSDK { const moreEvents = this.parseIntentEventsHex(moreEventsHex) const ix = await this.program.methods - .extendIntent(moreData, moreMaxFeesBn, moreEvents, isFinal) + .extendIntent(moreData, moreMaxFeesBn, moreEvents, finalize) .accountsPartial({ intentCreator: this.getSignerKey(), intent: this.getIntentKey(intentHashHex), @@ -135,13 +135,14 @@ export default class SettlerSDK { async addInstructionsToProposalIx( intentHashHex: string, moreInstructions: ProposalInstruction[], + finalize = true, solverPubkey?: web3.PublicKey ): Promise { const parsedInstructions = this.parseProposalInstructions(moreInstructions) const solver = solverPubkey || this.getSignerKey() const ix = await this.program.methods - .addInstructionsToProposal(parsedInstructions) + .addInstructionsToProposal(parsedInstructions, finalize) .accountsPartial({ proposalCreator: this.getSignerKey(), proposal: this.getProposalKey(intentHashHex, solver), @@ -152,17 +153,21 @@ export default class SettlerSDK { } async claimStaleProposalIx( - intentHashHex: string, + intentHashesHex: string[], solverPubkey?: web3.PublicKey ): Promise { - const solver = solverPubkey || this.getSignerKey() - const ix = await this.program.methods .claimStaleProposal() .accountsPartial({ proposalCreator: this.getSignerKey(), - proposal: this.getProposalKey(intentHashHex, solver), }) + .remainingAccounts( + intentHashesHex.map((intentHashHex) => ({ + pubkey: this.getProposalKey(intentHashHex, solverPubkey), + isWritable: true, + isSigner: false, + })) + ) .instruction() return ix diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 456c395..a62b198 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -866,5 +866,971 @@ describe('Settler Program', () => { expect(errorMsg.includes(`AccountNotInitialized`)).to.be.true }) }) + + describe('create_proposal', () => { + const generateIntentHash = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const generateNonce = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const createValidatedIntent = async (isFinal = true): Promise => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) + await makeTxSignAndSend(solverProvider, ix) + + // Set validations to meet min_validations requirement + const intentKey = sdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + // validations is at offset: 8 (disc) + 1 (op) + 32 (user) + 32 (intent_creator) + 32 (intent_hash) + 32 (nonce) + 8 (deadline) + 2 (min_validations) = 147 + // validations is u16, so 2 bytes + intentData.writeUInt16LE(1, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + return intentHash + } + + it('should create a proposal', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [ + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + ], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + await makeTxSignAndSend(solverProvider, ix) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.intent.toString()).to.be.eq(sdk.getIntentKey(intentHash).toString()) + expect(proposal.proposalCreator.toString()).to.be.eq(solver.publicKey.toString()) + expect(proposal.deadline.toNumber()).to.be.eq(deadline) + expect(proposal.isFinal).to.be.true + expect(proposal.instructions.length).to.be.eq(1) + expect(proposal.instructions[0].programId.toString()).to.be.eq(instructions[0].programId.toString()) + expect(Buffer.from(proposal.instructions[0].data).toString('hex')).to.be.eq('deadbeef') + expect(proposal.instructions[0].accounts.length).to.be.eq(1) + expect(proposal.instructions[0].accounts[0].pubkey.toString()).to.be.eq( + instructions[0].accounts[0].pubkey.toString() + ) + expect(proposal.instructions[0].accounts[0].isSigner).to.be.eq(false) + expect(proposal.instructions[0].accounts[0].isWritable).to.be.eq(true) + }) + + it('should create a proposal with multiple instructions', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [ + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + ], + data: '010203', + }, + { + programId: Keypair.generate().publicKey, + accounts: [ + { + pubkey: Keypair.generate().publicKey, + isSigner: true, + isWritable: false, + }, + ], + data: '040506', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + await makeTxSignAndSend(solverProvider, ix) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.instructions.length).to.be.eq(2) + expect(Buffer.from(proposal.instructions[0].data).toString('hex')).to.be.eq('010203') + expect(Buffer.from(proposal.instructions[1].data).toString('hex')).to.be.eq('040506') + expect(proposal.isFinal).to.be.true + expect(proposal.instructions[0].accounts.length).to.be.eq(1) + expect(proposal.instructions[0].accounts[0].pubkey.toString()).to.be.eq( + instructions[0].accounts[0].pubkey.toString() + ) + expect(proposal.instructions[0].accounts[0].isSigner).to.be.eq(false) + expect(proposal.instructions[0].accounts[0].isWritable).to.be.eq(true) + expect(proposal.instructions[1].accounts.length).to.be.eq(1) + expect(proposal.instructions[1].accounts[0].pubkey.toString()).to.be.eq( + instructions[1].accounts[0].pubkey.toString() + ) + expect(proposal.instructions[1].accounts[0].isSigner).to.be.eq(true) + expect(proposal.instructions[1].accounts[0].isWritable).to.be.eq(false) + }) + + it('should create a proposal with empty instructions', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + const instructions: any[] = [] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + await makeTxSignAndSend(solverProvider, ix) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.instructions.length).to.be.eq(0) + }) + + it('cant create proposal if not whitelisted solver', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix = await maliciousSdk.createProposalIx(intentHash, instructions, deadline) + const res = await makeTxSignAndSend(maliciousProvider, ix) + expect(res.toString()).to.include( + 'AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized' + ) + }) + + it('cant create proposal with deadline in the past', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now - 100 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`Deadline must be in the future`) + }) + + it('cant create proposal with deadline equal to now', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`Deadline must be in the future`) + }) + + it('cant create proposal if intent deadline has passed', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const intentDeadline = now + 100 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline: intentDeadline, + minValidations: 1, + dataHex: '010203', + maxFees: [], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params) + await makeTxSignAndSend(solverProvider, ix) + + // Set validations + const intentKey = sdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + intentData.writeUInt16LE(1, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + warpSeconds(provider, 101) + + const proposalDeadline = now + 200 + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix2 = await solverSdk.createProposalIx(intentHash, instructions, proposalDeadline) + const res = await makeTxSignAndSend(solverProvider, ix2) + + expect(res.toString()).to.include(`Intent has already expired`) + }) + + it('cant create proposal if proposal deadline exceeds intent deadline', async () => { + const intentHash = await createValidatedIntent(true) + const intentDeadline = Number((await program.account.intent.fetch(sdk.getIntentKey(intentHash))).deadline) + const proposalDeadline = intentDeadline + 100 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, proposalDeadline) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`Proposal deadline can't be after the Intent's deadline`) + }) + + it('cant create proposal if intent has insufficient validations', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 2, + dataHex: '010203', + maxFees: [], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params) + await makeTxSignAndSend(solverProvider, ix) + + // Set validations to 1 (less than min_validations of 2) + const intentKey = sdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + intentData.writeUInt16LE(1, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + const proposalDeadline = now + 1800 + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix2 = await solverSdk.createProposalIx(intentHash, instructions, proposalDeadline) + const res = await makeTxSignAndSend(solverProvider, ix2) + + expect(res.toString()).to.include(`Intent has insufficient validations`) + }) + + it('cant create proposal if intent is not final', async () => { + const intentHash = await createValidatedIntent(false) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`Intent is not final`) + }) + + it('cant create proposal if fulfilled_intent PDA already exists', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + // Mock FulfilledIntent + const fulfilledIntent = sdk.getFulfilledIntentKey(intentHash) + client.setAccount(fulfilledIntent, { + executable: false, + lamports: 1002240, + owner: program.programId, + data: Buffer.from('595168911b9267f7' + '010000000000000000', 'hex'), + }) + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include( + `AnchorError caused by account: fulfilled_intent. Error Code: AccountNotSystemOwned. Error Number: 3011. Error Message: The given account is not owned by the system program` + ) + }) + + it('cant create proposal with same intent_hash and solver twice', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + await makeTxSignAndSend(solverProvider, ix) + + client.expireBlockhash() + const ix2 = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const res = await makeTxSignAndSend(solverProvider, ix2) + + expect(res.toString()).to.include(`already in use`) + }) + + it('cant create proposal for non-existent intent', async () => { + const intentHash = generateIntentHash() + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`AccountNotInitialized`) + }) + }) + + describe('add_instructions_to_proposal', () => { + const generateIntentHash = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const generateNonce = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const createValidatedIntent = async (isFinal = true): Promise => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) + await makeTxSignAndSend(solverProvider, ix) + + // Set validations + const intentKey = sdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + intentData.writeUInt16LE(1, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + return intentHash + } + + const createTestProposal = async (isFinal = false): Promise => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [ + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + ], + data: '010203', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, isFinal) + await makeTxSignAndSend(solverProvider, ix) + return intentHash + } + + it('should add instructions to proposal', async () => { + const intentHash = await createTestProposal(false) + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [ + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + ], + data: '040506', + }, + ] + + const ix = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions, false) + await makeTxSignAndSend(solverProvider, ix) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.instructions.length).to.be.eq(2) + expect(Buffer.from(proposal.instructions[0].data).toString('hex')).to.be.eq('010203') + expect(Buffer.from(proposal.instructions[1].data).toString('hex')).to.be.eq('040506') + expect(proposal.isFinal).to.be.false + expect(proposal.instructions[1].accounts.length).to.be.eq(1) + expect(proposal.instructions[1].accounts[0].pubkey.toString()).to.be.eq( + moreInstructions[0].accounts[0].pubkey.toString() + ) + expect(proposal.instructions[1].accounts[0].isSigner).to.be.eq(false) + expect(proposal.instructions[1].accounts[0].isWritable).to.be.eq(true) + }) + + it('should add multiple instructions to proposal', async () => { + const intentHash = await createTestProposal(false) + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '070809', + }, + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '0a0b0c', + }, + ] + + const ix = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions, false) + await makeTxSignAndSend(solverProvider, ix) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.instructions.length).to.be.eq(3) + expect(Buffer.from(proposal.instructions[1].data).toString('hex')).to.be.eq('070809') + expect(Buffer.from(proposal.instructions[2].data).toString('hex')).to.be.eq('0a0b0c') + expect(proposal.isFinal).to.be.false + }) + + it('should add instructions to proposal multiple times', async () => { + const intentHash = await createTestProposal(false) + + const moreInstructions1 = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '0d0e0f', + }, + ] + const ix1 = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions1, false) + await makeTxSignAndSend(solverProvider, ix1) + + const moreInstructions2 = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '101112', + }, + ] + const ix2 = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions2, false) + await makeTxSignAndSend(solverProvider, ix2) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.instructions.length).to.be.eq(3) + expect(Buffer.from(proposal.instructions[1].data).toString('hex')).to.be.eq('0d0e0f') + expect(Buffer.from(proposal.instructions[2].data).toString('hex')).to.be.eq('101112') + expect(proposal.isFinal).to.be.false + }) + + it('cant add instructions if not proposal creator', async () => { + const intentHash = await createTestProposal(false) + const proposalCreator = (await program.account.proposal.fetch(solverSdk.getProposalKey(intentHash))) + .proposalCreator + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '131415', + }, + ] + + const ix = await maliciousSdk.addInstructionsToProposalIx( + intentHash, + moreInstructions, + undefined, + proposalCreator + ) + const res = await makeTxSignAndSend(maliciousProvider, ix) + + expect(res.toString()).to.include(`Signer must be proposal creator`) + }) + + it('cant add instructions to non-existent proposal', async () => { + const intentHash = generateIntentHash() + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '161718', + }, + ] + + const ix = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`AccountNotInitialized`) + }) + + it('cant add instructions if proposal deadline has passed', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 50 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '010203', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, false) + await makeTxSignAndSend(solverProvider, ix) + + warpSeconds(provider, 51) + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '19202a', + }, + ] + + const ix2 = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions) + const res = await makeTxSignAndSend(solverProvider, ix2) + + expect(res.toString()).to.include(`Proposal has already expired`) + }) + + it('cant add instructions if proposal deadline equals now', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 100 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '010203', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, false) + await makeTxSignAndSend(solverProvider, ix) + + warpSeconds(provider, 100) + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '1b1c1d', + }, + ] + + const ix2 = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions) + const res = await makeTxSignAndSend(solverProvider, ix2) + + expect(res.toString()).to.include(`Proposal has already expired`) + }) + + it('cant add instructions if proposal is final', async () => { + const intentHash = await createTestProposal(true) + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '1e1f20', + }, + ] + + const ix = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`Proposal is already final`) + }) + + it('should finalize proposal when adding instructions with finalize=true', async () => { + const intentHash = await createTestProposal(false) + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '212223', + }, + ] + + const ix = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions, true) + await makeTxSignAndSend(solverProvider, ix) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.isFinal).to.be.true + expect(proposal.instructions.length).to.be.eq(2) + }) + + it('should not finalize proposal when adding instructions with finalize=false', async () => { + const intentHash = await createTestProposal(false) + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '242526', + }, + ] + + const ix = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions, false) + await makeTxSignAndSend(solverProvider, ix) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.isFinal).to.be.false + expect(proposal.instructions.length).to.be.eq(2) + }) + + it('should finalize proposal by default when adding instructions', async () => { + const intentHash = await createTestProposal(false) + + const moreInstructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: '272829', + }, + ] + + const ix = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions) + await makeTxSignAndSend(solverProvider, ix) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.isFinal).to.be.true + expect(proposal.instructions.length).to.be.eq(2) + }) + }) + + describe('claim_stale_proposal', () => { + const generateIntentHash = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const generateNonce = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const createValidatedIntent = async (isFinal = true): Promise => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) + await makeTxSignAndSend(solverProvider, ix) + + // Set validations + const intentKey = sdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + intentData.writeUInt16LE(1, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + return intentHash + } + + const createTestProposalWithDeadline = async (deadline: number): Promise => { + const intentHash = await createValidatedIntent(true) + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [ + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + ], + data: '010203', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, false) + await makeTxSignAndSend(solverProvider, ix) + return intentHash + } + + it('should claim stale proposal', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 50 + const intentHash = await createTestProposalWithDeadline(deadline) + + const proposalBefore = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposalBefore).to.not.be.null + + warpSeconds(provider, 51) + + const proposalBalanceBefore = + Number(provider.client.getBalance(sdk.getProposalKey(intentHash, solver.publicKey))) || 0 + const proposalCreatorBalanceBefore = Number(provider.client.getBalance(proposalBefore.proposalCreator)) || 0 + + const ix = await solverSdk.claimStaleProposalIx([intentHash]) + await makeTxSignAndSend(solverProvider, ix) + + const proposalBalanceAfter = + Number(provider.client.getBalance(sdk.getProposalKey(intentHash, solver.publicKey))) || 0 + const proposalCreatorBalanceAfter = Number(provider.client.getBalance(proposalBefore.proposalCreator)) || 0 + + try { + await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect.fail('Proposal account should be closed') + } catch (error: any) { + expect(error.message).to.include(`Account does not exist`) + } + + expect(proposalCreatorBalanceAfter).to.be.eq(proposalCreatorBalanceBefore + proposalBalanceBefore - 5000) + expect(proposalBalanceAfter).to.be.eq(0) + }) + + it('should claim multiple stale proposals', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 50 + const intentHashes = await Promise.all( + Array.from({ length: 20 }, async () => await createTestProposalWithDeadline(deadline)) + ) + + for (const intentHash of intentHashes) { + const proposalBefore = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposalBefore).to.not.be.null + } + + warpSeconds(provider, 51) + + const proposalBalancesBefore = intentHashes.reduce( + (acc, intentHash) => + acc + Number(provider.client.getBalance(sdk.getProposalKey(intentHash, solver.publicKey))) || 0, + 0 + ) + const proposalCreatorBalanceBefore = Number(provider.client.getBalance(solver.publicKey)) || 0 + + const ix = await solverSdk.claimStaleProposalIx(intentHashes) + await makeTxSignAndSend(solverProvider, ix) + + const proposalBalancesAfter = intentHashes.reduce( + (acc, intentHash) => + acc + Number(provider.client.getBalance(sdk.getProposalKey(intentHash, solver.publicKey))) || 0, + 0 + ) + const proposalCreatorBalanceAfter = Number(provider.client.getBalance(solver.publicKey)) || 0 + + for (const intentHash of intentHashes) { + try { + await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect.fail('Proposal account should be closed') + } catch (error: any) { + expect(error.message).to.include(`Account does not exist`) + } + } + + expect(proposalCreatorBalanceAfter).to.be.eq(proposalCreatorBalanceBefore + proposalBalancesBefore - 5000) + expect(proposalBalancesAfter).to.be.eq(0) + }) + + it('cant claim proposal if deadline has not passed', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 500 + const intentHash = await createTestProposalWithDeadline(deadline) + + warpSeconds(provider, 100) + + const ix = await solverSdk.claimStaleProposalIx([intentHash]) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`Proposal not yet expired`) + }) + + it('cant claim proposal if deadline equals now', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 300 + const intentHash = await createTestProposalWithDeadline(deadline) + + warpSeconds(provider, 300) + + const ix = await solverSdk.claimStaleProposalIx([intentHash]) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`Proposal not yet expired`) + }) + + it('cant claim stale proposal if not proposal creator', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 80 + const intentHash = await createTestProposalWithDeadline(deadline) + + warpSeconds(provider, 81) + + const ix = await maliciousSdk.claimStaleProposalIx([intentHash], solver.publicKey) + const res = await makeTxSignAndSend(maliciousProvider, ix) + + expect(res.toString()).to.include(`Signer must be proposal creator`) + }) + + it('cant claim non-existent proposal', async () => { + const intentHash = generateIntentHash() + + const ix = await solverSdk.claimStaleProposalIx([intentHash]) + const res = await makeTxSignAndSend(solverProvider, ix) + + expect(res.toString()).to.include(`AccountNotInitialized`) + }) + + it('cant claim proposal twice', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 90 + const intentHash = await createTestProposalWithDeadline(deadline) + + warpSeconds(provider, 91) + + const ix = await solverSdk.claimStaleProposalIx([intentHash]) + await makeTxSignAndSend(solverProvider, ix) + + client.expireBlockhash() + const ix2 = await solverSdk.claimStaleProposalIx([intentHash]) + const res = await makeTxSignAndSend(solverProvider, ix2) + + const errorMsg = res.toString() + expect(errorMsg.includes(`AccountNotInitialized`)).to.be.true + }) + }) }) }) From b2814c2c75039e14943b58237be5d9035bb98a48 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Wed, 26 Nov 2025 18:18:57 -0300 Subject: [PATCH 29/41] WIP: add_validator_sig --- packages/svm/package.json | 7 +- packages/svm/programs/settler/src/errors.rs | 9 ++ .../src/instructions/add_validator_sig.rs | 103 ++++++++++++++++++ .../src/instructions/add_validator_sigs.rs | 8 -- .../programs/settler/src/instructions/mod.rs | 4 +- packages/svm/programs/settler/src/lib.rs | 5 +- .../svm/programs/settler/src/state/intent.rs | 3 + .../svm/programs/settler/src/utils/mod.rs | 3 + .../svm/programs/settler/src/utils/sigs.rs | 35 ++++++ packages/svm/sdks/settler/Settler.ts | 26 +++++ packages/svm/tests/settler.test.ts | 71 +++++++++++- yarn.lock | 5 + 12 files changed, 263 insertions(+), 16 deletions(-) create mode 100644 packages/svm/programs/settler/src/instructions/add_validator_sig.rs delete mode 100644 packages/svm/programs/settler/src/instructions/add_validator_sigs.rs create mode 100644 packages/svm/programs/settler/src/utils/mod.rs create mode 100644 packages/svm/programs/settler/src/utils/sigs.rs diff --git a/packages/svm/package.json b/packages/svm/package.json index 2d06e9b..9aeca27 100644 --- a/packages/svm/package.json +++ b/packages/svm/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@coral-xyz/anchor": "^0.32.1", + "@noble/ed25519": "^3.0.0", "@solana/spl-token": "^0.4.13", "anchor-litesvm": "=0.1.0", "litesvm": "=0.1.0" @@ -22,12 +23,12 @@ "@types/mocha": "^10.0.10", "@types/node": "^22.15.18", "chai": "^5.2.0", + "eslint": "^7.9.0", + "eslint-config-mimic": "^0.0.2", "mocha": "^11.2.2", "prettier": "^2.6.2", "ts-mocha": "^10.0.0", - "typescript": "~5.5.0", - "eslint": "^7.9.0", - "eslint-config-mimic": "^0.0.2" + "typescript": "~5.5.0" }, "eslintConfig": { "extends": "eslint-config-mimic" diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index 0f907ab..54bcfd3 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -14,6 +14,9 @@ pub enum SettlerError { #[msg("Only a whitelisted validator can call this instruction")] OnlyValidator, + #[msg("Validator is not whitelisted")] + ValidatorNotWhitelisted, + #[msg("Signer must be intent creator")] IncorrectIntentCreator, @@ -49,4 +52,10 @@ pub enum SettlerError { #[msg("Intent has insufficient validations")] InsufficientIntentValidations, + + #[msg("Signature verification failed")] + SigVerificationFailed, + + #[msg("Math Error")] + MathError, } diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs new file mode 100644 index 0000000..1ccdd24 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs @@ -0,0 +1,103 @@ +use anchor_lang::prelude::{ + instruction::Instruction, sysvar::instructions::get_instruction_relative, *, +}; + +use crate::{ + errors::SettlerError, + state::Intent, + utils::{check_ed25519_ix, get_args_from_ed25519_ix_data, Ed25519Args}, + whitelist::{ + accounts::EntityRegistry, + types::{EntityType, WhitelistStatus}, + }, +}; +use anchor_lang::solana_program::sysvar::instructions::{load_instruction_at_checked, ID as IX_ID}; + +#[derive(Accounts)] +pub struct AddValidatorSig<'info> { + #[account(mut)] + pub solver: Signer<'info>, + + #[account( + seeds = [b"entity-registry", &[EntityType::Solver as u8 + 1], solver.key().as_ref()], + bump = solver_registry.bump, + seeds::program = crate::whitelist::ID, + constraint = + solver_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::OnlySolver + )] + pub solver_registry: Box>, + + // Any Intent + #[account( + mut, + constraint = intent.is_final @ SettlerError::IntentIsNotFinal + )] + pub intent: Box>, + + #[account( + seeds = [b"fulfilled-intent", intent.intent_hash.as_ref()], + bump + )] + /// This PDA must be uninitialized + pub fulfilled_intent: SystemAccount<'info>, + + #[account( + constraint = + validator_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::ValidatorNotWhitelisted + )] + pub validator_registry: Box>, + + /// CHECK: The address check is needed because otherwise + /// the supplied Sysvar could be anything else. + #[account(address = IX_ID)] + pub ix_sysvar: AccountInfo<'info>, +} + +pub fn add_validator_sig(ctx: Context) -> Result<()> { + // Verify Intent is not expired + let now = Clock::get()?.unix_timestamp as u64; + let intent = &mut ctx.accounts.intent; + + require!(intent.deadline > now, SettlerError::IntentIsExpired,); + + // Get Ed25519 instruction + let ed25519_ix: Instruction = get_instruction_relative(-1, &ctx.accounts.ix_sysvar)?; + let ed25519_ix_args: Ed25519Args = get_args_from_ed25519_ix_data(&ed25519_ix.data)?; + + // Verify correct program and accounts + check_ed25519_ix(&ed25519_ix)?; + + // Verify correct message was signed + if ed25519_ix_args.msg != intent.intent_hash { + return err!(SettlerError::SigVerificationFailed); + } + + // Verify pubkey is a whitelisted Validator + require_keys_eq!( + ctx.accounts.validator_registry.key(), + Pubkey::create_program_address( + &[ + b"entity-registry", + &[EntityType::Validator as u8 + 1], + ed25519_ix_args.pubkey, + &[ctx.accounts.validator_registry.bump] + ], + &crate::whitelist::ID, + ) + .map_err(|_| SettlerError::ValidatorNotWhitelisted)?, + SettlerError::ValidatorNotWhitelisted, + ); + + // Updates intent PDA + // TODO: add step if intent.validators is at max size (realloc? return?) + if intent.validators.contains(ed25519_ix_args.pubkey) { + return Ok(()); + } + intent.validations = intent + .validations + .checked_add(1) + .ok_or(SettlerError::MathError)?; + intent.validators.push(*ed25519_ix_args.pubkey); + + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs b/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs deleted file mode 100644 index 8737d49..0000000 --- a/packages/svm/programs/settler/src/instructions/add_validator_sigs.rs +++ /dev/null @@ -1,8 +0,0 @@ -use anchor_lang::prelude::*; - -#[derive(Accounts)] -pub struct AddValidatorSigs {} - -pub fn add_validator_sigs(ctx: Context) -> Result<()> { - Ok(()) -} diff --git a/packages/svm/programs/settler/src/instructions/mod.rs b/packages/svm/programs/settler/src/instructions/mod.rs index 1c45098..f335e7a 100644 --- a/packages/svm/programs/settler/src/instructions/mod.rs +++ b/packages/svm/programs/settler/src/instructions/mod.rs @@ -1,6 +1,6 @@ pub mod add_axia_sig; pub mod add_instructions_to_proposal; -pub mod add_validator_sigs; +pub mod add_validator_sig; pub mod change_whitelist_program; pub mod claim_stale_intent; pub mod claim_stale_proposal; @@ -13,7 +13,7 @@ pub mod set_paused_state; pub use add_axia_sig::*; pub use add_instructions_to_proposal::*; -pub use add_validator_sigs::*; +pub use add_validator_sig::*; pub use change_whitelist_program::*; pub use claim_stale_intent::*; pub use claim_stale_proposal::*; diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index 93993fb..6aba829 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -8,6 +8,7 @@ pub mod errors; pub mod instructions; pub mod state; pub mod types; +pub mod utils; use crate::{instructions::*, state::*, types::*}; @@ -27,8 +28,8 @@ pub mod settler { instructions::add_instructions_to_proposal(ctx, more_instructions, finalize) } - pub fn add_validator_sigs(ctx: Context) -> Result<()> { - instructions::add_validator_sigs(ctx) + pub fn add_validator_sig(ctx: Context) -> Result<()> { + instructions::add_validator_sig(ctx) } pub fn change_whitelist_program(ctx: Context) -> Result<()> { diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs index 2b7bdd1..f93f71d 100644 --- a/packages/svm/programs/settler/src/state/intent.rs +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -12,6 +12,8 @@ pub struct Intent { pub deadline: u64, pub min_validations: u16, pub validations: u16, + // max 10 + pub validators: Vec<[u8; 32]>, // TODO: how to store more efficiently? how to know max beforehand? is min enough? pub is_final: bool, pub intent_data: Vec, pub max_fees: Vec, @@ -30,6 +32,7 @@ impl Intent { 8 + // deadline 2 + // min_validations 2 + // validations + 4 + 32 * 10 + // validators // TODO: rethink 1 + // is_final 1 // bump ; diff --git a/packages/svm/programs/settler/src/utils/mod.rs b/packages/svm/programs/settler/src/utils/mod.rs new file mode 100644 index 0000000..87c9682 --- /dev/null +++ b/packages/svm/programs/settler/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod sigs; + +pub use sigs::*; diff --git a/packages/svm/programs/settler/src/utils/sigs.rs b/packages/svm/programs/settler/src/utils/sigs.rs new file mode 100644 index 0000000..664a57c --- /dev/null +++ b/packages/svm/programs/settler/src/utils/sigs.rs @@ -0,0 +1,35 @@ +use anchor_lang::prelude::{instruction::Instruction, *}; + +use crate::errors::SettlerError; + +pub fn check_ed25519_ix(ix: &Instruction) -> Result<()> { + if ix.program_id.to_string() != "Ed25519SigVerify111111111111111111111111111" + || ix.accounts.len() != 0 + { + return err!(SettlerError::SigVerificationFailed); + } + + Ok(()) +} + +pub struct Ed25519Args<'a> { + pub pubkey: &'a [u8; 32], + pub sig: &'a [u8; 64], + pub msg: &'a [u8], +} + +pub fn get_args_from_ed25519_ix_data(data: &[u8]) -> Result> { + if data.len() < 112 { + return err!(SettlerError::SigVerificationFailed); + } + + let pubkey = data[16..16 + 32] + .try_into() + .map_err(|_| SettlerError::SigVerificationFailed)?; + let sig = data[48..48 + 64] + .try_into() + .map_err(|_| SettlerError::SigVerificationFailed)?; + let msg = &data[112..]; + + Ok(Ed25519Args { pubkey, sig, msg }) +} diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index 2a3116c..d80618e 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -173,6 +173,32 @@ export default class SettlerSDK { return ix } + async addValidatorSigIxs( + intent: web3.PublicKey, + intentHash: Buffer, + validator: web3.PublicKey, + signature: number[] + ): Promise { + const ed25519Ix = web3.Ed25519Program.createInstructionWithPublicKey({ + message: intentHash, + publicKey: validator.toBuffer(), + signature: Buffer.from(signature), + }) + + const ix = await this.program.methods + .addValidatorSig() + .accountsPartial({ + solver: this.getSignerKey(), + solverRegistry: this.getEntityRegistryPubkey(EntityType.Solver, this.getSignerKey()), + intent, + validatorRegistry: this.getEntityRegistryPubkey(EntityType.Validator, validator), + ixSysvar: web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + return [ed25519Ix, ix] + } + getSettlerSettingsPubkey(): web3.PublicKey { return web3.PublicKey.findProgramAddressSync([Buffer.from('settler-settings')], this.program.programId)[0] } diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index a62b198..66bfa18 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Program, Wallet } from '@coral-xyz/anchor' +import { signAsync } from '@noble/ed25519' import { Keypair } from '@solana/web3.js' import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' @@ -639,7 +640,7 @@ describe('Settler Program', () => { expect(intent.maxFees.length).to.be.eq(58) expect(intent.events.length).to.be.eq(51) expect(intent.isFinal).to.be.false - expect(intentAcc?.data.length).to.be.eq(19325) + expect(intentAcc?.data.length).to.be.eq(19649) }) it('should finalize an intent', async () => { @@ -1832,5 +1833,73 @@ describe('Settler Program', () => { expect(errorMsg.includes(`AccountNotInitialized`)).to.be.true }) }) + + describe('add_validator_sigs', () => { + const generateIntentHash = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const generateNonce = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + it('debug', async () => { + const intentHash = generateIntentHash() + const intentKey = sdk.getIntentKey(intentHash) + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], + eventsHex: [ + { + topicHex: Buffer.from(Array(32).fill(1)).toString('hex'), + dataHex: '040506', + }, + ], + } + + const createIntentIx = await solverSdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(solverProvider, createIntentIx) + + const validator = Keypair.generate() + + const whitelistValidatorIx = await whitelistSdk.setEntityWhitelistStatusIx( + EntityType.Validator, + validator.publicKey, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(provider, whitelistValidatorIx) + + // New stuff + + const signature = await signAsync(Buffer.from(intentHash, 'hex'), validator.secretKey.slice(0, 32)) + const sigBytes: Uint8Array = new Uint8Array(signature) + const sigNums: number[] = Array.from(sigBytes) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator.publicKey, + sigNums + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + console.log(res.toString()) + }) + }) }) }) diff --git a/yarn.lock b/yarn.lock index 154406d..c0512d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -526,6 +526,11 @@ dependencies: "@noble/hashes" "1.7.2" +"@noble/ed25519@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-3.0.0.tgz#720d4cdb6b5f632e29164a7e9d5cdfeb82a7ac86" + integrity sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg== + "@noble/hashes@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" From 8c4aefedf659f53dd074567851291d36c8bad582 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 27 Nov 2025 16:17:24 -0300 Subject: [PATCH 30/41] WIP: add_validator_sig pt. 2 and checked_math --- .../src/instructions/add_validator_sig.rs | 16 +++-- .../settler/src/instructions/create_intent.rs | 7 ++- .../settler/src/instructions/extend_intent.rs | 2 +- packages/svm/programs/settler/src/lib.rs | 4 +- .../svm/programs/settler/src/state/intent.rs | 60 +++++++++++++------ .../svm/programs/settler/src/utils/math.rs | 17 ++++++ .../svm/programs/settler/src/utils/mod.rs | 2 + packages/svm/sdks/settler/Settler.ts | 2 +- packages/svm/tests/settler.test.ts | 3 +- 9 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 packages/svm/programs/settler/src/utils/math.rs diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs index 1ccdd24..7415b5d 100644 --- a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs +++ b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs @@ -1,5 +1,6 @@ -use anchor_lang::prelude::{ - instruction::Instruction, sysvar::instructions::get_instruction_relative, *, +use anchor_lang::{ + prelude::{instruction::Instruction, sysvar::instructions::get_instruction_relative, *}, + solana_program::sysvar::instructions::ID as IX_ID, }; use crate::{ @@ -11,7 +12,6 @@ use crate::{ types::{EntityType, WhitelistStatus}, }, }; -use anchor_lang::solana_program::sysvar::instructions::{load_instruction_at_checked, ID as IX_ID}; #[derive(Accounts)] pub struct AddValidatorSig<'info> { @@ -88,15 +88,21 @@ pub fn add_validator_sig(ctx: Context) -> Result<()> { SettlerError::ValidatorNotWhitelisted, ); - // Updates intent PDA - // TODO: add step if intent.validators is at max size (realloc? return?) + // Updates intent PDA if signature not present and min_validations not met + + if intent.validators.len() == intent.min_validations as usize { + return Ok(()); + } + if intent.validators.contains(ed25519_ix_args.pubkey) { return Ok(()); } + intent.validations = intent .validations .checked_add(1) .ok_or(SettlerError::MathError)?; + intent.validators.push(*ed25519_ix_args.pubkey); Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index 2da26a6..c6f3cec 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -12,7 +12,7 @@ use crate::{ #[derive(Accounts)] // TODO: can we optimize this deser? we just need the three Vec for their length -#[instruction(intent_hash: [u8; 32], data: Vec, max_fees: Vec, events: Vec)] +#[instruction(intent_hash: [u8; 32], data: Vec, max_fees: Vec, events: Vec, min_validations: u16)] pub struct CreateIntent<'info> { #[account(mut)] pub solver: Signer<'info>, @@ -31,7 +31,7 @@ pub struct CreateIntent<'info> { seeds = [b"intent", intent_hash.as_ref()], bump, payer = solver, - space = 8 + Intent::BASE_LEN + Intent::data_size(data.len()) + Intent::max_fees_size(max_fees.len()) + Intent::events_size(&events) + space = Intent::total_size(data.len(), max_fees.len(), &events, min_validations)? )] // TODO: change to AccountLoader? // TODO: init within the handler body to save compute? @@ -53,11 +53,11 @@ pub fn create_intent( data: Vec, max_fees: Vec, events: Vec, + min_validations: u16, op: OpType, user: Pubkey, nonce: [u8; 32], deadline: u64, - min_validations: u16, is_final: bool, ) -> Result<()> { let now = Clock::get()?.unix_timestamp as u64; @@ -79,6 +79,7 @@ pub fn create_intent( intent.intent_data = data; intent.max_fees = max_fees; intent.events = events; + intent.validators = vec![]; intent.bump = ctx.bumps.intent; Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/extend_intent.rs b/packages/svm/programs/settler/src/instructions/extend_intent.rs index ab74148..2d34360 100644 --- a/packages/svm/programs/settler/src/instructions/extend_intent.rs +++ b/packages/svm/programs/settler/src/instructions/extend_intent.rs @@ -17,7 +17,7 @@ pub struct ExtendIntent<'info> { has_one = intent_creator @ SettlerError::IncorrectIntentCreator, constraint = !intent.is_final @ SettlerError::IntentIsFinal, realloc = - Intent::extended_size(intent.to_account_info().data_len(), &more_data, &more_max_fees, &more_events), + Intent::extended_size(intent.to_account_info().data_len(), &more_data, &more_max_fees, &more_events)?, realloc::payer = intent_creator, realloc::zero = true )] diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index 6aba829..fc6db33 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -52,11 +52,11 @@ pub mod settler { data: Vec, max_fees: Vec, events: Vec, + min_validations: u16, op: OpType, user: Pubkey, nonce: [u8; 32], deadline: u64, - min_validations: u16, is_final: bool, ) -> Result<()> { instructions::create_intent( @@ -65,11 +65,11 @@ pub mod settler { data, max_fees, events, + min_validations, op, user, nonce, deadline, - min_validations, is_final, ) } diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs index f93f71d..5f88eaf 100644 --- a/packages/svm/programs/settler/src/state/intent.rs +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -1,6 +1,9 @@ use anchor_lang::prelude::*; -use crate::types::{IntentEvent, MaxFee, OpType}; +use crate::{ + types::{IntentEvent, MaxFee, OpType}, + utils::{add, sub, mul}, +}; #[account] pub struct Intent { @@ -12,9 +15,8 @@ pub struct Intent { pub deadline: u64, pub min_validations: u16, pub validations: u16, - // max 10 - pub validators: Vec<[u8; 32]>, // TODO: how to store more efficiently? how to know max beforehand? is min enough? pub is_final: bool, + pub validators: Vec<[u8; 32]>, // TODO: how to store more efficiently? pub intent_data: Vec, pub max_fees: Vec, pub events: Vec, @@ -32,41 +34,61 @@ impl Intent { 8 + // deadline 2 + // min_validations 2 + // validations - 4 + 32 * 10 + // validators // TODO: rethink 1 + // is_final 1 // bump ; - pub fn data_size(len: usize) -> usize { - 4 + len + pub fn total_size( + data_len: usize, + max_fees_len: usize, + events: &[IntentEvent], + min_validations: u16, + ) -> Result { + let size = add(8, Intent::BASE_LEN)?; + let size = add(size, Intent::data_size(data_len)?)?; + let size = add(size, Intent::max_fees_size(max_fees_len)?)?; + let size = add(size, Intent::events_size(events)?)?; + let size = add(size, Intent::validators_size(min_validations)?)?; + Ok(size) } - pub fn max_fees_size(len: usize) -> usize { - 4 + MaxFee::INIT_SPACE * len + pub fn data_size(len: usize) -> Result { + add(4, len) } - pub fn events_size(events: &Vec) -> usize { - 4 + events.iter().map(|event| event.size()).sum::() + pub fn max_fees_size(len: usize) -> Result { + add(4, mul(MaxFee::INIT_SPACE, len)?) + } + + pub fn events_size(events: &[IntentEvent]) -> Result { + let sum = events.iter().try_fold(0usize, |acc, e| add(acc, e.size()))?; + add(4, sum) + } + + pub fn validators_size(min_validations: u16) -> Result { + add(4, mul(min_validations as usize, 32)?) } pub fn extended_size( - mut size: usize, + size: usize, more_data: &Option>, more_max_fees: &Option>, more_events: &Option>, - ) -> usize { - if let Some(_more_data) = more_data { - size += Intent::data_size(_more_data.len()) - 4; + ) -> Result { + let mut size = size; + + if let Some(v) = more_data { + size = add(size, sub(Intent::data_size(v.len())?, 4)?)?; } - if let Some(_more_max_fees) = more_max_fees { - size += Intent::max_fees_size(_more_max_fees.len()) - 4; + if let Some(v) = more_max_fees { + size = add(size, sub(Intent::max_fees_size(v.len())?, 4)?)?; } - if let Some(_more_events) = more_events { - size += Intent::events_size(&_more_events) - 4; + if let Some(v) = more_events { + size = add(size, sub(Intent::events_size(v)?, 4)?)?; } - size + Ok(size) } } diff --git a/packages/svm/programs/settler/src/utils/math.rs b/packages/svm/programs/settler/src/utils/math.rs new file mode 100644 index 0000000..cdbb179 --- /dev/null +++ b/packages/svm/programs/settler/src/utils/math.rs @@ -0,0 +1,17 @@ +use anchor_lang::prelude::*; +use crate::errors::SettlerError; + +#[inline] +pub fn add(a: usize, b: usize) -> Result { + Ok(a.checked_add(b).ok_or(SettlerError::MathError)?) +} + +#[inline] +pub fn sub(a: usize, b: usize) -> Result { + Ok(a.checked_sub(b).ok_or(SettlerError::MathError)?) +} + +#[inline] +pub fn mul(a: usize, b: usize) -> Result { + Ok(a.checked_mul(b).ok_or(SettlerError::MathError)?) +} diff --git a/packages/svm/programs/settler/src/utils/mod.rs b/packages/svm/programs/settler/src/utils/mod.rs index 87c9682..bd9ec41 100644 --- a/packages/svm/programs/settler/src/utils/mod.rs +++ b/packages/svm/programs/settler/src/utils/mod.rs @@ -1,3 +1,5 @@ +pub mod math; pub mod sigs; +pub use math::*; pub use sigs::*; diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index d80618e..8d74ea8 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -61,11 +61,11 @@ export default class SettlerSDK { data, maxFeesBn, events, + minValidations, this.opTypeToAnchorEnum(op), user, nonce, new BN(deadline), - minValidations, isFinal ) .accountsPartial({ diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 66bfa18..2265f68 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -156,6 +156,7 @@ describe('Settler Program', () => { expect(intent.maxFees[0].mint.toString()).to.be.eq(params.maxFees[0].mint.toString()) expect(intent.maxFees[0].amount.toNumber()).to.be.eq(1000) expect(intent.events.length).to.be.eq(1) + expect(intent.validators.length).to.be.eq(0) expect(Buffer.from(intent.events[0].topic).toString('hex')).to.be.eq(params.eventsHex[0].topicHex) expect(Buffer.from(intent.events[0].data).toString('hex')).to.be.eq('040506') }) @@ -640,7 +641,7 @@ describe('Settler Program', () => { expect(intent.maxFees.length).to.be.eq(58) expect(intent.events.length).to.be.eq(51) expect(intent.isFinal).to.be.false - expect(intentAcc?.data.length).to.be.eq(19649) + expect(intentAcc?.data.length).to.be.eq(19361) }) it('should finalize an intent', async () => { From 18b6560d8dc57878b1a8cddfd5c893c9ba4d90ce Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 27 Nov 2025 17:28:07 -0300 Subject: [PATCH 31/41] add_validator_sig finished and tests added --- .../src/instructions/add_validator_sig.rs | 8 +- .../svm/programs/settler/src/state/intent.rs | 8 +- .../svm/programs/settler/src/utils/math.rs | 2 +- packages/svm/tests/settler.test.ts | 397 ++++++++++++++++-- 4 files changed, 382 insertions(+), 33 deletions(-) diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs index 7415b5d..2555545 100644 --- a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs +++ b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs @@ -94,7 +94,9 @@ pub fn add_validator_sig(ctx: Context) -> Result<()> { return Ok(()); } - if intent.validators.contains(ed25519_ix_args.pubkey) { + let ed25519_pubkey = Pubkey::try_from_slice(ed25519_ix_args.pubkey)?; + + if intent.validators.contains(&ed25519_pubkey) { return Ok(()); } @@ -102,8 +104,8 @@ pub fn add_validator_sig(ctx: Context) -> Result<()> { .validations .checked_add(1) .ok_or(SettlerError::MathError)?; - - intent.validators.push(*ed25519_ix_args.pubkey); + + intent.validators.push(ed25519_pubkey); Ok(()) } diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs index 5f88eaf..bfd308f 100644 --- a/packages/svm/programs/settler/src/state/intent.rs +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use crate::{ types::{IntentEvent, MaxFee, OpType}, - utils::{add, sub, mul}, + utils::{add, mul, sub}, }; #[account] @@ -16,7 +16,7 @@ pub struct Intent { pub min_validations: u16, pub validations: u16, pub is_final: bool, - pub validators: Vec<[u8; 32]>, // TODO: how to store more efficiently? + pub validators: Vec, // TODO: how to store more efficiently? pub intent_data: Vec, pub max_fees: Vec, pub events: Vec, @@ -61,7 +61,9 @@ impl Intent { } pub fn events_size(events: &[IntentEvent]) -> Result { - let sum = events.iter().try_fold(0usize, |acc, e| add(acc, e.size()))?; + let sum = events + .iter() + .try_fold(0usize, |acc, e| add(acc, e.size()))?; add(4, sum) } diff --git a/packages/svm/programs/settler/src/utils/math.rs b/packages/svm/programs/settler/src/utils/math.rs index cdbb179..e48c97a 100644 --- a/packages/svm/programs/settler/src/utils/math.rs +++ b/packages/svm/programs/settler/src/utils/math.rs @@ -1,5 +1,5 @@ -use anchor_lang::prelude::*; use crate::errors::SettlerError; +use anchor_lang::prelude::*; #[inline] pub fn add(a: usize, b: usize) -> Result { diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 2265f68..bcf0d25 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -3,7 +3,7 @@ import { Program, Wallet } from '@coral-xyz/anchor' import { signAsync } from '@noble/ed25519' -import { Keypair } from '@solana/web3.js' +import { Ed25519Program, Keypair, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js' import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' import fs from 'fs' @@ -45,7 +45,7 @@ describe('Settler Program', () => { malicious = Keypair.generate() solver = Keypair.generate() - client = fromWorkspace(path.join(__dirname, '../')).withBuiltins() + client = fromWorkspace(path.join(__dirname, '../')).withBuiltins().withPrecompiles().withSysvars() provider = new LiteSVMProvider(client, new Wallet(admin)) maliciousProvider = new LiteSVMProvider(client, new Wallet(malicious)) @@ -1836,6 +1836,18 @@ describe('Settler Program', () => { }) describe('add_validator_sigs', () => { + let whitelistedValidator: Keypair + + before(async () => { + whitelistedValidator = Keypair.generate() + const whitelistValidatorIx = await whitelistSdk.setEntityWhitelistStatusIx( + EntityType.Validator, + whitelistedValidator.publicKey, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(provider, whitelistValidatorIx) + }) + const generateIntentHash = (): string => { return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') } @@ -1844,9 +1856,8 @@ describe('Settler Program', () => { return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') } - it('debug', async () => { + const createFinalizedIntent = async (minValidations = 1, isFinal = true): Promise => { const intentHash = generateIntentHash() - const intentKey = sdk.getIntentKey(intentHash) const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) @@ -1857,49 +1868,383 @@ describe('Settler Program', () => { user, nonceHex: nonce, deadline, - minValidations: 1, + minValidations, dataHex: '010203', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], - eventsHex: [ - { - topicHex: Buffer.from(Array(32).fill(1)).toString('hex'), - dataHex: '040506', - }, - ], + maxFees: [], + eventsHex: [], } - const createIntentIx = await solverSdk.createIntentIx(intentHash, params, true) - await makeTxSignAndSend(solverProvider, createIntentIx) + const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) + await makeTxSignAndSend(solverProvider, ix) - const validator = Keypair.generate() + return intentHash + } + const createWhitelistedValidator = async (): Promise => { + const validator = Keypair.generate() const whitelistValidatorIx = await whitelistSdk.setEntityWhitelistStatusIx( EntityType.Validator, validator.publicKey, WhitelistStatus.Whitelisted ) await makeTxSignAndSend(provider, whitelistValidatorIx) + return validator + } - // New stuff - + const createSignature = async (intentHash: string, validator: Keypair): Promise => { const signature = await signAsync(Buffer.from(intentHash, 'hex'), validator.secretKey.slice(0, 32)) - const sigBytes: Uint8Array = new Uint8Array(signature) - const sigNums: number[] = Array.from(sigBytes) + return Array.from(new Uint8Array(signature)) + } + + it('should add validator signature successfully', async () => { + const intentHash = await createFinalizedIntent(1, true) + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createSignature(intentHash, whitelistedValidator) + + const intentBefore = await program.account.intent.fetch(intentKey) + expect(intentBefore.validations).to.be.eq(0) + expect(intentBefore.validators.length).to.be.eq(0) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + await makeTxSignAndSend(solverProvider, ...ixs) + + const intentAfter = await program.account.intent.fetch(intentKey) + expect(intentAfter.validations).to.be.eq(1) + expect(intentAfter.validators.length).to.be.eq(1) + expect(intentAfter.validators[0].toString()).to.be.eq(whitelistedValidator.publicKey.toString()) + }) + + it('should add multiple validator signatures', async () => { + const intentHash = await createFinalizedIntent(3, true) + const intentKey = sdk.getIntentKey(intentHash) + + const validator1 = await createWhitelistedValidator() + const validator2 = await createWhitelistedValidator() + const validator3 = await createWhitelistedValidator() + + const signature1 = await createSignature(intentHash, validator1) + const ixs1 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator1.publicKey, + signature1 + ) + await makeTxSignAndSend(solverProvider, ...ixs1) + + const intentAfter1 = await program.account.intent.fetch(intentKey) + expect(intentAfter1.validations).to.be.eq(1) + expect(intentAfter1.validators.length).to.be.eq(1) + + const signature2 = await createSignature(intentHash, validator2) + const ixs2 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator2.publicKey, + signature2 + ) + await makeTxSignAndSend(solverProvider, ...ixs2) + + const intentAfter2 = await program.account.intent.fetch(intentKey) + expect(intentAfter2.validations).to.be.eq(2) + expect(intentAfter2.validators.length).to.be.eq(2) + + const signature3 = await createSignature(intentHash, validator3) + const ixs3 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator3.publicKey, + signature3 + ) + await makeTxSignAndSend(solverProvider, ...ixs3) + + const intentAfter3 = await program.account.intent.fetch(intentKey) + expect(intentAfter3.validations).to.be.eq(3) + expect(intentAfter3.validators.length).to.be.eq(3) + expect(intentAfter3.validators.map((v) => v.toString())).to.include(validator1.publicKey.toString()) + expect(intentAfter3.validators.map((v) => v.toString())).to.include(validator2.publicKey.toString()) + expect(intentAfter3.validators.map((v) => v.toString())).to.include(validator3.publicKey.toString()) + }) + + it('should handle duplicate validator signature gracefully', async () => { + const intentHash = await createFinalizedIntent(2, true) + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createSignature(intentHash, whitelistedValidator) + + const ixs1 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + await makeTxSignAndSend(solverProvider, ...ixs1) + + const intentAfter1 = await program.account.intent.fetch(intentKey) + expect(intentAfter1.validations).to.be.eq(1) + expect(intentAfter1.validators.length).to.be.eq(1) + + client.expireBlockhash() + const ixs2 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + await makeTxSignAndSend(solverProvider, ...ixs2) + + const intentAfter2 = await program.account.intent.fetch(intentKey) + expect(intentAfter2.validations).to.be.eq(1) + expect(intentAfter2.validators.length).to.be.eq(1) + }) + + it('should handle when min_validations already met', async () => { + const intentHash = await createFinalizedIntent(1, true) + const intentKey = sdk.getIntentKey(intentHash) + + const validator1 = await createWhitelistedValidator() + const validator2 = await createWhitelistedValidator() + + const signature1 = await createSignature(intentHash, validator1) + const ixs1 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator1.publicKey, + signature1 + ) + await makeTxSignAndSend(solverProvider, ...ixs1) + + const intentAfter1 = await program.account.intent.fetch(intentKey) + expect(intentAfter1.validations).to.be.eq(1) + expect(intentAfter1.validators.length).to.be.eq(1) + expect(intentAfter1.minValidations).to.be.eq(1) + + client.expireBlockhash() + const signature2 = await createSignature(intentHash, validator2) + const ixs2 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator2.publicKey, + signature2 + ) + await makeTxSignAndSend(solverProvider, ...ixs2) + + const intentAfter2 = await program.account.intent.fetch(intentKey) + expect(intentAfter2.validations).to.be.eq(1) + expect(intentAfter2.validators.length).to.be.eq(1) + expect(intentAfter2.validators[0].toString()).to.be.eq(validator1.publicKey.toString()) + }) + + it('cant add signature if intent is not final', async () => { + const intentHash = await createFinalizedIntent(1, false) + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createSignature(intentHash, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include(`Intent is not final`) + }) + + it('cant add signature if intent has expired', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 100 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(solverProvider, ix) + + const intentKey = sdk.getIntentKey(intentHash) + + warpSeconds(provider, 101) + + const signature = await createSignature(intentHash, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include(`Intent has already expired`) + }) + + it('cant add signature if validator is not whitelisted', async () => { + const intentHash = await createFinalizedIntent(1, true) + const intentKey = sdk.getIntentKey(intentHash) + + const validator = Keypair.generate() + + const signature = await createSignature(intentHash, validator) const ixs = await solverSdk.addValidatorSigIxs( intentKey, Buffer.from(intentHash, 'hex'), validator.publicKey, - sigNums + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include( + `AnchorError caused by account: validator_registry. Error Code: AccountNotInitialized.` + ) + }) + + it('cant add signature if solver is not whitelisted', async () => { + const intentHash = await createFinalizedIntent(1, true) + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createSignature(intentHash, whitelistedValidator) + + const ixs = await maliciousSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(maliciousProvider, ...ixs) + + expect(res.toString()).to.include( + `AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized.` + ) + }) + + it('cant add signature for non-existent intent', async () => { + const intentHash = generateIntentHash() + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createSignature(intentHash, whitelistedValidator) + + const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({ + message: Buffer.from(intentHash, 'hex'), + publicKey: whitelistedValidator.publicKey.toBuffer(), + signature: Buffer.from(signature), + }) + + const ix = await program.methods + .addValidatorSig() + .accountsPartial({ + solver: solverSdk.getSignerKey(), + solverRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Solver, solverSdk.getSignerKey()), + intent: intentKey, + fulfilledIntent: solverSdk.getFulfilledIntentKey(intentHash), + validatorRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Validator, whitelistedValidator.publicKey), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) + + expect(res.toString()).to.include(`AccountNotInitialized`) + }) + + it('cant add signature if fulfilled_intent PDA already exists', async () => { + const intentHash = await createFinalizedIntent(1, true) + const intentKey = sdk.getIntentKey(intentHash) + + const fulfilledIntent = sdk.getFulfilledIntentKey(intentHash) + client.setAccount(fulfilledIntent, { + executable: false, + lamports: 1002240, + owner: program.programId, + data: Buffer.from('595168911b9267f7' + '010000000000000000', 'hex'), + }) + + const signature = await createSignature(intentHash, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include( + `AnchorError caused by account: fulfilled_intent. Error Code: AccountNotSystemOwned. Error Number: 3011. Error Message: The given account is not owned by the system program` + ) + }) + + it('cant add signature with wrong intent hash', async () => { + const intentHash = await createFinalizedIntent(1, true) + const intentKey = sdk.getIntentKey(intentHash) + + const wrongIntentHash = generateIntentHash() + const signature = await createSignature(wrongIntentHash, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(wrongIntentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include(`Signature verification failed`) + }) + + it('cant add signature with invalid signature', async () => { + const intentHash = await createFinalizedIntent(1, true) + const intentKey = sdk.getIntentKey(intentHash) + + const invalidSignature: number[] = Array.from({ length: 64 }, () => Math.floor(Math.random() * 256)) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + invalidSignature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.be.eq( + `FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, Custom(2)), meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('cant add valid signature but for another intent', async () => { + const intentHash1 = await createFinalizedIntent(1, true) + const intentKey1 = sdk.getIntentKey(intentHash1) + + const intentHash2 = await createFinalizedIntent(1, true) + + const signature = await createSignature(intentHash2, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey1, + Buffer.from(intentHash2, 'hex'), + whitelistedValidator.publicKey, + signature ) const res = await makeTxSignAndSend(solverProvider, ...ixs) - console.log(res.toString()) + expect(res.toString()).to.include(`Signature verification failed`) }) }) }) From 6d7b6894dc5514de21d848b99d43021ae0d63367 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 11 Dec 2025 13:00:43 -0300 Subject: [PATCH 32/41] Implement add_axia_sig --- packages/svm/programs/settler/src/errors.rs | 7 +- .../settler/src/instructions/add_axia_sig.rs | 80 ++++++++++++++++++- .../src/instructions/add_validator_sig.rs | 2 +- .../src/instructions/create_proposal.rs | 1 + .../programs/settler/src/state/proposal.rs | 2 + 5 files changed, 87 insertions(+), 5 deletions(-) diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index 54bcfd3..34360b1 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -8,8 +8,8 @@ pub enum SettlerError { #[msg("Only a whitelisted solver can call this instruction")] OnlySolver, - #[msg("Only a whitelisted Axia address can call this instruction")] - OnlyAxia, + #[msg("Provided Axia address is not whitelisted")] + AxiaNotWhitelisted, #[msg("Only a whitelisted validator can call this instruction")] OnlyValidator, @@ -32,6 +32,9 @@ pub enum SettlerError { #[msg("Proposal is already final")] ProposalIsFinal, + #[msg("Proposal is not final")] + ProposalIsNotFinal, + #[msg("Intent not yet expired. Please wait for the deadline to pass")] IntentNotYetExpired, diff --git a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs index 2a626c7..a72d331 100644 --- a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs +++ b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs @@ -1,8 +1,84 @@ -use anchor_lang::prelude::*; +use anchor_lang::{ + prelude::{instruction::Instruction, sysvar::instructions::get_instruction_relative, *}, + solana_program::sysvar::instructions::ID as IX_ID, +}; + +use crate::{ + errors::SettlerError, + state::Proposal, + utils::{check_ed25519_ix, get_args_from_ed25519_ix_data, Ed25519Args}, + whitelist::{ + accounts::EntityRegistry, + types::{EntityType, WhitelistStatus}, + }, +}; #[derive(Accounts)] -pub struct AddAxiaSig {} +pub struct AddAxiaSig<'info> { + #[account(mut)] + pub solver: Signer<'info>, + + #[account( + seeds = [b"entity-registry", &[EntityType::Solver as u8 + 1], solver.key().as_ref()], + bump = solver_registry.bump, + seeds::program = crate::whitelist::ID, + constraint = + solver_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::OnlySolver + )] + pub solver_registry: Box>, + + #[account( + seeds = [b"entity-registry", &[EntityType::Axia as u8 + 1], axia_registry.entity_pubkey.as_ref()], + bump = axia_registry.bump, + seeds::program = crate::whitelist::ID, + constraint = + axia_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::AxiaNotWhitelisted, + constraint = axia_registry.entity_type as u8 == EntityType::Axia as u8 @ SettlerError::AxiaNotWhitelisted, + )] + pub axia_registry: Box>, + + /// CHECK: Any proposal + #[account(mut)] + pub proposal: Box>, + + /// CHECK: The address check is needed because otherwise + /// the supplied Sysvar could be anything else. + #[account(address = IX_ID)] + pub ix_sysvar: AccountInfo<'info>, +} pub fn add_axia_sig(ctx: Context) -> Result<()> { + let proposal = &mut ctx.accounts.proposal; + + // NOP if already signed + if proposal.is_signed { + return Ok(()); + } + + let now = Clock::get()?.unix_timestamp as u64; + + require!(proposal.deadline > now, SettlerError::ProposalIsExpired); + require!(proposal.is_final, SettlerError::ProposalIsNotFinal); + + // Get Ed25519 instruction + let ed25519_ix: Instruction = get_instruction_relative(-1, &ctx.accounts.ix_sysvar)?; + let ed25519_ix_args: Ed25519Args = get_args_from_ed25519_ix_data(&ed25519_ix.data)?; + + // Verify correct program and accounts + check_ed25519_ix(&ed25519_ix)?; + + // Verify correct message was signed + if ed25519_ix_args.msg != proposal.key().as_array() { + return err!(SettlerError::SigVerificationFailed); + } + + // Verify pubkey is whitelisted Axia + if ed25519_ix_args.pubkey != &ctx.accounts.axia_registry.entity_pubkey.to_bytes() { + return err!(SettlerError::AxiaNotWhitelisted); + } + + // Updates proposal as signed + proposal.is_signed = true; + Ok(()) } diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs index 2555545..4f5a8b3 100644 --- a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs +++ b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs @@ -58,7 +58,7 @@ pub fn add_validator_sig(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp as u64; let intent = &mut ctx.accounts.intent; - require!(intent.deadline > now, SettlerError::IntentIsExpired,); + require!(intent.deadline > now, SettlerError::IntentIsExpired); // Get Ed25519 instruction let ed25519_ix: Instruction = get_instruction_relative(-1, &ctx.accounts.ix_sysvar)?; diff --git a/packages/svm/programs/settler/src/instructions/create_proposal.rs b/packages/svm/programs/settler/src/instructions/create_proposal.rs index d7fda37..172e356 100644 --- a/packages/svm/programs/settler/src/instructions/create_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/create_proposal.rs @@ -73,6 +73,7 @@ pub fn create_proposal( proposal.proposal_creator = ctx.accounts.solver.key(); proposal.deadline = deadline; proposal.is_final = is_final; + proposal.is_signed = false; proposal.instructions = instructions; proposal.bump = ctx.bumps.proposal; diff --git a/packages/svm/programs/settler/src/state/proposal.rs b/packages/svm/programs/settler/src/state/proposal.rs index 894bc05..0baa0e7 100644 --- a/packages/svm/programs/settler/src/state/proposal.rs +++ b/packages/svm/programs/settler/src/state/proposal.rs @@ -6,6 +6,7 @@ pub struct Proposal { pub proposal_creator: Pubkey, pub deadline: u64, pub is_final: bool, + pub is_signed: bool, pub instructions: Vec, pub bump: u8, } @@ -17,6 +18,7 @@ impl Proposal { 32 + // proposal_creator 8 + // deadline 1 + // is_final + 1 + // is_signed 1 // bump ; From 10f31f17b9bdd1d2dcf7c00e8b1ad8e70008b5dc Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 11 Dec 2025 13:37:13 -0300 Subject: [PATCH 33/41] Update sigs.rs for header checks --- .../svm/programs/settler/src/utils/sigs.rs | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/svm/programs/settler/src/utils/sigs.rs b/packages/svm/programs/settler/src/utils/sigs.rs index 664a57c..8b6a8d9 100644 --- a/packages/svm/programs/settler/src/utils/sigs.rs +++ b/packages/svm/programs/settler/src/utils/sigs.rs @@ -23,13 +23,50 @@ pub fn get_args_from_ed25519_ix_data(data: &[u8]) -> Result> { return err!(SettlerError::SigVerificationFailed); } - let pubkey = data[16..16 + 32] - .try_into() - .map_err(|_| SettlerError::SigVerificationFailed)?; - let sig = data[48..48 + 64] - .try_into() - .map_err(|_| SettlerError::SigVerificationFailed)?; + // Header + let num_signatures = &[data[0]]; + let padding = &[data[1]]; + let signature_offset = &data[2..=3]; + let signature_instruction_index = &data[4..=5]; + let public_key_offset = &data[6..=7]; + let public_key_instruction_index = &data[8..=9]; + let message_data_offset = &data[10..=11]; + let message_data_size = &data[12..=13]; + let message_instruction_index = &data[14..=15]; + + // Data + let pubkey = &data[16..16 + 32]; + let sig = &data[48..48 + 64]; let msg = &data[112..]; - Ok(Ed25519Args { pubkey, sig, msg }) + // Expected values + let exp_public_key_offset: u16 = 16; // 2*u8 + 7*u16 + let exp_signature_offset: u16 = exp_public_key_offset + 32_u16; + let exp_message_data_offset: u16 = exp_signature_offset + 64_u16; + let exp_num_signatures: u8 = 1; + let exp_message_data_size: u16 = msg.len().try_into().map_err(|_| SettlerError::SigVerificationFailed)?; + + // Header + if num_signatures != &exp_num_signatures.to_le_bytes() + || padding != &[0] + || signature_offset != &exp_signature_offset.to_le_bytes() + || signature_instruction_index != &u16::MAX.to_le_bytes() + || public_key_offset != &exp_public_key_offset.to_le_bytes() + || public_key_instruction_index != &u16::MAX.to_le_bytes() + || message_data_offset != &exp_message_data_offset.to_le_bytes() + || message_data_size != &exp_message_data_size.to_le_bytes() + || message_instruction_index != &u16::MAX.to_le_bytes() + { + return err!(SettlerError::SigVerificationFailed); + } + + Ok(Ed25519Args { + pubkey: pubkey + .try_into() + .map_err(|_| SettlerError::SigVerificationFailed)?, + sig: sig + .try_into() + .map_err(|_| SettlerError::SigVerificationFailed)?, + msg, + }) } From 2f9f42af68fde98b24453e837714cf253475e69e Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 11 Dec 2025 13:55:30 -0300 Subject: [PATCH 34/41] Add tests for add_axia_sig --- packages/svm/sdks/settler/Settler.ts | 25 ++ packages/svm/tests/settler.test.ts | 420 ++++++++++++++++++++++++++- 2 files changed, 444 insertions(+), 1 deletion(-) diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index 8d74ea8..2bae38f 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -199,6 +199,31 @@ export default class SettlerSDK { return [ed25519Ix, ix] } + async addAxiaSigIxs( + proposal: web3.PublicKey, + axia: web3.PublicKey, + signature: number[] + ): Promise { + const ed25519Ix = web3.Ed25519Program.createInstructionWithPublicKey({ + message: proposal.toBuffer(), + publicKey: axia.toBuffer(), + signature: Buffer.from(signature), + }) + + const ix = await this.program.methods + .addAxiaSig() + .accountsPartial({ + solver: this.getSignerKey(), + solverRegistry: this.getEntityRegistryPubkey(EntityType.Solver, this.getSignerKey()), + proposal, + axiaRegistry: this.getEntityRegistryPubkey(EntityType.Axia, axia), + ixSysvar: web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + return [ed25519Ix, ix] + } + getSettlerSettingsPubkey(): web3.PublicKey { return web3.PublicKey.findProgramAddressSync([Buffer.from('settler-settings')], this.program.programId)[0] } diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index bcf0d25..1dd7d81 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -3,7 +3,7 @@ import { Program, Wallet } from '@coral-xyz/anchor' import { signAsync } from '@noble/ed25519' -import { Ed25519Program, Keypair, SYSVAR_INSTRUCTIONS_PUBKEY } from '@solana/web3.js' +import { Ed25519Program, Keypair, PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY, TransactionInstruction } from '@solana/web3.js' import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' import fs from 'fs' @@ -2247,5 +2247,423 @@ describe('Settler Program', () => { expect(res.toString()).to.include(`Signature verification failed`) }) }) + + describe('add_axia_sig', () => { + let whitelistedAxia: Keypair + + before(async () => { + whitelistedAxia = Keypair.generate() + const whitelistAxiaIx = await whitelistSdk.setEntityWhitelistStatusIx( + EntityType.Axia, + whitelistedAxia.publicKey, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(provider, whitelistAxiaIx) + }) + + const generateIntentHash = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const generateNonce = (): string => { + return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') + } + + const createValidatedIntent = async (isFinal = true): Promise => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) + await makeTxSignAndSend(solverProvider, ix) + + // Set validations to meet min_validations requirement + const intentKey = sdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + // validations is at offset: 8 (disc) + 1 (op) + 32 (user) + 32 (intent_creator) + 32 (intent_hash) + 32 (nonce) + 8 (deadline) + 2 (min_validations) = 147 + // validations is u16, so 2 bytes + intentData.writeUInt16LE(1, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + return intentHash + } + + const createFinalizedProposal = async (deadline?: number): Promise<{ intentHash: string; proposalKey: PublicKey }> => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const proposalDeadline = deadline || now + 1800 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [ + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + ], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, proposalDeadline, true) + await makeTxSignAndSend(solverProvider, ix) + + const proposalKey = sdk.getProposalKey(intentHash, solver.publicKey) + return { intentHash, proposalKey } + } + + const createWhitelistedAxia = async (): Promise => { + const axia = Keypair.generate() + const whitelistAxiaIx = await whitelistSdk.setEntityWhitelistStatusIx( + EntityType.Axia, + axia.publicKey, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(provider, whitelistAxiaIx) + return axia + } + + const createWhitelistedValidator = async (): Promise => { + const validator = Keypair.generate() + const whitelistValidatorIx = await whitelistSdk.setEntityWhitelistStatusIx( + EntityType.Validator, + validator.publicKey, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(provider, whitelistValidatorIx) + return validator + } + + const createSignature = async (proposalKey: PublicKey, axia: Keypair): Promise => { + const signature = await signAsync(proposalKey.toBuffer(), axia.secretKey.slice(0, 32)) + return Array.from(new Uint8Array(signature)) + } + + it('should add axia signature successfully', async () => { + const { proposalKey } = await createFinalizedProposal() + + const signature = await createSignature(proposalKey, whitelistedAxia) + + const proposalBefore = await program.account.proposal.fetch(proposalKey) + expect(proposalBefore.isSigned).to.be.false + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs) + + const proposalAfter = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter.isSigned).to.be.true + }) + + it('should handle duplicate signature gracefully', async () => { + const { proposalKey } = await createFinalizedProposal() + + const signature = await createSignature(proposalKey, whitelistedAxia) + + const ixs1 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs1) + + const proposalAfter1 = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter1.isSigned).to.be.true + + client.expireBlockhash() + const ixs2 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs2) + + const proposalAfter2 = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter2.isSigned).to.be.true + }) + + it('should add signature multiple times safely', async () => { + const { proposalKey } = await createFinalizedProposal() + + const signature = await createSignature(proposalKey, whitelistedAxia) + + const ixs1 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs1) + + client.expireBlockhash() + const ixs2 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs2) + + client.expireBlockhash() + const ixs3 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs3) + + const proposalAfter = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter.isSigned).to.be.true + }) + + it('cant add signature if proposal is not final', async () => { + const intentHash = await createValidatedIntent(true) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 1800 + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, false) + await makeTxSignAndSend(solverProvider, ix) + + const proposalKey = sdk.getProposalKey(intentHash, solver.publicKey) + const signature = await createSignature(proposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include(`Proposal is not final`) + }) + + it('cant add signature if proposal has expired', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 50 + const { proposalKey } = await createFinalizedProposal(deadline) + + warpSeconds(provider, 51) + + const signature = await createSignature(proposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include(`Proposal has already expired`) + }) + + it('cant add signature if axia is not whitelisted', async () => { + const { proposalKey } = await createFinalizedProposal() + + const axia = Keypair.generate() + const signature = await createSignature(proposalKey, axia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, axia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include( + `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.` + ) + }) + + it('cant add signature if solver is not whitelisted', async () => { + const { proposalKey } = await createFinalizedProposal() + + const signature = await createSignature(proposalKey, whitelistedAxia) + + const ixs = await maliciousSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(maliciousProvider, ...ixs) + + expect(res.toString()).to.include( + `AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized.` + ) + }) + + it('cant add signature for non-existent proposal', async () => { + const proposalKey = Keypair.generate().publicKey + + const signature = await createSignature(proposalKey, whitelistedAxia) + + const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({ + message: proposalKey.toBuffer(), + publicKey: whitelistedAxia.publicKey.toBuffer(), + signature: Buffer.from(signature), + }) + + const ix = await program.methods + .addAxiaSig() + .accountsPartial({ + solver: solverSdk.getSignerKey(), + solverRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Solver, solverSdk.getSignerKey()), + proposal: proposalKey, + axiaRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Axia, whitelistedAxia.publicKey), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) + + expect(res.toString()).to.include(`Program log: AnchorError caused by account: proposal. Error Code: AccountNotInitialized`) + }) + + it('cant add signature if proposal deadline equals now', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 100 + const { proposalKey } = await createFinalizedProposal(deadline) + + warpSeconds(provider, 100) + + const signature = await createSignature(proposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include(`Proposal has already expired`) + }) + + it('cant add signature with wrong proposal key', async () => { + const { proposalKey } = await createFinalizedProposal() + const wrongProposalKey = Keypair.generate().publicKey + + const signature = await createSignature(wrongProposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.be.eq( + `FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, Custom(2)), meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('cant add signature with invalid signature', async () => { + const { proposalKey } = await createFinalizedProposal() + + const invalidSignature: number[] = Array.from({ length: 64 }, () => Math.floor(Math.random() * 256)) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, invalidSignature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.be.eq( + `FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, Custom(2)), meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('cant add signature from wrong axia pubkey', async () => { + const { proposalKey } = await createFinalizedProposal() + + const wrongAxia = Keypair.generate() + const signature = await createSignature(proposalKey, wrongAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, wrongAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include( + `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.` + ) + }) + + it('cant add signature with signature from different axia', async () => { + const { proposalKey } = await createFinalizedProposal() + + const axia2 = await createWhitelistedAxia() + const signature = await createSignature(proposalKey, axia2) + + // Try to use axia2's signature but claim it's from whitelistedAxia + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.be.eq( + `FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, Custom(2)), meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('cant add signature with signature from validator instead of axia', async () => { + const { proposalKey } = await createFinalizedProposal() + + const validator = await createWhitelistedValidator() + const signature = await createSignature(proposalKey, validator) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, validator.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.include( + `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.` + ) + }) + + it('cant add signature if signed message is wrong', async () => { + const { proposalKey } = await createFinalizedProposal() + + // Sign a different message (e.g., intent hash instead of proposal key) + const intentHash = generateIntentHash() + const signature = await signAsync(Buffer.from(intentHash, 'hex'), whitelistedAxia.secretKey.slice(0, 32)) + const signatureArray = Array.from(new Uint8Array(signature)) + + const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({ + message: Buffer.from(intentHash, 'hex'), + publicKey: whitelistedAxia.publicKey.toBuffer(), + signature: Buffer.from(signatureArray), + }) + + const ix = await program.methods + .addAxiaSig() + .accountsPartial({ + solver: solverSdk.getSignerKey(), + solverRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Solver, solverSdk.getSignerKey()), + proposal: proposalKey, + axiaRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Axia, whitelistedAxia.publicKey), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) + + expect(res.toString()).to.include(`Signature verification failed`) + }) + + it('cant add signature with corrupted ed25519 instruction', async () => { + const { proposalKey } = await createFinalizedProposal() + + // Create corrupted Ed25519 instruction with wrong program ID + const corruptedEd25519Ix = new TransactionInstruction({ + programId: Keypair.generate().publicKey, + keys: [], + data: Buffer.from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + }) + + const ix = await program.methods + .addAxiaSig() + .accountsPartial({ + solver: solverSdk.getSignerKey(), + solverRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Solver, solverSdk.getSignerKey()), + proposal: proposalKey, + axiaRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Axia, whitelistedAxia.publicKey), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + const res = await makeTxSignAndSend(solverProvider, corruptedEd25519Ix, ix) + + expect(res.toString()).to.be.eq( + `FailedTransactionMetadata(FailedTransactionMetadata { err: InvalidProgramForExecution, meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('should add signature when proposal deadline is close', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 10 + const { proposalKey } = await createFinalizedProposal(deadline) + + const signature = await createSignature(proposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs) + + const proposalAfter = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter.isSigned).to.be.true + }) + }) }) }) From e5c9efd3679a32ec64084fca1c06393f91f9024d Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Thu, 11 Dec 2025 13:56:53 -0300 Subject: [PATCH 35/41] Lint --- packages/svm/programs/settler/src/utils/sigs.rs | 5 ++++- packages/svm/tests/settler.test.ts | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/svm/programs/settler/src/utils/sigs.rs b/packages/svm/programs/settler/src/utils/sigs.rs index 8b6a8d9..a5ce59b 100644 --- a/packages/svm/programs/settler/src/utils/sigs.rs +++ b/packages/svm/programs/settler/src/utils/sigs.rs @@ -44,7 +44,10 @@ pub fn get_args_from_ed25519_ix_data(data: &[u8]) -> Result> { let exp_signature_offset: u16 = exp_public_key_offset + 32_u16; let exp_message_data_offset: u16 = exp_signature_offset + 64_u16; let exp_num_signatures: u8 = 1; - let exp_message_data_size: u16 = msg.len().try_into().map_err(|_| SettlerError::SigVerificationFailed)?; + let exp_message_data_size: u16 = msg + .len() + .try_into() + .map_err(|_| SettlerError::SigVerificationFailed)?; // Header if num_signatures != &exp_num_signatures.to_le_bytes() diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 1dd7d81..33543a7 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -2307,7 +2307,9 @@ describe('Settler Program', () => { return intentHash } - const createFinalizedProposal = async (deadline?: number): Promise<{ intentHash: string; proposalKey: PublicKey }> => { + const createFinalizedProposal = async ( + deadline?: number + ): Promise<{ intentHash: string; proposalKey: PublicKey }> => { const intentHash = await createValidatedIntent(true) const now = Number(client.getClock().unixTimestamp) const proposalDeadline = deadline || now + 1800 @@ -2505,7 +2507,9 @@ describe('Settler Program', () => { const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) - expect(res.toString()).to.include(`Program log: AnchorError caused by account: proposal. Error Code: AccountNotInitialized`) + expect(res.toString()).to.include( + `Program log: AnchorError caused by account: proposal. Error Code: AccountNotInitialized` + ) }) it('cant add signature if proposal deadline equals now', async () => { @@ -2630,7 +2634,12 @@ describe('Settler Program', () => { const corruptedEd25519Ix = new TransactionInstruction({ programId: Keypair.generate().publicKey, keys: [], - data: Buffer.from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + data: Buffer.from([ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), }) const ix = await program.methods @@ -2647,6 +2656,7 @@ describe('Settler Program', () => { const res = await makeTxSignAndSend(solverProvider, corruptedEd25519Ix, ix) expect(res.toString()).to.be.eq( + // eslint-disable-next-line no-secrets/no-secrets `FailedTransactionMetadata(FailedTransactionMetadata { err: InvalidProgramForExecution, meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` ) }) From 9539b110e94e3671320aaa5ccf8207e3d362c5d5 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 15 Dec 2025 15:23:06 -0300 Subject: [PATCH 36/41] Rm entity_type check in EntityRegistry PDAs --- packages/svm/programs/settler/src/instructions/add_axia_sig.rs | 3 +-- .../svm/programs/settler/src/instructions/add_validator_sig.rs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs index a72d331..afebef9 100644 --- a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs +++ b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs @@ -32,8 +32,7 @@ pub struct AddAxiaSig<'info> { bump = axia_registry.bump, seeds::program = crate::whitelist::ID, constraint = - axia_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::AxiaNotWhitelisted, - constraint = axia_registry.entity_type as u8 == EntityType::Axia as u8 @ SettlerError::AxiaNotWhitelisted, + axia_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::AxiaNotWhitelisted )] pub axia_registry: Box>, diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs index 4f5a8b3..f08c3b6 100644 --- a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs +++ b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs @@ -41,6 +41,7 @@ pub struct AddValidatorSig<'info> { /// This PDA must be uninitialized pub fulfilled_intent: SystemAccount<'info>, + /// CHECK: other checks in ix body #[account( constraint = validator_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::ValidatorNotWhitelisted From 02b17d7000d51cb0bf2d8f6798a355d6e04ab937 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 15 Dec 2025 16:19:27 -0300 Subject: [PATCH 37/41] Add fees to Proposal PDA --- packages/svm/programs/settler/src/errors.rs | 12 + .../add_instructions_to_proposal.rs | 2 +- .../settler/src/instructions/create_intent.rs | 6 +- .../src/instructions/create_proposal.rs | 19 +- .../settler/src/instructions/extend_intent.rs | 6 +- packages/svm/programs/settler/src/lib.rs | 7 +- .../settler/src/state/fulfilled_intent.rs | 1 + .../svm/programs/settler/src/state/intent.rs | 8 +- .../programs/settler/src/state/proposal.rs | 35 ++- .../svm/programs/settler/src/types/max_fee.rs | 4 +- packages/svm/sdks/settler/Settler.ts | 20 +- packages/svm/sdks/settler/types.ts | 6 +- packages/svm/tests/settler.test.ts | 231 ++++++++++++++++-- 13 files changed, 301 insertions(+), 56 deletions(-) diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index 34360b1..dd26447 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -59,6 +59,18 @@ pub enum SettlerError { #[msg("Signature verification failed")] SigVerificationFailed, + #[msg("Incorrect intent for proposal")] + IncorrectIntentForProposal, + + #[msg("Proposal is not signed by Axia")] + ProposalIsNotSigned, + + #[msg("Invalid fee mint")] + InvalidFeeMint, + + #[msg("Fee amount exceeds max fee")] + FeeAmountExceedsMaxFee, + #[msg("Math Error")] MathError, } diff --git a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs index 8c9cc91..9755cee 100644 --- a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs @@ -13,7 +13,7 @@ pub struct AddInstructionsToProposal<'info> { #[account( mut, - realloc = Proposal::extended_size(proposal.to_account_info().data_len(), &more_instructions), + realloc = Proposal::extended_size(proposal.to_account_info().data_len(), &more_instructions)?, realloc::payer = proposal_creator, realloc::zero = true, has_one = proposal_creator @ SettlerError::IncorrectProposalCreator diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index c6f3cec..f2984ea 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; use crate::{ errors::SettlerError, state::Intent, - types::{IntentEvent, MaxFee, OpType}, + types::{IntentEvent, OpType, TokenFee}, whitelist::{ accounts::EntityRegistry, types::{EntityType, WhitelistStatus}, @@ -12,7 +12,7 @@ use crate::{ #[derive(Accounts)] // TODO: can we optimize this deser? we just need the three Vec for their length -#[instruction(intent_hash: [u8; 32], data: Vec, max_fees: Vec, events: Vec, min_validations: u16)] +#[instruction(intent_hash: [u8; 32], data: Vec, max_fees: Vec, events: Vec, min_validations: u16)] pub struct CreateIntent<'info> { #[account(mut)] pub solver: Signer<'info>, @@ -51,7 +51,7 @@ pub fn create_intent( ctx: Context, intent_hash: [u8; 32], data: Vec, - max_fees: Vec, + max_fees: Vec, events: Vec, min_validations: u16, op: OpType, diff --git a/packages/svm/programs/settler/src/instructions/create_proposal.rs b/packages/svm/programs/settler/src/instructions/create_proposal.rs index 172e356..f648794 100644 --- a/packages/svm/programs/settler/src/instructions/create_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/create_proposal.rs @@ -3,6 +3,7 @@ use anchor_lang::prelude::*; use crate::{ errors::SettlerError, state::{Intent, Proposal, ProposalInstruction}, + types::TokenFee, whitelist::{ accounts::EntityRegistry, types::{EntityType, WhitelistStatus}, @@ -10,7 +11,7 @@ use crate::{ }; #[derive(Accounts)] -#[instruction(instructions: Vec)] +#[instruction(instructions: Vec, fees: Vec,)] pub struct CreateProposal<'info> { #[account(mut)] pub solver: Signer<'info>, @@ -39,7 +40,7 @@ pub struct CreateProposal<'info> { seeds = [b"proposal", intent.key().as_ref(), solver.key().as_ref()], bump, payer = solver, - space = 8 + Proposal::BASE_LEN + Proposal::instructions_size(&instructions) + space = Proposal::total_size(&instructions, fees.len())? )] pub proposal: Box>, @@ -49,6 +50,7 @@ pub struct CreateProposal<'info> { pub fn create_proposal( ctx: Context, instructions: Vec, + fees: Vec, deadline: u64, is_final: bool, ) -> Result<()> { @@ -66,6 +68,18 @@ pub fn create_proposal( SettlerError::InsufficientIntentValidations ); require!(intent.is_final, SettlerError::IntentIsNotFinal); + require!( + fees.len() == intent.max_fees.len(), + SettlerError::InvalidFeeMint + ); + + fees.iter() + .zip(&intent.max_fees) + .try_for_each(|(fee, max_fee)| { + require_keys_eq!(fee.mint, max_fee.mint, SettlerError::InvalidFeeMint); + require_gte!(max_fee.amount, fee.amount, SettlerError::FeeAmountExceedsMaxFee); + Ok(()) + })?; let proposal = &mut ctx.accounts.proposal; @@ -75,6 +89,7 @@ pub fn create_proposal( proposal.is_final = is_final; proposal.is_signed = false; proposal.instructions = instructions; + proposal.fees = fees; proposal.bump = ctx.bumps.proposal; Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/extend_intent.rs b/packages/svm/programs/settler/src/instructions/extend_intent.rs index 2d34360..f14c596 100644 --- a/packages/svm/programs/settler/src/instructions/extend_intent.rs +++ b/packages/svm/programs/settler/src/instructions/extend_intent.rs @@ -3,11 +3,11 @@ use anchor_lang::prelude::*; use crate::{ errors::SettlerError, state::Intent, - types::{IntentEvent, MaxFee}, + types::{IntentEvent, TokenFee}, }; #[derive(Accounts)] -#[instruction(more_data: Option>, more_max_fees: Option>, more_events: Option>)] +#[instruction(more_data: Option>, more_max_fees: Option>, more_events: Option>)] pub struct ExtendIntent<'info> { #[account(mut)] pub intent_creator: Signer<'info>, @@ -29,7 +29,7 @@ pub struct ExtendIntent<'info> { pub fn extend_intent( ctx: Context, more_data: Option>, - more_max_fees: Option>, + more_max_fees: Option>, more_events: Option>, finalize: bool, ) -> Result<()> { diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index fc6db33..77058f3 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -50,7 +50,7 @@ pub mod settler { ctx: Context, intent_hash: [u8; 32], data: Vec, - max_fees: Vec, + max_fees: Vec, events: Vec, min_validations: u16, op: OpType, @@ -77,10 +77,11 @@ pub mod settler { pub fn create_proposal( ctx: Context, instructions: Vec, + fees: Vec, deadline: u64, is_final: bool, ) -> Result<()> { - instructions::create_proposal(ctx, instructions, deadline, is_final) + instructions::create_proposal(ctx, instructions, fees, deadline, is_final) } pub fn execute_proposal(ctx: Context) -> Result<()> { @@ -90,7 +91,7 @@ pub mod settler { pub fn extend_intent( ctx: Context, more_data: Option>, - more_max_fees: Option>, + more_max_fees: Option>, more_events: Option>, finalize: bool, ) -> Result<()> { diff --git a/packages/svm/programs/settler/src/state/fulfilled_intent.rs b/packages/svm/programs/settler/src/state/fulfilled_intent.rs index 0556f84..812a6e2 100644 --- a/packages/svm/programs/settler/src/state/fulfilled_intent.rs +++ b/packages/svm/programs/settler/src/state/fulfilled_intent.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; #[account] +#[derive(InitSpace)] pub struct FulfilledIntent { pub fulfilled_at: u64, } diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs index bfd308f..087eb01 100644 --- a/packages/svm/programs/settler/src/state/intent.rs +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use crate::{ - types::{IntentEvent, MaxFee, OpType}, + types::{IntentEvent, OpType, TokenFee}, utils::{add, mul, sub}, }; @@ -18,7 +18,7 @@ pub struct Intent { pub is_final: bool, pub validators: Vec, // TODO: how to store more efficiently? pub intent_data: Vec, - pub max_fees: Vec, + pub max_fees: Vec, pub events: Vec, pub bump: u8, } @@ -57,7 +57,7 @@ impl Intent { } pub fn max_fees_size(len: usize) -> Result { - add(4, mul(MaxFee::INIT_SPACE, len)?) + add(4, mul(TokenFee::INIT_SPACE, len)?) } pub fn events_size(events: &[IntentEvent]) -> Result { @@ -74,7 +74,7 @@ impl Intent { pub fn extended_size( size: usize, more_data: &Option>, - more_max_fees: &Option>, + more_max_fees: &Option>, more_events: &Option>, ) -> Result { let mut size = size; diff --git a/packages/svm/programs/settler/src/state/proposal.rs b/packages/svm/programs/settler/src/state/proposal.rs index 0baa0e7..5487320 100644 --- a/packages/svm/programs/settler/src/state/proposal.rs +++ b/packages/svm/programs/settler/src/state/proposal.rs @@ -1,5 +1,10 @@ use anchor_lang::prelude::*; +use crate::{ + types::TokenFee, + utils::{add, mul, sub}, +}; + #[account] pub struct Proposal { pub intent: Pubkey, @@ -8,6 +13,7 @@ pub struct Proposal { pub is_final: bool, pub is_signed: bool, pub instructions: Vec, + pub fees: Vec, pub bump: u8, } @@ -22,15 +28,32 @@ impl Proposal { 1 // bump ; - pub fn instructions_size(instructions: &Vec) -> usize { - 4 + instructions + pub fn total_size(instructions: &Vec, fees_len: usize) -> Result { + let size = add(8, Proposal::BASE_LEN)?; + let size = add(size, Proposal::instructions_size(instructions)?)?; + let size = add(size, Proposal::fees_size(fees_len)?)?; + Ok(size) + } + + pub fn instructions_size(instructions: &Vec) -> Result { + let sum = instructions .iter() - .map(|instruction| instruction.size()) - .sum::() + .try_fold(0usize, |acc, ix| add(acc, ix.size()))?; + add(4, sum) + } + + pub fn fees_size(len: usize) -> Result { + add(4, mul(TokenFee::INIT_SPACE, len)?) } - pub fn extended_size(size: usize, more_instructions: &Vec) -> usize { - size + Proposal::instructions_size(more_instructions) - 4 + pub fn extended_size( + size: usize, + more_instructions: &Vec, + ) -> Result { + sub( + add(size, Proposal::instructions_size(more_instructions)?)?, + 4, + ) } } diff --git a/packages/svm/programs/settler/src/types/max_fee.rs b/packages/svm/programs/settler/src/types/max_fee.rs index a5d5fe6..dc072b9 100644 --- a/packages/svm/programs/settler/src/types/max_fee.rs +++ b/packages/svm/programs/settler/src/types/max_fee.rs @@ -1,11 +1,11 @@ use anchor_lang::prelude::*; #[derive(Clone, AnchorSerialize, AnchorDeserialize)] -pub struct MaxFee { +pub struct TokenFee { pub mint: Pubkey, pub amount: u64, } -impl Space for MaxFee { +impl Space for TokenFee { const INIT_SPACE: usize = 32 + 8; } diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index 2bae38f..2d0978b 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -8,13 +8,13 @@ import { CreateIntentParams, ExtendIntentParams, IntentEvent, - MaxFee, OpType, ProposalInstruction, ProposalInstructionAccountMeta, + TokenFee, } from './types' -type MaxFeeAnchor = { +type TokenFeeAnchor = { mint: web3.PublicKey amount: BN } @@ -52,7 +52,7 @@ export default class SettlerSDK { const intentHash = this.parseIntentHashHex(intentHashHex) const nonce = this.parseIntentNonceHex(nonceHex) const data = Buffer.from(dataHex, 'hex') - const maxFeesBn = this.parseIntentMaxFees(maxFees) + const maxFeesBn = this.parseTokenFees(maxFees) const events = this.parseIntentEventsHex(eventsHex) const ix = await this.program.methods @@ -85,7 +85,7 @@ export default class SettlerSDK { const { moreDataHex = '', moreMaxFees = [], moreEventsHex = [] } = params const moreData = Buffer.from(moreDataHex, 'hex') - const moreMaxFeesBn = this.parseIntentMaxFees(moreMaxFees) + const moreMaxFeesBn = this.parseTokenFees(moreMaxFees) const moreEvents = this.parseIntentEventsHex(moreEventsHex) const ix = await this.program.methods @@ -114,13 +114,15 @@ export default class SettlerSDK { async createProposalIx( intentHashHex: string, instructions: ProposalInstruction[], + fees: TokenFee[], deadline: number, isFinal = true ): Promise { const parsedInstructions = this.parseProposalInstructions(instructions) + const parsedFees = this.parseTokenFees(fees) const ix = await this.program.methods - .createProposal(parsedInstructions, new BN(deadline), isFinal) + .createProposal(parsedInstructions, parsedFees, new BN(deadline), isFinal) .accountsPartial({ solver: this.getSignerKey(), solverRegistry: this.getEntityRegistryPubkey(EntityType.Solver, this.getSignerKey()), @@ -299,10 +301,10 @@ export default class SettlerSDK { return events } - private parseIntentMaxFees(maxFees: MaxFee[]): MaxFeeAnchor[] { - return maxFees.map((maxFee) => ({ - ...maxFee, - amount: new BN(maxFee.amount), + private parseTokenFees(tokenFees: TokenFee[]): TokenFeeAnchor[] { + return tokenFees.map((tokenFee) => ({ + ...tokenFee, + amount: new BN(tokenFee.amount), })) } diff --git a/packages/svm/sdks/settler/types.ts b/packages/svm/sdks/settler/types.ts index 8f21267..f3ac6eb 100644 --- a/packages/svm/sdks/settler/types.ts +++ b/packages/svm/sdks/settler/types.ts @@ -1,6 +1,6 @@ import { web3 } from '@coral-xyz/anchor' -export type MaxFee = { +export type TokenFee = { mint: web3.PublicKey amount: number } @@ -26,13 +26,13 @@ export type CreateIntentParams = { deadline: number minValidations: number dataHex: string - maxFees: MaxFee[] + maxFees: TokenFee[] eventsHex: IntentEvent[] } export type ExtendIntentParams = { moreDataHex?: string - moreMaxFees?: MaxFee[] + moreMaxFees?: TokenFee[] moreEventsHex?: IntentEvent[] } diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index 33543a7..d7ce873 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -18,6 +18,7 @@ import * as SettlerIDL from '../target/idl/settler.json' import * as WhitelistIDL from '../target/idl/whitelist.json' import { Settler } from '../target/types/settler' import { makeTxSignAndSend, warpSeconds } from './utils' +import { FailedTransactionMetadata } from 'litesvm' describe('Settler Program', () => { let client: LiteSVM @@ -935,7 +936,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) await makeTxSignAndSend(solverProvider, ix) const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) @@ -984,7 +985,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) await makeTxSignAndSend(solverProvider, ix) const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) @@ -1013,7 +1014,7 @@ describe('Settler Program', () => { const instructions: any[] = [] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) await makeTxSignAndSend(solverProvider, ix) const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) @@ -1033,7 +1034,7 @@ describe('Settler Program', () => { }, ] - const ix = await maliciousSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await maliciousSdk.createProposalIx(intentHash, instructions, [], deadline) const res = await makeTxSignAndSend(maliciousProvider, ix) expect(res.toString()).to.include( 'AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized' @@ -1053,7 +1054,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Deadline must be in the future`) @@ -1072,7 +1073,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Deadline must be in the future`) @@ -1122,7 +1123,7 @@ describe('Settler Program', () => { }, ] - const ix2 = await solverSdk.createProposalIx(intentHash, instructions, proposalDeadline) + const ix2 = await solverSdk.createProposalIx(intentHash, instructions, [], proposalDeadline) const res = await makeTxSignAndSend(solverProvider, ix2) expect(res.toString()).to.include(`Intent has already expired`) @@ -1141,7 +1142,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, proposalDeadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], proposalDeadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Proposal deadline can't be after the Intent's deadline`) @@ -1189,7 +1190,7 @@ describe('Settler Program', () => { }, ] - const ix2 = await solverSdk.createProposalIx(intentHash, instructions, proposalDeadline) + const ix2 = await solverSdk.createProposalIx(intentHash, instructions, [], proposalDeadline) const res = await makeTxSignAndSend(solverProvider, ix2) expect(res.toString()).to.include(`Intent has insufficient validations`) @@ -1208,7 +1209,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Intent is not final`) @@ -1236,7 +1237,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include( @@ -1257,11 +1258,11 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) await makeTxSignAndSend(solverProvider, ix) client.expireBlockhash() - const ix2 = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix2 = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) const res = await makeTxSignAndSend(solverProvider, ix2) expect(res.toString()).to.include(`already in use`) @@ -1280,11 +1281,201 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`AccountNotInitialized`) }) + + it('should create proposal with fees matching intent max_fees', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + const mint = Keypair.generate().publicKey + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [ + { + mint, + amount: 1000, + }, + ], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(solverProvider, ix) + + // Set validations + const intentKey = sdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + intentData.writeUInt16LE(1, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + const proposalDeadline = now + 1800 + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const fees = [ + { + mint, + amount: 500, + }, + ] + + const proposalIx = await solverSdk.createProposalIx(intentHash, instructions, fees, proposalDeadline) + await makeTxSignAndSend(solverProvider, proposalIx) + + const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) + expect(proposal.fees.length).to.be.eq(1) + expect(proposal.fees[0].mint.toString()).to.be.eq(mint.toString()) + expect(proposal.fees[0].amount.toNumber()).to.be.eq(500) + }) + + it('cant create proposal with fees exceeding max_fees', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + const mint = Keypair.generate().publicKey + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [ + { + mint, + amount: 1000, + }, + ], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(solverProvider, ix) + + // Set validations + const intentKey = sdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + intentData.writeUInt16LE(1, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + const proposalDeadline = now + 1800 + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const fees = [ + { + mint, + amount: 1500, // Exceeds max_fee of 1000 + }, + ] + + const proposalIx = await solverSdk.createProposalIx(intentHash, instructions, fees, proposalDeadline) + const res = await makeTxSignAndSend(solverProvider, proposalIx) + + expect(res).to.be.instanceOf(FailedTransactionMetadata) + expect(res.toString()).to.match(/FeeAmountExceedsMaxFee|Fee amount exceeds max fee/i) + }) + + it('cant create proposal with fees having wrong mint', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + 3600 + const mint = Keypair.generate().publicKey + const wrongMint = Keypair.generate().publicKey + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: 1, + dataHex: '010203', + maxFees: [ + { + mint, + amount: 1000, + }, + ], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(solverProvider, ix) + + // Set validations + const intentKey = sdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + intentData.writeUInt16LE(1, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + const proposalDeadline = now + 1800 + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: 'deadbeef', + }, + ] + + const fees = [ + { + mint: wrongMint, // Wrong mint + amount: 500, + }, + ] + + const proposalIx = await solverSdk.createProposalIx(intentHash, instructions, fees, proposalDeadline) + const res = await makeTxSignAndSend(solverProvider, proposalIx) + + expect(res).to.be.instanceOf(FailedTransactionMetadata) + expect(res.toString()).to.match(/InvalidFeeMint|Invalid fee mint/i) + }) + }) describe('add_instructions_to_proposal', () => { @@ -1351,7 +1542,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, isFinal) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, isFinal) await makeTxSignAndSend(solverProvider, ix) return intentHash } @@ -1499,7 +1690,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, false) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, false) await makeTxSignAndSend(solverProvider, ix) warpSeconds(provider, 51) @@ -1531,7 +1722,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, false) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, false) await makeTxSignAndSend(solverProvider, ix) warpSeconds(provider, 100) @@ -1687,7 +1878,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, false) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, false) await makeTxSignAndSend(solverProvider, ix) return intentHash } @@ -2328,7 +2519,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, proposalDeadline, true) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], proposalDeadline, true) await makeTxSignAndSend(solverProvider, ix) const proposalKey = sdk.getProposalKey(intentHash, solver.publicKey) @@ -2429,7 +2620,7 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, deadline, false) + const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, false) await makeTxSignAndSend(solverProvider, ix) const proposalKey = sdk.getProposalKey(intentHash, solver.publicKey) From 362acb5bea9a857194f53c0cf67935d85e2cc71d Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 15 Dec 2025 16:37:55 -0300 Subject: [PATCH 38/41] Add maxFee check (gt 0) --- packages/svm/programs/settler/src/errors.rs | 3 + .../settler/src/instructions/create_intent.rs | 1 + packages/svm/tests/settler.test.ts | 306 +++++++++++++++--- 3 files changed, 261 insertions(+), 49 deletions(-) diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index dd26447..ae62bae 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -14,6 +14,9 @@ pub enum SettlerError { #[msg("Only a whitelisted validator can call this instruction")] OnlyValidator, + #[msg("No max fees provided")] + NoMaxFees, + #[msg("Validator is not whitelisted")] ValidatorNotWhitelisted, diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index f2984ea..aa62b6d 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -62,6 +62,7 @@ pub fn create_intent( ) -> Result<()> { let now = Clock::get()?.unix_timestamp as u64; require!(deadline > now, SettlerError::DeadlineIsInThePast); + require!(max_fees.len() > 0, SettlerError::NoMaxFees); // TODO: check hash diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index d7ce873..da9764e 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -194,7 +194,7 @@ describe('Settler Program', () => { expect(intent.isFinal).to.be.true }) - it('should create an intent with empty max_fees', async () => { + it('cant create an intent with empty max_fees', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey @@ -213,11 +213,9 @@ describe('Settler Program', () => { } const ix = await solverSdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(solverProvider, ix) + const res = await makeTxSignAndSend(solverProvider, ix) - const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(intent.op).to.deep.include({ call: {} }) - expect(intent.maxFees.length).to.be.eq(0) + expect(res.toString()).to.include('No max fees provided') }) it('should create an intent with empty events', async () => { @@ -234,7 +232,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '0a0b0c', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -259,7 +262,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -284,7 +292,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -309,7 +322,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -339,7 +357,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -363,7 +386,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -387,7 +415,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -422,7 +455,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -450,7 +488,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -764,12 +807,20 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '010203', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - await makeTxSignAndSend(solverProvider, ix) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create intent: ${res.toString()}`) + } return intentHash } @@ -893,12 +944,20 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '010203', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - await makeTxSignAndSend(solverProvider, ix) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create intent: ${res.toString()}`) + } // Set validations to meet min_validations requirement const intentKey = sdk.getIntentKey(intentHash) @@ -919,6 +978,7 @@ describe('Settler Program', () => { it('should create a proposal', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 1800 @@ -936,8 +996,16 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) - await makeTxSignAndSend(solverProvider, ix) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create proposal: ${res.toString()}`) + } const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) expect(proposal.intent.toString()).to.be.eq(sdk.getIntentKey(intentHash).toString()) @@ -957,6 +1025,7 @@ describe('Settler Program', () => { it('should create a proposal with multiple instructions', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 1800 @@ -985,8 +1054,16 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) - await makeTxSignAndSend(solverProvider, ix) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create proposal: ${res.toString()}`) + } const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) expect(proposal.instructions.length).to.be.eq(2) @@ -1009,13 +1086,22 @@ describe('Settler Program', () => { it('should create a proposal with empty instructions', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 1800 const instructions: any[] = [] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) - await makeTxSignAndSend(solverProvider, ix) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create proposal: ${res.toString()}`) + } const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) expect(proposal.instructions.length).to.be.eq(0) @@ -1023,6 +1109,7 @@ describe('Settler Program', () => { it('cant create proposal if not whitelisted solver', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 1800 @@ -1034,7 +1121,12 @@ describe('Settler Program', () => { }, ] - const ix = await maliciousSdk.createProposalIx(intentHash, instructions, [], deadline) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await maliciousSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(maliciousProvider, ix) expect(res.toString()).to.include( 'AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized' @@ -1043,6 +1135,7 @@ describe('Settler Program', () => { it('cant create proposal with deadline in the past', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now - 100 @@ -1054,7 +1147,12 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Deadline must be in the future`) @@ -1062,6 +1160,7 @@ describe('Settler Program', () => { it('cant create proposal with deadline equal to now', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now @@ -1073,7 +1172,12 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Deadline must be in the future`) @@ -1093,7 +1197,12 @@ describe('Settler Program', () => { deadline: intentDeadline, minValidations: 1, dataHex: '010203', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -1162,7 +1271,12 @@ describe('Settler Program', () => { deadline, minValidations: 2, dataHex: '010203', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -1198,6 +1312,7 @@ describe('Settler Program', () => { it('cant create proposal if intent is not final', async () => { const intentHash = await createValidatedIntent(false) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 1800 @@ -1209,7 +1324,12 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include(`Intent is not final`) @@ -1229,6 +1349,7 @@ describe('Settler Program', () => { data: Buffer.from('595168911b9267f7' + '010000000000000000', 'hex'), }) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const instructions = [ { programId: Keypair.generate().publicKey, @@ -1237,7 +1358,12 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix) expect(res.toString()).to.include( @@ -1247,6 +1373,7 @@ describe('Settler Program', () => { it('cant create proposal with same intent_hash and solver twice', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 1800 @@ -1258,11 +1385,16 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) await makeTxSignAndSend(solverProvider, ix) client.expireBlockhash() - const ix2 = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) + const ix2 = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix2) expect(res.toString()).to.include(`already in use`) @@ -1501,12 +1633,20 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '010203', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - await makeTxSignAndSend(solverProvider, ix) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create intent: ${res.toString()}`) + } // Set validations const intentKey = sdk.getIntentKey(intentHash) @@ -1525,6 +1665,7 @@ describe('Settler Program', () => { const createTestProposal = async (isFinal = false): Promise => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 1800 @@ -1542,7 +1683,12 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, isFinal) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline, isFinal) await makeTxSignAndSend(solverProvider, ix) return intentHash } @@ -1679,6 +1825,7 @@ describe('Settler Program', () => { it('cant add instructions if proposal deadline has passed', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 50 @@ -1690,7 +1837,12 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, false) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline, false) await makeTxSignAndSend(solverProvider, ix) warpSeconds(provider, 51) @@ -1711,6 +1863,7 @@ describe('Settler Program', () => { it('cant add instructions if proposal deadline equals now', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 100 @@ -1722,7 +1875,12 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, false) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline, false) await makeTxSignAndSend(solverProvider, ix) warpSeconds(provider, 100) @@ -1839,12 +1997,20 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '010203', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - await makeTxSignAndSend(solverProvider, ix) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create intent: ${res.toString()}`) + } // Set validations const intentKey = sdk.getIntentKey(intentHash) @@ -1863,6 +2029,7 @@ describe('Settler Program', () => { const createTestProposalWithDeadline = async (deadline: number): Promise => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const instructions = [ { @@ -1878,7 +2045,12 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, false) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline, false) await makeTxSignAndSend(solverProvider, ix) return intentHash } @@ -2061,12 +2233,20 @@ describe('Settler Program', () => { deadline, minValidations, dataHex: '010203', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - await makeTxSignAndSend(solverProvider, ix) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create intent: ${res.toString()}`) + } return intentHash } @@ -2262,7 +2442,12 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '010203', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } @@ -2474,12 +2659,20 @@ describe('Settler Program', () => { deadline, minValidations: 1, dataHex: '010203', - maxFees: [], + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: 1000, + }, + ], eventsHex: [], } const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - await makeTxSignAndSend(solverProvider, ix) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create intent: ${res.toString()}`) + } // Set validations to meet min_validations requirement const intentKey = sdk.getIntentKey(intentHash) @@ -2502,6 +2695,7 @@ describe('Settler Program', () => { deadline?: number ): Promise<{ intentHash: string; proposalKey: PublicKey }> => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const proposalDeadline = deadline || now + 1800 @@ -2519,8 +2713,16 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], proposalDeadline, true) - await makeTxSignAndSend(solverProvider, ix) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, proposalDeadline, true) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create proposal: ${res.toString()}`) + } const proposalKey = sdk.getProposalKey(intentHash, solver.publicKey) return { intentHash, proposalKey } @@ -2609,6 +2811,7 @@ describe('Settler Program', () => { it('cant add signature if proposal is not final', async () => { const intentHash = await createValidatedIntent(true) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now + 1800 @@ -2620,7 +2823,12 @@ describe('Settler Program', () => { }, ] - const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline, false) + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline, false) await makeTxSignAndSend(solverProvider, ix) const proposalKey = sdk.getProposalKey(intentHash, solver.publicKey) From 7cb4577614708aee1e193bf6676935b3c851f507 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 15 Dec 2025 16:53:30 -0300 Subject: [PATCH 39/41] WIP: execute_proposal --- .../src/instructions/execute_proposal.rs | 83 ++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index 6f24c0e..a77fbbd 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -1,8 +1,89 @@ use anchor_lang::prelude::*; +use crate::{ + errors::SettlerError, + state::{FulfilledIntent, Intent, Proposal}, + types::IntentEvent, + whitelist::{ + accounts::EntityRegistry, + types::{EntityType, WhitelistStatus}, + }, +}; + #[derive(Accounts)] -pub struct ExecuteProposal {} +pub struct ExecuteProposal<'info> { + #[account(mut)] + pub solver: Signer<'info>, + + #[account( + seeds = [b"entity-registry", &[EntityType::Solver as u8 + 1], solver.key().as_ref()], + bump = solver_registry.bump, + seeds::program = crate::whitelist::ID, + constraint = + solver_registry.status as u8 == WhitelistStatus::Whitelisted as u8 @ SettlerError::OnlySolver + )] + pub solver_registry: Box>, + + /// CHECK: account defined in proposal + #[account(mut)] + pub proposal_creator: UncheckedAccount<'info>, + + #[account( + mut, + has_one = intent @ SettlerError::IncorrectIntentForProposal, + has_one = proposal_creator @ SettlerError::IncorrectProposalCreator, + constraint = proposal.is_signed @ SettlerError::ProposalIsNotSigned, + close = proposal_creator + )] + pub proposal: Box>, + + /// CHECK: account defined in intent + #[account(mut)] + pub intent_creator: UncheckedAccount<'info>, + + #[account( + mut, + has_one = intent_creator @ SettlerError::IncorrectIntentCreator, + close = intent_creator + )] + pub intent: Box>, + + #[account( + init, + seeds = [b"fulfilled-intent", intent.intent_hash.as_ref()], + bump, + space = 8 + FulfilledIntent::INIT_SPACE, + payer = solver + )] + pub fulfilled_intent: Box>, + + pub system_program: Program<'info, System>, +} pub fn execute_proposal(ctx: Context) -> Result<()> { + let now = Clock::get()?.unix_timestamp as u64; + let proposal = &ctx.accounts.proposal; + let intent = &ctx.accounts.intent; + + require!(proposal.deadline > now, SettlerError::ProposalIsExpired); + + // TODO: Execute proposal + + // TODO: Validate execution + + // TODO: Emit events + intent.events.iter().for_each(|event| { + emit!(IntentEventEvent { + event: event.clone() + }) + }); + + // TODO: Pay fees to Solver + Ok(()) } + +#[event] +pub struct IntentEventEvent { + event: IntentEvent, +} From 472f7d845b379db295a459eb575ce9c5a45e89b4 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Mon, 15 Dec 2025 16:56:00 -0300 Subject: [PATCH 40/41] Fix lint --- .../programs/settler/src/instructions/create_proposal.rs | 6 +++++- packages/svm/tests/settler.test.ts | 4 +--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/svm/programs/settler/src/instructions/create_proposal.rs b/packages/svm/programs/settler/src/instructions/create_proposal.rs index f648794..c0e892d 100644 --- a/packages/svm/programs/settler/src/instructions/create_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/create_proposal.rs @@ -77,7 +77,11 @@ pub fn create_proposal( .zip(&intent.max_fees) .try_for_each(|(fee, max_fee)| { require_keys_eq!(fee.mint, max_fee.mint, SettlerError::InvalidFeeMint); - require_gte!(max_fee.amount, fee.amount, SettlerError::FeeAmountExceedsMaxFee); + require_gte!( + max_fee.amount, + fee.amount, + SettlerError::FeeAmountExceedsMaxFee + ); Ok(()) })?; diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index da9764e..f493b0e 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -7,7 +7,7 @@ import { Ed25519Program, Keypair, PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY, Transac import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' import fs from 'fs' -import { LiteSVM } from 'litesvm' +import { FailedTransactionMetadata, LiteSVM } from 'litesvm' import os from 'os' import path from 'path' @@ -18,7 +18,6 @@ import * as SettlerIDL from '../target/idl/settler.json' import * as WhitelistIDL from '../target/idl/whitelist.json' import { Settler } from '../target/types/settler' import { makeTxSignAndSend, warpSeconds } from './utils' -import { FailedTransactionMetadata } from 'litesvm' describe('Settler Program', () => { let client: LiteSVM @@ -1607,7 +1606,6 @@ describe('Settler Program', () => { expect(res).to.be.instanceOf(FailedTransactionMetadata) expect(res.toString()).to.match(/InvalidFeeMint|Invalid fee mint/i) }) - }) describe('add_instructions_to_proposal', () => { From 3b8422212c57abb84918cfe2ea234a6c3119aec7 Mon Sep 17 00:00:00 2001 From: GuidoDipietro Date: Tue, 16 Dec 2025 16:33:14 -0300 Subject: [PATCH 41/41] Tidy up tests --- packages/svm/Anchor.toml | 2 +- packages/svm/tests/helpers/constants.ts | 51 + packages/svm/tests/helpers/settler-helpers.ts | 231 +++ packages/svm/tests/settler.test.ts | 1312 ++++++----------- packages/svm/tests/whitelist.test.ts | 83 +- 5 files changed, 772 insertions(+), 907 deletions(-) create mode 100644 packages/svm/tests/helpers/constants.ts create mode 100644 packages/svm/tests/helpers/settler-helpers.ts diff --git a/packages/svm/Anchor.toml b/packages/svm/Anchor.toml index a651c4c..8d4d942 100644 --- a/packages/svm/Anchor.toml +++ b/packages/svm/Anchor.toml @@ -17,4 +17,4 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] -test = "yarn run ts-mocha -p ./tsconfig.json tests/**/*.ts" +test = "yarn run ts-mocha -p ./tsconfig.json tests/*.ts" diff --git a/packages/svm/tests/helpers/constants.ts b/packages/svm/tests/helpers/constants.ts new file mode 100644 index 0000000..a7cd412 --- /dev/null +++ b/packages/svm/tests/helpers/constants.ts @@ -0,0 +1,51 @@ +// Test constants for time values (in seconds) +export const COOLDOWN_PERIOD = 3600 +export const COOLDOWN_PERIOD_PLUS_ONE = 3601 +export const INTENT_DEADLINE_OFFSET = 3600 +export const PROPOSAL_DEADLINE_OFFSET = 1800 +export const STALE_CLAIM_DELAY = 50 +export const STALE_CLAIM_DELAY_PLUS_ONE = 51 +export const SHORT_DEADLINE = 100 +export const MEDIUM_DEADLINE = 300 +export const LONG_DEADLINE = 500 +export const VERY_SHORT_DEADLINE = 10 +export const WARP_TIME_SHORT = 100 +export const WARP_TIME_MEDIUM = 300 +export const WARP_TIME_LONG = 500 +export const EXPIRATION_TEST_DELAY = 80 +export const EXPIRATION_TEST_DELAY_PLUS_ONE = 81 +export const DOUBLE_CLAIM_DELAY = 90 +export const DOUBLE_CLAIM_DELAY_PLUS_ONE = 91 + +// Test constants for amounts +export const DEFAULT_MAX_FEE = 1000 +export const DEFAULT_MAX_FEE_HALF = 500 +export const DEFAULT_MAX_FEE_EXCEED = 1500 +export const ACCOUNT_CLOSE_FEE = 5000 // Fee for closing accounts + +// Test constants for data +export const DEFAULT_DATA_HEX = '010203' +export const DEFAULT_TOPIC_HEX = Buffer.from(Array(32).fill(1)).toString('hex') +export const DEFAULT_EVENT_DATA_HEX = '040506' +export const EMPTY_DATA_HEX = '' +export const TEST_DATA_HEX_1 = '070809' +export const TEST_DATA_HEX_2 = '0a0b0c' +export const TEST_DATA_HEX_3 = 'deadbeef' + +// Test constants for validation +export const DEFAULT_MIN_VALIDATIONS = 1 +export const MULTIPLE_MIN_VALIDATIONS = 3 + +// Test constants for iterations +export const LARGE_EXTEND_ITERATIONS = 100 +export const MULTIPLE_PROPOSALS_COUNT = 20 + +// Test constants for hex string lengths +export const INTENT_HASH_LENGTH = 32 // bytes +export const NONCE_LENGTH = 32 // bytes +export const SIGNATURE_LENGTH = 64 // bytes + +// Test constants for cooldown validation +export const MAX_COOLDOWN = 3600 * 24 * 30 // 30 days +export const MAX_COOLDOWN_PLUS_ONE = MAX_COOLDOWN + 1 +export const MIN_COOLDOWN = 0 diff --git a/packages/svm/tests/helpers/settler-helpers.ts b/packages/svm/tests/helpers/settler-helpers.ts new file mode 100644 index 0000000..7435e83 --- /dev/null +++ b/packages/svm/tests/helpers/settler-helpers.ts @@ -0,0 +1,231 @@ +import { Program } from '@coral-xyz/anchor' +import { signAsync } from '@noble/ed25519' +import { Keypair, PublicKey } from '@solana/web3.js' +import { LiteSVMProvider } from 'anchor-litesvm' +import { expect } from 'chai' +import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from 'litesvm' + +import SettlerSDK from '../../sdks/settler/Settler' +import { CreateIntentParams, IntentEvent, OpType, ProposalInstruction, TokenFee } from '../../sdks/settler/types' +import WhitelistSDK, { EntityType, WhitelistStatus } from '../../sdks/whitelist/Whitelist' +import { Settler } from '../../target/types/settler' +import { makeTxSignAndSend } from '../utils' +import { + DEFAULT_DATA_HEX, + DEFAULT_EVENT_DATA_HEX, + DEFAULT_MAX_FEE, + DEFAULT_MIN_VALIDATIONS, + DEFAULT_TOPIC_HEX, + INTENT_DEADLINE_OFFSET, + INTENT_HASH_LENGTH, + NONCE_LENGTH, +} from './constants' + +/** + * Generate a random 32-byte hex string for intent hash + */ +export function generateIntentHash(): string { + return Buffer.from(Array.from({ length: INTENT_HASH_LENGTH }, () => Math.floor(Math.random() * 256))).toString('hex') +} + +/** + * Generate a random 32-byte hex string for nonce + */ +export function generateNonce(): string { + return Buffer.from(Array.from({ length: NONCE_LENGTH }, () => Math.floor(Math.random() * 256))).toString('hex') +} + +/** + * Create a test intent with configurable parameters + */ +export async function createTestIntent( + solverSdk: SettlerSDK, + solverProvider: LiteSVMProvider, + options: { + intentHash?: string + nonce?: string + user?: PublicKey + deadline?: number + op?: OpType + minValidations?: number + dataHex?: string + maxFees?: TokenFee[] + eventsHex?: IntentEvent[] + isFinal?: boolean + } = {} +): Promise { + const intentHash = options.intentHash || generateIntentHash() + const nonce = options.nonce || generateNonce() + const user = options.user || Keypair.generate().publicKey + const client = solverProvider.client + const now = Number(client.getClock().unixTimestamp) + const deadline = options.deadline ?? now + INTENT_DEADLINE_OFFSET + + const params: CreateIntentParams = { + op: options.op || OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: options.minValidations ?? DEFAULT_MIN_VALIDATIONS, + dataHex: options.dataHex ?? DEFAULT_DATA_HEX, + maxFees: options.maxFees || [ + { + mint: Keypair.generate().publicKey, + amount: DEFAULT_MAX_FEE, + }, + ], + eventsHex: options.eventsHex || [ + { + topicHex: DEFAULT_TOPIC_HEX, + dataHex: DEFAULT_EVENT_DATA_HEX, + }, + ], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, options.isFinal ?? false) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create intent: ${res.toString()}`) + } + return intentHash +} + +/** + * Create a validated intent (with validations set to meet min_validations requirement) + */ +export async function createValidatedIntent( + solverSdk: SettlerSDK, + solverProvider: LiteSVMProvider, + client: LiteSVM, + options: { + intentHash?: string + minValidations?: number + isFinal?: boolean + deadline?: number + } = {} +): Promise { + const intentHash = await createTestIntent(solverSdk, solverProvider, { + ...options, + isFinal: options.isFinal ?? true, + }) + + // Set validations to meet min_validations requirement + const intentKey = solverSdk.getIntentKey(intentHash) + const intentAccount = client.getAccount(intentKey) + if (intentAccount) { + const intentData = Buffer.from(intentAccount.data) + // validations is at offset: 8 (disc) + 1 (op) + 32 (user) + 32 (intent_creator) + 32 (intent_hash) + 32 (nonce) + 8 (deadline) + 2 (min_validations) = 147 + // validations is u16, so 2 bytes + const minValidations = options.minValidations ?? DEFAULT_MIN_VALIDATIONS + intentData.writeUInt16LE(minValidations, 147) + client.setAccount(intentKey, { + ...intentAccount, + data: intentData, + }) + } + + return intentHash +} + +/** + * Create a finalized proposal + */ +export async function createFinalizedProposal( + solverSdk: SettlerSDK, + solverProvider: LiteSVMProvider, + client: LiteSVM, + program: Program, + options: { + intentHash?: string + deadline?: number + instructions?: ProposalInstruction[] + fees?: TokenFee[] + } = {} +): Promise<{ intentHash: string; proposalKey: PublicKey }> { + const intentHash = + options.intentHash || (await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true })) + const intent = await program.account.intent.fetch(solverSdk.getIntentKey(intentHash)) + const now = Number(client.getClock().unixTimestamp) + const proposalDeadline = options.deadline ?? now + 1800 + + const instructions = options.instructions || [ + { + programId: Keypair.generate().publicKey, + accounts: [ + { + pubkey: Keypair.generate().publicKey, + isSigner: false, + isWritable: true, + }, + ], + data: 'deadbeef', + }, + ] + + const fees = + options.fees || + (intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) as TokenFee[]) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, proposalDeadline, true) + const res = await makeTxSignAndSend(solverProvider, ix) + if (res instanceof FailedTransactionMetadata) { + throw new Error(`Failed to create proposal: ${res.toString()}`) + } + + const proposalKey = solverSdk.getProposalKey(intentHash, solverProvider.wallet.publicKey) + return { intentHash, proposalKey } +} + +/** + * Create a whitelisted entity (validator, axia, or solver) + */ +export async function createWhitelistedEntity( + whitelistSdk: WhitelistSDK, + provider: LiteSVMProvider, + entityType: EntityType, + entityKeypair?: Keypair +): Promise { + const entity = entityKeypair || Keypair.generate() + const whitelistIx = await whitelistSdk.setEntityWhitelistStatusIx( + entityType, + entity.publicKey, + WhitelistStatus.Whitelisted + ) + await makeTxSignAndSend(provider, whitelistIx) + return entity +} + +/** + * Create an Ed25519 signature for a validator (signs intent hash) + */ +export async function createValidatorSignature(intentHash: string, validator: Keypair): Promise { + const signature = await signAsync(Buffer.from(intentHash, 'hex'), validator.secretKey.slice(0, 32)) + return Array.from(new Uint8Array(signature)) +} + +/** + * Create an Ed25519 signature for an axia (signs proposal key) + */ +export async function createAxiaSignature(proposalKey: PublicKey, axia: Keypair): Promise { + const signature = await signAsync(proposalKey.toBuffer(), axia.secretKey.slice(0, 32)) + return Array.from(new Uint8Array(signature)) +} + +/** + * Helper to expect transaction errors consistently + */ +export function expectTransactionError( + res: TransactionMetadata | FailedTransactionMetadata | string, + expectedMessage: string +): void { + expect(typeof res).to.not.be.eq('TransactionMetadata') + + if (typeof res === 'string') { + expect(res).to.include(expectedMessage) + } else { + expect(res.toString()).to.include(expectedMessage) + } +} diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index f493b0e..9ef7418 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -3,7 +3,7 @@ import { Program, Wallet } from '@coral-xyz/anchor' import { signAsync } from '@noble/ed25519' -import { Ed25519Program, Keypair, PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY, TransactionInstruction } from '@solana/web3.js' +import { Ed25519Program, Keypair, SYSVAR_INSTRUCTIONS_PUBKEY, TransactionInstruction } from '@solana/web3.js' import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' import fs from 'fs' @@ -17,6 +17,46 @@ import WhitelistSDK, { EntityType, WhitelistStatus } from '../sdks/whitelist/Whi import * as SettlerIDL from '../target/idl/settler.json' import * as WhitelistIDL from '../target/idl/whitelist.json' import { Settler } from '../target/types/settler' +import { + ACCOUNT_CLOSE_FEE, + DEFAULT_DATA_HEX, + DEFAULT_EVENT_DATA_HEX, + DEFAULT_MAX_FEE, + DEFAULT_MAX_FEE_EXCEED, + DEFAULT_MAX_FEE_HALF, + DEFAULT_MIN_VALIDATIONS, + DEFAULT_TOPIC_HEX, + DOUBLE_CLAIM_DELAY, + DOUBLE_CLAIM_DELAY_PLUS_ONE, + EMPTY_DATA_HEX, + EXPIRATION_TEST_DELAY, + EXPIRATION_TEST_DELAY_PLUS_ONE, + INTENT_DEADLINE_OFFSET, + LONG_DEADLINE, + MEDIUM_DEADLINE, + MULTIPLE_MIN_VALIDATIONS, + PROPOSAL_DEADLINE_OFFSET, + SHORT_DEADLINE, + STALE_CLAIM_DELAY, + STALE_CLAIM_DELAY_PLUS_ONE, + TEST_DATA_HEX_1, + TEST_DATA_HEX_2, + TEST_DATA_HEX_3, + VERY_SHORT_DEADLINE, + WARP_TIME_LONG, + WARP_TIME_SHORT, +} from './helpers/constants' +import { + createAxiaSignature, + createFinalizedProposal, + createTestIntent, + createValidatedIntent, + createValidatorSignature, + createWhitelistedEntity, + expectTransactionError, + generateIntentHash, + generateNonce, +} from './helpers/settler-helpers' import { makeTxSignAndSend, warpSeconds } from './utils' describe('Settler Program', () => { @@ -76,11 +116,11 @@ describe('Settler Program', () => { describe('Settler', () => { describe('initialize', () => { - it('cant initialize if not deployer', async () => { + it('cannot initialize if not deployer', async () => { const ix = await maliciousSdk.initializeIx() const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include(`Only Deployer can call this instruction.`) + expectTransactionError(res, 'Only Deployer can call this instruction.') }) it('should call initialize', async () => { @@ -92,49 +132,39 @@ describe('Settler Program', () => { expect(settings.isPaused).to.be.false }) - it('cant call initialize again', async () => { + it('cannot call initialize again', async () => { const ix = await sdk.initializeIx() const res = await makeTxSignAndSend(provider, ix) - expect(res.toString()).to.include( - `Allocate: account Address { address: ${sdk.getSettlerSettingsPubkey()}, base: None } already in use` - ) + expectTransactionError(res, 'already in use') }) }) describe('create_intent', () => { - const generateIntentHash = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const generateNonce = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - it('should create an intent', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + const deadline = now + INTENT_DEADLINE_OFFSET const params = { op: OpType.Transfer, user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '010203', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: DEFAULT_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [ { - topicHex: Buffer.from(Array(32).fill(1)).toString('hex'), - dataHex: '040506', + topicHex: DEFAULT_TOPIC_HEX, + dataHex: DEFAULT_EVENT_DATA_HEX, }, ], } @@ -148,33 +178,24 @@ describe('Settler Program', () => { expect(intent.intentCreator.toString()).to.be.eq(solver.publicKey.toString()) expect(Buffer.from(intent.nonce).toString('hex')).to.be.eq(nonce) expect(intent.deadline.toNumber()).to.be.eq(deadline) - expect(intent.minValidations).to.be.eq(1) + expect(intent.minValidations).to.be.eq(DEFAULT_MIN_VALIDATIONS) expect(intent.validations).to.be.eq(0) expect(intent.isFinal).to.be.false - expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq('010203') + expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq(DEFAULT_DATA_HEX) expect(intent.maxFees.length).to.be.eq(1) expect(intent.maxFees[0].mint.toString()).to.be.eq(params.maxFees[0].mint.toString()) - expect(intent.maxFees[0].amount.toNumber()).to.be.eq(1000) + expect(intent.maxFees[0].amount.toNumber()).to.be.eq(DEFAULT_MAX_FEE) expect(intent.events.length).to.be.eq(1) expect(intent.validators.length).to.be.eq(0) expect(Buffer.from(intent.events[0].topic).toString('hex')).to.be.eq(params.eventsHex[0].topicHex) - expect(Buffer.from(intent.events[0].data).toString('hex')).to.be.eq('040506') + expect(Buffer.from(intent.events[0].data).toString('hex')).to.be.eq(DEFAULT_EVENT_DATA_HEX) }) it('should create an intent with empty data', async () => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { + const intentHash = await createTestIntent(solverSdk, solverProvider, { op: OpType.Swap, - user, - nonceHex: nonce, - deadline, minValidations: 2, - dataHex: '', + dataHex: EMPTY_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, @@ -182,31 +203,29 @@ describe('Settler Program', () => { }, ], eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, true) - await makeTxSignAndSend(solverProvider, ix) + isFinal: true, + }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.op).to.deep.include({ swap: {} }) - expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq('') + expect(Buffer.from(intent.intentData).toString('hex')).to.be.eq(EMPTY_DATA_HEX) expect(intent.isFinal).to.be.true }) - it('cant create an intent with empty max_fees', async () => { + it('cannot create an intent with empty max_fees', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + const deadline = now + INTENT_DEADLINE_OFFSET const params = { op: OpType.Call, user, nonceHex: nonce, deadline, - minValidations: 3, - dataHex: '070809', + minValidations: MULTIPLE_MIN_VALIDATIONS, + dataHex: TEST_DATA_HEX_1, maxFees: [], eventsHex: [], } @@ -214,117 +233,59 @@ describe('Settler Program', () => { const ix = await solverSdk.createIntentIx(intentHash, params, false) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include('No max fees provided') + expectTransactionError(res, 'No max fees provided') }) it('should create an intent with empty events', async () => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '0a0b0c', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], + const intentHash = await createTestIntent(solverSdk, solverProvider, { + dataHex: TEST_DATA_HEX_2, eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(solverProvider, ix) + }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.events.length).to.be.eq(0) }) it('should create an intent with is_final true', async () => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], + const intentHash = await createTestIntent(solverSdk, solverProvider, { + dataHex: EMPTY_DATA_HEX, eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, true) - await makeTxSignAndSend(solverProvider, ix) + isFinal: true, + }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.isFinal).to.be.true }) it('should create an intent with is_final false', async () => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], + const intentHash = await createTestIntent(solverSdk, solverProvider, { + dataHex: EMPTY_DATA_HEX, eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(solverProvider, ix) + isFinal: false, + }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.isFinal).to.be.false }) - it('cant create intent if not whitelisted solver', async () => { + it('cannot create intent if not whitelisted solver', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + const deadline = now + INTENT_DEADLINE_OFFSET const params = { op: OpType.Transfer, user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: EMPTY_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -332,34 +293,32 @@ describe('Settler Program', () => { const ix = await maliciousSdk.createIntentIx(intentHash, params, false) const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include( - 'AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized' - ) + expectTransactionError(res, 'AccountNotInitialized') const intent = client.getAccount(sdk.getIntentKey(intentHash)) expect(intent).to.be.null }) - it('cant create intent with deadline in the past', async () => { - warpSeconds(provider, 500) + it('cannot create intent with deadline in the past', async () => { + warpSeconds(provider, WARP_TIME_LONG) const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now - 100 + const deadline = now - SHORT_DEADLINE const params = { op: OpType.Transfer, user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: EMPTY_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -368,10 +327,10 @@ describe('Settler Program', () => { const ix = await solverSdk.createIntentIx(intentHash, params, false) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Deadline must be in the future`) + expectTransactionError(res, 'Deadline must be in the future') }) - it('cant create intent with deadline equal to now', async () => { + it('cannot create intent with deadline equal to now', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey @@ -383,12 +342,12 @@ describe('Settler Program', () => { user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: EMPTY_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -397,27 +356,27 @@ describe('Settler Program', () => { const ix = await solverSdk.createIntentIx(intentHash, params, false) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Deadline must be in the future`) + expectTransactionError(res, 'Deadline must be in the future') }) - it('cant create intent if fulfilled_intent PDA already exists', async () => { + it('cannot create intent if fulfilled_intent PDA already exists', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + const deadline = now + INTENT_DEADLINE_OFFSET const params = { op: OpType.Transfer, user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: EMPTY_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -435,62 +394,54 @@ describe('Settler Program', () => { const ix = await solverSdk.createIntentIx(intentHash, params, false) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include( - `AnchorError caused by account: fulfilled_intent. Error Code: AccountNotSystemOwned. Error Number: 3011. Error Message: The given account is not owned by the system program` - ) + expectTransactionError(res, 'AccountNotSystemOwned') }) - it('cant create intent with same intent_hash twice', async () => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + it('cannot create intent with same intent_hash twice', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { + isFinal: false, + }) + client.expireBlockhash() const params = { op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '', + user: Keypair.generate().publicKey, + nonceHex: generateNonce(), + deadline: Number(client.getClock().unixTimestamp) + INTENT_DEADLINE_OFFSET, + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: EMPTY_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], } - - const ix = await solverSdk.createIntentIx(intentHash, params, false) - await makeTxSignAndSend(solverProvider, ix) - - client.expireBlockhash() const ix2 = await solverSdk.createIntentIx(intentHash, params, false) const res = await makeTxSignAndSend(solverProvider, ix2) - expect(res.toString()).to.include(`already in use`) + expectTransactionError(res, 'already in use') }) - it('cant create intent with invalid intent_hash', async () => { + it('cannot create intent with invalid intent_hash', async () => { const invalidIntentHash = '123456' const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + const deadline = now + INTENT_DEADLINE_OFFSET const params = { op: OpType.Transfer, user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: EMPTY_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -507,52 +458,13 @@ describe('Settler Program', () => { }) describe('extend_intent', () => { - const generateIntentHash = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const generateNonce = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const createTestIntent = async (isFinal = false): Promise => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '010203', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], - eventsHex: [ - { - topicHex: Buffer.from(Array(32).fill(1)).toString('hex'), - dataHex: '040506', - }, - ], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - await makeTxSignAndSend(solverProvider, ix) - return intentHash - } - it('should extend an intent with more data', async () => { - const intentHash = await createTestIntent(false) + const intentHash = await createTestIntent(solverSdk, solverProvider, { + isFinal: false, + }) const extendParams = { - moreDataHex: '070809', + moreDataHex: TEST_DATA_HEX_1, } const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) @@ -564,7 +476,7 @@ describe('Settler Program', () => { }) it('should extend an intent with more max_fees', async () => { - const intentHash = await createTestIntent(false) + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) const newMint = Keypair.generate().publicKey const extendParams = { @@ -581,20 +493,20 @@ describe('Settler Program', () => { const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intent.maxFees.length).to.be.eq(2) - expect(intent.maxFees[0].amount.toNumber()).to.be.eq(1000) + expect(intent.maxFees[0].amount.toNumber()).to.be.eq(DEFAULT_MAX_FEE) expect(intent.maxFees[1].mint.toString()).to.be.eq(newMint.toString()) expect(intent.maxFees[1].amount.toNumber()).to.be.eq(2000) }) it('should extend an intent with more events', async () => { - const intentHash = await createTestIntent(false) + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) const newTopic = Buffer.from(Array(32).fill(2)).toString('hex') const extendParams = { moreEventsHex: [ { topicHex: newTopic, - dataHex: '0a0b0c', + dataHex: TEST_DATA_HEX_2, }, ], } @@ -612,7 +524,7 @@ describe('Settler Program', () => { }) it('should extend an intent with all optional fields', async () => { - const intentHash = await createTestIntent(false) + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) const newMint = Keypair.generate().publicKey const newTopic = Buffer.from(Array(32).fill(3)).toString('hex') @@ -644,7 +556,7 @@ describe('Settler Program', () => { }) it('should extend an intent to a large size', async () => { - const intentHash = await createTestIntent(false) + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) const intentKey = sdk.getIntentKey(intentHash) for (let i = 0; i < 100; i++) { @@ -680,7 +592,7 @@ describe('Settler Program', () => { const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const intentAcc = client.getAccount(intentKey) - expect(intent.intentData.length).to.be.eq(3 + 5000) + expect(intent.intentData.length).to.be.eq(3 + 5000) // Keep literal for specific test case expect(intent.maxFees.length).to.be.eq(58) expect(intent.events.length).to.be.eq(51) expect(intent.isFinal).to.be.false @@ -688,7 +600,7 @@ describe('Settler Program', () => { }) it('should finalize an intent', async () => { - const intentHash = await createTestIntent(false) + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) const extendParams = {} @@ -700,7 +612,7 @@ describe('Settler Program', () => { }) it('should extend and finalize an intent in one call', async () => { - const intentHash = await createTestIntent(false) + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) const extendParams = { moreDataHex: '191a1b', @@ -715,7 +627,7 @@ describe('Settler Program', () => { }) it('should extend an intent multiple times', async () => { - const intentHash = await createTestIntent(false) + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) const extendParams1 = { moreDataHex: '1c1d1e', @@ -734,8 +646,8 @@ describe('Settler Program', () => { expect(intent.isFinal).to.be.false }) - it('cant extend intent if not intent creator', async () => { - const intentHash = await createTestIntent(false) + it('cannot extend intent if not intent creator', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) const extendParams = { moreDataHex: '222324', @@ -744,10 +656,10 @@ describe('Settler Program', () => { const ix = await maliciousSdk.extendIntentIx(intentHash, extendParams, false) const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include(`Signer must be intent creator`) + expectTransactionError(res, `Signer must be intent creator`) }) - it('cant extend non-existent intent', async () => { + it('cannot extend non-existent intent', async () => { const intentHash = generateIntentHash() const extendParams = { @@ -757,11 +669,11 @@ describe('Settler Program', () => { const ix = await sdk.extendIntentIx(intentHash, extendParams, false) const res = await makeTxSignAndSend(provider, ix) - expect(res.toString()).to.include(`AccountNotInitialized`) + expectTransactionError(res, `AccountNotInitialized`) }) - it('cant extend intent if already finalized', async () => { - const intentHash = await createTestIntent(true) + it('cannot extend intent if already finalized', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: true }) const extendParams = { moreDataHex: '28292a', @@ -770,68 +682,35 @@ describe('Settler Program', () => { const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Intent is already final`) + expectTransactionError(res, `Intent is already final`) }) - it('cant finalize already finalized intent', async () => { - const intentHash = await createTestIntent(true) + it('cannot finalize already finalized intent', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: true }) const extendParams = {} const ix = await solverSdk.extendIntentIx(intentHash, extendParams, true) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Intent is already final`) + expectTransactionError(res, `Intent is already final`) }) }) describe('claim_stale_intent', () => { - const generateIntentHash = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const generateNonce = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - const createTestIntentWithDeadline = async (deadline: number, isFinal = false): Promise => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '010203', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], - eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - const res = await makeTxSignAndSend(solverProvider, ix) - if (res instanceof FailedTransactionMetadata) { - throw new Error(`Failed to create intent: ${res.toString()}`) - } - return intentHash + return createTestIntent(solverSdk, solverProvider, { deadline, isFinal }) } it('should claim stale intent', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 50 + const deadline = now + STALE_CLAIM_DELAY const intentHash = await createTestIntentWithDeadline(deadline, false) const intentBefore = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) expect(intentBefore).to.not.be.null - warpSeconds(provider, 51) + warpSeconds(provider, STALE_CLAIM_DELAY_PLUS_ONE) const intentBalanceBefore = Number(provider.client.getBalance(sdk.getIntentKey(intentHash))) || 0 const intentCreatorBalanceBefore = Number(provider.client.getBalance(intentBefore.intentCreator)) || 0 @@ -849,64 +728,64 @@ describe('Settler Program', () => { expect(error.message).to.include(`Account does not exist`) } - expect(intentCreatorBalanceAfter).to.be.eq(intentCreatorBalanceBefore + intentBalanceBefore - 5000) + expect(intentCreatorBalanceAfter).to.be.eq(intentCreatorBalanceBefore + intentBalanceBefore - ACCOUNT_CLOSE_FEE) expect(intentBalanceAfter).to.be.eq(0) }) - it('cant claim intent if deadline has not passed', async () => { + it('cannot claim intent if deadline has not passed', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 500 + const deadline = now + LONG_DEADLINE const intentHash = await createTestIntentWithDeadline(deadline, false) - warpSeconds(provider, 100) + warpSeconds(provider, WARP_TIME_SHORT) const ix = await solverSdk.claimStaleIntentIx(intentHash) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Intent not yet expired`) + expectTransactionError(res, 'Intent not yet expired') }) - it('cant claim intent if deadline equals now', async () => { + it('cannot claim intent if deadline equals now', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 300 + const deadline = now + MEDIUM_DEADLINE const intentHash = await createTestIntentWithDeadline(deadline, false) - warpSeconds(provider, 300) + warpSeconds(provider, MEDIUM_DEADLINE) const ix = await solverSdk.claimStaleIntentIx(intentHash) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Intent not yet expired`) + expectTransactionError(res, 'Intent not yet expired') }) - it('cant claim stale intent if not intent creator', async () => { + it('cannot claim stale intent if not intent creator', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 80 + const deadline = now + EXPIRATION_TEST_DELAY const intentHash = await createTestIntentWithDeadline(deadline, false) - warpSeconds(provider, 81) + warpSeconds(provider, EXPIRATION_TEST_DELAY_PLUS_ONE) const ix = await maliciousSdk.claimStaleIntentIx(intentHash) const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include(`Signer must be intent creator`) + expectTransactionError(res, `Signer must be intent creator`) }) - it('cant claim non-existent intent', async () => { + it('cannot claim non-existent intent', async () => { const intentHash = generateIntentHash() const ix = await solverSdk.claimStaleIntentIx(intentHash) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`AccountNotInitialized`) + expectTransactionError(res, `AccountNotInitialized`) }) - it('cant claim intent twice', async () => { + it('cannot claim intent twice', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 90 + const deadline = now + DOUBLE_CLAIM_DELAY const intentHash = await createTestIntentWithDeadline(deadline, false) - warpSeconds(provider, 91) + warpSeconds(provider, DOUBLE_CLAIM_DELAY_PLUS_ONE) const ix = await solverSdk.claimStaleIntentIx(intentHash) await makeTxSignAndSend(solverProvider, ix) @@ -921,65 +800,11 @@ describe('Settler Program', () => { }) describe('create_proposal', () => { - const generateIntentHash = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const generateNonce = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const createValidatedIntent = async (isFinal = true): Promise => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '010203', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], - eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - const res = await makeTxSignAndSend(solverProvider, ix) - if (res instanceof FailedTransactionMetadata) { - throw new Error(`Failed to create intent: ${res.toString()}`) - } - - // Set validations to meet min_validations requirement - const intentKey = sdk.getIntentKey(intentHash) - const intentAccount = client.getAccount(intentKey) - if (intentAccount) { - const intentData = Buffer.from(intentAccount.data) - // validations is at offset: 8 (disc) + 1 (op) + 32 (user) + 32 (intent_creator) + 32 (intent_hash) + 32 (nonce) + 8 (deadline) + 2 (min_validations) = 147 - // validations is u16, so 2 bytes - intentData.writeUInt16LE(1, 147) - client.setAccount(intentKey, { - ...intentAccount, - data: intentData, - }) - } - - return intentHash - } - it('should create a proposal', async () => { - const intentHash = await createValidatedIntent(true) + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { @@ -991,7 +816,7 @@ describe('Settler Program', () => { isWritable: true, }, ], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] @@ -1023,10 +848,10 @@ describe('Settler Program', () => { }) it('should create a proposal with multiple instructions', async () => { - const intentHash = await createValidatedIntent(true) + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { @@ -1084,10 +909,10 @@ describe('Settler Program', () => { }) it('should create a proposal with empty instructions', async () => { - const intentHash = await createValidatedIntent(true) + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET const instructions: any[] = [] @@ -1106,17 +931,17 @@ describe('Settler Program', () => { expect(proposal.instructions.length).to.be.eq(0) }) - it('cant create proposal if not whitelisted solver', async () => { - const intentHash = await createValidatedIntent(true) + it('cannot create proposal if not whitelisted solver', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] @@ -1127,22 +952,20 @@ describe('Settler Program', () => { const ix = await maliciousSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include( - 'AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized' - ) + expectTransactionError(res, 'AccountNotInitialized') }) - it('cant create proposal with deadline in the past', async () => { - const intentHash = await createValidatedIntent(true) + it('cannot create proposal with deadline in the past', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now - 100 + const deadline = now - SHORT_DEADLINE const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] @@ -1154,11 +977,11 @@ describe('Settler Program', () => { const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Deadline must be in the future`) + expectTransactionError(res, `Deadline must be in the future`) }) - it('cant create proposal with deadline equal to now', async () => { - const intentHash = await createValidatedIntent(true) + it('cannot create proposal with deadline equal to now', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) const deadline = now @@ -1167,7 +990,7 @@ describe('Settler Program', () => { { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] @@ -1179,27 +1002,27 @@ describe('Settler Program', () => { const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Deadline must be in the future`) + expectTransactionError(res, `Deadline must be in the future`) }) - it('cant create proposal if intent deadline has passed', async () => { + it('cannot create proposal if intent deadline has passed', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const intentDeadline = now + 100 + const intentDeadline = now + SHORT_DEADLINE const params = { op: OpType.Transfer, user, nonceHex: nonce, deadline: intentDeadline, - minValidations: 1, - dataHex: '010203', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: DEFAULT_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -1227,41 +1050,41 @@ describe('Settler Program', () => { { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] const ix2 = await solverSdk.createProposalIx(intentHash, instructions, [], proposalDeadline) const res = await makeTxSignAndSend(solverProvider, ix2) - expect(res.toString()).to.include(`Intent has already expired`) + expectTransactionError(res, `Intent has already expired`) }) - it('cant create proposal if proposal deadline exceeds intent deadline', async () => { - const intentHash = await createValidatedIntent(true) + it('cannot create proposal if proposal deadline exceeds intent deadline', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intentDeadline = Number((await program.account.intent.fetch(sdk.getIntentKey(intentHash))).deadline) - const proposalDeadline = intentDeadline + 100 + const proposalDeadline = intentDeadline + SHORT_DEADLINE const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] const ix = await solverSdk.createProposalIx(intentHash, instructions, [], proposalDeadline) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Proposal deadline can't be after the Intent's deadline`) + expectTransactionError(res, `Proposal deadline can't be after the Intent's deadline`) }) - it('cant create proposal if intent has insufficient validations', async () => { + it('cannot create proposal if intent has insufficient validations', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + const deadline = now + INTENT_DEADLINE_OFFSET const params = { op: OpType.Transfer, @@ -1269,11 +1092,11 @@ describe('Settler Program', () => { nonceHex: nonce, deadline, minValidations: 2, - dataHex: '010203', + dataHex: DEFAULT_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -1294,32 +1117,32 @@ describe('Settler Program', () => { }) } - const proposalDeadline = now + 1800 + const proposalDeadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] const ix2 = await solverSdk.createProposalIx(intentHash, instructions, [], proposalDeadline) const res = await makeTxSignAndSend(solverProvider, ix2) - expect(res.toString()).to.include(`Intent has insufficient validations`) + expectTransactionError(res, `Intent has insufficient validations`) }) - it('cant create proposal if intent is not final', async () => { - const intentHash = await createValidatedIntent(false) + it('cannot create proposal if intent is not final', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: false }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] @@ -1331,13 +1154,13 @@ describe('Settler Program', () => { const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Intent is not final`) + expectTransactionError(res, `Intent is not final`) }) - it('cant create proposal if fulfilled_intent PDA already exists', async () => { - const intentHash = await createValidatedIntent(true) + it('cannot create proposal if fulfilled_intent PDA already exists', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET // Mock FulfilledIntent const fulfilledIntent = sdk.getFulfilledIntentKey(intentHash) @@ -1353,7 +1176,7 @@ describe('Settler Program', () => { { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] @@ -1365,22 +1188,23 @@ describe('Settler Program', () => { const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include( + expectTransactionError( + res, `AnchorError caused by account: fulfilled_intent. Error Code: AccountNotSystemOwned. Error Number: 3011. Error Message: The given account is not owned by the system program` ) }) - it('cant create proposal with same intent_hash and solver twice', async () => { - const intentHash = await createValidatedIntent(true) + it('cannot create proposal with same intent_hash and solver twice', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] @@ -1396,26 +1220,26 @@ describe('Settler Program', () => { const ix2 = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline) const res = await makeTxSignAndSend(solverProvider, ix2) - expect(res.toString()).to.include(`already in use`) + expectTransactionError(res, `already in use`) }) - it('cant create proposal for non-existent intent', async () => { + it('cannot create proposal for non-existent intent', async () => { const intentHash = generateIntentHash() const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] const ix = await solverSdk.createProposalIx(intentHash, instructions, [], deadline) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`AccountNotInitialized`) + expectTransactionError(res, `AccountNotInitialized`) }) it('should create proposal with fees matching intent max_fees', async () => { @@ -1423,7 +1247,7 @@ describe('Settler Program', () => { const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + const deadline = now + INTENT_DEADLINE_OFFSET const mint = Keypair.generate().publicKey const params = { @@ -1431,12 +1255,12 @@ describe('Settler Program', () => { user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '010203', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: DEFAULT_DATA_HEX, maxFees: [ { mint, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -1457,19 +1281,19 @@ describe('Settler Program', () => { }) } - const proposalDeadline = now + 1800 + const proposalDeadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] const fees = [ { mint, - amount: 500, + amount: DEFAULT_MAX_FEE_HALF, }, ] @@ -1479,15 +1303,15 @@ describe('Settler Program', () => { const proposal = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) expect(proposal.fees.length).to.be.eq(1) expect(proposal.fees[0].mint.toString()).to.be.eq(mint.toString()) - expect(proposal.fees[0].amount.toNumber()).to.be.eq(500) + expect(proposal.fees[0].amount.toNumber()).to.be.eq(DEFAULT_MAX_FEE_HALF) }) - it('cant create proposal with fees exceeding max_fees', async () => { + it('cannot create proposal with fees exceeding max_fees', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + const deadline = now + INTENT_DEADLINE_OFFSET const mint = Keypair.generate().publicKey const params = { @@ -1495,12 +1319,12 @@ describe('Settler Program', () => { user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '010203', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: DEFAULT_DATA_HEX, maxFees: [ { mint, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -1521,19 +1345,19 @@ describe('Settler Program', () => { }) } - const proposalDeadline = now + 1800 + const proposalDeadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] const fees = [ { mint, - amount: 1500, // Exceeds max_fee of 1000 + amount: DEFAULT_MAX_FEE_EXCEED, // Exceeds max_fee }, ] @@ -1544,12 +1368,12 @@ describe('Settler Program', () => { expect(res.toString()).to.match(/FeeAmountExceedsMaxFee|Fee amount exceeds max fee/i) }) - it('cant create proposal with fees having wrong mint', async () => { + it('cannot create proposal with fees having wrong mint', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 + const deadline = now + INTENT_DEADLINE_OFFSET const mint = Keypair.generate().publicKey const wrongMint = Keypair.generate().publicKey @@ -1558,12 +1382,12 @@ describe('Settler Program', () => { user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '010203', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: DEFAULT_DATA_HEX, maxFees: [ { mint, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -1584,19 +1408,19 @@ describe('Settler Program', () => { }) } - const proposalDeadline = now + 1800 + const proposalDeadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] const fees = [ { mint: wrongMint, // Wrong mint - amount: 500, + amount: DEFAULT_MAX_FEE_HALF, }, ] @@ -1609,63 +1433,11 @@ describe('Settler Program', () => { }) describe('add_instructions_to_proposal', () => { - const generateIntentHash = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const generateNonce = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const createValidatedIntent = async (isFinal = true): Promise => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '010203', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], - eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - const res = await makeTxSignAndSend(solverProvider, ix) - if (res instanceof FailedTransactionMetadata) { - throw new Error(`Failed to create intent: ${res.toString()}`) - } - - // Set validations - const intentKey = sdk.getIntentKey(intentHash) - const intentAccount = client.getAccount(intentKey) - if (intentAccount) { - const intentData = Buffer.from(intentAccount.data) - intentData.writeUInt16LE(1, 147) - client.setAccount(intentKey, { - ...intentAccount, - data: intentData, - }) - } - - return intentHash - } - const createTestProposal = async (isFinal = false): Promise => { - const intentHash = await createValidatedIntent(true) + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { @@ -1677,7 +1449,7 @@ describe('Settler Program', () => { isWritable: true, }, ], - data: '010203', + data: DEFAULT_DATA_HEX, }, ] @@ -1780,7 +1552,7 @@ describe('Settler Program', () => { expect(proposal.isFinal).to.be.false }) - it('cant add instructions if not proposal creator', async () => { + it('cannot add instructions if not proposal creator', async () => { const intentHash = await createTestProposal(false) const proposalCreator = (await program.account.proposal.fetch(solverSdk.getProposalKey(intentHash))) .proposalCreator @@ -1801,10 +1573,10 @@ describe('Settler Program', () => { ) const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include(`Signer must be proposal creator`) + expectTransactionError(res, `Signer must be proposal creator`) }) - it('cant add instructions to non-existent proposal', async () => { + it('cannot add instructions to non-existent proposal', async () => { const intentHash = generateIntentHash() const moreInstructions = [ @@ -1818,14 +1590,14 @@ describe('Settler Program', () => { const ix = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`AccountNotInitialized`) + expectTransactionError(res, `AccountNotInitialized`) }) - it('cant add instructions if proposal deadline has passed', async () => { - const intentHash = await createValidatedIntent(true) + it('cannot add instructions if proposal deadline has passed', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 50 + const deadline = now + STALE_CLAIM_DELAY const instructions = [ { @@ -1843,7 +1615,7 @@ describe('Settler Program', () => { const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline, false) await makeTxSignAndSend(solverProvider, ix) - warpSeconds(provider, 51) + warpSeconds(provider, STALE_CLAIM_DELAY_PLUS_ONE) const moreInstructions = [ { @@ -1856,14 +1628,14 @@ describe('Settler Program', () => { const ix2 = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions) const res = await makeTxSignAndSend(solverProvider, ix2) - expect(res.toString()).to.include(`Proposal has already expired`) + expectTransactionError(res, 'Proposal has already expired') }) - it('cant add instructions if proposal deadline equals now', async () => { - const intentHash = await createValidatedIntent(true) + it('cannot add instructions if proposal deadline equals now', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 100 + const deadline = now + SHORT_DEADLINE const instructions = [ { @@ -1881,7 +1653,7 @@ describe('Settler Program', () => { const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline, false) await makeTxSignAndSend(solverProvider, ix) - warpSeconds(provider, 100) + warpSeconds(provider, WARP_TIME_SHORT) const moreInstructions = [ { @@ -1894,10 +1666,10 @@ describe('Settler Program', () => { const ix2 = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions) const res = await makeTxSignAndSend(solverProvider, ix2) - expect(res.toString()).to.include(`Proposal has already expired`) + expectTransactionError(res, 'Proposal has already expired') }) - it('cant add instructions if proposal is final', async () => { + it('cannot add instructions if proposal is final', async () => { const intentHash = await createTestProposal(true) const moreInstructions = [ @@ -1911,7 +1683,7 @@ describe('Settler Program', () => { const ix = await solverSdk.addInstructionsToProposalIx(intentHash, moreInstructions) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Proposal is already final`) + expectTransactionError(res, `Proposal is already final`) }) it('should finalize proposal when adding instructions with finalize=true', async () => { @@ -1973,60 +1745,8 @@ describe('Settler Program', () => { }) describe('claim_stale_proposal', () => { - const generateIntentHash = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const generateNonce = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const createValidatedIntent = async (isFinal = true): Promise => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '010203', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], - eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - const res = await makeTxSignAndSend(solverProvider, ix) - if (res instanceof FailedTransactionMetadata) { - throw new Error(`Failed to create intent: ${res.toString()}`) - } - - // Set validations - const intentKey = sdk.getIntentKey(intentHash) - const intentAccount = client.getAccount(intentKey) - if (intentAccount) { - const intentData = Buffer.from(intentAccount.data) - intentData.writeUInt16LE(1, 147) - client.setAccount(intentKey, { - ...intentAccount, - data: intentData, - }) - } - - return intentHash - } - const createTestProposalWithDeadline = async (deadline: number): Promise => { - const intentHash = await createValidatedIntent(true) + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const instructions = [ @@ -2039,7 +1759,7 @@ describe('Settler Program', () => { isWritable: true, }, ], - data: '010203', + data: DEFAULT_DATA_HEX, }, ] @@ -2055,13 +1775,13 @@ describe('Settler Program', () => { it('should claim stale proposal', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 50 + const deadline = now + STALE_CLAIM_DELAY const intentHash = await createTestProposalWithDeadline(deadline) const proposalBefore = await program.account.proposal.fetch(sdk.getProposalKey(intentHash, solver.publicKey)) expect(proposalBefore).to.not.be.null - warpSeconds(provider, 51) + warpSeconds(provider, STALE_CLAIM_DELAY_PLUS_ONE) const proposalBalanceBefore = Number(provider.client.getBalance(sdk.getProposalKey(intentHash, solver.publicKey))) || 0 @@ -2081,13 +1801,15 @@ describe('Settler Program', () => { expect(error.message).to.include(`Account does not exist`) } - expect(proposalCreatorBalanceAfter).to.be.eq(proposalCreatorBalanceBefore + proposalBalanceBefore - 5000) + expect(proposalCreatorBalanceAfter).to.be.eq( + proposalCreatorBalanceBefore + proposalBalanceBefore - ACCOUNT_CLOSE_FEE + ) expect(proposalBalanceAfter).to.be.eq(0) }) it('should claim multiple stale proposals', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 50 + const deadline = now + STALE_CLAIM_DELAY const intentHashes = await Promise.all( Array.from({ length: 20 }, async () => await createTestProposalWithDeadline(deadline)) ) @@ -2097,7 +1819,7 @@ describe('Settler Program', () => { expect(proposalBefore).to.not.be.null } - warpSeconds(provider, 51) + warpSeconds(provider, STALE_CLAIM_DELAY_PLUS_ONE) const proposalBalancesBefore = intentHashes.reduce( (acc, intentHash) => @@ -2125,64 +1847,66 @@ describe('Settler Program', () => { } } - expect(proposalCreatorBalanceAfter).to.be.eq(proposalCreatorBalanceBefore + proposalBalancesBefore - 5000) + expect(proposalCreatorBalanceAfter).to.be.eq( + proposalCreatorBalanceBefore + proposalBalancesBefore - ACCOUNT_CLOSE_FEE + ) expect(proposalBalancesAfter).to.be.eq(0) }) - it('cant claim proposal if deadline has not passed', async () => { + it('cannot claim proposal if deadline has not passed', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 500 + const deadline = now + LONG_DEADLINE const intentHash = await createTestProposalWithDeadline(deadline) - warpSeconds(provider, 100) + warpSeconds(provider, WARP_TIME_SHORT) const ix = await solverSdk.claimStaleProposalIx([intentHash]) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Proposal not yet expired`) + expectTransactionError(res, `Proposal not yet expired`) }) - it('cant claim proposal if deadline equals now', async () => { + it('cannot claim proposal if deadline equals now', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 300 + const deadline = now + MEDIUM_DEADLINE const intentHash = await createTestProposalWithDeadline(deadline) - warpSeconds(provider, 300) + warpSeconds(provider, MEDIUM_DEADLINE) const ix = await solverSdk.claimStaleProposalIx([intentHash]) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`Proposal not yet expired`) + expectTransactionError(res, `Proposal not yet expired`) }) - it('cant claim stale proposal if not proposal creator', async () => { + it('cannot claim stale proposal if not proposal creator', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 80 + const deadline = now + EXPIRATION_TEST_DELAY const intentHash = await createTestProposalWithDeadline(deadline) - warpSeconds(provider, 81) + warpSeconds(provider, EXPIRATION_TEST_DELAY_PLUS_ONE) const ix = await maliciousSdk.claimStaleProposalIx([intentHash], solver.publicKey) const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include(`Signer must be proposal creator`) + expectTransactionError(res, `Signer must be proposal creator`) }) - it('cant claim non-existent proposal', async () => { + it('cannot claim non-existent proposal', async () => { const intentHash = generateIntentHash() const ix = await solverSdk.claimStaleProposalIx([intentHash]) const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include(`AccountNotInitialized`) + expectTransactionError(res, `AccountNotInitialized`) }) - it('cant claim proposal twice', async () => { + it('cannot claim proposal twice', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 90 + const deadline = now + DOUBLE_CLAIM_DELAY const intentHash = await createTestProposalWithDeadline(deadline) - warpSeconds(provider, 91) + warpSeconds(provider, DOUBLE_CLAIM_DELAY_PLUS_ONE) const ix = await solverSdk.claimStaleProposalIx([intentHash]) await makeTxSignAndSend(solverProvider, ix) @@ -2209,67 +1933,14 @@ describe('Settler Program', () => { await makeTxSignAndSend(provider, whitelistValidatorIx) }) - const generateIntentHash = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const generateNonce = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const createFinalizedIntent = async (minValidations = 1, isFinal = true): Promise => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations, - dataHex: '010203', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], - eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - const res = await makeTxSignAndSend(solverProvider, ix) - if (res instanceof FailedTransactionMetadata) { - throw new Error(`Failed to create intent: ${res.toString()}`) - } - - return intentHash - } - - const createWhitelistedValidator = async (): Promise => { - const validator = Keypair.generate() - const whitelistValidatorIx = await whitelistSdk.setEntityWhitelistStatusIx( - EntityType.Validator, - validator.publicKey, - WhitelistStatus.Whitelisted - ) - await makeTxSignAndSend(provider, whitelistValidatorIx) - return validator - } - - const createSignature = async (intentHash: string, validator: Keypair): Promise => { - const signature = await signAsync(Buffer.from(intentHash, 'hex'), validator.secretKey.slice(0, 32)) - return Array.from(new Uint8Array(signature)) - } - it('should add validator signature successfully', async () => { - const intentHash = await createFinalizedIntent(1, true) + const intentHash = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) const intentKey = sdk.getIntentKey(intentHash) - const signature = await createSignature(intentHash, whitelistedValidator) + const signature = await createValidatorSignature(intentHash, whitelistedValidator) const intentBefore = await program.account.intent.fetch(intentKey) expect(intentBefore.validations).to.be.eq(0) @@ -2290,14 +1961,17 @@ describe('Settler Program', () => { }) it('should add multiple validator signatures', async () => { - const intentHash = await createFinalizedIntent(3, true) + const intentHash = await createTestIntent(solverSdk, solverProvider, { + minValidations: MULTIPLE_MIN_VALIDATIONS, + isFinal: true, + }) const intentKey = sdk.getIntentKey(intentHash) - const validator1 = await createWhitelistedValidator() - const validator2 = await createWhitelistedValidator() - const validator3 = await createWhitelistedValidator() + const validator1 = await createWhitelistedEntity(whitelistSdk, provider, EntityType.Validator) + const validator2 = await createWhitelistedEntity(whitelistSdk, provider, EntityType.Validator) + const validator3 = await createWhitelistedEntity(whitelistSdk, provider, EntityType.Validator) - const signature1 = await createSignature(intentHash, validator1) + const signature1 = await createValidatorSignature(intentHash, validator1) const ixs1 = await solverSdk.addValidatorSigIxs( intentKey, Buffer.from(intentHash, 'hex'), @@ -2310,7 +1984,7 @@ describe('Settler Program', () => { expect(intentAfter1.validations).to.be.eq(1) expect(intentAfter1.validators.length).to.be.eq(1) - const signature2 = await createSignature(intentHash, validator2) + const signature2 = await createValidatorSignature(intentHash, validator2) const ixs2 = await solverSdk.addValidatorSigIxs( intentKey, Buffer.from(intentHash, 'hex'), @@ -2323,7 +1997,7 @@ describe('Settler Program', () => { expect(intentAfter2.validations).to.be.eq(2) expect(intentAfter2.validators.length).to.be.eq(2) - const signature3 = await createSignature(intentHash, validator3) + const signature3 = await createValidatorSignature(intentHash, validator3) const ixs3 = await solverSdk.addValidatorSigIxs( intentKey, Buffer.from(intentHash, 'hex'), @@ -2341,10 +2015,10 @@ describe('Settler Program', () => { }) it('should handle duplicate validator signature gracefully', async () => { - const intentHash = await createFinalizedIntent(2, true) + const intentHash = await createTestIntent(solverSdk, solverProvider, { minValidations: 2, isFinal: true }) const intentKey = sdk.getIntentKey(intentHash) - const signature = await createSignature(intentHash, whitelistedValidator) + const signature = await createValidatorSignature(intentHash, whitelistedValidator) const ixs1 = await solverSdk.addValidatorSigIxs( intentKey, @@ -2373,13 +2047,16 @@ describe('Settler Program', () => { }) it('should handle when min_validations already met', async () => { - const intentHash = await createFinalizedIntent(1, true) + const intentHash = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) const intentKey = sdk.getIntentKey(intentHash) - const validator1 = await createWhitelistedValidator() - const validator2 = await createWhitelistedValidator() + const validator1 = await createWhitelistedEntity(whitelistSdk, provider, EntityType.Validator) + const validator2 = await createWhitelistedEntity(whitelistSdk, provider, EntityType.Validator) - const signature1 = await createSignature(intentHash, validator1) + const signature1 = await createValidatorSignature(intentHash, validator1) const ixs1 = await solverSdk.addValidatorSigIxs( intentKey, Buffer.from(intentHash, 'hex'), @@ -2394,7 +2071,7 @@ describe('Settler Program', () => { expect(intentAfter1.minValidations).to.be.eq(1) client.expireBlockhash() - const signature2 = await createSignature(intentHash, validator2) + const signature2 = await createValidatorSignature(intentHash, validator2) const ixs2 = await solverSdk.addValidatorSigIxs( intentKey, Buffer.from(intentHash, 'hex'), @@ -2409,11 +2086,14 @@ describe('Settler Program', () => { expect(intentAfter2.validators[0].toString()).to.be.eq(validator1.publicKey.toString()) }) - it('cant add signature if intent is not final', async () => { - const intentHash = await createFinalizedIntent(1, false) + it('cannot add signature if intent is not final', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: false, + }) const intentKey = sdk.getIntentKey(intentHash) - const signature = await createSignature(intentHash, whitelistedValidator) + const signature = await createValidatorSignature(intentHash, whitelistedValidator) const ixs = await solverSdk.addValidatorSigIxs( intentKey, @@ -2423,27 +2103,27 @@ describe('Settler Program', () => { ) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include(`Intent is not final`) + expectTransactionError(res, `Intent is not final`) }) - it('cant add signature if intent has expired', async () => { + it('cannot add signature if intent has expired', async () => { const intentHash = generateIntentHash() const nonce = generateNonce() const user = Keypair.generate().publicKey const now = Number(client.getClock().unixTimestamp) - const deadline = now + 100 + const deadline = now + SHORT_DEADLINE const params = { op: OpType.Transfer, user, nonceHex: nonce, deadline, - minValidations: 1, - dataHex: '010203', + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: DEFAULT_DATA_HEX, maxFees: [ { mint: Keypair.generate().publicKey, - amount: 1000, + amount: DEFAULT_MAX_FEE, }, ], eventsHex: [], @@ -2456,7 +2136,7 @@ describe('Settler Program', () => { warpSeconds(provider, 101) - const signature = await createSignature(intentHash, whitelistedValidator) + const signature = await createValidatorSignature(intentHash, whitelistedValidator) const ixs = await solverSdk.addValidatorSigIxs( intentKey, @@ -2466,16 +2146,19 @@ describe('Settler Program', () => { ) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include(`Intent has already expired`) + expectTransactionError(res, `Intent has already expired`) }) - it('cant add signature if validator is not whitelisted', async () => { - const intentHash = await createFinalizedIntent(1, true) + it('cannot add signature if validator is not whitelisted', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) const intentKey = sdk.getIntentKey(intentHash) const validator = Keypair.generate() - const signature = await createSignature(intentHash, validator) + const signature = await createValidatorSignature(intentHash, validator) const ixs = await solverSdk.addValidatorSigIxs( intentKey, @@ -2485,16 +2168,20 @@ describe('Settler Program', () => { ) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include( + expectTransactionError( + res, `AnchorError caused by account: validator_registry. Error Code: AccountNotInitialized.` ) }) - it('cant add signature if solver is not whitelisted', async () => { - const intentHash = await createFinalizedIntent(1, true) + it('cannot add signature if solver is not whitelisted', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) const intentKey = sdk.getIntentKey(intentHash) - const signature = await createSignature(intentHash, whitelistedValidator) + const signature = await createValidatorSignature(intentHash, whitelistedValidator) const ixs = await maliciousSdk.addValidatorSigIxs( intentKey, @@ -2504,16 +2191,17 @@ describe('Settler Program', () => { ) const res = await makeTxSignAndSend(maliciousProvider, ...ixs) - expect(res.toString()).to.include( + expectTransactionError( + res, `AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized.` ) }) - it('cant add signature for non-existent intent', async () => { + it('cannot add signature for non-existent intent', async () => { const intentHash = generateIntentHash() const intentKey = sdk.getIntentKey(intentHash) - const signature = await createSignature(intentHash, whitelistedValidator) + const signature = await createValidatorSignature(intentHash, whitelistedValidator) const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({ message: Buffer.from(intentHash, 'hex'), @@ -2535,11 +2223,14 @@ describe('Settler Program', () => { const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) - expect(res.toString()).to.include(`AccountNotInitialized`) + expectTransactionError(res, `AccountNotInitialized`) }) - it('cant add signature if fulfilled_intent PDA already exists', async () => { - const intentHash = await createFinalizedIntent(1, true) + it('cannot add signature if fulfilled_intent PDA already exists', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) const intentKey = sdk.getIntentKey(intentHash) const fulfilledIntent = sdk.getFulfilledIntentKey(intentHash) @@ -2550,7 +2241,7 @@ describe('Settler Program', () => { data: Buffer.from('595168911b9267f7' + '010000000000000000', 'hex'), }) - const signature = await createSignature(intentHash, whitelistedValidator) + const signature = await createValidatorSignature(intentHash, whitelistedValidator) const ixs = await solverSdk.addValidatorSigIxs( intentKey, @@ -2560,17 +2251,21 @@ describe('Settler Program', () => { ) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include( + expectTransactionError( + res, `AnchorError caused by account: fulfilled_intent. Error Code: AccountNotSystemOwned. Error Number: 3011. Error Message: The given account is not owned by the system program` ) }) - it('cant add signature with wrong intent hash', async () => { - const intentHash = await createFinalizedIntent(1, true) + it('cannot add signature with wrong intent hash', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) const intentKey = sdk.getIntentKey(intentHash) const wrongIntentHash = generateIntentHash() - const signature = await createSignature(wrongIntentHash, whitelistedValidator) + const signature = await createValidatorSignature(wrongIntentHash, whitelistedValidator) const ixs = await solverSdk.addValidatorSigIxs( intentKey, @@ -2580,11 +2275,14 @@ describe('Settler Program', () => { ) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include(`Signature verification failed`) + expectTransactionError(res, `Signature verification failed`) }) - it('cant add signature with invalid signature', async () => { - const intentHash = await createFinalizedIntent(1, true) + it('cannot add signature with invalid signature', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) const intentKey = sdk.getIntentKey(intentHash) const invalidSignature: number[] = Array.from({ length: 64 }, () => Math.floor(Math.random() * 256)) @@ -2602,13 +2300,19 @@ describe('Settler Program', () => { ) }) - it('cant add valid signature but for another intent', async () => { - const intentHash1 = await createFinalizedIntent(1, true) + it('cannot add valid signature but for another intent', async () => { + const intentHash1 = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) const intentKey1 = sdk.getIntentKey(intentHash1) - const intentHash2 = await createFinalizedIntent(1, true) + const intentHash2 = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) - const signature = await createSignature(intentHash2, whitelistedValidator) + const signature = await createValidatorSignature(intentHash2, whitelistedValidator) const ixs = await solverSdk.addValidatorSigIxs( intentKey1, @@ -2618,7 +2322,7 @@ describe('Settler Program', () => { ) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include(`Signature verification failed`) + expectTransactionError(res, `Signature verification failed`) }) }) @@ -2635,128 +2339,10 @@ describe('Settler Program', () => { await makeTxSignAndSend(provider, whitelistAxiaIx) }) - const generateIntentHash = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const generateNonce = (): string => { - return Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256))).toString('hex') - } - - const createValidatedIntent = async (isFinal = true): Promise => { - const intentHash = generateIntentHash() - const nonce = generateNonce() - const user = Keypair.generate().publicKey - const now = Number(client.getClock().unixTimestamp) - const deadline = now + 3600 - - const params = { - op: OpType.Transfer, - user, - nonceHex: nonce, - deadline, - minValidations: 1, - dataHex: '010203', - maxFees: [ - { - mint: Keypair.generate().publicKey, - amount: 1000, - }, - ], - eventsHex: [], - } - - const ix = await solverSdk.createIntentIx(intentHash, params, isFinal) - const res = await makeTxSignAndSend(solverProvider, ix) - if (res instanceof FailedTransactionMetadata) { - throw new Error(`Failed to create intent: ${res.toString()}`) - } - - // Set validations to meet min_validations requirement - const intentKey = sdk.getIntentKey(intentHash) - const intentAccount = client.getAccount(intentKey) - if (intentAccount) { - const intentData = Buffer.from(intentAccount.data) - // validations is at offset: 8 (disc) + 1 (op) + 32 (user) + 32 (intent_creator) + 32 (intent_hash) + 32 (nonce) + 8 (deadline) + 2 (min_validations) = 147 - // validations is u16, so 2 bytes - intentData.writeUInt16LE(1, 147) - client.setAccount(intentKey, { - ...intentAccount, - data: intentData, - }) - } - - return intentHash - } - - const createFinalizedProposal = async ( - deadline?: number - ): Promise<{ intentHash: string; proposalKey: PublicKey }> => { - const intentHash = await createValidatedIntent(true) - const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) - const now = Number(client.getClock().unixTimestamp) - const proposalDeadline = deadline || now + 1800 - - const instructions = [ - { - programId: Keypair.generate().publicKey, - accounts: [ - { - pubkey: Keypair.generate().publicKey, - isSigner: false, - isWritable: true, - }, - ], - data: 'deadbeef', - }, - ] - - const fees = intent.maxFees.map((maxFee) => ({ - mint: maxFee.mint, - amount: maxFee.amount.toNumber(), - })) - - const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, proposalDeadline, true) - const res = await makeTxSignAndSend(solverProvider, ix) - if (res instanceof FailedTransactionMetadata) { - throw new Error(`Failed to create proposal: ${res.toString()}`) - } - - const proposalKey = sdk.getProposalKey(intentHash, solver.publicKey) - return { intentHash, proposalKey } - } - - const createWhitelistedAxia = async (): Promise => { - const axia = Keypair.generate() - const whitelistAxiaIx = await whitelistSdk.setEntityWhitelistStatusIx( - EntityType.Axia, - axia.publicKey, - WhitelistStatus.Whitelisted - ) - await makeTxSignAndSend(provider, whitelistAxiaIx) - return axia - } - - const createWhitelistedValidator = async (): Promise => { - const validator = Keypair.generate() - const whitelistValidatorIx = await whitelistSdk.setEntityWhitelistStatusIx( - EntityType.Validator, - validator.publicKey, - WhitelistStatus.Whitelisted - ) - await makeTxSignAndSend(provider, whitelistValidatorIx) - return validator - } - - const createSignature = async (proposalKey: PublicKey, axia: Keypair): Promise => { - const signature = await signAsync(proposalKey.toBuffer(), axia.secretKey.slice(0, 32)) - return Array.from(new Uint8Array(signature)) - } - it('should add axia signature successfully', async () => { - const { proposalKey } = await createFinalizedProposal() + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) - const signature = await createSignature(proposalKey, whitelistedAxia) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) const proposalBefore = await program.account.proposal.fetch(proposalKey) expect(proposalBefore.isSigned).to.be.false @@ -2769,9 +2355,9 @@ describe('Settler Program', () => { }) it('should handle duplicate signature gracefully', async () => { - const { proposalKey } = await createFinalizedProposal() + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) - const signature = await createSignature(proposalKey, whitelistedAxia) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) const ixs1 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) await makeTxSignAndSend(solverProvider, ...ixs1) @@ -2788,9 +2374,9 @@ describe('Settler Program', () => { }) it('should add signature multiple times safely', async () => { - const { proposalKey } = await createFinalizedProposal() + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) - const signature = await createSignature(proposalKey, whitelistedAxia) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) const ixs1 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) await makeTxSignAndSend(solverProvider, ...ixs1) @@ -2807,17 +2393,17 @@ describe('Settler Program', () => { expect(proposalAfter.isSigned).to.be.true }) - it('cant add signature if proposal is not final', async () => { - const intentHash = await createValidatedIntent(true) + it('cannot add signature if proposal is not final', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) const now = Number(client.getClock().unixTimestamp) - const deadline = now + 1800 + const deadline = now + PROPOSAL_DEADLINE_OFFSET const instructions = [ { programId: Keypair.generate().publicKey, accounts: [], - data: 'deadbeef', + data: TEST_DATA_HEX_3, }, ] @@ -2830,60 +2416,59 @@ describe('Settler Program', () => { await makeTxSignAndSend(solverProvider, ix) const proposalKey = sdk.getProposalKey(intentHash, solver.publicKey) - const signature = await createSignature(proposalKey, whitelistedAxia) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include(`Proposal is not final`) + expectTransactionError(res, 'Proposal is not final') }) - it('cant add signature if proposal has expired', async () => { + it('cannot add signature if proposal has expired', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 50 - const { proposalKey } = await createFinalizedProposal(deadline) + const deadline = now + STALE_CLAIM_DELAY + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program, { deadline }) - warpSeconds(provider, 51) + warpSeconds(provider, STALE_CLAIM_DELAY_PLUS_ONE) - const signature = await createSignature(proposalKey, whitelistedAxia) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include(`Proposal has already expired`) + expectTransactionError(res, 'Proposal has already expired') }) - it('cant add signature if axia is not whitelisted', async () => { - const { proposalKey } = await createFinalizedProposal() + it('cannot add signature if axia is not whitelisted', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) const axia = Keypair.generate() - const signature = await createSignature(proposalKey, axia) + const signature = await createAxiaSignature(proposalKey, axia) const ixs = await solverSdk.addAxiaSigIxs(proposalKey, axia.publicKey, signature) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include( - `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.` - ) + expectTransactionError(res, `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.`) }) - it('cant add signature if solver is not whitelisted', async () => { - const { proposalKey } = await createFinalizedProposal() + it('cannot add signature if solver is not whitelisted', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) - const signature = await createSignature(proposalKey, whitelistedAxia) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) const ixs = await maliciousSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) const res = await makeTxSignAndSend(maliciousProvider, ...ixs) - expect(res.toString()).to.include( + expectTransactionError( + res, `AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized.` ) }) - it('cant add signature for non-existent proposal', async () => { + it('cannot add signature for non-existent proposal', async () => { const proposalKey = Keypair.generate().publicKey - const signature = await createSignature(proposalKey, whitelistedAxia) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({ message: proposalKey.toBuffer(), @@ -2904,31 +2489,32 @@ describe('Settler Program', () => { const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) - expect(res.toString()).to.include( + expectTransactionError( + res, `Program log: AnchorError caused by account: proposal. Error Code: AccountNotInitialized` ) }) - it('cant add signature if proposal deadline equals now', async () => { + it('cannot add signature if proposal deadline equals now', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 100 - const { proposalKey } = await createFinalizedProposal(deadline) + const deadline = now + SHORT_DEADLINE + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program, { deadline }) - warpSeconds(provider, 100) + warpSeconds(provider, WARP_TIME_SHORT) - const signature = await createSignature(proposalKey, whitelistedAxia) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include(`Proposal has already expired`) + expectTransactionError(res, 'Proposal has already expired') }) - it('cant add signature with wrong proposal key', async () => { - const { proposalKey } = await createFinalizedProposal() + it('cannot add signature with wrong proposal key', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) const wrongProposalKey = Keypair.generate().publicKey - const signature = await createSignature(wrongProposalKey, whitelistedAxia) + const signature = await createAxiaSignature(wrongProposalKey, whitelistedAxia) const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) const res = await makeTxSignAndSend(solverProvider, ...ixs) @@ -2938,8 +2524,8 @@ describe('Settler Program', () => { ) }) - it('cant add signature with invalid signature', async () => { - const { proposalKey } = await createFinalizedProposal() + it('cannot add signature with invalid signature', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) const invalidSignature: number[] = Array.from({ length: 64 }, () => Math.floor(Math.random() * 256)) @@ -2951,25 +2537,23 @@ describe('Settler Program', () => { ) }) - it('cant add signature from wrong axia pubkey', async () => { - const { proposalKey } = await createFinalizedProposal() + it('cannot add signature from wrong axia pubkey', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) const wrongAxia = Keypair.generate() - const signature = await createSignature(proposalKey, wrongAxia) + const signature = await createAxiaSignature(proposalKey, wrongAxia) const ixs = await solverSdk.addAxiaSigIxs(proposalKey, wrongAxia.publicKey, signature) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include( - `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.` - ) + expectTransactionError(res, `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.`) }) - it('cant add signature with signature from different axia', async () => { - const { proposalKey } = await createFinalizedProposal() + it('cannot add signature with signature from different axia', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) - const axia2 = await createWhitelistedAxia() - const signature = await createSignature(proposalKey, axia2) + const axia2 = await createWhitelistedEntity(whitelistSdk, provider, EntityType.Axia) + const signature = await createAxiaSignature(proposalKey, axia2) // Try to use axia2's signature but claim it's from whitelistedAxia const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) @@ -2980,22 +2564,20 @@ describe('Settler Program', () => { ) }) - it('cant add signature with signature from validator instead of axia', async () => { - const { proposalKey } = await createFinalizedProposal() + it('cannot add signature with signature from validator instead of axia', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) - const validator = await createWhitelistedValidator() - const signature = await createSignature(proposalKey, validator) + const validator = await createWhitelistedEntity(whitelistSdk, provider, EntityType.Validator) + const signature = await createAxiaSignature(proposalKey, validator) const ixs = await solverSdk.addAxiaSigIxs(proposalKey, validator.publicKey, signature) const res = await makeTxSignAndSend(solverProvider, ...ixs) - expect(res.toString()).to.include( - `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.` - ) + expectTransactionError(res, `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.`) }) - it('cant add signature if signed message is wrong', async () => { - const { proposalKey } = await createFinalizedProposal() + it('cannot add signature if signed message is wrong', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) // Sign a different message (e.g., intent hash instead of proposal key) const intentHash = generateIntentHash() @@ -3021,11 +2603,11 @@ describe('Settler Program', () => { const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) - expect(res.toString()).to.include(`Signature verification failed`) + expectTransactionError(res, `Signature verification failed`) }) - it('cant add signature with corrupted ed25519 instruction', async () => { - const { proposalKey } = await createFinalizedProposal() + it('cannot add signature with corrupted ed25519 instruction', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) // Create corrupted Ed25519 instruction with wrong program ID const corruptedEd25519Ix = new TransactionInstruction({ @@ -3060,10 +2642,10 @@ describe('Settler Program', () => { it('should add signature when proposal deadline is close', async () => { const now = Number(client.getClock().unixTimestamp) - const deadline = now + 10 - const { proposalKey } = await createFinalizedProposal(deadline) + const deadline = now + VERY_SHORT_DEADLINE + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program, { deadline }) - const signature = await createSignature(proposalKey, whitelistedAxia) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) await makeTxSignAndSend(solverProvider, ...ixs) diff --git a/packages/svm/tests/whitelist.test.ts b/packages/svm/tests/whitelist.test.ts index ea4facd..561b37a 100644 --- a/packages/svm/tests/whitelist.test.ts +++ b/packages/svm/tests/whitelist.test.ts @@ -12,6 +12,8 @@ import path from 'path' import WhitelistSDK, { EntityType, WhitelistStatus } from '../sdks/whitelist/Whitelist' import * as WhitelistIDL from '../target/idl/whitelist.json' import { Whitelist } from '../target/types/whitelist' +import { COOLDOWN_PERIOD, COOLDOWN_PERIOD_PLUS_ONE, MAX_COOLDOWN_PLUS_ONE, MIN_COOLDOWN } from './helpers/constants' +import { expectTransactionError } from './helpers/settler-helpers' import { makeTxSignAndSend, warpSeconds } from './utils' describe('Whitelist Program', () => { @@ -68,36 +70,36 @@ describe('Whitelist Program', () => { describe('Whitelist', () => { describe('initialize', () => { - it('cant initialize if not deployer', async () => { + it('cannot initialize if not deployer', async () => { const newAdmin = web3.Keypair.generate().publicKey - const proposedAdminCooldown = 3600 + const proposedAdminCooldown = COOLDOWN_PERIOD const ix = await maliciousSdk.initializeIx(newAdmin, proposedAdminCooldown) const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include(`Only deployer can call this instruction`) + expectTransactionError(res, 'Only deployer can call this instruction') }) - it('cant initialize with cooldown = 0', async () => { - const proposedAdminCooldown = 0 + it('cannot initialize with cooldown = 0', async () => { + const proposedAdminCooldown = MIN_COOLDOWN const ix = await deployerSdk.initializeIx(admin.publicKey, proposedAdminCooldown) const res = await makeTxSignAndSend(deployerProvider, ix) - expect(res.toString()).to.include(`Cooldown can't be zero`) + expectTransactionError(res, "Cooldown can't be zero") }) - it('cant initialize with cooldown > MAX_COOLDOWN', async () => { - const proposedAdminCooldown = 3600 * 24 * 30 + 1 + it('cannot initialize with cooldown > MAX_COOLDOWN', async () => { + const proposedAdminCooldown = MAX_COOLDOWN_PLUS_ONE const ix = await deployerSdk.initializeIx(admin.publicKey, proposedAdminCooldown) const res = await makeTxSignAndSend(deployerProvider, ix) - expect(res.toString()).to.include(`Cooldown too large`) + expectTransactionError(res, 'Cooldown too large') }) it('should initialize', async () => { - const proposedAdminCooldown = 3600 + const proposedAdminCooldown = COOLDOWN_PERIOD const ix = await deployerSdk.initializeIx(admin.publicKey, proposedAdminCooldown) await makeTxSignAndSend(deployerProvider, ix) @@ -105,35 +107,33 @@ describe('Whitelist Program', () => { const settings = await program.account.globalSettings.fetch(deployerSdk.getGlobalSettingsPubkey()) expect(settings.admin.toString()).to.be.eq(admin.publicKey.toString()) expect(settings.proposedAdmin).to.be.null - expect(settings.proposedAdminCooldown.toNumber()).to.be.eq(3600) + expect(settings.proposedAdminCooldown.toNumber()).to.be.eq(COOLDOWN_PERIOD) expect(settings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX }) - it('cant call initialize again', async () => { - const proposedAdminCooldown = 3600 + it('cannot call initialize again', async () => { + const proposedAdminCooldown = COOLDOWN_PERIOD const ix = await deployerSdk.initializeIx(admin.publicKey, proposedAdminCooldown) const res = await makeTxSignAndSend(deployerProvider, ix) - expect(res.toString()).to.include( - `Allocate: account Address { address: ${deployerSdk.getGlobalSettingsPubkey()}, base: None } already in use` - ) + expectTransactionError(res, 'already in use') }) }) describe('propose_admin and set_proposed_admin', () => { - it('cant propose admin if not admin', async () => { + it('cannot propose admin if not admin', async () => { const ix = await maliciousSdk.proposeAdminIx(proposedAdmin.publicKey) const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include(`Only admin can call this instruction`) + expectTransactionError(res, 'Only admin can call this instruction') }) - it('cant set proposed admin if no next admin was proposed yet', async () => { + it('cannot set proposed admin if no next admin was proposed yet', async () => { const ix = await adminSdk.setProposedAdminIx() const res = await makeTxSignAndSend(adminProvider, ix) - expect(res.toString()).to.include(`Only proposed admin can call this instruction`) + expectTransactionError(res, 'Only proposed admin can call this instruction') }) it('should propose admin successfully', async () => { @@ -145,33 +145,34 @@ describe('Whitelist Program', () => { expect(updatedSettings.proposedAdminNextChangeTimestamp.toNumber()).to.be.greaterThan(0) }) - it('cant propose admin if one is already proposed', async () => { + it('cannot propose admin if one is already proposed', async () => { const proposedAdmin2 = web3.Keypair.generate().publicKey const ix = await adminSdk.proposeAdminIx(proposedAdmin2) const res = await makeTxSignAndSend(adminProvider, ix) - expect(res.toString()).to.include(`Proposed admin is already set`) + expectTransactionError(res, 'Proposed admin is already set') }) - it('cant set proposed admin if not proposed admin', async () => { + it('cannot set proposed admin if not proposed admin', async () => { const ix = await maliciousSdk.setProposedAdminIx() const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include(`Only proposed admin can call this instruction`) + expectTransactionError(res, 'Only proposed admin can call this instruction') }) - it('cant set proposed admin if cooldown hasnt passed', async () => { + it('cannot set proposed admin if cooldown hasnt passed', async () => { const ix = await proposedAdminSdk.setProposedAdminIx() const res = await makeTxSignAndSend(proposedAdminProvider, ix) - expect(res.toString()).to.include( - `Can't set proposed admin - either no next admin is proposed or cooldown period is not over yet` + expectTransactionError( + res, + "Can't set proposed admin - either no next admin is proposed or cooldown period is not over yet" ) }) it('should set proposed admin successfully after cooldown', async () => { - warpSeconds(deployerProvider, 3601) + warpSeconds(deployerProvider, COOLDOWN_PERIOD_PLUS_ONE) const ix = await proposedAdminSdk.setProposedAdminIx() await makeTxSignAndSend(proposedAdminProvider, ix) @@ -182,11 +183,11 @@ describe('Whitelist Program', () => { expect(updatedSettings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX }) - it('resets admin to original one for next tests', async () => { + it('should reset admin to original one for next tests', async () => { const ix = await proposedAdminSdk.proposeAdminIx(admin.publicKey) await makeTxSignAndSend(proposedAdminProvider, ix) - warpSeconds(deployerProvider, 3601) + warpSeconds(deployerProvider, COOLDOWN_PERIOD_PLUS_ONE) const ix2 = await adminSdk.setProposedAdminIx() await makeTxSignAndSend(adminProvider, ix2) @@ -197,7 +198,7 @@ describe('Whitelist Program', () => { expect(updatedSettings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX }) - it('can propose same admin as current admin', async () => { + it('should propose same admin as current admin', async () => { const settings = await program.account.globalSettings.fetch(adminSdk.getGlobalSettingsPubkey()) const currentAdmin = settings.admin @@ -207,7 +208,7 @@ describe('Whitelist Program', () => { const updatedSettings = await program.account.globalSettings.fetch(adminSdk.getGlobalSettingsPubkey()) expect(updatedSettings.proposedAdmin?.toString()).to.be.eq(currentAdmin.toString()) - warpSeconds(deployerProvider, 3601) + warpSeconds(deployerProvider, COOLDOWN_PERIOD_PLUS_ONE) await makeTxSignAndSend(adminProvider, await adminSdk.setProposedAdminIx()) const updatedSettings2 = await program.account.globalSettings.fetch(adminSdk.getGlobalSettingsPubkey()) @@ -248,7 +249,7 @@ describe('Whitelist Program', () => { solver2 = web3.Keypair.generate().publicKey }) - it('cant set status if not admin', async () => { + it('cannot set status if not admin', async () => { const ix = await maliciousSdk.setEntityWhitelistStatusIx( EntityType.Validator, validator, @@ -256,7 +257,7 @@ describe('Whitelist Program', () => { ) const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include(`Only admin can call this instruction`) + expectTransactionError(res, 'Only admin can call this instruction') }) it('should set whitelist status successfully (validator)', async () => { @@ -311,8 +312,8 @@ describe('Whitelist Program', () => { expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) }) - it('warps some seconds and changes admin for next tests', async () => { - const diff = 3601 + it('should warp some seconds and change admin for next tests', async () => { + const diff = COOLDOWN_PERIOD_PLUS_ONE const then = Number(client.getClock().unixTimestamp) const ix = await adminSdk.proposeAdminIx(proposedAdmin.publicKey) @@ -439,11 +440,11 @@ describe('Whitelist Program', () => { expect(entityRegistry.updatedBy.toString()).to.be.eq(proposedAdmin.publicKey.toString()) }) - it('resets admin to original one for next tests', async () => { + it('should reset admin to original one for next tests', async () => { const ix = await proposedAdminSdk.proposeAdminIx(admin.publicKey) await makeTxSignAndSend(proposedAdminProvider, ix) - warpSeconds(deployerProvider, 3601) + warpSeconds(deployerProvider, COOLDOWN_PERIOD_PLUS_ONE) const ix2 = await adminSdk.setProposedAdminIx() await makeTxSignAndSend(adminProvider, ix2) @@ -454,7 +455,7 @@ describe('Whitelist Program', () => { expect(updatedSettings.proposedAdminNextChangeTimestamp.toString()).to.be.eq('18446744073709551615') // u64::MAX }) - it('can whitelist another validator', async () => { + it('should whitelist another validator', async () => { const ix = await adminSdk.setEntityWhitelistStatusIx( EntityType.Validator, validator2, @@ -472,7 +473,7 @@ describe('Whitelist Program', () => { expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) }) - it('can whitelist another axia', async () => { + it('should whitelist another axia', async () => { const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Axia, axia2, WhitelistStatus.Whitelisted) await makeTxSignAndSend(adminProvider, ix) @@ -486,7 +487,7 @@ describe('Whitelist Program', () => { expect(entityRegistry.updatedBy.toString()).to.be.eq(admin.publicKey.toString()) }) - it('can whitelist another solver', async () => { + it('should whitelist another solver', async () => { const ix = await adminSdk.setEntityWhitelistStatusIx(EntityType.Solver, solver2, WhitelistStatus.Whitelisted) await makeTxSignAndSend(adminProvider, ix)