Skip to content

Commit b4d136f

Browse files
authored
Merge pull request #970 from extolkom/backend/src/services/sorobanService.ts
Backend/src/services/soroban service.ts
2 parents f58d55e + 7707ddb commit b4d136f

3 files changed

Lines changed: 299 additions & 25 deletions

File tree

backend/src/services/sorobanService.ts

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import { rpc, xdr, StrKey, Contract, nativeToScVal, Keypair, TransactionBuilder,
22
import logger from '../logger.js';
33

44
const RPC_URL = process.env.SOROBAN_RPC_URL ?? 'https://soroban-testnet.stellar.org';
5-
const CONTRACT_ID = process.env.STREAM_CONTRACT_ID ?? '';
6-
const KEEPER_SECRET = process.env.KEEPER_SECRET_KEY ?? '';
5+
6+
function getContractId(): string {
7+
return process.env.STREAM_CONTRACT_ID ?? '';
8+
}
9+
10+
function getKeeperSecret(): string {
11+
return process.env.KEEPER_SECRET_KEY ?? '';
12+
}
713
/**
814
* DB data older than this is considered stale and triggers an RPC fallback.
915
* 30 s ≈ avg Stellar ledger close time (~5 s) × 6 ledgers — a reasonable
@@ -27,7 +33,22 @@ const TX_TIMEOUT_SECONDS = 30;
2733
*/
2834
const SIMULATION_PLACEHOLDER_ACCOUNT = 'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN';
2935

30-
const server = new rpc.Server(RPC_URL, { allowHttp: true });
36+
let _server: rpc.Server | null = null;
37+
38+
function getServer(): rpc.Server {
39+
if (!_server) {
40+
_server = new rpc.Server(RPC_URL, { allowHttp: true });
41+
}
42+
return _server;
43+
}
44+
45+
export function setServer(server: rpc.Server): void {
46+
_server = server;
47+
}
48+
49+
export function resetServer(): void {
50+
_server = null;
51+
}
3152

3253
export interface ChainStream {
3354
streamId: number;
@@ -41,14 +62,14 @@ export interface ChainStream {
4162
isActive: boolean;
4263
}
4364

44-
function decodeI128(val: xdr.ScVal): string {
65+
export function decodeI128(val: xdr.ScVal): string {
4566
const parts = val.i128();
4667
const hi = BigInt.asIntN(64, BigInt(parts.hi().toString()));
4768
const lo = BigInt.asUintN(64, BigInt(parts.lo().toString()));
4869
return ((hi << 64n) | lo).toString();
4970
}
5071

51-
function decodeAddress(val: xdr.ScVal): string {
72+
export function decodeAddress(val: xdr.ScVal): string {
5273
const addr = val.address();
5374
if (addr.switch().value === xdr.ScAddressType.scAddressTypeAccount().value) {
5475
return StrKey.encodeEd25519PublicKey(addr.accountId().ed25519());
@@ -66,11 +87,13 @@ function decodeMap(val: xdr.ScVal): Record<string, xdr.ScVal> {
6687
}
6788

6889
async function simulateContractCall(method: string, args: xdr.ScVal[]): Promise<xdr.ScVal> {
69-
const contract = new Contract(CONTRACT_ID);
90+
const contract = new Contract(getContractId());
7091

7192
const op = contract.call(method, ...args);
7293

7394
const tx = new TransactionBuilder(
95+
// Read-only simulations don't consume a real source account; use a valid
96+
// placeholder so Account construction never throws.
7497
new Account(SIMULATION_PLACEHOLDER_ACCOUNT, '0'),
7598
{
7699
fee: SIMULATION_FEE,
@@ -84,7 +107,7 @@ async function simulateContractCall(method: string, args: xdr.ScVal[]): Promise<
84107
.setTimeout(TX_TIMEOUT_SECONDS)
85108
.build();
86109

87-
const result = await server.simulateTransaction(tx);
110+
const result = await getServer().simulateTransaction(tx);
88111

89112
if (rpc.Api.isSimulationError(result)) {
90113
throw new Error(`Simulation error: ${result.error}`);
@@ -94,12 +117,13 @@ async function simulateContractCall(method: string, args: xdr.ScVal[]): Promise<
94117
return simSuccess.result!.retval;
95118
}
96119

97-
async function submitContractCall(method: string, args: xdr.ScVal[], senderSecret: string): Promise<string> {
98-
if (!CONTRACT_ID) throw new Error('CONTRACT_ID not set');
120+
export async function submitContractCall(method: string, args: xdr.ScVal[], senderSecret: string): Promise<string> {
121+
const contractId = getContractId();
122+
if (!contractId) throw new Error('CONTRACT_ID not set');
99123

100124
const keypair = Keypair.fromSecret(senderSecret);
101-
const contract = new Contract(CONTRACT_ID);
102-
const account = await server.getAccount(keypair.publicKey());
125+
const contract = new Contract(contractId);
126+
const account = await getServer().getAccount(keypair.publicKey());
103127

104128
const op = contract.call(method, ...args);
105129

@@ -115,7 +139,7 @@ async function submitContractCall(method: string, args: xdr.ScVal[], senderSecre
115139
.build();
116140

117141
// Simulate first to get foot print and resource info
118-
const simulation = await server.simulateTransaction(tx);
142+
const simulation = await getServer().simulateTransaction(tx);
119143
if (rpc.Api.isSimulationError(simulation)) {
120144
throw new Error(`Simulation failed: ${simulation.error}`);
121145
}
@@ -124,7 +148,7 @@ async function submitContractCall(method: string, args: xdr.ScVal[], senderSecre
124148
const assembledTx = rpc.assembleTransaction(tx, simulation).build();
125149
assembledTx.sign(keypair);
126150

127-
const response = await server.sendTransaction(assembledTx);
151+
const response = await getServer().sendTransaction(assembledTx);
128152

129153
if (response.status === 'ERROR') {
130154
throw new Error(`Transaction failed: ${JSON.stringify(response.errorResult)}`);
@@ -134,7 +158,7 @@ async function submitContractCall(method: string, args: xdr.ScVal[], senderSecre
134158
}
135159

136160
export async function getStreamFromChain(streamId: number): Promise<ChainStream | null> {
137-
if (!CONTRACT_ID) return null;
161+
if (!getContractId()) return null;
138162

139163
try {
140164
const retval = await simulateContractCall('get_stream', [
@@ -166,7 +190,7 @@ export async function getStreamFromChain(streamId: number): Promise<ChainStream
166190
}
167191

168192
export async function getClaimableFromChain(streamId: number): Promise<string | null> {
169-
if (!CONTRACT_ID) return null;
193+
if (!getContractId()) return null;
170194

171195
try {
172196
const retval = await simulateContractCall('get_claimable_amount', [
@@ -187,12 +211,13 @@ export async function cancelStream(streamId: number, senderSecret: string): Prom
187211
}
188212

189213
export async function topUpStream(streamId: number, amount: bigint, callerAddress: string): Promise<string> {
190-
if (!KEEPER_SECRET) throw new Error('KEEPER_SECRET_KEY not configured');
214+
const keeperSecret = getKeeperSecret();
215+
if (!keeperSecret) throw new Error('KEEPER_SECRET_KEY not configured');
191216
return submitContractCall('top_up_stream', [
192217
nativeToScVal(streamId, { type: 'u64' }),
193218
nativeToScVal(amount, { type: 'i128' }),
194219
nativeToScVal(callerAddress, { type: 'address' }),
195-
], KEEPER_SECRET);
220+
], keeperSecret);
196221
}
197222

198223
/** Returns true when the DB record is older than STALE_THRESHOLD_MS. */
@@ -213,7 +238,7 @@ export async function pauseStream(
213238
senderAddress: string,
214239
streamId: number
215240
): Promise<PauseResumeResult> {
216-
if (!CONTRACT_ID) {
241+
if (!getContractId()) {
217242
throw new Error('Stream contract ID not configured');
218243
}
219244

@@ -247,7 +272,7 @@ export async function resumeStream(
247272
senderAddress: string,
248273
streamId: number
249274
): Promise<PauseResumeResult> {
250-
if (!CONTRACT_ID) {
275+
if (!getContractId()) {
251276
throw new Error('Stream contract ID not configured');
252277
}
253278

@@ -280,7 +305,7 @@ export async function withdraw(
280305
streamId: number,
281306
recipientAddress: string,
282307
): Promise<PauseResumeResult> {
283-
if (!CONTRACT_ID) {
308+
if (!getContractId()) {
284309
throw new Error('Stream contract ID not configured');
285310
}
286311

backend/tests/auth-jwt.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ describe('JWT helpers', () => {
4343
const now = Math.floor(Date.now() / 1000);
4444
const token = signJwt({ sub: 'GTESTPUBLICKEY123', iat: now, exp: now + 3600 });
4545
const parts = token.split('.') as [string, string, string];
46-
parts[2] = parts[2].slice(0, -1) + (parts[2].slice(-1) === 'A' ? 'B' : 'A');
46+
// Replace the signature with invalid data to ensure verification fails
47+
parts[2] = 'invalid-signature-data-1234567890abcdef';
4748

4849
expect(verifyJwt(parts.join('.'))).toBeNull();
4950
});

0 commit comments

Comments
 (0)