diff --git a/local-tests/test.ts b/local-tests/test.ts index da363737a0..b71cd6be1c 100644 --- a/local-tests/test.ts +++ b/local-tests/test.ts @@ -114,6 +114,7 @@ import { testFailBatchGeneratePrivateKeysAtomic } from './tests/wrapped-keys/tes import { setLitActionsCodeToLocal } from './tests/wrapped-keys/util'; import { testUseEoaSessionSigsToRequestSingleResponse } from './tests/testUseEoaSessionSigsToRequestSingleResponse'; import { testPkpSessionSigsDomain } from './tests/testPkpSessionSigsDomain'; +import { testPruneRLI } from './tests/testPruneRLI'; // Use the current LIT action code to test against setLitActionsCodeToLocal(); @@ -297,6 +298,10 @@ setLitActionsCodeToLocal(); }, }; + const others = { + testPruneRLI, + }; + const testConfig = { tests: { // testExample, @@ -319,6 +324,8 @@ setLitActionsCodeToLocal(); ...relayerTests, ...wrappedKeysTests, + + ...others, }, devEnv, }; diff --git a/local-tests/tests/testPruneRLI.ts b/local-tests/tests/testPruneRLI.ts new file mode 100644 index 0000000000..f1c4ab69db --- /dev/null +++ b/local-tests/tests/testPruneRLI.ts @@ -0,0 +1,18 @@ +import { TinnyEnvironment } from 'local-tests/setup/tinny-environment'; + +/** + * Test Commands: + * ✅ NETWORK=datil-dev yarn test:local --filter=testPruneRLI + * ✅ NETWORK=datil-test yarn test:local --filter=testPruneRLI + * ✅ NETWORK=custom yarn test:local --filter=testPruneRLI + */ +export const testPruneRLI = async (devEnv: TinnyEnvironment) => { + const alice = await devEnv.createRandomPerson(); + + const res = + await alice.contractsClient.rateLimitNftContractUtils.write.pruneExpired( + alice.wallet.address + ); + + console.log(res); +}; diff --git a/packages/contracts-sdk/src/lib/contracts-sdk.ts b/packages/contracts-sdk/src/lib/contracts-sdk.ts index 32b189cd3e..d92a2152e1 100644 --- a/packages/contracts-sdk/src/lib/contracts-sdk.ts +++ b/packages/contracts-sdk/src/lib/contracts-sdk.ts @@ -48,37 +48,36 @@ import * as stakingBalancesContract from '../abis/StakingBalances.sol/StakingBal // ----- autogen:imports:end ----- import { - AUTH_METHOD_TYPE_VALUES, AUTH_METHOD_SCOPE_VALUES, - METAMASK_CHAIN_INFO_BY_NETWORK, - NETWORK_CONTEXT_BY_NETWORK, - LIT_NETWORK_VALUES, - RPC_URL_BY_NETWORK, - HTTP_BY_NETWORK, + AUTH_METHOD_TYPE_VALUES, CENTRALISATION_BY_NETWORK, - LIT_NETWORK, HTTP, HTTPS, + HTTP_BY_NETWORK, InitError, - NetworkError, - WrongNetworkException, - ParamsMissingError, InvalidArgumentException, + LIT_NETWORK, + LIT_NETWORK_VALUES, + METAMASK_CHAIN_INFO_BY_NETWORK, + NETWORK_CONTEXT_BY_NETWORK, + ParamsMissingError, + RPC_URL_BY_NETWORK, TransactionError, + WrongNetworkException, } from '@lit-protocol/constants'; import { LogManager, Logger } from '@lit-protocol/logger'; +import { derivedAddresses } from '@lit-protocol/misc'; import { TokenInfo } from '@lit-protocol/types'; import { computeAddress } from 'ethers/lib/utils'; import { IPubkeyRouter } from '../abis/PKPNFT.sol/PKPNFT'; -import { derivedAddresses } from '@lit-protocol/misc'; import { getAuthIdByAuthMethod, stringToArrayify } from './auth-utils'; import { CIDParser, IPFSHash, getBytes32FromMultihash, } from './helpers/getBytes32FromMultihash'; -import { calculateUTCMidnightExpiration, requestsToKilosecond } from './utils'; import { ValidatorStruct } from './types'; +import { calculateUTCMidnightExpiration, requestsToKilosecond } from './utils'; // const DEFAULT_RPC = 'https://lit-protocol.calderachain.xyz/replica-http'; // const DEFAULT_READ_RPC = 'https://lit-protocol.calderachain.xyz/replica-http'; @@ -748,7 +747,8 @@ export class LitContracts { ): Promise { let address: string = ''; switch (contract) { - case 'Allowlist' || 'AllowList': + case 'Allowlist': + case 'AllowList': address = await resolverContract['getContract']( await resolverContract['ALLOWLIST_CONTRACT'](), environment @@ -2870,6 +2870,106 @@ https://developer.litprotocol.com/v3/sdk/wallets/auth-methods/#auth-method-scope return tx; }, + /** + * Prune expired Capacity Credits NFT (RLI) tokens for a specified owner address. + * This function burns all expired RLI tokens owned by the target address, helping to clean up the blockchain. + * Anyone can call this function to prune expired tokens for any address. + * + * @param {string} ownerAddress - The address of the owner to prune expired tokens for. + * @returns {Promise} - A promise that resolves to the pruning response with transaction details. + * @throws {Error} - If the input parameters are invalid or an error occurs during the pruning process. + */ + pruneExpired: async ( + ownerAddress: string + ): Promise<{ + txHash: string; + actualTokensBurned: number; + }> => { + this.log('Pruning expired Capacity Credits NFTs...'); + + // Validate input: ownerAddress must be a valid Ethereum address + if (!ownerAddress || !ethers.utils.isAddress(ownerAddress)) { + throw new InvalidArgumentException( + { + info: { + ownerAddress, + }, + }, + `A valid owner address is required to prune expired tokens` + ); + } + + this.log(`Target owner address: ${ownerAddress}`); + + try { + // Hardcoded ABI for pruneExpired function + const pruneExpiredABI = [ + { + inputs: [ + { + internalType: 'address', + name: 'owner', + type: 'address', + }, + ], + name: 'pruneExpired', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ]; + + // Create contract instance with the hardcoded ABI + const contractAddress = this.rateLimitNftContract.read.address; + const contract = new ethers.Contract( + contractAddress, + pruneExpiredABI, + this.signer + ); + + // Call the pruneExpired function + const res = await contract['pruneExpired'](ownerAddress); + + const txHash = res.hash; + this.log(`Prune transaction submitted: ${txHash}`); + + const tx = await res.wait(); + this.log('Prune transaction confirmed:', tx); + + // Count the burned tokens from Transfer events (transfers to zero address) + const burnEvents = tx.logs.filter((log: any) => { + try { + const parsedLog = + this.rateLimitNftContract.read.interface.parseLog(log); + return ( + parsedLog.name === 'Transfer' && + parsedLog.args['to'] === ethers.constants.AddressZero + ); + } catch { + return false; + } + }); + + const actualTokensBurned = burnEvents.length; + this.log(`Successfully burned ${actualTokensBurned} expired tokens`); + this.log(`Gas used: ${tx.gasUsed.toString()}`); + + return { + txHash, + actualTokensBurned, + }; + } catch (e) { + throw new TransactionError( + { + info: { + ownerAddress, + }, + cause: e, + }, + 'Pruning expired capacity credits NFTs failed' + ); + } + }, }, };