actproof 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
actproof/anchor.py ADDED
@@ -0,0 +1,586 @@
1
+ # SPDX-FileCopyrightText: 2026 Deyan Paroushev
2
+ # SPDX-License-Identifier: MIT
3
+ """
4
+ Anchor a manifest hash to the Algorand ledger via an ARC-2 disclosed-mode note.
5
+
6
+ This is the module that touches money. Each call to ``anchor_manifest`` (in
7
+ production mode) submits a real, signed, 0-Algo self-payment transaction to
8
+ mainnet. The transaction carries a small JSON note containing the
9
+ ``manifest_hash``; that note plus its txid and confirmation round are the
10
+ on-chain commitment.
11
+
12
+ Three modes per the architectural review
13
+ ----------------------------------------
14
+
15
+ * ``AnchorMode.DRAFT`` - build the note bytes and the Algorand transaction
16
+ object, do NOT sign, do NOT submit. Returns an ``AnchorRecord`` with
17
+ ``txid=""`` and ``block_round=None``. Useful for previewing what will go
18
+ on-chain, for offline test workflows, and for CI smoke tests that should
19
+ never burn real Algos.
20
+ * ``AnchorMode.DEMO`` - sign and submit to **testnet**. Real transaction,
21
+ real confirmation, but on the test network where transaction fees are
22
+ paid in test Algos. Used for end-to-end staging tests, integration suites,
23
+ partner demos.
24
+ * ``AnchorMode.PRODUCTION`` - sign and submit to **mainnet**. Real
25
+ transaction with non-zero economic value (the 1000-microAlgo fee, plus
26
+ the durability cost of permanent ledger storage).
27
+
28
+ The mode is an explicit argument with no default. Callers must say which
29
+ network they want. There is no "if I don't say anything, please submit to
30
+ mainnet."
31
+
32
+ ARC-2 disclosed-mode note format
33
+ --------------------------------
34
+
35
+ The on-chain note is::
36
+
37
+ actproof:j{"h":"<hex>","t":"<batching_profile>","v":1}
38
+
39
+ After the ``actproof:j`` ARC-2 prefix, the payload is RFC 8785 canonical
40
+ JSON with three short fields:
41
+
42
+ * ``"h"`` - the manifest_hash as lowercase hex (no ``"sha256:"`` prefix to
43
+ keep size minimal; the algorithm is implied by length and by ARC-2 dapp
44
+ name actproof always using SHA-256 in v1).
45
+ * ``"t"`` - the batching profile (``"single_attestation_anchor_v1"`` in v1).
46
+ * ``"v"`` - the format version (``1``).
47
+
48
+ Total bytes well under the 1000-byte Algorand note limit (about 125 bytes
49
+ for SHA-256, leaving plenty of headroom for future formats).
50
+
51
+ Signer abstraction
52
+ ------------------
53
+
54
+ This module defines a ``Signer`` Protocol with two operations: ``address``
55
+ (the Algorand address being anchored from) and ``sign_transaction(txn)``
56
+ (sign a built ``Transaction``, return a ``SignedTransaction``). Concrete
57
+ implementations land in v0.0.8 (``actproof.signers``): ``KMSSigner`` for
58
+ AWS KMS production use, ``MnemonicSigner`` for testing only. The Protocol
59
+ deliberately exposes ONLY transaction signing; concrete classes enforce
60
+ the discipline that the underlying key never signs arbitrary bytes.
61
+
62
+ API
63
+ ---
64
+
65
+ * ``anchor_manifest(manifest_hash, *, signer, mode, ...) -> AnchorRecord``
66
+ * ``build_note_payload(manifest_hash, batching_profile) -> bytes``
67
+ * ``build_note_bytes(manifest_hash, batching_profile) -> bytes``
68
+ * ``build_transaction(manifest_hash, *, signer_address, suggested_params,
69
+ batching_profile) -> PaymentTxn``
70
+ * ``AnchorMode`` - the three-mode enum.
71
+ * ``Signer`` - the signer Protocol.
72
+ * ``AnchorError`` - raised on submission or confirmation problems.
73
+ * Constants: ``DEFAULT_ALGOD_URL_MAINNET``, ``DEFAULT_ALGOD_URL_TESTNET``,
74
+ ``DEFAULT_CONFIRMATION_TIMEOUT_SECONDS``, ``ALGORAND_NOTE_MAX_BYTES``,
75
+ ``NOTE_VERSION``.
76
+ """
77
+
78
+ from __future__ import annotations
79
+
80
+ import base64
81
+ import logging
82
+ import time
83
+ from dataclasses import dataclass
84
+ from datetime import datetime, timezone
85
+ from enum import Enum
86
+ from typing import Any, Optional, Protocol, runtime_checkable
87
+
88
+ from actproof.canonical import canonicalize
89
+ from actproof.manifest import BATCHING_PROFILE_SINGLE
90
+ from actproof.receipt import (
91
+ ALGORAND_MAINNET,
92
+ ALGORAND_TESTNET,
93
+ ARC2_DAPP_NAME,
94
+ ARC2_FORMAT_VERSION_JSON,
95
+ ARC2_NOTE_FORMAT,
96
+ AnchorRecord,
97
+ )
98
+
99
+
100
+ logger = logging.getLogger(__name__)
101
+
102
+
103
+ # ─────────────────────────────────────────────────────────────────
104
+ # OPTIONAL IMPORT: py-algorand-sdk
105
+ # ─────────────────────────────────────────────────────────────────
106
+
107
+ try:
108
+ from algosdk import transaction as _algo_txn
109
+ from algosdk.v2client.algod import AlgodClient
110
+ _ALGOSDK_AVAILABLE: bool = True
111
+ _ALGOSDK_ERROR: Optional[str] = None
112
+ PaymentTxn = _algo_txn.PaymentTxn
113
+ SignedTransaction = _algo_txn.SignedTransaction
114
+ SuggestedParams = _algo_txn.SuggestedParams
115
+ Transaction = _algo_txn.Transaction
116
+ except Exception as exc: # noqa: BLE001
117
+ # py-algorand-sdk is a hard dependency; mirror the timestamp.py pattern
118
+ # of staying importable even if a transitive conflict (e.g. msgpack,
119
+ # cryptography, websockets) breaks the loader. Failures surface at
120
+ # call time.
121
+ _ALGOSDK_AVAILABLE = False
122
+ _ALGOSDK_ERROR = str(exc)
123
+ PaymentTxn = None # type: ignore[assignment,misc]
124
+ SignedTransaction = None # type: ignore[assignment,misc]
125
+ SuggestedParams = None # type: ignore[assignment,misc]
126
+ Transaction = None # type: ignore[assignment,misc]
127
+ AlgodClient = None # type: ignore[assignment,misc]
128
+
129
+
130
+ __all__ = [
131
+ "AnchorMode",
132
+ "AnchorError",
133
+ "Signer",
134
+ "anchor_manifest",
135
+ "build_note_payload",
136
+ "build_note_bytes",
137
+ "build_transaction",
138
+ "DEFAULT_ALGOD_URL_MAINNET",
139
+ "DEFAULT_ALGOD_URL_TESTNET",
140
+ "DEFAULT_CONFIRMATION_TIMEOUT_SECONDS",
141
+ "ALGORAND_NOTE_MAX_BYTES",
142
+ "NOTE_VERSION",
143
+ ]
144
+
145
+
146
+ # ─────────────────────────────────────────────────────────────────
147
+ # CONSTANTS
148
+ # ─────────────────────────────────────────────────────────────────
149
+
150
+ DEFAULT_ALGOD_URL_MAINNET: str = "https://mainnet-api.algonode.cloud"
151
+ """Default public algod endpoint for mainnet (Algonode, free)."""
152
+
153
+ DEFAULT_ALGOD_URL_TESTNET: str = "https://testnet-api.algonode.cloud"
154
+ """Default public algod endpoint for testnet (Algonode, free)."""
155
+
156
+ DEFAULT_CONFIRMATION_TIMEOUT_SECONDS: float = 60.0
157
+ """Default time to wait for a transaction to confirm on-chain (Algorand
158
+ blocks are ~3.3 seconds; 60s covers roughly 18 rounds)."""
159
+
160
+ ALGORAND_NOTE_MAX_BYTES: int = 1000
161
+ """Algorand transaction note max size in bytes (protocol limit)."""
162
+
163
+ NOTE_VERSION: int = 1
164
+ """Note format version. Increment when the ``h``/``t``/``v`` payload
165
+ structure changes (would require coordinated rollout of verifiers)."""
166
+
167
+ _ARC2_PREFIX: bytes = b"actproof:j"
168
+ """ARC-2 note prefix: dapp_name + ':' + format_version."""
169
+
170
+
171
+ # ─────────────────────────────────────────────────────────────────
172
+ # EXCEPTIONS
173
+ # ─────────────────────────────────────────────────────────────────
174
+
175
+ class AnchorError(RuntimeError):
176
+ """Raised on submission, signing, or confirmation problems.
177
+
178
+ Subclass of ``RuntimeError`` rather than ``ValueError`` because the
179
+ typical failure mode here is operational (network down, signer
180
+ unavailable, confirmation timed out) rather than input-validation.
181
+ """
182
+
183
+
184
+ # ─────────────────────────────────────────────────────────────────
185
+ # ENUMS
186
+ # ─────────────────────────────────────────────────────────────────
187
+
188
+ class AnchorMode(str, Enum):
189
+ """Three operational modes for ``anchor_manifest``.
190
+
191
+ * ``DRAFT`` - build the transaction without signing or submitting.
192
+ Returns an ``AnchorRecord`` with ``txid=""`` and ``block_round=None``.
193
+ Network defaults to testnet for the recorded ``network`` field.
194
+ * ``DEMO`` - sign and submit to **testnet**.
195
+ * ``PRODUCTION`` - sign and submit to **mainnet**.
196
+ """
197
+ DRAFT = "draft"
198
+ DEMO = "demo"
199
+ PRODUCTION = "production"
200
+
201
+
202
+ # ─────────────────────────────────────────────────────────────────
203
+ # SIGNER PROTOCOL
204
+ # ─────────────────────────────────────────────────────────────────
205
+
206
+ @runtime_checkable
207
+ class Signer(Protocol):
208
+ """Sign Algorand transactions. Implementations land in v0.0.8.
209
+
210
+ Concrete implementations in ``actproof.signers``:
211
+
212
+ * ``KMSSigner`` - AWS KMS-backed Ed25519 signing for production.
213
+ * ``MnemonicSigner`` - mnemonic-based local signing for testing only.
214
+
215
+ Defense-in-depth note: implementations should sign ONLY Algorand
216
+ transactions, never arbitrary bytes. The Protocol enforces this
217
+ structurally (it only exposes ``sign_transaction``); the v0.0.8
218
+ abstract base class additionally enforces it at class-definition
219
+ time via ``__init_subclass__``.
220
+
221
+ Attributes:
222
+ address: The signer's 58-character base32 Algorand address.
223
+
224
+ Methods:
225
+ sign_transaction(txn): Sign a built ``Transaction``, return a
226
+ ``SignedTransaction``.
227
+ """
228
+
229
+ @property
230
+ def address(self) -> str:
231
+ """The signer's 58-character base32 Algorand address."""
232
+ ...
233
+
234
+ def sign_transaction(self, txn: Any) -> Any:
235
+ """Sign an Algorand transaction. Returns a ``SignedTransaction``.
236
+
237
+ Args:
238
+ txn: An ``algosdk.transaction.Transaction`` (typically a
239
+ ``PaymentTxn``).
240
+
241
+ Returns:
242
+ An ``algosdk.transaction.SignedTransaction``.
243
+ """
244
+ ...
245
+
246
+
247
+ # ─────────────────────────────────────────────────────────────────
248
+ # NOTE CONSTRUCTION
249
+ # ─────────────────────────────────────────────────────────────────
250
+
251
+ def build_note_payload(
252
+ manifest_hash: bytes,
253
+ batching_profile: str = BATCHING_PROFILE_SINGLE,
254
+ ) -> bytes:
255
+ """Build the ARC-2 disclosed-mode note payload bytes (without prefix).
256
+
257
+ The payload is RFC 8785 canonical JSON with three short fields:
258
+ ``h`` (manifest hash hex), ``t`` (batching profile), ``v`` (version).
259
+
260
+ Args:
261
+ manifest_hash: Raw bytes of the manifest hash. Typically 32 bytes
262
+ for SHA-256.
263
+ batching_profile: The batching profile identifier. Defaults to
264
+ ``"single_attestation_anchor_v1"`` (v1).
265
+
266
+ Returns:
267
+ Canonical JSON payload as UTF-8 bytes. To get the full note bytes,
268
+ prepend ``actproof:j``.
269
+ """
270
+ payload_dict = {
271
+ "h": manifest_hash.hex(),
272
+ "t": batching_profile,
273
+ "v": NOTE_VERSION,
274
+ }
275
+ # RFC 8785 sorts keys alphabetically; payload becomes h, t, v.
276
+ return canonicalize(payload_dict)
277
+
278
+
279
+ def build_note_bytes(
280
+ manifest_hash: bytes,
281
+ batching_profile: str = BATCHING_PROFILE_SINGLE,
282
+ ) -> bytes:
283
+ """Build the full on-chain note bytes, including the ARC-2 prefix.
284
+
285
+ Args:
286
+ manifest_hash: Raw bytes of the manifest hash.
287
+ batching_profile: The batching profile identifier.
288
+
289
+ Returns:
290
+ Note bytes: ``b"actproof:j" + canonical_payload_bytes``. Goes
291
+ directly into the ``note`` field of an Algorand transaction.
292
+
293
+ Raises:
294
+ AnchorError: If the resulting note exceeds the Algorand 1000-byte
295
+ note limit (this should not happen for sensible inputs).
296
+ """
297
+ payload = build_note_payload(manifest_hash, batching_profile)
298
+ note_bytes = _ARC2_PREFIX + payload
299
+ if len(note_bytes) > ALGORAND_NOTE_MAX_BYTES:
300
+ raise AnchorError(
301
+ f"Note size {len(note_bytes)} bytes exceeds Algorand limit "
302
+ f"{ALGORAND_NOTE_MAX_BYTES} bytes. This should not happen for "
303
+ f"a SHA-256 single-attestation anchor; check the batching "
304
+ f"profile string length."
305
+ )
306
+ return note_bytes
307
+
308
+
309
+ # ─────────────────────────────────────────────────────────────────
310
+ # TRANSACTION CONSTRUCTION
311
+ # ─────────────────────────────────────────────────────────────────
312
+
313
+ def build_transaction(
314
+ manifest_hash: bytes,
315
+ *,
316
+ signer_address: str,
317
+ suggested_params: Any,
318
+ batching_profile: str = BATCHING_PROFILE_SINGLE,
319
+ ) -> Any:
320
+ """Build the unsigned Algorand transaction carrying the anchor note.
321
+
322
+ The transaction is a 0-Algo self-payment: ``sender = receiver =
323
+ signer_address``, ``amount = 0``, note = the ARC-2 disclosed-mode
324
+ note bytes. The 0-Algo self-payment pattern is the standard way to
325
+ publish a note on Algorand without moving funds.
326
+
327
+ Args:
328
+ manifest_hash: Raw bytes of the manifest hash to anchor.
329
+ signer_address: Algorand address (58 chars base32) that will
330
+ both send and receive the 0-Algo payment.
331
+ suggested_params: ``algosdk.transaction.SuggestedParams`` from
332
+ ``algod_client.suggested_params()``. Caller must fetch this
333
+ before calling; the function does not reach out for it.
334
+ batching_profile: The batching profile identifier. Defaults to v1.
335
+
336
+ Returns:
337
+ An unsigned ``algosdk.transaction.PaymentTxn``.
338
+
339
+ Raises:
340
+ AnchorError: If py-algorand-sdk is unavailable.
341
+ """
342
+ _ensure_algosdk()
343
+ note = build_note_bytes(manifest_hash, batching_profile)
344
+ return PaymentTxn(
345
+ sender=signer_address,
346
+ sp=suggested_params,
347
+ receiver=signer_address,
348
+ amt=0,
349
+ note=note,
350
+ )
351
+
352
+
353
+ # ─────────────────────────────────────────────────────────────────
354
+ # INTERNAL HELPERS
355
+ # ─────────────────────────────────────────────────────────────────
356
+
357
+ def _ensure_algosdk() -> None:
358
+ """Raise AnchorError if py-algorand-sdk is not importable."""
359
+ if not _ALGOSDK_AVAILABLE:
360
+ raise AnchorError(
361
+ f"py-algorand-sdk is required but cannot be imported: "
362
+ f"{_ALGOSDK_ERROR}. Install or repair with: "
363
+ f"pip install 'py-algorand-sdk>=2.6.1'"
364
+ )
365
+
366
+
367
+ def _network_for_mode(mode: AnchorMode) -> str:
368
+ """Map an AnchorMode to its associated network identifier."""
369
+ if mode is AnchorMode.PRODUCTION:
370
+ return ALGORAND_MAINNET
371
+ return ALGORAND_TESTNET # DRAFT and DEMO both use testnet semantics
372
+
373
+
374
+ def _default_algod_url_for_mode(mode: AnchorMode) -> str:
375
+ if mode is AnchorMode.PRODUCTION:
376
+ return DEFAULT_ALGOD_URL_MAINNET
377
+ return DEFAULT_ALGOD_URL_TESTNET
378
+
379
+
380
+ def _make_algod_client(
381
+ algod_client: Optional[Any],
382
+ algod_url: Optional[str],
383
+ algod_token: Optional[str],
384
+ mode: AnchorMode,
385
+ ) -> Any:
386
+ """Return an AlgodClient, either passed-in or constructed from args."""
387
+ _ensure_algosdk()
388
+ if algod_client is not None:
389
+ return algod_client
390
+ url = algod_url or _default_algod_url_for_mode(mode)
391
+ token = algod_token or ""
392
+ return AlgodClient(token, url)
393
+
394
+
395
+ def _wait_for_confirmation(
396
+ algod_client: Any,
397
+ txid: str,
398
+ timeout_seconds: float,
399
+ ) -> tuple[int, str]:
400
+ """Poll algod until ``txid`` is confirmed. Returns (block_round, iso_ts).
401
+
402
+ Raises:
403
+ AnchorError: On timeout or if algod reports an error.
404
+ """
405
+ deadline = time.monotonic() + timeout_seconds
406
+ last_round_pending: Optional[int] = None
407
+ while True:
408
+ try:
409
+ tx_info = algod_client.pending_transaction_info(txid)
410
+ except Exception as exc: # noqa: BLE001
411
+ raise AnchorError(
412
+ f"algod.pending_transaction_info failed for {txid}: {exc}"
413
+ ) from exc
414
+
415
+ confirmed_round = tx_info.get("confirmed-round") or 0
416
+ if confirmed_round > 0:
417
+ confirmed_at = datetime.now(timezone.utc).isoformat().replace(
418
+ "+00:00", "Z"
419
+ )
420
+ return int(confirmed_round), confirmed_at
421
+
422
+ pool_error = tx_info.get("pool-error")
423
+ if pool_error:
424
+ raise AnchorError(
425
+ f"Transaction {txid} rejected by algod pool: {pool_error}"
426
+ )
427
+
428
+ if time.monotonic() >= deadline:
429
+ raise AnchorError(
430
+ f"Timed out after {timeout_seconds}s waiting for {txid} to "
431
+ f"confirm (last pending round seen: {last_round_pending})"
432
+ )
433
+
434
+ last_round_pending = tx_info.get("last-round")
435
+ # Algorand block time is ~3.3s; poll every 1s for responsiveness.
436
+ time.sleep(1.0)
437
+
438
+
439
+ # ─────────────────────────────────────────────────────────────────
440
+ # PUBLIC ORCHESTRATOR
441
+ # ─────────────────────────────────────────────────────────────────
442
+
443
+ def anchor_manifest(
444
+ manifest_hash: bytes,
445
+ *,
446
+ signer: Signer,
447
+ mode: AnchorMode,
448
+ algod_client: Optional[Any] = None,
449
+ algod_url: Optional[str] = None,
450
+ algod_token: Optional[str] = None,
451
+ batching_profile: str = BATCHING_PROFILE_SINGLE,
452
+ wait_for_confirmation: bool = True,
453
+ confirmation_timeout_seconds: float = DEFAULT_CONFIRMATION_TIMEOUT_SECONDS,
454
+ ) -> AnchorRecord:
455
+ """Anchor a manifest hash to Algorand.
456
+
457
+ The end-to-end flow:
458
+
459
+ 1. Build the ARC-2 disclosed-mode note bytes from ``manifest_hash``.
460
+ 2. If ``mode`` is ``DRAFT``: return an unanchored AnchorRecord.
461
+ 3. Otherwise: fetch ``suggested_params`` from algod.
462
+ 4. Build the unsigned ``PaymentTxn``.
463
+ 5. Sign via ``signer.sign_transaction``.
464
+ 6. Submit to algod.
465
+ 7. If ``wait_for_confirmation``: poll until confirmed (or timeout).
466
+ 8. Return an ``AnchorRecord`` populated with the result.
467
+
468
+ Args:
469
+ manifest_hash: Raw bytes of the manifest hash to anchor.
470
+ Typically 32 bytes (SHA-256).
471
+ signer: A ``Signer`` implementation. Provides the Algorand address
472
+ and signs the built transaction.
473
+ mode: One of ``AnchorMode.DRAFT``, ``DEMO``, or ``PRODUCTION``.
474
+ No default; must be explicit.
475
+ algod_client: Optional pre-built ``AlgodClient``. If not provided,
476
+ one is constructed from ``algod_url`` and ``algod_token``, or
477
+ from Algonode defaults based on ``mode``.
478
+ algod_url: Optional algod HTTPS URL. Defaults per mode.
479
+ algod_token: Optional algod auth token. Default empty (public nodes
480
+ accept empty tokens).
481
+ batching_profile: Batching profile identifier. v1 default.
482
+ wait_for_confirmation: If ``True`` (default), poll until the
483
+ submitted transaction confirms or the timeout expires. If
484
+ ``False``, return as soon as the transaction is submitted
485
+ with ``block_round=None`` and ``confirmed_at=None``.
486
+ confirmation_timeout_seconds: How long to wait for confirmation.
487
+ Default 60.0 seconds.
488
+
489
+ Returns:
490
+ An ``AnchorRecord`` ready to slot into a ``Receipt`` via
491
+ ``build_receipt``.
492
+
493
+ Raises:
494
+ AnchorError: If py-algorand-sdk is unavailable, the signer rejects
495
+ the transaction, algod refuses to submit, or confirmation times
496
+ out.
497
+ """
498
+ _ensure_algosdk()
499
+
500
+ # Build the note bytes. Same in all three modes; the note is the
501
+ # commitment, the transaction is the carrier.
502
+ note_bytes = build_note_bytes(manifest_hash, batching_profile)
503
+ note_payload_b64 = base64.b64encode(note_bytes[len(_ARC2_PREFIX):]).decode("ascii")
504
+
505
+ network = _network_for_mode(mode)
506
+
507
+ # DRAFT mode: don't sign, don't submit, don't fetch suggested_params.
508
+ if mode is AnchorMode.DRAFT:
509
+ logger.info(
510
+ "DRAFT anchor: built note (%d bytes) for hash %s; not submitting",
511
+ len(note_bytes), manifest_hash.hex()[:16],
512
+ )
513
+ return AnchorRecord(
514
+ network=network,
515
+ txid="",
516
+ block_round=None,
517
+ confirmed_at=None,
518
+ note_format=ARC2_NOTE_FORMAT,
519
+ note_dapp_name=ARC2_DAPP_NAME,
520
+ note_format_version=ARC2_FORMAT_VERSION_JSON,
521
+ note_payload_b64=note_payload_b64,
522
+ )
523
+
524
+ # DEMO and PRODUCTION: real submission.
525
+ client = _make_algod_client(algod_client, algod_url, algod_token, mode)
526
+
527
+ # Fetch suggested params (algod call).
528
+ try:
529
+ suggested_params = client.suggested_params()
530
+ except Exception as exc: # noqa: BLE001
531
+ raise AnchorError(
532
+ f"algod.suggested_params() failed for mode {mode.value}: {exc}"
533
+ ) from exc
534
+
535
+ # Build the unsigned transaction.
536
+ unsigned_txn = build_transaction(
537
+ manifest_hash,
538
+ signer_address=signer.address,
539
+ suggested_params=suggested_params,
540
+ batching_profile=batching_profile,
541
+ )
542
+
543
+ # Sign via the signer abstraction. The signer enforces "transactions
544
+ # only, never raw bytes" at its own boundary.
545
+ try:
546
+ signed_txn = signer.sign_transaction(unsigned_txn)
547
+ except Exception as exc: # noqa: BLE001
548
+ raise AnchorError(
549
+ f"Signer rejected transaction: {exc}"
550
+ ) from exc
551
+
552
+ # Submit to algod.
553
+ try:
554
+ txid = client.send_transaction(signed_txn)
555
+ except Exception as exc: # noqa: BLE001
556
+ raise AnchorError(
557
+ f"algod.send_transaction failed for mode {mode.value}: {exc}"
558
+ ) from exc
559
+
560
+ logger.info(
561
+ "Submitted txn %s in mode=%s, hash=%s",
562
+ txid, mode.value, manifest_hash.hex()[:16],
563
+ )
564
+
565
+ # Optionally wait for confirmation.
566
+ block_round: Optional[int] = None
567
+ confirmed_at: Optional[str] = None
568
+ if wait_for_confirmation:
569
+ block_round, confirmed_at = _wait_for_confirmation(
570
+ client, txid, confirmation_timeout_seconds,
571
+ )
572
+ logger.info(
573
+ "Confirmed txn %s in round %d at %s",
574
+ txid, block_round, confirmed_at,
575
+ )
576
+
577
+ return AnchorRecord(
578
+ network=network,
579
+ txid=txid,
580
+ block_round=block_round,
581
+ confirmed_at=confirmed_at,
582
+ note_format=ARC2_NOTE_FORMAT,
583
+ note_dapp_name=ARC2_DAPP_NAME,
584
+ note_format_version=ARC2_FORMAT_VERSION_JSON,
585
+ note_payload_b64=note_payload_b64,
586
+ )