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
209 changes: 208 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pymongo = "^4.11.3"
requests = "^2.32.3"
python-multipart = "^0.0.20"
sqlalchemy = "^2.0.39"
xrpl-py = "^4.1.0"

[build-system]
requires = ["poetry-core"]
Expand Down
16 changes: 15 additions & 1 deletion src/main/document/service/document_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
#2. 이를 db에 저장한다.
#3. client에게 보낸다.

from src.main.document.repository.document_repository import get_all_documents, save_document, get_document, get_documents_by_user
from src.main.document.repository.document_repository import get_all_documents, save_document, get_document
from src.main.user.repository.UserRepository import get_user
from src.main.nft.service.nft_service import process_nft_issuance_with_response
from src.main.document.dto.document import saveDocument, documentRequestDto
from datetime import datetime
import asyncio
Expand All @@ -29,6 +31,18 @@ async def save_document_service(request: documentRequestDto, user_id: str) :
#db에 저장하기
document_id = save_document(saved)

#point & grade 구하기
user = get_user(user_id)
point = user.get("point")
grade = user.get("nft_grade")
user_id = user.get("user_id")

#point 올리기기
point += 500

#nft 발급하기
await process_nft_issuance_with_response(user_id, grade, point)

return document_id

# 조회하기
Expand Down
22 changes: 22 additions & 0 deletions src/main/nft/dto/nft_dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pydantic import BaseModel
from typing import Dict
from datetime import datetime

#Nft 를 ResponseDto (checking 용)
class NftResponseDto(BaseModel):
user_wallet_id: str
point: int
nft_id: str
nft_grade: str
transaction_hash: str
issued_at: datetime
expired_at: datetime

#db 는 따로 nft 따로 만들어서 저장하기
class NftSaveDto(BaseModel):
nft_id: str
user_wallet: str
nft_grade: str
transaction_hash: str
issued_at: datetime
expired_at: datetime
28 changes: 28 additions & 0 deletions src/main/nft/repository/nft_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from src.config.mongodb import get_mongo_client
from src.main.nft.dto.nft_dto import NftSaveDto
from datetime import datetime


#db에 nft_grade 집어넣기랑 point 갱신하기
def save_userDB_nft(user_id:str, nft_grade: str, point: int):
client = get_mongo_client()
db = client['xrpedia-data']
nft_collection = db['wallets']

if not user_id:
raise Exception(404, detail="회원 정보를 찾을 수 없습니다.")

nft_collection.update_one(
{"user_id": user_id}, #필터
{"$set": {"point": point, "nft_grade": nft_grade}}
)



# repository/nft_repository.py
def save_nfts_bulk(response: NftSaveDto):
client = get_mongo_client()
db = client['xrpedia-data']
nft_collection = db['nft']

nft_collection.insert_one(response)
145 changes: 145 additions & 0 deletions src/main/nft/service/nft_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from src.main.nft.dto.nft_dto import NftResponseDto, NftSaveDto
from src.main.user.repository.UserRepository import get_user
from src.main.nft.repository.nft_repository import save_userDB_nft, save_nfts_bulk
from datetime import datetime, timezone, timedelta
import asyncio

# XRPL 관련 모듈
#pip install xrpl-py + python -m poetry add xrpl.py
from xrpl.asyncio.transaction import submit_and_wait
from xrpl.models.transactions.nftoken_mint import NFTokenMint, NFTokenMintFlag
from xrpl.asyncio.wallet import generate_faucet_wallet
from xrpl.wallet import Wallet
from xrpl.clients import JsonRpcClient
import json


# XRPL 설정
print("Connecting to Testnet...")
JSON_RPC_URL = "https://s.devnet.rippletest.net:51234/"
client = JsonRpcClient(JSON_RPC_URL)

# 등급별 Taxon 값 설정 (NFT 분류 번호)
NFT_GRADE_TAXON = {
"platinum": 4,
"gold": 3,
"silver": 2,
"bronze": 1 # 기본값
}

# 테스트 지갑 생성 (테스트넷용)
# generate~ 함수는 내부적으로 비동기 함수임(asyncio.run()) -> ㅇ미 비동기에서 비동기로 겹침침
async def generate_wallet ():
wallet = await generate_faucet_wallet(client=client)
return wallet, wallet.address

# XRPL 기반 NFT 민팅(XRPL 에서 NFT를 실제로 발급(MINt) 하는 핵심 함수)
# 개별 사용자 1명에게 NFT를 발급하는 함수
async def mint_nft_on_xrpl(user, grade, issuser_wallet, issuserAddr):
issued_at = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
expired_at = issued_at + timedelta(days=180)

mint_tx = NFTokenMint(
account=issuserAddr, # 발급자(대표자)
nftoken_taxon=NFT_GRADE_TAXON[grade], # NFT 분류 Id (의미를 부여) grade에 따라 자동 적용용
flags=NFTokenMintFlag.TF_TRANSFERABLE # NFT 가 전송 가능한 것인지 여부 설정정
)

try:
# xrpl에서 해당 정보를 트랜잭션에 넘기기기
# submit_and_wait(transaction, client(노드 클라이언트 자체), wallet(서명에 사용될 지갑 객체체))
response = await submit_and_wait(transaction=mint_tx, client=client, wallet=issuser_wallet)
# transaction 처리 결과
result = response.result

# 트랜잭션 해시 추출(트랜잭션 고유 ID) -> 블록 탐색기에서 이 해시로 NFT 상태 조회 가능
tx_hash = result['hash']
if not tx_hash or not isinstance(tx_hash, str):
raise Exception("트랜잭션 해시를 정상적으로 받지 못했습니다.")

# NFT ID 파싱을 위함
nft_id = ""

for node in result['meta']['AffectedNodes']:
node_data = node.get("CreatedNode") or node.get("ModifiedNode")
if node_data and node_data["LedgerEntryType"] == "NFTokenPage":
tokens = node_data.get("NewFields", {}).get("NFTokens") or node_data.get("FinalFields", {}).get("NFTokens")
if tokens:
for token in tokens:
nft = token.get("NFToken")
if nft and "NFTokenID" in nft:
nft_id = nft["NFTokenID"]
break
if nft_id:
break

if not nft_id:
raise Exception("NFT ID 추출 실패")

return {
"user_wallet": user.get("_id"),
"point": user.get("point"),
"nft_id": nft_id,
"nft_grade": user.get("nft_grade"),
"transaction_hash": tx_hash,
"issued_at": issued_at,
"expired_at": expired_at
}

except Exception as e:
print(f"NFT 민팅 실패: {e}")
return None

#grade 다음 단계 반환
async def next_grade(grade:str)-> str:
if not grade:
raise Exception(422, detail="none 입니다.")

if grade =="bronze":
return "silver"
elif grade == "silver":
return "gold"
elif grade == "gold":
return "platinum"
elif grade == "platinum":
return "platinum" # 이미 최고 등급이면 그대로 유지
else:
raise Exception("400: 유효하지 않은 등급입니다.")

# 전체 로직
async def process_nft_issuance_with_response(user_id: str, origin_grade:str, point: int):
issuser_wallet, issuserAddr = await generate_wallet()

#grade 다음 단계 구하기 로직
grade = await next_grade(origin_grade)

#user_id
user = get_user(user_id)

# 하나 발급 -> grade의 다음 단계로 받아오게끔 하기
result = await mint_nft_on_xrpl(user, grade, issuser_wallet, issuserAddr)

# DB 저장용 객체 변환
nft_records = NftSaveDto(
nft_id=result["nft_id"],
user_wallet=result["user_wallet"],
nft_grade=result["nft_grade"],
transaction_hash=result["transaction_hash"],
issued_at=result["issued_at"],
expired_at=result["expired_at"]
)
save_nfts_bulk(nft_records.model_dump())

#user에도 반영되도록 구현현
save_userDB_nft(user_id, grade, point)

# DTO 변환
print(NftResponseDto(
user_wallet_id=result["user_wallet"],
point=result["point"],
nft_id=result["nft_id"],
nft_grade=result["nft_grade"],
transaction_hash=result["transaction_hash"],
issued_at=result["issued_at"],
expired_at=result["expired_at"]
))
21 changes: 21 additions & 0 deletions src/main/user/repository/UserRepository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from src.config.mongodb import get_mongo_client

#db에서 user 가져오기
def get_user(user_id: str) -> str:
client = get_mongo_client()
db = client['xrpedia-data']
nft_collection = db['wallets']

if not user_id:
raise Exception(404, detail="회원 정보를 찾을 수 없습니다.")


user = nft_collection.find_one({"user_id": user_id})

if not user:
raise Exception(404, detail="해당 유저를 찾을 수 없습니다.")

# ObjectId는 JSON 직렬화가 안 되므로 필요 시 string으로 바꿔줍니다
user["_id"] = str(user["_id"])

return user