UNPKG

@simbachain/libsimba-js

Version:

libsimba-js is a library simplifying the use of SIMBAChain APIs. We aim to abstract away the various blockchain concepts, reducing the necessary time needed to get to working code.

574 lines (500 loc) 18.5 kB
import SimbaBase from './simbabase'; import { WalletNotFoundException, MissingMetadataException, GenerateTransactionException, SubmitTransactionException, TransactionStatusCheckException, NotImplementedException, GetTransactionsException, GetRequestException, PostRequestException, RetriesExceededException } from '../exceptions'; import PagedResponse from "./pagedresponse"; var FormData = require('form-data'); import axios from 'axios'; /** * libsimba API Interaction for Simbachain.com */ export default class Simbachain extends SimbaBase { /** * libsimba API Interaction for Simbachain.com * @param {string} endpoint - The endpoint of the API * @param {Wallet} [wallet] - an optional {@link Wallet} instance */ constructor(endpoint, wallet) { super(endpoint, wallet); } /** * @private * Perform asynchronous actions needed to initialise this class */ async initialize() { let response = await axios.request({ url: `${this.endpoint}?format=openapi`, responseType: 'json' }); let swagger = response.data; if ('info' in swagger && 'x-simba-attrs' in swagger.info) { this.metadata = swagger.info['x-simba-attrs']; } } /** * @override * 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 */ async callMethod(method, parameters) { if (!this.wallet) { throw new WalletNotFoundException("No Wallet found"); } this.validateCall(method, parameters); let formData = new FormData(); let address = await this.wallet.getAddress(); formData.append('from', address); for (let [key, value] of Object.entries(parameters)) { formData.append(key, value); } return this.sendMethodRequest(method, formData); } /** * @override * 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) { return axios.request({ url: `${this.endpoint}transaction/${txnId}/`, method: 'GET', headers: this.apiAuthHeaders(), responseType: 'json' }) .then(async (response) => { return response.data; }) .catch(error=>{ throw new TransactionStatusCheckException(error); }); } /** * @override * @private * Gets the status of a transaction * @param {Object} txn - a transaction object * @return {Object} - an object with status details */ checkTransactionStatusFromObject(txn) { let ret = { status: '', transaction_hash: '' }; if (txn.transaction_hash) { ret.transaction_hash = txn.transaction_hash; } if (txn.error) { ret.status = 'error'; ret.error = txn.error; ret.error_details = txn.error_details; } else if (!txn.receipt) { ret.status = 'pending'; } else { ret.status = 'success'; } return ret; } /** * @override * @private * Check if the transaction is complete * @param {Object} txn - the transaction object * @return {boolean} - is the transaction complete */ checkTransactionDone(txn) { return txn.status !== 'pending'; } /** * @override * @private * Gets the status of a transaction by ID * @param {string} txnId - a transaction ID * @return {Object} - an object with status details */ checkTransactionStatus(txnId) { return this.getTransactionStatus(txnId) .then(this.checkTransactionStatusFromObject); } /** * @override * Get the balance for the attached Wallet * @return {Promise<Object>} - the balance */ async getBalance() { if (!this.metadata) { throw new MissingMetadataException("App Metadata not yet retrieved"); } if (!this.wallet) { throw new WalletNotFoundException("No Wallet found"); } if (this.metadata.poa) { return Promise.resolve({ amount: -1, currency: "", poa: true }); } let address = await this.wallet.getAddress(); let response = await axios.request( { url: `${this.endpoint}balance/${address}/`, method: 'GET', headers: Object.assign({'Content-Type':'application/json'},this.apiAuthHeaders()), responseType: 'json' } ).catch(error=>{ throw new GetRequestException(error); }); return Promise.resolve({ ...response.data, poa: false }); } /** * @override * 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 */ async addFunds() { if (!this.metadata) { throw new MissingMetadataException("App Metadata not yet retrieved"); } if (!this.wallet) { throw new WalletNotFoundException("No Wallet found"); } let address = await this.wallet.getAddress(); if (this.metadata.poa) { return Promise.resolve({ txnId: null, poa: true, faucet_url: null }); } if (!this.metadata.simba_faucet) { return Promise.resolve({ txnId: null, poa: false, faucet_url: this.metadata.faucet }); } let requestData = { account: address, value: "1", currency: "ether" }; let response = await axios.request( { url: `${this.endpoint}balance/${address}/`, method: 'POST', headers: Object.assign({'Content-Type':'application/json'}, this.apiAuthHeaders()), data: requestData, responseType: 'json' } ).catch(error=>{ throw new PostRequestException(error); }); return Promise.resolve({ ...response.data, poa: false, faucet_url: null }); } /** * @override * 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 */ async callMethodWithFile(method, parameters, files) { if (!this.wallet) { throw new WalletNotFoundException("No Wallet found"); } this.validateCall(method, parameters, files); let formData = new FormData(); let address = await this.wallet.getAddress(); formData.append('from', address); for (let [key, value] of Object.entries(parameters)) { formData.append(key, value); } for(let i = 0; i < files.length; i++){ formData.append('data', files[i], { filename: `files[${i}]`}); } return this.sendMethodRequest(method, formData); } /** * @private * Internal method for submitting method calls and retrying on nonce errors * @param {string} txnId - the txnId * @param {string} signed - The signed txn * @returns {Promise<Response>} - The response with transaction data */ async submitTxn(txnId, payload, maxTries, currentTry){ if(!maxTries) maxTries = 5; if(!currentTry) currentTry = 0; if(currentTry > maxTries){ throw new RetriesExceededException(); } // tslint:disable-next-line: no-unsafe-any const signed = await this.wallet.sign(payload); return axios.request({ url: `${this.endpoint}transaction/${txnId}/`, method: 'POST', headers: Object.assign({'Content-Type':'application/json'},this.apiAuthHeaders()), data: {payload: signed}, responseType: 'json' }).then(response => { // tslint:disable-next-line: no-console console.log('Success!', response.data); return txnId; }).catch(ex=>{ let body = ex.response.data; if(body.errors && body.errors.length){ let error = body.errors[0]; if(error.detail && error.detail.code){ let code = error.detail.code; //Nonce Error if(code === "15001" && error.detail.meta && error.detail.meta.suggested_nonce){ console.log("Nonce Too Low, trying again with suggested nonce " + error.detail.meta.suggested_nonce); payload.nonce = error.detail.meta.suggested_nonce; return this.submitTxn(txnId, payload, maxTries, currentTry++); } } } throw new SubmitTransactionException(JSON.stringify(body), ex); }); } /** * @private * Internal method for sending method calls * @param {string} url - the url * @param {object} formdata - Formdata for the POST * @returns {Promise<Response>} - The response with transaction data */ async sendMethodRequest(method, formdata){ let txnId = null; let payload; let headers = {}; headers = Object.assign(headers,this.apiAuthHeaders()); if(formdata.getHeaders){ //For NodeJS headers = Object.assign(headers,formdata.getHeaders()); } return axios.request({ url: `${this.endpoint}${method}/`, method: 'POST', headers: headers, data: formdata, responseType: 'json' }) .then((response) => { // tslint:disable-next-line: no-unsafe-any txnId = response.data.id; // tslint:disable-next-line: no-unsafe-any payload = response.data.payload.raw; return this.submitTxn(txnId, payload); }) .catch(ex=>{ throw new GenerateTransactionException(ex); }); } /** * @override * Gets a specific transaction * @param {string} transactionIdOrHash - Either a transaction ID or a transaction hash * @returns {Promise<Object>} - The transaction */ async getTransaction(transactionIdOrHash) { this.validateAnyGetCall(); let url = new URL(`${this.endpoint}transaction/${transactionIdOrHash}/`); let response = await axios.request({ url: url.toString(), method: 'GET', headers: this.apiAuthHeaders(), responseType: 'json' }) .catch(ex=>{ throw new GetTransactionsException(ex); }); return response.data; } /** * @override * Gets a paged list of transactions * @param {Object} parameters - The query parameters * @returns {Promise<PagedResponse>} - A response wrapped in a {@link PagedResponse} helper */ async getTransactions(parameters) { this.validateAnyGetCall(); let url = new URL(`${this.endpoint}transaction/`); for (let [key, value] of Object.entries(parameters)) { url.searchParams.set(key, value); } return this.sendTransactionRequest(url.toString()); } /** * @override * Gets a paged list of transactions for the method * @param {string} method - The method * @param {Object} parameters - The query parameters * @returns {Promise<PagedResponse>} - A response wrapped in a {@link PagedResponse} helper */ async getMethodTransactions(method, parameters) { this.validateGetCall(method, parameters); let url = new URL(`${this.endpoint}${method}/`); for (let [key, value] of Object.entries(parameters)) { url.searchParams.set(key, value); } return this.sendTransactionRequest(url.toString()); } /** * @protected * @override * 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){ let response = await axios.request({ url: url.toString(), method: 'GET', headers: this.apiAuthHeaders(), responseType: 'json' }) .catch(ex=>{ throw new GetTransactionsException(ex); }); return new PagedResponse(response.data, url, this); } /** * @override * 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 */ async getBundleMetadataForTransaction(transactionIdOrHash) { let url = new URL(`${this.endpoint}transaction/${transactionIdOrHash}/bundle/`); url.searchParams.append('no_files', true); let response = await axios.request({ url: url.toString(), method: 'GET', headers: this.apiAuthHeaders(), json: true }) .catch(ex=>{ throw new GetRequestException(ex); }); return response.data; } /** * @override * Gets the bundle for a transaction * @param {string} transactionIdOrHash - Either a transaction ID or a transaction hash * @returns {Promise<ReadableStream|Blob>} - The bundle */ async getBundleForTransaction(transactionIdOrHash) { let url = new URL(`${this.endpoint}transaction/${transactionIdOrHash}/bundle_raw/`); let responseType = 'arraybuffer'; if (typeof window !== 'undefined'){ //in a browser responseType = 'blob'; } let response = await axios.request({ url: url.toString(), method: 'GET', headers: this.apiAuthHeaders(), responseType: responseType }) .catch(ex=>{ throw new GetRequestException(ex); }); return response.data; } /** * @override * 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 */ async getFileFromBundleForTransaction(transactionIdOrHash, fileIdx, stream) { let url = new URL(`${this.endpoint}transaction/${transactionIdOrHash}/file/${fileIdx}/`); let responseType = 'arraybuffer'; if (typeof window !== 'undefined'){ //in a browser responseType = 'blob'; } if(stream){ responseType = 'stream'; } let response = await axios.request({ url: url.toString(), method: 'GET', headers: this.apiAuthHeaders(), responseType: responseType }) .catch(ex=>{ throw new GetRequestException(ex); }); console.log(response); return response.data; } /** * @override * 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 */ async getFileFromBundleByNameForTransaction(transactionIdOrHash, fileName, stream) { let url = new URL(`${this.endpoint}transaction/${transactionIdOrHash}/fileByName/${fileName}/`); let responseType = 'arraybuffer'; if (typeof window !== 'undefined'){ //in a browser responseType = 'blob'; } if(stream){ responseType = 'stream'; } let response = await axios.request({ url: url.toString(), method: 'GET', headers: this.apiAuthHeaders(), responseType: responseType }) .catch(ex=>{ throw new GetRequestException(ex); }); console.log(response); return response.data; } }