Skip to content
Open
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
203 changes: 150 additions & 53 deletions core/api-doc-config.generated.json

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions core/src/BaseExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import type {
ArbitrageOpportunity,
MatchedMarketPair,
MatchedPricePair,
AuthNonceResponse,
AuthLoginResponse,
AuthSession,
} from './router/types';

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -473,6 +476,7 @@ export abstract class PredictionMarketExchange {
private _snapshot?: { markets: UnifiedMarket[]; takenAt: number; id: string };
private _eventSnapshot?: { events: UnifiedEvent[]; takenAt: number; id: string };
private apiDescriptors: ApiDescriptor[] = [];
protected _sessions: Map<string, AuthSession> = new Map();

constructor(credentials?: ExchangeCredentials, options?: ExchangeOptions) {
this.credentials = credentials;
Expand Down Expand Up @@ -555,6 +559,95 @@ export abstract class PredictionMarketExchange {

abstract get name(): string;



/**
* Get a cryptographic nonce for Web3 login.
* @param walletAddress - The wallet address to authenticate
*/
async getAuthNonce(walletAddress: string): Promise<AuthNonceResponse> {
throw new Error(`getAuthNonce not implemented for ${this.name}`);
}

/**
* Login with a signed nonce to obtain session credentials.
* @param walletAddress - The wallet address
* @param signature - The signature of the nonce message
* @param nonce - The nonce that was signed
*/
async loginWithSignature(
walletAddress: string,
signature: string,
nonce: string
): Promise<AuthLoginResponse> {
throw new Error(`loginWithSignature not implemented for ${this.name}`);
}

/**
* Logout and invalidate the current session.
* @param walletAddress - The wallet address to logout (optional)
*/
async logout(walletAddress?: string): Promise<void> {
throw new Error(`logout not implemented for ${this.name}`);
}

/**
* Check if a session is active for the given wallet address.
* @param walletAddress - The wallet address to check
*/
async isSessionActive(walletAddress: string): Promise<boolean> {
const session = this._sessions.get(walletAddress);
if (!session) return false;
if (session.credentials.expiresAt && session.credentials.expiresAt < Date.now()) {
this._sessions.delete(walletAddress);
return false;
}
return session.credentials.active !== false;
}

/**
* Get the stored session for a wallet address.
* @param walletAddress - The wallet address
*/
getSession(walletAddress: string): AuthSession | undefined {
return this._sessions.get(walletAddress);
}

/**
* Store a session in memory.
* @param walletAddress - The wallet address
* @param credentials - The session credentials
*/
protected storeSession(walletAddress: string, credentials: AuthLoginResponse): void {
this._sessions.set(walletAddress, {
walletAddress,
credentials,
createdAt: Date.now(),
lastUsedAt: Date.now(),
});
}

/**
* Remove a session from memory.
* @param walletAddress - The wallet address
*/
protected removeSession(walletAddress: string): void {
this._sessions.delete(walletAddress);
}

/**
* Clean expired sessions from memory.
* Should be called periodically to prevent memory leaks.
*/
protected cleanExpiredSessions(): void {
const now = Date.now();
for (const [address, session] of this._sessions) {
if (session.credentials.expiresAt && session.credentials.expiresAt < now) {
this._sessions.delete(address);
}
}
}

/**
* Introspection getter: returns info about all implicit API methods.
*/
Expand Down
102 changes: 102 additions & 0 deletions core/src/exchanges/probable/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createWalletClient, http } from 'viem';
import { bsc, bscTestnet } from 'viem/chains';
import { ExchangeCredentials } from '../../BaseExchange';
import { PROBABLE_CHAIN_ID, PROBABLE_TESTNET_CHAIN_ID } from './config';
import { AuthNonceResponse, AuthLoginResponse } from '../../router/types';

/**
* Manages Probable authentication and CLOB client initialization.
Expand Down Expand Up @@ -80,3 +81,104 @@ export class ProbableAuth {
return this.walletAddress;
}
}

/**
* Get a nonce from Probable.
* Uses the implicit API endpoint 'getPublicApiV1AuthNonce'.
*/
export async function getAuthNonce(
walletAddress: string,
callApi: Function
): Promise<AuthNonceResponse> {
try {
const response = await callApi('getPublicApiV1AuthNonce', { address: walletAddress });
return {
nonce: response.nonce,
messageToSign: response.message || response.messageToSign,
expiresAt: response.expiresAt,
};
} catch (error: any) {
throw new Error(`Failed to get Probable auth nonce: ${error.message}`);
}
}

/**
* Login to Probable with signature.
* Uses the implicit API endpoint 'postPublicApiV1AuthLogin'.
*/
export async function loginWithSignature(
walletAddress: string,
signature: string,
nonce: string,
callApi: Function
): Promise<AuthLoginResponse> {
try {
const response = await callApi('postPublicApiV1AuthLogin', {
address: walletAddress,
signature,
nonce,
});
return {
apiKey: response.apiKey,
apiSecret: response.apiSecret,
passphrase: response.passphrase,
expiresAt: response.expiresAt,
active: true,
};
} catch (error: any) {
throw new Error(`Failed to login to Probable: ${error.message}`);
}
}

/**
* Logout from Probable.
* Uses the implicit API endpoint 'postPublicApiV1AuthLogout'.
*/
export async function logout(callApi: Function): Promise<void> {
try {
await callApi('postPublicApiV1AuthLogout', {});
} catch (error: any) {
// Log but don't throw - logout should be best-effort
console.warn(`Failed to logout from Probable: ${error.message}`);
}
}

/**
* Verify L1 signature.
* Uses the implicit API endpoint 'postPublicApiV1AuthVerifyL1'.
*/
export async function verifyL1(
walletAddress: string,
signature: string,
callApi: Function
): Promise<boolean> {
try {
const response = await callApi('postPublicApiV1AuthVerifyL1', {
address: walletAddress,
signature,
});
return response.verified === true;
} catch (error: any) {
return false;
}
}

/**
* Verify L2 signature.
* Uses the implicit API endpoint 'postPublicApiV1AuthVerifyL2'.
*/
export async function verifyL2(
walletAddress: string,
signature: string,
callApi: Function
): Promise<boolean> {
try {
const response = await callApi('postPublicApiV1AuthVerifyL2', {
address: walletAddress,
signature,
});
return response.verified === true;
} catch (error: any) {
return false;
}
}
45 changes: 45 additions & 0 deletions core/src/exchanges/probable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import {
TradesParams,
MyTradesParams,
} from '../../BaseExchange';
import {
getAuthNonce as getAuthNonceFn,
loginWithSignature as loginWithSignatureFn,
logout as logoutFn,
verifyL1 as verifyL1Fn,
verifyL2 as verifyL2Fn,
} from './auth';
import {
AuthNonceResponse,
AuthLoginResponse,
} from '../../router/types';
import {
UnifiedMarket,
UnifiedEvent,
Expand Down Expand Up @@ -90,6 +101,40 @@ export class ProbableExchange extends PredictionMarketExchange {
return this.auth;
}

async getAuthNonce(walletAddress: string): Promise<AuthNonceResponse> {
return getAuthNonceFn(walletAddress, this.callApi.bind(this));
}

async loginWithSignature(
walletAddress: string,
signature: string,
nonce: string
): Promise<AuthLoginResponse> {
const credentials = await loginWithSignatureFn(
walletAddress,
signature,
nonce,
this.callApi.bind(this)
);
this.storeSession(walletAddress, credentials);
return credentials;
}

async logout(walletAddress?: string): Promise<void> {
await logoutFn(this.callApi.bind(this));
if (walletAddress) {
this.removeSession(walletAddress);
}
}

async verifyL1(walletAddress: string, signature: string): Promise<boolean> {
return verifyL1Fn(walletAddress, signature, this.callApi.bind(this));
}

async verifyL2(walletAddress: string, signature: string): Promise<boolean> {
return verifyL2Fn(walletAddress, signature, this.callApi.bind(this));
}

// --------------------------------------------------------------------------
// Market Data (fetcher -> normalizer)
// --------------------------------------------------------------------------
Expand Down
44 changes: 44 additions & 0 deletions core/src/router/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,50 @@ export interface FetchMarketMatchesParams {
sort?: 'confidence' | 'volume' | 'priceDifference';
}

// ===== Probable Auth Types =====

/**
* Response from /auth/nonce
*/
export interface AuthNonceResponse {
/** Random nonce to be signed */
nonce: string;
/** Human-readable message to sign */
messageToSign: string;
/** Optional expiry timestamp (milliseconds) */
expiresAt?: number;
}

/**
* Response from /auth/login
*/
export interface AuthLoginResponse {
/** API key for authenticated requests */
apiKey: string;
/** API secret for signing */
apiSecret: string;
/** Passphrase for trading */
passphrase?: string;
/** Session expiry timestamp (milliseconds) */
expiresAt?: number;
/** Whether session is active */
active?: boolean;
}

/**
* Session state stored in memory
*/
export interface AuthSession {
/** Wallet address that authenticated */
walletAddress: string;
/** Session credentials */
credentials: AuthLoginResponse;
/** When session was created (milliseconds) */
createdAt: number;
/** When session was last used (milliseconds) */
lastUsedAt: number;
}

/** @deprecated Use {@link FetchMarketMatchesParams} instead. */
export type FetchMatchesParams = FetchMarketMatchesParams;

Expand Down
Loading
Loading