@moveflow/widget
Version:
> ⚠️ **This is a testnet version** of the MoveFlow Checkout Widget. It is intended for development and testing purposes only. Do not use for mainnet payments.
148 lines (147 loc) • 6.13 kB
JavaScript
// subscriptionUtils.ts
import { ethers, parseUnits } from "ethers";
import { abi as SubscriptionAbi } from "../abis/subscription";
import erc20 from "../abis/erc20.json";
import { useState } from "react";
import { convertRateTypeToSeconds } from "../helper/rateInSeconds";
export const useSubscription = (contractAddress, coinAddress, decimal) => {
const [state, setState] = useState({
isCreatingSubscription: false,
tokensApproved: false,
subscriptionCreated: false,
networkError: "",
});
const resetState = () => {
setState({
isCreatingSubscription: false,
tokensApproved: false,
subscriptionCreated: false,
networkError: "",
});
};
const handleCreateSubscription = async (payment, chainName) => {
setState((prev) => ({ ...prev, isCreatingSubscription: true }));
try {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const networkId = await provider.getNetwork();
const currentBlock = await provider.getBlock("latest");
const currentTime = currentBlock.timestamp;
// Network validation
const expectedChainId = getChainId(chainName);
if (networkId.chainId !== expectedChainId) {
throw new Error(`Please switch to ${chainName} network`);
}
// Convert times to seconds and validate
const startTimeSeconds = Math.floor(payment.startTime / 1000);
const endTimeSeconds = Math.floor(payment.endTime / 1000);
validateTimestamps(startTimeSeconds, endTimeSeconds, currentTime);
// Approve tokens (without allowance check)
await approveTokens(signer, contractAddress, coinAddress, payment, decimal);
// Create subscription
await createSubscription(signer, contractAddress, coinAddress, payment, decimal, startTimeSeconds, endTimeSeconds);
}
catch (error) {
handleSubscriptionError(error);
}
finally {
setState((prev) => ({ ...prev, isCreatingSubscription: false }));
}
};
const getChainId = (chainName) => {
switch (chainName) {
case "Viction":
return BigInt(89);
case "Lightlink":
return BigInt(1891);
case "Pharos":
return BigInt(50002);
case "Goerli":
return BigInt(5);
default:
throw new Error(`Unsupported chain: ${chainName}`);
}
};
const validateTimestamps = (start, end, current) => {
if (start <= current) {
throw new Error("Start time must be in the future");
}
if (end <= start) {
throw new Error("End time must be after start time");
}
if (end - start < 86400) {
throw new Error("Subscription duration must be at least 1 day");
}
};
const approveTokens = async (signer, contractAddress, coinAddress, payment, decimal) => {
const tokenContract = new ethers.Contract(coinAddress, erc20, signer);
try {
const approvalTx = await tokenContract.approve(contractAddress, ethers.parseUnits("99", 6), { gasLimit: 100000 } // Fixed gas limit for approval
);
setState((prev) => ({ ...prev, txHash: approvalTx.hash }));
const receipt = await approvalTx.wait();
if (receipt.status !== 1) {
throw new Error("Token approval failed");
}
}
catch (error) {
console.error("Approval error:", error);
throw new Error(`Token approval failed: ${error.message}`);
}
};
const createSubscription = async (signer, contractAddress, coinAddress, payment, decimal, startTimeSeconds, endTimeSeconds) => {
const contract = new ethers.Contract(contractAddress, SubscriptionAbi, signer);
const deposit = BigInt(parseUnits(payment.amountType === "fixed"
? payment.streamRate.toString()
: payment.depositAmount.toString(), decimal).toString());
try {
const args = [
payment.receiver,
deposit,
coinAddress,
startTimeSeconds,
endTimeSeconds,
convertRateTypeToSeconds(payment.rateType),
payment.amountType === "fixed" ? 0 : 0,
];
// console.log("args", args);
// Get gas estimate first
const estimatedGas = await contract
.getFunction("createSubscription")
.estimateGas(...args);
// Add 20% buffer
const gasLimit = (estimatedGas * 12n) / 10n;
// Then execute with the calculated limit
const tx = await contract.createSubscription(...args, { gasLimit });
setState((prev) => ({ ...prev, txHash: tx.hash }));
const receipt = await tx.wait();
if (receipt.status !== 1) {
throw new Error("Transaction failed on-chain");
}
setState((prev) => ({ ...prev, subscriptionCreated: true }));
return receipt;
}
catch (error) {
console.error("Create subscription error:", error);
throw error;
}
};
const handleSubscriptionError = (error) => {
console.error("Subscription error:", error);
let errorMessage = "Failed to create subscription";
if (error.info?.error?.message) {
errorMessage += `: ${error.info.error.message}`;
}
else if (error.message) {
errorMessage += `: ${error.message}`;
}
setState((prev) => ({ ...prev, networkError: errorMessage }));
setTimeout(() => setState((prev) => ({ ...prev, networkError: "" })), 5000);
throw error;
};
return {
state,
resetState,
handleCreateSubscription,
};
};