Skip to content

Commit

Permalink
feat(locksmith): API for eventcaster is now async (#15039)
Browse files Browse the repository at this point in the history
* wip

* adding file

* refactor

* more

* setting value from 1password

* wip

* adding async jobs

* cleaned up

* cleanup
  • Loading branch information
julien51 authored Nov 8, 2024
1 parent d22244c commit 0749772
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 100 deletions.
4 changes: 3 additions & 1 deletion locksmith/.op.env.production
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,6 @@ APPLE_WALLET_WWDR_CERT=op://secrets/apple-wallet/wwdr-cert
APPLE_WALLET_SIGNER_KEY_PASSPHRASE=op://secrets/apple-wallet/signer-key-passphrase

PRIVY_APP_ID=op://secrets/privy/app-id
PRIVY_APP_SECRET=op://secrets/privy/app-secret
PRIVY_APP_SECRET=op://secrets/privy/app-secret

EVENTCASTER_API_KEY=op://secrets/eventcaster/api-key
4 changes: 3 additions & 1 deletion locksmith/.op.env.staging
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ APPLE_WALLET_WWDR_CERT=op://secrets/apple-wallet/wwdr-cert
APPLE_WALLET_SIGNER_KEY_PASSPHRASE=op://secrets/apple-wallet/signer-key-passphrase

PRIVY_APP_ID=op://secrets/privy/staging-app-id
PRIVY_APP_SECRET=op://secrets/privy/staging-app-secret
PRIVY_APP_SECRET=op://secrets/privy/staging-app-secret

EVENTCASTER_API_KEY=op://secrets/eventcaster/api-key
81 changes: 46 additions & 35 deletions locksmith/__tests__/controllers/v2/eventCasterController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import app from '../../app'
import { Application } from '../../../src/models/application'
import { EVENT_CASTER_ADDRESS } from '../../../src/utils/constants'
import { ethers } from 'ethers'
import { addJob } from '../../../src/worker/worker'

const lockAddress = '0xce332211f030567bd301507443AD9240e0b13644'
const tokenId = 1337
Expand Down Expand Up @@ -42,6 +43,16 @@ vi.mock('../../../src/operations/eventCasterOperations', () => ({
deployLockForEventCaster: async () => {
return { address: lockAddress, network: 84532 }
},
getEventFormEventCaster: async () => {
return { contract: { network: 84532, address: lockAddress } }
},
mintNFTForRsvp: async () => {
return { id: tokenId, owner, network: 84532, address: lockAddress }
},
}))

vi.mock('../../../src/worker/worker', () => ({
addJob: vi.fn().mockResolvedValue(Promise.resolve(true)),
}))

// https://events.xyz/api/v1/event?event_id=195ede7f
Expand Down Expand Up @@ -258,58 +269,58 @@ describe('eventcaster endpoints', () => {
expect(response.status).toBe(403)
})

it('creates the contract and returns its address', async () => {
it('creates a job to deploy the contract', async () => {
const response = await request(app)
.post(`/v2/eventcaster/create-event`)
.set('Accept', 'json')
.set('Authorization', `Api-key ${eventCasterApplication.key}`)
.send(eventCasterEvent)

expect(response.status).toBe(201)
expect(response.body.address).toBe(lockAddress)
expect(response.status).toBe(204)
expect(addJob).toHaveBeenCalledWith('createEventCasterEvent', {
description: eventCasterEvent.description,
eventId: eventCasterEvent.id,
imageUrl: eventCasterEvent.image_url,
title: eventCasterEvent.title,
hosts: [
{
verified_addresses: {
eth_addresses: [
'0xdcf37d8Aa17142f053AAA7dc56025aB00D897a19',
'0x05e189E1BbaF77f1654F0983872fd938AE592eDD',
'0x70abdCd7A5A8Ff9cDef1ccA9eA15a5d315780986',
],
},
},
{
verified_addresses: {
eth_addresses: ['0xCEEd9585854F12F81A0103861b83b995A64AD915'],
},
},
],
})
})
})
describe('rsvp-for-event endpoint', () => {
it('mints the token and returns its id', async () => {
it('triggers the job to mint the NFT', async () => {
fetchMock.mockResponseOnce(
JSON.stringify({ success: true, event: eventCasterEvent })
)

const response = await request(app)
.post(`/v2/eventcaster/${eventCasterEvent.id}/rsvp`)
.set('Accept', 'json')
.set('Authorization', `Api-key ${eventCasterApplication.key}`)
.send(eventCasterRsvp)

expect(response.status).toBe(201)
expect(response.body.id).toBe(tokenId)
expect(response.body.owner).toBe(owner)
expect(response.body.network).toBe(eventCasterEvent.contract.network)
expect(response.body.address).toBe(eventCasterEvent.contract.address)
})
it('returns the existing token if one already exists', async () => {
fetchMock.mockResponseOnce(
JSON.stringify({ success: true, event: eventCasterEvent })
)

const response = await request(app)
.post(`/v2/eventcaster/${eventCasterEvent.id}/rsvp`)
.set('Accept', 'json')
.set('Authorization', `Api-key ${eventCasterApplication.key}`)
.send({
...eventCasterRsvp,
user: {
verified_addresses: {
eth_addresses: ['0xCEEd9585854F12F81A0103861b83b995A64AD915'],
},
},
})

expect(response.status).toBe(200)
expect(response.body.id).toBe(1337)
expect(response.body.owner).toBe(owner)
expect(response.body.network).toBe(eventCasterEvent.contract.network)
expect(response.body.address).toBe(eventCasterEvent.contract.address)
expect(response.status).toBe(204)
expect(addJob).toHaveBeenCalledWith('rsvpForEventCasterEvent', {
contract: {
address: lockAddress,
network: 84532,
},
eventId: eventCasterEvent.id,
farcasterId: eventCasterRsvp.user.fid,
ownerAddress: eventCasterRsvp.user.verified_addresses.eth_addresses[0],
})
})
})
describe('delete-event endpoint', () => {
Expand Down
66 changes: 66 additions & 0 deletions locksmith/__tests__/operations/eventCasterOperations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
saveContractOnEventCasterEvent,
saveTokenOnEventCasterRSVP,
} from '../../src/operations/eventCasterOperations'

describe('saveContractOnEventCasterEvent', () => {
beforeEach(() => {
fetchMock.mockResponseOnce(
JSON.stringify({
address: '0x662208945C988B1769d493d94e4DFdc9c681B6fF',
event_id: 'e7618561-dbb5-4b0d-81dd-5101e5c9729f',
network: 84532,
success: true,
}),
{
status: 200,
}
)
})
it("shoud call the EventCaster API to save the contract's address", async () => {
const result = await saveContractOnEventCasterEvent({
eventId: 'e76185',
contract: '0x662208945C988B1769d493d94e4DFdc9c681B6fF',
network: 84532,
})
expect(result).toEqual({
address: '0x662208945C988B1769d493d94e4DFdc9c681B6fF',
event_id: 'e7618561-dbb5-4b0d-81dd-5101e5c9729f',
network: 84532,
success: true,
})
})
})

describe('saveTokenOnEventCasterRSVP', () => {
beforeEach(() => {
fetchMock.mockResponseOnce(
JSON.stringify({
event_id: 'e7618561-dbb5-4b0d-81dd-5101e5c9729f',
fid: 6801,
rsvp_id: '360ad3c1-33d9-469a-a5b2-7785c88dd2b5',
success: true,
token_id: '5656',
}),
{
status: 200,
}
)
})

it('should update the token id for an RSVP to an event on EventCaster', async () => {
const result = await saveTokenOnEventCasterRSVP({
eventId: 'e76185',
farcasterId: '6801',
tokenId: '5656',
})
expect(result).toEqual({
event_id: 'e7618561-dbb5-4b0d-81dd-5101e5c9729f',
fid: 6801,
rsvp_id: '360ad3c1-33d9-469a-a5b2-7785c88dd2b5',
success: true,
token_id: '5656',
})
})
})
1 change: 1 addition & 0 deletions locksmith/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ const config = {
signerKeyPassphrase: process.env.APPLE_WALLET_SIGNER_KEY_PASSPHRASE,
privyAppId: process.env.PRIVY_APP_ID,
privyAppSecret: process.env.PRIVY_APP_SECRET,
eventCasterApiKey: process.env.EVENTCASTER_API_KEY,

logtailSourceToken: process.env.LOGTAIL,
sessionDuration: Number(process.env.SESSION_DURATION || 86400 * 60), // 60 days
Expand Down
79 changes: 19 additions & 60 deletions locksmith/src/controllers/v2/eventCasterController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import networks from '@unlock-protocol/networks'
import { WalletService, Web3Service } from '@unlock-protocol/unlock-js'
import { RequestHandler } from 'express'
import { z } from 'zod'
import {
getProviderForNetwork,
getPurchaser,
} from '../../fulfillment/dispatcher'
import { deployLockForEventCaster } from '../../operations/eventCasterOperations'
import { getEventFormEventCaster } from '../../operations/eventCasterOperations'
import { addJob } from '../../worker/worker'

// This is the API endpoint used by EventCaster to create events
const CreateEventBody = z.object({
Expand All @@ -29,9 +24,11 @@ const RsvpBody = z.object({
verified_addresses: z.object({
eth_addresses: z.array(z.string()),
}),
fid: z.number(),
}),
})

// Asynchronously creates an event on EventCaster
export const createEvent: RequestHandler = async (request, response) => {
const {
title,
Expand All @@ -40,90 +37,52 @@ export const createEvent: RequestHandler = async (request, response) => {
description,
image_url,
} = await CreateEventBody.parseAsync(request.body)
const { address, network } = await deployLockForEventCaster({
await addJob('createEventCasterEvent', {
title,
hosts,
eventId,
imageUrl: image_url,
description,
})
response.status(201).json({ address, network })
response.sendStatus(204)
return
}

// This is the API endpoint used by EventCaster to mint RSVP tokens
export const rsvpForEvent: RequestHandler = async (request, response) => {
const { user } = await RsvpBody.parseAsync(request.body)

// make the request to @event api
const eventCasterResponse = await fetch(
`https://events.xyz/api/v1/event?event_id=${request.params.eventId}`
)
// parse the response and continue
const { success, event } = await eventCasterResponse.json()

if (!success) {
response.status(422).json({ message: 'Could not retrieve event' })
return
}

if (!(event.contract?.address && event.contract?.network)) {
response
.status(422)
.json({ message: 'This event does not have a contract attached.' })
let event
try {
event = await getEventFormEventCaster(request.params.eventId)
} catch (error) {
response.status(422).json({ message: error.message })
return
}

// Get the recipient
if (!user.verified_addresses.eth_addresses[0]) {
const ownerAddress = user.verified_addresses.eth_addresses[0]
if (!ownerAddress) {
response
.status(422)
.json({ message: 'User does not have a verified address.' })
return
}

const [provider, wallet] = await Promise.all([
getProviderForNetwork(event.contract.network),
getPurchaser({ network: event.contract.network }),
])

const ownerAddress = user.verified_addresses.eth_addresses[0]
// Check first if the user has a key
const web3Service = new Web3Service(networks)
const existingKey = await web3Service.getKeyByLockForOwner(
event.contract.address,
await addJob('rsvpForEventCasterEvent', {
farcasterId: user.fid,
ownerAddress,
event.contract.network
)

if (existingKey.tokenId > 0) {
response.status(200).json({
network: event.contract.network,
address: event.contract.address,
id: Number(existingKey.tokenId),
owner: ownerAddress,
})
return
}

const walletService = new WalletService(networks)
await walletService.connect(provider, wallet)

const token = await walletService.grantKey({
lockAddress: event.contract.address,
recipient: ownerAddress,
contract: event.contract,
eventId: request.params.eventId,
})

response.status(201).json({
network: event.contract.network,
address: event.contract.address,
...token,
})
response.sendStatus(204)
return
}

// Deletes an event. Unsure how to proceed here...
export const deleteEvent: RequestHandler = async (_request, response) => {
// TODO: implement this
response.status(200).json({})
return
}
Expand Down
Loading

0 comments on commit 0749772

Please sign in to comment.