@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.
- package/README.md +36 -0
- package/lib/execute.js +97 -30
- package/lib/index.js +56 -14
- package/lib/protocols/account/declare.js +36 -30
- package/lib/protocols/account/delegate.js +78 -40
- package/lib/protocols/account/migrate.js +65 -49
- package/lib/protocols/account/revoke-delegate.js +39 -23
- package/lib/protocols/asset/acquire-v2.js +159 -0
- package/lib/protocols/asset/acquire-v3.js +242 -0
- package/lib/protocols/asset/calls/README.md +5 -0
- package/lib/protocols/asset/calls/transfer-token.js +37 -0
- package/lib/protocols/asset/calls/transfer.js +29 -0
- package/lib/protocols/asset/create.js +85 -36
- package/lib/protocols/asset/mint.js +133 -0
- package/lib/protocols/asset/pipes/exec-mint-hook.js +59 -0
- package/lib/protocols/asset/pipes/extract-factory-tokens.js +18 -0
- package/lib/protocols/asset/pipes/verify-acquire-params.js +30 -0
- package/lib/protocols/asset/pipes/verify-itx-address.js +41 -0
- package/lib/protocols/asset/pipes/verify-itx-assets.js +49 -0
- package/lib/protocols/asset/pipes/verify-itx-variables.js +26 -0
- package/lib/protocols/asset/pipes/verify-mint-limit.js +13 -0
- package/lib/protocols/asset/update.js +76 -44
- package/lib/protocols/factory/create.js +146 -0
- package/lib/protocols/governance/claim-stake.js +219 -0
- package/lib/protocols/governance/revoke-stake.js +136 -0
- package/lib/protocols/governance/stake.js +176 -0
- package/lib/protocols/rollup/claim-reward.js +283 -0
- package/lib/protocols/rollup/create-block.js +333 -0
- package/lib/protocols/rollup/create.js +169 -0
- package/lib/protocols/rollup/join.js +156 -0
- package/lib/protocols/rollup/leave.js +127 -0
- package/lib/protocols/rollup/migrate-contract.js +53 -0
- package/lib/protocols/rollup/migrate-token.js +66 -0
- package/lib/protocols/rollup/pause.js +52 -0
- package/lib/protocols/rollup/pipes/ensure-service-fee.js +37 -0
- package/lib/protocols/rollup/pipes/ensure-validator.js +10 -0
- package/lib/protocols/rollup/pipes/verify-evidence.js +37 -0
- package/lib/protocols/rollup/pipes/verify-paused.js +10 -0
- package/lib/protocols/rollup/pipes/verify-signers.js +88 -0
- package/lib/protocols/rollup/resume.js +52 -0
- package/lib/protocols/rollup/update.js +98 -0
- package/lib/protocols/token/create.js +150 -0
- package/lib/protocols/token/deposit-v2.js +241 -0
- package/lib/protocols/token/withdraw-v2.js +255 -0
- package/lib/protocols/trade/exchange-v2.js +179 -0
- package/lib/protocols/trade/transfer-v2.js +136 -0
- package/lib/protocols/trade/transfer-v3.js +241 -0
- package/lib/util.js +325 -2
- package/package.json +23 -16
- package/lib/protocols/misc/poke.js +0 -106
- package/lib/protocols/trade/exchange.js +0 -139
- 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 {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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: '
|
|
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
|
-
//
|
|
54
|
+
// Ensure we are in append-only mode update proposed by issuer
|
|
41
55
|
runner.use(async (context, next) => {
|
|
42
|
-
const {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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;
|