@stellar-broker/client 0.7.0 → 0.8.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/src/mediator.js CHANGED
@@ -1,387 +1,398 @@
1
- import {Asset, Keypair, Memo, Operation, TransactionBuilder, Horizon, Networks, StrKey, TimeoutInfinite, NotFoundError} from '@stellar/stellar-sdk'
2
- import {fromStroops, toStroops} from './stroops.js'
3
- import {convertToStellarAsset} from './asset.js'
4
- import {AuthorizationWrapper} from './authorization.js'
5
-
6
- const defaultStoragePrefix = 'msb_'
7
-
8
- export class Mediator {
9
- /**
10
- * Create a trader mediator account instance for a given source account
11
- * @param {string} source - Creator account address
12
- * @param {string|Asset} sellingAsset - Identifier of the asset to sell
13
- * @param {string|Asset} buyingAsset - Identifier of the asset to buy
14
- * @param {string} sellingAmount - Asset amount to sell
15
- * @param {ClientAuthorizationParams} authorization - Authorization callback or secret key
16
- * @param {number} [reserveFeeAmount] - Amount reserved to cover tx fees (all unused funds will be refunded)
17
- */
18
- constructor(source, sellingAsset, buyingAsset, sellingAmount, authorization, reserveFeeAmount = 5) {
19
- if (!StrKey.isValidEd25519PublicKey(source))
20
- throw new Error('Invalid source account')
21
- this.source = source
22
- try {
23
- this.sellingAsset = convertToStellarAsset(sellingAsset)
24
- } catch (e) {
25
- console.error(e)
26
- throw new Error('Invalid selling asset')
27
- }
28
- try {
29
- this.buyingAsset = convertToStellarAsset(buyingAsset)
30
- } catch (e) {
31
- console.error(e)
32
- throw new Error('Invalid buying asset')
33
- }
34
- try {
35
- this.sellingAmount = toStroops(sellingAmount)
36
- } catch (e) {
37
- console.error(e)
38
- throw new Error('Invalid selling amount')
39
- }
40
- this.authorization = new AuthorizationWrapper(authorization)
41
- this.reserveFeeAmount = reserveFeeAmount
42
- }
43
-
44
- /**
45
- * Creator account address
46
- * @type {string}
47
- * @readonly
48
- */
49
- source
50
- /**
51
- * @type {Asset}
52
- * @readonly
53
- */
54
- sellingAsset
55
- /**
56
- * @type {Asset}
57
- * @readonly
58
- */
59
- buyingAsset
60
- /**
61
- * @type {bigint}
62
- * @readonly
63
- */
64
- sellingAmount
65
- /**
66
- * @type {AuthorizationWrapper}
67
- * @private
68
- */
69
- authorization
70
- /**
71
- * @type {Keypair}
72
- * @private
73
- */
74
- mediator
75
- /**
76
- * @type {string}
77
- * @readonly
78
- */
79
- mediatorAddress
80
- /**
81
- * Prefix of the mediator address record in the localStorage
82
- * @type {string}
83
- * @private
84
- */
85
- storagePrefix = defaultStoragePrefix
86
- /**
87
- * @type {number}
88
- * @private
89
- */
90
- reserveFeeAmount
91
-
92
- /**
93
- * Check if there are any non-disposed mediators that belong to lost swap sessions
94
- * @return {boolean}
95
- */
96
- get hasObsoleteMediators() {
97
- return this.constructor.hasObsoleteMediators(this.source, this.storagePrefix)
98
- }
99
-
100
- /**
101
- * Check if there are any non-disposed mediators that belong to lost swap sessions
102
- * @param {string} source - Initiator account that created a mediator
103
- * @param {string} [storagePrefix] - Local storage key prefix
104
- * @return {boolean}
105
- */
106
- static hasObsoleteMediators(source, storagePrefix = defaultStoragePrefix) {
107
- return Object.entries(localStorage).some(([key, initiator]) =>
108
- initiator === source && key.startsWith(storagePrefix))
109
- }
110
-
111
- /**
112
- * Retrieve funds from mediator accounts that belong to lost swap sessions
113
- * @return {Promise}
114
- */
115
- async disposeObsoleteMediators() { //TODO: deal with potential problems caused by concurrent swaps
116
- for (let [key, initiator] of Object.entries(localStorage)) {
117
- if (initiator === this.source && key.startsWith(this.storagePrefix)) {
118
- await this.dispose(key.replace(this.storagePrefix, ''))
119
- }
120
- }
121
- }
122
-
123
- /**
124
- * Retrieve funds from mediator accounts that belong to lost swap sessions
125
- * @param {string} source - Initiator account that created a mediator
126
- * @param {ClientAuthorizationParams} authorization - Authorization callback or secret key
127
- * @param {string} [storagePrefix] - Local storage key prefix
128
- * @return {Promise}
129
- */
130
- static async disposeObsoleteMediators(source, authorization, storagePrefix = defaultStoragePrefix) {
131
- const wrapper = new Mediator(source, 'XLM', 'XLM', '0', authorization)
132
- wrapper.storagePrefix = storagePrefix
133
- await wrapper.disposeObsoleteMediators()
134
- }
135
-
136
- /**
137
- * Create mediator account and deposit tokens to sell
138
- * @return {Promise<string>} - Newly created mediator secret key
139
- */
140
- async init() {
141
- const sourceAccount = await this.loadAccount(this.source)
142
- if (!sourceAccount)
143
- throw new Error('Mediator account doesn\'t exist on the ledger')
144
- //calculate fees reserve + account entries reserve
145
- const subentries = 2 + sourceAccount.signers.length - 1 + [this.sellingAsset, this.buyingAsset].filter(a => !a.isNative()).length
146
- const feesReserve = this.reserveFeeAmount + 0.5 * subentries
147
- const ops = []
148
- //create new random keypair for the trade
149
- this.mediator = Keypair.random()
150
- this.mediatorAddress = this.mediator.publicKey()
151
- //create mediator account and deposit funds
152
- const {sellingAsset, buyingAsset} = this
153
- if (sellingAsset.isNative()) { //for XLM total amount should include fee reserves
154
- const amount = this.sellingAmount + BigInt(feesReserve * 10000000)
155
- //check available balance
156
- if (toStroops(findTrustline(sourceAccount, sellingAsset).balance) < amount)
157
- throw new Error('Insufficient XLM balance for selling amount + potential trading fees')
158
- //only create account is required for asset transfer
159
- ops.push(Operation.createAccount({
160
- source: this.source,
161
- destination: this.mediatorAddress,
162
- startingBalance: fromStroops(amount)
163
- }))
164
- } else {
165
- //check available XLM balance
166
- const xlmBalance = findTrustline(sourceAccount, Asset.native())
167
- if (parseFloat(xlmBalance.balance) < feesReserve)
168
- throw new Error('Insufficient XLM balance for potential trading fees')
169
- //create mediator account
170
- ops.push(Operation.createAccount({
171
- source: this.source,
172
- destination: this.mediatorAddress,
173
- startingBalance: feesReserve.toString() // for tx fees
174
- }))
175
- //check available XLM balance
176
- const sellingTrustline = findTrustline(sourceAccount, this.sellingAsset)
177
- if (!sellingTrustline || toStroops(sellingTrustline.balance) < this.sellingAmount)
178
- throw new Error('Insufficient selling asset balance')
179
- //TODO: check authorizations
180
- //create trustline for selling asset
181
- ops.push(Operation.changeTrust({
182
- source: this.mediatorAddress,
183
- asset: sellingAsset
184
- }))
185
- //transfer tokens to sell
186
- ops.push(Operation.payment({
187
- source: this.source,
188
- asset: sellingAsset,
189
- destination: this.mediatorAddress,
190
- amount: fromStroops(this.sellingAmount)
191
- }))
192
- }
193
- //create trustline for buying asset
194
- if (!buyingAsset.isNative()) {
195
- ops.push(Operation.changeTrust({
196
- source: this.mediatorAddress,
197
- asset: buyingAsset
198
- }))
199
- //if source account itself doesn't have trustline for buying asset
200
- if (!findTrustline(sourceAccount, buyingAsset)) {
201
- ops.push(Operation.changeTrust({
202
- source: this.source,
203
- asset: buyingAsset
204
- }))
205
- }
206
- }
207
-
208
- if (sourceAccount.signers.length > 1) { //multisig or delegated schema
209
- let {thresholds, signers} = sourceAccount
210
- signers = signers.filter(signer => signer.type === 'ed25519_public_key')
211
- //add source account as a signer
212
- ops.push(Operation.setOptions({
213
- source: this.mediatorAddress,
214
- inflationDest: this.source, //store source account to the inflation dest
215
- homeDomain: 'mediator.stellar.broker',
216
- masterWeight: Math.max(1, thresholds.high_threshold, thresholds.med_threshold), //own signer always has the highest weight
217
- highThreshold: thresholds.high_threshold,
218
- medThreshold: thresholds.med_threshold,
219
- lowThreshold: thresholds.low_threshold,
220
- signer: { //add first signer
221
- ed25519PublicKey: signers[0].key,
222
- weight: Math.max(1, signers[0].weight)
223
- }
224
- }))
225
- //add all other signers
226
- for (let i = 1; i < signers.length; i++) {
227
- ops.push(Operation.setOptions({
228
- source: this.mediatorAddress,
229
- signer: { //cope signers and weights
230
- ed25519PublicKey: signers[i].key,
231
- weight: Math.max(1, signers[i].weight)
232
- }
233
- }))
234
- }
235
- } else { //simple schema
236
- ops.push(Operation.setOptions({
237
- source: this.mediatorAddress,
238
- homeDomain: 'mediator.stellar.broker',
239
- masterWeight: 1,
240
- highThreshold: 1,
241
- medThreshold: 1,
242
- lowThreshold: 1,
243
- signer: { //add source account as a signer
244
- ed25519PublicKey: this.source,
245
- weight: 1
246
- }
247
- }))
248
- }
249
-
250
- //store the record in the localStorage
251
- localStorage.setItem(this.storagePrefix + this.mediatorAddress, this.source)
252
- //confirm tx
253
- await this.buildAndSend(sourceAccount, ops)
254
- //client will need mediator secret for trading
255
- return this.mediator.secret()
256
- }
257
-
258
- /**
259
- * Dispose mediator account
260
- * @param {string} [address]
261
- * @return {Promise}
262
- */
263
- async dispose(address) {
264
- if (!address) {
265
- address = this.mediatorAddress
266
- }
267
- //load account
268
- let mediatorAccount = await this.loadAccount(address)
269
- if (!mediatorAccount) {
270
- //remove reference from local storage only if account doesn't exist on the ledger
271
- localStorage.removeItem(this.storagePrefix + address)
272
- throw new Error(`Mediator account ${address} doesn't exist on the ledger`)
273
- }
274
-
275
- if (!mediatorAccount.signers.find(s => s.key === this.source))
276
- throw new Error(`${address} is not a mediator account for ${this.source}`)
277
- const ops = []
278
- //remove trustlines for each account balance
279
- for (const balance of mediatorAccount.balances) {
280
- if (balance.asset_type === 'native')
281
- continue //skip XLM trustline - merge will handle the transfer
282
- const asset = convertToStellarAsset(balance)
283
- //transfer remaining balance to the source account
284
- if (balance.balance > 0) {
285
- ops.push(Operation.payment({
286
- source: address,
287
- asset,
288
- destination: this.source,
289
- amount: balance.balance
290
- }))
291
- }
292
- //remove trustline
293
- ops.push(Operation.changeTrust({
294
- source: address,
295
- asset,
296
- limit: '0'
297
- }))
298
- }
299
- //merge
300
- ops.push(Operation.accountMerge({
301
- source: address,
302
- destination: this.source
303
- }))
304
- if (this.mediatorAddress !== address) {
305
- mediatorAccount = await this.loadAccount(this.source)
306
- }
307
- await this.buildAndSend(mediatorAccount, ops)
308
- //remove reference from local storage
309
- localStorage.removeItem(this.storagePrefix + address)
310
- }
311
-
312
- /**
313
- * @param {AccountResponse} account
314
- * @param {Operation[]} operations
315
- * @param {string} [memo]
316
- * @private
317
- */
318
- async buildAndSend(account, operations, memo) {
319
- //create builder
320
- const builder = new TransactionBuilder(account, {
321
- fee: '1000000',
322
- networkPassphrase: Networks.PUBLIC
323
- })
324
- builder.setTimeout(TimeoutInfinite)
325
- //add memo if needed
326
- if (memo) {
327
- builder.addMemo(Memo.text(memo))
328
- }
329
- //add operations
330
- for (const op of operations) {
331
- builder.addOperation(op)
332
- }
333
- //build tx
334
- let tx = builder.build()
335
- //sign it on behalf of the mediator account (if available)
336
- if (this.mediator) {
337
- tx.sign(this.mediator)
338
- }
339
- //request signature from the client (if required)
340
- if (account.account_id === this.source) {
341
- tx = await this.authorization.authorize(tx)
342
- }
343
- //execute the tx
344
- const res = await this.constructor.createHorizon().submitTransaction(tx, {skipMemoRequiredCheck: true})
345
- if (!res.successful)
346
- throw new Error('Failed to create mediator account')
347
- }
348
-
349
- /**
350
- * @param {string} address
351
- * @return {Promise<AccountResponse>}
352
- * @private
353
- */
354
- async loadAccount(address) {
355
- try {
356
- return await this.constructor.createHorizon().loadAccount(address)
357
- } catch (e) {
358
- if (e instanceof NotFoundError) {
359
- console.warn(`Account ${address} doesn't exist on the ledger`)
360
- return null
361
- }
362
- console.error(e)
363
- throw e
364
- }
365
- }
366
-
367
- /**
368
- * @return {HorizonServer}
369
- * @private
370
- */
371
- static createHorizon() {
372
- return new Horizon.Server('https://horizon.stellar.org')
373
- }
374
- }
375
-
376
-
377
- /**
378
- * @param {AccountResponse} account
379
- * @param {Asset} asset
380
- * @return {BalanceLine}
381
- */
382
- function findTrustline(account, asset) {
383
- return account.balances.find(b => asset.isNative() ?
384
- b.asset_type === 'native' :
385
- b.asset_code === asset.code && b.asset_issuer === asset.issuer
386
- )
1
+ import {Asset, Keypair, Memo, Operation, TransactionBuilder, Horizon, Networks, StrKey, TimeoutInfinite, NotFoundError} from '@stellar/stellar-sdk'
2
+ import {fromStroops, toStroops} from './stroops.js'
3
+ import {convertToStellarAsset} from './asset.js'
4
+ import {AuthorizationWrapper} from './authorization.js'
5
+
6
+ const defaultStoragePrefix = 'msb_'
7
+
8
+ export class Mediator {
9
+ /**
10
+ * Create a trader mediator account instance for a given source account
11
+ * @param {string} source - Creator account address
12
+ * @param {string|Asset} sellingAsset - Identifier of the asset to sell
13
+ * @param {string|Asset} buyingAsset - Identifier of the asset to buy
14
+ * @param {string} sellingAmount - Asset amount to sell
15
+ * @param {ClientAuthorizationParams} authorization - Authorization callback or secret key
16
+ * @param {number} [reserveFeeAmount] - Amount reserved to cover tx fees (all unused funds will be refunded)
17
+ */
18
+ constructor(source, sellingAsset, buyingAsset, sellingAmount, authorization, reserveFeeAmount = 5) {
19
+ if (!StrKey.isValidEd25519PublicKey(source))
20
+ throw new Error('Invalid source account')
21
+ this.source = source
22
+ try {
23
+ this.sellingAsset = convertToStellarAsset(sellingAsset)
24
+ } catch (e) {
25
+ console.error(e)
26
+ throw new Error('Invalid selling asset')
27
+ }
28
+ try {
29
+ this.buyingAsset = convertToStellarAsset(buyingAsset)
30
+ } catch (e) {
31
+ console.error(e)
32
+ throw new Error('Invalid buying asset')
33
+ }
34
+ try {
35
+ this.sellingAmount = toStroops(sellingAmount)
36
+ } catch (e) {
37
+ console.error(e)
38
+ throw new Error('Invalid selling amount')
39
+ }
40
+ this.authorization = new AuthorizationWrapper(authorization)
41
+ this.reserveFeeAmount = reserveFeeAmount
42
+ }
43
+
44
+ /**
45
+ * Creator account address
46
+ * @type {string}
47
+ * @readonly
48
+ */
49
+ source
50
+ /**
51
+ * @type {Asset}
52
+ * @readonly
53
+ */
54
+ sellingAsset
55
+ /**
56
+ * @type {Asset}
57
+ * @readonly
58
+ */
59
+ buyingAsset
60
+ /**
61
+ * @type {bigint}
62
+ * @readonly
63
+ */
64
+ sellingAmount
65
+ /**
66
+ * @type {AuthorizationWrapper}
67
+ * @private
68
+ */
69
+ authorization
70
+ /**
71
+ * @type {Keypair}
72
+ * @private
73
+ */
74
+ mediator
75
+ /**
76
+ * @type {string}
77
+ * @readonly
78
+ */
79
+ mediatorAddress
80
+ /**
81
+ * Prefix of the mediator address record in the localStorage
82
+ * @type {string}
83
+ * @private
84
+ */
85
+ storagePrefix = defaultStoragePrefix
86
+ /**
87
+ * @type {number}
88
+ * @private
89
+ */
90
+ reserveFeeAmount
91
+
92
+ /**
93
+ * Check if there are any non-disposed mediators that belong to lost swap sessions
94
+ * @return {boolean}
95
+ */
96
+ get hasObsoleteMediators() {
97
+ return this.constructor.hasObsoleteMediators(this.source, this.storagePrefix)
98
+ }
99
+
100
+ /**
101
+ * Check if there are any non-disposed mediators that belong to lost swap sessions
102
+ * @param {string} source - Initiator account that created a mediator
103
+ * @param {string} [storagePrefix] - Local storage key prefix
104
+ * @return {boolean}
105
+ */
106
+ static hasObsoleteMediators(source, storagePrefix = defaultStoragePrefix) {
107
+ return Object.entries(localStorage).some(([key, initiator]) =>
108
+ initiator === source && key.startsWith(storagePrefix))
109
+ }
110
+
111
+ /**
112
+ * Retrieve funds from mediator accounts that belong to lost swap sessions
113
+ * @return {Promise}
114
+ */
115
+ async disposeObsoleteMediators() { //TODO: deal with potential problems caused by concurrent swaps
116
+ for (let [key, initiator] of Object.entries(localStorage)) {
117
+ if (initiator === this.source && key.startsWith(this.storagePrefix)) {
118
+ await this.dispose(key.replace(this.storagePrefix, ''))
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Retrieve funds from mediator accounts that belong to lost swap sessions
125
+ * @param {string} source - Initiator account that created a mediator
126
+ * @param {ClientAuthorizationParams} authorization - Authorization callback or secret key
127
+ * @param {string} [storagePrefix] - Local storage key prefix
128
+ * @return {Promise}
129
+ */
130
+ static async disposeObsoleteMediators(source, authorization, storagePrefix = defaultStoragePrefix) {
131
+ const wrapper = new Mediator(source, 'XLM', 'XLM', '0', authorization)
132
+ wrapper.storagePrefix = storagePrefix
133
+ await wrapper.disposeObsoleteMediators()
134
+ }
135
+
136
+ /**
137
+ * Create mediator account and deposit tokens to sell
138
+ * @return {Promise<string>} - Newly created mediator secret key
139
+ */
140
+ async init() {
141
+ const sourceAccount = await this.loadAccount(this.source)
142
+ if (!sourceAccount)
143
+ throw new Error('Mediator account doesn\'t exist on the ledger')
144
+ //calculate fees reserve + account entries reserve
145
+ const subentries = 2 + sourceAccount.signers.length - 1 + [this.sellingAsset, this.buyingAsset].filter(a => !a.isNative()).length
146
+ const feesReserve = this.reserveFeeAmount + 0.5 * subentries
147
+ const ops = []
148
+ //create new random keypair for the trade
149
+ this.mediator = Keypair.random()
150
+ this.mediatorAddress = this.mediator.publicKey()
151
+ //create mediator account and deposit funds
152
+ const {sellingAsset, buyingAsset} = this
153
+ if (sellingAsset.isNative()) { //for XLM total amount should include fee reserves
154
+ const amount = this.sellingAmount + BigInt(feesReserve * 10000000)
155
+ //check available balance
156
+ if (toStroops(findTrustline(sourceAccount, sellingAsset).balance) < amount)
157
+ throw new Error('Insufficient XLM balance for selling amount + potential trading fees')
158
+ //only create account is required for asset transfer
159
+ ops.push(Operation.createAccount({
160
+ source: this.source,
161
+ destination: this.mediatorAddress,
162
+ startingBalance: fromStroops(amount)
163
+ }))
164
+ } else {
165
+ //check available XLM balance
166
+ const xlmBalance = findTrustline(sourceAccount, Asset.native())
167
+ if (parseFloat(xlmBalance.balance) < feesReserve)
168
+ throw new Error('Insufficient XLM balance for potential trading fees')
169
+ //create mediator account
170
+ ops.push(Operation.createAccount({
171
+ source: this.source,
172
+ destination: this.mediatorAddress,
173
+ startingBalance: feesReserve.toString() // for tx fees
174
+ }))
175
+ //check available XLM balance
176
+ const sellingTrustline = findTrustline(sourceAccount, this.sellingAsset)
177
+ if (!sellingTrustline || toStroops(sellingTrustline.balance) < this.sellingAmount)
178
+ throw new Error('Insufficient selling asset balance')
179
+ //TODO: check authorizations
180
+ //create trustline for selling asset
181
+ ops.push(Operation.changeTrust({
182
+ source: this.mediatorAddress,
183
+ asset: sellingAsset
184
+ }))
185
+ //transfer tokens to sell
186
+ ops.push(Operation.payment({
187
+ source: this.source,
188
+ asset: sellingAsset,
189
+ destination: this.mediatorAddress,
190
+ amount: fromStroops(this.sellingAmount)
191
+ }))
192
+ }
193
+ //create trustline for buying asset
194
+ if (!buyingAsset.isNative()) {
195
+ ops.push(Operation.changeTrust({
196
+ source: this.mediatorAddress,
197
+ asset: buyingAsset
198
+ }))
199
+ //if source account itself doesn't have trustline for buying asset
200
+ if (!findTrustline(sourceAccount, buyingAsset)) {
201
+ ops.push(Operation.changeTrust({
202
+ source: this.source,
203
+ asset: buyingAsset
204
+ }))
205
+ }
206
+ }
207
+
208
+ if (sourceAccount.signers.length > 1) { //multisig or delegated schema
209
+ let {thresholds, signers} = sourceAccount
210
+ signers = signers.filter(signer => signer.type === 'ed25519_public_key')
211
+ //add source account as a signer
212
+ ops.push(Operation.setOptions({
213
+ source: this.mediatorAddress,
214
+ inflationDest: this.source, //store source account to the inflation dest
215
+ homeDomain: 'mediator.stellar.broker',
216
+ masterWeight: Math.max(1, thresholds.high_threshold, thresholds.med_threshold), //own signer always has the highest weight
217
+ highThreshold: thresholds.high_threshold,
218
+ medThreshold: thresholds.med_threshold,
219
+ lowThreshold: thresholds.low_threshold,
220
+ signer: { //add first signer
221
+ ed25519PublicKey: signers[0].key,
222
+ weight: Math.max(1, signers[0].weight)
223
+ }
224
+ }))
225
+ //add all other signers
226
+ for (let i = 1; i < signers.length; i++) {
227
+ ops.push(Operation.setOptions({
228
+ source: this.mediatorAddress,
229
+ signer: { //cope signers and weights
230
+ ed25519PublicKey: signers[i].key,
231
+ weight: Math.max(1, signers[i].weight)
232
+ }
233
+ }))
234
+ }
235
+ } else { //simple schema
236
+ ops.push(Operation.setOptions({
237
+ source: this.mediatorAddress,
238
+ homeDomain: 'mediator.stellar.broker',
239
+ masterWeight: 1,
240
+ highThreshold: 1,
241
+ medThreshold: 1,
242
+ lowThreshold: 1,
243
+ signer: { //add source account as a signer
244
+ ed25519PublicKey: this.source,
245
+ weight: 1
246
+ }
247
+ }))
248
+ }
249
+
250
+ //store the record in the localStorage
251
+ localStorage.setItem(this.storagePrefix + this.mediatorAddress, this.source)
252
+ //confirm tx
253
+ await this.buildAndSend(sourceAccount, ops)
254
+ //client will need mediator secret for trading
255
+ return this.mediator.secret()
256
+ }
257
+
258
+ /**
259
+ * Dispose mediator account
260
+ * @param {string} [address]
261
+ * @return {Promise}
262
+ */
263
+ async dispose(address) {
264
+ if (!address) {
265
+ address = this.mediatorAddress
266
+ }
267
+ //load account
268
+ let mediatorAccount = await this.loadAccount(address)
269
+ if (!mediatorAccount) {
270
+ //remove reference from local storage only if account doesn't exist on the ledger
271
+ localStorage.removeItem(this.storagePrefix + address)
272
+ throw new Error(`Mediator account ${address} doesn't exist on the ledger`)
273
+ }
274
+
275
+ if (!mediatorAccount.signers.find(s => s.key === this.source))
276
+ throw new Error(`${address} is not a mediator account for ${this.source}`)
277
+ const ops = []
278
+ //remove trustlines for each account balance
279
+ for (const balance of mediatorAccount.balances) {
280
+ if (balance.asset_type === 'native')
281
+ continue //skip XLM trustline - merge will handle the transfer
282
+ const asset = convertToStellarAsset(balance)
283
+ //transfer remaining balance to the source account
284
+ if (balance.balance > 0) {
285
+ ops.push(Operation.payment({
286
+ source: address,
287
+ asset,
288
+ destination: this.source,
289
+ amount: balance.balance
290
+ }))
291
+ }
292
+ //remove trustline
293
+ ops.push(Operation.changeTrust({
294
+ source: address,
295
+ asset,
296
+ limit: '0'
297
+ }))
298
+ }
299
+ //merge
300
+ ops.push(Operation.accountMerge({
301
+ source: address,
302
+ destination: this.source
303
+ }))
304
+ if (this.mediatorAddress !== address) {
305
+ mediatorAccount = await this.loadAccount(this.source)
306
+ }
307
+ await this.buildAndSend(mediatorAccount, ops)
308
+ //remove reference from local storage
309
+ localStorage.removeItem(this.storagePrefix + address)
310
+ }
311
+
312
+ /**
313
+ * @param {AccountResponse} account
314
+ * @param {Operation[]} operations
315
+ * @param {string} [memo]
316
+ * @private
317
+ */
318
+ async buildAndSend(account, operations, memo) {
319
+ //create builder
320
+ const builder = new TransactionBuilder(account, {
321
+ fee: '1000000',
322
+ networkPassphrase: this.constructor.network
323
+ })
324
+ builder.setTimeout(TimeoutInfinite)
325
+ //add memo if needed
326
+ if (memo) {
327
+ builder.addMemo(Memo.text(memo))
328
+ }
329
+ //add operations
330
+ for (const op of operations) {
331
+ builder.addOperation(op)
332
+ }
333
+ //build tx
334
+ let tx = builder.build()
335
+ //sign it on behalf of the mediator account (if available)
336
+ if (this.mediator) {
337
+ tx.sign(this.mediator)
338
+ }
339
+ //request signature from the client (if required)
340
+ if (account.account_id === this.source) {
341
+ tx = await this.authorization.authorize(tx)
342
+ }
343
+ //execute the tx
344
+ const res = await this.constructor.createHorizon().submitTransaction(tx, {skipMemoRequiredCheck: true})
345
+ if (!res.successful)
346
+ throw new Error('Failed to create mediator account')
347
+ }
348
+
349
+ /**
350
+ * @param {string} address
351
+ * @return {Promise<AccountResponse>}
352
+ * @private
353
+ */
354
+ async loadAccount(address) {
355
+ try {
356
+ return await this.constructor.createHorizon().loadAccount(address)
357
+ } catch (e) {
358
+ if (e instanceof NotFoundError) {
359
+ console.warn(`Account ${address} doesn't exist on the ledger`)
360
+ return null
361
+ }
362
+ console.error(e)
363
+ throw e
364
+ }
365
+ }
366
+
367
+ /**
368
+ * @return {HorizonServer}
369
+ * @private
370
+ */
371
+ static createHorizon() {
372
+ return new Horizon.Server(this.horizonUrl)
373
+ }
374
+
375
+ /**
376
+ * Stellar network passphrase used by mediator accounts
377
+ * @type {string}
378
+ */
379
+ static network = Networks.PUBLIC
380
+ /**
381
+ * Horizon server used by mediator accounts
382
+ * @type {string}
383
+ */
384
+ static horizonUrl = 'https://horizon.stellar.org'
385
+ }
386
+
387
+
388
+ /**
389
+ * @param {AccountResponse} account
390
+ * @param {Asset} asset
391
+ * @return {BalanceLine}
392
+ */
393
+ function findTrustline(account, asset) {
394
+ return account.balances.find(b => asset.isNative() ?
395
+ b.asset_type === 'native' :
396
+ b.asset_code === asset.code && b.asset_issuer === asset.issuer
397
+ )
387
398
  }