walley-sdk 0.1.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.
Files changed (31) hide show
  1. walley_sdk-0.1.0/.gitignore +5 -0
  2. walley_sdk-0.1.0/PKG-INFO +57 -0
  3. walley_sdk-0.1.0/README.md +38 -0
  4. walley_sdk-0.1.0/pyproject.toml +41 -0
  5. walley_sdk-0.1.0/src/walley/__init__.py +88 -0
  6. walley_sdk-0.1.0/src/walley/_transport.py +45 -0
  7. walley_sdk-0.1.0/src/walley/auth.py +115 -0
  8. walley_sdk-0.1.0/src/walley/client.py +206 -0
  9. walley_sdk-0.1.0/src/walley/enums.py +45 -0
  10. walley_sdk-0.1.0/src/walley/errors.py +71 -0
  11. walley_sdk-0.1.0/src/walley/models/__init__.py +53 -0
  12. walley_sdk-0.1.0/src/walley/models/_base.py +5 -0
  13. walley_sdk-0.1.0/src/walley/models/balance.py +47 -0
  14. walley_sdk-0.1.0/src/walley/models/fee.py +51 -0
  15. walley_sdk-0.1.0/src/walley/models/ledger.py +16 -0
  16. walley_sdk-0.1.0/src/walley/models/party.py +9 -0
  17. walley_sdk-0.1.0/src/walley/models/preapproval.py +61 -0
  18. walley_sdk-0.1.0/src/walley/models/prepared.py +72 -0
  19. walley_sdk-0.1.0/src/walley/models/transaction.py +61 -0
  20. walley_sdk-0.1.0/src/walley/models/transfer.py +25 -0
  21. walley_sdk-0.1.0/src/walley/resources/__init__.py +21 -0
  22. walley_sdk-0.1.0/src/walley/resources/_base.py +7 -0
  23. walley_sdk-0.1.0/src/walley/resources/_tokens.py +33 -0
  24. walley_sdk-0.1.0/src/walley/resources/balances.py +60 -0
  25. walley_sdk-0.1.0/src/walley/resources/fees.py +47 -0
  26. walley_sdk-0.1.0/src/walley/resources/ledger.py +106 -0
  27. walley_sdk-0.1.0/src/walley/resources/party.py +16 -0
  28. walley_sdk-0.1.0/src/walley/resources/preapprovals.py +58 -0
  29. walley_sdk-0.1.0/src/walley/resources/transactions.py +240 -0
  30. walley_sdk-0.1.0/src/walley/resources/transfers.py +155 -0
  31. walley_sdk-0.1.0/src/walley/signer.py +202 -0
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ dist/
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: walley-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Walley wallet API on Canton Network
5
+ Project-URL: Homepage, https://walley.cc
6
+ Project-URL: Documentation, https://docs.walley.cc
7
+ Project-URL: Source, https://github.com/k2flabs/walley
8
+ Author: Walley
9
+ License-Expression: MIT
10
+ Keywords: canton,cip-56,sdk,wallet,walley
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: cryptography>=42
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: mnemonic>=0.20
15
+ Requires-Dist: pydantic>=2.7
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # walley-sdk
21
+
22
+ Official Python SDK for [Walley](https://walley.cc) — programmatic
23
+ self-custody of CIP-56 tokens on Canton Network. Your process holds the
24
+ wallet's key, signs locally, and submits transactions; the API only ever
25
+ sees public keys and signatures.
26
+
27
+ ```bash
28
+ pip install walley-sdk
29
+ ```
30
+
31
+ ```python
32
+ from walley import Walley
33
+
34
+ w = Walley(
35
+ key_file="~/.config/walley/alice.key", # PEM or 24-word mnemonic file
36
+ party_hint="walley-alice",
37
+ )
38
+
39
+ for balance in w.balances:
40
+ print(balance.instrument_id, balance.total_balance)
41
+
42
+ result = w.transactions.execute(commands) # your dapp's own ledger commands
43
+ result = w.transfers.send(receiver="walley-bob::1220...", amount="5")
44
+ print(result.update_id, result.fee)
45
+ ```
46
+
47
+ **Full documentation: [docs.walley.cc/python-sdk](https://docs.walley.cc/python-sdk/overview)** —
48
+ quickstart, keys & authentication, transactions & the fee model, wallet
49
+ operations, and ledger reads.
50
+
51
+ ## Development
52
+
53
+ ```bash
54
+ cd walley-sdk
55
+ pip install -e '.[dev]'
56
+ pytest
57
+ ```
@@ -0,0 +1,38 @@
1
+ # walley-sdk
2
+
3
+ Official Python SDK for [Walley](https://walley.cc) — programmatic
4
+ self-custody of CIP-56 tokens on Canton Network. Your process holds the
5
+ wallet's key, signs locally, and submits transactions; the API only ever
6
+ sees public keys and signatures.
7
+
8
+ ```bash
9
+ pip install walley-sdk
10
+ ```
11
+
12
+ ```python
13
+ from walley import Walley
14
+
15
+ w = Walley(
16
+ key_file="~/.config/walley/alice.key", # PEM or 24-word mnemonic file
17
+ party_hint="walley-alice",
18
+ )
19
+
20
+ for balance in w.balances:
21
+ print(balance.instrument_id, balance.total_balance)
22
+
23
+ result = w.transactions.execute(commands) # your dapp's own ledger commands
24
+ result = w.transfers.send(receiver="walley-bob::1220...", amount="5")
25
+ print(result.update_id, result.fee)
26
+ ```
27
+
28
+ **Full documentation: [docs.walley.cc/python-sdk](https://docs.walley.cc/python-sdk/overview)** —
29
+ quickstart, keys & authentication, transactions & the fee model, wallet
30
+ operations, and ledger reads.
31
+
32
+ ## Development
33
+
34
+ ```bash
35
+ cd walley-sdk
36
+ pip install -e '.[dev]'
37
+ pytest
38
+ ```
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "walley-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Walley wallet API on Canton Network"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Walley" }]
13
+ keywords = ["walley", "canton", "wallet", "cip-56", "sdk"]
14
+ dependencies = [
15
+ "httpx>=0.27",
16
+ "pydantic>=2.7",
17
+ "cryptography>=42",
18
+ "mnemonic>=0.20",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "pytest>=8",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://walley.cc"
28
+ Documentation = "https://docs.walley.cc"
29
+ Source = "https://github.com/k2flabs/walley"
30
+
31
+ # Import package is `walley` (PyPI distribution is `walley-sdk`):
32
+ # pip install walley-sdk -> from walley import Walley
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/walley"]
35
+
36
+ # Published artifacts carry only the SDK itself.
37
+ [tool.hatch.build.targets.sdist]
38
+ include = ["src/walley", "README.md", "pyproject.toml"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
@@ -0,0 +1,88 @@
1
+ from .client import Walley
2
+ from .enums import (
3
+ PreapprovalStatus,
4
+ TransactionSource,
5
+ TransferStatus,
6
+ TxKind,
7
+ TxKindFilter,
8
+ )
9
+ from .errors import (
10
+ AuthError,
11
+ FeeDueError,
12
+ NotFoundError,
13
+ RateLimitError,
14
+ ServerError,
15
+ UnprocessableError,
16
+ ValidationError,
17
+ WalleyError,
18
+ )
19
+ from .models import (
20
+ ActiveContract,
21
+ Balance,
22
+ Fee,
23
+ FeeDue,
24
+ FreeSends,
25
+ Holding,
26
+ MergeDelegation,
27
+ MergeDelegationState,
28
+ MergeEvent,
29
+ Party,
30
+ PreparedSubmission,
31
+ PreparedTransaction,
32
+ SignedMessage,
33
+ SubmitResult,
34
+ SubmittedTransaction,
35
+ Token,
36
+ Transaction,
37
+ TransactionPage,
38
+ Transfer,
39
+ TransferEvent,
40
+ TransferPreapproval,
41
+ TransferPreapprovalState,
42
+ )
43
+ from .signer import Signer
44
+
45
+ __all__ = [
46
+ "Walley",
47
+ "Signer",
48
+ # enums
49
+ "TransferStatus",
50
+ "TransactionSource",
51
+ "TxKind",
52
+ "TxKindFilter",
53
+ "PreapprovalStatus",
54
+ # models
55
+ "ActiveContract",
56
+ "Balance",
57
+ "Holding",
58
+ "Token",
59
+ "Party",
60
+ "Transfer",
61
+ "Transaction",
62
+ "TransactionPage",
63
+ "TransferEvent",
64
+ "MergeEvent",
65
+ "Fee",
66
+ "FeeDue",
67
+ "FreeSends",
68
+ "PreparedTransaction",
69
+ "PreparedSubmission",
70
+ "SubmittedTransaction",
71
+ "SubmitResult",
72
+ "SignedMessage",
73
+ "TransferPreapproval",
74
+ "TransferPreapprovalState",
75
+ "MergeDelegation",
76
+ "MergeDelegationState",
77
+ # errors
78
+ "WalleyError",
79
+ "AuthError",
80
+ "ValidationError",
81
+ "FeeDueError",
82
+ "UnprocessableError",
83
+ "NotFoundError",
84
+ "RateLimitError",
85
+ "ServerError",
86
+ ]
87
+
88
+ __version__ = "0.1.0"
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from .errors import error_from_response
8
+
9
+ # Response-envelope metadata flattened into every body by the server. Stripped
10
+ # so payloads (e.g. Commands from /transfers/prepare) can be echoed back clean.
11
+ ENVELOPE_KEYS = ("trace_id", "request_id", "timestamp")
12
+
13
+
14
+ class Transport:
15
+ """Issues requests against a fixed base URL and returns the body.
16
+
17
+ Success bodies are flat: the payload fields sit at the top level next to
18
+ ``trace_id``/``request_id``/``timestamp``. Errors carry a ``message`` and
19
+ the HTTP status.
20
+ """
21
+
22
+ def __init__(self, client: httpx.Client, base_url: str) -> None:
23
+ self._client = client
24
+ self._base = base_url.rstrip("/")
25
+
26
+ @property
27
+ def base_url(self) -> str:
28
+ return self._base
29
+
30
+ def request(self, method: str, path: str, **kwargs: Any) -> dict:
31
+ response = self._client.request(method, self._base + path, **kwargs)
32
+ body = response.json() if response.content else {}
33
+ if response.status_code >= 400:
34
+ raise error_from_response(response.status_code, body)
35
+ return body
36
+
37
+ def get(self, path: str, **kwargs: Any) -> dict:
38
+ return self.request("GET", path, **kwargs)
39
+
40
+ def post(self, path: str, **kwargs: Any) -> dict:
41
+ return self.request("POST", path, **kwargs)
42
+
43
+
44
+ def strip_envelope(body: dict) -> dict:
45
+ return {k: v for k, v in body.items() if k not in ENVELOPE_KEYS}
@@ -0,0 +1,115 @@
1
+ """Challenge/verify authentication.
2
+
3
+ Read endpoints (balances, holdings, transaction history) require a bearer
4
+ token. Walley issues one to whoever proves control of a party's signing key:
5
+
6
+ 1. ``POST /v1/auth/challenge`` → ``{nonce, expires_at, token}``.
7
+ 2. Sign the canonical message ``"{audience}|{origin}|{nonce}|{expires_at}"``
8
+ (UTF-8; the nonce is signed as its base64url string) with the party's
9
+ Ed25519 key. Must byte-match ``canonical_message`` in
10
+ ``vendor/sherpa-api/src/auth/crypto.rs``.
11
+ 3. ``POST /v1/auth/verify`` → ``{access_token, expires_at}``.
12
+
13
+ The party id itself is not part of the signed message — the server binds the
14
+ party via the key fingerprint (the party id's namespace).
15
+
16
+ Prepare/submit endpoints don't need the bearer; they are guarded by the
17
+ prepare token and the ledger's own signature check. The SDK still attaches
18
+ it everywhere — it's harmless and keeps the client uniform.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import base64
24
+ import time
25
+ from typing import Generator
26
+
27
+ import httpx
28
+
29
+ from .errors import AuthError
30
+ from .signer import Signer
31
+
32
+ # Re-authenticate this many seconds before the token's stated expiry.
33
+ _EXPIRY_SKEW_SECONDS = 30
34
+
35
+
36
+ class ChallengeAuth(httpx.Auth):
37
+ """Proves key ownership via the challenge/verify flow, caches the issued
38
+ bearer token, and re-authenticates transparently on expiry or 401."""
39
+
40
+ requires_response_body = True
41
+
42
+ def __init__(
43
+ self,
44
+ *,
45
+ signer: Signer,
46
+ party_id: str,
47
+ challenge_url: str,
48
+ verify_url: str,
49
+ audience: str,
50
+ origin: str,
51
+ ) -> None:
52
+ self._signer = signer
53
+ self._party_id = party_id
54
+ self._challenge_url = challenge_url
55
+ self._verify_url = verify_url
56
+ self._audience = audience
57
+ self._origin = origin
58
+ self._token: str | None = None
59
+ self._expires_at: float = 0.0
60
+
61
+ def sync_auth_flow(
62
+ self, request: httpx.Request
63
+ ) -> Generator[httpx.Request, httpx.Response, None]:
64
+ if not self._fresh():
65
+ yield from self._authenticate()
66
+ self._apply(request)
67
+ response = yield request
68
+ if response.status_code == 401:
69
+ yield from self._authenticate()
70
+ self._apply(request)
71
+ yield request
72
+
73
+ def _authenticate(self) -> Generator[httpx.Request, httpx.Response, None]:
74
+ response = yield httpx.Request("POST", self._challenge_url, json={})
75
+ response.read()
76
+ if response.status_code != 200:
77
+ raise AuthError("Auth challenge failed.", status=response.status_code)
78
+ challenge = response.json()
79
+
80
+ message = self.canonical_message(
81
+ self._audience, self._origin, challenge["nonce"], challenge["expires_at"]
82
+ )
83
+ signature = base64.b64encode(self._signer.sign(message)).decode()
84
+
85
+ response = yield httpx.Request(
86
+ "POST",
87
+ self._verify_url,
88
+ json={
89
+ "party_id": self._party_id,
90
+ "origin": self._origin,
91
+ "public_key": self._signer.public_key_base64,
92
+ "signature": signature,
93
+ "challenge": {
94
+ "nonce": challenge["nonce"],
95
+ "expires_at": challenge["expires_at"],
96
+ "token": challenge["token"],
97
+ },
98
+ },
99
+ )
100
+ response.read()
101
+ if response.status_code != 200:
102
+ raise AuthError("Auth verify failed.", status=response.status_code)
103
+ body = response.json()
104
+ self._token = body["access_token"]
105
+ self._expires_at = float(body["expires_at"])
106
+
107
+ @staticmethod
108
+ def canonical_message(audience: str, origin: str, nonce: str, expires_at: int) -> bytes:
109
+ return f"{audience}|{origin}|{nonce}|{expires_at}".encode()
110
+
111
+ def _fresh(self) -> bool:
112
+ return self._token is not None and time.time() < self._expires_at - _EXPIRY_SKEW_SECONDS
113
+
114
+ def _apply(self, request: httpx.Request) -> None:
115
+ request.headers["Authorization"] = f"Bearer {self._token}"
@@ -0,0 +1,206 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import os
5
+ import re
6
+ from typing import Optional, Union
7
+
8
+ import httpx
9
+
10
+ from ._transport import Transport
11
+ from .auth import ChallengeAuth
12
+ from .models import SignedMessage
13
+ from .resources import (
14
+ Balances,
15
+ Fees,
16
+ Holdings,
17
+ Ledger,
18
+ MergeDelegations,
19
+ Party,
20
+ Tokens,
21
+ TransferPreapprovals,
22
+ Transactions,
23
+ Transfers,
24
+ )
25
+ from .signer import Signer
26
+
27
+ DEFAULT_API_BASE = "https://api.walley.cc"
28
+ DEFAULT_AUDIENCE = "https://walley.cc/dapp"
29
+ DEFAULT_ORIGIN = "walley-python-sdk"
30
+
31
+ _PARTY_HINT_RE = re.compile(r"^walley-[a-zA-Z0-9-]+$")
32
+ _PARTY_ID_RE = re.compile(r"^[a-zA-Z0-9-]+::[0-9a-f]+$")
33
+
34
+
35
+ class Walley:
36
+ """Synchronous Walley client, signed in to one existing self-custodied
37
+ party.
38
+
39
+ The client holds the party's Ed25519 key and does all signing locally —
40
+ the API only ever sees public keys and signatures. Reads authenticate
41
+ automatically via the challenge/verify flow; writes follow Canton's
42
+ external-signing dance (prepare → sign → submit) behind one method call.
43
+
44
+ Parties are created in the Walley app; sign in here with the wallet's
45
+ recovery mnemonic (or exported key).
46
+
47
+ Args:
48
+ key_file: Path to a key file — a PKCS#8 PEM or the 24-word recovery
49
+ mnemonic (format detected). The recommended option: key material
50
+ stays out of source code. Falls back to ``$WALLEY_KEY_FILE``.
51
+ mnemonic: Alternatively, the 24-word mnemonic inline. Falls back to
52
+ ``$WALLEY_MNEMONIC``.
53
+ private_key: Alternatively, the raw Ed25519 private key — bytes,
54
+ hex, base64, or PEM. Falls back to ``$WALLEY_PRIVATE_KEY``.
55
+ signer: Alternatively, a pre-built :class:`Signer`.
56
+ party_id: The full party id (``hint::fingerprint``). Falls back to
57
+ ``$WALLEY_PARTY_ID``. Must match the key's fingerprint.
58
+ party_hint: Or just the hint (``walley-...``) — the id is derived
59
+ from the key. Falls back to ``$WALLEY_PARTY_HINT``.
60
+ api_base: Walley API base URL. Falls back to ``$WALLEY_API_URL``,
61
+ then ``https://api.walley.cc``.
62
+ audience: Auth audience string; must match the deployment's
63
+ ``AUTH_PROVIDER__AUDIENCE``.
64
+ origin: Free-form caller identifier recorded in issued tokens.
65
+ timeout: Per-request timeout in seconds (submits use a longer one).
66
+ auto_settle_fees: Settle each deferred network fee immediately after
67
+ the transaction that incurred it (the settlement lands on the
68
+ result's ``fee_settlement``). Pass ``False`` to manage debts
69
+ yourself via ``client.fees`` — but note an unsettled debt blocks
70
+ the party's next prepare with :class:`FeeDueError`.
71
+
72
+ Example::
73
+
74
+ w = Walley(mnemonic="...", party_hint="walley-alice")
75
+ result = w.transfers.send(receiver="walley-bob::1220...", amount="5")
76
+ print(result.fee) # the network fee this send incurred
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ *,
82
+ key_file: Optional[str] = None,
83
+ mnemonic: Optional[str] = None,
84
+ private_key: Union[bytes, str, None] = None,
85
+ signer: Optional[Signer] = None,
86
+ party_id: Optional[str] = None,
87
+ party_hint: Optional[str] = None,
88
+ api_base: Optional[str] = None,
89
+ audience: str = DEFAULT_AUDIENCE,
90
+ origin: str = DEFAULT_ORIGIN,
91
+ timeout: float = 30.0,
92
+ auto_settle_fees: bool = True,
93
+ ) -> None:
94
+ self.signer = _resolve_signer(key_file, mnemonic, private_key, signer)
95
+ self.party_id = _resolve_party_id(self.signer, party_id, party_hint)
96
+ api_base = (api_base or os.environ.get("WALLEY_API_URL") or DEFAULT_API_BASE).rstrip("/")
97
+
98
+ auth = ChallengeAuth(
99
+ signer=self.signer,
100
+ party_id=self.party_id,
101
+ challenge_url=f"{api_base}/v1/auth/challenge",
102
+ verify_url=f"{api_base}/v1/auth/verify",
103
+ audience=audience,
104
+ origin=origin,
105
+ )
106
+ self._http = httpx.Client(auth=auth, timeout=timeout)
107
+ transport = Transport(self._http, api_base)
108
+
109
+ self.tokens = Tokens(transport)
110
+ self.transactions = Transactions(
111
+ transport, self.party_id, self.signer, auto_settle_fees=auto_settle_fees
112
+ )
113
+ self.transfers = Transfers(transport, self.party_id, self.transactions, self.tokens)
114
+ self.fees = Fees(transport, self.party_id, self.transactions)
115
+ self.balances = Balances(transport, self.party_id)
116
+ self.holdings = Holdings(transport, self.party_id)
117
+ self.party = Party(transport, self.party_id)
118
+ self.ledger = Ledger(transport, self.party_id)
119
+ self.preapprovals = TransferPreapprovals(transport, self.party_id, self.transactions)
120
+ self.merge_delegations = MergeDelegations(transport, self.party_id, self.transactions)
121
+
122
+ @property
123
+ def fingerprint(self) -> str:
124
+ """The signing key's Canton fingerprint (the party id's namespace)."""
125
+ return self.signer.fingerprint
126
+
127
+ def sign(self, message: Union[bytes, str]) -> SignedMessage:
128
+ """Sign an arbitrary message with the party's key.
129
+
130
+ Strings are signed as UTF-8. Returns a :class:`SignedMessage`
131
+ bundling the signature with the public key and fingerprint, so any
132
+ verifier can check it against the party id.
133
+ """
134
+ data = message.encode() if isinstance(message, str) else message
135
+ return SignedMessage(
136
+ signature=base64.b64encode(self.signer.sign(data)).decode(),
137
+ signed_by=self.signer.fingerprint,
138
+ public_key=self.signer.public_key_base64,
139
+ )
140
+
141
+ def close(self) -> None:
142
+ self._http.close()
143
+
144
+ def __enter__(self) -> "Walley":
145
+ return self
146
+
147
+ def __exit__(self, *_exc: object) -> None:
148
+ self.close()
149
+
150
+ def __repr__(self) -> str:
151
+ return f"Walley(party_id={self.party_id!r})"
152
+
153
+
154
+ def _resolve_signer(
155
+ key_file: Optional[str],
156
+ mnemonic: Optional[str],
157
+ private_key: Union[bytes, str, None],
158
+ signer: Optional[Signer],
159
+ ) -> Signer:
160
+ if signer is not None:
161
+ return signer
162
+ key_file = key_file or os.environ.get("WALLEY_KEY_FILE")
163
+ if key_file:
164
+ return Signer.from_file(key_file)
165
+ mnemonic = mnemonic or os.environ.get("WALLEY_MNEMONIC")
166
+ if mnemonic:
167
+ return Signer.from_mnemonic(mnemonic)
168
+ private_key = private_key or os.environ.get("WALLEY_PRIVATE_KEY")
169
+ if private_key:
170
+ return Signer.from_private_key(private_key)
171
+ raise ValueError(
172
+ "a signing key is required — pass key_file=..., mnemonic=..., private_key=..., "
173
+ "or signer=... (or set WALLEY_KEY_FILE / WALLEY_MNEMONIC / WALLEY_PRIVATE_KEY)"
174
+ )
175
+
176
+
177
+ def _resolve_party_id(
178
+ signer: Signer,
179
+ party_id: Optional[str],
180
+ party_hint: Optional[str],
181
+ ) -> str:
182
+ party_id = party_id or os.environ.get("WALLEY_PARTY_ID")
183
+ if party_id:
184
+ if not _PARTY_ID_RE.match(party_id):
185
+ raise ValueError(f"invalid party_id {party_id!r} (expected hint::fingerprint)")
186
+ namespace = party_id.rsplit("::", 1)[1]
187
+ if namespace.lower() != signer.fingerprint:
188
+ raise ValueError(
189
+ f"party_id namespace {namespace!r} does not match the signing key's "
190
+ f"fingerprint {signer.fingerprint!r} — wrong key for this party?"
191
+ )
192
+ return party_id
193
+ party_hint = party_hint or os.environ.get("WALLEY_PARTY_HINT")
194
+ if party_hint:
195
+ if not _PARTY_HINT_RE.match(party_hint) or not 8 <= len(party_hint) <= 64:
196
+ raise ValueError(
197
+ f"invalid party_hint {party_hint!r} (expected 'walley-' + 8-64 "
198
+ "alphanumeric/hyphen chars)"
199
+ )
200
+ return signer.party_id(party_hint)
201
+ raise ValueError(
202
+ "pass party_id=... or party_hint=... (or set WALLEY_PARTY_ID / WALLEY_PARTY_HINT)"
203
+ )
204
+
205
+
206
+ __all__ = ["Walley", "DEFAULT_API_BASE", "DEFAULT_AUDIENCE", "DEFAULT_ORIGIN"]
@@ -0,0 +1,45 @@
1
+ from enum import Enum
2
+
3
+
4
+ class TransferStatus(str, Enum):
5
+ """Lifecycle of a transfer event in the transaction history."""
6
+
7
+ INITIATED = "INITIATED"
8
+ COMPLETED = "COMPLETED"
9
+ ACCEPTED = "ACCEPTED"
10
+ REJECTED = "REJECTED"
11
+ WITHDRAWN = "WITHDRAWN"
12
+
13
+
14
+ class TransactionSource(str, Enum):
15
+ """Which app initiated a transaction (``WALLEY`` for a plain wallet send)."""
16
+
17
+ WALLEY = "WALLEY"
18
+ NEXODE = "NEXODE"
19
+ ATOMIX = "ATOMIX"
20
+ VAULT = "VAULT"
21
+ CANTORY = "CANTORY"
22
+
23
+
24
+ class TxKind(str, Enum):
25
+ TRANSFER = "transfer"
26
+ FEE = "fee"
27
+
28
+
29
+ class TxKindFilter(str, Enum):
30
+ """Filter for the transaction history: everything, only network-fee
31
+ transactions, or everything but fees."""
32
+
33
+ ALL = "all"
34
+ FEES = "fees"
35
+ NON_FEES = "non_fees"
36
+
37
+
38
+ class PreapprovalStatus(str, Enum):
39
+ """State of a transfer preapproval or merge delegation. ``EXPIRED``
40
+ applies to transfer preapprovals only."""
41
+
42
+ ENABLED = "ENABLED"
43
+ PENDING = "PENDING"
44
+ NOT_ENABLED = "NOT_ENABLED"
45
+ EXPIRED = "EXPIRED"