Skip to content
Closed
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
18 changes: 18 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Account event tests
on:
pull_request:
branches: [master]
push:
branches: [master]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- run: npm ci
- run: npm run compile
- run: npm run test:unit
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
],
"contributes": {
"configuration": {
"title": "ΞTHcode",
"title": "\u039eTHcode",
"properties": {
"ethcode.networks": {
"scope": "application",
Expand Down Expand Up @@ -220,7 +220,8 @@
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src --ext ts"
"lint": "eslint src --ext ts",
"test:unit": "node --test test/*.test.cjs"
},
"author": "0mkara <0mkar@protonmail.com>",
"license": "MIT"
Expand Down
5 changes: 3 additions & 2 deletions src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type ExtensionContext } from 'vscode'
import { type PublicClient } from 'viem'
import { getProvider } from './provider'
import { type ContractABI, type CompiledJSONOutput, type NetworkConfig, type Fees } from '../types'
import { type ContractABI, type CompiledJSONOutput, type NetworkConfig, type Fees, type IAccountCreated } from '../types'
import { createConstructorInput, createDeployed, createFunctionInput, getConstructorInputFullPath, getDeployedFullPath, getFunctionInputFullPath } from '../utils/functions'
import { logger } from '../lib'
import * as vscode from 'vscode'
Expand All @@ -10,7 +10,8 @@ export const event = {
network: new vscode.EventEmitter<string>(),
account: new vscode.EventEmitter<string>(),
contracts: new vscode.EventEmitter<any>(),
updateAccountList: new vscode.EventEmitter<string[]>()
updateAccountList: new vscode.EventEmitter<string[]>(),
accountCreated: new vscode.EventEmitter<IAccountCreated>()
}

export async function getNetwork(context: ExtensionContext) {
Expand Down
13 changes: 12 additions & 1 deletion src/api/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type * as vscode from 'vscode'
import { event } from './api'
import { type IAccountCreated } from '../types'

/**
* Represents an interface for event emitters of network and account changes.
Expand Down Expand Up @@ -36,6 +37,14 @@ export interface EventsInterface {
* @type {vscode.EventEmitter<any>}
*/
updateAccountList: vscode.EventEmitter<any>

/**
* An event emitter for account creation.
*
* @event
* @type {vscode.EventEmitter<string>}
*/
accountCreated: vscode.EventEmitter<IAccountCreated>
}

/**
Expand All @@ -49,11 +58,13 @@ export function events (): EventsInterface {
const account = event.account
const contracts = event.contracts
const updateAccountList = event.updateAccountList
const accountCreated = event.accountCreated

return {
network,
account,
contracts,
updateAccountList
updateAccountList,
accountCreated
}
}
7 changes: 7 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ export async function activate (context: ExtensionContext): Promise<API | undefi
events: events()
}

context.subscriptions.push(api.events.accountCreated.event((info) => {
if (info.success) logger.success(info.successMsg)
else {
logger.error(info.error)
}
}))

const path_ = workspace.workspaceFolders
if (path_ === undefined) {
await window.showErrorMessage('No folder selected please open one.')
Expand Down
8 changes: 8 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,11 @@ export function isConstructorInputValue (
): obj is ConstructorInputValue {
return obj.value !== undefined
}

export type IAccountCreated = {
successMsg: string
success: true
} | {
error: unknown
success: false
}
84 changes: 44 additions & 40 deletions src/utils/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,48 +60,52 @@ const listAddresses: any = async (

// Create keypair (using viem mnemonic/account)
const createKeyPair: any = (context: vscode.ExtensionContext, keyPath: string, pswd: string) => {
// For now, keep using keythereum for keystore compatibility
const privateKey = generatePrivateKey()

// Generate salt and IV using crypto library
const salt = randomBytes(32)
const iv = randomBytes(16)

// Options for keythereum
const options = {
kdf: 'scrypt',
cipher: 'aes-128-ctr',
kdfparams: {
n: 8192,
r: 8,
p: 1,
dklen: 32
try {
// For now, keep using keythereum for keystore compatibility
const privateKey = generatePrivateKey()

// Generate salt and IV using crypto library
const salt = randomBytes(32)
const iv = randomBytes(16)

// Options for keythereum
const options = {
kdf: 'scrypt',
cipher: 'aes-128-ctr',
kdfparams: {
n: 8192,
r: 8,
p: 1,
dklen: 32
}
}

const keyObject = dump(
Buffer.from(pswd, 'utf-8'),
Buffer.from(privateKey.slice(2), 'hex'),
salt,
iv,
options
)
const pubAddr = `0x${keyObject.address}`
const account: Account = {
pubAddr,
checksumAddr: checksumAddress(pubAddr as `0x${string}`)
}
logger.log(JSON.stringify(account))
const keyStorePath = path.join(context.extensionPath, 'keystore')
if (!fs.existsSync(keyStorePath)) {
fs.mkdirSync(keyStorePath)
}
exportToFile(keyObject, keyStorePath)
event.accountCreated.fire({ success: true, successMsg: `New account created: ${pubAddr}` })
listAddresses(context, keyPath).then((addresses: string[]) => {
event.updateAccountList.fire(addresses)
}).catch((error: any) => logger.error(error))
return pubAddr
} catch (error) {
event.accountCreated.fire({ error, success: false })
}

const keyObject = dump(
Buffer.from(pswd, 'utf-8'),
Buffer.from(privateKey.slice(2), 'hex'),
salt,
iv,
options
)
const pubAddr = `0x${keyObject.address}`
const account: Account = {
pubAddr,
checksumAddr: checksumAddress(pubAddr as `0x${string}`)
}
logger.success('Account created!')
logger.log(JSON.stringify(account))
const keyStorePath = path.join(context.extensionPath, 'keystore')
if (!fs.existsSync(keyStorePath)) {
fs.mkdirSync(keyStorePath)
}
exportToFile(keyObject, keyStorePath)
listAddresses(context, keyPath).then((addresses: string[]) => {
event.updateAccountList.fire(addresses)
}).catch((error: any) => logger.error(error))
return pubAddr
}

// Delete privateKey against address
Expand Down
51 changes: 51 additions & 0 deletions test/account-created.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const path = require('node:path');
const { test } = require('node:test');
const vm = require('node:vm');
const ts = require('typescript');

for (const failure of [null, 'mkdir', 'export']) {
test(`account creation emits only ${failure ? 'failure after ' + failure : 'success after persistence'}`, async () => {
const events = [];
let persisted = false;
const failureError = new Error('storage failure');
const mocks = {
'viem': { checksumAddress: (address) => address },
'viem/accounts': { generatePrivateKey: () => '0x' + '1'.repeat(64) },
'fs': {
existsSync: () => false,
mkdirSync: () => { if (failure === 'mkdir') throw failureError; },
readdirSync: () => [],
},
'path': path,
'crypto': { randomBytes: (length) => Buffer.alloc(length) },
'vscode': { window: {} },
'../api/api': { event: {
accountCreated: { fire: (event) => events.push({ ...event, persisted }) },
updateAccountList: { fire: () => {} },
} },
'../lib': { logger: { log: () => {}, error: () => {} } },
'./networks': { isTestingNetwork: () => false },
'./keythereum': {
dump: () => ({ address: '2'.repeat(40) }),
exportToFile: () => { if (failure === 'export') throw failureError; persisted = true; },
},
};
const source = ts.transpileModule(readFileSync(path.resolve('src/utils/wallet.ts'), 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true },
}).outputText;
const module = { exports: {} };
vm.runInNewContext(source, {
module, exports: module.exports, Buffer,
require: (name) => { if (!(name in mocks)) throw new Error(`Unexpected import: ${name}`); return mocks[name]; },
});
const address = module.exports.createKeyPair({ extensionPath: '/test-only' }, '/test-only', 'test-only-password');
await Promise.resolve();
assert.equal(events.length, 1);
assert.equal(events[0].success, !failure);
assert.equal(events[0].persisted, !failure);
if (failure) { assert.equal(events[0].error, failureError); assert.equal(address, undefined); }
else { assert.equal(address, '0x' + '2'.repeat(40)); assert.match(events[0].successMsg, /New account created/); }
});
}