otomato-sdk 2.0.200 → 2.0.201

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.
@@ -1,4 +1,4 @@
1
- import { Workflow, Trigger, Action, Edge, TRIGGERS, ACTIONS, CHAINS, getTokenFromSymbol, convertToTokenUnitsFromSymbol, convertToTokenUnits } from '../index.js';
1
+ import { Workflow, Trigger, Action, Edge, TRIGGERS, ACTIONS, CHAINS, getTokenFromSymbol, convertToTokenUnitsFromSymbol, convertToTokenUnits, WORKFLOW_LOOPING_TYPES } from '../index.js';
2
2
  export const WORKFLOW_TEMPLATES_TAGS = {
3
3
  NFTS: 'NFTs',
4
4
  SOCIALS: 'Socials',
@@ -6,7 +6,9 @@ export const WORKFLOW_TEMPLATES_TAGS = {
6
6
  ON_CHAIN_MONITORING: 'On-chain monitoring',
7
7
  YIELD: 'Yield',
8
8
  NOTIFICATIONS: 'notifications',
9
- ABSTRACT: 'Abstract'
9
+ ABSTRACT: 'Abstract',
10
+ DEXES: 'Dexes',
11
+ LENDING: 'Lending'
10
12
  };
11
13
  const createModeTransferNotificationWorkflow = () => {
12
14
  const modeTransferTrigger = new Trigger(TRIGGERS.TOKENS.TRANSFER.TRANSFER);
@@ -192,9 +194,123 @@ const abstractGetNotifiedWhenStreamerIsLive = async () => {
192
194
  const edge = new Edge({ source: trigger, target: telegramAction });
193
195
  return new Workflow('Get notified when a given streamer goes live', [trigger, telegramAction], [edge]);
194
196
  };
197
+ // notify me when I can unstake my stakestone
198
+ const createStakestoneUnstakeNotificationWorkflow = async () => {
199
+ const trigger = new Trigger(TRIGGERS.YIELD.STAKESTONE.LATEST_ROUND_ID);
200
+ trigger.setParams('chainId', CHAINS.ETHEREUM);
201
+ trigger.setParams('contractAddress', "0x8f88ae3798e8ff3d0e0de7465a0863c9bbb577f0");
202
+ trigger.setCondition('neq');
203
+ trigger.setComparisonValue('{{history.0.value}}');
204
+ trigger.setPosition(400, 120);
205
+ const notificationAction = new Action(ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE);
206
+ notificationAction.setParams("message", "You can now unstake your Stakestone position!");
207
+ notificationAction.setPosition(400, 240);
208
+ const edge = new Edge({ source: trigger, target: notificationAction });
209
+ return new Workflow('Get notified when you can unstake your Stakestone position', [trigger, notificationAction], [edge]);
210
+ };
211
+ // notify me when a given uniswap position is out of range [looping enabled - 5 times]
212
+ const createUniswapPositionOutOfRangeNotificationWorkflow = async () => {
213
+ const trigger = new Trigger(TRIGGERS.DEXES.UNISWAP.IS_IN_RANGE);
214
+ trigger.setParams('chainId', CHAINS.ETHEREUM);
215
+ trigger.setCondition('eq');
216
+ trigger.setParams('comparisonValue', false);
217
+ trigger.setPosition(400, 120);
218
+ const notificationAction = new Action(ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE);
219
+ notificationAction.setParams("message", "Your Uniswap position is out of range! You can obtain your position Id at https://app.uniswap.org/positions");
220
+ notificationAction.setPosition(400, 240);
221
+ const edge = new Edge({ source: trigger, target: notificationAction });
222
+ const workflow = new Workflow('Get notified when a given uniswap position is out of range', [trigger, notificationAction], [edge]);
223
+ workflow.setSettings({
224
+ loopingType: WORKFLOW_LOOPING_TYPES.POLLING,
225
+ period: 600000,
226
+ limit: 5,
227
+ });
228
+ return workflow;
229
+ };
230
+ // notify me when Hyperlend raise their deposit cap for stHype [looping enabled - 10 times]
231
+ const createHyperLendDepositCapNotificationWorkflow = async () => {
232
+ const trigger = new Trigger(TRIGGERS.LENDING.HYPERLEND.SUPPLY_CAP);
233
+ trigger.setParams('chainId', CHAINS.HYPER_EVM);
234
+ trigger.setParams('asset', getTokenFromSymbol(CHAINS.HYPER_EVM, 'wstHYPE').contractAddress);
235
+ trigger.setCondition('gt');
236
+ trigger.setComparisonValue('{{history.0.value}}');
237
+ trigger.setPosition(400, 120);
238
+ const notificationAction = new Action(ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE);
239
+ notificationAction.setParams("message", "Hyperlend has raised their deposit cap for stHype!");
240
+ notificationAction.setPosition(400, 240);
241
+ const edge = new Edge({ source: trigger, target: notificationAction });
242
+ const workflow = new Workflow('Get notified when Hyperlend raise their deposit cap for stHype', [trigger, notificationAction], [edge]);
243
+ workflow.setSettings({
244
+ loopingType: WORKFLOW_LOOPING_TYPES.POLLING,
245
+ period: 600000,
246
+ limit: 10,
247
+ });
248
+ return workflow;
249
+ };
250
+ // Save all the current yields for USDC on base (AAVE, Compound, Moonwell & top 5 USDC morpho vault) every hour [repeat 100 times, every hour]
251
+ const createUSDCYieldsStorageWorkflow = async () => {
252
+ const trigger = new Trigger(TRIGGERS.CORE.EVERY_PERIOD.EVERY_PERIOD);
253
+ trigger.setParams('period', 3600000);
254
+ trigger.setParams('limit', 100);
255
+ trigger.setPosition(400, 120);
256
+ const notificationAction = new Action(ACTIONS.OTHERS.GSHEET.GSHEET);
257
+ notificationAction.setParams("data", [
258
+ ["aave", "moonwell", "compound", "Spark USDC Vault", "Moonwell Flagship USDC", "Seamless USDC Vault", "Steakhouse USDC", "Gauntlet USDC Prime"],
259
+ [
260
+ "{{external.functions.aaveLendingRate(8453,0x833589fcd6edb6e08f4c7c32d4f71b54bda02913,,)}}",
261
+ "{{external.functions.moonwellLendingRate(8453,0x833589fcd6edb6e08f4c7c32d4f71b54bda02913,,)}}",
262
+ "{{external.functions.compoundLendingRate(8453,0x833589fcd6edb6e08f4c7c32d4f71b54bda02913,,,)}}",
263
+ "{{external.functions.morphoLendingRate(8453,0x7BfA7C4f149E7415b73bdeDfe609237e29CBF34A)}}",
264
+ "{{external.functions.morphoLendingRate(8453,0xc1256Ae5FF1cf2719D4937adb3bbCCab2E00A2Ca)}}",
265
+ "{{external.functions.morphoLendingRate(8453,0x616a4E1db48e22028f6bbf20444Cd3b8e3273738)}}",
266
+ "{{external.functions.morphoLendingRate(8453,0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183)}}",
267
+ "{{external.functions.morphoLendingRate(8453,0xeE8F4eC5672F09119b96Ab6fB59C27E1b7e44b61)}}"
268
+ ]
269
+ ]);
270
+ // notificationAction.setParams("mode", "append");
271
+ // notificationAction.setParams("role", "writer");
272
+ // notificationAction.setParams("sheetId", "0");
273
+ // notificationAction.setParams("spreadsheetId", "1NxqGqgtUQkojBOl9g7CBkbqc7bB6mBkZxHWMPsu1uQY");
274
+ notificationAction.setPosition(400, 240);
275
+ const edge = new Edge({ source: trigger, target: notificationAction });
276
+ return new Workflow('Save all the current yields for USDC on base (AAVE, Compound, Moonwell, Spark USDC Vault, Moonwell Flagship USDC, Seamless USDC Vault, Steakhouse USDC, Gauntlet USDC Prime) every hour', [trigger, notificationAction], [edge]);
277
+ };
278
+ // notify me when there are more than 50 ETH in available liquidity for instant withdrawal on Stakestone
279
+ const createStakestoneInstantWithdrawalNotificationWorkflow = async () => {
280
+ const trigger = new Trigger(TRIGGERS.YIELD.STAKESTONE.STAKESTONE_VAULT_LIQUIDITY);
281
+ trigger.setParams('chainId', CHAINS.ETHEREUM);
282
+ trigger.setCondition('gt');
283
+ trigger.setComparisonValue('50');
284
+ trigger.setPosition(400, 120);
285
+ const notificationAction = new Action(ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE);
286
+ notificationAction.setParams("message", "There are more than 50 ETH in available liquidity for instant withdrawal on Stakestone!");
287
+ notificationAction.setPosition(400, 240);
288
+ const edge = new Edge({ source: trigger, target: notificationAction });
289
+ return new Workflow('Get notified when there are more than 50 ETH in available liquidity for instant withdrawal on Stakestone', [trigger, notificationAction], [edge]);
290
+ };
291
+ // notify me when I receive USDC [looping enabled - 30 times]
292
+ const createUSDCReceiveNotificationWorkflow = async () => {
293
+ const trigger = new Trigger(TRIGGERS.TOKENS.TRANSFER.TRANSFER);
294
+ trigger.setParams('chainId', CHAINS.BASE);
295
+ trigger.setParams('contractAddress', getTokenFromSymbol(CHAINS.BASE, 'USDC').contractAddress);
296
+ // TODO: add smart account address
297
+ trigger.setParams('abiParams.to', '{{smartAccountAddress}}');
298
+ trigger.setPosition(400, 120);
299
+ const notificationAction = new Action(ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE);
300
+ notificationAction.setParams("message", "You received USDC!");
301
+ notificationAction.setPosition(400, 240);
302
+ const edge = new Edge({ source: trigger, target: notificationAction });
303
+ const workflow = new Workflow('Get notified when I receive USDC', [trigger, notificationAction], [edge]);
304
+ workflow.setSettings({
305
+ loopingType: WORKFLOW_LOOPING_TYPES.SUBSCRIPTION,
306
+ timeout: 31536000000,
307
+ limit: 30,
308
+ });
309
+ return workflow;
310
+ };
195
311
  const createEthereumFoundationTransferNotificationWorkflow = () => {
196
312
  const ethTransferTrigger = new Trigger(TRIGGERS.TOKENS.TRANSFER.NATIVE_TRANSFER);
197
- ethTransferTrigger.setChainId(CHAINS.BASE);
313
+ ethTransferTrigger.setChainId(CHAINS.ETHEREUM);
198
314
  ethTransferTrigger.setParams('wallet', '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe');
199
315
  ethTransferTrigger.setParams('threshold', 0.004);
200
316
  ethTransferTrigger.setPosition(400, 120);
@@ -407,4 +523,94 @@ export const WORKFLOW_TEMPLATES = [
407
523
  ],
408
524
  createWorkflow: copyTradeVitalikOdos
409
525
  },*/
526
+ {
527
+ 'name': 'Get notified when you can unstake your Stakestone position',
528
+ 'description': 'Notify me when you can unstake your Stakestone position',
529
+ 'tags': [WORKFLOW_TEMPLATES_TAGS.YIELD, WORKFLOW_TEMPLATES_TAGS.NOTIFICATIONS],
530
+ 'thumbnail': 'https://otomato-sdk-images.s3.eu-west-1.amazonaws.com/templates/dailyYieldUpdates.jpg',
531
+ 'image': [
532
+ TRIGGERS.YIELD.STAKESTONE.STAKESTONE_VAULT_LIQUIDITY.image,
533
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.image
534
+ ],
535
+ 'blockIDs': [
536
+ TRIGGERS.YIELD.STAKESTONE.STAKESTONE_VAULT_LIQUIDITY.blockId,
537
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.blockId
538
+ ],
539
+ createWorkflow: createStakestoneUnstakeNotificationWorkflow
540
+ },
541
+ {
542
+ 'name': 'Get notified when a given uniswap position is out of range',
543
+ 'description': 'Notify me when a given uniswap position is out of range. Get your tokenId from https://app.uniswap.org/positions',
544
+ 'tags': [WORKFLOW_TEMPLATES_TAGS.DEXES, WORKFLOW_TEMPLATES_TAGS.NOTIFICATIONS],
545
+ 'thumbnail': 'https://otomato-sdk-images.s3.eu-west-1.amazonaws.com/templates/dailyYieldUpdates.jpg',
546
+ 'image': [
547
+ TRIGGERS.DEXES.UNISWAP.IS_IN_RANGE.image,
548
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.image
549
+ ],
550
+ 'blockIDs': [
551
+ TRIGGERS.DEXES.UNISWAP.IS_IN_RANGE.blockId,
552
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.blockId
553
+ ],
554
+ createWorkflow: createUniswapPositionOutOfRangeNotificationWorkflow
555
+ },
556
+ {
557
+ 'name': 'Get notified when Hyperlend raise their deposit cap for stHype',
558
+ 'description': 'Notify me when Hyperlend raise their deposit cap for stHype',
559
+ 'tags': [WORKFLOW_TEMPLATES_TAGS.LENDING, WORKFLOW_TEMPLATES_TAGS.NOTIFICATIONS],
560
+ 'thumbnail': 'https://otomato-sdk-images.s3.eu-west-1.amazonaws.com/templates/dailyYieldUpdates.jpg',
561
+ 'image': [
562
+ TRIGGERS.LENDING.HYPERLEND.SUPPLY_CAP.image,
563
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.image
564
+ ],
565
+ 'blockIDs': [
566
+ TRIGGERS.LENDING.HYPERLEND.SUPPLY_CAP.blockId,
567
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.blockId
568
+ ],
569
+ createWorkflow: createHyperLendDepositCapNotificationWorkflow
570
+ },
571
+ {
572
+ 'name': 'Get notified when there are more than 50 ETH in available liquidity for instant withdrawal on Stakestone',
573
+ 'description': 'Notify me when there are more than 50 ETH in available liquidity for instant withdrawal on Stakestone',
574
+ 'tags': [WORKFLOW_TEMPLATES_TAGS.YIELD, WORKFLOW_TEMPLATES_TAGS.NOTIFICATIONS],
575
+ 'thumbnail': 'https://otomato-sdk-images.s3.eu-west-1.amazonaws.com/templates/dailyYieldUpdates.jpg',
576
+ 'image': [
577
+ TRIGGERS.YIELD.STAKESTONE.STAKESTONE_VAULT_LIQUIDITY.image,
578
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.image
579
+ ],
580
+ 'blockIDs': [
581
+ TRIGGERS.YIELD.STAKESTONE.STAKESTONE_VAULT_LIQUIDITY.blockId,
582
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.blockId
583
+ ],
584
+ createWorkflow: createStakestoneInstantWithdrawalNotificationWorkflow
585
+ },
586
+ {
587
+ 'name': 'Get notified when I receive USDC',
588
+ 'description': 'Notify me when I receive USDC',
589
+ 'tags': [WORKFLOW_TEMPLATES_TAGS.TRADING, WORKFLOW_TEMPLATES_TAGS.NOTIFICATIONS],
590
+ 'thumbnail': 'https://otomato-sdk-images.s3.eu-west-1.amazonaws.com/templates/dailyYieldUpdates.jpg',
591
+ 'image': [
592
+ TRIGGERS.TOKENS.TRANSFER.TRANSFER.image,
593
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.image
594
+ ],
595
+ 'blockIDs': [
596
+ TRIGGERS.TOKENS.TRANSFER.TRANSFER.blockId,
597
+ ACTIONS.NOTIFICATIONS.TELEGRAM.SEND_MESSAGE.blockId
598
+ ],
599
+ createWorkflow: createUSDCReceiveNotificationWorkflow
600
+ },
601
+ {
602
+ 'name': 'Save all the current yields for USDC on base (AAVE, Compound, Moonwell, Spark USDC Vault, Moonwell Flagship USDC, Seamless USDC Vault, Steakhouse USDC, Gauntlet USDC Prime) every hour',
603
+ 'description': 'Save all the current yields for USDC on base (AAVE, Compound, Moonwell, Spark USDC Vault, Moonwell Flagship USDC, Seamless USDC Vault, Steakhouse USDC, Gauntlet USDC Prime) every hour',
604
+ 'tags': [WORKFLOW_TEMPLATES_TAGS.YIELD, WORKFLOW_TEMPLATES_TAGS.NOTIFICATIONS],
605
+ 'thumbnail': 'https://otomato-sdk-images.s3.eu-west-1.amazonaws.com/templates/dailyYieldUpdates.jpg',
606
+ 'image': [
607
+ TRIGGERS.CORE.EVERY_PERIOD.EVERY_PERIOD.image,
608
+ ACTIONS.OTHERS.GSHEET.GSHEET.image
609
+ ],
610
+ 'blockIDs': [
611
+ TRIGGERS.CORE.EVERY_PERIOD.EVERY_PERIOD.blockId,
612
+ ACTIONS.OTHERS.GSHEET.GSHEET.blockId
613
+ ],
614
+ createWorkflow: createUSDCYieldsStorageWorkflow
615
+ },
410
616
  ];
@@ -1,4 +1,4 @@
1
- export const SDK_VERSION = '2.0.200';
1
+ export const SDK_VERSION = '2.0.201';
2
2
  export function compareVersions(v1, v2) {
3
3
  // Split the version strings into parts
4
4
  const v1Parts = v1.split('.').map(Number);
@@ -3,6 +3,7 @@ export const xSpacing = 500;
3
3
  export const ySpacing = 120;
4
4
  export const ROOT_X = 400;
5
5
  export const ROOT_Y = 120;
6
+ export const TRIGGER_X_SPACING = 363;
6
7
  /**
7
8
  * Helper: Returns a group key for a node based on its primary parent.
8
9
  * If a node has multiple parents, we sort them numerically (by their ref)
@@ -70,64 +71,123 @@ export function positionWorkflowNodes(workflow) {
70
71
  const sortedLayers = Array.from(layers.keys()).sort((a, b) => a - b);
71
72
  for (const layer of sortedLayers) {
72
73
  const nodesInLayer = layers.get(layer);
73
- // Group nodes by primary parent.
74
- const groups = new Map();
75
- for (const node of nodesInLayer) {
76
- const groupKey = getGroupKey(node, workflow.edges);
77
- if (!groups.has(groupKey)) {
78
- groups.set(groupKey, []);
79
- }
80
- groups.get(groupKey).push(node);
81
- }
82
74
  // Determine the Y position for this layer.
83
75
  const yPos = (layer * ySpacing) + ROOT_Y;
84
- const groupInfos = [];
85
- // Sort group keys numerically.
86
- const sortedGroupKeys = Array.from(groups.keys()).sort((a, b) => Number(a) - Number(b));
87
- for (const key of sortedGroupKeys) {
88
- const groupNodes = groups.get(key);
89
- groupNodes.sort((a, b) => Number(a.getRef()) - Number(b.getRef()));
90
- let desiredCenter;
91
- if (key === "none") {
92
- desiredCenter = ROOT_X;
76
+ // Special handling for layer 0 (triggers)
77
+ if (layer === 0) {
78
+ const startingNodes = nodesInLayer.filter(node => getParents(node, workflow.edges).length === 0);
79
+ startingNodes.sort((a, b) => Number(a.getRef()) - Number(b.getRef())); // Consistent ordering
80
+ const numStartingNodes = startingNodes.length;
81
+ let totalWidth = 0; // Initialize totalWidth
82
+ if (numStartingNodes > 0) {
83
+ totalWidth = (numStartingNodes - 1) * TRIGGER_X_SPACING; // Assign value to totalWidth
84
+ let currentX = ROOT_X - totalWidth / 2;
85
+ for (let i = 0; i < numStartingNodes; i++) {
86
+ startingNodes[i].setPosition(currentX, yPos);
87
+ currentX += TRIGGER_X_SPACING;
88
+ }
93
89
  }
94
- else {
95
- const parent = workflow.nodes.find(n => n.getRef() === key);
96
- desiredCenter = parent && parent.position ? parent.position.x : ROOT_X;
90
+ // Handle non-starting nodes in layer 0 if any (should be rare for triggers)
91
+ const otherNodesInLayer0 = nodesInLayer.filter(node => getParents(node, workflow.edges).length > 0);
92
+ if (otherNodesInLayer0.length > 0) {
93
+ // Fallback to default group positioning for these nodes
94
+ // This part reuses the existing grouping logic but only for these specific nodes.
95
+ const groups = new Map();
96
+ for (const node of otherNodesInLayer0) {
97
+ const groupKey = getGroupKey(node, workflow.edges);
98
+ if (!groups.has(groupKey)) {
99
+ groups.set(groupKey, []);
100
+ }
101
+ groups.get(groupKey).push(node);
102
+ }
103
+ // Simplified group processing for these non-starting nodes in layer 0
104
+ // Calculate currentXOffset based on whether startingNodes were present or not
105
+ const startingNodesOffset = numStartingNodes > 0 ? totalWidth / 2 + TRIGGER_X_SPACING : 0;
106
+ let currentXOffset = ROOT_X + startingNodesOffset;
107
+ // If there were no starting nodes, and we only have otherNodesInLayer0,
108
+ // they should probably start near ROOT_X, not offset by totalWidth/2.
109
+ // If startingNodes were there, offset by totalWidth/2 + some spacing.
110
+ // If numStartingNodes is 0, totalWidth is 0, so currentXOffset starts at ROOT_X.
111
+ // If numStartingNodes > 0, currentXOffset starts at ROOT_X + totalWidth/2 + TRIGGER_X_SPACING
112
+ // (using TRIGGER_X_SPACING as a gap, or xSpacing could be used too)
113
+ if (numStartingNodes > 0) {
114
+ currentXOffset = ROOT_X + totalWidth / 2 + xSpacing; // Place them after triggers with xSpacing
115
+ }
116
+ else {
117
+ currentXOffset = ROOT_X; // If no triggers, start these at ROOT_X
118
+ }
119
+ const sortedGroupKeys = Array.from(groups.keys()).sort((a, b) => Number(a) - Number(b));
120
+ for (const key of sortedGroupKeys) {
121
+ const groupNodes = groups.get(key);
122
+ groupNodes.sort((a, b) => Number(a.getRef()) - Number(b.getRef()));
123
+ for (let i = 0; i < groupNodes.length; i++) {
124
+ groupNodes[i].setPosition(currentXOffset + i * xSpacing, yPos);
125
+ }
126
+ currentXOffset += groupNodes.length * xSpacing + xSpacing; // Add spacing between groups
127
+ }
97
128
  }
98
- const groupSize = groupNodes.length;
99
- const width = (groupSize - 1) * xSpacing;
100
- const desiredLeft = desiredCenter - width / 2;
101
- groupInfos.push({
102
- groupKey: key,
103
- nodes: groupNodes,
104
- desiredCenter,
105
- desiredLeft,
106
- width
107
- });
108
129
  }
109
- // Adjust groups so that adjacent groups do not overlap.
110
- // We require that the left edge of a group is at least xSpacing to the right of the previous group's right edge.
111
- groupInfos.sort((a, b) => a.desiredLeft - b.desiredLeft);
112
- let prevRight = -Infinity;
113
- for (const group of groupInfos) {
114
- let newLeft = group.desiredLeft;
115
- if (newLeft < prevRight + xSpacing) {
116
- newLeft = prevRight + xSpacing;
130
+ else {
131
+ // Original logic for layers other than 0
132
+ // Group nodes by primary parent.
133
+ const groups = new Map();
134
+ for (const node of nodesInLayer) {
135
+ const groupKey = getGroupKey(node, workflow.edges);
136
+ if (!groups.has(groupKey)) {
137
+ groups.set(groupKey, []);
138
+ }
139
+ groups.get(groupKey).push(node);
117
140
  }
118
- group.newLeft = newLeft;
119
- prevRight = newLeft + group.width;
120
- }
121
- // Now assign positions for nodes in each group using the new left.
122
- for (const group of groupInfos) {
123
- const { nodes, newLeft } = group;
124
- for (let i = 0; i < nodes.length; i++) {
125
- const nodeX = newLeft + i * xSpacing;
126
- nodes[i].setPosition(nodeX, yPos);
141
+ const groupInfos = [];
142
+ // Sort group keys numerically.
143
+ const sortedGroupKeys = Array.from(groups.keys()).sort((a, b) => Number(a) - Number(b));
144
+ for (const key of sortedGroupKeys) {
145
+ const groupNodes = groups.get(key);
146
+ groupNodes.sort((a, b) => Number(a.getRef()) - Number(b.getRef()));
147
+ let desiredCenter;
148
+ if (key === "none") { // Should not happen for layer > 0 if graph is connected
149
+ desiredCenter = ROOT_X;
150
+ }
151
+ else {
152
+ const parent = workflow.nodes.find(n => n.getRef() === key);
153
+ desiredCenter = parent && parent.position ? parent.position.x : ROOT_X;
154
+ }
155
+ const groupSize = groupNodes.length;
156
+ const width = (groupSize - 1) * xSpacing;
157
+ const desiredLeft = desiredCenter - width / 2;
158
+ groupInfos.push({
159
+ groupKey: key,
160
+ nodes: groupNodes,
161
+ desiredCenter,
162
+ desiredLeft,
163
+ width
164
+ });
165
+ }
166
+ // Adjust groups so that adjacent groups do not overlap.
167
+ // We require that the left edge of a group is at least xSpacing to the right of the previous group's right edge.
168
+ groupInfos.sort((a, b) => a.desiredLeft - b.desiredLeft);
169
+ let prevRight = -Infinity;
170
+ for (const group of groupInfos) {
171
+ let newLeft = group.desiredLeft;
172
+ if (newLeft < prevRight + xSpacing) {
173
+ newLeft = prevRight + xSpacing;
174
+ }
175
+ group.newLeft = newLeft;
176
+ prevRight = newLeft + group.width;
177
+ }
178
+ // Now assign positions for nodes in each group using the new left.
179
+ for (const group of groupInfos) {
180
+ const { nodes, newLeft } = group;
181
+ for (let i = 0; i < nodes.length; i++) {
182
+ const nodeX = newLeft + i * xSpacing;
183
+ nodes[i].setPosition(nodeX, yPos);
184
+ }
127
185
  }
128
186
  }
129
187
  }
130
188
  // Step 3: Resolve overlaps within each group in each layer.
189
+ // This step might need adjustment if TRIGGER_X_SPACING causes issues with xSpacing based overlap.
190
+ // For now, we assume xSpacing is the minimum desired gap after initial placement.
131
191
  layers.forEach((nodes, layer) => {
132
192
  const groups = new Map();
133
193
  for (const node of nodes) {
@@ -152,8 +212,19 @@ export function positionWorkflowNodes(workflow) {
152
212
  const prev = groupNodes[i - 1];
153
213
  const current = groupNodes[i];
154
214
  const diff = current.position.x - prev.position.x;
155
- if (diff < xSpacing) {
156
- const shift = xSpacing - diff;
215
+ // Determine the target spacing for overlap resolution
216
+ let targetSpacing = xSpacing;
217
+ if (layer === 0) {
218
+ // More direct check: if all nodes currently being processed together in this group are starting nodes.
219
+ const allNodesInGroupAreStartingNodes = groupNodes.every(gn => getParents(gn, workflow.edges).length === 0);
220
+ if (allNodesInGroupAreStartingNodes) {
221
+ // This condition implies that the group being processed should indeed be the "none" group,
222
+ // consisting only of starting nodes.
223
+ targetSpacing = TRIGGER_X_SPACING;
224
+ }
225
+ }
226
+ if (diff < targetSpacing) {
227
+ const shift = targetSpacing - diff;
157
228
  moveNodeAndChildren(current, shift, workflow.edges);
158
229
  changed = true;
159
230
  }
@@ -161,6 +232,32 @@ export function positionWorkflowNodes(workflow) {
161
232
  }
162
233
  });
163
234
  });
235
+ // Additional step: Ensure common children of multiple layer 0 triggers are centered at ROOT_X.
236
+ // This runs after initial layer placement and before final overlap resolution specific to children groups might occur,
237
+ // and crucially before parent centering which might pull triggers away.
238
+ for (const node of workflow.nodes) {
239
+ const nodeLayer = node.layer;
240
+ if (nodeLayer === 1) { // Check nodes in the layer immediately following triggers
241
+ const parents = getParents(node, workflow.edges);
242
+ if (parents.length > 1) { // Only if it has multiple parents
243
+ const allParentsAreLayer0Triggers = parents.every(p => {
244
+ const parentLayer = p.layer;
245
+ const parentIsStartingNode = getParents(p, workflow.edges).length === 0;
246
+ return parentLayer === 0 && parentIsStartingNode;
247
+ });
248
+ if (allParentsAreLayer0Triggers) {
249
+ if (node.position) { // Ensure node.position exists before trying to read node.position.y
250
+ node.setPosition(ROOT_X, node.position.y);
251
+ }
252
+ else {
253
+ // This case should ideally not happen if nodes in layer 1 are already positioned by the main loop.
254
+ // If it does, assign a default Y based on its layer.
255
+ node.setPosition(ROOT_X, (nodeLayer * ySpacing) + ROOT_Y);
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
164
261
  // Step 4: Bottom-up Parent Centering.
165
262
  // Now, we simply set each parent's x to the exact average of its children.
166
263
  centerParentXPositions(workflow);
@@ -193,6 +290,19 @@ function centerParentXPositions(workflow) {
193
290
  const child = queue.shift();
194
291
  const parents = getParents(child, workflow.edges);
195
292
  for (const parent of parents) {
293
+ // Check if the parent is a layer 0 starting node.
294
+ // Layer information should be available on the node from the assignLayers step.
295
+ const parentLayer = parent.layer;
296
+ const parentIsStartingNode = getParents(parent, workflow.edges).length === 0;
297
+ if (parentLayer === 0 && parentIsStartingNode) {
298
+ // Do not change the X position of layer 0 starting nodes.
299
+ // Still add to queue if not already present, for processing its own parents if it had any (though starting nodes don't).
300
+ // More importantly, it ensures the loop continues correctly for other parents of the current 'child'.
301
+ if (!queue.includes(parent)) {
302
+ queue.push(parent);
303
+ }
304
+ continue; // Skip X-position adjustment for this parent
305
+ }
196
306
  const children = getChildren(parent, workflow.edges);
197
307
  if (children.length) {
198
308
  const xs = children.map(c => c.position.x);
@@ -121,4 +121,3 @@ export function formatNonZeroDecimals(value, nonZeroDecimals = 2) {
121
121
  }
122
122
  return sign + result;
123
123
  }
124
- console.log(formatNonZeroDecimals(0.0000000000000001)); // 0.0000000000000001
@@ -0,0 +1,2 @@
1
+ import { Workflow } from '../../src/index.js';
2
+ export declare function abstract_streamer_live_multiple_triggers(): Promise<Workflow>;
@@ -7,6 +7,8 @@ export declare const WORKFLOW_TEMPLATES_TAGS: {
7
7
  YIELD: string;
8
8
  NOTIFICATIONS: string;
9
9
  ABSTRACT: string;
10
+ DEXES: string;
11
+ LENDING: string;
10
12
  };
11
13
  export declare const WORKFLOW_TEMPLATES: ({
12
14
  name: string;
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "2.0.200";
1
+ export declare const SDK_VERSION = "2.0.201";
2
2
  export declare function compareVersions(v1: string, v2: string): number;
@@ -5,6 +5,7 @@ export declare const xSpacing = 500;
5
5
  export declare const ySpacing = 120;
6
6
  export declare const ROOT_X = 400;
7
7
  export declare const ROOT_Y = 120;
8
+ export declare const TRIGGER_X_SPACING = 363;
8
9
  /**
9
10
  * Main function that positions workflow nodes using a hierarchical layout.
10
11
  * Nodes are grouped by their primary parent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "otomato-sdk",
3
- "version": "2.0.200",
3
+ "version": "2.0.201",
4
4
  "description": "An SDK for building and managing automations on Otomato",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/types/src/index.d.ts",