diff --git a/src/lib/alarms/constants.ts b/src/lib/alarms/constants.ts new file mode 100644 index 0000000..3c60be4 --- /dev/null +++ b/src/lib/alarms/constants.ts @@ -0,0 +1,2 @@ +// Max trades to fetch from GetTradeHistory API in background trade telemetry +export const MAX_TRADE_HISTORY_FETCH = 250; diff --git a/src/lib/alarms/error_report.ts b/src/lib/alarms/error_report.ts new file mode 100644 index 0000000..7af97f5 --- /dev/null +++ b/src/lib/alarms/error_report.ts @@ -0,0 +1,14 @@ +import {environment} from '../../environment'; + +export async function reportTradeError(tradeId: string, error: string): Promise { + 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); + } +} diff --git a/src/lib/alarms/notary.ts b/src/lib/alarms/notary.ts new file mode 100644 index 0000000..c0e7c4d --- /dev/null +++ b/src/lib/alarms/notary.ts @@ -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 { + 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 { + 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}`); + } +} diff --git a/src/lib/alarms/rollback.ts b/src/lib/alarms/rollback.ts index 465f11a..167f63c 100644 --- a/src/lib/alarms/rollback.ts +++ b/src/lib/alarms/rollback.ts @@ -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) @@ -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) { diff --git a/src/lib/alarms/trade_history.ts b/src/lib/alarms/trade_history.ts index ccd797f..563a584 100644 --- a/src/lib/alarms/trade_history.ts +++ b/src/lib/alarms/trade_history.ts @@ -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( @@ -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}; diff --git a/src/lib/bridge/handlers/fetch_notary_meta.ts b/src/lib/bridge/handlers/fetch_notary_meta.ts new file mode 100644 index 0000000..ebf8ada --- /dev/null +++ b/src/lib/bridge/handlers/fetch_notary_meta.ts @@ -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( + 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; + } +); diff --git a/src/lib/bridge/handlers/fetch_notary_token.ts b/src/lib/bridge/handlers/fetch_notary_token.ts new file mode 100644 index 0000000..9cc4d74 --- /dev/null +++ b/src/lib/bridge/handlers/fetch_notary_token.ts @@ -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( + 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; + } +); diff --git a/src/lib/bridge/handlers/handlers.ts b/src/lib/bridge/handlers/handlers.ts index a208286..ac1b2f0 100644 --- a/src/lib/bridge/handlers/handlers.ts +++ b/src/lib/bridge/handlers/handlers.ts @@ -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} = { [RequestType.EXECUTE_SCRIPT_ON_PAGE]: ExecuteScriptOnPage, @@ -70,4 +72,6 @@ export const HANDLERS_MAP: {[key in RequestType]: RequestHandler} = { [RequestType.FETCH_TRADE_HISTORY]: FetchTradeHistory, [RequestType.FETCH_SLIM_TRADES]: FetchSlimTrades, [RequestType.NOTARY_PROVE]: NotaryProve, + [RequestType.FETCH_NOTARY_META]: FetchNotaryMeta, + [RequestType.FETCH_NOTARY_TOKEN]: FetchNotaryToken, }; diff --git a/src/lib/bridge/handlers/types.ts b/src/lib/bridge/handlers/types.ts index 18c9f7e..033fc91 100644 --- a/src/lib/bridge/handlers/types.ts +++ b/src/lib/bridge/handlers/types.ts @@ -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, }