Skip to content
Open
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
5 changes: 4 additions & 1 deletion .github/workflows/forge.yml → .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ jobs:
done < ".env"
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}

- name: Run forge tests (with auto-repeat in case of error)
uses: Wandalen/[email protected]
with:
command: forge test
attempt_limit: 10
attempt_delay: 15000

- name: Run TypeScript tests
run: bun test:ts
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ EXCLUDED_PATHS=(
"safe/london/out/"
"bun.lock"
".bun/"
"script/deploy/safe/fixtures/"
)

# Load secrets from .env file
Expand Down
7 changes: 6 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@types/pino": "^7.0.5",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^7.10.0",
"bun-types": "^1.2.19",
"cross-env": "^7.0.2",
"dotenv": "^16.0.0",
"eslint": "^8.11.0",
Expand Down Expand Up @@ -664,7 +665,7 @@

"bufio": ["[email protected]", "", {}, "sha512-5Tt66bRzYUSlVZatc0E92uDenreJ+DpTBmSAUwL4VSxJn3e6cUyYwx+PoqML0GRZatgA/VX8ybhxItF8InZgqA=="],

"bun-types": ["[email protected].18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"bun-types": ["[email protected].19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],

"bytes": ["[email protected]", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],

Expand Down Expand Up @@ -2232,6 +2233,8 @@

"@types/bn.js/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="],

"@types/bun/bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],

"@types/connect/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="],

"@types/glob/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="],
Expand Down Expand Up @@ -2724,6 +2727,8 @@

"@sentry/node/https-proxy-agent/agent-base": ["[email protected]", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],

"@types/bun/bun-types/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="],

"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/[email protected]", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="],

"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="],
Expand Down
42 changes: 42 additions & 0 deletions conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,48 @@ All Solidity files must follow the rules defined in `.solhint.json`. This config
- **Execution Environment:**
- All scripts should use `bunx tsx` for TypeScript execution

### TypeScript Test Conventions

- **Testing Framework:**

- Use the Bun built-in testing framework exclusively
- Import test utilities from `bun:test` (e.g., `import { describe, test, expect } from 'bun:test'`)
- Do not use other testing frameworks like Jest, Mocha, or Vitest

- **File Naming and Location:**

- Test files must have a `.test.ts` extension
- Tests should live next to the file under test in the same directory
- Example: `foo.ts` will have a corresponding `foo.test.ts` file in the same directory
- This co-location makes it easier to find and maintain tests

- **Test Structure:**

- Use `describe` blocks to group related tests
- Use `test` for individual test cases
- Test names should be descriptive and explain what is being tested
- Follow the pattern: "should [expected behavior] when [condition]"

- **Test Organization:**

- Group tests by the function or module being tested
- Order tests from simple to complex scenarios
- Include both positive and negative test cases
- Test edge cases and error conditions

- **Running Tests:**

- Use `bun test:ts` to run all tests
- Use `bun test <path>` to run specific test files
- Example: `bun test script/deploy/safe/confirm-safe-tx.test.ts`

- **Best Practices:**
- Keep tests focused and test one thing at a time
- Use descriptive variable names in tests
- Avoid complex logic in tests - tests should be simple and readable
- Mock external dependencies when necessary
- Ensure tests are deterministic and don't depend on external state

### Bash Scripts

Bash scripts provide the robust deployment framework with automated retry mechanisms for handling RPC issues and other deployment challenges. These scripts wrap Foundry's deployment functionality to add reliability and automation.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"postinstall": "patch-package",
"remove-from-diamond": "bun script/tasks/cleanUpProdDiamond.ts",
"test": "forge test --evm-version 'cancun'",
"test:ts": "find script -name '*.test.ts' -type f -exec bun test {} +",
"test:fix": "npm run lint:fix; npm run format:fix; npm run test",
"mongo-logs:sync": "bun script/deploy/update-deployment-logs.ts sync",
"mongo-logs:add": "bun script/deploy/update-deployment-logs.ts add",
Expand Down
196 changes: 196 additions & 0 deletions script/deploy/safe/confirm-safe-tx-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import type { IDecodedTransaction } from './safe-decode-utils'

export interface ITimelockDetails {
target: string
value: string
data: string
predecessor: string
salt: string
delay: string
nestedCall?: IDecodedTransaction
}

/**
* Extracts timelock details from a decoded transaction
* @param decoded - The decoded transaction
* @returns Timelock details or null if not a schedule function
*/
export function extractTimelockDetails(
decoded: IDecodedTransaction
): ITimelockDetails | null {
if (
decoded.functionName !== 'schedule' ||
!decoded.args ||
decoded.args.length < 6
)
return null

const [target, value, data, predecessor, salt, delay] = decoded.args

return {
target,
value,
data,
predecessor,
salt,
delay,
nestedCall: decoded.nestedCall,
}
}

/**
* Prepares nested call data for display
* @param nested - The nested decoded transaction
* @returns Prepared display data
*/
export async function prepareNestedCallDisplay(
nested: IDecodedTransaction
): Promise<{
functionName: string
contractName?: string
decodedVia: string
diamondCutData?: any
decodedData?: any
args?: any[]
error?: string
}> {
const result: any = {
functionName: nested.functionName || nested.selector,
contractName: nested.contractName,
decodedVia: nested.decodedVia,
}

// Handle diamondCut specially
if (nested.functionName === 'diamondCut' && nested.args)
try {
// Note: decodeDiamondCut modifies console output directly
// For testing, we'd need to refactor that too
result.diamondCutData = {
functionName: 'diamondCut',
args: nested.args,
}
} catch (error) {
result.error = error instanceof Error ? error.message : String(error)
}
else if (
nested.functionName === 'diamondCut' &&
!nested.args &&
nested.rawData
)
// Try to decode if we have raw data but no args
try {
// In a real implementation, this would decode the raw data
// For now, we'll just return an error
result.error = 'Failed to decode diamondCut: Unknown signature'
} catch (error) {
result.error = error instanceof Error ? error.message : String(error)
}
else if (
nested.functionName === 'diamondCut' &&
nested.decodedVia === 'unknown'
)
result.error = 'No ABI found for diamondCut function'
else if (nested.args) result.args = nested.args

return result
}

export interface ITransactionDisplayData {
lines: string[]
type: 'regular' | 'diamondCut' | 'schedule' | 'unknown'
}

export interface ISafeTransactionDetails {
nonce: number
to: string
value: string
operation: string
data: string
proposer: string
safeTxHash: string
signatures: string
executionReady: boolean
}

/**
* Formats a decoded transaction for display
* @param decodedTx - The decoded transaction
* @param decoded - Additional decoded data from viem (optional)
* @returns Formatted display data
*/
export function formatTransactionDisplay(
decodedTx: IDecodedTransaction | null,
decoded?: { functionName: string; args?: readonly unknown[] } | null
): ITransactionDisplayData {
const lines: string[] = []
let type: ITransactionDisplayData['type'] = 'regular'

if (decodedTx?.functionName) {
lines.push(`Function: ${decodedTx.functionName}`)
if (decodedTx.contractName)
lines.push(`Contract: ${decodedTx.contractName}`)

lines.push(`Decoded via: ${decodedTx.decodedVia}`)

// Determine type
if (decodedTx.functionName === 'diamondCut') type = 'diamondCut'
else if (decodedTx.functionName === 'schedule') type = 'schedule'

// For regular functions (not diamondCut or schedule)
if (type === 'regular' && decoded) {
lines.push(`Function Name: ${decoded.functionName}`)
if (decoded.args && decoded.args.length > 0) {
lines.push('Decoded Arguments:')
decoded.args.forEach((arg: unknown, index: number) => {
const displayValue = formatArgument(arg)
lines.push(` [${index}]: ${displayValue}`)
})
} else lines.push('No arguments or failed to decode arguments')
}
} else if (decodedTx) {
// Function not found but we have a selector
type = 'unknown'
lines.push(`Unknown function with selector: ${decodedTx.selector}`)
lines.push(`Decoded via: ${decodedTx.decodedVia}`)
} else {
// No decoded transaction at all
type = 'unknown'
lines.push('Failed to decode transaction')
}

return { lines, type }
}

/**
* Formats a single argument for display
* @param arg - The argument to format
* @returns Formatted string representation
*/
export function formatArgument(arg: unknown): string {
if (typeof arg === 'bigint') return arg.toString()
else if (typeof arg === 'object' && arg !== null) return JSON.stringify(arg)

return String(arg)
}

/**
* Formats Safe transaction details for display
* @param details - The Safe transaction details
* @returns Formatted lines for display
*/
export function formatSafeTransactionDetails(
details: ISafeTransactionDetails
): string[] {
return [
'Safe Transaction Details:',
` Nonce: ${details.nonce}`,
` To: ${details.to}`,
` Value: ${details.value}`,
` Operation: ${details.operation}`,
` Data: ${details.data}`,
` Proposer: ${details.proposer}`,
` Safe Tx Hash: ${details.safeTxHash}`,
` Signatures: ${details.signatures}`,
` Execution Ready: ${details.executionReady ? '✓' : '✗'}`,
]
}
Loading
Loading