arnacon-sdk
Version:
A comprehensive SDK for deploying and managing Arnacon smart contracts across multiple networks
1,100 lines (939 loc) • 51.3 kB
JavaScript
const { ethers } = require("ethers");
const keccak256 = require("keccak256");
const namehash = require("../utils/namehash");
class ServiceProviderManager {
constructor(deploymentManager) {
this.deploymentManager = deploymentManager;
this.signer = deploymentManager.signer;
this.factoryLoader = deploymentManager.factoryLoader;
}
/**
* Register as a service provider
*/
async registerAsServiceProvider(name = undefined) {
console.log(`Registering as service provider with name: ${name}`);
const emptyAddress = "0x0000000000000000000000000000000000000000";
// Deploy required contracts
const globalRegistrarController = await this.deploymentManager.deployIfNeeded("GlobalRegistrarController");
const signatureVerifier = await this.deploymentManager.deployIfNeeded("SignatureVerifier");
// Deploy second level interactor
const secondLevelInteractor = await this.deploySecondLevelInteractor(globalRegistrarController.address, signatureVerifier.address);
// Deploy Arnacon resolver
const arnaconResolver = await this.deployArnaconResolver(secondLevelInteractor.address);
const secondLevelInteractorResolver = await secondLevelInteractor.arnaconResolver();
console.log("secondLevelInteractor resolver: ", secondLevelInteractorResolver);
// Set arnacon resolver address in interactor
if(secondLevelInteractorResolver !== arnaconResolver.address){
await this.deploymentManager.executeTransaction(
secondLevelInteractor,
"setArnaconResolver",
[arnaconResolver.address],
"Setting arnacon resolver in interactor"
);
} else {
console.log("Arnacon resolver already set correctly");
}
// Deploy and configure second level controller
const secondLevelControllerAddress = (await this.deploySecondLevelController(globalRegistrarController, secondLevelInteractor)).address
const secondLevelControllerABI = this.factoryLoader.getContractABI("SecondLevelController");
const secondLevelController = new ethers.Contract(secondLevelControllerAddress, secondLevelControllerABI, this.signer);
const interactorInController = await secondLevelController.getSecondLevelInteractor();
console.log("interactor in controller: ", interactorInController);
if (interactorInController !== secondLevelInteractor.address){
await this.deploymentManager.executeTransaction(
secondLevelInteractor,
"setSecondLevelController",
[secondLevelController.address],
"Setting second level controller in interactor"
);
const secondLevelControllerABI2 = this.factoryLoader.getContractABI("SecondLevelController");
const secondLevelControllerContract = new ethers.Contract(secondLevelController.address, secondLevelControllerABI2, this.signer);
await this.deploymentManager.executeTransaction(
secondLevelControllerContract,
"setSecondLevelInteractor",
[secondLevelInteractor.address],
"Setting second level interactor in controller"
);
const interactor = await secondLevelControllerContract.getSecondLevelInteractor();
console.log("Second level interactor set in controller ", interactor);
}
if(name && (await arnaconResolver.getProductTypeRegistry(name) === emptyAddress)){
console.log(`Deploying ProductTypeRegistry for name: ${name}`);
await this.deploymentManager.executeTransaction(
arnaconResolver,
"deployProductTypeRegistry",
[name],
"Deploying ProductTypeRegistry"
);
// Deploy ProductsNFT
console.log("Deploying ProductsNFT...");
const productsNFT = await this.deploymentManager.deployIfNeeded("ProductsNFT", arnaconResolver.address);
console.log("ProductsNFT deployed at:", productsNFT.address);
await this.deploymentManager.executeTransaction(
arnaconResolver,
"addNFTContract",
[name, productsNFT.address],
"Setting ProductsNFT in ArnaconResolver"
);
} else {
console.log("no name given so not deploying ProductTypeRegistry and ProductsNFT");
}
console.log("Service provider registration completed successfully!");
return {
globalRegistrarController: globalRegistrarController.address,
signatureVerifier: signatureVerifier.address,
secondLevelInteractor: secondLevelInteractor.address,
arnaconResolver: arnaconResolver.address,
secondLevelController: secondLevelController.address
};
}
/**
* Deploy second level interactor
*/
async deploySecondLevelInteractor(globalRegistrarControllerAddress, signatureVerifierAddress) {
const nameWrapper = await this.deploymentManager.deployIfNeeded("NameWrapper");
const provisionRegistry = await this.deploymentManager.deployIfNeeded("ProvisionRegistry", 1);
const emptyAddress = "0x0000000000000000000000000000000000000000";
const publicResolver = await this.deploymentManager.deployIfNeeded("PublicResolver");
const secondLevelInteractor = await this.deploymentManager.deployIfNeeded("SecondLevelInteractor",
globalRegistrarControllerAddress,
nameWrapper.address,
emptyAddress,
publicResolver.address,
signatureVerifierAddress,
provisionRegistry.address
);
console.log("Second level interactor deployed at:", secondLevelInteractor.address);
return secondLevelInteractor;
}
/**
* Deploy Arnacon resolver
*/
async deployArnaconResolver(secondLevelInteractorAddress) {
const ArnaconResolver = await this.deploymentManager.deployIfNeeded("ArnaconResolver", secondLevelInteractorAddress);
console.log("Arnacon resolver deployed at:", ArnaconResolver.address);
return ArnaconResolver;
}
/**
* Deploy second level controller
*/
async deploySecondLevelController(GlobalRegistrarControllerContract, SecondLevelInteractorContract) {
// const tx2 = await GlobalRegistrarControllerContract.becomeOwner();
// await tx2.wait();
// console.log("tx: ", tx2)
const node = await GlobalRegistrarControllerContract.getNodeFor(this.signer.address);
console.log("node: ", node)
const deployedControllerAddress = await GlobalRegistrarControllerContract.get2LDControllerFor(this.signer.address);
if(deployedControllerAddress !== "0x0000000000000000000000000000000000000000"){
console.log("Second level controller already deployed at:", deployedControllerAddress);
this.deploymentManager.contracts["SecondLevelController"] = deployedControllerAddress;
this.deploymentManager.saveContracts();
return {new: false, address: deployedControllerAddress};
}
const price = await GlobalRegistrarControllerContract.PRICE();
console.log("price: ", price)
// Try to get gas settings, but use a higher fallback for contract creation
let gasSettings;
try {
// throw new Error("test");
gasSettings = await this.deploymentManager.getGasSettings(GlobalRegistrarControllerContract, "deploy2LDController", [SecondLevelInteractorContract.address], price);
console.log("Gas estimation successful:", gasSettings.gasLimit.toString());
} catch (error) {
console.warn("Gas estimation failed, using fallback gas limit for contract creation:", error.message);
gasSettings = {
gasLimit: ethers.BigNumber.from("2000000"), // 2M gas for contract creation
maxPriorityFeePerGas: ethers.BigNumber.from("50000000000"), // 50 gwei
maxFeePerGas: ethers.BigNumber.from("100000000000") // 100 gwei
};
}
// Ensure minimum gas limit for contract creation
if (gasSettings.gasLimit.lt("1500000")) {
console.log("Gas limit too low for contract creation, using minimum 1.5M gas");
gasSettings.gasLimit = ethers.BigNumber.from("1500000");
}
const tx = await GlobalRegistrarControllerContract.deploy2LDController(SecondLevelInteractorContract.address, { ...gasSettings, value: price });
await tx.wait();
console.log("Second level controller deployed:", tx.hash);
const secondLevelControllerAddress = await GlobalRegistrarControllerContract.get2LDControllerFor(this.signer.address);
console.log("Second level controller address:", secondLevelControllerAddress);
this.deploymentManager.contracts["SecondLevelController"] = secondLevelControllerAddress;
this.deploymentManager.saveContracts();
return {new: true, address: secondLevelControllerAddress};
}
/**
* Purchase a top-level domain name
*/
async purchaseName(name, durationInDays = 365) {
console.log(`Purchasing domain name: ${name}.global for ${durationInDays} days...`);
try {
// Convert days to seconds for duration
const duration = durationInDays * 24 * 60 * 60;
// Get required contracts
const globalRegistrarController = await this.deploymentManager.deployIfNeeded("GlobalRegistrarController");
const ethRegistrarController = await this.deploymentManager.deployIfNeeded("ETHRegistrarController");
const publicResolver = await this.deploymentManager.deployIfNeeded("PublicResolver");
// Get second level controller for the current signer
const secondLevelController = await globalRegistrarController.get2LDControllerFor(this.signer.address);
console.log("Second level controller address:", secondLevelController);
if (secondLevelController === "0x0000000000000000000000000000000000000000") {
throw new Error("Second level controller not found. Please register as a service provider first.");
}
// Generate a secret for the commitment
const secret = "0x" + keccak256("secret").toString('hex');
// Prepare commitment parameters
const resolver = publicResolver.address;
const data = [];
const reverseRecord = false;
const ownerControlledFuses = 0;
// Get price for the domain
const price = await ethRegistrarController.rentPrice(name, duration);
const totalPrice = price.base.add(price.premium);
console.log(`Domain price: ${ethers.utils.formatEther(totalPrice)} ETH`);
// Create commitment hash - use second level controller as owner
const commitment = await ethRegistrarController.makeCommitment(
name,
secondLevelController, // Use second level controller as owner
duration,
secret,
resolver,
data,
reverseRecord,
ownerControlledFuses
);
const exists = (await ethRegistrarController.commitments(commitment)).toNumber() !== 0;
if(!exists){
// Send commitment transaction
console.log("Sending commitment transaction...");
const gasSettings = await this.deploymentManager.getGasSettings(ethRegistrarController, "commit", [commitment]);
const commitTx = await ethRegistrarController.commit(commitment, gasSettings);
console.log("Commitment transaction sent:", commitTx.hash);
await commitTx.wait();
console.log("Commitment confirmed!");
// Wait for the minimum required time
const minCommitmentAge = await ethRegistrarController.minCommitmentAge();
const waitTimeMs = Math.max(minCommitmentAge.toNumber() * 1000, 10000);
console.log(`Waiting ${waitTimeMs/1000} seconds before registration...`);
await new Promise(resolve => setTimeout(resolve, waitTimeMs));
}
// Register the domain - use second level controller as owner
console.log("Registering domain...");
// Register the domain with the global registrar controller
console.log("Registering with global registrar controller...");
const globalRegisterGasSettings = await this.deploymentManager.getGasSettings(
globalRegistrarController,
"register",
[name,
duration,
secret,
resolver,
data,
reverseRecord,
ownerControlledFuses],
totalPrice
);
const globalRegisterTx = await globalRegistrarController.register(
name,
duration,
secret,
resolver,
data,
reverseRecord,
ownerControlledFuses,
{ ...globalRegisterGasSettings, value: totalPrice }
);
console.log("Global registration transaction sent:", globalRegisterTx.hash);
await globalRegisterTx.wait();
console.log("Global registration successful!");
return {
name: `${name}.global`,
owner: secondLevelController, // Return second level controller as owner
signer: this.signer.address, // Include signer address for reference
duration: duration,
transactionHash: globalRegisterTx.hash,
price: ethers.utils.formatEther(totalPrice),
secondLevelController: secondLevelController
};
} catch (error) {
console.error("Error purchasing domain:", error.message);
throw error;
}
}
async purchaseNameETHController(name, durationInDays = 365) {
console.log(`Purchasing domain name: ${name}.global for ${durationInDays} days...`);
try {
// Convert days to seconds for duration
const duration = durationInDays * 24 * 60 * 60;
// Get required contracts
const ethRegistrarController = await this.deploymentManager.deployIfNeeded("ETHRegistrarController");
const publicResolver = await this.deploymentManager.deployIfNeeded("PublicResolver");
const owner = await ethRegistrarController.address;
console.log("Owner: ", owner);
return
// Generate a secret for the commitment
const secret = "0x" + keccak256("secret").toString('hex');
// Prepare commitment parameters
const resolver = publicResolver.address;
const data = [];
const reverseRecord = false;
const ownerControlledFuses = 0;
// Get price for the domain
const price = await ethRegistrarController.rentPrice(name, duration);
const totalPrice = price.base.add(price.premium);
console.log(`Domain price: ${ethers.utils.formatEther(totalPrice)} ETH`);
// Create commitment hash - use second level controller as owner
const commitment = await ethRegistrarController.makeCommitment(
name,
this.signer.address, // Use second level controller as owner
duration,
secret,
resolver,
data,
reverseRecord,
ownerControlledFuses
);
// Send commitment transaction
console.log("Sending commitment transaction...");
const gasSettings = await this.deploymentManager.getGasSettings(ethRegistrarController, "commit", [commitment]);
const commitTx = await ethRegistrarController.commit(commitment, gasSettings);
console.log("Commitment transaction sent:", commitTx.hash);
await commitTx.wait();
console.log("Commitment confirmed!");
// Wait for the minimum required time
const minCommitmentAge = await ethRegistrarController.minCommitmentAge();
const waitTimeMs = Math.max(minCommitmentAge.toNumber() * 1000, 10000);
console.log(`Waiting ${waitTimeMs/1000} seconds before registration...`);
await new Promise(resolve => setTimeout(resolve, waitTimeMs));
// Register the domain - use second level controller as owner
console.log("Registering domain...");
// Register the domain with the global registrar controller
const ethRegisterGasSettings = await this.deploymentManager.getGasSettings(
ethRegistrarController,
"register",
name,
this.signer.address,
duration,
secret,
resolver,
data,
reverseRecord,
ownerControlledFuses
);
const ethRegisterTx = await ethRegistrarController.register(
name,
this.signer.address,
duration,
secret,
resolver,
data,
reverseRecord,
ownerControlledFuses,
{ ...ethRegisterGasSettings, value: totalPrice }
);
console.log("Global registration transaction sent:", ethRegisterTx.hash);
await ethRegisterTx.wait();
console.log("Global registration successful!");
return {
name: `${name}.global`,
owner: this.signer.address, // Return second level controller as owner
signer: this.signer.address, // Include signer address for reference
duration: duration,
transactionHash: ethRegisterTx.hash,
price: ethers.utils.formatEther(totalPrice)
};
} catch (error) {
console.error("Error purchasing domain:", error.message);
throw error;
}
}
/**
* Get second level controller address for a service provider
*/
async getSecondLevelController(spAddress) {
const GlobalRegistrarController = await this.deploymentManager.deployIfNeeded("GlobalRegistrarController");
const secondLevelControllerAddress = await GlobalRegistrarController.get2LDControllerFor(spAddress);
if (secondLevelControllerAddress === "0x0000000000000000000000000000000000000000") {
throw new Error("No second level controller found for this address");
}
return await this.deploymentManager.getDeployedContract("SecondLevelController", secondLevelControllerAddress);
}
/**
* Get second level interactor for a service provider
*/
async getSecondLevelInteractor(spAddress) {
const secondLevelController = await this.getSecondLevelController(spAddress);
const secondLevelInteractorAddress = await secondLevelController.getSecondLevelInteractor();
if (secondLevelInteractorAddress === "0x0000000000000000000000000000000000000000") {
throw new Error("No second level interactor found for this service provider");
}
return await this.deploymentManager.getDeployedContract("SecondLevelInteractor", secondLevelInteractorAddress);
}
/**
* Get product type registry for a service provider and name
*/
async getProductRegistry(spAddress, name) {
try {
// Get the second level interactor
const secondLevelInteractor = await this.getSecondLevelInteractor(spAddress);
// Get the ArnaconResolver address from the interactor
const arnaconResolverAddress = await secondLevelInteractor.arnaconResolver();
if (!arnaconResolverAddress || arnaconResolverAddress === "0x0000000000000000000000000000000000000000") {
throw new Error("No ArnaconResolver found for this service provider");
}
// Get the ArnaconResolver contract
const arnaconResolver = await this.deploymentManager.getDeployedContract("ArnaconResolver", arnaconResolverAddress);
// Get the ProductTypeRegistry address from the resolver
const productTypeRegistryAddress = await arnaconResolver.getProductTypeRegistry(name);
if (!productTypeRegistryAddress || productTypeRegistryAddress === "0x0000000000000000000000000000000000000000") {
throw new Error("No ProductTypeRegistry found in ArnaconResolver");
}
// Return the ProductTypeRegistry contract
return await this.deploymentManager.getDeployedContract("ProductTypeRegistry", productTypeRegistryAddress);
} catch (error) {
console.error("Error getting ProductTypeRegistry:", error.message);
throw error;
}
}
/**
* Get product types from registry for a service provider and name
*/
async getProductTypes(spAddress, name) {
try {
console.log(`Getting product types for name: ${name} from spAddress: ${spAddress}`);
// Get product type registry through the contract chain
const productTypeRegistry = await this.getProductRegistry(spAddress, name);
console.log("productTypeRegistry: ", productTypeRegistry.address);
// Call the getProductTypes function on the contract
const productTypes = await productTypeRegistry.getProductTypes();
console.log("Product types retrieved:", productTypes);
return productTypes;
} catch (error) {
console.error("Error getting product types from registry:", error.message);
throw error;
}
}
/**
* Get all products with metadata from ProductTypeRegistry
*/
async getAllProductTypesWithMetadata(spAddress, name) {
try {
console.log(`Getting all products with metadata from spAddress: ${spAddress} for name: ${name}`);
// Get product type registry through the contract chain
const productTypeRegistry = await this.getProductRegistry(spAddress, name);
// Get list of product types for the specific name
const productTypes = await productTypeRegistry.getProductTypes();
console.log("Product types found:", productTypes);
if (productTypes.length === 0) {
return [];
}
// Fetch metadata for each product type (from IPFS)
const products = [];
for (const productType of productTypes) {
try {
const metadataUri = await productTypeRegistry.getMetadata(productType);
let parsedMetadata;
// Check if metadata is an IPFS URI
if (metadataUri.startsWith('ipfs://')) {
const ipfsHash = metadataUri.replace('ipfs://', '');
const response = await this.deploymentManager.ipfs.fetchFromIPFS(metadataUri);
parsedMetadata = response;
} else {
// Fallback to parsing as JSON string (original behavior)
parsedMetadata = JSON.parse(metadataUri);
}
products.push({
...parsedMetadata
});
} catch (error) {
console.error(`Failed to fetch metadata for ${productType}:`, error.message);
// Add a fallback product entry
products.push({
type: productType,
name: productType,
description: 'Product details unavailable',
error: true
});
}
}
console.log("Products with metadata retrieved:", products.length);
return products;
} catch (error) {
console.error("Error getting all products with metadata:", error.message);
throw error;
}
}
/**
* Get all products from a specific NFT contract
*/
async getProductsFromContract(nftContractAddress) {
try {
console.log(`Getting products from NFT contract: ${nftContractAddress}`);
// Get the NFT contract instance
const nftContract = await this.deploymentManager.getDeployedContract("ProductsNFT", nftContractAddress);
// Get all token IDs from the contract
const tokenIds = await nftContract.getTokens();
console.log(`Found ${tokenIds.length} tokens in contract ${nftContractAddress}`);
// Fetch metadata for each token
const products = await Promise.all(tokenIds.map(async (tokenId) => {
let owner = "0x0000000000000000000000000000000000000000";
try {
// Get the token URI (IPFS URL or JSON string)
const tokenURI = await nftContract.tokenURI(tokenId);
console.log(`Token ${tokenId} URI: ${tokenURI}`);
// Get the owner of the token
owner = await nftContract.ownerOf(tokenId);
console.log("Owner: ", owner);
// Fetch metadata from IPFS or parse JSON string
let metadata;
if (tokenURI.startsWith('ipfs://')) {
const ipfsHash = tokenURI.replace('ipfs://', '');
const response = await this.deploymentManager.ipfs.fetchFromIPFS(tokenURI);
metadata = response;
} else {
metadata = JSON.parse(tokenURI);
}
return {
tokenId: tokenId.toString(),
owner: owner,
metadata: metadata,
tokenURI: tokenURI
};
} catch (error) {
console.error(`Error fetching metadata for token ${tokenId}:`, error.message);
return {
tokenId: tokenId.toString(),
owner: owner,
metadata: {
name: `Token ${tokenId}`,
description: "Metadata unavailable",
error: true
},
tokenURI: "",
error: error.message
};
}
}));
console.log(`Successfully fetched ${products.length} products from contract`);
return products;
} catch (error) {
console.error("Error getting products from contract:", error.message);
throw error;
}
}
/**
* Get all products from all NFT contracts for a service provider and name
*/
async getAllProducts(spAddress, name) {
try {
console.log(`Getting all products from all NFT contracts for spAddress: ${spAddress} and name: ${name}`);
// Get all NFT contracts for this name
const nftContracts = await this.getNFTContracts(spAddress, name);
if (!nftContracts || nftContracts.length === 0) {
console.log("No NFT contracts found for this name");
return [];
}
// Get products from each contract
const allProducts = [];
for (const nftContractAddress of nftContracts) {
try {
const products = await this.getProductsFromContract(nftContractAddress);
if (products && products.length > 0) {
allProducts.push(...products);
}
} catch (error) {
console.error(`Error getting products from contract ${nftContractAddress}:`, error.message);
}
}
console.log(`Total products found across all contracts: ${allProducts.length}`);
return allProducts;
} catch (error) {
console.error("Error getting all products:", error.message);
throw error;
}
}
/**
* Create provision and update resolver
*/
async createProvisionAndUpdateResolver(spAddress, key, tokenURI, name, identifier) {
try {
console.log(`Creating provision and updating resolver for key: ${key}, name: ${name}, identifier: ${identifier}`);
// Get the ProvisionRegistry contract instance
const provisionRegistry = await this.deploymentManager.deployIfNeeded("ProvisionRegistry", 1);
// Get the mint price from the contract
const mintPrice = await provisionRegistry.mintPrice();
console.log("Mint price:", mintPrice.toString());
// Get the PublicResolver contract
const publicResolver = await this.deploymentManager.deployIfNeeded("PublicResolver");
// Check if provision registry is already set for this name
const node = namehash(`${name}.global`);
const currentProvisionRegistry = await publicResolver.text(node, "provisionRegistry");
console.log("Current provision registry:", currentProvisionRegistry);
let setProvisionRegistry = true;
if (currentProvisionRegistry === provisionRegistry.address) {
setProvisionRegistry = false;
console.log("Provision registry already set, skipping set operation");
}
// Upload metadata to IPFS if it's an object, otherwise use as-is
let metadataUri;
if (typeof tokenURI === 'object' && tokenURI !== null) {
metadataUri = await this.deploymentManager.ipfs.uploadToIPFS(tokenURI);
} else {
console.log("Using tokenURI as-is");
metadataUri = tokenURI;
}
// Get the second level interactor
const secondLevelInteractor = await this.getSecondLevelInteractor(spAddress);
console.log("Second level interactor:", secondLevelInteractor.address);
const gasSettings = await this.deploymentManager.getGasSettings(secondLevelInteractor, "createProvisionAndUpdateResolver", [key, metadataUri, name, identifier, setProvisionRegistry], mintPrice + 1);
// Create the provision and update resolver
const tx = await secondLevelInteractor.createProvisionAndUpdateResolver(
key, metadataUri, name, identifier, setProvisionRegistry,
{ value: mintPrice + 1, ...gasSettings }
);
await tx.wait();
console.log(`Provision created and resolver updated successfully for key: ${key}`);
return {
key,
tokenURI: metadataUri,
name,
identifier,
setProvisionRegistry,
mintPrice: mintPrice.toString(),
secondLevelInteractor: secondLevelInteractor.address
};
} catch (error) {
console.error("Error creating provision and updating resolver:", error.message);
throw error;
}
}
/**
* Create a text record
*/
async createRecord(spAddress, key, value, name) {
try {
console.log(`Creating text record for key: ${key}, value: ${value}, name: ${name}`);
// Get the second level interactor
const secondLevelInteractor = await this.getSecondLevelInteractor(spAddress);
// Create the text record
await this.deploymentManager.executeTransaction(
secondLevelInteractor,
"setText",
[name, key, value],
"Creating text record"
);
console.log(`Text record created successfully for key: ${key}`);
return {
key,
value,
name,
secondLevelInteractor: secondLevelInteractor.address
};
} catch (error) {
console.error("Error creating text record:", error.message);
throw error;
}
}
/**
* Get a text record
*/
async getRecord(key, name) {
try {
const resolver = await this.deploymentManager.deployIfNeeded("PublicResolver");
const value = await resolver.text(namehash(`${name}.global`), key);
console.log(`Text record for key: ${key}, name: ${name} is: ${value}`);
return value;
} catch (error) {
console.error("Error getting text record:", error.message);
throw error;
}
}
/**
* Create a product type
*/
async createProductType(spAddress, name, productType, productInfo) {
try {
console.log(`Creating product type: ${productType} for name: ${name}`);
// Get the product type registry
const productTypeRegistry = await this.getProductRegistry(spAddress, name);
// Update productInfo with additional fields
const enhancedProductInfo = {
...productInfo,
type: productType,
timestamp: Math.floor(Date.now() / 1000), // Current timestamp in seconds
productTypeRegistry: productTypeRegistry.address
};
// Upload metadata to IPFS and get URI
const metadata = await this.deploymentManager.ipfs.uploadToIPFS(enhancedProductInfo);
// Create the product type
await this.deploymentManager.executeTransaction(
productTypeRegistry,
"createProductType",
[productType, metadata],
"Creating product type"
);
console.log(`Product type ${productType} created successfully`);
return {
productType,
metadata,
productTypeRegistry: productTypeRegistry.address
};
} catch (error) {
console.error("Error creating product type:", error.message);
throw error;
}
}
/**
* Deploy NFT contract and add it to the resolver
*/
async deployNFTContract(spAddress, name) {
try {
console.log(`Deploying NFT contract for name: ${name}`);
// Get the second level interactor
const secondLevelInteractor = await this.getSecondLevelInteractor(spAddress);
// Get the ArnaconResolver address from the interactor
const arnaconResolverAddress = await secondLevelInteractor.arnaconResolver();
if (!arnaconResolverAddress || arnaconResolverAddress === "0x0000000000000000000000000000000000000000") {
throw new Error("No ArnaconResolver found for this service provider");
}
// Get the ArnaconResolver contract
const arnaconResolver = await this.deploymentManager.getDeployedContract("ArnaconResolver", arnaconResolverAddress);
// Check if NFT contracts already exist for this name
const existingNFTContracts = await arnaconResolver.getNFTContracts(name);
if (existingNFTContracts && existingNFTContracts.length > 0) {
console.log(`NFT contracts already exist for ${name}:`, existingNFTContracts);
return {
nftContractAddress: existingNFTContracts[0], // Return the first one
name: name,
arnaconResolver: arnaconResolverAddress,
existing: true
};
}
// Deploy ProductsNFT
const productsNFT = await this.deploymentManager.deployIfNeeded("ProductsNFT", arnaconResolverAddress);
console.log("ProductsNFT deployed at:", productsNFT.address);
// Add NFT contract to the resolver
await this.deploymentManager.executeTransaction(
arnaconResolver,
"addNFTContract",
[name, productsNFT.address],
"Adding NFT contract to resolver"
);
console.log(`NFT contract deployed and added successfully for ${name}`);
return {
nftContractAddress: productsNFT.address,
name: name,
arnaconResolver: arnaconResolverAddress,
existing: false
};
} catch (error) {
console.error("Error deploying NFT contract:", error.message);
throw error;
}
}
/**
* Get NFT contracts for a service provider and name
*/
async getNFTContracts(spAddress, name) {
try {
// Get the second level interactor
const secondLevelInteractor = await this.getSecondLevelInteractor(spAddress);
// Get the ArnaconResolver address from the interactor
const arnaconResolverAddress = await secondLevelInteractor.arnaconResolver();
if (!arnaconResolverAddress || arnaconResolverAddress === "0x0000000000000000000000000000000000000000") {
throw new Error("No ArnaconResolver found for this service provider");
}
// Get the ArnaconResolver contract
const arnaconResolver = await this.deploymentManager.getDeployedContract("ArnaconResolver", arnaconResolverAddress);
// Get the NFT contracts array for the specified name
const nftContracts = await arnaconResolver.getNFTContracts(name);
return nftContracts;
} catch (error) {
console.error("Error getting NFT contracts:", error.message);
throw error;
}
}
/**
* Create a product (mint and attach)
*/
async createProduct(spAddress, label, name, productInfo, productType) {
try {
console.log(`Creating product for ENS: type: ${productType}`);
console.log(`Product info received:`, JSON.stringify(productInfo, null, 2));
console.log(`Product type received:`, productType);
console.log(`SP Address:`, spAddress);
console.log(`Parameter types - ens: productInfo: ${typeof productInfo}, productType: ${typeof productType}`);
// Get the second level interactor
const secondLevelInteractor = await this.getSecondLevelInteractor(spAddress);
// Get the ArnaconResolver address from the interactor
const arnaconResolverAddress = await secondLevelInteractor.arnaconResolver();
if (!arnaconResolverAddress || arnaconResolverAddress === "0x0000000000000000000000000000000000000000") {
throw new Error("No ArnaconResolver found for this service provider");
}
// Get the ArnaconResolver contract
const arnaconResolver = await this.deploymentManager.getDeployedContract("ArnaconResolver", arnaconResolverAddress);
// Get NFT contracts for this ENS
const nftContracts = await arnaconResolver.getNFTContracts(name);
if (!nftContracts || nftContracts.length === 0) {
throw new Error(`No NFT contracts found for yes`);
}
// Use the first NFT contract
const nftContractAddress = nftContracts[0];
const nftContract = await this.deploymentManager.getDeployedContract("ProductsNFT", nftContractAddress);
// Try to get the product type registry address if possible
let enhancedProductInfo = { ...productInfo };
try {
// Get the ProductTypeRegistry address from the resolver
const productTypeRegistryAddress = await arnaconResolver.getProductTypeRegistry(name);
if (productTypeRegistryAddress && productTypeRegistryAddress !== "0x0000000000000000000000000000000000000000") {
enhancedProductInfo.productTypeRegistry = productTypeRegistryAddress;
console.log("Added product type registry to metadata:", productTypeRegistryAddress);
}
} catch (error) {
console.warn("Could not get product type registry address, creating product without it");
}
// Upload metadata to IPFS
const metadata = await this.deploymentManager.ipfs.uploadToIPFS(enhancedProductInfo);
// Step 1: Mint the product
console.log("Step 1: Minting product...");
await this.deploymentManager.executeTransaction(
nftContract,
"mint",
[this.signer.address, metadata],
"Minting product NFT"
);
// Get the minted token ID
const tokenIds = await nftContract.getTokens();
const tokenId = tokenIds[tokenIds.length - 1]; // Get the latest minted token
console.log(`Product minted with token ID: ${tokenId}`);
// Step 2: Attach the product to the resolver
console.log("Step 2: Attaching product to resolver...");
const fullName = `${label}.${name}.global`
const node = namehash(fullName);
await this.deploymentManager.executeTransaction(
arnaconResolver,
"createProduct",
[node, nftContractAddress, tokenId, 0], // duration = 0 for now
"Attaching product to resolver"
);
console.log(`Product created successfully for ${name}`);
return {
fullName,
productType,
productInfo: enhancedProductInfo,
tokenId: tokenId.toString(),
nftContractAddress: nftContractAddress,
transactionHash: "completed"
};
} catch (error) {
console.error("Error creating product:", error.message);
throw error;
}
}
async burnNFT(spAddress, name, tokenId) {
try {
console.log(`Burning NFT for ${tokenId}`);
const secondLevelInteractor = await this.getSecondLevelInteractor(spAddress);
const arnaconResolverAddress = await secondLevelInteractor.arnaconResolver();
if (!arnaconResolverAddress || arnaconResolverAddress === "0x0000000000000000000000000000000000000000") {
throw new Error("No ArnaconResolver found for this service provider");
}
const arnaconResolver = await this.deploymentManager.getDeployedContract("ArnaconResolver", arnaconResolverAddress);
const nftContracts = await arnaconResolver.getNFTContracts(name);
const nftContractAddress = nftContracts[0];
const nftContract = await this.deploymentManager.getDeployedContract("ProductsNFT", nftContractAddress);
await this.deploymentManager.executeTransaction(nftContract, "burn", [tokenId], "Burning NFT");
console.log(`NFT burned successfully for ${tokenId}`);
return {
tokenId: tokenId.toString(),
transactionHash: "completed"
};
} catch (error) {
console.error("Error burning NFT:", error.message);
throw error;
}
}
/**
* Register a subdomain
*/
async registerSubdomain(spAddress, owner, label, name) {
try {
console.log(`Registering subdomain: ${label}.${name} for owner: ${owner}`);
// Get the second level interactor
const secondLevelInteractor = await this.getSecondLevelInteractor(spAddress);
// Register the subdomain
await this.deploymentManager.executeTransaction(
secondLevelInteractor,
"registerSubnode",
[owner, label, name],
"Registering subdomain"
);
console.log(`Subdomain ${label}.${name} registered successfully for owner: ${owner}`);
return {
owner,
label,
name,
fullDomain: `${label}.${name}`,
secondLevelInteractor: secondLevelInteractor.address
};
} catch (error) {
console.error("Error registering subdomain:", error.message);
throw error;
}
}
/**
* Batch create products
*/
async batchCreateProduct(spAddress, ens, productInfos, productTypes) {
try {
console.log(`Batch creating ${productInfos.length} products for ENS: ${ens}`);
if (productInfos.length !== productTypes.length) {
throw new Error("Product infos and product types arrays must have the same length");
}
// Get the second level interactor
const secondLevelInteractor = await this.getSecondLevelInteractor(spAddress);
// Get the ArnaconResolver address from the interactor
const arnaconResolverAddress = await secondLevelInteractor.arnaconResolver();
if (!arnaconResolverAddress || arnaconResolverAddress === "0x0000000000000000000000000000000000000000") {
throw new Error("No ArnaconResolver found for this service provider");
}
// Get the ArnaconResolver contract
const arnaconResolver = await this.deploymentManager.getDeployedContract("ArnaconResolver", arnaconResolverAddress);
// Get NFT contracts for this ENS
const nftContracts = await arnaconResolver.getNFTContracts(ens);
if (!nftContracts || nftContracts.length === 0) {
throw new Error(`No NFT contracts found for ENS: ${ens}`);
}
// Use the first NFT contract
const nftContractAddress = nftContracts[0];
const nftContract = await this.deploymentManager.getDeployedContract("ProductsNFT", nftContractAddress);
// Try to get the product type registry address if possible
let enhancedProductInfos = productInfos.map(productInfo => ({ ...productInfo }));
try {
// Get the ProductTypeRegistry address from the resolver
const productTypeRegistryAddress = await arnaconResolver.getProductTypeRegistry(ens);
if (productTypeRegistryAddress && productTypeRegistryAddress !== "0x0000000000000000000000000000000000000000") {
enhancedProductInfos = enhancedProductInfos.map(productInfo => ({
...productInfo,
productTypeRegistry: productTypeRegistryAddress
}));
console.log("Added product type registry to metadata:", productTypeRegistryAddress);
}
} catch (error) {
console.warn("Could not get product type registry address:", error.message);
}
// Upload all metadata to IPFS
const metadataArray = await Promise.all(
enhancedProductInfos.map(productInfo =>
this.deploymentManager.ipfs.uploadToIPFS(productInfo)
)
);
// Step 1: Batch mint the products
console.log("Step 1: Batch minting products...");
await this.deploymentManager.executeTransaction(
nftContract,
"batchMint",
[this.signer.address, metadataArray],
"Batch minting product NFTs"
);
// Get the minted token IDs
const tokenIds = await nftContract.getTokens();
const newTokenIds = tokenIds.slice(-productInfos.length); // Get the latest min