Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib/alarms/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Max trades to fetch from GetTradeHistory API in background trade telemetry
export const MAX_TRADE_HISTORY_FETCH = 250;
14 changes: 14 additions & 0 deletions src/lib/alarms/error_report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {environment} from '../../environment';

export async function reportTradeError(tradeId: string, error: string): Promise<void> {
try {
await fetch(`${environment.csfloat_base_api_url}/v1/trades/${tradeId}/report-error`, {
credentials: 'include',
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({error}),
});
} catch (e) {
console.error(`failed to report trade error for ${tradeId}`, e);
}
}
66 changes: 66 additions & 0 deletions src/lib/alarms/notary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {TradeHistoryStatus} from '../bridge/handlers/trade_history_status';
import {NotaryProve} from '../bridge/handlers/notary_prove';
import {FetchNotaryToken} from '../bridge/handlers/fetch_notary_token';
import {FetchNotaryMeta} from '../bridge/handlers/fetch_notary_meta';
import {ProofType, NotaryProveRequest} from '../notary/types';
import {MAX_TRADE_HISTORY_FETCH} from './constants';
import {isFirefox} from '../utils/detect';
import {environment} from '../../environment';

export async function isBackgroundNotaryRollbackEnabled(): Promise<boolean> {
if (isFirefox()) {
return false;
}

try {
const meta = await FetchNotaryMeta.handleRequest({}, {});
return meta.rollback?.background === true;
} catch (e) {
console.error('failed to fetch notary meta', e);
return false;
}
}

function buildProveRequest(trades: TradeHistoryStatus[]): NotaryProveRequest {
if (trades.length === 1) {
return {
type: ProofType.TRADE_HISTORY,
max_trades: 5,
start_after_time: trades[0].time_init,
navigating_back: true,
};
}

// Multiple trades: start from the oldest and fetch enough to guarantee coverage
const oldestTimeInit = Math.min(...trades.map((t) => t.time_init));

return {
type: ProofType.TRADE_HISTORY,
max_trades: MAX_TRADE_HISTORY_FETCH,
start_after_time: oldestTimeInit,
navigating_back: true,
};
}

export async function proveTradesInBackground(trades: TradeHistoryStatus[]): Promise<void> {
if (trades.length === 0) {
return;
}

const notaryToken = await FetchNotaryToken.handleRequest({}, {});
const proveRequest = buildProveRequest(trades);
proveRequest.meta = {notary_token: notaryToken.token};

const result = await NotaryProve.handleRequest(proveRequest, {});

const resp = await fetch(`${environment.csfloat_base_api_url}/v1/trades/notary`, {
credentials: 'include',
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({payload: result.payload}),
});

if (resp.status !== 200) {
throw new Error(`failed to submit notary proof: ${resp.status}`);
}
}
51 changes: 41 additions & 10 deletions src/lib/alarms/rollback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import {SlimTrade, TradeState} from '../types/float_market';
import {TradeHistoryStatus} from '../bridge/handlers/trade_history_status';
import {PingRollbackTrade} from '../bridge/handlers/ping_rollback_trade';
import {TradeStatus} from '../types/steam_constants';
import {isBackgroundNotaryRollbackEnabled, proveTradesInBackground} from './notary';
import {reportTradeError} from './error_report';

export async function pingRollbackTrades(pendingTrades: SlimTrade[], tradeHistory: TradeHistoryStatus[]) {
if (!pendingTrades || pendingTrades.length === 0) {
return;
}
interface RollbackTradeInfo {
steamTrade: TradeHistoryStatus;
csfloatTrade: SlimTrade;
rollbackTrade?: TradeHistoryStatus;
}

if (!tradeHistory || tradeHistory.length === 0) {
return;
}
function findRollbackTrades(pendingTrades: SlimTrade[], tradeHistory: TradeHistoryStatus[]): RollbackTradeInfo[] {
const results: RollbackTradeInfo[] = [];

for (const trade of tradeHistory) {
// Status 12 corresponds to a rollback via trade protection (undocumented)
Expand All @@ -32,13 +34,42 @@ export async function pingRollbackTrades(pendingTrades: SlimTrade[], tradeHistor
continue;
}

// try to find the rollback trade id
const rollbackTrade = tradeHistory.find((e) => e.rollback_trade === trade.trade_id);
results.push({steamTrade: trade, csfloatTrade, rollbackTrade});
}

return results;
}

export async function pingRollbackTrades(pendingTrades: SlimTrade[], tradeHistory: TradeHistoryStatus[]) {
if (!pendingTrades?.length || !tradeHistory?.length) {
return;
}

const rollbackTrades = findRollbackTrades(pendingTrades, tradeHistory);
if (rollbackTrades.length === 0) {
return;
}

if (await isBackgroundNotaryRollbackEnabled()) {
try {
await proveTradesInBackground(rollbackTrades.map((r) => r.steamTrade));
console.log(`proved ${rollbackTrades.length} rollback trade(s) via notary`);
return;
} catch (e) {
console.error('notary proving failed, falling back to legacy ping', e);
reportTradeError(rollbackTrades[0].csfloatTrade.id, `background extension notary failed: ${e}`);
}
}

await pingRollbackTradesLegacy(rollbackTrades);
}

// Pinging the first asset in a trade will cancel all the items in the trade server-side
async function pingRollbackTradesLegacy(rollbackTrades: RollbackTradeInfo[]) {
for (const {csfloatTrade, rollbackTrade} of rollbackTrades) {
try {
await PingRollbackTrade.handleRequest(
{trade_id: csfloatTrade?.id, rollback_trade_id: rollbackTrade?.trade_id},
{trade_id: csfloatTrade.id, rollback_trade_id: rollbackTrade?.trade_id},
{}
);
} catch (e) {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/alarms/trade_history.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {SlimTrade} from '../types/float_market';
import {TradeHistoryStatus, TradeHistoryType} from '../bridge/handlers/trade_history_status';
import {AppId, TradeOfferState, TradeStatus} from '../types/steam_constants';
import {MAX_TRADE_HISTORY_FETCH} from './constants';
import {clearAccessTokenFromStorage, getAccessToken} from './access_token';

export async function pingTradeHistory(
Expand Down Expand Up @@ -49,7 +50,7 @@ export async function pingTradeHistory(

async function getTradeHistory(): Promise<{history: TradeHistoryStatus[]; type: TradeHistoryType}> {
try {
const history = await getTradeHistoryFromAPI(250);
const history = await getTradeHistoryFromAPI(MAX_TRADE_HISTORY_FETCH);
if (history.length > 0) {
// Hedge in case this endpoint gets killed, only return if there are results, fallback to HTML parser
return {history, type: TradeHistoryType.API};
Expand Down
32 changes: 32 additions & 0 deletions src/lib/bridge/handlers/fetch_notary_meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {SimpleHandler} from './main';
import {RequestType} from './types';
import {environment} from '../../../environment';

interface NotarySetting {
enabled: boolean;
background: boolean;
}

export interface NotaryMeta {
rollback: NotarySetting;
accepted: NotarySetting;
}

export interface FetchNotaryMetaRequest {}

export interface FetchNotaryMetaResponse extends NotaryMeta {}

export const FetchNotaryMeta = new SimpleHandler<FetchNotaryMetaRequest, FetchNotaryMetaResponse>(
RequestType.FETCH_NOTARY_META,
async () => {
const resp = await fetch(`${environment.csfloat_base_api_url}/v1/meta/notary`, {
credentials: 'include',
});

if (resp.status !== 200) {
throw new Error('failed to fetch notary meta');
}

return (await resp.json()) as NotaryMeta;
}
);
28 changes: 28 additions & 0 deletions src/lib/bridge/handlers/fetch_notary_token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {SimpleHandler} from './main';
import {RequestType} from './types';
import {environment} from '../../../environment';

export interface NotaryToken {
token: string;
expires_at: string;
}

export interface FetchNotaryTokenRequest {}

export interface FetchNotaryTokenResponse extends NotaryToken {}

export const FetchNotaryToken = new SimpleHandler<FetchNotaryTokenRequest, FetchNotaryTokenResponse>(
RequestType.FETCH_NOTARY_TOKEN,
async () => {
const resp = await fetch(`${environment.csfloat_base_api_url}/v1/me/notary-token`, {
credentials: 'include',
method: 'POST',
});

if (resp.status !== 200) {
throw new Error('failed to fetch notary token');
}

return (await resp.json()) as NotaryToken;
}
);
4 changes: 4 additions & 0 deletions src/lib/bridge/handlers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {PingRollbackTrade} from './ping_rollback_trade';
import {FetchTradeHistory} from './fetch_trade_history';
import {FetchSlimTrades} from './fetch_slim_trades';
import {NotaryProve} from './notary_prove';
import {FetchNotaryMeta} from './fetch_notary_meta';
import {FetchNotaryToken} from './fetch_notary_token';

export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
[RequestType.EXECUTE_SCRIPT_ON_PAGE]: ExecuteScriptOnPage,
Expand Down Expand Up @@ -70,4 +72,6 @@ export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
[RequestType.FETCH_TRADE_HISTORY]: FetchTradeHistory,
[RequestType.FETCH_SLIM_TRADES]: FetchSlimTrades,
[RequestType.NOTARY_PROVE]: NotaryProve,
[RequestType.FETCH_NOTARY_META]: FetchNotaryMeta,
[RequestType.FETCH_NOTARY_TOKEN]: FetchNotaryToken,
};
2 changes: 2 additions & 0 deletions src/lib/bridge/handlers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ export enum RequestType {
FETCH_TRADE_HISTORY = 31,
FETCH_SLIM_TRADES = 32,
NOTARY_PROVE = 33,
FETCH_NOTARY_META = 34,
FETCH_NOTARY_TOKEN = 35,
}