@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
@@ -1,73 +1,105 @@
1
+ const deepDiff = require('deep-diff');
2
+ const Error = require('@ocap/util/lib/error');
3
+ const Joi = require('@arcblock/validator');
1
4
  const { Runner, pipes } = require('@ocap/tx-pipeline');
2
5
  const { account, asset } = require('@ocap/state');
3
6
 
4
7
  // eslint-disable-next-line global-require
5
8
  const debug = require('debug')(`${require('../../../package.json').name}:update-asset`);
6
9
 
7
- const { decodeItxData } = require('../../util');
10
+ const { decodeAnySafe } = require('../../util');
8
11
 
9
12
  const runner = new Runner();
10
13
 
11
14
  runner.use(pipes.VerifyMultiSig(0));
12
15
 
13
- runner.use(
14
- pipes.VerifyInfo([
15
- {
16
- error: 'INSUFFICIENT_DATA',
17
- message: 'itx.data or itx.address must not be empty',
18
- fn: ({ itx }) => itx.address && itx.moniker && itx.data,
19
- },
20
- ])
21
- );
16
+ // Verify itx
17
+ const schema = Joi.object({
18
+ address: Joi.DID().role('ROLE_ASSET').required(),
19
+ moniker: Joi.string().min(2).max(255).required(),
20
+ data: Joi.any().optional(),
21
+ }).options({ stripUnknown: true, noDefaults: false });
22
+ runner.use((context, next) => {
23
+ const { itx } = context;
24
+ const { error } = schema.validate(itx);
25
+ if (error) {
26
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
27
+ }
28
+
29
+ context.newData = decodeAnySafe(itx.data);
30
+ return next();
31
+ });
32
+
33
+ // Ensure asset exist
34
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'assetState', status: 'INVALID_ASSET', table: 'asset' }));
35
+ runner.use(pipes.ExtractState({ from: 'assetState.issuer', to: 'issuerState', status: 'OK', table: 'account' }));
22
36
 
23
37
  // Ensure sender exist
24
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
38
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
25
39
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
26
40
 
27
- // Ensure asset exist owned by sender and can be modified
28
- runner.use(pipes.ExtractState({ from: 'itx.address', to: 'assetState', status: 'INVALID_ASSET' }));
41
+ // Ensure asset owned by sender and can be modified
29
42
  runner.use(pipes.VerifyUpdater({ assetKey: 'assetState', ownerKey: 'senderState', updaterKey: ['owner', 'issuer'] }));
30
43
  runner.use(
31
44
  pipes.VerifyInfo([
32
45
  {
33
46
  error: 'READONLY_ASSET',
34
- message: 'Asset is readonly',
47
+ message: 'Can not update a readonly asset',
35
48
  fn: ({ assetState }) => assetState.readonly === false,
49
+ persist: true,
36
50
  },
37
51
  ])
38
52
  );
39
53
 
40
- // Update asset state
54
+ // Ensure we are in append-only mode update proposed by issuer
41
55
  runner.use(async (context, next) => {
42
- const { tx, itx, statedb, senderState, assetState } = context;
43
- const data = decodeItxData(itx.data);
44
-
45
- // Update owner state
46
- const ownerState = await statedb.account.update(
47
- senderState.address,
48
- account.update(senderState, { nonce: tx.nonce }, context),
49
- context
50
- );
51
-
52
- const newAssetState = await statedb.asset.update(
53
- itx.address,
54
- asset.update(
55
- assetState,
56
- {
57
- moniker: itx.moniker,
58
- data,
59
- },
60
- context
61
- ),
62
- context
63
- );
64
-
65
- context.senderState = ownerState;
66
- context.assetState = newAssetState;
67
-
68
- debug('update', newAssetState);
69
-
70
- next();
56
+ const { itx, newData, senderState, assetState, issuerState } = context;
57
+ if (issuerState && senderState.address === issuerState.address) {
58
+ if (itx.moniker !== assetState.moniker) {
59
+ return next(new Error('FORBIDDEN', 'Asset moniker can only be updated by owner'));
60
+ }
61
+ if (newData.type !== assetState.data.type) {
62
+ return next(new Error('FORBIDDEN', 'Asset data type can only be updated by owner'));
63
+ }
64
+ const dataDiff = deepDiff(assetState.data.value, newData.value);
65
+ const appendOnly = dataDiff.every((x) => x.kind === 'N' || (x.kind === 'A' && x.item.kind === 'N'));
66
+ if (appendOnly === false) {
67
+ return next(new Error('APPEND_ONLY', 'Asset data value can only be updated in append only mode'));
68
+ }
69
+ }
70
+
71
+ return next();
71
72
  });
72
73
 
74
+ // Update asset state
75
+ runner.use(
76
+ async (context, next) => {
77
+ const { tx, itx, newData, statedb, senderState, assetState } = context;
78
+
79
+ const [newSenderState, newAssetState] = await Promise.all([
80
+ // update owner state
81
+ statedb.account.update(
82
+ senderState.address,
83
+ account.update(senderState, { nonce: tx.nonce, pk: tx.pk }, context),
84
+ context
85
+ ),
86
+
87
+ // update asset state
88
+ statedb.asset.update(
89
+ itx.address,
90
+ asset.update(assetState, { moniker: itx.moniker, data: newData }, context),
91
+ context
92
+ ),
93
+ ]);
94
+
95
+ context.senderState = newSenderState;
96
+ context.assetState = newAssetState;
97
+
98
+ debug('update', newAssetState);
99
+
100
+ next();
101
+ },
102
+ { persistError: true }
103
+ );
104
+
73
105
  module.exports = runner;
@@ -0,0 +1,146 @@
1
+ /* eslint-disable indent */
2
+ const get = require('lodash/get');
3
+ const isEmpty = require('empty-value');
4
+ const cloneDeep = require('lodash/cloneDeep');
5
+ const Error = require('@ocap/util/lib/error');
6
+ const { isValidFactory } = require('@ocap/asset');
7
+ const { formatMessage } = require('@ocap/message');
8
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
9
+ const { BN } = require('@ocap/util');
10
+ const { account, factory } = require('@ocap/state');
11
+ const { toFactoryAddress } = require('@arcblock/did-util');
12
+
13
+ // eslint-disable-next-line global-require
14
+ const debug = require('debug')(`${require('../../../package.json').name}:create-factory`);
15
+
16
+ const { decodeAnySafe } = require('../../util');
17
+ const ensureServiceFee = require('../rollup/pipes/ensure-service-fee');
18
+
19
+ const runner = new Runner();
20
+
21
+ runner.use(pipes.VerifyMultiSig(0));
22
+
23
+ // validate factory props
24
+ runner.use((context, next) => {
25
+ const factoryProps = formatMessage('CreateFactoryTx', context.itx);
26
+
27
+ try {
28
+ isValidFactory(factoryProps);
29
+ } catch (err) {
30
+ return next(new Error('INVALID_FACTORY_PROPS', err.message));
31
+ }
32
+
33
+ if (factoryProps.output.data) {
34
+ factoryProps.output.data = decodeAnySafe(factoryProps.output.data);
35
+ }
36
+
37
+ if (factoryProps.name.length < 2 || factoryProps.name.length > 255) {
38
+ return next(new Error('INVALID_FACTORY_PROPS', 'Length of factory name should between 2 and 255 characters'));
39
+ }
40
+
41
+ const props = cloneDeep(factoryProps);
42
+ props.address = '';
43
+ if (toFactoryAddress(props) !== factoryProps.address) {
44
+ return next(new Error('INVALID_FACTORY_PROPS', 'Factory address is not valid'));
45
+ }
46
+
47
+ context.factoryTokens = (factoryProps.input.tokens || []).map((x) => x.address);
48
+
49
+ // ensure max tokens length
50
+ const maxListSize = get(context, 'config.transaction.maxListSize');
51
+ if (context.factoryTokens.length > maxListSize) {
52
+ return next(new Error('INVALID_FACTORY_INPUT', `input.tokens exceeded max allowed length: ${maxListSize}`));
53
+ }
54
+ if (factoryProps.input.assets.length > maxListSize) {
55
+ return next(new Error('INVALID_FACTORY_INPUT', `input.assets exceeded max allowed length: ${maxListSize}`));
56
+ }
57
+
58
+ // For backwards compatibility, merge tokens
59
+ if (new BN(factoryProps.input.value || 0).gt(new BN(0))) {
60
+ context.factoryTokens.push(context.config.token.address);
61
+ factoryProps.input.tokens.push({ address: context.config.token.address, value: factoryProps.input.value });
62
+ factoryProps.input.value = '0';
63
+ }
64
+
65
+ context.factoryProps = factoryProps;
66
+
67
+ return next();
68
+ });
69
+
70
+ // Ensure factory not exist
71
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'factoryState', status: 'OK' }));
72
+ runner.use(
73
+ pipes.VerifyInfo([
74
+ {
75
+ error: 'DUPLICATE_FACTORY',
76
+ message: 'This asset factory already exist on chain',
77
+ fn: (context) => isEmpty(context.factoryState),
78
+ },
79
+ ])
80
+ );
81
+
82
+ // Ensure sender exist
83
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', table: 'account', status: 'INVALID_SENDER_STATE' })); // prettier-ignore
84
+ runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
85
+
86
+ // Ensure delegation
87
+ runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK', table: 'account' }));
88
+ runner.use(pipes.VerifyDelegation({ type: 'signature', signerKey: 'senderState', delegatorKey: 'delegatorState' }));
89
+
90
+ // Ensure tokens exist if we are creating an factory that consume tokens
91
+ runner.use(pipes.ExtractState({ from: 'factoryTokens', to: 'tokenStates', table: 'token', status: 'INVALID_FACTORY_INPUT' })); // prettier-ignore
92
+
93
+ // ensure input.assets all exist on chain
94
+ runner.use(pipes.ExtractState({ from: 'factoryProps.input.assets', to: 'inputAssetStates', table: 'asset', status: 'OK' })); // prettier-ignore
95
+ runner.use(pipes.ExtractState({ from: 'factoryProps.input.assets', to: 'inputFactoryStates', table: 'factory', status: 'OK' })); // prettier-ignore
96
+ runner.use((context, next) => {
97
+ const { inputAssetStates = [], inputFactoryStates = [], factoryProps } = context;
98
+ if (inputAssetStates.some((x) => !!x.consumedTime)) {
99
+ return next(new Error('INVALID_FACTORY_INPUT', 'Some of input.assets already consumed'));
100
+ }
101
+
102
+ if (inputAssetStates.length + inputFactoryStates.length === factoryProps.input.assets.length) {
103
+ return next();
104
+ }
105
+
106
+ return next(new Error('INVALID_FACTORY_INPUT', 'Not all input.assets exist on chain'));
107
+ });
108
+
109
+ runner.use(ensureServiceFee);
110
+
111
+ // Create factory state
112
+ runner.use(
113
+ async (context, next) => {
114
+ const { tx, itx, statedb, senderState, delegatorState, senderUpdates, vaultState, vaultUpdates, factoryProps } =
115
+ context;
116
+ const tokens = { [context.config.token.address]: '0' };
117
+ const owner = delegatorState ? delegatorState.address : senderState.address;
118
+
119
+ const [newSenderState, factoryState, newVaultState] = await Promise.all([
120
+ // Update owner state
121
+ statedb.account.update(
122
+ senderState.address,
123
+ account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
124
+ context
125
+ ),
126
+
127
+ // Create factory state
128
+ statedb.factory.create(itx.address, factory.create({ ...factoryProps, tokens, owner }, context), context),
129
+
130
+ isEmpty(vaultUpdates)
131
+ ? vaultState
132
+ : statedb.account.update(vaultState.address, account.update(vaultState, vaultUpdates, context), context),
133
+ ]);
134
+
135
+ context.senderState = newSenderState;
136
+ context.factoryState = factoryState;
137
+ context.vaultState = newVaultState;
138
+
139
+ debug('createFactory', factoryState);
140
+
141
+ next();
142
+ },
143
+ { persistError: true }
144
+ );
145
+
146
+ module.exports = runner;
@@ -0,0 +1,219 @@
1
+ const Error = require('@ocap/util/lib/error');
2
+ const Joi = require('@arcblock/validator');
3
+ const { BN } = require('@ocap/util');
4
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
5
+ const { account, asset, stake, evidence } = require('@ocap/state');
6
+ const getRelatedAddresses = require('@ocap/util/lib/get-related-addr');
7
+
8
+ // eslint-disable-next-line global-require
9
+ const debug = require('debug')(`${require('../../../package.json').name}:acquire-asset-v2`);
10
+
11
+ const { applyTokenUpdates } = require('../../util');
12
+
13
+ const runner = new Runner();
14
+
15
+ runner.use(pipes.VerifyMultiSig(0));
16
+
17
+ // 1. verify itx output
18
+ const schema = Joi.object({
19
+ address: Joi.DID().role('ROLE_STAKE').required(),
20
+ evidence: Joi.object({
21
+ hash: Joi.string().regex(Joi.patterns.txHash).required(),
22
+ }).required(),
23
+ data: Joi.any().optional(),
24
+ }).options({ stripUnknown: true, noDefaults: false });
25
+ runner.use(({ itx }, next) => {
26
+ const { error } = schema.validate(itx);
27
+ return next(error ? new Error('INVALID_TX', `Invalid itx: ${error.message}`) : null);
28
+ });
29
+
30
+ // 2. verify stake state & sender state
31
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
32
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
33
+ runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
34
+ runner.use(
35
+ pipes.VerifyInfo([
36
+ {
37
+ error: 'SENDER_NOT_MATCH',
38
+ message: 'You are not allowed to claim stake from this address',
39
+ fn: ({ senderState, stakeState }) => getRelatedAddresses(senderState).includes(stakeState.sender),
40
+ },
41
+ ])
42
+ );
43
+
44
+ // 3. verify evidence replay
45
+ runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'evidenceState', status: 'OK', table: 'evidence' }));
46
+ runner.use(
47
+ pipes.VerifyInfo([
48
+ {
49
+ error: 'ALREADY_CLAIMED',
50
+ message: 'Revoke evidence already seen on this chain',
51
+ fn: ({ evidenceState }) => !evidenceState,
52
+ },
53
+ ])
54
+ );
55
+
56
+ // 4. verify evidence tx
57
+ runner.use(pipes.ExtractState({ from: 'itx.evidence.hash', to: 'txState', status: 'INVALID_TX', table: 'tx' }));
58
+ runner.use(
59
+ pipes.VerifyInfo([
60
+ {
61
+ error: 'INVALID_TX',
62
+ message: 'Evidence tx is not valid',
63
+ fn: ({ txState }) => txState.code === 'OK',
64
+ },
65
+ {
66
+ error: 'INVALID_TX',
67
+ message: 'Evidence tx type is not valid',
68
+ fn: ({ txState }) => txState.type === 'revoke_stake',
69
+ },
70
+ {
71
+ error: 'INVALID_TX',
72
+ message: 'Evidence tx does not belong to same stake',
73
+ fn: ({ txState, itx }) => txState.tx.itxJson.address === itx.address,
74
+ },
75
+ {
76
+ error: 'WITHIN_WAITING_PERIOD',
77
+ message: 'You can not claim stake before waiting period ends',
78
+ fn: ({ txState, stakeState, txTime }) => {
79
+ const end = +new Date(txState.time) + (stakeState.revokeWaitingPeriod || 0) * 1000;
80
+ const now = +new Date(txTime);
81
+ return now > end;
82
+ },
83
+ },
84
+ ])
85
+ );
86
+
87
+ // 5. assemble output
88
+ runner.use(
89
+ pipes.VerifyTxInput({
90
+ fieldKey: 'txState.tx.itxJson.outputs',
91
+ inputsKey: 'outputs',
92
+ sendersKey: 'receivers',
93
+ tokensKey: 'tokens',
94
+ assetsKey: 'assets',
95
+ })
96
+ );
97
+
98
+ // 6. verify receiver states
99
+ runner.use(pipes.ExtractState({ from: 'receivers', to: 'receiverStates', status: 'INVALID_RECEIVER_STATE', table: 'account' })); // prettier-ignore
100
+
101
+ // 7. verify token state and balance
102
+ runner.use(pipes.ExtractState({ from: 'tokens', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
103
+ runner.use((context, next) => {
104
+ const { outputs, stakeState } = context;
105
+ const tokensToClaim = {};
106
+ outputs.forEach(({ tokens }) => {
107
+ tokens.forEach(({ address, value }) => {
108
+ if (typeof tokensToClaim[address] === 'undefined') {
109
+ tokensToClaim[address] = new BN(0);
110
+ }
111
+ tokensToClaim[address] = tokensToClaim[address].add(new BN(value));
112
+ });
113
+ });
114
+
115
+ context.tokenCondition = {
116
+ owner: stakeState.address,
117
+ tokens: Object.keys(tokensToClaim).map((address) => ({ address, value: tokensToClaim[address] })),
118
+ };
119
+
120
+ next();
121
+ });
122
+ runner.use(
123
+ pipes.VerifyTokenBalance({
124
+ ownerKey: 'stakeState',
125
+ conditionKey: 'tokenCondition',
126
+ tokensKey: 'revokedTokens',
127
+ })
128
+ );
129
+
130
+ // 7. verify asset state and ownership
131
+ runner.use(pipes.ExtractState({ from: 'assets', to: 'assetStates', status: 'OK', table: 'asset' }));
132
+ runner.use(pipes.VerifyTransferrable({ assets: 'assetStates' }));
133
+ runner.use(pipes.VerifyUpdater({ assetKey: 'assetStates', ownerKey: 'stakeState' }));
134
+
135
+ // 8. update statedb
136
+ runner.use(
137
+ async (context, next) => {
138
+ const { tx, itx, outputs, statedb, senderState, stakeState, receiverStates, assetStates = [] } = context;
139
+
140
+ const receiverUpdates = {};
141
+ const assetUpdates = {};
142
+ const stakeUpdates = {
143
+ revokedTokens: stakeState.revokedTokens || {},
144
+ revokedAssets: stakeState.revokedAssets || [],
145
+ };
146
+
147
+ outputs.forEach((x) => {
148
+ const { owner, tokens, assets } = x;
149
+
150
+ // Update receiver balance
151
+ const ownerState = receiverStates.find((s) => getRelatedAddresses(s).includes(owner));
152
+ receiverUpdates[ownerState.address] = applyTokenUpdates(tokens, ownerState, 'add');
153
+
154
+ // Update asset owner
155
+ assets.forEach((a) => {
156
+ assetUpdates[a] = { owner: ownerState.address };
157
+ });
158
+
159
+ // Update Stake Revoked fields
160
+ const revoked = applyTokenUpdates(tokens, { tokens: stakeUpdates.revokedTokens }, 'sub');
161
+ stakeUpdates.revokedTokens = revoked.tokens;
162
+ stakeUpdates.revokedAssets = stakeUpdates.revokedAssets.filter((a) => assets.includes(a) === false);
163
+ });
164
+
165
+ const isAlsoSigner = !!receiverUpdates[senderState.address];
166
+
167
+ const [newSenderState, newReceiverStates, newStakeState, newAssetStates, evidenceState] = await Promise.all([
168
+ // Update sender state
169
+ statedb.account.update(
170
+ senderState.address,
171
+ account.update(
172
+ senderState,
173
+ Object.assign({ nonce: tx.nonce }, receiverUpdates[senderState.address] || {}),
174
+ context
175
+ ),
176
+ context
177
+ ),
178
+
179
+ // Update receiver states
180
+ Promise.all(
181
+ receiverStates
182
+ .filter((x) => x.address !== senderState.address)
183
+ .map((x) =>
184
+ statedb.account.update(x.address, account.update(x, receiverUpdates[x.address], context), context)
185
+ )
186
+ ),
187
+
188
+ // Update stake state
189
+ statedb.stake.update(stakeState.address, stake.update(stakeState, stakeUpdates, context), context),
190
+
191
+ // Transfer assets to output account
192
+ Promise.all(
193
+ assetStates.map((x) =>
194
+ statedb.asset.update(x.address, asset.update(x, assetUpdates[x.address], context), context)
195
+ )
196
+ ),
197
+
198
+ // Create evidence state
199
+ statedb.evidence.create(
200
+ itx.evidence.hash,
201
+ evidence.create({ hash: itx.evidence.hash, data: 'claim-stake' }, context),
202
+ context
203
+ ),
204
+ ]);
205
+
206
+ context.senderState = newSenderState;
207
+ context.receiverStates = isAlsoSigner ? newReceiverStates.concat(newSenderState) : newReceiverStates;
208
+ context.stakeState = newStakeState;
209
+ context.assetStates = newAssetStates;
210
+ context.evidenceState = evidenceState;
211
+
212
+ debug('claim-stake', { address: itx.address, stakeUpdates, receiverUpdates, assetUpdates });
213
+
214
+ next();
215
+ },
216
+ { persistError: true }
217
+ );
218
+
219
+ module.exports = runner;
@@ -0,0 +1,136 @@
1
+ const isEmpty = require('empty-value');
2
+ const { BN } = require('@ocap/util');
3
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
4
+ const { account, stake } = require('@ocap/state');
5
+ const getRelatedAddresses = require('@ocap/util/lib/get-related-addr');
6
+
7
+ // eslint-disable-next-line global-require
8
+ const debug = require('debug')(`${require('../../../package.json').name}:acquire-asset-v2`);
9
+
10
+ const { applyTokenUpdates } = require('../../util');
11
+
12
+ const runner = new Runner();
13
+
14
+ runner.use(pipes.VerifyMultiSig(0));
15
+
16
+ // 1. verify itx output
17
+ runner.use(
18
+ pipes.VerifyTxInput({
19
+ fieldKey: 'itx.outputs',
20
+ inputsKey: 'outputs',
21
+ sendersKey: 'receivers',
22
+ tokensKey: 'tokens',
23
+ assetsKey: 'assets',
24
+ })
25
+ );
26
+
27
+ // 2. verify itx address
28
+ runner.use(
29
+ pipes.VerifyInfo([
30
+ {
31
+ error: 'INSUFFICIENT_DATA',
32
+ message: 'Can not revoke stake without stake address',
33
+ fn: ({ itx }) => itx.address,
34
+ },
35
+ {
36
+ error: 'INSUFFICIENT_DATA',
37
+ message: 'Can not revoke stake without any output',
38
+ fn: ({ assets, tokens }) => !(isEmpty(tokens) && isEmpty(assets)),
39
+ },
40
+ ])
41
+ );
42
+
43
+ // 3. verify itx size: set hard limit here because more output leads to longer tx execute time
44
+ runner.use(pipes.VerifyListSize({ listKey: ['outputs', 'receivers', 'tokens', 'assets'] }));
45
+
46
+ // 4. verify sender & receiver
47
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
48
+ runner.use(pipes.ExtractState({ from: 'receivers', to: 'receiverStates', status: 'INVALID_RECEIVER_STATE', table: 'account' })); // prettier-ignore
49
+ runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
50
+
51
+ // 5. verify stake state
52
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })); // prettier-ignore
53
+ runner.use(
54
+ pipes.VerifyInfo([
55
+ {
56
+ error: 'SENDER_NOT_MATCH',
57
+ message: 'You are not allowed to revoke stake from this address',
58
+ fn: ({ senderState, stakeState }) => getRelatedAddresses(senderState).includes(stakeState.sender),
59
+ },
60
+ {
61
+ error: 'STAKE_NOT_REVOCABLE',
62
+ message: 'This stake address is not revocable',
63
+ fn: ({ stakeState }) => stakeState.revocable,
64
+ },
65
+ ])
66
+ );
67
+
68
+ // 6. verify token state and balance
69
+ runner.use(pipes.ExtractState({ from: 'tokens', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
70
+ runner.use((context, next) => {
71
+ const { outputs, stakeState } = context;
72
+ const tokens = {};
73
+ outputs.forEach(({ tokensList }) => {
74
+ tokensList.forEach(({ address, value }) => {
75
+ if (typeof tokens[address] === 'undefined') {
76
+ tokens[address] = new BN(0);
77
+ }
78
+ tokens[address] = tokens[address].add(new BN(value));
79
+ });
80
+ });
81
+
82
+ context.tokenCondition = {
83
+ owner: stakeState.address,
84
+ tokens: Object.keys(tokens).map((address) => ({ address, value: tokens[address] })),
85
+ };
86
+
87
+ next();
88
+ });
89
+ runner.use(pipes.VerifyTokenBalance({ ownerKey: 'stakeState', conditionKey: 'tokenCondition' }));
90
+
91
+ // 7. verify asset state and ownership
92
+ runner.use(pipes.ExtractState({ from: 'assets', to: 'assetStates', status: 'OK', table: 'asset' }));
93
+ runner.use(pipes.VerifyTransferrable({ assets: 'assetStates' }));
94
+ runner.use(pipes.VerifyUpdater({ assetKey: 'assetStates', ownerKey: 'stakeState' }));
95
+
96
+ // 8. update statedb
97
+ runner.use(
98
+ async (context, next) => {
99
+ const { tx, itx, outputs, statedb, senderState, stakeState } = context;
100
+
101
+ const stakeUpdates = {
102
+ tokens: stakeState.tokens || {},
103
+ assets: stakeState.assets || [],
104
+ revokedTokens: stakeState.revokedTokens || {},
105
+ revokedAssets: stakeState.revokedAssets || [],
106
+ };
107
+
108
+ outputs.forEach((x) => {
109
+ const { tokensList, assetsList } = x;
110
+
111
+ // Move tokens and assets from staked to revoked
112
+ const staked = applyTokenUpdates(tokensList, { tokens: stakeUpdates.tokens }, 'sub');
113
+ stakeUpdates.tokens = staked.tokens;
114
+ stakeUpdates.assets = stakeUpdates.assets.filter((a) => assetsList.includes(a) === false);
115
+
116
+ const revoked = applyTokenUpdates(tokensList, { tokens: stakeUpdates.revokedTokens }, 'add');
117
+ stakeUpdates.revokedTokens = revoked.tokens;
118
+ stakeUpdates.revokedAssets = stakeUpdates.revokedAssets.concat(...assetsList);
119
+ });
120
+
121
+ const [newSenderState, newStakeState] = await Promise.all([
122
+ statedb.account.update(senderState.address, account.update(senderState, { nonce: tx.nonce }, context), context),
123
+ statedb.stake.update(stakeState.address, stake.update(stakeState, stakeUpdates, context), context),
124
+ ]);
125
+
126
+ context.senderState = newSenderState;
127
+ context.stakeState = newStakeState;
128
+
129
+ debug('revoke-stake', { address: itx.address, stakeUpdates });
130
+
131
+ next();
132
+ },
133
+ { persistError: true }
134
+ );
135
+
136
+ module.exports = runner;