spacerouter-cli 0.2.2__tar.gz → 0.3.0b1__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.
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/PKG-INFO +2 -2
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/pyproject.toml +2 -2
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/__init__.py +1 -1
- spacerouter_cli-0.3.0b1/src/spacerouter_cli/commands/escrow.py +236 -0
- spacerouter_cli-0.3.0b1/src/spacerouter_cli/commands/receipts.py +76 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/main.py +6 -1
- spacerouter_cli-0.3.0b1/tests/test_escrow_cli.py +152 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/.gitignore +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/commands/__init__.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/commands/api_key.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/commands/billing.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/commands/config_cmd.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/commands/dashboard.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/commands/node.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/commands/request.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/commands/status.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/config.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/output.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/__init__.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/conftest.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/test_api_key.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/test_billing.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/test_config_cmd.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/test_dashboard.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/test_integration.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/test_node.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/test_request.py +0 -0
- {spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/tests/test_status.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spacerouter-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0b1
|
|
4
4
|
Summary: CLI for the Space Router residential proxy network — designed for AI agents
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Requires-Dist: httpx<1.0,>=0.27
|
|
8
8
|
Requires-Dist: rich<14.0,>=13.0
|
|
9
|
-
Requires-Dist: spacerouter>=0.
|
|
9
|
+
Requires-Dist: spacerouter>=0.3.0b1
|
|
10
10
|
Requires-Dist: typer<1.0,>=0.12
|
|
11
11
|
Provides-Extra: dev
|
|
12
12
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "spacerouter-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0b1"
|
|
8
8
|
description = "CLI for the Space Router residential proxy network — designed for AI agents"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"typer>=0.12,<1.0",
|
|
13
13
|
"httpx>=0.27,<1.0",
|
|
14
|
-
"spacerouter>=0.
|
|
14
|
+
"spacerouter>=0.3.0b1",
|
|
15
15
|
"rich>=13.0,<14.0",
|
|
16
16
|
]
|
|
17
17
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""``spacerouter escrow`` — TokenPaymentEscrow wallet operations.
|
|
2
|
+
|
|
3
|
+
Consumer-facing view of on-chain balance and withdrawal state. JSON
|
|
4
|
+
output only (agent-friendly). Read operations work without a private
|
|
5
|
+
key; deposits / withdrawals require one via ``--private-key`` or
|
|
6
|
+
``SR_ESCROW_PRIVATE_KEY``.
|
|
7
|
+
|
|
8
|
+
This command group operates against the on-chain contract directly.
|
|
9
|
+
For a provider's *local* receipt state (signed/failed/retryable),
|
|
10
|
+
see the provider CLI at
|
|
11
|
+
``python -m app.main --receipts`` on the node.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from typing import Annotated, Optional
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
|
|
21
|
+
from spacerouter.escrow import EscrowClient
|
|
22
|
+
from spacerouter_cli.output import cli_error_handler, print_json
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(
|
|
25
|
+
help="Query and interact with the TokenPaymentEscrow contract on-chain.",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
ENV_RPC = "SR_ESCROW_CHAIN_RPC"
|
|
31
|
+
ENV_CONTRACT = "SR_ESCROW_CONTRACT_ADDRESS"
|
|
32
|
+
ENV_PRIVATE_KEY = "SR_ESCROW_PRIVATE_KEY"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
RpcOpt = Annotated[
|
|
36
|
+
Optional[str],
|
|
37
|
+
typer.Option(
|
|
38
|
+
"--rpc-url",
|
|
39
|
+
help=(
|
|
40
|
+
"Creditcoin RPC endpoint. Env: SR_ESCROW_CHAIN_RPC. "
|
|
41
|
+
"Default test: https://rpc.cc3-testnet.creditcoin.network"
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
]
|
|
45
|
+
ContractOpt = Annotated[
|
|
46
|
+
Optional[str],
|
|
47
|
+
typer.Option(
|
|
48
|
+
"--contract-address",
|
|
49
|
+
help="TokenPaymentEscrow proxy address. Env: SR_ESCROW_CONTRACT_ADDRESS.",
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
PrivateKeyOpt = Annotated[
|
|
53
|
+
Optional[str],
|
|
54
|
+
typer.Option(
|
|
55
|
+
"--private-key",
|
|
56
|
+
help=(
|
|
57
|
+
"Wallet private key for write operations. "
|
|
58
|
+
"Env: SR_ESCROW_PRIVATE_KEY. Never log or commit."
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _resolve_client(
|
|
65
|
+
rpc_url: Optional[str],
|
|
66
|
+
contract_address: Optional[str],
|
|
67
|
+
private_key: Optional[str] = None,
|
|
68
|
+
) -> EscrowClient:
|
|
69
|
+
rpc = rpc_url or os.environ.get(ENV_RPC)
|
|
70
|
+
contract = contract_address or os.environ.get(ENV_CONTRACT)
|
|
71
|
+
key = private_key or os.environ.get(ENV_PRIVATE_KEY)
|
|
72
|
+
|
|
73
|
+
if not rpc:
|
|
74
|
+
raise typer.BadParameter(
|
|
75
|
+
"Missing RPC URL. Use --rpc-url or set SR_ESCROW_CHAIN_RPC.",
|
|
76
|
+
)
|
|
77
|
+
if not contract:
|
|
78
|
+
raise typer.BadParameter(
|
|
79
|
+
"Missing contract address. Use --contract-address or set "
|
|
80
|
+
"SR_ESCROW_CONTRACT_ADDRESS.",
|
|
81
|
+
)
|
|
82
|
+
return EscrowClient(
|
|
83
|
+
rpc_url=rpc, contract_address=contract, private_key=key,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command("balance")
|
|
88
|
+
@cli_error_handler
|
|
89
|
+
def balance(
|
|
90
|
+
address: Annotated[
|
|
91
|
+
str, typer.Argument(help="Address to query escrow balance for."),
|
|
92
|
+
],
|
|
93
|
+
rpc_url: RpcOpt = None,
|
|
94
|
+
contract_address: ContractOpt = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Escrow balance for an address, in wei (18-decimal SPACE)."""
|
|
97
|
+
client = _resolve_client(rpc_url, contract_address)
|
|
98
|
+
wei = client.balance(address)
|
|
99
|
+
print_json({
|
|
100
|
+
"address": address,
|
|
101
|
+
"escrow_balance_wei": wei,
|
|
102
|
+
"escrow_balance_space": wei / 10**18,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command("token-balance")
|
|
107
|
+
@cli_error_handler
|
|
108
|
+
def token_balance(
|
|
109
|
+
address: Annotated[
|
|
110
|
+
str, typer.Argument(help="Address to query undeposited token balance for."),
|
|
111
|
+
],
|
|
112
|
+
rpc_url: RpcOpt = None,
|
|
113
|
+
contract_address: ContractOpt = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Undeposited (wallet-held) SPACE token balance in wei."""
|
|
116
|
+
client = _resolve_client(rpc_url, contract_address)
|
|
117
|
+
wei = client.token_balance(address)
|
|
118
|
+
print_json({
|
|
119
|
+
"address": address,
|
|
120
|
+
"token_balance_wei": wei,
|
|
121
|
+
"token_balance_space": wei / 10**18,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("withdrawal-request")
|
|
126
|
+
@cli_error_handler
|
|
127
|
+
def withdrawal_request(
|
|
128
|
+
address: Annotated[
|
|
129
|
+
str, typer.Argument(help="Address whose pending withdrawal to inspect."),
|
|
130
|
+
],
|
|
131
|
+
rpc_url: RpcOpt = None,
|
|
132
|
+
contract_address: ContractOpt = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Pending withdrawal state: amount + unlock timestamp."""
|
|
135
|
+
client = _resolve_client(rpc_url, contract_address)
|
|
136
|
+
amount, unlock_at, exists = client.withdrawal_request(address)
|
|
137
|
+
print_json({
|
|
138
|
+
"address": address,
|
|
139
|
+
"has_pending_withdrawal": exists,
|
|
140
|
+
"amount_wei": amount,
|
|
141
|
+
"amount_space": amount / 10**18,
|
|
142
|
+
"unlock_at_epoch_seconds": unlock_at,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command("withdrawal-delay")
|
|
147
|
+
@cli_error_handler
|
|
148
|
+
def withdrawal_delay(
|
|
149
|
+
rpc_url: RpcOpt = None,
|
|
150
|
+
contract_address: ContractOpt = None,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Contract-wide withdrawal delay in seconds."""
|
|
153
|
+
client = _resolve_client(rpc_url, contract_address)
|
|
154
|
+
delay = client.withdrawal_delay()
|
|
155
|
+
print_json({
|
|
156
|
+
"withdrawal_delay_seconds": delay,
|
|
157
|
+
"withdrawal_delay_days": delay / 86400,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command("deposit")
|
|
162
|
+
@cli_error_handler
|
|
163
|
+
def deposit(
|
|
164
|
+
amount_wei: Annotated[
|
|
165
|
+
int,
|
|
166
|
+
typer.Argument(help="Amount to deposit, in wei (18-decimal)."),
|
|
167
|
+
],
|
|
168
|
+
rpc_url: RpcOpt = None,
|
|
169
|
+
contract_address: ContractOpt = None,
|
|
170
|
+
private_key: PrivateKeyOpt = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Deposit tokens into the escrow. Requires a private key."""
|
|
173
|
+
client = _resolve_client(rpc_url, contract_address, private_key)
|
|
174
|
+
tx_hash = client.deposit(int(amount_wei))
|
|
175
|
+
print_json({
|
|
176
|
+
"action": "deposit",
|
|
177
|
+
"amount_wei": int(amount_wei),
|
|
178
|
+
"tx_hash": tx_hash,
|
|
179
|
+
"from": client.address,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command("initiate-withdrawal")
|
|
184
|
+
@cli_error_handler
|
|
185
|
+
def initiate_withdrawal(
|
|
186
|
+
amount_wei: Annotated[
|
|
187
|
+
int,
|
|
188
|
+
typer.Argument(help="Amount to withdraw, in wei."),
|
|
189
|
+
],
|
|
190
|
+
rpc_url: RpcOpt = None,
|
|
191
|
+
contract_address: ContractOpt = None,
|
|
192
|
+
private_key: PrivateKeyOpt = None,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Start a withdrawal. Subject to the contract's withdrawal delay."""
|
|
195
|
+
client = _resolve_client(rpc_url, contract_address, private_key)
|
|
196
|
+
tx_hash = client.initiate_withdrawal(int(amount_wei))
|
|
197
|
+
print_json({
|
|
198
|
+
"action": "initiate_withdrawal",
|
|
199
|
+
"amount_wei": int(amount_wei),
|
|
200
|
+
"tx_hash": tx_hash,
|
|
201
|
+
"from": client.address,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@app.command("execute-withdrawal")
|
|
206
|
+
@cli_error_handler
|
|
207
|
+
def execute_withdrawal(
|
|
208
|
+
rpc_url: RpcOpt = None,
|
|
209
|
+
contract_address: ContractOpt = None,
|
|
210
|
+
private_key: PrivateKeyOpt = None,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Finalise a previously-initiated withdrawal after the delay has elapsed."""
|
|
213
|
+
client = _resolve_client(rpc_url, contract_address, private_key)
|
|
214
|
+
tx_hash = client.execute_withdrawal()
|
|
215
|
+
print_json({
|
|
216
|
+
"action": "execute_withdrawal",
|
|
217
|
+
"tx_hash": tx_hash,
|
|
218
|
+
"from": client.address,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.command("cancel-withdrawal")
|
|
223
|
+
@cli_error_handler
|
|
224
|
+
def cancel_withdrawal(
|
|
225
|
+
rpc_url: RpcOpt = None,
|
|
226
|
+
contract_address: ContractOpt = None,
|
|
227
|
+
private_key: PrivateKeyOpt = None,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Cancel a pending withdrawal request before it unlocks."""
|
|
230
|
+
client = _resolve_client(rpc_url, contract_address, private_key)
|
|
231
|
+
tx_hash = client.cancel_withdrawal()
|
|
232
|
+
print_json({
|
|
233
|
+
"action": "cancel_withdrawal",
|
|
234
|
+
"tx_hash": tx_hash,
|
|
235
|
+
"from": client.address,
|
|
236
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""``spacerouter receipts`` — on-chain receipt state queries.
|
|
2
|
+
|
|
3
|
+
Consumer-facing view. Given a client address + request UUID, check
|
|
4
|
+
whether the escrow has settled that receipt on-chain. JSON output
|
|
5
|
+
only.
|
|
6
|
+
|
|
7
|
+
For a provider's *local* receipt state (signed vs failed vs locked),
|
|
8
|
+
see the provider CLI at ``python -m app.main --receipts`` on the
|
|
9
|
+
node — that operates against the provider's local SQLite, not the
|
|
10
|
+
chain.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Annotated, Optional
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
|
|
20
|
+
from spacerouter_cli.commands.escrow import (
|
|
21
|
+
ContractOpt, RpcOpt, _resolve_client,
|
|
22
|
+
)
|
|
23
|
+
from spacerouter_cli.output import cli_error_handler, print_json
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
help=(
|
|
27
|
+
"Query on-chain Leg 2 receipt state. For provider-local "
|
|
28
|
+
"receipt state, run `python -m app.main --receipts` on the node."
|
|
29
|
+
),
|
|
30
|
+
no_args_is_help=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("is-settled")
|
|
35
|
+
@cli_error_handler
|
|
36
|
+
def is_settled(
|
|
37
|
+
client_address: Annotated[
|
|
38
|
+
str, typer.Argument(help="Receipt client (payer) address."),
|
|
39
|
+
],
|
|
40
|
+
request_uuid: Annotated[
|
|
41
|
+
str, typer.Argument(help="Receipt UUID (per-client nonce)."),
|
|
42
|
+
],
|
|
43
|
+
rpc_url: RpcOpt = None,
|
|
44
|
+
contract_address: ContractOpt = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Check whether a specific receipt has been claimed on-chain."""
|
|
47
|
+
client = _resolve_client(rpc_url, contract_address)
|
|
48
|
+
used = client.is_nonce_used(client_address, request_uuid)
|
|
49
|
+
print_json({
|
|
50
|
+
"client_address": client_address,
|
|
51
|
+
"request_uuid": request_uuid,
|
|
52
|
+
"settled_on_chain": used,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command("show")
|
|
57
|
+
@cli_error_handler
|
|
58
|
+
def show(
|
|
59
|
+
client_address: Annotated[
|
|
60
|
+
str, typer.Argument(help="Receipt client (payer) address."),
|
|
61
|
+
],
|
|
62
|
+
request_uuid: Annotated[
|
|
63
|
+
str, typer.Argument(help="Receipt UUID."),
|
|
64
|
+
],
|
|
65
|
+
rpc_url: RpcOpt = None,
|
|
66
|
+
contract_address: ContractOpt = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Alias for ``is-settled`` — returns the same on-chain state."""
|
|
69
|
+
client = _resolve_client(rpc_url, contract_address)
|
|
70
|
+
used = client.is_nonce_used(client_address, request_uuid)
|
|
71
|
+
print_json({
|
|
72
|
+
"client_address": client_address,
|
|
73
|
+
"request_uuid": request_uuid,
|
|
74
|
+
"settled_on_chain": used,
|
|
75
|
+
"status": "claimed" if used else "unclaimed_on_chain",
|
|
76
|
+
})
|
|
@@ -7,7 +7,10 @@ import json
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
9
|
from spacerouter_cli import __version__
|
|
10
|
-
from spacerouter_cli.commands import
|
|
10
|
+
from spacerouter_cli.commands import (
|
|
11
|
+
api_key, billing, config_cmd, dashboard, escrow, node, receipts,
|
|
12
|
+
request, status,
|
|
13
|
+
)
|
|
11
14
|
|
|
12
15
|
app = typer.Typer(
|
|
13
16
|
name="spacerouter",
|
|
@@ -22,6 +25,8 @@ app.add_typer(node.app, name="node", help="Manage proxy nodes")
|
|
|
22
25
|
app.add_typer(billing.app, name="billing", help="Billing and checkout")
|
|
23
26
|
app.add_typer(dashboard.app, name="dashboard", help="Dashboard data")
|
|
24
27
|
app.add_typer(config_cmd.app, name="config", help="Configuration management")
|
|
28
|
+
app.add_typer(escrow.app, name="escrow", help="Escrow wallet / deposit / withdrawal")
|
|
29
|
+
app.add_typer(receipts.app, name="receipts", help="On-chain Leg 2 receipt state queries")
|
|
25
30
|
app.command(name="status", help="Check service health")(status.status)
|
|
26
31
|
|
|
27
32
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Tests for ``spacerouter escrow`` and ``spacerouter receipts`` sub-apps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import patch, MagicMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from spacerouter_cli.main import app
|
|
11
|
+
from tests.conftest import parse_json_output
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def runner():
|
|
16
|
+
return CliRunner()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def escrow_env(monkeypatch):
|
|
21
|
+
monkeypatch.setenv("SR_ESCROW_CHAIN_RPC",
|
|
22
|
+
"https://rpc.cc3-testnet.creditcoin.network")
|
|
23
|
+
monkeypatch.setenv("SR_ESCROW_CONTRACT_ADDRESS",
|
|
24
|
+
"0xC5740e4e9175301a24FB6d22bA184b8ec0762852")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def mock_escrow_client():
|
|
29
|
+
"""Patch EscrowClient so no real RPC calls go out."""
|
|
30
|
+
with patch(
|
|
31
|
+
"spacerouter_cli.commands.escrow.EscrowClient"
|
|
32
|
+
) as cls:
|
|
33
|
+
inst = MagicMock()
|
|
34
|
+
cls.return_value = inst
|
|
35
|
+
yield inst
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestEscrowBalance:
|
|
39
|
+
def test_balance_emits_wei_and_space(
|
|
40
|
+
self, runner, escrow_env, mock_escrow_client,
|
|
41
|
+
):
|
|
42
|
+
mock_escrow_client.balance.return_value = 5 * 10**18
|
|
43
|
+
result = runner.invoke(app, [
|
|
44
|
+
"escrow", "balance", "0xabcd" + "0" * 36,
|
|
45
|
+
])
|
|
46
|
+
assert result.exit_code == 0
|
|
47
|
+
data = parse_json_output(result.output)
|
|
48
|
+
assert data["escrow_balance_wei"] == 5 * 10**18
|
|
49
|
+
assert data["escrow_balance_space"] == 5.0
|
|
50
|
+
|
|
51
|
+
def test_balance_requires_rpc(self, runner, monkeypatch):
|
|
52
|
+
monkeypatch.delenv("SR_ESCROW_CHAIN_RPC", raising=False)
|
|
53
|
+
monkeypatch.delenv("SR_ESCROW_CONTRACT_ADDRESS", raising=False)
|
|
54
|
+
result = runner.invoke(app, [
|
|
55
|
+
"escrow", "balance", "0xabcd" + "0" * 36,
|
|
56
|
+
])
|
|
57
|
+
assert result.exit_code != 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestWithdrawalRequest:
|
|
61
|
+
def test_pending_withdrawal_fields(
|
|
62
|
+
self, runner, escrow_env, mock_escrow_client,
|
|
63
|
+
):
|
|
64
|
+
mock_escrow_client.withdrawal_request.return_value = (
|
|
65
|
+
10**18, 1_800_000_000, True,
|
|
66
|
+
)
|
|
67
|
+
result = runner.invoke(app, [
|
|
68
|
+
"escrow", "withdrawal-request", "0x" + "a" * 40,
|
|
69
|
+
])
|
|
70
|
+
assert result.exit_code == 0
|
|
71
|
+
data = parse_json_output(result.output)
|
|
72
|
+
assert data["has_pending_withdrawal"] is True
|
|
73
|
+
assert data["amount_wei"] == 10**18
|
|
74
|
+
assert data["amount_space"] == 1.0
|
|
75
|
+
assert data["unlock_at_epoch_seconds"] == 1_800_000_000
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestWithdrawalDelay:
|
|
79
|
+
def test_surfaces_days(self, runner, escrow_env, mock_escrow_client):
|
|
80
|
+
mock_escrow_client.withdrawal_delay.return_value = 5 * 86400
|
|
81
|
+
result = runner.invoke(app, ["escrow", "withdrawal-delay"])
|
|
82
|
+
assert result.exit_code == 0
|
|
83
|
+
data = parse_json_output(result.output)
|
|
84
|
+
assert data["withdrawal_delay_seconds"] == 432_000
|
|
85
|
+
assert data["withdrawal_delay_days"] == 5.0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestDeposit:
|
|
89
|
+
def test_deposit_returns_tx_hash(
|
|
90
|
+
self, runner, escrow_env, mock_escrow_client, monkeypatch,
|
|
91
|
+
):
|
|
92
|
+
monkeypatch.setenv("SR_ESCROW_PRIVATE_KEY", "0x" + "f" * 64)
|
|
93
|
+
mock_escrow_client.deposit.return_value = "0xabc123"
|
|
94
|
+
mock_escrow_client.address = "0x" + "a" * 40
|
|
95
|
+
result = runner.invoke(app, ["escrow", "deposit", "1000000000000000000"])
|
|
96
|
+
assert result.exit_code == 0
|
|
97
|
+
data = parse_json_output(result.output)
|
|
98
|
+
assert data["tx_hash"] == "0xabc123"
|
|
99
|
+
assert data["action"] == "deposit"
|
|
100
|
+
mock_escrow_client.deposit.assert_called_once_with(10**18)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestReceiptsIsSettled:
|
|
104
|
+
def test_returns_on_chain_state(
|
|
105
|
+
self, runner, escrow_env, mock_escrow_client,
|
|
106
|
+
):
|
|
107
|
+
mock_escrow_client.is_nonce_used.return_value = True
|
|
108
|
+
result = runner.invoke(app, [
|
|
109
|
+
"receipts", "is-settled",
|
|
110
|
+
"0x" + "a" * 40,
|
|
111
|
+
"9f8e5c21-1234-4567-89ab-cdef01234567",
|
|
112
|
+
])
|
|
113
|
+
assert result.exit_code == 0
|
|
114
|
+
data = parse_json_output(result.output)
|
|
115
|
+
assert data["settled_on_chain"] is True
|
|
116
|
+
assert data["request_uuid"] == "9f8e5c21-1234-4567-89ab-cdef01234567"
|
|
117
|
+
|
|
118
|
+
def test_unsettled_returns_false(
|
|
119
|
+
self, runner, escrow_env, mock_escrow_client,
|
|
120
|
+
):
|
|
121
|
+
mock_escrow_client.is_nonce_used.return_value = False
|
|
122
|
+
result = runner.invoke(app, [
|
|
123
|
+
"receipts", "is-settled",
|
|
124
|
+
"0x" + "a" * 40,
|
|
125
|
+
"11111111-2222-3333-4444-555555555555",
|
|
126
|
+
])
|
|
127
|
+
assert result.exit_code == 0
|
|
128
|
+
data = parse_json_output(result.output)
|
|
129
|
+
assert data["settled_on_chain"] is False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestReceiptsShow:
|
|
133
|
+
def test_emits_status_field(
|
|
134
|
+
self, runner, escrow_env, mock_escrow_client,
|
|
135
|
+
):
|
|
136
|
+
mock_escrow_client.is_nonce_used.return_value = False
|
|
137
|
+
result = runner.invoke(app, [
|
|
138
|
+
"receipts", "show",
|
|
139
|
+
"0x" + "b" * 40,
|
|
140
|
+
"22222222-3333-4444-5555-666666666666",
|
|
141
|
+
])
|
|
142
|
+
assert result.exit_code == 0
|
|
143
|
+
data = parse_json_output(result.output)
|
|
144
|
+
assert data["status"] == "unclaimed_on_chain"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestSubAppsRegistered:
|
|
148
|
+
def test_escrow_appears_in_help(self, runner):
|
|
149
|
+
result = runner.invoke(app, ["--help"])
|
|
150
|
+
assert result.exit_code == 0
|
|
151
|
+
assert "escrow" in result.output
|
|
152
|
+
assert "receipts" in result.output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spacerouter_cli-0.2.2 → spacerouter_cli-0.3.0b1}/src/spacerouter_cli/commands/config_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|