diff --git a/web/packages/api/src/registration/agent/agentInterface.ts b/web/packages/api/src/registration/agent/agentInterface.ts new file mode 100644 index 000000000..da212df6f --- /dev/null +++ b/web/packages/api/src/registration/agent/agentInterface.ts @@ -0,0 +1,53 @@ +import { AssetRegistry } from "@snowbridge/base-types" +import { Context } from "../../index" +import { IGatewayV2 as IGateway } from "@snowbridge/contract-types" +import { AbstractProvider, ContractTransaction } from "ethers" +import { OperationStatus } from "../../status" +import { FeeInfo, ValidationLog } from "../../toPolkadot_v2" + +export interface AgentConnections { + ethereum: AbstractProvider + gateway: IGateway +} + +export type AgentCreation = { + input: { + registry: AssetRegistry + sourceAccount: string + agentId: string + } + computed: { + gatewayAddress: string + } + tx: ContractTransaction +} + +export type AgentCreationValidationResult = { + logs: ValidationLog[] + success: boolean + data: { + etherBalance: bigint + feeInfo?: FeeInfo + agentAlreadyExists: boolean + agentAddress?: string + } + creation: AgentCreation +} + +export interface AgentCreationInterface { + createAgentCreation( + context: + | Context + | { + ethereum: AbstractProvider + }, + registry: AssetRegistry, + sourceAccount: string, + agentId: string, + ): Promise + + validateAgentCreation( + context: Context | AgentConnections, + creation: AgentCreation, + ): Promise +} diff --git a/web/packages/api/src/registration/agent/createAgent.ts b/web/packages/api/src/registration/agent/createAgent.ts new file mode 100644 index 000000000..618794721 --- /dev/null +++ b/web/packages/api/src/registration/agent/createAgent.ts @@ -0,0 +1,123 @@ +import { AssetRegistry } from "@snowbridge/base-types" +import { + AgentConnections, + AgentCreationInterface, + AgentCreation, + AgentCreationValidationResult, +} from "./agentInterface" +import { IGatewayV2__factory as IGateway__factory } from "@snowbridge/contract-types" +import { Context } from "../../index" +import { ValidationKind } from "../../toPolkadotSnowbridgeV2" +import { ValidationLog, ValidationReason } from "../../toPolkadot_v2" +import { AbstractProvider, Contract } from "ethers" + +export class CreateAgent implements AgentCreationInterface { + async createAgentCreation( + context: + | Context + | { + ethereum: AbstractProvider + }, + registry: AssetRegistry, + sourceAccount: string, + agentId: string, + ): Promise { + const ifce = IGateway__factory.createInterface() + const con = new Contract(registry.gatewayAddress, ifce) + + const tx = await con.getFunction("v2_createAgent").populateTransaction(agentId, { + from: sourceAccount, + }) + + return { + input: { + registry, + sourceAccount, + agentId, + }, + computed: { + gatewayAddress: registry.gatewayAddress, + }, + tx, + } + } + + async validateAgentCreation( + context: Context | AgentConnections, + creation: AgentCreation, + ): Promise { + const { tx } = creation + const { sourceAccount, agentId } = creation.input + const { ethereum, gateway } = + context instanceof Context + ? { + ethereum: context.ethereum(), + gateway: context.gatewayV2(), + } + : context + + const logs: ValidationLog[] = [] + + // Check if agent already exists + let agentAlreadyExists = false + let existingAgent: string | undefined + try { + existingAgent = await gateway.agentOf(agentId) + agentAlreadyExists = existingAgent !== "0x0000000000000000000000000000000000000000" + if (agentAlreadyExists) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.MinimumAmountValidation, + message: `Agent with ID ${agentId} already exists at address ${existingAgent}.`, + }) + } + } catch { + agentAlreadyExists = false + } + + const etherBalance = await ethereum.getBalance(sourceAccount) + + let feeInfo + if (logs.length === 0 || !agentAlreadyExists) { + const [estimatedGas, feeData] = await Promise.all([ + ethereum.estimateGas(tx), + ethereum.getFeeData(), + ]) + const executionFee = (feeData.gasPrice ?? 0n) * estimatedGas + if (executionFee === 0n) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.FeeEstimationError, + message: "Could not fetch fee details.", + }) + } + if (etherBalance < executionFee) { + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.InsufficientEther, + message: "Insufficient ether to submit transaction.", + }) + } + feeInfo = { + estimatedGas, + feeData, + executionFee, + totalTxCost: executionFee, + } + } + + const success = logs.find((l) => l.kind === ValidationKind.Error) === undefined + + return { + logs, + success, + data: { + etherBalance, + feeInfo, + agentAlreadyExists, + agentAddress: agentAlreadyExists ? existingAgent : undefined, + }, + creation, + } + } +} diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index 12b6fa489..14213a0f5 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -33,6 +33,8 @@ import { paraImplementation } from "./parachains" import { Context } from "./index" import { ETHER_TOKEN_ADDRESS, getAssetHubConversionPalletSwap } from "./assets_v2" import { getOperatingStatus } from "./status" +import { CreateAgent } from "./registration/agent/createAgent" +import { Wallet, TransactionReceipt } from "ethers" export { ValidationKind, signAndSend } from "./toEthereum_v2" @@ -830,3 +832,26 @@ export const validateTransferFromParachain = async ( transfer, } } + +// Agent creation exports +export type { + AgentCreation, + AgentCreationValidationResult, + AgentCreationInterface, +} from "./registration/agent/agentInterface" + +export function createAgentCreationImplementation() { + return new CreateAgent() +} + +export async function sendAgentCreation( + creation: any, + wallet: Wallet, +): Promise { + const response = await wallet.sendTransaction(creation.tx) + const receipt = await response.wait(1) + if (!receipt) { + throw Error(`Transaction ${response.hash} not included.`) + } + return receipt +} diff --git a/web/packages/operations/package.json b/web/packages/operations/package.json index 23fc2d284..39defcd44 100644 --- a/web/packages/operations/package.json +++ b/web/packages/operations/package.json @@ -40,6 +40,7 @@ "transferXQCYToEthereum": "npx ts-node src/transfer_from_p2e.ts 2313 xrqcy 1000000", "transferXQCYFromEthereumToAH": "npx ts-node src/transfer_from_e2p.ts 1000 xrqcy 100000", "registerWeth": "npx ts-node src/register_erc20.ts 0xb8ea8cb425d85536b158d661da1ef0895bb92f1d", + "createAgent": "npx ts-node src/create_agent.ts", "transferWethToAH": "npx ts-node src/transfer_from_e2p.ts 1000 WETH 200000000000000", "transferWndToEthereumV2": "npx ts-node src/transfer_from_p2e_v2.ts 1000 WND 2000000000", "transferEtherFromAHV2": "npx ts-node src/transfer_from_p2e_v2.ts 1000 Eth 100000000000000", diff --git a/web/packages/operations/src/create_agent.ts b/web/packages/operations/src/create_agent.ts new file mode 100644 index 000000000..6585a5928 --- /dev/null +++ b/web/packages/operations/src/create_agent.ts @@ -0,0 +1,109 @@ +import "dotenv/config" +import { Context, toEthereumSnowbridgeV2, contextConfigFor } from "@snowbridge/api" +import { cryptoWaitReady } from "@polkadot/util-crypto" +import { Wallet } from "ethers" +import { assetRegistryFor } from "@snowbridge/registry" + +// TODO add the ability to specify a location to create a agent from, using the EthereumSystemV2::agent_id API, +// once https://github.com/polkadot-fellows/runtimes/pull/978 has been released on-chain. +export const createAgent = async (agentId: string) => { + await cryptoWaitReady() + + let env = "local_e2e" + if (process.env.NODE_ENV !== undefined) { + env = process.env.NODE_ENV + } + console.log(`Using environment '${env}'`) + + const context = new Context(contextConfigFor(env)) + + const ETHEREUM_ACCOUNT = new Wallet( + process.env.ETHEREUM_KEY ?? + "0x5e002a1af63fd31f1c25258f3082dc889762664cb8f218d86da85dff8b07b342", + context.ethereum(), + ) + const ETHEREUM_ACCOUNT_PUBLIC = await ETHEREUM_ACCOUNT.getAddress() + + console.log("eth", ETHEREUM_ACCOUNT_PUBLIC) + + const registry = assetRegistryFor(env) + + console.log("Creating agent with ID:", agentId) + + console.log("Agent Creation on Snowbridge V2") + { + // Step 0. Create an agent creation implementation + const agentCreationImpl = toEthereumSnowbridgeV2.createAgentCreationImplementation() + + // Step 1. Create an agent creation tx + const creation = await agentCreationImpl.createAgentCreation( + { + ethereum: context.ethereum(), + }, + registry, + ETHEREUM_ACCOUNT_PUBLIC, + agentId, + ) + + // Step 2. Validate the transaction. + const validation = await agentCreationImpl.validateAgentCreation( + { + ethereum: context.ethereum(), + gateway: context.gatewayV2(), + }, + creation, + ) + + // Check validation logs for errors + const errorLogs = validation.logs.filter( + (l) => l.kind === toEthereumSnowbridgeV2.ValidationKind.Error, + ) + if (errorLogs.length > 0) { + console.error("Validation failed with errors:") + errorLogs.forEach((log) => { + console.error(` [ERROR] ${log.message}`) + }) + throw Error(`Validation has ${errorLogs.length} error(s).`) + } + + console.log("validation result", validation) + + if (process.env["DRY_RUN"] != "true") { + // Step 3. Submit the transaction + const response = await ETHEREUM_ACCOUNT.sendTransaction(creation.tx) + const receipt = await response.wait(1) + if (!receipt) { + throw Error(`Transaction ${response.hash} not included.`) + } + + if (receipt.status !== 1) { + throw Error(`Transaction ${receipt.hash} failed with status ${receipt.status}`) + } + + console.log(`Agent created successfully! + tx hash: ${receipt.hash} + agent address: ${await context.gatewayV2().agentOf(agentId)}`) + } else { + console.log(`DRY_RUN mode: Agent would be created with ID ${agentId}`) + } + } + await context.destroyContext() +} + +// Only run if this is the main module (not imported) +if (require.main === module) { + if (process.argv.length != 3) { + console.error("Expected arguments: `agentId`") + console.error( + "Example: npm run createAgent 0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314", + ) + process.exit(1) + } + + createAgent(process.argv[2]) + .then(() => process.exit(0)) + .catch((error) => { + console.error("Error:", error) + process.exit(1) + }) +}