Wallet SDK Integration
IntentGuard enforces deterministic constraints on the outcome of EVM transactions, protecting users against advanced phishing attacks, MEV, and price drift. If any declared constraint is violated, the transaction is rejected pre-inclusion and never executed. No gas is consumed.
Overview
The SDK submits a protected transaction via the intentguard_sendProtectedTransaction JSON-RPC method. Constraints are validated against the transaction before inclusion. If any constraint is violated, the transaction is dropped and never included in a block.
The return value is a transaction hash, identical to a standard eth_sendRawTransaction response.
Installation
npm install @intentguard/sdk
No peer dependencies required.
Configuration
Constructor
import { IntentGuardClient } from "@intentguard/sdk";
const client = new IntentGuardClient({
rpcUrl: "https://rpc.intentguard.xyz",
chainId: 1,
contractAddress: "0xDeployedContractAddress",
});
| Parameter | Type | Required | Description |
|---|---|---|---|
rpcUrl | string | No | IntentGuard RPC endpoint. Default: "https://rpc.intentguard.xyz" |
chainId | number | Yes | EVM chain ID (e.g. 1 for mainnet, 11155111 for Sepolia) |
contractAddress | string | Yes | Deployed IntentGuardBalanceEnforcement contract address |
timeoutMs | number | No | Request timeout in milliseconds applied to each RPC call. No timeout by default. |
fetch | FetchFn | No | Custom fetch implementation. Defaults to global fetch. |
All JSON-RPC calls are sent to rpcUrl. The SDK includes an X-IntentGuard-SDK header with the package version on every request.
Environment variables
import { IntentGuardClient, fromEnv } from "@intentguard/sdk";
const client = new IntentGuardClient(fromEnv());
| Variable | Required | Description |
|---|---|---|
INTENTGUARD_RPC_URL | No | IntentGuard RPC endpoint. Default: "https://rpc.intentguard.xyz" |
INTENTGUARD_CHAIN_ID | Yes | Chain ID (integer) |
INTENTGUARD_CONTRACT_ADDRESS | Yes | IntentGuardBalanceEnforcement contract address |
API reference
submitProtectedTransaction
const txHash = await client.submitProtectedTransaction(
rawTx, // 0x-prefixed signed raw transaction
constraints, // ProtectedConstraint[] — 1 to 10 constraints
signer, // EthereumSigner with signTypedData() and getAddress()
options?, // optional: { validUntilBlock?: number }
);
| Parameter | Type | Description |
|---|---|---|
rawTx | string | 0x-prefixed signed raw transaction hex |
constraints | ProtectedConstraint[] | 1–10 balance constraints to enforce |
signer | EthereumSigner | Wallet that signed rawTx. Must implement signTypedData() and getAddress(). Must be the same account that signed the raw transaction. |
options.validUntilBlock | number (optional) | Block number after which the request expires. If omitted, the SDK fetches the current block and adds 10. If provided, the SDK validates that validUntilBlock - currentBlock >= 5. Requests below this threshold are rejected client-side. |
Returns: Promise<string> — the transaction hash (0x-prefixed).
Ethereum read methods
Standard Ethereum JSON-RPC read methods routed through the configured RPC endpoint:
const blockNumber = await client.getBlockNumber();
const balance = await client.getBalance("0xAddress");
const nonce = await client.getTransactionCount("0xAddress");
const gasPrice = await client.getGasPrice();
const receipt = await client.getTransactionReceipt("0xTxHash");
Available: getBlockNumber, getChainId, getBalance, getTransactionCount, getCode, getGasPrice, getMaxPriorityFeePerGas, getFeeHistory, estimateGas, call, getBlockByNumber, getBlockByHash, getSyncing, getTransactionByHash, getTransactionReceipt, getLogs.
Constraint reference
Each constraint declares an enforcement rule for a specific account and asset:
interface ProtectedConstraint {
/** Ethereum address whose balance is tracked */
account: string;
/** Asset contract address */
token: string;
/**
* Maximum amount that may leave the account.
* Decimal string in base units (amount * 10^decimals).
* Use "0" to block all outflows.
*/
maxOutflow: string;
/**
* Minimum amount that must arrive at the account.
* Decimal string in base units (amount * 10^decimals).
* Use "0" if no minimum inflow is required.
*/
minInflow: string;
}
1–10 constraints per request. Validation is performed client-side before any network call.
Block unauthorized outflows
Set maxOutflow: "0" to enforce that no amount of the specified asset leaves the account. If any outflow occurs, the transaction is rejected pre-inclusion and not executed.
{
account: "0xCustodyWallet",
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
maxOutflow: "0",
minInflow: "0",
}
Cap outflows
Set maxOutflow to the expected amount. If the transaction violates declared constraints, it is rejected pre-inclusion and not executed. No gas is consumed.
{
account: "0xUserWallet",
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
maxOutflow: "500000000", // 500 USDC (6 decimals)
minInflow: "0",
}
Enforce expected outcomes
Use minInflow to require a minimum amount to arrive. Useful for swaps and other state-changing operations.
const constraints = [
{
account: "0xUserWallet",
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
maxOutflow: "1000000000", // max 1,000 USDC out
minInflow: "0",
},
{
account: "0xUserWallet",
token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
maxOutflow: "0",
minInflow: "500000000000000000", // at least 0.5 WETH in
},
];
Security model
- Constraint authenticity is verified on-chain via EIP-712 signature verification.
- Constraint integrity is enforced via deterministic hashing of the constraint set.
- The constraint signer must match the sender of the raw transaction. Mismatches are rejected.
- On-chain EIP-712 verification makes constraint enforcement trustless and independently verifiable by any party. Both wallets and users can audit constraint validation on-chain. No trust assumption on IntentGuard infrastructure is required for enforcement correctness.
- Constraint signatures include a
validUntilBlockexpiry. Expired requests are rejected before execution. - Constraint signatures are domain-separated by
chainIdandverifyingContractto prevent cross-chain or cross-contract replay. - IntentGuard does not custody funds and does not modify transaction semantics beyond constraint enforcement.
Third-party accounts
Constraints are not limited to the sender. Any account can be monitored:
{
account: "0xTreasuryMultisig",
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
maxOutflow: "0",
minInflow: "0",
}
Error handling
All errors extend IntentGuardError with a stable code property. Use isIntentGuardError for detection and IntentGuardErrorCode for exhaustive switch handling.
import { isIntentGuardError, IntentGuardErrorCode } from "@intentguard/sdk";
try {
const txHash = await client.submitProtectedTransaction(rawTx, constraints, signer);
} catch (err) {
if (!isIntentGuardError(err)) throw err;
switch (err.code) {
case IntentGuardErrorCode.INVALID_CONSTRAINTS:
case IntentGuardErrorCode.INVALID_VALIDITY_WINDOW:
case IntentGuardErrorCode.INVALID_REQUEST:
case IntentGuardErrorCode.INVALID_TRANSACTION:
case IntentGuardErrorCode.PROTECTED:
case IntentGuardErrorCode.NONCE_TOO_HIGH:
// Non-retryable.
break;
case IntentGuardErrorCode.NETWORK_ERROR:
case IntentGuardErrorCode.SUBMISSION_FAILED:
case IntentGuardErrorCode.SERVICE_UNAVAILABLE:
// Retryable with backoff.
break;
default:
throw err;
}
}
| Code | Origin | Retryable | Description |
|---|---|---|---|
INVALID_CONSTRAINTS | Client | No | Constraints contain invalid addresses, negative amounts, or exceed the 1–10 limit. |
INVALID_VALIDITY_WINDOW | Client | No | validUntilBlock is in the past or too close to the current block (minimum 5 blocks ahead). |
NETWORK_ERROR | Client | Yes | RPC endpoint unreachable. |
INVALID_REQUEST | Server | No | Constraint signer does not match transaction sender, or request has expired. |
INVALID_TRANSACTION | Server | No | Raw transaction is malformed, empty, or contains invalid fields. |
SUBMISSION_FAILED | Server | Yes | Transaction could not be submitted to the network. |
SERVICE_UNAVAILABLE | Server | Yes | Service temporarily unavailable. Retry with backoff. |
PROTECTED | Server | No | IntentGuard protection — declared constraints were violated. Transaction was not submitted. No gas consumed. |
NONCE_TOO_HIGH | Server | No | Transaction nonce too high; pending transactions must confirm first. |
Passthrough RPC failures surface as RPC_ERROR_<code> (e.g. "RPC_ERROR_-32603").
Low-level signing API
For direct access to EIP-712 typed data (custom UI signing prompts, hardware wallets):
import {
signConstraints,
computeConstraintsHash,
buildConstraintsTypedData,
} from "@intentguard/sdk";
const constraintsHash = computeConstraintsHash(constraints);
const { domain, types, value } = buildConstraintsTypedData(
userAddress,
txHash,
validUntilBlock,
constraintsHash,
chainId,
contractAddress,
);
const signature = await signConstraints(
signer, userAddress, txHash, validUntilBlock,
constraints, chainId, contractAddress,
);
EIP-712 domain
| Field | Value |
|---|---|
name | "IntentGuard" |
version | "1" |
chainId | Network chain ID |
verifyingContract | IntentGuardBalanceEnforcement contract address |
Limitations
- EOA only. IntentGuard requires the transaction sender to be an Externally Owned Account (EOA). Smart Accounts and Account Abstraction (ERC-4337) are currently supported. but will be supported in the next releases.
Operational notes
- The SDK does not retry requests. Retry logic is the integrator's responsibility.
- Set
timeoutMsto enforce deterministic timeouts. Otherwise, OS/runtime defaults apply. - The SDK enforces a minimum validity window of 5 blocks. Requests below this threshold are rejected client-side.
- Use the
fetchconfig option to route traffic through proxies, add mTLS, or inject observability. - Requires Node.js >= 18 or a browser environment with native
fetch.
License
MIT