diff --git a/flowallet/scripts/floBlockchainAPI.js b/flowallet/scripts/floBlockchainAPI.js index 8459877..936fa5f 100644 --- a/flowallet/scripts/floBlockchainAPI.js +++ b/flowallet/scripts/floBlockchainAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floBlockchainAPI v3.1.0 +(function (EXPORTS) { //floBlockchainAPI v3.1.1 /* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/ 'use strict'; const floBlockchainAPI = EXPORTS; diff --git a/flowallet/scripts/floBlockchainAPI.min.js b/flowallet/scripts/floBlockchainAPI.min.js new file mode 100644 index 0000000..800c36f --- /dev/null +++ b/flowallet/scripts/floBlockchainAPI.min.js @@ -0,0 +1 @@ +!function(EXPORTS){"use strict";const floBlockchainAPI="object"===typeof module?module.exports:window.floBlockchainAPI={},DEFAULT={blockchain:floGlobals.blockchain,apiURL:{FLO:["https://blockbook.ranchimall.net/"],FLO_TEST:[]},sendAmt:3e-4,fee:2e-4,minChangeAmt:2e-4,receiverID:floGlobals.adminID},isUndefined=val=>void 0===val,checkIfTor=floBlockchainAPI.checkIfTor=()=>fetch("https://check.torproject.org/api/ip",{mode:"no-cors"}).then((response=>response.json())).then((result=>result.IsTor)).catch((error=>!1));let isTor=!1;checkIfTor().then((result=>{isTor=result,isTor&&DEFAULT.apiURL.FLO.push("http://vl7ni6byqx7rbub5hypxtod5dbfeuhoj5r5exuyl44pspqh2gasjj4qd.onion:9166/")}));const util=floBlockchainAPI.util={};util.Sat_to_FLO=value=>parseFloat((value/1e8).toFixed(8)),util.FLO_to_Sat=value=>parseInt(1e8*value),util.toFixed=value=>parseFloat(value.toFixed(8)),Object.defineProperties(floBlockchainAPI,{sendAmt:{get:()=>DEFAULT.sendAmt,set:amt=>isNaN(amt)?null:DEFAULT.sendAmt=amt},fee:{get:()=>DEFAULT.fee,set:fee=>isNaN(fee)?null:DEFAULT.fee=fee},defaultReceiver:{get:()=>DEFAULT.receiverID,set:floID=>DEFAULT.receiverID=floID},blockchain:{get:()=>DEFAULT.blockchain}}),floGlobals.sendAmt&&(floBlockchainAPI.sendAmt=floGlobals.sendAmt),floGlobals.fee&&(floBlockchainAPI.fee=floGlobals.fee),Object.defineProperties(floGlobals,{sendAmt:{get:()=>DEFAULT.sendAmt,set:amt=>isNaN(amt)?null:DEFAULT.sendAmt=amt},fee:{get:()=>DEFAULT.fee,set:fee=>isNaN(fee)?null:DEFAULT.fee=fee}});const allServerList=new Set(floGlobals.apiURL&&floGlobals.apiURL[DEFAULT.blockchain]?floGlobals.apiURL[DEFAULT.blockchain]:DEFAULT.apiURL[DEFAULT.blockchain]);var serverList=Array.from(allServerList),curPos=floCrypto.randInt(0,serverList.length-1);function fetch_retry(apicall,rm_node){return new Promise(((resolve,reject)=>{let i=serverList.indexOf(rm_node);-1!=i&&serverList.splice(i,1),curPos=floCrypto.randInt(0,serverList.length-1),fetch_api(apicall,!1).then((result=>resolve(result))).catch((error=>reject(error)))}))}function fetch_api(apicall,ic=!0){return new Promise(((resolve,reject)=>{if(0===serverList.length)ic?(serverList=Array.from(allServerList),curPos=floCrypto.randInt(0,serverList.length-1),fetch_api(apicall,!1).then((result=>resolve(result))).catch((error=>reject(error)))):reject("No FLO blockbook server working");else{let serverURL=serverList[curPos];fetch(serverURL+apicall).then((response=>{response.ok?response.json().then((data=>resolve(data))):fetch_retry(apicall,serverURL).then((result=>resolve(result))).catch((error=>reject(error)))})).catch((error=>{fetch_retry(apicall,serverURL).then((result=>resolve(result))).catch((error=>reject(error)))}))}}))}Object.defineProperties(floBlockchainAPI,{serverList:{get:()=>Array.from(serverList)},current_server:{get:()=>serverList[curPos]}});const promisedAPI=floBlockchainAPI.promisedAPI=floBlockchainAPI.fetch=function(apicall,query_params=void 0){return new Promise(((resolve,reject)=>{isUndefined(query_params)||(apicall+="?"+new URLSearchParams(JSON.parse(JSON.stringify(query_params))).toString()),fetch_api(apicall).then((result=>resolve(result))).catch((error=>reject(error)))}))},getBalance=floBlockchainAPI.getBalance=function(addr){return new Promise(((resolve,reject)=>{promisedAPI(`api/address/${addr}`,{details:"basic"}).then((result=>resolve(result.balance))).catch((error=>reject(error)))}))};const getUTXOs=address=>new Promise(((resolve,reject)=>{promisedAPI(`api/utxo/${address}`,{confirmed:!0}).then((utxos=>{let scriptPubKey=function(address){var tx=bitjs.transaction();tx.addoutput(address,0);let outputBuffer=tx.outputs.pop().script;return Crypto.util.bytesToHex(outputBuffer)}(address);utxos.forEach((u=>u.scriptPubKey=scriptPubKey)),resolve(utxos)})).catch((error=>reject(error)))})),createTx=function(senderAddr,receiverAddr,sendAmt,floData="",strict_utxo=!0){return new Promise(((resolve,reject)=>floCrypto.validateASCII(floData)?floCrypto.validateFloID(senderAddr,!0)?floCrypto.validateFloID(receiverAddr)?"number"!=typeof sendAmt||sendAmt<=0?reject(`Invalid sendAmt : ${sendAmt}`):void getBalance(senderAddr).then((balance=>{var fee=DEFAULT.fee;if(balance{for(var trx=bitjs.transaction(),utxoAmt=0,i=utxos.length-1;i>=0&&utxoAmtDEFAULT.minChangeAmt&&trx.addoutput(senderAddr,change),trx.addflodata(floData.replace(/\n/g," ")),resolve(trx)}})).catch((error=>reject(error)))})).catch((error=>reject(error))):reject(`Invalid address : ${receiverAddr}`):reject(`Invalid address : ${senderAddr}`):reject("Invalid FLO_Data: only printable ASCII characters are allowed")))};floBlockchainAPI.createTx=function(senderAddr,receiverAddr,sendAmt,floData="",strict_utxo=!0){return new Promise(((resolve,reject)=>{createTx(senderAddr,receiverAddr,sendAmt,floData,strict_utxo).then((trx=>resolve(trx.serialize()))).catch((error=>reject(error)))}))};const sendTx=floBlockchainAPI.sendTx=function(senderAddr,receiverAddr,sendAmt,privKey,floData="",strict_utxo=!0){return new Promise(((resolve,reject)=>floCrypto.validateFloID(senderAddr,!0)?privKey.length<1||!floCrypto.verifyPrivKey(privKey,senderAddr)?reject("Invalid Private key!"):void createTx(senderAddr,receiverAddr,sendAmt,floData,strict_utxo).then((trx=>{var signedTxHash=trx.sign(privKey,1);broadcastTx(signedTxHash).then((txid=>resolve(txid))).catch((error=>reject(error)))})).catch((error=>reject(error))):reject(`Invalid address : ${senderAddr}`)))};floBlockchainAPI.writeData=function(senderAddr,data,privKey,receiverAddr=DEFAULT.receiverID,options={}){let strict_utxo=!1!==options.strict_utxo,sendAmt=isNaN(options.sendAmt)?DEFAULT.sendAmt:options.sendAmt;return new Promise(((resolve,reject)=>{"string"!=typeof data&&(data=JSON.stringify(data)),sendTx(senderAddr,receiverAddr,sendAmt,privKey,data,strict_utxo).then((txid=>resolve(txid))).catch((error=>reject(error)))}))},floBlockchainAPI.mergeUTXOs=function(floID,privKey,floData=""){return new Promise(((resolve,reject)=>{if(!floCrypto.validateFloID(floID,!0))return reject("Invalid floID");if(!floCrypto.verifyPrivKey(privKey,floID))return reject("Invalid Private Key");if(!floCrypto.validateASCII(floData))return reject("Invalid FLO_Data: only printable ASCII characters are allowed");var trx=bitjs.transaction(),utxoAmt=0,fee=DEFAULT.fee;getUTXOs(floID).then((utxos=>{for(var i=utxos.length-1;i>=0;i--)utxos[i].confirmations&&(trx.addinput(utxos[i].txid,utxos[i].vout,utxos[i].scriptPubKey),utxoAmt+=utxos[i].amount);trx.addoutput(floID,utxoAmt-fee),trx.addflodata(floData.replace(/\n/g," "));var signedTxHash=trx.sign(privKey,1);broadcastTx(signedTxHash).then((txid=>resolve(txid))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},floBlockchainAPI.splitUTXOs=function(floID,privKey,count,floData=""){return new Promise(((resolve,reject)=>{if(!floCrypto.validateFloID(floID,!0))return reject("Invalid floID");if(!floCrypto.verifyPrivKey(privKey,floID))return reject("Invalid Private Key");if(!floCrypto.validateASCII(floData))return reject("Invalid FLO_Data: only printable ASCII characters are allowed");var fee=DEFAULT.fee,splitAmt=DEFAULT.sendAmt+fee,totalAmt=splitAmt*count;getBalance(floID).then((balance=>{var fee=DEFAULT.fee;if(balance{var trx=bitjs.transaction(),utxoAmt=0;for(let i=utxos.length-1;i>=0&&utxoAmtDEFAULT.minChangeAmt&&trx.addoutput(floID,change),trx.addflodata(floData.replace(/\n/g," "));var signedTxHash=trx.sign(privKey,1);broadcastTx(signedTxHash).then((txid=>resolve(txid))).catch((error=>reject(error)))}})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},floBlockchainAPI.writeDataMultiple=function(senderPrivKeys,data,receivers=[DEFAULT.receiverID],options={}){return new Promise(((resolve,reject)=>{if(!Array.isArray(senderPrivKeys))return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array");if(!1===options.preserveRatio){let tmp={},amount=DEFAULT.sendAmt*receivers.length/senderPrivKeys.length;senderPrivKeys.forEach((key=>tmp[key]=amount)),senderPrivKeys=tmp}if(!Array.isArray(receivers))return reject("Invalid receivers: Receivers must be Array");{let tmp={},amount=options.sendAmt||DEFAULT.sendAmt;receivers.forEach((floID=>tmp[floID]=amount)),receivers=tmp}"string"!=typeof data&&(data=JSON.stringify(data)),sendTxMultiple(senderPrivKeys,receivers,data).then((txid=>resolve(txid))).catch((error=>reject(error)))}))};const sendTxMultiple=floBlockchainAPI.sendTxMultiple=function(senderPrivKeys,receivers,floData=""){return new Promise(((resolve,reject)=>{if(!floCrypto.validateASCII(floData))return reject("Invalid FLO_Data: only printable ASCII characters are allowed");let preserveRatio,senders={};try{let invalids={InvalidSenderPrivKeys:[],InvalidSenderAmountFor:[],InvalidReceiverIDs:[],InvalidReceiveAmountFor:[]},inputVal=0,outputVal=0;if(Array.isArray(senderPrivKeys))senderPrivKeys.forEach((key=>{try{if(key){let floID=floCrypto.getFloID(key);senders[floID]={wif:key}}else invalids.InvalidSenderPrivKeys.push(key)}catch(error){invalids.InvalidSenderPrivKeys.push(key)}})),preserveRatio=!0;else{for(let key in senderPrivKeys)try{if(key){"number"!=typeof senderPrivKeys[key]||senderPrivKeys[key]<=0?invalids.InvalidSenderAmountFor.push(key):inputVal+=senderPrivKeys[key];let floID=floCrypto.getFloID(key);senders[floID]={wif:key,coins:senderPrivKeys[key]}}else invalids.InvalidSenderPrivKeys.push(key)}catch(error){invalids.InvalidSenderPrivKeys.push(key)}preserveRatio=!1}for(let floID in receivers)floCrypto.validateFloID(floID)||invalids.InvalidReceiverIDs.push(floID),"number"!=typeof receivers[floID]||receivers[floID]<=0?invalids.InvalidReceiveAmountFor.push(floID):outputVal+=receivers[floID];for(let i in invalids)invalids[i].length||delete invalids[i];if(Object.keys(invalids).length)return reject(invalids);if(!preserveRatio&&inputVal!=outputVal)return reject(`Input Amount (${inputVal}) not equal to Output Amount (${outputVal})`)}catch(error){return reject(error)}let promises=[];for(let floID in senders)promises.push(getBalance(floID));Promise.all(promises).then((results=>{let totalBalance=0,totalFee=DEFAULT.fee,balance={};if(!preserveRatio)var dividedFee=totalFee/Object.keys(senders).length;let insufficient=[];for(let floID in senders)balance[floID]=parseFloat(results.shift()),(isNaN(balance[floID])||preserveRatio&&balance[floID]<=totalFee||!preserveRatio&&balance[floID]{var trx=bitjs.transaction();for(let floID in senders){let sendAmt,utxos=results.shift();if(preserveRatio){let ratio=balance[floID]/totalBalance;sendAmt=totalSendAmt*ratio}else sendAmt=senders[floID].coins+dividedFee;let utxoAmt=0;for(let i=utxos.length-1;i>=0&&utxoAmt0&&trx.addoutput(floID,change)}for(let floID in receivers)trx.addoutput(floID,receivers[floID]);trx.addflodata(floData.replace(/\n/g," "));for(let floID in senders)trx.sign(senders[floID].wif,1);var signedTxHash=trx.serialize();broadcastTx(signedTxHash).then((txid=>resolve(txid))).catch((error=>reject(error)))})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},createMultisigTx=function(redeemScript,receivers,amounts,floData="",strict_utxo=!0){return new Promise(((resolve,reject)=>{var multisig=floCrypto.decodeRedeemScript(redeemScript);if(!multisig)return reject("Invalid redeemScript");var senderAddr=multisig.address;if(!floCrypto.validateFloID(senderAddr))return reject(`Invalid multisig : ${senderAddr}`);if(!floCrypto.validateASCII(floData))return reject("Invalid FLO_Data: only printable ASCII characters are allowed");Array.isArray(receivers)||(receivers=[receivers]);for(let r of receivers)if(!floCrypto.validateFloID(r))return reject(`Invalid address : ${r}`);if(Array.isArray(amounts)||(amounts=[amounts]),amounts.length!=receivers.length)return reject("Receivers and amounts have different length");var sendAmt=0;for(let a of amounts){if("number"!=typeof a||a<=0)return reject(`Invalid amount : ${a}`);sendAmt+=a}getBalance(senderAddr).then((balance=>{var fee=DEFAULT.fee;if(balance{for(var trx=bitjs.transaction(),utxoAmt=0,i=utxos.length-1;i>=0&&utxoAmtDEFAULT.minChangeAmt&&trx.addoutput(senderAddr,change),trx.addflodata(floData.replace(/\n/g," ")),resolve(trx)}})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))};floBlockchainAPI.createMultisigTx=function(redeemScript,receivers,amounts,floData="",strict_utxo=!0){return new Promise(((resolve,reject)=>{createMultisigTx(redeemScript,receivers,amounts,floData,strict_utxo).then((trx=>resolve(trx.serialize()))).catch((error=>reject(error)))}))};const sendMultisigTx=floBlockchainAPI.sendMultisigTx=function(redeemScript,privateKeys,receivers,amounts,floData="",strict_utxo=!0){return new Promise(((resolve,reject)=>{var multisig=floCrypto.decodeRedeemScript(redeemScript);if(!multisig)return reject("Invalid redeemScript");if(privateKeys.length{for(let pk of privateKeys)trx.sign(pk,1);var signedTxHash=trx.serialize();broadcastTx(signedTxHash).then((txid=>resolve(txid))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))};function deserializeTx(tx){if("string"==typeof tx||Array.isArray(tx))try{tx=bitjs.transaction(tx)}catch{throw"Invalid transaction hex"}else if("object"!=typeof tx||"function"!=typeof tx.sign)throw"Invalid transaction object";return tx}floBlockchainAPI.writeMultisigData=function(redeemScript,data,privatekeys,receiverAddr=DEFAULT.receiverID,options={}){let strict_utxo=!1!==options.strict_utxo,sendAmt=isNaN(options.sendAmt)?DEFAULT.sendAmt:options.sendAmt;return new Promise(((resolve,reject)=>{if(!floCrypto.validateFloID(receiverAddr))return reject(`Invalid receiver: ${receiverAddr}`);sendMultisigTx(redeemScript,privatekeys,receiverAddr,sendAmt,data,strict_utxo).then((txid=>resolve(txid))).catch((error=>reject(error)))}))},floBlockchainAPI.signTx=function(tx,privateKey,sighashtype=1){if(!floCrypto.getFloID(privateKey))throw"Invalid Private key";return(tx=deserializeTx(tx)).sign(privateKey,sighashtype)};const checkSigned=floBlockchainAPI.checkSigned=function(tx,bool=!0){tx=deserializeTx(tx);let n=[];for(let i=0;ix.t)throw"signaturesRequired is more than publicKeys";x.s!0!==x)).length:n};floBlockchainAPI.checkIfSameTx=function(tx1,tx2){if(tx1=deserializeTx(tx1),tx2=deserializeTx(tx2),tx1.inputs.length!==tx2.inputs.length||tx1.outputs.length!==tx2.outputs.length)return!1;if(tx1.floData!==tx2.floData)return!1;for(let i=0;inew Promise(((resolve,reject)=>{promisedAPI(`api/tx/${txid}`).then((result=>resolve(result.vout[i]))).catch((error=>reject(error)))}));function getOutputAddress(outscript){var bytes,version;switch(outscript[0]){case 118:bytes=outscript.slice(3,outscript.length-2),version=bitjs.pub;break;case 169:bytes=outscript.slice(2,outscript.length-1),version=bitjs.multisig;break;default:return}bytes.unshift(version);var checksum=Crypto.SHA256(Crypto.SHA256(bytes,{asBytes:!0}),{asBytes:!0}).slice(0,4);return bitjs.Base58.encode(bytes.concat(checksum))}floBlockchainAPI.parseTransaction=function(tx){return new Promise(((resolve,reject)=>{tx=deserializeTx(tx);let result={},promises=[];for(let i=0;i{result.inputs=inputs.map((inp=>Object({address:inp.scriptPubKey.addresses[0],value:parseFloat(inp.value)})));let signed=checkSigned(tx,!1);result.inputs.forEach(((inp,i)=>inp.signed=signed[i])),result.outputs=tx.outputs.map((out=>Object({address:getOutputAddress(out.script),value:util.Sat_to_FLO(out.value)}))),result.total_input=parseFloat(result.inputs.reduce(((a,inp)=>a+inp.value),0).toFixed(8)),result.total_output=parseFloat(result.outputs.reduce(((a,out)=>a+out.value),0).toFixed(8)),result.fee=parseFloat((result.total_input-result.total_output).toFixed(8)),result.floData=tx.floData,resolve(result)})).catch((error=>reject(error)))}))};const broadcastTx=floBlockchainAPI.broadcastTx=function(signedTxHash){return new Promise(((resolve,reject)=>{if(signedTxHash.length<1)return reject("Empty Transaction Data");promisedAPI("/api/sendtx/"+signedTxHash).then((response=>resolve(response.result))).catch((error=>reject(error)))}))},getTx=floBlockchainAPI.getTx=function(txid){return new Promise(((resolve,reject)=>{promisedAPI(`api/tx/${txid}`).then((response=>resolve(response))).catch((error=>reject(error)))}))},waitForConfirmation=floBlockchainAPI.waitForConfirmation=function(txid,max_retry=-1,retry_timeout=20){return new Promise(((resolve,reject)=>{setTimeout((function(){getTx(txid).then((tx=>tx?tx.confirmations?resolve(tx):0===max_retry?reject("Waiting timeout: tx still not confirmed"):void waitForConfirmation(txid,max_retry=max_retry<0?-1:max_retry-1,retry_timeout).then((result=>resolve(result))).catch((error=>reject(error))):reject("Transaction not found"))).catch((error=>reject(error)))}),1e3*retry_timeout)}))},readTxs=floBlockchainAPI.readTxs=function(addr,options={}){return new Promise(((resolve,reject)=>{let query_params={details:"txs"};!isUndefined(options.page)&&Number.isInteger(options.page)&&(query_params.page=options.page),!isUndefined(options.pageSize)&&Number.isInteger(options.pageSize)&&(query_params.pageSize=options.pageSize),options.confirmed&&(query_params.confirmed=!0),promisedAPI(`api/address/${addr}`,query_params).then((response=>{Array.isArray(response.txs)||(response.txs=[]),resolve(response)})).catch((error=>reject(error)))}))};function readAllTxs_oldSupport(addr,options,ignoreOld=0,cacheTotal=0){return new Promise(((resolve,reject)=>{readTxs(addr,options).then((response=>{cacheTotal+=response.txs.length;let n_remaining=response.txApperances-cacheTotal;if(n_remainingresolve(response.txs.concat(result)))).catch((error=>reject(error))))})).catch((error=>reject(error)))}))}function readAllTxs_new(addr,options,lastItem){return new Promise(((resolve,reject)=>{readTxs(addr,options).then((response=>{let i=response.txs.findIndex((t=>t.txid===lastItem));-1!=i?resolve(response.txs.slice(0,i)):response.page==response.totalPages?resolve(response.txs):(options.page=response.page+1,readAllTxs_new(addr,options,lastItem).then((result=>resolve(response.txs.concat(result)))).catch((error=>reject(error))))})).catch((error=>reject(error)))}))}const readAllTxs=floBlockchainAPI.readAllTxs=function(addr,options={}){return new Promise(((resolve,reject)=>{Number.isInteger(options.ignoreOld)?readAllTxs_oldSupport(addr,options,options.ignoreOld).then((txs=>{let last_tx=txs.find((t=>t.confirmations>0)),new_lastItem=last_tx?last_tx.txid:options.ignoreOld;resolve({lastItem:new_lastItem,items:txs})})).catch((error=>reject(error))):readAllTxs_new(addr,options,options.after).then((txs=>{let last_tx=txs.find((t=>t.confirmations>0)),new_lastItem=last_tx?last_tx.txid:options.after;resolve({lastItem:new_lastItem,items:txs})})).catch((error=>reject(error)))}))};floBlockchainAPI.readData=function(addr,options={}){return new Promise(((resolve,reject)=>{let query_options={};query_options.confirmed=!!isUndefined(options.confirmed)||options.confirmed,isUndefined(options.after)?isUndefined(options.ignoreOld)||(query_options.ignoreOld=options.ignoreOld):query_options.after=options.after,readAllTxs(addr,query_options).then((response=>{"string"==typeof options.senders&&(options.senders=[options.senders]),"string"==typeof options.receivers&&(options.receivers=[options.receivers]);const filteredData=response.items.filter((tx=>{if(!tx.confirmations)return!1;if(options.sentOnly&&!tx.vin.some((vin=>vin.addresses[0]===addr)))return!1;if(Array.isArray(options.senders)&&!tx.vin.some((vin=>options.senders.includes(vin.addresses[0]))))return!1;if(options.receivedOnly&&!tx.vout.some((vout=>vout.scriptPubKey.addresses[0]===addr)))return!1;if(Array.isArray(options.receivers)&&!tx.vout.some((vout=>options.receivers.includes(vout.scriptPubKey.addresses[0]))))return!1;if(options.pattern)try{let jsonContent=JSON.parse(tx.floData);if(!Object.keys(jsonContent).includes(options.pattern))return!1}catch{return!1}return!(options.filter&&!options.filter(tx.floData))})).map((tx=>options.tx?{txid:tx.txid,time:tx.time,blockheight:tx.blockheight,senders:new Set(tx.vin.map((v=>v.addresses[0]))),receivers:new Set(tx.vout.map((v=>v.scriptPubKey.addresses[0]))),data:tx.floData}:tx.floData)),result={lastItem:response.lastItem};options.tx?result.items=filteredData:result.data=filteredData,resolve(result)})).catch((error=>reject(error)))}))};const getLatestData=floBlockchainAPI.getLatestData=function(addr,caseFn,options={}){return new Promise(((resolve,reject)=>{let new_lastItem,query_options={};query_options.confirmed=!!isUndefined(options.confirmed)||options.confirmed,isUndefined(options.page)||(query_options.page=options.page),readTxs(addr,query_options).then((response=>{if(!new_lastItem){let last_tx=response.items.find((t=>t.confirmations>0));last_tx&&(new_lastItem=last_tx.txid)}"string"==typeof options.senders&&(options.senders=[options.senders]),"string"==typeof options.receivers&&(options.receivers=[options.receivers]);let i_after=response.txs.findIndex((t=>t.txid===options.after));-1!=i_after&&response.items.splice(i_after);var item=response.items.find((tx=>!!tx.confirmations&&(!(options.sentOnly&&!tx.vin.some((vin=>vin.addresses[0]===addr)))&&(!(Array.isArray(options.senders)&&!tx.vin.some((vin=>options.senders.includes(vin.addresses[0]))))&&(!(options.receivedOnly&&!tx.vout.some((vout=>vout.scriptPubKey.addresses[0]===addr)))&&(!(Array.isArray(options.receivers)&&!tx.vout.some((vout=>options.receivers.includes(vout.scriptPubKey.addresses[0]))))&&!!caseFn(tx.floData)))))));if(!isUndefined(item)){const result={lastItem:new_lastItem||item.txid};return options.tx?result.item={txid:item.txid,time:item.time,blockheight:item.blockheight,senders:new Set(item.vin.map((v=>v.addresses[0]))),receivers:new Set(item.vout.map((v=>v.scriptPubKey.addresses[0]))),data:item.floData}:result.data=item.floData,resolve(result)}response.page==response.totalPages||-1!=i_after?resolve({lastItem:new_lastItem||options.after}):(options.page=response.page+1,getLatestData(addr,caseFn,options).then((result=>resolve(result))).catch((error=>reject(error))))})).catch((error=>reject(error)))}))}}(); \ No newline at end of file diff --git a/flowallet/scripts/floTokenAPI.js b/flowallet/scripts/floTokenAPI.js index 2456b88..3ef5575 100644 --- a/flowallet/scripts/floTokenAPI.js +++ b/flowallet/scripts/floTokenAPI.js @@ -4,13 +4,30 @@ const tokenAPI = EXPORTS; const DEFAULT = { - apiURL: floGlobals.tokenURL || "https://ranchimallflo.duckdns.org/", + apiURL: [floGlobals.tokenURL || "https://ranchimallflo.ranchimall.net/"], currency: floGlobals.currency || "rupee" } + + const checkIfTor = tokenAPI.checkIfTor = () => { + return fetch('https://check.torproject.org/api/ip', { + mode: 'no-cors' + }) + .then(response => response.json()) + .then(result => result.IsTor) + .catch(error => false) + } + let isTor = false; + checkIfTor().then(result => { + isTor = result + if (isTor) { + DEFAULT.apiURL = ['http://omwkzk6bd6zuragdqsrhdyzgxzre7yx4vzrou4vzftintzc2dmagp6qd.onion:5017/'] + } + }); + Object.defineProperties(tokenAPI, { URL: { - get: () => DEFAULT.apiURL + get: () => DEFAULT.apiURL[0], }, currency: { get: () => DEFAULT.currency, @@ -27,16 +44,25 @@ } }); - const fetch_api = tokenAPI.fetch = function (apicall) { + const fetch_api = tokenAPI.fetch = function (apicall, apiURLs = DEFAULT.apiURL) { return new Promise((resolve, reject) => { - console.debug(DEFAULT.apiURL + apicall); - fetch(DEFAULT.apiURL + apicall).then(response => { + if (apiURLs.length === 0) { + reject("No API URLs available"); + return; + } + const currentURL = apiURLs[0]; + console.debug(currentURL + apicall); + fetch(currentURL + apicall).then(response => { if (response.ok) response.json().then(data => resolve(data)); else - reject(response) - }).catch(error => reject(error)) - }) + reject(response); + }).catch(error => { + console.error(`Failed to fetch from ${currentURL}: ${error}`); + // Try the next API URL recursively + fetch_api(apicall, apiURLs.slice(1)).then(resolve).catch(reject); + }); + }); } const getBalance = tokenAPI.getBalance = function (floID, token = DEFAULT.currency) { diff --git a/flowallet/scripts/floTokenAPI.min.js b/flowallet/scripts/floTokenAPI.min.js new file mode 100644 index 0000000..1f9412f --- /dev/null +++ b/flowallet/scripts/floTokenAPI.min.js @@ -0,0 +1 @@ +!function(EXPORTS){"use strict";const tokenAPI="object"===typeof module?module.exports:window.floTokenAPI={},DEFAULT={apiURL:[floGlobals.tokenURL||"https://ranchimallflo.ranchimall.net/"],currency:floGlobals.currency||"rupee"},checkIfTor=tokenAPI.checkIfTor=()=>fetch("https://check.torproject.org/api/ip",{mode:"no-cors"}).then((response=>response.json())).then((result=>result.IsTor)).catch((error=>!1));let isTor=!1;checkIfTor().then((result=>{isTor=result,isTor&&(DEFAULT.apiURL=["http://omwkzk6bd6zuragdqsrhdyzgxzre7yx4vzrou4vzftintzc2dmagp6qd.onion:5017/"])})),Object.defineProperties(tokenAPI,{URL:{get:()=>DEFAULT.apiURL[0]},currency:{get:()=>DEFAULT.currency,set:currency=>DEFAULT.currency=currency}}),floGlobals.currency&&(tokenAPI.currency=floGlobals.currency),Object.defineProperties(floGlobals,{currency:{get:()=>DEFAULT.currency,set:currency=>DEFAULT.currency=currency}});const fetch_api=tokenAPI.fetch=function(apicall,apiURLs=DEFAULT.apiURL){return new Promise(((resolve,reject)=>{if(0===apiURLs.length)return void reject("No API URLs available");const currentURL=apiURLs[0];console.debug(currentURL+apicall),fetch(currentURL+apicall).then((response=>{response.ok?response.json().then((data=>resolve(data))):reject(response)})).catch((error=>{console.error(`Failed to fetch from ${currentURL}: ${error}`),fetch_api(apicall,apiURLs.slice(1)).then(resolve).catch(reject)}))}))},getBalance=tokenAPI.getBalance=function(floID,token=DEFAULT.currency){return new Promise(((resolve,reject)=>{fetch_api(`api/v1.0/getFloAddressBalance?token=${token}&floAddress=${floID}`).then((result=>resolve(result.balance||0))).catch((error=>reject(error)))}))};function sendTokens_raw(privKey,receiverID,token,amount,utxo,vout,scriptPubKey){return new Promise(((resolve,reject)=>{var trx=bitjs.transaction();trx.addinput(utxo,vout,scriptPubKey),trx.addoutput(receiverID,floBlockchainAPI.sendAmt),trx.addflodata(`send ${amount} ${token}#`);var signedTxHash=trx.sign(privKey,1);floBlockchainAPI.broadcastTx(signedTxHash).then((txid=>resolve([receiverID,txid]))).catch((error=>reject([receiverID,error])))}))}tokenAPI.getTx=function(txID){return new Promise(((resolve,reject)=>{fetch_api(`api/v1.0/getTransactionDetails/${txID}`).then((res=>{"error"===res.result?reject(res.description):res.parsedFloData?res.transactionDetails?resolve(res):reject("Data piece (transactionDetails) missing"):reject("Data piece (parsedFloData) missing")})).catch((error=>reject(error)))}))},tokenAPI.sendToken=function(privKey,amount,receiverID,message="",token=DEFAULT.currency,options={}){return new Promise(((resolve,reject)=>{let senderID=floCrypto.getFloID(privKey);if("number"!=typeof amount||isNaN(amount)||amount<=0)return reject("Invalid amount");getBalance(senderID,token).then((bal=>{if(amount>bal)return reject(`Insufficient ${token}# balance`);floBlockchainAPI.writeData(senderID,`send ${amount} ${token}# ${message}`,privKey,receiverID,options).then((txid=>resolve(txid))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},tokenAPI.bulkTransferTokens=function(sender,privKey,token,receivers){return new Promise(((resolve,reject)=>{if("object"!=typeof receivers)return reject("receivers must be object in format {receiver1: amount1, receiver2:amount2...}");let receiver_list=Object.keys(receivers),amount_list=Object.values(receivers),invalidReceivers=receiver_list.filter((id=>!floCrypto.validateFloID(id))),invalidAmount=amount_list.filter((val=>"number"!=typeof val||val<=0));if(invalidReceivers.length)return reject(`Invalid receivers: ${invalidReceivers}`);if(invalidAmount.length)return reject(`Invalid amounts: ${invalidAmount}`);if(0==receiver_list.length)return reject("Receivers cannot be empty");if(1==receiver_list.length){let receiver=receiver_list[0],amount=amount_list[0];floTokenAPI.sendToken(privKey,amount,receiver,"",token).then((txid=>resolve({success:{[receiver]:txid}}))).catch((error=>reject(error)))}else floTokenAPI.getBalance(sender,token).then((token_balance=>{if(amount_list.reduce(((a,e)=>a+e),0)>token_balance)return reject(`Insufficient ${token}# balance`);floBlockchainAPI.splitUTXOs(sender,privKey,receiver_list.length).then((split_txid=>{floBlockchainAPI.waitForConfirmation(split_txid).then((split_tx=>{var scriptPubKey=split_tx.vout[0].scriptPubKey.hex;let promises=[];for(let i in receiver_list)promises.push(sendTokens_raw(privKey,receiver_list[i],token,amount_list[i],split_txid,i,scriptPubKey));Promise.allSettled(promises).then((results=>{let success=Object.fromEntries(results.filter((r=>"fulfilled"==r.status)).map((r=>r.value))),failed=Object.fromEntries(results.filter((r=>"rejected"==r.status)).map((r=>r.reason)));resolve({success:success,failed:failed})}))})).catch((error=>reject(error)))})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},tokenAPI.getAllTxs=function(floID,token=DEFAULT.currency){return new Promise(((resolve,reject)=>{fetch_api(`api/v1.0/getFloAddressTransactions?token=${token}&floAddress=${floID}`).then((result=>resolve(result))).catch((error=>reject(error)))}))};(tokenAPI.util={}).parseTxData=function(txData){let parsedData={};for(let p in txData.parsedFloData)parsedData[p]=txData.parsedFloData[p];parsedData.sender=txData.transactionDetails.vin[0].addr;for(let vout of txData.transactionDetails.vout)vout.scriptPubKey.addresses[0]!==parsedData.sender&&(parsedData.receiver=vout.scriptPubKey.addresses[0]);return parsedData.time=txData.transactionDetails.time,parsedData}}(); \ No newline at end of file