@stellar-broker/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @stellar-broker/client
2
+
3
+ ## Installation
4
+
5
+ ```
6
+ npm i @stellar-broker/client
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ ### Example - Connect and trade in DirectFlow mode
12
+
13
+ ```js
14
+ import StellarBrokerClient from '@stellar-broker/client'
15
+
16
+ //create client instance
17
+ const client = new StellarBrokerClient({partnerKey: '<your_partner_key>'})
18
+
19
+ //subscribe to the quote notifications
20
+ client.on('quote', e => {
21
+ console.log('Received quote from the server', e.quote)
22
+ /*
23
+ {
24
+ "status": "success",
25
+ "sellingAsset": "USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
26
+ "buyingAsset": "XLM",
27
+ "slippageTolerance": 0.5,
28
+ "direction": "strict_send",
29
+ "directTrade": {
30
+ "selling": "10",
31
+ "buying": "100.23505",
32
+ "path": []
33
+ },
34
+ "sellingAmount": "10",
35
+ "estimatedBuyingAmount": "100.688121",
36
+ "ts": "2024-08-13T23:13:21.275Z"
37
+ }
38
+ */
39
+ })
40
+
41
+ client.on('finished', e => {
42
+ console.log('Trade finished', e.result)
43
+ /*
44
+ {
45
+ "status": "success",
46
+ "sold": "10",
47
+ "bought": "100.6704597"
48
+ }
49
+ */
50
+ })
51
+
52
+ client.on('progress', e => {
53
+ console.log('Progress', e.status)
54
+ /*
55
+ {
56
+ "sold": "10",
57
+ "bought": "100.6704597"
58
+ }
59
+ */
60
+ })
61
+
62
+ client.on('error', e => {
63
+ console.warn('StellarBroker error', e.error)
64
+ })
65
+
66
+ //connect
67
+ client.connect() //only once
68
+ .then(() => console.log('StellarBroker connected'))
69
+ .catch(e => console.error(e))
70
+
71
+ //request a price quote
72
+ client.quote({
73
+ destination: '<account_pubkey>', //trader account pubkey
74
+ sellingAsset: 'xlm',
75
+ buyingAsset: 'USD-GDK2GNB4Q6FKNW2GNJIQFARI4RMSHV5DN5G4BBXX2F24RT5I4QT7TWZ7',
76
+ sellingAmount: '1000', //1000.0000000
77
+ slippageTolerance: 0.02 //2%
78
+ })
79
+
80
+ //once the quote received from the server, we can confirm the quote
81
+ client.confirmQuote(async (tx) => { //async signing callback - implement custom logic here
82
+ const kp = Keypair.fromSecret('<account_secret>')
83
+ tx.sign(kp)
84
+ return tx
85
+ })
86
+
87
+ //trade can be interrupted from the client by calling
88
+ client.stop()
89
+
90
+ //cleanup resources and close connection once the trading has been finished
91
+ client.close()
92
+ ```
@@ -0,0 +1,2 @@
1
+ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("@stellar/stellar-base")):"function"==typeof define&&define.amd?define(["@stellar/stellar-base"],e):"object"==typeof exports?exports.stellarbroker=e(require("@stellar/stellar-base")):t.stellarbroker=e(t["@stellar/stellar-base"])}(this,(t=>(()=>{"use strict";var e={755:e=>{e.exports=t}},s={};function i(t){var o=s[t];if(void 0!==o)return o.exports;var r=s[t]={exports:{}};return e[t](r,r.exports,i),r.exports}i.d=(t,e)=>{for(var s in e)i.o(e,s)&&!i.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var o={};i.d(o,{default:()=>f});var r=i(755);class n extends Error{constructor(t,e){super(e),this.code=t}code=0}const a={invalidInitParam:t=>new n(1,`Invalid parameter value: "${t}"`),invalidAuthorizationParam:()=>new n(2,"Invalid authorization secret key or callback provided"),quoteNotSet:()=>new n(11,"Price quote not available"),quoteExpired:()=>new n(12,"Price quote expired"),quoteError:t=>new n(13,"Price quotation error: "+t),invalidQuoteParam:(t,e)=>new n(14,`Invalid quote request parameter: "${t}". ${e}`),tradeInProgress:()=>new n(20,"Cannot change quote while trade is in progress"),invalidSwapTx:()=>new n(21,"Invalid swap transaction received"),failedToSignTx:()=>new n(22,"Failed to sign received transaction"),unsupportedEventType:t=>new n(31,"Unknown event type: "+t),serverError:t=>new n(101,"Server error: "+t)};function u(t,e,s){const i=new CustomEvent(t,{detail:e});return i[s||t]=e,i}function l(t,e){if("string"==typeof t){if("XLM"===t||"xlm"===t||"native"===t)return"XLM";if(t.includes(":")){const[s,i]=t.split(":");return c(s),h(i,e),s+"-"+i}if(t.includes("-")){const[s,i]=t.split("-");return c(s),h(i,e),t}}throw a.invalidQuoteParam(e,"Invalid asset")}function c(t,e){if(!/^[a-zA-Z0-9]{1,12}$/.test(t))throw a.invalidQuoteParam(e,"Invalid asset code: "+(void 0===t?"missing":t));return t}function h(t,e,s=!1){if(void 0!==t||!s){if(!r.StrKey.isValidEd25519PublicKey(t))throw a.invalidQuoteParam(e,"Invalid account address: "+(void 0===t?"missing":t));return t}}function d(t,e){if(void 0===t)return;const s=parseFloat(t);if(isNaN(s)||s<=0)throw a.invalidQuoteParam(e,"Invalid asset amount: "+t);return t}function p(t,e){const s=parseFloat(t);if(isNaN(s))throw a.invalidQuoteParam(e,"Invalid slippage tolerance, number expected");if(s<0)throw a.invalidQuoteParam(e,"Slippage tolerance is too small, expected value >= 0");if(s>.5)throw a.invalidQuoteParam(e,"Slippage tolerance is too large, expected value < 0.5 (50%)");return s}const g=["quote","finished","progress","error"];function v(t){return!t instanceof Promise?t?Promise.resolve(t):Promise.reject():t}const f=class{constructor(t){if(!t.network)throw a.invalidInitParam("network");this.partnerKey=t.partnerKey,this.emitter=new EventTarget,this.network=r.Networks[t.network.toUpperCase()]||t.network,this.flow=t.flow||"direct"}status="disconnected";socket;emitter;origin="https://api.stellar.broker";network="";quoteRequest;lastQuote;tradeQuote;partnerKey;authorization;get source(){return this.quoteRequest?.destination}connect(){return this.socket?.readyState===WebSocket.OPEN?Promise.resolve(this):(this.socket=new WebSocket(this.origin+"/ws?partner="+encodeURIComponent(this.partnerKey)),this.socket.onmessage=this.processMessage.bind(this),this.socket.onclose=()=>{console.log("Connection closed"),this.status="disconnected"},this.socket.onerror=t=>console.error(t),new Promise(((t,e)=>{const s=setTimeout((()=>e(this)),2e3);this.onSocketOpen=()=>{this.status="ready",this.heartbeat(),this.onSocketOpen=void 0,clearTimeout(s),t(this)}})))}processMessage(t){const e=JSON.parse(t.data);switch(e.type){case"connected":this.onSocketOpen&&this.onSocketOpen();break;case"quote":this.lastQuote=e.quote,this.lastQuote.ts=new Date,this.emitter.dispatchEvent(u("quote",this.lastQuote));break;case"tx":if("trade"!==this.status)return void console.log("Received tx in non-trading state",this.status,e);this.processTxRequest(e);break;case"stop":this.emitter.dispatchEvent(u("finished",{status:e.status,sold:e.sold,bought:e.bought},"result")),this.status="ready";break;case"progress":this.emitter.dispatchEvent(u("progress",{sold:e.sold,bought:e.bought},"status"));break;case"ping":this.heartbeat(),this.send({type:"pong"});break;case"error":this.stop(),this.emitter.dispatchEvent(u("error","Server error: "+e.error));break;default:console.log("Unknown message type: "+e.type)}}quote(t){if("trade"===this.status)throw a.tradeInProgress();this.quoteRequest=function(t){const{destination:e,sellingAsset:s,selling_asset:i,buyingAsset:o,buying_asset:r,sellingAmount:n,selling_amount:u,buyingAmount:c,buying_amount:g,slippageTolerance:v,slippage_tolerance:f,...m}=t,w={sellingAsset:l(s||i,"sellingAsset"),buyingAsset:l(o||r,"buyingAsset"),sellingAmount:d(n||u,"sellingAmount"),buyingAmount:d(c||g,"buyingAmount"),slippageTolerance:p(v||f||.02,"slippageTolerance"),flow:"direct"};if(e&&(w.destination=h(e,"destination",!0)),w.buyingAsset===w.sellingAsset)throw a.invalidQuoteParam("buyingAsset","Buying asset can't be the same as selling asset");if(void 0===w.buyingAmount&&void 0===w.sellingAmount)throw a.invalidQuoteParam("sellingAmount",'Either "buyingAmount" or "sellingAmount" parameter is required');if(void 0!==w.buyingAmount&&void 0!==w.sellingAmount)throw a.invalidQuoteParam("sellingAmount",'Parameters "buyingAmount" and "sellingAmount" are mutually exclusive');return Object.assign(w,m),w}(t),this.status="quote",this.connect().then((()=>{this.send({type:"quote",...this.quoteRequest})}))}stop(){"trade"!==this.status&&"quote"!==this.status||(this.send({type:"stop"}),this.tradeQuote=void 0,this.status="ready")}confirmQuote(t){if("quote"!==this.status)throw a.tradeInProgress();if(!this.lastQuote)throw a.quoteNotSet();if(new Date-this.lastQuote.ts>7e3)throw a.quoteExpired();if("success"!==this.lastQuote.status)throw a.quoteError(this.lastQuote.error||"quote not available");if("string"==typeof t)try{this.authorization=r.Keypair.fromSecret(t)}catch(t){throw a.invalidAuthorizationParam()}else"function"==typeof t&&(this.authorization=t);this.tradeQuote=this.lastQuote,this.send({type:"trade"}),this.status="trade"}processTxRequest(t){let e;try{e=r.TransactionBuilder.fromXDR(t.xdr,this.network)}catch(t){throw a.invalidSwapTx()}if(!this.validateTransaction(e))throw a.invalidSwapTx();this.authorizeTx(e,t.networkFee).then((e=>{if(!e||e.signatures.length<1)return console.error(`Transaction ${t.hash} not signed`);this.send({type:"tx",hash:t.hash,xdr:e.toXDR()})})).catch((t=>{throw console.error(t),a.failedToSignTx()}))}async authorizeTx(t,e){this.authorization instanceof r.Keypair?t.sign(this.authorization):t=await v(this.authorization(t));let s=r.TransactionBuilder.buildFeeBumpTransaction(this.source,e,t,this.network);return this.authorization instanceof r.Keypair?s.sign(this.authorization):s=await v(this.authorization(s)),s}validateTransaction(t){for(let e of t.operations){if("pathPaymentStrictSend"!==e.type&&"pathPaymentStrictReceive"!==e.type)return!1;if(e.destination!==this.source){if("pathPaymentStrictSend"!==e.type)return!1;if(e.source&&e.source!==this.source)return!1}else if(e.source&&e.source!==e.destination||e.destination!==this.source)return!1}return!0}send(t){this.socket.send(JSON.stringify(t))}heartbeat(){clearTimeout(this.pingHandler),this.pingHandler=setTimeout((()=>{console.warn("Lost connection with the server"),this.socket.close()}),11e3)}on(t,e){if(!g.includes(t))throw a.unsupportedEventType(t);this.emitter.addEventListener(t,e)}once(t,e){if(!g.includes(t))throw a.unsupportedEventType(t);this.emitter.addEventListener(t,e,{once:!0})}off(t,e){if(!g.includes(t))throw a.unsupportedEventType(t);this.emitter.removeEventListener(t,e)}close(){try{this.socket.close()}catch(t){}}};return o.default})()));
2
+ //# sourceMappingURL=stellarbroker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stellarbroker.js","mappings":"CAAA,SAA2CA,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,EAAQG,QAAQ,0BACR,mBAAXC,QAAyBA,OAAOC,IAC9CD,OAAO,CAAC,yBAA0BJ,GACR,iBAAZC,QACdA,QAAuB,cAAID,EAAQG,QAAQ,0BAE3CJ,EAAoB,cAAIC,EAAQD,EAAK,yBACtC,CATD,CASGO,MAAOC,G,kCCTVL,EAAOD,QAAUM,C,GCCbC,EAA2B,CAAC,EAGhC,SAASC,EAAoBC,GAE5B,IAAIC,EAAeH,EAAyBE,GAC5C,QAAqBE,IAAjBD,EACH,OAAOA,EAAaV,QAGrB,IAAIC,EAASM,EAAyBE,GAAY,CAGjDT,QAAS,CAAC,GAOX,OAHAY,EAAoBH,GAAUR,EAAQA,EAAOD,QAASQ,GAG/CP,EAAOD,OACf,CCrBAQ,EAAoBK,EAAI,CAACb,EAASc,KACjC,IAAI,IAAIC,KAAOD,EACXN,EAAoBQ,EAAEF,EAAYC,KAASP,EAAoBQ,EAAEhB,EAASe,IAC5EE,OAAOC,eAAelB,EAASe,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDP,EAAoBQ,EAAI,CAACK,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,G,6CCA3E,MAAMI,UAA2BC,MACpCC,WAAAA,CAAYC,EAAMC,GACdC,MAAMD,GACNzB,KAAKwB,KAAOA,CAChB,CAOAA,KAAO,EAGX,MAoCA,EApCe,CACXG,iBAAiBC,GACN,IAAIP,EAAmB,EAAG,6BAA6BO,MAElEC,0BAAyBA,IACd,IAAIR,EAAmB,EAAG,yDAErCS,YAAWA,IACA,IAAIT,EAAmB,GAAI,6BAEtCU,aAAYA,IACD,IAAIV,EAAmB,GAAI,uBAEtCW,WAAWP,GACA,IAAIJ,EAAmB,GAAI,0BAA4BI,GAElEQ,kBAAiBA,CAACC,EAAkBC,IACzB,IAAId,EAAmB,GAAI,qCAAqCa,OAAsBC,KAEjGC,gBAAeA,IACJ,IAAIf,EAAmB,GAAI,kDAEtCgB,cAAaA,IACF,IAAIhB,EAAmB,GAAI,qCAEtCiB,eAAcA,IACH,IAAIjB,EAAmB,GAAI,uCAEtCkB,qBAAqBC,GACV,IAAInB,EAAmB,GAAI,uBAAyBmB,GAE/DC,YAAYhB,GACD,IAAIJ,EAAmB,IAAK,iBAAmBI,ICxCvD,SAASiB,EAAWF,EAAMG,EAAMjC,GACnC,MAAMkC,EAAM,IAAIC,YAAYL,EAAM,CAACM,OAAQH,IAE3C,OADAC,EAAIlC,GAAO8B,GAAQG,EACZC,CACX,CCiCA,SAASG,EAAWC,EAAOC,GACvB,GAAqB,iBAAVD,EAAoB,CAC3B,GAAc,QAAVA,GAA6B,QAAVA,GAA6B,WAAVA,EACtC,MAAO,MACX,GAAIA,EAAME,SAAS,KAAM,CACrB,MAAO1B,EAAM2B,GAAUH,EAAMI,MAAM,KAGnC,OAFAC,EAAa7B,GACb8B,EAAgBH,EAAQF,GACjBzB,EAAO,IAAM2B,CACxB,CACA,GAAIH,EAAME,SAAS,KAAM,CACrB,MAAO1B,EAAM2B,GAAUH,EAAMI,MAAM,KAGnC,OAFAC,EAAa7B,GACb8B,EAAgBH,EAAQF,GACjBD,CACX,CACJ,CACA,MAAMO,EAAOtB,kBAAkBgB,EAAW,gBAC9C,CAEA,SAASI,EAAa7B,EAAMyB,GACxB,IAAK,sBAAsBO,KAAKhC,GAC5B,MAAM+B,EAAOtB,kBAAkBgB,EAAW,6BAAmC3C,IAATkB,EAAqB,UAAYA,IACzG,OAAOA,CACX,CAEA,SAAS8B,EAAgBG,EAAS7B,EAAO8B,GAAW,GAChD,QAAgBpD,IAAZmD,IAAyBC,EAA7B,CAEA,IAAKC,EAAAA,OAAOC,wBAAwBH,GAChC,MAAMF,EAAOtB,kBAAkBL,EAAO,kCAA2CtB,IAAZmD,EAAwB,UAAYA,IAC7G,OAAOA,CAHa,CAIxB,CAEA,SAASI,EAAYC,EAAQb,GACzB,QAAe3C,IAAXwD,EACA,OACJ,MAAMC,EAASC,WAAWF,GAC1B,GAAIG,MAAMF,IAAWA,GAAU,EAC3B,MAAMR,EAAOtB,kBAAkBgB,EAAW,yBAA2Ba,GACzE,OAAOA,CACX,CAEA,SAASI,EAAuBC,EAAKlB,GACjC,MAAMmB,EAAYJ,WAAWG,GAC7B,GAAIF,MAAMG,GACN,MAAMb,EAAOtB,kBAAkBgB,EAAW,+CAC9C,GAAImB,EAAY,EACZ,MAAMb,EAAOtB,kBAAkBgB,EAAW,wDAC9C,GAAImB,EAAY,GACZ,MAAMb,EAAOtB,kBAAkBgB,EAAW,+DAC9C,OAAOmB,CACX,CCmRO,MAAMC,EAAsB,CAAC,QAAS,WAAY,WAAY,SAErE,SAASC,EAAcC,GACnB,OAAKA,aAAsBC,QACnBD,EACOC,QAAQC,QAAQF,GACpBC,QAAQE,SAEZH,CACX,CCzXA,QDGe,MAIXhD,WAAAA,CAAYoD,GACR,IAAKA,EAAOC,QACR,MAAMrB,EAAO5B,iBAAiB,WAClC3B,KAAK6E,WAAaF,EAAOE,WACzB7E,KAAK8E,QAAU,IAAIC,YACnB/E,KAAK4E,QAAUI,EAAAA,SAASL,EAAOC,QAAQK,gBAAkBN,EAAOC,QAChE5E,KAAKkF,KAAOP,EAAOO,MAAQ,QAC/B,CAMAC,OAAS,eAKTC,OAKAN,QAKAO,OAAS,6BAMTT,QAAU,GAMVU,aAMAC,UAKAC,WAMAX,WAMAY,cAMA,UAAIC,GACA,OAAO1F,KAAKsF,cAAcK,WAC9B,CAMAC,OAAAA,GACI,OAAI5F,KAAKoF,QAAQS,aAAeC,UAAUC,KAC/BvB,QAAQC,QAAQzE,OAE3BA,KAAKoF,OAAS,IAAIU,UAAU9F,KAAKqF,OAAS,eAAiBW,mBAAmBhG,KAAK6E,aACnF7E,KAAKoF,OAAOa,UAAYjG,KAAKkG,eAAeC,KAAKnG,MACjDA,KAAKoF,OAAOgB,QAAU,KAClBC,QAAQC,IAAI,qBACZtG,KAAKmF,OAAS,cAAc,EAGhCnF,KAAKoF,OAAOmB,QAAUC,GAAKH,QAAQI,MAAMD,GAElC,IAAIhC,SAAQ,CAACkC,EAAShC,KACzB,MAAMiC,EAAoBC,YAAW,IAAMlC,EAAO1E,OAAO,KACzDA,KAAK6G,aAAe,KAChB7G,KAAKmF,OAAS,QACdnF,KAAK8G,YACL9G,KAAK6G,kBAAevG,EACpByG,aAAaJ,GACbD,EAAQ1G,KAAK,CAChB,IAET,CAOAkG,cAAAA,CAAezE,GACX,MAAMuF,EAAMC,KAAKC,MAAMzF,EAAQkB,MAC/B,OAAQqE,EAAIxE,MACR,IAAK,YACGxC,KAAK6G,cACL7G,KAAK6G,eAET,MACJ,IAAK,QACD7G,KAAKuF,UAAYyB,EAAIG,MACrBnH,KAAKuF,UAAU6B,GAAK,IAAIC,KAExBrH,KAAK8E,QAAQwC,cAAc5E,EAAW,QAAS1C,KAAKuF,YACpD,MACJ,IAAK,KACD,GAAoB,UAAhBvF,KAAKmF,OAEL,YADAkB,QAAQC,IAAI,mCAAoCtG,KAAKmF,OAAQ6B,GAGjEhH,KAAKuH,iBAAiBP,GACtB,MACJ,IAAK,OACDhH,KAAK8E,QAAQwC,cAAc5E,EAAW,WAAY,CAC9CyC,OAAQ6B,EAAI7B,OACZqC,KAAMR,EAAIQ,KACVC,OAAQT,EAAIS,QACb,WACHzH,KAAKmF,OAAS,QACd,MACJ,IAAK,WACDnF,KAAK8E,QAAQwC,cAAc5E,EAAW,WAAY,CAC9C8E,KAAMR,EAAIQ,KACVC,OAAQT,EAAIS,QACb,WACH,MACJ,IAAK,OACDzH,KAAK8G,YACL9G,KAAK0H,KAAK,CAAClF,KAAM,SACjB,MACJ,IAAK,QACDxC,KAAK2H,OACL3H,KAAK8E,QAAQwC,cAAc5E,EAAW,QAAS,iBAAmBsE,EAAIP,QACtE,MACJ,QACIJ,QAAQC,IAAI,yBAA2BU,EAAIxE,MAGvD,CAMA2E,KAAAA,CAAMxC,GACF,GAAoB,UAAhB3E,KAAKmF,OACL,MAAM5B,EAAOnB,kBACjBpC,KAAKsF,aD3JN,SAA8BX,GACjC,MAAM,YACFgB,EAAW,aAAEiC,EAAY,cAAEC,EAAa,YAAEC,EAAW,aAAEC,EAAY,cAAEC,EAAa,eAAEC,EAAc,aAClGC,EAAY,cAAEC,EAAa,kBAAEC,EAAiB,mBAAEC,KAAuBC,GACvE3D,EACE4D,EAAM,CACRX,aAAc7E,EAAW6E,GAAgBC,EAAe,gBACxDC,YAAa/E,EAAW+E,GAAeC,EAAc,eACrDC,cAAenE,EAAYmE,GAAiBC,EAAgB,iBAC5DC,aAAcrE,EAAYqE,GAAgBC,EAAe,gBACzDC,kBAAmBlE,EAAuBkE,GAAqBC,GAAsB,IAAM,qBAC3FnD,KAAM,UAKV,GAHIS,IACA4C,EAAI5C,YAAcrC,EAAgBqC,EAAa,eAAe,IAE9D4C,EAAIT,cAAgBS,EAAIX,aACxB,MAAMrE,EAAOtB,kBAAkB,cAAe,mDAClD,QAAyB3B,IAArBiI,EAAIL,mBAAoD5H,IAAtBiI,EAAIP,cACtC,MAAMzE,EAAOtB,kBAAkB,gBAAiB,kEACpD,QAAyB3B,IAArBiI,EAAIL,mBAAoD5H,IAAtBiI,EAAIP,cACtC,MAAMzE,EAAOtB,kBAAkB,gBAAiB,wEAEpD,OADArB,OAAO4H,OAAOD,EAAKD,GACZC,CACX,CCmI4BE,CAAqB9D,GACzC3E,KAAKmF,OAAS,QACdnF,KAAK4F,UACA8C,MAAK,KACF1I,KAAK0H,KAAK,CACNlF,KAAM,WACHxC,KAAKsF,cACV,GAEd,CAKAqC,IAAAA,GACwB,UAAhB3H,KAAKmF,QAAsC,UAAhBnF,KAAKmF,SAEpCnF,KAAK0H,KAAK,CACNlF,KAAM,SAEVxC,KAAKwF,gBAAalF,EAClBN,KAAKmF,OAAS,QAClB,CAMAwD,YAAAA,CAAalD,GACT,GAAoB,UAAhBzF,KAAKmF,OACL,MAAM5B,EAAOnB,kBACjB,IAAKpC,KAAKuF,UACN,MAAMhC,EAAOzB,cACjB,GAAK,IAAIuF,KAASrH,KAAKuF,UAAU6B,GAAM,IACnC,MAAM7D,EAAOxB,eACjB,GAA8B,YAA1B/B,KAAKuF,UAAUJ,OACf,MAAM5B,EAAOvB,WAAWhC,KAAKuF,UAAUkB,OAAS,uBACpD,GAA6B,iBAAlBhB,EACP,IACIzF,KAAKyF,cAAgBmD,EAAAA,QAAQC,WAAWpD,EAC5C,CAAE,MAAOe,GACL,MAAMjD,EAAO1B,2BACjB,KACgC,mBAAlB4D,IACdzF,KAAKyF,cAAgBA,GAEzBzF,KAAKwF,WAAaxF,KAAKuF,UACvBvF,KAAK0H,KAAK,CACNlF,KAAM,UAEVxC,KAAKmF,OAAS,OAClB,CAMAoC,gBAAAA,CAAiBP,GAEb,IAAI8B,EACJ,IACIA,EAAKC,EAAAA,mBAAmBC,QAAQhC,EAAIiC,IAAKjJ,KAAK4E,QAClD,CAAE,MAAO4B,GACL,MAAMjD,EAAOlB,eACjB,CAEA,IAAKrC,KAAKkJ,oBAAoBJ,GAC1B,MAAMvF,EAAOlB,gBAEjBrC,KAAKmJ,YAAYL,EAAI9B,EAAIoC,YACpBV,MAAKI,IACF,IAAKA,GAAMA,EAAGO,WAAWC,OAAS,EAC9B,OAAOjD,QAAQI,MAAM,eAAeO,EAAIuC,mBAE5CvJ,KAAK0H,KAAK,CACNlF,KAAM,KACN+G,KAAMvC,EAAIuC,KACVN,IAAKH,EAAGU,SACV,IAELC,OAAMjD,IAEH,MADAH,QAAQI,MAAMD,GACRjD,EAAOjB,gBAAgB,GAEzC,CAQA,iBAAM6G,CAAYL,EAAIM,GAEdpJ,KAAKyF,yBAAyBmD,EAAAA,QAC9BE,EAAGY,KAAK1J,KAAKyF,eAEbqD,QAAWxE,EAActE,KAAKyF,cAAcqD,IAGhD,IAAIa,EAAUZ,EAAAA,mBAAmBa,wBAAwB5J,KAAK0F,OAAQ0D,EAAYN,EAAI9I,KAAK4E,SAO3F,OALI5E,KAAKyF,yBAAyBmD,EAAAA,QAC9Be,EAAQD,KAAK1J,KAAKyF,eAElBkE,QAAgBrF,EAActE,KAAKyF,cAAckE,IAE9CA,CACX,CAMAT,mBAAAA,CAAoBJ,GAMhB,IAAK,IAAIe,KAAQf,EAAGgB,WAAY,CAC5B,GAAkB,0BAAdD,EAAKrH,MAAkD,6BAAdqH,EAAKrH,KAC9C,OAAO,EAEX,GADcqH,EAAKlE,cAAgB3F,KAAK0F,OAC7B,CACP,GAAkB,0BAAdmE,EAAKrH,KACL,OAAO,EACX,GAAKqH,EAAKnE,QAAUmE,EAAKnE,SAAW1F,KAAK0F,OACrC,OAAO,CACf,MACI,GAAKmE,EAAKnE,QAAUmE,EAAKnE,SAAWmE,EAAKlE,aAAgBkE,EAAKlE,cAAgB3F,KAAK0F,OAC/E,OAAO,CAEnB,CAEA,OAAO,CACX,CAMAgC,IAAAA,CAAK/E,GACD3C,KAAKoF,OAAOsC,KAAKT,KAAK8C,UAAUpH,GACpC,CAEAmE,SAAAA,GACIC,aAAa/G,KAAKgK,aAClBhK,KAAKgK,YAAcpD,YAAW,KAC1BP,QAAQ4D,KAAK,mCACbjK,KAAKoF,OAAO8E,OAAO,GACpB,KACP,CAOAC,EAAAA,CAAG3H,EAAM4H,GACL,IAAK/F,EAAoBnB,SAASV,GAC9B,MAAMe,EAAOhB,qBAAqBC,GACtCxC,KAAK8E,QAAQuF,iBAAiB7H,EAAM4H,EACxC,CAOAE,IAAAA,CAAK9H,EAAM4H,GACP,IAAK/F,EAAoBnB,SAASV,GAC9B,MAAMe,EAAOhB,qBAAqBC,GACtCxC,KAAK8E,QAAQuF,iBAAiB7H,EAAM4H,EAAU,CAACE,MAAM,GACzD,CAOAC,GAAAA,CAAI/H,EAAM4H,GACN,IAAK/F,EAAoBnB,SAASV,GAC9B,MAAMe,EAAOhB,qBAAqBC,GACtCxC,KAAK8E,QAAQ0F,oBAAoBhI,EAAM4H,EAC3C,CAKAF,KAAAA,GACI,IACIlK,KAAKoF,OAAO8E,OAChB,CAAE,MAAO1D,GACT,CACJ,G","sources":["webpack://stellarbroker/webpack/universalModuleDefinition","webpack://stellarbroker/external umd \"@stellar/stellar-base\"","webpack://stellarbroker/webpack/bootstrap","webpack://stellarbroker/webpack/runtime/define property getters","webpack://stellarbroker/webpack/runtime/hasOwnProperty shorthand","webpack://stellarbroker/./src/errors.js","webpack://stellarbroker/./src/events.js","webpack://stellarbroker/./src/quote-request.js","webpack://stellarbroker/./src/client.js","webpack://stellarbroker/./src/index.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"@stellar/stellar-base\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"@stellar/stellar-base\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"stellarbroker\"] = factory(require(\"@stellar/stellar-base\"));\n\telse\n\t\troot[\"stellarbroker\"] = factory(root[\"@stellar/stellar-base\"]);\n})(this, (__WEBPACK_EXTERNAL_MODULE__755__) => {\nreturn ","module.exports = __WEBPACK_EXTERNAL_MODULE__755__;","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","export class StellarBrokerError extends Error {\r\n constructor(code, message) {\r\n super(message)\r\n this.code = code\r\n }\r\n\r\n /**\r\n * Numeric error code\r\n * @type {number}\r\n * @readonly\r\n */\r\n code = 0\r\n}\r\n\r\nconst errors = {\r\n invalidInitParam(param) {\r\n return new StellarBrokerError(1, `Invalid parameter value: \"${param}\"`)\r\n },\r\n invalidAuthorizationParam() {\r\n return new StellarBrokerError(2, 'Invalid authorization secret key or callback provided')\r\n },\r\n quoteNotSet() {\r\n return new StellarBrokerError(11, 'Price quote not available')\r\n },\r\n quoteExpired() {\r\n return new StellarBrokerError(12, 'Price quote expired')\r\n },\r\n quoteError(message) {\r\n return new StellarBrokerError(13, 'Price quotation error: ' + message)\r\n },\r\n invalidQuoteParam(invalidParamName, details) {\r\n return new StellarBrokerError(14, `Invalid quote request parameter: \"${invalidParamName}\". ${details}`)\r\n },\r\n tradeInProgress() {\r\n return new StellarBrokerError(20, 'Cannot change quote while trade is in progress')\r\n },\r\n invalidSwapTx() {\r\n return new StellarBrokerError(21, 'Invalid swap transaction received')\r\n },\r\n failedToSignTx() {\r\n return new StellarBrokerError(22, 'Failed to sign received transaction')\r\n },\r\n unsupportedEventType(type) {\r\n return new StellarBrokerError(31, 'Unknown event type: ' + type)\r\n },\r\n serverError(message) {\r\n return new StellarBrokerError(101, 'Server error: ' + message)\r\n }\r\n}\r\n\r\nexport default errors","/**\r\n * @param {string} type\r\n * @param {{}} data\r\n * @param {string} [key]\r\n * @return {CustomEvent}\r\n */\r\nexport function buildEvent(type, data, key) {\r\n const evt = new CustomEvent(type, {detail: data})\r\n evt[key || type] = data\r\n return evt\r\n}","import {StrKey} from '@stellar/stellar-base'\r\nimport errors from './errors.js'\r\n\r\n/**\r\n * @typedef {object} SwapQuoteParams\r\n * @property {string} sellingAsset - Asset to sell\r\n * @property {string} buyingAsset - Asset to buy\r\n * @property {string} [sellingAmount] - Amount of selling asset\r\n * @property {string} [buyingAmount] - Amount of buying asset\r\n * @property {number} [slippageTolerance] - Swap slippage tolerance (0.02 by default - 2%)\r\n * @property {string} [destination] - Trader account address\r\n */\r\n\r\n/**\r\n * @param {SwapQuoteParams} params\r\n * @return {SwapQuoteParams}\r\n */\r\nexport function validateQuoteRequest(params) {\r\n const {\r\n destination, sellingAsset, selling_asset, buyingAsset, buying_asset, sellingAmount, selling_amount,\r\n buyingAmount, buying_amount, slippageTolerance, slippage_tolerance, ...other\r\n } = params\r\n const res = {\r\n sellingAsset: parseAsset(sellingAsset || selling_asset, 'sellingAsset'),\r\n buyingAsset: parseAsset(buyingAsset || buying_asset, 'buyingAsset'),\r\n sellingAmount: parseAmount(sellingAmount || selling_amount, 'sellingAmount'),\r\n buyingAmount: parseAmount(buyingAmount || buying_amount, 'buyingAmount'),\r\n slippageTolerance: parseSlippageTolerance(slippageTolerance || slippage_tolerance || 0.02, 'slippageTolerance'),\r\n flow: 'direct'\r\n }\r\n if (destination) {\r\n res.destination = validateAccount(destination, 'destination', true)\r\n }\r\n if (res.buyingAsset === res.sellingAsset)\r\n throw errors.invalidQuoteParam('buyingAsset', 'Buying asset can\\'t be the same as selling asset')\r\n if (res.buyingAmount === undefined && res.sellingAmount === undefined)\r\n throw errors.invalidQuoteParam('sellingAmount', 'Either \"buyingAmount\" or \"sellingAmount\" parameter is required')\r\n if (res.buyingAmount !== undefined && res.sellingAmount !== undefined)\r\n throw errors.invalidQuoteParam('sellingAmount', 'Parameters \"buyingAmount\" and \"sellingAmount\" are mutually exclusive')\r\n Object.assign(res, other) //add remaining optional params\r\n return res\r\n}\r\n\r\nfunction parseAsset(asset, parameter) {\r\n if (typeof asset === 'string') {\r\n if (asset === 'XLM' || asset === 'xlm' || asset === 'native')\r\n return 'XLM'\r\n if (asset.includes(':')) {\r\n const [code, issuer] = asset.split(':')\r\n validateCode(code)\r\n validateAccount(issuer, parameter)\r\n return code + '-' + issuer\r\n }\r\n if (asset.includes('-')) {\r\n const [code, issuer] = asset.split('-')\r\n validateCode(code)\r\n validateAccount(issuer, parameter)\r\n return asset\r\n }\r\n }\r\n throw errors.invalidQuoteParam(parameter, 'Invalid asset')\r\n}\r\n\r\nfunction validateCode(code, parameter) {\r\n if (!/^[a-zA-Z0-9]{1,12}$/.test(code))\r\n throw errors.invalidQuoteParam(parameter, 'Invalid asset code: ' + (code === undefined ? 'missing' : code))\r\n return code\r\n}\r\n\r\nfunction validateAccount(account, param, optional = false) {\r\n if (account === undefined && optional)\r\n return undefined\r\n if (!StrKey.isValidEd25519PublicKey(account))\r\n throw errors.invalidQuoteParam(param, 'Invalid account address: ' + (account === undefined ? 'missing' : account))\r\n return account\r\n}\r\n\r\nfunction parseAmount(amount, parameter) {\r\n if (amount === undefined)\r\n return undefined\r\n const parsed = parseFloat(amount)\r\n if (isNaN(parsed) || parsed <= 0)\r\n throw errors.invalidQuoteParam(parameter, 'Invalid asset amount: ' + amount)\r\n return amount\r\n}\r\n\r\nfunction parseSlippageTolerance(src, parameter) {\r\n const tolerance = parseFloat(src)\r\n if (isNaN(tolerance))\r\n throw errors.invalidQuoteParam(parameter, 'Invalid slippage tolerance, number expected')\r\n if (tolerance < 0)\r\n throw errors.invalidQuoteParam(parameter, 'Slippage tolerance is too small, expected value >= 0')\r\n if (tolerance > 0.5)\r\n throw errors.invalidQuoteParam(parameter, 'Slippage tolerance is too large, expected value < 0.5 (50%)')\r\n return tolerance\r\n}","import {Networks, TransactionBuilder, Keypair, FeeBumpTransaction} from '@stellar/stellar-base'\r\nimport errors from './errors.js'\r\nimport {buildEvent} from './events.js'\r\nimport {validateQuoteRequest} from './quote-request.js'\r\n\r\nexport default class StellarBrokerClient {\r\n /**\r\n * @param {ClientInitializationParams} params\r\n */\r\n constructor(params) {\r\n if (!params.network)\r\n throw errors.invalidInitParam('network')\r\n this.partnerKey = params.partnerKey\r\n this.emitter = new EventTarget()\r\n this.network = Networks[params.network.toUpperCase()] || params.network\r\n this.flow = params.flow || 'direct'\r\n }\r\n\r\n /**\r\n * @type {ClientStatus}\r\n * @readonly\r\n */\r\n status = 'disconnected'\r\n /**\r\n * @type {WebSocket}\r\n * @private\r\n */\r\n socket\r\n /**\r\n * @type {EventTarget}\r\n * @private\r\n */\r\n emitter\r\n /**\r\n * @type {string}\r\n * @private\r\n */\r\n origin = 'https://api.stellar.broker'\r\n /**\r\n * Stellar network passphrase\r\n * @type {string}\r\n * @readonly\r\n */\r\n network = ''\r\n /**\r\n * Last quote request submitted by the client\r\n * @type {SwapQuoteParams}\r\n * @private\r\n */\r\n quoteRequest\r\n /**\r\n * Last quote received from the sever\r\n * @type {SwapQuoteResult}\r\n * @readonly\r\n */\r\n lastQuote\r\n /**\r\n * Current quote accepted for the trading\r\n * @private\r\n */\r\n tradeQuote\r\n /**\r\n * API key of the partner\r\n * @type {string}\r\n * @private\r\n */\r\n partnerKey\r\n /**\r\n * Account secret key (for direct swap flow)\r\n * @type {Keypair|ClientAuthorizationCallback}\r\n * @private\r\n */\r\n authorization\r\n\r\n /**\r\n * Trader account public key\r\n * @type {string}\r\n */\r\n get source() {\r\n return this.quoteRequest?.destination\r\n }\r\n\r\n /**\r\n * Connect to the StellarBroker server\r\n * @return {Promise<StellarBrokerClient>}\r\n */\r\n connect() {\r\n if (this.socket?.readyState === WebSocket.OPEN)\r\n return Promise.resolve(this) //already opened\r\n\r\n this.socket = new WebSocket(this.origin + '/ws?partner=' + encodeURIComponent(this.partnerKey))\r\n this.socket.onmessage = this.processMessage.bind(this)\r\n this.socket.onclose = () => {\r\n console.log('Connection closed')\r\n this.status = 'disconnected'\r\n }\r\n\r\n this.socket.onerror = e => console.error(e)\r\n\r\n return new Promise((confirm, reject) => { //TODO: use 'once'\r\n const expirationTimeout = setTimeout(() => reject(this), 2000) // 2s timeout\r\n this.onSocketOpen = () => {\r\n this.status = 'ready'\r\n this.heartbeat()\r\n this.onSocketOpen = undefined\r\n clearTimeout(expirationTimeout)\r\n confirm(this)\r\n }\r\n })\r\n }\r\n\r\n /**\r\n * Process incoming message\r\n * @param message\r\n * @private\r\n */\r\n processMessage(message) {\r\n const raw = JSON.parse(message.data)\r\n switch (raw.type) {\r\n case 'connected':\r\n if (this.onSocketOpen) {\r\n this.onSocketOpen()\r\n }\r\n break\r\n case 'quote':\r\n this.lastQuote = raw.quote\r\n this.lastQuote.ts = new Date()\r\n //send event to the client app\r\n this.emitter.dispatchEvent(buildEvent('quote', this.lastQuote))\r\n break\r\n case 'tx':\r\n if (this.status !== 'trade') {\r\n console.log('Received tx in non-trading state', this.status, raw)\r\n return //skip unless trading is in progress\r\n }\r\n this.processTxRequest(raw)\r\n break\r\n case 'stop':\r\n this.emitter.dispatchEvent(buildEvent('finished', {\r\n status: raw.status,\r\n sold: raw.sold,\r\n bought: raw.bought\r\n }, 'result'))\r\n this.status = 'ready'\r\n break\r\n case 'progress':\r\n this.emitter.dispatchEvent(buildEvent('progress', {\r\n sold: raw.sold,\r\n bought: raw.bought\r\n }, 'status'))\r\n break\r\n case 'ping':\r\n this.heartbeat()\r\n this.send({type: 'pong'})\r\n break\r\n case 'error':\r\n this.stop()\r\n this.emitter.dispatchEvent(buildEvent('error', 'Server error: ' + raw.error))\r\n break\r\n default:\r\n console.log('Unknown message type: ' + raw.type)\r\n break\r\n }\r\n }\r\n\r\n /**\r\n * Request swap quote\r\n * @param {SwapQuoteParams} params - Quote parameters\r\n */\r\n quote(params) {\r\n if (this.status === 'trade')\r\n throw errors.tradeInProgress()\r\n this.quoteRequest = validateQuoteRequest(params)\r\n this.status = 'quote'\r\n this.connect()\r\n .then(() => {\r\n this.send({\r\n type: 'quote',\r\n ...this.quoteRequest\r\n })\r\n })\r\n }\r\n\r\n /**\r\n * Stop quotation/trading\r\n */\r\n stop() {\r\n if (this.status !== 'trade' && this.status !== 'quote')\r\n return\r\n this.send({\r\n type: 'stop'\r\n })\r\n this.tradeQuote = undefined\r\n this.status = 'ready'\r\n }\r\n\r\n /**\r\n * Confirm current quote and start trading\r\n * @param {string|ClientAuthorizationCallback} authorization - Authorization method, either account secret key or an authorization callback\r\n */\r\n confirmQuote(authorization) {\r\n if (this.status !== 'quote')\r\n throw errors.tradeInProgress()\r\n if (!this.lastQuote)\r\n throw errors.quoteNotSet()\r\n if ((new Date() - this.lastQuote.ts) > 7000) //do not allow stale quotes quoted more than 7s ago\r\n throw errors.quoteExpired()\r\n if (this.lastQuote.status !== 'success')\r\n throw errors.quoteError(this.lastQuote.error || 'quote not available')\r\n if (typeof authorization === 'string') {\r\n try {\r\n this.authorization = Keypair.fromSecret(authorization)\r\n } catch (e) {\r\n throw errors.invalidAuthorizationParam()\r\n }\r\n } else if (typeof authorization === 'function') {\r\n this.authorization = authorization\r\n }\r\n this.tradeQuote = this.lastQuote\r\n this.send({\r\n type: 'trade'\r\n })\r\n this.status = 'trade'\r\n }\r\n\r\n /**\r\n * @param {{xdr: string, hash: string}} raw\r\n * @private\r\n */\r\n processTxRequest(raw) {\r\n //parse incoming transaction\r\n let tx\r\n try {\r\n tx = TransactionBuilder.fromXDR(raw.xdr, this.network)\r\n } catch (e) {\r\n throw errors.invalidSwapTx()\r\n }\r\n //check that transaction is correct\r\n if (!this.validateTransaction(tx))\r\n throw errors.invalidSwapTx()\r\n //sign transaction\r\n this.authorizeTx(tx, raw.networkFee)\r\n .then(tx => {\r\n if (!tx || tx.signatures.length < 1)\r\n return console.error(`Transaction ${raw.hash} not signed`)\r\n //respond with signed transaction\r\n this.send({\r\n type: 'tx',\r\n hash: raw.hash,\r\n xdr: tx.toXDR()\r\n })\r\n })\r\n .catch(e => {\r\n console.error(e)\r\n throw errors.failedToSignTx()\r\n })\r\n }\r\n\r\n /**\r\n * @param {Transaction} tx\r\n * @param {string} networkFee\r\n * @return {Promise<FeeBumpTransaction>}\r\n * @private\r\n */\r\n async authorizeTx(tx, networkFee) {\r\n //sign tx\r\n if (this.authorization instanceof Keypair) {\r\n tx.sign(this.authorization)\r\n } else {\r\n tx = await ensurePromise(this.authorization(tx))\r\n }\r\n //wrap with fee bump\r\n let wrapped = TransactionBuilder.buildFeeBumpTransaction(this.source, networkFee, tx, this.network)\r\n //sign fee bump wrapper tx\r\n if (this.authorization instanceof Keypair) {\r\n wrapped.sign(this.authorization)\r\n } else {\r\n wrapped = await ensurePromise(this.authorization(wrapped))\r\n }\r\n return wrapped\r\n }\r\n\r\n /**\r\n * @param {Transaction} tx\r\n * @throws {StellarBrokerError} Invalid swap transaction received\r\n */\r\n validateTransaction(tx) {\r\n /*if (!(tx instanceof FeeBumpTransaction))\r\n return false\r\n const {innerTransaction} = tx\r\n if (tx.feeSource !== this.source)\r\n return false*/\r\n for (let swap of tx.operations) {\r\n if (swap.type !== 'pathPaymentStrictSend' && swap.type !== 'pathPaymentStrictReceive')\r\n return false\r\n const isFee = swap.destination !== this.source\r\n if (isFee) {\r\n if (swap.type !== 'pathPaymentStrictSend')\r\n return false\r\n if ((swap.source && swap.source !== this.source))\r\n return false\r\n } else {\r\n if ((swap.source && swap.source !== swap.destination) || swap.destination !== this.source)\r\n return false\r\n }\r\n }\r\n //TODO: check assets and amounts\r\n return true\r\n }\r\n\r\n /**\r\n * @param {{}} data\r\n * @private\r\n */\r\n send(data) {\r\n this.socket.send(JSON.stringify(data))\r\n }\r\n\r\n heartbeat() {\r\n clearTimeout(this.pingHandler)\r\n this.pingHandler = setTimeout(() => {\r\n console.warn('Lost connection with the server')\r\n this.socket.close()\r\n }, 11_000) // 11 seconds heartbeat timeout\r\n }\r\n\r\n /**\r\n * Add event listener\r\n * @param {StellarBrokerClientEvent} type\r\n * @param {function} callback\r\n */\r\n on(type, callback) {\r\n if (!StellarBrokerEvents.includes(type))\r\n throw errors.unsupportedEventType(type)\r\n this.emitter.addEventListener(type, callback)\r\n }\r\n\r\n /**\r\n * Add event listener that will be executed once\r\n * @param {StellarBrokerClientEvent} type\r\n * @param {function} callback\r\n */\r\n once(type, callback) {\r\n if (!StellarBrokerEvents.includes(type))\r\n throw errors.unsupportedEventType(type)\r\n this.emitter.addEventListener(type, callback, {once: true})\r\n }\r\n\r\n /**\r\n * Remove event listener\r\n * @param {StellarBrokerClientEvent} type\r\n * @param {function} callback\r\n */\r\n off(type, callback) {\r\n if (!StellarBrokerEvents.includes(type))\r\n throw errors.unsupportedEventType(type)\r\n this.emitter.removeEventListener(type, callback)\r\n }\r\n\r\n /**\r\n * Close underlying connection and finalize the client\r\n */\r\n close() {\r\n try {\r\n this.socket.close()\r\n } catch (e) {\r\n }\r\n }\r\n}\r\n\r\nexport const StellarBrokerEvents = ['quote', 'finished', 'progress', 'error']\r\n\r\nfunction ensurePromise(callResult) {\r\n if (!callResult instanceof Promise) {\r\n if (callResult)\r\n return Promise.resolve(callResult)\r\n return Promise.reject()\r\n }\r\n return callResult\r\n}\r\n\r\n/**\r\n * @typedef {object} ClientInitializationParams\r\n * @property {string} [network] - Stellar network identifier or passphrase\r\n * @property {string} [partnerKey] - Partner key\r\n * @property {SwapFlowMode} [flow] - Swap flow mode\r\n */\r\n\r\n/**\r\n * @typedef {'disconnected'|'ready'|'quote'|'trade'} ClientStatus\r\n */\r\n\r\n/**\r\n * @typedef {'quote'|'finished'|'progress'|'error'} StellarBrokerClientEvent\r\n */\r\n\r\n/**\r\n * @typedef {'direct'} SwapFlowMode\r\n */\r\n\r\n/**\r\n * @typedef {object} SwapQuoteResult\r\n * @property {string} sellingAsset\r\n * @property {string} buyingAsset\r\n * @property {number} slippageTolerance\r\n * @property {string} destination\r\n * @property {number} ledger\r\n * @property {string} [sellingAmount]\r\n * @property {string} [estimatedBuyingAmount]\r\n * @property {string} [buyingAmount]\r\n * @property {string} [estimatedSellingAmount]\r\n * @property {{selling: string, buying: string, path: string[]}} [directTrade]\r\n */\r\n\r\n/**\r\n * @typedef {function(FeeBumpTransaction):Promise<FeeBumpTransaction>} ClientAuthorizationCallback\r\n */","import StellarBrokerClient from './client.js'\r\n\r\nexport default StellarBrokerClient"],"names":["root","factory","exports","module","require","define","amd","this","__WEBPACK_EXTERNAL_MODULE__755__","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","d","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","StellarBrokerError","Error","constructor","code","message","super","invalidInitParam","param","invalidAuthorizationParam","quoteNotSet","quoteExpired","quoteError","invalidQuoteParam","invalidParamName","details","tradeInProgress","invalidSwapTx","failedToSignTx","unsupportedEventType","type","serverError","buildEvent","data","evt","CustomEvent","detail","parseAsset","asset","parameter","includes","issuer","split","validateCode","validateAccount","errors","test","account","optional","StrKey","isValidEd25519PublicKey","parseAmount","amount","parsed","parseFloat","isNaN","parseSlippageTolerance","src","tolerance","StellarBrokerEvents","ensurePromise","callResult","Promise","resolve","reject","params","network","partnerKey","emitter","EventTarget","Networks","toUpperCase","flow","status","socket","origin","quoteRequest","lastQuote","tradeQuote","authorization","source","destination","connect","readyState","WebSocket","OPEN","encodeURIComponent","onmessage","processMessage","bind","onclose","console","log","onerror","e","error","confirm","expirationTimeout","setTimeout","onSocketOpen","heartbeat","clearTimeout","raw","JSON","parse","quote","ts","Date","dispatchEvent","processTxRequest","sold","bought","send","stop","sellingAsset","selling_asset","buyingAsset","buying_asset","sellingAmount","selling_amount","buyingAmount","buying_amount","slippageTolerance","slippage_tolerance","other","res","assign","validateQuoteRequest","then","confirmQuote","Keypair","fromSecret","tx","TransactionBuilder","fromXDR","xdr","validateTransaction","authorizeTx","networkFee","signatures","length","hash","toXDR","catch","sign","wrapped","buildFeeBumpTransaction","swap","operations","stringify","pingHandler","warn","close","on","callback","addEventListener","once","off","removeEventListener"],"sourceRoot":""}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@stellar-broker/client",
3
+ "version": "0.1.0",
4
+ "description": "Browser client library for StellarBroker",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "module": "src/index.js",
8
+ "types": "src/index.d.ts",
9
+ "scripts": {
10
+ "build": "webpack --mode=production --config webpack.config.cjs",
11
+ "test": "jest"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/stellar-broker/client"
16
+ },
17
+ "author": "",
18
+ "license": "MIT",
19
+ "peerDependencies": {
20
+ "@stellar/stellar-base": ">=11"
21
+ },
22
+ "devDependencies": {
23
+ "@babel/core": "^7.24.8",
24
+ "@babel/preset-env": "^7.24.8",
25
+ "babel-loader": "^9.1.3",
26
+ "jest": "^29.7.0",
27
+ "typescript": "^5.5.4",
28
+ "webpack": "^5.93.0",
29
+ "webpack-cli": "^5.1.4",
30
+ "ws": "^8.17.1"
31
+ }
32
+ }
package/src/client.js ADDED
@@ -0,0 +1,417 @@
1
+ import {Networks, TransactionBuilder, Keypair, FeeBumpTransaction} from '@stellar/stellar-base'
2
+ import errors from './errors.js'
3
+ import {buildEvent} from './events.js'
4
+ import {validateQuoteRequest} from './quote-request.js'
5
+
6
+ export default class StellarBrokerClient {
7
+ /**
8
+ * @param {ClientInitializationParams} params
9
+ */
10
+ constructor(params) {
11
+ if (!params.network)
12
+ throw errors.invalidInitParam('network')
13
+ this.partnerKey = params.partnerKey
14
+ this.emitter = new EventTarget()
15
+ this.network = Networks[params.network.toUpperCase()] || params.network
16
+ this.flow = params.flow || 'direct'
17
+ }
18
+
19
+ /**
20
+ * @type {ClientStatus}
21
+ * @readonly
22
+ */
23
+ status = 'disconnected'
24
+ /**
25
+ * @type {WebSocket}
26
+ * @private
27
+ */
28
+ socket
29
+ /**
30
+ * @type {EventTarget}
31
+ * @private
32
+ */
33
+ emitter
34
+ /**
35
+ * @type {string}
36
+ * @private
37
+ */
38
+ origin = 'https://api.stellar.broker'
39
+ /**
40
+ * Stellar network passphrase
41
+ * @type {string}
42
+ * @readonly
43
+ */
44
+ network = ''
45
+ /**
46
+ * Last quote request submitted by the client
47
+ * @type {SwapQuoteParams}
48
+ * @private
49
+ */
50
+ quoteRequest
51
+ /**
52
+ * Last quote received from the sever
53
+ * @type {SwapQuoteResult}
54
+ * @readonly
55
+ */
56
+ lastQuote
57
+ /**
58
+ * Current quote accepted for the trading
59
+ * @private
60
+ */
61
+ tradeQuote
62
+ /**
63
+ * API key of the partner
64
+ * @type {string}
65
+ * @private
66
+ */
67
+ partnerKey
68
+ /**
69
+ * Account secret key (for direct swap flow)
70
+ * @type {Keypair|ClientAuthorizationCallback}
71
+ * @private
72
+ */
73
+ authorization
74
+
75
+ /**
76
+ * Trader account public key
77
+ * @type {string}
78
+ */
79
+ get source() {
80
+ return this.quoteRequest?.destination
81
+ }
82
+
83
+ /**
84
+ * Connect to the StellarBroker server
85
+ * @return {Promise<StellarBrokerClient>}
86
+ */
87
+ connect() {
88
+ if (this.socket?.readyState === WebSocket.OPEN)
89
+ return Promise.resolve(this) //already opened
90
+
91
+ this.socket = new WebSocket(this.origin + '/ws?partner=' + encodeURIComponent(this.partnerKey))
92
+ this.socket.onmessage = this.processMessage.bind(this)
93
+ this.socket.onclose = () => {
94
+ console.log('Connection closed')
95
+ this.status = 'disconnected'
96
+ }
97
+
98
+ this.socket.onerror = e => console.error(e)
99
+
100
+ return new Promise((confirm, reject) => { //TODO: use 'once'
101
+ const expirationTimeout = setTimeout(() => reject(this), 2000) // 2s timeout
102
+ this.onSocketOpen = () => {
103
+ this.status = 'ready'
104
+ this.heartbeat()
105
+ this.onSocketOpen = undefined
106
+ clearTimeout(expirationTimeout)
107
+ confirm(this)
108
+ }
109
+ })
110
+ }
111
+
112
+ /**
113
+ * Process incoming message
114
+ * @param message
115
+ * @private
116
+ */
117
+ processMessage(message) {
118
+ const raw = JSON.parse(message.data)
119
+ switch (raw.type) {
120
+ case 'connected':
121
+ if (this.onSocketOpen) {
122
+ this.onSocketOpen()
123
+ }
124
+ break
125
+ case 'quote':
126
+ this.lastQuote = raw.quote
127
+ this.lastQuote.ts = new Date()
128
+ //send event to the client app
129
+ this.emitter.dispatchEvent(buildEvent('quote', this.lastQuote))
130
+ break
131
+ case 'tx':
132
+ if (this.status !== 'trade') {
133
+ console.log('Received tx in non-trading state', this.status, raw)
134
+ return //skip unless trading is in progress
135
+ }
136
+ this.processTxRequest(raw)
137
+ break
138
+ case 'stop':
139
+ this.emitter.dispatchEvent(buildEvent('finished', {
140
+ status: raw.status,
141
+ sold: raw.sold,
142
+ bought: raw.bought
143
+ }, 'result'))
144
+ this.status = 'ready'
145
+ break
146
+ case 'progress':
147
+ this.emitter.dispatchEvent(buildEvent('progress', {
148
+ sold: raw.sold,
149
+ bought: raw.bought
150
+ }, 'status'))
151
+ break
152
+ case 'ping':
153
+ this.heartbeat()
154
+ this.send({type: 'pong'})
155
+ break
156
+ case 'error':
157
+ this.stop()
158
+ this.emitter.dispatchEvent(buildEvent('error', 'Server error: ' + raw.error))
159
+ break
160
+ default:
161
+ console.log('Unknown message type: ' + raw.type)
162
+ break
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Request swap quote
168
+ * @param {SwapQuoteParams} params - Quote parameters
169
+ */
170
+ quote(params) {
171
+ if (this.status === 'trade')
172
+ throw errors.tradeInProgress()
173
+ this.quoteRequest = validateQuoteRequest(params)
174
+ this.status = 'quote'
175
+ this.connect()
176
+ .then(() => {
177
+ this.send({
178
+ type: 'quote',
179
+ ...this.quoteRequest
180
+ })
181
+ })
182
+ }
183
+
184
+ /**
185
+ * Stop quotation/trading
186
+ */
187
+ stop() {
188
+ if (this.status !== 'trade' && this.status !== 'quote')
189
+ return
190
+ this.send({
191
+ type: 'stop'
192
+ })
193
+ this.tradeQuote = undefined
194
+ this.status = 'ready'
195
+ }
196
+
197
+ /**
198
+ * Confirm current quote and start trading
199
+ * @param {string|ClientAuthorizationCallback} authorization - Authorization method, either account secret key or an authorization callback
200
+ */
201
+ confirmQuote(authorization) {
202
+ if (this.status !== 'quote')
203
+ throw errors.tradeInProgress()
204
+ if (!this.lastQuote)
205
+ throw errors.quoteNotSet()
206
+ if ((new Date() - this.lastQuote.ts) > 7000) //do not allow stale quotes quoted more than 7s ago
207
+ throw errors.quoteExpired()
208
+ if (this.lastQuote.status !== 'success')
209
+ throw errors.quoteError(this.lastQuote.error || 'quote not available')
210
+ if (typeof authorization === 'string') {
211
+ try {
212
+ this.authorization = Keypair.fromSecret(authorization)
213
+ } catch (e) {
214
+ throw errors.invalidAuthorizationParam()
215
+ }
216
+ } else if (typeof authorization === 'function') {
217
+ this.authorization = authorization
218
+ }
219
+ this.tradeQuote = this.lastQuote
220
+ this.send({
221
+ type: 'trade'
222
+ })
223
+ this.status = 'trade'
224
+ }
225
+
226
+ /**
227
+ * @param {{xdr: string, hash: string}} raw
228
+ * @private
229
+ */
230
+ processTxRequest(raw) {
231
+ //parse incoming transaction
232
+ let tx
233
+ try {
234
+ tx = TransactionBuilder.fromXDR(raw.xdr, this.network)
235
+ } catch (e) {
236
+ throw errors.invalidSwapTx()
237
+ }
238
+ //check that transaction is correct
239
+ if (!this.validateTransaction(tx))
240
+ throw errors.invalidSwapTx()
241
+ //sign transaction
242
+ this.authorizeTx(tx, raw.networkFee)
243
+ .then(tx => {
244
+ if (!tx || tx.signatures.length < 1)
245
+ return console.error(`Transaction ${raw.hash} not signed`)
246
+ //respond with signed transaction
247
+ this.send({
248
+ type: 'tx',
249
+ hash: raw.hash,
250
+ xdr: tx.toXDR()
251
+ })
252
+ })
253
+ .catch(e => {
254
+ console.error(e)
255
+ throw errors.failedToSignTx()
256
+ })
257
+ }
258
+
259
+ /**
260
+ * @param {Transaction} tx
261
+ * @param {string} networkFee
262
+ * @return {Promise<FeeBumpTransaction>}
263
+ * @private
264
+ */
265
+ async authorizeTx(tx, networkFee) {
266
+ //sign tx
267
+ if (this.authorization instanceof Keypair) {
268
+ tx.sign(this.authorization)
269
+ } else {
270
+ tx = await ensurePromise(this.authorization(tx))
271
+ }
272
+ //wrap with fee bump
273
+ let wrapped = TransactionBuilder.buildFeeBumpTransaction(this.source, networkFee, tx, this.network)
274
+ //sign fee bump wrapper tx
275
+ if (this.authorization instanceof Keypair) {
276
+ wrapped.sign(this.authorization)
277
+ } else {
278
+ wrapped = await ensurePromise(this.authorization(wrapped))
279
+ }
280
+ return wrapped
281
+ }
282
+
283
+ /**
284
+ * @param {Transaction} tx
285
+ * @throws {StellarBrokerError} Invalid swap transaction received
286
+ */
287
+ validateTransaction(tx) {
288
+ /*if (!(tx instanceof FeeBumpTransaction))
289
+ return false
290
+ const {innerTransaction} = tx
291
+ if (tx.feeSource !== this.source)
292
+ return false*/
293
+ for (let swap of tx.operations) {
294
+ if (swap.type !== 'pathPaymentStrictSend' && swap.type !== 'pathPaymentStrictReceive')
295
+ return false
296
+ const isFee = swap.destination !== this.source
297
+ if (isFee) {
298
+ if (swap.type !== 'pathPaymentStrictSend')
299
+ return false
300
+ if ((swap.source && swap.source !== this.source))
301
+ return false
302
+ } else {
303
+ if ((swap.source && swap.source !== swap.destination) || swap.destination !== this.source)
304
+ return false
305
+ }
306
+ }
307
+ //TODO: check assets and amounts
308
+ return true
309
+ }
310
+
311
+ /**
312
+ * @param {{}} data
313
+ * @private
314
+ */
315
+ send(data) {
316
+ this.socket.send(JSON.stringify(data))
317
+ }
318
+
319
+ heartbeat() {
320
+ clearTimeout(this.pingHandler)
321
+ this.pingHandler = setTimeout(() => {
322
+ console.warn('Lost connection with the server')
323
+ this.socket.close()
324
+ }, 11_000) // 11 seconds heartbeat timeout
325
+ }
326
+
327
+ /**
328
+ * Add event listener
329
+ * @param {StellarBrokerClientEvent} type
330
+ * @param {function} callback
331
+ */
332
+ on(type, callback) {
333
+ if (!StellarBrokerEvents.includes(type))
334
+ throw errors.unsupportedEventType(type)
335
+ this.emitter.addEventListener(type, callback)
336
+ }
337
+
338
+ /**
339
+ * Add event listener that will be executed once
340
+ * @param {StellarBrokerClientEvent} type
341
+ * @param {function} callback
342
+ */
343
+ once(type, callback) {
344
+ if (!StellarBrokerEvents.includes(type))
345
+ throw errors.unsupportedEventType(type)
346
+ this.emitter.addEventListener(type, callback, {once: true})
347
+ }
348
+
349
+ /**
350
+ * Remove event listener
351
+ * @param {StellarBrokerClientEvent} type
352
+ * @param {function} callback
353
+ */
354
+ off(type, callback) {
355
+ if (!StellarBrokerEvents.includes(type))
356
+ throw errors.unsupportedEventType(type)
357
+ this.emitter.removeEventListener(type, callback)
358
+ }
359
+
360
+ /**
361
+ * Close underlying connection and finalize the client
362
+ */
363
+ close() {
364
+ try {
365
+ this.socket.close()
366
+ } catch (e) {
367
+ }
368
+ }
369
+ }
370
+
371
+ export const StellarBrokerEvents = ['quote', 'finished', 'progress', 'error']
372
+
373
+ function ensurePromise(callResult) {
374
+ if (!callResult instanceof Promise) {
375
+ if (callResult)
376
+ return Promise.resolve(callResult)
377
+ return Promise.reject()
378
+ }
379
+ return callResult
380
+ }
381
+
382
+ /**
383
+ * @typedef {object} ClientInitializationParams
384
+ * @property {string} [network] - Stellar network identifier or passphrase
385
+ * @property {string} [partnerKey] - Partner key
386
+ * @property {SwapFlowMode} [flow] - Swap flow mode
387
+ */
388
+
389
+ /**
390
+ * @typedef {'disconnected'|'ready'|'quote'|'trade'} ClientStatus
391
+ */
392
+
393
+ /**
394
+ * @typedef {'quote'|'finished'|'progress'|'error'} StellarBrokerClientEvent
395
+ */
396
+
397
+ /**
398
+ * @typedef {'direct'} SwapFlowMode
399
+ */
400
+
401
+ /**
402
+ * @typedef {object} SwapQuoteResult
403
+ * @property {string} sellingAsset
404
+ * @property {string} buyingAsset
405
+ * @property {number} slippageTolerance
406
+ * @property {string} destination
407
+ * @property {number} ledger
408
+ * @property {string} [sellingAmount]
409
+ * @property {string} [estimatedBuyingAmount]
410
+ * @property {string} [buyingAmount]
411
+ * @property {string} [estimatedSellingAmount]
412
+ * @property {{selling: string, buying: string, path: string[]}} [directTrade]
413
+ */
414
+
415
+ /**
416
+ * @typedef {function(FeeBumpTransaction):Promise<FeeBumpTransaction>} ClientAuthorizationCallback
417
+ */
package/src/errors.js ADDED
@@ -0,0 +1,51 @@
1
+ export class StellarBrokerError extends Error {
2
+ constructor(code, message) {
3
+ super(message)
4
+ this.code = code
5
+ }
6
+
7
+ /**
8
+ * Numeric error code
9
+ * @type {number}
10
+ * @readonly
11
+ */
12
+ code = 0
13
+ }
14
+
15
+ const errors = {
16
+ invalidInitParam(param) {
17
+ return new StellarBrokerError(1, `Invalid parameter value: "${param}"`)
18
+ },
19
+ invalidAuthorizationParam() {
20
+ return new StellarBrokerError(2, 'Invalid authorization secret key or callback provided')
21
+ },
22
+ quoteNotSet() {
23
+ return new StellarBrokerError(11, 'Price quote not available')
24
+ },
25
+ quoteExpired() {
26
+ return new StellarBrokerError(12, 'Price quote expired')
27
+ },
28
+ quoteError(message) {
29
+ return new StellarBrokerError(13, 'Price quotation error: ' + message)
30
+ },
31
+ invalidQuoteParam(invalidParamName, details) {
32
+ return new StellarBrokerError(14, `Invalid quote request parameter: "${invalidParamName}". ${details}`)
33
+ },
34
+ tradeInProgress() {
35
+ return new StellarBrokerError(20, 'Cannot change quote while trade is in progress')
36
+ },
37
+ invalidSwapTx() {
38
+ return new StellarBrokerError(21, 'Invalid swap transaction received')
39
+ },
40
+ failedToSignTx() {
41
+ return new StellarBrokerError(22, 'Failed to sign received transaction')
42
+ },
43
+ unsupportedEventType(type) {
44
+ return new StellarBrokerError(31, 'Unknown event type: ' + type)
45
+ },
46
+ serverError(message) {
47
+ return new StellarBrokerError(101, 'Server error: ' + message)
48
+ }
49
+ }
50
+
51
+ export default errors
@@ -0,0 +1,20 @@
1
+ import errors, {StellarBrokerError} from './errors.js'
2
+ import {validateQuoteRequest} from './quote-request.js'
3
+
4
+ /**
5
+ * Estimate price swap without trading.
6
+ * @param {SwapQuoteParams} params
7
+ */
8
+ export async function estimateSwap(params) {
9
+ const query = Object.entries(validateQuoteRequest(params)).map(([param, value]) => encodeURIComponent(param) + '=' + encodeURIComponent(value))
10
+ const url = (params.origin || 'https://api.stellar.broker') + '/quote?' + query.join('&')
11
+ try {
12
+ const res = await fetch(url).then(r => r.json())
13
+ if (res.status !== 'success')
14
+ throw errors.quoteError(res.error || 'Quote not available')
15
+ } catch (e) {
16
+ if (e instanceof StellarBrokerError)
17
+ throw e
18
+ throw errors.quoteError('Failed to fetch quote')
19
+ }
20
+ }
package/src/events.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @param {string} type
3
+ * @param {{}} data
4
+ * @param {string} [key]
5
+ * @return {CustomEvent}
6
+ */
7
+ export function buildEvent(type, data, key) {
8
+ const evt = new CustomEvent(type, {detail: data})
9
+ evt[key || type] = data
10
+ return evt
11
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,142 @@
1
+ export default class StellarBrokerClient {
2
+ /**
3
+ * @param {ClientInitializationParams} params
4
+ */
5
+ constructor(params: ClientInitializationParams);
6
+
7
+ /**
8
+ * Trader account public key
9
+ * @type {string}
10
+ */
11
+ readonly source: string;
12
+ /**
13
+ * Stellar network passphrase
14
+ * @type {string}
15
+ */
16
+ readonly network: string;
17
+ /**
18
+ * @type {ClientStatus}
19
+ */
20
+ readonly status: ClientStatus;
21
+
22
+ /**
23
+ * Connect to the StellarBroker server
24
+ * @return {Promise<StellarBrokerClient>}
25
+ */
26
+ connect(): Promise<StellarBrokerClient>;
27
+
28
+ /**
29
+ * Request swap quote
30
+ * @param {SwapQuoteParams} params - Quote parameters
31
+ */
32
+ quote(params: SwapQuoteParams): void;
33
+
34
+ /**
35
+ * Stop quotation/trading
36
+ */
37
+ stop(): void;
38
+
39
+ /**
40
+ * Confirm current quote and start trading
41
+ * @param {string|ClientAuthorizationCallback} authorization - Authorization method, either account secret key or an authorization callback
42
+ */
43
+ confirmQuote(authorization: string | ClientAuthorizationCallback): void;
44
+
45
+ /**
46
+ * Add event listener
47
+ * @param {StellarBrokerClientEvent} type
48
+ * @param {function} callback
49
+ */
50
+ on(type: StellarBrokerClientEvent, callback: Function): void;
51
+
52
+ /**
53
+ * Add event listener that will be executed once
54
+ * @param {StellarBrokerClientEvent} type
55
+ * @param {function} callback
56
+ */
57
+ once(type: StellarBrokerClientEvent, callback: Function): void;
58
+
59
+ /**
60
+ * Remove event listener
61
+ * @param {StellarBrokerClientEvent} type
62
+ * @param {function} callback
63
+ */
64
+ off(type: StellarBrokerClientEvent, callback: Function): void;
65
+ /**
66
+ * Close underlying connection and finalize the client
67
+ */
68
+ close(): void;
69
+ }
70
+
71
+ export interface SwapQuoteParams {
72
+ /**
73
+ * Asset to sell
74
+ */
75
+ sellingAsset: string
76
+ /**
77
+ * Asset to buy
78
+ */
79
+ buyingAsset: string
80
+ /**
81
+ * Amount of selling asset
82
+ */
83
+ sellingAmount?: string
84
+ /**
85
+ * Amount of buying asset
86
+ */
87
+ buyingAmount?: string
88
+ /**
89
+ * Swap slippage tolerance (2% by default)
90
+ */
91
+ slippageTolerance?: number
92
+ }
93
+
94
+
95
+ export interface ClientInitializationParams {
96
+ /**
97
+ * Trader account public key
98
+ */
99
+ account: string;
100
+ /**
101
+ * Stellar network identifier or passphrase (pubnet by default)
102
+ */
103
+ network?: string;
104
+ /**
105
+ * Partner key
106
+ */
107
+ partnerKey?: string;
108
+ /**
109
+ * Swap flow mode
110
+ */
111
+ flow?: SwapFlowMode;
112
+ }
113
+
114
+ export type ClientStatus = "disconnected" | "ready" | "quote" | "trade";
115
+
116
+ export type StellarBrokerClientEvent = "quote" | "finished" | "progress";
117
+
118
+ export type SwapFlowMode = "direct";
119
+
120
+ export interface SwapQuoteResult {
121
+ sellingAsset: string;
122
+ buyingAsset: string;
123
+ slippageTolerance: number;
124
+ destination: string;
125
+ ledger: number;
126
+ sellingAmount?: string;
127
+ estimatedBuyingAmount?: string;
128
+ buyingAmount?: string;
129
+ estimatedSellingAmount?: string;
130
+ }
131
+
132
+ export class StellarBrokerError extends Error {
133
+ /**
134
+ * Numeric error code
135
+ * @type {number}
136
+ * @readonly
137
+ */
138
+ readonly code: number;
139
+ }
140
+
141
+ export type ClientAuthorizationCallback = (arg0: FeeBumpTransaction) => Promise<FeeBumpTransaction>;
142
+ import {FeeBumpTransaction} from "@stellar/stellar-base";
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import StellarBrokerClient from './client.js'
2
+
3
+ export default StellarBrokerClient
@@ -0,0 +1,96 @@
1
+ import {StrKey} from '@stellar/stellar-base'
2
+ import errors from './errors.js'
3
+
4
+ /**
5
+ * @typedef {object} SwapQuoteParams
6
+ * @property {string} sellingAsset - Asset to sell
7
+ * @property {string} buyingAsset - Asset to buy
8
+ * @property {string} [sellingAmount] - Amount of selling asset
9
+ * @property {string} [buyingAmount] - Amount of buying asset
10
+ * @property {number} [slippageTolerance] - Swap slippage tolerance (0.02 by default - 2%)
11
+ * @property {string} [destination] - Trader account address
12
+ */
13
+
14
+ /**
15
+ * @param {SwapQuoteParams} params
16
+ * @return {SwapQuoteParams}
17
+ */
18
+ export function validateQuoteRequest(params) {
19
+ const {
20
+ destination, sellingAsset, selling_asset, buyingAsset, buying_asset, sellingAmount, selling_amount,
21
+ buyingAmount, buying_amount, slippageTolerance, slippage_tolerance, ...other
22
+ } = params
23
+ const res = {
24
+ sellingAsset: parseAsset(sellingAsset || selling_asset, 'sellingAsset'),
25
+ buyingAsset: parseAsset(buyingAsset || buying_asset, 'buyingAsset'),
26
+ sellingAmount: parseAmount(sellingAmount || selling_amount, 'sellingAmount'),
27
+ buyingAmount: parseAmount(buyingAmount || buying_amount, 'buyingAmount'),
28
+ slippageTolerance: parseSlippageTolerance(slippageTolerance || slippage_tolerance || 0.02, 'slippageTolerance'),
29
+ flow: 'direct'
30
+ }
31
+ if (destination) {
32
+ res.destination = validateAccount(destination, 'destination', true)
33
+ }
34
+ if (res.buyingAsset === res.sellingAsset)
35
+ throw errors.invalidQuoteParam('buyingAsset', 'Buying asset can\'t be the same as selling asset')
36
+ if (res.buyingAmount === undefined && res.sellingAmount === undefined)
37
+ throw errors.invalidQuoteParam('sellingAmount', 'Either "buyingAmount" or "sellingAmount" parameter is required')
38
+ if (res.buyingAmount !== undefined && res.sellingAmount !== undefined)
39
+ throw errors.invalidQuoteParam('sellingAmount', 'Parameters "buyingAmount" and "sellingAmount" are mutually exclusive')
40
+ Object.assign(res, other) //add remaining optional params
41
+ return res
42
+ }
43
+
44
+ function parseAsset(asset, parameter) {
45
+ if (typeof asset === 'string') {
46
+ if (asset === 'XLM' || asset === 'xlm' || asset === 'native')
47
+ return 'XLM'
48
+ if (asset.includes(':')) {
49
+ const [code, issuer] = asset.split(':')
50
+ validateCode(code)
51
+ validateAccount(issuer, parameter)
52
+ return code + '-' + issuer
53
+ }
54
+ if (asset.includes('-')) {
55
+ const [code, issuer] = asset.split('-')
56
+ validateCode(code)
57
+ validateAccount(issuer, parameter)
58
+ return asset
59
+ }
60
+ }
61
+ throw errors.invalidQuoteParam(parameter, 'Invalid asset')
62
+ }
63
+
64
+ function validateCode(code, parameter) {
65
+ if (!/^[a-zA-Z0-9]{1,12}$/.test(code))
66
+ throw errors.invalidQuoteParam(parameter, 'Invalid asset code: ' + (code === undefined ? 'missing' : code))
67
+ return code
68
+ }
69
+
70
+ function validateAccount(account, param, optional = false) {
71
+ if (account === undefined && optional)
72
+ return undefined
73
+ if (!StrKey.isValidEd25519PublicKey(account))
74
+ throw errors.invalidQuoteParam(param, 'Invalid account address: ' + (account === undefined ? 'missing' : account))
75
+ return account
76
+ }
77
+
78
+ function parseAmount(amount, parameter) {
79
+ if (amount === undefined)
80
+ return undefined
81
+ const parsed = parseFloat(amount)
82
+ if (isNaN(parsed) || parsed <= 0)
83
+ throw errors.invalidQuoteParam(parameter, 'Invalid asset amount: ' + amount)
84
+ return amount
85
+ }
86
+
87
+ function parseSlippageTolerance(src, parameter) {
88
+ const tolerance = parseFloat(src)
89
+ if (isNaN(tolerance))
90
+ throw errors.invalidQuoteParam(parameter, 'Invalid slippage tolerance, number expected')
91
+ if (tolerance < 0)
92
+ throw errors.invalidQuoteParam(parameter, 'Slippage tolerance is too small, expected value >= 0')
93
+ if (tolerance > 0.5)
94
+ throw errors.invalidQuoteParam(parameter, 'Slippage tolerance is too large, expected value < 0.5 (50%)')
95
+ return tolerance
96
+ }
@@ -0,0 +1,43 @@
1
+ const path = require('path')
2
+ const webpack = require('webpack')
3
+
4
+ module.exports = function () {
5
+ return {
6
+ mode: 'production',
7
+ devtool: 'source-map',
8
+ entry: {
9
+ stellarbroker: [path.join(__dirname, '/src/index.js')]
10
+ },
11
+ output: {
12
+ path: path.join(__dirname, './lib'),
13
+ filename: '[name].js',
14
+ library: {
15
+ name: 'stellarbroker',
16
+ type: 'umd',
17
+ export: 'default'
18
+ },
19
+ libraryTarget: 'umd',
20
+ globalObject: 'this'
21
+ },
22
+ module: {
23
+ rules: [
24
+ {
25
+ test: /\.js?$/,
26
+ loader: 'babel-loader',
27
+ exclude: /node_modules/
28
+ }
29
+ ]
30
+ },
31
+ externals: {
32
+ '@stellar/stellar-base': '@stellar/stellar-base'
33
+ },
34
+ plugins: [
35
+ new webpack.DefinePlugin({
36
+ 'process.env.NODE_ENV': JSON.stringify('production')
37
+ })
38
+ ],
39
+ optimization: {
40
+ minimize: true
41
+ }
42
+ }
43
+ }