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:
anatoly yakovenko 2020-06-30 15:37:35 -07:00 committed by GitHub
parent f8f51c13fa
commit 97094e61e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 3157 additions and 10 deletions

3
token-swap/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
.env
config.json

476
token-swap/Cargo.lock generated Normal file
View File

@ -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"

26
token-swap/Cargo.toml Normal file
View File

@ -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"]

8
token-swap/README.md Normal file
View File

@ -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

2
token-swap/Xargo.toml Normal file
View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

13
token-swap/js/.babelrc Normal file
View File

@ -0,0 +1,13 @@
{
"presets": [
"env",
"flow",
"react",
"stage-2",
],
"plugins": [
"transform-class-properties",
"transform-function-bind",
"transform-runtime",
]
}

View File

@ -0,0 +1 @@
dist/

View File

@ -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'],
},
};

26
token-swap/js/.flowconfig Normal file
View File

@ -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

View File

@ -0,0 +1,7 @@
arrowParens: "avoid"
bracketSpacing: false
jsxBracketSameLine: false
semi: true
singleQuote: true
tabWidth: 2
trailingComma: "all"

62
token-swap/js/README.md Normal file
View File

@ -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
```

34
token-swap/js/cli/main.js Normal file
View File

@ -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());

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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,
});
}
}

View File

@ -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`);
}

View File

@ -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;
}

View File

@ -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, ''));
}

View File

@ -0,0 +1,6 @@
// @flow
// zzz
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@ -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');
}
}

View File

@ -0,0 +1,2 @@
LIVE=1
CLUSTER=devnet

View File

@ -0,0 +1,2 @@
LIVE=1
CLUSTER=mainnet-beta

View File

@ -0,0 +1,2 @@
LIVE=1
CLUSTER=testnet

4
token-swap/js/flow-typed/bn.js.js vendored Normal file
View File

@ -0,0 +1,4 @@
declare module 'bn.js' {
// TODO: Fill in types
declare module.exports: any;
}

6
token-swap/js/flow-typed/bs58.js vendored Normal file
View File

@ -0,0 +1,6 @@
declare module 'bs58' {
declare module.exports: {
encode(input: Buffer): string;
decode(input: string): Buffer;
};
}

View File

@ -0,0 +1,4 @@
declare module 'buffer-layout' {
// TODO: Fill in types
declare module.exports: any;
}

6
token-swap/js/flow-typed/cbor.js vendored Normal file
View File

@ -0,0 +1,6 @@
declare module 'cbor' {
declare module.exports: {
decode(input: Buffer): Object;
encode(input: any): Buffer;
};
}

View File

@ -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'>;
}

View File

@ -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'>;
}

View File

@ -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'>;
}

View File

@ -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'>;
}

View File

@ -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;
}
}

3
token-swap/js/flow-typed/semver.js vendored Normal file
View File

@ -0,0 +1,3 @@
declare module 'semver' {
declare module.exports: any;
}

View File

@ -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"
}
}

31
token-swap/js/url.js Normal file
View File

@ -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/';

934
token-swap/src/lib.rs Normal file
View File

@ -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();
}
}

View File

@ -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');

43
token/js/module.flow.js Normal file
View File

@ -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>;
}
}

View File

@ -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"
}

View File

@ -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",