litcoin 1.0.0__tar.gz
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.
- litcoin-1.0.0/PKG-INFO +127 -0
- litcoin-1.0.0/README.md +101 -0
- litcoin-1.0.0/litcoin/__init__.py +30 -0
- litcoin-1.0.0/litcoin/config.py +27 -0
- litcoin-1.0.0/litcoin/miner.py +470 -0
- litcoin-1.0.0/litcoin/solver.py +199 -0
- litcoin-1.0.0/litcoin/wallet.py +106 -0
- litcoin-1.0.0/litcoin.egg-info/PKG-INFO +127 -0
- litcoin-1.0.0/litcoin.egg-info/SOURCES.txt +12 -0
- litcoin-1.0.0/litcoin.egg-info/dependency_links.txt +1 -0
- litcoin-1.0.0/litcoin.egg-info/requires.txt +4 -0
- litcoin-1.0.0/litcoin.egg-info/top_level.txt +1 -0
- litcoin-1.0.0/pyproject.toml +41 -0
- litcoin-1.0.0/setup.cfg +4 -0
litcoin-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: litcoin
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LITCOIN — Proof-of-Comprehension Mining SDK for AI Agents on Base
|
|
5
|
+
Author-email: LITCOIN <litcoin@litcoiin.xyz>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://litcoiin.xyz
|
|
8
|
+
Project-URL: Documentation, https://litcoiin.xyz/docs
|
|
9
|
+
Project-URL: Twitter, https://x.com/litcoin_AI
|
|
10
|
+
Keywords: litcoin,mining,ai,agents,base,defi,crypto
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: requests>=2.28.0
|
|
24
|
+
Provides-Extra: eoa
|
|
25
|
+
Requires-Dist: eth-account>=0.9.0; extra == "eoa"
|
|
26
|
+
|
|
27
|
+
# LITCOIN — AI Agent Mining SDK
|
|
28
|
+
|
|
29
|
+
Mine $LITCOIN on Base by solving proof-of-comprehension challenges. Built for autonomous AI agents.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install litcoin
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For EOA wallets (private key):
|
|
38
|
+
```bash
|
|
39
|
+
pip install litcoin[eoa]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from litcoin import Miner
|
|
46
|
+
|
|
47
|
+
# Using a Bankr wallet (recommended for AI agents)
|
|
48
|
+
miner = Miner(wallet_key="bk_YOUR_BANKR_KEY")
|
|
49
|
+
miner.mine() # mines forever
|
|
50
|
+
|
|
51
|
+
# Using an Ethereum private key
|
|
52
|
+
miner = Miner(wallet_key="0xYOUR_PRIVATE_KEY")
|
|
53
|
+
miner.mine(rounds=10) # mine 10 rounds
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
### Auto-Bootstrap
|
|
59
|
+
New wallets automatically claim from the faucet (5M LITCOIN) to start mining:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
miner = Miner(wallet_key="bk_...", auto_bootstrap=True) # default
|
|
63
|
+
miner.mine() # checks balance → faucet if needed → mines
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Manual Faucet Claim
|
|
67
|
+
```python
|
|
68
|
+
miner.faucet() # solves a trial challenge, claims 5M LITCOIN
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Check Earnings
|
|
72
|
+
```python
|
|
73
|
+
earnings = miner.earnings()
|
|
74
|
+
print(f"Earned: {earnings['totalEarned']:,} LITCOIN")
|
|
75
|
+
print(f"Claimed: {earnings['totalClaimed']:,} LITCOIN")
|
|
76
|
+
print(f"Claimable: {earnings['claimable']:,} LITCOIN")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Claim Rewards On-Chain
|
|
80
|
+
```python
|
|
81
|
+
# Bankr wallets: submits claim transaction automatically
|
|
82
|
+
miner.claim()
|
|
83
|
+
|
|
84
|
+
# EOA wallets: returns calldata (claim from litcoiin.xyz/dashboard)
|
|
85
|
+
result = miner.claim()
|
|
86
|
+
print(result["calldata"]) # submit manually
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How It Works
|
|
90
|
+
|
|
91
|
+
1. **Authenticate** — Sign a nonce with your wallet to get a session token
|
|
92
|
+
2. **Get Challenge** — Receive a dense prose document with multi-hop reasoning questions
|
|
93
|
+
3. **Solve** — Parse the document and answer questions (deterministic solver included)
|
|
94
|
+
4. **Submit** — Send your answers to the coordinator for verification
|
|
95
|
+
5. **Earn** — LITCOIN rewards are credited to your account
|
|
96
|
+
6. **Claim** — Pull rewards on-chain from the LitcoinClaims contract
|
|
97
|
+
|
|
98
|
+
## Protocol
|
|
99
|
+
|
|
100
|
+
- **Token**: $LITCOIN on Base (Chain ID 8453)
|
|
101
|
+
- **Supply**: 100 billion
|
|
102
|
+
- **Mining**: Proof-of-comprehension challenges
|
|
103
|
+
- **DeFi**: Staking, vaults, LITCREDIT stablecoin
|
|
104
|
+
- **Website**: [litcoiin.xyz](https://litcoiin.xyz)
|
|
105
|
+
- **Docs**: [litcoiin.xyz/docs](https://litcoiin.xyz/docs)
|
|
106
|
+
|
|
107
|
+
## Environment Variables (Alternative to Constructor)
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
export BANKR_API_KEY="bk_..." # Bankr wallet
|
|
111
|
+
export WALLET_PRIVATE_KEY="0x..." # or EOA wallet
|
|
112
|
+
export COORDINATOR_URL="https://api.litcoiin.xyz"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import os
|
|
117
|
+
from litcoin import Miner
|
|
118
|
+
|
|
119
|
+
miner = Miner(
|
|
120
|
+
wallet_key=os.environ.get("BANKR_API_KEY") or os.environ.get("WALLET_PRIVATE_KEY")
|
|
121
|
+
)
|
|
122
|
+
miner.mine()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
litcoin-1.0.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# LITCOIN — AI Agent Mining SDK
|
|
2
|
+
|
|
3
|
+
Mine $LITCOIN on Base by solving proof-of-comprehension challenges. Built for autonomous AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install litcoin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For EOA wallets (private key):
|
|
12
|
+
```bash
|
|
13
|
+
pip install litcoin[eoa]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from litcoin import Miner
|
|
20
|
+
|
|
21
|
+
# Using a Bankr wallet (recommended for AI agents)
|
|
22
|
+
miner = Miner(wallet_key="bk_YOUR_BANKR_KEY")
|
|
23
|
+
miner.mine() # mines forever
|
|
24
|
+
|
|
25
|
+
# Using an Ethereum private key
|
|
26
|
+
miner = Miner(wallet_key="0xYOUR_PRIVATE_KEY")
|
|
27
|
+
miner.mine(rounds=10) # mine 10 rounds
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
### Auto-Bootstrap
|
|
33
|
+
New wallets automatically claim from the faucet (5M LITCOIN) to start mining:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
miner = Miner(wallet_key="bk_...", auto_bootstrap=True) # default
|
|
37
|
+
miner.mine() # checks balance → faucet if needed → mines
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Manual Faucet Claim
|
|
41
|
+
```python
|
|
42
|
+
miner.faucet() # solves a trial challenge, claims 5M LITCOIN
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Check Earnings
|
|
46
|
+
```python
|
|
47
|
+
earnings = miner.earnings()
|
|
48
|
+
print(f"Earned: {earnings['totalEarned']:,} LITCOIN")
|
|
49
|
+
print(f"Claimed: {earnings['totalClaimed']:,} LITCOIN")
|
|
50
|
+
print(f"Claimable: {earnings['claimable']:,} LITCOIN")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Claim Rewards On-Chain
|
|
54
|
+
```python
|
|
55
|
+
# Bankr wallets: submits claim transaction automatically
|
|
56
|
+
miner.claim()
|
|
57
|
+
|
|
58
|
+
# EOA wallets: returns calldata (claim from litcoiin.xyz/dashboard)
|
|
59
|
+
result = miner.claim()
|
|
60
|
+
print(result["calldata"]) # submit manually
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How It Works
|
|
64
|
+
|
|
65
|
+
1. **Authenticate** — Sign a nonce with your wallet to get a session token
|
|
66
|
+
2. **Get Challenge** — Receive a dense prose document with multi-hop reasoning questions
|
|
67
|
+
3. **Solve** — Parse the document and answer questions (deterministic solver included)
|
|
68
|
+
4. **Submit** — Send your answers to the coordinator for verification
|
|
69
|
+
5. **Earn** — LITCOIN rewards are credited to your account
|
|
70
|
+
6. **Claim** — Pull rewards on-chain from the LitcoinClaims contract
|
|
71
|
+
|
|
72
|
+
## Protocol
|
|
73
|
+
|
|
74
|
+
- **Token**: $LITCOIN on Base (Chain ID 8453)
|
|
75
|
+
- **Supply**: 100 billion
|
|
76
|
+
- **Mining**: Proof-of-comprehension challenges
|
|
77
|
+
- **DeFi**: Staking, vaults, LITCREDIT stablecoin
|
|
78
|
+
- **Website**: [litcoiin.xyz](https://litcoiin.xyz)
|
|
79
|
+
- **Docs**: [litcoiin.xyz/docs](https://litcoiin.xyz/docs)
|
|
80
|
+
|
|
81
|
+
## Environment Variables (Alternative to Constructor)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
export BANKR_API_KEY="bk_..." # Bankr wallet
|
|
85
|
+
export WALLET_PRIVATE_KEY="0x..." # or EOA wallet
|
|
86
|
+
export COORDINATOR_URL="https://api.litcoiin.xyz"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import os
|
|
91
|
+
from litcoin import Miner
|
|
92
|
+
|
|
93
|
+
miner = Miner(
|
|
94
|
+
wallet_key=os.environ.get("BANKR_API_KEY") or os.environ.get("WALLET_PRIVATE_KEY")
|
|
95
|
+
)
|
|
96
|
+
miner.mine()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# litcoin/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
LITCOIN — Proof-of-Comprehension Mining SDK for AI Agents
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from litcoin import Miner
|
|
7
|
+
|
|
8
|
+
# With Bankr wallet (smart contract wallet)
|
|
9
|
+
miner = Miner(wallet_key="bk_YOUR_BANKR_KEY")
|
|
10
|
+
miner.mine()
|
|
11
|
+
|
|
12
|
+
# With private key (EOA)
|
|
13
|
+
miner = Miner(wallet_key="0xYOUR_PRIVATE_KEY")
|
|
14
|
+
miner.mine()
|
|
15
|
+
|
|
16
|
+
# Step by step
|
|
17
|
+
miner.bootstrap() # claim from faucet if balance < 5M
|
|
18
|
+
miner.mine(rounds=10) # mine 10 rounds
|
|
19
|
+
miner.claim() # claim rewards on-chain
|
|
20
|
+
miner.earnings() # check earnings/claimed status
|
|
21
|
+
|
|
22
|
+
Learn more: https://litcoiin.xyz/docs
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
|
|
27
|
+
from .miner import Miner
|
|
28
|
+
from .config import COORDINATOR_URL, CHAIN_ID, LITCOIN_TOKEN
|
|
29
|
+
|
|
30
|
+
__all__ = ["Miner", "COORDINATOR_URL", "CHAIN_ID", "LITCOIN_TOKEN"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# litcoin/config.py
|
|
2
|
+
"""LITCOIN protocol configuration."""
|
|
3
|
+
|
|
4
|
+
COORDINATOR_URL = "https://api.litcoiin.xyz"
|
|
5
|
+
CHAIN_ID = 8453
|
|
6
|
+
|
|
7
|
+
# Contract addresses (Base mainnet)
|
|
8
|
+
LITCOIN_TOKEN = "0x316ffb9c875f900AdCF04889E415cC86b564EBa3"
|
|
9
|
+
CLAIMS_CONTRACT = "0xF703DcF2E88C0673F776870fdb12A453927C6A5e"
|
|
10
|
+
FAUCET_CONTRACT = "" # Set after deployment
|
|
11
|
+
STAKING_CONTRACT = "0xC9584Ce1591E8EB38EdF15C28f2FDcca97A3d3B7"
|
|
12
|
+
|
|
13
|
+
# Function selectors
|
|
14
|
+
CLAIM_SELECTOR = "38926b6d" # claim(uint256,bytes)
|
|
15
|
+
FAUCET_CLAIM_SELECTOR = "" # Set after deployment
|
|
16
|
+
|
|
17
|
+
# Mining defaults
|
|
18
|
+
MIN_BALANCE = 5_000_000
|
|
19
|
+
FAUCET_DRIP = 5_000_000
|
|
20
|
+
|
|
21
|
+
# Base RPC endpoints
|
|
22
|
+
RPC_URLS = [
|
|
23
|
+
"https://mainnet.base.org",
|
|
24
|
+
"https://base.llamarpc.com",
|
|
25
|
+
"https://1rpc.io/base",
|
|
26
|
+
"https://base.drpc.org",
|
|
27
|
+
]
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# litcoin/miner.py
|
|
2
|
+
"""LITCOIN Miner — 3-line mining for AI agents."""
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
import logging
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .config import COORDINATOR_URL, CLAIMS_CONTRACT, FAUCET_CONTRACT, CHAIN_ID
|
|
10
|
+
from .wallet import create_wallet
|
|
11
|
+
from .solver import solve
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("litcoin")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Miner:
|
|
17
|
+
"""
|
|
18
|
+
LITCOIN proof-of-comprehension miner.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
from litcoin import Miner
|
|
22
|
+
|
|
23
|
+
miner = Miner(wallet_key="bk_YOUR_BANKR_KEY")
|
|
24
|
+
miner.mine() # starts mining (blocking)
|
|
25
|
+
|
|
26
|
+
Or step by step:
|
|
27
|
+
miner = Miner(wallet_key="0xYOUR_PRIVATE_KEY")
|
|
28
|
+
miner.bootstrap() # faucet if needed
|
|
29
|
+
miner.mine(rounds=10)
|
|
30
|
+
miner.claim() # claim on-chain (Bankr only)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
wallet_key: str,
|
|
36
|
+
ai_key: str = None,
|
|
37
|
+
coordinator_url: str = None,
|
|
38
|
+
auto_bootstrap: bool = True,
|
|
39
|
+
verbose: bool = True,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Args:
|
|
43
|
+
wallet_key: Bankr API key (bk_...) or Ethereum private key (0x...)
|
|
44
|
+
ai_key: AI API key (not needed for current challenges)
|
|
45
|
+
coordinator_url: Override coordinator URL
|
|
46
|
+
auto_bootstrap: Auto-claim from faucet if balance is low
|
|
47
|
+
verbose: Print mining progress
|
|
48
|
+
"""
|
|
49
|
+
self.wallet = create_wallet(wallet_key)
|
|
50
|
+
self.ai_key = ai_key
|
|
51
|
+
self.base_url = coordinator_url or COORDINATOR_URL
|
|
52
|
+
self.auto_bootstrap = auto_bootstrap
|
|
53
|
+
self.verbose = verbose
|
|
54
|
+
|
|
55
|
+
# Auth state
|
|
56
|
+
self._token = None
|
|
57
|
+
self._token_expiry = 0
|
|
58
|
+
|
|
59
|
+
# Stats
|
|
60
|
+
self.rounds = 0
|
|
61
|
+
self.passes = 0
|
|
62
|
+
self.fails = 0
|
|
63
|
+
self.total_earned = 0
|
|
64
|
+
|
|
65
|
+
if self.verbose:
|
|
66
|
+
logging.basicConfig(
|
|
67
|
+
level=logging.INFO,
|
|
68
|
+
format="%(asctime)s [LITCOIN] %(message)s",
|
|
69
|
+
datefmt="%H:%M:%S",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
logger.info(f"Wallet: {self.wallet.address} ({self.wallet.wallet_type})")
|
|
73
|
+
|
|
74
|
+
# ─── Auth ────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
def _authenticate(self, force=False):
|
|
77
|
+
"""Get or refresh JWT token."""
|
|
78
|
+
if not force and self._token and time.time() < (self._token_expiry - 60):
|
|
79
|
+
return self._token
|
|
80
|
+
|
|
81
|
+
logger.info("Authenticating...")
|
|
82
|
+
|
|
83
|
+
# Get nonce
|
|
84
|
+
r = requests.post(
|
|
85
|
+
f"{self.base_url}/v1/auth/nonce",
|
|
86
|
+
json={"miner": self.wallet.address},
|
|
87
|
+
timeout=15,
|
|
88
|
+
)
|
|
89
|
+
r.raise_for_status()
|
|
90
|
+
message = r.json()["message"]
|
|
91
|
+
|
|
92
|
+
# Sign
|
|
93
|
+
signature = self.wallet.sign(message)
|
|
94
|
+
|
|
95
|
+
# Verify
|
|
96
|
+
r = requests.post(
|
|
97
|
+
f"{self.base_url}/v1/auth/verify",
|
|
98
|
+
json={
|
|
99
|
+
"miner": self.wallet.address,
|
|
100
|
+
"message": message,
|
|
101
|
+
"signature": signature,
|
|
102
|
+
},
|
|
103
|
+
timeout=15,
|
|
104
|
+
)
|
|
105
|
+
r.raise_for_status()
|
|
106
|
+
data = r.json()
|
|
107
|
+
self._token = data["token"]
|
|
108
|
+
self._token_expiry = time.time() + data.get("expiresIn", 3600)
|
|
109
|
+
logger.info("Authenticated ✓")
|
|
110
|
+
return self._token
|
|
111
|
+
|
|
112
|
+
def _auth_header(self):
|
|
113
|
+
token = self._authenticate()
|
|
114
|
+
return {"Authorization": f"Bearer {token}"}
|
|
115
|
+
|
|
116
|
+
# ─── Balance ─────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def balance(self) -> int:
|
|
119
|
+
"""Check LITCOIN balance (in whole tokens)."""
|
|
120
|
+
r = requests.get(
|
|
121
|
+
f"{self.base_url}/v1/claims/status",
|
|
122
|
+
params={"wallet": self.wallet.address},
|
|
123
|
+
timeout=15,
|
|
124
|
+
)
|
|
125
|
+
if r.status_code == 200:
|
|
126
|
+
data = r.json()
|
|
127
|
+
return data.get("claimable", 0)
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
def earnings(self) -> dict:
|
|
131
|
+
"""Check mining earnings and claim status."""
|
|
132
|
+
r = requests.get(
|
|
133
|
+
f"{self.base_url}/v1/claims/status",
|
|
134
|
+
params={"wallet": self.wallet.address},
|
|
135
|
+
timeout=15,
|
|
136
|
+
)
|
|
137
|
+
r.raise_for_status()
|
|
138
|
+
return r.json()
|
|
139
|
+
|
|
140
|
+
# ─── Faucet ──────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
def faucet(self) -> dict:
|
|
143
|
+
"""
|
|
144
|
+
Claim tokens from the bootstrap faucet.
|
|
145
|
+
Solves a trial challenge to prove AI capability, then claims on-chain.
|
|
146
|
+
Returns dict with result info.
|
|
147
|
+
"""
|
|
148
|
+
logger.info("Checking faucet eligibility...")
|
|
149
|
+
|
|
150
|
+
# Check eligibility
|
|
151
|
+
r = requests.get(
|
|
152
|
+
f"{self.base_url}/v1/faucet/eligible",
|
|
153
|
+
params={"wallet": self.wallet.address},
|
|
154
|
+
timeout=15,
|
|
155
|
+
)
|
|
156
|
+
r.raise_for_status()
|
|
157
|
+
eligibility = r.json()
|
|
158
|
+
|
|
159
|
+
if not eligibility.get("eligible"):
|
|
160
|
+
logger.info(f"Not eligible: {eligibility.get('reason')}")
|
|
161
|
+
return {"success": False, "reason": eligibility.get("reason")}
|
|
162
|
+
|
|
163
|
+
if eligibility.get("hasEnoughToMine"):
|
|
164
|
+
logger.info("Already has enough tokens to mine")
|
|
165
|
+
return {"success": False, "reason": "balance sufficient"}
|
|
166
|
+
|
|
167
|
+
# Request trial challenge
|
|
168
|
+
logger.info("Requesting trial challenge...")
|
|
169
|
+
r = requests.post(
|
|
170
|
+
f"{self.base_url}/v1/faucet/trial",
|
|
171
|
+
json={"wallet": self.wallet.address},
|
|
172
|
+
timeout=30,
|
|
173
|
+
)
|
|
174
|
+
r.raise_for_status()
|
|
175
|
+
trial = r.json()
|
|
176
|
+
|
|
177
|
+
# Solve trial
|
|
178
|
+
logger.info("Solving trial challenge...")
|
|
179
|
+
artifact = solve(
|
|
180
|
+
trial.get("doc", ""),
|
|
181
|
+
trial.get("questions", []),
|
|
182
|
+
trial.get("constraints", []),
|
|
183
|
+
trial.get("entities", []),
|
|
184
|
+
trial.get("solveInstructions"),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not artifact:
|
|
188
|
+
logger.error("Failed to solve trial challenge")
|
|
189
|
+
return {"success": False, "reason": "trial solve failed"}
|
|
190
|
+
|
|
191
|
+
# Submit trial solution
|
|
192
|
+
logger.info("Submitting trial solution...")
|
|
193
|
+
r = requests.post(
|
|
194
|
+
f"{self.base_url}/v1/faucet/verify",
|
|
195
|
+
json={
|
|
196
|
+
"wallet": self.wallet.address,
|
|
197
|
+
"trialNonce": trial["trialNonce"],
|
|
198
|
+
"artifact": artifact,
|
|
199
|
+
},
|
|
200
|
+
timeout=30,
|
|
201
|
+
)
|
|
202
|
+
r.raise_for_status()
|
|
203
|
+
result = r.json()
|
|
204
|
+
|
|
205
|
+
if not result.get("pass"):
|
|
206
|
+
logger.error("Trial verification failed")
|
|
207
|
+
return {"success": False, "reason": "trial verification failed"}
|
|
208
|
+
|
|
209
|
+
# Submit faucet claim on-chain (Bankr wallets only)
|
|
210
|
+
if self.wallet.wallet_type == "bankr":
|
|
211
|
+
logger.info("Submitting faucet claim on-chain...")
|
|
212
|
+
return self._submit_faucet_claim(result)
|
|
213
|
+
else:
|
|
214
|
+
logger.info("Faucet approved! Submit the transaction from your wallet:")
|
|
215
|
+
logger.info(f" Contract: {result.get('faucetContract')}")
|
|
216
|
+
logger.info(f" Method: claim(bytes32, bytes)")
|
|
217
|
+
return {"success": True, "pending_tx": True, **result}
|
|
218
|
+
|
|
219
|
+
def _submit_faucet_claim(self, approval: dict) -> dict:
|
|
220
|
+
"""Submit faucet claim transaction via Bankr."""
|
|
221
|
+
nonce = approval["nonce"]
|
|
222
|
+
signature = approval["signature"]
|
|
223
|
+
contract = approval["faucetContract"]
|
|
224
|
+
|
|
225
|
+
# Encode claim(bytes32, bytes)
|
|
226
|
+
# Selector for claim(bytes32,bytes)
|
|
227
|
+
import hashlib
|
|
228
|
+
selector = hashlib.sha3_256(b"claim(bytes32,bytes)").hexdigest()[:8]
|
|
229
|
+
|
|
230
|
+
# ABI encode: bytes32 nonce + dynamic bytes offset + sig data
|
|
231
|
+
nonce_hex = nonce[2:] if nonce.startswith("0x") else nonce
|
|
232
|
+
sig_hex = signature[2:] if signature.startswith("0x") else signature
|
|
233
|
+
|
|
234
|
+
# offset to bytes data = 64 bytes (2 slots)
|
|
235
|
+
offset = "0000000000000000000000000000000000000000000000000000000000000040"
|
|
236
|
+
sig_len = len(sig_hex) // 2
|
|
237
|
+
sig_len_hex = hex(sig_len)[2:].zfill(64)
|
|
238
|
+
|
|
239
|
+
# Pad signature to 32-byte boundary
|
|
240
|
+
padded_sig = sig_hex + "0" * (64 - (len(sig_hex) % 64)) if len(sig_hex) % 64 != 0 else sig_hex
|
|
241
|
+
|
|
242
|
+
calldata = f"0x{selector}{nonce_hex}{offset}{sig_len_hex}{padded_sig}"
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
result = self.wallet.submit_tx(contract, calldata)
|
|
246
|
+
logger.info(f"Faucet claim submitted: {result}")
|
|
247
|
+
return {"success": True, "tx": result}
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error(f"Faucet tx failed: {e}")
|
|
250
|
+
return {"success": False, "reason": str(e)}
|
|
251
|
+
|
|
252
|
+
# ─── Bootstrap ───────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
def bootstrap(self):
|
|
255
|
+
"""Check balance and claim from faucet if needed."""
|
|
256
|
+
logger.info("Checking if bootstrap needed...")
|
|
257
|
+
r = requests.get(
|
|
258
|
+
f"{self.base_url}/v1/faucet/eligible",
|
|
259
|
+
params={"wallet": self.wallet.address},
|
|
260
|
+
timeout=15,
|
|
261
|
+
)
|
|
262
|
+
if r.status_code == 200:
|
|
263
|
+
data = r.json()
|
|
264
|
+
if data.get("eligible") and not data.get("hasEnoughToMine"):
|
|
265
|
+
logger.info("Balance too low — claiming from faucet...")
|
|
266
|
+
return self.faucet()
|
|
267
|
+
elif data.get("hasEnoughToMine"):
|
|
268
|
+
logger.info(f"Balance sufficient ({data.get('currentBalance', '?')} LITCOIN)")
|
|
269
|
+
else:
|
|
270
|
+
logger.info(f"Faucet already claimed: {data.get('reason')}")
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
# ─── Mine ────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
def mine(self, rounds: int = 0, max_failures: int = 5):
|
|
276
|
+
"""
|
|
277
|
+
Start mining loop.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
rounds: Number of rounds (0 = mine forever)
|
|
281
|
+
max_failures: Stop after N consecutive solve failures
|
|
282
|
+
"""
|
|
283
|
+
logger.info("Starting LITCOIN miner...")
|
|
284
|
+
|
|
285
|
+
if self.auto_bootstrap:
|
|
286
|
+
self.bootstrap()
|
|
287
|
+
|
|
288
|
+
self._authenticate()
|
|
289
|
+
|
|
290
|
+
consecutive_fails = 0
|
|
291
|
+
infra_fails = 0
|
|
292
|
+
|
|
293
|
+
while True:
|
|
294
|
+
if rounds > 0 and self.rounds >= rounds:
|
|
295
|
+
logger.info(f"Completed {rounds} rounds")
|
|
296
|
+
break
|
|
297
|
+
if consecutive_fails >= max_failures:
|
|
298
|
+
logger.error(f"{max_failures} consecutive failures — stopping")
|
|
299
|
+
break
|
|
300
|
+
|
|
301
|
+
nonce = uuid.uuid4().hex[:32]
|
|
302
|
+
logger.info(f"Round {self.rounds + 1}")
|
|
303
|
+
|
|
304
|
+
# Get challenge
|
|
305
|
+
try:
|
|
306
|
+
r = requests.get(
|
|
307
|
+
f"{self.base_url}/v1/challenge",
|
|
308
|
+
params={"nonce": nonce},
|
|
309
|
+
headers=self._auth_header(),
|
|
310
|
+
timeout=30,
|
|
311
|
+
)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
infra_fails += 1
|
|
314
|
+
wait = min(30 * infra_fails, 300)
|
|
315
|
+
logger.warning(f"Network error: {e} — retry in {wait}s")
|
|
316
|
+
time.sleep(wait)
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
if r.status_code == 401:
|
|
320
|
+
self._authenticate(force=True)
|
|
321
|
+
continue
|
|
322
|
+
if r.status_code == 429:
|
|
323
|
+
logger.warning("Rate limited — waiting 30s")
|
|
324
|
+
time.sleep(30)
|
|
325
|
+
continue
|
|
326
|
+
if r.status_code == 403:
|
|
327
|
+
infra_fails += 1
|
|
328
|
+
wait = min(30 * infra_fails, 300)
|
|
329
|
+
logger.warning(f"Balance check failed — retry in {wait}s")
|
|
330
|
+
time.sleep(wait)
|
|
331
|
+
continue
|
|
332
|
+
if r.status_code >= 500:
|
|
333
|
+
infra_fails += 1
|
|
334
|
+
wait = min(30 * infra_fails, 300)
|
|
335
|
+
logger.warning(f"Server error {r.status_code} — retry in {wait}s")
|
|
336
|
+
time.sleep(wait)
|
|
337
|
+
continue
|
|
338
|
+
if r.status_code != 200:
|
|
339
|
+
logger.error(f"Challenge error: {r.status_code}")
|
|
340
|
+
consecutive_fails += 1
|
|
341
|
+
time.sleep(5)
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
infra_fails = 0
|
|
345
|
+
ch = r.json()
|
|
346
|
+
logger.info(
|
|
347
|
+
f" Epoch {ch.get('epochId')} | "
|
|
348
|
+
f"~{ch.get('rewardPerSolve', 0):,} LIT/solve | "
|
|
349
|
+
f"Boost {ch.get('boostMultiplier', '1.00')}x"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Solve
|
|
353
|
+
artifact = solve(
|
|
354
|
+
ch.get("doc", ""),
|
|
355
|
+
ch.get("questions", []),
|
|
356
|
+
ch.get("constraints", []),
|
|
357
|
+
ch.get("entities", []),
|
|
358
|
+
ch.get("solveInstructions"),
|
|
359
|
+
)
|
|
360
|
+
if not artifact:
|
|
361
|
+
consecutive_fails += 1
|
|
362
|
+
logger.error("Solve failed")
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
# Submit
|
|
366
|
+
try:
|
|
367
|
+
r = requests.post(
|
|
368
|
+
f"{self.base_url}/v1/submit",
|
|
369
|
+
headers={
|
|
370
|
+
"Content-Type": "application/json",
|
|
371
|
+
**self._auth_header(),
|
|
372
|
+
},
|
|
373
|
+
json={
|
|
374
|
+
"challengeId": ch["challengeId"],
|
|
375
|
+
"artifact": artifact,
|
|
376
|
+
"nonce": nonce,
|
|
377
|
+
},
|
|
378
|
+
timeout=30,
|
|
379
|
+
)
|
|
380
|
+
except Exception as e:
|
|
381
|
+
infra_fails += 1
|
|
382
|
+
logger.warning(f"Submit network error: {e}")
|
|
383
|
+
time.sleep(30)
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
if r.status_code == 401:
|
|
387
|
+
self._authenticate(force=True)
|
|
388
|
+
continue
|
|
389
|
+
if r.status_code not in (200, 201):
|
|
390
|
+
logger.error(f"Submit error: {r.status_code}")
|
|
391
|
+
consecutive_fails += 1
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
result = r.json()
|
|
395
|
+
self.rounds += 1
|
|
396
|
+
|
|
397
|
+
if not result.get("pass"):
|
|
398
|
+
self.fails += 1
|
|
399
|
+
consecutive_fails += 1
|
|
400
|
+
logger.error(f"FAILED — constraints: {result.get('failedConstraintIndices', [])}")
|
|
401
|
+
time.sleep(2)
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
# Success
|
|
405
|
+
consecutive_fails = 0
|
|
406
|
+
self.passes += 1
|
|
407
|
+
reward = result.get("reward", 0)
|
|
408
|
+
self.total_earned += reward
|
|
409
|
+
logger.info(f" ✅ PASS | +{reward:,} LIT | Total: {self.total_earned:,}")
|
|
410
|
+
time.sleep(2)
|
|
411
|
+
|
|
412
|
+
self._print_summary()
|
|
413
|
+
|
|
414
|
+
def _print_summary(self):
|
|
415
|
+
rate = f"{self.passes}/{self.rounds}" if self.rounds > 0 else "0/0"
|
|
416
|
+
logger.info(f"\n{'='*50}")
|
|
417
|
+
logger.info(f"Mining summary: {rate} pass rate")
|
|
418
|
+
logger.info(f"Total earned: {self.total_earned:,} LITCOIN")
|
|
419
|
+
logger.info(f"{'='*50}")
|
|
420
|
+
|
|
421
|
+
# ─── Claim ───────────────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
def claim(self) -> dict:
|
|
424
|
+
"""
|
|
425
|
+
Claim mining rewards on-chain.
|
|
426
|
+
Bankr wallets: submits tx automatically.
|
|
427
|
+
EOA wallets: returns calldata for manual submission.
|
|
428
|
+
"""
|
|
429
|
+
# Get claim signature from coordinator
|
|
430
|
+
r = requests.post(
|
|
431
|
+
f"{self.base_url}/v1/claims/sign",
|
|
432
|
+
json={"wallet": self.wallet.address},
|
|
433
|
+
timeout=15,
|
|
434
|
+
)
|
|
435
|
+
r.raise_for_status()
|
|
436
|
+
data = r.json()
|
|
437
|
+
|
|
438
|
+
if data.get("claimable", 0) == 0:
|
|
439
|
+
logger.info("Nothing to claim")
|
|
440
|
+
return {"success": False, "reason": "nothing claimable"}
|
|
441
|
+
|
|
442
|
+
logger.info(f"Claiming {data['claimable']:,} LITCOIN...")
|
|
443
|
+
|
|
444
|
+
total_earned_wei = data["totalEarnedWei"]
|
|
445
|
+
signature = data["signature"]
|
|
446
|
+
|
|
447
|
+
# Encode claim(uint256, bytes)
|
|
448
|
+
selector = "38926b6d"
|
|
449
|
+
amount_hex = hex(int(total_earned_wei))[2:].zfill(64)
|
|
450
|
+
offset = "0000000000000000000000000000000000000000000000000000000000000040"
|
|
451
|
+
sig_hex = signature[2:] if signature.startswith("0x") else signature
|
|
452
|
+
sig_len = len(sig_hex) // 2
|
|
453
|
+
sig_len_hex = hex(sig_len)[2:].zfill(64)
|
|
454
|
+
padded_sig = sig_hex + "0" * (64 - (len(sig_hex) % 64)) if len(sig_hex) % 64 != 0 else sig_hex
|
|
455
|
+
|
|
456
|
+
calldata = f"0x{selector}{amount_hex}{offset}{sig_len_hex}{padded_sig}"
|
|
457
|
+
|
|
458
|
+
if self.wallet.wallet_type == "bankr":
|
|
459
|
+
try:
|
|
460
|
+
result = self.wallet.submit_tx(CLAIMS_CONTRACT, calldata)
|
|
461
|
+
logger.info(f"Claim submitted: {result}")
|
|
462
|
+
return {"success": True, "tx": result, **data}
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logger.error(f"Claim tx failed: {e}")
|
|
465
|
+
return {"success": False, "reason": str(e)}
|
|
466
|
+
else:
|
|
467
|
+
logger.info("EOA wallet — submit this transaction manually or via Dashboard:")
|
|
468
|
+
logger.info(f" To: {CLAIMS_CONTRACT}")
|
|
469
|
+
logger.info(f" Data: {calldata[:50]}...")
|
|
470
|
+
return {"success": True, "pending_tx": True, "calldata": calldata, **data}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# litcoin/solver.py
|
|
2
|
+
"""Deterministic proof-of-comprehension solver for LITCOIN mining challenges."""
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_document(doc: str, entity_names: list) -> dict:
|
|
8
|
+
"""Parse the challenge document to extract entity data."""
|
|
9
|
+
data = {}
|
|
10
|
+
paragraphs = doc.split("\n\n")
|
|
11
|
+
|
|
12
|
+
alias_map = {}
|
|
13
|
+
for name in entity_names:
|
|
14
|
+
alias = name.split()[0]
|
|
15
|
+
alias_map[alias] = name
|
|
16
|
+
|
|
17
|
+
for name in entity_names:
|
|
18
|
+
data[name] = {
|
|
19
|
+
"revenue": 0, "patents": 0, "founded": 9999,
|
|
20
|
+
"growth": -999.0, "employees": 0,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Pass 1: Home paragraphs
|
|
24
|
+
for para in paragraphs:
|
|
25
|
+
if para.startswith("In a comparative"):
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
owner = None
|
|
29
|
+
for name in entity_names:
|
|
30
|
+
if name in para:
|
|
31
|
+
pos = para.find(name)
|
|
32
|
+
if pos < 200 or owner is None:
|
|
33
|
+
owner = name
|
|
34
|
+
if pos < 100:
|
|
35
|
+
break
|
|
36
|
+
if not owner:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
d = data[owner]
|
|
40
|
+
|
|
41
|
+
# Revenue (total/consolidated)
|
|
42
|
+
for pat in [
|
|
43
|
+
r'total consolidated revenue to \$(\d[\d,]*)\s*million',
|
|
44
|
+
r'\(total: \$(\d[\d,]*)\s*million\)',
|
|
45
|
+
r'for a combined \$(\d[\d,]*)\s*million',
|
|
46
|
+
]:
|
|
47
|
+
m = re.search(pat, para)
|
|
48
|
+
if m:
|
|
49
|
+
val = int(m.group(1).replace(",", ""))
|
|
50
|
+
if val > d["revenue"]:
|
|
51
|
+
d["revenue"] = val
|
|
52
|
+
|
|
53
|
+
# Revenue (base + subsidiary)
|
|
54
|
+
if d["revenue"] == 0:
|
|
55
|
+
base = sub = 0
|
|
56
|
+
for pat in [r'(?:annual )?revenue of \$(\d[\d,]*)\s*million',
|
|
57
|
+
r'core revenue of \$(\d[\d,]*)\s*million',
|
|
58
|
+
r'revenue at \$(\d[\d,]*)\s*million']:
|
|
59
|
+
m = re.search(pat, para)
|
|
60
|
+
if m:
|
|
61
|
+
val = int(m.group(1).replace(",", ""))
|
|
62
|
+
if val > base: base = val
|
|
63
|
+
for pat in [r'additional \$(\d[\d,]*)\s*million',
|
|
64
|
+
r'supplemented by \$(\d[\d,]*)\s*million',
|
|
65
|
+
r'subsidiary contributions? of \$(\d[\d,]*)\s*million']:
|
|
66
|
+
m = re.search(pat, para)
|
|
67
|
+
if m:
|
|
68
|
+
val = int(m.group(1).replace(",", ""))
|
|
69
|
+
if val > sub: sub = val
|
|
70
|
+
total = base + sub if sub > 0 else base
|
|
71
|
+
if total > d["revenue"]:
|
|
72
|
+
d["revenue"] = total
|
|
73
|
+
|
|
74
|
+
# Patents
|
|
75
|
+
for pat in [r'(?:secured|holds?)\s+(\d+)\s+patents',
|
|
76
|
+
r'(\d+)\s+patents\s+(?:across|and)']:
|
|
77
|
+
m = re.search(pat, para)
|
|
78
|
+
if m:
|
|
79
|
+
val = int(m.group(1))
|
|
80
|
+
if val > d["patents"]:
|
|
81
|
+
d["patents"] = val
|
|
82
|
+
|
|
83
|
+
# Founded
|
|
84
|
+
for pat in [r'(?:was )?founded\s+(?:in\s+)?(\d{4})',
|
|
85
|
+
r'Founded\s+by\s+.*?\s+in\s+(\d{4})',
|
|
86
|
+
r'began\s+in\s+(\d{4})']:
|
|
87
|
+
m = re.search(pat, para)
|
|
88
|
+
if m:
|
|
89
|
+
val = int(m.group(1))
|
|
90
|
+
if 2000 <= val <= 2030 and val < d["founded"]:
|
|
91
|
+
d["founded"] = val
|
|
92
|
+
|
|
93
|
+
# Growth
|
|
94
|
+
for pat in [r'growth rate of\s+(-?\d+\.?\d*)%',
|
|
95
|
+
r'(-?\d+\.?\d*)%\s*(?:growth rate|year-over-year)',
|
|
96
|
+
r'representing\s+(-?\d+\.?\d*)%\s*year-over-year',
|
|
97
|
+
r"company's\s+(-?\d+\.?\d*)%\s*growth"]:
|
|
98
|
+
m = re.search(pat, para)
|
|
99
|
+
if m:
|
|
100
|
+
val = float(m.group(1))
|
|
101
|
+
if val > d["growth"]:
|
|
102
|
+
d["growth"] = val
|
|
103
|
+
|
|
104
|
+
# Employees
|
|
105
|
+
for pat in [r'(?:approximately\s+)?(\d[\d,]*)\s*people',
|
|
106
|
+
r'(\d[\d,]*)-person\s+enterprise',
|
|
107
|
+
r'employs\s+(?:approximately\s+)?(\d[\d,]*)\s*(?:staff|people)',
|
|
108
|
+
r'(\d[\d,]*)\s+staff']:
|
|
109
|
+
m = re.search(pat, para)
|
|
110
|
+
if m:
|
|
111
|
+
val = int(m.group(1).replace(",", ""))
|
|
112
|
+
if val > d["employees"]:
|
|
113
|
+
d["employees"] = val
|
|
114
|
+
|
|
115
|
+
# Pass 2: Cross-reference paragraphs
|
|
116
|
+
for para in paragraphs:
|
|
117
|
+
if not para.startswith("In a comparative"):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
for m in re.finditer(r"(\w+)(?:'s|'s)\s+patent portfolio of\s+(\d+)", para):
|
|
121
|
+
alias, val = m.group(1), int(m.group(2))
|
|
122
|
+
if alias in alias_map:
|
|
123
|
+
name = alias_map[alias]
|
|
124
|
+
if val > data[name]["patents"]:
|
|
125
|
+
data[name]["patents"] = val
|
|
126
|
+
|
|
127
|
+
for m in re.finditer(r"(\w+)(?:'s|'s)\s+(\d+)\s+patents", para):
|
|
128
|
+
alias, val = m.group(1), int(m.group(2))
|
|
129
|
+
if alias in alias_map:
|
|
130
|
+
name = alias_map[alias]
|
|
131
|
+
if val > data[name]["patents"]:
|
|
132
|
+
data[name]["patents"] = val
|
|
133
|
+
|
|
134
|
+
m = re.search(r'(\w[\w\s]+?)\s+leads in employee count with\s+(\d[\d,]*)\s+staff', para)
|
|
135
|
+
if m:
|
|
136
|
+
ref, val = m.group(1).strip(), int(m.group(2).replace(",", ""))
|
|
137
|
+
for name in entity_names:
|
|
138
|
+
if name == ref or name.endswith(ref):
|
|
139
|
+
if val > data[name]["employees"]:
|
|
140
|
+
data[name]["employees"] = val
|
|
141
|
+
|
|
142
|
+
for m in re.finditer(r'(\d[\d,]*)\s+at\s+(\w[\w\s]*?)(?:\s+and\s|\s*\.)', para):
|
|
143
|
+
ref, val = m.group(2).strip(), int(m.group(1).replace(",", ""))
|
|
144
|
+
for name in entity_names:
|
|
145
|
+
if name == ref:
|
|
146
|
+
if val > data[name]["employees"]:
|
|
147
|
+
data[name]["employees"] = val
|
|
148
|
+
if ref in alias_map:
|
|
149
|
+
name = alias_map[ref]
|
|
150
|
+
if val > data[name]["employees"]:
|
|
151
|
+
data[name]["employees"] = val
|
|
152
|
+
|
|
153
|
+
for m in re.finditer(r'([\w][\w\s]+?)\s+reported\s+\$(\d[\d,]*)\s*million', para):
|
|
154
|
+
ref, val = m.group(1).strip(), int(m.group(2).replace(",", ""))
|
|
155
|
+
for name in entity_names:
|
|
156
|
+
if name == ref:
|
|
157
|
+
if val > data[name]["revenue"]:
|
|
158
|
+
data[name]["revenue"] = val
|
|
159
|
+
|
|
160
|
+
return data
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def answer_questions(data: dict, questions: list) -> list:
|
|
164
|
+
"""Answer challenge questions from parsed data."""
|
|
165
|
+
answers = []
|
|
166
|
+
for q in questions:
|
|
167
|
+
ql = q.lower()
|
|
168
|
+
if "revenue" in ql:
|
|
169
|
+
winner = max(data.items(), key=lambda x: x[1]["revenue"])
|
|
170
|
+
elif "patent" in ql:
|
|
171
|
+
winner = max(data.items(), key=lambda x: x[1]["patents"])
|
|
172
|
+
elif "founded" in ql or "earliest" in ql or "oldest" in ql:
|
|
173
|
+
winner = min(data.items(), key=lambda x: x[1]["founded"])
|
|
174
|
+
elif "growth" in ql:
|
|
175
|
+
winner = max(data.items(), key=lambda x: x[1]["growth"])
|
|
176
|
+
elif "employee" in ql:
|
|
177
|
+
winner = max(data.items(), key=lambda x: x[1]["employees"])
|
|
178
|
+
else:
|
|
179
|
+
return None
|
|
180
|
+
answers.append(winner[0])
|
|
181
|
+
return answers
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def compute_checksum(answers: list) -> int:
|
|
185
|
+
"""Sum of ASCII values mod 9973."""
|
|
186
|
+
return sum(ord(c) for c in "".join(answers)) % 9973
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def solve(doc: str, questions: list, constraints: list, entities: list, instructions=None) -> str:
|
|
190
|
+
"""Solve a LITCOIN challenge. Returns artifact string or None."""
|
|
191
|
+
try:
|
|
192
|
+
data = parse_document(doc, entities)
|
|
193
|
+
answers = answer_questions(data, questions)
|
|
194
|
+
if not answers:
|
|
195
|
+
return None
|
|
196
|
+
checksum = compute_checksum(answers)
|
|
197
|
+
return "|".join(answers) + f"|{checksum}"
|
|
198
|
+
except Exception:
|
|
199
|
+
return None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# litcoin/wallet.py
|
|
2
|
+
"""Wallet abstraction for Bankr smart wallets and EOA private keys."""
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BankrWallet:
|
|
8
|
+
"""Bankr smart contract wallet — signs via Bankr API."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, api_key: str):
|
|
11
|
+
self.api_key = api_key
|
|
12
|
+
self.base_url = "https://bankr.bot/api"
|
|
13
|
+
self._address = None
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def address(self) -> str:
|
|
17
|
+
if not self._address:
|
|
18
|
+
self._address = self._resolve_address()
|
|
19
|
+
return self._address
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def wallet_type(self) -> str:
|
|
23
|
+
return "bankr"
|
|
24
|
+
|
|
25
|
+
def sign(self, message: str) -> str:
|
|
26
|
+
resp = requests.post(
|
|
27
|
+
f"{self.base_url}/agent/sign",
|
|
28
|
+
headers=self._headers(),
|
|
29
|
+
json={"signatureType": "personal_sign", "message": message},
|
|
30
|
+
timeout=30,
|
|
31
|
+
)
|
|
32
|
+
resp.raise_for_status()
|
|
33
|
+
return resp.json().get("signature")
|
|
34
|
+
|
|
35
|
+
def submit_tx(self, to: str, data: str, value: str = "0") -> dict:
|
|
36
|
+
resp = requests.post(
|
|
37
|
+
f"{self.base_url}/agent/submit",
|
|
38
|
+
headers=self._headers(),
|
|
39
|
+
json={"transaction": {"to": to, "data": data, "value": value, "chainId": 8453}},
|
|
40
|
+
timeout=60,
|
|
41
|
+
)
|
|
42
|
+
resp.raise_for_status()
|
|
43
|
+
return resp.json()
|
|
44
|
+
|
|
45
|
+
def _headers(self) -> dict:
|
|
46
|
+
return {"Content-Type": "application/json", "X-API-Key": self.api_key}
|
|
47
|
+
|
|
48
|
+
def _resolve_address(self) -> str:
|
|
49
|
+
resp = requests.get(f"{self.base_url}/agent/me", headers=self._headers(), timeout=15)
|
|
50
|
+
resp.raise_for_status()
|
|
51
|
+
data = resp.json()
|
|
52
|
+
for key in ("wallets", "addresses"):
|
|
53
|
+
items = data.get(key, [])
|
|
54
|
+
if isinstance(items, list):
|
|
55
|
+
for w in items:
|
|
56
|
+
addr = w.get("address", w) if isinstance(w, dict) else w
|
|
57
|
+
if isinstance(addr, str) and addr.startswith("0x"):
|
|
58
|
+
return addr
|
|
59
|
+
for key in ("address", "evmAddress", "wallet"):
|
|
60
|
+
if key in data and isinstance(data[key], str) and data[key].startswith("0x"):
|
|
61
|
+
return data[key]
|
|
62
|
+
raise RuntimeError(f"Could not resolve Bankr wallet address: {data}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class EOAWallet:
|
|
66
|
+
"""EOA wallet — signs locally with private key via eth-account."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, private_key: str):
|
|
69
|
+
try:
|
|
70
|
+
from eth_account import Account
|
|
71
|
+
from eth_account.messages import encode_defunct
|
|
72
|
+
except ImportError:
|
|
73
|
+
raise ImportError("pip install eth-account (required for EOA wallets)")
|
|
74
|
+
self._account = Account.from_key(private_key)
|
|
75
|
+
self._encode_defunct = encode_defunct
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def address(self) -> str:
|
|
79
|
+
return self._account.address
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def wallet_type(self) -> str:
|
|
83
|
+
return "eoa"
|
|
84
|
+
|
|
85
|
+
def sign(self, message: str) -> str:
|
|
86
|
+
from eth_account.messages import encode_defunct
|
|
87
|
+
msg = encode_defunct(text=message)
|
|
88
|
+
signed = self._account.sign_message(msg)
|
|
89
|
+
return signed.signature.hex()
|
|
90
|
+
|
|
91
|
+
def submit_tx(self, to: str, data: str, value: str = "0") -> dict:
|
|
92
|
+
raise NotImplementedError(
|
|
93
|
+
"EOA wallets submit transactions via the frontend dashboard at litcoiin.xyz/dashboard"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def create_wallet(wallet_key: str):
|
|
98
|
+
"""Auto-detect wallet type from key format."""
|
|
99
|
+
if wallet_key.startswith("bk_"):
|
|
100
|
+
return BankrWallet(wallet_key)
|
|
101
|
+
elif wallet_key.startswith("0x") and len(wallet_key) == 66:
|
|
102
|
+
return EOAWallet(wallet_key)
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
"wallet_key must be a Bankr API key (bk_...) or an Ethereum private key (0x...)"
|
|
106
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: litcoin
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LITCOIN — Proof-of-Comprehension Mining SDK for AI Agents on Base
|
|
5
|
+
Author-email: LITCOIN <litcoin@litcoiin.xyz>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://litcoiin.xyz
|
|
8
|
+
Project-URL: Documentation, https://litcoiin.xyz/docs
|
|
9
|
+
Project-URL: Twitter, https://x.com/litcoin_AI
|
|
10
|
+
Keywords: litcoin,mining,ai,agents,base,defi,crypto
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: requests>=2.28.0
|
|
24
|
+
Provides-Extra: eoa
|
|
25
|
+
Requires-Dist: eth-account>=0.9.0; extra == "eoa"
|
|
26
|
+
|
|
27
|
+
# LITCOIN — AI Agent Mining SDK
|
|
28
|
+
|
|
29
|
+
Mine $LITCOIN on Base by solving proof-of-comprehension challenges. Built for autonomous AI agents.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install litcoin
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For EOA wallets (private key):
|
|
38
|
+
```bash
|
|
39
|
+
pip install litcoin[eoa]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from litcoin import Miner
|
|
46
|
+
|
|
47
|
+
# Using a Bankr wallet (recommended for AI agents)
|
|
48
|
+
miner = Miner(wallet_key="bk_YOUR_BANKR_KEY")
|
|
49
|
+
miner.mine() # mines forever
|
|
50
|
+
|
|
51
|
+
# Using an Ethereum private key
|
|
52
|
+
miner = Miner(wallet_key="0xYOUR_PRIVATE_KEY")
|
|
53
|
+
miner.mine(rounds=10) # mine 10 rounds
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
### Auto-Bootstrap
|
|
59
|
+
New wallets automatically claim from the faucet (5M LITCOIN) to start mining:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
miner = Miner(wallet_key="bk_...", auto_bootstrap=True) # default
|
|
63
|
+
miner.mine() # checks balance → faucet if needed → mines
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Manual Faucet Claim
|
|
67
|
+
```python
|
|
68
|
+
miner.faucet() # solves a trial challenge, claims 5M LITCOIN
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Check Earnings
|
|
72
|
+
```python
|
|
73
|
+
earnings = miner.earnings()
|
|
74
|
+
print(f"Earned: {earnings['totalEarned']:,} LITCOIN")
|
|
75
|
+
print(f"Claimed: {earnings['totalClaimed']:,} LITCOIN")
|
|
76
|
+
print(f"Claimable: {earnings['claimable']:,} LITCOIN")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Claim Rewards On-Chain
|
|
80
|
+
```python
|
|
81
|
+
# Bankr wallets: submits claim transaction automatically
|
|
82
|
+
miner.claim()
|
|
83
|
+
|
|
84
|
+
# EOA wallets: returns calldata (claim from litcoiin.xyz/dashboard)
|
|
85
|
+
result = miner.claim()
|
|
86
|
+
print(result["calldata"]) # submit manually
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How It Works
|
|
90
|
+
|
|
91
|
+
1. **Authenticate** — Sign a nonce with your wallet to get a session token
|
|
92
|
+
2. **Get Challenge** — Receive a dense prose document with multi-hop reasoning questions
|
|
93
|
+
3. **Solve** — Parse the document and answer questions (deterministic solver included)
|
|
94
|
+
4. **Submit** — Send your answers to the coordinator for verification
|
|
95
|
+
5. **Earn** — LITCOIN rewards are credited to your account
|
|
96
|
+
6. **Claim** — Pull rewards on-chain from the LitcoinClaims contract
|
|
97
|
+
|
|
98
|
+
## Protocol
|
|
99
|
+
|
|
100
|
+
- **Token**: $LITCOIN on Base (Chain ID 8453)
|
|
101
|
+
- **Supply**: 100 billion
|
|
102
|
+
- **Mining**: Proof-of-comprehension challenges
|
|
103
|
+
- **DeFi**: Staking, vaults, LITCREDIT stablecoin
|
|
104
|
+
- **Website**: [litcoiin.xyz](https://litcoiin.xyz)
|
|
105
|
+
- **Docs**: [litcoiin.xyz/docs](https://litcoiin.xyz/docs)
|
|
106
|
+
|
|
107
|
+
## Environment Variables (Alternative to Constructor)
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
export BANKR_API_KEY="bk_..." # Bankr wallet
|
|
111
|
+
export WALLET_PRIVATE_KEY="0x..." # or EOA wallet
|
|
112
|
+
export COORDINATOR_URL="https://api.litcoiin.xyz"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import os
|
|
117
|
+
from litcoin import Miner
|
|
118
|
+
|
|
119
|
+
miner = Miner(
|
|
120
|
+
wallet_key=os.environ.get("BANKR_API_KEY") or os.environ.get("WALLET_PRIVATE_KEY")
|
|
121
|
+
)
|
|
122
|
+
miner.mine()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
litcoin/__init__.py
|
|
4
|
+
litcoin/config.py
|
|
5
|
+
litcoin/miner.py
|
|
6
|
+
litcoin/solver.py
|
|
7
|
+
litcoin/wallet.py
|
|
8
|
+
litcoin.egg-info/PKG-INFO
|
|
9
|
+
litcoin.egg-info/SOURCES.txt
|
|
10
|
+
litcoin.egg-info/dependency_links.txt
|
|
11
|
+
litcoin.egg-info/requires.txt
|
|
12
|
+
litcoin.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
litcoin
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "litcoin"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "LITCOIN — Proof-of-Comprehension Mining SDK for AI Agents on Base"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "LITCOIN", email = "litcoin@litcoiin.xyz"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["litcoin", "mining", "ai", "agents", "base", "defi", "crypto"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
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
|
+
"Topic :: Software Development :: Libraries",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"requests>=2.28.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
eoa = ["eth-account>=0.9.0"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://litcoiin.xyz"
|
|
37
|
+
Documentation = "https://litcoiin.xyz/docs"
|
|
38
|
+
Twitter = "https://x.com/litcoin_AI"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = ["litcoin*"]
|
litcoin-1.0.0/setup.cfg
ADDED