Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 78 additions & 39 deletions .trix/client-lib/__init__.py.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ from __future__ import annotations

import json
from dataclasses import dataclass
from enum import Enum
from typing import Any

from tx3_sdk import (
Party,
Profile as SdkProfile,
Tx3Client,
Tx3ClientBuilder,
)
from tx3_sdk.core.bytes import TirEnvelope
from tx3_sdk.trp.client import TrpClient
from tx3_sdk.trp.spec import ResolveParams, SubmitParams, SubmitResponse, TxEnvelope
from tx3_sdk.facade.builder import TxBuilder
from tx3_sdk.trp.client import ClientOptions

PROTOCOL_NAME = "{{tii.protocol.name}}"
PROTOCOL_VERSION = "{{tii.protocol.version}}"
Expand All @@ -22,13 +29,13 @@ ENVIRONMENT_SCHEMA: dict[str, Any] = json.loads(
)

{{/if}}
# Profiles embedded from the TII, keyed by name. Each value carries that
# profile's `environment` values and `parties` addresses.
PROFILES: dict[str, dict[str, Any]] = json.loads(
"""{{{json tii.profiles}}}"""
{{#each tii.transactions}}
{{constantCase @key}}_TIR = TirEnvelope(
content="{{tir.content}}",
encoding="{{tir.encoding}}",
version="{{tir.version}}",
)

{{#each tii.transactions}}
@dataclass
class {{pascalCase @key}}Params:
"""Arguments for the {{@key}} transaction."""
Expand All @@ -38,51 +45,83 @@ class {{pascalCase @key}}Params:
{{/each}}


{{constantCase @key}}_TIR = TirEnvelope(
content="{{tir.content}}",
encoding="{{tir.encoding}}",
version="{{tir.version}}",
{{/each}}
{{#if tii.profiles}}
{{#each tii.profiles}}
_{{constantCase @key}}_PROFILE = SdkProfile(
environment=json.loads("""{{{json this.environment}}}"""),
parties=json.loads("""{{{json this.parties}}}"""),
)
{{/each}}


class Profile(str, Enum):
"""Profiles declared by the protocol. Required argument to `Client`."""

{{#each tii.profiles}}
{{constantCase @key}} = "{{@key}}"
{{/each}}


{{/if}}
class Client:
"""Thin protocol facade over the TRP client."""
"""Typed protocol client exposing the full transaction lifecycle.

Wraps the runtime SDK's `Tx3Client` — all lifecycle state, party storage,
resolve/sign/submit/wait, and error handling live in the SDK. This wrapper
only adds the typed shape of per-transaction methods, per-party setters,
and the embedded protocol fragments.
"""

def __init__(
self,
endpoint: str,
headers: dict[str, str] | None = None,
profile: str | None = None,
options: ClientOptions,
{{#if tii.profiles}}
profile: Profile,
{{/if}}
) -> None:
self._trp = TrpClient(endpoint=endpoint, headers=headers)
self._environment: dict[str, Any] | None = None
self._parties: dict[str, str] = {}
if profile is not None:
if profile not in PROFILES:
raise ValueError(f"unknown profile {profile!r}")
selected = PROFILES[profile]
self._environment = selected.get("environment") or None
self._parties = selected.get("parties") or {}

async def _resolve(self, tir: TirEnvelope, args: dict[str, Any]) -> TxEnvelope:
merged: dict[str, Any] = {**self._parties, **args}
return await self._trp.resolve(
ResolveParams(tir=tir, args=merged, env=self._environment)
)
transactions: dict[str, TirEnvelope] = {
{{#each tii.transactions}}
"{{@key}}": {{constantCase @key}}_TIR,
{{/each}}
}
{{#if tii.profiles}}
profiles: dict[str, SdkProfile] = {
{{#each tii.profiles}}
"{{@key}}": _{{constantCase @key}}_PROFILE,
{{/each}}
}
{{else}}
profiles: dict[str, SdkProfile] = {}
{{/if}}

async def {{snakeCase @key}}(self, args: {{pascalCase @key}}Params) -> TxEnvelope:
"""Resolves the {{@key}} transaction."""
return await self._resolve(
{{constantCase @key}}_TIR,
builder = Tx3ClientBuilder.from_parts(transactions, profiles, [])
builder = builder.trp(options)
{{#if tii.profiles}}
builder = builder.with_profile(profile.value)
{{/if}}
self._inner: Tx3Client = builder.build()

{{#each tii.parties}}
def with_{{snakeCase @key}}(self, party: Party) -> "Client":
"""Binds the `{{@key}}` party. Overrides any address declared for that
party by the selected profile.
"""
self._inner = self._inner.with_party_unchecked("{{@key}}", party)
return self

{{/each}}
{{#each tii.transactions}}
def {{snakeCase @key}}(self, args: {{pascalCase @key}}Params) -> TxBuilder:
"""Builds the `{{@key}}` transaction. Drive the returned builder with
`.resolve()` and the rest of the lifecycle chain.
"""
return self._inner.tx("{{@key}}").args(
{
{{#each params.properties}}
"{{@key}}": args.{{snakeCase @key}},
{{/each}}
},
}
)
{{/each}}

async def submit(self, params: SubmitParams) -> SubmitResponse:
"""Submits a signed transaction to the network."""
return await self._trp.submit(params)
{{/each}}
51 changes: 40 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,52 +27,61 @@ pip install tx3-sdk
```python
import asyncio

from tx3_sdk import CardanoSigner, Party, PollConfig, Protocol, TrpClient, Tx3Client
from tx3_sdk import CardanoSigner, Party, PollConfig, Protocol


async def main() -> None:
# 1) Load a compiled .tii protocol
protocol = Protocol.from_file("examples/transfer.tii")

# 2) Create a low-level TRP client
trp = TrpClient(endpoint="https://preprod.trp.tx3.dev")

# 3) Configure signer and parties
# 2) Build a client: configure TRP, profile, and parties on the builder
sender_signer = CardanoSigner.from_mnemonic(
address="addr_test1qz...",
phrase="word1 word2 ... word24",
)

client = (
Tx3Client(protocol, trp)
protocol.client()
.trp_endpoint("https://preprod.trp.tx3.dev")
.with_profile("preprod")
.with_party("sender", Party.signer(sender_signer))
.with_party("receiver", Party.address("addr_test1qz..."))
.build()
)

# 4) Build, resolve, sign, submit
# 3) Build, resolve, sign, submit
submitted = await (
await (
await client.tx("transfer").arg("quantity", 10_000_000).resolve()
).sign()
).submit()

# 5) Wait for confirmation
# 4) Wait for confirmation
status = await submitted.wait_for_confirmed(PollConfig.default())
print(f"Confirmed at stage: {status.stage}")


asyncio.run(main())
```

All fallible validation — TRP endpoint present, profile declared, every bound
party declared — happens inside `build()`, which raises `MissingTrpEndpointError`,
`UnknownProfileError`, or `UnknownPartyError` (all under the `BuilderError` family,
rooted at `Tx3Error`). Optional setters never raise, so chains stay fluent. Profile
selection is **builder-only**: there is no profile-switching method on the built
client. Switching profiles requires a new builder.

## Concepts

| SDK Type | Glossary Term | Description |
|---|---|---|
| `Protocol` | TII / Protocol | Loaded `.tii` with transactions, parties, and profiles |
| `Tx3Client` | Facade | High-level client holding protocol + TRP + party bindings |
| `TxBuilder` | Invocation builder | Collects args and resolves transactions |
| `Protocol` | TII / Protocol | Loaded `.tii` with transactions, parties, and profiles. `protocol.client()` returns a fresh `Tx3ClientBuilder` |
| `Tx3ClientBuilder` | Client builder | Fluent builder seeded by `Protocol.client()` or `Tx3ClientBuilder.from_parts(...)`; absorbs all fallible validation in `build()` |
| `Tx3Client` | Facade | Output of `Tx3ClientBuilder.build()` — owns the deconstructed protocol parts, TRP client, profile, and party bindings |
| `TxBuilder` | Invocation builder | Source-agnostic; collects args and resolves transactions |
| `Party` | Party | `Party.address(...)` or `Party.signer(...)` |
| `Profile` | Profile | `{ environment, parties }` value baked into the client; embedded by codegen plugins, decomposed from `Protocol` by `from_protocol` |
| `MissingTrpEndpointError` / `UnknownPartyError` | Builder errors | Raised by `build()`; subclass of `BuilderError`, rooted at `Tx3Error` |
| `Signer` | Signer | Protocol producing a `TxWitness` for a `SignRequest` |
| `SignRequest` | SignRequest | Input passed to `Signer.sign`: `tx_hash_hex` + `tx_cbor_hex` |
| `CardanoSigner` | Cardano Signer | BIP32-Ed25519 signer at `m/1852'/1815'/0'/0/0` |
Expand All @@ -84,6 +93,26 @@ asyncio.run(main())

## Advanced usage

### Skipping the runtime `.tii` (codegen flow)

If you've run `trix codegen` to generate typed bindings, your generated `Client`
embeds the per-transaction TIR envelopes and per-profile data at codegen time —
no `.tii` artifact at runtime. Under the hood it seeds the same builder via
`Tx3ClientBuilder.from_parts(transactions, profiles, known_parties)` and routes
typed per-party setters through `with_party_unchecked`. You can also call
`from_parts` directly from hand-written code:

```python
from tx3_sdk import ClientOptions, Party, Tx3ClientBuilder

client = (
Tx3ClientBuilder.from_parts(transactions, profiles, ["sender", "receiver"])
.trp(ClientOptions(endpoint="http://localhost:8000"))
.with_party_unchecked("sender", Party.signer(signer))
.build()
)
```

### Low-level TRP client

```python
Expand Down
13 changes: 12 additions & 1 deletion sdk/src/tx3_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,36 @@

from tx3_sdk.core.args import ArgValue, coerce_arg
from tx3_sdk.facade.client import Tx3Client
from tx3_sdk.facade.client_builder import Tx3ClientBuilder
from tx3_sdk.facade.errors import (
BuilderError,
MissingTrpEndpointError,
)
from tx3_sdk.facade.party import Party
from tx3_sdk.facade.poll import PollConfig
from tx3_sdk.facade.profile import Profile
from tx3_sdk.signer.cardano import CardanoSigner
from tx3_sdk.signer.ed25519 import Ed25519Signer
from tx3_sdk.signer.signer import SignRequest, Signer
from tx3_sdk.tii.protocol import Protocol
from tx3_sdk.trp.client import TrpClient
from tx3_sdk.trp.client import ClientOptions, TrpClient

__all__ = [
"ArgValue",
"BuilderError",
"CardanoSigner",
"ClientOptions",
"Ed25519Signer",
"MissingTrpEndpointError",
"Party",
"PollConfig",
"Profile",
"Protocol",
"SignRequest",
"Signer",
"TrpClient",
"Tx3Client",
"Tx3ClientBuilder",
"coerce_arg",
]

Expand Down
8 changes: 8 additions & 0 deletions sdk/src/tx3_sdk/facade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,39 @@

from tx3_sdk.facade.builder import TxBuilder
from tx3_sdk.facade.client import Tx3Client
from tx3_sdk.facade.client_builder import Tx3ClientBuilder
from tx3_sdk.facade.errors import (
BuilderError,
FinalizedFailedError,
FinalizedTimeoutError,
MissingParamsError,
MissingTrpEndpointError,
SubmitHashMismatchError,
UnknownArgError,
UnknownPartyError,
)
from tx3_sdk.facade.party import Party
from tx3_sdk.facade.poll import PollConfig
from tx3_sdk.facade.profile import Profile
from tx3_sdk.facade.resolved import ResolvedTx
from tx3_sdk.facade.signed import SignedTx
from tx3_sdk.facade.submitted import SubmittedTx

__all__ = [
"BuilderError",
"FinalizedFailedError",
"FinalizedTimeoutError",
"MissingParamsError",
"MissingTrpEndpointError",
"Party",
"PollConfig",
"Profile",
"ResolvedTx",
"SignedTx",
"SubmitHashMismatchError",
"SubmittedTx",
"Tx3Client",
"Tx3ClientBuilder",
"TxBuilder",
"UnknownArgError",
"UnknownPartyError",
Expand Down
Loading
Loading