Token swap (#50)
* initial * wip * withdraw * wip * wip * updates * fix imports * compiles * check delegates * wip * wip * wip * fixup * instruction serializer * unpack func * done! * update * wip docs * docs * boilerplate * docs * fix docs * Add token-swap test * Add token-swap js bindings Co-authored-by: Jack May <jack@solana.com>
This commit is contained in:
parent
f8f51c13fa
commit
97094e61e5
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
.env
|
||||
config.json
|
|
@ -0,0 +1,476 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"byte-tools",
|
||||
"byteorder",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
|
||||
dependencies = [
|
||||
"byte-tools",
|
||||
]
|
||||
|
||||
[[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 = "byte-tools"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-mac"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
|
||||
|
||||
[[package]]
|
||||
name = "fake-simd"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
||||
|
||||
[[package]]
|
||||
name = "feature-probe"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695"
|
||||
dependencies = [
|
||||
"crypto-mac",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eafd0b45c5537c3ba526f79d3e75120036502bebacbb3f3220914067ce39dbf2"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.30",
|
||||
"quote 0.6.13",
|
||||
"syn 0.15.44",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c8b15b261814f992e33760b1fca9fe8b693d8a65299f20c9901688636cfb746"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.18",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "006c038a43a45995a9670da19e67600114740e8511d4333bf97a56e66a7542d9"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crypto-mac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "0.4.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
|
||||
dependencies = [
|
||||
"unicode-xid 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
|
||||
dependencies = [
|
||||
"unicode-xid 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.6.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.30",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.18",
|
||||
]
|
||||
|
||||
[[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 = "serde"
|
||||
version = "1.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3"
|
||||
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.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.18",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"digest",
|
||||
"fake-simd",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-sdk"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b4ce21779c6854ad5719df2e173304728e24953343751aee91ae039d34a9fd6"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bs58",
|
||||
"bv",
|
||||
"hex",
|
||||
"hmac",
|
||||
"itertools",
|
||||
"log",
|
||||
"num-derive 0.3.0",
|
||||
"num-traits",
|
||||
"pbkdf2",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_derive",
|
||||
"sha2",
|
||||
"solana-sdk-macro",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solana-sdk-bpf-test"
|
||||
version = "1.2.3"
|
||||
|
||||
[[package]]
|
||||
name = "solana-sdk-macro"
|
||||
version = "1.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b16253b0b4059dad29d328265f59bbea50b33b3d6f3ffd13d0206266734c6ec"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"proc-macro2 1.0.18",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-token"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"num-derive 0.2.5",
|
||||
"num-traits",
|
||||
"solana-sdk",
|
||||
"solana-sdk-bpf-test",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-token-swap"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"num-derive 0.2.5",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"solana-sdk",
|
||||
"solana-sdk-bpf-test",
|
||||
"spl-token",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.15.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.30",
|
||||
"quote 0.6.13",
|
||||
"unicode-xid 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.18",
|
||||
"quote 1.0.7",
|
||||
"unicode-xid 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.18",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
# Note: This crate must be built using do.sh
|
||||
|
||||
[package]
|
||||
name = "spl-token-swap"
|
||||
version = "0.1.0"
|
||||
description = "Solana Program Library Token Swap"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
num-derive = "0.2"
|
||||
num-traits = "0.2"
|
||||
solana-sdk = { version = "=1.2.0", default-features = false, features=["program"] }
|
||||
solana-sdk-bpf-test = { path = "../bin/bpf-sdk/rust/test", default-features = false }
|
||||
spl-token = { path = "../token", default-features = false }
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = { version = "0.7.0"}
|
||||
|
||||
[lib]
|
||||
name = "spl_token_swap"
|
||||
crate-type = ["cdylib", "lib"]
|
|
@ -0,0 +1,8 @@
|
|||
# Token-swap program
|
||||
|
||||
An Uniswap-like exchange for the Token program on the Solana blockchain.
|
||||
|
||||
The project comprises:
|
||||
|
||||
* The Rust on-chain program
|
||||
* A JavaScript library to interact with the on-chain program
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"presets": [
|
||||
"env",
|
||||
"flow",
|
||||
"react",
|
||||
"stage-2",
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
"transform-function-bind",
|
||||
"transform-runtime",
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
dist/
|
|
@ -0,0 +1,52 @@
|
|||
module.exports = {
|
||||
// eslint-disable-line import/no-commonjs
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
plugins: ['react'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings',
|
||||
'plugin:react/recommended',
|
||||
],
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 8,
|
||||
},
|
||||
rules: {
|
||||
'no-trailing-spaces': ['error'],
|
||||
'import/first': ['error'],
|
||||
'import/no-commonjs': ['error'],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
['internal', 'external', 'builtin'],
|
||||
['index', 'sibling', 'parent'],
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
},
|
||||
],
|
||||
indent: [
|
||||
'error',
|
||||
2,
|
||||
{
|
||||
MemberExpression: 1,
|
||||
SwitchCase: 1,
|
||||
},
|
||||
],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'no-console': [0],
|
||||
quotes: [
|
||||
'error',
|
||||
'single',
|
||||
{avoidEscape: true, allowTemplateLiterals: true},
|
||||
],
|
||||
'require-await': ['error'],
|
||||
semi: ['error', 'always'],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
[ignore]
|
||||
<PROJECT_ROOT>/node_modules/*
|
||||
|
||||
[include]
|
||||
../../token/js/url.js
|
||||
../../token/js/cli/*
|
||||
../../token/js/client/*
|
||||
|
||||
[libs]
|
||||
node_modules/@solana/web3.js/module.flow.js
|
||||
flow-typed/
|
||||
../../token/js/module.flow.js
|
||||
|
||||
[options]
|
||||
|
||||
emoji=true
|
||||
esproposal.class_instance_fields=enable
|
||||
esproposal.class_static_fields=enable
|
||||
esproposal.decorators=ignore
|
||||
esproposal.export_star_as=enable
|
||||
module.system.node.resolve_dirname=./src
|
||||
module.use_strict=true
|
||||
experimental.const_params=true
|
||||
include_warnings=true
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
|
|
@ -0,0 +1,7 @@
|
|||
arrowParens: "avoid"
|
||||
bracketSpacing: false
|
||||
jsxBracketSameLine: false
|
||||
semi: true
|
||||
singleQuote: true
|
||||
tabWidth: 2
|
||||
trailingComma: "all"
|
|
@ -0,0 +1,62 @@
|
|||
# Token-swap Javascript API
|
||||
|
||||
The Token-swap JavaScript library comprises:
|
||||
|
||||
* A library to interact with the on-chain program
|
||||
* A test client that exercises the program
|
||||
* Scripts to facilitate building the program
|
||||
|
||||
## Getting Started
|
||||
|
||||
First fetch the npm dependencies, including `@solana/web3.js`, by running:
|
||||
```sh
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### Select a Network
|
||||
|
||||
The client connects to a local Solana cluster by default.
|
||||
|
||||
To enable on-chain program logs, set the `RUST_LOG` environment variable:
|
||||
|
||||
```bash
|
||||
$ export RUST_LOG=solana_runtime::native_loader=trace,solana_runtime::system_instruction_processor=trace,solana_runtime::bank=debug,solana_bpf_loader=debug,solana_rbpf=debug
|
||||
```
|
||||
|
||||
To start a local Solana cluster run:
|
||||
```bash
|
||||
$ npm run localnet:update
|
||||
$ npm run localnet:up
|
||||
```
|
||||
|
||||
Solana cluster logs are available with:
|
||||
```bash
|
||||
$ npm run localnet:logs
|
||||
```
|
||||
|
||||
For more details on working with a local cluster, see the [full instructions](https://github.com/solana-labs/solana-web3.js#local-network).
|
||||
|
||||
### Run the test client
|
||||
|
||||
```sh
|
||||
$ npm run start
|
||||
```
|
||||
|
||||
## Pointing to a public Solana cluster
|
||||
|
||||
Solana maintains three public clusters:
|
||||
- `devnet` - Development cluster with airdrops enabled
|
||||
- `testnet` - Tour De Sol test cluster without airdrops enabled
|
||||
- `mainnet-beta` - Main cluster
|
||||
|
||||
Use npm scripts to configure which cluster.
|
||||
|
||||
To point to `devnet`:
|
||||
```bash
|
||||
$ npm run cluster:devnet
|
||||
```
|
||||
|
||||
To point back to the local cluster:
|
||||
```bash
|
||||
$ npm run cluster:localnet
|
||||
```
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Exercises the token-swap program
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {
|
||||
loadPrograms,
|
||||
createNewTokenSwap,
|
||||
swap,
|
||||
deposit,
|
||||
withdraw,
|
||||
} from './token-swap-test';
|
||||
|
||||
async function main() {
|
||||
// These test cases are designed to run sequentially and in the following order
|
||||
console.log('Run test: createNewToken');
|
||||
await loadPrograms();
|
||||
console.log('Run test: createNewToken');
|
||||
await createNewTokenSwap();
|
||||
console.log('Run test: deposit');
|
||||
await deposit();
|
||||
console.log('Run test: withdraw');
|
||||
await withdraw();
|
||||
console.log('Run test: swap');
|
||||
await swap();
|
||||
console.log('Success\n');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
.then(() => process.exit());
|
|
@ -0,0 +1,325 @@
|
|||
// @flow
|
||||
|
||||
import fs from 'mz/fs';
|
||||
import semver from 'semver';
|
||||
import { Account, Connection, BpfLoader, PublicKey } from '@solana/web3.js';
|
||||
|
||||
import { Token, TokenAmount } from '../../../token/js/client/token';
|
||||
import { TokenSwap } from '../client/token-swap';
|
||||
import { Store } from '../client/util/store';
|
||||
import { newAccountWithLamports } from '../client/util/new-account-with-lamports';
|
||||
import { url } from '../url';
|
||||
import { sleep } from '../client/util/sleep';
|
||||
|
||||
// The following globals are created by `createNewTokenSwap` and used by subsequent tests
|
||||
// Token swap
|
||||
let tokenSwap: TokenSwap;
|
||||
// authority of the token and accounts
|
||||
let authority: PublicKey;
|
||||
// owner of the user accounts
|
||||
let owner: Account;
|
||||
// Token pool
|
||||
let tokenPool: Token;
|
||||
let tokenAccountPool: PublicKey;
|
||||
// Tokens swapped
|
||||
let tokenA: Token;
|
||||
let tokenB: Token;
|
||||
let tokenAccountA: PublicKey;
|
||||
let tokenAccountB: PublicKey;
|
||||
|
||||
// Initial amount in each swap token
|
||||
const BASE_AMOUNT = 1000;
|
||||
// Amount passed to instructions
|
||||
const USER_AMOUNT = 100;
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
console.log(Error().stack + ':token-test.js');
|
||||
throw message || 'Assertion failed';
|
||||
}
|
||||
}
|
||||
|
||||
let connection;
|
||||
async function getConnection(): Promise<Connection> {
|
||||
if (connection) return connection;
|
||||
|
||||
let newConnection = new Connection(url, 'recent',);
|
||||
const version = await newConnection.getVersion();
|
||||
|
||||
// commitment params are only supported >= 0.21.0
|
||||
const solanaCoreVersion = version['solana-core'].split(' ')[0];
|
||||
if (semver.gte(solanaCoreVersion, '0.21.0')) {
|
||||
newConnection = new Connection(url, 'recent');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
connection = newConnection;
|
||||
console.log('Connection to cluster established:', url, version);
|
||||
return newConnection;
|
||||
}
|
||||
|
||||
async function loadProgram(connection: Connection, path: string): Promise<PublicKey> {
|
||||
const NUM_RETRIES = 500; /* allow some number of retries */
|
||||
const data = await fs.readFile(path
|
||||
);
|
||||
const { feeCalculator } = await connection.getRecentBlockhash();
|
||||
const balanceNeeded =
|
||||
feeCalculator.lamportsPerSignature *
|
||||
(BpfLoader.getMinNumSignatures(data.length) + NUM_RETRIES) +
|
||||
(await connection.getMinimumBalanceForRentExemption(data.length));
|
||||
|
||||
const from = await newAccountWithLamports(connection, balanceNeeded);
|
||||
const program_account = new Account();
|
||||
console.log('Loading program:', path);
|
||||
await BpfLoader.load(connection, from, program_account, data);
|
||||
return program_account.publicKey;
|
||||
}
|
||||
|
||||
async function GetPrograms(connection: Connection): Promise<[PublicKey, PublicKey]> {
|
||||
const store = new Store();
|
||||
let tokenProgramId = null;
|
||||
let tokenSwapProgramId = null;
|
||||
try {
|
||||
const config = await store.load('config.json');
|
||||
console.log('Using pre-loaded Token and Token-swap programs');
|
||||
console.log(' Note: To reload programs remove client/util/sore/config.json');
|
||||
tokenProgramId = new PublicKey(config.tokenProgramId);
|
||||
tokenSwapProgramId = new PublicKey(config.tokenSwapProgramId);
|
||||
} catch (err) {
|
||||
tokenProgramId = await loadProgram(connection, '../../token/target/bpfel-unknown-unknown/release/spl_token.so');
|
||||
tokenSwapProgramId = await loadProgram(connection, '../target/bpfel-unknown-unknown/release/spl_token_swap.so');
|
||||
await store.save('config.json', {
|
||||
tokenProgramId: tokenProgramId.toString(),
|
||||
tokenSwapProgramId: tokenSwapProgramId.toString()
|
||||
});
|
||||
}
|
||||
return [tokenProgramId, tokenSwapProgramId];
|
||||
}
|
||||
|
||||
export async function loadPrograms(): Promise<void> {
|
||||
const connection = await getConnection();
|
||||
const [tokenProgramId, tokenSwapProgramId] = await GetPrograms(connection);
|
||||
|
||||
console.log('Token Program ID', tokenProgramId.toString());
|
||||
console.log('Token-swap Program ID', tokenSwapProgramId.toString());
|
||||
}
|
||||
|
||||
export async function createNewTokenSwap(): Promise<void> {
|
||||
const connection = await getConnection();
|
||||
const [tokenProgramId, tokenSwapProgramId] = await GetPrograms(connection);
|
||||
const payer = await Token.getAccount(connection);
|
||||
owner = await Token.getAccount(connection);
|
||||
const tokenSwapAccount = new Account();
|
||||
authority = await PublicKey.createProgramAddress(
|
||||
[tokenSwapAccount.publicKey.toString().substring(0, 32)],
|
||||
tokenSwapProgramId
|
||||
);
|
||||
|
||||
// create pool
|
||||
[tokenPool, tokenAccountPool] = await Token.createNewToken(
|
||||
connection,
|
||||
payer,
|
||||
authority,
|
||||
owner.publicKey,
|
||||
new TokenAmount(0),
|
||||
2,
|
||||
tokenProgramId,
|
||||
true,
|
||||
);
|
||||
|
||||
// create token A
|
||||
[tokenA, tokenAccountA] = await Token.createNewToken(
|
||||
connection,
|
||||
payer,
|
||||
owner.publicKey,
|
||||
authority,
|
||||
new TokenAmount(BASE_AMOUNT),
|
||||
2,
|
||||
tokenProgramId,
|
||||
true,
|
||||
);
|
||||
|
||||
// create token B
|
||||
[tokenB, tokenAccountB] = await Token.createNewToken(
|
||||
connection,
|
||||
payer,
|
||||
owner.publicKey,
|
||||
authority,
|
||||
new TokenAmount(BASE_AMOUNT),
|
||||
2,
|
||||
tokenProgramId,
|
||||
true,
|
||||
);
|
||||
|
||||
// create token swap
|
||||
const swapPayer = await newAccountWithLamports(connection, 100000000000 /* wag */);
|
||||
tokenSwap = await TokenSwap.createNewTokenSwap(
|
||||
connection,
|
||||
swapPayer,
|
||||
tokenSwapAccount,
|
||||
authority,
|
||||
tokenAccountA,
|
||||
tokenAccountB,
|
||||
tokenPool.publicKey,
|
||||
tokenAccountPool,
|
||||
tokenProgramId,
|
||||
1,
|
||||
4,
|
||||
tokenSwapProgramId
|
||||
);
|
||||
|
||||
const swapInfo = await tokenSwap.getInfo();
|
||||
assert(swapInfo.tokenAccountA.equals(tokenAccountA));
|
||||
assert(swapInfo.tokenAccountB.equals(tokenAccountB));
|
||||
assert(swapInfo.tokenPool.equals(tokenPool.publicKey));
|
||||
assert(1 == swapInfo.feesNumerator.toNumber());
|
||||
assert(4 == swapInfo.feesDenominator.toNumber());
|
||||
}
|
||||
|
||||
export async function deposit(): Promise<void> {
|
||||
let userAccountA = await tokenA.newAccount(owner.publicKey);
|
||||
await tokenA.mintTo(owner, userAccountA, USER_AMOUNT);
|
||||
let delegateAccountA = await tokenA.newAccount(authority, userAccountA);
|
||||
await tokenA.approve(
|
||||
owner,
|
||||
userAccountA,
|
||||
delegateAccountA,
|
||||
USER_AMOUNT,
|
||||
);
|
||||
let userAccountB = await tokenB.newAccount(owner.publicKey);
|
||||
await tokenB.mintTo(owner, userAccountB, USER_AMOUNT);
|
||||
let delegateAccountB = await tokenB.newAccount(authority, userAccountB);
|
||||
await tokenB.approve(
|
||||
owner,
|
||||
userAccountB,
|
||||
delegateAccountB,
|
||||
USER_AMOUNT,
|
||||
);
|
||||
let newAccountPool = await tokenPool.newAccount(owner.publicKey);
|
||||
const [tokenProgramId,] = await GetPrograms(connection);
|
||||
|
||||
await tokenSwap.deposit(
|
||||
authority,
|
||||
delegateAccountA,
|
||||
userAccountA,
|
||||
delegateAccountB,
|
||||
userAccountB,
|
||||
tokenAccountA,
|
||||
tokenAccountB,
|
||||
tokenPool.publicKey,
|
||||
newAccountPool,
|
||||
tokenProgramId,
|
||||
USER_AMOUNT,
|
||||
);
|
||||
|
||||
let info;
|
||||
info = await tokenA.getAccountInfo(delegateAccountA);
|
||||
console.log('delegageAccountA', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == 0);
|
||||
info = await tokenA.getAccountInfo(userAccountA);
|
||||
console.log('userAccountA', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == 0);
|
||||
info = await tokenB.getAccountInfo(delegateAccountB);
|
||||
console.log('delegageAccountB', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == 0);
|
||||
info = await tokenB.getAccountInfo(userAccountB);
|
||||
console.log('userAccountB', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == 0);
|
||||
info = await tokenA.getAccountInfo(tokenAccountA);
|
||||
console.log('tokenAccountA', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == BASE_AMOUNT + USER_AMOUNT);
|
||||
info = await tokenB.getAccountInfo(tokenAccountB);
|
||||
console.log('tokenAccountB', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == BASE_AMOUNT + USER_AMOUNT);
|
||||
info = await tokenPool.getAccountInfo(newAccountPool);
|
||||
console.log('newAccountPool', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == USER_AMOUNT);
|
||||
}
|
||||
|
||||
export async function withdraw(): Promise<void> {
|
||||
let userAccountA = await tokenA.newAccount(owner.publicKey);
|
||||
let userAccountB = await tokenB.newAccount(owner.publicKey);
|
||||
let delegateAccountPool = await tokenPool.newAccount(authority, tokenAccountPool);
|
||||
await tokenPool.approve(
|
||||
owner,
|
||||
tokenAccountPool,
|
||||
delegateAccountPool,
|
||||
USER_AMOUNT,
|
||||
);
|
||||
const [tokenProgramId,] = await GetPrograms(connection);
|
||||
|
||||
await tokenSwap.withdraw(
|
||||
authority,
|
||||
delegateAccountPool,
|
||||
tokenAccountPool,
|
||||
tokenPool.publicKey,
|
||||
tokenAccountA,
|
||||
tokenAccountB,
|
||||
userAccountA,
|
||||
userAccountB,
|
||||
tokenProgramId,
|
||||
USER_AMOUNT
|
||||
);
|
||||
|
||||
let info;
|
||||
info = await tokenPool.getAccountInfo(delegateAccountPool);
|
||||
console.log('delegateAccountPool', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == 0);
|
||||
info = await tokenPool.getAccountInfo(tokenAccountPool);
|
||||
console.log('tokenAccountPool', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == BASE_AMOUNT - USER_AMOUNT);
|
||||
info = await tokenA.getAccountInfo(tokenAccountA);
|
||||
console.log('tokenAccountA', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == BASE_AMOUNT);
|
||||
info = await tokenB.getAccountInfo(tokenAccountB);
|
||||
console.log('tokenAccountB', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == BASE_AMOUNT);
|
||||
info = await tokenA.getAccountInfo(userAccountA);
|
||||
console.log('userAccountA', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == USER_AMOUNT);
|
||||
info = await tokenB.getAccountInfo(userAccountB);
|
||||
console.log('userAccountB', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == USER_AMOUNT);
|
||||
}
|
||||
|
||||
export async function swap(): Promise<void> {
|
||||
let userAccountA = await tokenA.newAccount(owner.publicKey);
|
||||
await tokenA.mintTo(owner, userAccountA, USER_AMOUNT);
|
||||
let delegateAccountA = await tokenA.newAccount(authority, userAccountA);
|
||||
await tokenA.approve(
|
||||
owner,
|
||||
userAccountA,
|
||||
delegateAccountA,
|
||||
USER_AMOUNT,
|
||||
);
|
||||
let userAccountB = await tokenB.newAccount(owner.publicKey);
|
||||
const [tokenProgramId,] = await GetPrograms(connection);
|
||||
|
||||
await tokenSwap.swap(
|
||||
authority,
|
||||
delegateAccountA,
|
||||
userAccountA,
|
||||
tokenAccountA,
|
||||
tokenAccountB,
|
||||
userAccountB,
|
||||
tokenProgramId,
|
||||
USER_AMOUNT,
|
||||
);
|
||||
await sleep(500);
|
||||
let info;
|
||||
info = await tokenA.getAccountInfo(userAccountA);
|
||||
console.log('userAccountA', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == 0);
|
||||
info = await tokenA.getAccountInfo(tokenAccountA);
|
||||
console.log('tokenAccountA', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == BASE_AMOUNT + USER_AMOUNT);
|
||||
info = await tokenB.getAccountInfo(tokenAccountB);
|
||||
console.log('tokenAccountB', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == 931);
|
||||
info = await tokenB.getAccountInfo(userAccountB);
|
||||
console.log('userAccountB', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == 69);
|
||||
info = await tokenPool.getAccountInfo(tokenAccountPool);
|
||||
console.log('tokenAccountPool', info.amount.toNumber());
|
||||
assert(info.amount.toNumber() == BASE_AMOUNT - USER_AMOUNT);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
|
||||
import * as BufferLayout from 'buffer-layout';
|
||||
|
||||
/**
|
||||
* Layout for a public key
|
||||
*/
|
||||
export const publicKey = (property: string = 'publicKey'): Object => {
|
||||
return BufferLayout.blob(32, property);
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout for a 64bit unsigned value
|
||||
*/
|
||||
export const uint64 = (property: string = 'uint64'): Object => {
|
||||
return BufferLayout.blob(8, property);
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout for a Rust String type
|
||||
*/
|
||||
export const rustString = (property: string = 'string') => {
|
||||
const rsl = BufferLayout.struct(
|
||||
[
|
||||
BufferLayout.u32('length'),
|
||||
BufferLayout.u32('lengthPadding'),
|
||||
BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), 'chars'),
|
||||
],
|
||||
property,
|
||||
);
|
||||
const _decode = rsl.decode.bind(rsl);
|
||||
const _encode = rsl.encode.bind(rsl);
|
||||
|
||||
rsl.decode = (buffer, offset) => {
|
||||
const data = _decode(buffer, offset);
|
||||
return data.chars.toString('utf8');
|
||||
};
|
||||
|
||||
rsl.encode = (str, buffer, offset) => {
|
||||
const data = {
|
||||
chars: Buffer.from(str, 'utf8'),
|
||||
};
|
||||
return _encode(data, buffer, offset);
|
||||
};
|
||||
|
||||
return rsl;
|
||||
};
|
|
@ -0,0 +1,546 @@
|
|||
/**
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import BN from 'bn.js';
|
||||
import * as BufferLayout from 'buffer-layout';
|
||||
import {
|
||||
Account,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import type {Connection, TransactionSignature} from '@solana/web3.js';
|
||||
|
||||
import * as Layout from './layout';
|
||||
import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';
|
||||
|
||||
/**
|
||||
* Some amount of tokens
|
||||
*/
|
||||
export class Numberu64 extends BN {
|
||||
/**
|
||||
* Convert to Buffer representation
|
||||
*/
|
||||
toBuffer(): Buffer {
|
||||
const a = super.toArray().reverse();
|
||||
const b = Buffer.from(a);
|
||||
if (b.length === 8) {
|
||||
return b;
|
||||
}
|
||||
assert(b.length < 8, 'Numberu64 too large');
|
||||
|
||||
const zeroPad = Buffer.alloc(8);
|
||||
b.copy(zeroPad);
|
||||
return zeroPad;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Numberu64 from Buffer representation
|
||||
*/
|
||||
static fromBuffer(buffer: Buffer): Numberu64 {
|
||||
assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`);
|
||||
return new BN(
|
||||
[...buffer]
|
||||
.reverse()
|
||||
.map(i => `00${i.toString(16)}`.slice(-2))
|
||||
.join(''),
|
||||
16,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a token swap
|
||||
*/
|
||||
type TokenSwapInfo = {|
|
||||
/**
|
||||
* Token A. The Liquidity token is issued against this value.
|
||||
*/
|
||||
tokenAccountA: PublicKey,
|
||||
|
||||
/**
|
||||
* Token B
|
||||
*/
|
||||
tokenAccountB: PublicKey,
|
||||
/**
|
||||
* Pool tokens are issued when A or B tokens are deposited
|
||||
* Pool tokens can be withdrawn back to the original A or B token
|
||||
*/
|
||||
tokenPool: PublicKey,
|
||||
|
||||
/**
|
||||
* Fee numerator
|
||||
*/
|
||||
feesNumerator: Numberu64,
|
||||
|
||||
/**
|
||||
* Fee denominator
|
||||
*/
|
||||
feesDenominator: Numberu64,
|
||||
|
||||
/**
|
||||
* Fee ratio applied to the input token amount prior to output calculation
|
||||
*/
|
||||
feeRatio: number,
|
||||
|
||||
|};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
const TokenSwapLayout = BufferLayout.struct([
|
||||
BufferLayout.u8('state'),
|
||||
Layout.publicKey('tokenAccountA'),
|
||||
Layout.publicKey('tokenAccountB'),
|
||||
Layout.publicKey('tokenPool'),
|
||||
Layout.uint64('feesDenominator'),
|
||||
Layout.uint64('feesNumerator'),
|
||||
]);
|
||||
|
||||
/**
|
||||
* An ERC20-like Token
|
||||
*/
|
||||
export class TokenSwap {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
connection: Connection;
|
||||
|
||||
/**
|
||||
* The public key identifying this token
|
||||
*/
|
||||
tokenSwap: PublicKey;
|
||||
|
||||
/**
|
||||
* Program Identifier for the Token Swap program
|
||||
*/
|
||||
programId: PublicKey;
|
||||
|
||||
/**
|
||||
* Fee payer
|
||||
*/
|
||||
payer: Account;
|
||||
|
||||
/**
|
||||
* Create a Token object attached to the specific token
|
||||
*
|
||||
* @param connection The connection to use
|
||||
* @param token Public key of the token
|
||||
* @param programId Optional token programId, uses the system programId by default
|
||||
* @param payer Payer of fees
|
||||
*/
|
||||
constructor(connection: Connection, tokenSwap: PublicKey, programId: PublicKey, payer: Account) {
|
||||
Object.assign(this, {connection, tokenSwap, programId, payer});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum balance for the token swap account to be rent exempt
|
||||
*
|
||||
* @return Number of lamports required
|
||||
*/
|
||||
static async getMinBalanceRentForExemptTokenSwap(
|
||||
connection: Connection,
|
||||
): Promise<number> {
|
||||
return await connection.getMinimumBalanceForRentExemption(
|
||||
TokenSwapLayout.span,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Token Swap
|
||||
*
|
||||
* @param connection The connection to use
|
||||
* @param payer Pays for the transaction
|
||||
* @param tokenSwapAccount The token swap account
|
||||
* @param authority The authority over the swap and accounts
|
||||
* @param tokenAccountA: The Swap's Token A account
|
||||
* @param tokenAccountB: The Swap's Token B account
|
||||
* @param tokenPool The pool token
|
||||
* @param tokenAccountPool The pool token account
|
||||
* @param tokenProgramId The program id of the token program
|
||||
* @param feeNumerator Numerator of the fee ratio
|
||||
* @param feeDenominator Denominator of the fee ratio
|
||||
* @param programId Program ID of the token-swap program
|
||||
* @return Token object for the newly minted token, Public key of the account holding the total supply of new tokens
|
||||
*/
|
||||
static async createNewTokenSwap(
|
||||
connection: Connection,
|
||||
payer: Account,
|
||||
tokenSwapAccount: Account,
|
||||
authority: PublicKey,
|
||||
tokenAccountA: PublicKey,
|
||||
tokenAccountB: PublicKey,
|
||||
tokenPool: PublicKey,
|
||||
tokenAccountPool: PublicKey,
|
||||
tokenProgramId: PublicKey,
|
||||
feeNumerator: number,
|
||||
feeDenominator: number,
|
||||
programId: PublicKey,
|
||||
): Promise<TokenSwap> {
|
||||
let transaction;
|
||||
const tokenSwap = new TokenSwap(connection, tokenSwapAccount.publicKey, programId, payer);
|
||||
|
||||
// Allocate memory for the account
|
||||
const balanceNeeded = await TokenSwap.getMinBalanceRentForExemptTokenSwap(
|
||||
connection,
|
||||
);
|
||||
transaction = SystemProgram.createAccount({
|
||||
fromPubkey: payer.publicKey,
|
||||
newAccountPubkey: tokenSwapAccount.publicKey,
|
||||
lamports: balanceNeeded,
|
||||
space: TokenSwapLayout.span,
|
||||
programId,
|
||||
});
|
||||
await sendAndConfirmTransaction(
|
||||
'createAccount',
|
||||
connection,
|
||||
transaction,
|
||||
payer,
|
||||
tokenSwapAccount,
|
||||
);
|
||||
|
||||
let keys = [
|
||||
{pubkey: tokenSwapAccount.publicKey, isSigner: true, isWritable: true},
|
||||
{pubkey: authority, isSigner: false, isWritable: false},
|
||||
{pubkey: tokenAccountA, isSigner: false, isWritable: true},
|
||||
{pubkey: tokenAccountB, isSigner: false, isWritable: true},
|
||||
{pubkey: tokenPool, isSigner: false, isWritable: true},
|
||||
{pubkey: tokenAccountPool, isSigner: false, isWritable: true},
|
||||
{pubkey: tokenProgramId, isSigner: false, isWritable: false},
|
||||
];
|
||||
const commandDataLayout = BufferLayout.struct([
|
||||
BufferLayout.u8('instruction'),
|
||||
BufferLayout.nu64('feeDenominator'),
|
||||
BufferLayout.nu64('feeNumerator'),
|
||||
]);
|
||||
let data = Buffer.alloc(1024);
|
||||
{
|
||||
const encodeLength = commandDataLayout.encode(
|
||||
{
|
||||
instruction: 0, // Init instruction
|
||||
feeNumerator,
|
||||
feeDenominator,
|
||||
},
|
||||
data,
|
||||
);
|
||||
data = data.slice(0, encodeLength);
|
||||
}
|
||||
transaction = new Transaction().add({
|
||||
keys,
|
||||
programId,
|
||||
data,
|
||||
});
|
||||
await sendAndConfirmTransaction(
|
||||
'Init',
|
||||
connection,
|
||||
transaction,
|
||||
payer,
|
||||
tokenSwapAccount
|
||||
);
|
||||
|
||||
return tokenSwap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve tokenSwap information
|
||||
*/
|
||||
async getInfo(): Promise<TokenSwapInfo> {
|
||||
const accountInfo = await this.connection.getAccountInfo(this.tokenSwap);
|
||||
if (accountInfo === null) {
|
||||
throw new Error('Failed to find token swap account');
|
||||
}
|
||||
if (!accountInfo.owner.equals(this.programId)) {
|
||||
throw new Error(
|
||||
`Invalid token swap owner: ${JSON.stringify(accountInfo.owner)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = Buffer.from(accountInfo.data);
|
||||
const tokenSwapInfo = TokenSwapLayout.decode(data);
|
||||
if (tokenSwapInfo.state !== 1) {
|
||||
throw new Error(`Invalid token swap state`);
|
||||
}
|
||||
tokenSwapInfo.tokenAccountA = new PublicKey(tokenSwapInfo.tokenAccountA);
|
||||
tokenSwapInfo.tokenAccountB = new PublicKey(tokenSwapInfo.tokenAccountB);
|
||||
tokenSwapInfo.tokenPool = new PublicKey(tokenSwapInfo.tokenPool);
|
||||
tokenSwapInfo.feesNumerator = Numberu64.fromBuffer(tokenSwapInfo.feesNumerator);
|
||||
tokenSwapInfo.feesDenominator = Numberu64.fromBuffer(tokenSwapInfo.feesDenominator);
|
||||
tokenSwapInfo.feeRatio = tokenSwapInfo.feesNumerator.toNumber() / tokenSwapInfo.feesDenominator.toNumber();
|
||||
|
||||
return tokenSwapInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the tokens in the pool
|
||||
*
|
||||
* @param authority Authority
|
||||
* @param delegate Delegate account to transfer from
|
||||
* @param source Source account associated with delegate
|
||||
* @param into Base account to swap into, must be a source token
|
||||
* @param from Base account to swap from, must be a destination token
|
||||
* @param dest Destination token
|
||||
* @param tokenProgramId Token program id
|
||||
* @param amount Amount to transfer from source account
|
||||
*/
|
||||
async swap(
|
||||
authority: PublicKey,
|
||||
delegate: PublicKey,
|
||||
source: PublicKey,
|
||||
into: PublicKey,
|
||||
from: PublicKey,
|
||||
destination: PublicKey,
|
||||
tokenProgramId: PublicKey,
|
||||
amount: number | Numberu64,
|
||||
): Promise<?TransactionSignature> {
|
||||
return await sendAndConfirmTransaction(
|
||||
'swap',
|
||||
this.connection,
|
||||
new Transaction().add(
|
||||
this.swapInstruction(
|
||||
authority,
|
||||
delegate,
|
||||
source,
|
||||
into,
|
||||
from,
|
||||
destination,
|
||||
tokenProgramId,
|
||||
amount,
|
||||
),
|
||||
),
|
||||
this.payer,
|
||||
);
|
||||
}
|
||||
swapInstruction(
|
||||
authority: PublicKey,
|
||||
delegate: PublicKey,
|
||||
source: PublicKey,
|
||||
into: PublicKey,
|
||||
from: PublicKey,
|
||||
destination: PublicKey,
|
||||
tokenProgramId: PublicKey,
|
||||
amount: number | Numberu64,
|
||||
): TransactionInstruction {
|
||||
const dataLayout = BufferLayout.struct([
|
||||
BufferLayout.u8('instruction'),
|
||||
Layout.uint64('amount'),
|
||||
]);
|
||||
|
||||
const data = Buffer.alloc(dataLayout.span);
|
||||
dataLayout.encode(
|
||||
{
|
||||
instruction: 1, // Swap instruction
|
||||
amount: new Numberu64(amount).toBuffer(),
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
const keys = [
|
||||
{pubkey: this.tokenSwap, isSigner: false, isWritable: false},
|
||||
{pubkey: authority, isSigner: false, isWritable: false},
|
||||
{pubkey: delegate, isSigner: false, isWritable: true},
|
||||
{pubkey: source, isSigner: false, isWritable: true},
|
||||
{pubkey: into, isSigner: false, isWritable: true},
|
||||
{pubkey: from, isSigner: false, isWritable: true},
|
||||
{pubkey: destination, isSigner: false, isWritable: true},
|
||||
{pubkey: tokenProgramId, isSigner: false, isWritable: false},
|
||||
|
||||
];
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: this.programId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposit some tokens into the pool
|
||||
*
|
||||
* @param authority Authority
|
||||
* @param delegateA Delegate account to transfer token A from
|
||||
* @param sourceA Source account associated with delegate account A
|
||||
* @param delegateB Delegate account to transfer token B from
|
||||
* @param sourceB Source account associated with delegate account A
|
||||
* @param intoA Base account A to deposit into
|
||||
* @param intoB Base account B to deposit into
|
||||
* @param poolToken Pool token
|
||||
* @param poolAccount Pool account to deposit the generated tokens
|
||||
* @param tokenProgramId Token program id
|
||||
* @param amount Amount of token A to transfer, token B amount is set by the exchange rate
|
||||
*/
|
||||
async deposit(
|
||||
authority: PublicKey,
|
||||
delegateA: PublicKey,
|
||||
sourceA: PublicKey,
|
||||
delegateB: PublicKey,
|
||||
sourceB: PublicKey,
|
||||
intoA: PublicKey,
|
||||
intoB: PublicKey,
|
||||
poolToken: PublicKey,
|
||||
poolAccount: PublicKey,
|
||||
tokenProgramId: PublicKey,
|
||||
amount: number | Numberu64,
|
||||
): Promise<?TransactionSignature> {
|
||||
return await sendAndConfirmTransaction(
|
||||
'deposit',
|
||||
this.connection,
|
||||
new Transaction().add(
|
||||
this.depositInstruction(
|
||||
authority,
|
||||
delegateA,
|
||||
sourceA,
|
||||
delegateB,
|
||||
sourceB,
|
||||
intoA,
|
||||
intoB,
|
||||
poolToken,
|
||||
poolAccount,
|
||||
tokenProgramId,
|
||||
amount,
|
||||
),
|
||||
),
|
||||
this.payer,
|
||||
);
|
||||
}
|
||||
depositInstruction(
|
||||
authority: PublicKey,
|
||||
delegateA: PublicKey,
|
||||
sourceA: PublicKey,
|
||||
delegateB: PublicKey,
|
||||
sourceB: PublicKey,
|
||||
intoA: PublicKey,
|
||||
intoB: PublicKey,
|
||||
poolToken: PublicKey,
|
||||
poolAccount: PublicKey,
|
||||
tokenProgramId: PublicKey,
|
||||
amount: number | Numberu64,
|
||||
): TransactionInstruction {
|
||||
const dataLayout = BufferLayout.struct([
|
||||
BufferLayout.u8('instruction'),
|
||||
Layout.uint64('amount'),
|
||||
]);
|
||||
|
||||
const data = Buffer.alloc(dataLayout.span);
|
||||
dataLayout.encode(
|
||||
{
|
||||
instruction: 2, // Deposit instruction
|
||||
amount: new Numberu64(amount).toBuffer(),
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
const keys = [
|
||||
{pubkey: this.tokenSwap, isSigner: false, isWritable: false},
|
||||
{pubkey: authority, isSigner: false, isWritable: false},
|
||||
{pubkey: delegateA, isSigner: false, isWritable: true},
|
||||
{pubkey: sourceA, isSigner: false, isWritable: true},
|
||||
{pubkey: delegateB, isSigner: false, isWritable: true},
|
||||
{pubkey: sourceB, isSigner: false, isWritable: true},
|
||||
{pubkey: intoA, isSigner: false, isWritable: true},
|
||||
{pubkey: intoB, isSigner: false, isWritable: true},
|
||||
{pubkey: poolToken, isSigner: false, isWritable: true},
|
||||
{pubkey: poolAccount, isSigner: false, isWritable: true},
|
||||
{pubkey: tokenProgramId, isSigner: false, isWritable: false},
|
||||
|
||||
];
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: this.programId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw the token from the pool at the current ratio
|
||||
*
|
||||
* @param authority Authority
|
||||
* @param delegatePoolAccount Delegate pool account
|
||||
* @param sourcePoolAccount Source account associated with delegate
|
||||
* @param poolToken Pool token
|
||||
* @param fromA Base account A to withdraw from
|
||||
* @param fromB Base account B to withdraw from
|
||||
* @param userAccountA Token A user account
|
||||
* @param userAccountB token B user account
|
||||
* @param tokenProgramId Token program id
|
||||
* @param amount Amount of token A to transfer, token B amount is set by the exchange rate
|
||||
*/
|
||||
async withdraw(
|
||||
authority: PublicKey,
|
||||
delegatePoolAccount: PublicKey,
|
||||
sourcePoolAccount: PublicKey,
|
||||
poolToken: PublicKey,
|
||||
fromA: PublicKey,
|
||||
fromB: PublicKey,
|
||||
userAccountA: PublicKey,
|
||||
userAccountB: PublicKey,
|
||||
tokenProgramId: PublicKey,
|
||||
amount: number | Numberu64,
|
||||
): Promise<?TransactionSignature> {
|
||||
return await sendAndConfirmTransaction(
|
||||
'withdraw',
|
||||
this.connection,
|
||||
new Transaction().add(
|
||||
this.withdrawInstruction(
|
||||
authority,
|
||||
delegatePoolAccount,
|
||||
sourcePoolAccount,
|
||||
poolToken,
|
||||
fromA,
|
||||
fromB,
|
||||
userAccountA,
|
||||
userAccountB,
|
||||
tokenProgramId,
|
||||
amount,
|
||||
),
|
||||
),
|
||||
this.payer,
|
||||
);
|
||||
}
|
||||
withdrawInstruction(
|
||||
authority: PublicKey,
|
||||
delegatePoolAccount: PublicKey,
|
||||
sourcePoolAccount: PublicKey,
|
||||
poolToken: PublicKey,
|
||||
fromA: PublicKey,
|
||||
fromB: PublicKey,
|
||||
userAccountA: PublicKey,
|
||||
userAccountB: PublicKey,
|
||||
tokenProgramId: PublicKey,
|
||||
amount: number | Numberu64,
|
||||
): TransactionInstruction {
|
||||
const dataLayout = BufferLayout.struct([
|
||||
BufferLayout.u8('instruction'),
|
||||
Layout.uint64('amount'),
|
||||
]);
|
||||
|
||||
const data = Buffer.alloc(dataLayout.span);
|
||||
dataLayout.encode(
|
||||
{
|
||||
instruction: 3, // Withdraw instruction
|
||||
amount: new Numberu64(amount).toBuffer(),
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
const keys = [
|
||||
{pubkey: this.tokenSwap, isSigner: false, isWritable: false},
|
||||
{pubkey: authority, isSigner: false, isWritable: false},
|
||||
{pubkey: delegatePoolAccount, isSigner: false, isWritable: true},
|
||||
{pubkey: sourcePoolAccount, isSigner: false, isWritable: true},
|
||||
{pubkey: poolToken, isSigner: false, isWritable: true},
|
||||
{pubkey: fromA, isSigner: false, isWritable: true},
|
||||
{pubkey: fromB, isSigner: false, isWritable: true},
|
||||
{pubkey: userAccountA, isSigner: false, isWritable: true},
|
||||
{pubkey: userAccountB, isSigner: false, isWritable: true},
|
||||
{pubkey: tokenProgramId, isSigner: false, isWritable: false},
|
||||
|
||||
];
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: this.programId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
|
||||
import {Account, Connection} from '@solana/web3.js';
|
||||
|
||||
import {sleep} from './sleep';
|
||||
|
||||
export async function newAccountWithLamports(
|
||||
connection: Connection,
|
||||
lamports: number = 1000000,
|
||||
): Promise<Account> {
|
||||
const account = new Account();
|
||||
|
||||
let retries = 30;
|
||||
await connection.requestAirdrop(account.publicKey, lamports);
|
||||
for (;;) {
|
||||
await sleep(500);
|
||||
if (lamports == (await connection.getBalance(account.publicKey))) {
|
||||
return account;
|
||||
}
|
||||
if (--retries <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new Error(`Airdrop of ${lamports} failed`);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// @flow
|
||||
|
||||
import {Account, Connection} from '@solana/web3.js';
|
||||
|
||||
/**
|
||||
* Create a new system account and airdrop it some lamports
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export async function newSystemAccountWithAirdrop(
|
||||
connection: Connection,
|
||||
lamports: number = 1,
|
||||
): Promise<Account> {
|
||||
const account = new Account();
|
||||
await connection.requestAirdrop(account.publicKey, lamports);
|
||||
return account;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
|
||||
import {sendAndConfirmTransaction as realSendAndConfirmTransaction} from '@solana/web3.js';
|
||||
import type {Account, Connection, Transaction} from '@solana/web3.js';
|
||||
import YAML from 'json-to-pretty-yaml';
|
||||
|
||||
type TransactionNotification = (string, string) => void;
|
||||
|
||||
let notify: TransactionNotification = () => undefined;
|
||||
|
||||
export function onTransaction(callback: TransactionNotification) {
|
||||
notify = callback;
|
||||
}
|
||||
|
||||
export async function sendAndConfirmTransaction(
|
||||
title: string,
|
||||
connection: Connection,
|
||||
transaction: Transaction,
|
||||
...signers: Array<Account>
|
||||
): Promise<void> {
|
||||
const when = Date.now();
|
||||
|
||||
const signature = await realSendAndConfirmTransaction(
|
||||
connection,
|
||||
transaction,
|
||||
signers,
|
||||
{
|
||||
confirmations: 1,
|
||||
skipPreflight: true,
|
||||
},
|
||||
);
|
||||
|
||||
const body = {
|
||||
time: new Date(when).toString(),
|
||||
from: signers[0].publicKey.toBase58(),
|
||||
signature,
|
||||
instructions: transaction.instructions.map(i => {
|
||||
return {
|
||||
keys: i.keys.map(keyObj => keyObj.pubkey.toBase58()),
|
||||
programId: i.programId.toBase58(),
|
||||
data: '0x' + i.data.toString('hex'),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
notify(title, YAML.stringify(body).replace(/"/g, ''));
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
// @flow
|
||||
|
||||
// zzz
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Simple file-based datastore
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'mz/fs';
|
||||
import mkdirp from 'mkdirp-promise';
|
||||
|
||||
export class Store {
|
||||
dir = path.join(__dirname, 'store');
|
||||
|
||||
async load(uri: string): Promise<Object> {
|
||||
const filename = path.join(this.dir, uri);
|
||||
const data = await fs.readFile(filename, 'utf8');
|
||||
const config = JSON.parse(data);
|
||||
return config;
|
||||
}
|
||||
|
||||
async save(uri: string, config: Object): Promise<void> {
|
||||
await mkdirp(this.dir);
|
||||
const filename = path.join(this.dir, uri);
|
||||
await fs.writeFile(filename, JSON.stringify(config), 'utf8');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
LIVE=1
|
||||
CLUSTER=devnet
|
|
@ -0,0 +1,2 @@
|
|||
LIVE=1
|
||||
CLUSTER=mainnet-beta
|
|
@ -0,0 +1,2 @@
|
|||
LIVE=1
|
||||
CLUSTER=testnet
|
|
@ -0,0 +1,4 @@
|
|||
declare module 'bn.js' {
|
||||
// TODO: Fill in types
|
||||
declare module.exports: any;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
declare module 'bs58' {
|
||||
declare module.exports: {
|
||||
encode(input: Buffer): string;
|
||||
decode(input: string): Buffer;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
declare module 'buffer-layout' {
|
||||
// TODO: Fill in types
|
||||
declare module.exports: any;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
declare module 'cbor' {
|
||||
declare module.exports: {
|
||||
decode(input: Buffer): Object;
|
||||
encode(input: any): Buffer;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
// flow-typed signature: 4f92d81ee3831cb415b4b216cc0679d9
|
||||
// flow-typed version: <<STUB>>/event-emitter_v0.3.5/flow_v0.84.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'event-emitter'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'event-emitter' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'event-emitter/all-off' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/benchmark/many-on' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/benchmark/single-on' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/emit-error' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/has-listeners' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/pipe' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/test/all-off' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/test/emit-error' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/test/has-listeners' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/test/index' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/test/pipe' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/test/unify' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'event-emitter/unify' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'event-emitter/all-off.js' {
|
||||
declare module.exports: $Exports<'event-emitter/all-off'>;
|
||||
}
|
||||
declare module 'event-emitter/benchmark/many-on.js' {
|
||||
declare module.exports: $Exports<'event-emitter/benchmark/many-on'>;
|
||||
}
|
||||
declare module 'event-emitter/benchmark/single-on.js' {
|
||||
declare module.exports: $Exports<'event-emitter/benchmark/single-on'>;
|
||||
}
|
||||
declare module 'event-emitter/emit-error.js' {
|
||||
declare module.exports: $Exports<'event-emitter/emit-error'>;
|
||||
}
|
||||
declare module 'event-emitter/has-listeners.js' {
|
||||
declare module.exports: $Exports<'event-emitter/has-listeners'>;
|
||||
}
|
||||
declare module 'event-emitter/index' {
|
||||
declare module.exports: $Exports<'event-emitter'>;
|
||||
}
|
||||
declare module 'event-emitter/index.js' {
|
||||
declare module.exports: $Exports<'event-emitter'>;
|
||||
}
|
||||
declare module 'event-emitter/pipe.js' {
|
||||
declare module.exports: $Exports<'event-emitter/pipe'>;
|
||||
}
|
||||
declare module 'event-emitter/test/all-off.js' {
|
||||
declare module.exports: $Exports<'event-emitter/test/all-off'>;
|
||||
}
|
||||
declare module 'event-emitter/test/emit-error.js' {
|
||||
declare module.exports: $Exports<'event-emitter/test/emit-error'>;
|
||||
}
|
||||
declare module 'event-emitter/test/has-listeners.js' {
|
||||
declare module.exports: $Exports<'event-emitter/test/has-listeners'>;
|
||||
}
|
||||
declare module 'event-emitter/test/index.js' {
|
||||
declare module.exports: $Exports<'event-emitter/test/index'>;
|
||||
}
|
||||
declare module 'event-emitter/test/pipe.js' {
|
||||
declare module.exports: $Exports<'event-emitter/test/pipe'>;
|
||||
}
|
||||
declare module 'event-emitter/test/unify.js' {
|
||||
declare module.exports: $Exports<'event-emitter/test/unify'>;
|
||||
}
|
||||
declare module 'event-emitter/unify.js' {
|
||||
declare module.exports: $Exports<'event-emitter/unify'>;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// flow-typed signature: a65f8ee05f35bc382c3b0f8740bc609d
|
||||
// flow-typed version: <<STUB>>/json-to-pretty-yaml_v1.2.2/flow_v0.84.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'json-to-pretty-yaml'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'json-to-pretty-yaml' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'json-to-pretty-yaml/index.test' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'json-to-pretty-yaml/index' {
|
||||
declare module.exports: $Exports<'json-to-pretty-yaml'>;
|
||||
}
|
||||
declare module 'json-to-pretty-yaml/index.js' {
|
||||
declare module.exports: $Exports<'json-to-pretty-yaml'>;
|
||||
}
|
||||
declare module 'json-to-pretty-yaml/index.test.js' {
|
||||
declare module.exports: $Exports<'json-to-pretty-yaml/index.test'>;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// flow-typed signature: 65e18196703cbb222ea294226e99826d
|
||||
// flow-typed version: <<STUB>>/mkdirp-promise_v5.0.1/flow_v0.84.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'mkdirp-promise'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'mkdirp-promise' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'mkdirp-promise/lib/index' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'mkdirp-promise/lib/index.js' {
|
||||
declare module.exports: $Exports<'mkdirp-promise/lib/index'>;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
// flow-typed signature: ed29f42bf4f4916e4f3ba1f5e7343c9d
|
||||
// flow-typed version: <<STUB>>/mz_v2.7.0/flow_v0.81.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'mz'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'mz' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'mz/child_process' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'mz/crypto' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'mz/dns' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'mz/fs' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'mz/readline' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'mz/zlib' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'mz/child_process.js' {
|
||||
declare module.exports: $Exports<'mz/child_process'>;
|
||||
}
|
||||
declare module 'mz/crypto.js' {
|
||||
declare module.exports: $Exports<'mz/crypto'>;
|
||||
}
|
||||
declare module 'mz/dns.js' {
|
||||
declare module.exports: $Exports<'mz/dns'>;
|
||||
}
|
||||
declare module 'mz/fs.js' {
|
||||
declare module.exports: $Exports<'mz/fs'>;
|
||||
}
|
||||
declare module 'mz/index' {
|
||||
declare module.exports: $Exports<'mz'>;
|
||||
}
|
||||
declare module 'mz/index.js' {
|
||||
declare module.exports: $Exports<'mz'>;
|
||||
}
|
||||
declare module 'mz/readline.js' {
|
||||
declare module.exports: $Exports<'mz/readline'>;
|
||||
}
|
||||
declare module 'mz/zlib.js' {
|
||||
declare module.exports: $Exports<'mz/zlib'>;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
declare module 'readline-promise' {
|
||||
|
||||
declare class ReadLine {
|
||||
questionAsync(prompt: string): Promise<string>;
|
||||
write(text: string): void;
|
||||
}
|
||||
|
||||
declare module.exports: {
|
||||
createInterface({
|
||||
input: Object,
|
||||
output: Object,
|
||||
terminal: boolean
|
||||
}): ReadLine;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
declare module 'semver' {
|
||||
declare module.exports: any;
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"name": "spl-token-swap",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/solana-labs/solana-program-library"
|
||||
},
|
||||
"testnetDefaultChannel": "v1.2.3",
|
||||
"scripts": {
|
||||
"start": "babel-node cli/main.js",
|
||||
"lint": "npm run pretty && eslint .",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"flow": "flow",
|
||||
"flow:watch": "watch 'flow' . --wait=1 --ignoreDirectoryPattern=/doc/",
|
||||
"lint:watch": "watch 'npm run lint:fix' . --wait=1",
|
||||
"cluster:localnet": "rm -f .env",
|
||||
"cluster:devnet": "cp cluster-devnet.env .env",
|
||||
"cluster:testnet": "cp cluster-testnet.env .env",
|
||||
"cluster:mainnet-beta": "cp cluster-mainnet-beta.env .env",
|
||||
"localnet:update": "solana-localnet update",
|
||||
"localnet:up": "rm client/util/store/config.json; set -x; solana-localnet down; set -e; solana-localnet up",
|
||||
"localnet:down": "solana-localnet down",
|
||||
"localnet:logs": "solana-localnet logs -f",
|
||||
"pretty": "prettier --write '{,src/**/}*.js'"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"prettier": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solana/web3.js": "^0.62.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^7.1.5",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-function-bind": "^6.22.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-flow": "^6.23.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bn.js": "^5.0.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"css-loader": "^3.1.0",
|
||||
"dotenv": "8.2.0",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-loader": "^3.0.0",
|
||||
"eslint-plugin-import": "^2.13.0",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"event-emitter": "^0.3.5",
|
||||
"express": "^4.16.4",
|
||||
"flow-bin": "0.121.0",
|
||||
"flow-typed": "^3.0.0",
|
||||
"http-server": "^0.12.3",
|
||||
"jayson": "^3.0.1",
|
||||
"json-to-pretty-yaml": "^1.2.2",
|
||||
"mkdirp-promise": "^5.0.1",
|
||||
"moment": "^2.22.2",
|
||||
"mz": "^2.7.0",
|
||||
"node-fetch": "^2.2.0",
|
||||
"react": "^16.5.2",
|
||||
"react-bootstrap": "^1.0.0",
|
||||
"react-dom": "^16.5.2",
|
||||
"readline-promise": "^1.0.3",
|
||||
"semver": "^7.0.0",
|
||||
"superstruct": "^0.8.0",
|
||||
"watch": "^1.0.2",
|
||||
"webpack": "^4.20.2",
|
||||
"webpack-cli": "^3.1.1",
|
||||
"webpack-dev-server": "^3.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "11.x"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// To connect to a public cluster, set `export LIVE=1` in your
|
||||
// environment. By default, `LIVE=1` will connect to the devnet cluster.
|
||||
|
||||
import {clusterApiUrl, Cluster} from '@solana/web3.js';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
function chooseCluster(): Cluster | undefined {
|
||||
dotenv.config();
|
||||
if (!process.env.LIVE) return;
|
||||
switch (process.env.CLUSTER) {
|
||||
case 'devnet':
|
||||
case 'testnet':
|
||||
case 'mainnet-beta': {
|
||||
return process.env.CLUSTER;
|
||||
}
|
||||
}
|
||||
throw 'Unknown cluster "' + process.env.CLUSTER + '", check the .env file';
|
||||
}
|
||||
|
||||
export const cluster = chooseCluster();
|
||||
|
||||
export const url =
|
||||
process.env.RPC_URL ||
|
||||
(process.env.LIVE ? clusterApiUrl(cluster, false) : 'http://localhost:8899');
|
||||
|
||||
export const urlTls =
|
||||
process.env.RPC_URL ||
|
||||
(process.env.LIVE ? clusterApiUrl(cluster, true) : 'http://localhost:8899');
|
||||
|
||||
export let walletUrl =
|
||||
process.env.WALLET_URL || 'https://solana-example-webwallet.herokuapp.com/';
|
|
@ -0,0 +1,934 @@
|
|||
extern crate spl_token;
|
||||
|
||||
use num_derive::FromPrimitive;
|
||||
use num_traits::FromPrimitive;
|
||||
#[cfg(target_arch = "bpf")]
|
||||
use solana_sdk::program::invoke_signed;
|
||||
use solana_sdk::{
|
||||
account_info::AccountInfo,
|
||||
entrypoint,
|
||||
entrypoint::ProgramResult,
|
||||
info,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
program_error::{PrintProgramError, ProgramError},
|
||||
program_utils::{next_account_info, DecodeError},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use std::mem::size_of;
|
||||
use thiserror::Error;
|
||||
|
||||
// TODO update instruction documentation
|
||||
/// Instructions supported by the TokenSwap program.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SwapInstruction {
|
||||
/// Initializes a new TokenSwap.
|
||||
///
|
||||
/// 0. `[writable, signer]` New Token-swap to create.
|
||||
/// 1. `[]` $authority derived from `create_program_address(&[Token-swap account])`
|
||||
/// 2. `[]` token_a Account. Must be non zero, owned by $authority.
|
||||
/// 3. `[]` token_b Account. Must be non zero, owned by $authority.
|
||||
/// 4. `[writable]` pool Token. Must be empty, owned by $authority.
|
||||
/// 5. `[writable]` Pool Account to deposit the generated tokens, user is the owner.
|
||||
/// 6. '[]` Token program id
|
||||
/// userdata: fee rate as a ratio
|
||||
Init((u64, u64)),
|
||||
|
||||
/// Swap the tokens in the pool.
|
||||
///
|
||||
/// 0. `[]` Token-swap
|
||||
/// 1. `[]` $authority
|
||||
/// 2. `[writable]` token_(A|B) SOURCE delegate Account, amount is transferable by $authority,
|
||||
/// 3. `[writable]` token_(A|B) SOURCE Account associated with the delegate
|
||||
/// 4. `[writable]` token_(A|B) Base Account to swap INTO. Must be the SOURCE token.
|
||||
/// 5. `[writable]` token_(A|B) Base Account to swap FROM. Must be the DEST token.
|
||||
/// 6. `[writable]` token_(A|B) DEST Account assigned to USER as the owner.
|
||||
/// 7. '[]` Token program id
|
||||
/// userdata: SOURCE amount to transfer, output to DEST is based on the exchange rate
|
||||
Swap(u64),
|
||||
|
||||
/// Deposit some tokens into the pool. The output is a "pool" token representing ownership
|
||||
/// into the pool. Inputs are converted to the current ratio.
|
||||
///
|
||||
/// 0. `[]` Token-swap
|
||||
/// 1. `[]` $authority
|
||||
/// 2. `[writable]` token_a delegate $authority can transfer amount,
|
||||
/// 3. `[writable]` token_a account associated with delegate
|
||||
/// 4. `[writable]` token_b delegate $authority can transfer amount,
|
||||
/// 5. `[writable]` token_b account associated with delegate
|
||||
/// 6. `[writable]` token_a Base Account to deposit into.
|
||||
/// 7. `[writable]` token_b Base Account to deposit into.
|
||||
/// 8. `[writable]` Pool MINT account, $authority is the owner.
|
||||
/// 9. `[writable]` Pool Account to deposit the generated tokens, user is the owner.
|
||||
/// 10. '[]` Token program id
|
||||
/// userdata: token_a amount to transfer. token_b amount is set by the current exchange rate.
|
||||
Deposit(u64),
|
||||
|
||||
/// Withdraw the token from the pool at the current ratio.
|
||||
///
|
||||
/// 0. `[]` Token-swap
|
||||
/// 1. `[]` $authority
|
||||
/// 2. `[writable]` SOURCE Pool delegate, amount is transferable by $authority.
|
||||
/// 3. `[writable]` SOURCE Pool account associated with the delegate
|
||||
/// 4. `[writable]` Pool MINT account, $authority is the owner.
|
||||
/// 5. `[writable]` token_a Account to withdraw FROM.
|
||||
/// 6. `[writable]` token_b Account to withdraw FROM.
|
||||
/// 7. `[writable]` token_a user Account.
|
||||
/// 8. `[writable]` token_b user Account.
|
||||
/// 9. '[]` Token program id
|
||||
/// userdata: SOURCE amount of pool tokens to transfer. User receives an output based on the
|
||||
/// percentage of the pool tokens that are returned.
|
||||
Withdraw(u64),
|
||||
}
|
||||
|
||||
/// Creates an 'Init' instruction
|
||||
pub fn init(
|
||||
program_id: &Pubkey,
|
||||
token_program_id: &Pubkey,
|
||||
swap_pubkey: &Pubkey,
|
||||
authority_pubkey: &Pubkey,
|
||||
token_a_pubkey: &Pubkey,
|
||||
token_b_pubkey: &Pubkey,
|
||||
pool_pubkey: &Pubkey,
|
||||
user_output_pubkey: &Pubkey,
|
||||
fees: (u64, u64),
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let data = SwapInstruction::Init(fees).serialize()?;
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*swap_pubkey, true),
|
||||
AccountMeta::new(*authority_pubkey, false),
|
||||
AccountMeta::new(*token_a_pubkey, false),
|
||||
AccountMeta::new(*token_b_pubkey, false),
|
||||
AccountMeta::new(*pool_pubkey, false),
|
||||
AccountMeta::new(*user_output_pubkey, false),
|
||||
AccountMeta::new(*token_program_id, false),
|
||||
];
|
||||
|
||||
Ok(Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unpack<T>(input: &[u8]) -> Result<&T, ProgramError> {
|
||||
if input.len() < size_of::<u8>() + size_of::<T>() {
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
let val: &T = unsafe { &*(&input[1] as *const u8 as *const T) };
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
impl SwapInstruction {
|
||||
/// Deserializes a byte buffer into an [SwapInstruction](enum.SwapInstruction.html)
|
||||
pub fn deserialize(input: &[u8]) -> Result<Self, ProgramError> {
|
||||
if input.len() < size_of::<u8>() {
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
Ok(match input[0] {
|
||||
0 => {
|
||||
let fee: &(u64, u64) = unpack(input)?;
|
||||
Self::Init(*fee)
|
||||
}
|
||||
1 => {
|
||||
let fee: &u64 = unpack(input)?;
|
||||
Self::Swap(*fee)
|
||||
}
|
||||
2 => {
|
||||
let fee: &u64 = unpack(input)?;
|
||||
Self::Deposit(*fee)
|
||||
}
|
||||
3 => {
|
||||
let fee: &u64 = unpack(input)?;
|
||||
Self::Withdraw(*fee)
|
||||
}
|
||||
_ => return Err(ProgramError::InvalidAccountData),
|
||||
})
|
||||
}
|
||||
|
||||
/// Serializes an [SwapInstruction](enum.SwapInstruction.html) into a byte buffer
|
||||
pub fn serialize(self: &Self) -> Result<Vec<u8>, ProgramError> {
|
||||
let mut output = vec![0u8; size_of::<SwapInstruction>()];
|
||||
match self {
|
||||
Self::Init(fees) => {
|
||||
output[0] = 0;
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
let value = unsafe { &mut *(&mut output[1] as *mut u8 as *mut (u64, u64)) };
|
||||
*value = *fees;
|
||||
}
|
||||
Self::Swap(amount) => {
|
||||
output[0] = 1;
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
let value = unsafe { &mut *(&mut output[1] as *mut u8 as *mut u64) };
|
||||
*value = *amount;
|
||||
}
|
||||
Self::Deposit(amount) => {
|
||||
output[0] = 2;
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
let value = unsafe { &mut *(&mut output[1] as *mut u8 as *mut u64) };
|
||||
*value = *amount;
|
||||
}
|
||||
Self::Withdraw(amount) => {
|
||||
output[0] = 3;
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
let value = unsafe { &mut *(&mut output[1] as *mut u8 as *mut u64) };
|
||||
*value = *amount;
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
|
||||
pub enum Error {
|
||||
/// The account cannot be initialized because it is already being used.
|
||||
#[error("AlreadyInUse")]
|
||||
AlreadyInUse,
|
||||
/// The program address provided doesn't match the value generated by the program.
|
||||
#[error("InvalidProgramAddress")]
|
||||
InvalidProgramAddress,
|
||||
/// The owner of the input isn't set to the program address generated by the program.
|
||||
#[error("InvalidOwner")]
|
||||
InvalidOwner,
|
||||
/// The deserialization of the Token state returned something besides State::Token
|
||||
#[error("ExpectedToken")]
|
||||
ExpectedToken,
|
||||
/// The deserialization of the Token state returned something besides State::Account
|
||||
#[error("ExpectedAccount")]
|
||||
ExpectedAccount,
|
||||
/// The initialized pool had a non zero supply
|
||||
#[error("InvalidSupply")]
|
||||
InvalidSupply,
|
||||
/// The intiailized token has a delegate
|
||||
#[error("InvalidDelegate")]
|
||||
InvalidDelegate,
|
||||
/// The token swap state is invalid
|
||||
#[error("InvalidState")]
|
||||
InvalidState,
|
||||
/// The input token is invalid for swap
|
||||
#[error("InvalidInput")]
|
||||
InvalidInput,
|
||||
/// The output token is invalid for swap
|
||||
#[error("InvalidOutput")]
|
||||
InvalidOutput,
|
||||
/// The calculation failed
|
||||
#[error("CalculationFailure")]
|
||||
CalculationFailure,
|
||||
}
|
||||
impl From<Error> for ProgramError {
|
||||
fn from(e: Error) -> Self {
|
||||
ProgramError::Custom(e as u32)
|
||||
}
|
||||
}
|
||||
impl<T> DecodeError<T> for Error {
|
||||
fn type_of() -> &'static str {
|
||||
"Swap Error"
|
||||
}
|
||||
}
|
||||
impl PrintProgramError for Error {
|
||||
fn print<E>(&self)
|
||||
where
|
||||
E: 'static + std::error::Error + DecodeError<E> + PrintProgramError + FromPrimitive,
|
||||
{
|
||||
match self {
|
||||
Error::AlreadyInUse => info!("Error: AlreadyInUse"),
|
||||
Error::InvalidProgramAddress => info!("Error: InvalidProgramAddress"),
|
||||
Error::InvalidOwner => info!("Error: InvalidOwner"),
|
||||
Error::ExpectedToken => info!("Error: ExpectedToken"),
|
||||
Error::ExpectedAccount => info!("Error: ExpectedAccount"),
|
||||
Error::InvalidSupply => info!("Error: InvalidSupply"),
|
||||
Error::InvalidDelegate => info!("Error: InvalidDelegate"),
|
||||
Error::InvalidState => info!("Error: InvalidState"),
|
||||
Error::InvalidInput => info!("Error: InvalidInput"),
|
||||
Error::InvalidOutput => info!("Error: InvalidOutput"),
|
||||
Error::CalculationFailure => info!("Error: CalculationFailure"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct TokenSwap {
|
||||
/// token A
|
||||
/// The Liquidity token is issued against this value.
|
||||
token_a: Pubkey,
|
||||
/// token B
|
||||
token_b: Pubkey,
|
||||
/// pool tokens are issued when A or B tokens are deposited
|
||||
/// pool tokens can be withdrawn back to the original A or B token
|
||||
pool_mint: Pubkey,
|
||||
/// fee applied to the input token amount prior to output calculation
|
||||
fee: (u64, u64),
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum State {
|
||||
/// Unallocated state, may be initialized into another state.
|
||||
Unallocated,
|
||||
Init(TokenSwap),
|
||||
}
|
||||
|
||||
struct Invariant {
|
||||
token_a: u64,
|
||||
token_b: u64,
|
||||
pool: Option<u64>,
|
||||
fee: (u64, u64),
|
||||
}
|
||||
|
||||
impl Invariant {
|
||||
fn swap(&mut self, token_a: u64) -> Option<u64> {
|
||||
let invariant = self.token_a.checked_mul(self.token_b)?;
|
||||
let new_a = self.token_a.checked_add(token_a)?;
|
||||
let new_b = invariant.checked_div(new_a)?;
|
||||
let remove = self.token_b.checked_sub(new_b)?;
|
||||
let fee = remove.checked_mul(self.fee.1)?.checked_div(self.fee.0)?;
|
||||
let new_b_with_fee = new_b.checked_add(fee)?;
|
||||
let remove_less_fee = remove.checked_sub(fee)?;
|
||||
self.token_a = new_a;
|
||||
self.token_b = new_b_with_fee;
|
||||
Some(remove_less_fee)
|
||||
}
|
||||
fn exchange_rate(&self, token_a: u64) -> Option<u64> {
|
||||
token_a.checked_mul(self.token_b)?.checked_div(self.token_a)
|
||||
}
|
||||
fn redeem(&self, user_pool: u64) -> Option<(u64, u64)> {
|
||||
let token_a = self
|
||||
.token_a
|
||||
.checked_mul(user_pool)?
|
||||
.checked_div(self.pool?)?;
|
||||
let token_b = self
|
||||
.token_b
|
||||
.checked_mul(user_pool)?
|
||||
.checked_div(self.pool?)?;
|
||||
Some((token_a, token_b))
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn deserialize(input: &[u8]) -> Result<Self, ProgramError> {
|
||||
if input.len() < size_of::<u8>() {
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
Ok(match input[0] {
|
||||
0 => Self::Unallocated,
|
||||
1 => {
|
||||
let swap: &TokenSwap = unpack(input)?;
|
||||
Self::Init(*swap)
|
||||
}
|
||||
_ => return Err(ProgramError::InvalidAccountData),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize(self: &Self, output: &mut [u8]) -> ProgramResult {
|
||||
if output.len() < size_of::<u8>() {
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
match self {
|
||||
Self::Unallocated => output[0] = 0,
|
||||
Self::Init(swap) => {
|
||||
if output.len() < size_of::<u8>() + size_of::<TokenSwap>() {
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
output[0] = 1;
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
let value = unsafe { &mut *(&mut output[1] as *mut u8 as *mut TokenSwap) };
|
||||
*value = *swap;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn token_swap(&self) -> Result<TokenSwap, ProgramError> {
|
||||
if let State::Init(swap) = &self {
|
||||
Ok(*swap)
|
||||
} else {
|
||||
Err(Error::InvalidState.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token_account_deserialize(
|
||||
info: &AccountInfo,
|
||||
) -> Result<spl_token::state::Account, Error> {
|
||||
if let Some(spl_token::state::State::Account(account)) =
|
||||
spl_token::state::State::deserialize(&info.data.borrow()).ok()
|
||||
{
|
||||
Ok(account)
|
||||
} else {
|
||||
Err(Error::ExpectedAccount)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token_deserialize(info: &AccountInfo) -> Result<spl_token::state::Token, Error> {
|
||||
if let Some(spl_token::state::State::Token(token)) =
|
||||
spl_token::state::State::deserialize(&info.data.borrow()).ok()
|
||||
{
|
||||
Ok(token)
|
||||
} else {
|
||||
Err(Error::ExpectedToken)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authority_id(program_id: &Pubkey, my_info: &Pubkey) -> Result<Pubkey, Error> {
|
||||
Pubkey::create_program_address(&[&my_info.to_string()[..32]], program_id)
|
||||
.or(Err(Error::InvalidProgramAddress))
|
||||
}
|
||||
pub fn token_burn(
|
||||
accounts: &[AccountInfo],
|
||||
token_program_id: &Pubkey,
|
||||
swap: &Pubkey,
|
||||
authority: &Pubkey,
|
||||
token: &Pubkey,
|
||||
source: Option<&Pubkey>,
|
||||
burn_account: &Pubkey,
|
||||
amount: u64,
|
||||
) -> Result<(), ProgramError> {
|
||||
let swap_string = swap.to_string();
|
||||
let signers = &[&[&swap_string[..32]][..]];
|
||||
let ix = spl_token::instruction::burn(
|
||||
token_program_id,
|
||||
authority,
|
||||
burn_account,
|
||||
token,
|
||||
source,
|
||||
amount,
|
||||
)?;
|
||||
invoke_signed(&ix, accounts, signers)
|
||||
}
|
||||
|
||||
pub fn token_mint_to(
|
||||
accounts: &[AccountInfo],
|
||||
token_program_id: &Pubkey,
|
||||
swap: &Pubkey,
|
||||
authority: &Pubkey,
|
||||
token: &Pubkey,
|
||||
destination: &Pubkey,
|
||||
amount: u64,
|
||||
) -> Result<(), ProgramError> {
|
||||
let swap_string = swap.to_string();
|
||||
let signers = &[&[&swap_string[..32]][..]];
|
||||
let ix = spl_token::instruction::mint_to(
|
||||
token_program_id,
|
||||
authority,
|
||||
token,
|
||||
destination,
|
||||
amount,
|
||||
)?;
|
||||
invoke_signed(&ix, accounts, signers)
|
||||
}
|
||||
|
||||
pub fn token_transfer(
|
||||
accounts: &[AccountInfo],
|
||||
token_program_id: &Pubkey,
|
||||
swap: &Pubkey,
|
||||
authority: &Pubkey,
|
||||
token: &Pubkey,
|
||||
source: Option<&Pubkey>,
|
||||
destination: &Pubkey,
|
||||
amount: u64,
|
||||
) -> Result<(), ProgramError> {
|
||||
let swap_string = swap.to_string();
|
||||
let signers = &[&[&swap_string[..32]][..]];
|
||||
let ix = spl_token::instruction::transfer(
|
||||
token_program_id,
|
||||
authority,
|
||||
token,
|
||||
destination,
|
||||
source,
|
||||
amount,
|
||||
)?;
|
||||
invoke_signed(&ix, accounts, signers)
|
||||
}
|
||||
|
||||
pub fn process_init(
|
||||
program_id: &Pubkey,
|
||||
fee: (u64, u64),
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
let swap_info = next_account_info(account_info_iter)?;
|
||||
let authority_info = next_account_info(account_info_iter)?;
|
||||
let token_a_info = next_account_info(account_info_iter)?;
|
||||
let token_b_info = next_account_info(account_info_iter)?;
|
||||
let pool_info = next_account_info(account_info_iter)?;
|
||||
let user_output_info = next_account_info(account_info_iter)?;
|
||||
let token_program_info = next_account_info(account_info_iter)?;
|
||||
|
||||
if State::Unallocated != State::deserialize(&swap_info.data.borrow())? {
|
||||
return Err(Error::AlreadyInUse.into());
|
||||
}
|
||||
|
||||
if *authority_info.key != Self::authority_id(program_id, swap_info.key)? {
|
||||
return Err(Error::InvalidProgramAddress.into());
|
||||
}
|
||||
let token_a = Self::token_account_deserialize(token_a_info)?;
|
||||
let token_b = Self::token_account_deserialize(token_b_info)?;
|
||||
let pool_mint = Self::token_deserialize(pool_info)?;
|
||||
if *authority_info.key != token_a.owner {
|
||||
return Err(Error::InvalidOwner.into());
|
||||
}
|
||||
if *authority_info.key != token_b.owner {
|
||||
return Err(Error::InvalidOwner.into());
|
||||
}
|
||||
if Some(*authority_info.key) != pool_mint.owner {
|
||||
return Err(Error::InvalidOwner.into());
|
||||
}
|
||||
if 0 != pool_mint.info.supply {
|
||||
return Err(Error::InvalidSupply.into());
|
||||
}
|
||||
if token_b.amount == 0 {
|
||||
return Err(Error::InvalidSupply.into());
|
||||
}
|
||||
if token_a.amount == 0 {
|
||||
return Err(Error::InvalidSupply.into());
|
||||
}
|
||||
if token_a.delegate.is_some() {
|
||||
return Err(Error::InvalidDelegate.into());
|
||||
}
|
||||
if token_b.delegate.is_some() {
|
||||
return Err(Error::InvalidDelegate.into());
|
||||
}
|
||||
|
||||
// liqudity is measured in terms of token_a's value
|
||||
// since both sides of the pool are equal
|
||||
let amount = token_a.amount;
|
||||
Self::token_mint_to(
|
||||
accounts,
|
||||
token_program_info.key,
|
||||
swap_info.key,
|
||||
authority_info.key,
|
||||
pool_info.key,
|
||||
user_output_info.key,
|
||||
amount,
|
||||
)?;
|
||||
|
||||
let obj = State::Init(TokenSwap {
|
||||
token_a: *token_a_info.key,
|
||||
token_b: *token_b_info.key,
|
||||
pool_mint: *pool_info.key,
|
||||
fee,
|
||||
});
|
||||
obj.serialize(&mut swap_info.data.borrow_mut())
|
||||
}
|
||||
|
||||
pub fn process_swap(
|
||||
program_id: &Pubkey,
|
||||
amount: u64,
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
let swap_info = next_account_info(account_info_iter)?;
|
||||
let authority_info = next_account_info(account_info_iter)?;
|
||||
let source_delegate_info = next_account_info(account_info_iter)?;
|
||||
let source_info = next_account_info(account_info_iter)?;
|
||||
let into_info = next_account_info(account_info_iter)?;
|
||||
let from_info = next_account_info(account_info_iter)?;
|
||||
let dest_info = next_account_info(account_info_iter)?;
|
||||
let token_program_info = next_account_info(account_info_iter)?;
|
||||
|
||||
let token_swap = Self::deserialize(&swap_info.data.borrow())?.token_swap()?;
|
||||
|
||||
if *authority_info.key != Self::authority_id(program_id, swap_info.key)? {
|
||||
return Err(Error::InvalidProgramAddress.into());
|
||||
}
|
||||
if !(*into_info.key == token_swap.token_a || *into_info.key == token_swap.token_b) {
|
||||
return Err(Error::InvalidInput.into());
|
||||
}
|
||||
if !(*from_info.key == token_swap.token_a || *from_info.key == token_swap.token_b) {
|
||||
return Err(Error::InvalidOutput.into());
|
||||
}
|
||||
if *into_info.key == *from_info.key {
|
||||
return Err(Error::InvalidInput.into());
|
||||
}
|
||||
let into_token = Self::token_account_deserialize(into_info)?;
|
||||
let from_token = Self::token_account_deserialize(from_info)?;
|
||||
let mut invariant = Invariant {
|
||||
token_a: into_token.amount,
|
||||
token_b: from_token.amount,
|
||||
fee: token_swap.fee,
|
||||
pool: None,
|
||||
};
|
||||
let output = invariant
|
||||
.swap(amount)
|
||||
.ok_or_else(|| Error::CalculationFailure)?;
|
||||
Self::token_transfer(
|
||||
accounts,
|
||||
token_program_info.key,
|
||||
swap_info.key,
|
||||
authority_info.key,
|
||||
source_delegate_info.key,
|
||||
Some(source_info.key),
|
||||
into_info.key,
|
||||
amount,
|
||||
)?;
|
||||
Self::token_transfer(
|
||||
accounts,
|
||||
token_program_info.key,
|
||||
swap_info.key,
|
||||
authority_info.key,
|
||||
from_info.key,
|
||||
None,
|
||||
dest_info.key,
|
||||
output,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn process_deposit(
|
||||
program_id: &Pubkey,
|
||||
a_amount: u64,
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
let swap_info = next_account_info(account_info_iter)?;
|
||||
let authority_info = next_account_info(account_info_iter)?;
|
||||
let delegate_a_info = next_account_info(account_info_iter)?;
|
||||
let source_a_info = next_account_info(account_info_iter)?;
|
||||
let delegate_b_info = next_account_info(account_info_iter)?;
|
||||
let source_b_info = next_account_info(account_info_iter)?;
|
||||
let token_a_info = next_account_info(account_info_iter)?;
|
||||
let token_b_info = next_account_info(account_info_iter)?;
|
||||
let pool_info = next_account_info(account_info_iter)?;
|
||||
let dest_info = next_account_info(account_info_iter)?;
|
||||
let token_program_info = next_account_info(account_info_iter)?;
|
||||
|
||||
let token_swap = Self::deserialize(&swap_info.data.borrow())?.token_swap()?;
|
||||
if *authority_info.key != Self::authority_id(program_id, swap_info.key)? {
|
||||
return Err(Error::InvalidProgramAddress.into());
|
||||
}
|
||||
if *token_a_info.key != token_swap.token_a {
|
||||
return Err(Error::InvalidInput.into());
|
||||
}
|
||||
if *token_b_info.key != token_swap.token_b {
|
||||
return Err(Error::InvalidInput.into());
|
||||
}
|
||||
if *pool_info.key != token_swap.pool_mint {
|
||||
return Err(Error::InvalidInput.into());
|
||||
}
|
||||
let token_a = Self::token_account_deserialize(token_a_info)?;
|
||||
let token_b = Self::token_account_deserialize(token_b_info)?;
|
||||
|
||||
let invariant = Invariant {
|
||||
token_a: token_a.amount,
|
||||
token_b: token_b.amount,
|
||||
fee: token_swap.fee,
|
||||
pool: None,
|
||||
};
|
||||
let b_amount = invariant
|
||||
.exchange_rate(a_amount)
|
||||
.ok_or_else(|| Error::CalculationFailure)?;
|
||||
|
||||
// liqudity is measured in terms of token_a's value
|
||||
// since both sides of the pool are equal
|
||||
let output = a_amount;
|
||||
|
||||
Self::token_transfer(
|
||||
accounts,
|
||||
token_program_info.key,
|
||||
swap_info.key,
|
||||
authority_info.key,
|
||||
delegate_a_info.key,
|
||||
Some(source_a_info.key),
|
||||
token_a_info.key,
|
||||
a_amount,
|
||||
)?;
|
||||
Self::token_transfer(
|
||||
accounts,
|
||||
token_program_info.key,
|
||||
swap_info.key,
|
||||
authority_info.key,
|
||||
delegate_b_info.key,
|
||||
Some(source_b_info.key),
|
||||
token_b_info.key,
|
||||
b_amount,
|
||||
)?;
|
||||
Self::token_mint_to(
|
||||
accounts,
|
||||
token_program_info.key,
|
||||
swap_info.key,
|
||||
authority_info.key,
|
||||
pool_info.key,
|
||||
dest_info.key,
|
||||
output,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_withdraw(
|
||||
program_id: &Pubkey,
|
||||
amount: u64,
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
let swap_info = next_account_info(account_info_iter)?;
|
||||
let authority_info = next_account_info(account_info_iter)?;
|
||||
let delegate_info = next_account_info(account_info_iter)?;
|
||||
let source_info = next_account_info(account_info_iter)?;
|
||||
let pool_info = next_account_info(account_info_iter)?;
|
||||
let token_a_info = next_account_info(account_info_iter)?;
|
||||
let token_b_info = next_account_info(account_info_iter)?;
|
||||
let dest_token_a_info = next_account_info(account_info_iter)?;
|
||||
let dest_token_b_info = next_account_info(account_info_iter)?;
|
||||
let token_program_info = next_account_info(account_info_iter)?;
|
||||
|
||||
let token_swap = Self::deserialize(&swap_info.data.borrow())?.token_swap()?;
|
||||
if *authority_info.key != Self::authority_id(program_id, swap_info.key)? {
|
||||
return Err(Error::InvalidProgramAddress.into());
|
||||
}
|
||||
if *token_a_info.key != token_swap.token_a {
|
||||
return Err(Error::InvalidInput.into());
|
||||
}
|
||||
if *token_b_info.key != token_swap.token_b {
|
||||
return Err(Error::InvalidInput.into());
|
||||
}
|
||||
if *pool_info.key != token_swap.pool_mint {
|
||||
return Err(Error::InvalidInput.into());
|
||||
}
|
||||
let token_a = Self::token_account_deserialize(token_a_info)?;
|
||||
let token_b = Self::token_account_deserialize(token_b_info)?;
|
||||
let pool_token = Self::token_deserialize(pool_info)?;
|
||||
|
||||
let invariant = Invariant {
|
||||
token_a: token_a.amount,
|
||||
token_b: token_b.amount,
|
||||
fee: token_swap.fee,
|
||||
pool: Some(pool_token.info.supply),
|
||||
};
|
||||
|
||||
let (a_amount, b_amount) = invariant
|
||||
.redeem(amount)
|
||||
.ok_or_else(|| Error::CalculationFailure)?;
|
||||
Self::token_transfer(
|
||||
accounts,
|
||||
token_program_info.key,
|
||||
swap_info.key,
|
||||
authority_info.key,
|
||||
token_a_info.key,
|
||||
None,
|
||||
dest_token_a_info.key,
|
||||
a_amount,
|
||||
)?;
|
||||
Self::token_transfer(
|
||||
accounts,
|
||||
token_program_info.key,
|
||||
swap_info.key,
|
||||
authority_info.key,
|
||||
token_b_info.key,
|
||||
None,
|
||||
dest_token_b_info.key,
|
||||
b_amount,
|
||||
)?;
|
||||
Self::token_burn(
|
||||
accounts,
|
||||
token_program_info.key,
|
||||
swap_info.key,
|
||||
authority_info.key,
|
||||
pool_info.key,
|
||||
Some(source_info.key),
|
||||
delegate_info.key,
|
||||
amount,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
/// Processes an [SwapInstruction](enum.Instruction.html).
|
||||
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
|
||||
let instruction = SwapInstruction::deserialize(input)?;
|
||||
match instruction {
|
||||
SwapInstruction::Init(fee) => {
|
||||
info!("Instruction: Init");
|
||||
Self::process_init(program_id, fee, accounts)
|
||||
}
|
||||
SwapInstruction::Swap(amount) => {
|
||||
info!("Instruction: Swap");
|
||||
Self::process_swap(program_id, amount, accounts)
|
||||
}
|
||||
SwapInstruction::Deposit(amount) => {
|
||||
info!("Instruction: Deposit");
|
||||
Self::process_deposit(program_id, amount, accounts)
|
||||
}
|
||||
SwapInstruction::Withdraw(amount) => {
|
||||
info!("Instruction: Withdraw");
|
||||
Self::process_withdraw(program_id, amount, accounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entrypoint!(process_instruction);
|
||||
fn process_instruction<'a>(
|
||||
program_id: &Pubkey,
|
||||
accounts: &'a [AccountInfo<'a>],
|
||||
instruction_data: &[u8],
|
||||
) -> ProgramResult {
|
||||
if let Err(error) = State::process(program_id, accounts, instruction_data) {
|
||||
// catch the error so we can print it
|
||||
error.print::<Error>();
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Test program id for the swap program
|
||||
#[cfg(not(target_arch = "bpf"))]
|
||||
const SWAP_PROGRAM_ID: Pubkey = Pubkey::new_from_array([2u8; 32]);
|
||||
|
||||
/// Routes invokes to the token program, used for testing
|
||||
#[cfg(not(target_arch = "bpf"))]
|
||||
pub fn invoke_signed<'a>(
|
||||
instruction: &Instruction,
|
||||
account_infos: &[AccountInfo<'a>],
|
||||
signers_seeds: &[&[&str]],
|
||||
) -> ProgramResult {
|
||||
let mut new_account_infos = vec![];
|
||||
for meta in instruction.accounts.iter() {
|
||||
for account_info in account_infos.iter() {
|
||||
if meta.pubkey == *account_info.key {
|
||||
let mut new_account_info = account_info.clone();
|
||||
for seeds in signers_seeds.iter() {
|
||||
let signer = Pubkey::create_program_address(seeds, &SWAP_PROGRAM_ID).unwrap();
|
||||
if *account_info.key == signer {
|
||||
new_account_info.is_signer = true;
|
||||
}
|
||||
}
|
||||
new_account_infos.push(new_account_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
spl_token::state::State::process(
|
||||
&instruction.program_id,
|
||||
&new_account_infos,
|
||||
&instruction.data,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_sdk::{
|
||||
account::Account, account_info::create_is_signer_account_infos, instruction::Instruction,
|
||||
};
|
||||
use spl_token::{
|
||||
instruction::{new_account, new_token, TokenInfo},
|
||||
state::State as SplState,
|
||||
};
|
||||
|
||||
const TOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array([1u8; 32]);
|
||||
|
||||
// Pulls in the stubs required for `info!()`
|
||||
#[cfg(not(target_arch = "bpf"))]
|
||||
solana_sdk_bpf_test::stubs!();
|
||||
|
||||
fn pubkey_rand() -> Pubkey {
|
||||
Pubkey::new(&rand::random::<[u8; 32]>())
|
||||
}
|
||||
|
||||
fn do_process_instruction(
|
||||
instruction: Instruction,
|
||||
accounts: Vec<&mut Account>,
|
||||
) -> ProgramResult {
|
||||
let mut meta = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.zip(accounts)
|
||||
.map(|(account_meta, account)| (&account_meta.pubkey, account_meta.is_signer, account))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let account_infos = create_is_signer_account_infos(&mut meta);
|
||||
if instruction.program_id == SWAP_PROGRAM_ID {
|
||||
State::process(&instruction.program_id, &account_infos, &instruction.data)
|
||||
} else {
|
||||
SplState::process(&instruction.program_id, &account_infos, &instruction.data)
|
||||
}
|
||||
}
|
||||
|
||||
fn mint_token(
|
||||
program_id: &Pubkey,
|
||||
authority_key: &Pubkey,
|
||||
supply: u64,
|
||||
) -> ((Pubkey, Account), (Pubkey, Account)) {
|
||||
let token_key = pubkey_rand();
|
||||
let mut token_account = Account::new(0, size_of::<SplState>(), &program_id);
|
||||
let account_key = pubkey_rand();
|
||||
let mut account_account = Account::new(0, size_of::<SplState>(), &program_id);
|
||||
|
||||
// create pool and pool account
|
||||
do_process_instruction(
|
||||
new_account(&program_id, &account_key, &authority_key, &token_key, None).unwrap(),
|
||||
vec![
|
||||
&mut account_account,
|
||||
&mut Account::default(),
|
||||
&mut token_account,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let mut authority_account = Account::default();
|
||||
do_process_instruction(
|
||||
new_token(
|
||||
&program_id,
|
||||
&token_key,
|
||||
Some(&account_key),
|
||||
Some(&authority_key),
|
||||
TokenInfo {
|
||||
supply,
|
||||
decimals: 2,
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
if supply == 0 {
|
||||
vec![&mut token_account, &mut authority_account]
|
||||
} else {
|
||||
vec![
|
||||
&mut token_account,
|
||||
&mut account_account,
|
||||
&mut authority_account,
|
||||
]
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
return ((token_key, token_account), (account_key, account_account));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init() {
|
||||
let swap_key = pubkey_rand();
|
||||
let mut swap_account = Account::new(0, size_of::<State>(), &SWAP_PROGRAM_ID);
|
||||
let authority_key = State::authority_id(&SWAP_PROGRAM_ID, &swap_key).unwrap();
|
||||
let mut authority_account = Account::default();
|
||||
|
||||
let ((pool_key, mut pool_account), (pool_token_key, mut pool_token_account)) =
|
||||
mint_token(&TOKEN_PROGRAM_ID, &authority_key, 0);
|
||||
let ((_token_a_mint_key, mut _token_a_mint_account), (token_a_key, mut token_a_account)) =
|
||||
mint_token(&TOKEN_PROGRAM_ID, &authority_key, 1000);
|
||||
let ((_token_b_mint_key, mut _token_b_mint_account), (token_b_key, mut token_b_account)) =
|
||||
mint_token(&TOKEN_PROGRAM_ID, &authority_key, 1000);
|
||||
|
||||
// Swap Init
|
||||
do_process_instruction(
|
||||
init(
|
||||
&SWAP_PROGRAM_ID,
|
||||
&TOKEN_PROGRAM_ID,
|
||||
&swap_key,
|
||||
&authority_key,
|
||||
&token_a_key,
|
||||
&token_b_key,
|
||||
&pool_key,
|
||||
&pool_token_key,
|
||||
(1, 2),
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
&mut swap_account,
|
||||
&mut authority_account,
|
||||
&mut token_a_account,
|
||||
&mut token_b_account,
|
||||
&mut pool_account,
|
||||
&mut pool_token_account,
|
||||
&mut Account::default(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ import {
|
|||
|
||||
async function main() {
|
||||
console.log('Run test: loadTokenProgram');
|
||||
await loadTokenProgram();
|
||||
await loadTokenProgram('../target/bpfel-unknown-unknown/release/spl_token.so');
|
||||
console.log('Run test: createNewToken');
|
||||
await createNewToken();
|
||||
console.log('Run test: createNewAccount');
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Flow Library definition for token
|
||||
*
|
||||
* This file is manually maintained
|
||||
*
|
||||
* Usage: add the following line under the [libs] section of your project's
|
||||
* .flowconfig:
|
||||
* [libs]
|
||||
* token/module.flow.js
|
||||
*
|
||||
*/
|
||||
|
||||
declare module 'spl-token' {
|
||||
// === client/token.js ===
|
||||
declare export class TokenAmount extends BN {
|
||||
/**
|
||||
* Convert to Buffer representation
|
||||
*/
|
||||
toBuffer(): Buffer;
|
||||
static fromBuffer(buffer: Buffer): TokenAmount;
|
||||
}
|
||||
declare export class Token {
|
||||
constructor(
|
||||
connection: Connection,
|
||||
publicKey: PublicKey,
|
||||
programId: PublicKey,
|
||||
payer: Account,
|
||||
): Token;
|
||||
static createNewToken(
|
||||
connection: Connection,
|
||||
payer: Account,
|
||||
owner: Account,
|
||||
supply: TokenAmount,
|
||||
decimals: number,
|
||||
programId: PublicKey,
|
||||
is_owned: boolean,
|
||||
): Promise<TokenAndPublicKey>;
|
||||
static getAccount(connection: Connection): Promise<Account>;
|
||||
newAccount(owner: Account, source: null | PublicKey): Promise<PublicKey>;
|
||||
getTokenInfo(): Promise<TokenInfo>;
|
||||
getTokenAccountInfo(account: PublicKey): Promise<TokenAccountInfo>;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "token",
|
||||
"name": "spl-token",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
|
@ -482,9 +482,9 @@
|
|||
"integrity": "sha512-lXKXfypKo644k4Da4yXkPCrwcvn6SlUW2X2zFbuflKHNjf0w9htru01bo26uMhleMXsDmnZ12eJLdrAZa9MANg=="
|
||||
},
|
||||
"@solana/web3.js": {
|
||||
"version": "0.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.60.0.tgz",
|
||||
"integrity": "sha512-UUfo8gCbJTmCeN0CMSP9hKD18CzQUTSuV+ikoU7tuKWlEWn1eRazycOrdXsFyQlY4o30lqAdTmWNulpCoi0IpA==",
|
||||
"version": "0.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.62.0.tgz",
|
||||
"integrity": "sha512-GwGkLChr7zfsh5d+EaeiJ8G1w01HofQpYOq5mRWv/8j3sPkleWPaL4IyNxVAoE1pUx+pBY26BjFj5gGi5IqxCg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"bn.js": "^5.0.0",
|
||||
|
@ -9424,9 +9424,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz",
|
||||
"integrity": "sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==",
|
||||
"version": "7.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
|
||||
"integrity": "sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "token",
|
||||
"name": "spl-token",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"repository": {
|
||||
|
@ -31,7 +31,7 @@
|
|||
"prettier": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solana/web3.js": "^0.60.0",
|
||||
"@solana/web3.js": "^0.62.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^10.0.1",
|
||||
|
|
Loading…
Reference in New Issue