UNPKG

@myria/airdrop-js

Version:

Airdrop in L1 with claim based approach

392 lines 28.7 kB
"use strict"; /** * Set of single functions in our airdrop-js * to let consumers pick and use with their strategies * @module Transaction/Core */ Object.defineProperty(exports, "__esModule", { value: true }); exports.retryPrepareAndSubmitRawTransaction = exports.sendTransactionAndWaitForReceipt = exports.saveSnapshotByOwner = exports.saveMerkleRootByOwner = exports.generateMerkleTreeInfoERC20ForWhitelist = exports.approveAirdropAsSpender = exports.claimAirdropToken = exports.getThirdwebContract = exports.getOwnerOfContract = exports.getNextNonce = exports.getGasFeeInfo = exports.getRpcClientByChain = exports.setMerkleRoot = exports.saveSnapshot = void 0; // re-exports: forward export from thirdweb var airdrop_1 = require("thirdweb/extensions/airdrop"); Object.defineProperty(exports, "saveSnapshot", { enumerable: true, get: function () { return airdrop_1.saveSnapshot; } }); Object.defineProperty(exports, "setMerkleRoot", { enumerable: true, get: function () { return airdrop_1.setMerkleRoot; } }); // External imports below this line const thirdweb_1 = require("thirdweb"); const airdrop_2 = require("thirdweb/extensions/airdrop"); const erc20_1 = require("thirdweb/extensions/erc20"); // Internal imports below this line const Retry_1 = require("../common/Retry"); const ConstType_1 = require("../type/ConstType"); const internalUse_1 = require("./internalUse"); /** * Returns an RPC request that can be used to make JSON-RPC requests * * @param {ThirdwebClient} client - Thirdweb client. * @param {CreateRpcClientOptions} options - Options to create RpcClient. */ /* eslint-disable @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types */ const getRpcClientByChain = (client, options) => { const { selectingChain, chain } = options; if (chain) { return (0, thirdweb_1.getRpcClient)({ client, chain, }); } return (0, thirdweb_1.getRpcClient)({ client, chain: (0, internalUse_1.getThirdWebChain)(selectingChain), }); }; exports.getRpcClientByChain = getRpcClientByChain; /** * This callback type is called `transactionCallback` and is displayed as a global symbol. * * @callback transactionCallback * @param {TransactionReceipt} result - The transaction receipt received from submitting. */ /** * Calculate gas fee info need to paid to submit a transaction * @see {@link https://ethereum.org/en/developers/docs/gas|Ethereum's gas} or @see {@link https://support.metamask.io/transactions-and-gas/gas-fees/user-guide-gas/|Metamask's gas} * @see {@link https://etherscan.io/gastracker|Gas Tracker} * * @param {EstimateGasOptions} options - The options for estimating gas. * @param {boolean} isLogResult - Whether to log the result or not. Default true. * @returns {Promise<GasFeeInfo>} Promise object represents the gas fee info to perform an on-chain transaction * @throws An error if the account is missed. */ const getGasFeeInfo = async (options, extraGasOptions = {}, isLogResult = true) => { const { transaction, account } = options; if (!account) { throw Error('require the account as sender of the transaction'); } const { extraGasPercentage = ConstType_1.DEFAULT_EXTRA_GAS_PERCENTAGE, extraMaxPriorityFeePerGasPercentage = ConstType_1.DEFAULT_EXTRA_PRIORITY_TIP_PERCENTAGE, extraOnRetryPercentage = ConstType_1.DEFAULT_EXTRA_ON_RETRY_PERCENTAGE, } = extraGasOptions; const { chain, client } = transaction; const rpcClient = (0, exports.getRpcClientByChain)(client, { chain }); // Estimate gas use for the transaction // -> gas limit const gasLimit = await (0, thirdweb_1.estimateGas)({ transaction, account, }); // -> gas extra const extraGas = (gasLimit / BigInt(100)) * BigInt(extraGasPercentage + extraOnRetryPercentage); // Gas price per unit of gas // -> base fee const gasPrice = await (0, thirdweb_1.eth_gasPrice)(rpcClient); // -> priority & max fee const maxPriorityFeePerGas = await (0, thirdweb_1.eth_maxPriorityFeePerGas)(rpcClient); const extraMaxPriorityFeePerGas = (maxPriorityFeePerGas / BigInt(100)) * BigInt(extraMaxPriorityFeePerGasPercentage + extraOnRetryPercentage); const maxFeePerGas = gasPrice + maxPriorityFeePerGas + extraMaxPriorityFeePerGas; // Estimate Gas Cost to compare with real transaction const gasCost = await (0, thirdweb_1.estimateGasCost)({ transaction, account }); console.log(` [getGasFeeInfo] gasCost.ether = ${gasCost.ether} - gasCost.wei = ${gasCost.wei}`); const gasFeeInfo = { gas: gasLimit, gasPrice, maxPriorityFeePerGas, maxFeePerGas, extraGas, }; if (isLogResult) { console.log(` [getGasFeeInfo]: gasFeeInfo = ${JSON.stringify(gasFeeInfo)}`); } return gasFeeInfo; }; exports.getGasFeeInfo = getGasFeeInfo; /** * Retrieves the transaction count (nonce) for a given Ethereum address. * * @see {@link https://ethereum.org/en/developers/docs/gas} for GAS AND FEES. * @see {@link https://etherscan.io/gastracker|Gas Tracker} * * @param {Address} address - The Ethereum address of Sender. * @param {ThirdwebClient} client - Thirdweb client. * @param {CreateRpcClientOptions} options - Options to create RpcClient. * @param {boolean} isLogResult - Whether to log the result or not. Default true. * @returns {Promise<number>} Promise object represents the next transaction nonce */ const getNextNonce = async (address, client, options, isLogResult = true) => { const startTime = (0, internalUse_1.logFunctionTrackStartTime)(exports.getNextNonce.name); const rpcClient = (0, exports.getRpcClientByChain)(client, options); const transactionNonce = await (0, thirdweb_1.eth_getTransactionCount)(rpcClient, { address, }); if (isLogResult) { // TODO: replace with the logger library for better format or other targets console.log(` [getNextNonce]>[response] transactionNonce = ${transactionNonce}`); } (0, internalUse_1.logFunctionDuration)(exports.getNextNonce.name, startTime); return transactionNonce; }; exports.getNextNonce = getNextNonce; /** * Reads owner of a smart contract. * * @param {ThirdwebContract} contract - The Thirdweb contract. * @returns {Promise<string>} A promise that resolves with the result of the owner ethereum address. */ const getOwnerOfContract = async (contract) => { return await (0, thirdweb_1.readContract)({ contract, // Pass a snippet of the ABI for the method you want to call. method: { type: 'function', name: 'owner', inputs: [], outputs: [ { type: 'address', name: '', internalType: 'address', }, ], stateMutability: 'view', }, params: [], }); }; exports.getOwnerOfContract = getOwnerOfContract; /** * Creates a Thirdweb contract by combining the Thirdweb client and contract options. * * @param {Address} address - The ethereum smart contract address. * @param {ThirdwebClient} client - Thirdweb client. * @param {SupportingChain} selectingChain - The selecting chain. * @returns The Thirdweb contract. */ /* eslint-disable @typescript-eslint/explicit-function-return-type*/ const getThirdwebContract = (address, client, selectingChain) => { return (0, thirdweb_1.getContract)({ client, chain: (0, internalUse_1.getThirdWebChain)(selectingChain), address, }); }; exports.getThirdwebContract = getThirdwebContract; /** * End-User connects wallet to trigger Claim from Client side * * @param {Address} tokenAddress - The token address to claim. * @param {Account} account - The Account represent as sender @see {@link https://ethereum.org/en/glossary/#account|Account's Ethereum}. * @param {ThirdwebContract} airdropContract - The airdrop Thirdweb contract. * @param {transactionCallback} callback - The callback that handles the post-submit state. * @param {boolean} isLogResult - Whether to log the result or not. Default true. * @returns {Promise<TransactionReceipt>} A promise that resolves to the confirmed transaction receipt. * @throws An error if the wallet is not connected. * @example * ```ts * import { claimAirdropToken } from "./index"; * * const transactionReceipt = await claimAirdropToken( * tokenAddress, * account, * airdropContract * ); * ``` */ const claimAirdropToken = async (tokenAddress, account, airdropContract, callback, isLogResult = true) => { const claimTransaction = (0, airdrop_2.claimERC20)({ contract: airdropContract, tokenAddress, recipient: account.address, }); // Send the transaction return (0, internalUse_1.doSubmitTransaction)({ transaction: claimTransaction, account }, callback, isLogResult); }; exports.claimAirdropToken = claimAirdropToken; /** * Token contract owner approve airdrop contract address as spender with amount * * @param {Address} spender - The airdrop smart contract address as spender. * @param {number} amount - The total airdrop amount in ether format. * @param {Account} account - The Account represent as sender @see {@link https://ethereum.org/en/glossary/#account|Account's Ethereum}. * @param {ThirdwebContract} tokenContract - The token Thirdweb contract to airdrop * @param {transactionCallback} callback - The callback that handles the post-submit state. * @param {boolean} isLogResult - Whether to log the result or not. Default true. * @returns {Promise<TransactionReceipt>} A promise that resolves to the confirmed transaction receipt. * @throws An error if the amount <= 0. * @throws An error if the wallet is not connected. * @example * ```ts * import { approveAirdropAsSpender } from "./index"; * * const transactionReceipt = await approveAirdropAsSpender( * spender, * amount, * account, * tokenContract * ); * ``` */ const approveAirdropAsSpender = async (spender, amount, account, tokenContract, callback, isLogResult = true) => { if (amount <= 0) { throw Error('total airdrop amount must be greater than 0'); } const startTime = (0, internalUse_1.logFunctionTrackStartTime)(exports.approveAirdropAsSpender.name); const transaction = (0, erc20_1.approve)({ contract: tokenContract, spender, amount, }); // Send the transaction const result = await (0, internalUse_1.doSubmitTransaction)({ transaction, account }, callback, isLogResult); (0, internalUse_1.logFunctionDuration)(exports.approveAirdropAsSpender.name, startTime); return result; }; exports.approveAirdropAsSpender = approveAirdropAsSpender; /** * Generate merkle tree info for a whitelist * * @param {WhiteListItem[]} whitelist - The list of items is available for airdrop. * @param {ThirdwebContract} airdropContract - The Airdrop Thirdweb contract. * @param {Address} tokenAddress - The token address to claim. * @param {boolean} isLogResult - Whether to log the result or not. Default true. * @returns {Promise<GenerateMerkleTreeInfo>} A promise that resolves to the generated info. * @throws An error if the wallet is zero. * @example * ```ts * import { generateMerkleTreeForWhitelist } from "./index"; * * const generateMerkleTreeInfo = await generateMerkleTreeForWhitelist( * whitelist, * airdropContract, * tokenAddress, * tokenContract * ); * ``` */ const generateMerkleTreeInfoERC20ForWhitelist = async (whitelist, airdropContract, tokenAddress, isLogResult = true) => { const startTime = (0, internalUse_1.logFunctionTrackStartTime)(exports.generateMerkleTreeInfoERC20ForWhitelist.name); const params = { contract: airdropContract, snapshot: whitelist, tokenAddress, }; const generateMerkleTreeInfo = await (0, airdrop_2.generateMerkleTreeInfoERC20)(params); if (isLogResult) { // TODO: replace with the logger library for better format or other targets console.log(' [generateMerkleTreeForWhitelist]'); console.log(` [request] params = ${JSON.stringify(params)}`); console.log(` [response] result = ${JSON.stringify(generateMerkleTreeInfo)}`); } (0, internalUse_1.logFunctionDuration)(exports.generateMerkleTreeInfoERC20ForWhitelist.name, startTime); return generateMerkleTreeInfo; }; exports.generateMerkleTreeInfoERC20ForWhitelist = generateMerkleTreeInfoERC20ForWhitelist; /** * Airdrop's owner saved merkleRoot to on-chain. * * @remarks * MUST execute saveSnapshotByOwner first. Order master * * @param {Account} account - The Account represent as sender @see {@link https://ethereum.org/en/glossary/#account|Account's Ethereum}. * @param {ThirdwebContract} airdropContract - The airdrop Thirdweb contract. * @param {Address} tokenAddress - The token address to claim. * @param {string} merkleRoot - The generated merkleRoot from whitelist @see {@link generateMerkleTreeInfoERC20ForWhitelist|Generate merkleRoot} * @param {RetryOptions} retryOptions - The configuration on retry * @param {ExtraGasOptions} extraGasOptions - The extra gas options bidding for your transaction to be included in the next block. * @returns {Promise<TransactionReceipt>} A promise that resolves to the confirmed transaction receipt. * @throws An error if the wallet is not connected. */ const saveMerkleRootByOwner = async (account, airdropContract, tokenAddress, merkleRoot, retryOptions = {}, extraGasOptions = {}) => { const startTime = (0, internalUse_1.logFunctionTrackStartTime)(exports.saveMerkleRootByOwner.name); const merkleRootTransaction = (0, airdrop_2.setMerkleRoot)({ contract: airdropContract, token: `0x${tokenAddress.replace('0x', '')}`, tokenMerkleRoot: `0x${merkleRoot.replace('0x', '')}`, resetClaimStatus: true, }); const result = await retryPrepareAndSubmitRawTransaction(merkleRootTransaction, account, retryOptions, extraGasOptions); (0, internalUse_1.logFunctionDuration)(exports.saveMerkleRootByOwner.name, startTime); return result; }; exports.saveMerkleRootByOwner = saveMerkleRootByOwner; /** * Airdrop's owner saved snapshotUri to on-chain. * * @param {Account} account - The Account represent as sender @see {@link https://ethereum.org/en/glossary/#account|Account's Ethereum}. * @param {ThirdwebContract} airdropContract - The airdrop Thirdweb contract. * @param {string} merkleRoot - The generated merkleRoot from whitelist @see {@link generateMerkleTreeInfoERC20ForWhitelist|Generate merkleRoot} * @param {string} snapshotUri - The generated snapshotUri from whitelist @see {@link generateMerkleTreeInfoERC20ForWhitelist|Generate snapshotUri} * @param {RetryOptions} retryOptions - The configuration on retry * @param {ExtraGasOptions} extraGasOptions - The extra gas options bidding for your transaction to be included in the next block. * @returns {Promise<TransactionReceipt>} A promise that resolves to the confirmed transaction receipt. * @throws An error if the wallet is not connected. */ const saveSnapshotByOwner = async (account, airdropContract, merkleRoot, snapshotUri, retryOptions = {}, extraGasOptions = {}) => { const startTime = (0, internalUse_1.logFunctionTrackStartTime)(exports.saveSnapshotByOwner.name); const saveSnapshotTransaction = (0, airdrop_2.saveSnapshot)({ contract: airdropContract, merkleRoot, snapshotUri, }); const result = await retryPrepareAndSubmitRawTransaction(saveSnapshotTransaction, account, retryOptions, extraGasOptions); (0, internalUse_1.logFunctionDuration)(exports.saveSnapshotByOwner.name, startTime); return result; }; exports.saveSnapshotByOwner = saveSnapshotByOwner; /** * Sends a transaction using the provided wallet. * @param {SendTransactionOptions} options - The options for sending the transaction. * @param {number} maxBlocksWaitTime - The maximum of blocks to wait for confirmation before considering success. * @returns {Promise<TransactionReceipt>} A promise that resolves to the confirmed transaction receipt. * @throws An error if the wallet is not connected. * @example * ```ts * import { sendAndConfirmTransaction } from "./index"; * * const transactionReceipt = await sendAndConfirmTransaction( * options, * maxBlocksWaitTime * ); * ``` */ async function sendTransactionAndWaitForReceipt(options, maxBlocksWaitTime = ConstType_1.DEFAULT_MAX_BLOCKS_WAIT_TIME) { const submittedTx = await (0, thirdweb_1.sendTransaction)(options); return (0, thirdweb_1.waitForReceipt)({ ...submittedTx, maxBlocksWaitTime, }); } exports.sendTransactionAndWaitForReceipt = sendTransactionAndWaitForReceipt; /** * Retry on preparing the gas fee, and nonce, and send a transaction using the provided wallet. * * @param {PreparedTransaction} transaction - The raw transaction to submit * @param {Account} account - The Account represent as sender @see {@link https://ethereum.org/en/glossary/#account|Account's Ethereum}. * @param {ExtraGasOptions} extraGasOptions - The extra gas options bidding for your transaction to be included in the next block. Otherwise, Use default our sdk {@link Type | Default Variables} * @param {RetryOptions} retryOptions - The configuration on retry * @returns {Promise<TransactionReceipt>} A promise that resolves to the confirmed transaction receipt. * @throws An error if the wallet is not connected. */ /* eslint-disable @typescript-eslint/no-explicit-any */ async function retryPrepareAndSubmitRawTransaction(transaction, account, retryOptions = {}, extraGasOptions = {}) { const { extraGasPercentage = ConstType_1.DEFAULT_EXTRA_GAS_PERCENTAGE, extraMaxPriorityFeePerGasPercentage = ConstType_1.DEFAULT_EXTRA_PRIORITY_TIP_PERCENTAGE, extraOnRetryPercentage = ConstType_1.DEFAULT_EXTRA_ON_RETRY_PERCENTAGE, } = extraGasOptions; const { client, chain } = transaction; return (0, Retry_1.retry)(async (retryCount) => { // Get transaction nonce const nextNonce = await (0, exports.getNextNonce)(account.address, client, { chain, }); // Calculate gas fee of a transaction const gasFeeInfo = await (0, exports.getGasFeeInfo)({ transaction, account }, { extraGasPercentage, extraMaxPriorityFeePerGasPercentage, extraOnRetryPercentage: extraOnRetryPercentage * retryCount, }); const sendingSnapshotTransaction = { ...transaction, ...gasFeeInfo, nonce: nextNonce, }; return await sendTransactionAndWaitForReceipt({ transaction: sendingSnapshotTransaction, account: account, }); }, retryOptions); } exports.retryPrepareAndSubmitRawTransaction = retryPrepareAndSubmitRawTransaction; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29yZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90cmFuc2FjdGlvbi9jb3JlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7OztHQUlHOzs7QUFFSCwyQ0FBMkM7QUFDM0MsdURBQTBFO0FBQWpFLHVHQUFBLFlBQVksT0FBQTtBQUFFLHdHQUFBLGFBQWEsT0FBQTtBQUVwQyxtQ0FBbUM7QUFDbkMsdUNBaUJrQjtBQUNsQix5REFNcUM7QUFDckMscURBQW9EO0FBR3BELG1DQUFtQztBQUNuQywyQ0FBd0M7QUFZeEMsaURBSzJCO0FBQzNCLCtDQUt1QjtBQUV2Qjs7Ozs7R0FLRztBQUNILHdIQUF3SDtBQUNqSCxNQUFNLG1CQUFtQixHQUFHLENBQy9CLE1BQXNCLEVBQ3RCLE9BQStCLEVBQ2pDLEVBQUU7SUFDQSxNQUFNLEVBQUUsY0FBYyxFQUFFLEtBQUssRUFBRSxHQUFHLE9BQU8sQ0FBQztJQUMxQyxJQUFJLEtBQUssRUFBRSxDQUFDO1FBQ1IsT0FBTyxJQUFBLHVCQUFZLEVBQUM7WUFDaEIsTUFBTTtZQUNOLEtBQUs7U0FDUixDQUFDLENBQUM7SUFDUCxDQUFDO0lBQ0QsT0FBTyxJQUFBLHVCQUFZLEVBQUM7UUFDaEIsTUFBTTtRQUNOLEtBQUssRUFBRSxJQUFBLDhCQUFnQixFQUFDLGNBQWMsQ0FBQztLQUMxQyxDQUFDLENBQUM7QUFDUCxDQUFDLENBQUM7QUFmVyxRQUFBLG1CQUFtQix1QkFlOUI7QUFFRjs7Ozs7R0FLRztBQUVIOzs7Ozs7Ozs7R0FTRztBQUNJLE1BQU0sYUFBYSxHQUFHLEtBQUssRUFDOUIsT0FBMkIsRUFDM0Isa0JBQW1DLEVBQUUsRUFDckMsV0FBVyxHQUFHLElBQUksRUFDQyxFQUFFO0lBQ3JCLE1BQU0sRUFBRSxXQUFXLEVBQUUsT0FBTyxFQUFFLEdBQUcsT0FBTyxDQUFDO0lBQ3pDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUNYLE1BQU0sS0FBSyxDQUFDLGtEQUFrRCxDQUFD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