├── .gitignore ├── LICENSE ├── README.md ├── openapi ├── __init__.py ├── api.py ├── clsp │ ├── __init__.py │ ├── cat.clvm │ ├── cat.clvm.hex │ ├── did_innerpuz.clvm │ ├── did_innerpuz.clvm.hex │ ├── include │ │ ├── __init__.py │ │ ├── cat_truths.clib │ │ ├── condition_codes.clvm │ │ ├── curry-and-treehash.clinc │ │ ├── singleton_truths.clib │ │ └── utility_macros.clib │ ├── nft_metadata_updater_default.clvm │ ├── nft_metadata_updater_default.clvm.hex │ ├── nft_ownership_layer.clvm │ ├── nft_ownership_layer.clvm.hex │ ├── nft_ownership_transfer_program_one_way_claim_with_royalties.clvm │ ├── nft_ownership_transfer_program_one_way_claim_with_royalties.clvm.hex │ ├── nft_state_layer.clvm │ ├── nft_state_layer.clvm.hex │ ├── p2_delegated_puzzle_or_hidden_puzzle.clvm │ ├── p2_delegated_puzzle_or_hidden_puzzle.clvm.hex │ ├── settlement_payments.clvm │ ├── settlement_payments.clvm.hex │ ├── singleton_launcher.clvm │ ├── singleton_launcher.clvm.hex │ ├── singleton_top_layer_v1_1.clvm │ └── singleton_top_layer_v1_1.clvm.hex ├── config.py ├── db.py ├── did.py ├── nft.py ├── puzzles.py ├── rpc_client.py ├── sync.py ├── types.py ├── utils │ ├── __init__.py │ ├── bech32m.py │ ├── singleflight.py │ └── tree_hash.py └── watcher.py ├── requirements.txt └── settings.toml.default /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | __pycache__ 3 | logs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-present Goby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openapi 2 | 3 | DeFi wallet on Chia Network. 4 | 5 | ## Install 6 | 7 | You can install Goby [here](https://chrome.google.com/webstore/detail/goby/jnkelfanjkeadonecabehalmbgpfodjm). 8 | 9 | ## Run your own node 10 | 11 | ``` 12 | git clone https://github.com/GobyWallet/openapi.git 13 | cp settings.toml.default settings.toml 14 | # change settings.toml 15 | pip install -r requirements.txt 16 | 17 | # start api 18 | uvicorn openapi.api:app 19 | 20 | # start watcher 21 | python -m openapi.watcher 22 | ``` 23 | 24 | ## Thanks 25 | 26 | Thanks to the contributions of [Chia Mine](https://github.com/Chia-Mine/clvm-js), MetaMask, and DeBank to crypto, we stand on your shoulders to complete this project. (🌱, 🌱) 27 | 28 | Also, thanks to Catcoin and [Taildatabase](https://www.taildatabase.com/) for sharing the token list. 29 | 30 | -------------------------------------------------------------------------------- /openapi/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import logzero 4 | 5 | 6 | cwd = os.path.dirname(__file__) 7 | 8 | log_dir = os.path.join(cwd, "../", "logs") 9 | 10 | if not os.path.exists(log_dir): 11 | os.mkdir(log_dir) 12 | 13 | from .config import settings 14 | 15 | logger = logzero.setup_logger( 16 | __name__, level=logging.getLevelName(settings['LOG_LEVEL']), logfile=os.path.join(log_dir, "api.log"), 17 | disableStderrLogger=True) -------------------------------------------------------------------------------- /openapi/api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from dataclasses import dataclass 4 | from enum import Enum 5 | from typing import List, Optional, Dict 6 | import asyncio 7 | import logging 8 | from fastapi import FastAPI, APIRouter, Request, Body, Depends, HTTPException 9 | from fastapi.responses import JSONResponse 10 | from aiocache import caches, cached, Cache 11 | from pydantic import BaseModel 12 | from .utils import int_to_hex, hexstr_to_bytes, to_hex, sanitize_obj_hex 13 | from .utils.bech32m import decode_puzzle_hash, encode_puzzle_hash 14 | from .utils.singleflight import SingleFlight 15 | from .rpc_client import FullNodeRpcClient 16 | from .types import Coin, Program 17 | from .sync import sync_user_assets, get_and_sync_singleton 18 | from .db import ( 19 | get_db, 20 | get_assets, 21 | register_db, 22 | connect_db, 23 | disconnect_db, 24 | get_metadata_by_hashes, 25 | ) 26 | from .config import settings 27 | 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | caches.set_config({"default": settings.CACHE}) 32 | 33 | app = FastAPI() 34 | 35 | 36 | RPC_METHOD_WHITE_LIST = set(settings.RPC_METHOD_WHITE_LIST) 37 | 38 | 39 | @dataclass 40 | class Chain: 41 | id: str 42 | network_name: str 43 | network_prefix: str 44 | client: FullNodeRpcClient 45 | # db: 46 | 47 | 48 | async def init_chains(app, chains_config): 49 | chains: Dict[str, Chain] = {} 50 | for row in chains_config: 51 | if row.get("enable") == False: 52 | continue 53 | id_hex = int_to_hex(row["id"]) 54 | 55 | rpc_url_or_chia_path = row.get("rpc_url_or_chia_path") 56 | if rpc_url_or_chia_path: 57 | if rpc_url_or_chia_path.startswith("http"): 58 | client = await FullNodeRpcClient.create_by_proxy_url( 59 | rpc_url_or_chia_path 60 | ) 61 | else: 62 | client = await FullNodeRpcClient.create_by_chia_root_path( 63 | rpc_url_or_chia_path 64 | ) 65 | else: 66 | raise ValueError(f"chian {row['id']} has no full node rpc config") 67 | 68 | # check client 69 | network_info = await client.get_network_info() 70 | chain = Chain(id_hex, row["network_name"], row["network_prefix"], client) 71 | chains[chain.id] = chain 72 | chains[chain.network_name] = chain 73 | register_db(chain.id, row["database_uri"]) 74 | await connect_db(chain.id) 75 | 76 | app.state.chains = chains 77 | 78 | 79 | @app.on_event("startup") 80 | async def startup(): 81 | logger.info("begin init") 82 | await init_chains(app, settings.SUPPORTED_CHAINS.values()) 83 | logger.info("finish init") 84 | 85 | 86 | @app.on_event("shutdown") 87 | async def shutdown(): 88 | for chain in app.state.chains.values(): 89 | chain.client.close() 90 | await chain.client.await_closed() 91 | await disconnect_db(chain.id) 92 | 93 | 94 | def decode_address(address, prefix): 95 | try: 96 | _prefix, puzzle_hash = decode_puzzle_hash(address) 97 | if _prefix != prefix: 98 | raise ValueError("wrong prefix") 99 | return puzzle_hash 100 | except ValueError: 101 | raise HTTPException(400, "Invalid Address") 102 | 103 | 104 | async def get_chain(request: Request, chain="0x01") -> Chain: 105 | if chain not in request.app.state.chains: 106 | raise HTTPException(400, "Ivalid Chain") 107 | return request.app.state.chains[chain] 108 | 109 | 110 | async def get_cache(request: Request) -> Cache: 111 | return caches.get("default") 112 | 113 | 114 | router = APIRouter() 115 | 116 | 117 | class UTXO(BaseModel): 118 | parent_coin_info: str 119 | puzzle_hash: str 120 | amount: str 121 | 122 | 123 | def coin_javascript_compat(coin): 124 | return { 125 | "parent_coin_info": coin["parent_coin_info"], 126 | "puzzle_hash": coin["puzzle_hash"], 127 | "amount": str(coin["amount"]), 128 | } 129 | 130 | 131 | @router.get("/utxos", response_model=List[UTXO]) 132 | @cached( 133 | ttl=10, 134 | key_builder=lambda *args, **kwargs: f"utxos:{kwargs['address']}", 135 | alias="default", 136 | ) 137 | async def get_utxos(address: str, chain: Chain = Depends(get_chain)): 138 | # todo: use block indexer 139 | pzh = decode_address(address, chain.network_prefix) 140 | 141 | coin_records = await chain.client.get_coin_records_by_puzzle_hash( 142 | puzzle_hash=pzh, include_spent_coins=False 143 | ) 144 | data = [] 145 | 146 | for row in coin_records: 147 | if row["spent"]: 148 | continue 149 | data.append(coin_javascript_compat(row["coin"])) 150 | return data 151 | 152 | 153 | class SendTxBody(BaseModel): 154 | spend_bundle: dict 155 | 156 | 157 | @router.post("/sendtx") 158 | async def create_transaction(item: SendTxBody, chain: Chain = Depends(get_chain)): 159 | spb = item.spend_bundle 160 | spb = sanitize_obj_hex(spb) 161 | retry_during_sync_number = 2 162 | while retry_during_sync_number: 163 | retry_during_sync_number -= 1 164 | try: 165 | resp = await chain.client.push_tx(spb) 166 | return { 167 | "status": resp["status"], 168 | } 169 | except ValueError as e: 170 | err_str = str(e) 171 | if "NO_TRANSACTIONS_WHILE_SYNCING" in err_str: 172 | await asyncio.sleep(5) 173 | continue 174 | logger.warning("sendtx: %s, error: %r", spb, e) 175 | raise HTTPException(400, err_str) 176 | raise HTTPException(400, "sendtx: timeout") 177 | 178 | 179 | class ChiaRpcParams(BaseModel): 180 | method: str 181 | params: Optional[Dict] = None 182 | 183 | 184 | @router.post("/chia_rpc") 185 | async def full_node_rpc(item: ChiaRpcParams, chain: Chain = Depends(get_chain)): 186 | """ 187 | ref: https://docs.chia.net/docs/12rpcs/full_node_api 188 | """ 189 | # todo: limit method and add cache 190 | if item.method not in RPC_METHOD_WHITE_LIST: 191 | raise HTTPException(400, f"unspport chia rpc method: {item.method}") 192 | 193 | return await chain.client.raw_fetch(item.method, item.params) 194 | 195 | 196 | @router.get("/balance") 197 | @cached( 198 | ttl=10, 199 | key_builder=lambda *args, **kwargs: f"balance:{kwargs['address']}", 200 | alias="default", 201 | ) 202 | async def query_balance(address: str, chain: Chain = Depends(get_chain)): 203 | # todo: use block indexer 204 | puzzle_hash = decode_address(address, chain.network_prefix) 205 | coin_records = await chain.client.get_coin_records_by_puzzle_hash( 206 | puzzle_hash=puzzle_hash, include_spent_coins=False 207 | ) 208 | amount = 0 209 | coin_num = 0 210 | for row in coin_records: 211 | if row["spent"]: 212 | continue 213 | amount += row["coin"]["amount"] 214 | coin_num += 1 215 | data = { 216 | "amount": amount, 217 | "coin_num": coin_num, 218 | } 219 | return data 220 | 221 | 222 | sf = SingleFlight() 223 | 224 | 225 | class AssetTypeEnum(str, Enum): 226 | NFT = "nft" 227 | DID = "did" 228 | 229 | 230 | @router.get("/assets") 231 | async def list_assets( 232 | address: str, 233 | chain: Chain = Depends(get_chain), 234 | asset_type: AssetTypeEnum = AssetTypeEnum.NFT, 235 | asset_id: Optional[str] = None, 236 | offset=0, 237 | limit=10, 238 | ): 239 | """ 240 | - the api only support did coins that use inner puzzle hash for hint, so some did coins may not return 241 | """ 242 | 243 | puzzle_hash = decode_address(address, chain.network_prefix) 244 | await sf.do(address, lambda: sync_user_assets(chain.id, puzzle_hash, chain.client)) 245 | db = get_db(chain.id) 246 | # todo: use nftd/did indexer, now use db for cache 247 | assets = await get_assets( 248 | db, 249 | asset_type=asset_type, 250 | asset_id=hexstr_to_bytes(asset_id) if asset_id else None, 251 | p2_puzzle_hash=puzzle_hash, 252 | offset=offset, 253 | limit=limit, 254 | ) 255 | 256 | data = [] 257 | for asset in assets: 258 | item = { 259 | "asset_type": asset.asset_type, 260 | "asset_id": to_hex(asset.asset_id), 261 | "coin": asset.coin, 262 | "coin_id": to_hex(asset.coin_id), 263 | "confirmed_height": asset.confirmed_height, 264 | "lineage_proof": asset.lineage_proof, 265 | "curried_params": asset.curried_params, 266 | } 267 | data.append(item) 268 | 269 | return data 270 | 271 | 272 | @router.get("/latest_singleton") 273 | async def get_latest_singleton(singleton_id: str, chain: Chain = Depends(get_chain)): 274 | try: 275 | singleton_id = hexstr_to_bytes(singleton_id) 276 | if len(singleton_id) != 32: 277 | raise ValueError("invalid singleton id") 278 | except Exception as e: 279 | raise HTTPException(status_code=400, detail="Invalid singleton ID") from e 280 | 281 | try: 282 | return await sf.do( 283 | b"singleton" + singleton_id, 284 | lambda: get_and_sync_singleton(chain.id, singleton_id, chain.client), 285 | ) 286 | except ValueError as e: 287 | raise HTTPException(status_code=400, detail=str(e)) 288 | 289 | 290 | class FeeEstimateBody(BaseModel): 291 | cost: int 292 | 293 | 294 | @router.post("/fee_estimate") 295 | async def get_fee_estimate(item: FeeEstimateBody, chain: Chain = Depends(get_chain)): 296 | target_times = [30, 120, 300] 297 | if item.cost <= 0: 298 | raise HTTPException(400, "invalid `cost`") 299 | resp = await chain.client.get_fee_estimate(target_times, item.cost) 300 | estimates = resp["estimates"] 301 | is_full = item.cost + resp["mempool_size"] > resp["mempool_max_size"] 302 | nonzero_fee_minimum_fpc = 5 303 | if is_full: 304 | minimum_fee = nonzero_fee_minimum_fpc * item.cost 305 | estimates = [int(minimum_fee * 1.5), int(minimum_fee * 1.1), minimum_fee] 306 | return {"estimates": estimates} 307 | 308 | 309 | app.include_router(router, prefix="/v1") 310 | -------------------------------------------------------------------------------- /openapi/clsp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GobyWallet/openapi/b9d36e3fced171ab4384359da21877ec65e23a9d/openapi/clsp/__init__.py -------------------------------------------------------------------------------- /openapi/clsp/cat.clvm: -------------------------------------------------------------------------------- 1 | ; Coins locked with this puzzle are spendable cats. 2 | ; 3 | ; Choose a list of n inputs (n>=1), I_1, ... I_n with amounts A_1, ... A_n. 4 | ; 5 | ; We put them in a ring, so "previous" and "next" have intuitive k-1 and k+1 semantics, 6 | ; wrapping so {n} and 0 are the same, ie. all indices are mod n. 7 | ; 8 | ; Each coin creates 0 or more coins with total output value O_k. 9 | ; Let D_k = the "debt" O_k - A_k contribution of coin I_k, ie. how much debt this input accumulates. 10 | ; Some coins may spend more than they contribute and some may spend less, ie. D_k need 11 | ; not be zero. That's okay. It's enough for the total of all D_k in the ring to be 0. 12 | ; 13 | ; A coin can calculate its own D_k since it can verify A_k (it's hashed into the coin id) 14 | ; and it can sum up `CREATE_COIN` conditions for O_k. 15 | ; 16 | ; Defines a "subtotal of debts" S_k for each coin as follows: 17 | ; 18 | ; S_1 = 0 19 | ; S_k = S_{k-1} + D_{k-1} 20 | ; 21 | ; Here's the main trick that shows the ring sums to 0. 22 | ; You can prove by induction that S_{k+1} = D_1 + D_2 + ... + D_k. 23 | ; But it's a ring, so S_{n+1} is also S_1, which is 0. So D_1 + D_2 + ... + D_k = 0. 24 | ; So the total debts must be 0, ie. no coins are created or destroyed. 25 | ; 26 | ; Each coin's solution includes I_{k-1}, I_k, and I_{k+1} along with proofs that I_{k}, and I_{k+1} are CATs of the same type. 27 | ; Each coin's solution includes S_{k-1}. It calculates D_k = O_k - A_k, and then S_k = S_{k-1} + D_{k-1} 28 | ; 29 | ; Announcements are used to ensure that each S_k follows the pattern is valid. 30 | ; Announcements automatically commit to their own coin id. 31 | ; Coin I_k creates an announcement that further commits to I_{k-1} and S_{k-1}. 32 | ; 33 | ; Coin I_k gets a proof that I_{k+1} is a cat, so it knows it must also create an announcement 34 | ; when spent. It checks that I_{k+1} creates an announcement committing to I_k and S_k. 35 | ; 36 | ; So S_{k+1} is correct iff S_k is correct. 37 | ; 38 | ; Coins also receive proofs that their neighbours are CATs, ensuring the announcements aren't forgeries. 39 | ; Inner puzzles and the CAT layer prepend `CREATE_COIN_ANNOUNCEMENT` with different prefixes to avoid forgeries. 40 | ; Ring announcements use 0xcb, and inner puzzles are given 0xca 41 | ; 42 | ; In summary, I_k generates a coin_announcement Y_k ("Y" for "yell") as follows: 43 | ; 44 | ; Y_k: hash of I_k (automatically), I_{k-1}, S_k 45 | ; 46 | ; Each coin creates an assert_coin_announcement to ensure that the next coin's announcement is as expected: 47 | ; Y_{k+1} : hash of I_{k+1}, I_k, S_{k+1} 48 | ; 49 | ; TLDR: 50 | ; I_k : coins 51 | ; A_k : amount coin k contributes 52 | ; O_k : amount coin k spend 53 | ; D_k : difference/delta that coin k incurs (A - O) 54 | ; S_k : subtotal of debts D_1 + D_2 ... + D_k 55 | ; Y_k : announcements created by coin k commiting to I_{k-1}, I_k, S_k 56 | ; 57 | ; All conditions go through a "transformer" that looks for CREATE_COIN conditions 58 | ; generated by the inner solution, and wraps the puzzle hash ensuring the output is a cat. 59 | ; 60 | ; Three output conditions are prepended to the list of conditions for each I_k: 61 | ; (ASSERT_MY_ID I_k) to ensure that the passed in value for I_k is correct 62 | ; (CREATE_COIN_ANNOUNCEMENT I_{k-1} S_k) to create this coin's announcement 63 | ; (ASSERT_COIN_ANNOUNCEMENT hashed_announcement(Y_{k+1})) to ensure the next coin really is next and 64 | ; the relative values of S_k and S_{k+1} are correct 65 | ; 66 | ; This is all we need to do to ensure cats exactly balance in the inputs and outputs. 67 | ; 68 | ; Proof: 69 | ; Consider n, k, I_k values, O_k values, S_k and A_k as above. 70 | ; For the (CREATE_COIN_ANNOUNCEMENT Y_{k+1}) (created by the next coin) 71 | ; and (ASSERT_COIN_ANNOUNCEMENT hashed(Y_{k+1})) to match, 72 | ; we see that I_k can ensure that is has the correct value for S_{k+1}. 73 | ; 74 | ; By induction, we see that S_{m+1} = sum(i, 1, m) [O_i - A_i] = sum(i, 1, m) O_i - sum(i, 1, m) A_i 75 | ; So S_{n+1} = sum(i, 1, n) O_i - sum(i, 1, n) A_i. But S_{n+1} is actually S_1 = 0, 76 | ; so thus sum(i, 1, n) O_i = sum (i, 1, n) A_i, ie. output total equals input total. 77 | 78 | ;; GLOSSARY: 79 | ;; MOD_HASH: this code's sha256 tree hash 80 | ;; TAIL_PROGRAM_HASH: the program that determines if a coin can mint new cats, burn cats, and check if its lineage is valid if its parent is not a CAT 81 | ;; INNER_PUZZLE: an independent puzzle protecting the coins. Solutions to this puzzle are expected to generate `AGG_SIG` conditions and possibly `CREATE_COIN` conditions. 82 | ;; ---- items above are curried into the puzzle hash ---- 83 | ;; inner_puzzle_solution: the solution to the inner puzzle 84 | ;; prev_coin_id: the id for the previous coin 85 | ;; tail_program_reveal: reveal of TAIL_PROGRAM_HASH required to run the program if desired 86 | ;; tail_solution: optional solution passed into tail_program 87 | ;; lineage_proof: optional proof that our coin's parent is a CAT 88 | ;; this_coin_info: (parent_id puzzle_hash amount) 89 | ;; next_coin_proof: (parent_id inner_puzzle_hash amount) 90 | ;; prev_subtotal: the subtotal between prev-coin and this-coin 91 | ;; extra_delta: an amount that is added to our delta and checked by the TAIL program 92 | ;; 93 | 94 | (mod ( 95 | MOD_HASH ;; curried into puzzle 96 | TAIL_PROGRAM_HASH ;; curried into puzzle 97 | INNER_PUZZLE ;; curried into puzzle 98 | inner_puzzle_solution ;; if invalid, INNER_PUZZLE will fail 99 | lineage_proof ;; This is the parent's coin info, used to check if the parent was a CAT. Optional if using tail_program. 100 | prev_coin_id ;; used in this coin's announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong 101 | this_coin_info ;; verified with ASSERT_MY_COIN_ID 102 | next_coin_proof ;; used to generate ASSERT_COIN_ANNOUNCEMENT 103 | prev_subtotal ;; included in announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong 104 | extra_delta ;; this is the "legal discrepancy" between your real delta and what you're announcing your delta is 105 | ) 106 | 107 | ;;;;; start library code 108 | 109 | (include condition_codes.clvm) 110 | (include curry-and-treehash.clinc) 111 | (include cat_truths.clib) 112 | 113 | (defconstant ANNOUNCEMENT_MORPH_BYTE 0xca) 114 | (defconstant RING_MORPH_BYTE 0xcb) 115 | 116 | (defmacro assert items 117 | (if (r items) 118 | (list if (f items) (c assert (r items)) (q . (x))) 119 | (f items) 120 | ) 121 | ) 122 | 123 | (defmacro and ARGS 124 | (if ARGS 125 | (qq (if (unquote (f ARGS)) 126 | (unquote (c and (r ARGS))) 127 | () 128 | )) 129 | 1) 130 | ) 131 | 132 | ; takes a lisp tree and returns the hash of it 133 | (defun sha256tree1 (TREE) 134 | (if (l TREE) 135 | (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) 136 | (sha256 ONE TREE))) 137 | 138 | ; take two lists and merge them into one 139 | (defun merge_list (list_a list_b) 140 | (if list_a 141 | (c (f list_a) (merge_list (r list_a) list_b)) 142 | list_b 143 | ) 144 | ) 145 | 146 | ; cat_mod_struct = (MOD_HASH MOD_HASH_hash GENESIS_COIN_CHECKER GENESIS_COIN_CHECKER_hash) 147 | 148 | (defun-inline mod_hash_from_cat_mod_struct (cat_mod_struct) (f cat_mod_struct)) 149 | (defun-inline mod_hash_hash_from_cat_mod_struct (cat_mod_struct) (f (r cat_mod_struct))) 150 | (defun-inline tail_program_hash_from_cat_mod_struct (cat_mod_struct) (f (r (r cat_mod_struct)))) 151 | 152 | ;;;;; end library code 153 | 154 | ;; return the puzzle hash for a cat with the given `GENESIS_COIN_CHECKER_hash` & `INNER_PUZZLE` 155 | (defun-inline cat_puzzle_hash (cat_mod_struct inner_puzzle_hash) 156 | (puzzle-hash-of-curried-function (mod_hash_from_cat_mod_struct cat_mod_struct) 157 | inner_puzzle_hash 158 | (sha256 ONE (tail_program_hash_from_cat_mod_struct cat_mod_struct)) 159 | (mod_hash_hash_from_cat_mod_struct cat_mod_struct) 160 | ) 161 | ) 162 | 163 | ;; tweak `CREATE_COIN` condition by wrapping the puzzle hash, forcing it to be a cat 164 | ;; prepend `CREATE_COIN_ANNOUNCEMENT` with 0xca as bytes so it cannot be used to cheat the coin ring 165 | 166 | (defun-inline morph_condition (condition cat_mod_struct) 167 | (if (= (f condition) CREATE_COIN) 168 | (c CREATE_COIN 169 | (c (cat_puzzle_hash cat_mod_struct (f (r condition))) 170 | (r (r condition))) 171 | ) 172 | (if (= (f condition) CREATE_COIN_ANNOUNCEMENT) 173 | (c CREATE_COIN_ANNOUNCEMENT 174 | (c (sha256 ANNOUNCEMENT_MORPH_BYTE (f (r condition))) 175 | (r (r condition)) 176 | ) 177 | ) 178 | condition 179 | ) 180 | ) 181 | ) 182 | 183 | ;; given a coin's parent, inner_puzzle and amount, and the cat_mod_struct, calculate the id of the coin 184 | (defun-inline coin_id_for_proof (coin cat_mod_struct) 185 | (sha256 (f coin) (cat_puzzle_hash cat_mod_struct (f (r coin))) (f (r (r coin)))) 186 | ) 187 | 188 | ;; utility to fetch coin amount from coin 189 | (defun-inline input_amount_for_coin (coin) 190 | (f (r (r coin))) 191 | ) 192 | 193 | ;; calculate the hash of an announcement 194 | ;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles 195 | (defun-inline calculate_annoucement_id (this_coin_id this_subtotal next_coin_id cat_mod_struct) 196 | (sha256 next_coin_id (sha256 RING_MORPH_BYTE this_coin_id this_subtotal)) 197 | ) 198 | 199 | ;; create the `ASSERT_COIN_ANNOUNCEMENT` condition that ensures the next coin's announcement is correct 200 | (defun-inline create_assert_next_announcement_condition (this_coin_id this_subtotal next_coin_id cat_mod_struct) 201 | (list ASSERT_COIN_ANNOUNCEMENT 202 | (calculate_annoucement_id this_coin_id 203 | this_subtotal 204 | next_coin_id 205 | cat_mod_struct 206 | ) 207 | ) 208 | ) 209 | 210 | ;; here we commit to I_{k-1} and S_k 211 | ;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles 212 | (defun-inline create_announcement_condition (prev_coin_id prev_subtotal) 213 | (list CREATE_COIN_ANNOUNCEMENT 214 | (sha256 RING_MORPH_BYTE prev_coin_id prev_subtotal) 215 | ) 216 | ) 217 | 218 | ;;;;;;;;;;;;;;;;;;;;;;;;;;; 219 | 220 | ;; this function takes a condition and returns an integer indicating 221 | ;; the value of all output coins created with CREATE_COIN. If it's not 222 | ;; a CREATE_COIN condition, it returns 0. 223 | 224 | (defun-inline output_value_for_condition (condition) 225 | (if (= (f condition) CREATE_COIN) 226 | (f (r (r condition))) 227 | 0 228 | ) 229 | ) 230 | 231 | ;; add two conditions to the list of morphed conditions: 232 | ;; CREATE_COIN_ANNOUNCEMENT for my announcement 233 | ;; ASSERT_COIN_ANNOUNCEMENT for the next coin's announcement 234 | (defun-inline generate_final_output_conditions 235 | ( 236 | prev_subtotal 237 | this_subtotal 238 | morphed_conditions 239 | prev_coin_id 240 | this_coin_id 241 | next_coin_id 242 | cat_mod_struct 243 | ) 244 | (c (create_announcement_condition prev_coin_id prev_subtotal) 245 | (c (create_assert_next_announcement_condition this_coin_id this_subtotal next_coin_id cat_mod_struct) 246 | morphed_conditions) 247 | ) 248 | ) 249 | 250 | 251 | ;; This next section of code loops through all of the conditions to do three things: 252 | ;; 1) Look for a "magic" value of -113 and, if one exists, filter it, and take note of the tail reveal and solution 253 | ;; 2) Morph any CREATE_COIN or CREATE_COIN_ANNOUNCEMENT conditions 254 | ;; 3) Sum the total output amount of all of the CREATE_COINs that are output by the inner puzzle 255 | ;; 256 | ;; After everything return a struct in the format (morphed_conditions . (output_sum . tail_reveal_and_solution)) 257 | ;; If multiple magic conditions are specified, the later one will take precedence 258 | 259 | (defun-inline condition_tail_reveal (condition) (f (r (r (r condition))))) 260 | (defun-inline condition_tail_solution (condition) (f (r (r (r (r condition)))))) 261 | 262 | (defun cons_onto_first_and_add_to_second (morphed_condition output_value struct) 263 | (c (c morphed_condition (f struct)) (c (+ output_value (f (r struct))) (r (r struct)))) 264 | ) 265 | 266 | (defun find_and_strip_tail_info (inner_conditions cat_mod_struct tail_reveal_and_solution) 267 | (if inner_conditions 268 | (if (= (output_value_for_condition (f inner_conditions)) -113) ; Checks this is a CREATE_COIN of value -113 269 | (find_and_strip_tail_info 270 | (r inner_conditions) 271 | cat_mod_struct 272 | (c (condition_tail_reveal (f inner_conditions)) (condition_tail_solution (f inner_conditions))) 273 | ) 274 | (cons_onto_first_and_add_to_second 275 | (morph_condition (f inner_conditions) cat_mod_struct) 276 | (output_value_for_condition (f inner_conditions)) 277 | (find_and_strip_tail_info 278 | (r inner_conditions) 279 | cat_mod_struct 280 | tail_reveal_and_solution 281 | ) 282 | ) 283 | ) 284 | (c () (c 0 tail_reveal_and_solution)) 285 | ) 286 | ) 287 | 288 | ;;;;;;;;;;;;;;;;;;;;;;;;;;; lineage checking 289 | 290 | ;; return true iff parent of `this_coin_info` is provably a cat 291 | ;; A 'lineage proof' consists of (parent_parent_id parent_INNER_puzzle_hash parent_amount) 292 | ;; We use this information to construct a coin who's puzzle has been wrapped in this MOD and verify that, 293 | ;; once wrapped, it matches our parent coin's ID. 294 | (defun-inline is_parent_cat ( 295 | cat_mod_struct 296 | parent_id 297 | lineage_proof 298 | ) 299 | (= parent_id 300 | (sha256 (f lineage_proof) 301 | (cat_puzzle_hash cat_mod_struct (f (r lineage_proof))) 302 | (f (r (r lineage_proof))) 303 | ) 304 | ) 305 | ) 306 | 307 | (defun check_lineage_or_run_tail_program 308 | ( 309 | this_coin_info 310 | tail_reveal_and_solution 311 | parent_is_cat ; flag which says whether or not the parent CAT check ran and passed 312 | lineage_proof 313 | Truths 314 | extra_delta 315 | inner_conditions 316 | ) 317 | (if tail_reveal_and_solution 318 | (assert (= (sha256tree1 (f tail_reveal_and_solution)) (cat_tail_program_hash_truth Truths)) 319 | (merge_list 320 | (a (f tail_reveal_and_solution) 321 | (list 322 | Truths 323 | parent_is_cat 324 | lineage_proof ; Lineage proof is only guaranteed to be true if parent_is_cat 325 | extra_delta 326 | inner_conditions 327 | (r tail_reveal_and_solution) 328 | ) 329 | ) 330 | inner_conditions 331 | ) 332 | ) 333 | (assert parent_is_cat (not extra_delta) 334 | inner_conditions 335 | ) 336 | ) 337 | ) 338 | 339 | ;;;;;;;;;;;;;;;;;;;;;;;;;;; 340 | 341 | (defun stager_two ( 342 | Truths 343 | (inner_conditions . (output_sum . tail_reveal_and_solution)) 344 | lineage_proof 345 | prev_coin_id 346 | this_coin_info 347 | next_coin_id 348 | prev_subtotal 349 | extra_delta 350 | ) 351 | (check_lineage_or_run_tail_program 352 | this_coin_info 353 | tail_reveal_and_solution 354 | (if lineage_proof (is_parent_cat (cat_struct_truth Truths) (my_parent_cat_truth Truths) lineage_proof) ()) 355 | lineage_proof 356 | Truths 357 | extra_delta 358 | (generate_final_output_conditions 359 | prev_subtotal 360 | ; the expression on the next line calculates `this_subtotal` by adding the delta to `prev_subtotal` 361 | (+ prev_subtotal (- (input_amount_for_coin this_coin_info) output_sum) extra_delta) 362 | inner_conditions 363 | prev_coin_id 364 | (my_id_cat_truth Truths) 365 | next_coin_id 366 | (cat_struct_truth Truths) 367 | ) 368 | ) 369 | ) 370 | 371 | ; CAT TRUTHS struct is: ; CAT Truths is: ((Inner puzzle hash . (MOD hash . (MOD hash hash . TAIL hash))) . (my_id . (my_parent_info my_puzhash my_amount))) 372 | ; create truths - this_coin_info verified true because we calculated my ID from it! 373 | ; lineage proof is verified later by cat parent check or tail_program 374 | 375 | (defun stager ( 376 | cat_mod_struct 377 | inner_conditions 378 | lineage_proof 379 | inner_puzzle_hash 380 | my_id 381 | prev_coin_id 382 | this_coin_info 383 | next_coin_proof 384 | prev_subtotal 385 | extra_delta 386 | ) 387 | (c (list ASSERT_MY_COIN_ID my_id) (stager_two 388 | (cat_truth_data_to_truth_struct 389 | inner_puzzle_hash 390 | cat_mod_struct 391 | my_id 392 | this_coin_info 393 | ) 394 | (find_and_strip_tail_info inner_conditions cat_mod_struct ()) 395 | lineage_proof 396 | prev_coin_id 397 | this_coin_info 398 | (coin_id_for_proof next_coin_proof cat_mod_struct) 399 | prev_subtotal 400 | extra_delta 401 | )) 402 | ) 403 | 404 | (stager 405 | ;; calculate cat_mod_struct, inner_puzzle_hash, coin_id 406 | (list MOD_HASH (sha256 ONE MOD_HASH) TAIL_PROGRAM_HASH) 407 | (a INNER_PUZZLE inner_puzzle_solution) 408 | lineage_proof 409 | (sha256tree1 INNER_PUZZLE) 410 | (sha256 (f this_coin_info) (f (r this_coin_info)) (f (r (r this_coin_info)))) 411 | prev_coin_id ; ID 412 | this_coin_info ; (parent_id puzzle_hash amount) 413 | next_coin_proof ; (parent_id innerpuzhash amount) 414 | prev_subtotal 415 | extra_delta 416 | ) 417 | ) 418 | -------------------------------------------------------------------------------- /openapi/clsp/cat.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057fff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff80808080808080808080ff018080 2 | -------------------------------------------------------------------------------- /openapi/clsp/did_innerpuz.clvm: -------------------------------------------------------------------------------- 1 | ; The DID innerpuzzle is designed to sit inside the singleton layer and provide functionality related to being an identity. 2 | ; At the moment the two pieces of functionality are recovery and message creation. 3 | ; A DID's ID is it's Singleton ID 4 | ; Recovery is based around having a list of known other DIDs which can send messages approving you change the innerpuzzle of your DID singleton 5 | 6 | (mod 7 | ( 8 | INNER_PUZZLE ; Standard P2 inner puzzle, used to record the ownership of the DID. 9 | RECOVERY_DID_LIST_HASH ; the list of DIDs that can send messages to you for recovery we store only the hash so that we don't have to reveal every time we make a message spend 10 | NUM_VERIFICATIONS_REQUIRED ; how many of the above list are required for a recovery 11 | SINGLETON_STRUCT ; my singleton_struct, formerly a Truth - ((SINGLETON_MOD_HASH, (LAUNCHER_ID, LAUNCHER_PUZZLE_HASH))) 12 | METADATA ; Customized metadata, e.g KYC info 13 | mode ; this indicates which spend mode we want. 0. Recovery mode 1. Run INNER_PUZZLE with p2_solution 14 | my_amount_or_inner_solution ; In mode 0, we use this to recover our coin and assert it is our actual amount 15 | ; In mode 1 this is the solution of the inner P2 puzzle, only required in the create message mode and transfer mode. 16 | new_inner_puzhash ; In recovery mode, this will be the new wallet DID puzzle hash 17 | parent_innerpuzhash_amounts_for_recovery_ids ; during a recovery we need extra information about our recovery list coins 18 | pubkey ; this is the new pubkey used for a recovery 19 | recovery_list_reveal ; this is the reveal of the stored list of DIDs approved for recovery 20 | my_id ; my coin ID 21 | ) 22 | ;message is the new puzzle in the recovery and standard spend cases 23 | 24 | ;MOD_HASH, MY_PUBKEY, RECOVERY_DID_LIST_HASH are curried into the puzzle 25 | ;EXAMPLE SOLUTION (0xcafef00d 0x12341234 0x923bf9a7856b19d335a65f12d68957d497e1f0c16c0e14baf6d120e60753a1ce 2 1 100 (q "source code") 0xdeadbeef 0xcafef00d ((0xdadadada 0xdad5dad5 200) () (0xfafafafa 0xfaf5faf5 200)) 0xfadeddab (0x22222222 0x33333333 0x44444444)) 26 | 27 | (include condition_codes.clvm) 28 | (include curry-and-treehash.clinc) 29 | 30 | ; takes a lisp tree and returns the hash of it 31 | (defun sha256tree1 (TREE) 32 | (if (l TREE) 33 | (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) 34 | (sha256 1 TREE) 35 | ) 36 | ) 37 | 38 | ; recovery message module - gets values curried in to make the puzzle 39 | (defun make_message_puzzle (recovering_coin newpuz pubkey) 40 | (qq (q . (((unquote CREATE_COIN_ANNOUNCEMENT) (unquote recovering_coin)) ((unquote AGG_SIG_UNSAFE) (unquote pubkey) (unquote newpuz))))) 41 | ) 42 | 43 | ; this function creates the assert announcement for each message coin approving a recovery 44 | (defun-inline create_consume_message (coin_id my_id new_innerpuz pubkey) 45 | (list ASSERT_COIN_ANNOUNCEMENT (sha256 (sha256 coin_id (sha256tree1 (make_message_puzzle my_id new_innerpuz pubkey))) my_id)) 46 | ) 47 | 48 | ; this function calculates a coin ID given the inner puzzle and singleton information 49 | (defun create_coin_ID_for_recovery (SINGLETON_STRUCT launcher_id parent innerpuzhash amount) 50 | (sha256 parent (calculate_full_puzzle_hash (c (f SINGLETON_STRUCT) (c launcher_id (r (r SINGLETON_STRUCT)))) innerpuzhash) amount) 51 | ) 52 | 53 | 54 | ; return the full puzzlehash for a singleton with the innerpuzzle curried in 55 | ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc 56 | (defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) 57 | (puzzle-hash-of-curried-function (f SINGLETON_STRUCT) 58 | inner_puzzle_hash 59 | (sha256tree1 SINGLETON_STRUCT) 60 | ) 61 | ) 62 | 63 | ; this loops over our identities to check list, and checks if we have been given parent information for this identity 64 | ; the reason for this is because we might only require 3/5 of the IDs give approval messages for a recovery 65 | ; if we have the information for an identity then we create a consume message using that information 66 | 67 | (defun check_messages_from_identities (SINGLETON_STRUCT num_verifications_required identities my_id new_puz parent_innerpuzhash_amounts_for_recovery_ids pubkey num_verifications) 68 | (if identities 69 | (if (f parent_innerpuzhash_amounts_for_recovery_ids) 70 | ; if we have parent information then we should create a consume coin condition 71 | (c 72 | (create_consume_message 73 | ; create coin_id from DID 74 | (create_coin_ID_for_recovery 75 | SINGLETON_STRUCT 76 | (f identities) 77 | (f (f parent_innerpuzhash_amounts_for_recovery_ids)) 78 | (f (r (f parent_innerpuzhash_amounts_for_recovery_ids))) 79 | (f (r (r (f parent_innerpuzhash_amounts_for_recovery_ids))))) 80 | my_id 81 | new_puz 82 | pubkey 83 | ) 84 | (check_messages_from_identities 85 | SINGLETON_STRUCT 86 | num_verifications_required 87 | (r identities) 88 | my_id 89 | new_puz 90 | (r parent_innerpuzhash_amounts_for_recovery_ids) 91 | pubkey 92 | (+ num_verifications 1) 93 | ) 94 | ) 95 | ; if no parent information found for this identity, move on to next in list 96 | (check_messages_from_identities 97 | SINGLETON_STRUCT 98 | (r identities) 99 | my_id 100 | new_puz 101 | (r parent_innerpuzhash_amounts_for_recovery_ids) 102 | pubkey 103 | num_verifications 104 | ) 105 | ) 106 | ;if we're out of identites to check for, check we have enough 107 | (if (> num_verifications (- num_verifications_required 1)) 108 | (list (list AGG_SIG_UNSAFE pubkey new_puz) ) 109 | (x) 110 | ) 111 | ) 112 | ) 113 | 114 | ;Spend modes: 115 | ;0 = recovery 116 | ;1 = run the INNER_PUZZLE 117 | 118 | ;MAIN 119 | (if mode 120 | ; mode 1 - run INNER_PUZZLE 121 | (a INNER_PUZZLE my_amount_or_inner_solution) 122 | 123 | ; mode 0 - recovery 124 | (if (all (= (sha256tree1 recovery_list_reveal) RECOVERY_DID_LIST_HASH) (> NUM_VERIFICATIONS_REQUIRED 0)) 125 | (c (list ASSERT_MY_AMOUNT my_amount_or_inner_solution) 126 | (c (list CREATE_COIN new_inner_puzhash my_amount_or_inner_solution (list new_inner_puzhash)) 127 | (c (list ASSERT_MY_COIN_ID my_id) 128 | (check_messages_from_identities SINGLETON_STRUCT NUM_VERIFICATIONS_REQUIRED recovery_list_reveal my_id new_inner_puzhash parent_innerpuzhash_amounts_for_recovery_ids pubkey 0) 129 | ) 130 | ) 131 | ) 132 | (x) 133 | ) 134 | ) 135 | ) 136 | -------------------------------------------------------------------------------- /openapi/clsp/did_innerpuz.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ffff03ff81bfffff01ff02ff05ff82017f80ffff01ff02ffff03ffff22ffff09ffff02ff7effff04ff02ffff04ff8217ffff80808080ff0b80ffff15ff17ff808080ffff01ff04ffff04ff28ffff04ff82017fff808080ffff04ffff04ff34ffff04ff8202ffffff04ff82017fffff04ffff04ff8202ffff8080ff8080808080ffff04ffff04ff38ffff04ff822fffff808080ffff02ff26ffff04ff02ffff04ff2fffff04ff17ffff04ff8217ffffff04ff822fffffff04ff8202ffffff04ff8205ffffff04ff820bffffff01ff8080808080808080808080808080ffff01ff088080ff018080ff0180ffff04ffff01ffffffff313dff4946ffff0233ff3c04ffffff0101ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff22ff3c80ffff0bff2affff0bff2affff0bff22ff3280ff0980ffff0bff2aff0bffff0bff22ff8080808080ff8080808080ffff010b80ff0180ffffff02ffff03ff17ffff01ff02ffff03ff82013fffff01ff04ffff04ff30ffff04ffff0bffff0bffff02ff36ffff04ff02ffff04ff05ffff04ff27ffff04ff82023fffff04ff82053fffff04ff820b3fff8080808080808080ffff02ff7effff04ff02ffff04ffff02ff2effff04ff02ffff04ff2fffff04ff5fffff04ff82017fff808080808080ff8080808080ff2f80ff808080ffff02ff26ffff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff2fffff04ff5fffff04ff8201bfffff04ff82017fffff04ffff10ff8202ffffff010180ff808080808080808080808080ffff01ff02ff26ffff04ff02ffff04ff05ffff04ff37ffff04ff2fffff04ff5fffff04ff8201bfffff04ff82017fffff04ff8202ffff8080808080808080808080ff0180ffff01ff02ffff03ffff15ff8202ffffff11ff0bffff01018080ffff01ff04ffff04ff20ffff04ff82017fffff04ff5fff80808080ff8080ffff01ff088080ff018080ff0180ff0bff17ffff02ff5effff04ff02ffff04ff09ffff04ff2fffff04ffff02ff7effff04ff02ffff04ffff04ff09ffff04ff0bff1d8080ff80808080ff808080808080ff5f80ffff04ffff0101ffff04ffff04ff2cffff04ff05ff808080ffff04ffff04ff20ffff04ff17ffff04ff0bff80808080ff80808080ffff0bff2affff0bff22ff2480ffff0bff2affff0bff2affff0bff22ff3280ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff22ff2280ff8080808080ffff0bff22ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff7effff04ff02ffff04ff09ff80808080ffff02ff7effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 2 | -------------------------------------------------------------------------------- /openapi/clsp/include/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GobyWallet/openapi/b9d36e3fced171ab4384359da21877ec65e23a9d/openapi/clsp/include/__init__.py -------------------------------------------------------------------------------- /openapi/clsp/include/cat_truths.clib: -------------------------------------------------------------------------------- 1 | ( 2 | (defun-inline cat_truth_data_to_truth_struct (innerpuzhash cat_struct my_id this_coin_info) 3 | (c 4 | (c 5 | innerpuzhash 6 | cat_struct 7 | ) 8 | (c 9 | my_id 10 | this_coin_info 11 | ) 12 | ) 13 | ) 14 | 15 | ; CAT Truths is: ((Inner puzzle hash . (MOD hash . (MOD hash hash . TAIL hash))) . (my_id . (my_parent_info my_puzhash my_amount))) 16 | 17 | (defun-inline my_inner_puzzle_hash_cat_truth (Truths) (f (f Truths))) 18 | (defun-inline cat_struct_truth (Truths) (r (f Truths))) 19 | (defun-inline my_id_cat_truth (Truths) (f (r Truths))) 20 | (defun-inline my_coin_info_truth (Truths) (r (r Truths))) 21 | (defun-inline my_amount_cat_truth (Truths) (f (r (r (my_coin_info_truth Truths))))) 22 | (defun-inline my_full_puzzle_hash_cat_truth (Truths) (f (r (my_coin_info_truth Truths)))) 23 | (defun-inline my_parent_cat_truth (Truths) (f (my_coin_info_truth Truths))) 24 | 25 | 26 | ; CAT mod_struct is: (MOD_HASH MOD_HASH_hash TAIL_PROGRAM TAIL_PROGRAM_hash) 27 | 28 | (defun-inline cat_mod_hash_truth (Truths) (f (cat_struct_truth Truths))) 29 | (defun-inline cat_mod_hash_hash_truth (Truths) (f (r (cat_struct_truth Truths)))) 30 | (defun-inline cat_tail_program_hash_truth (Truths) (f (r (r (cat_struct_truth Truths))))) 31 | ) 32 | -------------------------------------------------------------------------------- /openapi/clsp/include/condition_codes.clvm: -------------------------------------------------------------------------------- 1 | ; See chia/types/condition_opcodes.py 2 | 3 | ( 4 | (defconstant AGG_SIG_UNSAFE 49) 5 | (defconstant AGG_SIG_ME 50) 6 | 7 | ; the conditions below reserve coin amounts and have to be accounted for in output totals 8 | 9 | (defconstant CREATE_COIN 51) 10 | (defconstant RESERVE_FEE 52) 11 | 12 | ; the conditions below deal with announcements, for inter-coin communication 13 | 14 | ; coin announcements 15 | (defconstant CREATE_COIN_ANNOUNCEMENT 60) 16 | (defconstant ASSERT_COIN_ANNOUNCEMENT 61) 17 | 18 | ; puzzle announcements 19 | (defconstant CREATE_PUZZLE_ANNOUNCEMENT 62) 20 | (defconstant ASSERT_PUZZLE_ANNOUNCEMENT 63) 21 | 22 | ; the conditions below let coins inquire about themselves 23 | 24 | (defconstant ASSERT_MY_COIN_ID 70) 25 | (defconstant ASSERT_MY_PARENT_ID 71) 26 | (defconstant ASSERT_MY_PUZZLEHASH 72) 27 | (defconstant ASSERT_MY_AMOUNT 73) 28 | 29 | ; the conditions below ensure that we're "far enough" in the future 30 | 31 | ; wall-clock time 32 | (defconstant ASSERT_SECONDS_RELATIVE 80) 33 | (defconstant ASSERT_SECONDS_ABSOLUTE 81) 34 | 35 | ; block index 36 | (defconstant ASSERT_HEIGHT_RELATIVE 82) 37 | (defconstant ASSERT_HEIGHT_ABSOLUTE 83) 38 | 39 | ; A condition that is always true and always ignore all arguments 40 | (defconstant REMARK 1) 41 | ) 42 | -------------------------------------------------------------------------------- /openapi/clsp/include/curry-and-treehash.clinc: -------------------------------------------------------------------------------- 1 | ( 2 | ;; The code below is used to calculate of the tree hash of a curried function 3 | ;; without actually doing the curry, and using other optimization tricks 4 | ;; like unrolling `sha256tree`. 5 | 6 | (defconstant ONE 1) 7 | (defconstant TWO 2) 8 | (defconstant A_KW #a) 9 | (defconstant Q_KW #q) 10 | (defconstant C_KW #c) 11 | 12 | ;; Given the tree hash `environment-hash` of an environment tree E 13 | ;; and the tree hash `parameter-hash` of a constant parameter P 14 | ;; return the tree hash of the tree corresponding to 15 | ;; `(c (q . P) E)` 16 | ;; This is the new environment tree with the addition parameter P curried in. 17 | ;; 18 | ;; Note that `(c (q . P) E)` = `(c . ((q . P) . (E . 0)))` 19 | 20 | (defun-inline update-hash-for-parameter-hash (parameter-hash environment-hash) 21 | (sha256 TWO (sha256 ONE C_KW) 22 | (sha256 TWO (sha256 TWO (sha256 ONE Q_KW) parameter-hash) 23 | (sha256 TWO environment-hash (sha256 ONE 0)))) 24 | ) 25 | 26 | ;; This function recursively calls `update-hash-for-parameter-hash`, updating `environment-hash` 27 | ;; along the way. 28 | 29 | (defun build-curry-list (reversed-curry-parameter-hashes environment-hash) 30 | (if reversed-curry-parameter-hashes 31 | (build-curry-list (r reversed-curry-parameter-hashes) 32 | (update-hash-for-parameter-hash (f reversed-curry-parameter-hashes) environment-hash)) 33 | environment-hash 34 | ) 35 | ) 36 | 37 | ;; Given the tree hash `environment-hash` of an environment tree E 38 | ;; and the tree hash `function-hash` of a function tree F 39 | ;; return the tree hash of the tree corresponding to 40 | ;; `(a (q . F) E)` 41 | ;; This is the hash of a new function that adopts the new environment E. 42 | ;; This is used to build of the tree hash of a curried function. 43 | ;; 44 | ;; Note that `(a (q . F) E)` = `(a . ((q . F) . (E . 0)))` 45 | 46 | (defun-inline tree-hash-of-apply (function-hash environment-hash) 47 | (sha256 TWO (sha256 ONE A_KW) 48 | (sha256 TWO (sha256 TWO (sha256 ONE Q_KW) function-hash) 49 | (sha256 TWO environment-hash (sha256 ONE 0)))) 50 | ) 51 | 52 | ;; function-hash: 53 | ;; the hash of a puzzle function, ie. a `mod` 54 | ;; 55 | ;; reversed-curry-parameter-hashes: 56 | ;; a list of pre-hashed trees representing parameters to be curried into the puzzle. 57 | ;; Note that this must be applied in REVERSED order. This may seem strange, but it greatly simplifies 58 | ;; the underlying code, since we calculate the tree hash from the bottom nodes up, and the last 59 | ;; parameters curried must have their hashes calculated first. 60 | ;; 61 | ;; we return the hash of the curried expression 62 | ;; (a (q . function-hash) (c (cp1 (c cp2 (c ... 1)...)))) 63 | ;; 64 | ;; Note that from a user's perspective the hashes passed in here aren't simply 65 | ;; the hashes of the desired parameters, but their treehash representation since 66 | ;; that's the form we're assuming they take in the acutal curried program. 67 | 68 | (defun puzzle-hash-of-curried-function (function-hash . reversed-curry-parameter-hashes) 69 | (tree-hash-of-apply function-hash 70 | (build-curry-list reversed-curry-parameter-hashes (sha256 ONE ONE))) 71 | ) 72 | 73 | (defconstant b32 32) 74 | 75 | (defun-inline size_b32 (var) 76 | (= (strlen var) b32) 77 | ) 78 | 79 | (defun calculate_coin_id (parent puzzlehash amount) 80 | (if (all (size_b32 parent) (size_b32 puzzlehash) (> amount -1)) 81 | (sha256 parent puzzlehash amount) 82 | (x) 83 | ) 84 | ) 85 | 86 | ; takes a lisp tree and returns the hash of it 87 | (defun sha256tree (TREE) 88 | (if (l TREE) 89 | (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) 90 | (sha256 1 TREE))) 91 | 92 | ) 93 | -------------------------------------------------------------------------------- /openapi/clsp/include/singleton_truths.clib: -------------------------------------------------------------------------------- 1 | ( 2 | (defun-inline truth_data_to_truth_struct (my_id full_puzhash innerpuzhash my_amount lineage_proof singleton_struct) (c (c my_id full_puzhash) (c (c innerpuzhash my_amount) (c lineage_proof singleton_struct)))) 3 | 4 | (defun-inline my_id_truth (Truths) (f (f Truths))) 5 | (defun-inline my_full_puzzle_hash_truth (Truths) (r (f Truths))) 6 | (defun-inline my_inner_puzzle_hash_truth (Truths) (f (f (r Truths)))) 7 | (defun-inline my_amount_truth (Truths) (r (f (r Truths)))) 8 | (defun-inline my_lineage_proof_truth (Truths) (f (r (r Truths)))) 9 | (defun-inline singleton_struct_truth (Truths) (r (r (r Truths)))) 10 | 11 | (defun-inline singleton_mod_hash_truth (Truths) (f (singleton_struct_truth Truths))) 12 | (defun-inline singleton_launcher_id_truth (Truths) (f (r (singleton_struct_truth Truths)))) 13 | (defun-inline singleton_launcher_puzzle_hash_truth (Truths) (f (r (r (singleton_struct_truth Truths))))) 14 | 15 | (defun-inline parent_info_for_lineage_proof (lineage_proof) (f lineage_proof)) 16 | (defun-inline puzzle_hash_for_lineage_proof (lineage_proof) (f (r lineage_proof))) 17 | (defun-inline amount_for_lineage_proof (lineage_proof) (f (r (r lineage_proof)))) 18 | (defun-inline is_not_eve_proof (lineage_proof) (r (r lineage_proof))) 19 | (defun-inline parent_info_for_eve_proof (lineage_proof) (f lineage_proof)) 20 | (defun-inline amount_for_eve_proof (lineage_proof) (f (r lineage_proof))) 21 | ) -------------------------------------------------------------------------------- /openapi/clsp/include/utility_macros.clib: -------------------------------------------------------------------------------- 1 | ( 2 | (defmacro assert items 3 | (if (r items) 4 | (list if (f items) (c assert (r items)) (q . (x))) 5 | (f items) 6 | ) 7 | ) 8 | 9 | (defmacro and ARGS 10 | (if ARGS 11 | (qq (if (unquote (f ARGS)) 12 | (unquote (c and (r ARGS))) 13 | () 14 | )) 15 | 1) 16 | ) 17 | ) -------------------------------------------------------------------------------- /openapi/clsp/nft_metadata_updater_default.clvm: -------------------------------------------------------------------------------- 1 | (mod (CURRENT_METADATA METADATA_UPDATER_PUZZLE_HASH (key . new_url)) 2 | 3 | ; METADATA and METADATA_UPDATER_PUZZLE_HASH are passed in as truths from the layer above 4 | ; This program returns ((new_metadata new_metadata_updater_puzhash) conditions) 5 | 6 | ; Add uri to a field 7 | (defun add_url (METADATA key new_url) 8 | (if METADATA 9 | (if (= (f (f METADATA)) key) 10 | (c (c key (c new_url (r (f METADATA)))) (r METADATA)) 11 | (c (f METADATA) (add_url (r METADATA) key new_url)) 12 | ) 13 | () 14 | ) 15 | ) 16 | ; main 17 | ; returns ((new_metadata new_metadata_updater_puzhash) conditions) 18 | (list 19 | (list 20 | (if (all key new_url) 21 | (if (any (= key "mu") (= key "lu") (= key "u")) 22 | (add_url CURRENT_METADATA key new_url) 23 | CURRENT_METADATA 24 | ) 25 | CURRENT_METADATA 26 | ) 27 | METADATA_UPDATER_PUZZLE_HASH) 28 | 0 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /openapi/clsp/nft_metadata_updater_default.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff04ffff04ffff02ffff03ffff22ff27ff3780ffff01ff02ffff03ffff21ffff09ff27ffff01826d7580ffff09ff27ffff01826c7580ffff09ff27ffff01758080ffff01ff02ff02ffff04ff02ffff04ff05ffff04ff27ffff04ff37ff808080808080ffff010580ff0180ffff010580ff0180ffff04ff0bff808080ffff01ff808080ffff04ffff01ff02ffff03ff05ffff01ff02ffff03ffff09ff11ff0b80ffff01ff04ffff04ff0bffff04ff17ff198080ff0d80ffff01ff04ff09ffff02ff02ffff04ff02ffff04ff0dffff04ff0bffff04ff17ff8080808080808080ff0180ff8080ff0180ff018080 2 | -------------------------------------------------------------------------------- /openapi/clsp/nft_ownership_layer.clvm: -------------------------------------------------------------------------------- 1 | (mod ( 2 | NFT_OWNERSHIP_LAYER_MOD_HASH 3 | CURRENT_OWNER 4 | TRANSFER_PROGRAM 5 | INNER_PUZZLE 6 | inner_solution 7 | ) 8 | 9 | (include condition_codes.clvm) 10 | (include curry-and-treehash.clinc) 11 | (include utility_macros.clib) 12 | 13 | (defconstant NEW_OWNER_CONDITION -10) 14 | (defconstant ANNOUNCEMENT_PREFIX 0xad4c) ; first 2 bytes of (sha256 "Ownership Layer") 15 | 16 | (defun-inline nft_ownership_layer_puzzle_hash (NFT_OWNERSHIP_LAYER_MOD_HASH new_owner TRANSFER_PROGRAM inner_puzzle_hash) 17 | (puzzle-hash-of-curried-function NFT_OWNERSHIP_LAYER_MOD_HASH 18 | inner_puzzle_hash 19 | (sha256tree TRANSFER_PROGRAM) 20 | (sha256 ONE new_owner) 21 | (sha256 ONE NFT_OWNERSHIP_LAYER_MOD_HASH) 22 | ) 23 | ) 24 | 25 | (defun construct_end_conditions (NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM odd_args (new_owner new_tp conditions)) 26 | (c 27 | (c 28 | CREATE_COIN 29 | (c 30 | (nft_ownership_layer_puzzle_hash NFT_OWNERSHIP_LAYER_MOD_HASH new_owner (if new_tp new_tp TRANSFER_PROGRAM) (f odd_args)) 31 | (r odd_args) 32 | ) 33 | ) 34 | conditions 35 | ) 36 | ) 37 | 38 | (defun wrap_odd_create_coins (NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions conditions odd_args tp_output) 39 | (if conditions 40 | (if (= (f (f conditions)) CREATE_COIN) 41 | (if (= (logand (f (r (r (f conditions))))) ONE) 42 | (assert (not odd_args) 43 | ; then 44 | (wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) (r (f conditions)) tp_output) 45 | ) 46 | (c (f conditions) (wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) odd_args tp_output)) 47 | ) 48 | (if (= (f (f conditions)) NEW_OWNER_CONDITION) 49 | (assert (not tp_output) 50 | (c 51 | (list CREATE_PUZZLE_ANNOUNCEMENT (concat ANNOUNCEMENT_PREFIX (sha256tree (r (f conditions))))) 52 | (wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) odd_args (a TRANSFER_PROGRAM (list CURRENT_OWNER all_conditions (r (f conditions))))) 53 | ) 54 | ) 55 | (if (= (f (f conditions)) CREATE_PUZZLE_ANNOUNCEMENT) 56 | (assert (not (and 57 | (= 34 (strlen (f (r (f conditions))))) 58 | (= (substr (f (r (f conditions))) 0 2) ANNOUNCEMENT_PREFIX) ; lazy eval 59 | )) 60 | ; then 61 | (c (f conditions) (wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) odd_args tp_output)) 62 | ) 63 | (c (f conditions) (wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) odd_args tp_output)) 64 | ) 65 | ) 66 | ) 67 | ; odd_args is guaranteed to not be nil or else we'll have a path into atom error 68 | (construct_end_conditions NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM odd_args 69 | (if tp_output 70 | tp_output 71 | (a TRANSFER_PROGRAM (list CURRENT_OWNER all_conditions ())) 72 | ) 73 | ) 74 | ) 75 | ) 76 | 77 | (defun main ( 78 | NFT_OWNERSHIP_LAYER_MOD_HASH 79 | TRANSFER_PROGRAM 80 | CURRENT_OWNER 81 | conditions 82 | ) 83 | (wrap_odd_create_coins 84 | NFT_OWNERSHIP_LAYER_MOD_HASH 85 | TRANSFER_PROGRAM 86 | CURRENT_OWNER 87 | conditions 88 | conditions 89 | () () 90 | ) 91 | ) 92 | 93 | ; main 94 | (main 95 | NFT_OWNERSHIP_LAYER_MOD_HASH 96 | TRANSFER_PROGRAM 97 | CURRENT_OWNER 98 | (a INNER_PUZZLE inner_solution) 99 | ) 100 | ) 101 | -------------------------------------------------------------------------------- /openapi/clsp/nft_ownership_layer.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ff26ffff04ff02ffff04ff05ffff04ff17ffff04ff0bffff04ffff02ff2fff5f80ff80808080808080ffff04ffff01ffffff82ad4cff0233ffff3e04ff81f601ffffff0102ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff32ffff0bff3cff3480ffff0bff32ffff0bff32ffff0bff3cff2280ff0980ffff0bff32ff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ff04ffff04ff38ffff04ffff02ff36ffff04ff02ffff04ff05ffff04ff27ffff04ffff02ff2effff04ff02ffff04ffff02ffff03ff81afffff0181afffff010b80ff0180ff80808080ffff04ffff0bff3cff4f80ffff04ffff0bff3cff0580ff8080808080808080ff378080ff82016f80ffffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff2fffff01ff80ff808080808080808080ff0bff32ffff0bff3cff2880ffff0bff32ffff0bff32ffff0bff3cff2280ff0580ffff0bff32ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff5fffff01ff02ffff03ffff09ff82011fff3880ffff01ff02ffff03ffff09ffff18ff82059f80ff3c80ffff01ff02ffff03ffff20ff81bf80ffff01ff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff82019fffff04ff82017fff80808080808080808080ffff01ff088080ff0180ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff808080808080808080808080ff0180ffff01ff02ffff03ffff09ff82011fff2c80ffff01ff02ffff03ffff20ff82017f80ffff01ff04ffff04ff24ffff04ffff0eff10ffff02ff2effff04ff02ffff04ff82019fff8080808080ff808080ffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ffff02ff0bffff04ff17ffff04ff2fffff04ff82019fff8080808080ff8080808080808080808080ffff01ff088080ff0180ffff01ff02ffff03ffff09ff82011fff2480ffff01ff02ffff03ffff20ffff02ffff03ffff09ffff0122ffff0dff82029f8080ffff01ff02ffff03ffff09ffff0cff82029fff80ffff010280ff1080ffff01ff0101ff8080ff0180ff8080ff018080ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff8080808080808080808080ffff01ff088080ff0180ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff808080808080808080808080ff018080ff018080ff0180ffff01ff02ff3affff04ff02ffff04ff05ffff04ff0bffff04ff81bfffff04ffff02ffff03ff82017fffff0182017fffff01ff02ff0bffff04ff17ffff04ff2fffff01ff808080808080ff0180ff8080808080808080ff0180ff018080 2 | -------------------------------------------------------------------------------- /openapi/clsp/nft_ownership_transfer_program_one_way_claim_with_royalties.clvm: -------------------------------------------------------------------------------- 1 | (mod 2 | ( 3 | SINGLETON_STRUCT 4 | ROYALTY_ADDRESS 5 | TRADE_PRICE_PERCENTAGE 6 | Current_Owner ; Truth 7 | conditions ; Truth 8 | solution ; created from the NFT's inner puzzle - solution is (new_owner trade_prices_list new_did_inner_hash) 9 | ) 10 | 11 | ; This is a transfer program - which must return (new_owner, Optional[new_transfer_program], conditions) 12 | 13 | (include condition_codes.clvm) 14 | (include curry-and-treehash.clinc) 15 | 16 | (defconstant TEN_THOUSAND 10000) 17 | 18 | ;; return the full puzzlehash for a singleton with the innerpuzzle curried in 19 | ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc 20 | (defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) 21 | (puzzle-hash-of-curried-function (f SINGLETON_STRUCT) 22 | inner_puzzle_hash 23 | (sha256tree SINGLETON_STRUCT) 24 | ) 25 | ) 26 | 27 | ; Given a singleton ID, generate the singleton struct 28 | (defun-inline get_singleton_struct (SINGLETON_STRUCT singleton_id) 29 | (c (f SINGLETON_STRUCT) (c singleton_id (r (r SINGLETON_STRUCT)))) 30 | ) 31 | 32 | (defun-inline calculate_percentage (amount percentage) 33 | (f (divmod (* amount percentage) TEN_THOUSAND)) 34 | ) 35 | 36 | ; Loop of the trade prices list and either assert a puzzle announcement or generate xch 37 | (defun parse_trade_prices_list (ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE trade_prices_list my_nft_id) 38 | (if trade_prices_list 39 | (c 40 | (list 41 | ASSERT_PUZZLE_ANNOUNCEMENT 42 | (sha256 43 | (f (r (f trade_prices_list))) 44 | (sha256tree (c my_nft_id (list (list ROYALTY_ADDRESS (calculate_percentage (f (f trade_prices_list)) TRADE_PRICE_PERCENTAGE) (list ROYALTY_ADDRESS))))) 45 | ) 46 | ) 47 | (parse_trade_prices_list ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE (r trade_prices_list) my_nft_id) 48 | ) 49 | () 50 | ) 51 | ) 52 | 53 | ; main 54 | ; Returning (new_owner new_transfer_program conditions) 55 | ; solution is (new_owner trade_prices_list new_did_inner_hash) 56 | (if solution 57 | (list 58 | (f solution) 59 | 0 60 | (if (all (f solution) (not (= (f solution) Current_Owner))) 61 | (c 62 | (list 63 | ASSERT_PUZZLE_ANNOUNCEMENT 64 | (sha256 65 | (calculate_full_puzzle_hash (get_singleton_struct SINGLETON_STRUCT (f solution)) (f (r (r solution)))) 66 | (f (r SINGLETON_STRUCT)) 67 | ) 68 | ) 69 | (parse_trade_prices_list ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE (f (r solution)) (f (r SINGLETON_STRUCT))) 70 | ) 71 | (parse_trade_prices_list ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE (f (r solution)) (f (r SINGLETON_STRUCT))) 72 | ) 73 | ) 74 | (list Current_Owner () ()) 75 | ) 76 | 77 | 78 | ) 79 | -------------------------------------------------------------------------------- /openapi/clsp/nft_ownership_transfer_program_one_way_claim_with_royalties.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ffff03ff81bfffff01ff04ff82013fffff04ff80ffff04ffff02ffff03ffff22ff82013fffff20ffff09ff82013fff2f808080ffff01ff04ffff04ff10ffff04ffff0bffff02ff2effff04ff02ffff04ff09ffff04ff8205bfffff04ffff02ff3effff04ff02ffff04ffff04ff09ffff04ff82013fff1d8080ff80808080ff808080808080ff1580ff808080ffff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff8202bfffff04ff15ff8080808080808080ffff01ff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff8202bfffff04ff15ff8080808080808080ff0180ff80808080ffff01ff04ff2fffff01ff80ff80808080ff0180ffff04ffff01ffffff3f02ff04ff0101ffff822710ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff2cff1480ffff0bff2affff0bff2affff0bff2cff3c80ff0980ffff0bff2aff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff17ffff01ff04ffff04ff10ffff04ffff0bff81a7ffff02ff3effff04ff02ffff04ffff04ff2fffff04ffff04ff05ffff04ffff05ffff14ffff12ff47ff0b80ff128080ffff04ffff04ff05ff8080ff80808080ff808080ff8080808080ff808080ffff02ff16ffff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff2fff8080808080808080ff8080ff0180ffff0bff2affff0bff2cff1880ffff0bff2affff0bff2affff0bff2cff3c80ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 2 | -------------------------------------------------------------------------------- /openapi/clsp/nft_state_layer.clvm: -------------------------------------------------------------------------------- 1 | (mod ( 2 | NFT_STATE_LAYER_MOD_HASH 3 | METADATA 4 | METADATA_UPDATER_PUZZLE_HASH 5 | INNER_PUZZLE 6 | inner_solution 7 | ) 8 | 9 | (include condition_codes.clvm) 10 | (include curry-and-treehash.clinc) 11 | (include utility_macros.clib) 12 | 13 | (defun-inline nft_state_layer_puzzle_hash (NFT_STATE_LAYER_MOD_HASH METADATA METADATA_UPDATER_PUZZLE_HASH inner_puzzle_hash) 14 | (puzzle-hash-of-curried-function NFT_STATE_LAYER_MOD_HASH 15 | inner_puzzle_hash 16 | (sha256 ONE METADATA_UPDATER_PUZZLE_HASH) 17 | (sha256tree METADATA) 18 | (sha256 ONE NFT_STATE_LAYER_MOD_HASH) 19 | ) 20 | ) 21 | 22 | 23 | ; this function does two things - it wraps the odd value create coins, and it also filters out all negative conditions 24 | ; odd_coin_params is (puzhash amount ...) 25 | ; new_metadata_info is ((METADATA METADATA_UPDATER_PUZZLE_HASH) conditions) 26 | (defun wrap_odd_create_coins (NFT_STATE_LAYER_MOD_HASH conditions odd_coin_params new_metadata_info metadata_seen) 27 | (if conditions 28 | (if (= (f (f conditions)) CREATE_COIN) 29 | (if (logand (f (r (r (f conditions)))) ONE) 30 | (assert (not odd_coin_params) 31 | (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (r conditions) (r (f conditions)) new_metadata_info metadata_seen) 32 | ) 33 | (c (f conditions) (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (r conditions) odd_coin_params new_metadata_info metadata_seen)) 34 | ) 35 | (if (= (f (f conditions)) -24) 36 | (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (r conditions) odd_coin_params 37 | (assert (all 38 | (= (sha256tree (f (r (f conditions)))) (f (r (f new_metadata_info)))) 39 | (not metadata_seen) 40 | ) 41 | ; then 42 | (a (f (r (f conditions))) (list (f (f new_metadata_info)) (f (r (f new_metadata_info))) (f (r (r (f conditions)))))) 43 | ) 44 | ONE ; the metadata update has been seen now 45 | ) 46 | (c (f conditions) (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (r conditions) odd_coin_params new_metadata_info metadata_seen)) 47 | ) 48 | ) 49 | (c 50 | (c CREATE_COIN 51 | (c 52 | (nft_state_layer_puzzle_hash 53 | NFT_STATE_LAYER_MOD_HASH 54 | (f (f new_metadata_info)) 55 | (f (r (f new_metadata_info))) 56 | (f odd_coin_params) ; metadata updater solution 57 | ) 58 | (r odd_coin_params) 59 | ) 60 | ) 61 | (f (r new_metadata_info)) ; metadata_updater conditions 62 | ) 63 | ) 64 | ) 65 | 66 | ; main 67 | (wrap_odd_create_coins 68 | NFT_STATE_LAYER_MOD_HASH 69 | (a INNER_PUZZLE inner_solution) 70 | () 71 | (list (list METADATA METADATA_UPDATER_PUZZLE_HASH) 0) ; if the magic condition is never seen, this is the information we us to recurry 72 | () 73 | ) 74 | ) 75 | -------------------------------------------------------------------------------- /openapi/clsp/nft_state_layer.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080 2 | -------------------------------------------------------------------------------- /openapi/clsp/p2_delegated_puzzle_or_hidden_puzzle.clvm: -------------------------------------------------------------------------------- 1 | ; build a pay-to delegated puzzle or hidden puzzle 2 | ; coins can be unlocked by signing a delegated puzzle and its solution 3 | ; OR by revealing the hidden puzzle and the underlying original key 4 | 5 | ; glossary of parameter names: 6 | 7 | ; hidden_puzzle: a "hidden puzzle" that can be revealed and used as an alternate 8 | ; way to unlock the underlying funds 9 | ; 10 | ; synthetic_key_offset: a private key cryptographically generated using the hidden 11 | ; puzzle and as inputs `original_public_key` 12 | ; 13 | ; SYNTHETIC_PUBLIC_KEY: the public key that is the sum of `original_public_key` and the 14 | ; public key corresponding to `synthetic_key_offset` 15 | ; 16 | ; original_public_key: a public key, where knowledge of the corresponding private key 17 | ; represents ownership of the file 18 | ; 19 | ; delegated_puzzle: a delegated puzzle, as in "graftroot", which should return the 20 | ; desired conditions. 21 | ; 22 | ; solution: the solution to the delegated puzzle 23 | 24 | 25 | (mod 26 | ; A puzzle should commit to `SYNTHETIC_PUBLIC_KEY` 27 | ; 28 | ; The solution should pass in 0 for `original_public_key` if it wants to use 29 | ; an arbitrary `delegated_puzzle` (and `solution`) signed by the 30 | ; `SYNTHETIC_PUBLIC_KEY` (whose corresponding private key can be calculated 31 | ; if you know the private key for `original_public_key`) 32 | ; 33 | ; Or you can solve the hidden puzzle by revealing the `original_public_key`, 34 | ; the hidden puzzle in `delegated_puzzle`, and a solution to the hidden puzzle. 35 | 36 | (SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle solution) 37 | 38 | ; "assert" is a macro that wraps repeated instances of "if" 39 | ; usage: (assert A0 A1 ... An R) 40 | ; all of A0, A1, ... An must evaluate to non-null, or an exception is raised 41 | ; return the value of R (if we get that far) 42 | 43 | (defmacro assert items 44 | (if (r items) 45 | (list if (f items) (c assert (r items)) (q . (x))) 46 | (f items) 47 | ) 48 | ) 49 | 50 | (include condition_codes.clvm) 51 | 52 | ;; hash a tree 53 | ;; This is used to calculate a puzzle hash given a puzzle program. 54 | (defun sha256tree1 55 | (TREE) 56 | (if (l TREE) 57 | (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) 58 | (sha256 1 TREE) 59 | ) 60 | ) 61 | 62 | ; "is_hidden_puzzle_correct" returns true iff the hidden puzzle is correctly encoded 63 | 64 | (defun-inline is_hidden_puzzle_correct (SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle) 65 | (= 66 | SYNTHETIC_PUBLIC_KEY 67 | (point_add 68 | original_public_key 69 | (pubkey_for_exp (sha256 original_public_key (sha256tree1 delegated_puzzle))) 70 | ) 71 | ) 72 | ) 73 | 74 | ; "possibly_prepend_aggsig" is the main entry point 75 | 76 | (defun-inline possibly_prepend_aggsig (SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle conditions) 77 | (if original_public_key 78 | (assert 79 | (is_hidden_puzzle_correct SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle) 80 | conditions 81 | ) 82 | (c (list AGG_SIG_ME SYNTHETIC_PUBLIC_KEY (sha256tree1 delegated_puzzle)) conditions) 83 | ) 84 | ) 85 | 86 | ; main entry point 87 | 88 | (possibly_prepend_aggsig 89 | SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle 90 | (a delegated_puzzle solution)) 91 | ) 92 | -------------------------------------------------------------------------------- /openapi/clsp/p2_delegated_puzzle_or_hidden_puzzle.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 2 | -------------------------------------------------------------------------------- /openapi/clsp/settlement_payments.clvm: -------------------------------------------------------------------------------- 1 | (mod notarized_payments 2 | ;; `notarized_payments` is a list of notarized coin payments 3 | ;; a notarized coin payment is `(nonce . ((puzzle_hash amount ...) (puzzle_hash amount ...) ...))` 4 | ;; Each notarized coin payment creates some `(CREATE_COIN puzzle_hash amount ...)` payments 5 | ;; and a `(CREATE_PUZZLE_ANNOUNCEMENT (sha256tree notarized_coin_payment))` announcement 6 | ;; The idea is the other side of this trade requires observing the announcement from a 7 | ;; `settlement_payments` puzzle hash as a condition of one or more coin spends. 8 | 9 | (include condition_codes.clvm) 10 | 11 | (defun sha256tree (TREE) 12 | (if (l TREE) 13 | (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) 14 | (sha256 1 TREE) 15 | ) 16 | ) 17 | 18 | (defun create_coins_for_payment (payment_params so_far) 19 | (if payment_params 20 | (c (c CREATE_COIN (f payment_params)) (create_coins_for_payment (r payment_params) so_far)) 21 | so_far 22 | ) 23 | ) 24 | 25 | (defun-inline create_announcement_for_payment (notarized_payment) 26 | (list CREATE_PUZZLE_ANNOUNCEMENT 27 | (sha256tree notarized_payment)) 28 | ) 29 | 30 | (defun-inline augment_condition_list (notarized_payment so_far) 31 | (c 32 | (create_announcement_for_payment notarized_payment) 33 | (create_coins_for_payment (r notarized_payment) so_far) 34 | ) 35 | ) 36 | 37 | (defun construct_condition_list (notarized_payments) 38 | (if notarized_payments 39 | (augment_condition_list (f notarized_payments) (construct_condition_list (r notarized_payments))) 40 | () 41 | ) 42 | ) 43 | 44 | (construct_condition_list notarized_payments) 45 | ) -------------------------------------------------------------------------------- /openapi/clsp/settlement_payments.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 2 | -------------------------------------------------------------------------------- /openapi/clsp/singleton_launcher.clvm: -------------------------------------------------------------------------------- 1 | (mod (singleton_full_puzzle_hash amount key_value_list) 2 | 3 | (include condition_codes.clvm) 4 | 5 | ; takes a lisp tree and returns the hash of it 6 | (defun sha256tree1 (TREE) 7 | (if (l TREE) 8 | (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) 9 | (sha256 1 TREE) 10 | ) 11 | ) 12 | 13 | ; main 14 | (list (list CREATE_COIN singleton_full_puzzle_hash amount) 15 | (list CREATE_COIN_ANNOUNCEMENT (sha256tree1 (list singleton_full_puzzle_hash amount key_value_list)))) 16 | ) 17 | -------------------------------------------------------------------------------- /openapi/clsp/singleton_launcher.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff04ffff04ff04ffff04ff05ffff04ff0bff80808080ffff04ffff04ff0affff04ffff02ff0effff04ff02ffff04ffff04ff05ffff04ff0bffff04ff17ff80808080ff80808080ff808080ff808080ffff04ffff01ff33ff3cff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff0effff04ff02ffff04ff09ff80808080ffff02ff0effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 2 | -------------------------------------------------------------------------------- /openapi/clsp/singleton_top_layer_v1_1.clvm: -------------------------------------------------------------------------------- 1 | (mod (SINGLETON_STRUCT INNER_PUZZLE lineage_proof my_amount inner_solution) 2 | 3 | ;; SINGLETON_STRUCT = (MOD_HASH . (LAUNCHER_ID . LAUNCHER_PUZZLE_HASH)) 4 | 5 | ; SINGLETON_STRUCT, INNER_PUZZLE are curried in by the wallet 6 | 7 | ; EXAMPLE SOLUTION '(0xfadeddab 0xdeadbeef 1 (0xdeadbeef 200) 50 ((51 0xfadeddab 100) (60 "trash") (51 deadbeef 0)))' 8 | 9 | 10 | ; This puzzle is a wrapper around an inner smart puzzle which guarantees uniqueness. 11 | ; It takes its singleton identity from a coin with a launcher puzzle which guarantees that it is unique. 12 | 13 | (include condition_codes.clvm) 14 | (include curry-and-treehash.clinc) ; also imports the constant ONE == 1 15 | (include singleton_truths.clib) 16 | (include utility_macros.clib) 17 | 18 | (defun-inline mod_hash_for_singleton_struct (SINGLETON_STRUCT) (f SINGLETON_STRUCT)) 19 | (defun-inline launcher_id_for_singleton_struct (SINGLETON_STRUCT) (f (r SINGLETON_STRUCT))) 20 | (defun-inline launcher_puzzle_hash_for_singleton_struct (SINGLETON_STRUCT) (r (r SINGLETON_STRUCT))) 21 | 22 | ;; return the full puzzlehash for a singleton with the innerpuzzle curried in 23 | ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc 24 | (defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) 25 | (puzzle-hash-of-curried-function (mod_hash_for_singleton_struct SINGLETON_STRUCT) 26 | inner_puzzle_hash 27 | (sha256tree SINGLETON_STRUCT) 28 | ) 29 | ) 30 | 31 | (defun-inline morph_condition (condition SINGLETON_STRUCT) 32 | (c (f condition) (c (calculate_full_puzzle_hash SINGLETON_STRUCT (f (r condition))) (r (r condition)))) 33 | ) 34 | 35 | (defun is_odd_create_coin (condition) 36 | (and (= (f condition) CREATE_COIN) (logand (f (r (r condition))) 1)) 37 | ) 38 | 39 | ; Assert exactly one output with odd value exists - ignore it if value is -113 40 | 41 | ;; this function iterates over the output conditions from the inner puzzle & solution 42 | ;; and both checks that exactly one unique singleton child is created (with odd valued output), 43 | ;; and wraps the inner puzzle with this same singleton wrapper puzzle 44 | ;; 45 | ;; The special case where the output value is -113 means a child singleton is intentionally 46 | ;; *NOT* being created, thus forever ending this singleton's existence 47 | 48 | (defun check_and_morph_conditions_for_singleton (SINGLETON_STRUCT conditions has_odd_output_been_found) 49 | (if conditions 50 | ; check if it's an odd create coin 51 | (if (is_odd_create_coin (f conditions)) 52 | ; check that we haven't already found one 53 | (assert (not has_odd_output_been_found) 54 | ; then 55 | (if (= (f (r (r (f conditions)))) -113) 56 | ; If it's the melt condition we don't bother prepending this condition 57 | (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (r conditions) ONE) 58 | ; If it isn't the melt condition, we morph it and prepend it 59 | (c (morph_condition (f conditions) SINGLETON_STRUCT) (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (r conditions) ONE)) 60 | ) 61 | ) 62 | (c (f conditions) (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (r conditions) has_odd_output_been_found)) 63 | ) 64 | (assert has_odd_output_been_found ()) 65 | ) 66 | ) 67 | 68 | ; assert that either the lineage proof is for a parent singleton, or, if it's for the launcher, verify it matched our launcher ID 69 | ; then return a condition asserting it actually is our parent ID 70 | (defun verify_lineage_proof (SINGLETON_STRUCT parent_id is_not_launcher) 71 | (assert (any is_not_launcher (= parent_id (launcher_id_for_singleton_struct SINGLETON_STRUCT))) 72 | ; then 73 | (list ASSERT_MY_PARENT_ID parent_id) 74 | ) 75 | ) 76 | 77 | ; main 78 | 79 | ; if our value is not an odd amount then we are invalid 80 | (assert (logand my_amount ONE) 81 | ; then 82 | (c 83 | (list ASSERT_MY_AMOUNT my_amount) 84 | (c 85 | ; Verify the lineage proof by asserting our parent's ID 86 | (verify_lineage_proof 87 | SINGLETON_STRUCT 88 | ; calculate our parent's ID 89 | (calculate_coin_id 90 | (parent_info_for_lineage_proof lineage_proof) 91 | (if (is_not_eve_proof lineage_proof) ; The PH calculation changes based on the lineage proof 92 | (calculate_full_puzzle_hash SINGLETON_STRUCT (puzzle_hash_for_lineage_proof lineage_proof)) ; wrap the innerpuz in a singleton 93 | (launcher_puzzle_hash_for_singleton_struct SINGLETON_STRUCT) ; Use the static launcher puzzle hash 94 | ) 95 | (if (is_not_eve_proof lineage_proof) ; The position of "amount" changes based on the type on lineage proof 96 | (amount_for_lineage_proof lineage_proof) 97 | (amount_for_eve_proof lineage_proof) 98 | ) 99 | ) 100 | (is_not_eve_proof lineage_proof) 101 | ) 102 | ; finally check all of the conditions for a single odd output to wrap 103 | (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (a INNER_PUZZLE inner_solution) 0) 104 | ) 105 | ) 106 | ) 107 | ) 108 | -------------------------------------------------------------------------------- /openapi/clsp/singleton_top_layer_v1_1.clvm.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080 2 | -------------------------------------------------------------------------------- /openapi/config.py: -------------------------------------------------------------------------------- 1 | 2 | from dynaconf import Dynaconf 3 | 4 | settings = Dynaconf( 5 | envvar_prefix="DYNACONF", 6 | settings_files=['settings.toml'], 7 | ) 8 | -------------------------------------------------------------------------------- /openapi/db.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Any 2 | from databases import Database 3 | import sqlalchemy 4 | from sqlalchemy import inspect, Column, ForeignKey, Integer, String, BINARY, BLOB, JSON, Boolean 5 | from sqlalchemy import select, update, insert, delete, func 6 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 7 | 8 | from . import config as settings 9 | 10 | KEY_DBS = {} 11 | 12 | 13 | 14 | def get_db(key) -> Database: 15 | return KEY_DBS[key] 16 | 17 | 18 | def register_db(key, uri): 19 | if key in KEY_DBS: 20 | raise ValueError(f"db: {key} has exists") 21 | KEY_DBS[key] = Database(uri) 22 | 23 | 24 | def create_tables(db: Database): 25 | database_url = db.url 26 | if database_url.scheme in ["mysql", "mysql+aiomysql", "mysql+asyncmy"]: 27 | url = str(database_url.replace(driver="pymysql")) 28 | elif database_url.scheme in [ 29 | "postgresql+aiopg", 30 | "sqlite+aiosqlite", 31 | "postgresql+asyncpg", 32 | ]: 33 | url = str(database_url.replace(driver=None)) 34 | engine = sqlalchemy.create_engine(url) 35 | Base.metadata.create_all(bind=engine) 36 | 37 | async def connect_db(key=None): 38 | if key is None: 39 | for db in KEY_DBS.values(): 40 | create_tables(db) 41 | await db.connect() 42 | else: 43 | db = KEY_DBS[key] 44 | create_tables(db) 45 | await db.connect() 46 | if "sqlite" in str(db.url): 47 | await db.execute( 48 | "PRAGMA journal_mode = WAL" 49 | ) 50 | 51 | 52 | async def disconnect_db(key=None): 53 | if key is None: 54 | for db in KEY_DBS.values(): 55 | await db.disconnect() 56 | else: 57 | await KEY_DBS[key].disconnect() 58 | 59 | 60 | @as_declarative() 61 | class Base: 62 | id: Any 63 | __name__: str 64 | # Generate __tablename__ automatically 65 | @declared_attr 66 | def __tablename__(cls) -> str: 67 | return cls.__name__.lower() 68 | 69 | def to_dict(self): 70 | return { 71 | c.key: getattr(self, c.key) 72 | for c in inspect(self).mapper.column_attrs 73 | } 74 | 75 | 76 | class Asset(Base): 77 | coin_id = Column(BINARY(32), primary_key=True) 78 | asset_type = Column(String(16), nullable=False, doc='did/nft') 79 | asset_id = Column(BINARY(32), nullable=False) 80 | confirmed_height = Column(Integer, nullable=False, server_default='0') 81 | spent_height = Column(Integer, index=True, nullable=False, server_default='0') # spent record can be deleted 82 | coin = Column(JSON, nullable=False) 83 | lineage_proof = Column(JSON, nullable=False) 84 | p2_puzzle_hash = Column(BINARY(32), nullable=False, index=True) 85 | nft_did_id = Column(BINARY(32), nullable=True, doc='for nft') 86 | curried_params = Column(JSON, nullable=False, doc='for recurry') 87 | 88 | 89 | class SingletonSpend(Base): 90 | singleton_id = Column(BINARY(32), primary_key=True) 91 | coin_id = Column(BINARY(32), nullable=False) 92 | spent_block_index = Column(Integer, nullable=False, server_default='0') 93 | 94 | 95 | class NftMetadata(Base): 96 | hash = Column(BINARY(32), primary_key=True, doc='sha256') 97 | format = Column(String(32), nullable=False, server_default='') 98 | name = Column(String(256), nullable=False, server_default='') 99 | collection_id = Column(String(256), nullable=False, server_default='') 100 | collection_name = Column(String(256), nullable=False, server_default='') 101 | full_data = Column(JSON, nullable=False) 102 | 103 | 104 | class Block(Base): 105 | hash = Column(BINARY(32), primary_key=True) 106 | height = Column(Integer, unique=True, nullable=False) 107 | timestamp = Column(Integer, nullable=False) 108 | prev_hash = Column(BINARY(32), nullable=False) 109 | is_tx = Column(Boolean, nullable=False) 110 | 111 | 112 | class AddressSync(Base): 113 | __tablename__ = 'address_sync' 114 | address = Column(BINARY(32), primary_key=True) 115 | height = Column(Integer, nullable=False, server_default='0') 116 | 117 | 118 | def get_assets(db: Database, asset_type: Optional[str]=None, asset_id: Optional[bytes]=None, p2_puzzle_hash: Optional[bytes]=None, 119 | nft_did_id: Optional[bytes]=None, include_spent_coins=False, 120 | start_height: Optional[int]=None, offset: Optional[int]=None, limit: Optional[int]=None) -> List[Asset]: 121 | query = select(Asset).order_by(Asset.confirmed_height.asc()) 122 | if asset_type: 123 | query = query.where(Asset.asset_type == asset_type) 124 | if p2_puzzle_hash: 125 | query = query.where(Asset.p2_puzzle_hash == p2_puzzle_hash) 126 | if nft_did_id: 127 | query = query.where(Asset.nft_did_id == nft_did_id) 128 | if not include_spent_coins: 129 | query = query.where(Asset.spent_height == 0) 130 | if asset_id: 131 | query = query.where(Asset.asset_id == asset_id) 132 | if start_height: 133 | query = query.where(Asset.confirmed_height > start_height) 134 | if offset: 135 | query = query.offset(offset) 136 | if limit: 137 | query = query.limit(limit) 138 | return db.fetch_all(query) 139 | 140 | 141 | async def update_asset_coin_spent_height(db: Database, coin_ids: List[bytes], spent_height: int): 142 | 143 | chunk_size = 200 144 | async with db.transaction(): 145 | for i in range(0, len(coin_ids), chunk_size): 146 | chunk_ids = coin_ids[i: i+chunk_size] 147 | sql = update(Asset)\ 148 | .where(Asset.coin_id.in_(chunk_ids))\ 149 | .values(spent_height=spent_height) 150 | await db.execute(sql) 151 | 152 | 153 | async def save_asset(db: Database, asset: Asset): 154 | async with db.transaction(): 155 | return await db.execute(insert(Asset).values(asset.to_dict()).prefix_with('OR REPLACE')) 156 | 157 | 158 | async def get_unspent_asset_coin_ids(db: Database, p2_puzzle_hash: Optional[bytes]=None): 159 | query = select(Asset.coin_id).where(Asset.spent_height == 0) 160 | if p2_puzzle_hash: 161 | query = query.where(Asset.p2_puzzle_hash == p2_puzzle_hash) 162 | coin_ids = [] 163 | for row in await db.fetch_all(query): 164 | coin_ids.append(row.coin_id) 165 | return coin_ids 166 | 167 | 168 | async def get_nft_metadata_by_hash(db: Database, hash: bytes): 169 | query = select(NftMetadata).where(NftMetadata.hash == hash) 170 | return await db.fetch_val(query) 171 | 172 | 173 | async def save_metadata(db: Database, metadata: NftMetadata): 174 | async with db.transaction(): 175 | return await db.execute(insert(NftMetadata).values(metadata.to_dict()).prefix_with('OR REPLACE')) 176 | 177 | 178 | async def get_metadata_by_hashes(db: Database, hashes: List[bytes]): 179 | query = select(NftMetadata).where(NftMetadata.hash.in_(hashes)) 180 | return await db.fetch_all(query) 181 | 182 | 183 | async def get_singelton_spend_by_id(db: Database, singleton_id): 184 | query = select(SingletonSpend).where(SingletonSpend.singleton_id == singleton_id) 185 | return await db.fetch_one(query) 186 | 187 | 188 | async def delete_singleton_spend_by_id(db: Database, singleton_id): 189 | query = delete(SingletonSpend).where(SingletonSpend.singleton_id == singleton_id) 190 | async with db.transaction(): 191 | return await db.execute(query) 192 | 193 | 194 | async def save_singleton_spend(db: Database, item: SingletonSpend): 195 | async with db.transaction(): 196 | return await db.execute(insert(SingletonSpend).values(item.to_dict()).prefix_with('OR REPLACE')) 197 | 198 | 199 | 200 | async def get_latest_tx_block_number(db: Database): 201 | query = select(Block.height).where(Block.is_tx == True).order_by(Block.height.desc()).limit(1) 202 | return await db.fetch_val(query) 203 | 204 | 205 | async def get_latest_blocks(db: Database, num): 206 | query = select(Block).order_by(Block.height.desc()).limit(num) 207 | return await db.fetch_all(query) 208 | 209 | 210 | async def save_block(db: Database, block: Block): 211 | async with db.transaction(): 212 | return await db.execute(insert(Block).values(block.to_dict())) 213 | 214 | async def get_block_by_height(db: Database, height): 215 | query = select(Block).where(Block.height == height) 216 | return await db.fetch_one(query) 217 | 218 | 219 | async def delete_block_after_height(db: Database, height): 220 | query = delete(Block).where(Block.height > height) 221 | async with db.transaction(): 222 | return await db.execute(query) 223 | 224 | 225 | async def save_address_sync_height(db: Database, address: bytes, height: int): 226 | async with db.transaction(): 227 | return await db.execute(insert(AddressSync).values(address=address, height=height).prefix_with('OR REPLACE')) 228 | 229 | 230 | async def get_address_sync_height(db: Database, address: bytes): 231 | query = select(AddressSync).where(AddressSync.address == address) 232 | return await db.fetch_one(query) 233 | 234 | 235 | async def reorg(db: Database, block_height: int): 236 | # block_height is correct, +1 is error 237 | async with db.transaction(): 238 | # delete confiremd_height > block_height 239 | await db.execute(delete(Asset).where(Asset.confirmed_height > block_height)) 240 | 241 | # make spent_height = 0 where spent_height > block_height 242 | await db.execute(update(Asset).where(Asset.spent_height > block_height).values(spent_height=0)) 243 | 244 | # update address sync height 245 | await db.execute(update(AddressSync).where(AddressSync.height > block_height).values(height=block_height)) 246 | 247 | # delete block > block_height 248 | await db.execute(delete(Block).where(Block.height > block_height)) -------------------------------------------------------------------------------- /openapi/did.py: -------------------------------------------------------------------------------- 1 | import json 2 | import asyncio 3 | from aiocache import caches 4 | from typing import Dict 5 | from .types import Program, Coin, LineageProof 6 | from .puzzles import ( 7 | SINGLETON_TOP_LAYER_MOD, SINGLETON_TOP_LAYER_MOD_HASH, 8 | SINGLETON_LAUNCHER_MOD_HASH, 9 | DID_INNERPUZ_MOD 10 | ) 11 | from .utils import to_hex 12 | 13 | 14 | def match_did_puzzle(puzzle: Program): 15 | try: 16 | mod, curried_args = puzzle.uncurry() 17 | if mod == SINGLETON_TOP_LAYER_MOD: 18 | mod, curried_args = curried_args.rest().first().uncurry() 19 | if mod == DID_INNERPUZ_MOD: 20 | return True, curried_args.as_iter() 21 | except Exception: 22 | return False, iter(()) 23 | return False, iter(()) 24 | 25 | 26 | def get_did_inner_puzzle_hash(address: bytes, recovery_list_hash: bytes, num_verification: int, singleton_struct, metadata): 27 | return DID_INNERPUZ_MOD.curry(address, recovery_list_hash, num_verification, singleton_struct, metadata).get_tree_hash(address) 28 | 29 | 30 | def to_full_pzh(inner_puzzle_hash: bytes, launcher_id: bytes): 31 | singleton_struct = Program.to((SINGLETON_TOP_LAYER_MOD_HASH, (launcher_id, SINGLETON_LAUNCHER_MOD_HASH))) 32 | return SINGLETON_TOP_LAYER_MOD.curry(singleton_struct, inner_puzzle_hash).get_tree_hash(inner_puzzle_hash) 33 | 34 | 35 | def program_to_metadata(program: Program) -> Dict: 36 | """ 37 | Convert a program to a metadata dict 38 | :param program: Chialisp program contains the metadata 39 | :return: Metadata dict 40 | """ 41 | metadata = {} 42 | for key, val in program.as_python(): 43 | metadata[str(key, "utf-8")] = str(val, "utf-8") 44 | return metadata 45 | 46 | 47 | def get_did_info_from_coin_spend(coin: Coin, parent_cs: dict, address: bytes): 48 | parent_coin = Coin.from_json_dict(parent_cs['coin']) 49 | puzzle = Program.fromhex(parent_cs['puzzle_reveal']) 50 | 51 | try: 52 | mod, curried_args_pz = puzzle.uncurry() 53 | if mod != SINGLETON_TOP_LAYER_MOD: 54 | return 55 | singleton_inner_puzzle = curried_args_pz.rest().first() 56 | mod, curried_args_pz = singleton_inner_puzzle.uncurry() 57 | if mod != DID_INNERPUZ_MOD: 58 | return 59 | curried_args = curried_args_pz.as_iter() 60 | except Exception: 61 | return 62 | 63 | solution = Program.fromhex(parent_cs['solution']) 64 | 65 | p2_puzzle, recovery_list_hash, num_verification, singleton_struct, metadata = curried_args 66 | recovery_list_hash = recovery_list_hash.as_atom() 67 | 68 | p2_puzzle_hash = p2_puzzle.get_tree_hash() 69 | 70 | launcher_id = singleton_struct.rest().first().as_atom() 71 | 72 | full_puzzle_hash = to_full_pzh(get_did_inner_puzzle_hash(address, recovery_list_hash, num_verification, singleton_struct, metadata), bytes(launcher_id)) 73 | 74 | 75 | if coin.puzzle_hash != full_puzzle_hash: 76 | recovery_list_hash = Program.to([]).get_tree_hash() 77 | num_verification = 0 78 | full_empty_puzzle_hash = to_full_pzh(get_did_inner_puzzle_hash(address, recovery_list_hash, num_verification, singleton_struct, metadata), launcher_id) 79 | if coin.puzzle_hash != full_empty_puzzle_hash: 80 | # the recovery list was reset by the previous owner 81 | return None 82 | 83 | inner_solution = solution.rest().rest().first() 84 | recovery_list = [] 85 | if recovery_list_hash != Program.to([]).get_tree_hash(): 86 | try: 87 | for did in inner_solution.rest().rest().rest().rest().rest().as_python(): 88 | recovery_list.append(did[0]) 89 | except: 90 | pass 91 | 92 | return { 93 | 'did_id': launcher_id, 94 | 'coin': coin, 95 | 'p2_puzzle_hash': p2_puzzle_hash, 96 | 'recovery_list_hash': recovery_list_hash, 97 | 'recovery_list': recovery_list, 98 | 'num_verification': num_verification.as_int(), 99 | 'metadata': metadata, 100 | 'lineage_proof': LineageProof(parent_coin.parent_coin_info, singleton_inner_puzzle.get_tree_hash(), parent_coin.amount) 101 | } 102 | -------------------------------------------------------------------------------- /openapi/nft.py: -------------------------------------------------------------------------------- 1 | """ 2 | ref https://github.com/Chia-Network/chia-blockchain/blob/main_dids/chia/wallet/nft_wallet/uncurry_nft.py 3 | """ 4 | import logging 5 | from typing import Type, TypeVar, Any, List, Dict, Optional, Tuple 6 | import dataclasses 7 | from dataclasses import dataclass 8 | from clvm.casts import int_from_bytes 9 | 10 | from .puzzles import SINGLETON_TOP_LAYER_MOD, NFT_STATE_LAYER_MOD, NFT_OWNERSHIP_LAYER 11 | from .types import Coin, Program, LineageProof 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | _T_UncurriedNFT = TypeVar("_T_UncurriedNFT", bound="UncurriedNFT") 18 | 19 | 20 | NFT_MOD = NFT_STATE_LAYER_MOD 21 | bytes32 = bytes 22 | uint16 = int 23 | 24 | 25 | @dataclass(frozen=True) 26 | class UncurriedNFT: 27 | """ 28 | A simple solution for uncurry NFT puzzle. 29 | Initial the class with a full NFT puzzle, it will do a deep uncurry. 30 | This is the only place you need to change after modified the Chialisp curried parameters. 31 | """ 32 | 33 | nft_mod_hash: bytes32 34 | """NFT module hash""" 35 | 36 | nft_state_layer: Program 37 | """NFT state layer puzzle""" 38 | 39 | singleton_struct: Program 40 | """ 41 | Singleton struct 42 | [singleton_mod_hash, singleton_launcher_id, launcher_puzhash] 43 | """ 44 | singleton_mod_hash: Program 45 | singleton_launcher_id: bytes32 46 | launcher_puzhash: Program 47 | 48 | metadata_updater_hash: Program 49 | """Metadata updater puzzle hash""" 50 | 51 | metadata: Program 52 | """ 53 | NFT metadata 54 | [("u", data_uris), ("h", data_hash)] 55 | """ 56 | data_uris: Program 57 | data_hash: Program 58 | meta_uris: Program 59 | meta_hash: Program 60 | license_uris: Program 61 | license_hash: Program 62 | series_number: Program 63 | series_total: Program 64 | 65 | inner_puzzle: Program 66 | """NFT state layer inner puzzle""" 67 | 68 | p2_puzzle: Program 69 | """p2 puzzle of the owner, either for ownership layer or standard""" 70 | 71 | # ownership layer fields 72 | owner_did: Optional[bytes32] 73 | """Owner's DID""" 74 | 75 | supports_did: bool 76 | """If the inner puzzle support the DID""" 77 | 78 | nft_inner_puzzle_hash: Optional[bytes32] 79 | """Puzzle hash of the ownership layer inner puzzle """ 80 | 81 | transfer_program: Optional[Program] 82 | """Puzzle hash of the transfer program""" 83 | 84 | transfer_program_curry_params: Optional[Program] 85 | """ 86 | Curried parameters of the transfer program 87 | [royalty_address, trade_price_percentage, settlement_mod_hash, cat_mod_hash] 88 | """ 89 | royalty_address: Optional[bytes32] 90 | trade_price_percentage: Optional[uint16] 91 | 92 | @classmethod 93 | def uncurry(cls, puzzle: Program) -> "UncurriedNFT": 94 | """ 95 | Try to uncurry a NFT puzzle 96 | :param cls UncurriedNFT class 97 | :param puzzle: Puzzle program 98 | :return Uncurried NFT 99 | """ 100 | mod, curried_args = puzzle.uncurry() 101 | if mod != SINGLETON_TOP_LAYER_MOD: 102 | raise ValueError(f"Cannot uncurry NFT puzzle, failed on singleton top layer {mod.get_tree_hash().hex()}") 103 | try: 104 | (singleton_struct, nft_state_layer) = curried_args.as_iter() 105 | singleton_mod_hash = singleton_struct.first() 106 | singleton_launcher_id = singleton_struct.rest().first() 107 | launcher_puzhash = singleton_struct.rest().rest() 108 | except ValueError as e: 109 | raise ValueError(f"Cannot uncurry singleton top layer: Args {curried_args}") from e 110 | 111 | mod, curried_args = curried_args.rest().first().uncurry() 112 | if mod != NFT_MOD: 113 | raise ValueError(f"Cannot uncurry NFT puzzle, failed on NFT state layer") 114 | try: 115 | # Set nft parameters 116 | nft_mod_hash, metadata, metadata_updater_hash, inner_puzzle = curried_args.as_iter() 117 | data_uris = Program.to([]) 118 | data_hash = Program.to(0) 119 | meta_uris = Program.to([]) 120 | meta_hash = Program.to(0) 121 | license_uris = Program.to([]) 122 | license_hash = Program.to(0) 123 | series_number = Program.to(1) 124 | series_total = Program.to(1) 125 | # Set metadata 126 | for kv_pair in metadata.as_iter(): 127 | if kv_pair.first().as_atom() == b"u": 128 | data_uris = kv_pair.rest() 129 | if kv_pair.first().as_atom() == b"h": 130 | data_hash = kv_pair.rest() 131 | if kv_pair.first().as_atom() == b"mu": 132 | meta_uris = kv_pair.rest() 133 | if kv_pair.first().as_atom() == b"mh": 134 | meta_hash = kv_pair.rest() 135 | if kv_pair.first().as_atom() == b"lu": 136 | license_uris = kv_pair.rest() 137 | if kv_pair.first().as_atom() == b"lh": 138 | license_hash = kv_pair.rest() 139 | if kv_pair.first().as_atom() == b"sn": 140 | series_number = kv_pair.rest() 141 | if kv_pair.first().as_atom() == b"st": 142 | series_total = kv_pair.rest() 143 | current_did = None 144 | transfer_program = None 145 | transfer_program_args = None 146 | royalty_address = None 147 | royalty_percentage = None 148 | nft_inner_puzzle_mod = None 149 | mod, ol_args = inner_puzzle.uncurry() 150 | supports_did = False 151 | if mod == NFT_OWNERSHIP_LAYER: 152 | supports_did = True 153 | _, current_did, transfer_program, p2_puzzle = ol_args.as_iter() 154 | transfer_program_mod, transfer_program_args = transfer_program.uncurry() 155 | _, royalty_address_p, royalty_percentage = transfer_program_args.as_iter() 156 | royalty_percentage = uint16(royalty_percentage.as_int()) 157 | royalty_address = royalty_address_p.atom 158 | current_did = current_did.atom 159 | if current_did == b"": 160 | # For unassigned NFT, set owner DID to None 161 | current_did = None 162 | else: 163 | p2_puzzle = inner_puzzle 164 | except Exception as e: 165 | raise ValueError(f"Cannot uncurry NFT state layer: Args {curried_args}") from e 166 | return cls( 167 | nft_mod_hash=nft_mod_hash, 168 | nft_state_layer=nft_state_layer, 169 | singleton_struct=singleton_struct, 170 | singleton_mod_hash=singleton_mod_hash, 171 | singleton_launcher_id=singleton_launcher_id.atom, 172 | launcher_puzhash=launcher_puzhash, 173 | metadata=metadata, 174 | data_uris=data_uris, 175 | data_hash=data_hash, 176 | p2_puzzle=p2_puzzle, 177 | metadata_updater_hash=metadata_updater_hash, 178 | meta_uris=meta_uris, 179 | meta_hash=meta_hash, 180 | license_uris=license_uris, 181 | license_hash=license_hash, 182 | series_number=series_number, 183 | series_total=series_total, 184 | inner_puzzle=inner_puzzle, 185 | owner_did=current_did, 186 | supports_did=supports_did, 187 | transfer_program=transfer_program, 188 | transfer_program_curry_params=transfer_program_args, 189 | royalty_address=royalty_address, 190 | trade_price_percentage=royalty_percentage, 191 | nft_inner_puzzle_hash=nft_inner_puzzle_mod, 192 | ) 193 | 194 | def get_innermost_solution(self, solution: Program) -> Program: 195 | state_layer_inner_solution: Program = solution.at("rrff") 196 | if self.supports_did: 197 | return state_layer_inner_solution.first() # type: ignore 198 | else: 199 | return state_layer_inner_solution 200 | 201 | 202 | def metadata_to_program(metadata: Dict[bytes, Any]) -> Program: 203 | """ 204 | Convert the metadata dict to a Chialisp program 205 | :param metadata: User defined metadata 206 | :return: Chialisp program 207 | """ 208 | kv_list = [] 209 | for key, value in metadata.items(): 210 | kv_list.append((key, value)) 211 | program: Program = Program.to(kv_list) 212 | return program 213 | 214 | 215 | def program_to_metadata(program: Program) -> Dict[bytes, Any]: 216 | """ 217 | Convert a program to a metadata dict 218 | :param program: Chialisp program contains the metadata 219 | :return: Metadata dict 220 | """ 221 | metadata = {} 222 | for kv_pair in program.as_iter(): 223 | metadata[kv_pair.first().as_atom()] = kv_pair.rest().as_python() 224 | return metadata 225 | 226 | 227 | def prepend_value(key: bytes, value: Program, metadata: Dict[bytes, Any]) -> None: 228 | """ 229 | Prepend a value to a list in the metadata 230 | :param key: Key of the field 231 | :param value: Value want to add 232 | :param metadata: Metadata 233 | :return: 234 | """ 235 | 236 | if value != Program.to(0): 237 | if metadata[key] == b"": 238 | metadata[key] = [value.as_python()] 239 | else: 240 | metadata[key].insert(0, value.as_python()) 241 | 242 | 243 | def update_metadata(metadata: Program, update_condition: Program) -> Program: 244 | """ 245 | Apply conditions of metadata updater to the previous metadata 246 | :param metadata: Previous metadata 247 | :param update_condition: Update metadata conditions 248 | :return: Updated metadata 249 | """ 250 | new_metadata: Dict[bytes, Any] = program_to_metadata(metadata) 251 | uri: Program = update_condition.rest().rest().first() 252 | prepend_value(uri.first().as_python(), uri.rest(), new_metadata) 253 | return metadata_to_program(new_metadata) 254 | 255 | 256 | def get_metadata_and_phs(unft: UncurriedNFT, solution: Program) -> Tuple[Program, bytes32]: 257 | conditions = unft.p2_puzzle.run(unft.get_innermost_solution(solution)) 258 | metadata = unft.metadata 259 | puzhash_for_derivation: Optional[bytes32] = None 260 | for condition in conditions.as_iter(): 261 | if condition.list_len() < 2: 262 | # invalid condition 263 | continue 264 | condition_code = condition.first().as_int() 265 | if condition_code == -24: 266 | # metadata update 267 | metadata = update_metadata(metadata, condition) 268 | metadata = Program.to(metadata) 269 | elif condition_code == 51 and condition.rest().rest().first().as_int() == 1: 270 | # destination puzhash 271 | if puzhash_for_derivation is not None: 272 | # ignore duplicated create coin conditions 273 | continue 274 | puzhash_for_derivation = condition.rest().first().as_atom() 275 | assert puzhash_for_derivation 276 | return metadata, puzhash_for_derivation 277 | 278 | 279 | def get_new_owner_did(unft: UncurriedNFT, solution: Program) -> Optional[bytes32]: 280 | conditions = unft.p2_puzzle.run(unft.get_innermost_solution(solution)) 281 | new_did_id = None 282 | for condition in conditions.as_iter(): 283 | if condition.first().as_int() == -10: 284 | # this is the change owner magic condition 285 | new_did_id = condition.at("rf").atom 286 | return new_did_id 287 | 288 | 289 | def get_nft_info_from_coin_spend(nft_coin: Coin, parent_cs: dict, address: bytes): 290 | puzzle = Program.fromhex(parent_cs['puzzle_reveal']) 291 | try: 292 | uncurried_nft = UncurriedNFT.uncurry(puzzle) 293 | except Exception as e: 294 | logger.debug('uncurry nft puzzle: %r', e) 295 | return 296 | solution = Program.fromhex(parent_cs['solution']) 297 | 298 | # DID ID determines which NFT wallet should process the NFT 299 | new_did_id = None 300 | old_did_id = None 301 | # P2 puzzle hash determines if we should ignore the NFT 302 | old_p2_puzhash = uncurried_nft.p2_puzzle.get_tree_hash() 303 | metadata, new_p2_puzhash = get_metadata_and_phs( 304 | uncurried_nft, 305 | solution, 306 | ) 307 | if uncurried_nft.supports_did: 308 | new_did_id = get_new_owner_did(uncurried_nft, solution) 309 | old_did_id = uncurried_nft.owner_did 310 | if new_did_id is None: 311 | new_did_id = old_did_id 312 | if new_did_id == b"": 313 | new_did_id = None 314 | 315 | if new_p2_puzhash != address: 316 | return 317 | parent_coin = Coin.from_json_dict(parent_cs['coin']) 318 | lineage_proof = LineageProof(parent_coin.parent_coin_info, uncurried_nft.nft_state_layer.get_tree_hash(), parent_coin.amount) 319 | return (uncurried_nft, new_did_id, new_p2_puzhash, lineage_proof) 320 | 321 | 322 | def get_metadata_json(metadata: Program) -> dict: 323 | info = {} 324 | for kv_pair in metadata.as_iter(): 325 | key = kv_pair.first().as_atom() 326 | v = kv_pair.rest() 327 | try: 328 | if key == b'u': 329 | info['data_uri'] = v.first().as_atom().decode("utf-8") 330 | elif key == 'h': 331 | info['data_hash'] = v.as_atom().hex() 332 | elif key == 'mu': 333 | info['metadata_uri'] = v.first().as_atom().decode("utf-8") 334 | elif key == 'mh': 335 | info['metadata_hash'] = v.as_atom().hex() 336 | elif key == 'lu': 337 | info['license_uri'] = v.first().as_atom().decode("utf-8") 338 | elif key == 'lh': 339 | info['license_hash'] = v.as_atom().hex() 340 | elif key == 'st': 341 | info['series_total'] = v.as_int() 342 | elif key == 'sn': 343 | info['series_number'] = v.as_int() 344 | except: 345 | pass 346 | 347 | return info 348 | -------------------------------------------------------------------------------- /openapi/puzzles.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import os 4 | import pathlib 5 | from typing import Union 6 | 7 | from clvm_tools.clvmc import compile_clvm 8 | from .types import Program 9 | 10 | 11 | # Helper function that allows for packages to be decribed as a string, Path, or package string 12 | def string_to_path(pkg_or_path: Union[str, pathlib.Path]) -> pathlib.Path: 13 | as_path = pathlib.Path(pkg_or_path) 14 | if as_path.exists(): 15 | if as_path.is_dir(): 16 | return as_path 17 | else: 18 | raise ValueError("Cannot search for includes or CLVM in a file") 19 | elif isinstance(pkg_or_path, pathlib.Path): 20 | raise ModuleNotFoundError(f"Cannot find a path matching {pkg_or_path}") 21 | else: 22 | path = importlib.import_module(pkg_or_path).__file__ 23 | if path is None: 24 | raise ModuleNotFoundError(f"Cannot find a package at {pkg_or_path}") 25 | else: 26 | return pathlib.Path(path).parent 27 | 28 | 29 | def load_serialized_clvm( 30 | clvm_filename, 31 | package_or_requirement=None, 32 | search_paths=["openapi.clsp.include"], 33 | ) -> str: 34 | """ 35 | This function takes a chialisp file in the given package and compiles it to a 36 | .hex file if the .hex file is missing or older than the chialisp file, then 37 | returns the contents of the .hex file as a `SerializedProgram`. 38 | clvm_filename: file name 39 | package_or_requirement: Defaults to the module from which the function was called 40 | search_paths: A list of paths to search for `(include` files. Defaults to a standard chia-blockchain module. 41 | """ 42 | if package_or_requirement is None: 43 | module_name = inspect.getmodule(inspect.stack()[1][0]) 44 | if module_name is not None: 45 | package_or_requirement = module_name.__name__ 46 | else: 47 | raise ModuleNotFoundError("Couldn't find the module that load_clvm was called from") 48 | package_or_requirement = string_to_path(package_or_requirement) 49 | 50 | path_list = [str(string_to_path(search_path)) for search_path in search_paths] 51 | 52 | full_path = package_or_requirement.joinpath(clvm_filename) 53 | hex_filename = package_or_requirement.joinpath(f"{clvm_filename}.hex") 54 | 55 | if full_path.exists(): 56 | compile_clvm( 57 | str(full_path), 58 | str(hex_filename), 59 | search_paths=[str(full_path.parent), *path_list], 60 | ) 61 | 62 | clvm_hex = "".join(open(hex_filename, "r").read().split()) # Eliminate whitespace 63 | return clvm_hex 64 | 65 | 66 | def load_clvm(clvm_filename, package_or_requirement="openapi.clsp", search_paths=["openapi.clsp.include"]) -> Program: 67 | if package_or_requirement is None: 68 | module_name = inspect.getmodule(inspect.stack()[1][0]) 69 | if module_name is not None: 70 | package_or_requirement = module_name.__name__ 71 | else: 72 | raise ModuleNotFoundError("Couldn't find the module that load_clvm was called from") 73 | return Program.fromhex(load_serialized_clvm( 74 | clvm_filename, package_or_requirement=package_or_requirement, search_paths=search_paths 75 | ) 76 | ) 77 | 78 | 79 | STANDARD_PUZZLE_MOD = load_clvm("p2_delegated_puzzle_or_hidden_puzzle.clvm") 80 | CAT_MOD = load_clvm("cat.clvm") 81 | OFFER_MOD = load_clvm("settlement_payments.clvm") 82 | SINGLETON_TOP_LAYER_MOD = load_clvm("singleton_top_layer_v1_1.clvm") 83 | SINGLETON_TOP_LAYER_MOD_HASH = SINGLETON_TOP_LAYER_MOD.get_tree_hash() 84 | SINGLETON_LAUNCHER_MOD = load_clvm("singleton_launcher.clvm") 85 | SINGLETON_LAUNCHER_MOD_HASH = SINGLETON_LAUNCHER_MOD.get_tree_hash() 86 | NFT_STATE_LAYER_MOD = load_clvm("nft_state_layer.clvm") 87 | NFT_METADATA_UPDATER = load_clvm("nft_metadata_updater_default.clvm") 88 | NFT_OWNERSHIP_LAYER = load_clvm("nft_ownership_layer.clvm") 89 | NFT_TRANSFER_PROGRAM_DEFAULT = load_clvm("nft_ownership_transfer_program_one_way_claim_with_royalties.clvm") 90 | DID_INNERPUZ_MOD = load_clvm("did_innerpuz.clvm") -------------------------------------------------------------------------------- /openapi/rpc_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | from urllib.parse import urljoin 4 | import ssl 5 | from ssl import SSLContext 6 | from typing import Dict, List, Optional, Any 7 | import yaml 8 | import aiohttp 9 | from .utils.singleflight import SingleFlight 10 | 11 | bytes32 = bytes 12 | 13 | 14 | def ssl_context_for_client( 15 | ca_cert: Path, 16 | ca_key: Path, 17 | private_cert_path: Path, 18 | private_key_path: Path, 19 | 20 | ): 21 | 22 | ssl_context = ssl._create_unverified_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=str(ca_cert)) 23 | ssl_context.check_hostname = False 24 | ssl_context.load_cert_chain(certfile=str(private_cert_path), keyfile=str(private_key_path)) 25 | ssl_context.verify_mode = ssl.CERT_REQUIRED 26 | return ssl_context 27 | 28 | 29 | class FullNodeRpcClient: 30 | url: str 31 | session: aiohttp.ClientSession 32 | closing_task: Optional[asyncio.Task] 33 | ssl_context: Optional[SSLContext] 34 | 35 | def __init__(self): 36 | self.sf = SingleFlight() 37 | 38 | @classmethod 39 | async def create_by_chia_root_path(cls, chia_root_path): 40 | self = cls() 41 | root_path = Path(chia_root_path) 42 | config_path = Path(chia_root_path) / "config" / "config.yaml" 43 | config = yaml.safe_load(config_path.open("r", encoding="utf-8")) 44 | self.url = f"https://{config['self_hostname']}:{config['full_node']['rpc_port']}" 45 | ca_cert_path = root_path / config["private_ssl_ca"]["crt"] 46 | ca_key_path = root_path / config["private_ssl_ca"]["key"] 47 | private_cert_path = root_path / config["daemon_ssl"]["private_crt"] 48 | private_key_path = root_path / config["daemon_ssl"]["private_key"] 49 | self.session = aiohttp.ClientSession() 50 | self.ssl_context = ssl_context_for_client(ca_cert_path, ca_key_path, private_cert_path, private_key_path) 51 | self.closing_task = None 52 | return self 53 | 54 | @classmethod 55 | async def create_by_proxy_url(cls, proxy_url): 56 | self = cls() 57 | self.url = proxy_url 58 | self.session = aiohttp.ClientSession() 59 | self.ssl_context = None 60 | self.closing_task = None 61 | return self 62 | 63 | async def raw_fetch(self, path, request_json): 64 | async with self.session.post(urljoin(self.url, path), json=request_json, ssl_context=self.ssl_context) as response: 65 | res_json = await response.json() 66 | return res_json 67 | 68 | async def fetch(self, path, request_json) -> Any: 69 | async with self.session.post(urljoin(self.url, path), json=request_json, ssl_context=self.ssl_context) as response: 70 | response.raise_for_status() 71 | res_json = await response.json() 72 | if not res_json["success"]: 73 | raise ValueError(res_json) 74 | return res_json 75 | 76 | def close(self): 77 | self.closing_task = asyncio.create_task(self.session.close()) 78 | 79 | async def await_closed(self): 80 | if self.closing_task is not None: 81 | await self.closing_task 82 | 83 | async def get_network_info(self): 84 | return await self.fetch("get_network_info", {}) 85 | 86 | async def get_blockchain_state(self): 87 | resp = await self.fetch("get_blockchain_state", {}) 88 | return resp['blockchain_state'] 89 | 90 | async def get_block_number(self): 91 | resp = await self.sf.do('block_number', lambda: self.get_blockchain_state()) 92 | return resp['peak']['height'] 93 | 94 | async def get_coin_records_by_puzzle_hash( 95 | self, 96 | puzzle_hash: bytes32, 97 | include_spent_coins: bool = True, 98 | start_height: Optional[int] = None, 99 | end_height: Optional[int] = None, 100 | ) -> List: 101 | d = {"puzzle_hash": puzzle_hash.hex(), "include_spent_coins": include_spent_coins} 102 | if start_height is not None: 103 | d["start_height"] = start_height 104 | if end_height is not None: 105 | d["end_height"] = end_height 106 | 107 | response = await self.fetch("get_coin_records_by_puzzle_hash", d) 108 | return response['coin_records'] 109 | 110 | async def get_coin_records_by_puzzle_hashes( 111 | self, 112 | puzzle_hashes: List[bytes32], 113 | include_spent_coins: bool = True, 114 | start_height: Optional[int] = None, 115 | end_height: Optional[int] = None, 116 | ) -> List: 117 | puzzle_hashes_hex = [ph.hex() for ph in puzzle_hashes] 118 | d = {"puzzle_hashes": puzzle_hashes_hex, "include_spent_coins": include_spent_coins} 119 | if start_height is not None: 120 | d["start_height"] = start_height 121 | if end_height is not None: 122 | d["end_height"] = end_height 123 | 124 | response = await self.fetch("get_coin_records_by_puzzle_hashes", d) 125 | return response["coin_records"] 126 | 127 | async def push_tx(self, spend_bundle: dict): 128 | return await self.fetch("push_tx", {"spend_bundle": spend_bundle}) 129 | 130 | async def get_coin_records_by_hint( 131 | self, 132 | hint: bytes32, 133 | include_spent_coins: bool = True, 134 | start_height: Optional[int] = None, 135 | end_height: Optional[int] = None, 136 | ): 137 | request = { 138 | 'hint': hint.hex(), 139 | 'include_spent_coins': include_spent_coins, 140 | } 141 | if start_height: 142 | request['start_height'] = start_height 143 | if end_height: 144 | request['end_height'] = end_height 145 | response = await self.fetch("get_coin_records_by_hint", request) 146 | return response["coin_records"] 147 | 148 | async def get_coin_records_by_names( 149 | self, 150 | names: List[bytes32], 151 | include_spent_coins: bool = True, 152 | start_height: Optional[int] = None, 153 | end_height: Optional[int] = None, 154 | ) -> List: 155 | names_hex = [name.hex() for name in names] 156 | d = {"names": names_hex, "include_spent_coins": include_spent_coins} 157 | if start_height is not None: 158 | d["start_height"] = start_height 159 | if end_height is not None: 160 | d["end_height"] = end_height 161 | 162 | response = await self.fetch("get_coin_records_by_names", d) 163 | return response["coin_records"] 164 | 165 | 166 | async def get_coin_record_by_name(self, name: bytes32): 167 | response = await self.fetch("get_coin_record_by_name", {"name": name.hex()}) 168 | return response['coin_record'] 169 | 170 | 171 | async def get_puzzle_and_solution(self, coin_id: bytes32, height: int): 172 | response = await self.fetch("get_puzzle_and_solution", {"coin_id": coin_id.hex(), "height": height}) 173 | return response['coin_solution'] 174 | 175 | async def get_coin_records_by_parent_ids(self, parent_ids: List[bytes32], include_spent_coins: bool = True, 176 | start_height: Optional[int] = None, 177 | end_height: Optional[int] = None): 178 | parent_ids_hex = [pid.hex() for pid in parent_ids] 179 | d = {"parent_ids": parent_ids_hex, "include_spent_coins": include_spent_coins} 180 | if start_height is not None: 181 | d["start_height"] = start_height 182 | if end_height is not None: 183 | d["end_height"] = end_height 184 | response = await self.fetch("get_coin_records_by_parent_ids", d) 185 | return response['coin_records'] 186 | 187 | async def get_block_record_by_height(self, height: int): 188 | response = await self.fetch("get_block_record_by_height", {"height": height}) 189 | return response['block_record'] 190 | 191 | async def get_additions_and_removals(self, header_hash: bytes32): 192 | response = await self.fetch("get_additions_and_removals", {"header_hash": header_hash.hex()}) 193 | return response['additions'], response['removals'] 194 | 195 | async def get_fee_estimate(self, target_times: Optional[List[int]], cost: Optional[int]): 196 | response = await self.fetch("get_fee_estimate", {"target_times": target_times, "cost": cost}) 197 | return response -------------------------------------------------------------------------------- /openapi/sync.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | import json 4 | import aiohttp 5 | from aiocache import caches 6 | from .utils import hexstr_to_bytes, coin_name, to_hex, sha256 7 | from .types import Coin 8 | from .db import ( 9 | Asset, NftMetadata, SingletonSpend, 10 | get_db, save_asset, get_unspent_asset_coin_ids, 11 | update_asset_coin_spent_height, get_nft_metadata_by_hash, save_metadata, 12 | get_singelton_spend_by_id, delete_singleton_spend_by_id, save_singleton_spend, 13 | get_address_sync_height, save_address_sync_height, get_latest_tx_block_number, 14 | ) 15 | 16 | from .did import get_did_info_from_coin_spend 17 | from .nft import get_nft_info_from_coin_spend 18 | from .rpc_client import FullNodeRpcClient 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | 24 | async def fetch_nft_metadata(db, url: str, hash: bytes): 25 | row = await get_nft_metadata_by_hash(db, hash) 26 | if row: 27 | return 28 | async with aiohttp.ClientSession() as session: 29 | async with session.get(url, timeout=60) as response: 30 | response.raise_for_status() 31 | binary = await response.read() 32 | binary_sha256 = sha256(binary) 33 | if binary_sha256 != hash: 34 | raise ValueError("nft metadta hash mismatch") 35 | data = json.loads(binary) 36 | await save_metadata(db, NftMetadata( 37 | hash=binary_sha256, 38 | format=data.get('format'), 39 | name=data.get('name'), 40 | collection_id=data.get('collection', {}).get('id'), 41 | collection_name=data.get('collection', {}).get('name'), 42 | full_data=data 43 | )) 44 | logger.debug('fetch metadata: %s success', hash.hex()) 45 | 46 | 47 | async def handle_coin(address, coin_record, parent_coin_spend, db): 48 | coin = Coin.from_json_dict(coin_record['coin']) 49 | logger.debug('handle coin: %s', coin.name().hex()) 50 | did_info = get_did_info_from_coin_spend(coin, parent_coin_spend, address) 51 | if did_info is not None: 52 | curried_params = { 53 | 'recovery_list_hash': to_hex(did_info['recovery_list_hash']), 54 | 'recovery_list': [to_hex(r) for r in did_info['recovery_list']], 55 | 'num_verification': did_info['num_verification'], 56 | 'metadata': to_hex(bytes(did_info['metadata'])) 57 | } 58 | asset = Asset( 59 | coin_id=coin.name(), 60 | asset_type='did', 61 | asset_id=did_info['did_id'], 62 | confirmed_height=coin_record['confirmed_block_index'], 63 | spent_height=0, 64 | coin=coin.to_json_dict(), 65 | lineage_proof=did_info['lineage_proof'].to_json_dict(), 66 | p2_puzzle_hash=did_info['p2_puzzle_hash'], 67 | curried_params=curried_params, 68 | ) 69 | 70 | await save_asset(db, asset) 71 | logger.debug('new asset, type: %s, id: %s', asset.asset_type, asset.asset_id.hex()) 72 | return 73 | 74 | nft_info = get_nft_info_from_coin_spend(coin, parent_coin_spend, address) 75 | if nft_info is not None: 76 | uncurried_nft, new_did_id, new_p2_puzhash, lineage_proof = nft_info 77 | curried_params = { 78 | 'metadata': to_hex(bytes(uncurried_nft.metadata)), 79 | 'transfer_program': to_hex(bytes(uncurried_nft.transfer_program) if uncurried_nft.transfer_program else None), 80 | 'metadata_updater_hash': to_hex(uncurried_nft.metadata_updater_hash.as_atom()), 81 | 'supports_did': uncurried_nft.supports_did, 82 | 'owner_did': to_hex(new_did_id) if new_did_id else None, 83 | } 84 | asset = Asset( 85 | coin_id=coin.name(), 86 | asset_type='nft', 87 | asset_id=uncurried_nft.singleton_launcher_id, 88 | confirmed_height=coin_record['confirmed_block_index'], 89 | spent_height=0, 90 | coin=coin.to_json_dict(), 91 | p2_puzzle_hash=new_p2_puzhash, 92 | nft_did_id=new_did_id, 93 | lineage_proof=lineage_proof.to_json_dict(), 94 | curried_params=curried_params 95 | ) 96 | await save_asset(db, asset) 97 | logger.info('new asset, address: %s, type: %s, id: %s', address.hex(), asset.asset_type, asset.asset_id.hex()) 98 | 99 | 100 | async def sync_user_assets(chain_id, address: bytes, client: FullNodeRpcClient): 101 | """ 102 | sync did / nft by https://docs.chia.net/docs/12rpcs/full_node_api/#get_coin_records_by_hint 103 | """ 104 | # todo: use singleflight or use special process to sync 105 | db = get_db(chain_id) 106 | 107 | start_height_info = await get_address_sync_height(db, address) 108 | if start_height_info: 109 | start_height = start_height_info['height'] + 1 110 | else: 111 | start_height = 1 112 | 113 | end_height = await get_latest_tx_block_number(db) 114 | if end_height is None: 115 | end_height = await client.get_block_number() 116 | 117 | if start_height >= end_height: 118 | return 119 | 120 | logger.debug('chain: %s, address: %s, sync from %d to %d', chain_id, address.hex(), start_height, end_height) 121 | 122 | coin_records = await client.get_coin_records_by_hint( 123 | address, include_spent_coins=False, start_height=start_height, end_height=end_height+1) 124 | 125 | logger.debug('hint records: %d', len(coin_records)) 126 | if coin_records: 127 | pz_and_solutions = await asyncio.gather(*[ 128 | client.get_puzzle_and_solution(hexstr_to_bytes(cr['coin']['parent_coin_info']), cr['confirmed_block_index']) 129 | for cr in coin_records 130 | ]) 131 | 132 | for coin_record, parent_coin_spend in zip(coin_records, pz_and_solutions): 133 | await handle_coin(address, coin_record, parent_coin_spend, db) 134 | 135 | await save_address_sync_height(db, address, end_height) 136 | 137 | 138 | 139 | async def get_and_sync_singleton(chain_id, singleton_id: bytes, client: FullNodeRpcClient): 140 | db = get_db(chain_id) 141 | singleton_spend = await get_singelton_spend_by_id(db, singleton_id) 142 | if singleton_spend is None: 143 | fetch_coin_id = singleton_id 144 | spent_block_index = None 145 | else: 146 | fetch_coin_id = singleton_spend.coin_id 147 | spent_block_index = singleton_spend.spent_block_index 148 | odd_coin_record = None 149 | while spent_block_index is None or spent_block_index > 0: 150 | coin_recrods = await client.get_coin_records_by_parent_ids([fetch_coin_id, ], include_spent_coins=True, start_height=spent_block_index, end_height=spent_block_index + 1 if spent_block_index else None) 151 | odd_coin_record = None 152 | for cr in coin_recrods: 153 | if cr['coin']['amount'] % 2 == 1: 154 | if odd_coin_record is not None: 155 | raise ValueError('more than one odd coin') 156 | odd_coin_record = cr 157 | if odd_coin_record is None: 158 | break 159 | spent_block_index = odd_coin_record['spent_block_index'] 160 | odd_coin = Coin.from_json_dict(odd_coin_record['coin']) 161 | fetch_coin_id = odd_coin.name() 162 | 163 | if odd_coin_record is None: 164 | if singleton_spend: 165 | # maybe reorg 166 | await delete_singleton_spend_by_id(db, singleton_id) 167 | return await get_and_sync_singleton(chain_id, singleton_id, client) 168 | else: 169 | raise ValueError("This is not a singleton") 170 | 171 | parent_coin_id = hexstr_to_bytes(odd_coin_record['coin']['parent_coin_info']) 172 | await save_singleton_spend(db, SingletonSpend( 173 | singleton_id=singleton_id, 174 | coin_id=parent_coin_id, 175 | spent_block_index=odd_coin_record['confirmed_block_index'])) 176 | 177 | 178 | coin_spend = await client.get_puzzle_and_solution(parent_coin_id, odd_coin_record['confirmed_block_index']) 179 | return { 180 | 'parent_coin_spend': coin_spend, 181 | 'current_coin': odd_coin_record['coin'] 182 | } 183 | 184 | -------------------------------------------------------------------------------- /openapi/types.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Tuple, Optional 2 | from dataclasses import dataclass, asdict 3 | import io 4 | from clvm import SExp 5 | from clvm import run_program as default_run_program 6 | from clvm.casts import int_from_bytes, int_to_bytes 7 | from clvm.EvalError import EvalError 8 | from clvm.operators import OPERATOR_LOOKUP 9 | from clvm.serialize import sexp_from_stream, sexp_to_stream 10 | from clvm_tools.curry import uncurry 11 | 12 | from .utils import hexstr_to_bytes, sha256, to_hex 13 | from .utils.tree_hash import sha256_treehash 14 | 15 | 16 | def run_program( 17 | program, 18 | args, 19 | max_cost, 20 | operator_lookup=OPERATOR_LOOKUP, 21 | pre_eval_f=None, 22 | ): 23 | return default_run_program( 24 | program, 25 | args, 26 | operator_lookup, 27 | max_cost, 28 | pre_eval_f=pre_eval_f, 29 | ) 30 | 31 | INFINITE_COST = 0x7FFFFFFFFFFFFFFF 32 | 33 | @dataclass(frozen=True) 34 | class Coin: 35 | parent_coin_info: bytes 36 | puzzle_hash: bytes 37 | amount: int 38 | 39 | def name(self): 40 | return sha256(self.parent_coin_info + self.puzzle_hash + int_to_bytes(self.amount)) 41 | 42 | def to_json_dict(self) -> Dict[str, Any]: 43 | return { 44 | 'parent_coin_info': to_hex(self.parent_coin_info), 45 | 'puzzle_hash': to_hex(self.puzzle_hash), 46 | 'amount': self.amount 47 | } 48 | 49 | @classmethod 50 | def from_json_dict(cls, json_dict: Dict[str, Any]) -> 'Coin': 51 | return cls( 52 | hexstr_to_bytes(json_dict['parent_coin_info']), 53 | hexstr_to_bytes(json_dict['puzzle_hash']), 54 | int(json_dict['amount']), 55 | ) 56 | 57 | 58 | class Program(SExp): 59 | @classmethod 60 | def parse(cls, f) -> "Program": 61 | return sexp_from_stream(f, cls.to) 62 | 63 | def stream(self, f): 64 | sexp_to_stream(self, f) 65 | 66 | @classmethod 67 | def from_bytes(cls, blob: bytes) -> "Program": 68 | f = io.BytesIO(blob) 69 | result = cls.parse(f) # noqa 70 | assert f.read() == b"" 71 | return result 72 | 73 | @classmethod 74 | def fromhex(cls, hexstr: str) -> "Program": 75 | return cls.from_bytes(hexstr_to_bytes(hexstr)) 76 | 77 | def __bytes__(self) -> bytes: 78 | f = io.BytesIO() 79 | self.stream(f) # noqa 80 | return f.getvalue() 81 | 82 | def __str__(self) -> str: 83 | return bytes(self).hex() 84 | 85 | def curry(self, *args) -> "Program": 86 | fixed_args: Any = 1 87 | for arg in reversed(args): 88 | fixed_args = [4, (1, arg), fixed_args] 89 | return Program.to([2, (1, self), fixed_args]) 90 | 91 | def uncurry(self) -> Tuple["Program", "Program"]: 92 | r = uncurry(self) 93 | if r is None: 94 | return self, self.to(0) 95 | return r 96 | 97 | def as_int(self) -> int: 98 | return int_from_bytes(self.as_atom()) 99 | 100 | def __deepcopy__(self, memo): 101 | return type(self).from_bytes(bytes(self)) 102 | 103 | def get_tree_hash(self, *args: bytes) -> bytes: 104 | return sha256_treehash(self, set(args)) 105 | 106 | def at(self, position: str) -> "Program": 107 | v = self 108 | for c in position.lower(): 109 | if c == "f": 110 | v = v.first() 111 | elif c == "r": 112 | v = v.rest() 113 | else: 114 | raise ValueError(f"`at` got illegal character `{c}`. Only `f` & `r` allowed") 115 | return v 116 | 117 | def run_with_cost(self, max_cost: int, args) -> Tuple[int, "Program"]: 118 | prog_args = Program.to(args) 119 | cost, r = run_program(self, prog_args, max_cost) 120 | return cost, Program.to(r) 121 | 122 | def run(self, args) -> "Program": 123 | cost, r = self.run_with_cost(INFINITE_COST, args) 124 | return r 125 | 126 | @dataclass(frozen=True) 127 | class LineageProof: 128 | parent_name: Optional[bytes] = None 129 | inner_puzzle_hash: Optional[bytes] = None 130 | amount: Optional[int] = None 131 | 132 | def to_json_dict(self): 133 | return { 134 | 'parent_name': to_hex(self.parent_name), 135 | 'inner_puzzle_hash': to_hex(self.inner_puzzle_hash), 136 | 'amount': self.amount if self.amount else None, 137 | } 138 | -------------------------------------------------------------------------------- /openapi/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def int_to_hex(num: int): 5 | s = "%x" % num 6 | if len(s) % 2 == 1: 7 | s = "0" + s 8 | return "0x" + s 9 | 10 | 11 | def to_hex(data: bytes): 12 | if data: 13 | return f"0x{data.hex()}" 14 | 15 | 16 | def sanitize_hex(s: str): 17 | if s.startswith("0x"): 18 | return s 19 | return "0x" + s 20 | 21 | 22 | def hexstr_to_bytes(input_str: str) -> bytes: 23 | if input_str.startswith("0x") or input_str.startswith("0X"): 24 | return bytes.fromhex(input_str[2:]) 25 | return bytes.fromhex(input_str) 26 | 27 | 28 | def int_to_bytes(v) -> bytes: 29 | byte_count = (v.bit_length() + 8) >> 3 30 | if v == 0: 31 | return b"" 32 | r = v.to_bytes(byte_count, "big", signed=True) 33 | while len(r) > 1 and r[0] == (0xFF if r[1] & 0x80 else 0): 34 | r = r[1:] 35 | return r 36 | 37 | 38 | def sha256(data) -> bytes: 39 | return hashlib.sha256(data).digest() 40 | 41 | 42 | def coin_name(parent_coin_info: str, puzzle_hash: str, amount: int) -> bytes: 43 | return sha256(hexstr_to_bytes(parent_coin_info) + hexstr_to_bytes(puzzle_hash) + int_to_bytes(amount)) 44 | 45 | 46 | def sanitize_obj_hex(obj): 47 | if isinstance(obj, str): 48 | return sanitize_hex(obj) 49 | elif isinstance(obj, dict): 50 | return {k: sanitize_obj_hex(v) for k, v in obj.items()} 51 | elif isinstance(obj, (tuple, list)): 52 | return [sanitize_obj_hex(r) for r in obj] 53 | return obj 54 | -------------------------------------------------------------------------------- /openapi/utils/bech32m.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 Pieter Wuille 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | # Based on this specification from Pieter Wuille: 22 | # https://github.com/sipa/bips/blob/bip-bech32m/bip-bech32m.mediawiki 23 | 24 | """Reference implementation for Bech32m and segwit addresses.""" 25 | from typing import List, Iterable, Optional, Tuple 26 | 27 | bytes32 = bytes 28 | 29 | CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 30 | 31 | 32 | def bech32_polymod(values: List[int]) -> int: 33 | """Internal function that computes the Bech32 checksum.""" 34 | generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] 35 | chk = 1 36 | for value in values: 37 | top = chk >> 25 38 | chk = (chk & 0x1FFFFFF) << 5 ^ value 39 | for i in range(5): 40 | chk ^= generator[i] if ((top >> i) & 1) else 0 41 | return chk 42 | 43 | 44 | def bech32_hrp_expand(hrp: str) -> List[int]: 45 | """Expand the HRP into values for checksum computation.""" 46 | return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] 47 | 48 | 49 | M = 0x2BC830A3 50 | 51 | 52 | def bech32_verify_checksum(hrp: str, data: List[int]) -> bool: 53 | return bech32_polymod(bech32_hrp_expand(hrp) + data) == M 54 | 55 | 56 | def bech32_create_checksum(hrp: str, data: List[int]) -> List[int]: 57 | values = bech32_hrp_expand(hrp) + data 58 | polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ M 59 | return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] 60 | 61 | 62 | def bech32_encode(hrp: str, data: List[int]) -> str: 63 | """Compute a Bech32 string given HRP and data values.""" 64 | combined = data + bech32_create_checksum(hrp, data) 65 | return hrp + "1" + "".join([CHARSET[d] for d in combined]) 66 | 67 | 68 | def bech32_decode(bech: str, max_length: int = 90) -> Tuple[Optional[str], Optional[List[int]]]: 69 | """Validate a Bech32 string, and determine HRP and data.""" 70 | if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (bech.lower() != bech and bech.upper() != bech): 71 | return (None, None) 72 | bech = bech.lower() 73 | pos = bech.rfind("1") 74 | if pos < 1 or pos + 7 > len(bech) or len(bech) > max_length: 75 | return (None, None) 76 | if not all(x in CHARSET for x in bech[pos + 1 :]): 77 | return (None, None) 78 | hrp = bech[:pos] 79 | data = [CHARSET.find(x) for x in bech[pos + 1 :]] 80 | if not bech32_verify_checksum(hrp, data): 81 | return (None, None) 82 | return hrp, data[:-6] 83 | 84 | 85 | def convertbits(data: Iterable[int], frombits: int, tobits: int, pad: bool = True) -> List[int]: 86 | """General power-of-2 base conversion.""" 87 | acc = 0 88 | bits = 0 89 | ret = [] 90 | maxv = (1 << tobits) - 1 91 | max_acc = (1 << (frombits + tobits - 1)) - 1 92 | for value in data: 93 | if value < 0 or (value >> frombits): 94 | raise ValueError("Invalid Value") 95 | acc = ((acc << frombits) | value) & max_acc 96 | bits += frombits 97 | while bits >= tobits: 98 | bits -= tobits 99 | ret.append((acc >> bits) & maxv) 100 | if pad: 101 | if bits: 102 | ret.append((acc << (tobits - bits)) & maxv) 103 | elif bits >= frombits or ((acc << (tobits - bits)) & maxv): 104 | raise ValueError("Invalid bits") 105 | return ret 106 | 107 | 108 | def encode_puzzle_hash(puzzle_hash: bytes32, prefix: str) -> str: 109 | encoded = bech32_encode(prefix, convertbits(puzzle_hash, 8, 5)) 110 | return encoded 111 | 112 | 113 | def decode_puzzle_hash(address: str): 114 | hrpgot, data = bech32_decode(address) 115 | if data is None: 116 | raise ValueError("Invalid Address") 117 | decoded = convertbits(data, 5, 8, False) 118 | decoded_bytes = bytes32(decoded) 119 | return hrpgot, decoded_bytes 120 | -------------------------------------------------------------------------------- /openapi/utils/singleflight.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from functools import wraps, partial 3 | from typing import Callable 4 | 5 | 6 | class SingleFlight: 7 | 8 | def __init__(self): 9 | self.key_future = {} 10 | 11 | async def do(self, key, coro_lambda): 12 | if key in self.key_future: 13 | return await self.key_future[key] 14 | 15 | fut = asyncio.ensure_future(coro_lambda()) 16 | self.key_future[key] = fut 17 | try: 18 | res = await fut 19 | return res 20 | finally: 21 | del self.key_future[key] 22 | -------------------------------------------------------------------------------- /openapi/utils/tree_hash.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Set 2 | 3 | from clvm import CLVMObject 4 | from . import sha256 5 | 6 | 7 | bytes32 = bytes 8 | std_hash = sha256 9 | 10 | 11 | def sha256_treehash(sexp: CLVMObject, precalculated: Optional[Set[bytes32]] = None) -> bytes32: 12 | """ 13 | Hash values in `precalculated` are presumed to have been hashed already. 14 | """ 15 | 16 | if precalculated is None: 17 | precalculated = set() 18 | 19 | def handle_sexp(sexp_stack, op_stack, precalculated: Set[bytes32]) -> None: 20 | sexp = sexp_stack.pop() 21 | if sexp.pair: 22 | p0, p1 = sexp.pair 23 | sexp_stack.append(p0) 24 | sexp_stack.append(p1) 25 | op_stack.append(handle_pair) 26 | op_stack.append(handle_sexp) 27 | op_stack.append(roll) 28 | op_stack.append(handle_sexp) 29 | else: 30 | if sexp.atom in precalculated: 31 | r = sexp.atom 32 | else: 33 | r = std_hash(b"\1" + sexp.atom) 34 | sexp_stack.append(r) 35 | 36 | def handle_pair(sexp_stack, op_stack, precalculated) -> None: 37 | p0 = sexp_stack.pop() 38 | p1 = sexp_stack.pop() 39 | sexp_stack.append(std_hash(b"\2" + p0 + p1)) 40 | 41 | def roll(sexp_stack, op_stack, precalculated) -> None: 42 | p0 = sexp_stack.pop() 43 | p1 = sexp_stack.pop() 44 | sexp_stack.append(p0) 45 | sexp_stack.append(p1) 46 | 47 | sexp_stack = [sexp] 48 | op_stack = [handle_sexp] 49 | while len(op_stack) > 0: 50 | op = op_stack.pop() 51 | op(sexp_stack, op_stack, precalculated) 52 | return bytes32(sexp_stack[0]) 53 | -------------------------------------------------------------------------------- /openapi/watcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | check tx status 3 | """ 4 | import os 5 | import asyncio 6 | import argparse 7 | import json 8 | import logging 9 | import time 10 | import logzero 11 | from databases import Database 12 | from decimal import Decimal 13 | from datetime import datetime 14 | from dataclasses import dataclass 15 | from typing import List, Dict 16 | from collections import defaultdict 17 | from .rpc_client import FullNodeRpcClient 18 | from .db import ( 19 | get_latest_blocks, Block, get_block_by_height, 20 | reorg as reorg_db, save_block, update_asset_coin_spent_height, 21 | ) 22 | from .utils import hexstr_to_bytes, coin_name 23 | 24 | 25 | logger = logging.getLogger("openapi.watcher") 26 | 27 | 28 | class Watcher: 29 | def __init__(self, url_or_path: str, db: Database): 30 | self.url_or_path = url_or_path 31 | self.client = None 32 | self.db = db 33 | 34 | async def reorg(self, block_height: int): 35 | # block height block is correct, +1 is error 36 | await reorg_db(self.db, block_height) 37 | logger.info("reorg success: %d", block_height) 38 | 39 | 40 | async def start(self): 41 | if self.url_or_path.startswith('http'): 42 | self.client = await FullNodeRpcClient.create_by_proxy_url(self.url_or_path) 43 | else: 44 | self.client = await FullNodeRpcClient.create_by_chia_root_path(self.url_or_path) 45 | await self.db.connect() 46 | if "sqlite" in str(self.db.url): 47 | await self.db.execute("PRAGMA journal_mode = WAL") 48 | 49 | try: 50 | prev_block = (await get_latest_blocks(self.db, 1))[0] 51 | prev_block = Block( 52 | hash=prev_block['hash'], 53 | height=prev_block['height'], 54 | timestamp=prev_block['timestamp'], 55 | prev_hash=prev_block['prev_hash'], 56 | ) 57 | except IndexError: 58 | prev_block = None 59 | 60 | if prev_block: 61 | start_height = prev_block.height + 1 62 | else: 63 | resp = await self.client.get_blockchain_state() 64 | start_height = resp['peak']['height'] 65 | logger.info("start height: %d", start_height) 66 | while True: 67 | 68 | peak_height = (await self.client.get_blockchain_state())['peak']['height'] 69 | 70 | if start_height > peak_height: 71 | time.sleep(3) 72 | continue 73 | 74 | try: 75 | bc = await self.client.get_block_record_by_height(start_height) 76 | except Exception as e: 77 | logger.error("fetch block error: %s", e) 78 | time.sleep(3) 79 | continue 80 | 81 | block = Block( 82 | hash=hexstr_to_bytes(bc['header_hash']), 83 | height=int(bc['height']), 84 | timestamp=int(bc['timestamp'] or 0), 85 | prev_hash=hexstr_to_bytes(bc['prev_hash']), 86 | is_tx=bool(bc['timestamp']), 87 | ) 88 | 89 | logger.info("fetch block %d, %s, %d", start_height, block.hash.hex(), block.timestamp) 90 | 91 | if prev_block and bytes(prev_block.hash) != bytes(block.prev_hash): 92 | logger.warning("block chain reorg, prev: %d(%s), curr: %d(%s", 93 | prev_block.height, prev_block.hash.hex(), block.height, block.prev_hash.hex()) 94 | check_height = start_height - 1 95 | while check_height: 96 | bc = await self.client.get_block_record_by_height(height=check_height) 97 | db_block = await get_block_by_height(self.db, check_height) 98 | if hexstr_to_bytes(bc['header_hash']) == bytes(db_block['hash']): 99 | prev_block = db_block 100 | break 101 | else: 102 | check_height -= 1 103 | start_height = check_height # will +1 in the func end 104 | 105 | logger.info("reorg to height: %d", check_height) 106 | await self.reorg(check_height) 107 | else: 108 | if block.is_tx: 109 | try: 110 | s = time.monotonic() 111 | await self.new_block(block) 112 | logger.info('block time cost: %s', time.monotonic() - s) 113 | except Exception as e: 114 | logger.error("new block error: %s", e, exc_info=True) 115 | continue 116 | 117 | await save_block(self.db, block) 118 | prev_block = block 119 | start_height += 1 120 | 121 | async def new_block(self, block: Block): 122 | additions, removals = await self.client.get_additions_and_removals(block.hash) 123 | 124 | removals_id = [] 125 | for coin_record in removals: 126 | coin_id = coin_name(**coin_record['coin']) 127 | removals_id.append(coin_id) 128 | 129 | await update_asset_coin_spent_height(self.db, removals_id, block.height) 130 | 131 | 132 | async def main(networks: List[str] = None): 133 | from .config import settings 134 | tasks = [] 135 | for row in settings.SUPPORTED_CHAINS.values(): 136 | 137 | if networks is None: 138 | if row.get('enable') == False: 139 | continue 140 | else: 141 | if row['network_name'] not in networks: 142 | continue 143 | 144 | db = Database(row['database_uri']) 145 | tasks.append(Watcher(row['rpc_url_or_chia_path'], db).start()) 146 | await asyncio.gather(*tasks) 147 | 148 | if __name__ == '__main__': 149 | parser = argparse.ArgumentParser() 150 | parser.add_argument('--networks', nargs='+', help='networks to watch') 151 | args = parser.parse_args() 152 | from . import log_dir 153 | from .config import settings 154 | logzero.setup_logger( 155 | 'openapi', level=logging.getLevelName(settings['LOG_LEVEL']), logfile=os.path.join(log_dir, "watcher.log"), 156 | disableStderrLogger=True) 157 | asyncio.run(main(args.networks)) 158 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.111.0 2 | async-timeout==4.0.2 3 | uvicorn[standard]==0.15.0 4 | aioredis==1.3.0 5 | PyYAML==5.4.1 6 | aiohttp==3.9.5 7 | logzero==1.7.0 8 | aiocache==0.11.1 9 | clvm==0.9.7 10 | clvm_tools==0.4.4 11 | SQLAlchemy==1.4.38 12 | databases[aiosqlite]==0.6.0 13 | dynaconf==3.1.11 14 | ujson -------------------------------------------------------------------------------- /settings.toml.default: -------------------------------------------------------------------------------- 1 | LOG_LEVEL = "INFO" 2 | 3 | SECONDS_PER_BLOCK = 18.75 4 | 5 | RPC_METHOD_WHITE_LIST = [ 6 | 'get_puzzle_and_solution', 7 | 'get_coin_records_by_puzzle_hash', 8 | 'get_coin_records_by_puzzle_hashes', 9 | 'get_coin_record_by_name', 10 | 'get_coin_records_by_names', 11 | 'get_coin_records_by_parent_ids', 12 | ] 13 | 14 | [CACHE] 15 | cache="aiocache.SimpleMemoryCache" 16 | 17 | #cache="aiocache.RedisCache" 18 | #endpoint="127.0.0.1" 19 | #port=6379 20 | #password="" 21 | 22 | [SUPPORTED_CHAINS] 23 | [SUPPORTED_CHAINS.mainnet] 24 | id = 1 25 | network_name = "mainnet" 26 | network_prefix = "xch" 27 | rpc_url_or_chia_path = "http://127.0.0.1:8555" 28 | database_uri = "sqlite+aiosqlite:///wallet_mainnet.db" 29 | enable = true 30 | 31 | [SUPPORTED_CHAINS.testnet10] 32 | id = 2 33 | network_name = "testnet10" 34 | network_prefix = "txch" 35 | rpc_url_or_chia_path = "http://127.0.0.1:8556" 36 | database_uri = "sqlite+aiosqlite:///wallet_testnet10.db" 37 | enable = false --------------------------------------------------------------------------------