@usearete/automations 0.0.0-stage → 0.44.1
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/LICENSE +21 -0
- package/README.md +299 -2
- package/dist/cli.js +4518 -0
- package/dist/index.cjs +3735 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1379 -0
- package/dist/index.js +3690 -0
- package/dist/index.js.map +1 -0
- package/dist/keypair.cjs +267 -0
- package/dist/keypair.cjs.map +1 -0
- package/dist/keypair.d.ts +70 -0
- package/dist/keypair.js +265 -0
- package/dist/keypair.js.map +1 -0
- package/dist/postgres.cjs +757 -0
- package/dist/postgres.cjs.map +1 -0
- package/dist/postgres.d.ts +449 -0
- package/dist/postgres.js +749 -0
- package/dist/postgres.js.map +1 -0
- package/dist/privy.cjs +618 -0
- package/dist/privy.cjs.map +1 -0
- package/dist/privy.d.ts +236 -0
- package/dist/privy.js +614 -0
- package/dist/privy.js.map +1 -0
- package/dist/testing.cjs +467 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.ts +381 -0
- package/dist/testing.js +465 -0
- package/dist/testing.js.map +1 -0
- package/package.json +108 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arete
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,300 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @usearete/automations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server-side automation of Arete Protocol steps on a user's behalf: when a
|
|
4
|
+
step needs the user's signature, a backend prepares it, checks it against a
|
|
5
|
+
signing policy and the user's consent, signs it with a delegated signer, and
|
|
6
|
+
submits it. Arete never holds a key; the signer runs in your backend.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @usearete/automations @usearete/sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Start with [Automating steps for users](https://github.com/AreteA4/arete/blob/main/typescript/automations/docs/automating-steps.md), the
|
|
13
|
+
[consent UX guide](https://github.com/AreteA4/arete/blob/main/typescript/automations/docs/consent-ux.md) and the
|
|
14
|
+
[threat model](https://github.com/AreteA4/arete/blob/main/typescript/automations/docs/threat-model.md); [`examples/token-raise`](https://github.com/AreteA4/arete/tree/main/typescript/automations/examples/token-raise)
|
|
15
|
+
is a complete backend.
|
|
16
|
+
|
|
17
|
+
## Use the least powerful mechanism
|
|
18
|
+
|
|
19
|
+
Reach for delegated signing only when nothing simpler works:
|
|
20
|
+
|
|
21
|
+
| Situation | Mechanism |
|
|
22
|
+
|---|---|
|
|
23
|
+
| The program lets anyone advance it | A keeper step |
|
|
24
|
+
| The money is committed now | Deposit straight into the pot or escrow |
|
|
25
|
+
| A fixed recurring payment from a wallet the user keeps using | An on-chain allowance |
|
|
26
|
+
| The user's signature is needed each time, and the wallet is where their funds already live | Delegated signing under consent (this package) |
|
|
27
|
+
|
|
28
|
+
## The runtime
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createAutomations, definePolicy, memoryStore, rpcSimulator, scheduleSlot } from '@usearete/automations';
|
|
32
|
+
|
|
33
|
+
const automations = createAutomations({
|
|
34
|
+
client, // a secret-key protocol client
|
|
35
|
+
signer, // a ProtocolSigner
|
|
36
|
+
policy: definePolicy({ ... }),
|
|
37
|
+
simulator: rpcSimulator(RPC_URL),
|
|
38
|
+
store: memoryStore(), // tests and development only
|
|
39
|
+
}); // targets dev for a client `a4 dev` generated, mainnet for a published one
|
|
40
|
+
|
|
41
|
+
// A protocol event: read state, return the steps to run. Handlers never prepare.
|
|
42
|
+
automations.on('round.settled', async (event, ctx) => {
|
|
43
|
+
const player = await lookUpPlayer(event);
|
|
44
|
+
return [ctx.step('claimAndEnter', { wallet: player.wallet, instance: event.instanceId })];
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Webhooks: verified, accepted once, answered straight away.
|
|
48
|
+
export const POST = automations.webhookHandler({ secret: process.env.ARETE_WEBHOOK_SECRET! });
|
|
49
|
+
|
|
50
|
+
// Cron: your scheduler dispatches; the slot id makes a double fire a no-op.
|
|
51
|
+
await automations.dispatch('weeklyBuy', {}, { id: scheduleSlot('weekly-buy', 'week') });
|
|
52
|
+
|
|
53
|
+
automations.start(); // a long-running worker, or `await automations.drain()` per request
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each trigger becomes a **delivery job** that runs its handler. Each step
|
|
57
|
+
request the handler returns becomes a **step job** (`${deliveryId}:${step}:${wallet}`, plus
|
|
58
|
+
`:${instance}` for a step on an instance),
|
|
59
|
+
so rerunning a handler never doubles a step. A step job:
|
|
60
|
+
|
|
61
|
+
1. is skipped without an active consent grant (`no_consent`) or inside its
|
|
62
|
+
`cooldown`;
|
|
63
|
+
2. is rejected when `allowedSteps` or prepare says no (a normal outcome);
|
|
64
|
+
3. is prepared with idempotency key `${jobId}:${generation}` in the
|
|
65
|
+
negotiated format, checked against the intent, simulated and checked
|
|
66
|
+
against the policy (`refused(<rule>)`);
|
|
67
|
+
4. re-checks consent, is signed, and the signed bytes are checked and saved
|
|
68
|
+
before submit;
|
|
69
|
+
5. is tracked to `completed`, `chain_failed`, or `expired` (prepared once
|
|
70
|
+
more with a new key). An intent in `submitted_unknown` is read again
|
|
71
|
+
later and never resubmitted blind.
|
|
72
|
+
|
|
73
|
+
Only one step job per wallet runs at a time, from claim until it settles.
|
|
74
|
+
After a crash, a job that had signed reads its intent before doing anything
|
|
75
|
+
else. `onOutcome`, `onRefused` and `onRejected` report final states.
|
|
76
|
+
|
|
77
|
+
A store must pass `runStoreConformance` from `@usearete/automations/testing`.
|
|
78
|
+
|
|
79
|
+
## Definitions and `a4 dev`
|
|
80
|
+
|
|
81
|
+
Put the handlers and view sources in a definition, apart from the client,
|
|
82
|
+
signer, store and secrets:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// automations.ts
|
|
86
|
+
export default defineAutomations(TOKEN_RAISE_PROTOCOL, (automations) => {
|
|
87
|
+
automations.on('raise.failed', async (event, ctx) => { /* ... */ });
|
|
88
|
+
}, { policy: { programs: [RAISE_PROGRAM_ID], maxSolPerTransaction: '0.001' } });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`[dev.automations] entry = "automations.ts"` in the protocol project's
|
|
92
|
+
arete.toml makes `a4 dev up` run it (`arete-automations dev`) against the
|
|
93
|
+
project's dev environment: dev wallets sign, a saved memory store keeps jobs
|
|
94
|
+
and consent across restarts, and the environment's events arrive as signed
|
|
95
|
+
webhook deliveries. It restarts when the handlers or the generated client
|
|
96
|
+
change. In production, `await automations.use(definition)` registers the same
|
|
97
|
+
handlers on a runtime you configure, with
|
|
98
|
+
`definePolicy({ ...definition.policy, deployments, clusters })`.
|
|
99
|
+
|
|
100
|
+
## Consent
|
|
101
|
+
|
|
102
|
+
Nothing runs for a wallet without a grant the wallet signed:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const automations = createAutomations({ ...options, consent: { domain: 'app.example' } });
|
|
106
|
+
|
|
107
|
+
// 1. Your backend builds the message; it issues a single-use nonce.
|
|
108
|
+
const proof = await automations.consent.proofMessage({
|
|
109
|
+
wallet,
|
|
110
|
+
deploymentId: DCA_DEPLOYMENT,
|
|
111
|
+
steps: ['buy'],
|
|
112
|
+
limits: {
|
|
113
|
+
perExecution: { SOL: '10000', [USDC]: '50000000' }, // lamports / base units, per run
|
|
114
|
+
perWindow: { [USDC]: { amount: '200000000', window: '30d' } },
|
|
115
|
+
},
|
|
116
|
+
params: { amount: '50000000', weekday: 'fri' }, // the user's settings, signed too
|
|
117
|
+
expiresAt: new Date('2027-01-01'),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// 2. The user's wallet signs `proof.bytes` (signMessage).
|
|
121
|
+
// 3. Your backend rebuilds the message from the same fields and checks the signature.
|
|
122
|
+
const { grant, warnings } = await automations.consent.grant({ ...fields, nonce: proof.nonce, issuedAt: proof.issuedAt, signature });
|
|
123
|
+
|
|
124
|
+
await automations.consent.revoke({ wallet }); // no signature needed
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- The message is human-readable and bound to your domain. Changing any
|
|
128
|
+
field, raising a limit or changing a setting needs a new signature.
|
|
129
|
+
- An asset the step moves without a per-run limit is refused
|
|
130
|
+
(`consent_limit`); going past a window is refused (`consent_window`). The
|
|
131
|
+
window counts a step from the moment it is signed and stops counting it
|
|
132
|
+
if its intent never lands. Only one step per wallet runs at a time, so
|
|
133
|
+
concurrent workers cannot overspend a window.
|
|
134
|
+
- A grant scoped to an instance ends when the instance is terminal.
|
|
135
|
+
- `grant` warns when the signer reports no provider-side policy. The
|
|
136
|
+
signature records what the user agreed to; it does not stop a compromised
|
|
137
|
+
backend. The wallet provider's policy and the protocol's on-chain rules do.
|
|
138
|
+
|
|
139
|
+
Handlers fan out over the wallets that consented:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
automations.on('weeklyBuy', async (_trigger, ctx) => {
|
|
143
|
+
const grants = await ctx.consents.list({ step: 'buy' });
|
|
144
|
+
return grants
|
|
145
|
+
.filter((grant) => grant.params.weekday === today)
|
|
146
|
+
.map((grant) => ctx.step('buy', { wallet: grant.wallet, args: { amount: grant.params.amount } }));
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## View sources
|
|
151
|
+
|
|
152
|
+
A view source watches a live view and dispatches when a condition becomes
|
|
153
|
+
true, for example when the best lending rate moves to another market:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { fromView, fromViewRows } from '@usearete/automations';
|
|
157
|
+
|
|
158
|
+
const bestRate = fromView(client.views['jupiter-lend-rates'].markets.list, {
|
|
159
|
+
name: 'bestRateChanged',
|
|
160
|
+
key: (market) => market.id, // a row's identity in the view
|
|
161
|
+
derive: (markets) => pickBest(markets, { minEdgeBps: 25 }), // hysteresis lives in derive
|
|
162
|
+
changed: (previous, next) => previous !== undefined && previous !== next,
|
|
163
|
+
stableFor: '2m', // a new value must hold 2 minutes
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
automations.on('bestRateChanged', async (trigger, ctx) => { /* fan out over consenting wallets */ });
|
|
167
|
+
|
|
168
|
+
automations.source(bestRate); // long-running: subscribe
|
|
169
|
+
export async function GET() { await automations.poll(bestRate); } // serverless cron: one snapshot per call
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`fromViewRows(view, { name, key, derive, filter })` keeps one signal per
|
|
173
|
+
row, for example "this wallet's position crossed below a health of 1.15".
|
|
174
|
+
`fromAppendView(view, { name })` dispatches one trigger per appended row and
|
|
175
|
+
resumes from its stored cursor, which only ever moves forward.
|
|
176
|
+
|
|
177
|
+
Exactly once comes from the store: each key's last stable value is
|
|
178
|
+
advanced by compare-and-set and the delivery is accepted in the same step.
|
|
179
|
+
Replicas watching one view produce one delivery per transition; after a
|
|
180
|
+
restart, a change made while every replica was down fires once; A → B → A
|
|
181
|
+
→ B fires three times. A value that changed and changed back while nothing
|
|
182
|
+
was watching is not seen. A trigger means "go and look": handlers read live
|
|
183
|
+
state before returning steps.
|
|
184
|
+
|
|
185
|
+
## The Postgres store
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { postgresStore } from '@usearete/automations/postgres';
|
|
189
|
+
|
|
190
|
+
const store = postgresStore(pool); // your pg Pool; opens no connections of its own
|
|
191
|
+
await store.migrate(); // once, from a deploy step, with a role that may create tables
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- Everything lives in one schema, `arete_automation` by default
|
|
195
|
+
(`postgresStore(pool, { schema })`). Nothing outside it is created,
|
|
196
|
+
altered or read.
|
|
197
|
+
- Migrations never run at startup. Run `store.migrate()` (transactional,
|
|
198
|
+
behind an advisory lock, so concurrent callers run one at a time), or
|
|
199
|
+
export the SQL for your own tool:
|
|
200
|
+
`npx arete-automations migrations --out db/migrations`. A store whose
|
|
201
|
+
database is behind refuses to run (`SchemaVersionError`).
|
|
202
|
+
- Use two roles: one that migrates, and a runtime role limited to rows.
|
|
203
|
+
`npx arete-automations grants --role app_runtime` prints its grants.
|
|
204
|
+
- A `pg.Pool` is detected; for another pool adapter pass
|
|
205
|
+
`postgresStore(pool, { executor: 'pool' })`.
|
|
206
|
+
- Pass a client instead of a pool to write inside your own transaction, for
|
|
207
|
+
example a consent grant with the user record:
|
|
208
|
+
`store.using(client).consent.put(grant)`.
|
|
209
|
+
- Jobs are claimed with `FOR UPDATE SKIP LOCKED`; a partial unique index
|
|
210
|
+
keeps one active step per wallet. Tested on Postgres 15, 16 and 17.
|
|
211
|
+
|
|
212
|
+
## Privy
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { PrivyClient } from '@privy-io/node';
|
|
216
|
+
import { privyPolicyFor, privySigner } from '@usearete/automations/privy';
|
|
217
|
+
|
|
218
|
+
const signer = privySigner({
|
|
219
|
+
privy: new PrivyClient({ appId, appSecret }),
|
|
220
|
+
walletIdFor: async (address) => lookUpPrivyWalletId(address), // null when you have no server access
|
|
221
|
+
authorizationPrivateKey: process.env.PRIVY_AUTHORIZATION_KEY!,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// The provider-side bound for one user's consent; attach it to their wallet in Privy.
|
|
225
|
+
const { policy, unenforced } = privyPolicyFor(signingPolicy, grant);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The signer asks Privy to sign (not to send) each prepared transaction, as
|
|
229
|
+
v0 until v1 message signing is verified. A Privy policy refusal ends the job
|
|
230
|
+
`refused(provider_policy)`. Privy's policy sees top-level instructions only,
|
|
231
|
+
so `unenforced` lists what it cannot bound, notably amounts a protocol
|
|
232
|
+
program moves by CPI; see
|
|
233
|
+
[docs/privy-policy-expressiveness.md](https://github.com/AreteA4/arete/blob/main/typescript/automations/docs/privy-policy-expressiveness.md).
|
|
234
|
+
|
|
235
|
+
## The signing policy
|
|
236
|
+
|
|
237
|
+
Every prepared transaction is checked before it is signed. A refusal names
|
|
238
|
+
the rule:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { checkPrepared, checkTarget, definePolicy, rpcSimulator, summarizePrepared } from '@usearete/automations';
|
|
242
|
+
|
|
243
|
+
const policy = definePolicy({
|
|
244
|
+
deployments: [DEPLOYMENT_ID],
|
|
245
|
+
clusters: ['dev', 'mainnet'], // defaults to ['dev']; mainnet only when listed
|
|
246
|
+
programs: [VAULT_PROGRAM_ID], // optional; runtime programs are always allowed
|
|
247
|
+
maxSolPerTransaction: '0.01', // SOL, wrapped SOL and closed token accounts included
|
|
248
|
+
maxTokenAmounts: { [USDC]: '5000000000' }, // base units; a mint without a cap may not leave
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const target = checkTarget(policy, DEPLOYMENT_ID, 'mainnet'); // before preparing; null when allowed
|
|
252
|
+
if (target) throw new Error(`${target.rule}: ${target.reason}`);
|
|
253
|
+
|
|
254
|
+
const summary = await summarizePrepared(prepared, wallet, rpcSimulator(RPC_URL));
|
|
255
|
+
const refusal = checkPrepared(policy, { deploymentId: DEPLOYMENT_ID, prepared, summary, simulates: true });
|
|
256
|
+
if (refusal) throw new Error(`${refusal.rule}: ${refusal.reason}`);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- Outflows are measured by simulating the exact bytes, per transaction. A
|
|
260
|
+
transaction whose effect on the signer cannot be simulated is refused.
|
|
261
|
+
- Instructions that hand control of the signer's funds to someone else
|
|
262
|
+
(token approvals, authority changes, System assignments, nonce, stake and
|
|
263
|
+
loader authority changes) are always refused, top-level and inner.
|
|
264
|
+
- The rules match the Arete MCP server's local signer. Both run the shared
|
|
265
|
+
vectors in [`test-vectors/signer-policy/`](../../test-vectors/signer-policy/).
|
|
266
|
+
|
|
267
|
+
What a wallet provider can enforce on its side is narrower; see
|
|
268
|
+
[docs/privy-policy-expressiveness.md](https://github.com/AreteA4/arete/blob/main/typescript/automations/docs/privy-policy-expressiveness.md).
|
|
269
|
+
|
|
270
|
+
## Signers
|
|
271
|
+
|
|
272
|
+
A `ProtocolSigner` fills the wallet's signature slot of each prepared
|
|
273
|
+
transaction and nothing else. The package does not trust it:
|
|
274
|
+
|
|
275
|
+
- Before signing, `checkPreparedAgainstIntent` confirms the prepared bytes
|
|
276
|
+
are the ones the intent committed to (`messageSha256`), in the negotiated
|
|
277
|
+
format, with the wallet as a required signer.
|
|
278
|
+
- After signing, `checkSignedTransactions` re-hashes every message against
|
|
279
|
+
the intent, confirms no other signature slot changed, and verifies the
|
|
280
|
+
wallet's signature. A signer that adds a compute-budget instruction or
|
|
281
|
+
re-encodes a transaction is refused as `signer_modified`.
|
|
282
|
+
|
|
283
|
+
`negotiateTransactionFormat` picks the first format, in the deployment's
|
|
284
|
+
order (`v1`, then `v0`), that the signer supports.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { keypairSigner } from '@usearete/automations/keypair';
|
|
288
|
+
|
|
289
|
+
const signer = keypairSigner(JSON.parse(readFileSync(DEV_WALLET, 'utf8'))); // dev and tests only
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Development
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
npm install
|
|
296
|
+
npm test # unit tests and the shared signer-policy vectors
|
|
297
|
+
npm run typecheck
|
|
298
|
+
npm run lint
|
|
299
|
+
npm run build
|
|
300
|
+
```
|