196 lines
5.0 KiB
Python
196 lines
5.0 KiB
Python
import flask
|
|
import pbc_client
|
|
import base64
|
|
import dataclasses
|
|
import requests_cache
|
|
from collections.abc import Iterator
|
|
import logging
|
|
|
|
HTML_INDEX = """
|
|
|
|
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
|
|
html {
|
|
background-color: #222;
|
|
color: white;
|
|
font-family: sans;
|
|
}
|
|
|
|
body {
|
|
width: 80%;
|
|
margin: auto;
|
|
}
|
|
|
|
.notamon-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(10, 1fr);
|
|
justify-items: center;
|
|
text-align: center;
|
|
gap: 4em 1em;
|
|
}
|
|
|
|
.notamon h2, h3 {
|
|
margin: 0;
|
|
}
|
|
|
|
</style>
|
|
<title>Hello from Flask</title>
|
|
</head>
|
|
<body>
|
|
<div class="notamon-grid">
|
|
|
|
{% for notamon in notamons %}
|
|
<div class="notamon">
|
|
<img src="{{ notamon.image_src }}" style="{{ notamon.effect_css }}">
|
|
<h2>{{ notamon.nickname }}</h2>
|
|
<h3>{{ notamon.species_name }}</h3>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class Notamon:
|
|
image_src: str
|
|
effect_css: str
|
|
nickname: str
|
|
species_name: str
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class NotamonNFTData:
|
|
nickname: str
|
|
owner: str
|
|
species_id: int
|
|
skin_id: int
|
|
effect_id: int
|
|
stat_hp: int
|
|
stat_attack: int
|
|
stat_defense: int
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class AssetVariable:
|
|
variable_id: int
|
|
asset_id: int
|
|
opened: bytes | None
|
|
|
|
|
|
|
|
app = flask.Flask(__name__)
|
|
|
|
TEST_NOTAMON = Notamon(
|
|
image_src = 'https://img.pokemondb.net/sprites/ruby-sapphire/normal/mudkip.png',
|
|
effect_css = '',
|
|
nickname = 'Dude',
|
|
species_name = 'Mudkip',
|
|
)
|
|
|
|
ADDRESS_NFTS = '02b82d36784146bd29415dbaa623a5f1cf6e6e1108'
|
|
ADDRESS_ASSETS = '036080dc8a15e496e5b970f65112b4fe4ac022e62c'
|
|
|
|
|
|
ASSET_ID_OFFSET_SPECIES = 0x0100_0000
|
|
ASSET_ID_OFFSET_SKIN = 0x0200_0000
|
|
ASSET_ID_OFFSET_EFFECT = 0x0300_0000
|
|
|
|
SESSION = requests_cache.CachedSession()
|
|
|
|
|
|
def compress(b: bytes) -> bytes:
|
|
l = []
|
|
for i in range(0, len(b)//8):
|
|
derp = 0
|
|
for j in range(0, 8):
|
|
derp |= b[i*8+j] << j
|
|
del j
|
|
l.append(derp)
|
|
del i, derp
|
|
return bytes(l)
|
|
|
|
|
|
def decode_opened_variable(value: str) -> bytes:
|
|
return compress(base64.b64decode(value))[4:]
|
|
|
|
|
|
def get_asset_variables(client, address) -> Iterator[AssetVariable]:
|
|
asset_contract_state, _ = client.get_contract_state(ADDRESS_ASSETS)
|
|
|
|
for variable in asset_contract_state['variables']:
|
|
asset_id = int.from_bytes(base64.b64decode(variable['value']['information']['data']))
|
|
opened = decode_opened_variable(variable['value']['openValue']['data']) if 'openValue' in variable['value'] else None
|
|
|
|
yield AssetVariable(
|
|
variable_id = variable['key'],
|
|
asset_id = asset_id,
|
|
opened = opened,
|
|
)
|
|
|
|
|
|
def to_base64_png(b: bytes) -> str:
|
|
return f'data:image/png;base64,{base64.b64encode(b)}'
|
|
|
|
|
|
def get_notamon_nfts(client: pbc_client.PbcClient) -> Iterator[NotamonNFTData]:
|
|
asset_contract_state, _ = client.get_typed_contract_state(ADDRESS_NFTS)
|
|
|
|
owners, _ = client.get_typed_contract_avl_tree(ADDRESS_NFTS, asset_contract_state['owners'])
|
|
attributes, _ = client.get_typed_contract_avl_tree(ADDRESS_NFTS, asset_contract_state['notamon_attributes'])
|
|
|
|
for notamon_id in owners:
|
|
attr = attributes.get(notamon_id)
|
|
yield NotamonNFTData(
|
|
nickname = 'Dude',
|
|
owner =owners.get(notamon_id),
|
|
species_id= attr['species_id']['id'],
|
|
skin_id = attr['skin_id']['id'],
|
|
effect_id = attr['effect_id']['id'],
|
|
stat_hp = attr['stat_hp'],
|
|
stat_attack = attr['stat_attack'],
|
|
stat_defense = attr['stat_defense'],
|
|
)
|
|
|
|
|
|
def get_asset(assets, asset_type_id, asset_type_offset) -> bytes | None:
|
|
asset_id = asset_type_id | asset_type_offset
|
|
asset = assets.get(asset_id)
|
|
if asset is None:
|
|
app.logger.error('Unknown asset with id %s = %s | %s', asset_id, asset_type_id, asset_type_offset)
|
|
return None
|
|
asset = asset.opened
|
|
if asset is None:
|
|
app.logger.warning('Secret asset with id %s = %s | %s', asset_id, asset_type_id, asset_type_offset)
|
|
return asset
|
|
|
|
def nft_to_notamon_view(nft: NotamonNFTData, assets) -> Notamon | None:
|
|
species_image_bytes = get_asset(assets, nft.species_id, ASSET_ID_OFFSET_SPECIES)
|
|
effect_str = get_asset(assets, nft.effect_id, ASSET_ID_OFFSET_EFFECT)
|
|
|
|
if species_image_bytes is None or effect_str is None:
|
|
return None
|
|
|
|
return Notamon(
|
|
image_src = to_base64_png(species_image_bytes),
|
|
effect_css = effect_str,
|
|
nickname = nft.nickname,
|
|
species_name = 'Mudkip',
|
|
)
|
|
|
|
def select_notamons():
|
|
client = pbc_client.PbcClient(SESSION, pbc_client.HOSTNAME_TESTNET)
|
|
nfts = list(get_notamon_nfts(client))
|
|
assets = {v.asset_id: v for v in get_asset_variables(client, ADDRESS_ASSETS)}
|
|
notamon_views = [nft_to_notamon_view(nft, assets) for nft in nfts]
|
|
return [n for n in notamon_views if n is not None]
|
|
|
|
@app.route("/")
|
|
def hello_world():
|
|
return flask.render_template_string(HTML_INDEX, notamons = select_notamons())
|