Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# required by .github/workflows/require-dev-to-main.yml
version: 2
updates:
# Root workspace / tooling
- package-ecosystem: "npm"
directory: "/"
schedule:
Expand All @@ -11,3 +12,24 @@ updates:
npm_and_yarn:
patterns:
- "*"

# Doc generation utilities
- package-ecosystem: "npm"
directory: "/docgen"
schedule:
interval: "weekly"
target-branch: "dev"

# Published contract package
- package-ecosystem: "npm"
directory: "/package"
schedule:
interval: "weekly"
target-branch: "dev"

# TypeScript SDK package
- package-ecosystem: "npm"
directory: "/sdk/typescript"
schedule:
interval: "weekly"
target-branch: "dev"
10 changes: 5 additions & 5 deletions scripts/check-remote-ganache-health.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'dotenv/config';
import { createPublicClient, http, Hex, Address } from 'viem';
import { createPublicClient, http } from 'viem';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
Expand Down Expand Up @@ -63,14 +63,14 @@ async function main() {
const rootDir = path.resolve(__dirname, '..');
const deployedPath = path.join(rootDir, 'deployed-addresses.json');

let accountBloxAddress: Address | null = null;
let accountBloxAddress: `0x${string}` | null = null;

if (fs.existsSync(deployedPath)) {
try {
const deployed = JSON.parse(fs.readFileSync(deployedPath, 'utf8'));
const dev = deployed.development;
if (dev && dev.AccountBlox?.address) {
accountBloxAddress = dev.AccountBlox.address as Address;
accountBloxAddress = dev.AccountBlox.address as `0x${string}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate file/env address strings before assigning contract address.

These direct casts accept malformed values and delay failure until readContract. Add a format check and fail early with a clear message.

🛡️ Suggested fix
 let accountBloxAddress: `0x${string}` | null = null;
+const isHexAddress = (v: unknown): v is `0x${string}` =>
+  typeof v === 'string' && /^0x[a-fA-F0-9]{40}$/.test(v);

 ...
- accountBloxAddress = dev.AccountBlox.address as `0x${string}`;
+ if (isHexAddress(dev.AccountBlox.address)) {
+   accountBloxAddress = dev.AccountBlox.address;
+ } else {
+   console.warn('⚠️  Invalid AccountBlox address in deployed-addresses.json');
+ }

 ...
- accountBloxAddress = process.env.ACCOUNTBLOX_ADDRESS as `0x${string}`;
+ if (isHexAddress(process.env.ACCOUNTBLOX_ADDRESS)) {
+   accountBloxAddress = process.env.ACCOUNTBLOX_ADDRESS;
+ } else {
+   console.warn('⚠️  Invalid ACCOUNTBLOX_ADDRESS in .env');
+ }

Also applies to: 84-84

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-remote-ganache-health.ts` at line 73, The assignment of
accountBloxAddress from dev.AccountBlox.address (and the similar assignment at
line 84) blindly casts strings to `0x${string}`, allowing malformed addresses to
slip through; validate the address before assigning by using a proper Ethereum
address check (e.g., ethers.utils.isAddress or a /^0x[0-9a-fA-F]{40}$/ regex)
and throw or process.exit with a clear error message including the offending
value if validation fails so failures happen early and clearly (locate the
assignments to accountBloxAddress and the other casted contract address in this
file and replace the direct cast with the validation + fail-fast behavior).

console.log(
`📋 AccountBlox from deployed-addresses.json (development): ${accountBloxAddress}`
);
Expand All @@ -81,7 +81,7 @@ async function main() {
}

if (!accountBloxAddress && process.env.ACCOUNTBLOX_ADDRESS) {
accountBloxAddress = process.env.ACCOUNTBLOX_ADDRESS as Address;
accountBloxAddress = process.env.ACCOUNTBLOX_ADDRESS as `0x${string}`;
console.log(`📋 AccountBlox from ACCOUNTBLOX_ADDRESS (env): ${accountBloxAddress}`);
}

Expand Down Expand Up @@ -145,7 +145,7 @@ async function main() {
} catch (err: any) {
console.error('❌ AccountBlox read failed (owner/getBroadcasters/getRecovery reverted)');
console.error(` Message: ${err?.message || err}`);
const data: Hex | undefined =
const data: `0x${string}` | undefined =
err?.data ?? err?.cause?.data ?? err?.cause?.cause?.data;
if (typeof data === 'string' && data.startsWith('0x')) {
console.error(` Revert data: ${data}`);
Expand Down
20 changes: 20 additions & 0 deletions scripts/sanity-sdk/secure-ownable/base-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,5 +356,25 @@ export abstract class BaseSecureOwnableTest extends BaseSDKTest {
return false;
}
}

/**
* Map high-level SecureOwnable operation names to their EngineBlox operationType hashes.
* Mirrors the CJS SecureOwnable base test helper so shared tests can use a single helper.
*/
protected getOperationType(
operationName: 'OWNERSHIP_TRANSFER' | 'BROADCASTER_UPDATE' | 'RECOVERY_UPDATE' | 'TIMELOCK_UPDATE'
): Hex {
const map: Record<string, Hex> = {
OWNERSHIP_TRANSFER: '0xb23d8fa2f62c8a954db45521d1249908693b29ffd3d2dab6348898c4198996b2' as Hex,
BROADCASTER_UPDATE: '0xae23396f8eb008d2f5f9673f91ccf20bf248201a6e0dbeaf46c421777ad8dc5b' as Hex,
RECOVERY_UPDATE: '0x032398090b003ba6aff30213cf16b7307ece6fbd6d969286006538a576526983' as Hex,
TIMELOCK_UPDATE: '0x06e0fdee0e8a4d2e629ae3d26c7bc6342072096facbcbe06d204d6051d97c50f' as Hex
};
const value = map[operationName];
if (!value) {
throw new Error(`Unknown operation type: ${operationName}`);
}
return value;
}
}

25 changes: 21 additions & 4 deletions scripts/sanity-sdk/secure-ownable/broadcaster-update-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,12 @@ export class BroadcasterUpdateTests extends BaseSecureOwnableTest {
) || 'wallet1';

const secureOwnableOwner = this.createSecureOwnableWithWallet(ownerWalletName);
const result = await secureOwnableOwner.updateBroadcasterRequest(newBroadcaster, 0n, this.getTxOptions(ownerWallet.address));
const result = await secureOwnableOwner.updateBroadcasterRequest(
newBroadcaster,
0n,
// Explicit gas so viem does not call eth_estimateGas for broadcaster update flows (remote RPC may hang).
this.getTxOptions(ownerWallet.address, { gas: 500_000n })
);

await result.wait();
await new Promise(resolve => setTimeout(resolve, 1000));
Expand Down Expand Up @@ -159,7 +164,11 @@ export class BroadcasterUpdateTests extends BaseSecureOwnableTest {
) || 'wallet1';

const secureOwnableOwner = this.createSecureOwnableWithWallet(ownerWalletName);
const result = await secureOwnableOwner.updateBroadcasterRequest(newBroadcaster, 0n, this.getTxOptions(ownerWallet.address));
const result = await secureOwnableOwner.updateBroadcasterRequest(
newBroadcaster,
0n,
this.getTxOptions(ownerWallet.address, { gas: 500_000n })
);

await result.wait();
await new Promise(resolve => setTimeout(resolve, 1000));
Expand Down Expand Up @@ -208,7 +217,11 @@ export class BroadcasterUpdateTests extends BaseSecureOwnableTest {
) || 'wallet1';

const secureOwnableOwner = this.createSecureOwnableWithWallet(ownerWalletName);
const result = await secureOwnableOwner.updateBroadcasterRequest(newBroadcaster, 0n, this.getTxOptions(ownerWallet.address));
const result = await secureOwnableOwner.updateBroadcasterRequest(
newBroadcaster,
0n,
this.getTxOptions(ownerWallet.address, { gas: 500_000n })
);

await result.wait();
await new Promise(resolve => setTimeout(resolve, 1000));
Expand Down Expand Up @@ -291,7 +304,11 @@ export class BroadcasterUpdateTests extends BaseSecureOwnableTest {
) || 'wallet1';

const secureOwnableOwner = this.createSecureOwnableWithWallet(ownerWalletName);
const result = await secureOwnableOwner.updateBroadcasterRequest(newBroadcaster, 0n, this.getTxOptions(ownerWallet.address));
const result = await secureOwnableOwner.updateBroadcasterRequest(
newBroadcaster,
0n,
this.getTxOptions(ownerWallet.address, { gas: 500_000n })
);

await result.wait();
await new Promise(resolve => setTimeout(resolve, 1000));
Expand Down
64 changes: 35 additions & 29 deletions scripts/sanity-sdk/secure-ownable/eip712-signing-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,34 +38,42 @@ export class EIP712SigningTests extends BaseSecureOwnableTest {
secureOwnableRecovery: any,
recoveryWallet: TestWallet
): Promise<bigint> {
// Prefer reusing an existing pending ownership-transfer transaction if present.
try {
const ownershipOp = this.getOperationType('OWNERSHIP_TRANSFER').toLowerCase();
const recoveryAddr = recoveryWallet.address.toLowerCase();

const findMatchingOwnershipTransfer = async (): Promise<bigint | null> => {
const pendingTxs = await secureOwnableRecovery.getPendingTransactions();
if (pendingTxs && pendingTxs.length > 0) {
for (const id of pendingTxs as bigint[]) {
const tx = await secureOwnableRecovery.getTransaction(id);
const params = (tx as any).params ?? (tx as any)[3];
const op = params?.operationType ?? params?.[4];
const requester = params?.requester ?? params?.[0];

const isOwnershipTransfer =
String(op).toLowerCase() ===
this.getOperationType('OWNERSHIP_TRANSFER').toLowerCase();
const isFromRecovery =
requester &&
String(recoveryWallet.address).toLowerCase() ===
String(requester).toLowerCase();

if (isOwnershipTransfer && isFromRecovery) {
console.log(` 📋 Reusing existing OWNERSHIP_TRANSFER txId: ${id}`);
return id;
}
if (!pendingTxs || pendingTxs.length === 0) return null;

for (const id of pendingTxs as bigint[]) {
const tx = await secureOwnableRecovery.getTransaction(id);
const params = (tx as any).params ?? (tx as any)[3];
const op = (params?.operationType ?? params?.[4]) as string | undefined;
const requester = (params?.requester ?? params?.[0]) as string | undefined;

const isOwnershipTransfer =
!!op && op.toLowerCase() === ownershipOp;
const isFromRecovery =
!!requester && requester.toLowerCase() === recoveryAddr;

if (isOwnershipTransfer && isFromRecovery) {
console.log(` 📋 Reusing existing OWNERSHIP_TRANSFER txId: ${id}`);
return id;
}
}
return null;
};

// Prefer reusing an existing pending ownership-transfer transaction if present.
try {
const existing = await findMatchingOwnershipTransfer();
if (existing !== null) {
return existing;
}
} catch (e: unknown) {
const err = e as Error;
console.log(
` ⚠️ getPendingTransactions failed while searching for reusable tx: ${err.message}`
` ⚠️ getPendingTransactions/getTransaction failed while searching for reusable tx: ${err.message}`
);
}

Expand All @@ -84,13 +92,12 @@ export class EIP712SigningTests extends BaseSecureOwnableTest {
// Allow the chain indexer / state to settle before querying again.
await new Promise((resolve) => setTimeout(resolve, 1000));

const pendingAfter = await secureOwnableRecovery.getPendingTransactions();
if (!pendingAfter || pendingAfter.length === 0) {
throw new Error('No pending transactions found after transferOwnershipRequest');
const created = await findMatchingOwnershipTransfer();
if (created === null) {
throw new Error('No OWNERSHIP_TRANSFER transaction found after transferOwnershipRequest');
}
const txId = pendingAfter[pendingAfter.length - 1] as bigint;
console.log(` 📋 Using newly created transaction ID: ${txId}`);
return txId;
console.log(` 📋 Using newly created transaction ID: ${created}`);
return created;
}

async testEIP712Initialization(): Promise<void> {
Expand Down Expand Up @@ -133,7 +140,6 @@ export class EIP712SigningTests extends BaseSecureOwnableTest {
const secureOwnableRecovery = this.createSecureOwnableWithWallet(recoveryWalletName);
// Reuse an existing pending OWNERSHIP_TRANSFER tx when possible, otherwise create a new one.
const txId = await this.getOrCreateOwnershipTransferTxId(secureOwnableRecovery, recoveryWallet);
this.assertTest(!!txId, 'Pending transaction found for meta-tx signing');

// Create meta-transaction parameters (owner signs approval)
const metaTxParams = await this.createMetaTxParams(
Expand Down
7 changes: 6 additions & 1 deletion scripts/sanity-sdk/secure-ownable/meta-tx-execution-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ export class MetaTxExecutionTests extends BaseSecureOwnableTest {
) || 'wallet1';

const secureOwnableOwner = this.createSecureOwnableWithWallet(ownerWalletName);
const result = await secureOwnableOwner.updateBroadcasterRequest(newBroadcaster, 0n, this.getTxOptions(ownerWallet.address));
const result = await secureOwnableOwner.updateBroadcasterRequest(
newBroadcaster,
0n,
// Explicit gas so viem does not call eth_estimateGas for broadcaster request (remote RPC may hang).
this.getTxOptions(ownerWallet.address, { gas: 500_000n })
);

await result.wait();
await new Promise((resolve) => setTimeout(resolve, 1000));
Expand Down
4 changes: 3 additions & 1 deletion scripts/sanity-sdk/secure-ownable/timelock-period-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ export class TimelockPeriodTests extends BaseSecureOwnableTest {
const secureOwnableBroadcaster = this.createSecureOwnableWithWallet(broadcasterWalletName);
const result = await secureOwnableBroadcaster.updateTimeLockRequestAndApprove(
fullMetaTx,
this.getTxOptions(broadcasterWallet.address)
// Provide explicit gas so viem does not call eth_estimateGas for this
// complex timelock meta-tx payload (can hang/timeout on remote RPCs).
this.getTxOptions(broadcasterWallet.address, { gas: 1_500_000n })
);

this.assertTest(!!result.hash, 'Timelock update transaction created');
Expand Down
8 changes: 6 additions & 2 deletions scripts/sanity/guard-controller/base-test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -577,13 +577,17 @@ class BaseGuardControllerTest {
// Provide an explicit gas limit so web3/provider does not call
// eth_estimateGas (which can hang) or interpret gas as 0.
const gas = 1_500_000;
let timeoutId;
try {
const sendPromise = method.send({ from, value, gas });
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Transaction receipt timeout after ${receiptTimeoutMs / 1000}s (RPC may be slow or tx stuck)`)), receiptTimeoutMs);
timeoutId = setTimeout(
() => reject(new Error(`Transaction receipt timeout after ${receiptTimeoutMs / 1000}s (RPC may be slow or tx stuck)`)),
receiptTimeoutMs
);
});
const result = await Promise.race([sendPromise, timeoutPromise]);

if (timeoutId) clearTimeout(timeoutId);
// Check if transaction actually succeeded by examining the receipt
if (result && result.receipt) {
console.log(` 🔍 Transaction receipt status: ${result.receipt.status}`);
Expand Down
8 changes: 7 additions & 1 deletion scripts/sanity/runtime-rbac/base-test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -485,14 +485,20 @@ class BaseRuntimeRBACTest {
// first, the underlying send() call may still complete later and change
// on-chain state. Callers must treat a timeout as "result unknown" rather
// than assuming the transaction did not execute.
let timeoutId;
try {
const sendPromise = method.send({ from, gas });
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Transaction receipt timeout after ${receiptTimeoutMs / 1000}s (RPC may be slow or tx stuck)`)), receiptTimeoutMs);
timeoutId = setTimeout(
() => reject(new Error(`Transaction receipt timeout after ${receiptTimeoutMs / 1000}s (RPC may be slow or tx stuck)`)),
receiptTimeoutMs
);
});
const result = await Promise.race([sendPromise, timeoutPromise]);
if (timeoutId) clearTimeout(timeoutId);
return result;
} catch (error) {
if (timeoutId) clearTimeout(timeoutId);
// Try to extract revert reason if available
let errorMessage = error.message;
if (error.data) {
Expand Down
3 changes: 2 additions & 1 deletion scripts/sanity/secure-ownable/base-test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,9 @@ class BaseSecureOwnableTest {
// Use an explicit gas limit so web3/provider does not invoke
// eth_estimateGas (which can be unreliable) or treat gas as 0.
const gas = 1_500_000;
let timeoutId;
try {
const sendPromise = method.send({ from, gas });
let timeoutId;
const sendTimeout = new Promise((_, reject) => {
timeoutId = setTimeout(
() => reject(new Error(`Transaction receipt timeout after ${receiptTimeoutMs / 1000}s (RPC may be slow or tx stuck)`)),
Expand All @@ -319,6 +319,7 @@ class BaseSecureOwnableTest {
if (timeoutId) clearTimeout(timeoutId);
return result;
} catch (error) {
if (timeoutId) clearTimeout(timeoutId);
let errorMessage = error.message;
const rawData = error.data?.result ?? error.data;
if (rawData) {
Expand Down
3 changes: 2 additions & 1 deletion scripts/sanity/utils/eip712-signing.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ class EIP712Signer {
*/
async generateMessageHash(metaTx, contract) {
const hex = this._normalizeMessageHex(metaTx.message);
if (!hex) {
const ZERO_SENTINEL = '0x0000000000000000000000000000000000000000000000000000000000000000';
if (!hex || hex === ZERO_SENTINEL) {
throw new Error(
'Meta-transaction missing valid message hash in `metaTx.message`. ' +
'Use generateUnsignedMetaTransactionForNew/Existing so the contract fills this field.'
Expand Down
31 changes: 30 additions & 1 deletion sdk/typescript/contracts/core/BaseStateMachine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,36 @@ export abstract class BaseStateMachine implements IBaseStateMachine {
}
}

// Add gas override for write only (EIP-1559); viem rejects mixing gasPrice with maxFeePerGas.
// Forward explicit gas limit when provided so callers can bypass eth_estimateGas.
if (options.gas !== undefined) {
const rawGas = options.gas;
let gasLimit: bigint;
if (typeof rawGas === 'bigint') {
gasLimit = rawGas;
} else if (typeof rawGas === 'number') {
if (!Number.isSafeInteger(rawGas) || rawGas < 0) {
throw new Error(
`Invalid gas: number inputs must be non-negative safe integers (got "${rawGas}"). ` +
'Use a bigint or decimal string for larger values.'
);
}
gasLimit = BigInt(rawGas);
} else {
const s = String(rawGas).trim();
if (!/^\d+$/.test(s)) {
throw new Error(
`Invalid gas: must be a non-negative integer (got "${options.gas}"). Use a number-like value, decimal string, or bigint.`
);
}
gasLimit = BigInt(s);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (gasLimit < 0n) {
throw new Error(`Invalid gas: must be non-negative (got ${gasLimit.toString()})`);
}
writeContractParams.gas = gasLimit;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Add gas price override for write only (EIP-1559); viem rejects mixing gasPrice with maxFeePerGas.
if (options.gasPrice !== undefined && options.gasPrice !== '') {
const raw = options.gasPrice;
let gasPriceWei: bigint;
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/interfaces/base.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface TransactionResult {
// Common transaction options interface used across all contracts
export interface TransactionOptions {
from: Address;
gas?: number;
gas?: number | bigint | string;
/** Max fee per gas (EIP-1559) in wei. Prefer bigint; string must be a non-negative integer. */
gasPrice?: string | bigint;
value?: string;
Expand Down