diff --git a/api/resolvers/invite.js b/api/resolvers/invite.js
index 056a356a..df6bdc1b 100644
--- a/api/resolvers/invite.js
+++ b/api/resolvers/invite.js
@@ -10,6 +10,16 @@ export default {
return await models.invite.findMany({
where: {
userId: me.id
+ },
+ orderBy: {
+ createdAt: 'desc'
+ }
+ })
+ },
+ invite: async (parent, { id }, { me, models }) => {
+ return await models.invite.findUnique({
+ where: {
+ id
}
})
}
@@ -44,6 +54,13 @@ export default {
Invite: {
invitees: async (invite, args, { me, models }) => {
return await models.user.findMany({ where: { inviteId: invite.id } })
+ },
+ user: async (invite, args, { me, models }) => {
+ return await models.user.findUnique({ where: { id: invite.userId } })
+ },
+ poor: async (invite, args, { me, models }) => {
+ const user = await models.user.findUnique({ where: { id: invite.userId } })
+ return user.msats < invite.gift * 1000
}
}
}
diff --git a/api/resolvers/serial.js b/api/resolvers/serial.js
index da841435..3ab93108 100644
--- a/api/resolvers/serial.js
+++ b/api/resolvers/serial.js
@@ -23,6 +23,12 @@ async function serialize (models, call) {
if (error.message.includes('SN_PENDING_WITHDRAWL_EXISTS')) {
bail(new Error('withdrawal invoice exists and is pending'))
}
+ if (error.message.includes('SN_INELIGIBLE')) {
+ bail(new Error('user ineligible for gift'))
+ }
+ if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
+ bail(new Error('faucet has been revoked or is exhausted'))
+ }
if (error.message.includes('40001')) {
throw new Error('wallet balance serialization failure - retry again')
}
diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index 983a0b15..a0ed9062 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -84,6 +84,12 @@ export default {
bio: async (user, args, { models }) => {
return getItem(user, { id: user.bioId }, { models })
},
+ hasInvites: async (user, args, { models }) => {
+ const anInvite = await models.invite.findFirst({
+ where: { userId: user.id }
+ })
+ return !!anInvite
+ },
hasNewNotes: async (user, args, { models }) => {
// check if any votes have been cast for them since checkedNotesAt
const votes = await models.$queryRaw(`
diff --git a/api/typeDefs/invite.js b/api/typeDefs/invite.js
index 4c996776..8d377f84 100644
--- a/api/typeDefs/invite.js
+++ b/api/typeDefs/invite.js
@@ -3,6 +3,7 @@ import { gql } from 'apollo-server-micro'
export default gql`
extend type Query {
invites: [Invite!]!
+ invite(id: ID!): Invite
}
extend type Mutation {
@@ -16,6 +17,8 @@ export default gql`
invitees: [User!]!
gift: Int!
limit: Int
+ user: User!
revoked: Boolean!
+ poor: Boolean!
}
`
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index 8ce4d543..a0407f06 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -23,6 +23,7 @@ export default gql`
freePosts: Int!
freeComments: Int!
hasNewNotes: Boolean!
+ hasInvites: Boolean!
tipDefault: Int!
bio: Item
sats: Int!
diff --git a/components/header.js b/components/header.js
index db3ce78b..1c70570a 100644
--- a/components/header.js
+++ b/components/header.js
@@ -64,7 +64,12 @@ export default function Header () {
- invites
+ invites
+ {me && !me.hasInvites &&
+
+ {' '}
+
}
+
diff --git a/components/login.js b/components/login.js
index 881d982f..0d320a07 100644
--- a/components/login.js
+++ b/components/login.js
@@ -17,7 +17,7 @@ export const EmailSchema = Yup.object({
email: Yup.string().email('email is no good').required('required').trim()
})
-export default function Login ({ providers, callbackUrl, error }) {
+export default function Login ({ providers, callbackUrl, error, Header }) {
const errors = {
Signin: 'Try signing with a different account.',
OAuthSignin: 'Try signing with a different account.',
@@ -37,6 +37,7 @@ export default function Login ({ providers, callbackUrl, error }) {
return (
+ {Header &&
}
{errorMessage &&
setErrorMessage(undefined)} dismissible>{errorMessage} }
{router.query.type === 'lightning'
diff --git a/components/me.js b/components/me.js
index 0dc9831d..b41969c3 100644
--- a/components/me.js
+++ b/components/me.js
@@ -20,6 +20,7 @@ export function MeProvider ({ children }) {
bio {
id
}
+ hasInvites
}
}`
const { data } = useQuery(query, { pollInterval: 1000 })
diff --git a/fragments/invites.js b/fragments/invites.js
index 4ed25a8b..11e8436a 100644
--- a/fragments/invites.js
+++ b/fragments/invites.js
@@ -11,5 +11,10 @@ export const INVITE_FIELDS = gql`
gift
limit
revoked
+ user {
+ name
+ id
+ }
+ poor
}
`
diff --git a/pages/invites/[id].js b/pages/invites/[id].js
index 5c436962..ca009fe6 100644
--- a/pages/invites/[id].js
+++ b/pages/invites/[id].js
@@ -1,11 +1,42 @@
import Login from '../../components/login'
import { providers, getSession } from 'next-auth/client'
+import models from '../../api/models'
+import serialize from '../../api/resolvers/serial'
+import { gql } from '@apollo/client'
+import { INVITE_FIELDS } from '../../fragments/invites'
+import getSSRApolloClient from '../../api/ssrApollo'
+import Link from 'next/link'
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
const session = await getSession({ req })
+ const client = await getSSRApolloClient(req)
+ const { data } = await client.query({
+ query: gql`
+ ${INVITE_FIELDS}
+ {
+ invite(id: "${id}") {
+ ...InviteFields
+ }
+ }`
+ })
+
+ if (!data?.invite) {
+ return {
+ notFound: true
+ }
+ }
+
if (session && res) {
- // send down the userid and the invite to the db
+ try {
+ // attempt to send gift
+ // catch any errors and just ignore them for now
+ await serialize(models,
+ models.$queryRaw('SELECT invite_drain($1, $2)', session.user.id, id))
+ } catch (e) {
+ console.log(e)
+ }
+
res.writeHead(302, {
Location: '/'
})
@@ -17,9 +48,36 @@ export async function getServerSideProps ({ req, res, query: { id, error = null
props: {
providers: await providers({ req, res }),
callbackUrl: process.env.SELF_URL + req.url,
+ invite: data.invite,
error
}
}
}
-export default Login
+function InviteHeader ({ invite }) {
+ console.log(invite.poor)
+ let Inner
+ if (invite.revoked) {
+ Inner = () =>
this invite link expired
+ } else if ((invite.limit && invite.limit <= invite.invitees.length) || invite.poor) {
+ Inner = () =>
this invite link has no more sats
+ } else {
+ Inner = () => (
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default function Invite ({ invite, ...props }) {
+ return
} {...props} />
+}
diff --git a/pages/invites/index.js b/pages/invites/index.js
index b0b64e69..621d5bb3 100644
--- a/pages/invites/index.js
+++ b/pages/invites/index.js
@@ -46,10 +46,10 @@ function InviteForm () {
limit: undefined
}}
schema={InviteSchema}
- onSubmit={async ({ limit, ...values }) => {
+ onSubmit={async ({ limit, gift }) => {
const { error } = await createInvite({
variables: {
- ...values, limit: limit ? Number(limit) : limit
+ gift: Number(gift), limit: limit ? Number(limit) : limit
}
})
if (error) {
@@ -95,6 +95,8 @@ function Invite ({ invite, active }) {
placeholder={`https://stacker.news/invites/${invite.id}`} readOnly
/>
+ {invite.gift} sat gift
+ \
{invite.invitees.length} joined{invite.limit ? ` of ${invite.limit}` : ''}
{active
? (
diff --git a/prisma/migrations/20211015180613_invite_func/migration.sql b/prisma/migrations/20211015180613_invite_func/migration.sql
new file mode 100644
index 00000000..1ec96af8
--- /dev/null
+++ b/prisma/migrations/20211015180613_invite_func/migration.sql
@@ -0,0 +1,45 @@
+CREATE OR REPLACE FUNCTION invite_drain(user_id INTEGER, invite_id TEXT)
+RETURNS INTEGER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ inviter_id INTEGER;
+ inviter_sats INTEGER;
+ gift INTEGER;
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+ -- check user was created in last hour
+ -- check user did not already redeem an invite
+ PERFORM FROM users
+ WHERE id = user_id AND users.created_at >= NOW() AT TIME ZONE 'UTC' - INTERVAL '1 HOUR'
+ AND users."inviteId" IS NULL;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'SN_INELIGIBLE';
+ END IF;
+
+ -- check that invite has not reached limit
+ -- check that invite is not revoked
+ SELECT "Invite"."userId", "Invite".gift INTO inviter_id, gift FROM "Invite"
+ LEFT JOIN users ON users."inviteId" = invite_id
+ WHERE "Invite".id = invite_id AND NOT "Invite".revoked
+ GROUP BY "Invite".id
+ HAVING COUNT(DISTINCT users.id) < "Invite".limit OR "Invite".limit IS NULL;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED';
+ END IF;
+
+ -- check that inviter has sufficient balance
+ SELECT (msats / 1000) INTO inviter_sats
+ FROM users WHERE id = inviter_id;
+ IF inviter_sats < gift THEN
+ RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED';
+ END IF;
+
+ -- subtract amount from inviter
+ UPDATE users SET msats = msats - (1000 * gift) WHERE id = inviter_id;
+ -- add amount to invitee
+ UPDATE users SET msats = msats + (1000 * gift), "inviteId" = invite_id WHERE id = user_id;
+
+ RETURN 0;
+END;
+$$;
\ No newline at end of file