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
33 changes: 33 additions & 0 deletions backend/src/services/blockchain/market.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,39 @@ export class MarketBlockchainService extends BaseBlockchainService {
);
}
}

/**
* Commit a prediction on the blockchain
*/
async commitPrediction(
marketContractAddress: string,
commitmentHash: string,
amountUsdc: number
): Promise<MarketActionResult> {
// TODO: Implement actual Stellar contract call
logger.info('Blockchain: commiting prediction', {
marketContractAddress,
commitmentHash,
amountUsdc,
});
return { txHash: 'mock-commit-tx-' + Date.now() };
}

/**
* Reveal a prediction on the blockchain
*/
async revealPrediction(
marketContractAddress: string,
predictedOutcome: number,
salt: string
): Promise<MarketActionResult> {
// TODO: Implement actual Stellar contract call
logger.info('Blockchain: revealing prediction', {
marketContractAddress,
predictedOutcome,
});
return { txHash: 'mock-reveal-tx-' + Date.now() };
}
}

export const marketBlockchainService = new MarketBlockchainService();
36 changes: 22 additions & 14 deletions backend/src/services/cron.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ export class CronService {
private marketRepository: MarketRepository;
private marketService: MarketService;

constructor(
marketRepo?: MarketRepository,
marketSvc?: MarketService
) {
constructor(marketRepo?: MarketRepository, marketSvc?: MarketService) {
this.marketRepository = marketRepo || new MarketRepository();
this.marketService = marketSvc || new MarketService();
}
Expand Down Expand Up @@ -53,7 +50,8 @@ export class CronService {

let markets;
try {
markets = await this.marketRepository.getClosedMarketsAwaitingResolution();
markets =
await this.marketRepository.getClosedMarketsAwaitingResolution();
} catch (error) {
logger.error('Oracle polling: failed to fetch closed markets', { error });
return;
Expand All @@ -64,31 +62,41 @@ export class CronService {
return;
}

logger.info(`Oracle polling: checking consensus for ${markets.length} market(s)`);
logger.info(
`Oracle polling: checking consensus for ${markets.length} market(s)`
);

for (const market of markets) {
try {
const winningOutcome = await oracleService.checkConsensus(market.id);

if (winningOutcome === null) {
logger.info(`Oracle polling: no consensus yet for market ${market.id}`);
logger.info(
`Oracle polling: no consensus yet for market ${market.id}`
);
continue;
}

logger.info(`Oracle polling: consensus reached for market ${market.id}`, {
winningOutcome,
});
logger.info(
`Oracle polling: consensus reached for market ${market.id}`,
{
winningOutcome,
}
);

const resolved = await this.marketService.resolveMarket(
market.id,
winningOutcome,
'oracle-consensus'
);

logger.info(`Oracle polling: market ${market.id} resolved successfully`, {
winningOutcome,
resolvedAt: resolved.resolvedAt,
});
logger.info(
`Oracle polling: market ${market.id} resolved successfully`,
{
winningOutcome,
resolvedAt: resolved.resolvedAt,
}
);
} catch (error) {
logger.error(`Oracle polling: failed to process market ${market.id}`, {
error,
Expand Down
52 changes: 32 additions & 20 deletions backend/src/services/prediction.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,27 @@ import {
encrypt,
decrypt,
} from '../utils/crypto.js';
import {
marketBlockchainService,
MarketBlockchainService,
} from './blockchain/market.js';

export class PredictionService {
private predictionRepository: PredictionRepository;
private marketRepository: MarketRepository;
private userRepository: UserRepository;
private blockchainService: MarketBlockchainService;

constructor() {
this.predictionRepository = new PredictionRepository();
this.marketRepository = new MarketRepository();
this.userRepository = new UserRepository();
constructor(
predictionRepo?: PredictionRepository,
marketRepo?: MarketRepository,
userRepo?: UserRepository,
blockchainSvc?: MarketBlockchainService
) {
this.predictionRepository = predictionRepo || new PredictionRepository();
this.marketRepository = marketRepo || new MarketRepository();
this.userRepository = userRepo || new UserRepository();
this.blockchainService = blockchainSvc || marketBlockchainService;
}

/**
Expand Down Expand Up @@ -87,13 +98,12 @@ export class PredictionService {
// Encrypt salt for secure storage
const { encrypted: encryptedSalt, iv: saltIv } = encrypt(salt);

// TODO: Call blockchain contract - Market.commit_prediction()
// const txHash = await blockchainService.commitPrediction(
// marketId,
// commitmentHash,
// amountUsdc
// );
const txHash = 'mock-tx-hash-' + Date.now(); // Mock for now
// Call blockchain contract - Market.commit_prediction()
const { txHash } = await this.blockchainService.commitPrediction(
market.contractAddress,
commitmentHash,
amountUsdc
);

// Create prediction and update balances in transaction
return await executeTransaction(async (tx) => {
Expand Down Expand Up @@ -170,15 +180,10 @@ export class PredictionService {
// Decrypt the stored salt
const salt = decrypt(prediction.encryptedSalt, prediction.saltIv);

// TODO: Call blockchain contract - Market.reveal_prediction()
// const revealTxHash = await blockchainService.revealPrediction(
// marketId,
// predictedOutcome,
// salt
// );
const revealTxHash = 'mock-reveal-tx-' + Date.now(); // Mock for now

// Calculate the original predicted outcome from commitment hash
// Call blockchain contract - Market.reveal_prediction()
// We reveal ONLY after finding the correct outcome below
// (Actually the reveal call on chain needs the outcome and salt)
// First, calculate the original predicted outcome from commitment hash
// We need to try both outcomes to verify which one matches
let predictedOutcome: number | null = null;
for (const outcome of [0, 1]) {
Expand All @@ -195,6 +200,13 @@ export class PredictionService {
);
}

const { txHash: revealTxHash } =
await this.blockchainService.revealPrediction(
market.contractAddress,
predictedOutcome,
salt
);

// Update prediction to revealed status
return await this.predictionRepository.revealPrediction(
predictionId,
Expand Down
63 changes: 52 additions & 11 deletions backend/tests/auth.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
import {
describe,
it,
expect,
beforeAll,
afterAll,
beforeEach,
vi,
} from 'vitest';
import { Keypair } from '@stellar/stellar-sdk';
import Redis from 'ioredis';
import jwt from 'jsonwebtoken';
Expand All @@ -9,7 +17,14 @@ const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
// Import services
import { SessionService } from '../src/services/session.service.js';
import { StellarService } from '../src/services/stellar.service.js';
import { signAccessToken, signRefreshToken, verifyAccessToken, verifyRefreshToken, getAccessTokenTTLSeconds, getRefreshTokenTTLSeconds } from '../src/utils/jwt.js';
import {
signAccessToken,
signRefreshToken,
verifyAccessToken,
verifyRefreshToken,
getAccessTokenTTLSeconds,
getRefreshTokenTTLSeconds,
} from '../src/utils/jwt.js';
import { generateNonce } from '../src/utils/crypto.js';

describe('Auth Integration Tests', () => {
Expand Down Expand Up @@ -53,7 +68,10 @@ describe('Auth Integration Tests', () => {
expect(isValid).toBe(true);

// Step 4: Consume nonce (simulating login)
const consumed = await sessionService.consumeNonce(publicKey, nonceData.nonce);
const consumed = await sessionService.consumeNonce(
publicKey,
nonceData.nonce
);
expect(consumed).not.toBeNull();

// Step 5: Generate tokens
Expand Down Expand Up @@ -109,7 +127,10 @@ describe('Auth Integration Tests', () => {
await sessionService.consumeNonce(publicKey, nonceData.nonce);

// Try to use same nonce again (replay attack)
const consumed = await sessionService.consumeNonce(publicKey, nonceData.nonce);
const consumed = await sessionService.consumeNonce(
publicKey,
nonceData.nonce
);
expect(consumed).toBeNull();
});
});
Expand Down Expand Up @@ -260,7 +281,10 @@ describe('Auth Integration Tests', () => {
})
);

const consumed = await sessionService.consumeNonce(publicKey, expiredNonce);
const consumed = await sessionService.consumeNonce(
publicKey,
expiredNonce
);
expect(consumed).toBeNull();
});

Expand Down Expand Up @@ -302,9 +326,15 @@ describe('Auth Integration Tests', () => {
const publicKey = keypair.publicKey();

const nonceData = await sessionService.createNonce(publicKey);
const signature = keypair.sign(Buffer.from(nonceData.message)).toString('base64');
const signature = keypair
.sign(Buffer.from(nonceData.message))
.toString('base64');

const isValid = stellarService.verifySignature(publicKey, nonceData.message, signature);
const isValid = stellarService.verifySignature(
publicKey,
nonceData.message,
signature
);
expect(isValid).toBe(true);
});

Expand All @@ -313,7 +343,9 @@ describe('Auth Integration Tests', () => {
const keypair2 = Keypair.random();

const nonceData = await sessionService.createNonce(keypair1.publicKey());
const signature = keypair2.sign(Buffer.from(nonceData.message)).toString('base64');
const signature = keypair2
.sign(Buffer.from(nonceData.message))
.toString('base64');

const isValid = stellarService.verifySignature(
keypair1.publicKey(),
Expand Down Expand Up @@ -345,10 +377,16 @@ describe('Auth Integration Tests', () => {
const publicKey = keypair.publicKey();

const nonceData = await sessionService.createNonce(publicKey);
const signature = keypair.sign(Buffer.from(nonceData.message)).toString('base64');
const signature = keypair
.sign(Buffer.from(nonceData.message))
.toString('base64');

const tamperedMessage = nonceData.message + ' TAMPERED';
const isValid = stellarService.verifySignature(publicKey, tamperedMessage, signature);
const isValid = stellarService.verifySignature(
publicKey,
tamperedMessage,
signature
);
expect(isValid).toBe(false);
});
});
Expand Down Expand Up @@ -416,7 +454,10 @@ describe('Auth Integration Tests', () => {
});

it('should reject refresh token used as access token', () => {
const refreshToken = signRefreshToken({ userId: 'test', tokenId: 'test' });
const refreshToken = signRefreshToken({
userId: 'test',
tokenId: 'test',
});

expect(() => verifyAccessToken(refreshToken)).toThrow();
});
Expand Down
13 changes: 8 additions & 5 deletions backend/tests/database/transaction.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Integration tests for transaction utilities
import { describe, it, expect, beforeEach } from 'vitest';
import { executeTransaction, executeTransactionWithRetry } from '../../src/database/transaction.js';
import {
executeTransaction,
executeTransactionWithRetry,
} from '../../src/database/transaction.js';
import { UserRepository } from '../../src/repositories/user.repository.js';
import { MarketRepository } from '../../src/repositories/market.repository.js';
import { MarketCategory } from '@prisma/client';
Expand All @@ -13,7 +16,7 @@ describe('Transaction Utilities Integration Tests', () => {
it('should commit transaction on success', async () => {
const result = await executeTransaction(async (tx) => {
const userRepoTx = new UserRepository(tx);

const user = await userRepoTx.createUser({
email: `tx-success-${Date.now()}@example.com`,
username: `txsuccess-${Date.now()}`,
Expand All @@ -36,7 +39,7 @@ describe('Transaction Utilities Integration Tests', () => {
try {
await executeTransaction(async (tx) => {
const userRepoTx = new UserRepository(tx);

await userRepoTx.createUser({
email,
username: `txrollback-${Date.now()}`,
Expand Down Expand Up @@ -65,7 +68,7 @@ describe('Transaction Utilities Integration Tests', () => {
});

const contractAddress = `CONTRACT_PARTIAL_${Date.now()}`;

try {
await executeTransaction(async (tx) => {
const userRepoTx = new UserRepository(tx);
Expand Down Expand Up @@ -109,7 +112,7 @@ describe('Transaction Utilities Integration Tests', () => {

const result = await executeTransactionWithRetry(async (tx) => {
attemptCount++;

if (attemptCount < 2) {
throw new Error('Simulated transient failure');
}
Expand Down
Loading
Loading