Skip to content

Commit

Permalink
add swift taker and maker example
Browse files Browse the repository at this point in the history
  • Loading branch information
NourAlharithi committed Oct 22, 2024
1 parent c3cba52 commit 0073a35
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 16 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@
"async": "^3.2.2",
"async-mutex": "0.3.2",
"aws-sdk": "^2.1062.0",
"axios": "^1.1.3",
"axios": "^1.7.7",
"commander": "^9.4.0",
"dotenv": "^10.0.0",
"jito-ts": "3.0.1",
"lru-cache": "^10.2.0",
"minimist": "^1.2.8",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "4.5.4",
"undici": "^6.19.2",
"winston": "^3.8.1",
"ws": "^8.18.0",
"yaml": "^2.2.1"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ export type BotConfigMap = {
uncrossArb?: BaseBotConfig;
pythCranker?: PythCrankerBotConfig;
switchboardCranker?: SwitchboardCrankerBotConfig;
swiftTaker?: BaseBotConfig;
swiftMaker?: BaseBotConfig;
};

export interface GlobalConfig {
Expand Down
41 changes: 41 additions & 0 deletions src/experimental-bots/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
SlotSubscriber,
initialize,
WhileValidTxSender,
UserMap,
} from '@drift-labs/sdk';
import {
Commitment,
Expand All @@ -34,6 +35,8 @@ import { promiseTimeout } from '@drift-labs/sdk';
import { SpotFillerMultithreaded } from './spotFiller/spotFillerMultithreaded';
import { setGlobalDispatcher, Agent } from 'undici';
import { PythPriceFeedSubscriber } from '../pythPriceFeedSubscriber';
import { SwiftMaker } from './swift/makerExample';
import { SwiftTaker } from './swift/takerExample';

setGlobalDispatcher(
new Agent({
Expand Down Expand Up @@ -366,6 +369,44 @@ const runBot = async () => {
);
bots.push(spotFillerMultithreaded);
}

if (configHasBot(config, 'swiftMaker')) {
const userMap = new UserMap({
connection,
driftClient,
subscriptionConfig: {
type: 'polling',
frequency: 5000,
},
fastDecode: true,
});
await userMap.subscribe();

const swiftMaker = new SwiftMaker(driftClient, userMap, {
rpcEndpoint: endpoint,
commit: '',
driftEnv: config.global.driftEnv!,
driftPid: driftPublicKey.toBase58(),
walletAuthority: wallet.publicKey.toBase58(),
});
bots.push(swiftMaker);
}

if (configHasBot(config, 'swiftTaker')) {
const swiftMaker = new SwiftTaker(
driftClient,
{
rpcEndpoint: endpoint,
commit: '',
driftEnv: config.global.driftEnv!,
driftPid: driftPublicKey.toBase58(),
walletAuthority: wallet.publicKey.toBase58(),
},
1000
);
bots.push(swiftMaker);
}

// Initialize bots
logger.info(`initializing bots`);
await Promise.all(bots.map((bot: any) => bot.init()));
Expand Down
151 changes: 151 additions & 0 deletions src/experimental-bots/swift/makerExample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {
DriftClient,
getUserAccountPublicKey,
getUserStatsAccountPublicKey,
PublicKey,
UserMap,
} from '@drift-labs/sdk';
import { RuntimeSpec } from 'src/metrics';
import WebSocket from 'ws';
import nacl from 'tweetnacl';
import { decodeUTF8 } from 'tweetnacl-util';

export class SwiftMaker {
interval: NodeJS.Timeout | null = null;
private ws: WebSocket | null = null;
private heartbeatTimeout: NodeJS.Timeout | null = null;
private readonly heartbeatIntervalMs = 30000;
constructor(
private driftClient: DriftClient,
private userMap: UserMap,
runtimeSpec: RuntimeSpec
) {
if (runtimeSpec.driftEnv != 'devnet') {
throw new Error('SwiftMaker only works on devnet');
}
}

async init() {
await this.subscribeWs();
}

async subscribeWs() {
const keypair = this.driftClient.wallet.payer!;
const ws = new WebSocket(
`wss://master.swift.drift.trade/ws?pubkey=` + keypair.publicKey.toBase58()
);

ws.on('open', async () => {
console.log('Connected to the server');
this.startHeartbeatTimer();

ws.on('message', async (data: WebSocket.Data) => {
const message = JSON.parse(data.toString());
console.log(message);

this.startHeartbeatTimer();

if (message['channel'] === 'auth' && message['nonce'] != null) {
const messageBytes = decodeUTF8(message['nonce']);
const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
const signatureBase64 = Buffer.from(signature).toString('base64');
ws.send(
JSON.stringify({
pubkey: keypair.publicKey.toBase58(),
signature: signatureBase64,
})
);
}

if (
message['channel'] === 'auth' &&
message['message'] === 'Authenticated'
) {
ws.send(
JSON.stringify({
action: 'subscribe',
market_type: 'perp',
market_name: 'SOL-PERP',
})
);
}

if (message['order'] && this.driftClient.isSubscribed) {
const order = JSON.parse(message['order']);
const takerAuthority = new PublicKey(order['taker_authority']);
const takerSubaccountId = order['taker_sub_account_id'] ?? 0;
const takerUserPubkey = await getUserAccountPublicKey(
this.driftClient.program.programId,
takerAuthority,
takerSubaccountId
);
const takerUserAccount = (
await this.userMap.mustGet(takerUserPubkey.toString())
).getUserAccount();
const ixs = await this.driftClient.getPlaceSwiftTakerPerpOrderIxs(
Buffer.from(order['swift_message'], 'base64'),
Buffer.from(order['swift_signature'], 'base64'),
Buffer.from(order['order_message'], 'base64'),
Buffer.from(order['order_signature'], 'base64'),
order['market_index'],
{
taker: takerUserPubkey,
takerUserAccount,
takerStats: getUserStatsAccountPublicKey(
this.driftClient.program.programId,
takerUserAccount.authority
),
}
);
const tx = await this.driftClient.txSender.getVersionedTransaction(
ixs,
[this.driftClient.lookupTableAccount],
undefined,
undefined,
await this.driftClient.connection.getLatestBlockhash()
);

this.driftClient.txSender
.sendVersionedTransaction(tx)
.then((response) => {
console.log(response);
});
}
});

ws.on('close', () => {
console.log('Disconnected from the server');
this.reconnect();
});

ws.on('error', (error: Error) => {
console.error('WebSocket error:', error);
this.reconnect();
});
});

this.ws = ws;
}

private startHeartbeatTimer() {
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout);
}
this.heartbeatTimeout = setTimeout(() => {
console.warn('No heartbeat received within 30 seconds, reconnecting...');
this.reconnect();
}, this.heartbeatIntervalMs);
}

private reconnect() {
if (this.ws) {
this.ws.removeAllListeners();
this.ws.terminate();
}

console.log('Reconnecting to WebSocket...');
setTimeout(() => {
this.subscribeWs();
}, 1000);
}
}
69 changes: 69 additions & 0 deletions src/experimental-bots/swift/takerExample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
BASE_PRECISION,
DriftClient,
getMarketOrderParams,
MarketType,
PositionDirection,
} from '@drift-labs/sdk';
import { RuntimeSpec } from 'src/metrics';
import * as axios from 'axios';

export class SwiftTaker {
interval: NodeJS.Timeout | null = null;

constructor(
private driftClient: DriftClient,
runtimeSpec: RuntimeSpec,
private intervalMs: number
) {
if (runtimeSpec.driftEnv != 'devnet') {
throw new Error('SwiftTaker only works on devnet');
}
}

async init() {
await this.startInterval();
}

async startInterval() {
this.interval = setInterval(async () => {
const slot = await this.driftClient.connection.getSlot();
const direction =
Math.random() > 0.5 ? PositionDirection.LONG : PositionDirection.SHORT;
console.log('Sending order in slot:', slot, Date.now());
const oracleInfo = this.driftClient.getOracleDataForPerpMarket(0);
const orderMessage = this.driftClient.encodeSwiftOrderParamsMessage({
swiftOrderParams: getMarketOrderParams({
marketIndex: 0,
marketType: MarketType.PERP,
direction,
baseAssetAmount: BASE_PRECISION,
price: oracleInfo.price.muln(105).divn(100),
}),
subAccountId: 0,
expectedOrderId: this.driftClient.getUser().getUserAccount()
.nextOrderId,
stopLossOrderParams: null,
takeProfitOrderParams: null,
});

const signature = this.driftClient.signMessage(orderMessage);
const response = await axios.default.post(
'https://master.swift.drift.trade/orders',
{
market_index: 0,
market_type: 'perp',
message: orderMessage.toString('base64'),
signature: signature.toString('base64'),
taker_pubkey: this.driftClient.wallet.publicKey.toBase58(),
},
{
headers: {
'Content-Type': 'application/json',
},
}
);
console.log(response.data);
}, this.intervalMs);
}
}
35 changes: 20 additions & 15 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2464,15 +2464,6 @@ axios-retry@^3.8.0:
"@babel/runtime" "^7.15.4"
is-retry-allowed "^2.2.0"

axios@^1.1.3:
version "1.6.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2"
integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"

axios@^1.5.1, axios@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621"
Expand All @@ -2491,6 +2482,15 @@ axios@^1.7.4:
form-data "^4.0.0"
proxy-from-env "^1.1.0"

axios@^1.7.7:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"

balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
Expand Down Expand Up @@ -3364,11 +3364,6 @@ [email protected]:
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==

follow-redirects@^1.15.0:
version "1.15.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==

follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
Expand Down Expand Up @@ -5047,7 +5042,12 @@ tsutils@^3.21.0:
dependencies:
tslib "^1.8.1"

[email protected]:
tweetnacl-util@^0.15.1:
version "0.15.1"
resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b"
integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==

[email protected], tweetnacl@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
Expand Down Expand Up @@ -5233,6 +5233,11 @@ ws@^7.5.10:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==

ws@^8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==

ws@^8.5.0:
version "8.16.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
Expand Down

0 comments on commit 0073a35

Please sign in to comment.