@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.
|
|
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 +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
|
-
});
|