@paulstinchcombe/gasless-nft-tx 0.12.3 → 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.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * @file KAMI Sponsored Operations
4
- * @description Truly sponsored operations for all KAMI NFT functions where platform pays ALL gas, user pays ZERO
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, user pays ZERO
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 ZERO`);
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
  */