Skip to content

Commit dca594c

Browse files
committed
feat: transaction error handling
1 parent cfd3f46 commit dca594c

File tree

5 files changed

+270
-111
lines changed

5 files changed

+270
-111
lines changed

src/common/njord.ts

Lines changed: 179 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Currency,
66
EntityType,
77
GetBalanceResponse,
8+
TransferStatus,
89
TransferType,
910
type BalanceChange,
1011
type TransferResult,
@@ -25,7 +26,12 @@ import {
2526
} from '../redis';
2627
import { generateStorageKey, StorageKey, StorageTopic } from '../config';
2728
import { coresBalanceExpirationSeconds } from './constants';
28-
import { ConflictError, NjordErrorMessages } from '../errors';
29+
import {
30+
ConflictError,
31+
NjordErrorMessages,
32+
throwUserTransactionError,
33+
TransferError,
34+
} from '../errors';
2935
import { GarmrService } from '../integrations/garmr';
3036
import { BrokenCircuitError } from 'cockatiel';
3137
import type { EntityManager } from 'typeorm';
@@ -53,6 +59,9 @@ const garmNjordService = new GarmrService({
5359
threshold: 0.1,
5460
duration: 10 * 1000,
5561
},
62+
retryOpts: {
63+
maxAttempts: 1,
64+
},
5665
});
5766

5867
export const getNjordClient = (clientTransport = transport) => {
@@ -146,27 +155,30 @@ export const transferCores = createAuthProtectedFn(
146155

147156
const njordClient = getNjordClient();
148157

149-
const transferResult = await garmNjordService.execute(async () => {
150-
if (!transaction.id) {
151-
throw new Error('No transaction id');
152-
}
158+
if (!transaction.id) {
159+
throw new Error('No transaction id');
160+
}
153161

154-
if (!transaction.senderId) {
155-
throw new Error('No sender id');
156-
}
162+
if (!transaction.senderId) {
163+
throw new Error('No sender id');
164+
}
165+
166+
const senderId = transaction.senderId;
167+
const receiverId = transaction.receiverId;
157168

158-
const { results } = await njordClient.transfer({
169+
const response = await garmNjordService.execute(async () => {
170+
const response = await njordClient.transfer({
159171
idempotencyKey: transaction.id,
160172
transfers: [
161173
{
162174
transferType: TransferType.TRANSFER,
163175
currency: Currency.CORES,
164176
sender: {
165-
id: transaction.senderId,
177+
id: senderId,
166178
type: EntityType.USER,
167179
},
168180
receiver: {
169-
id: transaction.receiverId,
181+
id: receiverId,
170182
type: EntityType.USER,
171183
},
172184
amount: transaction.value,
@@ -176,47 +188,52 @@ export const transferCores = createAuthProtectedFn(
176188
},
177189
],
178190
});
179-
// we always have single transfer
180-
const result = results.find(
181-
(item) => item.transferType === TransferType.TRANSFER,
182-
);
183191

184-
if (!result) {
185-
throw new Error('No transfer result');
186-
}
192+
return response;
193+
});
187194

188-
await Promise.allSettled([
189-
[
190-
parseBalanceUpdate({
191-
balance: result.senderBalance,
192-
userId: transaction.senderId,
193-
}),
194-
parseBalanceUpdate({
195-
balance: result.receiverBalance,
196-
userId: transaction.receiverId,
197-
}),
198-
].map(async (balanceUpdate) => {
199-
if (!balanceUpdate) {
200-
return;
201-
}
195+
if (response.status !== TransferStatus.SUCCESS) {
196+
throw new TransferError(response);
197+
}
202198

203-
await updateBalanceCache({
204-
ctx: {
205-
userId: balanceUpdate.userId,
206-
},
207-
value: {
208-
amount: parseBigInt(balanceUpdate.balance.newBalance),
209-
},
210-
});
211-
}),
212-
]);
199+
const { results } = response;
213200

214-
// TODO feat/transactions error handling
201+
// we always have single transfer
202+
const result = results.find(
203+
(item) => item.transferType === TransferType.TRANSFER,
204+
);
215205

216-
return result;
217-
});
206+
if (!result) {
207+
throw new Error('No transfer result');
208+
}
209+
210+
await Promise.allSettled([
211+
[
212+
parseBalanceUpdate({
213+
balance: result.senderBalance,
214+
userId: transaction.senderId,
215+
}),
216+
parseBalanceUpdate({
217+
balance: result.receiverBalance,
218+
userId: transaction.receiverId,
219+
}),
220+
].map(async (balanceUpdate) => {
221+
if (!balanceUpdate) {
222+
return;
223+
}
224+
225+
await updateBalanceCache({
226+
ctx: {
227+
userId: balanceUpdate.userId,
228+
},
229+
value: {
230+
amount: parseBigInt(balanceUpdate.balance.newBalance),
231+
},
232+
});
233+
}),
234+
]);
218235

219-
return transferResult;
236+
return result;
220237
},
221238
);
222239

@@ -225,22 +242,22 @@ export const purchaseCores = async ({
225242
}: {
226243
transaction: UserTransaction;
227244
}): Promise<TransferResult> => {
228-
const transferResult = await garmNjordService.execute(async () => {
229-
if (!transaction.id) {
230-
throw new Error('No transaction id');
231-
}
245+
if (!transaction.id) {
246+
throw new Error('No transaction id');
247+
}
232248

233-
if (transaction.senderId) {
234-
throw new Error('Purchase cores transaction can not have sender');
235-
}
249+
if (transaction.senderId) {
250+
throw new Error('Purchase cores transaction can not have sender');
251+
}
236252

237-
if (transaction.productId) {
238-
throw new Error('Purchase cores transaction can not have product');
239-
}
253+
if (transaction.productId) {
254+
throw new Error('Purchase cores transaction can not have product');
255+
}
240256

241-
const njordClient = getNjordClient();
257+
const njordClient = getNjordClient();
242258

243-
const { results } = await njordClient.transfer({
259+
const response = await garmNjordService.execute(async () => {
260+
const response = await njordClient.transfer({
244261
idempotencyKey: transaction.id,
245262
transfers: [
246263
{
@@ -261,43 +278,48 @@ export const purchaseCores = async ({
261278
},
262279
],
263280
});
264-
// we always have single transfer
265-
const result = results.find(
266-
(item) => item.transferType === TransferType.TRANSFER,
267-
);
268281

269-
if (!result) {
270-
throw new Error('No transfer result');
271-
}
282+
return response;
283+
});
272284

273-
await Promise.allSettled([
274-
[
275-
parseBalanceUpdate({
276-
balance: result.receiverBalance,
277-
userId: transaction.receiverId,
278-
}),
279-
].map(async (balanceUpdate) => {
280-
if (!balanceUpdate) {
281-
return;
282-
}
285+
if (response.status !== TransferStatus.SUCCESS) {
286+
throw new TransferError(response);
287+
}
283288

284-
await updateBalanceCache({
285-
ctx: {
286-
userId: balanceUpdate.userId,
287-
},
288-
value: {
289-
amount: parseBigInt(balanceUpdate.balance.newBalance),
290-
},
291-
});
292-
}),
293-
]);
289+
const { results } = response;
290+
291+
// we always have single transfer
292+
const result = results.find(
293+
(item) => item.transferType === TransferType.TRANSFER,
294+
);
294295

295-
// TODO feat/transactions error handling
296+
if (!result) {
297+
throw new Error('No transfer result');
298+
}
296299

297-
return result;
298-
});
300+
await Promise.allSettled([
301+
[
302+
parseBalanceUpdate({
303+
balance: result.receiverBalance,
304+
userId: transaction.receiverId,
305+
}),
306+
].map(async (balanceUpdate) => {
307+
if (!balanceUpdate) {
308+
return;
309+
}
299310

300-
return transferResult;
311+
await updateBalanceCache({
312+
ctx: {
313+
userId: balanceUpdate.userId,
314+
},
315+
value: {
316+
amount: parseBigInt(balanceUpdate.balance.newBalance),
317+
},
318+
});
319+
}),
320+
]);
321+
322+
return result;
301323
};
302324

303325
export type GetBalanceProps = Pick<AuthContext, 'userId'>;
@@ -482,12 +504,24 @@ export const awardUser = async (
482504
note,
483505
});
484506

485-
const transfer = await transferCores({
486-
ctx,
487-
transaction,
488-
});
507+
try {
508+
const transfer = await transferCores({
509+
ctx,
510+
transaction,
511+
});
512+
513+
return { transaction, transfer };
514+
} catch (error) {
515+
if (error instanceof TransferError) {
516+
await throwUserTransactionError({
517+
entityManager,
518+
error,
519+
transaction,
520+
});
521+
}
489522

490-
return { transaction, transfer };
523+
throw error;
524+
}
491525
},
492526
);
493527

@@ -581,12 +615,28 @@ export const awardPost = async (
581615
)
582616
.execute();
583617

584-
const transfer = await transferCores({
585-
ctx,
586-
transaction,
587-
});
618+
try {
619+
const transfer = await transferCores({
620+
ctx,
621+
transaction,
622+
});
623+
624+
return { transaction, transfer };
625+
} catch (error) {
626+
if (error instanceof TransferError) {
627+
await entityManager.getRepository(UserPost).delete({
628+
awardTransactionId: transaction.id,
629+
});
630+
631+
await throwUserTransactionError({
632+
entityManager,
633+
error,
634+
transaction,
635+
});
636+
}
588637

589-
return { transaction, transfer };
638+
throw error;
639+
}
590640
},
591641
);
592642

@@ -679,10 +729,12 @@ export const awardComment = async (
679729
)
680730
.execute();
681731

732+
let newComment: Comment | undefined;
733+
682734
if (note) {
683735
const post = await comment.post;
684736

685-
const newComment = entityManager.getRepository(Comment).create({
737+
newComment = entityManager.getRepository(Comment).create({
686738
id: await generateShortId(),
687739
postId: comment.postId,
688740
parentId: comment.parentId || comment.id,
@@ -709,12 +761,34 @@ export const awardComment = async (
709761
await saveComment(entityManager, newComment, post.sourceId);
710762
}
711763

712-
const transfer = await transferCores({
713-
ctx,
714-
transaction,
715-
});
764+
try {
765+
const transfer = await transferCores({
766+
ctx,
767+
transaction,
768+
});
769+
770+
return { transaction, transfer };
771+
} catch (error) {
772+
if (error instanceof TransferError) {
773+
if (newComment) {
774+
await entityManager.getRepository(Comment).delete({
775+
id: newComment.id,
776+
});
777+
}
778+
779+
await entityManager.getRepository(UserComment).delete({
780+
awardTransactionId: transaction.id,
781+
});
716782

717-
return { transaction, transfer };
783+
await throwUserTransactionError({
784+
entityManager,
785+
error,
786+
transaction,
787+
});
788+
}
789+
790+
throw error;
791+
}
718792
},
719793
);
720794

0 commit comments

Comments
 (0)