@deeeed/metamask-harness 0.50.1 → 0.50.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.50.2 - 2026-09-08
6
+
7
+ - Remove Mobile receipt actions that require unpublished app/SDK APIs; reject stale receipt-based assertion and cleanup inputs before execution.
8
+
5
9
  ## 0.50.1 - 2026-09-08
6
10
 
7
11
  - Hand Extension builds to a fresh background worker before readiness, including watched edits and quick reloads, while preserving wallet profiles and native sidepanels. Refuse development copies over release artifacts.
@@ -1,7 +1,6 @@
1
1
  // Assert that selected Perps orders are open or absent using the Mobile controller.
2
2
  import { runAdapter } from '../platform/bridge.mjs';
3
3
  import { assertOrders } from './perps.mjs';
4
- import { useOrderReceipt } from './order-receipt.mjs';
5
4
 
6
5
  function expectedOpen(input) {
7
6
  if (input.node?.state == null) throw new Error('metamask.perps.assert_orders requires state=open or state=none.');
@@ -11,6 +10,7 @@ function expectedOpen(input) {
11
10
  throw new Error(`metamask.perps.assert_orders received unsupported state: ${state}`);
12
11
  }
13
12
 
14
- runAdapter((input) => Object.hasOwn(input.node ?? {}, 'confirmation')
15
- ? useOrderReceipt(input, 'orders')
16
- : assertOrders(input, expectedOpen(input)));
13
+ runAdapter((input) => {
14
+ if (Object.hasOwn(input.node ?? {}, 'confirmation')) throw new Error('Receipt-based Mobile assertions are unsupported. Next: mm-harness help');
15
+ return assertOrders(input, expectedOpen(input));
16
+ });
@@ -1,7 +1,6 @@
1
1
  // Assert that selected Perps positions are open or absent using the Mobile controller.
2
2
  import { runAdapter } from '../platform/bridge.mjs';
3
3
  import { assertPositions } from './perps.mjs';
4
- import { useOrderReceipt } from './order-receipt.mjs';
5
4
 
6
5
  function expectedOpen(input) {
7
6
  if (input.node?.state == null) throw new Error('metamask.perps.assert_positions requires state=open or state=none.');
@@ -11,6 +10,7 @@ function expectedOpen(input) {
11
10
  throw new Error(`metamask.perps.assert_positions received unsupported state: ${state}`);
12
11
  }
13
12
 
14
- runAdapter((input) => Object.hasOwn(input.node ?? {}, 'confirmation')
15
- ? useOrderReceipt(input, 'positions')
16
- : assertPositions(input, expectedOpen(input)));
13
+ runAdapter((input) => {
14
+ if (Object.hasOwn(input.node ?? {}, 'confirmation')) throw new Error('Receipt-based Mobile assertions are unsupported. Next: mm-harness help');
15
+ return assertPositions(input, expectedOpen(input));
16
+ });
@@ -1,10 +1,9 @@
1
1
  // Cancel selected Perps orders through the Mobile controller and assert their absence.
2
2
  import { runAdapter } from '../platform/bridge.mjs';
3
3
  import { assertOrders, closeOrders } from './perps.mjs';
4
- import { useOrderReceipt } from './order-receipt.mjs';
5
4
 
6
5
  runAdapter(async (input) => {
7
- if (Object.hasOwn(input.node ?? {}, 'confirmation')) return useOrderReceipt(input, 'cancel');
6
+ if (Object.hasOwn(input.node ?? {}, 'confirmation')) throw new Error('Receipt-based Mobile cleanup is unsupported; no orders were cancelled. Next: mm-harness help');
8
7
  const close = await closeOrders(input);
9
8
  const assertion = await assertOrders(input, false);
10
9
  return { ...assertion, close };
@@ -1,10 +1,9 @@
1
1
  // Close selected Perps positions through the Mobile controller and assert their absence.
2
2
  import { runAdapter } from '../platform/bridge.mjs';
3
3
  import { assertPositions, closePositions } from './perps.mjs';
4
- import { useOrderReceipt } from './order-receipt.mjs';
5
4
 
6
5
  runAdapter(async (input) => {
7
- if (Object.hasOwn(input.node ?? {}, 'confirmation')) return useOrderReceipt(input, 'close');
6
+ if (Object.hasOwn(input.node ?? {}, 'confirmation')) throw new Error('Receipt-based Mobile cleanup is unsupported; no positions were closed. Next: mm-harness help');
8
7
  const close = await closePositions(input);
9
8
  const assertion = await assertPositions(input, false);
10
9
  return { ...assertion, close };
@@ -2399,7 +2399,6 @@
2399
2399
  "schema": {
2400
2400
  "type": "object",
2401
2401
  "properties": {
2402
- "confirmation": { "type": "object", "description": "Frozen read_order_input output; requires exact live UI receipt identity." },
2403
2402
  "market": {
2404
2403
  "type": "string",
2405
2404
  "description": "Single market symbol, e.g. BTC or ETH. Alias: symbol."
@@ -2491,7 +2490,6 @@
2491
2490
  "schema": {
2492
2491
  "type": "object",
2493
2492
  "properties": {
2494
- "confirmation": { "type": "object", "description": "Frozen read_order_input output; requires exact live UI receipt identity." },
2495
2493
  "market": {
2496
2494
  "type": "string",
2497
2495
  "description": "Single market symbol, e.g. BTC or ETH. Alias: symbol."
@@ -2578,32 +2576,6 @@
2578
2576
  ],
2579
2577
  "execution_capabilities": ["app-mutation", "external-mutation"]
2580
2578
  },
2581
- "metamask.perps.read_order_input": {
2582
- "description": "Freeze committed unsigned Mobile inputs and require a receipt observer before UI submission.",
2583
- "examples": [{ "action": "metamask.perps.read_order_input", "amount": "10", "leverage": 1, "protection": "none", "intent": "Freeze the exact unsigned order before its UI submission", "next": "submit-order" }],
2584
- "schema": {
2585
- "type": "object", "required": ["amount", "leverage"], "additionalProperties": false,
2586
- "properties": {
2587
- "amount": { "type": ["string", "number"] },
2588
- "leverage": { "type": "number" },
2589
- "identity": { "type": "object" },
2590
- "protection": { "type": "string", "enum": ["none", "take_profit", "stop_loss", "both"], "default": "none" }
2591
- }
2592
- },
2593
- "execution_capabilities": []
2594
- },
2595
- "metamask.perps.read_order_receipt": {
2596
- "description": "Observe the actual Mobile UI submission result including failure and accepted IDs; never dispatch.",
2597
- "examples": [{ "action": "metamask.perps.read_order_receipt", "confirmation": "{{outputs.capture-input.confirmation}}", "intent": "Observe the result of the frozen UI submission", "next": "done" }],
2598
- "schema": {
2599
- "type": "object", "required": ["confirmation"], "additionalProperties": false,
2600
- "properties": {
2601
- "confirmation": { "type": "object" },
2602
- "timeout_ms": { "type": "number", "minimum": 1, "default": 30000 }
2603
- }
2604
- },
2605
- "execution_capabilities": []
2606
- },
2607
2579
  "metamask.perps.configure_order": {
2608
2580
  "description": "Set USD notional and visible leverage on an already-open instrumented Mobile confirmation; verify exact mounted values and native readback against its unchanged unsigned request, without submitting.",
2609
2581
  "schema": {
@@ -2734,7 +2706,6 @@
2734
2706
  "schema": {
2735
2707
  "type": "object",
2736
2708
  "properties": {
2737
- "confirmation": { "type": "object", "description": "Frozen read_order_input output; requires exact live UI receipt identity." },
2738
2709
  "market": {
2739
2710
  "type": "string",
2740
2711
  "description": "Single market symbol, e.g. BTC or ETH. Alias: symbol."
@@ -2827,7 +2798,6 @@
2827
2798
  "schema": {
2828
2799
  "type": "object",
2829
2800
  "properties": {
2830
- "confirmation": { "type": "object", "description": "Frozen read_order_input output; requires exact live UI receipt identity." },
2831
2801
  "market": {
2832
2802
  "type": "string",
2833
2803
  "description": "Single market symbol, e.g. BTC or ETH. Alias: symbol."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.50.1",
3
+ "version": "0.50.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -1,241 +0,0 @@
1
- // Bind live order results and cleanup to the exact UI approval, request and accepted fills.
2
- import { bridgeCommand } from '../platform/bridge.mjs';
3
-
4
- function confirmationFor(input) {
5
- const confirmation = input.node?.confirmation;
6
- const identity = confirmation?.identity;
7
- if (!identity || ['approvalId', 'account', 'provider', 'chainId', 'routeKey', 'asset', 'direction']
8
- .some((field) => typeof identity[field] !== 'string' || !identity[field])
9
- || !Number.isSafeInteger(identity.transactionTime) || identity.isTestnet !== true
10
- || identity.provider !== 'hyperliquid' || confirmation.form?.leverage !== 1
11
- || !(Number(confirmation.form?.amount) > 0 && Number(confirmation.form.amount) <= 20)) {
12
- throw new Error('Mobile receipt requires a frozen Hyperliquid testnet confirmation at 1x and at most $20.');
13
- }
14
- return confirmation;
15
- }
16
-
17
- function receiptExpression(confirmation, allowMissing = false) {
18
- return `
19
- const expected = ${JSON.stringify(confirmation)};
20
- const controller = Engine.context.PerpsController;
21
- function assertIdentity() {
22
- const state = controller.state;
23
- const account = Engine.context.AccountsController.getSelectedAccount();
24
- if (account.address.toLowerCase() !== expected.identity.account.toLowerCase()
25
- || state.activeProvider !== expected.identity.provider || state.isTestnet !== true) {
26
- throw new Error('Mobile receipt account, provider or network changed.');
27
- }
28
- }
29
- assertIdentity();
30
- if (typeof __AGENTIC__.readPerpsOrderReceipts !== 'function') throw new Error('Mobile UI receipt observer is unavailable.');
31
- const receipts = __AGENTIC__.readPerpsOrderReceipts(expected.identity.approvalId);
32
- if (${allowMissing} && Array.isArray(receipts) && receipts.length === 0) return null;
33
- if (!Array.isArray(receipts) || receipts.length !== 1) throw new Error('Exactly one UI submission receipt is required.');
34
- const receipt = receipts[0];
35
- for (const key of ['approvalId', 'transactionTime', 'account', 'provider', 'isTestnet', 'chainId', 'routeKey']) {
36
- if (receipt.identity[key] !== expected.identity[key]) throw new Error('UI receipt identity differs from the frozen confirmation.');
37
- }
38
- for (const key of ['asset', 'direction']) if (receipt.request[key] !== expected.identity[key]) throw new Error('UI receipt market or direction differs.');
39
- for (const key of ['amount', 'leverage', 'orderType', 'limitPrice', 'takeProfitPrice', 'stopLossPrice']) {
40
- if ((receipt.request[key] ?? null) !== (expected.form[key] ?? null)) throw new Error('UI receipt request differs from committed ' + key + '.');
41
- }
42
- const ids = [...new Set([receipt.result?.orderId, ...(receipt.result?.childOrderIds ?? [])].filter(id => typeof id === 'string' && id))];
43
- `;
44
- }
45
-
46
- export async function readOrderReceipt(input) {
47
- const confirmation = confirmationFor(input);
48
- const deadline = Date.now() + Number(input.node.timeout_ms ?? 30000);
49
- input = { ...input, node: { ...input.node, _action_deadline_epoch_ms: deadline } };
50
- for (;;) {
51
- const receipt = await bridgeCommand(input, ['eval', `(() => { ${receiptExpression(confirmation, true)} return receipt; })()`]);
52
- if (receipt?.status === 'settled') {
53
- const protectionCount = ['takeProfitPrice', 'stopLossPrice'].filter((key) => receipt.request[key] != null).length;
54
- if (receipt.result?.success === true && protectionCount) {
55
- if (receipt.request.orderType !== 'market') throw new Error('Protected limit receipts remain unsupported.');
56
- if (!receipt.protectionResult) {
57
- if (Date.now() >= deadline) throw new Error('Mobile protection result is unknown; do not submit again.');
58
- await new Promise((resolve) => setTimeout(resolve, 250));
59
- continue;
60
- }
61
- const protectionIds = receipt.protectionResult.childOrderIds;
62
- if (receipt.protectionResult.success !== true || !Array.isArray(protectionIds)
63
- || protectionIds.length !== protectionCount || new Set(protectionIds).size !== protectionCount
64
- || protectionIds.some((id) => typeof id !== 'string' || !/^[1-9][0-9]*$/u.test(id)
65
- || id === receipt.result.orderId || receipt.result.childOrderIds?.includes(id))) {
66
- throw new Error(`Mobile protection result lacks complete accepted IDs: ${JSON.stringify(receipt.protectionResult)}.`);
67
- }
68
- }
69
- return { action: input.action, receipt };
70
- }
71
- if (Date.now() >= deadline) throw new Error('Mobile UI submission result is unknown; do not submit again.');
72
- await new Promise((resolve) => setTimeout(resolve, 250));
73
- }
74
- }
75
-
76
- export async function useOrderReceipt(input, operation) {
77
- const confirmation = confirmationFor(input);
78
- const deadline = Date.now() + Number(input.node.timeout_ms ?? 30000);
79
- input = { ...input, node: { ...input.node, _action_deadline_epoch_ms: deadline } };
80
- if ((input.node.market && input.node.market !== confirmation.identity.asset)
81
- || (input.node.side && input.node.side !== confirmation.identity.direction)
82
- || (['positions', 'orders'].includes(operation) && input.node.state !== 'open')) {
83
- throw new Error('Receipt assertion requires the same market/direction and state=open.');
84
- }
85
- const mutation = operation === 'cancel' || operation === 'close';
86
- const result = await bridgeCommand(input, ['eval-async', `(() => {
87
- ${receiptExpression(confirmation)}
88
- if (receipt.status !== 'settled' || receipt.result?.success !== true || !ids.length) throw new Error('Accepted UI receipt IDs are required before state assertion or cleanup.');
89
- if (receipt.result.providerId && receipt.result.providerId !== expected.identity.provider) throw new Error('Accepted provider differs from UI receipt identity.');
90
- if (!['market', 'limit'].includes(receipt.request.orderType)) throw new Error('Receipt cleanup supports ordinary market and limit orders only.');
91
- const market = receipt.request.asset;
92
- const protectionKeys = ['takeProfitPrice', 'stopLossPrice'].filter(key => receipt.request[key] != null);
93
- const protectionIds = protectionKeys.length ? receipt.protectionResult?.childOrderIds : [];
94
- if (protectionKeys.length && (receipt.request.orderType !== 'market' || receipt.protectionResult?.success !== true
95
- || !Array.isArray(protectionIds) || protectionIds.length !== protectionKeys.length
96
- || new Set(protectionIds).size !== protectionKeys.length
97
- || protectionIds.some(id => typeof id !== 'string' || !/^[1-9][0-9]*$/.test(id) || ids.includes(id)))) {
98
- throw new Error('Protected cleanup requires complete distinct accepted market protection IDs.');
99
- }
100
- const ownedIds = ids.concat(protectionIds);
101
- const sign = receipt.request.direction === 'long' ? 1n : -1n;
102
- function decimal(value) {
103
- const match = /^(-?)(0|[1-9][0-9]{0,19})(?:\\.([0-9]{1,8}))?$/.exec(String(value));
104
- if (!match) throw new Error('Receipt fill decimal is invalid.');
105
- return BigInt(match[1] + match[2] + (match[3] ?? '').padEnd(8, '0'));
106
- }
107
- function readSnapshot() {
108
- assertIdentity();
109
- if (typeof controller.getUserDataSnapshot !== 'function') throw new Error('Complete user-data snapshots are unavailable.');
110
- return controller.getUserDataSnapshot().then(snapshot => {
111
- assertIdentity();
112
- const identity = snapshot?.identity;
113
- if (!Array.isArray(snapshot?.positions) || !Array.isArray(snapshot?.orders)
114
- || typeof identity?.address !== 'string' || identity.address.toLowerCase() !== expected.identity.account.toLowerCase()
115
- || identity.provider !== expected.identity.provider || identity.network !== 'testnet') {
116
- throw new Error('Receipt snapshot is incomplete or belongs to another identity.');
117
- }
118
- const orders = snapshot.orders.filter(order => order.symbol === market);
119
- if (orders.some(order => !ownedIds.includes(String(order.orderId)))
120
- || new Set(orders.map(order => String(order.orderId))).size !== orders.length) {
121
- throw new Error('Extra or unowned market orders prevent receipt assertion or cleanup.');
122
- }
123
- return {orders, positions: snapshot.positions.filter(position => position.symbol === market)};
124
- });
125
- }
126
- function verifyProtection(position, orders, present) {
127
- const matchedIds = [];
128
- for (const [priceKey, ordersKey] of [['takeProfitPrice','takeProfitOrders'],['stopLossPrice','stopLossOrders']]) {
129
- const wanted = present ? receipt.request[priceKey] : null;
130
- const triggers = position[ordersKey];
131
- const count = position[priceKey === 'takeProfitPrice' ? 'takeProfitCount' : 'stopLossCount'];
132
- if (!Array.isArray(triggers)) throw new Error('Exact position protection arrays are unavailable.');
133
- if (wanted == null) {
134
- if (triggers.length || count !== 0 || position[priceKey] != null) throw new Error('Unexpected position protection remains.');
135
- continue;
136
- }
137
- const direction = priceKey === 'takeProfitPrice' ? 'take_profit' : 'stop';
138
- const type = direction === 'take_profit' ? 'take_profit_limit' : 'stop_market';
139
- if (triggers.length !== 1 || count !== 1) throw new Error('Position protection count differs from the frozen UI request.');
140
- const trigger = triggers[0];
141
- const order = orders.find(candidate => String(candidate.orderId) === trigger.orderId);
142
- if (!protectionIds.includes(trigger.orderId) || matchedIds.includes(trigger.orderId)
143
- || trigger.direction !== direction || trigger.orderType !== type || decimal(trigger.triggerPrice) !== decimal(wanted)
144
- || decimal(trigger.size) !== sign * decimal(position.size) || trigger.reduceOnly !== true || trigger.isPartial !== false
145
- || !order || order.side !== (sign === 1n ? 'sell' : 'buy') || order.reduceOnly !== true
146
- || order.isTrigger !== true || order.isPositionTpsl !== true || order.triggerOrderType !== type
147
- || decimal(order.triggerPrice) !== decimal(wanted) || decimal(order.size) !== decimal(trigger.size)) {
148
- throw new Error('Exact protection ID, price, size, side or type differs from the UI receipt.');
149
- }
150
- matchedIds.push(trigger.orderId);
151
- }
152
- if (present && matchedIds.length !== protectionIds.length) throw new Error('Accepted protection IDs are not all represented in the position.');
153
- }
154
- function verifyLineage(position) {
155
- return controller.getOrderFills({aggregateByTime: false, startTime: receipt.identity.transactionTime, endTime: Date.now()}).then(fills => {
156
- assertIdentity();
157
- if (!Array.isArray(fills) || fills.length >= 2000) throw new Error('Receipt fill history is incomplete.');
158
- const marketFills = fills.filter(fill => fill.symbol === market);
159
- if (!marketFills.length || marketFills.some(fill => !ids.includes(String(fill.orderId)))) throw new Error('Another order changed the market or parent accepted fills are unavailable.');
160
- marketFills.sort((a,b) => { const difference = sign * (decimal(a.startPosition) - decimal(b.startPosition)); return difference < 0n ? -1 : difference > 0n ? 1 : 0; });
161
- let size = 0n;
162
- for (const fill of marketFills) {
163
- if (fill.side !== (sign === 1n ? 'buy' : 'sell') || decimal(fill.size) <= 0n || decimal(fill.startPosition) !== size) throw new Error('Parent accepted fills do not prove a position starting from zero.');
164
- size += sign * decimal(fill.size);
165
- }
166
- if (size !== decimal(position.size) || position.leverage?.value !== receipt.request.leverage) throw new Error('Current position differs from exact parent fills or leverage.');
167
- return marketFills;
168
- });
169
- }
170
- function cancelOwned(orders, baseline) {
171
- const results = [];
172
- function waitForCancellation() {
173
- return readSnapshot().then(after => {
174
- if (baseline && (after.positions.length !== 1 || after.positions[0].size !== baseline.size
175
- || after.positions[0].entryPrice !== baseline.entryPrice || after.positions[0].leverage?.value !== baseline.leverage.value)) {
176
- throw new Error('Owned position changed during protective cancellation.');
177
- }
178
- if (!after.orders.length) {
179
- if (baseline) verifyProtection(after.positions[0], after.orders, false);
180
- return {after, results};
181
- }
182
- if (Date.now() >= ${deadline}) throw new Error('Accepted order cancellation is not independently confirmed.');
183
- return new Promise(resolve => setTimeout(resolve, 250)).then(waitForCancellation);
184
- });
185
- }
186
- return orders.reduce((previous, order) => previous.then(() => {
187
- if (Date.now() >= ${deadline}) throw new Error('Receipt cleanup deadline elapsed before cancellation.');
188
- assertIdentity();
189
- return controller.cancelOrder({symbol: market, orderId: String(order.orderId)}).then(result => {
190
- results.push({orderId: order.orderId, result});
191
- if (result?.success !== true) throw new Error('Exact receipt cancellation failed: ' + JSON.stringify(results));
192
- });
193
- }), Promise.resolve()).then(waitForCancellation);
194
- }
195
- return readSnapshot().then(snapshot => {
196
- const orders = snapshot.orders;
197
- if (${JSON.stringify(operation)} === 'orders') {
198
- if (!orders.length || orders.some(order => !ids.includes(String(order.orderId))
199
- || order.side !== (sign === 1n ? 'buy' : 'sell'))) throw new Error('No matching exact accepted UI order remains open.');
200
- return {receipt, orders};
201
- }
202
- if (${JSON.stringify(operation)} === 'cancel' && !protectionKeys.length) {
203
- return cancelOwned(orders).then(({results}) => ({receipt, results, remainingOwnedOrders: 0}));
204
- }
205
- if (!snapshot.positions.length && ${JSON.stringify(operation)} === 'close' && !orders.length) return {receipt, closed: false, alreadyAbsent: true};
206
- if (snapshot.positions.length !== 1) throw new Error('Receipt position is absent or ambiguous.');
207
- const position = snapshot.positions[0];
208
- if (protectionKeys.length && orders.some(order => !protectionIds.includes(String(order.orderId)))) throw new Error('Extra parent orders prevent protected position assertion or cleanup.');
209
- return verifyLineage(position).then(fills => {
210
- const protectionPresent = protectionIds.some(id => orders.some(order => String(order.orderId) === id));
211
- verifyProtection(position, orders, ${JSON.stringify(operation)} === 'positions' || protectionPresent);
212
- if (${JSON.stringify(operation)} === 'positions') return {receipt, position, fills};
213
- if (orders.some(order => !protectionIds.includes(String(order.orderId)))) throw new Error('Parent order remainder prevents owned position cleanup.');
214
- return cancelOwned(orders, position).then(({after, results}) => {
215
- if (${JSON.stringify(operation)} === 'cancel') return {receipt, results, remainingOwnedOrders: 0};
216
- const unchanged = after.positions[0];
217
- return verifyLineage(unchanged).then(readSnapshot).then(beforeClose => {
218
- if (beforeClose.positions.length !== 1 || beforeClose.orders.length
219
- || beforeClose.positions[0].size !== unchanged.size || beforeClose.positions[0].entryPrice !== unchanged.entryPrice
220
- || beforeClose.positions[0].leverage?.value !== unchanged.leverage.value) throw new Error('Owned position changed before close.');
221
- verifyProtection(beforeClose.positions[0], beforeClose.orders, false);
222
- if (Date.now() >= ${deadline}) throw new Error('Receipt cleanup deadline elapsed before close.');
223
- assertIdentity();
224
- return controller.closePosition({symbol: market, size: String(sign === 1n ? unchanged.size : String(unchanged.size).slice(1)), orderType: 'market', providerId: 'hyperliquid', position: unchanged}).then(closeResult => {
225
- if (closeResult?.success !== true || !closeResult.orderId) throw new Error('Receipt-owned close has no accepted result: ' + JSON.stringify(closeResult));
226
- function waitForClose() {
227
- return readSnapshot().then(final => {
228
- if (!final.positions.length && !final.orders.length) return {receipt, results, closeResult, closed: true};
229
- if (Date.now() >= ${deadline}) throw new Error('Receipt-owned close is not independently confirmed.');
230
- return new Promise(resolve => setTimeout(resolve, 250)).then(waitForClose);
231
- });
232
- }
233
- return waitForClose();
234
- });
235
- });
236
- });
237
- });
238
- });
239
- })()`]);
240
- return { action: input.action, ...result, mutation };
241
- }
@@ -1,33 +0,0 @@
1
- // Freeze the visible unsigned order inputs and refuse a repeated or unobservable submission.
2
- import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
3
- import { readOrderInputState } from './order-input-state.mjs';
4
- import { observeNativeUi } from '../platform/observe-ui.mjs';
5
-
6
- runAdapter(async (input) => {
7
- const confirmation = await readOrderInputState(input, input.node?.identity);
8
- const { identity, form } = confirmation;
9
- const observation = await observeNativeUi({ refs: ['ui.visible'], node: input.node }, input.context);
10
- const visible = observation.observations?.['ui.visible'];
11
- const payRows = visible?.items?.filter((item) => item.test_id === 'pay-with') ?? [];
12
- if (!visible || visible.truncated || payRows.length !== 1 || payRows[0].label !== 'Pay with, Perps balance') {
13
- throw new Error('Receipt proof requires the visible Perps balance payment source; funding is not permitted.');
14
- }
15
- if (identity.provider !== 'hyperliquid' || identity.isTestnet !== true
16
- || form.leverage !== 1 || !(Number(form.amount) > 0 && Number(form.amount) <= 20)
17
- || !['market', 'limit'].includes(form.orderType)) {
18
- throw new Error('Mobile receipt proof requires Hyperliquid testnet, 1x and at most $20 on a market or limit order.');
19
- }
20
- if (Number(form.amount) !== Number(input.node.amount) || form.leverage !== Number(input.node.leverage)) {
21
- throw new Error('Committed Mobile order inputs differ from the requested amount or leverage.');
22
- }
23
- const protection = input.node.protection ?? 'none';
24
- for (const [field, required] of [['takeProfitPrice', ['take_profit', 'both'].includes(protection)], ['stopLossPrice', ['stop_loss', 'both'].includes(protection)]]) {
25
- if (required ? !(Number(form[field]) > 0) : form[field] !== null) throw new Error(`Committed Mobile ${field} does not match requested protection.`);
26
- }
27
- const prior = await bridgeCommand(input, ['eval', `(() => {
28
- if (typeof __AGENTIC__.readPerpsOrderReceipts !== 'function') throw new Error('Mobile UI receipt observer is unavailable.');
29
- return __AGENTIC__.readPerpsOrderReceipts(${JSON.stringify(identity.approvalId)});
30
- })()`]);
31
- if (!Array.isArray(prior) || prior.length) throw new Error('This unsigned confirmation already has a submission attempt; do not submit it again.');
32
- return { action: input.action, confirmation, submitted: false };
33
- });
@@ -1,5 +0,0 @@
1
- // Observe the actual result of one UI submission without dispatching an order.
2
- import { runAdapter } from '../platform/bridge.mjs';
3
- import { readOrderReceipt } from './order-receipt.mjs';
4
-
5
- runAdapter(readOrderReceipt);