Skip to content

Commit 9d99fb3

Browse files
authored
Merge pull request #368 from csfloat/feature/background-rollback-notary-proof
Implements Background Rollback Notary Proofs
2 parents 364b0ec + a4b267a commit 9d99fb3

9 files changed

Lines changed: 191 additions & 11 deletions

File tree

src/lib/alarms/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Max trades to fetch from GetTradeHistory API in background trade telemetry
2+
export const MAX_TRADE_HISTORY_FETCH = 250;

src/lib/alarms/error_report.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {environment} from '../../environment';
2+
3+
export async function reportTradeError(tradeId: string, error: string): Promise<void> {
4+
try {
5+
await fetch(`${environment.csfloat_base_api_url}/v1/trades/${tradeId}/report-error`, {
6+
credentials: 'include',
7+
method: 'POST',
8+
headers: {'Content-Type': 'application/json'},
9+
body: JSON.stringify({error}),
10+
});
11+
} catch (e) {
12+
console.error(`failed to report trade error for ${tradeId}`, e);
13+
}
14+
}

src/lib/alarms/notary.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {TradeHistoryStatus} from '../bridge/handlers/trade_history_status';
2+
import {NotaryProve} from '../bridge/handlers/notary_prove';
3+
import {FetchNotaryToken} from '../bridge/handlers/fetch_notary_token';
4+
import {FetchNotaryMeta} from '../bridge/handlers/fetch_notary_meta';
5+
import {ProofType, NotaryProveRequest} from '../notary/types';
6+
import {MAX_TRADE_HISTORY_FETCH} from './constants';
7+
import {isFirefox} from '../utils/detect';
8+
import {environment} from '../../environment';
9+
10+
export async function isBackgroundNotaryRollbackEnabled(): Promise<boolean> {
11+
if (isFirefox()) {
12+
return false;
13+
}
14+
15+
try {
16+
const meta = await FetchNotaryMeta.handleRequest({}, {});
17+
return meta.rollback?.background === true;
18+
} catch (e) {
19+
console.error('failed to fetch notary meta', e);
20+
return false;
21+
}
22+
}
23+
24+
function buildProveRequest(trades: TradeHistoryStatus[]): NotaryProveRequest {
25+
if (trades.length === 1) {
26+
return {
27+
type: ProofType.TRADE_HISTORY,
28+
max_trades: 5,
29+
start_after_time: trades[0].time_init,
30+
navigating_back: true,
31+
};
32+
}
33+
34+
// Multiple trades: start from the oldest and fetch enough to guarantee coverage
35+
const oldestTimeInit = Math.min(...trades.map((t) => t.time_init));
36+
37+
return {
38+
type: ProofType.TRADE_HISTORY,
39+
max_trades: MAX_TRADE_HISTORY_FETCH,
40+
start_after_time: oldestTimeInit,
41+
navigating_back: true,
42+
};
43+
}
44+
45+
export async function proveTradesInBackground(trades: TradeHistoryStatus[]): Promise<void> {
46+
if (trades.length === 0) {
47+
return;
48+
}
49+
50+
const notaryToken = await FetchNotaryToken.handleRequest({}, {});
51+
const proveRequest = buildProveRequest(trades);
52+
proveRequest.meta = {notary_token: notaryToken.token};
53+
54+
const result = await NotaryProve.handleRequest(proveRequest, {});
55+
56+
const resp = await fetch(`${environment.csfloat_base_api_url}/v1/trades/notary`, {
57+
credentials: 'include',
58+
method: 'POST',
59+
headers: {'Content-Type': 'application/json'},
60+
body: JSON.stringify({payload: result.payload}),
61+
});
62+
63+
if (resp.status !== 200) {
64+
throw new Error(`failed to submit notary proof: ${resp.status}`);
65+
}
66+
}

src/lib/alarms/rollback.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import {SlimTrade, TradeState} from '../types/float_market';
22
import {TradeHistoryStatus} from '../bridge/handlers/trade_history_status';
33
import {PingRollbackTrade} from '../bridge/handlers/ping_rollback_trade';
44
import {TradeStatus} from '../types/steam_constants';
5+
import {isBackgroundNotaryRollbackEnabled, proveTradesInBackground} from './notary';
6+
import {reportTradeError} from './error_report';
57

6-
export async function pingRollbackTrades(pendingTrades: SlimTrade[], tradeHistory: TradeHistoryStatus[]) {
7-
if (!pendingTrades || pendingTrades.length === 0) {
8-
return;
9-
}
8+
interface RollbackTradeInfo {
9+
steamTrade: TradeHistoryStatus;
10+
csfloatTrade: SlimTrade;
11+
rollbackTrade?: TradeHistoryStatus;
12+
}
1013

11-
if (!tradeHistory || tradeHistory.length === 0) {
12-
return;
13-
}
14+
function findRollbackTrades(pendingTrades: SlimTrade[], tradeHistory: TradeHistoryStatus[]): RollbackTradeInfo[] {
15+
const results: RollbackTradeInfo[] = [];
1416

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

35-
// try to find the rollback trade id
3637
const rollbackTrade = tradeHistory.find((e) => e.rollback_trade === trade.trade_id);
38+
results.push({steamTrade: trade, csfloatTrade, rollbackTrade});
39+
}
40+
41+
return results;
42+
}
43+
44+
export async function pingRollbackTrades(pendingTrades: SlimTrade[], tradeHistory: TradeHistoryStatus[]) {
45+
if (!pendingTrades?.length || !tradeHistory?.length) {
46+
return;
47+
}
48+
49+
const rollbackTrades = findRollbackTrades(pendingTrades, tradeHistory);
50+
if (rollbackTrades.length === 0) {
51+
return;
52+
}
53+
54+
if (await isBackgroundNotaryRollbackEnabled()) {
55+
try {
56+
await proveTradesInBackground(rollbackTrades.map((r) => r.steamTrade));
57+
console.log(`proved ${rollbackTrades.length} rollback trade(s) via notary`);
58+
return;
59+
} catch (e) {
60+
console.error('notary proving failed, falling back to legacy ping', e);
61+
reportTradeError(rollbackTrades[0].csfloatTrade.id, `background extension notary failed: ${e}`);
62+
}
63+
}
64+
65+
await pingRollbackTradesLegacy(rollbackTrades);
66+
}
3767

38-
// Pinging the first asset in a trade will cancel all the items in the trade server-side
68+
async function pingRollbackTradesLegacy(rollbackTrades: RollbackTradeInfo[]) {
69+
for (const {csfloatTrade, rollbackTrade} of rollbackTrades) {
3970
try {
4071
await PingRollbackTrade.handleRequest(
41-
{trade_id: csfloatTrade?.id, rollback_trade_id: rollbackTrade?.trade_id},
72+
{trade_id: csfloatTrade.id, rollback_trade_id: rollbackTrade?.trade_id},
4273
{}
4374
);
4475
} catch (e) {

src/lib/alarms/trade_history.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {SlimTrade} from '../types/float_market';
22
import {TradeHistoryStatus, TradeHistoryType} from '../bridge/handlers/trade_history_status';
33
import {AppId, TradeOfferState, TradeStatus} from '../types/steam_constants';
4+
import {MAX_TRADE_HISTORY_FETCH} from './constants';
45
import {clearAccessTokenFromStorage, getAccessToken} from './access_token';
56

67
export async function pingTradeHistory(
@@ -49,7 +50,7 @@ export async function pingTradeHistory(
4950

5051
async function getTradeHistory(): Promise<{history: TradeHistoryStatus[]; type: TradeHistoryType}> {
5152
try {
52-
const history = await getTradeHistoryFromAPI(250);
53+
const history = await getTradeHistoryFromAPI(MAX_TRADE_HISTORY_FETCH);
5354
if (history.length > 0) {
5455
// Hedge in case this endpoint gets killed, only return if there are results, fallback to HTML parser
5556
return {history, type: TradeHistoryType.API};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {SimpleHandler} from './main';
2+
import {RequestType} from './types';
3+
import {environment} from '../../../environment';
4+
5+
interface NotarySetting {
6+
enabled: boolean;
7+
background: boolean;
8+
}
9+
10+
export interface NotaryMeta {
11+
rollback: NotarySetting;
12+
accepted: NotarySetting;
13+
}
14+
15+
export interface FetchNotaryMetaRequest {}
16+
17+
export interface FetchNotaryMetaResponse extends NotaryMeta {}
18+
19+
export const FetchNotaryMeta = new SimpleHandler<FetchNotaryMetaRequest, FetchNotaryMetaResponse>(
20+
RequestType.FETCH_NOTARY_META,
21+
async () => {
22+
const resp = await fetch(`${environment.csfloat_base_api_url}/v1/meta/notary`, {
23+
credentials: 'include',
24+
});
25+
26+
if (resp.status !== 200) {
27+
throw new Error('failed to fetch notary meta');
28+
}
29+
30+
return (await resp.json()) as NotaryMeta;
31+
}
32+
);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {SimpleHandler} from './main';
2+
import {RequestType} from './types';
3+
import {environment} from '../../../environment';
4+
5+
export interface NotaryToken {
6+
token: string;
7+
expires_at: string;
8+
}
9+
10+
export interface FetchNotaryTokenRequest {}
11+
12+
export interface FetchNotaryTokenResponse extends NotaryToken {}
13+
14+
export const FetchNotaryToken = new SimpleHandler<FetchNotaryTokenRequest, FetchNotaryTokenResponse>(
15+
RequestType.FETCH_NOTARY_TOKEN,
16+
async () => {
17+
const resp = await fetch(`${environment.csfloat_base_api_url}/v1/me/notary-token`, {
18+
credentials: 'include',
19+
method: 'POST',
20+
});
21+
22+
if (resp.status !== 200) {
23+
throw new Error('failed to fetch notary token');
24+
}
25+
26+
return (await resp.json()) as NotaryToken;
27+
}
28+
);

src/lib/bridge/handlers/handlers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {PingRollbackTrade} from './ping_rollback_trade';
3434
import {FetchTradeHistory} from './fetch_trade_history';
3535
import {FetchSlimTrades} from './fetch_slim_trades';
3636
import {NotaryProve} from './notary_prove';
37+
import {FetchNotaryMeta} from './fetch_notary_meta';
38+
import {FetchNotaryToken} from './fetch_notary_token';
3739

3840
export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
3941
[RequestType.EXECUTE_SCRIPT_ON_PAGE]: ExecuteScriptOnPage,
@@ -70,4 +72,6 @@ export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
7072
[RequestType.FETCH_TRADE_HISTORY]: FetchTradeHistory,
7173
[RequestType.FETCH_SLIM_TRADES]: FetchSlimTrades,
7274
[RequestType.NOTARY_PROVE]: NotaryProve,
75+
[RequestType.FETCH_NOTARY_META]: FetchNotaryMeta,
76+
[RequestType.FETCH_NOTARY_TOKEN]: FetchNotaryToken,
7377
};

src/lib/bridge/handlers/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ export enum RequestType {
3333
FETCH_TRADE_HISTORY = 31,
3434
FETCH_SLIM_TRADES = 32,
3535
NOTARY_PROVE = 33,
36+
FETCH_NOTARY_META = 34,
37+
FETCH_NOTARY_TOKEN = 35,
3638
}

0 commit comments

Comments
 (0)