ID: task_300726_8
Assigned to: Santiago Rodriguez Cetran (@Santiagocetran)
Created: 2026-07-30
Status: Open
Repo: https://github.com/InfiniteZeroFoundation/DevNet
Base branch: feat/din-sdk (PR #31, draft) — feat/din-daemon (PR #32) stays stacked on top, no changes expected there this task
Roadmap ref: P4-1.2 (remaining slice); resolves BL-2, addresses BL-1 as part of tx.py
Dates: Jul 31–Aug 6, 2026 (5 working days; deadline Thursday, Aug 6)
This is the task flagged as "next" in three places: task_220726_7.md's scope boundary #1 and closing note, Santiago's own progress updates on Discussion #50 ("deferred next task, flagged for @umeradl — the wallet/session/tx keystone"), and Umer's proposal review on issue #20 ("§2, §5b's core design... good to build on"). Both Part A and Part B of task_220726_7 landed clean (PR #31 @ 6eac109, PR #32 @ 99f060b) with no scope adjustments needed, and their code reviews (pr31_code_review.md, pr32_code_review.md) found no correctness blockers — see those docs for the full independent verification if useful background.
What this task builds, per Developer/design/sdk-interface-proposal.md §2, §5b (approved, with the open questions below to resolve as you go — not blockers to starting):
dincli/sdk/session.py—DinSession+ theSignerProviderprotocol (§2)dincli/sdk/wallet.py— non-interactive keystore/account resolution (§8's package layout: "fromutils.py+system.connect_wallet")dincli/sdk/tx.py—send()+decode_events()+ tx-param building, replacingutils.py::build_and_send_tx(§5b)
Deliberately not in this task: the sdk/operations/ role-command layer (registry.py, gi.py, aggregation.py, auditor.py, client.py, model.py, dao.py) and the plan/apply confirmation pattern (§6) that sits on top of it. Those consume DinSession/send() but are a substantial, separately-scoped follow-on — flag as the next task rather than starting early, same convention as this task itself was deferred out of task_220726_7. dind's JOB_HANDLERS registry stays a no-op "demo" entry until operations/ lands (see BL-9).
Scope boundaries — read before starting:
- This is the security-sensitive slice — private key handling, signing, transaction submission. Take the same care task_220726_7 explicitly deferred this for. If anything about the keystore/signing flow feels underspecified once you're in the code, stop and ask rather than guessing at a security-relevant default.
- No CLI command behavior change.
cli/utils.py::build_and_send_tx,system.py::connect_wallet, and every caller ofload_accountkeep their current exit codes and printed output — same re-export-shim pattern as the prior three extraction waves.dincli/sdk/tx.py/wallet.py/session.pyare additive;cli/*becomes a thin wrapper over them. operations/and plan/apply (§6) are out of scope, per above — don't start them even if the week's budget allows.dindgets no new job handlers.dincli/dind/jobs.py'sJOB_HANDLERSregistry stays untouched — daemon automation that actually signs/submits is downstream ofoperations/, not this task.- Daemon-side config/reload commands are out of scope for implementation, but see item 1 below — the doc gap Umer flagged needs fixing regardless of whether
dind config/dind load configget built this week.
Per proposal §2 (already approved):
# dincli/sdk/session.py
class DinSession:
"""Lazily resolves network, web3, account, config. No printing, no exit, no prompts."""
def __init__(self, network: str | None = None, wallet: str | None = None,
signer: "SignerProvider | None" = None): ...
@property
def network(self) -> str: ...
@property
def w3(self) -> Web3: ... # raises NetworkError if RPC unreachable
@property
def account(self) -> LocalAccount:
"""Resolve the signing account via the configured SignerProvider.
NEVER calls getpass or any interactive prompt. Raises SignerUnavailable
if no non-interactive credential is available, WalletError if the
keystore is malformed or missing."""
@property
def config(self) -> dict: ...
@runtime_checkable
class SignerProvider(Protocol):
def address(self) -> str: ... # raises SignerUnavailable if none resolvable
def can_decrypt(self) -> bool: ... # True if usable key resolvable without further input
def sign_transaction(self, tx: dict) -> SignedTransaction: ...
# raises SignerUnavailable (no credential), WalletError (bad keystore/decrypt failure)This replaces the runtime-resolution parts of DinContext (cli/context.py) minus console/exit/prompting — read DinContext end-to-end first to confirm exactly which resolution logic (network, w3, account, config) moves here vs. what's already SDK-side (sdk/web3.py::get_w3, sdk/config.py).
Two adapters, not implemented in the SDK itself:
- CLI adapter (lives in
cli/, wrapsSignerProvider) — wraps the existing keystore flow incli/utils.py::load_account; it is allowed to prompt viagetpass(that's the interactive boundary), then hands the resolved signer intoDinSession. - Daemon adapter (lives in
dincli/dind/) — backed by a configured password source, must never prompt; any missing credential raisesSignerUnavailable, whichdindturns into a job error rather than a hang.
Doc/design gap to close as part of this work (from Umer's §2 review on issue #20, not yet reflected in the proposal doc): dind start prompts interactively for wallet/password once at process start, and the resolved signer stays in memory for that daemon's lifetime. Config can be added/changed later while the daemon is running, but only through a separate dind config command (interactive there is fine, same pattern as dincli system connect-wallet) plus a non-interactive dind load config trigger that just points the running daemon at already-resolved state — the daemon process itself never blocks on stdin mid-run. Document the daemon SignerProvider adapter against that session-held, interactively-bootstrapped state, not a raw env-var lookup, in whatever module/docstring ends up describing the adapter. You don't have to build dind config/dind load config this task (scope boundary #5) — just make sure the adapter's contract and docstring don't imply raw env-var-only resolution, since that's the gap Umer flagged.
Per §8's package layout: "keystore/account/demo-key, non-interactive signing (from utils.py + system.connect_wallet)". Grep cli/utils.py and cli/system.py for the current keystore/account/demo-key logic (load_account and anything connect-wallet depends on) and extract the non-interactive parts here — same verbatim-extraction discipline as state.py in task_220726_7 (snapshot current behavior before moving, diff after). The interactive getpass prompt itself stays in the CLI adapter (item 1), not in wallet.py.
Concretely implement the SignerProvider contract from item 1 against the existing keystore format (§7's backward-compat guarantee: no changes to on-disk keystore format/location).
# dincli/sdk/tx.py
@dataclass
class TxReceiptInfo:
tx_hash: str # 0x hex
status: int # 1 success, 0 revert
block_number: int
gas_used: int
nonce: int
contract_address: str | None # set for deployment txs (deploy flows read this)
logs: list[dict] # raw, JSON-serializable log entries
_raw: "TxReceipt" = field(repr=False, metadata={"json": "omit"})
# web3 AttributeDict receipt, kept for in-SDK event decoding; NOT serialized
def send(session: DinSession, contract_function, *, tx_params: dict | None = None,
on_event: Callable[[str, dict], None] | None = None) -> TxReceiptInfo:
"""Estimate -> build -> sign -> send -> wait. Returns receipt info.
Raises TransactionError with subcode tx_estimation_failed | tx_reverted | tx_timeout |
tx_nonce_conflict | tx_replacement_underpriced | receipt_missing (sdk/errors.py)."""
def decode_events(receipt: TxReceiptInfo, contract_event) -> list[dict]:
"""Decode a specific event from a receipt, e.g. registry.events.ModelRegistrationRequested().
Wraps web3's process_receipt so a future operations layer can build typed results
without exposing raw web3 objects to consumers."""Replaces cli/utils.py::build_and_send_tx — that function becomes a thin wrapper calling send() and mapping TransactionError to today's console.print + typer.Exit(1) behavior (scope boundary #2: identical CLI exit codes/output).
Retry-safety shape (§10, already resolved): on failure, TransactionError.details must carry tx_hash if broadcast, nonce if known, and a broadcast: bool flag distinguishing pre-broadcast (safe to rebuild) from post-broadcast (must confirm the existing tx, don't resend) failures. On success, TxReceiptInfo carries tx_hash/nonce/status — that's the full retry-decision surface a future daemon job-retry policy needs.
Four open design questions from Umer's §5b review — resolve these as you build, not upfront in a separate design doc:
- Nonce management (
BL-1, weigh this one heaviest).context.py::get_tx_params()currently callsget_transaction_count(address)with no block param (defaults to"latest", confirmed-only) — two transactions fired back-to-back before the first confirms collide on the same nonce today. Decide and document: doessend()itself prevent this for concurrent submission (e.g. track an in-process nonce counter, or use"pending"block param), or is that a layer abovesend()'s responsibility? Whichever you pick, fix the live gap inget_tx_params()'s nonce fetch as part of this task — don't leave it as a documented-but-unfixed known issue. on_eventcallback vocabulary. The proposal only shows"submitted"as an example event name. Define the full setsend()actually emits (e.g.submitted,confirmed,estimation_failed,reverted— your call on exact names) and document it in thesend()docstring so CLI and daemon consumers both know what to listen for.- Timeout/polling parameterization.
send()'s signature above has no timeout/poll-interval params, but CLI (interactive, human waiting) and daemon (background job, different patience) likely want different wait behavior. Decide whether to add explicit params (e.g.timeout_s,poll_interval_s) with sensible defaults, or handle this a different way — document the choice. - Replacement transactions.
tx_replacement_underpricedis a defined error subcode but the replacement flow itself isn't designed. Decide: is replacing a stuck tx a freshsend()call with a manually bumped nonce/gas (caller's responsibility), or doestx.pyneed its ownreplace()function? A fresh-send()-with-bumped-params approach is probably enough for this task — don't over-build a dedicated replacement flow unless the four-question review implies otherwise once you're in the code.
tests/test_sdk_session.py(new):DinSessionnever prompts (assert nogetpass/stdin access — same style as the existing import-boundary tests);SignerProviderprotocol conformance;SignerUnavailable/WalletErrorraised on the documented conditions, not silently swallowed.tests/test_sdk_wallet.py(new): keystore load/decrypt round-trip against the existing on-disk format (no format change, per scope boundary #2); non-interactive resolution never callsgetpass.tests/test_sdk_tx.py(new):send()'s happy path (mock web3, assertTxReceiptInfofields populate correctly, includingcontract_addresson a deployment-shaped mock andlogson an event-emitting one); eachTransactionErrorsubcode raised on its corresponding failure mode (estimation failure, revert, timeout, nonce conflict);TransactionError.detailscarries the rightbroadcast: bool/tx_hash/nonceshape per §10 on at least the pre-broadcast and post-broadcast failure cases;decode_events()against a mock receipt.- Extend
tests/test_sdk_boundary.py's fresh-subprocess import check to includesession,wallet,txin the module list asserted clean oftyper/rich/dincli.cli.*. - Golden CLI-compatibility check (per proposal §7): existing
build_and_send_txcallers (dinrep.py,modelownerd/deploy.py,task.py, etc.) keep identical exit codes and printed output once wrapped oversend()— spot-check at least the deploy and one registry-write call site by hand, don't just trust the existing test suite catches every regression here given how security-sensitive this path is. - Full suite green on both
feat/din-sdkandfeat/din-daemonbefore opening/updating PRs, same convention as task_220726_7.
-
dincli/sdk/session.py—DinSession+SignerProviderper §2, with the daemon-adapter doc gap (session-held bootstrapped state, not raw env lookup) fixed in the adapter's docstring/contract -
dincli/sdk/wallet.py— non-interactive keystore/account resolution extracted fromcli/utils.py+cli/system.py, old call sites re-exported as a shim -
dincli/sdk/tx.py—send()+decode_events()+ tx-param building per §5b, replacingbuild_and_send_tx's internals;cli/utils.py::build_and_send_txbecomes a thin wrapper with identical exit codes/output - Nonce-fetch gap (
BL-1) fixed as part ofsend()/get_tx_params(), with the chosen approach documented - The three remaining §5b open questions (event vocabulary, timeout/polling, replacement flow) resolved and documented in code/docstrings, not left as open questions in a follow-up doc
- New SDK tests per section 4; import-boundary test extended to cover
session,wallet,tx - Existing test suite still green on both PRs — no CLI behavior change
- PR #31 updated with the new commits; PR body notes this slice covers §2/§5b of the SDK proposal
- Discussion posted on #50, referencing this task file, flagging
sdk/operations/+ plan/apply (§6) as the explicitly-deferred next slice
- Release valve if the 5-day budget runs tight: the replacement-transaction question (item 3, #4) is the most deferrable — a documented "not yet implemented, caller must construct a fresh
send()call with bumped params" is an acceptable placeholder if time runs out, since nothing calls it yet. Do not cut the nonce-management fix (BL-1) or the coresend()/SignerProvidercontract — those are load-bearing for everything downstream, including theoperations/layer's next task. - The next task after this one is
sdk/operations/+ plan/apply (§6). Per Umer's §6 review, the priority order there is aggregator/auditor flows first (least configuration,dind's primary automation surface), thengi.start, withregister_request/client/model-owner tooling explicitly lower priority (separate tooling, notdind's job — seeBL-4/BL-5). Flag it in the closing discussion post rather than starting early, same as this task was flagged rather than started under task_220726_7. - Re-pull and re-run the full suite on
feat/din-sdkright before opening/updating the PR, per the existing convention —develop's tip may have moved.