@@ -80,111 +80,109 @@ export async function applyContractEvent(
8080) : Promise < void > {
8181 validateEvent ( event ) ;
8282
83- if ( event . type === 'MembershipMinted' ) {
84- const wallet = event . to . toLowerCase ( ) ;
85- const expiresAt = new Date ( event . expiresAt * 1000 ) ;
83+ // Access-affecting writes must be atomic.
84+ await prisma . $transaction ( async ( tx ) => {
85+ if ( event . type === 'MembershipMinted' ) {
86+ const wallet = event . to . toLowerCase ( ) ;
87+ const expiresAt = new Date ( event . expiresAt * 1000 ) ;
8688
87- // Ensure wallet exists
88- const walletRecord = await prisma . wallet . upsert ( {
89- where : { address : wallet } ,
90- update : { } ,
91- create : { address : wallet } ,
92- } ) ;
89+ // Ensure wallet exists
90+ const walletRecord = await tx . wallet . upsert ( {
91+ where : { address : wallet } ,
92+ update : { } ,
93+ create : { address : wallet } ,
94+ } ) ;
9395
94- // Ensure community exists
95- await prisma . community . upsert ( {
96- where : { id : event . communityId } ,
97- update : { } ,
98- create : {
99- id : event . communityId ,
100- name : `${ event . communityId } Community` ,
101- } ,
102- } ) ;
96+ // Ensure community exists
97+ await tx . community . upsert ( {
98+ where : { id : event . communityId } ,
99+ update : { } ,
100+ create : {
101+ id : event . communityId ,
102+ name : `${ event . communityId } Community` ,
103+ } ,
104+ } ) ;
103105
104- // Ensure member exists in community
105- const member = await prisma . member . upsert ( {
106- where : {
107- communityId_walletId : {
106+ // Ensure member exists in community
107+ const member = await tx . member . upsert ( {
108+ where : {
109+ communityId_walletId : {
110+ communityId : event . communityId ,
111+ walletId : walletRecord . id ,
112+ } ,
113+ } ,
114+ update : { } ,
115+ create : {
108116 communityId : event . communityId ,
109117 walletId : walletRecord . id ,
110118 } ,
111- } ,
112- update : { } ,
113- create : {
114- communityId : event . communityId ,
115- walletId : walletRecord . id ,
116- } ,
117- } ) ;
119+ } ) ;
118120
119- // Create or update membership
120- // Note: If a member receives a second MembershipMinted for the same community,
121- // this updates their existing membership (replacing the tokenId and resetting state)
122- await prisma . membership . upsert ( {
123- where : { memberId : member . id } ,
124- update : {
125- tokenId : event . tokenId ,
126- state : 'active' ,
127- expiresAt,
128- renewedAt : new Date ( ) ,
129- } ,
130- create : {
131- memberId : member . id ,
132- tokenId : event . tokenId ,
133- state : 'active' ,
134- expiresAt,
135- } ,
136- } ) ;
137- } else if ( event . type === 'MembershipRenewed' ) {
138- // Find membership by tokenId and update expiry.
139- // Contract events don't include communityId in this decoded type, so we
140- // update by membership.id after resolving the membership record.
141- const membership = await prisma . membership . findFirst ( {
142- where : {
143- tokenId : event . tokenId ,
144- } ,
145- } ) ;
121+ // Create or update membership
122+ // Note: If a member receives a second MembershipMinted for the same community,
123+ // this updates their existing membership (replacing the tokenId and resetting state)
124+ await tx . membership . upsert ( {
125+ where : { memberId : member . id } ,
126+ update : {
127+ tokenId : event . tokenId ,
128+ state : 'active' ,
129+ expiresAt,
130+ renewedAt : new Date ( ) ,
131+ } ,
132+ create : {
133+ memberId : member . id ,
134+ tokenId : event . tokenId ,
135+ state : 'active' ,
136+ expiresAt,
137+ } ,
138+ } ) ;
139+ } else if ( event . type === 'MembershipRenewed' ) {
140+ const membership = await tx . membership . findFirst ( {
141+ where : {
142+ tokenId : event . tokenId ,
143+ } ,
144+ } ) ;
146145
146+ if ( ! membership ) {
147+ throw new Error (
148+ `Cannot renew membership: tokenId ${ event . tokenId } not found in database` ,
149+ ) ;
150+ }
147151
148- if ( ! membership ) {
149- throw new Error (
150- `Cannot renew membership: tokenId ${ event . tokenId } not found in database` ,
151- ) ;
152- }
152+ const newExpiresAt = new Date ( event . newExpiresAt * 1000 ) ;
153+ await tx . membership . update ( {
154+ where : { id : membership . id } ,
155+ data : {
156+ expiresAt : newExpiresAt ,
157+ renewedAt : new Date ( ) ,
158+ } ,
159+ } ) ;
160+ } else if ( event . type === 'MembershipSuspended' ) {
161+ const membership = await tx . membership . findFirst ( {
162+ where : {
163+ tokenId : event . tokenId ,
164+ } ,
165+ include : { member : { select : { communityId : true } } } ,
166+ } ) ;
153167
154- const newExpiresAt = new Date ( event . newExpiresAt * 1000 ) ;
155- await prisma . membership . update ( {
156- where : { id : membership . id } ,
157- data : {
158- expiresAt : newExpiresAt ,
159- renewedAt : new Date ( ) ,
160- } ,
161- } ) ;
162- } else if ( event . type === 'MembershipSuspended' ) {
163- // Find membership by tokenId and update suspension state.
164- // Same rationale as in renew: we resolve membership first, then update by membership.id.
165- const membership = await prisma . membership . findFirst ( {
166- where : {
167- tokenId : event . tokenId ,
168- } ,
169- include : { member : { select : { communityId : true } } } ,
170- } ) ;
168+ if ( ! membership ) {
169+ throw new Error (
170+ `Cannot suspend membership: tokenId ${ event . tokenId } not found in database` ,
171+ ) ;
172+ }
171173
172- if ( ! membership ) {
173- throw new Error (
174- `Cannot suspend membership: tokenId ${ event . tokenId } not found in database` ,
175- ) ;
174+ await tx . membership . update ( {
175+ where : { id : membership . id } ,
176+ data : {
177+ state : event . isSuspended ? 'suspended' : 'active' ,
178+ } ,
179+ } ) ;
176180 }
177-
178- await prisma . membership . update ( {
179- where : { id : membership . id } ,
180- data : {
181- state : event . isSuspended ? 'suspended' : 'active' ,
182- } ,
183- } ) ;
184- }
181+ } ) ;
185182}
186183
187184
185+
188186/**
189187 * Apply multiple contract events in order
190188 *
0 commit comments