-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2094 from dev-protocol/invitations
Invitations
- Loading branch information
Showing
10 changed files
with
655 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,3 +28,5 @@ SEND_TX_API_KEY= | |
PUBLIC_WALLET_CONNECT_PROJECT_ID= | ||
|
||
PUBLIC_ONDATO_VERIFICATION_URL= | ||
|
||
SEND_DEVPROTOCOL_API_KEY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
117
src/plugins/invitations/handlers/get-invitations-check.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.