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

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions core/src/BaseExchange.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { EventNotFound, MarketNotFound } from './errors';
import { SubscribedAddressSnapshot, SubscriptionOption } from './subscriber/base';
import { AuthNonceResponse, SessionCredentials } from './types';
import {
Balance,
BuiltOrder,
Expand Down Expand Up @@ -661,6 +662,29 @@ export abstract class PredictionMarketExchange {
return limit !== undefined ? markets.slice(start, start + limit) : markets.slice(start);
}

/**
* Retrieves a cryptographic challenge string (nonce) from a Web3 exchange.
* Required for EIP-712 / wallet signature authentication workflows.
*/
async getAuthNonce(walletAddress: string): Promise<AuthNonceResponse> {
throw new Error(`getAuthNonce not implemented for exchange ${this.id}`);
}

/**
* Submits a completed wallet signature to exchange authentication endpoints
* to acquire temporary API credentials or establish a valid session.
*/
async loginWithSignature(walletAddress: string, signature: string, nonce: string): Promise<SessionCredentials> {
throw new Error(`loginWithSignature not implemented for exchange ${this.id}`);
}

/**
* Programmatically terminates the active session or revokes the current credentials.
*/
async logout(): Promise<void> {
throw new Error(`logout not implemented for exchange ${this.id}`);
}

/**
* Fetch markets with cursor-based pagination backed by a stable in-memory snapshot.
*
Expand Down
47 changes: 47 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, SessionCredentials } from '../../types';

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

/**
* Fetches a cryptographic nonce for EIP-712 wallet login.
*/
export async function getAuthNonce(walletAddress: string, callApi: Function): Promise<AuthNonceResponse> {
// OperationId based on Probable's OpenAPI spec for GET /public/api/v1/auth/nonce
const response = await callApi('getAuthNonce', { address: walletAddress });

return {
nonce: response.nonce,
// Fallbacks based on how different versions of the spec name the message field
messageToSign: response.message || response.messageToSign || `Sign to login: ${response.nonce}`
};
}

/**
* Submits the signed nonce to generate an API Key triplet.
*/
export async function loginWithSignature(
walletAddress: string,
signature: string,
nonce: string,
callApi: Function
): Promise<SessionCredentials> {
// OperationId for POST /auth/login
const response = await callApi('postAuthLogin', {
address: walletAddress,
signature,
nonce
});

return {
apiKey: response.apiKey,
apiSecret: response.apiSecret,
passphrase: response.passphrase,
expiresAt: response.expiresAt
};
}

/**
* Destroys the current API key session.
*/
export async function logoutSession(callApi: Function): Promise<void> {
// OperationId for POST /auth/logout
await callApi('postAuthLogout', {});
}
14 changes: 14 additions & 0 deletions core/src/router/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ export interface FetchMarketMatchesParams {
sort?: 'confidence' | 'volume' | 'priceDifference';
}

export interface AuthNonceResponse {
nonce: string;
messageToSign: string;
expiresAt?: number;
}

export interface SessionCredentials {
apiKey: string;
apiSecret: string;
passphrase?: string;
expiresAt?: number;
active?: boolean;
}

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

Expand Down
34 changes: 34 additions & 0 deletions core/src/server/method-verbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,40 @@
}
]
},
"getAuthNonce": {
"verb": "post",
"args": [
{
"name": "walletAddress",
"kind": "string",
"optional": false
}
]
},
"loginWithSignature": {
"verb": "post",
"args": [
{
"name": "walletAddress",
"kind": "string",
"optional": false
},
{
"name": "signature",
"kind": "string",
"optional": false
},
{
"name": "nonce",
"kind": "string",
"optional": false
}
]
},
"logout": {
"verb": "post",
"args": []
},
"fetchMarketsPaginated": {
"verb": "get",
"args": [
Expand Down
106 changes: 106 additions & 0 deletions core/src/server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,112 @@ paths:
description: >-
Fetch markets with optional filtering, search, or slug lookup. Always hits the exchange API — results reflect
the live state at the time of the call.
'/api/{exchange}/getAuthNonce':
post:
summary: Get Auth Nonce
operationId: getAuthNonce
parameters:
- $ref: '#/components/parameters/ExchangeParam'
requestBody:
content:
application/json:
schema:
title: GetAuthNonceRequest
type: object
properties:
args:
type: array
maxItems: 1
items:
type: string
minItems: 1
credentials:
$ref: '#/components/schemas/ExchangeCredentials'
required:
- args
responses:
'200':
description: Get Auth Nonce response
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/BaseResponse'
- type: object
properties:
data:
type: object
description: >-
Retrieves a cryptographic challenge string (nonce) from a Web3 exchange. Required for EIP-712 / wallet signature
authentication workflows.
'/api/{exchange}/loginWithSignature':
post:
summary: Login With Signature
operationId: loginWithSignature
parameters:
- $ref: '#/components/parameters/ExchangeParam'
requestBody:
content:
application/json:
schema:
title: LoginWithSignatureRequest
type: object
properties:
args:
type: array
minItems: 3
maxItems: 3
items:
oneOf:
- type: string
- type: string
- type: string
credentials:
$ref: '#/components/schemas/ExchangeCredentials'
required:
- args
responses:
'200':
description: Login With Signature response
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/BaseResponse'
- type: object
properties:
data:
type: object
description: >-
Submits a completed wallet signature to exchange authentication endpoints to acquire temporary API credentials
or establish a valid session.
'/api/{exchange}/logout':
post:
summary: Logout
operationId: logout
parameters:
- $ref: '#/components/parameters/ExchangeParam'
requestBody:
content:
application/json:
schema:
title: LogoutRequest
type: object
properties:
args:
type: array
maxItems: 0
items: {}
credentials:
$ref: '#/components/schemas/ExchangeCredentials'
responses:
'200':
description: Logout response
content:
application/json:
schema:
$ref: '#/components/schemas/BaseResponse'
description: Programmatically terminates the active session or revokes the current credentials.
'/api/{exchange}/fetchMarketsPaginated':
get:
summary: Fetch Markets Paginated
Expand Down
11 changes: 11 additions & 0 deletions core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ export interface UserTrade extends Trade {
blockNumber?: number | null;
}

export interface AuthNonceResponse {
nonce: string;
messageToSign: string;
}

export interface SessionCredentials {
apiKey: string;
apiSecret: string;
passphrase?: string;
expiresAt?: number;
}

export interface QueuedPromise<T> {
/** Internal: resolver for a queued promise. */
Expand Down
51 changes: 51 additions & 0 deletions core/test/exchanges/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getAuthNonce, loginWithSignature, logoutSession } from '../../src/exchanges/probable/auth';

describe('Probable Auth Lifecycle', () => {
const mockWalletAddress = '0x1234567890abcdef1234567890abcdef12345678';
const mockSignature = '0xabc123...';
const mockNonce = 'random-nonce-string';

afterEach(() => {
jest.clearAllMocks();
});

it('should fetch and map nonce correctly', async () => {
const mockCallApi = jest.fn().mockResolvedValue({
nonce: mockNonce,
message: 'Sign this message to authenticate with Probable'
});

const result = await getAuthNonce(mockWalletAddress, mockCallApi);

expect(mockCallApi).toHaveBeenCalledWith('getAuthNonce', { address: mockWalletAddress });
expect(result.nonce).toBe(mockNonce);
expect(result.messageToSign).toBe('Sign this message to authenticate with Probable');
});

it('should submit signature and map session credentials correctly', async () => {
const mockCallApi = jest.fn().mockResolvedValue({
apiKey: 'key-123',
apiSecret: 'secret-456',
passphrase: 'passphrase-789',
expiresAt: 1700000000000
});

const result = await loginWithSignature(mockWalletAddress, mockSignature, mockNonce, mockCallApi);

expect(mockCallApi).toHaveBeenCalledWith('postAuthLogin', {
address: mockWalletAddress,
signature: mockSignature,
nonce: mockNonce
});
expect(result.apiKey).toBe('key-123');
expect(result.apiSecret).toBe('secret-456');
});

it('should trigger logout successfully', async () => {
const mockCallApi = jest.fn().mockResolvedValue({});

await logoutSession(mockCallApi);

expect(mockCallApi).toHaveBeenCalledWith('postAuthLogout', {});
});
});
Loading
Loading