Skip to content
Snippets Groups Projects
Verified Commit e96e861c authored by Mateusz Tyszczak's avatar Mateusz Tyszczak :scroll:
Browse files

Add object-oriented beekeeper typescript api

parent 2d159289
No related branches found
No related tags found
5 merge requests!1538Merge develop to master for 1.27.11,!1506Changes made since last release,!1463Merge develop to master for release,!1205Draft: add libfaketime to image,!1088Update Beekeeper interface
Showing
with 503 additions and 3 deletions
import type BeekeeperModule from '../../build/beekeeper_wasm';
import type { MainModule } from '../../build/beekeeper_wasm';
import type BeekeeperFactory from '../../dist/index';
import type BeekeeperFactory2 from '../../dist/detailed/index';
import type { BeekeeperInstanceHelper, ExtractError } from './run_node_helper.js';
......@@ -8,6 +9,7 @@ declare global {
var beekeeper: typeof BeekeeperModule;
var provider: MainModule;
var factory: typeof BeekeeperFactory;
var factory2: typeof BeekeeperFactory2;
var BeekeeperInstanceHelper: BeekeeperInstanceHelper;
var ExtractError: ExtractError;
......
......@@ -10,7 +10,8 @@
{
"imports": {
"@hive/beekeeper": "/build/beekeeper_wasm.js",
"@hive/beekeeperfactory": "/dist/index.js"
"@hive/beekeeperfactory": "/dist/index.js",
"@hive/beekeeperfactory2": "/dist/detailed/index.js"
}
}
</script>
......@@ -18,6 +19,7 @@
<script type="module">
import BeekeeperModule from '@hive/beekeeper';
import beekeeperFactory from '@hive/beekeeperfactory';
import beekeeperFactory2 from '@hive/beekeeperfactory2';
Object.defineProperties(window, {
beekeeper: {
......@@ -30,6 +32,11 @@
return beekeeperFactory;
}
},
factory2: {
get() {
return beekeeperFactory2;
}
},
assert: {
get() {
return {
......
import { ChromiumBrowser, ConsoleMessage, chromium } from 'playwright';
import { expect, test } from '@playwright/test';
import "../assets/data";
let browser!: ChromiumBrowser;
test.describe('Beekeeper factory2 tests', () => {
test.beforeAll(async () => {
browser = await chromium.launch({
headless: true
});
});
test.beforeEach(async({ page }) => {
page.on('console', (msg: ConsoleMessage) => {
console.log('>>', msg.type(), msg.text())
});
await page.goto(`http://localhost:8080/__tests__/assets/test.html`);
await page.waitForURL('**/test.html', { waitUntil: 'load' });
});
test('Should be able to init the beekeeper factory2', async ({ page }) => {
await page.evaluate(async () => {
const beekeeper = await factory2();
await beekeeper.createSession("my.salt");
await beekeeper.delete();
});
});
test('Should be able to get_info based on the created session', async ({ page }) => {
const info = await page.evaluate(async () => {
const beekeeper = await factory2();
const session = await beekeeper.createSession("my.salt");
return await session.getInfo();
});
expect(info).toHaveProperty('now');
expect(info).toHaveProperty('timeout_time');
const timeMatch = /\d+-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d/;
expect(info.now).toMatch(timeMatch);
expect(info.timeout_time).toMatch(timeMatch);
});
test('Should be able to create a wallet and import and remove keys', async ({ page }) => {
const info = await page.evaluate(async () => {
const beekeeper = await factory2();
const session = await beekeeper.createSession("my.salt");
const unlocked = await session.createWallet('w0', 'mypassword');
await unlocked.importKey('5JkFnXrLM2ap9t3AmAxBJvQHF7xSKtnTrCTginQCkhzU5S7ecPT');
await unlocked.importKey('5KGKYWMXReJewfj5M29APNMqGEu173DzvHv5TeJAg9SkjUeQV78');
await unlocked.removeKey('mypassword', '6oR6ckA4TejTWTjatUdbcS98AKETc3rcnQ9dWxmeNiKDzfhBZa');
return await session.getPublicKeys();
});
expect(info).toStrictEqual(['5RqVBAVNp5ufMCetQtvLGLJo7unX9nyCBMMrTXRWQ9i1Zzzizh']);
});
test('Should not be able to import keys after closing a wallet', async ({ page }) => {
const threw = await page.evaluate(async () => {
const beekeeper = await factory2();
const session = await beekeeper.createSession("my.salt");
const unlocked = await session.createWallet('w0', 'mypassword');
await unlocked.close();
try {
await unlocked.importKey('5JkFnXrLM2ap9t3AmAxBJvQHF7xSKtnTrCTginQCkhzU5S7ecPT'); // This should fail
return false;
} catch(error) {
return true;
}
});
expect(threw).toBeTruthy();
});
test.afterAll(async () => {
await browser.close();
});
});
......@@ -189,7 +189,7 @@ EMSCRIPTEN_BINDINGS(beekeeper_api_instance) {
{ "signature":"1f69e091fc79b0e8d1812fc662f12076561f9e38ffc212b901ae90fe559f863ad266fe459a8e946cff9bbe7e56ce253bbfab0cccdde944edc1d05161c61ae86340"}
signature: a signature of a transaction
*/
.function("sign_digest(token, public_key, sig_digest)", &beekeeper_api::sign_digest)
.function("sign_digest(token, sig_digest, public_key)", &beekeeper_api::sign_digest)
/*
****information about a session****
......
import type { BeekeeperModule, beekeeper_api } from "../beekeeper.js";
import { BeekeeperError } from "../errors.js";
import { BeekeeperFileSystem } from "./fs.js";
import { IBeekeeperInstance, IBeekeeperOptions, IBeekeeperSession } from "../interfaces.js";
import { BeekeeperSession } from "./session.js";
// We would like to expose our api using BeekeeperInstance interface, but we would not like to expose users a way of creating instance of BeekeeperApi
export class BeekeeperApi implements IBeekeeperInstance {
public readonly fs: BeekeeperFileSystem;
public api!: Readonly<beekeeper_api>;
public readonly sessions: Map<string, BeekeeperSession> = new Map();
public constructor(
private readonly provider: BeekeeperModule
) {
this.fs = new BeekeeperFileSystem(this.provider.FS);
}
public extract(json: string) {
try {
const parsed = JSON.parse(json);
if(parsed.hasOwnProperty('error'))
throw new BeekeeperError(`Beekeeper API error: "${String(parsed.error)}"`);
if( !parsed.hasOwnProperty('result') )
throw new BeekeeperError(`Beekeeper response does not have contain the result: "${json}"`);
return JSON.parse(parsed.result);
} catch(error) {
if(!(error instanceof BeekeeperError))
throw new BeekeeperError(`${error instanceof Error ? error.name : 'Unknown error'}: Could not extract the result from the beekeeper response: "${json}"`);
throw error;
}
}
public async init({ storageRoot, enableLogs, unlockTimeout }: IBeekeeperOptions) {
await this.fs.init(storageRoot);
const WALLET_OPTIONS = ['--wallet-dir', `${storageRoot}/.beekeeper`, '--enable-logs', Boolean(enableLogs).toString(), '--unlock-timeout', String(unlockTimeout)];
const beekeeperOptions = new this.provider.StringList();
WALLET_OPTIONS.forEach((opt) => void beekeeperOptions.push_back(opt));
this.api = new this.provider.beekeeper_api(beekeeperOptions);
beekeeperOptions.delete();
this.extract(this.api.init() as string);
}
public async createSession(salt: string): Promise<IBeekeeperSession> {
const { token } = this.extract(this.api.create_session(salt) as string) as { token: string };
const session = new BeekeeperSession(this, token);
this.sessions.set(token, session);
await this.fs.sync();
return session;
}
public closeSession(token: string): void {
if(!this.sessions.delete(token))
throw new BeekeeperError(`This Beekeeper API instance is not the owner of session identified by token: "${token}"`);
this.extract(this.api.close_session(token) as string);
}
public async delete(): Promise<void> {
await this.fs.sync();
for(const session of this.sessions.values())
await session.close();
this.api.delete();
}
}
import Beekeeper from "../beekeeper.js";
import { BeekeeperApi } from "./api.js";
import { IBeekeeperInstance, IBeekeeperOptions } from "../interfaces.js";
export const DEFAULT_BEEKEEPER_OPTIONS: IBeekeeperOptions = {
storageRoot: "/storage_root",
enableLogs: true,
unlockTimeout: 900
};
const createBeekeeper = async(
options: Partial<IBeekeeperOptions> = {}
): Promise<IBeekeeperInstance> => {
const beekeeperProvider = await Beekeeper();
const api = new BeekeeperApi(beekeeperProvider);
await api.init({ ...DEFAULT_BEEKEEPER_OPTIONS, ...options });
return api;
};
export default createBeekeeper;
import { BeekeeperModule } from "../beekeeper.js";
export class BeekeeperFileSystem {
public constructor(
private readonly fs: BeekeeperModule['FS']
) {}
public sync(): Promise<void> {
return new Promise((resolve, reject) => {
this.fs.syncfs((err?: unknown) => {
if (err) reject(err);
resolve(undefined);
});
});
}
public init(walletDir: string): Promise<void> {
this.fs.mkdir(walletDir);
this.fs.mount(this.fs.filesystems.IDBFS, {}, walletDir);
return new Promise((resolve, reject) => {
this.fs.syncfs(true, (err?: unknown) => {
if (err) reject(err);
resolve(undefined);
});
});
}
}
import createBeekeeper from "./beekeeper.js";
export * from "../interfaces.js";
export default createBeekeeper;
import { BeekeeperError } from "../errors.js";
import { BeekeeperApi } from "./api.js";
import { IBeekeeperInfo, IBeekeeperInstance, IBeekeeperSession, IBeekeeperWallet, IWalletCreated } from "../interfaces.js";
import { BeekeeperLockedWallet, BeekeeperUnlockedWallet } from "./wallet.js";
interface IBeekeeperWalletPassword {
password: string;
}
interface IBeekeeperWallets {
wallets: Array<{
name: string;
unlocked: boolean;
}>;
}
export class BeekeeperSession implements IBeekeeperSession {
public constructor(
private readonly api: BeekeeperApi,
public readonly token: string
) {}
public readonly wallets: Map<string, BeekeeperLockedWallet> = new Map();
public async getInfo(): Promise<IBeekeeperInfo> {
const result = this.api.extract(this.api.api.get_info(this.token) as string) as IBeekeeperInfo;
await this.api.fs.sync();
return result;
}
public async listWallets(): Promise<Array<IBeekeeperWallet>> {
const result = this.api.extract(this.api.api.list_wallets(this.token) as string) as IBeekeeperWallets;
await this.api.fs.sync();
const wallets: IBeekeeperWallet[] = [];
for(const value of result.wallets) {
const wallet = await this.openWallet(value.name);
wallets.push(wallet);
}
return wallets;
}
public async createWallet(name: string, password?: string): Promise<IWalletCreated> {
if(typeof password === 'string')
this.api.extract(this.api.api.create(this.token, name, password) as string);
else {
const result = this.api.extract(this.api.api.create(this.token, name) as string) as IBeekeeperWalletPassword;
({ password } = result);
}
await this.api.fs.sync();
const wallet = new BeekeeperLockedWallet(this.api, this, name);
wallet.unlocked = new BeekeeperUnlockedWallet(this.api, this, wallet);
this.wallets.set(name, wallet);
return {
wallet: wallet.unlocked,
password
};
}
public async openWallet(name: string): Promise<IBeekeeperWallet> {
if(this.wallets.has(name))
return this.wallets.get(name) as BeekeeperLockedWallet;
this.api.extract(this.api.api.open(this.token, name) as string);
const wallet = new BeekeeperLockedWallet(this.api, this, name);
await this.api.fs.sync();
this.wallets.set(name, wallet);
return wallet;
}
public async closeWallet(name: string): Promise<void> {
if(!this.wallets.delete(name))
throw new BeekeeperError(`This Beekeeper API session is not the owner of wallet identified by name: "${name}"`);
this.api.extract(this.api.api.close(this.token, name) as string);
await this.api.fs.sync();
}
public async lockAll(): Promise<Array<IBeekeeperWallet>> {
const wallets = Array.from(this.wallets.values());
for(const wallet of wallets)
if(typeof wallet.unlocked !== 'undefined')
await wallet.unlocked.lock();
return wallets;
}
public async close(): Promise<IBeekeeperInstance> {
for(const wallet of this.wallets.values())
await wallet.close();
this.api.closeSession(this.token);
return this.api;
}
}
import { BeekeeperApi } from "./api.js";
import { BeekeeperSession } from "./session.js";
import { IBeekeeperUnlockedWallet, IBeekeeperSession, TPublicKey, IBeekeeperWallet, TSignature } from "../interfaces.js";
interface IImportKeyResponse {
public_key: string;
}
interface IBeekeeperSignature {
signature: string;
}
interface IBeekeeperKeys {
keys: Array<{
public_key: string;
}>;
}
export class BeekeeperUnlockedWallet implements IBeekeeperUnlockedWallet {
public constructor(
private readonly api: BeekeeperApi,
private readonly session: BeekeeperSession,
private readonly locked: BeekeeperLockedWallet
) {}
get name(): string {
return this.locked.name;
}
public async lock(): Promise<BeekeeperLockedWallet> {
this.api.extract(this.api.api.lock(this.session.token, this.locked.name) as string);
this.locked.unlocked = undefined;
await this.api.fs.sync();
return this.locked;
}
public async importKey(wifKey: string): Promise<TPublicKey> {
const { public_key } = this.api.extract(this.api.api.import_key(this.session.token, this.locked.name, wifKey) as string) as IImportKeyResponse;
await this.api.fs.sync();
return public_key;
}
public async removeKey(password: string, publicKey: TPublicKey): Promise<void> {
this.api.extract(this.api.api.remove_key(this.session.token, this.locked.name, password, publicKey) as string);
await this.api.fs.sync();
}
public async signDigest(publicKey: string, sigDigest: string): Promise<TSignature> {
const result = this.api.extract(this.api.api.sign_digest(this.session.token, sigDigest, publicKey) as string) as IBeekeeperSignature;
await this.api.fs.sync();
return result.signature;
}
public async getPublicKeys(): Promise<TPublicKey[]> {
const result = this.api.extract(this.api.api.get_public_keys(this.session.token) as string) as IBeekeeperKeys;
await this.api.fs.sync();
return result.keys.map(value => value.public_key);
}
public close(): Promise<IBeekeeperSession> {
return this.locked.close();
}
}
export class BeekeeperLockedWallet implements IBeekeeperWallet {
public unlocked: BeekeeperUnlockedWallet | undefined = undefined;
public constructor(
private readonly api: BeekeeperApi,
private readonly session: BeekeeperSession,
public readonly name: string
) {}
public async unlock(password: string): Promise<IBeekeeperUnlockedWallet> {
this.api.extract(this.api.api.unlock(this.session.token, this.name, password) as string);
this.unlocked = new BeekeeperUnlockedWallet(this.api, this.session, this);
await this.api.fs.sync();
return this.unlocked;
}
public async close(): Promise<IBeekeeperSession> {
if(typeof this.unlocked !== 'undefined')
await this.unlocked.lock();
await this.session.closeWallet(this.name);
return this.session;
}
}
......@@ -8,7 +8,7 @@ const requiresSyncFs = [
] as const;
const doesNotRequireSyncFs = [
"init", "list_wallets", "get_public_keys", "sign_digest",
"sign_binary_transaction", "sign_transaction", "get_info"
"sign_binary_transaction", "get_info"
] as const;
type Callable = {
......
export type TPublicKey = string;
export type TSignature = string;
export interface IWallet {
close(): Promise<IBeekeeperSession>;
readonly name: string;
};
export interface IBeekeeperInfo {
now: string;
timeout_time: string;
}
export interface IBeekeeperOptions {
storageRoot: string;
enableLogs: boolean;
unlockTimeout: number;
}
export interface IBeekeeperUnlockedWallet extends IWallet {
lock(): Promise<IBeekeeperWallet>;
importKey(wifKey: string): Promise<TPublicKey>;
removeKey(password: string, publicKey: TPublicKey): Promise<void>;
signDigest(publicKey: TPublicKey, sigDigest: string): Promise<TSignature>;
getPublicKeys(): Promise<TPublicKey[]>;
}
export interface IBeekeeperWallet extends IWallet {
unlock(password: string): Promise<IBeekeeperUnlockedWallet>;
readonly unlocked?: IBeekeeperUnlockedWallet;
};
export interface IWalletCreated {
wallet: IBeekeeperUnlockedWallet;
password: string;
}
export interface IBeekeeperSession {
getInfo(): Promise<IBeekeeperInfo>;
listWallets(): Promise<Array<IBeekeeperWallet>>;
createWallet(name: string, password?: string): Promise<IWalletCreated>;
lockAll(): Promise<Array<IBeekeeperWallet>>;
close(): Promise<IBeekeeperInstance>;
}
export interface IBeekeeperInstance {
createSession(salt: string): Promise<IBeekeeperSession>;
delete(): Promise<void>;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment