memwal 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
memwal/types.py ADDED
@@ -0,0 +1,340 @@
1
+ """
2
+ memwal — Core Types
3
+
4
+ Dataclasses for all API request options and response types.
5
+ Ed25519 delegate key based SDK that communicates with
6
+ the MemWal Rust server (TEE).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import List, Optional
13
+
14
+ # ============================================================
15
+ # Config
16
+ # ============================================================
17
+
18
+
19
+ @dataclass
20
+ class MemWalConfig:
21
+ """Configuration for creating a MemWal client.
22
+
23
+ Attributes:
24
+ key: Ed25519 private key (hex string). This is the delegate key from app.memwal.com.
25
+ account_id: MemWalAccount object ID on Sui.
26
+ server_url: Server URL (default: https://relayer.memwal.ai).
27
+ namespace: Default namespace for memory isolation (default: "default").
28
+ """
29
+
30
+ key: str
31
+ account_id: str
32
+ server_url: str = "https://relayer.memwal.ai"
33
+ namespace: str = "default"
34
+
35
+
36
+ # ============================================================
37
+ # API Response Types
38
+ # ============================================================
39
+
40
+
41
+ @dataclass
42
+ class RememberResult:
43
+ """Result from remember()."""
44
+
45
+ id: str
46
+ blob_id: str
47
+ owner: str
48
+ namespace: str
49
+
50
+
51
+ @dataclass
52
+ class RecallMemory:
53
+ """A single recalled memory."""
54
+
55
+ blob_id: str
56
+ text: str
57
+ distance: float
58
+
59
+
60
+ @dataclass
61
+ class RecallResult:
62
+ """Result from recall()."""
63
+
64
+ results: List[RecallMemory]
65
+ total: int
66
+
67
+
68
+ @dataclass
69
+ class AnalyzedFact:
70
+ """A single extracted fact."""
71
+
72
+ text: str
73
+ id: str
74
+ blob_id: str
75
+
76
+
77
+ @dataclass
78
+ class AnalyzeResult:
79
+ """Result from analyze().
80
+
81
+ Per PR #121: each extracted fact is enqueued as a background remember
82
+ job. ``job_ids`` aligns with ``facts`` positionally; poll those
83
+ job_ids (e.g. via :meth:`MemWal.wait_for_remember_jobs`) to know when
84
+ the corresponding memory is fully persisted on Walrus.
85
+ """
86
+
87
+ facts: List[AnalyzedFact]
88
+ fact_count: int
89
+ job_ids: List[str]
90
+ status: str
91
+ owner: str
92
+
93
+ @property
94
+ def total(self) -> int:
95
+ """Backward-compat alias for ``fact_count`` (v0.1 callers)."""
96
+ return self.fact_count
97
+
98
+
99
+ @dataclass
100
+ class HealthResult:
101
+ """Server health response."""
102
+
103
+ status: str
104
+ version: str
105
+
106
+
107
+ @dataclass
108
+ class RestoreResult:
109
+ """Result from restore()."""
110
+
111
+ restored: int
112
+ skipped: int
113
+ total: int
114
+ namespace: str
115
+ owner: str
116
+
117
+
118
+ @dataclass
119
+ class AskMemory:
120
+ """A memory used to answer a question."""
121
+
122
+ blob_id: str
123
+ text: str
124
+ distance: float
125
+
126
+
127
+ @dataclass
128
+ class AskResult:
129
+ """Result from ask()."""
130
+
131
+ answer: str
132
+ memories_used: int
133
+ memories: List[AskMemory]
134
+
135
+
136
+ # ============================================================
137
+ # Manual Flow Types
138
+ # ============================================================
139
+
140
+
141
+ @dataclass
142
+ class RememberManualOptions:
143
+ """Options for remember_manual().
144
+
145
+ Attributes:
146
+ blob_id: Walrus blob ID (user already uploaded encrypted data).
147
+ vector: Embedding vector (user already generated).
148
+ namespace: Namespace (default: config namespace or "default").
149
+ """
150
+
151
+ blob_id: str
152
+ vector: List[float]
153
+ namespace: Optional[str] = None
154
+
155
+
156
+ @dataclass
157
+ class RememberManualResult:
158
+ """Result from remember_manual()."""
159
+
160
+ id: str
161
+ blob_id: str
162
+ owner: str
163
+ namespace: str
164
+
165
+
166
+ @dataclass
167
+ class RecallManualOptions:
168
+ """Options for recall_manual().
169
+
170
+ Attributes:
171
+ vector: Pre-computed query embedding vector.
172
+ limit: Max number of results (default: 10).
173
+ namespace: Namespace (default: config namespace or "default").
174
+ """
175
+
176
+ vector: List[float]
177
+ limit: int = 10
178
+ namespace: Optional[str] = None
179
+
180
+
181
+ @dataclass
182
+ class RecallManualHit:
183
+ """A single search hit -- raw blob_id + distance (no decrypted text)."""
184
+
185
+ blob_id: str
186
+ distance: float
187
+
188
+
189
+ @dataclass
190
+ class RecallManualResult:
191
+ """Result from recall_manual()."""
192
+
193
+ results: List[RecallManualHit]
194
+ total: int
195
+
196
+
197
+ # ============================================================
198
+ # Async remember (PR #121: ENG-1406 / ENG-1408)
199
+ # ============================================================
200
+
201
+
202
+ @dataclass
203
+ class RememberAcceptedResult:
204
+ """Result from remember() / remember_async() — server returns 202 immediately.
205
+
206
+ The actual upload + on-chain commit happen in a background worker.
207
+ Poll ``GET /api/remember/{job_id}`` (via wait_for_remember_job) to follow
208
+ progress until status reaches "done" or "failed".
209
+ """
210
+
211
+ job_id: str
212
+ status: str
213
+
214
+
215
+ @dataclass
216
+ class RememberJobStatus:
217
+ """One snapshot of an async remember job, returned by the status endpoint.
218
+
219
+ ``status`` transitions: pending → running → uploaded → done, or → failed.
220
+ ``not_found`` is returned when the job_id is unknown or not the caller's.
221
+ """
222
+
223
+ job_id: str
224
+ status: str
225
+ owner: Optional[str] = None
226
+ namespace: Optional[str] = None
227
+ blob_id: Optional[str] = None
228
+ error: Optional[str] = None
229
+
230
+
231
+ # ============================================================
232
+ # Bulk remember (ENG-1408)
233
+ # ============================================================
234
+
235
+
236
+ @dataclass
237
+ class RememberBulkItem:
238
+ """One item in a bulk remember request.
239
+
240
+ ``namespace`` overrides the client default for this item only.
241
+ """
242
+
243
+ text: str
244
+ namespace: Optional[str] = None
245
+
246
+
247
+ @dataclass
248
+ class RememberBulkAcceptedResult:
249
+ """Result from remember_bulk() / remember_bulk_async() — 202 with job_ids.
250
+
251
+ ``job_ids`` aligns positionally with the input ``items`` list.
252
+ """
253
+
254
+ job_ids: List[str]
255
+ total: int
256
+ status: str
257
+
258
+
259
+ @dataclass
260
+ class RememberBulkStatusItem:
261
+ """Per-item status returned by the bulk status endpoint."""
262
+
263
+ job_id: str
264
+ status: str
265
+ blob_id: Optional[str] = None
266
+ error: Optional[str] = None
267
+
268
+
269
+ @dataclass
270
+ class RememberBulkStatusResult:
271
+ """Result from get_remember_bulk_status()."""
272
+
273
+ results: List[RememberBulkStatusItem]
274
+
275
+
276
+ @dataclass
277
+ class RememberBulkOptions:
278
+ """Polling options for remember_bulk_and_wait() / wait_for_remember_jobs().
279
+
280
+ ``poll_interval_ms`` is the base poll cadence (default 1500ms).
281
+ ``timeout_ms`` is the total wait budget before raising TimeoutError
282
+ (default 120_000ms).
283
+ """
284
+
285
+ poll_interval_ms: int = 1500
286
+ timeout_ms: int = 120_000
287
+
288
+
289
+ @dataclass
290
+ class RememberBulkItemResult:
291
+ """One settled item in a bulk-and-wait result."""
292
+
293
+ id: str
294
+ blob_id: str
295
+ status: str # "done" | "failed" | "timeout"
296
+ error: Optional[str] = None
297
+
298
+
299
+ @dataclass
300
+ class RememberBulkResult:
301
+ """Aggregate result from remember_bulk_and_wait() / wait_for_remember_jobs().
302
+
303
+ ``succeeded + failed + timed_out == total``. ``results`` preserves input order.
304
+ """
305
+
306
+ results: List[RememberBulkItemResult]
307
+ total: int
308
+ succeeded: int
309
+ failed: int
310
+ timed_out: int
311
+
312
+
313
+ # ============================================================
314
+ # Embed + analyze
315
+ # ============================================================
316
+
317
+
318
+ @dataclass
319
+ class EmbedResult:
320
+ """Result from embed() — raw embedding vector for a piece of text."""
321
+
322
+ vector: List[float]
323
+
324
+
325
+ @dataclass
326
+ class AnalyzeWaitResult:
327
+ """Result from analyze_and_wait().
328
+
329
+ Combines the analyze fact-extraction output with the bulk-style settled
330
+ results for each enqueued background remember job. Mirrors the TS SDK's
331
+ ``AnalyzeWaitResult extends RememberBulkResult``.
332
+ """
333
+
334
+ results: List[RememberBulkItemResult]
335
+ total: int
336
+ succeeded: int
337
+ failed: int
338
+ timed_out: int
339
+ facts: List[AnalyzedFact]
340
+ owner: str
memwal/utils.py ADDED
@@ -0,0 +1,172 @@
1
+ """
2
+ memwal — Shared Utilities
3
+
4
+ Crypto and encoding helpers for Ed25519 signing and SHA-256 hashing.
5
+ Uses PyNaCl (nacl.signing) as the primary Ed25519 implementation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ from typing import Tuple
12
+
13
+ import nacl.signing
14
+
15
+
16
+ def hex_to_bytes(hex_str: str) -> bytes:
17
+ """Convert a hex string to bytes.
18
+
19
+ Handles optional ``0x`` prefix.
20
+
21
+ Args:
22
+ hex_str: Hex-encoded string, optionally prefixed with ``0x``.
23
+
24
+ Returns:
25
+ Raw bytes.
26
+ """
27
+ clean = hex_str[2:] if hex_str.startswith("0x") else hex_str
28
+ return bytes.fromhex(clean)
29
+
30
+
31
+ def bytes_to_hex(b: bytes) -> str:
32
+ """Convert bytes to a lowercase hex string (no ``0x`` prefix).
33
+
34
+ Args:
35
+ b: Raw bytes.
36
+
37
+ Returns:
38
+ Hex-encoded string.
39
+ """
40
+ return b.hex()
41
+
42
+
43
+ def sha256_hex(data: str) -> str:
44
+ """Compute the SHA-256 hex digest of a UTF-8 string.
45
+
46
+ Args:
47
+ data: Input string.
48
+
49
+ Returns:
50
+ Lowercase hex SHA-256 digest.
51
+ """
52
+ return hashlib.sha256(data.encode("utf-8")).hexdigest()
53
+
54
+
55
+ def build_signing_key(private_key_hex: str) -> nacl.signing.SigningKey:
56
+ """Build a PyNaCl ``SigningKey`` from an Ed25519 private key hex string.
57
+
58
+ Args:
59
+ private_key_hex: 32-byte Ed25519 seed as hex (64 hex chars), optionally ``0x``-prefixed.
60
+
61
+ Returns:
62
+ A ``nacl.signing.SigningKey`` instance.
63
+
64
+ Raises:
65
+ ValueError: If the decoded seed is not exactly 32 bytes.
66
+ """
67
+ seed_bytes = hex_to_bytes(private_key_hex)
68
+ if len(seed_bytes) != 32:
69
+ raise ValueError(
70
+ f"Ed25519 seed must be exactly 32 bytes, got {len(seed_bytes)}"
71
+ )
72
+ return nacl.signing.SigningKey(seed_bytes)
73
+
74
+
75
+ def sign_message(message: str, signing_key: nacl.signing.SigningKey) -> Tuple[str, str]:
76
+ """Sign a UTF-8 message with an Ed25519 signing key.
77
+
78
+ Args:
79
+ message: The message string to sign.
80
+ signing_key: A PyNaCl ``SigningKey``.
81
+
82
+ Returns:
83
+ A tuple of ``(signature_hex, public_key_hex)``.
84
+ """
85
+ signed = signing_key.sign(message.encode("utf-8"))
86
+ signature_bytes: bytes = signed.signature
87
+ public_key_bytes: bytes = bytes(signing_key.verify_key)
88
+ return bytes_to_hex(signature_bytes), bytes_to_hex(public_key_bytes)
89
+
90
+
91
+ def build_signature_message(
92
+ timestamp: str,
93
+ method: str,
94
+ path: str,
95
+ body_sha256: str,
96
+ nonce: str = "",
97
+ account_id: str = "",
98
+ ) -> str:
99
+ """Build the canonical signing message.
100
+
101
+ Current format (matches Rust server ``services/server/src/auth.rs``)::
102
+
103
+ "{timestamp}.{method}.{path}.{body_sha256}.{nonce}.{account_id}"
104
+
105
+ The trailing ``nonce`` was added in MED-1 (replay protection); the
106
+ ``account_id`` was added in LOW-23 so an intermediary can't swap the
107
+ account hint without invalidating the signature. Both fields are
108
+ REQUIRED — passing empty strings will fail signature verification on
109
+ the server.
110
+
111
+ Args:
112
+ timestamp: Unix seconds as string.
113
+ method: Uppercase HTTP method (e.g. ``"POST"``).
114
+ path: URL path with query (e.g. ``"/api/remember"``).
115
+ body_sha256: SHA-256 hex digest of the JSON body string.
116
+ nonce: UUID v4 sent as the ``x-nonce`` header (required).
117
+ account_id: MemWalAccount object ID sent as ``x-account-id``
118
+ (required; empty string here will mismatch on server).
119
+ """
120
+ return f"{timestamp}.{method}.{path}.{body_sha256}.{nonce}.{account_id}"
121
+
122
+
123
+ def delegate_key_to_sui_address(private_key_hex: str) -> str:
124
+ """Derive the Sui address from an Ed25519 delegate key.
125
+
126
+ Sui Ed25519 address derivation:
127
+ blake2b-256(0x00 || public_key)[0:32]
128
+
129
+ where ``0x00`` is the Ed25519 scheme flag byte.
130
+
131
+ This matches the TypeScript SDK's ``delegateKeyToSuiAddress()`` exactly,
132
+ and is the same derivation used by the Sui wallet.
133
+
134
+ Args:
135
+ private_key_hex: Ed25519 private key as hex (64 hex chars / 32 bytes),
136
+ optionally ``0x``-prefixed.
137
+
138
+ Returns:
139
+ Sui address as a ``0x``-prefixed lowercase hex string (32 bytes / 64 hex chars).
140
+
141
+ Example::
142
+
143
+ address = delegate_key_to_sui_address("944aa24c09d8b6d6...")
144
+ # "0x1a2b3c..."
145
+ """
146
+ signing_key = build_signing_key(private_key_hex)
147
+ public_key_bytes = bytes(signing_key.verify_key) # 32 bytes
148
+
149
+ # Sui scheme flag for Ed25519 = 0x00, then the 32-byte public key
150
+ scheme_input = bytes([0x00]) + public_key_bytes # 33 bytes total
151
+
152
+ # blake2b with 32-byte (256-bit) digest
153
+ address_bytes = hashlib.blake2b(scheme_input, digest_size=32).digest()
154
+ return "0x" + bytes_to_hex(address_bytes)
155
+
156
+
157
+ def delegate_key_to_public_key(private_key_hex: str) -> bytes:
158
+ """Get the Ed25519 public key bytes from a delegate key.
159
+
160
+ Args:
161
+ private_key_hex: Ed25519 private key as hex, optionally ``0x``-prefixed.
162
+
163
+ Returns:
164
+ 32-byte Ed25519 public key.
165
+
166
+ Example::
167
+
168
+ pub = delegate_key_to_public_key("944aa24c...")
169
+ print(pub.hex()) # "d5b76c57..."
170
+ """
171
+ signing_key = build_signing_key(private_key_hex)
172
+ return bytes(signing_key.verify_key)
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: memwal
3
+ Version: 0.1.0.dev0
4
+ Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
5
+ Project-URL: Homepage, https://memwal.ai
6
+ Project-URL: Documentation, https://docs.memwal.ai
7
+ Project-URL: Repository, https://github.com/MystenLabs/MemWal
8
+ Author: MemWal Team
9
+ License-Expression: MIT
10
+ Keywords: ai,ed25519,memory,memwal,privacy,sui,walrus
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.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: httpx>=0.27.0
24
+ Requires-Dist: pynacl>=1.5.0
25
+ Provides-Extra: all
26
+ Requires-Dist: langchain-core>=0.2.0; extra == 'all'
27
+ Requires-Dist: openai>=1.0.0; extra == 'all'
28
+ Provides-Extra: dev
29
+ Requires-Dist: langchain-core>=0.2.0; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
31
+ Requires-Dist: pytest>=7.0; extra == 'dev'
32
+ Requires-Dist: respx>=0.21.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
34
+ Provides-Extra: langchain
35
+ Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
36
+ Provides-Extra: openai
37
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # memwal
41
+
42
+ Python SDK for [MemWal](https://memwal.ai) — Privacy-first AI memory with Ed25519 signing.
43
+
44
+ All data processing (encryption, embedding, Walrus storage) happens server-side in a TEE. The SDK signs requests with your Ed25519 delegate key and sends text over HTTPS.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install memwal
50
+ ```
51
+
52
+ With optional integrations:
53
+
54
+ ```bash
55
+ pip install memwal[langchain] # LangChain support
56
+ pip install memwal[openai] # OpenAI SDK support
57
+ pip install memwal[all] # Everything
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ Set your environment variables first:
63
+
64
+ ```bash
65
+ export MEMWAL_KEY="your-ed25519-delegate-key-hex"
66
+ export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
67
+ export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
68
+ ```
69
+
70
+ ### Async (recommended)
71
+
72
+ ```python
73
+ import asyncio
74
+ import os
75
+ from memwal import MemWal
76
+
77
+ async def main():
78
+ memwal = MemWal.create(
79
+ key=os.environ["MEMWAL_KEY"],
80
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
81
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
82
+ )
83
+
84
+ # Store a memory
85
+ result = await memwal.remember("I'm allergic to peanuts")
86
+ print(result.blob_id)
87
+
88
+ # Recall memories
89
+ matches = await memwal.recall("food allergies")
90
+ for memory in matches.results:
91
+ print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
92
+
93
+ # Analyze conversation for facts
94
+ analysis = await memwal.analyze("I love coffee and live in Tokyo")
95
+ for fact in analysis.facts:
96
+ print(fact.text)
97
+
98
+ await memwal.close()
99
+
100
+ asyncio.run(main())
101
+ ```
102
+
103
+ ### Sync
104
+
105
+ ```python
106
+ import os
107
+ from memwal import MemWalSync
108
+
109
+ client = MemWalSync.create(
110
+ key=os.environ["MEMWAL_KEY"],
111
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
112
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
113
+ )
114
+
115
+ result = client.remember("I'm allergic to peanuts")
116
+ matches = client.recall("food allergies")
117
+ client.close()
118
+ ```
119
+
120
+ ### Context Manager
121
+
122
+ ```python
123
+ import os
124
+ from memwal import MemWal
125
+
126
+ async with MemWal.create(
127
+ key=os.environ["MEMWAL_KEY"],
128
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
129
+ ) as memwal:
130
+ await memwal.remember("I prefer dark mode")
131
+ ```
132
+
133
+ ## AI Middleware
134
+
135
+ ### LangChain
136
+
137
+ ```python
138
+ import os
139
+ from langchain_openai import ChatOpenAI
140
+ from langchain_core.messages import HumanMessage
141
+ from memwal import with_memwal_langchain
142
+
143
+ llm = ChatOpenAI(model="gpt-4o")
144
+ smart_llm = with_memwal_langchain(
145
+ llm,
146
+ key=os.environ["MEMWAL_KEY"],
147
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
148
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
149
+ max_memories=5,
150
+ min_relevance=0.3,
151
+ )
152
+
153
+ # Memories are automatically recalled and injected
154
+ response = await smart_llm.ainvoke([HumanMessage("What are my food allergies?")])
155
+ ```
156
+
157
+ ### OpenAI SDK
158
+
159
+ ```python
160
+ import os
161
+ from openai import AsyncOpenAI
162
+ from memwal import with_memwal_openai
163
+
164
+ client = AsyncOpenAI()
165
+ smart_client = with_memwal_openai(
166
+ client,
167
+ key=os.environ["MEMWAL_KEY"],
168
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
169
+ server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
170
+ )
171
+
172
+ # Memories are automatically recalled and injected
173
+ response = await smart_client.chat.completions.create(
174
+ model="gpt-4o",
175
+ messages=[{"role": "user", "content": "What are my food allergies?"}],
176
+ )
177
+ ```
178
+
179
+ ## API Reference
180
+
181
+ ### `MemWal.create(key, account_id, server_url?, namespace?)`
182
+
183
+ Create a new async client.
184
+
185
+ ### Methods
186
+
187
+ | Method | Description |
188
+ |--------|-------------|
189
+ | `await remember(text, namespace?)` | Store a memory |
190
+ | `await recall(query, limit?, namespace?)` | Search memories |
191
+ | `await analyze(text, namespace?)` | Extract and store facts |
192
+ | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
193
+ | `await restore(namespace, limit?)` | Restore a namespace |
194
+ | `await health()` | Check server health |
195
+ | `await remember_manual(opts)` | Store with pre-computed vector |
196
+ | `await recall_manual(opts)` | Search with pre-computed vector |
197
+ | `await get_public_key_hex()` | Get Ed25519 public key |
198
+
199
+ ## Authentication
200
+
201
+ Every request is signed with Ed25519:
202
+
203
+ ```
204
+ message = f"{timestamp}.{method}.{path}.{sha256(body)}.{nonce}.{account_id}"
205
+ ```
206
+
207
+ Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-nonce`, `x-delegate-key`, `x-account-id`.
208
+
209
+ ## License
210
+
211
+ MIT