Skip to content

Commit

Permalink
Merge pull request #2094 from dev-protocol/invitations
Browse files Browse the repository at this point in the history
Invitations
  • Loading branch information
stuartwk authored Mar 4, 2024
2 parents edb752e + 5f5b805 commit 753c1df
Show file tree
Hide file tree
Showing 10 changed files with 655 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ SEND_TX_API_KEY=
PUBLIC_WALLET_CONNECT_PROJECT_ID=

PUBLIC_ONDATO_VERIFICATION_URL=

SEND_DEVPROTOCOL_API_KEY=
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clubs",
"version": "0.12.3-beta.3",
"version": "0.12.2-beta.4",
"private": true,
"type": "module",
"scripts": {
Expand Down Expand Up @@ -97,6 +97,7 @@
"firebase-admin": "^12.0.0",
"jsonwebtoken": "^9.0.2",
"marked": "10.0.0",
"nanoid": "^5.0.5",
"p-queue": "^8.0.0",
"postcss": "8.4.35",
"ramda": "0.29.1",
Expand Down
158 changes: 158 additions & 0 deletions src/plugins/invitations/handlers/claim-invitation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { getDefaultClient } from '../redis'
import { parseUnits, verifyMessage } from 'ethers'
import { check } from './get-invitations-check'
import type {
ClubsFunctionGetPluginConfigById,
Membership,
} from '@devprotocol/clubs-core'
import {
whenNotErrorAll,
type UndefinedOr,
whenNotError,
} from '@devprotocol/util-ts'
import {
Index,
schemaInvitation,
uuidToQuery,
type Invitation,
} from '../redis-schema'

type HandlerParams = {
rpcUrl: string
chainId: number
property: string
getPluginConfigById: ClubsFunctionGetPluginConfigById
}

export const handler =
({ rpcUrl, chainId, property, getPluginConfigById }: HandlerParams) =>
async ({ request }: { readonly request: Request }) => {
const { signature, message, invitationId } = (await request.json()) as {
signature: string
message: string
invitationId: string
}

const client = await getDefaultClient()

// Try to fetch the mapped invitation.
const data = await whenNotErrorAll(
[invitationId, client],
([_id, _client]) =>
_client.ft.search(
Index.Invitation,
`@${schemaInvitation['$.id'].AS}:{${uuidToQuery(_id)}}`,
{
LIMIT: {
from: 0,
size: 1,
},
},
),
)

const invitation = whenNotError(
data,
(d) =>
(d.documents.find((x) => x.value)?.value as UndefinedOr<Invitation>) ??
new Error('ID is not found.'),
)

if (invitation instanceof Error) {
return new Response(JSON.stringify({ error: 'ID is not found' }), {
status: 400,
})
}

const membershipsPlugin = getPluginConfigById(
'devprotocol:clubs:simple-memberships',
)

if (!membershipsPlugin || !membershipsPlugin[0]?.options) {
return new Response(
JSON.stringify({ error: 'Simple memberships plugin not found' }),
{
status: 500,
},
)
}

const memberships =
(membershipsPlugin[0]?.options?.find(({ key }) => key === 'memberships')
?.value as UndefinedOr<Membership[]>) ?? []

const invitationMembership = memberships.find(
(m) => m.payload === invitation.membership.payload,
)

// get the ethereum address from the signature
// const signer = await getSigne(signature, message)
const address = verifyMessage(message, signature)

const available = await check({
id: invitationId,
account: address,
client,
})

if (!available) {
return new Response(
JSON.stringify({ error: 'Invitation not available' }),
{
status: 401,
},
)
}

const sendDevProtocolApiKey = process.env.SEND_DEVPROTOCOL_API_KEY ?? ''

const membershipPrice = invitationMembership?.price

if (!membershipPrice) {
return new Response(
JSON.stringify({ error: 'Membership price not found' }),
{
status: 500,
},
)
}

const parsedPrice =
invitationMembership?.currency === 'USDC'
? parseUnits(invitationMembership?.price.toString(), 6)
: parseUnits(invitationMembership?.price.toString(), 18)

const fee =
(parsedPrice * BigInt(invitationMembership.fee?.percentage ?? 0)) /
BigInt(10)

const res = fetch(
'https://send.devprotocol.xyz/api/send-transactions/SwapTokensAndStakeDev',
{
method: 'POST',
headers: {
Authorization: `Bearer ${sendDevProtocolApiKey}`,
},
body: JSON.stringify({
requestId: invitationId,
rpcUrl,
chainId,
args: {
to: address,
property,
payload: invitationMembership?.payload,
gatewayAddress: invitationMembership?.fee?.beneficiary,
amounts: {
token: invitationMembership?.currency,
input: parsedPrice,
fee,
},
},
}),
},
)

return new Response(JSON.stringify({ id: invitationId }), { status: 200 })
}

export default handler
117 changes: 117 additions & 0 deletions src/plugins/invitations/handlers/get-invitations-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { APIRoute } from 'astro'
import { aperture } from 'ramda'
import { getDefaultClient } from '../redis'
import {
Index,
Prefix,
schemaInvitation,
schemaHistory,
uuidToQuery,
type Invitation,
type History,
} from '../redis-schema'
import {
isNotError,
whenDefined,
whenDefinedAll,
whenNotError,
whenNotErrorAll,
type UndefinedOr,
} from '@devprotocol/util-ts'
import type { AsyncReturnType } from 'type-fest'
import { withCheckingIndex } from '../redis'

export const check = async ({
id,
account,
client,
}: {
id: string
account: string
client: AsyncReturnType<typeof withCheckingIndex>
}) => {
// Try to fetch the mapped invitation.
const invitation = await client.ft
.search(
Index.Invitation,
`@${schemaInvitation['$.id'].AS}:{${uuidToQuery(id)}}`,
{
LIMIT: {
from: 0,
size: 1,
},
},
)
.catch((err) => err as Error)
const invItem = whenNotError(
invitation,
(d) =>
(d.documents.find((x) => x.value)?.value as UndefinedOr<Invitation>) ??
new Error('ID is not found.'),
)

// Try to fetch the history.
const history = await client.ft
.search(
Index.History,
`@${schemaHistory['$.usedId'].AS}:{${uuidToQuery(id)}} @${schemaHistory['$.account'].AS}:{${uuidToQuery(account)}}`,
{
LIMIT: {
from: 0,
size: 1,
},
},
)
.catch((err) => err as Error)
const historyItem = whenNotError(
history,
(d) => d.documents.find((x) => x.value)?.value as UndefinedOr<History>,
)

const valid = whenNotErrorAll(
[invItem, historyItem],
([_invitation, _history]) => {
return typeof _history === 'undefined'
? true
: new Error('ID is already used.')
},
)

return valid
}

const handler: APIRoute = async (req) => {
const body = await req.request
.json()
.then((r) => r as { account?: string })
.catch((err) => err as Error)
const props = whenNotError(
body,
(_body) =>
whenDefinedAll([_body.account], ([account]) => ({ account })) ??
new Error('Missing parameters.'),
)

// Detect the passed invitation ID
const [, givenId] =
aperture(2, req.url.pathname.split('/')).find(([p]) => p === 'check') ?? []

const id = whenDefined(givenId, (_id) => _id) ?? new Error('ID is required')

// Generate a redis client while checking the latest schema is indexing and create/update index if it's not.
const client = await withCheckingIndex(getDefaultClient).catch(
(err) => err as Error,
)

const res = await whenNotErrorAll(
[id, client, props],
([_id, _client, { account }]) =>
check({ id: _id, client: _client, account }),
)

return new Response(JSON.stringify(res), {
status: isNotError(res) ? 200 : 400,
})
}

export default handler
61 changes: 61 additions & 0 deletions src/plugins/invitations/handlers/get-invitations-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { APIRoute } from 'astro'
import { aperture } from 'ramda'
import { withCheckingIndex, getDefaultClient } from '../redis'
import {
Index,
Prefix,
schemaInvitation,
schemaHistory,
uuidToQuery,
type Invitation,
type History,
} from '../redis-schema'
import {
isNotError,
whenDefined,
whenNotError,
whenNotErrorAll,
type UndefinedOr,
} from '@devprotocol/util-ts'

const handler: APIRoute = async (req) => {
// Detect the passed invitation ID
const [, givenId] =
aperture(2, req.url.pathname.split('/')).find(
([p]) => p === 'invitations',
) ?? []

const id = whenDefined(givenId, (_id) => _id) ?? new Error('ID is required')

// Generate a redis client while checking the latest schema is indexing and create/update index if it's not.
const client = await withCheckingIndex(getDefaultClient).catch(
(err) => err as Error,
)

// Try to fetch the mapped invitation.
const data = await whenNotErrorAll([id, client], ([_id, _client]) =>
_client.ft.search(
Index.Invitation,
`@${schemaInvitation['$.id'].AS}:{${uuidToQuery(_id)}}`,
{
LIMIT: {
from: 0,
size: 1,
},
},
),
)

const res = whenNotError(
data,
(d) =>
(d.documents.find((x) => x.value)?.value as UndefinedOr<Invitation>) ??
new Error('ID is not found.'),
)

return new Response(JSON.stringify(res), {
status: isNotError(res) ? 200 : 400,
})
}

export default handler
Loading

0 comments on commit 753c1df

Please sign in to comment.