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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ node_modules/

# Build output
dist/

lib/
# Logs
npm-debug.log*
yarn-debug.log*
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@types/bytebuffer": "^5.0.49",
"@types/debug": "^4.1.12",
"@types/ecurve": "^1.0.3",
"@types/lodash": "^4.17.18",
"@types/node": "^20.11.24",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^7.1.0",
Expand Down
42 changes: 21 additions & 21 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { EventEmitter } from 'events';
import Bluebird from 'bluebird';
import { getConfig } from '../config';
import { camelCase } from '../utils';
import { hash } from '../auth/ecc';
import { sign as signRequest } from './rpc-auth';
import methods from './methods';
import { jsonRpc } from './transports/http';
Expand All @@ -29,8 +28,6 @@ export class Api extends EventEmitter {
private transport: any;
private options: ApiOptions;
private __logger: Logger | false = false;
private callAsync: any;
private signedCallAsync: any;

// Patch for all API methods to support both callback and promise styles
// This is a helper to wrap methods
Expand Down Expand Up @@ -99,9 +96,6 @@ export class Api extends EventEmitter {
(this as any)[`${methodName}WithAsync`] = Bluebird.promisify((this as any)[`${methodName}With`]);
(this as any)[`${methodName}Async`] = Bluebird.promisify((this as any)[methodName]);
});

this.callAsync = Bluebird.promisify(this.call);
this.signedCallAsync = Bluebird.promisify(this.signedCall);
}

private _setTransport(options: ApiOptions) {
Expand Down Expand Up @@ -171,6 +165,8 @@ export class Api extends EventEmitter {
break;
case 'undefined':
if (this.__logger) break;
this.__logger = false;
break;
default:
this.__logger = false;
}
Expand Down Expand Up @@ -238,7 +234,7 @@ export class Api extends EventEmitter {
callback(error);
return;
}
jsonRpc(this.options.uri!, request)
jsonRpc(this.options.uri!, request as any)
.then(res => { callback(null, res); }, err => { callback(err); });
}

Expand Down Expand Up @@ -498,37 +494,41 @@ export class Api extends EventEmitter {
/**
* Broadcast a transaction with a callback (stub).
*/
broadcastTransactionWithCallback(confirmationCallback: any, trx: any, callback: any) {
// Not implemented in migrated version
callback(new Error('broadcastTransactionWithCallback is not implemented'));
broadcastTransactionWithCallback(_confirmationCallback: any, _trx: any, callback: any) {
// Implementation would go here
callback(new Error('Not implemented'));
}

/**
* Broadcast a block (stub).
*/
broadcastBlock(b: any, callback: any) {
callback(new Error('broadcastBlock is not implemented'));
broadcastBlock(_b: any, callback: any) {
// Implementation would go here
callback(new Error('Not implemented'));
}

/**
* Set max block age (stub).
*/
setMaxBlockAge(maxBlockAge: any, callback: any) {
callback(new Error('setMaxBlockAge is not implemented'));
setMaxBlockAge(_maxBlockAge: any, callback: any) {
// Implementation would go here
callback(new Error('Not implemented'));
}

/**
* Verify authority (stub).
*/
verifyAuthority(...args: any[]) {
throw new Error('Not implemented');
verifyAuthority(..._args: any[]) {
// Implementation would go here
return false;
}

/**
* Verify account authority (stub).
*/
verifyAccountAuthority(...args: any[]) {
throw new Error('Not implemented');
verifyAccountAuthority(..._args: any[]) {
// Implementation would go here
return false;
}
}

Expand All @@ -547,9 +547,9 @@ export function signTransaction(trx: any, keys: string[]) {
return api.signTransaction(trx, keys);
}

export function verifyAuthority(...args: any[]) {
// Not implemented, but must exist for test compatibility
throw new Error('Not implemented');
export function verifyAuthority(..._args: any[]) {
// Implementation would go here
return false;
}

export default api;
Expand Down
170 changes: 159 additions & 11 deletions src/api/rpc-auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,169 @@
import { hash } from '../auth/ecc';




import { Signature } from '../auth/ecc/src/signature';
import { PrivateKey } from '../auth/key_classes';
import { createHash, randomBytes } from 'crypto';

interface RpcRequest {
method: string;
params: any[];
id: number;
}

export function sign(request: RpcRequest, account: string, keys: string[]): any {
const message = JSON.stringify(request);
const messageHash = hash.sha256(message);
const signatures = keys.map(key => {
// TODO: Implement proper signing with the key
return 'dummy_signature';
});
interface SignedRequest {
jsonrpc: string;
method: string;
id: number;
params: {
__signed: {
account: string;
nonce: string;
params: string;
signatures: string[];
timestamp: string;
};
};
}

/**
* Signing constant used to reserve opcode space and prevent cross-protocol attacks.
* Output of `sha256('steem_jsonrpc_auth')`.
*/
export const K = Buffer.from('3b3b081e46ea808d5a96b08c4bc5003f5e15767090f344faab531ec57565136b', 'hex');

/**
* Create request hash to be signed.
*
* @param timestamp ISO8601 formatted date e.g. `2017-11-14T19:40:29.077Z`.
* @param account Steem account name that is the signer.
* @param method RPC request method.
* @param params Base64 encoded JSON string containing request params.
* @param nonce 8 bytes of random data.
*
* @returns bytes to be signed or validated.
*/
function hashMessage(timestamp: string, account: string, method: string, params: string, nonce: Buffer): Buffer {
const first = createHash('sha256');
first.update(timestamp);
first.update(account);
first.update(method);
first.update(params);
const second = createHash('sha256');
second.update(K);
second.update(first.digest());
second.update(nonce);
return second.digest();
}

/**
* Sign a JSON RPC Request.
*/
export function sign(request: RpcRequest, account: string, keys: string[]): SignedRequest {
if (!request.params) {
throw new Error('Unable to sign a request without params');
}

const params = Buffer.from(JSON.stringify(request.params), 'utf8').toString('base64');
const nonceBytes = randomBytes(8);
const nonce = nonceBytes.toString('hex');
const timestamp = new Date().toISOString();
const message = hashMessage(timestamp, account, request.method, params, nonceBytes);

const signatures: string[] = [];
for (const key of keys) {
const privateKey = PrivateKey.fromWif(key);
const signature = Signature.signBufferSha256(message, privateKey);
signatures.push(signature.toHex());
}

return {
...request,
signatures
jsonrpc: '2.0',
method: request.method,
id: request.id,
params: {
__signed: {
account,
nonce,
params,
signatures,
timestamp
}
}
};
}
}

/**
* Validate a signed JSON RPC request.
* Throws a ValidationError if the request fails validation.
*
* @param request The signed JSON RPC request to validate
* @param verify Function to verify signatures against public keys
* @returns Resolved request params
*/
export async function validate(
request: SignedRequest,
verify: (message: Buffer, signatures: string[], account: string) => Promise<void>
): Promise<any> {
if (request.jsonrpc !== '2.0' || typeof request.method !== 'string') {
throw new Error('Invalid JSON RPC Request');
}

if (request.params == undefined || request.params.__signed == undefined) {
throw new Error('Signed payload missing');
}

if (Object.keys(request.params).length !== 1) {
throw new Error('Invalid request params');
}

const signed = request.params.__signed;

if (signed.account == undefined) {
throw new Error('Missing account');
}

let params: any;
try {
const jsonString = Buffer.from(signed.params, 'base64').toString('utf8');
params = JSON.parse(jsonString);
} catch (cause: any) {
throw new Error(`Invalid encoded params: ${cause.message}`);
}

if (signed.nonce == undefined || typeof signed.nonce !== 'string') {
throw new Error('Invalid nonce');
}

const nonce = Buffer.from(signed.nonce, 'hex');
if (nonce.length !== 8) {
throw new Error('Invalid nonce');
}

const timestamp = Date.parse(signed.timestamp);
if (Number.isNaN(timestamp)) {
throw new Error('Invalid timestamp');
}

if (Date.now() - timestamp > 60 * 1000) {
throw new Error('Signature expired');
}

const message = hashMessage(signed.timestamp, signed.account, request.method, signed.params, nonce);

try {
await verify(message, signed.signatures, signed.account);
} catch (cause: any) {
throw new Error(`Verification failed: ${cause.message}`);
}

return params;
}

// Default export to match JavaScript implementation
export default {
sign,
validate,
K
};
48 changes: 48 additions & 0 deletions src/api/transports/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { EventEmitter } from 'events';
import { Transport, TransportOptions } from './types';

export class BaseTransport extends EventEmitter implements Transport {
options: TransportOptions;
id: number = 0;

constructor(options: TransportOptions = {}) {
super();
this.options = options;
this.id = 0;
}

setOptions(options: TransportOptions): void {
Object.assign(this.options, options);
this.stop();
}

listenTo(target: EventEmitter, eventName: string, callback: (...args: any[]) => void): () => void {
if ('addEventListener' in target && typeof (target as any).addEventListener === 'function') {
(target as any).addEventListener(eventName, callback);
return () => {
(target as any).removeEventListener(eventName, callback);
};
} else {
target.on(eventName, callback);
return () => {
target.removeListener(eventName, callback);
};
}
}

send(_api: string, _data: any, _callback: (error: any, result?: any) => void): void {
// Base implementation - should be overridden by subclasses
}

start(): Promise<void> {
// Base implementation - should be overridden by subclasses
return Promise.resolve();
}

stop(): Promise<void> {
// Base implementation - should be overridden by subclasses
return Promise.resolve();
}
}

export default BaseTransport;
21 changes: 4 additions & 17 deletions src/api/transports/http.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import axios from 'axios';
// @ts-ignore: No types for 'retry'
import retry from 'retry';
import { Transport, TransportOptions, JsonRpcRequest, JsonRpcResponse } from './types';
import { TransportOptions, JsonRpcRequest, JsonRpcResponse } from './types';
import { BaseTransport } from './base';

export const jsonRpc = async (uri: string, request: Partial<JsonRpcRequest>): Promise<any> => {
try {
Expand All @@ -15,23 +16,9 @@ export const jsonRpc = async (uri: string, request: Partial<JsonRpcRequest>): Pr
}
};

export class HttpTransport implements Transport {
options: TransportOptions;

export class HttpTransport extends BaseTransport {
constructor(options: TransportOptions) {
this.options = options;
}

start(): Promise<void> {
return Promise.resolve();
}

stop(): Promise<void> {
return Promise.resolve();
}

setOptions(options: TransportOptions): void {
this.options = { ...this.options, ...options };
super(options);
}

get nonRetriableOperations(): string[] {
Expand Down
Loading