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
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ VITE_RPC_URL=https://sepolia.infura.io/v3/your-infura-project-id
# VITE_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/your-alchemy-key

ETHERSCAN_API_KEY=your-etherscan-api-key
PRIVATE_KEY=0x1234567890123456789012345678901234567890123456789012345678901234
# The deployer key is never an env var: DEPLOYER_KEYRING=<logicsrc keyring entry>, or type it at the prompt.
DEPLOYER_KEYRING=
# Network configuration
VITE_NETWORK_NAME=Sepolia Testnet
VITE_CHAIN_ID=11155111
Expand Down Expand Up @@ -128,7 +129,7 @@ DEBUG=true
# HARDHAT DEPLOYMENT CONFIGURATION
# =============================================================================
# Private key for contract deployment (NEVER commit this!)
# PRIVATE_KEY=your-wallet-private-key-for-deployment
# (deployer key: see DEPLOYER_KEYRING above; PRIVATE_KEY is refused)

# Etherscan API key for contract verification
# ETHERSCAN_API_KEY=your-etherscan-api-key
Expand Down
22 changes: 17 additions & 5 deletions hardhat.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import '@nomicfoundation/hardhat-toolbox';
import 'dotenv/config';
import { accountsFor } from './scripts/lib/deployer-key.js';

const {
MAINNET_RPC_URL,
SEPOLIA_RPC_URL,
PRIVATE_KEY,
ETHERSCAN_API_KEY,
BASESCAN_API_KEY,
ARBISCAN_API_KEY,
Expand Down Expand Up @@ -77,22 +77,34 @@ const config = {
},
sepolia: {
url: resolvedSepoliaUrl,
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
// The deployer key is asked for (keyring or prompt) only when this network is used; never env.
get accounts() {
return accountsFor('sepolia');
},
chainId: 11155111,
},
mainnet: {
url: resolvedMainnetUrl,
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
// The deployer key is asked for (keyring or prompt) only when this network is used; never env.
get accounts() {
return accountsFor('mainnet');
},
chainId: 1,
},
base: {
url: 'https://mainnet.base.org',
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
// The deployer key is asked for (keyring or prompt) only when this network is used; never env.
get accounts() {
return accountsFor('base');
},
chainId: 8453,
},
arbitrum: {
url: 'https://arb1.arbitrum.io/rpc',
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
// The deployer key is asked for (keyring or prompt) only when this network is used; never env.
get accounts() {
return accountsFor('arbitrum');
},
chainId: 42161,
},
},
Expand Down
12 changes: 4 additions & 8 deletions scripts/check-address.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { ethers } from 'ethers';
import 'dotenv/config';
import { deployerKey } from './lib/deployer-key.js';

async function checkAddresses() {
console.log('🔍 Checking deployment addresses...\n');

// Check deployer address (from PRIVATE_KEY)
if (!process.env.PRIVATE_KEY) {
console.log('❌ No PRIVATE_KEY found in .env file');
return;
}

// The deployer key comes from the logicsrc keyring or a prompt, never env.
try {
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
console.log('👤 Deployer Address (from PRIVATE_KEY):');
const wallet = new ethers.Wallet(deployerKey());
console.log('👤 Deployer Address:');
console.log(` ${wallet.address}`);
console.log(' → This address deploys the contract and pays gas fees\n');

Expand Down
8 changes: 4 additions & 4 deletions scripts/deploy-phantom.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ function loadContractConfig() {
function validateEnvironment() {
log.step('Validating environment configuration...');

// The deployer key is not an env var any more (logicsrc keyring or a prompt,
// see scripts/lib/deployer-key.js); hardhat asks for it for --network sepolia.
const required = [
'PRIVATE_KEY',
'SEPOLIA_RPC_URL'
];

Expand All @@ -98,9 +99,8 @@ function validateEnvironment() {
process.exit(1);
}

// Validate private key format
if (!process.env.PRIVATE_KEY.startsWith('0x')) {
log.error('PRIVATE_KEY must start with 0x');
if (process.env.PRIVATE_KEY) {
log.error('PRIVATE_KEY is set in the environment; it is no longer read. Remove it (use DEPLOYER_KEYRING or the prompt).');
process.exit(1);
}

Expand Down
97 changes: 97 additions & 0 deletions scripts/lib/deployer-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* The deployer wallet key, for the moment a script signs and no longer.
*
* Fleet rule (2026-10-28, after the dev2 wallet drain): a private key is never
* read from the environment, a .env file or a command-line argument. It comes
* from, in order:
*
* 1. the logicsrc keyring, when DEPLOYER_KEYRING names an entry
* (`logicsrc keyring add <file> --name <entry> --type generic`):
* `logicsrc keyring restore <entry> --print-private`, which asks for the
* keyring passphrase on the terminal;
* 2. a hidden prompt on the terminal.
*
* A PRIVATE_KEY still in the environment is refused, so a stale .env cannot
* quietly keep working. Nothing here writes the key anywhere.
*/
import { execFileSync } from 'node:child_process';
import { closeSync, openSync, readSync, writeSync } from 'node:fs';

let cached = null;

function normalize(raw) {
const text = String(raw ?? '').trim();
const m = /(?:0x)?([0-9a-fA-F]{64})/.exec(text);
if (!m) throw new Error('That is not a 32-byte hex private key.');
return `0x${m[1]}`;
}

function fromKeyring(entry) {
const out = execFileSync('logicsrc', ['keyring', 'restore', entry, '--print-private'], {
stdio: ['inherit', 'pipe', 'inherit'],
encoding: 'utf8',
});
return normalize(out);
}

function fromPrompt() {
let fd;
try {
fd = openSync('/dev/tty', 'r+');
} catch {
throw new Error('No terminal to ask for the deployer key on. Set DEPLOYER_KEYRING to a logicsrc keyring entry.');
}
try {
writeSync(fd, 'Deployer private key (hidden): ');
try {
execFileSync('stty', ['-echo'], { stdio: [fd, 'ignore', 'ignore'] });
} catch {
// no stty: the key will echo, but it still never touches env or disk
}
let line = '';
const buf = Buffer.alloc(1);
while (readSync(fd, buf, 0, 1, null) === 1) {
const ch = buf.toString('utf8');
if (ch === '\n' || ch === '\r') break;
line += ch;
}
try {
execFileSync('stty', ['echo'], { stdio: [fd, 'ignore', 'ignore'] });
} catch {
// ignore
}
writeSync(fd, '\n');
return normalize(line);
} finally {
closeSync(fd);
}
}

/** Resolve the deployer key once per process (keyring, else prompt). Never from env. */
export function deployerKey() {
if (cached) return cached;
if (process.env.PRIVATE_KEY) {
throw new Error(
'PRIVATE_KEY is set in the environment (or .env). It is no longer read: remove it, and store the key with ' +
'`logicsrc keyring add <file> --name <entry> --type generic` then set DEPLOYER_KEYRING=<entry>, or type it at the prompt.',
);
}
const entry = process.env.DEPLOYER_KEYRING;
cached = entry ? fromKeyring(entry) : fromPrompt();
return cached;
}

/** Networks that sign with the deployer key; everything else (hardhat, localhost) never asks. */
export const LIVE_NETWORKS = ['sepolia', 'mainnet', 'base', 'arbitrum'];

/** The network this hardhat process was started for. */
export function selectedNetwork(argv = process.argv, env = process.env) {
if (env.HARDHAT_NETWORK) return env.HARDHAT_NETWORK;
const i = argv.indexOf('--network');
return i >= 0 ? argv[i + 1] : undefined;
}

/** `accounts` for a hardhat network: the key only when that network is the one being used. */
export function accountsFor(network) {
return selectedNetwork() === network ? [deployerKey()] : [];
}
11 changes: 4 additions & 7 deletions scripts/prefund-contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { ethers } from 'ethers';
import { deployerKey } from './lib/deployer-key.js';
import { config } from 'dotenv';

// Load environment variables
Expand Down Expand Up @@ -123,13 +124,9 @@ async function prefundContract(network = 'sepolia', privateKey, fundingAmount =
async function main() {
const args = process.argv.slice(2);
const network = args[0] || 'sepolia';
const privateKey = args[1] || process.env.PRIVATE_KEY;
const fundingAmount = args[2] || '0.01';

if (!privateKey) {
console.error('❌ Private key required. Provide as argument or set PRIVATE_KEY env var.');
process.exit(1);
}
// Never from argv (process lists, shell history) or env: keyring or prompt.
const fundingAmount = args[1] || '0.01';
const privateKey = deployerKey();

await prefundContract(network, privateKey, fundingAmount);
}
Expand Down
3 changes: 1 addition & 2 deletions scripts/setup-deployment.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ function checkEnvironmentFile() {
// Read and check .env content
const envContent = fs.readFileSync(envPath, 'utf8');
const requiredVars = [
'PRIVATE_KEY',
'SEPOLIA_RPC_URL',
'VITE_CONTRACT_ADDRESS'
];
Expand Down Expand Up @@ -120,7 +119,7 @@ function showSetupInstructions() {

console.log('\n5. ⚙️ Configure .env file:');
console.log(' • Edit .env in your project root');
console.log(' • Set PRIVATE_KEY=your_private_key_from_phantom');
console.log(' • Store the deployer key: logicsrc keyring add <file> --name <entry> --type generic, then DEPLOYER_KEYRING=<entry> (or type it at the prompt). Never PRIVATE_KEY in .env.');
console.log(' • Set SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/your_project_id');
console.log(' • Set ETHERSCAN_API_KEY=your_etherscan_key (optional, for verification)');
console.log(' • Configure game parameters (optional, defaults provided):');
Expand Down
23 changes: 23 additions & 0 deletions test/deployer-key.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect } from 'chai';
import { afterEach, describe, it } from 'mocha';
import { accountsFor, deployerKey, selectedNetwork } from '../scripts/lib/deployer-key.js';

describe('deployer key (never from env)', () => {
afterEach(() => {
delete process.env.PRIVATE_KEY;
});

it('does not ask for a key when no live network is selected', () => {
expect(accountsFor('sepolia')).to.deep.equal([]);
});

it('reads the selected network from --network or HARDHAT_NETWORK', () => {
expect(selectedNetwork(['node', 'hardhat', 'run', 'x.js', '--network', 'base'], {})).to.equal('base');
expect(selectedNetwork([], { HARDHAT_NETWORK: 'mainnet' })).to.equal('mainnet');
});

it('refuses a PRIVATE_KEY left in the environment', () => {
process.env.PRIVATE_KEY = `0x${'1'.repeat(64)}`;
expect(() => deployerKey()).to.throw(/no longer read/);
});
});
Loading