solid-drift 0.16.0 → 0.17.0

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.
package/README.md CHANGED
@@ -1094,6 +1094,41 @@ const connect = createConnectButton(() => btn, { strength: 0.35 });
1094
1094
 
1095
1095
  Returns `{ copyTick, chainPulse, status }`. `status()` is `"idle"`, `"ticking"` (check visible), or `"pulsing"` (ring expanding). Call `chainPulse()` after a successful connection or network switch. Under reduced motion there is no magnetic pull or scale; `copyTick()` and `chainPulse()` still show their overlays statically.
1096
1096
 
1097
+ ### `createAgentTx(options?)`
1098
+
1099
+ AI proposes, the user approves, the transaction executes. The agent (an LLM) calls `propose()` with a plain-data proposal the user can read (`to`, `value`, `data`, `description`, `chainId`); the user calls `approve()` or `reject()`; `execute()` hands the approved proposal to your wallet adapter and the inner `createTxLifecycle` tracks signing to confirmation. The library never signs: `execute` is your wagmi/viem send function.
1100
+
1101
+ States flow `idle` to `proposed` to `approved` to `executing` to `confirmed`, with `rejected` and `failed` as the off-ramps. Invalid transitions are no-ops, so an LLM-driven UI cannot skip the user's approval. `progress()` is spring-smoothed across the whole flow for progress UI, and `tx` exposes the inner lifecycle for manual driving or extra rendering.
1102
+
1103
+ ```tsx
1104
+ const agentTx = createAgentTx({
1105
+ execute: async (p) => sendTransaction({ to: p.to, value: p.value }),
1106
+ source: () => receiptQuery(), // wagmi/viem-style status
1107
+ });
1108
+ // The AI proposes:
1109
+ agentTx.propose({
1110
+ to: "0x…",
1111
+ value: "1000000000000000000",
1112
+ description: "Swap 1 ETH for USDC at the current rate.",
1113
+ });
1114
+ // The user reviews agentTx.proposal() and taps approve:
1115
+ agentTx.approve();
1116
+ await agentTx.execute(); // "executing" to "confirmed"
1117
+ ```
1118
+
1119
+ DriftSpec gains two LLM-generatable web3 steps for full dApp choreography: `"agentTx"` (options `to`, `description`, `value`, `data`, `chainId`, `autoApprove`) proposes a transaction mid-spec and waits for the host, via the new `createSpecPlayer(spec, refs, hooks)` third parameter, to approve and execute it through `hooks.onAgentTxStep`; `"txReceipt"` (options `hash`, `endpoint`, `timeout`) waits for a transaction hash to mine. A typical generated ceremony reads: `streamReveal` (explain) to `agentTx` (approve and send) to `txReceipt` (confirm).
1120
+
1121
+ ```json
1122
+ {
1123
+ "version": 1,
1124
+ "scenes": [
1125
+ { "primitive": "streamReveal", "target": "explainer", "options": { "text": "The agent proposes swapping 1 ETH for USDC." } },
1126
+ { "primitive": "agentTx", "options": { "to": "0x…", "description": "Swap 1 ETH for USDC.", "value": "1000000000000000000" } },
1127
+ { "primitive": "txReceipt", "options": { "hash": "0x…" } }
1128
+ ]
1129
+ }
1130
+ ```
1131
+
1097
1132
  ### Web3 data layer
1098
1133
 
1099
1134
  A zero-dependency read layer for chain and market data as signals: public RPC and API endpoints over `fetch`, with user-swappable endpoints. Every network primitive shares the `{ data, error, status, retry, abort }` shape, is SSR-safe (nothing fetches on the server), and polls with error backoff. Defaults are conservative because public endpoints are rate-limited. This is read-only: transaction signing stays with wallet libraries.
package/dist/ai.d.ts CHANGED
@@ -15,6 +15,9 @@
15
15
  import { type Accessor } from "solid-js";
16
16
  import { type Easing, type EasingName } from "./easing.js";
17
17
  import { type KineticTypeFrom } from "./motion.js";
18
+ import { type AgentTxControls, type AgentTxProposal } from "./web3.js";
19
+ /** Re-exported for `SpecPlayerHooks` consumers. */
20
+ export type { AgentTxProposal, AgentTxControls };
18
21
  type MaybeElement = () => Element | null | undefined;
19
22
  export type StreamRevealStatus = "idle" | "streaming" | "done";
20
23
  export interface StreamRevealOptions {
@@ -125,7 +128,7 @@ export interface AgentStateControls {
125
128
  */
126
129
  export declare function createAgentState(options?: AgentStateOptions): AgentStateControls;
127
130
  /** Primitives a drift spec can choreograph. */
128
- export type DriftSpecPrimitive = "kineticType" | "camera" | "colorShift" | "transition" | "beat" | "streamReveal";
131
+ export type DriftSpecPrimitive = "kineticType" | "camera" | "colorShift" | "transition" | "beat" | "streamReveal" | "agentTx" | "txReceipt";
129
132
  /** One choreographed step. */
130
133
  export interface DriftSpecStep {
131
134
  /** Which primitive renders this step. */
@@ -139,7 +142,9 @@ export interface DriftSpecStep {
139
142
  /**
140
143
  * Primitive options. Validated against each primitive's minimal
141
144
  * shape. "camera" reads `keyframes`, "colorShift" reads `stops`,
142
- * "streamReveal" reads `text` here.
145
+ * "streamReveal" reads `text`, "agentTx" reads `to`, `description`,
146
+ * `value`, `data`, `chainId` and `autoApprove`, "txReceipt" reads
147
+ * `hash`, `endpoint` and `timeout`.
143
148
  */
144
149
  options?: Record<string, unknown>;
145
150
  /** Step budget in milliseconds, for timed players. */
@@ -185,10 +190,23 @@ export interface SpecPlayerControls {
185
190
  /** Current scene index, or -1 before the first play(). */
186
191
  scene: Accessor<number>;
187
192
  }
193
+ /** Host hooks for spec steps that need the outside world. */
194
+ export interface SpecPlayerHooks {
195
+ /**
196
+ * Called when an "agentTx" step proposes its transaction. Show your
197
+ * approval UI here and call `tx.approve()` / `tx.reject()` (then
198
+ * `tx.execute()`) on the controls. The step waits for a terminal
199
+ * state ("confirmed", "rejected", "failed"); `stop()` skips it.
200
+ */
201
+ onAgentTxStep?: (proposal: AgentTxProposal, tx: AgentTxControls) => void;
202
+ }
188
203
  /**
189
204
  * Render a validated DriftSpec: each scene's primitive plays in
190
205
  * order against the element refs the host supplies. `duration` on a
191
- * step caps that step's budget.
206
+ * step caps that step's budget. Web3 steps ("agentTx", "txReceipt")
207
+ * choreograph on-chain actions: "agentTx" proposes a transaction and
208
+ * waits for the host (via `hooks.onAgentTxStep`) to approve and
209
+ * execute it; "txReceipt" waits for a transaction hash to mine.
192
210
  *
193
211
  * SSR-safe: `play()` is a no-op on the server. Under reduced motion
194
212
  * `play()` jumps straight to the last scene (the clean final frame),
@@ -203,5 +221,4 @@ export interface SpecPlayerControls {
203
221
  * await player.play()
204
222
  * ```
205
223
  */
206
- export declare function createSpecPlayer(spec: DriftSpec, refs: Record<string, MaybeElement>): SpecPlayerControls;
207
- export {};
224
+ export declare function createSpecPlayer(spec: DriftSpec, refs: Record<string, MaybeElement>, hooks?: SpecPlayerHooks): SpecPlayerControls;
package/dist/ai.js CHANGED
@@ -19,6 +19,9 @@ import { now, schedule } from "./engine.js";
19
19
  import { createBeat, createCamera, createColorShift, createKineticType, createTransition, } from "./motion.js";
20
20
  import { prefersReducedMotion } from "./reduced-motion.js";
21
21
  import { appendUnits } from "./text.js";
22
+ import { createAgentTx, } from "./web3.js";
23
+ import { createTxReceipt } from "./web3data.js";
24
+ import { isAddress } from "./web3data.js";
22
25
  function clamp01(v) {
23
26
  return v < 0 ? 0 : v > 1 ? 1 : v;
24
27
  }
@@ -258,6 +261,8 @@ const PRIMITIVES = [
258
261
  "transition",
259
262
  "beat",
260
263
  "streamReveal",
264
+ "agentTx",
265
+ "txReceipt",
261
266
  ];
262
267
  /** Primitives that render into an element and need a target key. */
263
268
  const DOM_PRIMITIVES = [
@@ -316,6 +321,41 @@ function checkOptions(primitive, options, base) {
316
321
  throw new DriftSpecError(at("text"), "expected a string");
317
322
  }
318
323
  break;
324
+ case "agentTx":
325
+ if (typeof options.to !== "string" || !isAddress(options.to)) {
326
+ throw new DriftSpecError(at("to"), "expected a valid 0x address");
327
+ }
328
+ if (typeof options.description !== "string" ||
329
+ options.description.length === 0) {
330
+ throw new DriftSpecError(at("description"), "expected a non-empty string");
331
+ }
332
+ if (options.value !== undefined && typeof options.value !== "string") {
333
+ throw new DriftSpecError(at("value"), "expected a string");
334
+ }
335
+ if (options.data !== undefined && typeof options.data !== "string") {
336
+ throw new DriftSpecError(at("data"), "expected a string");
337
+ }
338
+ if (options.chainId !== undefined) {
339
+ assertPositiveNumber(options.chainId, at("chainId"));
340
+ }
341
+ if (options.autoApprove !== undefined &&
342
+ typeof options.autoApprove !== "boolean") {
343
+ throw new DriftSpecError(at("autoApprove"), "expected a boolean");
344
+ }
345
+ break;
346
+ case "txReceipt":
347
+ if (typeof options.hash !== "string" ||
348
+ !/^0x[0-9a-fA-F]{64}$/.test(options.hash)) {
349
+ throw new DriftSpecError(at("hash"), "expected a 0x transaction hash");
350
+ }
351
+ if (options.endpoint !== undefined &&
352
+ typeof options.endpoint !== "string") {
353
+ throw new DriftSpecError(at("endpoint"), "expected a string");
354
+ }
355
+ if (options.timeout !== undefined) {
356
+ assertPositiveNumber(options.timeout, at("timeout"));
357
+ }
358
+ break;
319
359
  }
320
360
  }
321
361
  /**
@@ -478,11 +518,74 @@ const stepPlayers = {
478
518
  };
479
519
  return { promise: timer.finished.then(() => c.stop()), stop: stopAll };
480
520
  },
521
+ agentTx: (_target, options, _budget, hooks) => {
522
+ const tx = createAgentTx();
523
+ const proposal = {
524
+ to: options.to,
525
+ description: options.description,
526
+ };
527
+ if (typeof options.value === "string")
528
+ proposal.value = options.value;
529
+ if (typeof options.data === "string")
530
+ proposal.data = options.data;
531
+ if (typeof options.chainId === "number") {
532
+ proposal.chainId = options.chainId;
533
+ }
534
+ tx.propose(proposal);
535
+ hooks?.onAgentTxStep?.(proposal, tx);
536
+ if (options.autoApprove === true)
537
+ tx.approve();
538
+ // The host approves through the hook; the step ends at a terminal
539
+ // state. Without a host the step simply waits until stop() skips it.
540
+ return whenDone(() => {
541
+ const s = tx.state();
542
+ return s === "confirmed" || s === "rejected" || s === "failed";
543
+ });
544
+ },
545
+ txReceipt: (_target, options) => {
546
+ const watcher = createTxReceipt(options.hash, {
547
+ endpoint: typeof options.endpoint === "string" ? options.endpoint : undefined,
548
+ interval: 4000,
549
+ });
550
+ const timeout = typeof options.timeout === "number" ? options.timeout : 120000;
551
+ let cancel = null;
552
+ let resolveFn;
553
+ const started = Date.now();
554
+ const promise = new Promise((resolve) => {
555
+ resolveFn = resolve;
556
+ if (watcher.mined()) {
557
+ resolve();
558
+ return;
559
+ }
560
+ cancel = schedule(() => {
561
+ if (watcher.mined() || Date.now() - started >= timeout) {
562
+ resolve();
563
+ return false;
564
+ }
565
+ return true;
566
+ });
567
+ });
568
+ const done = () => {
569
+ cancel?.();
570
+ cancel = null;
571
+ watcher.abort();
572
+ };
573
+ return {
574
+ promise: promise.then(done),
575
+ stop: () => {
576
+ done();
577
+ resolveFn();
578
+ },
579
+ };
580
+ },
481
581
  };
482
582
  /**
483
583
  * Render a validated DriftSpec: each scene's primitive plays in
484
584
  * order against the element refs the host supplies. `duration` on a
485
- * step caps that step's budget.
585
+ * step caps that step's budget. Web3 steps ("agentTx", "txReceipt")
586
+ * choreograph on-chain actions: "agentTx" proposes a transaction and
587
+ * waits for the host (via `hooks.onAgentTxStep`) to approve and
588
+ * execute it; "txReceipt" waits for a transaction hash to mine.
486
589
  *
487
590
  * SSR-safe: `play()` is a no-op on the server. Under reduced motion
488
591
  * `play()` jumps straight to the last scene (the clean final frame),
@@ -497,7 +600,7 @@ const stepPlayers = {
497
600
  * await player.play()
498
601
  * ```
499
602
  */
500
- export function createSpecPlayer(spec, refs) {
603
+ export function createSpecPlayer(spec, refs, hooks) {
501
604
  const [status, setStatus] = createSignal("idle");
502
605
  const [scene, setScene] = createSignal(-1);
503
606
  let currentStop = null;
@@ -505,7 +608,7 @@ export function createSpecPlayer(spec, refs) {
505
608
  let stopped = false;
506
609
  const runStep = async (step) => {
507
610
  const target = step.target === undefined ? undefined : refs[step.target];
508
- const handle = stepPlayers[step.primitive](target, step.options ?? {}, step.duration);
611
+ const handle = stepPlayers[step.primitive](target, step.options ?? {}, step.duration, hooks);
509
612
  currentStop = handle.stop;
510
613
  if (step.duration === undefined) {
511
614
  await handle.promise;
package/dist/index.d.ts CHANGED
@@ -31,8 +31,8 @@ export { createKineticType, createScenePlayer, createShowreel, createCamera, cre
31
31
  export type { KineticTypeFrom, KineticTypeOptions, KineticTypeStatus, KineticTypeControls, MotionScene, ScenePlayerStatus, ScenePlayerControls, ShowreelScene, ShowreelSceneKind, CameraKeyframe, CameraOptions, ColorShiftOptions, ColorShiftStatus, ColorShiftControls, TransitionType, TransitionDirection, TransitionOptions, TransitionLayerStyle, TransitionStatus, TransitionControls, BeatOptions, BeatStatus, BeatControls, BeatCutOptions, } from "./motion.js";
32
32
  export { createDrag, type DragStatus, type DragAxis, type DragConstraints, type DragEndInfo, type DragOptions, type DragControls, } from "./gesture.js";
33
33
  export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
34
- export type { StreamRevealStatus, StreamRevealOptions, StreamRevealControls, AgentState, AgentStateTransition, AgentStateOptions, AgentStateControls, DriftSpecPrimitive, DriftSpecStep, DriftSpec, SpecPlayerStatus, SpecPlayerControls, } from "./ai.js";
35
- export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, } from "./web3.js";
36
- export type { TxState, TxStatusInput, TxLifecycleOptions, TxLifecycleControls, TickerOptions, TickerControls, MintRevealStatus, MintRevealOptions, MintRevealControls, ConnectButtonOptions, ConnectButtonStatus, ConnectButtonControls, } from "./web3.js";
34
+ export type { StreamRevealStatus, StreamRevealOptions, StreamRevealControls, AgentState, AgentStateTransition, AgentStateOptions, AgentStateControls, DriftSpecPrimitive, DriftSpecStep, DriftSpec, SpecPlayerStatus, SpecPlayerHooks, SpecPlayerControls, } from "./ai.js";
35
+ export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, createAgentTx, } from "./web3.js";
36
+ export type { TxState, TxStatusInput, TxLifecycleOptions, TxLifecycleControls, TickerOptions, TickerControls, MintRevealStatus, MintRevealOptions, MintRevealControls, ConnectButtonOptions, ConnectButtonStatus, ConnectButtonControls, AgentTxState, AgentTxProposal, AgentTxOptions, AgentTxControls, } from "./web3.js";
37
37
  export { createPoll, shortenAddress, isAddress, formatUnits, parseUnits, CHAINS, createChain, createTokenPrice, createPriceChange, createPriceCompare, createGasPrice, createBalance, createTxReceipt, createBlockNumber, createChainlinkPrice, createNFTMetadata, createENS, createIdenticon, } from "./web3data.js";
38
38
  export type { PollStatus, PollOptions, PollControls, ChainInfo, TokenPrice, TokenPriceOptions, PriceChangeOptions, GasPriceOptions, GasPriceData, BalanceOptions, BalanceData, TxReceiptData, TxReceiptOptions, BlockNumberOptions, ChainlinkPriceOptions, NFTMetadata, NFTMetadataOptions, ENSOptions, IdenticonOptions, } from "./web3data.js";
package/dist/index.js CHANGED
@@ -30,5 +30,5 @@ export { easings, cubicBezier, linear, easeInQuad, easeOutQuad, easeInOutQuad, e
30
30
  export { createKineticType, createScenePlayer, createShowreel, createCamera, createColorShift, createTransition, createBeat, createBeatCuts, } from "./motion.js";
31
31
  export { createDrag, } from "./gesture.js";
32
32
  export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
33
- export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, } from "./web3.js";
33
+ export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, createAgentTx, } from "./web3.js";
34
34
  export { createPoll, shortenAddress, isAddress, formatUnits, parseUnits, CHAINS, createChain, createTokenPrice, createPriceChange, createPriceCompare, createGasPrice, createBalance, createTxReceipt, createBlockNumber, createChainlinkPrice, createNFTMetadata, createENS, createIdenticon, } from "./web3data.js";
package/dist/web3.d.ts CHANGED
@@ -82,6 +82,104 @@ export interface TxLifecycleControls {
82
82
  * changes apply instantly and `progress()` jumps to its target.
83
83
  */
84
84
  export declare function createTxLifecycle(options?: TxLifecycleOptions): TxLifecycleControls;
85
+ /**
86
+ * Stages of an AI-proposed transaction: the agent proposes, the user
87
+ * approves or rejects, the transaction executes.
88
+ */
89
+ export type AgentTxState = "idle" | "proposed" | "approved" | "executing" | "confirmed" | "rejected" | "failed";
90
+ /** An AI-proposed on-chain action, in plain data the user can review. */
91
+ export interface AgentTxProposal {
92
+ /** Destination address. */
93
+ to: string;
94
+ /** Wei value as a decimal string. Default "0". */
95
+ value?: string;
96
+ /** Hex calldata. */
97
+ data?: string;
98
+ /** The AI's plain-language explanation of what this does. */
99
+ description: string;
100
+ /** Chain id. Default 1. */
101
+ chainId?: number;
102
+ }
103
+ export interface AgentTxOptions {
104
+ /**
105
+ * Executes the approved proposal against the wallet/chain adapter
106
+ * and resolves with the transaction hash. Throw to fail. Omit to
107
+ * drive the inner lifecycle manually through `tx`.
108
+ */
109
+ execute?: (proposal: AgentTxProposal, signal: AbortSignal) => Promise<string>;
110
+ /** wagmi/viem-style status accessor, fed to the inner lifecycle. */
111
+ source?: Accessor<TxStatusInput>;
112
+ /** Confirmations that promote "pending" to "confirming". Default 1. */
113
+ requiredConfirmations?: number;
114
+ /** Spring stiffness for the progress value. Default 170. */
115
+ stiffness?: number;
116
+ /** Spring damping for the progress value. Default 26. */
117
+ damping?: number;
118
+ /** Called after entering a state, with the previous state. */
119
+ onEnter?: (state: AgentTxState, prev: AgentTxState) => void;
120
+ }
121
+ export interface AgentTxControls {
122
+ /** Current agent-tx state. */
123
+ state: Accessor<AgentTxState>;
124
+ /** The proposal under review, if any. */
125
+ proposal: Accessor<AgentTxProposal | undefined>;
126
+ /**
127
+ * Inner transaction lifecycle, live during "executing". Feed it
128
+ * through `source` or drive it manually with `tx.set()`.
129
+ */
130
+ tx: TxLifecycleControls;
131
+ /** Propose a transaction for the user to review. */
132
+ propose: (proposal: AgentTxProposal) => void;
133
+ /** Approve the proposal; only from "proposed". */
134
+ approve: () => void;
135
+ /** Reject the proposal; only from "proposed". */
136
+ reject: () => void;
137
+ /**
138
+ * Run the approved proposal: calls `execute`, then tracks the inner
139
+ * lifecycle to "confirmed" or "failed". Only from "approved".
140
+ */
141
+ execute: () => Promise<void>;
142
+ /** Back to "idle", aborting any in-flight execution. */
143
+ reset: () => void;
144
+ /**
145
+ * 0 to 1 across the whole flow, spring-smoothed for progress UI:
146
+ * idle 0, proposed 0.2, approved 0.35, executing 0.65,
147
+ * confirmed/failed 1, rejected back to 0.
148
+ */
149
+ progress: Accessor<number>;
150
+ }
151
+ /**
152
+ * AI proposes, the user approves, the transaction executes.
153
+ *
154
+ * The agent (an LLM) calls `propose()` with a plain-data proposal the
155
+ * user can read; the user calls `approve()` or `reject()`; `execute()`
156
+ * hands the approved proposal to the wallet adapter and the inner
157
+ * `createTxLifecycle` tracks signing to confirmation. The library
158
+ * never signs: `execute` is your wagmi/viem send function.
159
+ *
160
+ * ```ts
161
+ * const agentTx = createAgentTx({
162
+ * execute: async (p) => sendTransaction({
163
+ * to: p.to, value: p.value, data: p.data,
164
+ * }),
165
+ * source: () => receiptQuery(),
166
+ * })
167
+ * // The AI proposes:
168
+ * agentTx.propose({
169
+ * to: "0x…",
170
+ * value: "1000000000000000000",
171
+ * description: "Swap 1 ETH for USDC at the current rate.",
172
+ * })
173
+ * // The user reviews proposal() and taps approve:
174
+ * agentTx.approve()
175
+ * await agentTx.execute() // "executing" to "confirmed"
176
+ * ```
177
+ *
178
+ * Invalid transitions are no-ops, so LLM-driven UIs cannot skip the
179
+ * user's approval. SSR-safe. Under reduced motion `progress()` jumps
180
+ * to its target.
181
+ */
182
+ export declare function createAgentTx(options?: AgentTxOptions): AgentTxControls;
85
183
  export interface TickerOptions {
86
184
  /** Decimals in the formatted output. Default 2. */
87
185
  decimals?: number;
package/dist/web3.js CHANGED
@@ -20,6 +20,7 @@ import { createMagnetic } from "./pointer.js";
20
20
  import { prefersReducedMotion } from "./reduced-motion.js";
21
21
  import { createSpring } from "./spring.js";
22
22
  import { ownerDoc } from "./text.js";
23
+ import { isAddress } from "./web3data.js";
23
24
  function clamp01(v) {
24
25
  return v < 0 ? 0 : v > 1 ? 1 : v;
25
26
  }
@@ -103,6 +104,143 @@ export function createTxLifecycle(options = {}) {
103
104
  progress,
104
105
  };
105
106
  }
107
+ const AGENT_TX_PROGRESS = {
108
+ idle: 0,
109
+ proposed: 0.2,
110
+ approved: 0.35,
111
+ executing: 0.65,
112
+ confirmed: 1,
113
+ rejected: 0,
114
+ failed: 1,
115
+ };
116
+ /**
117
+ * AI proposes, the user approves, the transaction executes.
118
+ *
119
+ * The agent (an LLM) calls `propose()` with a plain-data proposal the
120
+ * user can read; the user calls `approve()` or `reject()`; `execute()`
121
+ * hands the approved proposal to the wallet adapter and the inner
122
+ * `createTxLifecycle` tracks signing to confirmation. The library
123
+ * never signs: `execute` is your wagmi/viem send function.
124
+ *
125
+ * ```ts
126
+ * const agentTx = createAgentTx({
127
+ * execute: async (p) => sendTransaction({
128
+ * to: p.to, value: p.value, data: p.data,
129
+ * }),
130
+ * source: () => receiptQuery(),
131
+ * })
132
+ * // The AI proposes:
133
+ * agentTx.propose({
134
+ * to: "0x…",
135
+ * value: "1000000000000000000",
136
+ * description: "Swap 1 ETH for USDC at the current rate.",
137
+ * })
138
+ * // The user reviews proposal() and taps approve:
139
+ * agentTx.approve()
140
+ * await agentTx.execute() // "executing" to "confirmed"
141
+ * ```
142
+ *
143
+ * Invalid transitions are no-ops, so LLM-driven UIs cannot skip the
144
+ * user's approval. SSR-safe. Under reduced motion `progress()` jumps
145
+ * to its target.
146
+ */
147
+ export function createAgentTx(options = {}) {
148
+ const { execute, source, requiredConfirmations = 1, stiffness = 170, damping = 26, onEnter, } = options;
149
+ const [state, setState] = createSignal("idle");
150
+ const [proposal, setProposal] = createSignal(undefined);
151
+ const [target, setTarget] = createSignal(0);
152
+ const progress = createSpring(target, { stiffness, damping });
153
+ const tx = createTxLifecycle({
154
+ source,
155
+ requiredConfirmations,
156
+ stiffness,
157
+ damping,
158
+ });
159
+ let execAborter = null;
160
+ const apply = (next) => {
161
+ const current = state();
162
+ if (next === current)
163
+ return;
164
+ setState(next);
165
+ setTarget(AGENT_TX_PROGRESS[next]);
166
+ onEnter?.(next, current);
167
+ };
168
+ // Mirror the inner lifecycle into the agent flow.
169
+ createEffect(() => {
170
+ const inner = tx.state();
171
+ if (state() !== "executing")
172
+ return;
173
+ if (inner === "success")
174
+ apply("confirmed");
175
+ else if (inner === "failed")
176
+ apply("failed");
177
+ });
178
+ const propose = (p) => {
179
+ const s = state();
180
+ if (s !== "idle" && s !== "rejected" && s !== "failed" && s !== "confirmed") {
181
+ return;
182
+ }
183
+ if (!isAddress(p.to)) {
184
+ throw new Error("createAgentTx: proposal 'to' is not a valid address.");
185
+ }
186
+ if (!p.description) {
187
+ throw new Error("createAgentTx: proposal needs a description.");
188
+ }
189
+ setProposal(p);
190
+ apply("proposed");
191
+ };
192
+ const approve = () => {
193
+ if (state() === "proposed")
194
+ apply("approved");
195
+ };
196
+ const reject = () => {
197
+ if (state() === "proposed")
198
+ apply("rejected");
199
+ };
200
+ const runExecute = async () => {
201
+ if (state() !== "approved")
202
+ return;
203
+ const p = proposal();
204
+ if (!p)
205
+ return;
206
+ apply("executing");
207
+ tx.set("signing");
208
+ // Without an execute function the host drives tx manually.
209
+ if (!execute)
210
+ return;
211
+ execAborter?.abort();
212
+ execAborter = new AbortController();
213
+ const signal = execAborter.signal;
214
+ try {
215
+ await execute(p, signal);
216
+ if (!signal.aborted)
217
+ tx.set("pending");
218
+ }
219
+ catch {
220
+ if (!signal.aborted)
221
+ tx.set("failed");
222
+ }
223
+ };
224
+ const reset = () => {
225
+ execAborter?.abort();
226
+ execAborter = null;
227
+ setProposal(undefined);
228
+ tx.reset();
229
+ apply("idle");
230
+ };
231
+ onCleanup(() => execAborter?.abort());
232
+ return {
233
+ state,
234
+ proposal,
235
+ tx,
236
+ propose,
237
+ approve,
238
+ reject,
239
+ execute: runExecute,
240
+ reset,
241
+ progress,
242
+ };
243
+ }
106
244
  /**
107
245
  * Animated price/balance ticker: per-digit roll, direction flash.
108
246
  *
package/package.json CHANGED
@@ -43,5 +43,5 @@
43
43
  },
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "0.16.0"
46
+ "version": "0.17.0"
47
47
  }