Skip to content

Added first wallet transaction reward endpoint#471

Closed
SergeyG-Solicy wants to merge 4 commits intotestnetfrom
feature/first-wallet-transaction-reward
Closed

Added first wallet transaction reward endpoint#471
SergeyG-Solicy wants to merge 4 commits intotestnetfrom
feature/first-wallet-transaction-reward

Conversation

@SergeyG-Solicy
Copy link
Contributor

@SergeyG-Solicy SergeyG-Solicy commented Sep 29, 2025

Summary by CodeRabbit

  • New Features

    • Earn bonus points automatically for your first wallet transaction; totals update after a successful transfer.
    • Points breakdown now shows a “First Wallet Transaction” category and includes Telegram under social accounts.
    • Duplicate rewards are prevented to ensure fairness.
  • Bug Fixes

    • Ensured points breakdown fields are initialized to avoid missing/undefined values.
  • Refactor

    • Improved background processing and logging around first-transaction rewards.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 29, 2025

Walkthrough

Adds FIRST_WALLET_TRANSACTION points and supporting breakdown fields, implements awardFirstTransactionReward and non-blocking first-transaction reward checks/awards triggered for native transactions, updates account initialization to include telegram and firstWalletTransaction, and adjusts executeNativeTransaction to return operations on success.

Changes

Cohort / File(s) Summary
Incentive Points System
src/features/incentive/PointSystem.ts
Adds FIRST_WALLET_TRANSACTION to point values; initializes and exposes breakdown.firstWalletTransaction; updates addPointsToGCR to accept "firstWalletTransaction"; adds awardFirstTransactionReward(userId, transactionHash) with idempotency, logging, RPC-like responses, and error handling.
GCR Model & Account Init
src/model/entities/GCRv2/GCR_Main.ts, ... (account creation in src/libs/blockchain/gcr/handleGCR.ts)
Extends JSONB points shape to include socialAccounts.telegram: number and breakdown.firstWalletTransaction: number; ensures account creation initializes telegram and firstWalletTransaction to 0.
GCR Handling / Reward Workflow
src/libs/blockchain/gcr/handleGCR.ts
Adds native-transaction detection, participant extraction, hasAlreadyReceivedFirstTransactionReward, and checkAndAwardFirstTransactionReward methods; invokes non-blocking reward checks/awards for native tx participants after applying GCR edits; imports PointSystem; logs errors without failing main flow.
Blockchain Routine Return Shape
src/libs/blockchain/routines/executeNativeTransaction.ts
Success branch for simple native transfers now returns [success, message, operations] (includes operations array) instead of two-element tuple; no changes to error branches.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant RT as executeNativeTransaction
  participant BC as Blockchain
  participant GCR as HandleGCR
  participant PS as PointSystem

  User->>RT: initiate native transfer
  RT->>BC: submit tx
  BC-->>RT: success (txHash), operations
  RT-->>User: [ok, txHash, operations]

  note right of RT: Post-success (non-blocking)
  RT->>GCR: apply GCR edits (tx)
  GCR-->>GCR: detect native tx & extract participants
  par For each participant (async, non-blocking)
    GCR->>PS: hasAlreadyReceivedFirstTransactionReward(address)?
    alt not received
      GCR->>PS: awardFirstTransactionReward(userId, txHash)
      PS-->>GCR: success / totalPoints
    else already received or error
      PS-->>GCR: skip / error logged
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Integrated Discord Identity #464 — Similar PointSystem/GCR changes adding new reward types and social fields (e.g., discord); likely overlaps in points schema and addPointsToGCR handling.

Suggested labels

Possible security concern, Review effort 3/5

Poem

A rabbit hops where ledgers gleam,
First wallet blinks — two points! I beam.
Async carrots tumble in a row,
Idempotent hops keep errors low. 🥕
I log a cheer and scamper slow.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Added first wallet transaction reward endpoint” succinctly captures the primary change in the pull request, which is the introduction of an RPC-style endpoint and associated logic for awarding points on a user’s first wallet transaction. It directly reflects the developer’s main objective without extraneous details or vague phrasing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/first-wallet-transaction-reward

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a93bf27 and c51d0c3.

📒 Files selected for processing (4)
  • src/features/incentive/PointSystem.ts (6 hunks)
  • src/libs/blockchain/gcr/handleGCR.ts (4 hunks)
  • src/libs/blockchain/routines/executeNativeTransaction.ts (1 hunks)
  • src/model/entities/GCRv2/GCR_Main.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/model/entities/GCRv2/GCR_Main.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/features/incentive/PointSystem.ts (1)
src/utilities/logger.ts (1)
  • error (125-132)
src/libs/blockchain/gcr/handleGCR.ts (3)
src/utilities/logger.ts (1)
  • error (125-132)
src/features/incentive/PointSystem.ts (1)
  • PointSystem (21-948)
src/libs/blockchain/transaction.ts (1)
  • Transaction (44-525)
🔇 Additional comments (12)
src/libs/blockchain/routines/executeNativeTransaction.ts (1)

81-82: LGTM: Operations now returned for successful native transactions.

The addition of the operations array to the return tuple is correct and aligns with the function signature. This enables downstream consumers to track the pending operations.

src/features/incentive/PointSystem.ts (5)

18-18: LGTM: Point value constant added.

The FIRST_WALLET_TRANSACTION constant follows the existing pattern and is properly defined.


127-127: LGTM: Breakdown fields extended for new reward types.

The additions of telegram and firstWalletTransaction fields with safe defaults are consistent with the existing breakdown structure.

Also applies to: 131-132


148-148: LGTM: Type parameter extended.

The type parameter correctly includes "firstWalletTransaction" to support the new reward type.


203-220: LGTM: Defensive initialization added.

The breakdown structure is now properly initialized with all required fields, including telegram and firstWalletTransaction. The explicit check at Lines 218-220 ensures firstWalletTransaction is set even on legacy accounts with partial breakdown structures.


242-247: LGTM: First transaction points handling added.

The firstWalletTransaction type is properly handled with safe fallback for the old value. The defensive initialization at Lines 203-220 ensures the breakdown structure exists before this code executes.

src/libs/blockchain/gcr/handleGCR.ts (6)

52-52: LGTM: PointSystem import added.

Required for the first-transaction reward functionality.


535-535: LGTM: Account initialization includes new reward fields.

The telegram and firstWalletTransaction fields are properly initialized to 0 in new accounts.

Also applies to: 539-539


559-576: Pre-check helper relies on race-prone award method.

This method provides a preliminary check but returns false on errors, which may trigger unnecessary retries. Ensure the downstream awardFirstTransactionReward method (Lines 884-947 in PointSystem.ts) is fixed to be truly idempotent under concurrency before relying on this helper.


578-614: Orchestration logic is sound but should run in background job.

The check-and-award flow is correctly structured with nested error handling to prevent crashing the GCR apply path. However, this should be enqueued as a background job (as noted for Lines 374-393) rather than executed inline, to ensure retries on failure and avoid blocking GCR operations.


616-620: LGTM: Native transaction detection is correct.

The logic correctly identifies native transactions with positive amounts.


622-635: LGTM: Participant extraction handles edge cases.

The method correctly deduplicates participants, handles self-transfers, and filters invalid addresses.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

In awardFirstTransactionReward, the duplicate-prevention check relies solely on a non-zero breakdown value, which could be bypassed by concurrent requests. Consider adding an atomic check/update or unique marker to prevent race conditions that may double-award points.

if (userPointsWithIdentities.breakdown.firstWalletTransaction > 0) {
    return {
        result: 200,
        response: {
            pointsAwarded: 0,
            totalPoints: userPointsWithIdentities.totalPoints,
            message: "First transaction reward already awarded",
            alreadyAwarded: true,
        },
        require_reply: false,
        extra: { transactionHash },
    }
}

await this.addPointsToGCR(
    userId,
    pointValues.FIRST_WALLET_TRANSACTION,
    "firstWalletTransaction",
    "",
)
Input Validation

The RPC handler requires publicKey and validates transactionType but does not validate transactionHash presence/format before passing to awardFirstTransactionReward. Add validation to avoid processing incomplete/invalid requests and to improve logs.

    const params = payload.params[0]
    const { publicKey, transactionType } = params

    if (!publicKey) {
        return {
            result: 400,
            response: "Missing required parameter: publicKey",
            require_reply: false,
            extra: null,
        }
    }

    if (transactionType !== "first_wallet_transaction") {
        return {
            result: 400,
            response:
                "Invalid transaction type. Expected 'first_wallet_transaction'",
            require_reply: false,
            extra: null,
        }
    }

    const result = await pointSystem.awardFirstTransactionReward(
        publicKey,
        params.transactionHash,
    )

    return result
}
Type Safety

addPointsToGCR now accepts a union including "firstWalletTransaction" but still requires a platform string; passing an empty string may be error-prone. Consider making platform optional or conditional by type to avoid accidental misuse.

    userId: string,
    points: number,
    type: "web3Wallets" | "socialAccounts" | "firstWalletTransaction",
    platform: string,
    referralCode?: string,
    twitterUserId?: string,
): Promise<void> {

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Sep 29, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
New reward endpoint lacks transaction validation

The new awardFirstTransactionReward RPC endpoint is insecure because it does not
validate the transactionHash, allowing anyone to claim points fraudulently. This
should be replaced with a secure, backend-driven process that monitors the
blockchain for actual transactions before awarding points.

Examples:

src/libs/network/server_rpc.ts [305-336]
        case "awardFirstTransactionReward": {
            const pointSystem = PointSystem.getInstance()

            const params = payload.params[0]
            const { publicKey, transactionType } = params

            if (!publicKey) {
                return {
                    result: 400,
                    response: "Missing required parameter: publicKey",

 ... (clipped 22 lines)
src/features/incentive/PointSystem.ts [864-927]
    async awardFirstTransactionReward(
        userId: string,
        transactionHash: string,
    ): Promise<RPCResponse> {
        try {
            const userPointsWithIdentities = await this.getUserPointsInternal(
                userId,
            )

            if (userPointsWithIdentities.breakdown.firstWalletTransaction > 0) {

 ... (clipped 54 lines)

Solution Walkthrough:

Before:

// in server_rpc.ts
async function processPayload(payload) {
  switch (payload.method) {
    case "awardFirstTransactionReward": {
      const { publicKey, transactionHash } = payload.params[0];
      // No validation of transactionHash is performed.
      const result = await pointSystem.awardFirstTransactionReward(
        publicKey,
        transactionHash,
      );
      return result;
    }
  }
}

// in PointSystem.ts
async awardFirstTransactionReward(userId, transactionHash) {
  // ... check for duplicates ...
  // Award points without validating the transaction ever happened.
  await this.addPointsToGCR(...);
  log.info(`... Hash: ${transactionHash}`); // Hash is only used for logging.
}

After:

// Conceptual: No public RPC endpoint for awarding points.

// A new backend service listens to the blockchain
class BlockchainListener {
  listenForTransactions() {
    onNewTransaction(async (tx) => {
      const user = await findUserByAddress(tx.from);
      const isFirstTx = await isUsersFirstTransaction(user.id, tx.hash);

      if (user && isFirstTx) {
        // Call the point system internally after successful validation
        const pointSystem = PointSystem.getInstance();
        await pointSystem.awardFirstTransactionReward(user.id, tx.hash);
      }
    });
  }
}

// in PointSystem.ts (method could be made private)
async awardFirstTransactionReward(userId, transactionHash) {
  // ... check for duplicates ...
  // The caller is now a trusted internal service.
  await this.addPointsToGCR(...);
}
Suggestion importance[1-10]: 10

__

Why: The suggestion correctly identifies a critical security vulnerability where the new RPC endpoint awardFirstTransactionReward does not validate the transaction, allowing fraudulent point claims.

High
Possible issue
Prevent potential null pointer exception
Suggestion Impact:The commit adds a guard to initialize account.points.breakdown with default fields and ensures firstWalletTransaction is set to 0 if undefined before assignment, directly addressing the potential TypeError.

code diff:

+        if (!account.points.breakdown) {
+            account.points.breakdown = {
+                web3Wallets: {},
+                socialAccounts: {
+                    twitter: 0,
+                    github: 0,
+                    discord: 0,
+                    telegram: 0,
+                },
+                referrals: 0,
+                demosFollow: 0,
+                firstWalletTransaction: 0,
+            }
+        }
+
+        if (account.points.breakdown.firstWalletTransaction === undefined) {
+            account.points.breakdown.firstWalletTransaction = 0
+        }
+

In addPointsToGCR, add a check to initialize account.points.breakdown if it is
nullish before attempting to assign a value to
account.points.breakdown.firstWalletTransaction to prevent a potential
TypeError.

src/features/incentive/PointSystem.ts [222-227]

 } else if (type === "firstWalletTransaction") {
+    if (!account.points.breakdown) {
+        account.points.breakdown = {
+            web3Wallets: {},
+            socialAccounts: {},
+            referrals: 0,
+            demosFollow: 0,
+            firstWalletTransaction: 0,
+        }
+    }
     const oldFirstTransactionPoints =
-        account.points.breakdown?.firstWalletTransaction || 0
+        account.points.breakdown.firstWalletTransaction || 0
     account.points.breakdown.firstWalletTransaction =
         oldFirstTransactionPoints + points
 }

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential TypeError if account.points.breakdown is nullish, as the assignment on line 225 lacks optional chaining, unlike the read on line 224. This is a valid bug fix.

Medium
General
Add validation for missing parameter

In the awardFirstTransactionReward case, add a validation check to ensure the
transactionHash parameter exists in the request, returning a 400 error if it is
missing.

src/libs/network/server_rpc.ts [320-328]

 if (transactionType !== "first_wallet_transaction") {
     return {
         result: 400,
         response:
             "Invalid transaction type. Expected 'first_wallet_transaction'",
         require_reply: false,
         extra: null,
     }
 }
 
+if (!params.transactionHash) {
+    return {
+        result: 400,
+        response: "Missing required parameter: transactionHash",
+        require_reply: false,
+        extra: null,
+    }
+}
+
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out that the transactionHash parameter is used without validation. Adding this check improves the robustness of the new RPC endpoint by ensuring all required parameters are present.

Low
  • Update

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/model/entities/GCRv2/GCR_Main.ts (1)

37-41: Make field optional or ensure backfill/defaults

Older rows won’t have this JSON key. Typing it as required increases mismatch risk and can cause runtime writes to fail elsewhere if breakdown is missing. Prefer making it optional (or guarantee a 0 default on create/backfill).

-            firstWalletTransaction: number
+            firstWalletTransaction?: number
src/libs/network/server_rpc.ts (1)

370-377: Return JSON with proper headers for “/”

Align with other routes using jsonResponse (sets content-type, consistent formatting).

-    server.get("/", req => {
+    server.get("/", req => {
         const clientIP = rateLimiter.getClientIP(req, server.server)
-        return new Response(
-            JSON.stringify({
-                message: "Hello, World!",
-                yourIP: clientIP,
-            }),
-        )
+        return jsonResponse({
+            message: "Hello, World!",
+            yourIP: clientIP,
+        })
     })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0a13d0f and 3759b1f.

📒 Files selected for processing (3)
  • src/features/incentive/PointSystem.ts (5 hunks)
  • src/libs/network/server_rpc.ts (3 hunks)
  • src/model/entities/GCRv2/GCR_Main.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/features/incentive/PointSystem.ts (1)
src/utilities/logger.ts (1)
  • error (125-132)
src/libs/network/server_rpc.ts (1)
src/features/incentive/PointSystem.ts (1)
  • PointSystem (21-928)
🔇 Additional comments (4)
src/features/incentive/PointSystem.ts (4)

18-19: LGTM: point value defined

Constant fits existing pattern and type.


130-132: LGTM: safe read with fallback

Gracefully handles missing JSON key.


147-151: LGTM: type union extended

Public API surface matches new category.


144-156: Hidden message: awaiting script results.

Comment on lines +864 to +927
async awardFirstTransactionReward(
userId: string,
transactionHash: string,
): Promise<RPCResponse> {
try {
const userPointsWithIdentities = await this.getUserPointsInternal(
userId,
)

if (userPointsWithIdentities.breakdown.firstWalletTransaction > 0) {
return {
result: 200,
response: {
pointsAwarded: 0,
totalPoints: userPointsWithIdentities.totalPoints,
message: "First transaction reward already awarded",
alreadyAwarded: true,
},
require_reply: false,
extra: { transactionHash },
}
}

await this.addPointsToGCR(
userId,
pointValues.FIRST_WALLET_TRANSACTION,
"firstWalletTransaction",
"",
)

const updatedPoints = await this.getUserPointsInternal(userId)

log.info(
`[PointSystem] First transaction reward awarded to ${userId}. Hash: ${transactionHash}`,
)

return {
result: 200,
response: {
pointsAwarded: pointValues.FIRST_WALLET_TRANSACTION,
totalPoints: updatedPoints.totalPoints,
message: "Points awarded for first wallet transaction",
alreadyAwarded: false,
},
require_reply: false,
extra: { transactionHash },
}
} catch (error) {
log.error(
`[PointSystem] Error awarding first transaction reward to ${userId}: ${error}`,
)

return {
result: 500,
response: "Error awarding first transaction reward",
require_reply: false,
extra: {
error:
error instanceof Error ? error.message : String(error),
transactionHash,
},
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Race: not truly idempotent under concurrent calls

Two parallel calls can both see 0 and double‑award. Lock and recheck within a transaction (or perform a conditional update).

-    async awardFirstTransactionReward(
+    async awardFirstTransactionReward(
         userId: string,
         transactionHash: string,
     ): Promise<RPCResponse> {
         try {
-            const userPointsWithIdentities = await this.getUserPointsInternal(userId)
-            if (userPointsWithIdentities.breakdown.firstWalletTransaction > 0) {
-                return { ... }
-            }
-
-            await this.addPointsToGCR(
-                userId,
-                pointValues.FIRST_WALLET_TRANSACTION,
-                "firstWalletTransaction",
-                "",
-            )
-
-            const updatedPoints = await this.getUserPointsInternal(userId)
+            const db = await Datasource.getInstance()
+            const dataSource = db.getDataSource()
+            let updatedTotal = 0
+
+            const alreadyAwarded = await dataSource.transaction(
+                "READ COMMITTED",
+                async manager => {
+                    const account = await manager.findOne(GCRMain, {
+                        where: { pubkey: userId },
+                        lock: { mode: "pessimistic_write" }, // FOR UPDATE
+                    })
+                    if (!account) return false
+                    const current =
+                        account.points?.breakdown?.firstWalletTransaction || 0
+                    if (current > 0) return true
+
+                    // Initialize structure defensively
+                    account.points = account.points || { totalPoints: 0, breakdown: {} as any, lastUpdated: new Date() }
+                    account.points.breakdown = account.points.breakdown || {
+                        web3Wallets: {},
+                        socialAccounts: { twitter: 0, github: 0, discord: 0 },
+                        referrals: 0,
+                        demosFollow: 0,
+                        firstWalletTransaction: 0,
+                    }
+                    account.points.breakdown.firstWalletTransaction =
+                        (account.points.breakdown.firstWalletTransaction || 0) +
+                        pointValues.FIRST_WALLET_TRANSACTION
+                    account.points.totalPoints =
+                        (account.points.totalPoints || 0) +
+                        pointValues.FIRST_WALLET_TRANSACTION
+                    account.points.lastUpdated = new Date()
+                    await manager.save(account)
+                    updatedTotal = account.points.totalPoints
+                    return false
+                },
+            )
+
+            if (alreadyAwarded) {
+                return {
+                    result: 200,
+                    response: {
+                        pointsAwarded: 0,
+                        totalPoints: (await this.getUserPointsInternal(userId))
+                            .totalPoints,
+                        message: "First transaction reward already awarded",
+                        alreadyAwarded: true,
+                    },
+                    require_reply: false,
+                    extra: { transactionHash },
+                }
+            }
 
             log.info(
                 `[PointSystem] First transaction reward awarded to ${userId}. Hash: ${transactionHash}`,
             )
 
             return {
                 result: 200,
                 response: {
                     pointsAwarded: pointValues.FIRST_WALLET_TRANSACTION,
-                    totalPoints: updatedPoints.totalPoints,
+                    totalPoints: updatedTotal,
                     message: "Points awarded for first wallet transaction",
                     alreadyAwarded: false,
                 },
                 require_reply: false,
                 extra: { transactionHash },
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async awardFirstTransactionReward(
userId: string,
transactionHash: string,
): Promise<RPCResponse> {
try {
const userPointsWithIdentities = await this.getUserPointsInternal(
userId,
)
if (userPointsWithIdentities.breakdown.firstWalletTransaction > 0) {
return {
result: 200,
response: {
pointsAwarded: 0,
totalPoints: userPointsWithIdentities.totalPoints,
message: "First transaction reward already awarded",
alreadyAwarded: true,
},
require_reply: false,
extra: { transactionHash },
}
}
await this.addPointsToGCR(
userId,
pointValues.FIRST_WALLET_TRANSACTION,
"firstWalletTransaction",
"",
)
const updatedPoints = await this.getUserPointsInternal(userId)
log.info(
`[PointSystem] First transaction reward awarded to ${userId}. Hash: ${transactionHash}`,
)
return {
result: 200,
response: {
pointsAwarded: pointValues.FIRST_WALLET_TRANSACTION,
totalPoints: updatedPoints.totalPoints,
message: "Points awarded for first wallet transaction",
alreadyAwarded: false,
},
require_reply: false,
extra: { transactionHash },
}
} catch (error) {
log.error(
`[PointSystem] Error awarding first transaction reward to ${userId}: ${error}`,
)
return {
result: 500,
response: "Error awarding first transaction reward",
require_reply: false,
extra: {
error:
error instanceof Error ? error.message : String(error),
transactionHash,
},
}
}
}
async awardFirstTransactionReward(
userId: string,
transactionHash: string,
): Promise<RPCResponse> {
try {
// Begin a transaction to lock the user’s points row and prevent races
const db = await Datasource.getInstance();
const dataSource = db.getDataSource();
let updatedTotal = 0;
const alreadyAwarded = await dataSource.transaction(
"READ COMMITTED",
async manager => {
const account = await manager.findOne(GCRMain, {
where: { pubkey: userId },
lock: { mode: "pessimistic_write" }, // FOR UPDATE
});
if (!account) return false;
const current =
account.points?.breakdown?.firstWalletTransaction || 0;
if (current > 0) return true;
// Ensure the points structure is initialized
account.points = account.points || {
totalPoints: 0,
breakdown: {} as any,
lastUpdated: new Date(),
};
account.points.breakdown =
account.points.breakdown || {
web3Wallets: {},
socialAccounts: { twitter: 0, github: 0, discord: 0 },
referrals: 0,
demosFollow: 0,
firstWalletTransaction: 0,
};
// Award the first‐transaction bonus
account.points.breakdown.firstWalletTransaction =
(account.points.breakdown.firstWalletTransaction || 0) +
pointValues.FIRST_WALLET_TRANSACTION;
account.points.totalPoints =
(account.points.totalPoints || 0) +
pointValues.FIRST_WALLET_TRANSACTION;
account.points.lastUpdated = new Date();
await manager.save(account);
updatedTotal = account.points.totalPoints;
return false;
},
);
if (alreadyAwarded) {
return {
result: 200,
response: {
pointsAwarded: 0,
totalPoints: (
await this.getUserPointsInternal(userId)
).totalPoints,
message: "First transaction reward already awarded",
alreadyAwarded: true,
},
require_reply: false,
extra: { transactionHash },
};
}
log.info(
`[PointSystem] First transaction reward awarded to ${userId}. Hash: ${transactionHash}`,
);
return {
result: 200,
response: {
pointsAwarded: pointValues.FIRST_WALLET_TRANSACTION,
totalPoints: updatedTotal,
message: "Points awarded for first wallet transaction",
alreadyAwarded: false,
},
require_reply: false,
extra: { transactionHash },
};
} catch (error) {
log.error(
`[PointSystem] Error awarding first transaction reward to ${userId}: ${error}`,
);
return {
result: 500,
response: "Error awarding first transaction reward",
require_reply: false,
extra: {
error:
error instanceof Error ? error.message : String(error),
transactionHash,
},
};
}
}

Comment on lines +305 to +336
case "awardFirstTransactionReward": {
const pointSystem = PointSystem.getInstance()

const params = payload.params[0]
const { publicKey, transactionType } = params

if (!publicKey) {
return {
result: 400,
response: "Missing required parameter: publicKey",
require_reply: false,
extra: null,
}
}

if (transactionType !== "first_wallet_transaction") {
return {
result: 400,
response:
"Invalid transaction type. Expected 'first_wallet_transaction'",
require_reply: false,
extra: null,
}
}

const result = await pointSystem.awardFirstTransactionReward(
publicKey,
params.transactionHash,
)

return result
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Bind caller identity to publicKey (or restrict to SUDO)

As written, any authenticated caller can award this reward to any publicKey. Enforce sender === publicKey or allow SUDO to act on behalf of others.

         case "awardFirstTransactionReward": {
             const pointSystem = PointSystem.getInstance()

             const params = payload.params[0]
-            const { publicKey, transactionType } = params
+            const { publicKey, transactionType } = params

             if (!publicKey) {
               ...
             }

+            // Enforce caller binding (or SUDO override)
+            const isSudo = sender === getSharedState.SUDO_PUBKEY
+            if (!isSudo && publicKey !== sender) {
+                return {
+                    result: 401,
+                    response: "Unauthorized: caller does not match publicKey",
+                    require_reply: false,
+                    extra: null,
+                }
+            }
+
             if (transactionType !== "first_wallet_transaction") {
               ...
             }

+            if (
+                typeof params.transactionHash !== "string" ||
+                params.transactionHash.length === 0
+            ) {
+                return {
+                    result: 400,
+                    response: "Missing or invalid parameter: transactionHash",
+                    require_reply: false,
+                    extra: null,
+                }
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "awardFirstTransactionReward": {
const pointSystem = PointSystem.getInstance()
const params = payload.params[0]
const { publicKey, transactionType } = params
if (!publicKey) {
return {
result: 400,
response: "Missing required parameter: publicKey",
require_reply: false,
extra: null,
}
}
if (transactionType !== "first_wallet_transaction") {
return {
result: 400,
response:
"Invalid transaction type. Expected 'first_wallet_transaction'",
require_reply: false,
extra: null,
}
}
const result = await pointSystem.awardFirstTransactionReward(
publicKey,
params.transactionHash,
)
return result
}
case "awardFirstTransactionReward": {
const pointSystem = PointSystem.getInstance()
const params = payload.params[0]
const { publicKey, transactionType } = params
if (!publicKey) {
return {
result: 400,
response: "Missing required parameter: publicKey",
require_reply: false,
extra: null,
}
}
// Enforce caller binding (or SUDO override)
const isSudo = sender === getSharedState.SUDO_PUBKEY
if (!isSudo && publicKey !== sender) {
return {
result: 401,
response: "Unauthorized: caller does not match publicKey",
require_reply: false,
extra: null,
}
}
if (transactionType !== "first_wallet_transaction") {
return {
result: 400,
response:
"Invalid transaction type. Expected 'first_wallet_transaction'",
require_reply: false,
extra: null,
}
}
if (
typeof params.transactionHash !== "string" ||
params.transactionHash.length === 0
) {
return {
result: 400,
response: "Missing or invalid parameter: transactionHash",
require_reply: false,
extra: null,
}
}
const result = await pointSystem.awardFirstTransactionReward(
publicKey,
params.transactionHash,
)
return result
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/libs/blockchain/routines/executeNativeTransaction.ts (3)

34-50: Avoid TOCTOU pre-check; drop this helper and let the award method enforce idempotency.

This helper duplicates the check that awardFirstTransactionReward already performs, adds extra I/O, and widens the race window. Prefer calling awardFirstTransactionReward directly and handling its “already awarded” response.

Apply this diff to remove the helper:

-async function hasAlreadyReceivedFirstTransactionReward(
-    address: string,
-): Promise<boolean> {
-    try {
-        const pointSystem = PointSystem.getInstance()
-
-        const response = await pointSystem.getUserPoints(address)
-
-        if (response.result === 200 && response.response) {
-            return response.response.breakdown.firstWalletTransaction > 0
-        }
-
-        return false
-    } catch (error) {
-        return false
-    }
-}

52-92: Refactor to call the idempotent award API directly, dedupe addresses, and handle both outcomes.

Removes the TOCTOU pre-check, reduces DB calls, and narrows race windows. Also handles sender===receiver explicitly and logs “already awarded” responses.

-async function checkAndAwardFirstTransactionRewards(
-    sender: string,
-    receiver: string,
-    txHash: string,
-): Promise<void> {
-    const pointSystem = PointSystem.getInstance()
-
-    try {
-        if (!(await hasAlreadyReceivedFirstTransactionReward(sender))) {
-            log.info(
-                `[FirstTransactionReward] Awarding points to sender: ${sender} for tx: ${txHash}`,
-            )
-
-            try {
-                await pointSystem.awardFirstTransactionReward(sender, txHash)
-            } catch (error) {
-                log.error(
-                    `[FirstTransactionReward] Failed to award points to sender ${sender}: ${error}`,
-                )
-            }
-        }
-
-        if (!(await hasAlreadyReceivedFirstTransactionReward(receiver))) {
-            log.info(
-                `[FirstTransactionReward] Awarding points to receiver: ${receiver} for tx: ${txHash}`,
-            )
-
-            try {
-                await pointSystem.awardFirstTransactionReward(receiver, txHash)
-            } catch (error) {
-                log.error(
-                    `[FirstTransactionReward] Failed to award points to receiver ${receiver}: ${error}`,
-                )
-            }
-        }
-    } catch (error) {
-        log.error(
-            `[FirstTransactionReward] Error checking first transaction rewards: ${error}`,
-        )
-    }
-}
+async function checkAndAwardFirstTransactionRewards(
+    sender: string,
+    receiver: string,
+    txHash: string,
+): Promise<void> {
+    const pointSystem = PointSystem.getInstance()
+    const addresses = sender === receiver ? [sender] : [sender, receiver]
+
+    await Promise.allSettled(
+        addresses.map(async addr => {
+            try {
+                log.info(
+                    `[FirstTransactionReward] Attempting award for ${addr} (tx: ${txHash})`,
+                )
+                const res = await pointSystem.awardFirstTransactionReward(addr, txHash)
+                if (res.result !== 200) {
+                    log.error(
+                        `[FirstTransactionReward] awardFirstTransactionReward returned ${res.result} for ${addr} (tx: ${txHash})`,
+                    )
+                } else if (res.response?.alreadyAwarded) {
+                    log.info(
+                        `[FirstTransactionReward] Already awarded for ${addr}, skipping (tx: ${txHash})`,
+                    )
+                }
+            } catch (error) {
+                log.error(
+                    `[FirstTransactionReward] Failed to award points to ${addr}: ${error}`,
+                )
+            }
+        }),
+    )
+}

52-92: Ensure true single-award semantics under concurrency across nodes.

awardFirstTransactionReward (in PointSystem) reads then writes; without an atomic guard, two nodes could award simultaneously. Please confirm the repository performs an atomic “set if zero” or enforces a unique constraint for this reward type per user, or wrap in a DB transaction/compare-and-set to prevent double-awards.

You could persist a reward ledger with a unique key on (userId, rewardType) and upsert using a single atomic write; or use $setOnInsert with a unique index in Mongo/SQL equivalent. Relevant to the call sites here. (Based on relevant code snippet: PointSystem.awardFirstTransactionReward.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3759b1f and a93bf27.

📒 Files selected for processing (1)
  • src/libs/blockchain/routines/executeNativeTransaction.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/libs/blockchain/routines/executeNativeTransaction.ts (2)
src/features/incentive/PointSystem.ts (1)
  • PointSystem (21-928)
src/utilities/logger.ts (1)
  • error (125-132)
🔇 Additional comments (1)
src/libs/blockchain/routines/executeNativeTransaction.ts (1)

20-21: LGTM on new imports.

Assuming project path aliases are configured, these look correct.

If not already, ensure tsconfig paths resolve "@/features/" and "@/utilities/" at runtime (ts-node/build tooling).

Comment on lines +144 to +153
checkAndAwardFirstTransactionRewards(
sender,
receiver,
transaction.hash,
).catch(error => {
log.error(
`[FirstTransactionReward] Background reward processing failed: ${error}`,
)
})

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t award pre-consensus; trigger after the operation is applied/confirmed.

executeNativeTransaction runs before consensus/GCR apply (see note at Lines 12–15). Awarding here can grant points for transactions that never finalize or get reorged.

Prefer hooking into the GCR apply/commit path (when the “add_native/remove_native” ops transition to applied) or a block-finalization event, then enqueue award work (idempotent) via a job queue/outbox to ensure durability and retries.

🤖 Prompt for AI Agents
In src/libs/blockchain/routines/executeNativeTransaction.ts around lines
144–153, the code awards first-transaction rewards during
executeNativeTransaction which runs before consensus/GCR apply; move this logic
out of the pre-consensus path. Remove the direct call to
checkAndAwardFirstTransactionRewards here and instead enqueue an idempotent
"award-first-transaction" job (including tx hash, sender, receiver, and
metadata) from the GCR apply/commit path or block-finalization handler when the
"add_native"/"remove_native" ops transition to applied; use the outbox/job-queue
for durability and retries, ensure the worker that processes the job is
idempotent and logs errors (with full error details) rather than awarding
inline.

@SergeyG-Solicy SergeyG-Solicy changed the title [WIP] Added first wallet transaction reward endpoint Added first wallet transaction reward endpoint Sep 30, 2025
@tcsenpai tcsenpai closed this Dec 4, 2025
@tcsenpai tcsenpai deleted the feature/first-wallet-transaction-reward branch December 4, 2025 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants