commit 35e6bfcd663d536fbc4c0d5de786ff82f2cac4ba Author: armaniferrante Date: Thu Apr 29 22:20:44 2021 -0700 Init repo 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, +};