Skip to content
Open
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
14 changes: 14 additions & 0 deletions web/packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
SnowbridgeL1Adaptor__factory,
SnowbridgeL2Adaptor,
SnowbridgeL2Adaptor__factory,
Gateway,
Gateway__factory,
} from "@snowbridge/contract-types"
import { Environment } from "@snowbridge/base-types"

Expand Down Expand Up @@ -53,6 +55,7 @@ export class Context {
#ethChains: EthereumChains
#gateway?: IGatewayV1
#gatewayV2?: IGatewayV2
#gatewayProxy?: Gateway
#beefyClient?: BeefyClient
#l1Adapter?: SnowbridgeL1Adaptor
#l1SwapQuoter?: ISwapQuoter
Expand Down Expand Up @@ -280,6 +283,17 @@ export class Context {
return this.#gatewayV2
}

gatewayProxy(): Gateway {
if (this.#gatewayProxy) {
return this.#gatewayProxy
}
this.#gatewayProxy = Gateway__factory.connect(
this.environment.gatewayContract,
this.ethereum(),
)
return this.#gatewayProxy
}

beefyClient(): BeefyClient {
if (this.#beefyClient) {
return this.#beefyClient
Expand Down
139 changes: 137 additions & 2 deletions web/packages/api/src/toEthereumSnowbridgeV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,18 @@ import { paraImplementation } from "./parachains"
import { Context } from "./index"
import { ETHER_TOKEN_ADDRESS, findL2TokenAddress } from "./assets_v2"
import { getOperatingStatus } from "./status"
import { AbstractProvider, ethers, Wallet, TransactionReceipt } from "ethers"
import {
AbstractProvider,
ethers,
Wallet,
TransactionReceipt,
AbiCoder,
ContractTransaction,
} from "ethers"
import { CreateAgent } from "./registration/agent/createAgent"
import { estimateFees } from "./across/api"
import { AgentCreation } from "./registration/agent/agentInterface"
import { CommandStruct } from "@snowbridge/contract-types/dist/Gateway"

export { ValidationKind, signAndSend } from "./toEthereum_v2"

Expand Down Expand Up @@ -659,6 +667,8 @@ export const validateTransferFromAssetHub = async (
let sourceDryRunError
let assetHubDryRunError
let bridgeHubDryRunError
let ethereumDryRunError
let estimatedDryRunGas: bigint | undefined
// do the dry run, get the forwarded xcm and dry run that
const dryRunResultAssetHub = await dryRunOnSourceParachain(
sourceParachain,
Expand All @@ -673,7 +683,64 @@ export const validateTransferFromAssetHub = async (
registry.assetHubParaId,
dryRunResultAssetHub.bridgeHubForwarded[1][0],
)
if (!dryRunResultBridgeHub.success) {
if (dryRunResultBridgeHub.success) {
try {
const ethereumTx = await buildEthereumDryRunCall(
context,
registry.assetHubParaId,
sourceAccountHex,
transfer,
)
estimatedDryRunGas = await ethereum.estimateGas(ethereumTx)
const forkedProvider = new ethers.JsonRpcProvider(
process.env.FORKED_PROVIDER_URL ||
process.env.NEXT_PUBLIC_FORKED_PROVIDER_URL ||
"https://virtual.mainnet.eu.rpc.tenderly.co/1d4ab8c5-01fe-45a7-8583-8b4925e5a435",
)
const txHash = await forkedProvider.send("eth_sendTransaction", [
{
from: ethereumTx.from,
to: ethereumTx.to,
data: ethereumTx.data,
value: ethereumTx.value,
gas: "0x" + (estimatedDryRunGas! * 2n).toString(16), // multiply by 2 to be safe
},
])

console.log("Tx hash:", txHash)

const receipt = await forkedProvider.waitForTransaction(txHash)
console.log("Logs:", receipt?.logs)
const parsedLogs = receipt?.logs
.map((log) => {
try {
return context.gatewayProxy().interface.parseLog(log)
} catch (e) {
return null
}
})
.filter((log) => log !== null)
const errorLogs = parsedLogs?.filter((log) => {
return log.name === "CommandFailed"
})
if (errorLogs && errorLogs.length > 0) {
ethereumDryRunError =
"Dry run failed on Ethereum at command index: " + errorLogs[0].args.index
logs.push({
kind: ValidationKind.Error,
reason: ValidationReason.DryRunFailed,
message: ethereumDryRunError,
})
}
} catch (e) {
ethereumDryRunError = "Could not estimate gas on Ethereum." + (e as Error).message
logs.push({
kind: ValidationKind.Error,
reason: ValidationReason.FeeEstimationError,
message: ethereumDryRunError,
})
}
} else {
logs.push({
kind: ValidationKind.Error,
reason: ValidationReason.DryRunFailed,
Expand Down Expand Up @@ -733,6 +800,7 @@ export const validateTransferFromAssetHub = async (
sourceDryRunError,
assetHubDryRunError,
bridgeHubDryRunError,
ethereumDryRunError,
},
transfer,
}
Expand Down Expand Up @@ -1108,3 +1176,70 @@ export async function sourceAgentAddress(
let agentAddress = await gateway.agentOf(agentID)
return agentAddress
}

export async function buildEthereumDryRunCall(
context: Context,
parachainId: number,
sourceAccountHex: string,
transfer: Transfer,
): Promise<ContractTransaction> {
let commands: CommandStruct[] = []
const agentID = await sourceAgentId(context, parachainId, sourceAccountHex)
if (transfer.computed.sourceAssetMetadata.foreignId) {
// PNA
const mintForeignParams = AbiCoder.defaultAbiCoder().encode(
["bytes32", "address", "uint128"],
[
transfer.computed.sourceAssetMetadata.foreignId,
transfer.input.beneficiaryAccount,
transfer.input.amount,
],
)
const mintCommand: CommandStruct = {
kind: 4,
gas: transfer.computed.tokenErcMetadata.deliveryGas || 200_000n,
payload: mintForeignParams,
}
commands.push(mintCommand)
} else {
// ENA
const unlockNativeParams = AbiCoder.defaultAbiCoder().encode(
["bytes32", "address", "address", "uint128"],
[
agentID,
transfer.input.tokenAddress,
transfer.input.beneficiaryAccount,
transfer.input.amount,
],
)
const unlockCommand: CommandStruct = {
kind: 2,
gas: transfer.computed.tokenErcMetadata.deliveryGas || 200_000n,
payload: unlockNativeParams,
}
commands.push(unlockCommand)
}

if (transfer.input.contractCall) {
let callInfo = transfer.input.contractCall
// 2. Transact
const transactParams = AbiCoder.defaultAbiCoder().encode(
["address", "bytes", "uint256"],
[callInfo.target, callInfo.calldata, callInfo.value || 0n],
)
const transactCommand: CommandStruct = {
kind: 5,
gas: callInfo.gas,
payload: transactParams,
}
commands.push(transactCommand)
}
let ethereumTx: ContractTransaction = await context
.gatewayProxy()
.getFunction("v2_dispatch")
// nonce is irrelevant in the dry run, can be set to 0
.populateTransaction(commands, agentID, 0n, {
from: context.environment.gatewayContract,
})
return ethereumTx
}
1 change: 1 addition & 0 deletions web/packages/api/src/toEthereum_v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ export type ValidationResult = {
sourceDryRunError: any
assetHubDryRunError: any
bridgeHubDryRunError?: any
ethereumDryRunError?: any
}
transfer: Transfer
}
Expand Down
2 changes: 1 addition & 1 deletion web/packages/contract-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "rm -rf src && rm -rf dist && cd ../../../contracts && forge build && cd ../web/packages/contract-types && pnpm typechain && tsc --build --force",
"typechain": "typechain --target ethers-v6 '../../../contracts/out/?(IERC20.sol|IERC20Metadata.sol|IGateway.sol|BeefyClient.sol|WETH9.sol|SnowbridgeL1Adaptor.sol|SnowbridgeL2Adaptor.sol|ISwapQuoter.sol|ISwapRouter.sol|ISwapLegacyRouter.sol)/!(*.abi).json' --out-dir src"
"typechain": "typechain --target ethers-v6 '../../../contracts/out/?(IERC20.sol|IERC20Metadata.sol|IGateway.sol|Gateway.sol|BeefyClient.sol|WETH9.sol|SnowbridgeL1Adaptor.sol|SnowbridgeL2Adaptor.sol|ISwapQuoter.sol|ISwapRouter.sol|ISwapLegacyRouter.sol)/!(*.abi).json' --out-dir src"
},
"devDependencies": {
"@typechain/ethers-v6": "0.5.1",
Expand Down