@paulstinchcombe/gasless-nft-tx 0.12.4 → 0.12.5
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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kami-sponsored-operations.d.ts +46 -2
- package/dist/kami-sponsored-operations.d.ts.map +1 -1
- package/dist/kami-sponsored-operations.js +876 -3
- package/dist/kami-sponsored-operations.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* @file KAMI Sponsored Operations
|
|
4
|
-
* @description
|
|
4
|
+
* @description Sponsored gas operations for all KAMI NFT functions where platform pays ALL gas fees, users pay for tokens only
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.KamiSponsoredOperations = void 0;
|
|
@@ -35,7 +35,7 @@ function getChainFromId(chainId) {
|
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Sponsored KAMI Operations Handler
|
|
38
|
-
* Platform pays ALL gas
|
|
38
|
+
* Platform pays ALL gas fees - Users pay for tokens only
|
|
39
39
|
*/
|
|
40
40
|
class KamiSponsoredOperations {
|
|
41
41
|
publicClient;
|
|
@@ -117,7 +117,7 @@ class KamiSponsoredOperations {
|
|
|
117
117
|
}
|
|
118
118
|
const amount = params.amount !== undefined ? (typeof params.amount === 'string' ? BigInt(params.amount) : params.amount) : 1n; // Default amount for KAMI1155C
|
|
119
119
|
console.log(` Amount: ${amount.toString()}`);
|
|
120
|
-
console.log(` 💰 Platform pays ALL gas - User pays
|
|
120
|
+
console.log(` 💰 Platform pays ALL gas fees - User pays for tokens only`);
|
|
121
121
|
// Track state for cleanup if minting fails
|
|
122
122
|
let userFunded = false;
|
|
123
123
|
let tokensTransferred = false;
|
|
@@ -460,6 +460,840 @@ class KamiSponsoredOperations {
|
|
|
460
460
|
};
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Batch mint tokens with sponsored gas (KAMI721AC only)
|
|
465
|
+
* Owner pays for all tokens - uses batchClaimFor
|
|
466
|
+
* Platform pays ALL gas fees - Owner pays for tokens only
|
|
467
|
+
*/
|
|
468
|
+
async batchMintTokenFor(contractAddress, contractType, params, userSignature) {
|
|
469
|
+
console.log('🎨 Batch minting tokens (owner pays) with sponsored gas...');
|
|
470
|
+
console.log(` Contract: ${contractAddress} (${contractType})`);
|
|
471
|
+
console.log(` Recipients: ${params.recipients.length}`);
|
|
472
|
+
// Only KAMI721AC supports batch minting
|
|
473
|
+
if (contractType !== 'KAMI721AC') {
|
|
474
|
+
return {
|
|
475
|
+
success: false,
|
|
476
|
+
error: `Batch minting is only supported for KAMI721AC, got ${contractType}`,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
// Validate inputs
|
|
480
|
+
if (params.recipients.length === 0) {
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
error: 'Recipients array cannot be empty',
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (params.recipients.length > 100) {
|
|
487
|
+
return {
|
|
488
|
+
success: false,
|
|
489
|
+
error: 'Maximum 100 recipients allowed per batch',
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (params.recipients.length !== params.uris.length) {
|
|
493
|
+
return {
|
|
494
|
+
success: false,
|
|
495
|
+
error: `Recipients length (${params.recipients.length}) must match URIs length (${params.uris.length})`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
// Check for empty URIs
|
|
499
|
+
for (let i = 0; i < params.uris.length; i++) {
|
|
500
|
+
if (!params.uris[i] || params.uris[i].trim().length === 0) {
|
|
501
|
+
return {
|
|
502
|
+
success: false,
|
|
503
|
+
error: `URI at index ${i} cannot be empty`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// Get global mintPrice from contract
|
|
508
|
+
const abi = this.getContractABI(contractType);
|
|
509
|
+
if (!abi) {
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
error: `Unsupported contract type: ${contractType}`,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
let tokenPrice;
|
|
516
|
+
try {
|
|
517
|
+
tokenPrice = (await this.publicClient.readContract({
|
|
518
|
+
address: contractAddress,
|
|
519
|
+
abi,
|
|
520
|
+
functionName: 'mintPrice',
|
|
521
|
+
args: [],
|
|
522
|
+
}));
|
|
523
|
+
console.log(` Using global mintPrice: ${(0, viem_1.formatEther)(tokenPrice)} ETH`);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
return {
|
|
527
|
+
success: false,
|
|
528
|
+
error: `Could not read mintPrice from contract: ${error.message || error}`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (tokenPrice === 0n) {
|
|
532
|
+
return {
|
|
533
|
+
success: false,
|
|
534
|
+
error: 'Global mint price not set. Please set mint price first using setMintPrice().',
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const totalCost = tokenPrice * BigInt(params.recipients.length);
|
|
538
|
+
console.log(` Total cost: ${(0, viem_1.formatEther)(totalCost)} ETH (${params.recipients.length} tokens × ${(0, viem_1.formatEther)(tokenPrice)} ETH)`);
|
|
539
|
+
console.log(` 💰 Platform pays ALL gas fees - Owner pays for tokens only`);
|
|
540
|
+
// Track state for cleanup
|
|
541
|
+
let ownerFunded = false;
|
|
542
|
+
let tokensTransferred = false;
|
|
543
|
+
let ownerAccount = null;
|
|
544
|
+
try {
|
|
545
|
+
// Verify user signature
|
|
546
|
+
const isValidSignature = await this.verifyUserSignature(userSignature);
|
|
547
|
+
if (!isValidSignature) {
|
|
548
|
+
return {
|
|
549
|
+
success: false,
|
|
550
|
+
error: 'Invalid user signature',
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
// Get owner address from signature (should be the one paying)
|
|
554
|
+
const ownerAddress = userSignature.userAddress;
|
|
555
|
+
// Check owner's token balance
|
|
556
|
+
if (totalCost > 0n) {
|
|
557
|
+
console.log('💰 Checking owner payment token balance...');
|
|
558
|
+
const ownerTokenBalance = await this.publicClient.readContract({
|
|
559
|
+
address: this.config.paymentToken,
|
|
560
|
+
abi: [
|
|
561
|
+
{
|
|
562
|
+
type: 'function',
|
|
563
|
+
name: 'balanceOf',
|
|
564
|
+
inputs: [{ name: 'account', type: 'address' }],
|
|
565
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
functionName: 'balanceOf',
|
|
569
|
+
args: [ownerAddress],
|
|
570
|
+
});
|
|
571
|
+
console.log(` Owner Token Balance: ${ownerTokenBalance.toString()}`);
|
|
572
|
+
console.log(` Required: ${totalCost.toString()}`);
|
|
573
|
+
if (ownerTokenBalance < totalCost) {
|
|
574
|
+
return {
|
|
575
|
+
success: false,
|
|
576
|
+
error: `Insufficient owner payment token balance: ${ownerTokenBalance.toString()} (required: ${totalCost.toString()})`,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Handle owner payment token approval
|
|
581
|
+
// Note: Sponsorship is for gas only - owner must still pay for tokens
|
|
582
|
+
// If ownerPrivateKey is not provided, skip token transfer (same pattern as mintToken)
|
|
583
|
+
if (totalCost > 0n) {
|
|
584
|
+
// Check if owner private key is provided
|
|
585
|
+
if (!params.ownerPrivateKey) {
|
|
586
|
+
console.log('⚠️ Owner private key not provided in parameters');
|
|
587
|
+
console.log(' 💡 Frontend needs to include ownerPrivateKey in batch mint parameters');
|
|
588
|
+
console.log(' 🔧 For now, we will skip the token transfer and proceed with batch minting');
|
|
589
|
+
console.log(' 💰 The contract will handle the payment directly');
|
|
590
|
+
console.log('💡 Token transfer skipped - ownerPrivateKey not provided');
|
|
591
|
+
console.log(' 🔧 In production, frontend should provide ownerPrivateKey');
|
|
592
|
+
console.log(' 💰 For now, contract will handle payment directly');
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
ownerAccount = (0, accounts_1.privateKeyToAccount)(params.ownerPrivateKey);
|
|
596
|
+
// Verify owner account matches signature
|
|
597
|
+
if (ownerAccount.address.toLowerCase() !== ownerAddress.toLowerCase()) {
|
|
598
|
+
console.log('⚠️ Owner account mismatch!');
|
|
599
|
+
console.log(` Expected: ${ownerAddress}`);
|
|
600
|
+
console.log(` Got: ${ownerAccount.address}`);
|
|
601
|
+
return {
|
|
602
|
+
success: false,
|
|
603
|
+
error: 'Owner private key does not match signature address',
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
console.log('✅ Owner private key matches signature address');
|
|
607
|
+
// Check owner's ETH balance for gas
|
|
608
|
+
const ownerEthBalance = await this.publicClient.getBalance({
|
|
609
|
+
address: ownerAddress,
|
|
610
|
+
});
|
|
611
|
+
console.log(` Owner ETH Balance: ${(0, viem_1.formatEther)(ownerEthBalance)} ETH`);
|
|
612
|
+
const minGasBalance = (0, viem_1.parseEther)('0.001');
|
|
613
|
+
if (ownerEthBalance < minGasBalance) {
|
|
614
|
+
console.log('💰 Funding owner EOA with gas...');
|
|
615
|
+
console.log(` Current owner balance: ${(0, viem_1.formatEther)(ownerEthBalance)} ETH`);
|
|
616
|
+
console.log(` Funding with: ${(0, viem_1.formatEther)(minGasBalance)} ETH`);
|
|
617
|
+
const fundTx = await this.walletClient.sendTransaction({
|
|
618
|
+
account: this.platformAccount,
|
|
619
|
+
to: ownerAddress,
|
|
620
|
+
value: minGasBalance,
|
|
621
|
+
gas: 21000n,
|
|
622
|
+
chain: this.config.chain || chains_1.baseSepolia,
|
|
623
|
+
});
|
|
624
|
+
console.log(` Funding transaction: ${fundTx}`);
|
|
625
|
+
const fundReceipt = await this.publicClient.waitForTransactionReceipt({ hash: fundTx });
|
|
626
|
+
if (fundReceipt.status !== 'success') {
|
|
627
|
+
return {
|
|
628
|
+
success: false,
|
|
629
|
+
error: `Failed to fund owner EOA: ${fundReceipt.status}`,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
console.log('✅ Owner EOA funded with gas');
|
|
633
|
+
ownerFunded = true;
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
console.log('✅ Owner EOA already has sufficient gas');
|
|
637
|
+
}
|
|
638
|
+
// Owner approves SimpleAccount to spend tokens
|
|
639
|
+
console.log('🔐 Owner approving SimpleAccount to spend tokens...');
|
|
640
|
+
console.log(` Amount: ${totalCost.toString()}`);
|
|
641
|
+
const ownerNonce = await this.publicClient.getTransactionCount({
|
|
642
|
+
address: ownerAddress,
|
|
643
|
+
blockTag: 'pending',
|
|
644
|
+
});
|
|
645
|
+
console.log(` 📊 Using nonce: ${ownerNonce} for SimpleAccount approval transaction`);
|
|
646
|
+
const ownerWalletClient = (0, viem_1.createWalletClient)({
|
|
647
|
+
account: ownerAccount,
|
|
648
|
+
transport: (0, viem_1.http)(this.config.rpcUrl),
|
|
649
|
+
chain: this.config.chain || chains_1.baseSepolia,
|
|
650
|
+
});
|
|
651
|
+
const approveTx = await ownerWalletClient.sendTransaction({
|
|
652
|
+
account: ownerAccount,
|
|
653
|
+
to: this.config.paymentToken,
|
|
654
|
+
data: (0, viem_1.encodeFunctionData)({
|
|
655
|
+
abi: [
|
|
656
|
+
{
|
|
657
|
+
type: 'function',
|
|
658
|
+
name: 'approve',
|
|
659
|
+
inputs: [
|
|
660
|
+
{ name: 'spender', type: 'address' },
|
|
661
|
+
{ name: 'amount', type: 'uint256' },
|
|
662
|
+
],
|
|
663
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
functionName: 'approve',
|
|
667
|
+
args: [this.config.platformSimpleAccountAddress, totalCost],
|
|
668
|
+
}),
|
|
669
|
+
gas: 100000n,
|
|
670
|
+
chain: this.config.chain || chains_1.baseSepolia,
|
|
671
|
+
nonce: ownerNonce,
|
|
672
|
+
});
|
|
673
|
+
console.log(` SimpleAccount approval transaction: ${approveTx}`);
|
|
674
|
+
const approveReceipt = await this.publicClient.waitForTransactionReceipt({ hash: approveTx });
|
|
675
|
+
if (approveReceipt.status !== 'success') {
|
|
676
|
+
return {
|
|
677
|
+
success: false,
|
|
678
|
+
error: `Failed to approve SimpleAccount: ${approveReceipt.status}`,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
console.log('✅ SimpleAccount approved to spend owner tokens');
|
|
682
|
+
// SimpleAccount transfers tokens from owner to itself
|
|
683
|
+
console.log('💸 SimpleAccount transferring tokens from owner to itself...');
|
|
684
|
+
const transferResult = await this.executeViaPlatformSimpleAccount(this.config.paymentToken, [
|
|
685
|
+
{
|
|
686
|
+
type: 'function',
|
|
687
|
+
name: 'transferFrom',
|
|
688
|
+
inputs: [
|
|
689
|
+
{ name: 'from', type: 'address' },
|
|
690
|
+
{ name: 'to', type: 'address' },
|
|
691
|
+
{ name: 'amount', type: 'uint256' },
|
|
692
|
+
],
|
|
693
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
694
|
+
},
|
|
695
|
+
], 'transferFrom', [ownerAddress, this.config.platformSimpleAccountAddress, totalCost], userSignature);
|
|
696
|
+
if (!transferResult.success) {
|
|
697
|
+
return {
|
|
698
|
+
success: false,
|
|
699
|
+
error: `Failed to transfer tokens from owner: ${transferResult.error}`,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
console.log('✅ Tokens transferred from owner to SimpleAccount');
|
|
703
|
+
tokensTransferred = true;
|
|
704
|
+
// SimpleAccount approves KAMI contract to spend tokens
|
|
705
|
+
console.log('🔐 SimpleAccount approving KAMI contract to spend tokens...');
|
|
706
|
+
const contractApproveResult = await this.executeViaPlatformSimpleAccount(this.config.paymentToken, [
|
|
707
|
+
{
|
|
708
|
+
type: 'function',
|
|
709
|
+
name: 'approve',
|
|
710
|
+
inputs: [
|
|
711
|
+
{ name: 'spender', type: 'address' },
|
|
712
|
+
{ name: 'amount', type: 'uint256' },
|
|
713
|
+
],
|
|
714
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
715
|
+
},
|
|
716
|
+
], 'approve', [contractAddress, totalCost], userSignature);
|
|
717
|
+
if (!contractApproveResult.success) {
|
|
718
|
+
return {
|
|
719
|
+
success: false,
|
|
720
|
+
error: `Failed to approve KAMI contract: ${contractApproveResult.error}`,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
console.log('✅ KAMI contract approved to spend SimpleAccount tokens');
|
|
724
|
+
}
|
|
725
|
+
// No need to transfer tokens to SimpleAccount - contract will handle payment directly
|
|
726
|
+
console.log('💡 Contract will handle token payment directly with royalty splitting');
|
|
727
|
+
}
|
|
728
|
+
// Execute batchClaimFor
|
|
729
|
+
console.log('🚀 Executing batchClaimFor...');
|
|
730
|
+
const result = await this.executeViaPlatformSimpleAccount(contractAddress, abi, 'batchClaimFor', [params.recipients, params.uris, params.mintRoyalties], userSignature);
|
|
731
|
+
// Cleanup if successful
|
|
732
|
+
if (result.success && ownerFunded && ownerAccount) {
|
|
733
|
+
await this.cleanupUserEth(ownerAccount);
|
|
734
|
+
}
|
|
735
|
+
if (result.success) {
|
|
736
|
+
// Extract token IDs from transaction logs
|
|
737
|
+
const tokenIds = await this.extractBatchTokenIdsFromTransaction(result.transactionHash, params.recipients);
|
|
738
|
+
return {
|
|
739
|
+
...result,
|
|
740
|
+
data: { tokenIds, recipients: params.recipients },
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
console.log('❌ Error during batch minting process, initiating cleanup...');
|
|
747
|
+
if (tokensTransferred && ownerAccount) {
|
|
748
|
+
console.log('🔄 Refunding tokens to owner...');
|
|
749
|
+
await this.refundTokensToUser(ownerAccount, totalCost);
|
|
750
|
+
}
|
|
751
|
+
if (ownerFunded && ownerAccount) {
|
|
752
|
+
console.log('🔄 Cleaning up owner ETH...');
|
|
753
|
+
await this.cleanupUserEth(ownerAccount);
|
|
754
|
+
}
|
|
755
|
+
console.error('❌ Sponsored batch minting failed:', error);
|
|
756
|
+
return {
|
|
757
|
+
success: false,
|
|
758
|
+
error: error.message || 'Unknown batch minting error',
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Batch mint tokens with sponsored gas (KAMI721AC only)
|
|
764
|
+
* Each recipient pays for themselves - uses batchClaim
|
|
765
|
+
* Platform pays ALL gas fees - Recipients pay for tokens only
|
|
766
|
+
*/
|
|
767
|
+
async batchMintToken(contractAddress, contractType, params, userSignature) {
|
|
768
|
+
console.log('🎨 Batch minting tokens (each pays) with sponsored gas...');
|
|
769
|
+
console.log(` Contract: ${contractAddress} (${contractType})`);
|
|
770
|
+
console.log(` Recipients: ${params.recipients.length}`);
|
|
771
|
+
// Only KAMI721AC supports batch minting
|
|
772
|
+
if (contractType !== 'KAMI721AC') {
|
|
773
|
+
return {
|
|
774
|
+
success: false,
|
|
775
|
+
error: `Batch minting is only supported for KAMI721AC, got ${contractType}`,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
// Validate inputs
|
|
779
|
+
if (params.recipients.length === 0) {
|
|
780
|
+
return {
|
|
781
|
+
success: false,
|
|
782
|
+
error: 'Recipients array cannot be empty',
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
if (params.recipients.length > 100) {
|
|
786
|
+
return {
|
|
787
|
+
success: false,
|
|
788
|
+
error: 'Maximum 100 recipients allowed per batch',
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
if (params.recipients.length !== params.uris.length) {
|
|
792
|
+
return {
|
|
793
|
+
success: false,
|
|
794
|
+
error: `Recipients length (${params.recipients.length}) must match URIs length (${params.uris.length})`,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
// Check for empty URIs
|
|
798
|
+
for (let i = 0; i < params.uris.length; i++) {
|
|
799
|
+
if (!params.uris[i] || params.uris[i].trim().length === 0) {
|
|
800
|
+
return {
|
|
801
|
+
success: false,
|
|
802
|
+
error: `URI at index ${i} cannot be empty`,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
// Get global mintPrice from contract
|
|
807
|
+
const abi = this.getContractABI(contractType);
|
|
808
|
+
if (!abi) {
|
|
809
|
+
return {
|
|
810
|
+
success: false,
|
|
811
|
+
error: `Unsupported contract type: ${contractType}`,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
let tokenPrice;
|
|
815
|
+
try {
|
|
816
|
+
tokenPrice = (await this.publicClient.readContract({
|
|
817
|
+
address: contractAddress,
|
|
818
|
+
abi,
|
|
819
|
+
functionName: 'mintPrice',
|
|
820
|
+
args: [],
|
|
821
|
+
}));
|
|
822
|
+
console.log(` Using global mintPrice: ${(0, viem_1.formatEther)(tokenPrice)} ETH`);
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
return {
|
|
826
|
+
success: false,
|
|
827
|
+
error: `Could not read mintPrice from contract: ${error.message || error}`,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (tokenPrice === 0n) {
|
|
831
|
+
return {
|
|
832
|
+
success: false,
|
|
833
|
+
error: 'Global mint price not set. Please set mint price first using setMintPrice().',
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
console.log(` Cost per token: ${(0, viem_1.formatEther)(tokenPrice)} ETH`);
|
|
837
|
+
console.log(` 💰 Platform pays ALL gas fees - Each recipient pays for their token only`);
|
|
838
|
+
try {
|
|
839
|
+
// Verify user signature
|
|
840
|
+
const isValidSignature = await this.verifyUserSignature(userSignature);
|
|
841
|
+
if (!isValidSignature) {
|
|
842
|
+
return {
|
|
843
|
+
success: false,
|
|
844
|
+
error: 'Invalid user signature',
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
// Check each recipient's token balance
|
|
848
|
+
if (tokenPrice > 0n) {
|
|
849
|
+
console.log('💰 Checking recipient payment token balances...');
|
|
850
|
+
for (let i = 0; i < params.recipients.length; i++) {
|
|
851
|
+
const recipient = params.recipients[i];
|
|
852
|
+
const recipientBalance = await this.publicClient.readContract({
|
|
853
|
+
address: this.config.paymentToken,
|
|
854
|
+
abi: [
|
|
855
|
+
{
|
|
856
|
+
type: 'function',
|
|
857
|
+
name: 'balanceOf',
|
|
858
|
+
inputs: [{ name: 'account', type: 'address' }],
|
|
859
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
860
|
+
},
|
|
861
|
+
],
|
|
862
|
+
functionName: 'balanceOf',
|
|
863
|
+
args: [recipient],
|
|
864
|
+
});
|
|
865
|
+
console.log(` Recipient ${i + 1} (${recipient}): ${recipientBalance.toString()}`);
|
|
866
|
+
if (recipientBalance < tokenPrice) {
|
|
867
|
+
return {
|
|
868
|
+
success: false,
|
|
869
|
+
error: `Insufficient balance for recipient ${i + 1} (${recipient}): ${recipientBalance.toString()} (required: ${tokenPrice.toString()})`,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Handle recipient approvals if private keys are provided
|
|
875
|
+
if (tokenPrice > 0n && params.recipientPrivateKeys && params.recipientPrivateKeys.length > 0) {
|
|
876
|
+
if (params.recipientPrivateKeys.length !== params.recipients.length) {
|
|
877
|
+
return {
|
|
878
|
+
success: false,
|
|
879
|
+
error: `Recipient private keys length (${params.recipientPrivateKeys.length}) must match recipients length (${params.recipients.length})`,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
console.log('🔐 Processing recipient approvals...');
|
|
883
|
+
for (let i = 0; i < params.recipients.length; i++) {
|
|
884
|
+
const recipient = params.recipients[i];
|
|
885
|
+
const recipientPrivateKey = params.recipientPrivateKeys[i];
|
|
886
|
+
if (!recipientPrivateKey) {
|
|
887
|
+
console.log(` ⚠️ Skipping approval for recipient ${i + 1} (no private key)`);
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const recipientAccount = (0, accounts_1.privateKeyToAccount)(recipientPrivateKey);
|
|
891
|
+
// Verify recipient account matches
|
|
892
|
+
if (recipientAccount.address.toLowerCase() !== recipient.toLowerCase()) {
|
|
893
|
+
console.log(` ⚠️ Private key mismatch for recipient ${i + 1}, skipping approval`);
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
// Check recipient's ETH balance
|
|
897
|
+
const recipientEthBalance = await this.publicClient.getBalance({
|
|
898
|
+
address: recipient,
|
|
899
|
+
});
|
|
900
|
+
const minGasBalance = (0, viem_1.parseEther)('0.001');
|
|
901
|
+
if (recipientEthBalance < minGasBalance) {
|
|
902
|
+
console.log(`💰 Funding recipient ${i + 1} EOA with gas...`);
|
|
903
|
+
const fundTx = await this.walletClient.sendTransaction({
|
|
904
|
+
account: this.platformAccount,
|
|
905
|
+
to: recipient,
|
|
906
|
+
value: minGasBalance,
|
|
907
|
+
gas: 21000n,
|
|
908
|
+
chain: this.config.chain || chains_1.baseSepolia,
|
|
909
|
+
});
|
|
910
|
+
await this.publicClient.waitForTransactionReceipt({ hash: fundTx });
|
|
911
|
+
}
|
|
912
|
+
// Recipient approves SimpleAccount
|
|
913
|
+
const recipientNonce = await this.publicClient.getTransactionCount({
|
|
914
|
+
address: recipient,
|
|
915
|
+
blockTag: 'pending',
|
|
916
|
+
});
|
|
917
|
+
const recipientWalletClient = (0, viem_1.createWalletClient)({
|
|
918
|
+
account: recipientAccount,
|
|
919
|
+
transport: (0, viem_1.http)(this.config.rpcUrl),
|
|
920
|
+
chain: this.config.chain || chains_1.baseSepolia,
|
|
921
|
+
});
|
|
922
|
+
const approveTx = await recipientWalletClient.sendTransaction({
|
|
923
|
+
account: recipientAccount,
|
|
924
|
+
to: this.config.paymentToken,
|
|
925
|
+
data: (0, viem_1.encodeFunctionData)({
|
|
926
|
+
abi: [
|
|
927
|
+
{
|
|
928
|
+
type: 'function',
|
|
929
|
+
name: 'approve',
|
|
930
|
+
inputs: [
|
|
931
|
+
{ name: 'spender', type: 'address' },
|
|
932
|
+
{ name: 'amount', type: 'uint256' },
|
|
933
|
+
],
|
|
934
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
935
|
+
},
|
|
936
|
+
],
|
|
937
|
+
functionName: 'approve',
|
|
938
|
+
args: [this.config.platformSimpleAccountAddress, tokenPrice],
|
|
939
|
+
}),
|
|
940
|
+
gas: 100000n,
|
|
941
|
+
chain: this.config.chain || chains_1.baseSepolia,
|
|
942
|
+
nonce: recipientNonce,
|
|
943
|
+
});
|
|
944
|
+
await this.publicClient.waitForTransactionReceipt({ hash: approveTx });
|
|
945
|
+
console.log(` ✅ Recipient ${i + 1} approved SimpleAccount`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// Execute batchClaim
|
|
949
|
+
console.log('🚀 Executing batchClaim...');
|
|
950
|
+
const result = await this.executeViaPlatformSimpleAccount(contractAddress, abi, 'batchClaim', [params.recipients, params.uris, params.mintRoyalties], userSignature);
|
|
951
|
+
if (result.success) {
|
|
952
|
+
// Extract token IDs from transaction logs
|
|
953
|
+
const tokenIds = await this.extractBatchTokenIdsFromTransaction(result.transactionHash, params.recipients);
|
|
954
|
+
return {
|
|
955
|
+
...result,
|
|
956
|
+
data: { tokenIds, recipients: params.recipients },
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
return result;
|
|
960
|
+
}
|
|
961
|
+
catch (error) {
|
|
962
|
+
console.error('❌ Sponsored batch minting failed:', error);
|
|
963
|
+
return {
|
|
964
|
+
success: false,
|
|
965
|
+
error: error.message || 'Unknown batch minting error',
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Batch mint multiple tokens to a single recipient (KAMI721AC only)
|
|
971
|
+
* Uses mint() function multiple times in a single transaction
|
|
972
|
+
* Platform pays ALL gas fees - Owner pays for tokens only
|
|
973
|
+
*/
|
|
974
|
+
async batchMintToRecipient(contractAddress, contractType, params, userSignature) {
|
|
975
|
+
console.log('🎨 Batch minting multiple tokens to single recipient with sponsored gas...');
|
|
976
|
+
console.log(` Contract: ${contractAddress} (${contractType})`);
|
|
977
|
+
console.log(` Recipient: ${params.recipient}`);
|
|
978
|
+
console.log(` Number of tokens: ${params.uris.length}`);
|
|
979
|
+
// Only KAMI721AC supports minting multiple tokens to same recipient
|
|
980
|
+
if (contractType !== 'KAMI721AC') {
|
|
981
|
+
return {
|
|
982
|
+
success: false,
|
|
983
|
+
error: `Batch minting to single recipient is only supported for KAMI721AC, got ${contractType}`,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
// Validate inputs
|
|
987
|
+
if (params.uris.length === 0) {
|
|
988
|
+
return {
|
|
989
|
+
success: false,
|
|
990
|
+
error: 'URIs array cannot be empty',
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
if (params.uris.length > 50) {
|
|
994
|
+
return {
|
|
995
|
+
success: false,
|
|
996
|
+
error: 'Maximum 50 tokens per batch mint to single recipient',
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
// Check for empty URIs
|
|
1000
|
+
for (let i = 0; i < params.uris.length; i++) {
|
|
1001
|
+
if (!params.uris[i] || params.uris[i].trim().length === 0) {
|
|
1002
|
+
return {
|
|
1003
|
+
success: false,
|
|
1004
|
+
error: `URI at index ${i} cannot be empty`,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// Get global mintPrice from contract
|
|
1009
|
+
const abi = this.getContractABI(contractType);
|
|
1010
|
+
if (!abi) {
|
|
1011
|
+
return {
|
|
1012
|
+
success: false,
|
|
1013
|
+
error: `Unsupported contract type: ${contractType}`,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
let tokenPrice;
|
|
1017
|
+
try {
|
|
1018
|
+
tokenPrice = (await this.publicClient.readContract({
|
|
1019
|
+
address: contractAddress,
|
|
1020
|
+
abi,
|
|
1021
|
+
functionName: 'mintPrice',
|
|
1022
|
+
args: [],
|
|
1023
|
+
}));
|
|
1024
|
+
console.log(` Using global mintPrice: ${(0, viem_1.formatEther)(tokenPrice)} ETH`);
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
return {
|
|
1028
|
+
success: false,
|
|
1029
|
+
error: `Could not read mintPrice from contract: ${error.message || error}`,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
if (tokenPrice === 0n) {
|
|
1033
|
+
return {
|
|
1034
|
+
success: false,
|
|
1035
|
+
error: 'Global mint price not set. Please set mint price first using setMintPrice().',
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const totalCost = tokenPrice * BigInt(params.uris.length);
|
|
1039
|
+
console.log(` Total cost: ${(0, viem_1.formatEther)(totalCost)} ETH (${params.uris.length} tokens × ${(0, viem_1.formatEther)(tokenPrice)} ETH)`);
|
|
1040
|
+
console.log(` 💰 Platform pays ALL gas fees - Owner pays for tokens only`);
|
|
1041
|
+
// Track state for cleanup
|
|
1042
|
+
let ownerFunded = false;
|
|
1043
|
+
let tokensTransferred = false;
|
|
1044
|
+
let ownerAccount = null;
|
|
1045
|
+
try {
|
|
1046
|
+
// Verify user signature
|
|
1047
|
+
const isValidSignature = await this.verifyUserSignature(userSignature);
|
|
1048
|
+
if (!isValidSignature) {
|
|
1049
|
+
return {
|
|
1050
|
+
success: false,
|
|
1051
|
+
error: 'Invalid user signature',
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
// Get owner address from signature (should be the one paying)
|
|
1055
|
+
const ownerAddress = userSignature.userAddress;
|
|
1056
|
+
// Check owner's token balance
|
|
1057
|
+
if (totalCost > 0n) {
|
|
1058
|
+
console.log('💰 Checking owner payment token balance...');
|
|
1059
|
+
const ownerTokenBalance = await this.publicClient.readContract({
|
|
1060
|
+
address: this.config.paymentToken,
|
|
1061
|
+
abi: [
|
|
1062
|
+
{
|
|
1063
|
+
type: 'function',
|
|
1064
|
+
name: 'balanceOf',
|
|
1065
|
+
inputs: [{ name: 'account', type: 'address' }],
|
|
1066
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
1067
|
+
},
|
|
1068
|
+
],
|
|
1069
|
+
functionName: 'balanceOf',
|
|
1070
|
+
args: [ownerAddress],
|
|
1071
|
+
});
|
|
1072
|
+
console.log(` Owner Token Balance: ${ownerTokenBalance.toString()}`);
|
|
1073
|
+
console.log(` Required: ${totalCost.toString()}`);
|
|
1074
|
+
if (ownerTokenBalance < totalCost) {
|
|
1075
|
+
return {
|
|
1076
|
+
success: false,
|
|
1077
|
+
error: `Insufficient owner payment token balance: ${ownerTokenBalance.toString()} (required: ${totalCost.toString()})`,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
// Handle owner payment token approval
|
|
1082
|
+
// Note: Sponsorship is for gas only - owner must still pay for tokens
|
|
1083
|
+
// If ownerPrivateKey is not provided, skip token transfer (same pattern as mintToken)
|
|
1084
|
+
if (totalCost > 0n) {
|
|
1085
|
+
// Check if owner private key is provided
|
|
1086
|
+
if (!params.ownerPrivateKey) {
|
|
1087
|
+
console.log('⚠️ Owner private key not provided in parameters');
|
|
1088
|
+
console.log(' 💡 Frontend needs to include ownerPrivateKey in batch mint parameters');
|
|
1089
|
+
console.log(' 🔧 For now, we will skip the token transfer and proceed with batch minting');
|
|
1090
|
+
console.log(' 💰 The contract will handle the payment directly');
|
|
1091
|
+
console.log('💡 Token transfer skipped - ownerPrivateKey not provided');
|
|
1092
|
+
console.log(' 🔧 In production, frontend should provide ownerPrivateKey');
|
|
1093
|
+
console.log(' 💰 For now, contract will handle payment directly');
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
ownerAccount = (0, accounts_1.privateKeyToAccount)(params.ownerPrivateKey);
|
|
1097
|
+
// Verify owner account matches signature
|
|
1098
|
+
if (ownerAccount.address.toLowerCase() !== ownerAddress.toLowerCase()) {
|
|
1099
|
+
console.log('⚠️ Owner account mismatch!');
|
|
1100
|
+
console.log(` Expected: ${ownerAddress}`);
|
|
1101
|
+
console.log(` Got: ${ownerAccount.address}`);
|
|
1102
|
+
return {
|
|
1103
|
+
success: false,
|
|
1104
|
+
error: 'Owner private key does not match signature address',
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
console.log('✅ Owner private key matches signature address');
|
|
1108
|
+
// Check owner's ETH balance for gas
|
|
1109
|
+
const ownerEthBalance = await this.publicClient.getBalance({
|
|
1110
|
+
address: ownerAddress,
|
|
1111
|
+
});
|
|
1112
|
+
console.log(` Owner ETH Balance: ${(0, viem_1.formatEther)(ownerEthBalance)} ETH`);
|
|
1113
|
+
const minGasBalance = (0, viem_1.parseEther)('0.001');
|
|
1114
|
+
if (ownerEthBalance < minGasBalance) {
|
|
1115
|
+
console.log('💰 Funding owner EOA with gas...');
|
|
1116
|
+
console.log(` Current owner balance: ${(0, viem_1.formatEther)(ownerEthBalance)} ETH`);
|
|
1117
|
+
console.log(` Funding with: ${(0, viem_1.formatEther)(minGasBalance)} ETH`);
|
|
1118
|
+
const fundTx = await this.walletClient.sendTransaction({
|
|
1119
|
+
account: this.platformAccount,
|
|
1120
|
+
to: ownerAddress,
|
|
1121
|
+
value: minGasBalance,
|
|
1122
|
+
gas: 21000n,
|
|
1123
|
+
chain: this.config.chain || chains_1.baseSepolia,
|
|
1124
|
+
});
|
|
1125
|
+
console.log(` Funding transaction: ${fundTx}`);
|
|
1126
|
+
const fundReceipt = await this.publicClient.waitForTransactionReceipt({ hash: fundTx });
|
|
1127
|
+
if (fundReceipt.status !== 'success') {
|
|
1128
|
+
return {
|
|
1129
|
+
success: false,
|
|
1130
|
+
error: `Failed to fund owner EOA: ${fundReceipt.status}`,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
console.log('✅ Owner EOA funded with gas');
|
|
1134
|
+
ownerFunded = true;
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
console.log('✅ Owner EOA already has sufficient gas');
|
|
1138
|
+
}
|
|
1139
|
+
// Owner approves SimpleAccount to spend tokens
|
|
1140
|
+
console.log('🔐 Owner approving SimpleAccount to spend tokens...');
|
|
1141
|
+
console.log(` Amount: ${totalCost.toString()}`);
|
|
1142
|
+
const ownerNonce = await this.publicClient.getTransactionCount({
|
|
1143
|
+
address: ownerAddress,
|
|
1144
|
+
blockTag: 'pending',
|
|
1145
|
+
});
|
|
1146
|
+
console.log(` 📊 Using nonce: ${ownerNonce} for SimpleAccount approval transaction`);
|
|
1147
|
+
const ownerWalletClient = (0, viem_1.createWalletClient)({
|
|
1148
|
+
account: ownerAccount,
|
|
1149
|
+
transport: (0, viem_1.http)(this.config.rpcUrl),
|
|
1150
|
+
chain: this.config.chain || chains_1.baseSepolia,
|
|
1151
|
+
});
|
|
1152
|
+
const approveTx = await ownerWalletClient.sendTransaction({
|
|
1153
|
+
account: ownerAccount,
|
|
1154
|
+
to: this.config.paymentToken,
|
|
1155
|
+
data: (0, viem_1.encodeFunctionData)({
|
|
1156
|
+
abi: [
|
|
1157
|
+
{
|
|
1158
|
+
type: 'function',
|
|
1159
|
+
name: 'approve',
|
|
1160
|
+
inputs: [
|
|
1161
|
+
{ name: 'spender', type: 'address' },
|
|
1162
|
+
{ name: 'amount', type: 'uint256' },
|
|
1163
|
+
],
|
|
1164
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
1165
|
+
},
|
|
1166
|
+
],
|
|
1167
|
+
functionName: 'approve',
|
|
1168
|
+
args: [this.config.platformSimpleAccountAddress, totalCost],
|
|
1169
|
+
}),
|
|
1170
|
+
gas: 100000n,
|
|
1171
|
+
chain: this.config.chain || chains_1.baseSepolia,
|
|
1172
|
+
nonce: ownerNonce,
|
|
1173
|
+
});
|
|
1174
|
+
console.log(` SimpleAccount approval transaction: ${approveTx}`);
|
|
1175
|
+
const approveReceipt = await this.publicClient.waitForTransactionReceipt({ hash: approveTx });
|
|
1176
|
+
if (approveReceipt.status !== 'success') {
|
|
1177
|
+
return {
|
|
1178
|
+
success: false,
|
|
1179
|
+
error: `Failed to approve SimpleAccount: ${approveReceipt.status}`,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
console.log('✅ SimpleAccount approved to spend owner tokens');
|
|
1183
|
+
// SimpleAccount transfers tokens from owner to itself
|
|
1184
|
+
console.log('💸 SimpleAccount transferring tokens from owner to itself...');
|
|
1185
|
+
const transferResult = await this.executeViaPlatformSimpleAccount(this.config.paymentToken, [
|
|
1186
|
+
{
|
|
1187
|
+
type: 'function',
|
|
1188
|
+
name: 'transferFrom',
|
|
1189
|
+
inputs: [
|
|
1190
|
+
{ name: 'from', type: 'address' },
|
|
1191
|
+
{ name: 'to', type: 'address' },
|
|
1192
|
+
{ name: 'amount', type: 'uint256' },
|
|
1193
|
+
],
|
|
1194
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
1195
|
+
},
|
|
1196
|
+
], 'transferFrom', [ownerAddress, this.config.platformSimpleAccountAddress, totalCost], userSignature);
|
|
1197
|
+
if (!transferResult.success) {
|
|
1198
|
+
return {
|
|
1199
|
+
success: false,
|
|
1200
|
+
error: `Failed to transfer tokens from owner: ${transferResult.error}`,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
console.log('✅ Tokens transferred from owner to SimpleAccount');
|
|
1204
|
+
tokensTransferred = true;
|
|
1205
|
+
// SimpleAccount approves KAMI contract to spend tokens
|
|
1206
|
+
console.log('🔐 SimpleAccount approving KAMI contract to spend tokens...');
|
|
1207
|
+
const contractApproveResult = await this.executeViaPlatformSimpleAccount(this.config.paymentToken, [
|
|
1208
|
+
{
|
|
1209
|
+
type: 'function',
|
|
1210
|
+
name: 'approve',
|
|
1211
|
+
inputs: [
|
|
1212
|
+
{ name: 'spender', type: 'address' },
|
|
1213
|
+
{ name: 'amount', type: 'uint256' },
|
|
1214
|
+
],
|
|
1215
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
1216
|
+
},
|
|
1217
|
+
], 'approve', [contractAddress, totalCost], userSignature);
|
|
1218
|
+
if (!contractApproveResult.success) {
|
|
1219
|
+
return {
|
|
1220
|
+
success: false,
|
|
1221
|
+
error: `Failed to approve KAMI contract: ${contractApproveResult.error}`,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
console.log('✅ KAMI contract approved to spend SimpleAccount tokens');
|
|
1225
|
+
}
|
|
1226
|
+
// No need to transfer tokens to SimpleAccount - contract will handle payment directly
|
|
1227
|
+
console.log('💡 Contract will handle token payment directly with royalty splitting');
|
|
1228
|
+
}
|
|
1229
|
+
// Execute multiple mint() calls
|
|
1230
|
+
// We'll use SimpleAccount.executeBatch to call mint() multiple times in a single transaction
|
|
1231
|
+
console.log('🚀 Executing batch mint() calls...');
|
|
1232
|
+
// Prepare calldata for each mint call
|
|
1233
|
+
const mintCalldatas = params.uris.map((uri) => (0, viem_1.encodeFunctionData)({
|
|
1234
|
+
abi,
|
|
1235
|
+
functionName: 'mint',
|
|
1236
|
+
args: [params.recipient, uri, params.mintRoyalties],
|
|
1237
|
+
}));
|
|
1238
|
+
// Use SimpleAccount.executeBatch for efficiency (single transaction)
|
|
1239
|
+
const simpleAccountAbi = [
|
|
1240
|
+
{
|
|
1241
|
+
type: 'function',
|
|
1242
|
+
name: 'executeBatch',
|
|
1243
|
+
inputs: [
|
|
1244
|
+
{
|
|
1245
|
+
name: 'dest',
|
|
1246
|
+
type: 'address[]',
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
name: 'value',
|
|
1250
|
+
type: 'uint256[]',
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
name: 'func',
|
|
1254
|
+
type: 'bytes[]',
|
|
1255
|
+
},
|
|
1256
|
+
],
|
|
1257
|
+
outputs: [],
|
|
1258
|
+
},
|
|
1259
|
+
];
|
|
1260
|
+
// Execute batch mint calls in a single transaction
|
|
1261
|
+
const result = await this.executeViaPlatformSimpleAccount(this.config.platformSimpleAccountAddress, simpleAccountAbi, 'executeBatch', [
|
|
1262
|
+
new Array(params.uris.length).fill(contractAddress), // dest array (all same contract)
|
|
1263
|
+
new Array(params.uris.length).fill(0n), // value array (all 0)
|
|
1264
|
+
mintCalldatas, // func array (mint calldatas)
|
|
1265
|
+
], userSignature);
|
|
1266
|
+
// Cleanup if successful
|
|
1267
|
+
if (result.success && ownerFunded && ownerAccount) {
|
|
1268
|
+
await this.cleanupUserEth(ownerAccount);
|
|
1269
|
+
}
|
|
1270
|
+
if (result.success) {
|
|
1271
|
+
// Extract token IDs from transaction logs
|
|
1272
|
+
const tokenIds = await this.extractBatchTokenIdsFromTransaction(result.transactionHash, new Array(params.uris.length).fill(params.recipient));
|
|
1273
|
+
return {
|
|
1274
|
+
...result,
|
|
1275
|
+
data: { tokenIds, recipient: params.recipient, count: params.uris.length },
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
return result;
|
|
1279
|
+
}
|
|
1280
|
+
catch (error) {
|
|
1281
|
+
console.log('❌ Error during batch minting process, initiating cleanup...');
|
|
1282
|
+
if (tokensTransferred && ownerAccount) {
|
|
1283
|
+
console.log('🔄 Refunding tokens to owner...');
|
|
1284
|
+
await this.refundTokensToUser(ownerAccount, totalCost);
|
|
1285
|
+
}
|
|
1286
|
+
if (ownerFunded && ownerAccount) {
|
|
1287
|
+
console.log('🔄 Cleaning up owner ETH...');
|
|
1288
|
+
await this.cleanupUserEth(ownerAccount);
|
|
1289
|
+
}
|
|
1290
|
+
console.error('❌ Sponsored batch minting to recipient failed:', error);
|
|
1291
|
+
return {
|
|
1292
|
+
success: false,
|
|
1293
|
+
error: error.message || 'Unknown batch minting error',
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
463
1297
|
// ============ RENTAL OPERATIONS ============
|
|
464
1298
|
/**
|
|
465
1299
|
* Rent a token with sponsored gas
|
|
@@ -2027,6 +2861,45 @@ class KamiSponsoredOperations {
|
|
|
2027
2861
|
}
|
|
2028
2862
|
return undefined;
|
|
2029
2863
|
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Extract token IDs from batch mint transaction logs
|
|
2866
|
+
*/
|
|
2867
|
+
async extractBatchTokenIdsFromTransaction(txHash, recipients) {
|
|
2868
|
+
try {
|
|
2869
|
+
const receipt = await this.publicClient.getTransactionReceipt({ hash: txHash });
|
|
2870
|
+
// Find Transfer events: Transfer(address indexed from, address indexed to, uint256 indexed tokenId)
|
|
2871
|
+
const transferEventSignature = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
|
|
2872
|
+
const tokenIds = new Array(recipients.length).fill(undefined);
|
|
2873
|
+
const recipientSet = new Set(recipients.map((r) => r.toLowerCase()));
|
|
2874
|
+
for (const log of receipt.logs) {
|
|
2875
|
+
if (log.topics[0] === transferEventSignature) {
|
|
2876
|
+
// topics[1] = from (0x0 for mint)
|
|
2877
|
+
// topics[2] = to (recipient)
|
|
2878
|
+
// topics[3] = tokenId
|
|
2879
|
+
const from = log.topics[1];
|
|
2880
|
+
const to = log.topics[2];
|
|
2881
|
+
// Check if this is a mint (from = 0x0) to one of our recipients
|
|
2882
|
+
if (from === '0x0000000000000000000000000000000000000000000000000000000000000000' && to) {
|
|
2883
|
+
const toAddress = '0x' + to.slice(-40).toLowerCase();
|
|
2884
|
+
if (recipientSet.has(toAddress)) {
|
|
2885
|
+
const tokenId = BigInt(log.topics[3]);
|
|
2886
|
+
// Find which recipient this token belongs to
|
|
2887
|
+
const recipientIndex = recipients.findIndex((r) => r.toLowerCase() === toAddress);
|
|
2888
|
+
if (recipientIndex >= 0) {
|
|
2889
|
+
tokenIds[recipientIndex] = tokenId;
|
|
2890
|
+
console.log(`📝 Minted Token ID ${tokenId} for recipient ${recipientIndex + 1} (${recipients[recipientIndex]})`);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
return tokenIds;
|
|
2897
|
+
}
|
|
2898
|
+
catch (error) {
|
|
2899
|
+
console.warn('Could not extract tokenIds from batch transaction:', error);
|
|
2900
|
+
return new Array(recipients.length).fill(undefined);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2030
2903
|
/**
|
|
2031
2904
|
* Check platform SimpleAccount balance
|
|
2032
2905
|
*/
|