dev@locazia: make deploy blockchain route
This commit is contained in:
parent
44abce2aae
commit
9859121c40
|
|
@ -2,6 +2,8 @@ from app.core.models.user import User
|
||||||
from app.core.models.keys import KnownKey
|
from app.core.models.keys import KnownKey
|
||||||
from app.core.storage import Session
|
from app.core.storage import Session
|
||||||
from app.core.logger import make_log
|
from app.core.logger import make_log
|
||||||
|
from app.core._crypto.signer import Signer
|
||||||
|
from app.core._secrets import hot_seed
|
||||||
from sanic import response as sanic_response
|
from sanic import response as sanic_response
|
||||||
|
|
||||||
from base58 import b58encode, b58decode
|
from base58 import b58encode, b58decode
|
||||||
|
|
@ -54,11 +56,28 @@ async def try_authorization(request):
|
||||||
request.ctx.user_key = known_key
|
request.ctx.user_key = known_key
|
||||||
|
|
||||||
|
|
||||||
|
async def try_service_authorization(request):
|
||||||
|
signature = request.headers.get('X-Service-Signature')
|
||||||
|
if not signature:
|
||||||
|
return
|
||||||
|
|
||||||
|
message_hash_b58 = request.headers.get('X-Message-Hash')
|
||||||
|
if not message_hash_b58:
|
||||||
|
return
|
||||||
|
|
||||||
|
message_hash = b58decode(message_hash_b58)
|
||||||
|
signer = Signer(hot_seed)
|
||||||
|
if signer.verify(message_hash, signature):
|
||||||
|
request.ctx.verified_hash = message_hash
|
||||||
|
|
||||||
|
|
||||||
async def attach_user_to_request(request):
|
async def attach_user_to_request(request):
|
||||||
|
request.ctx.verified_hash = None
|
||||||
request.ctx.user = None
|
request.ctx.user = None
|
||||||
request.ctx.user_key = None
|
request.ctx.user_key = None
|
||||||
request.ctx.db_session = Session()
|
request.ctx.db_session = Session()
|
||||||
await try_authorization(request)
|
await try_authorization(request)
|
||||||
|
await try_service_authorization(request)
|
||||||
|
|
||||||
|
|
||||||
async def close_request_handler(request, response):
|
async def close_request_handler(request, response):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
from app.core._blockchain.ton.connect import TonConnect, unpack_wallet_info
|
from app.core._blockchain.ton.connect import TonConnect, unpack_wallet_info
|
||||||
|
from app.core.content.utils import create_metadata_for_item
|
||||||
|
from app.core.models.node_storage import StoredContent
|
||||||
from sanic import response
|
from sanic import response
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from app.core._blockchain.ton.platform import platform
|
||||||
|
from app.core._config import PROJECT_HOST
|
||||||
|
from base64 import b64encode, b64decode
|
||||||
|
from base58 import b58decode, b58encode
|
||||||
|
from app.core._utils.resolve_content import resolve_content
|
||||||
|
from tonsdk.boc import begin_cell, Cell, begin_dict
|
||||||
|
from tonsdk.utils import Address
|
||||||
|
|
||||||
|
|
||||||
def valid_royalty_params(royalty_params):
|
def valid_royalty_params(royalty_params):
|
||||||
|
|
@ -37,17 +46,84 @@ async def s_api_v1_blockchain_send_new_content_message(request):
|
||||||
await ton_connect.restore_connection()
|
await ton_connect.restore_connection()
|
||||||
assert ton_connect.connected, "No connected wallet"
|
assert ton_connect.connected, "No connected wallet"
|
||||||
|
|
||||||
|
encrypted_content_cid, err = resolve_content(request.json['content'])
|
||||||
|
assert not err, f"Invalid content CID"
|
||||||
|
encrypted_content = request.ctx.db_session.query(StoredContent).filter(
|
||||||
|
StoredContent.hash == encrypted_content_cid.content_hash_b58
|
||||||
|
).first()
|
||||||
|
assert encrypted_content, "No content locally found"
|
||||||
|
assert encrypted_content.type == "local/content_bin", "Invalid content type"
|
||||||
|
|
||||||
|
image_content_cid = resolve_content(request.json['image'])
|
||||||
|
assert not err, f"Invalid image CID"
|
||||||
|
image_content = request.ctx.db_session.query(StoredContent).filter(
|
||||||
|
StoredContent.hash == image_content_cid.content_hash_b58
|
||||||
|
).first()
|
||||||
|
assert image_content, "No image locally found"
|
||||||
|
|
||||||
# await ton_connect._sdk_client.send_transaction({
|
metadata_content = await create_metadata_for_item(
|
||||||
# 'valid_until': int(datetime.now().timestamp()),
|
request.ctx.db_session,
|
||||||
# 'messages': [
|
title=request.json['title'],
|
||||||
# {
|
cover_url=request.json['image'],
|
||||||
# 'address':
|
authors=request.json['authors']
|
||||||
# }
|
)
|
||||||
# ]
|
|
||||||
# })
|
|
||||||
|
|
||||||
|
royalties_dict = begin_dict(8)
|
||||||
|
i = 0
|
||||||
|
for royalty_param in request.json['royaltyParams']:
|
||||||
|
royalties_dict.store_ref(
|
||||||
|
i, begin_cell()
|
||||||
|
.store_address(Address(royalty_param['address']))
|
||||||
|
.store_uint(royalty_param['value'], 16)
|
||||||
|
.end_cell()
|
||||||
|
)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
await ton_connect._sdk_client.send_transaction({
|
||||||
|
'valid_until': int(datetime.now().timestamp() + 120),
|
||||||
|
'messages': [
|
||||||
|
{
|
||||||
|
'address': platform.address.to_string(1, 1, 1),
|
||||||
|
'amount': str(int(0.15 * 10 ** 9)),
|
||||||
|
'payload': b64encode(
|
||||||
|
begin_cell()
|
||||||
|
.store_uint(0x5491d08c, 32)
|
||||||
|
.store_uint(int(encrypted_content_cid.content_hash.hex()[2:], 16), 256)
|
||||||
|
.store_ref(
|
||||||
|
begin_cell()
|
||||||
|
.store_ref(
|
||||||
|
begin_cell()
|
||||||
|
.store_coins(int(request.json['price']))
|
||||||
|
.store_coins(int(1000000 * 10 ** 9))
|
||||||
|
.store_coins(int(5000000 * 10 ** 9))
|
||||||
|
.end_cell()
|
||||||
|
)
|
||||||
|
.store_maybe_ref(royalties_dict.end_dict())
|
||||||
|
.store_uint(0, 1)
|
||||||
|
.end_cell()
|
||||||
|
)
|
||||||
|
.store_ref(
|
||||||
|
begin_cell()
|
||||||
|
.store_ref(
|
||||||
|
begin_cell()
|
||||||
|
.store_uint(1, 8)
|
||||||
|
.store_bytes(f"{PROJECT_HOST}/api/v1/storage/{metadata_content.hash}".encode())
|
||||||
|
.end_cell()
|
||||||
|
)
|
||||||
|
.store_ref(
|
||||||
|
begin_cell()
|
||||||
|
.store_ref(begin_cell().store_bytes(f"{encrypted_content_cid.serialize_v1()}".encode()).end_cell())
|
||||||
|
.store_ref(begin_cell().store_bytes(f"{image_content_cid.serialize_v1()}".encode()).end_cell())
|
||||||
|
.store_ref(begin_cell().store_bytes(f"{metadata_content.serialize_v1()}".encode()).end_cell())
|
||||||
|
.end_cell()
|
||||||
|
)
|
||||||
|
.end_cell()
|
||||||
|
)
|
||||||
|
.end_cell().to_boc(False)
|
||||||
|
).decode()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
return response.json({"message": "Transaction requested"})
|
return response.json({"message": "Transaction requested"})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ import aiofiles
|
||||||
|
|
||||||
|
|
||||||
async def s_api_v1_storage_post(request):
|
async def s_api_v1_storage_post(request):
|
||||||
assert request.ctx.user, "No authorized"
|
if not request.files:
|
||||||
if not request.files and not request.json:
|
|
||||||
return response.json({"error": "No file provided"}, status=400)
|
return response.json({"error": "No file provided"}, status=400)
|
||||||
|
|
||||||
file_param = list(request.files.values())[0][0] if request.files else None
|
file_param = list(request.files.values())[0][0] if request.files else None
|
||||||
|
|
@ -34,7 +33,8 @@ async def s_api_v1_storage_post(request):
|
||||||
file_meta["extension_encoding"] = file_encoding
|
file_meta["extension_encoding"] = file_encoding
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_hash = b58encode(hashlib.sha256(file_content).digest()).decode()
|
file_hash_bin = hashlib.sha256(file_content).digest()
|
||||||
|
file_hash = b58encode(file_hash_bin).decode()
|
||||||
stored_content = request.ctx.db_session.query(StoredContent).filter(StoredContent.hash == file_hash).first()
|
stored_content = request.ctx.db_session.query(StoredContent).filter(StoredContent.hash == file_hash).first()
|
||||||
if stored_content:
|
if stored_content:
|
||||||
stored_cid = stored_content.cid.serialize_v1()
|
stored_cid = stored_content.cid.serialize_v1()
|
||||||
|
|
@ -44,6 +44,13 @@ async def s_api_v1_storage_post(request):
|
||||||
"content_url": f"dmy://storage?cid={stored_cid}",
|
"content_url": f"dmy://storage?cid={stored_cid}",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if request.ctx.user:
|
||||||
|
pass
|
||||||
|
elif request.ctx.verified_hash:
|
||||||
|
assert request.ctx.verified_hash == file_hash_bin, "Invalid service request hash"
|
||||||
|
else:
|
||||||
|
return response.json({"error": "Unauthorized"}, status=401)
|
||||||
|
|
||||||
new_content = StoredContent(
|
new_content = StoredContent(
|
||||||
type="local/content_bin",
|
type="local/content_bin",
|
||||||
user_id=request.ctx.user.id,
|
user_id=request.ctx.user.id,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from app.core.content.content_id import ContentId
|
from app.core.content.content_id import ContentId
|
||||||
|
|
||||||
|
|
||||||
def resolve_content(content_id): # -> [content, error]
|
def resolve_content(content_id) -> ContentId: # -> [content, error]
|
||||||
try:
|
try:
|
||||||
return ContentId.deserialize(content_id), None
|
return ContentId.deserialize(content_id), None
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ class ContentId:
|
||||||
|
|
||||||
self.accept_type = accept_type
|
self.accept_type = accept_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_hash_b58(self) -> str:
|
||||||
|
return b58encode(self.content_hash).decode()
|
||||||
|
|
||||||
def serialize_v1(self) -> str:
|
def serialize_v1(self) -> str:
|
||||||
at_bin = string_to_bytes_fixed_size(self.accept_type, 15)
|
at_bin = string_to_bytes_fixed_size(self.accept_type, 15)
|
||||||
assert len(self.content_hash) == 32, "Invalid hash length"
|
assert len(self.content_hash) == 32, "Invalid hash length"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,64 @@
|
||||||
from app.core.models.node_storage import StoredContent
|
from app.core.models.node_storage import StoredContent
|
||||||
|
from app.core._crypto.signer import Signer
|
||||||
|
from app.core._secrets import hot_seed
|
||||||
|
from hashlib import sha256
|
||||||
|
from base58 import b58encode, b58decode
|
||||||
|
|
||||||
|
from app.core._config import PROJECT_HOST
|
||||||
|
from httpx import AsyncClient
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
async def create_metadata_for_item(**kwargs):
|
async def create_metadata_for_item(
|
||||||
|
db_session,
|
||||||
|
title: str = None,
|
||||||
|
cover_url: str = None,
|
||||||
|
authors: list = None
|
||||||
|
) -> StoredContent:
|
||||||
|
assert title, "No title provided"
|
||||||
|
assert cover_url, "No cover_url provided"
|
||||||
|
assert len(title) > 3, "Title too short"
|
||||||
|
title = title[:100].strip()
|
||||||
|
|
||||||
|
item_metadata = {
|
||||||
|
'name': title,
|
||||||
|
'description': '@MY Content Ownership Proof NFT',
|
||||||
|
'attributes': [
|
||||||
|
# {
|
||||||
|
# 'trait_type': 'Artist',
|
||||||
|
# 'value': 'Unknown'
|
||||||
|
# },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if cover_url:
|
||||||
|
item_metadata['image'] = cover_url
|
||||||
|
|
||||||
|
item_metadata['authors'] = [
|
||||||
|
''.join([_a_ch for _a_ch in _a if len(_a_ch.encode()) == 1]) for _a in (authors or [])[:500]
|
||||||
|
]
|
||||||
|
|
||||||
|
signer = Signer(hot_seed)
|
||||||
|
|
||||||
|
# Upload file
|
||||||
|
metadata_bin = json.dumps(item_metadata).encode()
|
||||||
|
metadata_hash = sha256(metadata_bin).digest()
|
||||||
|
metadata_hash_b58 = b58encode(metadata_hash).decode()
|
||||||
|
|
||||||
|
async with AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{PROJECT_HOST}/api/v1/storage",
|
||||||
|
files={"file": ('metadata.json', metadata_bin, 'application/json')},
|
||||||
|
headers={
|
||||||
|
'X-Service-Signature': signer.sign(metadata_bin),
|
||||||
|
'X-Message-Hash': metadata_hash_b58,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
metadata_sha256 = response_json['content_sha256']
|
||||||
|
|
||||||
|
metadata_content = db_session.query(StoredContent).filter(StoredContent.hash == metadata_sha256).first()
|
||||||
|
if metadata_content:
|
||||||
|
return metadata_content
|
||||||
|
|
||||||
|
raise Exception("Metadata not created")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
## Indexation
|
||||||
|
|
||||||
|
### Stored content types
|
||||||
|
|
||||||
|
- `local/content_bin` – binary content stored only locally (or indexer no found it on chain)
|
||||||
|
- `onchain/content` - content stored onchain
|
||||||
|
- `onchain/content_unknown` - content stored onchain, but we don't have a private key to decrypt it
|
||||||
|
|
||||||
|
Content item may have multiple types, for example, `local/content_bin` and `onchain/content`.
|
||||||
|
|
||||||
|
But `content cover`, `content metadata` and `decrypted content` always stored locally.
|
||||||
|
|
||||||
|
### Content Ownership Proof NFT Values Cell Deserialization
|
||||||
|
|
||||||
|
```text
|
||||||
|
values:^[
|
||||||
|
content_hash:uint256
|
||||||
|
metadata:^[
|
||||||
|
offchain?:int1 = always 1
|
||||||
|
https://my-public-node-1.projscale.dev/*:bytes
|
||||||
|
]
|
||||||
|
content:^[
|
||||||
|
content_cid:^Cell = b58encoded CID
|
||||||
|
cover_cid:^Cell = b58encoded CID
|
||||||
|
metadata_cid:^Cell = b58encoded CID
|
||||||
|
]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available content statuses
|
||||||
|
|
||||||
|
- `UPLOAD_TO_BTFS` – content is stored locally, upload all content parts to BTFS. This status means that payment is received yet.
|
||||||
|
|
||||||
|
|
||||||
|
### Upload content flow
|
||||||
|
|
||||||
|
1. User uploads content to server (/api/v1/storage)
|
||||||
|
2. User uploads content cover to server (/api/v1/storage)
|
||||||
|
3. User send /api/v1/blockchain.sendNewContentMessage to server and accept the transaction in wallet
|
||||||
|
4. Indexer receives the transaction and indexes the content. And send telegram notification to user.
|
||||||
|
|
||||||
Loading…
Reference in New Issue