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)

onRequest: async function (request) {
        let { method, data: { params } } = request;
        switch (method) {

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.

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
    }
}

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.

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() 
    }
}

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).

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()
    };
}

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)

signParams: function (request, result) {
        switch (request.method) {

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).

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 }
    ]
}

2.2 get-transaction

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

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 },
    ]
}

2.3 contract-call

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

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
    ];
}

Special Notes

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

const now = this.getTimestamp();

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:

throw { message: `invalid method ${method}` }

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