Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Demo: swap feature and skeleton UI/SDK step definitions #257

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/bdd/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require("@swim-io/eslint-config/patch/modern-module-resolution.cjs");

module.exports = {
extends: ["@swim-io/eslint-config"],
parserOptions: {
// Make sure correct `tsconfig.json` is found in monorepo
tsconfigRootDir: __dirname,
},
};
2 changes: 2 additions & 0 deletions packages/bdd/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
types/
3 changes: 3 additions & 0 deletions packages/bdd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# BDD

Swim multi-loop BDD features and step definitions.
14 changes: 14 additions & 0 deletions packages/bdd/cucumber.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"sdk": {
"publishQuiet": true,
"requireModule": ["ts-node/register"],
"require": ["step_definitions/sdk/*.ts"],
"tags": "@sdk"
},
"ui": {
"publishQuiet": true,
"requireModule": ["ts-node/register"],
"require": ["step_definitions/ui/*.ts"],
"tags": "@ui"
}
}
35 changes: 35 additions & 0 deletions packages/bdd/features/swap.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@sdk
@ui
Feature: Swap

As a crypto enthusiast
Patrick Swapze wants to swap between different tokens which live on various blockchains
So that he can trade on whichever platform with whichever tokens are most profitable at any time


Background:

Given there is a Solana blockchain
And Patrick has a Solana wallet with address "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J"
And Patrick has 5 SOL in his Solana wallet
And Patrick has a USDC token account with address "TP19UrkLUihiEg3y98VjM8Gmh7GjWayucsbpyo195wC"
And Patrick has a USDT token account with address "TP2gzosaKJNf5UjM8eWKKnN7Yni1uLbYJr88rvEvgPA"


Example: Solana USDC -> Solana USDT

Given there is a Solana-only pool with USDC and USDT tokens, an amp factor of 1000, an LP fee of 0.03%, and a governance fee of 0.01%
And 1000 USDC has been deposited into the pool
And 1000 USDT has been deposited into the pool
And Patrick has 100 USDC tokens in his Solana wallet
And Patrick has 100 USDT tokens in his Solana wallet

When Patrick swaps an exact input of 10 USDC for USDT with a slippage setting of 0.5%

Then Patrick's USDC balance should be 90
And Patrick's USDT balance should be at least 109.945


Example: Solana USDC -> Ethereum USDC

Given somebody has written the example
5 changes: 5 additions & 0 deletions packages/bdd/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};
57 changes: 57 additions & 0 deletions packages/bdd/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@swim-io/bdd",
"private": true,
"version": "0.0.0",
"description": "Swim multi-loop BDD features and step definitions.",
"main": "build",
"types": "types",
"files": [
"build/",
"types/",
"*.md",
"!*.test.*",
"!**/fixtures/"
],
"repository": {
"type": "git",
"url": "https://github.com/swim-io/swim/tree/master/packages/bdd"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"scripts": {
"typecheck": "tsc",
"format": "prettier --write \"./step_definitions/**/*.ts\"",
"format:check": "prettier --check \"./step_definitions/**/*.ts\"",
"lint": "eslint \"./step_definitions/**/*.ts\"",
"lint:fix": "eslint --fix \"./step_definitions/**/*.ts\"",
"test:sdk": "cucumber-js --profile sdk",
"test:ui": "cucumber-js --profile ui",
"test": "yarn test:sdk && yarn test:ui",
"verify": "yarn typecheck && yarn format:check && yarn lint && yarn test --watchAll=false",
"build": "rm -rf ./build/ ./types/ ./tsconfig.tsbuildinfo && tsc",
"prepare": "yarn verify && yarn build"
},
"devDependencies": {
"@cucumber/cucumber": "^8.5.1",
"@solana/web3.js": "^1.44.3",
"@swim-io/eslint-config": "workspace:^",
"@swim-io/solana-usdc-usdt-swap": "workspace:^",
"@swim-io/tsconfig": "workspace:^",
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"decimal.js": "^10.3.1",
"eslint": "^8.18.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-deprecation": "^1.3.2",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^4.2.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^25.7.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.7.1",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
}
}
73 changes: 73 additions & 0 deletions packages/bdd/step_definitions/sdk/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
import assert from "assert";

import { Given, Then } from "@cucumber/cucumber";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import Decimal from "decimal.js";

async function getUserBalance(this: any, token: string): Promise<Decimal> {
const userTokenKey = this.userTokenKeys[token] as PublicKey;
const solanaConnection = this.solanaConnection as Connection;
const { value } = await solanaConnection.getTokenAccountBalance(userTokenKey);
return new Decimal(value.amount).div(value.decimals);
}

Given("there is a Solana blockchain", function () {
// TODO: Set up a development chain
this.solanaConnection = new Connection("http://127.0.0.1");
});

Given(
"{word} has a Solana wallet with address {string}",
function (user: string, address: string) {
// TODO: Use a look-up table to match addresses to secret keys
this.solanaWallet = Keypair.fromSecretKey(
Uint8Array.from([
14, 173, 153, 4, 176, 224, 201, 111, 32, 237, 183, 185, 159, 247, 22,
161, 89, 84, 215, 209, 212, 137, 10, 92, 157, 49, 29, 192, 101, 164,
152, 70, 87, 65, 8, 174, 214, 157, 175, 126, 98, 90, 54, 24, 100, 177,
247, 77, 19, 112, 47, 44, 165, 109, 233, 102, 14, 86, 109, 29, 134, 145,
132, 141,
]),
);
},
);

Given(
"{word} has {float} SOL in his Solana wallet",
function (user: string, amount: number) {
// TODO: Seed wallet with SOL
},
);

Given(
"{word} has a {word} token account with address {string}",
function (user: string, token: string, address: string) {
// TODO: Create token account
this.userTokenKeys = this.userTokenKeys ?? {};
this.userTokenKeys[token] = new PublicKey(address);
},
);

Given(
"{word} has {float} {word} tokens in his Solana wallet",
function (user: string, amount: number, token: string) {
// TODO: Seed wallet with tokens
},
);

Then(
"{word}'s {word} balance should be {float}",
async function (user: string, token: string, amount: number) {
const balance = await getUserBalance.call(this, token);
assert(balance.equals(amount));
},
);

Then(
"{word}'s {word} balance should be at least {float}",
async function (user: string, token: string, amount: number) {
const balance = await getUserBalance.call(this, token);
assert(balance.greaterThanOrEqualTo(amount));
},
);
82 changes: 82 additions & 0 deletions packages/bdd/step_definitions/sdk/swim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Given, When } from "@cucumber/cucumber";
import type { Connection, PublicKey } from "@solana/web3.js";
import { Keypair, Transaction } from "@solana/web3.js";
import {
SwapDirection,
createApproveAndSwapIxs,
createPoolMath,
fetchSwimPool,
} from "@swim-io/solana-usdc-usdt-swap";
import Decimal from "decimal.js";

Given(
"there is a Solana-only pool with {word} and {word} tokens, an amp factor of {float}, an LP fee of {float}%, and a governance fee of {float}%",
function (
token1: string,
token2: string,
ampFactor: number,
lpFee: number,
governanceFee: number,
) {
// TODO: Set up pool
this.poolTokens = [token1, token2];
},
);

Given(
"{float} {word} has been deposited into the pool",
function (amount: number, token: string) {
// TODO: Deposit to pool
},
);

When(
"{word} swaps an exact input of {float} {word} for {word} with a slippage setting of {float}%",
async function (
user: string,
amount: number,
fromToken: string,
toToken: string,
slippage: number,
) {
const solanaConnection = this.solanaConnection as Connection;
const pool = await fetchSwimPool(solanaConnection);
const poolMath = createPoolMath(pool);
const inputAmount = new Decimal(amount);
// TODO: Handle token direction generically
const inputAmounts = [inputAmount, new Decimal(0)];
const outputIndex = 1;
const outputAmount = poolMath.swapExactInput(
inputAmounts,
outputIndex,
).stableOutputAmount;
const minimumOutputAmount = outputAmount.mul(1 - slippage / 100);
const userWallet = this.solanaWallet as Keypair;
const userDelegate = Keypair.generate();
const userTokenKeys = [
this.userTokenKeys[this.poolTokens[0]],
this.userTokenKeys[this.poolTokens[1]],
] as readonly [PublicKey, PublicKey];
const ixs = createApproveAndSwapIxs(
SwapDirection.UsdcToUsdt,
inputAmount,
minimumOutputAmount,
userTokenKeys,
userDelegate.publicKey,
userWallet.publicKey,
);
const { blockhash, lastValidBlockHeight } =
await solanaConnection.getLatestBlockhash();
const tx = new Transaction({
feePayer: userWallet.publicKey,
blockhash,
lastValidBlockHeight,
}).add(...ixs);
const signers = [userWallet, userDelegate];
this.txId = await solanaConnection.sendTransaction(tx, signers);

// TODO: Use the step description to design the SDK interface
// this.txId = await sdk.swap(userWallet, inputAmount, fromToken, toToken, slippage);
},
);
65 changes: 65 additions & 0 deletions packages/bdd/step_definitions/ui/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
import assert from "assert";

import { Given, Then } from "@cucumber/cucumber";
import { PublicKey } from "@solana/web3.js";
import Decimal from "decimal.js";

async function getUserBalance(this: any, token: string): Promise<Decimal> {
const userTokenKey = this.userTokenKeys[token] as PublicKey;
const balance: string = await this.wallet.getTokenAccountBalance(
userTokenKey,
);
return new Decimal(balance);
}

Given("there is a Solana blockchain", function () {
// TODO: Set up a development chain
});

Given(
"{word} has a Solana wallet with address {string}",
function (user: string, address: string) {
// TODO: Set up Phantom
this.wallet = (window as any).phantom.solana;
},
);

Given(
"{word} has {float} SOL in his Solana wallet",
function (user: string, amount: number) {
// TODO: Seed wallet with SOL
},
);

Given(
"{word} has a {word} token account with address {string}",
function (user: string, token: string, address: string) {
this.userTokenKeys = this.userTokenKeys ?? {};
this.userTokenKeys[token] = new PublicKey(address);
// TODO: Seed wallet with tokens
},
);

Given(
"{word} has {float} {word} tokens in his Solana wallet",
function (user: string, amount: number, token: string) {
// TODO: Seed wallet with tokens
},
);

Then(
"{word}'s {word} balance should be {float}",
async function (user: string, token: string, amount: number) {
const balance = await getUserBalance.call(this, token);
assert(balance.equals(amount));
},
);

Then(
"{word}'s {word} balance should be at least {float}",
async function (user: string, token: string, amount: number) {
const balance = await getUserBalance.call(this, token);
assert(balance.greaterThanOrEqualTo(amount));
},
);
Loading