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
JavaScript
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