dev@locazia: add blockchain routes, edit resolve content system to cid class, add web2 docs
This commit is contained in:
parent
78a4a84c2b
commit
d7ebc26bce
|
|
@ -5,3 +5,4 @@ logs
|
||||||
sqlStorage
|
sqlStorage
|
||||||
playground
|
playground
|
||||||
alembic.ini
|
alembic.ini
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ from app.api.routes.auth import s_api_v1_auth_twa
|
||||||
from app.api.routes.tonconnect import s_api_tonconnect_manifest
|
from app.api.routes.tonconnect import s_api_tonconnect_manifest
|
||||||
from app.api.routes.node_storage import s_api_v1_storage_post, s_api_v1_storage_get
|
from app.api.routes.node_storage import s_api_v1_storage_post, s_api_v1_storage_get
|
||||||
from app.api.routes.account import s_api_v1_account_get
|
from app.api.routes.account import s_api_v1_account_get
|
||||||
from app.api.routes.custodial import s_api_v1_custodial_upload_content
|
from app.api.routes._blockchain import s_api_v1_blockchain_send_new_content_message, \
|
||||||
|
s_api_v1_blockchain_send_purchase_content_message
|
||||||
|
|
||||||
app.add_route(s_index, "/")
|
app.add_route(s_index, "/")
|
||||||
|
|
||||||
|
|
@ -31,7 +32,8 @@ app.add_route(s_api_v1_storage_get, "/api/v1/storage/<file_hash>", methods=["GET
|
||||||
|
|
||||||
app.add_route(s_api_v1_account_get, "/api/v1/account", methods=["GET", "OPTIONS"])
|
app.add_route(s_api_v1_account_get, "/api/v1/account", methods=["GET", "OPTIONS"])
|
||||||
|
|
||||||
app.add_route(s_api_v1_custodial_upload_content, "/api/v1/custodial.uploadContent", methods=["POST", "OPTIONS"])
|
app.add_route(s_api_v1_blockchain_send_new_content_message, "/api/v1/blockchain.sendNewContentMessage", methods=["POST", "OPTIONS"])
|
||||||
|
app.add_route(s_api_v1_blockchain_send_purchase_content_message, "/api/v1/blockchain.sendPurchaseContentMessage", methods=["POST", "OPTIONS"])
|
||||||
|
|
||||||
|
|
||||||
@app.exception(BaseException)
|
@app.exception(BaseException)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
|
||||||
|
from sanic import response
|
||||||
|
|
||||||
|
|
||||||
|
def valid_royalty_params(royalty_params):
|
||||||
|
assert sum([x['value'] for x in royalty_params]) == 10000, "Values of royalties should sum to 10000"
|
||||||
|
for field_key, field_value in {
|
||||||
|
'address': lambda x: isinstance(x, str),
|
||||||
|
'value': lambda x: (isinstance(x, int) and 0 <= x <= 10000)
|
||||||
|
}.items():
|
||||||
|
assert field_key in royalty_params, f"No {field_key} provided"
|
||||||
|
assert field_value(royalty_params[field_key]), f"Invalid {field_key} provided"
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def s_api_v1_blockchain_send_new_content_message(request):
|
||||||
|
assert request.json, "No data provided"
|
||||||
|
|
||||||
|
for field_key, field_value in {
|
||||||
|
'title': lambda x: isinstance(x, str),
|
||||||
|
'authors': lambda x: isinstance(x, list),
|
||||||
|
'content': lambda x: isinstance(x, str),
|
||||||
|
'image': lambda x: isinstance(x, str),
|
||||||
|
'description': lambda x: isinstance(x, str),
|
||||||
|
'price': lambda x: (isinstance(x, str) and x.isdigit()),
|
||||||
|
'allowResale': lambda x: isinstance(x, bool),
|
||||||
|
'royaltyParams': lambda x: (isinstance(x, dict) and valid_royalty_params(x)),
|
||||||
|
|
||||||
|
}.items():
|
||||||
|
assert field_key in request.json, f"No {field_key} provided"
|
||||||
|
assert field_value(request.json[field_key]), f"Invalid {field_key} provided"
|
||||||
|
|
||||||
|
assert request.json.get("title"), "No title provided"
|
||||||
|
return response.json({"message": "Transaction requested"})
|
||||||
|
|
||||||
|
|
||||||
|
async def s_api_v1_blockchain_send_purchase_content_message(request):
|
||||||
|
assert request.json, "No data provided"
|
||||||
|
|
||||||
|
for field_key, field_value in {
|
||||||
|
'content_address': lambda x: isinstance(x, str),
|
||||||
|
'price': lambda x: (isinstance(x, str) and x.isdigit()),
|
||||||
|
}.items():
|
||||||
|
assert field_key in request.json, f"No {field_key} provided"
|
||||||
|
assert field_value(request.json[field_key]), f"Invalid {field_key} provided"
|
||||||
|
|
||||||
|
return response.json({"message": "Transaction requested"})
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
from sanic import response
|
|
||||||
from app.core._config import TELEGRAM_API_KEY
|
|
||||||
from app.core.models.user import User
|
|
||||||
from app.core.logger import make_log
|
|
||||||
from datetime import datetime
|
|
||||||
import os
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
|
|
||||||
async def s_api_v1_custodial_upload_content(request):
|
|
||||||
if not request.json:
|
|
||||||
return response.json({"error": "No data provided"}, status=400)
|
|
||||||
|
|
||||||
if not request.json.get('content'):
|
|
||||||
return response.json({"error": "No content provided"}, status=400)
|
|
||||||
|
|
||||||
if not request.json.get('content_hash'):
|
|
||||||
return response.json({"error": "No content hash provided"}, status=400)
|
|
||||||
|
|
||||||
content = request.json['content']
|
|
||||||
content_hash = request.json['content_hash']
|
|
||||||
|
|
||||||
known_user = request.ctx.db_session.query(User).filter(User.telegram_id == request.ctx.user.telegram_id).first()
|
|
||||||
if not known_user:
|
|
||||||
return response.json({"error": "User not found"}, status=400)
|
|
||||||
|
|
||||||
content_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), f"../../../content/{content_hash}")
|
|
||||||
if os.path.exists(content_path):
|
|
||||||
return response.json({"error": "Content already exists"}, status=400)
|
|
||||||
|
|
||||||
with open(content_path, "wb") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
return response.json({
|
|
||||||
'content_hash': content_hash
|
|
||||||
})
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
from sanic import response
|
from sanic import response
|
||||||
from app.core._config import UPLOADS_DIR
|
from app.core._config import UPLOADS_DIR
|
||||||
from app.core.storage import db_session
|
from app.core.storage import db_session
|
||||||
|
from app.core.content.content_id import ContentId
|
||||||
|
from app.core._utils.resolve_content import resolve_content
|
||||||
from app.core.models.node_storage import StoredContent
|
from app.core.models.node_storage import StoredContent
|
||||||
from app.core.logger import make_log
|
from app.core.logger import make_log
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from base58 import b58encode, b58decode
|
from base58 import b58encode, b58decode
|
||||||
|
from mimetypes import guess_type
|
||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
|
@ -22,13 +25,21 @@ async def s_api_v1_storage_post(request):
|
||||||
else:
|
else:
|
||||||
return response.json({"error": "No file provided"}, status=400)
|
return response.json({"error": "No file provided"}, status=400)
|
||||||
|
|
||||||
|
file_meta = {}
|
||||||
|
file_mimetype, file_encoding = guess_type(file_name)[0]
|
||||||
|
if file_mimetype:
|
||||||
|
file_meta["content_type"] = file_mimetype
|
||||||
|
|
||||||
|
if file_encoding:
|
||||||
|
file_meta["extension_encoding"] = file_encoding
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_hash = b58encode(hashlib.sha256(file_content).digest()).decode()
|
file_hash = b58encode(hashlib.sha256(file_content).digest()).decode()
|
||||||
new_content = StoredContent(
|
new_content = StoredContent(
|
||||||
hash=file_hash,
|
hash=file_hash,
|
||||||
filename=file_name,
|
filename=file_name,
|
||||||
user_id=None,
|
user_id=None,
|
||||||
meta={},
|
meta=file_meta,
|
||||||
created=datetime.now(),
|
created=datetime.now(),
|
||||||
key_id=None
|
key_id=None
|
||||||
)
|
)
|
||||||
|
|
@ -39,19 +50,30 @@ async def s_api_v1_storage_post(request):
|
||||||
with open(file_path, "wb") as file:
|
with open(file_path, "wb") as file:
|
||||||
file.write(file_content)
|
file.write(file_content)
|
||||||
|
|
||||||
return response.json({"content_sha256": file_hash})
|
new_content_id = new_content.cid
|
||||||
|
new_cid = new_content_id.serialize_v1()
|
||||||
|
|
||||||
|
return response.json({
|
||||||
|
"content_sha256": file_hash,
|
||||||
|
"content_id_v1": new_cid,
|
||||||
|
"content_url": f"dmy://storage?cid={new_cid}",
|
||||||
|
})
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
return response.json({"error": f"Error: {e}"}, status=500)
|
return response.json({"error": f"Error: {e}"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
async def s_api_v1_storage_get(request, file_hash):
|
async def s_api_v1_storage_get(request, content_id):
|
||||||
content = request.ctx.db_session.query(StoredContent).filter(StoredContent.hash == file_hash).first()
|
cid, errmsg = resolve_content(content_id)
|
||||||
|
if errmsg:
|
||||||
|
return response.json({"error": errmsg}, status=400)
|
||||||
|
|
||||||
|
content_sha256 = b58encode(cid.content_hash).decode()
|
||||||
|
content = request.ctx.db_session.query(StoredContent).filter(StoredContent.hash == content_sha256).first()
|
||||||
if not content:
|
if not content:
|
||||||
return response.json({"error": "File not found"}, status=404)
|
return response.json({"error": "File not found"}, status=404)
|
||||||
|
|
||||||
make_log(f"File {file_hash} requested by {request.ip}")
|
make_log(f"File {content_sha256} requested by {request.ip}")
|
||||||
|
file_path = os.path.join(UPLOADS_DIR, content_sha256)
|
||||||
file_path = os.path.join(UPLOADS_DIR, file_hash)
|
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
return response.json({"error": "File not found"}, status=404)
|
return response.json({"error": "File not found"}, status=404)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ async def t_tonconnect_dev_menu(message: types.Message, memory=None, user=None,
|
||||||
keyboard = []
|
keyboard = []
|
||||||
|
|
||||||
ton_connect, ton_connection = TonConnect.by_user(db_session, user, callback_fn=())
|
ton_connect, ton_connection = TonConnect.by_user(db_session, user, callback_fn=())
|
||||||
|
make_log("TonConnect_DevMenu", f"Available wallets: {ton_connect._sdk_client.get_wallets()}", level='debug')
|
||||||
await ton_connect.restore_connection()
|
await ton_connect.restore_connection()
|
||||||
make_log("TonConnect_DevMenu", f"SDK connected?: {ton_connect.connected}", level='info')
|
make_log("TonConnect_DevMenu", f"SDK connected?: {ton_connect.connected}", level='info')
|
||||||
if not ton_connect.connected:
|
if not ton_connect.connected:
|
||||||
|
|
@ -63,4 +64,6 @@ Use /dev_tonconnect <code>{wallet_app_name}</code> for connect to wallet."""
|
||||||
keyboard=get_inline_keyboard(keyboard) if keyboard else None
|
keyboard=get_inline_keyboard(keyboard) if keyboard else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# async def t_tonconnect_wallets_list()
|
||||||
|
|
||||||
router.message.register(t_tonconnect_dev_menu, Command('dev_tonconnect'))
|
router.message.register(t_tonconnect_dev_menu, Command('dev_tonconnect'))
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,9 @@ LOG_FILEPATH = f"{LOG_DIR}/{_now_str}.log"
|
||||||
WEB_APP_URLS = {
|
WEB_APP_URLS = {
|
||||||
'uploadContent': f"https://web2-client.vercel.app/uploadContent"
|
'uploadContent': f"https://web2-client.vercel.app/uploadContent"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ALLOWED_CONTENT_TYPES = [
|
||||||
|
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||||
|
'video/mp4', 'video/webm', 'video/ogg',
|
||||||
|
'audio/mpeg', 'audio/ogg', 'audio/wav',
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from app.core.content.content_id import ContentId
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_content(content_id): # -> [content, error]
|
||||||
|
try:
|
||||||
|
return ContentId.deserialize(content_id), None
|
||||||
|
except BaseException as e:
|
||||||
|
return None, f"{e}"
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
def string_to_bytes_fixed_size(src: str, size: int, encoding='utf-8') -> bytes:
|
||||||
|
assert type(src) is str, "src must be a string"
|
||||||
|
src_bin = src.encode(encoding)
|
||||||
|
assert len(src_bin) <= size, "src is too long"
|
||||||
|
return src_bin + b'\x00' * (size - len(src_bin))
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_string(src: bytes, encoding='utf-8') -> str:
|
||||||
|
assert type(src) is bytes, "src must be bytes"
|
||||||
|
return src.decode(encoding).rstrip('\x00')
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from app.core.content.content_id import ContentId
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
from app.core._utils.string_binary import string_to_bytes_fixed_size, bytes_to_string
|
||||||
|
from base58 import b58encode, b58decode
|
||||||
|
from app.core._config import ALLOWED_CONTENT_TYPES
|
||||||
|
|
||||||
|
# cid_v1#_ cid_version:int8 accept_type:uint120 content_sha256:uint256 onchain_index:uint128 = CIDv1;
|
||||||
|
|
||||||
|
|
||||||
|
class ContentId:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
content_hash: bytes = None, # only SHA256
|
||||||
|
onchain_index: int = None,
|
||||||
|
accept_type: str = 'image/jpeg'
|
||||||
|
):
|
||||||
|
self.content_hash = content_hash
|
||||||
|
self.onchain_index = onchain_index or -1
|
||||||
|
|
||||||
|
self.accept_type = accept_type
|
||||||
|
|
||||||
|
def serialize_v1(self) -> str:
|
||||||
|
at_bin = string_to_bytes_fixed_size(self.accept_type, 15)
|
||||||
|
assert len(self.content_hash) == 32, "Invalid hash length"
|
||||||
|
if self.onchain_index < 0:
|
||||||
|
oi_bin = b''
|
||||||
|
else:
|
||||||
|
oi_bin = self.onchain_index.to_bytes(16, 'big', signed=False)
|
||||||
|
assert len(oi_bin) == 16, "Invalid onchain_index"
|
||||||
|
|
||||||
|
return b58encode(
|
||||||
|
(1).to_bytes(1, 'big') # cid version
|
||||||
|
+ at_bin
|
||||||
|
+ self.content_hash
|
||||||
|
+ oi_bin
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_v1(cls, cid: str):
|
||||||
|
cid_bin = b58decode(cid)
|
||||||
|
(
|
||||||
|
cid_version,
|
||||||
|
accept_type,
|
||||||
|
content_sha256,
|
||||||
|
onchain_index
|
||||||
|
) = (
|
||||||
|
int.from_bytes(cid_bin[0:1], 'big'),
|
||||||
|
bytes_to_string(cid_bin[1:16]),
|
||||||
|
cid_bin[16:48],
|
||||||
|
int.from_bytes(cid_bin[48:], 'big') if len(cid_bin) > 48 else -1
|
||||||
|
)
|
||||||
|
assert cid_version == 1, "Invalid version"
|
||||||
|
content_type = accept_type.split('/')
|
||||||
|
assert '/'.join(content_type[0:2]) in ALLOWED_CONTENT_TYPES, "Invalid accept type"
|
||||||
|
assert len(content_sha256) == 32, "Invalid hash length"
|
||||||
|
return cls(
|
||||||
|
content_hash=content_sha256,
|
||||||
|
onchain_index=onchain_index,
|
||||||
|
accept_type=accept_type
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deserialize(cls, cid: str):
|
||||||
|
cid_version = int.from_bytes(b58decode(cid)[0:1], 'big')
|
||||||
|
if cid_version == 1:
|
||||||
|
return cls.from_v1(cid)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid cid version")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -4,12 +4,7 @@ from sqlalchemy.orm import relationship
|
||||||
from .base import AlchemyBase
|
from .base import AlchemyBase
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from base58 import b58encode, b58decode
|
from base58 import b58encode, b58decode
|
||||||
|
from app.core.content.content_id import ContentId
|
||||||
|
|
||||||
# DMY CID v1 specs
|
|
||||||
# 1. int8 cid version
|
|
||||||
# 2. int256 sha256 of content
|
|
||||||
# 3. int128 onchain content index
|
|
||||||
|
|
||||||
|
|
||||||
class StoredContent(AlchemyBase):
|
class StoredContent(AlchemyBase):
|
||||||
|
|
@ -37,13 +32,10 @@ class StoredContent(AlchemyBase):
|
||||||
user = relationship('User', uselist=False, foreign_keys=[user_id])
|
user = relationship('User', uselist=False, foreign_keys=[user_id])
|
||||||
key = relationship('KnownKey', uselist=False, foreign_keys=[key_id])
|
key = relationship('KnownKey', uselist=False, foreign_keys=[key_id])
|
||||||
|
|
||||||
def make_dmy_cid_v1(self) -> str:
|
@property
|
||||||
vhash = sha256(
|
def cid(self) -> ContentId:
|
||||||
(1).to_bytes(1, 'big') # cid version
|
return ContentId(
|
||||||
+ b58decode(hash)
|
content_hash=b58decode(self.hash),
|
||||||
+ (self.onchain_index or 0).to_bytes(16, 'big')
|
onchain_index=self.onchain_index,
|
||||||
).digest()
|
accept_type=self.meta.get('content_type', 'image/jpeg')
|
||||||
return b58encode(vhash).decode()
|
)
|
||||||
|
|
||||||
def make_deeplink(self):
|
|
||||||
return f"dmy://storage?cid={self.make_dmy_cid_v1()}"
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
## Web2 Client (through HTTP API)
|
||||||
|
|
||||||
|
### API Public Endpoints
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://music-gateway.letsw.app
|
||||||
|
– /api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Telegram WebApp Authorization
|
||||||
|
|
||||||
|
[Implementation](../app/api/routes/auth.py)
|
||||||
|
|
||||||
|
#### Request (POST, /api/v1/auth.twa, JSON)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
twa_data: window.Telegram.WebApp.initData
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (JSON)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
user: { ...User },
|
||||||
|
connected_wallet: null | {
|
||||||
|
version: string,
|
||||||
|
address: string,
|
||||||
|
ton_balance: string // nanoTON bignum
|
||||||
|
},
|
||||||
|
auth_v1_token: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use** `auth_v1_token` as `Authorization` header for all authorized requests.
|
||||||
|
|
||||||
|
### Upload file
|
||||||
|
|
||||||
|
[Implementation](../app/api/routes/node_storage.py)
|
||||||
|
|
||||||
|
#### Request (POST, /api/v1/storage, FormData)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
file: File
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (JSON)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
content_sha256: string,
|
||||||
|
content_id_v1: string,
|
||||||
|
content_url: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download file
|
||||||
|
|
||||||
|
[Implementation](../app/api/routes/node_storage.py)
|
||||||
|
|
||||||
|
#### Request (GET, /api/v1/storage/:content_id)
|
||||||
|
|
||||||
|
#### Response (File)
|
||||||
|
|
||||||
|
### Create new content
|
||||||
|
|
||||||
|
[Implementation](../app/api/routes/blockchain.py)
|
||||||
|
|
||||||
|
#### Request (POST, /api/v1/blockchain.sendNewContentMessage, JSON)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
title: string,
|
||||||
|
authors: list,
|
||||||
|
content: string, // recommended dmy://
|
||||||
|
image: string, // recommended dmy://
|
||||||
|
description: string,
|
||||||
|
price: string, // nanoTON bignum
|
||||||
|
resaleLicensePrice: string // nanoTON bignum (default = 0)
|
||||||
|
allowResale: boolean,
|
||||||
|
royaltyParams: [{
|
||||||
|
address: string,
|
||||||
|
value: number // 10000 = 100%
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (JSON)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
message: "Transaction requested"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Purchase content
|
||||||
|
|
||||||
|
[Implementation](../app/api/routes/blockchain.py)
|
||||||
|
|
||||||
|
#### Request (POST, /api/v1/blockchain.sendPurchaseContentMessage, JSON)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
content_address: string,
|
||||||
|
price: string // nanoTON bignum
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response (JSON)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
message: "Transaction requested"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
## Web2 Client Task #280224
|
||||||
|
|
||||||
|
1. В процессе изменения дизайна сделать все элементы по нормальному в отличие от того как сейчас: чтобы страница состояла из компонентов, а не монолитно написана.
|
||||||
|
2. Сделать чтобы при нажатии на кнопку "Загрузить контент" открывалось окно с "Перейдите в кошелек, вы запросили транзакцию" и если сервер в дополнении к обычному message вернул еще и walletLink, то отобразить кнопку для перехода в кошелек
|
||||||
|
3. Чтобы запросить транзакцию, нужно отправить запрос `docs/web2-client/UploadFile` с файлом и получить в ответ content_url, который после загрузки изображения и самого контента нужно приложить в запрос `docs/web2-client/CreateNewContent` в поле image и content соответственно
|
||||||
|
4. Желательно: сделать отображение загруженной обложки в виде карточки с кнопкой "Удалить" и "Изменить" (при нажатии на изменить открывается окно загрузки контента)
|
||||||
|
5. Обработать чтобы контент проходил полную цепочку загрузки (загрузка изображения, загрузка контента, запрос транзакции через бэкенд) и после всего вебапп закрывался через window.Telegram.WebApp.close()
|
||||||
|
6. Сделать дизайн как хочет Миша
|
||||||
|
7. Обработать ситуацию когда кошелек не подключен, то есть в ответе на запрос `docs/web2-client/auth.twa` приходит connected_wallet: null
|
||||||
Loading…
Reference in New Issue