nano-account-crawler
Advanced tools
Comparing version 1.0.27 to 2.0.0
import { INanoAccountForwardIterable, INanoBlock } from './nano-interfaces'; | ||
import { NanoAccountForwardCrawler } from './nano-account-forward-crawler'; | ||
import { IStatusReturn } from './status-return-interfaces'; | ||
export declare class BananoAccountVerifiedForwardCrawler implements INanoAccountForwardIterable { | ||
@@ -8,3 +9,3 @@ private _nanoAccountForwardCrawler; | ||
initialize(): Promise<void>; | ||
[Symbol.asyncIterator](): AsyncIterator<INanoBlock>; | ||
[Symbol.asyncIterator](): AsyncIterator<IStatusReturn<INanoBlock>>; | ||
} |
@@ -17,3 +17,3 @@ "use strict"; | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
while (g && (g = 0, op[0] && (_ = 0)), _) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
@@ -41,3 +41,4 @@ if (y = 0, t) op = [op[0] & 2, t.value]; | ||
exports.BananoAccountVerifiedForwardCrawler = void 0; | ||
var bananojs = require("@bananocoin/bananojs"); | ||
var bananojsImport = require("@bananocoin/bananojs"); | ||
var bananojs = bananojsImport; | ||
// Iterable that makes requests as required when looping through blocks in an account. | ||
@@ -75,3 +76,3 @@ var BananoAccountVerifiedForwardCrawler = /** @class */ (function () { | ||
next: function () { return __awaiter(_this, void 0, void 0, function () { | ||
var iteratorResult, error_2, block, tempBlock, calculatedHash, hash, hashBytes, workBytes; | ||
var iteratorResult, error_2, blockStatusReturn, block, tempBlock, calculatedHash, hash, hashBytes, workBytes; | ||
return __generator(this, function (_a) { | ||
@@ -89,9 +90,16 @@ switch (_a.label) { | ||
case 3: | ||
block = iteratorResult.value; | ||
blockStatusReturn = iteratorResult.value; | ||
if (blockStatusReturn.status === "error") { | ||
return [2 /*return*/, { value: blockStatusReturn, done: true }]; | ||
} | ||
block = blockStatusReturn.value; | ||
if (!block) { | ||
return [2 /*return*/, { value: { status: "error", error_type: "MissingBlock", message: "expected block, got nothing" }, done: true }]; | ||
} | ||
if (iteratorResult.done) { | ||
return [2 /*return*/, { value: undefined, done: true }]; | ||
return [2 /*return*/, { value: { status: "ok", data: undefined }, done: true }]; | ||
} | ||
// Verify block has expected value for previous | ||
if (typeof expectedPrevious === "string" && block.previous !== expectedPrevious) { | ||
throw Error("InvalidChain: expectedPrevious: ".concat(expectedPrevious, " actual block.previous: ").concat(block.previous, " for block: ").concat(block.hash)); | ||
return [2 /*return*/, { value: { status: "error", error_type: "InvalidChain", message: "expectedPrevious: ".concat(expectedPrevious, " actual block.previous: ").concat(block.previous, " for block: ").concat(block.hash) }, done: true }]; | ||
} | ||
@@ -107,3 +115,3 @@ tempBlock = { | ||
if (calculatedHash !== block.hash) { | ||
throw Error("InvalidChain: unexpected hash: ".concat(block.hash, " calculated: ").concat(calculatedHash)); | ||
return [2 /*return*/, { value: { status: "error", error_type: "InvalidChain", message: "unexpected hash: ".concat(block.hash, " calculated: ").concat(calculatedHash) }, done: true }]; | ||
} | ||
@@ -117,7 +125,7 @@ hash = block.previous; | ||
if (!bananojs.bananoUtil.isWorkValid(hashBytes, workBytes)) { | ||
throw Error("InvalidChain: unable to verify work for: ".concat(hash, " with work: ").concat(block.work)); | ||
return [2 /*return*/, { value: { status: "error", error_type: "InvalidChain", message: "unable to verify work for: ".concat(hash, " with work: ").concat(block.work) }, done: true }]; | ||
} | ||
// Verify signature | ||
if (!bananojs.verify(block.hash, block.signature, this._publicKeyHex)) { | ||
throw Error("InvalidChain: unable to verify signature for block: ".concat(block.hash)); | ||
return [2 /*return*/, { value: { status: "error", error_type: "InvalidChain", message: "unable to verify signature for block: ".concat(block.hash) }, done: true }]; | ||
} | ||
@@ -124,0 +132,0 @@ expectedPrevious = block.hash; |
import { NanoNode } from './nano-node'; | ||
import { INanoAccountBackwardIterable, INanoBlock, TAccount, TBlockHash } from './nano-interfaces'; | ||
import { IStatusReturn } from './status-return-interfaces'; | ||
export declare class NanoAccountBackwardCrawler implements INanoAccountBackwardIterable { | ||
@@ -15,4 +16,4 @@ nanoNode: NanoNode; | ||
constructor(nanoNode: NanoNode, account: TAccount, head?: string, accountFilter?: TAccount[], count?: number, maxBlocksPerRequest?: number); | ||
initialize(): Promise<void>; | ||
[Symbol.asyncIterator](): AsyncIterator<INanoBlock>; | ||
initialize(): Promise<IStatusReturn<void>>; | ||
[Symbol.asyncIterator](): AsyncIterator<IStatusReturn<INanoBlock>>; | ||
private reachedCount; | ||
@@ -19,0 +20,0 @@ get maxBlocksPerRequest(): number; |
@@ -17,3 +17,3 @@ "use strict"; | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
while (g && (g = 0, op[0] && (_ = 0)), _) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
@@ -58,25 +58,52 @@ if (y = 0, t) op = [op[0] & 2, t.value]; | ||
NanoAccountBackwardCrawler.prototype.initialize = function () { | ||
var _a, _b, _c, _d; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var historySegmentPromise, accountInfoPromise, _a, _b, error_1; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
var accountHistoryStatusReturn, accountInfoStatusReturn, historySegmentPromise, accountInfoPromise, error_1; | ||
return __generator(this, function (_e) { | ||
switch (_e.label) { | ||
case 0: | ||
_c.trys.push([0, 3, , 4]); | ||
_e.trys.push([0, 3, , 4]); | ||
historySegmentPromise = this.nanoNode.getBackwardHistory(this.account, this.head, "0", this.accountFilter, this._maxBlocksPerRequest); | ||
accountInfoPromise = this.nanoNode.getAccountInfo(this.account); | ||
_a = this; | ||
return [4 /*yield*/, historySegmentPromise]; | ||
case 1: | ||
_a.accountHistory = _c.sent(); | ||
_b = this; | ||
accountHistoryStatusReturn = _e.sent(); | ||
return [4 /*yield*/, accountInfoPromise]; | ||
case 2: | ||
_b.accountInfo = _c.sent(); | ||
accountInfoStatusReturn = _e.sent(); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
error_1 = _c.sent(); | ||
throw (error_1); | ||
error_1 = _e.sent(); | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: "Unexpected error occurred while initializing: ".concat(error_1) | ||
}]; | ||
case 4: | ||
this.confirmationHeight = BigInt('' + this.accountInfo.confirmation_height); | ||
return [2 /*return*/]; | ||
if (accountHistoryStatusReturn.status === "error") { | ||
return [2 /*return*/, accountHistoryStatusReturn]; | ||
} | ||
if (accountInfoStatusReturn.status === "error") { | ||
return [2 /*return*/, accountInfoStatusReturn]; | ||
} | ||
if (!accountInfoStatusReturn.value) { | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "MissingAccountInfo", | ||
message: "Account info is missing after initializing" | ||
}]; | ||
} | ||
this.accountHistory = accountHistoryStatusReturn.value; | ||
this.accountInfo = accountInfoStatusReturn.value; | ||
if (typeof (((_a = this.accountInfo) === null || _a === void 0 ? void 0 : _a.confirmation_height) || 0) !== 'string' || !((_b = this.accountInfo) === null || _b === void 0 ? void 0 : _b.confirmation_height.match(/^\d+$/))) { | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "MissingAccountInfo", | ||
message: "Expected confirmation_height to be a string containing an integer, got: ".concat((_c = this.accountInfo) === null || _c === void 0 ? void 0 : _c.confirmation_height, " of type ").concat(typeof ((_d = this.accountInfo) === null || _d === void 0 ? void 0 : _d.confirmation_height)) | ||
}]; | ||
} | ||
this.confirmationHeight = BigInt('' + (this.accountInfo.confirmation_height || 0)); | ||
return [2 /*return*/, { | ||
status: "ok" | ||
}]; | ||
} | ||
@@ -89,3 +116,18 @@ }); | ||
if (this.accountHistory === null || this.accountInfo === null || this.confirmationHeight <= BigInt('0')) { | ||
throw Error('NanoAccountCrawlerError: not initialized. Did you call initialize() before iterating?'); | ||
return { | ||
next: function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "NanoAccountCrawlerError", | ||
message: "not initialized. Did you call initialize() before iterating?", | ||
}, | ||
done: true, | ||
}]; | ||
}); | ||
}); | ||
}, | ||
}; | ||
} | ||
@@ -115,4 +157,4 @@ var maxRpcIterations = 1000; | ||
case 0: | ||
if (endReached || historyIndex === undefined || history.length === 0 || historyIndex >= history.length) { | ||
return [2 /*return*/, { value: undefined, done: true }]; | ||
if (endReached || history == undefined || historyIndex === undefined || history.length === 0 || historyIndex >= history.length) { | ||
return [2 /*return*/, { value: { status: "ok", value: undefined }, done: true }]; | ||
} | ||
@@ -122,6 +164,13 @@ block = history[historyIndex]; | ||
if (blockHeight <= BigInt('0')) { | ||
return [2 /*return*/, { value: undefined, done: true }]; | ||
return [2 /*return*/, { value: { status: "ok", value: undefined }, done: true }]; | ||
} | ||
if (typeof this.accountFilter === "undefined" && typeof nextHash === "string" && block.hash !== nextHash) { | ||
throw Error("InvalidChain: Expected nextHash: ".concat(nextHash, ", got: ").concat(block.hash)); | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "InvalidChain", | ||
message: "Expected nextHash: ".concat(nextHash, ", got: ").concat(block.hash), | ||
}, | ||
done: true, | ||
}]; | ||
} | ||
@@ -132,3 +181,3 @@ historyIndex += 1; | ||
endReached = true; | ||
return [2 /*return*/, { value: block, done: false }]; | ||
return [2 /*return*/, { value: { status: "ok", value: block }, done: false }]; | ||
case 1: | ||
@@ -138,3 +187,10 @@ // Guard against infinite loops and making too many RPC calls. | ||
if (rpcIterations > maxRpcIterations) { | ||
throw Error("TooManyRpcIterations: Expected to fetch full history from nano node within ".concat(maxRpcIterations, " requests.")); | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "TooManyRpcIterations", | ||
message: "Expected to fetch full history from nano node within ".concat(maxRpcIterations, " requests."), | ||
}, | ||
done: true, | ||
}]; | ||
} | ||
@@ -151,3 +207,10 @@ _accountHistory = void 0; | ||
error_2 = _a.sent(); | ||
throw (error_2); | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "NanoNodeError", | ||
message: error_2.message, | ||
}, | ||
done: true, | ||
}]; | ||
case 5: | ||
@@ -161,6 +224,6 @@ history = _accountHistory.history; | ||
endReached = true; | ||
return [2 /*return*/, { value: block, done: false }]; | ||
return [2 /*return*/, { value: { status: "ok", value: block }, done: false }]; | ||
} | ||
else { | ||
return [2 /*return*/, { value: block, done: false }]; | ||
return [2 /*return*/, { value: { status: "ok", value: block }, done: false }]; | ||
} | ||
@@ -170,3 +233,3 @@ return [2 /*return*/]; | ||
}); | ||
}); } | ||
}); }, | ||
}; | ||
@@ -173,0 +236,0 @@ }; |
import { NanoNode } from './nano-node'; | ||
import { INanoAccountForwardIterable, INanoBlock, TAccount, TBlockHash, TStringBigInt } from './nano-interfaces'; | ||
import { IStatusReturn } from './status-return-interfaces'; | ||
export declare class NanoAccountForwardCrawler implements INanoAccountForwardIterable { | ||
@@ -16,4 +17,4 @@ private _nanoNode; | ||
constructor(nanoNode: NanoNode, account: TAccount, head?: TBlockHash, offset?: TStringBigInt, accountFilter?: TAccount[], count?: number, maxBlocksPerRequest?: number); | ||
initialize(): Promise<void>; | ||
[Symbol.asyncIterator](): AsyncIterator<INanoBlock>; | ||
initialize(): Promise<IStatusReturn<void>>; | ||
[Symbol.asyncIterator](): AsyncIterator<IStatusReturn<INanoBlock>>; | ||
private exceededCount; | ||
@@ -20,0 +21,0 @@ private reachedCount; |
@@ -17,3 +17,3 @@ "use strict"; | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
while (g && (g = 0, op[0] && (_ = 0)), _) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
@@ -62,24 +62,41 @@ if (y = 0, t) op = [op[0] & 2, t.value]; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var historySegmentPromise, accountInfoPromise, _a, _b, error_1; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
var historySegmentPromise, accountInfoPromise, historySegmentResponse, accountInfoResponse, error_1; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
_c.trys.push([0, 3, , 4]); | ||
_a.trys.push([0, 3, , 4]); | ||
historySegmentPromise = this._nanoNode.getForwardHistory(this._account, this._head, this._offset, this._accountFilter, this._maxBlocksPerRequest); | ||
accountInfoPromise = this._nanoNode.getAccountInfo(this._account); | ||
_a = this; | ||
return [4 /*yield*/, historySegmentPromise]; | ||
case 1: | ||
_a._accountHistory = _c.sent(); | ||
_b = this; | ||
historySegmentResponse = _a.sent(); | ||
return [4 /*yield*/, accountInfoPromise]; | ||
case 2: | ||
_b._accountInfo = _c.sent(); | ||
accountInfoResponse = _a.sent(); | ||
if (historySegmentResponse.status === "error") { | ||
return [2 /*return*/, historySegmentResponse]; | ||
} | ||
if (accountInfoResponse.status === "error") { | ||
return [2 /*return*/, accountInfoResponse]; | ||
} | ||
this._accountHistory = historySegmentResponse.value; | ||
this._accountInfo = accountInfoResponse.value; | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
error_1 = _c.sent(); | ||
throw (error_1); | ||
error_1 = _a.sent(); | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: "Unexpected error occurred while initializing: ".concat(error_1) | ||
}]; | ||
case 4: | ||
if (!this._accountInfo) { | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "MissingAccountInfo", | ||
message: "Account info is missing after initialization" | ||
}]; | ||
} | ||
this._confirmationHeight = BigInt('' + this._accountInfo.confirmation_height); | ||
return [2 /*return*/]; | ||
return [2 /*return*/, { status: "ok" }]; | ||
} | ||
@@ -92,3 +109,18 @@ }); | ||
if (this._accountHistory === undefined || this._accountInfo === undefined || this._confirmationHeight <= BigInt('0')) { | ||
throw Error('NanoAccountCrawlerError: not initialized. Did you call initialize() before iterating?'); | ||
return { | ||
next: function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "NanoAccountCrawlerError", | ||
message: "not initialized. Did you call initialize() before iterating?", | ||
}, | ||
done: true, | ||
}]; | ||
}); | ||
}); | ||
}, | ||
}; | ||
} | ||
@@ -108,3 +140,3 @@ var rpcIterations = 0; | ||
if (endReached || history.length === 0 || historyIndex >= history.length) { | ||
return [2 /*return*/, { value: undefined, done: true }]; | ||
return [2 /*return*/, { value: { status: "done", done: true }, done: true }]; | ||
} | ||
@@ -114,9 +146,20 @@ block = history[historyIndex]; | ||
if (blockHeight <= BigInt('0') || blockHeight > this._confirmationHeight) { | ||
return [2 /*return*/, { value: undefined, done: true }]; | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "InvalidBlockHeightError", | ||
message: "Block height ".concat(blockHeight, " is outside valid range (0, ").concat(this._confirmationHeight, "]"), | ||
}, | ||
done: true, | ||
}]; | ||
} | ||
if (blockHeight <= BigInt('0') || blockHeight > this._confirmationHeight) { | ||
return [2 /*return*/, { value: undefined, done: true }]; | ||
} | ||
if (typeof this._accountFilter === "undefined" && typeof previous === "string" && block.previous !== previous) { | ||
throw Error("InvalidChain: Expected previous: ".concat(previous, " got ").concat(block.previous, " for ").concat(block.hash)); | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "InvalidChainError", | ||
message: "Expected previous: ".concat(previous, ", got ").concat(block.previous, " for ").concat(block.hash), | ||
}, | ||
done: true, | ||
}]; | ||
} | ||
@@ -129,3 +172,10 @@ historyIndex += 1; | ||
if (rpcIterations > this._maxRpcIterations) { | ||
throw Error("TooManyRpcIterations: Expected to fetch full history from nano node within ".concat(this._maxRpcIterations, " requests.")); | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "TooManyRpcIterationsError", | ||
message: "Expected to fetch full history from nano node within ".concat(this._maxRpcIterations, " requests."), | ||
}, | ||
done: true, | ||
}]; | ||
} | ||
@@ -142,3 +192,10 @@ _accountHistory = void 0; | ||
error_2 = _a.sent(); | ||
throw (error_2); | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "RpcError", | ||
message: error_2.message, | ||
}, | ||
done: true, | ||
}]; | ||
case 4: | ||
@@ -151,10 +208,17 @@ history = _accountHistory.history; | ||
if (this.exceededCount(startBlockHeight, blockHeight + BigInt(1))) { | ||
return [2 /*return*/, { value: undefined, done: true }]; | ||
return [2 /*return*/, { | ||
value: { | ||
status: "error", | ||
error_type: "CountExceededError", | ||
message: "Reached maximum count (".concat(this._count, ") before reaching confirmation height (").concat(this._confirmationHeight, ")"), | ||
}, | ||
done: true, | ||
}]; | ||
} | ||
else if (this.reachedCount(startBlockHeight, blockHeight + BigInt(1))) { | ||
endReached = true; | ||
return [2 /*return*/, { value: block, done: false }]; | ||
return [2 /*return*/, { value: { status: "ok", value: block }, done: false }]; | ||
} | ||
{ | ||
return [2 /*return*/, { value: block, done: false }]; | ||
return [2 /*return*/, { value: { status: "ok", value: block }, done: false }]; | ||
} | ||
@@ -161,0 +225,0 @@ return [2 /*return*/]; |
@@ -1,8 +0,9 @@ | ||
export declare type TAccount = `${'ban_' | 'nano_'}${string}`; | ||
export declare type TNanoBlockType = "state"; | ||
export declare type TNanoBlockSubtype = "send" | "receive" | "open" | "change" | "epoch"; | ||
export declare type TStringBigInt = `${number}`; | ||
export declare type TBlockHeight = `${number}`; | ||
export declare type TBlockHash = string; | ||
export declare type TPublicKey = string; | ||
import { IStatusReturn } from "./status-return-interfaces"; | ||
export type TAccount = `${'ban_' | 'nano_'}${string}`; | ||
export type TNanoBlockType = "state"; | ||
export type TNanoBlockSubtype = "send" | "receive" | "open" | "change" | "epoch"; | ||
export type TStringBigInt = `${number}`; | ||
export type TBlockHeight = `${number}`; | ||
export type TBlockHash = string; | ||
export type TPublicKey = string; | ||
export interface INanoBlock { | ||
@@ -39,5 +40,5 @@ type: TNanoBlockType; | ||
} | ||
export interface INanoAccountForwardIterable extends AsyncIterable<INanoBlock> { | ||
export interface INanoAccountForwardIterable extends AsyncIterable<IStatusReturn<INanoBlock>> { | ||
} | ||
export interface INanoAccountBackwardIterable extends AsyncIterable<INanoBlock> { | ||
export interface INanoAccountBackwardIterable extends AsyncIterable<IStatusReturn<INanoBlock>> { | ||
} |
import { INanoAccountHistory, INanoAccountInfo, TAccount, TStringBigInt, TBlockHash } from './nano-interfaces'; | ||
import { IStatusReturn } from './status-return-interfaces'; | ||
export declare class NanoNode { | ||
@@ -6,8 +7,8 @@ private nodeApiUrl; | ||
constructor(nodeApiUrl: string, fetch: Function); | ||
getForwardHistory(account: TAccount, head?: TBlockHash, offset?: TStringBigInt, account_filter?: TAccount[], count?: number, max_retries?: number): Promise<INanoAccountHistory>; | ||
getBackwardHistory(account: TAccount, head?: TBlockHash, offset?: TStringBigInt, account_filter?: TAccount[], count?: number, max_retries?: number): Promise<INanoAccountHistory>; | ||
getAccountInfo(account: TAccount): Promise<INanoAccountInfo>; | ||
hasMoreHistory(history: any, confirmationHeight: BigInt): boolean; | ||
getForwardHistory(account: TAccount, head?: TBlockHash, offset?: TStringBigInt, account_filter?: TAccount[], count?: number, max_retries?: number): Promise<IStatusReturn<INanoAccountHistory>>; | ||
getBackwardHistory(account: TAccount, head?: TBlockHash, offset?: TStringBigInt, account_filter?: TAccount[], count?: number, max_retries?: number): Promise<IStatusReturn<INanoAccountHistory>>; | ||
getAccountInfo(account: TAccount): Promise<IStatusReturn<INanoAccountInfo>>; | ||
hasMoreHistory(history: any, confirmationHeight: bigint): boolean; | ||
historyIsEmpty(history: any): boolean; | ||
jsonRequest(jsonRequest: any): Promise<any>; | ||
jsonRequest(jsonRequest: any): Promise<IStatusReturn<any>>; | ||
private validateIsAccountHistory; | ||
@@ -14,0 +15,0 @@ private validateIsAccountInfo; |
@@ -17,3 +17,3 @@ "use strict"; | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
while (g && (g = 0, op[0] && (_ = 0)), _) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
@@ -53,3 +53,3 @@ if (y = 0, t) op = [op[0] & 2, t.value]; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var request, retries, response, error_1; | ||
var request, retries, response, errorResponse, responseStatus, isAccountHistoryValid, isAccountValid, error_1; | ||
return __generator(this, function (_a) { | ||
@@ -77,23 +77,58 @@ switch (_a.label) { | ||
response = undefined; | ||
errorResponse = undefined; | ||
_a.label = 1; | ||
case 1: | ||
if (!true) return [3 /*break*/, 6]; | ||
if (!(retries < max_retries)) return [3 /*break*/, 8]; | ||
retries += 1; | ||
_a.label = 2; | ||
case 2: | ||
_a.trys.push([2, 4, , 5]); | ||
_a.trys.push([2, 6, , 7]); | ||
return [4 /*yield*/, this.jsonRequest(request)]; | ||
case 3: | ||
response = _a.sent(); | ||
this.validateIsAccountHistory(account, response); | ||
this.validateAccount(account, response); | ||
return [3 /*break*/, 6]; | ||
responseStatus = _a.sent(); | ||
if (responseStatus.status === "error") { | ||
errorResponse = responseStatus; | ||
return [3 /*break*/, 1]; | ||
} | ||
response = responseStatus.value; | ||
return [4 /*yield*/, this.validateIsAccountHistory(account, response)]; | ||
case 4: | ||
isAccountHistoryValid = _a.sent(); | ||
return [4 /*yield*/, this.validateAccount(account, response)]; | ||
case 5: | ||
isAccountValid = _a.sent(); | ||
if (isAccountHistoryValid.status === "error") { | ||
errorResponse = isAccountHistoryValid; | ||
return [3 /*break*/, 1]; | ||
} | ||
if (isAccountValid.status === "error") { | ||
errorResponse = isAccountValid; | ||
return [3 /*break*/, 1]; | ||
} | ||
errorResponse = undefined; | ||
return [3 /*break*/, 8]; | ||
case 6: | ||
error_1 = _a.sent(); | ||
if (retries >= max_retries || !error_1.message.match(/NanoNodeError:/)) { | ||
throw error_1; | ||
errorResponse = { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: "Unexpected error occurred while getting forward history: ".concat(error_1) | ||
}; | ||
return [3 /*break*/, 1]; | ||
case 7: return [3 /*break*/, 1]; | ||
case 8: | ||
if (!response) { | ||
errorResponse = { | ||
status: "error", | ||
error_type: "MissingNanoNodeResponse", | ||
message: "NanoNode response is missing after getting forward history" | ||
}; | ||
} | ||
return [3 /*break*/, 5]; | ||
case 5: return [3 /*break*/, 1]; | ||
case 6: return [2 /*return*/, response]; | ||
if (errorResponse) { | ||
return [2 /*return*/, errorResponse]; | ||
} | ||
return [2 /*return*/, { | ||
status: "ok", | ||
value: response | ||
}]; | ||
} | ||
@@ -110,3 +145,3 @@ }); | ||
return __awaiter(this, void 0, void 0, function () { | ||
var request, retries, response, error_2; | ||
var request, retries, response, errorResponse, responseStatus, isAccountHistoryValid, isAccountValid, error_2; | ||
return __generator(this, function (_a) { | ||
@@ -132,23 +167,58 @@ switch (_a.label) { | ||
response = undefined; | ||
errorResponse = undefined; | ||
_a.label = 1; | ||
case 1: | ||
if (!true) return [3 /*break*/, 6]; | ||
if (!(retries < max_retries)) return [3 /*break*/, 8]; | ||
retries += 1; | ||
_a.label = 2; | ||
case 2: | ||
_a.trys.push([2, 4, , 5]); | ||
_a.trys.push([2, 6, , 7]); | ||
return [4 /*yield*/, this.jsonRequest(request)]; | ||
case 3: | ||
response = _a.sent(); | ||
this.validateIsAccountHistory(account, response); | ||
this.validateAccount(account, response); | ||
return [3 /*break*/, 6]; | ||
responseStatus = _a.sent(); | ||
if (responseStatus.status === "error") { | ||
errorResponse = responseStatus; | ||
return [3 /*break*/, 1]; | ||
} | ||
response = responseStatus.value; | ||
return [4 /*yield*/, this.validateIsAccountHistory(account, response)]; | ||
case 4: | ||
isAccountHistoryValid = _a.sent(); | ||
return [4 /*yield*/, this.validateAccount(account, response)]; | ||
case 5: | ||
isAccountValid = _a.sent(); | ||
if (isAccountHistoryValid.status === "error") { | ||
errorResponse = isAccountHistoryValid; | ||
return [3 /*break*/, 1]; | ||
} | ||
if (isAccountValid.status === "error") { | ||
errorResponse = isAccountValid; | ||
return [3 /*break*/, 1]; | ||
} | ||
errorResponse = undefined; | ||
return [3 /*break*/, 8]; | ||
case 6: | ||
error_2 = _a.sent(); | ||
if (retries >= max_retries || !error_2.message.match(/NanoNodeError:/)) { | ||
throw error_2; | ||
errorResponse = { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: "Unexpected error occurred while getting backward history: ".concat(error_2) | ||
}; | ||
return [3 /*break*/, 1]; | ||
case 7: return [3 /*break*/, 1]; | ||
case 8: | ||
if (!response) { | ||
errorResponse = { | ||
status: "error", | ||
error_type: "MissingNanoNodeResponse", | ||
message: "NanoNode response is missing after getting backward history" | ||
}; | ||
} | ||
return [3 /*break*/, 5]; | ||
case 5: return [3 /*break*/, 1]; | ||
case 6: return [2 /*return*/, response]; | ||
if (errorResponse) { | ||
return [2 /*return*/, errorResponse]; | ||
} | ||
return [2 /*return*/, { | ||
status: "ok", | ||
value: response | ||
}]; | ||
} | ||
@@ -160,3 +230,3 @@ }); | ||
return __awaiter(this, void 0, void 0, function () { | ||
var request, response, error_3; | ||
var request, response, responseStatus, isAccountValid, error_3; | ||
return __generator(this, function (_a) { | ||
@@ -171,12 +241,36 @@ switch (_a.label) { | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
_a.trys.push([1, 4, , 5]); | ||
return [4 /*yield*/, this.jsonRequest(request)]; | ||
case 2: | ||
response = _a.sent(); | ||
this.validateIsAccountInfo(account, response); | ||
return [3 /*break*/, 4]; | ||
responseStatus = _a.sent(); | ||
if (responseStatus.status === "error") { | ||
return [2 /*return*/, responseStatus]; | ||
} | ||
response = responseStatus.value; | ||
return [4 /*yield*/, this.validateIsAccountInfo(account, response)]; | ||
case 3: | ||
isAccountValid = _a.sent(); | ||
if (isAccountValid.status === "error") { | ||
return [2 /*return*/, isAccountValid]; | ||
} | ||
return [3 /*break*/, 5]; | ||
case 4: | ||
error_3 = _a.sent(); | ||
throw (error_3); | ||
case 4: return [2 /*return*/, response]; | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: "Unexpected error occurred while getting account info: ".concat(error_3) | ||
}]; | ||
case 5: | ||
if (!response) { | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "MissingNanoNodeResponse", | ||
message: "NanoNode response is missing after getting account info" | ||
}]; | ||
} | ||
return [2 /*return*/, { | ||
status: "ok", | ||
value: response | ||
}]; | ||
} | ||
@@ -194,3 +288,3 @@ }); | ||
return __awaiter(this, void 0, void 0, function () { | ||
var request, response, jsonResponse; | ||
var request, response, jsonResponse, error_4; | ||
return __generator(this, function (_a) { | ||
@@ -205,11 +299,43 @@ switch (_a.label) { | ||
}, | ||
body: JSON.stringify(jsonRequest) | ||
body: undefined | ||
}; | ||
return [4 /*yield*/, this.fetch(this.nodeApiUrl, request).catch(function (error) { throw (error); })]; | ||
try { | ||
request.body = JSON.stringify(jsonRequest); | ||
} | ||
catch (error) { | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "JsonStringifyError", | ||
message: "Error occurred while converting JSON request to string: ".concat(error) | ||
}]; | ||
} | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 4, , 5]); | ||
return [4 /*yield*/, this.fetch(this.nodeApiUrl, request)]; | ||
case 2: | ||
response = _a.sent(); | ||
return [4 /*yield*/, response.json().catch(function (error) { throw (error); })]; | ||
case 2: | ||
return [4 /*yield*/, response.json()]; | ||
case 3: | ||
jsonResponse = _a.sent(); | ||
return [2 /*return*/, jsonResponse]; | ||
return [3 /*break*/, 5]; | ||
case 4: | ||
error_4 = _a.sent(); | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: "Unexpected error occurred while making JSON request: ".concat(error_4) | ||
}]; | ||
case 5: | ||
if (!jsonResponse) { | ||
return [2 /*return*/, { | ||
status: "error", | ||
error_type: "MissingJsonResponse", | ||
message: "JSON response is missing after making JSON request" | ||
}]; | ||
} | ||
return [2 /*return*/, { | ||
status: "ok", | ||
value: jsonResponse | ||
}]; | ||
} | ||
@@ -224,28 +350,62 @@ }); | ||
if (typeof (accountHistory) !== 'object') { | ||
throw Error("UnexpectedNanoNodeResponse: Unexpected accountHistory. Expected type to be 'object', got: ".concat(typeof (accountHistory), " for ").concat(account)); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: "Unexpected accountHistory. Expected type to be 'object', got: ".concat(typeof (accountHistory), " for ").concat(account) | ||
}; | ||
} | ||
if (accountHistory.hasOwnProperty('error')) { | ||
throw Error("NanoNodeError: ".concat(accountHistory.error, " for ").concat(account)); | ||
return { | ||
status: "error", | ||
error_type: "NanoNodeError", | ||
message: "".concat(accountHistory.error, " for ").concat(account) | ||
}; | ||
} | ||
if (typeof (accountHistory.account) !== 'string') { | ||
throw Error("UnexpectedNanoNodeResponse: Unexpected accountHistory.account. Expected type to be 'string', got: ".concat(typeof (accountHistory.account), " for ").concat(account)); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: "Unexpected accountHistory.account. Expected type to be 'string', got: ".concat(typeof (accountHistory.account), " for ").concat(account) | ||
}; | ||
} | ||
if (!accountHistory.hasOwnProperty('history')) { | ||
throw Error("UnexpectedNanoNodeResponse: accountHistory doesn't have property 'history' for ".concat(account)); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: "accountHistory doesn't have property 'history' for ".concat(account) | ||
}; | ||
} | ||
var _prototype = Object.prototype.toString.call(accountHistory.history); | ||
if (!(_prototype === '[object String]' || _prototype === '[object Array]')) { | ||
throw Error("UnexpectedNanoNodeResponse: accountHistory.history not of type array or string. Got: ".concat(_prototype, " for ").concat(account)); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: "accountHistory.history not of type array or string. Got: ".concat(_prototype, " for ").concat(account) | ||
}; | ||
} | ||
return { status: "ok" }; | ||
}; | ||
NanoNode.prototype.validateIsAccountInfo = function (account, accountInfo) { | ||
if (typeof (accountInfo) !== 'object') { | ||
throw Error("UnexpectedNanoNodeResponse: Unexpected accountInfo. Expected type to be 'object', got: '".concat(typeof (accountInfo), "' for ").concat(account)); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: "Unexpected accountInfo. Expected type to be 'object', got: '".concat(typeof (accountInfo), "' for ").concat(account) | ||
}; | ||
} | ||
if (accountInfo.hasOwnProperty('error')) { | ||
throw Error("NanoNodeError: ".concat(accountInfo.error, " for ").concat(account)); | ||
return { | ||
status: "error", | ||
error_type: "NanoNodeError", | ||
message: "".concat(accountInfo.error, " for ").concat(account) | ||
}; | ||
} | ||
if (typeof (accountInfo['confirmation_height']) !== 'string') { | ||
throw Error("UnexpectedNanoNodeResponse: Unexpected accountInfo['confirmation_height']. Expected type to be 'string', got: ".concat(typeof (accountInfo['confirmation_height']), " for ").concat(account)); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: "Unexpected accountInfo['confirmation_height']. Expected type to be 'string', got: ".concat(typeof (accountInfo['confirmation_height']), " for ").concat(account) | ||
}; | ||
} | ||
return { status: "ok" }; | ||
}; | ||
@@ -255,4 +415,9 @@ NanoNode.prototype.validateAccount = function (account, accountHistory) { | ||
if (account !== accountHistory['account']) { | ||
throw Error("AccountMismatch: requested info for account '".concat(account, "' but head was for account '").concat(accountHistory['account'], "'")); | ||
return { | ||
status: "error", | ||
error_type: "AccountMismatch", | ||
message: "requested info for account '".concat(account, "' but head was for account '").concat(accountHistory['account'], "'") | ||
}; | ||
} | ||
return { status: "ok" }; | ||
}; | ||
@@ -259,0 +424,0 @@ ; |
{ | ||
"name": "nano-account-crawler", | ||
"version": "1.0.27", | ||
"version": "2.0.0", | ||
"description": "Makes block iterators that make multiple RPC calls to Nano nodes if neccessary. Block confirmation height is checked so only confirmed blocks are iterated.", | ||
@@ -17,13 +17,14 @@ "repository": "https://github.com/Airtune/nano-account-crawler", | ||
"devDependencies": { | ||
"@bananocoin/bananojs": "^2.5.7", | ||
"@bananocoin/bananojs": "^2.8.7", | ||
"@types/mocha": "^9.1.0", | ||
"@types/node": "^16.11.26", | ||
"chai": "^4.3.6", | ||
"mocha": "^9.2.2", | ||
"ts-node": "^10.7.0", | ||
"typescript": "^4.6.3" | ||
"@types/node": "^18.15.5", | ||
"chai": "^4.3.7", | ||
"mocha": "^10.2.0", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^5.0.2" | ||
}, | ||
"dependencies": { | ||
"chai-as-promised": "^7.1.1", | ||
"node-fetch": "^2.6.7" | ||
} | ||
} |
@@ -1,2 +0,3 @@ | ||
import * as bananojs from '@bananocoin/bananojs'; | ||
import * as bananojsImport from '@bananocoin/bananojs'; | ||
const bananojs = bananojsImport as any; | ||
@@ -9,2 +10,3 @@ import { | ||
import { NanoAccountForwardCrawler } from './nano-account-forward-crawler'; | ||
import { IStatusReturn } from './status-return-interfaces'; | ||
@@ -31,10 +33,10 @@ | ||
[Symbol.asyncIterator](): AsyncIterator<INanoBlock> { | ||
const nanoAccountForwardIterator: AsyncIterator<INanoBlock> = this._nanoAccountForwardCrawler[Symbol.asyncIterator](); | ||
[Symbol.asyncIterator](): AsyncIterator<IStatusReturn<INanoBlock>> { | ||
const nanoAccountForwardIterator: AsyncIterator<IStatusReturn<INanoBlock>> = this._nanoAccountForwardCrawler[Symbol.asyncIterator](); | ||
let expectedPrevious: string = undefined; | ||
return { | ||
next: async () => { | ||
let iteratorResult: IteratorResult<INanoBlock>; | ||
next: async (): Promise<IteratorResult<IStatusReturn<INanoBlock>>> => { | ||
let iteratorResult: IteratorResult<IStatusReturn<INanoBlock>>; | ||
try { | ||
@@ -46,13 +48,21 @@ iteratorResult = await nanoAccountForwardIterator.next(); | ||
const block: INanoBlock = iteratorResult.value; | ||
const blockStatusReturn: IStatusReturn<INanoBlock> = iteratorResult.value; | ||
if (blockStatusReturn.status === "error") { | ||
return { value: blockStatusReturn, done: true }; | ||
} | ||
const block = blockStatusReturn.value; | ||
if (!block) { | ||
return { value: { status: "error", error_type: "MissingBlock", message: `expected block, got nothing` }, done: true }; | ||
} | ||
if (iteratorResult.done) { | ||
return { value: undefined, done: true }; | ||
return { value: { status: "ok", data: undefined }, done: true }; | ||
} | ||
// Verify block has expected value for previous | ||
if (typeof expectedPrevious === "string" && block.previous !== expectedPrevious) { | ||
throw Error(`InvalidChain: expectedPrevious: ${expectedPrevious} actual block.previous: ${block.previous} for block: ${block.hash}`); | ||
return { value: { status: "error", error_type: "InvalidChain", message: `expectedPrevious: ${expectedPrevious} actual block.previous: ${block.previous} for block: ${block.hash}` }, done: true }; | ||
} | ||
// Verify block hash | ||
@@ -68,3 +78,3 @@ const tempBlock = { | ||
if (calculatedHash !== block.hash) { | ||
throw Error(`InvalidChain: unexpected hash: ${block.hash} calculated: ${calculatedHash}`); | ||
return { value: { status: "error", error_type: "InvalidChain", message: `unexpected hash: ${block.hash} calculated: ${calculatedHash}` }, done: true }; | ||
} | ||
@@ -80,12 +90,12 @@ | ||
if (!bananojs.bananoUtil.isWorkValid(hashBytes, workBytes)) { | ||
throw Error(`InvalidChain: unable to verify work for: ${hash} with work: ${block.work}`); | ||
return { value: { status: "error", error_type: "InvalidChain", message: `unable to verify work for: ${hash} with work: ${block.work}` }, done: true }; | ||
} | ||
// Verify signature | ||
if (!bananojs.verify(block.hash, block.signature, this._publicKeyHex)) { | ||
throw Error(`InvalidChain: unable to verify signature for block: ${block.hash}`); | ||
return { value: { status: "error", error_type: "InvalidChain", message: `unable to verify signature for block: ${block.hash}` }, done: true }; | ||
} | ||
expectedPrevious = block.hash; | ||
return iteratorResult; | ||
@@ -95,2 +105,3 @@ } | ||
} | ||
} |
@@ -10,2 +10,3 @@ import { NanoNode } from './nano-node'; | ||
} from './nano-interfaces'; | ||
import { IStatusReturn } from './status-return-interfaces'; | ||
@@ -21,3 +22,3 @@ // Iterable that makes requests as required when looping through blocks in an account. | ||
private accountInfo: INanoAccountInfo; | ||
private confirmationHeight: BigInt; | ||
private confirmationHeight: bigint; | ||
private count: number; | ||
@@ -38,18 +39,66 @@ private _maxBlocksPerRequest: number; | ||
async initialize() { | ||
async initialize(): Promise<IStatusReturn<void>> { | ||
let accountHistoryStatusReturn: IStatusReturn<INanoAccountHistory>, accountInfoStatusReturn: IStatusReturn<INanoAccountInfo>; | ||
try { | ||
const historySegmentPromise = this.nanoNode.getBackwardHistory(this.account, this.head, "0", this.accountFilter, this._maxBlocksPerRequest); | ||
const accountInfoPromise = this.nanoNode.getAccountInfo(this.account); | ||
this.accountHistory = await historySegmentPromise; | ||
this.accountInfo = await accountInfoPromise; | ||
} catch(error) { | ||
throw(error); | ||
const accountInfoPromise = this.nanoNode.getAccountInfo(this.account); | ||
accountHistoryStatusReturn = await historySegmentPromise; | ||
accountInfoStatusReturn = await accountInfoPromise; | ||
} catch (error) { | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: `Unexpected error occurred while initializing: ${error}` | ||
}; | ||
} | ||
if (accountHistoryStatusReturn.status === "error") { | ||
return accountHistoryStatusReturn; | ||
} | ||
if (accountInfoStatusReturn.status === "error") { | ||
return accountInfoStatusReturn; | ||
} | ||
if (!accountInfoStatusReturn.value) { | ||
return { | ||
status: "error", | ||
error_type: "MissingAccountInfo", | ||
message: "Account info is missing after initializing" | ||
}; | ||
} | ||
this.accountHistory = accountHistoryStatusReturn.value; | ||
this.accountInfo = accountInfoStatusReturn.value; | ||
this.confirmationHeight = BigInt('' + this.accountInfo.confirmation_height); | ||
if (typeof(this.accountInfo?.confirmation_height || 0) !== 'string' || !this.accountInfo?.confirmation_height.match(/^\d+$/)) { | ||
return { | ||
status: "error", | ||
error_type: "MissingAccountInfo", | ||
message: `Expected confirmation_height to be a string containing an integer, got: ${this.accountInfo?.confirmation_height} of type ${typeof this.accountInfo?.confirmation_height}` | ||
}; | ||
} | ||
this.confirmationHeight = BigInt('' + (this.accountInfo.confirmation_height || 0)); | ||
return { | ||
status: "ok" | ||
}; | ||
} | ||
[Symbol.asyncIterator](): AsyncIterator<INanoBlock> { | ||
[Symbol.asyncIterator](): AsyncIterator<IStatusReturn<INanoBlock>> { | ||
if (this.accountHistory === null || this.accountInfo === null || this.confirmationHeight <= BigInt('0')) { | ||
throw Error('NanoAccountCrawlerError: not initialized. Did you call initialize() before iterating?'); | ||
return { | ||
async next(): Promise<IteratorResult<IStatusReturn<INanoBlock>>> { | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "NanoAccountCrawlerError", | ||
message: "not initialized. Did you call initialize() before iterating?", | ||
}, | ||
done: true, | ||
}; | ||
}, | ||
}; | ||
} | ||
@@ -80,18 +129,25 @@ | ||
return { | ||
next: async (): Promise<IteratorResult<INanoBlock>> => { | ||
if (endReached || historyIndex === undefined || history.length === 0 || historyIndex >= history.length) { | ||
return { value: undefined, done: true }; | ||
next: async (): Promise<IteratorResult<IStatusReturn<INanoBlock>>> => { | ||
if (endReached || history == undefined || historyIndex === undefined || history.length === 0 || historyIndex >= history.length) { | ||
return { value: { status: "ok", value: undefined }, done: true }; | ||
} | ||
const block: INanoBlock = history[historyIndex]; | ||
const blockHeight = BigInt('' + block.height); | ||
if (blockHeight <= BigInt('0')) { | ||
return { value: undefined, done: true }; | ||
return { value: { status: "ok", value: undefined }, done: true }; | ||
} | ||
if (typeof this.accountFilter === "undefined" && typeof nextHash === "string" && block.hash !== nextHash) { | ||
throw Error(`InvalidChain: Expected nextHash: ${nextHash}, got: ${block.hash}`); | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "InvalidChain", | ||
message: `Expected nextHash: ${nextHash}, got: ${block.hash}`, | ||
}, | ||
done: true, | ||
}; | ||
} | ||
historyIndex += 1; | ||
@@ -101,3 +157,3 @@ if (historyIndex >= history.length) { | ||
endReached = true; | ||
return { value: block, done: false }; | ||
return { value: { status: "ok", value: block }, done: false }; | ||
} else { | ||
@@ -107,5 +163,12 @@ // Guard against infinite loops and making too many RPC calls. | ||
if (rpcIterations > maxRpcIterations) { | ||
throw Error(`TooManyRpcIterations: Expected to fetch full history from nano node within ${maxRpcIterations} requests.`); | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "TooManyRpcIterations", | ||
message: `Expected to fetch full history from nano node within ${maxRpcIterations} requests.`, | ||
}, | ||
done: true, | ||
}; | ||
} | ||
// TODO: Edge case optimization that reduce count on each rpc iteration so last iteration doesn't include bloat blocks for large requests. | ||
@@ -115,4 +178,11 @@ let _accountHistory; | ||
_accountHistory = await this.nanoNode.getBackwardHistory(this.account, block.previous, "0", this.accountFilter, this._maxBlocksPerRequest); | ||
} catch(error) { | ||
throw(error); | ||
} catch (error) { | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "NanoNodeError", | ||
message: error.message, | ||
}, | ||
done: true, | ||
}; | ||
} | ||
@@ -123,3 +193,3 @@ history = _accountHistory.history; | ||
} | ||
nextHash = block.previous; | ||
@@ -129,8 +199,8 @@ | ||
endReached = true; | ||
return { value: block, done: false }; | ||
return { value: { status: "ok", value: block }, done: false }; | ||
} else { | ||
return { value: block, done: false }; | ||
return { value: { status: "ok", value: block }, done: false }; | ||
} | ||
} | ||
}; | ||
}, | ||
}; | ||
} | ||
@@ -137,0 +207,0 @@ |
@@ -11,2 +11,3 @@ import { NanoNode } from './nano-node'; | ||
} from './nano-interfaces'; | ||
import { IStatusReturn } from './status-return-interfaces'; | ||
@@ -22,3 +23,3 @@ // Iterable that makes requests as required when looping through blocks in an account. | ||
private _accountInfo: INanoAccountInfo; | ||
private _confirmationHeight: BigInt; | ||
private _confirmationHeight: bigint; | ||
private _count: number; | ||
@@ -41,22 +42,58 @@ private _maxBlocksPerRequest: number; | ||
async initialize() { | ||
async initialize(): Promise<IStatusReturn<void>> { | ||
try { | ||
const historySegmentPromise = this._nanoNode.getForwardHistory(this._account, this._head, this._offset, this._accountFilter, this._maxBlocksPerRequest); | ||
const accountInfoPromise = this._nanoNode.getAccountInfo(this._account); | ||
this._accountHistory = await historySegmentPromise; | ||
this._accountInfo = await accountInfoPromise; | ||
const historySegmentResponse = await historySegmentPromise; | ||
const accountInfoResponse = await accountInfoPromise; | ||
if (historySegmentResponse.status === "error") { | ||
return historySegmentResponse; | ||
} | ||
if (accountInfoResponse.status === "error") { | ||
return accountInfoResponse; | ||
} | ||
this._accountHistory = historySegmentResponse.value; | ||
this._accountInfo = accountInfoResponse.value; | ||
} catch(error) { | ||
throw(error); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: `Unexpected error occurred while initializing: ${error}` | ||
}; | ||
} | ||
if (!this._accountInfo) { | ||
return { | ||
status: "error", | ||
error_type: "MissingAccountInfo", | ||
message: "Account info is missing after initialization" | ||
}; | ||
} | ||
this._confirmationHeight = BigInt('' + this._accountInfo.confirmation_height); | ||
return { status: "ok" }; | ||
} | ||
[Symbol.asyncIterator](): AsyncIterator<INanoBlock> { | ||
[Symbol.asyncIterator](): AsyncIterator<IStatusReturn<INanoBlock>> { | ||
if (this._accountHistory === undefined || this._accountInfo === undefined || this._confirmationHeight <= BigInt('0')) { | ||
throw Error('NanoAccountCrawlerError: not initialized. Did you call initialize() before iterating?'); | ||
return { | ||
async next(): Promise<IteratorResult<IStatusReturn<INanoBlock>>> { | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "NanoAccountCrawlerError", | ||
message: "not initialized. Did you call initialize() before iterating?", | ||
}, | ||
done: true, | ||
}; | ||
}, | ||
}; | ||
} | ||
let rpcIterations = 0; | ||
let history: INanoBlock[] = this._accountHistory.history; | ||
@@ -66,9 +103,9 @@ let historyIndex: number = 0; | ||
let endReached = false; | ||
const startBlockHeight: (boolean|bigint) = history[historyIndex] && BigInt(history[historyIndex].height); | ||
return { | ||
next: async (): Promise<IteratorResult<INanoBlock>> => { | ||
next: async (): Promise<IteratorResult<IStatusReturn<INanoBlock>>> => { | ||
if (endReached || history.length === 0 || historyIndex >= history.length) { | ||
return { value: undefined, done: true }; | ||
return { value: { status: "done", done: true }, done: true }; | ||
} | ||
@@ -80,11 +117,21 @@ | ||
if (blockHeight <= BigInt('0') || blockHeight > this._confirmationHeight) { | ||
return { value: undefined, done: true }; | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "InvalidBlockHeightError", | ||
message: `Block height ${blockHeight} is outside valid range (0, ${this._confirmationHeight}]`, | ||
}, | ||
done: true, | ||
}; | ||
} | ||
if (blockHeight <= BigInt('0') || blockHeight > this._confirmationHeight) { | ||
return { value: undefined, done: true }; | ||
} | ||
if (typeof this._accountFilter === "undefined" && typeof previous === "string" && block.previous !== previous) { | ||
throw Error(`InvalidChain: Expected previous: ${previous} got ${block.previous} for ${block.hash}`); | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "InvalidChainError", | ||
message: `Expected previous: ${previous}, got ${block.previous} for ${block.hash}`, | ||
}, | ||
done: true, | ||
}; | ||
} | ||
@@ -101,3 +148,10 @@ | ||
if (rpcIterations > this._maxRpcIterations) { | ||
throw Error(`TooManyRpcIterations: Expected to fetch full history from nano node within ${this._maxRpcIterations} requests.`); | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "TooManyRpcIterationsError", | ||
message: `Expected to fetch full history from nano node within ${this._maxRpcIterations} requests.`, | ||
}, | ||
done: true, | ||
}; | ||
} | ||
@@ -109,3 +163,10 @@ // TODO: Edge case optimization that reduce count on each rpc iteration so last iteration doesn't include bloat blocks for large requests. | ||
} catch(error) { | ||
throw(error); | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "RpcError", | ||
message: error.message, | ||
}, | ||
done: true, | ||
}; | ||
} | ||
@@ -119,11 +180,19 @@ history = _accountHistory.history; | ||
if (this.exceededCount(startBlockHeight, blockHeight + BigInt(1))) { | ||
return { value: undefined, done: true }; | ||
return { | ||
value: { | ||
status: "error", | ||
error_type: "CountExceededError", | ||
message: `Reached maximum count (${this._count}) before reaching confirmation height (${this._confirmationHeight})`, | ||
}, | ||
done: true, | ||
}; | ||
} else if (this.reachedCount(startBlockHeight, blockHeight + BigInt(1))) { | ||
endReached = true; | ||
return { value: block, done: false }; | ||
return { value: { status: "ok", value: block }, done: false }; | ||
} { | ||
return { value: block, done: false }; | ||
return { value: { status: "ok", value: block }, done: false }; | ||
} | ||
} | ||
}; | ||
} | ||
@@ -130,0 +199,0 @@ |
@@ -0,1 +1,3 @@ | ||
import { IStatusReturn } from "./status-return-interfaces"; | ||
export type TAccount = `${'ban_' | 'nano_'}${string}`; | ||
@@ -43,3 +45,3 @@ export type TNanoBlockType = "state"; | ||
export interface INanoAccountForwardIterable extends AsyncIterable<INanoBlock> {} | ||
export interface INanoAccountBackwardIterable extends AsyncIterable<INanoBlock> {} | ||
export interface INanoAccountForwardIterable extends AsyncIterable<IStatusReturn<INanoBlock>> {} | ||
export interface INanoAccountBackwardIterable extends AsyncIterable<IStatusReturn<INanoBlock>> {} |
@@ -8,2 +8,3 @@ import { | ||
} from './nano-interfaces'; | ||
import { IErrorReturn, IStatusReturn } from './status-return-interfaces'; | ||
@@ -19,3 +20,3 @@ export class NanoNode { | ||
async getForwardHistory(account: TAccount, head: TBlockHash = undefined, offset: TStringBigInt = "0", account_filter: TAccount[] = undefined, count: number = undefined, max_retries: number = 3): Promise<INanoAccountHistory> { | ||
async getForwardHistory(account: TAccount, head: TBlockHash = undefined, offset: TStringBigInt = "0", account_filter: TAccount[] = undefined, count: number = undefined, max_retries: number = 3): Promise<IStatusReturn<INanoAccountHistory>> { | ||
const request: any = { | ||
@@ -38,25 +39,59 @@ action: 'account_history', | ||
} | ||
let retries: number = 0; | ||
let response: INanoAccountHistory = undefined; | ||
while (true) { | ||
let errorResponse: IErrorReturn = undefined; | ||
while (retries < max_retries) { | ||
retries += 1; | ||
try { | ||
response = await this.jsonRequest(request); | ||
this.validateIsAccountHistory(account, response); | ||
this.validateAccount(account, response); | ||
const responseStatus: IStatusReturn<any> = await this.jsonRequest(request); | ||
if (responseStatus.status === "error") { | ||
errorResponse = responseStatus; | ||
continue; | ||
} | ||
response = responseStatus.value; | ||
const isAccountHistoryValid: IStatusReturn<void> = await this.validateIsAccountHistory(account, response); | ||
const isAccountValid: IStatusReturn<void> = await this.validateAccount(account, response); | ||
if (isAccountHistoryValid.status === "error") { | ||
errorResponse = isAccountHistoryValid; | ||
continue; | ||
} | ||
if (isAccountValid.status === "error") { | ||
errorResponse = isAccountValid; | ||
continue; | ||
} | ||
errorResponse = undefined; | ||
break; | ||
} catch (error) { | ||
if (retries >= max_retries || !error.message.match(/NanoNodeError:/)) { | ||
throw error; | ||
} | ||
errorResponse = { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: `Unexpected error occurred while getting forward history: ${error}` | ||
}; | ||
continue; | ||
} | ||
} | ||
return response; | ||
if (!response) { | ||
errorResponse = { | ||
status: "error", | ||
error_type: "MissingNanoNodeResponse", | ||
message: "NanoNode response is missing after getting forward history" | ||
}; | ||
} | ||
if (errorResponse) { | ||
return errorResponse; | ||
} | ||
return { | ||
status: "ok", | ||
value: response | ||
}; | ||
} | ||
async getBackwardHistory(account: TAccount, head: TBlockHash = undefined, offset: TStringBigInt = "0", account_filter: TAccount[] = undefined, count: number = undefined, max_retries: number = 3): Promise<INanoAccountHistory> { | ||
async getBackwardHistory(account: TAccount, head: TBlockHash = undefined, offset: TStringBigInt = "0", account_filter: TAccount[] = undefined, count: number = undefined, max_retries: number = 3): Promise<IStatusReturn<INanoAccountHistory>> { | ||
const request: any = { | ||
@@ -77,25 +112,59 @@ action: 'account_history', | ||
} | ||
let retries: number = 0; | ||
let response: INanoAccountHistory = undefined; | ||
while (true) { | ||
let errorResponse: IErrorReturn = undefined; | ||
while (retries < max_retries) { | ||
retries += 1; | ||
try { | ||
response = await this.jsonRequest(request); | ||
this.validateIsAccountHistory(account, response); | ||
this.validateAccount(account, response); | ||
const responseStatus: IStatusReturn<any> = await this.jsonRequest(request); | ||
if (responseStatus.status === "error") { | ||
errorResponse = responseStatus; | ||
continue; | ||
} | ||
response = responseStatus.value; | ||
const isAccountHistoryValid = await this.validateIsAccountHistory(account, response); | ||
const isAccountValid = await this.validateAccount(account, response); | ||
if (isAccountHistoryValid.status === "error") { | ||
errorResponse = isAccountHistoryValid; | ||
continue; | ||
} | ||
if (isAccountValid.status === "error") { | ||
errorResponse = isAccountValid; | ||
continue; | ||
} | ||
errorResponse = undefined; | ||
break; | ||
} catch (error) { | ||
if (retries >= max_retries || !error.message.match(/NanoNodeError:/)) { | ||
throw error; | ||
} | ||
errorResponse = { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: `Unexpected error occurred while getting backward history: ${error}` | ||
}; | ||
continue; | ||
} | ||
} | ||
return response; | ||
if (!response) { | ||
errorResponse = { | ||
status: "error", | ||
error_type: "MissingNanoNodeResponse", | ||
message: "NanoNode response is missing after getting backward history" | ||
}; | ||
} | ||
if (errorResponse) { | ||
return errorResponse; | ||
} | ||
return { | ||
status: "ok", | ||
value: response | ||
}; | ||
} | ||
async getAccountInfo(account: TAccount): Promise<INanoAccountInfo> { | ||
async getAccountInfo(account: TAccount): Promise<IStatusReturn<INanoAccountInfo>> { | ||
const request = { | ||
@@ -105,15 +174,38 @@ action: 'account_info', | ||
}; | ||
let response: INanoAccountInfo; | ||
try { | ||
response = await this.jsonRequest(request); | ||
this.validateIsAccountInfo(account, response); | ||
} catch(error) { | ||
throw(error); | ||
const responseStatus: IStatusReturn<any> = await this.jsonRequest(request); | ||
if (responseStatus.status === "error") { | ||
return responseStatus; | ||
} | ||
response = responseStatus.value; | ||
const isAccountValid = await this.validateIsAccountInfo(account, response); | ||
if (isAccountValid.status === "error") { | ||
return isAccountValid; | ||
} | ||
} catch (error) { | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: `Unexpected error occurred while getting account info: ${error}` | ||
}; | ||
} | ||
return response; | ||
if (!response) { | ||
return { | ||
status: "error", | ||
error_type: "MissingNanoNodeResponse", | ||
message: "NanoNode response is missing after getting account info" | ||
}; | ||
} | ||
return { | ||
status: "ok", | ||
value: response | ||
}; | ||
} | ||
hasMoreHistory(history: any, confirmationHeight: BigInt): boolean { | ||
hasMoreHistory(history: any, confirmationHeight: bigint): boolean { | ||
return !this.historyIsEmpty(history) && this.historyFrontierIsBehind(history, confirmationHeight); | ||
@@ -126,3 +218,3 @@ } | ||
async jsonRequest(jsonRequest: any): Promise<any> { | ||
async jsonRequest(jsonRequest: any): Promise<IStatusReturn<any>> { | ||
const request = { | ||
@@ -134,10 +226,41 @@ method: 'POST', | ||
}, | ||
body: JSON.stringify(jsonRequest) | ||
body: undefined | ||
}; | ||
let response, jsonResponse; | ||
response = await this.fetch(this.nodeApiUrl, request).catch((error) => { throw(error); }); | ||
jsonResponse = await response.json().catch((error) => { throw(error); }); | ||
return jsonResponse; | ||
try { | ||
request.body = JSON.stringify(jsonRequest); | ||
} catch (error) { | ||
return { | ||
status: "error", | ||
error_type: "JsonStringifyError", | ||
message: `Error occurred while converting JSON request to string: ${error}` | ||
}; | ||
} | ||
try { | ||
response = await this.fetch(this.nodeApiUrl, request); | ||
jsonResponse = await response.json(); | ||
} catch (error) { | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedError", | ||
message: `Unexpected error occurred while making JSON request: ${error}` | ||
}; | ||
} | ||
if (!jsonResponse) { | ||
return { | ||
status: "error", | ||
error_type: "MissingJsonResponse", | ||
message: "JSON response is missing after making JSON request" | ||
}; | ||
} | ||
return { | ||
status: "ok", | ||
value: jsonResponse | ||
}; | ||
} | ||
@@ -148,49 +271,92 @@ ///////////// | ||
private validateIsAccountHistory(account: TAccount, accountHistory: INanoAccountHistory) { | ||
private validateIsAccountHistory(account: TAccount, accountHistory: INanoAccountHistory): IStatusReturn<void> { | ||
if (typeof(accountHistory) !== 'object') { | ||
throw Error(`UnexpectedNanoNodeResponse: Unexpected accountHistory. Expected type to be 'object', got: ${typeof(accountHistory)} for ${account}`); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: `Unexpected accountHistory. Expected type to be 'object', got: ${typeof(accountHistory)} for ${account}` | ||
}; | ||
} | ||
if (accountHistory.hasOwnProperty('error')) { | ||
throw Error(`NanoNodeError: ${accountHistory.error} for ${account}`); | ||
return { | ||
status: "error", | ||
error_type: "NanoNodeError", | ||
message: `${accountHistory.error} for ${account}` | ||
}; | ||
} | ||
if (typeof(accountHistory.account) !== 'string') { | ||
throw Error(`UnexpectedNanoNodeResponse: Unexpected accountHistory.account. Expected type to be 'string', got: ${typeof(accountHistory.account)} for ${account}`); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: `Unexpected accountHistory.account. Expected type to be 'string', got: ${typeof(accountHistory.account)} for ${account}` | ||
}; | ||
} | ||
if (!accountHistory.hasOwnProperty('history')) { | ||
throw Error(`UnexpectedNanoNodeResponse: accountHistory doesn't have property 'history' for ${account}`); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: `accountHistory doesn't have property 'history' for ${account}` | ||
}; | ||
} | ||
const _prototype: string = Object.prototype.toString.call(accountHistory.history); | ||
if (!(_prototype === '[object String]' || _prototype === '[object Array]')) { | ||
throw Error(`UnexpectedNanoNodeResponse: accountHistory.history not of type array or string. Got: ${_prototype} for ${account}`); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: `accountHistory.history not of type array or string. Got: ${_prototype} for ${account}` | ||
}; | ||
} | ||
return { status: "ok" }; | ||
} | ||
private validateIsAccountInfo(account: TAccount, accountInfo: INanoAccountInfo) { | ||
private validateIsAccountInfo(account: TAccount, accountInfo: INanoAccountInfo): IStatusReturn<void> { | ||
if (typeof(accountInfo) !== 'object') { | ||
throw Error(`UnexpectedNanoNodeResponse: Unexpected accountInfo. Expected type to be 'object', got: '${typeof(accountInfo)}' for ${account}`); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: `Unexpected accountInfo. Expected type to be 'object', got: '${typeof(accountInfo)}' for ${account}` | ||
}; | ||
} | ||
if (accountInfo.hasOwnProperty('error')) { | ||
throw Error(`NanoNodeError: ${accountInfo.error} for ${account}`); | ||
return { | ||
status: "error", | ||
error_type: "NanoNodeError", | ||
message: `${accountInfo.error} for ${account}` | ||
}; | ||
} | ||
if (typeof(accountInfo['confirmation_height']) !== 'string') { | ||
throw Error(`UnexpectedNanoNodeResponse: Unexpected accountInfo['confirmation_height']. Expected type to be 'string', got: ${typeof(accountInfo['confirmation_height'])} for ${account}`); | ||
return { | ||
status: "error", | ||
error_type: "UnexpectedNanoNodeResponse", | ||
message: `Unexpected accountInfo['confirmation_height']. Expected type to be 'string', got: ${typeof(accountInfo['confirmation_height'])} for ${account}` | ||
}; | ||
} | ||
return { status: "ok" }; | ||
} | ||
private validateAccount(account: string, accountHistory) { | ||
private validateAccount(account: string, accountHistory): IStatusReturn<void> { | ||
// Warning: Nano node returns history for templatePrevious block ignoring if there's an issuer account mismatch. | ||
if (account !== accountHistory['account']) { | ||
throw Error(`AccountMismatch: requested info for account '${account}' but head was for account '${accountHistory['account']}'`); | ||
return { | ||
status: "error", | ||
error_type: "AccountMismatch", | ||
message: `requested info for account '${account}' but head was for account '${accountHistory['account']}'` | ||
}; | ||
} | ||
return { status: "ok" }; | ||
}; | ||
private historyFrontierIsBehind(history: any, confirmationHeight: BigInt): boolean { | ||
private historyFrontierIsBehind(history: any, confirmationHeight: bigint): boolean { | ||
const historyLastBlock = history[history.length - 1]; | ||
const historyHeight: BigInt = BigInt('' + historyLastBlock['height']); | ||
const historyHeight: bigint = BigInt('' + historyLastBlock['height']); | ||
@@ -197,0 +363,0 @@ return historyHeight > BigInt('0') && historyHeight < confirmationHeight |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
118445
26
2009
2
+ Addedchai-as-promised@^7.1.1
+ Addedassertion-error@2.0.1(transitive)
+ Addedchai@5.2.0(transitive)
+ Addedchai-as-promised@7.1.2(transitive)
+ Addedcheck-error@1.0.32.1.1(transitive)
+ Addeddeep-eql@5.0.2(transitive)
+ Addedget-func-name@2.0.2(transitive)
+ Addedloupe@3.1.3(transitive)
+ Addedpathval@2.0.0(transitive)