@@ -21,7 +21,7 @@ type Teleport = {
21
21
dateRequested : string ;
22
22
status : TeleportStatus ;
23
23
receiverAddress : EvmAddress ;
24
- memo ? : string ;
24
+ memo : string ;
25
25
id ?: string ;
26
26
} ;
27
27
@@ -34,6 +34,7 @@ export class BrlaTeleportService {
34
34
35
35
private intervalMs = 10000 ;
36
36
37
+ // Key is a composite of subaccountId and memo: `${subaccountId}:${memo}`
37
38
private teleports : Map < string , Teleport > = new Map ( ) ;
38
39
39
40
private completedTeleports : Map < string , Teleport > = new Map ( ) ;
@@ -53,32 +54,73 @@ export class BrlaTeleportService {
53
54
return BrlaTeleportService . teleportService ;
54
55
}
55
56
56
- public async requestTeleport ( subaccountId : string , amount : number , receiverAddress : EvmAddress ) : Promise < void > {
57
+ private getCompositeKey ( subaccountId : string , memo : string ) : string {
58
+ const safeMemo = typeof memo === 'string' ? memo : '' ;
59
+ return `${ subaccountId } :${ safeMemo } ` ;
60
+ }
61
+
62
+ public async requestTeleport (
63
+ subaccountId : string ,
64
+ amount : number ,
65
+ receiverAddress : EvmAddress ,
66
+ memo : string ,
67
+ ) : Promise < void > {
68
+ const compositeKey = this . getCompositeKey ( subaccountId , memo ) ;
69
+ const existing = this . teleports . get ( compositeKey ) ;
70
+
71
+ if (
72
+ existing &&
73
+ existing . amount === amount &&
74
+ existing . receiverAddress === receiverAddress &&
75
+ ( existing . status === 'claimed' || existing . status === 'started' )
76
+ ) {
77
+ logger . info (
78
+ `Skipping duplicate teleport request for subaccount ${ subaccountId } , memo "${ memo } " ` +
79
+ `(amount=${ amount } , receiver=${ receiverAddress } ). Current status: ${ existing . status } .` ,
80
+ ) ;
81
+ return ;
82
+ }
83
+
57
84
const teleport : Teleport = {
58
85
amount,
59
86
subaccountId,
60
87
dateRequested : new Date ( ) . toISOString ( ) ,
61
88
status : 'claimed' ,
62
89
receiverAddress,
90
+ memo,
63
91
} ;
64
- logger . info ( ' Requesting teleport:' , teleport ) ;
65
- this . teleports . set ( subaccountId , teleport ) ;
92
+ logger . info ( ` Requesting teleport " ${ compositeKey } ":` , teleport ) ;
93
+ this . teleports . set ( compositeKey , teleport ) ;
66
94
this . maybeStartPeriodicChecks ( ) ;
67
95
}
68
96
69
- private async startTeleport ( subaccountId : string ) : Promise < void > {
70
- const teleport = this . teleports . get ( subaccountId ) ;
97
+ public cancelPendingTeleport ( subaccountId : string , memo : string ) : void {
98
+ const compositeKey = this . getCompositeKey ( subaccountId , memo ) ;
99
+ const pending = this . teleports . get ( compositeKey ) ;
100
+ if ( pending ) {
101
+ this . teleports . delete ( compositeKey ) ;
102
+ logger . info ( `Cancelled pending teleport for key "${ compositeKey } " (subaccount ${ subaccountId } , memo "${ memo } ").` ) ;
103
+ } else {
104
+ logger . info (
105
+ `No pending teleport found to cancel for key "${ compositeKey } " (subaccount ${ subaccountId } , memo "${ memo } ").` ,
106
+ ) ;
107
+ }
108
+ }
109
+
110
+ private async startTeleport ( compositeKey : string ) : Promise < void > {
111
+ const teleport = this . teleports . get ( compositeKey ) ;
71
112
72
- // Ignore operation
73
113
if ( teleport === undefined ) {
74
- throw new Error ( 'Teleport not found. This cannot not happen.' ) ;
114
+ logger . error ( `Teleport not found for key "${ compositeKey } " at startTeleport. This should not happen.` ) ;
115
+ return ;
75
116
}
76
117
77
118
if ( teleport . status !== 'arrived' ) {
78
- throw new Error ( 'Teleport not in arrived state.' ) ;
119
+ logger . warn ( `Teleport "${ compositeKey } " not in 'arrived' state.` ) ;
120
+ return ;
79
121
}
80
122
81
- logger . info ( ' Starting teleport:' , teleport ) ;
123
+ logger . info ( ` Starting teleport " ${ compositeKey } ":` , teleport ) ;
82
124
const fastQuoteParams : FastQuoteQueryParams = {
83
125
subaccountId : teleport . subaccountId ,
84
126
operation : 'swap' ,
@@ -91,31 +133,35 @@ export class BrlaTeleportService {
91
133
92
134
try {
93
135
const { token : quoteToken } = await this . brlaApiService . createFastQuote ( fastQuoteParams ) ;
94
- this . teleports . set ( subaccountId , { ...teleport , status : 'quoted' } ) ;
136
+ const quotedTeleportState : Teleport = { ...teleport , status : 'quoted' } ;
137
+ this . teleports . set ( compositeKey , quotedTeleportState ) ;
138
+ logger . info ( `Teleport ${ compositeKey } status: quoted` ) ;
95
139
96
- // Execute the actual swap operation
97
140
const { id } = await this . brlaApiService . swapRequest ( {
98
141
token : quoteToken ,
99
- receiverAddress : teleport . receiverAddress ,
142
+ receiverAddress : quotedTeleportState . receiverAddress ,
100
143
} ) ;
101
- this . teleports . set ( subaccountId , { ...teleport , status : 'started' , id } ) ;
144
+
145
+ const startedTeleportState : Teleport = { ...quotedTeleportState , status : 'started' , id } ;
146
+ this . teleports . set ( compositeKey , startedTeleportState ) ;
147
+ logger . info ( `Teleport ${ compositeKey } status: started, API operationId: ${ id } ` ) ;
102
148
103
149
this . maybeStartPeriodicChecks ( ) ;
104
150
} catch ( e ) {
105
- logger . error ( ' Error starting teleport:' , e ) ;
106
- this . teleports . set ( subaccountId , { ...teleport , status : 'failed' } ) ;
151
+ logger . error ( ` Error starting teleport " ${ compositeKey } ":` , e ) ;
152
+ this . teleports . set ( compositeKey , { ...teleport , status : 'failed' } ) ;
107
153
}
108
154
}
109
155
110
156
private maybeStartPeriodicChecks ( ) : void {
111
- const pendingTeleports = [ ...this . teleports . entries ( ) ] . filter (
112
- ( entry ) => entry [ 1 ] . status === 'claimed' || entry [ 1 ] . status === 'started' ,
157
+ const pendingTeleports = [ ...this . teleports . values ( ) ] . filter (
158
+ ( teleport ) => teleport . status === 'claimed' || teleport . status === 'started' ,
113
159
) . length ;
114
160
115
161
if ( this . checkInterval === null && pendingTeleports > 0 ) {
116
162
this . checkInterval = setInterval ( ( ) => {
117
163
this . checkPendingTeleports ( ) . catch ( ( err ) => {
118
- console . error ( 'Error in periodic teleport check:' , err ) ;
164
+ logger . error ( 'Error in periodic teleport check:' , err ) ;
119
165
} ) ;
120
166
} , this . intervalMs ) ;
121
167
}
@@ -125,55 +171,72 @@ export class BrlaTeleportService {
125
171
if ( this . teleports . size === 0 && this . checkInterval !== null ) {
126
172
clearInterval ( this . checkInterval ) ;
127
173
this . checkInterval = null ;
174
+ return ;
128
175
}
129
176
130
- this . teleports . forEach ( async ( teleport , subaccountId ) => {
177
+ // Process 'started' teleports first
178
+ for ( const [ compositeKey , teleport ] of this . teleports ) {
131
179
if ( teleport . status === 'started' ) {
132
- const onChainOuts = await this . brlaApiService . getOnChainHistoryOut ( subaccountId ) ;
133
- const relevantOut = onChainOuts . find ( ( out : OnchainLog ) => out . id === teleport . id ) ;
134
-
135
- if ( ! relevantOut ) {
136
- return ;
137
- }
138
-
139
- // We are interested in the last contract op for this operation being
140
- // a mint one. smartContractOps are ordered descending by timestamp.
141
- const lastContractOp = relevantOut . smartContractOps [ 0 ] ;
142
-
143
- if ( lastContractOp . operationName === SmartContractOperationType . MINT && lastContractOp . executed === true ) {
144
- this . completedTeleports . set ( subaccountId , { ...teleport , status : 'completed' } ) ;
145
- logger . info ( 'Teleport completed:' , teleport ) ;
146
- this . teleports . delete ( subaccountId ) ;
180
+ try {
181
+ const onChainOuts = await this . brlaApiService . getOnChainHistoryOut ( teleport . subaccountId ) ;
182
+ const relevantOut = onChainOuts . find ( ( out : OnchainLog ) => out . id === teleport . id ) ;
183
+
184
+ if ( ! relevantOut ) {
185
+ return ;
186
+ }
187
+
188
+ // We are interested in the last contract op for this operation being
189
+ // a mint one. smartContractOps are ordered descending by timestamp.
190
+ const lastContractOp = relevantOut . smartContractOps [ 0 ] ;
191
+
192
+ if (
193
+ lastContractOp &&
194
+ lastContractOp . operationName === SmartContractOperationType . MINT &&
195
+ lastContractOp . executed === true
196
+ ) {
197
+ const completedTeleport = { ...teleport , status : 'completed' as TeleportStatus } ;
198
+ this . completedTeleports . set ( compositeKey , completedTeleport ) ;
199
+ logger . info ( `Teleport completed "${ compositeKey } ":` , completedTeleport ) ;
200
+ this . teleports . delete ( compositeKey ) ;
201
+ }
202
+ } catch ( error ) {
203
+ logger . error ( `Error checking 'started' teleport ${ compositeKey } :` , error ) ;
147
204
}
148
205
}
149
- } ) ;
206
+ }
150
207
151
- this . teleports . forEach ( async ( teleport , subaccountId ) => {
152
- // For claimed teleports, we need to wait for funds to arrive, only then it makes sense to
153
- // start the actual transfer process.
208
+ // Process 'claimed' teleports
209
+ for ( const [ compositeKey , teleport ] of this . teleports ) {
154
210
if ( teleport . status === 'claimed' ) {
155
- const payIns = await this . brlaApiService . getPayInHistory ( subaccountId ) ;
156
- // TODO we also need to check actual balance, which has to be gte what is requested.
157
- if ( payIns . length === 0 ) {
158
- return ;
159
- }
160
-
161
- // Must be the last one, if any.
162
- const lastPayIn = payIns [ 0 ] ;
163
- // Check the referceLabel to match the address requested, and amount.
164
- // Last mintOp should match the amount.
165
- if (
166
- verifyReferenceLabel ( lastPayIn . referenceLabel , teleport . receiverAddress ) // TODO testing && lastPayIn.mintOps[0].amount === teleport.amount
167
- ) {
168
- this . teleports . set ( subaccountId , { ...teleport , status : 'arrived' } ) ;
169
- this . startTeleport ( subaccountId ) ;
170
- }
171
-
172
- // delete teleports that have been waiting for more than 15 minutes
173
- if ( teleport . status === 'claimed' && Date . now ( ) - new Date ( teleport . dateRequested ) . getTime ( ) > 15 * 60 * 1000 ) {
174
- this . teleports . delete ( subaccountId ) ;
211
+ try {
212
+ const payIns = await this . brlaApiService . getPayInHistory ( teleport . subaccountId ) ;
213
+ if ( payIns . length === 0 ) {
214
+ return ;
215
+ }
216
+
217
+ // Must be the last one, if any.
218
+ const matchingPayIn = payIns . find (
219
+ ( payIn ) =>
220
+ verifyReferenceLabel ( payIn . referenceLabel , teleport . memo ) &&
221
+ Number ( payIn . amount ) >= Number ( teleport . amount ) ,
222
+ ) ;
223
+
224
+ if ( matchingPayIn ) {
225
+ logger . info ( `Matching PayIn found for teleport "${ compositeKey } ". Status changing to 'arrived'.` ) ;
226
+ this . teleports . set ( compositeKey , { ...teleport , status : 'arrived' } ) ;
227
+ // Intentionally not awaiting startTeleport to allow checkPendingTeleports to complete its iteration.
228
+ // startTeleport is async and will handle its own errors.
229
+ this . startTeleport ( compositeKey ) . catch ( ( err ) => {
230
+ logger . error (
231
+ `Error occurred during startTeleport called for ${ compositeKey } from checkPendingTeleports:` ,
232
+ err ,
233
+ ) ;
234
+ } ) ;
235
+ } // Deletion of teleports is handled by the phase processor.
236
+ } catch ( error ) {
237
+ logger . error ( `Error checking 'claimed' teleport ${ compositeKey } :` , error ) ;
175
238
}
176
239
}
177
- } ) ;
240
+ }
178
241
}
179
242
}
0 commit comments