@aboutcircles/sdk-transfers 0.1.5 → 0.1.7

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,18 +1,120 @@
1
- import { createFlowMatrix as createFlowMatrixUtil, getTokenInfoMapFromPath, getWrappedTokensFromPath, replaceWrappedTokensWithAvatars, } from '@aboutcircles/sdk-pathfinder';
2
- import { CirclesRpc } from '@aboutcircles/sdk-rpc';
3
- import { bytesToHex, CirclesConverter, encodeFunctionData, ZERO_ADDRESS } from '@aboutcircles/sdk-utils';
4
- import { InflationaryCirclesContract, DemurrageCirclesContract, CirclesType } from '@aboutcircles/sdk-core';
1
+ import { createFlowMatrix as createFlowMatrixUtil, prepareFlowMatrixStreams, getTokenInfoMapFromPath, getWrappedTokensFromPath, replaceWrappedTokensWithAvatars, } from '@aboutcircles/sdk-pathfinder';
2
+ import { RpcClient, PathfinderMethods, BalanceMethods, GroupMethods } from '@aboutcircles/sdk-rpc';
3
+ import { CirclesConverter } from '@aboutcircles/sdk-utils/circlesConverter';
4
+ import { ZERO_ADDRESS } from '@aboutcircles/sdk-utils/constants';
5
+ import { HubV2ContractMinimal, LiftERC20ContractMinimal, DemurrageCirclesContractMinimal, InflationaryCirclesContractMinimal, } from '@aboutcircles/sdk-core/minimal';
6
+ import { CirclesType } from '@aboutcircles/sdk-types';
5
7
  import { TransferError } from './errors';
6
8
  /**
7
9
  * TransferBuilder constructs transfer transactions without executing them
8
10
  * Handles pathfinding, wrapped token unwrapping/wrapping, and flow matrix construction
9
11
  */
10
12
  export class TransferBuilder {
11
- core;
12
- rpc;
13
- constructor(core) {
14
- this.core = core;
15
- this.rpc = new CirclesRpc(core.config.circlesRpcUrl);
13
+ config;
14
+ hubV2;
15
+ liftERC20;
16
+ rpcClient;
17
+ pathfinder;
18
+ balance;
19
+ group;
20
+ constructor(config) {
21
+ this.config = config;
22
+ this.hubV2 = new HubV2ContractMinimal({
23
+ address: config.v2HubAddress,
24
+ rpcUrl: config.circlesRpcUrl,
25
+ });
26
+ this.liftERC20 = new LiftERC20ContractMinimal({
27
+ address: config.liftERC20Address,
28
+ rpcUrl: config.circlesRpcUrl,
29
+ });
30
+ this.rpcClient = new RpcClient(config.circlesRpcUrl);
31
+ this.pathfinder = new PathfinderMethods(this.rpcClient);
32
+ this.balance = new BalanceMethods(this.rpcClient);
33
+ this.group = new GroupMethods(this.rpcClient);
34
+ }
35
+ /**
36
+ * Build flow matrix transaction from a pre-computed path
37
+ * This is a lower-level function useful when you already have a path and want to build transactions
38
+ *
39
+ * @param from Sender address
40
+ * @param to Recipient address
41
+ * @param path Pathfinding result with transfers
42
+ * @param options Advanced transfer options
43
+ * @param aggregate Whether to aggregate tokens at destination
44
+ * @returns Array of transactions to execute in order
45
+ */
46
+ async buildFlowMatrixTx(from, to, path, options, aggregate = false) {
47
+ const fromAddr = from.toLowerCase();
48
+ const toAddr = to.toLowerCase();
49
+ // Validate path
50
+ if (!path.transfers || path.transfers.length === 0) {
51
+ throw TransferError.noPathFound(fromAddr, toAddr);
52
+ }
53
+ let workingPath = { ...path };
54
+ // If aggregate flag is set and toTokens has exactly one element,
55
+ // add an aggregation transfer step from recipient to themselves.
56
+ if (aggregate && options?.toTokens?.length === 1) {
57
+ const aggregateToken = options.toTokens[0].toLowerCase();
58
+ if (path.maxFlow > 0n) {
59
+ // Add a self-transfer to aggregate all tokens into the single token type
60
+ workingPath.transfers.push({
61
+ from: toAddr,
62
+ to: toAddr,
63
+ tokenOwner: aggregateToken,
64
+ value: path.maxFlow
65
+ });
66
+ }
67
+ }
68
+ // Get token info for all tokens in the path using pathfinder utility
69
+ const tokenInfoMap = await getTokenInfoMapFromPath(fromAddr, this.config.circlesRpcUrl, workingPath);
70
+ // Get wrapped tokens found in the path with their amounts and types
71
+ const wrappedTokensInPath = getWrappedTokensFromPath(workingPath, tokenInfoMap);
72
+ const hasWrappedTokens = Object.keys(wrappedTokensInPath).length > 0;
73
+ // Validate that wrapped tokens are enabled if they're needed
74
+ if (hasWrappedTokens && !options?.useWrappedBalances) {
75
+ throw TransferError.wrappedTokensRequired();
76
+ }
77
+ let unwrapCalls = [];
78
+ let wrapCalls = [];
79
+ if (hasWrappedTokens) {
80
+ // Fetch token balances once for both unwrap and wrap operations
81
+ const balanceMap = await this._getTokenBalanceMap(fromAddr);
82
+ // Create unwrap calls for demurraged tokens (unwrap exact amount used in path)
83
+ const demurragedUnwrapCalls = this._createDemurragedUnwrapCalls(wrappedTokensInPath);
84
+ // Create unwrap and wrap calls for inflationary tokens
85
+ const { unwrapCalls: inflationaryUnwrapCalls, wrapCalls: inflationaryWrapCalls } = this._createInflationaryUnwrapAndWrapCalls(wrappedTokensInPath, tokenInfoMap, balanceMap);
86
+ // Combine all unwrap calls
87
+ unwrapCalls = [...demurragedUnwrapCalls, ...inflationaryUnwrapCalls];
88
+ wrapCalls = inflationaryWrapCalls;
89
+ // Replace wrapped token addresses with avatar addresses in the path
90
+ workingPath = replaceWrappedTokensWithAvatars(workingPath, tokenInfoMap);
91
+ }
92
+ // Create flow matrix from the (possibly rewritten) path
93
+ const flowMatrix = createFlowMatrixUtil(fromAddr, toAddr, workingPath.maxFlow, workingPath.transfers);
94
+ // Prepare streams with hex-encoded data and optional txData
95
+ const streamsWithHexData = prepareFlowMatrixStreams(flowMatrix, options?.txData);
96
+ // Create the operateFlowMatrix transaction
97
+ const operateFlowMatrixTx = this.hubV2.operateFlowMatrix(flowMatrix.flowVertices, flowMatrix.flowEdges, streamsWithHexData, flowMatrix.packedCoordinates);
98
+ // Check if self-approval is needed
99
+ let isApproved = false;
100
+ try {
101
+ isApproved = await this.hubV2.isApprovedForAll(fromAddr, fromAddr);
102
+ }
103
+ catch (error) {
104
+ console.warn('Failed to check approval status, including approval transaction:', error);
105
+ }
106
+ // Assemble all transactions in strict order:
107
+ // 1. Self-approval (only if not already approved)
108
+ // 2. All unwraps
109
+ // 3. operateFlowMatrix
110
+ // 4. All wraps (for leftover inflationary tokens)
111
+ const allTransactions = [
112
+ ...(isApproved ? [] : [this.hubV2.setApprovalForAll(fromAddr, true)]),
113
+ ...unwrapCalls,
114
+ operateFlowMatrixTx,
115
+ ...wrapCalls,
116
+ ];
117
+ return allTransactions;
16
118
  }
17
119
  /**
18
120
  * Construct an advanced transfer transaction
@@ -24,7 +126,7 @@ export class TransferBuilder {
24
126
  * @param options Advanced transfer options
25
127
  * @returns Array of transactions to execute in order
26
128
  */
27
- async constructAdvancedTransfer(from, to, amount, options) {
129
+ async constructAdvancedTransfer(from, to, amount, options, aggregate = false) {
28
130
  // Normalize addresses
29
131
  const fromAddr = from.toLowerCase();
30
132
  const toAddr = to.toLowerCase();
@@ -40,16 +142,16 @@ export class TransferBuilder {
40
142
  const toTokenAddr = options.toTokens[0];
41
143
  // Use lift contract to check if fromToken is a wrapper and determine its type
42
144
  const [demurragedWrapper, inflationaryWrapper] = await Promise.all([
43
- this.core.liftERC20.erc20Circles(CirclesType.Demurrage, toTokenAddr),
44
- this.core.liftERC20.erc20Circles(CirclesType.Inflation, toTokenAddr)
145
+ this.liftERC20.erc20Circles(CirclesType.Demurrage, toTokenAddr),
146
+ this.liftERC20.erc20Circles(CirclesType.Inflation, toTokenAddr)
45
147
  ]);
46
148
  // Check if fromToken is a demurraged wrapper for the toToken avatar
47
149
  if (fromTokenAddr.toLowerCase() === demurragedWrapper.toLowerCase() &&
48
150
  demurragedWrapper !== ZERO_ADDRESS) {
49
151
  // Use demurraged wrapper contract to unwrap
50
- const wrapper = new DemurrageCirclesContract({
152
+ const wrapper = new DemurrageCirclesContractMinimal({
51
153
  address: fromTokenAddr,
52
- rpcUrl: this.core.config.circlesRpcUrl
154
+ rpcUrl: this.config.circlesRpcUrl
53
155
  });
54
156
  const unwrapTx = wrapper.unwrap(amountBigInt);
55
157
  return [{
@@ -62,9 +164,9 @@ export class TransferBuilder {
62
164
  if (fromTokenAddr.toLowerCase() === inflationaryWrapper.toLowerCase() &&
63
165
  inflationaryWrapper !== ZERO_ADDRESS) {
64
166
  // Use inflationary wrapper contract to unwrap
65
- const wrapper = new InflationaryCirclesContract({
167
+ const wrapper = new InflationaryCirclesContractMinimal({
66
168
  address: fromTokenAddr,
67
- rpcUrl: this.core.config.circlesRpcUrl
169
+ rpcUrl: this.config.circlesRpcUrl
68
170
  });
69
171
  // Convert demurraged amount to static atto circles for inflationary unwrap
70
172
  const unwrapAmount = CirclesConverter.attoCirclesToAttoStaticCircles(amountBigInt);
@@ -80,12 +182,14 @@ export class TransferBuilder {
80
182
  const truncatedAmount = this._truncateToSixDecimals(amountBigInt);
81
183
  // Get default token exclude list if sending to a group mint handler
82
184
  const completeExcludeFromTokens = await this._getDefaultTokenExcludeList(toAddr, options?.excludeFromTokens);
83
- // Update options with complete exclude list
185
+ // Update options with complete exclude list, but exclude the 'aggregate' flag
186
+ // as it should only be used at the constructAdvancedTransfer level
187
+ const { ...pathfindingOptionsBase } = options || {};
84
188
  const pathfindingOptions = {
85
- ...options,
189
+ ...pathfindingOptionsBase,
86
190
  ...(completeExcludeFromTokens ? { excludeFromTokens: completeExcludeFromTokens } : {}),
87
191
  };
88
- let path = await this.rpc.pathfinder.findPath({
192
+ let path = await this.pathfinder.findPath({
89
193
  from: fromAddr,
90
194
  to: toAddr,
91
195
  targetFlow: truncatedAmount,
@@ -99,88 +203,281 @@ export class TransferBuilder {
99
203
  if (path.maxFlow < truncatedAmount) {
100
204
  throw TransferError.insufficientBalance(truncatedAmount, path.maxFlow, fromAddr, toAddr);
101
205
  }
102
- // Get token info for all tokens in the path using pathfinder utility
103
- // @dev returning a Map<string, TokenInfo>
104
- const tokenInfoMap = await getTokenInfoMapFromPath(fromAddr, this.core.config.circlesRpcUrl, path);
105
- // Get wrapped tokens found in the path with their amounts and types
106
- // @dev returning a Record<string (wrapperAddress), [bigint (amount used in path), string (type)]>
206
+ // Use the buildFlowMatrixTx helper to construct transactions from the path
207
+ return this.buildFlowMatrixTx(fromAddr, toAddr, path, options, aggregate);
208
+ }
209
+ /**
210
+ * Construct a replenish transaction to acquire a specific token in unwrapped form
211
+ *
212
+ * This function tops up your unwrapped balance to reach the target amount (not adding on top).
213
+ *
214
+ * Process:
215
+ * 1. Checks current balance of the target token (unwrapped and wrapped)
216
+ * 2. If sufficient wrapped tokens exist, unwraps only what's needed
217
+ * 3. If insufficient, uses pathfinding with trust simulation to acquire tokens
218
+ * 4. Temporarily trusts the token owner if needed for the transfer
219
+ * 5. Untrusts after the transfer completes
220
+ *
221
+ * Note on Precision:
222
+ * - Pathfinding uses 6-decimal precision (last 12 decimals are truncated)
223
+ * - The function rounds UP to the next 6-decimal boundary to ensure you get at least the target
224
+ * - Final balance will be AT or SLIGHTLY ABOVE target (e.g., 1900.000001 instead of exactly 1900.000000)
225
+ * - The excess is always less than 0.000001 CRC and ensures you never fall short of the target
226
+ *
227
+ * @param from The account address that needs tokens
228
+ * @param tokenId The token ID to replenish (avatar address whose tokens we want)
229
+ * @param amount Target unwrapped balance in atto-circles (will top up to this amount)
230
+ * @param receiver Optional receiver address (defaults to 'from')
231
+ * @returns Array of transactions to execute in order
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * // If you have 100 CRC unwrapped and call replenish(1000 CRC),
236
+ * // it will acquire 900 CRC to reach a total of 1000 CRC
237
+ * const txs = await transferBuilder.constructReplenish(
238
+ * myAddress,
239
+ * tokenAddress,
240
+ * 1000n * 10n**18n // 1000 CRC
241
+ * );
242
+ * ```
243
+ */
244
+ // @todo review the impleementation
245
+ async constructReplenish(from, tokenId, amount, receiver) {
246
+ const fromAddr = from.toLowerCase();
247
+ const tokenIdAddr = tokenId.toLowerCase();
248
+ const receiverAddr = (receiver || from).toLowerCase();
249
+ const amountBigInt = BigInt(amount);
250
+ // Step 1: Check current balances (unwrapped + wrapped)
251
+ const balances = await this.balance.getTokenBalances(fromAddr);
252
+ // Filter balances for the target token
253
+ const targetTokenBalances = balances.filter(b => b.tokenOwner.toLowerCase() === tokenIdAddr);
254
+ let unwrappedBalance = 0n;
255
+ let wrappedDemurrageBalance = 0n;
256
+ let wrappedInflationaryBalance = 0n;
257
+ let wrappedDemurrageAddress = null;
258
+ let wrappedInflationaryAddress = null;
259
+ for (const balance of targetTokenBalances) {
260
+ if (balance.isWrapped) {
261
+ const isDemurrage = balance.tokenType.includes('Demurrage');
262
+ if (isDemurrage) {
263
+ wrappedDemurrageBalance = BigInt(balance.attoCircles);
264
+ wrappedDemurrageAddress = balance.tokenAddress;
265
+ }
266
+ else {
267
+ // For inflationary, use staticAttoCircles to get actual balance
268
+ wrappedInflationaryBalance = BigInt(balance.staticAttoCircles);
269
+ wrappedInflationaryAddress = balance.tokenAddress;
270
+ }
271
+ }
272
+ else {
273
+ unwrappedBalance = BigInt(balance.attoCircles);
274
+ }
275
+ }
276
+ const totalAvailable = unwrappedBalance + wrappedDemurrageBalance +
277
+ CirclesConverter.attoStaticCirclesToAttoCircles(wrappedInflationaryBalance);
278
+ const transactions = [];
279
+ // Step 2: If we already have enough in unwrapped form, we're done
280
+ if (unwrappedBalance >= amountBigInt) {
281
+ console.log(`✓ Already have ${Number(unwrappedBalance) / 1e18} CRC unwrapped (target: ${Number(amountBigInt) / 1e18} CRC). No replenish needed.`);
282
+ // If receiver is different from sender, create transfer transaction
283
+ if (receiverAddr !== fromAddr) {
284
+ const tokenIdBigInt = await this.hubV2.toTokenId(tokenIdAddr);
285
+ const transferTx = this.hubV2.safeTransferFrom(fromAddr, receiverAddr, tokenIdBigInt, amountBigInt);
286
+ transactions.push({
287
+ to: transferTx.to,
288
+ data: transferTx.data,
289
+ value: transferTx.value ?? 0n
290
+ });
291
+ }
292
+ return transactions;
293
+ }
294
+ // Step 3: Calculate deficit (how much more we need to reach the target)
295
+ const deficit = amountBigInt - unwrappedBalance;
296
+ console.log(`Current unwrapped: ${Number(unwrappedBalance) / 1e18} CRC`);
297
+ console.log(`Target amount: ${Number(amountBigInt) / 1e18} CRC`);
298
+ console.log(`Need to acquire: ${Number(deficit) / 1e18} CRC`);
299
+ // Step 4: Try to unwrap if we have enough wrapped tokens
300
+ if (totalAvailable >= amountBigInt) {
301
+ let remainingToUnwrap = deficit;
302
+ // Unwrap demurrage first (exact amount)
303
+ if (wrappedDemurrageBalance > 0n && wrappedDemurrageAddress && remainingToUnwrap > 0n) {
304
+ const toUnwrap = remainingToUnwrap > wrappedDemurrageBalance
305
+ ? wrappedDemurrageBalance
306
+ : remainingToUnwrap;
307
+ const wrapper = new DemurrageCirclesContractMinimal({
308
+ address: wrappedDemurrageAddress,
309
+ rpcUrl: this.config.circlesRpcUrl
310
+ });
311
+ const unwrapTx = wrapper.unwrap(toUnwrap);
312
+ transactions.push({
313
+ to: unwrapTx.to,
314
+ data: unwrapTx.data,
315
+ value: unwrapTx.value ?? 0n,
316
+ });
317
+ remainingToUnwrap -= toUnwrap;
318
+ }
319
+ // Unwrap inflationary if still needed
320
+ if (wrappedInflationaryBalance > 0n && wrappedInflationaryAddress && remainingToUnwrap > 0n) {
321
+ // For inflationary, we need to unwrap in static (inflationary) units
322
+ const staticToUnwrap = CirclesConverter.attoCirclesToAttoStaticCircles(remainingToUnwrap);
323
+ const actualUnwrap = staticToUnwrap > wrappedInflationaryBalance
324
+ ? wrappedInflationaryBalance
325
+ : staticToUnwrap;
326
+ const wrapper = new InflationaryCirclesContractMinimal({
327
+ address: wrappedInflationaryAddress,
328
+ rpcUrl: this.config.circlesRpcUrl
329
+ });
330
+ const unwrapTx = wrapper.unwrap(actualUnwrap);
331
+ transactions.push({
332
+ to: unwrapTx.to,
333
+ data: unwrapTx.data,
334
+ value: unwrapTx.value ?? 0n,
335
+ });
336
+ }
337
+ // If receiver is different, add transfer
338
+ if (receiverAddr !== fromAddr) {
339
+ const tokenIdBigInt = await this.hubV2.toTokenId(tokenIdAddr);
340
+ const transferTx = this.hubV2.safeTransferFrom(fromAddr, receiverAddr, tokenIdBigInt, amountBigInt);
341
+ transactions.push({
342
+ to: transferTx.to,
343
+ data: transferTx.data,
344
+ value: transferTx.value ?? 0n
345
+ });
346
+ }
347
+ return transactions;
348
+ }
349
+ // Step 5: Not enough tokens even with unwrapping, try pathfinding
350
+ // Check if we already trust the token owner
351
+ const alreadyTrusted = await this.hubV2.isTrusted(fromAddr, tokenIdAddr);
352
+ const needsTemporaryTrust = !alreadyTrusted;
353
+ // Calculate current time + 1 year for trust expiry
354
+ const trustExpiry = BigInt(Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60);
355
+ // Try pathfinding with trust simulation (only for the deficit)
356
+ // Round UP the deficit to the next 6-decimal boundary to ensure we get at least the target amount
357
+ // This compensates for the pathfinder's 6-decimal precision
358
+ const truncatedDeficit = CirclesConverter.truncateToInt64(deficit);
359
+ const hasRemainder = deficit % CirclesConverter.FACTOR_1E12 !== 0n;
360
+ const roundedUpDeficit = CirclesConverter.blowUpToBigInt(hasRemainder ? truncatedDeficit + 1n : truncatedDeficit);
361
+ let path;
362
+ try {
363
+ path = await this.pathfinder.findPath({
364
+ from: fromAddr,
365
+ to: receiverAddr,
366
+ targetFlow: roundedUpDeficit,
367
+ toTokens: [tokenIdAddr],
368
+ useWrappedBalances: true,
369
+ simulatedTrusts: needsTemporaryTrust ? [{
370
+ truster: fromAddr,
371
+ trustee: tokenIdAddr
372
+ }] : undefined
373
+ });
374
+ }
375
+ catch (error) {
376
+ // Pathfinding failed
377
+ const availableCrc = Number(totalAvailable) / 1e18;
378
+ const targetCrc = Number(amountBigInt) / 1e18;
379
+ const deficitCrc = Number(deficit) / 1e18;
380
+ throw new TransferError(`Insufficient tokens to replenish. Target: ${targetCrc.toFixed(6)} CRC, ` +
381
+ `Current unwrapped: ${Number(unwrappedBalance) / 1e18} CRC, ` +
382
+ `Need: ${deficitCrc.toFixed(6)} CRC, ` +
383
+ `Available (including all paths): ${availableCrc.toFixed(6)} CRC. ` +
384
+ `Cannot acquire the remaining ${(Number(deficit - (totalAvailable - unwrappedBalance)) / 1e18).toFixed(6)} CRC.`, {
385
+ code: 'REPLENISH_INSUFFICIENT_TOKENS',
386
+ source: 'VALIDATION',
387
+ context: {
388
+ from: fromAddr,
389
+ tokenId: tokenIdAddr,
390
+ target: amountBigInt.toString(),
391
+ unwrapped: unwrappedBalance.toString(),
392
+ deficit: deficit.toString(),
393
+ available: totalAvailable.toString(),
394
+ targetCrc,
395
+ unwrappedCrc: Number(unwrappedBalance) / 1e18,
396
+ deficitCrc,
397
+ availableCrc
398
+ },
399
+ });
400
+ }
401
+ // Check if pathfinder found enough
402
+ if (!path.transfers || path.transfers.length === 0) {
403
+ throw TransferError.noPathFound(fromAddr, receiverAddr, `No path to acquire token ${tokenIdAddr}`);
404
+ }
405
+ // Check if we got enough flow
406
+ // We requested roundedUpDeficit, so we should get at least that much
407
+ if (path.maxFlow < roundedUpDeficit) {
408
+ const pathFlowCrc = Number(path.maxFlow) / 1e18;
409
+ const deficitCrc = Number(roundedUpDeficit) / 1e18;
410
+ throw new TransferError(`Pathfinder can only provide ${pathFlowCrc.toFixed(6)} CRC of the ${deficitCrc.toFixed(6)} CRC deficit needed for token ${tokenIdAddr}.`, {
411
+ code: 'REPLENISH_INSUFFICIENT_PATH_FLOW',
412
+ source: 'PATHFINDING',
413
+ context: {
414
+ from: fromAddr,
415
+ tokenId: tokenIdAddr,
416
+ deficit: roundedUpDeficit.toString(),
417
+ pathFlow: path.maxFlow.toString(),
418
+ deficitCrc,
419
+ pathFlowCrc
420
+ },
421
+ });
422
+ }
423
+ // Step 6: Add temporary trust if needed
424
+ if (needsTemporaryTrust) {
425
+ const trustTx = this.hubV2.trust(tokenIdAddr, trustExpiry);
426
+ transactions.push({
427
+ to: trustTx.to,
428
+ data: trustTx.data,
429
+ value: trustTx.value ?? 0n
430
+ });
431
+ }
432
+ // Step 7: Handle wrapped tokens in path (similar to constructAdvancedTransfer)
433
+ const tokenInfoMap = await getTokenInfoMapFromPath(fromAddr, this.config.circlesRpcUrl, path);
107
434
  const wrappedTokensInPath = getWrappedTokensFromPath(path, tokenInfoMap);
108
- // @todo maybe there is an easier way to check if there are wrapped tokens
109
435
  const hasWrappedTokens = Object.keys(wrappedTokensInPath).length > 0;
110
- // Validate that wrapped tokens are enabled if they're needed
111
- if (hasWrappedTokens && !options?.useWrappedBalances) {
112
- throw TransferError.wrappedTokensRequired();
113
- }
114
436
  let unwrapCalls = [];
115
437
  let wrapCalls = [];
116
438
  if (hasWrappedTokens) {
117
- // Fetch token balances once for both unwrap and wrap operations
118
439
  const balanceMap = await this._getTokenBalanceMap(fromAddr);
119
- // Create unwrap calls for demurraged tokens (unwrap exact amount used in path)
120
440
  const demurragedUnwrapCalls = this._createDemurragedUnwrapCalls(wrappedTokensInPath);
121
- // Create unwrap and wrap calls for inflationary tokens
122
- // Unwrap entire balance, then wrap back leftovers after transfer
123
441
  const { unwrapCalls: inflationaryUnwrapCalls, wrapCalls: inflationaryWrapCalls } = this._createInflationaryUnwrapAndWrapCalls(wrappedTokensInPath, tokenInfoMap, balanceMap);
124
- // Combine all unwrap calls
125
442
  unwrapCalls = [...demurragedUnwrapCalls, ...inflationaryUnwrapCalls];
126
443
  wrapCalls = inflationaryWrapCalls;
127
- // Replace wrapped token addresses with avatar addresses in the path
128
444
  path = replaceWrappedTokensWithAvatars(path, tokenInfoMap);
129
445
  }
130
- // Create flow matrix from the (possibly rewritten) path
131
- const flowMatrix = createFlowMatrixUtil(fromAddr, toAddr, path.maxFlow, path.transfers);
132
- // If txData is provided, attach it to the streams
133
- if (options?.txData && flowMatrix.streams.length > 0) {
134
- flowMatrix.streams[0].data = options.txData;
135
- }
136
- // Convert Uint8Array data to hex strings for ABI encoding
137
- const streamsWithHexData = flowMatrix.streams.map((stream) => ({
138
- sourceCoordinate: stream.sourceCoordinate,
139
- flowEdgeIds: stream.flowEdgeIds,
140
- data: stream.data instanceof Uint8Array ? bytesToHex(stream.data) : stream.data,
141
- }));
142
- // Create the operateFlowMatrix transaction
143
- const operateFlowMatrixTx = this.core.hubV2.operateFlowMatrix(flowMatrix.flowVertices, flowMatrix.flowEdges, streamsWithHexData, flowMatrix.packedCoordinates);
144
- // Check if self-approval is needed
145
- // If the check fails (e.g., network error), we'll include the approval anyway to be safe
446
+ // Step 8: Create flow matrix
447
+ const flowMatrix = createFlowMatrixUtil(fromAddr, receiverAddr, path.maxFlow, path.transfers);
448
+ // Prepare streams with hex-encoded data
449
+ const streamsWithHexData = prepareFlowMatrixStreams(flowMatrix);
450
+ const operateFlowMatrixTxRaw = this.hubV2.operateFlowMatrix(flowMatrix.flowVertices, flowMatrix.flowEdges, streamsWithHexData, flowMatrix.packedCoordinates);
451
+ const operateFlowMatrixTx = {
452
+ to: operateFlowMatrixTxRaw.to,
453
+ data: operateFlowMatrixTxRaw.data,
454
+ value: operateFlowMatrixTxRaw.value ?? 0n
455
+ };
456
+ // Check self-approval
146
457
  let isApproved = false;
147
458
  try {
148
- isApproved = await this.core.hubV2.isApprovedForAll(fromAddr, fromAddr);
459
+ isApproved = await this.hubV2.isApprovedForAll(fromAddr, fromAddr);
149
460
  }
150
461
  catch (error) {
151
- // If checking approval fails, assume not approved and include the approval transaction
152
462
  console.warn('Failed to check approval status, including approval transaction:', error);
153
463
  }
154
- // Assemble all transactions in strict order:
155
- // 1. Self-approval (only if not already approved)
156
- // 2. All unwraps
157
- // 3. operateFlowMatrix
158
- // 4. All wraps (for leftover inflationary tokens)
159
- const allTransactions = [
160
- ...(isApproved ? [] : [this.core.hubV2.setApprovalForAll(fromAddr, true)]),
161
- ...unwrapCalls,
162
- operateFlowMatrixTx,
163
- ...wrapCalls,
164
- ];
165
- return allTransactions;
166
- }
167
- /**
168
- * Construct a replenish transaction to convert wrapped/other tokens into unwrapped personal CRC
169
- * This uses pathfinder to find the best way to convert available tokens (including wrapped tokens)
170
- * into the sender's own unwrapped ERC1155 personal CRC tokens
171
- *
172
- * @param avatarAddress The avatar address to replenish (convert tokens to their personal CRC)
173
- * @param options Optional pathfinding options
174
- * @returns Array of transactions to execute in order to perform the replenish
175
- */
176
- async constructReplenish(avatarAddress, options) {
177
- // @todo Implement replenish functionality
178
- // This should:
179
- // 1. Find maximum flow from avatar to itself targeting personal tokens
180
- // 2. Handle wrapped token unwrapping similar to constructAdvancedTransfer
181
- // 3. Create flow matrix for self-transfer
182
- // 4. Handle wrap calls for leftover inflationary tokens
183
- throw new Error('constructReplenish is not yet implemented. Please use constructAdvancedTransfer with same from/to address as a workaround.');
464
+ // Step 9: Add untrust if we added temporary trust
465
+ if (needsTemporaryTrust) {
466
+ const untrustTx = this.hubV2.trust(tokenIdAddr, 0n); // 0 expiry = untrust
467
+ wrapCalls.push({
468
+ to: untrustTx.to,
469
+ data: untrustTx.data,
470
+ value: untrustTx.value ?? 0n
471
+ });
472
+ }
473
+ // Assemble all transactions in order
474
+ const approvalTxs = isApproved ? [] : [{
475
+ to: this.hubV2.setApprovalForAll(fromAddr, true).to,
476
+ data: this.hubV2.setApprovalForAll(fromAddr, true).data,
477
+ value: 0n
478
+ }];
479
+ transactions.push(...approvalTxs, ...unwrapCalls, operateFlowMatrixTx, ...wrapCalls);
480
+ return transactions;
184
481
  }
185
482
  // ============================================================================
186
483
  // Private Helper Methods
@@ -192,7 +489,7 @@ export class TransferBuilder {
192
489
  * @returns Map of token address to balance (in static units)
193
490
  */
194
491
  async _getTokenBalanceMap(from) {
195
- const allBalances = await this.rpc.balance.getTokenBalances(from);
492
+ const allBalances = await this.balance.getTokenBalances(from);
196
493
  const balanceMap = new Map();
197
494
  // @todo remove any
198
495
  allBalances.forEach((balance) => {
@@ -215,21 +512,15 @@ export class TransferBuilder {
215
512
  continue;
216
513
  }
217
514
  // Create unwrap call for the exact amount used in path
218
- const data = encodeFunctionData({
219
- abi: [{
220
- type: 'function',
221
- name: 'unwrap',
222
- inputs: [{ name: '_amount', type: 'uint256' }],
223
- outputs: [],
224
- stateMutability: 'nonpayable',
225
- }],
226
- functionName: 'unwrap',
227
- args: [amountUsedInPath],
515
+ const wrapper = new DemurrageCirclesContractMinimal({
516
+ address: wrapperAddr,
517
+ rpcUrl: this.config.circlesRpcUrl
228
518
  });
519
+ const unwrapTx = wrapper.unwrap(amountUsedInPath);
229
520
  unwrapCalls.push({
230
- to: wrapperAddr,
231
- data,
232
- value: 0n,
521
+ to: unwrapTx.to,
522
+ data: unwrapTx.data,
523
+ value: unwrapTx.value ?? 0n,
233
524
  });
234
525
  }
235
526
  return unwrapCalls;
@@ -257,21 +548,15 @@ export class TransferBuilder {
257
548
  continue;
258
549
  }
259
550
  // Create unwrap call for the entire balance (in static units)
260
- const unwrapData = encodeFunctionData({
261
- abi: [{
262
- type: 'function',
263
- name: 'unwrap',
264
- inputs: [{ name: '_amount', type: 'uint256' }],
265
- outputs: [],
266
- stateMutability: 'nonpayable',
267
- }],
268
- functionName: 'unwrap',
269
- args: [currentBalance],
551
+ const wrapper = new InflationaryCirclesContractMinimal({
552
+ address: wrapperAddr,
553
+ rpcUrl: this.config.circlesRpcUrl
270
554
  });
555
+ const unwrapTx = wrapper.unwrap(currentBalance);
271
556
  unwrapCalls.push({
272
- to: wrapperAddr,
273
- data: unwrapData,
274
- value: 0n,
557
+ to: unwrapTx.to,
558
+ data: unwrapTx.data,
559
+ value: unwrapTx.value ?? 0n,
275
560
  });
276
561
  // Calculate leftover amount: balance before unwrap (converted to demurraged) - amount used in path
277
562
  const tokenOwner = tokenInfo?.tokenOwner;
@@ -279,7 +564,7 @@ export class TransferBuilder {
279
564
  // Only create wrap call if there's leftover amount
280
565
  if (leftoverAmount > 0n) {
281
566
  // Create wrap call using hubV2 contract
282
- const wrapTx = this.core.hubV2.wrap(tokenOwner, leftoverAmount, CirclesType.Inflation // 1 = Inflationary
567
+ const wrapTx = this.hubV2.wrap(tokenOwner, leftoverAmount, CirclesType.Inflation // 1 = Inflationary
283
568
  );
284
569
  wrapCalls.push({
285
570
  to: wrapTx.to,
@@ -308,7 +593,7 @@ export class TransferBuilder {
308
593
  */
309
594
  async _getDefaultTokenExcludeList(to, excludeFromTokens) {
310
595
  // Check if recipient is a group mint handler
311
- const groups = await this.rpc.group.findGroups(1, {
596
+ const groups = await this.group.findGroups(1, {
312
597
  mintHandlerEquals: to,
313
598
  });
314
599
  const completeExcludeFromTokenList = new Set();
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CirclesError } from '@aboutcircles/sdk-utils';
1
+ import { CirclesError } from '@aboutcircles/sdk-utils/errors';
2
2
  import type { Address } from '@aboutcircles/sdk-types';
3
3
  /**
4
4
  * Transfers package error source
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEvD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,aAAa,GAAG,aAAa,GAAG,YAAY,CAAC;AAE9F;;GAEG;AACH,qBAAa,aAAc,SAAQ,YAAY,CAAC,oBAAoB,CAAC;gBAEjE,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,oBAAoB,CAAC;QAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KAC/B;IAKH;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa;IAW9E;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,aAAa;IAqB3G;;OAEG;IACH,MAAM,CAAC,qBAAqB,IAAI,aAAa;IAU7C;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,aAAa;IAc/D;;OAEG;IACH,MAAM,CAAC,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa;IAc/E;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,aAAa;CAU5D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEvD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,aAAa,GAAG,aAAa,GAAG,YAAY,CAAC;AAE9F;;GAEG;AACH,qBAAa,aAAc,SAAQ,YAAY,CAAC,oBAAoB,CAAC;gBAEjE,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,oBAAoB,CAAC;QAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KAC/B;IAKH;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa;IAW9E;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,aAAa;IAqB3G;;OAEG;IACH,MAAM,CAAC,qBAAqB,IAAI,aAAa;IAU7C;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,aAAa;IAc/D;;OAEG;IACH,MAAM,CAAC,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa;IAc/E;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,aAAa;CAU5D"}
package/dist/errors.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CirclesError } from '@aboutcircles/sdk-utils';
1
+ import { CirclesError } from '@aboutcircles/sdk-utils/errors';
2
2
  /**
3
3
  * Base error for transfers package
4
4
  */