archerprotocol-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.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: archerprotocol-sdk
3
+ Version: 0.1.0
4
+ Summary: Build and monetize an Archer intent from Python: typed envelope builders, embeds, caller context, effect inference, and a one-call FastAPI handler.
5
+ License: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/Archer-Laboratories/archer-sdk
7
+ Project-URL: Repository, https://github.com/Archer-Laboratories/archer-sdk
8
+ Project-URL: Issues, https://github.com/Archer-Laboratories/archer-sdk/issues
9
+ Keywords: archer,archer-protocol,intent,partner-sdk,fastapi,ed25519
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: archer-verify>=0.1.0
16
+ Provides-Extra: fastapi
17
+ Requires-Dist: fastapi>=0.110; extra == "fastapi"
18
+ Requires-Dist: starlette>=0.36; extra == "fastapi"
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8; extra == "dev"
21
+ Requires-Dist: httpx>=0.27; extra == "dev"
22
+ Requires-Dist: fastapi>=0.110; extra == "dev"
23
+
24
+ # archerprotocol-sdk
25
+
26
+ Build, monetize, and publish an Archer intent from Python. This package
27
+ mirrors the Node `@archerprotocol/sdk` builder surface; both are held to the
28
+ same shared test vectors, so an envelope built here is byte-identical to one
29
+ built in TypeScript.
30
+
31
+ ```bash
32
+ pip install "archerprotocol-sdk[fastapi]"
33
+ ```
34
+
35
+ ## One-call handler (FastAPI)
36
+
37
+ ```python
38
+ from fastapi import FastAPI
39
+ from archer_sdk import archer, require_archer_caller, BadInputError
40
+ from archer_sdk.fastapi import mount_archer_tools
41
+
42
+ app = FastAPI()
43
+
44
+ async def deposit(ctx):
45
+ caller = require_archer_caller(ctx.args)
46
+ amount = ctx.args.get("amount")
47
+ if not amount:
48
+ raise BadInputError('amount is required (e.g. "1.5")')
49
+ return archer.envelope(
50
+ cost=archer.cost(model="embedded", archer_fee_micro=200),
51
+ authorizations=[
52
+ archer.authorization(
53
+ type="tx",
54
+ namespace="evm",
55
+ label=f"Deposit {amount} USDC",
56
+ payload={"chainId": 8453, "to": "0x...", "data": "0x...", "value": "0x0"},
57
+ )
58
+ ],
59
+ record=archer.record(
60
+ record_kind="position",
61
+ basis_micro=1_000_000,
62
+ disclosure={"custody": "SELF", "exitModel": "PERMISSIONLESS"},
63
+ ),
64
+ )
65
+
66
+ mount_archer_tools(app, {"/deposit": deposit})
67
+ ```
68
+
69
+ The handler verifies the signed Archer envelope (via `archer-verify`),
70
+ hands you `ctx.args / ctx.caller / ctx.request_id / ctx.intent_definition_id /
71
+ ctx.user_id`, and wraps your return in the partner response envelope.
72
+ Raise `BadInputError` (400) or `UpstreamError` (503) for clean failures.
73
+
74
+ ## Reads
75
+
76
+ Return the flat render payload instead of an envelope:
77
+
78
+ ```python
79
+ from archer_sdk import archer
80
+
81
+ async def price_chart(ctx):
82
+ return archer.render(
83
+ text="ETH, last 30 days",
84
+ embed=archer.embed.chart(chart_type="line", data=[{"x": "2026-01", "y": 3000}]),
85
+ )
86
+ ```
87
+
88
+ ## Effect inference (pre-flight)
89
+
90
+ `infer_effects` is the same function the platform runs server-side to decode
91
+ a `sign` authorization into a verified, bounded confirmation. Run it locally
92
+ against your descriptors to see exactly what the user's confirmation screen
93
+ will derive; the platform always re-derives its own report.
94
+
95
+ ```python
96
+ from archer_sdk import infer_effects
97
+
98
+ report = await infer_effects(auth, descriptors=my_descriptors, price=my_price_fn)
99
+ assert report["verdict"] == "DECODED_BOUNDED"
100
+ ```
101
+
102
+ ## Publishing
103
+
104
+ Publishing and lifecycle management use the Node CLI, which works without a
105
+ Node project: `npx @archerprotocol/sdk` gives you `archer init / publish /
106
+ activate / stats`.
@@ -0,0 +1,83 @@
1
+ # archerprotocol-sdk
2
+
3
+ Build, monetize, and publish an Archer intent from Python. This package
4
+ mirrors the Node `@archerprotocol/sdk` builder surface; both are held to the
5
+ same shared test vectors, so an envelope built here is byte-identical to one
6
+ built in TypeScript.
7
+
8
+ ```bash
9
+ pip install "archerprotocol-sdk[fastapi]"
10
+ ```
11
+
12
+ ## One-call handler (FastAPI)
13
+
14
+ ```python
15
+ from fastapi import FastAPI
16
+ from archer_sdk import archer, require_archer_caller, BadInputError
17
+ from archer_sdk.fastapi import mount_archer_tools
18
+
19
+ app = FastAPI()
20
+
21
+ async def deposit(ctx):
22
+ caller = require_archer_caller(ctx.args)
23
+ amount = ctx.args.get("amount")
24
+ if not amount:
25
+ raise BadInputError('amount is required (e.g. "1.5")')
26
+ return archer.envelope(
27
+ cost=archer.cost(model="embedded", archer_fee_micro=200),
28
+ authorizations=[
29
+ archer.authorization(
30
+ type="tx",
31
+ namespace="evm",
32
+ label=f"Deposit {amount} USDC",
33
+ payload={"chainId": 8453, "to": "0x...", "data": "0x...", "value": "0x0"},
34
+ )
35
+ ],
36
+ record=archer.record(
37
+ record_kind="position",
38
+ basis_micro=1_000_000,
39
+ disclosure={"custody": "SELF", "exitModel": "PERMISSIONLESS"},
40
+ ),
41
+ )
42
+
43
+ mount_archer_tools(app, {"/deposit": deposit})
44
+ ```
45
+
46
+ The handler verifies the signed Archer envelope (via `archer-verify`),
47
+ hands you `ctx.args / ctx.caller / ctx.request_id / ctx.intent_definition_id /
48
+ ctx.user_id`, and wraps your return in the partner response envelope.
49
+ Raise `BadInputError` (400) or `UpstreamError` (503) for clean failures.
50
+
51
+ ## Reads
52
+
53
+ Return the flat render payload instead of an envelope:
54
+
55
+ ```python
56
+ from archer_sdk import archer
57
+
58
+ async def price_chart(ctx):
59
+ return archer.render(
60
+ text="ETH, last 30 days",
61
+ embed=archer.embed.chart(chart_type="line", data=[{"x": "2026-01", "y": 3000}]),
62
+ )
63
+ ```
64
+
65
+ ## Effect inference (pre-flight)
66
+
67
+ `infer_effects` is the same function the platform runs server-side to decode
68
+ a `sign` authorization into a verified, bounded confirmation. Run it locally
69
+ against your descriptors to see exactly what the user's confirmation screen
70
+ will derive; the platform always re-derives its own report.
71
+
72
+ ```python
73
+ from archer_sdk import infer_effects
74
+
75
+ report = await infer_effects(auth, descriptors=my_descriptors, price=my_price_fn)
76
+ assert report["verdict"] == "DECODED_BOUNDED"
77
+ ```
78
+
79
+ ## Publishing
80
+
81
+ Publishing and lifecycle management use the Node CLI, which works without a
82
+ Node project: `npx @archerprotocol/sdk` gives you `archer init / publish /
83
+ activate / stats`.
@@ -0,0 +1,55 @@
1
+ """Archer contribution SDK for Python: build, monetize, and publish an
2
+ Archer intent. Envelope builders, GUI embeds, caller context, effect
3
+ inference, and a one-call FastAPI handler (archer_sdk.fastapi). Mirrors the
4
+ Node @archerprotocol/sdk; the shared test-vectors enforce parity."""
5
+
6
+ from types import SimpleNamespace
7
+
8
+ from . import embed as _embed
9
+ from .builders import (
10
+ authorization,
11
+ cost,
12
+ data,
13
+ envelope,
14
+ is_invocation_envelope,
15
+ record,
16
+ )
17
+ from .caller import ArcherCaller, get_archer_caller, require_archer_caller
18
+ from .effects import infer_effects
19
+ from .errors import BadInputError, UpstreamError
20
+ from .handler import ArcherHandler, HandlerContext, run_invocation
21
+ from .money import to_micro, to_micro_optional
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ # The one-object surface most handlers want: archer.envelope(...), archer.render(...).
26
+ archer = SimpleNamespace(
27
+ data=data,
28
+ cost=cost,
29
+ authorization=authorization,
30
+ record=record,
31
+ envelope=envelope,
32
+ embed=_embed,
33
+ render=_embed.render,
34
+ )
35
+
36
+ __all__ = [
37
+ "archer",
38
+ "authorization",
39
+ "cost",
40
+ "data",
41
+ "envelope",
42
+ "record",
43
+ "is_invocation_envelope",
44
+ "get_archer_caller",
45
+ "require_archer_caller",
46
+ "ArcherCaller",
47
+ "infer_effects",
48
+ "BadInputError",
49
+ "UpstreamError",
50
+ "ArcherHandler",
51
+ "HandlerContext",
52
+ "run_invocation",
53
+ "to_micro",
54
+ "to_micro_optional",
55
+ ]
@@ -0,0 +1,149 @@
1
+ """Envelope builders for the generic Archer invocation contract. Inputs take
2
+ pythonic snake_case keywords; outputs are plain dicts whose keys and shapes
3
+ are byte-identical to the Node SDK's JSON (the shared test-vectors enforce
4
+ this). Optional fields are omitted when absent, never set to null."""
5
+
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from .money import Money, to_micro, to_micro_optional
9
+
10
+ _COST_MODELS = {"flat", "percent", "x402", "embedded", "none"}
11
+
12
+
13
+ def _prune(obj: Dict[str, Any]) -> Dict[str, Any]:
14
+ return {k: v for k, v in obj.items() if v is not None}
15
+
16
+
17
+ def data(value: Any) -> Dict[str, Any]:
18
+ """The Render arm."""
19
+ return {"data": value}
20
+
21
+
22
+ def cost(
23
+ *,
24
+ model: str,
25
+ amount_micro: Optional[Money] = None,
26
+ value_micro: Optional[Money] = None,
27
+ bps: Optional[int] = None,
28
+ bob_fee_micro: Optional[Money] = None,
29
+ archer_fee_micro: Optional[Money] = None,
30
+ ) -> Dict[str, Any]:
31
+ """The Settle arm: a declared fee model plus its parameters."""
32
+ if model in ("flat", "x402"):
33
+ params = _prune(
34
+ {
35
+ "amountMicro": to_micro(amount_micro) if amount_micro is not None else None,
36
+ "archerFeeMicro": to_micro_optional(archer_fee_micro),
37
+ }
38
+ )
39
+ elif model == "percent":
40
+ params = _prune(
41
+ {
42
+ "valueMicro": to_micro(value_micro) if value_micro is not None else None,
43
+ "bps": bps,
44
+ "archerFeeMicro": to_micro_optional(archer_fee_micro),
45
+ }
46
+ )
47
+ elif model == "embedded":
48
+ params = _prune(
49
+ {
50
+ "bobFeeMicro": to_micro_optional(bob_fee_micro),
51
+ "archerFeeMicro": to_micro_optional(archer_fee_micro),
52
+ }
53
+ )
54
+ elif model == "none":
55
+ params = {}
56
+ else:
57
+ raise ValueError(f'unknown fee model "{model}" (expected one of {sorted(_COST_MODELS)})')
58
+ return {"model": model, "params": params}
59
+
60
+
61
+ def authorization(
62
+ *,
63
+ type: str,
64
+ label: str,
65
+ payload: Dict[str, Any],
66
+ namespace: Optional[str] = None,
67
+ ) -> Dict[str, Any]:
68
+ """One entry of the Authorize arm. The label is the human disclosure."""
69
+ if not label or not isinstance(label, str):
70
+ raise ValueError("authorization label is required (human-readable disclosure)")
71
+ return _prune(
72
+ {
73
+ "type": type,
74
+ "namespace": namespace,
75
+ "label": label,
76
+ "payload": payload if payload is not None else {},
77
+ }
78
+ )
79
+
80
+
81
+ def record(
82
+ *,
83
+ record_kind: str,
84
+ basis_micro: Optional[Money] = None,
85
+ value_micro: Optional[Money] = None,
86
+ bob_fee_micro: Optional[Money] = None,
87
+ archer_fee_micro: Optional[Money] = None,
88
+ value_read: Optional[Dict[str, Any]] = None,
89
+ exit_instructions: Optional[Dict[str, Any]] = None,
90
+ ref: Optional[str] = None,
91
+ group_key: Optional[str] = None,
92
+ view_url: Optional[str] = None,
93
+ disclosure: Optional[Dict[str, Any]] = None,
94
+ ) -> Dict[str, Any]:
95
+ """The Record arm: a durable, user-owned outcome (opaque to the engine)."""
96
+ if not record_kind or not isinstance(record_kind, str):
97
+ raise ValueError("record_kind is required")
98
+ return _prune(
99
+ {
100
+ "recordKind": record_kind,
101
+ "basisMicro": to_micro_optional(basis_micro),
102
+ "valueMicro": to_micro_optional(value_micro),
103
+ "bobFeeMicro": to_micro_optional(bob_fee_micro),
104
+ "archerFeeMicro": to_micro_optional(archer_fee_micro),
105
+ "valueRead": value_read,
106
+ "exitInstructions": exit_instructions,
107
+ "ref": ref,
108
+ "groupKey": group_key,
109
+ "viewUrl": view_url,
110
+ "disclosure": disclosure,
111
+ }
112
+ )
113
+
114
+
115
+ def envelope(
116
+ *,
117
+ data: Any = None,
118
+ cost: Optional[Dict[str, Any]] = None,
119
+ authorizations: Optional[List[Dict[str, Any]]] = None,
120
+ record: Optional[Dict[str, Any]] = None,
121
+ ) -> Dict[str, Any]:
122
+ """Assemble the arms into one invocation envelope, validated structurally."""
123
+ env = _prune(
124
+ {"data": data, "cost": cost, "authorizations": authorizations, "record": record}
125
+ )
126
+ if not is_invocation_envelope(env):
127
+ raise ValueError(
128
+ "envelope arms are malformed (cost needs a model, record needs a recordKind, "
129
+ "authorizations must be a list)"
130
+ )
131
+ return env
132
+
133
+
134
+ def is_invocation_envelope(value: Any) -> bool:
135
+ """Structural check mirroring the engine's isInvocationEnvelope."""
136
+ if value is None or not isinstance(value, dict):
137
+ return False
138
+ if "authorizations" in value and value["authorizations"] is not None:
139
+ if not isinstance(value["authorizations"], list):
140
+ return False
141
+ if "cost" in value and value["cost"] is not None:
142
+ c = value["cost"]
143
+ if not isinstance(c, dict) or not isinstance(c.get("model"), str):
144
+ return False
145
+ if "record" in value and value["record"] is not None:
146
+ r = value["record"]
147
+ if not isinstance(r, dict) or not isinstance(r.get("recordKind"), str):
148
+ return False
149
+ return True
@@ -0,0 +1,39 @@
1
+ """Caller context. Archer injects the invoking user's context inside the
2
+ signed args as args["archerCaller"], so it is verified as part of the
3
+ envelope. Read it here."""
4
+
5
+ from typing import Any, Dict, Optional, TypedDict
6
+
7
+
8
+ class ArcherCaller(TypedDict):
9
+ evmAddress: Optional[str]
10
+ svmAddress: Optional[str]
11
+ archerFeeRate: Optional[float]
12
+
13
+
14
+ def get_archer_caller(args: Dict[str, Any]) -> Optional[ArcherCaller]:
15
+ raw = args.get("archerCaller") if isinstance(args, dict) else None
16
+ if raw is None or not isinstance(raw, dict):
17
+ return None
18
+ evm = raw.get("evmAddress")
19
+ svm = raw.get("svmAddress")
20
+ rate = raw.get("archerFeeRate")
21
+ return {
22
+ "evmAddress": evm if isinstance(evm, str) else None,
23
+ "svmAddress": svm if isinstance(svm, str) else None,
24
+ "archerFeeRate": float(rate)
25
+ if isinstance(rate, (int, float)) and not isinstance(rate, bool)
26
+ else None,
27
+ }
28
+
29
+
30
+ def require_archer_caller(args: Dict[str, Any]) -> ArcherCaller:
31
+ """For intents that move user funds or cause side effects, caller context
32
+ is mandatory. A missing caller almost always means the intent was
33
+ published without requiresCaller: true."""
34
+ caller = get_archer_caller(args)
35
+ if caller is None:
36
+ raise ValueError(
37
+ "caller context is missing; declare requiresCaller: true when publishing this intent"
38
+ )
39
+ return caller