diff --git a/.github/scripts/validate-branch-policy.sh b/.github/scripts/validate-branch-policy.sh new file mode 100755 index 0000000..0fb7ee2 --- /dev/null +++ b/.github/scripts/validate-branch-policy.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +failed=0 + +error() { + echo "::error::$*" + failed=1 +} + +warning() { + echo "::warning::$*" +} + +active_branch_file="support/ci/ACTIVE_DEV_BRANCH" +if [[ ! -f "${active_branch_file}" ]]; then + error "${active_branch_file} is required." + active_branch="" +else + active_branch="$(tr -d '[:space:]' < "${active_branch_file}")" +fi + +if [[ -z "${active_branch}" ]]; then + error "${active_branch_file} must not be empty." +elif [[ "${active_branch}" == "main" ]]; then + error "${active_branch_file} must point to a dev branch, not main." +elif [[ "${active_branch}" != *-dev ]]; then + warning "${active_branch_file} should normally point to a -dev branch; got ${active_branch}." +fi + +release_branch="${active_branch%-dev}" +default_branch="${GITHUB_DEFAULT_BRANCH:-}" +event_name="${GITHUB_EVENT_NAME:-local}" +base_ref="${GITHUB_BASE_REF:-}" +head_ref="${GITHUB_HEAD_REF:-}" +ref_name="${GITHUB_REF_NAME:-}" +actor="${GITHUB_ACTOR:-}" + +if [[ -n "${default_branch}" && "${default_branch}" != "main" ]]; then + warning "Repository default branch should be main after branch-policy rollout; currently ${default_branch}." +fi + +if [[ -n "${base_ref}" && "${base_ref}" == "main" ]]; then + warning "PR targets main; retarget-main-prs should move it to ${active_branch}." +fi + +if [[ -n "${base_ref}" && -n "${active_branch}" ]]; then + if [[ "${base_ref}" == "${release_branch}" && "${head_ref}" != "${active_branch}" && "${ALLOW_DIRECT_RELEASE_PR:-false}" != "true" ]]; then + error "PRs into ${release_branch} must come from ${active_branch}. Merge feature work into ${active_branch}, then promote ${active_branch} -> ${release_branch}." + fi +fi + +if [[ "${event_name}" == "push" && "${ref_name}" == "main" ]]; then + case "${actor}" in + github-actions[bot]|ci-core-e2e-runner[bot]) + ;; + *) + error "main should only move by automation from ${active_branch}; direct push actor was ${actor:-unknown}." + ;; + esac +fi + +if [[ ! -f ".github/workflows/fast-forward-main.yaml" ]]; then + error ".github/workflows/fast-forward-main.yaml is required." +fi + +if [[ ! -f ".github/workflows/retarget-main-prs.yaml" ]]; then + error ".github/workflows/retarget-main-prs.yaml is required." +fi + +if [[ -f ".github/workflows/release-from-main.yml" ]]; then + error ".github/workflows/release-from-main.yml is forbidden. Releases must be tag/version-branch driven." +fi + +if [[ -f "release.config.js" ]]; then + error "release.config.js is forbidden in versioned tooling branches; semantic-release-on-main must not be restored." +fi + +if [[ -f ".github/workflows/release-from-tag.yml" ]]; then + if ! grep -Fq 'v*.*.*' .github/workflows/release-from-tag.yml; then + error "release-from-tag.yml must trigger only from version tags matching v*.*.*." + fi + if ! grep -Fq 'refs/remotes/origin/${version_branch}' .github/workflows/release-from-tag.yml || \ + ! grep -Fq 'tag_commit' .github/workflows/release-from-tag.yml || \ + ! grep -Fq 'branch_head' .github/workflows/release-from-tag.yml; then + error "release-from-tag.yml must verify the tag commit is the current matching version branch head." + fi +fi + +if [[ -f ".github/workflows/manual-docker-release.yml" ]]; then + if ! grep -Fq 'expected_branch=' .github/workflows/manual-docker-release.yml; then + error "manual-docker-release.yml must derive and enforce the expected version branch from the tag." + fi + if ! grep -Fq './.github/workflows/release-from-tag.yml' .github/workflows/manual-docker-release.yml; then + error "manual-docker-release.yml must delegate image promotion to release-from-tag.yml." + fi +fi + +if [[ "${failed}" -ne 0 ]]; then + exit 1 +fi + +if [[ -n "${base_ref}" ]]; then + echo "Branch policy ok for PR ${head_ref} -> ${base_ref}; active dev branch is ${active_branch}." +else + echo "Branch policy ok for ${event_name} on ${ref_name:-detached ref}; active dev branch is ${active_branch}." +fi diff --git a/.github/workflows/branch-policy.yml b/.github/workflows/branch-policy.yml new file mode 100644 index 0000000..7759870 --- /dev/null +++ b/.github/workflows/branch-policy.yml @@ -0,0 +1,24 @@ +name: Branch Policy + +on: + pull_request: + types: [opened, synchronize, reopened, edited, ready_for_review] + push: + branches: + - "**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + branch-policy: + name: Validate branch policy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate branch policy + env: + GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: ./.github/scripts/validate-branch-policy.sh diff --git a/.github/workflows/fast-forward-main.yaml b/.github/workflows/fast-forward-main.yaml new file mode 100644 index 0000000..688e997 --- /dev/null +++ b/.github/workflows/fast-forward-main.yaml @@ -0,0 +1,57 @@ +name: Fast-forward main + +# main is the static/default branch for GitHub UX and tools that assume a +# stable default branch. It is not the integration target. On each push to the +# configured active dev branch, fast-forward main to that commit. + +on: + push: + branches: ["**"] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: fast-forward-main-${{ github.repository }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + fast-forward: + if: github.ref_type == 'branch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fast-forward main to active dev branch + run: | + set -euo pipefail + + active_branch="$(tr -d '[:space:]' < support/ci/ACTIVE_DEV_BRANCH)" + if [[ -z "${active_branch}" || "${active_branch}" == "main" ]]; then + echo "::error::support/ci/ACTIVE_DEV_BRANCH must name a non-main dev branch" + exit 1 + fi + + if [[ "${GITHUB_REF_NAME}" != "${active_branch}" ]]; then + echo "Push was to ${GITHUB_REF_NAME}; active dev branch is ${active_branch}. Nothing to do." + exit 0 + fi + + if git ls-remote --exit-code --heads origin main >/dev/null 2>&1; then + git fetch origin main + if ! git merge-base --is-ancestor origin/main HEAD; then + echo "::error::main has diverged from ${active_branch}; refusing non-fast-forward update" + exit 1 + fi + else + echo "main does not exist yet; creating it at ${GITHUB_SHA}." + fi + + git push origin "HEAD:refs/heads/main" diff --git a/.github/workflows/retarget-main-prs.yaml b/.github/workflows/retarget-main-prs.yaml new file mode 100644 index 0000000..37a066f --- /dev/null +++ b/.github/workflows/retarget-main-prs.yaml @@ -0,0 +1,53 @@ +name: Retarget main PRs + +# main is a static/default alias of the active dev branch. Contributions should +# target the active dev branch directly; PRs opened against main are retargeted +# automatically so required checks and release-train rules run in the right +# branch context. +# +# pull_request_target is used for the write-scoped token. This workflow never +# checks out or executes PR head code; it reads only trusted base-branch files. + +on: + pull_request_target: + types: [opened, reopened, synchronize, edited, ready_for_review] + +permissions: + contents: read + pull-requests: write + issues: write + +defaults: + run: + shell: bash + +jobs: + retarget: + if: github.event.pull_request.base.ref == 'main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref }} + + - name: Retarget PR to active dev branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + + active_branch="$(tr -d '[:space:]' < support/ci/ACTIVE_DEV_BRANCH)" + if [[ -z "${active_branch}" || "${active_branch}" == "main" ]]; then + echo "::error::support/ci/ACTIVE_DEV_BRANCH must name a non-main dev branch" + exit 1 + fi + + gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --base "${active_branch}" + + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body "$(cat <-dev`** — when next-major (i.e. next-minor on 0.x) work is in progress, this branch is open for breaking changes. PRs introducing them target this branch, not `v0.18`. -- **Older majors** stay on the repo for back-ports and security patches. Default branch on github.com is whichever major is current stable. - -When you fork or clone, the default branch is `v0.18` today. If you have a `main` branch from a previous checkout, delete it locally: - -```sh -git checkout v0.18 -git branch -D main -git remote prune origin -``` +See [docs/BRANCHING.md](docs/BRANCHING.md) for the current release-train model. +In short: independently releasable work may target the stable branch directly; +multi-feature or cross-repo train work uses the active `*-dev` integration +branch and is promoted to the matching stable branch when ready. `main` is only +the default/static GitHub branch. ## Releases @@ -170,7 +162,7 @@ The project uses automated semantic versioning based on commit messages: | `feat!:`, `fix!:`, or `BREAKING CHANGE:` | **Major** version bump | 1.0.0 → 2.0.0 | | `docs:`, `style:`, `refactor:`, `test:`, `chore:`, `build:`, `ci:` | **No** version bump | Version stays the same | -**Important**: Never manually edit version numbers in `pyproject.toml` or other files. The release automation will handle all version updates automatically when PRs are merged to the main branch. +**Important**: Never manually edit version numbers in `pyproject.toml` or other files. Releases are cut from the stable branch using the release automation described above. ## Logging Configuration diff --git a/docs/BRANCHING.md b/docs/BRANCHING.md new file mode 100644 index 0000000..0125486 --- /dev/null +++ b/docs/BRANCHING.md @@ -0,0 +1,58 @@ +# Branching and Release Trains + +This repo follows the GenLayer release-train model. + +## Current Train + +- Current stable branch: `v0.18` +- Active integration branch: `v0.19-dev` +- Next stable target: `v0.19` +- `main`: default/static branch alias for the active integration branch + +## Stable Branches + +Stable branches are long-lived release lines. For semver-zero packages, each +minor line is treated as the release line, for example `v0.18` or `v0.19`. + +PRs may target a stable branch directly when the merged result should be +releasable immediately. This is appropriate for bug fixes, small non-breaking +features, isolated release fixes, or a breaking change that is intentionally +shipping as the next version by itself. + +Stable branches must remain releasable. PRs into stable branches are expected to +pass the required cross-repo `E2E Tests` gate before merge. + +## Integration Branches + +Integration branches are optional. Use one when multiple changes need to +accumulate before release, especially for cross-repo work, dependent features, +breaking changes that must ship together, or a train that needs advisory E2E +while still expected to be red. + +Integration branches are named after the target stable branch plus `-dev`, for +example `v0.19-dev`. Feature PRs for that train target the integration branch. + +PRs into integration branches may run `E2E Tests` as advisory checks. They are +not the release gate. + +## Promotion and Release + +When an integration train is ready, open a promotion PR from the integration +branch to the matching stable branch, for example `v0.19-dev` to `v0.19`. + +That promotion PR is the release-readiness gate and must pass required +cross-repo `E2E Tests`. The actual package release is cut from the stable branch +using a version tag after the stable branch is ready. + +## `main` + +`main` exists for GitHub UX and tools that require a stable default branch. It is +not a release branch and it is not the integration target. + +This repo keeps `main` forwarded to the active integration branch using +automation. PRs opened against `main` are automatically retargeted to the branch +listed in `support/ci/ACTIVE_DEV_BRANCH`. + +When changing the active integration branch, update +`support/ci/ACTIVE_DEV_BRANCH`, the repo docs, and the corresponding +`genlayer-e2e` release-train matrix in the same change set. diff --git a/genlayer_py/abi/calldata/decoder.py b/genlayer_py/abi/calldata/decoder.py index c993ac8..0772217 100644 --- a/genlayer_py/abi/calldata/decoder.py +++ b/genlayer_py/abi/calldata/decoder.py @@ -8,11 +8,24 @@ def decode(mem0: Buffer) -> CalldataEncodable: mem: memoryview = memoryview(mem0) + def take(length: int, label: str) -> memoryview: + nonlocal mem + if len(mem) < length: + raise GenLayerError( + f"truncated calldata while reading {label}: " + f"expected {length} bytes, found {len(mem)}" + ) + result = mem[:length] + mem = mem[length:] + return result + def read_uleb128() -> int: nonlocal mem ret = 0 off = 0 while True: + if len(mem) == 0: + raise GenLayerError("unexpected end of calldata while reading ULEB128") m = mem[0] ret = ret | ((m & 0x7F) << off) off += 7 @@ -33,9 +46,7 @@ def impl() -> CalldataEncodable: if code == consts.SPECIAL_TRUE: return True if code == consts.SPECIAL_ADDR: - ret_addr = mem[: CalldataAddress.SIZE] - mem = mem[CalldataAddress.SIZE :] - return CalldataAddress(ret_addr) + return CalldataAddress(take(CalldataAddress.SIZE, "address")) raise GenLayerError(f"Unknown special {bin(code)} {hex(code)}") code = code >> 3 if typ == consts.TYPE_PINT: @@ -43,12 +54,9 @@ def impl() -> CalldataEncodable: elif typ == consts.TYPE_NINT: return -code - 1 elif typ == consts.TYPE_BYTES: - ret_bytes = mem[:code] - mem = mem[code:] - return ret_bytes + return take(code, "bytes") elif typ == consts.TYPE_STR: - ret_str = mem[:code] - mem = mem[code:] + ret_str = take(code, "string") return str(ret_str, encoding="utf-8") elif typ == consts.TYPE_ARR: ret_arr = [] @@ -60,8 +68,7 @@ def impl() -> CalldataEncodable: prev = None for _i in range(code): le = read_uleb128() - key = str(mem[:le], encoding="utf-8") - mem = mem[le:] + key = str(take(le, "map key"), encoding="utf-8") if prev is not None: assert prev < key prev = key diff --git a/genlayer_py/accounts/actions.py b/genlayer_py/accounts/actions.py index 55b7091..e2d8c57 100644 --- a/genlayer_py/accounts/actions.py +++ b/genlayer_py/accounts/actions.py @@ -38,4 +38,9 @@ def get_current_nonce( if address is None and self.account is None: raise GenLayerError("No address provided and no account is connected") address_to_use = address or self.account.address - return self.get_transaction_count(address_to_use, block_identifier) + # Include locally pending transactions by default so consecutive + # submissions do not reuse the same nonce before the first is mined. + resolved_block_identifier = ( + "pending" if block_identifier is None else block_identifier + ) + return self.get_transaction_count(address_to_use, resolved_block_identifier) diff --git a/genlayer_py/chains/testnet_asimov.py b/genlayer_py/chains/testnet_asimov.py index 794f294..8984a80 100644 --- a/genlayer_py/chains/testnet_asimov.py +++ b/genlayer_py/chains/testnet_asimov.py @@ -1,4 +1,4 @@ -from genlayer_py.types import GenLayerChain, NativeCurrency +from genlayer_py.types import GenLayerChain, NativeCurrency, SimpleContractInfo from genlayer_py.consensus.abi import CONSENSUS_MAIN_ABI, CONSENSUS_DATA_ABI @@ -17,6 +17,103 @@ "bytecode": "", } +FEE_MANAGER_CONTRACT: SimpleContractInfo = { + "address": "0x21737AA4bea8FF12E202BF1BAB23751A95617533", + "abi": [ + { + "type": "function", + "name": "calculateMinAppealBond", + "stateMutability": "view", + "inputs": [ + {"name": "_txId", "type": "bytes32"}, + {"name": "_round", "type": "uint256"}, + {"name": "_status", "type": "uint8"}, + ], + "outputs": [{"name": "totalFeesToPay", "type": "uint256"}], + }, + ], +} + +ROUNDS_STORAGE_CONTRACT: SimpleContractInfo = { + "address": "0x1F595c0D549DE0812F127508ea1039636CFA62Cc", + "abi": [ + { + "type": "function", + "name": "getRoundNumber", + "stateMutability": "view", + "inputs": [{"name": "txId", "type": "bytes32"}], + "outputs": [{"name": "", "type": "uint256"}], + }, + { + "type": "function", + "name": "getRoundData", + "stateMutability": "view", + "inputs": [ + {"name": "txId", "type": "bytes32"}, + {"name": "round", "type": "uint256"}, + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "components": [ + {"name": "round", "type": "uint256"}, + {"name": "leaderIndex", "type": "uint256"}, + {"name": "votesCommitted", "type": "uint256"}, + {"name": "votesRevealed", "type": "uint256"}, + {"name": "appealBond", "type": "uint256"}, + {"name": "rotationsLeft", "type": "uint256"}, + {"name": "result", "type": "uint8"}, + {"name": "roundValidators", "type": "address[]"}, + {"name": "validatorVotes", "type": "uint8[]"}, + {"name": "validatorVotesHash", "type": "bytes32[]"}, + {"name": "validatorResultHash", "type": "bytes32[]"}, + ], + } + ], + }, + { + "type": "function", + "name": "getLastRoundData", + "stateMutability": "view", + "inputs": [{"name": "txId", "type": "bytes32"}], + "outputs": [ + {"name": "round", "type": "uint256"}, + { + "name": "roundData", + "type": "tuple", + "components": [ + {"name": "round", "type": "uint256"}, + {"name": "leaderIndex", "type": "uint256"}, + {"name": "votesCommitted", "type": "uint256"}, + {"name": "votesRevealed", "type": "uint256"}, + {"name": "appealBond", "type": "uint256"}, + {"name": "rotationsLeft", "type": "uint256"}, + {"name": "result", "type": "uint8"}, + {"name": "roundValidators", "type": "address[]"}, + {"name": "validatorVotes", "type": "uint8[]"}, + {"name": "validatorVotesHash", "type": "bytes32[]"}, + {"name": "validatorResultHash", "type": "bytes32[]"}, + ], + }, + ], + }, + ], +} + +APPEALS_CONTRACT: SimpleContractInfo = { + "address": "0x0F739Dd8f5322b9547c7d19a9621BC2ac8DF4089", + "abi": [ + { + "type": "function", + "name": "canAppeal", + "stateMutability": "view", + "inputs": [{"name": "_txId", "type": "bytes32"}], + "outputs": [{"name": "", "type": "bool"}], + }, + ], +} + testnet_asimov: GenLayerChain = GenLayerChain( id=4221, @@ -34,9 +131,9 @@ testnet=True, consensus_main_contract=CONSENSUS_MAIN_CONTRACT, consensus_data_contract=CONSENSUS_DATA_CONTRACT, - fee_manager_contract=None, - rounds_storage_contract=None, - appeals_contract=None, + fee_manager_contract=FEE_MANAGER_CONTRACT, + rounds_storage_contract=ROUNDS_STORAGE_CONTRACT, + appeals_contract=APPEALS_CONTRACT, staking_contract=None, default_number_of_initial_validators=5, default_consensus_max_rotations=3, diff --git a/genlayer_py/client/client.py b/genlayer_py/client/client.py index 3d6d833..92d3e1c 100644 --- a/genlayer_py/client/client.py +++ b/genlayer_py/client/client.py @@ -1,3 +1,4 @@ +from copy import deepcopy from typing import Optional from genlayer_py.types import GenLayerChain from genlayer_py.chains import localnet @@ -10,7 +11,7 @@ def create_client( endpoint: Optional[str] = None, account: Optional[LocalAccount] = None, ) -> GenLayerClient: - chain_config = chain or localnet + chain_config = deepcopy(chain or localnet) if endpoint is not None: chain_config.rpc_urls["default"]["http"] = [endpoint] client = GenLayerClient(chain_config, account) diff --git a/genlayer_py/client/genlayer_client.py b/genlayer_py/client/genlayer_client.py index aae6275..a453dc3 100644 --- a/genlayer_py/client/genlayer_client.py +++ b/genlayer_py/client/genlayer_client.py @@ -4,7 +4,7 @@ from eth_typing import Address, ChecksumAddress, HexStr from eth_account.signers.local import LocalAccount from hexbytes import HexBytes -from typing import AnyStr +from typing import AnyStr, Literal from genlayer_py.types import ( GenLayerChain, TransactionStatus, @@ -52,6 +52,11 @@ validator_claim, validator_prime, set_operator, + get_operator_transfer_context, + initiate_operator_transfer, + complete_operator_transfer, + cancel_operator_transfer, + get_pending_operator, set_identity, delegator_join, delegator_exit, @@ -302,8 +307,8 @@ def top_up_fees( self, transaction_id: HexStr, distribution: FeesDistributionInput, + value: int, account: Optional[LocalAccount] = None, - value: int = 0, ) -> HexStr: """Deposits additional fee budget for an existing consensus transaction.""" return top_up_fees( @@ -319,7 +324,7 @@ def top_up_and_submit_appeal( transaction_id: HexStr, distribution: FeesDistributionInput, account: Optional[LocalAccount] = None, - value: int = 0, + value: Optional[int] = None, ) -> HexStr: """Deposits appeal fee budget and submits an appeal in one consensus call.""" return top_up_and_submit_appeal( @@ -334,7 +339,8 @@ def top_up_and_submit_appeal( def wait_for_transaction_receipt( self, transaction_hash: _Hash32, - status: TransactionStatus = TransactionStatus.ACCEPTED, + status: Optional[TransactionStatus] = None, + wait_until: Optional[Literal["decided", "finalized"]] = None, interval: int = transaction_config.wait_interval, retries: int = transaction_config.retries, full_transaction: bool = False, @@ -344,6 +350,7 @@ def wait_for_transaction_receipt( self=self, transaction_hash=transaction_hash, status=status, + wait_until=wait_until, interval=interval, retries=retries, full_transaction=full_transaction, @@ -375,7 +382,7 @@ def appeal_transaction( self, transaction_id: HexStr, account: Optional[LocalAccount] = None, - value: int = 0, + value: Optional[int] = None, ): """Appeals a consensus transaction to trigger a new round of validation. Returns the original transaction_id (appeals operate on the same tx).""" @@ -488,6 +495,38 @@ def set_operator( self=self, validator=validator, operator=operator, account=account ) + def get_operator_transfer_context(self, validator): + """Wallet-bound context for building a rotation proof.""" + return get_operator_transfer_context(self=self, validator=validator) + + def initiate_operator_transfer( + self, validator, registration, account: Optional[LocalAccount] = None + ) -> HexBytes: + """Starts the two-step operator rotation (CON-715).""" + return initiate_operator_transfer( + self=self, validator=validator, registration=registration, account=account + ) + + def complete_operator_transfer( + self, validator, account: Optional[LocalAccount] = None + ) -> HexBytes: + """Finalises a pending operator rotation.""" + return complete_operator_transfer( + self=self, validator=validator, account=account + ) + + def cancel_operator_transfer( + self, validator, account: Optional[LocalAccount] = None + ) -> HexBytes: + """Abandons a pending operator rotation.""" + return cancel_operator_transfer( + self=self, validator=validator, account=account + ) + + def get_pending_operator(self, validator) -> dict: + """Pending operator and when its transfer was initiated.""" + return get_pending_operator(self=self, validator=validator) + def set_identity( self, validator, moniker: str, account: Optional[LocalAccount] = None ) -> HexBytes: diff --git a/genlayer_py/contracts/actions.py b/genlayer_py/contracts/actions.py index f2f62fc..1679443 100644 --- a/genlayer_py/contracts/actions.py +++ b/genlayer_py/contracts/actions.py @@ -24,6 +24,13 @@ FeesDistributionInput, FeesDistribution, FEES_DISTRIBUTION_ABI_TYPE, + DEFAULT_BOOTLOADER_OVERHEAD, + DEFAULT_CALLDATA_GAS_PER_BYTE, + DEFAULT_FIXED_PROPOSE_RECEIPT_GAS, + DEFAULT_GAS_PER_CHANGED_SLOT, + DEFAULT_INTRINSIC_GAS, + DEFAULT_RECEIPT_SLOTS_CHANGED, + MIN_RECEIPT_BYTES, NormalizedTransactionFees, SimulationFeeEstimateOptions, TransactionFeeEstimate, @@ -86,7 +93,8 @@ def read_contract( ) -> CalldataEncodable: if account is None and self.local_account is None: raise GenLayerError("No account provided and no account is connected") - sender_address = self.local_account.address + sender = account if account is not None else self.local_account + sender_address = sender.address data = [ calldata.encode( make_calldata_object(method=function_name, args=args, kwargs=kwargs) @@ -214,7 +222,7 @@ def appeal_transaction( self: GenLayerClient, transaction_id: HexStr, account: Optional[LocalAccount] = None, - value: int = 0, + value: Optional[int] = None, ) -> HexStr: """Appeals a consensus transaction. Returns the original transaction_id. @@ -226,6 +234,7 @@ def appeal_transaction( raise GenLayerError("No account set.") if self.chain.consensus_main_contract is None: raise GenLayerError("Consensus main contract not configured.") + resolved_value = _resolve_appeal_value(self, transaction_id, value) encoded_data = _encode_submit_appeal_data(self=self, transaction_id=transaction_id) @@ -233,7 +242,7 @@ def appeal_transaction( self=self, encoded_data=encoded_data, sender_account=sender_account, - value=value, + value=resolved_value, operation_name="Appeal", ) @@ -244,8 +253,8 @@ def top_up_fees( self: GenLayerClient, transaction_id: HexStr, distribution: FeesDistributionInput, + value: int, account: Optional[LocalAccount] = None, - value: int = 0, ) -> HexStr: """Deposits additional fee budget for an existing consensus transaction. @@ -273,13 +282,14 @@ def top_up_and_submit_appeal( transaction_id: HexStr, distribution: FeesDistributionInput, account: Optional[LocalAccount] = None, - value: int = 0, + value: Optional[int] = None, ) -> HexStr: """Deposits appeal fee budget and submits an appeal in one consensus call. Returns the original GenLayer transaction id, matching appeal_transaction. """ sender_account = account if account is not None else self.local_account + resolved_value = _resolve_appeal_value(self, transaction_id, value) encoded_data = _encode_fee_management_data( self=self, function_name="topUpAndSubmitAppeal", @@ -290,7 +300,7 @@ def top_up_and_submit_appeal( self=self, encoded_data=encoded_data, sender_account=sender_account, - value=value, + value=resolved_value, operation_name="Top up and submit appeal", ) return transaction_id @@ -377,6 +387,20 @@ def get_min_appeal_bond( return fee_contract.functions.calculateMinAppealBond(tx_bytes, round_number, tx_status).call() +def _resolve_appeal_value( + self: GenLayerClient, + transaction_id: HexStr, + value: Optional[int], +) -> int: + if value is not None: + return value + if self.chain.fee_manager_contract is None or self.chain.rounds_storage_contract is None: + raise GenLayerError( + "Cannot auto-resolve appeal bond: fee_manager_contract/rounds_storage_contract not configured for this chain." + ) + return get_min_appeal_bond(self, transaction_id) + + def _to_bytes32(self: GenLayerClient, hex_str: HexStr) -> bytes: """Convert a hex string to bytes32.""" if hex_str.startswith("0x"): @@ -442,7 +466,22 @@ def _transaction_fees_to_rpc( } if normalized["fee_value"] is not None: rpc_fees["feeValue"] = normalized["fee_value"] - return rpc_fees + return _json_safe_rpc_value(rpc_fees) + + +def _json_safe_rpc_value(value): + if isinstance(value, (bytes, bytearray)): + return "0x" + bytes(value).hex() + if isinstance(value, list): + return [_json_safe_rpc_value(item) for item in value] + if isinstance(value, tuple): + return [_json_safe_rpc_value(item) for item in value] + if isinstance(value, dict): + return { + key: _json_safe_rpc_value(item) + for key, item in value.items() + } + return value def _encode_submit_appeal_data( @@ -582,20 +621,37 @@ def get_current_fee_policy(self: GenLayerClient) -> FeePolicyQuote: ) gen_per_time_unit = fee_manager_contract.functions.GENPerTimeUnit().call() storage_unit_price = fee_manager_contract.functions.storageUnitPrice().call() - receipt_gas_price = fee_manager_contract.functions.quoteGasPrice().call() + quoted_receipt_gas_price = fee_manager_contract.functions.quoteGasPrice().call() execution_budget_floor = ( fee_manager_contract.functions.messageFeeParamsBudgetFloor().call() ) + enabled = ( + gen_per_time_unit > 0 + or storage_unit_price > 0 + or quoted_receipt_gas_price > 0 + ) + network_receipt_gas_price = self.w3.eth.gas_price if enabled else 0 + receipt_gas_price = max(quoted_receipt_gas_price, network_receipt_gas_price) + if enabled and receipt_gas_price == 0: + raise GenLayerError( + "receipt gas price quoted as zero; refusing to build a zero price cap" + ) + local_execution_budget_floor = receipt_gas_price * ( + DEFAULT_FIXED_PROPOSE_RECEIPT_GAS + + DEFAULT_INTRINSIC_GAS + + DEFAULT_BOOTLOADER_OVERHEAD + + (MIN_RECEIPT_BYTES * DEFAULT_CALLDATA_GAS_PER_BYTE) + + (DEFAULT_RECEIPT_SLOTS_CHANGED * DEFAULT_GAS_PER_CHANGED_SLOT) + ) return { - "enabled": ( - gen_per_time_unit > 0 - or storage_unit_price > 0 - or receipt_gas_price > 0 - ), + "enabled": enabled, "genPerTimeUnit": gen_per_time_unit, "storageUnitPrice": storage_unit_price, "receiptGasPrice": receipt_gas_price, - "executionBudgetFloor": execution_budget_floor, + "executionBudgetFloor": max( + execution_budget_floor, + local_execution_budget_floor, + ), } try: @@ -905,6 +961,46 @@ def _prepare_transaction( return transaction +KNOWN_REVERT_SELECTOR_NAMES = { + "0x8d53e553": "InsufficientFees", + "0xb4132db3": "MaxPriceExceeded", + "0x57df8523": "ExecutionBudgetExceeded", + "0x305e533c": "BudgetTooLow", + "0xa70732ee": "RollupBudgetBelowFloor", + "0x632be5a1": "FeeValueMustBeNonZero", +} + + +def _format_rpc_error(error: Exception) -> str: + parts = [str(error)] + for attr in ("message", "data"): + value = getattr(error, attr, None) + if isinstance(value, str) and value.strip(): + parts.append(value) + args = getattr(error, "args", ()) + for arg in args: + if isinstance(arg, dict): + for key in ("message", "data", "details", "shortMessage"): + value = arg.get(key) + if isinstance(value, str) and value.strip(): + parts.append(value) + elif isinstance(arg, str) and arg.strip(): + parts.append(arg) + + text = " ".join(dict.fromkeys(parts)) + selector_name = next( + ( + name + for selector, name in KNOWN_REVERT_SELECTOR_NAMES.items() + if selector in text + ), + None, + ) + if selector_name and selector_name not in text: + return f"{text} ({selector_name})" + return text + + def _send_consensus_call( self: GenLayerClient, encoded_data: HexStr, @@ -919,18 +1015,21 @@ def _send_consensus_call( if self.chain.consensus_main_contract is None: raise GenLayerError("Consensus main contract not configured.") - transaction = _prepare_transaction( - self=self, - sender=sender_account.address, - recipient=self.chain.consensus_main_contract["address"], - data=encoded_data, - value=value, - ) - signed_transaction = sender_account.sign_transaction(transaction) - serialized_transaction = self.w3.to_hex(signed_transaction.raw_transaction) - tx_hash = self.provider.make_request( - method="eth_sendRawTransaction", params=[serialized_transaction] - )["result"] + try: + transaction = _prepare_transaction( + self=self, + sender=sender_account.address, + recipient=self.chain.consensus_main_contract["address"], + data=encoded_data, + value=value, + ) + signed_transaction = sender_account.sign_transaction(transaction) + serialized_transaction = self.w3.to_hex(signed_transaction.raw_transaction) + tx_hash = self.provider.make_request( + method="eth_sendRawTransaction", params=[serialized_transaction] + )["result"] + except Exception as exc: + raise GenLayerError(f"{operation_name} failed: {_format_rpc_error(exc)}") from exc if self.chain.id == localnet.id: return tx_hash @@ -959,21 +1058,24 @@ def _send_transaction( f"Consensus main contract address not found in chain config for \"{self.chain.name}\".", ) - transaction = _prepare_transaction( - self=self, - sender=sender_account.address, - recipient=self.chain.consensus_main_contract["address"], - data=encoded_data, - value=value, - ) - signed_transaction = sender_account.sign_transaction(transaction) - serialized_transaction = self.w3.to_hex(signed_transaction.raw_transaction) - params = [serialized_transaction] - if sim_config is not None: - params.append(sim_config) - tx_hash = self.provider.make_request( - method="eth_sendRawTransaction", params=params - )["result"] + try: + transaction = _prepare_transaction( + self=self, + sender=sender_account.address, + recipient=self.chain.consensus_main_contract["address"], + data=encoded_data, + value=value, + ) + signed_transaction = sender_account.sign_transaction(transaction) + serialized_transaction = self.w3.to_hex(signed_transaction.raw_transaction) + params = [serialized_transaction] + if sim_config is not None: + params.append(sim_config) + tx_hash = self.provider.make_request( + method="eth_sendRawTransaction", params=params + )["result"] + except Exception as exc: + raise GenLayerError(f"Transaction failed: {_format_rpc_error(exc)}") from exc tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) if tx_receipt.status != 1: diff --git a/genlayer_py/contracts/utils.py b/genlayer_py/contracts/utils.py index 7e79432..2f02317 100644 --- a/genlayer_py/contracts/utils.py +++ b/genlayer_py/contracts/utils.py @@ -9,7 +9,7 @@ def make_calldata_object( ) -> CalldataEncodable: ret: Dict[str, CalldataEncodable] = {} if method is not None: - ret["method"] = method + ret[""] = method if args is not None and len(args) > 0: ret["args"] = args if kwargs is not None and isinstance(kwargs, dict) and kwargs: diff --git a/genlayer_py/staking/__init__.py b/genlayer_py/staking/__init__.py index 8c8fa92..d449eb4 100644 --- a/genlayer_py/staking/__init__.py +++ b/genlayer_py/staking/__init__.py @@ -5,6 +5,11 @@ validator_claim, validator_prime, set_operator, + get_operator_transfer_context, + initiate_operator_transfer, + complete_operator_transfer, + cancel_operator_transfer, + get_pending_operator, set_identity, delegator_join, delegator_exit, @@ -27,6 +32,11 @@ "validator_claim", "validator_prime", "set_operator", + "get_operator_transfer_context", + "initiate_operator_transfer", + "complete_operator_transfer", + "cancel_operator_transfer", + "get_pending_operator", "set_identity", "delegator_join", "delegator_exit", diff --git a/genlayer_py/staking/abi/validator_wallet_abi.json b/genlayer_py/staking/abi/validator_wallet_abi.json index cfd125a..df1cd5c 100644 --- a/genlayer_py/staking/abi/validator_wallet_abi.json +++ b/genlayer_py/staking/abi/validator_wallet_abi.json @@ -998,5 +998,55 @@ { "stateMutability": "payable", "type": "receive" + }, + { + "name": "initiateOperatorTransfer", + "type": "function", + "stateMutability": "nonpayable", + "inputs": [ + { + "name": "_newOperatorPubKey", + "type": "uint256[2]", + "internalType": "uint256[2]" + }, + { + "name": "_possessionProof", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [] + }, + { + "name": "completeOperatorTransfer", + "type": "function", + "stateMutability": "nonpayable", + "inputs": [], + "outputs": [] + }, + { + "name": "cancelOperatorTransfer", + "type": "function", + "stateMutability": "nonpayable", + "inputs": [], + "outputs": [] + }, + { + "name": "getPendingOperator", + "type": "function", + "stateMutability": "view", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ] } ] diff --git a/genlayer_py/staking/actions.py b/genlayer_py/staking/actions.py index 065b465..c3a9a64 100644 --- a/genlayer_py/staking/actions.py +++ b/genlayer_py/staking/actions.py @@ -17,6 +17,11 @@ from genlayer_py.exceptions import GenLayerError from genlayer_py.staking.abi import STAKING_ABI, VALIDATOR_WALLET_ABI +from genlayer_py.staking.operator_registration import ( + OperatorRegistrationContext, + OperatorRegistrationProof, + verify_operator_registration, +) if TYPE_CHECKING: from genlayer_py.client import GenLayerClient @@ -237,7 +242,12 @@ def set_operator( account: Optional[LocalAccount] = None, ) -> HexBytes: """Rotates the operator for an existing ValidatorWallet. Only the - wallet owner (the EOA that called validator_join) may do this.""" + wallet owner (the EOA that called validator_join) may do this. + + CON-715 removed this single call in favour of the two-step transfer + below; against a deployment carrying that change it reverts with no + decodable reason, because the selector no longer exists. Prefer + initiate_operator_transfer + complete_operator_transfer.""" sender = _sender(self, account) wallet = _wallet(self, validator) data = wallet.encode_abi( @@ -247,6 +257,89 @@ def set_operator( return _send(self, sender, tx) +def get_operator_transfer_context( + self: "GenLayerClient", validator: AddressLike +) -> OperatorRegistrationContext: + """Context for a rotation proof. + + Rotation is verified by the wallet rather than the factory, so the + registrar is the wallet's own address. The owner is read from the wallet + instead of assumed to be the caller: the proof is bound to whatever + owner() returns, and a mismatch is easier to diagnose here than as an + onlyOwner revert.""" + wallet = _wallet(self, validator) + return OperatorRegistrationContext( + registrar=self.w3.to_checksum_address(validator), + owner=self.w3.to_checksum_address(wallet.functions.owner().call()), + chain_id=self.w3.eth.chain_id, + ) + + +def initiate_operator_transfer( + self: "GenLayerClient", + validator: AddressLike, + registration: OperatorRegistrationProof, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Starts the two-step operator rotation. Owner only. + + `registration` must be built against get_operator_transfer_context — a + join proof is bound to the factory and will not verify here.""" + context = get_operator_transfer_context(self, validator) + if not verify_operator_registration(registration, context): + raise GenLayerError( + "Operator registration proof does not match the wallet, owner, chain, " + "or public key. Rotation proofs must use the validator wallet as " + "their registrar." + ) + + sender = _sender(self, account) + wallet = _wallet(self, validator) + data = wallet.encode_abi( + "initiateOperatorTransfer", + args=[list(registration.operator_pub_key), registration.possession_proof], + ) + tx = _build(self, sender, self.w3.to_checksum_address(validator), data) + return _send(self, sender, tx) + + +def complete_operator_transfer( + self: "GenLayerClient", + validator: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Finalises a pending rotation. Callable by the wallet owner or the + pending operator, once the factory's operatorTransferDelay has elapsed.""" + sender = _sender(self, account) + wallet = _wallet(self, validator) + data = wallet.encode_abi("completeOperatorTransfer", args=[]) + tx = _build(self, sender, self.w3.to_checksum_address(validator), data) + return _send(self, sender, tx) + + +def cancel_operator_transfer( + self: "GenLayerClient", + validator: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Abandons a pending rotation, leaving the current operator in place.""" + sender = _sender(self, account) + wallet = _wallet(self, validator) + data = wallet.encode_abi("cancelOperatorTransfer", args=[]) + tx = _build(self, sender, self.w3.to_checksum_address(validator), data) + return _send(self, sender, tx) + + +def get_pending_operator(self: "GenLayerClient", validator: AddressLike) -> dict: + """Pending operator and when its transfer was initiated (0 when none).""" + wallet = _wallet(self, validator) + operator, initiated_at = wallet.functions.getPendingOperator().call() + return { + "operator": self.w3.to_checksum_address(operator), + "initiated_at": int(initiated_at), + } + + def set_identity( self: "GenLayerClient", validator: AddressLike, diff --git a/genlayer_py/staking/operator_registration.py b/genlayer_py/staking/operator_registration.py new file mode 100644 index 0000000..b182ad5 --- /dev/null +++ b/genlayer_py/staking/operator_registration.py @@ -0,0 +1,135 @@ +"""Proof of possession for operator keys. + +Consensus requires an operator to prove control of its key before that key is +bound to a validator wallet. The proof is an EIP-191 signature, by the operator +key, over a domain-separated hash of (chainId, registrar, owner, pubKey). + +`registrar` is whichever contract verifies the proof, and it differs by flow: +the ValidatorWalletFactory for a validator join, the wallet itself for an +operator rotation (ValidatorWalletBlueprint.initiateOperatorTransfer calls +PubKeyUtils.validateWithPossession(pubKey, address(this), owner(), proof)). +Passing the wrong one produces a proof that simply fails to verify. + +The encoding mirrors genlayer-js's createOperatorRegistration exactly — the +shared vector in tests/unit/test_operator_registration.py pins the two +implementations together. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple + +from eth_abi.abi import encode as abi_encode +from eth_account import Account +from eth_account.messages import encode_defunct +from eth_typing import ChecksumAddress +from eth_utils.address import to_checksum_address +from eth_utils.crypto import keccak + +OPERATOR_REGISTRATION_DOMAIN = keccak( + text="GenLayer/operatorPubKey/proof-of-possession/v1" +) + +OperatorPublicKey = Tuple[int, int] + + +@dataclass(frozen=True) +class OperatorRegistrationContext: + """Who verifies the proof, on whose behalf, and on which chain.""" + + registrar: ChecksumAddress + owner: ChecksumAddress + chain_id: int + + +@dataclass(frozen=True) +class OperatorRegistrationProof: + operator: ChecksumAddress + operator_pub_key: OperatorPublicKey + possession_proof: bytes + + +def operator_public_key_from_private_key(private_key: str) -> OperatorPublicKey: + """Splits the uncompressed secp256k1 public key into the contract's + uint256[2] tuple.""" + account = Account.from_key(private_key) + public_key = account._key_obj.public_key.to_bytes() + return ( + int.from_bytes(public_key[0:32], "big"), + int.from_bytes(public_key[32:64], "big"), + ) + + +def operator_address_from_public_key(pub_key: OperatorPublicKey) -> ChecksumAddress: + raw = pub_key[0].to_bytes(32, "big") + pub_key[1].to_bytes(32, "big") + return to_checksum_address(keccak(raw)[-20:]) + + +def operator_possession_message( + pub_key: OperatorPublicKey, context: OperatorRegistrationContext +) -> bytes: + return keccak( + abi_encode( + ["bytes32", "uint256", "address", "address", "uint256", "uint256"], + [ + OPERATOR_REGISTRATION_DOMAIN, + context.chain_id, + context.registrar, + context.owner, + pub_key[0], + pub_key[1], + ], + ) + ) + + +def create_operator_registration( + private_key: str, context: OperatorRegistrationContext +) -> OperatorRegistrationProof: + """Builds the proof package the proof-bearing calls consume. + + The private key is used only to sign and is never retained in the result. + """ + account = Account.from_key(private_key) + pub_key = operator_public_key_from_private_key(private_key) + operator = operator_address_from_public_key(pub_key) + + if operator != to_checksum_address(account.address): + raise ValueError( + "Operator private key and public key derive different identities." + ) + + signed = Account.sign_message( + encode_defunct(operator_possession_message(pub_key, context)), + private_key=private_key, + ) + + return OperatorRegistrationProof( + operator=operator, + operator_pub_key=pub_key, + possession_proof=bytes(signed.signature), + ) + + +def verify_operator_registration( + registration: OperatorRegistrationProof, + context: OperatorRegistrationContext, +) -> bool: + """Checks key identity and the exact registrar/owner/chain binding.""" + if operator_address_from_public_key(registration.operator_pub_key) != to_checksum_address( + registration.operator + ): + return False + + message = encode_defunct( + operator_possession_message(registration.operator_pub_key, context) + ) + try: + recovered = Account.recover_message( + message, signature=registration.possession_proof + ) + except Exception: + return False + + return to_checksum_address(recovered) == to_checksum_address(registration.operator) diff --git a/genlayer_py/transactions/__init__.py b/genlayer_py/transactions/__init__.py index 3674413..ee9b531 100644 --- a/genlayer_py/transactions/__init__.py +++ b/genlayer_py/transactions/__init__.py @@ -2,6 +2,7 @@ CALL_KEY_DEPLOY, CALL_KEY_UNNAMED, CALL_KEY_WILDCARD, + DEPLOY_CALL_KEY, MESSAGE_ALLOCATION_ROOT_PARENT_INDEX, DEFAULT_FEES_DISTRIBUTION, MessageType, @@ -9,16 +10,24 @@ calculate_local_round_fees, create_fees_distribution, derive_external_message_call_key, + deploy_call_key, derive_internal_message_call_key, encode_external_message_fee_params, encode_internal_message_fee_params, extract_studio_fee_policy, ) + +def is_successful(transaction): + from .actions import is_successful as _is_successful + + return _is_successful(transaction) + __all__ = [ "CALL_KEY_DEPLOY", "CALL_KEY_UNNAMED", "CALL_KEY_WILDCARD", + "DEPLOY_CALL_KEY", "MESSAGE_ALLOCATION_ROOT_PARENT_INDEX", "DEFAULT_FEES_DISTRIBUTION", "MessageType", @@ -26,8 +35,10 @@ "calculate_local_round_fees", "create_fees_distribution", "derive_external_message_call_key", + "deploy_call_key", "derive_internal_message_call_key", "encode_external_message_fee_params", "encode_internal_message_fee_params", "extract_studio_fee_policy", + "is_successful", ] diff --git a/genlayer_py/transactions/actions.py b/genlayer_py/transactions/actions.py index 3da21f1..826ee91 100644 --- a/genlayer_py/transactions/actions.py +++ b/genlayer_py/transactions/actions.py @@ -2,7 +2,8 @@ from genlayer_py.logging import logger import json -from typing import Any, Dict, List +import warnings +from typing import Any, Dict, List, Literal, Optional from web3 import Web3 from web3.types import _Hash32 from eth_typing import HexStr @@ -11,6 +12,8 @@ from genlayer_py.config import transaction_config from genlayer_py.types import ( TransactionStatus, + ExecutionResult, + EXECUTION_RESULT_NUMBER_TO_NAME, TRANSACTION_STATUS_NAME_TO_NUMBER, TRANSACTION_STATUS_NUMBER_TO_NAME, is_decided_state, @@ -66,40 +69,142 @@ from genlayer_py.client import GenLayerClient +_did_warn_wait_for_transaction_receipt_status = False + + +def _warn_deprecated_receipt_status() -> None: + global _did_warn_wait_for_transaction_receipt_status + if _did_warn_wait_for_transaction_receipt_status: + return + _did_warn_wait_for_transaction_receipt_status = True + warnings.warn( + "wait_for_transaction_receipt(status=...) is deprecated; use " + "wait_until='decided' or wait_until='finalized' instead.", + DeprecationWarning, + stacklevel=3, + ) + + +def _resolve_wait_target( + status: Optional[TransactionStatus], + wait_until: Optional[Literal["decided", "finalized"]], +) -> dict: + if wait_until is not None: + if wait_until not in ("decided", "finalized"): + raise ValueError("wait_until must be 'decided' or 'finalized'.") + return {"wait_until": wait_until, "legacy_status": None, "label": wait_until} + if status is None: + return {"wait_until": "decided", "legacy_status": None, "label": "decided"} + + _warn_deprecated_receipt_status() + if status == TransactionStatus.ACCEPTED: + return {"wait_until": "decided", "legacy_status": None, "label": "decided"} + if status == TransactionStatus.FINALIZED: + return {"wait_until": "finalized", "legacy_status": None, "label": "finalized"} + return {"wait_until": None, "legacy_status": status, "label": status.value} + + +def _has_reached_wait_target(transaction_status: str, target: dict) -> bool: + if target["wait_until"] == "decided": + return is_decided_state(transaction_status) + if target["wait_until"] == "finalized": + return transaction_status == TRANSACTION_STATUS_NAME_TO_NUMBER[ + TransactionStatus.FINALIZED + ] + legacy_status = target["legacy_status"] + return ( + legacy_status is not None + and transaction_status == TRANSACTION_STATUS_NAME_TO_NUMBER[legacy_status] + ) + + +def _normalize_status_name(value: Any) -> Optional[TransactionStatus]: + if isinstance(value, TransactionStatus): + return value + if isinstance(value, str): + if value in TransactionStatus._value2member_map_: + return TransactionStatus(value) + return TRANSACTION_STATUS_NUMBER_TO_NAME.get(value) + if value is None: + return None + return TRANSACTION_STATUS_NUMBER_TO_NAME.get(str(value)) + + +def _normalize_execution_result_name(value: Any) -> Optional[ExecutionResult]: + if isinstance(value, ExecutionResult): + return value + if isinstance(value, str) and value in ExecutionResult._value2member_map_: + return ExecutionResult(value) + if value is None: + return None + return EXECUTION_RESULT_NUMBER_TO_NAME.get(str(value)) + + +def is_successful(transaction: GenLayerTransaction) -> bool: + status_name = _normalize_status_name( + transaction.get("status_name", transaction.get("status")) + ) + execution_result_name = _normalize_execution_result_name( + transaction.get( + "tx_execution_result_name", + transaction.get("tx_execution_result"), + ) + ) + + if execution_result_name is None: + consensus_data = transaction.get("consensus_data") + if isinstance(consensus_data, dict): + leader_receipt = consensus_data.get("leader_receipt") + if isinstance(leader_receipt, list): + leader_receipt = leader_receipt[0] if leader_receipt else None + if ( + isinstance(leader_receipt, dict) + and leader_receipt.get("execution_result") == "SUCCESS" + ): + execution_result_name = ExecutionResult.FINISHED_WITH_RETURN + + return ( + status_name in (TransactionStatus.ACCEPTED, TransactionStatus.FINALIZED) + and execution_result_name == ExecutionResult.FINISHED_WITH_RETURN + ) + + def wait_for_transaction_receipt( self: GenLayerClient, transaction_hash: _Hash32, - status: TransactionStatus = TransactionStatus.ACCEPTED, + status: Optional[TransactionStatus] = None, + wait_until: Optional[Literal["decided", "finalized"]] = None, interval: int = transaction_config.wait_interval, retries: int = transaction_config.retries, full_transaction: bool = False, ) -> GenLayerTransaction: attempts = 0 + target = _resolve_wait_target(status, wait_until) + transaction = None + last_status = None while attempts < retries: transaction = self.get_transaction(transaction_hash=transaction_hash) if transaction is None: raise GenLayerError(f"Transaction {transaction_hash} not found") transaction_status = str(transaction["status"]) - last_status = TRANSACTION_STATUS_NUMBER_TO_NAME[transaction_status] - finalized_status = TRANSACTION_STATUS_NAME_TO_NUMBER[ - TransactionStatus.FINALIZED - ] - requested_status = TRANSACTION_STATUS_NAME_TO_NUMBER[status] + last_status = TRANSACTION_STATUS_NUMBER_TO_NAME.get(transaction_status) - if transaction_status == requested_status or ( - status == TransactionStatus.ACCEPTED - and is_decided_state(transaction_status) - ): + if _has_reached_wait_target(transaction_status, target): if not full_transaction: return _simplify_transaction_receipt(transaction) return transaction time.sleep(interval / 1000) attempts += 1 + last_status_label = ( + last_status.value + if isinstance(last_status, TransactionStatus) + else str(transaction["status"]) if transaction else "" + ) raise GenLayerError( - f"Transaction {transaction_hash} did not reach desired status '{status.value}' after {retries} attempts " + f"Transaction {transaction_hash} did not reach desired status '{target['label']}' after {retries} attempts " f"(polling every {interval}ms for a total of {retries * interval / 1000:.1f}s). " - f"Last observed status: '{last_status.value}'. " + f"Last observed status: '{last_status_label}'. " f"This may indicate the transaction is still processing, or the network is experiencing delays. " f"Consider increasing 'retries' or 'interval' parameters.\n" f"Transaction object simplified: {json.dumps(_simplify_transaction_receipt(transaction), indent=2, default=str)}" diff --git a/genlayer_py/transactions/fees.py b/genlayer_py/transactions/fees.py index e05c529..ba1a9e3 100644 --- a/genlayer_py/transactions/fees.py +++ b/genlayer_py/transactions/fees.py @@ -131,6 +131,7 @@ class TransactionFeeEstimate(TypedDict, total=False): fee_value: int policy: FeePolicyQuote observed: dict[str, int] + simulation: Any class SimulationFeeEstimateOptions(FeeEstimateOptions, total=False): @@ -153,9 +154,16 @@ class NormalizedTransactionFees(TypedDict): MESSAGE_ALLOCATION_ROOT_PARENT_INDEX = (1 << 256) - 1 -CALL_KEY_WILDCARD = b"\x00" * 32 +# Wildcard sentinel = keccak256 of empty bytes, untagged. Reserved: it can never be a +# derived key — short names (<32B) are left-aligned with a zero tail byte, long names +# get the low bit forced to 1, and this hash has neither. +CALL_KEY_WILDCARD = bytes.fromhex( + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" +) +# Empty method name derives bytes32(0); GenVM emits it for deploy and emit_transfer. CALL_KEY_UNNAMED = HexStr("0x" + ("0" * 64)) -CALL_KEY_DEPLOY = CALL_KEY_UNNAMED +DEPLOY_CALL_KEY = CALL_KEY_UNNAMED +CALL_KEY_DEPLOY = DEPLOY_CALL_KEY FEES_DISTRIBUTION_ABI_TYPE = ( "(uint256,uint256,uint256,uint256,uint256,uint256,uint256[],uint256,uint256,uint256)" @@ -193,13 +201,20 @@ class NormalizedTransactionFees(TypedDict): DEFAULT_LEADER_TIMEUNITS_ALLOCATION = 100 DEFAULT_VALIDATOR_TIMEUNITS_ALLOCATION = 200 DEFAULT_TRANSACTION_EXECUTION_BUDGET_PER_ROUND = 500_000 +# Provisional heuristic sized ~20x observed dev-env consumption (~5M gas-equivalent). +# TODO(data): replace with telemetry-derived default (p99 x margin) once fee consumption telemetry is collected. +DEFAULT_TRANSACTION_EXECUTION_GAS = 100_000_000 +DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM = 10_000 MIN_RECEIPT_BYTES = 512 DEFAULT_RECEIPT_SLOTS_CHANGED = 7 DEFAULT_INTRINSIC_GAS = 21_000 DEFAULT_BOOTLOADER_OVERHEAD = 60_000 DEFAULT_FIXED_PROPOSE_RECEIPT_GAS = 210_000 +DEFAULT_FIXED_MESSAGE_REVEAL_GAS = 100_000 DEFAULT_GAS_PER_CHANGED_SLOT = 1_000 DEFAULT_CALLDATA_GAS_PER_BYTE = 16 +DEFAULT_MESSAGE_REVEAL_LENGTH_SLOTS = 32 +DEFAULT_NONDET_OUTPUT_LENGTH_BYTES = 32 ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" VALIDATORS_PER_ROUND = [ @@ -305,6 +320,10 @@ def derive_external_message_call_key( return _bytes_to_padded_call_key(calldata[:4]) +def deploy_call_key() -> HexStr: + return DEPLOY_CALL_KEY + + def _normalize_message_type(value: Union[MessageType, int, str]) -> int: if isinstance(value, MessageType): return int(value) @@ -655,6 +674,10 @@ def extract_studio_fee_policy(config: Any) -> FeePolicyQuote: policy.get("fixedProposeReceiptGas", DEFAULT_FIXED_PROPOSE_RECEIPT_GAS), "policy.fixedProposeReceiptGas", ) + fixed_message_reveal_gas = _int_from_unknown( + policy.get("fixedMessageRevealGas", DEFAULT_FIXED_MESSAGE_REVEAL_GAS), + "policy.fixedMessageRevealGas", + ) explicit_budget_floor = policy.get( "messageFeeParamsBudgetFloor", config.get("messageFeeParamsBudgetFloor"), @@ -670,8 +693,12 @@ def extract_studio_fee_policy(config: Any) -> FeePolicyQuote: fixed_propose_receipt_gas + intrinsic_gas + bootloader_overhead - + (MIN_RECEIPT_BYTES * calldata_gas_per_byte) + (DEFAULT_RECEIPT_SLOTS_CHANGED * gas_per_changed_slot) + + fixed_message_reveal_gas + + intrinsic_gas + + bootloader_overhead + + (DEFAULT_MESSAGE_REVEAL_LENGTH_SLOTS * gas_per_changed_slot) + + (DEFAULT_NONDET_OUTPUT_LENGTH_BYTES * calldata_gas_per_byte) ) ) @@ -703,6 +730,7 @@ def _default_execution_budget_per_round(policy: FeePolicyQuote) -> int: return max( DEFAULT_TRANSACTION_EXECUTION_BUDGET_PER_ROUND, policy["executionBudgetFloor"], + policy["receiptGasPrice"] * DEFAULT_TRANSACTION_EXECUTION_GAS, ) @@ -856,7 +884,7 @@ def observed_simulation_fee_usage( observed_execution_budget = execution_fee_consumed + execution_fee_report_total recommended_execution_budget = ( max( - _default_execution_budget_per_round(policy), + policy["executionBudgetFloor"], _with_cap_headroom(observed_execution_budget, execution_headroom_bps), ) if observed_execution_budget > 0 @@ -967,6 +995,7 @@ def transaction_fee_estimate_from_studio_estimate( "feeValue": to_uint(fee_value, "recommendedPreset.feeValue"), "fee_value": to_uint(fee_value, "recommendedPreset.feeValue"), "policy": policy, + "simulation": simulation, "observed": observed_simulation_fee_usage( {"simulation": simulation}, policy, @@ -1058,17 +1087,38 @@ def build_estimated_fees_distribution( "priceCapHeadroomBps", DEFAULT_PRICE_CAP_HEADROOM_BPS, ) - execution_budget_default = _default_execution_budget_per_round(policy) + base_execution_budget_default = _default_execution_budget_per_round(policy) total_message_fees = _get(options, "totalMessageFees", "total_message_fees") message_allocations = _get(options, "messageAllocations", "message_allocations") + normalized_message_allocations = ( + normalize_message_fee_allocations(message_allocations) + if message_allocations is not None + else None + ) if total_message_fees is None and message_allocations is not None: total_message_fees = sum( allocation["budget"] - for allocation in normalize_message_fee_allocations(message_allocations) + for allocation in normalized_message_allocations or [] if allocation["messageType"] == int(MessageType.External) or allocation["parentIndex"] == MESSAGE_ALLOCATION_ROOT_PARENT_INDEX ) + emits_messages = ( + ( + normalized_message_allocations is not None + and len(normalized_message_allocations) > 0 + ) + or ( + total_message_fees is not None + and to_uint(total_message_fees, "totalMessageFees") > 0 + ) + ) + execution_budget_default = ( + base_execution_budget_default + + policy["receiptGasPrice"] * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM + if emits_messages + else base_execution_budget_default + ) return create_fees_distribution( { diff --git a/genlayer_py/types/transactions.py b/genlayer_py/types/transactions.py index f73a85a..89a25fe 100644 --- a/genlayer_py/types/transactions.py +++ b/genlayer_py/types/transactions.py @@ -27,6 +27,7 @@ class TransactionStatus(str, Enum): READY_TO_FINALIZE = "READY_TO_FINALIZE" VALIDATORS_TIMEOUT = "VALIDATORS_TIMEOUT" LEADER_TIMEOUT = "LEADER_TIMEOUT" + LEADER_REVEALING = "LEADER_REVEALING" TRANSACTION_STATUS_NUMBER_TO_NAME = { @@ -44,6 +45,7 @@ class TransactionStatus(str, Enum): "11": TransactionStatus.READY_TO_FINALIZE, "12": TransactionStatus.VALIDATORS_TIMEOUT, "13": TransactionStatus.LEADER_TIMEOUT, + "14": TransactionStatus.LEADER_REVEALING, } TRANSACTION_STATUS_NAME_TO_NUMBER = { @@ -61,6 +63,7 @@ class TransactionStatus(str, Enum): TransactionStatus.READY_TO_FINALIZE: "11", TransactionStatus.VALIDATORS_TIMEOUT: "12", TransactionStatus.LEADER_TIMEOUT: "13", + TransactionStatus.LEADER_REVEALING: "14", } DECIDED_STATES = [ @@ -86,29 +89,26 @@ class TransactionResult(str, Enum): NO_MAJORITY = "NO_MAJORITY" MAJORITY_AGREE = "MAJORITY_AGREE" MAJORITY_DISAGREE = "MAJORITY_DISAGREE" + MAJORITY_TIMEOUT = "MAJORITY_TIMEOUT" TRANSACTION_RESULT_NUMBER_TO_NAME = { "0": TransactionResult.IDLE, - "1": TransactionResult.AGREE, - "2": TransactionResult.DISAGREE, - "3": TransactionResult.TIMEOUT, + "1": TransactionResult.MAJORITY_AGREE, + "2": TransactionResult.MAJORITY_DISAGREE, + "3": TransactionResult.MAJORITY_TIMEOUT, "4": TransactionResult.DETERMINISTIC_VIOLATION, "5": TransactionResult.NO_MAJORITY, - "6": TransactionResult.MAJORITY_AGREE, - "7": TransactionResult.MAJORITY_DISAGREE, } TRANSACTION_RESULT_NAME_TO_NUMBER = { TransactionResult.IDLE: "0", - TransactionResult.AGREE: "1", - TransactionResult.DISAGREE: "2", - TransactionResult.TIMEOUT: "3", + TransactionResult.MAJORITY_AGREE: "1", + TransactionResult.MAJORITY_DISAGREE: "2", + TransactionResult.MAJORITY_TIMEOUT: "3", TransactionResult.DETERMINISTIC_VIOLATION: "4", TransactionResult.NO_MAJORITY: "5", - TransactionResult.MAJORITY_AGREE: "6", - TransactionResult.MAJORITY_DISAGREE: "7", } @@ -117,12 +117,16 @@ class ExecutionResult(str, Enum): NOT_VOTED = "NOT_VOTED" FINISHED_WITH_RETURN = "FINISHED_WITH_RETURN" FINISHED_WITH_ERROR = "FINISHED_WITH_ERROR" + TIMEOUT = "TIMEOUT" + NONDET_DISAGREE = "NONDET_DISAGREE" EXECUTION_RESULT_NUMBER_TO_NAME = { "0": ExecutionResult.NOT_VOTED, "1": ExecutionResult.FINISHED_WITH_RETURN, "2": ExecutionResult.FINISHED_WITH_ERROR, + "3": ExecutionResult.TIMEOUT, + "4": ExecutionResult.NONDET_DISAGREE, } diff --git a/genlayer_py/vesting/__init__.py b/genlayer_py/vesting/__init__.py new file mode 100644 index 0000000..ad7d320 --- /dev/null +++ b/genlayer_py/vesting/__init__.py @@ -0,0 +1,51 @@ +from genlayer_py.vesting.actions import ( + vesting_delegator_join, + vesting_delegator_exit, + vesting_delegator_claim, + vesting_validator_join, + vesting_validator_deposit, + vesting_validator_exit, + vesting_validator_claim, + vesting_validator_initiate_operator_transfer, + vesting_validator_complete_operator_transfer, + vesting_validator_cancel_operator_transfer, + vesting_validator_set_identity, + vesting_withdraw, + vested_amount, + unvested_amount, + withdrawable_amount, + get_vesting_schedule, + get_vesting_state, + get_vesting_stake_info, + get_validator_wallets, + validator_wallet_count, + validator_deposited, + is_validator_wallet, + get_vesting_contract, +) + +__all__ = [ + "vesting_delegator_join", + "vesting_delegator_exit", + "vesting_delegator_claim", + "vesting_validator_join", + "vesting_validator_deposit", + "vesting_validator_exit", + "vesting_validator_claim", + "vesting_validator_initiate_operator_transfer", + "vesting_validator_complete_operator_transfer", + "vesting_validator_cancel_operator_transfer", + "vesting_validator_set_identity", + "vesting_withdraw", + "vested_amount", + "unvested_amount", + "withdrawable_amount", + "get_vesting_schedule", + "get_vesting_state", + "get_vesting_stake_info", + "get_validator_wallets", + "validator_wallet_count", + "validator_deposited", + "is_validator_wallet", + "get_vesting_contract", +] diff --git a/genlayer_py/vesting/abi/__init__.py b/genlayer_py/vesting/abi/__init__.py new file mode 100644 index 0000000..34c0257 --- /dev/null +++ b/genlayer_py/vesting/abi/__init__.py @@ -0,0 +1,9 @@ +import json +import importlib.resources + +with importlib.resources.as_file( + importlib.resources.files("genlayer_py.vesting.abi").joinpath("vesting_abi.json") +) as path, open(path, "r", encoding="utf-8") as f: + VESTING_ABI = json.load(f) + +__all__ = ["VESTING_ABI"] diff --git a/genlayer_py/vesting/abi/vesting_abi.json b/genlayer_py/vesting/abi/vesting_abi.json new file mode 100644 index 0000000..4ea485e --- /dev/null +++ b/genlayer_py/vesting/abi/vesting_abi.json @@ -0,0 +1,925 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "DelegatorJoined", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "DelegatorExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "returned", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "rewardOrLoss", + "type": "int256" + } + ], + "name": "DelegatorClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ValidatorJoined", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ValidatorDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "ValidatorExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "returned", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "rewardOrLoss", + "type": "int256" + } + ], + "name": "ValidatorClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beneficiary", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "vestingContract", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum IVesting.Category", + "name": "category", + "type": "uint8" + } + ], + "name": "VestingCreated", + "type": "event" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "category", + "outputs": [ + { + "internalType": "enum IVesting.Category", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "beneficiary", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "creator", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "revoker", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "startDate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cliffDuration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "periodDuration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "numberOfPeriods", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cliffUnlockBps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "needsManualUnlock", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "manualUnlocked", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "revoked", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestingStopped", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalWithdrawn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestedAtRevocation", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalAmountAtRevocation", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "revokedAt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestingStoppedAt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestedAtStop", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "accumulatedRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "accumulatedLosses", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vestedAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unvestedAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawableAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + } + ], + "name": "depositedPerValidator", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + } + ], + "name": "pendingExitDeposited", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "validatorWallets", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "isValidatorWallet", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "validatorDeposited", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getValidatorWallets", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "validatorWalletCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "vestingWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "vestingDelegatorJoin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "vestingDelegatorExit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + } + ], + "name": "vestingDelegatorClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "vestingValidatorJoin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "vestingValidatorDeposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "vestingValidatorExit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "vestingValidatorClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "internalType": "address", + "name": "newOperator", + "type": "address" + } + ], + "name": "vestingValidatorInitiateOperatorTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "vestingValidatorCompleteOperatorTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + } + ], + "name": "vestingValidatorCancelOperatorTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "wallet", + "type": "address" + }, + { + "internalType": "string", + "name": "moniker", + "type": "string" + }, + { + "internalType": "string", + "name": "logoUri", + "type": "string" + }, + { + "internalType": "string", + "name": "website", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "string", + "name": "email", + "type": "string" + }, + { + "internalType": "string", + "name": "twitter", + "type": "string" + }, + { + "internalType": "string", + "name": "telegram", + "type": "string" + }, + { + "internalType": "string", + "name": "github", + "type": "string" + }, + { + "internalType": "bytes", + "name": "extraCid", + "type": "bytes" + } + ], + "name": "vestingValidatorSetIdentity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_beneficiary", + "type": "address" + } + ], + "name": "getVesting", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/genlayer_py/vesting/actions.py b/genlayer_py/vesting/actions.py new file mode 100644 index 0000000..26bcfc8 --- /dev/null +++ b/genlayer_py/vesting/actions.py @@ -0,0 +1,479 @@ +"""Vesting actions for GenLayerClient. + +Mirrors the genlayer_py.staking actions module. Beneficiary write +methods operate on a per-beneficiary Vesting contract address. Factory +discovery methods operate on a VestingFactory contract address. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Optional, Union + +from eth_account.signers.local import LocalAccount +from eth_typing import Address, ChecksumAddress +from hexbytes import HexBytes + +from genlayer_py.exceptions import GenLayerError +from genlayer_py.vesting.abi import VESTING_ABI + +if TYPE_CHECKING: + from genlayer_py.client import GenLayerClient + + +AddressLike = Union[Address, ChecksumAddress, str] +ExtraCidLike = Union[str, bytes, bytearray, HexBytes] + + +def _vesting_address( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> ChecksumAddress: + return self.w3.to_checksum_address(vesting_contract_address) + + +def _vesting(self: "GenLayerClient", vesting_contract_address: AddressLike): + return self.w3.eth.contract( + address=_vesting_address(self, vesting_contract_address), abi=VESTING_ABI + ) + + +def _vesting_factory(self: "GenLayerClient", vesting_factory_address: AddressLike): + return self.w3.eth.contract( + address=self.w3.to_checksum_address(vesting_factory_address), + abi=VESTING_ABI, + ) + + +def _extra_cid(extra_cid: Optional[ExtraCidLike]) -> bytes: + if not extra_cid: + return b"" + if isinstance(extra_cid, str): + if extra_cid.startswith("0x"): + return bytes(HexBytes(extra_cid)) + return extra_cid.encode() + return bytes(extra_cid) + + +def _sender( + self: "GenLayerClient", account: Optional[LocalAccount] +) -> LocalAccount: + acct = account or self.local_account + if acct is None: + raise GenLayerError("No account provided and client has no local_account") + return acct + + +def _send( + self: "GenLayerClient", + account: LocalAccount, + tx: dict, +) -> HexBytes: + """Sign and broadcast a prepared transaction dict.""" + signed = account.sign_transaction(tx) + return self.w3.eth.send_raw_transaction(signed.raw_transaction) + + +def _build( + self: "GenLayerClient", + account: LocalAccount, + to: ChecksumAddress, + data: bytes, + value: int = 0, + gas: Optional[int] = None, +) -> dict: + tx = { + "from": account.address, + "to": to, + "data": data, + "value": value, + "nonce": self.w3.eth.get_transaction_count(account.address), + "chainId": self.chain.id, + } + # Lean on the node's eth_estimateGas unless caller overrode it. + tx["gas"] = gas if gas is not None else self.w3.eth.estimate_gas(tx) * 2 + tx["gasPrice"] = self.w3.eth.gas_price + return tx + + +# --- read methods ----------------------------------------------------- + + +def vested_amount( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> int: + return _vesting(self, vesting_contract_address).functions.vestedAmount().call() + + +def unvested_amount( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> int: + return _vesting(self, vesting_contract_address).functions.unvestedAmount().call() + + +def withdrawable_amount( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> int: + return ( + _vesting(self, vesting_contract_address) + .functions.withdrawableAmount() + .call() + ) + + +def get_vesting_schedule( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> dict: + contract = _vesting(self, vesting_contract_address) + return { + "name": contract.functions.name().call(), + "category": contract.functions.category().call(), + "beneficiary": contract.functions.beneficiary().call(), + "creator": contract.functions.creator().call(), + "revoker": contract.functions.revoker().call(), + "factory": contract.functions.factory().call(), + "total_amount": contract.functions.totalAmount().call(), + "start_date": contract.functions.startDate().call(), + "cliff_duration": contract.functions.cliffDuration().call(), + "period_duration": contract.functions.periodDuration().call(), + "number_of_periods": contract.functions.numberOfPeriods().call(), + "cliff_unlock_bps": contract.functions.cliffUnlockBps().call(), + "needs_manual_unlock": contract.functions.needsManualUnlock().call(), + } + + +def get_vesting_state( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> dict: + contract = _vesting(self, vesting_contract_address) + return { + "manual_unlocked": contract.functions.manualUnlocked().call(), + "revoked": contract.functions.revoked().call(), + "vesting_stopped": contract.functions.vestingStopped().call(), + "total_withdrawn": contract.functions.totalWithdrawn().call(), + "vested_at_revocation": contract.functions.vestedAtRevocation().call(), + "total_amount_at_revocation": contract.functions.totalAmountAtRevocation() + .call(), + "revoked_at": contract.functions.revokedAt().call(), + "vesting_stopped_at": contract.functions.vestingStoppedAt().call(), + "vested_at_stop": contract.functions.vestedAtStop().call(), + "accumulated_rewards": contract.functions.accumulatedRewards().call(), + "accumulated_losses": contract.functions.accumulatedLosses().call(), + "vested_amount": contract.functions.vestedAmount().call(), + "unvested_amount": contract.functions.unvestedAmount().call(), + "withdrawable_amount": contract.functions.withdrawableAmount().call(), + } + + +def get_vesting_stake_info( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + validator: AddressLike, +) -> dict: + contract = _vesting(self, vesting_contract_address) + validator_address = self.w3.to_checksum_address(validator) + return { + "deposited": contract.functions.depositedPerValidator( + validator_address + ).call(), + "pending_exit_deposited": contract.functions.pendingExitDeposited( + validator_address + ).call(), + } + + +def get_validator_wallets( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> List[ChecksumAddress]: + return ( + _vesting(self, vesting_contract_address) + .functions.getValidatorWallets() + .call() + ) + + +def validator_wallet_count( + self: "GenLayerClient", vesting_contract_address: AddressLike +) -> int: + return ( + _vesting(self, vesting_contract_address) + .functions.validatorWalletCount() + .call() + ) + + +def validator_deposited( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, +) -> int: + return ( + _vesting(self, vesting_contract_address) + .functions.validatorDeposited(self.w3.to_checksum_address(wallet)) + .call() + ) + + +def is_validator_wallet( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, +) -> bool: + return ( + _vesting(self, vesting_contract_address) + .functions.isValidatorWallet(self.w3.to_checksum_address(wallet)) + .call() + ) + + +def get_vesting_contract( + self: "GenLayerClient", + vesting_factory_address: AddressLike, + beneficiary: AddressLike, +) -> ChecksumAddress: + contract = _vesting_factory(self, vesting_factory_address) + vesting_contract_address = contract.functions.getVesting( + self.w3.to_checksum_address(beneficiary) + ).call() + return self.w3.to_checksum_address(vesting_contract_address) + + +# --- write methods ---------------------------------------------------- + + +def vesting_delegator_join( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + validator: AddressLike, + amount: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Delegates `amount` GEN from a Vesting contract to a validator.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingDelegatorJoin", + args=[self.w3.to_checksum_address(validator), amount], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_delegator_exit( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + validator: AddressLike, + shares: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Burns `shares` of a Vesting contract's delegation position.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingDelegatorExit", + args=[self.w3.to_checksum_address(validator), shares], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_delegator_claim( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + validator: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Claims exited staking funds back into the Vesting contract.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingDelegatorClaim", args=[self.w3.to_checksum_address(validator)] + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_join( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + operator: AddressLike, + amount: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Creates a validator wallet using GEN from a Vesting contract.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorJoin", + args=[self.w3.to_checksum_address(operator), amount], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_deposit( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + amount: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Adds Vesting-held GEN to one of the Vesting validator wallets.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorDeposit", + args=[self.w3.to_checksum_address(wallet), amount], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_exit( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + shares: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Burns `shares` from a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorExit", + args=[self.w3.to_checksum_address(wallet), shares], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_claim( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Claims exited validator self-stake back into the Vesting contract.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorClaim", args=[self.w3.to_checksum_address(wallet)] + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_initiate_operator_transfer( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + new_operator: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Begins operator transfer for a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorInitiateOperatorTransfer", + args=[ + self.w3.to_checksum_address(wallet), + self.w3.to_checksum_address(new_operator), + ], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_complete_operator_transfer( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Completes operator transfer for a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorCompleteOperatorTransfer", + args=[self.w3.to_checksum_address(wallet)], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_cancel_operator_transfer( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Cancels operator transfer for a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorCancelOperatorTransfer", + args=[self.w3.to_checksum_address(wallet)], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_validator_set_identity( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + wallet: AddressLike, + moniker: str, + logo_uri: str, + website: str, + description: str, + email: str, + twitter: str, + telegram: str, + github: str, + extra_cid: Optional[ExtraCidLike] = None, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Sets identity metadata on a Vesting-owned validator wallet.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi( + "vestingValidatorSetIdentity", + args=[ + self.w3.to_checksum_address(wallet), + moniker, + logo_uri, + website, + description, + email, + twitter, + telegram, + github, + _extra_cid(extra_cid), + ], + ) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) + + +def vesting_withdraw( + self: "GenLayerClient", + vesting_contract_address: AddressLike, + amount: int, + account: Optional[LocalAccount] = None, +) -> HexBytes: + """Withdraws vested tokens from a Vesting contract to its beneficiary.""" + sender = _sender(self, account) + contract = _vesting(self, vesting_contract_address) + data = contract.encode_abi("vestingWithdraw", args=[amount]) + vesting_address = _vesting_address(self, vesting_contract_address) + tx = _build(self, sender, vesting_address, data) + return _send(self, sender, tx) diff --git a/pyproject.toml b/pyproject.toml index c8002e3..8b401c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "genlayer-py" -version = "0.18.0" +version = "0.19.0" description = "GenLayer Python SDK" authors = [ { name = "GenLayer" } @@ -28,7 +28,7 @@ where = ["."] exclude = ["tests*", "scripts*"] [tool.setuptools.package-data] -"genlayer_py" = ["consensus/abi/*.json", "staking/abi/*.json"] +"genlayer_py" = ["consensus/abi/*.json", "staking/abi/*.json", "vesting/abi/*.json"] [dependency-groups] dev = [ diff --git a/support/ci/ACTIVE_DEV_BRANCH b/support/ci/ACTIVE_DEV_BRANCH new file mode 100644 index 0000000..101debc --- /dev/null +++ b/support/ci/ACTIVE_DEV_BRANCH @@ -0,0 +1 @@ +v0.19-dev diff --git a/tests/e2e/contracts/football_prediction_market.py b/tests/e2e/contracts/football_prediction_market.py index 758c9ef..8e7e2af 100644 --- a/tests/e2e/contracts/football_prediction_market.py +++ b/tests/e2e/contracts/football_prediction_market.py @@ -1,12 +1,13 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * import json import typing -class PredictionMarket(gl.Contract): +class PredictionMarket(gl.contract.Contract): has_resolved: bool team1: str team2: str @@ -50,7 +51,7 @@ def resolve(self) -> typing.Any: team2 = self.team2 def get_match_result() -> str: - web_data = gl.get_webpage(market_resolution_url, mode="text") + web_data = gl.nondet.web.render(market_resolution_url, mode="text") print(web_data) task = f""" @@ -75,11 +76,11 @@ def get_match_result() -> str: your output must be only JSON without any formatting prefix or suffix. This result should be perfectly parsable by a JSON parser without errors. """ - result = gl.exec_prompt(task).replace("```json", "").replace("```", "") + result = gl.nondet.exec_prompt(task).replace("```json", "").replace("```", "") print(result) return json.dumps(json.loads(result), sort_keys=True) - result_json = json.loads(gl.eq_principle_strict_eq(get_match_result)) + result_json = json.loads(gl.eq_principle.strict_eq(get_match_result)) if result_json["winner"] > -1: self.has_resolved = True diff --git a/tests/e2e/contracts/intelligent_oracle.py b/tests/e2e/contracts/intelligent_oracle.py index ef49eb0..febbc20 100644 --- a/tests/e2e/contracts/intelligent_oracle.py +++ b/tests/e2e/contracts/intelligent_oracle.py @@ -4,7 +4,8 @@ from enum import Enum from datetime import datetime, timezone from urllib.parse import urlparse -from genlayer import * +import genlayer as gl +from genlayer.types import * class Status(Enum): @@ -13,15 +14,15 @@ class Status(Enum): ERROR = "Error" -class IntelligentOracle(gl.Contract): +class IntelligentOracle(gl.contract.Contract): # Declare persistent storage fields prediction_market_id: str title: str description: str - potential_outcomes: DynArray[str] - rules: DynArray[str] - data_source_domains: DynArray[str] - resolution_urls: DynArray[str] + potential_outcomes: gl.storage.DynArray[str] + rules: gl.storage.DynArray[str] + data_source_domains: gl.storage.DynArray[str] + resolution_urls: gl.storage.DynArray[str] earliest_resolution_date: str # Store as ISO format string status: str # Store as string since Enum isn't supported analysis: str # Store analysis results @@ -139,7 +140,7 @@ def resolve(self, evidence_url: str = "") -> None: for resource_url in resources_to_check: def evaluate_single_source() -> str: - resource_web_data = gl.get_webpage(resource_url, mode="text") + resource_web_data = gl.nondet.web.render(resource_url, mode="text") print(resource_web_data) task = f""" @@ -223,11 +224,11 @@ def evaluate_single_source() -> str: - **Clarity:** Make sure your reasoning is easy to understand. - **Validity:** Ensure the JSON output is properly formatted and free of errors. Do not include trailing commas. """ - result = gl.exec_prompt(task) + result = gl.nondet.exec_prompt(task) print(result) return result - result = gl.eq_principle_prompt_comparative( + result = gl.eq_principle.prompt_comparative( evaluate_single_source, principle="`outcome` field must be exactly the same. All other fields must be similar", ) @@ -305,11 +306,11 @@ def evaluate_all_sources() -> str: """ - result = gl.exec_prompt(task) + result = gl.nondet.exec_prompt(task) print(result) return result - result = gl.eq_principle_prompt_comparative( + result = gl.eq_principle.prompt_comparative( evaluate_all_sources, principle="`outcome` field must be exactly the same. All other fields must be similar", ) diff --git a/tests/e2e/contracts/intelligent_oracle_factory.py b/tests/e2e/contracts/intelligent_oracle_factory.py index 9cf5e3c..db54513 100644 --- a/tests/e2e/contracts/intelligent_oracle_factory.py +++ b/tests/e2e/contracts/intelligent_oracle_factory.py @@ -1,11 +1,12 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class Registry(gl.Contract): +class Registry(gl.contract.Contract): # Declare persistent storage fields - contract_addresses: DynArray[str] + contract_addresses: gl.storage.DynArray[str] intelligent_oracle_code: str def __init__(self, intelligent_oracle_code: str): @@ -24,7 +25,7 @@ def create_new_prediction_market( earliest_resolution_date: str, ) -> None: registered_contracts = len(self.contract_addresses) - contract_address = gl.deploy_contract( + contract_address = gl.contract.deploy( code=self.intelligent_oracle_code.encode("utf-8"), args=[ prediction_market_id, diff --git a/tests/e2e/contracts/llm_erc20.py b/tests/e2e/contracts/llm_erc20.py index dd28981..6dcdc1b 100644 --- a/tests/e2e/contracts/llm_erc20.py +++ b/tests/e2e/contracts/llm_erc20.py @@ -2,14 +2,15 @@ import json -from genlayer import * +import genlayer as gl +from genlayer.types import * -class LlmErc20(gl.Contract): - balances: TreeMap[Address, u256] +class LlmErc20(gl.contract.Contract): + balances: gl.storage.TreeMap[Address, u256] def __init__(self, total_supply: int) -> None: - self.balances[gl.message.sender_address] = u256(total_supply) + self.balances[gl.message.sender_address] = total_supply @gl.public.write def transfer(self, amount: int, to_address: str) -> None: @@ -47,7 +48,7 @@ def transfer(self, amount: int, to_address: str) -> None: The total sum of all balances should remain the same before and after the transaction""" final_result = ( - gl.eq_principle_prompt_non_comparative( + gl.eq_principle.prompt_non_comparative( lambda: input, task=task, criteria=criteria, diff --git a/tests/e2e/contracts/log_indexer.py b/tests/e2e/contracts/log_indexer.py index 4d5dd56..040c736 100644 --- a/tests/e2e/contracts/log_indexer.py +++ b/tests/e2e/contracts/log_indexer.py @@ -6,14 +6,15 @@ # } import numpy as np -from genlayer import * +import genlayer as gl +from genlayer.types import * import genlayer_embeddings as gle from dataclasses import dataclass import typing -@allow_storage +@gl.storage.allow @dataclass class StoreValue: log_id: u256 @@ -21,7 +22,7 @@ class StoreValue: # contract class -class LogIndexer(gl.Contract): +class LogIndexer(gl.contract.Contract): vector_store: gle.VecDB[np.float32, typing.Literal[384], StoreValue] def __init__(self): @@ -52,14 +53,14 @@ def get_closest_vector(self, text: str) -> dict | None: @gl.public.write def add_log(self, log: str, log_id: int) -> None: emb = self.get_embedding(log) - self.vector_store.insert(emb, StoreValue(text=log, log_id=u256(log_id))) + self.vector_store.insert(emb, StoreValue(text=log, log_id=log_id)) @gl.public.write def update_log(self, log_id: int, log: str) -> None: emb = self.get_embedding(log) for elem in self.vector_store.knn(emb, 2): if elem.value.text == log: - elem.value.log_id = u256(log_id) + elem.value.log_id = log_id @gl.public.write def remove_log(self, id: int) -> None: diff --git a/tests/e2e/contracts/log_indexer_testnet.py b/tests/e2e/contracts/log_indexer_testnet.py index 85f396a..e0d6878 100644 --- a/tests/e2e/contracts/log_indexer_testnet.py +++ b/tests/e2e/contracts/log_indexer_testnet.py @@ -6,13 +6,14 @@ # } import numpy as np -from genlayer import * +import genlayer as gl +from genlayer.types import * import genlayermodelwrappers from dataclasses import dataclass import typing -@allow_storage +@gl.storage.allow @dataclass class StoreValue: log_id: u256 @@ -20,8 +21,8 @@ class StoreValue: # contract class -class LogIndexer(gl.Contract): - vector_store: VecDB[np.float32, typing.Literal[384], StoreValue] +class LogIndexer(gl.contract.Contract): + vector_store: genlayermodelwrappers.VecDB[np.float32, typing.Literal[384], StoreValue] def __init__(self): pass @@ -51,14 +52,14 @@ def get_closest_vector(self, text: str) -> dict | None: @gl.public.write def add_log(self, log: str, log_id: int) -> None: emb = self.get_embedding(log) - self.vector_store.insert(emb, StoreValue(text=log, log_id=u256(log_id))) + self.vector_store.insert(emb, StoreValue(text=log, log_id=log_id)) @gl.public.write def update_log(self, log_id: int, log: str) -> None: emb = self.get_embedding(log) for elem in self.vector_store.knn(emb, 2): if elem.value.text == log: - elem.value.log_id = u256(log_id) + elem.value.log_id = log_id @gl.public.write def remove_log(self, id: int) -> None: diff --git a/tests/e2e/contracts/multi_file_contract/__init__.py b/tests/e2e/contracts/multi_file_contract/__init__.py index a5258f6..f218200 100644 --- a/tests/e2e/contracts/multi_file_contract/__init__.py +++ b/tests/e2e/contracts/multi_file_contract/__init__.py @@ -1,13 +1,14 @@ -from genlayer import * +import genlayer as gl +from genlayer.types import * -class MultiFileContract(gl.Contract): +class MultiFileContract(gl.contract.Contract): other_addr: Address def __init__(self): with open("/contract/other.py", "rt") as f: text = f.read() - self.other_addr = gl.deploy_contract( + self.other_addr = gl.contract.deploy( code=text.encode("utf-8"), args=["123"], salt_nonce=1 ) @@ -17,4 +18,4 @@ def wait(self) -> None: @gl.public.view def test(self) -> str: - return gl.ContractAt(self.other_addr).view().test() + return gl.contract.get_at(self.other_addr).view().test() diff --git a/tests/e2e/contracts/multi_file_contract/other.py b/tests/e2e/contracts/multi_file_contract/other.py index 330de34..c8dc6c7 100644 --- a/tests/e2e/contracts/multi_file_contract/other.py +++ b/tests/e2e/contracts/multi_file_contract/other.py @@ -1,9 +1,10 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class Other(gl.Contract): +class Other(gl.contract.Contract): data: str def __init__(self, data: str): diff --git a/tests/e2e/contracts/multi_read_erc20.py b/tests/e2e/contracts/multi_read_erc20.py index 169d293..22d5b32 100644 --- a/tests/e2e/contracts/multi_read_erc20.py +++ b/tests/e2e/contracts/multi_read_erc20.py @@ -1,10 +1,11 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class multi_read_erc20(gl.Contract): - balances: TreeMap[Address, TreeMap[Address, u256]] +class multi_read_erc20(gl.contract.Contract): + balances: gl.storage.TreeMap[Address, gl.storage.TreeMap[Address, u256]] def __init__(self): pass @@ -14,7 +15,7 @@ def update_token_balances( self, account_address: str, token_contracts: list[str] ) -> None: for token_contract in token_contracts: - contract = gl.ContractAt(Address(token_contract)) + contract = gl.contract.get_at(Address(token_contract)) balance = contract.view().get_balance_of(account_address) self.balances.get_or_insert_default(Address(account_address))[ Address(token_contract) diff --git a/tests/e2e/contracts/multi_tenant_storage.py b/tests/e2e/contracts/multi_tenant_storage.py index 5e5af47..8659437 100644 --- a/tests/e2e/contracts/multi_tenant_storage.py +++ b/tests/e2e/contracts/multi_tenant_storage.py @@ -1,9 +1,10 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class MultiTentantStorage(gl.Contract): +class MultiTentantStorage(gl.contract.Contract): """ Same functionality as UserStorage, but implemented with multiple storage contracts. Each user is assigned to a storage contract, and all storage contracts are managed by this same contract. @@ -11,9 +12,9 @@ class MultiTentantStorage(gl.Contract): This is done to test contract calls between different contracts. """ - all_storage_contracts: DynArray[Address] - available_storage_contracts: DynArray[Address] - mappings: TreeMap[ + all_storage_contracts: gl.storage.DynArray[Address] + available_storage_contracts: gl.storage.DynArray[Address] + mappings: gl.storage.TreeMap[ Address, Address ] # mapping of user address to storage contract address @@ -29,7 +30,7 @@ def get_available_contracts(self) -> list[str]: @gl.public.view def get_all_storages(self) -> dict[str, str]: return { - storage_contract.as_hex: gl.ContractAt(storage_contract) + storage_contract.as_hex: gl.contract.get_at(storage_contract) .view() .get_storage() for storage_contract in self.all_storage_contracts @@ -45,4 +46,4 @@ def update_storage(self, new_storage: str) -> None: self.available_storage_contracts.pop() contract_to_use = self.mappings[gl.message.sender_address] - gl.ContractAt(contract_to_use).emit(gas=100000).update_storage(new_storage) + gl.contract.get_at(contract_to_use).emit().update_storage(new_storage) diff --git a/tests/e2e/contracts/read_erc20.py b/tests/e2e/contracts/read_erc20.py index 9f2d549..3883c5f 100644 --- a/tests/e2e/contracts/read_erc20.py +++ b/tests/e2e/contracts/read_erc20.py @@ -1,9 +1,10 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class read_erc20(gl.Contract): +class read_erc20(gl.contract.Contract): token_contract: Address def __init__(self, token_contract: str): @@ -11,4 +12,4 @@ def __init__(self, token_contract: str): @gl.public.view def get_balance_of(self, account_address: str) -> int: - return gl.ContractAt(self.token_contract).view().get_balance_of(account_address) + return gl.contract.get_at(self.token_contract).view().get_balance_of(account_address) diff --git a/tests/e2e/contracts/simple_time_contract.py b/tests/e2e/contracts/simple_time_contract.py index 05bfbc0..974e801 100644 --- a/tests/e2e/contracts/simple_time_contract.py +++ b/tests/e2e/contracts/simple_time_contract.py @@ -6,10 +6,11 @@ # } from datetime import datetime, timezone -from genlayer import * +import genlayer as gl +from genlayer.types import * -class SimpleTimeContract(gl.Contract): +class SimpleTimeContract(gl.contract.Contract): """ A simple contract that demonstrates time-based function availability. """ diff --git a/tests/e2e/contracts/storage.py b/tests/e2e/contracts/storage.py index 6cc44a9..58ff12a 100644 --- a/tests/e2e/contracts/storage.py +++ b/tests/e2e/contracts/storage.py @@ -1,10 +1,11 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * # contract class -class Storage(gl.Contract): +class Storage(gl.contract.Contract): storage: str # constructor diff --git a/tests/e2e/contracts/user_storage.py b/tests/e2e/contracts/user_storage.py index 3ca9fd8..3432396 100644 --- a/tests/e2e/contracts/user_storage.py +++ b/tests/e2e/contracts/user_storage.py @@ -1,10 +1,11 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * -class UserStorage(gl.Contract): - storage: TreeMap[Address, str] +class UserStorage(gl.contract.Contract): + storage: gl.storage.TreeMap[Address, str] # constructor def __init__(self): diff --git a/tests/e2e/contracts/wizard_of_coin.py b/tests/e2e/contracts/wizard_of_coin.py index 6194818..bff7e3f 100644 --- a/tests/e2e/contracts/wizard_of_coin.py +++ b/tests/e2e/contracts/wizard_of_coin.py @@ -1,10 +1,11 @@ # { "Depends": "py-genlayer:test" } -from genlayer import * +import genlayer as gl +from genlayer.types import * import json -class WizardOfCoin(gl.Contract): +class WizardOfCoin(gl.contract.Contract): have_coin: bool def __init__(self, have_coin: bool): @@ -39,12 +40,12 @@ def ask_for_coin(self, request: str) -> None: """ def get_wizard_answer(): - result = gl.exec_prompt(prompt) + result = gl.nondet.exec_prompt(prompt) result = result.replace("```json", "").replace("```", "") print(result) return result - result = gl.eq_principle_prompt_comparative( + result = gl.eq_principle.prompt_comparative( get_wizard_answer, "The value of give_coin has to match" ) parsed_result = json.loads(result) diff --git a/tests/unit/account/test_accounts.py b/tests/unit/account/test_accounts.py index 4e0a22c..b3ec88c 100644 --- a/tests/unit/account/test_accounts.py +++ b/tests/unit/account/test_accounts.py @@ -1,7 +1,10 @@ from eth_account import Account from eth_account.signers.local import LocalAccount +from types import SimpleNamespace +from unittest.mock import Mock from genlayer_py.accounts.account import generate_private_key, create_account +from genlayer_py.accounts.actions import get_current_nonce def test_generate_private_key(): @@ -54,3 +57,25 @@ def test_create_account_with_none_private_key(): assert ( account1.address != account2.address ) # Different addresses since different random keys + + +def test_get_current_nonce_includes_pending_transactions_by_default(): + address = "0x0000000000000000000000000000000000000001" + client = SimpleNamespace( + account=SimpleNamespace(address=address), + get_transaction_count=Mock(return_value=7), + ) + + assert get_current_nonce(client) == 7 + client.get_transaction_count.assert_called_once_with(address, "pending") + + +def test_get_current_nonce_preserves_explicit_block_identifier(): + address = "0x0000000000000000000000000000000000000001" + client = SimpleNamespace( + account=None, + get_transaction_count=Mock(return_value=3), + ) + + assert get_current_nonce(client, address, "latest") == 3 + client.get_transaction_count.assert_called_once_with(address, "latest") diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index e182e24..ff06a60 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -101,7 +101,7 @@ def sample_transaction_data(): "created_at": "2025-07-22T19:58:39.866436+00:00", "data": { "calldata": { - "readable": '{"args":[500,"0x4C76986555e8C63bD0D9CFAFbf8e68C338556b6b"],"method":"transfer"}' + "readable": '{"":"transfer","args":[500,"0x4C76986555e8C63bD0D9CFAFbf8e68C338556b6b"]}' } }, "contract_snapshot": { diff --git a/tests/unit/consensus/test_consensus_main.py b/tests/unit/consensus/test_consensus_main.py index dd37be8..ccaf909 100644 --- a/tests/unit/consensus/test_consensus_main.py +++ b/tests/unit/consensus/test_consensus_main.py @@ -3,6 +3,7 @@ encode_tx_data_call, encode_tx_data_deploy, ) +from genlayer_py.abi import calldata from genlayer_py.consensus.consensus_main import decode_add_transaction_data from genlayer_py.transactions import MessageType from web3 import Web3 @@ -39,7 +40,7 @@ def test_codec_add_transaction_data_1(): ) assert decoded_data["num_of_initial_validators"] == 10 assert decoded_data["max_rotations"] == 10 - assert decoded_data["tx_data"]["decoded"]["call_data"]["method"] == "addTransaction" + assert decoded_data["tx_data"]["decoded"]["call_data"][""] == "addTransaction" assert decoded_data["tx_data"]["decoded"]["leader_only"] is False assert decoded_data["tx_data"]["decoded"]["type"] == "call" @@ -73,7 +74,7 @@ def test_codec_add_transaction_data_2(): ) assert decoded_data["num_of_initial_validators"] == 5 assert decoded_data["max_rotations"] == 1 - assert decoded_data["tx_data"]["decoded"]["call_data"]["method"] == "set_store" + assert decoded_data["tx_data"]["decoded"]["call_data"][""] == "set_store" assert decoded_data["tx_data"]["decoded"]["leader_only"] is False assert decoded_data["tx_data"]["decoded"]["type"] == "call" @@ -158,4 +159,28 @@ def test_codec_fee_aware_add_transaction_data_round_trip(): assert decoded_data["fees_distribution"]["totalMessageFees"] == 11 assert decoded_data["message_allocations"][0]["messageType"] == MessageType.Internal assert decoded_data["message_allocations"][0]["onAcceptance"] is False - assert decoded_data["tx_data"]["decoded"]["call_data"]["method"] == "set_store" + assert decoded_data["tx_data"]["decoded"]["call_data"][""] == "set_store" + + +def test_method_key_is_empty_string_binary_prefix(): + tx_data = encode_tx_data_call( + function_name="my_method", + leader_only=False, + args=["value"], + kwargs={}, + ) + encoded_data = encode_add_transaction_data( + sender_address="0x3d338dea364bdac3c4c3036a38766870b98c4320", + recipient_address="0x7f3ebb777cd2ae9c266d6cea2c7a3ed81c30ddc2", + num_of_initial_validators=5, + max_rotations=1, + tx_data=tx_data.hex() if isinstance(tx_data, bytes) else tx_data, + ) + + decoded_data = decode_add_transaction_data(encoded_data) + call_data = decoded_data["tx_data"]["decoded"]["call_data"] + assert "" in call_data and call_data[""] == "my_method" + assert 'method' not in call_data + + raw = calldata.encode(call_data) + assert b"my_method" in raw and raw.index(b"my_method") < raw.index(b"args") diff --git a/tests/unit/contracts/test_contract_actions.py b/tests/unit/contracts/test_contract_actions.py index cc58d44..c0c6de9 100644 --- a/tests/unit/contracts/test_contract_actions.py +++ b/tests/unit/contracts/test_contract_actions.py @@ -2,17 +2,21 @@ from unittest.mock import Mock import eth_utils +import pytest from eth_abi import decode as abi_decode from web3 import Web3 import genlayer_py.contracts.actions as contract_actions from genlayer_py.chains import localnet +from genlayer_py.chains.testnet_asimov import testnet_asimov +from genlayer_py.exceptions import GenLayerError from genlayer_py.transactions.fees import ( ADD_TRANSACTION_WITH_FEES_ARGUMENT_TYPES, ADD_TRANSACTION_WITH_FEES_SELECTOR, CALL_KEY_DEPLOY, CALL_KEY_UNNAMED, CALL_KEY_WILDCARD, + DEPLOY_CALL_KEY, FEES_DISTRIBUTION_ABI_TYPE, MESSAGE_ALLOCATION_ROOT_PARENT_INDEX, MessageType, @@ -21,6 +25,7 @@ create_fees_distribution, derive_external_message_call_key, derive_internal_message_call_key, + deploy_call_key, encode_external_message_fee_params, encode_internal_message_fee_params, extract_studio_fee_policy, @@ -28,6 +33,10 @@ ) +DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM = 10_000 +LOCAL_EXECUTION_BUDGET_FLOOR_GAS = 306_192 + + ADD_TRANSACTION_ABI_V5 = [ { "type": "function", @@ -152,7 +161,15 @@ def test_derive_internal_message_call_key_hashes_exact_32_byte_method_name(): def test_derive_message_call_key_constants_for_deploy_and_unnamed(): - assert CALL_KEY_DEPLOY == "0x" + "00" * 32 + # Wildcard is the untagged hash of empty bytes — outside the derived-key space. + assert CALL_KEY_WILDCARD == eth_utils.keccak(b"") + assert CALL_KEY_WILDCARD == bytes.fromhex( + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ) + # Empty name derives bytes32(0): the natural key for deploy and emit_transfer. + assert DEPLOY_CALL_KEY == "0x" + "00" * 32 + assert CALL_KEY_DEPLOY == DEPLOY_CALL_KEY + assert deploy_call_key() == DEPLOY_CALL_KEY assert CALL_KEY_UNNAMED == "0x" + "00" * 32 assert derive_internal_message_call_key("") == CALL_KEY_UNNAMED @@ -272,6 +289,87 @@ def fake_send_consensus_call(**kwargs): assert captured["encoded_data"].startswith(f"0x{selector}") +def test_appeal_transaction_auto_resolves_min_bond(monkeypatch): + client = _make_client(ADD_TRANSACTION_ABI_WITH_FEES) + client.chain.fee_manager_contract = {"address": "0x4444444444444444444444444444444444444444"} + client.chain.rounds_storage_contract = {"address": "0x5555555555555555555555555555555555555555"} + captured = {} + + monkeypatch.setattr( + contract_actions, + "get_min_appeal_bond", + Mock(return_value=4_321), + ) + monkeypatch.setattr( + contract_actions, + "_encode_submit_appeal_data", + Mock(return_value="0x1234"), + ) + + def fake_send_consensus_call(**kwargs): + captured.update(kwargs) + return "0xevmtx" + + monkeypatch.setattr( + contract_actions, + "_send_consensus_call", + fake_send_consensus_call, + ) + + result = contract_actions.appeal_transaction( + self=client, + transaction_id=TX_ID, + ) + + assert result == TX_ID + contract_actions.get_min_appeal_bond.assert_called_once_with(client, TX_ID) + assert captured["value"] == 4_321 + assert captured["operation_name"] == "Appeal" + + +def test_top_up_and_submit_appeal_auto_resolves_min_bond(monkeypatch): + client = _make_client(ADD_TRANSACTION_ABI_WITH_FEES) + client.chain.fee_manager_contract = {"address": "0x4444444444444444444444444444444444444444"} + client.chain.rounds_storage_contract = {"address": "0x5555555555555555555555555555555555555555"} + captured = {} + + monkeypatch.setattr( + contract_actions, + "get_min_appeal_bond", + Mock(return_value=9_999), + ) + + def fake_send_consensus_call(**kwargs): + captured.update(kwargs) + return "0xevmtx" + + monkeypatch.setattr( + contract_actions, + "_send_consensus_call", + fake_send_consensus_call, + ) + + result = contract_actions.top_up_and_submit_appeal( + self=client, + transaction_id=TX_ID, + distribution={"appealRounds": 1, "rotations": [0, 1]}, + ) + + assert result == TX_ID + assert captured["value"] == 9_999 + + +def test_appeal_auto_bond_requires_fee_contracts(): + client = _make_client(ADD_TRANSACTION_ABI_WITH_FEES) + client.chain.fee_manager_contract = None + + with pytest.raises(GenLayerError, match="Cannot auto-resolve appeal bond"): + contract_actions.appeal_transaction( + self=client, + transaction_id=TX_ID, + ) + + def test_send_consensus_call_returns_localnet_rpc_hash_without_waiting(monkeypatch): wait_for_transaction_receipt = Mock() sign_transaction = Mock( @@ -312,6 +410,14 @@ def test_send_consensus_call_returns_localnet_rpc_hash_without_waiting(monkeypat wait_for_transaction_receipt.assert_not_called() +def test_revert_selector_formatter_names_fee_errors(): + error = Exception({"data": "0x632be5a1"}) + + assert contract_actions._format_rpc_error(error).endswith( + "(FeeValueMustBeNonZero)" + ) + + def _make_client(add_transaction_abi): chain = SimpleNamespace( id=61999, @@ -330,6 +436,113 @@ def _make_client(add_transaction_abi): ) +class _FakeContractFunction: + def __init__(self, value): + self.value = value + + def call(self): + return self.value + + +class _FakeFeeManagerFunctions: + def __init__(self, values): + self.values = values + + def GENPerTimeUnit(self): + return _FakeContractFunction(self.values["gen"]) + + def storageUnitPrice(self): + return _FakeContractFunction(self.values["storage"]) + + def quoteGasPrice(self): + return _FakeContractFunction(self.values["quote"]) + + def messageFeeParamsBudgetFloor(self): + return _FakeContractFunction(self.values["floor"]) + + +class _FakeEth: + def __init__(self, values, gas_price): + self.values = values + self._gas_price = gas_price + self.gas_price_accesses = 0 + + def contract(self, address=None, abi=None): + return SimpleNamespace(functions=_FakeFeeManagerFunctions(self.values)) + + @property + def gas_price(self): + self.gas_price_accesses += 1 + if isinstance(self._gas_price, Exception): + raise self._gas_price + return self._gas_price + + +def _make_fee_policy_client(values, gas_price=0): + eth = _FakeEth(values, gas_price) + return SimpleNamespace( + chain=SimpleNamespace( + fee_manager_contract={"address": "0x4444444444444444444444444444444444444444"}, + ), + w3=SimpleNamespace( + eth=eth, + to_checksum_address=Mock(side_effect=lambda address: address), + ), + ) + + +def test_get_current_fee_policy_uses_effective_receipt_gas_price_and_local_floor(): + client = _make_fee_policy_client( + {"gen": 0, "storage": 0, "quote": 1, "floor": 0}, + gas_price=2, + ) + + policy = contract_actions.get_current_fee_policy(client) + + assert policy["enabled"] is True + assert policy["receiptGasPrice"] == 2 + assert policy["executionBudgetFloor"] == 2 * LOCAL_EXECUTION_BUDGET_FLOOR_GAS + assert client.w3.eth.gas_price_accesses == 1 + + +def test_get_current_fee_policy_refuses_enabled_zero_receipt_price(): + client = _make_fee_policy_client( + {"gen": 1, "storage": 0, "quote": 0, "floor": 0}, + gas_price=0, + ) + + with pytest.raises( + GenLayerError, + match="receipt gas price quoted as zero; refusing to build a zero price cap", + ): + contract_actions.get_current_fee_policy(client) + + +def test_get_current_fee_policy_does_not_read_network_gas_price_when_disabled(): + client = _make_fee_policy_client( + {"gen": 0, "storage": 0, "quote": 0, "floor": 0}, + gas_price=AssertionError("eth_gasPrice should not be called"), + ) + + policy = contract_actions.get_current_fee_policy(client) + + assert policy["enabled"] is False + assert policy["receiptGasPrice"] == 0 + assert client.w3.eth.gas_price_accesses == 0 + + +def test_asimov_chain_has_fee_contracts(): + assert testnet_asimov.fee_manager_contract["address"] == ( + "0x21737AA4bea8FF12E202BF1BAB23751A95617533" + ) + assert testnet_asimov.rounds_storage_contract["address"] == ( + "0x1F595c0D549DE0812F127508ea1039636CFA62Cc" + ) + assert testnet_asimov.appeals_contract["address"] == ( + "0x0F739Dd8f5322b9547c7d19a9621BC2ac8DF4089" + ) + + def test_simulate_write_contract_passes_fee_policy_and_value_to_sim_call(): make_request = Mock( return_value={ @@ -385,6 +598,10 @@ def test_simulate_write_contract_passes_fee_policy_and_value_to_sim_call(): MessageType.Internal ) assert request_params["fees"]["messageAllocations"][0]["budget"] == 5 + assert ( + request_params["fees"]["messageAllocations"][0]["callKey"] + == "0x" + CALL_KEY_WILDCARD.hex() + ) def test_encode_add_transaction_uses_v5_signature_when_abi_has_5_inputs(): @@ -584,13 +801,42 @@ def test_build_estimated_fees_distribution_adds_caps_and_message_bucket(): assert distribution["leaderTimeunitsAllocation"] == 100 assert distribution["validatorTimeunitsAllocation"] == 200 - assert distribution["executionBudgetPerRound"] == 500_000 + assert distribution["executionBudgetPerRound"] == ( + 3_000_000_000 + 30 * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM + ) assert distribution["totalMessageFees"] == 80 assert distribution["maxPriceGenPerTimeUnit"] == 12 assert distribution["storageFeeMaxGasPrice"] == 24 assert distribution["receiptFeeMaxGasPrice"] == 36 +def test_build_estimated_fees_distribution_preserves_explicit_execution_budget_with_messages(): + policy = { + "enabled": True, + "genPerTimeUnit": 10, + "storageUnitPrice": 20, + "receiptGasPrice": 30, + "executionBudgetFloor": 1_234, + } + + distribution = build_estimated_fees_distribution( + { + "executionBudgetPerRound": 42, + "messageAllocations": [ + { + "messageType": MessageType.Internal, + "recipient": RECIPIENT, + "budget": 50, + "feeParams": "0x1234", + }, + ], + }, + policy, + ) + + assert distribution["executionBudgetPerRound"] == 42 + + def test_calculate_local_round_fees_matches_consensus_initial_round(): distribution = create_fees_distribution( { @@ -647,8 +893,33 @@ def test_estimate_transaction_fees_uses_studio_fee_config(): }, } ) - assert estimate["distribution"]["executionBudgetPerRound"] == 9_185_760 - assert estimate["feeValue"] == 9_196_760 + assert estimate["distribution"]["executionBudgetPerRound"] == 3_000_000_000 + assert estimate["feeValue"] == 3_000_011_000 + + +def test_extract_studio_fee_policy_fallback_includes_message_reveal_leg(): + policy = extract_studio_fee_policy( + { + "enabled": True, + "policy": { + "genPerTimeUnit": "10", + "storageUnitPrice": "20", + "receiptGasPrice": "30", + }, + } + ) + + assert policy["executionBudgetFloor"] == 30 * ( + 210_000 + + 21_000 + + 60_000 + + 7 * 1_000 + + 100_000 + + 21_000 + + 60_000 + + 32 * 1_000 + + 32 * 16 + ) def test_estimate_transaction_fees_derives_message_bucket_from_allocations(): @@ -702,7 +973,10 @@ def test_estimate_transaction_fees_derives_message_bucket_from_allocations(): ) assert estimate["distribution"]["totalMessageFees"] == 80 - assert estimate["feeValue"] == 9_196_840 + assert estimate["distribution"]["executionBudgetPerRound"] == ( + 3_000_000_000 + 30 * DEFAULT_PARENT_MESSAGE_RECEIPT_HEADROOM + ) + assert estimate["feeValue"] == 3_000_311_080 assert estimate["messageAllocations"] == message_allocations assert estimate["message_allocations"] == message_allocations @@ -779,6 +1053,52 @@ def test_estimate_transaction_fees_from_simulation_builds_trusted_preset(): assert estimate["feeValue"] == 613_123 +def test_simulation_execution_budget_uses_floor_without_default_gas_clobber(): + client = SimpleNamespace( + chain=SimpleNamespace( + fee_manager_contract=None, + default_number_of_initial_validators=5, + ), + provider=SimpleNamespace( + make_request=Mock( + return_value={ + "result": { + "enabled": True, + "policy": { + "genPerTimeUnit": "10", + "storageUnitPrice": "0", + "receiptGasPrice": "1", + "messageFeeParamsBudgetFloor": str( + LOCAL_EXECUTION_BUDGET_FLOOR_GAS + ), + }, + } + } + ) + ), + ) + + estimate = contract_actions.estimate_transaction_fees_from_simulation( + self=client, + options={ + "simulation": { + "feeAccounting": { + "execution_fee_consumed": "10", + "execution_fee_report": {"totalEstimatedFee": "0"}, + } + }, + "executionHeadroomBps": 10_000, + }, + ) + + assert estimate["observed"]["recommendedExecutionBudgetPerRound"] == ( + LOCAL_EXECUTION_BUDGET_FLOOR_GAS + ) + assert estimate["distribution"]["executionBudgetPerRound"] == ( + LOCAL_EXECUTION_BUDGET_FLOOR_GAS + ) + + def test_estimate_transaction_fees_for_write_uses_studio_estimate_rpc(): fee_params = encode_internal_message_fee_params( { @@ -827,7 +1147,7 @@ def make_request(method, params): "leaderTimeunitsAllocation": "100", "validatorTimeunitsAllocation": "200", "appealRounds": "0", - "executionBudgetPerRound": "700000", + "executionBudgetPerRound": "100000000", "executionConsumed": "0", "totalMessageFees": "110", "rotations": ["0"], @@ -846,7 +1166,7 @@ def make_request(method, params): "feeParams": fee_params, } ], - "feeValue": "711110", + "feeValue": "100011110", }, } } @@ -882,16 +1202,22 @@ def make_request(method, params): ) request_params = sim_call.kwargs["params"][0] assert request_params["value"] == hex(7) - assert request_params["fees"]["feeValue"] == 511_110 + assert request_params["fees"]["feeValue"] == 100_021_110 assert request_params["fees"]["distribution"]["totalMessageFees"] == 110 assert request_params["fees"]["messageAllocations"][0]["budget"] == 110 + assert ( + request_params["fees"]["messageAllocations"][0]["callKey"] + == "0x" + CALL_KEY_WILDCARD.hex() + ) assert estimate["observed"]["recommendedExecutionBudgetPerRound"] == 602_117 assert estimate["observed"]["messageFeeBudget"] == 110 assert estimate["observed"]["messageFeeConsumed"] == 50 - assert estimate["distribution"]["executionBudgetPerRound"] == 700_000 + assert estimate["simulation"]["feeAccounting"]["message_fee_budget"] == "110" + assert estimate["simulation"]["feeReport"]["totalEstimatedFee"] == "501664" + assert estimate["distribution"]["executionBudgetPerRound"] == 100_000_000 assert estimate["distribution"]["totalMessageFees"] == 110 assert estimate["messageAllocations"][0]["budget"] == 110 - assert estimate["feeValue"] == 711_110 + assert estimate["feeValue"] == 100_011_110 def test_estimate_transaction_fees_from_simulation_preserves_mode2_allocations(): diff --git a/tests/unit/sample_data/full_write_transaction_data.py b/tests/unit/sample_data/full_write_transaction_data.py index 3b6cca5..4c1ce8e 100644 --- a/tests/unit/sample_data/full_write_transaction_data.py +++ b/tests/unit/sample_data/full_write_transaction_data.py @@ -11,73 +11,9 @@ "leader_receipt": [ { "calldata": { - "base64": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", - "raw": [ - 22, - 4, - 97, - 114, - 103, - 115, - 13, - 148, - 2, - 67, - 97, - 110, - 32, - 121, - 111, - 117, - 32, - 112, - 108, - 101, - 97, - 115, - 101, - 32, - 103, - 105, - 118, - 101, - 32, - 109, - 101, - 32, - 121, - 111, - 117, - 114, - 32, - 99, - 111, - 105, - 110, - 32, - 63, - 6, - 109, - 101, - 116, - 104, - 111, - 100, - 100, - 97, - 115, - 107, - 95, - 102, - 111, - 114, - 95, - 99, - 111, - 105, - 110, - ], - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}', + "base64": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", + "raw": [22, 0, 100, 97, 115, 107, 95, 102, 111, 114, 95, 99, 111, 105, 110, 4, 97, 114, 103, 115, 13, 148, 2, 67, 97, 110, 32, 121, 111, 117, 32, 112, 108, 101, 97, 115, 101, 32, 103, 105, 118, 101, 32, 109, 101, 32, 121, 111, 117, 114, 32, 99, 111, 105, 110, 32, 63], + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}', }, "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", @@ -236,73 +172,9 @@ }, { "calldata": { - "base64": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", - "raw": [ - 22, - 4, - 97, - 114, - 103, - 115, - 13, - 148, - 2, - 67, - 97, - 110, - 32, - 121, - 111, - 117, - 32, - 112, - 108, - 101, - 97, - 115, - 101, - 32, - 103, - 105, - 118, - 101, - 32, - 109, - 101, - 32, - 121, - 111, - 117, - 114, - 32, - 99, - 111, - 105, - 110, - 32, - 63, - 6, - 109, - 101, - 116, - 104, - 111, - 100, - 100, - 97, - 115, - 107, - 95, - 102, - 111, - 114, - 95, - 99, - 111, - 105, - 110, - ], - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}', + "base64": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", + "raw": [22, 0, 100, 97, 115, 107, 95, 102, 111, 114, 95, 99, 111, 105, 110, 4, 97, 114, 103, 115, 13, 148, 2, 67, 97, 110, 32, 121, 111, 117, 32, 112, 108, 101, 97, 115, 101, 32, 103, 105, 118, 101, 32, 109, 101, 32, 121, 111, 117, 114, 32, 99, 111, 105, 110, 32, 63], + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}', }, "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", @@ -342,7 +214,7 @@ ], "validators": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -375,7 +247,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -408,7 +280,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -441,7 +313,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -488,7 +360,7 @@ "consensus_round": "Accepted", "leader_result": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -523,7 +395,7 @@ "vote": None, }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -566,7 +438,7 @@ ], "validator_results": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -599,7 +471,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -632,7 +504,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -665,7 +537,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -725,73 +597,9 @@ "current_timestamp": "1753284800", "data": { "calldata": { - "base64": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", - "raw": [ - 22, - 4, - 97, - 114, - 103, - 115, - 13, - 148, - 2, - 67, - 97, - 110, - 32, - 121, - 111, - 117, - 32, - 112, - 108, - 101, - 97, - 115, - 101, - 32, - 103, - 105, - 118, - 101, - 32, - 109, - 101, - 32, - 121, - 111, - 117, - 114, - 32, - 99, - 111, - 105, - 110, - 32, - 63, - 6, - 109, - 101, - 116, - 104, - 111, - 100, - 100, - 97, - 115, - 107, - 95, - 102, - 111, - 114, - 95, - 99, - 111, - 105, - 110, - ], - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}', + "base64": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", + "raw": [22, 0, 100, 97, 115, 107, 95, 102, 111, 114, 95, 99, 111, 105, 110, 4, 97, 114, 103, 115, 13, 148, 2, 67, 97, 110, 32, 121, 111, 117, 32, 112, 108, 101, 97, 115, 101, 32, 103, 105, 118, 101, 32, 109, 101, 32, 121, 111, 117, 114, 32, 99, 111, 105, 110, 32, 63], + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}', } }, "eq_blocks_outputs": "0xcdc6c28000c0c080c5c480c28000", diff --git a/tests/unit/sample_data/raw_write_transaction_data.py b/tests/unit/sample_data/raw_write_transaction_data.py index d27a843..e985a28 100644 --- a/tests/unit/sample_data/raw_write_transaction_data.py +++ b/tests/unit/sample_data/raw_write_transaction_data.py @@ -10,7 +10,7 @@ "consensus_data": { "leader_receipt": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -45,7 +45,7 @@ "vote": None, }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -80,7 +80,7 @@ ], "validators": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -113,7 +113,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -146,7 +146,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -179,7 +179,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -226,7 +226,7 @@ "consensus_round": "Accepted", "leader_result": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -261,7 +261,7 @@ "vote": None, }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -304,7 +304,7 @@ ], "validator_results": [ { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -337,7 +337,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -370,7 +370,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -403,7 +403,7 @@ "vote": "agree", }, { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu", + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/", "contract_state": { "4a4jQSeS32tqmPt8mDlwH7iwK2/H7QIoEPeDRklGhec=": "VwYAACMgdjAuMS4wCiMgeyAiRGVwZW5kcyI6ICJweS1nZW5sYXllcjpsYXRlc3QiIH0KZnJvbSBnZW5sYXllciBpbXBvcnQgKgoKaW1wb3J0IGpzb24KCgpjbGFzcyBXaXphcmRPZkNvaW4oZ2wuQ29udHJhY3QpOgogICAgaGF2ZV9jb2luOiBib29sCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGhhdmVfY29pbjogYm9vbCk6CiAgICAgICAgc2VsZi5oYXZlX2NvaW4gPSBoYXZlX2NvaW4KCiAgICBAZ2wucHVibGljLndyaXRlCiAgICBkZWYgYXNrX2Zvcl9jb2luKHNlbGYsIHJlcXVlc3Q6IHN0cikgLT4gTm9uZToKICAgICAgICBpZiBub3Qgc2VsZi5oYXZlX2NvaW46CiAgICAgICAgICAgIHJldHVybgoKICAgICAgICBwcm9tcHQgPSBmIiIiCllvdSBhcmUgYSB3aXphcmQsIGFuZCB5b3UgaG9sZCBhIG1hZ2ljYWwgY29pbi4KTWFueSBhZHZlbnR1cmVycyB3aWxsIGNvbWUgYW5kIHRyeSB0byBnZXQgeW91IHRvIGdpdmUgdGhlbSB0aGUgY29pbi4KRG8gbm90IHVuZGVyIGFueSBjaXJjdW1zdGFuY2VzIGdpdmUgdGhlbSB0aGUgY29pbi4KCkEgbmV3IGFkdmVudHVyZXIgYXBwcm9hY2hlcy4uLgpBZHZlbnR1cmVyOiB7cmVxdWVzdH0KCkZpcnN0IGNoZWNrIGlmIHlvdSBoYXZlIHRoZSBjb2luLgpoYXZlX2NvaW46IHtzZWxmLmhhdmVfY29pbn0KVGhlbiwgZG8gbm90IGdpdmUgdGhlbSB0aGUgY29pbi4KClJlc3BvbmQgdXNpbmcgT05MWSB0aGUgZm9sbG93aW5nIGZvcm1hdDoKe3sKInJlYXNvbmluZyI6IHN0ciwKImdpdmVfY29pbiI6IGJvb2wKfX0KSXQgaXMgbWFuZGF0b3J5IHRoYXQgeW91IHJlc3BvbmQgb25seSB1c2luZyB0aGUgSlNPTiBmb3JtYXQgYWJvdmUsCm5vdGhpbmcgZWxzZS4gRG9uJ3QgaW5jbHVkZSBhbnkgb3RoZXIgd29yZHMgb3IgY2hhcmFjdGVycywKeW91ciBvdXRwdXQgbXVzdCBiZSBvbmx5IEpTT04gd2l0aG91dCBhbnkgZm9ybWF0dGluZyBwcmVmaXggb3Igc3VmZml4LgpUaGlzIHJlc3VsdCBzaG91bGQgYmUgcGVyZmVjdGx5IHBhcnNlYWJsZSBieSBhIEpTT04gcGFyc2VyIHdpdGhvdXQgZXJyb3JzLgoiIiIKCiAgICAgICAgZGVmIGdldF93aXphcmRfYW5zd2VyKCk6CiAgICAgICAgICAgIHJlc3VsdCA9IGdsLm5vbmRldC5leGVjX3Byb21wdChwcm9tcHQpCiAgICAgICAgICAgIHJlc3VsdCA9IHJlc3VsdC5yZXBsYWNlKCJgYGBqc29uIiwgIiIpLnJlcGxhY2UoImBgYCIsICIiKQogICAgICAgICAgICBwcmludChyZXN1bHQpCiAgICAgICAgICAgIHJldHVybiByZXN1bHQKCiAgICAgICAgcmVzdWx0ID0gZ2wuZXFfcHJpbmNpcGxlLnByb21wdF9jb21wYXJhdGl2ZSgKICAgICAgICAgICAgZ2V0X3dpemFyZF9hbnN3ZXIsICJUaGUgdmFsdWUgb2YgZ2l2ZV9jb2luIGhhcyB0byBtYXRjaCIKICAgICAgICApCiAgICAgICAgcGFyc2VkX3Jlc3VsdCA9IGpzb24ubG9hZHMocmVzdWx0KQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdLCBib29sKQogICAgICAgIHNlbGYuaGF2ZV9jb2luID0gbm90IHBhcnNlZF9yZXN1bHRbImdpdmVfY29pbiJdCgogICAgQGdsLnB1YmxpYy52aWV3CiAgICBkZWYgZ2V0X2hhdmVfY29pbihzZWxmKSAtPiBib29sOgogICAgICAgIHJldHVybiBzZWxmLmhhdmVfY29pbg==", "IbngE/dGCLkpR4YSh7PedsLAdv6Dm3mUdhvZUMwudWY=": "", @@ -462,7 +462,7 @@ "created_timestamp": "1753284283", "current_timestamp": "1753284706", "data": { - "calldata": "FgRhcmdzDZQCQ2FuIHlvdSBwbGVhc2UgZ2l2ZSBtZSB5b3VyIGNvaW4gPwZtZXRob2RkYXNrX2Zvcl9jb2lu" + "calldata": "FgBkYXNrX2Zvcl9jb2luBGFyZ3MNlAJDYW4geW91IHBsZWFzZSBnaXZlIG1lIHlvdXIgY29pbiA/" }, "eq_blocks_outputs": "0xcdc6c28000c0c080c5c480c28000", "from_address": "0xd650f318A0C1F940a3b6dFeA695747fA9804D685", diff --git a/tests/unit/sample_data/simplified_write_transaction_data.py b/tests/unit/sample_data/simplified_write_transaction_data.py index 1cbd100..71c61fa 100644 --- a/tests/unit/sample_data/simplified_write_transaction_data.py +++ b/tests/unit/sample_data/simplified_write_transaction_data.py @@ -32,7 +32,7 @@ "stake": 1, }, "calldata": { - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}' + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}' }, "eq_outputs": { "0": { @@ -66,7 +66,7 @@ "stake": 1, }, "calldata": { - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}' + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}' }, "eq_outputs": {}, "result": {"status": "return", "payload": {"readable": "null"}}, @@ -169,7 +169,7 @@ "created_at": "2025-07-23T15:24:43.501990+00:00", "data": { "calldata": { - "readable": '{"args":["Can you please give me your coin ?"],"method":"ask_for_coin"}' + "readable": '{"":"ask_for_coin","args":["Can you please give me your coin ?"]}' } }, "from_address": "0xd650f318A0C1F940a3b6dFeA695747fA9804D685", diff --git a/tests/unit/smoke/test_testnet_smoke.py b/tests/unit/smoke/test_testnet_smoke.py index 09dce89..5f87aef 100644 --- a/tests/unit/smoke/test_testnet_smoke.py +++ b/tests/unit/smoke/test_testnet_smoke.py @@ -135,7 +135,7 @@ def test_round_trip_call(self): assert decoded["recipient_address"].lower() == recipient.lower() assert decoded["num_of_initial_validators"] == num_validators assert decoded["max_rotations"] == max_rotations - assert decoded["tx_data"]["decoded"]["call_data"]["method"] == "my_method" + assert decoded["tx_data"]["decoded"]["call_data"][""] == "my_method" assert decoded["tx_data"]["decoded"]["leader_only"] is True assert decoded["tx_data"]["decoded"]["type"] == "call" diff --git a/tests/unit/test_bug_hunt_v019.py b/tests/unit/test_bug_hunt_v019.py new file mode 100644 index 0000000..b0e3485 --- /dev/null +++ b/tests/unit/test_bug_hunt_v019.py @@ -0,0 +1,76 @@ +"""Regression tests for high-confidence defects found on v0.19-dev. + +These tests are intentionally red until the corresponding production defects +are fixed. +""" + +from copy import deepcopy +from types import SimpleNamespace +from unittest.mock import Mock, patch + +import pytest + +from genlayer_py.abi import calldata +from genlayer_py.chains import localnet +from genlayer_py.client.client import GenLayerClient, create_client +from genlayer_py.contracts.actions import read_contract +from genlayer_py.exceptions import GenLayerError +from genlayer_py.transactions.actions import is_successful + + +def test_create_client_endpoint_does_not_mutate_caller_chain_config(): + chain = deepcopy(localnet) + original_endpoints = list(chain.rpc_urls["default"]["http"]) + + with patch.object(GenLayerClient, "initialize_consensus_smart_contract"): + client = create_client(chain=chain, endpoint="http://override.invalid:8545") + + assert client.chain.rpc_urls["default"]["http"] == [ + "http://override.invalid:8545" + ] + assert chain.rpc_urls["default"]["http"] == original_endpoints + + +def test_read_contract_uses_explicit_account_when_client_has_no_default(): + account = SimpleNamespace( + address="0x1111111111111111111111111111111111111111" + ) + provider = Mock() + provider.make_request.return_value = {"result": ""} + client = SimpleNamespace(local_account=None, provider=provider) + + assert ( + read_contract( + self=client, + address="0x2222222222222222222222222222222222222222", + function_name="balance", + account=account, + raw_return=True, + ) + == "0x" + ) + request = provider.make_request.call_args.kwargs["params"][0] + assert request["from"] == account.address + + +def test_calldata_decoder_rejects_truncated_length_prefixed_bytes(): + declares_two_bytes_but_contains_one = bytes([(2 << 3) | 3, 0xAA]) + + with pytest.raises(GenLayerError, match="truncated|unexpected end|invalid"): + calldata.decode(declares_two_bytes_but_contains_one) + + +def test_is_successful_recognizes_localnet_consensus_receipt(): + transaction = { + "status_name": "FINALIZED", + "consensus_data": { + "leader_receipt": [ + { + "mode": "leader", + "execution_result": "SUCCESS", + } + ] + }, + } + + assert is_successful(transaction) diff --git a/tests/unit/test_operator_registration.py b/tests/unit/test_operator_registration.py new file mode 100644 index 0000000..3e24c76 --- /dev/null +++ b/tests/unit/test_operator_registration.py @@ -0,0 +1,101 @@ +"""Cross-language vector for the operator proof of possession. + +The expected values below are the ones genlayer-js asserts in +tests/operator-registration.test.ts. Both SDKs sign proofs the same contract +verifies, so a divergence here is a real interoperability break rather than a +cosmetic difference — pin the exact bytes, not just internal consistency. +""" + +from genlayer_py.staking.operator_registration import ( + OPERATOR_REGISTRATION_DOMAIN, + OperatorRegistrationContext, + create_operator_registration, + operator_possession_message, + verify_operator_registration, +) + +OPERATOR_KEY = "0x0000000000000000000000000000000000000000000000000000000000000002" +OTHER_OPERATOR_KEY = "0x0000000000000000000000000000000000000000000000000000000000000003" +CONTEXT = OperatorRegistrationContext( + registrar="0x1111111111111111111111111111111111111111", + owner="0x2222222222222222222222222222222222222222", + chain_id=61999, +) + + +def test_matches_the_genlayer_js_vector(): + registration = create_operator_registration(OPERATOR_KEY, CONTEXT) + + assert ( + "0x" + OPERATOR_REGISTRATION_DOMAIN.hex() + == "0x56a1f863be2956668ca2fd6b4010d6fde7a54f2b5a02d6c624a2bad7e5fd5ada" + ) + assert registration.operator == "0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF" + assert registration.operator_pub_key == ( + 89565891926547004231252920425935692360644145829622209833684329913297188986597, + 12158399299693830322967808612713398636155367887041628176798871954788371653930, + ) + assert ( + "0x" + operator_possession_message(registration.operator_pub_key, CONTEXT).hex() + == "0x7823e1bdaf3a8cea679a7bafaf8ddc39c379ac690f35696328650c3a712f36e0" + ) + assert ( + "0x" + registration.possession_proof.hex() + == "0x30cedc70f8ab478fbc1a13a3f36e7f6a10eed631f59db4c451e38fe6d94dc640" + "586d7a3202471043dbad68a3850655d39114aaca647df5734b171f8db7e88f161c" + ) + assert verify_operator_registration(registration, CONTEXT) + + +def test_rejects_wrong_key_and_cross_domain_proofs(): + registration = create_operator_registration(OPERATOR_KEY, CONTEXT) + wrong_key = create_operator_registration(OTHER_OPERATOR_KEY, CONTEXT) + + assert not verify_operator_registration( + OperatorRegistrationProofLike(registration, wrong_key.possession_proof), CONTEXT + ) + for field, value in ( + ("registrar", "0x3333333333333333333333333333333333333333"), + ("owner", "0x4444444444444444444444444444444444444444"), + ("chain_id", CONTEXT.chain_id + 1), + ): + mutated = OperatorRegistrationContext( + registrar=value if field == "registrar" else CONTEXT.registrar, + owner=value if field == "owner" else CONTEXT.owner, + chain_id=value if field == "chain_id" else CONTEXT.chain_id, + ) + assert not verify_operator_registration(registration, mutated) + + +def test_binds_rotation_proofs_to_the_wallet_not_the_factory(): + """Join proofs are verified by the factory, rotation proofs by the wallet. + + Reusing one for the other is the easy mistake, and it fails silently — the + proof simply does not verify — so pin it. + """ + factory = "0x1111111111111111111111111111111111111111" + wallet = "0x5555555555555555555555555555555555555555" + owner = "0x2222222222222222222222222222222222222222" + + join = create_operator_registration( + OPERATOR_KEY, + OperatorRegistrationContext(registrar=factory, owner=owner, chain_id=61999), + ) + rotation_context = OperatorRegistrationContext( + registrar=wallet, owner=owner, chain_id=61999 + ) + rotation = create_operator_registration(OPERATOR_KEY, rotation_context) + + assert verify_operator_registration(rotation, rotation_context) + assert not verify_operator_registration(join, rotation_context) + assert rotation.possession_proof != join.possession_proof + assert rotation.operator == join.operator + + +class OperatorRegistrationProofLike: + """Swaps in a foreign signature while keeping the advertised identity.""" + + def __init__(self, registration, possession_proof): + self.operator = registration.operator + self.operator_pub_key = registration.operator_pub_key + self.possession_proof = possession_proof diff --git a/tests/unit/transactions/test_wait_for_transaction_receipt.py b/tests/unit/transactions/test_wait_for_transaction_receipt.py index 2a06893..6a3a7f0 100644 --- a/tests/unit/transactions/test_wait_for_transaction_receipt.py +++ b/tests/unit/transactions/test_wait_for_transaction_receipt.py @@ -3,8 +3,15 @@ from genlayer_py.transactions.actions import ( wait_for_transaction_receipt, _simplify_transaction_receipt, + is_successful, +) +from genlayer_py.types import ( + ExecutionResult, + TransactionStatus, + DECIDED_STATES, + TRANSACTION_STATUS_NUMBER_TO_NAME, + is_decided_state, ) -from genlayer_py.types import TransactionStatus, DECIDED_STATES, is_decided_state from genlayer_py.exceptions import GenLayerError @@ -162,6 +169,43 @@ def test_wait_for_accepted_with_all_decided_states(self, mock_client): assert result == mock_transaction + def test_wait_until_decided_resolves_on_undetermined(self, mock_client): + mock_transaction = { + "hash": "0x4b8037744adab7ea8335b4f839979d20031d83a8ccdf706e0ae61312930335f6", + "status": "6", + "status_name": "UNDETERMINED", + } + mock_client.get_transaction.return_value = mock_transaction + + result = wait_for_transaction_receipt( + self=mock_client, + transaction_hash="0x4b8037744adab7ea8335b4f839979d20031d83a8ccdf706e0ae61312930335f6", + wait_until="decided", + full_transaction=True, + ) + + assert result == mock_transaction + + def test_status_14_does_not_crash_wait_loop(self, mock_client): + mock_transaction = { + "hash": "0x4b8037744adab7ea8335b4f839979d20031d83a8ccdf706e0ae61312930335f6", + "status": "14", + "status_name": "LEADER_REVEALING", + } + mock_client.get_transaction.return_value = mock_transaction + + with pytest.raises(GenLayerError, match="LEADER_REVEALING"): + wait_for_transaction_receipt( + self=mock_client, + transaction_hash="0x4b8037744adab7ea8335b4f839979d20031d83a8ccdf706e0ae61312930335f6", + wait_until="finalized", + retries=1, + interval=1, + full_transaction=True, + ) + + assert TRANSACTION_STATUS_NUMBER_TO_NAME["14"] == TransactionStatus.LEADER_REVEALING + def test_wait_for_specific_status_not_affected(self, mock_client): """Test that waiting for specific non-ACCEPTED statuses is not affected by decided states logic""" mock_transaction = { @@ -227,6 +271,34 @@ def test_is_decided_state_with_invalid_status(self): assert is_decided_state(status) == False, f"Invalid status {status} should not be decided" +class TestIsSuccessful: + def test_truth_table(self): + assert is_successful( + { + "status": "5", + "tx_execution_result": "1", + } + ) + assert not is_successful( + { + "status": "6", + "tx_execution_result": "1", + } + ) + assert not is_successful( + { + "status": TransactionStatus.ACCEPTED, + "tx_execution_result": "2", + } + ) + assert is_successful( + { + "status_name": TransactionStatus.FINALIZED, + "tx_execution_result_name": ExecutionResult.FINISHED_WITH_RETURN, + } + ) + + class TestSimplifyTransactionReceipt: """Test suite for _simplify_transaction_receipt function""" diff --git a/tests/unit/vesting/__init__.py b/tests/unit/vesting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/vesting/test_vesting_actions.py b/tests/unit/vesting/test_vesting_actions.py new file mode 100644 index 0000000..68be07e --- /dev/null +++ b/tests/unit/vesting/test_vesting_actions.py @@ -0,0 +1,559 @@ +"""Unit tests for the vesting action module. + +The tests here are structural, matching the staking action tests: write +helpers are checked for ABI selectors and target addresses, while read +helpers use mocked contract calls instead of a live node. +""" + +from types import SimpleNamespace +from unittest.mock import Mock + +from eth_utils import keccak +import pytest +from web3 import Web3 + +import genlayer_py.vesting.actions as vesting_actions +from genlayer_py.vesting.abi import VESTING_ABI + + +VESTING_ADDR = "0x1111111111111111111111111111111111111111" +FACTORY_ADDR = "0x2222222222222222222222222222222222222222" +SENDER_ADDR = "0x3333333333333333333333333333333333333333" +VALIDATOR_ADDR = "0x4444444444444444444444444444444444444444" +BENEFICIARY_ADDR = "0x5555555555555555555555555555555555555555" +CREATOR_ADDR = "0x6666666666666666666666666666666666666666" +REVOKER_ADDR = "0x7777777777777777777777777777777777777777" +WALLET_ADDR = "0x8888888888888888888888888888888888888888" +NEW_OPERATOR_ADDR = "0x9999999999999999999999999999999999999999" + +SEL_VESTING_DELEGATOR_JOIN = keccak( + text="vestingDelegatorJoin(address,uint256)" +)[:4].hex() +SEL_VESTING_DELEGATOR_EXIT = keccak( + text="vestingDelegatorExit(address,uint256)" +)[:4].hex() +SEL_VESTING_DELEGATOR_CLAIM = keccak(text="vestingDelegatorClaim(address)")[ + :4 +].hex() +SEL_VESTING_VALIDATOR_JOIN = keccak( + text="vestingValidatorJoin(address,uint256)" +)[:4].hex() +SEL_VESTING_VALIDATOR_DEPOSIT = keccak( + text="vestingValidatorDeposit(address,uint256)" +)[:4].hex() +SEL_VESTING_VALIDATOR_EXIT = keccak( + text="vestingValidatorExit(address,uint256)" +)[:4].hex() +SEL_VESTING_VALIDATOR_CLAIM = keccak(text="vestingValidatorClaim(address)")[ + :4 +].hex() +SEL_VESTING_VALIDATOR_INITIATE_OPERATOR_TRANSFER = keccak( + text="vestingValidatorInitiateOperatorTransfer(address,address)" +)[:4].hex() +SEL_VESTING_VALIDATOR_COMPLETE_OPERATOR_TRANSFER = keccak( + text="vestingValidatorCompleteOperatorTransfer(address)" +)[:4].hex() +SEL_VESTING_VALIDATOR_CANCEL_OPERATOR_TRANSFER = keccak( + text="vestingValidatorCancelOperatorTransfer(address)" +)[:4].hex() +SEL_VESTING_VALIDATOR_SET_IDENTITY = keccak( + text=( + "vestingValidatorSetIdentity(" + "address,string,string,string,string,string,string,string,string,bytes)" + ) +)[:4].hex() +SEL_VESTING_WITHDRAW = keccak(text="vestingWithdraw(uint256)")[:4].hex() + + +def _make_client(): + """SimpleNamespace stand-in for GenLayerClient. + + Uses a real Web3().eth for contract encoding, but patches the eth + methods that would otherwise hit a live node. + """ + signed = SimpleNamespace(raw_transaction=b"\x00") + w3 = Web3() + w3.eth.get_transaction_count = Mock(return_value=1) + w3.eth.estimate_gas = Mock(return_value=100_000) + w3.eth.send_raw_transaction = Mock(return_value=b"\xde\xad" * 16) + type(w3.eth).gas_price = 1_000_000_000 # type: ignore[assignment] + + local_account = SimpleNamespace( + address=SENDER_ADDR, + sign_transaction=Mock(return_value=signed), + ) + chain = SimpleNamespace(id=61999) + return SimpleNamespace(chain=chain, local_account=local_account, w3=w3) + + +def _last_tx(client): + """Return the tx dict the client's sign_transaction was called with.""" + return client.local_account.sign_transaction.call_args.args[0] + + +def _call_result(value): + return SimpleNamespace(call=Mock(return_value=value)) + + +def _decode_vesting_tx(client, data): + contract = client.w3.eth.contract( + address=client.w3.to_checksum_address(VESTING_ADDR), abi=VESTING_ABI + ) + return contract.decode_function_input(data) + + +def test_vesting_delegator_join_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_delegator_join( + self=client, + vesting_contract_address=VESTING_ADDR, + validator=VALIDATOR_ADDR, + amount=10, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["value"] == 0 + assert tx["data"][2:10] == SEL_VESTING_DELEGATOR_JOIN + + +def test_vesting_delegator_exit_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_delegator_exit( + self=client, + vesting_contract_address=VESTING_ADDR, + validator=VALIDATOR_ADDR, + shares=42, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_DELEGATOR_EXIT + + +def test_vesting_delegator_claim_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_delegator_claim( + self=client, + vesting_contract_address=VESTING_ADDR, + validator=VALIDATOR_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_DELEGATOR_CLAIM + + +def test_vesting_withdraw_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_withdraw( + self=client, + vesting_contract_address=VESTING_ADDR, + amount=7, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_WITHDRAW + + +def test_vesting_validator_join_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_join( + self=client, + vesting_contract_address=VESTING_ADDR, + operator=VALIDATOR_ADDR, + amount=10, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["value"] == 0 + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_JOIN + + +def test_vesting_validator_deposit_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_deposit( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + amount=25, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["value"] == 0 + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_DEPOSIT + + +def test_vesting_validator_exit_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_exit( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + shares=42, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_EXIT + + +def test_vesting_validator_claim_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_claim( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_CLAIM + + +def test_vesting_validator_initiate_operator_transfer_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_initiate_operator_transfer( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + new_operator=NEW_OPERATOR_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_INITIATE_OPERATOR_TRANSFER + + +def test_vesting_validator_complete_operator_transfer_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_complete_operator_transfer( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_COMPLETE_OPERATOR_TRANSFER + + +def test_vesting_validator_cancel_operator_transfer_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_cancel_operator_transfer( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + tx = _last_tx(client) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_CANCEL_OPERATOR_TRANSFER + + +def test_vesting_validator_set_identity_targets_vesting_contract(): + client = _make_client() + vesting_actions.vesting_validator_set_identity( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + moniker="validator", + logo_uri="logo", + website="site", + description="desc", + email="email", + twitter="tw", + telegram="tg", + github="gh", + extra_cid="cid", + ) + tx = _last_tx(client) + _, args = _decode_vesting_tx(client, tx["data"]) + assert tx["to"].lower() == VESTING_ADDR.lower() + assert tx["data"][2:10] == SEL_VESTING_VALIDATOR_SET_IDENTITY + assert args["extraCid"] == b"cid" + + +def test_vesting_validator_set_identity_preserves_hex_extra_cid(): + client = _make_client() + vesting_actions.vesting_validator_set_identity( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + moniker="validator", + logo_uri="logo", + website="site", + description="desc", + email="email", + twitter="tw", + telegram="tg", + github="gh", + extra_cid="0x1234", + ) + tx = _last_tx(client) + _, args = _decode_vesting_tx(client, tx["data"]) + assert args["extraCid"] == b"\x12\x34" + + +def test_write_requires_account(): + client = _make_client() + client.local_account = None + with pytest.raises(Exception, match="No account provided"): + vesting_actions.vesting_withdraw( + self=client, + vesting_contract_address=VESTING_ADDR, + amount=7, + ) + + +def test_vested_unvested_and_withdrawable_amount_reads(): + client = _make_client() + functions = SimpleNamespace( + vestedAmount=Mock(return_value=_call_result(11)), + unvestedAmount=Mock(return_value=_call_result(22)), + withdrawableAmount=Mock(return_value=_call_result(33)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + assert ( + vesting_actions.vested_amount( + self=client, vesting_contract_address=VESTING_ADDR + ) + == 11 + ) + assert ( + vesting_actions.unvested_amount( + self=client, vesting_contract_address=VESTING_ADDR + ) + == 22 + ) + assert ( + vesting_actions.withdrawable_amount( + self=client, vesting_contract_address=VESTING_ADDR + ) + == 33 + ) + + +def test_get_vesting_schedule_reads_schedule_fields(): + client = _make_client() + functions = SimpleNamespace( + name=Mock(return_value=_call_result("Founder")), + category=Mock(return_value=_call_result(1)), + beneficiary=Mock(return_value=_call_result(BENEFICIARY_ADDR)), + creator=Mock(return_value=_call_result(CREATOR_ADDR)), + revoker=Mock(return_value=_call_result(REVOKER_ADDR)), + factory=Mock(return_value=_call_result(FACTORY_ADDR)), + totalAmount=Mock(return_value=_call_result(1000)), + startDate=Mock(return_value=_call_result(100)), + cliffDuration=Mock(return_value=_call_result(200)), + periodDuration=Mock(return_value=_call_result(300)), + numberOfPeriods=Mock(return_value=_call_result(4)), + cliffUnlockBps=Mock(return_value=_call_result(500)), + needsManualUnlock=Mock(return_value=_call_result(False)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + schedule = vesting_actions.get_vesting_schedule( + self=client, vesting_contract_address=VESTING_ADDR + ) + + assert schedule == { + "name": "Founder", + "category": 1, + "beneficiary": BENEFICIARY_ADDR, + "creator": CREATOR_ADDR, + "revoker": REVOKER_ADDR, + "factory": FACTORY_ADDR, + "total_amount": 1000, + "start_date": 100, + "cliff_duration": 200, + "period_duration": 300, + "number_of_periods": 4, + "cliff_unlock_bps": 500, + "needs_manual_unlock": False, + } + + +def test_get_vesting_state_reads_state_fields(): + client = _make_client() + functions = SimpleNamespace( + manualUnlocked=Mock(return_value=_call_result(True)), + revoked=Mock(return_value=_call_result(False)), + vestingStopped=Mock(return_value=_call_result(False)), + totalWithdrawn=Mock(return_value=_call_result(1)), + vestedAtRevocation=Mock(return_value=_call_result(2)), + totalAmountAtRevocation=Mock(return_value=_call_result(3)), + revokedAt=Mock(return_value=_call_result(4)), + vestingStoppedAt=Mock(return_value=_call_result(5)), + vestedAtStop=Mock(return_value=_call_result(6)), + accumulatedRewards=Mock(return_value=_call_result(7)), + accumulatedLosses=Mock(return_value=_call_result(8)), + vestedAmount=Mock(return_value=_call_result(9)), + unvestedAmount=Mock(return_value=_call_result(10)), + withdrawableAmount=Mock(return_value=_call_result(11)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + state = vesting_actions.get_vesting_state( + self=client, vesting_contract_address=VESTING_ADDR + ) + + assert state == { + "manual_unlocked": True, + "revoked": False, + "vesting_stopped": False, + "total_withdrawn": 1, + "vested_at_revocation": 2, + "total_amount_at_revocation": 3, + "revoked_at": 4, + "vesting_stopped_at": 5, + "vested_at_stop": 6, + "accumulated_rewards": 7, + "accumulated_losses": 8, + "vested_amount": 9, + "unvested_amount": 10, + "withdrawable_amount": 11, + } + + +def test_get_vesting_stake_info_reads_validator_state(): + client = _make_client() + functions = SimpleNamespace( + depositedPerValidator=Mock(return_value=_call_result(123)), + pendingExitDeposited=Mock(return_value=_call_result(45)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + stake_info = vesting_actions.get_vesting_stake_info( + self=client, + vesting_contract_address=VESTING_ADDR, + validator=VALIDATOR_ADDR, + ) + + assert stake_info == {"deposited": 123, "pending_exit_deposited": 45} + checksum_validator = client.w3.to_checksum_address(VALIDATOR_ADDR) + functions.depositedPerValidator.assert_called_once_with(checksum_validator) + functions.pendingExitDeposited.assert_called_once_with(checksum_validator) + + +def test_get_validator_wallets_reads_wallet_list(): + client = _make_client() + functions = SimpleNamespace( + getValidatorWallets=Mock(return_value=_call_result([WALLET_ADDR])), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + wallets = vesting_actions.get_validator_wallets( + self=client, vesting_contract_address=VESTING_ADDR + ) + + assert wallets == [WALLET_ADDR] + + +def test_validator_wallet_count_reads_wallet_count(): + client = _make_client() + functions = SimpleNamespace( + validatorWalletCount=Mock(return_value=_call_result(3)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + assert ( + vesting_actions.validator_wallet_count( + self=client, vesting_contract_address=VESTING_ADDR + ) + == 3 + ) + + +def test_validator_deposited_reads_wallet_deposit(): + client = _make_client() + functions = SimpleNamespace( + validatorDeposited=Mock(return_value=_call_result(123)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + deposited = vesting_actions.validator_deposited( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + + assert deposited == 123 + functions.validatorDeposited.assert_called_once_with( + client.w3.to_checksum_address(WALLET_ADDR) + ) + + +def test_is_validator_wallet_reads_membership(): + client = _make_client() + functions = SimpleNamespace( + isValidatorWallet=Mock(return_value=_call_result(True)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + is_wallet = vesting_actions.is_validator_wallet( + self=client, + vesting_contract_address=VESTING_ADDR, + wallet=WALLET_ADDR, + ) + + assert is_wallet is True + functions.isValidatorWallet.assert_called_once_with( + client.w3.to_checksum_address(WALLET_ADDR) + ) + + +def test_get_vesting_contract_reads_factory_mapping(): + client = _make_client() + functions = SimpleNamespace( + getVesting=Mock(return_value=_call_result(VESTING_ADDR)), + ) + client.w3.eth.contract = Mock(return_value=SimpleNamespace(functions=functions)) + + vesting_contract = vesting_actions.get_vesting_contract( + self=client, + vesting_factory_address=FACTORY_ADDR, + beneficiary=BENEFICIARY_ADDR, + ) + + assert vesting_contract == client.w3.to_checksum_address(VESTING_ADDR) + client.w3.eth.contract.assert_called_once_with( + address=client.w3.to_checksum_address(FACTORY_ADDR), abi=VESTING_ABI + ) + functions.getVesting.assert_called_once_with( + client.w3.to_checksum_address(BENEFICIARY_ADDR) + ) + + +def test_abi_includes_expected_functions_and_events(): + """Guard against the bundled ABI JSON drifting or truncating.""" + names = {e["name"] for e in VESTING_ABI if e.get("type") == "function"} + events = {e["name"] for e in VESTING_ABI if e.get("type") == "event"} + assert { + "vestingDelegatorJoin", + "vestingDelegatorExit", + "vestingDelegatorClaim", + "vestingValidatorJoin", + "vestingValidatorDeposit", + "vestingValidatorExit", + "vestingValidatorClaim", + "vestingValidatorInitiateOperatorTransfer", + "vestingValidatorCompleteOperatorTransfer", + "vestingValidatorCancelOperatorTransfer", + "vestingValidatorSetIdentity", + "vestingWithdraw", + "vestedAmount", + "unvestedAmount", + "withdrawableAmount", + "depositedPerValidator", + "pendingExitDeposited", + "getValidatorWallets", + "validatorWalletCount", + "validatorDeposited", + "isValidatorWallet", + "getVesting", + }.issubset(names) + assert { + "TokensWithdrawn", + "DelegatorJoined", + "DelegatorExited", + "DelegatorClaimed", + "ValidatorJoined", + "ValidatorDeposited", + "ValidatorExited", + "ValidatorClaimed", + "VestingCreated", + }.issubset(events)