matrixcrypto 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrixcrypto/__init__.py +3 -0
- matrixcrypto/__main__.py +3 -0
- matrixcrypto/app.py +329 -0
- matrixcrypto/data/crypto_list.json +104 -0
- matrixcrypto/data/ethereum_ecosystem_crypto_list.json +104 -0
- matrixcrypto/data/offline_crypto_list.json +64 -0
- matrixcrypto/data/solana_ecosystem_crypto_list.json +104 -0
- matrixcrypto/lists.py +62 -0
- matrixcrypto-0.1.0.dist-info/METADATA +119 -0
- matrixcrypto-0.1.0.dist-info/RECORD +14 -0
- matrixcrypto-0.1.0.dist-info/WHEEL +5 -0
- matrixcrypto-0.1.0.dist-info/entry_points.txt +4 -0
- matrixcrypto-0.1.0.dist-info/licenses/LICENSE.md +21 -0
- matrixcrypto-0.1.0.dist-info/top_level.txt +1 -0
matrixcrypto/__init__.py
ADDED
matrixcrypto/__main__.py
ADDED
matrixcrypto/app.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Terminal animation and optional CoinGecko price updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import curses
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import random
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from importlib import resources
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
SETTINGS = {
|
|
20
|
+
"ANIMATION_SPEED": 0.01,
|
|
21
|
+
"BACKGROUND_PATTERN": (1, 2, 3, 1),
|
|
22
|
+
"BACKGROUND_CHARS": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?/",
|
|
23
|
+
"CRYPTO_DISPLAY_COUNT": 3,
|
|
24
|
+
"CRYPTO_DISPLAY_CHANCE": 0.13,
|
|
25
|
+
"BACKGROUND_COLUMN_LENGTH_RANGE": (0.3, 0.6),
|
|
26
|
+
"CRYPTO_FALL_SPEED_RANGE": (0.12, 0.16),
|
|
27
|
+
"BACKGROUND_CHANGE_CHANCE": 0.5,
|
|
28
|
+
"BACKGROUND_FALL_SPEED_RANGE": (0.06, 0.1),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
COLOR_MAP = {
|
|
32
|
+
"red": curses.COLOR_RED,
|
|
33
|
+
"green": curses.COLOR_GREEN,
|
|
34
|
+
"blue": curses.COLOR_BLUE,
|
|
35
|
+
"yellow": curses.COLOR_YELLOW,
|
|
36
|
+
"cyan": curses.COLOR_CYAN,
|
|
37
|
+
"magenta": curses.COLOR_MAGENTA,
|
|
38
|
+
"white": curses.COLOR_WHITE,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
PRICE_URL = "https://api.coingecko.com/api/v3/simple/price"
|
|
42
|
+
PRICE_UPDATE_SECONDS = 75
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
46
|
+
parser = argparse.ArgumentParser(
|
|
47
|
+
description="Matrix-style cryptocurrency terminal display"
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument("--bg-color", choices=COLOR_MAP, default="green")
|
|
50
|
+
parser.add_argument("--crypto-color", choices=COLOR_MAP, default="white")
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--config", type=Path, help="Path to a custom cryptocurrency JSON list"
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--offline", action="store_true", help="Show tickers without fetching prices"
|
|
56
|
+
)
|
|
57
|
+
ecosystem = parser.add_mutually_exclusive_group()
|
|
58
|
+
ecosystem.add_argument(
|
|
59
|
+
"--solana", action="store_true", help="Use the Solana ecosystem list"
|
|
60
|
+
)
|
|
61
|
+
ecosystem.add_argument(
|
|
62
|
+
"--eth", action="store_true", help="Use the Ethereum ecosystem list"
|
|
63
|
+
)
|
|
64
|
+
return parser.parse_args(argv)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def default_config_name(args: argparse.Namespace, offline: bool) -> str:
|
|
68
|
+
if args.solana:
|
|
69
|
+
return "solana_ecosystem_crypto_list.json"
|
|
70
|
+
if args.eth:
|
|
71
|
+
return "ethereum_ecosystem_crypto_list.json"
|
|
72
|
+
if offline:
|
|
73
|
+
return "offline_crypto_list.json"
|
|
74
|
+
return "crypto_list.json"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load_cryptos(config: Path | str) -> list[dict[str, Any]]:
|
|
78
|
+
if isinstance(config, Path):
|
|
79
|
+
with config.open(encoding="utf-8") as file:
|
|
80
|
+
data = json.load(file)
|
|
81
|
+
else:
|
|
82
|
+
resource = resources.files("matrixcrypto").joinpath("data", config)
|
|
83
|
+
with resource.open("r", encoding="utf-8") as file:
|
|
84
|
+
data = json.load(file)
|
|
85
|
+
|
|
86
|
+
if not isinstance(data, dict) or not isinstance(data.get("cryptos"), list):
|
|
87
|
+
raise TypeError("Configuration must contain a 'cryptos' list")
|
|
88
|
+
cryptos = data["cryptos"]
|
|
89
|
+
if not cryptos or any(
|
|
90
|
+
not isinstance(crypto, dict)
|
|
91
|
+
or not isinstance(crypto.get("id"), str)
|
|
92
|
+
or not isinstance(crypto.get("ticker"), str)
|
|
93
|
+
for crypto in cryptos
|
|
94
|
+
):
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"Configuration needs at least one crypto with string 'id' and 'ticker'"
|
|
97
|
+
)
|
|
98
|
+
return cryptos
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def fetch_current_prices(cryptos: list[dict[str, Any]]) -> bool:
|
|
102
|
+
ids = ",".join(crypto["id"] for crypto in cryptos)
|
|
103
|
+
for crypto in cryptos:
|
|
104
|
+
crypto.pop("price", None)
|
|
105
|
+
try:
|
|
106
|
+
response = requests.get(
|
|
107
|
+
PRICE_URL,
|
|
108
|
+
params={"ids": ids, "vs_currencies": "usd"},
|
|
109
|
+
timeout=20,
|
|
110
|
+
)
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
prices = response.json()
|
|
113
|
+
if not isinstance(prices, dict):
|
|
114
|
+
return False
|
|
115
|
+
for crypto in cryptos:
|
|
116
|
+
price_data = prices.get(crypto["id"])
|
|
117
|
+
if not isinstance(price_data, dict):
|
|
118
|
+
continue
|
|
119
|
+
price = price_data.get("usd")
|
|
120
|
+
if (
|
|
121
|
+
isinstance(price, (int, float))
|
|
122
|
+
and not isinstance(price, bool)
|
|
123
|
+
and math.isfinite(price)
|
|
124
|
+
):
|
|
125
|
+
crypto["price"] = f"{price:.2f}" if price < 1000 else f"{price:,.0f}"
|
|
126
|
+
return True
|
|
127
|
+
except (requests.RequestException, ValueError):
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def update_prices_periodically(
|
|
132
|
+
cryptos: list[dict[str, Any]], stop_event: threading.Event
|
|
133
|
+
) -> None:
|
|
134
|
+
while not stop_event.is_set():
|
|
135
|
+
fetch_current_prices(cryptos)
|
|
136
|
+
stop_event.wait(PRICE_UPDATE_SECONDS)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def init_color_pairs(bg_color: str, crypto_color: str) -> None:
|
|
140
|
+
curses.start_color()
|
|
141
|
+
try:
|
|
142
|
+
curses.use_default_colors()
|
|
143
|
+
background = -1
|
|
144
|
+
except curses.error:
|
|
145
|
+
background = curses.COLOR_BLACK
|
|
146
|
+
curses.init_pair(1, COLOR_MAP[bg_color], background)
|
|
147
|
+
curses.init_pair(2, curses.COLOR_WHITE, background)
|
|
148
|
+
curses.init_pair(3, COLOR_MAP[crypto_color], background)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class MatrixColumn:
|
|
152
|
+
def __init__(self, height: int) -> None:
|
|
153
|
+
self.height = height
|
|
154
|
+
self.length = max(
|
|
155
|
+
1, int(random.uniform(*SETTINGS["BACKGROUND_COLUMN_LENGTH_RANGE"]) * height)
|
|
156
|
+
)
|
|
157
|
+
self.chars = [
|
|
158
|
+
random.choice(SETTINGS["BACKGROUND_CHARS"]) for _ in range(self.length)
|
|
159
|
+
]
|
|
160
|
+
self.speed = random.uniform(*SETTINGS["BACKGROUND_FALL_SPEED_RANGE"])
|
|
161
|
+
self.counter = 0.0
|
|
162
|
+
self.top = -self.length
|
|
163
|
+
|
|
164
|
+
def update(self, dt: float) -> None:
|
|
165
|
+
self.counter += dt
|
|
166
|
+
if self.counter < self.speed:
|
|
167
|
+
return
|
|
168
|
+
self.counter = 0.0
|
|
169
|
+
self.top += 1
|
|
170
|
+
if self.top >= self.height:
|
|
171
|
+
self.top = -self.length
|
|
172
|
+
if random.random() < SETTINGS["BACKGROUND_CHANGE_CHANCE"]:
|
|
173
|
+
self.chars[random.randrange(self.length)] = random.choice(
|
|
174
|
+
SETTINGS["BACKGROUND_CHARS"]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def draw(self, stdscr: Any, x: int) -> None:
|
|
178
|
+
for index, char in enumerate(self.chars):
|
|
179
|
+
y = self.top + index
|
|
180
|
+
if 0 <= y < self.height:
|
|
181
|
+
attr = (
|
|
182
|
+
curses.color_pair(2) | curses.A_BOLD
|
|
183
|
+
if index == self.length - 1
|
|
184
|
+
else curses.color_pair(1) | curses.A_DIM
|
|
185
|
+
)
|
|
186
|
+
safe_addstr(stdscr, y, x, char, attr)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class CryptoDisplay:
|
|
190
|
+
def __init__(
|
|
191
|
+
self, crypto: dict[str, Any], x: int, max_y: int, offline: bool
|
|
192
|
+
) -> None:
|
|
193
|
+
self.crypto = crypto
|
|
194
|
+
self.x = x
|
|
195
|
+
self.y = 0
|
|
196
|
+
self.max_y = max_y
|
|
197
|
+
self.offline = offline
|
|
198
|
+
self.speed = random.uniform(*SETTINGS["CRYPTO_FALL_SPEED_RANGE"])
|
|
199
|
+
self.counter = 0.0
|
|
200
|
+
|
|
201
|
+
def update(self, dt: float) -> None:
|
|
202
|
+
self.counter += dt
|
|
203
|
+
if self.counter >= self.speed:
|
|
204
|
+
self.counter = 0.0
|
|
205
|
+
self.y += 1
|
|
206
|
+
|
|
207
|
+
def get_display_text(self) -> str:
|
|
208
|
+
ticker = self.crypto["ticker"].upper()
|
|
209
|
+
return (
|
|
210
|
+
ticker if self.offline else f"{ticker} $ {self.crypto.get('price', 'N/A')}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def is_offscreen(self) -> bool:
|
|
214
|
+
return self.y >= self.max_y
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def safe_addstr(stdscr: Any, y: int, x: int, text: str, attr: int) -> None:
|
|
218
|
+
try:
|
|
219
|
+
stdscr.addstr(y, x, text, attr)
|
|
220
|
+
except curses.error:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def build_columns(height: int, width: int) -> list[tuple[int, MatrixColumn]]:
|
|
225
|
+
columns: list[tuple[int, MatrixColumn]] = []
|
|
226
|
+
x = 0
|
|
227
|
+
for index in range(width):
|
|
228
|
+
if x >= width:
|
|
229
|
+
break
|
|
230
|
+
columns.append((x, MatrixColumn(height)))
|
|
231
|
+
x += (
|
|
232
|
+
1
|
|
233
|
+
+ SETTINGS["BACKGROUND_PATTERN"][
|
|
234
|
+
index % len(SETTINGS["BACKGROUND_PATTERN"])
|
|
235
|
+
]
|
|
236
|
+
)
|
|
237
|
+
return columns
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def run_screen(
|
|
241
|
+
stdscr: Any, cryptos: list[dict[str, Any]], args: argparse.Namespace, offline: bool
|
|
242
|
+
) -> None:
|
|
243
|
+
init_color_pairs(args.bg_color, args.crypto_color)
|
|
244
|
+
try:
|
|
245
|
+
curses.curs_set(0)
|
|
246
|
+
except curses.error:
|
|
247
|
+
pass
|
|
248
|
+
stdscr.nodelay(True)
|
|
249
|
+
|
|
250
|
+
stop_event = threading.Event()
|
|
251
|
+
if not offline:
|
|
252
|
+
threading.Thread(
|
|
253
|
+
target=update_prices_periodically, args=(cryptos, stop_event), daemon=True
|
|
254
|
+
).start()
|
|
255
|
+
|
|
256
|
+
height, width = stdscr.getmaxyx()
|
|
257
|
+
columns = build_columns(height, width)
|
|
258
|
+
displays: list[CryptoDisplay] = []
|
|
259
|
+
last_time = time.monotonic()
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
while True:
|
|
263
|
+
now = time.monotonic()
|
|
264
|
+
dt = now - last_time
|
|
265
|
+
last_time = now
|
|
266
|
+
|
|
267
|
+
current_size = stdscr.getmaxyx()
|
|
268
|
+
if current_size != (height, width):
|
|
269
|
+
height, width = current_size
|
|
270
|
+
columns = build_columns(height, width)
|
|
271
|
+
displays.clear()
|
|
272
|
+
|
|
273
|
+
stdscr.erase()
|
|
274
|
+
for x, column in columns:
|
|
275
|
+
column.update(dt)
|
|
276
|
+
column.draw(stdscr, x)
|
|
277
|
+
|
|
278
|
+
for display in displays[:]:
|
|
279
|
+
display.update(dt)
|
|
280
|
+
if display.is_offscreen():
|
|
281
|
+
displays.remove(display)
|
|
282
|
+
continue
|
|
283
|
+
for index, char in enumerate(display.get_display_text()):
|
|
284
|
+
y = display.y + index
|
|
285
|
+
if y < height:
|
|
286
|
+
safe_addstr(
|
|
287
|
+
stdscr,
|
|
288
|
+
y,
|
|
289
|
+
display.x,
|
|
290
|
+
char,
|
|
291
|
+
curses.color_pair(3) | curses.A_BOLD,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if (
|
|
295
|
+
width
|
|
296
|
+
and len(displays) < SETTINGS["CRYPTO_DISPLAY_COUNT"]
|
|
297
|
+
and random.random() < SETTINGS["CRYPTO_DISPLAY_CHANCE"]
|
|
298
|
+
):
|
|
299
|
+
displays.append(
|
|
300
|
+
CryptoDisplay(
|
|
301
|
+
random.choice(cryptos), random.randrange(width), height, offline
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
stdscr.refresh()
|
|
306
|
+
if stdscr.getch() != -1:
|
|
307
|
+
break
|
|
308
|
+
time.sleep(SETTINGS["ANIMATION_SPEED"])
|
|
309
|
+
finally:
|
|
310
|
+
stop_event.set()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def main(argv: list[str] | None = None, *, offline: bool = False) -> int:
|
|
314
|
+
args = parse_args(argv)
|
|
315
|
+
offline = offline or args.offline
|
|
316
|
+
try:
|
|
317
|
+
cryptos = load_cryptos(args.config or default_config_name(args, offline))
|
|
318
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
319
|
+
print(f"matrixcrypto: cannot load cryptocurrency list: {exc}", file=sys.stderr)
|
|
320
|
+
return 1
|
|
321
|
+
try:
|
|
322
|
+
curses.wrapper(run_screen, cryptos, args, offline)
|
|
323
|
+
except KeyboardInterrupt:
|
|
324
|
+
pass
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def offline_main() -> int:
|
|
329
|
+
return main(offline=True)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cryptos": [
|
|
3
|
+
{
|
|
4
|
+
"id": "bitcoin",
|
|
5
|
+
"ticker": "BTC",
|
|
6
|
+
"name": "Bitcoin"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "ethereum",
|
|
10
|
+
"ticker": "ETH",
|
|
11
|
+
"name": "Ethereum"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "tether",
|
|
15
|
+
"ticker": "USDT",
|
|
16
|
+
"name": "Tether"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "binancecoin",
|
|
20
|
+
"ticker": "BNB",
|
|
21
|
+
"name": "BNB"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "ripple",
|
|
25
|
+
"ticker": "XRP",
|
|
26
|
+
"name": "XRP"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "usd-coin",
|
|
30
|
+
"ticker": "USDC",
|
|
31
|
+
"name": "USDC"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "solana",
|
|
35
|
+
"ticker": "SOL",
|
|
36
|
+
"name": "Solana"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "tron",
|
|
40
|
+
"ticker": "TRX",
|
|
41
|
+
"name": "TRON"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "zcash",
|
|
45
|
+
"ticker": "ZEC",
|
|
46
|
+
"name": "Zcash"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "figure-heloc",
|
|
50
|
+
"ticker": "FIGR_HELOC",
|
|
51
|
+
"name": "Figure Heloc"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "hyperliquid",
|
|
55
|
+
"ticker": "HYPE",
|
|
56
|
+
"name": "Hyperliquid"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "dogecoin",
|
|
60
|
+
"ticker": "DOGE",
|
|
61
|
+
"name": "Dogecoin"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "monero",
|
|
65
|
+
"ticker": "XMR",
|
|
66
|
+
"name": "Monero"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "rain",
|
|
70
|
+
"ticker": "RAIN",
|
|
71
|
+
"name": "Rain"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "whitebit",
|
|
75
|
+
"ticker": "WBT",
|
|
76
|
+
"name": "WhiteBIT Coin"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "usds",
|
|
80
|
+
"ticker": "USDS",
|
|
81
|
+
"name": "USDS"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "chainlink",
|
|
85
|
+
"ticker": "LINK",
|
|
86
|
+
"name": "Chainlink"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "cardano",
|
|
90
|
+
"ticker": "ADA",
|
|
91
|
+
"name": "Cardano"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "leo-token",
|
|
95
|
+
"ticker": "LEO",
|
|
96
|
+
"name": "LEO Token"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "stellar",
|
|
100
|
+
"ticker": "XLM",
|
|
101
|
+
"name": "Stellar"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cryptos": [
|
|
3
|
+
{
|
|
4
|
+
"id": "ethereum",
|
|
5
|
+
"ticker": "ETH",
|
|
6
|
+
"name": "Ethereum"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "tether",
|
|
10
|
+
"ticker": "USDT",
|
|
11
|
+
"name": "Tether"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "binancecoin",
|
|
15
|
+
"ticker": "BNB",
|
|
16
|
+
"name": "BNB"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "usd-coin",
|
|
20
|
+
"ticker": "USDC",
|
|
21
|
+
"name": "USDC"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "staked-ether",
|
|
25
|
+
"ticker": "STETH",
|
|
26
|
+
"name": "Lido Staked Ether"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "wrapped-steth",
|
|
30
|
+
"ticker": "WSTETH",
|
|
31
|
+
"name": "Wrapped stETH"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "whitebit",
|
|
35
|
+
"ticker": "WBT",
|
|
36
|
+
"name": "WhiteBIT Coin"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "wrapped-beacon-eth",
|
|
40
|
+
"ticker": "WBETH",
|
|
41
|
+
"name": "Wrapped Beacon ETH"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "usds",
|
|
45
|
+
"ticker": "USDS",
|
|
46
|
+
"name": "USDS"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "wrapped-bitcoin",
|
|
50
|
+
"ticker": "WBTC",
|
|
51
|
+
"name": "Wrapped Bitcoin"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "chainlink",
|
|
55
|
+
"ticker": "LINK",
|
|
56
|
+
"name": "Chainlink"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "leo-token",
|
|
60
|
+
"ticker": "LEO",
|
|
61
|
+
"name": "LEO Token"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "coinbase-wrapped-btc",
|
|
65
|
+
"ticker": "CBBTC",
|
|
66
|
+
"name": "Coinbase Wrapped BTC"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "aave-v3-weth",
|
|
70
|
+
"ticker": "AWETH",
|
|
71
|
+
"name": "Aave v3 WETH"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "wrapped-eeth",
|
|
75
|
+
"ticker": "WEETH",
|
|
76
|
+
"name": "Wrapped eETH"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "weth",
|
|
80
|
+
"ticker": "WETH",
|
|
81
|
+
"name": "WETH"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "uniswap",
|
|
85
|
+
"ticker": "UNI",
|
|
86
|
+
"name": "Uniswap"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "ethena-usde",
|
|
90
|
+
"ticker": "USDE",
|
|
91
|
+
"name": "Ethena USDe"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "dai",
|
|
95
|
+
"ticker": "DAI",
|
|
96
|
+
"name": "Dai"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "susds",
|
|
100
|
+
"ticker": "SUSDS",
|
|
101
|
+
"name": "sUSDS"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cryptos": [
|
|
3
|
+
{
|
|
4
|
+
"id": "bitcoin",
|
|
5
|
+
"ticker": "BTC",
|
|
6
|
+
"name": "Bitcoin"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "ethereum",
|
|
10
|
+
"ticker": "ETH",
|
|
11
|
+
"name": "Ethereum"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "binancecoin",
|
|
15
|
+
"ticker": "BNB",
|
|
16
|
+
"name": "BNB"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "ripple",
|
|
20
|
+
"ticker": "XRP",
|
|
21
|
+
"name": "XRP"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "solana",
|
|
25
|
+
"ticker": "SOL",
|
|
26
|
+
"name": "Solana"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "tron",
|
|
30
|
+
"ticker": "TRX",
|
|
31
|
+
"name": "TRON"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "zcash",
|
|
35
|
+
"ticker": "ZEC",
|
|
36
|
+
"name": "Zcash"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "hyperliquid",
|
|
40
|
+
"ticker": "HYPE",
|
|
41
|
+
"name": "Hyperliquid"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "dogecoin",
|
|
45
|
+
"ticker": "DOGE",
|
|
46
|
+
"name": "Dogecoin"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "monero",
|
|
50
|
+
"ticker": "XMR",
|
|
51
|
+
"name": "Monero"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "chainlink",
|
|
55
|
+
"ticker": "LINK",
|
|
56
|
+
"name": "Chainlink"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "cardano",
|
|
60
|
+
"ticker": "ADA",
|
|
61
|
+
"name": "Cardano"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cryptos": [
|
|
3
|
+
{
|
|
4
|
+
"id": "tether",
|
|
5
|
+
"ticker": "USDT",
|
|
6
|
+
"name": "Tether"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "usd-coin",
|
|
10
|
+
"ticker": "USDC",
|
|
11
|
+
"name": "USDC"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "solana",
|
|
15
|
+
"ticker": "SOL",
|
|
16
|
+
"name": "Solana"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "usds",
|
|
20
|
+
"ticker": "USDS",
|
|
21
|
+
"name": "USDS"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "wrapped-bitcoin",
|
|
25
|
+
"ticker": "WBTC",
|
|
26
|
+
"name": "Wrapped Bitcoin"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "chainlink",
|
|
30
|
+
"ticker": "LINK",
|
|
31
|
+
"name": "Chainlink"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "coinbase-wrapped-btc",
|
|
35
|
+
"ticker": "CBBTC",
|
|
36
|
+
"name": "Coinbase Wrapped BTC"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "ethena-usde",
|
|
40
|
+
"ticker": "USDE",
|
|
41
|
+
"name": "Ethena USDe"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "usd1-wlfi",
|
|
45
|
+
"ticker": "USD1",
|
|
46
|
+
"name": "USD1"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "global-dollar",
|
|
50
|
+
"ticker": "USDG",
|
|
51
|
+
"name": "Global Dollar"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "paypal-usd",
|
|
55
|
+
"ticker": "PYUSD",
|
|
56
|
+
"name": "PayPal USD"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "hashnote-usyc",
|
|
60
|
+
"ticker": "USYC",
|
|
61
|
+
"name": "Circle USYC"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "blackrock-usd-institutional-digital-liquidity-fund",
|
|
65
|
+
"ticker": "BUIDL",
|
|
66
|
+
"name": "BlackRock USD Institutional Digital Liquidity Fund"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "ondo-us-dollar-yield",
|
|
70
|
+
"ticker": "USDY",
|
|
71
|
+
"name": "Ondo US Dollar Yield"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "ethena",
|
|
75
|
+
"ticker": "ENA",
|
|
76
|
+
"name": "Ethena"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "aave",
|
|
80
|
+
"ticker": "AAVE",
|
|
81
|
+
"name": "Aave"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "pump-fun",
|
|
85
|
+
"ticker": "PUMP",
|
|
86
|
+
"name": "Pump.fun"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "world-liberty-financial",
|
|
90
|
+
"ticker": "WLFI",
|
|
91
|
+
"name": "World Liberty Financial"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "wrapped-solana",
|
|
95
|
+
"ticker": "WSOL",
|
|
96
|
+
"name": "Wrapped SOL"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "usdgo",
|
|
100
|
+
"ticker": "USDGO",
|
|
101
|
+
"name": "USDGO"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
matrixcrypto/lists.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Refresh cryptocurrency lists from the public CoinGecko API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
MARKETS_URL = "https://api.coingecko.com/api/v3/coins/markets"
|
|
14
|
+
DEFAULT_FILES = {
|
|
15
|
+
"all": "crypto_list.json",
|
|
16
|
+
"solana": "solana_ecosystem_crypto_list.json",
|
|
17
|
+
"ethereum": "ethereum_ecosystem_crypto_list.json",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fetch_list(ecosystem: str, limit: int) -> list[dict[str, str]]:
|
|
22
|
+
params: dict[str, Any] = {
|
|
23
|
+
"vs_currency": "usd",
|
|
24
|
+
"order": "market_cap_desc",
|
|
25
|
+
"per_page": limit,
|
|
26
|
+
"page": 1,
|
|
27
|
+
"sparkline": "false",
|
|
28
|
+
}
|
|
29
|
+
if ecosystem != "all":
|
|
30
|
+
params["category"] = f"{ecosystem}-ecosystem"
|
|
31
|
+
response = requests.get(MARKETS_URL, params=params, timeout=20)
|
|
32
|
+
response.raise_for_status()
|
|
33
|
+
data = response.json()
|
|
34
|
+
if not isinstance(data, list) or not data:
|
|
35
|
+
raise ValueError("CoinGecko returned no coins")
|
|
36
|
+
return [
|
|
37
|
+
{"id": coin["id"], "ticker": coin["symbol"].upper(), "name": coin["name"]}
|
|
38
|
+
for coin in data
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main(argv: list[str] | None = None) -> int:
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
description="Fetch a cryptocurrency list from CoinGecko"
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument("--ecosystem", choices=DEFAULT_FILES, default="all")
|
|
47
|
+
parser.add_argument("--limit", type=int, default=20)
|
|
48
|
+
parser.add_argument("--output", type=Path, help="Destination JSON file")
|
|
49
|
+
args = parser.parse_args(argv)
|
|
50
|
+
if not 1 <= args.limit <= 250:
|
|
51
|
+
parser.error("--limit must be between 1 and 250")
|
|
52
|
+
output = args.output or Path(DEFAULT_FILES[args.ecosystem])
|
|
53
|
+
try:
|
|
54
|
+
cryptos = fetch_list(args.ecosystem, args.limit)
|
|
55
|
+
output.write_text(
|
|
56
|
+
json.dumps({"cryptos": cryptos}, indent=2) + "\n", encoding="utf-8"
|
|
57
|
+
)
|
|
58
|
+
except (requests.RequestException, OSError, ValueError, KeyError, TypeError) as exc:
|
|
59
|
+
print(f"matrixcrypto-update-list: {exc}", file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
print(f"Saved {len(cryptos)} cryptocurrencies to {output}")
|
|
62
|
+
return 0
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matrixcrypto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Matrix-style terminal display for cryptocurrency tickers and prices
|
|
5
|
+
Author: bigsk1
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bigsk1/matrix-crypto
|
|
8
|
+
Project-URL: Issues, https://github.com/bigsk1/matrix-crypto/issues
|
|
9
|
+
Keywords: cryptocurrency,terminal,curses,matrix
|
|
10
|
+
Classifier: Environment :: Console :: Curses
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Terminals
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE.md
|
|
22
|
+
Requires-Dist: requests<3,>=2.31
|
|
23
|
+
Requires-Dist: windows-curses<3,>=2.4.2; platform_system == "Windows"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Matrix Crypto
|
|
27
|
+
|
|
28
|
+
A Matrix-style cryptocurrency ticker for your terminal. It displays live USD prices from CoinGecko's public API, or tickers only in offline mode. No API key or `.env` file is needed.
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python 3.12 or newer
|
|
35
|
+
- A terminal with color support. On Windows, `windows-curses` is installed automatically.
|
|
36
|
+
- Network access for live prices. Offline mode does not make API requests.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
After the first PyPI release, install into a virtual environment:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python3.12 -m venv .venv
|
|
44
|
+
source .venv/bin/activate
|
|
45
|
+
python -m pip install matrixcrypto
|
|
46
|
+
matrixcrypto
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
On Windows, create a virtual environment with `py -3.12 -m venv .venv`, activate it with `.venv\Scripts\activate`, then run `python -m pip install matrixcrypto` and `matrixcrypto`.
|
|
50
|
+
|
|
51
|
+
With uv, create a virtual environment and install the same package:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv venv --python 3.12
|
|
55
|
+
uv pip install matrixcrypto
|
|
56
|
+
source .venv/bin/activate
|
|
57
|
+
matrixcrypto
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The installed commands are `matrixcrypto`, `matrixcrypto-offline`, and `matrixcrypto-update-list`. Press any key, including Ctrl+C, to exit the display.
|
|
61
|
+
|
|
62
|
+
## Run from a source checkout
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python3.12 -m venv .venv
|
|
66
|
+
source .venv/bin/activate
|
|
67
|
+
python -m pip install -e .
|
|
68
|
+
matrixcrypto
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The included `run_linux.sh` and `run_windows.bat` scripts also create a local virtual environment and install the checkout. The `install.sh` script remains available for Linux users who prefer the repository installer and its `matrixc` command.
|
|
72
|
+
|
|
73
|
+
## Options
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
matrixcrypto --bg-color red --crypto-color yellow --eth
|
|
77
|
+
matrixcrypto --solana
|
|
78
|
+
matrixcrypto --offline
|
|
79
|
+
matrixcrypto-offline
|
|
80
|
+
matrixcrypto --config ./my-cryptos.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`--bg-color` and `--crypto-color` accept `red`, `green`, `blue`, `yellow`, `cyan`, `magenta`, or `white`. The `--solana` and `--eth` options select bundled ecosystem lists. The display requests fresh prices about every 75 seconds; a coin without a current price shows `N/A`.
|
|
84
|
+
|
|
85
|
+
CoinGecko may rate-limit public requests. If a price request fails, the display shows `N/A` until a later request succeeds.
|
|
86
|
+
|
|
87
|
+
The offline default is a curated set of 12 active tickers from the refreshed overall list. Older entries from the previous offline snapshot, including MATIC, have been removed. You can use `--config` to show any other coins.
|
|
88
|
+
|
|
89
|
+
For a custom list, save a JSON file like this and pass its path with `--config`:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"cryptos": [
|
|
94
|
+
{"id": "bitcoin", "ticker": "BTC", "name": "Bitcoin"},
|
|
95
|
+
{"id": "ethereum", "ticker": "ETH", "name": "Ethereum"}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The `id` values are CoinGecko coin IDs. Run `matrixcrypto --help` for all display options.
|
|
101
|
+
|
|
102
|
+
## Refresh a list
|
|
103
|
+
|
|
104
|
+
The bundled lists are release snapshots. To generate a current list in the working directory, run:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
matrixcrypto-update-list --ecosystem all --output crypto_list.json
|
|
108
|
+
matrixcrypto-update-list --ecosystem solana --output solana.json
|
|
109
|
+
matrixcrypto-update-list --ecosystem ethereum --output ethereum.json
|
|
110
|
+
matrixcrypto --config ./crypto_list.json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The list updater only writes a file after CoinGecko returns a valid, nonempty result. Use `--limit` to change the default of 20 coins. To update a future release's bundled defaults, write the new files under `src/matrixcrypto/data/` in a source checkout and rebuild the package.
|
|
114
|
+
|
|
115
|
+
## Publishing
|
|
116
|
+
|
|
117
|
+
See [RELEASING.md](https://github.com/bigsk1/matrix-crypto/blob/main/RELEASING.md) for the first release checklist and token-safe upload commands.
|
|
118
|
+
|
|
119
|
+
Licensed under MIT. See [LICENSE.md](https://github.com/bigsk1/matrix-crypto/blob/main/LICENSE.md).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
matrixcrypto/__init__.py,sha256=QJMJbpLgGXN9w-wqrh0pECfaWsrD_vyLaPuGrVglkRI,75
|
|
2
|
+
matrixcrypto/__main__.py,sha256=pnwXIEYyOw78r-ksuDq8Jm1d1LyTHZMSzeG3vtUGQ9s,48
|
|
3
|
+
matrixcrypto/app.py,sha256=7xasZD1laTefcBRp6z9-qmQMFtRkxN_oYz0HfedVUeU,10408
|
|
4
|
+
matrixcrypto/lists.py,sha256=rHxNhdBygJVBAEZWdwwX-DUX7V8fHDKAFnjMRutIL6k,2110
|
|
5
|
+
matrixcrypto/data/crypto_list.json,sha256=4ETAdH4jKgcCgpfhTSJBYQ7mGAP7fIN2cOeBO5NNQtA,1698
|
|
6
|
+
matrixcrypto/data/ethereum_ecosystem_crypto_list.json,sha256=9Nem9qYoWOOEh7_4C9leyER7XjxQsuSGmreckrufVtA,1812
|
|
7
|
+
matrixcrypto/data/offline_crypto_list.json,sha256=6nqzyM6TvevVbiwh9MOcsdyYVM_EJD26HOEqnEGNTnQ,1018
|
|
8
|
+
matrixcrypto/data/solana_ecosystem_crypto_list.json,sha256=teZOoWw5xsEdtOG7F6lA2xwu-Z5JZxPle1iKhL3smmU,1918
|
|
9
|
+
matrixcrypto-0.1.0.dist-info/licenses/LICENSE.md,sha256=APFsHfyF2CeEHflz1l0_fp4jgLSAGBW3RO-fkkxdgaY,1063
|
|
10
|
+
matrixcrypto-0.1.0.dist-info/METADATA,sha256=F5Bk35VV2qGT7ZIdLzFFaDr93cTFEtSlj1-byiem9_Y,4640
|
|
11
|
+
matrixcrypto-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
12
|
+
matrixcrypto-0.1.0.dist-info/entry_points.txt,sha256=7oegpArcHYsrS7xsMMPi6FCaX1QhGQGC4cheEDYnyTI,159
|
|
13
|
+
matrixcrypto-0.1.0.dist-info/top_level.txt,sha256=5RnWSahN70Q4CGpZVUWliQ_EJmAVZYByH8FfNAEzyNA,13
|
|
14
|
+
matrixcrypto-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 bigsk1
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
matrixcrypto
|