@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/lib/stellarbroker.js +1 -1
- package/lib/stellarbroker.js.map +1 -1
- package/package.json +3 -3
- package/src/authorization.js +3 -3
- package/src/client.js +360 -357
- package/src/index.d.ts +294 -277
- package/src/mediator.js +397 -386
- package/src/tx-processor.js +16 -42
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:
|
|
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(
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
}
|