@dfm-fi/agent 0.2.2065 → 0.2.2084
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/api-client.d.ts +4 -4
- package/dist/api-client.js +11 -8
- package/dist/mcp-server.js +9 -6
- package/dist/types.d.ts +4 -0
- package/dist/zap-v2-flow.d.ts +3 -1
- package/dist/zap-v2-flow.js +38 -30
- package/package.json +1 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export declare class DfmApiClient {
|
|
|
78
78
|
* null. The session wallet must equal `user` (or be the vault admin), else 403.
|
|
79
79
|
* Returns `null` when the user has no open escrow.
|
|
80
80
|
*/
|
|
81
|
-
getZapV2Status(vaultAddress: string, user: string): Promise<ZapV2Status | null>;
|
|
81
|
+
getZapV2Status(vaultAddress: string, user: string, escrowSlot?: number): Promise<ZapV2Status | null>;
|
|
82
82
|
/**
|
|
83
83
|
* `GET /portfolio` (auth) — the API reads the wallet from the JWT; there is NO
|
|
84
84
|
* portfolio-by-arbitrary-address endpoint. So this returns the SESSION
|
|
@@ -287,7 +287,7 @@ export declare class DfmApiClient {
|
|
|
287
287
|
expirySecs?: number;
|
|
288
288
|
}): Promise<ZapV2PrepareResponse>;
|
|
289
289
|
/** `POST /vaults/:address/zap-in-v2/close/prepare` — CPI mint_in_kind_v2. */
|
|
290
|
-
prepareZapInCloseV2(vaultAddress: string): Promise<ZapV2PrepareResponse>;
|
|
290
|
+
prepareZapInCloseV2(vaultAddress: string, escrowSlot?: number): Promise<ZapV2PrepareResponse>;
|
|
291
291
|
/**
|
|
292
292
|
* `POST /tx/submit` (#18) — hand a SIGNED prepared tx back to the backend so
|
|
293
293
|
* its premium RPC broadcasts it. Used when no client RPC is configured (the
|
|
@@ -305,13 +305,13 @@ export declare class DfmApiClient {
|
|
|
305
305
|
expirySecs?: number;
|
|
306
306
|
}): Promise<ZapV2PrepareResponse>;
|
|
307
307
|
/** `POST /vaults/:address/zap-out-v2/close/prepare` — exit fee, pay net USDC. */
|
|
308
|
-
prepareZapOutCloseV2(vaultAddress: string): Promise<ZapV2PrepareResponse>;
|
|
308
|
+
prepareZapOutCloseV2(vaultAddress: string, escrowSlot?: number): Promise<ZapV2PrepareResponse>;
|
|
309
309
|
/**
|
|
310
310
|
* `POST /vaults/:address/zap-v2/cancel/prepare` — return the escrow's current
|
|
311
311
|
* USDC + per-asset balances to the test wallet as-is (no swaps, no oracle),
|
|
312
312
|
* then close it. 404 server-side when no escrow is open. Single v0 tx.
|
|
313
313
|
*/
|
|
314
|
-
prepareZapCancelV2(vaultAddress: string): Promise<ZapV2SingleTxResponse>;
|
|
314
|
+
prepareZapCancelV2(vaultAddress: string, escrowSlot?: number): Promise<ZapV2SingleTxResponse>;
|
|
315
315
|
/**
|
|
316
316
|
* `POST /vaults/:address/zap-v2/update-envelope/prepare` — LOOSEN the pinned
|
|
317
317
|
* per-leg floors + slippage (and optionally extend expiry) so a stalled escrow
|
package/dist/api-client.js
CHANGED
|
@@ -219,10 +219,13 @@ export class DfmApiClient {
|
|
|
219
219
|
* null. The session wallet must equal `user` (or be the vault admin), else 403.
|
|
220
220
|
* Returns `null` when the user has no open escrow.
|
|
221
221
|
*/
|
|
222
|
-
async getZapV2Status(vaultAddress, user) {
|
|
222
|
+
async getZapV2Status(vaultAddress, user, escrowSlot) {
|
|
223
223
|
requireBase58(vaultAddress, 'vault address');
|
|
224
224
|
requireBase58(user, 'user wallet');
|
|
225
|
-
|
|
225
|
+
// A wallet can hold up to 8 escrows per fund; without a slot the API
|
|
226
|
+
// returns the oldest open one.
|
|
227
|
+
const q = escrowSlot === undefined ? '' : `?slot=${escrowSlot}`;
|
|
228
|
+
return this.authRequest(`/vaults/${vaultAddress}/zap-v2/status/${user}${q}`);
|
|
226
229
|
}
|
|
227
230
|
/**
|
|
228
231
|
* `GET /portfolio` (auth) — the API reads the wallet from the JWT; there is NO
|
|
@@ -490,9 +493,9 @@ export class DfmApiClient {
|
|
|
490
493
|
return this.authRequest(`/vaults/${vaultAddress}/zap-in-v2/open/prepare`, { method: 'POST', body: JSON.stringify({ usdcAmount: usdcAmountRaw, ...opts }) });
|
|
491
494
|
}
|
|
492
495
|
/** `POST /vaults/:address/zap-in-v2/close/prepare` — CPI mint_in_kind_v2. */
|
|
493
|
-
async prepareZapInCloseV2(vaultAddress) {
|
|
496
|
+
async prepareZapInCloseV2(vaultAddress, escrowSlot) {
|
|
494
497
|
requireBase58(vaultAddress, 'vault address');
|
|
495
|
-
return this.authRequest(`/vaults/${vaultAddress}/zap-in-v2/close/prepare`, { method: 'POST', body: JSON.stringify({}) });
|
|
498
|
+
return this.authRequest(`/vaults/${vaultAddress}/zap-in-v2/close/prepare`, { method: 'POST', body: JSON.stringify(escrowSlot === undefined ? {} : { escrowSlot }) });
|
|
496
499
|
}
|
|
497
500
|
/**
|
|
498
501
|
* `POST /tx/submit` (#18) — hand a SIGNED prepared tx back to the backend so
|
|
@@ -518,18 +521,18 @@ export class DfmApiClient {
|
|
|
518
521
|
return this.authRequest(`/vaults/${vaultAddress}/zap-out-v2/open/prepare`, { method: 'POST', body: JSON.stringify({ shares: sharesRaw, ...opts }) });
|
|
519
522
|
}
|
|
520
523
|
/** `POST /vaults/:address/zap-out-v2/close/prepare` — exit fee, pay net USDC. */
|
|
521
|
-
async prepareZapOutCloseV2(vaultAddress) {
|
|
524
|
+
async prepareZapOutCloseV2(vaultAddress, escrowSlot) {
|
|
522
525
|
requireBase58(vaultAddress, 'vault address');
|
|
523
|
-
return this.authRequest(`/vaults/${vaultAddress}/zap-out-v2/close/prepare`, { method: 'POST', body: JSON.stringify({}) });
|
|
526
|
+
return this.authRequest(`/vaults/${vaultAddress}/zap-out-v2/close/prepare`, { method: 'POST', body: JSON.stringify(escrowSlot === undefined ? {} : { escrowSlot }) });
|
|
524
527
|
}
|
|
525
528
|
/**
|
|
526
529
|
* `POST /vaults/:address/zap-v2/cancel/prepare` — return the escrow's current
|
|
527
530
|
* USDC + per-asset balances to the test wallet as-is (no swaps, no oracle),
|
|
528
531
|
* then close it. 404 server-side when no escrow is open. Single v0 tx.
|
|
529
532
|
*/
|
|
530
|
-
async prepareZapCancelV2(vaultAddress) {
|
|
533
|
+
async prepareZapCancelV2(vaultAddress, escrowSlot) {
|
|
531
534
|
requireBase58(vaultAddress, 'vault address');
|
|
532
|
-
return this.authRequest(`/vaults/${vaultAddress}/zap-v2/cancel/prepare`, { method: 'POST', body: JSON.stringify({}) });
|
|
535
|
+
return this.authRequest(`/vaults/${vaultAddress}/zap-v2/cancel/prepare`, { method: 'POST', body: JSON.stringify(escrowSlot === undefined ? {} : { escrowSlot }) });
|
|
533
536
|
}
|
|
534
537
|
/**
|
|
535
538
|
* `POST /vaults/:address/zap-v2/update-envelope/prepare` — LOOSEN the pinned
|
package/dist/mcp-server.js
CHANGED
|
@@ -419,15 +419,16 @@ server.registerTool('zap_status', {
|
|
|
419
419
|
inputSchema: {
|
|
420
420
|
vault: z.string().regex(BASE58, 'Invalid base58 vault address'),
|
|
421
421
|
user: z.string().regex(BASE58, 'Invalid base58 user wallet').optional(),
|
|
422
|
+
escrowSlot: z.number().int().min(0).max(7).optional().describe('Escrow slot 0-7 (from zap_status). A wallet can hold several escrows per fund; omit for the oldest open one.'),
|
|
422
423
|
},
|
|
423
424
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
424
|
-
}, async ({ vault, user }) => {
|
|
425
|
+
}, async ({ vault, user, escrowSlot }) => {
|
|
425
426
|
try {
|
|
426
427
|
const identity = await resolveWalletIdentity();
|
|
427
428
|
const escrowOwner = resolveZapStatusWallet(identity, user);
|
|
428
429
|
if (!escrowOwner)
|
|
429
430
|
return errText('zap_status could not resolve this agent wallet');
|
|
430
|
-
const res = await api.getZapV2Status(vault, escrowOwner);
|
|
431
|
+
const res = await api.getZapV2Status(vault, escrowOwner, escrowSlot);
|
|
431
432
|
return text(res ?? { escrow: null, note: 'No open zap-v2 escrow for this vault+user.' });
|
|
432
433
|
}
|
|
433
434
|
catch (e) {
|
|
@@ -1216,19 +1217,20 @@ server.registerTool('zap_v2_cancel', {
|
|
|
1216
1217
|
'just want your funds back. Returns the held balances + the cancel signature.',
|
|
1217
1218
|
inputSchema: {
|
|
1218
1219
|
vault: z.string().regex(BASE58, 'Invalid base58 vault address'),
|
|
1220
|
+
escrowSlot: z.number().int().min(0).max(7).optional().describe('Escrow slot 0-7 (from zap_status). A wallet can hold several escrows per fund; omit for the oldest open one.'),
|
|
1219
1221
|
confirmPhrase: confirmPhraseField,
|
|
1220
1222
|
},
|
|
1221
1223
|
annotations: { title: 'DFM zap-v2 cancel', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1222
1224
|
},
|
|
1223
1225
|
// REVIEW: real-money path — parent must review before enabling.
|
|
1224
|
-
async ({ vault, confirmPhrase }) => {
|
|
1226
|
+
async ({ vault, escrowSlot, confirmPhrase }) => {
|
|
1225
1227
|
const gate = confirmGate('zap_v2_cancel', confirmPhrase);
|
|
1226
1228
|
if (!gate.ok)
|
|
1227
1229
|
return gate.result;
|
|
1228
1230
|
const write = writeGate();
|
|
1229
1231
|
if (!write.ok)
|
|
1230
1232
|
return write.result;
|
|
1231
|
-
return keylessWrite('zap_v2_cancel', { vault });
|
|
1233
|
+
return keylessWrite('zap_v2_cancel', { vault, escrowSlot });
|
|
1232
1234
|
});
|
|
1233
1235
|
server.registerTool('zap_v2_update_envelope', {
|
|
1234
1236
|
title: 'Loosen pinned floors on a stalled zap-v2 escrow',
|
|
@@ -1239,6 +1241,7 @@ server.registerTool('zap_v2_update_envelope', {
|
|
|
1239
1241
|
'slippage and ≤1000 (onchain monotonic-loosening). Optionally extend expiry. ' +
|
|
1240
1242
|
'Returns the exact loosening applied (old→new). If it still will not fill, cancel.',
|
|
1241
1243
|
inputSchema: {
|
|
1244
|
+
escrowSlot: z.number().int().min(0).max(7).optional().describe('Escrow slot 0-7 (from zap_status). A wallet can hold several escrows per fund; omit for the oldest open one.'),
|
|
1242
1245
|
vault: z.string().regex(BASE58, 'Invalid base58 vault address'),
|
|
1243
1246
|
loosenPct: z.number().min(0).max(50).optional(),
|
|
1244
1247
|
newSlippageBps: z.number().int().min(0).max(1000).optional(),
|
|
@@ -1248,14 +1251,14 @@ server.registerTool('zap_v2_update_envelope', {
|
|
|
1248
1251
|
annotations: { title: 'DFM zap-v2 update-envelope', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1249
1252
|
},
|
|
1250
1253
|
// REVIEW: real-money path — parent must review before enabling.
|
|
1251
|
-
async ({ vault, loosenPct, newSlippageBps, extendSecs, confirmPhrase }) => {
|
|
1254
|
+
async ({ vault, loosenPct, newSlippageBps, extendSecs, escrowSlot, confirmPhrase }) => {
|
|
1252
1255
|
const gate = confirmGate('zap_v2_update_envelope', confirmPhrase);
|
|
1253
1256
|
if (!gate.ok)
|
|
1254
1257
|
return gate.result;
|
|
1255
1258
|
const write = writeGate();
|
|
1256
1259
|
if (!write.ok)
|
|
1257
1260
|
return write.result;
|
|
1258
|
-
return keylessWrite('zap_v2_update_envelope', { vault, loosenPct, newSlippageBps, extendSecs });
|
|
1261
|
+
return keylessWrite('zap_v2_update_envelope', { vault, loosenPct, newSlippageBps, extendSecs, escrowSlot });
|
|
1259
1262
|
});
|
|
1260
1263
|
// ─── WRITE TOOL: update_vault_assets (config-only, same semantics in both lanes) ─
|
|
1261
1264
|
server.registerTool('update_vault_assets', {
|
package/dist/types.d.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface VaultDetailResponse {
|
|
|
71
71
|
*/
|
|
72
72
|
export interface ZapV2Status {
|
|
73
73
|
escrowPda: string;
|
|
74
|
+
/** Escrow slot 0..7 — a wallet can hold several escrows per fund. */
|
|
75
|
+
escrowSlot?: number;
|
|
74
76
|
vault: string;
|
|
75
77
|
user: string;
|
|
76
78
|
mode: number;
|
|
@@ -111,6 +113,8 @@ export interface ZapV2PrepareResponse {
|
|
|
111
113
|
lastValidBlockHeight: number;
|
|
112
114
|
computeUnits: number;
|
|
113
115
|
escrowPda: string;
|
|
116
|
+
/** Escrow slot (0..7) the tx acts on. */
|
|
117
|
+
escrowSlot?: number;
|
|
114
118
|
/** zap-out OPEN additionally returns these (redeem economics). */
|
|
115
119
|
minUsdcOut?: string;
|
|
116
120
|
expectedUsdc?: string;
|
package/dist/zap-v2-flow.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface ZapFlowResult {
|
|
|
44
44
|
* #61 — 'complete' = escrow closed/settled (shares minted / USDC paid).
|
|
45
45
|
* 'pending' = the poll window elapsed but the escrow is STILL OPEN and
|
|
46
46
|
* self-healing (the backend orchestrator keeps cranking) — this is NOT a
|
|
47
|
-
* failure; call
|
|
47
|
+
* failure; call again with the escrow's `escrowSlot` to RESUME polling to close.
|
|
48
48
|
*/
|
|
49
49
|
settlement: 'complete' | 'pending';
|
|
50
50
|
openSignatures: string[];
|
|
@@ -75,6 +75,7 @@ export declare function runDeposit(config: AgentConfig, api: DfmApiClient, user:
|
|
|
75
75
|
slippageBps?: number;
|
|
76
76
|
expirySecs?: number;
|
|
77
77
|
poll?: Partial<PollOptions>;
|
|
78
|
+
escrowSlot?: number;
|
|
78
79
|
}): Promise<ZapFlowResult>;
|
|
79
80
|
/**
|
|
80
81
|
* Drive a full REDEEM (shares → basket → USDC). `sharesRaw` is 6-dp raw units.
|
|
@@ -84,6 +85,7 @@ export declare function runRedeem(config: AgentConfig, api: DfmApiClient, user:
|
|
|
84
85
|
slippageBps?: number;
|
|
85
86
|
expirySecs?: number;
|
|
86
87
|
poll?: Partial<PollOptions>;
|
|
88
|
+
escrowSlot?: number;
|
|
87
89
|
}): Promise<ZapFlowResult>;
|
|
88
90
|
export {};
|
|
89
91
|
//# sourceMappingURL=zap-v2-flow.d.ts.map
|
package/dist/zap-v2-flow.js
CHANGED
|
@@ -28,21 +28,22 @@ async function submitOpen(config, prepared, kind, api) {
|
|
|
28
28
|
catch (e) {
|
|
29
29
|
if (e instanceof BlockhashExpiredError) {
|
|
30
30
|
throw new Error(`The ${kind} OPEN tx did not confirm before its blockhash expired. Check zap_status: ` +
|
|
31
|
-
`if an escrow now EXISTS, the open landed —
|
|
32
|
-
`
|
|
31
|
+
`if an escrow now EXISTS in slot ${prepared.escrowSlot ?? 0}, the open landed — re-run this ${kind} ` +
|
|
32
|
+
`with escrowSlot=${prepared.escrowSlot ?? 0} to resume it; if NO escrow exists, nothing happened and it is ` +
|
|
33
33
|
`safe to re-run the ${kind}. Original: ${e.message}`);
|
|
34
34
|
}
|
|
35
35
|
// Backend broadcast the OPEN but it did NOT confirm inside the API window. The
|
|
36
36
|
// open pulls funds + Anchor-`init`s the escrow, so it MAY still land — a blind
|
|
37
37
|
// re-run could double-pull before the escrow becomes visible. Do NOT re-run
|
|
38
38
|
// immediately: verify the broadcast signature on-chain first, THEN use
|
|
39
|
-
// zap_status (
|
|
39
|
+
// zap_status (resume = re-run with the escrowSlot of this open, so a
|
|
40
40
|
// re-run is only safe once that escrow is visible OR the open is confirmed dead).
|
|
41
41
|
if (e instanceof BroadcastUnconfirmedError) {
|
|
42
42
|
throw new Error(`The ${kind} OPEN tx ${e.signature} was broadcast but did NOT confirm in time — it may ` +
|
|
43
43
|
`still land. Do NOT blindly re-run (the open pulls funds + creates the escrow; a duplicate ` +
|
|
44
44
|
`could double-pull). First verify ${e.signature} onchain: if it LANDED, an escrow now ` +
|
|
45
|
-
`exists — re-run this ${kind} to resume (poll → close);
|
|
45
|
+
`exists — re-run this ${kind} with escrowSlot=${prepared.escrowSlot ?? 0} to resume (poll → close); ` +
|
|
46
|
+
`a re-run WITHOUT it opens a second ${kind}. If it truly DROPPED (sig not found), ` +
|
|
46
47
|
`it is safe to re-run the ${kind}. Original: ${e.message}`);
|
|
47
48
|
}
|
|
48
49
|
throw e;
|
|
@@ -110,12 +111,12 @@ async function closeWithRetry(config, api, prepareClose, confirmClosed, attempts
|
|
|
110
111
|
* surface a resume-able `pending` instead of a misleading hard error (a redeem
|
|
111
112
|
* that out-runs the poll window is still self-healing, not failed).
|
|
112
113
|
*/
|
|
113
|
-
async function pollUntilReady(api, vault, user, poll) {
|
|
114
|
+
async function pollUntilReady(api, vault, user, poll, escrowSlot) {
|
|
114
115
|
const deadline = Date.now() + poll.timeoutSecs * 1000;
|
|
115
116
|
let last = null;
|
|
116
117
|
let sawOpen = false; // have we observed the escrow OPEN at least once this poll?
|
|
117
118
|
while (Date.now() < deadline) {
|
|
118
|
-
last = await api.getZapV2Status(vault, user);
|
|
119
|
+
last = await api.getZapV2Status(vault, user, escrowSlot);
|
|
119
120
|
if (!last) {
|
|
120
121
|
// A null read is ambiguous: (a) the escrow SETTLED + closed (the backend
|
|
121
122
|
// keeper finished it — the #61 success case), OR (b) a freshly-opened escrow
|
|
@@ -139,10 +140,13 @@ async function pollUntilReady(api, vault, user, poll) {
|
|
|
139
140
|
return { ready: false, reason: 'timeout', status: last };
|
|
140
141
|
}
|
|
141
142
|
/** Build the non-fatal `pending` ZapFlowResult (#61) — escrow still open / self-healing. */
|
|
142
|
-
function pendingResult(kind, vault, escrowPda, openSignatures, pr) {
|
|
143
|
+
function pendingResult(kind, vault, escrowPda, openSignatures, pr, escrowSlot) {
|
|
143
144
|
const legsFilled = pr.status?.legsFilled ?? 0;
|
|
144
145
|
const legsTotal = pr.status?.legsTotal ?? 0;
|
|
145
146
|
const verb = kind === 'redeem' ? 'redeem' : 'deposit';
|
|
147
|
+
// Without the slot a repeat call OPENS A NEW escrow (several per fund are
|
|
148
|
+
// allowed) — resuming this one needs it.
|
|
149
|
+
const again = `call ${verb} again with escrowSlot=${escrowSlot ?? 0}`;
|
|
146
150
|
const why = pr.reason === 'gone'
|
|
147
151
|
? `The escrow is no longer open — most likely it SETTLED (the backend keeper closed it ` +
|
|
148
152
|
`after cranking the legs) or was cancelled. Verify with get_portfolio / your balances ` +
|
|
@@ -150,10 +154,10 @@ function pendingResult(kind, vault, escrowPda, openSignatures, pr) {
|
|
|
150
154
|
: pr.reason === 'expired'
|
|
151
155
|
? `The escrow passed its expiry with ${legsFilled}/${legsTotal} legs filled. The backend ` +
|
|
152
156
|
`stall-recovery may STILL settle it automatically (it loosens floors + extends expiry) — ` +
|
|
153
|
-
|
|
157
|
+
`${again} to resume polling. If it stays stuck, use ` +
|
|
154
158
|
`zap_v2_update_envelope (loosen floors) or zap_v2_cancel (recover holdings).`
|
|
155
159
|
: `Still cranking: ${legsFilled}/${legsTotal} legs filled. The escrow is OPEN and ` +
|
|
156
|
-
`self-healing — this is NOT a failure.
|
|
160
|
+
`self-healing — this is NOT a failure. To RESUME polling, ${again} ` +
|
|
157
161
|
`to completion (consider a larger pollTimeoutSecs), or inspect with zap_status.`;
|
|
158
162
|
return {
|
|
159
163
|
ok: true,
|
|
@@ -175,49 +179,50 @@ function pendingResult(kind, vault, escrowPda, openSignatures, pr) {
|
|
|
175
179
|
*/
|
|
176
180
|
export async function runDeposit(config, api, user, vault, usdcAmountRaw, opts) {
|
|
177
181
|
const poll = { ...DEFAULT_POLL, ...(opts?.poll ?? {}) };
|
|
178
|
-
// 0. RESUME
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
// 0. RESUME only when asked: a wallet may hold several escrows per fund, so a
|
|
183
|
+
// plain call always OPENS a new one. `opts.escrowSlot` resumes that escrow
|
|
184
|
+
// (poll → close) — the idempotent retry after a pending/unconfirmed open.
|
|
185
|
+
const existing = opts?.escrowSlot === undefined ? null : await api.getZapV2Status(vault, user, opts.escrowSlot);
|
|
186
|
+
if (opts?.escrowSlot !== undefined && !existing) {
|
|
187
|
+
throw new Error(`No open escrow in slot ${opts.escrowSlot} for this vault+wallet — nothing to resume.`);
|
|
188
|
+
}
|
|
183
189
|
let openSignatures = [];
|
|
184
190
|
let escrowPda;
|
|
191
|
+
let escrowSlot = opts?.escrowSlot;
|
|
185
192
|
// Set when resuming an open escrow whose pinned input amount differs from the
|
|
186
193
|
// amount this call requested (the requested amount is NOT honored on resume).
|
|
187
194
|
let resumedPinnedAmount = null;
|
|
188
195
|
if (existing) {
|
|
189
196
|
if (existing.mode !== 0) {
|
|
190
|
-
throw new Error(`
|
|
191
|
-
`(zap_v2_cancel / redeem) before depositing.`);
|
|
197
|
+
throw new Error(`Slot ${opts?.escrowSlot} holds a REDEEM escrow, not a deposit.`);
|
|
192
198
|
}
|
|
193
199
|
escrowPda = existing.escrowPda;
|
|
194
200
|
// The existing escrow's input amount was PINNED at its open; resuming
|
|
195
201
|
// completes THAT amount and ignores the `usdcAmountRaw` just requested.
|
|
196
|
-
// Surface the mismatch loudly so a caller acting on the requested size
|
|
197
|
-
// isn't misled into thinking a different amount was deposited.
|
|
198
202
|
if (existing.inputAmount && existing.inputAmount !== usdcAmountRaw) {
|
|
199
203
|
resumedPinnedAmount = existing.inputAmount;
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
else {
|
|
203
|
-
// 1. OPEN — pull USDC into escrow, pin the plan.
|
|
207
|
+
// 1. OPEN — pull USDC into escrow, pin the plan (the API picks a free slot).
|
|
204
208
|
const open = await api.prepareZapInOpenV2(vault, usdcAmountRaw, {
|
|
205
209
|
slippageBps: opts?.slippageBps,
|
|
206
210
|
expirySecs: opts?.expirySecs,
|
|
207
211
|
});
|
|
212
|
+
escrowSlot = open.escrowSlot;
|
|
208
213
|
openSignatures = await submitOpen(config, open, 'deposit', api);
|
|
209
214
|
escrowPda = open.escrowPda;
|
|
210
215
|
}
|
|
211
216
|
// 2. POLL — wait for the backend orchestrator to crank every USDC→asset leg.
|
|
212
217
|
// #61: on timeout/expiry/auto-settle, return a resume-able `pending` rather
|
|
213
218
|
// than throwing — the escrow keeps self-healing.
|
|
214
|
-
const pr = await pollUntilReady(api, vault, user, poll);
|
|
219
|
+
const pr = await pollUntilReady(api, vault, user, poll, escrowSlot);
|
|
215
220
|
if (!pr.ready)
|
|
216
|
-
return pendingResult('deposit', vault, escrowPda, openSignatures, pr);
|
|
221
|
+
return pendingResult('deposit', vault, escrowPda, openSignatures, pr, escrowSlot);
|
|
217
222
|
const ready = pr.status;
|
|
218
223
|
// 3. CLOSE — mint_in_kind_v2, forward shares, close escrow. Retries on
|
|
219
224
|
// blockhash expiry (escrow stays READY between attempts).
|
|
220
|
-
const closeSignatures = await closeWithRetry(config, api, () => api.prepareZapInCloseV2(vault), async () => !(await api.getZapV2Status(vault, user)));
|
|
225
|
+
const closeSignatures = await closeWithRetry(config, api, () => api.prepareZapInCloseV2(vault, escrowSlot), async () => !(await api.getZapV2Status(vault, user, escrowSlot)));
|
|
221
226
|
return {
|
|
222
227
|
ok: true,
|
|
223
228
|
vault,
|
|
@@ -242,19 +247,21 @@ export async function runDeposit(config, api, user, vault, usdcAmountRaw, opts)
|
|
|
242
247
|
*/
|
|
243
248
|
export async function runRedeem(config, api, user, vault, sharesRaw, opts) {
|
|
244
249
|
const poll = { ...DEFAULT_POLL, ...(opts?.poll ?? {}) };
|
|
245
|
-
// 0. RESUME
|
|
246
|
-
|
|
247
|
-
|
|
250
|
+
// 0. RESUME only when asked (see runDeposit): a plain call opens a new escrow.
|
|
251
|
+
const existing = opts?.escrowSlot === undefined ? null : await api.getZapV2Status(vault, user, opts.escrowSlot);
|
|
252
|
+
if (opts?.escrowSlot !== undefined && !existing) {
|
|
253
|
+
throw new Error(`No open escrow in slot ${opts.escrowSlot} for this vault+wallet — nothing to resume.`);
|
|
254
|
+
}
|
|
248
255
|
let openSignatures = [];
|
|
249
256
|
let escrowPda;
|
|
257
|
+
let escrowSlot = opts?.escrowSlot;
|
|
250
258
|
// Set when resuming an open escrow whose pinned share amount differs from
|
|
251
259
|
// this call. The existing escrow wins; opening it already burned shares and
|
|
252
260
|
// fixed `sharesIn`, so the new request cannot resize the in-flight redeem.
|
|
253
261
|
let resumedPinnedShares = null;
|
|
254
262
|
if (existing) {
|
|
255
263
|
if (existing.mode !== 1) {
|
|
256
|
-
throw new Error(`
|
|
257
|
-
`(deposit / zap_v2_cancel) before redeeming.`);
|
|
264
|
+
throw new Error(`Slot ${opts?.escrowSlot} holds a DEPOSIT escrow, not a redeem.`);
|
|
258
265
|
}
|
|
259
266
|
escrowPda = existing.escrowPda;
|
|
260
267
|
if (existing.sharesIn && existing.sharesIn !== sharesRaw) {
|
|
@@ -267,19 +274,20 @@ export async function runRedeem(config, api, user, vault, sharesRaw, opts) {
|
|
|
267
274
|
slippageBps: opts?.slippageBps,
|
|
268
275
|
expirySecs: opts?.expirySecs,
|
|
269
276
|
});
|
|
277
|
+
escrowSlot = open.escrowSlot;
|
|
270
278
|
openSignatures = await submitOpen(config, open, 'redeem', api);
|
|
271
279
|
escrowPda = open.escrowPda;
|
|
272
280
|
}
|
|
273
281
|
// 2. POLL — wait for the orchestrator to crank every asset→USDC leg.
|
|
274
282
|
// #61: a slow/retrying leg (e.g. ORCA→USDC) can self-heal over many minutes;
|
|
275
283
|
// on timeout/expiry/auto-settle return a resume-able `pending`, not an error.
|
|
276
|
-
const pr = await pollUntilReady(api, vault, user, poll);
|
|
284
|
+
const pr = await pollUntilReady(api, vault, user, poll, escrowSlot);
|
|
277
285
|
if (!pr.ready)
|
|
278
|
-
return pendingResult('redeem', vault, escrowPda, openSignatures, pr);
|
|
286
|
+
return pendingResult('redeem', vault, escrowPda, openSignatures, pr, escrowSlot);
|
|
279
287
|
const ready = pr.status;
|
|
280
288
|
// 3. CLOSE — apply exit fee, pay net USDC, close escrow. Retries on blockhash
|
|
281
289
|
// expiry (escrow stays READY between attempts).
|
|
282
|
-
const closeSignatures = await closeWithRetry(config, api, () => api.prepareZapOutCloseV2(vault), async () => !(await api.getZapV2Status(vault, user)));
|
|
290
|
+
const closeSignatures = await closeWithRetry(config, api, () => api.prepareZapOutCloseV2(vault, escrowSlot), async () => !(await api.getZapV2Status(vault, user, escrowSlot)));
|
|
283
291
|
return {
|
|
284
292
|
ok: true,
|
|
285
293
|
vault,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfm-fi/agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2084",
|
|
4
4
|
"description": "DFM v2 MCP server — drive the full fund launch → deposit → redeem (+ manage) lifecycle from an AI agent against the live API.",
|
|
5
5
|
"homepage": "https://app.dfm.finance/docs/agent",
|
|
6
6
|
"mcpName": "io.github.DFM-Finance/dfm-agent",
|