Skip to content

Commit 2714283

Browse files
authored
Merge pull request #44 from pattern-tech/feat/wallet-accounting
Feat/wallet accounting
2 parents 781ab71 + 55ea643 commit 2714283

16 files changed

Lines changed: 642 additions & 61 deletions

File tree

.github/workflows/dev.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
-e MORALIS_API_KEY=${{ secrets.MORALIS_API_KEY }} \
7474
-e ETHER_SCAN_API_KEY=${{ secrets.ETHER_SCAN_API_KEY }} \
7575
-e GOLDRUSH_API_KEY=${{ secrets.GOLDRUSH_API_KEY }} \
76-
-e ETH_RPC_URL=${{ secrets.ETH_RPC_URL }} \
76+
-e ETH_RPC=${{ secrets.ETH_RPC }} \
7777
-e LLM_PROVIDER=${{ secrets.LLM_PROVIDER }} \
7878
-e LLM_MODEL=${{ secrets.LLM_MODEL }} \
7979
-e LLM_API_KEY=${{ secrets.LLM_API_KEY }} \

api/.env.sample

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ EXA_API_KEY=
5555
PERPLEXITY_API_KEY=
5656
TAVILY_API_KEY=
5757

58-
ETH_RPC_URL=
58+
ETH_RPC=
5959

6060

6161
#------------------------

api/src/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from src.agent.routers import agent_router
66
from src.project.routers import project_router
77
from src.workspace.routers import workspace_router
8+
from src.query_usage.routers import query_usage_router
89
from src.conversation.routers import playground_conversation_router
910

1011
api_router = APIRouter()
@@ -14,4 +15,5 @@
1415
api_router.include_router(project_router.router, tags=["Project"])
1516
api_router.include_router(agent_router.router, tags=["Agent"])
1617
api_router.include_router(
17-
playground_conversation_router.router, tags=["Conversation"])
18+
playground_conversation_router.router, tags=["Conversation"])
19+
api_router.include_router(query_usage_router.router, tags=["Query Usage"])

api/src/auth/routers/auth_router.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
from sqlalchemy.orm import Session
2+
from fastapi import APIRouter, Depends
13
from src.auth.utils.bcrypt_helper import generate_access_token
24
from src.auth.services.auth_service import (
35
AuthService,
46
LoginInput,
57
RegisterInput,
68
VerifyInput
79
)
10+
811
from src.db.sql_alchemy import Database
912
from src.util.response import global_response
10-
from fastapi import APIRouter, Depends, HTTPException
11-
from sqlalchemy.orm import Session
1213

1314
router = APIRouter(prefix="/auth")
1415
database = Database()
@@ -69,19 +70,18 @@ def login(input: LoginInput, db: Session = Depends(database.get_db)):
6970
summary="Verify a Signature",
7071
description="Verify a signature according to SIWE spec",
7172
)
72-
def verify(input: VerifyInput):
73+
def verify(input: VerifyInput, db: Session = Depends(database.get_db)):
7374
"""
7475
Verify a signature according to SIWE spec and return an access token
7576
7677
- **message**: A SIWE message
7778
- **signature**: User's signature for the message
7879
"""
79-
verification_result = auth.verify_signature(
80-
input.message, input.signature)
8180

82-
payload = {"id": "{}:{}".format(
83-
verification_result["chain_id"], verification_result["address"])}
81+
user = auth.verify_signature(
82+
input.message, input.signature, db)
8483

84+
payload = {"id": str(user.id)}
8585
token = generate_access_token(data=payload)
8686
return global_response(
8787
{

api/src/auth/services/auth_service.py

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
1+
from typing import Optional
2+
from siwe import SiweMessage
13
from fastapi import HTTPException
2-
from pydantic import BaseModel, Field, EmailStr
34
from sqlalchemy.orm import Session
4-
from src.workspace.services.workspace_service import WorkspaceService
5-
from src.auth.utils.bcrypt_helper import hash_password, verify_password
5+
from pydantic import BaseModel, Field, EmailStr
6+
67
from src.db.models import UserModel
78
from src.db.sql_alchemy import Database
8-
from siwe import SiweMessage
9+
from src.share.base_types import WalletAddress
10+
from src.user.services.user_service import UserService
11+
from src.workspace.services.workspace_service import WorkspaceService
12+
from src.auth.utils.bcrypt_helper import hash_password, verify_password
913

1014
database = Database()
1115

1216

1317
class RegisterInput(BaseModel):
14-
email: EmailStr = Field(
15-
..., example="user@example.com", description="The email address of the user"
18+
email: Optional[EmailStr] = Field(
19+
None, example="user@example.com", description="The email address of the user"
1620
)
17-
password: str = Field(
21+
password: Optional[str] = Field(
22+
None, example="securepassword123", description="The password for the user account"
23+
)
24+
wallet_address: WalletAddress = Field(
1825
...,
19-
example="securepassword123",
20-
description="The password for the user account",
26+
example="0x0...",
27+
description="The wallet address of the user"
28+
)
29+
chain_id: int = Field(
30+
...,
31+
example="1",
32+
description="The chain id of the user"
2133
)
2234

2335

@@ -47,34 +59,44 @@ class AuthService:
4759
"""
4860

4961
def __init__(self):
50-
self.workspace = WorkspaceService()
62+
self.workspace_service = WorkspaceService()
63+
self.user_service = UserService()
5164

5265
def register(self, input: RegisterInput, db: Session) -> str:
5366
"""
5467
Registers a new user by saving their details into the database.
5568
5669
Args:
57-
input (RegisterInput): The registration input containing email and password.
70+
input (RegisterInput): The registration input .
5871
db (Session): The database session for executing queries.
5972
6073
Returns:
6174
str: Success message indicating the user was registered.
6275
6376
Raises:
64-
HTTPException: If a user with the same email already exists.
77+
HTTPException: If a user with the same wallet_address already exists.
6578
"""
6679
# Check if the user already exists
67-
existing_user = db.query(UserModel).filter_by(email=input.email.lower()).first()
80+
existing_user = db.query(UserModel).filter_by(
81+
wallet_address=input.wallet_address).first()
6882
if existing_user:
6983
raise HTTPException(status_code=400, detail="User already exists")
7084

71-
# Hash the user's password and create a new user record
72-
hashed_password = hash_password(input.password)
73-
new_user = UserModel(email=input.email.lower(), password=hashed_password)
74-
db.add(new_user)
75-
db.commit()
85+
# # Create a new user record
86+
if input.email and input.password:
87+
existing_user = db.query(UserModel).filter_by(
88+
email=input.email.lower()).first()
89+
if existing_user:
90+
raise HTTPException(
91+
status_code=400, detail="This email is already exists")
92+
93+
if input.password:
94+
input.password = hash_password(input.password)
95+
96+
new_user = self.user_service.create_user(
97+
db, input.wallet_address, input.chain_id, input.email, input.password, )
7698

77-
self.workspace.create_workspace(db, "Default", new_user.id)
99+
self.workspace_service.create_workspace(db, "Default", new_user.id)
78100

79101
return new_user
80102

@@ -93,16 +115,19 @@ def authenticate_user(self, email: str, password: str, db: Session):
93115
"""
94116
# Fetch the user from the database using the provided email
95117
user = db.query(UserModel).filter_by(email=email).first()
118+
print(password, user.password)
96119
if not user:
97-
raise HTTPException(status_code=401, detail="Incorrect email or password")
120+
raise HTTPException(
121+
status_code=401, detail="Incorrect email or password")
98122

99123
# Verify the provided password matches the stored hash
100124
if not verify_password(password, user.password):
101-
raise HTTPException(status_code=401, detail="Incorrect email or password")
125+
raise HTTPException(
126+
status_code=401, detail="Incorrect email or password")
102127

103128
return user
104129

105-
def verify_signature(self, message: str, signature: str):
130+
def verify_signature(self, message: str, signature: str, db: Session):
106131
"""
107132
Verify a signature according to SIWE spec
108133
@@ -116,7 +141,6 @@ def verify_signature(self, message: str, signature: str):
116141
Raises:
117142
HTTPException: If message cannot be parsed or verified
118143
"""
119-
120144
try:
121145
siwe_message = SiweMessage.from_message(message)
122146
siwe_message.verify(signature)
@@ -128,7 +152,15 @@ def verify_signature(self, message: str, signature: str):
128152
raise HTTPException(
129153
status_code=401, detail="Signature is not valid")
130154

131-
return {
132-
"chain_id": siwe_message.chain_id,
133-
"address": siwe_message.address,
134-
}
155+
# check if not exist create new user
156+
user = self.user_service.get_user_by_wallet_address(
157+
siwe_message.address, db)
158+
if not user:
159+
user = self.register(RegisterInput(
160+
email=None,
161+
password=None,
162+
wallet_address=siwe_message.address,
163+
chain_id=siwe_message.chain_id
164+
), db)
165+
166+
return user

api/src/conversation/routers/playground_conversation_router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ async def send_message(
272272
dict: A JSON response containing the complete message data if `stream` is false.
273273
"""
274274
try:
275+
service.check_user_eligibility(db, user_id)
276+
275277
if input.stream:
276278
return StreamingResponse(
277279
service.send_message(db,

api/src/conversation/services/conversation_service.py

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
from uuid import UUID
22
from typing import List
3+
from datetime import timedelta
34
from sqlalchemy.orm import Session
5+
from fastapi import HTTPException, status
46
from langchain_core.messages.human import HumanMessage
57

6-
from src.db.models import Conversation
78
from src.util.configuration import Config
89
from src.agentflow.agents.hub import AgentHub
9-
from src.agentflow.utils.tools_index import get_all_tools
10+
from src.db.models import Conversation, QueryUsage
11+
from src.user.services.user_service import UserService
12+
from src.share.staked_tokens import get_user_staked_tokens
1013
from src.agent.services.memory_service import MemoryService
1114
from src.project.services.project_service import ProjectService
1215
from src.agent.services.agent_service import RouterAgentService
16+
from src.query_usage.services.query_usage_service import QueryUsageService
1317
from src.conversation.repositories.conversation_repository import ConversationRepository
1418

1519

@@ -22,6 +26,8 @@ def __init__(self):
2226
self.repository = ConversationRepository()
2327
self.memory_service = MemoryService()
2428
self.project_service = ProjectService()
29+
self.query_usage_service = QueryUsageService()
30+
self.user_service = UserService()
2531

2632
def create_conversation(
2733
self, db_session: Session, name: str, project_id: UUID, user_id: UUID
@@ -132,6 +138,56 @@ def get_project_associated_with_conversation(self, db_session: Session, conversa
132138
"""
133139
return self.repository.get_project_associated_with_conversation(db_session, conversation_id)
134140

141+
def check_user_eligibility(self, db_session: Session, user_id: UUID) -> bool:
142+
"""
143+
Checks if a user is eligible to use the service by checking their payment status.
144+
145+
Args:
146+
db_session (Session): The database session.
147+
user_id (UUID): The ID of the user to check.
148+
149+
Returns:
150+
bool: True if the user is eligible, False otherwise.
151+
152+
Raises:
153+
Exception: If the user is not eligible, an exception is raised with a message explaining why.
154+
"""
155+
user = self.user_service.get_user(db_session, user_id)
156+
157+
whitelist = self.user_service.get_whitelist(db_session)
158+
159+
# check user payment
160+
for wl in whitelist:
161+
if str(user_id) == str(wl.user_id):
162+
max_allowed_query = wl.max_query
163+
break
164+
else:
165+
staked_morpheus = get_user_staked_tokens(
166+
wallet_address=user.wallet_address, provider="morpheus")
167+
168+
if staked_morpheus == 0:
169+
raise Exception(
170+
"You need to stake Morpheus tokens to use this service")
171+
172+
usage_setting = self.query_usage_service.get_usage_setting(
173+
db_session)
174+
max_allowed_query = 0
175+
for setting in usage_setting:
176+
if setting.provider == "morpheus":
177+
max_allowed_query = setting.max_query * \
178+
(int(staked_morpheus) / 1e18)
179+
180+
user_query_usage_until_previous_24h = self.query_usage_service.get_all_query_usages(
181+
db_session, user_id, "morpheus",
182+
timedelta(hours=24))
183+
184+
if len(user_query_usage_until_previous_24h) >= max_allowed_query:
185+
raise Exception(
186+
"You have reached your daily query limit. Please try again tomorrow or stake more to get more queries."
187+
)
188+
189+
return True
190+
135191
async def send_message(
136192
self,
137193
db_session: Session,
@@ -163,6 +219,7 @@ async def send_message(
163219
Raises:
164220
Exception: If associated project is not found
165221
"""
222+
166223
config = Config.get_config()
167224

168225
sub_agents = AgentHub().get_agents(config["agents"])
@@ -173,9 +230,14 @@ async def send_message(
173230
sub_agents=sub_agents, memory=memory, streaming=stream)
174231

175232
if stream:
176-
# Stream tokens as they become available.
177-
async for token in agent.stream(message):
178-
yield token
233+
try:
234+
# Stream tokens as they become available.
235+
async for token in agent.stream(message):
236+
yield token
237+
except Exception as e:
238+
raise HTTPException(
239+
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
240+
)
179241
else:
180242
result = agent.ask(message)
181243

@@ -192,6 +254,13 @@ async def send_message(
192254
"intermediate_steps": intermediate_steps
193255
}
194256

257+
query_usage = QueryUsage(
258+
user_id=user_id,
259+
provider="morpheus",
260+
)
261+
self.query_usage_service.create_query_usage(
262+
db_session, query_usage)
263+
195264
def get_history(
196265
self,
197266
db_session: Session,

0 commit comments

Comments
 (0)