@clonegod/ttd-sol-common 2.0.75 → 2.0.76
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.
- package/dist/common/subscribe_account_update.d.ts +2 -2
- package/dist/common/subscribe_account_update.js +8 -3
- package/dist/quote/abstract_dex_quote.d.ts +3 -2
- package/dist/quote/abstract_dex_quote.js +33 -16
- package/dist/quote/chain_ops.d.ts +2 -1
- package/dist/quote/chain_ops.js +13 -2
- package/dist/quote/pool_subscription_registry.js +5 -1
- package/dist/quote/verify/quote_price_verify.d.ts +2 -0
- package/dist/quote/verify/quote_price_verify.js +11 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.js +2 -1
- package/package.json +2 -2
- package/src/common/subscribe_account_update.ts +21 -6
- package/src/quote/abstract_dex_quote.ts +59 -23
- package/src/quote/chain_ops.ts +31 -9
- package/src/quote/pool_event.ts +3 -2
- package/src/quote/pool_subscription_registry.ts +8 -2
- package/src/quote/verify/quote_price_verify.ts +36 -0
- package/src/types/index.ts +32 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { StandardPoolInfoType } from "@clonegod/ttd-core";
|
|
2
2
|
import { DEX_ID } from "@clonegod/ttd-core/dist";
|
|
3
|
-
import { SolanaPoolAccountUpdateEventData } from "../types";
|
|
4
|
-
export declare function subscribe_pool_account_update(dex_id: DEX_ID, pool_list: StandardPoolInfoType[], callback: (eventData: SolanaPoolAccountUpdateEventData) => void): void;
|
|
3
|
+
import { SolanaPoolAccountUpdateEventData, SolanaSwapVerifyEventData } from "../types";
|
|
4
|
+
export declare function subscribe_pool_account_update(dex_id: DEX_ID, pool_list: StandardPoolInfoType[], callback: (eventData: SolanaPoolAccountUpdateEventData) => void, onSwapVerify?: (eventData: SolanaSwapVerifyEventData) => void): void;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.subscribe_pool_account_update = subscribe_pool_account_update;
|
|
4
4
|
const dist_1 = require("@clonegod/ttd-core/dist");
|
|
5
|
-
function subscribe_pool_account_update(dex_id, pool_list, callback) {
|
|
5
|
+
function subscribe_pool_account_update(dex_id, pool_list, callback, onSwapVerify) {
|
|
6
6
|
const port = dist_1.SERVICE_PORT.STREAM_QUOTE_WS;
|
|
7
7
|
const ws_url = `ws://127.0.0.1:${port}`;
|
|
8
8
|
const ws_client = new dist_1.WebSocketClient(ws_url);
|
|
@@ -16,11 +16,16 @@ function subscribe_pool_account_update(dex_id, pool_list, callback) {
|
|
|
16
16
|
(0, dist_1.log_info)(`subscribe_pool_account_update: ACK ${eventData.ws_id || eventData.pool_address || ''}`);
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
if (
|
|
19
|
+
if (eventData.kind === 'swap_verify') {
|
|
20
|
+
onSwapVerify?.(eventData);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const accountData = eventData;
|
|
24
|
+
if (!accountData.data) {
|
|
20
25
|
(0, dist_1.log_warn)(`subscribe_pool_account_update: no data`, eventData);
|
|
21
26
|
return;
|
|
22
27
|
}
|
|
23
|
-
callback(
|
|
28
|
+
callback(accountData);
|
|
24
29
|
}));
|
|
25
30
|
ws_client.connect();
|
|
26
31
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AppConfig, QuoteDepthOutput, QuoteResultType, StandardPoolInfoType } from '@clonegod/ttd-core/dist';
|
|
2
2
|
import { SolanaChainOps } from './chain_ops';
|
|
3
3
|
import { SolPoolEvent } from './pool_event';
|
|
4
|
-
import { QuotePriceVerify
|
|
4
|
+
import { QuotePriceVerify } from './verify/quote_price_verify';
|
|
5
5
|
import { QuoteTrace } from './quote_trace';
|
|
6
6
|
export type ConsistencyPolicy = 'snapshot' | 'slot-gate' | 'tx-driven';
|
|
7
7
|
export interface PoolInitSummary {
|
|
@@ -50,7 +50,7 @@ export declare abstract class AbstractDexQuote<C extends SolanaChainOps = Solana
|
|
|
50
50
|
protected calculateDepth(_poolInfo: StandardPoolInfoType, _poolAddress: string, _priceMap: Map<string, {
|
|
51
51
|
price: string;
|
|
52
52
|
} | undefined>): Promise<QuoteDepthOutput | undefined>;
|
|
53
|
-
protected
|
|
53
|
+
protected readonly feeInVault: boolean;
|
|
54
54
|
protected isStateConsistentForQuote(_poolInfo: StandardPoolInfoType): boolean;
|
|
55
55
|
protected accountSlots: Map<string, number>;
|
|
56
56
|
protected recordAccountSlot(accountAddr: string, slot: number): void;
|
|
@@ -58,6 +58,7 @@ export declare abstract class AbstractDexQuote<C extends SolanaChainOps = Solana
|
|
|
58
58
|
protected beforeLoadPools(): Promise<void>;
|
|
59
59
|
protected getQuoteAmountUsd(poolInfo: StandardPoolInfoType): number;
|
|
60
60
|
private registerEventHandlers;
|
|
61
|
+
private handleSwapVerify;
|
|
61
62
|
private assertValidBlockNumber;
|
|
62
63
|
private isStaleUpdate;
|
|
63
64
|
private shouldApplyState;
|
|
@@ -15,13 +15,13 @@ class AbstractDexQuote {
|
|
|
15
15
|
this.latestBlockSlot = 0;
|
|
16
16
|
this.MIN_QUOTE_INTERVAL_MS = Math.max(3000, parseInt(process.env.MIN_QUOTE_INTERVAL_MS || '10000', 10) || 10000);
|
|
17
17
|
this.consistencyPolicy = 'snapshot';
|
|
18
|
+
this.feeInVault = true;
|
|
18
19
|
this.accountSlots = new Map();
|
|
19
20
|
this.appConfig = appConfig;
|
|
20
21
|
this.chain = chain;
|
|
21
22
|
}
|
|
22
23
|
async refreshStateFromEvent(_poolInfo, _evt) { }
|
|
23
24
|
async calculateDepth(_poolInfo, _poolAddress, _priceMap) { return undefined; }
|
|
24
|
-
buildSwapVerify(_poolInfo, _evt) { return null; }
|
|
25
25
|
isStateConsistentForQuote(_poolInfo) { return true; }
|
|
26
26
|
recordAccountSlot(accountAddr, slot) {
|
|
27
27
|
this.accountSlots.set(accountAddr, slot);
|
|
@@ -50,12 +50,34 @@ class AbstractDexQuote {
|
|
|
50
50
|
registerEventHandlers() {
|
|
51
51
|
this.chain.subscribeNewBlock((raw) => this.handleBlockUpdateEvent(raw));
|
|
52
52
|
this.chain.subscribePoolEvents(this.dexId, Array.from(this.poolInfoMap.values()), async (evt) => {
|
|
53
|
-
const
|
|
53
|
+
const bundle = await this.calculateQuote(evt);
|
|
54
54
|
if (bundle)
|
|
55
55
|
this.publishQuote(bundle);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
}, (sv) => this.handleSwapVerify(sv));
|
|
57
|
+
}
|
|
58
|
+
handleSwapVerify(sv) {
|
|
59
|
+
const poolInfo = this.poolInfoMap.get(sv.pool_address);
|
|
60
|
+
if (!poolInfo)
|
|
61
|
+
return;
|
|
62
|
+
const params = {
|
|
63
|
+
poolAddress: poolInfo.pool_address,
|
|
64
|
+
poolName: poolInfo.pool_name,
|
|
65
|
+
blockNumber: Number(sv.slot),
|
|
66
|
+
txHash: sv.tx_hash || 'swap',
|
|
67
|
+
amount0: sv.amount0,
|
|
68
|
+
amount1: sv.amount1,
|
|
69
|
+
token0Address: poolInfo.tokenA?.address,
|
|
70
|
+
token1Address: poolInfo.tokenB?.address,
|
|
71
|
+
token0Decimals: Number(poolInfo.tokenA?.decimals ?? 0),
|
|
72
|
+
token1Decimals: Number(poolInfo.tokenB?.decimals ?? 0),
|
|
73
|
+
poolInfo: { tokenA: poolInfo.tokenA, tokenB: poolInfo.tokenB, quote_token: poolInfo.quote_token },
|
|
74
|
+
swapperDeltaConvention: false,
|
|
75
|
+
feeExclusiveExec: !this.feeInVault,
|
|
76
|
+
feeRateBps: poolInfo.fee_rate,
|
|
77
|
+
};
|
|
78
|
+
const verifyLog = this.quotePriceVerify.checkSwap(params);
|
|
79
|
+
if (verifyLog)
|
|
80
|
+
(0, dist_1.log_info)(verifyLog);
|
|
59
81
|
}
|
|
60
82
|
assertValidBlockNumber(blockNumber, ctx) {
|
|
61
83
|
if (!Number.isInteger(blockNumber) || blockNumber <= 0) {
|
|
@@ -76,7 +98,7 @@ class AbstractDexQuote {
|
|
|
76
98
|
async handleBlockUpdateEvent(eventData) {
|
|
77
99
|
const parsed = JSON.parse(eventData);
|
|
78
100
|
const slot = Number(parsed.slot);
|
|
79
|
-
const
|
|
101
|
+
const streamRecvMs = Number(parsed.recvBlockTime) || Date.now();
|
|
80
102
|
this.assertValidBlockNumber(slot, `${this.dexId} block-event`);
|
|
81
103
|
this.latestBlockSlot = slot;
|
|
82
104
|
for (const poolInfo of this.poolInfoMap.values()) {
|
|
@@ -85,7 +107,7 @@ class AbstractDexQuote {
|
|
|
85
107
|
const last = this.poolLastQuoteTimeMap.get(poolAddress) || 0;
|
|
86
108
|
if (now - last >= this.MIN_QUOTE_INTERVAL_MS) {
|
|
87
109
|
this.poolLastQuoteTimeMap.set(poolAddress, now);
|
|
88
|
-
const result = await this.calculateQuoteForPool(poolInfo,
|
|
110
|
+
const result = await this.calculateQuoteForPool(poolInfo, streamRecvMs, slot);
|
|
89
111
|
if (result)
|
|
90
112
|
this.publishQuote(result);
|
|
91
113
|
}
|
|
@@ -94,23 +116,18 @@ class AbstractDexQuote {
|
|
|
94
116
|
async calculateQuote(evt) {
|
|
95
117
|
const poolInfo = this.poolInfoMap.get(evt.pool_address);
|
|
96
118
|
if (!poolInfo)
|
|
97
|
-
return
|
|
119
|
+
return null;
|
|
98
120
|
this.assertValidBlockNumber(evt.blockNumber, `${this.dexId} ${poolInfo.pool_name} account-update`);
|
|
99
|
-
let verifyLog;
|
|
100
|
-
const v = this.buildSwapVerify(poolInfo, evt);
|
|
101
|
-
if (v)
|
|
102
|
-
verifyLog = this.quotePriceVerify.checkSwap(v) || undefined;
|
|
103
121
|
const hasWv = Number.isInteger(evt.writeVersion);
|
|
104
122
|
const skipApply = hasWv && !this.shouldApplyState(evt.pool_address, evt.blockNumber, evt.writeVersion);
|
|
105
123
|
if (!skipApply)
|
|
106
124
|
await this.refreshStateFromEvent(poolInfo, evt);
|
|
107
125
|
if (hasWv && this.isStaleUpdate(evt.pool_address, evt.blockNumber, evt.writeVersion))
|
|
108
|
-
return
|
|
126
|
+
return null;
|
|
109
127
|
if (this.consistencyPolicy === 'slot-gate' && !this.isStateConsistentForQuote(poolInfo)) {
|
|
110
|
-
return
|
|
128
|
+
return null;
|
|
111
129
|
}
|
|
112
|
-
|
|
113
|
-
return { bundle, verifyLog };
|
|
130
|
+
return this.calculateQuoteForPool(poolInfo, evt.blockTime, evt.blockNumber, evt);
|
|
114
131
|
}
|
|
115
132
|
async calculateQuoteForPool(poolInfo, streamTimestamp, blockNumber, evt) {
|
|
116
133
|
const { pool_address } = poolInfo;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AppConfig, CHAIN_ID, StandardPoolInfoType } from '@clonegod/ttd-core/dist';
|
|
2
2
|
import { Connection } from '@solana/web3.js';
|
|
3
|
+
import { SolanaSwapVerifyEventData } from '../types';
|
|
3
4
|
import { SolPoolEvent } from './pool_event';
|
|
4
5
|
import { TokenPriceCache } from './pricing/token_price_cache';
|
|
5
6
|
export declare class SolanaChainOps {
|
|
@@ -14,5 +15,5 @@ export declare class SolanaChainOps {
|
|
|
14
15
|
price: string;
|
|
15
16
|
} | undefined>>;
|
|
16
17
|
subscribeNewBlock(handler: (raw: string) => void): void;
|
|
17
|
-
subscribePoolEvents(dexId: string, pools: StandardPoolInfoType[], handler: (evt: SolPoolEvent) => void): void;
|
|
18
|
+
subscribePoolEvents(dexId: string, pools: StandardPoolInfoType[], handler: (evt: SolPoolEvent) => void, onSwapVerify?: (evt: SolanaSwapVerifyEventData) => void): void;
|
|
18
19
|
}
|
package/dist/quote/chain_ops.js
CHANGED
|
@@ -48,7 +48,7 @@ class SolanaChainOps {
|
|
|
48
48
|
subscribeNewBlock(handler) {
|
|
49
49
|
this.appConfig.arb_event_subscriber.subscribe_new_block(this.chainId, handler);
|
|
50
50
|
}
|
|
51
|
-
subscribePoolEvents(dexId, pools, handler) {
|
|
51
|
+
subscribePoolEvents(dexId, pools, handler, onSwapVerify) {
|
|
52
52
|
const dex = dexId.toUpperCase();
|
|
53
53
|
const poolSet = new Set(pools.map(p => p.pool_address));
|
|
54
54
|
(0, subscribe_account_update_1.subscribe_pool_account_update)(dex, pools, (data) => {
|
|
@@ -60,7 +60,18 @@ class SolanaChainOps {
|
|
|
60
60
|
catch (e) {
|
|
61
61
|
(0, dist_1.log_warn)(`[SolanaChainOps] bad pool account update`, data);
|
|
62
62
|
}
|
|
63
|
-
}
|
|
63
|
+
}, onSwapVerify
|
|
64
|
+
? (sv) => {
|
|
65
|
+
try {
|
|
66
|
+
if (!poolSet.has(sv.pool_address))
|
|
67
|
+
return;
|
|
68
|
+
onSwapVerify(sv);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
(0, dist_1.log_warn)(`[SolanaChainOps] bad swap-verify event`, sv);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
: undefined);
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
exports.SolanaChainOps = SolanaChainOps;
|
|
@@ -46,7 +46,11 @@ async function unregisterPoolSubscriptions(appConfig, quote_app_id) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
async function attachPoolSubscriptionLifecycle(appConfig, pools) {
|
|
49
|
-
const quote_app_id = String(appConfig.env_args.app_name || '')
|
|
49
|
+
const quote_app_id = process.env.name || String(appConfig.env_args.app_name || '');
|
|
50
|
+
if (!quote_app_id) {
|
|
51
|
+
throw new Error('[pool/subscriptions] 无法确定 quote_app_id:process.env.name(PM2 进程名) 与 APP_NAME 均为空。'
|
|
52
|
+
+ '询价进程必须由 PM2/trade-mgt 以确定进程名启动,否则订阅会被 reconcile 当孤儿清除。');
|
|
53
|
+
}
|
|
50
54
|
await registerPoolSubscriptions(appConfig, pools, quote_app_id);
|
|
51
55
|
let cleaned = false;
|
|
52
56
|
const cleanup = async (sig) => {
|
|
@@ -47,6 +47,11 @@ function pickMatchingTier(tiers, actualAmountIn) {
|
|
|
47
47
|
}
|
|
48
48
|
return { tier: top, mode: 'extrapolated_high', lo_pct: top.pct, hi_pct: null };
|
|
49
49
|
}
|
|
50
|
+
function stripFeeExclusive(refPrice, isBuy, r) {
|
|
51
|
+
if (!(r > 0) || r >= 1)
|
|
52
|
+
return refPrice;
|
|
53
|
+
return isBuy ? refPrice * (1 - r) : refPrice / (1 - r);
|
|
54
|
+
}
|
|
50
55
|
class QuotePriceVerify {
|
|
51
56
|
constructor() {
|
|
52
57
|
this.quoteCache = new Map();
|
|
@@ -109,6 +114,9 @@ class QuotePriceVerify {
|
|
|
109
114
|
if (!match)
|
|
110
115
|
continue;
|
|
111
116
|
refPrice = match.tier.price;
|
|
117
|
+
if (params.feeExclusiveExec && match.tier.amount_in > 0) {
|
|
118
|
+
refPrice = stripFeeExclusive(refPrice, isBuy, match.tier.fee / match.tier.amount_in);
|
|
119
|
+
}
|
|
112
120
|
tierInfo = {
|
|
113
121
|
matched_pct: parseFloat(match.tier.pct.toFixed(4)),
|
|
114
122
|
mode: match.mode,
|
|
@@ -119,6 +127,9 @@ class QuotePriceVerify {
|
|
|
119
127
|
}
|
|
120
128
|
else {
|
|
121
129
|
refPrice = isBuy ? cached.askPrice : cached.bidPrice;
|
|
130
|
+
if (params.feeExclusiveExec && params.feeRateBps && params.feeRateBps > 0) {
|
|
131
|
+
refPrice = stripFeeExclusive(refPrice, isBuy, params.feeRateBps / 10000);
|
|
132
|
+
}
|
|
122
133
|
}
|
|
123
134
|
if (refPrice <= 0)
|
|
124
135
|
continue;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -19,6 +19,21 @@ export interface SolanaAccountUpdateEvent {
|
|
|
19
19
|
accountType: SolanaPoolAccountType;
|
|
20
20
|
accountData?: Buffer | string;
|
|
21
21
|
}
|
|
22
|
+
export interface SolanaSwapVerifyEventData {
|
|
23
|
+
kind: 'swap_verify';
|
|
24
|
+
provider_id: string;
|
|
25
|
+
event_time: number;
|
|
26
|
+
dex_id: string;
|
|
27
|
+
pool_address: string;
|
|
28
|
+
pool_name: string;
|
|
29
|
+
slot: number;
|
|
30
|
+
tx_index: number;
|
|
31
|
+
tx_hash: string;
|
|
32
|
+
block_time: number;
|
|
33
|
+
amount0: string;
|
|
34
|
+
amount1: string;
|
|
35
|
+
}
|
|
36
|
+
export declare const EVENT_POOL_SWAP_VERIFY = "EVENT_POOL_SWAP_VERIFY";
|
|
22
37
|
export interface SolanaBlockMetaUpdateEvent {
|
|
23
38
|
slot: number;
|
|
24
39
|
blockHash: string;
|
package/dist/types/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SolanaPoolAccountType = void 0;
|
|
3
|
+
exports.EVENT_POOL_SWAP_VERIFY = exports.SolanaPoolAccountType = void 0;
|
|
4
4
|
var SolanaPoolAccountType;
|
|
5
5
|
(function (SolanaPoolAccountType) {
|
|
6
6
|
SolanaPoolAccountType["POOL"] = "pool";
|
|
@@ -9,3 +9,4 @@ var SolanaPoolAccountType;
|
|
|
9
9
|
SolanaPoolAccountType["TICK_ARRAY"] = "tickArray";
|
|
10
10
|
SolanaPoolAccountType["BIN_ARRAY"] = "binArray";
|
|
11
11
|
})(SolanaPoolAccountType || (exports.SolanaPoolAccountType = SolanaPoolAccountType = {}));
|
|
12
|
+
exports.EVENT_POOL_SWAP_VERIFY = 'EVENT_POOL_SWAP_VERIFY';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clonegod/ttd-sol-common",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.76",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"push": "npm run build && npm publish"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@clonegod/ttd-core": "3.1.
|
|
16
|
+
"@clonegod/ttd-core": "3.1.89",
|
|
17
17
|
"@solana/web3.js": "1.91.6",
|
|
18
18
|
"rpc-websockets": "7.10.0",
|
|
19
19
|
"axios": "1.17.0",
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { StandardPoolInfoType } from "@clonegod/ttd-core";
|
|
2
2
|
import { DEX_ID, log_info, log_warn, SERVICE_PORT, WebSocketClient } from "@clonegod/ttd-core/dist";
|
|
3
|
-
import { SolanaPoolAccountUpdateEventData } from "../types";
|
|
3
|
+
import { SolanaPoolAccountUpdateEventData, SolanaSwapVerifyEventData } from "../types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* 集中订阅方式,丛 solana-stream 获取池子变化事件,获得poolRawData
|
|
6
|
+
* 集中订阅方式,丛 solana-stream 获取池子变化事件,获得poolRawData
|
|
7
7
|
* - 询价:基于池子数据计算价格
|
|
8
8
|
* - 交易使用:交易中需要池子的最新状态
|
|
9
|
+
*
|
|
10
|
+
* 同一条 WS 上承载两类消息(按 `kind` 判别):
|
|
11
|
+
* - 账户快照(无 kind)→ callback(刷新本地状态算价)
|
|
12
|
+
* - swap-verify(kind:'swap_verify')→ onSwapVerify(对账报价偏差)
|
|
9
13
|
*/
|
|
10
|
-
export function subscribe_pool_account_update(
|
|
14
|
+
export function subscribe_pool_account_update(
|
|
15
|
+
dex_id: DEX_ID,
|
|
16
|
+
pool_list: StandardPoolInfoType[],
|
|
17
|
+
callback: (eventData: SolanaPoolAccountUpdateEventData) => void,
|
|
18
|
+
onSwapVerify?: (eventData: SolanaSwapVerifyEventData) => void,
|
|
19
|
+
) {
|
|
11
20
|
const port = SERVICE_PORT.STREAM_QUOTE_WS;
|
|
12
21
|
const ws_url = `ws://127.0.0.1:${port}`;
|
|
13
22
|
const ws_client = new WebSocketClient(ws_url)
|
|
@@ -16,17 +25,23 @@ export function subscribe_pool_account_update(dex_id: DEX_ID, pool_list: Standar
|
|
|
16
25
|
ws_client.send(JSON.stringify({ dex_id, pool_name, pool_address }))
|
|
17
26
|
})
|
|
18
27
|
})
|
|
19
|
-
ws_client.onMessage(((eventData: SolanaPoolAccountUpdateEventData): void => {
|
|
28
|
+
ws_client.onMessage(((eventData: SolanaPoolAccountUpdateEventData | SolanaSwapVerifyEventData): void => {
|
|
20
29
|
// 订阅 ACK(stream-quote 确认订阅成功,无 data 字段)→ 打 info 确认链路通,不当异常告警
|
|
21
30
|
if ((eventData as any).type === 'subscribed') {
|
|
22
31
|
log_info(`subscribe_pool_account_update: ACK ${(eventData as any).ws_id || (eventData as any).pool_address || ''}`);
|
|
23
32
|
return;
|
|
24
33
|
}
|
|
25
|
-
|
|
34
|
+
// swap-verify 消息:pool-side vault Δ,交给 onSwapVerify 对账(必须早于 !data 判定,本消息无 data 字段)
|
|
35
|
+
if ((eventData as any).kind === 'swap_verify') {
|
|
36
|
+
onSwapVerify?.(eventData as SolanaSwapVerifyEventData);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const accountData = eventData as SolanaPoolAccountUpdateEventData;
|
|
40
|
+
if (!accountData.data) {
|
|
26
41
|
log_warn(`subscribe_pool_account_update: no data`, eventData);
|
|
27
42
|
return;
|
|
28
43
|
}
|
|
29
|
-
callback(
|
|
44
|
+
callback(accountData);
|
|
30
45
|
}));
|
|
31
46
|
ws_client.connect();
|
|
32
47
|
}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
AppConfig, QuoteDepthOutput, QuoteResultType, StandardPoolInfoType,
|
|
3
3
|
log_info, log_warn, on_quote_response, report_quote_candidate,
|
|
4
4
|
} from '@clonegod/ttd-core/dist'
|
|
5
|
-
import { SolanaBlockMetaUpdateEvent } from '../types'
|
|
5
|
+
import { SolanaBlockMetaUpdateEvent, SolanaSwapVerifyEventData } from '../types'
|
|
6
6
|
import { SolanaChainOps } from './chain_ops'
|
|
7
7
|
import { SolPoolEvent } from './pool_event'
|
|
8
8
|
import { QuotePriceVerify, CheckSwapParams } from './verify/quote_price_verify'
|
|
@@ -15,7 +15,9 @@ import { QuoteTrace } from './quote_trace'
|
|
|
15
15
|
// 因此去掉 SUI 的 swap/add/remove 事件分类(debouncedEventTypes)。
|
|
16
16
|
// · 单调水位线用 Solana 账户 writeVersion(= txIndex 对等物);wire 暂未带 → 缺省按"无序号"处理。
|
|
17
17
|
// · 无 v3 PriceFeed(同 SUI,省 do_quote_v3)。
|
|
18
|
-
// · verify swap-
|
|
18
|
+
// · verify(报价偏差)走**独立的 swap-verify 通道**(非账户快照):stream-quote 从真实交易的
|
|
19
|
+
// pre/postTokenBalances 解码订阅池子 vault Δ(DEX 无关)→ WS push → handleSwapVerify → checkSwap。
|
|
20
|
+
// exec_price = |Δquote/Δbase|(pool-side vault Δ),fee-exclusive 对账(费路由出池的 DEX 由 feeInVault=false 触发剥费)。
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* 流动性一致性策略(设计文档 §2.2 定稿:3 订阅原型)。各 DEX 子类按协议特性声明:
|
|
@@ -111,8 +113,13 @@ export abstract class AbstractDexQuote<C extends SolanaChainOps = SolanaChainOps
|
|
|
111
113
|
protected async refreshStateFromEvent(_poolInfo: StandardPoolInfoType, _evt: SolPoolEvent): Promise<void> {}
|
|
112
114
|
/** 深度(tier 化);fee 来自链上 load。priceMap 由基类预取。默认 undefined(step 3 各 DEX 逐档实现)。 */
|
|
113
115
|
protected async calculateDepth(_poolInfo: StandardPoolInfoType, _poolAddress: string, _priceMap: Map<string, { price: string } | undefined>): Promise<QuoteDepthOutput | undefined> { return undefined }
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
+
/**
|
|
117
|
+
* verify 口径:池子手续费是否留在 vault 内。
|
|
118
|
+
* - true(默认;Raydium/Orca/Meteora 等):fee 累进储备 → vault Δ 含费 → 与含费报价同口径,不剥费。
|
|
119
|
+
* - false(pumpswap:protocol/creator fee 路由出池到独立账户):vault Δ 不含费 → checkSwap 把参考价剥成 fee-exclusive。
|
|
120
|
+
* 详见 quote_price_verify CheckSwapParams.feeExclusiveExec。
|
|
121
|
+
*/
|
|
122
|
+
protected readonly feeInVault: boolean = true
|
|
116
123
|
|
|
117
124
|
/**
|
|
118
125
|
* slot 门控钩子(仅 consistencyPolicy='slot-gate' 生效):判断本池"走单触碰的多账户"状态是否 slot 一致。
|
|
@@ -160,12 +167,43 @@ export abstract class AbstractDexQuote<C extends SolanaChainOps = SolanaChainOps
|
|
|
160
167
|
// v1 兜底:new_block 通道(stream-block)
|
|
161
168
|
this.chain.subscribeNewBlock((raw) => this.handleBlockUpdateEvent(raw))
|
|
162
169
|
// v2 事件:pool 账户更新(stream-quote WS)—— 直连 refresh → compute → publish,(block,writeVersion) 水位线兜底
|
|
163
|
-
this.chain.subscribePoolEvents(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
this.chain.subscribePoolEvents(
|
|
171
|
+
this.dexId,
|
|
172
|
+
Array.from(this.poolInfoMap.values()),
|
|
173
|
+
async (evt: SolPoolEvent) => {
|
|
174
|
+
const bundle = await this.calculateQuote(evt)
|
|
175
|
+
if (bundle) this.publishQuote(bundle)
|
|
176
|
+
},
|
|
177
|
+
// swap-verify 通道(链上真实成交 vault Δ):独立于账户快照,对账缓存报价 → [Verify]
|
|
178
|
+
(sv: SolanaSwapVerifyEventData) => this.handleSwapVerify(sv),
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 消费 stream-quote 推来的 swap-verify(pool-side vault Δ)→ checkSwap 对账缓存报价 → 打 [Verify]。
|
|
184
|
+
* exec_price 由 |Δquote/Δbase| 算;fee-exclusive 口径由 feeInVault 决定(详见 CheckSwapParams.feeExclusiveExec)。
|
|
185
|
+
*/
|
|
186
|
+
private handleSwapVerify(sv: SolanaSwapVerifyEventData): void {
|
|
187
|
+
const poolInfo = this.poolInfoMap.get(sv.pool_address)
|
|
188
|
+
if (!poolInfo) return
|
|
189
|
+
const params: CheckSwapParams = {
|
|
190
|
+
poolAddress: poolInfo.pool_address,
|
|
191
|
+
poolName: poolInfo.pool_name,
|
|
192
|
+
blockNumber: Number(sv.slot),
|
|
193
|
+
txHash: sv.tx_hash || 'swap',
|
|
194
|
+
amount0: sv.amount0,
|
|
195
|
+
amount1: sv.amount1,
|
|
196
|
+
token0Address: poolInfo.tokenA?.address,
|
|
197
|
+
token1Address: poolInfo.tokenB?.address,
|
|
198
|
+
token0Decimals: Number(poolInfo.tokenA?.decimals ?? 0),
|
|
199
|
+
token1Decimals: Number(poolInfo.tokenB?.decimals ?? 0),
|
|
200
|
+
poolInfo: { tokenA: poolInfo.tokenA, tokenB: poolInfo.tokenB, quote_token: poolInfo.quote_token },
|
|
201
|
+
swapperDeltaConvention: false, // amount0/1 是 pool-side vault Δ
|
|
202
|
+
feeExclusiveExec: !this.feeInVault, // 费路由出池(pumpswap)→ 剥参考价的费
|
|
203
|
+
feeRateBps: poolInfo.fee_rate, // market-data 强制整数 bps
|
|
204
|
+
}
|
|
205
|
+
const verifyLog = this.quotePriceVerify.checkSwap(params)
|
|
206
|
+
if (verifyLog) log_info(verifyLog)
|
|
169
207
|
}
|
|
170
208
|
|
|
171
209
|
// ════════════ 事件处理(基类)════════════
|
|
@@ -192,9 +230,11 @@ export abstract class AbstractDexQuote<C extends SolanaChainOps = SolanaChainOps
|
|
|
192
230
|
|
|
193
231
|
private async handleBlockUpdateEvent(eventData: string): Promise<void> {
|
|
194
232
|
const parsed = JSON.parse(eventData) as SolanaBlockMetaUpdateEvent
|
|
195
|
-
// slot
|
|
233
|
+
// slot 可能是 protobuf u64 字符串 → 在"blockNumber"归一化边界统一转 number
|
|
196
234
|
const slot = Number(parsed.slot)
|
|
197
|
-
|
|
235
|
+
// streamTimestamp 用本地接收 ms(recvBlockTime),与 v2 的 event_time(ms) 同单位;
|
|
236
|
+
// 链上 blockTime 是秒,直接当时间戳会让 analyze 延迟列混入绝对秒值(与 ms 相减失真)
|
|
237
|
+
const streamRecvMs = Number(parsed.recvBlockTime) || Date.now()
|
|
198
238
|
this.assertValidBlockNumber(slot, `${this.dexId} block-event`)
|
|
199
239
|
this.latestBlockSlot = slot // v1 兜底询价取当前 slot 用(动态费 currentPoint)
|
|
200
240
|
for (const poolInfo of this.poolInfoMap.values()) {
|
|
@@ -203,21 +243,18 @@ export abstract class AbstractDexQuote<C extends SolanaChainOps = SolanaChainOps
|
|
|
203
243
|
const last = this.poolLastQuoteTimeMap.get(poolAddress) || 0
|
|
204
244
|
if (now - last >= this.MIN_QUOTE_INTERVAL_MS) {
|
|
205
245
|
this.poolLastQuoteTimeMap.set(poolAddress, now) // await 前占位,防节流被打穿
|
|
206
|
-
const result = await this.calculateQuoteForPool(poolInfo,
|
|
246
|
+
const result = await this.calculateQuoteForPool(poolInfo, streamRecvMs, slot)
|
|
207
247
|
if (result) this.publishQuote(result)
|
|
208
248
|
}
|
|
209
249
|
}
|
|
210
250
|
}
|
|
211
251
|
|
|
212
|
-
private async calculateQuote(evt: SolPoolEvent): Promise<
|
|
252
|
+
private async calculateQuote(evt: SolPoolEvent): Promise<QuoteBundle | null> {
|
|
213
253
|
const poolInfo = this.poolInfoMap.get(evt.pool_address)
|
|
214
|
-
if (!poolInfo) return
|
|
254
|
+
if (!poolInfo) return null
|
|
215
255
|
this.assertValidBlockNumber(evt.blockNumber, `${this.dexId} ${poolInfo.pool_name} account-update`)
|
|
216
256
|
|
|
217
|
-
//
|
|
218
|
-
let verifyLog: string | undefined
|
|
219
|
-
const v = this.buildSwapVerify(poolInfo, evt)
|
|
220
|
-
if (v) verifyLog = this.quotePriceVerify.checkSwap(v) || undefined
|
|
257
|
+
// 注:报价偏差 verify 不在此(账户快照不含 swap amount),走独立 swap-verify 通道(handleSwapVerify)。
|
|
221
258
|
|
|
222
259
|
// 状态更新:每个账户快照都做(不受报价 staleness 短路影响)。
|
|
223
260
|
// 例外:乱序晚到的旧 writeVersion 跳过,防状态回拉(writeVersion 缺省时无条件应用)。
|
|
@@ -226,15 +263,14 @@ export abstract class AbstractDexQuote<C extends SolanaChainOps = SolanaChainOps
|
|
|
226
263
|
if (!skipApply) await this.refreshStateFromEvent(poolInfo, evt)
|
|
227
264
|
|
|
228
265
|
// compute 前短路:已陈旧 → 跳过重算+推送(状态已更新)
|
|
229
|
-
if (hasWv && this.isStaleUpdate(evt.pool_address, evt.blockNumber, evt.writeVersion as number)) return
|
|
266
|
+
if (hasWv && this.isStaleUpdate(evt.pool_address, evt.blockNumber, evt.writeVersion as number)) return null
|
|
230
267
|
|
|
231
268
|
// slot 门控(slot-gate 协议):走单触碰的多账户 slot 不一致 → 本拍不报价,等落后账户的下一条更新重触发
|
|
232
269
|
if (this.consistencyPolicy === 'slot-gate' && !this.isStateConsistentForQuote(poolInfo)) {
|
|
233
|
-
return
|
|
270
|
+
return null
|
|
234
271
|
}
|
|
235
272
|
|
|
236
|
-
|
|
237
|
-
return { bundle, verifyLog }
|
|
273
|
+
return this.calculateQuoteForPool(poolInfo, evt.blockTime, evt.blockNumber, evt)
|
|
238
274
|
}
|
|
239
275
|
|
|
240
276
|
private async calculateQuoteForPool(poolInfo: StandardPoolInfoType, streamTimestamp: number, blockNumber: number, evt?: SolPoolEvent): Promise<QuoteBundle | null> {
|
package/src/quote/chain_ops.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AppConfig, CHAIN_ID, DEX_ID, log_warn, StandardPoolInfoType } from '@clonegod/ttd-core/dist'
|
|
2
2
|
import { Connection, PublicKey } from '@solana/web3.js'
|
|
3
3
|
import { subscribe_pool_account_update } from '../common/subscribe_account_update'
|
|
4
|
+
import { SolanaSwapVerifyEventData } from '../types'
|
|
4
5
|
import { SolPoolEvent, toSolPoolEvent } from './pool_event'
|
|
5
6
|
import { TokenPriceCache } from './pricing/token_price_cache'
|
|
6
7
|
|
|
@@ -75,17 +76,38 @@ export class SolanaChainOps {
|
|
|
75
76
|
* 订阅池子账户更新(v2 触发)—— 消费 stream-quote 的 WS 通道。
|
|
76
77
|
* subscribe_pool_account_update 内部 onOpen 时只 send 注册池,stream-quote 据此扇出;
|
|
77
78
|
* 这里把原始 wire 归一化为 SolPoolEvent 再回调。
|
|
79
|
+
*
|
|
80
|
+
* onSwapVerify(可选):同一 WS 上的 swap-verify 消息(链上真实成交 vault Δ),用于报价偏差对账。
|
|
78
81
|
*/
|
|
79
|
-
subscribePoolEvents(
|
|
82
|
+
subscribePoolEvents(
|
|
83
|
+
dexId: string,
|
|
84
|
+
pools: StandardPoolInfoType[],
|
|
85
|
+
handler: (evt: SolPoolEvent) => void,
|
|
86
|
+
onSwapVerify?: (evt: SolanaSwapVerifyEventData) => void,
|
|
87
|
+
): void {
|
|
80
88
|
const dex = dexId.toUpperCase() as DEX_ID
|
|
81
89
|
const poolSet = new Set(pools.map(p => p.pool_address))
|
|
82
|
-
subscribe_pool_account_update(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
subscribe_pool_account_update(
|
|
91
|
+
dex,
|
|
92
|
+
pools,
|
|
93
|
+
(data) => {
|
|
94
|
+
try {
|
|
95
|
+
if (!poolSet.has(data.pool_address)) return
|
|
96
|
+
handler(toSolPoolEvent(data))
|
|
97
|
+
} catch (e) {
|
|
98
|
+
log_warn(`[SolanaChainOps] bad pool account update`, data)
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
onSwapVerify
|
|
102
|
+
? (sv) => {
|
|
103
|
+
try {
|
|
104
|
+
if (!poolSet.has(sv.pool_address)) return
|
|
105
|
+
onSwapVerify(sv)
|
|
106
|
+
} catch (e) {
|
|
107
|
+
log_warn(`[SolanaChainOps] bad swap-verify event`, sv)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
: undefined,
|
|
111
|
+
)
|
|
90
112
|
}
|
|
91
113
|
}
|
package/src/quote/pool_event.ts
CHANGED
|
@@ -18,8 +18,9 @@ import { SolanaPoolAccountUpdateEventData } from '../types'
|
|
|
18
18
|
* AMM 聚合快照取 pool+vaultA+vaultB 三账户的 max writeVersion。
|
|
19
19
|
* (writeVersion 缺省时基类回退"无序号无条件推",对齐 SUI 缺 txIndex 行为,防御性兜底。)
|
|
20
20
|
*
|
|
21
|
-
* verify(swap amount
|
|
22
|
-
*
|
|
21
|
+
* verify(swap amount 对账)走**独立的 swap-verify 通道**(不在账户快照里):stream-quote 从真实交易
|
|
22
|
+
* pre/postTokenBalances 解码订阅池子 vault Δ → WS push(SolanaSwapVerifyEventData)→ 询价进程 checkSwap。
|
|
23
|
+
* 故 SolPoolEvent 仍只承载状态快照,不带 swap amount(详见 abstract_dex_quote.handleSwapVerify)。
|
|
23
24
|
*/
|
|
24
25
|
export interface SolPoolEvent {
|
|
25
26
|
pool_address: string
|
|
@@ -62,10 +62,16 @@ export async function unregisterPoolSubscriptions(appConfig: AppConfig, quote_ap
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* 询价进程启动后调用一次:确保 helius provider + 注册全部池子 + 装退出时自动 unregister。
|
|
65
|
-
* quote_app_id
|
|
65
|
+
* quote_app_id **必须 = PM2 进程名**(pm2 注入 process.env.name),与 trade-mgt register/reconcile 用的 app_id 一致。
|
|
66
|
+
* 否则 trade-mgt 的 60s reconcile 按 PM2 进程名核对,会把本进程自注册的订阅当"孤儿"注销 → 订阅消失、收不到事件。
|
|
67
|
+
* **取不到则 fail-loud(绝不兜底)**:塌成 'sol-quote' 这类 PM2 里不存在的名字 = 必被 reconcile 清掉,等于没注册还掩盖问题。
|
|
66
68
|
*/
|
|
67
69
|
export async function attachPoolSubscriptionLifecycle(appConfig: AppConfig, pools: StandardPoolInfoType[]): Promise<void> {
|
|
68
|
-
const quote_app_id = String(appConfig.env_args.app_name || '')
|
|
70
|
+
const quote_app_id = process.env.name || String(appConfig.env_args.app_name || '')
|
|
71
|
+
if (!quote_app_id) {
|
|
72
|
+
throw new Error('[pool/subscriptions] 无法确定 quote_app_id:process.env.name(PM2 进程名) 与 APP_NAME 均为空。'
|
|
73
|
+
+ '询价进程必须由 PM2/trade-mgt 以确定进程名启动,否则订阅会被 reconcile 当孤儿清除。')
|
|
74
|
+
}
|
|
69
75
|
await registerPoolSubscriptions(appConfig, pools, quote_app_id)
|
|
70
76
|
|
|
71
77
|
let cleaned = false
|
|
@@ -119,6 +119,20 @@ function pickMatchingTier(tiers: QuoteTier[], actualAmountIn: number): TierMatch
|
|
|
119
119
|
return { tier: top, mode: 'extrapolated_high', lo_pct: top.pct, hi_pct: null };
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* 把含费报价价剥成 fee-exclusive(曲线价),与"费路由出池"DEX(pumpswap)的 vault Δ exec 同口径。
|
|
124
|
+
*
|
|
125
|
+
* 关键:费总是把 ask/bid **向两侧各撑开** r(曲线价 M 在中间),方向**不对称**:
|
|
126
|
+
* - ask(买基础币) = M / (1 − r) → ask_excl = ask × (1 − r)
|
|
127
|
+
* - bid(卖基础币) = M × (1 − r) → bid_excl = bid / (1 − r) ← 是除,不是乘!
|
|
128
|
+
* 不论费记输入侧还是输出侧(pumpswap 实测两种都有),上式都成立(逐项核对见 session memory)。
|
|
129
|
+
* ask/bid 同为 quote/base 口径(calculateStandardPrice),仅方向决定撑开符号。
|
|
130
|
+
*/
|
|
131
|
+
function stripFeeExclusive(refPrice: number, isBuy: boolean, r: number): number {
|
|
132
|
+
if (!(r > 0) || r >= 1) return refPrice;
|
|
133
|
+
return isBuy ? refPrice * (1 - r) : refPrice / (1 - r);
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
export interface CheckSwapParams {
|
|
123
137
|
poolAddress: string;
|
|
124
138
|
poolName: string;
|
|
@@ -142,6 +156,19 @@ export interface CheckSwapParams {
|
|
|
142
156
|
* V4 caller 必须传 true,否则 side 会被算反(参见 tx 0xea45784b... / 0xdae0faa7... 验证)。
|
|
143
157
|
*/
|
|
144
158
|
swapperDeltaConvention?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* fee-exclusive 对账口径(Solana pool-side vault Δ 用)。
|
|
161
|
+
*
|
|
162
|
+
* 背景:exec_price 由 **pool vault Δ** 算(Δquote/Δbase)。vault Δ 是否含费**因 DEX 而异**:
|
|
163
|
+
* - 费留池内(Raydium/Orca/Meteora 等):vault Δ 含费 → 与"含费 tier 价"同口径 → 不剥(false,默认)。
|
|
164
|
+
* - 费路由出池(pumpswap protocol/creator fee 进独立账户):vault Δ 不含费 → 须把参考价剥成 fee-exclusive
|
|
165
|
+
* 才与 vault Δ 同口径(否则系统性偏差≈费率)。
|
|
166
|
+
* 为 true 时:refPrice ×= (amount_in − fee)/amount_in(tier 模式,用该档自身的含费/费额);
|
|
167
|
+
* scalar 兜底用 (1 − feeRateBps/1e4) 一阶近似。
|
|
168
|
+
*/
|
|
169
|
+
feeExclusiveExec?: boolean;
|
|
170
|
+
/** 池子费率(bps);feeExclusiveExec 且 scalar 兜底(无 tier)时用作一阶剥费因子。 */
|
|
171
|
+
feeRateBps?: number;
|
|
145
172
|
}
|
|
146
173
|
|
|
147
174
|
// ========== 主类 ==========
|
|
@@ -276,6 +303,11 @@ export class QuotePriceVerify {
|
|
|
276
303
|
const match = pickMatchingTier(tiers, swapData.inputAmountUi);
|
|
277
304
|
if (!match) continue;
|
|
278
305
|
refPrice = match.tier.price;
|
|
306
|
+
// fee-exclusive 对账:把含费 tier 价剥成 fee-exclusive,与 fee-out-of-vault DEX(pumpswap)的 vault Δ 同口径。
|
|
307
|
+
// r = 该档费率(fee/amount_in,amount_in 含费)。
|
|
308
|
+
if (params.feeExclusiveExec && match.tier.amount_in > 0) {
|
|
309
|
+
refPrice = stripFeeExclusive(refPrice, isBuy, match.tier.fee / match.tier.amount_in);
|
|
310
|
+
}
|
|
279
311
|
tierInfo = {
|
|
280
312
|
matched_pct: parseFloat(match.tier.pct.toFixed(4)),
|
|
281
313
|
mode: match.mode,
|
|
@@ -286,6 +318,10 @@ export class QuotePriceVerify {
|
|
|
286
318
|
} else {
|
|
287
319
|
// scalar fallback:旧 DEX 包未传 tiers 时使用
|
|
288
320
|
refPrice = isBuy ? cached.askPrice : cached.bidPrice;
|
|
321
|
+
// scalar 无 tier 费额 → 用池子费率一阶剥费(feeExclusiveExec 时)
|
|
322
|
+
if (params.feeExclusiveExec && params.feeRateBps && params.feeRateBps > 0) {
|
|
323
|
+
refPrice = stripFeeExclusive(refPrice, isBuy, params.feeRateBps / 10000);
|
|
324
|
+
}
|
|
289
325
|
}
|
|
290
326
|
|
|
291
327
|
if (refPrice <= 0) continue;
|
package/src/types/index.ts
CHANGED
|
@@ -34,6 +34,38 @@ export interface SolanaAccountUpdateEvent {
|
|
|
34
34
|
accountData?: Buffer | string; // 账户原始数据
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* 链上 swap 的 **pool-side vault Δ** 事件(verify 报价偏差用)。
|
|
39
|
+
*
|
|
40
|
+
* 由 stream-quote 从真实交易的 pre/postTokenBalances 解码订阅池子 vaultA/vaultB 的余额差,
|
|
41
|
+
* 经 WS push(与账户快照同通道,按 dex_id_pool_name_pool_address 路由)推给询价进程,
|
|
42
|
+
* 询价进程用它对账缓存报价(exec_price = |Δquote/Δbase|,fee-exclusive 口径,见 quote_price_verify)。
|
|
43
|
+
*
|
|
44
|
+
* 与 SolanaPoolAccountUpdateEventData 的区别:
|
|
45
|
+
* - 后者是**账户最新快照**(base64,用于刷新本地状态算价);
|
|
46
|
+
* - 本结构是**一笔 swap 的成交量**(vault Δ,用于验证报价准不准)。
|
|
47
|
+
* - 用 `kind:'swap_verify'` 判别(账户快照消息无 kind 字段)。
|
|
48
|
+
*/
|
|
49
|
+
export interface SolanaSwapVerifyEventData {
|
|
50
|
+
kind: 'swap_verify'
|
|
51
|
+
provider_id: string
|
|
52
|
+
event_time: number // stream 本地收到 tx 的 ms(新鲜度/延迟用)
|
|
53
|
+
dex_id: string
|
|
54
|
+
pool_address: string
|
|
55
|
+
pool_name: string
|
|
56
|
+
slot: number // swap 所在 slot(= verify 的 swap_block)
|
|
57
|
+
tx_index: number // 块内交易序号
|
|
58
|
+
tx_hash: string
|
|
59
|
+
block_time: number // 链上出块时间 ms;0=未知,由消费端处理
|
|
60
|
+
/** vaultA(base/token0) 余额 Δ;pool-side signed(>0=流入池子);raw(含 decimals) */
|
|
61
|
+
amount0: string
|
|
62
|
+
/** vaultB(quote/token1) 余额 Δ;pool-side signed */
|
|
63
|
+
amount1: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** swap-verify 事件总线名(stream-quote 内 appConfig.emit → ws_push_server 路由)。 */
|
|
67
|
+
export const EVENT_POOL_SWAP_VERIFY = 'EVENT_POOL_SWAP_VERIFY'
|
|
68
|
+
|
|
37
69
|
/**
|
|
38
70
|
* Solana 区块元数据更新事件
|
|
39
71
|
*/
|