├── static ├── demo.gif ├── valknut.svg ├── aesir-banner.svg └── aesir-social.svg ├── src └── aesir │ ├── types │ ├── cluster_enum.py │ ├── new_address.py │ ├── open_channel.py │ ├── litd_info.py │ ├── image.py │ ├── blockchain_info.py │ ├── lnd_invoice.py │ ├── lnd_info.py │ ├── chunk.py │ ├── build.py │ ├── electrs_features.py │ ├── service.py │ ├── litd_status.py │ ├── jsonrpc.py │ ├── ord_status.py │ ├── mint_info.py │ ├── mutex_option.py │ ├── __init__.py │ └── mempool_info.py │ ├── exceptions.py │ ├── __init__.py │ ├── views │ ├── __init__.py │ ├── midgard.py │ ├── vanaheim.py │ ├── alfheim.py │ ├── asgard.py │ ├── utgard.py │ ├── helheim.py │ ├── yggdrasil.py │ └── bifrost.py │ ├── commands │ ├── __init__.py │ ├── nodekeys.py │ ├── clean.py │ ├── invoice.py │ ├── pull.py │ ├── mine.py │ ├── build.py │ ├── ping_pong.py │ └── deploy.py │ ├── core.py │ ├── configs.py │ └── schemas.yml ├── .gitignore ├── LICENSE ├── pyproject.toml └── README.md /static/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krutt/aesir/HEAD/static/demo.gif -------------------------------------------------------------------------------- /src/aesir/types/cluster_enum.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/cluster_enum.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Literal 15 | 16 | ClusterEnum = Literal["cat", "duo", "ohm", "uno"] 17 | 18 | __all__: tuple[str, ...] = ("ClusterEnum",) 19 | -------------------------------------------------------------------------------- /src/aesir/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/exceptions.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-05-05 01:23 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | 14 | class BuildUnsuccessful(OSError): 15 | def __init__(self, code: int, message: str): 16 | self.errno = code 17 | self.strerr = message 18 | 19 | 20 | __all__: tuple[str, ...] = ("BuildUnsuccessful",) 21 | -------------------------------------------------------------------------------- /src/aesir/types/new_address.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/new_address.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from pydantic import BaseModel, StrictStr 15 | 16 | 17 | class NewAddress(BaseModel): 18 | address: StrictStr 19 | 20 | 21 | __all__: tuple[str, ...] = ("NewAddress",) 22 | -------------------------------------------------------------------------------- /src/aesir/types/open_channel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/open_channel.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-02 20:50 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from pydantic import BaseModel, StrictStr 15 | 16 | 17 | class OpenChannel(BaseModel): 18 | funding_txid: StrictStr 19 | 20 | 21 | __all__: tuple[str, ...] = ("OpenChannel",) 22 | -------------------------------------------------------------------------------- /src/aesir/types/litd_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/litd_info.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-23 19:49 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from pydantic import BaseModel, StrictStr 15 | 16 | 17 | class LitdInfo(BaseModel): 18 | commit_hash: StrictStr 19 | version: StrictStr 20 | 21 | 22 | __all__: tuple[str, ...] = ("LitdInfo",) 23 | -------------------------------------------------------------------------------- /src/aesir/types/image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/image.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Literal 15 | 16 | ### Local modules ### 17 | from aesir.types.build import BuildEnum 18 | 19 | Image = BuildEnum | Literal["postgres:latest", "redis:latest"] 20 | 21 | __all__: tuple[str, ...] = ("Image",) 22 | -------------------------------------------------------------------------------- /src/aesir/types/blockchain_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/blockchain_info.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from pydantic import BaseModel, StrictInt, StrictStr 15 | 16 | 17 | class BlockchainInfo(BaseModel): 18 | blocks: StrictInt = 0 19 | chain: StrictStr = "" 20 | size_on_disk: StrictInt = 0 21 | time: StrictInt = 0 22 | 23 | 24 | __all__: tuple[str, ...] = ("BlockchainInfo",) 25 | -------------------------------------------------------------------------------- /src/aesir/types/lnd_invoice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/lnd_invoice.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2024-11-15 00:56 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from pydantic import BaseModel, Field, StrictStr 15 | 16 | 17 | class LNDInvoice(BaseModel): 18 | add_index: int 19 | r_hash: StrictStr 20 | payment_address: StrictStr = Field(alias="payment_addr") 21 | payment_request: StrictStr 22 | 23 | 24 | __all__: tuple[str, ...] = ("LNDInvoice",) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Byte-compiled ### 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | 7 | ### C extensions ### 8 | *.so 9 | 10 | 11 | ### Distribution / packaging ### 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | 31 | ### Environments ### 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | env.bak/ 38 | venv.bak/ 39 | 40 | ### Mac OSX finder indexer ### 41 | **/.DS_Store 42 | 43 | 44 | ### Mypy ### 45 | .mypy_cache/ 46 | .dmypy.json 47 | dmypy.json 48 | 49 | 50 | ### Ruff ### 51 | .ruff_cache 52 | 53 | 54 | ### Workspace settings ### 55 | .idea 56 | .ignore 57 | .vscode 58 | -------------------------------------------------------------------------------- /src/aesir/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/__init__.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: https://www.w3docs.com/snippets/python/what-is-init-py-for.html 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Local modules ### 14 | from aesir.commands import build, clean, deploy, invoice, mine, nodekeys, ping_pong, pull 15 | 16 | __all__: tuple[str, ...] = ( 17 | "build", 18 | "clean", 19 | "deploy", 20 | "invoice", 21 | "mine", 22 | "nodekeys", 23 | "ping_pong", 24 | "pull", 25 | ) 26 | __version__: str = "0.5.5" 27 | -------------------------------------------------------------------------------- /src/aesir/types/lnd_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/lnd_info.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from pydantic import BaseModel, StrictBool, StrictInt, StrictStr 15 | 16 | 17 | class LNDInfo(BaseModel): 18 | block_height: StrictInt = 0 19 | identity_pubkey: StrictStr = "" 20 | num_active_channels: StrictInt = 0 21 | num_peers: StrictInt = 0 22 | synced_to_chain: StrictBool = False 23 | 24 | 25 | __all__: tuple[str, ...] = ("LNDInfo",) 26 | -------------------------------------------------------------------------------- /src/aesir/types/chunk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/chunk.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-05-05 01:23 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from pydantic import BaseModel, Field, StrictInt, StrictStr 15 | 16 | 17 | class ErrorDetail(BaseModel): 18 | code: StrictInt 19 | message: StrictStr 20 | 21 | 22 | class Chunk(BaseModel): 23 | error: None | StrictStr = None 24 | error_detail: None | ErrorDetail = Field(alias="errorDetail", default=None) 25 | stream: None | StrictStr = None 26 | 27 | 28 | __all__: tuple[str, ...] = ("Chunk", "ErrorDetail") 29 | -------------------------------------------------------------------------------- /src/aesir/views/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/views/__init__.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2024-06-25 19:43 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: https://www.w3docs.com/snippets/python/what-is-init-py-for.html 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Local modules ### 14 | from aesir.views.alfheim import Alfheim 15 | from aesir.views.asgard import Asgard 16 | from aesir.views.bifrost import Bifrost 17 | from aesir.views.utgard import Utgard 18 | from aesir.views.vanaheim import Vanaheim 19 | from aesir.views.yggdrasil import Yggdrasil 20 | 21 | __all__: tuple[str, ...] = ("Alfheim", "Asgard", "Bifrost", "Utgard", "Vanaheim", "Yggdrasil") 22 | -------------------------------------------------------------------------------- /src/aesir/types/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/build_enum.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-06 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Literal 15 | 16 | ### Third-party packages ### 17 | from pydantic import BaseModel, StrictStr 18 | 19 | 20 | class Build(BaseModel): 21 | instructions: list[StrictStr] 22 | platform: StrictStr = "linux/amd64" 23 | 24 | 25 | BuildEnum = Literal[ 26 | "aesir-bitcoind", 27 | "aesir-bitcoind-cat", 28 | "aesir-cashu-mint", 29 | "aesir-electrs", 30 | "aesir-litd", 31 | "aesir-lnd", 32 | "aesir-ord-server", 33 | ] 34 | 35 | 36 | __all__: tuple[str, ...] = ("Build", "BuildEnum") 37 | -------------------------------------------------------------------------------- /src/aesir/commands/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/commands/__init__.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: https://www.w3docs.com/snippets/python/what-is-init-py-for.html 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Local modules ### 14 | from aesir.commands.build import build 15 | from aesir.commands.clean import clean 16 | from aesir.commands.deploy import deploy 17 | from aesir.commands.invoice import invoice 18 | from aesir.commands.mine import mine 19 | from aesir.commands.nodekeys import nodekeys 20 | from aesir.commands.ping_pong import ping_pong 21 | from aesir.commands.pull import pull 22 | 23 | __all__: tuple[str, ...] = ( 24 | "build", 25 | "clean", 26 | "deploy", 27 | "invoice", 28 | "mine", 29 | "nodekeys", 30 | "ping_pong", 31 | "pull", 32 | ) 33 | -------------------------------------------------------------------------------- /src/aesir/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/core.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 02:20 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from click import group 15 | 16 | ### Local modules ### 17 | from aesir.commands import build, clean, deploy, invoice, mine, nodekeys, ping_pong, pull 18 | 19 | 20 | @group 21 | def cmdline() -> None: 22 | """aesir""" 23 | 24 | 25 | cmdline.add_command(build, "build") 26 | cmdline.add_command(clean, "clean") 27 | cmdline.add_command(deploy, "deploy") 28 | cmdline.add_command(invoice, "invoice") 29 | cmdline.add_command(mine, "mine") 30 | cmdline.add_command(nodekeys, "nodekeys") 31 | cmdline.add_command(ping_pong, "ping-pong") 32 | cmdline.add_command(pull, "pull") 33 | 34 | 35 | if __name__ == "__main__": 36 | cmdline() 37 | -------------------------------------------------------------------------------- /src/aesir/types/electrs_features.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/electrs_features.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-01 15:50 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Literal 15 | 16 | ### Third-party packages ### 17 | from pydantic import BaseModel, Field, StrictBool, StrictInt, StrictStr 18 | 19 | 20 | class ElectrsFeatures(BaseModel): 21 | genesis_hash: StrictStr 22 | hash_function: StrictStr = Field(default="sha256") 23 | hosts: dict[Literal["tcp_port"], StrictInt] 24 | protocol_max: StrictStr = Field(default="1.4") 25 | protocol_min: StrictStr = Field(default="1.4") 26 | pruning: None | StrictBool = Field(default=None) 27 | server_version: StrictStr = Field(default="electrs/0.10.10") 28 | 29 | 30 | __all__: tuple[str, ...] = ("ElectrsFeatures",) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2022-2025, Sitt Guruvanich 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 | -------------------------------------------------------------------------------- /src/aesir/types/service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/service.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Literal, Mapping 15 | 16 | ### Third-party packages ### 17 | from pydantic import BaseModel, StrictInt, StrictStr 18 | 19 | ### Local modules ### 20 | from aesir.types.image import Image 21 | 22 | 23 | class Service(BaseModel): 24 | command: Mapping[StrictInt, StrictStr] = {} 25 | env_vars: list[StrictStr] = [] 26 | image: Image 27 | ports: list[StrictStr] 28 | 29 | 30 | ServiceName = Literal[ 31 | "aesir-bitcoind", 32 | "aesir-cashu-mint", 33 | "aesir-electrs", 34 | "aesir-litd", 35 | "aesir-lnd", 36 | "aesir-ord-server", 37 | "aesir-ping", 38 | "aesir-pong", 39 | "aesir-postgres", 40 | "aesir-redis", 41 | ] 42 | 43 | __all__: tuple[str, ...] = ("Service", "ServiceName") 44 | -------------------------------------------------------------------------------- /src/aesir/types/litd_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/litd_status.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-23 19:49 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Literal 15 | 16 | ### Third-party packages ### 17 | from pydantic import BaseModel, Field, StrictBool, StrictStr 18 | 19 | 20 | class SubServer(BaseModel): 21 | custom_status: StrictStr 22 | disabled: StrictBool 23 | error: StrictStr 24 | running: StrictBool 25 | 26 | 27 | class SubServers(BaseModel): 28 | accounts: SubServer 29 | faraday: SubServer 30 | lit: SubServer 31 | lnd: SubServer 32 | loop: SubServer 33 | pool: SubServer 34 | taproot_assets: SubServer = Field(alias="taproot-assets") 35 | 36 | 37 | class LitdStatus(BaseModel): 38 | state: Literal["SERVER_ACTIVE"] 39 | sub_servers: SubServers 40 | 41 | 42 | __all__: tuple[str, ...] = ("LitdStatus", "SubServer", "SubServers") 43 | -------------------------------------------------------------------------------- /src/aesir/types/jsonrpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/jsonrpc.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-01 15:50 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Any, Generic, TypeVar 15 | 16 | ### Third-party packages ### 17 | from pydantic import BaseModel, Field, StrictInt, StrictStr 18 | 19 | T = TypeVar("T") 20 | 21 | 22 | class JsonrpcError(BaseModel): 23 | code: StrictInt 24 | data: None | dict[str, Any] = Field(default=None) 25 | message: StrictStr 26 | 27 | 28 | class JsonrpcResponse(BaseModel, Generic[T]): 29 | id: None | int | str = Field(description="Request identifier") 30 | error: None | JsonrpcError = Field(default=None, description="Error object when unsuccessful") 31 | jsonrpc: StrictStr = Field(default="2.0", description="JSON-RPC Version") 32 | result: None | T = Field(default=None, description="Result object when successful") 33 | 34 | 35 | __all__: tuple[str, ...] = ("JsonrpcError", "JsonrpcResponse") 36 | -------------------------------------------------------------------------------- /src/aesir/types/ord_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/ord_status.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-02 14:09 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Literal 15 | 16 | ### Third-party packages ### 17 | from pydantic import BaseModel, StrictBool, StrictInt, StrictStr 18 | 19 | 20 | class OrdStatus(BaseModel): 21 | address_index: StrictBool 22 | blessed_inscriptions: StrictInt 23 | chain: Literal["mainnet", "regtest", "signet", "testnet"] 24 | cursed_inscriptions: StrictInt 25 | height: StrictInt 26 | initial_sync_time: dict[Literal["secs", "nanos"], StrictInt] 27 | inscriptions: StrictInt 28 | lost_sats: StrictInt 29 | minimum_rune_for_next_block: StrictStr 30 | rune_index: StrictBool 31 | runes: StrictInt 32 | sat_index: StrictBool 33 | started: StrictStr 34 | transaction_index: StrictBool 35 | unrecoverably_reorged: StrictBool 36 | uptime: dict[Literal["secs", "nanos"], StrictInt] 37 | 38 | 39 | __all__: tuple[str, ...] = ("OrdStatus",) 40 | -------------------------------------------------------------------------------- /src/aesir/types/mint_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/mint_info.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-05 18:42 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Literal 15 | 16 | ### Third-party packages ### 17 | from pydantic import BaseModel, Field, StrictBool, StrictInt, StrictStr 18 | from pydantic.json_schema import SkipJsonSchema 19 | 20 | 21 | class Method(BaseModel): 22 | commands: SkipJsonSchema[None] | list[StrictStr] = None 23 | description: SkipJsonSchema[None] | StrictBool = None 24 | name: StrictStr = Field(alias="method") 25 | unit: Literal["sat"] 26 | 27 | 28 | class Nut(BaseModel): 29 | disabled: SkipJsonSchema[None] | StrictBool = None 30 | methods: SkipJsonSchema[None] | list[Method] = None 31 | supported: SkipJsonSchema[None] | StrictBool | list[Method] = None 32 | 33 | 34 | class MintInfo(BaseModel): 35 | contact: list[StrictStr] 36 | name: StrictStr 37 | pubkey: StrictStr 38 | version: StrictStr 39 | time: StrictInt 40 | nuts: dict[int, Nut] 41 | 42 | 43 | __all__: tuple[str, ...] = ("Method", "MintInfo", "Nut") 44 | -------------------------------------------------------------------------------- /src/aesir/configs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/configs.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from pathlib import Path 15 | from typing import Any 16 | 17 | ### Third-party packages ### 18 | from pydantic import TypeAdapter 19 | from yaml import Loader, load 20 | 21 | ### Local modules ### 22 | from aesir.types import ( 23 | Build, 24 | BuildEnum, 25 | ClusterEnum, 26 | Service, 27 | ServiceName, 28 | ) 29 | 30 | ### Parse schemas ### 31 | BUILDS: dict[BuildEnum, Build] 32 | CLUSTERS: dict[ClusterEnum, dict[ServiceName, Service]] 33 | NETWORK: str 34 | PERIPHERALS: dict[ServiceName, Service] 35 | 36 | file_path: Path = Path(__file__).resolve() 37 | with open(str(file_path).replace("configs.py", "schemas.yml"), "rb") as stream: 38 | schema: None | dict[str, Any] = load(stream, Loader=Loader) 39 | if schema: 40 | BUILDS = TypeAdapter(dict[BuildEnum, Build]).validate_python(schema["builds"]) 41 | CLUSTERS = TypeAdapter(dict[ClusterEnum, dict[ServiceName, Service]]).validate_python( 42 | schema["clusters"] 43 | ) 44 | NETWORK = schema.get("network", "aesir") 45 | PERIPHERALS = TypeAdapter(dict[ServiceName, Service]).validate_python(schema["peripherals"]) 46 | 47 | __all__: tuple[str, ...] = ("BUILDS", "CLUSTERS", "NETWORK", "PERIPHERALS") 48 | -------------------------------------------------------------------------------- /src/aesir/types/mutex_option.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/mutex_option.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Any, Mapping 15 | 16 | ### Third-party packages ### 17 | from click import Context, Option, UsageError 18 | 19 | 20 | class MutexOption(Option): 21 | def __init__(self, *args: Any, **kwargs: Any) -> None: 22 | self.alternatives: list[Any] = kwargs.pop("alternatives") 23 | assert self.alternatives, "'alternatives' parameter required." 24 | kwargs["help"] = ( 25 | kwargs.get("help", "") + f"Option is mutually exclusive with {', '.join(self.alternatives)}." 26 | ).strip() 27 | super(MutexOption, self).__init__(*args, **kwargs) 28 | 29 | def handle_parse_result( 30 | self, ctx: Context, opts: Mapping[str, Any], args: list[str] 31 | ) -> tuple[Any, list[str]]: 32 | current_opt: bool = self.name in opts 33 | for mutex_option in self.alternatives: 34 | if mutex_option in opts: 35 | if current_opt: 36 | raise UsageError( 37 | f"Illegal usage: '{self.name}' is mutually exclusive with {mutex_option}." 38 | ) 39 | else: 40 | self.prompt = None 41 | return super(MutexOption, self).handle_parse_result(ctx, opts, args) 42 | 43 | 44 | __all__: tuple[str, ...] = ("MutexOption",) 45 | -------------------------------------------------------------------------------- /src/aesir/commands/nodekeys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/commands/nodekeys.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from re import match 15 | 16 | ### Third-party packages ### 17 | from click import command 18 | from docker import DockerClient, from_env 19 | from docker.errors import DockerException 20 | from docker.models.containers import Container 21 | from pydantic import TypeAdapter 22 | from rich import print as rich_print 23 | from rich.progress import track 24 | 25 | ### Local modules ### 26 | from aesir.types import LNDInfo 27 | 28 | 29 | @command 30 | def nodekeys() -> None: 31 | """Fetch nodekeys from active LND containers.""" 32 | try: 33 | client: DockerClient = from_env() 34 | client.ping() 35 | except DockerException: 36 | rich_print("[red bold]Unable to connect to docker daemon.") 37 | return 38 | 39 | lnds: list[Container] = list( 40 | filter( 41 | lambda container: match(r"aesir-lnd|aesir-ping|aesir-pong", container.name), 42 | reversed(client.containers.list()), 43 | ) 44 | ) 45 | outputs: list[str] = [] 46 | for container in track(lnds, "Fetch LND nodekeys:".ljust(42)): 47 | lnd_info: LNDInfo = TypeAdapter(LNDInfo).validate_json( 48 | container.exec_run( 49 | """ 50 | lncli 51 | --macaroonpath=/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon 52 | --rpcserver=localhost:10001 53 | --tlscertpath=/home/lnd/.lnd/tls.cert 54 | getinfo 55 | """ 56 | ).output 57 | ) 58 | outputs.append(f"") 59 | list(map(rich_print, outputs)) 60 | 61 | 62 | __all__: tuple[str, ...] = ("nodekeys",) 63 | -------------------------------------------------------------------------------- /src/aesir/types/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/__init__.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: https://www.w3docs.com/snippets/python/what-is-init-py-for.html 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Local modules ### 14 | from aesir.types.blockchain_info import BlockchainInfo 15 | from aesir.types.build import Build, BuildEnum 16 | from aesir.types.chunk import Chunk, ErrorDetail 17 | from aesir.types.cluster_enum import ClusterEnum 18 | from aesir.types.electrs_features import ElectrsFeatures 19 | from aesir.types.image import Image 20 | from aesir.types.jsonrpc import JsonrpcError, JsonrpcResponse 21 | from aesir.types.litd_info import LitdInfo 22 | from aesir.types.litd_status import LitdStatus, SubServer, SubServers 23 | from aesir.types.lnd_info import LNDInfo 24 | from aesir.types.lnd_invoice import LNDInvoice 25 | from aesir.types.mempool_info import MempoolInfo 26 | from aesir.types.mint_info import Method, MintInfo, Nut 27 | from aesir.types.mutex_option import MutexOption 28 | from aesir.types.new_address import NewAddress 29 | from aesir.types.open_channel import OpenChannel 30 | from aesir.types.ord_status import OrdStatus 31 | from aesir.types.service import Service, ServiceName 32 | 33 | 34 | __all__: tuple[str, ...] = ( 35 | "BlockchainInfo", 36 | "Build", 37 | "BuildEnum", 38 | "Chunk", 39 | "ClusterEnum", 40 | "ErrorDetail", 41 | "ElectrsFeatures", 42 | "Image", 43 | "JsonrpcError", 44 | "JsonrpcResponse", 45 | "LitdInfo", 46 | "LitdStatus", 47 | "LNDInfo", 48 | "LNDInvoice", 49 | "MempoolInfo", 50 | "Method", 51 | "MintInfo", 52 | "Nut", 53 | "MutexOption", 54 | "NewAddress", 55 | "OpenChannel", 56 | "OrdStatus", 57 | "Service", 58 | "ServiceName", 59 | "SubServer", 60 | "SubServers", 61 | ) 62 | -------------------------------------------------------------------------------- /src/aesir/types/mempool_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/types/blockchain_info.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2024-06-23 14:30 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from pydantic import BaseModel, Field, StrictBool, StrictInt, StrictFloat 15 | 16 | 17 | class MempoolInfo(BaseModel): 18 | full_rbf: StrictBool = Field( 19 | alias="fullrbf", 20 | default=0, 21 | description="True if mempool accepts RBF without replaceability signaling introspection", 22 | ) 23 | loaded: StrictBool = Field(default=0, description="True if mempool is fully loaded") 24 | max_mempool: StrictInt = Field( 25 | alias="maxmempool", default=0, description="Maxmimum memory usage for the mempool" 26 | ) 27 | mempool_minimum_fee: StrictFloat = Field( 28 | alias="mempoolminfee", 29 | default=0, 30 | description="Minimum fee rate in BTC/kvB for transaction to be accepted", 31 | ) 32 | minimum_relay_transaction_fee: StrictFloat = Field( 33 | alias="minrelaytxfee", default=0, description="Minimum relay fees for transaction" 34 | ) 35 | total_fee: StrictFloat = Field(default=0, description="Total fees for the mempool in BTC") 36 | txn_count: StrictInt = Field(alias="size", default=0, description="Current transaction count") 37 | txn_bytes: StrictInt = Field( 38 | alias="bytes", 39 | default=0, 40 | description="Sum of all virtual transaction sizes as defined in BIP-141", 41 | ) 42 | usage: StrictInt = Field(default=0, description="Total memory usage for mempool") 43 | unbroadcast_count: StrictInt = Field( 44 | alias="unbroadcastcount", 45 | default=0, 46 | description="Number of transactions that have not passed initial broadcast", 47 | ) 48 | 49 | 50 | __all__: tuple[str, ...] = ("MempoolInfo",) 51 | -------------------------------------------------------------------------------- /src/aesir/commands/clean.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/commands/clean.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from re import match 15 | 16 | ### Third-party packages ### 17 | from click import command, option 18 | from docker import DockerClient, from_env 19 | from docker.errors import DockerException, NotFound 20 | from docker.models.containers import Container 21 | from docker.models.networks import Network 22 | from rich import print as rich_print 23 | from rich.progress import track 24 | from requests.exceptions import JSONDecodeError 25 | 26 | ### Local modules ### 27 | from aesir.configs import NETWORK 28 | 29 | 30 | @command 31 | @option("--inactive", help="Query inactive containers for removal.", is_flag=True, type=bool) 32 | def clean(inactive: bool) -> None: 33 | """Remove all active "aesir-*" containers, drop network.""" 34 | try: 35 | client: DockerClient = from_env() 36 | client.ping() 37 | except DockerException: 38 | rich_print("[red bold]Unable to connect to docker daemon.") 39 | return 40 | 41 | outputs: list[str] = [] 42 | containers: list[Container] = client.containers.list(all=inactive) 43 | for container in track(containers, f"Clean {('active','all')[inactive]} containers:".ljust(42)): 44 | if match(r"aesir-*", container.name) is not None: 45 | try: 46 | container.stop() 47 | except JSONDecodeError: 48 | pass 49 | container.remove(v=True) # if `v` is true, remove associated volume 50 | outputs.append(f" removed.") 51 | try: 52 | network: Network = client.networks.get(NETWORK) 53 | network.remove() 54 | outputs.append(f" removed.") 55 | except NotFound: 56 | pass 57 | list(map(rich_print, outputs)) 58 | 59 | 60 | __all__: tuple[str, ...] = ("clean",) 61 | -------------------------------------------------------------------------------- /src/aesir/views/midgard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.9 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/views/midgard.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-03 13:00 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from docker.models.containers import Container 15 | from pydantic import BaseModel, ConfigDict, TypeAdapter 16 | from rich.console import RenderableType 17 | from rich.text import Text 18 | 19 | ### Local modules ### 20 | from aesir.types import LNDInfo 21 | 22 | 23 | class Midgard(BaseModel): 24 | """ 25 | Realm of men, created by Odin and his two brothers Veli and Ve 26 | """ 27 | 28 | model_config = ConfigDict(arbitrary_types_allowed=True) 29 | container: Container 30 | 31 | @property 32 | def renderable(self) -> RenderableType: 33 | lnd_info: LNDInfo = TypeAdapter(LNDInfo).validate_json( 34 | self.container.exec_run( 35 | """ 36 | lncli 37 | --macaroonpath=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon 38 | --rpcserver=localhost:10009 39 | --tlscertpath=/root/.lnd/tls.cert 40 | getinfo 41 | """ 42 | ).output 43 | ) 44 | return Text.assemble( 45 | "\n\n\n", 46 | ("Nodekey:\n", "light_coral bold"), 47 | lnd_info.identity_pubkey, 48 | "\n", 49 | "\n".ljust(19), 50 | ("Channels:".ljust(10), "green bold"), 51 | str(lnd_info.num_active_channels).rjust(20), 52 | "\n".ljust(19), 53 | ("Peers:".ljust(10), "cyan bold"), 54 | str(lnd_info.num_peers).rjust(20), 55 | "\n".ljust(19), 56 | ("Blocks:".ljust(10), "rosy_brown bold"), 57 | str(lnd_info.block_height).rjust(20), 58 | "\n".ljust(19), 59 | ("Synced?:".ljust(10), "steel_blue bold"), 60 | ("true".rjust(20), "green") if lnd_info.synced_to_chain else ("false".rjust(20), "red"), 61 | "\n\n\n", 62 | ) 63 | 64 | 65 | __all__: tuple[str, ...] = ("Midgard",) 66 | -------------------------------------------------------------------------------- /src/aesir/views/vanaheim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/views/vanaheim.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-05 18:42 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Iterator 15 | 16 | ### Third-party packages ### 17 | from docker.models.containers import Container 18 | from pydantic import BaseModel, ConfigDict, TypeAdapter 19 | from rich.console import RenderableType 20 | from rich.text import Text 21 | 22 | ### Local modules ### 23 | from aesir.types import MintInfo 24 | 25 | 26 | class Vanaheim(BaseModel): 27 | """ 28 | Lush varied realm sealed off by Odin. Home of the Vanir tribe and deities. 29 | Flora and fauna abundant with magic, fertility and wisdom. 30 | """ 31 | 32 | model_config = ConfigDict(arbitrary_types_allowed=True) 33 | container: Container 34 | 35 | @property 36 | def renderable(self) -> RenderableType: 37 | mint_info: MintInfo = TypeAdapter(MintInfo).validate_json( 38 | self.container.exec_run( 39 | """ 40 | curl -sSL -H "Accept: application/json" http://localhost:3338/v1/info 41 | """ 42 | ).output 43 | ) 44 | supported_nuts: list[int] = list( 45 | map( 46 | lambda num_nut: num_nut[0], 47 | filter(lambda num_nut: not num_nut[1].disabled, mint_info.nuts.items()), 48 | ) 49 | ) 50 | return Text.assemble( 51 | "\n\n".ljust(20), 52 | ("Name:".ljust(13), "light_slate_gray bold"), 53 | f"{mint_info.name}".rjust(17), 54 | "\n".ljust(19), 55 | ("Version:".ljust(11), "rosy_brown bold"), 56 | f"{mint_info.version}".rjust(19), 57 | "\n\n", 58 | ("Supported NUTs:\n", "light_coral bold"), 59 | str(supported_nuts), 60 | "\n\n", 61 | ("Pubkey:".ljust(13), "sandy_brown bold"), 62 | f"{mint_info.pubkey}".rjust(13), 63 | "\n\n\n", 64 | ) 65 | 66 | 67 | __all__: tuple[str, ...] = ("Vanaheim",) 68 | -------------------------------------------------------------------------------- /src/aesir/views/alfheim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.9 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/views/alfheim.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-03 13:00 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from docker.models.containers import Container 15 | from pydantic import BaseModel, ConfigDict, TypeAdapter 16 | from rich.console import RenderableType 17 | from rich.text import Text 18 | 19 | ### Local modules ### 20 | from aesir.types import ElectrsFeatures, JsonrpcResponse 21 | 22 | 23 | class Alfheim(BaseModel): 24 | """ 25 | Realm of the Light Elves. Alfheim is another heavenly world, ruled over by Freyr, 26 | the twin brother of Freya and another of the Vanir gods. 27 | """ 28 | 29 | model_config = ConfigDict(arbitrary_types_allowed=True) 30 | container: Container 31 | 32 | @property 33 | def renderable(self) -> RenderableType: 34 | features: JsonrpcResponse[ElectrsFeatures] = TypeAdapter( 35 | JsonrpcResponse[ElectrsFeatures] 36 | ).validate_json( 37 | self.container.exec_run( 38 | [ 39 | "sh", 40 | "-c", 41 | """ 42 | echo '{"id": "feat", "jsonrpc": "2.0", "method": "server.features"}' | nc -N localhost 50001 43 | """, 44 | ] 45 | ).output 46 | ) 47 | return Text.assemble( 48 | "\n\n\n", 49 | ("Genesis Hash:".ljust(15), "green bold"), 50 | features.result.genesis_hash, 51 | "\n\n".ljust(20), 52 | ("Hash function:".ljust(16), "cyan bold"), 53 | f"{features.result.hash_function}".rjust(14), 54 | "\n".ljust(19), 55 | ("Pruning?:".ljust(15), "bright_magenta bold"), 56 | ("true".rjust(15), "green") if features.result.pruning else ("false".rjust(15), "red"), 57 | "\n".ljust(19), 58 | ("Protocol Minimum:".ljust(13), "light_coral bold"), 59 | f"{features.result.protocol_min}".rjust(13), 60 | "\n".ljust(19), 61 | ("Protocol Maximum:".ljust(13), "blue bold"), 62 | f"{features.result.protocol_max}".rjust(13), 63 | "\n\n\n", 64 | ) 65 | 66 | 67 | __all__: tuple[str, ...] = ("Alfheim",) 68 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'hatchling.build' 3 | requires = [ 'hatchling' ] 4 | 5 | 6 | [project] 7 | authors = [ 8 | {email='aekasitt.g+github@siamintech.co.th', name='Sitt Guruvanich'}, 9 | ] 10 | classifiers = [ 11 | 'Development Status :: 3 - Alpha', 12 | 'Environment :: Console', 13 | 'Intended Audience :: Developers', 14 | 'Intended Audience :: Financial and Insurance Industry', 15 | 'License :: OSI Approved :: MIT License', 16 | 'Operating System :: MacOS', 17 | 'Operating System :: MacOS :: MacOS 9', 18 | 'Operating System :: MacOS :: MacOS X', 19 | 'Operating System :: OS Independent', 20 | 'Operating System :: Unix', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.9', 23 | 'Programming Language :: Python :: 3.10', 24 | 'Programming Language :: Python :: 3.11', 25 | 'Programming Language :: Python :: 3.12', 26 | 'Programming Language :: Python :: 3.13', 27 | 'Programming Language :: Python :: 3 :: Only', 28 | 'Topic :: Office/Business :: Financial', 29 | 'Topic :: Office/Business :: Financial :: Investment', 30 | 'Topic :: Other/Nonlisted Topic', 31 | 'Topic :: Security :: Cryptography', 32 | ] 33 | dependencies = [ 34 | 'APScheduler >=3.10.4', 35 | 'blessed >=1.20.0', 36 | 'click >=8.1.7', 37 | 'docker >=7.1.0', 38 | 'pydantic >=2.5.2', 39 | 'pyyaml >=6.0.1', 40 | 'requests >=2.32.3', 41 | 'rich >=13.7.0', 42 | ] 43 | description = 'Command line interface used for generating local Lightning test environment' 44 | homepage = 'https://github.com/krutt/aesir' 45 | include = [ 46 | {format=['sdist', 'wheel'], path='src/aesir/schemas.yml'} 47 | ] 48 | keywords = ['anonymous', 'bitcoin', 'cashu', 'chaum', 'chaumian', 'cli', 'ecash', 'lightning'] 49 | license = 'MIT' 50 | name = 'aesir' 51 | packages = [{from='src', include='aesir'}] 52 | readme = 'README.md' 53 | repository = 'https://github.com/krutt/aesir' 54 | requires-python = '>=3.10' 55 | version = '0.5.5' 56 | 57 | 58 | [project.scripts] 59 | aesir = 'aesir.core:cmdline' 60 | 61 | 62 | [tool.mypy] 63 | disallow_incomplete_defs = true 64 | disallow_untyped_calls = true 65 | disallow_untyped_defs = true 66 | ignore_missing_imports = true 67 | strict = true 68 | 69 | 70 | [tool.ruff] 71 | indent-width = 2 72 | line-length = 100 73 | target-version = 'py310' 74 | 75 | 76 | [tool.ruff.lint.per-file-ignores] 77 | '__init__.py' = ['F401'] # Ignore unused imports 78 | 79 | 80 | [tool.uv] 81 | dev-dependencies = [ 82 | 'mypy >=1.7.1', 83 | 'pytest >=7.4.3', 84 | 'ruff >=0.2.2', 85 | 'types-pyyaml >=6.0.12.12', 86 | 'types-requests >=2.32.0.20250328', 87 | ] 88 | -------------------------------------------------------------------------------- /src/aesir/commands/invoice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/commands/invoice.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2024-11-15 00:56 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from uuid import uuid4 as uuid 15 | 16 | ### Third-party packages ### 17 | from click import argument, command, option 18 | from docker import DockerClient, from_env 19 | from docker.errors import DockerException, NotFound 20 | from docker.models.containers import Container 21 | from pydantic import TypeAdapter 22 | from rich import print as rich_print 23 | 24 | ### Local modules ### 25 | from aesir.types import LNDInvoice, MutexOption, ServiceName 26 | 27 | 28 | @argument("amount", default=1_000) 29 | @argument("memo", default=str(uuid())) 30 | @command 31 | @option("--lnd", alternatives=["ping", "pong"], cls=MutexOption, is_flag=True, type=bool) 32 | @option("--ping", alternatives=["lnd", "pong"], cls=MutexOption, is_flag=True, type=bool) 33 | @option("--pong", alternatives=["lnd", "ping"], cls=MutexOption, is_flag=True, type=bool) 34 | def invoice(amount: int, lnd: bool, memo: str, ping: bool, pong: bool) -> None: 35 | """For either "uno" or "duo" cluster, create an invoice from specified lnd container""" 36 | try: 37 | client: DockerClient = from_env() 38 | client.ping() 39 | except DockerException: 40 | rich_print("[red bold]Unable to connect to docker daemon.") 41 | return 42 | 43 | ### Select LND container from specified mutually exclusive options ### 44 | container_name: ServiceName = "aesir-lnd" 45 | container_selector: dict[ServiceName, bool] = { 46 | "aesir-lnd": lnd, 47 | "aesir-ping": ping, 48 | "aesir-pong": pong, 49 | } 50 | try: 51 | container_name = next(filter(lambda key_values: key_values[1], container_selector.items()))[0] 52 | except StopIteration: 53 | pass 54 | 55 | ### Initiate parameters ### 56 | container: Container 57 | try: 58 | container = client.containers.get(container_name) 59 | except NotFound: 60 | rich_print(f'[red bold]Unable to find specified LND container (name="{container_name}")') 61 | return 62 | 63 | ### Generate invoice ### 64 | lnd_invoice: LNDInvoice = TypeAdapter(LNDInvoice).validate_json( 65 | container.exec_run( 66 | f""" 67 | lncli 68 | --macaroonpath=/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon 69 | --rpcserver=localhost:10001 70 | --tlscertpath=/home/lnd/.lnd/tls.cert 71 | addinvoice 72 | --amt={amount} 73 | --memo={memo} 74 | """ 75 | ).output 76 | ) 77 | rich_print(lnd_invoice) 78 | 79 | 80 | __all__: tuple[str, ...] = ("invoice",) 81 | -------------------------------------------------------------------------------- /src/aesir/views/asgard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.9 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/views/asgard.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-03 13:00 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from docker.models.containers import Container 15 | from pydantic import BaseModel, ConfigDict, TypeAdapter 16 | from rich.console import Group, RenderableType 17 | from rich.rule import Rule 18 | from rich.text import Text 19 | 20 | ### Local modules ### 21 | from aesir.types import BlockchainInfo, MempoolInfo 22 | 23 | 24 | class Asgard(BaseModel): 25 | """ 26 | Homeland of the Aesir gods. Asgard lies in the middle of the sky, also on the top of Yggdrasil. 27 | """ 28 | 29 | model_config = ConfigDict(arbitrary_types_allowed=True) 30 | container: Container 31 | 32 | @property 33 | def renderable(self) -> RenderableType: 34 | blockchain_info: BlockchainInfo = TypeAdapter(BlockchainInfo).validate_json( 35 | self.container.exec_run( 36 | """ 37 | bitcoin-cli -regtest -rpcuser=aesir -rpcpassword=aesir getblockchaininfo 38 | """ 39 | ).output 40 | ) 41 | mempool_info: MempoolInfo = TypeAdapter(MempoolInfo).validate_json( 42 | self.container.exec_run( 43 | """ 44 | bitcoin-cli -regtest -rpcuser=aesir -rpcpassword=aesir getmempoolinfo 45 | """ 46 | ).output 47 | ) 48 | return Group( 49 | Text.assemble( 50 | f"\n{'Blockchain information:'.ljust(20)}\n", 51 | ("Chain: ", "bright_magenta bold"), 52 | blockchain_info.chain.ljust(9), 53 | ("Blocks: ", "green bold"), 54 | f"{blockchain_info.blocks}".ljust(8), 55 | ("Size: ", "blue bold"), 56 | f"{blockchain_info.size_on_disk}".ljust(10), 57 | ("Time: ", "cyan bold"), 58 | f"{blockchain_info.time}".rjust(10), 59 | "\n", 60 | ), 61 | Rule(), 62 | Text.assemble( 63 | "\n", 64 | ("Mempool information:".ljust(19), "bold"), 65 | "\n".ljust(19), 66 | ("Fees:".ljust(15), "green bold"), 67 | f"{mempool_info.total_fee}".rjust(15), 68 | "\n".ljust(19), 69 | ("Transactions:".ljust(15), "cyan bold"), 70 | f"{mempool_info.txn_count}".rjust(15), 71 | "\n".ljust(19), 72 | ("Size:".ljust(15), "blue bold"), 73 | f"{mempool_info.txn_bytes}".rjust(15), 74 | "\n".ljust(19), 75 | ("Loaded?:".ljust(15), "bright_magenta bold"), 76 | ("true".rjust(15), "green") if mempool_info.loaded else ("false".rjust(15), "red"), 77 | "\n".ljust(19), 78 | ("Usage:".ljust(15), "light_coral bold"), 79 | f"{mempool_info.usage}".rjust(15), 80 | "\n", 81 | ), 82 | ) 83 | 84 | 85 | __all__: tuple[str, ...] = ("Asgard",) 86 | -------------------------------------------------------------------------------- /src/aesir/views/utgard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.9 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/views/utgard.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-03 13:00 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from docker.models.containers import Container 15 | from pydantic import BaseModel, ConfigDict, TypeAdapter 16 | from rich.console import RenderableType 17 | from rich.text import Text 18 | 19 | ### Local modules ### 20 | from aesir.types import OrdStatus 21 | 22 | 23 | class Utgard(BaseModel): 24 | """ 25 | Unpleasantly rough realm and home to giants who mostly disagree with gods. 26 | """ 27 | 28 | model_config = ConfigDict(arbitrary_types_allowed=True) 29 | container: Container 30 | 31 | @property 32 | def renderable(self) -> RenderableType: 33 | ord_status: OrdStatus = TypeAdapter(OrdStatus).validate_json( 34 | self.container.exec_run( 35 | """ 36 | curl -s -H "Accept: application/json" http://localhost:8080/status 37 | """ 38 | ).output 39 | ) 40 | return Text.assemble( 41 | "\n".ljust(19), 42 | ("Address index:".ljust(15), "light_coral bold"), 43 | ("true".rjust(15), "green") if ord_status.address_index else ("false".rjust(15), "red"), 44 | "\n".ljust(19), 45 | ("Blessed:".ljust(15), "rosy_brown bold"), 46 | f"{ord_status.blessed_inscriptions}".rjust(15), 47 | "\n".ljust(19), 48 | ("Cursed:".ljust(15), "hot_pink bold"), 49 | f"{ord_status.cursed_inscriptions}".rjust(15), 50 | "\n".ljust(19), 51 | ("Chain:".ljust(15), "bright_magenta bold"), 52 | f"{ord_status.chain}".rjust(15), 53 | "\n".ljust(19), 54 | ("Inscriptions:".ljust(15), "sandy_brown bold"), 55 | f"{ord_status.inscriptions}".rjust(15), 56 | "\n".ljust(19), 57 | ("Lost sats:".ljust(15), "light_slate_gray bold"), 58 | f"{ord_status.lost_sats}".rjust(15), 59 | "\n".ljust(19), 60 | ("Rune index:".ljust(15), "cyan bold"), 61 | ("true".rjust(15), "green") if ord_status.rune_index else ("false".rjust(15), "red"), 62 | "\n".ljust(19), 63 | ("Sat Index:".ljust(15), "steel_blue bold"), 64 | ("true".rjust(15), "green") if ord_status.sat_index else ("false".rjust(15), "red"), 65 | "\n".ljust(19), 66 | ("Runes:".ljust(15), "medium_purple bold"), 67 | f"{ord_status.runes}".rjust(15), 68 | "\n".ljust(19), 69 | ("Transaction index:".ljust(13), "dark_sea_green bold"), 70 | ("true".rjust(13), "green") if ord_status.transaction_index else ("false".rjust(12), "red"), 71 | "\n".ljust(19), 72 | ("Unrecoverably reorged:".ljust(13), "tan bold"), 73 | ("true".rjust(9), "green") if ord_status.unrecoverably_reorged else ("false".rjust(8), "red"), 74 | "\n", 75 | ) 76 | 77 | 78 | __all__: tuple[str, ...] = ("Utgard",) 79 | -------------------------------------------------------------------------------- /src/aesir/commands/pull.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/commands/pull.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 06:18 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from typing import Iterator 15 | 16 | ### Third-party packages ### 17 | from click import command, option 18 | from docker import DockerClient, from_env 19 | from docker.errors import DockerException 20 | from rich import print as rich_print 21 | from rich.progress import track 22 | 23 | ### Local modules ### 24 | from aesir.configs import BUILDS, PERIPHERALS 25 | from aesir.types import Image, Service, ServiceName 26 | 27 | 28 | @command 29 | @option("--postgres", is_flag=True, help="Pull postgres optional image", type=bool) 30 | @option("--redis", is_flag=True, help="Pull redis optional image", type=bool) 31 | def pull(postgres: bool, redis: bool) -> None: 32 | """Download required and flagged optional docker images from hub.""" 33 | try: 34 | client: DockerClient = from_env() 35 | client.ping() 36 | except DockerException: 37 | rich_print("[red bold]Unable to connect to docker daemon.") 38 | return 39 | 40 | ### Pull required images ### 41 | outputs: list[str] = [] 42 | docker_images: set[str] = {image.tags[0] for image in client.images.list()} 43 | for registry_id in track(BUILDS.keys(), "Pull buildable images:".ljust(42)): 44 | if registry_id in docker_images: 45 | outputs.append( 46 | f"<[bright_magenta]Image: [green]'{registry_id}'[reset]> already exists in registry." 47 | ) 48 | else: 49 | repository, tag = registry_id.split(":") 50 | client.images.pull(repository=repository, tag=tag) 51 | outputs.append(f"<[bright_magenta]Image: [green]'{registry_id}'[reset]> downloaded.") 52 | list(map(rich_print, outputs)) 53 | 54 | ### Pull peripheral images ### 55 | outputs = [] 56 | peripheral_selector: dict[ServiceName, bool] = { 57 | "aesir-cashu-mint": False, 58 | "aesir-ord-server": False, 59 | "aesir-postgres": postgres, 60 | "aesir-redis": redis, 61 | } 62 | peripherals: Iterator[tuple[ServiceName, Service]] = filter( 63 | lambda service_tuple: peripheral_selector[service_tuple[0]], 64 | PERIPHERALS.items(), 65 | ) 66 | for _, peripheral in track(peripherals, "Pull peripherals images flagged:".ljust(42)): 67 | registry_id: Image = peripheral.image # type: ignore[no-redef] 68 | if registry_id in docker_images: 69 | outputs.append( 70 | f"<[bright_magenta]Image: [green]'{registry_id}'[reset]> already exists in registry." 71 | ) 72 | else: 73 | repository, tag = registry_id.split(":") 74 | client.images.pull(repository=repository, tag=tag) 75 | outputs.append(f"<[bright_magenta]Image: [green]'{registry_id}'[reset]> downloaded.") 76 | list(map(rich_print, outputs)) 77 | 78 | 79 | __all__: tuple[str, ...] = ("pull",) 80 | -------------------------------------------------------------------------------- /src/aesir/views/helheim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.9 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/views/helheim.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2025-10-23 18:59 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Third-party packages ### 14 | from docker.models.containers import Container 15 | from pydantic import BaseModel, ConfigDict, TypeAdapter 16 | from rich.console import RenderableType 17 | from rich.console import Group 18 | from rich.rule import Rule 19 | from rich.text import Text 20 | 21 | ### Local modules ### 22 | from aesir.types import LitdInfo, LitdStatus 23 | 24 | 25 | class Helheim(BaseModel): 26 | """ 27 | Realm of the Light Elves. Alfheim is another heavenly world, ruled over by Freyr, 28 | the twin brother of Freya and another of the Vanir gods. 29 | """ 30 | 31 | model_config = ConfigDict(arbitrary_types_allowed=True) 32 | container: Container 33 | 34 | @property 35 | def renderable(self) -> RenderableType: 36 | litd_info: LitdInfo = TypeAdapter(LitdInfo).validate_json( 37 | self.container.exec_run( 38 | """ 39 | litcli --network=regtest getinfo 40 | """ 41 | ).output 42 | ) 43 | litd_status: LitdStatus = TypeAdapter(LitdStatus).validate_json( 44 | self.container.exec_run( 45 | """ 46 | litcli --network=regtest status 47 | """ 48 | ).output.replace(b"}\n{", b",") 49 | ) 50 | return Group( 51 | Text.assemble( 52 | "\n".ljust(19), 53 | ("Version:".ljust(11), "steel_blue bold"), 54 | f"{litd_info.version}".rjust(19), 55 | "\n".ljust(14), 56 | ("Commit hash:", "light_coral bold"), 57 | "\n".ljust(14), 58 | litd_info.commit_hash, 59 | "\n", 60 | ), 61 | Rule(), 62 | Text.assemble( 63 | "".rjust(18), 64 | ("Accounts?:".ljust(15), "bright_magenta bold"), 65 | ("true".rjust(15), "green") 66 | if litd_status.sub_servers.accounts.running 67 | else ("false".rjust(15), "red"), 68 | "\n".ljust(19), 69 | ("Faraday?:".ljust(15), "hot_pink bold"), 70 | ("true".rjust(15), "green") 71 | if litd_status.sub_servers.faraday.running 72 | else ("false".rjust(15), "red"), 73 | "\n".ljust(19), 74 | ("Lit?:".ljust(15), "light_coral bold"), 75 | ("true".rjust(15), "green") 76 | if litd_status.sub_servers.lit.running 77 | else ("false".rjust(15), "red"), 78 | "\n".ljust(19), 79 | ("Lnd?:".ljust(15), "blue bold"), 80 | ("true".rjust(15), "green") 81 | if litd_status.sub_servers.lnd.running 82 | else ("false".rjust(15), "red"), 83 | "\n".ljust(19), 84 | ("Loop?:".ljust(15), "sandy_brown bold"), 85 | ("true".rjust(15), "green") 86 | if litd_status.sub_servers.loop.running 87 | else ("false".rjust(15), "red"), 88 | "\n".ljust(19), 89 | ("Pool?:".ljust(15), "light_slate_gray bold"), 90 | ("true".rjust(15), "green") 91 | if litd_status.sub_servers.pool.running 92 | else ("false".rjust(15), "red"), 93 | "\n".ljust(19), 94 | ("TaprootAssets?:".ljust(15), "rosy_brown bold"), 95 | ("true".rjust(15), "green") 96 | if litd_status.sub_servers.taproot_assets.running 97 | else ("false".rjust(15), "red"), 98 | ), 99 | ) 100 | 101 | 102 | __all__: tuple[str, ...] = ("Helheim",) 103 | -------------------------------------------------------------------------------- /src/aesir/commands/mine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/commands/mine.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from re import match 15 | 16 | ### Third-party packages ### 17 | from apscheduler.schedulers.background import BackgroundScheduler 18 | from click import argument, command 19 | from docker import DockerClient, from_env 20 | from docker.errors import DockerException, NotFound 21 | from docker.models.containers import Container 22 | from pydantic import TypeAdapter 23 | from rich import print as rich_print 24 | from rich.progress import track 25 | 26 | ### Local modules ### 27 | from aesir.types import NewAddress 28 | from aesir.views import Bifrost 29 | 30 | 31 | @command 32 | @argument("blockcount", default=1, type=int) 33 | @argument("blocktime", default=5, type=int) 34 | def mine(blockcount: int, blocktime: int) -> None: 35 | """Scheduled mining with "blockcount" and "blocktime".""" 36 | try: 37 | client: DockerClient = from_env() 38 | client.ping() 39 | except DockerException: 40 | rich_print("[red bold]Unable to connect to daemon.") 41 | return 42 | 43 | ### Retrieve bitcoind container ### 44 | bitcoind: Container 45 | try: 46 | bitcoind = client.containers.get("aesir-bitcoind") 47 | except NotFound: 48 | try: 49 | bitcoind = client.containers.get("aesir-bitcoind-cat") 50 | except NotFound: 51 | rich_print('[red bold]Unable to find "aesir-bitcoind" container.') 52 | return 53 | 54 | ### Retrieve other containers ### 55 | aesir_containers: list[Container] = list( 56 | filter(lambda container: match(r"aesir-*", container.name), reversed(client.containers.list())) 57 | ) 58 | container_names: list[str] = list(map(lambda container: container.name, aesir_containers)) 59 | lnd_containers: list[Container] = list( 60 | filter(lambda container: match(r"aesir-(lnd|ping|pong)", container.name), aesir_containers) 61 | ) 62 | 63 | ### Generate treasury addresses as mining destinations ### 64 | treasuries: list[str] = [] 65 | if len(lnd_containers) == 0: # FIXME: current bitcoind does not have BerkeleyDB 66 | bitcoind.exec_run( 67 | """ 68 | bitcoin-cli -regtest -rpcuser=aesir -rpcpassword=aesir createwallet default 69 | """ 70 | ) 71 | treasury_address: str = bitcoind.exec_run( 72 | """ 73 | bitcoin-cli -regtest -rpcuser=aesir -rpcpassword=aesir getnewaddress treasury bech32 74 | """ 75 | ).output.decode("utf-8") 76 | treasuries.append(treasury_address) 77 | else: 78 | for container in track(lnd_containers, "Generate mining treasuries:".ljust(42)): 79 | new_address: NewAddress = TypeAdapter(NewAddress).validate_json( 80 | container.exec_run( 81 | """ 82 | lncli 83 | --macaroonpath=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon 84 | --rpcserver=localhost:10009 85 | --tlscertpath=/root/.lnd/tls.cert 86 | newaddress p2wkh 87 | """ 88 | ).output 89 | ) 90 | treasuries.append(new_address.address) 91 | 92 | ### Set up mining schedule using command arguments ### 93 | scheduler: BackgroundScheduler = BackgroundScheduler() 94 | for address in treasuries: 95 | scheduler.add_job( 96 | bitcoind.exec_run, 97 | "interval", 98 | [ 99 | """ 100 | bitcoin-cli -regtest -rpcuser=aesir -rpcpassword=aesir generatetoaddress %d %s 101 | """ 102 | % (blockcount, address) 103 | ], 104 | seconds=blocktime, 105 | ) 106 | scheduler.start() 107 | 108 | bifrost: Bifrost = Bifrost( 109 | bitcoind=bitcoind, 110 | containers=aesir_containers, 111 | container_index=0, 112 | container_names=container_names, 113 | ) 114 | bifrost.display() 115 | 116 | 117 | __all__: tuple[str, ...] = ("mine",) 118 | -------------------------------------------------------------------------------- /src/aesir/commands/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/commands/build.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2024-02-27 23:52 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from io import BytesIO 15 | 16 | ### Third-party packages ### 17 | from click import command, option 18 | from docker import DockerClient, from_env 19 | from docker.errors import DockerException, BuildError 20 | from pydantic import ValidationError 21 | from rich import print as rich_print 22 | from rich.progress import TaskID 23 | 24 | ### Local modules ### 25 | from aesir.configs import BUILDS 26 | from aesir.exceptions import BuildUnsuccessful 27 | from aesir.types import Build, BuildEnum 28 | from aesir.views import Yggdrasil 29 | 30 | 31 | @command 32 | @option("--bitcoind", is_flag=True, help="Build bitcoind image", type=bool) 33 | @option("--bitcoind-cat", is_flag=True, help="Build bitcoind-cat optional image", type=bool) 34 | @option("--cashu-mint", is_flag=True, help="Build cashu-mint optional image", type=bool) 35 | @option("--electrs", is_flag=True, help="Build electrs optional image", type=bool) 36 | @option("--litd", is_flag=True, help="Build litd optional image", type=bool) 37 | @option("--lnd", is_flag=True, help="Build lnd image", type=bool) 38 | @option("--ord-server", is_flag=True, help="Build ord-server optional image", type=bool) 39 | def build( 40 | bitcoind: bool, 41 | bitcoind_cat: bool, 42 | cashu_mint: bool, 43 | electrs: bool, 44 | litd: bool, 45 | lnd: bool, 46 | ord_server: bool, 47 | ) -> None: 48 | """Build peripheral images for the desired cluster.""" 49 | try: 50 | client: DockerClient = from_env() 51 | client.ping() 52 | except DockerException: 53 | rich_print("[red bold]Unable to connect to docker daemon.") 54 | return 55 | 56 | ### Build optional images ### 57 | image_names: list[str] = list( 58 | map( 59 | lambda image: image.tags[0].split(":")[0], 60 | filter(lambda image: len(image.tags) != 0, client.images.list()), 61 | ) 62 | ) 63 | build_select: dict[BuildEnum, bool] = { 64 | "aesir-bitcoind": bitcoind, 65 | "aesir-bitcoind-cat": bitcoind_cat, 66 | "aesir-cashu-mint": cashu_mint, 67 | "aesir-electrs": electrs, 68 | "aesir-litd": litd, 69 | "aesir-lnd": lnd, 70 | "aesir-ord-server": ord_server, 71 | } 72 | 73 | outputs: list[str] = [] 74 | built: set[str] = {tag for tag in BUILDS.keys() if build_select[tag] and tag in image_names} 75 | outputs += map(lambda tag: f" already exists within images.", built) 76 | list(map(rich_print, outputs)) 77 | 78 | builds: dict[str, Build] = { 79 | tag: build for tag, build in BUILDS.items() if build_select[tag] and tag not in image_names 80 | } 81 | build_count: int = len(builds.keys()) 82 | if build_count != 0: 83 | builds_items = builds.items() 84 | with Yggdrasil(row_count=10) as yggdrasil: 85 | task_id: TaskID = yggdrasil.add_task("", progress_type="primary", total=build_count) 86 | for tag, build in builds_items: 87 | build_task_id: TaskID = yggdrasil.add_task(tag, progress_type="build", total=100) 88 | with BytesIO("\n".join(build.instructions).encode("utf-8")) as fileobj: 89 | try: 90 | yggdrasil.progress_build( 91 | client.api.build( 92 | decode=True, fileobj=fileobj, gzip=True, platform=build.platform, rm=True, tag=tag 93 | ), 94 | build_task_id, 95 | ) 96 | except (BuildError, BuildUnsuccessful, ValidationError): 97 | yggdrasil.update(build_task_id, completed=-1) 98 | continue 99 | yggdrasil.update(build_task_id, completed=100) 100 | yggdrasil.update(task_id, advance=1) 101 | yggdrasil.update(task_id, completed=build_count, description="[blue]Complete[reset]") 102 | 103 | 104 | __all__: tuple[str, ...] = ("build",) 105 | -------------------------------------------------------------------------------- /src/aesir/views/yggdrasil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/views/yggdrasil.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2024-06-26 00:14 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from collections import deque 15 | from math import floor 16 | from re import search 17 | from textwrap import wrap 18 | from typing import Deque, Generator 19 | 20 | ### Third-party packages ### 21 | from rich.box import MINIMAL 22 | from rich.console import ConsoleRenderable, Group, RichCast 23 | from rich.progress import BarColumn, Progress, Task, TaskID 24 | from rich.table import Table 25 | 26 | 27 | ### Local modules ### 28 | from aesir.exceptions import BuildUnsuccessful 29 | from aesir.types import Chunk 30 | 31 | 32 | class Yggdrasil(Progress): 33 | primary_task: Task 34 | rows: Deque[str] 35 | table: Table = Table(box=MINIMAL, show_lines=False, show_header=False) 36 | 37 | def __init__(self, row_count: int) -> None: 38 | self.rows = deque(maxlen=row_count) 39 | super().__init__() 40 | 41 | def get_renderable(self) -> ConsoleRenderable | RichCast | str: 42 | return Group(self.table, *self.get_renderables()) 43 | 44 | def get_renderables(self) -> Generator[Table, None, None]: 45 | for task in self.tasks: 46 | if task.fields.get("progress_type") == "build": 47 | image_name: str = task.description or "undefined" 48 | if task.completed < 0: 49 | self.columns = ( 50 | f"[red bold]Build unsuccessful for .", 51 | "".ljust(9), 52 | BarColumn(), 53 | ) 54 | elif task.completed == 0: 55 | self.columns = ( 56 | f"Preparing to build <[bright_magenta]Image [green]'{image_name}'[reset]>…", 57 | "".ljust(9), 58 | BarColumn(), 59 | ) 60 | elif task.completed > 0 and task.completed < 100: 61 | self.columns = ( 62 | f"Building <[bright_magenta]Image [green]'{image_name}'[reset]>…", 63 | "".ljust(9), 64 | BarColumn(), 65 | ) 66 | else: 67 | self.columns = ( 68 | f"[blue]Built [reset]<[bright_magenta]Image [green]'{ image_name }'[reset]>" 69 | "[blue] successfully.[reset]", 70 | BarColumn(), 71 | ) 72 | elif task.fields.get("progress_type") == "primary": 73 | self.columns = ("Build specified images:".ljust(42), BarColumn()) 74 | yield self.make_tasks_table([task]) 75 | 76 | def progress_build(self, chunks: Generator[dict[str, str], None, None], task_id: TaskID) -> None: 77 | """ 78 | :raises BuildUnsuccessful: 79 | :raises pydantic.ValidationError: 80 | """ 81 | for dictionary in chunks: 82 | chunk: Chunk = Chunk.model_validate(dictionary) 83 | if chunk.stream is not None: 84 | step = search(r"^Step (?P\d+)\/(?P\d+) :", chunk.stream) 85 | if step is not None: 86 | divided: int = int(step.group("divided")) 87 | divisor: int = int(step.group("divisor")) 88 | self.update(task_id, completed=floor(divided / divisor * 100)) 89 | self.update_table(chunk.stream) 90 | elif chunk.error is not None: 91 | self.update_table(f"[red]{chunk.error}[reset]") 92 | if chunk.error_detail is not None: 93 | raise BuildUnsuccessful(code=chunk.error_detail.code, message=chunk.error_detail.message) 94 | else: 95 | raise BuildUnsuccessful(code=255, message=chunk.error) 96 | 97 | def update_table(self, row: None | str = None) -> None: 98 | if row is not None: 99 | self.rows.append(row) 100 | table: Table = Table(box=MINIMAL, show_lines=False, show_header=False) 101 | list(map(lambda row: table.add_row("\n".join(wrap(row, width=92)), style="grey50"), self.rows)) 102 | self.table = table 103 | 104 | 105 | __all__: tuple[str, ...] = ("Yggdrasil",) 106 | -------------------------------------------------------------------------------- /src/aesir/views/bifrost.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/views/bifrost.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2024-06-25 19:43 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from re import match 15 | from typing import ClassVar 16 | 17 | ### Third-party packages ### 18 | from blessed import Terminal 19 | from blessed.keyboard import Keystroke 20 | from curses import KEY_DOWN, KEY_UP 21 | from docker.models.containers import Container 22 | from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr 23 | from rich.box import ROUNDED 24 | from rich.layout import Layout 25 | from rich.live import Live 26 | from rich.panel import Panel 27 | from rich.table import Table 28 | from rich.text import Text 29 | 30 | ### Local modules ### 31 | from aesir.views.alfheim import Alfheim 32 | from aesir.views.asgard import Asgard 33 | from aesir.views.helheim import Helheim 34 | from aesir.views.midgard import Midgard 35 | from aesir.views.vanaheim import Vanaheim 36 | from aesir.views.utgard import Utgard 37 | 38 | 39 | class Bifrost(BaseModel): 40 | model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) 41 | bitcoind: Container 42 | container_index: StrictInt = 0 43 | container_names: list[StrictStr] = [] 44 | containers: list[Container] = [] 45 | 46 | ### Split layouts ### 47 | body: ClassVar[Layout] = Layout(name="body", minimum_size=4, ratio=8, size=17) 48 | footer: ClassVar[Layout] = Layout(name="footer", size=3) 49 | main: ClassVar[Layout] = Layout(size=72) 50 | pane: ClassVar[Layout] = Layout() 51 | realms: ClassVar[Layout] = Layout(name="realms", size=20) 52 | sidebar: ClassVar[Layout] = Layout(size=24) 53 | 54 | ### Terminal ### 55 | terminal: ClassVar[Terminal] = Terminal() 56 | 57 | def model_post_init(self, _) -> None: # type: ignore[no-untyped-def] 58 | self.pane.split_row(self.sidebar, self.main) 59 | self.main.split_column(self.body, self.footer) 60 | self.sidebar.split_column(self.realms) 61 | 62 | def display(self) -> None: 63 | with ( 64 | self.terminal.cbreak(), 65 | self.terminal.hidden_cursor(), 66 | Live(self.pane, refresh_per_second=4, transient=True), 67 | ): 68 | try: 69 | while True: 70 | ### Process input key ### 71 | keystroke: Keystroke = self.terminal.inkey(timeout=0.25) 72 | if keystroke.code == KEY_UP and self.container_index > 0: 73 | self.container_index -= 1 74 | elif keystroke.code == KEY_DOWN and self.container_index < len(self.container_names) - 1: 75 | self.container_index += 1 76 | elif keystroke in {"Q", "q"}: 77 | raise StopIteration 78 | 79 | container_rows: str = "" 80 | if self.container_index > 0: 81 | container_rows = "\n".join(self.container_names[: self.container_index]) 82 | container_rows += f"\n[reverse]{self.container_names[self.container_index]}[reset]\n" 83 | else: 84 | container_rows = f"[reverse]{self.container_names[self.container_index]}[reset]\n" 85 | if self.container_index < len(self.container_names) - 1: 86 | container_rows += "\n".join(self.container_names[self.container_index + 1 :]) # noqa: E203 87 | self.pane["realms"].update(Panel(container_rows, title="realms")) 88 | 89 | body_table: Table = Table(box=ROUNDED, expand=True, show_lines=True) 90 | container_name: str = self.container_names[self.container_index] 91 | container: Container = next( 92 | filter(lambda container: container.name == container_name, self.containers) 93 | ) 94 | body_table.add_column(container_name, "dark_sea_green bold") 95 | if match(r"aesir-bitcoind", container_name): 96 | body_table.add_row(Asgard(container=container).renderable) 97 | elif match(r"aesir-cashu-mint", container_name): 98 | body_table.add_row(Vanaheim(container=container).renderable) 99 | elif match(r"aesir-electrs", container_name): 100 | body_table.add_row(Alfheim(container=container).renderable) 101 | elif match(r"aesir-litd", container_name): 102 | body_table.add_row(Helheim(container=container).renderable) 103 | elif match(r"aesir-ord-server", container_name): 104 | body_table.add_row(Utgard(container=container).renderable) 105 | elif match(r"aesir-(lnd|ping|pong)", container_name): 106 | body_table.add_row(Midgard(container=container).renderable) 107 | self.pane["body"].update(body_table) 108 | self.pane["footer"].update( 109 | Panel( 110 | Text.assemble( 111 | "Select:".rjust(16), 112 | (" ↑↓ ", "bright_magenta bold"), 113 | " " * 20, 114 | "Exit:".rjust(16), 115 | (" Q ", "red bold"), 116 | ) 117 | ) 118 | ) 119 | except StopIteration: 120 | print("Valhalla!") 121 | 122 | 123 | __all__: tuple[str, ...] = ("Bifrost",) 124 | -------------------------------------------------------------------------------- /src/aesir/commands/ping_pong.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/commands/ping_pong.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 06:18 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from re import match 15 | 16 | ### Third-party packages ### 17 | from click import argument, command 18 | from docker import DockerClient, from_env 19 | from docker.errors import DockerException, NotFound 20 | from docker.models.containers import Container 21 | from pydantic import TypeAdapter, ValidationError 22 | from rich import print as rich_print 23 | from rich.progress import track 24 | 25 | ### Local modules ### 26 | from aesir.types import LNDInfo, NewAddress, OpenChannel 27 | 28 | 29 | @command 30 | @argument("channel_size", default=16777215) 31 | def ping_pong(channel_size: int) -> None: 32 | """For "duo" cluster, create channels between LND nodes.""" 33 | try: 34 | client: DockerClient = from_env() 35 | client.ping() 36 | except DockerException: 37 | rich_print("[red bold]Unable to connect to docker daemon.") 38 | return 39 | 40 | ### Fetch bitcoind container ### 41 | bitcoind: Container 42 | try: 43 | bitcoind = client.containers.get("aesir-bitcoind") 44 | except NotFound: 45 | rich_print('[red bold]Unable to find "aesir-bitcoind" container.') 46 | return 47 | 48 | ### Initiate parameters ### 49 | nodekeys: dict[str, str] = {} 50 | paddles: list[Container] = list( 51 | filter( 52 | lambda container: match(r"aesir-ping|aesir-pong", container.name), 53 | reversed(client.containers.list()), 54 | ) 55 | ) 56 | treasuries: dict[str, str] = {} 57 | 58 | ### Fetch nodekeys ### 59 | for container in track(paddles, "Fetch LND nodekeys:".ljust(42)): 60 | lnd_info: LNDInfo = TypeAdapter(LNDInfo).validate_json( 61 | container.exec_run( 62 | """ 63 | lncli 64 | --macaroonpath=/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon 65 | --rpcserver=localhost:10001 66 | --tlscertpath=/home/lnd/.lnd/tls.cert 67 | getinfo 68 | """ 69 | ).output 70 | ) 71 | nodekeys[container.name] = lnd_info.identity_pubkey 72 | new_address: NewAddress = TypeAdapter(NewAddress).validate_json( 73 | container.exec_run( 74 | """ 75 | lncli 76 | --macaroonpath=/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon 77 | --rpcserver=localhost:10001 78 | --tlscertpath=/home/lnd/.lnd/tls.cert 79 | newaddress p2wkh 80 | """ 81 | ).output 82 | ) 83 | treasuries[container.name] = new_address.address 84 | 85 | ### Open channels ### 86 | outputs: list[str] = [] 87 | for container in track(paddles, "Open channels:".ljust(42)): 88 | if container.name == "aesir-ping": 89 | try: 90 | open_channel: OpenChannel = TypeAdapter(OpenChannel).validate_json( 91 | container.exec_run( 92 | """ 93 | lncli 94 | --macaroonpath=/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon 95 | --rpcserver=localhost:10001 96 | --tlscertpath=/home/lnd/.lnd/tls.cert 97 | openchannel %d 98 | --node_key %s 99 | --connect aesir-pong:9735 100 | """ 101 | % (channel_size, nodekeys.get("aesir-pong", "")) 102 | ).output 103 | ) 104 | outputs.append( 105 | f" aesir-pong' : txid='{open_channel.funding_txid}'>" 106 | ) 107 | bitcoind.exec_run( 108 | """ 109 | bitcoin-cli -regtest -rpcuser=aesir -rpcpassword=aesir generatetoaddress %d %s 110 | """ 111 | % (6, treasuries.get("aesir-ping", "")) 112 | ) 113 | except ValidationError: 114 | outputs.append("[dim yellow1]Unable to open 'aesir-pong --> aesir-ping' channel.") 115 | elif container.name == "aesir-pong": 116 | try: 117 | open_channel: OpenChannel = TypeAdapter(OpenChannel).validate_json( # type: ignore[no-redef] 118 | container.exec_run( 119 | """ 120 | lncli 121 | --macaroonpath=/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon 122 | --rpcserver=localhost:10001 123 | --tlscertpath=/home/lnd/.lnd/tls.cert 124 | openchannel %d 125 | --node_key %s 126 | --connect aesir-ping:9735 127 | """ 128 | % (channel_size, nodekeys.get("aesir-ping", "")) 129 | ).output 130 | ) 131 | outputs.append( 132 | f" aesir-ping' : txid='{open_channel.funding_txid}'>" 133 | ) 134 | bitcoind.exec_run( 135 | """ 136 | bitcoin-cli -regtest -rpcuser=aesir -rpcpassword=aesir generatetoaddress %d %s 137 | """ 138 | % (6, treasuries.get("aesir-pong", "")) 139 | ) 140 | except ValidationError: 141 | outputs.append("[dim yellow1]Unable to open 'aesir-pong --> aesir-ping' channel.") 142 | list(map(rich_print, outputs)) 143 | 144 | 145 | __all__: tuple[str, ...] = ("ping_pong",) 146 | -------------------------------------------------------------------------------- /static/valknut.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 43 | 48 | 52 | 56 | 57 | 61 | 73 | 74 | 75 | 87 | 88 | 89 | 98 | 99 | 108 | 109 | 118 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aesir 2 | 3 | [![Bitcoin-only](https://img.shields.io/badge/bitcoin-only-FF9900?logo=bitcoin)](https://twentyone.world) 4 | [![Lightning](https://img.shields.io/badge/lightning-792EE5?logo=lightning)](https://mempool.space/lightning) 5 | [![Docker](https://img.shields.io/badge/docker-#2496ED?&logo=docker&logoColor=white)](https://www.docker.com) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/krutt/aesir/blob/master/LICENSE) 7 | [![Top](https://img.shields.io/github/languages/top/krutt/aesir)](https://github.com/krutt/aesir) 8 | [![Languages](https://img.shields.io/github/languages/count/krutt/aesir)](https://github.com/krutt/aesir) 9 | [![Size](https://img.shields.io/github/repo-size/krutt/aesir)](https://github.com/krutt/aesir) 10 | [![Last commit](https://img.shields.io/github/last-commit/krutt/aesir/master)](https://github.com/krutt/aesir) 11 | 12 | [![Aesir banner](https://github.com/krutt/aesir/blob/master/static/aesir-banner.svg)](static/aesir-banner.svg) 13 | 14 | ## Prerequisites 15 | 16 | * [python](https://www.python.org) 3.10 and above - High-level general-purpose programming language 17 | * [pip](https://pypi.org/project/pip) - package installer for Python 18 | * [docker](https://www.docker.com) - build, deploy run, update and manage [containerized](https://opencontainers.org) applications 19 | 20 | ## Getting started 21 | 22 | You can use `aesir` simply by installing via `pip` on your Terminal. 23 | 24 | ```sh 25 | pip install aesir 26 | ``` 27 | 28 | And then you can begin deploying local cluster as such: 29 | 30 | ```sh 31 | aesir deploy 32 | ``` 33 | 34 | The initial deployment may take some time at pulling required images from their respective 35 | repositories. Results may look as such: 36 | 37 | ```sh 38 | $ pip install aesir 39 | > ... 40 | > Installing collected packages: aesir 41 | > Successfully installed aesir-0.4.3 42 | $ aesir deploy 43 | > Deploy specified local cluster: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:01 44 | > Generate addresses: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00 45 | > Mine initial capital for parties: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00 46 | ``` 47 | 48 | You will have podman containers running in the backend, ready to be interfaced by your local 49 | environment applications you are developing. 50 | 51 | ## Begin local mining 52 | 53 | In order to properly test many functionalities, you will need to send mining commands to local 54 | setup. You can achieve completely local and running environment with the following command: 55 | 56 | [![Demonstration](static/demo.gif)](https://github.com/krutt/aesir/blob/master/static/demo.gif) 57 | 58 | ### Cluster types 59 | 60 | Currently there are two supported cluster-types in this project. Specified by flags, 61 | `--duo` (default), or `--cat`, `--ohm`, `--uno` with the following set-up: 62 | 63 | | Type | Description | 64 | | ---- | -------------------------------------------------------------------------- | 65 | | cat | Customized `aesir-bitcoind-cat` node that has OP_CAT enabled for experiments | 66 | | duo | Contains two LND nodes named `aesir-ping` and `aesir-pong` unified by
one single `aesir-bitcoind` service. | 67 | | ohm | Only has `aesir-bitcoind` without any Lightning nodes. | 68 | | uno | Only has one LND node named `aesir-lnd` connected to `aesir-bitcoind`. | 69 | 70 | ### Peripheral containers 71 | 72 | This project also helps you setup peripheral services to make development process easier, too. 73 | For example, if you want to deploy a duo-cluster with attached postgres database, run the following: 74 | 75 | ```sh 76 | $ aesir deploy --with-postgres 77 | > ... 78 | $ aesir mine 79 | > FIXME 80 | ``` 81 | 82 | Or run an uno-cluster with both attached postgres database and redis solid store cache like this: 83 | 84 | ```sh 85 | $ aesir deploy --uno --with-postgres --with-redis 86 | > ... 87 | $ aesir mine 88 | > FIXME 89 | ``` 90 | 91 | ## Cleanup 92 | 93 | Use the following command to clean up active `aesir-*` containers: 94 | 95 | ```sh 96 | aesir clean 97 | ``` 98 | 99 | 🚧 This will resets the current test state, so use with care. Example below: 100 | 101 | ```sh 102 | $ aesir clean 103 | > Remove active containers: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:01 104 | ``` 105 | 106 | ## Change-logs 107 | 108 | * **0.3.1** Add `aesir-cashu-mint` & `aesir-lnd-krub` image setups and deployments w/ shared volumes 109 | * **0.3.2** Define classifiers on `pyproject.toml` for PyPI metadata 110 | * **0.3.3** Drop `black` and use [ruff](https://github.com/astral-sh/ruff) formatter and linter 111 | * **0.3.4** Simplify deployment workflows and 112 | * **0.3.5** Restructure project so that when installed, `src` folder will not be created 113 | * **0.3.6** Breakdown "setup" command into "build" and "pull" 114 | * **0.3.7** Lightning cluster now with [ord](https://github.com/ordinals/ord) 115 | * **0.3.8** Rename "ord" to "ord-server" to avoid confusion with cli 116 | * **0.3.9** Remove intermediate containers 117 | * **0.4.0** Resist electricity with "ohm" mode 118 | * **0.4.1** Remove Ordinals' spiked ball 119 | * **0.4.2** Disable bitcoind prune mode 120 | * **0.4.3** Implement dashboard walkthrough using [blessed](https://github.com/chj/blessed) 121 | * **0.4.4** Add `cat` cluster in deployment and `--bitcoind-cat` flag for customized build 122 | * **0.4.5** The Yggdrasil update where docker build logs are chunked for display 123 | * **0.4.6** Create default wallet if no lnd containers 124 | * **0.4.7** (reverted) Remove unittesting from image build process 125 | * **0.4.8** Follow [Productivity Notes](https://github.com/bitcoin/bitcoin/blob/master/doc/productivity.md) for `bitcoind-cat`; Fix progress bar overlap. 126 | * **0.4.9** Change package manager and add `invoice` command 127 | * **0.5.0** Adopt podman to encourage daemonlessness and rootlessness 128 | * **0.5.1** Drop podman support for unstable SSH connections, viva Docker 129 | * **0.5.2** Stricten type definitions and fix undefined/attribute bugs 130 | * **0.5.3** Define `aesir-bitcoind` and `aesir-lnd` build instructions. Decouple from Polar's 131 | * **0.5.4** Add Lightning Terminal to list of available peripherals as `aesir-litd` 132 | * **0.5.5** Fix build selection and ordering for missing images in deploy command 133 | 134 | ## Roadmap 135 | 136 | * Add CI/CD with testcontainers 137 | * Simplify `schemas.yml` and embed commands to service definitions 138 | * Drop `docker-py` and replace with `podman-py` (rolled back and on hold) 139 | * Write [click](https://click.palletsprojects.com) tests. 140 | * Use [joblib](https://github.com/joblib/joblib) to speed up deployment with parallelization. 141 | * Create and add some type of `ordapi` peripheral service. 142 | 143 | ## Known issues 144 | 145 | * Mining dashboard hangs when using `aesir-bitcoind` & `aesir-ord-server` together in `ohm` cluster 146 | * Rust images like `aesir-electrs` and `aesir-ord-server` are prone to build failure 147 | 148 | ## Contributions 149 | 150 | ### Prerequisites 151 | 152 | * [pyenv](https://github.com/pyenv/pyenv) - simple Python version management 153 | * [uv](https://docs.astral.sh/uv) - extremely fast Python package & project manager written in Rust 154 | 155 | ### Set up local environment 156 | 157 | The following guide walks through setting up your local working environment using `pyenv` 158 | as Python version manager and `uv` as Python package manager. If you do not have `pyenv` 159 | installed, run the following command. 160 | 161 |
162 | Install using Homebrew (Darwin) 163 | 164 | ```sh 165 | brew install pyenv --head 166 | ``` 167 |
168 | 169 |
170 | Install using standalone installer (Darwin and Linux) 171 | 172 | ```sh 173 | curl https://pyenv.run | bash 174 | ``` 175 |
176 | 177 | If you do not have `uv` installed, run the following command. 178 | 179 |
180 | Install using Homebrew (Darwin) 181 | 182 | ```sh 183 | brew install uv 184 | ``` 185 |
186 | 187 |
188 | Install using standalone installer (Darwin and Linux) 189 | 190 | ```sh 191 | curl -LsSf https://astral.sh/uv/install.sh | sh 192 | ``` 193 |
194 | 195 | 196 | Once you have `pyenv` Python version manager installed, you can 197 | install any version of Python above version 3.10 for this project. 198 | The following commands help you set up and activate a Python virtual 199 | environment where `uv` can download project dependencies from the `PyPI` 200 | open-sourced registry defined under `pyproject.toml` file. 201 | 202 |
203 | Set up environment and synchroniz project dependencies 204 | 205 | ```sh 206 | pyenv shell 3.11.9 207 | uv venv --python-preference system 208 | source .venv/bin/activate 209 | uv sync --dev 210 | ``` 211 |
212 | 213 | ## License 214 | 215 | This project is licensed under the terms of the MIT license. 216 | -------------------------------------------------------------------------------- /src/aesir/commands/deploy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | # coding:utf-8 3 | # Copyright (C) 2022-2025 All rights reserved. 4 | # FILENAME: ~~/src/aesir/commands/deploy.py 5 | # VERSION: 0.5.5 6 | # CREATED: 2023-12-01 05:31 7 | # AUTHOR: Sitt Guruvanich 8 | # DESCRIPTION: 9 | # 10 | # HISTORY: 11 | # ************************************************************* 12 | 13 | ### Standard packages ### 14 | from io import BytesIO 15 | from re import match 16 | from time import sleep 17 | from typing import Iterator 18 | 19 | ### Third-party packages ### 20 | from click import command, option 21 | from docker import DockerClient, from_env 22 | from docker.errors import APIError, BuildError, DockerException, ImageNotFound, NotFound 23 | from docker.models.containers import Container 24 | from pydantic import TypeAdapter 25 | from rich import print as rich_print 26 | from rich.progress import TaskID, track 27 | 28 | ### Local modules ### 29 | from aesir.configs import BUILDS, CLUSTERS, NETWORK, PERIPHERALS 30 | from aesir.types import ( 31 | Build, 32 | BuildEnum, 33 | ClusterEnum, 34 | MutexOption, 35 | NewAddress, 36 | Service, 37 | ServiceName, 38 | ) 39 | from aesir.views import Yggdrasil 40 | 41 | 42 | @command 43 | @option("--cat", alternatives=["duo", "ohm", "uno"], cls=MutexOption, is_flag=True, type=bool) 44 | @option("--duo", alternatives=["cat", "ohm", "uno"], cls=MutexOption, is_flag=True, type=bool) 45 | @option("--ohm", alternatives=["cat", "duo", "uno"], cls=MutexOption, is_flag=True, type=bool) 46 | @option("--uno", alternatives=["cat", "duo", "ohm"], cls=MutexOption, is_flag=True, type=bool) 47 | @option("--with-cashu-mint", is_flag=True, help="Deploy cashu-mint peripheral service", type=bool) 48 | @option("--with-electrs", is_flag=True, help="Deploy electrs peripheral service", type=bool) 49 | @option("--with-litd", is_flag=True, help="Deploy litd peripheral service", type=bool) 50 | @option("--with-ord-server", is_flag=True, help="Deploy ord-server peripheral service", type=bool) 51 | @option("--with-postgres", is_flag=True, help="Deploy postgres peripheral service", type=bool) 52 | @option("--with-redis", is_flag=True, help="Deploy redis peripheral service", type=bool) 53 | def deploy( 54 | cat: bool, 55 | duo: bool, 56 | ohm: bool, 57 | uno: bool, 58 | with_cashu_mint: bool, 59 | with_electrs: bool, 60 | with_litd: bool, 61 | with_ord_server: bool, 62 | with_postgres: bool, 63 | with_redis: bool, 64 | ) -> None: 65 | """Deploy cluster, either with one or two LND nodes.""" 66 | try: 67 | client: DockerClient = from_env() 68 | client.ping() 69 | except DockerException: 70 | rich_print("[red bold]Unable to connect to docker daemon.") 71 | return 72 | 73 | ### Defaults to duo network; Derive cluster information from parameters ### 74 | cluster_selector: dict[ClusterEnum, bool] = {"cat": cat, "duo": duo, "ohm": ohm, "uno": uno} 75 | cluster_name: ClusterEnum = "duo" 76 | try: 77 | cluster_name = next(filter(lambda value: value[1], cluster_selector.items()))[0] 78 | except StopIteration: 79 | duo = cluster_name == "duo" 80 | pass 81 | cluster: dict[ServiceName, Service] = CLUSTERS[cluster_name] 82 | image_selector: dict[ServiceName, bool] = { 83 | "aesir-cashu-mint": False, 84 | "aesir-electrs": False, 85 | "aesir-litd": False, 86 | "aesir-ord-server": False, 87 | "aesir-postgres": with_postgres, 88 | "aesir-redis": with_redis, 89 | } 90 | peripherals: Iterator[tuple[ServiceName, Service]] = filter( 91 | lambda peripheral_tuple: image_selector[peripheral_tuple[0]], PERIPHERALS.items() 92 | ) 93 | cluster.update(peripherals) 94 | 95 | ### Define build targets for missing peripherals ### 96 | build_selector: dict[BuildEnum, bool] = { 97 | "aesir-bitcoind": True if duo or uno else False, 98 | "aesir-bitcoind-cat": True if cat else False, 99 | "aesir-cashu-mint": with_cashu_mint, 100 | "aesir-electrs": with_electrs, 101 | "aesir-litd": with_litd, 102 | "aesir-lnd": True if duo or uno else False, 103 | "aesir-ord-server": with_ord_server, 104 | } 105 | ### Build missing images if any ### 106 | image_names: list[str] = list( 107 | map( 108 | lambda image: image.tags[0].split(":")[0], 109 | filter(lambda image: len(image.tags) != 0, client.images.list()), 110 | ) 111 | ) 112 | builds: dict[str, Build] = { 113 | tag: build for tag, build in BUILDS.items() if build_selector[tag] and tag not in image_names 114 | } 115 | build_count: int = len(builds.keys()) 116 | if build_count != 0: 117 | builds_items = builds.items() 118 | with Yggdrasil(row_count=10) as yggdrasil: 119 | task_id: TaskID = yggdrasil.add_task("", progress_type="primary", total=build_count) 120 | for tag, build in builds_items: 121 | build_task_id: TaskID = yggdrasil.add_task(tag, progress_type="build", total=100) 122 | with BytesIO("\n".join(build.instructions).encode("utf-8")) as fileobj: 123 | try: 124 | yggdrasil.progress_build( 125 | client.api.build( 126 | decode=True, fileobj=fileobj, platform=build.platform, rm=True, tag=tag 127 | ), 128 | build_task_id, 129 | ) 130 | except BuildError: 131 | yggdrasil.update( 132 | build_task_id, 133 | completed=0, 134 | description=f"[red bold]Build unsuccessful for .", 135 | ) 136 | yggdrasil.update( 137 | build_task_id, 138 | completed=100, 139 | description=f"[blue]Built <[bright_magenta]Image [green]'{tag}'[reset]> successfully.", 140 | ) 141 | yggdrasil.update(task_id, advance=1) 142 | yggdrasil.update(task_id, completed=build_count, description="[blue]Complete") 143 | 144 | ### Attempts to create network if not exist ### 145 | try: 146 | client.networks.create(NETWORK, check_duplicate=True) 147 | except APIError: 148 | pass 149 | 150 | ### Deploy specified cluster ### 151 | run_errors: list[str] = [] 152 | for name, service in track(cluster.items(), f"Deploy {cluster_name} cluster:".ljust(42)): 153 | image_name: str = service.image 154 | flags: list[str] = list(service.command.values()) 155 | ports: dict[str, str] = dict( 156 | map(lambda item: (item[0], item[1]), [port.split(":") for port in service.ports]) 157 | ) 158 | try: 159 | client.containers.run( 160 | image_name, 161 | command=flags, 162 | detach=True, 163 | environment=service.env_vars, 164 | name=name, 165 | network=NETWORK, 166 | ports=ports, 167 | ) 168 | except APIError as err: 169 | run_errors.append(f"[red]Failed cluster setup due to: [reset]{err.explanation}") 170 | break 171 | if len(run_errors) != 0: 172 | list(map(rich_print, run_errors)) 173 | return 174 | 175 | treasuries: list[str] = [] 176 | if duo or uno: 177 | ### Wait until lnd(s) ready ### 178 | sleep(3) 179 | 180 | ### Mine starting capital ### 181 | for container in track(client.containers.list(), "Generate addresses:".ljust(42)): 182 | if match(r"aesir-lnd|aesir-ping|aesir-pong", container.name) is not None: 183 | new_address: NewAddress = TypeAdapter(NewAddress).validate_json( 184 | container.exec_run( 185 | """ 186 | lncli 187 | --macaroonpath=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon 188 | --rpcserver=localhost:10009 189 | --tlscertpath=/root/.lnd/tls.cert 190 | newaddress p2wkh 191 | """ 192 | ).output 193 | ) 194 | treasuries.append(new_address.address) 195 | 196 | ### Define selection for shared-volume peripherals ### 197 | shared_volume_selector: dict[str, bool] = { 198 | "aesir-cashu-mint": with_cashu_mint and (duo or uno), 199 | "aesir-electrs": with_electrs, 200 | "aesir-litd": with_litd, 201 | "aesir-ord-server": with_ord_server, 202 | "aesir-postgres": False, 203 | "aesir-redis": False, 204 | } 205 | peripherals = filter( 206 | lambda peripheral_tuple: shared_volume_selector[peripheral_tuple[0]], PERIPHERALS.items() 207 | ) 208 | run_errors = [] 209 | for name, service in track(peripherals, "Deploy shared-volume peripherals:".ljust(42)): 210 | volume_target: str = "aesir-bitcoind" if not cat else "aesir-bitcoind-cat" 211 | volume_target = ( 212 | "aesir-lnd" if (name in {"aesir-cashu-mint", "aesir-litd"}) and uno else "aesir-ping" 213 | ) 214 | flags = list(service.command.values()) 215 | ports = dict(map(lambda item: (item[0], item[1]), [port.split(":") for port in service.ports])) 216 | try: 217 | if duo and name == "aesir-cashu-mint": 218 | pass # Target aesir-ping for LND REST instead 219 | elif duo and name == "aesir-litd": 220 | flags[3] = "--lnd.rpclisten=aesir-ping" 221 | flags[5] = "--remote.lnd.rpcserver=aesir-ping:10009" 222 | client.containers.run( 223 | service.image, 224 | command=flags, 225 | detach=True, 226 | environment=service.env_vars, 227 | name=name, 228 | network=NETWORK, 229 | ports=ports, 230 | volumes_from=[volume_target], 231 | ) 232 | except ImageNotFound: 233 | run_errors.append( 234 | f"<[bright_magenta]Image [green]'{service.image}'[reset]> [red]is not found.[reset]" 235 | ) 236 | if with_cashu_mint and not (duo or uno): 237 | run_errors.append("[red]Cashu Mint needs to be run with at least one LND Node.[reset]") 238 | list(map(rich_print, run_errors)) 239 | 240 | if len(treasuries) != 0: 241 | ### Retrieve bitcoind container ### 242 | bitcoind: Container 243 | try: 244 | bitcoind = client.containers.get("aesir-bitcoind") 245 | except NotFound: 246 | rich_print('[dim yellow1]Unable to find "aesir-bitcoind"; initial capital not yet mined.') 247 | return 248 | for address in track(treasuries, "Mine initial capital for parties:".ljust(42)): 249 | bitcoind.exec_run( 250 | """ 251 | bitcoin-cli -regtest -rpcuser=aesir -rpcpassword=aesir generatetoaddress 101 %s 252 | """ 253 | % address 254 | ) 255 | 256 | 257 | __all__: tuple[str, ...] = ("deploy",) 258 | -------------------------------------------------------------------------------- /src/aesir/schemas.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | aesir-bitcoind: 3 | instructions: 4 | - FROM debian:stable-slim AS builder 5 | - RUN apt-get update 6 | - RUN apt-get install -y autoconf bsdmainutils build-essential ccache clang git libboost-all-dev libtool libzmq3-dev pkg-config 7 | - WORKDIR /usr 8 | - RUN mkdir ~/.ccache && echo "max_size = 50.0G\nbase_dir = /usr/bitcoin\n" > ~/.ccache/ccache.conf 9 | - RUN git clone --depth 1 https://github.com/bitcoin/bitcoin.git 10 | - RUN cd bitcoin && git fetch origin --tags && git checkout tags/v27.1 11 | - RUN cd bitcoin && ./autogen.sh 12 | - RUN cd bitcoin && ./configure CC="clang" CXX="clang++" 13 | - RUN cd bitcoin && make -j"$(($(nproc)+1))" src/bitcoind src/bitcoin-cli 14 | - FROM debian:stable-slim AS runner 15 | - WORKDIR /usr/app 16 | - RUN apt-get update 17 | - RUN apt-get install -y libevent-dev libzmq3-dev 18 | - COPY --from=builder /usr/bitcoin/src/bitcoind /usr/app/bitcoind 19 | - COPY --from=builder /usr/bitcoin/src/bitcoin-cli /usr/app/bitcoin-cli 20 | - ENV PATH=$PATH:/usr/app 21 | - RUN mkdir -p /home/bitcoin/.bitcoin 22 | - VOLUME [ "/home/bitcoin/.bitcoin" ] 23 | - EXPOSE 18443 18444 28334 28335 24 | - ENTRYPOINT ["/usr/app/bitcoind"] 25 | aesir-bitcoind-cat: 26 | instructions: 27 | - FROM debian:stable-slim AS builder 28 | - RUN apt-get update 29 | - RUN apt-get install -y autoconf bsdmainutils build-essential ccache clang git libboost-all-dev libtool pkg-config 30 | - WORKDIR /usr 31 | - RUN mkdir ~/.ccache && echo "max_size = 50.0G\nbase_dir = /usr/bitcoin\n" > ~/.ccache/ccache.conf 32 | - RUN git clone --depth 1 --branch dont-success-cat https://github.com/rot13maxi/bitcoin.git 33 | - RUN cd bitcoin && ./autogen.sh && ./configure CC="clang" CXX="clang++" && make -j"$(($(nproc)+1))" src/bitcoind src/bitcoin-cli 34 | - FROM debian:stable-slim AS runner 35 | - WORKDIR /usr/app 36 | - RUN apt-get update 37 | - RUN apt-get install -y libevent-dev 38 | - COPY --from=builder /usr/bitcoin/src/bitcoind /usr/app/bitcoind 39 | - COPY --from=builder /usr/bitcoin/src/bitcoin-cli /usr/app/bitcoin-cli 40 | - ENV PATH=$PATH:/usr/app 41 | - RUN mkdir -p /home/bitcoin/.bitcoin 42 | - VOLUME ["/home/bitcoin/.bitcoin"] 43 | - EXPOSE 18443 18444 28334 28335 44 | - ENTRYPOINT ["/usr/app/bitcoind"] 45 | aesir-cashu-mint: 46 | instructions: 47 | - FROM python:3.13.2-slim AS builder 48 | - RUN apt-get update -y 49 | - RUN apt-get install -y --no-install-recommends automake build-essential curl g++ gcc git pkg-config 50 | - RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.8.22/uv-installer.sh | sh 51 | - ENV PATH=$PATH:/root/.local/bin 52 | - WORKDIR /usr/src 53 | - RUN git clone --branch main https://github.com/cashubtc/nutshell.git 54 | - WORKDIR /usr/src/nutshell 55 | - RUN git reset --hard 67b7ea6 56 | - RUN echo 3.13.2 > .python-version && uv venv --python-preference system 57 | - RUN sed -i 's/authors\s=\s\[.*\]/authors = []/g' pyproject.toml 58 | - RUN sed -i 's/^\[tool\.poetry\]$/[project]/g' pyproject.toml 59 | - RUN sed -i 's/^\[tool\.poetry\.scripts\]$/[project.scripts]/g' pyproject.toml 60 | - RUN sed -i 's/^build-backend\s=\s"poetry\.core\.masonry\.api"$/build-backend = "uv_build"/g' pyproject.toml 61 | - RUN sed -i 's/^requires\s=\s\["poetry-core>=1\.0\.0"\]$/requires = ["uv_build"]/g' pyproject.toml 62 | - RUN echo '\n[tool.uv.build-backend]\nmodule-name = "cashu"\nmodule-root = ""\n' >> pyproject.toml 63 | - RUN uv add -r requirements.txt 64 | - RUN uv sync --frozen --no-dev 65 | - RUN apt-get purge -y automake build-essential g++ gcc git 66 | - ENV PATH=$PATH:/usr/src/nutshell/.venv/bin 67 | - EXPOSE 3338 68 | - ENTRYPOINT [ "mint" ] 69 | aesir-electrs: 70 | instructions: 71 | - FROM rust:1.90.0-slim-bullseye AS builder 72 | - ENV ELECTRS_VERSION=0.10.10 73 | - WORKDIR /build 74 | - RUN apt-get update 75 | - RUN apt-get install -y git clang cmake libsnappy-dev netcat 76 | - RUN git clone --branch v$ELECTRS_VERSION https://github.com/romanz/electrs . 77 | - ENV CARGO_NET_GIT_FETCH_WITH_CLI true 78 | - RUN cargo install --locked --path . 79 | - FROM debian:bullseye-slim AS runner 80 | - WORKDIR /data 81 | - COPY --from=builder /usr/bin/netcat /usr/bin/netcat 82 | - COPY --from=builder /usr/local/cargo/bin/electrs /bin/electrs 83 | - EXPOSE 4224 50001 84 | - STOPSIGNAL SIGINT 85 | - ENTRYPOINT ["electrs"] 86 | aesir-litd: 87 | instructions: 88 | - FROM golang:1.24.6-alpine3.22 AS builder 89 | - ENV GODEBUG netdns=cgo 90 | - RUN apk add --no-cache --update alpine-sdk git make 91 | - WORKDIR /usr/src 92 | - RUN git clone https://github.com/lightninglabs/lightning-terminal 93 | - WORKDIR /usr/src/lightning-terminal 94 | - RUN make go-install 95 | - RUN make go-install-cli 96 | - FROM alpine:3.22.1 AS runner 97 | - COPY --from=builder /go/bin/litd /bin/ 98 | - COPY --from=builder /go/bin/litcli /bin/ 99 | - EXPOSE 8443 100 | - VOLUME /root/.lnd 101 | - ENTRYPOINT ["litd"] 102 | aesir-lnd: 103 | instructions: 104 | - FROM debian:stable-slim 105 | - ENV LND_VERSION=0.19.0-beta 106 | - WORKDIR /usr/app 107 | - RUN apt-get update -y 108 | - RUN apt-get install -y curl wait-for-it 109 | - RUN curl -SLO https://github.com/lightningnetwork/lnd/releases/download/v$LND_VERSION/lnd-linux-arm64-v$LND_VERSION.tar.gz 110 | - RUN tar -xzf lnd-linux-arm64-v$LND_VERSION.tar.gz 111 | - RUN mv lnd-linux-arm64-v$LND_VERSION/lnd lnd 112 | - RUN mv lnd-linux-arm64-v$LND_VERSION/lncli lncli 113 | - RUN rm lnd-linux-arm64-v$LND_VERSION.tar.gz 114 | - RUN rm -rf lns-linux-arm64-v$LND_VERSION 115 | - RUN curl -SLO https://raw.githubusercontent.com/lightningnetwork/lnd/master/contrib/lncli.bash-completion 116 | - RUN mkdir -p /etc/bash_completion.d 117 | - RUN mv lncli.bash-completion /etc/bash_completion.d/lncli.bash-completion 118 | - RUN curl -SLO https://raw.githubusercontent.com/scop/bash-completion/master/bash_completion 119 | - RUN mv bash_completion /usr/share/bash-completion/bash_completion 120 | - RUN bash /usr/share/bash-completion/bash_completion 121 | - ENV PATH=$PATH:/usr/app 122 | - VOLUME [ "/root/.lnd" ] 123 | - EXPOSE 8080 9735 10009 124 | - ENTRYPOINT [ "lnd" ] 125 | aesir-ord-server: 126 | instructions: 127 | - FROM rust:1.89.0-bookworm AS builder 128 | - WORKDIR /usr/src 129 | - RUN apt-get install -y git 130 | - RUN git clone --branch master --depth=1 https://github.com/ordinals/ord.git 131 | - WORKDIR /usr/src/ord 132 | - RUN cargo build --bin ord --release 133 | - FROM debian:bookworm-slim AS runner 134 | - COPY --from=builder /usr/src/ord/target/release/ord /usr/local/bin 135 | - RUN apt-get update && apt-get install -y curl openssl 136 | - ENV RUST_BACKTRACE=1 137 | - ENV RUST_LOG=info 138 | - ENTRYPOINT [ "ord" ] 139 | clusters: 140 | cat: 141 | aesir-bitcoind: 142 | command: &bitcoindFlags 143 | 0: -blockfilterindex=1 144 | 1: -datadir=/home/bitcoin/.bitcoin 145 | 2: -debug=1 146 | 3: -dnsseed=0 147 | 4: -fallbackfee=0.0002 148 | 5: -listen=1 149 | 6: -listenonion=0 150 | 7: -peerblockfilters=1 151 | 8: -regtest=1 152 | 9: -rest 153 | 10: -rpcallowip=0.0.0.0/0 154 | 11: -rpcauth=aesir:0a17eed40accdd0f2271a548547ec9bb$b269b686f2ad7ccf0b5ea3450e0687dc1a90846d3828811ad0d570de83b65f4c 155 | 12: -rpcbind=0.0.0.0 156 | 13: -rpcport=18443 157 | 14: -rpcworkqueue=128 158 | 15: -server=1 159 | 16: -txindex=1 160 | 17: -upnp=0 161 | 18: -zmqpubhashblock=tcp://0.0.0.0:28336 162 | 19: -zmqpubrawblock=tcp://0.0.0.0:28334 163 | 20: -zmqpubrawtx=tcp://0.0.0.0:28335 164 | image: aesir-bitcoind-cat 165 | ports: 166 | - 18443:18443 167 | - 28334:28334 168 | - 28335:28335 169 | duo: 170 | aesir-bitcoind: 171 | command: 172 | <<: *bitcoindFlags 173 | image: aesir-bitcoind 174 | ports: 175 | - 18443:18443 176 | - 28334:28334 177 | - 28335:28335 178 | aesir-ping: 179 | command: &lndFlags 180 | 0: --noseedbackup 181 | 1: --trickledelay=5000 182 | 2: --externalip=aesir-ping 183 | 3: --tlsextradomain=aesir-ping 184 | 4: --listen=0.0.0.0:9735 185 | 5: --rpclisten=0.0.0.0:10009 186 | 6: --restlisten=0.0.0.0:8080 187 | 7: --bitcoin.regtest 188 | 8: --bitcoin.node=bitcoind 189 | 9: --bitcoind.rpchost=aesir-bitcoind 190 | 10: --bitcoind.rpcuser=aesir 191 | 11: --bitcoind.rpcpass=aesir 192 | 12: --bitcoind.zmqpubrawblock=tcp://aesir-bitcoind:28334 193 | 13: --bitcoind.zmqpubrawtx=tcp://aesir-bitcoind:28335 194 | image: aesir-lnd 195 | ports: 196 | - 8080:8080 197 | - 9735:9735 198 | - 10009:10009 199 | aesir-pong: 200 | command: 201 | <<: *lndFlags 202 | 2: --externalip=aesir-pong 203 | 3: --tlsextradomain=aesir-pong 204 | image: aesir-lnd 205 | ports: 206 | - 8080:8081 207 | - 9735:9736 208 | - 10009:10010 209 | ohm: 210 | aesir-bitcoind: 211 | command: 212 | <<: *bitcoindFlags 213 | image: aesir-bitcoind 214 | ports: 215 | - 18443:18443 216 | - 28334:28334 217 | - 28335:28335 218 | uno: 219 | aesir-bitcoind: 220 | command: 221 | <<: *bitcoindFlags 222 | image: aesir-bitcoind 223 | ports: 224 | - 18443:18443 225 | - 28334:28334 226 | - 28335:28335 227 | aesir-lnd: 228 | command: 229 | <<: *lndFlags 230 | 2: --externalip=aesir-lnd 231 | 3: --tlsextradomain=aesir-lnd 232 | image: aesir-lnd 233 | ports: 234 | - 8080:8080 235 | - 9735:9735 236 | - 10009:10009 237 | network: aesir 238 | peripherals: 239 | aesir-cashu-mint: 240 | env_vars: 241 | - MINT_LIGHTNING_BACKEND=LndRestWallet 242 | - MINT_LISTEN_HOST=0.0.0.0 243 | - MINT_LISTEN_PORT=3338 244 | - MINT_LND_REST_CERT=/root/.lnd/tls.cert 245 | - MINT_LND_REST_ENDPOINT=aesir-lnd:8080 246 | - MINT_LND_REST_MACAROON=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon 247 | - MINT_PRIVATE_KEY=aesir 248 | image: aesir-cashu-mint 249 | ports: 250 | - 3338:3338 251 | aesir-electrs: 252 | command: 253 | 0: --daemon-dir=/home/bitcoin/.bitcoin 254 | 1: --daemon-rpc-addr=aesir-bitcoind:18443 255 | 2: --daemon-p2p-addr=aesir-bitcoind:18444 256 | 3: --electrum-rpc-addr=0.0.0.0:50001 257 | 4: --log-filters=INFO 258 | 5: --network=regtest 259 | image: aesir-electrs 260 | ports: 261 | - 4224:4224 262 | - 50001:50001 263 | aesir-ord-server: 264 | command: 265 | 0: --bitcoin-rpc-url=aesir-bitcoind:18443 266 | 1: --chain=regtest 267 | 2: --cookie-file=/home/bitcoin/.bitcoin/regtest/.cookie 268 | 3: server 269 | 4: --http-port=8080 270 | image: aesir-ord-server 271 | env_vars: 272 | - ORD_BITCOIN_RPC_PASS=aesir 273 | - ORD_BITCOIN_RPC_USER=aesir 274 | ports: 275 | - 8080:8081 276 | aesir-litd: 277 | command: 278 | 0: --autopilot.disable 279 | 1: --lit-dir=/root/.lit 280 | 2: --lnd-mode=remote 281 | 3: --lnd.rpclisten=aesir-lnd 282 | 4: --network=regtest 283 | 5: --remote.lnd.rpcserver=aesir-lnd:10009 284 | 6: --remote.lnd.macaroonpath=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon 285 | 7: --remote.lnd.tlscertpath=/root/.lnd/tls.cert 286 | 8: --uipassword=heimdall 287 | image: aesir-litd 288 | ports: 289 | - 8443:8443 290 | aesir-postgres: 291 | image: postgres:latest 292 | env_vars: 293 | - POSTGRES_USER=aesir 294 | - POSTGRES_PASSWORD=aesir 295 | ports: 296 | - 5432:5432 297 | aesir-redis: 298 | image: redis:latest 299 | ports: 300 | - 6379:6379 301 | -------------------------------------------------------------------------------- /static/aesir-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 26 | 27 | 33 | 34 | 39 | 40 | 45 | 46 | 47 | 54 | 62 | 63 | 64 | 73 | 74 | 83 | 90 | Aesir 91 | 92 | 97 | 98 | Command line interface for setting up local regtest 99 | 100 | 101 | Bitcoin & Lightning ⚡️ cluster using Docker Daemon 102 | 103 | 104 | -------------------------------------------------------------------------------- /static/aesir-social.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 23 | 29 | 34 | 39 | 40 | 47 | 53 | 54 | 64 | 65 | 70 | 71 | 76 | 77 | 78 | 85 | 91 | 92 | 98 | 99 | 106 | 115 | 122 | Aesir 123 | 124 | 128 | 131 | Command line interface for setting up 132 | 133 | 136 | local regtest Bitcoin & Lightning ⚡️ 137 | 138 | 141 | cluster using Docker Daemon 142 | 143 | 144 | 153 | 160 | 161 | Python 162 | 163 | 166 | 100.0% 167 | 168 | 169 | --------------------------------------------------------------------------------