Skip to content

Commit

Permalink
feat: add gitpod and init script with prompts
Browse files Browse the repository at this point in the history
  • Loading branch information
bguiz committed Jul 3, 2024
1 parent a37f905 commit 7f6f7c9
Show file tree
Hide file tree
Showing 10 changed files with 502 additions and 1 deletion.
12 changes: 12 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ECDSA secp256k1 account obtained from https://portal.hedera.com/dashboard
OPERATOR_ACCOUNT_PRIVATE_KEY=""
OPERATOR_ACCOUNT_ID=""

# ECDSA secp256k1 account generated from seed phrase
SEED_PHRASE=""
ACCOUNT_PRIVATE_KEY=""
ACCOUNT_EVM_ADDRESS=""
ACCOUNT_ID=""

# JSON-RPC endpoint
RPC_URL=""
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.env
/node_modules
/rpcrelay
.env
.rpcrelay.env
package-lock.json
44 changes: 44 additions & 0 deletions .gitpod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Configuration for gitpod.io
# Ref: https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml
# Ref: https://www.gitpod.io/docs/configure/workspaces/tasks
tasks:
- name: "dockerbackground"
openMode: tab-after
init: |
bash ./init-02-dockerbackground.sh
- name: "rpcrelay"
openMode: tab-after
init: |
(
set -e
gp sync-await setup
source .env
node ./init-01-rpcrelay-p00.js
bash ./init-01-rpcrelay-p01-docker.sh
export RPC_URL="$( gp url 7546 )"
bash ./init-01-rpcrelay-p02.sh
gp sync-done rpcrelay
)
- name: "setup"
openMode: tab-before
init: |
(
set -e
npm install
cp .env.sample .env
export RPC_URL="$( gp url 7546 )"
node ./init-00-main.js
source .env
gp sync-done setup
)
ports:
- name: "dapp"
description: "DApp"
port: 3131
onOpen: open-browser
visibility: public
- name: "rpcrelay"
description: "JSON-RPC relay"
port: 7546
onOpen: notify
visibility: public
284 changes: 284 additions & 0 deletions init-00-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
#!/usr/bin/env node

const fs = require('node:fs/promises');
const path = require('node:path');
const readline = require('node:readline/promises');
const { stdin, stdout } = require('node:process');
const {
PrivateKey,
Mnemonic,
} = require ('@hashgraph/sdk');
const dotenv = require('dotenv');

async function init01Main() {

let {
use1stAccountAsOperator,
operatorId,
operatorKey,
seedPhrase,
numAccounts,
accounts,
rpcUrl,
allowOverwrite1stChar,
} = await promptForInputs();
let operatorEvmAddress = '';

if (use1stAccountAsOperator) {
operatorEvmAddress = operatorId;
operatorId = '';
}

// construct env vars file contents
const dotEnvAccounts = accounts.map((account, accountIndex) => {
const {
privateKey,
evmAddress,
} = account;
const text =
`## ${accountIndex} ECDSA secp256k1 account generated from seed phrase
ACCOUNT_${accountIndex}_PRIVATE_KEY="${privateKey}"
ACCOUNT_${accountIndex}_EVM_ADDRESS="${evmAddress}"
ACCOUNT_${accountIndex}_ID=""`;
return text;
}).join('\n\n');

const dotEnvFileContentsProposed = `
# ECDSA secp256k1 account obtained from https://portal.hedera.com/dashboard
OPERATOR_ACCOUNT_PRIVATE_KEY="${operatorKey}"
OPERATOR_ACCOUNT_EVM_ADDRESS="${operatorEvmAddress}"
OPERATOR_ACCOUNT_ID="${operatorId}"
# BIP-39 seed phrase used to generate multiple accounts
SEED_PHRASE="${seedPhrase}"
NUM_ACCOUNTS="${numAccounts}"
${dotEnvAccounts}
# JSON-RPC endpoint
RPC_URL="${rpcUrl}"
`;

console.log('####');
console.log(dotEnvFileContentsProposed);
console.log('####');

if (allowOverwrite1stChar === 'y') {
console.log('OK, overwriting .env file');
const fileName = path.resolve('.env');
await fs.writeFile(fileName, dotEnvFileContentsProposed);
} else {
console.log('OK, leaving current .env file as it was');
}
}

async function promptForInputs() {
dotenv.config();
const {
OPERATOR_ACCOUNT_ID,
OPERATOR_ACCOUNT_PRIVATE_KEY,
SEED_PHRASE,
RPC_URL,
NUM_ACCOUNTS,
} = process.env;

let use1stAccountAsOperator = false;
let operatorId = OPERATOR_ACCOUNT_ID;
let operatorKey = OPERATOR_ACCOUNT_PRIVATE_KEY;
let seedPhrase = SEED_PHRASE;
let numAccounts = NUM_ACCOUNTS;
let accounts = [];
let rpcUrl = RPC_URL;
let allowOverwrite1stChar;

const rlPrompt = readline.createInterface({
input: stdin,
output: stdout,
});

let restart;
do {
restart = false;
// prompt user to input their operator account details
console.log('What is your operator account ID?');
if (operatorId) {
console.log(`Current: "${operatorId}"`);
console.log('(leave blank to use the above)');
} else {
console.log('e.g. "0.0.12345"');
}
console.log('(enter "none" to use first account from BIP-39 seedphrase)');
const inputOperatorId = await rlPrompt.question('> ');
if (inputOperatorId === 'none') {
use1stAccountAsOperator = true;
} else {
operatorId = inputOperatorId || operatorId;
if (!operatorId) {
console.error('Operator account ID must be specified.');
restart = true;
continue;
}
}

if (!use1stAccountAsOperator) {
console.log('What is your operator account private key?');
if (operatorKey) {
console.log(`Current: "${operatorKey}"`);
console.log('(leave blank to use the above)');
} else {
console.log('e.g. "0x1234abcdef5678abcdef90d7edc0242ce802d1c3d5a2bccf7a9aa0cae63632d"');
}
const inputOperatorKey = await rlPrompt.question('> ');
operatorKey = inputOperatorKey || operatorKey;
if (!operatorKey) {
console.error('Operator account private key must be specified.');
restart = true;
continue;
}

// validate operator account details
const operatorPrivateKey = PrivateKey.fromStringECDSA(operatorKey);
const operatorPublicKeyHex = `0x${operatorPrivateKey.publicKey.toStringRaw()}`;

const accountFetchApiUrl =
`https://testnet.mirrornode.hedera.com/api/v1/accounts?account.publickey=${operatorPublicKeyHex}&balance=true&limit=1&order=desc`;
let accountBalanceTinybar;
let accountId;
try {
const accountFetch = await fetch(accountFetchApiUrl);
const accountJson = await accountFetch.json();
const fetchedAccount = accountJson?.accounts[0];
accountId = fetchedAccount?.account;
accountBalanceTinybar = fetchedAccount?.balance?.balance;
const accountKey = fetchedAccount?.key;
const accountAlias = fetchedAccount?.alias;
console.log('Fetched account:', {
accountId,
accountBalanceTinybar,
accountKey,
accountAlias,
});
} catch (ex) {
// do nothing
}
if (accountId !== operatorId || !accountBalanceTinybar) {
console.error('Specified operator account ID does not exist, its private key is a mismatch, or is currently unfunded.');
restart = true;
continue;
}
}

// prompt user to input their seed phrase or leave blank to generate new one
console.log('What is your BIP-39 seed phrase?');
if (seedPhrase) {
console.log(`Current: "${seedPhrase}"`);
console.log('(leave blank to use the above)');
} else {
console.log('(leave blank to use randomly generate a new one)');
}
const inputSeedPhrase = await rlPrompt.question('> ');
seedPhrase = inputSeedPhrase || seedPhrase;

// validate seed phrase OR generate new one
let mnemonic;
if (!seedPhrase) {
// generate a new seed phrase
mnemonic = await Mnemonic.generate12();
seedPhrase = mnemonic.toString();
} else {
// validate specified seed phrase
let isValidSeedPhrase = true;
try {
mnemonic = await Mnemonic.fromString(seedPhrase);
} catch (ex) {
isValidSeedPhrase = false;
}
if (!isValidSeedPhrase) {
console.error('Specified seed phrase is invalid.');
restart = true;
continue;
}
}

// prompt user to input the number of accounts that they would like
console.log('How many accounts would you like to generate from your BIP-39 seed phrase?');
if (numAccounts) {
console.log(`Current: "${numAccounts}"`);
console.log('(leave blank to use the above)');
} else {
console.log('(leave blank to use default (1))');
}
const inputNumAccounts = await rlPrompt.question('> ');
numAccounts = inputNumAccounts || numAccounts || 1;
numAccounts = parseInt(numAccounts, 10);
numAccounts = Math.max(1, numAccounts);

accounts = new Array(numAccounts);
for (let accountIndex = 0; accountIndex < numAccounts; ++accountIndex) {
// generate account ID and key from seed phrase and derivation path
// NOTE can now do this directly in the SDKs,
// no longer need to import ethers.js or viem to accomplish this
// See: https://github.com/hashgraph/hedera-sdk-js/pull/2341
// See: https://github.com/hashgraph/hedera-sdk-java/pull/1842
// See: https://github.com/hashgraph/hedera-sdk-go/pull/958
const accountPrivateKeyObj =
await mnemonic.toStandardECDSAsecp256k1PrivateKeyCustomDerivationPath(
'',
`m/44'/60'/0'/0/${accountIndex}`,
);
const accountPrivateKey = `0x${ accountPrivateKeyObj.toStringRaw() }`;
const accountEvmAddress = `0x${ accountPrivateKeyObj.publicKey.toEvmAddress() }`;
accounts[accountIndex] = {
privateKey: accountPrivateKey,
evmAddress: accountEvmAddress,
};
}

if (use1stAccountAsOperator) {
operatorId = accounts[0].evmAddress;
operatorKey = accounts[0].privateKey;
}

// work out what the RPC URL should be
// Note that when run in gitpod, the task is expected to set RPC_RELAY_URL.
// Otherwise it default to a localhost instance of the RPC relay
console.log('What is your preferred JSON-RPC endpoint URL?');
if (rpcUrl) {
console.log(`Current: "${rpcUrl}"`);
console.log('(leave blank to use the above)');
} else {
console.log('(leave blank to use a default value)');
}
const inputRpcUrl = await rlPrompt.question('> ');
if (!inputRpcUrl) {
rpcUrl = rpcUrl || 'http://localhost:7546/';
} else {
rpcUrl = inputRpcUrl;
}

// prompt user y/n to overwrite .env file
console.log('Are you OK to overwrite the .env file in this directory with the above? (restart/yes/NO)');
const inputAllowOverwrite = await rlPrompt.question('> ');

allowOverwrite1stChar = inputAllowOverwrite.toLowerCase().charAt(0);
if (allowOverwrite1stChar === 'r') {
console.log('OK, restarting');
restart = true;
}
} while (restart);

rlPrompt.close();

return {
use1stAccountAsOperator,
operatorId,
operatorKey,
seedPhrase,
numAccounts,
accounts,
rpcUrl,
allowOverwrite1stChar,
};
}

init01Main();
Loading

0 comments on commit 7f6f7c9

Please sign in to comment.