Skip to content

Commit a929c50

Browse files
authored
Merge pull request #381 from ShantelPeters/feature/tikka-oracle-implementation
feat: implement EventListenerService, ContractService and fix oracle …
2 parents a1062f0 + 70a7a9e commit a929c50

13 files changed

Lines changed: 323 additions & 195 deletions
Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import * as StellarSdk from '@stellar/stellar-sdk';
24

35
export interface RaffleData {
46
raffleId: number;
@@ -8,15 +10,74 @@ export interface RaffleData {
810

911
@Injectable()
1012
export class ContractService {
13+
private readonly logger = new Logger(ContractService.name);
14+
private readonly rpcServer: StellarSdk.rpc.Server;
15+
private readonly contractId: string;
16+
private readonly networkPassphrase: string;
17+
18+
constructor(private readonly configService: ConfigService) {
19+
const rpcUrl = this.configService.get<string>('SOROBAN_RPC_URL', 'https://soroban-testnet.stellar.org');
20+
this.networkPassphrase = this.configService.get<string>('NETWORK_PASSPHRASE', StellarSdk.Networks.TESTNET);
21+
this.contractId = this.configService.get<string>('RAFFLE_CONTRACT_ID', '');
22+
23+
this.rpcServer = new StellarSdk.rpc.Server(rpcUrl);
24+
25+
if (!this.contractId) {
26+
this.logger.warn('RAFFLE_CONTRACT_ID is not set. ContractService calls will fail.');
27+
}
28+
}
29+
1130
/**
1231
* Fetches raffle data from the Soroban contract
1332
* @param raffleId The raffle ID
1433
* @returns Raffle data including prize amount
1534
*/
1635
async getRaffleData(raffleId: number): Promise<RaffleData> {
17-
// TODO: Implement Soroban RPC call to get_raffle_data
18-
// This will use stellar-sdk to simulate the read-only call
19-
throw new Error('Contract RPC not yet implemented');
36+
if (!this.contractId) {
37+
throw new Error('RAFFLE_CONTRACT_ID is not configured');
38+
}
39+
40+
try {
41+
this.logger.debug(`Fetching raffle data for ID: ${raffleId}`);
42+
43+
// Create a dummy source account for simulation
44+
const sourceAccount = new StellarSdk.Account('GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF', '0');
45+
const contract = new StellarSdk.Contract(this.contractId);
46+
47+
const tx = new StellarSdk.TransactionBuilder(sourceAccount, {
48+
fee: '100',
49+
networkPassphrase: this.networkPassphrase,
50+
})
51+
.addOperation(contract.call('get_raffle_data', StellarSdk.xdr.ScVal.scvU32(raffleId >>> 0)))
52+
.setTimeout(30)
53+
.build();
54+
55+
const simulated = await this.rpcServer.simulateTransaction(tx);
56+
57+
if (StellarSdk.rpc.Api.isSimulationError(simulated)) {
58+
throw new Error(`Simulation failed: ${JSON.stringify(simulated.error)}`);
59+
}
60+
61+
if (!simulated.result) {
62+
throw new Error('Simulation returned no result');
63+
}
64+
65+
const resultValue = simulated.result.retval;
66+
const data = this.decodeScVal(resultValue);
67+
68+
if (!data || typeof data !== 'object') {
69+
throw new Error('Failed to decode raffle data from contract response');
70+
}
71+
72+
return {
73+
raffleId: Number(data.raffle_id ?? raffleId),
74+
prizeAmount: Number(data.prize_amount ?? 0),
75+
status: String(data.status ?? 'UNKNOWN'),
76+
};
77+
} catch (error: any) {
78+
this.logger.error(`Failed to fetch raffle data for ${raffleId}: ${error.message}`);
79+
throw error;
80+
}
2081
}
2182

2283
/**
@@ -25,15 +86,62 @@ export class ContractService {
2586
* @returns True if already finalized
2687
*/
2788
async isRandomnessSubmitted(raffleId: number): Promise<boolean> {
28-
const data = await this.getRaffleData(raffleId);
29-
return data.status === 'FINALIZED' || data.status === 'CANCELLED';
89+
try {
90+
const data = await this.getRaffleData(raffleId);
91+
return data.status === 'FINALIZED' || data.status === 'CANCELLED';
92+
} catch (error) {
93+
// If we can't fetch data, we assume it's safer to retry or log error
94+
this.logger.error(`Could not determine if randomness is submitted for raffle ${raffleId}: ${error.message}`);
95+
return false;
96+
}
3097
}
3198

3299
/**
33100
* Pings the Soroban contract to inform the ecosystem that the oracle is alive
34101
*/
35102
async ping(): Promise<void> {
36-
// TODO: Implement Soroban RPC call to ping
37-
// throw new Error('Contract RPC not yet implemented');
103+
if (!this.contractId) return;
104+
105+
try {
106+
this.logger.log('Sending oracle heartbeat (ping) to contract...');
107+
// Note: Ping is usually a write operation, so it requires a real account and signing.
108+
// For this MVP, we might just use it to check RPC health.
109+
await this.rpcServer.getLatestLedger();
110+
} catch (error: any) {
111+
this.logger.error(`Oracle ping failed: ${error.message}`);
112+
}
113+
}
114+
115+
/**
116+
* Helper to decode ScVal into JS types
117+
*/
118+
private decodeScVal(val: StellarSdk.xdr.ScVal): any {
119+
switch (val.switch()) {
120+
case StellarSdk.xdr.ScValType.scvU32(): return val.u32();
121+
case StellarSdk.xdr.ScValType.scvU64(): return val.u64().toString();
122+
case StellarSdk.xdr.ScValType.scvI32(): return val.i32();
123+
case StellarSdk.xdr.ScValType.scvI64(): return val.i64().toString();
124+
case StellarSdk.xdr.ScValType.scvSymbol(): return val.sym().toString();
125+
case StellarSdk.xdr.ScValType.scvString(): return val.str().toString();
126+
case StellarSdk.xdr.ScValType.scvBytes(): return val.bytes().toString('hex');
127+
case StellarSdk.xdr.ScValType.scvAddress():
128+
const addr = val.address();
129+
if (addr.switch() === StellarSdk.xdr.ScAddressType.scAddressTypeAccount()) {
130+
return StellarSdk.Address.account(Buffer.from(addr.accountId().ed25519() as any)).toString();
131+
} else {
132+
return StellarSdk.Address.contract(Buffer.from(addr.contractId() as any)).toString();
133+
}
134+
case StellarSdk.xdr.ScValType.scvBool(): return val.b();
135+
case StellarSdk.xdr.ScValType.scvMap():
136+
const result: Record<string, any> = {};
137+
for (const entry of val.map() ?? []) {
138+
const key = entry.key().sym().toString();
139+
result[key] = this.decodeScVal(entry.val());
140+
}
141+
return result;
142+
case StellarSdk.xdr.ScValType.scvVec():
143+
return (val.vec() ?? []).map(v => this.decodeScVal(v));
144+
default: return null;
145+
}
38146
}
39147
}

oracle/src/keys/key.service.integration.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { ConfigService } from '@nestjs/config';
33
import { KeyService } from './key.service';
4-
import * as StellarSdk from 'stellar-sdk';
4+
import * as StellarSdk from '@stellar/stellar-sdk';
55

66
describe('KeyService Integration', () => {
77
let service: KeyService;

oracle/src/keys/key.service.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import { Test, TestingModule } from '@nestjs/testing';
22
import { ConfigService } from '@nestjs/config';
33
import { KeyService } from './key.service';
44
import { EnvKeyProvider } from './providers/env-key.provider';
5+
import * as StellarSdk from '@stellar/stellar-sdk';
56

67
describe('KeyService', () => {
78
let service: KeyService;
89
let configService: ConfigService;
910

1011
// Test Stellar keypair
11-
const TEST_SECRET = 'SBZVMB74YWMVED3F6X3LQJZCOQKDIZHIVSKRK6JKHFXX2XQIMKPEKLM3';
12-
const TEST_PUBLIC = 'GDQERENWDDSQZS7R7WKHZI3BSOYMV3U3YDVYA7XQFVQVXQZQXQZQXQZQ';
12+
const TEST_KEYPAIR = StellarSdk.Keypair.random();
13+
const TEST_SECRET = TEST_KEYPAIR.secret();
14+
const TEST_PUBLIC = TEST_KEYPAIR.publicKey();
1315

1416
beforeEach(async () => {
1517
const module: TestingModule = await Test.createTestingModule({

oracle/src/keys/key.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config';
33
import { KeyProvider } from './key-provider.interface';
44
import { KeyProviderFactory } from './key-provider.factory';
55
import { EnvKeyProvider } from './providers/env-key.provider';
6-
import * as StellarSdk from 'stellar-sdk';
6+
import * as StellarSdk from '@stellar/stellar-sdk';
77

88
/**
99
* KeyService — manages the oracle's Ed25519 keypair using pluggable providers.

oracle/src/keys/providers/env-key.provider.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Injectable, Logger } from '@nestjs/common';
2-
import { Keypair } from 'stellar-sdk';
2+
import { Keypair } from '@stellar/stellar-sdk';
33
import { KeyProvider } from '../key-provider.interface';
44

55
/**
@@ -25,8 +25,8 @@ export class EnvKeyProvider implements KeyProvider {
2525
this.keypair = Keypair.fromSecret(privateKey);
2626
this.logger.log(`EnvKeyProvider initialized for address: ${this.keypair.publicKey()}`);
2727
} catch (error) {
28-
this.logger.error(`Failed to load keypair: ${error.message}`);
29-
throw new Error('Invalid private key format');
28+
this.logger.error(`Failed to load keypair from secret: ${error.message}`);
29+
throw new Error(`Invalid private key format: ${error.message}`);
3030
}
3131
}
3232

0 commit comments

Comments
 (0)