Skip to content

feat: add @metamask/foundryup package #5810

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9c6da58
feat: add `@metamask/foundryup` package
davidmurdoch May 15, 2025
0826ab0
make it more flexible
davidmurdoch May 15, 2025
1593955
some fixes
davidmurdoch May 15, 2025
30fb702
some tips
davidmurdoch May 15, 2025
6ac55b2
adding shebang to cli script.
cortisiko May 19, 2025
2c366b2
add conditional to check if the yarnrc file doesn't exist or can't be…
cortisiko May 19, 2025
a8ed967
ignore cache so that the foundry binaries cache directory won't be tr…
cortisiko May 19, 2025
a187b22
attempt to fix unit tests + make test follow unit test best practice …
cortisiko May 19, 2025
c1fef6e
remove nock from package json
cortisiko May 19, 2025
54f6380
add more tests
cortisiko May 21, 2025
9a7c16e
fix lint issues within tests
cortisiko May 21, 2025
e2f4380
increase test coverage and reduce coverage threshold to 50%
cortisiko May 23, 2025
362d8ba
attempting to fix lint error: 'TSError: ⨯ Unable to compile TypeScript:'
cortisiko May 23, 2025
23b6f64
add jsdocs for cli.ts file
cortisiko May 23, 2025
e303de9
add js docs to download.ts
cortisiko May 23, 2025
e8cc2e9
add jsdocs for index.ts
cortisiko May 23, 2025
f9f190b
fix lint issues with extract.ts
cortisiko May 23, 2025
7b1ee0d
fix linting issue with test file
cortisiko May 23, 2025
e249f64
Merge branch 'main' into add-foundryup
cortisiko May 23, 2025
59227bf
fix more linting in: index/options/types
cortisiko May 23, 2025
a91cd2d
fix linting on utils file
cortisiko May 23, 2025
cb02fdf
Revert "fix more linting in: index/options/types"
cortisiko May 23, 2025
55badf4
Revert "fix linting on utils file"
cortisiko May 23, 2025
aeccadb
address most of the linting warnings/errors
cortisiko May 27, 2025
1bb5bbb
fix jsdoc for download.ts
cortisiko May 27, 2025
4e0a45b
adding some jsdocs for options.ts
cortisiko May 27, 2025
04d71d9
added some js docs to utils.ts
cortisiko May 27, 2025
6e2b446
fix lint error: Use 'String#startsWith' method instead.
cortisiko May 27, 2025
53f2c14
fix order of imports in types.ts file
cortisiko May 27, 2025
410089a
attempt to fix index.ts fiel
cortisiko May 27, 2025
9cd1c7f
add lint disable
cortisiko May 27, 2025
29c8199
readd comment
cortisiko May 27, 2025
dee3f17
more linting fixes
cortisiko May 28, 2025
ce07926
fix no-misused-promises lint warning
cortisiko May 28, 2025
bbcbf1f
prettier style issues
cortisiko May 28, 2025
2242cfe
clean up package json
cortisiko May 28, 2025
58beb89
Merge branch 'main' into add-foundryup
cortisiko May 28, 2025
560f21f
add code owners
cortisiko May 28, 2025
4ff51b7
auto changelog script
cortisiko May 28, 2025
8eba3af
fix typo
cortisiko May 28, 2025
9fb5bed
fix code owners
cortisiko May 28, 2025
8ef7b12
fix codeowners
cortisiko May 28, 2025
de21a3f
fix dependencies
cortisiko May 28, 2025
7d2ed49
dedupe
cortisiko May 28, 2025
839d43f
Update packages/foundryup/src/cli.ts
cortisiko May 28, 2025
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
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
/packages/permission-log-controller @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers
/packages/profile-sync-controller @MetaMask/identity
/packages/remote-feature-flag-controller @MetaMask/extension-platform @MetaMask/mobile-platform
/packages/foundryup @MetaMask/mobile-platform @MetaMask/extension-platform

## Package Release related
/packages/accounts-controller/package.json @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers
Expand Down Expand Up @@ -148,3 +149,5 @@
/packages/bridge-status-controller/CHANGELOG.md @MetaMask/swaps-engineers @MetaMask/wallet-framework-engineers
/packages/app-metadata-controller/package.json @MetaMask/mobile-platform @MetaMask/wallet-framework-engineers
/packages/app-metadata-controller/CHANGELOG.md @MetaMask/mobile-platform @MetaMask/wallet-framework-engineers
/packages/foundryup/package.json @MetaMask/mobile-platform @MetaMask/extension-platform @MetaMask/wallet-framework-engineers
/packages/foundryup/CHANGELOG.md @MetaMask/mobile-platform @MetaMask/extension-platform @MetaMask/wallet-framework-engineers
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ scripts/coverage
!.yarn/versions

# typescript
packages/*/*.tsbuildinfo
packages/*/*.tsbuildinfo

# foundry cache
.metamask/cache
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Each package in this repository has its own README where you can find installati
- [`@metamask/ens-controller`](packages/ens-controller)
- [`@metamask/error-reporting-service`](packages/error-reporting-service)
- [`@metamask/eth-json-rpc-provider`](packages/eth-json-rpc-provider)
- [`@metamask/foundryup`](packages/foundryup)
- [`@metamask/gas-fee-controller`](packages/gas-fee-controller)
- [`@metamask/json-rpc-engine`](packages/json-rpc-engine)
- [`@metamask/json-rpc-middleware-stream`](packages/json-rpc-middleware-stream)
Expand Down Expand Up @@ -95,6 +96,7 @@ linkStyle default opacity:0.5
ens_controller(["@metamask/ens-controller"]);
error_reporting_service(["@metamask/error-reporting-service"]);
eth_json_rpc_provider(["@metamask/eth-json-rpc-provider"]);
foundryup(["@metamask/foundryup"]);
gas_fee_controller(["@metamask/gas-fee-controller"]);
json_rpc_engine(["@metamask/json-rpc-engine"]);
json_rpc_middleware_stream(["@metamask/json-rpc-middleware-stream"]);
Expand Down
7 changes: 7 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ const config = createConfig([
// e.g. `txreceipt_status`, `signTypedData_v4`, `token_id`
camelcase: 'off',
'id-length': 'off',
'import-x/no-nodejs-modules': 'off',
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice if we didn't have to add these rules globally. It's one of our (soft) goals to simplify this file eventually.

Can you explain why these are needed? I can probably guess why, but just want to make sure.

Copy link
Member

Choose a reason for hiding this comment

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

ah so the rule is disabled because this is a Node.js package that needs to use Node.js built-in modules.

Copy link
Author

Choose a reason for hiding this comment

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

not sure why these needed to be added. cc @cortisiko ?

'n/no-unsupported-features/node-builtins': 'off',
'n/no-missing-import': 'off',
'n/no-restricted-import': 'off',
'n/no-deprecated-api': 'off',

// TODO: re-enble most of these rules
'function-paren-newline': 'off',
Expand Down Expand Up @@ -73,6 +78,8 @@ const config = createConfig([
// TODO: These rules created more errors after the upgrade to ESLint 9.
// Re-enable these rules and address any lint violations.
'n/no-unsupported-features/node-builtins': 'warn',
'n/no-missing-import': 'off',
'n/no-restricted-import': 'off', // Allow Node.js built-in module imports
},
},
{
Expand Down
1 change: 1 addition & 0 deletions packages/foundryup/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.metamask
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we adding this?

Copy link
Author

Choose a reason for hiding this comment

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

.metamask is the name of the root directory for the cache for the binaries we download

1 change: 1 addition & 0 deletions packages/foundryup/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enableGlobalCache: false
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we adding this?

Copy link
Author

@davidmurdoch davidmurdoch May 28, 2025

Choose a reason for hiding this comment

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

This is the default yarn setting on Extension, so it best replicates how it is used in practice right now.

10 changes: 10 additions & 0 deletions packages/foundryup/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

[Unreleased]: https://github.com/MetaMask/core/
20 changes: 20 additions & 0 deletions packages/foundryup/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
MIT License

Copyright (c) 2025 MetaMask

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43 changes: 43 additions & 0 deletions packages/foundryup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# `@metamask/foundryup`

foundryup

## Installation

`yarn add @metamask/foundryup`

or

`npm install @metamask/foundryup`

## Usage

Once installed into a package you can do `yarn bin mm-foundryup`.

This will install the latest version of Foundry things by default.

Try `yarn bin mm-foundryup --help` for more options.

Once you have the binaries installed, you have to figure out how to get to them.

Probably best to just add each as a `package.json` script:

```json
"scripts": {
"anvil": "node_modules/.bin/anvil",
}
```

Kind of weird, but it seems to work okay. You can probably use `npx anvil` in place of `node_modules/.bin/anvil`, but
getting it to work in all scenarios (cross platform and in CI) wasn't straightforward. `yarn bin anvil` doesn't work
Copy link
Contributor

Choose a reason for hiding this comment

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

Does yarn dlx anvil work instead? That's the equivalent to npx in Yarn v2+.

Copy link
Author

Choose a reason for hiding this comment

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

IIRC, dlx isn't exactly equivalent. I think it was that npx executes anything in the .bin directory, dlx only executes executables declared by the package.jsons's bin. I can't check right now.

in yarn v4 because it isn't a bin of `@metamask/foundryup`, so yarn pretends it doesn't exist.

This all needs to work.

---

You can try it here in the monorepo by running `yarn workspace @metamask/foundryup anvil`.

## Contributing

This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
26 changes: 26 additions & 0 deletions packages/foundryup/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/

const merge = require('deepmerge');
const path = require('path');

const baseConfig = require('../../jest.config.packages');

const displayName = path.basename(__dirname);

module.exports = merge(baseConfig, {
// The display name when running multiple projects
displayName,

// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 50,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way we can start with 100% test coverage instead of 50%? If the threshold is below 100%, it ends up being extremely annoying in the future, as any new gaps in coverage that are introduced in the future are very difficult to find.

Copy link
Member

Choose a reason for hiding this comment

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

Hey @mcmire, the reason for adjusting the threshold is that we need a bit more flexibility to reach 100% coverage over time. For now, the team's goal was to hit at least 50%, and we're currently at around 55%, so we're above that baseline.

Since I have another PR that depends on this package, would you be open to us following up in a separate PR focused solely on bringing coverage to 100%?

Copy link
Author

Choose a reason for hiding this comment

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

I'd love to, and normally wouldn't put a PR with such low coverage. But I didn't want to keep blocking Mobile. I won't be able to do any more work for at least a month. Not sure if @cortisiko has the time to increase coverage.

functions: 50,
lines: 50,
statements: 50,
},
},
});
82 changes: 82 additions & 0 deletions packages/foundryup/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"name": "@metamask/foundryup",
"version": "0.0.0",
"description": "foundryup",
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a more descriptive, hmm, description that we can give here?

"keywords": [
"MetaMask",
"Ethereum"
],
"homepage": "https://github.com/MetaMask/core/tree/main/packages/foundryup#readme",
"bugs": {
"url": "https://github.com/MetaMask/core/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/MetaMask/core.git"
},
"license": "MIT",
"sideEffects": false,
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
Comment on lines +21 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this package isn't a library, should we remove these lines? Note that the Yarn constraints currently enforce that all libraries have the default root export, but we can make an exception for this package.

Suggested change
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}

Copy link
Member

Choose a reason for hiding this comment

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

hmmm, the main reason i added this was because the CI lint job would fail because

│  ├─ ⚙ Missing field exports["."].import.types; expected './dist/index.d.mts'
│  ├─ ⚙ Missing field exports["."].import.default; expected './dist/index.mjs'
│  ├─ ⚙ Missing field exports["."].require.types; expected './dist/index.d.cts'
│  ├─ ⚙ Missing field exports["."].require.default; expected './dist/index.cjs'
│  ├─ ⚙ Missing field main; expected './dist/index.cjs'
│  ├─ ⚙ Missing field types; expected './dist/index.d.cts'

https://github.com/MetaMask/core/actions/runs/15289137090/job/43005311736

I am happy to remove it. Can I ignore the lint rule for the above? Is that possible?

},
"bin": {
"mm-foundryup": "./dist/cli.mjs"
},
"files": [
"dist/"
],
"scripts": {
"anvil": "node_modules/.bin/anvil",
"build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references",
"build:docs": "typedoc",
"changelog:update": "../../scripts/update-changelog.sh @metamask/foundryup",
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/foundryup",
"publish:preview": "yarn npm publish --tag preview",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter",
"test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache",
"test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
"since-latest-release": "../../scripts/since-latest-release.sh"
},
"devDependencies": {
"@metamask/auto-changelog": "^3.4.4",
"@types/jest": "^27.4.1",
"@types/unzipper": "^0.10.10",
"@types/yargs": "^17.0.32",
"@types/yargs-parser": "^21.0.3",
"deepmerge": "^4.2.2",
"jest": "^27.5.1",
"nock": "^13.3.1",
"ts-jest": "^27.1.4",
"typedoc": "^0.24.8",
"typedoc-plugin-missing-exports": "^2.0.0",
"typescript": "~5.2.2",
"yaml": "^2.3.4"
},
"engines": {
"node": "^18.18 || >=20"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"minipass": "^7.1.2",
"tar": "^7.4.3",
"unzipper": "^0.12.3",
"yargs": "^17.7.2",
"yargs-parser": "^21.1.1"
},
"main": "./dist/index.cjs",
"types": "./dist/index.d.cts"
}
22 changes: 22 additions & 0 deletions packages/foundryup/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env node

/**
* CLI entry point for Foundryup.
*
* This script downloads and installs Foundry binaries.
* If an error occurs, it logs the error and exits with code 1.
*/
import { downloadAndInstallFoundryBinaries } from '.';

/**
* Run the main installation process and handle errors.
*/
downloadAndInstallFoundryBinaries().catch((error) => {
/**
* Log any error that occurs during installation and exit with code 1.
*
* @param error - The error thrown during installation.
*/
console.error('Error:', error);
process.exit(1);
});
90 changes: 90 additions & 0 deletions packages/foundryup/src/download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { request as httpRequest, type IncomingMessage } from 'node:http';
import { request as httpsRequest } from 'node:https';
import { Stream } from 'node:stream';
import { pipeline } from 'node:stream/promises';

import type { DownloadOptions } from './types';

/**
* A PassThrough stream that emits a 'response' event when the HTTP(S) response is available.
*/
class DownloadStream extends Stream.PassThrough {
/**
* Returns a promise that resolves with the HTTP(S) IncomingMessage response.
*
* @returns The HTTP(S) response stream.
*/
async response(): Promise<IncomingMessage> {
return new Promise((resolve, reject) => {
this.once('response', resolve);
this.once('error', reject);
});
}
}

/**
* Starts a download from the given URL.
*
* @param url - The URL to download from
* @param options - The download options
* @param redirects - The number of redirects that have occurred
* @returns A stream of the download
*/
export function startDownload(
url: URL,
options: DownloadOptions = {},
redirects: number = 0,
) {
const MAX_REDIRECTS = options.maxRedirects ?? 5;
const request = url.protocol === 'http:' ? httpRequest : httpsRequest;
const stream = new DownloadStream();
request(url, options, (response) => {
stream.once('close', () => {
response.destroy();
});

const { statusCode, statusMessage, headers } = response;
// handle redirects
if (
statusCode &&
statusCode >= 300 &&
statusCode < 400 &&
headers.location
) {
if (redirects >= MAX_REDIRECTS) {
stream.emit('error', new Error('Too many redirects'));
response.destroy();
} else {
// note: we don't emit a response until we're done redirecting, because
// handlers only expect it to be emitted once.
pipeline(
startDownload(new URL(headers.location, url), options, redirects + 1)
// emit the response event to the stream
.once('response', stream.emit.bind(stream, 'response')),
stream,
).catch(stream.emit.bind(stream, 'error'));
response.destroy();
}
}

// check for HTTP errors
else if (!statusCode || statusCode < 200 || statusCode >= 300) {
stream.emit(
'error',
new Error(
`Request to ${url} failed. Status Code: ${statusCode} - ${statusMessage}`,
),
);
response.destroy();
} else {
// resolve with response stream
stream.emit('response', response);

response.once('error', stream.emit.bind(stream, 'error'));
pipeline(response, stream).catch(stream.emit.bind(stream, 'error'));
}
})
.once('error', stream.emit.bind(stream, 'error'))
.end();
return stream;
}
Loading
Loading