Skip to content

Commit 261d2fe

Browse files
authored
Merge pull request #337 from csfloat/feature/rollback-telemetry
Adds Rollback Telemetry
2 parents 0ae0ea8 + cb48ef8 commit 261d2fe

File tree

10 files changed

+175
-10
lines changed

10 files changed

+175
-10
lines changed

src/lib/alarms/csfloat_trade_pings.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {AccessToken, getAccessToken} from './access_token';
88
import {gStore} from '../storage/store';
99
import {StorageKey} from '../storage/keys';
1010
import {reportBlockedBuyers} from './blocked_users';
11+
import {TradeHistoryStatus} from '../bridge/handlers/trade_history_status';
12+
import {pingRollbackTrades} from './rollback';
1113

1214
export const PING_CSFLOAT_TRADE_STATUS_ALARM_NAME = 'ping_csfloat_trade_status_alarm';
1315

@@ -28,7 +30,7 @@ export async function pingTradeStatus(expectedSteamID?: string) {
2830

2931
let pendingTrades: Trade[];
3032
try {
31-
const resp = await FetchPendingTrades.handleRequest({limit: 1000}, {});
33+
const resp = await FetchPendingTrades.handleRequest({limit: 3000, exclude_wait_for_settlement: true}, {});
3234
pendingTrades = resp.trades;
3335
} catch (e) {
3436
console.error(e);
@@ -70,6 +72,7 @@ interface UpdateErrors {
7072
history_error?: string;
7173
trade_offer_error?: string;
7274
blocked_buyers_error?: string;
75+
rollback_trades_error?: string;
7376
}
7477

7578
async function pingUpdates(pendingTrades: Trade[]): Promise<UpdateErrors> {
@@ -88,8 +91,9 @@ async function pingUpdates(pendingTrades: Trade[]): Promise<UpdateErrors> {
8891
console.error(`failed to cancel unconfirmed trade offers`, e);
8992
}
9093

94+
let tradeHistory: TradeHistoryStatus[] = [];
9195
try {
92-
await pingTradeHistory(pendingTrades);
96+
tradeHistory = await pingTradeHistory(pendingTrades);
9397
} catch (e) {
9498
console.error('failed to ping trade history', e);
9599
errors.history_error = (e as any).toString();
@@ -108,5 +112,12 @@ async function pingUpdates(pendingTrades: Trade[]): Promise<UpdateErrors> {
108112
console.error('failed to ping cancel ping trade offers', e);
109113
}
110114

115+
try {
116+
await pingRollbackTrades(pendingTrades, tradeHistory);
117+
} catch (e) {
118+
console.error('failed to ping rollback trades', e);
119+
errors.rollback_trades_error = (e as any).toString();
120+
}
121+
111122
return errors;
112123
}

src/lib/alarms/rollback.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {Trade, TradeState} from '../types/float_market';
2+
import {TradeHistoryStatus} from '../bridge/handlers/trade_history_status';
3+
import {PingRollbackTrade} from '../bridge/handlers/ping_rollback_trade';
4+
import {TradeStatus} from '../types/steam_constants';
5+
6+
export async function pingRollbackTrades(pendingTrades: Trade[], tradeHistory: TradeHistoryStatus[]) {
7+
if (!pendingTrades || pendingTrades.length === 0) {
8+
return;
9+
}
10+
11+
if (!tradeHistory || tradeHistory.length === 0) {
12+
return;
13+
}
14+
15+
for (const trade of tradeHistory) {
16+
// Status 12 corresponds to a rollback via trade protection (undocumented)
17+
// The original trade gets updated to this status once a rollback occurs
18+
// (and creates a new complete trade for sending the items back)
19+
if (trade.status !== TradeStatus.TradeProtectionRollback) {
20+
continue;
21+
}
22+
23+
const received_ids = trade.received_assets.map((e) => e.asset_id);
24+
const given_ids = trade.given_assets.map((e) => e.asset_id);
25+
const all_ids = [...received_ids, ...given_ids];
26+
27+
// Does it correspond to an active CSFloat sale?
28+
const csfloatTrade = pendingTrades.find(
29+
(e) => e.state === TradeState.PENDING && all_ids.includes(e.contract.item.asset_id)
30+
);
31+
if (!csfloatTrade) {
32+
continue;
33+
}
34+
35+
// try to find the rollback trade id
36+
const rollbackTrade = tradeHistory.find((e) => e.rollback_trade === trade.trade_id);
37+
38+
// Pinging the first asset in a trade will cancel all the items in the trade server-side
39+
try {
40+
await PingRollbackTrade.handleRequest(
41+
{trade_id: csfloatTrade?.id, rollback_trade_id: rollbackTrade?.trade_id},
42+
{}
43+
);
44+
} catch (e) {
45+
console.error(`failed to send rollback ping for csfloat trade ${csfloatTrade.id}`, e);
46+
}
47+
}
48+
}

src/lib/alarms/trade_history.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {Trade} from '../types/float_market';
22
import {TradeHistoryStatus, TradeHistoryType} from '../bridge/handlers/trade_history_status';
3-
import {AppId} from '../types/steam_constants';
3+
import {AppId, TradeStatus} from '../types/steam_constants';
44
import {clearAccessTokenFromStorage, getAccessToken} from './access_token';
55

6-
export async function pingTradeHistory(pendingTrades: Trade[]) {
6+
export async function pingTradeHistory(pendingTrades: Trade[]): Promise<TradeHistoryStatus[]> {
77
const {history, type} = await getTradeHistory();
88

99
// premature optimization in case it's 100 trades
@@ -25,15 +25,17 @@ export async function pingTradeHistory(pendingTrades: Trade[]) {
2525
});
2626

2727
if (historyForCSFloat.length === 0) {
28-
return;
28+
return history;
2929
}
3030

3131
await TradeHistoryStatus.handleRequest({history: historyForCSFloat, type}, {});
32+
33+
return history;
3234
}
3335

3436
async function getTradeHistory(): Promise<{history: TradeHistoryStatus[]; type: TradeHistoryType}> {
3537
try {
36-
const history = await getTradeHistoryFromAPI();
38+
const history = await getTradeHistoryFromAPI(250);
3739
if (history.length > 0) {
3840
// Hedge in case this endpoint gets killed, only return if there are results, fallback to HTML parser
3941
return {history, type: TradeHistoryType.API};
@@ -63,16 +65,18 @@ interface TradeHistoryAPIResponse {
6365
assets_given?: HistoryAsset[];
6466
assets_received?: HistoryAsset[];
6567
time_escrow_end?: string;
68+
time_settlement?: number;
69+
rollback_trade?: string;
6670
}[];
6771
};
6872
}
6973

70-
async function getTradeHistoryFromAPI(): Promise<TradeHistoryStatus[]> {
74+
export async function getTradeHistoryFromAPI(maxTrades: number): Promise<TradeHistoryStatus[]> {
7175
const access = await getAccessToken();
7276

7377
// This only works if they have granted permission for https://api.steampowered.com
7478
const resp = await fetch(
75-
`https://api.steampowered.com/IEconService/GetTradeHistory/v1/?access_token=${access.token}&max_trades=200`,
79+
`https://api.steampowered.com/IEconService/GetTradeHistory/v1/?access_token=${access.token}&max_trades=${maxTrades}`,
7680
{
7781
credentials: 'include',
7882
}
@@ -84,7 +88,7 @@ async function getTradeHistoryFromAPI(): Promise<TradeHistoryStatus[]> {
8488

8589
const data = (await resp.json()) as TradeHistoryAPIResponse;
8690
return (data.response?.trades || [])
87-
.filter((e) => e.status === 3) // Ensure we only count _complete_ trades (k_ETradeStatus_Complete)
91+
.filter((e) => e.status === TradeStatus.Complete || e.status === TradeStatus.TradeProtectionRollback) // Ensure we only count _complete_ trades (k_ETradeStatus_Complete) or rolled back (for reporting)
8892
.filter((e) => !e.time_escrow_end || new Date(parseInt(e.time_escrow_end) * 1000).getTime() < Date.now())
8993
.map((e) => {
9094
return {
@@ -99,6 +103,10 @@ async function getTradeHistoryFromAPI(): Promise<TradeHistoryStatus[]> {
99103
.map((e) => {
100104
return {asset_id: e.assetid, new_asset_id: e.new_assetid};
101105
}),
106+
trade_id: e.tradeid,
107+
time_settlement: e.time_settlement,
108+
status: e.status,
109+
rollback_trade: e.rollback_trade,
102110
} as TradeHistoryStatus;
103111
})
104112
.filter((e) => {
@@ -132,6 +140,10 @@ function parseTradeHistoryHTML(body: string): TradeHistoryStatus[] {
132140
other_party_url: `https://steamcommunity.com/${e[1]}`,
133141
received_assets: [],
134142
given_assets: [],
143+
trade_id: '',
144+
time_settlement: 0,
145+
status: 0,
146+
rollback_trade: '',
135147
} as TradeHistoryStatus;
136148
});
137149

src/lib/bridge/handlers/fetch_pending_trades.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {environment} from '../../../environment';
66
export interface FetchPendingTradesRequest {
77
state?: string;
88
limit?: number;
9+
exclude_wait_for_settlement?: boolean;
910
}
1011

1112
export interface FetchPendingTradesResponse {
@@ -19,7 +20,7 @@ export const FetchPendingTrades = new SimpleHandler<FetchPendingTradesRequest, F
1920
const state = req.state ? req.state : 'pending';
2021
const limit = req.limit ? req.limit : 100;
2122
const resp = await fetch(
22-
`${environment.csfloat_base_api_url}/v1/me/trades?state=${state}&limit=${limit}&page=0`,
23+
`${environment.csfloat_base_api_url}/v1/me/trades?state=${state}&limit=${limit}&exclude_wait_for_settlement=${req.exclude_wait_for_settlement || false}&page=0`,
2324
{
2425
credentials: 'include',
2526
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {SimpleHandler} from './main';
2+
import {RequestType} from './types';
3+
import {TradeHistoryStatus} from './trade_history_status';
4+
import {getTradeHistoryFromAPI} from '../../alarms/trade_history';
5+
6+
export interface FetchTradeHistoryRequest {
7+
max_trades: number;
8+
}
9+
10+
export interface FetchTradeHistoryResponse {
11+
trades: TradeHistoryStatus[];
12+
}
13+
14+
export const FetchTradeHistory = new SimpleHandler<FetchTradeHistoryRequest, FetchTradeHistoryResponse>(
15+
RequestType.FETCH_TRADE_HISTORY,
16+
async (req) => {
17+
const trades = await getTradeHistoryFromAPI(req.max_trades);
18+
return {
19+
trades,
20+
};
21+
}
22+
);

src/lib/bridge/handlers/handlers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {FetchBluegem} from './fetch_bluegem';
3030
import {ListItem} from './list_item';
3131
import {FetchRecommendedPrice} from './fetch_recommended_price';
3232
import {FetchCSFloatMe} from './fetch_csfloat_me';
33+
import {PingRollbackTrade} from './ping_rollback_trade';
34+
import {FetchTradeHistory} from './fetch_trade_history';
3335

3436
export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
3537
[RequestType.EXECUTE_SCRIPT_ON_PAGE]: ExecuteScriptOnPage,
@@ -62,4 +64,6 @@ export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
6264
[RequestType.LIST_ITEM]: ListItem,
6365
[RequestType.FETCH_RECOMMENDED_PRICE]: FetchRecommendedPrice,
6466
[RequestType.FETCH_CSFLOAT_ME]: FetchCSFloatMe,
67+
[RequestType.PING_ROLLBACK_TRADE]: PingRollbackTrade,
68+
[RequestType.FETCH_TRADE_HISTORY]: FetchTradeHistory,
6569
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {SimpleHandler} from './main';
2+
import {RequestType} from './types';
3+
import {environment} from '../../../environment';
4+
import {Trade} from '../../types/float_market';
5+
6+
export enum RollbackOrigin {
7+
MANUAL_USER = 0,
8+
EXTENSION = 1,
9+
}
10+
11+
export interface PingRollbackTradeRequest {
12+
trade_id: string;
13+
rollback_trade_id?: string;
14+
}
15+
16+
export interface PingRollbackTradeResponse {
17+
trade: Trade;
18+
}
19+
20+
export const PingRollbackTrade = new SimpleHandler<PingRollbackTradeRequest, PingRollbackTradeResponse>(
21+
RequestType.PING_ROLLBACK_TRADE,
22+
async (req) => {
23+
const resp = await fetch(`${environment.csfloat_base_api_url}/v1/trades/${req.trade_id}/rollback`, {
24+
credentials: 'include',
25+
method: 'POST',
26+
headers: {
27+
'Content-Type': 'application/json',
28+
},
29+
body: JSON.stringify({
30+
rollback_trade_id: req.rollback_trade_id,
31+
origin: RollbackOrigin.EXTENSION,
32+
}),
33+
});
34+
35+
if (resp.status !== 200) {
36+
throw new Error('invalid status');
37+
}
38+
39+
const trade = (await resp.json()) as Trade;
40+
return {
41+
trade,
42+
};
43+
}
44+
);

src/lib/bridge/handlers/trade_history_status.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ export interface TradeHistoryAsset {
88
}
99

1010
export interface TradeHistoryStatus {
11+
trade_id: string;
12+
status: number;
1113
other_party_url: string;
1214
received_assets: TradeHistoryAsset[];
1315
given_assets: TradeHistoryAsset[];
16+
time_settlement?: number;
17+
rollback_trade?: string;
1418
}
1519

1620
export enum TradeHistoryType {

src/lib/bridge/handlers/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ export enum RequestType {
2929
LIST_ITEM = 27,
3030
FETCH_RECOMMENDED_PRICE = 28,
3131
FETCH_CSFLOAT_ME = 29,
32+
PING_ROLLBACK_TRADE = 30,
33+
FETCH_TRADE_HISTORY = 31,
3234
}

src/lib/types/steam_constants.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,20 @@ export enum TradeOfferState {
2525
CancelledBySecondFactor = 10,
2626
InEscrow = 11,
2727
}
28+
29+
// https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService
30+
export enum TradeStatus {
31+
Init = 0,
32+
PreCommitted = 1,
33+
Committed = 2,
34+
Complete = 3,
35+
Failed = 4,
36+
PartialSupportRollback = 5,
37+
FullSupportRollback = 6,
38+
SupportRollbackSelective = 7,
39+
RollbackFailed = 8,
40+
RollbackAbandoned = 9,
41+
InEscrow = 10,
42+
EscrowRollback = 11,
43+
TradeProtectionRollback = 12, // Undocumented
44+
}

0 commit comments

Comments
 (0)