@myria/airdrop-js
Version:
Airdrop in L1 with claim based approach
197 lines (188 loc) • 7.56 kB
text/typescript
/**
* Collection of wrapper functions which include single functions in core to simplify consumers's integration like plug (config in) and play
* @module Transaction/Wrapper
*/
// External imports below this line
import { ThirdwebContract } from 'thirdweb';
import {
Account,
ApproveWhitelistAndAllowanceResult,
ExtraGasOptions,
RetryOptions,
SaveMerkleTreeResult,
} from '../type';
import {
ALREADY_SUBMITTED_SKIP_TRANSACTION,
GENERIC_ERROR,
} from '../type/ErrorCodeType';
import {
approveAirdropAsSpender,
/* eslint-disable @typescript-eslint/no-unused-vars */
generateMerkleTreeInfoERC20ForWhitelist,
getOwnerOfContract,
saveMerkleRootByOwner,
saveSnapshotByOwner,
} from './core';
import { logFunctionDuration, logFunctionTrackStartTime } from './internalUse';
import {
isAlreadyApprovedAllowanceOnchain,
isAlreadySubmittedOnchain,
} from './validation';
/**
* Wrapper function: Airdrop's owner approve whitelist to save on-chain include both setMerkleRoot and saveSnapshot
*
* @param {Account} account - The Account represent as sender @see {@link https://ethereum.org/en/glossary/#account|Account's Ethereum}.
* @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 {ThirdwebContract} airdropContract - The airdrop Thirdweb contract.
* @param {Address} tokenAddress - The token address to claim.
* @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<SaveMerkleTreeResult>} A promise that resolves to the confirmed transaction hashes accordingly.
*/
export async function saveMerkleTreeByOwner(
account: Account,
merkleRoot: string,
snapshotUri: string,
airdropContract: ThirdwebContract,
tokenAddress: string,
retryOptions: RetryOptions = {},
extraGasOptions: ExtraGasOptions = {},
): Promise<SaveMerkleTreeResult> {
const startTime = logFunctionTrackStartTime(saveMerkleTreeByOwner.name);
// Check to skip approve allowance to reduce our cost
const shouldSkipSubmitting = await isAlreadySubmittedOnchain(
airdropContract,
tokenAddress,
merkleRoot,
);
if (shouldSkipSubmitting) {
console.log(
`Skipping submission because it already exists onchain merkleRoot ${merkleRoot}, snapshotUri ${snapshotUri}`,
);
logFunctionDuration(saveMerkleTreeByOwner.name, startTime);
return {
snapshotResult: ALREADY_SUBMITTED_SKIP_TRANSACTION,
merkleRootResult: ALREADY_SUBMITTED_SKIP_TRANSACTION,
};
}
// Save snapshot
const { transactionHash: snapshotTransactionHash } =
await saveSnapshotByOwner(
account,
airdropContract,
merkleRoot,
snapshotUri,
retryOptions,
extraGasOptions,
);
console.log(
`saveSnapshotByOwner transactionHash: ${snapshotTransactionHash}`,
);
const snapshotResult = { transactionHash: snapshotTransactionHash };
// Set MerkleRoot
const { transactionHash: merkleRootTransactionHash } =
await saveMerkleRootByOwner(
account,
airdropContract,
tokenAddress,
merkleRoot,
retryOptions,
extraGasOptions,
);
console.log(
`saveMerkleRootByOwner transactionHash: ${merkleRootTransactionHash}`,
);
const merkleRootResult = { transactionHash: merkleRootTransactionHash };
logFunctionDuration(saveMerkleTreeByOwner.name, startTime);
// In case of resubmit with the same data. The transaction hash will be empty
return {
snapshotResult,
merkleRootResult,
};
}
/**
* Wrapper function: Owner approve the whitelist on behalf of Airdrop's contract owner and approve allowance on behalf of Token's contract owner.
*
* @param {Account} account - The Account represent as sender @see {@link https://ethereum.org/en/glossary/#account|Account's Ethereum}.
* @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 {ThirdwebContract} airdropContract - The airdrop Thirdweb contract.
* @param {ThirdwebContract} tokenContract - The token Thirdweb contract to airdrop
* @param {number} totalAmount - The total airdrop amount in ether format.
* @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<ApproveWhitelistAndAllowanceResult>} A promise that resolves to the confirmed transaction hashes accordingly.
* @throws An error if the totalAmount <= 0.
*/
export async function approveWhitelistAndAllowance(
account: Account,
merkleRoot: string,
snapshotUri: string,
airdropContract: ThirdwebContract,
tokenContract: ThirdwebContract,
totalAmount: number,
retryOptions: RetryOptions = {},
extraGasOptions: ExtraGasOptions = {},
): Promise<ApproveWhitelistAndAllowanceResult> {
if (totalAmount <= 0) {
throw Error('totalAmount must be greater than 0');
}
const ownerAddress = await getOwnerOfContract(airdropContract);
if (account.address.toLowerCase() !== ownerAddress.toLowerCase()) {
throw Error('account must belong to airdropContract owner');
}
const startTime = logFunctionTrackStartTime(
approveWhitelistAndAllowance.name,
);
// Save MerkleTree on-chain
const saveMerkleTreeResult = await saveMerkleTreeByOwner(
account,
merkleRoot,
snapshotUri,
airdropContract,
tokenContract.address,
retryOptions,
extraGasOptions,
);
// Approve Allowance
// Check to skip approve allowance to reduce our cost
const shouldSkipApproveSpender = await isAlreadyApprovedAllowanceOnchain(
account.address,
airdropContract.address,
tokenContract,
totalAmount,
);
console.log(
`shouldSkipApproveSpender ${shouldSkipApproveSpender} and total amount: ${totalAmount}`,
);
if (shouldSkipApproveSpender) {
logFunctionDuration(approveWhitelistAndAllowance.name, startTime);
return {
...saveMerkleTreeResult,
approveAllowanceResult: ALREADY_SUBMITTED_SKIP_TRANSACTION,
};
}
let approveAllowanceResult;
try {
const { transactionHash } = await approveAirdropAsSpender(
airdropContract.address,
totalAmount,
account,
tokenContract,
);
approveAllowanceResult = {
transactionHash,
};
} catch (error) {
approveAllowanceResult = {
...GENERIC_ERROR,
errorMessage: error.message,
};
}
logFunctionDuration(approveWhitelistAndAllowance.name, startTime);
return {
...saveMerkleTreeResult,
approveAllowanceResult: approveAllowanceResult,
};
}