Skip to content

Commit cea74b2

Browse files
authored
Merge pull request #280 from Meme11030/feat/transaction-idempotency-key
feat(transactions): add idempotency key support
2 parents 5ad0ed3 + 1804431 commit cea74b2

5 files changed

Lines changed: 55 additions & 17 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- AlterTable
2+
ALTER TABLE "Transaction" ADD COLUMN "idempotencyKey" TEXT;
3+
4+
-- CreateIndex
5+
CREATE UNIQUE INDEX "Transaction_idempotencyKey_key" ON "Transaction"("idempotencyKey");
6+
7+
-- CreateIndex
8+
CREATE INDEX "Transaction_idempotencyKey_idx" ON "Transaction"("idempotencyKey");

prisma/schema.prisma

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,9 @@ model Transaction {
645645
646646
/// Metadata for future webhooks and analytics
647647
metadata Json? // Flexible JSON for additional transaction context
648+
649+
/// Client-supplied idempotency key to prevent duplicate submissions
650+
idempotencyKey String? @unique
648651
649652
/// Operational metadata
650653
createdAt DateTime @default(now())
@@ -658,4 +661,5 @@ model Transaction {
658661
@@index([stellarHash])
659662
@@index([stellarLedger])
660663
@@index([createdAt])
664+
@@index([idempotencyKey])
661665
}

src/transactions/dto/create-transaction.dto.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ export class CreateTransactionDto {
1010
senderWalletId: string;
1111
receiverWalletId?: string;
1212
metadata?: Record<string, any>;
13+
/** Optional client-supplied key to prevent duplicate submissions */
14+
idempotencyKey?: string;
1315
}

src/transactions/entities/transaction.entity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export class Transaction {
3030

3131
metadata?: Record<string, any> | null;
3232

33+
idempotencyKey?: string | null;
34+
3335
createdAt: Date;
3436
updatedAt: Date;
3537
}

src/transactions/transactions.service.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,34 @@ export class TransactionsService {
3333
) {}
3434

3535
/**
36-
* Create a new transaction in PENDING state
36+
* Create a new transaction in PENDING state.
37+
* If an idempotencyKey is supplied and a transaction with that key already
38+
* exists, the existing transaction is returned without creating a duplicate.
3739
*/
3840
async create(
3941
createTransactionDto: CreateTransactionDto,
4042
): Promise<TransactionEntity> {
41-
const { amount, asset, senderWalletId, receiverWalletId, metadata } =
42-
createTransactionDto;
43+
const {
44+
amount,
45+
asset,
46+
senderWalletId,
47+
receiverWalletId,
48+
metadata,
49+
idempotencyKey,
50+
} = createTransactionDto;
51+
52+
// Idempotency check: return existing transaction if key already used
53+
if (idempotencyKey) {
54+
const existing = await this.prisma.transaction.findUnique({
55+
where: { idempotencyKey },
56+
});
57+
if (existing) {
58+
this.logger.log(
59+
`Idempotency hit for key ${idempotencyKey}, returning existing transaction ${existing.id}`,
60+
);
61+
return this.mapPrismaToEntity(existing);
62+
}
63+
}
4364

4465
// Validate wallets exist
4566
const senderWallet = await this.prisma.wallet.findUnique({
@@ -83,20 +104,20 @@ export class TransactionsService {
83104
}
84105

85106
// Create transaction in database
86-
const created = await this.prisma.transaction.create({
87-
data: {
88-
amount,
89-
assetType: asset.type,
90-
assetCode: asset.code ?? null,
91-
assetIssuer: asset.issuer ?? null,
92-
senderWalletId,
93-
receiverWalletId: receiverWalletId ?? null,
94-
status: TransactionStatus.PENDING,
95-
metadata: metadata ?? null,
96-
},
97-
});
98-
99-
this.logger.log(`Created transaction ${created.id} in PENDING state`);
107+
try {
108+
const created = await this.prisma.transaction.create({
109+
data: {
110+
amount,
111+
assetType: asset.type,
112+
assetCode: asset.code ?? null,
113+
assetIssuer: asset.issuer ?? null,
114+
senderWalletId,
115+
receiverWalletId: receiverWalletId ?? null,
116+
status: TransactionStatus.PENDING,
117+
metadata: metadata ?? null,
118+
idempotencyKey: idempotencyKey ?? null,
119+
},
120+
});
100121

101122
this.webhookEventEmitter
102123
.emitTransactionCreated({
@@ -329,6 +350,7 @@ export class TransactionsService {
329350
confirmedAt: prismaTransaction.confirmedAt,
330351
failedAt: prismaTransaction.failedAt,
331352
metadata: prismaTransaction.metadata,
353+
idempotencyKey: prismaTransaction.idempotencyKey,
332354
createdAt: prismaTransaction.createdAt,
333355
updatedAt: prismaTransaction.updatedAt,
334356
};

0 commit comments

Comments
 (0)