@ocap/tx-protocols 1.6.3 → 1.6.10

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.
Files changed (52) hide show
  1. package/README.md +36 -0
  2. package/lib/execute.js +97 -30
  3. package/lib/index.js +56 -14
  4. package/lib/protocols/account/declare.js +36 -30
  5. package/lib/protocols/account/delegate.js +78 -40
  6. package/lib/protocols/account/migrate.js +65 -49
  7. package/lib/protocols/account/revoke-delegate.js +39 -23
  8. package/lib/protocols/asset/acquire-v2.js +159 -0
  9. package/lib/protocols/asset/acquire-v3.js +242 -0
  10. package/lib/protocols/asset/calls/README.md +5 -0
  11. package/lib/protocols/asset/calls/transfer-token.js +37 -0
  12. package/lib/protocols/asset/calls/transfer.js +29 -0
  13. package/lib/protocols/asset/create.js +85 -36
  14. package/lib/protocols/asset/mint.js +133 -0
  15. package/lib/protocols/asset/pipes/exec-mint-hook.js +59 -0
  16. package/lib/protocols/asset/pipes/extract-factory-tokens.js +18 -0
  17. package/lib/protocols/asset/pipes/verify-acquire-params.js +30 -0
  18. package/lib/protocols/asset/pipes/verify-itx-address.js +41 -0
  19. package/lib/protocols/asset/pipes/verify-itx-assets.js +49 -0
  20. package/lib/protocols/asset/pipes/verify-itx-variables.js +26 -0
  21. package/lib/protocols/asset/pipes/verify-mint-limit.js +13 -0
  22. package/lib/protocols/asset/update.js +76 -44
  23. package/lib/protocols/factory/create.js +146 -0
  24. package/lib/protocols/governance/claim-stake.js +219 -0
  25. package/lib/protocols/governance/revoke-stake.js +136 -0
  26. package/lib/protocols/governance/stake.js +176 -0
  27. package/lib/protocols/rollup/claim-reward.js +283 -0
  28. package/lib/protocols/rollup/create-block.js +333 -0
  29. package/lib/protocols/rollup/create.js +169 -0
  30. package/lib/protocols/rollup/join.js +156 -0
  31. package/lib/protocols/rollup/leave.js +127 -0
  32. package/lib/protocols/rollup/migrate-contract.js +53 -0
  33. package/lib/protocols/rollup/migrate-token.js +66 -0
  34. package/lib/protocols/rollup/pause.js +52 -0
  35. package/lib/protocols/rollup/pipes/ensure-service-fee.js +37 -0
  36. package/lib/protocols/rollup/pipes/ensure-validator.js +10 -0
  37. package/lib/protocols/rollup/pipes/verify-evidence.js +37 -0
  38. package/lib/protocols/rollup/pipes/verify-paused.js +10 -0
  39. package/lib/protocols/rollup/pipes/verify-signers.js +88 -0
  40. package/lib/protocols/rollup/resume.js +52 -0
  41. package/lib/protocols/rollup/update.js +98 -0
  42. package/lib/protocols/token/create.js +150 -0
  43. package/lib/protocols/token/deposit-v2.js +241 -0
  44. package/lib/protocols/token/withdraw-v2.js +255 -0
  45. package/lib/protocols/trade/exchange-v2.js +179 -0
  46. package/lib/protocols/trade/transfer-v2.js +136 -0
  47. package/lib/protocols/trade/transfer-v3.js +241 -0
  48. package/lib/util.js +325 -2
  49. package/package.json +23 -16
  50. package/lib/protocols/misc/poke.js +0 -106
  51. package/lib/protocols/trade/exchange.js +0 -139
  52. package/lib/protocols/trade/transfer.js +0 -101
package/lib/util.js CHANGED
@@ -1,5 +1,328 @@
1
+ const groupBy = require('lodash/groupBy');
2
+ const flattenDeep = require('lodash/flattenDeep');
3
+ const Error = require('@ocap/util/lib/error');
4
+ const { BN } = require('@ocap/util');
1
5
  const { decodeAny } = require('@ocap/message');
6
+ const { toStakeAddress } = require('@arcblock/did-util');
7
+ const cloneDeep = require('lodash/cloneDeep');
2
8
 
3
- const decodeItxData = (encoded) => (encoded ? decodeAny(encoded) : null);
9
+ const ZERO = new BN(0);
10
+ const RATE_BASE = new BN(10000);
4
11
 
5
- module.exports = { decodeItxData };
12
+ const decodeAnyNested = (encoded) => {
13
+ if (!encoded) {
14
+ return encoded;
15
+ }
16
+
17
+ if (Array.isArray(encoded)) {
18
+ return encoded.map((k) => decodeAnyNested(k));
19
+ }
20
+
21
+ if (typeof encoded === 'object') {
22
+ if (encoded.typeUrl && encoded.value) {
23
+ const result = decodeAny(encoded);
24
+ result.value = decodeAnyNested(result.value);
25
+ return result;
26
+ }
27
+
28
+ return Object.keys(encoded).reduce((acc, x) => {
29
+ acc[x] = decodeAnyNested(encoded[x]);
30
+ return acc;
31
+ }, {});
32
+ }
33
+
34
+ return encoded;
35
+ };
36
+
37
+ const decodeAnySafe = (encoded) => {
38
+ if (!encoded) {
39
+ return null;
40
+ }
41
+
42
+ try {
43
+ const decoded = decodeAny(encoded);
44
+ if (decoded.value && typeof decoded.value === 'object') {
45
+ decoded.value = decodeAnyNested(decoded.value);
46
+ }
47
+
48
+ return decoded;
49
+ } catch (err) {
50
+ console.error('decodeAnySafe failed', err);
51
+ return null;
52
+ }
53
+ };
54
+
55
+ const applyTokenUpdates = (tokens, state, operator) => {
56
+ if (['add', 'sub'].includes(operator) === false) {
57
+ throw new Error('UNEXPECTED_OPERATOR', `Invalid operator when applyTokenUpdates: ${operator}`);
58
+ }
59
+
60
+ if (!state) {
61
+ return {};
62
+ }
63
+
64
+ const oldTokens = state.tokens || {};
65
+ const newTokens = cloneDeep(oldTokens);
66
+ for (let i = 0; i < tokens.length; i++) {
67
+ const { address, value } = tokens[i];
68
+ const requirement = new BN(value);
69
+ const balance = new BN(oldTokens[address] || 0);
70
+ const newBalance = balance[operator](requirement);
71
+ if (newBalance.lt(ZERO)) {
72
+ throw new Error('NEGATIVE_TOKEN_BALANCE', `Negative token balance when applyTokenUpdates for ${address}`);
73
+ }
74
+ newTokens[address] = newBalance.toString(10);
75
+ }
76
+
77
+ return {
78
+ tokens: newTokens,
79
+ };
80
+ };
81
+ const applyTokenChange = (state, change) => {
82
+ const delta = typeof change.delta === 'string' ? new BN(change.delta) : change.delta;
83
+ if (delta.gt(ZERO)) {
84
+ return applyTokenUpdates([{ address: change.token, value: delta.toString(10) }], state, 'add');
85
+ }
86
+ return applyTokenUpdates([{ address: change.token, value: delta.abs().toString(10) }], state, 'sub');
87
+ };
88
+
89
+ const fixTokenInput = (input, config) => {
90
+ input.tokensList.forEach((t) => {
91
+ if (!t.address) {
92
+ t.address = config.token.address;
93
+ }
94
+ });
95
+
96
+ return input;
97
+ };
98
+
99
+ const getTxFee = ({ amount, feeRate, maxFee, minFee, stringify = true }) => {
100
+ const userAmount = new BN(amount);
101
+ const maxFeeAmount = new BN(maxFee);
102
+ const minFeeAmount = new BN(minFee);
103
+
104
+ if (feeRate < 0) {
105
+ throw new Error('NEGATIVE_FEE_RATE', 'Unexpected negative feeRate when getTxFee, abort!');
106
+ }
107
+ if (userAmount.lt(ZERO)) {
108
+ throw new Error('NEGATIVE_AMOUNT', 'Unexpected negative amount when getTxFee, abort!');
109
+ }
110
+ if (maxFeeAmount.lt(ZERO)) {
111
+ throw new Error('NEGATIVE_MAX_FEE', 'Unexpected negative maxFee when getTxFee, abort!');
112
+ }
113
+ if (minFeeAmount.lt(ZERO)) {
114
+ throw new Error('NEGATIVE_MIN_FEE', 'Unexpected negative minFee when getTxFee, abort!');
115
+ }
116
+
117
+ // total fee
118
+ let rewardAmount = userAmount.mul(new BN(feeRate)).div(RATE_BASE);
119
+ if (rewardAmount.lt(minFeeAmount)) {
120
+ rewardAmount = minFeeAmount;
121
+ }
122
+ if (rewardAmount.gt(maxFeeAmount)) {
123
+ rewardAmount = maxFeeAmount;
124
+ }
125
+
126
+ // total amount
127
+ const totalAmount = userAmount.add(rewardAmount);
128
+
129
+ if (stringify) {
130
+ return {
131
+ total: totalAmount.toString(10),
132
+ user: userAmount.toString(10),
133
+ reward: rewardAmount.toString(10),
134
+ };
135
+ }
136
+
137
+ return {
138
+ total: totalAmount,
139
+ user: userAmount,
140
+ reward: rewardAmount,
141
+ };
142
+ };
143
+
144
+ const splitTxFee = ({ total, shares = {}, stringify = true }) => {
145
+ const totalAmount = new BN(total);
146
+ if (totalAmount.lt(ZERO)) {
147
+ throw new Error('NEGATIVE_TOTAL_AMOUNT', 'Unexpected negative total when splitTxFee, abort!');
148
+ }
149
+ Object.keys(shares).forEach((key) => {
150
+ if (shares[key] < 0) {
151
+ throw new Error('NEGATIVE_FEE_SHARE', `Unexpected negative shares[${key}] when splitTxFee, abort!`);
152
+ }
153
+ });
154
+
155
+ const rewardShares = Object.keys(shares).reduce((acc, x) => {
156
+ acc[x] = totalAmount.mul(new BN(shares[x])).div(RATE_BASE);
157
+ return acc;
158
+ }, {});
159
+
160
+ return Object.keys(rewardShares).reduce((acc, x) => {
161
+ acc[x] = stringify ? rewardShares[x].toString(10) : rewardShares[x];
162
+ return acc;
163
+ }, {});
164
+ };
165
+
166
+ const getRewardLocker = (rollupAddress) => toStakeAddress(rollupAddress, rollupAddress);
167
+ const getBNSum = (...args) => flattenDeep(args).reduce((sum, x) => sum.add(new BN(x)), new BN(0)).toString(10); // prettier-ignore
168
+ const isFixedFee = (x) => !x.tx.itxJson.maxFee || new BN(x.tx.itxJson.maxFee).isZero();
169
+
170
+ const ensureBlockReward = (rollupState, minReward, txStates) => {
171
+ const { address, withdrawFeeRate, minWithdrawFee, maxWithdrawFee, tokenAddress } = rollupState;
172
+ const locker = getRewardLocker(address);
173
+
174
+ const result = {
175
+ mintedAmount: new BN(0),
176
+ burnedAmount: new BN(0),
177
+ rewardAmount: new BN(0),
178
+ };
179
+
180
+ // 0. ensure reward requirement
181
+ const maxPossibleReward = txStates.reduce(
182
+ (sum, x) => sum.add(new BN(isFixedFee(x) ? x.tx.itxJson.actualFee : x.tx.itxJson.maxFee)),
183
+ new BN(0)
184
+ );
185
+ const minRequiredReward = new BN(minReward);
186
+ if (maxPossibleReward.lt(minRequiredReward)) {
187
+ throw new Error('INVALID_BLOCK', 'Block reward does not match minReward requirement');
188
+ }
189
+
190
+ // 1. find dynamic reward tx
191
+ const dynamicFeeTxs = txStates.filter((x) => isFixedFee(x) === false);
192
+ const totalDynamicFee = dynamicFeeTxs.reduce((sum, x) => sum.add(new BN(x.tx.itxJson.maxFee)), new BN(0));
193
+
194
+ const fixedFeeTxs = txStates.filter((x) => isFixedFee(x));
195
+ const totalFixedFee = fixedFeeTxs.reduce((sum, x) => sum.add(new BN(x.tx.itxJson.actualFee)), new BN(0));
196
+
197
+ const totalMissingFee = minRequiredReward.sub(totalFixedFee);
198
+
199
+ // 2. calculate actual reward for each dynamic reward tx, mark tx to be updated
200
+ const minTxFee = minRequiredReward.div(new BN(txStates.length));
201
+ const changes = { stake: [], account: [] };
202
+ dynamicFeeTxs.forEach((x) => {
203
+ const maxFee = new BN(x.tx.itxJson.maxFee);
204
+ const defaults = getTxFee({
205
+ amount: x.tx.itxJson.token.value,
206
+ feeRate: withdrawFeeRate,
207
+ maxFee: maxWithdrawFee,
208
+ minFee: minWithdrawFee,
209
+ stringify: false,
210
+ });
211
+
212
+ let actualFee = new BN(0);
213
+ // If totalMissingFee is less than 0, then the tx will be charged for fixedFee
214
+ if (totalMissingFee.lt(ZERO)) {
215
+ actualFee = defaults.reward;
216
+ } else {
217
+ // Else the tx is charged for a portion of totalMissingFee
218
+ actualFee = totalMissingFee.mul(maxFee).div(totalDynamicFee);
219
+ }
220
+
221
+ if (actualFee.lt(ZERO)) {
222
+ throw new Error('NEGATIVE_ACTUAL_FEE', 'Got negative actualFee for tx, abort!');
223
+ }
224
+
225
+ // If the actualFee is less than default fee, user will be charged the default fee
226
+ if (actualFee.lt(defaults.reward)) {
227
+ actualFee = defaults.reward;
228
+ }
229
+
230
+ // If the actualFee is less than minTxFee, user will be charged the minTxFee
231
+ if (actualFee.lt(minTxFee)) {
232
+ actualFee = minTxFee;
233
+ }
234
+
235
+ // If the actualFee is less than the maxFee, user will have a refund
236
+ if (actualFee.lt(maxFee)) {
237
+ const refundFee = maxFee.sub(actualFee).toString(10);
238
+ changes.account.push({
239
+ address: x.tx.from,
240
+ delta: refundFee,
241
+ action: 'refund',
242
+ });
243
+ changes.stake.push({
244
+ address: locker,
245
+ delta: `-${refundFee}`,
246
+ action: 'refund',
247
+ });
248
+ }
249
+
250
+ x.tx.itxJson.actualFee = actualFee.toString(10);
251
+ });
252
+
253
+ // 3. bur/mint tokens, update stakes
254
+ txStates.forEach((x) => {
255
+ const user = x.tx.itxJson.token.value;
256
+ const fee = x.tx.itxJson.actualFee;
257
+ const total = getBNSum(user, fee);
258
+
259
+ if (x.type === 'deposit_token_v2') {
260
+ result.rewardAmount = result.rewardAmount.add(new BN(fee));
261
+ result.mintedAmount = result.mintedAmount.add(new BN(total));
262
+
263
+ // mint tokens for deposit proposer
264
+ changes.stake.push({
265
+ address: toStakeAddress(x.tx.itxJson.proposer, address),
266
+ delta: total,
267
+ action: 'mint',
268
+ });
269
+ } else if (x.type === 'withdraw_token_v2') {
270
+ result.rewardAmount = result.rewardAmount.add(new BN(fee));
271
+ result.burnedAmount = result.burnedAmount.add(new BN(user));
272
+
273
+ // burn tokens from locked withdraws: user amount
274
+ changes.stake.push({
275
+ address: toStakeAddress(x.tx.from, address),
276
+ delta: `-${user}`,
277
+ action: 'burn',
278
+ });
279
+ }
280
+ });
281
+
282
+ const grouped = {
283
+ stake: groupBy(changes.stake, 'address'),
284
+ account: groupBy(changes.account, 'address'),
285
+ };
286
+
287
+ result.stakeUpdates = Object.keys(grouped.stake).reduce((acc, x) => {
288
+ acc[x] = {
289
+ address: x,
290
+ token: tokenAddress,
291
+ delta: getBNSum(...grouped.stake[x].map((c) => c.delta)),
292
+ action: grouped.stake[x][grouped.stake[x].length - 1].action,
293
+ };
294
+
295
+ return acc;
296
+ }, {});
297
+
298
+ result.accountUpdates = Object.keys(grouped.account).reduce((acc, x) => {
299
+ acc[x] = {
300
+ address: x,
301
+ token: tokenAddress,
302
+ delta: getBNSum(...grouped.account[x].map((c) => c.delta)),
303
+ action: grouped.account[x][grouped.account[x].length - 1].action,
304
+ };
305
+
306
+ return acc;
307
+ }, {});
308
+
309
+ result.mintedAmount = result.mintedAmount.toString(10);
310
+ result.burnedAmount = result.burnedAmount.toString(10);
311
+ result.rewardAmount = result.rewardAmount.toString(10);
312
+
313
+ return result;
314
+ };
315
+
316
+ module.exports = {
317
+ decodeAnySafe,
318
+ applyTokenUpdates,
319
+ applyTokenChange,
320
+ fixTokenInput,
321
+ getTxFee,
322
+ splitTxFee,
323
+ getRewardLocker,
324
+ ensureBlockReward,
325
+ getBNSum,
326
+ isFixedFee,
327
+ RATE_BASE,
328
+ };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.6.3",
6
+ "version": "1.6.10",
7
7
  "description": "Predefined tx pipeline sets to execute certain type of transactions",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -12,32 +12,39 @@
12
12
  "scripts": {
13
13
  "lint": "eslint tests lib",
14
14
  "lint:fix": "eslint --fix tests lib",
15
- "test": "node tools/jest.js",
16
- "coverage": "npm run test -- --coverage"
15
+ "start": "node tools/start-chain.js",
16
+ "test": "jest --forceExit --detectOpenHandles",
17
+ "test:ci": "jest --forceExit --detectOpenHandles --coverage",
18
+ "coverage": "start-server-and-test start http://127.0.0.1:4001 test:ci"
17
19
  },
18
20
  "keywords": [],
19
21
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
20
22
  "license": "MIT",
21
23
  "dependencies": {
22
- "@arcblock/did": "^1.6.3",
23
- "@arcblock/did-util": "^1.6.3",
24
- "@ocap/mcrypto": "^1.6.3",
25
- "@ocap/message": "^1.6.3",
26
- "@ocap/sdk": "^1.6.3",
27
- "@ocap/state": "^1.6.3",
28
- "@ocap/tx-pipeline": "^1.6.3",
29
- "@ocap/util": "^1.6.3",
30
- "@ocap/wallet": "^1.6.3",
31
- "debug": "^4.3.1",
24
+ "@arcblock/did": "1.6.10",
25
+ "@arcblock/did-util": "1.6.10",
26
+ "@arcblock/validator": "1.6.10",
27
+ "@ocap/asset": "1.6.10",
28
+ "@ocap/mcrypto": "1.6.10",
29
+ "@ocap/merkle-tree": "1.6.10",
30
+ "@ocap/message": "1.6.10",
31
+ "@ocap/state": "1.6.10",
32
+ "@ocap/tx-pipeline": "1.6.10",
33
+ "@ocap/util": "1.6.10",
34
+ "@ocap/wallet": "1.6.10",
35
+ "debug": "^4.3.3",
36
+ "deep-diff": "^1.0.2",
32
37
  "empty-value": "^1.0.1",
33
- "lodash": "^4.17.21"
38
+ "lodash": "^4.17.21",
39
+ "url-join": "^4.0.1"
34
40
  },
35
41
  "resolutions": {
36
42
  "bn.js": "5.1.3",
37
43
  "elliptic": "6.5.3"
38
44
  },
39
45
  "devDependencies": {
40
- "jest": "^26.6.3"
46
+ "jest": "^27.3.1",
47
+ "start-server-and-test": "^1.14.0"
41
48
  },
42
- "gitHead": "5246168dbbc268a3e6f95c3992d9d343647ca25b"
49
+ "gitHead": "ab272e8db3a15c6571cc7fae7cc3d3e0fdd4bdb1"
43
50
  }
@@ -1,106 +0,0 @@
1
- const get = require('lodash/get');
2
- const padStart = require('lodash/padStart');
3
- const { account } = require('@ocap/state');
4
- const { Runner, Error, pipes } = require('@ocap/tx-pipeline');
5
- const { toBN, fromTokenToUnit } = require('@ocap/util');
6
-
7
- // eslint-disable-next-line global-require
8
- const debug = require('debug')(`${require('../../../package.json').name}:declare`);
9
-
10
- const runner = new Runner();
11
-
12
- runner.use(pipes.VerifyMultiSig(0));
13
-
14
- // Verify itx
15
- runner.use(
16
- pipes.VerifyInfo([
17
- {
18
- error: 'FORBIDDEN',
19
- message: 'Poke is not enabled for this ledger',
20
- fn: ({ config }) => get(config, 'transaction.poke.enabled') === true,
21
- },
22
- {
23
- error: 'INVALID_NONCE',
24
- message: 'Poke transaction must set nonce to 0',
25
- fn: ({ tx }) => tx.nonce === 0,
26
- },
27
- {
28
- error: 'INSUFFICIENT_DATA',
29
- message: 'itx.date and itx.address must be set',
30
- fn: ({ itx }) => itx.date && itx.address,
31
- },
32
- {
33
- error: 'INVALID_TX',
34
- message: 'Poke config invalid or receiver address invalid',
35
- fn: ({ itx, config }) => {
36
- const pokeAccount = config.accounts.find((x) => !x.pk);
37
- return pokeAccount && itx.address === pokeAccount.address;
38
- },
39
- },
40
- {
41
- error: 'INVALID_TX',
42
- message: 'Poke date must be set to current date',
43
- fn: ({ itx }) => {
44
- const now = new Date();
45
- const year = now.getUTCFullYear();
46
- const month = now.getUTCMonth() + 1;
47
- const day = now.getUTCDate();
48
- const date = `${year}-${padStart(month, 2, '0')}-${padStart(day, 2, '0')}`;
49
-
50
- return itx.date === date;
51
- },
52
- },
53
- ])
54
- );
55
-
56
- // Ensure sender does not exist
57
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState' }));
58
- runner.use(pipes.VerifySender({ state: 'senderState' }));
59
- runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
60
-
61
- // The receiver must exist in the ledger
62
- runner.use(pipes.ExtractState({ from: 'itx.address', to: 'receiverState', status: 'INVALID_RECEIVER_STATE' }));
63
-
64
- // Create account state
65
- runner.use(async (context, next) => {
66
- const { itx, statedb, config, senderState, receiverState } = context;
67
-
68
- const decimal = get(config, 'token.decimal');
69
- const amount = get(config, 'transaction.poke.amount');
70
- const delta = fromTokenToUnit(amount, decimal);
71
-
72
- if (senderState.poke && senderState.poke.date === itx.date) {
73
- return next(new Error('INVALID_TX', `Sender already poked on ${itx.date}`));
74
- }
75
-
76
- const senderBalance = toBN(senderState.balance);
77
- const receiverBalance = toBN(receiverState.balance);
78
- if (delta.gt(receiverBalance)) {
79
- return next(new Error('INSUFFICIENT_FUND', 'Receiver does not have enough balance for poke'));
80
- }
81
-
82
- const newSenderState = account.update(
83
- senderState,
84
- {
85
- balance: senderBalance.add(delta).toString(),
86
- poke: { date: itx.date },
87
- },
88
- context
89
- );
90
- const newReceiverState = account.update(receiverState, { balance: receiverBalance.sub(delta).toString() }, context);
91
-
92
- context.senderState = newSenderState;
93
- context.receiverState = newReceiverState;
94
-
95
- debug('sender', newSenderState);
96
- debug('receiver', newReceiverState);
97
-
98
- await Promise.all([
99
- statedb.account.update(senderState.address, newSenderState, context),
100
- statedb.account.update(receiverState.address, newReceiverState, context),
101
- ]);
102
-
103
- return next();
104
- });
105
-
106
- module.exports = runner;
@@ -1,139 +0,0 @@
1
- const get = require('lodash/get');
2
- const isEmpty = require('empty-value');
3
- const getListField = require('@ocap/util/lib/get-list-field');
4
- const { decodeBigInt } = require('@ocap/message');
5
- const { fromUnitToToken, toBN } = require('@ocap/util');
6
- const { Runner, pipes } = require('@ocap/tx-pipeline');
7
- const { account } = require('@ocap/state');
8
-
9
- // eslint-disable-next-line global-require
10
- const debug = require('debug')(`${require('../../../package.json').name}:exchange`);
11
-
12
- const runner = new Runner();
13
-
14
- runner.use(pipes.VerifyMultiSig(1));
15
- runner.use(pipes.ExtractReceiver({ from: ['itx.to', 'tx.signaturesList', 'tx.signatures'] }));
16
-
17
- runner.use(
18
- pipes.VerifyInfo([
19
- {
20
- error: 'INSUFFICIENT_DATA',
21
- fn: ({ itx }) => itx.sender && itx.receiver,
22
- },
23
- {
24
- error: 'INVALID_TX',
25
- message: 'Can not exchange without any token or assets',
26
- fn: (context) => {
27
- const { itx, config } = context;
28
- const decimal = get(config, 'token.decimal');
29
-
30
- context.senderAssets = getListField(itx, 'sender.assets');
31
- context.receiverAssets = getListField(itx, 'receiver.assets');
32
- context.senderAmount = itx.sender.value ? decodeBigInt(itx.sender.value) : 0;
33
- context.receiverAmount = itx.receiver.value ? decodeBigInt(itx.receiver.value) : 0;
34
-
35
- if (Number(fromUnitToToken(context.senderAmount, decimal)) <= 0 && isEmpty(context.senderAssets)) {
36
- return false;
37
- }
38
- if (Number(fromUnitToToken(context.receiverAmount, decimal)) <= 0 && isEmpty(context.receiverAssets)) {
39
- return false;
40
- }
41
-
42
- return true;
43
- },
44
- },
45
- ])
46
- );
47
-
48
- // TODO: verify-expiration
49
-
50
- runner.use(pipes.VerifySigner({ signer: 'itx.to' }));
51
- runner.use(pipes.VerifyItxSize({ value: ['senderAssets', 'receiverAssets'] }));
52
-
53
- // TODO: anti-replay-exchange-attack
54
-
55
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
56
- runner.use(pipes.VerifyBalance({ state: 'senderState', value: 'itx.sender.value' }));
57
-
58
- runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK' }));
59
- runner.use(pipes.VerifyDelegation({ type: 'signature', signerKey: 'senderState', delegatorKey: 'delegatorState' }));
60
- runner.use(pipes.ExtractSigner({ signerKey: 'signerStates', delegatorKey: 'delegatorStates' }));
61
- runner.use(
62
- pipes.VerifyDelegation({
63
- type: 'multisig',
64
- signerKey: 'signerStates',
65
- delegatorKey: 'delegatorStates',
66
- delegationKey: 'delegationStates',
67
- })
68
- );
69
-
70
- runner.use(pipes.ExtractState({ from: 'receiver', to: 'receiverState', status: 'INVALID_RECEIVER_STATE' }));
71
- runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState', signerKey: 'signerStates' }));
72
- runner.use(pipes.VerifyBalance({ state: 'receiverState', value: 'itx.receiver.value' }));
73
-
74
- runner.use(pipes.AntiLandAttack({ senderState: 'senderState', receiverState: 'receiverState' }));
75
-
76
- runner.use(pipes.ExtractState({ from: 'senderAssets', to: 'priv.senderAssets', status: 'INVALID_ASSET' }));
77
- runner.use(pipes.VerifyTransferrable({ assets: 'priv.senderAssets' }));
78
- runner.use(pipes.VerifyUpdater({ assetKey: 'priv.senderAssets', ownerKey: 'senderState' }));
79
-
80
- runner.use(pipes.ExtractState({ from: 'receiverAssets', to: 'priv.receiverAssets', status: 'INVALID_ASSET' }));
81
- runner.use(pipes.VerifyTransferrable({ assets: 'priv.receiverAssets' }));
82
- runner.use(pipes.VerifyUpdater({ assetKey: 'priv.receiverAssets', ownerKey: 'receiverState' }));
83
-
84
- runner.use(pipes.UpdateOwner({ assets: 'priv.senderAssets', owner: 'receiverState' }));
85
- runner.use(pipes.UpdateOwner({ assets: 'priv.receiverAssets', owner: 'senderState' }));
86
-
87
- runner.use(async (context, next) => {
88
- const {
89
- tx,
90
- itx,
91
- senderAssets,
92
- receiverAssets,
93
- senderAmount,
94
- receiverAmount,
95
- senderState,
96
- receiverState,
97
- statedb,
98
- } = context;
99
-
100
- const senderDelta = toBN(senderAmount);
101
- const receiverDelta = toBN(receiverAmount);
102
-
103
- // Update sender state
104
- const newSenderState = account.update(
105
- senderState,
106
- {
107
- balance: toBN(senderState.balance).sub(senderDelta).add(receiverDelta).toString(),
108
- nonce: tx.nonce,
109
- numAssets: Math.max(0, senderState.numAssets - senderAssets.length + receiverAssets.length),
110
- },
111
- context
112
- );
113
- await statedb.account.update(senderState.address, newSenderState, context);
114
-
115
- // Update receiver state
116
- const newReceiverState = account.update(
117
- receiverState,
118
- {
119
- balance: toBN(receiverState.balance).add(senderDelta).sub(receiverDelta).toString(),
120
- numAssets: Math.max(0, receiverState.numAssets + senderAssets.length - receiverAssets.length),
121
- },
122
- context
123
- );
124
- await statedb.account.update(receiverState.address, newReceiverState, context);
125
-
126
- context.senderState = newSenderState;
127
- context.receiverState = newReceiverState;
128
-
129
- debug('exchange', {
130
- from: tx.from,
131
- to: itx.to,
132
- sender: { assets: senderAssets.length, token: senderAmount },
133
- receiver: { assets: receiverAssets.length, token: receiverAmount },
134
- });
135
-
136
- next();
137
- });
138
-
139
- module.exports = runner;