Sample MuonApp 2: EVM Data Verifier

This Muon app is called evm_data_verifier, and its main job is to fetch and verify on-chain EVM data from any Ethereum-compatible network. It can return information about:

  1. A specific block

  2. A specific transaction

  3. The result of a smart contract call

It uses Muon’s eth* helper functions (e.g., ethGetBlock, ethGetTransaction, ethCall) to talk to EVM-compatible networks like Ethereum, Avalanche, BNB Chain, etc. Thus it is ideal for use-cases requiring transparent validation of on-chain data, like cross-chain messaging, oracle feeds, and smart contract state verification. (See Code Breakdown section for a detailed explanation of the code.)

const { ethGetBlock, ethGetTransaction, ethGetTransactionReceipt, ethCall} = MuonAppUtils

const EVMDataVerifierApp = {
    APP_NAME: 'evm_data_verifier',
    useFrost: true,

    getTimestamp: () => Math.floor(Date.now() / 1000),

    onRequest: async function (request) {
        let { method, data: { params } } = request;
        switch (method) {
            case 'get-block': {
                let {
                    network,
                    block
                } = params

                let {
                    number,
                    hash,
                    timestamp,
                    transactions
                } = await ethGetBlock(network, block);

                return {
                    number: number.toString(),
                    hash: hash.toString(),
                    timestamp: timestamp.toString(),
                    transactions
                }
            }
            case 'get-transaction': {
                let {
                    network,
                    txHash
                } = params

                let {
                    hash,
                    nonce,
                    blockHash,
                    blockNumber,
                    transactionIndex,
                    from,
                    to,
                    value,
                    gas,
                    gasPrice
                } = await ethGetTransaction(txHash, network);

                let {
                    timestamp
                } = await ethGetBlock(network, blockNumber);

                return {
                    timestamp: timestamp.toString(),
                    hash: hash.toString(),
                    nonce: nonce.toString(),
                    blockHash: blockHash.toString(),
                    blockNumber: blockNumber.toString(),
                    transactionIndex: transactionIndex.toString(),
                    from: from.toString(),
                    to: to?.toString() || "",
                    value: value.toString(),
                    gas: gas.toString(),
                    gasPrice: gasPrice.toString() 
                }
            }
            case 'contract-call': {
                let {
                    contractAddress, 
                    method, 
                    args,
                    abi, 
                    network
                } = params

                args = args.split(",")

                let functionResult = await ethCall(contractAddress, method, args, JSON.parse(abi), network);

                const now = this.getTimestamp();

                if (typeof functionResult == "object") {
                    let result = []
                    for (let index = 0; index < functionResult.__length__; index++) {
                        result.push(functionResult[index].toString());
                    }
                    return {
                        timestamp: now.toString(),
                        functionResult: result
                    };
                }

                return {
                    timestamp: now.toString(),
                    functionResult: functionResult.toString()
                };
            }
            default:
                throw { message: `invalid method ${method}` }
        }
    },

    signParams: function (request, result) {
        switch (request.method) {
            case 'get-block': {
                let {
                    number,
                    hash,
                    timestamp,
                    transactions
                } = result

                return [
                    { type: 'uint256', value: number },
                    { type: 'string', value: hash },
                    { type: 'uint256', value: timestamp },
                    { type: 'string[]', value: transactions }
                ]
            }
            case 'get-transaction': {
                let {
                    timestamp,
                    hash,
                    nonce,
                    blockHash,
                    blockNumber,
                    transactionIndex,
                    from,
                    to,
                    value,
                    gas,
                    gasPrice
                } = result


                return [
                    { type: "string", value: timestamp },
                    { type: 'string', value: hash },
                    { type: 'uint256', value: nonce },
                    { type: 'string', value: blockHash },
                    { type: 'uint256', value: blockNumber },
                    { type: 'uint256', value: transactionIndex },
                    { type: 'string', value: from },
                    { type: 'string', value: to },
                    { type: 'uint256', value: value },
                    { type: 'uint256', value: gasPrice },
                    { type: 'uint256', value: gas },
                ]
            }
            case 'contract-call': {
                let { data: { params: { abi }}} = request
                abi = JSON.parse(abi)
                const {outputs} = abi[0]

                let {
                    timestamp,
                    functionResult
                } = result;

                const res = outputs.reduce((res, currentItem, i) => {
                    res.push({type: currentItem['type'], value: functionResult[i]});
                    return res;
                }, []);
                
                return [
                    { type: "string", value: timestamp },
                    ...res
                ];
            }
            default:
                throw { message: `Unknown method: ${request.method}` }
        }
    }
}

module.exports = EVMDataVerifierApp

Code Breakdown

At the beginning, useFrost: true enables Muon’s Frost-based Threshold Signature Scheme. Then there are two main functions: onRequest and signParams .

1- onRequest(request)

This is the function that runs when a Muon node receives a request. Based on the method, it performs different operations:

1.1 get-block

It fetches data about a specific block.

Parameters expected:

  • network: Which EVM chain (e.g., 'ethereum', 'bsc', etc.)

  • block: Block number or hash

Returns:

  • Block number, hash, timestamp, and list of transactions

1.2 get-transaction

It gets detailed data about a specific transaction.

Parameters expected:

  • network: EVM chain

  • txHash: Transaction hash

Returns:

  • Various fields from the transaction: hash, nonce, from, to, gas, etc.

  • Plus the block timestamp for that transaction

1.3 contract-call

This makes a read-only call to a smart contract method (like checking a balance or a state variable).

Parameters expected:

  • contractAddress

  • method: Name of the function to call

  • args: Comma-separated arguments (converted to array)

  • abi: JSON ABI for the contract

  • network

Returns:

  • timestamp of the request

  • The function’s result (either a single value or an array of values)

2. signParams(request, result)

This function defines what data should be signed by Muon nodes so it can be verified across the network. The signature structure depends on the method:

2.1 get-block

This case signs number, hash, timestamp, and the transactions (as an array of strings).

2.2 get-transaction

The get-transaction case signs everything in the returned transaction data, including from, to, value, gas, etc.

2.3 contract-call

It signs the timestamp plus all values returned by the function, matched to their type from the ABI.

Special Notes

NB1: Each method response includes a timestamp field, ensuring the result is tied to the specific moment it was fetched.

This way:

  • Consumers of this data know when it was fetched.

  • It adds another layer of integrity, making sure the data wasn’t fetched long ago and reused.

  • In systems that rely on fresh or time-bound data (like bridges or DeFi protocols), this helps enforce timeliness.

So, timestamp is like a freshness guarantee for the data being signed.

NB2: The app also has strict method validation through throw statement and will return a clear error ("invalid method") if an unsupported call is made. It prevents the app from continuing with undefined behavior and provides a clear and informative error if the developer or client made a typo or used an unsupported method.

throw appears at the end of the onRequest and signParams functions:

NB3: Dynamic ABI parsing is achieved through the contract-call method. It uses the ABI provided in the request to figure out how to structure the returned values and their types.

Last updated