UNPKG

minauth-erc721-timelock-plugin

Version:

This package contains an implementation of a MinAuth plugin that combines zero-knowledge Merkle "membership" proofs with the ability to register into to a Merkle tree with locking an Ethereum NFT.

117 lines 5.41 kB
import { ethers } from 'ethers'; import { ERC721TimeLock__factory } from './typechain/factories/contracts/ERC721TimeLock__factory.js'; import { ERC721Mock__factory } from './typechain/factories/contracts/ERC721Mock__factory.js'; import { MerkleTree } from './merkle-tree.js'; import { commitmentHexToField } from './commitment-types.js'; export class Erc721TimeLock { constructor(signer, lockContractAddress, erc721ContractAddress, ethereumProvider, logger) { this.signer = signer; this.lockContractAddress = lockContractAddress; this.erc721ContractAddress = erc721ContractAddress; this.ethereumProvider = ethereumProvider; this.logger = logger; /** * Fetch the commitments from the events and build a merkle tree. */ this.buildCommitmentTree = async () => { const { commitments } = await this.fetchEligibleCommitments(); const merkleTree = new MerkleTree(commitments.map((x) => commitmentHexToField(x).commitment)); return merkleTree; }; this.contract = ERC721TimeLock__factory.connect(lockContractAddress, this.signer); this.nftContract = ERC721Mock__factory.connect(erc721ContractAddress, this.signer); this.logger?.debug('Erc721TimeLock contract:', this.contract); } static async initialize(addresses, ethereumProvider, logger) { const signer = await ethereumProvider.getSigner(); logger?.debug('Signer:', signer); return new Erc721TimeLock(signer, addresses.lockContractAddress, addresses.nftContractAddress, ethereumProvider, logger); } /** * Fetch the TokenLocked and TokenUnlocked events to get the hashes of currently locked tokens. */ async fetchEligibleCommitments() { // Fetch TokenLocked events const lockedFilter = this.contract.filters.TokenLocked(); const lockedEvents = await this.contract.queryFilter(lockedFilter); // Fetch TokenUnlocked events const unlockedFilter = this.contract.filters.TokenUnlocked(); const unlockedEvents = await this.contract.queryFilter(unlockedFilter); // Process events to determine currently locked tokens const currentlyLockedTokens = new Map(); // Map of tokenId to hash for (const event of lockedEvents) { currentlyLockedTokens.set(event.args.tokenId.toString(), event.args.hash); } for (const event of unlockedEvents) { currentlyLockedTokens.delete(event.args.tokenId.toString()); } const commitments = Array.from(currentlyLockedTokens.values()).map((hash) => ({ commitmentHex: hash })); return { commitments }; } /** * Locks a token by first approving its transfer and then interacting with the lock contract. * @param tokenId The ID of the token to be locked. * @param hash The hash representing the token's commitment. */ async lockToken(tokenId, { commitmentHex }) { // Approve the ERC721TimeLock contract to transfer the token try { const approvalTx = await this.nftContract.approve(this.lockContractAddress, tokenId); await approvalTx.wait(); } catch (e) { this.logger?.error('Error approving nft transfer:', e); throw e; } const commitmentBytes = hexToUInt8Array(commitmentHex.startsWith('0x') ? commitmentHex : '0x' + commitmentHex); // Lock the token try { this.logger?.info(`Locking token ${tokenId} with commitment ${commitmentHex} started.`); this.logger?.debug(`Commitment bytes: ${commitmentBytes}`); const lockTx = await this.contract.lockToken(this.erc721ContractAddress, tokenId, commitmentBytes); await lockTx.wait(); } catch (e) { this.logger?.error('Error locking token in timelock contract:', e); throw e; } } /** * Unlocks a token by interacting with the lock contract. * @param index The index of the token in the locked tokens array. */ async unlockToken(index) { // Approve the ERC721TimeLock contract to transfer the token const unlockTx = await this.contract.unlockToken(index); await unlockTx.wait(); } } export function hexlify(bytes) { return ethers.hexlify(bytes); } export function hexToUInt8Array(hexString, size) { // Validate the input if (!/^0x[a-fA-F0-9]+$/.test(hexString)) { throw new Error('Invalid hexadecimal string'); } // Remove the '0x' prefix hexString = hexString.slice(2); const sz = size || hexString.length / 2; // pad with zeros const padding = sz * 2 - hexString.length; hexString = '0'.repeat(padding) + hexString; // Calculate the number of bytes in the hex string const numBytes = hexString.length / 2; // Check if the hex string represents more than 32 bytes if (numBytes > sz) { throw new Error('Hexadecimal string represents more than 32 bytes'); } // Create a buffer of 32 bytes, filled with zeros const bytes = new Uint8Array(sz); // Convert hex string to bytes in a big-endian format for (let i = 0, j = 0; i < hexString.length; i += 2, j++) { bytes[j] = parseInt(hexString.substring(i, i + 2), 16); } return bytes; } //# sourceMappingURL=erc721timelock.js.map