playweb-node 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- playweb/__init__.py +18 -0
- playweb/api/__init__.py +4 -0
- playweb/api/erc20.py +66 -0
- playweb/api/erc721.py +88 -0
- playweb/api/metadata.py +86 -0
- playweb/api/node_api.py +224 -0
- playweb/api/public_api.py +496 -0
- playweb/api/rpc.py +247 -0
- playweb/client.py +341 -0
- playweb/config.py +168 -0
- playweb/consensus/__init__.py +5 -0
- playweb/consensus/leader.py +50 -0
- playweb/consensus/nvf_bft.py +540 -0
- playweb/consensus/vote.py +108 -0
- playweb/core/__init__.py +6 -0
- playweb/core/block.py +156 -0
- playweb/core/blockchain.py +358 -0
- playweb/core/fee_engine.py +312 -0
- playweb/core/mempool.py +112 -0
- playweb/core/royalty_engine.py +207 -0
- playweb/core/transaction.py +270 -0
- playweb/network/__init__.py +6 -0
- playweb/network/bootstrap.py +217 -0
- playweb/network/gossip.py +222 -0
- playweb/network/peer_manager.py +164 -0
- playweb/network/sync.py +244 -0
- playweb/node.py +303 -0
- playweb/plugin/__init__.py +4 -0
- playweb/plugin/base_plugin.py +225 -0
- playweb/plugin/plugin_manager.py +131 -0
- playweb/registry/__init__.py +4 -0
- playweb/registry/content_registry.py +259 -0
- playweb/registry/edition_registry.py +220 -0
- playweb/storage/__init__.py +11 -0
- playweb/storage/base.py +154 -0
- playweb/storage/ram_storage.py +123 -0
- playweb/storage/sqlite_storage.py +369 -0
- playweb/storage/supabase_storage.py +389 -0
- playweb_node-1.0.0.dist-info/METADATA +416 -0
- playweb_node-1.0.0.dist-info/RECORD +43 -0
- playweb_node-1.0.0.dist-info/WHEEL +5 -0
- playweb_node-1.0.0.dist-info/licenses/LICENSE +201 -0
- playweb_node-1.0.0.dist-info/top_level.txt +1 -0
playweb/api/rpc.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network — Ethereum JSON-RPC Compatibility Layer
|
|
3
|
+
Allows MetaMask to connect to PlayWebit Network.
|
|
4
|
+
|
|
5
|
+
MetaMask setup:
|
|
6
|
+
RPC URL: https://your-node.com/rpc
|
|
7
|
+
Chain ID: 4968
|
|
8
|
+
Currency: PLWB
|
|
9
|
+
|
|
10
|
+
Contract address = Node wallet address (for both ERC20 + ERC721)
|
|
11
|
+
No Solidity. No deployment. No gas fees to deploy.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from flask import Blueprint, request, jsonify
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_rpc(node) -> Blueprint:
|
|
21
|
+
bp = Blueprint("rpc", __name__)
|
|
22
|
+
|
|
23
|
+
@bp.route("/rpc", methods=["POST"])
|
|
24
|
+
def rpc():
|
|
25
|
+
data = request.get_json(silent=True) or {}
|
|
26
|
+
method = data.get("method", "")
|
|
27
|
+
params = data.get("params", [])
|
|
28
|
+
req_id = data.get("id", 1)
|
|
29
|
+
|
|
30
|
+
def ok(val):
|
|
31
|
+
return jsonify({
|
|
32
|
+
"jsonrpc": "2.0",
|
|
33
|
+
"id": req_id,
|
|
34
|
+
"result": val,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
def err(code, msg):
|
|
38
|
+
return jsonify({
|
|
39
|
+
"jsonrpc": "2.0",
|
|
40
|
+
"id": req_id,
|
|
41
|
+
"error": {"code": code, "message": msg},
|
|
42
|
+
}), 400
|
|
43
|
+
|
|
44
|
+
# ── Network identity ──────────────────────────────────
|
|
45
|
+
if method == "eth_chainId":
|
|
46
|
+
return ok("0x1368") # 4968 in hex
|
|
47
|
+
|
|
48
|
+
if method == "net_version":
|
|
49
|
+
return ok("4968")
|
|
50
|
+
|
|
51
|
+
if method == "eth_blockNumber":
|
|
52
|
+
length = node.blockchain.get_chain_length()
|
|
53
|
+
return ok(hex(max(0, length - 1)))
|
|
54
|
+
|
|
55
|
+
if method == "eth_accounts":
|
|
56
|
+
return ok([])
|
|
57
|
+
|
|
58
|
+
if method == "eth_estimateGas":
|
|
59
|
+
return ok("0x5208") # 21000 fixed
|
|
60
|
+
|
|
61
|
+
if method == "eth_gasPrice":
|
|
62
|
+
return ok("0x1") # 1 wei fixed
|
|
63
|
+
|
|
64
|
+
if method == "eth_getTransactionCount":
|
|
65
|
+
return ok("0x1")
|
|
66
|
+
|
|
67
|
+
# ── PLWB native balance ───────────────────────────────
|
|
68
|
+
if method == "eth_getBalance":
|
|
69
|
+
addr = params[0] if params else "0x0"
|
|
70
|
+
balance = node.blockchain.get_balance(addr)
|
|
71
|
+
wei = int(balance * 10**18)
|
|
72
|
+
return ok(hex(wei))
|
|
73
|
+
|
|
74
|
+
# ── ERC20 + ERC721 via eth_call ───────────────────────
|
|
75
|
+
if method == "eth_call":
|
|
76
|
+
if not params or not isinstance(params[0], dict):
|
|
77
|
+
return ok("0x")
|
|
78
|
+
|
|
79
|
+
from playweb.api.erc20 import handle_erc20_call
|
|
80
|
+
from playweb.api.erc721 import handle_erc721_call
|
|
81
|
+
|
|
82
|
+
call_data = params[0].get("data", "0x")
|
|
83
|
+
to_addr = params[0].get("to", "").lower()
|
|
84
|
+
|
|
85
|
+
# Both ERC20 + ERC721 use node wallet as contract address
|
|
86
|
+
if to_addr == node.node_wallet.lower():
|
|
87
|
+
# Try ERC20 first (PLWB token)
|
|
88
|
+
result = handle_erc20_call(call_data, node)
|
|
89
|
+
if result is not None:
|
|
90
|
+
return ok(result)
|
|
91
|
+
|
|
92
|
+
# Then try ERC721 (NFT ownership)
|
|
93
|
+
result = handle_erc721_call(call_data, node)
|
|
94
|
+
if result is not None:
|
|
95
|
+
return ok(result)
|
|
96
|
+
|
|
97
|
+
return ok("0x")
|
|
98
|
+
|
|
99
|
+
# ── Transaction submission ────────────────────────────
|
|
100
|
+
if method == "eth_sendRawTransaction":
|
|
101
|
+
raw_tx = params[0] if params else None
|
|
102
|
+
if not raw_tx:
|
|
103
|
+
return err(-32602, "Missing raw transaction")
|
|
104
|
+
try:
|
|
105
|
+
from eth_account import Account
|
|
106
|
+
from eth_account.messages import encode_defunct
|
|
107
|
+
from playweb.core.transaction import Transaction
|
|
108
|
+
import time
|
|
109
|
+
|
|
110
|
+
decoded = Account.decode_transaction(raw_tx)
|
|
111
|
+
from_addr = decoded.get("from", "")
|
|
112
|
+
if not from_addr:
|
|
113
|
+
# recover from signature
|
|
114
|
+
from_addr = Account.recover_transaction(raw_tx)
|
|
115
|
+
|
|
116
|
+
tx = Transaction(
|
|
117
|
+
from_addr = from_addr.lower(),
|
|
118
|
+
to_addr = decoded["to"].lower(),
|
|
119
|
+
amount = decoded["value"] / 10**18,
|
|
120
|
+
tx_type = "transfer",
|
|
121
|
+
signature = raw_tx,
|
|
122
|
+
nonce = decoded.get("nonce", int(time.time())),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
success, result = node.blockchain.add_transaction(
|
|
126
|
+
tx = tx,
|
|
127
|
+
node_wallet = node.node_wallet,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if success:
|
|
131
|
+
node.gossip.broadcast_transaction(
|
|
132
|
+
tx = tx,
|
|
133
|
+
peers = node.peer_manager.get_active_peers(),
|
|
134
|
+
)
|
|
135
|
+
return ok(tx.hash)
|
|
136
|
+
else:
|
|
137
|
+
return err(-32000, result)
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"eth_sendRawTransaction error: {e}")
|
|
141
|
+
return err(-32000, str(e))
|
|
142
|
+
|
|
143
|
+
# ── Transaction queries ───────────────────────────────
|
|
144
|
+
if method == "eth_getTransactionByHash":
|
|
145
|
+
tx_hash = params[0] if params else None
|
|
146
|
+
if not tx_hash:
|
|
147
|
+
return ok(None)
|
|
148
|
+
tx = node.blockchain.get_transaction(tx_hash)
|
|
149
|
+
if not tx:
|
|
150
|
+
return ok(None)
|
|
151
|
+
return ok({
|
|
152
|
+
"hash": tx.hash,
|
|
153
|
+
"from": tx.from_addr,
|
|
154
|
+
"to": tx.to_addr,
|
|
155
|
+
"value": hex(int(tx.amount * 10**18)),
|
|
156
|
+
"blockNumber": None,
|
|
157
|
+
"transactionIndex": "0x0",
|
|
158
|
+
"gas": "0x5208",
|
|
159
|
+
"gasPrice": "0x1",
|
|
160
|
+
"nonce": hex(tx.nonce or 0),
|
|
161
|
+
"input": "0x",
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if method == "eth_getTransactionReceipt":
|
|
165
|
+
tx_hash = params[0] if params else None
|
|
166
|
+
if not tx_hash:
|
|
167
|
+
return ok(None)
|
|
168
|
+
tx = node.blockchain.get_transaction(tx_hash)
|
|
169
|
+
if not tx:
|
|
170
|
+
return ok(None)
|
|
171
|
+
tip = node.blockchain.get_chain_tip()
|
|
172
|
+
return ok({
|
|
173
|
+
"transactionHash": tx_hash,
|
|
174
|
+
"status": "0x1",
|
|
175
|
+
"blockNumber": hex(tip.index) if tip else "0x0",
|
|
176
|
+
"blockHash": tip.hash if tip else "0x0",
|
|
177
|
+
"gasUsed": "0x5208",
|
|
178
|
+
"cumulativeGasUsed":"0x5208",
|
|
179
|
+
"logs": [],
|
|
180
|
+
"logsBloom": "0x" + "0" * 512,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if method == "eth_getBlockByNumber":
|
|
184
|
+
block_param = params[0] if params else "latest"
|
|
185
|
+
include_txs = params[1] if len(params) > 1 else False
|
|
186
|
+
|
|
187
|
+
if block_param == "latest":
|
|
188
|
+
block = node.blockchain.get_chain_tip()
|
|
189
|
+
elif block_param == "earliest":
|
|
190
|
+
block = node.blockchain.get_block_by_index(0)
|
|
191
|
+
else:
|
|
192
|
+
try:
|
|
193
|
+
block = node.blockchain.get_block_by_index(
|
|
194
|
+
int(block_param, 16)
|
|
195
|
+
)
|
|
196
|
+
except:
|
|
197
|
+
block = node.blockchain.get_chain_tip()
|
|
198
|
+
|
|
199
|
+
if not block:
|
|
200
|
+
return ok(None)
|
|
201
|
+
|
|
202
|
+
txs = (
|
|
203
|
+
[tx.to_dict() for tx in block.transactions]
|
|
204
|
+
if include_txs
|
|
205
|
+
else [tx.hash for tx in block.transactions]
|
|
206
|
+
)
|
|
207
|
+
return ok({
|
|
208
|
+
"number": hex(block.index),
|
|
209
|
+
"hash": block.hash,
|
|
210
|
+
"parentHash": block.previous_hash,
|
|
211
|
+
"timestamp": hex(int(block.timestamp)),
|
|
212
|
+
"transactions": txs,
|
|
213
|
+
"miner": block.validator_wallet,
|
|
214
|
+
"gasLimit": "0x1c9c380",
|
|
215
|
+
"gasUsed": "0x0",
|
|
216
|
+
"difficulty": "0x1",
|
|
217
|
+
"nonce": "0x0000000000000000",
|
|
218
|
+
"extraData": "0x",
|
|
219
|
+
"logsBloom": "0x" + "0" * 512,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
if method == "eth_getBlockByHash":
|
|
223
|
+
block_hash = params[0] if params else None
|
|
224
|
+
include_txs = params[1] if len(params) > 1 else False
|
|
225
|
+
if not block_hash:
|
|
226
|
+
return ok(None)
|
|
227
|
+
block = node.blockchain.get_block(block_hash)
|
|
228
|
+
if not block:
|
|
229
|
+
return ok(None)
|
|
230
|
+
txs = (
|
|
231
|
+
[tx.to_dict() for tx in block.transactions]
|
|
232
|
+
if include_txs
|
|
233
|
+
else [tx.hash for tx in block.transactions]
|
|
234
|
+
)
|
|
235
|
+
return ok({
|
|
236
|
+
"number": hex(block.index),
|
|
237
|
+
"hash": block.hash,
|
|
238
|
+
"parentHash": block.previous_hash,
|
|
239
|
+
"timestamp": hex(int(block.timestamp)),
|
|
240
|
+
"transactions": txs,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
# ── Unknown method ────────────────────────────────────
|
|
244
|
+
logger.debug(f"Unknown RPC method: {method}")
|
|
245
|
+
return ok("0x")
|
|
246
|
+
|
|
247
|
+
return bp
|
playweb/client.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network — PlayWebitClient
|
|
3
|
+
Lightweight client for dApps that connect to a validator node.
|
|
4
|
+
No local chain storage. No consensus participation.
|
|
5
|
+
Just calls a node's public API.
|
|
6
|
+
|
|
7
|
+
Use this for:
|
|
8
|
+
- HuggingFace apps (CipherVault frontend)
|
|
9
|
+
- Any app that doesn't want to run a full node
|
|
10
|
+
- spiderweave-sdk integration without running a node
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from playweb import PlayWebitClient
|
|
14
|
+
|
|
15
|
+
client = PlayWebitClient(
|
|
16
|
+
validator_url = "https://node1.playwebit.com",
|
|
17
|
+
platform_wallet = os.getenv("PLATFORM_WALLET"),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
result = client.register_content(
|
|
21
|
+
cid = "QmXyz...",
|
|
22
|
+
owner = "0xabc...",
|
|
23
|
+
editions = 100,
|
|
24
|
+
royalty_pct = 10,
|
|
25
|
+
signature = "0x...",
|
|
26
|
+
)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
import requests
|
|
31
|
+
from typing import Optional, Dict, Tuple
|
|
32
|
+
|
|
33
|
+
from playweb.config import PEER_TIMEOUT, CHAIN_ID
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PlayWebitClient:
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
validator_url: str,
|
|
43
|
+
platform_wallet: str,
|
|
44
|
+
timeout: int = PEER_TIMEOUT,
|
|
45
|
+
):
|
|
46
|
+
self.validator_url = validator_url.rstrip("/")
|
|
47
|
+
self.platform_wallet = platform_wallet.lower()
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
|
|
50
|
+
# ─────────────────────────────────────────────────────────────
|
|
51
|
+
# Internal HTTP helpers
|
|
52
|
+
# ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
def _get(self, path: str) -> Dict:
|
|
55
|
+
try:
|
|
56
|
+
res = requests.get(
|
|
57
|
+
f"{self.validator_url}{path}",
|
|
58
|
+
timeout=self.timeout,
|
|
59
|
+
)
|
|
60
|
+
return res.json()
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(f"Client GET {path} failed: {e}")
|
|
63
|
+
return {"success": False, "error": str(e)}
|
|
64
|
+
|
|
65
|
+
def _post(self, path: str, data: Dict) -> Dict:
|
|
66
|
+
try:
|
|
67
|
+
res = requests.post(
|
|
68
|
+
f"{self.validator_url}{path}",
|
|
69
|
+
json = data,
|
|
70
|
+
timeout = self.timeout,
|
|
71
|
+
)
|
|
72
|
+
return res.json()
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Client POST {path} failed: {e}")
|
|
75
|
+
return {"success": False, "error": str(e)}
|
|
76
|
+
|
|
77
|
+
def transfer_plwb(
|
|
78
|
+
self,
|
|
79
|
+
from_addr: str,
|
|
80
|
+
to_addr: str,
|
|
81
|
+
amount: float,
|
|
82
|
+
signature: str,
|
|
83
|
+
) -> Tuple[bool, str, Optional[str]]:
|
|
84
|
+
"""
|
|
85
|
+
Transfer PLWB between any two wallets.
|
|
86
|
+
Requires MetaMask signature from sender.
|
|
87
|
+
Network fee: 1 PLWB (split 50/50).
|
|
88
|
+
Returns (success, reason, tx_hash).
|
|
89
|
+
"""
|
|
90
|
+
res = self._post("/api/transfer", {
|
|
91
|
+
"from_addr": from_addr.lower(),
|
|
92
|
+
"to_addr": to_addr.lower(),
|
|
93
|
+
"amount": amount,
|
|
94
|
+
"signature": signature,
|
|
95
|
+
})
|
|
96
|
+
success = res.get("success", False)
|
|
97
|
+
result = res.get("tx_hash") or res.get("error", "Unknown error")
|
|
98
|
+
return success, result, res.get("tx_hash")
|
|
99
|
+
|
|
100
|
+
# ─────────────────────────────────────────────────────────────
|
|
101
|
+
# Content Registry
|
|
102
|
+
# ─────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
def register_content(
|
|
105
|
+
self,
|
|
106
|
+
cid: str,
|
|
107
|
+
owner: str,
|
|
108
|
+
editions: int = 1,
|
|
109
|
+
royalty_pct: float = 0,
|
|
110
|
+
signature: str = None,
|
|
111
|
+
platform_id: str = None,
|
|
112
|
+
extra_data: Dict = None,
|
|
113
|
+
) -> Tuple[bool, str, Optional[str]]:
|
|
114
|
+
"""
|
|
115
|
+
Register a CID on the PlayWebit Network.
|
|
116
|
+
Checks for duplicates automatically.
|
|
117
|
+
Returns (success, reason, tx_hash).
|
|
118
|
+
"""
|
|
119
|
+
res = self._post("/api/transaction", {
|
|
120
|
+
"from_addr": owner.lower(),
|
|
121
|
+
"to_addr": owner.lower(),
|
|
122
|
+
"amount": 0,
|
|
123
|
+
"tx_type": "content_register",
|
|
124
|
+
"signature": signature,
|
|
125
|
+
"chain_id": CHAIN_ID,
|
|
126
|
+
"cid": cid,
|
|
127
|
+
"editions": editions,
|
|
128
|
+
"royalty_pct": royalty_pct,
|
|
129
|
+
"data": {
|
|
130
|
+
"platform_id": platform_id or "unknown",
|
|
131
|
+
**(extra_data or {}),
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
success = res.get("success", False)
|
|
135
|
+
result = res.get("result") or res.get("error", "Unknown error")
|
|
136
|
+
tx_hash = result if success else None
|
|
137
|
+
return success, result, tx_hash
|
|
138
|
+
|
|
139
|
+
def check_duplicate(self, cid: str) -> Dict:
|
|
140
|
+
"""
|
|
141
|
+
Check if a CID is already registered anywhere on the network.
|
|
142
|
+
Returns { exists, first_owner, first_platform, first_seen, ... }
|
|
143
|
+
"""
|
|
144
|
+
return self._get(f"/api/check_duplicate/{cid}")
|
|
145
|
+
|
|
146
|
+
def get_owner(self, cid: str) -> Optional[Dict]:
|
|
147
|
+
"""Get current owner of a CID."""
|
|
148
|
+
res = self._get(f"/api/owner/{cid}")
|
|
149
|
+
return res if res.get("success") else None
|
|
150
|
+
|
|
151
|
+
def verify_ownership(self, cid: str, wallet: str) -> bool:
|
|
152
|
+
"""Verify a wallet owns a CID."""
|
|
153
|
+
res = self._post("/api/verify_ownership", {
|
|
154
|
+
"cid": cid,
|
|
155
|
+
"wallet": wallet,
|
|
156
|
+
})
|
|
157
|
+
return res.get("owns", False)
|
|
158
|
+
|
|
159
|
+
def transfer_ownership(
|
|
160
|
+
self,
|
|
161
|
+
cid: str,
|
|
162
|
+
from_wallet: str,
|
|
163
|
+
to_wallet: str,
|
|
164
|
+
signature: str = None,
|
|
165
|
+
sale_price: float = 0,
|
|
166
|
+
platform_id: str = None,
|
|
167
|
+
) -> Tuple[bool, str, Optional[str]]:
|
|
168
|
+
"""
|
|
169
|
+
Transfer ownership of a CID.
|
|
170
|
+
If sale_price > 0, royalty is automatically paid to creator.
|
|
171
|
+
Returns (success, reason, tx_hash).
|
|
172
|
+
"""
|
|
173
|
+
res = self._post("/api/transaction", {
|
|
174
|
+
"from_addr": from_wallet.lower(),
|
|
175
|
+
"to_addr": to_wallet.lower(),
|
|
176
|
+
"amount": sale_price,
|
|
177
|
+
"tx_type": "ownership_transfer",
|
|
178
|
+
"signature": signature,
|
|
179
|
+
"chain_id": CHAIN_ID,
|
|
180
|
+
"cid": cid,
|
|
181
|
+
"data": {
|
|
182
|
+
"platform_id": platform_id or "unknown",
|
|
183
|
+
"sale_price": sale_price,
|
|
184
|
+
"transfer_type": "sale" if sale_price > 0 else "gift",
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
success = res.get("success", False)
|
|
188
|
+
result = res.get("result") or res.get("error", "Unknown error")
|
|
189
|
+
return success, result, result if success else None
|
|
190
|
+
|
|
191
|
+
# ─────────────────────────────────────────────────────────────
|
|
192
|
+
# Edition Registry
|
|
193
|
+
# ─────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
def get_editions(self, cid: str) -> Dict:
|
|
196
|
+
"""Get all editions for a CID across all platforms."""
|
|
197
|
+
return self._get(f"/api/editions/{cid}")
|
|
198
|
+
|
|
199
|
+
def get_edition(self, cid: str, edition_number: int) -> Optional[Dict]:
|
|
200
|
+
"""Get a specific edition."""
|
|
201
|
+
res = self._get(f"/api/editions/{cid}/{edition_number}")
|
|
202
|
+
return res.get("edition") if res.get("success") else None
|
|
203
|
+
|
|
204
|
+
def transfer_edition(
|
|
205
|
+
self,
|
|
206
|
+
cid: str,
|
|
207
|
+
edition_number: int,
|
|
208
|
+
from_wallet: str,
|
|
209
|
+
to_wallet: str,
|
|
210
|
+
signature: str = None,
|
|
211
|
+
sale_price: float = 0,
|
|
212
|
+
platform_id: str = None,
|
|
213
|
+
) -> Tuple[bool, str, Optional[str]]:
|
|
214
|
+
"""Transfer a specific edition to a new owner."""
|
|
215
|
+
res = self._post("/api/transaction", {
|
|
216
|
+
"from_addr": from_wallet.lower(),
|
|
217
|
+
"to_addr": to_wallet.lower(),
|
|
218
|
+
"amount": sale_price,
|
|
219
|
+
"tx_type": "edition_transfer",
|
|
220
|
+
"signature": signature,
|
|
221
|
+
"chain_id": CHAIN_ID,
|
|
222
|
+
"cid": cid,
|
|
223
|
+
"edition_number": edition_number,
|
|
224
|
+
"data": {
|
|
225
|
+
"platform_id": platform_id or "unknown",
|
|
226
|
+
"sale_price": sale_price,
|
|
227
|
+
"action": "transfer",
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
success = res.get("success", False)
|
|
231
|
+
result = res.get("result") or res.get("error", "Unknown error")
|
|
232
|
+
return success, result, result if success else None
|
|
233
|
+
|
|
234
|
+
# ─────────────────────────────────────────────────────────────
|
|
235
|
+
# SpiderWeave hash anchoring
|
|
236
|
+
# Called internally by spiderweave-sdk's PlayWebitAdapter
|
|
237
|
+
# ─────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
def anchor_spider_hash(
|
|
240
|
+
self,
|
|
241
|
+
chain_name: str,
|
|
242
|
+
spider_hash: str,
|
|
243
|
+
event_type: str = "integrity_check",
|
|
244
|
+
platform_wallet: str = None,
|
|
245
|
+
signature: str = None,
|
|
246
|
+
metadata: Dict = None,
|
|
247
|
+
) -> Tuple[bool, Optional[str]]:
|
|
248
|
+
"""
|
|
249
|
+
Anchor a SpiderWeave hash on the chain.
|
|
250
|
+
L1 stores it, never interprets it.
|
|
251
|
+
Returns (success, tx_hash).
|
|
252
|
+
"""
|
|
253
|
+
res = self._post("/api/anchor_spider_hash", {
|
|
254
|
+
"chain_id": chain_name,
|
|
255
|
+
"chain_name": chain_name,
|
|
256
|
+
"spider_hash": spider_hash,
|
|
257
|
+
"event_type": event_type,
|
|
258
|
+
"platform_wallet": platform_wallet or self.platform_wallet,
|
|
259
|
+
"signature": signature,
|
|
260
|
+
"metadata": metadata or {},
|
|
261
|
+
})
|
|
262
|
+
success = res.get("success", False)
|
|
263
|
+
tx_hash = res.get("tx_hash")
|
|
264
|
+
return success, tx_hash
|
|
265
|
+
|
|
266
|
+
def verify_spider_hash(self, tx_hash: str) -> Optional[Dict]:
|
|
267
|
+
"""Verify a previously anchored spider hash by tx_hash."""
|
|
268
|
+
res = self._get(f"/api/transaction/{tx_hash}")
|
|
269
|
+
if not res.get("success"):
|
|
270
|
+
return None
|
|
271
|
+
tx = res.get("transaction", {})
|
|
272
|
+
if tx.get("tx_type") != "spider_hash_anchor":
|
|
273
|
+
return None
|
|
274
|
+
return {
|
|
275
|
+
"tx_hash": tx_hash,
|
|
276
|
+
"spider_hash": tx.get("spider_hash"),
|
|
277
|
+
"chain_name": tx.get("chain_name"),
|
|
278
|
+
"timestamp": tx.get("timestamp"),
|
|
279
|
+
"confirmed": True,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
def get_spider_hashes(self, chain_name: str) -> Dict:
|
|
283
|
+
"""Get all anchored hashes for a chain_name."""
|
|
284
|
+
return self._get(f"/api/spider_hashes/{chain_name}")
|
|
285
|
+
|
|
286
|
+
# ─────────────────────────────────────────────────────────────
|
|
287
|
+
# Payments
|
|
288
|
+
# ─────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
def pay(
|
|
291
|
+
self,
|
|
292
|
+
from_addr: str,
|
|
293
|
+
to_addr: str,
|
|
294
|
+
amount: float,
|
|
295
|
+
signature: str,
|
|
296
|
+
) -> Tuple[bool, str, Optional[str]]:
|
|
297
|
+
"""
|
|
298
|
+
Send PLWB from one wallet to another.
|
|
299
|
+
Returns (success, reason, tx_hash).
|
|
300
|
+
"""
|
|
301
|
+
res = self._post("/api/transaction", {
|
|
302
|
+
"from_addr": from_addr.lower(),
|
|
303
|
+
"to_addr": to_addr.lower(),
|
|
304
|
+
"amount": amount,
|
|
305
|
+
"tx_type": "transfer",
|
|
306
|
+
"signature": signature,
|
|
307
|
+
"chain_id": CHAIN_ID,
|
|
308
|
+
})
|
|
309
|
+
success = res.get("success", False)
|
|
310
|
+
result = res.get("result") or res.get("error", "Unknown error")
|
|
311
|
+
return success, result, result if success else None
|
|
312
|
+
|
|
313
|
+
def get_balance(self, address: str) -> float:
|
|
314
|
+
"""Get PLWB balance for an address."""
|
|
315
|
+
res = self._get(f"/api/balance/{address}")
|
|
316
|
+
return float(res.get("balance", 0.0))
|
|
317
|
+
|
|
318
|
+
# ─────────────────────────────────────────────────────────────
|
|
319
|
+
# Network info
|
|
320
|
+
# ─────────────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
def get_network_stats(self) -> Dict:
|
|
323
|
+
"""Get network stats from the connected validator."""
|
|
324
|
+
return self._get("/api/network/stats")
|
|
325
|
+
|
|
326
|
+
def get_transaction(self, tx_hash: str) -> Optional[Dict]:
|
|
327
|
+
"""Get a transaction by hash."""
|
|
328
|
+
res = self._get(f"/api/transaction/{tx_hash}")
|
|
329
|
+
return res.get("transaction") if res.get("success") else None
|
|
330
|
+
|
|
331
|
+
def health_check(self) -> bool:
|
|
332
|
+
"""Check if the connected validator is online."""
|
|
333
|
+
res = self._get("/peer/health")
|
|
334
|
+
return res.get("status") == "ok"
|
|
335
|
+
|
|
336
|
+
def __repr__(self):
|
|
337
|
+
return (
|
|
338
|
+
f"PlayWebitClient("
|
|
339
|
+
f"validator={self.validator_url}, "
|
|
340
|
+
f"wallet={self.platform_wallet[:10]}...)"
|
|
341
|
+
)
|