This commit is contained in:
armaniferrante 2021-07-18 14:39:03 -07:00
commit 94908126b9
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
22 changed files with 2796 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*~
target/
localnet-logs/
test-ledger/
.anchor/

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[submodule "permissioned-markets/deps/serum-dex"]
path = permissioned-markets/deps/serum-dex
url = https://github.com/project-serum/serum-dex
[submodule "deps/serum-dex"]
path = deps/serum-dex
url = git@github.com:project-serum/serum-dex.git

7
Anchor.toml Normal file
View File

@ -0,0 +1,7 @@
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[[test.genesis]]
address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
program = "./deps/serum-dex/dex/target/deploy/serum_dex.so"

1179
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

8
Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[workspace]
members = [
"programs/*"
]
exclude = [
"deps/serum-dex",
]

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
.PHONY: test build build-deps build-dex localnet
test: build
anchor test
build: build-dex
anchor build
build-dex:
cd deps/serum-dex/dex/ && cargo build-bpf && cd ../../../
localnet: build
./scripts/localnet.sh

103
README.md Normal file
View File

@ -0,0 +1,103 @@
# Permissioned Markets
This repo demonstrates how to create "permissioned markets" on Serum via a proxy smart contract.
A permissioned market is a regular Serum market with an additional
open orders authority, which must sign every transaction to create an
open orders account.
In practice, what this means is that one can create a program that acts
as this authority *and* that marks its own PDAs as the *owner* of all
created open orders accounts, making the program the sole arbiter over
who can trade on a given market.
For example, this program forces all trades that execute on this market
to set the referral to a hardcoded address--`referral::ID`--and requires
the client to pass in an identity token, authorizing the user.
See the [code](https://github.com/project-serum/permissioned-markets-quickstart/blob/master/programs/permissioned-markets/src/lib.rs).
## Developing
This program requires building the Serum DEX from source, which is done using
git submodules.
### Install Submodules
Pull the source
```
git submodule init
git submodule update
```
### Build
[Anchor](https://github.com/project-serum/anchor) is used for developoment, and it's
recommended workflow is used here. To get started, see the [guide](https://project-serum.github.io/anchor/getting-started/introduction.html).
Verify the installation with `anchor -h` and build the dex.
```
make build-dex
```
### Test
A set of integration tests are provided. See these for an example of how to use a
permissioned market from JavaScript.
```bash
make test
```
### Localnet
To start a localnetwork with both the dex and proxy deployed and an orderbook
listed with posted orders, first install the "crank" cli.
```
cargo install --git https://github.com/project-serum/serum-dex --branch armani/auth crank --locked
```
Then run,
```bash
make localnet
```
### Connect a GUI
To connect a GUI to the localnet, either run one locally or go to
dex.projectserum.com and select the *localnet* network and enter the
market address: `FcZntrVjDRPv8JnU2mHt8ejvvA1eiHqxM8d8JNEC8q9q`.
In short, go to this [link](https://dex.projectserum.com/#/market/FcZntrVjDRPv8JnU2mHt8ejvvA1eiHqxM8d8JNEC8q9q).
Don't forget to click the `+` button to "Add a custom market" so that the GUI
can recognize the market running locally.
## Extending the Proxy
To implement a custom proxy, one can implement the [MarketMiddleware](https://github.com/project-serum/permissioned-markets-quickstart/blob/master/programs/permissioned-markets/src/lib.rs#L71) trait
to intercept, modify, and perform any access control on DEX requests before
they get forwarded to the orderbook. These middleware can be mixed and
matched. Note, however, that the order of middleware matters since they can
mutate the request.
One useful pattern is to treat the request like layers of an onion, where
each middleware unwraps the request by stripping accounts and instruction
data before relaying it to the next middleware and ultimately to the
orderbook. This allows one to easily extend the behavior of a proxy by
adding a custom middleware that may process information that is unknown to
any other middleware or to the DEX.
After adding a middleware, the only additional requirement, of course, is
to make sure the client sending transactions does the same, but in reverse.
It should wrap the transaction in the opposite order. For convenience, an
identical abstraction is provided in the JavaScript [client](https://github.com/project-serum/permissioned-markets-quickstart/blob/master/tests/utils/market-proxy.js#L15).
## Alternatives to Middleware
Note that this middleware abstraction is not required to host a
permissioned market. One could write a regular program that manages the PDAs
and CPI invocations onesself.

1
deps/serum-dex vendored Submodule

@ -0,0 +1 @@
Subproject commit 86a413a0f99eb7b2195f797e60183878d82e7fed

13
migrations/deploy.js Normal file
View File

@ -0,0 +1,13 @@
// Migrations are an early feature. Currently, they're nothing more than this
// single deploy script that's invoked from the CLI, injecting a provider
// configured from the workspace's Anchor.toml.
const anchor = require("@project-serum/anchor");
module.exports = async function (provider) {
// Configure client to use the provider.
anchor.setProvider(provider);
// Add your deploy script here.
}

View File

@ -0,0 +1,20 @@
[package]
name = "permissioned-markets"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "permissioned_markets"
[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = { git = "https://github.com/project-serum/anchor" }
anchor-spl = { git = "https://github.com/project-serum/anchor" }
solana-program = "1.7.4"

View File

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

View File

@ -0,0 +1,155 @@
// Note. This example depends on unreleased Serum DEX changes.
use anchor_lang::prelude::*;
use anchor_spl::dex::serum_dex::instruction::{CancelOrderInstructionV2, NewOrderInstructionV3};
use anchor_spl::dex::{
Context, Logger, MarketMiddleware, MarketProxy, OpenOrdersPda, ReferralFees,
};
use solana_program::account_info::AccountInfo;
use solana_program::entrypoint::ProgramResult;
use solana_program::pubkey::Pubkey;
use solana_program::sysvar::rent;
/// # Permissioned Markets
///
/// This demonstrates how to create "permissioned markets" on Serum via a proxy.
/// A permissioned market is a regular Serum market with an additional
/// open orders authority, which must sign every transaction to create an open
/// orders account.
///
/// In practice, what this means is that one can create a program that acts
/// as this authority *and* that marks its own PDAs as the *owner* of all
/// created open orders accounts, making the program the sole arbiter over
/// who can trade on a given market.
///
/// For example, this example forces all trades that execute on this market
/// to set the referral to a hardcoded address--`referral::ID`--and requires
/// the client to pass in an identity token, authorizing the user.
///
/// # Extending the proxy via middleware
///
/// To implement a custom proxy, one can implement the `MarketMiddleware` trait
/// to intercept, modify, and perform any access control on DEX requests before
/// they get forwarded to the orderbook. These middleware can be mixed and
/// matched. Note, however, that the order of middleware matters since they can
/// mutate the request.
///
/// One useful pattern is to treat the request like layers of an onion, where
/// each middleware unwraps the request by stripping accounts and instruction
/// data before relaying it to the next middleware and ultimately to the
/// orderbook. This allows one to easily extend the behavior of a proxy by
/// adding a custom middleware that may process information that is unknown to
/// any other middleware or to the DEX.
///
/// After adding a middleware, the only additional requirement, of course, is
/// to make sure the client sending transactions does the same, but in reverse.
/// It should wrap the transaction in the opposite order. For convenience, an
/// identical abstraction is provided in the JavaScript client.
///
/// # Alternatives to middleware
///
/// Note that this middleware abstraction is not required to host a
/// permissioned market. One could write a regular program that manages the PDAs
/// and CPI invocations onesself, if desired.
#[program]
pub mod permissioned_markets {
use super::*;
pub fn entry(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
MarketProxy::new()
.middleware(&mut Logger)
.middleware(&mut Identity)
.middleware(&mut ReferralFees::new(referral::ID))
.middleware(&mut OpenOrdersPda::new())
.run(program_id, accounts, data)
}
}
/// Performs token based authorization, confirming the identity of the user.
/// The identity token must be given as the fist account.
struct Identity;
impl MarketMiddleware for Identity {
/// Accounts:
///
/// 0. Authorization token.
/// ..
fn init_open_orders(&self, ctx: &mut Context) -> ProgramResult {
verify_and_strip_auth(ctx)
}
/// Accounts:
///
/// 0. Authorization token.
/// ..
fn new_order_v3(&self, ctx: &mut Context, _ix: &NewOrderInstructionV3) -> ProgramResult {
verify_and_strip_auth(ctx)
}
/// Accounts:
///
/// 0. Authorization token.
/// ..
fn cancel_order_v2(&self, ctx: &mut Context, _ix: &CancelOrderInstructionV2) -> ProgramResult {
verify_and_strip_auth(ctx)
}
/// Accounts:
///
/// 0. Authorization token.
/// ..
fn cancel_order_by_client_id_v2(&self, ctx: &mut Context, _client_id: u64) -> ProgramResult {
verify_and_strip_auth(ctx)
}
/// Accounts:
///
/// 0. Authorization token.
/// ..
fn settle_funds(&self, ctx: &mut Context) -> ProgramResult {
verify_and_strip_auth(ctx)
}
/// Accounts:
///
/// 0. Authorization token.
/// ..
fn close_open_orders(&self, ctx: &mut Context) -> ProgramResult {
verify_and_strip_auth(ctx)
}
/// Accounts:
///
/// 0. Authorization token.
/// ..
fn fallback(&self, ctx: &mut Context) -> ProgramResult {
verify_and_strip_auth(ctx)
}
}
// Utils.
fn verify_and_strip_auth(ctx: &mut Context) -> ProgramResult {
// The rent sysvar is used as a dummy example of an identity token.
let auth = &ctx.accounts[0];
require!(auth.key == &rent::ID, InvalidAuth);
// Strip off the account before possing on the message.
ctx.accounts = (&ctx.accounts[1..]).to_vec();
Ok(())
}
// Error.
#[error]
pub enum ErrorCode {
#[msg("Invalid auth token provided")]
InvalidAuth,
}
// Constants.
pub mod referral {
// This is a dummy address for testing. Do not use in production.
solana_program::declare_id!("3oSfkjQZKCneYvsCTZc9HViGAPqR8pYr4h9YeGB5ZxHf");
}

23
scripts/list-market.js Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env node
// Script to list a permissioned market, logging the address to stdout.
const utils = require("../tests/utils");
const utilsCmn = require("../tests/utils/common");
const anchor = require("@project-serum/anchor");
const provider = anchor.Provider.local();
const program = anchor.workspace.PermissionedMarkets;
const Account = anchor.web3.Account;
async function main() {
const { marketProxyClient } = await utils.genesis({
provider,
proxyProgramId: utilsCmn.PROGRAM_KP.publicKey,
});
const out = {
market: marketProxyClient.market.address.toString(),
};
console.log(JSON.stringify(out));
}
main();

110
scripts/localnet.sh Executable file
View File

@ -0,0 +1,110 @@
#!/usr/bin/env bash
################################################################################
#
# localnet.sh runs a localnet with a permissioned market up and running.
#
# Usage:
#
# ./localnet.sh
#
# Installation:
#
# Before using, one must make sure to have the serum crank software built or
# installed locally on one's machine. To build, clone the serum dex and run
# `cargo build` inside `serum-dex/dex/crank`. Then change the $CRANK variable
# below.
#
################################################################################
DEX_PID="9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
PAYER_FILEPATH="$HOME/.config/solana/id.json"
#
# If the crank cli isn't installed, use a local path.
# Replace the path below if you'd like to use your own build.
#
CRANK=$(which crank)
if [ $? -ne 0 ]; then
CRANK="/home/armaniferrante/Documents/code/src/github.com/project-serum/permissioned-markets-quickstart/deps/serum-dex/target/debug/crank"
fi
LOG_DIR="./localnet-logs"
VALIDATOR_OUT="${LOG_DIR}/validator-stdout.txt"
CRANK_LOGS="${LOG_DIR}/crank-logs.txt"
CRANK_STDOUT="${LOG_DIR}/crank-stdout.txt"
TRADE_BOT_STDOUT="${LOG_DIR}/trade-bot-stdout.txt"
echo "CRANK: $CRANK"
set -euo pipefail
main () {
#
# Cleanup.
#
echo "Cleaning old output files..."
mkdir -p $LOG_DIR
rm -rf test-ledger
rm -f $TRADE_BOT_STDOUT
rm -f $VALIDATOR_OUT
rm -f $CRANK_LOGS && touch $CRANK_LOGS
#
# Bootup cluster.
#
echo "Starting local network..."
solana-test-validator \
--bpf-program 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin ./deps/serum-dex/dex/target/deploy/serum_dex.so \
--bpf-program CGnjwsdrQfJpiXqeAe1p13pUsfZnS9rgEy1DWKimGHqo ./target/deploy/permissioned_markets.so > $VALIDATOR_OUT &
sleep 2
#
# List market at a predetermined address.
#
local market="FcZntrVjDRPv8JnU2mHt8ejvvA1eiHqxM8d8JNEC8q9q"
echo "Listing market $market..."
./scripts/list-market.js
sleep 10
echo "Market listed $market"
echo "Run the trade bot"
./scripts/trade-bot.js $market > $TRADE_BOT_STDOUT &
echo "Running crank..."
$CRANK localnet consume-events \
-c $market \
-d $DEX_PID -e 5 \
--log-directory $CRANK_LOGS \
--market $market \
--num-workers 1 \
--payer $PAYER_FILEPATH \
--pc-wallet $market > $CRANK_STDOUT &
#
# Park.
#
echo "Localnet running..."
echo "Ctl-c to exit."
wait
}
cleanup() {
pkill -P $$ || true
wait || true
}
trap_add() {
trap_add_cmd=$1; shift || fatal "${FUNCNAME} usage error"
for trap_add_name in "$@"; do
trap -- "$(
extract_trap_cmd() { printf '%s\n' "${3:-}"; }
eval "extract_trap_cmd $(trap -p "${trap_add_name}")"
printf '%s\n' "${trap_add_cmd}"
)" "${trap_add_name}" \
|| fatal "unable to add to trap ${trap_add_name}"
done
}
declare -f -t trap_add
trap_add 'cleanup' EXIT
main

180
scripts/trade-bot.js Executable file
View File

@ -0,0 +1,180 @@
#!/usr/bin/env node
// Script to infinitely post orders that are immediately filled.
const process = require("process");
const anchor = require("@project-serum/anchor");
const PublicKey = anchor.web3.PublicKey;
const marketMaker = require("../tests/utils/market-maker");
const MARKET_MAKER = marketMaker.KEYPAIR;
async function main() {
const market = new PublicKey(process.argv[2]);
const provider = anchor.Provider.local();
// TODO: enable the trade bot.
// runTradeBot(market, provider);
}
async function runTradeBot(market, provider, iterations = undefined) {
const marketProxyClient = marketProxy.load(
provider.connection,
proxyProgramId,
DEX_PID,
market
);
const baseTokenUser1 = (
await marketProxyClient.market.getTokenAccountsByOwnerForMint(
provider.connection,
MARKET_MAKER.publicKey,
marketProxyClient.market.baseMintAddress
)
)[0].pubkey;
const quoteTokenUser1 = (
await marketProxyClient.market.getTokenAccountsByOwnerForMint(
provider.connection,
MARKET_MAKER.publicKey,
marketProxyClient.market.quoteMintAddress
)
)[0].pubkey;
const baseTokenUser2 = (
await marketProxyClient.market.getTokenAccountsByOwnerForMint(
provider.connection,
provider.wallet.publicKey,
marketProxyClient.market.baseMintAddress
)
)[0].pubkey;
const quoteTokenUser2 = (
await marketProxyClient.market.getTokenAccountsByOwnerForMint(
provider.connection,
provider.wallet.publicKey,
marketProxyClient.market.quoteMintAddress
)
)[0].pubkey;
const makerOpenOrdersUser1 = (
await OpenOrders.findForMarketAndOwner(
provider.connection,
market,
MARKET_MAKER.publicKey,
DEX_PID
)
)[0];
makerOpenOrdersUser2 = (
await OpenOrders.findForMarketAndOwner(
provider.connection,
market,
provider.wallet.publicKey,
DEX_PID
)
)[0];
const price = 6.041;
const size = 700000.8;
let maker = MARKET_MAKER;
let taker = provider.wallet.payer;
let baseToken = baseTokenUser1;
let quoteToken = quoteTokenUser2;
let makerOpenOrders = makerOpenOrdersUser1;
let k = 1;
while (true) {
if (iterations && k > iterations) {
break;
}
const clientId = new BN(k);
if (k % 5 === 0) {
if (maker.publicKey.equals(MARKET_MAKER.publicKey)) {
maker = provider.wallet.payer;
makerOpenOrders = makerOpenOrdersUser2;
taker = MARKET_MAKER;
baseToken = baseTokenUser2;
quoteToken = quoteTokenUser1;
} else {
maker = MARKET_MAKER;
makerOpenOrders = makerOpenOrdersUser1;
taker = provider.wallet.payer;
baseToken = baseTokenUser1;
quoteToken = quoteTokenUser2;
}
}
// Post ask.
const txAsk = new Transaction();
txAsk.add(
await marketProxyClient.instruction.newOrderV3({
owner: maker,
payer: baseToken,
side: "sell",
price,
size,
orderType: "postOnly",
clientId,
openOrdersAddressKey: undefined,
openOrdersAccount: undefined,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
})
);
let txSig = await provider.send(txAsk, [maker]);
console.log("Ask", txSig);
// Take.
const txBid = new Transaction();
tx.add(
await marketProxyClient.instruction.newOrderV3({
owner: taker,
payer: quoteToken,
side: "buy",
price,
size,
orderType: "ioc",
clientId: undefined,
openOrdersAddressKey: undefined,
openOrdersAccount: undefined,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
})
);
txSig = await provider.send(txBid, [taker]);
console.log("Bid", txSig);
await sleep(1000);
// Cancel anything remaining.
try {
const tx = new Transaction();
tx.add(
marketProxyClient.instruction.cancelOrderByClientId(
provider.connection,
maker.publicKey,
makerOpenOrders.address,
clientId
)
);
txSig = await provider.send(tx, [maker]);
console.log("Cancelled the rest", txSig);
await sleep(1000);
} catch (e) {
console.log("Unable to cancel order", e);
}
k += 1;
// If the open orders account wasn't previously initialized, it is now.
if (makerOpenOrdersUser2 === undefined) {
makerOpenOrdersUser2 = (
await OpenOrders.findForMarketAndOwner(
provider.connection,
market,
provider.wallet.publicKey,
DEX_PID
)
)[0];
}
}
}
main();

View File

@ -0,0 +1,234 @@
const assert = require("assert");
const { Token, TOKEN_PROGRAM_ID } = require("@solana/spl-token");
const anchor = require("@project-serum/anchor");
const serum = require("@project-serum/serum");
const { BN } = anchor;
const {
Keypair,
Transaction,
TransactionInstruction,
PublicKey,
SystemProgram,
SYSVAR_RENT_PUBKEY,
} = anchor.web3;
const {
DexInstructions,
OpenOrders,
OpenOrdersPda,
Logger,
ReferralFees,
MarketProxyBuilder,
} = serum;
const { genesis, sleep } = require("./utils");
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
const REFERRAL_AUTHORITY = new PublicKey(
"3oSfkjQZKCneYvsCTZc9HViGAPqR8pYr4h9YeGB5ZxHf"
);
describe("permissioned-markets", () => {
// Anchor client setup.
const provider = anchor.Provider.env();
anchor.setProvider(provider);
const program = anchor.workspace.PermissionedMarkets;
// Token client.
let usdcClient;
// Global DEX accounts and clients shared accross all tests.
let marketProxy, tokenAccount, usdcAccount;
let openOrders, openOrdersBump, openOrdersInitAuthority, openOrdersBumpinit;
let usdcPosted;
let referralTokenAddress;
it("BOILERPLATE: Initializes an orderbook", async () => {
const { marketProxyClient, godA, godUsdc, usdc } = await genesis({
provider,
proxyProgramId: program.programId,
});
marketProxy = marketProxyClient;
usdcAccount = godUsdc;
tokenAccount = godA;
usdcClient = new Token(
provider.connection,
usdc,
TOKEN_PROGRAM_ID,
provider.wallet.payer
);
referral = await usdcClient.createAccount(REFERRAL_AUTHORITY);
});
it("BOILERPLATE: Calculates open orders addresses", async () => {
const [_openOrders, bump] = await PublicKey.findProgramAddress(
[
anchor.utils.bytes.utf8.encode("open-orders"),
DEX_PID.toBuffer(),
marketProxy.market.address.toBuffer(),
program.provider.wallet.publicKey.toBuffer(),
],
program.programId
);
const [
_openOrdersInitAuthority,
bumpInit,
] = await PublicKey.findProgramAddress(
[
anchor.utils.bytes.utf8.encode("open-orders-init"),
DEX_PID.toBuffer(),
marketProxy.market.address.toBuffer(),
],
program.programId
);
// Save global variables re-used across tests.
openOrders = _openOrders;
openOrdersBump = bump;
openOrdersInitAuthority = _openOrdersInitAuthority;
openOrdersBumpInit = bumpInit;
});
it("Creates an open orders account", async () => {
const tx = new Transaction();
tx.add(
await marketProxy.instruction.initOpenOrders(
program.provider.wallet.publicKey,
marketProxy.market.address,
marketProxy.market.address, // Dummy. Replaced by middleware.
marketProxy.market.address // Dummy. Replaced by middleware.
)
);
await provider.send(tx);
const account = await provider.connection.getAccountInfo(openOrders);
assert.ok(account.owner.toString() === DEX_PID.toString());
});
it("Posts a bid on the orderbook", async () => {
const size = 1;
const price = 1;
usdcPosted = new BN(
marketProxy.market._decoded.quoteLotSize.toNumber()
).mul(
marketProxy.market
.baseSizeNumberToLots(size)
.mul(marketProxy.market.priceNumberToLots(price))
);
const tx = new Transaction();
tx.add(
marketProxy.instruction.newOrderV3({
owner: program.provider.wallet.publicKey,
payer: usdcAccount,
side: "buy",
price,
size,
orderType: "postOnly",
clientId: new BN(999),
openOrdersAddressKey: openOrders,
selfTradeBehavior: "abortTransaction",
})
);
await provider.send(tx);
});
it("Cancels a bid on the orderbook", async () => {
// Given.
const beforeOoAccount = await OpenOrders.load(
provider.connection,
openOrders,
DEX_PID
);
// When.
const tx = new Transaction();
tx.add(
await marketProxy.instruction.cancelOrderByClientId(
program.provider.wallet.publicKey,
openOrders,
new BN(999)
)
);
await provider.send(tx);
// Then.
const afterOoAccount = await OpenOrders.load(
provider.connection,
openOrders,
DEX_PID
);
assert.ok(beforeOoAccount.quoteTokenFree.eq(new BN(0)));
assert.ok(beforeOoAccount.quoteTokenTotal.eq(usdcPosted));
assert.ok(afterOoAccount.quoteTokenFree.eq(usdcPosted));
assert.ok(afterOoAccount.quoteTokenTotal.eq(usdcPosted));
});
// Need to crank the cancel so that we can close later.
it("Cranks the cancel transaction", async () => {
// TODO: can do this in a single transaction if we covert the pubkey bytes
// into a [u64; 4] array and sort. I'm lazy though.
let eq = await marketProxy.market.loadEventQueue(provider.connection);
while (eq.length > 0) {
const tx = new Transaction();
tx.add(
marketProxy.market.makeConsumeEventsInstruction([eq[0].openOrders], 1)
);
await provider.send(tx);
eq = await marketProxy.market.loadEventQueue(provider.connection);
}
});
it("Settles funds on the orderbook", async () => {
// Given.
const beforeTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
// When.
const tx = new Transaction();
tx.add(
await marketProxy.instruction.settleFunds(
openOrders,
provider.wallet.publicKey,
tokenAccount,
usdcAccount,
referral
)
);
await provider.send(tx);
// Then.
const afterTokenAccount = await usdcClient.getAccountInfo(usdcAccount);
assert.ok(
afterTokenAccount.amount.sub(beforeTokenAccount.amount).toNumber() ===
usdcPosted.toNumber()
);
});
it("Closes an open orders account", async () => {
// Given.
const beforeAccount = await program.provider.connection.getAccountInfo(
program.provider.wallet.publicKey
);
// When.
const tx = new Transaction();
tx.add(
marketProxy.instruction.closeOpenOrders(
openOrders,
provider.wallet.publicKey,
provider.wallet.publicKey
)
);
await provider.send(tx);
// Then.
const afterAccount = await program.provider.connection.getAccountInfo(
program.provider.wallet.publicKey
);
const closedAccount = await program.provider.connection.getAccountInfo(
openOrders
);
assert.ok(23352768 === afterAccount.lamports - beforeAccount.lamports);
assert.ok(closedAccount === null);
});
});

81
tests/utils/common.js Normal file
View File

@ -0,0 +1,81 @@
const { PublicKey, Account } = require("@project-serum/anchor").web3;
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
// This msut be kept in sync with `scripts/localnet.sh`.
const PROGRAM_KP = new Account([
168,
86,
206,
125,
127,
105,
201,
250,
37,
102,
161,
124,
80,
181,
60,
2,
166,
123,
176,
161,
228,
188,
134,
186,
158,
68,
197,
240,
202,
193,
174,
234,
167,
123,
252,
186,
72,
51,
203,
70,
153,
234,
190,
2,
134,
184,
197,
156,
113,
8,
65,
1,
83,
220,
152,
62,
200,
174,
40,
180,
218,
61,
224,
6,
]);
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = {
sleep,
DEX_PID,
PROGRAM_KP,
};

97
tests/utils/faucet.js Normal file
View File

@ -0,0 +1,97 @@
const anchor = require("@project-serum/anchor");
const BN = anchor.BN;
const { Account, Transaction, SystemProgram } = anchor.web3;
const serumCmn = require("@project-serum/common");
const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
const DECIMALS = 6;
// Creates mints and a token account funded with each mint.
async function createMintGods(provider, mintCount) {
// Setup mints with initial tokens owned by the provider.
let mintGods = [];
for (let k = 0; k < mintCount; k += 1) {
const [mint, god] = await serumCmn.createMintAndVault(
provider,
new BN("1000000000000000000"),
undefined,
DECIMALS
);
mintGods.push({ mint, god });
}
return mintGods;
}
async function createFundedAccount(provider, mints, newAccount) {
if (!newAccount) {
newAccount = new Account();
}
const marketMaker = {
tokens: {},
account: newAccount,
};
// Transfer lamports to market maker.
await provider.send(
(() => {
const tx = new Transaction();
tx.add(
SystemProgram.transfer({
fromPubkey: provider.wallet.publicKey,
toPubkey: newAccount.publicKey,
lamports: 100000000000,
})
);
return tx;
})()
);
// Transfer SPL tokens to the market maker.
for (let k = 0; k < mints.length; k += 1) {
const { mint, god, amount } = mints[k];
let MINT_A = mint;
let GOD_A = god;
// Setup token accounts owned by the market maker.
const mintAClient = new Token(
provider.connection,
MINT_A,
TOKEN_PROGRAM_ID,
provider.wallet.payer // node only
);
const marketMakerTokenA = await mintAClient.createAccount(
newAccount.publicKey
);
await provider.send(
(() => {
const tx = new Transaction();
tx.add(
Token.createTransferCheckedInstruction(
TOKEN_PROGRAM_ID,
GOD_A,
MINT_A,
marketMakerTokenA,
provider.wallet.publicKey,
[],
amount,
DECIMALS
)
);
return tx;
})()
);
marketMaker.tokens[mint.toString()] = marketMakerTokenA;
}
return marketMaker;
}
module.exports = {
createMintGods,
createFundedAccount,
DECIMALS,
};

98
tests/utils/index.js Normal file
View File

@ -0,0 +1,98 @@
const { BN } = require("@project-serum/anchor");
const { PublicKey } = require("@project-serum/anchor").web3;
const marketProxy = require("./market-proxy");
const marketLister = require("./market-lister");
const faucet = require("./faucet");
const { DEX_PID } = require("./common");
const marketMaker = require("./market-maker");
// Initializes the genesis state for the tests and localnetwork.
async function genesis({ provider, proxyProgramId }) {
//
// Create all mints and funded god accounts.
//
const mintGods = await faucet.createMintGods(provider, 2);
const [mintGodA, mintGodB] = mintGods;
//
// Fund an additional account.
//
const fundedAccount = await faucet.createFundedAccount(
provider,
mintGods.map((mintGod) => {
return {
...mintGod,
amount: new BN("10000000000000").muln(10 ** faucet.DECIMALS),
};
}),
marketMaker.KEYPAIR
);
//
// Structure the market maker object.
//
const marketMakerAccounts = {
...fundedAccount,
baseToken: fundedAccount.tokens[mintGodA.mint.toString()],
quoteToken: fundedAccount.tokens[mintGodB.mint.toString()],
};
//
// List the market.
//
const [marketAPublicKey] = await marketLister.list({
connection: provider.connection,
wallet: provider.wallet,
baseMint: mintGodA.mint,
quoteMint: mintGodB.mint,
baseLotSize: 100000,
quoteLotSize: 100,
dexProgramId: DEX_PID,
proxyProgramId,
feeRateBps: 0,
});
//
// Load a proxy client for the market.
//
const marketProxyClient = await marketProxy.load(
provider.connection,
proxyProgramId,
DEX_PID,
marketAPublicKey
);
//
// Market maker initializes an open orders account.
//
await marketMaker.initOpenOrders(
provider,
marketProxyClient,
marketMakerAccounts
);
//
// Market maker posts trades on the orderbook.
//
await marketMaker.postOrders(
provider,
marketProxyClient,
marketMakerAccounts
);
//
// Done.
//
return {
marketProxyClient,
mintA: mintGodA.mint,
usdc: mintGodB.mint,
godA: mintGodA.god,
godUsdc: mintGodB.god,
};
}
module.exports = {
genesis,
DEX_PID,
};

View File

@ -0,0 +1,237 @@
const anchor = require("@project-serum/anchor");
const { BN } = anchor;
const {
Account,
PublicKey,
Transaction,
SystemProgram,
} = require("@project-serum/anchor").web3;
const { TOKEN_PROGRAM_ID } = require("@solana/spl-token");
const {
DexInstructions,
TokenInstructions,
OpenOrdersPda,
MARKET_STATE_LAYOUT_V3,
} = require("@project-serum/serum");
const { DEX_PID } = require("./common");
// Creates a market on the dex.
async function list({
connection,
wallet,
baseMint,
quoteMint,
baseLotSize,
quoteLotSize,
dexProgramId,
proxyProgramId,
feeRateBps,
}) {
const market = MARKET_KP;
const requestQueue = new Account();
const eventQueue = new Account();
const bids = new Account();
const asks = new Account();
const baseVault = new Account();
const quoteVault = new Account();
const quoteDustThreshold = new BN(100);
const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(
market.publicKey,
dexProgramId
);
const tx1 = new Transaction();
tx1.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: baseVault.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: quoteVault.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
TokenInstructions.initializeAccount({
account: baseVault.publicKey,
mint: baseMint,
owner: vaultOwner,
}),
TokenInstructions.initializeAccount({
account: quoteVault.publicKey,
mint: quoteMint,
owner: vaultOwner,
})
);
const tx2 = new Transaction();
tx2.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: market.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
MARKET_STATE_LAYOUT_V3.span
),
space: MARKET_STATE_LAYOUT_V3.span,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: requestQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
space: 5120 + 12,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: eventQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
space: 262144 + 12,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: bids.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
space: 65536 + 12,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: asks.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
space: 65536 + 12,
programId: dexProgramId,
}),
DexInstructions.initializeMarket({
market: market.publicKey,
requestQueue: requestQueue.publicKey,
eventQueue: eventQueue.publicKey,
bids: bids.publicKey,
asks: asks.publicKey,
baseVault: baseVault.publicKey,
quoteVault: quoteVault.publicKey,
baseMint,
quoteMint,
baseLotSize: new BN(baseLotSize),
quoteLotSize: new BN(quoteLotSize),
feeRateBps,
vaultSignerNonce,
quoteDustThreshold,
programId: dexProgramId,
authority: await OpenOrdersPda.marketAuthority(
market.publicKey,
DEX_PID,
proxyProgramId
),
})
);
const transactions = [
{ transaction: tx1, signers: [baseVault, quoteVault] },
{
transaction: tx2,
signers: [market, requestQueue, eventQueue, bids, asks],
},
];
for (let tx of transactions) {
await anchor.getProvider().send(tx.transaction, tx.signers);
}
const acc = await connection.getAccountInfo(market.publicKey);
return [market.publicKey, vaultOwner];
}
async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) {
const nonce = new BN(0);
while (nonce.toNumber() < 255) {
try {
const vaultOwner = await PublicKey.createProgramAddress(
[marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
dexProgramId
);
return [vaultOwner, nonce];
} catch (e) {
nonce.iaddn(1);
}
}
throw new Error("Unable to find nonce");
}
// Dummy keypair for a consistent market address. Helpful when doing UI work.
// Don't use in production.
const MARKET_KP = new Account([
13,
174,
53,
150,
78,
228,
12,
98,
170,
254,
212,
211,
125,
193,
2,
241,
97,
137,
49,
209,
189,
199,
27,
215,
220,
65,
57,
203,
215,
93,
105,
203,
217,
32,
5,
194,
157,
118,
162,
47,
102,
126,
235,
65,
99,
80,
56,
231,
217,
114,
25,
225,
239,
140,
169,
92,
150,
146,
211,
218,
183,
139,
9,
104,
]);
module.exports = {
list,
};

160
tests/utils/market-maker.js Normal file
View File

@ -0,0 +1,160 @@
const { Account, Transaction } = require("@project-serum/anchor").web3;
const { OpenOrdersPda } = require("@project-serum/serum");
// Dummy keypair.
const KEYPAIR = new Account([
54,
213,
91,
255,
163,
120,
88,
183,
223,
23,
220,
204,
82,
117,
212,
214,
118,
184,
2,
29,
89,
149,
22,
233,
108,
177,
60,
249,
218,
166,
30,
221,
59,
168,
233,
123,
204,
37,
123,
124,
86,
176,
214,
12,
63,
195,
231,
15,
1,
143,
7,
7,
232,
38,
69,
214,
45,
58,
115,
55,
129,
25,
228,
30,
]);
async function initOpenOrders(provider, marketProxy, marketMakerAccounts) {
const tx = new Transaction();
tx.add(
marketProxy.instruction.initOpenOrders(
marketMakerAccounts.account.publicKey,
marketProxy.market.address,
marketProxy.market.address, // Dummy. Replaced by middleware.
marketProxy.market.address // Dummy. Replaced by middleware.
)
);
let signers = [marketMakerAccounts.account];
await provider.send(tx, signers);
}
async function postOrders(provider, marketProxy, marketMakerAccounts) {
const asks = [
[6.041, 7.8],
[6.051, 72.3],
[6.055, 5.4],
[6.067, 15.7],
[6.077, 390.0],
[6.09, 24.0],
[6.11, 36.3],
[6.133, 300.0],
[6.167, 687.8],
];
const bids = [
[6.004, 8.5],
[5.995, 12.9],
[5.987, 6.2],
[5.978, 15.3],
[5.965, 82.8],
[5.961, 25.4],
];
const openOrdersAddressKey = await OpenOrdersPda.openOrdersAddress(
marketProxy.market.address,
marketMakerAccounts.account.publicKey,
marketProxy.dexProgramId,
marketProxy.proxyProgramId
);
// Use an explicit signer because the provider wallet, which pays for
// the tx, is different from the market maker wallet.
let signers = [marketMakerAccounts.account];
for (let k = 0; k < asks.length; k += 1) {
let ask = asks[k];
const tx = new Transaction();
tx.add(
await marketProxy.instruction.newOrderV3({
owner: marketMakerAccounts.account.publicKey,
payer: marketMakerAccounts.baseToken,
side: "sell",
price: ask[0],
size: ask[1],
orderType: "postOnly",
clientId: undefined,
openOrdersAddressKey,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
})
);
await provider.send(tx, signers);
}
for (let k = 0; k < bids.length; k += 1) {
let bid = bids[k];
const tx = new Transaction();
tx.add(
await marketProxy.instruction.newOrderV3({
owner: marketMakerAccounts.account.publicKey,
payer: marketMakerAccounts.quoteToken,
side: "buy",
price: bid[0],
size: bid[1],
orderType: "postOnly",
clientId: undefined,
openOrdersAddressKey,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
})
);
await provider.send(tx, signers);
}
}
module.exports = {
postOrders,
initOpenOrders,
KEYPAIR,
};

View File

@ -0,0 +1,64 @@
const { SYSVAR_RENT_PUBKEY } = require("@solana/web3.js");
const {
OpenOrders,
OpenOrdersPda,
Logger,
ReferralFees,
MarketProxyBuilder,
} = require("/home/armaniferrante/Documents/code/src/github.com/project-serum/serum-ts/packages/serum");
// Returns a client for the market proxy.
//
// If changing the program, one will likely need to change the builder/middleware
// here as well.
async function load(connection, proxyProgramId, dexProgramId, market) {
return new MarketProxyBuilder()
.middleware(
new OpenOrdersPda({
proxyProgramId,
dexProgramId,
})
)
.middleware(new ReferralFees())
.middleware(new Identity())
.middleware(new Logger())
.load({
connection,
market,
dexProgramId,
proxyProgramId,
options: { commitment: "recent" },
});
}
// Dummy identity middleware used for testing.
class Identity {
initOpenOrders(ix) {
this.proxy(ix);
}
newOrderV3(ix) {
this.proxy(ix);
}
cancelOrderV2(ix) {
this.proxy(ix);
}
cancelOrderByClientIdV2(ix) {
this.proxy(ix);
}
settleFunds(ix) {
this.proxy(ix);
}
closeOpenOrders(ix) {
this.proxy(ix);
}
proxy(ix) {
ix.keys = [
{ pubkey: SYSVAR_RENT_PUBKEY, isWritable: false, isSigner: false },
...ix.keys,
];
}
}
module.exports = {
load,
};