@stellar-broker/client 0.4.0 → 0.5.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/LICENSE +21 -0
- package/README.md +160 -109
- package/jest.config.cjs +4 -0
- package/lib/stellarbroker.js +1 -1
- package/lib/stellarbroker.js.map +1 -1
- package/package.json +14 -12
- package/src/asset.js +51 -47
- package/src/authorization.js +65 -0
- package/src/client.js +353 -369
- package/src/errors.js +55 -53
- package/src/index.d.ts +95 -25
- package/src/index.js +4 -3
- package/src/mediator.js +309 -0
- package/src/quote-request.js +56 -60
- package/src/tx-processor.js +133 -161
package/src/errors.js
CHANGED
|
@@ -1,54 +1,56 @@
|
|
|
1
|
-
export class StellarBrokerError extends Error {
|
|
2
|
-
constructor(code, message) {
|
|
3
|
-
super(message)
|
|
4
|
-
this.code = code
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Numeric error code
|
|
9
|
-
* @type {number}
|
|
10
|
-
* @readonly
|
|
11
|
-
*/
|
|
12
|
-
code = 0
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const errors = {
|
|
16
|
-
invalidInitParam(param) {
|
|
17
|
-
return new StellarBrokerError(1, `Invalid parameter value: "${param}"`)
|
|
18
|
-
},
|
|
19
|
-
invalidAuthorizationParam() {
|
|
20
|
-
return new StellarBrokerError(2, 'Invalid authorization secret key or callback provided')
|
|
21
|
-
},
|
|
22
|
-
notConnected() {
|
|
23
|
-
return new StellarBrokerError(3, 'Client not connected to the server')
|
|
24
|
-
},
|
|
25
|
-
quoteNotSet() {
|
|
26
|
-
return new StellarBrokerError(11, 'Price quote not available')
|
|
27
|
-
},
|
|
28
|
-
quoteExpired() {
|
|
29
|
-
return new StellarBrokerError(12, 'Price quote expired')
|
|
30
|
-
},
|
|
31
|
-
quoteError(message) {
|
|
32
|
-
return new StellarBrokerError(13, 'Price quotation error: ' + message)
|
|
33
|
-
},
|
|
34
|
-
invalidQuoteParam(invalidParamName = 'asset', details) {
|
|
35
|
-
return new StellarBrokerError(14, `Invalid quote request parameter: "${invalidParamName}". ${details}`)
|
|
36
|
-
},
|
|
37
|
-
tradeInProgress() {
|
|
38
|
-
return new StellarBrokerError(20, 'Cannot change quote while trade is in progress')
|
|
39
|
-
},
|
|
40
|
-
invalidSwapTx() {
|
|
41
|
-
return new StellarBrokerError(21, 'Invalid swap transaction received from the server')
|
|
42
|
-
},
|
|
43
|
-
failedToSignTx() {
|
|
44
|
-
return new StellarBrokerError(22, 'Failed to sign received transaction')
|
|
45
|
-
},
|
|
46
|
-
unsupportedEventType(type) {
|
|
47
|
-
return new StellarBrokerError(31, 'Unknown event type: ' + type)
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
export class StellarBrokerError extends Error {
|
|
2
|
+
constructor(code, message) {
|
|
3
|
+
super(message)
|
|
4
|
+
this.code = code
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Numeric error code
|
|
9
|
+
* @type {number}
|
|
10
|
+
* @readonly
|
|
11
|
+
*/
|
|
12
|
+
code = 0
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const errors = {
|
|
16
|
+
invalidInitParam(param) {
|
|
17
|
+
return new StellarBrokerError(1, `Invalid parameter value: "${param}"`)
|
|
18
|
+
},
|
|
19
|
+
invalidAuthorizationParam() {
|
|
20
|
+
return new StellarBrokerError(2, 'Invalid authorization secret key or callback provided')
|
|
21
|
+
},
|
|
22
|
+
notConnected() {
|
|
23
|
+
return new StellarBrokerError(3, 'Client not connected to the server')
|
|
24
|
+
},
|
|
25
|
+
quoteNotSet() {
|
|
26
|
+
return new StellarBrokerError(11, 'Price quote not available')
|
|
27
|
+
},
|
|
28
|
+
quoteExpired() {
|
|
29
|
+
return new StellarBrokerError(12, 'Price quote expired')
|
|
30
|
+
},
|
|
31
|
+
quoteError(message) {
|
|
32
|
+
return new StellarBrokerError(13, 'Price quotation error: ' + message)
|
|
33
|
+
},
|
|
34
|
+
invalidQuoteParam(invalidParamName = 'asset', details) {
|
|
35
|
+
return new StellarBrokerError(14, `Invalid quote request parameter: "${invalidParamName}". ${details}`)
|
|
36
|
+
},
|
|
37
|
+
tradeInProgress() {
|
|
38
|
+
return new StellarBrokerError(20, 'Cannot change quote while trade is in progress')
|
|
39
|
+
},
|
|
40
|
+
invalidSwapTx() {
|
|
41
|
+
return new StellarBrokerError(21, 'Invalid swap transaction received from the server')
|
|
42
|
+
},
|
|
43
|
+
failedToSignTx() {
|
|
44
|
+
return new StellarBrokerError(22, 'Failed to sign received transaction')
|
|
45
|
+
},
|
|
46
|
+
unsupportedEventType(type) {
|
|
47
|
+
return new StellarBrokerError(31, 'Unknown event type: ' + type)
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
serverError(message) {
|
|
52
|
+
return new StellarBrokerError(101, message)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
54
56
|
export default errors
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {FeeBumpTransaction} from "@stellar/stellar-sdk";
|
|
1
|
+
import {Asset, FeeBumpTransaction, TransactionI} from "@stellar/stellar-sdk";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Client for StellarBroker service
|
|
5
|
+
*/
|
|
3
6
|
export default class StellarBrokerClient {
|
|
4
7
|
/**
|
|
5
8
|
* @param {ClientInitializationParams} params
|
|
@@ -10,7 +13,7 @@ export default class StellarBrokerClient {
|
|
|
10
13
|
* Trader account public key
|
|
11
14
|
* @type {string}
|
|
12
15
|
*/
|
|
13
|
-
readonly
|
|
16
|
+
readonly trader: string;
|
|
14
17
|
/**
|
|
15
18
|
* Stellar network passphrase
|
|
16
19
|
* @type {string}
|
|
@@ -40,10 +43,8 @@ export default class StellarBrokerClient {
|
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
45
|
* Confirm current quote and start trading
|
|
43
|
-
* @param {string} account - Trader account address
|
|
44
|
-
* @param {string|ClientAuthorizationCallback} authorization - Authorization method, either account secret key or an authorization callback
|
|
45
46
|
*/
|
|
46
|
-
confirmQuote(
|
|
47
|
+
confirmQuote(): void;
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Add event listener
|
|
@@ -91,11 +92,7 @@ export interface SwapQuoteParams {
|
|
|
91
92
|
*/
|
|
92
93
|
sellingAmount?: string
|
|
93
94
|
/**
|
|
94
|
-
*
|
|
95
|
-
*/
|
|
96
|
-
buyingAmount?: string
|
|
97
|
-
/**
|
|
98
|
-
* Swap slippage tolerance (2% by default)
|
|
95
|
+
* Swap slippage tolerance (0.02 by default - 2%)
|
|
99
96
|
*/
|
|
100
97
|
slippageTolerance?: number
|
|
101
98
|
}
|
|
@@ -103,36 +100,56 @@ export interface SwapQuoteParams {
|
|
|
103
100
|
|
|
104
101
|
export interface ClientInitializationParams {
|
|
105
102
|
/**
|
|
106
|
-
*
|
|
103
|
+
* Trader account address
|
|
107
104
|
*/
|
|
108
|
-
|
|
105
|
+
account: string;
|
|
109
106
|
/**
|
|
110
|
-
*
|
|
107
|
+
* Partner key
|
|
111
108
|
*/
|
|
112
|
-
|
|
109
|
+
partnerKey?: string;
|
|
113
110
|
/**
|
|
114
|
-
*
|
|
111
|
+
* Authorization method, either account secret key or an authorization callback
|
|
115
112
|
*/
|
|
116
|
-
|
|
113
|
+
authorization: ClientAuthorizationParams;
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
export type ClientStatus = "disconnected" | "ready" | "quote" | "trade";
|
|
120
117
|
|
|
121
|
-
export type StellarBrokerClientEvent = "quote" | "
|
|
122
|
-
|
|
123
|
-
export type SwapFlowMode = "direct";
|
|
118
|
+
export type StellarBrokerClientEvent = "quote" | "progress" | "paused" | "finished" | "error";
|
|
124
119
|
|
|
125
120
|
export interface SwapQuoteResult {
|
|
121
|
+
/**
|
|
122
|
+
* Quote timestamp formatted as ISO date
|
|
123
|
+
*/
|
|
124
|
+
ts: string,
|
|
125
|
+
/**
|
|
126
|
+
* Asset to sell
|
|
127
|
+
*/
|
|
126
128
|
sellingAsset: string;
|
|
129
|
+
/**
|
|
130
|
+
* Asset to buy
|
|
131
|
+
*/
|
|
127
132
|
buyingAsset: string;
|
|
133
|
+
/**
|
|
134
|
+
* Swap slippage tolerance
|
|
135
|
+
*/
|
|
128
136
|
slippageTolerance: number;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Amount of the selling asset
|
|
139
|
+
*/
|
|
140
|
+
sellingAmount: string;
|
|
141
|
+
/**
|
|
142
|
+
* Estimated amount of buyingAsset to receive
|
|
143
|
+
*/
|
|
132
144
|
estimatedBuyingAmount?: string;
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Equivalent direct path_payment trade estimate
|
|
147
|
+
*/
|
|
135
148
|
directTrade?: SwapQuoteDirectTradeResult;
|
|
149
|
+
/**
|
|
150
|
+
* Error details from the server (for failed quotes)
|
|
151
|
+
*/
|
|
152
|
+
error?: string;
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
export interface SwapQuoteDirectTradeResult {
|
|
@@ -150,4 +167,57 @@ export class StellarBrokerError extends Error {
|
|
|
150
167
|
readonly code: number;
|
|
151
168
|
}
|
|
152
169
|
|
|
153
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Async authorization callback for transactions and simulated auth entries
|
|
172
|
+
*/
|
|
173
|
+
export type ClientAuthorizationCallback = (txOrAuthEntry: TransactionI | Buffer) => Promise<TransactionI | Buffer>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Authorization method, either account secret key or an authorization callback
|
|
177
|
+
*/
|
|
178
|
+
export type ClientAuthorizationParams = string | ClientAuthorizationCallback;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Trader mediator account wrapper
|
|
182
|
+
*/
|
|
183
|
+
export class Mediator {
|
|
184
|
+
/**
|
|
185
|
+
* Create a trader mediator account instance for a given source account
|
|
186
|
+
* @param source - Source account
|
|
187
|
+
* @param sellingAsset - Asset to sell
|
|
188
|
+
* @param buyingAsset - Asset to buy
|
|
189
|
+
* @param sellingAmount - Amount to sell
|
|
190
|
+
* @param authorization - Authorization callback or secret key
|
|
191
|
+
*/
|
|
192
|
+
constructor(source: string, sellingAsset: string | Asset, buyingAsset: string | Asset, sellingAmount: string, authorization: ClientAuthorizationParams)
|
|
193
|
+
|
|
194
|
+
readonly source: string;
|
|
195
|
+
|
|
196
|
+
readonly sellingAsset: Asset;
|
|
197
|
+
|
|
198
|
+
readonly buyingAsset: Asset;
|
|
199
|
+
|
|
200
|
+
readonly sellingAmount: bigint;
|
|
201
|
+
|
|
202
|
+
readonly mediatorAddress: string;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if there are any non-disposed mediators that belong to lost swap sessions
|
|
206
|
+
*/
|
|
207
|
+
get hasObsoleteMediators(): boolean;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Retrieve funds from mediator accounts that belong to lost swap sessions
|
|
211
|
+
*/
|
|
212
|
+
disposeObsoleteMediators(): Promise<void>;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create mediator account and deposit tokens to sell
|
|
216
|
+
*/
|
|
217
|
+
init(): Promise<string>;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Dispose mediator account
|
|
221
|
+
*/
|
|
222
|
+
dispose(address:string): Promise<void>;
|
|
223
|
+
}
|
package/src/index.js
CHANGED
package/src/mediator.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {Asset, Keypair, Memo, Operation, TransactionBuilder, Horizon, Networks, StrKey} 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
|
+
//additional XLM amount to cover tx fees
|
|
7
|
+
const feesReserve = '2'
|
|
8
|
+
|
|
9
|
+
export class Mediator {
|
|
10
|
+
/**
|
|
11
|
+
* Create a trader mediator account instance for a given source account
|
|
12
|
+
* @param {string} source - Creator account address
|
|
13
|
+
* @param {string|Asset} sellingAsset - Identifier of the asset to sell
|
|
14
|
+
* @param {string|Asset} buyingAsset - Identifier of the asset to buy
|
|
15
|
+
* @param {string} sellingAmount - Asset amount to sell
|
|
16
|
+
* @param {ClientAuthorizationParams} authorization - Authorization
|
|
17
|
+
*/
|
|
18
|
+
constructor(source, sellingAsset, buyingAsset, sellingAmount, authorization) {
|
|
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
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creator account address
|
|
45
|
+
* @type {string}
|
|
46
|
+
* @readonly
|
|
47
|
+
*/
|
|
48
|
+
source
|
|
49
|
+
/**
|
|
50
|
+
* @type {Asset}
|
|
51
|
+
* @readonly
|
|
52
|
+
*/
|
|
53
|
+
sellingAsset
|
|
54
|
+
/**
|
|
55
|
+
* @type {Asset}
|
|
56
|
+
* @readonly
|
|
57
|
+
*/
|
|
58
|
+
buyingAsset
|
|
59
|
+
/**
|
|
60
|
+
* @type {bigint}
|
|
61
|
+
* @readonly
|
|
62
|
+
*/
|
|
63
|
+
sellingAmount
|
|
64
|
+
/**
|
|
65
|
+
* @type {AuthorizationWrapper}
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
authorization
|
|
69
|
+
/**
|
|
70
|
+
* @type {Keypair}
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
mediator
|
|
74
|
+
/**
|
|
75
|
+
* @type {string}
|
|
76
|
+
* @readonly
|
|
77
|
+
*/
|
|
78
|
+
mediatorAddress
|
|
79
|
+
/**
|
|
80
|
+
* Prefix of the mediator address record in the localStorage
|
|
81
|
+
* @type {string}
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
storagePrefix = 'msb_'
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if there are any non-disposed mediators that belong to lost swap sessions
|
|
88
|
+
* @return {boolean}
|
|
89
|
+
*/
|
|
90
|
+
get hasObsoleteMediators() {
|
|
91
|
+
return Object.entries(localStorage).some(([key, initiator]) =>
|
|
92
|
+
initiator === this.source && key.startsWith(this.storagePrefix))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Retrieve funds from mediator accounts that belong to lost swap sessions
|
|
97
|
+
* @return {Promise}
|
|
98
|
+
*/
|
|
99
|
+
async disposeObsoleteMediators() { //TODO: deal with potential problems caused by concurrent swaps
|
|
100
|
+
for (let [key, initiator] of Object.entries(localStorage)) {
|
|
101
|
+
if (initiator === this.source && key.startsWith(this.storagePrefix)) {
|
|
102
|
+
await this.dispose(key.replace(this.storagePrefix, ''))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create mediator account and deposit tokens to sell
|
|
109
|
+
* @return {Promise<string>} - Newly created mediator secret key
|
|
110
|
+
*/
|
|
111
|
+
async init() {
|
|
112
|
+
const sourceAccount = await this.loadAccount(this.source)
|
|
113
|
+
if (!sourceAccount)
|
|
114
|
+
throw new Error('Mediator account doesn\'t exist on the ledger')
|
|
115
|
+
const ops = []
|
|
116
|
+
//create new random keypair for the trade
|
|
117
|
+
this.mediator = Keypair.random()
|
|
118
|
+
this.mediatorAddress = this.mediator.publicKey()
|
|
119
|
+
const {sellingAsset, buyingAsset} = this
|
|
120
|
+
if (sellingAsset.isNative()) { //for XLM total amount should include fee reserves
|
|
121
|
+
const amount = this.sellingAmount + BigInt(feesReserve * 10000000)
|
|
122
|
+
//check available balance
|
|
123
|
+
if (toStroops(findTrustline(sourceAccount, sellingAsset).balance) < amount)
|
|
124
|
+
throw new Error('Insufficient XLM balance for selling amount + potential trading fees')
|
|
125
|
+
//only create account is required for asset transfer
|
|
126
|
+
ops.push(Operation.createAccount({
|
|
127
|
+
source: this.source,
|
|
128
|
+
destination: this.mediatorAddress,
|
|
129
|
+
startingBalance: fromStroops(amount)
|
|
130
|
+
}))
|
|
131
|
+
} else {
|
|
132
|
+
//check available XLM balance
|
|
133
|
+
const xlmBalance = findTrustline(sourceAccount, Asset.native())
|
|
134
|
+
if (xlmBalance.balance < parseInt(feesReserve))
|
|
135
|
+
throw new Error('Insufficient XLM balance for potential trading fees')
|
|
136
|
+
//create mediator account
|
|
137
|
+
ops.push(Operation.createAccount({
|
|
138
|
+
source: this.source,
|
|
139
|
+
destination: this.mediatorAddress,
|
|
140
|
+
startingBalance: feesReserve // for tx fees
|
|
141
|
+
}))
|
|
142
|
+
//check available XLM balance
|
|
143
|
+
const sellingTrustline = findTrustline(sourceAccount, this.sellingAsset)
|
|
144
|
+
if (!sellingTrustline || toStroops(sellingTrustline.balance) < this.sellingAmount)
|
|
145
|
+
throw new Error('Insufficient selling asset balance')
|
|
146
|
+
if (!findTrustline(sourceAccount, this.buyingAsset))
|
|
147
|
+
throw new Error('Source account doesn\'t have buying asset trustline')
|
|
148
|
+
//TODO: check authorizations
|
|
149
|
+
//create trustline for selling asset
|
|
150
|
+
ops.push(Operation.changeTrust({
|
|
151
|
+
source: this.mediatorAddress,
|
|
152
|
+
asset: sellingAsset
|
|
153
|
+
}))
|
|
154
|
+
//transfer tokens to sell
|
|
155
|
+
ops.push(Operation.payment({
|
|
156
|
+
source: this.source,
|
|
157
|
+
asset: sellingAsset,
|
|
158
|
+
destination: this.mediatorAddress,
|
|
159
|
+
amount: fromStroops(this.sellingAmount)
|
|
160
|
+
}))
|
|
161
|
+
}
|
|
162
|
+
//add source account as a signer
|
|
163
|
+
ops.push(Operation.setOptions({
|
|
164
|
+
source: this.mediatorAddress,
|
|
165
|
+
homeDomain: 'mediator.stellar.broker',
|
|
166
|
+
masterWeight: 1,
|
|
167
|
+
highThreshold: 1,
|
|
168
|
+
medThreshold: 1,
|
|
169
|
+
lowThreshold: 1,
|
|
170
|
+
signer: {
|
|
171
|
+
ed25519PublicKey: this.source,
|
|
172
|
+
weight: 1
|
|
173
|
+
}
|
|
174
|
+
}))
|
|
175
|
+
//create trustline for buying asset
|
|
176
|
+
ops.push(Operation.changeTrust({
|
|
177
|
+
source: this.mediatorAddress,
|
|
178
|
+
asset: buyingAsset
|
|
179
|
+
}))
|
|
180
|
+
//confirm tx
|
|
181
|
+
await this.buildAndSend(sourceAccount, ops)
|
|
182
|
+
//store the record in the localStorage
|
|
183
|
+
localStorage.setItem(this.storagePrefix + this.mediatorAddress, this.source)
|
|
184
|
+
//client will need mediator secret for trading
|
|
185
|
+
return this.mediator.secret()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Dispose mediator account
|
|
190
|
+
* @param {string} [address]
|
|
191
|
+
* @return {Promise}
|
|
192
|
+
*/
|
|
193
|
+
async dispose(address) {
|
|
194
|
+
if (!address) {
|
|
195
|
+
address = this.mediatorAddress
|
|
196
|
+
}
|
|
197
|
+
//load account
|
|
198
|
+
let account = await this.loadAccount(address)
|
|
199
|
+
if (!account)
|
|
200
|
+
throw new Error(`Mediator account ${address} doesn't exist on the ledger`)
|
|
201
|
+
|
|
202
|
+
if (!account.signers.find(s => s.key === this.source))
|
|
203
|
+
throw new Error(`${address} is not a mediator account for ${this.source}`)
|
|
204
|
+
const ops = []
|
|
205
|
+
//remove trustlines for each account balance
|
|
206
|
+
for (const balance of account.balances) {
|
|
207
|
+
if (balance.asset_type === 'native')
|
|
208
|
+
continue //skip XLM trustline - merge will handle the transfer
|
|
209
|
+
const asset = convertToStellarAsset(balance)
|
|
210
|
+
//transfer remaining balance to the source account
|
|
211
|
+
if (balance.balance > 0) {
|
|
212
|
+
ops.push(Operation.payment({
|
|
213
|
+
source: address,
|
|
214
|
+
asset,
|
|
215
|
+
destination: this.source,
|
|
216
|
+
amount: balance.balance
|
|
217
|
+
}))
|
|
218
|
+
}
|
|
219
|
+
//remove trustline
|
|
220
|
+
ops.push(Operation.changeTrust({
|
|
221
|
+
source: address,
|
|
222
|
+
asset,
|
|
223
|
+
limit: '0'
|
|
224
|
+
}))
|
|
225
|
+
}
|
|
226
|
+
//merge
|
|
227
|
+
ops.push(Operation.accountMerge({
|
|
228
|
+
source: address,
|
|
229
|
+
destination: this.source
|
|
230
|
+
}))
|
|
231
|
+
if (this.mediatorAddress !== address) {
|
|
232
|
+
account = await this.loadAccount(this.source)
|
|
233
|
+
}
|
|
234
|
+
await this.buildAndSend(account, ops)
|
|
235
|
+
//remove reference from local storage
|
|
236
|
+
localStorage.removeItem(this.storagePrefix + address)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {AccountResponse} account
|
|
241
|
+
* @param {Operation[]} operations
|
|
242
|
+
* @param {string} [memo]
|
|
243
|
+
* @private
|
|
244
|
+
*/
|
|
245
|
+
async buildAndSend(account, operations, memo) {
|
|
246
|
+
//create builder
|
|
247
|
+
const builder = new TransactionBuilder(account, {
|
|
248
|
+
fee: '1000000',
|
|
249
|
+
networkPassphrase: Networks.PUBLIC
|
|
250
|
+
})
|
|
251
|
+
builder.setTimeout(30)
|
|
252
|
+
//add memo if needed
|
|
253
|
+
if (memo) {
|
|
254
|
+
builder.addMemo(Memo.text(memo))
|
|
255
|
+
}
|
|
256
|
+
//add operations
|
|
257
|
+
for (const op of operations) {
|
|
258
|
+
builder.addOperation(op)
|
|
259
|
+
}
|
|
260
|
+
//build tx
|
|
261
|
+
let tx = builder.build()
|
|
262
|
+
//sign it on behalf of the mediator account (if available)
|
|
263
|
+
if (this.mediator) {
|
|
264
|
+
tx.sign(this.mediator)
|
|
265
|
+
}
|
|
266
|
+
//request signature from the client (if required)
|
|
267
|
+
if (account.account_id === this.source) {
|
|
268
|
+
tx = await this.authorization.authorize(tx)
|
|
269
|
+
}
|
|
270
|
+
//execute the tx
|
|
271
|
+
const res = await this.createHorizon().submitTransaction(tx, {skipMemoRequiredCheck: true})
|
|
272
|
+
if (!res.successful)
|
|
273
|
+
throw new Error('Failed to create mediator account')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {string} address
|
|
278
|
+
* @return {Promise<AccountResponse>}
|
|
279
|
+
* @private
|
|
280
|
+
*/
|
|
281
|
+
async loadAccount(address) {
|
|
282
|
+
try {
|
|
283
|
+
return this.createHorizon().loadAccount(address)
|
|
284
|
+
} catch (e) {
|
|
285
|
+
console.error(e)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @return {HorizonServer}
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
createHorizon() {
|
|
294
|
+
return new Horizon.Server('https://horizon.stellar.org')
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {AccountResponse} account
|
|
301
|
+
* @param {Asset} asset
|
|
302
|
+
* @return {BalanceLine}
|
|
303
|
+
*/
|
|
304
|
+
function findTrustline(account, asset) {
|
|
305
|
+
return account.balances.find(b => asset.isNative() ?
|
|
306
|
+
b.asset_type === 'native' :
|
|
307
|
+
b.asset_code === asset.code && b.asset_issuer === asset.issuer
|
|
308
|
+
)
|
|
309
|
+
}
|