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:
A specific block
A specific transaction
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(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
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 oftransactions
1.2 get-transaction
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 chaintxHash
: 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
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 callargs
: Comma-separated arguments (converted to array)abi
: JSON ABI for the contractnetwork
Returns:
timestamp
of the requestThe function’s result (either a single value or an array of values)
2. signParams(request, result)
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
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
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
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