src/simba/simbabase.js
import {pollWrapper} from "poll-js";
import {
MissingMetadataException,
NotImplementedException,
BadMetadataException,
MethodCallValidationMetadataException,
GetTransactionsException
} from '../exceptions';
import PagedResponse from "./pagedresponse";
/**
* @interface
* Base class for libsimba API Interaction implementations
*/
export default class SimbaBase {
/**
* Base class for libsimba API Interaction implementations
* @param {string} endpoint - The endpoint of the API
* @param {Wallet} [wallet] - an optional {@link Wallet} instance
*/
constructor(endpoint, wallet) {
if (!endpoint.endsWith('/')) {
this.endpoint = `${endpoint}/`;
} else {
this.endpoint = endpoint;
}
this.wallet = wallet;
this.metadata = {};
if (this.constructor === SimbaBase) {
// Error Type 1. Abstract class can not be constructed.
throw new TypeError('Can not construct abstract class.');
}
if (this.initialize === SimbaBase.prototype.initialize) {
throw new NotImplementedException('Please implement abstract method callMethod.');
}
if (this.callMethod === SimbaBase.prototype.callMethod) {
throw new NotImplementedException('Please implement abstract method callMethod.');
}
if (this.getTransactionStatus === SimbaBase.prototype.getTransactionStatus) {
throw new NotImplementedException('Please implement abstract method getTransactionStatus.');
}
if (this.checkTransactionStatusFromObject === SimbaBase.prototype.checkTransactionStatusFromObject) {
throw new NotImplementedException('Please implement abstract method checkTransactionStatusFromObject.');
}
if (this.checkTransactionDone === SimbaBase.prototype.checkTransactionDone) {
throw new NotImplementedException('Please implement abstract method checkTransactionDone.');
}
if (this.checkTransactionStatus === SimbaBase.prototype.checkTransactionStatus) {
throw new NotImplementedException('Please implement abstract method checkTransactionStatus.');
}
if (this.getBalance === SimbaBase.prototype.getBalance) {
throw new NotImplementedException('Please implement abstract method getBalance.');
}
if (this.addFunds === SimbaBase.prototype.addFunds) {
throw new NotImplementedException('Please implement abstract method addFunds.');
}
if (this.getMethodTransactions === SimbaBase.prototype.getMethodTransactions) {
throw new NotImplementedException('Please implement abstract method getMethodTransactions.');
}
if (this.getTransaction === SimbaBase.prototype.getTransaction) {
throw new NotImplementedException('Please implement abstract method getTransaction.');
}
if (this.getTransactions === SimbaBase.prototype.getTransactions) {
throw new NotImplementedException('Please implement abstract method getTransactions.');
}
if (this.getBundleMetadataForTransaction === SimbaBase.prototype.getBundleMetadataForTransaction) {
throw new NotImplementedException('Please implement abstract method getBundleMetadataForTransaction.');
}
if (this.getBundleForTransaction === SimbaBase.prototype.getBundleForTransaction) {
throw new NotImplementedException('Please implement abstract method getBundleForTransaction.');
}
if (this.getFileFromBundleForTransaction === SimbaBase.prototype.getFileFromBundleForTransaction) {
throw new NotImplementedException('Please implement abstract method getFileFromBundleForTransaction.');
}
if (this.getFileFromBundleByNameForTransaction === SimbaBase.prototype.getFileFromBundleByNameForTransaction) {
throw new NotImplementedException('Please implement abstract method getFileFromBundleByNameForTransaction.');
}
if (this.sendTransactionRequest === SimbaBase.prototype.sendTransactionRequest) {
throw new NotImplementedException('Please implement abstract method sendTransactionRequest.');
}
}
/**
* @abstract
* (Abstract) Perform any asynchronous actions needed to initialise this class
*/
initialize() {
throw new NotImplementedException('SimbaBase.initialize Not Implemented');
}
/**
* @abstract
* (Abstract) Call a method on the API
* @param {string} method - the method to call
* @param {Object} parameters - the parameters for the method
* @return {Promise} - a promise resolving with the transaction details
*/
callMethod(method, parameters) {
throw new NotImplementedException('SimbaBase.callMethod Not Implemented');
}
/**
* @abstract
* (Abstract) Gets a paged list of transactions for the method
* @param {string} method - The method
* @param {Object} parameters - The query parameters
*/
getMethodTransactions(method, parameters) {
throw new NotImplementedException('SimbaBase.callMethod Not Implemented');
}
/**
* @abstract
* (Abstract) Gets a specific transaction
* @param {string} transactionIdOrHash - Either a transaction ID or a transaction hash
* @returns {Promise<Object>} - The transaction
*/
getTransaction(transactionIdOrHash) {
throw new NotImplementedException('SimbaBase.getTransaction Not Implemented');
}
/**
* @abstract
* (Abstract) Gets a the bundle metadata for a transaction
* @param {string} transactionIdOrHash - Either a transaction ID or a transaction hash
* @returns {Promise<Object>} - The bundle metadata
*/
getBundleMetadataForTransaction(transactionIdOrHash) {
throw new NotImplementedException('SimbaBase.getTransaction Not Implemented');
}
/**
* @abstract
* (Abstract) Gets the bundle for a transaction
* @param {string} transactionIdOrHash - Either a transaction ID or a transaction hash
* @param {boolean} stream - If true, returns a {@link ReadableStream}, otherwise returns a {@link Blob}
* @returns {Promise<ReadableStream|Blob>} - The bundle
*/
getBundleForTransaction(transactionIdOrHash, stream) {
throw new NotImplementedException('SimbaBase.getTransaction Not Implemented');
}
/**
* @abstract
* (Abstract) Gets a file from the bundle for a transaction
* @param {string} transactionIdOrHash - Either a transaction ID or a transaction hash
* @param {number} fileIdx - The index of the file in the bundle metadata
* @param {boolean} stream - If true, returns a {@link ReadableStream}, otherwise returns a {@link Blob}
* @returns {Promise<ReadableStream|Blob>} - The file
*/
getFileFromBundleForTransaction(transactionIdOrHash, fileIdx, stream) {
throw new NotImplementedException('SimbaBase.getTransaction Not Implemented');
}
/**
* @abstract
* (Abstract) Gets a file from the bundle for a transaction
* @param {string} transactionIdOrHash - Either a transaction ID or a transaction hash
* @param {string} fileName - The name of the file in the bundle metadata
* @param {boolean} stream - If true, returns a {@link ReadableStream}, otherwise returns a {@link Blob}
* @returns {Promise<ReadableStream|Blob>} - The file
*/
getFileFromBundleByNameForTransaction(transactionIdOrHash, fileName, stream) {
throw new NotImplementedException('SimbaBase.getTransaction Not Implemented');
}
/**
* @abstract
* (Abstract) Gets a paged list of transactions
* @param {Object} parameters - The query parameters
* @returns {Promise<PagedResponse>} - A response wrapped in a {@link PagedResponse} helper
*/
getTransactions(parameters) {
throw new NotImplementedException('SimbaBase.callMethod Not Implemented');
}
/**
* @abstract
* Internal function for sending transaction GET requests
* @param {URL} url - The URL
* @returns {Promise<PagedResponse>} - A response wrapped in a {@link PagedResponse} helper
*/
async sendTransactionRequest(url){
throw new NotImplementedException('SimbaBase.sendTransactionRequest Not Implemented');
}
/**
* @abstract
* (Abstract) Call a method on the API with files
* @param {string} method - the method to call
* @param {Object} parameters - the parameters for the method
* @param {Array<Blob|File>} files - the files
* @return {Promise<Object>} - a promise resolving with the transaction details
*/
callMethodWithFile(method, parameters, files) {
throw new NotImplementedException('SimbaBase.callMethod Not Implemented');
}
/**
* @abstract
* (Abstract) Get the status of a transaction by ID
* @param {string} txnId - the transaction ID
* @return {Promise<Object>} - a promise resolving with the transaction details
*/
getTransactionStatus(txnId){
throw new NotImplementedException('SimbaBase.getTransactionStatus Not Implemented');
}
/**
* @abstract
* @private
* (Abstract) Gets the status of a transaction
* @param {Object} txn - a transaction object
* @return {Object} - an object with status details
*/
checkTransactionStatusFromObject(txn){
throw new NotImplementedException('SimbaBase.checkTransactionStatusFromObject Not Implemented');
}
/**
* @abstract
* @private
* (Abstract) Check if the transaction is complete
* @param {Object} txn - the transaction object
* @return {boolean} - is the transaction complete
*/
checkTransactionDone(txn){
throw new NotImplementedException('SimbaBase.checkTransactionDone Not Implemented');
}
/**
* @abstract
* (Abstract) Gets the status of a transaction by ID
* @param {string} txnId - a transaction ID
* @return {Object} - an object with status details
*/
checkTransactionStatus(txnId){
throw new NotImplementedException('SimbaBase.checkTransactionStatus Not Implemented');
}
/**
* @abstract
* (Abstract) Get the balance for the attached Wallet
* @return {Promise<Object>} - the balance
*/
getBalance(){
throw new NotImplementedException('SimbaBase.getBalance Not Implemented');
}
/**
* @abstract
* (Abstract) Add funds to the attached Wallet.
* Please check the output of this method. It is of the form
* ```
* {
* txnId: null,
* faucet_url: null,
* poa: true
* }
* ```
*
* If successful, txnId will be populated.
* If the network is PoA, then poa will be true, and txnId will be null
* If the faucet for the network is external (e.g. Rinkeby, Ropsten, etc), then txnId will be null,
* and faucet_url will be populated with a URL. You should present this URL to your users to direct them
* to request funds there.
* @return {Promise<Object>} - details of the txn
*/
addFunds(){
throw new NotImplementedException('SimbaBase.addFunds Not Implemented');
}
/**
* Returns an object with 'future' and 'cancel' keys.
* future is the promise to listen on for the response or an error.
* cancel is a function - call it to cancel the polling.
* @param {string} txnId - the transaction ID
* @param {number} [pollInterval=5000] - the interval in ms for polling
*/
waitForSuccessOrError(txnId, pollInterval){
if(!pollInterval) pollInterval = 5000;
return pollWrapper({
request: () => this.getTransactionStatus(txnId),
pollingPeriod: pollInterval, // ms
shouldStop: (txn)=> this.checkTransactionDone(this.checkTransactionStatusFromObject(txn))
});
}
/**
* Set the wallet
* @param {Wallet} wallet - the wallet
*/
setWallet(wallet){
this.wallet = wallet;
}
/**
* Set the API Key to authenticate calls
* @param {string} apiKey - the API Key
*/
setApiKey(apiKey){
this.apiKey = apiKey;
}
/**
* Set the API Key to authenticate management calls
* @param {string} managementKey - the management API Key
*/
setManagementKey(managementKey){
this.managementKey = managementKey;
}
/**
* @private
* Get API Call auth headers
* @returns {{APIKEY: *, "Content-Type": string}}
*/
apiAuthHeaders() {
return {
APIKEY: this.apiKey,
}
}
/**
* @private
* Get management API Call auth headers
* @returns {{APIKEY: *, "Content-Type": string}}
*/
managementAuthHeaders() {
return {
APIKEY: this.managementKey,
}
}
/**
* @private
* Validate the method call against the app metadata
* @param {string} methodName - the methods name
* @param {Object} parameters - the parameters for the method call
* @param {Array} [files] - Optional array of files
* @returns {boolean}
* @throws {MissingMetadataException} - App Metadata not yet retrieved
* @throws {BadMetadataException} - App Metadata doesn't have methods
* @throws {MethodCallValidationMetadataException} - Method call fails validation
*/
validateCall(methodName, parameters, files){
if (!this.metadata) {
throw new MissingMetadataException("App Metadata not yet retrieved");
}
if (!this.metadata.methods) {
throw new BadMetadataException("App Metadata doesn't have methods!");
}
if(!(methodName in this.metadata.methods)){
throw new MethodCallValidationMetadataException(`Method "${methodName}" not found`);
}
let methodMeta = this.metadata.methods[methodName];
if(files && !('_files' in methodMeta.parameters)){
throw new MethodCallValidationMetadataException(`Method "${methodName}" does not accept files`);
}
if(parameters['_files']){
throw new MethodCallValidationMetadataException(`Files must not be passed in through the parameters argument`);
}
if(files){
for(let i = 0; i < files.length; i++){
if(typeof window !== 'undefined' && typeof window.document !== 'undefined'){
if(!(files[i] instanceof Blob) && !(files[i] instanceof File)){
throw new MethodCallValidationMetadataException(`Item at position ${i} of "files" is not a Blob or a File`);
}
}else{
if(!(files[i] instanceof ArrayBuffer)
&& !(files[i] instanceof Uint8Array)
&& !(files[i].constructor.name === 'ReadStream')
&& !(files[i] instanceof Buffer)){
throw new MethodCallValidationMetadataException(`Item at position ${i} of "files" is not a Blob or a File`);
}
}
}
}
let paramNames = Object.keys(parameters);
paramNames.forEach((key)=>{
if(!(key in methodMeta.parameters)){
throw new MethodCallValidationMetadataException(`Parameter "${key}" is not valid for method "${methodName}"`);
}
//TODO: Type checks
});
//We expect _files to be missing, as it's passed separately
let missing = Object.keys(methodMeta.parameters).filter((key)=>paramNames.indexOf(key) < 0 && key !== '_files');
if(missing.length){
throw new MethodCallValidationMetadataException(`Parameters [${missing.join(',')}] not present for method "${methodName}"`);
}
return true;
}
/**
* @private
* Validate the transaction list call against the app metadata
* @param {string} methodName - the methods name
* @param {Object} parameters - the parameters for the query
* @returns {boolean}
* @throws {MissingMetadataException} - App Metadata not yet retrieved
* @throws {BadMetadataException} - App Metadata doesn't have methods
* @throws {MethodCallValidationMetadataException} - Method call fails validation
*/
validateGetCall(methodName, parameters){
if (!this.metadata) {
throw new MissingMetadataException("App Metadata not yet retrieved");
}
if (!this.metadata.methods) {
throw new BadMetadataException("App Metadata doesn't have methods!");
}
if(!(methodName in this.metadata.methods)){
throw new MethodCallValidationMetadataException(`Method "${methodName}" not found`);
}
return true;
}
/**
* @private
* Validate the transaction list call against the app metadata
* @returns {boolean}
* @throws {MissingMetadataException} - App Metadata not yet retrieved
* @throws {BadMetadataException} - App Metadata doesn't have methods
*/
validateAnyGetCall(){
if (!this.metadata) {
throw new MissingMetadataException("App Metadata not yet retrieved");
}
if (!this.metadata.methods) {
throw new BadMetadataException("App Metadata doesn't have methods!");
}
return true;
}
}