Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions web/packages/api/src/registration/agent/agentInterface.ts
Original file line number Diff line number Diff line change
@@ -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<AgentCreation>

validateAgentCreation(
context: Context | AgentConnections,
creation: AgentCreation,
): Promise<AgentCreationValidationResult>
}
123 changes: 123 additions & 0 deletions web/packages/api/src/registration/agent/createAgent.ts
Original file line number Diff line number Diff line change
@@ -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<AgentCreation> {
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<AgentCreationValidationResult> {
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,
}
}
}
25 changes: 25 additions & 0 deletions web/packages/api/src/toEthereumSnowbridgeV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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<TransactionReceipt> {
const response = await wallet.sendTransaction(creation.tx)
const receipt = await response.wait(1)
if (!receipt) {
throw Error(`Transaction ${response.hash} not included.`)
}
return receipt
}
1 change: 1 addition & 0 deletions web/packages/operations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
109 changes: 109 additions & 0 deletions web/packages/operations/src/create_agent.ts
Original file line number Diff line number Diff line change
@@ -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)
})
}