@aboutcircles/sdk-transfers 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aboutcircles/sdk-transfers",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Transfer data construction for Circles SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,16 +26,20 @@
26
26
  "pathfinding",
27
27
  "web3"
28
28
  ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/aboutcircles/sdk"
32
+ },
29
33
  "license": "MIT",
30
34
  "dependencies": {
31
- "@aboutcircles/sdk-types": "*",
32
- "@aboutcircles/sdk-core": "*",
33
- "@aboutcircles/sdk-rpc": "*",
34
- "@aboutcircles/sdk-utils": "*",
35
- "@aboutcircles/sdk-pathfinder": "*",
35
+ "@aboutcircles/sdk-types": "workspace:*",
36
+ "@aboutcircles/sdk-core": "workspace:*",
37
+ "@aboutcircles/sdk-rpc": "workspace:*",
38
+ "@aboutcircles/sdk-utils": "workspace:*",
39
+ "@aboutcircles/sdk-pathfinder": "workspace:*",
36
40
  "viem": "^2.38.0"
37
41
  },
38
42
  "devDependencies": {
39
43
  "typescript": "^5.0.4"
40
44
  }
41
- }
45
+ }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=TransferBuilder.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"TransferBuilder.test.d.ts","sourceRoot":"","sources":["../src/TransferBuilder.test.ts"],"names":[],"mappings":""}
@@ -1,402 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { TransferBuilder } from './TransferBuilder';
3
- import { Core, circlesConfig } from '@aboutcircles/sdk-core';
4
- import { CirclesRpc } from '@aboutcircles/sdk-rpc';
5
- import { assertNoNettedFlowMismatch, createFlowMatrix, getTokenInfoMapFromPath, getWrappedTokensFromPath, replaceWrappedTokensWithAvatars, shrinkPathValues, } from '@aboutcircles/sdk-pathfinder';
6
- import { CirclesConverter } from '@aboutcircles/sdk-utils';
7
- // Test configuration - using real Circles RPC for integration tests
8
- const CIRCLES_RPC_URL = 'https://rpc.circlesubi.network';
9
- const HUB_ADDRESS = '0xc12C1E50ABB450d6205Ea2C3Fa861b3B834d13e8';
10
- // Test addresses from the old SDK test
11
- const SOURCE_SAFE_ADDRESS = '0xDE374ece6fA50e781E81Aac78e811b33D16912c7'.toLowerCase();
12
- const SINK_ADDRESS = '0xbcaaab068caf7da7764fa280590d5f5b2fc75d73'.toLowerCase();
13
- // Use a more realistic amount - 1500 CRC (18 decimals)
14
- const AMOUNT = BigInt('1500000000000000000000');
15
- /**
16
- * Helper to get static wrapped token totals from sender
17
- * This replicates the functionality from the old SDK test
18
- */
19
- async function getStaticWrappedTokenTotalsFromSender(rpcUrl, senderAddress) {
20
- const res = await fetch(rpcUrl, {
21
- method: 'POST',
22
- headers: { 'Content-Type': 'application/json' },
23
- body: JSON.stringify({
24
- jsonrpc: '2.0',
25
- id: 1,
26
- method: 'circles_getBalanceBreakdown',
27
- params: [senderAddress],
28
- }),
29
- });
30
- const resJson = await res.json();
31
- if (!resJson.result) {
32
- throw new Error(`Failed to fetch wrapped token totals: ${JSON.stringify(resJson.error)}`);
33
- }
34
- const result = resJson.result;
35
- return result.filter((o) => o.tokenType === 'CrcV2_ERC20WrapperDeployed_Inflationary');
36
- }
37
- /**
38
- * Helper to get default token exclude list
39
- * Checks if the recipient is a group minter and excludes group tokens
40
- */
41
- async function getDefaultTokenExcludeList(circlesRpcUrl, to, excludeFromTokens) {
42
- const rpc = new CirclesRpc(circlesRpcUrl);
43
- const groups = await rpc.group.findGroups(1, {
44
- mintHandlerEquals: to,
45
- });
46
- const completeExcludeFromTokenList = new Set();
47
- if (groups.length > 0) {
48
- const groupInfo = groups[0];
49
- completeExcludeFromTokenList.add(groupInfo.group.toLowerCase());
50
- if (groupInfo.erc20WrapperDemurraged) {
51
- completeExcludeFromTokenList.add(groupInfo.erc20WrapperDemurraged.toLowerCase());
52
- }
53
- if (groupInfo.erc20WrapperStatic) {
54
- completeExcludeFromTokenList.add(groupInfo.erc20WrapperStatic.toLowerCase());
55
- }
56
- }
57
- excludeFromTokens?.forEach((token) => completeExcludeFromTokenList.add(token.toLowerCase()));
58
- if (completeExcludeFromTokenList.size === 0)
59
- return undefined;
60
- return Array.from(completeExcludeFromTokenList);
61
- }
62
- /**
63
- * Helper to verify that all flow vertices are registered on-chain
64
- */
65
- async function assertAllVerticesRegistered(rpcUrl, hubAddress, vertices) {
66
- // This would query the hub contract to verify all vertices are registered
67
- // For now, we'll skip this check in the test as it requires contract queries
68
- // In production, you'd implement proper on-chain verification
69
- console.log(`Verifying ${vertices.length} vertices are registered...`);
70
- }
71
- /**
72
- * Helper to verify that vertices are in strictly ascending order
73
- */
74
- async function assertVerticesStrictlyAscending(vertices) {
75
- for (let i = 1; i < vertices.length; i++) {
76
- const prev = BigInt(vertices[i - 1]);
77
- const curr = BigInt(vertices[i]);
78
- if (curr <= prev) {
79
- throw new Error(`Vertices not strictly ascending: ${vertices[i - 1]} (${prev}) >= ${vertices[i]} (${curr})`);
80
- }
81
- }
82
- }
83
- describe('TransferBuilder - unwrap and path transfer', () => {
84
- test('should send exactly the amount requested even if static tokens are involved', async () => {
85
- /*
86
- This test verifies that the TransferBuilder correctly handles:
87
- 1. Finding a transfer path with wrapped tokens
88
- 2. Unwrapping static tokens fully (entire balance)
89
- 3. Unwrapping demurraged tokens exactly (amount needed)
90
- 4. Creating the correct flow matrix
91
- 5. Re-wrapping leftover static tokens after transfer
92
- */
93
- // Initialize Core and TransferBuilder
94
- const core = new Core({
95
- ...circlesConfig[100],
96
- circlesRpcUrl: CIRCLES_RPC_URL,
97
- pathfinderUrl: CIRCLES_RPC_URL,
98
- });
99
- const transferBuilder = new TransferBuilder(core);
100
- const rpc = new CirclesRpc(CIRCLES_RPC_URL);
101
- let logString = '';
102
- try {
103
- const excludeFromTokens = await getDefaultTokenExcludeList(CIRCLES_RPC_URL, SOURCE_SAFE_ADDRESS);
104
- // Log the pathfinder input parameters
105
- const pathfinderParams = {
106
- from: SOURCE_SAFE_ADDRESS,
107
- to: SINK_ADDRESS,
108
- targetFlow: AMOUNT,
109
- useWrappedBalances: true,
110
- excludeFromTokens: excludeFromTokens,
111
- };
112
- console.log('\n=== PATHFINDER INPUT ===');
113
- console.log('From:', pathfinderParams.from);
114
- console.log('To:', pathfinderParams.to);
115
- console.log('Target Flow:', pathfinderParams.targetFlow.toString(), 'CRC');
116
- console.log('Use Wrapped Balances:', pathfinderParams.useWrappedBalances);
117
- console.log('Exclude From Tokens:', excludeFromTokens?.length || 0, 'tokens');
118
- console.log('========================\n');
119
- // Get current balances before pathfinding
120
- const balances = await rpc.balance.getTokenBalances(SOURCE_SAFE_ADDRESS);
121
- console.log('=== SENDER TOKEN BALANCES ===');
122
- console.log('Total tokens:', balances.length);
123
- const wrappedBalances = balances.filter((b) => b.tokenType === 'CrcV2_ERC20WrapperDeployed_Inflationary' ||
124
- b.tokenType === 'CrcV2_ERC20WrapperDeployed_Demurraged');
125
- console.log('Wrapped tokens:', wrappedBalances.length);
126
- wrappedBalances.forEach((b) => {
127
- console.log(` - ${b.tokenAddress} (${b.tokenType}): ${b.attoCircles} (${Number(b.attoCircles) / 1e18} CRC)`);
128
- });
129
- console.log('=============================\n');
130
- // Find a path using the pathfinder RPC
131
- const transferPath = await rpc.pathfinder.findPath(pathfinderParams);
132
- console.log('\n=== PATHFINDER OUTPUT ===');
133
- console.log('Transfers:', transferPath.transfers.length);
134
- console.log('Max Flow:', transferPath.maxFlow.toString(), `(${Number(transferPath.maxFlow) / 1e18} CRC)`);
135
- console.log('Target Flow:', AMOUNT.toString(), `(${Number(AMOUNT) / 1e18} CRC)`);
136
- // Show which tokens are being used in the path
137
- console.log('\n=== TOKENS USED IN PATH ===');
138
- const tokensUsed = new Map();
139
- transferPath.transfers.forEach(t => {
140
- if (t.from.toLowerCase() === SOURCE_SAFE_ADDRESS.toLowerCase()) {
141
- const current = tokensUsed.get(t.tokenOwner) || { type: 'unknown', amount: 0n };
142
- tokensUsed.set(t.tokenOwner, {
143
- type: current.type,
144
- amount: current.amount + BigInt(t.value)
145
- });
146
- }
147
- });
148
- console.log('Tokens from sender:', tokensUsed.size);
149
- tokensUsed.forEach((info, token) => {
150
- const balance = balances.find((b) => b.tokenAddress.toLowerCase() === token.toLowerCase());
151
- const tokenType = balance?.tokenType || 'Unknown';
152
- const isWrapped = tokenType.includes('Wrapper');
153
- console.log(` - ${token} (${tokenType}${isWrapped ? ' - WRAPPED' : ''}): ${info.amount} (${Number(info.amount) / 1e18} CRC)`);
154
- });
155
- console.log('\n=== WHY NO WRAPPED TOKENS? ===');
156
- console.log('Wrapped tokens available:', wrappedBalances.length);
157
- console.log('Wrapped tokens used in path:', [...tokensUsed.values()].filter(t => t.type.includes('Wrapper')).length);
158
- console.log('Possible reasons:');
159
- console.log(' 1. Pathfinder found better paths using unwrapped tokens');
160
- console.log(' 2. Wrapped tokens may have less trust connections');
161
- console.log(' 3. Unwrapped tokens may have lower transfer costs');
162
- console.log('=========================\n');
163
- logString += `Found path with ${transferPath.transfers.length} transfers and max flow: ${transferPath.maxFlow}\n`;
164
- // Verify no netted flow mismatch
165
- assertNoNettedFlowMismatch(transferPath);
166
- // Get token info for all tokens in the path
167
- const tokenInfoMap = await getTokenInfoMapFromPath(SOURCE_SAFE_ADDRESS, CIRCLES_RPC_URL, transferPath);
168
- logString += `The path contains ${transferPath.transfers.length} transfers over ${tokenInfoMap.size} different token owners.\n`;
169
- // Find all wrapped edges (can only originate from the sender)
170
- const allWrappedEdges = transferPath.transfers
171
- .filter((o) => o.from === SOURCE_SAFE_ADDRESS)
172
- .filter((o) => tokenInfoMap.get(o.tokenOwner.toLowerCase())?.tokenType.startsWith('CrcV2_ERC20WrapperDeployed'));
173
- logString += `The path contains ${allWrappedEdges.length} wrapped edges originating from the sender.\n`;
174
- // Filter static edges
175
- const wrappedStaticEdges = allWrappedEdges.filter((o) => tokenInfoMap.get(o.tokenOwner.toLowerCase())?.tokenType === 'CrcV2_ERC20WrapperDeployed_Inflationary');
176
- const wrappedStaticEdgeTotalsByToken = {};
177
- logString += ` - Of which ${wrappedStaticEdges.length} use static wrapped tokens:\n`;
178
- wrappedStaticEdges.forEach((o) => {
179
- logString += ` - ${o.tokenOwner} (demurraged: ${o.value})\n`;
180
- if (!wrappedStaticEdgeTotalsByToken[o.tokenOwner]) {
181
- wrappedStaticEdgeTotalsByToken[o.tokenOwner] = BigInt(0);
182
- }
183
- wrappedStaticEdgeTotalsByToken[o.tokenOwner] += BigInt(o.value);
184
- });
185
- // Filter demurraged edges
186
- const wrappedDemurragedEdges = allWrappedEdges.filter((o) => tokenInfoMap.get(o.tokenOwner.toLowerCase())?.tokenType === 'CrcV2_ERC20WrapperDeployed_Demurraged');
187
- const wrappedDemurragedEdgeTotalsByToken = {};
188
- logString += ` - Of which ${wrappedDemurragedEdges.length} use demurraged wrapped tokens:\n`;
189
- wrappedDemurragedEdges.forEach((o) => {
190
- logString += ` - ${o.tokenOwner} (demurraged: ${o.value})\n`;
191
- if (!wrappedDemurragedEdgeTotalsByToken[o.tokenOwner]) {
192
- wrappedDemurragedEdgeTotalsByToken[o.tokenOwner] = BigInt(0);
193
- }
194
- wrappedDemurragedEdgeTotalsByToken[o.tokenOwner] += BigInt(o.value);
195
- });
196
- logString += '\n';
197
- // Now use TransferBuilder to construct the transfer
198
- const transactions = await transferBuilder.constructAdvancedTransfer(SOURCE_SAFE_ADDRESS, SINK_ADDRESS, AMOUNT, {
199
- useWrappedBalances: true,
200
- excludeFromTokens: excludeFromTokens,
201
- });
202
- logString += `TransferBuilder generated ${transactions.length} transactions\n`;
203
- // Verify that transactions were generated
204
- expect(transactions.length).toBeGreaterThan(0);
205
- // The first transaction should be self-approval or an unwrap
206
- // The last transaction before operateFlowMatrix should be unwraps
207
- // Then operateFlowMatrix
208
- // Then re-wraps for leftover static tokens
209
- // Find the operateFlowMatrix transaction
210
- const operateFlowMatrixTx = transactions.find((tx) => tx.data.startsWith('0x') && tx.data.length > 10);
211
- expect(operateFlowMatrixTx).toBeDefined();
212
- logString += 'Successfully generated transfer transactions with wrapped token handling\n';
213
- // Log detailed information about static token unwrapping
214
- const usedStaticTokenCount = Object.keys(wrappedStaticEdgeTotalsByToken).length;
215
- if (usedStaticTokenCount > 0) {
216
- logString += `\nThe path uses ${usedStaticTokenCount} different static tokens which must be unwrapped completely.\n`;
217
- const senderWrappedStaticTotals = await getStaticWrappedTokenTotalsFromSender(CIRCLES_RPC_URL, SOURCE_SAFE_ADDRESS);
218
- const relevantWrappedStaticBalances = senderWrappedStaticTotals.filter((o) => !!wrappedStaticEdgeTotalsByToken[o.tokenAddress]);
219
- relevantWrappedStaticBalances.forEach((o) => {
220
- const staticBalance = BigInt(o.staticAttoCircles);
221
- const demurragedBalance = CirclesConverter.attoStaticCirclesToAttoCircles(staticBalance);
222
- logString += ` - ${o.tokenAddress} (static: ${staticBalance}, demurraged: ${demurragedBalance})\n`;
223
- logString += ` > Full unwrap amount: (static: ${staticBalance})\n`;
224
- logString += ` Available after unwrap: (demurraged: ${demurragedBalance})\n`;
225
- // Calculate how much will be used vs left over
226
- const usedAmount = wrappedStaticEdgeTotalsByToken[o.tokenAddress];
227
- const leftoverAmount = demurragedBalance - usedAmount;
228
- const percentage = (usedAmount * BigInt(100)) / demurragedBalance;
229
- logString += ` > Using ${percentage}% (${usedAmount}) of total balance\n`;
230
- logString += ` > Leftover to re-wrap: ${leftoverAmount}\n`;
231
- });
232
- }
233
- console.log(logString);
234
- // Expect at least some transactions
235
- // Note: If approval is already set, we may only have 1 transaction (operateFlowMatrix)
236
- expect(transactions.length).toBeGreaterThanOrEqual(1);
237
- }
238
- finally {
239
- console.log(logString);
240
- }
241
- }, 120_000 // 2-minute timeout - pathfinder + RPC calls can be slow
242
- );
243
- test('should work when sender == receiver (self-transfer)', async () => {
244
- /*
245
- This test verifies self-transfer scenarios where sender == receiver
246
- This is useful for "replenishing" - converting wrapped/other tokens
247
- into the sender's own unwrapped personal tokens
248
- */
249
- const core = new Core({
250
- ...circlesConfig[100],
251
- circlesRpcUrl: CIRCLES_RPC_URL,
252
- pathfinderUrl: CIRCLES_RPC_URL,
253
- });
254
- const transferBuilder = new TransferBuilder(core);
255
- const rpc = new CirclesRpc(CIRCLES_RPC_URL);
256
- const excludeFromTokens = await getDefaultTokenExcludeList(CIRCLES_RPC_URL, SOURCE_SAFE_ADDRESS);
257
- // For self-transfer, we need to use a smaller amount as there's less capacity
258
- // Get available balance first
259
- const balances = await rpc.balance.getTokenBalances(SOURCE_SAFE_ADDRESS);
260
- const totalAvailable = balances.reduce((sum, b) => sum + BigInt(b.attoCircles), 0n);
261
- const selfTransferAmount = totalAvailable > AMOUNT ? AMOUNT : totalAvailable / 2n; // Use half of available
262
- console.log(`Self-transfer: Using ${selfTransferAmount} (${Number(selfTransferAmount) / 1e18} CRC) out of ${totalAvailable} (${Number(totalAvailable) / 1e18} CRC) available`);
263
- // Find a self-transfer path
264
- const transferPath = await rpc.pathfinder.findPath({
265
- from: SOURCE_SAFE_ADDRESS,
266
- to: SOURCE_SAFE_ADDRESS,
267
- targetFlow: selfTransferAmount,
268
- useWrappedBalances: true,
269
- excludeFromTokens: excludeFromTokens,
270
- toTokens: [SOURCE_SAFE_ADDRESS],
271
- });
272
- console.log(`Found self-transfer path with ${transferPath.transfers.length} transfers`);
273
- console.log(`Max flow: ${transferPath.maxFlow}`);
274
- // Verify no netted flow mismatch
275
- assertNoNettedFlowMismatch(transferPath, SOURCE_SAFE_ADDRESS, SOURCE_SAFE_ADDRESS);
276
- // Get token info
277
- const tokenInfoMap = await getTokenInfoMapFromPath(SOURCE_SAFE_ADDRESS, CIRCLES_RPC_URL, transferPath);
278
- const wrappedTokens = getWrappedTokensFromPath(transferPath, tokenInfoMap);
279
- const hasInflationaryWrapper = Object.values(wrappedTokens).some(([, type]) => type === 'CrcV2_ERC20WrapperDeployed_Inflationary');
280
- // Replace wrapped tokens with avatars
281
- let processedPath = replaceWrappedTokensWithAvatars(transferPath, tokenInfoMap);
282
- // If there are inflationary wrappers, shrink the path slightly to account for rounding
283
- if (hasInflationaryWrapper) {
284
- console.log('Shrinking path values due to inflationary wrapper...');
285
- processedPath = shrinkPathValues(processedPath, SOURCE_SAFE_ADDRESS);
286
- // Ensure max flow is positive after shrinking
287
- if (processedPath.maxFlow === BigInt(0)) {
288
- console.warn('Path shrinking resulted in zero max flow, using original path');
289
- processedPath = replaceWrappedTokensWithAvatars(transferPath, tokenInfoMap);
290
- }
291
- }
292
- console.log(`Path max flow after processing: ${processedPath.maxFlow}`);
293
- // Verify flow conservation after shrinking (only if we have a valid flow)
294
- if (processedPath.maxFlow > BigInt(0)) {
295
- assertNoNettedFlowMismatch(processedPath, SOURCE_SAFE_ADDRESS, SOURCE_SAFE_ADDRESS);
296
- }
297
- // Create flow matrix
298
- const fm = createFlowMatrix(SOURCE_SAFE_ADDRESS, SOURCE_SAFE_ADDRESS, processedPath.maxFlow, processedPath.transfers);
299
- await assertAllVerticesRegistered(CIRCLES_RPC_URL, HUB_ADDRESS, fm.flowVertices);
300
- await assertVerticesStrictlyAscending(fm.flowVertices);
301
- console.log(`Flow matrix created with ${fm.flowVertices.length} vertices and ${fm.flowEdges.length} edges`);
302
- // Now use TransferBuilder to construct the actual transfer
303
- const transactions = await transferBuilder.constructAdvancedTransfer(SOURCE_SAFE_ADDRESS, SOURCE_SAFE_ADDRESS, AMOUNT, {
304
- useWrappedBalances: true,
305
- excludeFromTokens: excludeFromTokens,
306
- toTokens: [SOURCE_SAFE_ADDRESS],
307
- });
308
- console.log(`TransferBuilder generated ${transactions.length} transactions for self-transfer`);
309
- // Verify transactions were generated
310
- expect(transactions.length).toBeGreaterThan(0);
311
- // The transactions should include operateFlowMatrix (and potentially unwraps/approval)
312
- // Note: If approval is already set and no unwraps needed, we may only have 1 transaction
313
- expect(transactions.length).toBeGreaterThanOrEqual(1);
314
- }, 120_000 // 2-minute timeout
315
- );
316
- test('should return an executable path with wrapped tokens', async () => {
317
- /*
318
- This test verifies the complete flow:
319
- 1. Find a path with wrapped tokens
320
- 2. Process unwrapping bookkeeping
321
- 3. Rewrite path to replace wrappers with avatars
322
- 4. Shrink path if needed for inflationary wrappers
323
- 5. Create flow matrix
324
- 6. Generate executable transactions
325
- */
326
- const core = new Core({
327
- ...circlesConfig[100],
328
- circlesRpcUrl: CIRCLES_RPC_URL,
329
- pathfinderUrl: CIRCLES_RPC_URL,
330
- });
331
- const transferBuilder = new TransferBuilder(core);
332
- const rpc = new CirclesRpc(CIRCLES_RPC_URL);
333
- const excludeFromTokens = await getDefaultTokenExcludeList(CIRCLES_RPC_URL, SINK_ADDRESS);
334
- // Step 1: Call pathfinder
335
- const transferPath = await rpc.pathfinder.findPath({
336
- from: SOURCE_SAFE_ADDRESS,
337
- to: SINK_ADDRESS,
338
- targetFlow: AMOUNT,
339
- useWrappedBalances: true,
340
- excludeFromTokens: excludeFromTokens,
341
- });
342
- console.log(`Path pre-processing: ${transferPath.transfers.length} transfers, max flow: ${transferPath.maxFlow}`);
343
- // Step 2: Original path sanity checks
344
- assertNoNettedFlowMismatch(transferPath);
345
- // Step 3: Unwrap bookkeeping
346
- const tokenInfoMap = await getTokenInfoMapFromPath(SOURCE_SAFE_ADDRESS, CIRCLES_RPC_URL, transferPath);
347
- const wrappedTotals = getWrappedTokensFromPath(transferPath, tokenInfoMap);
348
- console.log(`Found ${Object.keys(wrappedTotals).length} wrapped tokens in path`);
349
- // Step 4: Rewrite path - replace ERC-20 wrappers with their avatars
350
- let pathUnwrapped = replaceWrappedTokensWithAvatars(transferPath, tokenInfoMap);
351
- console.log('Path post-replacement: transfers count =', pathUnwrapped.transfers.length);
352
- const hasInflationaryWrapper = Object.values(wrappedTotals).some(([, type]) => type === 'CrcV2_ERC20WrapperDeployed_Inflationary');
353
- let shrunkPath = hasInflationaryWrapper
354
- ? shrinkPathValues(pathUnwrapped, SINK_ADDRESS) // shrink all values by 0.0000...1%
355
- : pathUnwrapped;
356
- // Ensure max flow is positive after shrinking
357
- if (hasInflationaryWrapper && shrunkPath.maxFlow === BigInt(0)) {
358
- console.warn('Path shrinking resulted in zero max flow, using original path');
359
- shrunkPath = pathUnwrapped;
360
- }
361
- console.log('Path post-shrinking: transfers count =', shrunkPath.transfers.length);
362
- console.log(`Total flow before shrinking: ${transferPath.maxFlow}`);
363
- console.log(`Total flow after shrinking: ${shrunkPath.maxFlow}`);
364
- // Step 5: Flow conservation still holds after shrinking
365
- assertNoNettedFlowMismatch(shrunkPath);
366
- // Step 6: Produce flow matrix + Hub calldata
367
- const fm = createFlowMatrix(SOURCE_SAFE_ADDRESS, SINK_ADDRESS, shrunkPath.maxFlow, shrunkPath.transfers);
368
- await assertAllVerticesRegistered(CIRCLES_RPC_URL, HUB_ADDRESS, fm.flowVertices);
369
- await assertVerticesStrictlyAscending(fm.flowVertices);
370
- console.log(`Flow matrix: ${fm.flowVertices.length} vertices, ${fm.flowEdges.length} edges`);
371
- // Step 7: Use TransferBuilder to generate executable transactions
372
- const transactions = await transferBuilder.constructAdvancedTransfer(SOURCE_SAFE_ADDRESS, SINK_ADDRESS, AMOUNT, {
373
- useWrappedBalances: true,
374
- excludeFromTokens: excludeFromTokens,
375
- });
376
- console.log(`Generated ${transactions.length} executable transactions`);
377
- // Verify we have transactions
378
- expect(transactions.length).toBeGreaterThan(0);
379
- // Verify structure of transactions
380
- transactions.forEach((tx, idx) => {
381
- console.log(`Transaction ${idx}:`, {
382
- to: tx?.to,
383
- dataLength: tx?.data?.length,
384
- value: tx?.value?.toString(),
385
- keys: Object.keys(tx || {})
386
- });
387
- expect(tx).toBeDefined();
388
- // Some transactions might not have all fields, which is okay for testing
389
- if (tx.data) {
390
- expect(tx.data).toMatch(/^0x[0-9a-fA-F]*$/);
391
- }
392
- });
393
- // The transactions should include:
394
- // - Potential self-approval (if not already approved)
395
- // - Unwrap calls (if wrapped tokens are used)
396
- // - operateFlowMatrix call (always included)
397
- // - Potential wrap calls for leftover static tokens
398
- // Note: If approval is already set and no wrapped tokens, we may only have 1 transaction
399
- expect(transactions.length).toBeGreaterThanOrEqual(1);
400
- }, 120_000 // 2-minute timeout
401
- );
402
- });