瀏覽代碼

Initial commit

master
jbell 2 月之前
當前提交
943f4d43d7
共有 38 個文件被更改,包括 500 次插入0 次删除
  1. +8
    -0
      .idea/.gitignore
  2. +12
    -0
      .idea/dataSources.xml
  3. +6
    -0
      .idea/httpClient.xml
  4. +6
    -0
      .idea/inspectionProfiles/Project_Default.xml
  5. +6
    -0
      .idea/inspectionProfiles/profiles_settings.xml
  6. +7
    -0
      .idea/misc.xml
  7. +8
    -0
      .idea/modules.xml
  8. +10
    -0
      .idea/puffpastry.iml
  9. +6
    -0
      .idea/vcs.xml
  10. 二進制
      app/__pycache__/main.cpython-312.pyc
  11. +0
    -0
      app/api/deps.py
  12. 二進制
      app/api/v1/__pycache__/api.cpython-312.pyc
  13. +8
    -0
      app/api/v1/api.py
  14. 二進制
      app/api/v1/endpoints/__pycache__/issue.cpython-312.pyc
  15. 二進制
      app/api/v1/endpoints/__pycache__/user.cpython-312.pyc
  16. +0
    -0
      app/api/v1/endpoints/comment.py
  17. +63
    -0
      app/api/v1/endpoints/issue.py
  18. +20
    -0
      app/api/v1/endpoints/user.py
  19. 二進制
      app/contracts/__pycache__/contract.cpython-312.pyc
  20. 二進制
      app/contracts/__pycache__/issue.cpython-312.pyc
  21. +106
    -0
      app/contracts/contract.py
  22. +42
    -0
      app/contracts/issue.py
  23. 二進制
      app/core/__pycache__/config.cpython-312.pyc
  24. +15
    -0
      app/core/config.py
  25. +0
    -0
      app/core/security.py
  26. 二進制
      app/crud/__pycache__/user.cpython-312.pyc
  27. +17
    -0
      app/crud/user.py
  28. 二進制
      app/db/__pycache__/base.cpython-312.pyc
  29. 二進制
      app/db/__pycache__/session.cpython-312.pyc
  30. +26
    -0
      app/db/base.py
  31. +17
    -0
      app/db/session.py
  32. +40
    -0
      app/main.py
  33. 二進制
      app/models/__pycache__/user.cpython-312.pyc
  34. +17
    -0
      app/models/user.py
  35. 二進制
      app/schemas/__pycache__/issue.cpython-312.pyc
  36. 二進制
      app/schemas/__pycache__/user.cpython-312.pyc
  37. +41
    -0
      app/schemas/issue.py
  38. +19
    -0
      app/schemas/user.py

+ 8
- 0
.idea/.gitignore 查看文件

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

+ 12
- 0
.idea/dataSources.xml 查看文件

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="postgres@localhost" uuid="94d2e7f4-35ff-4539-829f-43905c948f13">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

+ 6
- 0
.idea/httpClient.xml 查看文件

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="HttpClientEndpointsTabState">
<option name="requestToText" value="&lt;CachedHttpClientTabRequests&gt;&#10; &lt;entry key=&quot;63d726ce0c4905b3c3271c9a6717db45e9bccbbb&quot; value=&quot;###&amp;#10;GET http://localhost:8000/&quot; /&gt;&#10;&lt;/CachedHttpClientTabRequests&gt;" />
</component>
</project>

+ 6
- 0
.idea/inspectionProfiles/Project_Default.xml 查看文件

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

+ 6
- 0
.idea/inspectionProfiles/profiles_settings.xml 查看文件

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

+ 7
- 0
.idea/misc.xml 查看文件

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (puffpastry)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (puffpastry)" project-jdk-type="Python SDK" />
</project>

+ 8
- 0
.idea/modules.xml 查看文件

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/puffpastry.iml" filepath="$PROJECT_DIR$/.idea/puffpastry.iml" />
</modules>
</component>
</project>

+ 10
- 0
.idea/puffpastry.iml 查看文件

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

+ 6
- 0
.idea/vcs.xml 查看文件

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

二進制
app/__pycache__/main.cpython-312.pyc 查看文件


+ 0
- 0
app/api/deps.py 查看文件


二進制
app/api/v1/__pycache__/api.cpython-312.pyc 查看文件


+ 8
- 0
app/api/v1/api.py 查看文件

@@ -0,0 +1,8 @@
from fastapi import APIRouter

from app.api.v1.endpoints import issue, user

api_router = APIRouter()

api_router.include_router(issue.router, prefix="/issues", tags=["users"])
api_router.include_router(user.router, prefix="/user", tags=["users"])

二進制
app/api/v1/endpoints/__pycache__/issue.cpython-312.pyc 查看文件


二進制
app/api/v1/endpoints/__pycache__/user.cpython-312.pyc 查看文件


+ 0
- 0
app/api/v1/endpoints/comment.py 查看文件


+ 63
- 0
app/api/v1/endpoints/issue.py 查看文件

@@ -0,0 +1,63 @@
import pika
from fastapi import APIRouter

from datetime import datetime

from app.core.config import settings

from app.contracts.issue import IssueContract
from app.crud.user import get_user
from app.db.session import get_db_session
from app.schemas.issue import IssuePost, AddIssueRequest, VoteIssueRequest, AddIssueResponse

router = APIRouter()


@router.get("/list", response_model=list[IssuePost])
async def list_issues():
smart_contract = IssueContract(
settings.CONTRACT_ID,
settings.WALLET_ID
)
issue_list = await smart_contract.list_issues()
return issue_list


@router.get("/paragraphs")
async def get_paragraphs(issue_id: str):
smart_contract = IssueContract(
settings.CONTRACT_ID,
settings.WALLET_ID
)
paragraphs = await smart_contract.get_paragraphs(bytes.fromhex(issue_id))
return paragraphs


@router.post("/create", response_model=AddIssueResponse)
async def create_issue(request: AddIssueRequest):
smart_contract = IssueContract(
settings.CONTRACT_ID,
settings.WALLET_ID
)
async for session in get_db_session():
user = await get_user(session, request.session_id)
time_since_auth = datetime.now() - datetime.fromtimestamp(user.auth_date)
if user and time_since_auth.total_seconds() < settings.AUTH_TIMEOUT:
await smart_contract.add_issue(request.title, request.paragraphs, user.username)
return {'result': True}
else:
return {'result': False}
return {'result': False}


@router.post("/vote")
async def vote_issue(request: VoteIssueRequest):
smart_contract = IssueContract(
settings.CONTRACT_ID,
settings.WALLET_ID
)
if request.increase and not request.decrease:
await smart_contract.increase_vote(bytes.fromhex(request.issue_id))
elif request.decrease and not request.increase:
await smart_contract.decrease_vote(bytes.fromhex(request.issue_id))
return

+ 20
- 0
app/api/v1/endpoints/user.py 查看文件

@@ -0,0 +1,20 @@
from typing import Optional

from fastapi import APIRouter
from uuid import uuid4

from app.crud.user import add_user, get_user
from app.db.session import get_db_session
from app.schemas.user import User

router = APIRouter()


@router.post("/authenticate")
async def authenticate(user: dict):
session_id = uuid4()
session_obj = user
session_obj['session_id'] = str(session_id)
async for session in get_db_session():
await add_user(session, session_obj)
return {'session_id': session_id}

二進制
app/contracts/__pycache__/contract.cpython-312.pyc 查看文件


二進制
app/contracts/__pycache__/issue.cpython-312.pyc 查看文件


+ 106
- 0
app/contracts/contract.py 查看文件

@@ -0,0 +1,106 @@
import asyncio

from stellar_sdk import Keypair, Network, SorobanServer, TransactionBuilder, xdr as stellar_xdr
from stellar_sdk.exceptions import PrepareTransactionException
from stellar_sdk.soroban_rpc import GetTransactionStatus, SendTransactionStatus
from stellar_sdk.xdr import SCVal, SCValType


class SingletonMeta(type):
_instances = {}

def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]


class SmartContract(metaclass=SingletonMeta):
contract_id: str
user_key: str
def __init__(self, contract_id: str, user_key: str):
self.contract_id = contract_id
self.user_key = user_key

async def _execute_procedure(self, procedure_name: str, parameters=None):
source_keypair = Keypair.from_secret(self.user_key)

soroban_server = SorobanServer('https://soroban-testnet.stellar.org')

source_account = soroban_server.load_account(source_keypair.public_key)

built_transaction = (
TransactionBuilder(
source_account=source_account,
base_fee=100,
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
)
.append_invoke_contract_function_op(
contract_id=self.contract_id,
function_name=procedure_name,
parameters=parameters,
)
.set_timeout(30)
.build()
)

try:
prepared_transaction = soroban_server.prepare_transaction(built_transaction)
except PrepareTransactionException as e:
print(f"Exception preparing transaction: {e}\n{e.simulate_transaction_response.error}")
raise e

prepared_transaction.sign(source_keypair)

send_response = soroban_server.send_transaction(prepared_transaction)

if send_response.status != SendTransactionStatus.PENDING:
raise Exception("sending transaction failed")

while True:
get_response = soroban_server.get_transaction(send_response.hash)
if get_response.status != GetTransactionStatus.NOT_FOUND:
break
await asyncio.sleep(2)

print(f"get_transaction response: {get_response}")

if get_response.status == GetTransactionStatus.SUCCESS:
assert get_response.result_meta_xdr is not None

transaction_meta = stellar_xdr.TransactionMeta.from_xdr(
get_response.result_meta_xdr
)
return_value = transaction_meta.v3.soroban_meta.return_value
output = translate_soroban_value(return_value)
return output
else:
print(f"Transaction failed: {get_response.result_xdr}")


def translate_soroban_value(val: SCVal) -> int or str or bool or list[int or str or bool] or None:
def sanitize(k: str) -> str:
return k.lstrip('b\'').rstrip('\'')

type_handlers = {
SCValType.SCV_U32: lambda v: v.u32.uint32,
SCValType.SCV_I32: lambda v: v.i32.int32,
SCValType.SCV_U64: lambda v: v.u64.uint64,
SCValType.SCV_I64: lambda v: v.i64.int64,
SCValType.SCV_BOOL: lambda v: v.bool.boolean,
SCValType.SCV_STRING: lambda v: sanitize(str(v.str.sc_string)),
SCValType.SCV_SYMBOL: lambda v: sanitize(str(v.sym.sc_symbol)),
SCValType.SCV_BYTES: lambda v: bytes(v.bytes.sc_bytes),
SCValType.SCV_VEC: lambda v: [translate_soroban_value(item) for item in v.vec.sc_vec],
SCValType.SCV_MAP: lambda v: {translate_soroban_value(item.key): translate_soroban_value(item.val) for item in v.map.sc_map},
}

if val.type == SCValType.SCV_VOID:
return None

handler = type_handlers.get(val.type)
if handler:
return handler(val)
else:
raise ValueError(f"Unsupported SCVal type: {val.type}")

+ 42
- 0
app/contracts/issue.py 查看文件

@@ -0,0 +1,42 @@
from uuid import uuid4

from stellar_sdk.xdr import SCString, SCVal, SCBytes, SCVec, SCValType
from typing_extensions import TypeVar

from app.contracts.contract import SmartContract
from app.schemas.issue import IssuePost

T = TypeVar("T")

class IssueContract(SmartContract):
def __init__(self, contract_id, user_key):
super().__init__(contract_id, user_key)

async def list_issues(self):
issues = await self._execute_procedure("list_issues")
return [IssuePost(**issue) for issue in issues]

async def add_issue(self, title: str, paragraphs: list[str], telegram_handle: str):
issue_id = uuid4().bytes
print(f' => {telegram_handle}')
await self._execute_procedure("add_issue", [
SCVal(type=SCValType.SCV_BYTES, bytes=SCBytes(issue_id)),
SCVal(type=SCValType.SCV_STRING, str=SCString(title.encode('utf-8'))),
SCVal(type=SCValType.SCV_VEC, vec=SCVec([SCVal(SCValType.SCV_STRING, str=SCString(paragraph.encode('utf-8'))) for paragraph in paragraphs])),
SCVal(type=SCValType.SCV_STRING, str=SCString(telegram_handle.encode('utf-8'))),
])

async def get_paragraphs(self, issue_id: bytes):
return await self._execute_procedure("get_paragraphs_for_issue", [
SCVal(type=SCValType.SCV_BYTES, bytes=SCBytes(issue_id))
])

async def increase_vote(self, issue_id: bytes):
await self._execute_procedure("increase_positive_vote", [
SCVal(type=SCValType.SCV_BYTES, bytes=SCBytes(issue_id))
])

async def decrease_vote(self, issue_id: bytes):
await self._execute_procedure("increase_negative_vote", [
SCVal(type=SCValType.SCV_BYTES, bytes=SCBytes(issue_id))
])

二進制
app/core/__pycache__/config.cpython-312.pyc 查看文件


+ 15
- 0
app/core/config.py 查看文件

@@ -0,0 +1,15 @@
class Settings:
PROJECT_NAME = 'Puff Pastry'
VERSION = '0.1.0'
DESCRIPTION = 'A DAO platform on the Stellar network'
ALLOWED_ORIGINS = ('0.0.0.0', '127.0.0.1')
API_V1_STR = '/api/v1'

AUTH_TIMEOUT = 10800

WALLET_ID = 'SANMQAR5UXHAQNLXGYIDZDIPFH3DADD3UCS6EFXN52QBI4AK6YEDWDJ3'
CONTRACT_ID = 'CAZNJSDVIQSGMYBLUINX3CS4WC4N6ZEDZEK64P45XDXHXPL4R7OYM4HB'

DATABASE_URL = 'postgresql+asyncpg://mein:MxS2p37ViXtXikeb@localhost:5432/puffpastry'

settings = Settings()

+ 0
- 0
app/core/security.py 查看文件


二進制
app/crud/__pycache__/user.cpython-312.pyc 查看文件


+ 17
- 0
app/crud/user.py 查看文件

@@ -0,0 +1,17 @@
from sqlalchemy.ext.asyncio import AsyncSession

from sqlalchemy import insert, select

from app.models.user import user


async def add_user(session: AsyncSession, user_data: dict):
stmt = insert(user).values(**user_data)
await session.execute(stmt)

async def get_user(session: AsyncSession, session_id: str):
stmt = select(user).where(user.c.session_id == session_id)
result = await session.execute(stmt)
if result:
return result.first()
return None

二進制
app/db/__pycache__/base.cpython-312.pyc 查看文件


二進制
app/db/__pycache__/session.cpython-312.pyc 查看文件


+ 26
- 0
app/db/base.py 查看文件

@@ -0,0 +1,26 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings

# Create async engine
engine = create_async_engine(
settings.DATABASE_URL,
pool_pre_ping=True,
pool_size=10,
max_overflow=20
)

# Create a custom session class
AsyncSessionLocal = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False
)

# Create a base class for declarative models
Base = declarative_base()

+ 17
- 0
app/db/session.py 查看文件

@@ -0,0 +1,17 @@
from typing import AsyncGenerator
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession

from app.db.base import AsyncSessionLocal


async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except SQLAlchemyError as e:
await session.rollback()
raise
finally:
await session.close()

+ 40
- 0
app/main.py 查看文件

@@ -0,0 +1,40 @@
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.api.v1.api import api_router
from app.db.base import engine, Base

async def init_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

@asynccontextmanager
async def lifespan(application: FastAPI):
await init_db()
yield

app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.VERSION,
description=settings.DESCRIPTION,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
lifespan=lifespan,
)

# Set up CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Include API router
app.include_router(router=api_router, prefix=settings.API_V1_STR)

if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)

二進制
app/models/__pycache__/user.cpython-312.pyc 查看文件


+ 17
- 0
app/models/user.py 查看文件

@@ -0,0 +1,17 @@
from datetime import datetime, UTC

from sqlalchemy import Column, Integer, String, Table

from app.db.base import Base

user = Table(
"sessions",
Base.metadata,
Column("id", Integer, primary_key=True),
Column("session_id", String, nullable=False),
Column("auth_date", Integer, nullable=False),
Column("username", String, nullable=False),
Column("first_name", String, nullable=False),
Column("last_name", String, nullable=False),
Column("photo_url", String, nullable=False)
)

二進制
app/schemas/__pycache__/issue.cpython-312.pyc 查看文件


二進制
app/schemas/__pycache__/user.cpython-312.pyc 查看文件


+ 41
- 0
app/schemas/issue.py 查看文件

@@ -0,0 +1,41 @@
from pydantic import BaseModel, computed_field, PrivateAttr


class IssuePost(BaseModel):
_id: bytes = PrivateAttr()
title: str
summary: str
paragraph_count: int
positive_votes: int
negative_votes: int
telegram_handle: str
created_at: int

def __init__(self, id: bytes, **data):
super().__init__(**data)
self._id = id

@computed_field(return_type=str)
@property
def id(self) -> str:
return self._id.hex()

@id.setter
def id(self, value: bytes) -> None:
self._id = value


class AddIssueRequest(BaseModel):
session_id: str
title: str
paragraphs: list[str]


class AddIssueResponse(BaseModel):
result: bool


class VoteIssueRequest(BaseModel):
issue_id: str
increase: bool
decrease: bool

+ 19
- 0
app/schemas/user.py 查看文件

@@ -0,0 +1,19 @@
from pydantic import BaseModel


class User(BaseModel):
session_id: str
auth_date: int
username: str
first_name: str
last_name: str
photo_url: str


class AuthenticationRequest(BaseModel):
id: int
auth_date: int
username: str
first_name: str
last_name: str
photo_url: str

Loading…
取消
儲存