@intent-engine/cli 0.1.11 → 0.1.14
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/dist/src/agent/create.d.ts +1 -0
- package/dist/src/agent/create.js +18 -5
- package/dist/src/agent/create.js.map +1 -1
- package/dist/src/agent/mcp.js +5 -2
- package/dist/src/agent/mcp.js.map +1 -1
- package/dist/src/agent/owner.d.ts +2 -0
- package/dist/src/agent/owner.js +8 -0
- package/dist/src/agent/owner.js.map +1 -1
- package/dist/src/agent/wallet.d.ts +12 -1
- package/dist/src/agent/wallet.js +341 -43
- package/dist/src/agent/wallet.js.map +1 -1
- package/dist/src/commands.js +5 -2
- package/dist/src/commands.js.map +1 -1
- package/dist/src/transport.d.ts +9 -1
- package/dist/src/transport.js +49 -16
- package/dist/src/transport.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +3 -3
package/dist/src/agent/wallet.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { decodeEvent, findIntentPda, formatAmount, getIntentDecoder, IntentStatus, parseAmount } from "@intent-engine/core";
|
|
2
|
-
import { chainTime, CLOCK_SYSVAR, mintOf } from "../accounts.js";
|
|
1
|
+
import { attestedFillMessage, buildFillAttested, decodeEvent, decodeParams, feeBpsFor, findIntentPda, formatAmount, getIntentDecoder, IntentStatus, Kind, parseAmount, signEd25519Instruction, splitFees, WARN_ATTESTOR, } from "@intent-engine/core";
|
|
2
|
+
import { chainTime, CLOCK_SYSVAR, loadConfig, mintOf } from "../accounts.js";
|
|
3
3
|
import { ENV_KEYPAIR, WALLET_NOT_MAINNET } from "../args.js";
|
|
4
4
|
import { execute } from "../flows.js";
|
|
5
|
-
import { TOKEN_NAMES, tokenNamed } from "../tokens.js";
|
|
6
|
-
import { unmemoized } from "../transport.js";
|
|
5
|
+
import { NATIVE_MINT, TOKEN_NAMES, tokenNamed } from "../tokens.js";
|
|
6
|
+
import { neverLanded, unmemoized } from "../transport.js";
|
|
7
7
|
import { balances, defaultMints, FAUCET, PUBLIC_FAUCET, setup, withSettings } from "../wallet.js";
|
|
8
8
|
import { define } from "./actions.js";
|
|
9
|
-
import { MAX_EXPIRES_IN, MIN_EXPIRES_IN, prepareCreate } from "./create.js";
|
|
9
|
+
import { MAX_EXPIRES_IN, MIN_EXPIRES_IN, prepareCreate, WRAPPED } from "./create.js";
|
|
10
10
|
import { tool } from "./names.js";
|
|
11
|
-
import { AgentRefusal } from "./problems.js";
|
|
11
|
+
import { AgentRefusal, problemOf } from "./problems.js";
|
|
12
12
|
import { listIntents } from "./read.js";
|
|
13
13
|
import { InvalidInput, account, amount, choice, flag, integer, list, object, optional, text, time } from "./schema.js";
|
|
14
|
-
import { orderSchema } from "./shapes.js";
|
|
14
|
+
import { orderSchema, preparedView } from "./shapes.js";
|
|
15
|
+
import { tokenAccountIfMissing, walletRecipient } from "./owner.js";
|
|
16
|
+
import { prepare } from "./prepare.js";
|
|
15
17
|
import { prepareCancel } from "./settle.js";
|
|
16
18
|
import { STATUS } from "../accounts.js";
|
|
17
19
|
export const walletGuard = (limits) => ({ limits, sales: [] });
|
|
@@ -19,7 +21,7 @@ export const SALE_WINDOW = 3600n;
|
|
|
19
21
|
export const ESTIMATE_SCAN = 12;
|
|
20
22
|
export const ESTIMATE_FILLS = 5;
|
|
21
23
|
export const WAIT_DEFAULT = 30;
|
|
22
|
-
export const WAIT_MAX =
|
|
24
|
+
export const WAIT_MAX = 40;
|
|
23
25
|
export const WAIT_SCAN = 10;
|
|
24
26
|
export const DEFAULT_EXPIRES_IN = 3_600;
|
|
25
27
|
export const LANDING_SECONDS = 10;
|
|
@@ -28,7 +30,7 @@ export const WALLET_NOTICE = [
|
|
|
28
30
|
"Test network only: this wallet works on Solana devnet, testnet or localnet and refuses mainnet. Its tokens have no value.",
|
|
29
31
|
"Your key stays on this machine: setup makes one unless a keypair is already configured, and names its file. Only signed transactions leave it.",
|
|
30
32
|
"This server talks to no one but the RPC endpoint it was started with.",
|
|
31
|
-
"Nothing moves until you approve: swaps, auctions and cancels only preview until you say yes.",
|
|
33
|
+
"Nothing moves until you approve: swaps, auctions, escrows, releases and cancels only preview until you say yes.",
|
|
32
34
|
"Do not send real funds to this wallet or use its key anywhere else.",
|
|
33
35
|
];
|
|
34
36
|
export const walletEmpty = (address) => `the wallet ${address} holds no SOL yet; request devnet SOL for it at ${FAUCET} or ${PUBLIC_FAUCET}, then try again`;
|
|
@@ -113,7 +115,14 @@ export function limitLines(limits) {
|
|
|
113
115
|
"It sells no other token. Plan trades within these limits and tell the user when a request exceeds them.",
|
|
114
116
|
];
|
|
115
117
|
}
|
|
116
|
-
|
|
118
|
+
function capOf(text, decimals, mint) {
|
|
119
|
+
try {
|
|
120
|
+
return parseAmount(text, decimals);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new AgentRefusal("WalletTokenNotAllowed", `this server's limit of ${text} ${nameOf(mint)} does not fit a token with ${decimals} decimals, so it sells none; the user restarts it with a valid limit`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
117
126
|
function checkSale(guard, mint, decimals, amount, now) {
|
|
118
127
|
if (!limited(guard)) {
|
|
119
128
|
return;
|
|
@@ -125,16 +134,20 @@ function checkSale(guard, mint, decimals, amount, now) {
|
|
|
125
134
|
throw new AgentRefusal("WalletTokenNotAllowed", `this server sells only ${allowed}, not ${name}; its limits were set when it started`);
|
|
126
135
|
}
|
|
127
136
|
const trade = perTrade.get(mint);
|
|
128
|
-
if (trade !== undefined && amount > capOf(trade, decimals)) {
|
|
137
|
+
if (trade !== undefined && amount > capOf(trade, decimals, mint)) {
|
|
129
138
|
throw new AgentRefusal("WalletTradeLimit", `this server sells at most ${trade} ${name} per trade and this sells ${formatAmount(amount, decimals)}; sell less, or the user restarts the server with a higher --wallet-per-trade`);
|
|
130
139
|
}
|
|
131
|
-
|
|
140
|
+
checkHour(guard, mint, decimals, amount, now);
|
|
141
|
+
}
|
|
142
|
+
function checkHour(guard, mint, decimals, amount, now) {
|
|
143
|
+
const hour = guard.limits.perHour.get(mint);
|
|
144
|
+
const name = nameOf(mint);
|
|
132
145
|
if (hour === undefined) {
|
|
133
146
|
return;
|
|
134
147
|
}
|
|
135
148
|
guard.sales = guard.sales.filter((s) => s.at > now - SALE_WINDOW);
|
|
136
149
|
const used = guard.sales.filter((s) => s.mint === mint).reduce((sum, s) => sum + s.amount, 0n);
|
|
137
|
-
const cap = capOf(hour, decimals);
|
|
150
|
+
const cap = capOf(hour, decimals, mint);
|
|
138
151
|
if (used + amount > cap) {
|
|
139
152
|
throw new AgentRefusal("WalletHourLimit", `this server sells at most ${hour} ${name} per hour and has ${formatAmount(cap > used ? cap - used : 0n, decimals)} left; this sells ${formatAmount(amount, decimals)}`);
|
|
140
153
|
}
|
|
@@ -192,7 +205,7 @@ export const walletBalance = define({
|
|
|
192
205
|
return balances(ctx.transport, owner, mints);
|
|
193
206
|
},
|
|
194
207
|
});
|
|
195
|
-
const
|
|
208
|
+
const offerFields = {
|
|
196
209
|
intent: account(),
|
|
197
210
|
nonce: amount(),
|
|
198
211
|
summary: list(text(), 24),
|
|
@@ -200,7 +213,8 @@ const offerOutput = object({
|
|
|
200
213
|
sent: flag(),
|
|
201
214
|
signature: optional(text()),
|
|
202
215
|
next: optional(text()),
|
|
203
|
-
}
|
|
216
|
+
};
|
|
217
|
+
const offerOutput = object(offerFields);
|
|
204
218
|
async function offered(ctx, call, input, terms) {
|
|
205
219
|
const wallet = access(ctx);
|
|
206
220
|
const signer = await signerOf(ctx);
|
|
@@ -215,9 +229,9 @@ async function offered(ctx, call, input, terms) {
|
|
|
215
229
|
mintOf(ctx.transport, outputMint),
|
|
216
230
|
chainTime(ctx.transport),
|
|
217
231
|
]);
|
|
218
|
-
const made = terms(inputToken, outputToken, now);
|
|
232
|
+
const made = await terms(inputToken, outputToken, now, inputMint, outputMint);
|
|
219
233
|
checkSale(wallet.limits, inputMint, inputToken.decimals, made.order.amount_in, now);
|
|
220
|
-
const view = (await prepareCreate.run(ctx, { owner: signer.address, input_mint: inputMint, output_mint: outputMint, ...made }, call));
|
|
234
|
+
const view = (await prepareCreate.run(ctx, { owner: signer.address, input_mint: inputMint, output_mint: outputMint, ...(input.pay_to === undefined ? {} : { pay_to: input.pay_to }), ...made }, call));
|
|
221
235
|
const { intent, nonce, summary, warnings, prepared } = view;
|
|
222
236
|
const sold = made.order.amount_in;
|
|
223
237
|
const guard = wallet.limits;
|
|
@@ -231,14 +245,41 @@ async function offered(ctx, call, input, terms) {
|
|
|
231
245
|
return await sent(ctx, signer, { intent, nonce, summary, warnings, prepared }, input.send, call);
|
|
232
246
|
}
|
|
233
247
|
catch (error) {
|
|
234
|
-
|
|
248
|
+
if (neverLanded(error)) {
|
|
249
|
+
guard.sales = guard.sales.filter((s) => s !== sale);
|
|
250
|
+
}
|
|
235
251
|
throw error;
|
|
236
252
|
}
|
|
237
253
|
}
|
|
254
|
+
export const SWAP_FALLS_OVER = 120;
|
|
255
|
+
export const PAY_TO = "the wallet that receives what is bought, such as someone this wallet pays; default this wallet. This wallet pays for its token account if it has none";
|
|
256
|
+
export const SWAP_HEADROOM_BPS = 200n;
|
|
257
|
+
export const SWAP_BLIND_BPS = 2500n;
|
|
258
|
+
async function recentFills(transport, inputMint, outputMint) {
|
|
259
|
+
const { created, fills } = await engineHistory(transport, ESTIMATE_SCAN);
|
|
260
|
+
const unknown = [...new Set(fills.map((f) => f.intent).filter((intent) => !created.has(intent)))];
|
|
261
|
+
const views = unknown.length === 0 ? [] : await transport.getAccounts(unknown);
|
|
262
|
+
unknown.forEach((intent, i) => {
|
|
263
|
+
const view = views[i];
|
|
264
|
+
const pair = view === null || view === undefined ? null : pairOf(view.data);
|
|
265
|
+
if (pair !== null) {
|
|
266
|
+
created.set(intent, pair);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return fills
|
|
270
|
+
.filter((f) => f.amountIn > 0n && created.get(f.intent)?.inputMint === inputMint && created.get(f.intent)?.outputMint === outputMint)
|
|
271
|
+
.slice(0, ESTIMATE_FILLS);
|
|
272
|
+
}
|
|
273
|
+
const bestOf = (fills) => fills.reduce((a, b) => (a === undefined || b.delivered * a.amountIn > a.delivered * b.amountIn ? b : a), undefined);
|
|
274
|
+
function openingAsk(fills, amount, floor) {
|
|
275
|
+
const best = bestOf(fills);
|
|
276
|
+
const ask = best === undefined ? (floor * (10000n + SWAP_BLIND_BPS)) / 10000n : (amount * best.delivered * (10000n + SWAP_HEADROOM_BPS)) / (best.amountIn * 10000n);
|
|
277
|
+
return ask > floor ? ask : floor;
|
|
278
|
+
}
|
|
238
279
|
const walletSwap = define({
|
|
239
280
|
name: tool("wallet_swap"),
|
|
240
281
|
title: "Swap from the wallet",
|
|
241
|
-
description:
|
|
282
|
+
description: `Sell an exact amount of one token for at least min_receive of another, from this server's wallet: the tokens are escrowed in an intent that a solver fills or that the wallet gets back. Solvers pay the least an intent asks, so by default the ask starts at start_receive, a little above the best recent fill, and falls to min_receive over falls_over seconds (default ${SWAP_FALLS_OVER}): a solver takes it near the most it can pay. exact: true asks min_receive from the start instead. Amounts are whole tokens such as 0.05. Selling SOL wraps it first. Without send it only previews; show the user the summary, then call again with send: true and the start_receive the preview returned once they approve.`,
|
|
242
283
|
effect: "submit",
|
|
243
284
|
repeats: true,
|
|
244
285
|
input: object({
|
|
@@ -246,18 +287,282 @@ const walletSwap = define({
|
|
|
246
287
|
buy: text("SOL, TEST or a mint address"),
|
|
247
288
|
amount: text("whole tokens to sell, such as 0.05"),
|
|
248
289
|
min_receive: text("least whole tokens to receive for all of it"),
|
|
290
|
+
start_receive: optional(text("whole tokens asked when the offer lands; default a little above the best recent fill")),
|
|
291
|
+
falls_over: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `seconds the ask takes to fall to min_receive; default ${SWAP_FALLS_OVER}`)),
|
|
292
|
+
exact: optional(flag("true asks min_receive from the start, with no falling ask")),
|
|
293
|
+
pay_to: optional(account(PAY_TO)),
|
|
249
294
|
expires_in: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `least seconds the offer stays open once sent; default ${DEFAULT_EXPIRES_IN}`)),
|
|
250
295
|
send: optional(flag("true signs and sends; leave out to preview")),
|
|
251
296
|
}),
|
|
297
|
+
output: object({ ...offerFields, start_receive: optional(text("whole tokens asked when the offer lands; pass it back when sending")) }),
|
|
298
|
+
run: async (ctx, input, call) => {
|
|
299
|
+
let opening;
|
|
300
|
+
const result = await offered(ctx, call, input, async (sold, bought, now, inputMint, outputMint) => {
|
|
301
|
+
const amountIn = raw(input.amount, sold.decimals, "amount");
|
|
302
|
+
const floor = raw(input.min_receive, bought.decimals, "min_receive");
|
|
303
|
+
const expiresIn = BigInt(input.expires_in ?? DEFAULT_EXPIRES_IN);
|
|
304
|
+
const ask = input.exact === true
|
|
305
|
+
? floor
|
|
306
|
+
: input.start_receive === undefined
|
|
307
|
+
? openingAsk(await recentFills(ctx.transport, inputMint, outputMint), amountIn, floor)
|
|
308
|
+
: raw(input.start_receive, bought.decimals, "start_receive");
|
|
309
|
+
if (ask <= floor) {
|
|
310
|
+
return { order: { kind: "swap", amount_in: amountIn, min_out: floor }, expires_at: now + expiresIn + BigInt(LANDING_SECONDS) };
|
|
311
|
+
}
|
|
312
|
+
opening = formatAmount(ask, bought.decimals);
|
|
313
|
+
const start = now + BigInt(LANDING_SECONDS);
|
|
314
|
+
const falls = BigInt(input.falls_over ?? SWAP_FALLS_OVER);
|
|
315
|
+
return {
|
|
316
|
+
order: { kind: "dutch", amount_in: amountIn, start_out: ask, end_out: floor, start_time: start, end_time: start + falls },
|
|
317
|
+
expires_at: start + (expiresIn > falls ? expiresIn : falls),
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
return opening === undefined ? result : { ...result, start_receive: opening };
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
export const ESCROW_EXPIRES_IN = 86_400;
|
|
324
|
+
export const RECORD_MINT_NEEDED = "selling SOL on this RPC needs record_mint: any other token's mint, which only labels the escrow";
|
|
325
|
+
function recordedMint(sell, url, chosen) {
|
|
326
|
+
if (chosen !== undefined) {
|
|
327
|
+
return mintNamed(chosen, url, "record_mint");
|
|
328
|
+
}
|
|
329
|
+
if (sell !== NATIVE_MINT) {
|
|
330
|
+
return NATIVE_MINT;
|
|
331
|
+
}
|
|
332
|
+
const test = tokenNamed("TEST", url);
|
|
333
|
+
if (test instanceof Error) {
|
|
334
|
+
throw new InvalidInput([{ path: "record_mint", message: RECORD_MINT_NEEDED }]);
|
|
335
|
+
}
|
|
336
|
+
return test;
|
|
337
|
+
}
|
|
338
|
+
const walletEscrow = define({
|
|
339
|
+
name: tool("wallet_escrow"),
|
|
340
|
+
title: "Escrow a payment for an attestor to release",
|
|
341
|
+
description: `Lock an exact amount of one token from this server's wallet in an attested intent: it goes, minus the engine fee, to whichever wallet the named attestor later signs for, once the attestor attests a result between min_result and max_result. Use it to pay another agent for work that a third agent or a person judges. The attestor alone decides who is paid; nothing else is checked on chain, so name an attestor the user trusts. Until then the wallet can cancel it with ${tool("wallet_cancel")}, and after it expires anyone can return the escrow. Amounts are whole tokens such as 0.05. Without send it only previews; show the user the summary and call again with send: true once they approve.`,
|
|
342
|
+
effect: "submit",
|
|
343
|
+
repeats: true,
|
|
344
|
+
input: object({
|
|
345
|
+
sell: text("SOL, TEST or a mint address: the token to pay with"),
|
|
346
|
+
amount: text("whole tokens to escrow, such as 0.05"),
|
|
347
|
+
attestor: account("the wallet whose signature alone releases the escrow"),
|
|
348
|
+
min_result: optional(amount("least result the attestor may attest; default 1")),
|
|
349
|
+
max_result: optional(amount("greatest result the attestor may attest; default min_result")),
|
|
350
|
+
expires_in: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `least seconds the escrow stays open once sent; default ${ESCROW_EXPIRES_IN}`)),
|
|
351
|
+
record_mint: optional(text("a token the escrow records as its output, other than sell; only a label. Default SOL, or TEST when selling SOL on devnet")),
|
|
352
|
+
send: optional(flag("true signs and sends; leave out to preview")),
|
|
353
|
+
}),
|
|
252
354
|
output: offerOutput,
|
|
253
|
-
run: async (ctx, input, call) =>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
355
|
+
run: async (ctx, input, call) => {
|
|
356
|
+
const wallet = access(ctx);
|
|
357
|
+
const sold = mintNamed(input.sell, wallet.url, "sell");
|
|
358
|
+
const least = input.min_result ?? 1n;
|
|
359
|
+
const most = input.max_result ?? least;
|
|
360
|
+
if (most < least) {
|
|
361
|
+
throw new InvalidInput([{ path: "max_result", message: "must be at least min_result" }]);
|
|
362
|
+
}
|
|
363
|
+
return offered(ctx, call, { sell: sold, buy: recordedMint(sold, wallet.url, input.record_mint), send: input.send }, (token, _, now) => ({
|
|
364
|
+
order: { kind: "attested", amount_in: raw(input.amount, token.decimals, "amount"), min_result: least, max_result: most, attestor: input.attestor },
|
|
365
|
+
expires_at: now + BigInt((input.expires_in ?? ESCROW_EXPIRES_IN) + LANDING_SECONDS),
|
|
366
|
+
}));
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
export const notAttestor = (attestor) => `this intent names ${attestor} as its attestor, not this server's wallet; only that key can release it`;
|
|
370
|
+
const walletAttest = define({
|
|
371
|
+
name: tool("wallet_attest"),
|
|
372
|
+
title: "Release an escrow as its attestor",
|
|
373
|
+
description: `As the attestor an escrow names, sign that it pays pay_to and settle it on chain from this server's wallet: the escrowed tokens, minus the engine fee, go to pay_to's token account, which this creates if needed. Use it when this server's wallet was named attestor of an intent made with ${tool("wallet_escrow")} and the work it pays for is done. Without send it only previews and signs nothing; show the user the summary and call again with send: true once they approve.`,
|
|
374
|
+
effect: "submit",
|
|
375
|
+
input: object({
|
|
376
|
+
owner: account("the wallet that made the escrow"),
|
|
377
|
+
nonce: amount("the escrow intent's nonce"),
|
|
378
|
+
pay_to: account("the wallet to pay"),
|
|
379
|
+
result: optional(amount("the result to attest, between the intent's min_result and max_result; default min_result")),
|
|
380
|
+
send: optional(flag("true signs the attestation and settles; leave out to preview")),
|
|
381
|
+
}),
|
|
382
|
+
output: object({
|
|
383
|
+
intent: account(),
|
|
384
|
+
pay_to: account(),
|
|
385
|
+
payee: account("pay_to's token account that receives the escrow"),
|
|
386
|
+
paid: text("whole tokens pay_to receives"),
|
|
387
|
+
fee: text("whole tokens kept as fees"),
|
|
388
|
+
result: amount(),
|
|
389
|
+
summary: list(text(), 8),
|
|
390
|
+
warnings: list(text(), 8),
|
|
391
|
+
sent: flag(),
|
|
392
|
+
signature: optional(text()),
|
|
393
|
+
next: optional(text()),
|
|
394
|
+
}),
|
|
395
|
+
run: async (ctx, input, call) => {
|
|
396
|
+
const signer = await signerOf(ctx);
|
|
397
|
+
const [intent] = await findIntentPda({ owner: input.owner, nonce: input.nonce });
|
|
398
|
+
const [view, { config }] = await Promise.all([ctx.transport.getAccount(intent), loadConfig(ctx.transport)]);
|
|
399
|
+
if (view === null) {
|
|
400
|
+
throw new AgentRefusal("NotFound", `no intent ${intent}; it was settled, cancelled and closed, or never made`);
|
|
401
|
+
}
|
|
402
|
+
const held = getIntentDecoder().decode(view.data);
|
|
403
|
+
const params = decodeParams(held.kind, Uint8Array.from(held.params));
|
|
404
|
+
if (params.kind !== Kind.AttestedResult) {
|
|
405
|
+
throw new AgentRefusal("KindMismatch", "this intent is not an attested escrow");
|
|
406
|
+
}
|
|
407
|
+
if (params.attestor !== signer.address) {
|
|
408
|
+
throw new AgentRefusal("Unauthorized", notAttestor(params.attestor));
|
|
409
|
+
}
|
|
410
|
+
if (held.status !== IntentStatus.Open) {
|
|
411
|
+
throw new AgentRefusal("InvalidTransition", `this escrow is ${STATUS[held.status] ?? "not open"}, so it can no longer be released`);
|
|
412
|
+
}
|
|
413
|
+
if (held.expiresAt <= (await chainTime(ctx.transport))) {
|
|
414
|
+
throw new AgentRefusal("IntentExpired", "this escrow has expired; its owner gets it back instead");
|
|
415
|
+
}
|
|
416
|
+
const result = input.result ?? params.minResult;
|
|
417
|
+
if (result < params.minResult || result > params.maxResult) {
|
|
418
|
+
throw new InvalidInput([{ path: "result", message: `must be between ${params.minResult} and ${params.maxResult}` }]);
|
|
419
|
+
}
|
|
420
|
+
await walletRecipient(ctx.transport, input.pay_to, "pay_to");
|
|
421
|
+
const token = await mintOf(ctx.transport, held.inputMint);
|
|
422
|
+
const payee = await tokenAccountIfMissing(ctx.transport, signer, input.pay_to, held.inputMint, token.tokenProgram);
|
|
423
|
+
const amountIn = held.escrowed - held.filled;
|
|
424
|
+
const maxFeeBps = feeBpsFor(config, held.kind) + held.partnerBps;
|
|
425
|
+
const shares = splitFees(amountIn, feeBpsFor(config, held.kind), held.partnerBps, held.partnerCutBps);
|
|
426
|
+
const treasury = shares.protocol > 0n
|
|
427
|
+
? await tokenAccountIfMissing(ctx.transport, signer, config.feeRecipient, held.inputMint, token.tokenProgram)
|
|
428
|
+
: { create: [] };
|
|
429
|
+
const partner = shares.partner > 0n ? await tokenAccountIfMissing(ctx.transport, signer, held.partner, held.inputMint, token.tokenProgram) : { create: [] };
|
|
430
|
+
const message = await attestedFillMessage(signer.address, {
|
|
431
|
+
domain: Uint8Array.from(config.domain),
|
|
432
|
+
intent,
|
|
433
|
+
payee: payee.address,
|
|
434
|
+
amountIn,
|
|
435
|
+
result,
|
|
436
|
+
filled: held.filled,
|
|
437
|
+
revision: held.revision,
|
|
438
|
+
maxFeeBps,
|
|
439
|
+
});
|
|
440
|
+
const settle = await buildFillAttested({
|
|
441
|
+
solver: signer,
|
|
442
|
+
owner: input.owner,
|
|
443
|
+
nonce: input.nonce,
|
|
444
|
+
inputMint: held.inputMint,
|
|
445
|
+
inputTokenProgram: token.tokenProgram,
|
|
446
|
+
payee: payee.address,
|
|
447
|
+
...(shares.protocol > 0n ? { feeRecipient: config.feeRecipient } : {}),
|
|
448
|
+
...(held.partnerBps === 0 ? {} : { partner: held.partner }),
|
|
449
|
+
amountIn,
|
|
450
|
+
result,
|
|
451
|
+
maxFeeBps,
|
|
452
|
+
});
|
|
453
|
+
const name = TOKEN_NAMES[held.inputMint] ?? held.inputMint;
|
|
454
|
+
const paid = formatAmount(shares.net, token.decimals);
|
|
455
|
+
const setup = [...payee.create, ...treasury.create, ...partner.create];
|
|
456
|
+
const prepared = input.send === true
|
|
457
|
+
? preparedView(await prepare(ctx.transport, [...setup, await signEd25519Instruction(signer, message), settle], signer.address))
|
|
458
|
+
: undefined;
|
|
459
|
+
const view2 = {
|
|
460
|
+
intent,
|
|
461
|
+
pay_to: input.pay_to,
|
|
462
|
+
payee: payee.address,
|
|
463
|
+
paid,
|
|
464
|
+
fee: formatAmount(shares.protocol + shares.partner, token.decimals),
|
|
465
|
+
result,
|
|
466
|
+
summary: [
|
|
467
|
+
`Pays: ${paid} of ${name} to ${input.pay_to} from ${input.owner}'s escrow${held.inputMint === NATIVE_MINT ? WRAPPED : ""}`,
|
|
468
|
+
`Attests: result ${result}, signed by this server's wallet as the escrow's attestor`,
|
|
469
|
+
...(payee.create.length === 0 ? [] : [`Setup: creates ${input.pay_to}'s ${name} account, paid by this wallet`]),
|
|
470
|
+
...(treasury.create.length === 0 ? [] : [`Setup: creates the engine treasury's ${name} account for the fee, paid by this wallet`]),
|
|
471
|
+
...(partner.create.length === 0 ? [] : [`Setup: creates the partner ${held.partner}'s ${name} account for its fee, paid by this wallet`]),
|
|
472
|
+
],
|
|
473
|
+
warnings: [WARN_ATTESTOR],
|
|
474
|
+
prepared,
|
|
475
|
+
};
|
|
476
|
+
return sent(ctx, signer, view2, input.send, call);
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
export const MAX_RECURRING = 12;
|
|
480
|
+
export const mayHaveLanded = (sent, times, problem) => `${sent} of ${times} orders were sent and the next may have landed too (${problem}); check ${tool("wallet_intents")} before sending any again`;
|
|
481
|
+
export const MAX_EVERY = 30 * 24 * 60 * 60;
|
|
482
|
+
const walletRecurring = define({
|
|
483
|
+
name: tool("wallet_recurring"),
|
|
484
|
+
title: "Repeat a swap on a schedule",
|
|
485
|
+
description: `Sell the same amount of one token for another several times, every so many seconds, from this server's wallet, with no bot left running: all orders are sent now and escrowed now. Each order holds its ask at start_receive, a little above the best recent fill, until its turn, then lets it fall to min_receive over falls_over seconds; it expires when the next one opens, or falls_over seconds after its own opening if that is later, and anyone can then return its escrow. Any order can be cancelled with ${tool("wallet_cancel")}. Each order pays the create fee and rent, and the rent returns when it closes. Amounts are whole tokens per order. Without send it only previews; show the user the summary, then call again with send: true and the start_receive the preview returned once they approve.`,
|
|
486
|
+
effect: "submit",
|
|
487
|
+
repeats: true,
|
|
488
|
+
input: object({
|
|
489
|
+
sell: text("SOL, TEST or a mint address"),
|
|
490
|
+
buy: text("SOL, TEST or a mint address"),
|
|
491
|
+
amount: text("whole tokens each order sells, such as 0.05"),
|
|
492
|
+
min_receive: text("least whole tokens each order receives"),
|
|
493
|
+
every: integer(MIN_EXPIRES_IN, MAX_EVERY, "seconds between one order opening and the next"),
|
|
494
|
+
times: integer(2, MAX_RECURRING, "how many orders"),
|
|
495
|
+
start_receive: optional(text("whole tokens each order asks when it opens; default a little above the best recent fill")),
|
|
496
|
+
falls_over: optional(integer(MIN_EXPIRES_IN, MAX_EVERY, `seconds each order's ask takes to fall to min_receive; default ${SWAP_FALLS_OVER}`)),
|
|
497
|
+
pay_to: optional(account(PAY_TO)),
|
|
498
|
+
send: optional(flag("true signs and sends every order; leave out to preview")),
|
|
499
|
+
}),
|
|
500
|
+
output: object({
|
|
501
|
+
orders: list(object({ opens_at: time("when the order's ask starts to fall"), intent: optional(account()), nonce: optional(amount()), signature: optional(text()) }), MAX_RECURRING),
|
|
502
|
+
summary: list(text(), 24, "the first order"),
|
|
503
|
+
warnings: list(text(), 16),
|
|
504
|
+
start_receive: text("whole tokens each order asks when it opens; pass it back when sending"),
|
|
505
|
+
sent: flag(),
|
|
506
|
+
next: optional(text()),
|
|
507
|
+
}),
|
|
508
|
+
run: async (ctx, input, call) => {
|
|
509
|
+
const wallet = access(ctx);
|
|
510
|
+
const inputMint = mintNamed(input.sell, wallet.url, "sell");
|
|
511
|
+
const outputMint = mintNamed(input.buy, wallet.url, "buy");
|
|
512
|
+
const [inputToken, outputToken, now] = await Promise.all([
|
|
513
|
+
mintOf(ctx.transport, inputMint),
|
|
514
|
+
mintOf(ctx.transport, outputMint),
|
|
515
|
+
chainTime(ctx.transport),
|
|
516
|
+
]);
|
|
517
|
+
const amountIn = raw(input.amount, inputToken.decimals, "amount");
|
|
518
|
+
const floor = raw(input.min_receive, outputToken.decimals, "min_receive");
|
|
519
|
+
const ask = input.start_receive === undefined
|
|
520
|
+
? openingAsk(await recentFills(ctx.transport, inputMint, outputMint), amountIn, floor)
|
|
521
|
+
: raw(input.start_receive, outputToken.decimals, "start_receive");
|
|
522
|
+
if (ask <= floor) {
|
|
523
|
+
throw new InvalidInput([{ path: "start_receive", message: "must be more than min_receive, or the order has no ask to fall" }]);
|
|
524
|
+
}
|
|
525
|
+
checkSale(wallet.limits, inputMint, inputToken.decimals, amountIn, now);
|
|
526
|
+
checkHour(wallet.limits, inputMint, inputToken.decimals, amountIn * BigInt(input.times), now);
|
|
527
|
+
const falls = BigInt(input.falls_over ?? SWAP_FALLS_OVER);
|
|
528
|
+
const every = BigInt(input.every);
|
|
529
|
+
const life = falls > every ? falls : every;
|
|
530
|
+
const opens = Array.from({ length: input.times }, (_, k) => now + BigInt(LANDING_SECONDS) + every * BigInt(k));
|
|
531
|
+
const terms = (open) => () => ({
|
|
532
|
+
order: { kind: "dutch", amount_in: amountIn, start_out: ask, end_out: floor, start_time: open, end_time: open + falls },
|
|
533
|
+
expires_at: open + life,
|
|
534
|
+
});
|
|
535
|
+
const shown = { start_receive: formatAmount(ask, outputToken.decimals) };
|
|
536
|
+
const first = (await offered(ctx, call, { ...input, send: input.send }, terms(opens[0])));
|
|
537
|
+
if (input.send !== true) {
|
|
538
|
+
const { summary, warnings, next } = first;
|
|
539
|
+
return { orders: opens.map((opens_at) => ({ opens_at })), summary, warnings, ...shown, sent: false, next };
|
|
540
|
+
}
|
|
541
|
+
const orders = [{ opens_at: opens[0], intent: first["intent"], nonce: first["nonce"], signature: first["signature"] }];
|
|
542
|
+
for (const open of opens.slice(1)) {
|
|
543
|
+
try {
|
|
544
|
+
const made = (await offered(ctx, call, { ...input, send: true }, terms(open)));
|
|
545
|
+
orders.push({ opens_at: open, intent: made["intent"], nonce: made["nonce"], signature: made["signature"] });
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
for (const undo of call.undo.splice(0)) {
|
|
549
|
+
undo();
|
|
550
|
+
}
|
|
551
|
+
const problem = problemOf(error);
|
|
552
|
+
return {
|
|
553
|
+
orders: [...orders, ...opens.slice(orders.length).map((opens_at) => ({ opens_at }))],
|
|
554
|
+
summary: first["summary"],
|
|
555
|
+
warnings: first["warnings"],
|
|
556
|
+
...shown,
|
|
557
|
+
sent: true,
|
|
558
|
+
next: neverLanded(error)
|
|
559
|
+
? `${orders.length} of ${input.times} orders were sent; the next failed with ${problem.code}: ${problem.hint}`
|
|
560
|
+
: mayHaveLanded(orders.length, input.times, `${problem.code}: ${problem.hint}`),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return { orders, summary: first["summary"], warnings: first["warnings"], ...shown, sent: true };
|
|
565
|
+
},
|
|
261
566
|
});
|
|
262
567
|
const walletAuction = define({
|
|
263
568
|
name: tool("wallet_auction"),
|
|
@@ -273,6 +578,7 @@ const walletAuction = define({
|
|
|
273
578
|
end_receive: text("least whole tokens accepted once the asked amount stops falling"),
|
|
274
579
|
falls_over: integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, "seconds the asked amount takes to fall from start_receive to end_receive"),
|
|
275
580
|
expires_in: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `least seconds the offer stays open once sent, never less than falls_over; default ${DEFAULT_EXPIRES_IN}`)),
|
|
581
|
+
pay_to: optional(account(PAY_TO)),
|
|
276
582
|
send: optional(flag("true signs and sends; leave out to preview")),
|
|
277
583
|
}),
|
|
278
584
|
output: offerOutput,
|
|
@@ -394,19 +700,7 @@ const walletEstimate = define({
|
|
|
394
700
|
const outputMint = mintNamed(input.buy, wallet.url, "buy");
|
|
395
701
|
const [inputToken, outputToken] = await Promise.all([mintOf(ctx.transport, inputMint), mintOf(ctx.transport, outputMint)]);
|
|
396
702
|
const amount = raw(input.amount, inputToken.decimals, "amount");
|
|
397
|
-
const
|
|
398
|
-
const unknown = [...new Set(fills.map((f) => f.intent).filter((intent) => !created.has(intent)))];
|
|
399
|
-
const views = unknown.length === 0 ? [] : await ctx.transport.getAccounts(unknown);
|
|
400
|
-
unknown.forEach((intent, i) => {
|
|
401
|
-
const view = views[i];
|
|
402
|
-
const pair = view === null || view === undefined ? null : pairOf(view.data);
|
|
403
|
-
if (pair !== null) {
|
|
404
|
-
created.set(intent, pair);
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
const matching = fills
|
|
408
|
-
.filter((f) => f.amountIn > 0n && created.get(f.intent)?.inputMint === inputMint && created.get(f.intent)?.outputMint === outputMint)
|
|
409
|
-
.slice(0, ESTIMATE_FILLS);
|
|
703
|
+
const matching = await recentFills(ctx.transport, inputMint, outputMint);
|
|
410
704
|
const shown = matching.map((f) => ({
|
|
411
705
|
sold: formatAmount(f.amountIn, inputToken.decimals),
|
|
412
706
|
received: formatAmount(f.delivered, outputToken.decimals),
|
|
@@ -417,7 +711,7 @@ const walletEstimate = define({
|
|
|
417
711
|
if (latest === undefined) {
|
|
418
712
|
return { fills: shown, next: NO_FILLS };
|
|
419
713
|
}
|
|
420
|
-
const best = matching
|
|
714
|
+
const best = bestOf(matching) ?? latest;
|
|
421
715
|
const at = (f) => formatAmount((amount * f.delivered) / f.amountIn, outputToken.decimals);
|
|
422
716
|
return { fills: shown, receive_at_best: at(best), receive_at_latest: at(latest), next: ESTIMATE_NEXT };
|
|
423
717
|
},
|
|
@@ -437,7 +731,7 @@ const walletWait = define({
|
|
|
437
731
|
intent: account(),
|
|
438
732
|
status: choice(WAIT_STATUS, "closed means the account is gone and its recent transactions name no fill, cancel or expiry"),
|
|
439
733
|
waited: integer(0, WAIT_MAX, "seconds waited"),
|
|
440
|
-
received: optional(text("whole tokens the wallet
|
|
734
|
+
received: optional(text("whole tokens the fills delivered, to this wallet or the pay_to it named")),
|
|
441
735
|
token: optional(text("the token received")),
|
|
442
736
|
signature: optional(text("the fill's transaction")),
|
|
443
737
|
next: optional(text()),
|
|
@@ -448,6 +742,7 @@ const walletWait = define({
|
|
|
448
742
|
const [intent] = await findIntentPda({ owner, nonce: input.nonce });
|
|
449
743
|
const within = input.within ?? WAIT_DEFAULT;
|
|
450
744
|
const fresh = unmemoized(ctx.transport);
|
|
745
|
+
const deadline = wallet.now() + BigInt(within);
|
|
451
746
|
let waited = 0;
|
|
452
747
|
let pair = null;
|
|
453
748
|
let status = "closed";
|
|
@@ -460,7 +755,7 @@ const walletWait = define({
|
|
|
460
755
|
const decoded = getIntentDecoder().decode(view.data);
|
|
461
756
|
pair = { inputMint: decoded.inputMint, outputMint: decoded.outputMint };
|
|
462
757
|
status = STATUS[decoded.status] ?? "open";
|
|
463
|
-
if ((status !== "open" && status !== "filling") || waited >= within || !(await wallet.sleep(1))) {
|
|
758
|
+
if ((status !== "open" && status !== "filling") || waited >= within || wallet.now() >= deadline || !(await wallet.sleep(1))) {
|
|
464
759
|
break;
|
|
465
760
|
}
|
|
466
761
|
waited += 1;
|
|
@@ -494,6 +789,9 @@ export const WALLET_ACTIONS = [
|
|
|
494
789
|
walletEstimate,
|
|
495
790
|
walletSwap,
|
|
496
791
|
walletAuction,
|
|
792
|
+
walletRecurring,
|
|
793
|
+
walletEscrow,
|
|
794
|
+
walletAttest,
|
|
497
795
|
walletWait,
|
|
498
796
|
walletIntents,
|
|
499
797
|
walletCancel,
|