@clonegod/ttd-sol-common 2.0.11 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ import { StandardPoolInfoType, StandardTokenInfoType } from "@clonegod/ttd-core";
2
+ import { Connection, Keypair, PublicKey } from "@solana/web3.js";
3
+ export declare const get_token_program_id: (token: StandardTokenInfoType) => PublicKey;
4
+ export declare function get_wallet_token_account(wallet_pubkey: string, pool_list: StandardPoolInfoType[]): Promise<Map<string, string>>;
5
+ export declare const create_token_account_if_not_exist: (connection: Connection, owner: Keypair, token_list: StandardTokenInfoType[]) => Promise<void>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.create_token_account_if_not_exist = exports.get_token_program_id = void 0;
4
+ exports.get_wallet_token_account = get_wallet_token_account;
5
+ const dist_1 = require("@clonegod/ttd-core/dist");
6
+ const constants_1 = require("./constants");
7
+ const spl_token_1 = require("@solana/spl-token");
8
+ const web3_js_1 = require("@solana/web3.js");
9
+ const get_token_program_id = (token) => {
10
+ let programId = spl_token_1.TOKEN_PROGRAM_ID;
11
+ if (token.is_token2022) {
12
+ programId = spl_token_1.TOKEN_2022_PROGRAM_ID;
13
+ }
14
+ return programId;
15
+ };
16
+ exports.get_token_program_id = get_token_program_id;
17
+ async function get_wallet_token_account(wallet_pubkey, pool_list) {
18
+ let account_map = new Map();
19
+ for (let { tokenA, tokenB } of pool_list) {
20
+ account_map.set(tokenA.symbol, (await (0, spl_token_1.getAssociatedTokenAddress)(new web3_js_1.PublicKey(tokenA.address), new web3_js_1.PublicKey(wallet_pubkey), false, (0, exports.get_token_program_id)(tokenA))).toBase58());
21
+ account_map.set(tokenB.symbol, (await (0, spl_token_1.getAssociatedTokenAddress)(new web3_js_1.PublicKey(tokenB.address), new web3_js_1.PublicKey(wallet_pubkey), false, (0, exports.get_token_program_id)(tokenB))).toBase58());
22
+ }
23
+ return account_map;
24
+ }
25
+ const create_token_account_if_not_exist = async (connection, owner, token_list) => {
26
+ token_list.forEach(async (token) => {
27
+ var _a;
28
+ if (['SOL', 'WSOL', 'USDC', 'USDT'].includes((_a = token.symbol) === null || _a === void 0 ? void 0 : _a.toUpperCase())) {
29
+ return;
30
+ }
31
+ try {
32
+ const mint = new web3_js_1.PublicKey(token.address);
33
+ let ata = await (0, spl_token_1.getAssociatedTokenAddress)(new web3_js_1.PublicKey(mint), owner.publicKey);
34
+ const account_data = await connection.getAccountInfo(ata);
35
+ if (account_data) {
36
+ (0, dist_1.log_info)(`token account already exist, skip!`, {
37
+ owner: owner.publicKey.toBase58(),
38
+ token,
39
+ ata: ata.toBase58()
40
+ });
41
+ }
42
+ else {
43
+ (0, dist_1.log_info)(`token account not exist, create new one!`, {
44
+ owner: owner.publicKey.toBase58(),
45
+ token,
46
+ });
47
+ if (token.is_token2022) {
48
+ ata = await (0, spl_token_1.createAssociatedTokenAccountIdempotent)(connection, owner, mint, owner.publicKey, {
49
+ commitment: constants_1.COMMITMENT_LEVEL.CONFIRMED,
50
+ }, spl_token_1.TOKEN_2022_PROGRAM_ID, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
51
+ }
52
+ else {
53
+ ata = await (0, spl_token_1.createAssociatedTokenAccountIdempotent)(connection, owner, mint, owner.publicKey, {
54
+ commitment: constants_1.COMMITMENT_LEVEL.CONFIRMED,
55
+ }, spl_token_1.TOKEN_PROGRAM_ID, spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID);
56
+ }
57
+ (0, dist_1.log_info)(`Create New Token Account`, {
58
+ owner: owner.publicKey.toBase58(),
59
+ token,
60
+ ata: ata.toBase58()
61
+ });
62
+ }
63
+ }
64
+ catch (err) {
65
+ (0, dist_1.log_error)(`create_token_account_if_not_exist error!`, err);
66
+ }
67
+ });
68
+ };
69
+ exports.create_token_account_if_not_exist = create_token_account_if_not_exist;
@@ -1 +1,2 @@
1
1
  export * from './constants';
2
+ export * from './get_wallet_token_account';
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./constants"), exports);
18
+ __exportStar(require("./get_wallet_token_account"), exports);
@@ -1,13 +1,4 @@
1
1
  "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
12
3
  exports.TokenPriceCache = void 0;
13
4
  const dist_1 = require("@clonegod/ttd-core/dist");
@@ -17,23 +8,21 @@ class TokenPriceCache {
17
8
  this.PRICE_CACHE_TIMEOUT_MILLS = parseInt(process.env.PRICE_CACHE_TIMEOUT_MILLS || Number(1000 * 60 * 60 * 1).toString());
18
9
  (0, dist_1.log_info)(`代币价格缓存超时时间: ${this.PRICE_CACHE_TIMEOUT_MILLS} ms`);
19
10
  }
20
- getTokenPrice(tokenAddress) {
21
- return __awaiter(this, void 0, void 0, function* () {
22
- const now = Date.now();
23
- const cachedData = this.tokenPriceCache.get(tokenAddress);
24
- if (cachedData && (now - cachedData.timestamp) < this.PRICE_CACHE_TIMEOUT_MILLS) {
25
- (0, dist_1.log_debug)(`use cached token price: ${tokenAddress}, price: ${cachedData.price}`, '');
26
- return cachedData;
27
- }
28
- const priceMap = yield (0, dist_1.get_solana_token_price_info)([tokenAddress]);
29
- const tokenPrice = priceMap.get(tokenAddress);
30
- if (!tokenPrice || !tokenPrice.price || Number(tokenPrice.price) <= 0) {
31
- throw new Error(`无法获取代币 ${tokenAddress} 的有效USD价格`);
32
- }
33
- const newPrice = { price: tokenPrice.price, timestamp: now };
34
- this.tokenPriceCache.set(tokenAddress, newPrice);
35
- return newPrice;
36
- });
11
+ async getTokenPrice(tokenAddress) {
12
+ const now = Date.now();
13
+ const cachedData = this.tokenPriceCache.get(tokenAddress);
14
+ if (cachedData && (now - cachedData.timestamp) < this.PRICE_CACHE_TIMEOUT_MILLS) {
15
+ (0, dist_1.log_debug)(`use cached token price: ${tokenAddress}, price: ${cachedData.price}`, '');
16
+ return cachedData;
17
+ }
18
+ const priceMap = await (0, dist_1.get_solana_token_price_info)([tokenAddress]);
19
+ const tokenPrice = priceMap.get(tokenAddress);
20
+ if (!tokenPrice || !tokenPrice.price || Number(tokenPrice.price) <= 0) {
21
+ throw new Error(`无法获取代币 ${tokenAddress} 的有效USD价格`);
22
+ }
23
+ const newPrice = { price: tokenPrice.price, timestamp: now };
24
+ this.tokenPriceCache.set(tokenAddress, newPrice);
25
+ return newPrice;
37
26
  }
38
27
  }
39
28
  exports.TokenPriceCache = TokenPriceCache;
@@ -0,0 +1,11 @@
1
+ import { StandardPoolInfoType } from "@clonegod/ttd-core";
2
+ import { AbstractTradeAppConfig, DEX_ID } from "@clonegod/ttd-core/dist";
3
+ import { Connection, Keypair } from "@solana/web3.js";
4
+ import { SolanaPoolAccountUpdateEventData } from "../types";
5
+ export declare class SolanaTradeAppConfig extends AbstractTradeAppConfig {
6
+ connection: Connection;
7
+ keypair: Keypair;
8
+ constructor();
9
+ subscribe_pool_events_from_multiple_providers(dex_id: DEX_ID, pool_list: StandardPoolInfoType[], callback: (eventData: SolanaPoolAccountUpdateEventData) => void): void;
10
+ subscribe_wallet_raw_txn_event(): void;
11
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SolanaTradeAppConfig = void 0;
4
+ const dist_1 = require("@clonegod/ttd-core/dist");
5
+ class SolanaTradeAppConfig extends dist_1.AbstractTradeAppConfig {
6
+ constructor() {
7
+ super();
8
+ }
9
+ subscribe_pool_events_from_multiple_providers(dex_id, pool_list, callback) {
10
+ const ws_url = process.env.SUBSCIBE_MULTIPLE_PROVIDERS_WS_URL || 'ws://127.0.0.1:10000';
11
+ const ws_client = new dist_1.WebSocketClient(ws_url);
12
+ ws_client.onOpen(() => {
13
+ pool_list.forEach(({ pair, pool_address }, _) => {
14
+ ws_client.send(JSON.stringify({ dex_id, pair, pool_address }));
15
+ });
16
+ });
17
+ ws_client.onMessage(((eventData) => {
18
+ if (!eventData.data) {
19
+ (0, dist_1.log_warn)(`[SolanaTradeAppConfig] subscribe_pool_events_from_multiple_providers: no data`, eventData);
20
+ }
21
+ else {
22
+ callback(eventData);
23
+ }
24
+ }));
25
+ ws_client.connect();
26
+ }
27
+ subscribe_wallet_raw_txn_event() {
28
+ return;
29
+ }
30
+ }
31
+ exports.SolanaTradeAppConfig = SolanaTradeAppConfig;
@@ -1,2 +1,4 @@
1
1
  export * from './tx_result_check';
2
2
  export * from './tx_result_parse';
3
+ export * from './SolanaTradeAppConfig';
4
+ export * from './tx_builder';
@@ -16,3 +16,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./tx_result_check"), exports);
18
18
  __exportStar(require("./tx_result_parse"), exports);
19
+ __exportStar(require("./SolanaTradeAppConfig"), exports);
20
+ __exportStar(require("./tx_builder"), exports);
@@ -0,0 +1,18 @@
1
+ import { TradeContext } from "@clonegod/ttd-core/dist";
2
+ import { Connection, Keypair, Transaction, TransactionInstruction } from "@solana/web3.js";
3
+ import { SolanaTradeAppConfig } from "./SolanaTradeAppConfig";
4
+ export declare class SolTransactionBuilder {
5
+ appConfig: SolanaTradeAppConfig;
6
+ protected connection: Connection;
7
+ protected keypair: Keypair;
8
+ protected recentBlockhash: string;
9
+ protected recentBlockheight: number;
10
+ constructor(appConfig: SolanaTradeAppConfig);
11
+ init(): Promise<void>;
12
+ private handleBlockUpdateEvent;
13
+ execute(context: TradeContext): Promise<string>;
14
+ getPreInstructions(context: TradeContext): TransactionInstruction[];
15
+ getPostInstructions(context: TradeContext): TransactionInstruction[];
16
+ buildTransactionForSwap(context: TradeContext, swapInstructions: TransactionInstruction[]): Transaction;
17
+ buildTransactionForTip(context: TradeContext, tipAccount: string, tipAmount: number, tipPayer: Keypair): Transaction;
18
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SolTransactionBuilder = void 0;
4
+ const dist_1 = require("@clonegod/ttd-core/dist");
5
+ const web3_js_1 = require("@solana/web3.js");
6
+ const LAMPORTS_PER_MICRO_LAMPORTS = 1000000;
7
+ class SolTransactionBuilder {
8
+ constructor(appConfig) {
9
+ this.appConfig = appConfig;
10
+ this.connection = appConfig.connection;
11
+ this.keypair = appConfig.keypair;
12
+ this.init();
13
+ }
14
+ async init() {
15
+ this.appConfig.arb_event_subscriber.subscribe_new_block(dist_1.CHAIN_ID.SOLANA, this.handleBlockUpdateEvent.bind(this));
16
+ }
17
+ async handleBlockUpdateEvent(eventData) {
18
+ let blockUpdateEvent = JSON.parse(eventData);
19
+ const { blockHash, blockHeight } = blockUpdateEvent;
20
+ this.recentBlockhash = blockHash;
21
+ this.recentBlockheight = blockHeight;
22
+ }
23
+ execute(context) {
24
+ throw new Error('not implemented!!!');
25
+ }
26
+ getPreInstructions(context) {
27
+ const cu_limit = context.pool_info.cu_limit || 600000;
28
+ const sol_priority_fee_lamports = context.trade_runtime.settings.strategy.sol_priority_fee;
29
+ const priorityFeeMicroLamports = Math.floor((sol_priority_fee_lamports * LAMPORTS_PER_MICRO_LAMPORTS) / cu_limit);
30
+ const computeBudgetInstruction = web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({
31
+ units: cu_limit,
32
+ });
33
+ const priorityFeeInstruction = web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({
34
+ microLamports: priorityFeeMicroLamports,
35
+ });
36
+ return [computeBudgetInstruction, priorityFeeInstruction];
37
+ }
38
+ getPostInstructions(context) {
39
+ return [];
40
+ }
41
+ buildTransactionForSwap(context, swapInstructions) {
42
+ const max_block_offset = context.trade_runtime.settings.strategy.max_block_offset;
43
+ const swapTx = new web3_js_1.Transaction();
44
+ const preInstructions = this.getPreInstructions(context);
45
+ const postInstructions = this.getPostInstructions(context);
46
+ swapTx.add(...preInstructions, ...swapInstructions, ...postInstructions);
47
+ swapTx.recentBlockhash = this.recentBlockhash;
48
+ swapTx.lastValidBlockHeight = this.recentBlockheight + max_block_offset;
49
+ swapTx.feePayer = this.keypair.publicKey;
50
+ swapTx.sign(this.keypair);
51
+ return swapTx;
52
+ }
53
+ buildTransactionForTip(context, tipAccount, tipAmount, tipPayer) {
54
+ const max_block_offset = context.trade_runtime.settings.strategy.max_block_offset;
55
+ if (!tipPayer) {
56
+ tipPayer = this.keypair;
57
+ }
58
+ const tipTx = new web3_js_1.Transaction();
59
+ tipTx.add(web3_js_1.SystemProgram.transfer({
60
+ fromPubkey: tipPayer.publicKey,
61
+ toPubkey: new web3_js_1.PublicKey(tipAccount),
62
+ lamports: tipAmount,
63
+ }));
64
+ tipTx.recentBlockhash = this.recentBlockhash;
65
+ tipTx.lastValidBlockHeight = this.recentBlockheight + max_block_offset;
66
+ tipTx.feePayer = tipPayer.publicKey;
67
+ tipTx.sign(tipPayer);
68
+ return tipTx;
69
+ }
70
+ }
71
+ exports.SolTransactionBuilder = SolTransactionBuilder;
@@ -1,13 +1,4 @@
1
1
  "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
12
3
  exports.TransactionResultChecker = void 0;
13
4
  const dist_1 = require("@clonegod/ttd-core/dist");
@@ -39,11 +30,9 @@ class TransactionResultChecker extends dist_1.AbstractTransactionResultCheck {
39
30
  this.event_emitter.emit(dist_1.TRANSACTION_STATE_PROCESSING, empty_trade_result);
40
31
  return this;
41
32
  }
42
- check() {
43
- return __awaiter(this, void 0, void 0, function* () {
44
- this.check_tx_result_interval();
45
- this.on_subscibe_transaction();
46
- });
33
+ async check() {
34
+ this.check_tx_result_interval();
35
+ this.on_subscibe_transaction();
47
36
  }
48
37
  on_subscibe_transaction() {
49
38
  this.event_emitter.once(dist_1.LOCAL_EVENT_NAME.EVENT_WALLET_TRANSACTION + '#' + this.txid, (messageStr) => {
@@ -84,57 +73,55 @@ class TransactionResultChecker extends dist_1.AbstractTransactionResultCheck {
84
73
  }
85
74
  });
86
75
  }
87
- check_tx_result_interval() {
88
- return __awaiter(this, void 0, void 0, function* () {
89
- const check_start_time = Date.now();
90
- const check_interval = parseInt(process.env.CHECK_TX_RESULT_INTERVAL_MILLS || '3000');
91
- const check_timeout = parseInt(process.env.CHECK_TX_RESULT_TIMEOUT_MILLS || '10000');
92
- (0, dist_1.log_info)(`check transaction start: check_interval=${check_interval}, check_timeout=${check_timeout}`);
93
- if (check_interval >= check_timeout) {
94
- (0, dist_1.log_warn)(`check_interval=${check_interval} >= check_timeout=${check_timeout}, check_tx_result_interval failed!`);
95
- return;
96
- }
97
- const intervalId = setInterval(() => __awaiter(this, void 0, void 0, function* () {
98
- this.check_count += 1;
99
- (0, dist_1.log_info)(`check transaction: seq=[${this.check_count}], txhash= ${this.txid}, trace_id=${this.trace_id}`);
100
- try {
101
- if (Date.now() - check_start_time < check_timeout) {
102
- let tx_result = yield this.connection.getParsedTransaction(this.txid, {
103
- commitment: common_1.COMMITMENT_LEVEL.CONFIRMED,
104
- maxSupportedTransactionVersion: 0,
105
- });
106
- if ((0, dist_1.isEmpty)(tx_result)) {
107
- return;
108
- }
109
- clearInterval(intervalId);
110
- let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info);
111
- let trade_result = this.map_swap_result_to_tx_result(swap_result);
112
- if (this.trade_result_already_processed) {
113
- (0, dist_1.log_warn)(`trade_result_already_processed=${this.trade_result_already_processed}, ignore trade result fetch by interval check!`);
76
+ async check_tx_result_interval() {
77
+ const check_start_time = Date.now();
78
+ const check_interval = parseInt(process.env.CHECK_TX_RESULT_INTERVAL_MILLS || '3000');
79
+ const check_timeout = parseInt(process.env.CHECK_TX_RESULT_TIMEOUT_MILLS || '10000');
80
+ (0, dist_1.log_info)(`check transaction start: check_interval=${check_interval}, check_timeout=${check_timeout}`);
81
+ if (check_interval >= check_timeout) {
82
+ (0, dist_1.log_warn)(`check_interval=${check_interval} >= check_timeout=${check_timeout}, check_tx_result_interval failed!`);
83
+ return;
84
+ }
85
+ const intervalId = setInterval(async () => {
86
+ this.check_count += 1;
87
+ (0, dist_1.log_info)(`check transaction: seq=[${this.check_count}], txhash= ${this.txid}, trace_id=${this.trace_id}`);
88
+ try {
89
+ if (Date.now() - check_start_time < check_timeout) {
90
+ let tx_result = await this.connection.getParsedTransaction(this.txid, {
91
+ commitment: common_1.COMMITMENT_LEVEL.CONFIRMED,
92
+ maxSupportedTransactionVersion: 0,
93
+ });
94
+ if ((0, dist_1.isEmpty)(tx_result)) {
95
+ return;
96
+ }
97
+ clearInterval(intervalId);
98
+ let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info);
99
+ let trade_result = this.map_swap_result_to_tx_result(swap_result);
100
+ if (this.trade_result_already_processed) {
101
+ (0, dist_1.log_warn)(`trade_result_already_processed=${this.trade_result_already_processed}, ignore trade result fetch by interval check!`);
102
+ }
103
+ else {
104
+ if (trade_result.success) {
105
+ this.event_emitter.emit(dist_1.TRANSACTION_STATE_SUCCESS, trade_result);
114
106
  }
115
107
  else {
116
- if (trade_result.success) {
117
- this.event_emitter.emit(dist_1.TRANSACTION_STATE_SUCCESS, trade_result);
118
- }
119
- else {
120
- this.event_emitter.emit(dist_1.TRANSACTION_STATE_FAILED, trade_result);
121
- }
122
- console.log('<====================================================');
123
- console.dir(trade_result, { depth: 8 });
124
- console.log('====================================================>');
108
+ this.event_emitter.emit(dist_1.TRANSACTION_STATE_FAILED, trade_result);
125
109
  }
126
- }
127
- else {
128
- clearInterval(intervalId);
110
+ console.log('<====================================================');
111
+ console.dir(trade_result, { depth: 8 });
112
+ console.log('====================================================>');
129
113
  }
130
114
  }
131
- catch (err) {
115
+ else {
132
116
  clearInterval(intervalId);
133
- (0, dist_1.log_error)('parse transaction error!', err);
134
117
  }
135
- }), check_interval);
136
- this.intervalId = intervalId;
137
- });
118
+ }
119
+ catch (err) {
120
+ clearInterval(intervalId);
121
+ (0, dist_1.log_error)('parse transaction error!', err);
122
+ }
123
+ }, check_interval);
124
+ this.intervalId = intervalId;
138
125
  }
139
126
  map_swap_result_to_tx_result(swap_result) {
140
127
  let { success, error_code, wallet, block_number, block_time: order_block_time, txid, tx_price, tokenA, tokenB, gas_fee } = swap_result;
@@ -20,3 +20,18 @@ export interface SolanaBlockMetaUpdateEvent {
20
20
  executedTransactionCount: number;
21
21
  entriesCount: number;
22
22
  }
23
+ export interface SolanaPoolAccountUpdateEventData {
24
+ type: string;
25
+ provider_id: string;
26
+ event_time: number;
27
+ dexId: string;
28
+ pair: string;
29
+ pool_address: string;
30
+ pool_name: string;
31
+ data: {
32
+ slot: number;
33
+ writeVersion: number;
34
+ transactionHash: string;
35
+ accountData: string;
36
+ };
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonegod/ttd-sol-common",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "types/index.d.ts",
@@ -0,0 +1,90 @@
1
+ import { StandardPoolInfoType, StandardTokenInfoType } from "@clonegod/ttd-core";
2
+ import { log_info, log_error } from "@clonegod/ttd-core/dist";
3
+ import { COMMITMENT_LEVEL } from "./constants";
4
+ import { ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotent, getAssociatedTokenAddress, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
5
+ import { Connection, Keypair, PublicKey } from "@solana/web3.js";
6
+
7
+
8
+ export const get_token_program_id = (token: StandardTokenInfoType) => {
9
+ let programId: PublicKey = TOKEN_PROGRAM_ID
10
+ if(token.is_token2022) {
11
+ programId = TOKEN_2022_PROGRAM_ID
12
+ }
13
+ return programId
14
+ }
15
+
16
+ export async function get_wallet_token_account(wallet_pubkey:string, pool_list:StandardPoolInfoType[]
17
+ ) {
18
+ let account_map = new Map<string,string>()
19
+
20
+ for(let {tokenA, tokenB} of pool_list) {
21
+ account_map.set(tokenA.symbol, (await getAssociatedTokenAddress(new PublicKey(tokenA.address), new PublicKey(wallet_pubkey), false, get_token_program_id(tokenA))).toBase58())
22
+ account_map.set(tokenB.symbol, (await getAssociatedTokenAddress(new PublicKey(tokenB.address), new PublicKey(wallet_pubkey), false, get_token_program_id(tokenB))).toBase58())
23
+ }
24
+ return account_map
25
+ }
26
+
27
+
28
+
29
+ /**
30
+ * 创建Token Account
31
+ */
32
+ export const create_token_account_if_not_exist = async (connection:Connection, owner:Keypair, token_list: StandardTokenInfoType[]) => {
33
+ token_list.forEach(async token => {
34
+ if(['SOL','WSOL','USDC','USDT'].includes(token.symbol?.toUpperCase())) {
35
+ return
36
+ }
37
+
38
+ try {
39
+ const mint = new PublicKey(token.address)
40
+ // calculate ATA
41
+ let ata = await getAssociatedTokenAddress(new PublicKey(mint), owner.publicKey)
42
+ const account_data = await connection.getAccountInfo(ata)
43
+ if(account_data) {
44
+ log_info(`token account already exist, skip!`, {
45
+ owner: owner.publicKey.toBase58(),
46
+ token,
47
+ ata: ata.toBase58()
48
+ });
49
+ } else {
50
+ log_info(`token account not exist, create new one!`, {
51
+ owner: owner.publicKey.toBase58(),
52
+ token,
53
+ });
54
+ if(token.is_token2022) {
55
+ ata = await createAssociatedTokenAccountIdempotent(
56
+ connection,
57
+ owner,
58
+ mint,
59
+ owner.publicKey,
60
+ {
61
+ commitment: COMMITMENT_LEVEL.CONFIRMED,
62
+ },
63
+ TOKEN_2022_PROGRAM_ID,
64
+ ASSOCIATED_TOKEN_PROGRAM_ID
65
+ )
66
+ } else {
67
+ ata = await createAssociatedTokenAccountIdempotent(
68
+ connection,
69
+ owner,
70
+ mint,
71
+ owner.publicKey,
72
+ {
73
+ commitment: COMMITMENT_LEVEL.CONFIRMED,
74
+ },
75
+ TOKEN_PROGRAM_ID,
76
+ ASSOCIATED_TOKEN_PROGRAM_ID
77
+ )
78
+ }
79
+ log_info(`Create New Token Account`, {
80
+ owner: owner.publicKey.toBase58(),
81
+ token,
82
+ ata: ata.toBase58()
83
+ });
84
+ }
85
+ } catch (err) {
86
+ log_error(`create_token_account_if_not_exist error!`, err)
87
+ }
88
+ })
89
+ }
90
+
@@ -1,2 +1,3 @@
1
1
  export * from './constants'
2
+ export * from './get_wallet_token_account'
2
3
 
@@ -0,0 +1,50 @@
1
+ import { StandardPoolInfoType } from "@clonegod/ttd-core";
2
+ import { AbstractTradeAppConfig, DEX_ID, log_warn, WebSocketClient } from "@clonegod/ttd-core/dist";
3
+ import { Connection, Keypair } from "@solana/web3.js";
4
+ import { SolanaPoolAccountUpdateEventData } from "../types";
5
+
6
+ export class SolanaTradeAppConfig extends AbstractTradeAppConfig {
7
+
8
+ // 子类需要设置的属性
9
+ public connection: Connection;
10
+ public keypair: Keypair;
11
+
12
+ constructor() {
13
+ super();
14
+ }
15
+
16
+ /**
17
+ * 集中订阅方式,丛 solana-stream 获取池子变化事件,获得poolRawData
18
+ * - 询价:基于池子数据计算价格
19
+ * - 交易使用:交易中需要池子的最新状态
20
+ */
21
+ subscribe_pool_events_from_multiple_providers(dex_id:DEX_ID, pool_list:StandardPoolInfoType[], callback:(eventData:SolanaPoolAccountUpdateEventData)=>void) {
22
+ const ws_url = process.env.SUBSCIBE_MULTIPLE_PROVIDERS_WS_URL || 'ws://127.0.0.1:10000';
23
+ const ws_client = new WebSocketClient(ws_url)
24
+ ws_client.onOpen(() => {
25
+ pool_list.forEach(({ pair, pool_address }, _) => {
26
+ ws_client.send(JSON.stringify({ dex_id, pair, pool_address }))
27
+ })
28
+ })
29
+ ws_client.onMessage(((eventData: SolanaPoolAccountUpdateEventData): void => {
30
+ if (!eventData.data) {
31
+ log_warn(`[SolanaTradeAppConfig] subscribe_pool_events_from_multiple_providers: no data`, eventData);
32
+ } else {
33
+ callback(eventData);
34
+ }
35
+ }));
36
+ ws_client.connect();
37
+ }
38
+
39
+
40
+ // 订阅 Wallet 在链上发生的tx data
41
+ subscribe_wallet_raw_txn_event(): void {
42
+ // not used
43
+ return;
44
+ }
45
+
46
+
47
+
48
+ }
49
+
50
+
@@ -1,2 +1,4 @@
1
1
  export * from './tx_result_check'
2
- export * from './tx_result_parse'
2
+ export * from './tx_result_parse'
3
+ export * from './SolanaTradeAppConfig'
4
+ export * from './tx_builder'
@@ -0,0 +1,113 @@
1
+ import { CHAIN_ID, TradeContext } from "@clonegod/ttd-core/dist";
2
+ import { SolanaBlockMetaUpdateEvent } from "../types";
3
+ import { ComputeBudgetProgram, Connection, Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction, TransactionInstruction } from "@solana/web3.js";
4
+ import { SolanaTradeAppConfig } from "./SolanaTradeAppConfig";
5
+
6
+
7
+ // There are 10^6 micro-lamports in one lamport
8
+ const LAMPORTS_PER_MICRO_LAMPORTS = 1000000;
9
+
10
+ export class SolTransactionBuilder {
11
+ appConfig: SolanaTradeAppConfig
12
+ protected connection: Connection
13
+ protected keypair: Keypair
14
+
15
+ protected recentBlockhash: string
16
+ protected recentBlockheight: number
17
+
18
+ constructor(appConfig: SolanaTradeAppConfig) {
19
+ this.appConfig = appConfig
20
+ this.connection = appConfig.connection
21
+ this.keypair = appConfig.keypair
22
+ this.init()
23
+ }
24
+
25
+ public async init(): Promise<void> {
26
+ // 订阅区块更新事件
27
+ this.appConfig.arb_event_subscriber.subscribe_new_block(CHAIN_ID.SOLANA, this.handleBlockUpdateEvent.bind(this));
28
+ }
29
+
30
+ private async handleBlockUpdateEvent(eventData: string): Promise<void> {
31
+ let blockUpdateEvent = JSON.parse(eventData) as SolanaBlockMetaUpdateEvent;
32
+ const { blockHash, blockHeight } = blockUpdateEvent;
33
+ this.recentBlockhash = blockHash
34
+ this.recentBlockheight = blockHeight
35
+ // console.log(`handleBlockUpdateEvent, blockHeight: ${blockHeight}, blockHash: ${blockHash}`)
36
+ }
37
+
38
+ execute(context: TradeContext): Promise<string> {
39
+ throw new Error('not implemented!!!')
40
+ }
41
+
42
+
43
+ // pre-instructions
44
+ getPreInstructions(context: TradeContext): TransactionInstruction[] {
45
+ const cu_limit = context.pool_info.cu_limit || 600000
46
+ const sol_priority_fee_lamports = context.trade_runtime.settings.strategy.sol_priority_fee
47
+ const priorityFeeMicroLamports = Math.floor((sol_priority_fee_lamports * LAMPORTS_PER_MICRO_LAMPORTS) / cu_limit)
48
+ // console.log('getPreInstructions', { cu_limit, sol_priority_fee_lamports, priorityFeeMicroLamports, })
49
+
50
+ // set compute budget instruction
51
+ const computeBudgetInstruction = ComputeBudgetProgram.setComputeUnitLimit({
52
+ units: cu_limit,
53
+ })
54
+
55
+ // set priority fee instruction
56
+ const priorityFeeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
57
+ microLamports: priorityFeeMicroLamports,
58
+ })
59
+
60
+ return [computeBudgetInstruction, priorityFeeInstruction]
61
+ }
62
+
63
+
64
+ // post-instructions
65
+ getPostInstructions(context: TradeContext): TransactionInstruction[] {
66
+ return []
67
+ }
68
+
69
+
70
+ buildTransactionForSwap(context: TradeContext, swapInstructions: TransactionInstruction[]): Transaction {
71
+ const max_block_offset = context.trade_runtime.settings.strategy.max_block_offset
72
+ // console.log(`buildTransactionForSwap, max_block_offset: ${max_block_offset}`)
73
+
74
+ const swapTx = new Transaction();
75
+
76
+ const preInstructions = this.getPreInstructions(context)
77
+ const postInstructions = this.getPostInstructions(context)
78
+
79
+ swapTx.add(...preInstructions, ...swapInstructions, ...postInstructions)
80
+ swapTx.recentBlockhash = this.recentBlockhash
81
+ swapTx.lastValidBlockHeight = this.recentBlockheight + max_block_offset
82
+ swapTx.feePayer = this.keypair.publicKey
83
+
84
+ swapTx.sign(this.keypair)
85
+
86
+ return swapTx
87
+ }
88
+
89
+ buildTransactionForTip(context: TradeContext, tipAccount: string, tipAmount: number, tipPayer: Keypair): Transaction {
90
+ const max_block_offset = context.trade_runtime.settings.strategy.max_block_offset
91
+
92
+ if(!tipPayer) {
93
+ tipPayer = this.keypair
94
+ }
95
+
96
+ const tipTx = new Transaction();
97
+ tipTx.add(
98
+ SystemProgram.transfer({
99
+ fromPubkey: tipPayer.publicKey,
100
+ toPubkey: new PublicKey(tipAccount),
101
+ lamports: tipAmount,
102
+ }),
103
+ );
104
+ tipTx.recentBlockhash = this.recentBlockhash
105
+ tipTx.lastValidBlockHeight = this.recentBlockheight + max_block_offset
106
+ tipTx.feePayer = tipPayer.publicKey;
107
+
108
+ tipTx.sign(tipPayer)
109
+
110
+ return tipTx
111
+ }
112
+
113
+ }
@@ -36,3 +36,22 @@ export interface SolanaBlockMetaUpdateEvent {
36
36
  entriesCount: number;
37
37
  }
38
38
 
39
+ /**
40
+ * 推送给询价、交易进程的Account Change Event 结构
41
+ */
42
+ export interface SolanaPoolAccountUpdateEventData {
43
+ type: string
44
+ provider_id: string
45
+ event_time: number
46
+ dexId: string
47
+ pair: string
48
+ pool_address: string
49
+ pool_name: string
50
+ data: {
51
+ slot: number
52
+ writeVersion: number
53
+ transactionHash: string
54
+ accountData: string
55
+ }
56
+ }
57
+
package/tsconfig.json CHANGED
@@ -5,10 +5,11 @@
5
5
  "./types",
6
6
  ],
7
7
  "lib": [
8
- "es2015"
8
+ "es2017",
9
+ "dom"
9
10
  ],
10
11
  "module": "CommonJS",
11
- "target": "es6",
12
+ "target": "es2017",
12
13
  "rootDirs": [
13
14
  "src"
14
15
  ],
@@ -17,6 +18,8 @@
17
18
  "esModuleInterop": true,
18
19
  "skipLibCheck": true,
19
20
  "resolveJsonModule": true,
20
- "removeComments": true,
21
- }
22
- }
21
+ "removeComments": true
22
+ },
23
+ "include": ["src/**/*"],
24
+ "exclude": ["dist"]
25
+ }
@@ -1,22 +0,0 @@
1
- export interface SolanaAccountUpdateEvent {
2
- recv_ms: number;
3
- providerId: string;
4
- slot: number;
5
- writeVersion: number;
6
- transactionHash: string;
7
- dexId: string;
8
- pair: string;
9
- poolAddress: string;
10
- poolName?: string;
11
- accountData?: Buffer | string;
12
- }
13
- export interface SolanaBlockMetaUpdateEvent {
14
- slot: number;
15
- blockhash: string;
16
- blockTime: number;
17
- BlockHeight: number;
18
- parentSlot: number;
19
- parentBlockhash: string;
20
- executedTransactionCount: number;
21
- entriesCount: number;
22
- }
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });