@aboutcircles/sdk 0.1.0

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.
@@ -0,0 +1,644 @@
1
+ import { Observable as ObservableClass, CirclesRpc } from '@aboutcircles/sdk-rpc';
2
+ import { cidV0ToHex, bytesToHex, ValidationError } from '@aboutcircles/sdk-utils';
3
+ import { Profiles } from '@aboutcircles/sdk-profiles';
4
+ import { SdkError } from '../errors';
5
+ import { CirclesType, DemurrageCirclesContract, InflationaryCirclesContract } from '@aboutcircles/sdk-core';
6
+ import { encodeFunctionData } from 'viem';
7
+ import { TransferBuilder } from '@aboutcircles/sdk-transfers';
8
+ /**
9
+ * CommonAvatar abstract class
10
+ * Provides common functionality shared across all avatar types (Human, Organisation, Group)
11
+ *
12
+ * This class extracts common logic for:
13
+ * - Profile management (get, update, metadata)
14
+ * - Balance queries
15
+ * - Trust relationships
16
+ * - Transaction history
17
+ * - Event subscriptions
18
+ */
19
+ export class CommonAvatar {
20
+ address;
21
+ avatarInfo;
22
+ core;
23
+ contractRunner;
24
+ events;
25
+ runner;
26
+ profiles;
27
+ rpc;
28
+ transferBuilder;
29
+ _cachedProfile;
30
+ _cachedProfileCid;
31
+ _eventSubscription;
32
+ constructor(address, core, contractRunner, avatarInfo) {
33
+ this.address = address;
34
+ this.core = core;
35
+ this.contractRunner = contractRunner;
36
+ this.avatarInfo = avatarInfo;
37
+ // Validate contract runner is available
38
+ if (!contractRunner) {
39
+ throw SdkError.notInitialized('ContractRunner');
40
+ }
41
+ if (!contractRunner.sendTransaction) {
42
+ throw SdkError.unsupportedOperation('sendTransaction', 'Contract runner does not support transaction sending');
43
+ }
44
+ this.runner = contractRunner;
45
+ // Initialize profiles client with the profile service URL from config
46
+ this.profiles = new Profiles(core.config.profileServiceUrl);
47
+ // Initialize RPC client
48
+ this.rpc = new CirclesRpc(core.config.circlesRpcUrl);
49
+ // Initialize transfer builder
50
+ this.transferBuilder = new TransferBuilder(core);
51
+ // Event subscription is optional - initialize with stub observable
52
+ const stub = ObservableClass.create();
53
+ this.events = stub.property;
54
+ }
55
+ // ============================================================================
56
+ // Common Balance Methods
57
+ // ============================================================================
58
+ balances = {
59
+ /**
60
+ * Get total balance across all tokens
61
+ */
62
+ getTotal: async () => {
63
+ return await this.rpc.balance.getTotalBalance(this.address);
64
+ },
65
+ /**
66
+ * Get detailed token balances
67
+ */
68
+ getTokenBalances: async () => {
69
+ return await this.rpc.balance.getTokenBalances(this.address);
70
+ },
71
+ /**
72
+ * Get total supply of this avatar's token
73
+ * Override this in subclasses if needed
74
+ */
75
+ getTotalSupply: async () => {
76
+ throw SdkError.unsupportedOperation('getTotalSupply', 'This method is not yet implemented');
77
+ },
78
+ };
79
+ // ============================================================================
80
+ // Common Trust Methods
81
+ // ============================================================================
82
+ trust = {
83
+ /**
84
+ * Trust another avatar or multiple avatars
85
+ *
86
+ * When using Safe runner, all trust operations are executed atomically in a single transaction.
87
+ * When using EOA runner, only single avatars are supported (pass array length 1).
88
+ *
89
+ * @param avatar Single avatar address or array of avatar addresses
90
+ * @param expiry Trust expiry timestamp (in seconds since epoch). Defaults to max uint96 for indefinite trust
91
+ * @returns Transaction response
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * // Trust single avatar indefinitely
96
+ * await avatar.trust.add('0x123...');
97
+ *
98
+ * // Trust with custom expiry
99
+ * const oneYear = BigInt(Date.now() / 1000 + 31536000);
100
+ * await avatar.trust.add('0x123...', oneYear);
101
+ *
102
+ * // Trust multiple avatars (Safe only - throws error with EOA)
103
+ * await avatar.trust.add(['0x123...', '0x456...', '0x789...']);
104
+ * ```
105
+ */
106
+ add: async (avatar, expiry) => {
107
+ // Default to max uint96 for indefinite trust
108
+ const trustExpiry = expiry ?? BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFF');
109
+ // Prepare transaction(s)
110
+ const avatars = Array.isArray(avatar) ? avatar : [avatar];
111
+ if (avatars.length === 0) {
112
+ throw ValidationError.missingParameter('avatar');
113
+ }
114
+ // Create trust transactions for all avatars
115
+ const transactions = avatars.map((trustee) => this.core.hubV2.trust(trustee, trustExpiry));
116
+ // Send transactions to runner
117
+ return await this.runner.sendTransaction(transactions);
118
+ },
119
+ /**
120
+ * Remove trust from another avatar or multiple avatars
121
+ * This is done by setting the trust expiry to 0
122
+ *
123
+ * When using Safe runner, all operations are batched atomically.
124
+ * When using EOA runner, only single avatars are supported (pass array length 1).
125
+ *
126
+ * @param avatar Single avatar address or array of avatar addresses
127
+ * @returns Transaction response
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * // Remove trust from single avatar
132
+ * await avatar.trust.remove('0x123...');
133
+ *
134
+ * // Remove trust from multiple avatars (Safe only)
135
+ * await avatar.trust.remove(['0x123...', '0x456...', '0x789...']);
136
+ * ```
137
+ */
138
+ remove: async (avatar) => {
139
+ // Prepare transaction(s)
140
+ const avatars = Array.isArray(avatar) ? avatar : [avatar];
141
+ if (avatars.length === 0) {
142
+ throw ValidationError.missingParameter('avatar');
143
+ }
144
+ // Validate addresses
145
+ for (const addr of avatars) {
146
+ if (!addr || addr.length !== 42 || !addr.startsWith('0x')) {
147
+ throw ValidationError.invalidAddress(addr);
148
+ }
149
+ }
150
+ // Untrust by setting expiry to 0
151
+ const untrustExpiry = BigInt(0);
152
+ // Create untrust transactions for all avatars
153
+ const transactions = avatars.map((trustee) => this.core.hubV2.trust(trustee, untrustExpiry));
154
+ // Send transactions to runner
155
+ return await this.runner.sendTransaction(transactions);
156
+ },
157
+ /**
158
+ * Check if this avatar trusts another avatar
159
+ * @param otherAvatar The avatar address to check
160
+ * @returns True if this avatar trusts the other avatar
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * const trusting = await avatar.trust.isTrusting('0x123...');
165
+ * if (trusting) {
166
+ * console.log('You trust this avatar');
167
+ * }
168
+ * ```
169
+ */
170
+ isTrusting: async (otherAvatar) => {
171
+ return await this.core.hubV2.isTrusted(this.address, otherAvatar);
172
+ },
173
+ /**
174
+ * Check if another avatar trusts this avatar
175
+ * @param otherAvatar The avatar address to check
176
+ * @returns True if the other avatar trusts this avatar
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const trustedBy = await avatar.trust.isTrustedBy('0x123...');
181
+ * if (trustedBy) {
182
+ * console.log('This avatar trusts you');
183
+ * }
184
+ * ```
185
+ */
186
+ isTrustedBy: async (otherAvatar) => {
187
+ return await this.core.hubV2.isTrusted(otherAvatar, this.address);
188
+ },
189
+ /**
190
+ * Get all trust relations for this avatar
191
+ */
192
+ getAll: async () => {
193
+ return await this.rpc.trust.getAggregatedTrustRelations(this.address);
194
+ },
195
+ };
196
+ // ============================================================================
197
+ // Common Profile Methods
198
+ // ============================================================================
199
+ profile = {
200
+ /**
201
+ * Get the profile for this avatar from IPFS
202
+ * Uses caching to avoid redundant fetches for the same CID
203
+ *
204
+ * @returns The profile data, or undefined if no profile is set or fetch fails
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * const profile = await avatar.profile.get();
209
+ * if (profile) {
210
+ * console.log('Name:', profile.name);
211
+ * console.log('Description:', profile.description);
212
+ * }
213
+ * ```
214
+ */
215
+ get: async () => {
216
+ const profileCid = this.avatarInfo?.cidV0;
217
+ // Return cached profile if CID hasn't changed
218
+ if (this._cachedProfile && this._cachedProfileCid === profileCid) {
219
+ return this._cachedProfile;
220
+ }
221
+ if (!profileCid) {
222
+ return undefined;
223
+ }
224
+ try {
225
+ const profileData = await this.profiles.get(profileCid);
226
+ if (profileData) {
227
+ this._cachedProfile = profileData;
228
+ this._cachedProfileCid = profileCid;
229
+ return this._cachedProfile;
230
+ }
231
+ }
232
+ catch (e) {
233
+ console.warn(`Couldn't load profile for CID ${profileCid}`, e);
234
+ }
235
+ return undefined;
236
+ },
237
+ /**
238
+ * Update the profile for this avatar
239
+ * This will:
240
+ * 1. Pin the new profile data to IPFS via the profile service
241
+ * 2. Update the metadata digest in the name registry contract
242
+ *
243
+ * @param profile The profile data to update
244
+ * @returns The CID of the newly pinned profile
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * const profile = {
249
+ * name: 'Alice',
250
+ * description: 'Hello, Circles!',
251
+ * avatarUrl: 'https://example.com/avatar.png'
252
+ * };
253
+ *
254
+ * const cid = await avatar.profile.update(profile);
255
+ * console.log('Profile updated with CID:', cid);
256
+ * ```
257
+ */
258
+ update: async (profile) => {
259
+ // Step 1: Pin the profile to IPFS and get CID
260
+ const cid = await this.profiles.create(profile);
261
+ if (!cid) {
262
+ throw SdkError.configError('Profile service did not return a CID', { profile });
263
+ }
264
+ // Step 2: Update the metadata digest in the name registry
265
+ const updateReceipt = await this.profile.updateMetadata(cid);
266
+ if (!updateReceipt) {
267
+ throw SdkError.configError('Failed to update metadata digest in name registry', { cid });
268
+ }
269
+ // Update local avatar info if available
270
+ if (this.avatarInfo) {
271
+ this.avatarInfo.cidV0 = cid;
272
+ }
273
+ // Clear cache to force re-fetch
274
+ this._cachedProfile = undefined;
275
+ this._cachedProfileCid = undefined;
276
+ return cid;
277
+ },
278
+ /**
279
+ * Update the metadata digest (CID) in the name registry
280
+ * This updates the on-chain pointer to the profile data stored on IPFS
281
+ *
282
+ * @param cid The IPFS CIDv0 to set as the metadata digest (e.g., "QmXxxx...")
283
+ * @returns Transaction receipt
284
+ *
285
+ * @example
286
+ * ```typescript
287
+ * const receipt = await avatar.profile.updateMetadata('QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG');
288
+ * console.log('Metadata updated, tx hash:', receipt.hash);
289
+ * ```
290
+ */
291
+ updateMetadata: async (cid) => {
292
+ // Convert CIDv0 (base58-encoded multihash) to bytes32 hex format
293
+ // This extracts the 32-byte SHA-256 digest from the CID
294
+ const cidHex = cidV0ToHex(cid);
295
+ const updateTx = this.core.nameRegistry.updateMetadataDigest(cidHex);
296
+ return await this.runner.sendTransaction([updateTx]);
297
+ },
298
+ /**
299
+ * Register a short name for this avatar using a specific nonce
300
+ * Short names are numeric identifiers that can be used instead of addresses
301
+ *
302
+ * @param nonce The nonce to use for generating the short name
303
+ * @returns Transaction receipt
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * // Find available nonce first
308
+ * const [shortName, nonce] = await core.nameRegistry.searchShortName(avatar.address);
309
+ * console.log('Available short name:', shortName.toString());
310
+ *
311
+ * // Register it
312
+ * const receipt = await avatar.profile.registerShortName(Number(nonce));
313
+ * console.log('Short name registered, tx hash:', receipt.hash);
314
+ * ```
315
+ */
316
+ registerShortName: async (nonce) => {
317
+ const registerTx = this.core.nameRegistry.registerShortNameWithNonce(BigInt(nonce));
318
+ return await this.runner.sendTransaction([registerTx]);
319
+ },
320
+ };
321
+ // ============================================================================
322
+ // Common History Methods
323
+ // ============================================================================
324
+ history = {
325
+ /**
326
+ * Get transaction history for this avatar using cursor-based pagination
327
+ * Returns incoming/outgoing transactions and minting events
328
+ *
329
+ * @param limit Number of transactions per page (default: 50)
330
+ * @param sortOrder Sort order for results (default: 'DESC')
331
+ * @returns PagedQuery instance for iterating through transactions
332
+ *
333
+ * @example
334
+ * ```typescript
335
+ * const query = avatar.history.getTransactions(20);
336
+ *
337
+ * // Get first page
338
+ * await query.queryNextPage();
339
+ * query.currentPage.results.forEach(tx => {
340
+ * console.log(`${tx.from} -> ${tx.to}: ${tx.circles} CRC`);
341
+ * });
342
+ *
343
+ * // Get next page if available
344
+ * if (query.currentPage.hasMore) {
345
+ * await query.queryNextPage();
346
+ * }
347
+ * ```
348
+ */
349
+ getTransactions: (limit = 50, sortOrder = 'DESC') => {
350
+ return this.rpc.transaction.getTransactionHistory(this.address, limit, sortOrder);
351
+ },
352
+ };
353
+ // ============================================================================
354
+ // Common Transfer Methods
355
+ // ============================================================================
356
+ transfer = {
357
+ /**
358
+ * Send Circles tokens directly to another address
359
+ * This is a simple direct transfer without pathfinding
360
+ *
361
+ * Supports both ERC1155 (personal/group tokens) and ERC20 (wrapped tokens) transfers.
362
+ * The token type is automatically detected and the appropriate transfer method is used.
363
+ *
364
+ * For transfers using pathfinding (which can use trust network and multiple token types),
365
+ * use transfer.advanced() instead.
366
+ *
367
+ * @param to Recipient address
368
+ * @param amount Amount to transfer (in atto-circles)
369
+ * @param tokenAddress Token address to transfer (defaults to sender's personal token)
370
+ * @param txData Optional transaction data (only used for ERC1155 transfers)
371
+ * @returns Transaction receipt
372
+ *
373
+ * @example
374
+ * ```typescript
375
+ * // Send 100 of your personal CRC directly
376
+ * const receipt = await avatar.transfer.direct('0x123...', BigInt(100e18));
377
+ *
378
+ * // Send wrapped tokens
379
+ * const receipt = await avatar.transfer.direct('0x123...', BigInt(100e18), '0xWrappedTokenAddress...');
380
+ * ```
381
+ */
382
+ direct: async (to, amount, tokenAddress, txData) => {
383
+ // Validate inputs
384
+ if (!to || to.length !== 42 || !to.startsWith('0x')) {
385
+ throw ValidationError.invalidAddress(to);
386
+ }
387
+ if (amount <= 0n) {
388
+ throw ValidationError.invalidAmount(amount, 'Amount must be positive');
389
+ }
390
+ const token = tokenAddress || this.address;
391
+ // Validate token address if provided
392
+ if (tokenAddress && (!tokenAddress || tokenAddress.length !== 42 || !tokenAddress.startsWith('0x'))) {
393
+ throw ValidationError.invalidAddress(tokenAddress);
394
+ }
395
+ // Get token info to determine transfer type
396
+ const tokenInfo = await this.rpc.token.getTokenInfo(token);
397
+ if (!tokenInfo) {
398
+ throw SdkError.configError(`Token not found: ${token}`, { token });
399
+ }
400
+ // Define token type sets
401
+ const erc1155Types = new Set(['CrcV2_RegisterHuman', 'CrcV2_RegisterGroup']);
402
+ const erc20Types = new Set([
403
+ 'CrcV2_ERC20WrapperDeployed_Demurraged',
404
+ 'CrcV2_ERC20WrapperDeployed_Inflationary'
405
+ ]);
406
+ // Route to appropriate transfer method based on token type
407
+ if (erc1155Types.has(tokenInfo.tokenType)) {
408
+ return await this._transferErc1155(token, to, amount, txData);
409
+ }
410
+ else if (erc20Types.has(tokenInfo.tokenType)) {
411
+ return await this._transferErc20(to, amount, token);
412
+ }
413
+ throw SdkError.unsupportedOperation('direct transfer', `Token type ${tokenInfo.tokenType} is not supported for direct transfers`);
414
+ },
415
+ /**
416
+ * Send tokens using pathfinding through the trust network
417
+ * This enables transfers even when you don't have the recipient's token
418
+ *
419
+ * @param to Recipient address
420
+ * @param amount Amount to transfer (in atto-circles or CRC)
421
+ * @param options Advanced transfer options (pathfinding parameters)
422
+ * @returns Transaction receipt
423
+ *
424
+ * @example
425
+ * ```typescript
426
+ * // Send 100 CRC using pathfinding
427
+ * await avatar.transfer.advanced('0x123...', BigInt(100e18));
428
+ *
429
+ * // With custom options
430
+ * await avatar.transfer.advanced('0x123...', 100, {
431
+ * maxTransfers: 5,
432
+ * maxDistance: 3
433
+ * });
434
+ * ```
435
+ */
436
+ advanced: async (to, amount, options) => {
437
+ // Construct transfer using TransferBuilder
438
+ const transactions = await this.transferBuilder.constructAdvancedTransfer(this.address, to, amount, options);
439
+ // Execute the constructed transactions
440
+ return await this.runner.sendTransaction(transactions);
441
+ },
442
+ /**
443
+ * Get the maximum amount that can be transferred to an address using pathfinding
444
+ *
445
+ * @param to Recipient address
446
+ * @returns Maximum transferable amount (in atto-circles)
447
+ *
448
+ * @example
449
+ * ```typescript
450
+ * const maxAmount = await avatar.transfer.getMaxAmount('0x123...');
451
+ * console.log(`Can transfer up to: ${maxAmount}`);
452
+ * ```
453
+ */
454
+ getMaxAmount: async (to) => {
455
+ return await this.rpc.pathfinder.findMaxFlow({
456
+ from: this.address.toLowerCase(),
457
+ to: to.toLowerCase()
458
+ });
459
+ },
460
+ /**
461
+ * Get the maximum amount that can be transferred with custom pathfinding options
462
+ *
463
+ * @param to Recipient address
464
+ * @param options Pathfinding options (maxTransfers, maxDistance, etc.)
465
+ * @returns Maximum transferable amount (in atto-circles)
466
+ *
467
+ * @example
468
+ * ```typescript
469
+ * const maxAmount = await avatar.transfer.getMaxAmountAdvanced('0x123...', {
470
+ * maxTransfers: 3,
471
+ * maxDistance: 2
472
+ * });
473
+ * ```
474
+ */
475
+ getMaxAmountAdvanced: async (to, options) => {
476
+ return await this.rpc.pathfinder.findMaxFlow({
477
+ from: this.address.toLowerCase(),
478
+ to: to.toLowerCase(),
479
+ ...options
480
+ });
481
+ },
482
+ };
483
+ // ============================================================================
484
+ // Common Wrap Methods
485
+ // ============================================================================
486
+ wrap = {
487
+ /**
488
+ * Wrap personal CRC tokens as demurraged ERC20 tokens
489
+ *
490
+ * @param avatarAddress The avatar whose tokens to wrap
491
+ * @param amount Amount to wrap (in atto-circles)
492
+ * @returns Transaction receipt
493
+ *
494
+ * @example
495
+ * ```typescript
496
+ * // Wrap 100 CRC as demurraged ERC20
497
+ * const receipt = await avatar.wrap.asDemurraged(avatar.address, BigInt(100e18));
498
+ * ```
499
+ */
500
+ asDemurraged: async (avatarAddress, amount) => {
501
+ const wrapTx = this.core.hubV2.wrap(avatarAddress, amount, CirclesType.Demurrage);
502
+ return await this.runner.sendTransaction([wrapTx]);
503
+ },
504
+ /**
505
+ * Wrap personal CRC tokens as inflationary ERC20 tokens
506
+ *
507
+ * @param avatarAddress The avatar whose tokens to wrap
508
+ * @param amount Amount to wrap (in atto-circles)
509
+ * @returns Transaction receipt
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * // Wrap 100 CRC as inflationary ERC20
514
+ * const receipt = await avatar.wrap.asInflationary(avatar.address, BigInt(100e18));
515
+ * ```
516
+ */
517
+ asInflationary: async (avatarAddress, amount) => {
518
+ const wrapTx = this.core.hubV2.wrap(avatarAddress, amount, CirclesType.Inflation);
519
+ return await this.runner.sendTransaction([wrapTx]);
520
+ },
521
+ /**
522
+ * Unwrap demurraged ERC20 tokens back to personal CRC
523
+ *
524
+ * @param tokenAddress The demurraged token address to unwrap
525
+ * @param amount Amount to unwrap (in atto-circles)
526
+ * @returns Transaction receipt
527
+ *
528
+ * @example
529
+ * ```typescript
530
+ * const receipt = await avatar.wrap.unwrapDemurraged('0xTokenAddress...', BigInt(100e18));
531
+ * ```
532
+ */
533
+ unwrapDemurraged: async (tokenAddress, amount) => {
534
+ const demurrageContract = new DemurrageCirclesContract({
535
+ address: tokenAddress,
536
+ rpcUrl: this.core.rpcUrl
537
+ });
538
+ const unwrapTx = demurrageContract.unwrap(amount);
539
+ return await this.runner.sendTransaction([unwrapTx]);
540
+ },
541
+ /**
542
+ * Unwrap inflationary ERC20 tokens back to personal CRC
543
+ *
544
+ * @param tokenAddress The inflationary token address to unwrap
545
+ * @param amount Amount to unwrap (in atto-circles)
546
+ * @returns Transaction receipt
547
+ *
548
+ * @example
549
+ * ```typescript
550
+ * const receipt = await avatar.wrap.unwrapInflationary('0xTokenAddress...', BigInt(100e18));
551
+ * ```
552
+ */
553
+ unwrapInflationary: async (tokenAddress, amount) => {
554
+ const inflationaryContract = new InflationaryCirclesContract({
555
+ address: tokenAddress,
556
+ rpcUrl: this.core.rpcUrl
557
+ });
558
+ const unwrapTx = inflationaryContract.unwrap(amount);
559
+ return await this.runner.sendTransaction([unwrapTx]);
560
+ },
561
+ };
562
+ // ============================================================================
563
+ // Common Event Subscription Methods
564
+ // ============================================================================
565
+ /**
566
+ * Subscribe to Circles events for this avatar
567
+ * Events are filtered to only include events related to this avatar's address
568
+ *
569
+ * @returns Promise that resolves when subscription is established
570
+ *
571
+ * @example
572
+ * ```typescript
573
+ * await avatar.subscribeToEvents();
574
+ *
575
+ * // Listen for events
576
+ * avatar.events.subscribe((event) => {
577
+ * console.log('Event received:', event.$event, event);
578
+ *
579
+ * if (event.$event === 'CrcV2_PersonalMint') {
580
+ * console.log('Minted:', event.amount);
581
+ * }
582
+ * });
583
+ * ```
584
+ */
585
+ async subscribeToEvents() {
586
+ // Subscribe to events via RPC WebSocket
587
+ const observable = await this.rpc.client.subscribe(this.address);
588
+ this.events = observable;
589
+ }
590
+ /**
591
+ * Unsubscribe from events
592
+ * Cleans up the WebSocket connection and event listeners
593
+ */
594
+ unsubscribeFromEvents() {
595
+ if (this._eventSubscription) {
596
+ this._eventSubscription();
597
+ this._eventSubscription = undefined;
598
+ }
599
+ }
600
+ // ============================================================================
601
+ // Protected Helper Methods
602
+ // ============================================================================
603
+ /**
604
+ * Transfer ERC1155 tokens using safeTransferFrom
605
+ * @protected
606
+ */
607
+ async _transferErc1155(tokenAddress, to, amount, txData) {
608
+ // Get the token ID for the token address
609
+ const tokenId = await this.core.hubV2.toTokenId(tokenAddress);
610
+ // Convert txData to hex string if provided, otherwise use empty hex
611
+ const data = txData ? bytesToHex(txData) : '0x';
612
+ // Create the safeTransferFrom transaction
613
+ const transferTx = this.core.hubV2.safeTransferFrom(this.address, to, tokenId, amount, data);
614
+ // Execute the transaction
615
+ return await this.runner.sendTransaction([transferTx]);
616
+ }
617
+ /**
618
+ * Transfer ERC20 tokens using the standard transfer function
619
+ * @protected
620
+ */
621
+ async _transferErc20(to, amount, tokenAddress) {
622
+ // Encode the ERC20 transfer function call
623
+ const data = encodeFunctionData({
624
+ abi: [{
625
+ type: 'function',
626
+ name: 'transfer',
627
+ inputs: [
628
+ { name: 'to', type: 'address' },
629
+ { name: 'value', type: 'uint256' }
630
+ ],
631
+ outputs: [{ name: '', type: 'bool' }],
632
+ stateMutability: 'nonpayable',
633
+ }],
634
+ functionName: 'transfer',
635
+ args: [to, amount],
636
+ });
637
+ // Create and send the transaction
638
+ return await this.runner.sendTransaction([{
639
+ to: tokenAddress,
640
+ data,
641
+ value: 0n,
642
+ }]);
643
+ }
644
+ }