@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.
- package/dist/TransferBuilder.d.ts +59 -13
- package/dist/TransferBuilder.d.ts.map +1 -1
- package/dist/TransferBuilder.js +397 -112
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.js +4 -8974
- package/package.json +3 -4
package/dist/TransferBuilder.js
CHANGED
|
@@ -1,18 +1,120 @@
|
|
|
1
|
-
import { createFlowMatrix as createFlowMatrixUtil, getTokenInfoMapFromPath, getWrappedTokensFromPath, replaceWrappedTokensWithAvatars, } from '@aboutcircles/sdk-pathfinder';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
44
|
-
this.
|
|
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
|
|
152
|
+
const wrapper = new DemurrageCirclesContractMinimal({
|
|
51
153
|
address: fromTokenAddr,
|
|
52
|
-
rpcUrl: this.
|
|
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
|
|
167
|
+
const wrapper = new InflationaryCirclesContractMinimal({
|
|
66
168
|
address: fromTokenAddr,
|
|
67
|
-
rpcUrl: this.
|
|
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
|
-
...
|
|
189
|
+
...pathfindingOptionsBase,
|
|
86
190
|
...(completeExcludeFromTokens ? { excludeFromTokens: completeExcludeFromTokens } : {}),
|
|
87
191
|
};
|
|
88
|
-
let path = await this.
|
|
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
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
131
|
-
const flowMatrix = createFlowMatrixUtil(fromAddr,
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
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
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
]
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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.
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
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:
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
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:
|
|
273
|
-
data:
|
|
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.
|
|
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.
|
|
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
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,
|
|
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