@intent-engine/cli 0.1.14 → 0.1.16
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.js +5 -2
- package/dist/src/agent/create.js.map +1 -1
- package/dist/src/agent/mcp.js +7 -5
- package/dist/src/agent/mcp.js.map +1 -1
- package/dist/src/agent/owner.d.ts +1 -0
- package/dist/src/agent/owner.js +5 -3
- package/dist/src/agent/owner.js.map +1 -1
- package/dist/src/agent/problems.d.ts +1 -0
- package/dist/src/agent/problems.js +1 -0
- package/dist/src/agent/problems.js.map +1 -1
- package/dist/src/agent/wallet.d.ts +15 -2
- package/dist/src/agent/wallet.js +258 -32
- package/dist/src/agent/wallet.js.map +1 -1
- package/dist/src/transport.d.ts +15 -1
- package/dist/src/transport.js +42 -20
- 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,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AccountRole, getAddressDecoder } from "@solana/kit";
|
|
3
|
+
import { attestedFillMessage, buildFillAttested, decodeEvent, decodeParams, feeBpsFor, findIntentPda, formatAmount, formatTime, getIntentDecoder, IntentStatus, Kind, LONG_LIVED_SECONDS, parseAmount, signEd25519Instruction, splitFees, WARN_ATTESTOR, WARN_LONG_LIVED, } from "@intent-engine/core";
|
|
2
4
|
import { chainTime, CLOCK_SYSVAR, loadConfig, mintOf } from "../accounts.js";
|
|
3
5
|
import { ENV_KEYPAIR, WALLET_NOT_MAINNET } from "../args.js";
|
|
4
6
|
import { execute } from "../flows.js";
|
|
5
7
|
import { NATIVE_MINT, TOKEN_NAMES, tokenNamed } from "../tokens.js";
|
|
6
|
-
import { neverLanded, unmemoized } from "../transport.js";
|
|
8
|
+
import { MEMO_PROGRAM_ADDRESS, neverLanded, unmemoized } from "../transport.js";
|
|
7
9
|
import { balances, defaultMints, FAUCET, PUBLIC_FAUCET, setup, withSettings } from "../wallet.js";
|
|
8
10
|
import { define } from "./actions.js";
|
|
9
11
|
import { MAX_EXPIRES_IN, MIN_EXPIRES_IN, prepareCreate, WRAPPED } from "./create.js";
|
|
@@ -255,6 +257,7 @@ export const SWAP_FALLS_OVER = 120;
|
|
|
255
257
|
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
258
|
export const SWAP_HEADROOM_BPS = 200n;
|
|
257
259
|
export const SWAP_BLIND_BPS = 2500n;
|
|
260
|
+
export const SWAP_CAP_BPS = 20000n;
|
|
258
261
|
async function recentFills(transport, inputMint, outputMint) {
|
|
259
262
|
const { created, fills } = await engineHistory(transport, ESTIMATE_SCAN);
|
|
260
263
|
const unknown = [...new Set(fills.map((f) => f.intent).filter((intent) => !created.has(intent)))];
|
|
@@ -270,16 +273,21 @@ async function recentFills(transport, inputMint, outputMint) {
|
|
|
270
273
|
.filter((f) => f.amountIn > 0n && created.get(f.intent)?.inputMint === inputMint && created.get(f.intent)?.outputMint === outputMint)
|
|
271
274
|
.slice(0, ESTIMATE_FILLS);
|
|
272
275
|
}
|
|
273
|
-
const
|
|
276
|
+
const byRate = (fills) => [...fills].sort((a, b) => (a.delivered * b.amountIn < b.delivered * a.amountIn ? -1 : a.delivered * b.amountIn > b.delivered * a.amountIn ? 1 : 0));
|
|
277
|
+
const bestOf = (fills) => byRate(fills).at(-1);
|
|
278
|
+
const medianOf = (fills) => byRate(fills)[Math.floor((fills.length - 1) / 2)];
|
|
274
279
|
function openingAsk(fills, amount, floor) {
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
280
|
+
const typical = medianOf(fills);
|
|
281
|
+
const blind = (floor * (10000n + SWAP_BLIND_BPS)) / 10000n;
|
|
282
|
+
const seen = typical === undefined ? 0n : (amount * typical.delivered * (10000n + SWAP_HEADROOM_BPS)) / (typical.amountIn * 10000n);
|
|
283
|
+
const cap = (floor * SWAP_CAP_BPS) / 10000n;
|
|
284
|
+
const ask = seen > blind ? seen : blind;
|
|
285
|
+
return ask < cap ? ask : cap;
|
|
278
286
|
}
|
|
279
287
|
const walletSwap = define({
|
|
280
288
|
name: tool("wallet_swap"),
|
|
281
289
|
title: "Swap from the wallet",
|
|
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
|
|
290
|
+
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 median of recent fills but at least a quarter above min_receive and at most twice it, 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.`,
|
|
283
291
|
effect: "submit",
|
|
284
292
|
repeats: true,
|
|
285
293
|
input: object({
|
|
@@ -287,7 +295,7 @@ const walletSwap = define({
|
|
|
287
295
|
buy: text("SOL, TEST or a mint address"),
|
|
288
296
|
amount: text("whole tokens to sell, such as 0.05"),
|
|
289
297
|
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
|
|
298
|
+
start_receive: optional(text("whole tokens asked when the offer lands; default a little above the median recent fill, between 1.25 and 2 times min_receive")),
|
|
291
299
|
falls_over: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `seconds the ask takes to fall to min_receive; default ${SWAP_FALLS_OVER}`)),
|
|
292
300
|
exact: optional(flag("true asks min_receive from the start, with no falling ask")),
|
|
293
301
|
pay_to: optional(account(PAY_TO)),
|
|
@@ -338,7 +346,7 @@ function recordedMint(sell, url, chosen) {
|
|
|
338
346
|
const walletEscrow = define({
|
|
339
347
|
name: tool("wallet_escrow"),
|
|
340
348
|
title: "Escrow a payment for an attestor to release",
|
|
341
|
-
description: `
|
|
349
|
+
description: `Hold 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. Have the worker deliver to the attestor rather than to this wallet, since this wallet can cancel until the release; ${tool("wallet_receipt")} later shows what the release recorded. 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
350
|
effect: "submit",
|
|
343
351
|
repeats: true,
|
|
344
352
|
input: object({
|
|
@@ -366,17 +374,118 @@ const walletEscrow = define({
|
|
|
366
374
|
}));
|
|
367
375
|
},
|
|
368
376
|
});
|
|
377
|
+
export const MAX_PIECES = 12;
|
|
378
|
+
export const piecesPartlySent = (sent, total, problem) => `${sent} of ${total} pieces were escrowed; the next failed with ${problem}. To finish, escrow only the ${total - sent} remaining pieces; do not repeat this call`;
|
|
379
|
+
export const piecesMayHaveLanded = (sent, total, problem) => `${sent} of ${total} pieces were escrowed and the next may have landed too (${problem}); check ${tool("wallet_intents")} before escrowing any again, and do not repeat this call`;
|
|
380
|
+
const walletEscrowPlan = define({
|
|
381
|
+
name: tool("wallet_escrow_plan"),
|
|
382
|
+
title: "Escrow a task's pieces in one plan",
|
|
383
|
+
description: `Split a task into pieces and escrow each piece's payment from this server's wallet in one call, each as ${tool("wallet_escrow")} would: every piece is its own escrow that its attestor releases on its own, to whichever wallet it signs for, so workers are paid as each piece passes. One preview shows every piece and the total; one send escrows them all. A piece not yet released can be cancelled on its own with ${tool("wallet_cancel")}. Have workers deliver to the attestor rather than to this wallet. Amounts are whole tokens. Without send it only previews; show the user the summary and call again with send: true once they approve.`,
|
|
384
|
+
effect: "submit",
|
|
385
|
+
repeats: true,
|
|
386
|
+
input: object({
|
|
387
|
+
sell: text("SOL, TEST or a mint address: the token to pay with"),
|
|
388
|
+
attestor: account("the wallet that releases every piece that names no attestor of its own"),
|
|
389
|
+
pieces: list(object({
|
|
390
|
+
name: text("a label for the piece, shown in the plan only"),
|
|
391
|
+
amount: text("whole tokens this piece pays, such as 0.01"),
|
|
392
|
+
attestor: optional(account("this piece's attestor, instead of the plan's")),
|
|
393
|
+
}), MAX_PIECES, "the pieces, in order", 2),
|
|
394
|
+
expires_in: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `least seconds each escrow stays open once sent; default ${ESCROW_EXPIRES_IN}`)),
|
|
395
|
+
record_mint: optional(text("a token the escrows record as their output, other than sell; only a label. Default SOL, or TEST when selling SOL on devnet")),
|
|
396
|
+
send: optional(flag("true signs and sends every piece; leave out to preview")),
|
|
397
|
+
}),
|
|
398
|
+
output: object({
|
|
399
|
+
pieces: list(object({ name: text(), amount: text(), attestor: account(), intent: optional(account()), nonce: optional(amount()), signature: optional(text()) }), MAX_PIECES),
|
|
400
|
+
total: text("whole tokens escrowed across all pieces"),
|
|
401
|
+
summary: list(text(), 24, "the plan, then the first piece"),
|
|
402
|
+
warnings: list(text(), 16),
|
|
403
|
+
sent: flag(),
|
|
404
|
+
next: optional(text()),
|
|
405
|
+
}),
|
|
406
|
+
run: async (ctx, input, call) => {
|
|
407
|
+
const wallet = access(ctx);
|
|
408
|
+
const sold = mintNamed(input.sell, wallet.url, "sell");
|
|
409
|
+
const buy = recordedMint(sold, wallet.url, input.record_mint);
|
|
410
|
+
const [token, now] = await Promise.all([mintOf(ctx.transport, sold), chainTime(ctx.transport)]);
|
|
411
|
+
const planned = input.pieces.map((piece, i) => ({
|
|
412
|
+
name: piece.name,
|
|
413
|
+
amountIn: raw(piece.amount, token.decimals, `pieces.${i}.amount`),
|
|
414
|
+
attestor: piece.attestor ?? input.attestor,
|
|
415
|
+
}));
|
|
416
|
+
const total = planned.reduce((sum, piece) => sum + piece.amountIn, 0n);
|
|
417
|
+
for (const piece of planned) {
|
|
418
|
+
checkSale(wallet.limits, sold, token.decimals, piece.amountIn, now);
|
|
419
|
+
}
|
|
420
|
+
checkHour(wallet.limits, sold, token.decimals, total, now);
|
|
421
|
+
const terms = (piece) => (_sold, _bought, at) => ({
|
|
422
|
+
order: { kind: "attested", amount_in: piece.amountIn, min_result: 1n, max_result: 1n, attestor: piece.attestor },
|
|
423
|
+
expires_at: at + BigInt((input.expires_in ?? ESCROW_EXPIRES_IN) + LANDING_SECONDS),
|
|
424
|
+
});
|
|
425
|
+
const name = TOKEN_NAMES[sold] ?? sold;
|
|
426
|
+
const listed = (piece) => ({ name: piece.name, amount: formatAmount(piece.amountIn, token.decimals), attestor: piece.attestor });
|
|
427
|
+
const [head, ...rest] = planned;
|
|
428
|
+
const first = (await offered(ctx, call, { sell: sold, buy, send: input.send }, terms(head)));
|
|
429
|
+
const shown = {
|
|
430
|
+
total: formatAmount(total, token.decimals),
|
|
431
|
+
summary: [
|
|
432
|
+
`Plan: ${planned.length} pieces, ${formatAmount(total, token.decimals)} of ${name} in all, each escrowed now and released on its own`,
|
|
433
|
+
...planned.map((piece, i) => `Piece ${i + 1}: ${piece.name}, ${formatAmount(piece.amountIn, token.decimals)} of ${name}, released by ${piece.attestor}`),
|
|
434
|
+
...first["summary"],
|
|
435
|
+
],
|
|
436
|
+
warnings: first["warnings"],
|
|
437
|
+
};
|
|
438
|
+
if (input.send !== true) {
|
|
439
|
+
return { pieces: planned.map(listed), ...shown, sent: false, next: first["next"] };
|
|
440
|
+
}
|
|
441
|
+
const pieces = [{ ...listed(head), intent: first["intent"], nonce: first["nonce"], signature: first["signature"] }];
|
|
442
|
+
for (const piece of rest) {
|
|
443
|
+
try {
|
|
444
|
+
const made = (await offered(ctx, call, { sell: sold, buy, send: true }, terms(piece)));
|
|
445
|
+
pieces.push({ ...listed(piece), intent: made["intent"], nonce: made["nonce"], signature: made["signature"] });
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
for (const undo of call.undo.splice(0)) {
|
|
449
|
+
undo();
|
|
450
|
+
}
|
|
451
|
+
const problem = problemOf(error);
|
|
452
|
+
const reason = `${problem.code}: ${problem.hint}`;
|
|
453
|
+
return {
|
|
454
|
+
pieces: [...pieces, ...planned.slice(pieces.length).map(listed)],
|
|
455
|
+
...shown,
|
|
456
|
+
sent: true,
|
|
457
|
+
next: refusedBeforeSending(error) ? piecesPartlySent(pieces.length, planned.length, reason) : piecesMayHaveLanded(pieces.length, planned.length, reason),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return { pieces, ...shown, sent: true };
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
export const PAYEE_MOVED = "this wallet's token account for the escrowed token now belongs to someone else, so a release would not reach it; ask the payee for another wallet";
|
|
465
|
+
export const RELEASE_CHANGED = "this release differs from its preview: the payee, amount, result, fee or the escrow itself changed, or no approval was given; preview it again and show the user";
|
|
466
|
+
export const approvalOf = (message, memo = "") => createHash("sha256").update(message).update(memo).digest("hex").slice(0, 16);
|
|
467
|
+
export const DELIVERY_PREFIX = "delivery sha256:";
|
|
468
|
+
const DELIVERY_MEMO = /^delivery sha256:([0-9a-f]{64})$/;
|
|
469
|
+
function sha256Of(text, path) {
|
|
470
|
+
const hex = text.trim().toLowerCase();
|
|
471
|
+
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
|
472
|
+
throw new InvalidInput([{ path, message: "give a sha256 as 64 hex digits" }]);
|
|
473
|
+
}
|
|
474
|
+
return hex;
|
|
475
|
+
}
|
|
369
476
|
export const notAttestor = (attestor) => `this intent names ${attestor} as its attestor, not this server's wallet; only that key can release it`;
|
|
370
477
|
const walletAttest = define({
|
|
371
478
|
name: tool("wallet_attest"),
|
|
372
479
|
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
|
|
480
|
+
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 and any partner 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. A release cannot be undone. Without send it only previews and signs nothing; show the user the summary and, once they approve, call again with send: true and the approval the preview returned, which holds the send to exactly what was previewed.`,
|
|
374
481
|
effect: "submit",
|
|
375
482
|
input: object({
|
|
376
483
|
owner: account("the wallet that made the escrow"),
|
|
377
484
|
nonce: amount("the escrow intent's nonce"),
|
|
378
485
|
pay_to: account("the wallet to pay"),
|
|
379
486
|
result: optional(amount("the result to attest, between the intent's min_result and max_result; default min_result")),
|
|
487
|
+
delivered_sha256: optional(text(`the sha256, as 64 hex digits, of the work this release pays for; recorded in a memo this wallet signs with the release, so the payer can check what it receives with ${tool("wallet_receipt")}`)),
|
|
488
|
+
approval: optional(text("the approval the preview returned; needed with send")),
|
|
380
489
|
send: optional(flag("true signs the attestation and settles; leave out to preview")),
|
|
381
490
|
}),
|
|
382
491
|
output: object({
|
|
@@ -386,6 +495,8 @@ const walletAttest = define({
|
|
|
386
495
|
paid: text("whole tokens pay_to receives"),
|
|
387
496
|
fee: text("whole tokens kept as fees"),
|
|
388
497
|
result: amount(),
|
|
498
|
+
approval: text("pass it back with send: true; a send that would differ from this preview is refused"),
|
|
499
|
+
delivered_sha256: optional(text("the sha256 the release records")),
|
|
389
500
|
summary: list(text(), 8),
|
|
390
501
|
warnings: list(text(), 8),
|
|
391
502
|
sent: flag(),
|
|
@@ -420,13 +531,21 @@ const walletAttest = define({
|
|
|
420
531
|
await walletRecipient(ctx.transport, input.pay_to, "pay_to");
|
|
421
532
|
const token = await mintOf(ctx.transport, held.inputMint);
|
|
422
533
|
const payee = await tokenAccountIfMissing(ctx.transport, signer, input.pay_to, held.inputMint, token.tokenProgram);
|
|
534
|
+
if (payee.create.length === 0) {
|
|
535
|
+
const existing = await ctx.transport.getAccount(payee.address);
|
|
536
|
+
const holder = existing === null || existing.data.length < 64 ? null : getAddressDecoder().decode(existing.data.subarray(32, 64));
|
|
537
|
+
if (holder !== input.pay_to) {
|
|
538
|
+
throw new InvalidInput([{ path: "pay_to", message: PAYEE_MOVED }]);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
423
541
|
const amountIn = held.escrowed - held.filled;
|
|
424
542
|
const maxFeeBps = feeBpsFor(config, held.kind) + held.partnerBps;
|
|
425
543
|
const shares = splitFees(amountIn, feeBpsFor(config, held.kind), held.partnerBps, held.partnerCutBps);
|
|
426
544
|
const treasury = shares.protocol > 0n
|
|
427
545
|
? await tokenAccountIfMissing(ctx.transport, signer, config.feeRecipient, held.inputMint, token.tokenProgram)
|
|
428
546
|
: { create: [] };
|
|
429
|
-
const
|
|
547
|
+
const partnerPaid = shares.partner > 0n;
|
|
548
|
+
const partner = partnerPaid ? await tokenAccountIfMissing(ctx.transport, signer, held.partner, held.inputMint, token.tokenProgram) : { create: [] };
|
|
430
549
|
const message = await attestedFillMessage(signer.address, {
|
|
431
550
|
domain: Uint8Array.from(config.domain),
|
|
432
551
|
intent,
|
|
@@ -445,7 +564,7 @@ const walletAttest = define({
|
|
|
445
564
|
inputTokenProgram: token.tokenProgram,
|
|
446
565
|
payee: payee.address,
|
|
447
566
|
...(shares.protocol > 0n ? { feeRecipient: config.feeRecipient } : {}),
|
|
448
|
-
...(
|
|
567
|
+
...(partnerPaid ? { partner: held.partner } : {}),
|
|
449
568
|
amountIn,
|
|
450
569
|
result,
|
|
451
570
|
maxFeeBps,
|
|
@@ -453,8 +572,17 @@ const walletAttest = define({
|
|
|
453
572
|
const name = TOKEN_NAMES[held.inputMint] ?? held.inputMint;
|
|
454
573
|
const paid = formatAmount(shares.net, token.decimals);
|
|
455
574
|
const setup = [...payee.create, ...treasury.create, ...partner.create];
|
|
575
|
+
const delivered = input.delivered_sha256 === undefined ? undefined : sha256Of(input.delivered_sha256, "delivered_sha256");
|
|
576
|
+
const memoText = delivered === undefined ? "" : `${DELIVERY_PREFIX}${delivered}`;
|
|
577
|
+
const memo = delivered === undefined
|
|
578
|
+
? []
|
|
579
|
+
: [{ programAddress: MEMO_PROGRAM_ADDRESS, accounts: [{ address: signer.address, role: AccountRole.READONLY_SIGNER, signer }], data: new TextEncoder().encode(memoText) }];
|
|
580
|
+
const approval = approvalOf(message, memoText);
|
|
581
|
+
if (input.send === true && input.approval !== approval) {
|
|
582
|
+
throw new AgentRefusal("ReleaseChanged", RELEASE_CHANGED);
|
|
583
|
+
}
|
|
456
584
|
const prepared = input.send === true
|
|
457
|
-
? preparedView(await prepare(ctx.transport, [...setup, await signEd25519Instruction(signer, message), settle], signer.address))
|
|
585
|
+
? preparedView(await prepare(ctx.transport, [...setup, await signEd25519Instruction(signer, message), settle, ...memo], signer.address))
|
|
458
586
|
: undefined;
|
|
459
587
|
const view2 = {
|
|
460
588
|
intent,
|
|
@@ -463,9 +591,12 @@ const walletAttest = define({
|
|
|
463
591
|
paid,
|
|
464
592
|
fee: formatAmount(shares.protocol + shares.partner, token.decimals),
|
|
465
593
|
result,
|
|
594
|
+
approval,
|
|
595
|
+
...(delivered === undefined ? {} : { delivered_sha256: delivered }),
|
|
466
596
|
summary: [
|
|
467
597
|
`Pays: ${paid} of ${name} to ${input.pay_to} from ${input.owner}'s escrow${held.inputMint === NATIVE_MINT ? WRAPPED : ""}`,
|
|
468
598
|
`Attests: result ${result}, signed by this server's wallet as the escrow's attestor`,
|
|
599
|
+
...(delivered === undefined ? [] : [`Records: the delivered work's sha256 ${delivered}, in a memo this wallet signs with the release`]),
|
|
469
600
|
...(payee.create.length === 0 ? [] : [`Setup: creates ${input.pay_to}'s ${name} account, paid by this wallet`]),
|
|
470
601
|
...(treasury.create.length === 0 ? [] : [`Setup: creates the engine treasury's ${name} account for the fee, paid by this wallet`]),
|
|
471
602
|
...(partner.create.length === 0 ? [] : [`Setup: creates the partner ${held.partner}'s ${name} account for its fee, paid by this wallet`]),
|
|
@@ -477,12 +608,16 @@ const walletAttest = define({
|
|
|
477
608
|
},
|
|
478
609
|
});
|
|
479
610
|
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`;
|
|
611
|
+
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, and do not repeat this call`;
|
|
612
|
+
export const partlySent = (sent, times, problem) => `${sent} of ${times} orders were sent; the next failed with ${problem}. To finish, schedule only the ${times - sent} remaining orders; do not repeat this call`;
|
|
613
|
+
export const WARN_EARLY = "an order fills before its turn if a solver can already pay its start_receive";
|
|
614
|
+
export const NO_FALL = "must be more than 0, or the orders have no ask to fall";
|
|
615
|
+
const refusedBeforeSending = (error) => neverLanded(error) || error instanceof AgentRefusal || error instanceof InvalidInput;
|
|
481
616
|
export const MAX_EVERY = 30 * 24 * 60 * 60;
|
|
482
617
|
const walletRecurring = define({
|
|
483
618
|
name: tool("wallet_recurring"),
|
|
484
619
|
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
|
|
620
|
+
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 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. An order fills before its turn if a solver can already pay start_receive, so set it above what solvers pay now to hold the orders. 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
621
|
effect: "submit",
|
|
487
622
|
repeats: true,
|
|
488
623
|
input: object({
|
|
@@ -492,14 +627,14 @@ const walletRecurring = define({
|
|
|
492
627
|
min_receive: text("least whole tokens each order receives"),
|
|
493
628
|
every: integer(MIN_EXPIRES_IN, MAX_EVERY, "seconds between one order opening and the next"),
|
|
494
629
|
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
|
|
630
|
+
start_receive: optional(text("whole tokens each order asks until and when it opens; default a little above the median recent fill, between 1.25 and 2 times min_receive")),
|
|
496
631
|
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
632
|
pay_to: optional(account(PAY_TO)),
|
|
498
633
|
send: optional(flag("true signs and sends every order; leave out to preview")),
|
|
499
634
|
}),
|
|
500
635
|
output: object({
|
|
501
636
|
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"),
|
|
637
|
+
summary: list(text(), 24, "the whole plan, then the first order"),
|
|
503
638
|
warnings: list(text(), 16),
|
|
504
639
|
start_receive: text("whole tokens each order asks when it opens; pass it back when sending"),
|
|
505
640
|
sent: flag(),
|
|
@@ -520,7 +655,9 @@ const walletRecurring = define({
|
|
|
520
655
|
? openingAsk(await recentFills(ctx.transport, inputMint, outputMint), amountIn, floor)
|
|
521
656
|
: raw(input.start_receive, outputToken.decimals, "start_receive");
|
|
522
657
|
if (ask <= floor) {
|
|
523
|
-
throw new InvalidInput([
|
|
658
|
+
throw new InvalidInput([
|
|
659
|
+
input.start_receive === undefined ? { path: "min_receive", message: NO_FALL } : { path: "start_receive", message: "must be more than min_receive, or the order has no ask to fall" },
|
|
660
|
+
]);
|
|
524
661
|
}
|
|
525
662
|
checkSale(wallet.limits, inputMint, inputToken.decimals, amountIn, now);
|
|
526
663
|
checkHour(wallet.limits, inputMint, inputToken.decimals, amountIn * BigInt(input.times), now);
|
|
@@ -533,10 +670,22 @@ const walletRecurring = define({
|
|
|
533
670
|
expires_at: open + life,
|
|
534
671
|
});
|
|
535
672
|
const shown = { start_receive: formatAmount(ask, outputToken.decimals) };
|
|
673
|
+
const last = opens[opens.length - 1];
|
|
536
674
|
const first = (await offered(ctx, call, { ...input, send: input.send }, terms(opens[0])));
|
|
675
|
+
const sold = TOKEN_NAMES[inputMint] ?? inputMint;
|
|
676
|
+
const plan = [
|
|
677
|
+
`Plan: ${input.times} orders of ${formatAmount(amountIn, inputToken.decimals)} of ${sold}, ${formatAmount(amountIn * BigInt(input.times), inputToken.decimals)} of ${sold} in all, escrowed now`,
|
|
678
|
+
`Schedule: the orders open from ${formatTime(opens[0])} to ${formatTime(last)}; the last expires ${formatTime(last + life)}`,
|
|
679
|
+
...first["summary"],
|
|
680
|
+
];
|
|
681
|
+
const warned = first["warnings"];
|
|
682
|
+
const warnings = [
|
|
683
|
+
...warned,
|
|
684
|
+
WARN_EARLY,
|
|
685
|
+
...(last + life - now > LONG_LIVED_SECONDS && !warned.includes(WARN_LONG_LIVED) ? [WARN_LONG_LIVED] : []),
|
|
686
|
+
];
|
|
537
687
|
if (input.send !== true) {
|
|
538
|
-
|
|
539
|
-
return { orders: opens.map((opens_at) => ({ opens_at })), summary, warnings, ...shown, sent: false, next };
|
|
688
|
+
return { orders: opens.map((opens_at) => ({ opens_at })), summary: plan, warnings, ...shown, sent: false, next: first["next"] };
|
|
540
689
|
}
|
|
541
690
|
const orders = [{ opens_at: opens[0], intent: first["intent"], nonce: first["nonce"], signature: first["signature"] }];
|
|
542
691
|
for (const open of opens.slice(1)) {
|
|
@@ -551,17 +700,17 @@ const walletRecurring = define({
|
|
|
551
700
|
const problem = problemOf(error);
|
|
552
701
|
return {
|
|
553
702
|
orders: [...orders, ...opens.slice(orders.length).map((opens_at) => ({ opens_at }))],
|
|
554
|
-
summary:
|
|
555
|
-
warnings
|
|
703
|
+
summary: plan,
|
|
704
|
+
warnings,
|
|
556
705
|
...shown,
|
|
557
706
|
sent: true,
|
|
558
|
-
next:
|
|
559
|
-
?
|
|
707
|
+
next: refusedBeforeSending(error)
|
|
708
|
+
? partlySent(orders.length, input.times, `${problem.code}: ${problem.hint}`)
|
|
560
709
|
: mayHaveLanded(orders.length, input.times, `${problem.code}: ${problem.hint}`),
|
|
561
710
|
};
|
|
562
711
|
}
|
|
563
712
|
}
|
|
564
|
-
return { orders, summary:
|
|
713
|
+
return { orders, summary: plan, warnings, ...shown, sent: true };
|
|
565
714
|
},
|
|
566
715
|
});
|
|
567
716
|
const walletAuction = define({
|
|
@@ -612,7 +761,7 @@ const walletIntents = define({
|
|
|
612
761
|
const walletCancel = define({
|
|
613
762
|
name: tool("wallet_cancel"),
|
|
614
763
|
title: "Cancel a wallet intent",
|
|
615
|
-
description: "Cancel an open intent of this server's wallet by its nonce: the escrow returns to the wallet and the intent account closes, returning its rent. Without send it only previews; call again with send: true once the user approves.",
|
|
764
|
+
description: "Cancel an open intent of this server's wallet by its nonce: the escrow returns to the wallet's token account (wrapped SOL for SOL) and the intent account closes, returning its rent. Without send it only previews; call again with send: true once the user approves.",
|
|
616
765
|
effect: "submit",
|
|
617
766
|
input: object({ nonce: amount("the intent's nonce, from wallet_intents"), send: optional(flag("true signs and sends; leave out to preview")) }),
|
|
618
767
|
output: object({
|
|
@@ -634,7 +783,11 @@ async function engineHistory(transport, limit, of) {
|
|
|
634
783
|
const created = new Map();
|
|
635
784
|
const fills = [];
|
|
636
785
|
const ended = new Map();
|
|
786
|
+
let unread = 0;
|
|
637
787
|
for (const tx of await transport.engineTransactions(limit, of)) {
|
|
788
|
+
if (tx.unread === true) {
|
|
789
|
+
unread += 1;
|
|
790
|
+
}
|
|
638
791
|
if (tx.failed) {
|
|
639
792
|
continue;
|
|
640
793
|
}
|
|
@@ -660,7 +813,7 @@ async function engineHistory(transport, limit, of) {
|
|
|
660
813
|
}
|
|
661
814
|
}
|
|
662
815
|
}
|
|
663
|
-
return { created, fills, ended };
|
|
816
|
+
return { created, fills, ended, unread };
|
|
664
817
|
}
|
|
665
818
|
function pairOf(data) {
|
|
666
819
|
try {
|
|
@@ -671,7 +824,7 @@ function pairOf(data) {
|
|
|
671
824
|
return null;
|
|
672
825
|
}
|
|
673
826
|
}
|
|
674
|
-
export const ESTIMATE_NEXT = "solvers pay the least an intent asks, so these fills show what owners asked and got, not the most a solver would pay; ask a minimum near
|
|
827
|
+
export const ESTIMATE_NEXT = "solvers pay the least an intent asks, so these fills show what owners asked and got, not the most a solver would pay; ask a minimum near receive_at_median, since a single fill can be staged by one party on both sides, or auction from a little above it down to the least the user accepts to find the price";
|
|
675
828
|
export const NO_FILLS = `no recent fill sold this token for that one; auction from a high ask down to the least the user accepts with ${tool("wallet_auction")} to find the price`;
|
|
676
829
|
const walletEstimate = define({
|
|
677
830
|
name: tool("wallet_estimate"),
|
|
@@ -690,7 +843,8 @@ const walletEstimate = define({
|
|
|
690
843
|
at: optional(time("when the fill landed")),
|
|
691
844
|
signature: text(),
|
|
692
845
|
}), ESTIMATE_FILLS, "newest first"),
|
|
693
|
-
receive_at_best: optional(text("whole tokens amount would get at the best recent rate")),
|
|
846
|
+
receive_at_best: optional(text("whole tokens amount would get at the best recent rate; one fill can be staged by its own two sides")),
|
|
847
|
+
receive_at_median: optional(text("whole tokens amount would get at the median recent rate")),
|
|
694
848
|
receive_at_latest: optional(text("whole tokens amount would get at the newest fill's rate")),
|
|
695
849
|
next: text(),
|
|
696
850
|
}),
|
|
@@ -713,15 +867,16 @@ const walletEstimate = define({
|
|
|
713
867
|
}
|
|
714
868
|
const best = bestOf(matching) ?? latest;
|
|
715
869
|
const at = (f) => formatAmount((amount * f.delivered) / f.amountIn, outputToken.decimals);
|
|
716
|
-
return { fills: shown, receive_at_best: at(best), receive_at_latest: at(latest), next: ESTIMATE_NEXT };
|
|
870
|
+
return { fills: shown, receive_at_best: at(best), receive_at_median: at(medianOf(matching) ?? latest), receive_at_latest: at(latest), next: ESTIMATE_NEXT };
|
|
717
871
|
},
|
|
718
872
|
});
|
|
719
873
|
export const WAIT_STATUS = ["open", "filling", "filled", "cancelled", "expired", "closed"];
|
|
874
|
+
export const UNREAD = `the node could not yet return all of this intent's transactions, so whether it filled is unknown; call ${tool("wallet_wait")} again in a few seconds before acting on it`;
|
|
720
875
|
export const stillWaiting = (status) => `still ${status}; call ${tool("wallet_wait")} again to keep waiting, or ${tool("wallet_cancel")} to take the escrow back`;
|
|
721
876
|
const walletWait = define({
|
|
722
877
|
name: tool("wallet_wait"),
|
|
723
878
|
title: "Wait for a wallet intent",
|
|
724
|
-
description: `Wait until an intent of this server's wallet is filled, cancelled or expired, or until within seconds pass, then say which and, for a fill, how much
|
|
879
|
+
description: `Wait until an intent of this server's wallet is filled, cancelled or expired, or until within seconds pass, then say which and, for a fill, how much it delivered to this wallet or to the pay_to it named. Use it after sending instead of asking the user to check again; call it again while the status is still open.`,
|
|
725
880
|
effect: "read",
|
|
726
881
|
input: object({
|
|
727
882
|
nonce: amount("the intent's nonce, from the send or wallet_intents"),
|
|
@@ -769,7 +924,8 @@ const walletWait = define({
|
|
|
769
924
|
const history = await engineHistory(ctx.transport, WAIT_SCAN, intent);
|
|
770
925
|
const fills = history.fills.filter((f) => f.intent === intent);
|
|
771
926
|
if (fills.length === 0) {
|
|
772
|
-
|
|
927
|
+
const ended = status === "closed" ? (history.ended.get(intent) ?? status) : status;
|
|
928
|
+
return { intent, status: ended, waited, ...(ended === "closed" && history.unread > 0 ? { next: UNREAD } : {}) };
|
|
773
929
|
}
|
|
774
930
|
pair ??= history.created.get(intent) ?? null;
|
|
775
931
|
const decimals = pair === null ? undefined : (await mintOf(ctx.transport, pair.outputMint)).decimals;
|
|
@@ -783,6 +939,74 @@ const walletWait = define({
|
|
|
783
939
|
};
|
|
784
940
|
},
|
|
785
941
|
});
|
|
942
|
+
export const NOT_RELEASED = `no release of this escrow is among its recent transactions; ${tool("wallet_wait")} or ${tool("wallet_intents")} shows whether it is still open`;
|
|
943
|
+
const walletReceipt = define({
|
|
944
|
+
name: tool("wallet_receipt"),
|
|
945
|
+
title: "Check an escrow's release",
|
|
946
|
+
description: `Look up whether an escrow made with ${tool("wallet_escrow")} was released: to whom, for how much, by which attestor, and which delivered work the release recorded. Give sha256, the hash of the work you received, to check it is the work that was paid for. It reads the escrow's own transactions and sends nothing.`,
|
|
947
|
+
effect: "read",
|
|
948
|
+
input: object({
|
|
949
|
+
owner: optional(account("the wallet that made the escrow; default this server's wallet")),
|
|
950
|
+
nonce: amount("the escrow intent's nonce"),
|
|
951
|
+
sha256: optional(text("the sha256, as 64 hex digits, of the work you received, to compare with the one the release recorded")),
|
|
952
|
+
}),
|
|
953
|
+
output: object({
|
|
954
|
+
intent: account(),
|
|
955
|
+
released: flag(),
|
|
956
|
+
paid_to: optional(account("the wallet the release paid")),
|
|
957
|
+
payee: optional(account("the token account the release paid")),
|
|
958
|
+
paid: optional(text("whole tokens the payee received")),
|
|
959
|
+
attestor: optional(account()),
|
|
960
|
+
result: optional(amount()),
|
|
961
|
+
signature: optional(text("the release's transaction")),
|
|
962
|
+
delivered_sha256: optional(text("the sha256 the release recorded, if any")),
|
|
963
|
+
matches: optional(flag("whether sha256 equals the recorded one")),
|
|
964
|
+
next: optional(text()),
|
|
965
|
+
}),
|
|
966
|
+
run: async (ctx, input) => {
|
|
967
|
+
const owner = input.owner ?? (await signerOf(ctx)).address;
|
|
968
|
+
const wanted = input.sha256 === undefined ? undefined : sha256Of(input.sha256, "sha256");
|
|
969
|
+
const [intent] = await findIntentPda({ owner, nonce: input.nonce });
|
|
970
|
+
const transactions = await ctx.transport.engineTransactions(WAIT_SCAN, intent);
|
|
971
|
+
let mint;
|
|
972
|
+
for (const tx of transactions) {
|
|
973
|
+
for (const event of tx.emitted.map(decodeEvent)) {
|
|
974
|
+
const fields = (event?.fields ?? {});
|
|
975
|
+
if (event?.name === "IntentCreated" && fields["intent"] === intent) {
|
|
976
|
+
mint = fields["inputMint"];
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
for (const tx of transactions) {
|
|
981
|
+
if (tx.failed) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const release = tx.emitted.map(decodeEvent).find((event) => event?.name === "IntentAttested" && event.fields["intent"] === intent);
|
|
985
|
+
if (release === undefined || release === null) {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
const fields = release.fields;
|
|
989
|
+
const payee = fields["payee"];
|
|
990
|
+
const held = await ctx.transport.getAccount(payee);
|
|
991
|
+
mint ??= held === null || held.data.length < 64 ? undefined : getAddressDecoder().decode(held.data.subarray(0, 32));
|
|
992
|
+
const decimals = mint === undefined ? undefined : (await mintOf(ctx.transport, mint)).decimals;
|
|
993
|
+
const recorded = (tx.memos ?? []).map((memo) => DELIVERY_MEMO.exec(memo)?.[1]).find((hash) => hash !== undefined);
|
|
994
|
+
return {
|
|
995
|
+
intent,
|
|
996
|
+
released: true,
|
|
997
|
+
...(held === null || held.data.length < 64 ? {} : { paid_to: getAddressDecoder().decode(held.data.subarray(32, 64)) }),
|
|
998
|
+
payee,
|
|
999
|
+
paid: formatAmount(fields["net"], decimals),
|
|
1000
|
+
attestor: fields["attestor"],
|
|
1001
|
+
result: fields["result"],
|
|
1002
|
+
signature: tx.signature,
|
|
1003
|
+
...(recorded === undefined ? {} : { delivered_sha256: recorded }),
|
|
1004
|
+
...(wanted === undefined ? {} : { matches: wanted === recorded }),
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
return { intent, released: false, next: NOT_RELEASED };
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
786
1010
|
export const WALLET_ACTIONS = [
|
|
787
1011
|
walletSetup,
|
|
788
1012
|
walletBalance,
|
|
@@ -791,8 +1015,10 @@ export const WALLET_ACTIONS = [
|
|
|
791
1015
|
walletAuction,
|
|
792
1016
|
walletRecurring,
|
|
793
1017
|
walletEscrow,
|
|
1018
|
+
walletEscrowPlan,
|
|
794
1019
|
walletAttest,
|
|
795
1020
|
walletWait,
|
|
1021
|
+
walletReceipt,
|
|
796
1022
|
walletIntents,
|
|
797
1023
|
walletCancel,
|
|
798
1024
|
];
|