Skip to content

Commit 908c649

Browse files
authored
Merge pull request #1028 from ogundelevictory/fix/issues-946-947-986-990
fix: webhook requeue, concurrent cap enforcement, asset dedup, README docs
2 parents 0af3345 + 5dfd512 commit 908c649

2 files changed

Lines changed: 68 additions & 37 deletions

File tree

backend/src/app.ts

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,6 +2467,20 @@ All errors return JSON with an \`error\` field and optional \`code\`:
24672467
}
24682468

24692469
try {
2470+
// Deduplicate assets by (code, issuer) before insert
2471+
const seen = new Set<string>();
2472+
const duplicates: string[] = [];
2473+
for (const asset of parsed.data.assets) {
2474+
const key = `${asset.code}:${asset.issuer ?? ""}`;
2475+
if (seen.has(key)) {
2476+
duplicates.push(`${asset.code}${asset.issuer ? ` (issuer ${asset.issuer})` : ""}`);
2477+
}
2478+
seen.add(key);
2479+
}
2480+
if (duplicates.length > 0) {
2481+
return sendError(res, 422, `Duplicate assets: ${duplicates.join(", ")}`);
2482+
}
2483+
24702484
await prisma.$transaction([
24712485
prisma.acceptedAsset.deleteMany({ where: { profileId: profile.id } }),
24722486
prisma.acceptedAsset.createMany({
@@ -2919,20 +2933,23 @@ All errors return JSON with an \`error\` field and optional \`code\`:
29192933
if (!profile) return;
29202934

29212935
try {
2922-
const result = await prisma.$transaction(async (tx) => {
2923-
const existingCount = await tx.webhook.count({
2924-
where: { profileId: profile.id },
2925-
});
2926-
if (existingCount >= 10) {
2927-
throw new Error("MAX_WEBHOOKS_EXCEEDED");
2928-
}
2936+
const result = await prisma.$transaction(
2937+
async (tx) => {
2938+
const existingCount = await tx.webhook.count({
2939+
where: { profileId: profile.id },
2940+
});
2941+
if (existingCount >= 10) {
2942+
throw new Error("MAX_WEBHOOKS_EXCEEDED");
2943+
}
29292944

2930-
const secret = randomBytes(32).toString("hex");
2931-
const webhook = await tx.webhook.create({
2932-
data: { url: parsed.data.url, secretHash: secret, profileId: profile.id },
2933-
});
2934-
return { webhook, secret };
2935-
});
2945+
const secret = randomBytes(32).toString("hex");
2946+
const webhook = await tx.webhook.create({
2947+
data: { url: parsed.data.url, secretHash: secret, profileId: profile.id },
2948+
});
2949+
return { webhook, secret };
2950+
},
2951+
{ isolationLevel: "Serializable" },
2952+
);
29362953

29372954
return res.status(201).json({ id: result.webhook.id, url: result.webhook.url, secret: result.secret });
29382955
} catch (err) {
@@ -3999,15 +4016,26 @@ All errors return JSON with an \`error\` field and optional \`code\`:
39994016
}
40004017

40014018
try {
4002-
const result = await prisma.webhookDelivery.updateMany({
4019+
const failed = await prisma.webhookDelivery.findMany({
40034020
where: { status: "failed" },
4021+
select: { id: true },
4022+
});
4023+
4024+
const result = await prisma.webhookDelivery.updateMany({
4025+
where: { id: { in: failed.map((d) => d.id) } },
40044026
data: {
40054027
status: "pending",
40064028
attemptCount: 0,
40074029
nextRetryAt: new Date(),
40084030
},
40094031
});
40104032

4033+
for (const delivery of failed) {
4034+
enqueueWebhookDelivery(delivery.id).catch((err) => {
4035+
req.log.warn({ deliveryId: delivery.id, err }, "Failed to enqueue requeued webhook");
4036+
});
4037+
}
4038+
40114039
req.log.info({ count: result.count }, "requeued failed webhooks");
40124040
return res.json({ count: result.count });
40134041
} catch (e: unknown) {
@@ -4104,26 +4132,29 @@ All errors return JSON with an \`error\` field and optional \`code\`:
41044132
);
41054133
}
41064134

4107-
const milestone = await prisma.$transaction(async (tx) => {
4108-
const activeCount = await tx.milestone.count({
4109-
where: { profileId: profile.id, status: { not: "reached" } },
4110-
});
4111-
if (activeCount >= 20) {
4112-
throw new Error("MAX_MILESTONES_EXCEEDED");
4113-
}
4135+
const milestone = await prisma.$transaction(
4136+
async (tx) => {
4137+
const activeCount = await tx.milestone.count({
4138+
where: { profileId: profile.id, status: { not: "reached" } },
4139+
});
4140+
if (activeCount >= 20) {
4141+
throw new Error("MAX_MILESTONES_EXCEEDED");
4142+
}
41144143

4115-
const created = await tx.milestone.create({
4116-
data: {
4117-
title: parsed.data.title,
4118-
description: parsed.data.description,
4119-
targetAmount: parsed.data.targetAmount,
4120-
assetCode: parsed.data.assetCode,
4121-
assetIssuer: parsed.data.assetIssuer ?? null,
4122-
profileId: profile.id,
4123-
},
4124-
});
4125-
return created;
4126-
});
4144+
const created = await tx.milestone.create({
4145+
data: {
4146+
title: parsed.data.title,
4147+
description: parsed.data.description,
4148+
targetAmount: parsed.data.targetAmount,
4149+
assetCode: parsed.data.assetCode,
4150+
assetIssuer: parsed.data.assetIssuer ?? null,
4151+
profileId: profile.id,
4152+
},
4153+
});
4154+
return created;
4155+
},
4156+
{ isolationLevel: "Serializable" },
4157+
);
41274158

41284159
res.status(201).json(milestone);
41294160
} catch (err) {

contract/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,11 @@ stellar contract invoke --network testnet-alt ...
222222
3. **Verify contract state:**
223223

224224
```bash
225-
# Check if contract is initialized
226-
stellar contract invoke --id <CONTRACT_ID> --network testnet -- is_initialized
225+
# Check contract support count
226+
stellar contract invoke --id <CONTRACT_ID> --network testnet -- support_count
227227

228-
# Check pause status
229-
stellar contract invoke --id <CONTRACT_ID> --network testnet -- is_paused
228+
# Check total by asset
229+
stellar contract invoke --id <CONTRACT_ID> --network testnet -- get_total_by_asset --asset_code XLM
230230
```
231231

232232
4. **Test with Stellar Laboratory:**

0 commit comments

Comments
 (0)