Copy import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://rpc.obsidianchain.net");
const wallet = new ethers.Wallet(privateKey, provider);
// Create message
const chainId = 421;
const payload = "0x" + Buffer.from("Hello Obsidian!").toString("hex");
const nonce = BigInt(Date.now());
const targetBlock = 0;
function toUint64BE(n) {
// 8-byte big-endian hex string (no 0x prefix)
return n.toString(16).padStart(16, "0");
}
function computeVdfChallenge({ chainId, sender, payload, nonce }) {
// challenge = keccak256(rlp([chainId, sender, payload, nonce]))
const rlp = ethers.encodeRlp([ethers.toBeHex(chainId), sender, payload, ethers.toBeHex(nonce)]);
return ethers.keccak256(rlp);
}
function computeVdfCheckpoints({ challenge, iterations, checkpointInterval }) {
// h_i = keccak256(h_{i-1} || i)
// Store a checkpoint every `checkpointInterval` iterations (and always store the final).
let hash = challenge;
const checkpoints = [];
for (let i = 0; i < iterations; i++) {
const iterHex = toUint64BE(i);
hash = ethers.keccak256(hash + iterHex);
if ((i + 1) % checkpointInterval === 0 || i === iterations - 1) {
checkpoints.push(hash);
}
}
return checkpoints;
}
async function signMessageForBid(bid) {
// What the client signs:
// keccak256(rlp([chainId, targetBlock, nonce, sender, payload, bidBytes]))
const bidHex = bid > 0n ? ethers.toBeHex(bid) : "0x"; // empty bytes when bid==0
const signatureHash = ethers.keccak256(
ethers.encodeRlp([
ethers.toBeHex(chainId),
ethers.toBeHex(targetBlock),
ethers.toBeHex(nonce),
wallet.address,
payload,
bidHex,
]),
);
return wallet.signMessage(ethers.getBytes(signatureHash));
}
// -----------------------------
// Example 1: Priority message (PM) - NO VDF
// -----------------------------
// Set a non-zero bid (must be >= the network minimum).
const pmBid = 1_000_000_000_000_000n; // 0.001 OBS in wei (example)
const pmSignature = await signMessageForBid(pmBid);
const pmResult = await provider.send("eth_sendMessageBlob", [
{
from: wallet.address,
data: payload,
signature: pmSignature,
targetBlock: ethers.toBeHex(targetBlock),
nonce: ethers.toBeHex(nonce),
chainId: ethers.toBeHex(chainId),
bid: ethers.toBeHex(pmBid),
},
]);
console.log("PM message hash:", pmResult);
// -----------------------------
// Example 2: Free message (SM) - VDF required (if enabled)
// -----------------------------
const smBid = 0n;
const smSignature = await signMessageForBid(smBid);
// Ask the node how much work is required for this payload size.
const payloadSizeBytes = ethers.getBytes(payload).length;
const work = await provider.send("eth_getMessageWork", [
{
payloadSize: ethers.toBeHex(payloadSizeBytes),
},
]);
if (!work?.enabled) {
// If the network has VDF disabled, you can submit SM messages without VDF fields.
// If enabled, compute checkpoints and include them as shown below.
}
const requiredIterations = parseInt(work.requiredIterations, 16);
const checkpointInterval = parseInt(work.checkpointInterval, 16);
const algVersion = parseInt(work.algVersion, 16);
if (!requiredIterations || !checkpointInterval) {
throw new Error("Invalid VDF parameters from eth_getMessageWork");
}
const challenge = computeVdfChallenge({
chainId,
sender: wallet.address,
payload,
nonce,
});
const vdfCheckpoints = computeVdfCheckpoints({
challenge,
iterations: requiredIterations,
checkpointInterval,
});
const vdfCheckpointInterval = "0x" + checkpointInterval.toString(16);
const vdfIterations = "0x" + requiredIterations.toString(16);
const vdfAlgVersion = "0x" + algVersion.toString(16);
const smResult = await provider.send("eth_sendMessageBlob", [
{
from: wallet.address,
data: payload,
signature: smSignature,
targetBlock: ethers.toBeHex(targetBlock),
nonce: ethers.toBeHex(nonce),
chainId: ethers.toBeHex(chainId),
bid: ethers.toBeHex(smBid),
// VDF fields (required for SM on Obsidian)
vdfCheckpoints,
vdfCheckpointInterval,
vdfIterations,
vdfAlgVersion,
},
]);
console.log("SM message hash:", smResult);