@intent-engine/cli 0.1.22 → 0.1.26
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/admin-commands.d.ts +12 -0
- package/dist/src/admin-commands.js +140 -0
- package/dist/src/admin-commands.js.map +1 -0
- package/dist/src/agent/actions.d.ts +2 -0
- package/dist/src/agent/actions.js +1 -1
- package/dist/src/agent/actions.js.map +1 -1
- package/dist/src/agent/claims.js +13 -12
- package/dist/src/agent/claims.js.map +1 -1
- package/dist/src/agent/create.d.ts +1 -0
- package/dist/src/agent/create.js +18 -4
- package/dist/src/agent/create.js.map +1 -1
- package/dist/src/agent/mandate-agent.d.ts +2 -0
- package/dist/src/agent/mandate-agent.js +136 -0
- package/dist/src/agent/mandate-agent.js.map +1 -0
- package/dist/src/agent/mandate-grant.d.ts +2 -0
- package/dist/src/agent/mandate-grant.js +192 -0
- package/dist/src/agent/mandate-grant.js.map +1 -0
- package/dist/src/agent/mandate-revoke.d.ts +1 -0
- package/dist/src/agent/mandate-revoke.js +142 -0
- package/dist/src/agent/mandate-revoke.js.map +1 -0
- package/dist/src/agent/mandate-state.d.ts +33 -0
- package/dist/src/agent/mandate-state.js +85 -0
- package/dist/src/agent/mandate-state.js.map +1 -0
- package/dist/src/agent/mandate.d.ts +4 -26
- package/dist/src/agent/mandate.js +4 -479
- package/dist/src/agent/mandate.js.map +1 -1
- package/dist/src/agent/mcp.js +10 -5
- package/dist/src/agent/mcp.js.map +1 -1
- package/dist/src/agent/prepare.js +1 -1
- package/dist/src/agent/prepare.js.map +1 -1
- package/dist/src/agent/problems.d.ts +15 -1
- package/dist/src/agent/problems.js +38 -2
- package/dist/src/agent/problems.js.map +1 -1
- package/dist/src/agent/read.d.ts +13 -0
- package/dist/src/agent/read.js +56 -7
- package/dist/src/agent/read.js.map +1 -1
- package/dist/src/agent/registry.js +8 -2
- package/dist/src/agent/registry.js.map +1 -1
- package/dist/src/agent/relay.d.ts +2 -0
- package/dist/src/agent/relay.js +11 -3
- package/dist/src/agent/relay.js.map +1 -1
- package/dist/src/agent/sales.d.ts +29 -0
- package/dist/src/agent/sales.js +248 -0
- package/dist/src/agent/sales.js.map +1 -0
- package/dist/src/agent/signed-prepare.d.ts +1 -0
- package/dist/src/agent/signed-prepare.js +141 -0
- package/dist/src/agent/signed-prepare.js.map +1 -0
- package/dist/src/agent/signed-relay.d.ts +25 -0
- package/dist/src/agent/signed-relay.js +68 -0
- package/dist/src/agent/signed-relay.js.map +1 -0
- package/dist/src/agent/signed-submit.d.ts +2 -0
- package/dist/src/agent/signed-submit.js +137 -0
- package/dist/src/agent/signed-submit.js.map +1 -0
- package/dist/src/agent/signed-terms.d.ts +85 -0
- package/dist/src/agent/signed-terms.js +84 -0
- package/dist/src/agent/signed-terms.js.map +1 -0
- package/dist/src/agent/signed.d.ts +4 -13
- package/dist/src/agent/signed.js +4 -361
- package/dist/src/agent/signed.js.map +1 -1
- package/dist/src/agent/wallet/account.d.ts +4 -0
- package/dist/src/agent/wallet/account.js +84 -0
- package/dist/src/agent/wallet/account.js.map +1 -0
- package/dist/src/agent/wallet/attest.d.ts +4 -0
- package/dist/src/agent/wallet/attest.js +178 -0
- package/dist/src/agent/wallet/attest.js.map +1 -0
- package/dist/src/agent/wallet/delivery.d.ts +13 -0
- package/dist/src/agent/wallet/delivery.js +171 -0
- package/dist/src/agent/wallet/delivery.js.map +1 -0
- package/dist/src/agent/wallet/escrow.d.ts +7 -0
- package/dist/src/agent/wallet/escrow.js +164 -0
- package/dist/src/agent/wallet/escrow.js.map +1 -0
- package/dist/src/agent/wallet/fills.d.ts +27 -0
- package/dist/src/agent/wallet/fills.js +80 -0
- package/dist/src/agent/wallet/fills.js.map +1 -0
- package/dist/src/agent/wallet/guard.d.ts +57 -0
- package/dist/src/agent/wallet/guard.js +205 -0
- package/dist/src/agent/wallet/guard.js.map +1 -0
- package/dist/src/agent/wallet/offer.d.ts +50 -0
- package/dist/src/agent/wallet/offer.js +80 -0
- package/dist/src/agent/wallet/offer.js.map +1 -0
- package/dist/src/agent/wallet/receipt.d.ts +42 -0
- package/dist/src/agent/wallet/receipt.js +102 -0
- package/dist/src/agent/wallet/receipt.js.map +1 -0
- package/dist/src/agent/wallet/record.d.ts +31 -0
- package/dist/src/agent/wallet/record.js +178 -0
- package/dist/src/agent/wallet/record.js.map +1 -0
- package/dist/src/agent/wallet/releases.d.ts +40 -0
- package/dist/src/agent/wallet/releases.js +236 -0
- package/dist/src/agent/wallet/releases.js.map +1 -0
- package/dist/src/agent/wallet/split.d.ts +10 -0
- package/dist/src/agent/wallet/split.js +271 -0
- package/dist/src/agent/wallet/split.js.map +1 -0
- package/dist/src/agent/wallet/trade.d.ts +14 -0
- package/dist/src/agent/wallet/trade.js +243 -0
- package/dist/src/agent/wallet/trade.js.map +1 -0
- package/dist/src/agent/wallet/wait.d.ts +5 -0
- package/dist/src/agent/wallet/wait.js +99 -0
- package/dist/src/agent/wallet/wait.js.map +1 -0
- package/dist/src/agent/wallet.d.ts +18 -72
- package/dist/src/agent/wallet.js +26 -1216
- package/dist/src/agent/wallet.js.map +1 -1
- package/dist/src/arg-options.d.ts +333 -0
- package/dist/src/arg-options.js +310 -0
- package/dist/src/arg-options.js.map +1 -0
- package/dist/src/arg-usage.d.ts +7 -0
- package/dist/src/arg-usage.js +53 -0
- package/dist/src/arg-usage.js.map +1 -0
- package/dist/src/arg-values.d.ts +47 -0
- package/dist/src/arg-values.js +280 -0
- package/dist/src/arg-values.js.map +1 -0
- package/dist/src/args.d.ts +7 -487
- package/dist/src/args.js +89 -880
- package/dist/src/args.js.map +1 -1
- package/dist/src/artifact.d.ts +22 -0
- package/dist/src/artifact.js +89 -0
- package/dist/src/artifact.js.map +1 -0
- package/dist/src/check-process.d.ts +39 -0
- package/dist/src/check-process.js +338 -0
- package/dist/src/check-process.js.map +1 -0
- package/dist/src/command-builders.d.ts +37 -0
- package/dist/src/command-builders.js +216 -0
- package/dist/src/command-builders.js.map +1 -0
- package/dist/src/command-types.d.ts +227 -0
- package/dist/src/command-types.js +2 -0
- package/dist/src/command-types.js.map +1 -0
- package/dist/src/commands.d.ts +7 -43
- package/dist/src/commands.js +67 -426
- package/dist/src/commands.js.map +1 -1
- package/dist/src/event-feed.d.ts +13 -0
- package/dist/src/event-feed.js +46 -0
- package/dist/src/event-feed.js.map +1 -0
- package/dist/src/flow-commands.d.ts +8 -0
- package/dist/src/flow-commands.js +116 -0
- package/dist/src/flow-commands.js.map +1 -0
- package/dist/src/flows.d.ts +6 -0
- package/dist/src/flows.js +84 -10
- package/dist/src/flows.js.map +1 -1
- package/dist/src/job-commands.d.ts +9 -0
- package/dist/src/job-commands.js +89 -0
- package/dist/src/job-commands.js.map +1 -0
- package/dist/src/judge-errors.d.ts +12 -0
- package/dist/src/judge-errors.js +16 -0
- package/dist/src/judge-errors.js.map +1 -0
- package/dist/src/judge.d.ts +28 -0
- package/dist/src/judge.js +169 -0
- package/dist/src/judge.js.map +1 -0
- package/dist/src/main.d.ts +4 -1
- package/dist/src/main.js +26 -10
- package/dist/src/main.js.map +1 -1
- package/dist/src/order-builders.d.ts +10 -0
- package/dist/src/order-builders.js +172 -0
- package/dist/src/order-builders.js.map +1 -0
- package/dist/src/render.d.ts +1 -0
- package/dist/src/render.js +9 -0
- package/dist/src/render.js.map +1 -1
- package/dist/src/runtime.d.ts +22 -0
- package/dist/src/runtime.js +2 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/solve-commands.d.ts +25 -0
- package/dist/src/solve-commands.js +171 -0
- package/dist/src/solve-commands.js.map +1 -0
- package/dist/src/tokens.js +2 -1
- package/dist/src/tokens.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/wallet.d.ts +4 -1
- package/dist/src/wallet.js +25 -5
- package/dist/src/wallet.js.map +1 -1
- package/package.json +16 -7
- package/dist/src/events.d.ts +0 -8
- package/dist/src/events.js +0 -43
- package/dist/src/events.js.map +0 -1
- package/dist/src/transport.d.ts +0 -208
- package/dist/src/transport.js +0 -441
- package/dist/src/transport.js.map +0 -1
package/dist/src/agent/wallet.js
CHANGED
|
@@ -1,1219 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export
|
|
22
|
-
export
|
|
23
|
-
export
|
|
24
|
-
export const ESTIMATE_FILLS = 5;
|
|
25
|
-
export const WAIT_DEFAULT = 30;
|
|
26
|
-
export const WAIT_MAX = 40;
|
|
27
|
-
export const WAIT_SCAN = 10;
|
|
28
|
-
export const DEFAULT_EXPIRES_IN = 3_600;
|
|
29
|
-
export const LANDING_SECONDS = 10;
|
|
30
|
-
export const MAINNET_GENESIS = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
|
|
31
|
-
export const WALLET_NOTICE = [
|
|
32
|
-
"Test network only: this wallet works on Solana devnet, testnet or localnet and refuses mainnet. Its tokens have no value.",
|
|
33
|
-
"Your key stays on this machine: setup makes one unless a keypair is already configured, and names its file. Only signed transactions leave it.",
|
|
34
|
-
"This server talks to no one but the RPC endpoint it was started with.",
|
|
35
|
-
"Nothing moves until you approve: swaps, auctions, escrows, releases and cancels only preview until you say yes.",
|
|
36
|
-
"Do not send real funds to this wallet or use its key anywhere else.",
|
|
37
|
-
];
|
|
38
|
-
export const walletEmpty = (address) => `the wallet ${address} holds no SOL yet; request devnet SOL for it at ${FAUCET} or ${PUBLIC_FAUCET}, then try again`;
|
|
39
|
-
export const NO_WALLET = `this server has no wallet yet; call ${tool("wallet_setup")} first`;
|
|
40
|
-
export const WALLET_OFF = "this server runs without a wallet; start it with mcp --wallet to let it sign";
|
|
41
|
-
export const PREVIEW_ONLY = "nothing was sent; show the summary to the user and call again with send: true only after they approve";
|
|
42
|
-
function access(ctx) {
|
|
43
|
-
if (ctx.wallet === undefined) {
|
|
44
|
-
throw new AgentRefusal("WalletNotConfigured", WALLET_OFF);
|
|
45
|
-
}
|
|
46
|
-
return ctx.wallet;
|
|
47
|
-
}
|
|
48
|
-
const genesisOf = new WeakMap();
|
|
49
|
-
async function testCluster(ctx) {
|
|
50
|
-
const wallet = access(ctx);
|
|
51
|
-
let genesis = genesisOf.get(wallet);
|
|
52
|
-
if (genesis === undefined) {
|
|
53
|
-
const read = ctx.transport.genesisHash();
|
|
54
|
-
genesis = read;
|
|
55
|
-
genesisOf.set(wallet, read);
|
|
56
|
-
read.catch(() => {
|
|
57
|
-
if (genesisOf.get(wallet) === read) {
|
|
58
|
-
genesisOf.delete(wallet);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
if ((await genesis) === MAINNET_GENESIS) {
|
|
63
|
-
throw new AgentRefusal("WalletNotMainnet", WALLET_NOT_MAINNET);
|
|
64
|
-
}
|
|
65
|
-
return wallet;
|
|
66
|
-
}
|
|
67
|
-
async function savedKeypair(wallet) {
|
|
68
|
-
return (await withSettings(wallet.files, wallet.env))[ENV_KEYPAIR];
|
|
69
|
-
}
|
|
70
|
-
async function signerOf(ctx) {
|
|
71
|
-
const wallet = await testCluster(ctx);
|
|
72
|
-
const path = await savedKeypair(wallet);
|
|
73
|
-
if (path === undefined) {
|
|
74
|
-
throw new AgentRefusal("NoWallet", NO_WALLET);
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
return await wallet.files.loadSigner(path);
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
throw new AgentRefusal("NoWallet", NO_WALLET);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function mintNamed(text, url, path) {
|
|
84
|
-
const mint = tokenNamed(text.trim(), url);
|
|
85
|
-
if (mint instanceof Error) {
|
|
86
|
-
throw new InvalidInput([{ path, message: mint.message }]);
|
|
87
|
-
}
|
|
88
|
-
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(mint)) {
|
|
89
|
-
throw new InvalidInput([{ path, message: "name SOL, TEST or a token mint address" }]);
|
|
90
|
-
}
|
|
91
|
-
return mint;
|
|
92
|
-
}
|
|
93
|
-
function raw(tokens, decimals, path) {
|
|
94
|
-
try {
|
|
95
|
-
return parseAmount(tokens, decimals);
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
throw new InvalidInput([{ path, message: error instanceof Error ? error.message : String(error) }]);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const nameOf = (mint) => TOKEN_NAMES[mint] ?? mint;
|
|
102
|
-
const limited = (guard) => guard.limits.perTrade.size + guard.limits.perHour.size > 0;
|
|
103
|
-
export function limitLines(limits) {
|
|
104
|
-
const mints = [...new Set([...limits.perTrade.keys(), ...limits.perHour.keys()])];
|
|
105
|
-
if (mints.length === 0) {
|
|
106
|
-
return [];
|
|
107
|
-
}
|
|
108
|
-
return [
|
|
109
|
-
"Whoever started this server set sell limits that no tool call can change:",
|
|
110
|
-
...mints.map((mint) => {
|
|
111
|
-
const caps = [
|
|
112
|
-
...(limits.perTrade.has(mint) ? [`${limits.perTrade.get(mint)} per trade`] : []),
|
|
113
|
-
...(limits.perHour.has(mint) ? [`${limits.perHour.get(mint)} per hour`] : []),
|
|
114
|
-
];
|
|
115
|
-
return `- ${nameOf(mint)}: at most ${caps.join(" and ")}`;
|
|
116
|
-
}),
|
|
117
|
-
"It sells no other token. Plan trades within these limits and tell the user when a request exceeds them.",
|
|
118
|
-
];
|
|
119
|
-
}
|
|
120
|
-
function capOf(text, decimals, mint) {
|
|
121
|
-
try {
|
|
122
|
-
return parseAmount(text, decimals);
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
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`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
function checkSale(guard, mint, decimals, amount, now) {
|
|
129
|
-
if (!limited(guard)) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
const { perTrade, perHour } = guard.limits;
|
|
133
|
-
const name = nameOf(mint);
|
|
134
|
-
if (!perTrade.has(mint) && !perHour.has(mint)) {
|
|
135
|
-
const allowed = [...new Set([...perTrade.keys(), ...perHour.keys()])].map(nameOf).join(", ");
|
|
136
|
-
throw new AgentRefusal("WalletTokenNotAllowed", `this server sells only ${allowed}, not ${name}; its limits were set when it started`);
|
|
137
|
-
}
|
|
138
|
-
const trade = perTrade.get(mint);
|
|
139
|
-
if (trade !== undefined && amount > capOf(trade, decimals, mint)) {
|
|
140
|
-
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`);
|
|
141
|
-
}
|
|
142
|
-
checkHour(guard, mint, decimals, amount, now);
|
|
143
|
-
}
|
|
144
|
-
function checkHour(guard, mint, decimals, amount, now) {
|
|
145
|
-
const hour = guard.limits.perHour.get(mint);
|
|
146
|
-
const name = nameOf(mint);
|
|
147
|
-
if (hour === undefined) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
guard.sales = guard.sales.filter((s) => s.at > now - SALE_WINDOW);
|
|
151
|
-
const used = guard.sales.filter((s) => s.mint === mint).reduce((sum, s) => sum + s.amount, 0n);
|
|
152
|
-
const cap = capOf(hour, decimals, mint);
|
|
153
|
-
if (used + amount > cap) {
|
|
154
|
-
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)}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
async function sent(ctx, signer, view, send, call) {
|
|
158
|
-
for (const undo of call.undo.splice(0)) {
|
|
159
|
-
undo();
|
|
160
|
-
}
|
|
161
|
-
const { prepared, ...shown } = view;
|
|
162
|
-
if (send !== true) {
|
|
163
|
-
return { ...shown, sent: false, next: PREVIEW_ONLY };
|
|
164
|
-
}
|
|
165
|
-
if ((await ctx.transport.genesisHash()) === MAINNET_GENESIS) {
|
|
166
|
-
throw new AgentRefusal("WalletNotMainnet", WALLET_NOT_MAINNET);
|
|
167
|
-
}
|
|
168
|
-
return { ...shown, sent: true, signature: await execute(ctx.transport, prepared, signer) };
|
|
169
|
-
}
|
|
170
|
-
const tokenRow = object({
|
|
171
|
-
mint: account(),
|
|
172
|
-
name: optional(text()),
|
|
173
|
-
account: account("the wallet's token account for this mint"),
|
|
174
|
-
balance: text("in whole tokens"),
|
|
175
|
-
});
|
|
176
|
-
const walletSetup = define({
|
|
177
|
-
name: tool("wallet_setup"),
|
|
178
|
-
title: "Set up the wallet",
|
|
179
|
-
description: "Create this server's devnet wallet if it has none, save it, and fund it from the devnet airdrop when it holds less than 0.5 SOL. Safe to call again: it reuses the saved wallet. Call this first.",
|
|
180
|
-
effect: "submit",
|
|
181
|
-
input: object({}),
|
|
182
|
-
output: object({
|
|
183
|
-
address: account(),
|
|
184
|
-
keypair: text("the file holding the wallet's key on this machine"),
|
|
185
|
-
created: flag("true when this call made the wallet"),
|
|
186
|
-
url: text(),
|
|
187
|
-
sol: text("SOL the wallet holds"),
|
|
188
|
-
next: text("what to tell the user"),
|
|
189
|
-
notice: list(text(), 8, "the notice from the server instructions; show it if the user has not seen it yet"),
|
|
190
|
-
}),
|
|
191
|
-
run: async (ctx) => {
|
|
192
|
-
const wallet = await testCluster(ctx);
|
|
193
|
-
return { ...(await setup(ctx.transport, wallet.files, wallet.url, await savedKeypair(wallet))), notice: WALLET_NOTICE };
|
|
194
|
-
},
|
|
195
|
-
});
|
|
196
|
-
export const walletBalance = define({
|
|
197
|
-
name: tool("wallet_balance"),
|
|
198
|
-
title: "Show the wallet's balances",
|
|
199
|
-
description: "Show the SOL and token balances of this server's wallet, in whole tokens. Without mints it shows wrapped SOL and, on devnet, the TEST token.",
|
|
200
|
-
effect: "read",
|
|
201
|
-
input: object({ mints: optional(list(text("SOL, TEST or a mint address"), 8)) }),
|
|
202
|
-
output: object({ address: account(), sol: text(), tokens: list(tokenRow, 8) }),
|
|
203
|
-
run: async (ctx, input) => {
|
|
204
|
-
const wallet = access(ctx);
|
|
205
|
-
const owner = (await signerOf(ctx)).address;
|
|
206
|
-
const mints = input.mints?.map((m, i) => mintNamed(m, wallet.url, `mints.${i}`)) ?? defaultMints(wallet.url);
|
|
207
|
-
return balances(ctx.transport, owner, mints);
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
const offerFields = {
|
|
211
|
-
intent: account(),
|
|
212
|
-
nonce: amount(),
|
|
213
|
-
summary: list(text(), 24),
|
|
214
|
-
warnings: list(text(), 16),
|
|
215
|
-
sent: flag(),
|
|
216
|
-
signature: optional(text()),
|
|
217
|
-
next: optional(text()),
|
|
218
|
-
};
|
|
219
|
-
const offerOutput = object(offerFields);
|
|
220
|
-
async function offered(ctx, call, input, terms) {
|
|
221
|
-
const wallet = access(ctx);
|
|
222
|
-
const signer = await signerOf(ctx);
|
|
223
|
-
const inputMint = mintNamed(input.sell, wallet.url, "sell");
|
|
224
|
-
const outputMint = mintNamed(input.buy, wallet.url, "buy");
|
|
225
|
-
await ctx.transport.warm?.([signer.address, inputMint, outputMint, CLOCK_SYSVAR]);
|
|
226
|
-
if (((await ctx.transport.getAccount(signer.address))?.lamports ?? 0n) === 0n) {
|
|
227
|
-
throw new AgentRefusal("WalletEmpty", walletEmpty(signer.address));
|
|
228
|
-
}
|
|
229
|
-
const [inputToken, outputToken, now] = await Promise.all([
|
|
230
|
-
mintOf(ctx.transport, inputMint),
|
|
231
|
-
mintOf(ctx.transport, outputMint),
|
|
232
|
-
chainTime(ctx.transport),
|
|
233
|
-
]);
|
|
234
|
-
const made = await terms(inputToken, outputToken, now, inputMint, outputMint);
|
|
235
|
-
checkSale(wallet.limits, inputMint, inputToken.decimals, made.order.amount_in, now);
|
|
236
|
-
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));
|
|
237
|
-
const { intent, nonce, summary, warnings, prepared } = view;
|
|
238
|
-
const sold = made.order.amount_in;
|
|
239
|
-
const guard = wallet.limits;
|
|
240
|
-
checkSale(guard, inputMint, inputToken.decimals, sold, now);
|
|
241
|
-
if (input.send !== true || !limited(guard)) {
|
|
242
|
-
return sent(ctx, signer, { intent, nonce, summary, warnings, prepared }, input.send, call);
|
|
243
|
-
}
|
|
244
|
-
const sale = { mint: inputMint, at: now, amount: sold };
|
|
245
|
-
guard.sales.push(sale);
|
|
246
|
-
try {
|
|
247
|
-
return await sent(ctx, signer, { intent, nonce, summary, warnings, prepared }, input.send, call);
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
if (neverLanded(error)) {
|
|
251
|
-
guard.sales = guard.sales.filter((s) => s !== sale);
|
|
252
|
-
}
|
|
253
|
-
throw error;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
export const SWAP_FALLS_OVER = 120;
|
|
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";
|
|
258
|
-
export const SWAP_HEADROOM_BPS = 200n;
|
|
259
|
-
export const SWAP_BLIND_BPS = 2500n;
|
|
260
|
-
export const SWAP_CAP_BPS = 20000n;
|
|
261
|
-
async function recentFills(transport, inputMint, outputMint) {
|
|
262
|
-
const { created, fills } = await engineHistory(transport, ESTIMATE_SCAN);
|
|
263
|
-
const unknown = [...new Set(fills.map((f) => f.intent).filter((intent) => !created.has(intent)))];
|
|
264
|
-
const views = unknown.length === 0 ? [] : await transport.getAccounts(unknown);
|
|
265
|
-
unknown.forEach((intent, i) => {
|
|
266
|
-
const view = views[i];
|
|
267
|
-
const pair = view === null || view === undefined ? null : pairOf(view.data);
|
|
268
|
-
if (pair !== null) {
|
|
269
|
-
created.set(intent, pair);
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
return fills
|
|
273
|
-
.filter((f) => f.amountIn > 0n && created.get(f.intent)?.inputMint === inputMint && created.get(f.intent)?.outputMint === outputMint)
|
|
274
|
-
.slice(0, ESTIMATE_FILLS);
|
|
275
|
-
}
|
|
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)];
|
|
279
|
-
function openingAsk(fills, amount, floor) {
|
|
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;
|
|
286
|
-
}
|
|
287
|
-
const walletSwap = define({
|
|
288
|
-
name: tool("wallet_swap"),
|
|
289
|
-
title: "Swap from the wallet",
|
|
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.`,
|
|
291
|
-
effect: "submit",
|
|
292
|
-
repeats: true,
|
|
293
|
-
input: object({
|
|
294
|
-
sell: text("SOL, TEST or a mint address"),
|
|
295
|
-
buy: text("SOL, TEST or a mint address"),
|
|
296
|
-
amount: text("whole tokens to sell, such as 0.05"),
|
|
297
|
-
min_receive: text("least whole tokens to receive for all of it"),
|
|
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")),
|
|
299
|
-
falls_over: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `seconds the ask takes to fall to min_receive; default ${SWAP_FALLS_OVER}`)),
|
|
300
|
-
exact: optional(flag("true asks min_receive from the start, with no falling ask")),
|
|
301
|
-
pay_to: optional(account(PAY_TO)),
|
|
302
|
-
expires_in: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `least seconds the offer stays open once sent; default ${DEFAULT_EXPIRES_IN}`)),
|
|
303
|
-
send: optional(flag("true signs and sends; leave out to preview")),
|
|
304
|
-
}),
|
|
305
|
-
output: object({ ...offerFields, start_receive: optional(text("whole tokens asked when the offer lands; pass it back when sending")) }),
|
|
306
|
-
run: async (ctx, input, call) => {
|
|
307
|
-
let opening;
|
|
308
|
-
const result = await offered(ctx, call, input, async (sold, bought, now, inputMint, outputMint) => {
|
|
309
|
-
const amountIn = raw(input.amount, sold.decimals, "amount");
|
|
310
|
-
const floor = raw(input.min_receive, bought.decimals, "min_receive");
|
|
311
|
-
const expiresIn = BigInt(input.expires_in ?? DEFAULT_EXPIRES_IN);
|
|
312
|
-
const ask = input.exact === true
|
|
313
|
-
? floor
|
|
314
|
-
: input.start_receive === undefined
|
|
315
|
-
? openingAsk(await recentFills(ctx.transport, inputMint, outputMint), amountIn, floor)
|
|
316
|
-
: raw(input.start_receive, bought.decimals, "start_receive");
|
|
317
|
-
if (ask <= floor) {
|
|
318
|
-
return { order: { kind: "swap", amount_in: amountIn, min_out: floor }, expires_at: now + expiresIn + BigInt(LANDING_SECONDS) };
|
|
319
|
-
}
|
|
320
|
-
opening = formatAmount(ask, bought.decimals);
|
|
321
|
-
const start = now + BigInt(LANDING_SECONDS);
|
|
322
|
-
const falls = BigInt(input.falls_over ?? SWAP_FALLS_OVER);
|
|
323
|
-
return {
|
|
324
|
-
order: { kind: "dutch", amount_in: amountIn, start_out: ask, end_out: floor, start_time: start, end_time: start + falls },
|
|
325
|
-
expires_at: start + (expiresIn > falls ? expiresIn : falls),
|
|
326
|
-
};
|
|
327
|
-
});
|
|
328
|
-
return opening === undefined ? result : { ...result, start_receive: opening };
|
|
329
|
-
},
|
|
330
|
-
});
|
|
331
|
-
export const ESCROW_EXPIRES_IN = 86_400;
|
|
332
|
-
export const RECORD_MINT_NEEDED = "selling SOL on this RPC needs record_mint: any other token's mint, which only labels the escrow";
|
|
333
|
-
function recordedMint(sell, url, chosen) {
|
|
334
|
-
if (chosen !== undefined) {
|
|
335
|
-
return mintNamed(chosen, url, "record_mint");
|
|
336
|
-
}
|
|
337
|
-
if (sell !== NATIVE_MINT) {
|
|
338
|
-
return NATIVE_MINT;
|
|
339
|
-
}
|
|
340
|
-
const test = tokenNamed("TEST", url);
|
|
341
|
-
if (test instanceof Error) {
|
|
342
|
-
throw new InvalidInput([{ path: "record_mint", message: RECORD_MINT_NEEDED }]);
|
|
343
|
-
}
|
|
344
|
-
return test;
|
|
345
|
-
}
|
|
346
|
-
const walletEscrow = define({
|
|
347
|
-
name: tool("wallet_escrow"),
|
|
348
|
-
title: "Escrow a payment for an attestor to release",
|
|
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.`,
|
|
350
|
-
effect: "submit",
|
|
351
|
-
repeats: true,
|
|
352
|
-
input: object({
|
|
353
|
-
sell: text("SOL, TEST or a mint address: the token to pay with"),
|
|
354
|
-
amount: text("whole tokens to escrow, such as 0.05"),
|
|
355
|
-
attestor: account("the wallet whose signature alone releases the escrow"),
|
|
356
|
-
min_result: optional(amount("least result the attestor may attest; default 1")),
|
|
357
|
-
max_result: optional(amount("greatest result the attestor may attest; default min_result")),
|
|
358
|
-
spec_hash: optional(hex32("64 hex: the sha256 of the acceptance spec the attestor must verify. Setting it binds the release to a signature over that exact spec, so the attestor cannot sign a generic result")),
|
|
359
|
-
judge_fee: optional(text("whole tokens of sell paid to the attestor out of the escrow when it settles, such as 0.001; needs spec_hash and must be below amount")),
|
|
360
|
-
expires_in: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `least seconds the escrow stays open once sent; default ${ESCROW_EXPIRES_IN}`)),
|
|
361
|
-
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")),
|
|
362
|
-
send: optional(flag("true signs and sends; leave out to preview")),
|
|
363
|
-
}),
|
|
364
|
-
output: offerOutput,
|
|
365
|
-
run: async (ctx, input, call) => {
|
|
366
|
-
const wallet = access(ctx);
|
|
367
|
-
const sold = mintNamed(input.sell, wallet.url, "sell");
|
|
368
|
-
const least = input.min_result ?? 1n;
|
|
369
|
-
const most = input.max_result ?? least;
|
|
370
|
-
if (most < least) {
|
|
371
|
-
throw new InvalidInput([{ path: "max_result", message: "must be at least min_result" }]);
|
|
372
|
-
}
|
|
373
|
-
const specHash = input.spec_hash;
|
|
374
|
-
if (input.judge_fee !== undefined && specHash === undefined) {
|
|
375
|
-
throw new InvalidInput([{ path: "judge_fee", message: "needs spec_hash" }]);
|
|
376
|
-
}
|
|
377
|
-
return offered(ctx, call, { sell: sold, buy: recordedMint(sold, wallet.url, input.record_mint), send: input.send }, (token, _, now) => ({
|
|
378
|
-
order: {
|
|
379
|
-
kind: "attested",
|
|
380
|
-
amount_in: raw(input.amount, token.decimals, "amount"),
|
|
381
|
-
min_result: least,
|
|
382
|
-
max_result: most,
|
|
383
|
-
attestor: input.attestor,
|
|
384
|
-
...(specHash === undefined ? {} : { spec_hash: specHash }),
|
|
385
|
-
...(input.judge_fee === undefined ? {} : { judge_fee: raw(input.judge_fee, token.decimals, "judge_fee") }),
|
|
386
|
-
},
|
|
387
|
-
expires_at: now + BigInt((input.expires_in ?? ESCROW_EXPIRES_IN) + LANDING_SECONDS),
|
|
388
|
-
}));
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
export const MAX_PIECES = 12;
|
|
392
|
-
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`;
|
|
393
|
-
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`;
|
|
394
|
-
const walletEscrowPlan = define({
|
|
395
|
-
name: tool("wallet_escrow_plan"),
|
|
396
|
-
title: "Escrow a task's pieces in one plan",
|
|
397
|
-
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.`,
|
|
398
|
-
effect: "submit",
|
|
399
|
-
repeats: true,
|
|
400
|
-
input: object({
|
|
401
|
-
sell: text("SOL, TEST or a mint address: the token to pay with"),
|
|
402
|
-
attestor: account("the wallet that releases every piece that names no attestor of its own"),
|
|
403
|
-
pieces: list(object({
|
|
404
|
-
name: text("a label for the piece, shown in the plan only"),
|
|
405
|
-
amount: text("whole tokens this piece pays, such as 0.01"),
|
|
406
|
-
attestor: optional(account("this piece's attestor, instead of the plan's")),
|
|
407
|
-
}), MAX_PIECES, "the pieces, in order", 2),
|
|
408
|
-
expires_in: optional(integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, `least seconds each escrow stays open once sent; default ${ESCROW_EXPIRES_IN}`)),
|
|
409
|
-
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")),
|
|
410
|
-
send: optional(flag("true signs and sends every piece; leave out to preview")),
|
|
411
|
-
}),
|
|
412
|
-
output: object({
|
|
413
|
-
pieces: list(object({ name: text(), amount: text(), attestor: account(), intent: optional(account()), nonce: optional(amount()), signature: optional(text()) }), MAX_PIECES),
|
|
414
|
-
total: text("whole tokens escrowed across all pieces"),
|
|
415
|
-
summary: list(text(), 24, "the plan, then the first piece"),
|
|
416
|
-
warnings: list(text(), 16),
|
|
417
|
-
sent: flag(),
|
|
418
|
-
next: optional(text()),
|
|
419
|
-
}),
|
|
420
|
-
run: async (ctx, input, call) => {
|
|
421
|
-
const wallet = access(ctx);
|
|
422
|
-
const sold = mintNamed(input.sell, wallet.url, "sell");
|
|
423
|
-
const buy = recordedMint(sold, wallet.url, input.record_mint);
|
|
424
|
-
const [token, now] = await Promise.all([mintOf(ctx.transport, sold), chainTime(ctx.transport)]);
|
|
425
|
-
const planned = input.pieces.map((piece, i) => ({
|
|
426
|
-
name: piece.name,
|
|
427
|
-
amountIn: raw(piece.amount, token.decimals, `pieces.${i}.amount`),
|
|
428
|
-
attestor: piece.attestor ?? input.attestor,
|
|
429
|
-
}));
|
|
430
|
-
const total = planned.reduce((sum, piece) => sum + piece.amountIn, 0n);
|
|
431
|
-
for (const piece of planned) {
|
|
432
|
-
checkSale(wallet.limits, sold, token.decimals, piece.amountIn, now);
|
|
433
|
-
}
|
|
434
|
-
checkHour(wallet.limits, sold, token.decimals, total, now);
|
|
435
|
-
const terms = (piece) => (_sold, _bought, at) => ({
|
|
436
|
-
order: { kind: "attested", amount_in: piece.amountIn, min_result: 1n, max_result: 1n, attestor: piece.attestor },
|
|
437
|
-
expires_at: at + BigInt((input.expires_in ?? ESCROW_EXPIRES_IN) + LANDING_SECONDS),
|
|
438
|
-
});
|
|
439
|
-
const name = TOKEN_NAMES[sold] ?? sold;
|
|
440
|
-
const listed = (piece) => ({ name: piece.name, amount: formatAmount(piece.amountIn, token.decimals), attestor: piece.attestor });
|
|
441
|
-
const [head, ...rest] = planned;
|
|
442
|
-
const first = (await offered(ctx, call, { sell: sold, buy, send: input.send }, terms(head)));
|
|
443
|
-
const shown = {
|
|
444
|
-
total: formatAmount(total, token.decimals),
|
|
445
|
-
summary: [
|
|
446
|
-
`Plan: ${planned.length} pieces, ${formatAmount(total, token.decimals)} of ${name} in all, each escrowed now and released on its own`,
|
|
447
|
-
...planned.map((piece, i) => `Piece ${i + 1}: ${piece.name}, ${formatAmount(piece.amountIn, token.decimals)} of ${name}, released by ${piece.attestor}`),
|
|
448
|
-
...first["summary"],
|
|
449
|
-
],
|
|
450
|
-
warnings: first["warnings"],
|
|
451
|
-
};
|
|
452
|
-
if (input.send !== true) {
|
|
453
|
-
return { pieces: planned.map(listed), ...shown, sent: false, next: first["next"] };
|
|
454
|
-
}
|
|
455
|
-
const pieces = [{ ...listed(head), intent: first["intent"], nonce: first["nonce"], signature: first["signature"] }];
|
|
456
|
-
for (const piece of rest) {
|
|
457
|
-
try {
|
|
458
|
-
const made = (await offered(ctx, call, { sell: sold, buy, send: true }, terms(piece)));
|
|
459
|
-
pieces.push({ ...listed(piece), intent: made["intent"], nonce: made["nonce"], signature: made["signature"] });
|
|
460
|
-
}
|
|
461
|
-
catch (error) {
|
|
462
|
-
for (const undo of call.undo.splice(0)) {
|
|
463
|
-
undo();
|
|
464
|
-
}
|
|
465
|
-
const problem = problemOf(error);
|
|
466
|
-
const reason = `${problem.code}: ${problem.hint}`;
|
|
467
|
-
return {
|
|
468
|
-
pieces: [...pieces, ...planned.slice(pieces.length).map(listed)],
|
|
469
|
-
...shown,
|
|
470
|
-
sent: true,
|
|
471
|
-
next: refusedBeforeSending(error) ? piecesPartlySent(pieces.length, planned.length, reason) : piecesMayHaveLanded(pieces.length, planned.length, reason),
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return { pieces, ...shown, sent: true };
|
|
476
|
-
},
|
|
477
|
-
});
|
|
478
|
-
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";
|
|
479
|
-
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";
|
|
480
|
-
export const approvalOf = (message, memo = "") => createHash("sha256").update(message).update(memo).digest("hex").slice(0, 16);
|
|
481
|
-
export const DELIVERY_PREFIX = "delivery sha256:";
|
|
482
|
-
const DELIVERY_MEMO = /^delivery sha256:([0-9a-f]{64})$/;
|
|
483
|
-
function sha256Of(text, path) {
|
|
484
|
-
const hex = text.trim().toLowerCase();
|
|
485
|
-
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
|
486
|
-
throw new InvalidInput([{ path, message: "give a sha256 as 64 hex digits" }]);
|
|
487
|
-
}
|
|
488
|
-
return hex;
|
|
489
|
-
}
|
|
490
|
-
export const notAttestor = (attestor) => `this intent names ${attestor} as its attestor, not this server's wallet; only that key can release it`;
|
|
491
|
-
const walletAttest = define({
|
|
492
|
-
name: tool("wallet_attest"),
|
|
493
|
-
title: "Release an escrow as its attestor",
|
|
494
|
-
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.`,
|
|
495
|
-
effect: "submit",
|
|
496
|
-
input: object({
|
|
497
|
-
owner: account("the wallet that made the escrow"),
|
|
498
|
-
nonce: amount("the escrow intent's nonce"),
|
|
499
|
-
pay_to: account("the wallet to pay"),
|
|
500
|
-
result: optional(amount("the result to attest, between the intent's min_result and max_result; default min_result")),
|
|
501
|
-
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")}`)),
|
|
502
|
-
approval: optional(text("the approval the preview returned; needed with send")),
|
|
503
|
-
send: optional(flag("true signs the attestation and settles; leave out to preview")),
|
|
504
|
-
}),
|
|
505
|
-
output: object({
|
|
506
|
-
intent: account(),
|
|
507
|
-
pay_to: account(),
|
|
508
|
-
payee: account("pay_to's token account that receives the escrow"),
|
|
509
|
-
paid: text("whole tokens pay_to receives"),
|
|
510
|
-
fee: text("whole tokens kept as fees"),
|
|
511
|
-
result: amount(),
|
|
512
|
-
approval: text("pass it back with send: true; a send that would differ from this preview is refused"),
|
|
513
|
-
delivered_sha256: optional(text("the sha256 the release records")),
|
|
514
|
-
summary: list(text(), 8),
|
|
515
|
-
warnings: list(text(), 8),
|
|
516
|
-
sent: flag(),
|
|
517
|
-
signature: optional(text()),
|
|
518
|
-
next: optional(text()),
|
|
519
|
-
}),
|
|
520
|
-
run: async (ctx, input, call) => {
|
|
521
|
-
const signer = await signerOf(ctx);
|
|
522
|
-
const [intent] = await findIntentPda({ owner: input.owner, nonce: input.nonce });
|
|
523
|
-
const [view, { config }] = await Promise.all([ctx.transport.getAccount(intent), loadConfig(ctx.transport)]);
|
|
524
|
-
if (view === null) {
|
|
525
|
-
throw new AgentRefusal("NotFound", `no intent ${intent}; it was settled, cancelled and closed, or never made`);
|
|
526
|
-
}
|
|
527
|
-
const held = getIntentDecoder().decode(view.data);
|
|
528
|
-
const params = decodeParams(held.kind, Uint8Array.from(held.params));
|
|
529
|
-
if (params.kind !== Kind.AttestedResult) {
|
|
530
|
-
throw new AgentRefusal("KindMismatch", "this intent is not an attested escrow");
|
|
531
|
-
}
|
|
532
|
-
if (params.attestor !== signer.address) {
|
|
533
|
-
throw new AgentRefusal("Unauthorized", notAttestor(params.attestor));
|
|
534
|
-
}
|
|
535
|
-
if (held.status !== IntentStatus.Open) {
|
|
536
|
-
throw new AgentRefusal("InvalidTransition", `this escrow is ${STATUS[held.status] ?? "not open"}, so it can no longer be released`);
|
|
537
|
-
}
|
|
538
|
-
if (held.expiresAt <= (await chainTime(ctx.transport))) {
|
|
539
|
-
throw new AgentRefusal("IntentExpired", "this escrow has expired; its owner gets it back instead");
|
|
540
|
-
}
|
|
541
|
-
const result = input.result ?? params.minResult;
|
|
542
|
-
if (result < params.minResult || result > params.maxResult) {
|
|
543
|
-
throw new InvalidInput([{ path: "result", message: `must be between ${params.minResult} and ${params.maxResult}` }]);
|
|
544
|
-
}
|
|
545
|
-
await walletRecipient(ctx.transport, input.pay_to, "pay_to");
|
|
546
|
-
const token = await mintOf(ctx.transport, held.inputMint);
|
|
547
|
-
const payee = await tokenAccountIfMissing(ctx.transport, signer, input.pay_to, held.inputMint, token.tokenProgram);
|
|
548
|
-
if (payee.create.length === 0) {
|
|
549
|
-
const existing = await ctx.transport.getAccount(payee.address);
|
|
550
|
-
const holder = existing === null || existing.data.length < 64 ? null : getAddressDecoder().decode(existing.data.subarray(32, 64));
|
|
551
|
-
if (holder !== input.pay_to) {
|
|
552
|
-
throw new InvalidInput([{ path: "pay_to", message: PAYEE_MOVED }]);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
const amountIn = held.escrowed - held.filled;
|
|
556
|
-
const isProof = params.specHash.some((b) => b !== 0);
|
|
557
|
-
const settleAmount = amountIn - params.judgeFee;
|
|
558
|
-
const maxFeeBps = feeBpsFor(config, held.kind) + held.partnerBps;
|
|
559
|
-
const shares = splitFees(settleAmount, feeBpsFor(config, held.kind), held.partnerBps, held.partnerCutBps);
|
|
560
|
-
const treasury = shares.protocol > 0n
|
|
561
|
-
? await tokenAccountIfMissing(ctx.transport, signer, config.feeRecipient, held.inputMint, token.tokenProgram)
|
|
562
|
-
: { create: [] };
|
|
563
|
-
const partnerPaid = shares.partner > 0n;
|
|
564
|
-
const partner = partnerPaid ? await tokenAccountIfMissing(ctx.transport, signer, held.partner, held.inputMint, token.tokenProgram) : { create: [] };
|
|
565
|
-
const judge = isProof && params.judgeFee > 0n
|
|
566
|
-
? await tokenAccountIfMissing(ctx.transport, signer, signer.address, held.inputMint, token.tokenProgram)
|
|
567
|
-
: { create: [] };
|
|
568
|
-
const attestFields = {
|
|
569
|
-
domain: Uint8Array.from(config.domain),
|
|
570
|
-
intent,
|
|
571
|
-
payee: payee.address,
|
|
572
|
-
amountIn,
|
|
573
|
-
result,
|
|
574
|
-
filled: held.filled,
|
|
575
|
-
revision: held.revision,
|
|
576
|
-
maxFeeBps,
|
|
577
|
-
};
|
|
578
|
-
const message = isProof
|
|
579
|
-
? await attestedProofMessage(signer.address, { ...attestFields, specHash: params.specHash })
|
|
580
|
-
: await attestedFillMessage(signer.address, attestFields);
|
|
581
|
-
const settle = await buildFillAttested({
|
|
582
|
-
solver: signer,
|
|
583
|
-
owner: input.owner,
|
|
584
|
-
nonce: input.nonce,
|
|
585
|
-
inputMint: held.inputMint,
|
|
586
|
-
inputTokenProgram: token.tokenProgram,
|
|
587
|
-
payee: payee.address,
|
|
588
|
-
...(shares.protocol > 0n ? { feeRecipient: config.feeRecipient } : {}),
|
|
589
|
-
...(partnerPaid ? { partner: held.partner } : {}),
|
|
590
|
-
...(isProof && params.judgeFee > 0n ? { judge: signer.address } : {}),
|
|
591
|
-
amountIn,
|
|
592
|
-
result,
|
|
593
|
-
maxFeeBps,
|
|
594
|
-
});
|
|
595
|
-
const name = TOKEN_NAMES[held.inputMint] ?? held.inputMint;
|
|
596
|
-
const paid = formatAmount(shares.net, token.decimals);
|
|
597
|
-
const setup = [...payee.create, ...treasury.create, ...partner.create, ...judge.create];
|
|
598
|
-
const delivered = input.delivered_sha256 === undefined ? undefined : sha256Of(input.delivered_sha256, "delivered_sha256");
|
|
599
|
-
const memoText = delivered === undefined ? "" : `${DELIVERY_PREFIX}${delivered}`;
|
|
600
|
-
const memo = delivered === undefined
|
|
601
|
-
? []
|
|
602
|
-
: [{ programAddress: MEMO_PROGRAM_ADDRESS, accounts: [{ address: signer.address, role: AccountRole.READONLY_SIGNER, signer }], data: new TextEncoder().encode(memoText) }];
|
|
603
|
-
const approval = approvalOf(message, memoText);
|
|
604
|
-
if (input.send === true && input.approval !== approval) {
|
|
605
|
-
throw new AgentRefusal("ReleaseChanged", RELEASE_CHANGED);
|
|
606
|
-
}
|
|
607
|
-
const prepared = input.send === true
|
|
608
|
-
? preparedView(await prepare(ctx.transport, [...setup, await signEd25519Instruction(signer, message), settle, ...memo], signer.address))
|
|
609
|
-
: undefined;
|
|
610
|
-
const view2 = {
|
|
611
|
-
intent,
|
|
612
|
-
pay_to: input.pay_to,
|
|
613
|
-
payee: payee.address,
|
|
614
|
-
paid,
|
|
615
|
-
fee: formatAmount(shares.protocol + shares.partner, token.decimals),
|
|
616
|
-
result,
|
|
617
|
-
approval,
|
|
618
|
-
...(delivered === undefined ? {} : { delivered_sha256: delivered }),
|
|
619
|
-
summary: [
|
|
620
|
-
`Pays: ${paid} of ${name} to ${input.pay_to} from ${input.owner}'s escrow${held.inputMint === NATIVE_MINT ? WRAPPED : ""}`,
|
|
621
|
-
...(isProof && params.judgeFee > 0n ? [`Pays the judge: ${formatAmount(params.judgeFee, token.decimals)} of ${name} to this wallet, carved out of the escrow`] : []),
|
|
622
|
-
...(isProof ? [`Binds: the result to the committed spec ${getBase16Decoder().decode(params.specHash)}`] : []),
|
|
623
|
-
`Attests: result ${result}, signed by this server's wallet as the escrow's attestor`,
|
|
624
|
-
...(delivered === undefined ? [] : [`Records: the delivered work's sha256 ${delivered}, in a memo this wallet signs with the release`]),
|
|
625
|
-
...(payee.create.length === 0 ? [] : [`Setup: creates ${input.pay_to}'s ${name} account, paid by this wallet`]),
|
|
626
|
-
...(treasury.create.length === 0 ? [] : [`Setup: creates the engine treasury's ${name} account for the fee, paid by this wallet`]),
|
|
627
|
-
...(partner.create.length === 0 ? [] : [`Setup: creates the partner ${held.partner}'s ${name} account for its fee, paid by this wallet`]),
|
|
628
|
-
],
|
|
629
|
-
warnings: settlementWarnings(params),
|
|
630
|
-
prepared,
|
|
631
|
-
};
|
|
632
|
-
return sent(ctx, signer, view2, input.send, call);
|
|
633
|
-
},
|
|
634
|
-
});
|
|
635
|
-
export const PACKET_DATA_SIZE = 1_232;
|
|
636
|
-
export const splitTooLarge = (size, recipients, withTable) => `this ${recipients}-co-author split builds a ${size}-byte transaction, over the ${PACKET_DATA_SIZE}-byte limit${withTable ? " even with the lookup table" : ""}; roughly ${withTable ? 4 : 2} co-authors fit${withTable ? " with a lookup table, so reduce recipients or pay the rest in a second release" : ", so pass lookup_table with the preview's lookup_accounts to fit more, or reduce recipients"}`;
|
|
637
|
-
export const SPLIT_WEIGHTS_WRONG = "the weights must be whole basis points that add up to 10000";
|
|
638
|
-
export const SPLIT_SIG_NEEDED = "every co-author must sign the split before send: true; preview returns the envelope each one signs";
|
|
639
|
-
export const SIGNATURE_PATTERN = "^[0-9a-fA-F]{128}$";
|
|
640
|
-
export const WARN_COAUTHORS = "every co-author must sign the exact split; a release pays only the recipients and weights they all signed";
|
|
641
|
-
const coauthor = object({
|
|
642
|
-
wallet: account("the co-author's wallet"),
|
|
643
|
-
weight: integer(1, 10_000, "this co-author's share in basis points; all weights must add up to 10000"),
|
|
644
|
-
signature: optional(text("128 hex: this co-author's signature over the envelope the preview returned for this wallet; needed with send")),
|
|
645
|
-
});
|
|
646
|
-
const walletAttestSplit = define({
|
|
647
|
-
name: tool("wallet_attest_split"),
|
|
648
|
-
title: "Release a proof escrow split among co-authors",
|
|
649
|
-
description: `As the attestor a proof escrow names, release it on chain divided among several co-authors by weight, from this server's wallet: the escrowed tokens, minus the engine fee, any partner fee and the judge fee, are split by the weights and paid to each co-author's token account, which this creates if needed. Every co-author must authorize the exact split: preview without send to get the digest and the envelope each wallet must sign, collect those signatures, then call again with send: true and each co-author's signature. Changing any recipient, weight or the result changes what everyone signs. The escrow must have been made with a spec_hash. 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.`,
|
|
650
|
-
effect: "submit",
|
|
651
|
-
input: object({
|
|
652
|
-
owner: account("the wallet that made the escrow"),
|
|
653
|
-
nonce: amount("the escrow intent's nonce"),
|
|
654
|
-
recipients: list(coauthor, MAX_SPLIT_RECIPIENTS, "the co-authors and their weighted shares, up to 8", 1),
|
|
655
|
-
result: optional(amount("the result to attest, between the intent's min_result and max_result; default min_result")),
|
|
656
|
-
lookup_table: optional(account("an address lookup table holding this split's lookup_accounts, to fit more co-authors under the transaction size limit; create one with the preview's lookup_accounts and pass it with send")),
|
|
657
|
-
approval: optional(text("the approval the preview returned; needed with send")),
|
|
658
|
-
send: optional(flag("true signs the attestation and settles; leave out to preview")),
|
|
659
|
-
}),
|
|
660
|
-
output: object({
|
|
661
|
-
intent: account(),
|
|
662
|
-
digest: text("64 hex: the split digest every co-author and this wallet sign over"),
|
|
663
|
-
envelope: text("the one message every co-author and this wallet sign, as hex; collect each co-author's signature over it"),
|
|
664
|
-
lookup_accounts: list(account(), 24, "put these in an address lookup table (solana address-lookup-table create, then extend) and pass it as lookup_table to fit more co-authors"),
|
|
665
|
-
paid: list(object({ wallet: account(), weight: amount(), amount: text("whole tokens this co-author receives") }), MAX_SPLIT_RECIPIENTS),
|
|
666
|
-
net: text("whole tokens shared among the co-authors"),
|
|
667
|
-
fee: text("whole tokens kept as fees"),
|
|
668
|
-
result: amount(),
|
|
669
|
-
approval: text("pass it back with send: true; a send that would differ from this preview is refused"),
|
|
670
|
-
summary: list(text(), 12),
|
|
671
|
-
warnings: list(text(), 8),
|
|
672
|
-
sent: flag(),
|
|
673
|
-
signature: optional(text()),
|
|
674
|
-
next: optional(text()),
|
|
675
|
-
}),
|
|
676
|
-
run: async (ctx, input, call) => {
|
|
677
|
-
const signer = await signerOf(ctx);
|
|
678
|
-
const [intent] = await findIntentPda({ owner: input.owner, nonce: input.nonce });
|
|
679
|
-
const [view, { config }] = await Promise.all([ctx.transport.getAccount(intent), loadConfig(ctx.transport)]);
|
|
680
|
-
if (view === null) {
|
|
681
|
-
throw new AgentRefusal("NotFound", `no intent ${intent}; it was settled, cancelled and closed, or never made`);
|
|
682
|
-
}
|
|
683
|
-
const held = getIntentDecoder().decode(view.data);
|
|
684
|
-
const params = decodeParams(held.kind, Uint8Array.from(held.params));
|
|
685
|
-
if (params.kind !== Kind.AttestedResult) {
|
|
686
|
-
throw new AgentRefusal("KindMismatch", "this intent is not an attested escrow");
|
|
687
|
-
}
|
|
688
|
-
if (!params.specHash.some((b) => b !== 0)) {
|
|
689
|
-
throw new AgentRefusal("NotAProof", "a split release needs an escrow made with a spec_hash");
|
|
690
|
-
}
|
|
691
|
-
if (params.attestor !== signer.address) {
|
|
692
|
-
throw new AgentRefusal("Unauthorized", notAttestor(params.attestor));
|
|
693
|
-
}
|
|
694
|
-
if (held.status !== IntentStatus.Open) {
|
|
695
|
-
throw new AgentRefusal("InvalidTransition", `this escrow is ${STATUS[held.status] ?? "not open"}, so it can no longer be released`);
|
|
696
|
-
}
|
|
697
|
-
if (held.expiresAt <= (await chainTime(ctx.transport))) {
|
|
698
|
-
throw new AgentRefusal("IntentExpired", "this escrow has expired; its owner gets it back instead");
|
|
699
|
-
}
|
|
700
|
-
const result = input.result ?? params.minResult;
|
|
701
|
-
if (result < params.minResult || result > params.maxResult) {
|
|
702
|
-
throw new InvalidInput([{ path: "result", message: `must be between ${params.minResult} and ${params.maxResult}` }]);
|
|
703
|
-
}
|
|
704
|
-
const weights = input.recipients.map((r) => r.weight);
|
|
705
|
-
if (weights.reduce((a, b) => a + b, 0) !== 10_000) {
|
|
706
|
-
throw new InvalidInput([{ path: "recipients", message: SPLIT_WEIGHTS_WRONG }]);
|
|
707
|
-
}
|
|
708
|
-
const wallets = input.recipients.map((r) => r.wallet);
|
|
709
|
-
for (const [i, r] of input.recipients.entries()) {
|
|
710
|
-
await walletRecipient(ctx.transport, r.wallet, `recipients.${i}.wallet`);
|
|
711
|
-
}
|
|
712
|
-
const token = await mintOf(ctx.transport, held.inputMint);
|
|
713
|
-
const payees = await Promise.all(wallets.map((w) => tokenAccountIfMissing(ctx.transport, signer, w, held.inputMint, token.tokenProgram)));
|
|
714
|
-
const amountIn = held.escrowed - held.filled;
|
|
715
|
-
const settleAmount = amountIn - params.judgeFee;
|
|
716
|
-
const maxFeeBps = feeBpsFor(config, held.kind) + held.partnerBps;
|
|
717
|
-
const shares = splitFees(settleAmount, feeBpsFor(config, held.kind), held.partnerBps, held.partnerCutBps);
|
|
718
|
-
const payout = splitN(shares.net, weights);
|
|
719
|
-
const treasury = shares.protocol > 0n ? await tokenAccountIfMissing(ctx.transport, signer, config.feeRecipient, held.inputMint, token.tokenProgram) : { create: [] };
|
|
720
|
-
const partnerPaid = shares.partner > 0n;
|
|
721
|
-
const partner = partnerPaid ? await tokenAccountIfMissing(ctx.transport, signer, held.partner, held.inputMint, token.tokenProgram) : { create: [] };
|
|
722
|
-
const judge = params.judgeFee > 0n ? await tokenAccountIfMissing(ctx.transport, signer, signer.address, held.inputMint, token.tokenProgram) : { create: [] };
|
|
723
|
-
const splitHash = await splitCommit(wallets, weights);
|
|
724
|
-
const fields = {
|
|
725
|
-
domain: Uint8Array.from(config.domain),
|
|
726
|
-
intent,
|
|
727
|
-
amountIn,
|
|
728
|
-
result,
|
|
729
|
-
filled: held.filled,
|
|
730
|
-
revision: held.revision,
|
|
731
|
-
maxFeeBps,
|
|
732
|
-
specHash: params.specHash,
|
|
733
|
-
splitHash,
|
|
734
|
-
};
|
|
735
|
-
const message = await attestedSplitMessage(fields);
|
|
736
|
-
const digestHex = getBase16Decoder().decode(Uint8Array.from(createHash("sha256").update(await attestedSplitPreimage(fields)).digest()));
|
|
737
|
-
const settle = await buildFillAttestedSplit({
|
|
738
|
-
solver: signer,
|
|
739
|
-
owner: input.owner,
|
|
740
|
-
nonce: input.nonce,
|
|
741
|
-
inputMint: held.inputMint,
|
|
742
|
-
inputTokenProgram: token.tokenProgram,
|
|
743
|
-
recipients: wallets,
|
|
744
|
-
weights,
|
|
745
|
-
...(shares.protocol > 0n ? { feeRecipient: config.feeRecipient } : {}),
|
|
746
|
-
...(partnerPaid ? { partner: held.partner } : {}),
|
|
747
|
-
...(params.judgeFee > 0n ? { judge: signer.address } : {}),
|
|
748
|
-
amountIn,
|
|
749
|
-
result,
|
|
750
|
-
maxFeeBps,
|
|
751
|
-
});
|
|
752
|
-
const name = TOKEN_NAMES[held.inputMint] ?? held.inputMint;
|
|
753
|
-
const approval = approvalOf(message);
|
|
754
|
-
if (input.send === true && input.approval !== approval) {
|
|
755
|
-
throw new AgentRefusal("ReleaseChanged", RELEASE_CHANGED);
|
|
756
|
-
}
|
|
757
|
-
let batch;
|
|
758
|
-
if (input.send === true) {
|
|
759
|
-
const entries = input.recipients.map((r, i) => {
|
|
760
|
-
if (r.signature === undefined || !new RegExp(SIGNATURE_PATTERN).test(r.signature)) {
|
|
761
|
-
throw new InvalidInput([{ path: `recipients.${i}.signature`, message: SPLIT_SIG_NEEDED }]);
|
|
762
|
-
}
|
|
763
|
-
return { publicKey: r.wallet, signature: Uint8Array.from(getBase16Encoder().encode(r.signature)) };
|
|
764
|
-
});
|
|
765
|
-
const attestorSignature = new Uint8Array(await signBytes(signer.keyPair.privateKey, message));
|
|
766
|
-
batch = buildEd25519BatchInstruction({ entries: [...entries, { publicKey: signer.address, signature: attestorSignature }], message });
|
|
767
|
-
}
|
|
768
|
-
const setup = [...payees.flatMap((p) => p.create), ...treasury.create, ...partner.create, ...judge.create];
|
|
769
|
-
const invoked = new Set([INTENT_ENGINE_PROGRAM_ADDRESS, token.tokenProgram, signer.address]);
|
|
770
|
-
const lookupAccounts = [...new Set((settle.accounts ?? []).map((a) => a.address))].filter((a) => !invoked.has(a));
|
|
771
|
-
let tables;
|
|
772
|
-
if (input.send === true && input.lookup_table !== undefined) {
|
|
773
|
-
const view = await ctx.transport.getAccount(input.lookup_table);
|
|
774
|
-
if (view === null) {
|
|
775
|
-
throw new AgentRefusal("NotFound", `no lookup table ${input.lookup_table} on this cluster`);
|
|
776
|
-
}
|
|
777
|
-
tables = { [input.lookup_table]: decodeLookupTable(view.data) };
|
|
778
|
-
}
|
|
779
|
-
let prepared;
|
|
780
|
-
if (input.send === true && batch !== undefined) {
|
|
781
|
-
const ixs = [...setup, batch, settle];
|
|
782
|
-
const budgeted = await ctx.transport.budget(ixs, signer.address);
|
|
783
|
-
const size = await transactionSize(ctx.transport, budgeted, signer.address, tables);
|
|
784
|
-
if (size > PACKET_DATA_SIZE) {
|
|
785
|
-
throw new AgentRefusal("ReleaseTooLarge", splitTooLarge(size, input.recipients.length, input.lookup_table !== undefined));
|
|
786
|
-
}
|
|
787
|
-
prepared = preparedView(await prepare(ctx.transport, ixs, signer.address, tables));
|
|
788
|
-
}
|
|
789
|
-
const view2 = {
|
|
790
|
-
intent,
|
|
791
|
-
digest: digestHex,
|
|
792
|
-
envelope: getBase16Decoder().decode(message),
|
|
793
|
-
lookup_accounts: lookupAccounts,
|
|
794
|
-
paid: input.recipients.map((r, i) => ({
|
|
795
|
-
wallet: r.wallet,
|
|
796
|
-
weight: BigInt(r.weight),
|
|
797
|
-
amount: formatAmount(payout[i] ?? 0n, token.decimals),
|
|
798
|
-
})),
|
|
799
|
-
net: formatAmount(shares.net, token.decimals),
|
|
800
|
-
fee: formatAmount(shares.protocol + shares.partner + params.judgeFee, token.decimals),
|
|
801
|
-
result,
|
|
802
|
-
approval,
|
|
803
|
-
summary: [
|
|
804
|
-
`Splits: ${formatAmount(shares.net, token.decimals)} of ${name} among ${input.recipients.length} co-authors from ${input.owner}'s escrow${held.inputMint === NATIVE_MINT ? WRAPPED : ""}`,
|
|
805
|
-
...input.recipients.map((r, i) => ` ${r.wallet}: ${r.weight} bps -> ${formatAmount(payout[i] ?? 0n, token.decimals)} of ${name}`),
|
|
806
|
-
...(params.judgeFee > 0n ? [`Pays the judge: ${formatAmount(params.judgeFee, token.decimals)} of ${name} to this wallet, carved out of the escrow`] : []),
|
|
807
|
-
`Binds: the result to the committed spec ${getBase16Decoder().decode(params.specHash)}`,
|
|
808
|
-
`Attests: result ${result}, signed by this server's wallet as the escrow's attestor`,
|
|
809
|
-
...(setup.length === 0 ? [] : [`Setup: creates ${setup.length} token account(s), paid by this wallet`]),
|
|
810
|
-
],
|
|
811
|
-
warnings: [...settlementWarnings(params), WARN_COAUTHORS],
|
|
812
|
-
prepared,
|
|
813
|
-
};
|
|
814
|
-
return sent(ctx, signer, view2, input.send, call);
|
|
815
|
-
},
|
|
816
|
-
});
|
|
817
|
-
export const MAX_RECURRING = 12;
|
|
818
|
-
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`;
|
|
819
|
-
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`;
|
|
820
|
-
export const WARN_EARLY = "an order fills before its turn if a solver can already pay its start_receive";
|
|
821
|
-
export const NO_FALL = "must be more than 0, or the orders have no ask to fall";
|
|
822
|
-
const refusedBeforeSending = (error) => neverLanded(error) || error instanceof AgentRefusal || error instanceof InvalidInput;
|
|
823
|
-
export const MAX_EVERY = 30 * 24 * 60 * 60;
|
|
824
|
-
const walletRecurring = define({
|
|
825
|
-
name: tool("wallet_recurring"),
|
|
826
|
-
title: "Repeat a swap on a schedule",
|
|
827
|
-
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.`,
|
|
828
|
-
effect: "submit",
|
|
829
|
-
repeats: true,
|
|
830
|
-
input: object({
|
|
831
|
-
sell: text("SOL, TEST or a mint address"),
|
|
832
|
-
buy: text("SOL, TEST or a mint address"),
|
|
833
|
-
amount: text("whole tokens each order sells, such as 0.05"),
|
|
834
|
-
min_receive: text("least whole tokens each order receives"),
|
|
835
|
-
every: integer(MIN_EXPIRES_IN, MAX_EVERY, "seconds between one order opening and the next"),
|
|
836
|
-
times: integer(2, MAX_RECURRING, "how many orders"),
|
|
837
|
-
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")),
|
|
838
|
-
falls_over: optional(integer(MIN_EXPIRES_IN, MAX_EVERY, `seconds each order's ask takes to fall to min_receive; default ${SWAP_FALLS_OVER}`)),
|
|
839
|
-
pay_to: optional(account(PAY_TO)),
|
|
840
|
-
send: optional(flag("true signs and sends every order; leave out to preview")),
|
|
841
|
-
}),
|
|
842
|
-
output: object({
|
|
843
|
-
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),
|
|
844
|
-
summary: list(text(), 24, "the whole plan, then the first order"),
|
|
845
|
-
warnings: list(text(), 16),
|
|
846
|
-
start_receive: text("whole tokens each order asks when it opens; pass it back when sending"),
|
|
847
|
-
sent: flag(),
|
|
848
|
-
next: optional(text()),
|
|
849
|
-
}),
|
|
850
|
-
run: async (ctx, input, call) => {
|
|
851
|
-
const wallet = access(ctx);
|
|
852
|
-
const inputMint = mintNamed(input.sell, wallet.url, "sell");
|
|
853
|
-
const outputMint = mintNamed(input.buy, wallet.url, "buy");
|
|
854
|
-
const [inputToken, outputToken, now] = await Promise.all([
|
|
855
|
-
mintOf(ctx.transport, inputMint),
|
|
856
|
-
mintOf(ctx.transport, outputMint),
|
|
857
|
-
chainTime(ctx.transport),
|
|
858
|
-
]);
|
|
859
|
-
const amountIn = raw(input.amount, inputToken.decimals, "amount");
|
|
860
|
-
const floor = raw(input.min_receive, outputToken.decimals, "min_receive");
|
|
861
|
-
const ask = input.start_receive === undefined
|
|
862
|
-
? openingAsk(await recentFills(ctx.transport, inputMint, outputMint), amountIn, floor)
|
|
863
|
-
: raw(input.start_receive, outputToken.decimals, "start_receive");
|
|
864
|
-
if (ask <= floor) {
|
|
865
|
-
throw new InvalidInput([
|
|
866
|
-
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" },
|
|
867
|
-
]);
|
|
868
|
-
}
|
|
869
|
-
checkSale(wallet.limits, inputMint, inputToken.decimals, amountIn, now);
|
|
870
|
-
checkHour(wallet.limits, inputMint, inputToken.decimals, amountIn * BigInt(input.times), now);
|
|
871
|
-
const falls = BigInt(input.falls_over ?? SWAP_FALLS_OVER);
|
|
872
|
-
const every = BigInt(input.every);
|
|
873
|
-
const life = falls > every ? falls : every;
|
|
874
|
-
const opens = Array.from({ length: input.times }, (_, k) => now + BigInt(LANDING_SECONDS) + every * BigInt(k));
|
|
875
|
-
const terms = (open) => () => ({
|
|
876
|
-
order: { kind: "dutch", amount_in: amountIn, start_out: ask, end_out: floor, start_time: open, end_time: open + falls },
|
|
877
|
-
expires_at: open + life,
|
|
878
|
-
});
|
|
879
|
-
const shown = { start_receive: formatAmount(ask, outputToken.decimals) };
|
|
880
|
-
const last = opens[opens.length - 1];
|
|
881
|
-
const first = (await offered(ctx, call, { ...input, send: input.send }, terms(opens[0])));
|
|
882
|
-
const sold = TOKEN_NAMES[inputMint] ?? inputMint;
|
|
883
|
-
const plan = [
|
|
884
|
-
`Plan: ${input.times} orders of ${formatAmount(amountIn, inputToken.decimals)} of ${sold}, ${formatAmount(amountIn * BigInt(input.times), inputToken.decimals)} of ${sold} in all, escrowed now`,
|
|
885
|
-
`Schedule: the orders open from ${formatTime(opens[0])} to ${formatTime(last)}; the last expires ${formatTime(last + life)}`,
|
|
886
|
-
...first["summary"],
|
|
887
|
-
];
|
|
888
|
-
const warned = first["warnings"];
|
|
889
|
-
const warnings = [
|
|
890
|
-
...warned,
|
|
891
|
-
WARN_EARLY,
|
|
892
|
-
...(last + life - now > LONG_LIVED_SECONDS && !warned.includes(WARN_LONG_LIVED) ? [WARN_LONG_LIVED] : []),
|
|
893
|
-
];
|
|
894
|
-
if (input.send !== true) {
|
|
895
|
-
return { orders: opens.map((opens_at) => ({ opens_at })), summary: plan, warnings, ...shown, sent: false, next: first["next"] };
|
|
896
|
-
}
|
|
897
|
-
const orders = [{ opens_at: opens[0], intent: first["intent"], nonce: first["nonce"], signature: first["signature"] }];
|
|
898
|
-
for (const open of opens.slice(1)) {
|
|
899
|
-
try {
|
|
900
|
-
const made = (await offered(ctx, call, { ...input, send: true }, terms(open)));
|
|
901
|
-
orders.push({ opens_at: open, intent: made["intent"], nonce: made["nonce"], signature: made["signature"] });
|
|
902
|
-
}
|
|
903
|
-
catch (error) {
|
|
904
|
-
for (const undo of call.undo.splice(0)) {
|
|
905
|
-
undo();
|
|
906
|
-
}
|
|
907
|
-
const problem = problemOf(error);
|
|
908
|
-
return {
|
|
909
|
-
orders: [...orders, ...opens.slice(orders.length).map((opens_at) => ({ opens_at }))],
|
|
910
|
-
summary: plan,
|
|
911
|
-
warnings,
|
|
912
|
-
...shown,
|
|
913
|
-
sent: true,
|
|
914
|
-
next: refusedBeforeSending(error)
|
|
915
|
-
? partlySent(orders.length, input.times, `${problem.code}: ${problem.hint}`)
|
|
916
|
-
: mayHaveLanded(orders.length, input.times, `${problem.code}: ${problem.hint}`),
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
return { orders, summary: plan, warnings, ...shown, sent: true };
|
|
921
|
-
},
|
|
922
|
-
});
|
|
923
|
-
const walletAuction = define({
|
|
924
|
-
name: tool("wallet_auction"),
|
|
925
|
-
title: "Auction from the wallet",
|
|
926
|
-
description: "Offer an exact amount of one token in a Dutch auction from this server's wallet: the least it accepts starts at start_receive once the offer lands, falls in a straight line to end_receive over falls_over seconds, then stays there until it expires, so a solver fills it as soon as the asked amount meets what it can pay. Amounts are whole tokens such as 0.05. Selling SOL wraps it first. Without send it only previews; show the user the summary and call again with send: true once they approve.",
|
|
927
|
-
effect: "submit",
|
|
928
|
-
repeats: true,
|
|
929
|
-
input: object({
|
|
930
|
-
sell: text("SOL, TEST or a mint address"),
|
|
931
|
-
buy: text("SOL, TEST or a mint address"),
|
|
932
|
-
amount: text("whole tokens to sell, such as 0.05"),
|
|
933
|
-
start_receive: text("whole tokens asked when the auction starts; at least end_receive"),
|
|
934
|
-
end_receive: text("least whole tokens accepted once the asked amount stops falling"),
|
|
935
|
-
falls_over: integer(MIN_EXPIRES_IN, MAX_EXPIRES_IN, "seconds the asked amount takes to fall from start_receive to end_receive"),
|
|
936
|
-
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}`)),
|
|
937
|
-
pay_to: optional(account(PAY_TO)),
|
|
938
|
-
send: optional(flag("true signs and sends; leave out to preview")),
|
|
939
|
-
}),
|
|
940
|
-
output: offerOutput,
|
|
941
|
-
run: async (ctx, input, call) => offered(ctx, call, input, (sold, bought, now) => {
|
|
942
|
-
const start = now + BigInt(LANDING_SECONDS);
|
|
943
|
-
return {
|
|
944
|
-
order: {
|
|
945
|
-
kind: "dutch",
|
|
946
|
-
amount_in: raw(input.amount, sold.decimals, "amount"),
|
|
947
|
-
start_out: raw(input.start_receive, bought.decimals, "start_receive"),
|
|
948
|
-
end_out: raw(input.end_receive, bought.decimals, "end_receive"),
|
|
949
|
-
start_time: start,
|
|
950
|
-
end_time: start + BigInt(input.falls_over),
|
|
951
|
-
},
|
|
952
|
-
expires_at: start + BigInt(Math.max(input.expires_in ?? DEFAULT_EXPIRES_IN, input.falls_over)),
|
|
953
|
-
};
|
|
954
|
-
}),
|
|
955
|
-
});
|
|
956
|
-
const walletIntents = define({
|
|
957
|
-
name: tool("wallet_intents"),
|
|
958
|
-
title: "List the wallet's intents",
|
|
959
|
-
description: "List the intents this server's wallet created, newest state first: open ones can still fill or be cancelled.",
|
|
960
|
-
effect: "read",
|
|
961
|
-
input: object({ status: optional(choice(STATUS, "only intents in this status")) }),
|
|
962
|
-
output: listIntents.output,
|
|
963
|
-
run: async (ctx, input, call) => {
|
|
964
|
-
const owner = (await signerOf(ctx)).address;
|
|
965
|
-
return listIntents.run(ctx, { owner, ...(input.status === undefined ? {} : { status: input.status }) }, call);
|
|
966
|
-
},
|
|
967
|
-
});
|
|
968
|
-
const walletCancel = define({
|
|
969
|
-
name: tool("wallet_cancel"),
|
|
970
|
-
title: "Cancel a wallet intent",
|
|
971
|
-
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.",
|
|
972
|
-
effect: "submit",
|
|
973
|
-
input: object({ nonce: amount("the intent's nonce, from wallet_intents"), send: optional(flag("true signs and sends; leave out to preview")) }),
|
|
974
|
-
output: object({
|
|
975
|
-
intent: account(),
|
|
976
|
-
refund: amount("raw input units returned to the wallet"),
|
|
977
|
-
summary: list(text(), 16),
|
|
978
|
-
warnings: list(text(), 8),
|
|
979
|
-
sent: flag(),
|
|
980
|
-
signature: optional(text()),
|
|
981
|
-
next: optional(text()),
|
|
982
|
-
}),
|
|
983
|
-
run: async (ctx, input, call) => {
|
|
984
|
-
const signer = await signerOf(ctx);
|
|
985
|
-
const view = (await prepareCancel.run(ctx, { owner: signer.address, nonce: input.nonce, close: true }, call));
|
|
986
|
-
return sent(ctx, signer, view, input.send, call);
|
|
987
|
-
},
|
|
988
|
-
});
|
|
989
|
-
async function engineHistory(transport, limit, of) {
|
|
990
|
-
const created = new Map();
|
|
991
|
-
const fills = [];
|
|
992
|
-
const ended = new Map();
|
|
993
|
-
let unread = 0;
|
|
994
|
-
for (const tx of await transport.engineTransactions(limit, of)) {
|
|
995
|
-
if (tx.unread === true) {
|
|
996
|
-
unread += 1;
|
|
997
|
-
}
|
|
998
|
-
if (tx.failed) {
|
|
999
|
-
continue;
|
|
1000
|
-
}
|
|
1001
|
-
const begun = new Map();
|
|
1002
|
-
for (const event of tx.emitted.map(decodeEvent)) {
|
|
1003
|
-
if (event === null) {
|
|
1004
|
-
continue;
|
|
1005
|
-
}
|
|
1006
|
-
const fields = event.fields;
|
|
1007
|
-
const intent = fields["intent"];
|
|
1008
|
-
if (event.name === "IntentCreated") {
|
|
1009
|
-
created.set(intent, { inputMint: fields["inputMint"], outputMint: fields["outputMint"] });
|
|
1010
|
-
}
|
|
1011
|
-
if (event.name === "FillBegun") {
|
|
1012
|
-
begun.set(intent, fields["amountIn"]);
|
|
1013
|
-
}
|
|
1014
|
-
const amountIn = event.name === "IntentFilled" ? fields["amountIn"] : event.name === "FillEnded" ? begun.get(intent) : undefined;
|
|
1015
|
-
if (amountIn !== undefined) {
|
|
1016
|
-
fills.push({ intent, amountIn, delivered: fields["delivered"], signature: tx.signature, blockTime: tx.blockTime });
|
|
1017
|
-
}
|
|
1018
|
-
if (event.name === "IntentCancelled" || event.name === "IntentExpired") {
|
|
1019
|
-
ended.set(intent, event.name === "IntentCancelled" ? "cancelled" : "expired");
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
return { created, fills, ended, unread };
|
|
1024
|
-
}
|
|
1025
|
-
function pairOf(data) {
|
|
1026
|
-
try {
|
|
1027
|
-
const { inputMint, outputMint } = getIntentDecoder().decode(data);
|
|
1028
|
-
return { inputMint, outputMint };
|
|
1029
|
-
}
|
|
1030
|
-
catch {
|
|
1031
|
-
return null;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
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";
|
|
1035
|
-
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`;
|
|
1036
|
-
const walletEstimate = define({
|
|
1037
|
-
name: tool("wallet_estimate"),
|
|
1038
|
-
title: "Estimate a swap from recent fills",
|
|
1039
|
-
description: `Estimate what selling an amount of one token for another gets now, from the rates of recent fills of the same pair on this cluster. It reads the engine's last ${ESTIMATE_SCAN} transactions and sends nothing. Use it before ${tool("wallet_swap")} or ${tool("wallet_auction")} to pick min_receive or the auction's start and end.`,
|
|
1040
|
-
effect: "read",
|
|
1041
|
-
input: object({
|
|
1042
|
-
sell: text("SOL, TEST or a mint address"),
|
|
1043
|
-
buy: text("SOL, TEST or a mint address"),
|
|
1044
|
-
amount: text("whole tokens to sell, such as 0.05"),
|
|
1045
|
-
}),
|
|
1046
|
-
output: object({
|
|
1047
|
-
fills: list(object({
|
|
1048
|
-
sold: text("whole tokens of the sold token"),
|
|
1049
|
-
received: text("whole tokens the owner received"),
|
|
1050
|
-
at: optional(time("when the fill landed")),
|
|
1051
|
-
signature: text(),
|
|
1052
|
-
}), ESTIMATE_FILLS, "newest first"),
|
|
1053
|
-
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")),
|
|
1054
|
-
receive_at_median: optional(text("whole tokens amount would get at the median recent rate")),
|
|
1055
|
-
receive_at_latest: optional(text("whole tokens amount would get at the newest fill's rate")),
|
|
1056
|
-
next: text(),
|
|
1057
|
-
}),
|
|
1058
|
-
run: async (ctx, input) => {
|
|
1059
|
-
const wallet = await testCluster(ctx);
|
|
1060
|
-
const inputMint = mintNamed(input.sell, wallet.url, "sell");
|
|
1061
|
-
const outputMint = mintNamed(input.buy, wallet.url, "buy");
|
|
1062
|
-
const [inputToken, outputToken] = await Promise.all([mintOf(ctx.transport, inputMint), mintOf(ctx.transport, outputMint)]);
|
|
1063
|
-
const amount = raw(input.amount, inputToken.decimals, "amount");
|
|
1064
|
-
const matching = await recentFills(ctx.transport, inputMint, outputMint);
|
|
1065
|
-
const shown = matching.map((f) => ({
|
|
1066
|
-
sold: formatAmount(f.amountIn, inputToken.decimals),
|
|
1067
|
-
received: formatAmount(f.delivered, outputToken.decimals),
|
|
1068
|
-
...(f.blockTime === null ? {} : { at: f.blockTime }),
|
|
1069
|
-
signature: f.signature,
|
|
1070
|
-
}));
|
|
1071
|
-
const [latest] = matching;
|
|
1072
|
-
if (latest === undefined) {
|
|
1073
|
-
return { fills: shown, next: NO_FILLS };
|
|
1074
|
-
}
|
|
1075
|
-
const best = bestOf(matching) ?? latest;
|
|
1076
|
-
const at = (f) => formatAmount((amount * f.delivered) / f.amountIn, outputToken.decimals);
|
|
1077
|
-
return { fills: shown, receive_at_best: at(best), receive_at_median: at(medianOf(matching) ?? latest), receive_at_latest: at(latest), next: ESTIMATE_NEXT };
|
|
1078
|
-
},
|
|
1079
|
-
});
|
|
1080
|
-
export const WAIT_STATUS = ["open", "filling", "filled", "cancelled", "expired", "closed"];
|
|
1081
|
-
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`;
|
|
1082
|
-
export const stillWaiting = (status) => `still ${status}; call ${tool("wallet_wait")} again to keep waiting, or ${tool("wallet_cancel")} to take the escrow back`;
|
|
1083
|
-
const walletWait = define({
|
|
1084
|
-
name: tool("wallet_wait"),
|
|
1085
|
-
title: "Wait for a wallet intent",
|
|
1086
|
-
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.`,
|
|
1087
|
-
effect: "read",
|
|
1088
|
-
input: object({
|
|
1089
|
-
nonce: amount("the intent's nonce, from the send or wallet_intents"),
|
|
1090
|
-
within: optional(integer(1, WAIT_MAX, `most seconds to wait; default ${WAIT_DEFAULT}`)),
|
|
1091
|
-
}),
|
|
1092
|
-
output: object({
|
|
1093
|
-
intent: account(),
|
|
1094
|
-
status: choice(WAIT_STATUS, "closed means the account is gone and its recent transactions name no fill, cancel or expiry"),
|
|
1095
|
-
waited: integer(0, WAIT_MAX, "seconds waited"),
|
|
1096
|
-
received: optional(text("whole tokens the fills delivered, to this wallet or the pay_to it named")),
|
|
1097
|
-
token: optional(text("the token received")),
|
|
1098
|
-
signature: optional(text("the fill's transaction")),
|
|
1099
|
-
next: optional(text()),
|
|
1100
|
-
}),
|
|
1101
|
-
run: async (ctx, input) => {
|
|
1102
|
-
const wallet = access(ctx);
|
|
1103
|
-
const owner = (await signerOf(ctx)).address;
|
|
1104
|
-
const [intent] = await findIntentPda({ owner, nonce: input.nonce });
|
|
1105
|
-
const within = input.within ?? WAIT_DEFAULT;
|
|
1106
|
-
const fresh = unmemoized(ctx.transport);
|
|
1107
|
-
const deadline = wallet.now() + BigInt(within);
|
|
1108
|
-
let waited = 0;
|
|
1109
|
-
let pair = null;
|
|
1110
|
-
let status = "closed";
|
|
1111
|
-
for (;;) {
|
|
1112
|
-
const view = await fresh.getAccount(intent);
|
|
1113
|
-
if (view === null) {
|
|
1114
|
-
status = "closed";
|
|
1115
|
-
break;
|
|
1116
|
-
}
|
|
1117
|
-
const decoded = getIntentDecoder().decode(view.data);
|
|
1118
|
-
pair = { inputMint: decoded.inputMint, outputMint: decoded.outputMint };
|
|
1119
|
-
status = STATUS[decoded.status] ?? "open";
|
|
1120
|
-
if ((status !== "open" && status !== "filling") || waited >= within || wallet.now() >= deadline || !(await wallet.sleep(1))) {
|
|
1121
|
-
break;
|
|
1122
|
-
}
|
|
1123
|
-
waited += 1;
|
|
1124
|
-
}
|
|
1125
|
-
if (status === "open" || status === "filling") {
|
|
1126
|
-
return { intent, status, waited, next: stillWaiting(status) };
|
|
1127
|
-
}
|
|
1128
|
-
if (status === "cancelled" || status === "expired") {
|
|
1129
|
-
return { intent, status, waited };
|
|
1130
|
-
}
|
|
1131
|
-
const history = await engineHistory(ctx.transport, WAIT_SCAN, intent);
|
|
1132
|
-
const fills = history.fills.filter((f) => f.intent === intent);
|
|
1133
|
-
if (fills.length === 0) {
|
|
1134
|
-
const ended = status === "closed" ? (history.ended.get(intent) ?? status) : status;
|
|
1135
|
-
return { intent, status: ended, waited, ...(ended === "closed" && history.unread > 0 ? { next: UNREAD } : {}) };
|
|
1136
|
-
}
|
|
1137
|
-
pair ??= history.created.get(intent) ?? null;
|
|
1138
|
-
const decimals = pair === null ? undefined : (await mintOf(ctx.transport, pair.outputMint)).decimals;
|
|
1139
|
-
return {
|
|
1140
|
-
intent,
|
|
1141
|
-
status: "filled",
|
|
1142
|
-
waited,
|
|
1143
|
-
received: formatAmount(fills.reduce((sum, f) => sum + f.delivered, 0n), decimals),
|
|
1144
|
-
...(pair === null ? {} : { token: nameOf(pair.outputMint) }),
|
|
1145
|
-
signature: fills[0]?.signature,
|
|
1146
|
-
};
|
|
1147
|
-
},
|
|
1148
|
-
});
|
|
1149
|
-
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`;
|
|
1150
|
-
const walletReceipt = define({
|
|
1151
|
-
name: tool("wallet_receipt"),
|
|
1152
|
-
title: "Check an escrow's release",
|
|
1153
|
-
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.`,
|
|
1154
|
-
effect: "read",
|
|
1155
|
-
input: object({
|
|
1156
|
-
owner: optional(account("the wallet that made the escrow; default this server's wallet")),
|
|
1157
|
-
nonce: amount("the escrow intent's nonce"),
|
|
1158
|
-
sha256: optional(text("the sha256, as 64 hex digits, of the work you received, to compare with the one the release recorded")),
|
|
1159
|
-
}),
|
|
1160
|
-
output: object({
|
|
1161
|
-
intent: account(),
|
|
1162
|
-
released: flag(),
|
|
1163
|
-
paid_to: optional(account("the wallet the release paid")),
|
|
1164
|
-
payee: optional(account("the token account the release paid")),
|
|
1165
|
-
paid: optional(text("whole tokens the payee received")),
|
|
1166
|
-
attestor: optional(account()),
|
|
1167
|
-
result: optional(amount()),
|
|
1168
|
-
signature: optional(text("the release's transaction")),
|
|
1169
|
-
delivered_sha256: optional(text("the sha256 the release recorded, if any")),
|
|
1170
|
-
matches: optional(flag("whether sha256 equals the recorded one")),
|
|
1171
|
-
next: optional(text()),
|
|
1172
|
-
}),
|
|
1173
|
-
run: async (ctx, input) => {
|
|
1174
|
-
const owner = input.owner ?? (await signerOf(ctx)).address;
|
|
1175
|
-
const wanted = input.sha256 === undefined ? undefined : sha256Of(input.sha256, "sha256");
|
|
1176
|
-
const [intent] = await findIntentPda({ owner, nonce: input.nonce });
|
|
1177
|
-
const transactions = await ctx.transport.engineTransactions(WAIT_SCAN, intent);
|
|
1178
|
-
let mint;
|
|
1179
|
-
for (const tx of transactions) {
|
|
1180
|
-
for (const event of tx.emitted.map(decodeEvent)) {
|
|
1181
|
-
const fields = (event?.fields ?? {});
|
|
1182
|
-
if (event?.name === "IntentCreated" && fields["intent"] === intent) {
|
|
1183
|
-
mint = fields["inputMint"];
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
for (const tx of transactions) {
|
|
1188
|
-
if (tx.failed) {
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
const release = tx.emitted.map(decodeEvent).find((event) => event?.name === "IntentAttested" && event.fields["intent"] === intent);
|
|
1192
|
-
if (release === undefined || release === null) {
|
|
1193
|
-
continue;
|
|
1194
|
-
}
|
|
1195
|
-
const fields = release.fields;
|
|
1196
|
-
const payee = fields["payee"];
|
|
1197
|
-
const held = await ctx.transport.getAccount(payee);
|
|
1198
|
-
mint ??= held === null || held.data.length < 64 ? undefined : getAddressDecoder().decode(held.data.subarray(0, 32));
|
|
1199
|
-
const decimals = mint === undefined ? undefined : (await mintOf(ctx.transport, mint)).decimals;
|
|
1200
|
-
const recorded = (tx.memos ?? []).map((memo) => DELIVERY_MEMO.exec(memo)?.[1]).find((hash) => hash !== undefined);
|
|
1201
|
-
return {
|
|
1202
|
-
intent,
|
|
1203
|
-
released: true,
|
|
1204
|
-
...(held === null || held.data.length < 64 ? {} : { paid_to: getAddressDecoder().decode(held.data.subarray(32, 64)) }),
|
|
1205
|
-
payee,
|
|
1206
|
-
paid: formatAmount(fields["net"], decimals),
|
|
1207
|
-
attestor: fields["attestor"],
|
|
1208
|
-
result: fields["result"],
|
|
1209
|
-
signature: tx.signature,
|
|
1210
|
-
...(recorded === undefined ? {} : { delivered_sha256: recorded }),
|
|
1211
|
-
...(wanted === undefined ? {} : { matches: wanted === recorded }),
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
return { intent, released: false, next: NOT_RELEASED };
|
|
1215
|
-
},
|
|
1216
|
-
});
|
|
1
|
+
import { walletSetup, walletBalance, walletIntents, walletCancel } from "./wallet/account.js";
|
|
2
|
+
import { walletEstimate, walletSwap, walletAuction, walletRecurring } from "./wallet/trade.js";
|
|
3
|
+
import { walletEscrow, walletEscrowPlan } from "./wallet/escrow.js";
|
|
4
|
+
import { walletAttest } from "./wallet/attest.js";
|
|
5
|
+
import { walletDeliver, walletCheckDelivery } from "./wallet/delivery.js";
|
|
6
|
+
import { walletAttestSplit } from "./wallet/split.js";
|
|
7
|
+
import { walletRecordDelivery } from "./wallet/record.js";
|
|
8
|
+
import { walletWait } from "./wallet/wait.js";
|
|
9
|
+
import { walletReceipt } from "./wallet/receipt.js";
|
|
10
|
+
export { walletGuard, SALE_WINDOW, ESTIMATE_SCAN, ESTIMATE_FILLS, WAIT_DEFAULT, WAIT_MAX, WAIT_SCAN, DEFAULT_EXPIRES_IN, LANDING_SECONDS, MAINNET_GENESIS, WALLET_NOTICE, walletEmpty, NO_WALLET, WALLET_OFF, PREVIEW_ONLY, limitLines, rpcRefused, } from "./wallet/guard.js";
|
|
11
|
+
export { walletBalance } from "./wallet/account.js";
|
|
12
|
+
export { nonceTaken } from "./wallet/offer.js";
|
|
13
|
+
export { SWAP_HEADROOM_BPS, SWAP_BLIND_BPS, SWAP_CAP_BPS } from "./wallet/fills.js";
|
|
14
|
+
export { SWAP_FALLS_OVER, PAY_TO, MAX_RECURRING, mayHaveLanded, partlySent, WARN_EARLY, NO_FALL, MAX_EVERY, ESTIMATE_NEXT, NO_FILLS, } from "./wallet/trade.js";
|
|
15
|
+
export { ESCROW_EXPIRES_IN, RECORD_MINT_NEEDED, MAX_PIECES, piecesPartlySent, piecesMayHaveLanded } from "./wallet/escrow.js";
|
|
16
|
+
export { PAYEE_MOVED, RELEASE_CHANGED, approvalOf } from "./wallet/attest.js";
|
|
17
|
+
export { notEarliest, notInSplit, DELIVERY_LIST, DELIVERED_NEEDED, ATTEST_PAGES, MAX_ATTEST_PAGES, HISTORY_TOO_LONG, checkDelivery, prepareDeliver, notAttestor, } from "./wallet/delivery.js";
|
|
18
|
+
export { SPLIT_FITS, SPLIT_FITS_WITH_TABLE, splitTooLarge, RECORDS_SPLIT, followUpFailed, SPLIT_WEIGHTS_WRONG, SPLIT_SIG_NEEDED, SIGNATURE_PATTERN, WARN_COAUTHORS, } from "./wallet/split.js";
|
|
19
|
+
export { RELEASE_INPUT, RELEASE_SIGNATURE, releaseOf, notReleasedYet, alreadyRecorded, releasedBy, escrowGone, creationUnread, notAProofEscrow, neverAnnounced, paidOther, splitPaidOthers, recipientsUnread, followUpsUnknown, historyLagging, otherLifecycle, COVERAGE_UNPROVABLE, coverageOf, recordingOf, } from "./wallet/record.js";
|
|
20
|
+
export { releaseNotOf, releaseUnread, HISTORY_PAGES, escrowHistory, RELEASE_PAGES, RELEASE_UNKNOWN, LIFECYCLE_FETCHES, outlives, releaseLookup, aroundRelease, namedRelease, releasedMint, SPLIT_INTENT_ACCOUNT, splitRecipients, } from "./wallet/releases.js";
|
|
21
|
+
export { WAIT_STATUS, UNREAD, FILL_UNKNOWN, stillWaiting } from "./wallet/wait.js";
|
|
22
|
+
export { NOT_RELEASED, RECEIPT_PARTIAL, getReceipt, escrowReceipt } from "./wallet/receipt.js";
|
|
23
|
+
export { PACKET_DATA_SIZE } from "@intent-engine/client";
|
|
1217
24
|
export const WALLET_ACTIONS = [
|
|
1218
25
|
walletSetup,
|
|
1219
26
|
walletBalance,
|
|
@@ -1225,6 +32,9 @@ export const WALLET_ACTIONS = [
|
|
|
1225
32
|
walletEscrowPlan,
|
|
1226
33
|
walletAttest,
|
|
1227
34
|
walletAttestSplit,
|
|
35
|
+
walletRecordDelivery,
|
|
36
|
+
walletDeliver,
|
|
37
|
+
walletCheckDelivery,
|
|
1228
38
|
walletWait,
|
|
1229
39
|
walletReceipt,
|
|
1230
40
|
walletIntents,
|