Using Typescript and Viem

EOA-initiated Facet Transactions

This Github project has the below code and instructions for running it:

The basic flow:

  1. Construct a Facet Transaction

  2. Send an Ethereum Transaction with the correct fields

  3. Find the receipt for the Facet Transaction created by the Ethereum transaction.

First, a function to construct the Facet Transaction using RLP encoding:

function buildFacetTransaction({
    network,
    to = '',
    value,
    maxFeePerGas,
    gasLimit,
    data
}: {
    network: number,
    to?: `0x${string}` | '',
    value: bigint | number,
    maxFeePerGas: bigint | number,
    gasLimit: bigint | number,
    data: `0x${string}`
}) {
    let chainId: number;

    if (network === 1) {
        chainId = 0xface7;
    } else if (network === 11155111) {
        chainId = 0xface7a;
    } else {
        throw new Error("Unsupported chainId");
    }

    const facetTxType = toBytes(70);

    const rlpEncoded = toRlp([
        toHex(chainId),
        (to == '' ? toHex(to) : to),
        toHex(value),
        toHex(maxFeePerGas),
        toHex(gasLimit),
        data,
    ], 'bytes');
    
    return new Uint8Array([...facetTxType, ...rlpEncoded]);
}

Now a function to send the transaction:

import { createPublicClient, createWalletClient, http, toRlp, toHex, toBytes } from 'viem';
import { sepolia } from 'viem/chains';
import dotenv from 'dotenv';
import { privateKeyToAccount } from 'viem/accounts';

dotenv.config();

const facetInboxAddress = "0x00000000000000000000000000000000000FacE7" as `0x${string}`;

export async function sendFacetTransaction({
    to = '',
    value = 0,
    maxFeePerGas,
    gasLimit,
    data
}: {
    to?: `0x${string}` | '',
    value?: bigint | number,
    maxFeePerGas: bigint | number,
    gasLimit: bigint | number,
    data: `0x${string}`
}) {
    const rpcUrl = process.env.SEPOLIA_RPC_URL!;
    const privateKey = process.env.PRIVATE_KEY!;

    const client = createPublicClient({
        chain: sepolia,
        transport: http(rpcUrl),
    });
    
    const account = privateKeyToAccount(privateKey as `0x${string}`);
    
    const walletClient = createWalletClient({
        account: account,
        chain: sepolia,
        transport: http(rpcUrl),
    });

    const [deployerAddress] = await walletClient.getAddresses();
    console.log("Sending transaction with the account:", deployerAddress);
    
    const network = await client.getChainId();

    const out = buildFacetTransaction({
        network,
        to,
        value,
        maxFeePerGas,
        gasLimit,
        data
    });

    const tx = {
        from: deployerAddress as `0x${string}`,
        to: facetInboxAddress as `0x${string}`,
        data: toHex(out)
    };
    
    const txHash: `0x${string}` = await walletClient.sendTransaction(tx);
    console.log("Transaction hash:", txHash);

    await client.waitForTransactionReceipt({ hash: txHash });
    
    const ethTxApi = `https://testnet-alpha.facet.org/eth_transactions/${txHash}`;
    let ethTxData;
    let attempts = 0;
    const maxAttempts = 6;
    const baseDelay = 1000;

    while (attempts < maxAttempts) {
        const response = await fetch(ethTxApi);
        ethTxData = await response.json();

        if (ethTxData.error !== "Transaction not found") {
            break;
        }

        attempts++;
        if (attempts < maxAttempts) {
            const delay = baseDelay * Math.pow(2, attempts - 1);
            console.log(`Transaction not found. Retrying in ${delay / 1000} seconds...`);
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }

    if (ethTxData.error === "Transaction not found") {
        throw new Error("Failed to fetch transaction data after 5 attempts");
    } else {
      const facetTransaction: FacetTransaction = {
        ...ethTxData.result.facet_transactions[0],
        facet_transaction_receipt: ethTxData.result.facet_transactions[0].facet_transaction_receipt
      };
      return facetTransaction;
    }
}

Note that when we are retrieving the created Facet Transactions we must retry failed requests because it might take a few moments for the Facet Node to pick up and process our Ethereum transaction.

Finally, a full script to demonstrate the process end-to-end. This creates a contract, updates its storage, reads the new value, and finally queries the sender's balance.

import { encodeFunctionData, decodeFunctionResult, formatEther } from 'viem';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
import { sendFacetTransaction } from './sendFacetTransaction';

dotenv.config();

async function main() {
  const contractPath = path.resolve(__dirname, '../artifacts/contracts/SimpleStorage.sol/SimpleStorage.json');
  const contractArtifact = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
  const bytecode = contractArtifact.bytecode;
  const abi = contractArtifact.abi;
  
  const facetTransaction = await sendFacetTransaction({
    maxFeePerGas: 10,
    gasLimit: 500_000,
    data: bytecode as `0x${string}`
  })

  console.log("Facet Transaction:", facetTransaction);
  
  const contractAddress = facetTransaction.facet_transaction_receipt.contract_address;
  console.log("Contract Address:", contractAddress);

  const setValueData = encodeFunctionData({
    abi: abi,
    functionName: "setValue",
    args: [42]
  });
  
  const facetTransaction2 = await sendFacetTransaction({
    to: contractAddress as `0x${string}`,
    maxFeePerGas: 10,
    gasLimit: 500_000,
    data: setValueData as `0x${string}`
  })

  console.log("Facet Transaction 2:", facetTransaction2);
  
  const txHash = facetTransaction2.tx_hash
  console.log("Transaction hash:", txHash);
  
  const getValueData = encodeFunctionData({
    abi: abi,
    functionName: "getValue",
    args: [],
  });
  
  const rpcBaseUrl = 'https://testnet-alpha.facet.org/rpc'
  
  const ethCallPayload = {
    jsonrpc: "2.0",
    method: "eth_call",
    params: [
      {
        to: contractAddress as `0x${string}`,
        data: getValueData as `0x${string}`
      },
      "latest"
    ],
    id: 1
  };

  const response = await fetch(rpcBaseUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(ethCallPayload)
  });

  const result = await response.json();
  console.log("eth_call result:", result);

  const decodedResult = decodeFunctionResult({
    abi: abi,
    functionName: "getValue",
    data: result.result as `0x${string}`
  });

  console.log("Decoded result:", decodedResult);
  
  const balancePayload = {
    jsonrpc: "2.0",
    method: "eth_getBalance",
    params: [
      facetTransaction2.from_address as `0x${string}`,
      "latest"
    ],
    id: 2
  };

  const balanceResponse = await fetch(rpcBaseUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(balancePayload)
  });

  const balanceResult = await balanceResponse.json();
  const balance = BigInt(balanceResult.result);
  console.log("Deployer balance:", formatEther(balance));
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The output should look something like this:

Sending transaction with the account: 0x0Fa846DEc44dd47bc74978b58c1FfB59816180c9
Transaction hash: 0xce354c8b4393b958ff7f8d3cc1f054192dccb50ff5137fcde2478aab5e417e4e
Transaction not found. Retrying in 1 seconds...
Transaction not found. Retrying in 2 seconds...
Facet Transaction: {
  id: 33,
  eth_transaction_hash: '0xce354c8b4393b958ff7f8d3cc1f054192dccb50ff5137fcde2478aab5e417e4e',
  eth_call_index: 573,
  block_hash: '0xa9ea461d9a8323b2e2435d33d4ce44c211fb7dd7fb5e5a26312be1ec63f0b0f3',
  block_number: 5733,
  deposit_receipt_version: '1',
  from_address: '0x0fa846dec44dd47bc74978b58c1ffb59816180c9',
  gas: 500000,
  gas_limit: 500000,
  gas_price: null,
  tx_hash: '0xe63074c403ca17c8e28d470807634d66fa0b03a61c0a05c20b626f95cb3397d6',
  input: '0x6080604052348015600f57600080fd5b5060e68061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c8063209652551460375780635524107714604c575b600080fd5b60005460405190815260200160405180910390f35b605b60573660046098565b605d565b005b60008190556040518181527f93fe6d397c74fdf1402a8b72e47b68512f0510d7b98a4bc4cbdf6ac7108b3c599060200160405180910390a150565b60006020828403121560a957600080fd5b503591905056fea2646970667358221220ba2bb782290b89fd7adaca4f707490dd0042caf4acb2d35e7ca32f4e5f615a3364736f6c634300081a0033',
  source_hash: '0x0b363a56f8662dbdf9fbcdac985f45858d9ab9b89353bdeced25de4058834dbb',
  to_address: null,
  transaction_index: 0,
  tx_type: '0x46',
  mint: 953556779138656,
  value: 0,
  max_fee_per_gas: 10,
  created_at: '2024-08-07T15:01:14.606Z',
  updated_at: '2024-08-07T15:01:14.606Z',
  facet_transaction_receipt: {
    id: 33,
    transaction_hash: '0xe63074c403ca17c8e28d470807634d66fa0b03a61c0a05c20b626f95cb3397d6',
    block_hash: '0xa9ea461d9a8323b2e2435d33d4ce44c211fb7dd7fb5e5a26312be1ec63f0b0f3',
    block_number: 5733,
    contract_address: '0xc764027a466098269f8420c5197602e6202fe089',
    legacy_contract_address_map: {},
    cumulative_gas_used: 103097,
    deposit_nonce: '30',
    deposit_receipt_version: '70',
    effective_gas_price: 7,
    from_address: '0x0fa846dec44dd47bc74978b58c1ffb59816180c9',
    gas_used: 103097,
    logs: [],
    logs_bloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    status: 1,
    to_address: null,
    transaction_index: 0,
    tx_type: '0x46',
    created_at: '2024-08-07T15:01:14.607Z',
    updated_at: '2024-08-07T15:01:14.607Z'
  }
}
Contract Address: 0xc764027a466098269f8420c5197602e6202fe089
Sending transaction with the account: 0x0Fa846DEc44dd47bc74978b58c1FfB59816180c9
Transaction hash: 0xf3cc1ec021e3b197346839981f5dde2d42994d25b611c1b027b39f609c966c4c
Transaction not found. Retrying in 1 seconds...
Facet Transaction 2: {
  id: 34,
  eth_transaction_hash: '0xf3cc1ec021e3b197346839981f5dde2d42994d25b611c1b027b39f609c966c4c',
  eth_call_index: 469,
  block_hash: '0x00e2297ab60dd3db8ac26790060fd02f53d0afd61acd8fd63a7d2552ac3af4bc',
  block_number: 5734,
  deposit_receipt_version: '1',
  from_address: '0x0fa846dec44dd47bc74978b58c1ffb59816180c9',
  gas: 500000,
  gas_limit: 500000,
  gas_price: null,
  tx_hash: '0xf51cbc932b04c22ccc183442ad4e5724ea75e94c3f7c96bdc2411acd5cc8e94e',
  input: '0x55241077000000000000000000000000000000000000000000000000000000000000002a',
  source_hash: '0x932846a4d4309eb211bf5e421299df9b04d88a029bd5ba3e3ab4ad0709a0c08f',
  to_address: '0xc764027a466098269f8420c5197602e6202fe089',
  transaction_index: 0,
  tx_type: '0x46',
  mint: 811224011260672,
  value: 0,
  max_fee_per_gas: 10,
  created_at: '2024-08-07T15:01:26.589Z',
  updated_at: '2024-08-07T15:01:26.589Z',
  facet_transaction_receipt: {
    id: 34,
    transaction_hash: '0xf51cbc932b04c22ccc183442ad4e5724ea75e94c3f7c96bdc2411acd5cc8e94e',
    block_hash: '0x00e2297ab60dd3db8ac26790060fd02f53d0afd61acd8fd63a7d2552ac3af4bc',
    block_number: 5734,
    contract_address: null,
    legacy_contract_address_map: {},
    cumulative_gas_used: 44593,
    deposit_nonce: '31',
    deposit_receipt_version: '70',
    effective_gas_price: 7,
    from_address: '0x0fa846dec44dd47bc74978b58c1ffb59816180c9',
    gas_used: 44593,
    logs: [ [Object] ],
    logs_bloom: '0x40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000001000000000000000000000',
    status: 1,
    to_address: '0xc764027a466098269f8420c5197602e6202fe089',
    transaction_index: 0,
    tx_type: '0x46',
    created_at: '2024-08-07T15:01:26.589Z',
    updated_at: '2024-08-07T15:01:26.589Z'
  }
}
Transaction hash: 0xf51cbc932b04c22ccc183442ad4e5724ea75e94c3f7c96bdc2411acd5cc8e94e
eth_call result: {
  result: '0x000000000000000000000000000000000000000000000000000000000000002a'
}
Decoded result: 42n
Deployer balance: 0.006185988901650127

Last updated