light-curate-data-service
Version:
A TypeScript library for interacting with LightGeneralizedTCR contracts
1,050 lines • 56.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.LightCurateRegistry = void 0;
const sonner_1 = require("sonner");
const types_1 = require("./types");
/**
* LightCurateRegistry provides a class-based interface to interact with the LightGeneralizedTCR contract
* using Web3.js
*/
class LightCurateRegistry {
/**
* Creates a new LightCurateRegistry instance
* @param contractAddress The address of the LightGeneralizedTCR contract
* @param chainId The chain ID (1 for Ethereum Mainnet, 100 for Gnosis Chain)
*/
constructor(contractAddress, chainId) {
this.web3Instance = null;
this.contractInstance = null;
/**
* Gets or creates a Web3 instance
* @param provider Optional provider to use (defaults to window.ethereum or Infura)
* @returns A Web3 instance
*/
this.getWeb3 = async (provider) => {
if (!this.web3Instance) {
const Web3 = (await Promise.resolve().then(() => __importStar(require("web3")))).default;
let rpcUrl;
if (this.chainId === LightCurateRegistry.SUPPORTED_CHAINS.ETHEREUM_MAINNET) {
rpcUrl = "https://rpc.ankr.com/eth";
}
else if (this.chainId === LightCurateRegistry.SUPPORTED_CHAINS.GNOSIS_CHAIN) {
rpcUrl = "https://gnosis-pokt.nodies.app";
}
else {
throw new Error(`Unsupported chain ID: ${this.chainId}. Supported chains are: ${Object.values(LightCurateRegistry.SUPPORTED_CHAINS).join(", ")}`);
}
this.web3Instance = new Web3(provider || (typeof window !== "undefined" && window.ethereum) || rpcUrl);
}
return this.web3Instance;
};
/**
* Gets or creates a contract instance
* @returns The contract instance
*/
this.getContract = async () => {
if (!this.contractInstance) {
const web3 = await this.getWeb3();
const LCURATE_ABI = (await Promise.resolve().then(() => __importStar(require("./references/LightCurate/LightGeneralizedTCR_ABI.json")))).default;
this.contractInstance = new web3.eth.Contract(LCURATE_ABI, this.contractAddress);
}
return this.contractInstance;
};
/**
* Connects to the user's Ethereum wallet and ensures correct chain
* @returns Promise resolving to the connected account address
*/
this.connectWallet = async () => {
if (typeof window === "undefined" || !window.ethereum) {
throw new Error("MetaMask is not installed. Please install MetaMask to continue.");
}
try {
// First request accounts
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
// Then ensure we're on the correct chain
await this.ensureCorrectChain();
return accounts[0];
}
catch (error) {
console.error("Error connecting wallet:", error);
throw new Error(`Failed to connect wallet: ${error.message}`);
}
};
/**
* Ensures the wallet is connected to the correct chain
*/
this.ensureCorrectChain = async () => {
if (typeof window === "undefined" || !window.ethereum) {
throw new Error("MetaMask is not installed");
}
try {
// Get current chain ID
const currentChainId = await window.ethereum.request({
method: "eth_chainId",
});
// Convert hex chainId to number
const currentChainIdNumber = parseInt(currentChainId, 16);
// If we're not on the correct chain, try to switch
if (currentChainIdNumber !== this.chainId) {
const chainIdHex = `0x${this.chainId.toString(16)}`;
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: chainIdHex }],
});
}
catch (switchError) {
// This error code indicates that the chain has not been added to MetaMask
if (switchError.code === 4902) {
await this.addChainToWallet();
}
else {
throw switchError;
}
}
}
}
catch (error) {
console.error("Error ensuring correct chain:", error);
throw new Error(`Please switch to ${this.getChainName()}: ${error.message}`);
}
};
/**
* Adds the chain to the wallet if it doesn't exist
*/
this.addChainToWallet = async () => {
if (typeof window === "undefined" || !window.ethereum)
return;
const chainParams = this.getChainParameters();
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [chainParams],
});
};
/**
* Gets the chain parameters for adding to wallet
*/
this.getChainParameters = () => {
if (this.chainId === LightCurateRegistry.SUPPORTED_CHAINS.ETHEREUM_MAINNET) {
return {
chainId: "0x1",
chainName: "Ethereum Mainnet",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: ["https://rpc.ankr.com/eth"],
blockExplorerUrls: ["https://etherscan.io"],
};
}
else if (this.chainId === LightCurateRegistry.SUPPORTED_CHAINS.GNOSIS_CHAIN) {
return {
chainId: "0x64",
chainName: "Gnosis Chain",
nativeCurrency: {
name: "xDai",
symbol: "xDAI",
decimals: 18,
},
rpcUrls: ["https://gnosis-pokt.nodies.app"],
blockExplorerUrls: ["https://gnosisscan.io"],
};
}
throw new Error("Unsupported chain ID");
};
/**
* Gets the chain name based on chain ID
*/
this.getChainName = () => {
return this.chainId ===
LightCurateRegistry.SUPPORTED_CHAINS.ETHEREUM_MAINNET
? "Ethereum Mainnet"
: "Gnosis Chain";
};
/**
* Gets the currently connected account
* @returns Promise resolving to the current account address or null
*/
this.getCurrentAccount = async () => {
if (typeof window === "undefined" || !window.ethereum)
return null;
try {
const accounts = await window.ethereum.request({
method: "eth_accounts",
});
return accounts[0] || null;
}
catch (error) {
console.error("Error getting current account:", error);
return null;
}
};
/**
* Gets the challenge period duration in days
* @returns Promise resolving to the challenge period in days
*/
this.getChallengePeriodDurationInDays = async () => {
try {
const contract = await this.getContract();
// Get challengePeriodDuration in seconds
const challengePeriodInSeconds = await contract.methods
.challengePeriodDuration()
.call();
// Convert seconds to days (86400 seconds in a day)
const challengePeriodInDays = Math.ceil(Number(challengePeriodInSeconds) / 86400);
return challengePeriodInDays;
}
catch (error) {
console.error("Error getting challenge period duration:", error);
throw new Error("Failed to retrieve challenge period duration");
}
};
/**
* Gets or creates a Kleros Liquid contract instance
* @param arbitratorAddress The address of the Kleros Liquid arbitrator
* @returns The Kleros Liquid contract instance
*/
this.getKlerosLiquidContract = async (arbitratorAddress) => {
const web3 = await this.getWeb3();
// Import the Kleros Liquid ABI
const KLEROS_LIQUID_ABI = (await Promise.resolve().then(() => __importStar(require("./references/KlerosLiquid/KlerosLiquid_ABI.json")))).default;
// Create new contract instance
return new web3.eth.Contract(KLEROS_LIQUID_ABI, arbitratorAddress);
};
/**
* Gets the arbitration cost
* @returns Promise resolving to the arbitration cost information
*/
this.getArbitrationCost = async () => {
try {
const web3 = await this.getWeb3();
const contract = await this.getContract();
// Get arbitrator address and extra data
const arbitratorAddress = await contract.methods.arbitrator().call();
const arbitratorExtraData = await contract.methods
.arbitratorExtraData()
.call();
console.log("Arbitrator address:", arbitratorAddress);
console.log("Arbitrator extra data:", arbitratorExtraData);
// Create Kleros Liquid arbitrator contract instance
if (!arbitratorAddress || typeof arbitratorAddress !== "string") {
throw new Error("Invalid arbitrator address");
}
// Use the helper method to get the Kleros Liquid instance
const klerosLiquidInstance = await this.getKlerosLiquidContract(arbitratorAddress);
// Get actual arbitration cost
const arbitrationCostWei = await klerosLiquidInstance.methods
.arbitrationCost(arbitratorExtraData)
.call();
if (!arbitrationCostWei) {
throw new Error("Failed to retrieve arbitration cost");
}
// Convert to ETH for display
const arbitrationCost = web3.utils.fromWei(arbitrationCostWei.toString(), "ether");
return {
arbitrationCost,
arbitrationCostWei: arbitrationCostWei.toString(),
arbitrator: klerosLiquidInstance,
};
}
catch (error) {
console.error("Error getting arbitration cost:", error);
throw new Error("Failed to retrieve arbitration cost");
}
};
/**
* Calculates deposit amount for various operations
* Using arrow function to preserve 'this' context
* @param baseDepositMethod The contract method to call for base deposit
* @param baseDepositName Human-readable name for the deposit type
* @returns Promise resolving to deposit information
*/
this.calculateDepositAmount = async (baseDepositMethod, baseDepositName) => {
try {
const web3 = await this.getWeb3();
const contract = await this.getContract();
// Get challenge period duration
const challengePeriodDays = await this.getChallengePeriodDurationInDays();
// Get base deposit
const baseDepositResult = await contract.methods[baseDepositMethod]().call();
const baseDeposit = baseDepositResult
? baseDepositResult.toString()
: "0";
// Get arbitration cost
const { arbitrationCost, arbitrationCostWei } = await this.getArbitrationCost();
// Calculate total deposit
const totalDepositWei = BigInt(baseDeposit) + BigInt(arbitrationCostWei);
// Convert to ETH for display
const baseDepositEth = web3.utils.fromWei(baseDeposit, "ether");
const depositAmountEth = web3.utils.fromWei(totalDepositWei.toString(), "ether");
console.log(`${baseDepositName} calculation breakdown:`, {
baseDeposit: baseDepositEth,
arbitrationCost,
total: depositAmountEth,
});
return {
depositAmount: depositAmountEth,
depositInWei: totalDepositWei.toString(),
breakdown: {
baseDeposit: baseDepositEth,
arbitrationCost,
total: depositAmountEth,
},
challengePeriodDays,
};
}
catch (error) {
console.error(`Error getting ${baseDepositName} amount:`, error);
throw new Error(`Failed to calculate required ${baseDepositName} amount: ${error instanceof Error ? error.message : "Unknown error"}`);
}
};
/**
* Gets the submission deposit amount
* @returns Promise resolving to deposit information
*/
this.getSubmissionDepositAmount = async () => {
return this.calculateDepositAmount("submissionBaseDeposit", "submission deposit");
};
/**
* Gets the submission challenge deposit amount
* @returns Promise resolving to deposit information
*/
this.getSubmissionChallengeDepositAmount = async () => {
return this.calculateDepositAmount("submissionChallengeBaseDeposit", "submission challenge deposit");
};
/**
* Gets the removal deposit amount
* @returns Promise resolving to deposit information
*/
this.getRemovalDepositAmount = async () => {
return this.calculateDepositAmount("removalBaseDeposit", "removal deposit");
};
/**
* Gets the removal challenge deposit amount
* @returns Promise resolving to deposit information
*/
this.getRemovalChallengeDepositAmount = async () => {
return this.calculateDepositAmount("removalChallengeBaseDeposit", "removal challenge deposit");
};
/**
* Submits an item to the registry
* @param ipfsPath The IPFS path of the item
* @returns Promise resolving to the transaction hash
*/
this.submitToRegistry = async (ipfsPath) => {
if (typeof window === "undefined" || !window.ethereum) {
throw new Error("MetaMask is not installed. Please install MetaMask to continue.");
}
// Ensure ipfsPath starts with "/ipfs/"
const formattedPath = ipfsPath.startsWith("/ipfs/")
? ipfsPath
: `/ipfs/${ipfsPath}`;
try {
// Ensure we're on the correct chain
await this.ensureCorrectChain();
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const from = accounts[0];
// Create Web3 instance
const web3 = await this.getWeb3(window.ethereum);
const contract = await this.getContract();
// Get required deposit amount
const { depositInWei } = await this.getSubmissionDepositAmount();
// Estimate gas and get current gas price
const gasEstimate = await contract.methods
.addItem(formattedPath)
.estimateGas({
from,
value: depositInWei,
});
const gasPrice = await web3.eth.getGasPrice();
// Calculate gas with 20% buffer and convert to string
const gasBigInt = BigInt(gasEstimate);
const gasWithBuffer = ((gasBigInt * BigInt(120)) /
BigInt(100)).toString();
// Convert gasPrice to string
const gasPriceString = gasPrice.toString();
// Submit transaction with the dynamic deposit amount
const txReceipt = await contract.methods.addItem(formattedPath).send({
from,
gas: gasWithBuffer,
gasPrice: gasPriceString,
value: depositInWei,
});
return txReceipt.transactionHash;
}
catch (error) {
console.error("Error submitting to registry:", error);
// Format error for user
let errorMessage = "Failed to submit to registry";
if (error.code === 4001) {
errorMessage = "Transaction rejected by user";
}
else if (error.message) {
errorMessage = `Error: ${error.message}`;
}
throw new Error(errorMessage);
}
};
/**
* Removes an item from the registry
* @param itemID The ID of the item to remove
* @param evidence Optional evidence IPFS path
* @returns Promise resolving to the transaction hash
*/
this.removeItem = async (itemID, evidence = "") => {
if (typeof window === "undefined" || !window.ethereum) {
throw new Error("MetaMask is not installed. Please install MetaMask to continue.");
}
try {
// Ensure we're on the correct chain
await this.ensureCorrectChain();
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const from = accounts[0];
// Create Web3 instance
const web3 = await this.getWeb3(window.ethereum);
const contract = await this.getContract();
// Get required deposit amount
const { depositInWei } = await this.getRemovalDepositAmount();
// Format evidence URL - ensure it starts with "/ipfs/"
const formattedEvidence = evidence
? evidence.startsWith("/ipfs/")
? evidence
: `/ipfs/${evidence}`
: "";
// Estimate gas and get current gas price
const gasEstimate = await contract.methods
.removeItem(itemID, formattedEvidence)
.estimateGas({
from,
value: depositInWei,
});
const gasPrice = await web3.eth.getGasPrice();
// Calculate gas with 20% buffer and convert to string
const gasBigInt = BigInt(gasEstimate);
const gasWithBuffer = ((gasBigInt * BigInt(120)) /
BigInt(100)).toString();
// Convert gasPrice to string
const gasPriceString = gasPrice.toString();
// Submit transaction with the dynamic deposit amount
const txReceipt = await contract.methods
.removeItem(itemID, formattedEvidence)
.send({
from,
gas: gasWithBuffer,
gasPrice: gasPriceString,
value: depositInWei,
});
return txReceipt.transactionHash;
}
catch (error) {
console.error("Error removing item from registry:", error);
// Format error for user
let errorMessage = "Failed to remove item from registry";
if (error.code === 4001) {
errorMessage = "Transaction rejected by user";
}
else if (error.message) {
errorMessage = `Error: ${error.message}`;
}
throw new Error(errorMessage);
}
};
/**
* Challenges a request
* @param itemID The ID of the item
* @param evidence Optional evidence IPFS path
* @returns Promise resolving to the transaction hash
*/
this.challengeRequest = async (itemID, evidence = "") => {
if (typeof window === "undefined" || !window.ethereum) {
throw new Error("MetaMask is not installed. Please install MetaMask to continue.");
}
try {
// Ensure we're on the correct chain
await this.ensureCorrectChain();
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const from = accounts[0];
// Create Web3 instance
const web3 = await this.getWeb3(window.ethereum);
const contract = await this.getContract();
// Get the item info to determine its status
const itemResult = (await contract.methods.items(itemID).call());
if (!itemResult) {
throw new Error("Failed to retrieve item information");
}
// Convert status to number and validate
const itemStatus = Number(itemResult.status);
if (isNaN(itemStatus)) {
throw new Error("Failed to retrieve valid item status");
}
// Use the enum for clearer status checks
if (itemStatus !== types_1.ItemStatus.RegistrationRequested &&
itemStatus !== types_1.ItemStatus.ClearingRequested) {
throw new Error("Item not in a challengeable state");
}
// Determine which deposit to use based on item status
let depositInfo;
if (itemStatus === types_1.ItemStatus.RegistrationRequested) {
depositInfo = await this.getSubmissionChallengeDepositAmount();
}
else {
depositInfo = await this.getRemovalChallengeDepositAmount();
}
// Format evidence URL
const formattedEvidence = evidence
? evidence.startsWith("/ipfs/")
? evidence
: `/ipfs/${evidence}`
: "";
// Estimate gas and get current gas price
const gasEstimate = await contract.methods
.challengeRequest(itemID, formattedEvidence)
.estimateGas({
from,
value: depositInfo.depositInWei,
});
const gasPrice = await web3.eth.getGasPrice();
// Calculate gas with 20% buffer
const gasBigInt = BigInt(gasEstimate);
const gasWithBuffer = ((gasBigInt * BigInt(120)) /
BigInt(100)).toString();
// Submit challenge transaction
const txReceipt = await contract.methods
.challengeRequest(itemID, formattedEvidence)
.send({
from,
gas: gasWithBuffer,
gasPrice: gasPrice.toString(),
value: depositInfo.depositInWei,
});
return txReceipt.transactionHash;
}
catch (error) {
console.error("Error challenging request:", error);
// Format error for user
let errorMessage = "Failed to challenge request";
if (error.code === 4001) {
errorMessage = "Transaction rejected by user";
}
else if (error.message) {
errorMessage = `Error: ${error.message}`;
}
throw new Error(errorMessage);
}
};
/**
* Formats a wallet address for display
* @param address The wallet address
* @returns Formatted address string
*/
this.formatWalletAddress = (address) => {
if (!address)
return "Not connected";
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`;
};
/**
* Handles Web3 errors
* @param error The error object
* @returns Formatted error message
*/
this.handleWeb3Error = (error) => {
let message = "An unknown error occurred";
if (typeof error === "string") {
message = error;
}
else if (error === null || error === void 0 ? void 0 : error.message) {
message = error.message.replace("MetaMask Tx Signature: ", "");
// Clean up common Web3 errors
if (message.includes("User denied")) {
message = "Transaction was rejected";
}
else if (message.includes("insufficient funds")) {
message = "Insufficient funds for transaction";
}
}
// Limit message length
if (message.length > 100) {
message = message.substring(0, 100) + "...";
}
return message;
};
/**
* Switches to the correct chain based on the chainId provided in constructor
* @returns Promise resolving to success status
*/
this.switchToCorrectChain = async () => {
try {
await this.ensureCorrectChain();
return true;
}
catch (error) {
console.error("Error switching network:", error);
if (typeof sonner_1.toast !== "undefined") {
sonner_1.toast.error(`Please switch to ${this.getChainName()}`);
}
return false;
}
};
/**
* Gets the current chain ID
* @returns The chain ID
*/
this.getChainId = () => {
return this.chainId;
};
/**
* Submit evidence for an item in the registry
* @param itemID The ID of the item which the evidence is related to
* @param evidenceURI A link to an evidence using its IPFS URI
* @returns Transaction hash of the evidence submission
*/
this.submitEvidence = async (itemID, evidenceURI) => {
if (typeof window === "undefined" || !window.ethereum) {
throw new Error("MetaMask is not installed. Please install MetaMask to continue.");
}
try {
// Ensure we're on the correct chain
await this.ensureCorrectChain();
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const from = accounts[0];
// Create Web3 instance
const web3 = await this.getWeb3(window.ethereum);
const contract = await this.getContract();
// Format evidence URI - ensure it starts with "/ipfs/"
const formattedEvidence = evidenceURI
? evidenceURI.startsWith("/ipfs/")
? evidenceURI
: `/ipfs/${evidenceURI}`
: "";
// Estimate gas and get current gas price
const gasEstimate = await contract.methods
.submitEvidence(itemID, formattedEvidence)
.estimateGas({
from,
});
const gasPrice = await web3.eth.getGasPrice();
// Calculate gas with 20% buffer
const gasBigInt = BigInt(gasEstimate);
const gasWithBuffer = ((gasBigInt * BigInt(120)) /
BigInt(100)).toString();
// Submit transaction
const txReceipt = await contract.methods
.submitEvidence(itemID, formattedEvidence)
.send({
from,
gas: gasWithBuffer,
gasPrice: gasPrice.toString(),
});
return txReceipt.transactionHash;
}
catch (error) {
console.error("Error submitting evidence:", error);
// Format error for user
let errorMessage = "Failed to submit evidence";
if (error.code === 4001) {
errorMessage = "Transaction rejected by user";
}
else if (error.message) {
errorMessage = `Error: ${error.message}`;
}
throw new Error(errorMessage);
}
};
/**
* Gets the appeal cost for a specific item and request
* @param itemID The ID of the item
* @param requestID The ID of the request (usually 0 for new items)
* @returns Promise resolving to appeal cost information
*/
this.getAppealCost = async (itemID, requestID = 0) => {
let web3;
let contract;
let disputeData;
let arbitratorAddress;
let disputeID;
let arbitratorExtraData;
let currentRuling;
let klerosLiquidInstance;
let arbitrationCostWei;
// Step 1: Initialize web3 and contract instances
try {
console.log(`Getting appeal cost for itemID: ${itemID}, requestID: ${requestID}`);
web3 = await this.getWeb3();
contract = await this.getContract();
}
catch (error) {
console.error("Error initializing web3 or contract:", error);
throw new Error(`Failed to initialize web3 or contract: ${error instanceof Error ? error.message : "Unknown error"}`);
}
// Step 2: Get dispute data
try {
console.log(`Fetching dispute data for itemID: ${itemID}, requestID: ${requestID}`);
disputeData = await contract.methods
.getRequestInfo(itemID, requestID)
.call();
if (!disputeData.disputed) {
console.error("Item is not disputed", disputeData);
throw new Error("Item is not disputed, no appeal cost available");
}
// Field names corrected to match the ABI
arbitratorAddress = disputeData.requestArbitrator;
disputeID = disputeData.disputeID;
arbitratorExtraData = disputeData.requestArbitratorExtraData;
currentRuling = parseInt(disputeData.ruling);
console.log("Dispute data:", {
arbitratorAddress,
disputeID,
arbitratorExtraData,
numberOfRounds: parseInt(disputeData.numberOfRounds),
currentRuling,
});
}
catch (error) {
console.error("Error getting dispute data:", error);
throw new Error(`Failed to get dispute data: ${error instanceof Error ? error.message : "Unknown error"}`);
}
// Step 3: Get Kleros Liquid contract instance
try {
console.log(`Getting Kleros Liquid contract instance for arbitrator: ${arbitratorAddress}`);
klerosLiquidInstance =
await this.getKlerosLiquidContract(arbitratorAddress);
}
catch (error) {
console.error("Error getting Kleros Liquid contract:", error);
throw new Error(`Failed to get Kleros Liquid contract: ${error instanceof Error ? error.message : "Unknown error"}`);
}
// Step 4: Get appeal cost from arbitrator
try {
console.log(`Getting appeal cost for disputeID: ${disputeID} and ${arbitratorExtraData}`);
arbitrationCostWei = await klerosLiquidInstance.methods
.appealCost(disputeID, arbitratorExtraData)
.call();
console.log(`Appeal base cost: ${arbitrationCostWei} wei`);
}
catch (error) {
console.error("Error getting appeal cost from arbitrator:", error);
throw new Error(`Failed to get appeal cost from arbitrator: ${error instanceof Error ? error.message : "Unknown error"}`);
}
// Step 5: Get multipliers and calculate fees
let loserStakeMultiplier;
let winnerStakeMultiplier;
let sharedStakeMultiplier;
let requesterAppealFeeWei;
let challengerAppealFeeWei;
try {
console.log("Getting stake multipliers");
loserStakeMultiplier = await contract.methods
.loserStakeMultiplier()
.call();
winnerStakeMultiplier = await contract.methods
.winnerStakeMultiplier()
.call();
sharedStakeMultiplier = await contract.methods
.sharedStakeMultiplier()
.call();
console.log("Stake multipliers:", {
loserStakeMultiplier,
winnerStakeMultiplier,
sharedStakeMultiplier,
});
const MULTIPLIER_DIVISOR = 10000; // 100% is 10000 in the contract
// Convert to BigInt for safe math operations with large numbers
const arbitrationCost = BigInt(arbitrationCostWei);
// Calculate appeal fees based on ruling
if (currentRuling === 0) {
// No ruling, use shared stake multiplier for both parties
const sharedMultiplier = BigInt(sharedStakeMultiplier);
const stake = (arbitrationCost * sharedMultiplier) / BigInt(MULTIPLIER_DIVISOR);
requesterAppealFeeWei = (arbitrationCost + stake).toString();
challengerAppealFeeWei = (arbitrationCost + stake).toString();
}
else if (currentRuling === 1) {
// Requester is winning, apply loser multiplier to challenger and winner to requester
const winnerMultiplier = BigInt(winnerStakeMultiplier);
const loserMultiplier = BigInt(loserStakeMultiplier);
const requesterStake = (arbitrationCost * winnerMultiplier) / BigInt(MULTIPLIER_DIVISOR);
const challengerStake = (arbitrationCost * loserMultiplier) / BigInt(MULTIPLIER_DIVISOR);
requesterAppealFeeWei = (arbitrationCost + requesterStake).toString();
challengerAppealFeeWei = (arbitrationCost + challengerStake).toString();
}
else if (currentRuling === 2) {
// Challenger is winning, apply loser multiplier to requester and winner to challenger
const winnerMultiplier = BigInt(winnerStakeMultiplier);
const loserMultiplier = BigInt(loserStakeMultiplier);
const requesterStake = (arbitrationCost * loserMultiplier) / BigInt(MULTIPLIER_DIVISOR);
const challengerStake = (arbitrationCost * winnerMultiplier) / BigInt(MULTIPLIER_DIVISOR);
requesterAppealFeeWei = (arbitrationCost + requesterStake).toString();
challengerAppealFeeWei = (arbitrationCost + challengerStake).toString();
}
console.log("Calculated appeal fees:", {
requesterAppealFeeWei,
challengerAppealFeeWei,
});
}
catch (error) {
console.error("Error calculating appeal fees:", error);
throw new Error(`Failed to calculate appeal fees: ${error instanceof Error ? error.message : "Unknown error"}`);
}
// Step 6: Convert Wei to ETH and return results
try {
console.log("Converting Wei to ETH");
const requesterAppealFee = web3.utils.fromWei(requesterAppealFeeWei, "ether");
const challengerAppealFee = web3.utils.fromWei(challengerAppealFeeWei, "ether");
// Ensure these variables are never undefined
requesterAppealFeeWei = requesterAppealFeeWei || "0";
challengerAppealFeeWei = challengerAppealFeeWei || "0";
console.log("Final appeal costs:", {
requesterAppealFee,
challengerAppealFee,
currentRuling,
});
return {
requesterAppealFee,
challengerAppealFee,
requesterAppealFeeWei,
challengerAppealFeeWei,
currentRuling,
};
}
catch (error) {
console.error("Error converting and returning results:", error);
throw new Error(`Failed to convert or return results: ${error instanceof Error ? error.message : "Unknown error"}`);
}
};
/**
* Contribute to a side in a dispute
* @param itemID The ID of the item
* @param requestID The ID of the request (usually 0 for new items)
* @param side The side to contribute to (1 = Requester, 2 = Challenger)
* @param amount Amount to contribute in ETH
* @returns Transaction hash of the contribution
*/
this.contribute = async (itemID, requestID = 0, side, amount) => {
if (typeof window === "undefined" || !window.ethereum) {
throw new Error("MetaMask is not installed. Please install MetaMask to continue.");
}
try {
// Ensure we're on the correct chain
await this.ensureCorrectChain();
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const from = accounts[0];
// Create Web3 instance
const web3 = await this.getWeb3(window.ethereum);
const contract = await this.getContract();
// Convert ETH amount to Wei
const amountWei = web3.utils.toWei(amount, "ether");
// Estimate gas and get current gas price
const gasEstimate = await contract.methods
.contribute(itemID, requestID, side)
.estimateGas({
from,
value: amountWei,
});
const gasPrice = await web3.eth.getGasPrice();
// Calculate gas with 20% buffer
const gasBigInt = BigInt(gasEstimate);
const gasWithBuffer = ((gasBigInt * BigInt(120)) /
BigInt(100)).toString();
// Submit contribution transaction
const txReceipt = await contract.methods
.contribute(itemID, requestID, side)
.send({
from,
gas: gasWithBuffer,
gasPrice: gasPrice.toString(),
value: amountWei,
});
return txReceipt.transactionHash;
}
catch (error) {
console.error("Error contributing to dispute:", error);
// Format error for user
let errorMessage = "Failed to contribute to dispute";
if (error.code === 4001) {
errorMessage = "Transaction rejected by user";
}
else if (error.message) {
errorMessage = `Error: ${error.message}`;
}
throw new Error(errorMessage);
}
};
/**
* Gets the current appeal funding status
* @param itemID The ID of the item
* @param requestID The ID of the request (usually 0 for new items)
* @returns Promise resolving to appeal funding information
*/
this.getAppealFundingStatus = async (itemID, requestID = 0) => {
try {
const web3 = await this.getWeb3();
const contract = await this.getContract();
// Get dispute data for the item and request
const disputeData = await contract.methods
.getRequestInfo(itemID, requestID)
.call();
// If the request isn't disputed, there's no appeal funding
if (!disputeData.disputed) {
throw new Error("Item is not disputed, no appeal funding available");
}
const numberOfRounds = parseInt(disputeData.numberOfRounds);
const currentRuling = parseInt(disputeData.ruling);
// Current round index (0-based, so subtract 1)
const roundIndex = numberOfRounds - 1;
// Get the round info for the current round
const roundInfo = await contract.methods
.getRoundInfo(itemID, requestID, roundIndex)
.call();
// Extract values from round info
const appealed = roundInfo.appealed;
// Party enum: None = 0, Requester = 1, Challenger = 2
const requesterAmountPaidWei = roundInfo.amountPaid[1]; // Requester = 1
const challengerAmountPaidWei = roundInfo.amountPaid[2]; // Challenger = 2
const requesterFunded = roundInfo.hasPaid[1]; // Requester = 1
const challengerFunded = roundInfo.hasPaid[2]; // Challenger = 2
// Convert Wei to ETH for display
const requesterAmountPaid = web3.utils.fromWei(requesterAmountPaidWei, "ether");
const challengerAmountPaid = web3.utils.fromWei(challengerAmountPaidWei, "ether");
// Get total appeal costs
const appealCosts = await this.getAppealCost(itemID, requestID);
// Calculate remaining amounts to fund
const requesterRemainingToFundWei = BigInt(appealCosts.requesterAppealFeeWei) -
BigInt(requesterAmountPaidWei);
const challengerRemainingToFundWei = BigInt(appealCosts.challengerAppealFeeWei) -
BigInt(challengerAmountPaidWei);
// Convert to strings, ensuring non-negative values
const requesterRemainingToFundWeiStr = requesterRemainingToFundWei > 0
? requesterRemainingToFundWei.toString()
: "0";
const challengerRemainingToFundWeiStr = challengerRemainingToFundWei > 0
? challengerRemainingToFundWei.toString()
: "0";
// Convert to ETH for display
const requesterRemainingToFund = web3.utils.fromWei(requesterRemainingToFundWeiStr, "ether");
const challengerRemainingToFund = web3.utils.fromWei(challengerRemainingToFundWeiStr, "ether");
return {
requesterFunded,
challengerFunded,
requesterAmountPaid,
challengerAmountPaid,
requesterAmountPaidWei,
challengerAmountPaidWei,
requesterRemainingToFund,
challengerRemainingToFund,
requesterRemainingToFundWei: requesterRemainingToFundWeiStr,
challengerRemainingToFundWei: challengerRemainingToFundWeiStr,
appealed,
currentRuling,
roundIndex,
};
}
catch (error) {
console.error("Error getting appeal funding status:", error);
throw new Error(`Failed to get appeal funding status: ${error.message}`);
}
};
/**
* Fund an appeal for a ruling, supporting partial funding for crowdfunding
* @param itemID The ID of the item
* @param requestID The ID of the request (usually 0 for new items)
* @param side The side to fund the appeal for (1 = Requester, 2 = Challenger)
* @param amount Optional amount to contribute (if not specified, will fund the remaining required amount).
* Partial amounts are allowed for crowdfunding appeals.
* @returns Transaction hash of the appeal funding
*/
this.fundAppeal = async (itemID, requestID = 0, side, amount) => {
if (typeof window === "undefined" || !window.ethereum) {
throw new Error("MetaMask is not installed. Please install MetaMask to continue.");
}
try {
// Ensure we're on the correct chain
await this.ensureCorrectChain();
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const from = accounts[0];
// Create Web3 instance
const web3 = await this.getWeb3(window.ethereum);
const contract = await this.getContract();
// Get current funding status
const fundingStatus = await this.getAppealFundingStatus(itemID, requestID);
// Check if side is already fully funded
if ((side === 1 && fundingStatus.requesterFunded) ||
(side === 2 && fundingStatus.challengerFunded)) {
throw new Error(`Appeal for this side is already fully funded`);
}
// Determine amount to send
let amountToSendWei;
if (amount) {
// User specified amount
amountToSendWei = web3.utils.toWei(amount, "ether");
// Get the remaining amount needed
const remainingNeededWei = side === 1
? fundingStatus.requesterRemainingToFundWei
: fundingStatus.challengerRemainingToFundWei;
// Check if user is trying to contribute more than needed
if (BigInt(amountToSendWei) > BigInt(remainingNeededWei)) {
amountToSendWei = remainingNeededWei;
}
}
else {
// Auto-calculate amount - send only what's remaining to fund
amountToSendWei =
side === 1
? fundingStatus.requesterRemainingToFundWei
: fundingStatus.challengerRemainingToFundWei;
}
// If amount is 0, the appeal is already fully funded
if (amountToSendWei === "0") {
throw new Error(`This side of the appeal is already fully funded`);
}
// Estimate gas and get current gas price
const gasEstimate = await contract.methods
.fundAppeal(itemID, requestID, side)
.estimateGas(