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
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ SERVER_PORT=53550
EXPOSED_URL=http://127.0.0.1:53550
PROD=false
SIGNALING_SERVER_PORT=3005
TURNSTILE_SECRET_KEY=0x4AAAAAABav_NndHphc_W7tGJIKMuaSBqc

TWITTER_USERNAME=
TWITTER_PASSWORD=
TWITTER_EMAIL=

GITHUB_TOKEN=
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"typescript": "^4.9.3"
},
"dependencies": {
"@cosmjs/encoding": "^0.33.1",
"@fastify/cors": "^9.0.1",
"@fastify/swagger": "^8.15.0",
"@fastify/swagger-ui": "^4.1.0",
Expand Down
358 changes: 358 additions & 0 deletions src/features/incentive/PointSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
import Datasource from "../../model/datasource"
import { RPCResponse } from "@kynesyslabs/demosdk/types"
import { UserPoints } from "@kynesyslabs/demosdk/abstraction"
import IdentityManager from "@/libs/blockchain/gcr/gcr_routines/identityManager"
import { GCRMain } from "@/model/entities/GCRv2/GCR_Main"
import HandleGCR from "@/libs/blockchain/gcr/handleGCR"

const pointValues = {
LINK_WEB3_WALLET: 2,
LINK_TWITTER: 5,
}

export class PointSystem {
private static instance: PointSystem

private constructor() {}

public static getInstance(): PointSystem {
if (!PointSystem.instance) {
PointSystem.instance = new PointSystem()
}
return PointSystem.instance
}

/**
* Get user's identities directly from the GCR
*/
private async getUserIdentitiesFromGCR(userId: string): Promise<{
linkedWallets: string[]
linkedSocials: { twitter?: string }
}> {
const xmIdentities = await IdentityManager.getIdentities(userId)
const twitterIdentities = await IdentityManager.getWeb2Identities(
userId,
"twitter",
)
const githubIdentities = await IdentityManager.getWeb2Identities(
userId,
"github",
)

const linkedWallets: string[] = []

if (xmIdentities?.xm) {
const chains = Object.keys(xmIdentities.xm)

for (const chain of chains) {
const subChains = xmIdentities.xm[chain]
const subChainKeys = Object.keys(subChains)

for (const subChain of subChainKeys) {
const addresses = subChains[subChain]

if (Array.isArray(addresses)) {
addresses.forEach(address => {
const walletId = `${chain}:${address}`
linkedWallets.push(walletId)
})
}
}
}
}

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

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

return { linkedWallets, linkedSocials }
}

/**
* Get user's points from GCR
*/
private async getUserPointsInternal(userId: string): Promise<UserPoints> {
// Convert userId to hex string if it's a Buffer
const userIdStr = Buffer.isBuffer(userId)
? userId.toString("hex")
: userId

const db = await Datasource.getInstance()
const gcrMainRepository = db.getDataSource().getRepository(GCRMain)
let account = await gcrMainRepository.findOneBy({ pubkey: userIdStr })

const { linkedWallets, linkedSocials } =
await this.getUserIdentitiesFromGCR(userIdStr)

if (!account) {
account = await HandleGCR.createAccount(userIdStr)
account.points.totalPoints = 0
account.points.breakdown = {
web3Wallets: 0,
socialAccounts: {
twitter: 0,
github: 0,
discord: 0,
},
}
account.points.lastUpdated = new Date()

await gcrMainRepository.save(account)
}

// Create and return the response object
return {
userId: userIdStr,
totalPoints: account.points.totalPoints || 0,
breakdown: {
web3Wallets: account.points.breakdown?.web3Wallets || 0,
socialAccounts: account.points.breakdown?.socialAccounts || {
twitter: 0,
github: 0,
discord: 0,
},
},
linkedWallets,
linkedSocials,
lastUpdated: account.points.lastUpdated || new Date(),
}
}

/**
* Add points to the GCR for a user
*/
private async addPointsToGCR(
userId: string,
points: number,
type: "web3Wallets" | "socialAccounts",
platform?: "twitter" | "github" | "discord",
): Promise<void> {
const db = await Datasource.getInstance()
const gcrMainRepository = db.getDataSource().getRepository(GCRMain)
const account = await gcrMainRepository.findOneBy({ pubkey: userId })

if (!account) {
const newAccount = await HandleGCR.createAccount(userId)
newAccount.points.totalPoints = points
if (type === "socialAccounts" && platform) {
newAccount.points.breakdown = {
web3Wallets: 0,
socialAccounts: {
twitter: platform === "twitter" ? points : 0,
github: platform === "github" ? points : 0,
discord: platform === "discord" ? points : 0,
},
}
} else {
newAccount.points.breakdown = {
web3Wallets: type === "web3Wallets" ? points : 0,
socialAccounts: {
twitter: 0,
github: 0,
discord: 0,
},
}
}
newAccount.points.lastUpdated = new Date()

await gcrMainRepository.save(newAccount)
} else {
const oldTotal = account.points.totalPoints || 0
account.points.totalPoints = oldTotal + points

if (type === "socialAccounts" && platform) {
const oldPlatformPoints =
account.points.breakdown?.socialAccounts?.[platform] || 0
account.points.breakdown.socialAccounts[platform] =
oldPlatformPoints + points
} else {
if (type === "web3Wallets") {
const oldCategoryPoints =
account.points.breakdown.web3Wallets || 0
account.points.breakdown.web3Wallets =
oldCategoryPoints + points
}
}
account.points.lastUpdated = new Date()

await gcrMainRepository.save(account)
}
}

/**
* Get user's current points
* @param userId The user's Demos address
* @returns User points with identity information from GCR
*/
async getUserPoints(userId: string): Promise<RPCResponse> {
try {
const userPoints = await this.getUserPointsInternal(userId)

return {
result: 200,
response: {
userId: userPoints.userId,
totalPoints: userPoints.totalPoints,
breakdown: userPoints.breakdown,
linkedWallets: userPoints.linkedWallets,
linkedSocials: userPoints.linkedSocials,
lastUpdated: userPoints.lastUpdated,
},
require_reply: false,
extra: {},
}
} catch (error) {
return {
result: 500,
response: "Error getting user points",
require_reply: false,
extra: {
error:
error instanceof Error ? error.message : String(error),
},
}
}
}

/**
* Award points for linking a Web3 wallet
* @param userId The user's Demos address
* @param walletAddress The wallet address
* @param chain The chain type
* @returns RPCResponse
*/
async awardWeb3WalletPoints(
userId: string,
walletAddress: string,
chain: string,
): Promise<RPCResponse> {
let walletIsAlreadyLinked = false
let hasExistingWalletOnChain = false
const walletIsAlreadyLinkedMessage = "This wallet is already linked"
const hasExistingWalletOnChainMessage = `A ${chain} wallet is already linked. Please disconnect it first.`
try {
// Get current points and identities from GCR
const userPointsWithIdentities = await this.getUserPointsInternal(
userId,
)

if (
userPointsWithIdentities.linkedWallets.includes(
`${chain}:${walletAddress}`,
)
) {
walletIsAlreadyLinked = true
}

// Check if any wallet of this chain type is already linked
const hasExistingChainWallet =
userPointsWithIdentities.linkedWallets.some(wallet =>
wallet.startsWith(`${chain}:`),
)

if (hasExistingChainWallet) {
hasExistingWalletOnChain = true
}

// Award points by updating the GCR
await this.addPointsToGCR(
userId,
pointValues.LINK_WEB3_WALLET,
"web3Wallets",
)

// Get updated points
const updatedPoints = await this.getUserPointsInternal(userId)

return {
result:
walletIsAlreadyLinked || hasExistingWalletOnChain
? 400
: 200,
response: {
pointsAwarded:
!walletIsAlreadyLinked && !hasExistingWalletOnChain
? pointValues.LINK_WEB3_WALLET
: 0,
totalPoints: updatedPoints.totalPoints,
message: walletIsAlreadyLinked
? walletIsAlreadyLinkedMessage
: hasExistingWalletOnChain
? hasExistingWalletOnChainMessage
: "Points awarded for linking wallet",
},
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),
},
}
}
}

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

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

await this.addPointsToGCR(
userId,
pointValues.LINK_TWITTER,
"socialAccounts",
"twitter",
)

const updatedPoints = await this.getUserPointsInternal(userId)

return {
result: 200,
response: {
pointsAwarded: pointValues.LINK_TWITTER,
totalPoints: updatedPoints.totalPoints,
message: "Points awarded for linking Twitter",
},
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),
},
}
}
}
}
3 changes: 1 addition & 2 deletions src/libs/abstraction/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Cryptography from "../crypto/cryptography"

import { GithubProofParser } from "./web2/github"
import { TwitterProofParser } from "./web2/twitter"
import { type Web2ProofParser } from "./web2/parsers"
Expand All @@ -12,7 +11,7 @@ import { Web2CoreTargetIdentityPayload } from "@kynesyslabs/demosdk/abstraction"
* @returns true if the proof is valid, false otherwise
*/
export async function verifyWeb2Proof(payload: Web2CoreTargetIdentityPayload) {
let parser: typeof Web2ProofParser
let parser: typeof TwitterProofParser | typeof GithubProofParser

switch (payload.context) {
case "twitter":
Expand Down
Loading