Merge pull request #3498 from poanetwork/va-staking-dapp-transferAndCall

Make Staking DApp work with transferAndCall function
pull/3500/head
Victor Baranov 4 years ago committed by GitHub
commit a4b04d38bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 5
      apps/block_scout_web/assets/js/pages/stakes.js
  3. 4
      apps/block_scout_web/assets/js/pages/stakes/become_candidate.js
  4. 3
      apps/block_scout_web/assets/js/pages/stakes/make_stake.js
  5. 1
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  6. 16
      apps/block_scout_web/priv/gettext/default.pot
  7. 16
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  8. 7
      apps/explorer/lib/explorer/staking/contract_state.ex
  9. 968
      apps/explorer/priv/contracts_abi/posdao/Token.json
  10. 4
      apps/explorer/test/explorer/staking/contract_state_test.exs

@ -20,6 +20,7 @@
- [#3457](https://github.com/poanetwork/blockscout/pull/3457) - Fix doubled token transfer on block's page if block has reorg
### Chore
- [#3498](https://github.com/poanetwork/blockscout/pull/3498) - Make Staking DApp work with transferAndCall function
- [#3496](https://github.com/poanetwork/blockscout/pull/3496) - Rollback websocket_client module to 1.3.0
- [#3489](https://github.com/poanetwork/blockscout/pull/3489) - Migrate to Webpack@5
- [#3487](https://github.com/poanetwork/blockscout/pull/3487) - Docker setup update to be compatible with Erlang OTP 23

@ -36,6 +36,7 @@ export const initialState = {
stakingAllowed: false,
stakingTokenDefined: false,
stakingContract: null,
tokenContract: null,
tokenDecimals: 0,
tokenSymbol: '',
validatorSetApplyBlock: 0,
@ -106,6 +107,7 @@ export function reducer (state = initialState, action) {
stakingContract: action.stakingContract,
blockRewardContract: action.blockRewardContract,
validatorSetContract: action.validatorSetContract,
tokenContract: action.tokenContract,
tokenDecimals: action.tokenDecimals,
tokenSymbol: action.tokenSymbol
})
@ -234,12 +236,15 @@ if ($stakesPage.length) {
new web3.eth.Contract(msg.block_reward_contract.abi, msg.block_reward_contract.address)
const validatorSetContract =
new web3.eth.Contract(msg.validator_set_contract.abi, msg.validator_set_contract.address)
const tokenContract =
new web3.eth.Contract(msg.token_contract.abi, msg.token_contract.address)
store.dispatch({
type: 'RECEIVED_CONTRACTS',
stakingContract,
blockRewardContract,
validatorSetContract,
tokenContract,
tokenDecimals: parseInt(msg.token_decimals, 10),
tokenSymbol: msg.token_symbol
})

@ -75,6 +75,7 @@ export function becomeCandidateConnectionLost () {
async function becomeCandidate ($modal, store, msg) {
const state = store.getState()
const stakingContract = state.stakingContract
const tokenContract = state.tokenContract
const decimals = state.tokenDecimals
const stake = new BigNumber($modal.find('[candidate-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue()
const $miningAddressInput = $modal.find('[mining-address]')
@ -95,8 +96,7 @@ async function becomeCandidate ($modal, store, msg) {
lockModal($modal)
console.log(`Call addPool(${stake.toFixed()}, ${miningAddress})`)
makeContractCall(stakingContract.methods.addPool(stake.toFixed(), miningAddress), store)
makeContractCall(tokenContract.methods.transferAndCall(stakingContract.options.address, stake.toFixed(), `${miningAddress}01`), store)
} catch (err) {
openErrorModal('Error', err.message)
}

@ -53,6 +53,7 @@ export function openMakeStakeModal (event, store) {
async function makeStake ($modal, address, store, msg) {
const state = store.getState()
const stakingContract = state.stakingContract
const tokenContract = state.tokenContract
const validatorSetContract = state.validatorSetContract
const decimals = state.tokenDecimals
@ -73,7 +74,7 @@ async function makeStake ($modal, address, store, msg) {
return
}
makeContractCall(stakingContract.methods.stake(address, stake.toFixed()), store)
makeContractCall(tokenContract.methods.transferAndCall(stakingContract.options.address, stake.toFixed(), address), store)
}
function isDelegatorStakeValid (value, store, msg, address) {

@ -723,6 +723,7 @@ defmodule BlockScoutWeb.StakesChannel do
staking_contract: ContractState.get(:staking_contract),
block_reward_contract: ContractState.get(:block_reward_contract),
validator_set_contract: ContractState.get(:validator_set_contract),
token_contract: ContractState.get(:token_contract),
token_decimals: to_string(token.decimals),
token_symbol: token.symbol
})

@ -2299,7 +2299,7 @@ msgid "Pool"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:753
#: lib/block_scout_web/channels/stakes_channel.ex:754
msgid "Pools searching is already in progress for this address"
msgstr ""
@ -2332,7 +2332,7 @@ msgid "Remove My Pool"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:794
#: lib/block_scout_web/channels/stakes_channel.ex:795
msgid "Reward calculating is already in progress for this address"
msgstr ""
@ -2412,7 +2412,7 @@ msgid "Stakes Ratio"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:797
#: lib/block_scout_web/channels/stakes_channel.ex:798
msgid "Staking epochs are not specified or not in the allowed range"
msgstr ""
@ -2507,19 +2507,19 @@ msgid "Unable to find any pools you could claim a reward from."
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:760
#: lib/block_scout_web/channels/stakes_channel.ex:807
#: lib/block_scout_web/channels/stakes_channel.ex:761
#: lib/block_scout_web/channels/stakes_channel.ex:808
msgid "Unknown address of Staking contract. Please, contact support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:800
#: lib/block_scout_web/channels/stakes_channel.ex:801
msgid "Unknown pool staking address. Please, contact support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:756
#: lib/block_scout_web/channels/stakes_channel.ex:803
#: lib/block_scout_web/channels/stakes_channel.ex:757
#: lib/block_scout_web/channels/stakes_channel.ex:804
msgid "Unknown staker address. Please, choose your account in MetaMask"
msgstr ""

@ -2299,7 +2299,7 @@ msgid "Pool"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:753
#: lib/block_scout_web/channels/stakes_channel.ex:754
msgid "Pools searching is already in progress for this address"
msgstr ""
@ -2332,7 +2332,7 @@ msgid "Remove My Pool"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:794
#: lib/block_scout_web/channels/stakes_channel.ex:795
msgid "Reward calculating is already in progress for this address"
msgstr ""
@ -2412,7 +2412,7 @@ msgid "Stakes Ratio"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:797
#: lib/block_scout_web/channels/stakes_channel.ex:798
msgid "Staking epochs are not specified or not in the allowed range"
msgstr ""
@ -2507,19 +2507,19 @@ msgid "Unable to find any pools you could claim a reward from."
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:760
#: lib/block_scout_web/channels/stakes_channel.ex:807
#: lib/block_scout_web/channels/stakes_channel.ex:761
#: lib/block_scout_web/channels/stakes_channel.ex:808
msgid "Unknown address of Staking contract. Please, contact support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:800
#: lib/block_scout_web/channels/stakes_channel.ex:801
msgid "Unknown pool staking address. Please, contact support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:756
#: lib/block_scout_web/channels/stakes_channel.ex:803
#: lib/block_scout_web/channels/stakes_channel.ex:757
#: lib/block_scout_web/channels/stakes_channel.ex:804
msgid "Unknown staker address. Please, choose your account in MetaMask"
msgstr ""

@ -29,7 +29,7 @@ defmodule Explorer.Staking.ContractState do
:snapshotted_epoch_number,
:staking_allowed,
:staking_contract,
:token_contract_address,
:token_contract,
:token,
:validator_min_reward_percent,
:validator_set_apply_block,
@ -73,6 +73,7 @@ defmodule Explorer.Staking.ContractState do
staking_abi = abi("StakingAuRa")
validator_set_abi = abi("ValidatorSetAuRa")
block_reward_abi = abi("BlockRewardAuRa")
token_abi = abi("Token")
staking_contract_address = Application.get_env(:explorer, __MODULE__)[:staking_contract_address]
# 2d21d217 = keccak256(erc677TokenContract())
@ -113,7 +114,7 @@ defmodule Explorer.Staking.ContractState do
is_snapshotting: false,
snapshotted_epoch_number: -1,
staking_contract: %{abi: staking_abi, address: staking_contract_address},
token_contract_address: token_contract_address,
token_contract: %{abi: token_abi, address: token_contract_address},
token: get_token(token_contract_address),
validator_set_contract: %{abi: validator_set_abi, address: validator_set_contract_address}
)
@ -313,7 +314,7 @@ defmodule Explorer.Staking.ContractState do
update_token =
get(:token) == nil or
get(:token_contract_address) != global_responses.token_contract_address or
get(:token_contract).address != global_responses.token_contract_address or
rem(block_number, @token_renew_frequency) == 0
if update_token do

@ -0,0 +1,968 @@
[
{
"constant": false,
"inputs": [
{
"name": "_bridge",
"type": "address"
}
],
"name": "removeBridge",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "PERMIT_TYPEHASH",
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "DOMAIN_SEPARATOR",
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "spender",
"type": "address"
},
{
"name": "addedValue",
"type": "uint256"
}
],
"name": "increaseAllowance",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
},
{
"name": "_data",
"type": "bytes"
}
],
"name": "transferAndCall",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_amount",
"type": "uint256"
}
],
"name": "mint",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_value",
"type": "uint256"
}
],
"name": "burn",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "bridgePointers",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "version",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "blockRewardContract",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_subtractedValue",
"type": "uint256"
}
],
"name": "decreaseApproval",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_token",
"type": "address"
},
{
"name": "_to",
"type": "address"
}
],
"name": "claimTokens",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_address",
"type": "address"
}
],
"name": "isBridge",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "nonces",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getTokenInterfacesVersion",
"outputs": [
{
"name": "major",
"type": "uint64"
},
{
"name": "minor",
"type": "uint64"
},
{
"name": "patch",
"type": "uint64"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_holder",
"type": "address"
},
{
"name": "_spender",
"type": "address"
},
{
"name": "_nonce",
"type": "uint256"
},
{
"name": "_expiry",
"type": "uint256"
},
{
"name": "_allowed",
"type": "bool"
},
{
"name": "_v",
"type": "uint8"
},
{
"name": "_r",
"type": "bytes32"
},
{
"name": "_s",
"type": "bytes32"
}
],
"name": "permit",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_bridge",
"type": "address"
}
],
"name": "addBridge",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "bridgeList",
"outputs": [
{
"name": "",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "spender",
"type": "address"
},
{
"name": "subtractedValue",
"type": "uint256"
}
],
"name": "decreaseAllowance",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_amount",
"type": "uint256"
}
],
"name": "push",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_amount",
"type": "uint256"
}
],
"name": "move",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "F_ADDR",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_addedValue",
"type": "uint256"
}
],
"name": "increaseApproval",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "stakingContract",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_amount",
"type": "uint256"
}
],
"name": "pull",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "bridgeCount",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "address"
}
],
"name": "expirations",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_name",
"type": "string"
},
{
"name": "_symbol",
"type": "string"
},
{
"name": "_decimals",
"type": "uint8"
},
{
"name": "_chainId",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "bridge",
"type": "address"
}
],
"name": "BridgeAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "bridge",
"type": "address"
}
],
"name": "BridgeRemoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "from",
"type": "address"
},
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "ContractFallbackCallFailed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "amount",
"type": "uint256"
}
],
"name": "Mint",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "burner",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Burn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": false,
"name": "data",
"type": "bytes"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"constant": false,
"inputs": [
{
"name": "_blockRewardContract",
"type": "address"
}
],
"name": "setBlockRewardContract",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_stakingContract",
"type": "address"
}
],
"name": "setStakingContract",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_amount",
"type": "uint256"
}
],
"name": "mintReward",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_staker",
"type": "address"
},
{
"name": "_amount",
"type": "uint256"
}
],
"name": "stake",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]

@ -16,7 +16,7 @@ defmodule Explorer.Staking.ContractStateTest do
assert ContractState.get(:epoch_end_block, 0) == 0
assert ContractState.get(:min_delegator_stake, 1) == 1
assert ContractState.get(:min_candidate_stake, 1) == 1
assert ContractState.get(:token_contract_address) == nil
assert ContractState.get(:token_contract) == nil
end
test "fetch new epoch data" do
@ -38,7 +38,7 @@ defmodule Explorer.Staking.ContractStateTest do
assert ContractState.get(:epoch_end_block) == 152
assert ContractState.get(:min_delegator_stake) == 1_000_000_000_000_000_000
assert ContractState.get(:min_candidate_stake) == 1_000_000_000_000_000_000
assert ContractState.get(:token_contract_address) == "0x6f7a73c96bd56f8b0debc795511eda135e105ea3"
assert ContractState.get(:token_contract).address == "0x6f7a73c96bd56f8b0debc795511eda135e105ea3"
assert Repo.aggregate(StakingPool, :count, :id) == 6
assert Repo.aggregate(StakingPoolsDelegator, :count, :id) == 16

Loading…
Cancel
Save