xcm-sentinel-sdk
v0.1.0
Published
Production SDK for XCM Sentinel integration
Readme
@xcm-sentinel/sdk
TypeScript SDK for integrating with XCM Sentinel on Polkadot Hub. Enforces declarative policy (STRICT / SAFE / PERMISSIVE) before any cross-chain intent reaches the XCM precompile.
Network: Polkadot Hub Testnet (chain ID 420420417)
Install
# npm
npm i @xcm-sentinel/sdk
# bun
bun add @xcm-sentinel/sdkDeployed contract addresses (Polkadot Hub Testnet)
| Contract | Address |
| --------------------- | -------------------------------------------- |
| XcmSentinelFirewall | 0x7daf425b9428ee97c1e52b094e2db42637265d73 |
| PolicyRegistry | 0x978524ae39575aaf308330466d29419a2affeef6 |
| ReplayProofRegistry | 0x6d8807ae9e75ca4307df6e9d0b40bacadb5f7fca |
Quickstart
import { createSentinelClient } from "@xcm-sentinel/sdk";
const client = createSentinelClient({
chainId: 420420417,
rpcUrl: "https://eth-rpc-testnet.polkadot.io/",
firewallAddress: "0x7daf425b9428ee97c1e52b094e2db42637265d73",
policyRegistryAddress: "0x978524ae39575aaf308330466d29419a2affeef6",
replayProofRegistryAddress: "0x6d8807ae9e75ca4307df6e9d0b40bacadb5f7fca",
apiBaseUrl: "http://localhost:3001", // Rust replay API
});
// 1. Build an intent
const intent = client.buildIntent({
routeId: "0x0000000000000000000000000000000000000000000000000000000000000001",
destinationParaId: 1000,
asset: "0xYourAssetAddress",
amount: 1_000_000_000_000_000_000n,
fee: 5_000_000_000n,
remoteSelector: "0xa9059cbb",
destination: "0x010203",
message: "0x01020304",
nonce: BigInt(Date.now()),
deadline: BigInt(Math.floor(Date.now() / 1000) + 1200),
mode: 0, // 0 = EXECUTE, 1 = SEND
});
// 2. Preflight simulation — check policy before spending gas
const preflight = await client.simulate(intent);
if (!preflight.willPass) {
console.error("Blocked:", preflight.reasonCode);
// e.g. "FEE_EXCEEDED", "SELECTOR_BLOCKED", "ROUTE_INACTIVE"
process.exit(1);
}
// 3. Execute on-chain (requires signer)
const tx = await client.execute(intent);
console.log("tx hash:", tx);
// 4. Generate a deterministic replay proof
const caller = "0xYourCallerAddress";
const replay = await client.simulateReplay({ intent, caller });
// 5. Submit + verify the proof
await client.submitReplayProof({
intentHash: replay.intentHash,
replayInputHash: replay.replayInputHash,
replayOutputHash: replay.replayOutputHash,
reasonCode: replay.reasonCode as `0x${string}`,
});
const verified = await client.verifyReplayProof({
intentHash: replay.intentHash,
replayInputHash: replay.replayInputHash,
replayOutputHash: replay.replayOutputHash,
});
console.log("proof verified:", verified); // trueAPI reference
createSentinelClient(config)
Returns a client instance with all action methods bound.
| Method | Description |
| ------------------------------------ | ---------------------------------------------------------------- |
| buildIntent(input) | Constructs a typed Intent struct |
| simulate(intent) | Calls simulateIntent view — returns { willPass, reasonCode } |
| execute(intent) | Sends executeIntent transaction (requires signer) |
| getRoutePolicy(routeId) | Fetches RoutePolicy from PolicyRegistry |
| decodeReasonCode(bytes32) | Decodes on-chain bytes32 reason to human string |
| simulateReplay({ intent, caller }) | Deterministic replay via API — returns hashes |
| submitReplayProof(payload) | POSTs proof to API then submits submitProof tx on-chain |
| verifyReplayProof(payload) | Reads ReplayProofRegistry and verifies hash tuple |
Exported constants
import { REASON_CODES, CHAIN_ID_POLKADOT_HUB_TESTNET } from "@xcm-sentinel/sdk";
REASON_CODES.FEE_EXCEEDED; // "FEE_EXCEEDED"
REASON_CODES.SELECTOR_BLOCKED; // "SELECTOR_BLOCKED"
// ROUTE_INACTIVE | CALLER_BLOCKED | ASSET_BLOCKED | WEIGHT_EXCEEDED | DEST_MISMATCH | DEADLINE_EXPIREDExported errors
import {
PolicyViolationError,
ReplayServiceError,
EncodingError,
} from "@xcm-sentinel/sdk";intentHash(caller, intent)
Computes keccak256(abi.encode(caller, intent)) — Solidity-parity verified. Safe to use server-side or in tests.
import { intentHash } from "@xcm-sentinel/sdk";
const hash = intentHash(callerAddress, intent);Next.js App Router example
"use client";
import { useMemo, useState } from "react";
import { useAccount } from "wagmi";
import { createSentinelClient } from "@xcm-sentinel/sdk";
const SENTINEL_CONFIG = {
chainId: 420420417,
rpcUrl: process.env.NEXT_PUBLIC_POLKADOT_HUB_RPC!,
firewallAddress:
"0x7daf425b9428ee97c1e52b094e2db42637265d73" as `0x${string}`,
policyRegistryAddress:
"0x978524ae39575aaf308330466d29419a2affeef6" as `0x${string}`,
replayProofRegistryAddress:
"0x6d8807ae9e75ca4307df6e9d0b40bacadb5f7fca" as `0x${string}`,
apiBaseUrl: process.env.NEXT_PUBLIC_REPLAY_API_BASE_URL!,
};
export default function Page() {
const { address } = useAccount();
const [result, setResult] = useState<string>("");
const client = useMemo(() => createSentinelClient(SENTINEL_CONFIG), []);
async function runFlow() {
if (!address) return;
const intent = client.buildIntent({
routeId:
"0x0000000000000000000000000000000000000000000000000000000000000001",
destinationParaId: 1000,
asset: address,
amount: 1n,
fee: 1n,
remoteSelector: "0xa9059cbb",
destination: "0x010203",
message: "0x01020304",
nonce: BigInt(Date.now()),
deadline: BigInt(Math.floor(Date.now() / 1000) + 1200),
mode: 0,
});
const preflight = await client.simulate(intent);
if (!preflight.willPass) {
setResult(`Rejected: ${preflight.reasonCode}`);
return;
}
const replay = await client.simulateReplay({ intent, caller: address });
setResult(`Replay hash: ${replay.intentHash}`);
}
return <button onClick={runFlow}>Run Sentinel Flow</button>;
}Migration guide
| Before | After |
| ------------------------------------------- | -------------------------------------------------- |
| Direct call to XCM precompile 0x...0A0000 | client.execute(intent) via XcmSentinelFirewall |
| No pre-execution policy check | client.simulate(intent) before every execute |
| No audit trail | client.simulateReplay + submitReplayProof |
| Manual bytes32 reason code parsing | client.decodeReasonCode(bytes32) |
