Using Typescript and Viem
EOA-initiated Facet Transactions
This Github project has the below code and instructions for running it:
The basic flow:
Construct a Facet Transaction
Send an Ethereum Transaction with the correct fields
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