├── Makefile ├── README.md ├── altnet.js ├── balance.js ├── cost.js ├── generate.js ├── hedge.js ├── index.js ├── kill.js ├── ledger.js ├── offer.js ├── package.json ├── send.js ├── trust.js ├── view.js ├── what.js └── xmm.js /Makefile: -------------------------------------------------------------------------------- 1 | CONF = dummy.json 2 | TEST = node xmm -c $(CONF) 3 | 4 | all: 5 | npm install 6 | $(TEST) altnet >|$(CONF) 7 | $(TEST) generate 8 | $(TEST) ledger 9 | $(TEST) what root 10 | $(TEST) what XRP@bank 11 | $(TEST) what EUR.bank:.1e-1@fund 12 | $(TEST) what USD:500/BTC:1~42@fund 13 | $(TEST) balance bank 14 | $(TEST) send XRP:123.456@bank XRP:123.456@fund 15 | $(TEST) ledger 2 16 | $(TEST) balance fund 17 | $(TEST) offer XMM:100/XRP:99~0@fund 18 | $(TEST) trust USD:100@fund 19 | $(TEST) cost USD:12.3@fund bank 20 | $(TEST) ledger 21 | $(TEST) view fund 22 | $(TEST) kill XMM:100/XRP:99~1@fund 23 | $(TEST) send USD:12.3@bank USD:12.3@fund 24 | $(TEST) balance -n 3 fund 25 | $(TEST) view fund 26 | 27 | clean: 28 | -rm -f $(CONF) 29 | -rm -fr node_modules 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This package encapsulates [RippleAPI][2], providing both CLI and API, 2 | and implements an automated multi-currency market maker for 3 | [the Ripple network][1] using [the Talmud strategy][4]. 4 | 5 | # CLI 6 | 7 | This package provides an `xmm` command with the following interface: 8 | 9 | ``` 10 | Usage: xmm [options] [arguments] 11 | 12 | Commands: 13 | altnet Generate altnet configuration [aliases: dummy] 14 | balance Check balances in a wallet [aliases: b, bal] 15 | cost [me] Estimate cost of a value [aliases: price] 16 | generate Create a new address [aliases: gen, new] 17 | hedge Apply the Talmud strategy [aliases: run, talmud] 18 | kill Cancel an existing order [aliases: cancel, rm] 19 | ledger [count] Wait for ledger(s) to close [aliases: wait] 20 | offer Create a limit order [aliases: create, order] 21 | send Pay or convert a value [aliases: pay] 22 | trust Set a trust line [aliases: set] 23 | view List active orders [aliases: active, list] 24 | what Tell what a string means [aliases: parse] 25 | 26 | Options: 27 | --assets Dictionary of assets [default: {}] 28 | --count, -n Number of ledgers to close [default: 1] 29 | --cushion, -f Factor to multiply estimated fee [default: 1] 30 | --delta, -d Stake to trade [default: 0.01] 31 | --dry, -p Output script without running [boolean] 32 | --hedge List of pairs to trade [default: []] 33 | --ledger, -l Historical ledger version [number] 34 | --maxfee, -m Maximum fee to pay [default: 0.00001] 35 | --offset, -o Offset from the current legder [default: 3] 36 | --server, -s WebSocket server [default: "wss://s1.ripple.com"] 37 | --timeout, -t Timeout in seconds [default: 60] 38 | --wallets Dictionary of wallets [default: {}] 39 | --yes, -y Do not ask for confirmation [boolean] 40 | --config, -c Path to JSON config file [default: ~/.xmm.json] 41 | --version, -v Show version number [boolean] 42 | --help, -h Show help [boolean] 43 | 44 | ``` 45 | 46 | # API 47 | 48 | Through `require("xmm")` this package exports two functions. 49 | 50 | The `.altnet()` function returns a Promise of dummy configuration 51 | for [the Ripple Test Net][3] with one funded account created for 52 | developing and testing purposes. 53 | 54 | The `.connect(config)` function takes configuration as its argument and 55 | returns a Promise of an XMM object encapsulating an instance of [RippleAPI][2] 56 | already connected to a server and after a ledger has been closed. 57 | 58 | [1]: https://ripple.com/ 59 | [2]: https://ripple.com/build/rippleapi/ 60 | [3]: https://ripple.com/build/ripple-test-net/ 61 | [4]: https://github.com/codedot/xmm/wiki 62 | 63 | # License 64 | 65 | Copyright (c) 2015 Anton Salikhmetov 66 | 67 | Permission is hereby granted, free of charge, to any person obtaining a copy 68 | of this software and associated documentation files (the "Software"), to deal 69 | in the Software without restriction, including without limitation the rights 70 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 71 | copies of the Software, and to permit persons to whom the Software is 72 | furnished to do so, subject to the following conditions: 73 | 74 | The above copyright notice and this permission notice shall be included in 75 | all copies or substantial portions of the Software. 76 | 77 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 78 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 79 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 80 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 81 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 82 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 83 | THE SOFTWARE. 84 | -------------------------------------------------------------------------------- /altnet.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "altnet"; 4 | exports.desc = "Generate altnet configuration"; 5 | exports.aliases = [ 6 | "dummy" 7 | ]; 8 | exports.builder = yargs => yargs; 9 | exports.handler = config => { 10 | require(".").altnet().then(dummy => { 11 | console.info(JSON.stringify(dummy, null, "\t")); 12 | process.exit(); 13 | }).catch(abort); 14 | }; 15 | -------------------------------------------------------------------------------- /balance.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "balance "; 4 | exports.desc = "Check balances in a wallet"; 5 | exports.aliases = [ 6 | "b", 7 | "bal" 8 | ]; 9 | exports.builder = yargs => yargs; 10 | exports.handler = connect((config, xmm) => { 11 | xmm.balance(config.me, config.ledger).then(lines => { 12 | lines.forEach(line => { 13 | console.info(line.human); 14 | }); 15 | process.exit(); 16 | }).catch(abort); 17 | }); 18 | -------------------------------------------------------------------------------- /cost.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "cost [me]"; 4 | exports.desc = "Estimate cost of a value"; 5 | exports.aliases = [ 6 | "price" 7 | ]; 8 | exports.builder = yargs => yargs; 9 | exports.handler = connect((config, xmm) => { 10 | xmm.cost(config.dst, config.me).then(lines => { 11 | lines.forEach(line => { 12 | console.info(line.human); 13 | }); 14 | process.exit(); 15 | }).catch(abort); 16 | }); 17 | -------------------------------------------------------------------------------- /generate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "generate"; 4 | exports.desc = "Create a new address"; 5 | exports.aliases = [ 6 | "gen", 7 | "new" 8 | ]; 9 | exports.builder = yargs => yargs; 10 | exports.handler = config => { 11 | const wallet = require(".").generate(); 12 | 13 | console.info(JSON.stringify(wallet, null, "\t")); 14 | process.exit(); 15 | }; 16 | -------------------------------------------------------------------------------- /hedge.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function talmud(src, dst, stake) 4 | { 5 | const sell = stake * src / (1 + stake); 6 | const buy = stake * dst / (1 - stake); 7 | const ratio = (1 - sell / src) * (1 + buy / dst); 8 | 9 | return { 10 | stake: stake, 11 | price: buy / sell, 12 | sell: sell, 13 | buy: buy, 14 | ratio: ratio 15 | }; 16 | } 17 | 18 | function decide(entry) 19 | { 20 | const need = entry.need; 21 | const proper = entry.proper; 22 | const offer = entry.offer; 23 | const active = entry.active.sort((a, b) => { 24 | if (a.ratio < b.ratio) 25 | return -1; 26 | else 27 | return 1; 28 | }); 29 | const best = active.pop(); 30 | 31 | entry.best = best; 32 | entry.rest = active; 33 | 34 | if (!best && need) 35 | return "absent"; 36 | 37 | if (best) { 38 | const ratio = best.ratio; 39 | const delta = best.price / proper.price - 1; 40 | 41 | offer.ratio = ratio; 42 | offer.delta = delta; 43 | offer.seq = best.seq; 44 | offer.old = best.human; 45 | 46 | if (ratio < Math.sqrt(proper.ratio)) { 47 | if (need) 48 | return "bad"; 49 | else 50 | return "zombie"; 51 | } 52 | 53 | if (delta > proper.stake) 54 | if (need) 55 | return "far"; 56 | } 57 | } 58 | 59 | exports.command = "hedge "; 60 | exports.desc = "Apply the Talmud strategy"; 61 | exports.aliases = [ 62 | "run", 63 | "talmud" 64 | ]; 65 | exports.builder = yargs => yargs; 66 | exports.handler = connect((config, xmm) => { 67 | const id = xmm.parse(config.me); 68 | const me = id.human; 69 | const wallet = id.wallet; 70 | const ledger = config.ledger; 71 | const cancel = (p, offer) => p.then(() => { 72 | return xmm.cancel(offer).then(print); 73 | }); 74 | const create = (p, offer) => p.then(() => { 75 | return xmm.create(offer).then(print); 76 | }); 77 | const select = (offers, saldo) => { 78 | const all = Object.keys(saldo); 79 | const pairs = config.hedge.map(pair => { 80 | const list = pair.split("/"); 81 | 82 | if (1 == list.length) 83 | list.push(list[0]); 84 | 85 | return list.map(assets => { 86 | if ("*" == assets) 87 | return all; 88 | 89 | assets = assets.split(","); 90 | return assets.map(asset => { 91 | asset = `${asset}@${me}`; 92 | asset = xmm.parse(asset); 93 | return asset.human; 94 | }); 95 | }); 96 | }).reduce((list, pair) => { 97 | while (pair.length >= 2) { 98 | list.push(pair.slice(0, 2)); 99 | pair.shift(); 100 | } 101 | 102 | return list; 103 | }, []).reduce((list, pair) => { 104 | const left = pair.shift(); 105 | const right = pair.shift(); 106 | 107 | left.forEach(a => { 108 | right.forEach(b => { 109 | list.push(`${a}/${b}`); 110 | list.push(`${b}/${a}`); 111 | }); 112 | }); 113 | return list; 114 | }, []); 115 | 116 | pairs.forEach(pair => { 117 | const offer = offers[pair]; 118 | 119 | if (offer) 120 | offer.need = true; 121 | }); 122 | }; 123 | const getoffer = (entry) => { 124 | const proper = entry.proper; 125 | const src = xmm.parse(entry.src); 126 | const dst = xmm.parse(entry.dst); 127 | 128 | return xmm.parse({ 129 | type: "offer", 130 | base: src.asset, 131 | asset: dst.asset, 132 | value: proper.buy, 133 | cost: proper.sell, 134 | wallet: wallet 135 | }); 136 | }; 137 | const compute = (saldo) => { 138 | const assets = Object.keys(saldo); 139 | const stake = config.delta; 140 | const offers = {}; 141 | 142 | assets.forEach(base => { 143 | assets.forEach(asset => { 144 | const pair = `${asset}/${base}`; 145 | const src = saldo[base]; 146 | const dst = saldo[asset]; 147 | let proper, offer, entry; 148 | 149 | if (asset == base) 150 | return; 151 | 152 | proper = talmud(src, dst, stake); 153 | 154 | entry = { 155 | src: base, 156 | dst: asset, 157 | proper: proper, 158 | active: [] 159 | }; 160 | entry.offer = getoffer(entry); 161 | offers[pair] = entry; 162 | }); 163 | }); 164 | 165 | select(offers, saldo); 166 | return offers; 167 | }; 168 | const dryrun = (zombie, bad, absent, far) => { 169 | const drykill = (offer) => { 170 | console.info("kill", offer.human); 171 | }; 172 | const dryoffer = (offer) => { 173 | console.info("offer", offer.human); 174 | }; 175 | 176 | console.info("# zombie"); 177 | zombie.forEach(drykill); 178 | 179 | console.info("# bad"); 180 | bad.forEach(dryoffer); 181 | 182 | console.info("# absent"); 183 | absent.forEach(dryoffer); 184 | 185 | console.info("# far"); 186 | far.forEach(dryoffer); 187 | }; 188 | const sequence = (zombie, bad, absent, far) => { 189 | let script = Promise.resolve(); 190 | let safe; 191 | 192 | script = zombie.reduce(cancel, script); 193 | script = bad.reduce(create, script); 194 | 195 | if (zombie.length || bad.length) 196 | return script; 197 | 198 | safe = absent.concat(far).slice(0, 1); 199 | return safe.reduce(create, script); 200 | }; 201 | 202 | Promise.all([ 203 | xmm.balance(me, ledger), 204 | xmm.offers(me, ledger) 205 | ]).then(state => { 206 | const saldo = {}; 207 | const zombie = []; 208 | const bad = []; 209 | const absent = []; 210 | const far = []; 211 | let offers; 212 | 213 | state[0].forEach(line => { 214 | const value = line.value; 215 | 216 | if (value > 0) { 217 | const asset = xmm.parse({ 218 | type: "asset", 219 | asset: line.asset, 220 | wallet: wallet 221 | }).human; 222 | 223 | saldo[asset] = value; 224 | return true; 225 | } 226 | }); 227 | 228 | offers = compute(saldo); 229 | 230 | state[1].forEach(line => { 231 | const src = xmm.parse({ 232 | type: "asset", 233 | asset: line.base, 234 | wallet: wallet 235 | }).human; 236 | const dst = xmm.parse({ 237 | type: "asset", 238 | asset: line.asset, 239 | wallet: wallet 240 | }).human; 241 | const pair = offers[`${dst}/${src}`]; 242 | let ratio = 1; 243 | 244 | if (!pair) 245 | return; 246 | 247 | ratio *= 1 - line.cost / saldo[src]; 248 | ratio *= 1 + line.value / saldo[dst]; 249 | line.ratio = ratio; 250 | 251 | pair.active.push(line); 252 | }); 253 | 254 | for (const pair in offers) { 255 | const entry = offers[pair]; 256 | const status = decide(entry); 257 | 258 | if ("zombie" == status) 259 | zombie.push(entry.best); 260 | 261 | if ("bad" == status) 262 | bad.push(entry.offer); 263 | 264 | if ("absent" == status) 265 | absent.push(entry.offer); 266 | 267 | if ("far" == status) 268 | far.push(entry.offer); 269 | 270 | zombie.push.apply(zombie, entry.rest); 271 | } 272 | 273 | bad.sort((a, b) => a.ratio - b.ratio); 274 | far.sort((a, b) => b.delta - a.delta); 275 | 276 | if (config.dry) { 277 | dryrun(zombie, bad, absent, far); 278 | process.exit(); 279 | } 280 | 281 | sequence(zombie, bad, absent, far).then(() => { 282 | process.exit(); 283 | }).catch(abort); 284 | }).catch(abort); 285 | }); 286 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const faucet = "http://faucet.altnet.rippletest.net/accounts"; 4 | const altnet = "ws://s.altnet.rippletest.net:51233"; 5 | const root = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; 6 | const syntax = "^(?:_(?::_(?:\\\/_:_~_)?)?@)?_$"; 7 | const pattern = syntax.replace(/_/g, "([^:@\\\/~]+)"); 8 | const re = new RegExp(pattern); 9 | 10 | class XMMarg { 11 | constructor(xmm, obj) { 12 | this.shorten = xmm.shorten.bind(xmm); 13 | this.toabs = xmm.toabs.bind(xmm); 14 | this.wallets = xmm.wallets; 15 | this.assets = xmm.assets; 16 | 17 | this.input = JSON.stringify(obj); 18 | this.type = "undefined"; 19 | 20 | if ("string" == typeof obj) 21 | obj = this.parse(obj); 22 | 23 | if (!obj) 24 | return; 25 | 26 | this.type = obj.type; 27 | this.base = obj.base; 28 | this.cost = obj.cost; 29 | this.seq = obj.seq; 30 | this.value = obj.value; 31 | this.asset = obj.asset; 32 | this.wallet = obj.wallet; 33 | } 34 | 35 | get tag() { 36 | let id = this.shorten(this.wallet); 37 | 38 | id = this.wallets[id]; 39 | if (id) 40 | return id.tag; 41 | } 42 | 43 | get key() { 44 | let id = this.shorten(this.wallet); 45 | 46 | id = this.wallets[id]; 47 | if (id) 48 | return id.secret; 49 | } 50 | 51 | toasset(str) { 52 | const obj = {}; 53 | let asset, issuer; 54 | 55 | if ("string" != typeof str) 56 | return; 57 | 58 | str = str.split("."); 59 | asset = str.shift(); 60 | issuer = str.shift(); 61 | 62 | if ("XRP" == asset) { 63 | if (issuer) 64 | return; 65 | 66 | obj.code = asset; 67 | return obj; 68 | } 69 | 70 | if (issuer) { 71 | issuer = this.toabs(issuer); 72 | if (!issuer) 73 | return; 74 | 75 | obj.code = asset; 76 | obj.issuer = issuer; 77 | return obj; 78 | } 79 | 80 | return this.assets[asset]; 81 | } 82 | 83 | tostr(asset) { 84 | let issuer = asset.issuer; 85 | 86 | asset = asset.code; 87 | 88 | if (issuer) { 89 | issuer = this.shorten(issuer); 90 | asset = asset + "." + issuer; 91 | asset = this.shorten(asset); 92 | } 93 | 94 | return asset; 95 | } 96 | 97 | toamount(value, asset) { 98 | const obj = { 99 | value: value.toPrecision(8), 100 | currency: asset.code 101 | }; 102 | const issuer = asset.issuer; 103 | 104 | if (issuer) 105 | obj.counterparty = issuer; 106 | 107 | return obj; 108 | } 109 | 110 | get amount() { 111 | if ("value" != this.type) 112 | return; 113 | 114 | return this.toamount(this.value, this.asset); 115 | } 116 | 117 | get price() { 118 | if ("offer" != this.type) 119 | return; 120 | 121 | return this.value / this.cost; 122 | } 123 | 124 | get src() { 125 | if ("offer" != this.type) 126 | return; 127 | 128 | return this.toamount(this.cost, this.base); 129 | } 130 | 131 | get dst() { 132 | if ("offer" != this.type) 133 | return; 134 | 135 | return this.toamount(this.value, this.asset); 136 | } 137 | 138 | get human() { 139 | let value, asset, issuer, wallet, seq; 140 | let str = ""; 141 | 142 | switch (this.type) { 143 | case "offer": 144 | seq = this.seq; 145 | seq = seq ? seq : 0; 146 | seq = seq.toString(); 147 | str = "~" + seq + str; 148 | value = this.cost.toPrecision(8); 149 | str = ":" + value + str; 150 | asset = this.tostr(this.base); 151 | str = "/" + asset + str; 152 | case "value": 153 | value = this.value.toPrecision(8); 154 | str = ":" + value + str; 155 | case "asset": 156 | asset = this.tostr(this.asset); 157 | str = asset + str + "@"; 158 | case "wallet": 159 | wallet = this.wallet; 160 | wallet = this.shorten(wallet); 161 | str = str + wallet; 162 | return str; 163 | } 164 | } 165 | 166 | parse(str) { 167 | const tokens = re.exec(str); 168 | let asset, value, wallet, type, base, cost, seq; 169 | 170 | if (!tokens) 171 | return; 172 | 173 | tokens.shift(); 174 | 175 | asset = tokens.shift(); 176 | value = tokens.shift(); 177 | base = tokens.shift(); 178 | cost = tokens.shift(); 179 | seq = tokens.shift(); 180 | wallet = tokens.shift(); 181 | 182 | if (cost) 183 | type = "offer"; 184 | else if (value) 185 | type = "value"; 186 | else if (asset) 187 | type = "asset"; 188 | else if (wallet) 189 | type = "wallet"; 190 | else 191 | return; 192 | 193 | switch (type) { 194 | case "offer": 195 | base = this.toasset(base); 196 | if (!base) 197 | return; 198 | cost = parseFloat(cost); 199 | if (!isFinite(cost)) 200 | return; 201 | seq = parseInt(seq); 202 | if (!(seq > 0)) 203 | seq = void(0); 204 | case "value": 205 | value = parseFloat(value); 206 | if (!isFinite(value)) 207 | return; 208 | case "asset": 209 | asset = this.toasset(asset); 210 | if (!asset) 211 | return; 212 | case "wallet": 213 | wallet = this.toabs(wallet); 214 | if (!wallet) 215 | return; 216 | } 217 | 218 | return { 219 | type: type, 220 | base: base, 221 | cost: cost, 222 | seq: seq, 223 | value: value, 224 | asset: asset, 225 | wallet: wallet 226 | }; 227 | } 228 | } 229 | 230 | class XMM { 231 | constructor(opts) { 232 | let yes = opts.yes; 233 | 234 | if ("function" != typeof yes) { 235 | const result = !!yes; 236 | 237 | yes = tx => result; 238 | } 239 | 240 | this.confirm = tx => { 241 | const ask = Promise.resolve(yes(tx)); 242 | 243 | return ask.then(granted => { 244 | if (granted) 245 | return tx; 246 | else 247 | throw "Not confirmed"; 248 | }); 249 | } 250 | 251 | this.instr = {}; 252 | this.instr.maxLedgerVersionOffset = opts.offset; 253 | this.instr.maxFee = opts.maxfee.toString(); 254 | this.ledger = opts.ledger; 255 | this.api = opts.api; 256 | this.wallets = opts.wallets; 257 | this.assets = opts.assets; 258 | this.dict = {}; 259 | this.expand(); 260 | this.reverse(); 261 | } 262 | 263 | expand() { 264 | const wallets = this.wallets; 265 | const assets = this.assets; 266 | 267 | for (let alias in wallets) { 268 | const entry = wallets[alias]; 269 | 270 | if ("string" == typeof entry) { 271 | const obj = { 272 | address: entry 273 | }; 274 | 275 | wallets[alias] = obj; 276 | } 277 | } 278 | 279 | for (let alias in assets) { 280 | const entry = assets[alias]; 281 | 282 | if ("string" == typeof entry) { 283 | const pair = entry.split("."); 284 | const code = pair.shift(); 285 | const issuer = pair.shift(); 286 | const obj = { 287 | code: code, 288 | issuer: issuer 289 | }; 290 | 291 | assets[alias] = obj; 292 | } 293 | } 294 | 295 | for (let alias in assets) { 296 | const entry = assets[alias]; 297 | 298 | entry.issuer = this.toabs(entry.issuer); 299 | } 300 | } 301 | 302 | reverse() { 303 | const dict = this.dict; 304 | const wallets = this.wallets; 305 | const assets = this.assets; 306 | 307 | for (let alias in wallets) 308 | dict[wallets[alias].address] = alias; 309 | 310 | for (let alias in assets) { 311 | const asset = assets[alias]; 312 | let issuer = asset.issuer; 313 | let key = asset.code; 314 | 315 | if (issuer) { 316 | issuer = this.shorten(issuer); 317 | key = key.concat(".", issuer); 318 | } 319 | 320 | dict[key] = alias; 321 | } 322 | } 323 | 324 | shorten(line) { 325 | const alias = this.dict[line]; 326 | 327 | if (alias) 328 | return alias; 329 | 330 | return line; 331 | } 332 | 333 | toabs(wallet) { 334 | if (/^r[A-Za-z0-9]{25,}$/.test(wallet)) 335 | return wallet; 336 | 337 | wallet = this.wallets[wallet]; 338 | if (wallet) 339 | return wallet.address; 340 | } 341 | 342 | tovalues(list, me) { 343 | const iou = {}; 344 | 345 | me = me.wallet; 346 | 347 | list = list.filter(line => { 348 | const value = parseFloat(line.value); 349 | 350 | if (value > 0) 351 | return true; 352 | 353 | if (value < 0) { 354 | const code = line.currency; 355 | 356 | if (!iou[code]) 357 | iou[code] = 0; 358 | 359 | iou[code] += value; 360 | } 361 | 362 | return false; 363 | }); 364 | 365 | for (let code in iou) { 366 | const value = iou[code].toString(); 367 | 368 | list.push({ 369 | currency: code, 370 | counterparty: me, 371 | value: value 372 | }); 373 | } 374 | 375 | return list.map(line => { 376 | const code = line.currency; 377 | const asset = { 378 | code: code 379 | }; 380 | 381 | if ("XRP" != code) { 382 | let issuer = line.counterparty; 383 | 384 | if (!issuer) 385 | issuer = me; 386 | 387 | asset.issuer = issuer; 388 | } 389 | 390 | return this.parse({ 391 | type: "value", 392 | value: parseFloat(line.value), 393 | asset: asset, 394 | wallet: me 395 | }); 396 | }); 397 | } 398 | 399 | balance(me, ledger) { 400 | me = this.parse(me, "wallet"); 401 | 402 | if (!ledger) 403 | ledger = this.ledger; 404 | 405 | return this.api.getBalances(me.wallet, { 406 | ledgerVersion: ledger 407 | }).then(list => this.tovalues(list, me)); 408 | } 409 | 410 | parse(arg, type) { 411 | arg = new XMMarg(this, arg); 412 | 413 | if (type && type != arg.type) 414 | throw `${arg.input} is not ${type}`; 415 | 416 | return arg; 417 | } 418 | 419 | send(src, dst) { 420 | src = this.parse(src, "value"); 421 | dst = this.parse(dst, "value"); 422 | 423 | return this.make("Payment", src, { 424 | source: { 425 | tag: src.tag, 426 | address: src.wallet, 427 | maxAmount: src.amount 428 | }, 429 | destination: { 430 | tag: dst.tag, 431 | address: dst.wallet, 432 | amount: dst.amount 433 | } 434 | }); 435 | } 436 | 437 | create(offer) { 438 | offer = this.parse(offer, "offer"); 439 | 440 | return this.make("Order", offer, { 441 | direction: "buy", 442 | quantity: offer.dst, 443 | totalPrice: offer.src, 444 | orderToReplace: offer.seq 445 | }); 446 | } 447 | 448 | cancel(offer) { 449 | offer = this.parse(offer, "offer"); 450 | 451 | return this.make("OrderCancellation", offer, { 452 | orderSequence: offer.seq 453 | }); 454 | } 455 | 456 | make(type, me, param) { 457 | const api = this.api; 458 | const method = api["prepare" + type].bind(api); 459 | const id = me.wallet; 460 | const key = me.key; 461 | 462 | return method(id, param, this.instr).then(tx => { 463 | const json = tx.txJSON; 464 | 465 | tx = this.api.sign(json, key); 466 | 467 | return this.confirm({ 468 | blob: tx.signedTransaction, 469 | hash: tx.id, 470 | json: json 471 | }); 472 | }).then(tx => this.submit(tx)); 473 | } 474 | 475 | submit(tx) { 476 | return this.api.submit(tx.blob).then(result => { 477 | tx.code = result.resultCode; 478 | tx.desc = result.resultMessage; 479 | return tx; 480 | }); 481 | } 482 | 483 | tooffer(order, me) { 484 | const seq = order.properties.sequence; 485 | const spec = order.specification; 486 | const sell = ("sell" == spec.direction); 487 | const quantity = spec.quantity; 488 | const price = spec.totalPrice; 489 | const src = sell ? quantity : price; 490 | const dst = sell ? price : quantity; 491 | const cost = parseFloat(src.value); 492 | const base = { 493 | code: src.currency, 494 | issuer: src.counterparty 495 | }; 496 | const value = parseFloat(dst.value); 497 | const asset = { 498 | code: dst.currency, 499 | issuer: dst.counterparty 500 | }; 501 | 502 | return this.parse({ 503 | type: "offer", 504 | wallet: me.wallet, 505 | seq: seq, 506 | cost: cost, 507 | base: base, 508 | value: value, 509 | asset: asset 510 | }); 511 | } 512 | 513 | tooffers(list, me) { 514 | return list.map(line => this.tooffer(line, me)); 515 | } 516 | 517 | offers(me, ledger) { 518 | me = this.parse(me, "wallet"); 519 | 520 | if (!ledger) 521 | ledger = this.ledger; 522 | 523 | return this.api.getOrders(me.wallet, { 524 | ledgerVersion: ledger 525 | }).then(list => this.tooffers(list, me)); 526 | } 527 | 528 | cost(dst, me) { 529 | dst = this.parse(dst, "value"); 530 | 531 | if (me) 532 | me = this.parse(me, "wallet"); 533 | else 534 | me = dst; 535 | 536 | return this.api.getPaths({ 537 | source: { 538 | address: me.wallet 539 | }, 540 | destination: { 541 | address: dst.wallet, 542 | amount: dst.amount 543 | } 544 | }).then(list => { 545 | list = list.map(line => { 546 | line = line.source; 547 | line = line.maxAmount; 548 | return line; 549 | }); 550 | 551 | return this.tovalues(list, me); 552 | }); 553 | } 554 | 555 | trust(dst) { 556 | let amount; 557 | 558 | dst = this.parse(dst, "value"); 559 | 560 | amount = dst.amount; 561 | amount.limit = amount.value; 562 | amount.ripplingDisabled = true; 563 | delete amount.value; 564 | 565 | return this.make("Trustline", dst, amount); 566 | } 567 | } 568 | 569 | exports.connect = config => new Promise((resolve, reject) => { 570 | const ripple = require("ripple-lib"); 571 | const api = new ripple.RippleAPI({ 572 | feeCushion: config.cushion, 573 | server: config.server 574 | }); 575 | let count = config.count > 0 ? config.count : 1; 576 | 577 | function tick(ledger) 578 | { 579 | if (count > 0) { 580 | --count; 581 | api.once("ledger", tick); 582 | } else { 583 | const xmm = new XMM({ 584 | yes: config.yes, 585 | maxfee: config.maxfee, 586 | offset: config.offset, 587 | ledger: ledger.ledgerVersion, 588 | api: api, 589 | wallets: config.wallets, 590 | assets: config.assets 591 | }); 592 | 593 | resolve(xmm); 594 | } 595 | } 596 | 597 | tick(); 598 | api.connect(); 599 | }); 600 | 601 | function generate() 602 | { 603 | const ripple = require("ripple-lib"); 604 | const api = new ripple.RippleAPI(); 605 | 606 | return api.generateAddress(); 607 | } 608 | 609 | exports.generate = generate; 610 | 611 | exports.altnet = opts => new Promise(resolve => { 612 | require("request").post({ 613 | url: faucet, 614 | json: true 615 | }, (error, response, body) => { 616 | let code, account; 617 | 618 | if (error) 619 | throw error; 620 | 621 | code = response.statusCode; 622 | if (201 != code) 623 | throw body + code.toString(); 624 | 625 | account = body.account; 626 | account.tag = 314; 627 | 628 | resolve({ 629 | yes: true, 630 | wallets: { 631 | bank: account, 632 | fund: generate(), 633 | root: root 634 | }, 635 | assets: { 636 | XMM: "XMM.fund", 637 | USD: "USD.bank", 638 | BTC: "BTC.bank" 639 | }, 640 | server: altnet 641 | }); 642 | }); 643 | }); 644 | -------------------------------------------------------------------------------- /kill.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "kill "; 4 | exports.desc = "Cancel an existing order"; 5 | exports.aliases = [ 6 | "cancel", 7 | "rm" 8 | ]; 9 | exports.builder = yargs => yargs; 10 | exports.handler = connect((config, xmm) => { 11 | xmm.cancel(config.offer).then(tx => { 12 | print(tx); 13 | process.exit(); 14 | }).catch(abort); 15 | }); 16 | -------------------------------------------------------------------------------- /ledger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "ledger [count]"; 4 | exports.desc = "Wait for ledger(s) to close"; 5 | exports.aliases = [ 6 | "wait" 7 | ]; 8 | exports.builder = yargs => yargs; 9 | exports.handler = connect((config, xmm) => { 10 | console.info(xmm.ledger); 11 | process.exit(); 12 | }); 13 | -------------------------------------------------------------------------------- /offer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "offer "; 4 | exports.desc = "Create a limit order"; 5 | exports.aliases = [ 6 | "create", 7 | "order" 8 | ]; 9 | exports.builder = yargs => yargs; 10 | exports.handler = connect((config, xmm) => { 11 | xmm.create(config.offer).then(tx => { 12 | print(tx); 13 | process.exit(); 14 | }).catch(abort); 15 | }); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Trading Bot for Ripple Network", 3 | "author": "Anton Salikhmetov", 4 | "repository": "codedot/xmm", 5 | "name": "xmm", 6 | "bin": "./xmm.js", 7 | "version": "0.3.4", 8 | "keywords": [ 9 | "ripple", 10 | "api", 11 | "cli", 12 | "talmud", 13 | "market-maker", 14 | "trading", 15 | "bot" 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "ripple-lib": "0.17.7", 20 | "yargs": "7.1.0", 21 | "request": "2.75.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /send.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "send "; 4 | exports.desc = "Pay or convert a value"; 5 | exports.aliases = [ 6 | "pay" 7 | ]; 8 | exports.builder = yargs => yargs; 9 | exports.handler = connect((config, xmm) => { 10 | xmm.send(config.src, config.dst).then(tx => { 11 | print(tx); 12 | process.exit(); 13 | }).catch(abort); 14 | }); 15 | -------------------------------------------------------------------------------- /trust.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "trust "; 4 | exports.desc = "Set a trust line"; 5 | exports.aliases = [ 6 | "set" 7 | ]; 8 | exports.builder = yargs => yargs; 9 | exports.handler = connect((config, xmm) => { 10 | xmm.trust(config.dst).then(tx => { 11 | print(tx); 12 | process.exit(); 13 | }).catch(abort); 14 | }); 15 | -------------------------------------------------------------------------------- /view.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "view "; 4 | exports.desc = "List active orders"; 5 | exports.aliases = [ 6 | "active", 7 | "list", 8 | ]; 9 | exports.builder = yargs => yargs; 10 | exports.handler = connect((config, xmm) => { 11 | xmm.offers(config.me, config.ledger).then(lines => { 12 | lines.forEach(line => { 13 | console.info(line.human); 14 | }); 15 | process.exit(); 16 | }).catch(abort); 17 | }); 18 | -------------------------------------------------------------------------------- /what.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.command = "what "; 4 | exports.desc = "Tell what a string means"; 5 | exports.aliases = [ 6 | "parse" 7 | ]; 8 | exports.builder = yargs => yargs; 9 | exports.handler = connect((config, xmm) => { 10 | const arg = xmm.parse(config.string); 11 | const human = arg.human; 12 | const msg = []; 13 | let type, asset, issuer, key, tag; 14 | 15 | if (human) { 16 | msg.push(human); 17 | msg.push("is"); 18 | } 19 | 20 | type = arg.type; 21 | msg.push(type); 22 | 23 | switch (type) { 24 | case "offer": 25 | msg.push(arg.seq); 26 | msg.push("of"); 27 | msg.push(arg.cost); 28 | asset = arg.base; 29 | msg.push(asset.code); 30 | 31 | issuer = asset.issuer; 32 | if (issuer) { 33 | msg.push("issued by"); 34 | msg.push(issuer); 35 | } 36 | 37 | msg.push("for"); 38 | case "value": 39 | msg.push(arg.value); 40 | case "asset": 41 | asset = arg.asset; 42 | msg.push(asset.code); 43 | 44 | issuer = asset.issuer; 45 | if (issuer) { 46 | msg.push("issued by"); 47 | msg.push(issuer); 48 | } 49 | 50 | msg.push("in wallet"); 51 | case "wallet": 52 | msg.push(arg.wallet); 53 | 54 | tag = arg.tag; 55 | if (tag) { 56 | msg.push("tagged as"); 57 | msg.push(tag); 58 | } 59 | 60 | key = arg.key; 61 | if (key) { 62 | msg.push("with key"); 63 | msg.push(key); 64 | } 65 | } 66 | 67 | console.info(msg.join(" ")); 68 | process.exit(); 69 | }); 70 | -------------------------------------------------------------------------------- /xmm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | function ask(tx) 6 | { 7 | const rl = require("readline").createInterface({ 8 | input: process.stdin, 9 | output: process.stdout 10 | }); 11 | 12 | console.info(tx.hash); 13 | console.info(JSON.parse(tx.json)); 14 | 15 | return new Promise(resolve => { 16 | const expected = "submit"; 17 | const query = `Type "${expected}" to confirm: `; 18 | 19 | rl.question(query, answer => { 20 | rl.close(); 21 | resolve(expected == answer); 22 | }); 23 | }); 24 | } 25 | 26 | global.print = tx => { 27 | console.info(tx.hash); 28 | console.info(JSON.parse(tx.json)); 29 | console.info(`${tx.code}: ${tx.desc}`); 30 | }; 31 | 32 | global.connect = callback => config => { 33 | setTimeout(() => { 34 | abort("Timed out"); 35 | }, config.timeout * 1e3); 36 | 37 | if (!config.yes) 38 | config.yes = ask; 39 | 40 | callback = callback.bind(null, config); 41 | require(".").connect(config).then(callback).catch(abort); 42 | }; 43 | 44 | global.abort = (msg, error) => { 45 | if (error) 46 | console.error(error); 47 | else 48 | console.error(msg); 49 | 50 | process.exit(1); 51 | }; 52 | 53 | const getobj = x => ("string" == typeof x) ? JSON.parse(x) : x; 54 | const opts = { 55 | assets: { 56 | coerce: getobj, 57 | describe: "Dictionary of assets", 58 | default: {} 59 | }, 60 | count: { 61 | alias: "n", 62 | describe: "Number of ledgers to close", 63 | default: 1 64 | }, 65 | cushion: { 66 | alias: "f", 67 | describe: "Factor to multiply estimated fee", 68 | default: 1 69 | }, 70 | delta: { 71 | alias: "d", 72 | describe: "Stake to trade", 73 | default: 0.01 74 | }, 75 | dry: { 76 | alias: "p", 77 | describe: "Output script without running", 78 | boolean: true 79 | }, 80 | hedge: { 81 | coerce: getobj, 82 | describe: "List of pairs to trade", 83 | default: [] 84 | }, 85 | ledger: { 86 | alias: "l", 87 | describe: "Historical ledger version", 88 | number: true 89 | }, 90 | maxfee: { 91 | alias: "m", 92 | describe: "Maximum fee to pay", 93 | default: 1e-5 94 | }, 95 | offset: { 96 | alias: "o", 97 | describe: "Offset from the current legder", 98 | default: 3 99 | }, 100 | server: { 101 | alias: "s", 102 | describe: "WebSocket server", 103 | default: "wss://s1.ripple.com" 104 | }, 105 | timeout: { 106 | alias: "t", 107 | describe: "Timeout in seconds", 108 | default: 60 109 | }, 110 | wallets: { 111 | coerce: getobj, 112 | describe: "Dictionary of wallets", 113 | default: {} 114 | }, 115 | yes: { 116 | alias: "y", 117 | describe: "Do not ask for confirmation", 118 | boolean: true 119 | } 120 | }; 121 | 122 | const home = require("os").homedir(); 123 | const conf = require("path").join(home, ".xmm.json"); 124 | 125 | function load(path) 126 | { 127 | try { 128 | const read = require("fs").readFileSync; 129 | const json = read(path, "utf-8"); 130 | const dict = JSON.parse(json); 131 | 132 | return dict; 133 | } catch (error) { 134 | console.warn("%s: Could not load configuration", path); 135 | 136 | return {}; 137 | } 138 | } 139 | 140 | require("yargs") 141 | .usage("Usage: $0 [options] [arguments]") 142 | .options(opts) 143 | .config("config", load) 144 | .alias("config", "c") 145 | .default("config", conf, "~/.xmm.json") 146 | .command(require("./altnet")) 147 | .command(require("./balance")) 148 | .command(require("./cost")) 149 | .command(require("./generate")) 150 | .command(require("./hedge")) 151 | .command(require("./kill")) 152 | .command(require("./ledger")) 153 | .command(require("./offer")) 154 | .command(require("./send")) 155 | .command(require("./trust")) 156 | .command(require("./view")) 157 | .command(require("./what")) 158 | .demand(1) 159 | .strict() 160 | .recommendCommands() 161 | .version() 162 | .alias("version", "v") 163 | .help() 164 | .alias("help", "h") 165 | .wrap(70) 166 | .fail(abort) 167 | .argv; 168 | --------------------------------------------------------------------------------