Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ TWITTER_USERNAME=
TWITTER_PASSWORD=
TWITTER_EMAIL=

GITHUB_TOKEN=
GITHUB_TOKEN=

DISCORD_API_URL=
DISCORD_BOT_TOKEN=
177 changes: 167 additions & 10 deletions src/features/incentive/PointSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const pointValues = {
LINK_TWITTER: 2,
LINK_GITHUB: 1,
FOLLOW_DEMOS: 1,
LINK_DISCORD: 1,
}

export class PointSystem {
Expand All @@ -33,13 +34,17 @@ export class PointSystem {
*/
private async getUserIdentitiesFromGCR(userId: string): Promise<{
linkedWallets: string[]
linkedSocials: { twitter?: string }
linkedSocials: { twitter?: string; discord?: string }
}> {
const xmIdentities = await IdentityManager.getIdentities(userId)
const twitterIdentities = await IdentityManager.getWeb2Identities(
userId,
"twitter",
)
const discordIdentities = await IdentityManager.getWeb2Identities(
userId,
"discord",
)

const linkedWallets: string[] = []

Expand All @@ -63,12 +68,16 @@ export class PointSystem {
}
}

const linkedSocials: { twitter?: string } = {}
const linkedSocials: { twitter?: string; discord?: string } = {}

if (Array.isArray(twitterIdentities) && twitterIdentities.length > 0) {
linkedSocials.twitter = twitterIdentities[0].username
}

if (Array.isArray(discordIdentities) && discordIdentities.length > 0) {
linkedSocials.discord = discordIdentities[0].username
}

return { linkedWallets, linkedSocials }
}

Expand Down Expand Up @@ -436,15 +445,18 @@ export class PointSystem {

// Verify the GitHub account is actually linked to this user
const githubIdentities = account.identities.web2?.github || []
const isOwner = githubIdentities.some((gh: any) => gh.userId === githubUserId)
const isOwner = githubIdentities.some(
(gh: any) => gh.userId === githubUserId,
)

if (!isOwner) {
return {
result: 400,
response: {
pointsAwarded: 0,
totalPoints: account.points.totalPoints || 0,
message: "Error: GitHub account not linked to this user",
message:
"Error: GitHub account not linked to this user",
},
require_reply: false,
extra: {},
Expand Down Expand Up @@ -616,22 +628,28 @@ export class PointSystem {
* @param githubUserId The GitHub user ID to verify ownership
* @returns RPCResponse
*/
async deductGithubPoints(userId: string, githubUserId: string): Promise<RPCResponse> {
async deductGithubPoints(
userId: string,
githubUserId: string,
): Promise<RPCResponse> {
try {
// Get user's account data from GCR to verify GitHub ownership
const account = await ensureGCRForUser(userId)

// Verify the GitHub account is actually linked to this user
const githubIdentities = account.identities.web2?.github || []
const isOwner = githubIdentities.some((gh: any) => gh.userId === githubUserId)
const isOwner = githubIdentities.some(
(gh: any) => gh.userId === githubUserId,
)

if (!isOwner) {
return {
result: 400,
response: {
pointsDeducted: 0,
totalPoints: account.points.totalPoints || 0,
message: "Error: GitHub account not linked to this user",
message:
"Error: GitHub account not linked to this user",
},
require_reply: false,
extra: {},
Expand All @@ -643,9 +661,7 @@ export class PointSystem {
)

// Check if user has GitHub points to deduct
if (
userPointsWithIdentities.breakdown.socialAccounts.github <= 0
) {
if (userPointsWithIdentities.breakdown.socialAccounts.github <= 0) {
return {
result: 200,
response: {
Expand Down Expand Up @@ -689,4 +705,145 @@ export class PointSystem {
}
}
}

/**
* Award points for linking a Discord account
* @param userId The user's Demos address
* @param referralCode Optional referral code
* @returns RPCResponse
*/
async awardDiscordPoints(
userId: string,
referralCode?: string,
): Promise<RPCResponse> {
try {
// Verify the Discord account is actually linked to this user
const account = await ensureGCRForUser(userId)
const discordIdentities = account.identities.web2?.discord || []

const hasDiscord =
Array.isArray(discordIdentities) && discordIdentities.length > 0
if (!hasDiscord) {
return {
result: 400,
response: {
pointsAwarded: 0,
totalPoints: account.points.totalPoints || 0,
message:
"Error: Discord account not linked to this user",
},
require_reply: false,
extra: {},
}
}

const userPointsWithIdentities = await this.getUserPointsInternal(
userId,
)

// Check if user already has Discord points specifically
if (userPointsWithIdentities.breakdown.socialAccounts.discord > 0) {
return {
result: 200,
response: {
pointsAwarded: 0,
totalPoints: userPointsWithIdentities.totalPoints,
message: "Discord points already awarded",
},
require_reply: false,
extra: {},
}
}

await this.addPointsToGCR(
userId,
pointValues.LINK_DISCORD,
"socialAccounts",
"discord",
referralCode,
)

const updatedPoints = await this.getUserPointsInternal(userId)

return {
result: 200,
response: {
pointsAwarded: pointValues.LINK_DISCORD,
totalPoints: updatedPoints.totalPoints,
message: "Points awarded for linking Discord",
},
require_reply: false,
extra: {},
}
} catch (error) {
return {
result: 500,
response: "Error awarding points",
require_reply: false,
extra: {
error:
error instanceof Error ? error.message : String(error),
},
}
}
}

/**
* Deduct points for unlinking a Discord account
* @param userId The user's Demos address
* @returns RPCResponse
*/
async deductDiscordPoints(userId: string): Promise<RPCResponse> {
try {
const userPointsWithIdentities = await this.getUserPointsInternal(
userId,
)

// Check if user has Discord points to deduct
if (
userPointsWithIdentities.breakdown.socialAccounts.discord <= 0
) {
return {
result: 200,
response: {
pointsDeducted: 0,
totalPoints: userPointsWithIdentities.totalPoints,
message: "No Discord points to deduct",
},
require_reply: false,
extra: {},
}
}

await this.addPointsToGCR(
userId,
-pointValues.LINK_DISCORD,
"socialAccounts",
"discord",
)

const updatedPoints = await this.getUserPointsInternal(userId)

return {
result: 200,
response: {
pointsDeducted: pointValues.LINK_DISCORD,
totalPoints: updatedPoints.totalPoints,
message: "Points deducted for unlinking Discord",
},
require_reply: false,
extra: {},
}
} catch (error) {
return {
result: 500,
response: "Error deducting points",
require_reply: false,
extra: {
error:
error instanceof Error ? error.message : String(error),
},
}
}
}
}
9 changes: 8 additions & 1 deletion src/libs/abstraction/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GithubProofParser } from "./web2/github"
import { TwitterProofParser } from "./web2/twitter"
import { DiscordProofParser } from "./web2/discord"
import { type Web2ProofParser } from "./web2/parsers"
import { Web2CoreTargetIdentityPayload } from "@kynesyslabs/demosdk/abstraction"
import { hexToUint8Array, ucrypto } from "@kynesyslabs/demosdk/encryption"
Expand All @@ -15,7 +16,10 @@ export async function verifyWeb2Proof(
payload: Web2CoreTargetIdentityPayload,
sender: string,
) {
let parser: typeof TwitterProofParser | typeof GithubProofParser
let parser:
| typeof TwitterProofParser
| typeof GithubProofParser
| typeof DiscordProofParser

switch (payload.context) {
case "twitter":
Expand All @@ -24,6 +28,9 @@ export async function verifyWeb2Proof(
case "github":
parser = GithubProofParser
break
case "discord":
parser = DiscordProofParser
break
default:
return {
success: false,
Expand Down
54 changes: 54 additions & 0 deletions src/libs/abstraction/web2/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Web2ProofParser } from "./parsers"
import { SigningAlgorithm } from "@kynesyslabs/demosdk/types"
import { Discord } from "@/libs/identity/tools/discord"

export class DiscordProofParser extends Web2ProofParser {
private static instance: DiscordProofParser
discord: Discord

constructor() {
super()
this.discord = Discord.getInstance()
}

private parseDiscordMessageUrl(proofUrl: string): {
channelId: string
messageId: string
} {
const { channelId, messageId } =
this.discord.extractMessageDetails(proofUrl)
return { channelId, messageId }
}

async getMessageFromUrl(messageUrl: string) {
return await this.discord.getMessageByUrl(messageUrl)
}

async readData(proofUrl: string): Promise<{
message: string
signature: string
type: SigningAlgorithm
}> {
this.verifyProofFormat(proofUrl, "discord")

// Validate and fetch via shared client
this.parseDiscordMessageUrl(proofUrl)
const msg = await this.discord.getMessageByUrl(proofUrl)
const content = (msg?.content as string) || ""

const payload = this.parsePayload(content)
if (!payload) {
throw new Error("Invalid proof format")
}

return payload
}

static async getInstance() {
if (!this.instance) {
this.instance = new DiscordProofParser()
}

return this.instance
}
}
6 changes: 6 additions & 0 deletions src/libs/abstraction/web2/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export abstract class Web2ProofParser {
"https://gist.githubusercontent.com",
],
twitter: ["https://x.com", "https://twitter.com"],
discord: [
"https://discord.com/channels",
"https://ptb.discord.com/channels",
"https://canary.discord.com/channels",
"https://discordapp.com/channels",
],
}

constructor() {}
Expand Down
Loading