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.
- archerprotocol_sdk-0.1.0/PKG-INFO +106 -0
- archerprotocol_sdk-0.1.0/README.md +83 -0
- archerprotocol_sdk-0.1.0/archer_sdk/__init__.py +55 -0
- archerprotocol_sdk-0.1.0/archer_sdk/builders.py +149 -0
- archerprotocol_sdk-0.1.0/archer_sdk/caller.py +39 -0
- archerprotocol_sdk-0.1.0/archer_sdk/effects.py +306 -0
- archerprotocol_sdk-0.1.0/archer_sdk/embed.py +164 -0
- archerprotocol_sdk-0.1.0/archer_sdk/errors.py +19 -0
- archerprotocol_sdk-0.1.0/archer_sdk/fastapi.py +58 -0
- archerprotocol_sdk-0.1.0/archer_sdk/handler.py +85 -0
- archerprotocol_sdk-0.1.0/archer_sdk/money.py +33 -0
- archerprotocol_sdk-0.1.0/archerprotocol_sdk.egg-info/PKG-INFO +106 -0
- archerprotocol_sdk-0.1.0/archerprotocol_sdk.egg-info/SOURCES.txt +22 -0
- archerprotocol_sdk-0.1.0/archerprotocol_sdk.egg-info/dependency_links.txt +1 -0
- archerprotocol_sdk-0.1.0/archerprotocol_sdk.egg-info/requires.txt +10 -0
- archerprotocol_sdk-0.1.0/archerprotocol_sdk.egg-info/top_level.txt +1 -0
- archerprotocol_sdk-0.1.0/pyproject.toml +30 -0
- archerprotocol_sdk-0.1.0/setup.cfg +4 -0
- archerprotocol_sdk-0.1.0/tests/test_builders.py +103 -0
- archerprotocol_sdk-0.1.0/tests/test_caller.py +25 -0
- archerprotocol_sdk-0.1.0/tests/test_effects.py +78 -0
- archerprotocol_sdk-0.1.0/tests/test_embed.py +70 -0
- archerprotocol_sdk-0.1.0/tests/test_handler.py +119 -0
- archerprotocol_sdk-0.1.0/tests/test_money.py +29 -0
|
@@ -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
|