Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for non-NFT spl-tokens #1

Open
wants to merge 103 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
bb37d53
fix dockerfile
Jan 24, 2022
956314a
add spl-token balance support and fix response UX
Jan 24, 2022
2b8e4dc
update mint list
Jan 26, 2022
ab585c0
dockerfile tweak
Jan 26, 2022
7c63586
node 14 for versel
Jan 26, 2022
0b330cc
undo that
Jan 26, 2022
79040d0
error handling
Jan 26, 2022
fbacdde
use update authority
Jan 26, 2022
b0bc1c8
remove branding from PR
Feb 2, 2022
4dc6c7a
Merge remote-tracking branch 'upstream/main'
Feb 2, 2022
5fc9da0
fix compiler error
Feb 2, 2022
88af4e4
Merge remote-tracking branch 'upstream/main'
Feb 2, 2022
0908f78
COS support
Feb 2, 2022
ae89ad6
update README with COS documentation
Feb 2, 2022
a3a4460
readme
Feb 2, 2022
c2bc758
remove custom fav icon
Feb 2, 2022
b1c8afe
configurable project name
Feb 2, 2022
09e423b
multitenant support
Feb 4, 2022
f7b0175
remove env var refs from middleware
Feb 4, 2022
2a28342
Merge branch 'cos-support' into production
Feb 4, 2022
4f50af9
update example remove COS
Feb 4, 2022
ba22bd5
read configs from COS
Feb 4, 2022
f014153
protect reload path from DoS
Feb 4, 2022
457cf8e
registration basic impl
Feb 5, 2022
aaa193b
update project support
Feb 6, 2022
b60702b
free tier validation
Feb 6, 2022
9c6984d
improve the UX
Feb 7, 2022
e471abf
cleanup UX and add video
Feb 9, 2022
79d9706
form validation and background tasks
Feb 10, 2022
57dc478
update landing page
Feb 11, 2022
6dcf2dc
only check spl token if configured
Feb 11, 2022
f64c643
sales tracking endpoint
Feb 11, 2022
6c9ef10
manage page update
Feb 11, 2022
1d8f8b9
sales tracking scaffolding
Feb 11, 2022
367c0c5
improve sales tracking page
Feb 12, 2022
63d24c2
loading animation
Feb 12, 2022
744d97a
metrics and API cleanup
Feb 13, 2022
f840cef
sales count included in project list
Feb 14, 2022
f8359d6
init last sales time to 0
Feb 14, 2022
83d2e5e
write-through cache for COS performance
Feb 14, 2022
39ac223
wallet connect error message UX
Feb 15, 2022
4f2bcd5
another UX error handling catch
Feb 15, 2022
95757f4
save discord webhook URL
Feb 16, 2022
45600ac
projects remain holders once detected
Feb 18, 2022
e15ac0f
input validation and string trimming
Feb 18, 2022
dc9f9e9
error handling bug fix
Feb 19, 2022
8774062
error handling bug fix
Feb 19, 2022
9058b92
WIP - hack, hack, hack
Feb 19, 2022
e36fca7
elapsed time metric to log
Feb 19, 2022
d4d25e7
render sales tracker stats in metrics JSON
Feb 19, 2022
e795799
update all verification logic to support multiple roles
Feb 20, 2022
1f70e15
specify trait based roles in project config
Feb 20, 2022
6437b8e
UI to manage trait based roles
Feb 20, 2022
3261682
ux cleanup
Feb 20, 2022
0ba7a65
remove vercel integration
Feb 20, 2022
66afa6d
Merge pull request #1 from qrtp/traits
qrtp Feb 20, 2022
d787f14
spinner when verifying
Feb 20, 2022
7891199
do not retrieve token metadata when not required
Feb 20, 2022
2071e71
twitter support
Feb 24, 2022
b652e2c
error handling for listing projects
Feb 25, 2022
89d9488
improve project metrics
Feb 26, 2022
01b5acf
clean up management page with twitter bot ux
Feb 26, 2022
10c6b47
session management to clean up constant requests for signatures
Mar 2, 2022
7bec8f7
collect additional project info for more informative display
Mar 2, 2022
2f0ac7e
migrate header to vuetify
Mar 3, 2022
0178ad8
backend support for multiple valid update authorities for verification
Mar 3, 2022
60c6d7e
multiple spl tokens
Mar 4, 2022
6d66986
solflare testing
Mar 4, 2022
69c47d4
multiple spl tokens
Mar 4, 2022
d40f72b
structured logging
Mar 5, 2022
6122234
Merge branch 'main' into solflare
Mar 5, 2022
3f991f0
preliminary solflare support
Mar 6, 2022
081bc1d
use solflare if phantom not found
Mar 6, 2022
ce07a1a
solflare support on verification landing page
Mar 6, 2022
495e9ed
update error messages to indicate supported wallets are phantom and s…
Mar 6, 2022
86e2b5d
select supported wallet
Mar 6, 2022
bf6bdc1
add modal text to describe wallet selection
Mar 6, 2022
e0ba765
multi wallet support for management page
Mar 6, 2022
de69f50
multi wallet support for management page
Mar 6, 2022
17b7fe6
Merge pull request #2 from qrtp/solflare
qrtp Mar 6, 2022
e2bccc4
use an axios timeout + project sales optimization
Mar 8, 2022
8096bc4
Merge branch 'solflare'
Mar 8, 2022
7c622b2
retry loading on failed discord clients
Mar 8, 2022
c9c4704
custom trait count support
Mar 9, 2022
8c5d695
cleanup ux managing count specification
Mar 9, 2022
989a5e1
Merge pull request #5 from qrtp/nft-count
qrtp Mar 9, 2022
49d3e8e
concurrency control
Mar 10, 2022
83fece9
environment variables to control pruning behavior
Mar 10, 2022
60a375d
revalidation refinement
Mar 11, 2022
6fbbb06
continue processing revalidation loop on error
Mar 12, 2022
1312172
include last revalidation timestamp in metrics
Mar 12, 2022
38999ba
long overdue README update
Mar 12, 2022
ea0ef9a
improved mobile navigation
Mar 16, 2022
2d83766
revalidation optimization
Mar 22, 2022
7250bdc
fix header overflow
Mar 22, 2022
ba173bf
continue
Mar 22, 2022
58b13fb
metrics and list management
Mar 22, 2022
e5009f8
refactor the monolith
Mar 24, 2022
7598010
discord role name lookup + slope wallet support
Mar 29, 2022
9145abd
saving progress
Mar 29, 2022
02ac49f
voting service implementation
Apr 3, 2022
0389087
Merge pull request #6 from qrtp/voting
qrtp Apr 3, 2022
930ca99
update README with new github repository link
Apr 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# base stage
FROM node:14 as base
FROM node:16 as base
WORKDIR /app

RUN yarn global add @vue/cli

# setup environment variables
RUN env
ENV HOST=0.0.0.0

COPY package.json yarn.lock ./
RUN yarn
COPY . /app
EXPOSE 8084
CMD ['yarn', 'dev']
EXPOSE 3000
CMD ["yarn","start"]
16 changes: 13 additions & 3 deletions pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
Review the message before signing and make sure that nothing else is requested except signature.
</div>
<div class="text-center" v-if="step === 5">
You're done! You can close this window now
You're verified! You can close this window now and flex your new discord power.
</div>
<div class="text-center" v-if="step === 6">
Unfortunately your wallet doesn't have the tokens required for validation.
</div>
</div>
</div>
Expand Down Expand Up @@ -96,11 +99,18 @@ export default Vue.extend({
// @ts-ignore I honestly didn't wanna bother with strong typing this.. Feel free if you'd like
publicKey: connection.publicKey.toString()
})
if (res2.status == 200) {
this.step = 5
}
} catch (e) {
console.log("API ERROR", e)
}

this.step = 5

// show error if failure
if (this.step == 4) {
this.step = 6
}

}
})
</script>
Expand Down
121 changes: 87 additions & 34 deletions server-middleware/logHodlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const bodyParser = require('body-parser')
const axios = require('axios')
const app = require('express')()
import {Request, Response} from 'express'
import { Request, Response } from 'express'
const { getParsedNftAccountsByOwner } = require('@nfteyez/sol-rayz')
const fs = require('fs')
const nacl = require('tweetnacl')
Expand All @@ -24,8 +25,45 @@ app.get('/getHodlers', async (req: Request, res: Response) => {
return res.json(hodlerList)
})

const getTokenBalance = async (walletAddress: any, tokenMintAddress: any) => {
const response = await axios({
url: `https://api.mainnet-beta.solana.com`,
method: "post",
headers: { "Content-Type": "application/json" },
data: {
jsonrpc: "2.0",
id: 1,
method: "getTokenAccountsByOwner",
params: [
walletAddress,
{
mint: tokenMintAddress,
},
{
encoding: "jsonParsed",
},
],
},
});
if (
Array.isArray(response?.data?.result?.value) &&
response?.data?.result?.value?.length > 0 &&
response?.data?.result?.value[0]?.account?.data?.parsed?.info?.tokenAmount
?.amount > 0
) {
return (
Number(
response?.data?.result?.value[0]?.account?.data?.parsed?.info
?.tokenAmount?.amount
) / 1000000000
);
} else {
return 0;
}
};

app.post('/logHodlers', async (req: Request, res: Response) => {
const publicKeyString = req.body.publicKey
const publicKeyString = req.body.publicKey
const signature = req.body.signature
const message = process.env.MESSAGE
const encodedMessage = new TextEncoder().encode(message)
Expand All @@ -34,73 +72,88 @@ app.post('/logHodlers', async (req: Request, res: Response) => {

// Validates signature sent from client
const isValid = nacl.sign.detached.verify(encodedMessage, encryptedSignature, publicKey)
if(!isValid) {
if (!isValid) {
return res.status(400)
}

const discordName = req.body.discordName
let tokenList
const discordName = req.body.discordName
let tokenList
let splTokenBalance = 0

// Parses all tokens from that public key
try {
tokenList = await getParsedNftAccountsByOwner({publicAddress: publicKeyString})
} catch (e) {
console.log("Error parsing NFTs", e)
}
try {
tokenList = await getParsedNftAccountsByOwner({ publicAddress: publicKeyString })
} catch (e) {
console.log("Error parsing NFTs", e)
}

// Basic ass way to find matched NFTs compared to the mint list ( PRs welcome <3 )
let matched = []
for (let item of tokenList) {
if(mint_list.includes(item.mint)) matched.push(item)
}
let matched = []
for (let item of tokenList) {
if (mint_list.includes(item.mint)) matched.push(item)
}

// Optionally check for spl-tokens matching mint IDs if NFTs were not found
if (matched.length == 0) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only added this to the first handler to initially add the role. Probably should also update the second one that updates roles later. How does that one get called, didn't see references to it elsewhere in code. Is it meant to be called externally via a curl or something?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeap, curl or just go to it in the browser

for (let item of mint_list) {
try {
splTokenBalance = await getTokenBalance(publicKeyString, item)
if (splTokenBalance > 0) {
break
}
} catch (e) {
console.log("Error getting spl token balance", e)
}
}
}

// If matched NFTs are not empty and it's not already in the JSON push it
if(matched.length !== 0) {
let hasHodler = false
for (let n of hodlerList) {
if(n.discordName === discordName) hasHodler = true
}
if(!hasHodler) {
hodlerList.push({
discordName: discordName,
publicKey: publicKeyString
})
}
} else {
if (matched.length !== 0 || splTokenBalance > 0) {
let hasHodler = false
for (let n of hodlerList) {
if (n.discordName === discordName) hasHodler = true
}
if (!hasHodler) {
hodlerList.push({
discordName: discordName,
publicKey: publicKeyString
})
}
} else {
return res.sendStatus(401)
}

const username = discordName.split('#')[0]
const discriminator = discordName.split('#')[1]

// Update role
const myGuild = await client.guilds.cache.get(process.env.DISCORD_SERVER_ID)
const role = await myGuild.roles.cache.find((r: any) => r.id === process.env.DISCORD_ROLE_ID)
const doer = await myGuild.members.cache.find((member: any) => (member.user.username === username && member.user.discriminator === discriminator))
await doer.roles.add(role)
const myGuild = await client.guilds.cache.get(process.env.DISCORD_SERVER_ID)
const role = await myGuild.roles.cache.find((r: any) => r.id === process.env.DISCORD_ROLE_ID)
const doer = await myGuild.members.cache.find((member: any) => (member.user.username === username && member.user.discriminator === discriminator))
await doer.roles.add(role)

fs.writeFileSync('./server-middleware/hodlers.json', JSON.stringify(hodlerList))

res.sendStatus(200)
res.sendStatus(200)
})

app.get('/reloadHolders', async (req: Request, res: Response) => {
for (let n in hodlerList) {
const holder = hodlerList[n]
let tokenList
try {
tokenList = await getParsedNftAccountsByOwner({publicAddress: holder.publicKey})
tokenList = await getParsedNftAccountsByOwner({ publicAddress: holder.publicKey })
} catch (e) {
res.status(400).send("There was a problem with parsing NFTs")
console.log("Error parsing NFTs", e)
}

let matched = []
for (let item of tokenList) {
if(mint_list.includes(item.mint)) matched.push(item)
if (mint_list.includes(item.mint)) matched.push(item)
}

if(matched.length === 0) {
if (matched.length === 0) {
hodlerList.splice(n, 1)
const username = holder.discordName.split('#')[0]
const discriminator = holder.discordName.split('#')[1]
Expand Down