@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/quote-request.js
CHANGED
|
@@ -1,61 +1,57 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* @
|
|
7
|
-
* @property {string}
|
|
8
|
-
* @property {string}
|
|
9
|
-
* @property {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (res.
|
|
33
|
-
throw errors.invalidQuoteParam('
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
throw errors.invalidQuoteParam(parameter, '
|
|
56
|
-
|
|
57
|
-
throw errors.invalidQuoteParam(parameter, 'Slippage tolerance is too small, expected value >= 0')
|
|
58
|
-
if (tolerance > 0.5)
|
|
59
|
-
throw errors.invalidQuoteParam(parameter, 'Slippage tolerance is too large, expected value < 0.5 (50%)')
|
|
60
|
-
return tolerance
|
|
1
|
+
import errors from './errors.js'
|
|
2
|
+
import {parseAsset} from './asset.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} SwapQuoteParams
|
|
6
|
+
* @property {string} sellingAsset - Asset to sell
|
|
7
|
+
* @property {string} buyingAsset - Asset to buy
|
|
8
|
+
* @property {string} [sellingAmount] - Amount of selling asset
|
|
9
|
+
* @property {number} [slippageTolerance] - Swap slippage tolerance (0.02 by default - 2%)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {SwapQuoteParams} params
|
|
14
|
+
* @return {SwapQuoteParams}
|
|
15
|
+
*/
|
|
16
|
+
export function validateQuoteRequest(params) {
|
|
17
|
+
const {
|
|
18
|
+
sellingAsset, selling_asset, buyingAsset, buying_asset, sellingAmount, selling_amount,
|
|
19
|
+
buyingAmount, buying_amount, slippageTolerance, slippage_tolerance, ...other
|
|
20
|
+
} = params
|
|
21
|
+
const res = {
|
|
22
|
+
sellingAsset: parseAsset(sellingAsset || selling_asset, 'sellingAsset'),
|
|
23
|
+
buyingAsset: parseAsset(buyingAsset || buying_asset, 'buyingAsset'),
|
|
24
|
+
sellingAmount: parseAmount(sellingAmount || selling_amount, 'sellingAmount'),
|
|
25
|
+
//buyingAmount: parseAmount(buyingAmount || buying_amount, 'buyingAmount'),
|
|
26
|
+
slippageTolerance: parseSlippageTolerance(slippageTolerance || slippage_tolerance || 0.02, 'slippageTolerance')
|
|
27
|
+
}
|
|
28
|
+
if (res.buyingAsset === res.sellingAsset)
|
|
29
|
+
throw errors.invalidQuoteParam('buyingAsset', 'Buying asset can\'t be the same as selling asset')
|
|
30
|
+
if (res.sellingAmount === undefined)
|
|
31
|
+
throw errors.invalidQuoteParam('sellingAmount', 'Parameter "sellingAmount" parameter is required')
|
|
32
|
+
/*if (res.buyingAmount !== undefined && res.sellingAmount !== undefined)
|
|
33
|
+
throw errors.invalidQuoteParam('sellingAmount', 'Parameters "buyingAmount" and "sellingAmount" are mutually exclusive')*/
|
|
34
|
+
Object.assign(res, other) //add remaining optional params
|
|
35
|
+
Object.freeze(res)
|
|
36
|
+
return res
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseAmount(amount, parameter) {
|
|
40
|
+
if (amount === undefined)
|
|
41
|
+
return undefined
|
|
42
|
+
const parsed = parseFloat(amount)
|
|
43
|
+
if (isNaN(parsed) || parsed <= 0)
|
|
44
|
+
throw errors.invalidQuoteParam(parameter, 'Invalid asset amount: ' + amount)
|
|
45
|
+
return amount
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseSlippageTolerance(src, parameter) {
|
|
49
|
+
const tolerance = parseFloat(src)
|
|
50
|
+
if (isNaN(tolerance))
|
|
51
|
+
throw errors.invalidQuoteParam(parameter, 'Invalid slippage tolerance, number expected')
|
|
52
|
+
if (tolerance < 0)
|
|
53
|
+
throw errors.invalidQuoteParam(parameter, 'Slippage tolerance is too small, expected value >= 0')
|
|
54
|
+
if (tolerance > 0.5)
|
|
55
|
+
throw errors.invalidQuoteParam(parameter, 'Slippage tolerance is too large, expected value < 0.5 (50%)')
|
|
56
|
+
return tolerance
|
|
61
57
|
}
|
package/src/tx-processor.js
CHANGED
|
@@ -1,162 +1,134 @@
|
|
|
1
|
-
import {hash, Keypair, nativeToScVal, StrKey, TransactionBuilder, xdr} from '@stellar/stellar-sdk'
|
|
2
|
-
import errors from './errors.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @param {StellarBrokerClient} client
|
|
6
|
-
* @param {{xdr: string, hash: string, networkFee: string, [confirmed]: boolean}} txRequest
|
|
7
|
-
* @return {Promise<string>}
|
|
8
|
-
*/
|
|
9
|
-
export async function processTxRequest(client, txRequest) {
|
|
10
|
-
//parse incoming transaction
|
|
11
|
-
let tx
|
|
12
|
-
try {
|
|
13
|
-
tx = TransactionBuilder.fromXDR(txRequest.xdr, client.network)
|
|
14
|
-
} catch (e) {
|
|
15
|
-
throw errors.invalidSwapTx()
|
|
16
|
-
}
|
|
17
|
-
//check that transaction is correct
|
|
18
|
-
if (!validateTransaction(client, tx))
|
|
19
|
-
throw errors.invalidSwapTx()
|
|
20
|
-
//if transaction has not been authorized yet by the channel account
|
|
21
|
-
const isSorobanTx = tx.operations[0].auth?.length > 0
|
|
22
|
-
if (isSorobanTx) {
|
|
23
|
-
//send tx back to the server
|
|
24
|
-
if (!tx.signatures.length) {
|
|
25
|
-
//sign auth
|
|
26
|
-
await authorizeInvocation(client, tx)
|
|
27
|
-
tx = await signTx(client, tx)
|
|
28
|
-
return tx.toXDR()//wait for the signed tx from the server to wrap it with fee bump tx
|
|
29
|
-
}
|
|
30
|
-
} else {
|
|
31
|
-
//sign transaction
|
|
32
|
-
tx = await signTx(client, tx)
|
|
33
|
-
}
|
|
34
|
-
//wrap with fee bump
|
|
35
|
-
let wrapped = TransactionBuilder.buildFeeBumpTransaction(client.trader, txRequest.networkFee, tx, client.network)
|
|
36
|
-
//sign fee bump wrapper tx
|
|
37
|
-
wrapped = await signTx(client, wrapped)
|
|
38
|
-
//respond with signed transaction
|
|
39
|
-
return wrapped.toXDR()
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param {StellarBrokerClient} client
|
|
44
|
-
* @param {TransactionI} tx
|
|
45
|
-
* @return {Promise<TransactionI>}
|
|
46
|
-
* @private
|
|
47
|
-
*/
|
|
48
|
-
async function authorizeInvocation(client, tx) {
|
|
49
|
-
//from https://github.com/stellar/js-stellar-base/blob/e3d6fc3351e7d242b374c7c6057668366364a279/src/auth.js#L97
|
|
50
|
-
const auth = tx.operations[0].auth[0]
|
|
51
|
-
/** @type {xdr.SorobanAddressCredentials} */
|
|
52
|
-
const addrAuth = auth.credentials().address()
|
|
53
|
-
addrAuth.signatureExpirationLedger(tx.ledgerBounds.maxLedger + 1)
|
|
54
|
-
|
|
55
|
-
const networkId = hash(Buffer.from(tx.networkPassphrase))
|
|
56
|
-
const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
57
|
-
new xdr.HashIdPreimageSorobanAuthorization({
|
|
58
|
-
networkId,
|
|
59
|
-
nonce: addrAuth.nonce(),
|
|
60
|
-
invocation: auth.rootInvocation(),
|
|
61
|
-
signatureExpirationLedger: addrAuth.signatureExpirationLedger()
|
|
62
|
-
})
|
|
63
|
-
)
|
|
64
|
-
const payload = hash(preimage.toXDR())
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return false*/
|
|
135
|
-
for (let swap of tx.operations) {
|
|
136
|
-
if (swap.type === 'invokeHostFunction')
|
|
137
|
-
continue //TODO: add additional validation
|
|
138
|
-
if (swap.type !== 'pathPaymentStrictSend' && swap.type !== 'pathPaymentStrictReceive')
|
|
139
|
-
return false
|
|
140
|
-
const isFee = swap.destination !== client.trader
|
|
141
|
-
if (isFee) {
|
|
142
|
-
if (swap.type !== 'pathPaymentStrictSend')
|
|
143
|
-
return false
|
|
144
|
-
if ((swap.source && swap.source !== client.trader))
|
|
145
|
-
return false
|
|
146
|
-
} else {
|
|
147
|
-
if ((swap.source && swap.source !== swap.destination) || swap.destination !== client.trader)
|
|
148
|
-
return false
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
//TODO: check assets and amounts
|
|
152
|
-
return true
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function promisify(callResult) {
|
|
156
|
-
if (!(callResult instanceof Promise)) {
|
|
157
|
-
if (!callResult)
|
|
158
|
-
return Promise.reject()
|
|
159
|
-
callResult = Promise.resolve(callResult)
|
|
160
|
-
}
|
|
161
|
-
return callResult
|
|
1
|
+
import {hash, Keypair, nativeToScVal, StrKey, TransactionBuilder, xdr} from '@stellar/stellar-sdk'
|
|
2
|
+
import errors from './errors.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {StellarBrokerClient} client
|
|
6
|
+
* @param {{xdr: string, hash: string, networkFee: string, [confirmed]: boolean}} txRequest
|
|
7
|
+
* @return {Promise<string>}
|
|
8
|
+
*/
|
|
9
|
+
export async function processTxRequest(client, txRequest) {
|
|
10
|
+
//parse incoming transaction
|
|
11
|
+
let tx
|
|
12
|
+
try {
|
|
13
|
+
tx = TransactionBuilder.fromXDR(txRequest.xdr, client.network)
|
|
14
|
+
} catch (e) {
|
|
15
|
+
throw errors.invalidSwapTx()
|
|
16
|
+
}
|
|
17
|
+
//check that transaction is correct
|
|
18
|
+
if (!validateTransaction(client, tx))
|
|
19
|
+
throw errors.invalidSwapTx()
|
|
20
|
+
//if transaction has not been authorized yet by the channel account
|
|
21
|
+
const isSorobanTx = tx.operations[0].auth?.length > 0
|
|
22
|
+
if (isSorobanTx) {
|
|
23
|
+
//send tx back to the server
|
|
24
|
+
if (!tx.signatures.length) {
|
|
25
|
+
//sign auth
|
|
26
|
+
await authorizeInvocation(client, tx)
|
|
27
|
+
tx = await signTx(client, tx)
|
|
28
|
+
return tx.toXDR()//wait for the signed tx from the server to wrap it with fee bump tx
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
//sign transaction
|
|
32
|
+
tx = await signTx(client, tx)
|
|
33
|
+
}
|
|
34
|
+
//wrap with fee bump
|
|
35
|
+
let wrapped = TransactionBuilder.buildFeeBumpTransaction(client.trader, txRequest.networkFee, tx, client.network)
|
|
36
|
+
//sign fee bump wrapper tx
|
|
37
|
+
wrapped = await signTx(client, wrapped)
|
|
38
|
+
//respond with signed transaction
|
|
39
|
+
return wrapped.toXDR()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {StellarBrokerClient} client
|
|
44
|
+
* @param {TransactionI} tx
|
|
45
|
+
* @return {Promise<TransactionI>}
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
async function authorizeInvocation(client, tx) {
|
|
49
|
+
//from https://github.com/stellar/js-stellar-base/blob/e3d6fc3351e7d242b374c7c6057668366364a279/src/auth.js#L97
|
|
50
|
+
const auth = tx.operations[0].auth[0]
|
|
51
|
+
/** @type {xdr.SorobanAddressCredentials} */
|
|
52
|
+
const addrAuth = auth.credentials().address()
|
|
53
|
+
addrAuth.signatureExpirationLedger(tx.ledgerBounds.maxLedger + 1)
|
|
54
|
+
|
|
55
|
+
const networkId = hash(Buffer.from(tx.networkPassphrase))
|
|
56
|
+
const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
57
|
+
new xdr.HashIdPreimageSorobanAuthorization({
|
|
58
|
+
networkId,
|
|
59
|
+
nonce: addrAuth.nonce(),
|
|
60
|
+
invocation: auth.rootInvocation(),
|
|
61
|
+
signatureExpirationLedger: addrAuth.signatureExpirationLedger()
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
const payload = hash(preimage.toXDR())
|
|
65
|
+
const signature = await client.authorization.authorize(payload)
|
|
66
|
+
const publicKey = StrKey.encodeEd25519PublicKey(addrAuth.address().accountId().value())
|
|
67
|
+
const sigScVal = nativeToScVal(
|
|
68
|
+
{
|
|
69
|
+
public_key: StrKey.decodeEd25519PublicKey(publicKey),
|
|
70
|
+
signature
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: {
|
|
74
|
+
public_key: ['symbol', null],
|
|
75
|
+
signature: ['symbol', null]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
addrAuth.signature(xdr.ScVal.scvVec([sigScVal]))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {StellarBrokerClient} client
|
|
85
|
+
* @param {TransactionI} tx
|
|
86
|
+
* @return {Promise<TransactionI>}
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
function signTx(client, tx) {
|
|
90
|
+
return client.authorization.authorize(tx)
|
|
91
|
+
.then(tx => {
|
|
92
|
+
if (typeof tx === 'string') { //normalize response
|
|
93
|
+
tx = TransactionBuilder.fromXDR(tx, client.network)
|
|
94
|
+
}
|
|
95
|
+
if (!tx || tx.signatures?.length < 1)
|
|
96
|
+
throw new Error(`Transaction was not signed`)
|
|
97
|
+
return tx
|
|
98
|
+
})
|
|
99
|
+
.catch(e => {
|
|
100
|
+
console.error(e)
|
|
101
|
+
throw errors.failedToSignTx()
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {StellarBrokerClient} client
|
|
107
|
+
* @param {Transaction} tx
|
|
108
|
+
* @throws {StellarBrokerError} Invalid swap transaction received
|
|
109
|
+
*/
|
|
110
|
+
function validateTransaction(client, tx) {
|
|
111
|
+
/*if (!(tx instanceof FeeBumpTransaction))
|
|
112
|
+
return false
|
|
113
|
+
const {innerTransaction} = tx
|
|
114
|
+
if (tx.feeSource !== this.source)
|
|
115
|
+
return false*/
|
|
116
|
+
for (let swap of tx.operations) {
|
|
117
|
+
if (swap.type === 'invokeHostFunction')
|
|
118
|
+
continue //TODO: add additional validation
|
|
119
|
+
if (swap.type !== 'pathPaymentStrictSend' && swap.type !== 'pathPaymentStrictReceive')
|
|
120
|
+
return false
|
|
121
|
+
const isFee = swap.destination !== client.trader
|
|
122
|
+
if (isFee) {
|
|
123
|
+
if (swap.type !== 'pathPaymentStrictSend')
|
|
124
|
+
return false
|
|
125
|
+
if ((swap.source && swap.source !== client.trader))
|
|
126
|
+
return false
|
|
127
|
+
} else {
|
|
128
|
+
if ((swap.source && swap.source !== swap.destination) || swap.destination !== client.trader)
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//TODO: check assets and amounts
|
|
133
|
+
return true
|
|
162
134
|
}
|