@@ -2,8 +2,14 @@ import { rpc, xdr, StrKey, Contract, nativeToScVal, Keypair, TransactionBuilder,
22import logger from '../logger.js' ;
33
44const 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 */
2834const 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
3253export 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
6889async 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
136160export 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
168192export 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
189213export 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
0 commit comments