import {
createPublicClient, createWalletClient, custom, defineChain, http,
decodeEventLog, erc20Abi, getAddress, isAddress, parseUnits, zeroAddress,
type Abi, type Address, type EIP1193Provider, type Hash,
} from "viem";
type ReleasedInputs = {
productId: string;
releaseVersion: string;
chainId: number;
nativeCurrency: { name: string; symbol: string; decimals: number };
rpcUrl: string;
confirmations: number;
paymentToken: Address;
productToken: Address;
depositVault: Address;
redemptionVault: Address;
};
type ContractAbis = { deposit: Abi; redemption: Abi };
type Submission = {
chainId: number; productId: string; contract: Address;
functionName: string; hash: Hash; replacesHash?: Hash; reason?: string;
};
export async function connectIntegration(
c: ReleasedInputs, abis: ContractAbis, provider: EIP1193Provider,
recordSubmission: (entry: Submission) => void,
) {
if (!c.productId || !c.releaseVersion ||
!Number.isInteger(c.chainId) || c.chainId <= 0 ||
!Number.isInteger(c.confirmations) || c.confirmations < 1) {
throw new Error("Enter the release specification and confirmation criteria");
}
for (const address of [
c.paymentToken, c.productToken, c.depositVault, c.redemptionVault,
]) {
if (!isAddress(address) || getAddress(address) === zeroAddress) {
throw new Error("Enter the official call addresses");
}
}
const chain = defineChain({
id: c.chainId, name: "Configured ARCO network",
nativeCurrency: c.nativeCurrency,
rpcUrls: { default: { http: [c.rpcUrl] } },
});
const publicClient = createPublicClient({ chain, transport: http(c.rpcUrl) });
const walletClient = createWalletClient({ chain, transport: custom(provider) });
const [account] = await walletClient.requestAddresses();
if (!account) throw new Error("No connected account");
async function checkNetwork() {
const [current] = await walletClient.getAddresses();
if (!current || getAddress(current) !== getAddress(account)) {
throw new Error("The account has changed. Prepare the integration state again");
}
if (await publicClient.getChainId() !== c.chainId ||
await walletClient.getChainId() !== c.chainId) {
throw new Error("Check the selected product's network");
}
}
await checkNetwork();
async function read<T>(
address: Address, abi: Abi, functionName: string, args: readonly unknown[] = [],
): Promise<T> {
return await publicClient.readContract({ address, abi, functionName, args }) as T;
}
async function submit(
address: Address, abi: Abi, functionName: string, args: readonly unknown[],
) {
await checkNetwork();
const { request } = await publicClient.simulateContract({
address, abi, functionName, args, account,
});
const hash = await walletClient.writeContract(request);
try {
recordSubmission({ chainId: c.chainId, productId: c.productId,
contract: address, functionName, hash });
const receipt = await publicClient.waitForTransactionReceipt({
hash, confirmations: c.confirmations,
onReplaced(replacement) {
recordSubmission({ chainId: c.chainId, productId: c.productId,
contract: address, functionName, hash: replacement.transactionReceipt.transactionHash,
replacesHash: hash, reason: replacement.reason });
},
});
if (receipt.status !== "success") throw new Error("Transaction failed");
return receipt;
} catch (cause) {
throw new Error("Original transaction lookup required: " + hash, { cause });
}
}
type Receipt = Awaited<ReturnType<typeof submit>>;
function eventArgs(receipt: Receipt, address: Address, abi: Abi, name: string) {
for (const log of receipt.logs) {
if (getAddress(log.address) !== getAddress(address)) continue;
try {
const event = decodeEventLog({ abi, data: log.data, topics: log.topics });
if (event.eventName === name && event.args && !Array.isArray(event.args)) {
return event.args as unknown as Record<string, unknown>;
}
} catch { /* Skip logs that do not belong to this ABI. */ }
}
throw new Error("Expected event not found. Query the original transaction");
}
function hasTransfer(
receipt: Receipt, token: Address, from: Address, to: Address, value: bigint,
) {
return receipt.logs.some((log) => {
if (getAddress(log.address) !== getAddress(token)) return false;
try {
const event = decodeEventLog({ abi: erc20Abi, data: log.data, topics: log.topics });
return event.eventName === "Transfer" &&
getAddress(event.args.from) === getAddress(from) &&
getAddress(event.args.to) === getAddress(to) && event.args.value === value;
} catch { return false; }
});
}
return { c, abis, account, publicClient, read, submit, eventArgs, hasTransfer };
}
type Integration = Awaited<ReturnType<typeof connectIntegration>>;
function exactUnits(amount: string, decimals: number) {
if (!/^(0|[1-9][0-9]*)([.][0-9]+)?$/.test(amount) ||
(amount.split(".")[1]?.length ?? 0) > decimals) {
throw new Error("Check the amount format and decimal precision");
}
return parseUnits(amount, decimals);
}