Skip to content

Commit 4e8ac11

Browse files
Rotorsoftrotorsoftclaude
authored
Implement prediction market SwapExecuted event pipeline (Slice 4) (#13390)
* feat: implement prediction market TokensMinted event pipeline Add the EVM event pipeline for prediction market mint operations: - Register TokensMinted event signature in eventSignatures.ts - Export BinaryVault contract source with ABI from @commonxyz/common-protocol-abis - Add predictionMarketTokensMintedMapper to decode raw EVM logs - Extend PredictionMarketProjection with TokensMinted handler that inserts trades, upserts positions, and increments market total_collateral - All DB mutations wrapped in a single Sequelize transaction - Idempotent: duplicate events (same tx hash) skip position/market updates - Add unit tests for mapper, projection handler, idempotency, and multi-user Completed GitHub issue #13387 Co-Authored-By: Claude <noreply@anthropic.com> * feat: implement prediction market SwapExecuted event pipeline Completed GitHub issue #13389 Co-Authored-By: Claude <noreply@anthropic.com> * fix: gate prediction market projection behind feature flag Conditionally register PredictionMarketProjection in rascalConsumerMap only when FLAG_FUTARCHY is enabled, preventing event consumption when the feature is disabled. Completed GitHub issue #13389 Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: rotorsoft <rotorsoft@outlook.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 20e6904 commit 4e8ac11

File tree

9 files changed

+555
-5
lines changed

9 files changed

+555
-5
lines changed

libs/evm-protocols/src/event-registry/eventRegistry.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CommunityStakeAbi,
66
ContestGovernorAbi,
77
ContestGovernorSingleAbi,
8+
FutarchyRouterAbi,
89
LPBondingCurveAbi,
910
LaunchpadAbi,
1011
NamespaceFactoryAbi,
@@ -125,6 +126,14 @@ export const binaryVaultSource: ContractSource = {
125126
eventSignatures: [EvmEventSignatures.PredictionMarket.TokensMinted],
126127
};
127128

129+
// FutarchyRouter is deployed per prediction market; addresses are stored in
130+
// EvmEventSources at deploy time. Exported for use by the EVM worker when
131+
// building contract sources from the DB.
132+
export const futarchyRouterSource: ContractSource = {
133+
abi: FutarchyRouterAbi,
134+
eventSignatures: [EvmEventSignatures.PredictionMarket.SwapExecuted],
135+
};
136+
128137
const tokenBondingCurveSource: ContractSource = {
129138
abi: TokenBondingCurveAbi,
130139
eventSignatures: [

libs/evm-protocols/src/event-registry/eventSignatures.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export const EvmEventSignatures = {
6262
PredictionMarket: {
6363
TokensMinted:
6464
'0xef616469a0b35ce807813d17c53c505b9d4796a93287cd361318dbca99ac9250',
65+
SwapExecuted:
66+
'0x6c3029970cad07cf2c4bef13d30bdb7b6b77093a579d543839b5386cb0184b03',
6567
},
6668
TokenCommunityManager: {
6769
CommunityNamespaceCreated:

libs/model/src/aggregates/prediction_market/PredictionMarket.projection.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const inputs = {
99
PredictionMarketProposalCreated: events.PredictionMarketProposalCreated,
1010
PredictionMarketMarketCreated: events.PredictionMarketMarketCreated,
1111
PredictionMarketTokensMinted: events.PredictionMarketTokensMinted,
12+
PredictionMarketSwapExecuted: events.PredictionMarketSwapExecuted,
1213
};
1314

1415
export function PredictionMarketProjection(): Projection<typeof inputs> {
@@ -120,6 +121,119 @@ export function PredictionMarketProjection(): Projection<typeof inputs> {
120121
);
121122
});
122123
},
124+
PredictionMarketSwapExecuted: async ({ payload }) => {
125+
const {
126+
market_id,
127+
eth_chain_id,
128+
transaction_hash,
129+
trader_address,
130+
buy_pass,
131+
amount_in,
132+
amount_out,
133+
timestamp,
134+
} = payload;
135+
136+
const market = await models.PredictionMarket.findOne({
137+
where: { market_id },
138+
});
139+
if (!market) {
140+
log.warn(
141+
`PredictionMarketSwapExecuted: market not found for market_id=${market_id}`,
142+
);
143+
return;
144+
}
145+
146+
await models.sequelize.transaction(async (transaction) => {
147+
const action = buy_pass
148+
? PredictionMarketTradeAction.SwapBuyPass
149+
: PredictionMarketTradeAction.SwapBuyFail;
150+
151+
// For trade record: map swap amounts to token fields
152+
// buyPass=true: spending f_tokens (amountIn), receiving p_tokens (amountOut)
153+
// buyPass=false: spending p_tokens (amountIn), receiving f_tokens (amountOut)
154+
const p_token_amount = buy_pass ? amount_out : amount_in;
155+
const f_token_amount = buy_pass ? amount_in : amount_out;
156+
157+
// Insert trade (idempotent via composite PK)
158+
const [, tradeCreated] =
159+
await models.PredictionMarketTrade.findOrCreate({
160+
where: { eth_chain_id, transaction_hash },
161+
defaults: {
162+
eth_chain_id,
163+
transaction_hash,
164+
prediction_market_id: market.id!,
165+
trader_address,
166+
action,
167+
collateral_amount: 0n,
168+
p_token_amount,
169+
f_token_amount,
170+
timestamp,
171+
},
172+
transaction,
173+
});
174+
175+
// Skip position/market updates if trade already existed (idempotency)
176+
if (!tradeCreated) return;
177+
178+
// Upsert position: swap exchanges one token for the other
179+
// buyPass=true: p_token += amount_out, f_token -= amount_in
180+
// buyPass=false: f_token += amount_out, p_token -= amount_in
181+
const [position, positionCreated] =
182+
await models.PredictionMarketPosition.findOrCreate({
183+
where: {
184+
prediction_market_id: market.id!,
185+
user_address: trader_address,
186+
},
187+
defaults: {
188+
prediction_market_id: market.id!,
189+
user_address: trader_address,
190+
p_token_balance: buy_pass ? amount_out : 0n,
191+
f_token_balance: buy_pass ? 0n : amount_out,
192+
total_collateral_in: 0n,
193+
},
194+
transaction,
195+
});
196+
197+
if (!positionCreated) {
198+
if (buy_pass) {
199+
await models.PredictionMarketPosition.update(
200+
{
201+
p_token_balance: models.sequelize.literal(
202+
`p_token_balance + ${amount_out}`,
203+
) as unknown as bigint,
204+
f_token_balance: models.sequelize.literal(
205+
`f_token_balance - ${amount_in}`,
206+
) as unknown as bigint,
207+
},
208+
{ where: { id: position.id }, transaction },
209+
);
210+
} else {
211+
await models.PredictionMarketPosition.update(
212+
{
213+
f_token_balance: models.sequelize.literal(
214+
`f_token_balance + ${amount_out}`,
215+
) as unknown as bigint,
216+
p_token_balance: models.sequelize.literal(
217+
`p_token_balance - ${amount_in}`,
218+
) as unknown as bigint,
219+
},
220+
{ where: { id: position.id }, transaction },
221+
);
222+
}
223+
}
224+
225+
// Update market probability from swap ratio
226+
const total = Number(amount_in) + Number(amount_out);
227+
const probability = buy_pass
228+
? Number(amount_out) / total
229+
: Number(amount_in) / total;
230+
231+
await models.PredictionMarket.update(
232+
{ current_probability: probability },
233+
{ where: { id: market.id }, transaction },
234+
);
235+
});
236+
},
123237
},
124238
};
125239
}

libs/model/src/services/evmChainEvents/chain-event-utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CommunityStakeAbi,
66
ContestGovernorAbi,
77
ContestGovernorSingleAbi,
8+
FutarchyRouterAbi,
89
LPBondingCurveAbi,
910
NamespaceFactoryAbi,
1011
ReferralFeeManagerAbi,
@@ -371,6 +372,30 @@ const predictionMarketTokensMintedMapper: EvmMapper<
371372
};
372373
};
373374

375+
const predictionMarketSwapExecutedMapper: EvmMapper<
376+
'PredictionMarketSwapExecuted'
377+
> = (event: EvmEvent) => {
378+
const decoded = decodeLog({
379+
abi: FutarchyRouterAbi,
380+
eventName: 'SwapExecuted',
381+
data: event.rawLog.data,
382+
topics: event.rawLog.topics,
383+
});
384+
return {
385+
event_name: 'PredictionMarketSwapExecuted',
386+
event_payload: {
387+
market_id: decoded.args.marketId,
388+
eth_chain_id: event.eventSource.ethChainId,
389+
transaction_hash: event.rawLog.transactionHash as `0x${string}`,
390+
trader_address: decoded.args.user,
391+
buy_pass: decoded.args.buyPass,
392+
amount_in: decoded.args.amountIn,
393+
amount_out: decoded.args.amountOut,
394+
timestamp: Number(event.block.timestamp),
395+
},
396+
};
397+
};
398+
374399
const communityNamespaceCreatedMapper: EvmMapper<
375400
'CommunityNamespaceCreated'
376401
> = (event: EvmEvent) => {
@@ -519,6 +544,8 @@ export const chainEventMappers: Record<string, EvmMapper<OutboxEvents>> = {
519544
// Prediction Markets
520545
[EvmEventSignatures.PredictionMarket.TokensMinted]:
521546
predictionMarketTokensMintedMapper,
547+
[EvmEventSignatures.PredictionMarket.SwapExecuted]:
548+
predictionMarketSwapExecutedMapper,
522549

523550
// TokenCommunityManager
524551
[EvmEventSignatures.TokenCommunityManager.CommunityNamespaceCreated]:

libs/model/test/prediction-market/prediction-market-mint.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ describe('Prediction Market Mint', () => {
104104
chainEventMappers[EvmEventSignatures.PredictionMarket.TokensMinted];
105105
expect(mapper).toBeDefined();
106106

107-
const traderAddress = '0x1234567890123456789012345678901234567890';
108107
const amount = 1000000000000000000n; // 1e18
109108

110109
const evmEvent: EvmEvent = {

0 commit comments

Comments
 (0)