Skip to content

Commit

Permalink
🗃️ Switch to upsert for campaign + campaign bank
Browse files Browse the repository at this point in the history
Prevent potential concurrency issue when multiple actions are on the same block
  • Loading branch information
KONFeature committed Nov 29, 2024
1 parent 9ea3030 commit 911eda1
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 208 deletions.
4 changes: 3 additions & 1 deletion packages/ponder/ponder.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ export const productInteractionContractTable = onchainTable(
productId: t.bigint().notNull(),
referralTree: t.hex().notNull(),

lastUpdateBlock: t.bigint().notNull(),

createdTimestamp: t.bigint().notNull(),
lastUpdateTimestamp: t.bigint(),
lastUpdateTimestamp: t.bigint().notNull(),
removedTimestamp: t.bigint(),
}),
(table) => ({
Expand Down
4 changes: 2 additions & 2 deletions packages/ponder/src/api/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ ponder.get("/products/:id/banks", async (ctx) => {
}

// Perform the sql query
const administrators = await ctx.db
const banks = await ctx.db
.select({
address: bankingContractTable.id,
totalDistributed: bankingContractTable.totalDistributed,
Expand All @@ -71,7 +71,7 @@ ponder.get("/products/:id/banks", async (ctx) => {
.where(eq(bankingContractTable.productId, BigInt(id)));

// Return the result as json
return ctx.json(administrators);
return ctx.json(banks);
});

/**
Expand Down
168 changes: 82 additions & 86 deletions packages/ponder/src/campaign/campaignBank.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,18 @@
import * as console from "node:console";
import { ponder } from "@/generated";
import { erc20Abi, isAddressEqual } from "viem";
import { type Context, ponder } from "@/generated";
import { type Address, isAddressEqual } from "viem";
import { campaignBankAbi } from "../../abis/campaignAbis";
import {
bankingContractTable,
campaignTable,
tokenTable,
} from "../../ponder.schema";
import { bankingContractTable, campaignTable } from "../../ponder.schema";
import { upsertTokenIfNeeded } from "../token";

ponder.on(
"CampaignBanksFactory:CampaignBankCreated",
async ({ event, context: { client, db } }) => {
const address = event.args.campaignBank;

// If not found, find the token of this campaign
const [[productId, token], isDistributing] = await client.multicall({
contracts: [
{
abi: campaignBankAbi,
address,
functionName: "getConfig",
} as const,
{
abi: campaignBankAbi,
address,
functionName: "isDistributionEnabled",
} as const,
],
allowFailure: false,
async ({ event, context }) => {
await upsertCampaignBank({
address: event.args.campaignBank,
blockNumber: event.block.number,
context,
});

await db.insert(bankingContractTable).values({
id: address,
tokenId: token,
totalDistributed: 0n,
totalClaimed: 0n,
productId,
isDistributing,
});
// Create the token if needed
const tokenDb = await db.find(tokenTable, { id: token });
if (!tokenDb) {
try {
// Fetch a few onchain data
const [name, symbol, decimals] = await client.multicall({
contracts: [
{
abi: erc20Abi,
functionName: "name",
address: token,
},
{
abi: erc20Abi,
functionName: "symbol",
address: token,
},
{
abi: erc20Abi,
functionName: "decimals",
address: token,
},
] as const,
allowFailure: false,
});

// Create the token
await db.insert(tokenTable).values({
id: token,
name,
symbol,
decimals,
});
} catch (e) {
console.error(e, "Unable to fetch token data");
}
}
}
);

Expand Down Expand Up @@ -114,23 +51,82 @@ ponder.on(

ponder.on(
"CampaignBanks:DistributionStateUpdated",
async ({ event, context: { db } }) => {
// Find the interaction contract
const banking = await db.find(bankingContractTable, {
id: event.log.address,
async ({ event, context }) => {
// Upsert the campaign and set the distributing status
await upsertCampaignBank({
address: event.log.address,
blockNumber: event.block.number,
context,
onConflictUpdate: {
isDistributing: event.args.isDistributing,
},
});
if (!banking) {
console.error(`Banking contract not found: ${event.log.address}`);
return;
}
}
);

// Update the campaign
await db
async function upsertCampaignBank({
address,
blockNumber,
context,
onConflictUpdate = {},
}: {
address: Address;
blockNumber: bigint;
context: Context;
onConflictUpdate?: Partial<typeof bankingContractTable.$inferInsert>;
}) {
// Check if the campaign bank already exist, if yes just update it if we got stuff to update
const campaignBank = await context.db.find(bankingContractTable, {
id: address,
});
if (campaignBank) {
if (Object.keys(onConflictUpdate).length === 0) return;

await context.db
.update(bankingContractTable, {
id: event.log.address,
id: address,
})
.set({
isDistributing: event.args.isDistributing,
});
.set(onConflictUpdate);
return;
}
);

// If not found, find the token of this campaign
const [[productId, token], isDistributing] = await context.client.multicall(
{
contracts: [
{
abi: campaignBankAbi,
address,
functionName: "getConfig",
} as const,
{
abi: campaignBankAbi,
address,
functionName: "isDistributionEnabled",
} as const,
],
allowFailure: false,
blockNumber: blockNumber,
}
);

// And upsert it
await context.db
.insert(bankingContractTable)
.values({
id: address,
tokenId: token,
totalDistributed: 0n,
totalClaimed: 0n,
productId,
isDistributing,
...onConflictUpdate,
})
.onConflictDoUpdate(onConflictUpdate);

// Upsert the token if needed
await upsertTokenIfNeeded({
address: token,
context,
});
}
151 changes: 94 additions & 57 deletions packages/ponder/src/campaign/campaignCreation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as console from "node:console";
import { ponder } from "@/generated";
import { type Context, ponder } from "@/generated";
import type { Address } from "viem";
import {
interactionCampaignAbi,
referralCampaignAbi,
Expand All @@ -11,50 +11,86 @@ import { bytesToString } from "../utils/format";
/**
* On new campaign creation
*/
ponder.on(
"CampaignsFactory:CampaignCreated",
async ({ event, context: { client, db } }) => {
// Get the metadata and config of the campaign
const [metadataResult, linkResult, configResult] =
await client.multicall({
contracts: [
{
abi: interactionCampaignAbi,
address: event.args.campaign,
functionName: "getMetadata",
} as const,
{
abi: interactionCampaignAbi,
address: event.args.campaign,
functionName: "getLink",
} as const,
{
abi: referralCampaignAbi,
address: event.args.campaign,
functionName: "getConfig",
} as const,
],
allowFailure: true,
blockNumber: event.block.number,
});
ponder.on("CampaignsFactory:CampaignCreated", async ({ event, context }) => {
// Upsert the campaign
await upsertNewCampaign({
address: event.args.campaign,
blockNumber: event.block.number,
context,
});
});

if (
metadataResult.status !== "success" ||
linkResult.status !== "success"
) {
console.error(
`Failed to get metadata/linkResult for campaign ${event.args.campaign}`,
{ event }
);
return;
}
const [type, version, name] = metadataResult.result;
const [productId, interactionContract] = linkResult.result;
const formattedName = bytesToString(name);
/**
* Upsert a fresh campaign in the db
* @param address
* @param block
*/
export async function upsertNewCampaign({
address,
blockNumber,
context: { client, db },
onConflictUpdate = {},
}: {
address: Address;
blockNumber: bigint;
context: Context;
onConflictUpdate?: Partial<typeof campaignTable.$inferInsert>;
}) {
// If the campaign already exist, just update it
const campaign = await db.find(campaignTable, { id: address });
if (campaign) {
if (Object.keys(onConflictUpdate).length === 0) return;

await db
.update(campaignTable, {
id: address,
})
.set(onConflictUpdate);
return;
}

// Get the metadata and config of the campaign
const [metadataResult, linkResult, configResult] = await client.multicall({
contracts: [
{
abi: interactionCampaignAbi,
address,
functionName: "getMetadata",
} as const,
{
abi: interactionCampaignAbi,
address,
functionName: "getLink",
} as const,
{
abi: referralCampaignAbi,
address,
functionName: "getConfig",
} as const,
],
allowFailure: true,
blockNumber,
});

if (
metadataResult.status !== "success" ||
linkResult.status !== "success"
) {
console.error(
`Failed to get metadata/linkResult for campaign ${address}`,
{ blockNumber }
);
return;
}
const [type, version, name] = metadataResult.result;
const [productId, interactionContract] = linkResult.result;
const formattedName = bytesToString(name);

// Create the campaign
await db.insert(campaignTable).values({
id: event.args.campaign,
// Create the campaign
await db
.insert(campaignTable)
.values({
id: address,
type,
name: formattedName,
version,
Expand All @@ -67,18 +103,19 @@ ponder.on(
? configResult.result[2]
: undefined,
isAuthorisedOnBanking: false,
lastUpdateBlock: event.block.number,
});
lastUpdateBlock: blockNumber,
...onConflictUpdate,
})
.onConflictDoUpdate(onConflictUpdate);

// Upsert press campaign stats if it's the right type
if (type === "frak.campaign.referral") {
await db
.insert(referralCampaignStatsTable)
.values({
campaignId: event.args.campaign,
...emptyCampaignStats,
})
.onConflictDoNothing();
}
// Upsert press campaign stats if it's the right type
if (type === "frak.campaign.referral") {
await db
.insert(referralCampaignStatsTable)
.values({
campaignId: address,
...emptyCampaignStats,
})
.onConflictDoNothing();
}
);
}
Loading

0 comments on commit 911eda1

Please sign in to comment.