@myria/airdrop-js
Version:
Airdrop in L1 with claim based approach
392 lines • 28.7 kB
JavaScript
;
/**
* 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29yZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90cmFuc2FjdGlvbi9jb3JlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7OztHQUlHOzs7QUFFSCwyQ0FBMkM7QUFDM0MsdURBQTBFO0FBQWpFLHVHQUFBLFlBQVksT0FBQTtBQUFFLHdHQUFBLGFBQWEsT0FBQTtBQUVwQyxtQ0FBbUM7QUFDbkMsdUNBaUJrQjtBQUNsQix5REFNcUM7QUFDckMscURBQW9EO0FBR3BELG1DQUFtQztBQUNuQywyQ0FBd0M7QUFZeEMsaURBSzJCO0FBQzNCLCtDQUt1QjtBQUV2Qjs7Ozs7R0FLRztBQUNILHdIQUF3SDtBQUNqSCxNQUFNLG1CQUFtQixHQUFHLENBQy9CLE1BQXNCLEVBQ3RCLE9BQStCLEVBQ2pDLEVBQUU7SUFDQSxNQUFNLEVBQUUsY0FBYyxFQUFFLEtBQUssRUFBRSxHQUFHLE9BQU8sQ0FBQztJQUMxQyxJQUFJLEtBQUssRUFBRSxDQUFDO1FBQ1IsT0FBTyxJQUFBLHVCQUFZLEVBQUM7WUFDaEIsTUFBTTtZQUNOLEtBQUs7U0FDUixDQUFDLENBQUM7SUFDUCxDQUFDO0lBQ0QsT0FBTyxJQUFBLHVCQUFZLEVBQUM7UUFDaEIsTUFBTTtRQUNOLEtBQUssRUFBRSxJQUFBLDhCQUFnQixFQUFDLGNBQWMsQ0FBQztLQUMxQyxDQUFDLENBQUM7QUFDUCxDQUFDLENBQUM7QUFmVyxRQUFBLG1CQUFtQix1QkFlOUI7QUFFRjs7Ozs7R0FLRztBQUVIOzs7Ozs7Ozs7R0FTRztBQUNJLE1BQU0sYUFBYSxHQUFHLEtBQUssRUFDOUIsT0FBMkIsRUFDM0Isa0JBQW1DLEVBQUUsRUFDckMsV0FBVyxHQUFHLElBQUksRUFDQyxFQUFFO0lBQ3JCLE1BQU0sRUFBRSxXQUFXLEVBQUUsT0FBTyxFQUFFLEdBQUcsT0FBTyxDQUFDO0lBQ3pDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUNYLE1BQU0sS0FBSyxDQUFDLGtEQUFrRCxDQUFDLENBQUM7SUFDcEUsQ0FBQztJQUNELE1BQU0sRUFDRixrQkFBa0IsR0FBRyx3Q0FBNEIsRUFDakQsbUNBQW1DLEdBQUcsaURBQXFDLEVBQzNFLHNCQUFzQixHQUFHLDZDQUFpQyxHQUM3RCxHQUFHLGVBQWUsQ0FBQztJQUNwQixNQUFNLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxHQUFHLFdBQVcsQ0FBQztJQUN0QyxNQUFNLFNBQVMsR0FBRyxJQUFBLDJCQUFtQixFQUFDLE1BQU0sRUFBRSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFDekQsdUNBQXVDO0lBQ3ZDLGVBQWU7SUFDZixNQUFNLFFBQVEsR0FBRyxNQUFNLElBQUEsc0JBQVcsRUFBQztRQUMvQixXQUFXO1FBQ1gsT0FBTztLQUNWLENBQUMsQ0FBQztJQUNILGVBQWU7SUFDZixNQUFNLFFBQVEsR0FDVixDQUFDLFFBQVEsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDeEIsTUFBTSxDQUFDLGtCQUFrQixHQUFHLHNCQUFzQixDQUFDLENBQUM7SUFDeEQsNEJBQTRCO0lBQzVCLGNBQWM7SUFDZCxNQUFNLFFBQVEsR0FBRyxNQUFNLElBQUEsdUJBQVksRUFBQyxTQUFTLENBQUMsQ0FBQztJQUMvQyx3QkFBd0I7SUFDeEIsTUFBTSxvQkFBb0IsR0FBRyxNQUFNLElBQUEsbUNBQXdCLEVBQUMsU0FBUyxDQUFDLENBQUM7SUFDdkUsTUFBTSx5QkFBeUIsR0FDM0IsQ0FBQyxvQkFBb0IsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDcEMsTUFBTSxDQUFDLG1DQUFtQyxHQUFHLHNCQUFzQixDQUFDLENBQUM7SUFDekUsTUFBTSxZQUFZLEdBQ2QsUUFBUSxHQUFHLG9CQUFvQixHQUFHLHlCQUF5QixDQUFDO0lBQ2hFLHFEQUFxRDtJQUNyRCxNQUFNLE9BQU8sR0FBRyxNQUFNLElBQUEsMEJBQWUsRUFBQyxFQUFFLFdBQVcsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDO0lBQ2hFLE9BQU8sQ0FBQyxHQUFHLENBQ1Asc0NBQXNDLE9BQU8sQ0FBQyxLQUFLLG9CQUFvQixPQUFPLENBQUMsR0FBRyxFQUFFLENBQ3ZGLENBQUM7SUFFRixNQUFNLFVBQVUsR0FBRztRQUNmLEdBQUcsRUFBRSxRQUFRO1FBQ2IsUUFBUTtRQUNSLG9CQUFvQjtRQUNwQixZQUFZO1FBQ1osUUFBUTtLQUNYLENBQUM7SUFDRixJQUFJLFdBQVcsRUFBRSxDQUFDO1FBQ2QsT0FBTyxDQUFDLEdBQUcsQ0FDUCxvQ0FBb0MsSUFBSSxDQUFDLFNBQVMsQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUNuRSxDQUFDO0lBQ04sQ0FBQztJQUNELE9BQU8sVUFBVSxDQUFDO0FBQ3RCLENBQUMsQ0FBQztBQXZEVyxRQUFBLGFBQWEsaUJBdUR4QjtBQUVGOzs7Ozs7Ozs7OztHQVdHO0FBQ0ksTUFBTSxZQUFZLEdBQUcsS0FBSyxFQUM3QixPQUFnQixFQUNoQixNQUFzQixFQUN0QixPQUErQixFQUMvQixXQUFXLEdBQUcsSUFBSSxFQUNILEVBQUU7SUFDakIsTUFBTSxTQUFTLEdBQUcsSUFBQSx1Q0FBeUIsRUFBQyxvQkFBWSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQy9ELE1BQU0sU0FBUyxHQUFHLElBQUEsMkJBQW1CLEVBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3ZELE1BQU0sZ0JBQWdCLEdBQUcsTUFBTSxJQUFBLGtDQUF1QixFQUFDLFNBQVMsRUFBRTtRQUM5RCxPQUFPO0tBQ1YsQ0FBQyxDQUFDO0lBQ0gsSUFBSSxXQUFXLEVBQUUsQ0FBQztRQUNkLDJFQUEyRTtRQUMzRSxPQUFPLENBQUMsR0FBRyxDQUNQLG1EQUFtRCxnQkFBZ0IsRUFBRSxDQUN4RSxDQUFDO0lBQ04sQ0FBQztJQUNELElBQUEsaUNBQW1CLEVBQUMsb0JBQVksQ0FBQyxJQUFJLEVBQUUsU0FBUyxDQUFDLENBQUM7SUFDbEQsT0FBTyxnQkFBZ0IsQ0FBQztBQUM1QixDQUFDLENBQUM7QUFuQlcsUUFBQSxZQUFZLGdCQW1CdkI7QUFFRjs7Ozs7R0FLRztBQUNJLE1BQU0sa0JBQWtCLEdBQUcsS0FBSyxFQUNuQyxRQUEwQixFQUNYLEVBQUU7SUFDakIsT0FBTyxNQUFNLElBQUEsdUJBQVksRUFBQztRQUN0QixRQUFRO1FBQ1IsNkRBQTZEO1FBQzdELE1BQU0sRUFBRTtZQUNKLElBQUksRUFBRSxVQUFVO1lBQ2hCLElBQUksRUFBRSxPQUFPO1lBQ2IsTUFBTSxFQUFFLEVBQUU7WUFDVixPQUFPLEVBQUU7Z0JBQ0w7b0JBQ0ksSUFBSSxFQUFFLFNBQVM7b0JBQ2YsSUFBSSxFQUFFLEVBQUU7b0JBQ1IsWUFBWSxFQUFFLFNBQVM7aUJBQzFCO2FBQ0o7WUFDRCxlQUFlLEVBQUUsTUFBTTtTQUMxQjtRQUNELE1BQU0sRUFBRSxFQUFFO0tBQ2IsQ0FBQyxDQUFDO0FBQ1AsQ0FBQyxDQUFDO0FBckJXLFFBQUEsa0JBQWtCLHNCQXFCN0I7QUFFRjs7Ozs7OztHQU9HO0FBQ0gsb0VBQW9FO0FBQzdELE1BQU0sbUJBQW1CLEdBQUcsQ0FDL0IsT0FBZ0IsRUFDaEIsTUFBc0IsRUFDdEIsY0FBK0IsRUFDakMsRUFBRTtJQUNBLE9BQU8sSUFBQSxzQkFBVyxFQUFDO1FBQ2YsTUFBTTtRQUNOLEtBQUssRUFBRSxJQUFBLDhCQUFnQixFQUFDLGNBQWMsQ0FBQztRQUN2QyxPQUFPO0tBQ1YsQ0FBQyxDQUFDO0FBQ1AsQ0FBQyxDQUFDO0FBVlcsUUFBQSxtQkFBbUIsdUJBVTlCO0FBRUY7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBb0JHO0FBQ0ksTUFBTSxpQkFBaUIsR0FBRyxLQUFLLEVBQ2xDLFlBQXFCLEVBQ3JCLE9BQWdCLEVBQ2hCLGVBQWlDLEVBQ2pDLFFBQStDLEVBQy9DLFdBQVcsR0FBRyxJQUFJLEVBQ1MsRUFBRTtJQUM3QixNQUFNLGdCQUFnQixHQUFHLElBQUEsb0JBQVUsRUFBQztRQUNoQyxRQUFRLEVBQUUsZUFBZTtRQUN6QixZQUFZO1FBQ1osU0FBUyxFQUFFLE9BQU8sQ0FBQyxPQUFPO0tBQzdCLENBQUMsQ0FBQztJQUNILHVCQUF1QjtJQUN2QixPQUFPLElBQUEsaUNBQW1CLEVBQ3RCLEVBQUUsV0FBVyxFQUFFLGdCQUFnQixFQUFFLE9BQU8sRUFBRSxFQUMxQyxRQUFRLEVBQ1IsV0FBVyxDQUNkLENBQUM7QUFDTixDQUFDLENBQUM7QUFsQlcsUUFBQSxpQkFBaUIscUJBa0I1QjtBQUVGOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXVCRztBQUNJLE1BQU0sdUJBQXVCLEdBQUcsS0FBSyxFQUN4QyxPQUFnQixFQUNoQixNQUFjLEVBQ2QsT0FBZ0IsRUFDaEIsYUFBK0IsRUFDL0IsUUFBK0MsRUFDL0MsV0FBVyxHQUFHLElBQUksRUFDUyxFQUFFO0lBQzdCLElBQUksTUFBTSxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQ2QsTUFBTSxLQUFLLENBQUMsNkNBQTZDLENBQUMsQ0FBQztJQUMvRCxDQUFDO0lBQ0QsTUFBTSxTQUFTLEdBQUcsSUFBQSx1Q0FBeUIsRUFBQywrQkFBdUIsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUMxRSxNQUFNLFdBQVcsR0FBRyxJQUFBLGVBQU8sRUFBQztRQUN4QixRQUFRLEVBQUUsYUFBYTtRQUN2QixPQUFPO1FBQ1AsTUFBTTtLQUNULENBQUMsQ0FBQztJQUNILHVCQUF1QjtJQUN2QixNQUFNLE1BQU0sR0FBRyxNQUFNLElBQUEsaUNBQW1CLEVBQ3BDLEVBQUUsV0FBVyxFQUFFLE9BQU8sRUFBRSxFQUN4QixRQUFRLEVBQ1IsV0FBVyxDQUNkLENBQUM7SUFDRixJQUFBLGlDQUFtQixFQUFDLCtCQUF1QixDQUFDLElBQUksRUFBRSxTQUFTLENBQUMsQ0FBQztJQUM3RCxPQUFPLE1BQU0sQ0FBQztBQUNsQixDQUFDLENBQUM7QUF6QlcsUUFBQSx1QkFBdUIsMkJBeUJsQztBQUVGOzs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQW9CRztBQUNJLE1BQU0sdUNBQXVDLEdBQUcsS0FBSyxFQUN4RCxTQUEwQixFQUMxQixlQUFpQyxFQUNqQyxZQUFxQixFQUNyQixXQUFXLEdBQUcsSUFBSSxFQUNhLEVBQUU7SUFDakMsTUFBTSxTQUFTLEdBQUcsSUFBQSx1Q0FBeUIsRUFDdkMsK0NBQXVDLENBQUMsSUFBSSxDQUMvQyxDQUFDO0lBQ0YsTUFBTSxNQUFNLEdBQThEO1FBQ3RFLFFBQVEsRUFBRSxlQUFlO1FBQ3pCLFFBQVEsRUFBRSxTQUFTO1FBQ25CLFlBQVk7S0FDZixDQUFDO0lBQ0YsTUFBTSxzQkFBc0IsR0FBRyxNQUFNLElBQUEscUNBQTJCLEVBQUMsTUFBTSxDQUFDLENBQUM7SUFDekUsSUFBSSxXQUFXLEVBQUUsQ0FBQztRQUNkLDJFQUEyRTtRQUMzRSxPQUFPLENBQUMsR0FBRyxDQUFDLHFDQUFxQyxDQUFDLENBQUM7UUFDbkQsT0FBTyxDQUFDLEdBQUcsQ0FBQyw2QkFBNkIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDbkUsT0FBTyxDQUFDLEdBQUcsQ0FDUCw4QkFBOEIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxzQkFBc0IsQ0FBQyxFQUFFLENBQ3pFLENBQUM7SUFDTixDQUFDO0lBQ0QsSUFBQSxpQ0FBbUIsRUFDZiwrQ0FBdUMsQ0FBQyxJQUFJLEVBQzVDLFNBQVMsQ0FDWixDQUFDO0lBQ0YsT0FBTyxzQkFBc0IsQ0FBQztBQUNsQyxDQUFDLENBQUM7QUE1QlcsUUFBQSx1Q0FBdUMsMkNBNEJsRDtBQUVGOzs7Ozs7Ozs7Ozs7OztHQWNHO0FBQ0ksTUFBTSxxQkFBcUIsR0FBRyxLQUFLLEVBQ3RDLE9BQWdCLEVBQ2hCLGVBQWlDLEVBQ2pDLFlBQW9CLEVBQ3BCLFVBQWtCLEVBQ2xCLGVBQTZCLEVBQUUsRUFDL0Isa0JBQW1DLEVBQUUsRUFDVixFQUFFO0lBQzdCLE1BQU0sU0FBUyxHQUFHLElBQUEsdUNBQXlCLEVBQUMsNkJBQXFCLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDeEUsTUFBTSxxQkFBcUIsR0FBRyxJQUFBLHVCQUFhLEVBQUM7UUFDeEMsUUFBUSxFQUFFLGVBQWU7UUFDekIsS0FBSyxFQUFFLEtBQUssWUFBWSxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLEVBQUU7UUFDNUMsZUFBZSxFQUFFLEtBQUssVUFBVSxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLEVBQUU7UUFDcEQsZ0JBQWdCLEVBQUUsSUFBSTtLQUN6QixDQUFDLENBQUM7SUFDSCxNQUFNLE1BQU0sR0FBRyxNQUFNLG1DQUFtQyxDQUNwRCxxQkFBcUIsRUFDckIsT0FBTyxFQUNQLFlBQVksRUFDWixlQUFlLENBQ2xCLENBQUM7SUFDRixJQUFBLGlDQUFtQixFQUFDLDZCQUFxQixDQUFDLElBQUksRUFBRSxTQUFTLENBQUMsQ0FBQztJQUMzRCxPQUFPLE1BQU0sQ0FBQztBQUNsQixDQUFDLENBQUM7QUF2QlcsUUFBQSxxQkFBcUIseUJBdUJoQztBQUVGOzs7Ozs7Ozs7OztHQVdHO0FBQ0ksTUFBTSxtQkFBbUIsR0FBRyxLQUFLLEVBQ3BDLE9BQWdCLEVBQ2hCLGVBQWlDLEVBQ2pDLFVBQWtCLEVBQ2xCLFdBQW1CLEVBQ25CLGVBQTZCLEVBQUUsRUFDL0Isa0JBQW1DLEVBQUUsRUFDVixFQUFFO0lBQzdCLE1BQU0sU0FBUyxHQUFHLElBQUEsdUNBQXlCLEVBQUMsMkJBQW1CLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDdEUsTUFBTSx1QkFBdUIsR0FBRyxJQUFBLHNCQUFZLEVBQUM7UUFDekMsUUFBUSxFQUFFLGVBQWU7UUFDekIsVUFBVTtRQUNWLFdBQVc7S0FDZCxDQUFDLENBQUM7SUFDSCxNQUFNLE1BQU0sR0FBRyxNQUFNLG1DQUFtQyxDQUNwRCx1QkFBdUIsRUFDdkIsT0FBTyxFQUNQLFlBQVksRUFDWixlQUFlLENBQ2xCLENBQUM7SUFDRixJQUFBLGlDQUFtQixFQUFDLDJCQUFtQixDQUFDLElBQUksRUFBRSxTQUFTLENBQUMsQ0FBQztJQUN6RCxPQUFPLE1BQU0sQ0FBQztBQUNsQixDQUFDLENBQUM7QUF0QlcsUUFBQSxtQkFBbUIsdUJBc0I5QjtBQUVGOzs7Ozs7Ozs7Ozs7Ozs7R0FlRztBQUNJLEtBQUssVUFBVSxnQ0FBZ0MsQ0FDbEQsT0FBK0IsRUFDL0IsaUJBQWlCLEdBQUcsd0NBQTRCO0lBRWhELE1BQU0sV0FBVyxHQUFHLE1BQU0sSUFBQSwwQkFBZSxFQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ25ELE9BQU8sSUFBQSx5QkFBYyxFQUFDO1FBQ2xCLEdBQUcsV0FBVztRQUNkLGlCQUFpQjtLQUNwQixDQUFDLENBQUM7QUFDUCxDQUFDO0FBVEQsNEVBU0M7QUFFRDs7Ozs7Ozs7O0dBU0c7QUFDSCx1REFBdUQ7QUFDaEQsS0FBSyxVQUFVLG1DQUFtQyxDQUNyRCxXQUFxQyxFQUNyQyxPQUFnQixFQUNoQixlQUE2QixFQUFFLEVBQy9CLGtCQUFtQyxFQUFFO0lBRXJDLE1BQU0sRUFDRixrQkFBa0IsR0FBRyx3Q0FBNEIsRUFDakQsbUNBQW1DLEdBQUcsaURBQXFDLEVBQzNFLHNCQUFzQixHQUFHLDZDQUFpQyxHQUM3RCxHQUFHLGVBQWUsQ0FBQztJQUNwQixNQUFNLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxHQUFHLFdBQVcsQ0FBQztJQUN0QyxPQUFPLElBQUEsYUFBSyxFQUFxQixLQUFLLEVBQUUsVUFBVSxFQUFFLEVBQUU7UUFDbEQsd0JBQXdCO1FBQ3hCLE1BQU0sU0FBUyxHQUFHLE1BQU0sSUFBQSxvQkFBWSxFQUFDLE9BQU8sQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFO1lBQzFELEtBQUs7U0FDUixDQUFDLENBQUM7UUFDSCxxQ0FBcUM7UUFDckMsTUFBTSxVQUFVLEdBQUcsTUFBTSxJQUFBLHFCQUFhLEVBQ2xDLEVBQUUsV0FBVyxFQUFFLE9BQU8sRUFBRSxFQUN4QjtZQUNJLGtCQUFrQjtZQUNsQixtQ0FBbUM7WUFDbkMsc0JBQXNCLEVBQUUsc0JBQXNCLEdBQUcsVUFBVTtTQUM5RCxDQUNKLENBQUM7UUFDRixNQUFNLDBCQUEwQixHQUFHO1lBQy9CLEdBQUcsV0FBVztZQUNkLEdBQUcsVUFBVTtZQUNiLEtBQUssRUFBRSxTQUFTO1NBQ25CLENBQUM7UUFDRixPQUFPLE1BQU0sZ0NBQWdDLENBQUM7WUFDMUMsV0FBVyxFQUFFLDBCQUEwQjtZQUN2QyxPQUFPLEVBQUUsT0FBTztTQUNuQixDQUFDLENBQUM7SUFDUCxDQUFDLEVBQUUsWUFBWSxDQUFDLENBQUM7QUFDckIsQ0FBQztBQXBDRCxrRkFvQ0MifQ==