From 35e6bfcd663d536fbc4c0d5de786ff82f2cac4ba Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Thu, 29 Apr 2021 22:20:44 -0700 Subject: [PATCH] Init repo --- .gitignore | 2 + .gitmodules | 3 + .travis.yml | 55 ++ Anchor.toml | 6 + Cargo.lock | 1142 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 + README.md | 64 +++ deps/serum-dex | 1 + migrations/deploy.js | 12 + programs/swap/Cargo.toml | 19 + programs/swap/Xargo.toml | 2 + programs/swap/src/lib.rs | 493 ++++++++++++++++ tests/swap.js | 311 +++++++++++ tests/utils/index.js | 510 +++++++++++++++++ 14 files changed, 2627 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .travis.yml create mode 100644 Anchor.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 160000 deps/serum-dex create mode 100644 migrations/deploy.js create mode 100644 programs/swap/Cargo.toml create mode 100644 programs/swap/Xargo.toml create mode 100644 programs/swap/src/lib.rs create mode 100644 tests/swap.js create mode 100644 tests/utils/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d499497 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.anchor/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..31942a0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "deps/serum-dex"] + path = deps/serum-dex + url = https://github.com/project-serum/serum-dex.git diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8ca37e2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,55 @@ +dist: bionic +language: rust +rust: + - stable +env: + global: + - NODE_VERSION="v14.7.0" + - SOLANA_VERSION="v1.6.6" + - ANCHOR_VERSION="v0.4.5" +git: + submodules: true + +before_deploy: + - anchor build --verifiable + - echo "### SHA256 Checksums" > release_notes.md + - sha256sum target/deploy/swap.so > binary.txt + - sha256sum target/idl/swap.json > idl.txt + - cat *.txt >> release_notes.md + - echo "" >> release_notes.md + - echo "Built with Anchor [${ANCHOR_VERSION}](https://github.com/project-serum/anchor/releases/tag/${ANCHOR_VERSION})." >> release_notes.md + +deploy: + provider: releases + edge: true + file: + - "target/deploy/swap.so" + - "target/idl/swap.json" + release_notes_file: release_notes.md + skip_cleanup: true + on: + tags: true + api_key: + secure: 1ixwvPLZd2ZleVAv0QuZcUdZW7JV94TBkCP2+UvJJSqi6kX2HDK5zzVQu5zBM6jOpmpjKUJpLRq6V56/Uj6tGt2w5mfvjeiu3YA5o6WUsMRsvdx+NbnIEIPBfX1ECDhn15FlfC+beGw5oewgjrf59t4Lk9KO5I7VkxdoUJwP0uWRp034fG85JufFziYCkJTobEuCDWthe0p7eym/4rC1nkG7RoOvAwD111QiatEi9AP6nBsi/wokY4u/qvcFjgNxMnpDi7NNLaLLZGDKk/mfpvvnRpYvGRf2xUnzsvSx68m9ycYGNiHrg+YbKVMFa78VbdJepuKheFe1ArpYPd6d0zhybvdsEYUB7PFx/4N5g639NpGX/AgYGtNLGD6pYOBy5cvsrs7JPymcQo1e7HIvtElW8wldSv5964WhCzX19E+/samPiRxVKXtoc3ognebM6IQaxEOtg4aXeV6VPvMvwcSn7V+lrdlMIZSW0Z7RCmsw668icb4yo7Kqt3VCVVuelrN+xvio78twsYcLeNWE2xxbpb1aEd/ZPWndij1+/O7gyheJHH0kkxU8mimrOe+K0XPn6Zk2yfWutsBzC3RdR/1qHR7JKfGN0XbXUrmbSnjcW5M7vVApDglymT9LLajMlvIcQIdS0nwyDfNkJDgVwxJgrTJpXVgfyHScC1AYJO4= + +_defaults: &defaults + before_install: + - nvm install $NODE_VERSION + - npm install -g mocha + - npm install -g @project-serum/anchor + - npm install -g @project-serum/serum + - npm install -g @project-serum/common + - npm install -g @solana/spl-token + - sudo apt-get install -y pkg-config build-essential libudev-dev + - sh -c "$(curl -sSfL https://release.solana.com/${SOLANA_VERSION}/install)" + - export PATH="/home/travis/.local/share/solana/install/active_release/bin:$PATH" + - export NODE_PATH="/home/travis/.nvm/versions/node/${NODE_VERSION}/lib/node_modules/:${NODE_PATH}" + - yes | solana-keygen new + - cargo install --git https://github.com/project-serum/anchor --tag ${ANCHOR_VERSION} anchor-cli --locked + +jobs: + include: + - <<: *defaults + name: Runs the tests + script: + - pushd deps/serum-dex/dex && cargo build-bpf && popd && anchor test diff --git a/Anchor.toml b/Anchor.toml new file mode 100644 index 0000000..171d813 --- /dev/null +++ b/Anchor.toml @@ -0,0 +1,6 @@ +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[[test.genesis]] +address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" +program = "./deps/serum-dex/dex/target/deploy/serum_dex.so" diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ead0c9b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1142 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ahash" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-traits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b2d54853319fd101b8dd81de382bcbf3e03410a64d8928bbee85a3e7dcde483" + +[[package]] +name = "anchor-attribute-access-control" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5753b98b698915b2c102224c1cf319beb625ee9749655f9b36eef37f4bfba8f" +dependencies = [ + "anchor-syn", + "anyhow", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "546290e4fd03da3e617a684d767fe92b7ad9dcf7d5862202211f147221167df6" +dependencies = [ + "anchor-syn", + "anyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659e40c9ea5651254949b21e6e58087a930b1353d02315a6e9115d5e17aa569" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1e2f99012c3eb302e75665d4be3666e7258c476e9a7ab7c267d9a4ba0fa8d1" +dependencies = [ + "anchor-syn", + "anyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anchor-attribute-interface" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c6aa4ad4cb1538d20797c8f6919ca8a8c6edd227526a6f9d1892a2121632f1" +dependencies = [ + "anchor-syn", + "anyhow", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afed272c6e1da83141f58ad793c965a939bf661991a97d18e36ecf8417ba9490" +dependencies = [ + "anchor-syn", + "anyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anchor-attribute-state" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1224797a32c8a2888afc6c62b8476506ce1d7ca984ec9da8c027b1f56fcaf057" +dependencies = [ + "anchor-syn", + "anyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3318d2fe412eda4fe64d424ded7b5cb706cac7e20b3524d7618a727ed33c51af" +dependencies = [ + "anchor-syn", + "anyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anchor-lang" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c07ac8ab867440e446ed30f20114baaa203ce68b25f4c889dd31604ef84565" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-interface", + "anchor-attribute-program", + "anchor-attribute-state", + "anchor-derive-accounts", + "base64", + "borsh", + "bytemuck", + "solana-program", + "thiserror", +] + +[[package]] +name = "anchor-spl" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e23e56970600fc71f346ec42c5e5943f36f936272a4717ca55368e189ece9c4" +dependencies = [ + "anchor-lang", + "lazy_static", + "serum_dex", + "solana-program", + "spl-token", +] + +[[package]] +name = "anchor-syn" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a19a89111e400bb27aad8a57c70b43e8d9442a18a10880fce1fa3af166d4139" +dependencies = [ + "anyhow", + "bs58", + "heck", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "syn", + "thiserror", +] + +[[package]] +name = "anyhow" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "blake3" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9ff35b701f3914bdb8fad3368d822c766ef2858b2583198e41639b936f09d3f" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 0.1.10", + "constant_time_eq", + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "borsh" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a7111f797cc721407885a323fb071636aee57f750b1a4ddc27397eba168a74" +dependencies = [ + "borsh-derive", + "hashbrown", +] + +[[package]] +name = "borsh-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307f3740906bac2c118a8122fe22681232b244f1369273e45f1156b45c43d2dd" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate", + "proc-macro2", + "syn", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2104c73179359431cc98e016998f2f23bc7a05bc53e79741bcba705f30047bc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae29eb8418fcd46f723f8691a2ac06857d31179d33d2f2d91eb13967de97c728" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bs58" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb" + +[[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.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.4", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434e1720189a637d44fe464f4df1e6eb900b4835255b14354497c78af37d9bb8" +dependencies = [ + "byteorder", + "digest 0.8.1", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "enumflags2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "field-offset" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf539fba70056b50f40a22e0da30639518a12ee18c35807858a63b158cb6dde7" +dependencies = [ + "memoffset", + "rustc_version 0.3.3", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "serde", + "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 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash", +] + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "memmap2" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustversion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "safe-transmute" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d95e7284b4bd97e24af76023904cd0157c9cc9da0310beb4139a1e88a748d47" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser 0.7.0", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser 0.10.2", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serum_dex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5614c9e8e72610b17a51f024da2634a03252581689a2efda061190797372c2ef" +dependencies = [ + "arrayref", + "bincode", + "bytemuck", + "byteorder", + "enumflags2", + "field-offset", + "itertools", + "num-traits", + "num_enum", + "safe-transmute", + "serde", + "solana-program", + "spl-token", + "static_assertions", + "thiserror", + "without-alloc", +] + +[[package]] +name = "sha2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "solana-frozen-abi" +version = "1.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10272e9486b3cb41b04e899929c521c5c2a037ba6be1651cff68ad3959f4d1f9" +dependencies = [ + "bs58", + "bv", + "generic-array 0.14.4", + "log", + "memmap2", + "rustc_version 0.2.3", + "serde", + "serde_derive", + "sha2", + "solana-frozen-abi-macro", + "solana-logger", + "thiserror", +] + +[[package]] +name = "solana-frozen-abi-macro" +version = "1.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f4b6a34f19cc4b09da1919ff9810c1a499c7e77fc9d26bea022f69dc965edf" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "rustc_version 0.2.3", + "syn", +] + +[[package]] +name = "solana-logger" +version = "1.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c17fa89f2e5fe988cf95a34df411950db4609f68af8df602371d9b7f83cefa7" +dependencies = [ + "env_logger", + "lazy_static", + "log", +] + +[[package]] +name = "solana-program" +version = "1.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885552ce43e9f2cf13fda274bf2b4ef75c5de6e5e0190f53acb83f84cda739c0" +dependencies = [ + "bincode", + "blake3", + "borsh", + "borsh-derive", + "bs58", + "bv", + "curve25519-dalek", + "hex", + "itertools", + "lazy_static", + "log", + "num-derive", + "num-traits", + "rand", + "rustc_version 0.2.3", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "sha2", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-logger", + "solana-sdk-macro", + "thiserror", +] + +[[package]] +name = "solana-sdk-macro" +version = "1.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8264149655cbbcfa1dccd0dc9f62eb04d6832ec08540fcb81db6f305a21d3b65" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "spl-token" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b795e50d15dfd35aa5460b80a16414503a322be115a417a43db987c5824c6798" +dependencies = [ + "arrayref", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "thiserror", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" + +[[package]] +name = "swap" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl", +] + +[[package]] +name = "syn" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "without-alloc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e34736feff52a0b3e5680927e947a4d8fac1f0b80dc8120b080dd8de24d75e2" +dependencies = [ + "alloc-traits", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..52ed11f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +members = [ + "programs/*" +] +exclude = [ + "deps/serum-dex" +] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..15ec842 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Swap + +Swap provides a convenient API to the Serum DEX for performing instantly +settled token swaps directly on the order book. + +## Developing + +This program requires building the Serum DEX from source, which is done using +git submodules. + +### Install Submodules + +Pull the source + +``` +git submodule init +git submodule update +``` + +### Build the DEX + +Build it + +``` +cd deps/serum-dex/dex/ && cargo build-bpf && cd ../../../ +``` + +### Build + +[Anchor](https://github.com/project-serum/anchor) is used for developoment, and it's +recommended workflow is used here. To get started, see the [guide](https://project-serum.github.io/anchor/getting-started/introduction.html). + +```bash +anchor build --verifiable +``` + +The `--verifiable` flag should be used before deploying so that your build artifacts +can be deterministically generated with docker. + +### Test + +```bash +anchor test +``` + +### Verify + +To verify the program deployed on Solana matches your local source code, install +docker, `cd programs/swap`, and run + +```bash +anchor verify +``` + +A list of build artifacts can be found under [releases](https://github.com/project-serum/swap/releases). + + +### Run the Test + +Run the test + +``` +anchor test +``` diff --git a/deps/serum-dex b/deps/serum-dex new file mode 160000 index 0000000..6690408 --- /dev/null +++ b/deps/serum-dex @@ -0,0 +1 @@ +Subproject commit 66904088599c1a8d42623f6a6d157cec46c8da62 diff --git a/migrations/deploy.js b/migrations/deploy.js new file mode 100644 index 0000000..325cf3d --- /dev/null +++ b/migrations/deploy.js @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@project-serum/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +} diff --git a/programs/swap/Cargo.toml b/programs/swap/Cargo.toml new file mode 100644 index 0000000..9757981 --- /dev/null +++ b/programs/swap/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "swap" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "swap" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = "0.4.5" +anchor-spl = "0.4.5" diff --git a/programs/swap/Xargo.toml b/programs/swap/Xargo.toml new file mode 100644 index 0000000..475fb71 --- /dev/null +++ b/programs/swap/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/swap/src/lib.rs b/programs/swap/src/lib.rs new file mode 100644 index 0000000..6d8182f --- /dev/null +++ b/programs/swap/src/lib.rs @@ -0,0 +1,493 @@ +//! Program to perform instantly settled token swaps on the Serum DEX. +//! +//! Before using any instruction here, a user must first create an open orders +//! account on all markets being used. This only needs to be done once. As a +//! convention established by the DEX, this should be done via the system +//! program create account instruction in the same transaction as the user's +//! first trade. Then, the DEX will lazily initialize the open orders account. + +use anchor_lang::prelude::*; +use anchor_spl::dex; +use anchor_spl::dex::serum_dex::instruction::SelfTradeBehavior; +use anchor_spl::dex::serum_dex::matching::{OrderType, Side as SerumSide}; +use anchor_spl::dex::serum_dex::state::MarketState; +use anchor_spl::token; +use std::num::NonZeroU64; + +#[program] +pub mod swap { + use super::*; + + /// Swaps two tokens on a single A/B market, where A is the base currency + /// and B is the quote currency. This is just a direct IOC trade that + /// instantly settles. + /// + /// When side is "bid", then swaps B for A. When side is "ask", then swaps + /// A for B. + /// + /// Arguments: + /// + /// * `side` - The direction to swap. + /// * `amount` - The amount to swap *from* + /// * `min_expected_swap_amount` - The minimum amount of the *to* token the + /// client expects to receive from the swap. The instruction fails if + /// execution would result in less. + #[access_control(is_valid_swap(&ctx))] + pub fn swap<'info>( + ctx: Context<'_, '_, '_, 'info, Swap<'info>>, + side: Side, + amount: u64, + min_expected_swap_amount: u64, + ) -> Result<()> { + // Optional referral account (earns a referral fee). + let referral = ctx.remaining_accounts.iter().next().map(Clone::clone); + + // Side determines swap direction. + let (from_token, to_token) = match side { + Side::Bid => (&ctx.accounts.pc_wallet, &ctx.accounts.market.coin_wallet), + Side::Ask => (&ctx.accounts.market.coin_wallet, &ctx.accounts.pc_wallet), + }; + + // Token balances before the trade. + let from_amount_before = token::accessor::amount(from_token)?; + let to_amount_before = token::accessor::amount(to_token)?; + + // Execute trade. + let orderbook: OrderbookClient<'info> = (&*ctx.accounts).into(); + match side { + Side::Bid => orderbook.buy(amount, referral.clone())?, + Side::Ask => orderbook.sell(amount, referral.clone())?, + }; + orderbook.settle(referral)?; + + // Token balances after the trade. + let from_amount_after = token::accessor::amount(from_token)?; + let to_amount_after = token::accessor::amount(to_token)?; + + // Calculate the delta, i.e. the amount swapped. + let from_amount = from_amount_before.checked_sub(from_amount_after).unwrap(); + let to_amount = to_amount_after.checked_sub(to_amount_before).unwrap(); + + // Safety checks. + apply_risk_checks(DidSwap { + authority: *ctx.accounts.authority.key, + given_amount: amount, + min_expected_swap_amount, + from_amount, + to_amount, + spill_amount: 0, + from_mint: token::accessor::mint(from_token)?, + to_mint: token::accessor::mint(to_token)?, + quote_mint: match side { + Side::Bid => token::accessor::mint(from_token)?, + Side::Ask => token::accessor::mint(to_token)?, + }, + })?; + + Ok(()) + } + + /// Swaps two base currencies across two different markets. + /// + /// That is, suppose there are two markets, A/USD(x) and B/USD(x). + /// Then swaps token A for token B via + /// + /// * IOC (immediate or cancel) sell order on A/USD(x) market. + /// * Settle open orders to get USD(x). + /// * IOC buy order on B/USD(x) market to convert USD(x) to token B. + /// * Settle open orders to get token B. + /// + /// Arguments: + /// + /// * `amount` - The amount to swap *from*. + /// * `min_expected_swap_amount - The minimum amount of the *to* token the + /// client expects to receive from the swap. The instruction fails if + /// execution would result in less. + #[access_control(is_valid_swap_transitive(&ctx))] + pub fn swap_transitive<'info>( + ctx: Context<'_, '_, '_, 'info, SwapTransitive<'info>>, + amount: u64, + min_expected_swap_amount: u64, + ) -> Result<()> { + // Optional referral account (earns a referral fee). + let referral = ctx.remaining_accounts.iter().next().map(Clone::clone); + + // Leg 1: Sell Token A for USD(x) (or whatever quote currency is used). + let (from_amount, sell_proceeds) = { + // Token balances before the trade. + let base_before = token::accessor::amount(&ctx.accounts.from.coin_wallet)?; + let quote_before = token::accessor::amount(&ctx.accounts.pc_wallet)?; + + // Execute the trade. + let orderbook = ctx.accounts.orderbook_from(); + orderbook.sell(amount, referral.clone())?; + orderbook.settle(referral.clone())?; + + // Token balances after the trade. + let base_after = token::accessor::amount(&ctx.accounts.from.coin_wallet)?; + let quote_after = token::accessor::amount(&ctx.accounts.pc_wallet)?; + + // Report the delta. + ( + base_before.checked_sub(base_after).unwrap(), + quote_after.checked_sub(quote_before).unwrap(), + ) + }; + + // Leg 2: Buy Token B with USD(x) (or whatever quote currency is used). + let (to_amount, spill_amount) = { + // Token balances before the trade. + let base_before = token::accessor::amount(&ctx.accounts.to.coin_wallet)?; + let quote_before = token::accessor::amount(&ctx.accounts.pc_wallet)?; + + // Execute the trade. + let orderbook = ctx.accounts.orderbook_to(); + orderbook.buy(sell_proceeds, referral.clone())?; + orderbook.settle(referral)?; + + // Token balances after the trade. + let base_after = token::accessor::amount(&ctx.accounts.to.coin_wallet)?; + let quote_after = token::accessor::amount(&ctx.accounts.pc_wallet)?; + + // Report the delta. + ( + base_after.checked_sub(base_before).unwrap(), + quote_before.checked_sub(quote_after).unwrap(), + ) + }; + + // Safety checks. + apply_risk_checks(DidSwap { + given_amount: amount, + min_expected_swap_amount, + from_amount, + to_amount, + spill_amount, + from_mint: token::accessor::mint(&ctx.accounts.from.coin_wallet)?, + to_mint: token::accessor::mint(&ctx.accounts.to.coin_wallet)?, + quote_mint: token::accessor::mint(&ctx.accounts.pc_wallet)?, + authority: *ctx.accounts.authority.key, + })?; + + Ok(()) + } +} + +// Asserts the swap event is valid. +fn apply_risk_checks(event: DidSwap) -> Result<()> { + // Reject if the resulting amount is less than the client's expectation. + if event.to_amount < event.min_expected_swap_amount { + return Err(ErrorCode::SlippageExceeded.into()); + } + emit!(event); + Ok(()) +} + +// The only constraint imposed on these accounts is that the market's base +// currency mint is not equal to the quote currency's. All other checks are +// done by the DEX on CPI. +#[derive(Accounts)] +pub struct Swap<'info> { + market: MarketAccounts<'info>, + #[account(signer)] + authority: AccountInfo<'info>, + #[account(mut)] + pc_wallet: AccountInfo<'info>, + // Programs. + dex_program: AccountInfo<'info>, + token_program: AccountInfo<'info>, + // Sysvars. + rent: AccountInfo<'info>, +} + +impl<'info> From<&Swap<'info>> for OrderbookClient<'info> { + fn from(accounts: &Swap<'info>) -> OrderbookClient<'info> { + OrderbookClient { + market: accounts.market.clone(), + authority: accounts.authority.clone(), + pc_wallet: accounts.pc_wallet.clone(), + dex_program: accounts.dex_program.clone(), + token_program: accounts.token_program.clone(), + rent: accounts.rent.clone(), + } + } +} + +// The only constraint imposed on these accounts is that the from market's +// base currency's is not equal to the to market's base currency. All other +// checks are done by the DEX on CPI (and the quote currency is ensured to be +// the same on both markets since there's only one account field for it). +#[derive(Accounts)] +pub struct SwapTransitive<'info> { + from: MarketAccounts<'info>, + to: MarketAccounts<'info>, + // Must be the authority over all open orders accounts used. + #[account(signer)] + authority: AccountInfo<'info>, + #[account(mut)] + pc_wallet: AccountInfo<'info>, + // Programs. + dex_program: AccountInfo<'info>, + token_program: AccountInfo<'info>, + // Sysvars. + rent: AccountInfo<'info>, +} + +impl<'info> SwapTransitive<'info> { + fn orderbook_from(&self) -> OrderbookClient<'info> { + OrderbookClient { + market: self.from.clone(), + authority: self.authority.clone(), + pc_wallet: self.pc_wallet.clone(), + dex_program: self.dex_program.clone(), + token_program: self.token_program.clone(), + rent: self.rent.clone(), + } + } + fn orderbook_to(&self) -> OrderbookClient<'info> { + OrderbookClient { + market: self.to.clone(), + authority: self.authority.clone(), + pc_wallet: self.pc_wallet.clone(), + dex_program: self.dex_program.clone(), + token_program: self.token_program.clone(), + rent: self.rent.clone(), + } + } +} + +// Client for sending orders to the Serum DEX. +struct OrderbookClient<'info> { + market: MarketAccounts<'info>, + authority: AccountInfo<'info>, + pc_wallet: AccountInfo<'info>, + dex_program: AccountInfo<'info>, + token_program: AccountInfo<'info>, + rent: AccountInfo<'info>, +} + +impl<'info> OrderbookClient<'info> { + // Executes the sell order portion of the swap, purchasing as much of the + // quote currency as possible for the given `base_amount`. + // + // `base_amount` is the "native" amount of the base currency, i.e., token + // amount including decimals. + fn sell(&self, base_amount: u64, referral: Option>) -> ProgramResult { + let limit_price = 1; + let max_coin_qty = { + // The loaded market must be dropped before CPI. + let market = MarketState::load(&self.market.market, &dex::ID)?; + coin_lots(&market, base_amount) + }; + let max_native_pc_qty = u64::MAX; + self.order_cpi( + limit_price, + max_coin_qty, + max_native_pc_qty, + Side::Ask, + referral, + ) + } + + // Executes the buy order portion of the swap, purchasing as much of the + // base currency as possible, for the given `quote_amount`. + // + // `quote_amount` is the "native" amount of the quote currency, i.e., token + // amount including decimals. + fn buy(&self, quote_amount: u64, referral: Option>) -> ProgramResult { + let limit_price = u64::MAX; + let max_coin_qty = u64::MAX; + let max_native_pc_qty = quote_amount; + self.order_cpi( + limit_price, + max_coin_qty, + max_native_pc_qty, + Side::Bid, + referral, + ) + } + + // Executes a new order on the serum dex via CPI. + // + // * `limit_price` - the limit order price in lot units. + // * `max_coin_qty`- the max number of the base currency lot units. + // * `max_native_pc_qty` - the max number of quote currency in native token + // units (includes decimals). + // * `side` - bid or ask, i.e. the type of order. + // * `referral` - referral account, earning a fee. + fn order_cpi( + &self, + limit_price: u64, + max_coin_qty: u64, + max_native_pc_qty: u64, + side: Side, + referral: Option>, + ) -> ProgramResult { + // Client order id is only used for cancels. Not used here so hardcode. + let client_order_id = 0; + // Limit is the dex's custom compute budge parameter, setting an upper + // bound on the number of matching cycles the program can perform + // before giving up and posting the remaining unmatched order. + let limit = 65535; + + let dex_accs = dex::NewOrderV3 { + market: self.market.market.clone(), + open_orders: self.market.open_orders.clone(), + request_queue: self.market.request_queue.clone(), + event_queue: self.market.event_queue.clone(), + market_bids: self.market.bids.clone(), + market_asks: self.market.asks.clone(), + order_payer_token_account: self.market.order_payer_token_account.clone(), + open_orders_authority: self.authority.clone(), + coin_vault: self.market.coin_vault.clone(), + pc_vault: self.market.pc_vault.clone(), + token_program: self.token_program.clone(), + rent: self.rent.clone(), + }; + let mut ctx = CpiContext::new(self.dex_program.clone(), dex_accs); + if let Some(referral) = referral { + ctx = ctx.with_remaining_accounts(vec![referral]); + } + dex::new_order_v3( + ctx, + side.into(), + NonZeroU64::new(limit_price).unwrap(), + NonZeroU64::new(max_coin_qty).unwrap(), + NonZeroU64::new(max_native_pc_qty).unwrap(), + SelfTradeBehavior::DecrementTake, + OrderType::ImmediateOrCancel, + client_order_id, + limit, + ) + } + + fn settle(&self, referral: Option>) -> ProgramResult { + let settle_accs = dex::SettleFunds { + market: self.market.market.clone(), + open_orders: self.market.open_orders.clone(), + open_orders_authority: self.authority.clone(), + coin_vault: self.market.coin_vault.clone(), + pc_vault: self.market.pc_vault.clone(), + coin_wallet: self.market.coin_wallet.clone(), + pc_wallet: self.pc_wallet.clone(), + vault_signer: self.market.vault_signer.clone(), + token_program: self.token_program.clone(), + }; + let mut ctx = CpiContext::new(self.dex_program.clone(), settle_accs); + if let Some(referral) = referral { + ctx = ctx.with_remaining_accounts(vec![referral]); + } + dex::settle_funds(ctx) + } +} + +// Returns the amount of lots for the base currency of a trade with `size`. +fn coin_lots(market: &MarketState, size: u64) -> u64 { + size.checked_div(market.coin_lot_size).unwrap() +} + +// Market accounts are the accounts used to place orders against the dex minus +// common accounts, i.e., program ids, sysvars, and the `pc_wallet`. +#[derive(Accounts, Clone)] +pub struct MarketAccounts<'info> { + #[account(mut)] + market: AccountInfo<'info>, + #[account(mut)] + open_orders: AccountInfo<'info>, + #[account(mut)] + request_queue: AccountInfo<'info>, + #[account(mut)] + event_queue: AccountInfo<'info>, + #[account(mut)] + bids: AccountInfo<'info>, + #[account(mut)] + asks: AccountInfo<'info>, + // The `spl_token::Account` that funds will be taken from, i.e., transferred + // from the user into the market's vault. + // + // For bids, this is the base currency. For asks, the quote. + #[account(mut)] + order_payer_token_account: AccountInfo<'info>, + // Also known as the "base" currency. For a given A/B market, + // this is the vault for the A mint. + #[account(mut)] + coin_vault: AccountInfo<'info>, + // Also known as the "quote" currency. For a given A/B market, + // this is the vault for the B mint. + #[account(mut)] + pc_vault: AccountInfo<'info>, + // PDA owner of the DEX's token accounts for base + quote currencies. + vault_signer: AccountInfo<'info>, + // User wallets. + #[account(mut)] + coin_wallet: AccountInfo<'info>, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub enum Side { + Bid, + Ask, +} + +impl From for SerumSide { + fn from(side: Side) -> SerumSide { + match side { + Side::Bid => SerumSide::Bid, + Side::Ask => SerumSide::Ask, + } + } +} + +// Access control modifiers. + +fn is_valid_swap(ctx: &Context) -> Result<()> { + _is_valid_swap(&ctx.accounts.market.coin_wallet, &ctx.accounts.pc_wallet) +} + +fn is_valid_swap_transitive(ctx: &Context) -> Result<()> { + _is_valid_swap(&ctx.accounts.from.coin_wallet, &ctx.accounts.to.coin_wallet) +} + +// Validates the tokens being swapped are of different mints. +fn _is_valid_swap<'info>(from: &AccountInfo<'info>, to: &AccountInfo<'info>) -> Result<()> { + let from_token_mint = token::accessor::mint(from)?; + let to_token_mint = token::accessor::mint(to)?; + if from_token_mint == to_token_mint { + return Err(ErrorCode::SwapTokensCannotMatch.into()); + } + Ok(()) +} + +// Event emitted when a swap occurs for two base currencies on two different +// markets (quoted in the same token). +#[event] +pub struct DidSwap { + // User given (max) amount to swap. + pub given_amount: u64, + // The minimum amount of the *to* token expected to be received from + // executing the swap. + pub min_expected_swap_amount: u64, + // Amount of the `from` token sold. + pub from_amount: u64, + // Amount of the `to` token purchased. + pub to_amount: u64, + // Amount of the quote currency accumulated from the swap. + pub spill_amount: u64, + // Mint sold. + pub from_mint: Pubkey, + // Mint purchased. + pub to_mint: Pubkey, + // Mint of the token used as the quote currency in the two markets used + // for swapping. + pub quote_mint: Pubkey, + // User that signed the transaction. + pub authority: Pubkey, +} + +#[error] +pub enum ErrorCode { + #[msg("The tokens being swapped must have different mints")] + SwapTokensCannotMatch, + #[msg("Slippage tolerance exceeded")] + SlippageExceeded, +} diff --git a/tests/swap.js b/tests/swap.js new file mode 100644 index 0000000..6b90686 --- /dev/null +++ b/tests/swap.js @@ -0,0 +1,311 @@ +const assert = require("assert"); +const anchor = require("@project-serum/anchor"); +const BN = anchor.BN; +const OpenOrders = require("@project-serum/serum").OpenOrders; +const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID; +const serumCmn = require("@project-serum/common"); +const utils = require("./utils"); + +// Taker fee rate (bps). +const TAKER_FEE = 0.0022; + +describe("swap", () => { + // Configure the client to use the local cluster. + anchor.setProvider(anchor.Provider.env()); + + // Swap program client. + const program = anchor.workspace.Swap; + + // Accounts used to setup the orderbook. + let ORDERBOOK_ENV, + // Accounts used for A -> USDC swap transactions. + SWAP_A_USDC_ACCOUNTS, + // Accounts used for USDC -> A swap transactions. + SWAP_USDC_A_ACCOUNTS, + // Serum DEX vault PDA for market A/USDC. + marketAVaultSigner, + // Serum DEX vault PDA for market B/USDC. + marketBVaultSigner; + + // Open orders accounts on the two markets for the provider. + const openOrdersA = new anchor.web3.Account(); + const openOrdersB = new anchor.web3.Account(); + + it("BOILERPLATE: Sets up two markets with resting orders", async () => { + ORDERBOOK_ENV = await utils.setupTwoMarkets({ + provider: program.provider, + }); + }); + + it("BOILERPLATE: Sets up reusable accounts", async () => { + const marketA = ORDERBOOK_ENV.marketA; + const marketB = ORDERBOOK_ENV.marketB; + + const [vaultSignerA] = await utils.getVaultOwnerAndNonce( + marketA._decoded.ownAddress + ); + const [vaultSignerB] = await utils.getVaultOwnerAndNonce( + marketB._decoded.ownAddress + ); + marketAVaultSigner = vaultSignerA; + marketBVaultSigner = vaultSignerB; + + SWAP_USDC_A_ACCOUNTS = { + market: { + market: marketA._decoded.ownAddress, + requestQueue: marketA._decoded.requestQueue, + eventQueue: marketA._decoded.eventQueue, + bids: marketA._decoded.bids, + asks: marketA._decoded.asks, + coinVault: marketA._decoded.baseVault, + pcVault: marketA._decoded.quoteVault, + vaultSigner: marketAVaultSigner, + // User params. + openOrders: openOrdersA.publicKey, + orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc, + coinWallet: ORDERBOOK_ENV.godA, + }, + pcWallet: ORDERBOOK_ENV.godUsdc, + authority: program.provider.wallet.publicKey, + dexProgram: utils.DEX_PID, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }; + SWAP_A_USDC_ACCOUNTS = { + ...SWAP_USDC_A_ACCOUNTS, + market: { + ...SWAP_USDC_A_ACCOUNTS.market, + orderPayerTokenAccount: ORDERBOOK_ENV.godA, + }, + }; + }); + + it("Swaps from USDC to Token A", async () => { + const marketA = ORDERBOOK_ENV.marketA; + + // Swap exactly enough USDC to get 1.2 A tokens (best offer price is 6.041 USDC). + const expectedResultantAmount = 7.2; + const bestOfferPrice = 6.041; + const amountToSpend = expectedResultantAmount * bestOfferPrice; + const swapAmount = new BN((amountToSpend / (1 - TAKER_FEE)) * 10 ** 6); + + const [tokenAChange, usdcChange] = await withBalanceChange( + program.provider, + [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godUsdc], + async () => { + await program.rpc.swap(Side.Bid, swapAmount, new BN(1.0), { + accounts: SWAP_USDC_A_ACCOUNTS, + instructions: [ + // First order to this market so one must create the open orders account. + await OpenOrders.makeCreateAccountTransaction( + program.provider.connection, + marketA._decoded.ownAddress, + program.provider.wallet.publicKey, + openOrdersA.publicKey, + utils.DEX_PID + ), + // Might as well create the second open orders account while we're here. + // In prod, this should actually be done within the same tx as an + // order to market B. + await OpenOrders.makeCreateAccountTransaction( + program.provider.connection, + ORDERBOOK_ENV.marketB._decoded.ownAddress, + program.provider.wallet.publicKey, + openOrdersB.publicKey, + utils.DEX_PID + ), + ], + signers: [openOrdersA, openOrdersB], + }); + } + ); + + assert.ok(tokenAChange === expectedResultantAmount); + assert.ok(usdcChange === -swapAmount.toNumber() / 10 ** 6); + }); + + it("Swaps from Token A to USDC", async () => { + const marketA = ORDERBOOK_ENV.marketA; + + // Swap out A tokens for USDC. + const swapAmount = 8.1; + const bestBidPrice = 6.004; + const amountToFill = swapAmount * bestBidPrice; + const takerFee = 0.0022; + const resultantAmount = new BN(amountToFill * (1 - TAKER_FEE) * 10 ** 6); + + const [tokenAChange, usdcChange] = await withBalanceChange( + program.provider, + [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godUsdc], + async () => { + await program.rpc.swap( + Side.Ask, + new BN(swapAmount * 10 ** 6), + new BN(swapAmount), + { + accounts: SWAP_A_USDC_ACCOUNTS, + } + ); + } + ); + + assert.ok(tokenAChange === -swapAmount); + assert.ok(usdcChange === resultantAmount.toNumber() / 10 ** 6); + }); + + it("Swaps from Token A to Token B", async () => { + const marketA = ORDERBOOK_ENV.marketA; + const marketB = ORDERBOOK_ENV.marketB; + const swapAmount = 10; + const [tokenAChange, tokenBChange, usdcChange] = await withBalanceChange( + program.provider, + [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godB, ORDERBOOK_ENV.godUsdc], + async () => { + // Perform the actual swap. + await program.rpc.swapTransitive( + new BN(swapAmount * 10 ** 6), + new BN(swapAmount - 1), + { + accounts: { + from: { + market: marketA._decoded.ownAddress, + requestQueue: marketA._decoded.requestQueue, + eventQueue: marketA._decoded.eventQueue, + bids: marketA._decoded.bids, + asks: marketA._decoded.asks, + coinVault: marketA._decoded.baseVault, + pcVault: marketA._decoded.quoteVault, + vaultSigner: marketAVaultSigner, + // User params. + openOrders: openOrdersA.publicKey, + // Swapping from A -> USDC. + orderPayerTokenAccount: ORDERBOOK_ENV.godA, + coinWallet: ORDERBOOK_ENV.godA, + }, + to: { + market: marketB._decoded.ownAddress, + requestQueue: marketB._decoded.requestQueue, + eventQueue: marketB._decoded.eventQueue, + bids: marketB._decoded.bids, + asks: marketB._decoded.asks, + coinVault: marketB._decoded.baseVault, + pcVault: marketB._decoded.quoteVault, + vaultSigner: marketBVaultSigner, + // User params. + openOrders: openOrdersB.publicKey, + // Swapping from USDC -> B. + orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc, + coinWallet: ORDERBOOK_ENV.godB, + }, + pcWallet: ORDERBOOK_ENV.godUsdc, + authority: program.provider.wallet.publicKey, + dexProgram: utils.DEX_PID, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + } + ); + } + ); + + assert.ok(tokenAChange === -swapAmount); + // TODO: calculate this dynamically from the swap amount. + assert.ok(tokenBChange === 9.8); + assert.ok(usdcChange === 0); + }); + + it("Swaps from Token B to Token A", async () => { + const marketA = ORDERBOOK_ENV.marketA; + const marketB = ORDERBOOK_ENV.marketB; + const swapAmount = 23; + const [tokenAChange, tokenBChange, usdcChange] = await withBalanceChange( + program.provider, + [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godB, ORDERBOOK_ENV.godUsdc], + async () => { + // Perform the actual swap. + await program.rpc.swapTransitive( + new BN(swapAmount * 10 ** 6), + new BN(swapAmount - 1), + { + accounts: { + from: { + market: marketB._decoded.ownAddress, + requestQueue: marketB._decoded.requestQueue, + eventQueue: marketB._decoded.eventQueue, + bids: marketB._decoded.bids, + asks: marketB._decoded.asks, + coinVault: marketB._decoded.baseVault, + pcVault: marketB._decoded.quoteVault, + vaultSigner: marketBVaultSigner, + // User params. + openOrders: openOrdersB.publicKey, + // Swapping from B -> USDC. + orderPayerTokenAccount: ORDERBOOK_ENV.godB, + coinWallet: ORDERBOOK_ENV.godB, + }, + to: { + market: marketA._decoded.ownAddress, + requestQueue: marketA._decoded.requestQueue, + eventQueue: marketA._decoded.eventQueue, + bids: marketA._decoded.bids, + asks: marketA._decoded.asks, + coinVault: marketA._decoded.baseVault, + pcVault: marketA._decoded.quoteVault, + vaultSigner: marketAVaultSigner, + // User params. + openOrders: openOrdersA.publicKey, + // Swapping from USDC -> A. + orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc, + coinWallet: ORDERBOOK_ENV.godA, + }, + pcWallet: ORDERBOOK_ENV.godUsdc, + authority: program.provider.wallet.publicKey, + dexProgram: utils.DEX_PID, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + } + ); + } + ); + + // TODO: calculate this dynamically from the swap amount. + assert.ok(tokenAChange === 22.6); + assert.ok(tokenBChange === -swapAmount); + assert.ok(usdcChange === 0); + }); +}); + +// Side rust enum used for the program's RPC API. +const Side = { + Bid: { bid: {} }, + Ask: { ask: {} }, +}; + +// Executes a closure. Returning the change in balances from before and after +// its execution. +async function withBalanceChange(provider, addrs, fn) { + const beforeBalances = []; + for (let k = 0; k < addrs.length; k += 1) { + beforeBalances.push( + (await serumCmn.getTokenAccount(provider, addrs[k])).amount + ); + } + + await fn(); + + const afterBalances = []; + for (let k = 0; k < addrs.length; k += 1) { + afterBalances.push( + (await serumCmn.getTokenAccount(provider, addrs[k])).amount + ); + } + + const deltas = []; + for (let k = 0; k < addrs.length; k += 1) { + deltas.push( + (afterBalances[k].toNumber() - beforeBalances[k].toNumber()) / 10 ** 6 + ); + } + return deltas; +} diff --git a/tests/utils/index.js b/tests/utils/index.js new file mode 100644 index 0000000..6274443 --- /dev/null +++ b/tests/utils/index.js @@ -0,0 +1,510 @@ +// Boilerplate utils to bootstrap an orderbook for testing on a localnet. +// not super relevant to the point of the example, though may be useful to +// include into your own workspace for testing. +// +// TODO: Modernize all these apis. This is all quite clunky. + +const Token = require("@solana/spl-token").Token; +const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID; +const TokenInstructions = require("@project-serum/serum").TokenInstructions; +const Market = require("@project-serum/serum").Market; +const DexInstructions = require("@project-serum/serum").DexInstructions; +const web3 = require("@project-serum/anchor").web3; +const Connection = web3.Connection; +const BN = require("@project-serum/anchor").BN; +const serumCmn = require("@project-serum/common"); +const Account = web3.Account; +const Transaction = web3.Transaction; +const PublicKey = web3.PublicKey; +const SystemProgram = web3.SystemProgram; +const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"); + +async function setupTwoMarkets({ provider }) { + // Setup mints with initial tokens owned by the provider. + const decimals = 6; + const [MINT_A, GOD_A] = await serumCmn.createMintAndVault( + provider, + new BN(1000000000000000), + undefined, + decimals + ); + const [MINT_B, GOD_B] = await serumCmn.createMintAndVault( + provider, + new BN(1000000000000000), + undefined, + decimals + ); + const [USDC, GOD_USDC] = await serumCmn.createMintAndVault( + provider, + new BN(1000000000000000), + undefined, + decimals + ); + + // Create a funded account to act as market maker. + const amount = 100000 * 10 ** decimals; + const marketMaker = await fundAccount({ + provider, + mints: [ + { god: GOD_A, mint: MINT_A, amount, decimals }, + { god: GOD_B, mint: MINT_B, amount, decimals }, + { god: GOD_USDC, mint: USDC, amount, decimals }, + ], + }); + + // Setup A/USDC and B/USDC markets with resting orders. + const asks = [ + [6.041, 7.8], + [6.051, 72.3], + [6.055, 5.4], + [6.067, 15.7], + [6.077, 390.0], + [6.09, 24.0], + [6.11, 36.3], + [6.133, 300.0], + [6.167, 687.8], + ]; + const bids = [ + [6.004, 8.5], + [5.995, 12.9], + [5.987, 6.2], + [5.978, 15.3], + [5.965, 82.8], + [5.961, 25.4], + ]; + + MARKET_A_USDC = await setupMarket({ + baseMint: MINT_A, + quoteMint: USDC, + marketMaker: { + account: marketMaker.account, + baseToken: marketMaker.tokens[MINT_A.toString()], + quoteToken: marketMaker.tokens[USDC.toString()], + }, + bids, + asks, + provider, + }); + MARKET_B_USDC = await setupMarket({ + baseMint: MINT_B, + quoteMint: USDC, + marketMaker: { + account: marketMaker.account, + baseToken: marketMaker.tokens[MINT_B.toString()], + quoteToken: marketMaker.tokens[USDC.toString()], + }, + bids, + asks, + provider, + }); + + return { + marketA: MARKET_A_USDC, + marketB: MARKET_B_USDC, + marketMaker, + mintA: MINT_A, + mintB: MINT_B, + usdc: USDC, + godA: GOD_A, + godB: GOD_B, + godUsdc: GOD_USDC, + }; +} + +// Creates everything needed for an orderbook to be running +// +// * Mints for both the base and quote currencies. +// * Lists the market. +// * Provides resting orders on the market. +// +// Returns a client that can be used to interact with the market +// (and some other data, e.g., the mints and market maker account). +async function initOrderbook({ provider, bids, asks }) { + if (!bids || !asks) { + asks = [ + [6.041, 7.8], + [6.051, 72.3], + [6.055, 5.4], + [6.067, 15.7], + [6.077, 390.0], + [6.09, 24.0], + [6.11, 36.3], + [6.133, 300.0], + [6.167, 687.8], + ]; + bids = [ + [6.004, 8.5], + [5.995, 12.9], + [5.987, 6.2], + [5.978, 15.3], + [5.965, 82.8], + [5.961, 25.4], + ]; + } + // Create base and quote currency mints. + const decimals = 6; + const [MINT_A, GOD_A] = await serumCmn.createMintAndVault( + provider, + new BN(1000000000000000), + undefined, + decimals + ); + const [USDC, GOD_USDC] = await serumCmn.createMintAndVault( + provider, + new BN(1000000000000000), + undefined, + decimals + ); + + // Create a funded account to act as market maker. + const amount = 100000 * 10 ** decimals; + const marketMaker = await fundAccount({ + provider, + mints: [ + { god: GOD_A, mint: MINT_A, amount, decimals }, + { god: GOD_USDC, mint: USDC, amount, decimals }, + ], + }); + + marketClient = await setupMarket({ + baseMint: MINT_A, + quoteMint: USDC, + marketMaker: { + account: marketMaker.account, + baseToken: marketMaker.tokens[MINT_A.toString()], + quoteToken: marketMaker.tokens[USDC.toString()], + }, + bids, + asks, + provider, + }); + + return { + marketClient, + baseMint: MINT_A, + quoteMint: USDC, + marketMaker, + }; +} + +async function fundAccount({ provider, mints }) { + const MARKET_MAKER = new Account(); + + const marketMaker = { + tokens: {}, + account: MARKET_MAKER, + }; + + // Transfer lamports to market maker. + await provider.send( + (() => { + const tx = new Transaction(); + tx.add( + SystemProgram.transfer({ + fromPubkey: provider.wallet.publicKey, + toPubkey: MARKET_MAKER.publicKey, + lamports: 100000000000, + }) + ); + return tx; + })() + ); + + // Transfer SPL tokens to the market maker. + for (let k = 0; k < mints.length; k += 1) { + const { mint, god, amount, decimals } = mints[k]; + let MINT_A = mint; + let GOD_A = god; + // Setup token accounts owned by the market maker. + const mintAClient = new Token( + provider.connection, + MINT_A, + TOKEN_PROGRAM_ID, + provider.wallet.payer // node only + ); + const marketMakerTokenA = await mintAClient.createAccount( + MARKET_MAKER.publicKey + ); + + await provider.send( + (() => { + const tx = new Transaction(); + tx.add( + Token.createTransferCheckedInstruction( + TOKEN_PROGRAM_ID, + GOD_A, + MINT_A, + marketMakerTokenA, + provider.wallet.publicKey, + [], + amount, + decimals + ) + ); + return tx; + })() + ); + + marketMaker.tokens[mint.toString()] = marketMakerTokenA; + } + + return marketMaker; +} + +async function setupMarket({ + provider, + marketMaker, + baseMint, + quoteMint, + bids, + asks, +}) { + const marketAPublicKey = await listMarket({ + connection: provider.connection, + wallet: provider.wallet, + baseMint: baseMint, + quoteMint: quoteMint, + baseLotSize: 100000, + quoteLotSize: 100, + dexProgramId: DEX_PID, + feeRateBps: 0, + }); + const MARKET_A_USDC = await Market.load( + provider.connection, + marketAPublicKey, + { commitment: "recent" }, + DEX_PID + ); + for (let k = 0; k < asks.length; k += 1) { + let ask = asks[k]; + const { + transaction, + signers, + } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, { + owner: marketMaker.account, + payer: marketMaker.baseToken, + side: "sell", + price: ask[0], + size: ask[1], + orderType: "postOnly", + clientId: undefined, + openOrdersAddressKey: undefined, + openOrdersAccount: undefined, + feeDiscountPubkey: null, + selfTradeBehavior: "abortTransaction", + }); + await provider.send(transaction, signers.concat(marketMaker.account)); + } + + for (let k = 0; k < bids.length; k += 1) { + let bid = bids[k]; + const { + transaction, + signers, + } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, { + owner: marketMaker.account, + payer: marketMaker.quoteToken, + side: "buy", + price: bid[0], + size: bid[1], + orderType: "postOnly", + clientId: undefined, + openOrdersAddressKey: undefined, + openOrdersAccount: undefined, + feeDiscountPubkey: null, + selfTradeBehavior: "abortTransaction", + }); + await provider.send(transaction, signers.concat(marketMaker.account)); + } + + return MARKET_A_USDC; +} + +async function listMarket({ + connection, + wallet, + baseMint, + quoteMint, + baseLotSize, + quoteLotSize, + dexProgramId, + feeRateBps, +}) { + const market = new Account(); + const requestQueue = new Account(); + const eventQueue = new Account(); + const bids = new Account(); + const asks = new Account(); + const baseVault = new Account(); + const quoteVault = new Account(); + const quoteDustThreshold = new BN(100); + + const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce( + market.publicKey, + dexProgramId + ); + + const tx1 = new Transaction(); + tx1.add( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: baseVault.publicKey, + lamports: await connection.getMinimumBalanceForRentExemption(165), + space: 165, + programId: TOKEN_PROGRAM_ID, + }), + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: quoteVault.publicKey, + lamports: await connection.getMinimumBalanceForRentExemption(165), + space: 165, + programId: TOKEN_PROGRAM_ID, + }), + TokenInstructions.initializeAccount({ + account: baseVault.publicKey, + mint: baseMint, + owner: vaultOwner, + }), + TokenInstructions.initializeAccount({ + account: quoteVault.publicKey, + mint: quoteMint, + owner: vaultOwner, + }) + ); + + const tx2 = new Transaction(); + tx2.add( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: market.publicKey, + lamports: await connection.getMinimumBalanceForRentExemption( + Market.getLayout(dexProgramId).span + ), + space: Market.getLayout(dexProgramId).span, + programId: dexProgramId, + }), + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: requestQueue.publicKey, + lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12), + space: 5120 + 12, + programId: dexProgramId, + }), + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: eventQueue.publicKey, + lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12), + space: 262144 + 12, + programId: dexProgramId, + }), + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: bids.publicKey, + lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12), + space: 65536 + 12, + programId: dexProgramId, + }), + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: asks.publicKey, + lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12), + space: 65536 + 12, + programId: dexProgramId, + }), + DexInstructions.initializeMarket({ + market: market.publicKey, + requestQueue: requestQueue.publicKey, + eventQueue: eventQueue.publicKey, + bids: bids.publicKey, + asks: asks.publicKey, + baseVault: baseVault.publicKey, + quoteVault: quoteVault.publicKey, + baseMint, + quoteMint, + baseLotSize: new BN(baseLotSize), + quoteLotSize: new BN(quoteLotSize), + feeRateBps, + vaultSignerNonce, + quoteDustThreshold, + programId: dexProgramId, + }) + ); + + const signedTransactions = await signTransactions({ + transactionsAndSigners: [ + { transaction: tx1, signers: [baseVault, quoteVault] }, + { + transaction: tx2, + signers: [market, requestQueue, eventQueue, bids, asks], + }, + ], + wallet, + connection, + }); + for (let signedTransaction of signedTransactions) { + await sendAndConfirmRawTransaction( + connection, + signedTransaction.serialize() + ); + } + const acc = await connection.getAccountInfo(market.publicKey); + + return market.publicKey; +} + +async function signTransactions({ + transactionsAndSigners, + wallet, + connection, +}) { + const blockhash = (await connection.getRecentBlockhash("max")).blockhash; + transactionsAndSigners.forEach(({ transaction, signers = [] }) => { + transaction.recentBlockhash = blockhash; + transaction.setSigners( + wallet.publicKey, + ...signers.map((s) => s.publicKey) + ); + if (signers?.length > 0) { + transaction.partialSign(...signers); + } + }); + return await wallet.signAllTransactions( + transactionsAndSigners.map(({ transaction }) => transaction) + ); +} + +async function sendAndConfirmRawTransaction( + connection, + raw, + commitment = "recent" +) { + let tx = await connection.sendRawTransaction(raw, { + skipPreflight: true, + }); + return await connection.confirmTransaction(tx, commitment); +} + +async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) { + const nonce = new BN(0); + while (nonce.toNumber() < 255) { + try { + const vaultOwner = await PublicKey.createProgramAddress( + [marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)], + dexProgramId + ); + return [vaultOwner, nonce]; + } catch (e) { + nonce.iaddn(1); + } + } + throw new Error("Unable to find nonce"); +} + +module.exports = { + fundAccount, + setupMarket, + initOrderbook, + setupTwoMarkets, + DEX_PID, + getVaultOwnerAndNonce, +};