From 494567fcdad2e30de7988f77f45c35617fd90ba7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 17:07:18 +0000 Subject: [PATCH 01/27] Initial plan From e10621e63ebb286ac54f428e8df122f2138b0fb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 17:09:05 +0000 Subject: [PATCH 02/27] chore: outline staking migration plan --- .agents/skills/gooddollar/CONTRIBUTING.md | 121 ++ .agents/skills/gooddollar/SKILL.md | 215 ++++ .../contracts/BuyGDCloneFactory.abi.yaml | 205 ++++ .../BuyGDCloneFactory.selectors.yaml | 27 + .../contracts/BuyGDCloneV2.abi.yaml | 282 +++++ .../contracts/BuyGDCloneV2.selectors.yaml | 36 + .../contracts/CFAv1Forwarder.abi.yaml | 237 ++++ .../contracts/CFAv1Forwarder.selectors.yaml | 19 + .../ConstantFlowAgreementV1.abi.yaml | 560 +++++++++ .../ConstantFlowAgreementV1.selectors.yaml | 33 + .../contracts/GoodDollarOFTAdapter.abi.yaml | 100 ++ .../GoodDollarOFTAdapter.selectors.yaml | 12 + .../GooddollarSavingsStream.abi.yaml | 210 ++++ .../GooddollarSavingsStream.selectors.yaml | 27 + .../contracts/GovernanceStakingV2.abi.yaml | 202 ++++ .../GovernanceStakingV2.selectors.yaml | 26 + .../references/contracts/IdentityV3.abi.yaml | 390 +++++++ .../contracts/IdentityV3.selectors.yaml | 49 + .../references/contracts/IdentityV4.abi.yaml | 424 +++++++ .../contracts/IdentityV4.selectors.yaml | 51 + .../references/contracts/InvitesV2.abi.yaml | 333 ++++++ .../contracts/InvitesV2.selectors.yaml | 41 + .../references/contracts/MentoBroker.abi.yaml | 259 +++++ .../contracts/MentoBroker.selectors.yaml | 24 + .../contracts/MessagePassingBridge.abi.yaml | 542 +++++++++ .../MessagePassingBridge.selectors.yaml | 52 + .../references/contracts/NameService.abi.yaml | 104 ++ .../contracts/NameService.selectors.yaml | 11 + .../references/contracts/SuperToken.abi.yaml | 1033 +++++++++++++++++ .../contracts/SuperToken.selectors.yaml | 99 ++ .../references/contracts/Superfluid.abi.yaml | 757 ++++++++++++ .../contracts/Superfluid.selectors.yaml | 51 + .../references/contracts/UBISchemeV2.abi.yaml | 305 +++++ .../contracts/UBISchemeV2.selectors.yaml | 40 + .../contracts/_rich-abi-yaml-format.md | 93 ++ .../deep-researches/faucet-flows.md | 42 + .../fuse-to-celo-staking-migration.md | 38 + .../gooddao-daostack-surface.md | 37 + .../deep-researches/how-ubi-is-minted.md | 68 ++ .../inviter-invitee-reward-model.md | 58 + .../mento-reserve-economics.md | 27 + .../deep-researches/on-off-ramp-service.md | 69 ++ .../gooddollar/references/guides/bridge.md | 187 +++ .../references/guides/check-identity.md | 87 ++ .../gooddollar/references/guides/claim.md | 83 ++ .../gooddollar/references/guides/faucet.md | 76 ++ .../gooddollar/references/guides/gooddocs.md | 37 + .../gooddollar/references/guides/goodsdks.md | 110 ++ .../references/guides/hypersync-hyperrpc.md | 110 ++ .../references/guides/invite-bounties.md | 115 ++ .../migrate-fuse-staking-to-celo-savings.md | 116 ++ .../references/guides/on-off-ramp.md | 77 ++ .../gooddollar/references/guides/save.md | 93 ++ .../gooddollar/references/guides/stream.md | 135 +++ .../gooddollar/references/guides/swap.md | 83 ++ .../references/subgraphs/_query-patterns.md | 35 + .../subgraphs/goodcollective-guide.md | 54 + .../subgraphs/goodcollective.graphql | 200 ++++ .../subgraphs/gooddollar-celo-guide.md | 104 ++ .../subgraphs/gooddollar-celo.graphql | 113 ++ .../subgraphs/reserve-celo-guide.md | 41 + .../references/subgraphs/reserve-celo.graphql | 90 ++ skills-lock.json | 11 + 63 files changed, 9266 insertions(+) create mode 100644 .agents/skills/gooddollar/CONTRIBUTING.md create mode 100644 .agents/skills/gooddollar/SKILL.md create mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/NameService.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/NameService.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/faucet-flows.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md create mode 100644 .agents/skills/gooddollar/references/guides/bridge.md create mode 100644 .agents/skills/gooddollar/references/guides/check-identity.md create mode 100644 .agents/skills/gooddollar/references/guides/claim.md create mode 100644 .agents/skills/gooddollar/references/guides/faucet.md create mode 100644 .agents/skills/gooddollar/references/guides/gooddocs.md create mode 100644 .agents/skills/gooddollar/references/guides/goodsdks.md create mode 100644 .agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md create mode 100644 .agents/skills/gooddollar/references/guides/invite-bounties.md create mode 100644 .agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md create mode 100644 .agents/skills/gooddollar/references/guides/on-off-ramp.md create mode 100644 .agents/skills/gooddollar/references/guides/save.md create mode 100644 .agents/skills/gooddollar/references/guides/stream.md create mode 100644 .agents/skills/gooddollar/references/guides/swap.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/_query-patterns.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/goodcollective.graphql create mode 100644 .agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql create mode 100644 .agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql create mode 100644 skills-lock.json diff --git a/.agents/skills/gooddollar/CONTRIBUTING.md b/.agents/skills/gooddollar/CONTRIBUTING.md new file mode 100644 index 0000000..ab855d3 --- /dev/null +++ b/.agents/skills/gooddollar/CONTRIBUTING.md @@ -0,0 +1,121 @@ +# Contributing to GoodSkills + +This repository is an AI skill pack. The goal of each update is to make agent behavior more reliable, more explicit, and easier to audit. + +## Update workflow + +1. Define the user-facing problem first. +2. For contract-related updates, add or update Rich ABI first (`references/contracts/*.abi.yaml`), then refresh selectors. +3. Decide the remaining artifact types: + - `references/guides/` for "what to do" + - `references/deep-researches/` for "why it works this way" + - `scripts/` for deterministic and repeatable execution +4. Update `SKILL.md` routing so the new artifact is discoverable. +5. Validate consistency (paths, naming, links, selectors, assumptions). + +If a change touches contract behavior, treat Rich ABI update/add as mandatory first step before guides, deep-research, or scripts. + +## Add or update a guide + +Use guides for execution playbooks and operator workflows. + +Required structure: + +- title and one-line usage trigger +- `## Goal` +- `## Required inputs` +- `## Execution flow` as numbered steps +- deterministic snippet when execution is non-trivial +- failure handling and output contract + +Guide rules: + +- prefer explicit pre-checks before state-changing actions +- include only one primary workflow per file +- use [GoodProtocol `releases/deployment.json`](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) as the **only** source for contract addresses (rich ABI `meta.deployments` mirrors those rows); use GoodDocs for behavior and UX, not for resolving addresses; use on-chain `NameService.getAddress` only when the deployment documents the string key +- avoid implementation-deep theory; keep that in deep-research files + +After adding a guide: + +- add it to `SKILL.md` in `Guides` +- add an entry in `Use-case to guide map` + +## Add or update a deep-research note + +Use deep-research files for architecture, rationale, tradeoffs, and root-cause logic. + +Deep-research rules: + +- explain causality, not only API surfaces +- distinguish current behavior from legacy behavior +- link source contracts/docs for traceability +- keep language natural and decision-oriented + +Do not turn deep-research files into step-by-step runbooks; move operational steps into guides. + +## Add or update Rich ABI YAML + +Location: `references/contracts/`. + +For each contract: + +- create or update `Foo.abi.yaml` +- generate or refresh `Foo.selectors.yaml` +- include function-level notes for non-obvious behavior +- when `meta.deployments` lists concrete addresses, add **`creationBlock`** next to each **`address`** (placement: `references/contracts/_rich-abi-yaml-format.md`; using it as **`fromBlock`** for log or HyperSync fetches: `references/guides/hypersync-hyperrpc.md`) + +Minimum ABI documentation quality: + +- correct mutability, inputs, outputs +- access pattern (`owner`, `avatar`, `anyone`, etc.) where relevant +- emitted events and practical errors +- notes for routing/edge-case semantics + +Source-of-truth policy: + +- prefer canonical contract repos (GoodProtocol, GoodBridge, mento-core) +- avoid inferred behavior when source is unclear +- update notes when protocol behavior changed + +Selector generation: + +```bash +node scripts/selectors.mjs generate Foo.abi.yaml +``` + +## Add or update scripts + +Location: `scripts/`. + +Use scripts when: + +- a workflow is repeated +- deterministic output is needed +- manual querying is error-prone + +Script standards: + +- require inputs through env vars or explicit args +- fail loudly with actionable messages +- print structured output for easy reuse +- keep script intent narrow + +When a script supports a guide: + +- reference it from that guide +- document expected inputs and outputs in the guide + +## Naming and organization + +- use lowercase kebab-case for guides and deep-research files +- keep one topic per file +- avoid duplicate guidance across files +- prefer updating existing files over creating near-duplicates + +## Update checklist before merge + +- `SKILL.md` routing updated +- links resolve and point to public sources +- guides and deep-research files respect "what" vs "why" separation +- ABI + selectors pairs are in sync +- new behavior is reflected in notes where needed diff --git a/.agents/skills/gooddollar/SKILL.md b/.agents/skills/gooddollar/SKILL.md new file mode 100644 index 0000000..2d59116 --- /dev/null +++ b/.agents/skills/gooddollar/SKILL.md @@ -0,0 +1,215 @@ +--- +name: gooddollar +description: > + Knowledge base for GoodProtocol action execution and GoodDollar (G$) integrations. + Use this skill BEFORE ad-hoc web search for claim, save/stake, swap, bridge, + stream, and identity tasks. Prefer GoodDocs (https://docs.gooddollar.org/) for + narrative; contract addresses only from GoodProtocol releases/deployment.json. +metadata: + version: 1.0.0 +license: MIT +--- + +# GoodDollar Skill Pack + +Routing index for GoodProtocol. This repo complements [GoodDocs](https://docs.gooddollar.org/) for behavior and user flows. **Contract addresses** come only from [GoodProtocol/releases/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (and `meta.deployments` in `references/contracts/*.abi.yaml`, which mirror those rows)—not from GoodDocs pages. + +Repository maintenance and update process is documented in `CONTRIBUTING.md`. + +## Protocol snapshot (from GoodDocs) + +- G$ is reserve-backed; issuance and pricing tie to the reserve and bonding-curve mechanics described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). +- The stack is multi-chain; which contracts exist per environment is defined only in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `GoodDollar`, `Identity`, `NameService`, `UBIScheme`, Mento keys, `MpbBridge`, and related entries under `production`, `production-celo`, and `production-xdc`). +- UBI is daily for verified users; identity verification and connected accounts are documented under [user guides](https://docs.gooddollar.org/user-guides). + +## Guides (single location for action playbooks) + +All task-specific instructions live under `references/guides/`. + +- `references/guides/claim.md` — daily UBI (`claim` / UBIScheme). +- `references/guides/save.md` — stake, rewards, unstake. +- `references/guides/swap.md` — buy or sell G$ (Mento on supported chains). +- `references/guides/bridge.md` — MessagePassingBridge (GoodDocs); optional OFT path via ABI refs. +- `references/guides/stream.md` — Superfluid streams (Celo-oriented in GoodDocs). +- `references/guides/check-identity.md` — whitelist and connected-address semantics. +- `references/guides/goodsdks.md` — SDK-first integration routing for GoodSDKs packages. +- `references/guides/gooddocs.md` — hub links to [GoodDocs](https://docs.gooddollar.org/). +- `references/guides/hypersync-hyperrpc.md` — Envio HyperSync/HyperRPC data-source routing for high-volume historical reads. +- `references/guides/faucet.md` — Faucet gas top-up execution flow and preflight checks. +- `references/guides/on-off-ramp.md` — stable-token ramp service flow into and out of G$. +- `references/guides/invite-bounties.md` — verify and execute inviter-invitee bounty payouts. +- `references/guides/migrate-fuse-staking-to-celo-savings.md` — migrate Fuse governance stake to CELO savings flow. + +## Subgraphs (indexed chain history) + +Use this folder with the same pattern as the protocol subgraph references: one `*-guide.md` plus one companion `.graphql` per deployment. + +For historical on-chain data, **start with the subgraph**: confirm the deployment covers the question (entities and fields in the guide, freshness via `_meta`). If the subgraph does not work for the request—missing schema coverage, stale or lagging indexing, query limits, or endpoint errors—**then** move to **HyperSync** or **HyperRPC** using `references/guides/hypersync-hyperrpc.md`. + +- `references/subgraphs/_query-patterns.md` — cross-cutting query discipline. +- `references/subgraphs/reserve-celo-guide.md` + `references/subgraphs/reserve-celo.graphql` — reserve pricing and swap history. +- `references/subgraphs/gooddollar-celo-guide.md` + `references/subgraphs/gooddollar-celo.graphql` — GoodDollar Celo schema discovery and starter probes. +- `references/subgraphs/goodcollective-guide.md` + `references/subgraphs/goodcollective.graphql` — GoodCollective schema discovery and starter probes. + +For Superfluid protocol subgraphs (streams, pools, vesting schedulers), see [Superfluid documentation](https://docs.superfluid.finance/) and [subgraph endpoints](https://subgraph-endpoints.superfluid.dev/). + +## Historical data routing policy (strict) + +1. Query subgraphs first for all historical/indexed requests. +2. Validate required entities and fields against the target subgraph schema and guide before declaring a gap. +3. Use **HyperSync** or **HyperRPC** fallback only when at least one of these is true: + - required entities or fields are not available in subgraph schema + - indexing lag makes subgraph data stale for the requested range + - query limits or endpoint instability block reliable retrieval +4. Do not start with HyperSync or HyperRPC when subgraph data is available and fresh. +5. HyperRPC fallback requires a valid Envio API key; if missing, **explicitly ask the user** to provide `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` (or paste a full `HYPERRPC_URL`); do not treat anonymous HyperRPC as production. +6. When **HyperSync** is the best option for the query and no Envio API token is available (`ENVIO_API_TOKEN` or equivalent per `references/guides/hypersync-hyperrpc.md`), **explicitly ask the user** to provide the token before proceeding; do not silently substitute anonymous HyperSync usage. +7. When fallback is used, report reason explicitly (schema gap, lag, or reliability issue). + +## Data source decision table + +| Query type | Primary source | Secondary source | Notes | +|---|---|---|---| +| Current on-chain state (latest balances, allowances, config, flags, view calls) | RPC | None | Use direct contract RPC reads for latest state. | +| Historical indexed entity data (time-series, aggregates, protocol entities, event-derived analytics) | Subgraph | HyperSync/HyperRPC | Prefer subgraph first; fall back when it cannot answer. | +| Historical raw on-chain data when subgraph is missing fields/entities or stale | HyperSync | HyperRPC | Prefer HyperSync for bulk scans and data pipelines. | +| Historical data for existing JSON-RPC integrations | HyperRPC | HyperSync | Use HyperRPC when strict JSON-RPC compatibility is required. | + +Decision rule: + +1. If request is current state -> use RPC. +2. If request is historical/indexed -> query subgraph first. +3. If subgraph cannot satisfy request -> fallback to HyperSync or HyperRPC per compatibility and scale needs. +4. HyperRPC fallback requires Envio API key credentials. +5. HyperSync client usage requires an Envio API token; if HyperSync is chosen and the token is missing, explicitly ask the user to provide it (see `references/guides/hypersync-hyperrpc.md`). + +## Mapping data retrieval rule + +Solidity mappings are not iterable on-chain by keyspace scan. Do not assume full-key enumeration is possible from RPC alone. + +When data is stored in mapping-like structures: + +1. Check contract source and ABI for key-discovery paths first: + - events emitted on set or update + - arrays, counters, linked lists, or index getters storing keys + - dedicated pagination or enumerable view functions +2. If key discovery exists, reconstruct key set from those sources and then read mapping entries. +3. If key discovery does not exist, report that complete iteration is not possible from chain state alone. +4. For historical reconstruction, prefer subgraph indexing first; if unavailable, use HyperSync or HyperRPC log scans with explicit limitations. + +## Use-case to guide map + +- Claim requests -> `references/guides/claim.md` +- Eligibility or connected-address questions -> `references/guides/check-identity.md` +- Stake, save, unstake -> `references/guides/save.md` +- Buy or sell G$ against reserve rails -> `references/guides/swap.md` +- Cross-chain bridge -> `references/guides/bridge.md` +- Stream management -> `references/guides/stream.md` +- SDK app integration tasks -> `references/guides/goodsdks.md` +- Bulk historical reads or data-engineering fetches -> `references/guides/hypersync-hyperrpc.md` +- Faucet top-up tasks -> `references/guides/faucet.md` +- On-/off-ramp service flow tasks -> `references/guides/on-off-ramp.md` +- Invite bounty eligibility and payout tasks -> `references/guides/invite-bounties.md` +- Fuse to CELO staking migration tasks -> `references/guides/migrate-fuse-staking-to-celo-savings.md` +- Indexed history, analytics, or GraphQL against GoodDollar subgraphs -> `references/subgraphs/_query-patterns.md` +- Historical on-chain fetch when subgraph data is insufficient -> subgraphs first, then HyperSync or HyperRPC per `references/guides/hypersync-hyperrpc.md`; if HyperSync is best and `ENVIO_API_TOKEN` is missing, ask the user for it explicitly. + +## Ambiguous prompts and incomplete inputs + +Stop and **ask the user** whenever the task is underspecified or required facts are missing. List what you need in short, concrete questions (for example chain, contract, address, amount, account, RPC or signer access, time or block range, prior tx hash, approval scope). + +- **Ambiguous** means the goal, environment, contract surface, or acceptance criteria are not clear enough to choose a safe path. +- **Incomplete** means you lack inputs that would change what you build, call, or sign next. + +**Do not invent** chain, address, amount, or policy details that affect correctness, funds, or eligibility. For **information-only** work you may state a single explicit assumption, label it, and ask the user to confirm or correct it before going further. + +**Execution work** (writing or editing runnable code, sending transactions, migrations, or anything that can move funds or alter on-chain state) has **no guessing**: settle every required input with the user, then implement or run. + +## Execution rules + +1. Collect missing required inputs before sending transactions. +2. Run pre-checks first (allowance, whitelist, quotes, bridge **amount** limits, peer wiring when using OFT paths). +3. If a pre-check fails, stop and return the exact corrective action. +4. Return tx hash and key output values. +5. Never fabricate addresses, amounts, or ABI behavior. +6. Resolve decimals and units per chain as in [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (for example 18 decimals on Celo, 2 on Fuse and Ethereum where applicable). + +## Pre-check matrix + +- Claim: verify identity whitelist status before `claim()`. +- Save or stake: verify balance and allowance before `stake()`. +- Swap: fetch quote, apply slippage bounds, verify allowance; confirm Mento contract keys for the active chain exist in `deployment.json` (for example `MentoBroker` under `production-celo` or `production-xdc`). +- Bridge (MessagePassingBridge): on the **source** chain approve G$ to the bridge; optionally preflight `canBridge(from, amount)` on that same contract (outbound `_bridgeTo` does not call it internally). For LZ use `estimateSendFee` with the **normalized** burn amount per `references/guides/bridge.md`, then `bridgeToWithLz` with nonzero `msg.value` for the **cross-chain transport** fee only (distinct from destination **`bridgeFees`** on minted G$; see **Bridge fee context** in that guide). Read **`bridgeLimits`** / daily trackers when debugging **amount** caps; see **Bridge amount limit context** in that guide. Respect `isClosed`, `LZ_FEE`, `MISSING_FEE`, and `UNSUPPORTED_CHAIN`. **Destination** mint applies `_enforceLimits` and can still revert. Use **Axelar** only when `toAxelarChainId` returns a route (implementation maps 1, 5, 42220, 44787); for Fuse or XDC style targets prefer LZ unless mapping is extended on-chain. +- Bridge (OFT adapter path): verify peer wiring and `quoteSend` fee data. +- Stream: confirm Celo (or documented Superfluid network) and correct Super Token and forwarder or host addresses. +- Identity: resolve Identity from NameService; remember connected addresses do not multiply daily claims ([connect wallet guide](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity)). + +## Output format requirements + +For any state-changing action return: + +- network and key contract addresses used +- normalized input amounts and min or max guards +- tx hash +- key post-state output when available +- follow-up action if user intervention is required + +## Rich contract ABI references + +Convention: each `Foo.abi.yaml` has a companion `Foo.selectors.yaml` (function, event, and custom error selectors). Schema: `references/contracts/_rich-abi-yaml-format.md`. + +GoodDollar / Mento: + +- `references/contracts/NameService.abi.yaml` +- `references/contracts/IdentityV3.abi.yaml` +- `references/contracts/IdentityV4.abi.yaml` +- `references/contracts/InvitesV2.abi.yaml` +- `references/contracts/BuyGDCloneFactory.abi.yaml` +- `references/contracts/BuyGDCloneV2.abi.yaml` +- `references/contracts/GovernanceStakingV2.abi.yaml` +- `references/contracts/GooddollarSavingsStream.abi.yaml` (Ubeswap Superfluid stream savings; Celo deployment) +- `references/contracts/UBISchemeV2.abi.yaml` +- `references/contracts/MentoBroker.abi.yaml` +- `references/contracts/MessagePassingBridge.abi.yaml` +- `references/contracts/GoodDollarOFTAdapter.abi.yaml` +- `references/contracts/CFAv1Forwarder.abi.yaml` +- `references/contracts/ConstantFlowAgreementV1.abi.yaml` +- `references/contracts/Superfluid.abi.yaml` +- `references/contracts/SuperToken.abi.yaml` + +Superfluid (CFA, CFAv1Forwarder, Host, full ABI library): use [Superfluid docs](https://docs.superfluid.finance/), npm packages such as `@superfluid-finance/ethereum-contracts` and `@sfpro/sdk`, and contract ABIs published with those packages. + +## Deep researches + +- `references/deep-researches/on-off-ramp-service.md` +- `references/deep-researches/how-ubi-is-minted.md` +- `references/deep-researches/inviter-invitee-reward-model.md` +- `references/deep-researches/mento-reserve-economics.md` +- `references/deep-researches/gooddao-daostack-surface.md` +- `references/deep-researches/faucet-flows.md` +- `references/deep-researches/fuse-to-celo-staking-migration.md` + +## Revert debugging quick map + +- Identity or eligibility errors -> Identity and UBIScheme ABIs; live addresses from `deployment.json` only; GoodDocs for whitelist and claim behavior. +- Approval or transfer failures -> token approvals and balances; see integration guide for `transferAndCall` vs `approve` plus `transferFrom`. +- Swap bound failures -> quote freshness and slippage settings. +- MessagePassingBridge failures -> `canBridge`; **`BRIDGE_LIMITS`** (amount caps, whitelist, **`closed`**, and related policy strings); transport `msg.value` (`MISSING_FEE`, `LZ_FEE`) vs destination protocol fee (`bridgeFees`, `feeRecipient`); correct `bridgeTo` arguments; [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars). +- OFT path failures -> peer wiring and `quoteSend` fee data. +- Stream failures -> CFA forwarder or host agreement calls, buffer and flow-rate limits per Superfluid docs linked from GoodDocs. +- Faucet top-up failures -> `canTop`, `onlyAuthorized`, daily or weekly caps; `references/deep-researches/faucet-flows.md`. +- DAO-gated reverts -> caller is not avatar; scheme not registered; `references/deep-researches/gooddao-daostack-surface.md`. + +## Library usage discipline + +1. Open `references/guides/gooddocs.md` when unsure which GoodDocs page applies. +2. Start at this file to classify intent. +3. Open one guide under `references/guides/` unless the user requests a multi-step workflow. For subgraph or indexed-data tasks, start at `references/subgraphs/_query-patterns.md`. +4. Read only the ABI references and matching `.selectors.yaml` files needed for the chosen action. +5. Prefer GoodDocs for documented behavior; use only `deployment.json` (and rich ABI `meta.deployments` aligned with it) for contract addresses—never infer addresses from GoodDocs. +6. For large historical reads, prefer `references/guides/hypersync-hyperrpc.md` and choose HyperSync over HyperRPC unless strict JSON-RPC compatibility is required. +7. Historical data routing is strict: subgraphs first; HyperSync or HyperRPC only with an explicit fallback reason. +8. HyperRPC usage requires Envio API key credentials; when absent, **explicitly ask the user** for `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` (or a full `HYPERRPC_URL`) and do not attempt anonymous production flow. +9. When HyperSync is the best historical-data path and no Envio API token is available, explicitly ask the user to provide `ENVIO_API_TOKEN` (or the token your client expects) before continuing; see `references/guides/hypersync-hyperrpc.md`. +10. For subgraph tasks, validate field availability from the relevant `references/subgraphs/*-guide.md` and companion `.graphql` before guessing alternate entities. +11. For local shells repeating HyperRPC log pulls (for example last N whitelist events), from the **GoodSkills repository root** run `scripts/fetch-whitelist-events-hyperrpc.mjs` per `references/guides/hypersync-hyperrpc.md` instead of re-deriving JSON-RPC setup each time; that script ships with **defaults for production Celo** (HyperRPC host + `Identity` contract from `deployment.json`) and URL composition from `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` unless you override `CONTRACT_ADDRESS` / `HYPERRPC_URL`. HyperSync remains a separate client install path documented in the same guide. diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml new file mode 100644 index 0000000..40226b3 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml @@ -0,0 +1,205 @@ +meta: + name: BuyGDCloneFactory + version: "1" + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/BuyGDClone.sol + note: > + Deterministic clone factory for BuyGDCloneV2 and DonateGDClone. Used to create per-owner + executors and optional create+swap flows for ramp services. + deployments: + mainnet: + production-celo: + BuyGDFactoryV2: + networkId: 42220 + address: "0x1F60C4C7037C6766924A43666B781ED1479587a2" + creationBlock: 22909812 + BuyGDFactory: + networkId: 42220 + address: "0x00e533B7d6255D05b7f15034B1c989c21F51b91C" + creationBlock: 21006324 + related: + - references/deep-researches/on-off-ramp-service.md + - references/guides/on-off-ramp.md + +create: + notes: + - "Deploys deterministic BuyGDCloneV2 clone for owner and initializes it." + - "Difference vs createAndSwap: this only deploys; no swap is executed." + mutability: nonpayable + access: anyone + inputs: + - owner: address + outputs: + - clone: address + +createDonation: + notes: + - "Deploys deterministic DonateGDClone with donation target and call payload." + - "Difference vs create: deploys donation-capable implementation instead of plain swap clone." + mutability: nonpayable + access: anyone + inputs: + - owner: address + - donateOrExecTo: address + - callData: bytes + outputs: + - clone: address + +createAndSwap: + notes: + - "One-transaction helper: create BuyGDCloneV2 then immediately call clone.swap(minAmount, msg.sender)." + - "Difference vs create: bundles deployment and first swap for faster ramp UX." + mutability: nonpayable + access: anyone + inputs: + - owner: address + - minAmount: uint256 + outputs: + - clone: address + +createDonationAndSwap: + notes: + - "One-transaction helper for DonateGDClone: deploy, then execute donation flow with optional pre-swap." + - "Difference vs createDonation: can execute immediate swap and/or target call instead of deployment only." + mutability: nonpayable + access: anyone + inputs: + - owner: address + - donateOrExecTo: address + - withSwap: bool + - minAmount: uint256 + - callData: bytes + outputs: + - clone: address + +predict: + notes: + - "Computes deterministic address for create(owner) salt." + mutability: view + inputs: + - owner: address + outputs: + - clone: address + +predictDonation: + notes: + - "Computes deterministic address for createDonation(owner, donateOrExecTo, callData) salt." + mutability: view + inputs: + - owner: address + - donateOrExecTo: address + - callData: bytes + outputs: + - clone: address + +getBaseFee: + mutability: view + inputs: [] + outputs: + - baseFee: uint256 + +quoter: + mutability: pure + inputs: [] + outputs: + - addr: address + +CUSD: + mutability: pure + inputs: [] + outputs: + - token: address + +celo: + mutability: pure + inputs: [] + outputs: + - token: address + +USDC: + mutability: pure + inputs: [] + outputs: + - token: address + +GLOUSD: + mutability: pure + inputs: [] + outputs: + - token: address + +PERIOD: + mutability: pure + inputs: [] + outputs: + - seconds: uint24 + +impl: + mutability: view + inputs: [] + outputs: + - addr: address + +donateImpl: + mutability: view + inputs: [] + outputs: + - addr: address + +gd: + mutability: view + inputs: [] + outputs: + - token: address + +stable: + mutability: view + inputs: [] + outputs: + - token: address + +oracle: + mutability: view + inputs: [] + outputs: + - addr: address + +router: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoBroker: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoExchangeProvider: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoExchangeId: + mutability: view + inputs: [] + outputs: + - id: bytes32 + +events: + GDSwapToCusd: + indexed: [] + data: + - from: address + - to: address + - amountIn: uint256 + - amountOut: uint256 + - note: bytes + +errors: + NOT_GD_TOKEN: "onTokenTransfer caller is not G$ token." + INVALID_TWAP: "TWAP validation failed." + RECIPIENT_ZERO: "Recipient cannot be zero address." + ZERO_MINAMOUNT: "Minimum amount cannot be zero." diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml new file mode 100644 index 0000000..16b13f2 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml @@ -0,0 +1,27 @@ +# Generated by scripts/selectors.mjs +functions: + create(address): 0x9ed93318 + createDonation(address,address,bytes): 0x48199e6b + createAndSwap(address,uint256): 0x89643a29 + createDonationAndSwap(address,address,bool,uint256,bytes): 0xeb5621b8 + predict(address): 0x901b96e7 + predictDonation(address,address,bytes): 0x3895de30 + getBaseFee(): 0x15e812ad + quoter(): 0xc6bbd5a7 + CUSD(): 0x758316c9 + celo(): 0x051ed8ef + USDC(): 0x89a30271 + GLOUSD(): 0x4b5b02d6 + PERIOD(): 0xb4d1d795 + impl(): 0x8abf6077 + donateImpl(): 0x21fc2eef + gd(): 0xa5e598fc + stable(): 0x22be3de1 + oracle(): 0x7dc0d1d0 + router(): 0xf887ea40 + mentoBroker(): 0x7b89f117 + mentoExchangeProvider(): 0x4f62feec + mentoExchangeId(): 0xd373b333 +events: + GDSwapToCusd(address,address,uint256,uint256,bytes): 0x252bc23e3fb01f9986fa157af621236fb8a706ea12622da296e7f2f30d4f1a56 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml new file mode 100644 index 0000000..5f35ee9 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml @@ -0,0 +1,282 @@ +meta: + name: BuyGDCloneV2 + version: "2" + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/BuyGDClone.sol + inherits: + - Initializable + note: > + Clone implementation used by BuyGDCloneFactory for deterministic per-owner swap executors. + Supports CELO and cUSD flows, with cUSD route selection between Uniswap and Mento when configured. + deployments: + mainnet: + production-celo: + BuyGDFactoryV2: + networkId: 42220 + address: "0x1F60C4C7037C6766924A43666B781ED1479587a2" + creationBlock: 22909812 + related: + - references/deep-researches/on-off-ramp-service.md + - references/guides/on-off-ramp.md + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _owner: address + outputs: [] + +getSwapPath: + mutability: pure + inputs: + - tokens: address[] + - fees: uint24[] + outputs: + - path: bytes + +swap: + notes: + - "Dispatcher entrypoint: if clone holds native CELO balance it executes CELO route, otherwise uses cUSD route." + - "Difference vs swapCelo/swapCusd: this function chooses route by contract balances; specialized functions force a specific asset path." + mutability: payable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + outputs: + - bought: uint256 + emits: [Bought] + errors: [NO_BALANCE] + +swapCelo: + notes: + - "CELO-only convenience path using the default preconfigured Uniswap route." + - "Difference vs swapCeloWithPath: same Uniswap execution, but caller cannot override token path and fee tiers." + - "Difference vs cUSD paths: CELO path does not use Mento route selection." + mutability: payable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + outputs: + - bought: uint256 + emits: [BoughtFromUniswap] + errors: [REFUND_FAILED] + +swapCeloWithPath: + notes: + - "CELO-only path with caller-supplied Uniswap multi-hop route." + - "Difference vs swapCelo: allows custom path optimization when default route is not preferred." + - "Difference vs cUSD paths: still Uniswap-only and does not compare against Mento." + mutability: payable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + - _path: (address[],uint24[]) + outputs: + - bought: uint256 + emits: [BoughtFromUniswap] + errors: [REFUND_FAILED] + +swapCusd: + notes: + - "cUSD-only convenience path that compares expected output from default-path Uniswap and Mento, then executes the better quote." + - "Difference vs swapCusdWithPath: this uses hardcoded default Uniswap path for comparison." + - "Difference vs CELO paths: includes dual-route best-execution logic with optional Mento." + mutability: nonpayable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + outputs: + - bought: uint256 + emits: [BoughtFromMento, BoughtFromUniswap] + errors: [MENTO_NOT_CONFIGURED] + +swapCusdWithPath: + notes: + - "cUSD path with caller-supplied Uniswap route; still compares custom Uniswap quote against Mento quote and picks larger expected output." + - "Difference vs swapCusd: custom path changes only the Uniswap side of the comparison." + - "Difference vs swapCeloWithPath: this function performs route competition (Uniswap vs Mento), not only route customization." + mutability: nonpayable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + - _path: (address[],uint24[]) + outputs: + - bought: uint256 + emits: [BoughtFromMento, BoughtFromUniswap] + errors: [MENTO_NOT_CONFIGURED] + +getExpectedReturnFromUniswapPath: + notes: + - "Quote helper for Uniswap path expected output used by cUSD route selection and preflight checks." + mutability: nonpayable + inputs: + - amountIn: uint256 + - _path: (address[],uint24[]) + outputs: + - expectedReturn: uint256 + +getExpectedReturnFromMento: + notes: + - "Quote helper for Mento expected output for cUSD->G$ used in best-route decision." + mutability: view + inputs: + - cusdAmount: uint256 + outputs: + - expectedReturn: uint256 + errors: [MENTO_NOT_CONFIGURED] + +minAmountByTWAP: + mutability: view + inputs: + - baseAmount: uint256 + - baseToken: address + - period: uint32 + outputs: + - minTwap: uint256 + - quote: uint256 + +recover: + mutability: nonpayable + access: anyone + inputs: + - token: address + outputs: [] + errors: [REFUND_FAILED] + +router: + mutability: view + inputs: [] + outputs: + - addr: address + +celo: + mutability: pure + inputs: [] + outputs: + - token: address + +CUSD: + mutability: pure + inputs: [] + outputs: + - token: address + +USDC: + mutability: pure + inputs: [] + outputs: + - token: address + +GLOUSD: + mutability: pure + inputs: [] + outputs: + - token: address + +GD_FEE_TIER: + mutability: pure + inputs: [] + outputs: + - tier: uint24 + +CUSD_STABLE_FEE_TIER: + mutability: pure + inputs: [] + outputs: + - tier: uint24 + +CELO_STABLE_FEE_TIER: + mutability: pure + inputs: [] + outputs: + - tier: uint24 + +twapPeriod: + mutability: view + inputs: [] + outputs: + - period: uint32 + +stable: + mutability: view + inputs: [] + outputs: + - token: address + +gd: + mutability: view + inputs: [] + outputs: + - token: address + +oracle: + mutability: view + inputs: [] + outputs: + - addr: address + +quoter: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoBroker: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoExchangeProvider: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoExchangeId: + mutability: view + inputs: [] + outputs: + - id: bytes32 + +owner: + mutability: view + inputs: [] + outputs: + - addr: address + +CUSD_GAS_COSTS: + mutability: pure + inputs: [] + outputs: + - amount: uint256 + +events: + Bought: + indexed: [] + data: + - inToken: address + - inAmount: uint256 + - outAmount: uint256 + BoughtFromMento: + indexed: [] + data: + - inToken: address + - inAmount: uint256 + - outAmount: uint256 + BoughtFromUniswap: + indexed: [] + data: + - inToken: address + - inAmount: uint256 + - outAmount: uint256 + +errors: + REFUND_FAILED: "Refund call failed." + NO_BALANCE: "No CELO or cUSD available on clone." + MENTO_NOT_CONFIGURED: "Mento broker path not configured." diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml new file mode 100644 index 0000000..1476ae3 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml @@ -0,0 +1,36 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address): 0xc4d66de8 + getSwapPath(address[],uint24[]): 0xf0036d5d + swap(uint256,address): 0xd3986f08 + swapCelo(uint256,address): 0xaa6bfa9d + swapCeloWithPath(uint256,address,(address[],uint24[])): 0x81b7d2e0 + swapCusd(uint256,address): 0xb1a5fa9f + swapCusdWithPath(uint256,address,(address[],uint24[])): 0x170ba915 + getExpectedReturnFromUniswapPath(uint256,(address[],uint24[])): 0x12194320 + getExpectedReturnFromMento(uint256): 0xdbb15eb2 + minAmountByTWAP(uint256,address,uint32): 0x821dc910 + recover(address): 0x0cd865ec + router(): 0xf887ea40 + celo(): 0x051ed8ef + CUSD(): 0x758316c9 + USDC(): 0x89a30271 + GLOUSD(): 0x4b5b02d6 + GD_FEE_TIER(): 0xe00e8fdd + CUSD_STABLE_FEE_TIER(): 0xba428926 + CELO_STABLE_FEE_TIER(): 0x60e4bf4b + twapPeriod(): 0xf6207326 + stable(): 0x22be3de1 + gd(): 0xa5e598fc + oracle(): 0x7dc0d1d0 + quoter(): 0xc6bbd5a7 + mentoBroker(): 0x7b89f117 + mentoExchangeProvider(): 0x4f62feec + mentoExchangeId(): 0xd373b333 + owner(): 0x8da5cb5b + CUSD_GAS_COSTS(): 0x32f90ac3 +events: + Bought(address,uint256,uint256): 0xa9a40dec7a304e5915d11358b968c1e8d365992abf20f82285d1df1b30c8e24c + BoughtFromMento(address,uint256,uint256): 0x8e2ac24d7ef5662ee242823a19dbd1c952b3e96ae127228f4bbce83e2816e3fb + BoughtFromUniswap(address,uint256,uint256): 0xdb1f2a6cbbfd964f19c648b140a35992aa80f482df8f519d3211d0bd86c9f335 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml new file mode 100644 index 0000000..32ba0de --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml @@ -0,0 +1,237 @@ +# CFAv1Forwarder — convenience wrapper for ConstantFlowAgreementV1 +# Allows direct interaction with CFA functions without manually routing through +# the Host's batchCall / forwardBatchCall. Operates as a trusted forwarder, +# preserving msg.sender via EIP-2771. +# +# Each forwarder call is a standalone transaction — forwarder calls cannot be +# combined in a Host.batchCall. To batch multiple operations atomically (e.g. +# wrap tokens + create stream), use Host.batchCall with the raw CFA agreement +# (operationType 201). See Superfluid.abi.yaml for batch operation details. +# Tradeoff: forwarder calls produce human-readable descriptions in wallets +# (e.g. "setFlowrate(token, receiver, flowrate)"), while Host.batchCall +# shows encoded tuples that are difficult for users to verify. +# +# This contract has no events — all events are emitted by the underlying CFA. +# Errors from the CFA (and Host) propagate through to the caller. + +meta: + name: CFAv1Forwarder + version: v1 + source: + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/utils/CFAv1Forwarder.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/utils/ForwarderBase.sol + inherits: [ForwarderBase] + deployments: + # Same address on all networks except avalanche-fuji + mainnet: + eth-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + polygon-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + xdai-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + base-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + optimism-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + arbitrum-one: "0xcfA132E353cB4E398080B9700609bb008eceB125" + bsc-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + avalanche-c: "0xcfA132E353cB4E398080B9700609bb008eceB125" + celo-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + scroll-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + degenchain: "0xcfA132E353cB4E398080B9700609bb008eceB125" + testnet: + avalanche-fuji: "0x2CDd45c5182602a36d391F7F16DD9f8386C3bD8D" + base-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" + eth-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" + optimism-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" + scroll-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" + deploymentCreationBlocks: + mainnet: + celo-mainnet: 17404156 + +# == Glossary == +# flowrate — token transfer rate in wad/second (int96) +# flowOperator — account authorized to create/update/delete flows on behalf of another +# buffer — deposit locked as solvency collateral while a flow is active +# permissions — bitmask of create | update | delete rights for an operator +# flowrateAllowance — max flowrate an operator may set per individual flow + +# == Flow Management == +# High-level functions that automatically create, update, or delete flows as needed. + +setFlowrate: + # Smart setter: creates a new flow if none exists, updates if flowrate changed, + # deletes if flowrate is zero. No-ops if the current rate already matches. + notes: + - "Gotcha: Negative flowrate values revert with CFA_FWD_INVALID_FLOW_RATE." + mutability: nonpayable + access: anyone # flow from msg.sender + inputs: + - token: address + - receiver: address + - flowrate: int96 # must be >= 0 + outputs: + - bool + errors: [CFA_FWD_INVALID_FLOW_RATE] + +setFlowrateFrom: + # Same as setFlowrate but can be called by a flow operator on behalf of `sender`. + # msg.sender must have sufficient operator permissions and flowrateAllowance. + mutability: nonpayable + access: sender | operator + inputs: + - token: address + - sender: address + - receiver: address + - flowrate: int96 + outputs: + - bool + errors: [CFA_FWD_INVALID_FLOW_RATE] + +# == Low-Level Flow Operations == +# Direct wrappers around CFA functions. These give full control (including userData) +# but require the caller to know whether a flow already exists. +# If sender != msg.sender, the *ByOperator variant is used internally. + +createFlow: + # Create a new flow. Reverts if a flow already exists between sender and receiver. + mutability: nonpayable + access: sender | operator + inputs: + - token: address + - sender: address + - receiver: address + - flowrate: int96 + - userData: bytes + outputs: + - bool + +updateFlow: + # Update an existing flow's rate. Reverts if no flow exists. + mutability: nonpayable + access: sender | operator + inputs: + - token: address + - sender: address + - receiver: address + - flowrate: int96 + - userData: bytes + outputs: + - bool + +deleteFlow: + # Delete an existing flow. Can be called by sender, receiver, or an operator. + # If msg.sender is neither sender nor receiver, deleteFlowByOperator is used. + mutability: nonpayable + access: sender | receiver | operator + inputs: + - token: address + - sender: address + - receiver: address + - userData: bytes + outputs: + - bool + +# == ACL (Operator Permissions) == + +grantPermissions: + # Grant full create/update/delete permissions with max flowrateAllowance to an operator. + # Convenience wrapper — equivalent to updateFlowOperatorPermissions with full control. + mutability: nonpayable + access: anyone # grants on msg.sender's flows + inputs: + - token: address + - flowOperator: address + outputs: + - bool + +revokePermissions: + # Revoke all permissions from an operator. Does not affect existing flows. + mutability: nonpayable + access: anyone # revokes on msg.sender's flows + inputs: + - token: address + - flowOperator: address + outputs: + - bool + +updateFlowOperatorPermissions: + # Set granular operator permissions and flowrate allowance. + notes: + - "Gotcha: flowrateAllowance limits per-flow rate, NOT aggregate net flow." + mutability: nonpayable + access: anyone # grants on msg.sender's flows + inputs: + - token: address + - flowOperator: address + - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete + - flowrateAllowance: int96 + outputs: + - bool + +# == Flow Queries == + +getFlowrate: + mutability: view + inputs: + - token: address + - sender: address + - receiver: address + outputs: + - flowrate: int96 + +getFlowInfo: + mutability: view + inputs: + - token: address + - sender: address + - receiver: address + outputs: + - lastUpdated: uint256 + - flowrate: int96 + - deposit: uint256 + - owedDeposit: uint256 + +getBufferAmountByFlowrate: + # Returns the deposit/buffer required for a given flowrate. + notes: + - "Gotcha: This value is governance-configurable and may change over time. Changes only affect newly created/updated flows." + mutability: view + inputs: + - token: address + - flowrate: int96 + outputs: + - bufferAmount: uint256 + +getAccountFlowrate: + # Net aggregate flowrate for an account (incoming minus outgoing). + mutability: view + inputs: + - token: address + - account: address + outputs: + - flowrate: int96 + +getAccountFlowInfo: + mutability: view + inputs: + - token: address + - account: address + outputs: + - lastUpdated: uint256 + - flowrate: int96 + - deposit: uint256 + - owedDeposit: uint256 + +# == ACL Queries == + +getFlowOperatorPermissions: + mutability: view + inputs: + - token: address + - sender: address + - flowOperator: address + outputs: + - permissions: uint8 + - flowrateAllowance: int96 + +# == Errors == + +errors: + - CFA_FWD_INVALID_FLOW_RATE # flowrate argument was negative diff --git a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml new file mode 100644 index 0000000..f98e21e --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml @@ -0,0 +1,19 @@ +# Generated by scripts/selectors.mjs +functions: + setFlowrate(address,address): 0x2f57fb6f + setFlowrateFrom(address,address,address,int96): 0xc5ad5c1a + createFlow(address,address,address,int96,bytes): 0xe15536b6 + updateFlow(address,address,address,int96,bytes): 0x0c033991 + deleteFlow(address,address,address,bytes): 0xb4b333c6 + grantPermissions(address,address): 0x7243fb93 + revokePermissions(address,address): 0x0bd0728d + updateFlowOperatorPermissions(address,address): 0x42294caf + getFlowrate(address,address,address): 0x1d8b6526 + getFlowInfo(address,address,address): 0x2860fd93 + getBufferAmountByFlowrate(address,int96): 0x09f0b495 + getAccountFlowrate(address,address): 0x22c904d9 + getAccountFlowInfo(address,address): 0x0f1ac495 + getFlowOperatorPermissions(address,address,address): 0x4d3f60f9 +events: +{} +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml new file mode 100644 index 0000000..afaec5d --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml @@ -0,0 +1,560 @@ +# Superfluid Constant Flow Agreement (CFA) v1 +# Manages continuous per-second token streams between accounts. +# +# NOTE: emits/errors mappings are traced from source code — verify against implementation. +# Proxy/upgradability functions (castrate, updateCode, getCodeAddress, proxiableUUID) +# are omitted — they belong to the UUPSProxiable / AgreementBase layer. +# Pure helpers addPermissions/removePermissions are omitted — they were made public +# for testability only and are not part of the protocol interface. + +meta: + name: ConstantFlowAgreementV1 + version: v1 + source: + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/agreements/IConstantFlowAgreementV1.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/agreements/AgreementBase.sol + implements: [IConstantFlowAgreementV1, ISuperAgreement] + inherits: [AgreementBase] + deployments: + mainnet: + eth-mainnet: "0x2844c1BBdA121E9E43105630b9C8310e5c72744b" + polygon-mainnet: "0x6EeE6060f715257b970700bc2656De21dEdF074C" + xdai-mainnet: "0xEbdA4ceF883A7B12c4E669Ebc58927FBa8447C7D" + base-mainnet: "0x19ba78B9cDB05A877718841c574325fdB53601bb" + optimism-mainnet: "0x204C6f131bb7F258b2Ea1593f5309911d8E458eD" + arbitrum-one: "0x731FdBB12944973B500518aea61942381d7e240D" + bsc-mainnet: "0x49c38108870e74Cb9420C0991a85D3edd6363F75" + avalanche-c: "0x6946c5B38Ffea373b0a2340b4AEf0De8F6782e58" + celo-mainnet: "0x9d369e78e1a682cE0F8d9aD849BeA4FE1c3bD3Ad" + scroll-mainnet: "0xB3bcD6da1eeB6c97258B3806A853A6dcD3B6C00c" + degenchain: "0x82cc052d1b17aC554a22A88D5876B56c6b51e95c" + testnet: + avalanche-fuji: "0x16843ac25Ccc58Aa7960ba05f61cBB17b36b130A" + base-sepolia: "0x6836F23d6171D74Ef62FcF776655aBcD2bcd62Ef" + eth-sepolia: "0x6836F23d6171D74Ef62FcF776655aBcD2bcd62Ef" + optimism-sepolia: "0x8a3170AdbC67233196371226141736E4151e7C26" + scroll-sepolia: "0xbc46B4Aa41c055578306820013d4B65fff42711E" + deploymentCreationBlocks: + mainnet: + celo-mainnet: 16393492 + +# == Abbreviations == +# 3Ps — Three Periods: liquidation, patrician, and pleb (solvency model) +# ACL — Access Control List (flow operator permissions) +# CFA — Constant Flow Agreement +# ctx — context (Superfluid call context bytes, carries msg.sender and userData) +# GDA — General Distribution Agreement (the other Superfluid agreement) +# PPP — Patrician-Pleb-Pirate (the 3Ps solvency periods) + +# == Glossary == +# flow / stream — continuous per-second token transfer, used interchangeably +# flowRate — tokens per second (int96, wei-denominated); positive = outgoing +# deposit — buffer locked when opening a flow; protects against insolvency +# owed deposit — portion of deposit owed back to the sender by a Super App receiver +# flow operator — an address authorized to create/update/delete flows on behalf of a sender +# flow rate allowance — maximum net flow rate an operator may allocate (int96); decrements on use +# permissions bitmask — uint8: bit 0 = create (1), bit 1 = update (2), bit 2 = delete (4) +# liquidation — closing an insolvent sender's flow; rewards go to the liquidator or bond +# patrician period — grace window after insolvency where reward goes to the bond account +# pleb period — window after patrician where reward goes to the liquidator +# pirate / bailout — state where total deposit cannot cover the deficit; protocol bails out +# Super App — a contract registered with the Host that receives agreement callbacks +# app credit — deposit credit enabling "zero-balance Super Apps" to relay flows +# without pre-funded tokens. 1:1 relay (one in, one out at same rate) +# always works at zero balance. Fan-out (1:N) needs the app to hold +# tokens. Credit is settled as "owed deposit" on the original sender. +# critical — an account whose available balance is negative (eligible for liquidation) +# jailed — a Super App penalized for violating protocol rules; loses callbacks +# +# == Time Conventions == +# Conventional seconds-per-period used in Superfluid apps for flow rate conversion: +# month: 2628000 (365.25 / 12 * 86400) +# year: 31536000 (365 * 86400) +# Flow rate from a monthly amount: flowRate = monthlyAmount / 2628000 +# Flow rate from a yearly amount: flowRate = yearlyAmount / 31536000 + +# == Flow Management == +# Core operations for creating, updating, and deleting token streams. +# Functions with ctx are called through the Host — either via +# Host.callAgreement (single op) or Host.batchCall (operationType 201). +# For batchCall, the data field is abi.encode(callData, userData) where +# callData is the full ABI-encoded function call with an empty ctx ("0x") +# as placeholder. The Host replaces the placeholder with the real context. +# The access field reflects who can initiate the call, not the direct caller. + +createFlow: + # Start a new stream from ctx.msgSender to receiver. + # The deposit amount depends on the governance-configured liquidation period + # and minimum deposit. The lower 32 bits of the deposit are clipped (rounded up). + notes: + - "Gotcha: A deposit is taken as a safety margin for solvency agents." + mutability: nonpayable + access: anyone # flow from ctx.msgSender + inputs: + - token: address + - receiver: address + - flowRate: int96 # must be > 0 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_ALREADY_EXISTS, CFA_INSUFFICIENT_BALANCE] + +updateFlow: + # Change the flow rate of an existing stream from ctx.msgSender to receiver. + # Note: owedDeposit accumulates when a Super App receiver uses app credit to + # open outgoing streams. The sender bears the deposit cost for those streams. + notes: + - "Gotcha: Deposit is adjusted to match the new flow rate but never refunds owed deposit — that adjustment happens separately via the app credit system." + mutability: nonpayable + access: anyone # flow from ctx.msgSender + inputs: + - token: address + - receiver: address + - flowRate: int96 # must be > 0 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_DOES_NOT_EXIST, CFA_INSUFFICIENT_BALANCE] + +deleteFlow: + # Stop a stream between sender and receiver. + # During liquidation the reward distribution depends on the solvency period: + # patrician → bond account gets reward; pleb → liquidator gets reward; + # pirate → liquidator gets full single deposit, protocol covers the bailout. + notes: + - "Gotcha: Third-party callers (not sender, receiver, or operator) only succeed if the sender is critical (negative available balance) or either party is jailed." + mutability: nonpayable + access: sender | receiver | operator | anyone(if-critical-or-jailed) + inputs: + - token: address + - sender: address + - receiver: address + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ZERO_ADDRESS_SENDER, CFA_ZERO_ADDRESS_RECEIVER, CFA_FLOW_DOES_NOT_EXIST, CFA_NON_CRITICAL_SENDER] + +# == Operator Flow Management == +# Act on behalf of another account. Requires ACL permissions granted by the sender. +# The operator's flow rate allowance is decremented on create/update. +# Functions with ctx are called through the Host. + +createFlowByOperator: + # Create a flow on behalf of sender. Consumes flow rate allowance. + notes: + - "Gotcha: Reverts if ctx.msgSender IS the sender — use createFlow instead." + mutability: nonpayable + access: operator + inputs: + - token: address + - sender: address + - receiver: address + - flowRate: int96 # must be > 0 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ACL_NO_SENDER_CREATE, CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS, CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED, CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_ALREADY_EXISTS, CFA_INSUFFICIENT_BALANCE] + +updateFlowByOperator: + # Update a flow on behalf of sender. Only consumes allowance if flow rate increases. + # If flowRateAllowance is type(int96).max, it is treated as unlimited. + notes: + - "Gotcha: If the new rate is lower than the old rate, no allowance is consumed." + mutability: nonpayable + access: operator + inputs: + - token: address + - sender: address + - receiver: address + - flowRate: int96 # must be > 0 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ACL_NO_SENDER_UPDATE, CFA_ACL_OPERATOR_NO_UPDATE_PERMISSIONS, CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED, CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_DOES_NOT_EXIST, CFA_INSUFFICIENT_BALANCE] + +deleteFlowByOperator: + # Delete a flow on behalf of sender. Does not consume flow rate allowance. + mutability: nonpayable + access: operator + inputs: + - token: address + - sender: address + - receiver: address + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ACL_OPERATOR_NO_DELETE_PERMISSIONS, CFA_ZERO_ADDRESS_SENDER, CFA_ZERO_ADDRESS_RECEIVER, CFA_FLOW_DOES_NOT_EXIST, CFA_NON_CRITICAL_SENDER] + +# == Flow Queries == + +realtimeBalanceOf: + # Compute the real-time CFA balance contribution for an account. + # Returns the dynamic balance delta (flowRate * elapsed), total deposit held, + # and total owed deposit. This is called by the token's realtimeBalanceOfNow + # to assemble the full balance across all agreements. + # To get an account's actual token balance, use SuperToken.realtimeBalanceOfNow + # (or SuperToken.balanceOf for ERC-20 compatible, clamped to zero). + # It can be deeply negative for net-outflow accounts as time progresses. + notes: + - "Gotcha: Returns only the CFA portion of the balance, not the total." + - "Gotcha: The returned dynamicBalance is a delta, not an absolute balance." + mutability: view + inputs: + - token: address + - account: address + - time: uint256 + outputs: + - dynamicBalance: int256 # flowRate * (time - lastUpdate); can be negative + - deposit: uint256 # total deposit locked across all outflows + - owedDeposit: uint256 # total owed deposit from Super App receivers + +getFlow: + # Get flow data between a specific sender-receiver pair. + mutability: view + inputs: + - token: address + - sender: address + - receiver: address + outputs: + - timestamp: uint256 # last update time + - flowRate: int96 + - deposit: uint256 + - owedDeposit: uint256 + +getFlowByID: + # Get flow data using the agreement ID (keccak256 of sender and receiver). + mutability: view + inputs: + - token: address + - flowId: bytes32 # keccak256(abi.encode(sender, receiver)) + outputs: + - timestamp: uint256 + - flowRate: int96 + - deposit: uint256 + - owedDeposit: uint256 + +getAccountFlowInfo: + # Aggregated flow state for an account across all its CFA flows. + mutability: view + inputs: + - token: address + - account: address + outputs: + - timestamp: uint256 # last time any flow was updated for this account + - flowRate: int96 # net flow rate (inflows - outflows) + - deposit: uint256 # sum of deposits across all outflows + - owedDeposit: uint256 # sum of owed deposits across all outflows + +getNetFlow: + # Net flow rate for an account (sum of inflows minus outflows). + mutability: view + inputs: + - token: address + - account: address + outputs: + - flowRate: int96 + +# == ACL Management == +# Manage flow operator permissions and flow rate allowances. +# The caller (ctx.msgSender) is always the permission granter — they control +# who can operate on their own flows. +# Functions with ctx are called through the Host. + +updateFlowOperatorPermissions: + # Set exact permissions and flow rate allowance for a flow operator. + mutability: nonpayable + access: anyone # grants on ctx.msgSender's flows + inputs: + - token: address + - flowOperator: address + - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete + - flowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +authorizeFlowOperatorWithFullControl: + # Grant all permissions (create+update+delete) with unlimited allowance. + # Shorthand for updateFlowOperatorPermissions(token, op, 7, type(int96).max, ctx). + mutability: nonpayable + access: anyone # grants on ctx.msgSender's flows + inputs: + - token: address + - flowOperator: address + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +revokeFlowOperatorWithFullControl: + # Revoke all permissions and set allowance to zero. + # Shorthand for updateFlowOperatorPermissions(token, op, 0, 0, ctx). + mutability: nonpayable + access: anyone # revokes on ctx.msgSender's flows + inputs: + - token: address + - flowOperator: address + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +increaseFlowRateAllowance: + # Increase flow rate allowance for an operator by a delta. + # Delegates to increaseFlowRateAllowanceWithPermissions with permissionsToAdd=0. + mutability: nonpayable + access: anyone # ctx.msgSender's allowance + inputs: + - token: address + - flowOperator: address + - addedFlowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +decreaseFlowRateAllowance: + # Decrease flow rate allowance for an operator by a delta. + # Delegates to decreaseFlowRateAllowanceWithPermissions with permissionsToRemove=0. + mutability: nonpayable + access: anyone # ctx.msgSender's allowance + inputs: + - token: address + - flowOperator: address + - subtractedFlowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +increaseFlowRateAllowanceWithPermissions: + # Increase flow rate allowance and add permission bits in one call. + # permissionsToAdd is OR'd with existing permissions. + mutability: nonpayable + access: anyone # ctx.msgSender's allowance + inputs: + - token: address + - flowOperator: address + - permissionsToAdd: uint8 # bitmask OR'd onto existing permissions + - addedFlowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +decreaseFlowRateAllowanceWithPermissions: + # Decrease flow rate allowance and remove permission bits in one call. + # permissionsToRemove is AND-NOT'd from existing permissions. + notes: + - "Gotcha: Reverts if the resulting allowance would go negative." + mutability: nonpayable + access: anyone # ctx.msgSender's allowance + inputs: + - token: address + - flowOperator: address + - permissionsToRemove: uint8 # bitmask AND-NOT'd from existing permissions + - subtractedFlowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +# == ACL Queries == + +getFlowOperatorData: + # Get permissions and allowance for a flow operator. + mutability: view + inputs: + - token: address + - sender: address # the permission granter + - flowOperator: address # the permission grantee + outputs: + - flowOperatorId: bytes32 # keccak256(abi.encode("flowOperator", sender, flowOperator)) + - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete + - flowRateAllowance: int96 + +getFlowOperatorDataByID: + # Get permissions and allowance using the pre-computed operator ID. + mutability: view + inputs: + - token: address + - flowOperatorId: bytes32 + outputs: + - permissions: uint8 + - flowRateAllowance: int96 + +# == Solvency Queries == +# Check whether an account is in the patrician period (grace window for liquidation). + +isPatricianPeriodNow: + # Check patrician period status using the Host's current timestamp. + mutability: view + inputs: + - token: address + - account: address + outputs: + - isCurrentlyPatricianPeriod: bool + - timestamp: uint256 # the Host's block.timestamp used for the check + +isPatricianPeriod: + # Check patrician period status at a specific timestamp. + mutability: view + inputs: + - token: address + - account: address + - timestamp: uint256 + outputs: + - bool + +# == Deposit Helpers == + +getMaximumFlowRateFromDeposit: + # Calculate the maximum flow rate achievable with a given deposit. + notes: + - "Gotcha: Deposit is clipped (lower 32 bits zeroed) and rounded down." + mutability: view + inputs: + - token: address # needed to look up liquidation period from governance + - deposit: uint256 + outputs: + - flowRate: int96 + errors: [CFA_DEPOSIT_TOO_BIG] + +getDepositRequiredForFlowRate: + # Calculate the deposit required for a given flow rate. + # Returns max(minimumDeposit, flowRate * liquidationPeriod) with rounding. + mutability: view + inputs: + - token: address # needed to look up liquidation period and minimum deposit + - flowRate: int96 + outputs: + - deposit: uint256 + errors: [CFA_INVALID_FLOW_RATE, CFA_FLOW_RATE_TOO_BIG] + +# == Protocol Constants == + +agreementType: + # Returns keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1") + mutability: pure + outputs: + - bytes32 + +DEFAULT_MINIMUM_DEPOSIT: + # Minimum deposit floor: uint96(1 << 32) ≈ 4.29 * 10^9 wei. + # Governance may set a higher per-token minimum; this is the absolute floor. + mutability: view + outputs: + - uint256 + +MAXIMUM_DEPOSIT: + # Maximum deposit cap: type(int96).max + mutability: view + outputs: + - uint256 + +MAXIMUM_FLOW_RATE: + # Maximum flow rate: type(int96).max + mutability: view + outputs: + - uint256 + +CFA_HOOK_GAS_LIMIT: + # Gas limit for external hook calls (Super App callbacks): 250,000 + mutability: view + outputs: + - uint64 + +# == Events == + +events: + FlowUpdated: + # Emitted on every create, update, and delete flow operation. + # totalSenderFlowRate and totalReceiverFlowRate are the NET flow rates + # after the operation, not the individual flow rate. + notes: + - "Gotcha: Always emitted together with FlowUpdatedExtension." + indexed: + - token: address + - sender: address + - receiver: address + data: + - flowRate: int96 # new rate for this specific flow (0 on delete) + - totalSenderFlowRate: int256 # sender's net flow rate after this operation + - totalReceiverFlowRate: int256 + - userData: bytes + + FlowUpdatedExtension: + # Companion event to FlowUpdated. Carries the operator and new deposit. + # Emitted immediately after FlowUpdated so indexers can correlate them. + indexed: + - flowOperator: address # ctx.msgSender who initiated the operation + data: + - deposit: uint256 # new deposit for this specific flow + + FlowOperatorUpdated: + # Emitted when operator permissions or flow rate allowance change. + indexed: + - token: address + - sender: address # the permission granter + - flowOperator: address # the permission grantee + data: + - permissions: uint8 # updated bitmask + - flowRateAllowance: int96 # updated allowance + + # Inherited events (from AgreementBase / UUPSProxiable): + # CodeUpdated — emitted on proxy upgrade (uuid, codeAddress) + # Initialized — emitted on proxy initialization (version) + +# == Errors == + +errors: + # Flow validation + - CFA_FLOW_ALREADY_EXISTS # createFlow when flow exists + - CFA_FLOW_DOES_NOT_EXIST # update/delete when no flow + - CFA_INVALID_FLOW_RATE # flowRate <= 0 + - CFA_NO_SELF_FLOW # sender == receiver + - CFA_ZERO_ADDRESS_SENDER # sender is address(0) + - CFA_ZERO_ADDRESS_RECEIVER # receiver is address(0) + # Solvency + - CFA_INSUFFICIENT_BALANCE # sender cannot cover deposit + - CFA_NON_CRITICAL_SENDER # third-party delete but sender is solvent + - CFA_DEPOSIT_TOO_BIG # deposit > MAXIMUM_DEPOSIT + - CFA_FLOW_RATE_TOO_BIG # flowRate * liquidationPeriod overflows + # ACL + - CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED # operator exceeds granted allowance + - CFA_ACL_NO_NEGATIVE_ALLOWANCE # resulting allowance would be < 0 + - CFA_ACL_NO_SENDER_CREATE # operator cannot be the sender (use createFlow) + - CFA_ACL_NO_SENDER_FLOW_OPERATOR # cannot set yourself as your own operator + - CFA_ACL_NO_SENDER_UPDATE # operator cannot be the sender (use updateFlow) + - CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS + - CFA_ACL_OPERATOR_NO_DELETE_PERMISSIONS + - CFA_ACL_OPERATOR_NO_UPDATE_PERMISSIONS + - CFA_ACL_UNCLEAN_PERMISSIONS # permission bits outside valid range + # Super App / Host + - CFA_HOOK_OUT_OF_GAS # Super App callback exceeded gas limit + - APP_RULE: # Super App rule violation + inputs: + - _code: uint256 + - AGREEMENT_BASE_ONLY_HOST # call not routed through the Host contract + # SafeCast (inherited from OpenZeppelin) + - SafeCastOverflowedIntToUint: # int256 value overflows on cast to uint256 + inputs: + - value: int256 + - SafeCastOverflowedUintToInt: # uint256 value overflows on cast to int256 + inputs: + - value: uint256 diff --git a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml new file mode 100644 index 0000000..a929e06 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml @@ -0,0 +1,33 @@ +# Generated by scripts/selectors.mjs +functions: + createFlow(address,address): 0x7deac86a + updateFlow(address,address): 0xb6214437 + deleteFlow(address,address,address,bytes): 0xb4b333c6 + createFlowByOperator(address,address,address): 0x97b0b745 + updateFlowByOperator(address,address,address): 0xaea4dc29 + deleteFlowByOperator(address,address,address,bytes): 0x4c8b181f + realtimeBalanceOf(address,address,uint256): 0x9b2e48bc + getFlow(address,address,address): 0xe6a1e888 + getFlowByID(address): 0x4eccd8ac + getAccountFlowInfo(address,address): 0x0f1ac495 + getNetFlow(address,address): 0xe8e7e2d1 + updateFlowOperatorPermissions(address,address): 0x42294caf + authorizeFlowOperatorWithFullControl(address,address,bytes): 0x54b770e3 + revokeFlowOperatorWithFullControl(address,address,bytes): 0x062e56ec + increaseFlowRateAllowance(address,address,int96,bytes): 0xac5f5d00 + decreaseFlowRateAllowance(address,address,int96,bytes): 0x5f51fb23 + increaseFlowRateAllowanceWithPermissions(address,address): 0x5e6a4dc3 + decreaseFlowRateAllowanceWithPermissions(address,address): 0x8de39d9b + getFlowOperatorData(address): 0x5f13dbbc + getFlowOperatorDataByID(address,bytes32): 0x09d256ef + isPatricianPeriodNow(address,address): 0x4fe9c291 + isPatricianPeriod(address,address,uint256): 0x4b839e0b + getMaximumFlowRateFromDeposit(): 0x4ba52c34 + getDepositRequiredForFlowRate(): 0xcc111b9e + CFA_HOOK_GAS_LIMIT(): 0xbf3fbc28 +events: + FlowUpdated(address,address,address,int256,bytes): 0x8e3b8f31fe09d2ca20fa7f76ec574bc9fea49f16a91f1ca828154ea76e76a20a + FlowUpdatedExtension(): 0x9bf0b3e0199b411ca09607d040b7672430af5dae9d159b715c9f8004c876065f + FlowOperatorUpdated(address): 0x401d2cb05b3db80617d70aabf976f52d2a5dac64298eadeed7a81ad4bb5d4fdd + # Inherited events (from AgreementBase / UUPSProxiable)(): 0xcabfd4ad95e3bb803739ac513717fccee724fc0c5741e01cf84319a96120eefa +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml new file mode 100644 index 0000000..c60d498 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml @@ -0,0 +1,100 @@ +# GoodDollarOFTAdapter — LayerZero V2 OFT adapter pattern bridging underlying G$ +# Exact bytecode lives in the GoodDollar OFT deployment; this documents the surface agents call. + +meta: + name: GoodDollarOFTAdapter + version: LayerZero-OFT-adapter (generic) + source: + - https://docs.layerzero.network/ + - https://github.com/LayerZero-Labs/devtools + note: > + Flow: optional peers(dstEid) check -> approve underlying to minterBurner if + MessagingFee, refundAddress) paying nativeFee in msg.value when applicable. + SendParam mirrors LayerZero OFT: dstEid, to (bytes32 padded recipient), + amountLD, minAmountLD, extraOptions, composeMsg, oftCmd. + Verify tuple field order and OFT revision against your deployed artifact before mainnet use. + Event names and topic0 hashes vary by OFT package version — pull from the deployment ABI. + related: + - references/guides/bridge.md + +token: + mutability: view + inputs: [] + outputs: + - underlyingToken: address + +minterBurner: + notes: + - "Underlying G$ must approve this spender when burning for cross-chain send." + mutability: view + inputs: [] + outputs: + - minterBurner: address + +oftVersion: + mutability: view + inputs: [] + outputs: + - interfaceId: bytes4 + - version: uint64 + +peers: + notes: + - "Returns bytes32 peer address configured for destination endpoint id." + mutability: view + inputs: + - dstEid: uint32 + outputs: + - peer: bytes32 + +endpoint: + mutability: view + inputs: [] + outputs: + - lzEndpoint: address + +owner: + mutability: view + inputs: [] + outputs: + - account: address + +quoteSend: + notes: + - "Simulates messaging + bridge fee; use nativeFee as msg.value on send when paying in native gas token." + mutability: view + inputs: + - sendParam: SendParam + - payInLzToken: bool + outputs: + - nativeFee: uint256 + - lzTokenFee: uint256 + errors: + - NO_PEER + - LZ_INVALID_OPTIONS + +send: + notes: + - "Payable: include MessagingFee.nativeFee in msg.value when fee is native." + - "Consumes sendParam.amountLD from sender on source chain and emits cross-chain message to dstEid peer." + - "sendParam.to must be destination receiver encoded as bytes32." + mutability: payable + access: sender + inputs: + - sendParam: SendParam + - fee: MessagingFee + - refundAddress: address + outputs: + - msgReceipt: MessageReceipt + - oftReceipt: OFTReceipt + errors: + - NO_PEER + - SLIPPAGE_OR_AMOUNT + - LZ_INVALID_OPTIONS + +events: {} + +errors: + NO_PEER: "Destination peer not configured for dstEid." + LZ_INVALID_OPTIONS: "Composer/options payload rejected by LayerZero." + SLIPPAGE_OR_AMOUNT: "Bridged amount violates minAmount or available balance." diff --git a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml new file mode 100644 index 0000000..e5615a1 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml @@ -0,0 +1,12 @@ +# Generated by scripts/selectors.mjs +functions: + token(): 0xfc0c546a + minterBurner(): 0x2ef8c5a4 + approvalRequired(): 0x9f68b964 + oftVersion(): 0x156a0d0f + peers(uint32): 0xbb0b6a53 + endpoint(): 0x5e280f11 + owner(): 0x8da5cb5b +events: +{} +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml new file mode 100644 index 0000000..d736bc1 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml @@ -0,0 +1,210 @@ +# GooddollarSavingsStream (Ubeswap) — G$ native Super Token stake with Superfluid GDA reward stream + +meta: + name: GooddollarSavingsStream + version: "1" + source: + - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/GooddollarSavingsStream.sol + - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/interfaces/IGooddollarSavingsStream.sol + - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/StakingVault.sol + inherits: + - IGooddollarSavingsStream + - Ownable + - ERC2771Context + - ReentrancyGuard + note: > + Celo production savings. Staked principal sits in StakingVault; this contract holds reward + balance and streams via a Superfluid GDA distribution pool (`distributeFlow`). Effective rate is + min(rewardRate, maxRewardRatePerToken * totalSupply / 1e18), throttled when reward balance is low. + Staking token is the G$ native Super Token (`superToken`). Fuse->CELO migration uses `stakeFor` + after bridge. Ubeswap lists this as Celo mainnet savings (streaming). + deployments: + mainnet: + production-celo: + GooddollarSavingsStream: + networkId: 42220 + address: "0x059ee811414230d1Fb157878D2b491240F4D8d3B" + creationBlock: 66685884 + related: + - https://github.com/Ubeswap/gooddollar-contracts/blob/main/README.md + - https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B + - references/contracts/SuperToken.abi.yaml + - references/guides/migrate-fuse-staking-to-celo-savings.md + - references/deep-researches/fuse-to-celo-staking-migration.md + +totalSupply: + mutability: view + inputs: [] + outputs: + - supply: uint256 + +balanceOf: + mutability: view + inputs: + - account: address + outputs: + - balance: uint256 + +getDailyRewards: + mutability: view + inputs: [] + outputs: + - daily: uint256 + +getEffectiveFlowRate: + notes: + - "Returns 0 when totalSupply is 0, rewardRate is 0, or reward balance cannot sustain MIN_STREAM_BUFFER_SECONDS." + - "Otherwise min(global rewardRate, APR cap) as int96 for Superfluid distributeFlow." + mutability: view + inputs: [] + outputs: + - flowRate: int96 + +periodFinish: + notes: + - "Estimated timestamp when current reward balance is drained at getEffectiveFlowRate; 0 if not streaming." + mutability: view + inputs: [] + outputs: + - finish: uint256 + +getUnits: + mutability: view + inputs: + - account: address + outputs: + - units: uint128 + +getTotalUnits: + mutability: view + inputs: [] + outputs: + - units: uint128 + +stake: + notes: + - "Pulls G$ Super Token from msg.sender, deposits principal to StakingVault, updates GDA pool units, syncFlowRate." + - "Requires prior superToken.approve(this, amount)." + mutability: nonpayable + access: anyone + inputs: + - amount: uint256 + outputs: [] + emits: [Staked] + +stakeFor: + notes: + - "Migrator path: caller funds transfer; stake credits `_balances[recipient]` and pool units for recipient." + - "recipient must not be zero, this, superToken, or vault." + - "Used after Fuse->CELO bridge when backend wallet holds bridged G$." + mutability: nonpayable + access: anyone + inputs: + - amount: uint256 + - recipient: address + outputs: [] + emits: [Staked] + +withdraw: + notes: + - "Reduces stake and pool units, syncFlowRate, then vault.withdraw to msg.sender." + mutability: nonpayable + access: anyone + inputs: + - amount: uint256 + outputs: [] + emits: [Withdrawn] + +exit: + notes: + - "Full withdraw of caller principal via withdraw(balance)." + mutability: nonpayable + access: anyone + inputs: [] + outputs: [] + emits: [Withdrawn] + +addToReward: + notes: + - "Pulls reward Super Token from caller; re-syncs flow rate when balance was throttling the stream." + mutability: nonpayable + access: anyone + inputs: + - reward: uint256 + outputs: [] + emits: [RewardAdded] + +syncFlowRate: + notes: + - "Permissionless; call when tokens were sent directly to the contract so distributeFlow picks up balance." + mutability: nonpayable + access: anyone + inputs: [] + outputs: [] + emits: [FlowRateUpdated] + +setDailyRewards: + mutability: nonpayable + access: owner + inputs: + - _dailyRewards: uint256 + outputs: [] + emits: [DailyRewardsUpdated] + +setMaxRewardRatePerToken: + mutability: nonpayable + access: owner + inputs: + - _value: uint256 + outputs: [] + emits: [MaxRewardRateUpdated] + +recoverERC20: + notes: + - "Cannot recover the G$ Super Token (staking/reward asset)." + mutability: nonpayable + access: owner + inputs: + - tokenAddress: address + - tokenAmount: uint256 + outputs: [] + emits: [Recovered] + +events: + Staked: + indexed: + - user: address + data: + - amount: uint256 + Withdrawn: + indexed: + - user: address + data: + - amount: uint256 + RewardAdded: + data: + - reward: uint256 + DailyRewardsUpdated: + data: + - rewardRate: uint256 + - givenDailyRewards: uint256 + MaxRewardRateUpdated: + data: + - newMaxRate: uint256 + FlowRateUpdated: + data: + - newFlowRate: int96 + Recovered: + indexed: + - token: address + data: + - amount: uint256 + - receiver: address + +errors: + - CannotStakeZero + - CannotWithdrawZero + - InsufficientStake + - InvalidAddress + - NoRewardToAdd + - CannotRecoverStakingToken diff --git a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml new file mode 100644 index 0000000..f04d75a --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml @@ -0,0 +1,27 @@ +# Generated by scripts/selectors.mjs +functions: + totalSupply(): 0x18160ddd + balanceOf(address): 0x70a08231 + getDailyRewards(): 0x68527008 + getEffectiveFlowRate(): 0xa2efe857 + periodFinish(): 0xebe2b12b + getUnits(address): 0x0fefbc09 + getTotalUnits(): 0xa754a702 + stake(uint256): 0xa694fc3a + stakeFor(uint256,address): 0x51746bb2 + withdraw(uint256): 0x2e1a7d4d + exit(): 0xe9fad8ee + addToReward(uint256): 0x2ce618fa + syncFlowRate(): 0x0e83284a + setDailyRewards(uint256): 0x4adef718 + setMaxRewardRatePerToken(uint256): 0x6a03a9e2 + recoverERC20(address,uint256): 0x8980f11f +events: + Staked(address,uint256): 0x9e71bc8eea02a63969f509818f2dafb9254532904319f9dbda79b67bd34a5f3d + Withdrawn(address,uint256): 0x7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d5 + RewardAdded(uint256): 0xde88a922e0d3b88b24e9623efeb464919c6bf9f66857a65e2bfcf2ce87a9433d + DailyRewardsUpdated(uint256,uint256): 0x54f5b7ef058007cebca11af0127cd80c0bac4e968788eabb3c1e70d1bfb78edd + MaxRewardRateUpdated(uint256): 0x9041b23d05af9bceefc73becffefd1887907ad55c3d6c6655230b37ef87dbb07 + FlowRateUpdated(int96): 0xd02fcf6b96acb60ac68942c7d5075e11a8a0b38cc865c1916934f4e1f129ded0 + Recovered(address,uint256,address): 0xb197f0a554c4d7840105e6ae65f0e275e9e8605a969dffa8caa7f1f118a2e1f5 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml new file mode 100644 index 0000000..5adccb3 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml @@ -0,0 +1,202 @@ +# GovernanceStakingV2 (Fuse) — G$ staking share token with GDAO reward accrual +# Deployment key in GoodProtocol release metadata is GovernanceStakingV2. + +meta: + name: GovernanceStakingV2 + version: "2" + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/governance/GovernanceStaking.sol + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/releases/deployment.json + inherits: + - ERC20Upgradeable + - MultiBaseGovernanceShareField + - DAOUpgradeableContract + - ReentrancyGuardUpgradeable + note: > + Fuse governance staking contract where users stake G$ and receive staking shares (`sG$`) with + GDAO-style rewards minted on reward withdrawal paths. `withdrawStake(0)` means full unstake. + In GoodProtocol `releases/deployment.json` this address is listed as GovernanceStakingV2 on Fuse `production`. + This is the old staking contract in Fuse->CELO migration flows. + deployments: + mainnet: + production: + GovernanceStakingV2: + networkId: 122 + address: "0xB7C3e738224625289C573c54d402E9Be46205546" + creationBlock: 15956809 + related: + - references/guides/migrate-fuse-staking-to-celo-savings.md + - references/deep-researches/fuse-to-celo-staking-migration.md + +getChainBlocksPerMonth: + mutability: pure + inputs: [] + outputs: + - blocks: uint256 + +stake: + notes: + - "Requires prior G$ ERC20 approval to staking contract." + - "Mints staking share token and updates reward productivity before event emission." + mutability: nonpayable + access: anyone + inputs: + - _amount: uint256 + outputs: [] + emits: [Staked] + +withdrawStake: + notes: + - "If _amount is 0, contract interprets it as full user unstake." + - "Burns staking shares, updates productivity, mints pending rewards, then transfers G$ out." + mutability: nonpayable + access: anyone + inputs: + - _amount: uint256 + outputs: [] + emits: [StakeWithdraw] + +withdrawRewards: + notes: + - "Claims rewards without changing staked principal." + mutability: nonpayable + access: anyone + inputs: [] + outputs: + - minted: uint256 + emits: [ReputationEarned] + +setMonthlyRewards: + notes: + - "DAO-controlled update of rewardsPerBlock schedule via avatar authorization." + mutability: nonpayable + access: avatar + inputs: + - _monthlyAmount: uint256 + outputs: [] + +getRewardsPerBlock: + mutability: view + inputs: [] + outputs: + - amount: uint256 + +getProductivity: + mutability: view + inputs: + - _user: address + outputs: + - productivity: uint256 + - rewardDebt: uint256 + +getUserPendingReward: + mutability: view + inputs: + - _user: address + outputs: + - pending: uint256 + +users: + notes: + - "UserInfo getter for internal staking productivity record." + mutability: view + inputs: + - _user: address + outputs: + - amount: uint256 + - rewardDebt: uint256 + +totalRewardsPerShare: + mutability: view + inputs: [] + outputs: + - value: uint256 + +decimals: + mutability: view + inputs: [] + outputs: + - d: uint8 + +totalSupply: + mutability: view + inputs: [] + outputs: + - supply: uint256 + +balanceOf: + mutability: view + inputs: + - account: address + outputs: + - balance: uint256 + +allowance: + mutability: view + inputs: + - owner: address + - spender: address + outputs: + - amount: uint256 + +approve: + mutability: nonpayable + access: anyone + inputs: + - spender: address + - amount: uint256 + outputs: + - ok: bool + emits: [Approval] + +transfer: + mutability: nonpayable + access: anyone + inputs: + - to: address + - amount: uint256 + outputs: + - ok: bool + emits: [Transfer] + +transferFrom: + mutability: nonpayable + access: anyone + inputs: + - from: address + - to: address + - amount: uint256 + outputs: + - ok: bool + emits: [Transfer, Approval] + +events: + ReputationEarned: + indexed: + - staker: address + data: + - amount: uint256 + Staked: + indexed: + - staker: address + data: + - amount: uint256 + StakeWithdraw: + indexed: + - staker: address + data: + - amount: uint256 + Transfer: + indexed: + - from: address + - to: address + data: + - value: uint256 + Approval: + indexed: + - owner: address + - spender: address + data: + - value: uint256 + +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml new file mode 100644 index 0000000..37388ba --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml @@ -0,0 +1,26 @@ +# Generated by scripts/selectors.mjs +functions: + getChainBlocksPerMonth(): 0x213b329e + stake(uint256): 0xa694fc3a + withdrawStake(uint256): 0x25d5971f + withdrawRewards(): 0xc7b8981c + setMonthlyRewards(uint256): 0xc76279a2 + getRewardsPerBlock(): 0x0c1cd7f3 + getProductivity(address): 0x28e964e9 + getUserPendingReward(address): 0xc6710629 + users(address): 0xa87430ba + totalRewardsPerShare(): 0xbf8e9b6e + decimals(): 0x313ce567 + totalSupply(): 0x18160ddd + balanceOf(address): 0x70a08231 + allowance(address,address): 0xdd62ed3e + approve(address,uint256): 0x095ea7b3 + transfer(address,uint256): 0xa9059cbb + transferFrom(address,address,uint256): 0x23b872dd +events: + ReputationEarned(address,uint256): 0x43848d0574703c28d68ae8958e0571521618f60c4bcacfb094cff2156eaae0f1 + Staked(address,uint256): 0x9e71bc8eea02a63969f509818f2dafb9254532904319f9dbda79b67bd34a5f3d + StakeWithdraw(address,uint256): 0x1248d48e2de900a1010c7fce73506969ecec243600bfc08b641b158f26d857cd + Transfer(address,address,uint256): 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + Approval(address,address,uint256): 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml new file mode 100644 index 0000000..aea2efe --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml @@ -0,0 +1,390 @@ +# IdentityV3 — whitelist, blacklist, DID, and connected-account graph for GoodDollar +# UBI eligibility uses getWhitelistedRoot; connectAccount links extra wallets to a root. + +meta: + name: IdentityV3 + version: v3 + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/identity/IdentityV3.sol + inherits: + - DAOUpgradeableContract + - AccessControlUpgradeable + - PausableUpgradeable + - EIP712Upgradeable + note: > + isWhitelisted enforces authenticationPeriod against dateAuthenticated and status==1, + with fallback to oldIdentity. getWhitelistedRoot returns the root address for a + connected wallet or the wallet itself when directly whitelisted. + deployments: + mainnet: + production: + Identity: + networkId: 122 + address: "0x2F9C28de9e6d44b71B91b8BA337A5D82e308E7BE" + creationBlock: 22022901 + IdentityOld: + networkId: 122 + address: "0xFa8d865A962ca8456dF331D78806152d3aC5B84F" + creationBlock: 6246324 + production-celo: + Identity: + networkId: 42220 + address: "0xC361A6E67822a0EDc17D899227dd9FC50BD62F42" + creationBlock: 17237952 + production-xdc: + Identity: + networkId: 50 + address: "0x27a4a02C9ed591E1a86e2e5D05870292c34622C9" + creationBlock: 95143058 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/identity + - https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _owner: address + - _oldIdentity: address + outputs: [] + +initDAO: + notes: + - "Wires NameService via setDAO and transfers DEFAULT_ADMIN_ROLE, PAUSER_ROLE, IDENTITY_ADMIN_ROLE to avatar." + mutability: nonpayable + access: DEFAULT_ADMIN_ROLE + inputs: + - _ns: address + outputs: [] + errors: [ALREADY_INITIALIZED] + +setAuthenticationPeriod: + mutability: nonpayable + access: avatar + inputs: + - period: uint256 + outputs: [] + errors: [WHEN_NOT_PAUSED_REVERT] + +authenticate: + notes: + - "Public wrapper; forwards to authenticateWithTimestamp(account, block.timestamp)." + mutability: nonpayable + inputs: + - account: address + outputs: [] + +authenticateWithTimestamp: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - timestamp: uint256 + outputs: [] + emits: [WhitelistedAuthenticated] + errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [WhitelistedAdded] + errors: [ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelistedWithDIDAndChain: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - did: string + - orgChain: uint256 + - dateAuthenticated: uint256 + outputs: [] + emits: [WhitelistedAdded] + errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelistedWithDID: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - did: string + outputs: [] + emits: [WhitelistedAdded] + errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +removeWhitelisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [WhitelistedRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +renounceWhitelisted: + mutability: nonpayable + access: whitelisted sender + inputs: [] + outputs: [] + emits: [WhitelistedRemoved] + errors: [NOT_WHITELISTED_SENDER, WHEN_NOT_PAUSED_REVERT] + +isWhitelisted: + notes: + - "Combines local Identity record with optional oldIdentity.isWhitelisted try/catch." + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +lastAuthenticated: + mutability: view + inputs: + - account: address + outputs: + - ts: uint256 + +addBlacklisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [BlacklistAdded, WhitelistedRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +removeBlacklisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [BlacklistRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +addContract: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [ContractAdded] + errors: [NOT_CONTRACT, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +removeContract: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [ContractRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +isDAOContract: + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +isBlacklisted: + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +connectAccount: + notes: + - "Caller must be whitelisted; target must not already be whitelisted or blacklisted; stores connectedAccounts[account]=msg.sender." + mutability: nonpayable + access: whitelisted sender + inputs: + - account: address + outputs: [] + emits: [AccountConnected] + errors: [INVALID_ACCOUNT_FOR_CONNECT, ALREADY_CONNECTED, WHEN_NOT_PAUSED_REVERT] + +disconnectAccount: + notes: + - "Caller must be either the linked root or the connected wallet." + mutability: nonpayable + inputs: + - connected: address + outputs: [] + emits: [AccountDisconnected] + errors: [UNAUTHORIZED_DISCONNECT] + +getWhitelistedRoot: + notes: + - "Returns account if directly whitelisted; returns connected root if connected account is whitelisted; else zero." + mutability: view + inputs: + - account: address + outputs: + - whitelisted: address + +pause: + mutability: nonpayable + access: PAUSER_ROLE + inputs: + - toPause: bool + outputs: [] + +setDID: + mutability: nonpayable + inputs: + - account: address + - did: string + outputs: [] + errors: [NOT_AUTHORIZED_SET_DID, NOT_WHITELISTED_FOR_DID, DID_EMPTY, DID_ALREADY_REGISTERED, DID_ALREADY_REGISTERED_OLD, WHEN_NOT_PAUSED_REVERT] + +addrToDID: + mutability: view + inputs: + - account: address + outputs: + - did: string + +getWhitelistedOnChainId: + mutability: view + inputs: + - account: address + outputs: + - chainId: uint256 + +isRegistered: + notes: + - "Compatibility shim; always returns true." + mutability: pure + inputs: [] + outputs: + - ok: bool + +IDENTITY_ADMIN_ROLE: + mutability: view + inputs: [] + outputs: + - role: bytes32 + +PAUSER_ROLE: + mutability: view + inputs: [] + outputs: + - role: bytes32 + +TYPED_STRUCTURE: + mutability: view + inputs: [] + outputs: + - schema: string + +whitelistedCount: + mutability: view + inputs: [] + outputs: + - n: uint256 + +whitelistedContracts: + mutability: view + inputs: [] + outputs: + - n: uint256 + +authenticationPeriod: + mutability: view + inputs: [] + outputs: + - seconds: uint256 + +identities: + mutability: view + inputs: + - account: address + outputs: + - dateAuthenticated: uint256 + - dateAdded: uint256 + - did: string + - whitelistedOnChainId: uint256 + - status: uint8 + +didHashToAddress: + mutability: view + inputs: + - hash: bytes32 + outputs: + - account: address + +connectedAccounts: + mutability: view + inputs: + - account: address + outputs: + - root: address + +oldIdentity: + mutability: view + inputs: [] + outputs: + - addr: address + +events: + BlacklistAdded: + indexed: + - account: address + data: [] + BlacklistRemoved: + indexed: + - account: address + data: [] + WhitelistedAdded: + indexed: + - account: address + data: [] + WhitelistedRemoved: + indexed: + - account: address + data: [] + WhitelistedAuthenticated: + indexed: + - account: address + data: + - timestamp: uint256 + ContractAdded: + indexed: + - account: address + data: [] + ContractRemoved: + indexed: + - account: address + data: [] + AccountConnected: + indexed: + - connected: address + - to: address + data: [] + AccountDisconnected: + indexed: + - disconnected: address + - from: address + data: [] + +errors: + ALREADY_INITIALIZED: "already initialized — initDAO twice." + NOT_WHITELISTED_STATUS: "not whitelisted — authenticateWithTimestamp." + NOT_WHITELISTED_SENDER: "not whitelisted — renounceWhitelisted modifier." + NOT_WHITELISTED_FOR_DID: "not whitelisted — setDID internal." + ALREADY_HAS_STATUS: "already has status — _addWhitelisted." + DID_ALREADY_REGISTERED: "DID already registered — _addWhitelistedWithDID or _setDID." + DID_ALREADY_REGISTERED_OLD: "DID already registered oldIdentity — _setDID conflict path." + NOT_CONTRACT: "Given address is not a contract — addContract." + INVALID_ACCOUNT_FOR_CONNECT: "invalid account — connectAccount target checks." + ALREADY_CONNECTED: "already connected — connectAccount." + UNAUTHORIZED_DISCONNECT: "unauthorized — disconnectAccount." + NOT_AUTHORIZED_SET_DID: "not authorized — setDID caller." + DID_EMPTY: "did empty — _setDID." + WHEN_NOT_PAUSED_REVERT: "OpenZeppelin Pausable whenNotPaused on gated calls." diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml new file mode 100644 index 0000000..9b53f00 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml @@ -0,0 +1,49 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address,address): 0x485cc955 + initDAO(address): 0x2b14dda8 + setAuthenticationPeriod(uint256): 0xfff930d4 + authenticate(address): 0x08e0d29d + authenticateWithTimestamp(address,uint256): 0x96a1ef79 + addWhitelisted(address): 0x10154bad + addWhitelistedWithDIDAndChain(address,string,uint256,uint256): 0xe737031a + addWhitelistedWithDID(address,string): 0x1b027099 + removeWhitelisted(address): 0x291d9549 + renounceWhitelisted(): 0xd6cd9473 + isWhitelisted(address): 0x3af32abf + lastAuthenticated(address): 0xe1e360ba + addBlacklisted(address): 0x188efc16 + removeBlacklisted(address): 0xc6a276c2 + addContract(address): 0x5f539d69 + removeContract(address): 0xc375c2ef + isDAOContract(address): 0xc73cc4ae + isBlacklisted(address): 0xfe575a87 + connectAccount(address): 0xd21685f4 + disconnectAccount(address): 0x37a1a987 + getWhitelistedRoot(address): 0x2d0e9b46 + pause(bool): 0x02329a29 + setDID(address,string): 0xd3262816 + addrToDID(address): 0x54f9f7a3 + getWhitelistedOnChainId(address): 0xa061922d + isRegistered(): 0x22366844 + IDENTITY_ADMIN_ROLE(): 0x1aaff63c + PAUSER_ROLE(): 0xe63ab1e9 + TYPED_STRUCTURE(): 0x2cec5330 + whitelistedCount(): 0xb2a1de22 + whitelistedContracts(): 0xb30f7e7f + authenticationPeriod(): 0x31b376e2 + identities(address): 0xf653b81e + didHashToAddress(bytes32): 0x67c75937 + connectedAccounts(address): 0x61320040 + oldIdentity(): 0x4125f0f2 +events: + BlacklistAdded(address): 0x44d5fe68b00f68950fb9c1ff0a61ef7f747b1a36359a7e3a7f3324db4b878967 + BlacklistRemoved(address): 0x1747ca720b1a174a464b6513ace29b1d3190b5f632b9f34147017c81425bfde8 + WhitelistedAdded(address): 0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f + WhitelistedRemoved(address): 0x270d9b30cf5b0793bbfd54c9d5b94aeb49462b8148399000265144a8722da6b6 + WhitelistedAuthenticated(address,uint256): 0xb2a82fce6d8c7a633efe9579f77b4edb96bfdf171a49bfc2ce666dc543a1f500 + ContractAdded(address): 0x89c66952b48f3e96bf1d8ba1b63189520fd988a6979b8b740bd5c5d8dc53e205 + ContractRemoved(address): 0x8d30d41865a0b811b9545d879520d2dde9f4cc49e4241f486ad9752bc904b565 + AccountConnected(address,address): 0x18f7736ef54539debd9afd3c9500b106e12ae7c70e685f5a5efd727b1ce1d54c + AccountDisconnected(address,address): 0x7cdef5f9c5cb8ce728661ede956fef26cb91eb4d7d2180cc041b73f9fef568d2 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml new file mode 100644 index 0000000..485254d --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml @@ -0,0 +1,424 @@ +# IdentityV4 — whitelist, blacklist, DID, connected accounts, and reverification schedule +# isWhitelisted uses shouldReverify against reverifyDaysOptions and authCount; oldIdentity fallback preserved. + +meta: + name: IdentityV4 + version: v4 + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/identity/IdentityV4.sol + inherits: + - DAOUpgradeableContract + - AccessControlUpgradeable + - PausableUpgradeable + - EIP712Upgradeable + note: > + authenticationPeriod() returns the largest reverify interval in days (last entry of reverifyDaysOptions), + not seconds. isWhitelisted is false when shouldReverify is true for the current elapsed days since + dateAuthenticated. Post-upgrade, accounts with dateAuthenticated before 1772697574 are treated as + having authCount at the last schedule step for reverify logic. + deployments: + mainnet: + production: + Identity: + networkId: 122 + address: "0x2F9C28de9e6d44b71B91b8BA337A5D82e308E7BE" + creationBlock: 22022901 + production-celo: + Identity: + networkId: 42220 + address: "0xC361A6E67822a0EDc17D899227dd9FC50BD62F42" + creationBlock: 17237952 + production-xdc: + Identity: + networkId: 50 + address: "0x27a4a02C9ed591E1a86e2e5D05870292c34622C9" + creationBlock: 95143058 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/identity + - https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _owner: address + - _oldIdentity: address + outputs: [] + +initDAO: + notes: + - "Wires NameService via setDAO and transfers DEFAULT_ADMIN_ROLE, PAUSER_ROLE, IDENTITY_ADMIN_ROLE to avatar." + mutability: nonpayable + access: DEFAULT_ADMIN_ROLE + inputs: + - _ns: address + outputs: [] + errors: [ALREADY_INITIALIZED] + +setReverifyDaysOptions: + notes: + - "Replaces the full schedule; values must be strictly ascending uint8 days; non-empty array." + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - options: uint8[] + outputs: [] + errors: [EMPTY_REVERIFY_OPTIONS, OPTIONS_NOT_ASCENDING, WHEN_NOT_PAUSED_REVERT] + +authenticate: + notes: + - "Forwards to authenticateWithTimestamp(account, block.timestamp); requires IDENTITY_ADMIN_ROLE on the inner call." + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] + +authenticateWithTimestamp: + notes: + - "Refreshes authentication timestamp for status-1 account and advances authCount when reverification threshold is reached." + - "Legacy accounts before cutoff are evaluated at last schedule step for reverify math." + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - timestamp: uint256 + outputs: [] + emits: [WhitelistedAuthenticated] + errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [WhitelistedAdded] + errors: [ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelistedWithDIDAndChain: + notes: + - "Adds whitelisted account and stores DID plus originating chain id and authentication timestamp." + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - did: string + - orgChain: uint256 + - dateAuthenticated: uint256 + outputs: [] + emits: [WhitelistedAdded] + errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelistedWithDID: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - did: string + outputs: [] + emits: [WhitelistedAdded] + errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +removeWhitelisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [WhitelistedRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +renounceWhitelisted: + mutability: nonpayable + access: whitelisted sender + inputs: [] + outputs: [] + emits: [WhitelistedRemoved] + errors: [NOT_WHITELISTED_SENDER, WHEN_NOT_PAUSED_REVERT] + +shouldReverify: + notes: + - "Compares daysSinceAuth against current reverifyDaysOptions step selected by authCount." + mutability: view + inputs: + - account: address + - daysSinceAuth: uint256 + outputs: + - needed: bool + +isWhitelisted: + notes: + - "Local status 1 with shouldReverify false; else oldIdentity.isWhitelisted try/catch." + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +lastAuthenticated: + notes: + - "Returns local dateAuthenticated or falls back to oldIdentity when local record has no timestamp." + mutability: view + inputs: + - account: address + outputs: + - ts: uint256 + +addBlacklisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [BlacklistAdded, WhitelistedRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +removeBlacklisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [BlacklistRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +addContract: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [ContractAdded] + errors: [NOT_CONTRACT, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +removeContract: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [ContractRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +isDAOContract: + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +isBlacklisted: + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +connectAccount: + notes: + - "Caller must be whitelisted; target must not already be whitelisted or blacklisted; stores connectedAccounts[account]=msg.sender." + mutability: nonpayable + access: whitelisted sender + inputs: + - account: address + outputs: [] + emits: [AccountConnected] + errors: [INVALID_ACCOUNT_FOR_CONNECT, ALREADY_CONNECTED, WHEN_NOT_PAUSED_REVERT] + +disconnectAccount: + notes: + - "Caller must be either the linked root or the connected wallet." + mutability: nonpayable + inputs: + - connected: address + outputs: [] + emits: [AccountDisconnected] + errors: [UNAUTHORIZED_DISCONNECT] + +getWhitelistedRoot: + notes: + - "Returns account if directly whitelisted; returns connected root if connected account is whitelisted; else zero." + mutability: view + inputs: + - account: address + outputs: + - whitelisted: address + +pause: + mutability: nonpayable + access: PAUSER_ROLE + inputs: + - toPause: bool + outputs: [] + +setDID: + notes: + - "Sets DID for account by admin or account itself when whitelisted; enforces uniqueness across local and old identity." + mutability: nonpayable + inputs: + - account: address + - did: string + outputs: [] + errors: [NOT_AUTHORIZED_SET_DID, NOT_WHITELISTED_FOR_DID, DID_EMPTY, DID_ALREADY_REGISTERED, DID_ALREADY_REGISTERED_OLD, WHEN_NOT_PAUSED_REVERT] + +addrToDID: + mutability: view + inputs: + - account: address + outputs: + - did: string + +getWhitelistedOnChainId: + notes: + - "Returns chain id recorded during whitelisting for cross-chain eligibility checks." + mutability: view + inputs: + - account: address + outputs: + - chainId: uint256 + +isRegistered: + notes: + - "Compatibility shim; always returns true." + mutability: pure + inputs: [] + outputs: + - ok: bool + +IDENTITY_ADMIN_ROLE: + mutability: view + inputs: [] + outputs: + - role: bytes32 + +PAUSER_ROLE: + mutability: view + inputs: [] + outputs: + - role: bytes32 + +TYPED_STRUCTURE: + mutability: view + inputs: [] + outputs: + - schema: string + +whitelistedCount: + mutability: view + inputs: [] + outputs: + - n: uint256 + +whitelistedContracts: + mutability: view + inputs: [] + outputs: + - n: uint256 + +authenticationPeriod: + notes: + - "Returns reverifyDaysOptions[last] in days, not seconds." + mutability: view + inputs: [] + outputs: + - days: uint256 + +reverifyDaysOptions: + mutability: view + inputs: + - index: uint256 + outputs: + - days: uint32 + +identities: + mutability: view + inputs: + - account: address + outputs: + - dateAuthenticated: uint256 + - dateAdded: uint256 + - did: string + - whitelistedOnChainId: uint256 + - status: uint8 + - authCount: uint32 + +didHashToAddress: + mutability: view + inputs: + - hash: bytes32 + outputs: + - account: address + +connectedAccounts: + mutability: view + inputs: + - account: address + outputs: + - root: address + +oldIdentity: + mutability: view + inputs: [] + outputs: + - addr: address + +events: + BlacklistAdded: + indexed: + - account: address + data: [] + BlacklistRemoved: + indexed: + - account: address + data: [] + WhitelistedAdded: + indexed: + - account: address + data: [] + WhitelistedRemoved: + indexed: + - account: address + data: [] + WhitelistedAuthenticated: + indexed: + - account: address + data: + - timestamp: uint256 + ContractAdded: + indexed: + - account: address + data: [] + ContractRemoved: + indexed: + - account: address + data: [] + AccountConnected: + indexed: + - connected: address + - to: address + data: [] + AccountDisconnected: + indexed: + - disconnected: address + - from: address + data: [] + +errors: + ALREADY_INITIALIZED: "already initialized — initDAO twice." + EMPTY_REVERIFY_OPTIONS: "empty options — setReverifyDaysOptions." + OPTIONS_NOT_ASCENDING: "options not in ascending order — setReverifyDaysOptions." + NOT_WHITELISTED_STATUS: "not whitelisted — authenticateWithTimestamp." + NOT_WHITELISTED_SENDER: "not whitelisted — renounceWhitelisted modifier." + NOT_WHITELISTED_FOR_DID: "not whitelisted — _setDID." + ALREADY_HAS_STATUS: "already has status — _addWhitelisted." + DID_ALREADY_REGISTERED: "DID already registered — _addWhitelistedWithDID or _setDID." + DID_ALREADY_REGISTERED_OLD: "DID already registered oldIdentity — _setDID conflict path." + NOT_CONTRACT: "Given address is not a contract — addContract." + INVALID_ACCOUNT_FOR_CONNECT: "invalid account — connectAccount target checks." + ALREADY_CONNECTED: "already connected — connectAccount." + UNAUTHORIZED_DISCONNECT: "unauthorized — disconnectAccount." + NOT_AUTHORIZED_SET_DID: "not authorized — setDID caller." + DID_EMPTY: "did empty — _setDID." + WHEN_NOT_PAUSED_REVERT: "OpenZeppelin Pausable whenNotPaused on gated calls." diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml new file mode 100644 index 0000000..3440a5e --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml @@ -0,0 +1,51 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address,address): 0x485cc955 + initDAO(address): 0x2b14dda8 + setReverifyDaysOptions(uint8[]): 0x925e8c1a + authenticate(address): 0x08e0d29d + authenticateWithTimestamp(address,uint256): 0x96a1ef79 + addWhitelisted(address): 0x10154bad + addWhitelistedWithDIDAndChain(address,string,uint256,uint256): 0xe737031a + addWhitelistedWithDID(address,string): 0x1b027099 + removeWhitelisted(address): 0x291d9549 + renounceWhitelisted(): 0xd6cd9473 + shouldReverify(address,uint256): 0x4a03813f + isWhitelisted(address): 0x3af32abf + lastAuthenticated(address): 0xe1e360ba + addBlacklisted(address): 0x188efc16 + removeBlacklisted(address): 0xc6a276c2 + addContract(address): 0x5f539d69 + removeContract(address): 0xc375c2ef + isDAOContract(address): 0xc73cc4ae + isBlacklisted(address): 0xfe575a87 + connectAccount(address): 0xd21685f4 + disconnectAccount(address): 0x37a1a987 + getWhitelistedRoot(address): 0x2d0e9b46 + pause(bool): 0x02329a29 + setDID(address,string): 0xd3262816 + addrToDID(address): 0x54f9f7a3 + getWhitelistedOnChainId(address): 0xa061922d + isRegistered(): 0x22366844 + IDENTITY_ADMIN_ROLE(): 0x1aaff63c + PAUSER_ROLE(): 0xe63ab1e9 + TYPED_STRUCTURE(): 0x2cec5330 + whitelistedCount(): 0xb2a1de22 + whitelistedContracts(): 0xb30f7e7f + authenticationPeriod(): 0x31b376e2 + reverifyDaysOptions(uint256): 0xcadef5ac + identities(address): 0xf653b81e + didHashToAddress(bytes32): 0x67c75937 + connectedAccounts(address): 0x61320040 + oldIdentity(): 0x4125f0f2 +events: + BlacklistAdded(address): 0x44d5fe68b00f68950fb9c1ff0a61ef7f747b1a36359a7e3a7f3324db4b878967 + BlacklistRemoved(address): 0x1747ca720b1a174a464b6513ace29b1d3190b5f632b9f34147017c81425bfde8 + WhitelistedAdded(address): 0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f + WhitelistedRemoved(address): 0x270d9b30cf5b0793bbfd54c9d5b94aeb49462b8148399000265144a8722da6b6 + WhitelistedAuthenticated(address,uint256): 0xb2a82fce6d8c7a633efe9579f77b4edb96bfdf171a49bfc2ce666dc543a1f500 + ContractAdded(address): 0x89c66952b48f3e96bf1d8ba1b63189520fd988a6979b8b740bd5c5d8dc53e205 + ContractRemoved(address): 0x8d30d41865a0b811b9545d879520d2dde9f4cc49e4241f486ad9752bc904b565 + AccountConnected(address,address): 0x18f7736ef54539debd9afd3c9500b106e12ae7c70e685f5a5efd727b1ce1d54c + AccountDisconnected(address,address): 0x7cdef5f9c5cb8ce728661ede956fef26cb91eb4d7d2180cc041b73f9fef568d2 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml new file mode 100644 index 0000000..d69e3d6 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml @@ -0,0 +1,333 @@ +# InvitesV2 — invite codes, inviter levels, and G$ bounties (UUPS upgradeable) +# Bounty amounts on Level use G$ cents (2 decimals). Invitee receives half the inviter bounty on payout. + +meta: + name: InvitesV2 + version: "2.4" + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/invite/InvitesV2.sol + inherits: + - DAOUpgradeableContract + note: > + join binds invite code and optional inviter; bounty eligibility uses minimumDays, minimumClaims, + whitelist on invitee (and inviter when present), and same-chain whitelisting via Identity + getWhitelistedOnChainId. collectBounties and bountyFor pay invitee bountyToPay/2 and inviter + bountyToPay when applicable. Campaign inviter is address(this) when inviter code maps to contract. + deployments: + mainnet: + production: + Invites: + networkId: 122 + address: "0xCa2F09c3ccFD7aD5cB9276918Bd1868f2b922ea0" + creationBlock: 8853311 + production-celo: + Invites: + networkId: 42220 + address: "0x36829D1Cda92FFF5782d5d48991620664FC857d3" + creationBlock: 18003063 + production-xdc: + Invites: + networkId: 50 + address: "0x6bd698566632bf2e81e2278f1656CB24aAF06D2e" + creationBlock: 95144756 + related: + - references/deep-researches/inviter-invitee-reward-model.md + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _ns: address + - _level0Bounty: uint256 + - _owner: address + outputs: [] + +updateAvatar: + mutability: nonpayable + access: anyone + inputs: [] + outputs: [] + +nativeToken: + mutability: view + inputs: [] + outputs: + - token: address + +dao: + mutability: view + inputs: [] + outputs: + - controller: address + +avatar: + mutability: view + inputs: [] + outputs: + - addr: address + +nameService: + mutability: view + inputs: [] + outputs: + - ns: address + +upgradeTo: + mutability: nonpayable + access: ownerOrAvatar + inputs: + - newImplementation: address + outputs: [] + +upgradeToAndCall: + mutability: payable + access: ownerOrAvatar + inputs: + - newImplementation: address + - data: bytes + outputs: [] + +proxiableUUID: + mutability: view + inputs: [] + outputs: + - slot: bytes32 + +setLevelExpirationEnabled: + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _isEnabled: bool + outputs: [] + +getIdentity: + mutability: view + inputs: [] + outputs: + - identity: address + +join: + notes: + - "Registers invite code; may set inviter; may trigger _bountyFor when canCollectBountyFor is already true." + mutability: nonpayable + access: anyone + inputs: + - _myCode: bytes32 + - _inviterCode: bytes32 + outputs: [] + emits: [InviteeJoined] + errors: [NOT_ACTIVE, INVITE_CODE_IN_USE, SELF_INVITE, USER_ALREADY_JOINED] + +canCollectBountyFor: + notes: + - "Primary eligibility gate: checks active state, whitelist, minimumDays, minimumClaims, unpaid bounty, and chain constraints." + mutability: view + inputs: + - _invitee: address + outputs: + - ok: bool + +getInvitees: + mutability: view + inputs: + - _inviter: address + outputs: + - invitees: address[] + +getPendingInvitees: + mutability: view + inputs: + - _inviter: address + outputs: + - pending: address[] + +getPendingBounties: + mutability: view + inputs: + - _inviter: address + outputs: + - count: uint256 + +bountyFor: + notes: + - "Single-invitee payout path; revalidates eligibility and transfers bounty shares to inviter/invitee on success." + mutability: nonpayable + access: anyone + inputs: + - _invitee: address + outputs: + - bounty: uint256 + emits: [InviterBounty] + errors: [NOT_ACTIVE, NOT_ELIGIBLE_BOUNTY] + +collectBounties: + notes: + - "Batch payout path over caller pending invitees; useful for claiming multiple matured bounties in one tx." + mutability: nonpayable + access: anyone + inputs: [] + outputs: [] + emits: [InviterBounty] + errors: [NOT_ACTIVE] + +setLevel: + notes: + - "Configures inviter level progression rule and bounty amount (bounty units are G$ cents)." + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _lvl: uint256 + - _toNext: uint256 + - _bounty: uint256 + - _daysToComplete: uint256 + outputs: [] + +setActive: + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _active: bool + outputs: [] + +setCampaignCode: + notes: + - "Maps a campaign code to contract-address inviter flow (address(this)) used by join." + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _code: bytes32 + outputs: [] + +setMinimums: + notes: + - "Sets minimumClaims and minimumDays thresholds used by canCollectBountyFor." + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _minClaims: uint8 + - _minDays: uint8 + outputs: [] + +end: + mutability: nonpayable + access: ownerOrAvatar + inputs: [] + outputs: [] + errors: [NOT_ACTIVE] + +setOwner: + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _owner: address + outputs: [] + +version: + mutability: pure + inputs: [] + outputs: + - v: string + +codeToUser: + mutability: view + inputs: + - code: bytes32 + outputs: + - user: address + +users: + notes: + - "Public getter omits dynamic invitees/pending arrays; use getInvitees and getPendingInvitees." + mutability: view + inputs: + - user: address + outputs: + - invitedBy: address + - inviteCode: bytes32 + - bountyPaid: bool + - level: uint256 + - levelStarted: uint256 + - totalApprovedInvites: uint256 + - totalEarned: uint256 + - joinedAt: uint256 + - bountyAtJoin: uint256 + +levels: + notes: + - "bounty is in G$ cents (2 decimals). Reserved struct slots are not exposed on the getter." + mutability: view + inputs: + - lvl: uint256 + outputs: + - toNext: uint256 + - bounty: uint256 + - daysToComplete: uint256 + +owner: + mutability: view + inputs: [] + outputs: + - addr: address + +goodDollar: + mutability: view + inputs: [] + outputs: + - token: address + +active: + mutability: view + inputs: [] + outputs: + - ok: bool + +stats: + notes: + - "Reserved Stats struct slots are not exposed on the getter." + mutability: view + inputs: [] + outputs: + - totalApprovedInvites: uint256 + - totalBountiesPaid: uint256 + - totalInvited: uint256 + +levelExpirationEnabled: + mutability: view + inputs: [] + outputs: + - ok: bool + +minimumClaims: + mutability: view + inputs: [] + outputs: + - n: uint8 + +minimumDays: + mutability: view + inputs: [] + outputs: + - n: uint8 + +events: + InviteeJoined: + indexed: + - inviter: address + - invitee: address + data: [] + InviterBounty: + indexed: + - inviter: address + - invitee: address + data: + - bountyPaid: uint256 + - inviterLevel: uint256 + - earnedLevel: bool + +errors: + NOT_ACTIVE: "not active — isActive modifier." + ONLY_OWNER_OR_AVATAR: "Only owner or avatar can perform this action — ownerOrAvatar." + INVITE_CODE_IN_USE: "invite code already in use — join." + SELF_INVITE: "self invite — join." + USER_ALREADY_JOINED: "user already joined — join." + NOT_ELIGIBLE_BOUNTY: "user not elligble for bounty yet — bountyFor." diff --git a/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml new file mode 100644 index 0000000..d717955 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml @@ -0,0 +1,41 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address,uint256,address): 0xc350a1b5 + updateAvatar(): 0x1b3c90a8 + nativeToken(): 0xe1758bd8 + dao(): 0x4162169f + avatar(): 0x5aef7de6 + nameService(): 0x3e6326fc + upgradeTo(address): 0x3659cfe6 + upgradeToAndCall(address,bytes): 0x4f1ef286 + proxiableUUID(): 0x52d1902d + setLevelExpirationEnabled(bool): 0x21132aad + getIdentity(): 0x36afc6fa + join(bytes32,bytes32): 0x5b419a65 + canCollectBountyFor(address): 0x6d619ef8 + getInvitees(address): 0xe9881a5e + getPendingInvitees(address): 0xe951a3aa + getPendingBounties(address): 0x41155d5e + bountyFor(address): 0xb6567cd5 + collectBounties(): 0xaf6346b0 + setLevel(uint256,uint256,uint256,uint256): 0xb9fb2d18 + setActive(bool): 0xacec338a + setCampaignCode(bytes32): 0xf14b8649 + setMinimums(uint8,uint8): 0x47826c82 + end(): 0xefbe1c1c + setOwner(address): 0x13af4035 + version(): 0x54fd4d50 + codeToUser(bytes32): 0xba6f5680 + users(address): 0xa87430ba + levels(uint256): 0xb2596a67 + owner(): 0x8da5cb5b + goodDollar(): 0x119e5bf3 + active(): 0x02fb0c5e + stats(): 0xd80528ae + levelExpirationEnabled(): 0xa1df6fd3 + minimumClaims(): 0xc121cb92 + minimumDays(): 0xdf0dca04 +events: + InviteeJoined(address,address): 0xd8c638d8979e2ba5dba1f0d66246ee4b1c54b838f0e0a2b601365345eb23b051 + InviterBounty(address,address,uint256,uint256,bool): 0x6081787cd1bd02ab1576c52f03e8710d792d460e7881c3155d77d23893f3768b +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml b/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml new file mode 100644 index 0000000..f6f47f4 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml @@ -0,0 +1,259 @@ +# Mento Broker (IBroker + IBrokerAdmin) — swap router and trading-limit gate for GoodDollar Mento rails +# Interface definitions are vendored in GoodProtocol; implementation lives in mento-core. + +meta: + name: Broker + version: IBroker / IBrokerAdmin + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/MentoInterfaces.sol + - https://raw.githubusercontent.com/mento-org/mento-core/main/contracts/swap/Broker.sol + - https://raw.githubusercontent.com/mento-org/mento-core/main/contracts/interfaces/IBroker.sol + implements: [IBroker, IBrokerAdmin] + note: > + swapIn is exact-input; swapOut is exact-output. Quotes route through the + configured exchangeProvider + exchangeId. Reverts use require strings in + Broker.sol plus TradingLimits library checks ("amountOutMin not met", + "amountInMax exceeded", trading-limit errors). + deployments: + mainnet: + production-celo: + MentoBroker: + networkId: 42220 + address: "0x88de45906D4F5a57315c133620cfa484cB297541" + creationBlock: 31415857 + production-xdc: + MentoBroker: + networkId: 50 + address: "0x88de45906D4F5a57315c133620cfa484cB297541" + creationBlock: 100091095 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/mentobroker + - references/guides/swap.md + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _exchangeProviders: address[] + - _reserves: address[] + outputs: [] + +setReserves: + notes: + - "Updates reserve address per already-listed provider without changing provider registration." + mutability: nonpayable + access: owner + inputs: + - _exchangeProviders: address[] + - _reserves: address[] + outputs: [] + emits: [ReserveSet] + errors: + - EXCHANGE_PROVIDER_MISSING + - RESERVE_ZERO + +addExchangeProvider: + mutability: nonpayable + access: owner + inputs: + - exchangeProvider: address + - reserve: address + outputs: + - index: uint256 + emits: [ExchangeProviderAdded, ReserveSet] + errors: + - PROVIDER_ALREADY_LISTED + - PROVIDER_ZERO + - RESERVE_ZERO + +removeExchangeProvider: + mutability: nonpayable + access: owner + inputs: + - exchangeProvider: address + - index: uint256 + outputs: [] + emits: [ExchangeProviderRemoved] + errors: + - INDEX_MISMATCH + +getAmountIn: + notes: + - "Exact-output quote path: returns required tokenIn for target amountOut." + - "Pre-checks collateral reserve balance when tokenOut is collateral." + mutability: view + inputs: + - exchangeProvider: address + - exchangeId: bytes32 + - tokenIn: address + - tokenOut: address + - amountOut: uint256 + outputs: + - amountIn: uint256 + errors: + - EXCHANGE_PROVIDER_MISSING + - INSUFFICIENT_RESERVE_BALANCE + +getAmountOut: + notes: + - "Exact-input quote path: returns expected tokenOut for fixed amountIn." + - "Pre-checks collateral reserve balance when tokenOut is collateral." + mutability: view + inputs: + - exchangeProvider: address + - exchangeId: bytes32 + - tokenIn: address + - tokenOut: address + - amountIn: uint256 + outputs: + - amountOut: uint256 + errors: + - EXCHANGE_PROVIDER_MISSING + - INSUFFICIENT_RESERVE_BALANCE + +swapIn: + notes: + - "Transfers tokenIn from msg.sender then delivers tokenOut; enforces amountOut >= amountOutMin after exchange + limits." + mutability: nonpayable + access: nonReentrant + inputs: + - exchangeProvider: address + - exchangeId: bytes32 + - tokenIn: address + - tokenOut: address + - amountIn: uint256 + - amountOutMin: uint256 + outputs: + - amountOut: uint256 + emits: [Swap] + errors: + - EXCHANGE_PROVIDER_MISSING + - INSUFFICIENT_RESERVE_BALANCE + - AMOUNT_OUT_MIN_NOT_MET + - TRADING_LIMIT_OR_SAFE_TRANSFER + +swapOut: + notes: + - "Computes required tokenIn then pulls up to amountInMax; reverts if amountIn would exceed cap." + mutability: nonpayable + access: nonReentrant + inputs: + - exchangeProvider: address + - exchangeId: bytes32 + - tokenIn: address + - tokenOut: address + - amountOut: uint256 + - amountInMax: uint256 + outputs: + - amountIn: uint256 + emits: [Swap] + errors: + - EXCHANGE_PROVIDER_MISSING + - INSUFFICIENT_RESERVE_BALANCE + - AMOUNT_IN_MAX_EXCEEDED + - TRADING_LIMIT_OR_SAFE_TRANSFER + +burnStableTokens: + notes: + - "Owner-maintained escape hatch burning stables from msg.sender via reserve plumbing." + mutability: nonpayable + access: owner + inputs: + - token: address + - amount: uint256 + outputs: + - ok: bool + errors: + - COLLATERAL_TRANSFER_FAILED + +configureTradingLimit: + notes: + - "Sets per (exchangeId, token) limit config used by swapIn/swapOut guardTradingLimits checks." + mutability: nonpayable + access: owner + inputs: + - exchangeId: bytes32 + - token: address + - config: TradingLimits.Config + outputs: [] + emits: [TradingLimitConfigured] + +getExchangeProviders: + notes: + - "Discovery helper for clients that need the active provider set before quoting/swapping." + mutability: view + inputs: [] + outputs: + - providers: address[] + +exchangeProviders: + mutability: view + inputs: + - index: uint256 + outputs: + - exchangeProvider: address + +isExchangeProvider: + mutability: view + inputs: + - exchangeProvider: address + outputs: + - ok: bool + +tradingLimitsState: + mutability: view + inputs: + - eid: bytes32 + outputs: + - state: TradingLimits.State + +tradingLimitsConfig: + mutability: view + inputs: + - eid: bytes32 + outputs: + - config: TradingLimits.Config + +events: + Swap: + indexed: + - exchangeId: bytes32 + - trader: address + - tokenIn: address + data: + - exchangeProvider: address + - tokenOut: address + - amountIn: uint256 + - amountOut: uint256 + TradingLimitConfigured: + indexed: [] + data: + - exchangeId: bytes32 + - token: address + - config: TradingLimits.Config + ExchangeProviderAdded: + indexed: + - exchangeProvider: address + data: [] + ExchangeProviderRemoved: + indexed: + - exchangeProvider: address + data: [] + ReserveSet: + indexed: + - newAddress: address + - prevAddress: address + data: [] + +errors: + EXCHANGE_PROVIDER_MISSING: "ExchangeProvider does not exist" + EXCHANGE_PROVIDER_EXISTS: "ExchangeProvider already exists in the list" + PROVIDER_ZERO: "ExchangeProvider address can't be 0" + RESERVE_ZERO: "Reserve address can't be 0" + INDEX_MISMATCH: "index doesn't match provider" + INSUFFICIENT_RESERVE_BALANCE: "Insufficient balance in reserve" + AMOUNT_OUT_MIN_NOT_MET: "amountOutMin not met" + AMOUNT_IN_MAX_EXCEEDED: "amountInMax exceeded" + COLLATERAL_TRANSFER_FAILED: "Transfer of the collateral asset failed" + AMOUNT_TOO_LARGE: "amountIn too large / amountOut too large — uint256 to int256 safety checks" + TRADING_LIMIT_OR_SAFE_TRANSFER: "TradingLimits library reverts or ERC20 safety failures during swap" diff --git a/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml b/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml new file mode 100644 index 0000000..249edce --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml @@ -0,0 +1,24 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address[],address[]): 0x73cf25f8 + setReserves(address[],address[]): 0xddeb9dd2 + addExchangeProvider(address,address): 0x770d0a34 + removeExchangeProvider(address,uint256): 0x04710d53 + getAmountIn(address,bytes32,address,address,uint256): 0x04e45640 + getAmountOut(address,bytes32,address,address,uint256): 0xa20f2305 + swapIn(address,bytes32,address,address,uint256,uint256): 0xddbbe850 + swapOut(address,bytes32,address,address,uint256,uint256): 0xd163b135 + burnStableTokens(address,uint256): 0x131cab2a + configureTradingLimit(bytes32,address,(uint32,uint32,int48,int48,int48,int48,int48,uint8)): 0xa868d140 + getExchangeProviders(): 0x2cac2568 + exchangeProviders(uint256): 0xc4454fdc + isExchangeProvider(address): 0xd1d786b1 + tradingLimitsState(bytes32): 0xf01ecd17 + tradingLimitsConfig(bytes32): 0x821a816c +events: + Swap(bytes32,address,address,address,address,uint256,uint256): 0x46e6aeaebfb8f1b6a9be6403b5fc420d9827172046f365e786acb9d5b56c9409 + TradingLimitConfigured(bytes32,address,(uint32,uint32,int48,int48,int48,int48,int48,uint8)): 0x3ed50aa123e1e547aa90fcf88624ef8278d253e252bb1feac75eda93e1905bb6 + ExchangeProviderAdded(address): 0x2ee2cb0721ec60b86190cae5c48e25064b69b35abad32452a4ec99d232033de2 + ExchangeProviderRemoved(address): 0x29e92ab2e30f4f74283034c28c451b6faac986b554f1808101eb6418bdba19d4 + ReserveSet(address,address): 0xb69e1c416d8be92ac92c8e97e77c4626fba5e6ab50161099f659ea3303479e50 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml new file mode 100644 index 0000000..d4bb1f2 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml @@ -0,0 +1,542 @@ +meta: + name: MessagePassingBridge + version: Axelar + LayerZero message passing + source: + - https://raw.githubusercontent.com/GoodDollar/GoodBridge/master/packages/bridge-contracts/contracts/messagePassingBridge/MessagePassingBridge.sol + - https://raw.githubusercontent.com/GoodDollar/GoodBridge/master/packages/bridge-contracts/contracts/messagePassingBridge/IMessagePassingBridge.sol + note: > + Bridge implementation that burns source tokens and mints destination tokens, + transporting requests through Axelar or LayerZero. + Cross-chain transport uses msg.value on the source (LayerZero estimateSendFee path, Axelar gas prepay); destination mint applies bridgeFees bps to the delivered G$ amount. See references/guides/bridge.md "Bridge fee context". + deployments: + mainnet: + production: + MpbBridge: + networkId: 122 + address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" + creationBlock: 25464921 + production-celo: + MpbBridge: + networkId: 42220 + address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" + creationBlock: 21473545 + production-xdc: + MpbBridge: + networkId: 50 + address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" + creationBlock: 95254417 + related: + - references/guides/bridge.md + +initialize: + notes: + - "Initial bootstrap for DAO wiring and bridge policy state." + - "Sets guardian to msg.sender and uses NameService UBISCHEME as feeRecipient fallback to avatar when missing." + - "Preloads default LZ mappings for Ethereum, Celo, Fuse, and XDC using this contract as trusted remote." + mutability: nonpayable + access: initializer + inputs: + - nameService: address + - limits: (uint256,uint256,uint256,uint256,bool) + - fees: (uint256,uint256,uint256) + outputs: [] + +upgrade: + notes: + - "Reinitializer for migration path; re-applies default LZ mappings through internal add function." + - "Used when upgrading existing proxy deployments to mapping-aware version." + mutability: nonpayable + access: reinitializer + inputs: [] + outputs: [] + +addLzChainSupport: + notes: + - "Administrative chain wiring method for LayerZero transport." + - "Writes both forward (chainId->lzChainId) and reverse (lzChainId->chainId) mappings." + - "Also updates trustedRemoteLookup to enforce source-contract authenticity on receive." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - lzChainId: uint16 + - chainId: uint256 + - remote: address + outputs: [] + +approveRequest: + notes: + - "Manual override for stuck/exceptional inbound requests." + - "When approved, destination _bridgeFrom skips limits enforcement for that request id." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - id: uint256 + outputs: [] + +preventRequest: + notes: + - "Emergency/manual block for a specific request id." + - "Prevents execution by pre-marking executedRequests[id]=true." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - id: uint256 + outputs: [] + +setFeeRecipient: + notes: + - "Sets destination fee mint recipient." + - "If feeRecipient is zero, fee minting is skipped in _bridgeFrom." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - recipient: address + outputs: [] + +setBridgeLimits: + notes: + - "Updates operational throttles: min amount, tx cap, account/day cap, global/day cap, whitelist mode." + - "These limits are checked by canBridge and enforced in _enforceLimits." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - limits: (uint256,uint256,uint256,uint256,bool) + outputs: [] + +setBridgeFees: + notes: + - "Updates bridge fee parameters in basis points with min/max clamps." + - "Reverts when fee exceeds 10000 bps." + - "Effective fee deduction is computed in _takeFee during destination execution." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - fees: (uint256,uint256,uint256) + outputs: [] + +setDisabledBridges: + notes: + - "Toggles allowlist state for source bridge combinations." + - "Key format is keccak256(abi.encode(sourceChainId, BridgeService))." + - "Checked in _bridgeFrom before request execution." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - bridgeKeys: bytes32[] + - disabled: bool[] + outputs: [] + +setFaucet: + notes: + - "Configures optional faucet top-up callback used after successful destination processing." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - _faucet: address + outputs: [] + +setGuardian: + notes: + - "Rotates guardian authority used by owner/avatar/guardian admin gate." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - _guardian: address + outputs: [] + +withdraw: + notes: + - "Avatar-only rescue function for arbitrary token balances held by bridge." + - "Amount 0 means full balance withdrawal." + mutability: nonpayable + access: avatar + inputs: + - token: address + - amount: uint256 + outputs: [] + +pauseBridge: + notes: + - "Sets isClosed state for outbound flow." + - "When closed, _bridgeTo reverts with BRIDGE_LIMITS('closed')." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - isPaused: bool + outputs: [] + +canBridge: + notes: + - "Purely diagnostic read path for preflight checks." + - "Returns (ok, reason) using BridgeHelperLibrary policy evaluation." + - "Includes close flag, whitelist gate, and per-account/global daily limit windows." + mutability: view + inputs: + - from: address + - amount: uint256 + outputs: + - isWithinLimit: bool + - error: string + +bridgeTo: + notes: + - "Generic outbound bridge entrypoint selecting BridgeService.AXELAR or BridgeService.LZ." + - "Burns source-side token amount, normalizes to 18 decimals, and emits BridgeRequest with request id." + - "Requires msg.value for transport fee payment." + mutability: payable + access: anyone + inputs: + - target: address + - targetChainId: uint256 + - amount: uint256 + - bridge: uint8 + outputs: [] + +bridgeToWithLz: + notes: + - "Outbound convenience wrapper fixed to LayerZero transport." + - "If adapterParams is empty, contract injects default type-1 options with 400k gas." + - "Compares required native fee from estimateSendFee against msg.value and reverts on underpayment." + mutability: payable + access: anyone + inputs: + - target: address + - targetChainId: uint256 + - amount: uint256 + - adapterParams: bytes + outputs: [] + +bridgeToWithAxelar: + notes: + - "Outbound convenience wrapper fixed to Axelar transport." + - "Uses gasRefundAddress when provided, otherwise msg.sender as refund recipient." + mutability: payable + access: anyone + inputs: + - target: address + - targetChainId: uint256 + - amount: uint256 + - gasRefundAddress: address + outputs: [] + +toLzChainId: + notes: + - "Lookup helper from local chain id to LayerZero chain id." + mutability: view + inputs: + - chainId: uint256 + outputs: + - lzChainId: uint16 + +fromLzChainId: + notes: + - "Lookup helper from LayerZero chain id to local chain id." + mutability: view + inputs: + - lzChainId: uint16 + outputs: + - chainId: uint256 + +toAxelarChainId: + notes: + - "Pure mapping in current implementation: chainId 1 -> 'Ethereum', 5 -> 'ethereum-2', 42220 and 44787 -> 'celo'; any other id returns empty string and outbound Axelar reverts UNSUPPORTED_CHAIN." + mutability: pure + inputs: + - chainId: uint256 + outputs: + - axlChainId: string + +fromAxelarChainId: + notes: + - "Maps Axelar chain-name strings back into local numeric chain ids." + mutability: pure + inputs: + - axlChainId: string + outputs: + - chainId: uint256 + +estimateSendFee: + notes: + - "Transport fee preflight for LZ path." + - "Builds payload with request id set to 0 for estimation." + - "Returns (0,0) on endpoint estimation failure in handler implementation." + - "_normalizedAmount must match BridgeHelperLibrary.normalizeFromTokenTo18Decimals burned raw amount using native token decimals on the outbound chain." + mutability: view + inputs: + - _dstChainId: uint16 + - _fromAddress: address + - _toAddress: address + - _normalizedAmount: uint256 + - _useZro: bool + - _adapterParams: bytes + outputs: + - nativeFee: uint256 + - zroFee: uint256 + +lzReceive: + notes: + - "LayerZero receive entrypoint." + - "Validates caller is configured endpoint before dispatching nonblocking receive pipeline." + mutability: nonpayable + inputs: + - _srcChainId: uint16 + - _srcAddress: bytes + - _nonce: uint64 + - _payload: bytes + outputs: [] + +lzEndpoint_: + notes: + - "Immutable LayerZero endpoint configured in constructor." + mutability: view + inputs: [] + outputs: + - endpoint: address + +HOME_CHAIN_ID: + notes: + - "Immutable deployment home chain marker." + mutability: view + inputs: [] + outputs: + - chainId: uint32 + +guardian: + notes: + - "Current guardian account authorized for admin operations." + mutability: view + inputs: [] + outputs: + - account: address + +executedRequests: + notes: + - "Replay protection state for inbound request ids." + mutability: view + inputs: + - id: uint256 + outputs: + - done: bool + +disabledSourceBridges: + notes: + - "Per-source bridge toggle state keyed by (sourceChainId, bridgeService) hash." + mutability: view + inputs: + - key: bytes32 + outputs: + - disabled: bool + +isClosed: + notes: + - "Outbound bridge pause state." + mutability: view + inputs: [] + outputs: + - closed: bool + +bridgeFees: + notes: + - "Current fee policy parameters: minFee, maxFee, feeBps." + mutability: view + inputs: [] + outputs: + - minFee: uint256 + - maxFee: uint256 + - fee: uint256 + +bridgeLimits: + notes: + - "Current bridge traffic limits and whitelist policy." + mutability: view + inputs: [] + outputs: + - dailyLimit: uint256 + - txLimit: uint256 + - accountDailyLimit: uint256 + - minAmount: uint256 + - onlyWhitelisted: bool + +bridgeDailyLimit: + notes: + - "Aggregate 24h bridge usage tracker." + mutability: view + inputs: [] + outputs: + - lastTransferReset: uint256 + - bridged24Hours: uint256 + +accountsDailyLimit: + notes: + - "Per-account 24h usage tracker." + mutability: view + inputs: + - account: address + outputs: + - lastTransferReset: uint256 + - bridged24Hours: uint256 + +faucet: + notes: + - "Optional gas top-up contract used on destination completion." + mutability: view + inputs: [] + outputs: + - addr: address + +currentId: + notes: + - "Monotonic nonce component used to derive outbound request ids." + mutability: view + inputs: [] + outputs: + - id: uint256 + +lzChainIdsMapping: + notes: + - "Local chain id -> LayerZero chain id mapping." + mutability: view + inputs: + - chainId: uint256 + outputs: + - lzChainId: uint16 + +feeRecipient: + notes: + - "Address receiving bridge fee mints on destination." + mutability: view + inputs: [] + outputs: + - recipient: address + +approvedRequests: + notes: + - "Manual approvals allowing request execution above standard limits." + mutability: view + inputs: + - id: uint256 + outputs: + - approved: bool + +lzChainToIdsMapping: + notes: + - "LayerZero chain id -> local chain id mapping." + mutability: view + inputs: + - lzChainId: uint16 + outputs: + - chainId: uint256 + +trustedRemoteLookup: + notes: + - "Trusted remote path used to authenticate inbound LZ/Axelar source contract." + mutability: view + inputs: + - lzChainId: uint16 + outputs: + - path: bytes + +gateway: + notes: + - "Axelar gateway address inherited from AxelarExecutable." + mutability: view + inputs: [] + outputs: + - addr: address + +gasService: + notes: + - "Axelar gas service used to prepay cross-chain execution." + mutability: view + inputs: [] + outputs: + - addr: address + +nameService: + notes: + - "DAO NameService pointer from DAOUpgradeableContract." + mutability: view + inputs: [] + outputs: + - addr: address + +avatar: + notes: + - "DAO avatar authority address." + mutability: view + inputs: [] + outputs: + - addr: address + +owner: + notes: + - "Ownable owner from upgradeable ownership flow." + mutability: view + inputs: [] + outputs: + - account: address + +events: + BridgeRequest: + indexed: + - from: address + - to: address + - id: uint256 + data: + - targetChainId: uint256 + - normalizedAmount: uint256 + - timestamp: uint256 + - bridge: uint8 + note: "Outbound intent event; relayers consume this to deliver payload on destination." + ExecutedTransfer: + indexed: + - from: address + - to: address + - id: uint256 + data: + - normalizedAmount: uint256 + - fee: uint256 + - sourceChainId: uint256 + - bridge: uint8 + note: "Destination completion event after replay checks, source-auth checks, limit checks, and mint/fee mint." + FalseSender: + indexed: [] + data: + - sourceChainId: uint256 + - sourceAddress: address + note: "Inbound message ignored because source contract does not match trusted remote path." + +errors: + AlreadyInitialized: "LZ chain support already initialized for this lz chain id." + NOT_GUARDIAN: + inputs: + - sender: address + WRONG_TOKEN: + inputs: + - token: address + INVALID_TARGET_OR_CHAINID: + inputs: + - target: address + - chainId: uint256 + BRIDGE_LIMITS: + inputs: + - reason: string + TRANSFER_FROM: "Source burn/transferFrom style failure." + TRANSFER: "Destination transfer style failure." + ALREADY_EXECUTED: + inputs: + - requestId: uint256 + MISSING_FEE: "Bridge call requires msg.value for transport fee." + UNSUPPORTED_CHAIN: + inputs: + - chainId: uint256 + LZ_FEE: + inputs: + - required: uint256 + - sent: uint256 + INVALID_SENDER: + inputs: + - _srcAddress: bytes + INVALID_ENDPOINT: + inputs: + - lzEndpoint: address diff --git a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml new file mode 100644 index 0000000..104b943 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml @@ -0,0 +1,52 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address,(uint256,uint256,uint256,uint256,bool),(uint256,uint256,uint256)): 0xfad16abe + upgrade(): 0xd55ec697 + addLzChainSupport(uint16,uint256,address): 0xc01c8123 + approveRequest(uint256): 0xd7d1bbdb + preventRequest(uint256): 0xc612f9ea + setFeeRecipient(address): 0xe74b981b + setBridgeLimits((uint256,uint256,uint256,uint256,bool)): 0x29b70872 + setBridgeFees((uint256,uint256,uint256)): 0x82dc737d + setDisabledBridges(bytes32[],bool[]): 0xc78ab882 + setFaucet(address): 0xd8b31c77 + setGuardian(address): 0x8a0dac4a + withdraw(address,uint256): 0xf3fef3a3 + pauseBridge(bool): 0x1a394795 + canBridge(address,uint256): 0x3095634a + bridgeTo(address,uint256,uint256,uint8): 0x1fec5c5c + bridgeToWithLz(address,uint256,uint256,bytes): 0xc56bbdd9 + bridgeToWithAxelar(address,uint256,uint256,address): 0x40a00aaf + toLzChainId(uint256): 0x5b23a990 + fromLzChainId(uint16): 0x16ad5512 + toAxelarChainId(uint256): 0x9a9ee081 + fromAxelarChainId(string): 0x56026f37 + estimateSendFee(uint16,address,address,uint256,bool,bytes): 0x05fead15 + lzReceive(uint16,bytes,uint64,bytes): 0x001d3567 + lzEndpoint_(): 0x020051cf + HOME_CHAIN_ID(): 0x8f65be85 + guardian(): 0x452a9320 + executedRequests(uint256): 0x425cfb53 + disabledSourceBridges(bytes32): 0x69c20d36 + isClosed(): 0xc2b6b58c + bridgeFees(): 0x7b0240c0 + bridgeLimits(): 0xc6dd812f + bridgeDailyLimit(): 0xb2f7667f + accountsDailyLimit(address): 0xd4227947 + faucet(): 0xde5f72fd + currentId(): 0xe00dd161 + lzChainIdsMapping(uint256): 0xb5569f18 + feeRecipient(): 0x46904840 + approvedRequests(uint256): 0x0dce292e + lzChainToIdsMapping(uint16): 0x73800fc4 + trustedRemoteLookup(uint16): 0x7533d788 + gateway(): 0x116191b6 + gasService(): 0x6a22d8cc + nameService(): 0x3e6326fc + avatar(): 0x5aef7de6 + owner(): 0x8da5cb5b +events: + BridgeRequest(address,address,uint256,uint256,uint256,uint256,uint8): 0x34b675c8c84b6a9e7979a0d3a54f2b036a19d6444167091a925af2d81e8e66fe + ExecutedTransfer(address,address,uint256,uint256,uint256,uint256,uint8): 0x81e772e0c4366ddbae472926005267ef278dbb257be45dfe97c676ceae348dbe + FalseSender(uint256,address): 0x1eafb58197ea0dc76b9278ccad47f61a239f00b017d89a19081a89321cce213d +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml b/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml new file mode 100644 index 0000000..01692b7 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml @@ -0,0 +1,104 @@ +# NameService — string-key to address registry for GoodProtocol deployments +# Resolves keys such as IDENTITY, UBISCHEME, GOODDOLLAR, GDAO_STAKING. + +meta: + name: NameService + version: UUPS-upgradeable v1 + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/NameService.sol + implements: [] + inherits: [Initializable, UUPSUpgradeable] + note: > + Almost every integration starts here. Writes are restricted to the DAO + avatar. Reads are permissionless. Use getAddress(string) with the same + key strings your deployment documented in releases/deployment.json. + deployments: + mainnet: + production: + NameService: + networkId: 122 + address: "0xec6dcE387B1616a0c44fF2E4fA9E90e53Cf14eb0" + creationBlock: 15740314 + production-celo: + NameService: + networkId: 42220 + address: "0x0F5dB7a64A6a64052693676CA898EC7F7A94FF4e" + creationBlock: 17237962 + production-xdc: + NameService: + networkId: 50 + address: "0x1e5154Bf5e31FF56051bbd45958b879Fb7a290FE" + creationBlock: 95143608 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/nameservice + - https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json + +addresses: + # Solidity auto-getter for public mapping addresses(bytes32 => address). + notes: + - "Prefer getAddress(string) off-chain; this expects keccak256(abi.encodePacked(name)) for manual lookups." + mutability: view + inputs: + - nameHash: bytes32 + outputs: + - addr: address + +dao: + mutability: view + inputs: [] + outputs: + - controller: address + +initialize: + notes: + - "Initializer wires Controller and optional pre-hashed name batch." + - "Automatically registers CONTROLLER and AVATAR hashes to dao and dao.avatar()." + mutability: nonpayable + access: initializer + inputs: + - _dao: address + - _nameHashes: bytes32[] + - _addresses: address[] + outputs: [] + +setAddress: + notes: + - "Stores addresses[keccak256(bytes(name))] = addr and emits AddressChanged." + mutability: nonpayable + access: avatar + inputs: + - name: string + - addr: address + outputs: [] + emits: [AddressChanged] + errors: [ONLY_AVATAR] + +setAddresses: + notes: + - "Batch write by raw bytes32 keys; no per-name event emission in this loop." + mutability: nonpayable + access: avatar + inputs: + - hash: bytes32[] + - addrs: address[] + outputs: [] + errors: [ONLY_AVATAR] + +getAddress: + notes: + - "Primary lookup used by claim, staking, identity, and swap integrations." + mutability: view + inputs: + - name: string + outputs: + - addr: address + +events: + AddressChanged: + indexed: [] + data: + - name: string + - addr: address + +errors: + ONLY_AVATAR: "only avatar can call this method — revert string on any avatar-gated path including UUPS _authorizeUpgrade." diff --git a/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml b/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml new file mode 100644 index 0000000..48e1bcf --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml @@ -0,0 +1,11 @@ +# Generated by scripts/selectors.mjs +functions: + addresses(bytes32): 0x699f200f + dao(): 0x4162169f + initialize(address,bytes32[],address[]): 0xd41b4e72 + setAddress(string,address): 0x9b2ea4bd + setAddresses(bytes32[],address[]): 0x4ab01f5b + getAddress(string): 0xbf40fac1 +events: + AddressChanged(string,address): 0x135cf55549d8538a41f19f46cc85625da93e68b63484cca8fcb9aaf19e520137 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml b/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml new file mode 100644 index 0000000..abfc91c --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml @@ -0,0 +1,1033 @@ +# SuperToken — the Superfluid Protocol's token standard +# Merges three variants into a single ABI surface: +# • Wrapper Super Token — wraps an existing ERC-20 (upgrade / downgrade) +# • Native Asset Super Token (SETH) — wraps chain-native asset (upgradeByETH / downgradeToETH) +# • Pure Super Token — pre-minted, no underlying token (upgrade/downgrade revert) +# +# Inherits ERC-20, ERC-777, ERC-2612 (permit), EIP-5267, and the Superfluid real-time +# balance and agreement hosting layer from SuperfluidToken. +# +# Proxy / upgradability functions are omitted: +# castrate, getCodeAddress, proxiableUUID, initialize, initializeWithAdmin, updateCode +# Proxy-related events are also omitted: CodeUpdated, Initialized. + +meta: + name: SuperToken + version: v1 + source: + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/IYieldBackend.sol + inherits: [SuperfluidToken, ISuperToken, UUPSProxiable] + yield_backends: + - name: AaveYieldBackend + note: "Production backend for ERC-20 wrapper tokens using Aave V3" + source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/AaveYieldBackend.sol + - name: AaveETHYieldBackend + note: "Variant for native-asset (ETH/WETH) wrapper tokens using Aave V3" + source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/AaveETHYieldBackend.sol + - name: ERC4626YieldBackend + note: "Generic backend for any ERC-4626 vault" + source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/ERC4626YieldBackend.sol + - name: SparkYieldBackend + note: "Extends ERC4626YieldBackend for Spark Protocol vaults with referral tracking" + source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SparkYieldBackend.sol + variants: + wrapper: "Standard ERC-20 wrapper — upgrade() / downgrade()" + native-asset: "Native asset wrapper (SETH) — upgradeByETH() / downgradeToETH()" + pure: "Pre-minted supply, no underlying — upgrade/downgrade revert" + note: > + Super Tokens are deployed as individual proxies (one per token) by the + SuperTokenFactory. There is no single canonical deployment address. + All proxies share the same logic contract which is upgraded via governance. + +# wrapper super token — a Super Token backed by an ERC-20 underlying token +# native asset super token (SETH) — a Super Token backed by the chain-native asset (ETH, MATIC, etc.) +# pure super token — a Super Token with no underlying; supply is pre-minted at deploy time +# underlying token — the ERC-20 token that a wrapper super token wraps +# real-time balance — balance that changes continuously based on active agreements (CFA/GDA) +# available balance — real-time balance minus locked deposits +# critical — an account whose available balance is negative +# solvent — an account whose real-time balance (before deposit deductions) is non-negative +# operator (ERC-777) — an address authorized to send/burn tokens on behalf of a holder +# buffer / deposit — collateral locked while an outgoing flow is active + +# == ERC-20 Token Info == + +name: + mutability: view + inputs: [] + outputs: + - string + +symbol: + mutability: view + inputs: [] + outputs: + - string + +decimals: + # Always returns 18 regardless of underlying token decimals. + notes: + - "Always returns 18, regardless of the underlying token's decimals (e.g. USDC has 6). The upgrade/downgrade functions handle decimal conversion internally." + mutability: pure + inputs: [] + outputs: + - uint8 + +totalSupply: + mutability: view + inputs: [] + outputs: + - uint256 + +# == ERC-20 Balance & Allowance == + +balanceOf: + # Returns max(0, availableBalance) — clamps negative balances to zero. + # For the full real-time picture, use realtimeBalanceOfNow. + notes: + - "Returns max(0, availableBalance) — negative/critical balances are clamped to zero. Use realtimeBalanceOfNow to detect critical accounts." + mutability: view + inputs: + - account: address + outputs: + - balance: uint256 + +allowance: + mutability: view + inputs: + - account: address + - spender: address + outputs: + - uint256 + +approve: + mutability: nonpayable + access: anyone + inputs: + - spender: address + - amount: uint256 + outputs: + - bool + emits: [Approval] + errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +increaseAllowance: + mutability: nonpayable + access: anyone + inputs: + - spender: address + - addedValue: uint256 + outputs: + - bool + emits: [Approval] + errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +decreaseAllowance: + mutability: nonpayable + access: anyone + inputs: + - spender: address + - subtractedValue: uint256 + outputs: + - bool + emits: [Approval] + errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +# == ERC-20 Transfers == + +transfer: + # ERC-20 transfer. Does NOT invoke ERC-777 send/receive hooks (by design). + mutability: nonpayable + access: anyone + inputs: + - recipient: address + - amount: uint256 + outputs: + - bool + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +transferFrom: + # ERC-20 transferFrom. Does NOT invoke ERC-777 send/receive hooks. + # Does NOT emit an Approval event on allowance spend (OZ v5 behaviour). + mutability: nonpayable + access: anyone + inputs: + - holder: address + - recipient: address + - amount: uint256 + outputs: + - bool + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +transferAll: + # Transfer the caller's entire balanceOf to recipient. + mutability: nonpayable + access: anyone # transfers msg.sender's entire balance + inputs: + - recipient: address + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +# == ERC-777 == + +granularity: + # Always returns 1. + mutability: pure + inputs: [] + outputs: + - uint256 + +send: + # ERC-777 send — invokes tokensToSend / tokensReceived hooks. + mutability: nonpayable + access: anyone + inputs: + - recipient: address + - amount: uint256 + - userData: bytes + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] + +burn: + # ERC-777 burn — in SuperToken this actually performs a downgrade (unwrap). + notes: + - "Gotcha: Reverts with SUPER_TOKEN_NO_UNDERLYING_TOKEN on Pure Super Tokens." + mutability: nonpayable + access: anyone + inputs: + - amount: uint256 + - userData: bytes + emits: [Burned, Transfer, TokenDowngraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED] + +authorizeOperator: + mutability: nonpayable + access: anyone + inputs: + - operator: address + emits: [AuthorizedOperator] + +revokeOperator: + mutability: nonpayable + access: anyone + inputs: + - operator: address + emits: [RevokedOperator] + +isOperatorFor: + mutability: view + inputs: + - operator: address + - tokenHolder: address + outputs: + - bool + +defaultOperators: + mutability: view + inputs: [] + outputs: + - "address[]" + +operatorSend: + # ERC-777 operatorSend — invokes tokensToSend / tokensReceived hooks. + mutability: nonpayable + access: operator + inputs: + - sender: address + - recipient: address + - amount: uint256 + - userData: bytes + - operatorData: bytes + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] + +operatorBurn: + # ERC-777 operatorBurn — like burn(), actually performs a downgrade. + mutability: nonpayable + access: operator + inputs: + - account: address + - amount: uint256 + - userData: bytes + - operatorData: bytes + emits: [Burned, Transfer, TokenDowngraded] + errors: [SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED] + +# == ERC-20 Permit == + +permit: + mutability: nonpayable + access: anyone + inputs: + - owner: address + - spender: address + - value: uint256 + - deadline: uint256 + - v: uint8 + - r: bytes32 + - s: bytes32 + emits: [Approval] + errors: [SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE, SUPER_TOKEN_PERMIT_INVALID_SIGNER, ECDSAInvalidSignature, ECDSAInvalidSignatureLength, ECDSAInvalidSignatureS, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +nonces: + mutability: view + inputs: + - owner: address + outputs: + - uint256 + +DOMAIN_SEPARATOR: + mutability: view + inputs: [] + outputs: + - bytes32 + +eip712Domain: + # EIP-5267 domain retrieval. + mutability: view + inputs: [] + outputs: + - fields: bytes1 + - name: string + - version: string + - chainId: uint256 + - verifyingContract: address + - salt: bytes32 + - "extensions: uint256[]" + +# == ERC-20 Wrapping == +# These functions interact with the underlying ERC-20 token. +# They revert with SUPER_TOKEN_NO_UNDERLYING_TOKEN on Pure and Native Asset +# Super Tokens (use upgradeByETH / downgradeToETH for SETH). + +upgrade: + # Wrap underlying ERC-20 into Super Tokens for msg.sender. + # Requires prior ERC-20 approval on the underlying token. + notes: + - "Gotcha: amount is always in SuperToken decimals (18), regardless of the underlying token's decimals. The contract handles downscaling internally when pulling from the underlying via transferFrom. However, the ERC-20 approval on the underlying token must use the underlying's native decimals. Example: to wrap 1000 USDC, approve 1000e6 on USDC, then call upgrade(1000e18)." + mutability: nonpayable + access: anyone # wraps for msg.sender + inputs: + - amount: uint256 # in SuperToken decimals (always 18) + emits: [Minted, Transfer, TokenUpgraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] + +upgradeTo: + # Wrap underlying ERC-20 and mint Super Tokens to a different address. + mutability: nonpayable + access: anyone + inputs: + - to: address + - amount: uint256 + - userData: bytes + emits: [Minted, Transfer, TokenUpgraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] + +downgrade: + # Unwrap Super Tokens back to underlying ERC-20 for msg.sender. + mutability: nonpayable + access: anyone # unwraps to msg.sender + inputs: + - amount: uint256 + emits: [Burned, Transfer, TokenDowngraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] + +downgradeTo: + # Unwrap Super Tokens and send underlying ERC-20 to a different address. + mutability: nonpayable + access: anyone + inputs: + - to: address + - amount: uint256 + emits: [Burned, Transfer, TokenDowngraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] + +getUnderlyingToken: + # Returns address(0) for Pure and Native Asset Super Tokens. + mutability: view + inputs: [] + outputs: + - address + +getUnderlyingDecimals: + mutability: view + inputs: [] + outputs: + - uint8 + +toUnderlyingAmount: + # Convert a Super Token amount to its underlying equivalent, accounting for + # decimal differences. adjustedAmount strips precision loss. + mutability: view + inputs: + - amount: uint256 + outputs: + - underlyingAmount: uint256 + - adjustedAmount: uint256 + +# == Native Asset Wrapping (SETH only) == +# These functions exist only on Native Asset Super Token (SETH) proxies. +# They are defined in the SETHProxy contract, not in the SuperToken logic. +# The SETH proxy also accepts plain ETH transfers via receive() which mints +# Super Tokens to the sender. + +upgradeByETH: + # Wrap sent ETH/native asset into Super Tokens for msg.sender. + # Variant: native-asset + mutability: payable + access: anyone # wraps for msg.sender + inputs: [] + emits: [Minted, Transfer, TokenUpgraded] + +upgradeByETHTo: + # Wrap sent ETH/native asset into Super Tokens for a different address. + # Variant: native-asset + mutability: payable + access: anyone + inputs: + - to: address + emits: [Minted, Transfer, TokenUpgraded] + +downgradeToETH: + # Unwrap Super Tokens back to ETH/native asset for msg.sender. + # Variant: native-asset + mutability: nonpayable + access: anyone # unwraps to msg.sender + inputs: + - wad: uint256 + emits: [Burned, Transfer, TokenDowngraded] + +# == Real-time Balance == + +realtimeBalanceOf: + # Full real-time balance across ALL agreements at a given timestamp. + # This aggregates the realtimeBalanceOf from every agreement (CFA, GDA, IDA) + # into a single balance. Use this (or realtimeBalanceOfNow) to check an + # account's actual balance — not the per-agreement versions. + # availableBalance = settled + agreement dynamics − max(0, deposit − owedDeposit) + mutability: view + inputs: + - account: address + - timestamp: uint256 + outputs: + - availableBalance: int256 + - deposit: uint256 + - owedDeposit: uint256 + +realtimeBalanceOfNow: + # Convenience wrapper: realtimeBalanceOf at the current Host timestamp. + # This is the recommended way to check an account's real-time balance. + mutability: view + inputs: + - account: address + outputs: + - availableBalance: int256 + - deposit: uint256 + - owedDeposit: uint256 + - timestamp: uint256 + +isAccountCritical: + # True if availableBalance < 0 at the given timestamp. + mutability: view + inputs: + - account: address + - timestamp: uint256 + outputs: + - isCritical: bool + +isAccountCriticalNow: + mutability: view + inputs: + - account: address + outputs: + - isCritical: bool + +isAccountSolvent: + # True if realtime balance (before deposit deduction) >= 0 at the given timestamp. + mutability: view + inputs: + - account: address + - timestamp: uint256 + outputs: + - isSolvent: bool + +isAccountSolventNow: + mutability: view + inputs: + - account: address + outputs: + - isSolvent: bool + +# == Admin == +# The admin defaults to the Host contract when address(0). +# An explicit admin can be set during initializeWithAdmin or via changeAdmin. + +changeAdmin: + mutability: nonpayable + access: admin + inputs: + - newAdmin: address + emits: [AdminChanged] + errors: [SUPER_TOKEN_ONLY_ADMIN] + +getAdmin: + # Returns address(0) if the Host is the implicit admin. + mutability: view + inputs: [] + outputs: + - address + +# == Yield Backend == +# Hot-pluggable yield generation for wrapper Super Tokens. The yield backend is a +# contract implementing IYieldBackend that is invoked via delegatecall. When enabled, +# the underlying ERC-20 is deposited into a yield protocol (e.g. Aave, Spark, ERC4626 vaults). +# Surplus yield (balance exceeding totalSupply) can be withdrawn by the admin. + +enableYieldBackend: + # Enable a yield backend and deposit all underlying token balance into it. + # Requires no existing yield backend to be set. + notes: + - "Gotcha: Reverts if a yield backend is already enabled — must call disableYieldBackend first to switch backends." + - "The yield backend contract is invoked via delegatecall — it executes in the SuperToken's context." + mutability: nonpayable + access: admin + inputs: + - newYieldBackend: address # must implement IYieldBackend + emits: [YieldBackendEnabled] + +disableYieldBackend: + # Withdraw all assets from the yield backend and disable it. + notes: + - "Calls withdrawMax() then disable() on the backend via delegatecall." + mutability: nonpayable + access: admin + inputs: [] + emits: [YieldBackendDisabled] + +getYieldBackend: + # Returns the current yield backend address, or address(0) if none is set. + mutability: view + inputs: [] + outputs: + - yieldBackend: address + +withdrawSurplusFromYieldBackend: + # Withdraw yield surplus (deposited amount exceeding totalSupply) from the backend. + # The surplus is sent to a receiver defined by the yield backend implementation. + mutability: nonpayable + access: admin + inputs: [] + +# == Self Operations == +# Can only be called by the token contract itself (address(this)). +# Used by custom super token proxies (SETH, Pure) to mint/burn/approve/transfer +# tokens through their own proxy logic. + +selfMint: + mutability: nonpayable + access: self + inputs: + - account: address + - amount: uint256 + - userData: bytes + emits: [Minted, Transfer] + errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS] + +selfBurn: + mutability: nonpayable + access: self + inputs: + - account: address + - amount: uint256 + - userData: bytes + emits: [Burned, Transfer] + errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE] + +selfApproveFor: + mutability: nonpayable + access: self + inputs: + - account: address + - spender: address + - amount: uint256 + emits: [Approval] + errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +selfTransferFrom: + mutability: nonpayable + access: self + inputs: + - holder: address + - spender: address + - recipient: address + - amount: uint256 + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +# == Host Batch Operations == +# Called by the Host contract on behalf of a user within batchCall / +# forwardBatchCall. Each mirrors a user-facing function but takes the +# account address explicitly. + +operationApprove: + mutability: nonpayable + access: host + inputs: + - account: address + - spender: address + - amount: uint256 + emits: [Approval] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +operationIncreaseAllowance: + mutability: nonpayable + access: host + inputs: + - account: address + - spender: address + - addedValue: uint256 + emits: [Approval] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +operationDecreaseAllowance: + mutability: nonpayable + access: host + inputs: + - account: address + - spender: address + - subtractedValue: uint256 + emits: [Approval] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +operationTransferFrom: + mutability: nonpayable + access: host + inputs: + - account: address + - spender: address + - recipient: address + - amount: uint256 + emits: [Sent, Transfer] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +operationSend: + mutability: nonpayable + access: host + inputs: + - spender: address + - recipient: address + - amount: uint256 + - userData: bytes + emits: [Sent, Transfer] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] + +operationUpgrade: + mutability: nonpayable + access: host + inputs: + - account: address + - amount: uint256 + emits: [Minted, Transfer, TokenUpgraded] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] + +operationDowngrade: + mutability: nonpayable + access: host + inputs: + - account: address + - amount: uint256 + emits: [Burned, Transfer, TokenDowngraded] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] + +operationUpgradeTo: + mutability: nonpayable + access: host + inputs: + - account: address + - to: address + - amount: uint256 + emits: [Minted, Transfer, TokenUpgraded] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] + +operationDowngradeTo: + mutability: nonpayable + access: host + inputs: + - account: address + - to: address + - amount: uint256 + emits: [Burned, Transfer, TokenDowngraded] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] + +# == Agreement Hosting == +# The token acts as storage host for agreement data. These functions are +# called by registered agreement contracts (CFA, GDA, IDA) through the Host. + +createAgreement: + mutability: nonpayable + access: agreement + inputs: + - id: bytes32 + - "data: bytes32[]" + emits: [AgreementCreated] + errors: [SF_TOKEN_AGREEMENT_ALREADY_EXISTS] + +getAgreementData: + mutability: view + inputs: + - agreementClass: address + - id: bytes32 + - dataLength: uint256 + outputs: + - "data: bytes32[]" + +updateAgreementData: + mutability: nonpayable + access: agreement + inputs: + - id: bytes32 + - "data: bytes32[]" + emits: [AgreementUpdated] + +terminateAgreement: + mutability: nonpayable + access: agreement + inputs: + - id: bytes32 + - dataLength: uint256 + emits: [AgreementTerminated] + errors: [SF_TOKEN_AGREEMENT_DOES_NOT_EXIST] + +updateAgreementStateSlot: + mutability: nonpayable + access: agreement + inputs: + - account: address + - slotId: uint256 + - "slotData: bytes32[]" + emits: [AgreementStateUpdated] + +getAgreementStateSlot: + mutability: view + inputs: + - agreementClass: address + - account: address + - slotId: uint256 + - dataLength: uint256 + outputs: + - "slotData: bytes32[]" + +settleBalance: + # Adjust an account's settled balance. Only listed agreements can call this. + mutability: nonpayable + access: agreement + inputs: + - account: address + - delta: int256 + errors: [SF_TOKEN_ONLY_LISTED_AGREEMENT] + +makeLiquidationPayoutsV2: + # Execute liquidation payouts. Transfers reward to liquidator or reward account, + # and adjusts the target account's balance. Only listed agreements can call this. + mutability: nonpayable + access: agreement + inputs: + - id: bytes32 + - liquidationTypeData: bytes + - liquidatorAccount: address + - useDefaultRewardAccount: bool + - targetAccount: address + - rewardAmount: uint256 + - targetAccountBalanceDelta: int256 + emits: [Transfer, AgreementLiquidatedV2] + errors: [SF_TOKEN_ONLY_LISTED_AGREEMENT] + +getAccountActiveAgreements: + mutability: view + inputs: + - account: address + outputs: + - "ISuperAgreement[]" + +# == Protocol Info == + +getHost: + mutability: view + inputs: [] + outputs: + - host: address + +POOL_ADMIN_NFT: + # Immutable address of the canonical PoolAdminNFT proxy. + mutability: view + inputs: [] + outputs: + - address + +POOL_MEMBER_NFT: + # Immutable address of the (deprecated) PoolMemberNFT proxy. + mutability: view + inputs: [] + outputs: + - address + +VERSION: + # Returns the version string of the SuperToken logic contract (e.g. "1.0.0"). + mutability: view + inputs: [] + outputs: + - string + +# == Events == + +events: + + # -- ERC-20 -- + + Transfer: + indexed: + - from: address + - to: address + data: + - value: uint256 + + Approval: + indexed: + - owner: address + - spender: address + data: + - value: uint256 + + # -- ERC-777 -- + + Sent: + indexed: + - operator: address + - from: address + - to: address + data: + - amount: uint256 + - data: bytes + - operatorData: bytes + + Minted: + indexed: + - operator: address + - to: address + data: + - amount: uint256 + - data: bytes + - operatorData: bytes + + Burned: + indexed: + - operator: address + - from: address + data: + - amount: uint256 + - data: bytes + - operatorData: bytes + + AuthorizedOperator: + indexed: + - operator: address + - tokenHolder: address + + RevokedOperator: + indexed: + - operator: address + - tokenHolder: address + + # -- EIP-5267 -- + + EIP712DomainChanged: + # Signalled when the EIP-712 domain changes. Not explicitly emitted by + # SuperToken, but declared in the EIP-5267 interface for indexing support. + + # -- Wrapping -- + + TokenUpgraded: + indexed: + - account: address + data: + - amount: uint256 + + TokenDowngraded: + indexed: + - account: address + data: + - amount: uint256 + + # -- Admin -- + + AdminChanged: + indexed: [] + data: + - oldAdmin: address + - newAdmin: address + + # -- Yield Backend -- + + YieldBackendEnabled: + indexed: + - yieldBackend: address + data: + - depositAmount: uint256 + + YieldBackendDisabled: + indexed: + - yieldBackend: address + + PoolAdminNFTCreated: + # Emitted once during logic contract constructor deployment. + indexed: [] + data: + - poolAdminNFT: address + + # -- Agreement Hosting -- + + AgreementCreated: + indexed: + - agreementClass: address + - id: bytes32 + data: + - "data: bytes32[]" + + AgreementUpdated: + indexed: + - agreementClass: address + - id: bytes32 + data: + - "data: bytes32[]" + + AgreementTerminated: + indexed: + - agreementClass: address + - id: bytes32 + + AgreementStateUpdated: + indexed: + - agreementClass: address + - account: address + data: + - slotId: uint256 + + AgreementLiquidatedV2: + indexed: + - agreementClass: address + - id: bytes32 + data: + - liquidatorAccount: address + - targetAccount: address + - rewardAmountReceiver: address + - rewardAmount: uint256 + - targetAccountBalanceDelta: int256 + - liquidationTypeData: bytes + + # -- Deprecated events (from ISuperfluidToken, no longer emitted) -- + + AgreementLiquidated: + deprecated: true + note: "Use AgreementLiquidatedV2" + indexed: + - agreementClass: address + - id: bytes32 + data: + - penaltyAccount: address + - rewardAccount: address + - rewardAmount: uint256 + + AgreementLiquidatedBy: + deprecated: true + note: "Use AgreementLiquidatedV2" + indexed: + - liquidatorAccount: address + - agreementClass: address + - id: bytes32 + data: + - penaltyAccount: address + - bondAccount: address + - rewardAmount: uint256 + - bailoutAmount: uint256 + + Bailout: + deprecated: true + note: "Use AgreementLiquidatedV2" + indexed: + - bailoutAccount: address + - bailoutAmount: uint256 + +# == Errors == + +errors: + + # -- Super Token access -- + - SUPER_TOKEN_ONLY_SELF + - SUPER_TOKEN_ONLY_ADMIN + - SUPER_TOKEN_ONLY_GOV_OWNER + + # -- SuperfluidToken access -- + - SF_TOKEN_ONLY_HOST + - SF_TOKEN_ONLY_LISTED_AGREEMENT + + # -- Wrapping -- + - SUPER_TOKEN_NO_UNDERLYING_TOKEN + - SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED + + # -- ERC-20 / ERC-777 transfer -- + - SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS + - SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS + - SF_TOKEN_MOVE_INSUFFICIENT_BALANCE + + # -- Approve -- + - SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS + - SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS + + # -- Mint / Burn -- + - SUPER_TOKEN_MINT_TO_ZERO_ADDRESS + - SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS + - SF_TOKEN_BURN_INSUFFICIENT_BALANCE + + # -- ERC-777 operator -- + - SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER + - SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT + + # -- ERC-2612 permit -- + - SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE: + inputs: + - deadline: uint256 + - SUPER_TOKEN_PERMIT_INVALID_SIGNER: + inputs: + - signer: address + - owner: address + + # -- NFT proxy -- + - SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED + + # -- Agreement hosting -- + - SF_TOKEN_AGREEMENT_ALREADY_EXISTS + - SF_TOKEN_AGREEMENT_DOES_NOT_EXIST + + # -- OpenZeppelin ECDSA (from permit) -- + - ECDSAInvalidSignature + - ECDSAInvalidSignatureLength: + inputs: + - length: uint256 + - ECDSAInvalidSignatureS: + inputs: + - s: bytes32 + + # -- OpenZeppelin SafeCast -- + - SafeCastOverflowedUintToInt: + inputs: + - value: uint256 + + # -- OpenZeppelin SafeERC20 -- + - SafeERC20FailedOperation: + inputs: + - token: address diff --git a/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml b/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml new file mode 100644 index 0000000..f9bb474 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml @@ -0,0 +1,99 @@ +# Generated by scripts/selectors.mjs +functions: + name(): 0x06fdde03 + symbol(): 0x95d89b41 + decimals(): 0x313ce567 + totalSupply(): 0x18160ddd + balanceOf(address): 0x70a08231 + allowance(address,address): 0xdd62ed3e + approve(address,uint256): 0x095ea7b3 + increaseAllowance(address,uint256): 0x39509351 + decreaseAllowance(address,uint256): 0xa457c2d7 + transfer(address,uint256): 0xa9059cbb + transferFrom(address,address,uint256): 0x23b872dd + transferAll(address): 0xa3a7e7f3 + granularity(): 0x556f0dc7 + send(address,uint256,bytes): 0x9bd9bbc6 + burn(uint256,bytes): 0xfe9d9303 + authorizeOperator(address): 0x959b8c3f + revokeOperator(address): 0xfad8b32a + isOperatorFor(address,address): 0xd95b6371 + defaultOperators(): 0x06e48538 + operatorSend(address,address,uint256,bytes,bytes): 0x62ad1b83 + operatorBurn(address,uint256,bytes,bytes): 0xfc673c4f + permit(address,address,uint256,uint256,uint8,bytes32,bytes32): 0xd505accf + nonces(address): 0x7ecebe00 + DOMAIN_SEPARATOR(): 0x3644e515 + eip712Domain(): 0x84b0196e + upgrade(): 0xd55ec697 + upgradeTo(address,uint256,bytes): 0x5b9d09cc + downgrade(uint256): 0x11bcc81e + downgradeTo(address,uint256): 0x83ba2525 + getUnderlyingToken(): 0xee719bc8 + getUnderlyingDecimals(): 0x92081a47 + toUnderlyingAmount(uint256): 0x282a050b + upgradeByETH(): 0xcf81464b + upgradeByETHTo(address): 0x7687d19b + downgradeToETH(uint256): 0x160e8be3 + realtimeBalanceOf(address,uint256): 0xeb3537cc + realtimeBalanceOfNow(address): 0x2ec8eec7 + isAccountCritical(address,uint256): 0xd9d078d6 + isAccountCriticalNow(address): 0x79359f6f + isAccountSolvent(address,uint256): 0xb84cdd4a + isAccountSolventNow(address): 0xbb0d196e + changeAdmin(address): 0x8f283970 + getAdmin(): 0x6e9960c3 + enableYieldBackend(): 0x5127b621 + disableYieldBackend(): 0x370a190f + getYieldBackend(): 0xe729804b + withdrawSurplusFromYieldBackend(): 0x4a3acda0 + selfMint(address,uint256,bytes): 0xc68d4283 + selfBurn(address,uint256,bytes): 0x9d876741 + selfApproveFor(address,address,uint256): 0x66a12fb6 + selfTransferFrom(address,address,address,uint256): 0x41b706be + operationApprove(address,address,uint256): 0x62aa5287 + operationIncreaseAllowance(address,address,uint256): 0x4b2763b3 + operationDecreaseAllowance(address,address,uint256): 0xc780fd82 + operationTransferFrom(address,address,address,uint256): 0x16d055d6 + operationSend(address,address,uint256,bytes): 0xca0c1e7f + operationUpgrade(address,uint256): 0xca789464 + operationDowngrade(address,uint256): 0x245887fc + operationUpgradeTo(address,address,uint256): 0x1ae88ffc + operationDowngradeTo(address,address,uint256): 0x47ba7ad1 + createAgreement(bytes32,bytes32[]"): 0x816e2dc5 + getAgreementData(address,bytes32,uint256): 0x6c2d9f2f + updateAgreementData(bytes32,bytes32[]"): 0x1bced9c7 + terminateAgreement(bytes32,uint256): 0x27048397 + updateAgreementStateSlot(address,uint256,bytes32[]"): 0x003af740 + getAgreementStateSlot(address,address,uint256,uint256): 0x4b61cc33 + settleBalance(address,int256): 0xcf97256d + makeLiquidationPayoutsV2(bytes32,bytes,address,bool,address,uint256,int256): 0x1863e809 + getAccountActiveAgreements(address): 0x386fa221 + getHost(): 0x20bc4425 + POOL_ADMIN_NFT(): 0xb20db1ac + POOL_MEMBER_NFT(): 0xf5a8b4dd + VERSION(): 0xffa1ad74 +events: + Transfer(address,address,uint256): 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + Approval(address,address,uint256): 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + Sent(address,address,address,uint256,bytes,bytes): 0x06b541ddaa720db2b10a4d0cdac39b8d360425fc073085fac19bc82614677987 + Minted(address,address,uint256,bytes,bytes): 0x2fe5be0146f74c5bce36c0b80911af6c7d86ff27e89d5cfa61fc681327954e5d + Burned(address,address,uint256,bytes,bytes): 0xa78a9be3a7b862d26933ad85fb11d80ef66b8f972d7cbba06621d583943a4098 + AuthorizedOperator(address,address): 0xf4caeb2d6ca8932a215a353d0703c326ec2d81fc68170f320eb2ab49e9df61f9 + RevokedOperator(address,address): 0x50546e66e5f44d728365dc3908c63bc5cfeeab470722c1677e3073a6ac294aa1 + EIP712DomainChanged(): 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31 + TokenUpgraded(address,uint256): 0x25ca84076773b0455db53621c459ddc84fe40840e4932a62706a032566f399df + TokenDowngraded(address,uint256): 0x3bc27981aebbb57f9247dc00fde9d6cd91e4b230083fec3238fedbcba1f9ab3d + AdminChanged(address,address): 0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f + YieldBackendEnabled(address,uint256): 0x8d15dd569157df615aedb4b16273001ae8980ce2aac93e39adc48481dfaefbb5 + YieldBackendDisabled(address): 0x2486b5241145ce1b97a13655ecd4e8e51094ac93259a0589d24524304d6d70d4 + PoolAdminNFTCreated(address): 0xeb87fb34067547f3dc0b85096c3da73c99d4fbb08ff41212b8d7c0b5008b42e6 + AgreementCreated(address,bytes32,bytes32[]"): 0x9a5caaf4460c6dff81ed3dc58f3aef40fe511737a163340e06a4abc1bfe21a73 + AgreementUpdated(address,bytes32,bytes32[]"): 0xc00aaaf56818c1a9d6c54e14c6f93965eb34c9729232fdafa62f90a10600e70a + AgreementTerminated(address,bytes32): 0x71a63dc095de07aa5512ad57a7596a39516317e316981a1cd71000057be1537b + AgreementStateUpdated(address,address,uint256): 0x30f416fa68fca014a0f334464c64b000ba53e99b6d2afdea9d5ca756372d5985 + AgreementLiquidatedV2(address,bytes32,address,address,address,uint256,int256,bytes): 0xb8381a3ce157650e06186e3e8f4dd4dc29236f2688b6eed1893d0a60d7c6386f + AgreementLiquidated(address,bytes32,address,address,uint256): 0x8505c3d8f1f184f032cf0bc4cd80ee61c8b9d94f8907c3281bf0101a2610fe80 + AgreementLiquidatedBy(address,address,bytes32,address,address,uint256,uint256): 0x5f22b60e58b1d6de858bc27c48d5a4653e052da99e083c1d88bb8c58e1abc8ef + Bailout(address,uint256): 0xd6c9a04afc81e8c614310bbee6c9e84f5abe15b82038bf8347014ce0852e6ffd +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml b/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml new file mode 100644 index 0000000..33ac5a9 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml @@ -0,0 +1,757 @@ +# Superfluid Host — the central protocol router +# All agreement calls (CFA, IDA, GDA) are routed through the Host, which manages +# the Superfluid call context, Super App callbacks, app credit, and governance. +# The Host is also the entry point for batch calls and meta-transactions. +# +# NOTE: emits/errors mappings are traced from source code — verify against implementation. +# Proxy/upgradability functions (castrate, updateCode, getCodeAddress, proxiableUUID, initialize) +# are omitted — they belong to the UUPSProxiable layer. + +meta: + name: Superfluid + version: v1 + source: + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol + implements: [ISuperfluid] + inherits: [UUPSProxiable, BaseRelayRecipient] + deployments: + mainnet: + eth-mainnet: "0x4E583d9390082B65Bef884b629DFA426114CED6d" + polygon-mainnet: "0x3E14dC1b13c488a8d5D310918780c983bD5982E7" + xdai-mainnet: "0x2dFe937cD98Ab92e59cF3139138f18c823a4efE7" + base-mainnet: "0x4C073B3baB6d8826b8C5b229f3cfdC1eC6E47E74" + optimism-mainnet: "0x567c4B141ED61923967cA25Ef4906C8781069a10" + arbitrum-one: "0xCf8Acb4eF033efF16E8080aed4c7D5B9285D2192" + bsc-mainnet: "0xd1e2cFb6441680002Eb7A44223160aB9B67d7E6E" + avalanche-c: "0x60377C7016E4cdB03C87EF474896C11cB560752C" + celo-mainnet: "0xA4Ff07cF81C02CFD356184879D953970cA957585" + scroll-mainnet: "0x0F86a21F6216c061B222c224e315d9FC34520bb7" + degenchain: "0xc1314EdcD7e478C831a7a24169F7dEADB2646eD2" + testnet: + avalanche-fuji: "0x85Fe79b998509B77BF10A8BD4001D58475D29386" + base-sepolia: "0x109412E3C84f0539b43d39dB691B08c90f58dC7c" + eth-sepolia: "0x109412E3C84f0539b43d39dB691B08c90f58dC7c" + optimism-sepolia: "0xd399e2Fb5f4cf3722a11F65b88FAB6B2B8621005" + scroll-sepolia: "0x42b05a6016B9eED232E13fd56a8F0725693DBF8e" + deploymentCreationBlocks: + mainnet: + celo-mainnet: 16393469 + +# == Abbreviations == +# ctx — context (Superfluid call context bytes) +# ACL — Access Control List +# ERC — Ethereum Request for Comments + +# == Glossary == +# Host — central Superfluid contract routing agreement/app calls +# agreement class — a registered agreement contract (CFA, IDA, GDA) +# Super App — a smart contract registered with the Host that receives callbacks +# callback — beforeAgreement* / afterAgreement* hooks called on Super Apps +# context (ctx) — encoded call metadata: msgSender, timestamp, userData, app credit +# app credit — temporary token allowance granted to a Super App during a callback +# jailed — a Super App that violated protocol rules; its callbacks are bypassed +# configWord — bitmask encoding a Super App's app level and callback noop flags. +# App level: APP_LEVEL_FINAL (1 << 1) or APP_LEVEL_SECOND (1 << 0). +# Noop flags disable specific before/after callbacks per agreement type. +# batch operation — a single step in a batchCall array (see BatchOperation in Definitions.sol) +# trusted forwarder — a contract allowed to call forwardBatchCall for meta-transactions + +# == Batch Call == +# Primary entry point for interacting with the protocol. Users compose arrays of +# typed operations (ERC-20 approvals, SuperToken wrapping, agreement calls, etc.). +# +# == Batch Operation Types == +# Used in the operationType field of batchCall/forwardBatchCall operations. +# Data encoding varies by category — see notes on each type. +# +# Category 1–5: ERC-20/ERC-777 token ops +# target: must be a SuperToken (routes through SuperToken.operation* methods, +# NOT arbitrary ERC-20 contracts — use a separate tx for underlying approvals) +# data: abi-encoded parameters only (strip the 4-byte function selector) +# +# Category 101–102: SuperToken wrap/unwrap +# target: the SuperToken +# data: abi-encoded parameters only (strip the 4-byte function selector) +# +# Category 201: agreement call +# target: the agreement contract (e.g. CFA, GDA) +# data: abi.encode(callData, userData) where callData is full +# encodeFunctionData (selector included, with empty ctx placeholder "0x") +# +# Category 202: app action +# target: the Super App +# data: abi.encode(callData) where callData is full encodeFunctionData +# (selector included, with empty ctx placeholder "0x") +# +# Category 301: simple forward +# target: any contract +# data: full encodeFunctionData (selector included), or "0x" for pure value transfers +# +# Category 302: ERC-2771 forward +# target: any contract (must be an ERC-2771 recipient to extract the real sender) +# data: full encodeFunctionData (selector included) +# The ERC2771Forwarder appends the original msg.sender to calldata per ERC-2771. +# Use when the target contract needs to know who initiated the batch. + +batch_operation_types: + ERC20_APPROVE: 1 + ERC20_TRANSFER_FROM: 2 + ERC777_SEND: 3 # deprecated — ERC-777 is being phased out + ERC20_INCREASE_ALLOWANCE: 4 + ERC20_DECREASE_ALLOWANCE: 5 + SUPERTOKEN_UPGRADE: 101 + SUPERTOKEN_DOWNGRADE: 102 + SUPERFLUID_CALL_AGREEMENT: 201 + CALL_APP_ACTION: 202 + SIMPLE_FORWARD_CALL: 301 + ERC2771_FORWARD_CALL: 302 + +batchCall: + # Execute an array of operations in a single transaction as msg.sender. + notes: + - "Gotcha: For SIMPLE_FORWARD_CALL (type 301), target sees msg.sender as the SimpleForwarder contract, NOT the original caller. Use only when the target doesn't need to identify the sender." + - "Gotcha: ERC-20 ops (types 1-5) route through SuperToken.operation* methods, NOT arbitrary ERC-20 contracts. The target must be a SuperToken address. To approve an underlying ERC-20 (e.g. for wrapping), use a separate transaction." + - "Gotcha: If msg.value > 0, the entire amount is forwarded to the first CALL_APP_ACTION, SIMPLE_FORWARD, or ERC2771_FORWARD operation. If none exist, the native tokens are returned to the sender." + mutability: payable + access: anyone + inputs: + - operations: tuple[] + components: + - operationType: uint32 # see batch_operation_types above + - target: address + - data: bytes # encoding varies by operationType — see above + errors: [HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE] + +forwardBatchCall: + # Same as batchCall but uses EIP-2771 to extract msgSender from calldata. + # Can only be called by contracts registered as trusted forwarders in governance. + # If native tokens are left over, they are refunded to the EIP-2771 sender. + mutability: payable + access: trusted-forwarder + inputs: + - operations: tuple[] + components: + - operationType: uint32 # see batch_operation_types above + - target: address + - data: bytes # encoding varies by operationType — see above + errors: [HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE] + +# == Contextless Call Proxies == +# Entry points for EOAs and non-app contracts to interact with agreements or apps. +# These create a fresh Superfluid context with msg.sender, inject it into the call, +# and route through to the target agreement/app. + +callAgreement: + # Call an agreement function. The Host creates a context with msg.sender and replaces + # the placeholder ctx bytes in callData. This is how EOAs interact with CFA/IDA/GDA. + mutability: nonpayable + access: anyone # context set to msg.sender + inputs: + - agreementClass: address # must be a registered agreement + - callData: bytes # abi-encoded agreement call with placeholder ctx + - userData: bytes # extra data forwarded to Super App callbacks + outputs: + - returnedData: bytes + errors: [APP_RULE, HOST_ONLY_LISTED_AGREEMENT] + +callAppAction: + # Call an app action function on a Super App. The Host creates a context and routes + # the call. The action must not be an agreement callback selector. + mutability: nonpayable + access: anyone # context set to msg.sender + inputs: + - app: address # must be a registered, non-jailed Super App + - callData: bytes # abi-encoded app action call with placeholder ctx + outputs: + - returnedData: bytes + errors: [APP_RULE, HOST_NOT_A_SUPER_APP, HOST_SUPER_APP_IS_JAILED, HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION] + +# == Contextual Call Proxies == +# For Super Apps to chain calls during callbacks. The app must pass the ctx it received. +# Only callable by the app currently executing a callback (ctx.appAddress == msg.sender). + +callAgreementWithContext: + # Chain an agreement call from within a Super App callback. + # The ctx must be valid and msg.sender must be the current callback app. + mutability: nonpayable + access: super-app + inputs: + - agreementClass: address + - callData: bytes + - userData: bytes + - ctx: bytes + outputs: + - newCtx: bytes + - returnedData: bytes + errors: [APP_RULE, HOST_ONLY_LISTED_AGREEMENT, HOST_CALL_AGREEMENT_WITH_CTX_FROM_WRONG_ADDRESS] + +callAppActionWithContext: + # Chain an app action call from within a Super App callback. + mutability: nonpayable + access: super-app + inputs: + - app: address + - callData: bytes + - ctx: bytes + outputs: + - newCtx: bytes + errors: [APP_RULE, HOST_NOT_A_SUPER_APP, HOST_SUPER_APP_IS_JAILED, HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION, HOST_CALL_APP_ACTION_WITH_CTX_FROM_WRONG_ADDRESS] + +# == Context Utilities == + +decodeCtx: + # Decode a raw context bytes blob into the structured Context fields. + mutability: pure + inputs: + - ctx: bytes + outputs: + - context: + type: tuple + components: + - appCallbackLevel: uint8 + - callType: uint8 + - timestamp: uint256 + - msgSender: address + - agreementSelector: bytes4 + - userData: bytes + - appCreditGranted: uint256 + - appCreditWantedDeprecated: uint256 + - appCreditUsed: int256 + - appAddress: address + - appCreditToken: address + +isCtxValid: + # Check if a context blob matches the current transaction's context stamp. + mutability: view + inputs: + - ctx: bytes + outputs: + - bool + +# == App Registry == +# Super App registration. On some deployments, governance permission (via SimpleACL +# or legacy governance config) is required before an app can register. + +registerApp: + # Register msg.sender as a Super App with the given config word. + # On whitelisting-enabled deployments, tx.origin must have the + # ACL_SUPERAPP_REGISTRATION_ROLE or a valid legacy governance key. + mutability: nonpayable + access: self + inputs: + - configWord: uint256 # Super App manifest flags (see SuperAppDefinitions) + emits: [AppRegistered] + errors: [HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] + +registerApp(address,uint256): + # Register an external contract as a Super App. + # Useful for factory patterns where the deployer registers the app. + mutability: nonpayable + access: anyone + inputs: + - app: address # must be a contract + - configWord: uint256 + emits: [AppRegistered] + errors: [HOST_MUST_BE_CONTRACT, HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] + +registerAppWithKey: + # DEPRECATED — use registerApp(uint256) instead. + # Legacy registration using a governance-provided registration key string. + mutability: nonpayable + access: self + inputs: + - configWord: uint256 + - registrationKey: string + emits: [AppRegistered] + errors: [HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] + +registerAppByFactory: + # DEPRECATED — use registerApp(address, uint256) instead. + # Legacy factory-based registration using governance-authorized factory addresses. + mutability: nonpayable + access: factory + inputs: + - app: address + - configWord: uint256 + emits: [AppRegistered] + errors: [HOST_MUST_BE_CONTRACT, HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] + +# == App Queries == + +isApp: + mutability: view + inputs: + - app: address + outputs: + - bool + +getAppCallbackLevel: + mutability: view + inputs: + - appAddr: address + outputs: + - uint8 + +getAppManifest: + # Returns the full manifest of a Super App. + # isSuperApp is false if the address was never registered. + mutability: view + inputs: + - app: address + outputs: + - isSuperApp: bool + - isJailed: bool + - noopMask: uint256 # bitmask of noop'd agreement callback selectors + +isAppJailed: + mutability: view + inputs: + - app: address + outputs: + - isJail: bool + +# == App Composition == +# Source apps can whitelist target apps for composability (calling downstream). +# Currently limited to MAX_APP_CALLBACK_LEVEL = 1 (one level of nesting). + +allowCompositeApp: + # Whitelist a target Super App for composition. msg.sender must be a Super App + # with a higher callback level than the target. + mutability: nonpayable + access: super-app + inputs: + - targetApp: address + errors: [HOST_SENDER_IS_NOT_SUPER_APP, HOST_RECEIVER_IS_NOT_SUPER_APP, HOST_SOURCE_APP_NEEDS_HIGHER_APP_LEVEL] + +isCompositeAppAllowed: + mutability: view + inputs: + - app: address + - targetApp: address + outputs: + - isAppAllowed: bool + +# == Agreement Framework == +# Internal protocol functions called by registered agreement contracts (CFA, IDA, GDA) +# to manage Super App callbacks and app credit. Only callable by agreements. +# +# Callback lifecycle (called by agreement during flow operations): +# 1. appCallbackPush — set up credit, push callback frame +# 2. callAppBeforeCallback — staticcall app's before hook (read-only) +# 3. (agreement executes core logic) +# 4. callAppAfterCallback — call app's after hook (state changes OK) +# 5. appCallbackPop — finalize credit accounting, pop frame + +callAppBeforeCallback: + # StaticCall a Super App's before-callback. Returns the callback data. + # If the callback reverts during a termination, the app is jailed instead of reverting. + mutability: nonpayable + access: agreement + inputs: + - app: address + - callData: bytes + - isTermination: bool + - ctx: bytes + outputs: + - cbdata: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT, HOST_NEED_MORE_GAS, APP_RULE] + +callAppAfterCallback: + # Call a Super App's after-callback. Returns the (potentially modified) context. + # If the app returns an invalid ctx, it is jailed on termination or reverts otherwise. + mutability: nonpayable + access: agreement + inputs: + - app: address + - callData: bytes + - isTermination: bool + - ctx: bytes + outputs: + - newCtx: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT, HOST_NEED_MORE_GAS, APP_RULE] + +appCallbackPush: + # Push a new callback frame onto the context stack before invoking a Super App. + # Sets up app credit and tracks the callback nesting level. + notes: + - "Gotcha: The appCreditGranted is backed by the flow sender's deposit as \"owed deposit\". If the Super App opens outgoing streams using this credit, the sender's total locked capital increases accordingly (roughly doubles for 1:1 relay, more for fan-out patterns)." + mutability: nonpayable + access: agreement + inputs: + - ctx: bytes + - app: address + - appCreditGranted: uint256 + - appCreditUsed: int256 + - appCreditToken: address + outputs: + - appCtx: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT, APP_RULE] + +appCallbackPop: + # Pop the callback frame after a Super App callback completes. + # Adjusts appCreditUsed by the delta from the callback. + mutability: nonpayable + access: agreement + inputs: + - ctx: bytes + - appCreditUsedDelta: int256 + outputs: + - newCtx: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT] + +ctxUseCredit: + # Record additional app credit usage during an agreement operation. + mutability: nonpayable + access: agreement + inputs: + - ctx: bytes + - appCreditUsedMore: int256 + outputs: + - newCtx: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT] + +jailApp: + # Jail a Super App from within an agreement. Jailed apps have their callbacks bypassed. + mutability: nonpayable + access: agreement + inputs: + - ctx: bytes + - app: address + - reason: uint256 # see SuperAppDefinitions for jail reason codes + outputs: + - newCtx: bytes + emits: [Jail] + errors: [HOST_ONLY_LISTED_AGREEMENT] + +# == Governance == + +getGovernance: + mutability: view + outputs: + - governance: address + +replaceGovernance: + mutability: nonpayable + access: governance + inputs: + - newGov: address + emits: [GovernanceReplaced] + errors: [HOST_ONLY_GOVERNANCE] + +# == Agreement Whitelisting == +# Governance registers and upgrades agreement classes (CFA, IDA, GDA). + +registerAgreementClass: + # Register a new agreement class. Creates a UUPS proxy in upgradable deployments. + mutability: nonpayable + access: governance + inputs: + - agreementClassLogic: address + emits: [AgreementClassRegistered] + errors: [HOST_ONLY_GOVERNANCE, HOST_AGREEMENT_ALREADY_REGISTERED, HOST_MAX_256_AGREEMENTS] + +updateAgreementClass: + # Upgrade an existing agreement class to a new implementation. + mutability: nonpayable + access: governance + inputs: + - agreementClassLogic: address + emits: [AgreementClassUpdated] + errors: [HOST_ONLY_GOVERNANCE, HOST_NON_UPGRADEABLE, HOST_AGREEMENT_IS_NOT_REGISTERED] + +isAgreementTypeListed: + mutability: view + inputs: + - agreementType: bytes32 # keccak256 of agreement name string + outputs: + - yes: bool + +isAgreementClassListed: + mutability: view + inputs: + - agreementClass: address + outputs: + - yes: bool + +getAgreementClass: + mutability: view + inputs: + - agreementType: bytes32 + outputs: + - agreementClass: address + errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] + +mapAgreementClasses: + # Decode a bitmap into an array of agreement class addresses. + mutability: view + inputs: + - bitmap: uint256 + outputs: + - agreementClasses: address[] + +addToAgreementClassesBitmap: + mutability: view + inputs: + - bitmap: uint256 + - agreementType: bytes32 + outputs: + - newBitmap: uint256 + errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] + +removeFromAgreementClassesBitmap: + mutability: view + inputs: + - bitmap: uint256 + - agreementType: bytes32 + outputs: + - newBitmap: uint256 + errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] + +# == Super Token Factory == + +getSuperTokenFactory: + mutability: view + outputs: + - factory: address + +getSuperTokenFactoryLogic: + # Returns the logic address behind the factory proxy. For non-upgradable + # deployments, returns the factory address itself. + mutability: view + outputs: + - logic: address + +updateSuperTokenFactory: + # Deploy or upgrade the SuperTokenFactory. On first call, creates a UUPS proxy. + mutability: nonpayable + access: governance + inputs: + - newFactory: address + emits: [SuperTokenFactoryUpdated] + errors: [HOST_ONLY_GOVERNANCE, HOST_NON_UPGRADEABLE] + +updateSuperTokenLogic: + # Upgrade a SuperToken proxy to new logic. If newLogicOverride is address(0), + # uses the canonical logic from the SuperTokenFactory. + mutability: nonpayable + access: governance + inputs: + - token: address + - newLogicOverride: address # address(0) means use canonical logic + emits: [SuperTokenLogicUpdated] + errors: [HOST_ONLY_GOVERNANCE] + +changeSuperTokenAdmin: + # Change the admin of a SuperToken. The admin is the only account allowed to + # upgrade the token logic. Default admin is the Host itself. + mutability: nonpayable + access: governance + inputs: + - token: address + - newAdmin: address + errors: [HOST_ONLY_GOVERNANCE] + +# == Pool Beacon == + +updatePoolBeaconLogic: + # Upgrade the SuperfluidPool beacon to new logic. Affects all GDA pool proxies. + mutability: nonpayable + access: governance + inputs: + - newLogic: address + emits: [PoolBeaconLogicUpdated] + errors: [HOST_ONLY_GOVERNANCE] + +# == ERC2771 / Forwarder == + +isTrustedForwarder: + # Check if an address is a governance-approved trusted forwarder for meta-transactions. + mutability: view + inputs: + - forwarder: address + outputs: + - bool + +versionRecipient: + # ERC2771 relay recipient version string. Returns "v1". + mutability: pure + outputs: + - string + +getERC2771Forwarder: + # Returns the ERC2771Forwarder contract used for ERC2771_FORWARD_CALL batch operations. + mutability: view + outputs: + - address + +getSimpleACL: + # Returns the SimpleACL contract used for Super App registration permissioning. + mutability: view + outputs: + - address + +# == Time == + +getNow: + # Returns block.timestamp. Useful for off-chain tooling and testing. + mutability: view + outputs: + - uint256 + +# == Protocol Constants == + +NON_UPGRADABLE_DEPLOYMENT: + # True if the Host was deployed in non-upgradable mode. + mutability: view + outputs: + - bool + +APP_WHITE_LISTING_ENABLED: + # True if Super App registration requires governance permission. + mutability: view + outputs: + - bool + +CALLBACK_GAS_LIMIT: + # Maximum gas forwarded to Super App callbacks. + mutability: view + outputs: + - uint64 + +MAX_APP_CALLBACK_LEVEL: + # Maximum callback nesting depth for composed Super Apps. Currently 1. + mutability: view + outputs: + - uint256 + +MAX_NUM_AGREEMENTS: + # Maximum number of agreement classes that can be registered. 256. + mutability: view + outputs: + - uint32 + +ACL_SUPERAPP_REGISTRATION_ROLE: + # keccak256("ACL_SUPERAPP_REGISTRATION_ROLE") — the SimpleACL role for app registration. + mutability: view + outputs: + - bytes32 + +SIMPLE_FORWARDER: + # Address of the SimpleForwarder used for SIMPLE_FORWARD_CALL batch operations. + mutability: view + outputs: + - address + +# == Events == + +events: + GovernanceReplaced: + data: + - oldGov: address + - newGov: address + + AgreementClassRegistered: + # agreementType is keccak256("org.superfluid-finance.agreements..") + data: + - agreementType: bytes32 + - code: address + + AgreementClassUpdated: + data: + - agreementType: bytes32 + - code: address + + SuperTokenFactoryUpdated: + data: + - newFactory: address + + SuperTokenLogicUpdated: + indexed: + - token: address + data: + - code: address + + PoolBeaconLogicUpdated: + indexed: + - beaconProxy: address + data: + - newBeaconLogic: address + + AppRegistered: + indexed: + - app: address + + Jail: + # Emitted when a Super App is jailed for violating protocol rules. + # See APP_RULE error below for reason code values. + indexed: + - app: address + data: + - reason: uint256 + + # Inherited events (from UUPSProxiable): + # CodeUpdated — emitted on proxy upgrade (uuid, codeAddress) + # Initialized — emitted on proxy initialization (version) + +# == Errors == + +errors: + # Governance + - HOST_ONLY_GOVERNANCE # caller is not the governance contract + + # Agreement whitelisting + - HOST_AGREEMENT_ALREADY_REGISTERED + - HOST_AGREEMENT_IS_NOT_REGISTERED + - HOST_MAX_256_AGREEMENTS # maximum agreement slots exhausted + - HOST_ONLY_LISTED_AGREEMENT # caller is not a registered agreement + + # Upgradability + - HOST_NON_UPGRADEABLE # deployment is non-upgradable + - HOST_CANNOT_DOWNGRADE_TO_NON_UPGRADEABLE + + # App registration + - HOST_MUST_BE_CONTRACT # app address has no code + - HOST_NO_APP_REGISTRATION_PERMISSION + - HOST_INVALID_CONFIG_WORD # invalid Super App manifest flags + - HOST_SUPER_APP_ALREADY_REGISTERED + - HOST_NOT_A_SUPER_APP # target is not a registered Super App + - HOST_SUPER_APP_IS_JAILED + + # App composition + - HOST_SENDER_IS_NOT_SUPER_APP + - HOST_RECEIVER_IS_NOT_SUPER_APP + - HOST_SOURCE_APP_NEEDS_HIGHER_APP_LEVEL + + # Context & call proxies + - HOST_NON_ZERO_LENGTH_PLACEHOLDER_CTX # placeholder ctx must be empty + - HOST_CALL_AGREEMENT_WITH_CTX_FROM_WRONG_ADDRESS # msg.sender != ctx.appAddress + - HOST_CALL_APP_ACTION_WITH_CTX_FROM_WRONG_ADDRESS # msg.sender != ctx.appAddress + - HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION # app action selector matches a callback + + # Batch call + - HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE + + # Callback execution + - HOST_NEED_MORE_GAS # insufficient gas for Super App callback + + # Super App rule violations (generic, carries a reason code) + # Reason codes (from SuperAppDefinitions.sol): + # 10 — NO_REVERT_ON_TERMINATION_CALLBACK (delete callback reverted) + # 11 — NO_CRITICAL_SENDER_ACCOUNT (callback made sender critical) + # 12 — NO_CRITICAL_RECEIVER_ACCOUNT (callback made receiver critical) + # 20 — CTX_IS_READONLY (state change in before-callback staticcall) + # 21 — CTX_IS_NOT_CLEAN (context not properly returned) + # 22 — CTX_IS_MALFORMATED (context bytes corrupted/fabricated) + # 30 — COMPOSITE_APP_IS_NOT_WHITELISTED (no allowCompositeApp) + # 31 — COMPOSITE_APP_IS_JAILED (downstream app is jailed) + # 40 — MAX_APP_LEVEL_REACHED (callback nesting exceeded limit) + - APP_RULE: + inputs: + - _code: uint256 + + # SafeCast (inherited from OpenZeppelin) + - SafeCastOverflowedUintDowncast: + inputs: + - bits: uint8 + - value: uint256 diff --git a/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml b/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml new file mode 100644 index 0000000..18edf9b --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml @@ -0,0 +1,51 @@ +# Generated by scripts/selectors.mjs +functions: + batchCall(tuple[]): 0x31131761 + forwardBatchCall(tuple[]): 0xbc5a2101 + callAgreement(): 0xb7a9d0f0 + callAppAction(): 0x40df3d29 + callAgreementWithContext(address,bytes,bytes,bytes): 0x4329d293 + callAppActionWithContext(address,bytes,bytes): 0xba48b5f8 + decodeCtx(bytes): 0x3f6c923a + isCtxValid(bytes): 0xbf428734 + registerApp(): 0x6d6c85c3 + registerAppWithKey(uint256,string): 0xbd1c448b + registerAppByFactory(address,uint256): 0xf3733052 + isApp(address): 0x3ca3ad4e + getAppCallbackLevel(address): 0x9378fa13 + getAppManifest(address): 0xf9f522f4 + isAppJailed(address): 0x6b4f3335 + allowCompositeApp(address): 0x57121e0c + isCompositeAppAllowed(address,address): 0xbb84cfa1 + callAppBeforeCallback(address,bytes,bool,bytes): 0x74041e02 + callAppAfterCallback(address,bytes,bool,bytes): 0x1e6d0a84 + appCallbackPush(bytes,address,uint256,int256,address): 0x768fabb0 + appCallbackPop(bytes,int256): 0x989b0c3e + ctxUseCredit(bytes,int256): 0x59a29141 + jailApp(bytes,address): 0x5578431e + replaceGovernance(address): 0x7283100c + registerAgreementClass(address): 0x15a024e1 + updateAgreementClass(address): 0x06cecba8 + isAgreementTypeListed(): 0xf8dba358 + isAgreementClassListed(address): 0x8ca48484 + getAgreementClass(bytes32): 0xb6d200de + mapAgreementClasses(uint256): 0xc56a069d + addToAgreementClassesBitmap(uint256,bytes32): 0xbced3ddc + removeFromAgreementClassesBitmap(uint256,bytes32): 0xa5dbbbcd + updateSuperTokenFactory(address): 0x54fbc493 + updateSuperTokenLogic(address): 0x787afde7 + changeSuperTokenAdmin(address,address): 0x0c565075 + updatePoolBeaconLogic(address): 0x2f89bf89 + isTrustedForwarder(address): 0x572b6c05 + SIMPLE_FORWARDER(): 0xf85263b9 +events: + GovernanceReplaced(address,address): 0x13abda02e63c790d0e2818b251282cfe5cbe0a8abd69c54bf5d2260c0907bd2e + AgreementClassRegistered(bytes32,address): 0x878135063a6cfb3bc333e534b1fdc83f4f12221cad6705c31c0567048a8bd3d1 + AgreementClassUpdated(bytes32,address): 0x9279aa773f2b588996032d8de89911555039f28b13a11a7c17074330bc082d9a + SuperTokenFactoryUpdated(address): 0xce13a9895a1719ad4493b2ac1a9bfb36070566161abab408e7ecbe586da8d499 + SuperTokenLogicUpdated(address,address): 0x840acbd291b38534819f47f875839277e502f40e1c7bfea2c5fc2c8017442cd3 + PoolBeaconLogicUpdated(address,address): 0x052cea8931962dd445ef48b0b998d3056bd0705f437087d60fe3c46a3fa09e1f + AppRegistered(address): 0x0d540ad8f39e07d19909687352b9fa017405d93c91a6760981fbae9cf28bfef7 + Jail(address,uint256): 0xbe3aa33bd245135e4e26b223d79d14ea479a47bff09f2b03c53838af1edbb14b + # Inherited events (from UUPSProxiable)(): 0x3a1d0f622cf5be33dc2558b359f025fbeca1d6fb271fb70452a02b0ec2666ce8 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml new file mode 100644 index 0000000..75e5e43 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml @@ -0,0 +1,305 @@ +# UBISchemeV2 — dynamic daily UBI pool with per-root claiming and cycle accounting +# Claim uses Identity getWhitelistedRoot(msg.sender); transfers use the root as claim key. + +meta: + name: UBISchemeV2 + version: v2 + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/ubi/UBISchemeV2.sol + inherits: [DAOUpgradeableContract] + note: > + Pool sizing uses cycleLength, reserveFactor, minActiveUsers, and on-chain G$ + balance. claim() reverts if Identity root is zero. Internal accounting keys + claims by whitelisted root, payouts go to msg.sender. + deployments: + mainnet: + production: + UBIScheme: + networkId: 122 + address: "0xd253A5203817225e9768C05E5996d642fb96bA86" + creationBlock: 15747401 + production-celo: + UBIScheme: + networkId: 42220 + address: "0x43d72Ff17701B2DA814620735C39C620Ce0ea4A1" + creationBlock: 18006679 + production-xdc: + UBIScheme: + networkId: 50 + address: "0x22867567E2D80f2049200E25C6F31CB6Ec2F0faf" + creationBlock: 95249624 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/ubischeme + - references/guides/claim.md + +getClaimerCount: + mutability: view + inputs: + - day: uint256 + outputs: + - count: uint256 + +getClaimAmount: + notes: + - "Returns aggregate amount claimed for the provided day index." + mutability: view + inputs: + - day: uint256 + outputs: + - amount: uint256 + +getDailyStats: + notes: + - "Returns (claimer count, total claimed amount) for the calendar day derived from periodStart." + mutability: view + inputs: [] + outputs: + - claimerCount: uint256 + - claimAmount: uint256 + +setCycleLength: + mutability: nonpayable + access: avatar + inputs: + - _newLength: uint256 + outputs: [] + emits: [CycleLengthSet] + errors: [CYCLE_TOO_SHORT, ONLY_AVATAR] + +setDay: + notes: + - "Anyone may advance currentDay when wall-clock day crosses the next boundary." + mutability: nonpayable + inputs: [] + outputs: [] + emits: [DaySet] + +hasClaimed: + mutability: view + inputs: + - account: address + outputs: + - claimed: bool + +isNotNewUser: + mutability: view + inputs: + - _account: address + outputs: + - isReturning: bool + +estimateNextDailyUBI: + notes: + - "View-only projection when dailyUbi not yet fixed for the current wall day." + mutability: view + inputs: [] + outputs: + - projected: uint256 + +"checkEntitlement()": + notes: + - "Uses msg.sender as member; resolves Identity root before hasClaimed check." + mutability: view + inputs: [] + outputs: + - amount: uint256 + +"checkEntitlement(address)": + notes: + - "Checks claimable amount for the provided member using that account's whitelisted root." + mutability: view + inputs: + - _member: address + outputs: + - amount: uint256 + +claim: + notes: + - "Resolves whitelistedRoot = Identity.getWhitelistedRoot(msg.sender); reverts if zero." + - "_claim keys bookkeeping by root but pays msg.sender." + - "May notify ClaimersDistribution at GDAO_CLAIMERS if configured." + - "On success emits UBIClaimed; distributionFormula may emit UBICalculated, UBICycleCalculated, DaySet, WithdrawFromDao." + mutability: nonpayable + access: anyone + inputs: [] + outputs: + - didClaim: bool + errors: + - NOT_IN_PERIOD_OR_PAUSED + - NOT_WHITELISTED + - CLAIM_TRANSFER_FAILED + - DAO_TRANSFER_FAILED + +setShouldWithdrawFromDAO: + mutability: nonpayable + access: avatar + inputs: + - _shouldWithdraw: bool + outputs: [] + emits: [ShouldWithdrawFromDAOSet] + errors: [ONLY_AVATAR] + +pause: + mutability: nonpayable + access: avatar + inputs: + - _pause: bool + outputs: [] + errors: [ONLY_AVATAR] + +setNewClaimersReserveFactor: + notes: + - "Adjusts reserveFactor used by distribution formula to reserve part of pool for new claimers." + mutability: nonpayable + access: avatar + inputs: + - _reserveFactor: uint32 + outputs: [] + errors: [ONLY_AVATAR] + +withdraw: + notes: + - "Avatar-controlled treasury action transferring G$ from scheme balance to recipient." + mutability: nonpayable + access: avatar + inputs: + - _amount: uint256 + - _recipient: address + outputs: [] + errors: + - ONLY_AVATAR + - WITHDRAW_FAILED + +lastClaimed: + notes: + - "Returns last claim timestamp keyed by account root logic used by hasClaimed checks." + mutability: view + inputs: + - account: address + outputs: + - ts: uint256 + +currentDay: + mutability: view + inputs: [] + outputs: + - day: uint256 + +periodStart: + mutability: view + inputs: [] + outputs: + - ts: uint256 + +dailyUbi: + mutability: view + inputs: [] + outputs: + - amount: uint256 + +lastWithdrawDay: + mutability: view + inputs: [] + outputs: + - day: uint256 + +shouldWithdrawFromDAO: + mutability: view + inputs: [] + outputs: + - flag: bool + +cycleLength: + mutability: view + inputs: [] + outputs: + - days: uint256 + +dailyCyclePool: + mutability: view + inputs: [] + outputs: + - pool: uint256 + +startOfCycle: + mutability: view + inputs: [] + outputs: + - ts: uint256 + +currentCycleLength: + mutability: view + inputs: [] + outputs: + - length: uint256 + +minActiveUsers: + mutability: view + inputs: [] + outputs: + - n: uint256 + +totalClaimsPerUser: + mutability: view + inputs: + - account: address + outputs: + - count: uint256 + +reserveFactor: + mutability: view + inputs: [] + outputs: + - bps: uint32 + +paused: + mutability: view + inputs: [] + outputs: + - isPaused: bool + +events: + WithdrawFromDao: + indexed: [] + data: + - prevBalance: uint256 + - newBalance: uint256 + UBICalculated: + indexed: [] + data: + - day: uint256 + - dailyUbi: uint256 + - blockNumber: uint256 + UBICycleCalculated: + indexed: [] + data: + - day: uint256 + - pool: uint256 + - cycleLength: uint256 + - dailyUBIPool: uint256 + UBIClaimed: + indexed: + - claimer: address + data: + - amount: uint256 + CycleLengthSet: + indexed: [] + data: + - newCycleLength: uint256 + DaySet: + indexed: [] + data: + - newDay: uint256 + ShouldWithdrawFromDAOSet: + indexed: [] + data: + - ShouldWithdrawFromDAO: bool + +errors: + NOT_IN_PERIOD_OR_PAUSED: "not in periodStarted or paused — requireStarted modifier." + CYCLE_TOO_SHORT: "cycle must be at least 1 day long" + DAO_TRANSFER_FAILED: "DAO transfer has failed — _withdrawFromDao balance check." + NOT_WHITELISTED: "UBIScheme: not whitelisted — claim()" + CLAIM_TRANSFER_FAILED: "claim transfer failed — G$ transfer in _transferTokens." + WITHDRAW_FAILED: "withdraw failed — avatar withdraw." + ONLY_AVATAR: "Inherited avatar-only guard on DAOUpgradeable paths." diff --git a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml new file mode 100644 index 0000000..666b89d --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml @@ -0,0 +1,40 @@ +# Generated by scripts/selectors.mjs +functions: + getClaimerCount(uint256): 0xc7a76adf + getClaimAmount(uint256): 0xcef63600 + getDailyStats(): 0x069786ea + setCycleLength(uint256): 0x3d84ceca + setDay(): 0xdddc3616 + hasClaimed(address): 0x73b2e80e + isNotNewUser(address): 0xa21f698a + estimateNextDailyUBI(): 0xc7713870 + checkEntitlement(): 0x98d6621b + checkEntitlement(address): 0x1a787f2e + claim(): 0x4e71d92d + setShouldWithdrawFromDAO(bool): 0xde1de3a0 + pause(bool): 0x02329a29 + setNewClaimersReserveFactor(uint32): 0x414089be + withdraw(uint256,address): 0x00f714ce + lastClaimed(address): 0x013eba92 + currentDay(): 0x5c9302c9 + periodStart(): 0xeda4e6d6 + dailyUbi(): 0x1d8f5ea9 + lastWithdrawDay(): 0xd7c4cbb8 + shouldWithdrawFromDAO(): 0x456ac1c2 + cycleLength(): 0xeac471a0 + dailyCyclePool(): 0x9dc2c033 + startOfCycle(): 0xba075410 + currentCycleLength(): 0x741470ac + minActiveUsers(): 0x37658574 + totalClaimsPerUser(address): 0xcc054dfc + reserveFactor(): 0x4322b714 + paused(): 0x5c975abb +events: + WithdrawFromDao(uint256,uint256): 0x3107ec7eaa50b775d2486c7a394472235804b6fe1c0d4b7bd1d79b09df60f2ba + UBICalculated(uint256,uint256,uint256): 0x836fa39995340265746dfe9587d9fe5c5de35b7bce778afd9b124ce1cfeafdc4 + UBICycleCalculated(uint256,uint256,uint256,uint256): 0x83e0d535b9e84324e0a25922406398d6ff5f96d0c686204ee490e16d7670566f + UBIClaimed(address,uint256): 0x89ed24731df6b066e4c5186901fffdba18cd9a10f07494aff900bdee260d1304 + CycleLengthSet(uint256): 0xa61e6cca2c12e2a0a493683acfe95b034f0f50d793434f4dfe3ba06ea201f344 + DaySet(uint256): 0x67eb03bd555181f9dd23f546e4331ddfb8b4a7d0c8d261ba44e037f30ce894ea + ShouldWithdrawFromDAOSet(bool): 0x6cd9a0fd2e006be39a9918bf56c85cae1d4f4599474483ff18cb93355ebaaf8e +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md b/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md new file mode 100644 index 0000000..41e2bae --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md @@ -0,0 +1,93 @@ +# Reading the Rich ABI YAMLs + +Each YAML is a self-contained contract reference. Here's how to parse them. + +## Root structure + +``` +# Header comment — contract name, description, key notes +meta: # name, version, source, implements, inherits, deployments, + # deploymentCreationBlocks (optional), notes +# == Section == # Grouped functions (these are the core content) +events: # All events the contract emits +errors: # Complete error index +``` + +Three root keys are reserved: `meta`, `events`, `errors`. Every other +root-level key is a **function**. + +## Function entries + +```yaml +createFlow: + # Description of what the function does. + notes: + - "Gotcha: Non-obvious behavior or edge cases listed here as structured data." + mutability: nonpayable # view | pure | nonpayable | payable + access: sender | operator # who can call (omitted for view/pure) + inputs: + - token: address + - receiver: address + - flowRate: int96 # inline comments for non-obvious params + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] # ordered by emission sequence + errors: [CFA_FLOW_ALREADY_EXISTS, CFA_INVALID_FLOW_RATE] # ordered by check sequence +``` + +Fields appear in this order: description comment, `notes`, `mutability`, +`access`, `inputs`, `outputs`, `emits`, `errors`. All are omitted when not +applicable. + +## Key conventions + +- **`ctx: bytes` parameter** = function is called through the Host + (`callAgreement` / `batchCall`), never directly. +- **`access` labels**: `anyone`, `host`, `self`, `admin`, `governance`, + `sender`, `receiver`, `operator`, `manager`, `pic`, `agreement`, + `trusted-forwarder`, `factory`, `super-app`. Combine with `|`. Conditional: + `anyone(if-critical-or-jailed)`. +- **`emits` and `errors` ordering** carries meaning: matches execution flow, + not alphabetical. First errors in the list are the most likely causes. +- **`notes:` field** on functions (and `meta.notes:` at contract level) + lists non-obvious behavior, common mistakes, or edge cases. Always read + these carefully. +- **`meta.source`** is an array of raw GitHub URLs to the Solidity source files + (implementation, interface, base — filenames are self-documenting). +- **`meta.deployments`** has per-network addresses split into `mainnet` and + `testnet` subgroups. +- **`creationBlock`**: decimal block where that **`address`** first has code; + put it on the same object as **`networkId`** / **`address`**, immediately after **`address`**. +- **`meta.deploymentCreationBlocks`**: same shape as **`meta.deployments`**, for + deployments stored as plain address strings; leaves are decimal block numbers. + +## Events section + +```yaml +events: + FlowUpdated: + indexed: # log topics (filterable) + - token: address + - sender: address + data: # log payload + - flowRate: int96 +``` + +## Errors section + +```yaml +errors: + # -- Category -- + - SIMPLE_ERROR # description + - PARAMETERIZED_ERROR: # errors with diagnostic data + inputs: + - value: uint256 +``` + +## Selector sidecar files + +Every `Foo.abi.yaml` has a companion `Foo.selectors.yaml` in the same +directory. These contain computed hex selectors (function 4-byte, error +4-byte, event 32-byte topic0) with full Solidity signatures for +verification. Generated by `scripts/selectors.mjs`. diff --git a/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md b/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md new file mode 100644 index 0000000..12b1899 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md @@ -0,0 +1,42 @@ +# Faucet flows (user-facing explanation) + +This note explains the Faucet in plain language: what it does for users, why a top-up can fail, and what limits exist. + +The on-chain Faucet contract is used to add a small amount of native gas token to a wallet so the user can pay transaction fees. +Reference implementation: [`contracts/fuseFaucet/Faucet.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/fuseFaucet/Faucet.sol). +Addresses per chain: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example `Faucet` under `production`, `production-celo`, `production-xdc`). + +## What this means for users + +- If your wallet is eligible, Faucet can send a small gas top-up. +- Eligibility usually depends on identity status and anti-abuse limits. +- This is a support mechanism for transaction fees, not a general transfer or swap service. + +## Main actions (in user language) + +- **Top up wallet** (`topWallet`) + Attempts to send gas to the target wallet after checks pass. + +- **Check eligibility first** (`canTop`) + Fast pre-check to see if top-up is currently allowed. + +- **Estimate top-up amount** (`getToppingAmount`) + Shows the amount Faucet would try to send right now. + +## Why a top-up may fail + +- You are not currently authorized by identity rules. +- Daily limit reached. +- Weekly limit reached. +- Wallet is temporarily banned. +- Wallet is too new for current policy. +- Calculated top-up is below minimum threshold. + +## Important safety note + +- The `onTokenTransfer` path includes a swap-like mechanism and is not meant as a normal user swap route. +- It does not enforce slippage protection in the same way users expect from a dedicated swap UI. + +## For developers and agents + +Use `references/guides/faucet.md` for step-by-step execution flow and deterministic preflight calls. diff --git a/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md b/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md new file mode 100644 index 0000000..261c2d7 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md @@ -0,0 +1,38 @@ +# Why Fuse to CELO staking migration uses a staged backend flow + +This explains why the migration is split into allowance detection, unstake, bridge, and destination re-stake instead of a single transaction. In this context, Fuse `GovernanceStakingV2` is the old/source staking contract and Celo **`GooddollarSavingsStream`** is the destination staking contract ([source](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol), [0x059ee811414230d1Fb157878D2b491240F4D8d3B](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B)). + +## Why this cannot be one-chain atomic + +Fuse governance staking and CELO destination savings live on different chains, so the system cannot atomically close and reopen stake in one EVM transaction. Cross-chain migration is asynchronous by design and must tolerate timing gaps between source completion and destination finalization. + +## Why user allowance is first + +The migration flow assumes a backend-operated execution wallet. If that wallet needs to pull or operate on user funds in the source path, user approval must exist first. Without allowance, all downstream steps fail, so approval state is the earliest hard gate. + +## Why unstake is separated from bridge + +The source staking position is the canonical balance record on Fuse. The migration must first materialize transferable G$ by closing or reducing stake, then bridge only the confirmed unlocked amount. Bridging before final unstake confirmation introduces mismatch risk. + +## Why bridge finalization must be explicit + +Bridge transfers are eventually consistent across chains. The destination stake step must only run after the bridged G$ is confirmed on CELO. This avoids phantom staking attempts and preserves deterministic accounting. + +## Why destination uses `stakeFor` + +Ubeswap `GooddollarSavingsStream` on Celo supports `stakeFor(amount, recipient)`, which allows the backend execution wallet to stake on behalf of the user after bridge finalization. This fits migration operations where custody is temporary during the transfer window. + +## Main operational risks + +- partial migration from source unstake or bridge limit constraints +- stuck-in-transit bridge messages delaying destination staking +- stale assumptions about contract addresses across networks +- reward expectation mismatch when moving from Fuse governance staking mechanics to CELO savings mechanics + +## Contract/source map + +- Fuse staking family (GoodProtocol): [`GovernanceStaking.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/governance/GovernanceStaking.sol) +- Fuse deployment mapping: [`deployment.json`](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`production.GovernanceStakingV2`) +- Celo savings (Ubeswap): [`GooddollarSavingsStream.sol`](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol) +- Ubeswap contracts repository: [Ubeswap/gooddollar-contracts](https://github.com/Ubeswap/gooddollar-contracts) +- Bridge normalization for LZ quotes: [`BridgeHelperLibrary.normalizeFromTokenTo18Decimals`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol) (off-chain LayerZero fee estimation must match this; `canBridge` and `bridgeToWithLz` use the raw burn amount on the source chain) diff --git a/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md b/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md new file mode 100644 index 0000000..754a3a0 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md @@ -0,0 +1,37 @@ +# GoodDAO and DAOStack surface + +GoodProtocol’s on-chain **governance shell** is largely **DAOStack-shaped**: an **Avatar** holds protocol assets and reputation context; a **Controller** registers **schemes** and routes privileged calls. GoodDocs summarizes DAO-facing roles; **Avatar**, **Controller**, and other DAO contract addresses live only in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). Implementation follows [DAOStack Arc](https://github.com/daostack/arc) patterns. + +## Core interfaces (GoodProtocol) + +[`DAOStackInterfaces.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/DAOStackInterfaces.sol) defines the pieces most integrations touch: + +### Avatar + +- **`nativeToken()`** — often the G$ token address for the deployment. +- **`nativeReputation()`** — **`GReputation`** token for voting weight (see `GReputation` in `deployment.json`). +- **`owner()`** — owner of the Avatar, typically the **`Controller`** contract. + +### Controller + +- **`avatar()`** — address of the Avatar contract. +- **`registerScheme` / `unregisterScheme` / `unregisterSelf` / `isSchemeRegistered` / `getSchemePermissions`** — scheme lifecycle and permission bitmask per scheme+avatar. +- **`genericCall(contract, data, avatar, value)`** — executes arbitrary calls **as the avatar** (used heavily by DAO-backed contracts to move tokens or call NameService). +- **`mintTokens`**, **`externalTokenTransfer`**, **`sendEther`** — treasury-style operations through the controller/avatar. + +## How GoodProtocol contracts use it + +- **`DAOUpgradeableContract`** / **`DAOContract`** descendants resolve **`dao`** (Controller) and **`avatar`** from **NameService** keys such as **`CONTROLLER`** and **`AVATAR`** (also written during **NameService.initialize**). +- **Avatar-gated writes** (for example **NameService.setAddress**, **UBISchemeV2** admin functions, **IdentityV3** after `initDAO`) require **`msg.sender == dao.avatar()`** (or equivalent role), not EOAs. +- Schemes that upgrade themselves (for example staking **`upgrade()`**) use **`dao.genericCall`** and **`unregisterSelf`** against the avatar. + +## What agents should do + +1. Treat **DAO calls** as **governance-only** unless the user explicitly controls the avatar or a registered scheme. +2. For **read-only** work, use **NameService** and per-contract **view** functions; use **Controller.isSchemeRegistered** when validating that a target contract is an approved scheme (for example staking migrations). +3. Never fabricate **Avatar** or **Controller** addresses — use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or NameService. + +## References + +- [Core contracts — DAO contracts](https://docs.gooddollar.org/for-developers/core-contracts) (narrative only; addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only) +- DAOStack Arc controller and avatar concepts in the upstream repo linked from GoodDocs. diff --git a/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md b/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md new file mode 100644 index 0000000..96d321a --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md @@ -0,0 +1,68 @@ +# How UBI is minted + +This document aligns agent explanations with [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and GoodDocs component pages; **contract addresses** come only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). + +## Monetary creation (protocol level) + +- New G$ is created in connection with reserve mechanics: purchases into the reserve and reserve-side parameters (including reserve ratio) influence how much G$ can be issued while maintaining backing (see sustainability and issuance sections in GoodDocs). +- Selling G$ back to the reserve burns supply in that model. +- G$ that the protocol creates is allocated across UBI, savings incentives, treasury, and ecosystem uses per the distribution section of GoodDocs. + +## Where G$ is actually created + +Creation happens at the G$ token `mint(...)` call site, not inside `UBIScheme.claim()`. + +Current implementation uses the Mento-core expansion path: + +- `GoodDollarExpansionController` mints and routes to distribution helper. + +**DistributionHelper recipients** decide how much eventually lands in the UBI pool. + +## Claim vs reserve minting (important distinction) + +- **Reserve path:** Buying G$ through the reserve-backed AMM (or related Mento rails) is where mint and burn tied to the reserve model most directly apply at the token level. +- **Daily UBI `claim()`:** On `UBISchemeV2`, a successful claim typically **transfers G$ from the scheme contract’s balance** to the user (`token.transfer` in `_transferTokens`). The scheme may be **refilled** from the DAO avatar via internal `_withdrawFromDao` when configured, not necessarily minting in the same transaction as `claim()`. So describe user-facing UBI as **receipt from the UBI scheme balance**; reserve **minting** is the macro story, **transfer** is the usual claim-time mechanism. + +## Mento-core expansion flow (detailed) + +This is the detailed path for modern reserve-ratio-aware expansion. + +1. A caller triggers `mintUBIFromExpansion(exchangeId)` on `GoodDollarExpansionController`. +2. Expansion is time-gated by config (`expansionFrequency`, `lastExpansion`), so it does not run every block. +3. Controller computes a reserve-ratio scalar (effectively compounding `(1 - expansionRate)` for elapsed periods). +4. Controller calls `GoodDollarExchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar)`. +5. Exchange provider updates exchange state (including reserve-ratio math) and returns `amountToMint`. +6. Controller mints G$ to `distributionHelper`. +7. Controller triggers distribution (`onDistribution`) so recipients (including UBIScheme when configured) receive allocation. + +Also in `GoodDollarExpansionController`: + +- `mintUBIFromInterest(exchangeId, reserveInterest)` +- `mintUBIFromReserveBalance(exchangeId)` + +These are additional funding paths that mint to distribution helper as part of reserve-driven policy. + +## Reserve ratio, expansion, and risk + +- Practical reserve-ratio intuition: collateral backing strength per unit of G$ supply. +- Lower reserve ratio means weaker backing and higher risk when adding new supply. +- Expansion uses reserve-ratio-aware math instead of blind fixed minting, but aggressive params can still increase sell pressure. +- Key policy levers are expansion rate and expansion frequency (plus caller cadence/automation quality). + +## What claimers experience + +- Verified users receive daily UBI from a pool split among those who claim in each period (GoodDocs). +- The user-facing transaction is a UBIScheme-style `claim` on chains where it is deployed; ABI and version follow your deployment. + +## On-chain components (typical) + +- Identity system for verification and whitelist roots. +- UBIScheme (or successor) for entitlement and claim execution. +- G$ token: value reaches users via **transfer** from scheme balance and/or broader minting economics from the reserve side depending on which action you analyze. +- DistributionHelper: bridge layer between mint source and recipient buckets (UBI, others). + +## Agent guidance + +- Use GoodDocs for macro issuance and allocation; use UBIScheme + token transfer behavior for **claim** explanations. +- Use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) for contract addresses instead of guessing. +- When users ask "why no UBI funding today", check whether mint functions were executed, then verify DistributionHelper recipient config and UBIScheme balance. diff --git a/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md b/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md new file mode 100644 index 0000000..267fa74 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md @@ -0,0 +1,58 @@ +# Inviter and invitee reward model + +This explains why invite rewards sometimes work and sometimes fail, in user-facing terms. + +Invite rewards are handled by `InvitesV2`, but eligibility depends heavily on the identity system (`Identity`, often `IdentityV4`). In practice, most confusing cases are caused by whitelist or reverification state, not by the invite contract itself. + +## How the reward model works + +There are two separate moments: + +- `join`: the invitee registers with an invite code. +- bounty payout: the contract later checks if this invitee is eligible and pays inviter and invitee when rules pass. + +So joining does not guarantee immediate payout. Payout depends on current eligibility at claim time. + +## Why whitelist status is the main gate + +For bounty eligibility, `InvitesV2` checks whitelist state through Identity. + +The important behavior is: + +- A user can still have status `1` in identity storage but fail `isWhitelisted(...)` if reverification is due. +- When reverification is due, bounty checks fail until an admin refreshes authentication(Face Verification). +- Connected-wallet setups can still fail if the specific address used in invite flow does not pass the whitelist check expected by the contract path. + +## Why reverification blocks rewards + +Reverification cadence is defined in `IdentityV4` with day-based options (`reverifyDaysOptions`) and per-user progression (`authCount`). + +When too many days pass since the last authentication for that user’s current step: + +- `shouldReverify(...)` becomes true +- `isWhitelisted(...)` becomes false for bounty gating +- `canCollectBountyFor(...)` fails until authentication is refreshed + +This is why teams may see users who were once valid but are currently not eligible for invite rewards. + +## Common reasons a bounty is not paid + +- Invitee or inviter is not currently whitelisted. +- Reverification is due for invitee or inviter. +- `minimumClaims` or `minimumDays` thresholds are not met yet. +- Bounty was already paid or was zero at join time. +- Contract is inactive, or identity-chain checks do not match the active chain. +- Invite code or join state is invalid (duplicate code, self-invite, already joined). + +## What to measure for analytics + +- Historical pass: account was authenticated at least once (`lastAuthenticated > 0`). +- Current eligibility: account is currently whitelist-valid (`isWhitelisted`/non-zero root, depending on query design). + +Do not treat these as the same metric. Historical pass explains past onboarding success; current eligibility explains current payout success. + +## Contract sources + +- Invite contract: [`InvitesV2.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/invite/InvitesV2.sol) +- Identity contract: [`IdentityV4.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/identity/IdentityV4.sol) +- Deployment addresses: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) diff --git a/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md b/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md new file mode 100644 index 0000000..e46db2a --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md @@ -0,0 +1,27 @@ +# Mento and reserve economics + +This note ties **macro G$ economics** (GoodDocs) to **Mento trading surfaces** (broker, reserve, expansion) that agents integrate on-chain. + +## Protocol-level story (GoodDocs) + +- G$ is **reserve-backed**; issuance and price discovery follow an **augmented bonding curve** (Bancor-style dynamics) described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). +- **Buying** from the reserve side increases reserve assets and can support **new G$ supply** within reserve rules; **selling back** burns G$ and returns collateral, with parameters governed by **GoodDAO**. +- Distribution of newly created G$ spans UBI, savings incentives, treasury, and ecosystem allocations (same doc), but execution timing depends on the mint path and distribution trigger used by the deployment flow. + +## User-facing buy and sell (historical vs current) + +- [Buy and Sell G$](https://docs.gooddollar.org/user-guides) describes reserve interaction, fees (including exit contribution on some paths), and older explorer flows (GoodMarketMaker / exchangeHelper on Ethereum testnets). Treat that page as **product narrative**; **live contract addresses** must come only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). + +## Mento stack (agent integration) + +On networks where GoodDollar uses Mento (see `MentoBroker` and related keys in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) for your environment): + +- **`IBroker`** (implementation in [mento-core `Broker.sol`](https://github.com/mento-org/mento-core)) is the usual **swap entrypoint**: `getAmountIn` / `getAmountOut`, `swapIn` (exact in), `swapOut` (exact out), plus **trading limits** state per exchange id and token. +- **Reserve** holds collateral; **exchange provider** contracts price trades against the reserve; **GoodDollarExpansionController** and related interfaces in GoodProtocol’s [`MentoInterfaces.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/MentoInterfaces.sol) describe expansion and avatar wiring for governance-facing changes. +- Agents should not hardcode **exchangeId** or provider addresses: read them from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or discover via `getExchangeProviders()` after confirming the broker address for the chain from that file. + +## Agent guidance + +1. Explain **macro** supply and reserve behavior with GoodDocs language. +2. Execute **swaps** with broker quotes, slippage bounds, and allowances per `references/guides/swap.md`. +3. On revert, distinguish **slippage / limit** failures (broker) from **reserve liquidity** messages (see `MentoBroker.abi.yaml` error map and mento-core source). diff --git a/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md b/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md new file mode 100644 index 0000000..b59e5c9 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md @@ -0,0 +1,69 @@ +# On- and off-ramp service via stable token swap + +Use this note for the service pattern where ramp providers do not list G$ directly. The practical path is: ramp in/out with a listed stable token (for example cUSD), then swap between stable and G$ on-chain. + +## Why this is required + +- Most on-/off-ramp providers list mainstream stable tokens, not G$. +- Service needs a bridge asset for fiat rails. +- Stable token becomes the integration point with ramp providers, while G$ remains the in-app asset. + +## On-chain source of truth + +- Solidity: [`contracts/utils/BuyGDClone.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/utils/BuyGDClone.sol) +- Components: + - `BuyGDCloneFactory` + - `BuyGDCloneV2` + - `DonateGDClone` +- Deployments: [releases/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) + +## Service architecture + +The factory deploys EIP-1167 minimal clones and wires swap infrastructure (router, oracle, quoter, optional Mento broker configuration, G$ token, stable token). + +Swap execution is **dual-path**: + +- **Uniswap-style route** (router/quoter path) +- **Mento-based route** (broker/exchange-provider path) + +For each swap request, the service compares quoted outputs and selects the route with the **larger `amountOut`** (best execution for the same input amount), then enforces `minAmount` guard on the selected path. + +Each user gets a deterministic clone address from owner-based salt: + +- `predict(owner)` for buy clone +- `predictDonation(owner, donor)` for donation clone + +This is why clone-per-user design is used: + +- predictable per-user addresses for audit and routing +- isolated execution context +- cheaper deployment than full contract instances + +## Execution surface (conceptual) + +The operational surface is intentionally small and deterministic: + +- `create(owner)` +- `createAndSwap(owner, minAmount)` +- `predict(owner)` +- `createDonation`, `createDonationAndSwap`, `predictDonation` + +This keeps on-/off-ramp architecture auditable and predictable across users. + +## Risks + +- Wrong factory or wrong chain causes permanent fund loss risk. +- Stale router/oracle/mento config can fail swap or produce bad execution. +- Missing `minAmount` protection increases slippage risk. +- Quote source mismatch or stale quotes across Uniswap/Mento can pick a suboptimal route if not refreshed just before execution. + +## Boundary note + +This file explains **why** this architecture exists for ramp services and why per-user clones matter. +For step-by-step service execution flow, use `references/guides/on-off-ramp.md`. + +## Cross-reference + +- User narrative: [Buy and Sell G$](https://docs.gooddollar.org/user-guides) +- Token integration details: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) +- Broker ABI: `references/contracts/MentoBroker.abi.yaml` diff --git a/.agents/skills/gooddollar/references/guides/bridge.md b/.agents/skills/gooddollar/references/guides/bridge.md new file mode 100644 index 0000000..71fd6e8 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/bridge.md @@ -0,0 +1,187 @@ +# Bridge guide + +Use for moving G$ across supported networks with deployment-specific bridge contracts. + +Primary local ABI reference for MessagePassingBridge flow: + +- `references/contracts/MessagePassingBridge.abi.yaml` + +## GoodDocs alignment + +- User flow and high-level behavior: [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars). +- Resolve supported bridge contract addresses per chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example `MpbBridge` under `production`, `production-celo`, `production-xdc`). Use [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars) for user-facing flow and troubleshooting, not for addresses. + +## Goal + +Bridge with deterministic pre-checks: bridge support, allowance, amount, cross-chain transport fee, and delivered G$ after destination **amount** limits and protocol fee. + +## Required inputs + +- source and destination chain metadata +- bridge contract address for source chain +- source G$ token address +- amount in source token decimals +- signer and rpc url + +## Execution flow + +1. Resolve source bridge and token addresses for the network pair. +2. Run bridge eligibility checks for sender and amount via `canBridge(from, amount)` on the **source** bridge (same contract you call for `bridgeToWithLz` / `bridgeToWithAxelar`). Outbound burn does not invoke `canBridge` inside `_bridgeTo`; destination mint still enforces **amount** limits (see **Bridge amount limit context**). +3. Read allowance and approve bridge spender when required. +4. Resolve transport mode (`LZ` or `AXELAR`) and estimate required native fee. +5. Send bridge transaction with nonzero `msg.value` and explicit transport method. +6. Return tx hash and normalized bridge parameters. + +## Bridge fee context + +Two different costs show up on `MessagePassingBridge`; do not conflate them. + +**1. Cross-chain transport fee (native gas token on the source chain)** +Paid as **`msg.value`** on the outbound call. The contract reverts with **`MISSING_FEE`** if `msg.value` is zero. On the **LayerZero** path the contract compares your `msg.value` to **`estimateSendFee`** and reverts **`LZ_FEE(required, sent)`** if it is too low. Use the same **normalized** amount for `estimateSendFee` as the contract uses internally (see the next section). On the **Axelar** path you still attach native value for Gas Service / execution; there is no single `estimateSendFee` analogue in the snippet—follow Envio/Axelar docs or simulate the exact call for production amounts. + +**2. Protocol fee on minted G$ (destination chain, basis points)** +When the message is executed on the **destination** chain, the bridge applies **`bridgeFees`** (min / max / fee bps via `setBridgeFees`) and mints the recipient **minus** that fee; the fee portion is minted to **`feeRecipient`** when it is non-zero (see `bridgeFees()`, `feeRecipient`, and `_takeFee` / `ExecutedTransfer` in `references/contracts/MessagePassingBridge.abi.yaml`). This is **not** the LayerZero relayer fee; it is a separate cut on the **token amount** delivered on arrival. + +**3. Optional OFT / LayerZero token-adapter path** +If the flow uses the GoodDollar OFT-style adapter instead of `MessagePassingBridge`, fee quoting follows **`quoteSend`** / **`MessagingFee`** on that contract; see `references/contracts/GoodDollarOFTAdapter.abi.yaml`. + +## Bridge amount limit context + +**Bridge limit** means **bridge amount limit**: policy on **how much G$** (token volume) may move—**`minAmount`**, per-transfer cap, per-account daily cap, and aggregate daily cap—plus **`onlyWhitelisted`**. It does **not** mean the cross-chain **native** transport fee (`msg.value`, **`LZ_FEE`**), and it does **not** mean the **destination mint fee** in **`bridgeFees`** (bps); those are covered under **Bridge fee context**. + +Amount caps and counters are **per bridge deployment**: read the **source** contract for outbound **amount** policy and usage meters tied to the burn, and the **destination** contract for **`_enforceLimits`** at inbound mint completion; do not assume identical **`bridgeLimits`** across chains. + +**1. Amount caps and usage meters** +**`bridgeLimits()`** exposes **`dailyLimit`**, **`txLimit`**, **`accountDailyLimit`**, **`minAmount`**, and **`onlyWhitelisted`**. Compare those caps to **`bridgeDailyLimit()`** (aggregate **`bridged24Hours`** and **`lastTransferReset`**) and **`accountsDailyLimit(account)`** (same fields per sender). Updates use **`setBridgeLimits`** (access per the ABI). Field-level notes and accessors live in `references/contracts/MessagePassingBridge.abi.yaml`. + +**2. Source preflight vs destination enforcement** +**`canBridge(from, amount)`** on the **source** is a view-only diagnostic for **that amount**: same policy family as amount limit checks, evaluated on the **raw** burn size (not the LayerZero fee normalization). Outbound **`_bridgeTo`** does **not** call **`canBridge`**; call it from the client if you want **`(false, reason)`** before signing instead of learning only from a revert after burn setup. When the message is executed on the **destination**, **`_enforceLimits`** is the hard gate for **amount** throttles and whitelist behavior at mint time. + +## Outbound pause, approved requests, and inbound source bridges + +These controls are separate from numeric **amount** caps; they still block or relax bridging and can surface as **`BRIDGE_LIMITS`** or inbound skips. + +**`pauseBridge`** sets **`isClosed`**; when closed, outbound flow reverts with **`BRIDGE_LIMITS('closed')`**. **`approvedRequests(requestId)`** on the destination lets **`_bridgeFrom`** skip standard **amount** limit enforcement for that completion when set. **`setDisabledBridges`** toggles **`disabledSourceBridges`** entries keyed by **`keccak256(abi.encode(sourceChainId, BridgeService))`**, controlling whether an inbound relay from that source is accepted before the rest of destination handling. See `references/contracts/MessagePassingBridge.abi.yaml`. + +## Axelar vs LayerZero on GoodDollar deployments + +LayerZero mappings are initialized for Ethereum, Celo, Fuse, and XDC in `initialize` / `upgrade` on `MessagePassingBridge`. The **Axelar** path is only usable where `toAxelarChainId(targetChainId)` returns a non-empty string; the on-chain pure function currently maps **1**, **5**, **42220**, and **44787** only. For **Fuse (122)** or **XDC (50)** targets, use **LZ** (`bridgeToWithLz`) unless governance ships a broader Axelar mapping. + +## LayerZero fee and `estimateSendFee` + +`bridgeToWithLz` burns the G$ **raw** amount in the source token’s `decimals()`. Inside the contract, LayerZero payload and `estimateSendFee` use a value normalized to **18 decimals** the same way as [GoodBridge `BridgeHelperLibrary.normalizeFromTokenTo18Decimals`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol): if `decimals < 18`, multiply by `10^(18 - decimals)`; if `decimals > 18`, divide by `10^(decimals - 18)`; otherwise use the raw amount. + +`canBridge(from, amount)` is evaluated on the **raw** burn amount, not the normalized value. + +Read `decimals()` from the source G$ contract when building off-chain fee quotes so you stay aligned if a deployment differs. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +function normalizedForLzFee(raw, tokenDecimals) { + if (tokenDecimals < 18) return raw * 10n ** BigInt(18 - tokenDecimals); + if (tokenDecimals > 18) return raw / 10n ** BigInt(tokenDecimals - 18); + return raw; +} + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const token = new ethers.Contract( + process.env.GOODDOLLAR_ADDRESS, + [ + "function decimals() view returns (uint8)", + "function allowance(address,address) view returns (uint256)", + "function approve(address,uint256) returns (bool)", + ], + signer, +); + +const bridge = new ethers.Contract( + process.env.BRIDGE_ADDRESS, + [ + "function canBridge(address,uint256) view returns (bool,string)", + "function toLzChainId(uint256) view returns (uint16)", + "function estimateSendFee(uint16,address,address,uint256,bool,bytes) view returns (uint256,uint256)", + "function bridgeToWithLz(address,uint256,uint256,bytes) payable", + "function bridgeToWithAxelar(address,uint256,uint256,address) payable", + ], + signer, +); + +const owner = await signer.getAddress(); +const targetChainId = Number(process.env.TARGET_CHAIN_ID); +const recipient = process.env.RECIPIENT; +const amount = ethers.parseUnits(process.env.AMOUNT, Number(process.env.DECIMALS)); +const transport = (process.env.BRIDGE_TRANSPORT || "LZ").toUpperCase(); +const tokenDecimals = await token.decimals(); +const normalizedForLzEstimate = normalizedForLzFee(amount, Number(tokenDecimals)); + +const [canBridge, reason] = await bridge.canBridge(owner, amount); +if (!canBridge) throw new Error(`Bridge blocked: ${reason}`); + +const allowance = await token.allowance(owner, process.env.BRIDGE_ADDRESS); +if (allowance < amount) { + const approveTx = await token.approve(process.env.BRIDGE_ADDRESS, amount); + await approveTx.wait(); +} + +let tx; + +if (transport === "LZ") { + const dstEid = await bridge.toLzChainId(targetChainId); + if (dstEid === 0) throw new Error("Unsupported target chain for LayerZero"); + + const adapterParams = process.env.LZ_ADAPTER_PARAMS || "0x"; + const [nativeFee] = await bridge.estimateSendFee( + dstEid, + owner, + recipient, + normalizedForLzEstimate, + false, + adapterParams, + ); + if (nativeFee <= 0n) throw new Error("Estimated LayerZero fee is zero"); + + tx = await bridge.bridgeToWithLz(recipient, targetChainId, amount, adapterParams, { + value: nativeFee, + }); +} else if (transport === "AXELAR") { + const nativeFee = ethers.parseEther(process.env.AXELAR_FEE_ETH || "0.01"); + tx = await bridge.bridgeToWithAxelar(recipient, targetChainId, amount, owner, { + value: nativeFee, + }); +} else { + throw new Error("Unsupported BRIDGE_TRANSPORT. Use LZ or AXELAR"); +} + +const receipt = await tx.wait(); +console.log( + JSON.stringify( + { + txHash: receipt.hash, + sourceBridge: process.env.BRIDGE_ADDRESS, + targetChainId, + transport, + recipient, + rawAmount: amount.toString(), + tokenDecimals: Number(tokenDecimals), + normalizedAmountForLz: normalizedForLzEstimate.toString(), + }, + null, + 2, + ), +); +``` + +## Failure handling + +- unsupported destination: return targetChainId, bridge address, and transport mode +- fee too low (`LZ_FEE` or underpriced Axelar fee): re-estimate and retry with user confirmation +- approval or balance issue: return required delta +- credited G$ on destination is reduced by **`bridgeFees`** (bps / min / max); that is independent of the source **`msg.value`** transport fee +- **`canBridge`** false on source: return the **`reason`** string from the view call +- **`BRIDGE_LIMITS(reason)`** custom error (see `references/contracts/MessagePassingBridge.abi.yaml` **errors**): **`reason`** labels the failing check (numeric **amount** limit, whitelist, **`closed`**, or other policy string from the implementation) +- source preflight passed but destination still reverts: re-read **`bridgeLimits`** and daily counters for **amount** caps and **`onlyWhitelisted`**; check **`isClosed`**, **`approvedRequests`**, and **`disabledSourceBridges`** per **Outbound pause, approved requests, and inbound source bridges**; message delivery can cross a reset boundary or policy change diff --git a/.agents/skills/gooddollar/references/guides/check-identity.md b/.agents/skills/gooddollar/references/guides/check-identity.md new file mode 100644 index 0000000..adc0165 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/check-identity.md @@ -0,0 +1,87 @@ +# Check identity guide + +Use when the user asks whether an address is eligible for UBI or how identity links wallets. + +## GoodDocs alignment + +- [Connect another wallet address to identity](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity): associated addresses resolve to a verified root in the Identity contract; `connectAccount` links wallets. +- One claim per day applies across all connected addresses for the same verified identity (see the hint on that page). + +## Goal + +Determine whitelist or authentication status with deterministic on-chain reads. + +## Metric semantics + +- **Passed whitelisting (historical):** use `lastAuthenticated(account) > 0`. +- **Still whitelisted (current):** use `getWhitelistedRoot(account) != 0x0` or `isWhitelisted(account) == true`. +- `getWhitelistedRoot(account) != 0x0` is a current-state signal, not an "ever passed" signal. + +## Required inputs + +- `nameServiceAddress` or explicit Identity address +- `account` to check +- `rpcUrl` and chain configuration + +## Execution flow + +1. Resolve `IDENTITY` from NameService when used on the deployment. +2. Read `getWhitelistedRoot(account)` or equivalent for the deployed Identity version. +3. Treat non-zero root as tied to a whitelisted identity tree when that is the protocol rule for the deployment. +4. Read `lastAuthenticated(account)` for historical pass status and `getWhitelistedRoot(account)` or `isWhitelisted(account)` for current status. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); + +const nameService = new ethers.Contract( + process.env.NAMESERVICE_ADDRESS, + ["function getAddress(string) view returns (address)"], + provider, +); + +const identityAddress = await nameService.getAddress("IDENTITY"); +const identity = new ethers.Contract( + identityAddress, + [ + "function getWhitelistedRoot(address) view returns (address)", + "function isWhitelisted(address) view returns (bool)", + "function lastAuthenticated(address) view returns (uint256)", + ], + provider, +); + +const account = process.env.ACCOUNT; +const root = await identity.getWhitelistedRoot(account); +const isWhitelisted = await identity.isWhitelisted(account); +const lastAuthenticated = await identity.lastAuthenticated(account); + +console.log( + JSON.stringify( + { + account, + identityAddress, + whitelistedRoot: root, + isWhitelisted, + lastAuthenticated: lastAuthenticated.toString(), + }, + null, + 2, + ), +); +``` + +## Return shape + +- `isWhitelisted` or equivalent boolean summary +- `whitelistedRoot` or equivalent +- `lastAuthenticated` for historical-pass classification +- optional metadata fields when available + +## Failure handling + +- NameService cannot resolve `IDENTITY`: stop and fix inputs using [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`Identity` / `NameService` for the target environment)—not GoodDocs tables. +- Read failures: return the failing call and next step. diff --git a/.agents/skills/gooddollar/references/guides/claim.md b/.agents/skills/gooddollar/references/guides/claim.md new file mode 100644 index 0000000..bd51abd --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/claim.md @@ -0,0 +1,83 @@ +# Claim guide + +Use when the user wants to claim daily UBI. Protocol context: [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and [UBIScheme (GoodDocs behavior)](https://docs.gooddollar.org/for-developers/core-contracts/ubischeme)—contract addresses only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). + +## Goal + +Execute a safe `claim()` with identity pre-checks and clear outputs. + +## GoodDocs alignment + +- UBI is distributed daily to verified users; the active pool is split among claimers in each period (see [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works)). +- UBIScheme deployments vary by chain (Fuse, Celo, XDC); resolve live contract addresses only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`UBIScheme` under `production`, `production-celo`, or `production-xdc`). Use [Core contracts / UBIScheme](https://docs.gooddollar.org/for-developers/core-contracts/ubischeme) for documented behavior, not for addresses. + +## Required inputs + +- `nameServiceAddress` or explicit UBIScheme and Identity addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) +- `rpcUrl` and chain configuration +- signer context + +## Execution flow + +1. Resolve `IDENTITY` and `UBISCHEME` from NameService when NameService is the source of truth for the deployment. +2. Confirm whitelist status for the claiming account. +3. Optionally read entitlement or claimable state before sending `claim()`. +4. Call `claim()` on the resolved UBIScheme (contract generation may differ by deployment; align ABI with your target). +5. Return tx hash and claimed amount when derivable from events or balance delta. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const nameService = new ethers.Contract( + process.env.NAMESERVICE_ADDRESS, + ["function getAddress(string) view returns (address)"], + provider, +); + +const identityAddress = await nameService.getAddress("IDENTITY"); +const ubiAddress = await nameService.getAddress("UBISCHEME"); + +const identity = new ethers.Contract( + identityAddress, + [ + "function isWhitelisted(address) view returns (bool)", + "function getWhitelistedRoot(address) view returns (address)", + ], + provider, +); + +const account = await signer.getAddress(); +const isWhitelisted = await identity.isWhitelisted(account); +if (!isWhitelisted) throw new Error("Account is not whitelisted"); + +const root = await identity.getWhitelistedRoot(account); +if (root === ethers.ZeroAddress) throw new Error("No whitelisted root"); + +const ubi = new ethers.Contract( + ubiAddress, + ["function claim()", "event UBICalculated(address,uint256,uint256,uint256)"], + signer, +); + +const tx = await ubi.claim(); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, account, root }, null, 2)); +``` + +## Pre-check failures + +- Not whitelisted: stop and point the user to identity verification flows in GoodDocs. +- Missing contract address: stop; use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only. +- Zero entitlement: communicate that nothing is claimable in the current period without guessing amounts. + +## Output contract + +- network +- resolved contract addresses +- tx hash +- claim outcome details when available diff --git a/.agents/skills/gooddollar/references/guides/faucet.md b/.agents/skills/gooddollar/references/guides/faucet.md new file mode 100644 index 0000000..d224e85 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/faucet.md @@ -0,0 +1,76 @@ +# Faucet top-up guide + +Use when the user needs native gas top-up via GoodProtocol Faucet. + +## Goal + +Run deterministic pre-checks and call `topWallet` only when eligibility and limits pass. + +## Required inputs + +- `rpcUrl`, chain configuration, signer +- Faucet address for the chain (from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) `Faucet` under the matching environment, or `NameService.getAddress` when the deployment documents the key) +- target user address + +## Execution flow + +1. Resolve Faucet contract address for the active chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). +2. Run `canTop(user)` as preflight. +3. Read `getToppingAmount(user)` and communicate expected top-up. +4. If eligible, call `topWallet(user)`. +5. Return tx hash and resulting top-up context. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const faucet = new ethers.Contract( + process.env.FAUCET_ADDRESS, + [ + "function canTop(address) view returns (bool)", + "function getToppingAmount(address) view returns (uint256)", + "function topWallet(address payable)", + ], + signer, +); + +const user = process.env.USER; +const canTop = await faucet.canTop(user); +if (!canTop) throw new Error("Faucet canTop returned false"); + +const amount = await faucet.getToppingAmount(user); +const tx = await faucet.topWallet(user); +const receipt = await tx.wait(); + +console.log( + JSON.stringify( + { + txHash: receipt.hash, + user, + toppingAmount: amount.toString(), + }, + null, + 2, + ), +); +``` + +## Common rejection reasons + +- `not authorized` +- daily or weekly cap reached +- banned address +- low effective `toTop` vs minimum threshold +- faucet inactive or wrong chain/address + +## Output contract + +- network +- faucet address +- `canTop` preflight result +- top-up amount estimate +- tx hash (when executed) diff --git a/.agents/skills/gooddollar/references/guides/gooddocs.md b/.agents/skills/gooddollar/references/guides/gooddocs.md new file mode 100644 index 0000000..5e5b4b3 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/gooddocs.md @@ -0,0 +1,37 @@ +# GoodDocs hub + +Canonical protocol documentation lives at [GoodDocs](https://docs.gooddollar.org/). + +This is a **routing guide** (quick link map), not a deep protocol analysis note. + +## Start here + +- [Welcome](https://docs.gooddollar.org/) +- [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) + +## User guides + +- [Buy and Sell G$](https://docs.gooddollar.org/user-guides) (reserve-backed buy/sell; includes historical Ethereum/Kovan explorer workflows in the doc) +- [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars) (MessagePassingBridge, fees, limits, troubleshooting) +- [Connect another wallet address to identity](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity) + +## Developers + +- [Core contracts](https://docs.gooddollar.org/for-developers/core-contracts) (module overview; **do not** read contract addresses from GoodDocs—use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only) +- [Developer guides index](https://docs.gooddollar.org/for-developers/developer-guides) +- [Integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (ERC-677, ERC-777, decimals by chain, `transferAndCall`, fees) +- [Use G$ streaming](https://docs.gooddollar.org/for-developers/developer-guides/use-gusd-streaming) (Superfluid on Celo, CFAv1Forwarder) + +## Chain IDs (bridge doc) + +| Network | Chain ID | +| -------- | -------- | +| Ethereum | 1 | +| Fuse | 122 | +| Celo | 42220 | +| XDC | 50 | + +## This repo + +- Action playbooks: `references/guides/*.md`. +- Rich ABIs: `references/contracts/*.abi.yaml`. diff --git a/.agents/skills/gooddollar/references/guides/goodsdks.md b/.agents/skills/gooddollar/references/guides/goodsdks.md new file mode 100644 index 0000000..8092e40 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/goodsdks.md @@ -0,0 +1,110 @@ +# GoodSDKs integration guide + +Use this guide when the task is SDK-first (app integration), not raw contract-first. + +## Scope + +GoodSDKs is the app integration layer for GoodDollar: + +- `@goodsdks/citizen-sdk` for identity and claim flows. +- `@goodsdks/react-hooks` for Wagmi React hooks. +- `@goodsdks/good-reserve` for reserve buy or sell flows. +- `@goodsdks/engagement-sdk` for engagement rewards flows. +- `@goodsdks/ui-components` and `@goodsdks/savings-widget` for web components. + +## Routing map + +- Check whitelist, identity root, FV link -> `@goodsdks/citizen-sdk` (`IdentitySDK`) +- Claim UBI with entitlement checks and fallback chains -> `@goodsdks/citizen-sdk` (`ClaimSDK`) +- React app with Wagmi and minimal glue code -> `@goodsdks/react-hooks` +- Buy or sell via reserve rails (Celo or XDC support rules) -> `@goodsdks/good-reserve` +- Reward app registration, claims, reward history -> `@goodsdks/engagement-sdk` +- Embeddable UI in non-React or mixed stacks -> `@goodsdks/ui-components` or `@goodsdks/savings-widget` + +## Deterministic setup + +Monorepo prerequisites: + +```bash +cd ~/Projects/GoodSDKs +corepack enable +yarn install --immutable +yarn build +``` + +Target one workspace: + +```bash +yarn workspace @goodsdks/citizen-sdk build +yarn workspace @goodsdks/react-hooks build +``` + +## Deterministic usage snippets + +Identity SDK: + +```ts +import { createPublicClient, createWalletClient, custom, http } from "viem"; +import { IdentitySDK } from "@goodsdks/citizen-sdk"; + +const publicClient = createPublicClient({ transport: http("https://forno.celo.org") }); +const walletClient = createWalletClient({ transport: custom(window.ethereum) }); + +const identitySDK = await IdentitySDK.init({ + publicClient, + walletClient, + env: "production", +}); + +const { isWhitelisted, root } = await identitySDK.getWhitelistedRoot("0xYourAccount"); +console.log({ isWhitelisted, root }); +``` + +Claim SDK: + +```ts +import { ClaimSDK, IdentitySDK } from "@goodsdks/citizen-sdk"; + +const identitySDK = await IdentitySDK.init({ publicClient, walletClient, env: "production" }); +const claimSDK = await ClaimSDK.init({ + publicClient, + walletClient, + identitySDK, + env: "production", +}); + +const entitlement = await claimSDK.checkEntitlement(); +if (entitlement.amount > 0n) { + const receipt = await claimSDK.claim(); + console.log(receipt.transactionHash); +} +``` + +React hooks: + +```tsx +import { useIdentitySDK, useClaimSDK, useGoodReserve } from "@goodsdks/react-hooks"; + +const identity = useIdentitySDK("production"); +const claim = useClaimSDK("production"); +const reserve = useGoodReserve("production"); +``` + +Reserve SDK: + +```ts +import { GoodReserveSDK } from "@goodsdks/good-reserve"; + +const sdk = new GoodReserveSDK(publicClient, walletClient, "production"); +const quote = await sdk.getBuyQuote(CUSD_ADDRESS, amountIn); +const tx = await sdk.buy(CUSD_ADDRESS, amountIn, (quote * 95n) / 100n); +console.log(tx.hash); +``` + +## Agent rules + +1. Prefer SDK methods first for app tasks. +2. Use contract-level guides only when SDK does not expose required behavior. +3. Do not invent SDK method names; align with package READMEs and exported types. +4. For chain support errors, report chain and env explicitly (do not silently fallback). +5. For UI tasks, prefer hooks or components over bespoke wallet and viem plumbing. diff --git a/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md b/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md new file mode 100644 index 0000000..e7b6f28 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md @@ -0,0 +1,110 @@ +# Envio HyperSync and HyperRPC + +Use this guide when the task is high-volume historical blockchain data fetch (events, blocks, txs), especially analytics and indexing workflows. + +## Official docs + +- HyperSync overview: [docs.envio.dev/docs/HyperSync/overview](https://docs.envio.dev/docs/HyperSync/overview) +- HyperRPC overview: [docs.envio.dev/docs/HyperRPC/overview-hyperrpc](https://docs.envio.dev/docs/HyperRPC/overview-hyperrpc) +- HyperRPC supported networks: [docs.envio.dev/docs/HyperRPC/hyperrpc-supported-networks](https://docs.envio.dev/docs/HyperRPC/hyperrpc-supported-networks) + +## What to use + +- **HyperSync**: preferred for new data pipelines and heavy historical scans. +- **HyperRPC**: read-only JSON-RPC drop-in for existing RPC code paths. + +## HyperRPC vs HyperSync (avoid mixing them up) + +- **HyperRPC** is a **hosted JSON-RPC URL** (same methods as `eth_getLogs`, `eth_blockNumber`, and so on). Any HTTP client or existing RPC stack can call it; put the API token in the URL path as Envio documents. +- **HyperSync** is a **separate high-throughput query API** used through **Envio client libraries** (for example `@envio-dev/hypersync-client` in Node). It is **not** “just another RPC endpoint” with the same ergonomics as a one-line `fetch` to `eth_getLogs` at large scale. + +## Decision rule + +1. For GoodDollar protocol history that exists on subgraphs, query the subgraph first and validate fields in `references/subgraphs/*-guide.md`. +2. If subgraph schema or freshness cannot satisfy the request, use **HyperSync** for large scans and pipelines, or **HyperRPC** when you must stay inside standard JSON-RPC. +3. For write operations (sending tx), use normal RPC providers; HyperRPC is read-only. + +## GoodDollar-relevant network coverage + +- Celo and XDC are supported on HyperRPC. +- Fuse is not currently listed; treat this as non-blocking and use existing providers for Fuse. + +## Access and auth + +- HyperRPC/HyperSync usage is account-based. +- HyperRPC requires an API key for reliable production use. +- Requests without API token are rate-limited and should be treated as non-production fallback only. +- Add API key in endpoint URL as documented by Envio. +- HyperRPC token pattern example from docs: `https://.rpc.hypersync.xyz/` + +## Agents: Envio API token when HyperSync is the best option + +After you decide **HyperSync** is the right tool for the user query (for example large historical scans or pipeline-scale log pulls where subgraphs are insufficient), check for a usable Envio credential in the execution environment (`ENVIO_API_TOKEN` for `@envio-dev/hypersync-client`, or the token Envio documents for your chosen URL pattern). + +If **no** Envio API token is available and you cannot complete the HyperSync path without it, **stop and explicitly ask the user** to provide an Envio API token (name the env var you need, typically `ENVIO_API_TOKEN`). Do not silently rely on anonymous or heavily rate-limited access as a substitute when HyperSync was already identified as the best approach. + +## Practical use in this repo + +- Keep subgraphs as first option for indexed protocol entities. +- Use HyperSync/HyperRPC when subgraph coverage is missing, stale, or insufficient for bulk historical pulls. +- When an agent chooses **HyperSync** as the best path and no Envio API token is available, follow **Agents: Envio API token when HyperSync is the best option** in this file and ask the user for `ENVIO_API_TOKEN` before proceeding. +- Keep contract addresses from [GoodProtocol/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only; use GoodDocs for product context, not for resolving contract addresses. +- For implementation details (client setup, query structure, supported methods), follow the Envio docs links above directly. + +## From block for historical fetches + +For **`eth_getLogs`**, HyperRPC, and HyperSync range queries, the lower bound is **`fromBlock`** (or the client’s equivalent). Prefer the deployment’s **`creationBlock`** from the matching row in `references/contracts/*.abi.yaml` (or **`meta.deploymentCreationBlocks`** where deployments are plain address strings) so scans do not start at genesis when you only need post-deploy history. Field placement for **`creationBlock`** is defined in `references/contracts/_rich-abi-yaml-format.md`. If you cannot determine the creation block, **`fromBlock` 0** is valid. + +## Prebuilt scripts (developers and local agents) + +These scripts avoid rediscovering HyperRPC wiring on every task. They require **Node.js 18 or newer** (global `fetch`). Paths like `scripts/...` are relative to the **GoodSkills repository root** (the directory that contains both `skills/` and `scripts/`), not relative to `skills/gooddollar/` alone. + +### Last N Identity `WhitelistedAdded` logs via HyperRPC + +- Script: `scripts/fetch-whitelist-events-hyperrpc.mjs` +- Default `EVENT_TOPIC0` matches `WhitelistedAdded(address)` on `IdentityV4`; override `EVENT_TOPIC0` for other events. +- Production Celo defaults: `CONTRACT_ADDRESS` defaults to `Identity` from `production-celo` in [GoodProtocol deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`0xC361A6E67822a0EDc17D899227dd9FC50BD62F42`). If `HYPERRPC_URL` is unset, the script builds `https://celo.rpc.hypersync.xyz/` from `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN`. +- Optional env: `HYPERRPC_URL` (overrides token-based default), `CONTRACT_ADDRESS`, `LIMIT` (default `500`), `STEP` (default `2000`), `FROM_BLOCK` (decimal; omit to read **`creationBlock`** for `CONTRACT_ADDRESS` from `ABI_PATH` or the default `skills/gooddollar/references/contracts/IdentityV4.abi.yaml`), `ABI_PATH`, `TO_BLOCK` (default `latest`). **`fromBlock`** behavior is described in **From block for historical fetches** above. + +```bash +cd /path/to/GoodSkills +export HYPERRPC_API_TOKEN='' +node scripts/fetch-whitelist-events-hyperrpc.mjs +``` + +Web-only assistants without a shell cannot run the file; they should return the same env keys and command text so the user runs it locally. + +## HyperSync client minimal path (install required) + +HyperSync uses the official client. Install and query pattern (Celo example URLs from [Envio Celo docs](https://docs.envio.dev/docs/HyperIndex/celo)): + +```bash +npm install @envio-dev/hypersync-client +export ENVIO_API_TOKEN='' +``` + +Save as a `.mjs` file (or use `"type": "module"` in a local `package.json`) and run with `node`: + +```javascript +import { HypersyncClient, presetQueryLogsOfEvent } from "@envio-dev/hypersync-client"; + +const client = new HypersyncClient({ + url: "https://celo.hypersync.xyz", + apiToken: process.env.ENVIO_API_TOKEN, +}); + +const identity = "0x..."; +const whitelistedAddedTopic0 = + "0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f"; + +const fromBlock = 17237952; +const toBlock = await client.getHeight(); + +const query = presetQueryLogsOfEvent(identity, whitelistedAddedTopic0, fromBlock, toBlock); +const res = await client.get(query); +console.log(res.data.logs.length); +``` + +The example **`fromBlock`** matches **`creationBlock`** for production Celo Identity in `skills/gooddollar/references/contracts/IdentityV4.abi.yaml`; see **From block for historical fetches** above. + +Full API and streaming patterns: [HyperSync clients](https://docs.envio.dev/docs/HyperSync/hypersync-clients) and the package README for `@envio-dev/hypersync-client`. diff --git a/.agents/skills/gooddollar/references/guides/invite-bounties.md b/.agents/skills/gooddollar/references/guides/invite-bounties.md new file mode 100644 index 0000000..d60aafd --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/invite-bounties.md @@ -0,0 +1,115 @@ +# Invite bounties guide + +Use when the task is to verify or execute inviter-invitee bounty payout flow and explain why payout is blocked. + +## Goal + +Check eligibility deterministically, execute payout only when eligible, and return exact failure reason when not eligible. + +## Required inputs + +- target chain +- `InvitesV2` address +- `Identity` address +- optional `UBISchemeV2` address when claims threshold is active +- invitee address +- inviter address +- rpc url and signer + +## Execution flow + +1. Resolve contract addresses from `deployment.json`. +2. Read invitee state from `users(invitee)` and global thresholds (`minimumClaims`, `minimumDays`, `active`). +3. Check current eligibility with `canCollectBountyFor(invitee)`. +4. If not eligible, read identity whitelist for invitee and inviter and return concrete blocker. +5. If eligible, execute `bountyFor(invitee)` or `collectBounties()` and return tx hash plus payout values from events. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const invites = new ethers.Contract( + process.env.INVITES_ADDRESS, + [ + "function canCollectBountyFor(address) view returns (bool)", + "function bountyFor(address)", + "function users(address) view returns (bytes32,address,uint40,uint24,bool,uint256)", + "function minimumClaims() view returns (uint256)", + "function minimumDays() view returns (uint256)", + "function active() view returns (bool)", + "event InviterBounty(address indexed inviter,address indexed invitee,uint256 bountyPaid,uint256 inviterLevel,bool earnedLevel)" + ], + signer, +); + +const identity = new ethers.Contract( + process.env.IDENTITY_ADDRESS, + [ + "function isWhitelisted(address) view returns (bool)" + ], + provider, +); + +const invitee = process.env.INVITEE_ADDRESS; +const inviter = process.env.INVITER_ADDRESS; + +const [isActive, eligible, inviteeWhitelisted, inviterWhitelisted, minClaims, minDays] = await Promise.all([ + invites.active(), + invites.canCollectBountyFor(invitee), + identity.isWhitelisted(invitee), + identity.isWhitelisted(inviter), + invites.minimumClaims(), + invites.minimumDays(), +]); + +if (!isActive) throw new Error("Invites contract is inactive"); +if (!eligible) { + throw new Error( + `Not eligible. inviteeWhitelisted=${inviteeWhitelisted} inviterWhitelisted=${inviterWhitelisted} minimumClaims=${minClaims} minimumDays=${minDays}`, + ); +} + +const tx = await invites.bountyFor(invitee); +const receipt = await tx.wait(); +const bountyEvent = receipt.logs + .map((log) => { + try { + return invites.interface.parseLog(log); + } catch { + return null; + } + }) + .find((e) => e && e.name === "InviterBounty"); + +console.log( + JSON.stringify( + { + txHash: receipt.hash, + invitee, + inviter, + bountyPaid: bountyEvent?.args?.bountyPaid?.toString() ?? null, + }, + null, + 2, + ), +); +``` + +## Failure handling + +- invitee or inviter is not currently whitelisted +- reverification is due and whitelist check fails until re-authentication +- minimum claims or minimum days is not met +- bounty already paid or bounty-at-join is zero +- contract inactive or wrong deployment addresses + +## Output contract + +- network and addresses used +- eligibility status and blockers +- tx hash when sent +- payout values when available from logs diff --git a/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md b/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md new file mode 100644 index 0000000..4ad9fd2 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md @@ -0,0 +1,116 @@ +# Fuse to CELO staking migration guide + +Use when the user wants to migrate an existing Fuse governance stake into a CELO destination savings flow. In this flow, Fuse `GovernanceStakingV2` is the old staking contract (source) and **`GooddollarSavingsStream`** on Celo is the destination savings contract ([`GooddollarSavingsStream.sol`](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol), [CeloScan](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B)). + +## Goal + +Close a user stake on Fuse, bridge the resulting G$ to CELO, and stake on CELO for that user in a controlled backend flow. + +## Required inputs + +- user address on Fuse and corresponding destination address on CELO +- Fuse `GovernanceStakingV2` address (`production.GovernanceStakingV2` in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json)) +- Fuse G$ token address and bridge contract address (from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json): `GoodDollar`, `MpbBridge` under `production`) +- CELO G$ token address (from `production-celo` in the same file) and destination savings contract address +- backend signer or service wallet with required execution permissions +- chain RPC URLs for Fuse and CELO + +## Address resolution quick table + +| Purpose | Network | Source key/path | Value | +|---|---|---|---| +| Governance staking (source close) | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GovernanceStakingV2` | `0xB7C3e738224625289C573c54d402E9Be46205546` | +| Governance staking (previous) | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GovernanceStaking` | `0xFAF457Fb4A978Be059506F6CD41f9B30fCa753b0` | +| Fuse G$ token | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GoodDollar` | `0x495d133B938596C9984d462F007B676bDc57eCEC` | +| Fuse bridge | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.MpbBridge` | `0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5` | +| Destination savings | CELO (`networkId: 42220`) | [CeloScan](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B) / `GooddollarSavingsStream` | `0x059ee811414230d1Fb157878D2b491240F4D8d3B` (`process.env.CELO_SAVINGS`) | + +Canonical sources: + +- [GoodProtocol deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) +- [Fuse explorer contract (GovernanceStakingV2 address)](https://explorer.fuse.io/address/0xB7C3e738224625289C573c54d402E9Be46205546?tab=contract) +- [Ubeswap gooddollar-contracts](https://github.com/Ubeswap/gooddollar-contracts) +- [GoodBridge bridge helper normalization](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol) + +## Token decimals and `MessagePassingBridge` LZ fees + +Production deployments checked on-chain: Fuse `GoodDollar` uses **2** decimals and Celo `GoodDollar` uses **18**. Fuse `GovernanceStakingV2` (sG$) uses **2** decimals. Resolve `decimals()` from each live token in your runner so you stay correct if deployments change. + +On the Fuse `MpbBridge` (`MessagePassingBridge`), `canBridge(from, amount)` and `bridgeToWithLz(..., amount, ...)` use the **raw G$ burn amount** in source token decimals. Off-chain **`estimateSendFee`’s `_normalizedAmount` argument** must match what the contract builds internally: **`normalizeFromTokenTo18Decimals(amount, IERC20(nativeToken()).decimals())`** ([`BridgeHelperLibrary`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol)). Format displayed amounts per chain (`formatUnits`/UI) using each chain’s G$ decimals. + +## Execution flow + +1. Confirm allowance for whichever asset your flow spends first (for example sG$ allowance to the backend on Fuse `GovernanceStakingV2`, or Fuse G$ allowance if you pull G$ directly), before any transfers or bridges. +2. Verify current stake state on Fuse before closing: + - stake token balance + - withdrawable stake amount + - pending rewards if any +3. Execute Fuse unstake or close flow on governance staking (`withdrawStake` or equivalent full-close path). +4. Compute net G$ available for migration after unstake completion and any reward claim behavior. +5. Bridge G$ from Fuse to CELO using the configured bridge path and track the transfer id or tx hash pair. +6. Wait for destination finalization on CELO and verify credited G$ balance at the backend execution wallet. +7. Approve destination savings contract to spend migrated G$ amount. +8. Stake for the user on CELO with `stakeFor(amount, recipient)` on `GooddollarSavingsStream` (G$ native Super Token; approve the savings contract, not only ERC20 GoodDollar from `deployment.json` if your wallet holds the Super Token). +9. Return a migration result with both chain tx hashes and final CELO staked amount. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const fuse = new ethers.JsonRpcProvider(process.env.FUSE_RPC_URL); +const celo = new ethers.JsonRpcProvider(process.env.CELO_RPC_URL); +const signerFuse = new ethers.Wallet(process.env.BACKEND_PK, fuse); +const signerCelo = new ethers.Wallet(process.env.BACKEND_PK, celo); + +const user = process.env.USER_ADDRESS; +const migrateAmount = BigInt(process.env.MIGRATE_AMOUNT); + +const celoGd = new ethers.Contract( + process.env.CELO_GD_TOKEN, + [ + "function approve(address spender,uint256 amount) returns (bool)", + "function balanceOf(address) view returns (uint256)", + ], + signerCelo, +); + +const savings = new ethers.Contract( + process.env.CELO_SAVINGS, + ["function stakeFor(uint256 amount,address recipient)"], + signerCelo, +); + +const approveTx = await celoGd.approve(process.env.CELO_SAVINGS, migrateAmount); +await approveTx.wait(); + +const stakeTx = await savings.stakeFor(migrateAmount, user); +const receipt = await stakeTx.wait(); + +console.log( + JSON.stringify( + { + user, + celoStakeTx: receipt.hash, + migratedAmount: migrateAmount.toString(), + }, + null, + 2, + ), +); +``` + +## Pre-check failures + +- User allowance missing on Fuse: stop and request allowance tx from user. +- Stake close fails on Fuse: stop and return exact revert reason before bridge. +- Bridge transfer not finalized on CELO: do not call `stakeFor` until destination balance is confirmed. +- CELO savings approval missing or too low: re-approve exact amount before staking. + +## Output contract + +- user address +- Fuse unstake tx hash +- bridge tx hash or transfer identifier +- CELO stake tx hash +- final staked amount on CELO diff --git a/.agents/skills/gooddollar/references/guides/on-off-ramp.md b/.agents/skills/gooddollar/references/guides/on-off-ramp.md new file mode 100644 index 0000000..d212705 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/on-off-ramp.md @@ -0,0 +1,77 @@ +# On- and off-ramp service guide + +Use when implementing service flow where fiat ramps support a listed stable token (for example cUSD), and the app needs stable <-> G$ swap on-chain. + +## Goal + +Operate deterministic clone-based swap routing with explicit chain/factory verification and slippage guard. + +## Required inputs + +- target chain and factory address +- owner address used for clone derivation +- stable token and G$ token addresses +- direction: on-ramp or off-ramp +- `minAmount` guard +- signer and rpc url + +## Execution flow + +1. Resolve factory for target chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`BuyGDFactory` / `BuyGDFactoryV2` under `production-celo`, or the key your deployment uses). +2. Compute expected clone via `predict(owner)`. +3. If clone not yet deployed for flow, call `create(owner)` or `createAndSwap(owner, minAmount)`. +4. Execute stable -> G$ (on-ramp) or G$ -> stable (off-ramp) through clone path. +5. Return chain id, factory, predicted clone, effective clone, tx hashes. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const factory = new ethers.Contract( + process.env.BUY_GD_CLONE_FACTORY, + [ + "function predict(address) view returns (address)", + "function create(address) returns (address)", + "function createAndSwap(address,uint256) returns (address)", + ], + signer, +); + +const owner = process.env.OWNER; +const minAmount = ethers.parseUnits(process.env.MIN_AMOUNT, Number(process.env.DECIMALS_OUT)); +const predicted = await factory.predict(owner); + +const tx = await factory.createAndSwap(owner, minAmount); +const receipt = await tx.wait(); + +console.log( + JSON.stringify( + { + txHash: receipt.hash, + owner, + predictedClone: predicted, + chainId: (await provider.getNetwork()).chainId.toString(), + }, + null, + 2, + ), +); +``` + +## Failure handling + +- predicted clone mismatch with trusted expectation +- wrong chain or wrong factory address +- swap output below `minAmount` +- stale router/oracle or exchange configuration + +## Output contract + +- network and chain id +- factory address +- predicted and actual clone addresses +- tx hashes diff --git a/.agents/skills/gooddollar/references/guides/save.md b/.agents/skills/gooddollar/references/guides/save.md new file mode 100644 index 0000000..d0e716c --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/save.md @@ -0,0 +1,93 @@ +# Save and stake guide + +Use when the user wants to stake G$, withdraw rewards, or exit stake. Staking economics sit alongside other protocol allocations described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). + +## GoodDocs alignment + +- Token integration and fee awareness: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (`_processFees`, decimals per chain). +- Contract addresses: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example staking and G$ token keys under `production` / `production-celo`). GoodDocs covers behavior and decimals patterns, not canonical deployment addresses. + +## Goal + +Run staking actions with balance and allowance safety checks. + +## Required inputs + +- `nameServiceAddress` or explicit staking and token addresses +- `amount` or `shares` depending on the action +- `rpcUrl`, chain configuration, signer + +## Execution flow + +1. Resolve staking and G$ token addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or, when the deployment documents the key, from `NameService.getAddress` on chain. +2. Read token balance and allowance. +3. Approve the staking contract when `stake` uses `transferFrom`. +4. Execute `stake`, `withdrawRewards`, or `withdrawStake` as requested. +5. Return tx hash and key resulting balances or events. + +## Deterministic snippets + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const token = new ethers.Contract( + process.env.GOODDOLLAR_ADDRESS, + [ + "function balanceOf(address) view returns (uint256)", + "function allowance(address,address) view returns (uint256)", + "function approve(address,uint256) returns (bool)", + ], + signer, +); + +const staking = new ethers.Contract( + process.env.STAKING_ADDRESS, + [ + "function stake(uint256)", + "function withdrawRewards()", + "function withdrawStake(uint256)", + ], + signer, +); +``` + +Stake: + +```js +const amount = ethers.parseUnits(process.env.AMOUNT, Number(process.env.DECIMALS)); +const owner = await signer.getAddress(); +const allowance = await token.allowance(owner, process.env.STAKING_ADDRESS); +if (allowance < amount) { + const approveTx = await token.approve(process.env.STAKING_ADDRESS, amount); + await approveTx.wait(); +} +const tx = await staking.stake(amount); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, action: "stake" }, null, 2)); +``` + +Withdraw rewards: + +```js +const tx = await staking.withdrawRewards(); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, action: "withdrawRewards" }, null, 2)); +``` + +Withdraw stake: + +```js +const shares = ethers.parseUnits(process.env.SHARES, Number(process.env.DECIMALS)); +const tx = await staking.withdrawStake(shares); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, action: "withdrawStake" }, null, 2)); +``` + +## Failure handling + +- Insufficient balance: report shortfall. +- Approval issues: report token, spender, and required allowance. +- Reverts: return attempted function and parameters without guessing custom errors. diff --git a/.agents/skills/gooddollar/references/guides/stream.md b/.agents/skills/gooddollar/references/guides/stream.md new file mode 100644 index 0000000..6cbf7a4 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/stream.md @@ -0,0 +1,135 @@ +# Stream guide + +Primary references for stream execution are local ABI assets in this repo: + +- `references/contracts/CFAv1Forwarder.abi.yaml` +- `references/contracts/ConstantFlowAgreementV1.abi.yaml` +- `references/contracts/Superfluid.abi.yaml` +- `references/contracts/SuperToken.abi.yaml` + +## Goal + +Create, update, or delete Superfluid constant flows using deterministic contract calls and local ABI references. + +## Protocol facts used by this guide + +- Forwarder path uses `CFAv1Forwarder.createFlow`, `updateFlow`, `deleteFlow`. +- Host path uses `Superfluid.callAgreement` with CFA calldata for `createFlow`, `updateFlow`, `deleteFlow`. +- Stream token is a SuperToken; flow rates are `int96` in token-wei per second. +- `getBufferAmountByFlowrate(token, flowRate)` is the canonical pre-check for required buffer. + +## Two implementation styles in this repo + +1. **Forwarder (matches GoodDocs):** call CFAv1Forwarder with token, sender, receiver, flowRate, userData. +2. **Host callAgreement:** encode CFA `createFlow` / `updateFlow` / `deleteFlow` and call `Superfluid.callAgreement`. + +## Minimal method map + +- Forwarder: + - `createFlow(address token, address receiver, int96 flowrate, bytes userData)` + - `updateFlow(address token, address receiver, int96 flowrate, bytes userData)` + - `deleteFlow(address token, address sender, address receiver, bytes userData)` + - `getBufferAmountByFlowrate(address token, int96 flowrate)` +- Host: + - `callAgreement(address agreementClass, bytes callData, bytes userData)` +- CFA: + - `createFlow(address token, address receiver, int96 flowRate, bytes ctx)` + - `updateFlow(address token, address receiver, int96 flowRate, bytes ctx)` + - `deleteFlow(address token, address sender, address receiver, bytes ctx)` + +## Required inputs + +- G$ Super Token address for the environment +- CFA forwarder address, or Superfluid host address plus CFA agreement address +- `action`: create, update, delete +- `receiver`, `flowRate` where applicable +- `rpcUrl`, chain configuration, signer + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const forwarder = new ethers.Contract( + process.env.CFA_FORWARDER, + [ + "function createFlow(address,address,int96,bytes)", + "function updateFlow(address,address,int96,bytes)", + "function deleteFlow(address,address,address,bytes)", + ], + signer, +); + +const token = process.env.SUPER_TOKEN; +const sender = await signer.getAddress(); +const receiver = process.env.RECEIVER; +const flowRate = BigInt(process.env.FLOW_RATE); + +if (process.env.ACTION === "create") { + const tx = await forwarder.createFlow(token, receiver, flowRate, "0x"); + const receipt = await tx.wait(); + console.log(JSON.stringify({ txHash: receipt.hash, action: "create" }, null, 2)); +} + +if (process.env.ACTION === "update") { + const tx = await forwarder.updateFlow(token, receiver, flowRate, "0x"); + const receipt = await tx.wait(); + console.log(JSON.stringify({ txHash: receipt.hash, action: "update" }, null, 2)); +} + +if (process.env.ACTION === "delete") { + const tx = await forwarder.deleteFlow(token, sender, receiver, "0x"); + const receipt = await tx.wait(); + console.log(JSON.stringify({ txHash: receipt.hash, action: "delete" }, null, 2)); +} +``` + +Host callAgreement example: + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const cfa = new ethers.Interface([ + "function createFlow(address,address,int96,bytes)", + "function updateFlow(address,address,int96,bytes)", + "function deleteFlow(address,address,address,bytes)", +]); + +const host = new ethers.Contract( + process.env.SUPERFLUID_HOST, + ["function callAgreement(address,bytes,bytes) returns (bytes)"], + signer, +); + +const token = process.env.SUPER_TOKEN; +const sender = await signer.getAddress(); +const receiver = process.env.RECEIVER; +const flowRate = BigInt(process.env.FLOW_RATE); + +let callData = "0x"; +if (process.env.ACTION === "create") { + callData = cfa.encodeFunctionData("createFlow", [token, receiver, flowRate, "0x"]); +} +if (process.env.ACTION === "update") { + callData = cfa.encodeFunctionData("updateFlow", [token, receiver, flowRate, "0x"]); +} +if (process.env.ACTION === "delete") { + callData = cfa.encodeFunctionData("deleteFlow", [token, sender, receiver, "0x"]); +} + +const tx = await host.callAgreement(process.env.CFA_ADDRESS, callData, "0x"); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, action: process.env.ACTION }, null, 2)); +``` + +## Failure handling + +- Wrong network or missing addresses: stop and return missing host or forwarder or token addresses. +- Insufficient buffer: use `getBufferAmountByFlowrate` and reduce flow rate or top up balance. +- Revert on create or update: verify token is a SuperToken and flowRate is positive. diff --git a/.agents/skills/gooddollar/references/guides/swap.md b/.agents/skills/gooddollar/references/guides/swap.md new file mode 100644 index 0000000..3db8c2d --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/swap.md @@ -0,0 +1,83 @@ +# Swap guide + +Use for buying or selling G$ through Mento-connected contracts on networks where they appear in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `MentoBroker`, `MentoReserve`, `MentoExchangeProvider`, `MentoExpansionController` keys under `production-celo` or `production-xdc`). GoodDocs describes Mento product behavior, not deployment addresses. + +## GoodDocs alignment + +- Reserve and buy or sell mechanics at the protocol level: [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and [Buy and Sell G$ user guide](https://docs.gooddollar.org/user-guides) (includes reserve AMM narrative; older explorer step-by-step for Ethereum testnets remains in that page for reference). +- Integration patterns and decimals: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token). + +## Goal + +Execute bounded swaps using broker quotes and correct allowances. + +## Required inputs + +- `direction` as buy or sell +- broker and exchange identifiers for the deployment +- amounts in correct token decimals for the chain +- `rpcUrl`, chain configuration, signer + +## Execution flow + +1. Confirm Mento Broker (and related) addresses for the chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only. +2. Fetch quote (`getAmountOut` or `getAmountIn` depending on direction and ABI). +3. Apply slippage bounds. +4. Approve the spent token for the broker when required. +5. Call `swapIn` or `swapOut` per your integration. +6. Return tx hash and effective amounts. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const broker = new ethers.Contract( + process.env.BROKER_ADDRESS, + [ + "function getAmountOut(address,address,uint256) view returns (uint256)", + "function swapIn(address,address,uint256,uint256) returns (uint256)", + ], + signer, +); + +const tokenIn = new ethers.Contract( + process.env.TOKEN_IN, + [ + "function allowance(address,address) view returns (uint256)", + "function approve(address,uint256) returns (bool)", + ], + signer, +); + +const amountIn = ethers.parseUnits(process.env.AMOUNT_IN, Number(process.env.DECIMALS_IN)); +const quotedOut = await broker.getAmountOut(process.env.TOKEN_IN, process.env.TOKEN_OUT, amountIn); +const slippageBps = BigInt(process.env.SLIPPAGE_BPS); +const minOut = quotedOut * (10000n - slippageBps) / 10000n; + +const owner = await signer.getAddress(); +const allowance = await tokenIn.allowance(owner, process.env.BROKER_ADDRESS); +if (allowance < amountIn) { + const approveTx = await tokenIn.approve(process.env.BROKER_ADDRESS, amountIn); + await approveTx.wait(); +} + +const tx = await broker.swapIn(process.env.TOKEN_IN, process.env.TOKEN_OUT, amountIn, minOut); +const receipt = await tx.wait(); +console.log( + JSON.stringify( + { txHash: receipt.hash, amountIn: amountIn.toString(), minOut: minOut.toString() }, + null, + 2, + ), +); +``` + +## Failure handling + +- No deployment on chain: direct the user to an environment that defines the needed keys in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `production-celo` with `MentoBroker`). +- Stale quote or tight slippage: refresh quote or relax bounds with user consent. +- Allowance or balance shortfall: report exact delta. diff --git a/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md b/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md new file mode 100644 index 0000000..36af639 --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md @@ -0,0 +1,35 @@ +# Subgraph query discipline (GoodDollar) + +## Subgraph vs RPC + +- **Subgraph:** historical events, lists, aggregates, time ranges, analytics. Data lags chain head. +- **RPC / SDK:** current balances, live `claim` eligibility, exact view calls. Prefer for user-facing “what is true right now”. + +## Generic GraphQL mechanics + +Graph-node generates `entity`, `entities`, `Entity_filter`, `Entity_orderBy`, pagination, and `_meta` from each deployment’s `schema.graphql`. For scalar rules, filters, `_meta`, and common pitfalls, see [The Graph — Querying a subgraph](https://thegraph.com/docs/en/querying/graphql-api/). + +## GoodDollar-specific + +- **Addresses in `where` clauses:** normalize to **lowercase** hex strings; subgraphs store addresses lowercased. +- **BigInt fields:** query as **string** literals in GraphQL JSON (e.g. `"1000000000000000000"`). +- **Schema truth:** entity names differ per deployment. Run introspection or read the deployment’s `schema.graphql` under the relevant package in [GoodDollar/GoodSubGraphs](https://github.com/GoodDollar/GoodSubGraphs) before assuming field names. + +## Meta block + +Use `_meta { block { number } }` to detect how far behind indexing is when debugging stale data. + +```graphql +{ + _meta { + block { + number + } + hasIndexingErrors + } +} +``` + +## When subgraphs are not enough + +If the subgraph cannot answer the question (missing entities or fields, or stale indexing per `_meta`), switch to the decision rules in `references/guides/hypersync-hyperrpc.md`. When **HyperSync** is the best fit and no Envio API token is available in the environment, **ask the user directly** for `ENVIO_API_TOKEN` (or the token your HyperSync client expects) before running large scans; do not silently use anonymous quota as a stand-in. diff --git a/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md b/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md new file mode 100644 index 0000000..a266cea --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md @@ -0,0 +1,54 @@ +# GoodCollective — Subgraph Usage Guide + +Companion to `goodcollective.graphql`. + +## Endpoint + +- Explorer: [GoodCollective](https://thegraph.com/explorer/subgraphs/3LbJh9DXhJVvuVDdm5i6StNboJmL9oMNNkBaKyzc4Y8Y?view=Query&chain=arbitrum-one) +- Gateway form: `https://gateway.thegraph.com/api/subgraphs/id/3LbJh9DXhJVvuVDdm5i6StNboJmL9oMNNkBaKyzc4Y8Y` + +--- + +## Terminology: “claim” here is not daily UBI + +In this subgraph, **Claim** and **ClaimEvent** refer to **GoodCollective reward or pool claim flows**, not the protocol’s **daily UBI claim** from `UBIScheme` / `UBISchemeV2`. + +When a user says **“claim”** in normal GoodDollar product language, they almost always mean **claim daily UBI**. For that, use the GoodDollar Celo subgraph (`walletStats` / claim-related aggregates) and on-chain `claim` per `references/guides/claim.md` — do not answer “last N UBI claims” from GoodCollective **Claim** alone. + +--- + +## Entity Overview + +### Core collective graph + +**Collective** — pool identity, limits/settings links, totals, and claim/payment counters. +**Donor** / **Steward** — participant-level donation/support state. +**DonorCollective** / **StewardCollective** — join entities tying participants to a collective. + +### Claim and support flow + +**Claim** and **ClaimEvent** — GoodCollective **reward** claim lifecycle and per-claim reward events (not daily UBI from UBIScheme). +**SupportEvent** — support/donation change events across donor/collective links. + +### Metadata and policy entities + +**IpfsCollective** — IPFS metadata projection. +**PoolSettings**, **UBILimits**, **SafetyLimits** — pool policy and operational bounds. +**ProvableNFT** — NFT linkage used in claim/reward flows. + +--- + +## Typical Questions This Subgraph Answers + +- Which donors/stewards are attached to a collective? +- How much was donated/rewarded per collective and per participant? +- Which claim events occurred and what reward quantities were emitted? +- What limits and settings govern a specific collective pool? + +--- + +## Query Discipline + +- Use authenticated gateway access for programmatic queries. +- Lowercase address-like identifiers where applicable. +- Validate `_meta` before operational dashboards or reporting exports. diff --git a/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql b/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql new file mode 100644 index 0000000..e503db9 --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql @@ -0,0 +1,200 @@ +scalar BigDecimal + +scalar BigInt + +scalar Boolean + +scalar Bytes + +type Claim { + id: String! + collective: Collective! + txHash: String! + networkFee: BigInt! + totalRewards: BigInt! + events: [ClaimEvent!]! + timestamp: Int! +} + +type ClaimEvent { + id: String! + eventType: Int! + timestamp: Int! + quantity: BigInt! + rewardPerContributor: BigInt! + contributors: [Steward!]! + nft: ProvableNFT + claim: Claim! +} + +type Collective { + id: String! + pooltype: PoolType! + ipfs: IpfsCollective + settings: PoolSettings! + ubiLimits: UBILimits + limits: SafetyLimits + donors: [DonorCollective!] + stewards: [StewardCollective!] + projectId: String! + isVerified: Boolean! + poolFactory: String! + timestamp: Int! + paymentsMade: Int! + totalDonations: BigInt! + totalRewards: BigInt! + claims: [Claim!]! +} + +type Donor { + id: String! + timestamp: Int! + totalDonated: BigInt! + collectives: [DonorCollective!]! +} + +""" +Represents the relationship between a Donor and a Collective + +""" + +type DonorCollective { + id: String! + donor: Donor! + collective: Collective! + contribution: BigInt! + flowRate: BigInt! + timestamp: Int! + events: [SupportEvent!]! +} + +scalar Float + +scalar ID + +""" +4 bytes signed integer + +""" + +scalar Int + +""" +8 bytes signed integer + +""" + +scalar Int8 + +type IpfsCollective { + id: String! + name: String! + description: String! + rewardDescription: String + goodidDescription: String + email: String + website: String + twitter: String + instagram: String + threads: String + infoLabel: String + headerImage: String! + logo: String! + images: [String!] +} + +type PoolSettings { + id: String! + nftType: BigInt! + manager: Bytes! + membersValidator: Bytes! + uniquenessValidator: Bytes! + rewardToken: Bytes! +} + +enum PoolType { + DirectPayments + UBI +} + +type ProvableNFT { + id: String! + owner: String! + hash: String! + stewards: [Steward!]! + collective: Collective +} + +type SafetyLimits { + id: String! + maxTotalPerMonth: BigInt! + maxMemberPerMonth: BigInt! + maxMemberPerDay: BigInt! +} + +type Steward { + """ + { user address} + + """ + id: String! + """ + Number of actions performed + + """ + actions: Int! + totalEarned: BigInt! + totalUBIEarned: BigInt! + """ + NFT's minted to steward + + """ + nfts: [ProvableNFT!]! + """ + Collectives the steward is apart of + + """ + collectives: [StewardCollective!]! +} + +""" +Represents the relationship between a Steward and a Collective + +""" + +type StewardCollective { + id: String! + steward: Steward! + collective: Collective! + actions: Int! + totalEarned: BigInt! +} + +scalar String + +type SupportEvent { + id: String! + networkFee: BigInt! + donor: Donor! + collective: Collective! + donorCollective: DonorCollective! + contribution: BigInt! + previousContribution: BigInt! + isFlowUpdate: Boolean! + flowRate: BigInt! + previousFlowRate: BigInt! + timestamp: Int! +} + +scalar Timestamp + +type UBILimits { + id: String! + cycleLengthDays: BigInt! + claimPeriodDays: BigInt! + minActiveUsers: BigInt! + claimForEnabled: Boolean! + maxClaimAmount: BigInt! + maxClaimers: BigInt! + onlyMembers: Boolean! +} diff --git a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md new file mode 100644 index 0000000..347951f --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md @@ -0,0 +1,104 @@ +# GoodDollar Celo — Subgraph Usage Guide + +Companion to `gooddollar-celo.graphql`. + +## Endpoint + +- Explorer: [GoodDollarCelo](https://thegraph.com/explorer/subgraphs/F7314rxGdcpKPC1nN5KCoFW84EGRoUyzseY2sAT9PEkw?view=Query&chain=arbitrum-one) +- Gateway form: `https://gateway.thegraph.com/api/subgraphs/id/F7314rxGdcpKPC1nN5KCoFW84EGRoUyzseY2sAT9PEkw` + +--- + +## Entity Overview + +### UBI and usage statistics + +**DailyUBI** — day-level UBI pool/quota activity and cycle fields. +**WalletStat** — wallet behavior aggregates: tx counts/values, claim stats, active/whitelist indicators. +**TransactionStat** — day-level transaction totals and circulation view. +**GlobalStatistics** — global claim and distribution rollups. + +### Additional UBI history entities + +**UBICollected** — collected UBI/community-pool values by block event. +**UBIHistory** — timeline totals for daily UBI/community-pool. + +--- + +## Field Availability Reference (use before drafting queries) + +Use this section to validate what the subgraph already provides before switching data sources. + +### DailyUBI + +- `id` — day index key (`unix / 86400`) +- `pool` — UBI cycle pool for that day +- `quota` — daily UBI amount per eligible claimer +- `activeUsers` — active users count in scheme context +- `totalUBIDistributed` — amount actually claimed/distributed that day +- `totalClaims` — claim tx count for that day +- `newClaimers` — newly whitelisted users for that day +- `timestamp` — last update timestamp for the record +- `ubiSchemeAddress` — UBIScheme address used for the record +- `balance` — G$ balance held by UBIScheme +- `cycleLength` — current cycle length +- `dayInCycle` — current day position inside cycle + +### WalletStat + +- `id` — wallet address +- `dateAppeared` — first indexed wallet activity timestamp +- `balance` — running token balance from transfers +- `inTransactionsCount`, `inTransactionsValue` — incoming tx count and value +- `outTransactionsCount`, `outTransactionsValue` — outgoing tx count and value +- `totalTransactionsCount`, `totalTransactionsValue` — total tx count and value +- `inTransactionsCountClean`, `inTransactionsValueClean` — incoming metrics excluding contract-address flows +- `outTransactionsCountClean`, `outTransactionsValueClean` — outgoing metrics excluding contract-address flows +- `totalTransactionsCountClean`, `totalTransactionsValueClean` — total clean traffic metrics +- `lastClaimed` — timestamp of latest UBI claim +- `totalClaimedCount`, `totalClaimedValue` — total claims and cumulative claimed value +- `claimStreak`, `longestClaimStreak` — current and best historical streaks +- `isWhitelisted` — current whitelist status +- `isActiveUser` — current active-user status +- `dateJoined` — first-whitelist timestamp +- `lastTransactionFrom`, `lastTransactionTo` — latest outgoing and incoming tx timestamps + +### TransactionStat + +- `id` — bucket key (day key or `"aggregated"`) +- `dayStartBlockNumber` — first block in bucket +- `transactionsCount`, `transactionsValue` — all transfer tx count and value +- `transactionsCountClean`, `transactionsValueClean` — transfer metrics excluding contract-address flows +- `totalInCirculation` — inferred circulating supply from mint or burn behavior + +### GlobalStatistics + +- `id` — fixed key (`"statistics"`) +- `TransactionStat` — link to aggregated transaction stats +- `totalUBIDistributed` — lifetime distributed UBI +- `uniqueClaimers` — tracked unique claimers via whitelist add or remove +- `totalClaims` — lifetime UBI claim events + +### Explorer naming + +- Singular names (`dailyUBI`, `walletStat`) fetch by `id` +- Plural names (`dailyUBIs`, `walletStats`) query lists with filters and pagination + +--- + +## Typical Questions This Subgraph Answers + +- How much UBI was distributed on a day/cycle? +- Which wallets are active or recently claiming? +- What are aggregate tx/circulation trends? +- How did collected UBI/community-pool values evolve over time? + +--- + +## Query Discipline + +- Use lowercase address strings in filters. +- Use string-safe handling for large integer values. +- Use `_meta` to validate freshness before cross-day analytics. +- Use authenticated gateway access for programmatic queries. +- Before claiming a field or entity is missing, verify availability from this guide and schema first. diff --git a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql new file mode 100644 index 0000000..e544a0d --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql @@ -0,0 +1,113 @@ +scalar BigDecimal + +scalar BigInt + +scalar Boolean + +scalar Bytes + +type DailyUBI { + id: ID! + pool: BigInt! + quota: BigInt! + activeUsers: BigInt! + totalUBIDistributed: BigInt! + totalClaims: BigInt! + newClaimers: BigInt! + timestamp: BigInt! + ubiSchemeAddress: Bytes + balance: BigInt! + cycleLength: BigInt! + dayInCycle: BigInt! +} + +scalar Float + +type GlobalStatistics { + id: ID! + TransactionStat: TransactionStat + totalUBIDistributed: BigInt! + uniqueClaimers: BigInt! + totalClaims: BigInt! +} + +scalar ID + +""" +4 bytes signed integer +""" + +scalar Int + +""" +8 bytes signed integer +""" + +scalar Int8 + +""" +Defines the order direction, either ascending or descending +""" + +scalar String + +""" +A string representation of microseconds UNIX timestamp (16 digits) +""" + +scalar Timestamp + +type TransactionStat { + id: ID! + dayStartBlockNumber: BigInt! + transactionsCount: BigInt! + transactionsCountClean: BigInt! + transactionsValue: BigInt! + transactionsValueClean: BigInt! + totalInCirculation: BigInt! +} + +type UBICollected { + id: ID! + contract: Bytes + block: BigInt! + blockTimestamp: BigInt! + ubi: BigDecimal! + communityPool: BigDecimal! +} + +type UBIHistory { + id: ID! + block: BigInt! + blockTimestamp: BigInt! + totalDailyUBI: BigDecimal! + totalDailyCommunityPool: BigDecimal! +} + +type WalletStat { + id: ID! + dateAppeared: BigInt! + balance: BigInt! + inTransactionsCount: BigInt! + inTransactionsCountClean: BigInt! + inTransactionsValue: BigInt! + inTransactionsValueClean: BigInt! + outTransactionsCount: BigInt! + outTransactionsCountClean: BigInt! + outTransactionsValue: BigInt! + outTransactionsValueClean: BigInt! + totalTransactionsCount: BigInt! + totalTransactionsCountClean: BigInt! + totalTransactionsValue: BigInt! + totalTransactionsValueClean: BigInt! + lastClaimed: BigInt! + totalClaimedCount: BigInt! + totalClaimedValue: BigInt! + claimStreak: BigInt! + longestClaimStreak: BigInt! + isWhitelisted: Boolean! + isActiveUser: Boolean! + dateJoined: BigInt! + lastTransactionFrom: BigInt! + lastTransactionTo: BigInt! +} diff --git a/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md b/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md new file mode 100644 index 0000000..9f55b73 --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md @@ -0,0 +1,41 @@ +# Reserve Celo — Subgraph Usage Guide + +Companion to `reserve-celo.graphql`. This guide is for reserve pricing and broker swap indexing on Celo. + +## Endpoint + +- Goldsky: `https://api.goldsky.com/api/public/project_cmizuamdtfouu01x4csuk5dk1/subgraphs/reserve_celo/1.0/gn` + +--- + +## Entity Overview + +### Core entity + +**ReservePrice** — one indexed reserve pricing point produced from broker swap flow plus exchange-provider price read. +Key fields: `exchangeId`, `exchangeProvider`, `price`, `timestamp`, `day`, `tokenIn`, `tokenOut`, `amountIn`, `amountOut`, `user`, `blockNumber`, `transactionHash`. + +--- + +## Typical Questions This Subgraph Answers + +- What are the most recent reserve prices? +- What was the reserve price on a specific day window? +- Which token pair and user triggered a pricing point? +- Which tx hash/block produced a given price point? + +--- + +## Query Discipline + +- Lowercase all address values in filters. +- Use string values for large integer variables. +- Use `_meta` before analytics queries when stale indexing is suspected. + +--- + +## Practical Start + +1. Check `_meta` block height and `hasIndexingErrors`. +2. Pull latest `ReservePrice` records sorted by `timestamp desc`. +3. Add `day`-based narrowing for historical windows. diff --git a/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql b/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql new file mode 100644 index 0000000..56cac37 --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql @@ -0,0 +1,90 @@ +scalar BigDecimal + +scalar BigInt + +scalar Boolean + +scalar Bytes + +scalar Float + +scalar ID + +""" +4 bytes signed integer +""" + +scalar Int + +""" +8 bytes signed integer +""" + +scalar Int8 + +""" +Defines the order direction, either ascending or descending +""" + +type ReservePrice { + """ + Exchange ID combined with timestamp + """ + id: ID! + """ + The exchange ID from the Swap event + """ + exchangeId: Bytes! + """ + The exchange provider address + """ + exchangeProvider: Bytes! + """ + The current price from IBancorExchangeProvider.currentPrice() + """ + price: BigInt! + """ + Transaction timestamp + """ + timestamp: BigInt! + """ + Day ID (timestamp / 86400) + """ + day: BigInt! + """ + Token in address from the swap + """ + tokenIn: Bytes! + """ + Token out address from the swap + """ + tokenOut: Bytes! + """ + Amount in from the swap + """ + amountIn: BigInt! + """ + Amount out from the swap + """ + amountOut: BigInt! + """ + User who initiated the swap + """ + user: Bytes! + """ + Block number + """ + blockNumber: BigInt! + """ + Transaction hash + """ + transactionHash: Bytes! +} + +scalar String + +""" +A string representation of microseconds UNIX timestamp (16 digits) +""" + +scalar Timestamp diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..9b92c72 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "gooddollar": { + "source": "GoodDollar/GoodSkills", + "sourceType": "github", + "skillPath": "skills/gooddollar/SKILL.md", + "computedHash": "d5f8cc5079f386e7170925e61214fb3334176aa6d0fc6bdc50400e535692b377" + } + } +} From 25ae9bce6e945862fae50a3617a24c2d83880ace Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 18:09:45 +0000 Subject: [PATCH 03/27] feat: add staking migration widget package and state coverage --- examples/storybook/package.json | 1 + .../StakingMigrationWidget.stories.tsx | 147 ++++ .../staking-migration-widget/package.json | 50 ++ .../src/MigrationProgressTimeline.tsx | 37 + .../src/MigrationStatusNotice.tsx | 40 ++ .../src/MigrationStepRow.tsx | 39 ++ .../src/MigrationSummaryCard.tsx | 49 ++ .../src/StakingMigrationWidget.tsx | 158 +++++ .../staking-migration-widget/src/adapter.ts | 637 ++++++++++++++++++ .../staking-migration-widget/src/element.ts | 13 + .../staking-migration-widget/src/index.ts | 22 + .../staking-migration-widget/src/register.ts | 13 + .../src/useStakingMigrationAdapter.ts | 3 + .../src/widgetRuntimeContract.ts | 89 +++ .../tsconfig.build.json | 11 + .../staking-migration-widget/tsconfig.json | 14 + .../staking-migration-widget/tsup.config.ts | 15 + pnpm-lock.yaml | 37 + .../staking-migration-widget/states.spec.ts | 72 ++ 19 files changed, 1447 insertions(+) create mode 100644 examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx create mode 100644 packages/staking-migration-widget/package.json create mode 100644 packages/staking-migration-widget/src/MigrationProgressTimeline.tsx create mode 100644 packages/staking-migration-widget/src/MigrationStatusNotice.tsx create mode 100644 packages/staking-migration-widget/src/MigrationStepRow.tsx create mode 100644 packages/staking-migration-widget/src/MigrationSummaryCard.tsx create mode 100644 packages/staking-migration-widget/src/StakingMigrationWidget.tsx create mode 100644 packages/staking-migration-widget/src/adapter.ts create mode 100644 packages/staking-migration-widget/src/element.ts create mode 100644 packages/staking-migration-widget/src/index.ts create mode 100644 packages/staking-migration-widget/src/register.ts create mode 100644 packages/staking-migration-widget/src/useStakingMigrationAdapter.ts create mode 100644 packages/staking-migration-widget/src/widgetRuntimeContract.ts create mode 100644 packages/staking-migration-widget/tsconfig.build.json create mode 100644 packages/staking-migration-widget/tsconfig.json create mode 100644 packages/staking-migration-widget/tsup.config.ts create mode 100644 tests/widgets/staking-migration-widget/states.spec.ts diff --git a/examples/storybook/package.json b/examples/storybook/package.json index 25373c9..23571e4 100644 --- a/examples/storybook/package.json +++ b/examples/storybook/package.json @@ -13,6 +13,7 @@ "@goodwidget/ui": "workspace:*", "@goodwidget/claim-widget-theme-demo": "workspace:*", "@goodwidget/citizen-claim-widget": "workspace:*", + "@goodwidget/staking-migration-widget": "workspace:*", "react": "^18.3.0", "react-dom": "^18.3.0", "react-native-web": "^0.19.13", diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx new file mode 100644 index 0000000..d00f257 --- /dev/null +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx @@ -0,0 +1,147 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import { YStack } from '@goodwidget/ui' +import { + StakingMigrationWidget, + type MigrationStep, + type StakingMigrationWidgetAdapterFactory, + type StakingMigrationWidgetStatus, +} from '@goodwidget/staking-migration-widget' +import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193' + +// This helper builds deterministic adapter snapshots for Storybook state coverage. +function createAdapterFactory( + status: StakingMigrationWidgetStatus, + overrides: { + stakedAmount?: string + stakedAmountRaw?: bigint + completedSteps?: MigrationStep[] + activeStep?: MigrationStep | null + failedStep?: MigrationStep | null + error?: string | null + hasRequiredConfig?: boolean + isWrongNetwork?: boolean + } = {}, +): StakingMigrationWidgetAdapterFactory { + return () => ({ + state: { + status, + address: '0x329377cbeeF39f01b0Ea04B80465c9eB47D3ED1', + chainId: 122, + stakedAmount: overrides.stakedAmount ?? '2500', + stakedAmountRaw: overrides.stakedAmountRaw ?? 250000n, + stakedTokenSymbol: 'sG$', + hasRequiredConfig: overrides.hasRequiredConfig ?? true, + isWrongNetwork: overrides.isWrongNetwork ?? false, + isBalanceLoading: false, + completedSteps: overrides.completedSteps ?? [], + activeStep: overrides.activeStep ?? null, + failedStep: overrides.failedStep ?? null, + approvalTxHash: '0xapprovalhash', + migrationId: 'migration-1', + error: overrides.error ?? null, + }, + actions: { + connect: async () => {}, + refresh: async () => {}, + approveAndMigrate: async () => {}, + retryApproval: async () => {}, + retryMigration: async () => {}, + }, + }) +} + +function StoryShell({ adapterFactory }: { adapterFactory: StakingMigrationWidgetAdapterFactory }) { + const migrationConfig = { + migrationApiBaseUrl: 'https://api.example.com', + migrationOperator: '0x1234567890123456789012345678901234567890' as const, + } + + try { + const provider = createCustodialEip1193Provider() + return ( + + + + ) + } catch (error: unknown) { + return ( + + {error instanceof Error ? error.message : 'Custodial fixture not configured'} + + ) + } +} + +const meta: Meta = { + title: 'Widgets/StakingMigrationWidget', + component: StakingMigrationWidget, + tags: ['autodocs'], + parameters: { layout: 'padded' }, +} + +export default meta +type Story = StoryObj + +export const EmptyBalance: Story = { + render: () => ( + + ), +} + +export const WrongNetwork: Story = { + render: () => ( + + ), +} + +export const ApprovalPending: Story = { + render: () => , +} + +export const Migrating: Story = { + render: () => ( + + ), +} + +export const Success: Story = { + render: () => ( + + ), +} + +export const Error: Story = { + render: () => ( + + ), +} diff --git a/packages/staking-migration-widget/package.json b/packages/staking-migration-widget/package.json new file mode 100644 index 0000000..a1899eb --- /dev/null +++ b/packages/staking-migration-widget/package.json @@ -0,0 +1,50 @@ +{ + "name": "@goodwidget/staking-migration-widget", + "version": "0.1.0", + "description": "Fuse staking migration widget for moving sG$ positions to Celo savings", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./element": { + "types": "./dist/element.d.ts", + "import": "./dist/element.js", + "require": "./dist/element.cjs" + }, + "./register": { + "types": "./dist/register.d.ts", + "import": "./dist/register.js", + "require": "./dist/register.cjs" + } + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "lint": "eslint src/", + "clean": "rm -rf dist .turbo" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "dependencies": { + "@goodwidget/core": "workspace:*", + "@goodwidget/embed": "workspace:*", + "@goodwidget/ui": "workspace:*", + "viem": "^2.0.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "tsup": "^8.4.0", + "typescript": "^5.7.0" + } +} diff --git a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx new file mode 100644 index 0000000..bbf995a --- /dev/null +++ b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { Card, Heading, Text, YStack } from '@goodwidget/ui' +import { MigrationStepRow } from './MigrationStepRow' +import type { MigrationStep } from './widgetRuntimeContract' + +const STEP_ORDER: MigrationStep[] = ['unstake', 'bridge sent', 'bridge received', 'stake'] + +interface MigrationProgressTimelineProps { + completedSteps: MigrationStep[] + activeStep: MigrationStep | null +} + +// This timeline preserves completed steps while advancing exactly one active spinner. +export function MigrationProgressTimeline({ + completedSteps, + activeStep, +}: MigrationProgressTimelineProps) { + return ( + + + Migration in progress + The backend is migrating your position from Fuse staking to Celo savings. + + + {STEP_ORDER.map((step) => ( + + ))} + + + + ) +} diff --git a/packages/staking-migration-widget/src/MigrationStatusNotice.tsx b/packages/staking-migration-widget/src/MigrationStatusNotice.tsx new file mode 100644 index 0000000..92b28da --- /dev/null +++ b/packages/staking-migration-widget/src/MigrationStatusNotice.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { Button, ButtonText, Card, Heading, Text, YStack } from '@goodwidget/ui' + +interface MigrationStatusNoticeProps { + title: string + message: string + status: 'error' | 'warning' | 'success' | 'info' + actionLabel?: string + onAction?: () => void + actionDisabled?: boolean +} + +// This notice standardizes state-specific messaging and optional recovery actions. +export function MigrationStatusNotice({ + title, + message, + status, + actionLabel, + onAction, + actionDisabled, +}: MigrationStatusNoticeProps) { + const color = + status === 'error' ? '$error' : status === 'warning' ? '$warning' : status === 'success' ? '$success' : '$color' + + return ( + + + + {title} + + {message} + {actionLabel && onAction && ( + + )} + + + ) +} diff --git a/packages/staking-migration-widget/src/MigrationStepRow.tsx b/packages/staking-migration-widget/src/MigrationStepRow.tsx new file mode 100644 index 0000000..3e9e5a0 --- /dev/null +++ b/packages/staking-migration-widget/src/MigrationStepRow.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { Spinner, Text, XStack } from '@goodwidget/ui' +import type { MigrationStep } from './widgetRuntimeContract' + +interface MigrationStepRowProps { + step: MigrationStep + isCompleted: boolean + isActive: boolean +} + +// This row keeps step visuals deterministic: completed checkmark, one active spinner, or idle bullet. +export function MigrationStepRow({ step, isCompleted, isActive }: MigrationStepRowProps) { + return ( + + + {isCompleted ? ( + + ✓ + + ) : isActive ? ( + + ) : ( + + )} + {step} + + {isCompleted && ( + + completed + + )} + {isActive && ( + + in progress + + )} + + ) +} diff --git a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx new file mode 100644 index 0000000..2a32538 --- /dev/null +++ b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { Button, ButtonText, Card, Heading, Text, TokenAmount, YStack } from '@goodwidget/ui' + +interface MigrationSummaryCardProps { + stakedAmount: string + isZeroBalance: boolean + isApprovalPending: boolean + isDisabled: boolean + actionLabel: string + onPrimaryAction: () => void +} + +// This summary card is the entry point for approve-and-migrate user action. +export function MigrationSummaryCard({ + stakedAmount, + isZeroBalance, + isApprovalPending, + isDisabled, + actionLabel, + onPrimaryAction, +}: MigrationSummaryCardProps) { + return ( + + + Migrate Fuse staking to Celo savings + + Approve migration once, then the backend completes: unstake → bridge sent → bridge received + → stake. + + + + + Your staked amount + + + {isZeroBalance && ( + + No staked sG$ found on Fuse for this wallet. + + )} + + + + + + ) +} diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx new file mode 100644 index 0000000..86aaca7 --- /dev/null +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -0,0 +1,158 @@ +import React, { useMemo } from 'react' +import { GoodWidgetProvider } from '@goodwidget/core' +import type { EIP1193Provider } from '@goodwidget/core' +import { ToastContainer, YStack } from '@goodwidget/ui' +import { MigrationProgressTimeline } from './MigrationProgressTimeline' +import { MigrationStatusNotice } from './MigrationStatusNotice' +import { MigrationSummaryCard } from './MigrationSummaryCard' +import { useStakingMigrationAdapter } from './adapter' +import type { + StakingMigrationWidgetAdapterFactory, + StakingMigrationWidgetProps, +} from './widgetRuntimeContract' + +interface StakingMigrationInnerProps { + migrationConfig: StakingMigrationWidgetProps['migrationConfig'] + adapterFactory?: StakingMigrationWidgetAdapterFactory + onMigrationSuccess?: StakingMigrationWidgetProps['onMigrationSuccess'] + onMigrationError?: StakingMigrationWidgetProps['onMigrationError'] +} + +// This inner component renders all migration states while staying inside provider context. +function StakingMigrationInner({ + migrationConfig, + adapterFactory, + onMigrationSuccess, + onMigrationError, +}: StakingMigrationInnerProps) { + const defaultAdapter = useStakingMigrationAdapter({ + migrationConfig, + onMigrationSuccess, + onMigrationError, + }) + + const activeAdapter = useMemo( + () => + adapterFactory + ? adapterFactory({ + config: migrationConfig ?? {}, + }) + : defaultAdapter, + [adapterFactory, defaultAdapter, migrationConfig], + ) + + const { state, actions } = activeAdapter + const isZeroBalance = state.stakedAmountRaw <= 0n + const isApprovalPending = state.status === 'approval-pending' + + const isSummaryActionDisabled = + isApprovalPending || + !state.hasRequiredConfig || + state.isWrongNetwork || + state.isBalanceLoading || + isZeroBalance + + return ( + + {state.status === 'missing-config' && ( + + )} + + {state.status === 'wrong-network' && ( + void actions.refresh()} + /> + )} + + {state.status === 'approval-failed' && ( + void actions.retryApproval()} + /> + )} + + {(state.status === 'migrating' || state.status === 'error' || state.status === 'success') && ( + + )} + + {state.status === 'error' && ( + void actions.retryMigration()} + /> + )} + + {state.status === 'success' && ( + void actions.refresh()} + /> + )} + + { + if (!state.address) { + void actions.connect() + return + } + void actions.approveAndMigrate() + }} + /> + + ) +} + +// This is the public React widget entrypoint with provider-first mounting. +export function StakingMigrationWidget({ + provider, + config, + defaultTheme = 'light', + themeOverrides, + migrationConfig, + onMigrationSuccess, + onMigrationError, + adapterFactory, +}: StakingMigrationWidgetProps) { + return ( + + + + + ) +} diff --git a/packages/staking-migration-widget/src/adapter.ts b/packages/staking-migration-widget/src/adapter.ts new file mode 100644 index 0000000..55f0327 --- /dev/null +++ b/packages/staking-migration-widget/src/adapter.ts @@ -0,0 +1,637 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useWallet } from '@goodwidget/core' +import type { EIP1193Provider } from '@goodwidget/core' +import { + createPublicClient, + createWalletClient, + custom, + formatUnits, + http, + parseAbi, + type Address, + type Chain, +} from 'viem' +import { + FUSE_CHAIN_ID, + FUSE_STAKING_CONTRACT_ADDRESS, + type MigrationStep, + type StakingMigrationErrorDetail, + type StakingMigrationSuccessDetail, + type StakingMigrationWidgetAdapterResult, + type StakingMigrationWidgetConfig, + type StakingMigrationWidgetState, +} from './widgetRuntimeContract' + +// This is the migration step order expected by the widget timeline. +const MIGRATION_STEPS: MigrationStep[] = ['unstake', 'bridge sent', 'bridge received', 'stake'] + +// This ABI covers the ERC-20-style methods required for sG$ balance and approval flow. +const fuseStakingAbi = parseAbi([ + 'function balanceOf(address account) view returns (uint256)', + 'function decimals() view returns (uint8)', + 'function approve(address spender, uint256 amount) returns (bool)', +]) + +// This chain descriptor keeps viem wallet/public clients aligned with Fuse mainnet. +const FUSE_CHAIN: Chain = { + id: FUSE_CHAIN_ID, + name: 'Fuse', + nativeCurrency: { + name: 'Fuse', + symbol: 'FUSE', + decimals: 18, + }, + rpcUrls: { + default: { http: ['https://rpc.fuse.io'] }, + public: { http: ['https://rpc.fuse.io'] }, + }, +} + +const DEFAULT_STATE: StakingMigrationWidgetState = { + status: 'summary', + address: null, + chainId: null, + stakedAmount: '0', + stakedAmountRaw: 0n, + stakedTokenSymbol: 'sG$', + hasRequiredConfig: false, + isWrongNetwork: false, + isBalanceLoading: false, + completedSteps: [], + activeStep: null, + failedStep: null, + approvalTxHash: null, + migrationId: null, + error: null, +} + +interface ApiProgressPayload { + migrationId: string | null + status: 'migrating' | 'success' | 'error' + completedSteps: MigrationStep[] + activeStep: MigrationStep | null + failedStep: MigrationStep | null + reason: string | null +} + +export interface UseStakingMigrationAdapterOptions { + migrationConfig?: StakingMigrationWidgetConfig + onMigrationSuccess?: (detail: StakingMigrationSuccessDetail) => void + onMigrationError?: (detail: StakingMigrationErrorDetail) => void +} + +function formatErrorMessage(error: unknown): string { + if (!(error instanceof Error)) return 'Unexpected migration error' + const lowerMessage = error.message.toLowerCase() + if (lowerMessage.includes('user rejected') || lowerMessage.includes('rejected the request')) { + return 'Approval rejected in wallet' + } + return error.message +} + +function normalizeStep(step: unknown): MigrationStep | null { + if (typeof step !== 'string') return null + const lowerStep = step.trim().toLowerCase().replace(/[_-]/g, ' ') + if (lowerStep.includes('unstake')) return 'unstake' + if (lowerStep.includes('bridge sent') || lowerStep === 'bridge') return 'bridge sent' + if (lowerStep.includes('bridge received')) return 'bridge received' + if (lowerStep.includes('stake')) return 'stake' + return null +} + +function collectCompletedSteps(payload: Record): MigrationStep[] { + const completed = new Set() + + const completedSteps = payload.completedSteps + if (Array.isArray(completedSteps)) { + for (const step of completedSteps) { + const normalizedStep = normalizeStep(step) + if (normalizedStep) completed.add(normalizedStep) + } + } + + const steps = payload.steps + if (Array.isArray(steps)) { + for (const stepEntry of steps) { + if (!stepEntry || typeof stepEntry !== 'object') continue + const entry = stepEntry as Record + const normalizedStep = normalizeStep(entry.step ?? entry.name ?? entry.id) + const normalizedStatus = + typeof entry.status === 'string' ? entry.status.toLowerCase().trim() : undefined + if (normalizedStep && normalizedStatus === 'completed') { + completed.add(normalizedStep) + } + } + } + + return MIGRATION_STEPS.filter((step) => completed.has(step)) +} + +function normalizeApiProgress(rawPayload: unknown): ApiProgressPayload { + const payload = rawPayload && typeof rawPayload === 'object' ? (rawPayload as Record) : {} + + const rawStatus = + typeof payload.status === 'string' + ? payload.status.toLowerCase().trim() + : typeof payload.state === 'string' + ? payload.state.toLowerCase().trim() + : 'migrating' + + const normalizedStatus: ApiProgressPayload['status'] = + rawStatus === 'success' || rawStatus === 'completed' || rawStatus === 'done' + ? 'success' + : rawStatus === 'error' || rawStatus === 'failed' + ? 'error' + : 'migrating' + + const activeStep = normalizeStep(payload.activeStep ?? payload.step ?? payload.currentStep) + const failedStep = normalizeStep(payload.failedStep ?? payload.errorStep) + + const reasonSource = + payload.reason ?? + payload.message ?? + (payload.error && typeof payload.error === 'object' + ? (payload.error as Record).message + : payload.error) + + const completedSteps = collectCompletedSteps(payload) + + const migrationId = + typeof payload.migrationId === 'string' + ? payload.migrationId + : typeof payload.id === 'string' + ? payload.id + : null + + return { + migrationId, + status: normalizedStatus, + completedSteps, + activeStep, + failedStep, + reason: typeof reasonSource === 'string' ? reasonSource : null, + } +} + +function buildApiHeaders(migrationConfig: StakingMigrationWidgetConfig): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (migrationConfig.migrationApiToken) { + headers.Authorization = 'Bearer ' + migrationConfig.migrationApiToken + } + + return headers +} + +function hasRequiredConfig(migrationConfig: StakingMigrationWidgetConfig): boolean { + return Boolean(migrationConfig.migrationApiBaseUrl && migrationConfig.migrationOperator) +} + +export function useStakingMigrationAdapter({ + migrationConfig, + onMigrationSuccess, + onMigrationError, +}: UseStakingMigrationAdapterOptions = {}): StakingMigrationWidgetAdapterResult { + const { address, chainId, isConnected, provider, connect } = useWallet() + + const resolvedConfig = useMemo( + () => ({ + migrationApiBaseUrl: migrationConfig?.migrationApiBaseUrl, + migrationOperator: migrationConfig?.migrationOperator, + migrationApiToken: migrationConfig?.migrationApiToken, + }), + [ + migrationConfig?.migrationApiBaseUrl, + migrationConfig?.migrationApiToken, + migrationConfig?.migrationOperator, + ], + ) + + const [state, setState] = useState(() => ({ + ...DEFAULT_STATE, + hasRequiredConfig: hasRequiredConfig(resolvedConfig), + })) + + const actionInFlightRef = useRef(false) + const unmountedRef = useRef(false) + + const publicClient = useMemo( + () => + createPublicClient({ + chain: FUSE_CHAIN, + transport: http(FUSE_CHAIN.rpcUrls.default.http[0]), + }), + [], + ) + + const walletClient = useMemo(() => { + if (!provider || !address) return null + + return createWalletClient({ + account: address as Address, + chain: FUSE_CHAIN, + transport: custom(provider as EIP1193Provider), + }) + }, [provider, address]) + + const refreshStakeState = useCallback(async () => { + if (!isConnected || !address) { + setState((previousState) => ({ + ...previousState, + status: hasRequiredConfig(resolvedConfig) ? 'summary' : 'missing-config', + address: null, + chainId: chainId ?? null, + stakedAmount: '0', + stakedAmountRaw: 0n, + isBalanceLoading: false, + hasRequiredConfig: hasRequiredConfig(resolvedConfig), + isWrongNetwork: false, + error: null, + })) + return + } + + const configReady = hasRequiredConfig(resolvedConfig) + const wrongNetwork = chainId !== FUSE_CHAIN_ID + + if (!configReady || wrongNetwork) { + setState((previousState) => ({ + ...previousState, + status: !configReady ? 'missing-config' : 'wrong-network', + address, + chainId: chainId ?? null, + hasRequiredConfig: configReady, + isWrongNetwork: wrongNetwork, + isBalanceLoading: false, + error: null, + })) + return + } + + setState((previousState) => ({ + ...previousState, + status: previousState.status === 'success' ? 'success' : 'summary', + address, + chainId: chainId ?? null, + hasRequiredConfig: true, + isWrongNetwork: false, + isBalanceLoading: true, + error: null, + })) + + try { + const [stakedAmountRaw, stakingTokenDecimals] = await Promise.all([ + publicClient.readContract({ + address: FUSE_STAKING_CONTRACT_ADDRESS, + abi: fuseStakingAbi, + functionName: 'balanceOf', + args: [address as Address], + }), + publicClient.readContract({ + address: FUSE_STAKING_CONTRACT_ADDRESS, + abi: fuseStakingAbi, + functionName: 'decimals', + }), + ]) + + const stakedAmount = formatUnits(stakedAmountRaw, stakingTokenDecimals) + + if (unmountedRef.current) return + + setState((previousState) => ({ + ...previousState, + status: previousState.status === 'success' ? 'success' : 'summary', + address, + chainId: chainId ?? null, + stakedAmount, + stakedAmountRaw, + isBalanceLoading: false, + error: null, + })) + } catch (error: unknown) { + if (unmountedRef.current) return + setState((previousState) => ({ + ...previousState, + status: 'error', + address, + chainId: chainId ?? null, + isBalanceLoading: false, + error: formatErrorMessage(error), + })) + } + }, [address, chainId, isConnected, publicClient, resolvedConfig]) + + useEffect(() => { + unmountedRef.current = false + return () => { + unmountedRef.current = true + } + }, []) + + useEffect(() => { + void refreshStakeState() + }, [refreshStakeState]) + + const applyMigrationProgress = useCallback( + (progress: ApiProgressPayload, approvalTxHash: string) => { + if (progress.status === 'success') { + setState((previousState) => ({ + ...previousState, + status: 'success', + approvalTxHash, + migrationId: progress.migrationId, + completedSteps: progress.completedSteps.length > 0 ? progress.completedSteps : MIGRATION_STEPS, + activeStep: null, + failedStep: null, + error: null, + })) + + onMigrationSuccess?.({ + address: address!, + approvalTxHash, + migrationId: progress.migrationId ?? 'unknown', + completedSteps: + progress.completedSteps.length > 0 ? progress.completedSteps : [...MIGRATION_STEPS], + }) + return true + } + + if (progress.status === 'error') { + const errorMessage = progress.reason ?? 'Migration failed during backend processing' + + setState((previousState) => ({ + ...previousState, + status: 'error', + approvalTxHash, + migrationId: progress.migrationId, + completedSteps: progress.completedSteps, + activeStep: progress.activeStep, + failedStep: progress.failedStep, + error: errorMessage, + })) + + onMigrationError?.({ + address: address ?? null, + reason: errorMessage, + failedStep: progress.failedStep, + }) + return true + } + + setState((previousState) => ({ + ...previousState, + status: 'migrating', + approvalTxHash, + migrationId: progress.migrationId, + completedSteps: progress.completedSteps, + activeStep: + progress.activeStep ?? + MIGRATION_STEPS.find((step) => !progress.completedSteps.includes(step)) ?? + null, + failedStep: null, + error: null, + })) + return false + }, + [address, onMigrationError, onMigrationSuccess], + ) + + const submitMigrationApproval = useCallback( + async (approvalTxHash: string): Promise => { + const baseUrl = resolvedConfig.migrationApiBaseUrl! + const endpoint = `${baseUrl.replace(/\/$/, '')}/staking-migrations` + + const response = await fetch(endpoint, { + method: 'POST', + headers: buildApiHeaders(resolvedConfig), + body: JSON.stringify({ + walletAddress: address, + approvalTxHash, + sourceChainId: FUSE_CHAIN_ID, + stakingContract: FUSE_STAKING_CONTRACT_ADDRESS, + migrationOperator: resolvedConfig.migrationOperator, + }), + }) + + const responsePayload = (await response.json().catch(() => ({}))) as unknown + + if (!response.ok) { + const normalizedPayload = normalizeApiProgress(responsePayload) + throw new Error(normalizedPayload.reason ?? `Migration API request failed (${response.status})`) + } + + return normalizeApiProgress(responsePayload) + }, + [address, resolvedConfig], + ) + + const fetchMigrationProgress = useCallback( + async (migrationId: string): Promise => { + const baseUrl = resolvedConfig.migrationApiBaseUrl! + const endpoint = `${baseUrl.replace(/\/$/, '')}/staking-migrations/${migrationId}` + + const response = await fetch(endpoint, { + method: 'GET', + headers: buildApiHeaders(resolvedConfig), + }) + + const responsePayload = (await response.json().catch(() => ({}))) as unknown + + if (!response.ok) { + const normalizedPayload = normalizeApiProgress(responsePayload) + throw new Error(normalizedPayload.reason ?? `Migration status request failed (${response.status})`) + } + + return normalizeApiProgress(responsePayload) + }, + [resolvedConfig], + ) + + const waitForMigrationCompletion = useCallback( + async (approvalTxHash: string, initialProgress: ApiProgressPayload): Promise => { + let progress = initialProgress + + if (applyMigrationProgress(progress, approvalTxHash)) { + return + } + + const migrationId = progress.migrationId + if (!migrationId) { + throw new Error('Migration API did not return a migration id') + } + + while (!unmountedRef.current) { + await new Promise((resolve) => setTimeout(resolve, 3000)) + progress = await fetchMigrationProgress(migrationId) + const reachedTerminalState = applyMigrationProgress(progress, approvalTxHash) + if (reachedTerminalState) { + return + } + } + }, + [applyMigrationProgress, fetchMigrationProgress], + ) + + const startApprovalAndMigration = useCallback(async () => { + if (actionInFlightRef.current) return + + const configReady = hasRequiredConfig(resolvedConfig) + if (!configReady) { + setState((previousState) => ({ + ...previousState, + status: 'missing-config', + hasRequiredConfig: false, + error: 'Missing migrationApiBaseUrl or migrationOperator in migrationConfig', + })) + return + } + + if (!isConnected || !address) { + await connect() + return + } + + if (chainId !== FUSE_CHAIN_ID) { + setState((previousState) => ({ + ...previousState, + status: 'wrong-network', + isWrongNetwork: true, + error: 'Switch wallet network to Fuse before approving migration', + })) + return + } + + if (!walletClient) { + setState((previousState) => ({ + ...previousState, + status: 'approval-failed', + error: 'Wallet client is not available for Fuse approval', + })) + return + } + + if (state.stakedAmountRaw <= 0n) { + setState((previousState) => ({ + ...previousState, + status: 'summary', + error: null, + })) + return + } + + actionInFlightRef.current = true + let approvalConfirmed = false + setState((previousState) => ({ + ...previousState, + status: 'approval-pending', + error: null, + failedStep: null, + })) + + try { + const approvalTxHash = await walletClient.writeContract({ + address: FUSE_STAKING_CONTRACT_ADDRESS, + abi: fuseStakingAbi, + functionName: 'approve', + args: [resolvedConfig.migrationOperator!, state.stakedAmountRaw], + }) + + const approvalReceipt = await publicClient.waitForTransactionReceipt({ + hash: approvalTxHash, + }) + + if (approvalReceipt.status !== 'success') { + throw new Error('Approval transaction did not confirm successfully') + } + approvalConfirmed = true + + const initialProgress = await submitMigrationApproval(approvalTxHash) + await waitForMigrationCompletion(approvalTxHash, initialProgress) + } catch (error: unknown) { + const errorMessage = formatErrorMessage(error) + const isApprovalFailure = !approvalConfirmed + + setState((previousState) => ({ + ...previousState, + status: isApprovalFailure ? 'approval-failed' : 'error', + error: errorMessage, + })) + + onMigrationError?.({ + address: address ?? null, + reason: errorMessage, + failedStep: state.activeStep, + }) + } finally { + actionInFlightRef.current = false + } + }, [ + address, + chainId, + connect, + isConnected, + onMigrationError, + publicClient, + resolvedConfig, + state.activeStep, + state.stakedAmountRaw, + state.status, + submitMigrationApproval, + waitForMigrationCompletion, + walletClient, + ]) + + const retryApproval = useCallback(async () => { + await startApprovalAndMigration() + }, [startApprovalAndMigration]) + + const retryMigration = useCallback(async () => { + if (!state.approvalTxHash) { + await startApprovalAndMigration() + return + } + + if (!hasRequiredConfig(resolvedConfig)) { + setState((previousState) => ({ + ...previousState, + status: 'missing-config', + error: 'Missing migrationApiBaseUrl or migrationOperator in migrationConfig', + })) + return + } + + actionInFlightRef.current = true + setState((previousState) => ({ + ...previousState, + status: 'migrating', + error: null, + })) + + try { + const initialProgress = await submitMigrationApproval(state.approvalTxHash) + await waitForMigrationCompletion(state.approvalTxHash, initialProgress) + } catch (error: unknown) { + const errorMessage = formatErrorMessage(error) + setState((previousState) => ({ + ...previousState, + status: 'error', + error: errorMessage, + })) + } finally { + actionInFlightRef.current = false + } + }, [resolvedConfig, startApprovalAndMigration, state.approvalTxHash, submitMigrationApproval, waitForMigrationCompletion]) + + return { + state, + actions: { + connect, + refresh: refreshStakeState, + approveAndMigrate: startApprovalAndMigration, + retryApproval, + retryMigration, + }, + } +} diff --git a/packages/staking-migration-widget/src/element.ts b/packages/staking-migration-widget/src/element.ts new file mode 100644 index 0000000..1532cb6 --- /dev/null +++ b/packages/staking-migration-widget/src/element.ts @@ -0,0 +1,13 @@ +import { createMiniAppElement } from '@goodwidget/embed' +import { StakingMigrationWidget } from './StakingMigrationWidget' +import type React from 'react' + +// This custom element wraps the staking migration widget for HTML integrators. +export const StakingMigrationWidgetElement = createMiniAppElement( + StakingMigrationWidget as React.ComponentType>, + { + shadow: true, + defaultTheme: 'light', + events: ['migration-success', 'migration-error'], + }, +) diff --git a/packages/staking-migration-widget/src/index.ts b/packages/staking-migration-widget/src/index.ts new file mode 100644 index 0000000..c4ac72b --- /dev/null +++ b/packages/staking-migration-widget/src/index.ts @@ -0,0 +1,22 @@ +export { StakingMigrationWidget } from './StakingMigrationWidget' +export { useStakingMigrationAdapter } from './adapter' +export type { UseStakingMigrationAdapterOptions } from './adapter' + +export type { + MigrationStep, + StakingMigrationErrorDetail, + StakingMigrationSuccessDetail, + StakingMigrationWidgetAdapterFactory, + StakingMigrationWidgetAdapterFactoryInput, + StakingMigrationWidgetAdapterResult, + StakingMigrationWidgetActions, + StakingMigrationWidgetConfig, + StakingMigrationWidgetProps, + StakingMigrationWidgetState, + StakingMigrationWidgetStatus, +} from './widgetRuntimeContract' + +export { + FUSE_CHAIN_ID, + FUSE_STAKING_CONTRACT_ADDRESS, +} from './widgetRuntimeContract' diff --git a/packages/staking-migration-widget/src/register.ts b/packages/staking-migration-widget/src/register.ts new file mode 100644 index 0000000..e788b1e --- /dev/null +++ b/packages/staking-migration-widget/src/register.ts @@ -0,0 +1,13 @@ +import { StakingMigrationWidgetElement } from './element' + +const TAG_NAME = 'gw-staking-migration-widget' + +// This helper registers the default staking migration custom element tag. +export function register(tagName: string = TAG_NAME): string { + if (!customElements.get(tagName)) { + customElements.define(tagName, StakingMigrationWidgetElement) + } + return tagName +} + +register() diff --git a/packages/staking-migration-widget/src/useStakingMigrationAdapter.ts b/packages/staking-migration-widget/src/useStakingMigrationAdapter.ts new file mode 100644 index 0000000..3fc8136 --- /dev/null +++ b/packages/staking-migration-widget/src/useStakingMigrationAdapter.ts @@ -0,0 +1,3 @@ +// This file keeps a dedicated hook entrypoint for integrators expecting use* naming. +export { useStakingMigrationAdapter } from './adapter' +export type { UseStakingMigrationAdapterOptions } from './adapter' diff --git a/packages/staking-migration-widget/src/widgetRuntimeContract.ts b/packages/staking-migration-widget/src/widgetRuntimeContract.ts new file mode 100644 index 0000000..beac70e --- /dev/null +++ b/packages/staking-migration-widget/src/widgetRuntimeContract.ts @@ -0,0 +1,89 @@ +import type { Address } from 'viem' +import type { GoodWidgetConfig, GoodWidgetThemeOverrides } from '@goodwidget/ui' + +// This is the expected network for Fuse staking approvals. +export const FUSE_CHAIN_ID = 122 + +// This address is sourced from GoodProtocol releases/deployment.json -> production.FuseStaking. +export const FUSE_STAKING_CONTRACT_ADDRESS: Address = '0xA199F0C353E25AdF022378B0c208D600f39a6505' + +export type MigrationStep = 'unstake' | 'bridge sent' | 'bridge received' | 'stake' + +export type StakingMigrationWidgetStatus = + | 'summary' + | 'approval-pending' + | 'approval-failed' + | 'migrating' + | 'success' + | 'error' + | 'wrong-network' + | 'missing-config' + +export interface StakingMigrationSuccessDetail { + address: string + approvalTxHash: string + migrationId: string + completedSteps: MigrationStep[] +} + +export interface StakingMigrationErrorDetail { + address: string | null + reason: string + failedStep: MigrationStep | null +} + +export interface StakingMigrationWidgetState { + status: StakingMigrationWidgetStatus + address: string | null + chainId: number | null + stakedAmount: string + stakedAmountRaw: bigint + stakedTokenSymbol: 'sG$' + hasRequiredConfig: boolean + isWrongNetwork: boolean + isBalanceLoading: boolean + completedSteps: MigrationStep[] + activeStep: MigrationStep | null + failedStep: MigrationStep | null + approvalTxHash: string | null + migrationId: string | null + error: string | null +} + +export interface StakingMigrationWidgetActions { + connect: () => Promise + refresh: () => Promise + approveAndMigrate: () => Promise + retryApproval: () => Promise + retryMigration: () => Promise +} + +export interface StakingMigrationWidgetAdapterResult { + state: StakingMigrationWidgetState + actions: StakingMigrationWidgetActions +} + +export interface StakingMigrationWidgetAdapterFactoryInput { + config: StakingMigrationWidgetConfig +} + +export type StakingMigrationWidgetAdapterFactory = ( + input: StakingMigrationWidgetAdapterFactoryInput, +) => StakingMigrationWidgetAdapterResult + +export interface StakingMigrationWidgetConfig { + migrationApiBaseUrl?: string + migrationOperator?: Address + migrationApiToken?: string +} + +export interface StakingMigrationWidgetProps { + provider?: unknown + config?: GoodWidgetConfig + defaultTheme?: 'light' | 'dark' + themeOverrides?: GoodWidgetThemeOverrides + migrationConfig?: StakingMigrationWidgetConfig + onMigrationSuccess?: (detail: StakingMigrationSuccessDetail) => void + onMigrationError?: (detail: StakingMigrationErrorDetail) => void + adapterFactory?: StakingMigrationWidgetAdapterFactory +} diff --git a/packages/staking-migration-widget/tsconfig.build.json b/packages/staking-migration-widget/tsconfig.build.json new file mode 100644 index 0000000..54871c4 --- /dev/null +++ b/packages/staking-migration-widget/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": { + "react-native": ["./node_modules/react-native-web"] + } + }, + "include": ["src"] +} diff --git a/packages/staking-migration-widget/tsconfig.json b/packages/staking-migration-widget/tsconfig.json new file mode 100644 index 0000000..006b6f4 --- /dev/null +++ b/packages/staking-migration-widget/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": { + "@goodwidget/core": ["../core/src/index.ts"], + "@goodwidget/core/*": ["../core/src/*"], + "@goodwidget/ui": ["../ui/src/index.ts"], + "react-native": ["./node_modules/react-native-web"] + } + }, + "include": ["src"] +} diff --git a/packages/staking-migration-widget/tsup.config.ts b/packages/staking-migration-widget/tsup.config.ts new file mode 100644 index 0000000..95c2de0 --- /dev/null +++ b/packages/staking-migration-widget/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: { + index: 'src/index.ts', + element: 'src/element.ts', + register: 'src/register.ts', + }, + format: ['esm', 'cjs'], + dts: true, + sourcemap: true, + clean: true, + tsconfig: 'tsconfig.build.json', + external: ['react', 'react-dom', 'react-native', 'react-native-web'], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04afa8b..c96c815 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,9 @@ importers: '@goodwidget/core': specifier: workspace:* version: link:../../packages/core + '@goodwidget/staking-migration-widget': + specifier: workspace:* + version: link:../../packages/staking-migration-widget '@goodwidget/ui': specifier: workspace:* version: link:../../packages/ui @@ -344,6 +347,40 @@ importers: specifier: ^5.7.0 version: 5.9.3 + packages/staking-migration-widget: + dependencies: + '@goodwidget/core': + specifier: workspace:* + version: link:../core + '@goodwidget/embed': + specifier: workspace:* + version: link:../embed + '@goodwidget/ui': + specifier: workspace:* + version: link:../ui + viem: + specifier: ^2.0.0 + version: 2.48.4(typescript@5.9.3) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.28) + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) + tsup: + specifier: ^8.4.0 + version: 8.5.1(@swc/core@1.15.30)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3) + typescript: + specifier: ^5.7.0 + version: 5.9.3 + packages/ui: dependencies: '@tamagui/animations-react-native': diff --git a/tests/widgets/staking-migration-widget/states.spec.ts b/tests/widgets/staking-migration-widget/states.spec.ts new file mode 100644 index 0000000..bb76dc5 --- /dev/null +++ b/tests/widgets/staking-migration-widget/states.spec.ts @@ -0,0 +1,72 @@ +import { expect, test, type Page } from '@playwright/test' + +// This map keeps each test state tied to one Storybook story for visual smoke coverage. +const STORY_IDS = { + empty: '/iframe.html?id=widgets-stakingmigrationwidget--empty-balance&viewMode=story', + wrongNetwork: '/iframe.html?id=widgets-stakingmigrationwidget--wrong-network&viewMode=story', + approvalPending: '/iframe.html?id=widgets-stakingmigrationwidget--approval-pending&viewMode=story', + migrating: '/iframe.html?id=widgets-stakingmigrationwidget--migrating&viewMode=story', + success: '/iframe.html?id=widgets-stakingmigrationwidget--success&viewMode=story', + error: '/iframe.html?id=widgets-stakingmigrationwidget--error&viewMode=story', +} as const + +async function gotoStory(page: Page, storyUrl: string): Promise { + await page.goto(storyUrl) + await page.waitForLoadState('domcontentloaded') +} + +test('StakingMigrationWidget empty balance summary', async ({ page }) => { + await gotoStory(page, STORY_IDS.empty) + await expect(page.getByText('No staked sG$ found on Fuse for this wallet.')).toBeVisible() + await page.screenshot({ + path: 'tests/widgets/staking-migration-widget/test-results/smw-01-empty-balance.png', + fullPage: true, + }) +}) + +test('StakingMigrationWidget wrong network notice', async ({ page }) => { + await gotoStory(page, STORY_IDS.wrongNetwork) + await expect(page.getByText('Wrong network')).toBeVisible() + await page.screenshot({ + path: 'tests/widgets/staking-migration-widget/test-results/smw-02-wrong-network.png', + fullPage: true, + }) +}) + +test('StakingMigrationWidget approval pending notice', async ({ page }) => { + await gotoStory(page, STORY_IDS.approvalPending) + await expect(page.getByText('Approval pending…')).toBeVisible() + await page.screenshot({ + path: 'tests/widgets/staking-migration-widget/test-results/smw-03-approval-pending.png', + fullPage: true, + }) +}) + +test('StakingMigrationWidget migrating timeline', async ({ page }) => { + await gotoStory(page, STORY_IDS.migrating) + await expect(page.getByText('Migration in progress')).toBeVisible() + await expect(page.getByText('bridge received')).toBeVisible() + await page.screenshot({ + path: 'tests/widgets/staking-migration-widget/test-results/smw-04-migrating.png', + fullPage: true, + }) +}) + +test('StakingMigrationWidget success state', async ({ page }) => { + await gotoStory(page, STORY_IDS.success) + await expect(page.getByText('Migration complete')).toBeVisible() + await page.screenshot({ + path: 'tests/widgets/staking-migration-widget/test-results/smw-05-success.png', + fullPage: true, + }) +}) + +test('StakingMigrationWidget error state', async ({ page }) => { + await gotoStory(page, STORY_IDS.error) + await expect(page.getByText('Migration failed')).toBeVisible() + await expect(page.getByText('Bridge finalization timeout')).toBeVisible() + await page.screenshot({ + path: 'tests/widgets/staking-migration-widget/test-results/smw-06-error.png', + fullPage: true, + }) +}) From ce66e76c9f631ed5f0df5e2a31d373ff7fe714bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 14:30:04 +0000 Subject: [PATCH 04/27] refactor: unify staking migration states into single journey card --- .../src/MigrationProgressTimeline.tsx | 87 ++++++++-- .../src/MigrationStatusNotice.tsx | 29 ++-- .../src/MigrationStepRow.tsx | 15 +- .../src/MigrationSummaryCard.tsx | 44 +++-- .../src/StakingMigrationWidget.tsx | 154 ++++++++++-------- .../staking-migration-widget/states.spec.ts | 2 +- 6 files changed, 203 insertions(+), 128 deletions(-) diff --git a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx index bbf995a..655971a 100644 --- a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx +++ b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx @@ -1,37 +1,88 @@ import React from 'react' -import { Card, Heading, Text, YStack } from '@goodwidget/ui' +import { Badge, BadgeText, Heading, Text, YStack } from '@goodwidget/ui' import { MigrationStepRow } from './MigrationStepRow' -import type { MigrationStep } from './widgetRuntimeContract' +import type { MigrationStep, StakingMigrationWidgetStatus } from './widgetRuntimeContract' const STEP_ORDER: MigrationStep[] = ['unstake', 'bridge sent', 'bridge received', 'stake'] interface MigrationProgressTimelineProps { + status: StakingMigrationWidgetStatus completedSteps: MigrationStep[] activeStep: MigrationStep | null + failedStep: MigrationStep | null } // This timeline preserves completed steps while advancing exactly one active spinner. export function MigrationProgressTimeline({ + status, completedSteps, activeStep, + failedStep, }: MigrationProgressTimelineProps) { + const approvalCompleted = status === 'migrating' || status === 'success' || status === 'error' + const approvalActive = status === 'approval-pending' + const approvalFailed = status === 'approval-failed' + + const statusBadgeType = + status === 'success' + ? 'success' + : status === 'error' || status === 'approval-failed' + ? 'error' + : status === 'wrong-network' || status === 'missing-config' + ? 'warning' + : 'info' + + const statusLabel = + status === 'success' + ? 'Completed' + : status === 'error' || status === 'approval-failed' + ? 'Failed' + : status === 'approval-pending' || status === 'migrating' + ? 'In progress' + : status === 'wrong-network' + ? 'Action needed' + : 'Ready' + + const timelineDescription = + status === 'success' + ? 'Your staked position was migrated from Fuse staking to Celo savings.' + : status === 'error' + ? 'Migration stopped before completion. Resolve the issue and retry.' + : status === 'approval-failed' + ? 'Approval did not complete. Retry approval to continue.' + : status === 'wrong-network' + ? 'Switch wallet network to Fuse to approve migration.' + : status === 'missing-config' + ? 'Provide migrationApiBaseUrl and migrationOperator before enabling migration.' + : 'Approve migration on Fuse, then backend steps continue automatically.' + return ( - - - Migration in progress - The backend is migrating your position from Fuse staking to Celo savings. - - - {STEP_ORDER.map((step) => ( - - ))} - + + + + {statusLabel} + + Migration journey + {timelineDescription} + + + + + {STEP_ORDER.map((step) => ( + + ))} - + ) } diff --git a/packages/staking-migration-widget/src/MigrationStatusNotice.tsx b/packages/staking-migration-widget/src/MigrationStatusNotice.tsx index 92b28da..f02c1d5 100644 --- a/packages/staking-migration-widget/src/MigrationStatusNotice.tsx +++ b/packages/staking-migration-widget/src/MigrationStatusNotice.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Button, ButtonText, Card, Heading, Text, YStack } from '@goodwidget/ui' +import { Badge, BadgeText, Button, ButtonText, Heading, Text, YStack } from '@goodwidget/ui' interface MigrationStatusNoticeProps { title: string @@ -23,18 +23,19 @@ export function MigrationStatusNotice({ status === 'error' ? '$error' : status === 'warning' ? '$warning' : status === 'success' ? '$success' : '$color' return ( - - - - {title} - - {message} - {actionLabel && onAction && ( - - )} - - + + + {status} + + + {title} + + {message} + {actionLabel && onAction && ( + + )} + ) } diff --git a/packages/staking-migration-widget/src/MigrationStepRow.tsx b/packages/staking-migration-widget/src/MigrationStepRow.tsx index 3e9e5a0..b372b1d 100644 --- a/packages/staking-migration-widget/src/MigrationStepRow.tsx +++ b/packages/staking-migration-widget/src/MigrationStepRow.tsx @@ -1,15 +1,15 @@ import React from 'react' import { Spinner, Text, XStack } from '@goodwidget/ui' -import type { MigrationStep } from './widgetRuntimeContract' interface MigrationStepRowProps { - step: MigrationStep + step: string isCompleted: boolean isActive: boolean + isFailed?: boolean } // This row keeps step visuals deterministic: completed checkmark, one active spinner, or idle bullet. -export function MigrationStepRow({ step, isCompleted, isActive }: MigrationStepRowProps) { +export function MigrationStepRow({ step, isCompleted, isActive, isFailed = false }: MigrationStepRowProps) { return ( @@ -17,6 +17,10 @@ export function MigrationStepRow({ step, isCompleted, isActive }: MigrationStepR + ) : isFailed ? ( + + ! + ) : isActive ? ( ) : ( @@ -29,6 +33,11 @@ export function MigrationStepRow({ step, isCompleted, isActive }: MigrationStepR completed )} + {isFailed && ( + + failed + + )} {isActive && ( in progress diff --git a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx index 2a32538..1062ca5 100644 --- a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx +++ b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Button, ButtonText, Card, Heading, Text, TokenAmount, YStack } from '@goodwidget/ui' +import { Button, ButtonText, Heading, Text, TokenAmount, YStack } from '@goodwidget/ui' interface MigrationSummaryCardProps { stakedAmount: string @@ -20,30 +20,28 @@ export function MigrationSummaryCard({ onPrimaryAction, }: MigrationSummaryCardProps) { return ( - - - Migrate Fuse staking to Celo savings - - Approve migration once, then the backend completes: unstake → bridge sent → bridge received - → stake. - + + Migrate Fuse staking to Celo savings + + Approve migration once, then the backend completes: unstake → bridge sent → bridge received → + stake. + - - - Your staked amount + + + Your staked amount + + + {isZeroBalance && ( + + No staked sG$ found on Fuse for this wallet. - - {isZeroBalance && ( - - No staked sG$ found on Fuse for this wallet. - - )} - - - + )} - + + + ) } diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 86aaca7..64a0a7b 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react' import { GoodWidgetProvider } from '@goodwidget/core' import type { EIP1193Provider } from '@goodwidget/core' -import { ToastContainer, YStack } from '@goodwidget/ui' +import { Card, ToastContainer, YStack } from '@goodwidget/ui' import { MigrationProgressTimeline } from './MigrationProgressTimeline' import { MigrationStatusNotice } from './MigrationStatusNotice' import { MigrationSummaryCard } from './MigrationSummaryCard' @@ -52,78 +52,94 @@ function StakingMigrationInner({ state.isBalanceLoading || isZeroBalance + const shouldShowStatusNotice = + state.status === 'missing-config' || + state.status === 'wrong-network' || + state.status === 'approval-failed' || + state.status === 'error' || + state.status === 'success' + return ( - {state.status === 'missing-config' && ( - - )} - - {state.status === 'wrong-network' && ( - void actions.refresh()} - /> - )} - - {state.status === 'approval-failed' && ( - void actions.retryApproval()} - /> - )} + + + { + if (!state.address) { + void actions.connect() + return + } + void actions.approveAndMigrate() + }} + /> - {(state.status === 'migrating' || state.status === 'error' || state.status === 'success') && ( - - )} + - {state.status === 'error' && ( - void actions.retryMigration()} - /> - )} - - {state.status === 'success' && ( - void actions.refresh()} - /> - )} - - { - if (!state.address) { - void actions.connect() - return - } - void actions.approveAndMigrate() - }} - /> + {shouldShowStatusNotice && ( + void actions.refresh() + : state.status === 'approval-failed' + ? () => void actions.retryApproval() + : state.status === 'error' + ? () => void actions.retryMigration() + : state.status === 'success' + ? () => void actions.refresh() + : undefined + } + /> + )} + + ) } diff --git a/tests/widgets/staking-migration-widget/states.spec.ts b/tests/widgets/staking-migration-widget/states.spec.ts index bb76dc5..df5ac78 100644 --- a/tests/widgets/staking-migration-widget/states.spec.ts +++ b/tests/widgets/staking-migration-widget/states.spec.ts @@ -44,7 +44,7 @@ test('StakingMigrationWidget approval pending notice', async ({ page }) => { test('StakingMigrationWidget migrating timeline', async ({ page }) => { await gotoStory(page, STORY_IDS.migrating) - await expect(page.getByText('Migration in progress')).toBeVisible() + await expect(page.getByText('Migration journey')).toBeVisible() await expect(page.getByText('bridge received')).toBeVisible() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-04-migrating.png', From f09ad55d372781ec89aef1f71381002f9ab5613d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 14:54:10 +0000 Subject: [PATCH 05/27] refactor: evolve staking migration CTA and compact status states --- .../StakingMigrationWidget.stories.tsx | 1 + .../src/MigrationStatusNotice.tsx | 23 ++-- .../src/MigrationSummaryCard.tsx | 39 ++++-- .../src/StakingMigrationWidget.tsx | 124 ++++++++++++------ .../staking-migration-widget/src/adapter.ts | 16 +++ .../src/widgetRuntimeContract.ts | 1 + .../staking-migration-widget/states.spec.ts | 5 + 7 files changed, 144 insertions(+), 65 deletions(-) diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx index d00f257..72fac23 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx @@ -43,6 +43,7 @@ function createAdapterFactory( }, actions: { connect: async () => {}, + switchToFuse: async () => {}, refresh: async () => {}, approveAndMigrate: async () => {}, retryApproval: async () => {}, diff --git a/packages/staking-migration-widget/src/MigrationStatusNotice.tsx b/packages/staking-migration-widget/src/MigrationStatusNotice.tsx index f02c1d5..8629456 100644 --- a/packages/staking-migration-widget/src/MigrationStatusNotice.tsx +++ b/packages/staking-migration-widget/src/MigrationStatusNotice.tsx @@ -1,13 +1,11 @@ import React from 'react' -import { Badge, BadgeText, Button, ButtonText, Heading, Text, YStack } from '@goodwidget/ui' +import { Badge, BadgeText, Heading, Text, YStack } from '@goodwidget/ui' interface MigrationStatusNoticeProps { title: string message: string status: 'error' | 'warning' | 'success' | 'info' - actionLabel?: string - onAction?: () => void - actionDisabled?: boolean + compact?: boolean } // This notice standardizes state-specific messaging and optional recovery actions. @@ -15,13 +13,19 @@ export function MigrationStatusNotice({ title, message, status, - actionLabel, - onAction, - actionDisabled, + compact = false, }: MigrationStatusNoticeProps) { const color = status === 'error' ? '$error' : status === 'warning' ? '$warning' : status === 'success' ? '$success' : '$color' + if (compact) { + return ( + + {title}: {message} + + ) + } + return ( @@ -31,11 +35,6 @@ export function MigrationStatusNotice({ {title} {message} - {actionLabel && onAction && ( - - )} ) } diff --git a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx index 1062ca5..b987770 100644 --- a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx +++ b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx @@ -4,28 +4,32 @@ import { Button, ButtonText, Heading, Text, TokenAmount, YStack } from '@goodwid interface MigrationSummaryCardProps { stakedAmount: string isZeroBalance: boolean - isApprovalPending: boolean - isDisabled: boolean - actionLabel: string - onPrimaryAction: () => void + isCompact?: boolean + actionLabel?: string + actionDisabled?: boolean + actionHint?: string + onPrimaryAction?: () => void } // This summary card is the entry point for approve-and-migrate user action. export function MigrationSummaryCard({ stakedAmount, isZeroBalance, - isApprovalPending, - isDisabled, + isCompact = false, actionLabel, + actionDisabled, + actionHint, onPrimaryAction, }: MigrationSummaryCardProps) { return ( - Migrate Fuse staking to Celo savings - - Approve migration once, then the backend completes: unstake → bridge sent → bridge received → - stake. - + Migrate Fuse staking to Celo savings + {!isCompact && ( + + Approve migration once, then the backend completes: unstake → bridge sent → bridge received → + stake. + + )} @@ -39,9 +43,16 @@ export function MigrationSummaryCard({ )} - + {actionLabel && onPrimaryAction && ( + + )} + {actionHint && ( + + {actionHint} + + )} ) } diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 64a0a7b..bff0156 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -43,14 +43,82 @@ function StakingMigrationInner({ const { state, actions } = activeAdapter const isZeroBalance = state.stakedAmountRaw <= 0n - const isApprovalPending = state.status === 'approval-pending' + const isJourneyPrimaryState = + state.status === 'approval-pending' || + state.status === 'migrating' || + state.status === 'success' || + state.status === 'error' || + state.status === 'approval-failed' + + const summaryAction = useMemo(() => { + if (!state.address) { + return { + label: 'Connect wallet', + disabled: false, + onPress: () => { + void actions.connect() + }, + } + } + + if (!state.hasRequiredConfig || state.isBalanceLoading || isZeroBalance) { + return null + } + + if (state.status === 'wrong-network') { + return { + label: 'Switch to Fuse', + disabled: false, + onPress: () => { + void actions.switchToFuse() + }, + } + } + + if (state.status === 'approval-pending') { + return { + label: 'Approval pending…', + disabled: true, + onPress: () => {}, + } + } + + if (state.status === 'migrating') { + return { + label: 'Migrating…', + disabled: true, + onPress: () => {}, + } + } + + if (state.status === 'success') { + return { + label: 'Refresh balance', + disabled: false, + onPress: () => { + void actions.refresh() + }, + } + } - const isSummaryActionDisabled = - isApprovalPending || - !state.hasRequiredConfig || - state.isWrongNetwork || - state.isBalanceLoading || - isZeroBalance + if (state.status === 'error' || state.status === 'approval-failed') { + return { + label: 'Retry migration', + disabled: false, + onPress: () => { + void actions.retryMigration() + }, + } + } + + return { + label: 'Approve and Migrate', + disabled: false, + onPress: () => { + void actions.approveAndMigrate() + }, + } + }, [actions, isZeroBalance, state.address, state.hasRequiredConfig, state.isBalanceLoading, state.status]) const shouldShowStatusNotice = state.status === 'missing-config' || @@ -66,16 +134,15 @@ function StakingMigrationInner({ { - if (!state.address) { - void actions.connect() - return - } - void actions.approveAndMigrate() - }} + isCompact={isJourneyPrimaryState} + actionLabel={summaryAction?.label} + actionDisabled={summaryAction?.disabled} + actionHint={ + isZeroBalance && state.address + ? 'No staked sG$ available to migrate from Fuse for this wallet.' + : undefined + } + onPrimaryAction={summaryAction?.onPress} /> void actions.refresh() - : state.status === 'approval-failed' - ? () => void actions.retryApproval() - : state.status === 'error' - ? () => void actions.retryMigration() - : state.status === 'success' - ? () => void actions.refresh() - : undefined - } + compact={state.status === 'success' || state.status === 'error'} /> )} diff --git a/packages/staking-migration-widget/src/adapter.ts b/packages/staking-migration-widget/src/adapter.ts index 55f0327..908e2ca 100644 --- a/packages/staking-migration-widget/src/adapter.ts +++ b/packages/staking-migration-widget/src/adapter.ts @@ -624,10 +624,26 @@ export function useStakingMigrationAdapter({ } }, [resolvedConfig, startApprovalAndMigration, state.approvalTxHash, submitMigrationApproval, waitForMigrationCompletion]) + const switchToFuse = useCallback(async () => { + if (!provider) return + + try { + await (provider as EIP1193Provider).request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: `0x${FUSE_CHAIN_ID.toString(16)}` }], + }) + } catch { + // no-op: wallet might not support programmatic switching + } finally { + await refreshStakeState() + } + }, [provider, refreshStakeState]) + return { state, actions: { connect, + switchToFuse, refresh: refreshStakeState, approveAndMigrate: startApprovalAndMigration, retryApproval, diff --git a/packages/staking-migration-widget/src/widgetRuntimeContract.ts b/packages/staking-migration-widget/src/widgetRuntimeContract.ts index beac70e..ef8bf96 100644 --- a/packages/staking-migration-widget/src/widgetRuntimeContract.ts +++ b/packages/staking-migration-widget/src/widgetRuntimeContract.ts @@ -52,6 +52,7 @@ export interface StakingMigrationWidgetState { export interface StakingMigrationWidgetActions { connect: () => Promise + switchToFuse: () => Promise refresh: () => Promise approveAndMigrate: () => Promise retryApproval: () => Promise diff --git a/tests/widgets/staking-migration-widget/states.spec.ts b/tests/widgets/staking-migration-widget/states.spec.ts index df5ac78..6ce7967 100644 --- a/tests/widgets/staking-migration-widget/states.spec.ts +++ b/tests/widgets/staking-migration-widget/states.spec.ts @@ -18,6 +18,7 @@ async function gotoStory(page: Page, storyUrl: string): Promise { test('StakingMigrationWidget empty balance summary', async ({ page }) => { await gotoStory(page, STORY_IDS.empty) await expect(page.getByText('No staked sG$ found on Fuse for this wallet.')).toBeVisible() + await expect(page.getByRole('button', { name: 'Approve and Migrate' })).toHaveCount(0) await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-01-empty-balance.png', fullPage: true, @@ -27,6 +28,7 @@ test('StakingMigrationWidget empty balance summary', async ({ page }) => { test('StakingMigrationWidget wrong network notice', async ({ page }) => { await gotoStory(page, STORY_IDS.wrongNetwork) await expect(page.getByText('Wrong network')).toBeVisible() + await expect(page.getByRole('button', { name: 'Switch to Fuse' })).toBeVisible() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-02-wrong-network.png', fullPage: true, @@ -36,6 +38,7 @@ test('StakingMigrationWidget wrong network notice', async ({ page }) => { test('StakingMigrationWidget approval pending notice', async ({ page }) => { await gotoStory(page, STORY_IDS.approvalPending) await expect(page.getByText('Approval pending…')).toBeVisible() + await expect(page.getByRole('button', { name: 'Approval pending…' })).toBeDisabled() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-03-approval-pending.png', fullPage: true, @@ -55,6 +58,7 @@ test('StakingMigrationWidget migrating timeline', async ({ page }) => { test('StakingMigrationWidget success state', async ({ page }) => { await gotoStory(page, STORY_IDS.success) await expect(page.getByText('Migration complete')).toBeVisible() + await expect(page.getByRole('button', { name: 'Refresh balance' })).toBeVisible() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-05-success.png', fullPage: true, @@ -65,6 +69,7 @@ test('StakingMigrationWidget error state', async ({ page }) => { await gotoStory(page, STORY_IDS.error) await expect(page.getByText('Migration failed')).toBeVisible() await expect(page.getByText('Bridge finalization timeout')).toBeVisible() + await expect(page.getByRole('button', { name: 'Retry migration' })).toBeVisible() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-06-error.png', fullPage: true, From 2e803a34c739e085ee2cd4f8173be75f990b53e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 15:09:21 +0000 Subject: [PATCH 06/27] fix: restore migration summary visual weight during journey states --- .../src/MigrationSummaryCard.tsx | 14 +++++--------- .../src/StakingMigrationWidget.tsx | 7 ------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx index b987770..750f49a 100644 --- a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx +++ b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx @@ -4,7 +4,6 @@ import { Button, ButtonText, Heading, Text, TokenAmount, YStack } from '@goodwid interface MigrationSummaryCardProps { stakedAmount: string isZeroBalance: boolean - isCompact?: boolean actionLabel?: string actionDisabled?: boolean actionHint?: string @@ -15,7 +14,6 @@ interface MigrationSummaryCardProps { export function MigrationSummaryCard({ stakedAmount, isZeroBalance, - isCompact = false, actionLabel, actionDisabled, actionHint, @@ -23,13 +21,11 @@ export function MigrationSummaryCard({ }: MigrationSummaryCardProps) { return ( - Migrate Fuse staking to Celo savings - {!isCompact && ( - - Approve migration once, then the backend completes: unstake → bridge sent → bridge received → - stake. - - )} + Migrate Fuse staking to Celo savings + + Approve migration once, then the backend completes: unstake → bridge sent → bridge received → + stake. + diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index bff0156..4a05f38 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -43,12 +43,6 @@ function StakingMigrationInner({ const { state, actions } = activeAdapter const isZeroBalance = state.stakedAmountRaw <= 0n - const isJourneyPrimaryState = - state.status === 'approval-pending' || - state.status === 'migrating' || - state.status === 'success' || - state.status === 'error' || - state.status === 'approval-failed' const summaryAction = useMemo(() => { if (!state.address) { @@ -134,7 +128,6 @@ function StakingMigrationInner({ Date: Fri, 29 May 2026 15:34:20 +0000 Subject: [PATCH 07/27] feat: add staking migration widget to react demo app --- examples/react-web/package.json | 1 + examples/react-web/src/App.tsx | 6 ++++++ pnpm-lock.yaml | 3 +++ 3 files changed, 10 insertions(+) diff --git a/examples/react-web/package.json b/examples/react-web/package.json index 47fde76..7b9f02d 100644 --- a/examples/react-web/package.json +++ b/examples/react-web/package.json @@ -15,6 +15,7 @@ "@goodwidget/ui": "workspace:*", "@goodwidget/embed": "workspace:*", "@goodwidget/claim-widget-theme-demo": "workspace:*", + "@goodwidget/staking-migration-widget": "workspace:*", "react": "^18.3.0", "react-dom": "^18.3.0", "react-native-web": "^0.19.13" diff --git a/examples/react-web/src/App.tsx b/examples/react-web/src/App.tsx index 39d93fa..54d1156 100644 --- a/examples/react-web/src/App.tsx +++ b/examples/react-web/src/App.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react' import { GoodWidgetProvider, useWallet, useHost } from '@goodwidget/core' import { ClaimWidget } from '@goodwidget/claim-widget-theme-demo' +import { StakingMigrationWidget } from '@goodwidget/staking-migration-widget' import { getThemeManifest, MiniAppShell, @@ -162,6 +163,11 @@ function OverrideShowcase() { + + StakingMigrationWidget: + + + Form Controls diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c96c815..4276ed7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,6 +130,9 @@ importers: '@goodwidget/embed': specifier: workspace:* version: link:../../packages/embed + '@goodwidget/staking-migration-widget': + specifier: workspace:* + version: link:../../packages/staking-migration-widget '@goodwidget/ui': specifier: workspace:* version: link:../../packages/ui From b61df67edb78369dadb30b1faeef02d8118f910e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:45:55 +0000 Subject: [PATCH 08/27] Refine migration step flow visibility --- .../src/MigrationProgressTimeline.tsx | 40 ++++++++++++++++--- .../src/MigrationStepRow.tsx | 17 ++++++-- .../src/StakingMigrationWidget.tsx | 5 +-- .../staking-migration-widget/states.spec.ts | 5 ++- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx index 655971a..8cf7bc3 100644 --- a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx +++ b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Badge, BadgeText, Heading, Text, YStack } from '@goodwidget/ui' +import { Badge, BadgeText, Card, Heading, Text, YStack } from '@goodwidget/ui' import { MigrationStepRow } from './MigrationStepRow' import type { MigrationStep, StakingMigrationWidgetStatus } from './widgetRuntimeContract' @@ -10,6 +10,7 @@ interface MigrationProgressTimelineProps { completedSteps: MigrationStep[] activeStep: MigrationStep | null failedStep: MigrationStep | null + error: string | null } // This timeline preserves completed steps while advancing exactly one active spinner. @@ -18,6 +19,7 @@ export function MigrationProgressTimeline({ completedSteps, activeStep, failedStep, + error, }: MigrationProgressTimelineProps) { const approvalCompleted = status === 'migrating' || status === 'success' || status === 'error' const approvalActive = status === 'approval-pending' @@ -45,16 +47,33 @@ export function MigrationProgressTimeline({ const timelineDescription = status === 'success' - ? 'Your staked position was migrated from Fuse staking to Celo savings.' + ? 'Migration completed and your position is now in Celo savings.' : status === 'error' - ? 'Migration stopped before completion. Resolve the issue and retry.' + ? failedStep + ? `Failed at ${failedStep}: ${error ?? 'Unknown backend error'}` + : error ?? 'Unknown backend error' : status === 'approval-failed' - ? 'Approval did not complete. Retry approval to continue.' + ? error ?? 'Approval did not complete. Retry approval to continue.' : status === 'wrong-network' - ? 'Switch wallet network to Fuse to approve migration.' + ? 'Switch to Fuse to start approval.' : status === 'missing-config' ? 'Provide migrationApiBaseUrl and migrationOperator before enabling migration.' - : 'Approve migration on Fuse, then backend steps continue automatically.' + : 'Approve on Fuse, then migration continues automatically.' + + const currentActionLabel = + status === 'wrong-network' + ? 'Switch to Fuse' + : status === 'approval-pending' + ? 'Approve on Fuse wallet' + : status === 'migrating' + ? activeStep + ? `${activeStep} in progress` + : 'Migration in progress' + : status === 'success' + ? 'Migration complete' + : status === 'error' || status === 'approval-failed' + ? 'Retry migration' + : 'Approve and migrate' return ( @@ -66,6 +85,15 @@ export function MigrationProgressTimeline({ {timelineDescription} + + + + Current action + + {currentActionLabel} + + + + {isCompleted ? ( @@ -26,7 +35,7 @@ export function MigrationStepRow({ step, isCompleted, isActive, isFailed = false ) : ( )} - {step} + {step} {isCompleted && ( @@ -39,8 +48,8 @@ export function MigrationStepRow({ step, isCompleted, isActive, isFailed = false )} {isActive && ( - - in progress + + current )} diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 4a05f38..ab7330f 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -117,9 +117,7 @@ function StakingMigrationInner({ const shouldShowStatusNotice = state.status === 'missing-config' || state.status === 'wrong-network' || - state.status === 'approval-failed' || - state.status === 'error' || - state.status === 'success' + state.status === 'approval-failed' return ( @@ -143,6 +141,7 @@ function StakingMigrationInner({ completedSteps={state.completedSteps} activeStep={state.activeStep} failedStep={state.failedStep} + error={state.error} /> {shouldShowStatusNotice && ( diff --git a/tests/widgets/staking-migration-widget/states.spec.ts b/tests/widgets/staking-migration-widget/states.spec.ts index 6ce7967..6e393b3 100644 --- a/tests/widgets/staking-migration-widget/states.spec.ts +++ b/tests/widgets/staking-migration-widget/states.spec.ts @@ -57,7 +57,8 @@ test('StakingMigrationWidget migrating timeline', async ({ page }) => { test('StakingMigrationWidget success state', async ({ page }) => { await gotoStory(page, STORY_IDS.success) - await expect(page.getByText('Migration complete')).toBeVisible() + await expect(page.getByText('Completed')).toBeVisible() + await expect(page.getByText('Current action')).toBeVisible() await expect(page.getByRole('button', { name: 'Refresh balance' })).toBeVisible() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-05-success.png', @@ -67,7 +68,7 @@ test('StakingMigrationWidget success state', async ({ page }) => { test('StakingMigrationWidget error state', async ({ page }) => { await gotoStory(page, STORY_IDS.error) - await expect(page.getByText('Migration failed')).toBeVisible() + await expect(page.getByText('Failed')).toBeVisible() await expect(page.getByText('Bridge finalization timeout')).toBeVisible() await expect(page.getByRole('button', { name: 'Retry migration' })).toBeVisible() await page.screenshot({ From 188293992681cd4604d59713386fd1303016bedd Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 1 Jun 2026 16:16:14 -0400 Subject: [PATCH 09/27] feat: enhance migration widget with detailed step descriptions and status messages --- .../src/MigrationProgressTimeline.tsx | 88 ++++++++++--- .../src/MigrationStepRow.tsx | 121 ++++++++++++------ .../src/MigrationSummaryCard.tsx | 7 + .../src/StakingMigrationWidget.tsx | 35 +++-- .../staking-migration-widget/states.spec.ts | 2 +- 5 files changed, 185 insertions(+), 68 deletions(-) diff --git a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx index 8cf7bc3..b77ff6c 100644 --- a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx +++ b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Badge, BadgeText, Card, Heading, Text, YStack } from '@goodwidget/ui' +import { Card, Heading, Text, YStack } from '@goodwidget/ui' import { MigrationStepRow } from './MigrationStepRow' import type { MigrationStep, StakingMigrationWidgetStatus } from './widgetRuntimeContract' @@ -13,6 +13,58 @@ interface MigrationProgressTimelineProps { error: string | null } +function formatStepLabel(step: string): string { + return step + .split(' ') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' ') +} + +function getApproveDescription(status: StakingMigrationWidgetStatus, error: string | null): string { + if (status === 'wrong-network') { + return 'Switch to the Fuse network to approve the migration.' + } + + if (status === 'approval-pending') { + return 'Confirm the approval transaction in your wallet.' + } + + if (status === 'approval-failed') { + return error ?? 'Approval did not complete. Retry to continue.' + } + + if (status === 'migrating' || status === 'success' || status === 'error') { + return 'Approval confirmed on Fuse.' + } + + return 'Approve the migration from your Fuse wallet.' +} + +function getStepDescription( + step: MigrationStep, + status: StakingMigrationWidgetStatus, + activeStep: MigrationStep | null, + failedStep: MigrationStep | null, + error: string | null, +): string { + if (failedStep === step) { + return error ?? 'This step failed. Retry the migration to continue.' + } + + if (activeStep === step) { + return 'Currently in progress.' + } + + if (status === 'success' || status === 'error' || status === 'migrating') { + if (step === 'unstake') return 'Release the staked position on Fuse.' + if (step === 'bridge sent') return 'Send the migrated assets from Fuse.' + if (step === 'bridge received') return 'Finalize the bridge transfer on Celo.' + return 'Deposit the migrated assets into Celo savings.' + } + + return 'Pending' +} + // This timeline preserves completed steps while advancing exactly one active spinner. export function MigrationProgressTimeline({ status, @@ -25,15 +77,6 @@ export function MigrationProgressTimeline({ const approvalActive = status === 'approval-pending' const approvalFailed = status === 'approval-failed' - const statusBadgeType = - status === 'success' - ? 'success' - : status === 'error' || status === 'approval-failed' - ? 'error' - : status === 'wrong-network' || status === 'missing-config' - ? 'warning' - : 'info' - const statusLabel = status === 'success' ? 'Completed' @@ -75,12 +118,21 @@ export function MigrationProgressTimeline({ ? 'Retry migration' : 'Approve and migrate' + const statusColor = + status === 'success' + ? '$success' + : status === 'error' || status === 'approval-failed' + ? '$error' + : status === 'wrong-network' || status === 'missing-config' + ? '$warning' + : '$primary' + return ( - - {statusLabel} - + + {statusLabel} + Migration journey {timelineDescription} @@ -96,18 +148,22 @@ export function MigrationProgressTimeline({ - {STEP_ORDER.map((step) => ( + {STEP_ORDER.map((step, index) => ( ))} diff --git a/packages/staking-migration-widget/src/MigrationStepRow.tsx b/packages/staking-migration-widget/src/MigrationStepRow.tsx index bfd4139..0b3c3d5 100644 --- a/packages/staking-migration-widget/src/MigrationStepRow.tsx +++ b/packages/staking-migration-widget/src/MigrationStepRow.tsx @@ -1,57 +1,96 @@ import React from 'react' -import { Spinner, Text, XStack } from '@goodwidget/ui' +import { Icon, Spinner, Text, XStack, YStack, ZStack } from '@goodwidget/ui' interface MigrationStepRowProps { step: string + description?: string isCompleted: boolean isActive: boolean isFailed?: boolean + isFirst?: boolean + isLast?: boolean } -// This row keeps step visuals deterministic: completed checkmark, one active spinner, or idle bullet. -export function MigrationStepRow({ step, isCompleted, isActive, isFailed = false }: MigrationStepRowProps) { +// This row renders a connected vertical stepper marker with stateful copy. +export function MigrationStepRow({ + step, + description, + isCompleted, + isActive, + isFailed = false, + isFirst = false, + isLast = false, +}: MigrationStepRowProps) { + const markerBorderColor = isFailed + ? '$warning' + : isCompleted || isActive + ? '$borderColorFocus' + : '$borderColor' + + const markerBackgroundColor = isCompleted || isActive ? '$backgroundPress' : '$background' + const lineColor = isCompleted ? '$borderColorFocus' : '$borderColor' + const titleColor = isFailed ? '$warning' : isCompleted || isActive ? '$color' : '$placeholderColor' + const statusCopy = isFailed + ? 'Needs attention' + : isCompleted + ? 'Completed' + : isActive + ? 'Current step' + : 'Pending' + return ( - - - {isCompleted ? ( - - ✓ - - ) : isFailed ? ( - - ! + + + + + {isCompleted ? ( + + ) : isFailed ? ( + + ) : isActive ? ( + + ) : null} + + + + + + + {step} + + {description && ( + + {description} - ) : isActive ? ( - - ) : ( - )} - {step} - - {isCompleted && ( - - completed - - )} - {isFailed && ( - - failed - - )} - {isActive && ( - - current + + {statusCopy} - )} + ) } diff --git a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx index 750f49a..bcc7f78 100644 --- a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx +++ b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx @@ -8,6 +8,7 @@ interface MigrationSummaryCardProps { actionDisabled?: boolean actionHint?: string onPrimaryAction?: () => void + statusMessage?: string } // This summary card is the entry point for approve-and-migrate user action. @@ -17,6 +18,7 @@ export function MigrationSummaryCard({ actionLabel, actionDisabled, actionHint, + statusMessage, onPrimaryAction, }: MigrationSummaryCardProps) { return ( @@ -32,6 +34,11 @@ export function MigrationSummaryCard({ Your staked amount + {statusMessage && ( + + {statusMessage} + + )} {isZeroBalance && ( No staked sG$ found on Fuse for this wallet. diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index ab7330f..9b8a91e 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -18,6 +18,14 @@ interface StakingMigrationInnerProps { onMigrationError?: StakingMigrationWidgetProps['onMigrationError'] } +function formatJourneyLabel(label: string | null): string | null { + if (!label) return null + return label + .split(' ') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' ') +} + // This inner component renders all migration states while staying inside provider context. function StakingMigrationInner({ migrationConfig, @@ -70,19 +78,11 @@ function StakingMigrationInner({ } if (state.status === 'approval-pending') { - return { - label: 'Approval pending…', - disabled: true, - onPress: () => {}, - } + return null } if (state.status === 'migrating') { - return { - label: 'Migrating…', - disabled: true, - onPress: () => {}, - } + return null } if (state.status === 'success') { @@ -114,6 +114,20 @@ function StakingMigrationInner({ } }, [actions, isZeroBalance, state.address, state.hasRequiredConfig, state.isBalanceLoading, state.status]) + const summaryStatusMessage = useMemo(() => { + if (state.status === 'approval-pending') { + return 'Waiting for wallet approval on Fuse.' + } + + if (state.status === 'migrating') { + return state.activeStep + ? `${formatJourneyLabel(state.activeStep)} is in progress.` + : 'Migration is in progress.' + } + + return undefined + }, [state.activeStep, state.status]) + const shouldShowStatusNotice = state.status === 'missing-config' || state.status === 'wrong-network' || @@ -128,6 +142,7 @@ function StakingMigrationInner({ isZeroBalance={isZeroBalance} actionLabel={summaryAction?.label} actionDisabled={summaryAction?.disabled} + statusMessage={summaryStatusMessage} actionHint={ isZeroBalance && state.address ? 'No staked sG$ available to migrate from Fuse for this wallet.' diff --git a/tests/widgets/staking-migration-widget/states.spec.ts b/tests/widgets/staking-migration-widget/states.spec.ts index 6e393b3..8d0d74d 100644 --- a/tests/widgets/staking-migration-widget/states.spec.ts +++ b/tests/widgets/staking-migration-widget/states.spec.ts @@ -48,7 +48,7 @@ test('StakingMigrationWidget approval pending notice', async ({ page }) => { test('StakingMigrationWidget migrating timeline', async ({ page }) => { await gotoStory(page, STORY_IDS.migrating) await expect(page.getByText('Migration journey')).toBeVisible() - await expect(page.getByText('bridge received')).toBeVisible() + await expect(page.getByText('Bridge received')).toBeVisible() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-04-migrating.png', fullPage: true, From 70e8caec91bdc896fc264559f582625fd7b203ec Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 1 Jun 2026 16:17:26 -0400 Subject: [PATCH 10/27] feat: improve migration step row styling and status indication --- .../src/MigrationProgressTimeline.tsx | 42 ++++--------------- .../src/MigrationStepRow.tsx | 25 ++++++++++- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx index b77ff6c..3fffdce 100644 --- a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx +++ b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Card, Heading, Text, YStack } from '@goodwidget/ui' +import { Heading, Text, YStack } from '@goodwidget/ui' import { MigrationStepRow } from './MigrationStepRow' import type { MigrationStep, StakingMigrationWidgetStatus } from './widgetRuntimeContract' @@ -103,29 +103,14 @@ export function MigrationProgressTimeline({ ? 'Provide migrationApiBaseUrl and migrationOperator before enabling migration.' : 'Approve on Fuse, then migration continues automatically.' - const currentActionLabel = - status === 'wrong-network' - ? 'Switch to Fuse' - : status === 'approval-pending' - ? 'Approve on Fuse wallet' - : status === 'migrating' - ? activeStep - ? `${activeStep} in progress` - : 'Migration in progress' - : status === 'success' - ? 'Migration complete' - : status === 'error' || status === 'approval-failed' - ? 'Retry migration' - : 'Approve and migrate' - - const statusColor = - status === 'success' - ? '$success' - : status === 'error' || status === 'approval-failed' - ? '$error' - : status === 'wrong-network' || status === 'missing-config' - ? '$warning' - : '$primary' + const statusColor = + status === 'success' + ? '$success' + : status === 'error' || status === 'approval-failed' + ? '$error' + : status === 'wrong-network' || status === 'missing-config' + ? '$warning' + : '$primary' return ( @@ -137,15 +122,6 @@ export function MigrationProgressTimeline({ {timelineDescription} - - - - Current action - - {currentActionLabel} - - - - + {step} @@ -87,7 +104,11 @@ export function MigrationStepRow({ {description} )} - + {statusCopy} From 8c5b9a2ed15803eb2bada9458dccde7602fe8f10 Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 1 Jun 2026 16:54:18 -0400 Subject: [PATCH 11/27] feat: enhance staking migration widget with action handling and improved status messaging --- .../src/MigrationProgressTimeline.tsx | 70 ++++++++++--------- .../src/MigrationStepRow.tsx | 54 +++++++++----- .../src/MigrationSummaryCard.tsx | 59 ++++++++-------- .../src/StakingMigrationWidget.tsx | 33 ++++----- .../staking-migration-widget/states.spec.ts | 10 +-- 5 files changed, 120 insertions(+), 106 deletions(-) diff --git a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx index 3fffdce..8af2cf5 100644 --- a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx +++ b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx @@ -11,9 +11,15 @@ interface MigrationProgressTimelineProps { activeStep: MigrationStep | null failedStep: MigrationStep | null error: string | null + hasAvailableBalance: boolean + actionLabel?: string + onAction?: () => void + actionDisabled?: boolean } -function formatStepLabel(step: string): string { +function formatStepLabel(step: MigrationStep): string { + if (step === 'bridge sent') return 'Bridge to Celo' + if (step === 'stake') return 'Stake on Celo' return step .split(' ') .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) @@ -65,6 +71,13 @@ function getStepDescription( return 'Pending' } +function getStepStatusLabel(status: StakingMigrationWidgetStatus): string | null { + if (status === 'success') return 'Completed' + if (status === 'error' || status === 'approval-failed') return 'Failed' + if (status === 'missing-config') return 'Configuration required' + return null +} + // This timeline preserves completed steps while advancing exactly one active spinner. export function MigrationProgressTimeline({ status, @@ -72,37 +85,18 @@ export function MigrationProgressTimeline({ activeStep, failedStep, error, + hasAvailableBalance, + actionLabel, + onAction, + actionDisabled, }: MigrationProgressTimelineProps) { const approvalCompleted = status === 'migrating' || status === 'success' || status === 'error' - const approvalActive = status === 'approval-pending' + const approvalActive = + ((status === 'summary' || status === 'wrong-network') && hasAvailableBalance) || + status === 'approval-pending' || + status === 'approval-failed' const approvalFailed = status === 'approval-failed' - const statusLabel = - status === 'success' - ? 'Completed' - : status === 'error' || status === 'approval-failed' - ? 'Failed' - : status === 'approval-pending' || status === 'migrating' - ? 'In progress' - : status === 'wrong-network' - ? 'Action needed' - : 'Ready' - - const timelineDescription = - status === 'success' - ? 'Migration completed and your position is now in Celo savings.' - : status === 'error' - ? failedStep - ? `Failed at ${failedStep}: ${error ?? 'Unknown backend error'}` - : error ?? 'Unknown backend error' - : status === 'approval-failed' - ? error ?? 'Approval did not complete. Retry approval to continue.' - : status === 'wrong-network' - ? 'Switch to Fuse to start approval.' - : status === 'missing-config' - ? 'Provide migrationApiBaseUrl and migrationOperator before enabling migration.' - : 'Approve on Fuse, then migration continues automatically.' - const statusColor = status === 'success' ? '$success' @@ -112,20 +106,29 @@ export function MigrationProgressTimeline({ ? '$warning' : '$primary' + const statusLabel = getStepStatusLabel(status) + return ( - - {statusLabel} - Migration journey - {timelineDescription} + {statusLabel && ( + + {statusLabel} + + )} + {!hasAvailableBalance && status === 'summary' && ( + No migration available for this wallet yet. + )} void + actionDisabled?: boolean isCompleted: boolean isActive: boolean isFailed?: boolean @@ -15,6 +18,9 @@ interface MigrationStepRowProps { export function MigrationStepRow({ step, description, + actionLabel, + onAction, + actionDisabled = false, isCompleted, isActive, isFailed = false, @@ -28,7 +34,7 @@ export function MigrationStepRow({ : '$borderColor' const markerBackgroundColor = isCompleted || isActive ? '$backgroundPress' : '$background' - const lineColor = isCompleted ? '$borderColorFocus' : '$borderColor' + const lineColor = isCompleted || isActive ? '$borderColorFocus' : '$borderColor' const titleColor = isFailed ? '$warning' : isCompleted || isActive ? '$color' : '$placeholderColor' const contentBackgroundColor = isActive ? '$backgroundHover' : undefined const contentBorderColor = isFailed @@ -36,6 +42,8 @@ export function MigrationStepRow({ : isActive ? '$borderColorFocus' : 'transparent' + const showDescription = Boolean(description) && (isActive || isFailed) + const showAction = Boolean(actionLabel && onAction) && (isActive || isFailed) const statusCopy = isFailed ? 'Needs attention' : isCompleted @@ -43,10 +51,12 @@ export function MigrationStepRow({ : isActive ? 'Current step' : 'Pending' + const markerSize = 24 + const railOffset = isActive || isFailed ? '$2' : '$1' return ( - + {isCompleted ? ( @@ -78,7 +85,7 @@ export function MigrationStepRow({ @@ -96,21 +103,30 @@ export function MigrationStepRow({ borderColor={contentBorderColor} backgroundColor={contentBackgroundColor} > - + {step} - {description && ( + {showDescription && ( {description} )} - - {statusCopy} - + {showAction && ( + + )} ) diff --git a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx index bcc7f78..6553b16 100644 --- a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx +++ b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx @@ -1,61 +1,62 @@ import React from 'react' -import { Button, ButtonText, Heading, Text, TokenAmount, YStack } from '@goodwidget/ui' +import { YStack, Heading, Text, TokenAmount } from '@goodwidget/ui' interface MigrationSummaryCardProps { stakedAmount: string isZeroBalance: boolean - actionLabel?: string - actionDisabled?: boolean actionHint?: string - onPrimaryAction?: () => void statusMessage?: string } -// This summary card is the entry point for approve-and-migrate user action. +// This summary card keeps the hero area compact and amount-led. export function MigrationSummaryCard({ stakedAmount, isZeroBalance, - actionLabel, - actionDisabled, actionHint, statusMessage, - onPrimaryAction, }: MigrationSummaryCardProps) { return ( - - Migrate Fuse staking to Celo savings - - Approve migration once, then the backend completes: unstake → bridge sent → bridge received → - stake. + + + Migrate Fuse staking to Celo savings + + + Move your assets to the new network to continue earning rewards. - - - Your staked amount + + + Amount to migrate {statusMessage && ( - + {statusMessage} )} {isZeroBalance && ( - + No staked sG$ found on Fuse for this wallet. )} + {actionHint && ( + + {actionHint} + + )} - - {actionLabel && onPrimaryAction && ( - - )} - {actionHint && ( - - {actionHint} - - )} ) } diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 9b8a91e..828681a 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -52,7 +52,11 @@ function StakingMigrationInner({ const { state, actions } = activeAdapter const isZeroBalance = state.stakedAmountRaw <= 0n - const summaryAction = useMemo(() => { + const journeyAction = useMemo(() => { + if (!state.hasRequiredConfig || state.isBalanceLoading || isZeroBalance) { + return null + } + if (!state.address) { return { label: 'Connect wallet', @@ -63,10 +67,6 @@ function StakingMigrationInner({ } } - if (!state.hasRequiredConfig || state.isBalanceLoading || isZeroBalance) { - return null - } - if (state.status === 'wrong-network') { return { label: 'Switch to Fuse', @@ -77,11 +77,7 @@ function StakingMigrationInner({ } } - if (state.status === 'approval-pending') { - return null - } - - if (state.status === 'migrating') { + if (state.status === 'approval-pending' || state.status === 'migrating') { return null } @@ -106,7 +102,7 @@ function StakingMigrationInner({ } return { - label: 'Approve and Migrate', + label: 'Approve and migrate', disabled: false, onPress: () => { void actions.approveAndMigrate() @@ -115,10 +111,6 @@ function StakingMigrationInner({ }, [actions, isZeroBalance, state.address, state.hasRequiredConfig, state.isBalanceLoading, state.status]) const summaryStatusMessage = useMemo(() => { - if (state.status === 'approval-pending') { - return 'Waiting for wallet approval on Fuse.' - } - if (state.status === 'migrating') { return state.activeStep ? `${formatJourneyLabel(state.activeStep)} is in progress.` @@ -129,9 +121,7 @@ function StakingMigrationInner({ }, [state.activeStep, state.status]) const shouldShowStatusNotice = - state.status === 'missing-config' || - state.status === 'wrong-network' || - state.status === 'approval-failed' + state.status === 'missing-config' return ( @@ -140,15 +130,12 @@ function StakingMigrationInner({ {shouldShowStatusNotice && ( diff --git a/tests/widgets/staking-migration-widget/states.spec.ts b/tests/widgets/staking-migration-widget/states.spec.ts index 8d0d74d..0deca9d 100644 --- a/tests/widgets/staking-migration-widget/states.spec.ts +++ b/tests/widgets/staking-migration-widget/states.spec.ts @@ -18,7 +18,8 @@ async function gotoStory(page: Page, storyUrl: string): Promise { test('StakingMigrationWidget empty balance summary', async ({ page }) => { await gotoStory(page, STORY_IDS.empty) await expect(page.getByText('No staked sG$ found on Fuse for this wallet.')).toBeVisible() - await expect(page.getByRole('button', { name: 'Approve and Migrate' })).toHaveCount(0) + await expect(page.getByText('No migration available for this wallet yet.')).toBeVisible() + await expect(page.getByRole('button')).toHaveCount(0) await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-01-empty-balance.png', fullPage: true, @@ -27,7 +28,7 @@ test('StakingMigrationWidget empty balance summary', async ({ page }) => { test('StakingMigrationWidget wrong network notice', async ({ page }) => { await gotoStory(page, STORY_IDS.wrongNetwork) - await expect(page.getByText('Wrong network')).toBeVisible() + await expect(page.getByText('Approve on Fuse')).toBeVisible() await expect(page.getByRole('button', { name: 'Switch to Fuse' })).toBeVisible() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-02-wrong-network.png', @@ -37,8 +38,8 @@ test('StakingMigrationWidget wrong network notice', async ({ page }) => { test('StakingMigrationWidget approval pending notice', async ({ page }) => { await gotoStory(page, STORY_IDS.approvalPending) - await expect(page.getByText('Approval pending…')).toBeVisible() - await expect(page.getByRole('button', { name: 'Approval pending…' })).toBeDisabled() + await expect(page.getByText('Confirm the approval transaction in your wallet.')).toBeVisible() + await expect(page.getByRole('button')).toHaveCount(0) await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-03-approval-pending.png', fullPage: true, @@ -58,7 +59,6 @@ test('StakingMigrationWidget migrating timeline', async ({ page }) => { test('StakingMigrationWidget success state', async ({ page }) => { await gotoStory(page, STORY_IDS.success) await expect(page.getByText('Completed')).toBeVisible() - await expect(page.getByText('Current action')).toBeVisible() await expect(page.getByRole('button', { name: 'Refresh balance' })).toBeVisible() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-05-success.png', From 0b67cdb2a1b9ad35ff06e77b00b97021ff40c9bb Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 1 Jun 2026 17:11:44 -0400 Subject: [PATCH 12/27] feat: enhance migration widget with attention indicators and status messaging --- .../src/MigrationProgressTimeline.tsx | 25 ++++- .../src/MigrationStepRow.tsx | 99 ++++++++++++------- .../src/MigrationSummaryCard.tsx | 30 +++++- .../src/StakingMigrationWidget.tsx | 25 ++--- 4 files changed, 125 insertions(+), 54 deletions(-) diff --git a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx index 8af2cf5..bd46c4a 100644 --- a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx +++ b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Heading, Text, YStack } from '@goodwidget/ui' +import { Button, ButtonText, Heading, Text, YStack } from '@goodwidget/ui' import { MigrationStepRow } from './MigrationStepRow' import type { MigrationStep, StakingMigrationWidgetStatus } from './widgetRuntimeContract' @@ -78,7 +78,6 @@ function getStepStatusLabel(status: StakingMigrationWidgetStatus): string | null return null } -// This timeline preserves completed steps while advancing exactly one active spinner. export function MigrationProgressTimeline({ status, completedSteps, @@ -96,6 +95,8 @@ export function MigrationProgressTimeline({ status === 'approval-pending' || status === 'approval-failed' const approvalFailed = status === 'approval-failed' + const approveNeedsAttention = status === 'wrong-network' || status === 'approval-failed' + const showFooterAction = Boolean(actionLabel && onAction) && status === 'success' const statusColor = status === 'success' @@ -129,6 +130,7 @@ export function MigrationProgressTimeline({ actionLabel={approvalActive || approvalFailed ? actionLabel : undefined} onAction={approvalActive || approvalFailed ? onAction : undefined} actionDisabled={actionDisabled} + needsAttention={approveNeedsAttention} isCompleted={approvalCompleted} isActive={approvalActive} isFailed={approvalFailed} @@ -142,6 +144,7 @@ export function MigrationProgressTimeline({ actionLabel={failedStep === step ? actionLabel : undefined} onAction={failedStep === step ? onAction : undefined} actionDisabled={actionDisabled} + needsAttention={failedStep === step} isCompleted={completedSteps.includes(step)} isActive={activeStep === step} isFailed={failedStep === step} @@ -149,6 +152,24 @@ export function MigrationProgressTimeline({ /> ))} + + {showFooterAction && ( + + )} ) } diff --git a/packages/staking-migration-widget/src/MigrationStepRow.tsx b/packages/staking-migration-widget/src/MigrationStepRow.tsx index fedc066..0445952 100644 --- a/packages/staking-migration-widget/src/MigrationStepRow.tsx +++ b/packages/staking-migration-widget/src/MigrationStepRow.tsx @@ -10,11 +10,11 @@ interface MigrationStepRowProps { isCompleted: boolean isActive: boolean isFailed?: boolean + needsAttention?: boolean isFirst?: boolean isLast?: boolean } -// This row renders a connected vertical stepper marker with stateful copy. export function MigrationStepRow({ step, description, @@ -24,35 +24,43 @@ export function MigrationStepRow({ isCompleted, isActive, isFailed = false, + needsAttention = false, isFirst = false, isLast = false, }: MigrationStepRowProps) { - const markerBorderColor = isFailed + const useAttentionStyle = needsAttention && (isActive || isFailed) + const markerBorderColor = useAttentionStyle ? '$warning' - : isCompleted || isActive - ? '$borderColorFocus' - : '$borderColor' - - const markerBackgroundColor = isCompleted || isActive ? '$backgroundPress' : '$background' - const lineColor = isCompleted || isActive ? '$borderColorFocus' : '$borderColor' - const titleColor = isFailed ? '$warning' : isCompleted || isActive ? '$color' : '$placeholderColor' + : isFailed + ? '$warning' + : isCompleted || isActive + ? '$borderColorFocus' + : '$borderColor' + const markerBackgroundColor = + useAttentionStyle && isActive && !isFailed ? '$background' : isCompleted || isActive ? '$backgroundPress' : '$background' + const lineColor = + useAttentionStyle || isCompleted || isActive ? (useAttentionStyle ? '$warning' : '$borderColorFocus') : '$borderColor' + const titleColor = useAttentionStyle + ? '$warning' + : isFailed + ? '$warning' + : isCompleted || isActive + ? '$color' + : '$placeholderColor' const contentBackgroundColor = isActive ? '$backgroundHover' : undefined - const contentBorderColor = isFailed + const contentBorderColor = useAttentionStyle ? '$warning' - : isActive - ? '$borderColorFocus' - : 'transparent' + : isFailed + ? '$warning' + : isActive + ? '$borderColorFocus' + : 'transparent' const showDescription = Boolean(description) && (isActive || isFailed) const showAction = Boolean(actionLabel && onAction) && (isActive || isFailed) - const statusCopy = isFailed - ? 'Needs attention' - : isCompleted - ? 'Completed' - : isActive - ? 'Current step' - : 'Pending' + const showPendingLabel = !isCompleted && !isActive && !isFailed const markerSize = 24 const railOffset = isActive || isFailed ? '$2' : '$1' + const showActiveSpinner = isActive && !isFailed && !useAttentionStyle return ( @@ -70,7 +78,7 @@ export function MigrationStepRow({ borderRadius="$full" alignItems="center" justifyContent="center" - borderWidth={isActive ? 2 : 1} + borderWidth={isActive || isFailed ? 2 : 1} borderColor={markerBorderColor} backgroundColor={markerBackgroundColor} > @@ -78,7 +86,7 @@ export function MigrationStepRow({ ) : isFailed ? ( - ) : isActive ? ( + ) : showActiveSpinner ? ( ) : null} @@ -96,35 +104,52 @@ export function MigrationStepRow({ gap={isActive ? '$2' : '$1'} paddingTop="$1" paddingBottom={isLast ? '$0' : '$3'} - paddingHorizontal={isActive ? '$3' : '$0'} - paddingVertical={isActive ? '$3' : '$0'} + paddingHorizontal={isActive || isFailed ? '$3' : '$0'} + paddingVertical={isActive || isFailed ? '$3' : '$0'} borderRadius="$3" borderWidth={isActive || isFailed ? 1 : 0} borderColor={contentBorderColor} backgroundColor={contentBackgroundColor} > - - {step} - + + + + {step} + + {useAttentionStyle && isActive && ( + + )} + + {showPendingLabel && ( + + Pending + + )} + {showDescription && ( - + {description} )} {showAction && ( )} diff --git a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx index 6553b16..515fb31 100644 --- a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx +++ b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx @@ -1,23 +1,26 @@ import React from 'react' -import { YStack, Heading, Text, TokenAmount } from '@goodwidget/ui' +import { YStack, Heading, Text, TokenAmount, Icon } from '@goodwidget/ui' interface MigrationSummaryCardProps { stakedAmount: string isZeroBalance: boolean actionHint?: string statusMessage?: string + statusIndicatorLabel?: string } -// This summary card keeps the hero area compact and amount-led. export function MigrationSummaryCard({ stakedAmount, isZeroBalance, actionHint, statusMessage, + statusIndicatorLabel, }: MigrationSummaryCardProps) { + const shouldShowStatusIndicator = Boolean(statusIndicatorLabel) + return ( - + Migrate Fuse staking to Celo savings @@ -41,6 +44,27 @@ export function MigrationSummaryCard({ Amount to migrate + + {shouldShowStatusIndicator && ( + + + + {statusIndicatorLabel} + + + )} + {statusMessage && ( {statusMessage} diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 828681a..5022860 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -125,19 +125,20 @@ function StakingMigrationInner({ return ( - - - + + + Date: Mon, 1 Jun 2026 17:12:04 -0400 Subject: [PATCH 13/27] feat: add migration step marker component and integrate with migration step row --- .../src/MigrationStepMarker.tsx | 56 +++++++++++++++++++ .../src/MigrationStepRow.tsx | 44 +++++---------- .../src/MigrationSummaryCard.tsx | 46 +++++++-------- .../src/StakingMigrationWidget.tsx | 34 ++++++++--- .../staking-migration-widget/states.spec.ts | 4 +- 5 files changed, 121 insertions(+), 63 deletions(-) create mode 100644 packages/staking-migration-widget/src/MigrationStepMarker.tsx diff --git a/packages/staking-migration-widget/src/MigrationStepMarker.tsx b/packages/staking-migration-widget/src/MigrationStepMarker.tsx new file mode 100644 index 0000000..b00b875 --- /dev/null +++ b/packages/staking-migration-widget/src/MigrationStepMarker.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import { Icon, YStack } from '@goodwidget/ui' + +export const MIGRATION_STEP_MARKER_SIZE = 28 + +export type MigrationStepMarkerVariant = 'completed' | 'active' | 'failed' | 'pending' | 'attention' + +interface MigrationStepMarkerProps { + variant: MigrationStepMarkerVariant +} + +export function MigrationStepMarker({ variant }: MigrationStepMarkerProps) { + if (variant === 'pending') { + return ( + + ) + } + + if (variant === 'attention') { + return ( + + ) + } + + const fillColor = variant === 'failed' ? '$warning' : '$borderColorFocus' + const iconName = variant === 'completed' ? 'check' : variant === 'failed' ? 'alert-triangle' : 'loader' + const iconSize = 'md' + + return ( + + + + ) +} diff --git a/packages/staking-migration-widget/src/MigrationStepRow.tsx b/packages/staking-migration-widget/src/MigrationStepRow.tsx index 0445952..bbe41c7 100644 --- a/packages/staking-migration-widget/src/MigrationStepRow.tsx +++ b/packages/staking-migration-widget/src/MigrationStepRow.tsx @@ -1,5 +1,6 @@ import React from 'react' -import { Button, ButtonText, Icon, Spinner, Text, XStack, YStack, ZStack } from '@goodwidget/ui' +import { Button, ButtonText, Icon, Text, XStack, YStack } from '@goodwidget/ui' +import { MIGRATION_STEP_MARKER_SIZE, MigrationStepMarker } from './MigrationStepMarker' interface MigrationStepRowProps { step: string @@ -29,15 +30,6 @@ export function MigrationStepRow({ isLast = false, }: MigrationStepRowProps) { const useAttentionStyle = needsAttention && (isActive || isFailed) - const markerBorderColor = useAttentionStyle - ? '$warning' - : isFailed - ? '$warning' - : isCompleted || isActive - ? '$borderColorFocus' - : '$borderColor' - const markerBackgroundColor = - useAttentionStyle && isActive && !isFailed ? '$background' : isCompleted || isActive ? '$backgroundPress' : '$background' const lineColor = useAttentionStyle || isCompleted || isActive ? (useAttentionStyle ? '$warning' : '$borderColorFocus') : '$borderColor' const titleColor = useAttentionStyle @@ -58,13 +50,20 @@ export function MigrationStepRow({ const showDescription = Boolean(description) && (isActive || isFailed) const showAction = Boolean(actionLabel && onAction) && (isActive || isFailed) const showPendingLabel = !isCompleted && !isActive && !isFailed - const markerSize = 24 const railOffset = isActive || isFailed ? '$2' : '$1' - const showActiveSpinner = isActive && !isFailed && !useAttentionStyle + const markerVariant = isCompleted + ? 'completed' + : isFailed + ? 'failed' + : isActive && useAttentionStyle + ? 'attention' + : isActive + ? 'active' + : 'pending' return ( - + - - {isCompleted ? ( - - ) : isFailed ? ( - - ) : showActiveSpinner ? ( - - ) : null} - + void + showWarningIcon?: boolean } export function MigrationSummaryCard({ @@ -14,10 +17,11 @@ export function MigrationSummaryCard({ isZeroBalance, actionHint, statusMessage, - statusIndicatorLabel, + actionLabel, + actionDisabled, + onPrimaryAction, + showWarningIcon = false, }: MigrationSummaryCardProps) { - const shouldShowStatusIndicator = Boolean(statusIndicatorLabel) - return ( @@ -45,25 +49,21 @@ export function MigrationSummaryCard({ - {shouldShowStatusIndicator && ( - - - - {statusIndicatorLabel} - + {statusMessage && ( diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 5022860..086b68b 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -53,14 +53,22 @@ function StakingMigrationInner({ const isZeroBalance = state.stakedAmountRaw <= 0n const journeyAction = useMemo(() => { + const defaultLabel = 'Approve and migrate' + if (!state.hasRequiredConfig || state.isBalanceLoading || isZeroBalance) { - return null + return { + label: defaultLabel, + disabled: true, + showWarningIcon: false, + onPress: undefined, + } } if (!state.address) { return { label: 'Connect wallet', disabled: false, + showWarningIcon: false, onPress: () => { void actions.connect() }, @@ -71,6 +79,7 @@ function StakingMigrationInner({ return { label: 'Switch to Fuse', disabled: false, + showWarningIcon: true, onPress: () => { void actions.switchToFuse() }, @@ -78,13 +87,19 @@ function StakingMigrationInner({ } if (state.status === 'approval-pending' || state.status === 'migrating') { - return null + return { + label: defaultLabel, + disabled: true, + showWarningIcon: false, + onPress: undefined, + } } if (state.status === 'success') { return { label: 'Refresh balance', disabled: false, + showWarningIcon: false, onPress: () => { void actions.refresh() }, @@ -95,6 +110,7 @@ function StakingMigrationInner({ return { label: 'Retry migration', disabled: false, + showWarningIcon: state.status === 'approval-failed', onPress: () => { void actions.retryMigration() }, @@ -102,8 +118,9 @@ function StakingMigrationInner({ } return { - label: 'Approve and migrate', + label: defaultLabel, disabled: false, + showWarningIcon: false, onPress: () => { void actions.approveAndMigrate() }, @@ -129,7 +146,10 @@ function StakingMigrationInner({ stakedAmount={state.stakedAmount} isZeroBalance={isZeroBalance} statusMessage={summaryStatusMessage} - statusIndicatorLabel={state.status === 'wrong-network' ? journeyAction?.label : undefined} + actionLabel={journeyAction.label} + actionDisabled={journeyAction.disabled} + onPrimaryAction={journeyAction.onPress} + showWarningIcon={journeyAction.showWarningIcon} actionHint={ isZeroBalance && state.address ? 'No staked sG$ available to migrate from Fuse for this wallet.' @@ -146,9 +166,9 @@ function StakingMigrationInner({ failedStep={state.failedStep} error={state.error} hasAvailableBalance={!isZeroBalance} - actionLabel={journeyAction?.label} - actionDisabled={journeyAction?.disabled} - onAction={journeyAction?.onPress} + actionLabel={journeyAction.disabled ? undefined : journeyAction.label} + actionDisabled={journeyAction.disabled} + onAction={journeyAction.onPress} /> {shouldShowStatusNotice && ( diff --git a/tests/widgets/staking-migration-widget/states.spec.ts b/tests/widgets/staking-migration-widget/states.spec.ts index 0deca9d..0ad9c29 100644 --- a/tests/widgets/staking-migration-widget/states.spec.ts +++ b/tests/widgets/staking-migration-widget/states.spec.ts @@ -19,7 +19,7 @@ test('StakingMigrationWidget empty balance summary', async ({ page }) => { await gotoStory(page, STORY_IDS.empty) await expect(page.getByText('No staked sG$ found on Fuse for this wallet.')).toBeVisible() await expect(page.getByText('No migration available for this wallet yet.')).toBeVisible() - await expect(page.getByRole('button')).toHaveCount(0) + await expect(page.getByRole('button', { name: 'Approve and migrate' })).toBeDisabled() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-01-empty-balance.png', fullPage: true, @@ -39,7 +39,7 @@ test('StakingMigrationWidget wrong network notice', async ({ page }) => { test('StakingMigrationWidget approval pending notice', async ({ page }) => { await gotoStory(page, STORY_IDS.approvalPending) await expect(page.getByText('Confirm the approval transaction in your wallet.')).toBeVisible() - await expect(page.getByRole('button')).toHaveCount(0) + await expect(page.getByRole('button', { name: 'Approve and migrate' })).toBeDisabled() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-03-approval-pending.png', fullPage: true, From 54032c9fb7e2901c7c977e4893c136584e86764c Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 1 Jun 2026 17:24:26 -0400 Subject: [PATCH 14/27] feat: add 'Ready' state to StakingMigrationWidget and update related tests --- .../StakingMigrationWidget.stories.tsx | 4 ++ .../src/MigrationProgressTimeline.tsx | 33 +-------- .../src/MigrationStepRow.tsx | 49 +++++-------- .../src/MigrationSummaryCard.tsx | 25 ++----- .../src/StakingMigrationWidget.tsx | 71 ++++++++++++++----- .../staking-migration-widget/states.spec.ts | 36 ++++++---- 6 files changed, 106 insertions(+), 112 deletions(-) diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx index 72fac23..d645adb 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx @@ -99,6 +99,10 @@ export const EmptyBalance: Story = { ), } +export const Ready: Story = { + render: () => , +} + export const WrongNetwork: Story = { render: () => ( void - actionDisabled?: boolean } function formatStepLabel(step: MigrationStep): string { @@ -85,9 +82,6 @@ export function MigrationProgressTimeline({ failedStep, error, hasAvailableBalance, - actionLabel, - onAction, - actionDisabled, }: MigrationProgressTimelineProps) { const approvalCompleted = status === 'migrating' || status === 'success' || status === 'error' const approvalActive = @@ -96,7 +90,6 @@ export function MigrationProgressTimeline({ status === 'approval-failed' const approvalFailed = status === 'approval-failed' const approveNeedsAttention = status === 'wrong-network' || status === 'approval-failed' - const showFooterAction = Boolean(actionLabel && onAction) && status === 'success' const statusColor = status === 'success' @@ -127,9 +120,6 @@ export function MigrationProgressTimeline({ ))} - - {showFooterAction && ( - - )} ) } diff --git a/packages/staking-migration-widget/src/MigrationStepRow.tsx b/packages/staking-migration-widget/src/MigrationStepRow.tsx index bbe41c7..a7df7c8 100644 --- a/packages/staking-migration-widget/src/MigrationStepRow.tsx +++ b/packages/staking-migration-widget/src/MigrationStepRow.tsx @@ -1,13 +1,10 @@ import React from 'react' -import { Button, ButtonText, Icon, Text, XStack, YStack } from '@goodwidget/ui' +import { Icon, Text, XStack, YStack } from '@goodwidget/ui' import { MIGRATION_STEP_MARKER_SIZE, MigrationStepMarker } from './MigrationStepMarker' interface MigrationStepRowProps { step: string description?: string - actionLabel?: string - onAction?: () => void - actionDisabled?: boolean isCompleted: boolean isActive: boolean isFailed?: boolean @@ -19,9 +16,6 @@ interface MigrationStepRowProps { export function MigrationStepRow({ step, description, - actionLabel, - onAction, - actionDisabled = false, isCompleted, isActive, isFailed = false, @@ -48,8 +42,6 @@ export function MigrationStepRow({ ? '$borderColorFocus' : 'transparent' const showDescription = Boolean(description) && (isActive || isFailed) - const showAction = Boolean(actionLabel && onAction) && (isActive || isFailed) - const showPendingLabel = !isCompleted && !isActive && !isFailed const railOffset = isActive || isFailed ? '$2' : '$1' const markerVariant = isCompleted ? 'completed' @@ -60,6 +52,20 @@ export function MigrationStepRow({ : isActive ? 'active' : 'pending' + const statusLabel = isFailed + ? 'Needs attention' + : isCompleted + ? 'Completed' + : isActive + ? 'In progress' + : 'Pending' + const statusColor = isFailed || useAttentionStyle + ? '$warning' + : isCompleted + ? '$success' + : isActive + ? '$primary' + : undefined return ( @@ -106,34 +112,15 @@ export function MigrationStepRow({ )} - {showPendingLabel && ( - - Pending - - )} + + {statusLabel} + {showDescription && ( {description} )} - {showAction && ( - - )} ) diff --git a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx index 6588772..db281b6 100644 --- a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx +++ b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx @@ -3,8 +3,6 @@ import { YStack, Heading, Text, TokenAmount, Button, ButtonText, Icon } from '@g interface MigrationSummaryCardProps { stakedAmount: string - isZeroBalance: boolean - actionHint?: string statusMessage?: string actionLabel: string actionDisabled: boolean @@ -14,8 +12,6 @@ interface MigrationSummaryCardProps { export function MigrationSummaryCard({ stakedAmount, - isZeroBalance, - actionHint, statusMessage, actionLabel, actionDisabled, @@ -57,11 +53,14 @@ export function MigrationSummaryCard({ borderRadius="$full" alignItems="center" justifyContent="center" - paddingHorizontal="$3" + paddingHorizontal="$2" + paddingVertical="$2" > - - {showWarningIcon && } - {actionLabel} + + {showWarningIcon && } + + {actionLabel} + @@ -70,16 +69,6 @@ export function MigrationSummaryCard({ {statusMessage} )} - {isZeroBalance && ( - - No staked sG$ found on Fuse for this wallet. - - )} - {actionHint && ( - - {actionHint} - - )} ) diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 086b68b..17657b4 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -26,7 +26,6 @@ function formatJourneyLabel(label: string | null): string | null { .join(' ') } -// This inner component renders all migration states while staying inside provider context. function StakingMigrationInner({ migrationConfig, adapterFactory, @@ -53,11 +52,27 @@ function StakingMigrationInner({ const isZeroBalance = state.stakedAmountRaw <= 0n const journeyAction = useMemo(() => { - const defaultLabel = 'Approve and migrate' + if (state.isBalanceLoading) { + return { + label: 'Loading...', + disabled: true, + showWarningIcon: false, + onPress: undefined, + } + } + + if (!state.hasRequiredConfig || state.status === 'missing-config') { + return { + label: 'Setup required', + disabled: true, + showWarningIcon: false, + onPress: undefined, + } + } - if (!state.hasRequiredConfig || state.isBalanceLoading || isZeroBalance) { + if (isZeroBalance) { return { - label: defaultLabel, + label: 'No balance', disabled: true, showWarningIcon: false, onPress: undefined, @@ -86,9 +101,18 @@ function StakingMigrationInner({ } } - if (state.status === 'approval-pending' || state.status === 'migrating') { + if (state.status === 'approval-pending') { return { - label: defaultLabel, + label: 'Approval pending', + disabled: true, + showWarningIcon: false, + onPress: undefined, + } + } + + if (state.status === 'migrating') { + return { + label: 'Migrating', disabled: true, showWarningIcon: false, onPress: undefined, @@ -106,11 +130,22 @@ function StakingMigrationInner({ } } - if (state.status === 'error' || state.status === 'approval-failed') { + if (state.status === 'approval-failed') { + return { + label: 'Retry approval', + disabled: false, + showWarningIcon: true, + onPress: () => { + void actions.retryMigration() + }, + } + } + + if (state.status === 'error') { return { label: 'Retry migration', disabled: false, - showWarningIcon: state.status === 'approval-failed', + showWarningIcon: false, onPress: () => { void actions.retryMigration() }, @@ -118,14 +153,21 @@ function StakingMigrationInner({ } return { - label: defaultLabel, + label: 'Approve & Migrate', disabled: false, showWarningIcon: false, onPress: () => { void actions.approveAndMigrate() }, } - }, [actions, isZeroBalance, state.address, state.hasRequiredConfig, state.isBalanceLoading, state.status]) + }, [ + actions, + isZeroBalance, + state.address, + state.hasRequiredConfig, + state.isBalanceLoading, + state.status, + ]) const summaryStatusMessage = useMemo(() => { if (state.status === 'migrating') { @@ -144,17 +186,11 @@ function StakingMigrationInner({ @@ -166,9 +202,6 @@ function StakingMigrationInner({ failedStep={state.failedStep} error={state.error} hasAvailableBalance={!isZeroBalance} - actionLabel={journeyAction.disabled ? undefined : journeyAction.label} - actionDisabled={journeyAction.disabled} - onAction={journeyAction.onPress} /> {shouldShowStatusNotice && ( diff --git a/tests/widgets/staking-migration-widget/states.spec.ts b/tests/widgets/staking-migration-widget/states.spec.ts index 0ad9c29..917c1bb 100644 --- a/tests/widgets/staking-migration-widget/states.spec.ts +++ b/tests/widgets/staking-migration-widget/states.spec.ts @@ -3,6 +3,7 @@ import { expect, test, type Page } from '@playwright/test' // This map keeps each test state tied to one Storybook story for visual smoke coverage. const STORY_IDS = { empty: '/iframe.html?id=widgets-stakingmigrationwidget--empty-balance&viewMode=story', + ready: '/iframe.html?id=widgets-stakingmigrationwidget--ready&viewMode=story', wrongNetwork: '/iframe.html?id=widgets-stakingmigrationwidget--wrong-network&viewMode=story', approvalPending: '/iframe.html?id=widgets-stakingmigrationwidget--approval-pending&viewMode=story', migrating: '/iframe.html?id=widgets-stakingmigrationwidget--migrating&viewMode=story', @@ -17,21 +18,31 @@ async function gotoStory(page: Page, storyUrl: string): Promise { test('StakingMigrationWidget empty balance summary', async ({ page }) => { await gotoStory(page, STORY_IDS.empty) - await expect(page.getByText('No staked sG$ found on Fuse for this wallet.')).toBeVisible() await expect(page.getByText('No migration available for this wallet yet.')).toBeVisible() - await expect(page.getByRole('button', { name: 'Approve and migrate' })).toBeDisabled() + await expect(page.getByRole('button', { name: 'No balance' })).toBeDisabled() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-01-empty-balance.png', fullPage: true, }) }) +test('StakingMigrationWidget ready summary', async ({ page }) => { + await gotoStory(page, STORY_IDS.ready) + await expect(page.getByText('Amount to migrate')).toBeVisible() + await expect(page.getByRole('button', { name: 'Approve & Migrate' })).toBeEnabled() + await page.screenshot({ + path: 'tests/widgets/staking-migration-widget/test-results/smw-02-ready.png', + fullPage: true, + }) +}) + test('StakingMigrationWidget wrong network notice', async ({ page }) => { await gotoStory(page, STORY_IDS.wrongNetwork) await expect(page.getByText('Approve on Fuse')).toBeVisible() - await expect(page.getByRole('button', { name: 'Switch to Fuse' })).toBeVisible() + await expect(page.getByText('In progress')).toBeVisible() + await expect(page.getByRole('button', { name: 'Switch to Fuse' })).toHaveCount(1) await page.screenshot({ - path: 'tests/widgets/staking-migration-widget/test-results/smw-02-wrong-network.png', + path: 'tests/widgets/staking-migration-widget/test-results/smw-03-wrong-network.png', fullPage: true, }) }) @@ -39,9 +50,9 @@ test('StakingMigrationWidget wrong network notice', async ({ page }) => { test('StakingMigrationWidget approval pending notice', async ({ page }) => { await gotoStory(page, STORY_IDS.approvalPending) await expect(page.getByText('Confirm the approval transaction in your wallet.')).toBeVisible() - await expect(page.getByRole('button', { name: 'Approve and migrate' })).toBeDisabled() + await expect(page.getByRole('button', { name: 'Approval pending' })).toBeDisabled() await page.screenshot({ - path: 'tests/widgets/staking-migration-widget/test-results/smw-03-approval-pending.png', + path: 'tests/widgets/staking-migration-widget/test-results/smw-04-approval-pending.png', fullPage: true, }) }) @@ -50,18 +61,19 @@ test('StakingMigrationWidget migrating timeline', async ({ page }) => { await gotoStory(page, STORY_IDS.migrating) await expect(page.getByText('Migration journey')).toBeVisible() await expect(page.getByText('Bridge received')).toBeVisible() + await expect(page.getByRole('button', { name: 'Migrating' })).toBeDisabled() await page.screenshot({ - path: 'tests/widgets/staking-migration-widget/test-results/smw-04-migrating.png', + path: 'tests/widgets/staking-migration-widget/test-results/smw-05-migrating.png', fullPage: true, }) }) test('StakingMigrationWidget success state', async ({ page }) => { await gotoStory(page, STORY_IDS.success) - await expect(page.getByText('Completed')).toBeVisible() - await expect(page.getByRole('button', { name: 'Refresh balance' })).toBeVisible() + await expect(page.getByText('Completed').first()).toBeVisible() + await expect(page.getByRole('button', { name: 'Refresh balance' })).toHaveCount(1) await page.screenshot({ - path: 'tests/widgets/staking-migration-widget/test-results/smw-05-success.png', + path: 'tests/widgets/staking-migration-widget/test-results/smw-06-success.png', fullPage: true, }) }) @@ -70,9 +82,9 @@ test('StakingMigrationWidget error state', async ({ page }) => { await gotoStory(page, STORY_IDS.error) await expect(page.getByText('Failed')).toBeVisible() await expect(page.getByText('Bridge finalization timeout')).toBeVisible() - await expect(page.getByRole('button', { name: 'Retry migration' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Retry migration' })).toHaveCount(1) await page.screenshot({ - path: 'tests/widgets/staking-migration-widget/test-results/smw-06-error.png', + path: 'tests/widgets/staking-migration-widget/test-results/smw-07-error.png', fullPage: true, }) }) From f58b1384fe7adef089b6cd6398fb863f85809165 Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 1 Jun 2026 17:34:41 -0400 Subject: [PATCH 15/27] feat: enhance MigrationProgressTimeline and MigrationStepRow with connector color handling --- .../src/MigrationProgressTimeline.tsx | 60 +++++++++++++++---- .../src/MigrationStepRow.tsx | 50 +++++++++++----- 2 files changed, 81 insertions(+), 29 deletions(-) diff --git a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx index d3d21c7..0887707 100644 --- a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx +++ b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Heading, Text, YStack } from '@goodwidget/ui' -import { MigrationStepRow } from './MigrationStepRow' +import { getStepConnectorColor, MigrationStepRow } from './MigrationStepRow' import type { MigrationStep, StakingMigrationWidgetStatus } from './widgetRuntimeContract' const STEP_ORDER: MigrationStep[] = ['unstake', 'bridge sent', 'bridge received', 'stake'] @@ -102,6 +102,28 @@ export function MigrationProgressTimeline({ const statusLabel = getStepStatusLabel(status) + const approveConnectorBelow = getStepConnectorColor( + approvalCompleted, + approvalFailed, + approvalActive, + approveNeedsAttention, + ) + + const migrationStepStates = STEP_ORDER.map((step) => { + const isCompleted = completedSteps.includes(step) + const isActive = activeStep === step + const isFailed = failedStep === step + const needsAttention = failedStep === step + return { + step, + isCompleted, + isActive, + isFailed, + needsAttention, + connectorBelow: getStepConnectorColor(isCompleted, isFailed, isActive, needsAttention), + } + }) + return ( @@ -126,18 +148,30 @@ export function MigrationProgressTimeline({ isFailed={approvalFailed} isFirst /> - {STEP_ORDER.map((step, index) => ( - - ))} + {migrationStepStates.map((stepState, index) => { + const connectorAbove = + index === 0 ? approveConnectorBelow : migrationStepStates[index - 1].connectorBelow + + return ( + + ) + })} ) diff --git a/packages/staking-migration-widget/src/MigrationStepRow.tsx b/packages/staking-migration-widget/src/MigrationStepRow.tsx index a7df7c8..17e0018 100644 --- a/packages/staking-migration-widget/src/MigrationStepRow.tsx +++ b/packages/staking-migration-widget/src/MigrationStepRow.tsx @@ -2,6 +2,8 @@ import React from 'react' import { Icon, Text, XStack, YStack } from '@goodwidget/ui' import { MIGRATION_STEP_MARKER_SIZE, MigrationStepMarker } from './MigrationStepMarker' +const TIMELINE_ROW_GAP_PX = 8 + interface MigrationStepRowProps { step: string description?: string @@ -11,6 +13,18 @@ interface MigrationStepRowProps { needsAttention?: boolean isFirst?: boolean isLast?: boolean + connectorAboveColor?: string +} + +export function getStepConnectorColor( + isCompleted: boolean, + isFailed: boolean, + isActive: boolean, + needsAttention: boolean, +): string { + if (isCompleted) return '$borderColorFocus' + if (isFailed || (needsAttention && isActive)) return '$warning' + return '$borderColor' } export function MigrationStepRow({ @@ -22,10 +36,10 @@ export function MigrationStepRow({ needsAttention = false, isFirst = false, isLast = false, + connectorAboveColor, }: MigrationStepRowProps) { const useAttentionStyle = needsAttention && (isActive || isFailed) - const lineColor = - useAttentionStyle || isCompleted || isActive ? (useAttentionStyle ? '$warning' : '$borderColorFocus') : '$borderColor' + const connectorBelowColor = getStepConnectorColor(isCompleted, isFailed, isActive, needsAttention) const titleColor = useAttentionStyle ? '$warning' : isFailed @@ -70,21 +84,25 @@ export function MigrationStepRow({ return ( - + {!isFirst && connectorAboveColor && ( + + )} - + {!isLast && ( + + )} Date: Mon, 1 Jun 2026 17:44:35 -0400 Subject: [PATCH 16/27] feat: implement Stepper component and integrate with migration progress timeline --- .../stories/design-system/Stepper.stories.tsx | 88 ++++++ .../src/MigrationProgressTimeline.tsx | 148 ++++----- .../src/MigrationStepMarker.tsx | 56 ---- .../src/MigrationStepRow.tsx | 145 --------- packages/ui/src/components/Stepper.tsx | 286 ++++++++++++++++++ packages/ui/src/index.ts | 2 + 6 files changed, 454 insertions(+), 271 deletions(-) create mode 100644 examples/storybook/src/stories/design-system/Stepper.stories.tsx delete mode 100644 packages/staking-migration-widget/src/MigrationStepMarker.tsx delete mode 100644 packages/staking-migration-widget/src/MigrationStepRow.tsx create mode 100644 packages/ui/src/components/Stepper.tsx diff --git a/examples/storybook/src/stories/design-system/Stepper.stories.tsx b/examples/storybook/src/stories/design-system/Stepper.stories.tsx new file mode 100644 index 0000000..4a54cb1 --- /dev/null +++ b/examples/storybook/src/stories/design-system/Stepper.stories.tsx @@ -0,0 +1,88 @@ +import React, { useMemo, useState } from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import { Button, ButtonText, Stepper, Text, YStack, type StepperStepItem } from '@goodwidget/ui' + +const meta: Meta = { + title: 'Design System/Stepper', + component: Stepper, + tags: ['autodocs'], + parameters: { layout: 'padded' }, +} + +export default meta +type Story = StoryObj + +const BASE_STEPS: StepperStepItem[] = [ + { id: 'connect', title: 'Connect wallet', status: 'completed' }, + { id: 'approve', title: 'Approve transaction', status: 'completed' }, + { id: 'submit', title: 'Submit migration', status: 'active', description: 'Waiting for wallet confirmation.' }, + { id: 'bridge', title: 'Bridge to Celo', status: 'pending' }, + { id: 'stake', title: 'Stake on Celo', status: 'pending' }, + { id: 'confirm', title: 'Confirm receipt', status: 'pending' }, + { id: 'finalize', title: 'Finalize savings', status: 'pending' }, +] + +export const TransactionFlow: Story = { + render: () => ( + + Transaction steps} + maxHeight={280} + /> + + ), +} + +export const InteractiveAdvance: Story = { + render: function InteractiveAdvanceStory() { + const [activeIndex, setActiveIndex] = useState(2) + + const steps = useMemo( + () => + BASE_STEPS.map((step, index) => { + if (index < activeIndex) { + return { ...step, status: 'completed' as const, description: undefined } + } + if (index === activeIndex) { + return { + ...step, + status: 'active' as const, + description: 'Currently in progress.', + } + } + return { ...step, status: 'pending' as const, description: undefined } + }), + [activeIndex], + ) + + const activeStepId = steps[activeIndex]?.id ?? null + + return ( + + Interactive transaction flow} + maxHeight={280} + /> + + + + + + ) + }, +} diff --git a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx index 0887707..8c46983 100644 --- a/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx +++ b/packages/staking-migration-widget/src/MigrationProgressTimeline.tsx @@ -1,6 +1,5 @@ -import React from 'react' -import { Heading, Text, YStack } from '@goodwidget/ui' -import { getStepConnectorColor, MigrationStepRow } from './MigrationStepRow' +import React, { useMemo } from 'react' +import { Heading, Stepper, Text, YStack, type StepperStepItem } from '@goodwidget/ui' import type { MigrationStep, StakingMigrationWidgetStatus } from './widgetRuntimeContract' const STEP_ORDER: MigrationStep[] = ['unstake', 'bridge sent', 'bridge received', 'stake'] @@ -75,6 +74,19 @@ function getStepStatusLabel(status: StakingMigrationWidgetStatus): string | null return null } +function resolveMigrationStepStatus( + isCompleted: boolean, + isActive: boolean, + isFailed: boolean, + needsAttention: boolean, +): StepperStepItem['status'] { + if (isCompleted) return 'completed' + if (isFailed) return 'failed' + if (isActive && needsAttention) return 'attention' + if (isActive) return 'active' + return 'pending' +} + export function MigrationProgressTimeline({ status, completedSteps, @@ -102,77 +114,73 @@ export function MigrationProgressTimeline({ const statusLabel = getStepStatusLabel(status) - const approveConnectorBelow = getStepConnectorColor( + const steps = useMemo(() => { + const approveStatus = resolveMigrationStepStatus( + approvalCompleted, + approvalActive, + approvalFailed, + approveNeedsAttention, + ) + + const migrationSteps = STEP_ORDER.map((step) => { + const isCompleted = completedSteps.includes(step) + const isActive = activeStep === step + const isFailed = failedStep === step + const needsAttention = failedStep === step + + return { + id: step, + title: formatStepLabel(step), + description: getStepDescription(step, status, activeStep, failedStep, error), + status: resolveMigrationStepStatus(isCompleted, isActive, isFailed, needsAttention), + } + }) + + return [ + { + id: 'approve-on-fuse', + title: 'Approve on Fuse', + description: getApproveDescription(status, error), + status: approveStatus, + }, + ...migrationSteps, + ] + }, [ + activeStep, + approvalActive, approvalCompleted, approvalFailed, - approvalActive, approveNeedsAttention, - ) - - const migrationStepStates = STEP_ORDER.map((step) => { - const isCompleted = completedSteps.includes(step) - const isActive = activeStep === step - const isFailed = failedStep === step - const needsAttention = failedStep === step - return { - step, - isCompleted, - isActive, - isFailed, - needsAttention, - connectorBelow: getStepConnectorColor(isCompleted, isFailed, isActive, needsAttention), - } - }) + completedSteps, + error, + failedStep, + status, + ]) + + const activeStepId = useMemo(() => { + if (approvalActive || approvalFailed) return 'approve-on-fuse' + if (failedStep) return failedStep + if (activeStep) return activeStep + return null + }, [activeStep, approvalActive, approvalFailed, failedStep]) return ( - - - Migration journey - {statusLabel && ( - - {statusLabel} - - )} - {!hasAvailableBalance && status === 'summary' && ( - No migration available for this wallet yet. - )} - - - - - {migrationStepStates.map((stepState, index) => { - const connectorAbove = - index === 0 ? approveConnectorBelow : migrationStepStates[index - 1].connectorBelow - - return ( - - ) - })} - - + + Migration journey + {statusLabel && ( + + {statusLabel} + + )} + {!hasAvailableBalance && status === 'summary' && ( + No migration available for this wallet yet. + )} + + } + /> ) } diff --git a/packages/staking-migration-widget/src/MigrationStepMarker.tsx b/packages/staking-migration-widget/src/MigrationStepMarker.tsx deleted file mode 100644 index b00b875..0000000 --- a/packages/staking-migration-widget/src/MigrationStepMarker.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import { Icon, YStack } from '@goodwidget/ui' - -export const MIGRATION_STEP_MARKER_SIZE = 28 - -export type MigrationStepMarkerVariant = 'completed' | 'active' | 'failed' | 'pending' | 'attention' - -interface MigrationStepMarkerProps { - variant: MigrationStepMarkerVariant -} - -export function MigrationStepMarker({ variant }: MigrationStepMarkerProps) { - if (variant === 'pending') { - return ( - - ) - } - - if (variant === 'attention') { - return ( - - ) - } - - const fillColor = variant === 'failed' ? '$warning' : '$borderColorFocus' - const iconName = variant === 'completed' ? 'check' : variant === 'failed' ? 'alert-triangle' : 'loader' - const iconSize = 'md' - - return ( - - - - ) -} diff --git a/packages/staking-migration-widget/src/MigrationStepRow.tsx b/packages/staking-migration-widget/src/MigrationStepRow.tsx deleted file mode 100644 index 17e0018..0000000 --- a/packages/staking-migration-widget/src/MigrationStepRow.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React from 'react' -import { Icon, Text, XStack, YStack } from '@goodwidget/ui' -import { MIGRATION_STEP_MARKER_SIZE, MigrationStepMarker } from './MigrationStepMarker' - -const TIMELINE_ROW_GAP_PX = 8 - -interface MigrationStepRowProps { - step: string - description?: string - isCompleted: boolean - isActive: boolean - isFailed?: boolean - needsAttention?: boolean - isFirst?: boolean - isLast?: boolean - connectorAboveColor?: string -} - -export function getStepConnectorColor( - isCompleted: boolean, - isFailed: boolean, - isActive: boolean, - needsAttention: boolean, -): string { - if (isCompleted) return '$borderColorFocus' - if (isFailed || (needsAttention && isActive)) return '$warning' - return '$borderColor' -} - -export function MigrationStepRow({ - step, - description, - isCompleted, - isActive, - isFailed = false, - needsAttention = false, - isFirst = false, - isLast = false, - connectorAboveColor, -}: MigrationStepRowProps) { - const useAttentionStyle = needsAttention && (isActive || isFailed) - const connectorBelowColor = getStepConnectorColor(isCompleted, isFailed, isActive, needsAttention) - const titleColor = useAttentionStyle - ? '$warning' - : isFailed - ? '$warning' - : isCompleted || isActive - ? '$color' - : '$placeholderColor' - const contentBackgroundColor = isActive ? '$backgroundHover' : undefined - const contentBorderColor = useAttentionStyle - ? '$warning' - : isFailed - ? '$warning' - : isActive - ? '$borderColorFocus' - : 'transparent' - const showDescription = Boolean(description) && (isActive || isFailed) - const railOffset = isActive || isFailed ? '$2' : '$1' - const markerVariant = isCompleted - ? 'completed' - : isFailed - ? 'failed' - : isActive && useAttentionStyle - ? 'attention' - : isActive - ? 'active' - : 'pending' - const statusLabel = isFailed - ? 'Needs attention' - : isCompleted - ? 'Completed' - : isActive - ? 'In progress' - : 'Pending' - const statusColor = isFailed || useAttentionStyle - ? '$warning' - : isCompleted - ? '$success' - : isActive - ? '$primary' - : undefined - - return ( - - - {!isFirst && connectorAboveColor && ( - - )} - - {!isLast && ( - - )} - - - - - - - {step} - - {useAttentionStyle && isActive && ( - - )} - - - {statusLabel} - - - {showDescription && ( - - {description} - - )} - - - ) -} diff --git a/packages/ui/src/components/Stepper.tsx b/packages/ui/src/components/Stepper.tsx new file mode 100644 index 0000000..aaa598a --- /dev/null +++ b/packages/ui/src/components/Stepper.tsx @@ -0,0 +1,286 @@ +import React, { useEffect, useRef } from 'react' +import type { ReactNode } from 'react' +import { Icon } from './Icon' +import { Text } from './Text' +import { XStack, YStack } from '../components-test/Stacks' +import { createComponent } from '../createComponent' + +export const STEPPER_MARKER_SIZE = 28 + +const STEPPER_ROW_GAP_PX = 8 + +export type StepperStepStatus = 'pending' | 'active' | 'completed' | 'failed' | 'attention' + +export interface StepperStepItem { + id: string + title: string + description?: string + status: StepperStepStatus +} + +export interface StepperProps { + steps: StepperStepItem[] + activeStepId?: string | null + header?: ReactNode + maxHeight?: number +} + +export function getStepperConnectorColor(status: StepperStepStatus): string { + if (status === 'completed') return '$borderColorFocus' + if (status === 'failed' || status === 'attention') return '$warning' + return '$borderColor' +} + +function getStepperStatusLabel(status: StepperStepStatus): string { + if (status === 'failed') return 'Needs attention' + if (status === 'completed') return 'Completed' + if (status === 'active' || status === 'attention') return 'In progress' + return 'Pending' +} + +function getStepperStatusColor(status: StepperStepStatus): string | undefined { + if (status === 'failed' || status === 'attention') return '$warning' + if (status === 'completed') return '$success' + if (status === 'active') return '$primary' + return undefined +} + +type StepperMarkerVariant = 'completed' | 'active' | 'failed' | 'pending' | 'attention' + +function getStepperMarkerVariant(status: StepperStepStatus): StepperMarkerVariant { + if (status === 'completed') return 'completed' + if (status === 'failed') return 'failed' + if (status === 'attention') return 'attention' + if (status === 'active') return 'active' + return 'pending' +} + +function StepperMarker({ variant }: { variant: StepperMarkerVariant }) { + if (variant === 'pending') { + return ( + + ) + } + + if (variant === 'attention') { + return ( + + ) + } + + const fillColor = variant === 'failed' ? '$warning' : '$borderColorFocus' + const iconName = variant === 'completed' ? 'check' : variant === 'failed' ? 'alert-triangle' : 'loader' + + return ( + + + + ) +} + +interface StepperStepRowProps { + step: StepperStepItem + isFirst: boolean + isLast: boolean + connectorAboveColor?: string + stepRef: (node: HTMLElement | null) => void +} + +function StepperStepRow({ + step, + isFirst, + isLast, + connectorAboveColor, + stepRef, +}: StepperStepRowProps) { + const isActive = step.status === 'active' || step.status === 'attention' + const isFailed = step.status === 'failed' + const isAttention = step.status === 'attention' + const connectorBelowColor = getStepperConnectorColor(step.status) + const titleColor = isAttention + ? '$warning' + : isFailed + ? '$warning' + : step.status === 'completed' || isActive + ? '$color' + : '$placeholderColor' + const contentBackgroundColor = isActive ? '$backgroundHover' : undefined + const contentBorderColor = isAttention + ? '$warning' + : isFailed + ? '$warning' + : isActive + ? '$borderColorFocus' + : 'transparent' + const showDescription = Boolean(step.description) && (isActive || isFailed) + const railOffset = isActive || isFailed ? '$2' : '$1' + const statusLabel = getStepperStatusLabel(step.status) + const statusColor = getStepperStatusColor(step.status) + + return ( + + + + {!isFirst && connectorAboveColor && ( + + )} + + {!isLast && ( + + )} + + + + + + + {step.title} + + {isAttention && } + + + {statusLabel} + + + {showDescription && ( + + {step.description} + + )} + + + + ) +} + +const STEPPER_SCROLL_HIDE_CLASS = 'gw-stepper-scroll-hide' + +let stepperScrollbarStyleInjected = false + +function ensureStepperScrollbarHidden() { + if (stepperScrollbarStyleInjected || typeof document === 'undefined') return + stepperScrollbarStyleInjected = true + const style = document.createElement('style') + style.id = 'gw-stepper-scroll-hide' + style.textContent = `.${STEPPER_SCROLL_HIDE_CLASS}::-webkit-scrollbar { display: none; width: 0; height: 0; }` + document.head.appendChild(style) +} + +const StepperScrollFrame = createComponent(YStack, { + name: 'Stepper', + width: '100%', + overflow: 'auto' as const, +}) + +function resolveActiveStepId(steps: StepperStepItem[], activeStepId?: string | null): string | null { + if (activeStepId) return activeStepId + const prioritized = steps.find( + (step) => step.status === 'active' || step.status === 'failed' || step.status === 'attention', + ) + return prioritized?.id ?? null +} + +export function Stepper({ steps, activeStepId, header, maxHeight = 360 }: StepperProps) { + const stepRefs = useRef(new Map()) + const resolvedActiveStepId = resolveActiveStepId(steps, activeStepId) + + ensureStepperScrollbarHidden() + + useEffect(() => { + if (!resolvedActiveStepId) return undefined + + const frame = requestAnimationFrame(() => { + const node = stepRefs.current.get(resolvedActiveStepId) + node?.scrollIntoView({ block: 'center', behavior: 'smooth' }) + }) + + return () => cancelAnimationFrame(frame) + }, [resolvedActiveStepId, steps]) + + return ( + + {header} + + + {steps.map((step, index) => { + const connectorAbove = + index === 0 ? undefined : getStepperConnectorColor(steps[index - 1].status) + + return ( + { + if (node) { + stepRefs.current.set(step.id, node) + return + } + stepRefs.current.delete(step.id) + }} + /> + ) + })} + + + + ) +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 27a33dc..6ff6812 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -93,6 +93,8 @@ export { WalletInfo } from './components-test/WalletInfo' // Patterns / Composites export { MiniAppShell } from './components/MiniAppShell' +export { Stepper, getStepperConnectorColor, STEPPER_MARKER_SIZE } from './components/Stepper' +export type { StepperProps, StepperStepItem, StepperStepStatus } from './components/Stepper' export { WidgetTabs } from './components/WidgetTabs' export { ActionSheet } from './components-test/ActionSheet' export { TokenInput } from './components-test/TokenInput' From c6d2e0da3f2472234d8f97640d3120a39f796804 Mon Sep 17 00:00:00 2001 From: blueogin Date: Wed, 3 Jun 2026 11:17:42 -0400 Subject: [PATCH 17/27] refactor: remove 'failed' and 'attention' status from connector color logic in Stepper component --- packages/ui/src/components/Stepper.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/components/Stepper.tsx b/packages/ui/src/components/Stepper.tsx index aaa598a..dd61dff 100644 --- a/packages/ui/src/components/Stepper.tsx +++ b/packages/ui/src/components/Stepper.tsx @@ -27,7 +27,6 @@ export interface StepperProps { export function getStepperConnectorColor(status: StepperStepStatus): string { if (status === 'completed') return '$borderColorFocus' - if (status === 'failed' || status === 'attention') return '$warning' return '$borderColor' } From f7790fa22daa256cd1ba4207ba1b969028ba8b7a Mon Sep 17 00:00:00 2001 From: blueogin Date: Wed, 3 Jun 2026 11:39:20 -0400 Subject: [PATCH 18/27] feat: add animated glyphs for stepper markers and improve visual representation of step statuses --- packages/ui/src/components/Stepper.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/Stepper.tsx b/packages/ui/src/components/Stepper.tsx index dd61dff..84480e8 100644 --- a/packages/ui/src/components/Stepper.tsx +++ b/packages/ui/src/components/Stepper.tsx @@ -7,6 +7,8 @@ import { createComponent } from '../createComponent' export const STEPPER_MARKER_SIZE = 28 +const STEPPER_MARKER_ICON_SIZE = 16 + const STEPPER_ROW_GAP_PX = 8 export type StepperStepStatus = 'pending' | 'active' | 'completed' | 'failed' | 'attention' @@ -84,6 +86,9 @@ function StepperMarker({ variant }: { variant: StepperMarkerVariant }) { const fillColor = variant === 'failed' ? '$warning' : '$borderColorFocus' const iconName = variant === 'completed' ? 'check' : variant === 'failed' ? 'alert-triangle' : 'loader' + const iconNudge = + variant === 'completed' ? { marginTop: 1 } : variant === 'failed' ? { marginTop: 2 } : undefined + return ( - + + + ) } From 514f9d06e9138aae29d7de5fbe479e4405174b8a Mon Sep 17 00:00:00 2001 From: blueogin Date: Wed, 3 Jun 2026 11:39:31 -0400 Subject: [PATCH 19/27] feat: introduce StepperMarkerGlyph for animated stepper icons and enhance step status representation --- packages/ui/src/components/Stepper.tsx | 90 +++++++++++++++++++++----- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/Stepper.tsx b/packages/ui/src/components/Stepper.tsx index 84480e8..37e0870 100644 --- a/packages/ui/src/components/Stepper.tsx +++ b/packages/ui/src/components/Stepper.tsx @@ -7,10 +7,81 @@ import { createComponent } from '../createComponent' export const STEPPER_MARKER_SIZE = 28 -const STEPPER_MARKER_ICON_SIZE = 16 +const STEPPER_GLYPH_SIZE = 16 + +const STEPPER_ACTIVE_GLYPH_SIZE = 20 const STEPPER_ROW_GAP_PX = 8 +let stepperSpinStyleInjected = false + +function ensureStepperSpinStyle() { + if (stepperSpinStyleInjected || typeof document === 'undefined') return + stepperSpinStyleInjected = true + const style = document.createElement('style') + style.id = 'gw-stepper-spin' + style.textContent = + '@keyframes gw-stepper-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }' + document.head.appendChild(style) +} + +const LOADER_GLYPH_PATHS = [ + 'M12 2v4', + 'M12 18v4', + 'M4.93 4.93l2.83 2.83', + 'M16.24 16.24l2.83 2.83', + 'M2 12h4', + 'M18 12h4', + 'M4.93 19.07l2.83-2.83', + 'M16.24 7.76l2.83-2.83', +] + +const STEPPER_GLYPH_STROKE = { + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2.25, + strokeLinecap: 'round' as const, + strokeLinejoin: 'round' as const, +} + +function StepperMarkerGlyph({ variant }: { variant: 'completed' | 'failed' | 'active' }) { + if (variant === 'active') { + ensureStepperSpinStyle() + return ( + + {LOADER_GLYPH_PATHS.map((d, index) => ( + + ))} + + ) + } + + if (variant === 'completed') { + return ( + + + + ) + } + + return ( + + + + + + ) +} + export type StepperStepStatus = 'pending' | 'active' | 'completed' | 'failed' | 'attention' export interface StepperStepItem { @@ -84,10 +155,6 @@ function StepperMarker({ variant }: { variant: StepperMarkerVariant }) { } const fillColor = variant === 'failed' ? '$warning' : '$borderColorFocus' - const iconName = variant === 'completed' ? 'check' : variant === 'failed' ? 'alert-triangle' : 'loader' - - const iconNudge = - variant === 'completed' ? { marginTop: 1 } : variant === 'failed' ? { marginTop: 2 } : undefined return ( - - - + ) } From bf6832ab575440ab4fdbd45a35b51261ebcbefed Mon Sep 17 00:00:00 2001 From: blueogin Date: Wed, 3 Jun 2026 11:55:55 -0400 Subject: [PATCH 20/27] refactor: remove unused MigrationStatusNotice component and related logic from StakingMigrationWidget --- .../stories/design-system/Stepper.stories.tsx | 61 +--- .../StakingMigrationWidget.stories.tsx | 2 - .../src/MigrationStatusNotice.tsx | 40 --- .../src/StakingMigrationWidget.tsx | 43 +-- .../staking-migration-widget/src/adapter.ts | 5 - .../src/useStakingMigrationAdapter.ts | 3 - .../src/widgetRuntimeContract.ts | 1 - packages/ui/src/components/Stepper.tsx | 280 +++++++----------- packages/ui/src/index.ts | 2 +- 9 files changed, 112 insertions(+), 325 deletions(-) delete mode 100644 packages/staking-migration-widget/src/MigrationStatusNotice.tsx delete mode 100644 packages/staking-migration-widget/src/useStakingMigrationAdapter.ts diff --git a/examples/storybook/src/stories/design-system/Stepper.stories.tsx b/examples/storybook/src/stories/design-system/Stepper.stories.tsx index 4a54cb1..41fcc67 100644 --- a/examples/storybook/src/stories/design-system/Stepper.stories.tsx +++ b/examples/storybook/src/stories/design-system/Stepper.stories.tsx @@ -1,6 +1,6 @@ -import React, { useMemo, useState } from 'react' +import React from 'react' import type { Meta, StoryObj } from '@storybook/react' -import { Button, ButtonText, Stepper, Text, YStack, type StepperStepItem } from '@goodwidget/ui' +import { Stepper, Text, YStack, type StepperStepItem } from '@goodwidget/ui' const meta: Meta = { title: 'Design System/Stepper', @@ -12,21 +12,20 @@ const meta: Meta = { export default meta type Story = StoryObj -const BASE_STEPS: StepperStepItem[] = [ +const STEPS: StepperStepItem[] = [ { id: 'connect', title: 'Connect wallet', status: 'completed' }, { id: 'approve', title: 'Approve transaction', status: 'completed' }, { id: 'submit', title: 'Submit migration', status: 'active', description: 'Waiting for wallet confirmation.' }, { id: 'bridge', title: 'Bridge to Celo', status: 'pending' }, { id: 'stake', title: 'Stake on Celo', status: 'pending' }, { id: 'confirm', title: 'Confirm receipt', status: 'pending' }, - { id: 'finalize', title: 'Finalize savings', status: 'pending' }, ] export const TransactionFlow: Story = { render: () => ( Transaction steps} maxHeight={280} @@ -34,55 +33,3 @@ export const TransactionFlow: Story = { ), } - -export const InteractiveAdvance: Story = { - render: function InteractiveAdvanceStory() { - const [activeIndex, setActiveIndex] = useState(2) - - const steps = useMemo( - () => - BASE_STEPS.map((step, index) => { - if (index < activeIndex) { - return { ...step, status: 'completed' as const, description: undefined } - } - if (index === activeIndex) { - return { - ...step, - status: 'active' as const, - description: 'Currently in progress.', - } - } - return { ...step, status: 'pending' as const, description: undefined } - }), - [activeIndex], - ) - - const activeStepId = steps[activeIndex]?.id ?? null - - return ( - - Interactive transaction flow} - maxHeight={280} - /> - - - - - - ) - }, -} diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx index d645adb..c6093dc 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx @@ -9,7 +9,6 @@ import { } from '@goodwidget/staking-migration-widget' import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193' -// This helper builds deterministic adapter snapshots for Storybook state coverage. function createAdapterFactory( status: StakingMigrationWidgetStatus, overrides: { @@ -46,7 +45,6 @@ function createAdapterFactory( switchToFuse: async () => {}, refresh: async () => {}, approveAndMigrate: async () => {}, - retryApproval: async () => {}, retryMigration: async () => {}, }, }) diff --git a/packages/staking-migration-widget/src/MigrationStatusNotice.tsx b/packages/staking-migration-widget/src/MigrationStatusNotice.tsx deleted file mode 100644 index 8629456..0000000 --- a/packages/staking-migration-widget/src/MigrationStatusNotice.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' -import { Badge, BadgeText, Heading, Text, YStack } from '@goodwidget/ui' - -interface MigrationStatusNoticeProps { - title: string - message: string - status: 'error' | 'warning' | 'success' | 'info' - compact?: boolean -} - -// This notice standardizes state-specific messaging and optional recovery actions. -export function MigrationStatusNotice({ - title, - message, - status, - compact = false, -}: MigrationStatusNoticeProps) { - const color = - status === 'error' ? '$error' : status === 'warning' ? '$warning' : status === 'success' ? '$success' : '$color' - - if (compact) { - return ( - - {title}: {message} - - ) - } - - return ( - - - {status} - - - {title} - - {message} - - ) -} diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 17657b4..2c9ed82 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -1,9 +1,8 @@ import React, { useMemo } from 'react' import { GoodWidgetProvider } from '@goodwidget/core' import type { EIP1193Provider } from '@goodwidget/core' -import { Card, ToastContainer, YStack } from '@goodwidget/ui' +import { Card, Text, ToastContainer, YStack } from '@goodwidget/ui' import { MigrationProgressTimeline } from './MigrationProgressTimeline' -import { MigrationStatusNotice } from './MigrationStatusNotice' import { MigrationSummaryCard } from './MigrationSummaryCard' import { useStakingMigrationAdapter } from './adapter' import type { @@ -179,9 +178,6 @@ function StakingMigrationInner({ return undefined }, [state.activeStep, state.status]) - const shouldShowStatusNotice = - state.status === 'missing-config' - return ( - {shouldShowStatusNotice && ( - + {state.status === 'missing-config' && ( + + Missing migration configuration: Provide migrationApiBaseUrl + and migrationOperator in migrationConfig before enabling migration. + )} @@ -242,7 +212,6 @@ function StakingMigrationInner({ ) } -// This is the public React widget entrypoint with provider-first mounting. export function StakingMigrationWidget({ provider, config, diff --git a/packages/staking-migration-widget/src/adapter.ts b/packages/staking-migration-widget/src/adapter.ts index 908e2ca..a3853d6 100644 --- a/packages/staking-migration-widget/src/adapter.ts +++ b/packages/staking-migration-widget/src/adapter.ts @@ -583,10 +583,6 @@ export function useStakingMigrationAdapter({ walletClient, ]) - const retryApproval = useCallback(async () => { - await startApprovalAndMigration() - }, [startApprovalAndMigration]) - const retryMigration = useCallback(async () => { if (!state.approvalTxHash) { await startApprovalAndMigration() @@ -646,7 +642,6 @@ export function useStakingMigrationAdapter({ switchToFuse, refresh: refreshStakeState, approveAndMigrate: startApprovalAndMigration, - retryApproval, retryMigration, }, } diff --git a/packages/staking-migration-widget/src/useStakingMigrationAdapter.ts b/packages/staking-migration-widget/src/useStakingMigrationAdapter.ts deleted file mode 100644 index 3fc8136..0000000 --- a/packages/staking-migration-widget/src/useStakingMigrationAdapter.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file keeps a dedicated hook entrypoint for integrators expecting use* naming. -export { useStakingMigrationAdapter } from './adapter' -export type { UseStakingMigrationAdapterOptions } from './adapter' diff --git a/packages/staking-migration-widget/src/widgetRuntimeContract.ts b/packages/staking-migration-widget/src/widgetRuntimeContract.ts index ef8bf96..f5eb6af 100644 --- a/packages/staking-migration-widget/src/widgetRuntimeContract.ts +++ b/packages/staking-migration-widget/src/widgetRuntimeContract.ts @@ -55,7 +55,6 @@ export interface StakingMigrationWidgetActions { switchToFuse: () => Promise refresh: () => Promise approveAndMigrate: () => Promise - retryApproval: () => Promise retryMigration: () => Promise } diff --git a/packages/ui/src/components/Stepper.tsx b/packages/ui/src/components/Stepper.tsx index 37e0870..8f21fc7 100644 --- a/packages/ui/src/components/Stepper.tsx +++ b/packages/ui/src/components/Stepper.tsx @@ -5,82 +5,8 @@ import { Text } from './Text' import { XStack, YStack } from '../components-test/Stacks' import { createComponent } from '../createComponent' -export const STEPPER_MARKER_SIZE = 28 - -const STEPPER_GLYPH_SIZE = 16 - -const STEPPER_ACTIVE_GLYPH_SIZE = 20 - -const STEPPER_ROW_GAP_PX = 8 - -let stepperSpinStyleInjected = false - -function ensureStepperSpinStyle() { - if (stepperSpinStyleInjected || typeof document === 'undefined') return - stepperSpinStyleInjected = true - const style = document.createElement('style') - style.id = 'gw-stepper-spin' - style.textContent = - '@keyframes gw-stepper-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }' - document.head.appendChild(style) -} - -const LOADER_GLYPH_PATHS = [ - 'M12 2v4', - 'M12 18v4', - 'M4.93 4.93l2.83 2.83', - 'M16.24 16.24l2.83 2.83', - 'M2 12h4', - 'M18 12h4', - 'M4.93 19.07l2.83-2.83', - 'M16.24 7.76l2.83-2.83', -] - -const STEPPER_GLYPH_STROKE = { - fill: 'none', - stroke: 'currentColor', - strokeWidth: 2.25, - strokeLinecap: 'round' as const, - strokeLinejoin: 'round' as const, -} - -function StepperMarkerGlyph({ variant }: { variant: 'completed' | 'failed' | 'active' }) { - if (variant === 'active') { - ensureStepperSpinStyle() - return ( - - {LOADER_GLYPH_PATHS.map((d, index) => ( - - ))} - - ) - } - - if (variant === 'completed') { - return ( - - - - ) - } - - return ( - - - - - - ) -} +const MARKER_SIZE = 28 +const ROW_GAP_PX = 8 export type StepperStepStatus = 'pending' | 'active' | 'completed' | 'failed' | 'attention' @@ -98,28 +24,28 @@ export interface StepperProps { maxHeight?: number } -export function getStepperConnectorColor(status: StepperStepStatus): string { +function connectorColor(status: StepperStepStatus): string { if (status === 'completed') return '$borderColorFocus' return '$borderColor' } -function getStepperStatusLabel(status: StepperStepStatus): string { +function statusLabel(status: StepperStepStatus): string { if (status === 'failed') return 'Needs attention' if (status === 'completed') return 'Completed' if (status === 'active' || status === 'attention') return 'In progress' return 'Pending' } -function getStepperStatusColor(status: StepperStepStatus): string | undefined { +function statusColor(status: StepperStepStatus): string | undefined { if (status === 'failed' || status === 'attention') return '$warning' if (status === 'completed') return '$success' if (status === 'active') return '$primary' return undefined } -type StepperMarkerVariant = 'completed' | 'active' | 'failed' | 'pending' | 'attention' +type MarkerVariant = 'completed' | 'active' | 'failed' | 'pending' | 'attention' -function getStepperMarkerVariant(status: StepperStepStatus): StepperMarkerVariant { +function markerVariant(status: StepperStepStatus): MarkerVariant { if (status === 'completed') return 'completed' if (status === 'failed') return 'failed' if (status === 'attention') return 'attention' @@ -127,12 +53,12 @@ function getStepperMarkerVariant(status: StepperStepStatus): StepperMarkerVarian return 'pending' } -function StepperMarker({ variant }: { variant: StepperMarkerVariant }) { +function StepperMarker({ variant }: { variant: MarkerVariant }) { if (variant === 'pending') { return ( + ) : variant === 'failed' ? ( + + ) : ( + + ) return ( - + {glyph} ) } @@ -191,7 +123,7 @@ function StepperStepRow({ const isActive = step.status === 'active' || step.status === 'attention' const isFailed = step.status === 'failed' const isAttention = step.status === 'attention' - const connectorBelowColor = getStepperConnectorColor(step.status) + const connectorBelowColor = connectorColor(step.status) const titleColor = isAttention ? '$warning' : isFailed @@ -209,85 +141,80 @@ function StepperStepRow({ : 'transparent' const showDescription = Boolean(step.description) && (isActive || isFailed) const railOffset = isActive || isFailed ? '$2' : '$1' - const statusLabel = getStepperStatusLabel(step.status) - const statusColor = getStepperStatusColor(step.status) return ( - + - - {!isFirst && connectorAboveColor && ( - - )} - - {!isLast && ( - - )} - + + {!isFirst && connectorAboveColor && ( + + )} + + {!isLast && ( + + )} + - - - - - {step.title} + + + + + {step.title} + + {isAttention && } + + + {statusLabel(step.status)} - {isAttention && } - - {statusLabel} - - - {showDescription && ( - - {step.description} - - )} - + {showDescription && ( + + {step.description} + + )} + ) } -const STEPPER_SCROLL_HIDE_CLASS = 'gw-stepper-scroll-hide' +const SCROLL_HIDE_CLASS = 'gw-stepper-scroll-hide' -let stepperScrollbarStyleInjected = false +let scrollbarStyleInjected = false -function ensureStepperScrollbarHidden() { - if (stepperScrollbarStyleInjected || typeof document === 'undefined') return - stepperScrollbarStyleInjected = true +function ensureScrollbarHidden() { + if (scrollbarStyleInjected || typeof document === 'undefined') return + scrollbarStyleInjected = true const style = document.createElement('style') style.id = 'gw-stepper-scroll-hide' - style.textContent = `.${STEPPER_SCROLL_HIDE_CLASS}::-webkit-scrollbar { display: none; width: 0; height: 0; }` + style.textContent = `.${SCROLL_HIDE_CLASS}::-webkit-scrollbar { display: none; width: 0; height: 0; }` document.head.appendChild(style) } @@ -309,7 +236,7 @@ export function Stepper({ steps, activeStepId, header, maxHeight = 360 }: Steppe const stepRefs = useRef(new Map()) const resolvedActiveStepId = resolveActiveStepId(steps, activeStepId) - ensureStepperScrollbarHidden() + ensureScrollbarHidden() useEffect(() => { if (!resolvedActiveStepId) return undefined @@ -327,31 +254,26 @@ export function Stepper({ steps, activeStepId, header, maxHeight = 360 }: Steppe {header} - {steps.map((step, index) => { - const connectorAbove = - index === 0 ? undefined : getStepperConnectorColor(steps[index - 1].status) - - return ( - { - if (node) { - stepRefs.current.set(step.id, node) - return - } - stepRefs.current.delete(step.id) - }} - /> - ) - })} + {steps.map((step, index) => ( + { + if (node) { + stepRefs.current.set(step.id, node) + return + } + stepRefs.current.delete(step.id) + }} + /> + ))} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 6ff6812..7fb3adc 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -93,7 +93,7 @@ export { WalletInfo } from './components-test/WalletInfo' // Patterns / Composites export { MiniAppShell } from './components/MiniAppShell' -export { Stepper, getStepperConnectorColor, STEPPER_MARKER_SIZE } from './components/Stepper' +export { Stepper } from './components/Stepper' export type { StepperProps, StepperStepItem, StepperStepStatus } from './components/Stepper' export { WidgetTabs } from './components/WidgetTabs' export { ActionSheet } from './components-test/ActionSheet' From 58f3b996db4ca11da806c4767b83f27b38e1c1fc Mon Sep 17 00:00:00 2001 From: blueogin Date: Wed, 3 Jun 2026 13:04:21 -0400 Subject: [PATCH 21/27] feat: replace button with CircularActionButton in MigrationSummaryCard and update action handling in StakingMigrationWidget --- .../src/MigrationSummaryCard.tsx | 43 +++---- .../src/StakingMigrationWidget.tsx | 19 +--- .../src/components/CircularActionButton.tsx | 106 ++++++++++++++++++ packages/ui/src/index.ts | 2 + 4 files changed, 128 insertions(+), 42 deletions(-) create mode 100644 packages/ui/src/components/CircularActionButton.tsx diff --git a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx index db281b6..6625c9b 100644 --- a/packages/staking-migration-widget/src/MigrationSummaryCard.tsx +++ b/packages/staking-migration-widget/src/MigrationSummaryCard.tsx @@ -1,22 +1,23 @@ import React from 'react' -import { YStack, Heading, Text, TokenAmount, Button, ButtonText, Icon } from '@goodwidget/ui' +import { YStack, Heading, Text, TokenAmount, CircularActionButton } from '@goodwidget/ui' + +export interface MigrationSummaryAction { + label: string + disabled: boolean + pending?: boolean + onPress?: () => void +} interface MigrationSummaryCardProps { stakedAmount: string statusMessage?: string - actionLabel: string - actionDisabled: boolean - onPrimaryAction?: () => void - showWarningIcon?: boolean + action: MigrationSummaryAction } export function MigrationSummaryCard({ stakedAmount, statusMessage, - actionLabel, - actionDisabled, - onPrimaryAction, - showWarningIcon = false, + action, }: MigrationSummaryCardProps) { return ( @@ -45,24 +46,12 @@ export function MigrationSummaryCard({ - + {statusMessage && ( diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 2c9ed82..a952945 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -55,7 +55,7 @@ function StakingMigrationInner({ return { label: 'Loading...', disabled: true, - showWarningIcon: false, + pending: true, onPress: undefined, } } @@ -64,7 +64,6 @@ function StakingMigrationInner({ return { label: 'Setup required', disabled: true, - showWarningIcon: false, onPress: undefined, } } @@ -73,7 +72,6 @@ function StakingMigrationInner({ return { label: 'No balance', disabled: true, - showWarningIcon: false, onPress: undefined, } } @@ -82,7 +80,6 @@ function StakingMigrationInner({ return { label: 'Connect wallet', disabled: false, - showWarningIcon: false, onPress: () => { void actions.connect() }, @@ -93,7 +90,6 @@ function StakingMigrationInner({ return { label: 'Switch to Fuse', disabled: false, - showWarningIcon: true, onPress: () => { void actions.switchToFuse() }, @@ -104,7 +100,7 @@ function StakingMigrationInner({ return { label: 'Approval pending', disabled: true, - showWarningIcon: false, + pending: true, onPress: undefined, } } @@ -113,7 +109,7 @@ function StakingMigrationInner({ return { label: 'Migrating', disabled: true, - showWarningIcon: false, + pending: true, onPress: undefined, } } @@ -122,7 +118,6 @@ function StakingMigrationInner({ return { label: 'Refresh balance', disabled: false, - showWarningIcon: false, onPress: () => { void actions.refresh() }, @@ -133,7 +128,6 @@ function StakingMigrationInner({ return { label: 'Retry approval', disabled: false, - showWarningIcon: true, onPress: () => { void actions.retryMigration() }, @@ -144,7 +138,6 @@ function StakingMigrationInner({ return { label: 'Retry migration', disabled: false, - showWarningIcon: false, onPress: () => { void actions.retryMigration() }, @@ -154,7 +147,6 @@ function StakingMigrationInner({ return { label: 'Approve & Migrate', disabled: false, - showWarningIcon: false, onPress: () => { void actions.approveAndMigrate() }, @@ -183,10 +175,7 @@ function StakingMigrationInner({ diff --git a/packages/ui/src/components/CircularActionButton.tsx b/packages/ui/src/components/CircularActionButton.tsx new file mode 100644 index 0000000..9efb302 --- /dev/null +++ b/packages/ui/src/components/CircularActionButton.tsx @@ -0,0 +1,106 @@ +import React from 'react' +import { createComponent } from '../createComponent' +import { ButtonFrame, ButtonText } from './Button' +import { Spinner } from '../components-test/Spinner' +import { XStack, YStack } from '../components-test/Stacks' + +const ClaimActionButton = createComponent(ButtonFrame, { + name: 'ClaimActionButton', + extends: 'Button', + width: 160, + height: 160, + borderRadius: 9999, + backgroundColor: '$backgroundTransparent', + borderWidth: 0, + shadowOpacity: 0, + overflow: 'visible' as const, + position: 'relative' as const, + paddingHorizontal: 0, + hoverStyle: { backgroundColor: '$backgroundTransparent' }, + pressStyle: { backgroundColor: '$backgroundTransparent', opacity: 0.95 }, + focusStyle: { backgroundColor: '$backgroundTransparent', outlineStyle: 'none' }, +}) + +const ClaimActionGlow = createComponent(YStack, { + name: 'ClaimActionGlow', + position: 'absolute' as const, + top: -16, + right: -16, + bottom: -16, + left: -16, + borderRadius: 9999, + backgroundColor: '$primary', + opacity: 0.45, +}) + +const ClaimActionRing = createComponent(YStack, { + name: 'ClaimActionRing', + position: 'absolute' as const, + top: 0, + right: 0, + bottom: 0, + left: 0, + borderRadius: 9999, + backgroundColor: '$primary', +}) + +const ClaimActionInner = createComponent(YStack, { + name: 'ClaimActionInner', + position: 'absolute' as const, + top: 2, + right: 2, + bottom: 2, + left: 2, + borderRadius: 9999, + backgroundColor: '$backgroundDark', +}) + +export interface CircularActionButtonProps { + label: string + disabled?: boolean + pending?: boolean + onPress?: () => void +} + +export function CircularActionButton({ + label, + disabled = false, + pending = false, + onPress, +}: CircularActionButtonProps) { + const labelColor = pending || disabled ? '$grey600' : '$primary' + + return ( + + + + + + + {pending ? ( + + + {label} + + + + ) : ( + + {label} + + )} + + + ) +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 7fb3adc..17ad19b 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -93,6 +93,8 @@ export { WalletInfo } from './components-test/WalletInfo' // Patterns / Composites export { MiniAppShell } from './components/MiniAppShell' +export { CircularActionButton } from './components/CircularActionButton' +export type { CircularActionButtonProps } from './components/CircularActionButton' export { Stepper } from './components/Stepper' export type { StepperProps, StepperStepItem, StepperStepStatus } from './components/Stepper' export { WidgetTabs } from './components/WidgetTabs' From b65605ae126e9d2f79e8feb9cecc6505573f7367 Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 8 Jun 2026 16:06:41 -0400 Subject: [PATCH 22/27] feat: enhance StakingMigrationWidget with environment configuration and primary action handling --- examples/react-web/src/App.tsx | 2 +- .../StakingMigrationWidget.stories.tsx | 47 +- .../src/StakingMigrationWidget.tsx | 171 ++-- .../staking-migration-widget/src/adapter.ts | 761 ++++++++++++------ .../staking-migration-widget/src/index.ts | 12 +- .../src/integration.ts | 21 + .../src/migrationEnvironments.ts | 48 ++ .../src/widgetRuntimeContract.ts | 27 +- 8 files changed, 687 insertions(+), 402 deletions(-) create mode 100644 packages/staking-migration-widget/src/integration.ts create mode 100644 packages/staking-migration-widget/src/migrationEnvironments.ts diff --git a/examples/react-web/src/App.tsx b/examples/react-web/src/App.tsx index 54d1156..12f2eaf 100644 --- a/examples/react-web/src/App.tsx +++ b/examples/react-web/src/App.tsx @@ -165,7 +165,7 @@ function OverrideShowcase() { StakingMigrationWidget: - + diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx index c6093dc..74c9c82 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx @@ -4,11 +4,38 @@ import { YStack } from '@goodwidget/ui' import { StakingMigrationWidget, type MigrationStep, + type StakingMigrationPrimaryAction, type StakingMigrationWidgetAdapterFactory, type StakingMigrationWidgetStatus, } from '@goodwidget/staking-migration-widget' import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193' +function deriveMockPrimary( + status: StakingMigrationWidgetStatus, + stakedAmountRaw: bigint, +): { primaryAction: StakingMigrationPrimaryAction; primaryLabel: string } { + if (stakedAmountRaw <= 0n) { + return { primaryAction: 'none', primaryLabel: 'No balance' } + } + + switch (status) { + case 'wrong-network': + return { primaryAction: 'switch_chain', primaryLabel: 'Switch to Fuse' } + case 'approval-pending': + return { primaryAction: 'none', primaryLabel: 'Approval pending' } + case 'migrating': + return { primaryAction: 'none', primaryLabel: 'Migrating' } + case 'success': + return { primaryAction: 'refresh', primaryLabel: 'Refresh balance' } + case 'approval-failed': + return { primaryAction: 'retry', primaryLabel: 'Retry approval' } + case 'error': + return { primaryAction: 'retry', primaryLabel: 'Retry migration' } + default: + return { primaryAction: 'migrate', primaryLabel: 'Approve & Migrate' } + } +} + function createAdapterFactory( status: StakingMigrationWidgetStatus, overrides: { @@ -22,13 +49,17 @@ function createAdapterFactory( isWrongNetwork?: boolean } = {}, ): StakingMigrationWidgetAdapterFactory { - return () => ({ + return () => { + const stakedAmountRaw = overrides.stakedAmountRaw ?? 250000n + const primary = deriveMockPrimary(status, stakedAmountRaw) + + return { state: { status, address: '0x329377cbeeF39f01b0Ea04B80465c9eB47D3ED1', chainId: 122, stakedAmount: overrides.stakedAmount ?? '2500', - stakedAmountRaw: overrides.stakedAmountRaw ?? 250000n, + stakedAmountRaw, stakedTokenSymbol: 'sG$', hasRequiredConfig: overrides.hasRequiredConfig ?? true, isWrongNetwork: overrides.isWrongNetwork ?? false, @@ -39,6 +70,8 @@ function createAdapterFactory( approvalTxHash: '0xapprovalhash', migrationId: 'migration-1', error: overrides.error ?? null, + primaryAction: primary.primaryAction, + primaryLabel: primary.primaryLabel, }, actions: { connect: async () => {}, @@ -47,22 +80,18 @@ function createAdapterFactory( approveAndMigrate: async () => {}, retryMigration: async () => {}, }, - }) + } + } } function StoryShell({ adapterFactory }: { adapterFactory: StakingMigrationWidgetAdapterFactory }) { - const migrationConfig = { - migrationApiBaseUrl: 'https://api.example.com', - migrationOperator: '0x1234567890123456789012345678901234567890' as const, - } - try { const provider = createCustodialEip1193Provider() return ( diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index a952945..b806bf2 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { GoodWidgetProvider } from '@goodwidget/core' import type { EIP1193Provider } from '@goodwidget/core' import { Card, Text, ToastContainer, YStack } from '@goodwidget/ui' @@ -11,7 +11,7 @@ import type { } from './widgetRuntimeContract' interface StakingMigrationInnerProps { - migrationConfig: StakingMigrationWidgetProps['migrationConfig'] + environment: StakingMigrationWidgetProps['environment'] adapterFactory?: StakingMigrationWidgetAdapterFactory onMigrationSuccess?: StakingMigrationWidgetProps['onMigrationSuccess'] onMigrationError?: StakingMigrationWidgetProps['onMigrationError'] @@ -26,13 +26,13 @@ function formatJourneyLabel(label: string | null): string | null { } function StakingMigrationInner({ - migrationConfig, + environment, adapterFactory, onMigrationSuccess, onMigrationError, }: StakingMigrationInnerProps) { const defaultAdapter = useStakingMigrationAdapter({ - migrationConfig, + environment, onMigrationSuccess, onMigrationError, }) @@ -41,124 +41,55 @@ function StakingMigrationInner({ () => adapterFactory ? adapterFactory({ - config: migrationConfig ?? {}, + environment: environment ?? 'production', }) : defaultAdapter, - [adapterFactory, defaultAdapter, migrationConfig], + [adapterFactory, defaultAdapter, environment], ) const { state, actions } = activeAdapter const isZeroBalance = state.stakedAmountRaw <= 0n - - const journeyAction = useMemo(() => { - if (state.isBalanceLoading) { - return { - label: 'Loading...', - disabled: true, - pending: true, - onPress: undefined, - } - } - - if (!state.hasRequiredConfig || state.status === 'missing-config') { - return { - label: 'Setup required', - disabled: true, - onPress: undefined, - } - } - - if (isZeroBalance) { - return { - label: 'No balance', - disabled: true, - onPress: undefined, - } - } - - if (!state.address) { - return { - label: 'Connect wallet', - disabled: false, - onPress: () => { - void actions.connect() - }, - } - } - - if (state.status === 'wrong-network') { - return { - label: 'Switch to Fuse', - disabled: false, - onPress: () => { - void actions.switchToFuse() - }, - } + const isPrimaryPending = + state.isBalanceLoading || + state.status === 'approval-pending' || + state.status === 'migrating' + + const handlePrimaryAction = useCallback(async () => { + switch (state.primaryAction) { + case 'connect': + await actions.connect() + break + case 'switch_chain': + await actions.switchToFuse() + break + case 'migrate': + await actions.approveAndMigrate() + break + case 'retry': + await actions.retryMigration() + break + case 'refresh': + await actions.refresh() + break + default: + break } - - if (state.status === 'approval-pending') { - return { - label: 'Approval pending', - disabled: true, - pending: true, - onPress: undefined, - } - } - - if (state.status === 'migrating') { - return { - label: 'Migrating', - disabled: true, - pending: true, - onPress: undefined, - } - } - - if (state.status === 'success') { - return { - label: 'Refresh balance', - disabled: false, - onPress: () => { - void actions.refresh() - }, - } - } - - if (state.status === 'approval-failed') { - return { - label: 'Retry approval', - disabled: false, - onPress: () => { - void actions.retryMigration() - }, - } - } - - if (state.status === 'error') { - return { - label: 'Retry migration', - disabled: false, - onPress: () => { - void actions.retryMigration() - }, - } - } - - return { - label: 'Approve & Migrate', - disabled: false, - onPress: () => { - void actions.approveAndMigrate() - }, - } - }, [ - actions, - isZeroBalance, - state.address, - state.hasRequiredConfig, - state.isBalanceLoading, - state.status, - ]) + }, [actions, state.primaryAction]) + + const summaryAction = useMemo( + () => ({ + label: state.primaryLabel, + disabled: state.primaryAction === 'none' || isPrimaryPending, + pending: isPrimaryPending, + onPress: + state.primaryAction === 'none' || isPrimaryPending + ? undefined + : () => { + void handlePrimaryAction() + }, + }), + [handlePrimaryAction, isPrimaryPending, state.primaryAction, state.primaryLabel], + ) const summaryStatusMessage = useMemo(() => { if (state.status === 'migrating') { @@ -175,7 +106,7 @@ function StakingMigrationInner({ @@ -191,8 +122,8 @@ function StakingMigrationInner({ {state.status === 'missing-config' && ( - Missing migration configuration: Provide migrationApiBaseUrl - and migrationOperator in migrationConfig before enabling migration. + Missing migration configuration: Set a supported environment + (`production`, `staging`, or `development`) before enabling migration. )} @@ -206,7 +137,7 @@ export function StakingMigrationWidget({ config, defaultTheme = 'light', themeOverrides, - migrationConfig, + environment = 'production', onMigrationSuccess, onMigrationError, adapterFactory, @@ -219,7 +150,7 @@ export function StakingMigrationWidget({ themeOverrides={themeOverrides} > = { + unstake: ['fuse_transfer', 'fuse_withdraw', 'fuse_bridge_sent', 'celo_bridge_received', 'celo_staked', 'completed'], + 'bridge sent': ['fuse_bridge_sent', 'celo_bridge_received', 'celo_staked', 'completed'], + 'bridge received': ['celo_bridge_received', 'celo_staked', 'completed'], + stake: ['celo_staked', 'completed'], +} + +interface WorkerMigrationState { + approvalTxHash: string + user?: string + status: 'pending' | 'completed' | 'failed' + lastSuccessfulStep?: string + lastError?: { + stage: string + message: string + at: string + } + updatedAt?: string +} + +interface ApiProgressPayload { + migrationId: string | null + status: 'migrating' | 'success' | 'error' + completedSteps: MigrationStep[] + activeStep: MigrationStep | null + failedStep: MigrationStep | null + reason: string | null +} + +export interface UseStakingMigrationAdapterOptions { + environment?: StakingMigrationWidgetEnvironment + onMigrationSuccess?: (detail: StakingMigrationSuccessDetail) => void + onMigrationError?: (detail: StakingMigrationErrorDetail) => void +} + const DEFAULT_STATE: StakingMigrationWidgetState = { status: 'summary', address: null, @@ -63,21 +106,8 @@ const DEFAULT_STATE: StakingMigrationWidgetState = { approvalTxHash: null, migrationId: null, error: null, -} - -interface ApiProgressPayload { - migrationId: string | null - status: 'migrating' | 'success' | 'error' - completedSteps: MigrationStep[] - activeStep: MigrationStep | null - failedStep: MigrationStep | null - reason: string | null -} - -export interface UseStakingMigrationAdapterOptions { - migrationConfig?: StakingMigrationWidgetConfig - onMigrationSuccess?: (detail: StakingMigrationSuccessDetail) => void - onMigrationError?: (detail: StakingMigrationErrorDetail) => void + primaryAction: 'none', + primaryLabel: '', } function formatErrorMessage(error: unknown): string { @@ -89,124 +119,341 @@ function formatErrorMessage(error: unknown): string { return error.message } -function normalizeStep(step: unknown): MigrationStep | null { - if (typeof step !== 'string') return null - const lowerStep = step.trim().toLowerCase().replace(/[_-]/g, ' ') - if (lowerStep.includes('unstake')) return 'unstake' - if (lowerStep.includes('bridge sent') || lowerStep === 'bridge') return 'bridge sent' - if (lowerStep.includes('bridge received')) return 'bridge received' - if (lowerStep.includes('stake')) return 'stake' +function hasRequiredConfig(migrationConfig: ResolvedStakingMigrationConfig): boolean { + return Boolean(migrationConfig.migrationApiBaseUrl && migrationConfig.migrationOperator) +} + +function buildApiHeaders(migrationConfig: ResolvedStakingMigrationConfig): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (migrationConfig.migrationApiToken) { + headers.Authorization = 'Bearer ' + migrationConfig.migrationApiToken + } + + return headers +} + +function buildMigrationApiUrl(baseUrl: string, path: string): string { + return `${baseUrl.replace(/\/$/, '')}${path}` +} + +function workerStepCompletesWidgetStep( + lastSuccessfulStep: string | undefined, + widgetStep: MigrationStep, +): boolean { + if (!lastSuccessfulStep) return false + return WORKER_STEP_CHECKPOINTS[widgetStep].includes(lastSuccessfulStep) +} + +function mapWorkerStageToFailedStep(stage: string): MigrationStep | null { + const normalizedStage = stage.toLowerCase() + if ( + normalizedStage.includes('fuse_transfer') || + normalizedStage.includes('fuse_withdraw') || + normalizedStage.includes('unstake') + ) { + return 'unstake' + } + if (normalizedStage.includes('fuse_bridge_sent') || normalizedStage.includes('bridge_sent')) { + return 'bridge sent' + } + if (normalizedStage.includes('celo_bridge_received') || normalizedStage.includes('bridge_received')) { + return 'bridge received' + } + if (normalizedStage.includes('celo_staked') || normalizedStage.includes('stake')) { + return 'stake' + } return null } -function collectCompletedSteps(payload: Record): MigrationStep[] { - const completed = new Set() +function mapWorkerStateToProgress(state: WorkerMigrationState): ApiProgressPayload { + const completedSteps = MIGRATION_STEPS.filter((step) => + workerStepCompletesWidgetStep(state.lastSuccessfulStep, step), + ) + const activeStep = MIGRATION_STEPS.find((step) => !completedSteps.includes(step)) ?? null + const failedStep = + state.status === 'failed' && state.lastError?.stage + ? mapWorkerStageToFailedStep(state.lastError.stage) + : null + + const status: ApiProgressPayload['status'] = + state.status === 'completed' ? 'success' : state.status === 'failed' ? 'error' : 'migrating' - const completedSteps = payload.completedSteps - if (Array.isArray(completedSteps)) { - for (const step of completedSteps) { - const normalizedStep = normalizeStep(step) - if (normalizedStep) completed.add(normalizedStep) + return { + migrationId: state.approvalTxHash, + status, + completedSteps, + activeStep: status === 'success' ? null : failedStep ?? activeStep, + failedStep, + reason: state.lastError?.message ?? null, + } +} + +function createCompletedProgress(approvalTxHash: string): ApiProgressPayload { + return { + migrationId: approvalTxHash, + status: 'success', + completedSteps: [...MIGRATION_STEPS], + activeStep: null, + failedStep: null, + reason: null, + } +} + +function parseSseFrame(frame: string): { event: string; data: unknown } | null { + let event = 'message' + let dataStr = '' + + for (const line of frame.split('\n')) { + if (line.startsWith('event:')) { + event = line.slice(6).trim() + } + if (line.startsWith('data:')) { + dataStr += line.slice(5).trim() } } - const steps = payload.steps - if (Array.isArray(steps)) { - for (const stepEntry of steps) { - if (!stepEntry || typeof stepEntry !== 'object') continue - const entry = stepEntry as Record - const normalizedStep = normalizeStep(entry.step ?? entry.name ?? entry.id) - const normalizedStatus = - typeof entry.status === 'string' ? entry.status.toLowerCase().trim() : undefined - if (normalizedStep && normalizedStatus === 'completed') { - completed.add(normalizedStep) - } + if (!dataStr) return null + + try { + return { event, data: JSON.parse(dataStr) as unknown } + } catch { + return null + } +} + +function sleep(ms: number, signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(new DOMException('Aborted', 'AbortError')) + return } + + const timeout = setTimeout(() => { + signal?.removeEventListener('abort', onAbort) + resolve() + }, ms) + + const onAbort = () => { + clearTimeout(timeout) + reject(new DOMException('Aborted', 'AbortError')) + } + + signal?.addEventListener('abort', onAbort, { once: true }) + }) +} + +async function fetchWorkerMigrationState( + approvalTxHash: string, + migrationConfig: ResolvedStakingMigrationConfig, +): Promise { + const endpoint = `${buildMigrationApiUrl(migrationConfig.migrationApiBaseUrl!, MIGRATION_STATUS_PATH)}?approvalTxHash=${encodeURIComponent(approvalTxHash)}` + const response = await fetch(endpoint, { + method: 'GET', + headers: buildApiHeaders(migrationConfig), + }) + + const responsePayload = (await response.json().catch(() => ({}))) as unknown + + if (response.status === 404) { + throw new Error('Migration state not found') } - return MIGRATION_STEPS.filter((step) => completed.has(step)) + if (!response.ok) { + const payload = + responsePayload && typeof responsePayload === 'object' + ? (responsePayload as Record) + : {} + throw new Error( + typeof payload.error === 'string' + ? payload.error + : `Migration status request failed (${response.status})`, + ) + } + + return responsePayload as WorkerMigrationState } -function normalizeApiProgress(rawPayload: unknown): ApiProgressPayload { - const payload = rawPayload && typeof rawPayload === 'object' ? (rawPayload as Record) : {} - - const rawStatus = - typeof payload.status === 'string' - ? payload.status.toLowerCase().trim() - : typeof payload.state === 'string' - ? payload.state.toLowerCase().trim() - : 'migrating' - - const normalizedStatus: ApiProgressPayload['status'] = - rawStatus === 'success' || rawStatus === 'completed' || rawStatus === 'done' - ? 'success' - : rawStatus === 'error' || rawStatus === 'failed' - ? 'error' - : 'migrating' - - const activeStep = normalizeStep(payload.activeStep ?? payload.step ?? payload.currentStep) - const failedStep = normalizeStep(payload.failedStep ?? payload.errorStep) - - const reasonSource = - payload.reason ?? - payload.message ?? - (payload.error && typeof payload.error === 'object' - ? (payload.error as Record).message - : payload.error) - - const completedSteps = collectCompletedSteps(payload) - - const migrationId = - typeof payload.migrationId === 'string' - ? payload.migrationId - : typeof payload.id === 'string' - ? payload.id - : null +async function submitMigrationStart( + approvalTxHash: string, + migrationConfig: ResolvedStakingMigrationConfig, +): Promise<'started' | 'completed'> { + const endpoint = buildMigrationApiUrl(migrationConfig.migrationApiBaseUrl!, MIGRATION_SUBMIT_PATH) + const response = await fetch(endpoint, { + method: 'POST', + headers: buildApiHeaders(migrationConfig), + body: JSON.stringify({ approvalTxHash }), + }) + + const responsePayload = (await response.json().catch(() => ({}))) as Record + + if (response.status === 409) { + return 'started' + } + + if (!response.ok) { + throw new Error( + typeof responsePayload.error === 'string' + ? responsePayload.error + : `Migration API request failed (${response.status})`, + ) + } - return { - migrationId, - status: normalizedStatus, - completedSteps, - activeStep, - failedStep, - reason: typeof reasonSource === 'string' ? reasonSource : null, + if (responsePayload.skipped === true) { + throw new Error( + typeof responsePayload.skipReason === 'string' + ? responsePayload.skipReason + : 'Migration was skipped by backend', + ) } + + return 'completed' } -function buildApiHeaders(migrationConfig: StakingMigrationWidgetConfig): Record { - const headers: Record = { - 'Content-Type': 'application/json', +async function watchMigrationViaPolling( + approvalTxHash: string, + migrationConfig: ResolvedStakingMigrationConfig, + signal: AbortSignal, + onProgress: (progress: ApiProgressPayload) => boolean, +): Promise { + while (!signal.aborted) { + try { + const workerState = await fetchWorkerMigrationState(approvalTxHash, migrationConfig) + if (onProgress(mapWorkerStateToProgress(workerState))) { + return + } + } catch (error: unknown) { + if (!(error instanceof Error) || error.message !== 'Migration state not found') { + throw error + } + } + + await sleep(MIGRATION_STATUS_POLL_INTERVAL_MS, signal) } +} - if (migrationConfig.migrationApiToken) { - headers.Authorization = 'Bearer ' + migrationConfig.migrationApiToken +async function watchMigrationViaSse( + approvalTxHash: string, + migrationConfig: ResolvedStakingMigrationConfig, + signal: AbortSignal, + onProgress: (progress: ApiProgressPayload) => boolean, +): Promise { + const endpoint = `${buildMigrationApiUrl(migrationConfig.migrationApiBaseUrl!, MIGRATION_STATUS_STREAM_PATH)}?approvalTxHash=${encodeURIComponent(approvalTxHash)}` + const response = await fetch(endpoint, { + method: 'GET', + headers: buildApiHeaders(migrationConfig), + signal, + }) + + if (!response.ok || !response.body) { + throw new Error(`Migration SSE request failed (${response.status})`) } - return headers + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + + while (!signal.aborted) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + + let frameEnd = buffer.indexOf('\n\n') + while (frameEnd >= 0) { + const frame = buffer.slice(0, frameEnd) + buffer = buffer.slice(frameEnd + 2) + const parsedFrame = parseSseFrame(frame) + + if (parsedFrame?.event === 'state' && parsedFrame.data && typeof parsedFrame.data === 'object') { + const payload = parsedFrame.data as Record + if (payload.status === 'not_found') { + frameEnd = buffer.indexOf('\n\n') + continue + } + + if (onProgress(mapWorkerStateToProgress(parsedFrame.data as WorkerMigrationState))) { + return + } + } + + if (parsedFrame?.event === 'done') { + return + } + + frameEnd = buffer.indexOf('\n\n') + } + } } -function hasRequiredConfig(migrationConfig: StakingMigrationWidgetConfig): boolean { - return Boolean(migrationConfig.migrationApiBaseUrl && migrationConfig.migrationOperator) +async function watchMigrationProgress( + approvalTxHash: string, + migrationConfig: ResolvedStakingMigrationConfig, + signal: AbortSignal, + onProgress: (progress: ApiProgressPayload) => boolean, +): Promise { + try { + await watchMigrationViaSse(approvalTxHash, migrationConfig, signal, onProgress) + } catch { + await watchMigrationViaPolling(approvalTxHash, migrationConfig, signal, onProgress) + } +} + +function derivePrimaryAction(state: StakingMigrationWidgetState): StakingMigrationPrimaryAction { + if (state.isBalanceLoading) return 'none' + if (!state.hasRequiredConfig || state.status === 'missing-config') return 'none' + if (state.stakedAmountRaw <= 0n) return 'none' + if (!state.address) return 'connect' + if (state.status === 'wrong-network') return 'switch_chain' + if (state.status === 'approval-pending' || state.status === 'migrating') return 'none' + if (state.status === 'success') return 'refresh' + if (state.status === 'approval-failed' || state.status === 'error') return 'retry' + return 'migrate' +} + +function derivePrimaryLabel( + state: StakingMigrationWidgetState, + primaryAction: StakingMigrationPrimaryAction, +): string { + if (state.isBalanceLoading) return 'Loading...' + if (!state.hasRequiredConfig || state.status === 'missing-config') return 'Setup required' + if (state.stakedAmountRaw <= 0n) return 'No balance' + if (state.status === 'approval-pending') return 'Approval pending' + if (state.status === 'migrating') return 'Migrating' + + switch (primaryAction) { + case 'connect': + return 'Connect wallet' + case 'switch_chain': + return 'Switch to Fuse' + case 'migrate': + return 'Approve & Migrate' + case 'retry': + return state.status === 'approval-failed' ? 'Retry approval' : 'Retry migration' + case 'refresh': + return 'Refresh balance' + default: + return '' + } } export function useStakingMigrationAdapter({ - migrationConfig, + environment, onMigrationSuccess, onMigrationError, }: UseStakingMigrationAdapterOptions = {}): StakingMigrationWidgetAdapterResult { const { address, chainId, isConnected, provider, connect } = useWallet() - const resolvedConfig = useMemo( - () => ({ - migrationApiBaseUrl: migrationConfig?.migrationApiBaseUrl, - migrationOperator: migrationConfig?.migrationOperator, - migrationApiToken: migrationConfig?.migrationApiToken, - }), - [ - migrationConfig?.migrationApiBaseUrl, - migrationConfig?.migrationApiToken, - migrationConfig?.migrationOperator, - ], + const resolvedEnvironment = useMemo( + () => normalizeStakingMigrationEnvironment(environment), + [environment], + ) + + const resolvedConfig = useMemo( + () => resolveMigrationConfigForEnvironment(resolvedEnvironment), + [resolvedEnvironment], ) const [state, setState] = useState(() => ({ @@ -216,6 +463,7 @@ export function useStakingMigrationAdapter({ const actionInFlightRef = useRef(false) const unmountedRef = useRef(false) + const migrationWatchAbortRef = useRef(null) const publicClient = useMemo( () => @@ -236,6 +484,120 @@ export function useStakingMigrationAdapter({ }) }, [provider, address]) + const stopMigrationWatch = useCallback(() => { + migrationWatchAbortRef.current?.abort() + migrationWatchAbortRef.current = null + }, []) + + const applyMigrationProgress = useCallback( + (progress: ApiProgressPayload, approvalTxHash: string) => { + if (progress.status === 'success') { + setState((previousState) => ({ + ...previousState, + status: 'success', + approvalTxHash, + migrationId: progress.migrationId ?? approvalTxHash, + completedSteps: progress.completedSteps.length > 0 ? progress.completedSteps : MIGRATION_STEPS, + activeStep: null, + failedStep: null, + error: null, + })) + + onMigrationSuccess?.({ + address: address!, + approvalTxHash, + migrationId: progress.migrationId ?? approvalTxHash, + completedSteps: + progress.completedSteps.length > 0 ? progress.completedSteps : [...MIGRATION_STEPS], + }) + return true + } + + if (progress.status === 'error') { + const errorMessage = progress.reason ?? 'Migration failed during backend processing' + + setState((previousState) => ({ + ...previousState, + status: 'error', + approvalTxHash, + migrationId: progress.migrationId ?? approvalTxHash, + completedSteps: progress.completedSteps, + activeStep: progress.activeStep, + failedStep: progress.failedStep, + error: errorMessage, + })) + + onMigrationError?.({ + address: address ?? null, + reason: errorMessage, + failedStep: progress.failedStep, + }) + return true + } + + setState((previousState) => ({ + ...previousState, + status: 'migrating', + approvalTxHash, + migrationId: progress.migrationId ?? approvalTxHash, + completedSteps: progress.completedSteps, + activeStep: + progress.activeStep ?? + MIGRATION_STEPS.find((step) => !progress.completedSteps.includes(step)) ?? + null, + failedStep: null, + error: null, + })) + return false + }, + [address, onMigrationError, onMigrationSuccess], + ) + + const runMigrationJob = useCallback( + async (approvalTxHash: string) => { + stopMigrationWatch() + + const abortController = new AbortController() + migrationWatchAbortRef.current = abortController + let reachedTerminalState = false + + const watchPromise = watchMigrationProgress( + approvalTxHash, + resolvedConfig, + abortController.signal, + (progress) => { + const terminal = applyMigrationProgress(progress, approvalTxHash) + if (terminal) { + reachedTerminalState = true + stopMigrationWatch() + } + return terminal + }, + ).catch((error: unknown) => { + if (reachedTerminalState || unmountedRef.current) return + if (error instanceof DOMException && error.name === 'AbortError') return + throw error + }) + + try { + const postResult = await submitMigrationStart(approvalTxHash, resolvedConfig) + if (postResult === 'completed' && !reachedTerminalState) { + reachedTerminalState = applyMigrationProgress( + createCompletedProgress(approvalTxHash), + approvalTxHash, + ) + } + } catch (error: unknown) { + if (!reachedTerminalState) { + throw error + } + } + + await watchPromise + }, + [applyMigrationProgress, resolvedConfig, stopMigrationWatch], + ) + const refreshStakeState = useCallback(async () => { if (!isConnected || !address) { setState((previousState) => ({ @@ -327,153 +689,14 @@ export function useStakingMigrationAdapter({ unmountedRef.current = false return () => { unmountedRef.current = true + stopMigrationWatch() } - }, []) + }, [stopMigrationWatch]) useEffect(() => { void refreshStakeState() }, [refreshStakeState]) - const applyMigrationProgress = useCallback( - (progress: ApiProgressPayload, approvalTxHash: string) => { - if (progress.status === 'success') { - setState((previousState) => ({ - ...previousState, - status: 'success', - approvalTxHash, - migrationId: progress.migrationId, - completedSteps: progress.completedSteps.length > 0 ? progress.completedSteps : MIGRATION_STEPS, - activeStep: null, - failedStep: null, - error: null, - })) - - onMigrationSuccess?.({ - address: address!, - approvalTxHash, - migrationId: progress.migrationId ?? 'unknown', - completedSteps: - progress.completedSteps.length > 0 ? progress.completedSteps : [...MIGRATION_STEPS], - }) - return true - } - - if (progress.status === 'error') { - const errorMessage = progress.reason ?? 'Migration failed during backend processing' - - setState((previousState) => ({ - ...previousState, - status: 'error', - approvalTxHash, - migrationId: progress.migrationId, - completedSteps: progress.completedSteps, - activeStep: progress.activeStep, - failedStep: progress.failedStep, - error: errorMessage, - })) - - onMigrationError?.({ - address: address ?? null, - reason: errorMessage, - failedStep: progress.failedStep, - }) - return true - } - - setState((previousState) => ({ - ...previousState, - status: 'migrating', - approvalTxHash, - migrationId: progress.migrationId, - completedSteps: progress.completedSteps, - activeStep: - progress.activeStep ?? - MIGRATION_STEPS.find((step) => !progress.completedSteps.includes(step)) ?? - null, - failedStep: null, - error: null, - })) - return false - }, - [address, onMigrationError, onMigrationSuccess], - ) - - const submitMigrationApproval = useCallback( - async (approvalTxHash: string): Promise => { - const baseUrl = resolvedConfig.migrationApiBaseUrl! - const endpoint = `${baseUrl.replace(/\/$/, '')}/staking-migrations` - - const response = await fetch(endpoint, { - method: 'POST', - headers: buildApiHeaders(resolvedConfig), - body: JSON.stringify({ - walletAddress: address, - approvalTxHash, - sourceChainId: FUSE_CHAIN_ID, - stakingContract: FUSE_STAKING_CONTRACT_ADDRESS, - migrationOperator: resolvedConfig.migrationOperator, - }), - }) - - const responsePayload = (await response.json().catch(() => ({}))) as unknown - - if (!response.ok) { - const normalizedPayload = normalizeApiProgress(responsePayload) - throw new Error(normalizedPayload.reason ?? `Migration API request failed (${response.status})`) - } - - return normalizeApiProgress(responsePayload) - }, - [address, resolvedConfig], - ) - - const fetchMigrationProgress = useCallback( - async (migrationId: string): Promise => { - const baseUrl = resolvedConfig.migrationApiBaseUrl! - const endpoint = `${baseUrl.replace(/\/$/, '')}/staking-migrations/${migrationId}` - - const response = await fetch(endpoint, { - method: 'GET', - headers: buildApiHeaders(resolvedConfig), - }) - - const responsePayload = (await response.json().catch(() => ({}))) as unknown - - if (!response.ok) { - const normalizedPayload = normalizeApiProgress(responsePayload) - throw new Error(normalizedPayload.reason ?? `Migration status request failed (${response.status})`) - } - - return normalizeApiProgress(responsePayload) - }, - [resolvedConfig], - ) - - const waitForMigrationCompletion = useCallback( - async (approvalTxHash: string, initialProgress: ApiProgressPayload): Promise => { - let progress = initialProgress - - if (applyMigrationProgress(progress, approvalTxHash)) { - return - } - - const migrationId = progress.migrationId - if (!migrationId) { - throw new Error('Migration API did not return a migration id') - } - - while (!unmountedRef.current) { - await new Promise((resolve) => setTimeout(resolve, 3000)) - progress = await fetchMigrationProgress(migrationId) - const reachedTerminalState = applyMigrationProgress(progress, approvalTxHash) - if (reachedTerminalState) { - return - } - } - }, - [applyMigrationProgress, fetchMigrationProgress], - ) - const startApprovalAndMigration = useCallback(async () => { if (actionInFlightRef.current) return @@ -483,7 +706,7 @@ export function useStakingMigrationAdapter({ ...previousState, status: 'missing-config', hasRequiredConfig: false, - error: 'Missing migrationApiBaseUrl or migrationOperator in migrationConfig', + error: 'Migration backend configuration is unavailable for the selected environment', })) return } @@ -523,6 +746,7 @@ export function useStakingMigrationAdapter({ actionInFlightRef.current = true let approvalConfirmed = false + setState((previousState) => ({ ...previousState, status: 'approval-pending', @@ -545,10 +769,17 @@ export function useStakingMigrationAdapter({ if (approvalReceipt.status !== 'success') { throw new Error('Approval transaction did not confirm successfully') } + approvalConfirmed = true - const initialProgress = await submitMigrationApproval(approvalTxHash) - await waitForMigrationCompletion(approvalTxHash, initialProgress) + setState((previousState) => ({ + ...previousState, + status: 'migrating', + approvalTxHash, + migrationId: approvalTxHash, + })) + + await runMigrationJob(approvalTxHash) } catch (error: unknown) { const errorMessage = formatErrorMessage(error) const isApprovalFailure = !approvalConfirmed @@ -566,6 +797,7 @@ export function useStakingMigrationAdapter({ }) } finally { actionInFlightRef.current = false + stopMigrationWatch() } }, [ address, @@ -575,11 +807,10 @@ export function useStakingMigrationAdapter({ onMigrationError, publicClient, resolvedConfig, + runMigrationJob, state.activeStep, state.stakedAmountRaw, - state.status, - submitMigrationApproval, - waitForMigrationCompletion, + stopMigrationWatch, walletClient, ]) @@ -593,12 +824,13 @@ export function useStakingMigrationAdapter({ setState((previousState) => ({ ...previousState, status: 'missing-config', - error: 'Missing migrationApiBaseUrl or migrationOperator in migrationConfig', + error: 'Migration backend configuration is unavailable for the selected environment', })) return } actionInFlightRef.current = true + setState((previousState) => ({ ...previousState, status: 'migrating', @@ -606,8 +838,7 @@ export function useStakingMigrationAdapter({ })) try { - const initialProgress = await submitMigrationApproval(state.approvalTxHash) - await waitForMigrationCompletion(state.approvalTxHash, initialProgress) + await runMigrationJob(state.approvalTxHash) } catch (error: unknown) { const errorMessage = formatErrorMessage(error) setState((previousState) => ({ @@ -617,8 +848,9 @@ export function useStakingMigrationAdapter({ })) } finally { actionInFlightRef.current = false + stopMigrationWatch() } - }, [resolvedConfig, startApprovalAndMigration, state.approvalTxHash, submitMigrationApproval, waitForMigrationCompletion]) + }, [resolvedConfig, runMigrationJob, startApprovalAndMigration, state.approvalTxHash, stopMigrationWatch]) const switchToFuse = useCallback(async () => { if (!provider) return @@ -629,14 +861,23 @@ export function useStakingMigrationAdapter({ params: [{ chainId: `0x${FUSE_CHAIN_ID.toString(16)}` }], }) } catch { - // no-op: wallet might not support programmatic switching } finally { await refreshStakeState() } }, [provider, refreshStakeState]) + const derivedState = useMemo(() => { + const primaryAction = derivePrimaryAction(state) + const primaryLabel = derivePrimaryLabel(state, primaryAction) + return { + ...state, + primaryAction, + primaryLabel, + } + }, [state]) + return { - state, + state: derivedState, actions: { connect, switchToFuse, diff --git a/packages/staking-migration-widget/src/index.ts b/packages/staking-migration-widget/src/index.ts index c4ac72b..96fc1fb 100644 --- a/packages/staking-migration-widget/src/index.ts +++ b/packages/staking-migration-widget/src/index.ts @@ -10,13 +10,23 @@ export type { StakingMigrationWidgetAdapterFactoryInput, StakingMigrationWidgetAdapterResult, StakingMigrationWidgetActions, - StakingMigrationWidgetConfig, + StakingMigrationWidgetEnvironment, StakingMigrationWidgetProps, StakingMigrationWidgetState, StakingMigrationWidgetStatus, + StakingMigrationPrimaryAction, } from './widgetRuntimeContract' export { FUSE_CHAIN_ID, FUSE_STAKING_CONTRACT_ADDRESS, } from './widgetRuntimeContract' + +export { + MIGRATION_OPERATOR_ADDRESS, + normalizeStakingMigrationEnvironment, + resolveMigrationConfigForEnvironment, + stakingMigrationCapabilities, +} from './migrationEnvironments' + +export type { ResolvedStakingMigrationConfig } from './migrationEnvironments' diff --git a/packages/staking-migration-widget/src/integration.ts b/packages/staking-migration-widget/src/integration.ts new file mode 100644 index 0000000..4043f3c --- /dev/null +++ b/packages/staking-migration-widget/src/integration.ts @@ -0,0 +1,21 @@ +export { stakingMigrationCapabilities } from './migrationEnvironments' + +export const stakingMigrationIntegration = { + id: 'staking-migration', + capabilitySource: 'stakingMigrationCapabilities', + uses: ['approve', 'migrate', 'status', 'statusStream'], + chains: [122], + states: [ + 'summary', + 'approval-pending', + 'approval-failed', + 'migrating', + 'success', + 'error', + 'wrong-network', + 'missing-config', + ], + events: ['migration-success', 'migration-error'], +} as const + +export type StakingMigrationIntegration = typeof stakingMigrationIntegration diff --git a/packages/staking-migration-widget/src/migrationEnvironments.ts b/packages/staking-migration-widget/src/migrationEnvironments.ts new file mode 100644 index 0000000..e2b0f17 --- /dev/null +++ b/packages/staking-migration-widget/src/migrationEnvironments.ts @@ -0,0 +1,48 @@ +import type { Address } from 'viem' + +export type StakingMigrationWidgetEnvironment = 'production' | 'staging' | 'development' + +export const stakingMigrationCapabilities = { + environments: ['production', 'staging', 'development'] as const, + chains: [122], + events: ['migration-success', 'migration-error'], +} as const + +export const MIGRATION_OPERATOR_ADDRESS: Address = '0xE3441bA0863AEFBf28eca5F6fAAFb4A2B608F3A1' + +const MIGRATION_API_BASE_URLS: Record = { + development: 'http://localhost:8787', + staging: 'https://monitoringworker-staging.gooddollar.workers.dev', + production: 'https://monitoringworker.gooddollar.workers.dev', +} + +const MIGRATION_API_TOKENS: Record = { + development: 'migration-test-token', + staging: undefined, + production: undefined, +} + +export interface ResolvedStakingMigrationConfig { + migrationApiBaseUrl: string + migrationOperator: Address + migrationApiToken?: string +} + +export function resolveMigrationConfigForEnvironment( + environment: StakingMigrationWidgetEnvironment, +): ResolvedStakingMigrationConfig { + return { + migrationApiBaseUrl: MIGRATION_API_BASE_URLS[environment], + migrationOperator: MIGRATION_OPERATOR_ADDRESS, + migrationApiToken: MIGRATION_API_TOKENS[environment], + } +} + +export function normalizeStakingMigrationEnvironment( + environment?: StakingMigrationWidgetEnvironment, +): StakingMigrationWidgetEnvironment { + if (environment && stakingMigrationCapabilities.environments.includes(environment)) { + return environment + } + return 'production' +} diff --git a/packages/staking-migration-widget/src/widgetRuntimeContract.ts b/packages/staking-migration-widget/src/widgetRuntimeContract.ts index f5eb6af..4393d33 100644 --- a/packages/staking-migration-widget/src/widgetRuntimeContract.ts +++ b/packages/staking-migration-widget/src/widgetRuntimeContract.ts @@ -1,11 +1,12 @@ import type { Address } from 'viem' import type { GoodWidgetConfig, GoodWidgetThemeOverrides } from '@goodwidget/ui' +import type { StakingMigrationWidgetEnvironment } from './migrationEnvironments' + +export type { StakingMigrationWidgetEnvironment } from './migrationEnvironments' -// This is the expected network for Fuse staking approvals. export const FUSE_CHAIN_ID = 122 -// This address is sourced from GoodProtocol releases/deployment.json -> production.FuseStaking. -export const FUSE_STAKING_CONTRACT_ADDRESS: Address = '0xA199F0C353E25AdF022378B0c208D600f39a6505' +export const FUSE_STAKING_CONTRACT_ADDRESS: Address = '0xB7C3e738224625289C573c54d402E9Be46205546' export type MigrationStep = 'unstake' | 'bridge sent' | 'bridge received' | 'stake' @@ -19,6 +20,14 @@ export type StakingMigrationWidgetStatus = | 'wrong-network' | 'missing-config' +export type StakingMigrationPrimaryAction = + | 'connect' + | 'switch_chain' + | 'migrate' + | 'retry' + | 'refresh' + | 'none' + export interface StakingMigrationSuccessDetail { address: string approvalTxHash: string @@ -48,6 +57,8 @@ export interface StakingMigrationWidgetState { approvalTxHash: string | null migrationId: string | null error: string | null + primaryAction: StakingMigrationPrimaryAction + primaryLabel: string } export interface StakingMigrationWidgetActions { @@ -64,25 +75,19 @@ export interface StakingMigrationWidgetAdapterResult { } export interface StakingMigrationWidgetAdapterFactoryInput { - config: StakingMigrationWidgetConfig + environment: StakingMigrationWidgetEnvironment } export type StakingMigrationWidgetAdapterFactory = ( input: StakingMigrationWidgetAdapterFactoryInput, ) => StakingMigrationWidgetAdapterResult -export interface StakingMigrationWidgetConfig { - migrationApiBaseUrl?: string - migrationOperator?: Address - migrationApiToken?: string -} - export interface StakingMigrationWidgetProps { provider?: unknown config?: GoodWidgetConfig defaultTheme?: 'light' | 'dark' themeOverrides?: GoodWidgetThemeOverrides - migrationConfig?: StakingMigrationWidgetConfig + environment?: StakingMigrationWidgetEnvironment onMigrationSuccess?: (detail: StakingMigrationSuccessDetail) => void onMigrationError?: (detail: StakingMigrationErrorDetail) => void adapterFactory?: StakingMigrationWidgetAdapterFactory From 257fcf196be018c5c56b5a7dddfab11db0d61095 Mon Sep 17 00:00:00 2001 From: blueogin Date: Tue, 9 Jun 2026 08:02:15 -0400 Subject: [PATCH 23/27] feat: add migration API token support and enhance StakingMigrationWidget configuration --- examples/react-web/src/App.tsx | 6 +++- examples/react-web/src/globals.d.ts | 10 +++++++ .../src/stakingMigrationDevConfig.ts | 1 + .../src/StakingMigrationWidget.tsx | 8 ++++- .../staking-migration-widget/src/adapter.ts | 13 +++++++-- .../staking-migration-widget/src/index.ts | 3 ++ .../src/migrationEnvironments.ts | 29 ++++++++++++------- .../src/widgetRuntimeContract.ts | 2 ++ 8 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 examples/react-web/src/stakingMigrationDevConfig.ts diff --git a/examples/react-web/src/App.tsx b/examples/react-web/src/App.tsx index 12f2eaf..b532156 100644 --- a/examples/react-web/src/App.tsx +++ b/examples/react-web/src/App.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react' import { GoodWidgetProvider, useWallet, useHost } from '@goodwidget/core' import { ClaimWidget } from '@goodwidget/claim-widget-theme-demo' import { StakingMigrationWidget } from '@goodwidget/staking-migration-widget' +import { LOCAL_MIGRATION_API_TOKEN } from './stakingMigrationDevConfig' import { getThemeManifest, MiniAppShell, @@ -165,7 +166,10 @@ function OverrideShowcase() { StakingMigrationWidget: - + diff --git a/examples/react-web/src/globals.d.ts b/examples/react-web/src/globals.d.ts index a0269a7..772c389 100644 --- a/examples/react-web/src/globals.d.ts +++ b/examples/react-web/src/globals.d.ts @@ -2,4 +2,14 @@ declare const process: { env: Record } +interface ImportMetaEnv { + readonly DEV: boolean + readonly MODE: string + readonly PROD: boolean +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + declare function setImmediate(callback: (...args: unknown[]) => void, ...args: unknown[]): number diff --git a/examples/react-web/src/stakingMigrationDevConfig.ts b/examples/react-web/src/stakingMigrationDevConfig.ts new file mode 100644 index 0000000..781c788 --- /dev/null +++ b/examples/react-web/src/stakingMigrationDevConfig.ts @@ -0,0 +1 @@ +export const LOCAL_MIGRATION_API_TOKEN = 'migration-test-token' diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index b806bf2..037d645 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -12,6 +12,7 @@ import type { interface StakingMigrationInnerProps { environment: StakingMigrationWidgetProps['environment'] + migrationApiToken: StakingMigrationWidgetProps['migrationApiToken'] adapterFactory?: StakingMigrationWidgetAdapterFactory onMigrationSuccess?: StakingMigrationWidgetProps['onMigrationSuccess'] onMigrationError?: StakingMigrationWidgetProps['onMigrationError'] @@ -27,12 +28,14 @@ function formatJourneyLabel(label: string | null): string | null { function StakingMigrationInner({ environment, + migrationApiToken, adapterFactory, onMigrationSuccess, onMigrationError, }: StakingMigrationInnerProps) { const defaultAdapter = useStakingMigrationAdapter({ environment, + migrationApiToken, onMigrationSuccess, onMigrationError, }) @@ -42,9 +45,10 @@ function StakingMigrationInner({ adapterFactory ? adapterFactory({ environment: environment ?? 'production', + migrationApiToken, }) : defaultAdapter, - [adapterFactory, defaultAdapter, environment], + [adapterFactory, defaultAdapter, environment, migrationApiToken], ) const { state, actions } = activeAdapter @@ -138,6 +142,7 @@ export function StakingMigrationWidget({ defaultTheme = 'light', themeOverrides, environment = 'production', + migrationApiToken, onMigrationSuccess, onMigrationError, adapterFactory, @@ -151,6 +156,7 @@ export function StakingMigrationWidget({ > void onMigrationError?: (detail: StakingMigrationErrorDetail) => void } @@ -123,13 +124,18 @@ function hasRequiredConfig(migrationConfig: ResolvedStakingMigrationConfig): boo return Boolean(migrationConfig.migrationApiBaseUrl && migrationConfig.migrationOperator) } +function normalizeMigrationApiToken(token: string): string { + return token.startsWith('Bearer ') ? token.slice(7) : token +} + function buildApiHeaders(migrationConfig: ResolvedStakingMigrationConfig): Record { const headers: Record = { 'Content-Type': 'application/json', } if (migrationConfig.migrationApiToken) { - headers.Authorization = 'Bearer ' + migrationConfig.migrationApiToken + headers.Authorization = + 'Bearer ' + normalizeMigrationApiToken(migrationConfig.migrationApiToken) } return headers @@ -441,6 +447,7 @@ function derivePrimaryLabel( export function useStakingMigrationAdapter({ environment, + migrationApiToken, onMigrationSuccess, onMigrationError, }: UseStakingMigrationAdapterOptions = {}): StakingMigrationWidgetAdapterResult { @@ -452,8 +459,8 @@ export function useStakingMigrationAdapter({ ) const resolvedConfig = useMemo( - () => resolveMigrationConfigForEnvironment(resolvedEnvironment), - [resolvedEnvironment], + () => resolveMigrationConfigForEnvironment(resolvedEnvironment, migrationApiToken), + [migrationApiToken, resolvedEnvironment], ) const [state, setState] = useState(() => ({ diff --git a/packages/staking-migration-widget/src/index.ts b/packages/staking-migration-widget/src/index.ts index 96fc1fb..105b998 100644 --- a/packages/staking-migration-widget/src/index.ts +++ b/packages/staking-migration-widget/src/index.ts @@ -1,3 +1,6 @@ +export { stakingMigrationIntegration } from './integration' +export type { StakingMigrationIntegration } from './integration' + export { StakingMigrationWidget } from './StakingMigrationWidget' export { useStakingMigrationAdapter } from './adapter' export type { UseStakingMigrationAdapterOptions } from './adapter' diff --git a/packages/staking-migration-widget/src/migrationEnvironments.ts b/packages/staking-migration-widget/src/migrationEnvironments.ts index e2b0f17..b2deb75 100644 --- a/packages/staking-migration-widget/src/migrationEnvironments.ts +++ b/packages/staking-migration-widget/src/migrationEnvironments.ts @@ -10,16 +10,23 @@ export const stakingMigrationCapabilities = { export const MIGRATION_OPERATOR_ADDRESS: Address = '0xE3441bA0863AEFBf28eca5F6fAAFb4A2B608F3A1' -const MIGRATION_API_BASE_URLS: Record = { - development: 'http://localhost:8787', - staging: 'https://monitoringworker-staging.gooddollar.workers.dev', - production: 'https://monitoringworker.gooddollar.workers.dev', +interface MigrationEnvironmentPreset { + migrationApiBaseUrl: string } -const MIGRATION_API_TOKENS: Record = { - development: 'migration-test-token', - staging: undefined, - production: undefined, +const MIGRATION_ENVIRONMENT_PRESETS: Record< + StakingMigrationWidgetEnvironment, + MigrationEnvironmentPreset +> = { + development: { + migrationApiBaseUrl: 'http://localhost:8787', + }, + staging: { + migrationApiBaseUrl: 'https://monitoringworker-staging.gooddollar.workers.dev', + }, + production: { + migrationApiBaseUrl: 'https://monitoringworker.gooddollar.workers.dev', + }, } export interface ResolvedStakingMigrationConfig { @@ -30,11 +37,13 @@ export interface ResolvedStakingMigrationConfig { export function resolveMigrationConfigForEnvironment( environment: StakingMigrationWidgetEnvironment, + migrationApiToken?: string, ): ResolvedStakingMigrationConfig { + const preset = MIGRATION_ENVIRONMENT_PRESETS[environment] return { - migrationApiBaseUrl: MIGRATION_API_BASE_URLS[environment], + migrationApiBaseUrl: preset.migrationApiBaseUrl, migrationOperator: MIGRATION_OPERATOR_ADDRESS, - migrationApiToken: MIGRATION_API_TOKENS[environment], + migrationApiToken, } } diff --git a/packages/staking-migration-widget/src/widgetRuntimeContract.ts b/packages/staking-migration-widget/src/widgetRuntimeContract.ts index 4393d33..3b041b7 100644 --- a/packages/staking-migration-widget/src/widgetRuntimeContract.ts +++ b/packages/staking-migration-widget/src/widgetRuntimeContract.ts @@ -76,6 +76,7 @@ export interface StakingMigrationWidgetAdapterResult { export interface StakingMigrationWidgetAdapterFactoryInput { environment: StakingMigrationWidgetEnvironment + migrationApiToken?: string } export type StakingMigrationWidgetAdapterFactory = ( @@ -88,6 +89,7 @@ export interface StakingMigrationWidgetProps { defaultTheme?: 'light' | 'dark' themeOverrides?: GoodWidgetThemeOverrides environment?: StakingMigrationWidgetEnvironment + migrationApiToken?: string onMigrationSuccess?: (detail: StakingMigrationSuccessDetail) => void onMigrationError?: (detail: StakingMigrationErrorDetail) => void adapterFactory?: StakingMigrationWidgetAdapterFactory From 5b934dd6396b017f6c8eb5d4c4fc2183f904077b Mon Sep 17 00:00:00 2001 From: blueogin Date: Tue, 9 Jun 2026 08:14:39 -0400 Subject: [PATCH 24/27] feat: update StakingMigrationWidget to use new migration API configuration and remove deprecated local token --- examples/react-web/src/App.tsx | 5 +- examples/react-web/src/globals.d.ts | 5 +- .../src/stakingMigrationDevConfig.ts | 1 - .../StakingMigrationWidget.stories.tsx | 99 ++++++++----------- .../src/StakingMigrationWidget.tsx | 36 ++++--- .../staking-migration-widget/src/adapter.ts | 28 ++---- .../staking-migration-widget/src/index.ts | 16 +-- .../src/integration.ts | 2 - .../src/migrationEnvironments.ts | 47 ++------- .../src/widgetRuntimeContract.ts | 8 +- 10 files changed, 87 insertions(+), 160 deletions(-) delete mode 100644 examples/react-web/src/stakingMigrationDevConfig.ts diff --git a/examples/react-web/src/App.tsx b/examples/react-web/src/App.tsx index b532156..8a7ac6a 100644 --- a/examples/react-web/src/App.tsx +++ b/examples/react-web/src/App.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react' import { GoodWidgetProvider, useWallet, useHost } from '@goodwidget/core' import { ClaimWidget } from '@goodwidget/claim-widget-theme-demo' import { StakingMigrationWidget } from '@goodwidget/staking-migration-widget' -import { LOCAL_MIGRATION_API_TOKEN } from './stakingMigrationDevConfig' import { getThemeManifest, MiniAppShell, @@ -167,8 +166,8 @@ function OverrideShowcase() { StakingMigrationWidget: diff --git a/examples/react-web/src/globals.d.ts b/examples/react-web/src/globals.d.ts index 772c389..001ef1e 100644 --- a/examples/react-web/src/globals.d.ts +++ b/examples/react-web/src/globals.d.ts @@ -3,9 +3,8 @@ declare const process: { } interface ImportMetaEnv { - readonly DEV: boolean - readonly MODE: string - readonly PROD: boolean + readonly VITE_MIGRATION_API_BASE_URL?: string + readonly VITE_MIGRATION_API_TOKEN?: string } interface ImportMeta { diff --git a/examples/react-web/src/stakingMigrationDevConfig.ts b/examples/react-web/src/stakingMigrationDevConfig.ts deleted file mode 100644 index 781c788..0000000 --- a/examples/react-web/src/stakingMigrationDevConfig.ts +++ /dev/null @@ -1 +0,0 @@ -export const LOCAL_MIGRATION_API_TOKEN = 'migration-test-token' diff --git a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx index 74c9c82..2869940 100644 --- a/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx +++ b/examples/storybook/src/stories/staking-migration-widget/StakingMigrationWidget.stories.tsx @@ -3,40 +3,16 @@ import type { Meta, StoryObj } from '@storybook/react' import { YStack } from '@goodwidget/ui' import { StakingMigrationWidget, + derivePrimaryAction, + derivePrimaryLabel, type MigrationStep, - type StakingMigrationPrimaryAction, type StakingMigrationWidgetAdapterFactory, + type StakingMigrationWidgetState, type StakingMigrationWidgetStatus, } from '@goodwidget/staking-migration-widget' import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193' -function deriveMockPrimary( - status: StakingMigrationWidgetStatus, - stakedAmountRaw: bigint, -): { primaryAction: StakingMigrationPrimaryAction; primaryLabel: string } { - if (stakedAmountRaw <= 0n) { - return { primaryAction: 'none', primaryLabel: 'No balance' } - } - - switch (status) { - case 'wrong-network': - return { primaryAction: 'switch_chain', primaryLabel: 'Switch to Fuse' } - case 'approval-pending': - return { primaryAction: 'none', primaryLabel: 'Approval pending' } - case 'migrating': - return { primaryAction: 'none', primaryLabel: 'Migrating' } - case 'success': - return { primaryAction: 'refresh', primaryLabel: 'Refresh balance' } - case 'approval-failed': - return { primaryAction: 'retry', primaryLabel: 'Retry approval' } - case 'error': - return { primaryAction: 'retry', primaryLabel: 'Retry migration' } - default: - return { primaryAction: 'migrate', primaryLabel: 'Approve & Migrate' } - } -} - -function createAdapterFactory( +function createMockState( status: StakingMigrationWidgetStatus, overrides: { stakedAmount?: string @@ -48,31 +24,41 @@ function createAdapterFactory( hasRequiredConfig?: boolean isWrongNetwork?: boolean } = {}, -): StakingMigrationWidgetAdapterFactory { - return () => { - const stakedAmountRaw = overrides.stakedAmountRaw ?? 250000n - const primary = deriveMockPrimary(status, stakedAmountRaw) +): StakingMigrationWidgetState { + const stakedAmountRaw = overrides.stakedAmountRaw ?? 250000n + const state: StakingMigrationWidgetState = { + status, + address: '0x329377cbeeF39f01b0Ea04B80465c9eB47D3ED1', + chainId: 122, + stakedAmount: overrides.stakedAmount ?? '2500', + stakedAmountRaw, + stakedTokenSymbol: 'sG$', + hasRequiredConfig: overrides.hasRequiredConfig ?? true, + isWrongNetwork: overrides.isWrongNetwork ?? false, + isBalanceLoading: false, + completedSteps: overrides.completedSteps ?? [], + activeStep: overrides.activeStep ?? null, + failedStep: overrides.failedStep ?? null, + approvalTxHash: '0xapprovalhash', + migrationId: 'migration-1', + error: overrides.error ?? null, + primaryAction: 'none', + primaryLabel: '', + } + const primaryAction = derivePrimaryAction(state) + return { + ...state, + primaryAction, + primaryLabel: derivePrimaryLabel(state, primaryAction), + } +} - return { - state: { - status, - address: '0x329377cbeeF39f01b0Ea04B80465c9eB47D3ED1', - chainId: 122, - stakedAmount: overrides.stakedAmount ?? '2500', - stakedAmountRaw, - stakedTokenSymbol: 'sG$', - hasRequiredConfig: overrides.hasRequiredConfig ?? true, - isWrongNetwork: overrides.isWrongNetwork ?? false, - isBalanceLoading: false, - completedSteps: overrides.completedSteps ?? [], - activeStep: overrides.activeStep ?? null, - failedStep: overrides.failedStep ?? null, - approvalTxHash: '0xapprovalhash', - migrationId: 'migration-1', - error: overrides.error ?? null, - primaryAction: primary.primaryAction, - primaryLabel: primary.primaryLabel, - }, +function createAdapterFactory( + status: StakingMigrationWidgetStatus, + overrides: Parameters[1] = {}, +): StakingMigrationWidgetAdapterFactory { + return () => ({ + state: createMockState(status, overrides), actions: { connect: async () => {}, switchToFuse: async () => {}, @@ -80,8 +66,7 @@ function createAdapterFactory( approveAndMigrate: async () => {}, retryMigration: async () => {}, }, - } - } + }) } function StoryShell({ adapterFactory }: { adapterFactory: StakingMigrationWidgetAdapterFactory }) { @@ -89,11 +74,7 @@ function StoryShell({ adapterFactory }: { adapterFactory: StakingMigrationWidget const provider = createCustodialEip1193Provider() return ( - + ) } catch (error: unknown) { diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 037d645..997fda0 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -5,18 +5,16 @@ import { Card, Text, ToastContainer, YStack } from '@goodwidget/ui' import { MigrationProgressTimeline } from './MigrationProgressTimeline' import { MigrationSummaryCard } from './MigrationSummaryCard' import { useStakingMigrationAdapter } from './adapter' -import type { - StakingMigrationWidgetAdapterFactory, - StakingMigrationWidgetProps, -} from './widgetRuntimeContract' +import type { StakingMigrationWidgetProps } from './widgetRuntimeContract' -interface StakingMigrationInnerProps { - environment: StakingMigrationWidgetProps['environment'] - migrationApiToken: StakingMigrationWidgetProps['migrationApiToken'] - adapterFactory?: StakingMigrationWidgetAdapterFactory - onMigrationSuccess?: StakingMigrationWidgetProps['onMigrationSuccess'] - onMigrationError?: StakingMigrationWidgetProps['onMigrationError'] -} +type StakingMigrationInnerProps = Pick< + StakingMigrationWidgetProps, + | 'migrationApiBaseUrl' + | 'migrationApiToken' + | 'adapterFactory' + | 'onMigrationSuccess' + | 'onMigrationError' +> function formatJourneyLabel(label: string | null): string | null { if (!label) return null @@ -27,14 +25,14 @@ function formatJourneyLabel(label: string | null): string | null { } function StakingMigrationInner({ - environment, + migrationApiBaseUrl, migrationApiToken, adapterFactory, onMigrationSuccess, onMigrationError, }: StakingMigrationInnerProps) { const defaultAdapter = useStakingMigrationAdapter({ - environment, + migrationApiBaseUrl, migrationApiToken, onMigrationSuccess, onMigrationError, @@ -44,11 +42,11 @@ function StakingMigrationInner({ () => adapterFactory ? adapterFactory({ - environment: environment ?? 'production', + migrationApiBaseUrl, migrationApiToken, }) : defaultAdapter, - [adapterFactory, defaultAdapter, environment, migrationApiToken], + [adapterFactory, defaultAdapter, migrationApiBaseUrl, migrationApiToken], ) const { state, actions } = activeAdapter @@ -126,8 +124,8 @@ function StakingMigrationInner({ {state.status === 'missing-config' && ( - Missing migration configuration: Set a supported environment - (`production`, `staging`, or `development`) before enabling migration. + Missing migration configuration: Provide a migration API base + URL before enabling migration. )} @@ -141,7 +139,7 @@ export function StakingMigrationWidget({ config, defaultTheme = 'light', themeOverrides, - environment = 'production', + migrationApiBaseUrl, migrationApiToken, onMigrationSuccess, onMigrationError, @@ -155,7 +153,7 @@ export function StakingMigrationWidget({ themeOverrides={themeOverrides} > void onMigrationError?: (detail: StakingMigrationErrorDetail) => void @@ -407,7 +402,7 @@ async function watchMigrationProgress( } } -function derivePrimaryAction(state: StakingMigrationWidgetState): StakingMigrationPrimaryAction { +export function derivePrimaryAction(state: StakingMigrationWidgetState): StakingMigrationPrimaryAction { if (state.isBalanceLoading) return 'none' if (!state.hasRequiredConfig || state.status === 'missing-config') return 'none' if (state.stakedAmountRaw <= 0n) return 'none' @@ -419,7 +414,7 @@ function derivePrimaryAction(state: StakingMigrationWidgetState): StakingMigrati return 'migrate' } -function derivePrimaryLabel( +export function derivePrimaryLabel( state: StakingMigrationWidgetState, primaryAction: StakingMigrationPrimaryAction, ): string { @@ -446,21 +441,16 @@ function derivePrimaryLabel( } export function useStakingMigrationAdapter({ - environment, + migrationApiBaseUrl, migrationApiToken, onMigrationSuccess, onMigrationError, }: UseStakingMigrationAdapterOptions = {}): StakingMigrationWidgetAdapterResult { const { address, chainId, isConnected, provider, connect } = useWallet() - const resolvedEnvironment = useMemo( - () => normalizeStakingMigrationEnvironment(environment), - [environment], - ) - const resolvedConfig = useMemo( - () => resolveMigrationConfigForEnvironment(resolvedEnvironment, migrationApiToken), - [migrationApiToken, resolvedEnvironment], + () => resolveMigrationConfig({ migrationApiBaseUrl, migrationApiToken }), + [migrationApiBaseUrl, migrationApiToken], ) const [state, setState] = useState(() => ({ @@ -713,7 +703,7 @@ export function useStakingMigrationAdapter({ ...previousState, status: 'missing-config', hasRequiredConfig: false, - error: 'Migration backend configuration is unavailable for the selected environment', + error: 'Migration backend configuration is unavailable', })) return } @@ -831,7 +821,7 @@ export function useStakingMigrationAdapter({ setState((previousState) => ({ ...previousState, status: 'missing-config', - error: 'Migration backend configuration is unavailable for the selected environment', + error: 'Migration backend configuration is unavailable', })) return } diff --git a/packages/staking-migration-widget/src/index.ts b/packages/staking-migration-widget/src/index.ts index 105b998..3cc38e0 100644 --- a/packages/staking-migration-widget/src/index.ts +++ b/packages/staking-migration-widget/src/index.ts @@ -2,7 +2,11 @@ export { stakingMigrationIntegration } from './integration' export type { StakingMigrationIntegration } from './integration' export { StakingMigrationWidget } from './StakingMigrationWidget' -export { useStakingMigrationAdapter } from './adapter' +export { + derivePrimaryAction, + derivePrimaryLabel, + useStakingMigrationAdapter, +} from './adapter' export type { UseStakingMigrationAdapterOptions } from './adapter' export type { @@ -13,7 +17,6 @@ export type { StakingMigrationWidgetAdapterFactoryInput, StakingMigrationWidgetAdapterResult, StakingMigrationWidgetActions, - StakingMigrationWidgetEnvironment, StakingMigrationWidgetProps, StakingMigrationWidgetState, StakingMigrationWidgetStatus, @@ -24,12 +27,3 @@ export { FUSE_CHAIN_ID, FUSE_STAKING_CONTRACT_ADDRESS, } from './widgetRuntimeContract' - -export { - MIGRATION_OPERATOR_ADDRESS, - normalizeStakingMigrationEnvironment, - resolveMigrationConfigForEnvironment, - stakingMigrationCapabilities, -} from './migrationEnvironments' - -export type { ResolvedStakingMigrationConfig } from './migrationEnvironments' diff --git a/packages/staking-migration-widget/src/integration.ts b/packages/staking-migration-widget/src/integration.ts index 4043f3c..c4dd117 100644 --- a/packages/staking-migration-widget/src/integration.ts +++ b/packages/staking-migration-widget/src/integration.ts @@ -1,5 +1,3 @@ -export { stakingMigrationCapabilities } from './migrationEnvironments' - export const stakingMigrationIntegration = { id: 'staking-migration', capabilitySource: 'stakingMigrationCapabilities', diff --git a/packages/staking-migration-widget/src/migrationEnvironments.ts b/packages/staking-migration-widget/src/migrationEnvironments.ts index b2deb75..facbcf6 100644 --- a/packages/staking-migration-widget/src/migrationEnvironments.ts +++ b/packages/staking-migration-widget/src/migrationEnvironments.ts @@ -1,7 +1,5 @@ import type { Address } from 'viem' -export type StakingMigrationWidgetEnvironment = 'production' | 'staging' | 'development' - export const stakingMigrationCapabilities = { environments: ['production', 'staging', 'development'] as const, chains: [122], @@ -10,48 +8,23 @@ export const stakingMigrationCapabilities = { export const MIGRATION_OPERATOR_ADDRESS: Address = '0xE3441bA0863AEFBf28eca5F6fAAFb4A2B608F3A1' -interface MigrationEnvironmentPreset { - migrationApiBaseUrl: string -} - -const MIGRATION_ENVIRONMENT_PRESETS: Record< - StakingMigrationWidgetEnvironment, - MigrationEnvironmentPreset -> = { - development: { - migrationApiBaseUrl: 'http://localhost:8787', - }, - staging: { - migrationApiBaseUrl: 'https://monitoringworker-staging.gooddollar.workers.dev', - }, - production: { - migrationApiBaseUrl: 'https://monitoringworker.gooddollar.workers.dev', - }, -} - export interface ResolvedStakingMigrationConfig { - migrationApiBaseUrl: string + migrationApiBaseUrl?: string migrationOperator: Address migrationApiToken?: string } -export function resolveMigrationConfigForEnvironment( - environment: StakingMigrationWidgetEnvironment, - migrationApiToken?: string, +export interface ResolveMigrationConfigInput { + migrationApiBaseUrl?: string + migrationApiToken?: string +} + +export function resolveMigrationConfig( + input: ResolveMigrationConfigInput = {}, ): ResolvedStakingMigrationConfig { - const preset = MIGRATION_ENVIRONMENT_PRESETS[environment] return { - migrationApiBaseUrl: preset.migrationApiBaseUrl, + migrationApiBaseUrl: input.migrationApiBaseUrl, migrationOperator: MIGRATION_OPERATOR_ADDRESS, - migrationApiToken, - } -} - -export function normalizeStakingMigrationEnvironment( - environment?: StakingMigrationWidgetEnvironment, -): StakingMigrationWidgetEnvironment { - if (environment && stakingMigrationCapabilities.environments.includes(environment)) { - return environment + migrationApiToken: input.migrationApiToken, } - return 'production' } diff --git a/packages/staking-migration-widget/src/widgetRuntimeContract.ts b/packages/staking-migration-widget/src/widgetRuntimeContract.ts index 3b041b7..74abab3 100644 --- a/packages/staking-migration-widget/src/widgetRuntimeContract.ts +++ b/packages/staking-migration-widget/src/widgetRuntimeContract.ts @@ -1,9 +1,5 @@ import type { Address } from 'viem' import type { GoodWidgetConfig, GoodWidgetThemeOverrides } from '@goodwidget/ui' -import type { StakingMigrationWidgetEnvironment } from './migrationEnvironments' - -export type { StakingMigrationWidgetEnvironment } from './migrationEnvironments' - export const FUSE_CHAIN_ID = 122 export const FUSE_STAKING_CONTRACT_ADDRESS: Address = '0xB7C3e738224625289C573c54d402E9Be46205546' @@ -75,7 +71,7 @@ export interface StakingMigrationWidgetAdapterResult { } export interface StakingMigrationWidgetAdapterFactoryInput { - environment: StakingMigrationWidgetEnvironment + migrationApiBaseUrl?: string migrationApiToken?: string } @@ -88,7 +84,7 @@ export interface StakingMigrationWidgetProps { config?: GoodWidgetConfig defaultTheme?: 'light' | 'dark' themeOverrides?: GoodWidgetThemeOverrides - environment?: StakingMigrationWidgetEnvironment + migrationApiBaseUrl?: string migrationApiToken?: string onMigrationSuccess?: (detail: StakingMigrationSuccessDetail) => void onMigrationError?: (detail: StakingMigrationErrorDetail) => void From ceb3caa1a8c9a9a47f5fc7e4d999069c434ac45c Mon Sep 17 00:00:00 2001 From: blueogin Date: Wed, 10 Jun 2026 14:38:41 -0400 Subject: [PATCH 25/27] chore: remove GoodDollar skill pack and related documentation files --- .agents/skills/gooddollar/CONTRIBUTING.md | 121 -- .agents/skills/gooddollar/SKILL.md | 215 ---- .../contracts/BuyGDCloneFactory.abi.yaml | 205 ---- .../BuyGDCloneFactory.selectors.yaml | 27 - .../contracts/BuyGDCloneV2.abi.yaml | 282 ----- .../contracts/BuyGDCloneV2.selectors.yaml | 36 - .../contracts/CFAv1Forwarder.abi.yaml | 237 ---- .../contracts/CFAv1Forwarder.selectors.yaml | 19 - .../ConstantFlowAgreementV1.abi.yaml | 560 --------- .../ConstantFlowAgreementV1.selectors.yaml | 33 - .../contracts/GoodDollarOFTAdapter.abi.yaml | 100 -- .../GoodDollarOFTAdapter.selectors.yaml | 12 - .../GooddollarSavingsStream.abi.yaml | 210 ---- .../GooddollarSavingsStream.selectors.yaml | 27 - .../contracts/GovernanceStakingV2.abi.yaml | 202 ---- .../GovernanceStakingV2.selectors.yaml | 26 - .../references/contracts/IdentityV3.abi.yaml | 390 ------- .../contracts/IdentityV3.selectors.yaml | 49 - .../references/contracts/IdentityV4.abi.yaml | 424 ------- .../contracts/IdentityV4.selectors.yaml | 51 - .../references/contracts/InvitesV2.abi.yaml | 333 ------ .../contracts/InvitesV2.selectors.yaml | 41 - .../references/contracts/MentoBroker.abi.yaml | 259 ----- .../contracts/MentoBroker.selectors.yaml | 24 - .../contracts/MessagePassingBridge.abi.yaml | 542 --------- .../MessagePassingBridge.selectors.yaml | 52 - .../references/contracts/NameService.abi.yaml | 104 -- .../contracts/NameService.selectors.yaml | 11 - .../references/contracts/SuperToken.abi.yaml | 1033 ----------------- .../contracts/SuperToken.selectors.yaml | 99 -- .../references/contracts/Superfluid.abi.yaml | 757 ------------ .../contracts/Superfluid.selectors.yaml | 51 - .../references/contracts/UBISchemeV2.abi.yaml | 305 ----- .../contracts/UBISchemeV2.selectors.yaml | 40 - .../contracts/_rich-abi-yaml-format.md | 93 -- .../deep-researches/faucet-flows.md | 42 - .../fuse-to-celo-staking-migration.md | 38 - .../gooddao-daostack-surface.md | 37 - .../deep-researches/how-ubi-is-minted.md | 68 -- .../inviter-invitee-reward-model.md | 58 - .../mento-reserve-economics.md | 27 - .../deep-researches/on-off-ramp-service.md | 69 -- .../gooddollar/references/guides/bridge.md | 187 --- .../references/guides/check-identity.md | 87 -- .../gooddollar/references/guides/claim.md | 83 -- .../gooddollar/references/guides/faucet.md | 76 -- .../gooddollar/references/guides/gooddocs.md | 37 - .../gooddollar/references/guides/goodsdks.md | 110 -- .../references/guides/hypersync-hyperrpc.md | 110 -- .../references/guides/invite-bounties.md | 115 -- .../migrate-fuse-staking-to-celo-savings.md | 116 -- .../references/guides/on-off-ramp.md | 77 -- .../gooddollar/references/guides/save.md | 93 -- .../gooddollar/references/guides/stream.md | 135 --- .../gooddollar/references/guides/swap.md | 83 -- .../references/subgraphs/_query-patterns.md | 35 - .../subgraphs/goodcollective-guide.md | 54 - .../subgraphs/goodcollective.graphql | 200 ---- .../subgraphs/gooddollar-celo-guide.md | 104 -- .../subgraphs/gooddollar-celo.graphql | 113 -- .../subgraphs/reserve-celo-guide.md | 41 - .../references/subgraphs/reserve-celo.graphql | 90 -- skills-lock.json | 11 - 63 files changed, 9266 deletions(-) delete mode 100644 .agents/skills/gooddollar/CONTRIBUTING.md delete mode 100644 .agents/skills/gooddollar/SKILL.md delete mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/NameService.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/NameService.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/faucet-flows.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md delete mode 100644 .agents/skills/gooddollar/references/guides/bridge.md delete mode 100644 .agents/skills/gooddollar/references/guides/check-identity.md delete mode 100644 .agents/skills/gooddollar/references/guides/claim.md delete mode 100644 .agents/skills/gooddollar/references/guides/faucet.md delete mode 100644 .agents/skills/gooddollar/references/guides/gooddocs.md delete mode 100644 .agents/skills/gooddollar/references/guides/goodsdks.md delete mode 100644 .agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md delete mode 100644 .agents/skills/gooddollar/references/guides/invite-bounties.md delete mode 100644 .agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md delete mode 100644 .agents/skills/gooddollar/references/guides/on-off-ramp.md delete mode 100644 .agents/skills/gooddollar/references/guides/save.md delete mode 100644 .agents/skills/gooddollar/references/guides/stream.md delete mode 100644 .agents/skills/gooddollar/references/guides/swap.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/_query-patterns.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/goodcollective.graphql delete mode 100644 .agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql delete mode 100644 .agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql delete mode 100644 skills-lock.json diff --git a/.agents/skills/gooddollar/CONTRIBUTING.md b/.agents/skills/gooddollar/CONTRIBUTING.md deleted file mode 100644 index ab855d3..0000000 --- a/.agents/skills/gooddollar/CONTRIBUTING.md +++ /dev/null @@ -1,121 +0,0 @@ -# Contributing to GoodSkills - -This repository is an AI skill pack. The goal of each update is to make agent behavior more reliable, more explicit, and easier to audit. - -## Update workflow - -1. Define the user-facing problem first. -2. For contract-related updates, add or update Rich ABI first (`references/contracts/*.abi.yaml`), then refresh selectors. -3. Decide the remaining artifact types: - - `references/guides/` for "what to do" - - `references/deep-researches/` for "why it works this way" - - `scripts/` for deterministic and repeatable execution -4. Update `SKILL.md` routing so the new artifact is discoverable. -5. Validate consistency (paths, naming, links, selectors, assumptions). - -If a change touches contract behavior, treat Rich ABI update/add as mandatory first step before guides, deep-research, or scripts. - -## Add or update a guide - -Use guides for execution playbooks and operator workflows. - -Required structure: - -- title and one-line usage trigger -- `## Goal` -- `## Required inputs` -- `## Execution flow` as numbered steps -- deterministic snippet when execution is non-trivial -- failure handling and output contract - -Guide rules: - -- prefer explicit pre-checks before state-changing actions -- include only one primary workflow per file -- use [GoodProtocol `releases/deployment.json`](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) as the **only** source for contract addresses (rich ABI `meta.deployments` mirrors those rows); use GoodDocs for behavior and UX, not for resolving addresses; use on-chain `NameService.getAddress` only when the deployment documents the string key -- avoid implementation-deep theory; keep that in deep-research files - -After adding a guide: - -- add it to `SKILL.md` in `Guides` -- add an entry in `Use-case to guide map` - -## Add or update a deep-research note - -Use deep-research files for architecture, rationale, tradeoffs, and root-cause logic. - -Deep-research rules: - -- explain causality, not only API surfaces -- distinguish current behavior from legacy behavior -- link source contracts/docs for traceability -- keep language natural and decision-oriented - -Do not turn deep-research files into step-by-step runbooks; move operational steps into guides. - -## Add or update Rich ABI YAML - -Location: `references/contracts/`. - -For each contract: - -- create or update `Foo.abi.yaml` -- generate or refresh `Foo.selectors.yaml` -- include function-level notes for non-obvious behavior -- when `meta.deployments` lists concrete addresses, add **`creationBlock`** next to each **`address`** (placement: `references/contracts/_rich-abi-yaml-format.md`; using it as **`fromBlock`** for log or HyperSync fetches: `references/guides/hypersync-hyperrpc.md`) - -Minimum ABI documentation quality: - -- correct mutability, inputs, outputs -- access pattern (`owner`, `avatar`, `anyone`, etc.) where relevant -- emitted events and practical errors -- notes for routing/edge-case semantics - -Source-of-truth policy: - -- prefer canonical contract repos (GoodProtocol, GoodBridge, mento-core) -- avoid inferred behavior when source is unclear -- update notes when protocol behavior changed - -Selector generation: - -```bash -node scripts/selectors.mjs generate Foo.abi.yaml -``` - -## Add or update scripts - -Location: `scripts/`. - -Use scripts when: - -- a workflow is repeated -- deterministic output is needed -- manual querying is error-prone - -Script standards: - -- require inputs through env vars or explicit args -- fail loudly with actionable messages -- print structured output for easy reuse -- keep script intent narrow - -When a script supports a guide: - -- reference it from that guide -- document expected inputs and outputs in the guide - -## Naming and organization - -- use lowercase kebab-case for guides and deep-research files -- keep one topic per file -- avoid duplicate guidance across files -- prefer updating existing files over creating near-duplicates - -## Update checklist before merge - -- `SKILL.md` routing updated -- links resolve and point to public sources -- guides and deep-research files respect "what" vs "why" separation -- ABI + selectors pairs are in sync -- new behavior is reflected in notes where needed diff --git a/.agents/skills/gooddollar/SKILL.md b/.agents/skills/gooddollar/SKILL.md deleted file mode 100644 index 2d59116..0000000 --- a/.agents/skills/gooddollar/SKILL.md +++ /dev/null @@ -1,215 +0,0 @@ ---- -name: gooddollar -description: > - Knowledge base for GoodProtocol action execution and GoodDollar (G$) integrations. - Use this skill BEFORE ad-hoc web search for claim, save/stake, swap, bridge, - stream, and identity tasks. Prefer GoodDocs (https://docs.gooddollar.org/) for - narrative; contract addresses only from GoodProtocol releases/deployment.json. -metadata: - version: 1.0.0 -license: MIT ---- - -# GoodDollar Skill Pack - -Routing index for GoodProtocol. This repo complements [GoodDocs](https://docs.gooddollar.org/) for behavior and user flows. **Contract addresses** come only from [GoodProtocol/releases/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (and `meta.deployments` in `references/contracts/*.abi.yaml`, which mirror those rows)—not from GoodDocs pages. - -Repository maintenance and update process is documented in `CONTRIBUTING.md`. - -## Protocol snapshot (from GoodDocs) - -- G$ is reserve-backed; issuance and pricing tie to the reserve and bonding-curve mechanics described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). -- The stack is multi-chain; which contracts exist per environment is defined only in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `GoodDollar`, `Identity`, `NameService`, `UBIScheme`, Mento keys, `MpbBridge`, and related entries under `production`, `production-celo`, and `production-xdc`). -- UBI is daily for verified users; identity verification and connected accounts are documented under [user guides](https://docs.gooddollar.org/user-guides). - -## Guides (single location for action playbooks) - -All task-specific instructions live under `references/guides/`. - -- `references/guides/claim.md` — daily UBI (`claim` / UBIScheme). -- `references/guides/save.md` — stake, rewards, unstake. -- `references/guides/swap.md` — buy or sell G$ (Mento on supported chains). -- `references/guides/bridge.md` — MessagePassingBridge (GoodDocs); optional OFT path via ABI refs. -- `references/guides/stream.md` — Superfluid streams (Celo-oriented in GoodDocs). -- `references/guides/check-identity.md` — whitelist and connected-address semantics. -- `references/guides/goodsdks.md` — SDK-first integration routing for GoodSDKs packages. -- `references/guides/gooddocs.md` — hub links to [GoodDocs](https://docs.gooddollar.org/). -- `references/guides/hypersync-hyperrpc.md` — Envio HyperSync/HyperRPC data-source routing for high-volume historical reads. -- `references/guides/faucet.md` — Faucet gas top-up execution flow and preflight checks. -- `references/guides/on-off-ramp.md` — stable-token ramp service flow into and out of G$. -- `references/guides/invite-bounties.md` — verify and execute inviter-invitee bounty payouts. -- `references/guides/migrate-fuse-staking-to-celo-savings.md` — migrate Fuse governance stake to CELO savings flow. - -## Subgraphs (indexed chain history) - -Use this folder with the same pattern as the protocol subgraph references: one `*-guide.md` plus one companion `.graphql` per deployment. - -For historical on-chain data, **start with the subgraph**: confirm the deployment covers the question (entities and fields in the guide, freshness via `_meta`). If the subgraph does not work for the request—missing schema coverage, stale or lagging indexing, query limits, or endpoint errors—**then** move to **HyperSync** or **HyperRPC** using `references/guides/hypersync-hyperrpc.md`. - -- `references/subgraphs/_query-patterns.md` — cross-cutting query discipline. -- `references/subgraphs/reserve-celo-guide.md` + `references/subgraphs/reserve-celo.graphql` — reserve pricing and swap history. -- `references/subgraphs/gooddollar-celo-guide.md` + `references/subgraphs/gooddollar-celo.graphql` — GoodDollar Celo schema discovery and starter probes. -- `references/subgraphs/goodcollective-guide.md` + `references/subgraphs/goodcollective.graphql` — GoodCollective schema discovery and starter probes. - -For Superfluid protocol subgraphs (streams, pools, vesting schedulers), see [Superfluid documentation](https://docs.superfluid.finance/) and [subgraph endpoints](https://subgraph-endpoints.superfluid.dev/). - -## Historical data routing policy (strict) - -1. Query subgraphs first for all historical/indexed requests. -2. Validate required entities and fields against the target subgraph schema and guide before declaring a gap. -3. Use **HyperSync** or **HyperRPC** fallback only when at least one of these is true: - - required entities or fields are not available in subgraph schema - - indexing lag makes subgraph data stale for the requested range - - query limits or endpoint instability block reliable retrieval -4. Do not start with HyperSync or HyperRPC when subgraph data is available and fresh. -5. HyperRPC fallback requires a valid Envio API key; if missing, **explicitly ask the user** to provide `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` (or paste a full `HYPERRPC_URL`); do not treat anonymous HyperRPC as production. -6. When **HyperSync** is the best option for the query and no Envio API token is available (`ENVIO_API_TOKEN` or equivalent per `references/guides/hypersync-hyperrpc.md`), **explicitly ask the user** to provide the token before proceeding; do not silently substitute anonymous HyperSync usage. -7. When fallback is used, report reason explicitly (schema gap, lag, or reliability issue). - -## Data source decision table - -| Query type | Primary source | Secondary source | Notes | -|---|---|---|---| -| Current on-chain state (latest balances, allowances, config, flags, view calls) | RPC | None | Use direct contract RPC reads for latest state. | -| Historical indexed entity data (time-series, aggregates, protocol entities, event-derived analytics) | Subgraph | HyperSync/HyperRPC | Prefer subgraph first; fall back when it cannot answer. | -| Historical raw on-chain data when subgraph is missing fields/entities or stale | HyperSync | HyperRPC | Prefer HyperSync for bulk scans and data pipelines. | -| Historical data for existing JSON-RPC integrations | HyperRPC | HyperSync | Use HyperRPC when strict JSON-RPC compatibility is required. | - -Decision rule: - -1. If request is current state -> use RPC. -2. If request is historical/indexed -> query subgraph first. -3. If subgraph cannot satisfy request -> fallback to HyperSync or HyperRPC per compatibility and scale needs. -4. HyperRPC fallback requires Envio API key credentials. -5. HyperSync client usage requires an Envio API token; if HyperSync is chosen and the token is missing, explicitly ask the user to provide it (see `references/guides/hypersync-hyperrpc.md`). - -## Mapping data retrieval rule - -Solidity mappings are not iterable on-chain by keyspace scan. Do not assume full-key enumeration is possible from RPC alone. - -When data is stored in mapping-like structures: - -1. Check contract source and ABI for key-discovery paths first: - - events emitted on set or update - - arrays, counters, linked lists, or index getters storing keys - - dedicated pagination or enumerable view functions -2. If key discovery exists, reconstruct key set from those sources and then read mapping entries. -3. If key discovery does not exist, report that complete iteration is not possible from chain state alone. -4. For historical reconstruction, prefer subgraph indexing first; if unavailable, use HyperSync or HyperRPC log scans with explicit limitations. - -## Use-case to guide map - -- Claim requests -> `references/guides/claim.md` -- Eligibility or connected-address questions -> `references/guides/check-identity.md` -- Stake, save, unstake -> `references/guides/save.md` -- Buy or sell G$ against reserve rails -> `references/guides/swap.md` -- Cross-chain bridge -> `references/guides/bridge.md` -- Stream management -> `references/guides/stream.md` -- SDK app integration tasks -> `references/guides/goodsdks.md` -- Bulk historical reads or data-engineering fetches -> `references/guides/hypersync-hyperrpc.md` -- Faucet top-up tasks -> `references/guides/faucet.md` -- On-/off-ramp service flow tasks -> `references/guides/on-off-ramp.md` -- Invite bounty eligibility and payout tasks -> `references/guides/invite-bounties.md` -- Fuse to CELO staking migration tasks -> `references/guides/migrate-fuse-staking-to-celo-savings.md` -- Indexed history, analytics, or GraphQL against GoodDollar subgraphs -> `references/subgraphs/_query-patterns.md` -- Historical on-chain fetch when subgraph data is insufficient -> subgraphs first, then HyperSync or HyperRPC per `references/guides/hypersync-hyperrpc.md`; if HyperSync is best and `ENVIO_API_TOKEN` is missing, ask the user for it explicitly. - -## Ambiguous prompts and incomplete inputs - -Stop and **ask the user** whenever the task is underspecified or required facts are missing. List what you need in short, concrete questions (for example chain, contract, address, amount, account, RPC or signer access, time or block range, prior tx hash, approval scope). - -- **Ambiguous** means the goal, environment, contract surface, or acceptance criteria are not clear enough to choose a safe path. -- **Incomplete** means you lack inputs that would change what you build, call, or sign next. - -**Do not invent** chain, address, amount, or policy details that affect correctness, funds, or eligibility. For **information-only** work you may state a single explicit assumption, label it, and ask the user to confirm or correct it before going further. - -**Execution work** (writing or editing runnable code, sending transactions, migrations, or anything that can move funds or alter on-chain state) has **no guessing**: settle every required input with the user, then implement or run. - -## Execution rules - -1. Collect missing required inputs before sending transactions. -2. Run pre-checks first (allowance, whitelist, quotes, bridge **amount** limits, peer wiring when using OFT paths). -3. If a pre-check fails, stop and return the exact corrective action. -4. Return tx hash and key output values. -5. Never fabricate addresses, amounts, or ABI behavior. -6. Resolve decimals and units per chain as in [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (for example 18 decimals on Celo, 2 on Fuse and Ethereum where applicable). - -## Pre-check matrix - -- Claim: verify identity whitelist status before `claim()`. -- Save or stake: verify balance and allowance before `stake()`. -- Swap: fetch quote, apply slippage bounds, verify allowance; confirm Mento contract keys for the active chain exist in `deployment.json` (for example `MentoBroker` under `production-celo` or `production-xdc`). -- Bridge (MessagePassingBridge): on the **source** chain approve G$ to the bridge; optionally preflight `canBridge(from, amount)` on that same contract (outbound `_bridgeTo` does not call it internally). For LZ use `estimateSendFee` with the **normalized** burn amount per `references/guides/bridge.md`, then `bridgeToWithLz` with nonzero `msg.value` for the **cross-chain transport** fee only (distinct from destination **`bridgeFees`** on minted G$; see **Bridge fee context** in that guide). Read **`bridgeLimits`** / daily trackers when debugging **amount** caps; see **Bridge amount limit context** in that guide. Respect `isClosed`, `LZ_FEE`, `MISSING_FEE`, and `UNSUPPORTED_CHAIN`. **Destination** mint applies `_enforceLimits` and can still revert. Use **Axelar** only when `toAxelarChainId` returns a route (implementation maps 1, 5, 42220, 44787); for Fuse or XDC style targets prefer LZ unless mapping is extended on-chain. -- Bridge (OFT adapter path): verify peer wiring and `quoteSend` fee data. -- Stream: confirm Celo (or documented Superfluid network) and correct Super Token and forwarder or host addresses. -- Identity: resolve Identity from NameService; remember connected addresses do not multiply daily claims ([connect wallet guide](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity)). - -## Output format requirements - -For any state-changing action return: - -- network and key contract addresses used -- normalized input amounts and min or max guards -- tx hash -- key post-state output when available -- follow-up action if user intervention is required - -## Rich contract ABI references - -Convention: each `Foo.abi.yaml` has a companion `Foo.selectors.yaml` (function, event, and custom error selectors). Schema: `references/contracts/_rich-abi-yaml-format.md`. - -GoodDollar / Mento: - -- `references/contracts/NameService.abi.yaml` -- `references/contracts/IdentityV3.abi.yaml` -- `references/contracts/IdentityV4.abi.yaml` -- `references/contracts/InvitesV2.abi.yaml` -- `references/contracts/BuyGDCloneFactory.abi.yaml` -- `references/contracts/BuyGDCloneV2.abi.yaml` -- `references/contracts/GovernanceStakingV2.abi.yaml` -- `references/contracts/GooddollarSavingsStream.abi.yaml` (Ubeswap Superfluid stream savings; Celo deployment) -- `references/contracts/UBISchemeV2.abi.yaml` -- `references/contracts/MentoBroker.abi.yaml` -- `references/contracts/MessagePassingBridge.abi.yaml` -- `references/contracts/GoodDollarOFTAdapter.abi.yaml` -- `references/contracts/CFAv1Forwarder.abi.yaml` -- `references/contracts/ConstantFlowAgreementV1.abi.yaml` -- `references/contracts/Superfluid.abi.yaml` -- `references/contracts/SuperToken.abi.yaml` - -Superfluid (CFA, CFAv1Forwarder, Host, full ABI library): use [Superfluid docs](https://docs.superfluid.finance/), npm packages such as `@superfluid-finance/ethereum-contracts` and `@sfpro/sdk`, and contract ABIs published with those packages. - -## Deep researches - -- `references/deep-researches/on-off-ramp-service.md` -- `references/deep-researches/how-ubi-is-minted.md` -- `references/deep-researches/inviter-invitee-reward-model.md` -- `references/deep-researches/mento-reserve-economics.md` -- `references/deep-researches/gooddao-daostack-surface.md` -- `references/deep-researches/faucet-flows.md` -- `references/deep-researches/fuse-to-celo-staking-migration.md` - -## Revert debugging quick map - -- Identity or eligibility errors -> Identity and UBIScheme ABIs; live addresses from `deployment.json` only; GoodDocs for whitelist and claim behavior. -- Approval or transfer failures -> token approvals and balances; see integration guide for `transferAndCall` vs `approve` plus `transferFrom`. -- Swap bound failures -> quote freshness and slippage settings. -- MessagePassingBridge failures -> `canBridge`; **`BRIDGE_LIMITS`** (amount caps, whitelist, **`closed`**, and related policy strings); transport `msg.value` (`MISSING_FEE`, `LZ_FEE`) vs destination protocol fee (`bridgeFees`, `feeRecipient`); correct `bridgeTo` arguments; [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars). -- OFT path failures -> peer wiring and `quoteSend` fee data. -- Stream failures -> CFA forwarder or host agreement calls, buffer and flow-rate limits per Superfluid docs linked from GoodDocs. -- Faucet top-up failures -> `canTop`, `onlyAuthorized`, daily or weekly caps; `references/deep-researches/faucet-flows.md`. -- DAO-gated reverts -> caller is not avatar; scheme not registered; `references/deep-researches/gooddao-daostack-surface.md`. - -## Library usage discipline - -1. Open `references/guides/gooddocs.md` when unsure which GoodDocs page applies. -2. Start at this file to classify intent. -3. Open one guide under `references/guides/` unless the user requests a multi-step workflow. For subgraph or indexed-data tasks, start at `references/subgraphs/_query-patterns.md`. -4. Read only the ABI references and matching `.selectors.yaml` files needed for the chosen action. -5. Prefer GoodDocs for documented behavior; use only `deployment.json` (and rich ABI `meta.deployments` aligned with it) for contract addresses—never infer addresses from GoodDocs. -6. For large historical reads, prefer `references/guides/hypersync-hyperrpc.md` and choose HyperSync over HyperRPC unless strict JSON-RPC compatibility is required. -7. Historical data routing is strict: subgraphs first; HyperSync or HyperRPC only with an explicit fallback reason. -8. HyperRPC usage requires Envio API key credentials; when absent, **explicitly ask the user** for `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` (or a full `HYPERRPC_URL`) and do not attempt anonymous production flow. -9. When HyperSync is the best historical-data path and no Envio API token is available, explicitly ask the user to provide `ENVIO_API_TOKEN` (or the token your client expects) before continuing; see `references/guides/hypersync-hyperrpc.md`. -10. For subgraph tasks, validate field availability from the relevant `references/subgraphs/*-guide.md` and companion `.graphql` before guessing alternate entities. -11. For local shells repeating HyperRPC log pulls (for example last N whitelist events), from the **GoodSkills repository root** run `scripts/fetch-whitelist-events-hyperrpc.mjs` per `references/guides/hypersync-hyperrpc.md` instead of re-deriving JSON-RPC setup each time; that script ships with **defaults for production Celo** (HyperRPC host + `Identity` contract from `deployment.json`) and URL composition from `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` unless you override `CONTRACT_ADDRESS` / `HYPERRPC_URL`. HyperSync remains a separate client install path documented in the same guide. diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml deleted file mode 100644 index 40226b3..0000000 --- a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml +++ /dev/null @@ -1,205 +0,0 @@ -meta: - name: BuyGDCloneFactory - version: "1" - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/BuyGDClone.sol - note: > - Deterministic clone factory for BuyGDCloneV2 and DonateGDClone. Used to create per-owner - executors and optional create+swap flows for ramp services. - deployments: - mainnet: - production-celo: - BuyGDFactoryV2: - networkId: 42220 - address: "0x1F60C4C7037C6766924A43666B781ED1479587a2" - creationBlock: 22909812 - BuyGDFactory: - networkId: 42220 - address: "0x00e533B7d6255D05b7f15034B1c989c21F51b91C" - creationBlock: 21006324 - related: - - references/deep-researches/on-off-ramp-service.md - - references/guides/on-off-ramp.md - -create: - notes: - - "Deploys deterministic BuyGDCloneV2 clone for owner and initializes it." - - "Difference vs createAndSwap: this only deploys; no swap is executed." - mutability: nonpayable - access: anyone - inputs: - - owner: address - outputs: - - clone: address - -createDonation: - notes: - - "Deploys deterministic DonateGDClone with donation target and call payload." - - "Difference vs create: deploys donation-capable implementation instead of plain swap clone." - mutability: nonpayable - access: anyone - inputs: - - owner: address - - donateOrExecTo: address - - callData: bytes - outputs: - - clone: address - -createAndSwap: - notes: - - "One-transaction helper: create BuyGDCloneV2 then immediately call clone.swap(minAmount, msg.sender)." - - "Difference vs create: bundles deployment and first swap for faster ramp UX." - mutability: nonpayable - access: anyone - inputs: - - owner: address - - minAmount: uint256 - outputs: - - clone: address - -createDonationAndSwap: - notes: - - "One-transaction helper for DonateGDClone: deploy, then execute donation flow with optional pre-swap." - - "Difference vs createDonation: can execute immediate swap and/or target call instead of deployment only." - mutability: nonpayable - access: anyone - inputs: - - owner: address - - donateOrExecTo: address - - withSwap: bool - - minAmount: uint256 - - callData: bytes - outputs: - - clone: address - -predict: - notes: - - "Computes deterministic address for create(owner) salt." - mutability: view - inputs: - - owner: address - outputs: - - clone: address - -predictDonation: - notes: - - "Computes deterministic address for createDonation(owner, donateOrExecTo, callData) salt." - mutability: view - inputs: - - owner: address - - donateOrExecTo: address - - callData: bytes - outputs: - - clone: address - -getBaseFee: - mutability: view - inputs: [] - outputs: - - baseFee: uint256 - -quoter: - mutability: pure - inputs: [] - outputs: - - addr: address - -CUSD: - mutability: pure - inputs: [] - outputs: - - token: address - -celo: - mutability: pure - inputs: [] - outputs: - - token: address - -USDC: - mutability: pure - inputs: [] - outputs: - - token: address - -GLOUSD: - mutability: pure - inputs: [] - outputs: - - token: address - -PERIOD: - mutability: pure - inputs: [] - outputs: - - seconds: uint24 - -impl: - mutability: view - inputs: [] - outputs: - - addr: address - -donateImpl: - mutability: view - inputs: [] - outputs: - - addr: address - -gd: - mutability: view - inputs: [] - outputs: - - token: address - -stable: - mutability: view - inputs: [] - outputs: - - token: address - -oracle: - mutability: view - inputs: [] - outputs: - - addr: address - -router: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoBroker: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoExchangeProvider: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoExchangeId: - mutability: view - inputs: [] - outputs: - - id: bytes32 - -events: - GDSwapToCusd: - indexed: [] - data: - - from: address - - to: address - - amountIn: uint256 - - amountOut: uint256 - - note: bytes - -errors: - NOT_GD_TOKEN: "onTokenTransfer caller is not G$ token." - INVALID_TWAP: "TWAP validation failed." - RECIPIENT_ZERO: "Recipient cannot be zero address." - ZERO_MINAMOUNT: "Minimum amount cannot be zero." diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml deleted file mode 100644 index 16b13f2..0000000 --- a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - create(address): 0x9ed93318 - createDonation(address,address,bytes): 0x48199e6b - createAndSwap(address,uint256): 0x89643a29 - createDonationAndSwap(address,address,bool,uint256,bytes): 0xeb5621b8 - predict(address): 0x901b96e7 - predictDonation(address,address,bytes): 0x3895de30 - getBaseFee(): 0x15e812ad - quoter(): 0xc6bbd5a7 - CUSD(): 0x758316c9 - celo(): 0x051ed8ef - USDC(): 0x89a30271 - GLOUSD(): 0x4b5b02d6 - PERIOD(): 0xb4d1d795 - impl(): 0x8abf6077 - donateImpl(): 0x21fc2eef - gd(): 0xa5e598fc - stable(): 0x22be3de1 - oracle(): 0x7dc0d1d0 - router(): 0xf887ea40 - mentoBroker(): 0x7b89f117 - mentoExchangeProvider(): 0x4f62feec - mentoExchangeId(): 0xd373b333 -events: - GDSwapToCusd(address,address,uint256,uint256,bytes): 0x252bc23e3fb01f9986fa157af621236fb8a706ea12622da296e7f2f30d4f1a56 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml deleted file mode 100644 index 5f35ee9..0000000 --- a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml +++ /dev/null @@ -1,282 +0,0 @@ -meta: - name: BuyGDCloneV2 - version: "2" - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/BuyGDClone.sol - inherits: - - Initializable - note: > - Clone implementation used by BuyGDCloneFactory for deterministic per-owner swap executors. - Supports CELO and cUSD flows, with cUSD route selection between Uniswap and Mento when configured. - deployments: - mainnet: - production-celo: - BuyGDFactoryV2: - networkId: 42220 - address: "0x1F60C4C7037C6766924A43666B781ED1479587a2" - creationBlock: 22909812 - related: - - references/deep-researches/on-off-ramp-service.md - - references/guides/on-off-ramp.md - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _owner: address - outputs: [] - -getSwapPath: - mutability: pure - inputs: - - tokens: address[] - - fees: uint24[] - outputs: - - path: bytes - -swap: - notes: - - "Dispatcher entrypoint: if clone holds native CELO balance it executes CELO route, otherwise uses cUSD route." - - "Difference vs swapCelo/swapCusd: this function chooses route by contract balances; specialized functions force a specific asset path." - mutability: payable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - outputs: - - bought: uint256 - emits: [Bought] - errors: [NO_BALANCE] - -swapCelo: - notes: - - "CELO-only convenience path using the default preconfigured Uniswap route." - - "Difference vs swapCeloWithPath: same Uniswap execution, but caller cannot override token path and fee tiers." - - "Difference vs cUSD paths: CELO path does not use Mento route selection." - mutability: payable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - outputs: - - bought: uint256 - emits: [BoughtFromUniswap] - errors: [REFUND_FAILED] - -swapCeloWithPath: - notes: - - "CELO-only path with caller-supplied Uniswap multi-hop route." - - "Difference vs swapCelo: allows custom path optimization when default route is not preferred." - - "Difference vs cUSD paths: still Uniswap-only and does not compare against Mento." - mutability: payable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - - _path: (address[],uint24[]) - outputs: - - bought: uint256 - emits: [BoughtFromUniswap] - errors: [REFUND_FAILED] - -swapCusd: - notes: - - "cUSD-only convenience path that compares expected output from default-path Uniswap and Mento, then executes the better quote." - - "Difference vs swapCusdWithPath: this uses hardcoded default Uniswap path for comparison." - - "Difference vs CELO paths: includes dual-route best-execution logic with optional Mento." - mutability: nonpayable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - outputs: - - bought: uint256 - emits: [BoughtFromMento, BoughtFromUniswap] - errors: [MENTO_NOT_CONFIGURED] - -swapCusdWithPath: - notes: - - "cUSD path with caller-supplied Uniswap route; still compares custom Uniswap quote against Mento quote and picks larger expected output." - - "Difference vs swapCusd: custom path changes only the Uniswap side of the comparison." - - "Difference vs swapCeloWithPath: this function performs route competition (Uniswap vs Mento), not only route customization." - mutability: nonpayable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - - _path: (address[],uint24[]) - outputs: - - bought: uint256 - emits: [BoughtFromMento, BoughtFromUniswap] - errors: [MENTO_NOT_CONFIGURED] - -getExpectedReturnFromUniswapPath: - notes: - - "Quote helper for Uniswap path expected output used by cUSD route selection and preflight checks." - mutability: nonpayable - inputs: - - amountIn: uint256 - - _path: (address[],uint24[]) - outputs: - - expectedReturn: uint256 - -getExpectedReturnFromMento: - notes: - - "Quote helper for Mento expected output for cUSD->G$ used in best-route decision." - mutability: view - inputs: - - cusdAmount: uint256 - outputs: - - expectedReturn: uint256 - errors: [MENTO_NOT_CONFIGURED] - -minAmountByTWAP: - mutability: view - inputs: - - baseAmount: uint256 - - baseToken: address - - period: uint32 - outputs: - - minTwap: uint256 - - quote: uint256 - -recover: - mutability: nonpayable - access: anyone - inputs: - - token: address - outputs: [] - errors: [REFUND_FAILED] - -router: - mutability: view - inputs: [] - outputs: - - addr: address - -celo: - mutability: pure - inputs: [] - outputs: - - token: address - -CUSD: - mutability: pure - inputs: [] - outputs: - - token: address - -USDC: - mutability: pure - inputs: [] - outputs: - - token: address - -GLOUSD: - mutability: pure - inputs: [] - outputs: - - token: address - -GD_FEE_TIER: - mutability: pure - inputs: [] - outputs: - - tier: uint24 - -CUSD_STABLE_FEE_TIER: - mutability: pure - inputs: [] - outputs: - - tier: uint24 - -CELO_STABLE_FEE_TIER: - mutability: pure - inputs: [] - outputs: - - tier: uint24 - -twapPeriod: - mutability: view - inputs: [] - outputs: - - period: uint32 - -stable: - mutability: view - inputs: [] - outputs: - - token: address - -gd: - mutability: view - inputs: [] - outputs: - - token: address - -oracle: - mutability: view - inputs: [] - outputs: - - addr: address - -quoter: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoBroker: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoExchangeProvider: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoExchangeId: - mutability: view - inputs: [] - outputs: - - id: bytes32 - -owner: - mutability: view - inputs: [] - outputs: - - addr: address - -CUSD_GAS_COSTS: - mutability: pure - inputs: [] - outputs: - - amount: uint256 - -events: - Bought: - indexed: [] - data: - - inToken: address - - inAmount: uint256 - - outAmount: uint256 - BoughtFromMento: - indexed: [] - data: - - inToken: address - - inAmount: uint256 - - outAmount: uint256 - BoughtFromUniswap: - indexed: [] - data: - - inToken: address - - inAmount: uint256 - - outAmount: uint256 - -errors: - REFUND_FAILED: "Refund call failed." - NO_BALANCE: "No CELO or cUSD available on clone." - MENTO_NOT_CONFIGURED: "Mento broker path not configured." diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml deleted file mode 100644 index 1476ae3..0000000 --- a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address): 0xc4d66de8 - getSwapPath(address[],uint24[]): 0xf0036d5d - swap(uint256,address): 0xd3986f08 - swapCelo(uint256,address): 0xaa6bfa9d - swapCeloWithPath(uint256,address,(address[],uint24[])): 0x81b7d2e0 - swapCusd(uint256,address): 0xb1a5fa9f - swapCusdWithPath(uint256,address,(address[],uint24[])): 0x170ba915 - getExpectedReturnFromUniswapPath(uint256,(address[],uint24[])): 0x12194320 - getExpectedReturnFromMento(uint256): 0xdbb15eb2 - minAmountByTWAP(uint256,address,uint32): 0x821dc910 - recover(address): 0x0cd865ec - router(): 0xf887ea40 - celo(): 0x051ed8ef - CUSD(): 0x758316c9 - USDC(): 0x89a30271 - GLOUSD(): 0x4b5b02d6 - GD_FEE_TIER(): 0xe00e8fdd - CUSD_STABLE_FEE_TIER(): 0xba428926 - CELO_STABLE_FEE_TIER(): 0x60e4bf4b - twapPeriod(): 0xf6207326 - stable(): 0x22be3de1 - gd(): 0xa5e598fc - oracle(): 0x7dc0d1d0 - quoter(): 0xc6bbd5a7 - mentoBroker(): 0x7b89f117 - mentoExchangeProvider(): 0x4f62feec - mentoExchangeId(): 0xd373b333 - owner(): 0x8da5cb5b - CUSD_GAS_COSTS(): 0x32f90ac3 -events: - Bought(address,uint256,uint256): 0xa9a40dec7a304e5915d11358b968c1e8d365992abf20f82285d1df1b30c8e24c - BoughtFromMento(address,uint256,uint256): 0x8e2ac24d7ef5662ee242823a19dbd1c952b3e96ae127228f4bbce83e2816e3fb - BoughtFromUniswap(address,uint256,uint256): 0xdb1f2a6cbbfd964f19c648b140a35992aa80f482df8f519d3211d0bd86c9f335 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml deleted file mode 100644 index 32ba0de..0000000 --- a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml +++ /dev/null @@ -1,237 +0,0 @@ -# CFAv1Forwarder — convenience wrapper for ConstantFlowAgreementV1 -# Allows direct interaction with CFA functions without manually routing through -# the Host's batchCall / forwardBatchCall. Operates as a trusted forwarder, -# preserving msg.sender via EIP-2771. -# -# Each forwarder call is a standalone transaction — forwarder calls cannot be -# combined in a Host.batchCall. To batch multiple operations atomically (e.g. -# wrap tokens + create stream), use Host.batchCall with the raw CFA agreement -# (operationType 201). See Superfluid.abi.yaml for batch operation details. -# Tradeoff: forwarder calls produce human-readable descriptions in wallets -# (e.g. "setFlowrate(token, receiver, flowrate)"), while Host.batchCall -# shows encoded tuples that are difficult for users to verify. -# -# This contract has no events — all events are emitted by the underlying CFA. -# Errors from the CFA (and Host) propagate through to the caller. - -meta: - name: CFAv1Forwarder - version: v1 - source: - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/utils/CFAv1Forwarder.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/utils/ForwarderBase.sol - inherits: [ForwarderBase] - deployments: - # Same address on all networks except avalanche-fuji - mainnet: - eth-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - polygon-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - xdai-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - base-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - optimism-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - arbitrum-one: "0xcfA132E353cB4E398080B9700609bb008eceB125" - bsc-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - avalanche-c: "0xcfA132E353cB4E398080B9700609bb008eceB125" - celo-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - scroll-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - degenchain: "0xcfA132E353cB4E398080B9700609bb008eceB125" - testnet: - avalanche-fuji: "0x2CDd45c5182602a36d391F7F16DD9f8386C3bD8D" - base-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" - eth-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" - optimism-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" - scroll-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" - deploymentCreationBlocks: - mainnet: - celo-mainnet: 17404156 - -# == Glossary == -# flowrate — token transfer rate in wad/second (int96) -# flowOperator — account authorized to create/update/delete flows on behalf of another -# buffer — deposit locked as solvency collateral while a flow is active -# permissions — bitmask of create | update | delete rights for an operator -# flowrateAllowance — max flowrate an operator may set per individual flow - -# == Flow Management == -# High-level functions that automatically create, update, or delete flows as needed. - -setFlowrate: - # Smart setter: creates a new flow if none exists, updates if flowrate changed, - # deletes if flowrate is zero. No-ops if the current rate already matches. - notes: - - "Gotcha: Negative flowrate values revert with CFA_FWD_INVALID_FLOW_RATE." - mutability: nonpayable - access: anyone # flow from msg.sender - inputs: - - token: address - - receiver: address - - flowrate: int96 # must be >= 0 - outputs: - - bool - errors: [CFA_FWD_INVALID_FLOW_RATE] - -setFlowrateFrom: - # Same as setFlowrate but can be called by a flow operator on behalf of `sender`. - # msg.sender must have sufficient operator permissions and flowrateAllowance. - mutability: nonpayable - access: sender | operator - inputs: - - token: address - - sender: address - - receiver: address - - flowrate: int96 - outputs: - - bool - errors: [CFA_FWD_INVALID_FLOW_RATE] - -# == Low-Level Flow Operations == -# Direct wrappers around CFA functions. These give full control (including userData) -# but require the caller to know whether a flow already exists. -# If sender != msg.sender, the *ByOperator variant is used internally. - -createFlow: - # Create a new flow. Reverts if a flow already exists between sender and receiver. - mutability: nonpayable - access: sender | operator - inputs: - - token: address - - sender: address - - receiver: address - - flowrate: int96 - - userData: bytes - outputs: - - bool - -updateFlow: - # Update an existing flow's rate. Reverts if no flow exists. - mutability: nonpayable - access: sender | operator - inputs: - - token: address - - sender: address - - receiver: address - - flowrate: int96 - - userData: bytes - outputs: - - bool - -deleteFlow: - # Delete an existing flow. Can be called by sender, receiver, or an operator. - # If msg.sender is neither sender nor receiver, deleteFlowByOperator is used. - mutability: nonpayable - access: sender | receiver | operator - inputs: - - token: address - - sender: address - - receiver: address - - userData: bytes - outputs: - - bool - -# == ACL (Operator Permissions) == - -grantPermissions: - # Grant full create/update/delete permissions with max flowrateAllowance to an operator. - # Convenience wrapper — equivalent to updateFlowOperatorPermissions with full control. - mutability: nonpayable - access: anyone # grants on msg.sender's flows - inputs: - - token: address - - flowOperator: address - outputs: - - bool - -revokePermissions: - # Revoke all permissions from an operator. Does not affect existing flows. - mutability: nonpayable - access: anyone # revokes on msg.sender's flows - inputs: - - token: address - - flowOperator: address - outputs: - - bool - -updateFlowOperatorPermissions: - # Set granular operator permissions and flowrate allowance. - notes: - - "Gotcha: flowrateAllowance limits per-flow rate, NOT aggregate net flow." - mutability: nonpayable - access: anyone # grants on msg.sender's flows - inputs: - - token: address - - flowOperator: address - - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete - - flowrateAllowance: int96 - outputs: - - bool - -# == Flow Queries == - -getFlowrate: - mutability: view - inputs: - - token: address - - sender: address - - receiver: address - outputs: - - flowrate: int96 - -getFlowInfo: - mutability: view - inputs: - - token: address - - sender: address - - receiver: address - outputs: - - lastUpdated: uint256 - - flowrate: int96 - - deposit: uint256 - - owedDeposit: uint256 - -getBufferAmountByFlowrate: - # Returns the deposit/buffer required for a given flowrate. - notes: - - "Gotcha: This value is governance-configurable and may change over time. Changes only affect newly created/updated flows." - mutability: view - inputs: - - token: address - - flowrate: int96 - outputs: - - bufferAmount: uint256 - -getAccountFlowrate: - # Net aggregate flowrate for an account (incoming minus outgoing). - mutability: view - inputs: - - token: address - - account: address - outputs: - - flowrate: int96 - -getAccountFlowInfo: - mutability: view - inputs: - - token: address - - account: address - outputs: - - lastUpdated: uint256 - - flowrate: int96 - - deposit: uint256 - - owedDeposit: uint256 - -# == ACL Queries == - -getFlowOperatorPermissions: - mutability: view - inputs: - - token: address - - sender: address - - flowOperator: address - outputs: - - permissions: uint8 - - flowrateAllowance: int96 - -# == Errors == - -errors: - - CFA_FWD_INVALID_FLOW_RATE # flowrate argument was negative diff --git a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml deleted file mode 100644 index f98e21e..0000000 --- a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - setFlowrate(address,address): 0x2f57fb6f - setFlowrateFrom(address,address,address,int96): 0xc5ad5c1a - createFlow(address,address,address,int96,bytes): 0xe15536b6 - updateFlow(address,address,address,int96,bytes): 0x0c033991 - deleteFlow(address,address,address,bytes): 0xb4b333c6 - grantPermissions(address,address): 0x7243fb93 - revokePermissions(address,address): 0x0bd0728d - updateFlowOperatorPermissions(address,address): 0x42294caf - getFlowrate(address,address,address): 0x1d8b6526 - getFlowInfo(address,address,address): 0x2860fd93 - getBufferAmountByFlowrate(address,int96): 0x09f0b495 - getAccountFlowrate(address,address): 0x22c904d9 - getAccountFlowInfo(address,address): 0x0f1ac495 - getFlowOperatorPermissions(address,address,address): 0x4d3f60f9 -events: -{} -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml deleted file mode 100644 index afaec5d..0000000 --- a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml +++ /dev/null @@ -1,560 +0,0 @@ -# Superfluid Constant Flow Agreement (CFA) v1 -# Manages continuous per-second token streams between accounts. -# -# NOTE: emits/errors mappings are traced from source code — verify against implementation. -# Proxy/upgradability functions (castrate, updateCode, getCodeAddress, proxiableUUID) -# are omitted — they belong to the UUPSProxiable / AgreementBase layer. -# Pure helpers addPermissions/removePermissions are omitted — they were made public -# for testability only and are not part of the protocol interface. - -meta: - name: ConstantFlowAgreementV1 - version: v1 - source: - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/agreements/IConstantFlowAgreementV1.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/agreements/AgreementBase.sol - implements: [IConstantFlowAgreementV1, ISuperAgreement] - inherits: [AgreementBase] - deployments: - mainnet: - eth-mainnet: "0x2844c1BBdA121E9E43105630b9C8310e5c72744b" - polygon-mainnet: "0x6EeE6060f715257b970700bc2656De21dEdF074C" - xdai-mainnet: "0xEbdA4ceF883A7B12c4E669Ebc58927FBa8447C7D" - base-mainnet: "0x19ba78B9cDB05A877718841c574325fdB53601bb" - optimism-mainnet: "0x204C6f131bb7F258b2Ea1593f5309911d8E458eD" - arbitrum-one: "0x731FdBB12944973B500518aea61942381d7e240D" - bsc-mainnet: "0x49c38108870e74Cb9420C0991a85D3edd6363F75" - avalanche-c: "0x6946c5B38Ffea373b0a2340b4AEf0De8F6782e58" - celo-mainnet: "0x9d369e78e1a682cE0F8d9aD849BeA4FE1c3bD3Ad" - scroll-mainnet: "0xB3bcD6da1eeB6c97258B3806A853A6dcD3B6C00c" - degenchain: "0x82cc052d1b17aC554a22A88D5876B56c6b51e95c" - testnet: - avalanche-fuji: "0x16843ac25Ccc58Aa7960ba05f61cBB17b36b130A" - base-sepolia: "0x6836F23d6171D74Ef62FcF776655aBcD2bcd62Ef" - eth-sepolia: "0x6836F23d6171D74Ef62FcF776655aBcD2bcd62Ef" - optimism-sepolia: "0x8a3170AdbC67233196371226141736E4151e7C26" - scroll-sepolia: "0xbc46B4Aa41c055578306820013d4B65fff42711E" - deploymentCreationBlocks: - mainnet: - celo-mainnet: 16393492 - -# == Abbreviations == -# 3Ps — Three Periods: liquidation, patrician, and pleb (solvency model) -# ACL — Access Control List (flow operator permissions) -# CFA — Constant Flow Agreement -# ctx — context (Superfluid call context bytes, carries msg.sender and userData) -# GDA — General Distribution Agreement (the other Superfluid agreement) -# PPP — Patrician-Pleb-Pirate (the 3Ps solvency periods) - -# == Glossary == -# flow / stream — continuous per-second token transfer, used interchangeably -# flowRate — tokens per second (int96, wei-denominated); positive = outgoing -# deposit — buffer locked when opening a flow; protects against insolvency -# owed deposit — portion of deposit owed back to the sender by a Super App receiver -# flow operator — an address authorized to create/update/delete flows on behalf of a sender -# flow rate allowance — maximum net flow rate an operator may allocate (int96); decrements on use -# permissions bitmask — uint8: bit 0 = create (1), bit 1 = update (2), bit 2 = delete (4) -# liquidation — closing an insolvent sender's flow; rewards go to the liquidator or bond -# patrician period — grace window after insolvency where reward goes to the bond account -# pleb period — window after patrician where reward goes to the liquidator -# pirate / bailout — state where total deposit cannot cover the deficit; protocol bails out -# Super App — a contract registered with the Host that receives agreement callbacks -# app credit — deposit credit enabling "zero-balance Super Apps" to relay flows -# without pre-funded tokens. 1:1 relay (one in, one out at same rate) -# always works at zero balance. Fan-out (1:N) needs the app to hold -# tokens. Credit is settled as "owed deposit" on the original sender. -# critical — an account whose available balance is negative (eligible for liquidation) -# jailed — a Super App penalized for violating protocol rules; loses callbacks -# -# == Time Conventions == -# Conventional seconds-per-period used in Superfluid apps for flow rate conversion: -# month: 2628000 (365.25 / 12 * 86400) -# year: 31536000 (365 * 86400) -# Flow rate from a monthly amount: flowRate = monthlyAmount / 2628000 -# Flow rate from a yearly amount: flowRate = yearlyAmount / 31536000 - -# == Flow Management == -# Core operations for creating, updating, and deleting token streams. -# Functions with ctx are called through the Host — either via -# Host.callAgreement (single op) or Host.batchCall (operationType 201). -# For batchCall, the data field is abi.encode(callData, userData) where -# callData is the full ABI-encoded function call with an empty ctx ("0x") -# as placeholder. The Host replaces the placeholder with the real context. -# The access field reflects who can initiate the call, not the direct caller. - -createFlow: - # Start a new stream from ctx.msgSender to receiver. - # The deposit amount depends on the governance-configured liquidation period - # and minimum deposit. The lower 32 bits of the deposit are clipped (rounded up). - notes: - - "Gotcha: A deposit is taken as a safety margin for solvency agents." - mutability: nonpayable - access: anyone # flow from ctx.msgSender - inputs: - - token: address - - receiver: address - - flowRate: int96 # must be > 0 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_ALREADY_EXISTS, CFA_INSUFFICIENT_BALANCE] - -updateFlow: - # Change the flow rate of an existing stream from ctx.msgSender to receiver. - # Note: owedDeposit accumulates when a Super App receiver uses app credit to - # open outgoing streams. The sender bears the deposit cost for those streams. - notes: - - "Gotcha: Deposit is adjusted to match the new flow rate but never refunds owed deposit — that adjustment happens separately via the app credit system." - mutability: nonpayable - access: anyone # flow from ctx.msgSender - inputs: - - token: address - - receiver: address - - flowRate: int96 # must be > 0 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_DOES_NOT_EXIST, CFA_INSUFFICIENT_BALANCE] - -deleteFlow: - # Stop a stream between sender and receiver. - # During liquidation the reward distribution depends on the solvency period: - # patrician → bond account gets reward; pleb → liquidator gets reward; - # pirate → liquidator gets full single deposit, protocol covers the bailout. - notes: - - "Gotcha: Third-party callers (not sender, receiver, or operator) only succeed if the sender is critical (negative available balance) or either party is jailed." - mutability: nonpayable - access: sender | receiver | operator | anyone(if-critical-or-jailed) - inputs: - - token: address - - sender: address - - receiver: address - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ZERO_ADDRESS_SENDER, CFA_ZERO_ADDRESS_RECEIVER, CFA_FLOW_DOES_NOT_EXIST, CFA_NON_CRITICAL_SENDER] - -# == Operator Flow Management == -# Act on behalf of another account. Requires ACL permissions granted by the sender. -# The operator's flow rate allowance is decremented on create/update. -# Functions with ctx are called through the Host. - -createFlowByOperator: - # Create a flow on behalf of sender. Consumes flow rate allowance. - notes: - - "Gotcha: Reverts if ctx.msgSender IS the sender — use createFlow instead." - mutability: nonpayable - access: operator - inputs: - - token: address - - sender: address - - receiver: address - - flowRate: int96 # must be > 0 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ACL_NO_SENDER_CREATE, CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS, CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED, CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_ALREADY_EXISTS, CFA_INSUFFICIENT_BALANCE] - -updateFlowByOperator: - # Update a flow on behalf of sender. Only consumes allowance if flow rate increases. - # If flowRateAllowance is type(int96).max, it is treated as unlimited. - notes: - - "Gotcha: If the new rate is lower than the old rate, no allowance is consumed." - mutability: nonpayable - access: operator - inputs: - - token: address - - sender: address - - receiver: address - - flowRate: int96 # must be > 0 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ACL_NO_SENDER_UPDATE, CFA_ACL_OPERATOR_NO_UPDATE_PERMISSIONS, CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED, CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_DOES_NOT_EXIST, CFA_INSUFFICIENT_BALANCE] - -deleteFlowByOperator: - # Delete a flow on behalf of sender. Does not consume flow rate allowance. - mutability: nonpayable - access: operator - inputs: - - token: address - - sender: address - - receiver: address - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ACL_OPERATOR_NO_DELETE_PERMISSIONS, CFA_ZERO_ADDRESS_SENDER, CFA_ZERO_ADDRESS_RECEIVER, CFA_FLOW_DOES_NOT_EXIST, CFA_NON_CRITICAL_SENDER] - -# == Flow Queries == - -realtimeBalanceOf: - # Compute the real-time CFA balance contribution for an account. - # Returns the dynamic balance delta (flowRate * elapsed), total deposit held, - # and total owed deposit. This is called by the token's realtimeBalanceOfNow - # to assemble the full balance across all agreements. - # To get an account's actual token balance, use SuperToken.realtimeBalanceOfNow - # (or SuperToken.balanceOf for ERC-20 compatible, clamped to zero). - # It can be deeply negative for net-outflow accounts as time progresses. - notes: - - "Gotcha: Returns only the CFA portion of the balance, not the total." - - "Gotcha: The returned dynamicBalance is a delta, not an absolute balance." - mutability: view - inputs: - - token: address - - account: address - - time: uint256 - outputs: - - dynamicBalance: int256 # flowRate * (time - lastUpdate); can be negative - - deposit: uint256 # total deposit locked across all outflows - - owedDeposit: uint256 # total owed deposit from Super App receivers - -getFlow: - # Get flow data between a specific sender-receiver pair. - mutability: view - inputs: - - token: address - - sender: address - - receiver: address - outputs: - - timestamp: uint256 # last update time - - flowRate: int96 - - deposit: uint256 - - owedDeposit: uint256 - -getFlowByID: - # Get flow data using the agreement ID (keccak256 of sender and receiver). - mutability: view - inputs: - - token: address - - flowId: bytes32 # keccak256(abi.encode(sender, receiver)) - outputs: - - timestamp: uint256 - - flowRate: int96 - - deposit: uint256 - - owedDeposit: uint256 - -getAccountFlowInfo: - # Aggregated flow state for an account across all its CFA flows. - mutability: view - inputs: - - token: address - - account: address - outputs: - - timestamp: uint256 # last time any flow was updated for this account - - flowRate: int96 # net flow rate (inflows - outflows) - - deposit: uint256 # sum of deposits across all outflows - - owedDeposit: uint256 # sum of owed deposits across all outflows - -getNetFlow: - # Net flow rate for an account (sum of inflows minus outflows). - mutability: view - inputs: - - token: address - - account: address - outputs: - - flowRate: int96 - -# == ACL Management == -# Manage flow operator permissions and flow rate allowances. -# The caller (ctx.msgSender) is always the permission granter — they control -# who can operate on their own flows. -# Functions with ctx are called through the Host. - -updateFlowOperatorPermissions: - # Set exact permissions and flow rate allowance for a flow operator. - mutability: nonpayable - access: anyone # grants on ctx.msgSender's flows - inputs: - - token: address - - flowOperator: address - - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete - - flowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -authorizeFlowOperatorWithFullControl: - # Grant all permissions (create+update+delete) with unlimited allowance. - # Shorthand for updateFlowOperatorPermissions(token, op, 7, type(int96).max, ctx). - mutability: nonpayable - access: anyone # grants on ctx.msgSender's flows - inputs: - - token: address - - flowOperator: address - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -revokeFlowOperatorWithFullControl: - # Revoke all permissions and set allowance to zero. - # Shorthand for updateFlowOperatorPermissions(token, op, 0, 0, ctx). - mutability: nonpayable - access: anyone # revokes on ctx.msgSender's flows - inputs: - - token: address - - flowOperator: address - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -increaseFlowRateAllowance: - # Increase flow rate allowance for an operator by a delta. - # Delegates to increaseFlowRateAllowanceWithPermissions with permissionsToAdd=0. - mutability: nonpayable - access: anyone # ctx.msgSender's allowance - inputs: - - token: address - - flowOperator: address - - addedFlowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -decreaseFlowRateAllowance: - # Decrease flow rate allowance for an operator by a delta. - # Delegates to decreaseFlowRateAllowanceWithPermissions with permissionsToRemove=0. - mutability: nonpayable - access: anyone # ctx.msgSender's allowance - inputs: - - token: address - - flowOperator: address - - subtractedFlowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -increaseFlowRateAllowanceWithPermissions: - # Increase flow rate allowance and add permission bits in one call. - # permissionsToAdd is OR'd with existing permissions. - mutability: nonpayable - access: anyone # ctx.msgSender's allowance - inputs: - - token: address - - flowOperator: address - - permissionsToAdd: uint8 # bitmask OR'd onto existing permissions - - addedFlowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -decreaseFlowRateAllowanceWithPermissions: - # Decrease flow rate allowance and remove permission bits in one call. - # permissionsToRemove is AND-NOT'd from existing permissions. - notes: - - "Gotcha: Reverts if the resulting allowance would go negative." - mutability: nonpayable - access: anyone # ctx.msgSender's allowance - inputs: - - token: address - - flowOperator: address - - permissionsToRemove: uint8 # bitmask AND-NOT'd from existing permissions - - subtractedFlowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -# == ACL Queries == - -getFlowOperatorData: - # Get permissions and allowance for a flow operator. - mutability: view - inputs: - - token: address - - sender: address # the permission granter - - flowOperator: address # the permission grantee - outputs: - - flowOperatorId: bytes32 # keccak256(abi.encode("flowOperator", sender, flowOperator)) - - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete - - flowRateAllowance: int96 - -getFlowOperatorDataByID: - # Get permissions and allowance using the pre-computed operator ID. - mutability: view - inputs: - - token: address - - flowOperatorId: bytes32 - outputs: - - permissions: uint8 - - flowRateAllowance: int96 - -# == Solvency Queries == -# Check whether an account is in the patrician period (grace window for liquidation). - -isPatricianPeriodNow: - # Check patrician period status using the Host's current timestamp. - mutability: view - inputs: - - token: address - - account: address - outputs: - - isCurrentlyPatricianPeriod: bool - - timestamp: uint256 # the Host's block.timestamp used for the check - -isPatricianPeriod: - # Check patrician period status at a specific timestamp. - mutability: view - inputs: - - token: address - - account: address - - timestamp: uint256 - outputs: - - bool - -# == Deposit Helpers == - -getMaximumFlowRateFromDeposit: - # Calculate the maximum flow rate achievable with a given deposit. - notes: - - "Gotcha: Deposit is clipped (lower 32 bits zeroed) and rounded down." - mutability: view - inputs: - - token: address # needed to look up liquidation period from governance - - deposit: uint256 - outputs: - - flowRate: int96 - errors: [CFA_DEPOSIT_TOO_BIG] - -getDepositRequiredForFlowRate: - # Calculate the deposit required for a given flow rate. - # Returns max(minimumDeposit, flowRate * liquidationPeriod) with rounding. - mutability: view - inputs: - - token: address # needed to look up liquidation period and minimum deposit - - flowRate: int96 - outputs: - - deposit: uint256 - errors: [CFA_INVALID_FLOW_RATE, CFA_FLOW_RATE_TOO_BIG] - -# == Protocol Constants == - -agreementType: - # Returns keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1") - mutability: pure - outputs: - - bytes32 - -DEFAULT_MINIMUM_DEPOSIT: - # Minimum deposit floor: uint96(1 << 32) ≈ 4.29 * 10^9 wei. - # Governance may set a higher per-token minimum; this is the absolute floor. - mutability: view - outputs: - - uint256 - -MAXIMUM_DEPOSIT: - # Maximum deposit cap: type(int96).max - mutability: view - outputs: - - uint256 - -MAXIMUM_FLOW_RATE: - # Maximum flow rate: type(int96).max - mutability: view - outputs: - - uint256 - -CFA_HOOK_GAS_LIMIT: - # Gas limit for external hook calls (Super App callbacks): 250,000 - mutability: view - outputs: - - uint64 - -# == Events == - -events: - FlowUpdated: - # Emitted on every create, update, and delete flow operation. - # totalSenderFlowRate and totalReceiverFlowRate are the NET flow rates - # after the operation, not the individual flow rate. - notes: - - "Gotcha: Always emitted together with FlowUpdatedExtension." - indexed: - - token: address - - sender: address - - receiver: address - data: - - flowRate: int96 # new rate for this specific flow (0 on delete) - - totalSenderFlowRate: int256 # sender's net flow rate after this operation - - totalReceiverFlowRate: int256 - - userData: bytes - - FlowUpdatedExtension: - # Companion event to FlowUpdated. Carries the operator and new deposit. - # Emitted immediately after FlowUpdated so indexers can correlate them. - indexed: - - flowOperator: address # ctx.msgSender who initiated the operation - data: - - deposit: uint256 # new deposit for this specific flow - - FlowOperatorUpdated: - # Emitted when operator permissions or flow rate allowance change. - indexed: - - token: address - - sender: address # the permission granter - - flowOperator: address # the permission grantee - data: - - permissions: uint8 # updated bitmask - - flowRateAllowance: int96 # updated allowance - - # Inherited events (from AgreementBase / UUPSProxiable): - # CodeUpdated — emitted on proxy upgrade (uuid, codeAddress) - # Initialized — emitted on proxy initialization (version) - -# == Errors == - -errors: - # Flow validation - - CFA_FLOW_ALREADY_EXISTS # createFlow when flow exists - - CFA_FLOW_DOES_NOT_EXIST # update/delete when no flow - - CFA_INVALID_FLOW_RATE # flowRate <= 0 - - CFA_NO_SELF_FLOW # sender == receiver - - CFA_ZERO_ADDRESS_SENDER # sender is address(0) - - CFA_ZERO_ADDRESS_RECEIVER # receiver is address(0) - # Solvency - - CFA_INSUFFICIENT_BALANCE # sender cannot cover deposit - - CFA_NON_CRITICAL_SENDER # third-party delete but sender is solvent - - CFA_DEPOSIT_TOO_BIG # deposit > MAXIMUM_DEPOSIT - - CFA_FLOW_RATE_TOO_BIG # flowRate * liquidationPeriod overflows - # ACL - - CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED # operator exceeds granted allowance - - CFA_ACL_NO_NEGATIVE_ALLOWANCE # resulting allowance would be < 0 - - CFA_ACL_NO_SENDER_CREATE # operator cannot be the sender (use createFlow) - - CFA_ACL_NO_SENDER_FLOW_OPERATOR # cannot set yourself as your own operator - - CFA_ACL_NO_SENDER_UPDATE # operator cannot be the sender (use updateFlow) - - CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS - - CFA_ACL_OPERATOR_NO_DELETE_PERMISSIONS - - CFA_ACL_OPERATOR_NO_UPDATE_PERMISSIONS - - CFA_ACL_UNCLEAN_PERMISSIONS # permission bits outside valid range - # Super App / Host - - CFA_HOOK_OUT_OF_GAS # Super App callback exceeded gas limit - - APP_RULE: # Super App rule violation - inputs: - - _code: uint256 - - AGREEMENT_BASE_ONLY_HOST # call not routed through the Host contract - # SafeCast (inherited from OpenZeppelin) - - SafeCastOverflowedIntToUint: # int256 value overflows on cast to uint256 - inputs: - - value: int256 - - SafeCastOverflowedUintToInt: # uint256 value overflows on cast to int256 - inputs: - - value: uint256 diff --git a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml deleted file mode 100644 index a929e06..0000000 --- a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - createFlow(address,address): 0x7deac86a - updateFlow(address,address): 0xb6214437 - deleteFlow(address,address,address,bytes): 0xb4b333c6 - createFlowByOperator(address,address,address): 0x97b0b745 - updateFlowByOperator(address,address,address): 0xaea4dc29 - deleteFlowByOperator(address,address,address,bytes): 0x4c8b181f - realtimeBalanceOf(address,address,uint256): 0x9b2e48bc - getFlow(address,address,address): 0xe6a1e888 - getFlowByID(address): 0x4eccd8ac - getAccountFlowInfo(address,address): 0x0f1ac495 - getNetFlow(address,address): 0xe8e7e2d1 - updateFlowOperatorPermissions(address,address): 0x42294caf - authorizeFlowOperatorWithFullControl(address,address,bytes): 0x54b770e3 - revokeFlowOperatorWithFullControl(address,address,bytes): 0x062e56ec - increaseFlowRateAllowance(address,address,int96,bytes): 0xac5f5d00 - decreaseFlowRateAllowance(address,address,int96,bytes): 0x5f51fb23 - increaseFlowRateAllowanceWithPermissions(address,address): 0x5e6a4dc3 - decreaseFlowRateAllowanceWithPermissions(address,address): 0x8de39d9b - getFlowOperatorData(address): 0x5f13dbbc - getFlowOperatorDataByID(address,bytes32): 0x09d256ef - isPatricianPeriodNow(address,address): 0x4fe9c291 - isPatricianPeriod(address,address,uint256): 0x4b839e0b - getMaximumFlowRateFromDeposit(): 0x4ba52c34 - getDepositRequiredForFlowRate(): 0xcc111b9e - CFA_HOOK_GAS_LIMIT(): 0xbf3fbc28 -events: - FlowUpdated(address,address,address,int256,bytes): 0x8e3b8f31fe09d2ca20fa7f76ec574bc9fea49f16a91f1ca828154ea76e76a20a - FlowUpdatedExtension(): 0x9bf0b3e0199b411ca09607d040b7672430af5dae9d159b715c9f8004c876065f - FlowOperatorUpdated(address): 0x401d2cb05b3db80617d70aabf976f52d2a5dac64298eadeed7a81ad4bb5d4fdd - # Inherited events (from AgreementBase / UUPSProxiable)(): 0xcabfd4ad95e3bb803739ac513717fccee724fc0c5741e01cf84319a96120eefa -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml deleted file mode 100644 index c60d498..0000000 --- a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml +++ /dev/null @@ -1,100 +0,0 @@ -# GoodDollarOFTAdapter — LayerZero V2 OFT adapter pattern bridging underlying G$ -# Exact bytecode lives in the GoodDollar OFT deployment; this documents the surface agents call. - -meta: - name: GoodDollarOFTAdapter - version: LayerZero-OFT-adapter (generic) - source: - - https://docs.layerzero.network/ - - https://github.com/LayerZero-Labs/devtools - note: > - Flow: optional peers(dstEid) check -> approve underlying to minterBurner if - MessagingFee, refundAddress) paying nativeFee in msg.value when applicable. - SendParam mirrors LayerZero OFT: dstEid, to (bytes32 padded recipient), - amountLD, minAmountLD, extraOptions, composeMsg, oftCmd. - Verify tuple field order and OFT revision against your deployed artifact before mainnet use. - Event names and topic0 hashes vary by OFT package version — pull from the deployment ABI. - related: - - references/guides/bridge.md - -token: - mutability: view - inputs: [] - outputs: - - underlyingToken: address - -minterBurner: - notes: - - "Underlying G$ must approve this spender when burning for cross-chain send." - mutability: view - inputs: [] - outputs: - - minterBurner: address - -oftVersion: - mutability: view - inputs: [] - outputs: - - interfaceId: bytes4 - - version: uint64 - -peers: - notes: - - "Returns bytes32 peer address configured for destination endpoint id." - mutability: view - inputs: - - dstEid: uint32 - outputs: - - peer: bytes32 - -endpoint: - mutability: view - inputs: [] - outputs: - - lzEndpoint: address - -owner: - mutability: view - inputs: [] - outputs: - - account: address - -quoteSend: - notes: - - "Simulates messaging + bridge fee; use nativeFee as msg.value on send when paying in native gas token." - mutability: view - inputs: - - sendParam: SendParam - - payInLzToken: bool - outputs: - - nativeFee: uint256 - - lzTokenFee: uint256 - errors: - - NO_PEER - - LZ_INVALID_OPTIONS - -send: - notes: - - "Payable: include MessagingFee.nativeFee in msg.value when fee is native." - - "Consumes sendParam.amountLD from sender on source chain and emits cross-chain message to dstEid peer." - - "sendParam.to must be destination receiver encoded as bytes32." - mutability: payable - access: sender - inputs: - - sendParam: SendParam - - fee: MessagingFee - - refundAddress: address - outputs: - - msgReceipt: MessageReceipt - - oftReceipt: OFTReceipt - errors: - - NO_PEER - - SLIPPAGE_OR_AMOUNT - - LZ_INVALID_OPTIONS - -events: {} - -errors: - NO_PEER: "Destination peer not configured for dstEid." - LZ_INVALID_OPTIONS: "Composer/options payload rejected by LayerZero." - SLIPPAGE_OR_AMOUNT: "Bridged amount violates minAmount or available balance." diff --git a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml deleted file mode 100644 index e5615a1..0000000 --- a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - token(): 0xfc0c546a - minterBurner(): 0x2ef8c5a4 - approvalRequired(): 0x9f68b964 - oftVersion(): 0x156a0d0f - peers(uint32): 0xbb0b6a53 - endpoint(): 0x5e280f11 - owner(): 0x8da5cb5b -events: -{} -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml deleted file mode 100644 index d736bc1..0000000 --- a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml +++ /dev/null @@ -1,210 +0,0 @@ -# GooddollarSavingsStream (Ubeswap) — G$ native Super Token stake with Superfluid GDA reward stream - -meta: - name: GooddollarSavingsStream - version: "1" - source: - - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/GooddollarSavingsStream.sol - - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/interfaces/IGooddollarSavingsStream.sol - - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/StakingVault.sol - inherits: - - IGooddollarSavingsStream - - Ownable - - ERC2771Context - - ReentrancyGuard - note: > - Celo production savings. Staked principal sits in StakingVault; this contract holds reward - balance and streams via a Superfluid GDA distribution pool (`distributeFlow`). Effective rate is - min(rewardRate, maxRewardRatePerToken * totalSupply / 1e18), throttled when reward balance is low. - Staking token is the G$ native Super Token (`superToken`). Fuse->CELO migration uses `stakeFor` - after bridge. Ubeswap lists this as Celo mainnet savings (streaming). - deployments: - mainnet: - production-celo: - GooddollarSavingsStream: - networkId: 42220 - address: "0x059ee811414230d1Fb157878D2b491240F4D8d3B" - creationBlock: 66685884 - related: - - https://github.com/Ubeswap/gooddollar-contracts/blob/main/README.md - - https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B - - references/contracts/SuperToken.abi.yaml - - references/guides/migrate-fuse-staking-to-celo-savings.md - - references/deep-researches/fuse-to-celo-staking-migration.md - -totalSupply: - mutability: view - inputs: [] - outputs: - - supply: uint256 - -balanceOf: - mutability: view - inputs: - - account: address - outputs: - - balance: uint256 - -getDailyRewards: - mutability: view - inputs: [] - outputs: - - daily: uint256 - -getEffectiveFlowRate: - notes: - - "Returns 0 when totalSupply is 0, rewardRate is 0, or reward balance cannot sustain MIN_STREAM_BUFFER_SECONDS." - - "Otherwise min(global rewardRate, APR cap) as int96 for Superfluid distributeFlow." - mutability: view - inputs: [] - outputs: - - flowRate: int96 - -periodFinish: - notes: - - "Estimated timestamp when current reward balance is drained at getEffectiveFlowRate; 0 if not streaming." - mutability: view - inputs: [] - outputs: - - finish: uint256 - -getUnits: - mutability: view - inputs: - - account: address - outputs: - - units: uint128 - -getTotalUnits: - mutability: view - inputs: [] - outputs: - - units: uint128 - -stake: - notes: - - "Pulls G$ Super Token from msg.sender, deposits principal to StakingVault, updates GDA pool units, syncFlowRate." - - "Requires prior superToken.approve(this, amount)." - mutability: nonpayable - access: anyone - inputs: - - amount: uint256 - outputs: [] - emits: [Staked] - -stakeFor: - notes: - - "Migrator path: caller funds transfer; stake credits `_balances[recipient]` and pool units for recipient." - - "recipient must not be zero, this, superToken, or vault." - - "Used after Fuse->CELO bridge when backend wallet holds bridged G$." - mutability: nonpayable - access: anyone - inputs: - - amount: uint256 - - recipient: address - outputs: [] - emits: [Staked] - -withdraw: - notes: - - "Reduces stake and pool units, syncFlowRate, then vault.withdraw to msg.sender." - mutability: nonpayable - access: anyone - inputs: - - amount: uint256 - outputs: [] - emits: [Withdrawn] - -exit: - notes: - - "Full withdraw of caller principal via withdraw(balance)." - mutability: nonpayable - access: anyone - inputs: [] - outputs: [] - emits: [Withdrawn] - -addToReward: - notes: - - "Pulls reward Super Token from caller; re-syncs flow rate when balance was throttling the stream." - mutability: nonpayable - access: anyone - inputs: - - reward: uint256 - outputs: [] - emits: [RewardAdded] - -syncFlowRate: - notes: - - "Permissionless; call when tokens were sent directly to the contract so distributeFlow picks up balance." - mutability: nonpayable - access: anyone - inputs: [] - outputs: [] - emits: [FlowRateUpdated] - -setDailyRewards: - mutability: nonpayable - access: owner - inputs: - - _dailyRewards: uint256 - outputs: [] - emits: [DailyRewardsUpdated] - -setMaxRewardRatePerToken: - mutability: nonpayable - access: owner - inputs: - - _value: uint256 - outputs: [] - emits: [MaxRewardRateUpdated] - -recoverERC20: - notes: - - "Cannot recover the G$ Super Token (staking/reward asset)." - mutability: nonpayable - access: owner - inputs: - - tokenAddress: address - - tokenAmount: uint256 - outputs: [] - emits: [Recovered] - -events: - Staked: - indexed: - - user: address - data: - - amount: uint256 - Withdrawn: - indexed: - - user: address - data: - - amount: uint256 - RewardAdded: - data: - - reward: uint256 - DailyRewardsUpdated: - data: - - rewardRate: uint256 - - givenDailyRewards: uint256 - MaxRewardRateUpdated: - data: - - newMaxRate: uint256 - FlowRateUpdated: - data: - - newFlowRate: int96 - Recovered: - indexed: - - token: address - data: - - amount: uint256 - - receiver: address - -errors: - - CannotStakeZero - - CannotWithdrawZero - - InsufficientStake - - InvalidAddress - - NoRewardToAdd - - CannotRecoverStakingToken diff --git a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml deleted file mode 100644 index f04d75a..0000000 --- a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - totalSupply(): 0x18160ddd - balanceOf(address): 0x70a08231 - getDailyRewards(): 0x68527008 - getEffectiveFlowRate(): 0xa2efe857 - periodFinish(): 0xebe2b12b - getUnits(address): 0x0fefbc09 - getTotalUnits(): 0xa754a702 - stake(uint256): 0xa694fc3a - stakeFor(uint256,address): 0x51746bb2 - withdraw(uint256): 0x2e1a7d4d - exit(): 0xe9fad8ee - addToReward(uint256): 0x2ce618fa - syncFlowRate(): 0x0e83284a - setDailyRewards(uint256): 0x4adef718 - setMaxRewardRatePerToken(uint256): 0x6a03a9e2 - recoverERC20(address,uint256): 0x8980f11f -events: - Staked(address,uint256): 0x9e71bc8eea02a63969f509818f2dafb9254532904319f9dbda79b67bd34a5f3d - Withdrawn(address,uint256): 0x7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d5 - RewardAdded(uint256): 0xde88a922e0d3b88b24e9623efeb464919c6bf9f66857a65e2bfcf2ce87a9433d - DailyRewardsUpdated(uint256,uint256): 0x54f5b7ef058007cebca11af0127cd80c0bac4e968788eabb3c1e70d1bfb78edd - MaxRewardRateUpdated(uint256): 0x9041b23d05af9bceefc73becffefd1887907ad55c3d6c6655230b37ef87dbb07 - FlowRateUpdated(int96): 0xd02fcf6b96acb60ac68942c7d5075e11a8a0b38cc865c1916934f4e1f129ded0 - Recovered(address,uint256,address): 0xb197f0a554c4d7840105e6ae65f0e275e9e8605a969dffa8caa7f1f118a2e1f5 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml deleted file mode 100644 index 5adccb3..0000000 --- a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml +++ /dev/null @@ -1,202 +0,0 @@ -# GovernanceStakingV2 (Fuse) — G$ staking share token with GDAO reward accrual -# Deployment key in GoodProtocol release metadata is GovernanceStakingV2. - -meta: - name: GovernanceStakingV2 - version: "2" - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/governance/GovernanceStaking.sol - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/releases/deployment.json - inherits: - - ERC20Upgradeable - - MultiBaseGovernanceShareField - - DAOUpgradeableContract - - ReentrancyGuardUpgradeable - note: > - Fuse governance staking contract where users stake G$ and receive staking shares (`sG$`) with - GDAO-style rewards minted on reward withdrawal paths. `withdrawStake(0)` means full unstake. - In GoodProtocol `releases/deployment.json` this address is listed as GovernanceStakingV2 on Fuse `production`. - This is the old staking contract in Fuse->CELO migration flows. - deployments: - mainnet: - production: - GovernanceStakingV2: - networkId: 122 - address: "0xB7C3e738224625289C573c54d402E9Be46205546" - creationBlock: 15956809 - related: - - references/guides/migrate-fuse-staking-to-celo-savings.md - - references/deep-researches/fuse-to-celo-staking-migration.md - -getChainBlocksPerMonth: - mutability: pure - inputs: [] - outputs: - - blocks: uint256 - -stake: - notes: - - "Requires prior G$ ERC20 approval to staking contract." - - "Mints staking share token and updates reward productivity before event emission." - mutability: nonpayable - access: anyone - inputs: - - _amount: uint256 - outputs: [] - emits: [Staked] - -withdrawStake: - notes: - - "If _amount is 0, contract interprets it as full user unstake." - - "Burns staking shares, updates productivity, mints pending rewards, then transfers G$ out." - mutability: nonpayable - access: anyone - inputs: - - _amount: uint256 - outputs: [] - emits: [StakeWithdraw] - -withdrawRewards: - notes: - - "Claims rewards without changing staked principal." - mutability: nonpayable - access: anyone - inputs: [] - outputs: - - minted: uint256 - emits: [ReputationEarned] - -setMonthlyRewards: - notes: - - "DAO-controlled update of rewardsPerBlock schedule via avatar authorization." - mutability: nonpayable - access: avatar - inputs: - - _monthlyAmount: uint256 - outputs: [] - -getRewardsPerBlock: - mutability: view - inputs: [] - outputs: - - amount: uint256 - -getProductivity: - mutability: view - inputs: - - _user: address - outputs: - - productivity: uint256 - - rewardDebt: uint256 - -getUserPendingReward: - mutability: view - inputs: - - _user: address - outputs: - - pending: uint256 - -users: - notes: - - "UserInfo getter for internal staking productivity record." - mutability: view - inputs: - - _user: address - outputs: - - amount: uint256 - - rewardDebt: uint256 - -totalRewardsPerShare: - mutability: view - inputs: [] - outputs: - - value: uint256 - -decimals: - mutability: view - inputs: [] - outputs: - - d: uint8 - -totalSupply: - mutability: view - inputs: [] - outputs: - - supply: uint256 - -balanceOf: - mutability: view - inputs: - - account: address - outputs: - - balance: uint256 - -allowance: - mutability: view - inputs: - - owner: address - - spender: address - outputs: - - amount: uint256 - -approve: - mutability: nonpayable - access: anyone - inputs: - - spender: address - - amount: uint256 - outputs: - - ok: bool - emits: [Approval] - -transfer: - mutability: nonpayable - access: anyone - inputs: - - to: address - - amount: uint256 - outputs: - - ok: bool - emits: [Transfer] - -transferFrom: - mutability: nonpayable - access: anyone - inputs: - - from: address - - to: address - - amount: uint256 - outputs: - - ok: bool - emits: [Transfer, Approval] - -events: - ReputationEarned: - indexed: - - staker: address - data: - - amount: uint256 - Staked: - indexed: - - staker: address - data: - - amount: uint256 - StakeWithdraw: - indexed: - - staker: address - data: - - amount: uint256 - Transfer: - indexed: - - from: address - - to: address - data: - - value: uint256 - Approval: - indexed: - - owner: address - - spender: address - data: - - value: uint256 - -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml deleted file mode 100644 index 37388ba..0000000 --- a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - getChainBlocksPerMonth(): 0x213b329e - stake(uint256): 0xa694fc3a - withdrawStake(uint256): 0x25d5971f - withdrawRewards(): 0xc7b8981c - setMonthlyRewards(uint256): 0xc76279a2 - getRewardsPerBlock(): 0x0c1cd7f3 - getProductivity(address): 0x28e964e9 - getUserPendingReward(address): 0xc6710629 - users(address): 0xa87430ba - totalRewardsPerShare(): 0xbf8e9b6e - decimals(): 0x313ce567 - totalSupply(): 0x18160ddd - balanceOf(address): 0x70a08231 - allowance(address,address): 0xdd62ed3e - approve(address,uint256): 0x095ea7b3 - transfer(address,uint256): 0xa9059cbb - transferFrom(address,address,uint256): 0x23b872dd -events: - ReputationEarned(address,uint256): 0x43848d0574703c28d68ae8958e0571521618f60c4bcacfb094cff2156eaae0f1 - Staked(address,uint256): 0x9e71bc8eea02a63969f509818f2dafb9254532904319f9dbda79b67bd34a5f3d - StakeWithdraw(address,uint256): 0x1248d48e2de900a1010c7fce73506969ecec243600bfc08b641b158f26d857cd - Transfer(address,address,uint256): 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef - Approval(address,address,uint256): 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml deleted file mode 100644 index aea2efe..0000000 --- a/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml +++ /dev/null @@ -1,390 +0,0 @@ -# IdentityV3 — whitelist, blacklist, DID, and connected-account graph for GoodDollar -# UBI eligibility uses getWhitelistedRoot; connectAccount links extra wallets to a root. - -meta: - name: IdentityV3 - version: v3 - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/identity/IdentityV3.sol - inherits: - - DAOUpgradeableContract - - AccessControlUpgradeable - - PausableUpgradeable - - EIP712Upgradeable - note: > - isWhitelisted enforces authenticationPeriod against dateAuthenticated and status==1, - with fallback to oldIdentity. getWhitelistedRoot returns the root address for a - connected wallet or the wallet itself when directly whitelisted. - deployments: - mainnet: - production: - Identity: - networkId: 122 - address: "0x2F9C28de9e6d44b71B91b8BA337A5D82e308E7BE" - creationBlock: 22022901 - IdentityOld: - networkId: 122 - address: "0xFa8d865A962ca8456dF331D78806152d3aC5B84F" - creationBlock: 6246324 - production-celo: - Identity: - networkId: 42220 - address: "0xC361A6E67822a0EDc17D899227dd9FC50BD62F42" - creationBlock: 17237952 - production-xdc: - Identity: - networkId: 50 - address: "0x27a4a02C9ed591E1a86e2e5D05870292c34622C9" - creationBlock: 95143058 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/identity - - https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _owner: address - - _oldIdentity: address - outputs: [] - -initDAO: - notes: - - "Wires NameService via setDAO and transfers DEFAULT_ADMIN_ROLE, PAUSER_ROLE, IDENTITY_ADMIN_ROLE to avatar." - mutability: nonpayable - access: DEFAULT_ADMIN_ROLE - inputs: - - _ns: address - outputs: [] - errors: [ALREADY_INITIALIZED] - -setAuthenticationPeriod: - mutability: nonpayable - access: avatar - inputs: - - period: uint256 - outputs: [] - errors: [WHEN_NOT_PAUSED_REVERT] - -authenticate: - notes: - - "Public wrapper; forwards to authenticateWithTimestamp(account, block.timestamp)." - mutability: nonpayable - inputs: - - account: address - outputs: [] - -authenticateWithTimestamp: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - timestamp: uint256 - outputs: [] - emits: [WhitelistedAuthenticated] - errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [WhitelistedAdded] - errors: [ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelistedWithDIDAndChain: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - did: string - - orgChain: uint256 - - dateAuthenticated: uint256 - outputs: [] - emits: [WhitelistedAdded] - errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelistedWithDID: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - did: string - outputs: [] - emits: [WhitelistedAdded] - errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -removeWhitelisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [WhitelistedRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -renounceWhitelisted: - mutability: nonpayable - access: whitelisted sender - inputs: [] - outputs: [] - emits: [WhitelistedRemoved] - errors: [NOT_WHITELISTED_SENDER, WHEN_NOT_PAUSED_REVERT] - -isWhitelisted: - notes: - - "Combines local Identity record with optional oldIdentity.isWhitelisted try/catch." - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -lastAuthenticated: - mutability: view - inputs: - - account: address - outputs: - - ts: uint256 - -addBlacklisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [BlacklistAdded, WhitelistedRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -removeBlacklisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [BlacklistRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -addContract: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [ContractAdded] - errors: [NOT_CONTRACT, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -removeContract: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [ContractRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -isDAOContract: - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -isBlacklisted: - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -connectAccount: - notes: - - "Caller must be whitelisted; target must not already be whitelisted or blacklisted; stores connectedAccounts[account]=msg.sender." - mutability: nonpayable - access: whitelisted sender - inputs: - - account: address - outputs: [] - emits: [AccountConnected] - errors: [INVALID_ACCOUNT_FOR_CONNECT, ALREADY_CONNECTED, WHEN_NOT_PAUSED_REVERT] - -disconnectAccount: - notes: - - "Caller must be either the linked root or the connected wallet." - mutability: nonpayable - inputs: - - connected: address - outputs: [] - emits: [AccountDisconnected] - errors: [UNAUTHORIZED_DISCONNECT] - -getWhitelistedRoot: - notes: - - "Returns account if directly whitelisted; returns connected root if connected account is whitelisted; else zero." - mutability: view - inputs: - - account: address - outputs: - - whitelisted: address - -pause: - mutability: nonpayable - access: PAUSER_ROLE - inputs: - - toPause: bool - outputs: [] - -setDID: - mutability: nonpayable - inputs: - - account: address - - did: string - outputs: [] - errors: [NOT_AUTHORIZED_SET_DID, NOT_WHITELISTED_FOR_DID, DID_EMPTY, DID_ALREADY_REGISTERED, DID_ALREADY_REGISTERED_OLD, WHEN_NOT_PAUSED_REVERT] - -addrToDID: - mutability: view - inputs: - - account: address - outputs: - - did: string - -getWhitelistedOnChainId: - mutability: view - inputs: - - account: address - outputs: - - chainId: uint256 - -isRegistered: - notes: - - "Compatibility shim; always returns true." - mutability: pure - inputs: [] - outputs: - - ok: bool - -IDENTITY_ADMIN_ROLE: - mutability: view - inputs: [] - outputs: - - role: bytes32 - -PAUSER_ROLE: - mutability: view - inputs: [] - outputs: - - role: bytes32 - -TYPED_STRUCTURE: - mutability: view - inputs: [] - outputs: - - schema: string - -whitelistedCount: - mutability: view - inputs: [] - outputs: - - n: uint256 - -whitelistedContracts: - mutability: view - inputs: [] - outputs: - - n: uint256 - -authenticationPeriod: - mutability: view - inputs: [] - outputs: - - seconds: uint256 - -identities: - mutability: view - inputs: - - account: address - outputs: - - dateAuthenticated: uint256 - - dateAdded: uint256 - - did: string - - whitelistedOnChainId: uint256 - - status: uint8 - -didHashToAddress: - mutability: view - inputs: - - hash: bytes32 - outputs: - - account: address - -connectedAccounts: - mutability: view - inputs: - - account: address - outputs: - - root: address - -oldIdentity: - mutability: view - inputs: [] - outputs: - - addr: address - -events: - BlacklistAdded: - indexed: - - account: address - data: [] - BlacklistRemoved: - indexed: - - account: address - data: [] - WhitelistedAdded: - indexed: - - account: address - data: [] - WhitelistedRemoved: - indexed: - - account: address - data: [] - WhitelistedAuthenticated: - indexed: - - account: address - data: - - timestamp: uint256 - ContractAdded: - indexed: - - account: address - data: [] - ContractRemoved: - indexed: - - account: address - data: [] - AccountConnected: - indexed: - - connected: address - - to: address - data: [] - AccountDisconnected: - indexed: - - disconnected: address - - from: address - data: [] - -errors: - ALREADY_INITIALIZED: "already initialized — initDAO twice." - NOT_WHITELISTED_STATUS: "not whitelisted — authenticateWithTimestamp." - NOT_WHITELISTED_SENDER: "not whitelisted — renounceWhitelisted modifier." - NOT_WHITELISTED_FOR_DID: "not whitelisted — setDID internal." - ALREADY_HAS_STATUS: "already has status — _addWhitelisted." - DID_ALREADY_REGISTERED: "DID already registered — _addWhitelistedWithDID or _setDID." - DID_ALREADY_REGISTERED_OLD: "DID already registered oldIdentity — _setDID conflict path." - NOT_CONTRACT: "Given address is not a contract — addContract." - INVALID_ACCOUNT_FOR_CONNECT: "invalid account — connectAccount target checks." - ALREADY_CONNECTED: "already connected — connectAccount." - UNAUTHORIZED_DISCONNECT: "unauthorized — disconnectAccount." - NOT_AUTHORIZED_SET_DID: "not authorized — setDID caller." - DID_EMPTY: "did empty — _setDID." - WHEN_NOT_PAUSED_REVERT: "OpenZeppelin Pausable whenNotPaused on gated calls." diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml deleted file mode 100644 index 9b53f00..0000000 --- a/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address,address): 0x485cc955 - initDAO(address): 0x2b14dda8 - setAuthenticationPeriod(uint256): 0xfff930d4 - authenticate(address): 0x08e0d29d - authenticateWithTimestamp(address,uint256): 0x96a1ef79 - addWhitelisted(address): 0x10154bad - addWhitelistedWithDIDAndChain(address,string,uint256,uint256): 0xe737031a - addWhitelistedWithDID(address,string): 0x1b027099 - removeWhitelisted(address): 0x291d9549 - renounceWhitelisted(): 0xd6cd9473 - isWhitelisted(address): 0x3af32abf - lastAuthenticated(address): 0xe1e360ba - addBlacklisted(address): 0x188efc16 - removeBlacklisted(address): 0xc6a276c2 - addContract(address): 0x5f539d69 - removeContract(address): 0xc375c2ef - isDAOContract(address): 0xc73cc4ae - isBlacklisted(address): 0xfe575a87 - connectAccount(address): 0xd21685f4 - disconnectAccount(address): 0x37a1a987 - getWhitelistedRoot(address): 0x2d0e9b46 - pause(bool): 0x02329a29 - setDID(address,string): 0xd3262816 - addrToDID(address): 0x54f9f7a3 - getWhitelistedOnChainId(address): 0xa061922d - isRegistered(): 0x22366844 - IDENTITY_ADMIN_ROLE(): 0x1aaff63c - PAUSER_ROLE(): 0xe63ab1e9 - TYPED_STRUCTURE(): 0x2cec5330 - whitelistedCount(): 0xb2a1de22 - whitelistedContracts(): 0xb30f7e7f - authenticationPeriod(): 0x31b376e2 - identities(address): 0xf653b81e - didHashToAddress(bytes32): 0x67c75937 - connectedAccounts(address): 0x61320040 - oldIdentity(): 0x4125f0f2 -events: - BlacklistAdded(address): 0x44d5fe68b00f68950fb9c1ff0a61ef7f747b1a36359a7e3a7f3324db4b878967 - BlacklistRemoved(address): 0x1747ca720b1a174a464b6513ace29b1d3190b5f632b9f34147017c81425bfde8 - WhitelistedAdded(address): 0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f - WhitelistedRemoved(address): 0x270d9b30cf5b0793bbfd54c9d5b94aeb49462b8148399000265144a8722da6b6 - WhitelistedAuthenticated(address,uint256): 0xb2a82fce6d8c7a633efe9579f77b4edb96bfdf171a49bfc2ce666dc543a1f500 - ContractAdded(address): 0x89c66952b48f3e96bf1d8ba1b63189520fd988a6979b8b740bd5c5d8dc53e205 - ContractRemoved(address): 0x8d30d41865a0b811b9545d879520d2dde9f4cc49e4241f486ad9752bc904b565 - AccountConnected(address,address): 0x18f7736ef54539debd9afd3c9500b106e12ae7c70e685f5a5efd727b1ce1d54c - AccountDisconnected(address,address): 0x7cdef5f9c5cb8ce728661ede956fef26cb91eb4d7d2180cc041b73f9fef568d2 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml deleted file mode 100644 index 485254d..0000000 --- a/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml +++ /dev/null @@ -1,424 +0,0 @@ -# IdentityV4 — whitelist, blacklist, DID, connected accounts, and reverification schedule -# isWhitelisted uses shouldReverify against reverifyDaysOptions and authCount; oldIdentity fallback preserved. - -meta: - name: IdentityV4 - version: v4 - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/identity/IdentityV4.sol - inherits: - - DAOUpgradeableContract - - AccessControlUpgradeable - - PausableUpgradeable - - EIP712Upgradeable - note: > - authenticationPeriod() returns the largest reverify interval in days (last entry of reverifyDaysOptions), - not seconds. isWhitelisted is false when shouldReverify is true for the current elapsed days since - dateAuthenticated. Post-upgrade, accounts with dateAuthenticated before 1772697574 are treated as - having authCount at the last schedule step for reverify logic. - deployments: - mainnet: - production: - Identity: - networkId: 122 - address: "0x2F9C28de9e6d44b71B91b8BA337A5D82e308E7BE" - creationBlock: 22022901 - production-celo: - Identity: - networkId: 42220 - address: "0xC361A6E67822a0EDc17D899227dd9FC50BD62F42" - creationBlock: 17237952 - production-xdc: - Identity: - networkId: 50 - address: "0x27a4a02C9ed591E1a86e2e5D05870292c34622C9" - creationBlock: 95143058 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/identity - - https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _owner: address - - _oldIdentity: address - outputs: [] - -initDAO: - notes: - - "Wires NameService via setDAO and transfers DEFAULT_ADMIN_ROLE, PAUSER_ROLE, IDENTITY_ADMIN_ROLE to avatar." - mutability: nonpayable - access: DEFAULT_ADMIN_ROLE - inputs: - - _ns: address - outputs: [] - errors: [ALREADY_INITIALIZED] - -setReverifyDaysOptions: - notes: - - "Replaces the full schedule; values must be strictly ascending uint8 days; non-empty array." - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - options: uint8[] - outputs: [] - errors: [EMPTY_REVERIFY_OPTIONS, OPTIONS_NOT_ASCENDING, WHEN_NOT_PAUSED_REVERT] - -authenticate: - notes: - - "Forwards to authenticateWithTimestamp(account, block.timestamp); requires IDENTITY_ADMIN_ROLE on the inner call." - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] - -authenticateWithTimestamp: - notes: - - "Refreshes authentication timestamp for status-1 account and advances authCount when reverification threshold is reached." - - "Legacy accounts before cutoff are evaluated at last schedule step for reverify math." - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - timestamp: uint256 - outputs: [] - emits: [WhitelistedAuthenticated] - errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [WhitelistedAdded] - errors: [ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelistedWithDIDAndChain: - notes: - - "Adds whitelisted account and stores DID plus originating chain id and authentication timestamp." - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - did: string - - orgChain: uint256 - - dateAuthenticated: uint256 - outputs: [] - emits: [WhitelistedAdded] - errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelistedWithDID: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - did: string - outputs: [] - emits: [WhitelistedAdded] - errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -removeWhitelisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [WhitelistedRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -renounceWhitelisted: - mutability: nonpayable - access: whitelisted sender - inputs: [] - outputs: [] - emits: [WhitelistedRemoved] - errors: [NOT_WHITELISTED_SENDER, WHEN_NOT_PAUSED_REVERT] - -shouldReverify: - notes: - - "Compares daysSinceAuth against current reverifyDaysOptions step selected by authCount." - mutability: view - inputs: - - account: address - - daysSinceAuth: uint256 - outputs: - - needed: bool - -isWhitelisted: - notes: - - "Local status 1 with shouldReverify false; else oldIdentity.isWhitelisted try/catch." - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -lastAuthenticated: - notes: - - "Returns local dateAuthenticated or falls back to oldIdentity when local record has no timestamp." - mutability: view - inputs: - - account: address - outputs: - - ts: uint256 - -addBlacklisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [BlacklistAdded, WhitelistedRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -removeBlacklisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [BlacklistRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -addContract: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [ContractAdded] - errors: [NOT_CONTRACT, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -removeContract: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [ContractRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -isDAOContract: - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -isBlacklisted: - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -connectAccount: - notes: - - "Caller must be whitelisted; target must not already be whitelisted or blacklisted; stores connectedAccounts[account]=msg.sender." - mutability: nonpayable - access: whitelisted sender - inputs: - - account: address - outputs: [] - emits: [AccountConnected] - errors: [INVALID_ACCOUNT_FOR_CONNECT, ALREADY_CONNECTED, WHEN_NOT_PAUSED_REVERT] - -disconnectAccount: - notes: - - "Caller must be either the linked root or the connected wallet." - mutability: nonpayable - inputs: - - connected: address - outputs: [] - emits: [AccountDisconnected] - errors: [UNAUTHORIZED_DISCONNECT] - -getWhitelistedRoot: - notes: - - "Returns account if directly whitelisted; returns connected root if connected account is whitelisted; else zero." - mutability: view - inputs: - - account: address - outputs: - - whitelisted: address - -pause: - mutability: nonpayable - access: PAUSER_ROLE - inputs: - - toPause: bool - outputs: [] - -setDID: - notes: - - "Sets DID for account by admin or account itself when whitelisted; enforces uniqueness across local and old identity." - mutability: nonpayable - inputs: - - account: address - - did: string - outputs: [] - errors: [NOT_AUTHORIZED_SET_DID, NOT_WHITELISTED_FOR_DID, DID_EMPTY, DID_ALREADY_REGISTERED, DID_ALREADY_REGISTERED_OLD, WHEN_NOT_PAUSED_REVERT] - -addrToDID: - mutability: view - inputs: - - account: address - outputs: - - did: string - -getWhitelistedOnChainId: - notes: - - "Returns chain id recorded during whitelisting for cross-chain eligibility checks." - mutability: view - inputs: - - account: address - outputs: - - chainId: uint256 - -isRegistered: - notes: - - "Compatibility shim; always returns true." - mutability: pure - inputs: [] - outputs: - - ok: bool - -IDENTITY_ADMIN_ROLE: - mutability: view - inputs: [] - outputs: - - role: bytes32 - -PAUSER_ROLE: - mutability: view - inputs: [] - outputs: - - role: bytes32 - -TYPED_STRUCTURE: - mutability: view - inputs: [] - outputs: - - schema: string - -whitelistedCount: - mutability: view - inputs: [] - outputs: - - n: uint256 - -whitelistedContracts: - mutability: view - inputs: [] - outputs: - - n: uint256 - -authenticationPeriod: - notes: - - "Returns reverifyDaysOptions[last] in days, not seconds." - mutability: view - inputs: [] - outputs: - - days: uint256 - -reverifyDaysOptions: - mutability: view - inputs: - - index: uint256 - outputs: - - days: uint32 - -identities: - mutability: view - inputs: - - account: address - outputs: - - dateAuthenticated: uint256 - - dateAdded: uint256 - - did: string - - whitelistedOnChainId: uint256 - - status: uint8 - - authCount: uint32 - -didHashToAddress: - mutability: view - inputs: - - hash: bytes32 - outputs: - - account: address - -connectedAccounts: - mutability: view - inputs: - - account: address - outputs: - - root: address - -oldIdentity: - mutability: view - inputs: [] - outputs: - - addr: address - -events: - BlacklistAdded: - indexed: - - account: address - data: [] - BlacklistRemoved: - indexed: - - account: address - data: [] - WhitelistedAdded: - indexed: - - account: address - data: [] - WhitelistedRemoved: - indexed: - - account: address - data: [] - WhitelistedAuthenticated: - indexed: - - account: address - data: - - timestamp: uint256 - ContractAdded: - indexed: - - account: address - data: [] - ContractRemoved: - indexed: - - account: address - data: [] - AccountConnected: - indexed: - - connected: address - - to: address - data: [] - AccountDisconnected: - indexed: - - disconnected: address - - from: address - data: [] - -errors: - ALREADY_INITIALIZED: "already initialized — initDAO twice." - EMPTY_REVERIFY_OPTIONS: "empty options — setReverifyDaysOptions." - OPTIONS_NOT_ASCENDING: "options not in ascending order — setReverifyDaysOptions." - NOT_WHITELISTED_STATUS: "not whitelisted — authenticateWithTimestamp." - NOT_WHITELISTED_SENDER: "not whitelisted — renounceWhitelisted modifier." - NOT_WHITELISTED_FOR_DID: "not whitelisted — _setDID." - ALREADY_HAS_STATUS: "already has status — _addWhitelisted." - DID_ALREADY_REGISTERED: "DID already registered — _addWhitelistedWithDID or _setDID." - DID_ALREADY_REGISTERED_OLD: "DID already registered oldIdentity — _setDID conflict path." - NOT_CONTRACT: "Given address is not a contract — addContract." - INVALID_ACCOUNT_FOR_CONNECT: "invalid account — connectAccount target checks." - ALREADY_CONNECTED: "already connected — connectAccount." - UNAUTHORIZED_DISCONNECT: "unauthorized — disconnectAccount." - NOT_AUTHORIZED_SET_DID: "not authorized — setDID caller." - DID_EMPTY: "did empty — _setDID." - WHEN_NOT_PAUSED_REVERT: "OpenZeppelin Pausable whenNotPaused on gated calls." diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml deleted file mode 100644 index 3440a5e..0000000 --- a/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address,address): 0x485cc955 - initDAO(address): 0x2b14dda8 - setReverifyDaysOptions(uint8[]): 0x925e8c1a - authenticate(address): 0x08e0d29d - authenticateWithTimestamp(address,uint256): 0x96a1ef79 - addWhitelisted(address): 0x10154bad - addWhitelistedWithDIDAndChain(address,string,uint256,uint256): 0xe737031a - addWhitelistedWithDID(address,string): 0x1b027099 - removeWhitelisted(address): 0x291d9549 - renounceWhitelisted(): 0xd6cd9473 - shouldReverify(address,uint256): 0x4a03813f - isWhitelisted(address): 0x3af32abf - lastAuthenticated(address): 0xe1e360ba - addBlacklisted(address): 0x188efc16 - removeBlacklisted(address): 0xc6a276c2 - addContract(address): 0x5f539d69 - removeContract(address): 0xc375c2ef - isDAOContract(address): 0xc73cc4ae - isBlacklisted(address): 0xfe575a87 - connectAccount(address): 0xd21685f4 - disconnectAccount(address): 0x37a1a987 - getWhitelistedRoot(address): 0x2d0e9b46 - pause(bool): 0x02329a29 - setDID(address,string): 0xd3262816 - addrToDID(address): 0x54f9f7a3 - getWhitelistedOnChainId(address): 0xa061922d - isRegistered(): 0x22366844 - IDENTITY_ADMIN_ROLE(): 0x1aaff63c - PAUSER_ROLE(): 0xe63ab1e9 - TYPED_STRUCTURE(): 0x2cec5330 - whitelistedCount(): 0xb2a1de22 - whitelistedContracts(): 0xb30f7e7f - authenticationPeriod(): 0x31b376e2 - reverifyDaysOptions(uint256): 0xcadef5ac - identities(address): 0xf653b81e - didHashToAddress(bytes32): 0x67c75937 - connectedAccounts(address): 0x61320040 - oldIdentity(): 0x4125f0f2 -events: - BlacklistAdded(address): 0x44d5fe68b00f68950fb9c1ff0a61ef7f747b1a36359a7e3a7f3324db4b878967 - BlacklistRemoved(address): 0x1747ca720b1a174a464b6513ace29b1d3190b5f632b9f34147017c81425bfde8 - WhitelistedAdded(address): 0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f - WhitelistedRemoved(address): 0x270d9b30cf5b0793bbfd54c9d5b94aeb49462b8148399000265144a8722da6b6 - WhitelistedAuthenticated(address,uint256): 0xb2a82fce6d8c7a633efe9579f77b4edb96bfdf171a49bfc2ce666dc543a1f500 - ContractAdded(address): 0x89c66952b48f3e96bf1d8ba1b63189520fd988a6979b8b740bd5c5d8dc53e205 - ContractRemoved(address): 0x8d30d41865a0b811b9545d879520d2dde9f4cc49e4241f486ad9752bc904b565 - AccountConnected(address,address): 0x18f7736ef54539debd9afd3c9500b106e12ae7c70e685f5a5efd727b1ce1d54c - AccountDisconnected(address,address): 0x7cdef5f9c5cb8ce728661ede956fef26cb91eb4d7d2180cc041b73f9fef568d2 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml deleted file mode 100644 index d69e3d6..0000000 --- a/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml +++ /dev/null @@ -1,333 +0,0 @@ -# InvitesV2 — invite codes, inviter levels, and G$ bounties (UUPS upgradeable) -# Bounty amounts on Level use G$ cents (2 decimals). Invitee receives half the inviter bounty on payout. - -meta: - name: InvitesV2 - version: "2.4" - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/invite/InvitesV2.sol - inherits: - - DAOUpgradeableContract - note: > - join binds invite code and optional inviter; bounty eligibility uses minimumDays, minimumClaims, - whitelist on invitee (and inviter when present), and same-chain whitelisting via Identity - getWhitelistedOnChainId. collectBounties and bountyFor pay invitee bountyToPay/2 and inviter - bountyToPay when applicable. Campaign inviter is address(this) when inviter code maps to contract. - deployments: - mainnet: - production: - Invites: - networkId: 122 - address: "0xCa2F09c3ccFD7aD5cB9276918Bd1868f2b922ea0" - creationBlock: 8853311 - production-celo: - Invites: - networkId: 42220 - address: "0x36829D1Cda92FFF5782d5d48991620664FC857d3" - creationBlock: 18003063 - production-xdc: - Invites: - networkId: 50 - address: "0x6bd698566632bf2e81e2278f1656CB24aAF06D2e" - creationBlock: 95144756 - related: - - references/deep-researches/inviter-invitee-reward-model.md - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _ns: address - - _level0Bounty: uint256 - - _owner: address - outputs: [] - -updateAvatar: - mutability: nonpayable - access: anyone - inputs: [] - outputs: [] - -nativeToken: - mutability: view - inputs: [] - outputs: - - token: address - -dao: - mutability: view - inputs: [] - outputs: - - controller: address - -avatar: - mutability: view - inputs: [] - outputs: - - addr: address - -nameService: - mutability: view - inputs: [] - outputs: - - ns: address - -upgradeTo: - mutability: nonpayable - access: ownerOrAvatar - inputs: - - newImplementation: address - outputs: [] - -upgradeToAndCall: - mutability: payable - access: ownerOrAvatar - inputs: - - newImplementation: address - - data: bytes - outputs: [] - -proxiableUUID: - mutability: view - inputs: [] - outputs: - - slot: bytes32 - -setLevelExpirationEnabled: - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _isEnabled: bool - outputs: [] - -getIdentity: - mutability: view - inputs: [] - outputs: - - identity: address - -join: - notes: - - "Registers invite code; may set inviter; may trigger _bountyFor when canCollectBountyFor is already true." - mutability: nonpayable - access: anyone - inputs: - - _myCode: bytes32 - - _inviterCode: bytes32 - outputs: [] - emits: [InviteeJoined] - errors: [NOT_ACTIVE, INVITE_CODE_IN_USE, SELF_INVITE, USER_ALREADY_JOINED] - -canCollectBountyFor: - notes: - - "Primary eligibility gate: checks active state, whitelist, minimumDays, minimumClaims, unpaid bounty, and chain constraints." - mutability: view - inputs: - - _invitee: address - outputs: - - ok: bool - -getInvitees: - mutability: view - inputs: - - _inviter: address - outputs: - - invitees: address[] - -getPendingInvitees: - mutability: view - inputs: - - _inviter: address - outputs: - - pending: address[] - -getPendingBounties: - mutability: view - inputs: - - _inviter: address - outputs: - - count: uint256 - -bountyFor: - notes: - - "Single-invitee payout path; revalidates eligibility and transfers bounty shares to inviter/invitee on success." - mutability: nonpayable - access: anyone - inputs: - - _invitee: address - outputs: - - bounty: uint256 - emits: [InviterBounty] - errors: [NOT_ACTIVE, NOT_ELIGIBLE_BOUNTY] - -collectBounties: - notes: - - "Batch payout path over caller pending invitees; useful for claiming multiple matured bounties in one tx." - mutability: nonpayable - access: anyone - inputs: [] - outputs: [] - emits: [InviterBounty] - errors: [NOT_ACTIVE] - -setLevel: - notes: - - "Configures inviter level progression rule and bounty amount (bounty units are G$ cents)." - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _lvl: uint256 - - _toNext: uint256 - - _bounty: uint256 - - _daysToComplete: uint256 - outputs: [] - -setActive: - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _active: bool - outputs: [] - -setCampaignCode: - notes: - - "Maps a campaign code to contract-address inviter flow (address(this)) used by join." - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _code: bytes32 - outputs: [] - -setMinimums: - notes: - - "Sets minimumClaims and minimumDays thresholds used by canCollectBountyFor." - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _minClaims: uint8 - - _minDays: uint8 - outputs: [] - -end: - mutability: nonpayable - access: ownerOrAvatar - inputs: [] - outputs: [] - errors: [NOT_ACTIVE] - -setOwner: - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _owner: address - outputs: [] - -version: - mutability: pure - inputs: [] - outputs: - - v: string - -codeToUser: - mutability: view - inputs: - - code: bytes32 - outputs: - - user: address - -users: - notes: - - "Public getter omits dynamic invitees/pending arrays; use getInvitees and getPendingInvitees." - mutability: view - inputs: - - user: address - outputs: - - invitedBy: address - - inviteCode: bytes32 - - bountyPaid: bool - - level: uint256 - - levelStarted: uint256 - - totalApprovedInvites: uint256 - - totalEarned: uint256 - - joinedAt: uint256 - - bountyAtJoin: uint256 - -levels: - notes: - - "bounty is in G$ cents (2 decimals). Reserved struct slots are not exposed on the getter." - mutability: view - inputs: - - lvl: uint256 - outputs: - - toNext: uint256 - - bounty: uint256 - - daysToComplete: uint256 - -owner: - mutability: view - inputs: [] - outputs: - - addr: address - -goodDollar: - mutability: view - inputs: [] - outputs: - - token: address - -active: - mutability: view - inputs: [] - outputs: - - ok: bool - -stats: - notes: - - "Reserved Stats struct slots are not exposed on the getter." - mutability: view - inputs: [] - outputs: - - totalApprovedInvites: uint256 - - totalBountiesPaid: uint256 - - totalInvited: uint256 - -levelExpirationEnabled: - mutability: view - inputs: [] - outputs: - - ok: bool - -minimumClaims: - mutability: view - inputs: [] - outputs: - - n: uint8 - -minimumDays: - mutability: view - inputs: [] - outputs: - - n: uint8 - -events: - InviteeJoined: - indexed: - - inviter: address - - invitee: address - data: [] - InviterBounty: - indexed: - - inviter: address - - invitee: address - data: - - bountyPaid: uint256 - - inviterLevel: uint256 - - earnedLevel: bool - -errors: - NOT_ACTIVE: "not active — isActive modifier." - ONLY_OWNER_OR_AVATAR: "Only owner or avatar can perform this action — ownerOrAvatar." - INVITE_CODE_IN_USE: "invite code already in use — join." - SELF_INVITE: "self invite — join." - USER_ALREADY_JOINED: "user already joined — join." - NOT_ELIGIBLE_BOUNTY: "user not elligble for bounty yet — bountyFor." diff --git a/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml deleted file mode 100644 index d717955..0000000 --- a/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address,uint256,address): 0xc350a1b5 - updateAvatar(): 0x1b3c90a8 - nativeToken(): 0xe1758bd8 - dao(): 0x4162169f - avatar(): 0x5aef7de6 - nameService(): 0x3e6326fc - upgradeTo(address): 0x3659cfe6 - upgradeToAndCall(address,bytes): 0x4f1ef286 - proxiableUUID(): 0x52d1902d - setLevelExpirationEnabled(bool): 0x21132aad - getIdentity(): 0x36afc6fa - join(bytes32,bytes32): 0x5b419a65 - canCollectBountyFor(address): 0x6d619ef8 - getInvitees(address): 0xe9881a5e - getPendingInvitees(address): 0xe951a3aa - getPendingBounties(address): 0x41155d5e - bountyFor(address): 0xb6567cd5 - collectBounties(): 0xaf6346b0 - setLevel(uint256,uint256,uint256,uint256): 0xb9fb2d18 - setActive(bool): 0xacec338a - setCampaignCode(bytes32): 0xf14b8649 - setMinimums(uint8,uint8): 0x47826c82 - end(): 0xefbe1c1c - setOwner(address): 0x13af4035 - version(): 0x54fd4d50 - codeToUser(bytes32): 0xba6f5680 - users(address): 0xa87430ba - levels(uint256): 0xb2596a67 - owner(): 0x8da5cb5b - goodDollar(): 0x119e5bf3 - active(): 0x02fb0c5e - stats(): 0xd80528ae - levelExpirationEnabled(): 0xa1df6fd3 - minimumClaims(): 0xc121cb92 - minimumDays(): 0xdf0dca04 -events: - InviteeJoined(address,address): 0xd8c638d8979e2ba5dba1f0d66246ee4b1c54b838f0e0a2b601365345eb23b051 - InviterBounty(address,address,uint256,uint256,bool): 0x6081787cd1bd02ab1576c52f03e8710d792d460e7881c3155d77d23893f3768b -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml b/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml deleted file mode 100644 index f6f47f4..0000000 --- a/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml +++ /dev/null @@ -1,259 +0,0 @@ -# Mento Broker (IBroker + IBrokerAdmin) — swap router and trading-limit gate for GoodDollar Mento rails -# Interface definitions are vendored in GoodProtocol; implementation lives in mento-core. - -meta: - name: Broker - version: IBroker / IBrokerAdmin - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/MentoInterfaces.sol - - https://raw.githubusercontent.com/mento-org/mento-core/main/contracts/swap/Broker.sol - - https://raw.githubusercontent.com/mento-org/mento-core/main/contracts/interfaces/IBroker.sol - implements: [IBroker, IBrokerAdmin] - note: > - swapIn is exact-input; swapOut is exact-output. Quotes route through the - configured exchangeProvider + exchangeId. Reverts use require strings in - Broker.sol plus TradingLimits library checks ("amountOutMin not met", - "amountInMax exceeded", trading-limit errors). - deployments: - mainnet: - production-celo: - MentoBroker: - networkId: 42220 - address: "0x88de45906D4F5a57315c133620cfa484cB297541" - creationBlock: 31415857 - production-xdc: - MentoBroker: - networkId: 50 - address: "0x88de45906D4F5a57315c133620cfa484cB297541" - creationBlock: 100091095 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/mentobroker - - references/guides/swap.md - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _exchangeProviders: address[] - - _reserves: address[] - outputs: [] - -setReserves: - notes: - - "Updates reserve address per already-listed provider without changing provider registration." - mutability: nonpayable - access: owner - inputs: - - _exchangeProviders: address[] - - _reserves: address[] - outputs: [] - emits: [ReserveSet] - errors: - - EXCHANGE_PROVIDER_MISSING - - RESERVE_ZERO - -addExchangeProvider: - mutability: nonpayable - access: owner - inputs: - - exchangeProvider: address - - reserve: address - outputs: - - index: uint256 - emits: [ExchangeProviderAdded, ReserveSet] - errors: - - PROVIDER_ALREADY_LISTED - - PROVIDER_ZERO - - RESERVE_ZERO - -removeExchangeProvider: - mutability: nonpayable - access: owner - inputs: - - exchangeProvider: address - - index: uint256 - outputs: [] - emits: [ExchangeProviderRemoved] - errors: - - INDEX_MISMATCH - -getAmountIn: - notes: - - "Exact-output quote path: returns required tokenIn for target amountOut." - - "Pre-checks collateral reserve balance when tokenOut is collateral." - mutability: view - inputs: - - exchangeProvider: address - - exchangeId: bytes32 - - tokenIn: address - - tokenOut: address - - amountOut: uint256 - outputs: - - amountIn: uint256 - errors: - - EXCHANGE_PROVIDER_MISSING - - INSUFFICIENT_RESERVE_BALANCE - -getAmountOut: - notes: - - "Exact-input quote path: returns expected tokenOut for fixed amountIn." - - "Pre-checks collateral reserve balance when tokenOut is collateral." - mutability: view - inputs: - - exchangeProvider: address - - exchangeId: bytes32 - - tokenIn: address - - tokenOut: address - - amountIn: uint256 - outputs: - - amountOut: uint256 - errors: - - EXCHANGE_PROVIDER_MISSING - - INSUFFICIENT_RESERVE_BALANCE - -swapIn: - notes: - - "Transfers tokenIn from msg.sender then delivers tokenOut; enforces amountOut >= amountOutMin after exchange + limits." - mutability: nonpayable - access: nonReentrant - inputs: - - exchangeProvider: address - - exchangeId: bytes32 - - tokenIn: address - - tokenOut: address - - amountIn: uint256 - - amountOutMin: uint256 - outputs: - - amountOut: uint256 - emits: [Swap] - errors: - - EXCHANGE_PROVIDER_MISSING - - INSUFFICIENT_RESERVE_BALANCE - - AMOUNT_OUT_MIN_NOT_MET - - TRADING_LIMIT_OR_SAFE_TRANSFER - -swapOut: - notes: - - "Computes required tokenIn then pulls up to amountInMax; reverts if amountIn would exceed cap." - mutability: nonpayable - access: nonReentrant - inputs: - - exchangeProvider: address - - exchangeId: bytes32 - - tokenIn: address - - tokenOut: address - - amountOut: uint256 - - amountInMax: uint256 - outputs: - - amountIn: uint256 - emits: [Swap] - errors: - - EXCHANGE_PROVIDER_MISSING - - INSUFFICIENT_RESERVE_BALANCE - - AMOUNT_IN_MAX_EXCEEDED - - TRADING_LIMIT_OR_SAFE_TRANSFER - -burnStableTokens: - notes: - - "Owner-maintained escape hatch burning stables from msg.sender via reserve plumbing." - mutability: nonpayable - access: owner - inputs: - - token: address - - amount: uint256 - outputs: - - ok: bool - errors: - - COLLATERAL_TRANSFER_FAILED - -configureTradingLimit: - notes: - - "Sets per (exchangeId, token) limit config used by swapIn/swapOut guardTradingLimits checks." - mutability: nonpayable - access: owner - inputs: - - exchangeId: bytes32 - - token: address - - config: TradingLimits.Config - outputs: [] - emits: [TradingLimitConfigured] - -getExchangeProviders: - notes: - - "Discovery helper for clients that need the active provider set before quoting/swapping." - mutability: view - inputs: [] - outputs: - - providers: address[] - -exchangeProviders: - mutability: view - inputs: - - index: uint256 - outputs: - - exchangeProvider: address - -isExchangeProvider: - mutability: view - inputs: - - exchangeProvider: address - outputs: - - ok: bool - -tradingLimitsState: - mutability: view - inputs: - - eid: bytes32 - outputs: - - state: TradingLimits.State - -tradingLimitsConfig: - mutability: view - inputs: - - eid: bytes32 - outputs: - - config: TradingLimits.Config - -events: - Swap: - indexed: - - exchangeId: bytes32 - - trader: address - - tokenIn: address - data: - - exchangeProvider: address - - tokenOut: address - - amountIn: uint256 - - amountOut: uint256 - TradingLimitConfigured: - indexed: [] - data: - - exchangeId: bytes32 - - token: address - - config: TradingLimits.Config - ExchangeProviderAdded: - indexed: - - exchangeProvider: address - data: [] - ExchangeProviderRemoved: - indexed: - - exchangeProvider: address - data: [] - ReserveSet: - indexed: - - newAddress: address - - prevAddress: address - data: [] - -errors: - EXCHANGE_PROVIDER_MISSING: "ExchangeProvider does not exist" - EXCHANGE_PROVIDER_EXISTS: "ExchangeProvider already exists in the list" - PROVIDER_ZERO: "ExchangeProvider address can't be 0" - RESERVE_ZERO: "Reserve address can't be 0" - INDEX_MISMATCH: "index doesn't match provider" - INSUFFICIENT_RESERVE_BALANCE: "Insufficient balance in reserve" - AMOUNT_OUT_MIN_NOT_MET: "amountOutMin not met" - AMOUNT_IN_MAX_EXCEEDED: "amountInMax exceeded" - COLLATERAL_TRANSFER_FAILED: "Transfer of the collateral asset failed" - AMOUNT_TOO_LARGE: "amountIn too large / amountOut too large — uint256 to int256 safety checks" - TRADING_LIMIT_OR_SAFE_TRANSFER: "TradingLimits library reverts or ERC20 safety failures during swap" diff --git a/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml b/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml deleted file mode 100644 index 249edce..0000000 --- a/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address[],address[]): 0x73cf25f8 - setReserves(address[],address[]): 0xddeb9dd2 - addExchangeProvider(address,address): 0x770d0a34 - removeExchangeProvider(address,uint256): 0x04710d53 - getAmountIn(address,bytes32,address,address,uint256): 0x04e45640 - getAmountOut(address,bytes32,address,address,uint256): 0xa20f2305 - swapIn(address,bytes32,address,address,uint256,uint256): 0xddbbe850 - swapOut(address,bytes32,address,address,uint256,uint256): 0xd163b135 - burnStableTokens(address,uint256): 0x131cab2a - configureTradingLimit(bytes32,address,(uint32,uint32,int48,int48,int48,int48,int48,uint8)): 0xa868d140 - getExchangeProviders(): 0x2cac2568 - exchangeProviders(uint256): 0xc4454fdc - isExchangeProvider(address): 0xd1d786b1 - tradingLimitsState(bytes32): 0xf01ecd17 - tradingLimitsConfig(bytes32): 0x821a816c -events: - Swap(bytes32,address,address,address,address,uint256,uint256): 0x46e6aeaebfb8f1b6a9be6403b5fc420d9827172046f365e786acb9d5b56c9409 - TradingLimitConfigured(bytes32,address,(uint32,uint32,int48,int48,int48,int48,int48,uint8)): 0x3ed50aa123e1e547aa90fcf88624ef8278d253e252bb1feac75eda93e1905bb6 - ExchangeProviderAdded(address): 0x2ee2cb0721ec60b86190cae5c48e25064b69b35abad32452a4ec99d232033de2 - ExchangeProviderRemoved(address): 0x29e92ab2e30f4f74283034c28c451b6faac986b554f1808101eb6418bdba19d4 - ReserveSet(address,address): 0xb69e1c416d8be92ac92c8e97e77c4626fba5e6ab50161099f659ea3303479e50 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml deleted file mode 100644 index d4bb1f2..0000000 --- a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml +++ /dev/null @@ -1,542 +0,0 @@ -meta: - name: MessagePassingBridge - version: Axelar + LayerZero message passing - source: - - https://raw.githubusercontent.com/GoodDollar/GoodBridge/master/packages/bridge-contracts/contracts/messagePassingBridge/MessagePassingBridge.sol - - https://raw.githubusercontent.com/GoodDollar/GoodBridge/master/packages/bridge-contracts/contracts/messagePassingBridge/IMessagePassingBridge.sol - note: > - Bridge implementation that burns source tokens and mints destination tokens, - transporting requests through Axelar or LayerZero. - Cross-chain transport uses msg.value on the source (LayerZero estimateSendFee path, Axelar gas prepay); destination mint applies bridgeFees bps to the delivered G$ amount. See references/guides/bridge.md "Bridge fee context". - deployments: - mainnet: - production: - MpbBridge: - networkId: 122 - address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" - creationBlock: 25464921 - production-celo: - MpbBridge: - networkId: 42220 - address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" - creationBlock: 21473545 - production-xdc: - MpbBridge: - networkId: 50 - address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" - creationBlock: 95254417 - related: - - references/guides/bridge.md - -initialize: - notes: - - "Initial bootstrap for DAO wiring and bridge policy state." - - "Sets guardian to msg.sender and uses NameService UBISCHEME as feeRecipient fallback to avatar when missing." - - "Preloads default LZ mappings for Ethereum, Celo, Fuse, and XDC using this contract as trusted remote." - mutability: nonpayable - access: initializer - inputs: - - nameService: address - - limits: (uint256,uint256,uint256,uint256,bool) - - fees: (uint256,uint256,uint256) - outputs: [] - -upgrade: - notes: - - "Reinitializer for migration path; re-applies default LZ mappings through internal add function." - - "Used when upgrading existing proxy deployments to mapping-aware version." - mutability: nonpayable - access: reinitializer - inputs: [] - outputs: [] - -addLzChainSupport: - notes: - - "Administrative chain wiring method for LayerZero transport." - - "Writes both forward (chainId->lzChainId) and reverse (lzChainId->chainId) mappings." - - "Also updates trustedRemoteLookup to enforce source-contract authenticity on receive." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - lzChainId: uint16 - - chainId: uint256 - - remote: address - outputs: [] - -approveRequest: - notes: - - "Manual override for stuck/exceptional inbound requests." - - "When approved, destination _bridgeFrom skips limits enforcement for that request id." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - id: uint256 - outputs: [] - -preventRequest: - notes: - - "Emergency/manual block for a specific request id." - - "Prevents execution by pre-marking executedRequests[id]=true." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - id: uint256 - outputs: [] - -setFeeRecipient: - notes: - - "Sets destination fee mint recipient." - - "If feeRecipient is zero, fee minting is skipped in _bridgeFrom." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - recipient: address - outputs: [] - -setBridgeLimits: - notes: - - "Updates operational throttles: min amount, tx cap, account/day cap, global/day cap, whitelist mode." - - "These limits are checked by canBridge and enforced in _enforceLimits." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - limits: (uint256,uint256,uint256,uint256,bool) - outputs: [] - -setBridgeFees: - notes: - - "Updates bridge fee parameters in basis points with min/max clamps." - - "Reverts when fee exceeds 10000 bps." - - "Effective fee deduction is computed in _takeFee during destination execution." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - fees: (uint256,uint256,uint256) - outputs: [] - -setDisabledBridges: - notes: - - "Toggles allowlist state for source bridge combinations." - - "Key format is keccak256(abi.encode(sourceChainId, BridgeService))." - - "Checked in _bridgeFrom before request execution." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - bridgeKeys: bytes32[] - - disabled: bool[] - outputs: [] - -setFaucet: - notes: - - "Configures optional faucet top-up callback used after successful destination processing." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - _faucet: address - outputs: [] - -setGuardian: - notes: - - "Rotates guardian authority used by owner/avatar/guardian admin gate." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - _guardian: address - outputs: [] - -withdraw: - notes: - - "Avatar-only rescue function for arbitrary token balances held by bridge." - - "Amount 0 means full balance withdrawal." - mutability: nonpayable - access: avatar - inputs: - - token: address - - amount: uint256 - outputs: [] - -pauseBridge: - notes: - - "Sets isClosed state for outbound flow." - - "When closed, _bridgeTo reverts with BRIDGE_LIMITS('closed')." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - isPaused: bool - outputs: [] - -canBridge: - notes: - - "Purely diagnostic read path for preflight checks." - - "Returns (ok, reason) using BridgeHelperLibrary policy evaluation." - - "Includes close flag, whitelist gate, and per-account/global daily limit windows." - mutability: view - inputs: - - from: address - - amount: uint256 - outputs: - - isWithinLimit: bool - - error: string - -bridgeTo: - notes: - - "Generic outbound bridge entrypoint selecting BridgeService.AXELAR or BridgeService.LZ." - - "Burns source-side token amount, normalizes to 18 decimals, and emits BridgeRequest with request id." - - "Requires msg.value for transport fee payment." - mutability: payable - access: anyone - inputs: - - target: address - - targetChainId: uint256 - - amount: uint256 - - bridge: uint8 - outputs: [] - -bridgeToWithLz: - notes: - - "Outbound convenience wrapper fixed to LayerZero transport." - - "If adapterParams is empty, contract injects default type-1 options with 400k gas." - - "Compares required native fee from estimateSendFee against msg.value and reverts on underpayment." - mutability: payable - access: anyone - inputs: - - target: address - - targetChainId: uint256 - - amount: uint256 - - adapterParams: bytes - outputs: [] - -bridgeToWithAxelar: - notes: - - "Outbound convenience wrapper fixed to Axelar transport." - - "Uses gasRefundAddress when provided, otherwise msg.sender as refund recipient." - mutability: payable - access: anyone - inputs: - - target: address - - targetChainId: uint256 - - amount: uint256 - - gasRefundAddress: address - outputs: [] - -toLzChainId: - notes: - - "Lookup helper from local chain id to LayerZero chain id." - mutability: view - inputs: - - chainId: uint256 - outputs: - - lzChainId: uint16 - -fromLzChainId: - notes: - - "Lookup helper from LayerZero chain id to local chain id." - mutability: view - inputs: - - lzChainId: uint16 - outputs: - - chainId: uint256 - -toAxelarChainId: - notes: - - "Pure mapping in current implementation: chainId 1 -> 'Ethereum', 5 -> 'ethereum-2', 42220 and 44787 -> 'celo'; any other id returns empty string and outbound Axelar reverts UNSUPPORTED_CHAIN." - mutability: pure - inputs: - - chainId: uint256 - outputs: - - axlChainId: string - -fromAxelarChainId: - notes: - - "Maps Axelar chain-name strings back into local numeric chain ids." - mutability: pure - inputs: - - axlChainId: string - outputs: - - chainId: uint256 - -estimateSendFee: - notes: - - "Transport fee preflight for LZ path." - - "Builds payload with request id set to 0 for estimation." - - "Returns (0,0) on endpoint estimation failure in handler implementation." - - "_normalizedAmount must match BridgeHelperLibrary.normalizeFromTokenTo18Decimals burned raw amount using native token decimals on the outbound chain." - mutability: view - inputs: - - _dstChainId: uint16 - - _fromAddress: address - - _toAddress: address - - _normalizedAmount: uint256 - - _useZro: bool - - _adapterParams: bytes - outputs: - - nativeFee: uint256 - - zroFee: uint256 - -lzReceive: - notes: - - "LayerZero receive entrypoint." - - "Validates caller is configured endpoint before dispatching nonblocking receive pipeline." - mutability: nonpayable - inputs: - - _srcChainId: uint16 - - _srcAddress: bytes - - _nonce: uint64 - - _payload: bytes - outputs: [] - -lzEndpoint_: - notes: - - "Immutable LayerZero endpoint configured in constructor." - mutability: view - inputs: [] - outputs: - - endpoint: address - -HOME_CHAIN_ID: - notes: - - "Immutable deployment home chain marker." - mutability: view - inputs: [] - outputs: - - chainId: uint32 - -guardian: - notes: - - "Current guardian account authorized for admin operations." - mutability: view - inputs: [] - outputs: - - account: address - -executedRequests: - notes: - - "Replay protection state for inbound request ids." - mutability: view - inputs: - - id: uint256 - outputs: - - done: bool - -disabledSourceBridges: - notes: - - "Per-source bridge toggle state keyed by (sourceChainId, bridgeService) hash." - mutability: view - inputs: - - key: bytes32 - outputs: - - disabled: bool - -isClosed: - notes: - - "Outbound bridge pause state." - mutability: view - inputs: [] - outputs: - - closed: bool - -bridgeFees: - notes: - - "Current fee policy parameters: minFee, maxFee, feeBps." - mutability: view - inputs: [] - outputs: - - minFee: uint256 - - maxFee: uint256 - - fee: uint256 - -bridgeLimits: - notes: - - "Current bridge traffic limits and whitelist policy." - mutability: view - inputs: [] - outputs: - - dailyLimit: uint256 - - txLimit: uint256 - - accountDailyLimit: uint256 - - minAmount: uint256 - - onlyWhitelisted: bool - -bridgeDailyLimit: - notes: - - "Aggregate 24h bridge usage tracker." - mutability: view - inputs: [] - outputs: - - lastTransferReset: uint256 - - bridged24Hours: uint256 - -accountsDailyLimit: - notes: - - "Per-account 24h usage tracker." - mutability: view - inputs: - - account: address - outputs: - - lastTransferReset: uint256 - - bridged24Hours: uint256 - -faucet: - notes: - - "Optional gas top-up contract used on destination completion." - mutability: view - inputs: [] - outputs: - - addr: address - -currentId: - notes: - - "Monotonic nonce component used to derive outbound request ids." - mutability: view - inputs: [] - outputs: - - id: uint256 - -lzChainIdsMapping: - notes: - - "Local chain id -> LayerZero chain id mapping." - mutability: view - inputs: - - chainId: uint256 - outputs: - - lzChainId: uint16 - -feeRecipient: - notes: - - "Address receiving bridge fee mints on destination." - mutability: view - inputs: [] - outputs: - - recipient: address - -approvedRequests: - notes: - - "Manual approvals allowing request execution above standard limits." - mutability: view - inputs: - - id: uint256 - outputs: - - approved: bool - -lzChainToIdsMapping: - notes: - - "LayerZero chain id -> local chain id mapping." - mutability: view - inputs: - - lzChainId: uint16 - outputs: - - chainId: uint256 - -trustedRemoteLookup: - notes: - - "Trusted remote path used to authenticate inbound LZ/Axelar source contract." - mutability: view - inputs: - - lzChainId: uint16 - outputs: - - path: bytes - -gateway: - notes: - - "Axelar gateway address inherited from AxelarExecutable." - mutability: view - inputs: [] - outputs: - - addr: address - -gasService: - notes: - - "Axelar gas service used to prepay cross-chain execution." - mutability: view - inputs: [] - outputs: - - addr: address - -nameService: - notes: - - "DAO NameService pointer from DAOUpgradeableContract." - mutability: view - inputs: [] - outputs: - - addr: address - -avatar: - notes: - - "DAO avatar authority address." - mutability: view - inputs: [] - outputs: - - addr: address - -owner: - notes: - - "Ownable owner from upgradeable ownership flow." - mutability: view - inputs: [] - outputs: - - account: address - -events: - BridgeRequest: - indexed: - - from: address - - to: address - - id: uint256 - data: - - targetChainId: uint256 - - normalizedAmount: uint256 - - timestamp: uint256 - - bridge: uint8 - note: "Outbound intent event; relayers consume this to deliver payload on destination." - ExecutedTransfer: - indexed: - - from: address - - to: address - - id: uint256 - data: - - normalizedAmount: uint256 - - fee: uint256 - - sourceChainId: uint256 - - bridge: uint8 - note: "Destination completion event after replay checks, source-auth checks, limit checks, and mint/fee mint." - FalseSender: - indexed: [] - data: - - sourceChainId: uint256 - - sourceAddress: address - note: "Inbound message ignored because source contract does not match trusted remote path." - -errors: - AlreadyInitialized: "LZ chain support already initialized for this lz chain id." - NOT_GUARDIAN: - inputs: - - sender: address - WRONG_TOKEN: - inputs: - - token: address - INVALID_TARGET_OR_CHAINID: - inputs: - - target: address - - chainId: uint256 - BRIDGE_LIMITS: - inputs: - - reason: string - TRANSFER_FROM: "Source burn/transferFrom style failure." - TRANSFER: "Destination transfer style failure." - ALREADY_EXECUTED: - inputs: - - requestId: uint256 - MISSING_FEE: "Bridge call requires msg.value for transport fee." - UNSUPPORTED_CHAIN: - inputs: - - chainId: uint256 - LZ_FEE: - inputs: - - required: uint256 - - sent: uint256 - INVALID_SENDER: - inputs: - - _srcAddress: bytes - INVALID_ENDPOINT: - inputs: - - lzEndpoint: address diff --git a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml deleted file mode 100644 index 104b943..0000000 --- a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address,(uint256,uint256,uint256,uint256,bool),(uint256,uint256,uint256)): 0xfad16abe - upgrade(): 0xd55ec697 - addLzChainSupport(uint16,uint256,address): 0xc01c8123 - approveRequest(uint256): 0xd7d1bbdb - preventRequest(uint256): 0xc612f9ea - setFeeRecipient(address): 0xe74b981b - setBridgeLimits((uint256,uint256,uint256,uint256,bool)): 0x29b70872 - setBridgeFees((uint256,uint256,uint256)): 0x82dc737d - setDisabledBridges(bytes32[],bool[]): 0xc78ab882 - setFaucet(address): 0xd8b31c77 - setGuardian(address): 0x8a0dac4a - withdraw(address,uint256): 0xf3fef3a3 - pauseBridge(bool): 0x1a394795 - canBridge(address,uint256): 0x3095634a - bridgeTo(address,uint256,uint256,uint8): 0x1fec5c5c - bridgeToWithLz(address,uint256,uint256,bytes): 0xc56bbdd9 - bridgeToWithAxelar(address,uint256,uint256,address): 0x40a00aaf - toLzChainId(uint256): 0x5b23a990 - fromLzChainId(uint16): 0x16ad5512 - toAxelarChainId(uint256): 0x9a9ee081 - fromAxelarChainId(string): 0x56026f37 - estimateSendFee(uint16,address,address,uint256,bool,bytes): 0x05fead15 - lzReceive(uint16,bytes,uint64,bytes): 0x001d3567 - lzEndpoint_(): 0x020051cf - HOME_CHAIN_ID(): 0x8f65be85 - guardian(): 0x452a9320 - executedRequests(uint256): 0x425cfb53 - disabledSourceBridges(bytes32): 0x69c20d36 - isClosed(): 0xc2b6b58c - bridgeFees(): 0x7b0240c0 - bridgeLimits(): 0xc6dd812f - bridgeDailyLimit(): 0xb2f7667f - accountsDailyLimit(address): 0xd4227947 - faucet(): 0xde5f72fd - currentId(): 0xe00dd161 - lzChainIdsMapping(uint256): 0xb5569f18 - feeRecipient(): 0x46904840 - approvedRequests(uint256): 0x0dce292e - lzChainToIdsMapping(uint16): 0x73800fc4 - trustedRemoteLookup(uint16): 0x7533d788 - gateway(): 0x116191b6 - gasService(): 0x6a22d8cc - nameService(): 0x3e6326fc - avatar(): 0x5aef7de6 - owner(): 0x8da5cb5b -events: - BridgeRequest(address,address,uint256,uint256,uint256,uint256,uint8): 0x34b675c8c84b6a9e7979a0d3a54f2b036a19d6444167091a925af2d81e8e66fe - ExecutedTransfer(address,address,uint256,uint256,uint256,uint256,uint8): 0x81e772e0c4366ddbae472926005267ef278dbb257be45dfe97c676ceae348dbe - FalseSender(uint256,address): 0x1eafb58197ea0dc76b9278ccad47f61a239f00b017d89a19081a89321cce213d -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml b/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml deleted file mode 100644 index 01692b7..0000000 --- a/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml +++ /dev/null @@ -1,104 +0,0 @@ -# NameService — string-key to address registry for GoodProtocol deployments -# Resolves keys such as IDENTITY, UBISCHEME, GOODDOLLAR, GDAO_STAKING. - -meta: - name: NameService - version: UUPS-upgradeable v1 - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/NameService.sol - implements: [] - inherits: [Initializable, UUPSUpgradeable] - note: > - Almost every integration starts here. Writes are restricted to the DAO - avatar. Reads are permissionless. Use getAddress(string) with the same - key strings your deployment documented in releases/deployment.json. - deployments: - mainnet: - production: - NameService: - networkId: 122 - address: "0xec6dcE387B1616a0c44fF2E4fA9E90e53Cf14eb0" - creationBlock: 15740314 - production-celo: - NameService: - networkId: 42220 - address: "0x0F5dB7a64A6a64052693676CA898EC7F7A94FF4e" - creationBlock: 17237962 - production-xdc: - NameService: - networkId: 50 - address: "0x1e5154Bf5e31FF56051bbd45958b879Fb7a290FE" - creationBlock: 95143608 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/nameservice - - https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json - -addresses: - # Solidity auto-getter for public mapping addresses(bytes32 => address). - notes: - - "Prefer getAddress(string) off-chain; this expects keccak256(abi.encodePacked(name)) for manual lookups." - mutability: view - inputs: - - nameHash: bytes32 - outputs: - - addr: address - -dao: - mutability: view - inputs: [] - outputs: - - controller: address - -initialize: - notes: - - "Initializer wires Controller and optional pre-hashed name batch." - - "Automatically registers CONTROLLER and AVATAR hashes to dao and dao.avatar()." - mutability: nonpayable - access: initializer - inputs: - - _dao: address - - _nameHashes: bytes32[] - - _addresses: address[] - outputs: [] - -setAddress: - notes: - - "Stores addresses[keccak256(bytes(name))] = addr and emits AddressChanged." - mutability: nonpayable - access: avatar - inputs: - - name: string - - addr: address - outputs: [] - emits: [AddressChanged] - errors: [ONLY_AVATAR] - -setAddresses: - notes: - - "Batch write by raw bytes32 keys; no per-name event emission in this loop." - mutability: nonpayable - access: avatar - inputs: - - hash: bytes32[] - - addrs: address[] - outputs: [] - errors: [ONLY_AVATAR] - -getAddress: - notes: - - "Primary lookup used by claim, staking, identity, and swap integrations." - mutability: view - inputs: - - name: string - outputs: - - addr: address - -events: - AddressChanged: - indexed: [] - data: - - name: string - - addr: address - -errors: - ONLY_AVATAR: "only avatar can call this method — revert string on any avatar-gated path including UUPS _authorizeUpgrade." diff --git a/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml b/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml deleted file mode 100644 index 48e1bcf..0000000 --- a/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - addresses(bytes32): 0x699f200f - dao(): 0x4162169f - initialize(address,bytes32[],address[]): 0xd41b4e72 - setAddress(string,address): 0x9b2ea4bd - setAddresses(bytes32[],address[]): 0x4ab01f5b - getAddress(string): 0xbf40fac1 -events: - AddressChanged(string,address): 0x135cf55549d8538a41f19f46cc85625da93e68b63484cca8fcb9aaf19e520137 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml b/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml deleted file mode 100644 index abfc91c..0000000 --- a/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml +++ /dev/null @@ -1,1033 +0,0 @@ -# SuperToken — the Superfluid Protocol's token standard -# Merges three variants into a single ABI surface: -# • Wrapper Super Token — wraps an existing ERC-20 (upgrade / downgrade) -# • Native Asset Super Token (SETH) — wraps chain-native asset (upgradeByETH / downgradeToETH) -# • Pure Super Token — pre-minted, no underlying token (upgrade/downgrade revert) -# -# Inherits ERC-20, ERC-777, ERC-2612 (permit), EIP-5267, and the Superfluid real-time -# balance and agreement hosting layer from SuperfluidToken. -# -# Proxy / upgradability functions are omitted: -# castrate, getCodeAddress, proxiableUUID, initialize, initializeWithAdmin, updateCode -# Proxy-related events are also omitted: CodeUpdated, Initialized. - -meta: - name: SuperToken - version: v1 - source: - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/IYieldBackend.sol - inherits: [SuperfluidToken, ISuperToken, UUPSProxiable] - yield_backends: - - name: AaveYieldBackend - note: "Production backend for ERC-20 wrapper tokens using Aave V3" - source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/AaveYieldBackend.sol - - name: AaveETHYieldBackend - note: "Variant for native-asset (ETH/WETH) wrapper tokens using Aave V3" - source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/AaveETHYieldBackend.sol - - name: ERC4626YieldBackend - note: "Generic backend for any ERC-4626 vault" - source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/ERC4626YieldBackend.sol - - name: SparkYieldBackend - note: "Extends ERC4626YieldBackend for Spark Protocol vaults with referral tracking" - source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SparkYieldBackend.sol - variants: - wrapper: "Standard ERC-20 wrapper — upgrade() / downgrade()" - native-asset: "Native asset wrapper (SETH) — upgradeByETH() / downgradeToETH()" - pure: "Pre-minted supply, no underlying — upgrade/downgrade revert" - note: > - Super Tokens are deployed as individual proxies (one per token) by the - SuperTokenFactory. There is no single canonical deployment address. - All proxies share the same logic contract which is upgraded via governance. - -# wrapper super token — a Super Token backed by an ERC-20 underlying token -# native asset super token (SETH) — a Super Token backed by the chain-native asset (ETH, MATIC, etc.) -# pure super token — a Super Token with no underlying; supply is pre-minted at deploy time -# underlying token — the ERC-20 token that a wrapper super token wraps -# real-time balance — balance that changes continuously based on active agreements (CFA/GDA) -# available balance — real-time balance minus locked deposits -# critical — an account whose available balance is negative -# solvent — an account whose real-time balance (before deposit deductions) is non-negative -# operator (ERC-777) — an address authorized to send/burn tokens on behalf of a holder -# buffer / deposit — collateral locked while an outgoing flow is active - -# == ERC-20 Token Info == - -name: - mutability: view - inputs: [] - outputs: - - string - -symbol: - mutability: view - inputs: [] - outputs: - - string - -decimals: - # Always returns 18 regardless of underlying token decimals. - notes: - - "Always returns 18, regardless of the underlying token's decimals (e.g. USDC has 6). The upgrade/downgrade functions handle decimal conversion internally." - mutability: pure - inputs: [] - outputs: - - uint8 - -totalSupply: - mutability: view - inputs: [] - outputs: - - uint256 - -# == ERC-20 Balance & Allowance == - -balanceOf: - # Returns max(0, availableBalance) — clamps negative balances to zero. - # For the full real-time picture, use realtimeBalanceOfNow. - notes: - - "Returns max(0, availableBalance) — negative/critical balances are clamped to zero. Use realtimeBalanceOfNow to detect critical accounts." - mutability: view - inputs: - - account: address - outputs: - - balance: uint256 - -allowance: - mutability: view - inputs: - - account: address - - spender: address - outputs: - - uint256 - -approve: - mutability: nonpayable - access: anyone - inputs: - - spender: address - - amount: uint256 - outputs: - - bool - emits: [Approval] - errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -increaseAllowance: - mutability: nonpayable - access: anyone - inputs: - - spender: address - - addedValue: uint256 - outputs: - - bool - emits: [Approval] - errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -decreaseAllowance: - mutability: nonpayable - access: anyone - inputs: - - spender: address - - subtractedValue: uint256 - outputs: - - bool - emits: [Approval] - errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -# == ERC-20 Transfers == - -transfer: - # ERC-20 transfer. Does NOT invoke ERC-777 send/receive hooks (by design). - mutability: nonpayable - access: anyone - inputs: - - recipient: address - - amount: uint256 - outputs: - - bool - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -transferFrom: - # ERC-20 transferFrom. Does NOT invoke ERC-777 send/receive hooks. - # Does NOT emit an Approval event on allowance spend (OZ v5 behaviour). - mutability: nonpayable - access: anyone - inputs: - - holder: address - - recipient: address - - amount: uint256 - outputs: - - bool - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -transferAll: - # Transfer the caller's entire balanceOf to recipient. - mutability: nonpayable - access: anyone # transfers msg.sender's entire balance - inputs: - - recipient: address - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -# == ERC-777 == - -granularity: - # Always returns 1. - mutability: pure - inputs: [] - outputs: - - uint256 - -send: - # ERC-777 send — invokes tokensToSend / tokensReceived hooks. - mutability: nonpayable - access: anyone - inputs: - - recipient: address - - amount: uint256 - - userData: bytes - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] - -burn: - # ERC-777 burn — in SuperToken this actually performs a downgrade (unwrap). - notes: - - "Gotcha: Reverts with SUPER_TOKEN_NO_UNDERLYING_TOKEN on Pure Super Tokens." - mutability: nonpayable - access: anyone - inputs: - - amount: uint256 - - userData: bytes - emits: [Burned, Transfer, TokenDowngraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED] - -authorizeOperator: - mutability: nonpayable - access: anyone - inputs: - - operator: address - emits: [AuthorizedOperator] - -revokeOperator: - mutability: nonpayable - access: anyone - inputs: - - operator: address - emits: [RevokedOperator] - -isOperatorFor: - mutability: view - inputs: - - operator: address - - tokenHolder: address - outputs: - - bool - -defaultOperators: - mutability: view - inputs: [] - outputs: - - "address[]" - -operatorSend: - # ERC-777 operatorSend — invokes tokensToSend / tokensReceived hooks. - mutability: nonpayable - access: operator - inputs: - - sender: address - - recipient: address - - amount: uint256 - - userData: bytes - - operatorData: bytes - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] - -operatorBurn: - # ERC-777 operatorBurn — like burn(), actually performs a downgrade. - mutability: nonpayable - access: operator - inputs: - - account: address - - amount: uint256 - - userData: bytes - - operatorData: bytes - emits: [Burned, Transfer, TokenDowngraded] - errors: [SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED] - -# == ERC-20 Permit == - -permit: - mutability: nonpayable - access: anyone - inputs: - - owner: address - - spender: address - - value: uint256 - - deadline: uint256 - - v: uint8 - - r: bytes32 - - s: bytes32 - emits: [Approval] - errors: [SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE, SUPER_TOKEN_PERMIT_INVALID_SIGNER, ECDSAInvalidSignature, ECDSAInvalidSignatureLength, ECDSAInvalidSignatureS, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -nonces: - mutability: view - inputs: - - owner: address - outputs: - - uint256 - -DOMAIN_SEPARATOR: - mutability: view - inputs: [] - outputs: - - bytes32 - -eip712Domain: - # EIP-5267 domain retrieval. - mutability: view - inputs: [] - outputs: - - fields: bytes1 - - name: string - - version: string - - chainId: uint256 - - verifyingContract: address - - salt: bytes32 - - "extensions: uint256[]" - -# == ERC-20 Wrapping == -# These functions interact with the underlying ERC-20 token. -# They revert with SUPER_TOKEN_NO_UNDERLYING_TOKEN on Pure and Native Asset -# Super Tokens (use upgradeByETH / downgradeToETH for SETH). - -upgrade: - # Wrap underlying ERC-20 into Super Tokens for msg.sender. - # Requires prior ERC-20 approval on the underlying token. - notes: - - "Gotcha: amount is always in SuperToken decimals (18), regardless of the underlying token's decimals. The contract handles downscaling internally when pulling from the underlying via transferFrom. However, the ERC-20 approval on the underlying token must use the underlying's native decimals. Example: to wrap 1000 USDC, approve 1000e6 on USDC, then call upgrade(1000e18)." - mutability: nonpayable - access: anyone # wraps for msg.sender - inputs: - - amount: uint256 # in SuperToken decimals (always 18) - emits: [Minted, Transfer, TokenUpgraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] - -upgradeTo: - # Wrap underlying ERC-20 and mint Super Tokens to a different address. - mutability: nonpayable - access: anyone - inputs: - - to: address - - amount: uint256 - - userData: bytes - emits: [Minted, Transfer, TokenUpgraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] - -downgrade: - # Unwrap Super Tokens back to underlying ERC-20 for msg.sender. - mutability: nonpayable - access: anyone # unwraps to msg.sender - inputs: - - amount: uint256 - emits: [Burned, Transfer, TokenDowngraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] - -downgradeTo: - # Unwrap Super Tokens and send underlying ERC-20 to a different address. - mutability: nonpayable - access: anyone - inputs: - - to: address - - amount: uint256 - emits: [Burned, Transfer, TokenDowngraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] - -getUnderlyingToken: - # Returns address(0) for Pure and Native Asset Super Tokens. - mutability: view - inputs: [] - outputs: - - address - -getUnderlyingDecimals: - mutability: view - inputs: [] - outputs: - - uint8 - -toUnderlyingAmount: - # Convert a Super Token amount to its underlying equivalent, accounting for - # decimal differences. adjustedAmount strips precision loss. - mutability: view - inputs: - - amount: uint256 - outputs: - - underlyingAmount: uint256 - - adjustedAmount: uint256 - -# == Native Asset Wrapping (SETH only) == -# These functions exist only on Native Asset Super Token (SETH) proxies. -# They are defined in the SETHProxy contract, not in the SuperToken logic. -# The SETH proxy also accepts plain ETH transfers via receive() which mints -# Super Tokens to the sender. - -upgradeByETH: - # Wrap sent ETH/native asset into Super Tokens for msg.sender. - # Variant: native-asset - mutability: payable - access: anyone # wraps for msg.sender - inputs: [] - emits: [Minted, Transfer, TokenUpgraded] - -upgradeByETHTo: - # Wrap sent ETH/native asset into Super Tokens for a different address. - # Variant: native-asset - mutability: payable - access: anyone - inputs: - - to: address - emits: [Minted, Transfer, TokenUpgraded] - -downgradeToETH: - # Unwrap Super Tokens back to ETH/native asset for msg.sender. - # Variant: native-asset - mutability: nonpayable - access: anyone # unwraps to msg.sender - inputs: - - wad: uint256 - emits: [Burned, Transfer, TokenDowngraded] - -# == Real-time Balance == - -realtimeBalanceOf: - # Full real-time balance across ALL agreements at a given timestamp. - # This aggregates the realtimeBalanceOf from every agreement (CFA, GDA, IDA) - # into a single balance. Use this (or realtimeBalanceOfNow) to check an - # account's actual balance — not the per-agreement versions. - # availableBalance = settled + agreement dynamics − max(0, deposit − owedDeposit) - mutability: view - inputs: - - account: address - - timestamp: uint256 - outputs: - - availableBalance: int256 - - deposit: uint256 - - owedDeposit: uint256 - -realtimeBalanceOfNow: - # Convenience wrapper: realtimeBalanceOf at the current Host timestamp. - # This is the recommended way to check an account's real-time balance. - mutability: view - inputs: - - account: address - outputs: - - availableBalance: int256 - - deposit: uint256 - - owedDeposit: uint256 - - timestamp: uint256 - -isAccountCritical: - # True if availableBalance < 0 at the given timestamp. - mutability: view - inputs: - - account: address - - timestamp: uint256 - outputs: - - isCritical: bool - -isAccountCriticalNow: - mutability: view - inputs: - - account: address - outputs: - - isCritical: bool - -isAccountSolvent: - # True if realtime balance (before deposit deduction) >= 0 at the given timestamp. - mutability: view - inputs: - - account: address - - timestamp: uint256 - outputs: - - isSolvent: bool - -isAccountSolventNow: - mutability: view - inputs: - - account: address - outputs: - - isSolvent: bool - -# == Admin == -# The admin defaults to the Host contract when address(0). -# An explicit admin can be set during initializeWithAdmin or via changeAdmin. - -changeAdmin: - mutability: nonpayable - access: admin - inputs: - - newAdmin: address - emits: [AdminChanged] - errors: [SUPER_TOKEN_ONLY_ADMIN] - -getAdmin: - # Returns address(0) if the Host is the implicit admin. - mutability: view - inputs: [] - outputs: - - address - -# == Yield Backend == -# Hot-pluggable yield generation for wrapper Super Tokens. The yield backend is a -# contract implementing IYieldBackend that is invoked via delegatecall. When enabled, -# the underlying ERC-20 is deposited into a yield protocol (e.g. Aave, Spark, ERC4626 vaults). -# Surplus yield (balance exceeding totalSupply) can be withdrawn by the admin. - -enableYieldBackend: - # Enable a yield backend and deposit all underlying token balance into it. - # Requires no existing yield backend to be set. - notes: - - "Gotcha: Reverts if a yield backend is already enabled — must call disableYieldBackend first to switch backends." - - "The yield backend contract is invoked via delegatecall — it executes in the SuperToken's context." - mutability: nonpayable - access: admin - inputs: - - newYieldBackend: address # must implement IYieldBackend - emits: [YieldBackendEnabled] - -disableYieldBackend: - # Withdraw all assets from the yield backend and disable it. - notes: - - "Calls withdrawMax() then disable() on the backend via delegatecall." - mutability: nonpayable - access: admin - inputs: [] - emits: [YieldBackendDisabled] - -getYieldBackend: - # Returns the current yield backend address, or address(0) if none is set. - mutability: view - inputs: [] - outputs: - - yieldBackend: address - -withdrawSurplusFromYieldBackend: - # Withdraw yield surplus (deposited amount exceeding totalSupply) from the backend. - # The surplus is sent to a receiver defined by the yield backend implementation. - mutability: nonpayable - access: admin - inputs: [] - -# == Self Operations == -# Can only be called by the token contract itself (address(this)). -# Used by custom super token proxies (SETH, Pure) to mint/burn/approve/transfer -# tokens through their own proxy logic. - -selfMint: - mutability: nonpayable - access: self - inputs: - - account: address - - amount: uint256 - - userData: bytes - emits: [Minted, Transfer] - errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS] - -selfBurn: - mutability: nonpayable - access: self - inputs: - - account: address - - amount: uint256 - - userData: bytes - emits: [Burned, Transfer] - errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE] - -selfApproveFor: - mutability: nonpayable - access: self - inputs: - - account: address - - spender: address - - amount: uint256 - emits: [Approval] - errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -selfTransferFrom: - mutability: nonpayable - access: self - inputs: - - holder: address - - spender: address - - recipient: address - - amount: uint256 - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -# == Host Batch Operations == -# Called by the Host contract on behalf of a user within batchCall / -# forwardBatchCall. Each mirrors a user-facing function but takes the -# account address explicitly. - -operationApprove: - mutability: nonpayable - access: host - inputs: - - account: address - - spender: address - - amount: uint256 - emits: [Approval] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -operationIncreaseAllowance: - mutability: nonpayable - access: host - inputs: - - account: address - - spender: address - - addedValue: uint256 - emits: [Approval] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -operationDecreaseAllowance: - mutability: nonpayable - access: host - inputs: - - account: address - - spender: address - - subtractedValue: uint256 - emits: [Approval] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -operationTransferFrom: - mutability: nonpayable - access: host - inputs: - - account: address - - spender: address - - recipient: address - - amount: uint256 - emits: [Sent, Transfer] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -operationSend: - mutability: nonpayable - access: host - inputs: - - spender: address - - recipient: address - - amount: uint256 - - userData: bytes - emits: [Sent, Transfer] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] - -operationUpgrade: - mutability: nonpayable - access: host - inputs: - - account: address - - amount: uint256 - emits: [Minted, Transfer, TokenUpgraded] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] - -operationDowngrade: - mutability: nonpayable - access: host - inputs: - - account: address - - amount: uint256 - emits: [Burned, Transfer, TokenDowngraded] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] - -operationUpgradeTo: - mutability: nonpayable - access: host - inputs: - - account: address - - to: address - - amount: uint256 - emits: [Minted, Transfer, TokenUpgraded] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] - -operationDowngradeTo: - mutability: nonpayable - access: host - inputs: - - account: address - - to: address - - amount: uint256 - emits: [Burned, Transfer, TokenDowngraded] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] - -# == Agreement Hosting == -# The token acts as storage host for agreement data. These functions are -# called by registered agreement contracts (CFA, GDA, IDA) through the Host. - -createAgreement: - mutability: nonpayable - access: agreement - inputs: - - id: bytes32 - - "data: bytes32[]" - emits: [AgreementCreated] - errors: [SF_TOKEN_AGREEMENT_ALREADY_EXISTS] - -getAgreementData: - mutability: view - inputs: - - agreementClass: address - - id: bytes32 - - dataLength: uint256 - outputs: - - "data: bytes32[]" - -updateAgreementData: - mutability: nonpayable - access: agreement - inputs: - - id: bytes32 - - "data: bytes32[]" - emits: [AgreementUpdated] - -terminateAgreement: - mutability: nonpayable - access: agreement - inputs: - - id: bytes32 - - dataLength: uint256 - emits: [AgreementTerminated] - errors: [SF_TOKEN_AGREEMENT_DOES_NOT_EXIST] - -updateAgreementStateSlot: - mutability: nonpayable - access: agreement - inputs: - - account: address - - slotId: uint256 - - "slotData: bytes32[]" - emits: [AgreementStateUpdated] - -getAgreementStateSlot: - mutability: view - inputs: - - agreementClass: address - - account: address - - slotId: uint256 - - dataLength: uint256 - outputs: - - "slotData: bytes32[]" - -settleBalance: - # Adjust an account's settled balance. Only listed agreements can call this. - mutability: nonpayable - access: agreement - inputs: - - account: address - - delta: int256 - errors: [SF_TOKEN_ONLY_LISTED_AGREEMENT] - -makeLiquidationPayoutsV2: - # Execute liquidation payouts. Transfers reward to liquidator or reward account, - # and adjusts the target account's balance. Only listed agreements can call this. - mutability: nonpayable - access: agreement - inputs: - - id: bytes32 - - liquidationTypeData: bytes - - liquidatorAccount: address - - useDefaultRewardAccount: bool - - targetAccount: address - - rewardAmount: uint256 - - targetAccountBalanceDelta: int256 - emits: [Transfer, AgreementLiquidatedV2] - errors: [SF_TOKEN_ONLY_LISTED_AGREEMENT] - -getAccountActiveAgreements: - mutability: view - inputs: - - account: address - outputs: - - "ISuperAgreement[]" - -# == Protocol Info == - -getHost: - mutability: view - inputs: [] - outputs: - - host: address - -POOL_ADMIN_NFT: - # Immutable address of the canonical PoolAdminNFT proxy. - mutability: view - inputs: [] - outputs: - - address - -POOL_MEMBER_NFT: - # Immutable address of the (deprecated) PoolMemberNFT proxy. - mutability: view - inputs: [] - outputs: - - address - -VERSION: - # Returns the version string of the SuperToken logic contract (e.g. "1.0.0"). - mutability: view - inputs: [] - outputs: - - string - -# == Events == - -events: - - # -- ERC-20 -- - - Transfer: - indexed: - - from: address - - to: address - data: - - value: uint256 - - Approval: - indexed: - - owner: address - - spender: address - data: - - value: uint256 - - # -- ERC-777 -- - - Sent: - indexed: - - operator: address - - from: address - - to: address - data: - - amount: uint256 - - data: bytes - - operatorData: bytes - - Minted: - indexed: - - operator: address - - to: address - data: - - amount: uint256 - - data: bytes - - operatorData: bytes - - Burned: - indexed: - - operator: address - - from: address - data: - - amount: uint256 - - data: bytes - - operatorData: bytes - - AuthorizedOperator: - indexed: - - operator: address - - tokenHolder: address - - RevokedOperator: - indexed: - - operator: address - - tokenHolder: address - - # -- EIP-5267 -- - - EIP712DomainChanged: - # Signalled when the EIP-712 domain changes. Not explicitly emitted by - # SuperToken, but declared in the EIP-5267 interface for indexing support. - - # -- Wrapping -- - - TokenUpgraded: - indexed: - - account: address - data: - - amount: uint256 - - TokenDowngraded: - indexed: - - account: address - data: - - amount: uint256 - - # -- Admin -- - - AdminChanged: - indexed: [] - data: - - oldAdmin: address - - newAdmin: address - - # -- Yield Backend -- - - YieldBackendEnabled: - indexed: - - yieldBackend: address - data: - - depositAmount: uint256 - - YieldBackendDisabled: - indexed: - - yieldBackend: address - - PoolAdminNFTCreated: - # Emitted once during logic contract constructor deployment. - indexed: [] - data: - - poolAdminNFT: address - - # -- Agreement Hosting -- - - AgreementCreated: - indexed: - - agreementClass: address - - id: bytes32 - data: - - "data: bytes32[]" - - AgreementUpdated: - indexed: - - agreementClass: address - - id: bytes32 - data: - - "data: bytes32[]" - - AgreementTerminated: - indexed: - - agreementClass: address - - id: bytes32 - - AgreementStateUpdated: - indexed: - - agreementClass: address - - account: address - data: - - slotId: uint256 - - AgreementLiquidatedV2: - indexed: - - agreementClass: address - - id: bytes32 - data: - - liquidatorAccount: address - - targetAccount: address - - rewardAmountReceiver: address - - rewardAmount: uint256 - - targetAccountBalanceDelta: int256 - - liquidationTypeData: bytes - - # -- Deprecated events (from ISuperfluidToken, no longer emitted) -- - - AgreementLiquidated: - deprecated: true - note: "Use AgreementLiquidatedV2" - indexed: - - agreementClass: address - - id: bytes32 - data: - - penaltyAccount: address - - rewardAccount: address - - rewardAmount: uint256 - - AgreementLiquidatedBy: - deprecated: true - note: "Use AgreementLiquidatedV2" - indexed: - - liquidatorAccount: address - - agreementClass: address - - id: bytes32 - data: - - penaltyAccount: address - - bondAccount: address - - rewardAmount: uint256 - - bailoutAmount: uint256 - - Bailout: - deprecated: true - note: "Use AgreementLiquidatedV2" - indexed: - - bailoutAccount: address - - bailoutAmount: uint256 - -# == Errors == - -errors: - - # -- Super Token access -- - - SUPER_TOKEN_ONLY_SELF - - SUPER_TOKEN_ONLY_ADMIN - - SUPER_TOKEN_ONLY_GOV_OWNER - - # -- SuperfluidToken access -- - - SF_TOKEN_ONLY_HOST - - SF_TOKEN_ONLY_LISTED_AGREEMENT - - # -- Wrapping -- - - SUPER_TOKEN_NO_UNDERLYING_TOKEN - - SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED - - # -- ERC-20 / ERC-777 transfer -- - - SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS - - SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS - - SF_TOKEN_MOVE_INSUFFICIENT_BALANCE - - # -- Approve -- - - SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS - - SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS - - # -- Mint / Burn -- - - SUPER_TOKEN_MINT_TO_ZERO_ADDRESS - - SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS - - SF_TOKEN_BURN_INSUFFICIENT_BALANCE - - # -- ERC-777 operator -- - - SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER - - SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT - - # -- ERC-2612 permit -- - - SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE: - inputs: - - deadline: uint256 - - SUPER_TOKEN_PERMIT_INVALID_SIGNER: - inputs: - - signer: address - - owner: address - - # -- NFT proxy -- - - SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED - - # -- Agreement hosting -- - - SF_TOKEN_AGREEMENT_ALREADY_EXISTS - - SF_TOKEN_AGREEMENT_DOES_NOT_EXIST - - # -- OpenZeppelin ECDSA (from permit) -- - - ECDSAInvalidSignature - - ECDSAInvalidSignatureLength: - inputs: - - length: uint256 - - ECDSAInvalidSignatureS: - inputs: - - s: bytes32 - - # -- OpenZeppelin SafeCast -- - - SafeCastOverflowedUintToInt: - inputs: - - value: uint256 - - # -- OpenZeppelin SafeERC20 -- - - SafeERC20FailedOperation: - inputs: - - token: address diff --git a/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml b/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml deleted file mode 100644 index f9bb474..0000000 --- a/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml +++ /dev/null @@ -1,99 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - name(): 0x06fdde03 - symbol(): 0x95d89b41 - decimals(): 0x313ce567 - totalSupply(): 0x18160ddd - balanceOf(address): 0x70a08231 - allowance(address,address): 0xdd62ed3e - approve(address,uint256): 0x095ea7b3 - increaseAllowance(address,uint256): 0x39509351 - decreaseAllowance(address,uint256): 0xa457c2d7 - transfer(address,uint256): 0xa9059cbb - transferFrom(address,address,uint256): 0x23b872dd - transferAll(address): 0xa3a7e7f3 - granularity(): 0x556f0dc7 - send(address,uint256,bytes): 0x9bd9bbc6 - burn(uint256,bytes): 0xfe9d9303 - authorizeOperator(address): 0x959b8c3f - revokeOperator(address): 0xfad8b32a - isOperatorFor(address,address): 0xd95b6371 - defaultOperators(): 0x06e48538 - operatorSend(address,address,uint256,bytes,bytes): 0x62ad1b83 - operatorBurn(address,uint256,bytes,bytes): 0xfc673c4f - permit(address,address,uint256,uint256,uint8,bytes32,bytes32): 0xd505accf - nonces(address): 0x7ecebe00 - DOMAIN_SEPARATOR(): 0x3644e515 - eip712Domain(): 0x84b0196e - upgrade(): 0xd55ec697 - upgradeTo(address,uint256,bytes): 0x5b9d09cc - downgrade(uint256): 0x11bcc81e - downgradeTo(address,uint256): 0x83ba2525 - getUnderlyingToken(): 0xee719bc8 - getUnderlyingDecimals(): 0x92081a47 - toUnderlyingAmount(uint256): 0x282a050b - upgradeByETH(): 0xcf81464b - upgradeByETHTo(address): 0x7687d19b - downgradeToETH(uint256): 0x160e8be3 - realtimeBalanceOf(address,uint256): 0xeb3537cc - realtimeBalanceOfNow(address): 0x2ec8eec7 - isAccountCritical(address,uint256): 0xd9d078d6 - isAccountCriticalNow(address): 0x79359f6f - isAccountSolvent(address,uint256): 0xb84cdd4a - isAccountSolventNow(address): 0xbb0d196e - changeAdmin(address): 0x8f283970 - getAdmin(): 0x6e9960c3 - enableYieldBackend(): 0x5127b621 - disableYieldBackend(): 0x370a190f - getYieldBackend(): 0xe729804b - withdrawSurplusFromYieldBackend(): 0x4a3acda0 - selfMint(address,uint256,bytes): 0xc68d4283 - selfBurn(address,uint256,bytes): 0x9d876741 - selfApproveFor(address,address,uint256): 0x66a12fb6 - selfTransferFrom(address,address,address,uint256): 0x41b706be - operationApprove(address,address,uint256): 0x62aa5287 - operationIncreaseAllowance(address,address,uint256): 0x4b2763b3 - operationDecreaseAllowance(address,address,uint256): 0xc780fd82 - operationTransferFrom(address,address,address,uint256): 0x16d055d6 - operationSend(address,address,uint256,bytes): 0xca0c1e7f - operationUpgrade(address,uint256): 0xca789464 - operationDowngrade(address,uint256): 0x245887fc - operationUpgradeTo(address,address,uint256): 0x1ae88ffc - operationDowngradeTo(address,address,uint256): 0x47ba7ad1 - createAgreement(bytes32,bytes32[]"): 0x816e2dc5 - getAgreementData(address,bytes32,uint256): 0x6c2d9f2f - updateAgreementData(bytes32,bytes32[]"): 0x1bced9c7 - terminateAgreement(bytes32,uint256): 0x27048397 - updateAgreementStateSlot(address,uint256,bytes32[]"): 0x003af740 - getAgreementStateSlot(address,address,uint256,uint256): 0x4b61cc33 - settleBalance(address,int256): 0xcf97256d - makeLiquidationPayoutsV2(bytes32,bytes,address,bool,address,uint256,int256): 0x1863e809 - getAccountActiveAgreements(address): 0x386fa221 - getHost(): 0x20bc4425 - POOL_ADMIN_NFT(): 0xb20db1ac - POOL_MEMBER_NFT(): 0xf5a8b4dd - VERSION(): 0xffa1ad74 -events: - Transfer(address,address,uint256): 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef - Approval(address,address,uint256): 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 - Sent(address,address,address,uint256,bytes,bytes): 0x06b541ddaa720db2b10a4d0cdac39b8d360425fc073085fac19bc82614677987 - Minted(address,address,uint256,bytes,bytes): 0x2fe5be0146f74c5bce36c0b80911af6c7d86ff27e89d5cfa61fc681327954e5d - Burned(address,address,uint256,bytes,bytes): 0xa78a9be3a7b862d26933ad85fb11d80ef66b8f972d7cbba06621d583943a4098 - AuthorizedOperator(address,address): 0xf4caeb2d6ca8932a215a353d0703c326ec2d81fc68170f320eb2ab49e9df61f9 - RevokedOperator(address,address): 0x50546e66e5f44d728365dc3908c63bc5cfeeab470722c1677e3073a6ac294aa1 - EIP712DomainChanged(): 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31 - TokenUpgraded(address,uint256): 0x25ca84076773b0455db53621c459ddc84fe40840e4932a62706a032566f399df - TokenDowngraded(address,uint256): 0x3bc27981aebbb57f9247dc00fde9d6cd91e4b230083fec3238fedbcba1f9ab3d - AdminChanged(address,address): 0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f - YieldBackendEnabled(address,uint256): 0x8d15dd569157df615aedb4b16273001ae8980ce2aac93e39adc48481dfaefbb5 - YieldBackendDisabled(address): 0x2486b5241145ce1b97a13655ecd4e8e51094ac93259a0589d24524304d6d70d4 - PoolAdminNFTCreated(address): 0xeb87fb34067547f3dc0b85096c3da73c99d4fbb08ff41212b8d7c0b5008b42e6 - AgreementCreated(address,bytes32,bytes32[]"): 0x9a5caaf4460c6dff81ed3dc58f3aef40fe511737a163340e06a4abc1bfe21a73 - AgreementUpdated(address,bytes32,bytes32[]"): 0xc00aaaf56818c1a9d6c54e14c6f93965eb34c9729232fdafa62f90a10600e70a - AgreementTerminated(address,bytes32): 0x71a63dc095de07aa5512ad57a7596a39516317e316981a1cd71000057be1537b - AgreementStateUpdated(address,address,uint256): 0x30f416fa68fca014a0f334464c64b000ba53e99b6d2afdea9d5ca756372d5985 - AgreementLiquidatedV2(address,bytes32,address,address,address,uint256,int256,bytes): 0xb8381a3ce157650e06186e3e8f4dd4dc29236f2688b6eed1893d0a60d7c6386f - AgreementLiquidated(address,bytes32,address,address,uint256): 0x8505c3d8f1f184f032cf0bc4cd80ee61c8b9d94f8907c3281bf0101a2610fe80 - AgreementLiquidatedBy(address,address,bytes32,address,address,uint256,uint256): 0x5f22b60e58b1d6de858bc27c48d5a4653e052da99e083c1d88bb8c58e1abc8ef - Bailout(address,uint256): 0xd6c9a04afc81e8c614310bbee6c9e84f5abe15b82038bf8347014ce0852e6ffd -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml b/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml deleted file mode 100644 index 33ac5a9..0000000 --- a/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml +++ /dev/null @@ -1,757 +0,0 @@ -# Superfluid Host — the central protocol router -# All agreement calls (CFA, IDA, GDA) are routed through the Host, which manages -# the Superfluid call context, Super App callbacks, app credit, and governance. -# The Host is also the entry point for batch calls and meta-transactions. -# -# NOTE: emits/errors mappings are traced from source code — verify against implementation. -# Proxy/upgradability functions (castrate, updateCode, getCodeAddress, proxiableUUID, initialize) -# are omitted — they belong to the UUPSProxiable layer. - -meta: - name: Superfluid - version: v1 - source: - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol - implements: [ISuperfluid] - inherits: [UUPSProxiable, BaseRelayRecipient] - deployments: - mainnet: - eth-mainnet: "0x4E583d9390082B65Bef884b629DFA426114CED6d" - polygon-mainnet: "0x3E14dC1b13c488a8d5D310918780c983bD5982E7" - xdai-mainnet: "0x2dFe937cD98Ab92e59cF3139138f18c823a4efE7" - base-mainnet: "0x4C073B3baB6d8826b8C5b229f3cfdC1eC6E47E74" - optimism-mainnet: "0x567c4B141ED61923967cA25Ef4906C8781069a10" - arbitrum-one: "0xCf8Acb4eF033efF16E8080aed4c7D5B9285D2192" - bsc-mainnet: "0xd1e2cFb6441680002Eb7A44223160aB9B67d7E6E" - avalanche-c: "0x60377C7016E4cdB03C87EF474896C11cB560752C" - celo-mainnet: "0xA4Ff07cF81C02CFD356184879D953970cA957585" - scroll-mainnet: "0x0F86a21F6216c061B222c224e315d9FC34520bb7" - degenchain: "0xc1314EdcD7e478C831a7a24169F7dEADB2646eD2" - testnet: - avalanche-fuji: "0x85Fe79b998509B77BF10A8BD4001D58475D29386" - base-sepolia: "0x109412E3C84f0539b43d39dB691B08c90f58dC7c" - eth-sepolia: "0x109412E3C84f0539b43d39dB691B08c90f58dC7c" - optimism-sepolia: "0xd399e2Fb5f4cf3722a11F65b88FAB6B2B8621005" - scroll-sepolia: "0x42b05a6016B9eED232E13fd56a8F0725693DBF8e" - deploymentCreationBlocks: - mainnet: - celo-mainnet: 16393469 - -# == Abbreviations == -# ctx — context (Superfluid call context bytes) -# ACL — Access Control List -# ERC — Ethereum Request for Comments - -# == Glossary == -# Host — central Superfluid contract routing agreement/app calls -# agreement class — a registered agreement contract (CFA, IDA, GDA) -# Super App — a smart contract registered with the Host that receives callbacks -# callback — beforeAgreement* / afterAgreement* hooks called on Super Apps -# context (ctx) — encoded call metadata: msgSender, timestamp, userData, app credit -# app credit — temporary token allowance granted to a Super App during a callback -# jailed — a Super App that violated protocol rules; its callbacks are bypassed -# configWord — bitmask encoding a Super App's app level and callback noop flags. -# App level: APP_LEVEL_FINAL (1 << 1) or APP_LEVEL_SECOND (1 << 0). -# Noop flags disable specific before/after callbacks per agreement type. -# batch operation — a single step in a batchCall array (see BatchOperation in Definitions.sol) -# trusted forwarder — a contract allowed to call forwardBatchCall for meta-transactions - -# == Batch Call == -# Primary entry point for interacting with the protocol. Users compose arrays of -# typed operations (ERC-20 approvals, SuperToken wrapping, agreement calls, etc.). -# -# == Batch Operation Types == -# Used in the operationType field of batchCall/forwardBatchCall operations. -# Data encoding varies by category — see notes on each type. -# -# Category 1–5: ERC-20/ERC-777 token ops -# target: must be a SuperToken (routes through SuperToken.operation* methods, -# NOT arbitrary ERC-20 contracts — use a separate tx for underlying approvals) -# data: abi-encoded parameters only (strip the 4-byte function selector) -# -# Category 101–102: SuperToken wrap/unwrap -# target: the SuperToken -# data: abi-encoded parameters only (strip the 4-byte function selector) -# -# Category 201: agreement call -# target: the agreement contract (e.g. CFA, GDA) -# data: abi.encode(callData, userData) where callData is full -# encodeFunctionData (selector included, with empty ctx placeholder "0x") -# -# Category 202: app action -# target: the Super App -# data: abi.encode(callData) where callData is full encodeFunctionData -# (selector included, with empty ctx placeholder "0x") -# -# Category 301: simple forward -# target: any contract -# data: full encodeFunctionData (selector included), or "0x" for pure value transfers -# -# Category 302: ERC-2771 forward -# target: any contract (must be an ERC-2771 recipient to extract the real sender) -# data: full encodeFunctionData (selector included) -# The ERC2771Forwarder appends the original msg.sender to calldata per ERC-2771. -# Use when the target contract needs to know who initiated the batch. - -batch_operation_types: - ERC20_APPROVE: 1 - ERC20_TRANSFER_FROM: 2 - ERC777_SEND: 3 # deprecated — ERC-777 is being phased out - ERC20_INCREASE_ALLOWANCE: 4 - ERC20_DECREASE_ALLOWANCE: 5 - SUPERTOKEN_UPGRADE: 101 - SUPERTOKEN_DOWNGRADE: 102 - SUPERFLUID_CALL_AGREEMENT: 201 - CALL_APP_ACTION: 202 - SIMPLE_FORWARD_CALL: 301 - ERC2771_FORWARD_CALL: 302 - -batchCall: - # Execute an array of operations in a single transaction as msg.sender. - notes: - - "Gotcha: For SIMPLE_FORWARD_CALL (type 301), target sees msg.sender as the SimpleForwarder contract, NOT the original caller. Use only when the target doesn't need to identify the sender." - - "Gotcha: ERC-20 ops (types 1-5) route through SuperToken.operation* methods, NOT arbitrary ERC-20 contracts. The target must be a SuperToken address. To approve an underlying ERC-20 (e.g. for wrapping), use a separate transaction." - - "Gotcha: If msg.value > 0, the entire amount is forwarded to the first CALL_APP_ACTION, SIMPLE_FORWARD, or ERC2771_FORWARD operation. If none exist, the native tokens are returned to the sender." - mutability: payable - access: anyone - inputs: - - operations: tuple[] - components: - - operationType: uint32 # see batch_operation_types above - - target: address - - data: bytes # encoding varies by operationType — see above - errors: [HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE] - -forwardBatchCall: - # Same as batchCall but uses EIP-2771 to extract msgSender from calldata. - # Can only be called by contracts registered as trusted forwarders in governance. - # If native tokens are left over, they are refunded to the EIP-2771 sender. - mutability: payable - access: trusted-forwarder - inputs: - - operations: tuple[] - components: - - operationType: uint32 # see batch_operation_types above - - target: address - - data: bytes # encoding varies by operationType — see above - errors: [HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE] - -# == Contextless Call Proxies == -# Entry points for EOAs and non-app contracts to interact with agreements or apps. -# These create a fresh Superfluid context with msg.sender, inject it into the call, -# and route through to the target agreement/app. - -callAgreement: - # Call an agreement function. The Host creates a context with msg.sender and replaces - # the placeholder ctx bytes in callData. This is how EOAs interact with CFA/IDA/GDA. - mutability: nonpayable - access: anyone # context set to msg.sender - inputs: - - agreementClass: address # must be a registered agreement - - callData: bytes # abi-encoded agreement call with placeholder ctx - - userData: bytes # extra data forwarded to Super App callbacks - outputs: - - returnedData: bytes - errors: [APP_RULE, HOST_ONLY_LISTED_AGREEMENT] - -callAppAction: - # Call an app action function on a Super App. The Host creates a context and routes - # the call. The action must not be an agreement callback selector. - mutability: nonpayable - access: anyone # context set to msg.sender - inputs: - - app: address # must be a registered, non-jailed Super App - - callData: bytes # abi-encoded app action call with placeholder ctx - outputs: - - returnedData: bytes - errors: [APP_RULE, HOST_NOT_A_SUPER_APP, HOST_SUPER_APP_IS_JAILED, HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION] - -# == Contextual Call Proxies == -# For Super Apps to chain calls during callbacks. The app must pass the ctx it received. -# Only callable by the app currently executing a callback (ctx.appAddress == msg.sender). - -callAgreementWithContext: - # Chain an agreement call from within a Super App callback. - # The ctx must be valid and msg.sender must be the current callback app. - mutability: nonpayable - access: super-app - inputs: - - agreementClass: address - - callData: bytes - - userData: bytes - - ctx: bytes - outputs: - - newCtx: bytes - - returnedData: bytes - errors: [APP_RULE, HOST_ONLY_LISTED_AGREEMENT, HOST_CALL_AGREEMENT_WITH_CTX_FROM_WRONG_ADDRESS] - -callAppActionWithContext: - # Chain an app action call from within a Super App callback. - mutability: nonpayable - access: super-app - inputs: - - app: address - - callData: bytes - - ctx: bytes - outputs: - - newCtx: bytes - errors: [APP_RULE, HOST_NOT_A_SUPER_APP, HOST_SUPER_APP_IS_JAILED, HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION, HOST_CALL_APP_ACTION_WITH_CTX_FROM_WRONG_ADDRESS] - -# == Context Utilities == - -decodeCtx: - # Decode a raw context bytes blob into the structured Context fields. - mutability: pure - inputs: - - ctx: bytes - outputs: - - context: - type: tuple - components: - - appCallbackLevel: uint8 - - callType: uint8 - - timestamp: uint256 - - msgSender: address - - agreementSelector: bytes4 - - userData: bytes - - appCreditGranted: uint256 - - appCreditWantedDeprecated: uint256 - - appCreditUsed: int256 - - appAddress: address - - appCreditToken: address - -isCtxValid: - # Check if a context blob matches the current transaction's context stamp. - mutability: view - inputs: - - ctx: bytes - outputs: - - bool - -# == App Registry == -# Super App registration. On some deployments, governance permission (via SimpleACL -# or legacy governance config) is required before an app can register. - -registerApp: - # Register msg.sender as a Super App with the given config word. - # On whitelisting-enabled deployments, tx.origin must have the - # ACL_SUPERAPP_REGISTRATION_ROLE or a valid legacy governance key. - mutability: nonpayable - access: self - inputs: - - configWord: uint256 # Super App manifest flags (see SuperAppDefinitions) - emits: [AppRegistered] - errors: [HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] - -registerApp(address,uint256): - # Register an external contract as a Super App. - # Useful for factory patterns where the deployer registers the app. - mutability: nonpayable - access: anyone - inputs: - - app: address # must be a contract - - configWord: uint256 - emits: [AppRegistered] - errors: [HOST_MUST_BE_CONTRACT, HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] - -registerAppWithKey: - # DEPRECATED — use registerApp(uint256) instead. - # Legacy registration using a governance-provided registration key string. - mutability: nonpayable - access: self - inputs: - - configWord: uint256 - - registrationKey: string - emits: [AppRegistered] - errors: [HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] - -registerAppByFactory: - # DEPRECATED — use registerApp(address, uint256) instead. - # Legacy factory-based registration using governance-authorized factory addresses. - mutability: nonpayable - access: factory - inputs: - - app: address - - configWord: uint256 - emits: [AppRegistered] - errors: [HOST_MUST_BE_CONTRACT, HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] - -# == App Queries == - -isApp: - mutability: view - inputs: - - app: address - outputs: - - bool - -getAppCallbackLevel: - mutability: view - inputs: - - appAddr: address - outputs: - - uint8 - -getAppManifest: - # Returns the full manifest of a Super App. - # isSuperApp is false if the address was never registered. - mutability: view - inputs: - - app: address - outputs: - - isSuperApp: bool - - isJailed: bool - - noopMask: uint256 # bitmask of noop'd agreement callback selectors - -isAppJailed: - mutability: view - inputs: - - app: address - outputs: - - isJail: bool - -# == App Composition == -# Source apps can whitelist target apps for composability (calling downstream). -# Currently limited to MAX_APP_CALLBACK_LEVEL = 1 (one level of nesting). - -allowCompositeApp: - # Whitelist a target Super App for composition. msg.sender must be a Super App - # with a higher callback level than the target. - mutability: nonpayable - access: super-app - inputs: - - targetApp: address - errors: [HOST_SENDER_IS_NOT_SUPER_APP, HOST_RECEIVER_IS_NOT_SUPER_APP, HOST_SOURCE_APP_NEEDS_HIGHER_APP_LEVEL] - -isCompositeAppAllowed: - mutability: view - inputs: - - app: address - - targetApp: address - outputs: - - isAppAllowed: bool - -# == Agreement Framework == -# Internal protocol functions called by registered agreement contracts (CFA, IDA, GDA) -# to manage Super App callbacks and app credit. Only callable by agreements. -# -# Callback lifecycle (called by agreement during flow operations): -# 1. appCallbackPush — set up credit, push callback frame -# 2. callAppBeforeCallback — staticcall app's before hook (read-only) -# 3. (agreement executes core logic) -# 4. callAppAfterCallback — call app's after hook (state changes OK) -# 5. appCallbackPop — finalize credit accounting, pop frame - -callAppBeforeCallback: - # StaticCall a Super App's before-callback. Returns the callback data. - # If the callback reverts during a termination, the app is jailed instead of reverting. - mutability: nonpayable - access: agreement - inputs: - - app: address - - callData: bytes - - isTermination: bool - - ctx: bytes - outputs: - - cbdata: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT, HOST_NEED_MORE_GAS, APP_RULE] - -callAppAfterCallback: - # Call a Super App's after-callback. Returns the (potentially modified) context. - # If the app returns an invalid ctx, it is jailed on termination or reverts otherwise. - mutability: nonpayable - access: agreement - inputs: - - app: address - - callData: bytes - - isTermination: bool - - ctx: bytes - outputs: - - newCtx: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT, HOST_NEED_MORE_GAS, APP_RULE] - -appCallbackPush: - # Push a new callback frame onto the context stack before invoking a Super App. - # Sets up app credit and tracks the callback nesting level. - notes: - - "Gotcha: The appCreditGranted is backed by the flow sender's deposit as \"owed deposit\". If the Super App opens outgoing streams using this credit, the sender's total locked capital increases accordingly (roughly doubles for 1:1 relay, more for fan-out patterns)." - mutability: nonpayable - access: agreement - inputs: - - ctx: bytes - - app: address - - appCreditGranted: uint256 - - appCreditUsed: int256 - - appCreditToken: address - outputs: - - appCtx: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT, APP_RULE] - -appCallbackPop: - # Pop the callback frame after a Super App callback completes. - # Adjusts appCreditUsed by the delta from the callback. - mutability: nonpayable - access: agreement - inputs: - - ctx: bytes - - appCreditUsedDelta: int256 - outputs: - - newCtx: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT] - -ctxUseCredit: - # Record additional app credit usage during an agreement operation. - mutability: nonpayable - access: agreement - inputs: - - ctx: bytes - - appCreditUsedMore: int256 - outputs: - - newCtx: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT] - -jailApp: - # Jail a Super App from within an agreement. Jailed apps have their callbacks bypassed. - mutability: nonpayable - access: agreement - inputs: - - ctx: bytes - - app: address - - reason: uint256 # see SuperAppDefinitions for jail reason codes - outputs: - - newCtx: bytes - emits: [Jail] - errors: [HOST_ONLY_LISTED_AGREEMENT] - -# == Governance == - -getGovernance: - mutability: view - outputs: - - governance: address - -replaceGovernance: - mutability: nonpayable - access: governance - inputs: - - newGov: address - emits: [GovernanceReplaced] - errors: [HOST_ONLY_GOVERNANCE] - -# == Agreement Whitelisting == -# Governance registers and upgrades agreement classes (CFA, IDA, GDA). - -registerAgreementClass: - # Register a new agreement class. Creates a UUPS proxy in upgradable deployments. - mutability: nonpayable - access: governance - inputs: - - agreementClassLogic: address - emits: [AgreementClassRegistered] - errors: [HOST_ONLY_GOVERNANCE, HOST_AGREEMENT_ALREADY_REGISTERED, HOST_MAX_256_AGREEMENTS] - -updateAgreementClass: - # Upgrade an existing agreement class to a new implementation. - mutability: nonpayable - access: governance - inputs: - - agreementClassLogic: address - emits: [AgreementClassUpdated] - errors: [HOST_ONLY_GOVERNANCE, HOST_NON_UPGRADEABLE, HOST_AGREEMENT_IS_NOT_REGISTERED] - -isAgreementTypeListed: - mutability: view - inputs: - - agreementType: bytes32 # keccak256 of agreement name string - outputs: - - yes: bool - -isAgreementClassListed: - mutability: view - inputs: - - agreementClass: address - outputs: - - yes: bool - -getAgreementClass: - mutability: view - inputs: - - agreementType: bytes32 - outputs: - - agreementClass: address - errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] - -mapAgreementClasses: - # Decode a bitmap into an array of agreement class addresses. - mutability: view - inputs: - - bitmap: uint256 - outputs: - - agreementClasses: address[] - -addToAgreementClassesBitmap: - mutability: view - inputs: - - bitmap: uint256 - - agreementType: bytes32 - outputs: - - newBitmap: uint256 - errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] - -removeFromAgreementClassesBitmap: - mutability: view - inputs: - - bitmap: uint256 - - agreementType: bytes32 - outputs: - - newBitmap: uint256 - errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] - -# == Super Token Factory == - -getSuperTokenFactory: - mutability: view - outputs: - - factory: address - -getSuperTokenFactoryLogic: - # Returns the logic address behind the factory proxy. For non-upgradable - # deployments, returns the factory address itself. - mutability: view - outputs: - - logic: address - -updateSuperTokenFactory: - # Deploy or upgrade the SuperTokenFactory. On first call, creates a UUPS proxy. - mutability: nonpayable - access: governance - inputs: - - newFactory: address - emits: [SuperTokenFactoryUpdated] - errors: [HOST_ONLY_GOVERNANCE, HOST_NON_UPGRADEABLE] - -updateSuperTokenLogic: - # Upgrade a SuperToken proxy to new logic. If newLogicOverride is address(0), - # uses the canonical logic from the SuperTokenFactory. - mutability: nonpayable - access: governance - inputs: - - token: address - - newLogicOverride: address # address(0) means use canonical logic - emits: [SuperTokenLogicUpdated] - errors: [HOST_ONLY_GOVERNANCE] - -changeSuperTokenAdmin: - # Change the admin of a SuperToken. The admin is the only account allowed to - # upgrade the token logic. Default admin is the Host itself. - mutability: nonpayable - access: governance - inputs: - - token: address - - newAdmin: address - errors: [HOST_ONLY_GOVERNANCE] - -# == Pool Beacon == - -updatePoolBeaconLogic: - # Upgrade the SuperfluidPool beacon to new logic. Affects all GDA pool proxies. - mutability: nonpayable - access: governance - inputs: - - newLogic: address - emits: [PoolBeaconLogicUpdated] - errors: [HOST_ONLY_GOVERNANCE] - -# == ERC2771 / Forwarder == - -isTrustedForwarder: - # Check if an address is a governance-approved trusted forwarder for meta-transactions. - mutability: view - inputs: - - forwarder: address - outputs: - - bool - -versionRecipient: - # ERC2771 relay recipient version string. Returns "v1". - mutability: pure - outputs: - - string - -getERC2771Forwarder: - # Returns the ERC2771Forwarder contract used for ERC2771_FORWARD_CALL batch operations. - mutability: view - outputs: - - address - -getSimpleACL: - # Returns the SimpleACL contract used for Super App registration permissioning. - mutability: view - outputs: - - address - -# == Time == - -getNow: - # Returns block.timestamp. Useful for off-chain tooling and testing. - mutability: view - outputs: - - uint256 - -# == Protocol Constants == - -NON_UPGRADABLE_DEPLOYMENT: - # True if the Host was deployed in non-upgradable mode. - mutability: view - outputs: - - bool - -APP_WHITE_LISTING_ENABLED: - # True if Super App registration requires governance permission. - mutability: view - outputs: - - bool - -CALLBACK_GAS_LIMIT: - # Maximum gas forwarded to Super App callbacks. - mutability: view - outputs: - - uint64 - -MAX_APP_CALLBACK_LEVEL: - # Maximum callback nesting depth for composed Super Apps. Currently 1. - mutability: view - outputs: - - uint256 - -MAX_NUM_AGREEMENTS: - # Maximum number of agreement classes that can be registered. 256. - mutability: view - outputs: - - uint32 - -ACL_SUPERAPP_REGISTRATION_ROLE: - # keccak256("ACL_SUPERAPP_REGISTRATION_ROLE") — the SimpleACL role for app registration. - mutability: view - outputs: - - bytes32 - -SIMPLE_FORWARDER: - # Address of the SimpleForwarder used for SIMPLE_FORWARD_CALL batch operations. - mutability: view - outputs: - - address - -# == Events == - -events: - GovernanceReplaced: - data: - - oldGov: address - - newGov: address - - AgreementClassRegistered: - # agreementType is keccak256("org.superfluid-finance.agreements..") - data: - - agreementType: bytes32 - - code: address - - AgreementClassUpdated: - data: - - agreementType: bytes32 - - code: address - - SuperTokenFactoryUpdated: - data: - - newFactory: address - - SuperTokenLogicUpdated: - indexed: - - token: address - data: - - code: address - - PoolBeaconLogicUpdated: - indexed: - - beaconProxy: address - data: - - newBeaconLogic: address - - AppRegistered: - indexed: - - app: address - - Jail: - # Emitted when a Super App is jailed for violating protocol rules. - # See APP_RULE error below for reason code values. - indexed: - - app: address - data: - - reason: uint256 - - # Inherited events (from UUPSProxiable): - # CodeUpdated — emitted on proxy upgrade (uuid, codeAddress) - # Initialized — emitted on proxy initialization (version) - -# == Errors == - -errors: - # Governance - - HOST_ONLY_GOVERNANCE # caller is not the governance contract - - # Agreement whitelisting - - HOST_AGREEMENT_ALREADY_REGISTERED - - HOST_AGREEMENT_IS_NOT_REGISTERED - - HOST_MAX_256_AGREEMENTS # maximum agreement slots exhausted - - HOST_ONLY_LISTED_AGREEMENT # caller is not a registered agreement - - # Upgradability - - HOST_NON_UPGRADEABLE # deployment is non-upgradable - - HOST_CANNOT_DOWNGRADE_TO_NON_UPGRADEABLE - - # App registration - - HOST_MUST_BE_CONTRACT # app address has no code - - HOST_NO_APP_REGISTRATION_PERMISSION - - HOST_INVALID_CONFIG_WORD # invalid Super App manifest flags - - HOST_SUPER_APP_ALREADY_REGISTERED - - HOST_NOT_A_SUPER_APP # target is not a registered Super App - - HOST_SUPER_APP_IS_JAILED - - # App composition - - HOST_SENDER_IS_NOT_SUPER_APP - - HOST_RECEIVER_IS_NOT_SUPER_APP - - HOST_SOURCE_APP_NEEDS_HIGHER_APP_LEVEL - - # Context & call proxies - - HOST_NON_ZERO_LENGTH_PLACEHOLDER_CTX # placeholder ctx must be empty - - HOST_CALL_AGREEMENT_WITH_CTX_FROM_WRONG_ADDRESS # msg.sender != ctx.appAddress - - HOST_CALL_APP_ACTION_WITH_CTX_FROM_WRONG_ADDRESS # msg.sender != ctx.appAddress - - HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION # app action selector matches a callback - - # Batch call - - HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE - - # Callback execution - - HOST_NEED_MORE_GAS # insufficient gas for Super App callback - - # Super App rule violations (generic, carries a reason code) - # Reason codes (from SuperAppDefinitions.sol): - # 10 — NO_REVERT_ON_TERMINATION_CALLBACK (delete callback reverted) - # 11 — NO_CRITICAL_SENDER_ACCOUNT (callback made sender critical) - # 12 — NO_CRITICAL_RECEIVER_ACCOUNT (callback made receiver critical) - # 20 — CTX_IS_READONLY (state change in before-callback staticcall) - # 21 — CTX_IS_NOT_CLEAN (context not properly returned) - # 22 — CTX_IS_MALFORMATED (context bytes corrupted/fabricated) - # 30 — COMPOSITE_APP_IS_NOT_WHITELISTED (no allowCompositeApp) - # 31 — COMPOSITE_APP_IS_JAILED (downstream app is jailed) - # 40 — MAX_APP_LEVEL_REACHED (callback nesting exceeded limit) - - APP_RULE: - inputs: - - _code: uint256 - - # SafeCast (inherited from OpenZeppelin) - - SafeCastOverflowedUintDowncast: - inputs: - - bits: uint8 - - value: uint256 diff --git a/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml b/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml deleted file mode 100644 index 18edf9b..0000000 --- a/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - batchCall(tuple[]): 0x31131761 - forwardBatchCall(tuple[]): 0xbc5a2101 - callAgreement(): 0xb7a9d0f0 - callAppAction(): 0x40df3d29 - callAgreementWithContext(address,bytes,bytes,bytes): 0x4329d293 - callAppActionWithContext(address,bytes,bytes): 0xba48b5f8 - decodeCtx(bytes): 0x3f6c923a - isCtxValid(bytes): 0xbf428734 - registerApp(): 0x6d6c85c3 - registerAppWithKey(uint256,string): 0xbd1c448b - registerAppByFactory(address,uint256): 0xf3733052 - isApp(address): 0x3ca3ad4e - getAppCallbackLevel(address): 0x9378fa13 - getAppManifest(address): 0xf9f522f4 - isAppJailed(address): 0x6b4f3335 - allowCompositeApp(address): 0x57121e0c - isCompositeAppAllowed(address,address): 0xbb84cfa1 - callAppBeforeCallback(address,bytes,bool,bytes): 0x74041e02 - callAppAfterCallback(address,bytes,bool,bytes): 0x1e6d0a84 - appCallbackPush(bytes,address,uint256,int256,address): 0x768fabb0 - appCallbackPop(bytes,int256): 0x989b0c3e - ctxUseCredit(bytes,int256): 0x59a29141 - jailApp(bytes,address): 0x5578431e - replaceGovernance(address): 0x7283100c - registerAgreementClass(address): 0x15a024e1 - updateAgreementClass(address): 0x06cecba8 - isAgreementTypeListed(): 0xf8dba358 - isAgreementClassListed(address): 0x8ca48484 - getAgreementClass(bytes32): 0xb6d200de - mapAgreementClasses(uint256): 0xc56a069d - addToAgreementClassesBitmap(uint256,bytes32): 0xbced3ddc - removeFromAgreementClassesBitmap(uint256,bytes32): 0xa5dbbbcd - updateSuperTokenFactory(address): 0x54fbc493 - updateSuperTokenLogic(address): 0x787afde7 - changeSuperTokenAdmin(address,address): 0x0c565075 - updatePoolBeaconLogic(address): 0x2f89bf89 - isTrustedForwarder(address): 0x572b6c05 - SIMPLE_FORWARDER(): 0xf85263b9 -events: - GovernanceReplaced(address,address): 0x13abda02e63c790d0e2818b251282cfe5cbe0a8abd69c54bf5d2260c0907bd2e - AgreementClassRegistered(bytes32,address): 0x878135063a6cfb3bc333e534b1fdc83f4f12221cad6705c31c0567048a8bd3d1 - AgreementClassUpdated(bytes32,address): 0x9279aa773f2b588996032d8de89911555039f28b13a11a7c17074330bc082d9a - SuperTokenFactoryUpdated(address): 0xce13a9895a1719ad4493b2ac1a9bfb36070566161abab408e7ecbe586da8d499 - SuperTokenLogicUpdated(address,address): 0x840acbd291b38534819f47f875839277e502f40e1c7bfea2c5fc2c8017442cd3 - PoolBeaconLogicUpdated(address,address): 0x052cea8931962dd445ef48b0b998d3056bd0705f437087d60fe3c46a3fa09e1f - AppRegistered(address): 0x0d540ad8f39e07d19909687352b9fa017405d93c91a6760981fbae9cf28bfef7 - Jail(address,uint256): 0xbe3aa33bd245135e4e26b223d79d14ea479a47bff09f2b03c53838af1edbb14b - # Inherited events (from UUPSProxiable)(): 0x3a1d0f622cf5be33dc2558b359f025fbeca1d6fb271fb70452a02b0ec2666ce8 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml deleted file mode 100644 index 75e5e43..0000000 --- a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml +++ /dev/null @@ -1,305 +0,0 @@ -# UBISchemeV2 — dynamic daily UBI pool with per-root claiming and cycle accounting -# Claim uses Identity getWhitelistedRoot(msg.sender); transfers use the root as claim key. - -meta: - name: UBISchemeV2 - version: v2 - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/ubi/UBISchemeV2.sol - inherits: [DAOUpgradeableContract] - note: > - Pool sizing uses cycleLength, reserveFactor, minActiveUsers, and on-chain G$ - balance. claim() reverts if Identity root is zero. Internal accounting keys - claims by whitelisted root, payouts go to msg.sender. - deployments: - mainnet: - production: - UBIScheme: - networkId: 122 - address: "0xd253A5203817225e9768C05E5996d642fb96bA86" - creationBlock: 15747401 - production-celo: - UBIScheme: - networkId: 42220 - address: "0x43d72Ff17701B2DA814620735C39C620Ce0ea4A1" - creationBlock: 18006679 - production-xdc: - UBIScheme: - networkId: 50 - address: "0x22867567E2D80f2049200E25C6F31CB6Ec2F0faf" - creationBlock: 95249624 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/ubischeme - - references/guides/claim.md - -getClaimerCount: - mutability: view - inputs: - - day: uint256 - outputs: - - count: uint256 - -getClaimAmount: - notes: - - "Returns aggregate amount claimed for the provided day index." - mutability: view - inputs: - - day: uint256 - outputs: - - amount: uint256 - -getDailyStats: - notes: - - "Returns (claimer count, total claimed amount) for the calendar day derived from periodStart." - mutability: view - inputs: [] - outputs: - - claimerCount: uint256 - - claimAmount: uint256 - -setCycleLength: - mutability: nonpayable - access: avatar - inputs: - - _newLength: uint256 - outputs: [] - emits: [CycleLengthSet] - errors: [CYCLE_TOO_SHORT, ONLY_AVATAR] - -setDay: - notes: - - "Anyone may advance currentDay when wall-clock day crosses the next boundary." - mutability: nonpayable - inputs: [] - outputs: [] - emits: [DaySet] - -hasClaimed: - mutability: view - inputs: - - account: address - outputs: - - claimed: bool - -isNotNewUser: - mutability: view - inputs: - - _account: address - outputs: - - isReturning: bool - -estimateNextDailyUBI: - notes: - - "View-only projection when dailyUbi not yet fixed for the current wall day." - mutability: view - inputs: [] - outputs: - - projected: uint256 - -"checkEntitlement()": - notes: - - "Uses msg.sender as member; resolves Identity root before hasClaimed check." - mutability: view - inputs: [] - outputs: - - amount: uint256 - -"checkEntitlement(address)": - notes: - - "Checks claimable amount for the provided member using that account's whitelisted root." - mutability: view - inputs: - - _member: address - outputs: - - amount: uint256 - -claim: - notes: - - "Resolves whitelistedRoot = Identity.getWhitelistedRoot(msg.sender); reverts if zero." - - "_claim keys bookkeeping by root but pays msg.sender." - - "May notify ClaimersDistribution at GDAO_CLAIMERS if configured." - - "On success emits UBIClaimed; distributionFormula may emit UBICalculated, UBICycleCalculated, DaySet, WithdrawFromDao." - mutability: nonpayable - access: anyone - inputs: [] - outputs: - - didClaim: bool - errors: - - NOT_IN_PERIOD_OR_PAUSED - - NOT_WHITELISTED - - CLAIM_TRANSFER_FAILED - - DAO_TRANSFER_FAILED - -setShouldWithdrawFromDAO: - mutability: nonpayable - access: avatar - inputs: - - _shouldWithdraw: bool - outputs: [] - emits: [ShouldWithdrawFromDAOSet] - errors: [ONLY_AVATAR] - -pause: - mutability: nonpayable - access: avatar - inputs: - - _pause: bool - outputs: [] - errors: [ONLY_AVATAR] - -setNewClaimersReserveFactor: - notes: - - "Adjusts reserveFactor used by distribution formula to reserve part of pool for new claimers." - mutability: nonpayable - access: avatar - inputs: - - _reserveFactor: uint32 - outputs: [] - errors: [ONLY_AVATAR] - -withdraw: - notes: - - "Avatar-controlled treasury action transferring G$ from scheme balance to recipient." - mutability: nonpayable - access: avatar - inputs: - - _amount: uint256 - - _recipient: address - outputs: [] - errors: - - ONLY_AVATAR - - WITHDRAW_FAILED - -lastClaimed: - notes: - - "Returns last claim timestamp keyed by account root logic used by hasClaimed checks." - mutability: view - inputs: - - account: address - outputs: - - ts: uint256 - -currentDay: - mutability: view - inputs: [] - outputs: - - day: uint256 - -periodStart: - mutability: view - inputs: [] - outputs: - - ts: uint256 - -dailyUbi: - mutability: view - inputs: [] - outputs: - - amount: uint256 - -lastWithdrawDay: - mutability: view - inputs: [] - outputs: - - day: uint256 - -shouldWithdrawFromDAO: - mutability: view - inputs: [] - outputs: - - flag: bool - -cycleLength: - mutability: view - inputs: [] - outputs: - - days: uint256 - -dailyCyclePool: - mutability: view - inputs: [] - outputs: - - pool: uint256 - -startOfCycle: - mutability: view - inputs: [] - outputs: - - ts: uint256 - -currentCycleLength: - mutability: view - inputs: [] - outputs: - - length: uint256 - -minActiveUsers: - mutability: view - inputs: [] - outputs: - - n: uint256 - -totalClaimsPerUser: - mutability: view - inputs: - - account: address - outputs: - - count: uint256 - -reserveFactor: - mutability: view - inputs: [] - outputs: - - bps: uint32 - -paused: - mutability: view - inputs: [] - outputs: - - isPaused: bool - -events: - WithdrawFromDao: - indexed: [] - data: - - prevBalance: uint256 - - newBalance: uint256 - UBICalculated: - indexed: [] - data: - - day: uint256 - - dailyUbi: uint256 - - blockNumber: uint256 - UBICycleCalculated: - indexed: [] - data: - - day: uint256 - - pool: uint256 - - cycleLength: uint256 - - dailyUBIPool: uint256 - UBIClaimed: - indexed: - - claimer: address - data: - - amount: uint256 - CycleLengthSet: - indexed: [] - data: - - newCycleLength: uint256 - DaySet: - indexed: [] - data: - - newDay: uint256 - ShouldWithdrawFromDAOSet: - indexed: [] - data: - - ShouldWithdrawFromDAO: bool - -errors: - NOT_IN_PERIOD_OR_PAUSED: "not in periodStarted or paused — requireStarted modifier." - CYCLE_TOO_SHORT: "cycle must be at least 1 day long" - DAO_TRANSFER_FAILED: "DAO transfer has failed — _withdrawFromDao balance check." - NOT_WHITELISTED: "UBIScheme: not whitelisted — claim()" - CLAIM_TRANSFER_FAILED: "claim transfer failed — G$ transfer in _transferTokens." - WITHDRAW_FAILED: "withdraw failed — avatar withdraw." - ONLY_AVATAR: "Inherited avatar-only guard on DAOUpgradeable paths." diff --git a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml deleted file mode 100644 index 666b89d..0000000 --- a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - getClaimerCount(uint256): 0xc7a76adf - getClaimAmount(uint256): 0xcef63600 - getDailyStats(): 0x069786ea - setCycleLength(uint256): 0x3d84ceca - setDay(): 0xdddc3616 - hasClaimed(address): 0x73b2e80e - isNotNewUser(address): 0xa21f698a - estimateNextDailyUBI(): 0xc7713870 - checkEntitlement(): 0x98d6621b - checkEntitlement(address): 0x1a787f2e - claim(): 0x4e71d92d - setShouldWithdrawFromDAO(bool): 0xde1de3a0 - pause(bool): 0x02329a29 - setNewClaimersReserveFactor(uint32): 0x414089be - withdraw(uint256,address): 0x00f714ce - lastClaimed(address): 0x013eba92 - currentDay(): 0x5c9302c9 - periodStart(): 0xeda4e6d6 - dailyUbi(): 0x1d8f5ea9 - lastWithdrawDay(): 0xd7c4cbb8 - shouldWithdrawFromDAO(): 0x456ac1c2 - cycleLength(): 0xeac471a0 - dailyCyclePool(): 0x9dc2c033 - startOfCycle(): 0xba075410 - currentCycleLength(): 0x741470ac - minActiveUsers(): 0x37658574 - totalClaimsPerUser(address): 0xcc054dfc - reserveFactor(): 0x4322b714 - paused(): 0x5c975abb -events: - WithdrawFromDao(uint256,uint256): 0x3107ec7eaa50b775d2486c7a394472235804b6fe1c0d4b7bd1d79b09df60f2ba - UBICalculated(uint256,uint256,uint256): 0x836fa39995340265746dfe9587d9fe5c5de35b7bce778afd9b124ce1cfeafdc4 - UBICycleCalculated(uint256,uint256,uint256,uint256): 0x83e0d535b9e84324e0a25922406398d6ff5f96d0c686204ee490e16d7670566f - UBIClaimed(address,uint256): 0x89ed24731df6b066e4c5186901fffdba18cd9a10f07494aff900bdee260d1304 - CycleLengthSet(uint256): 0xa61e6cca2c12e2a0a493683acfe95b034f0f50d793434f4dfe3ba06ea201f344 - DaySet(uint256): 0x67eb03bd555181f9dd23f546e4331ddfb8b4a7d0c8d261ba44e037f30ce894ea - ShouldWithdrawFromDAOSet(bool): 0x6cd9a0fd2e006be39a9918bf56c85cae1d4f4599474483ff18cb93355ebaaf8e -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md b/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md deleted file mode 100644 index 41e2bae..0000000 --- a/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md +++ /dev/null @@ -1,93 +0,0 @@ -# Reading the Rich ABI YAMLs - -Each YAML is a self-contained contract reference. Here's how to parse them. - -## Root structure - -``` -# Header comment — contract name, description, key notes -meta: # name, version, source, implements, inherits, deployments, - # deploymentCreationBlocks (optional), notes -# == Section == # Grouped functions (these are the core content) -events: # All events the contract emits -errors: # Complete error index -``` - -Three root keys are reserved: `meta`, `events`, `errors`. Every other -root-level key is a **function**. - -## Function entries - -```yaml -createFlow: - # Description of what the function does. - notes: - - "Gotcha: Non-obvious behavior or edge cases listed here as structured data." - mutability: nonpayable # view | pure | nonpayable | payable - access: sender | operator # who can call (omitted for view/pure) - inputs: - - token: address - - receiver: address - - flowRate: int96 # inline comments for non-obvious params - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] # ordered by emission sequence - errors: [CFA_FLOW_ALREADY_EXISTS, CFA_INVALID_FLOW_RATE] # ordered by check sequence -``` - -Fields appear in this order: description comment, `notes`, `mutability`, -`access`, `inputs`, `outputs`, `emits`, `errors`. All are omitted when not -applicable. - -## Key conventions - -- **`ctx: bytes` parameter** = function is called through the Host - (`callAgreement` / `batchCall`), never directly. -- **`access` labels**: `anyone`, `host`, `self`, `admin`, `governance`, - `sender`, `receiver`, `operator`, `manager`, `pic`, `agreement`, - `trusted-forwarder`, `factory`, `super-app`. Combine with `|`. Conditional: - `anyone(if-critical-or-jailed)`. -- **`emits` and `errors` ordering** carries meaning: matches execution flow, - not alphabetical. First errors in the list are the most likely causes. -- **`notes:` field** on functions (and `meta.notes:` at contract level) - lists non-obvious behavior, common mistakes, or edge cases. Always read - these carefully. -- **`meta.source`** is an array of raw GitHub URLs to the Solidity source files - (implementation, interface, base — filenames are self-documenting). -- **`meta.deployments`** has per-network addresses split into `mainnet` and - `testnet` subgroups. -- **`creationBlock`**: decimal block where that **`address`** first has code; - put it on the same object as **`networkId`** / **`address`**, immediately after **`address`**. -- **`meta.deploymentCreationBlocks`**: same shape as **`meta.deployments`**, for - deployments stored as plain address strings; leaves are decimal block numbers. - -## Events section - -```yaml -events: - FlowUpdated: - indexed: # log topics (filterable) - - token: address - - sender: address - data: # log payload - - flowRate: int96 -``` - -## Errors section - -```yaml -errors: - # -- Category -- - - SIMPLE_ERROR # description - - PARAMETERIZED_ERROR: # errors with diagnostic data - inputs: - - value: uint256 -``` - -## Selector sidecar files - -Every `Foo.abi.yaml` has a companion `Foo.selectors.yaml` in the same -directory. These contain computed hex selectors (function 4-byte, error -4-byte, event 32-byte topic0) with full Solidity signatures for -verification. Generated by `scripts/selectors.mjs`. diff --git a/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md b/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md deleted file mode 100644 index 12b1899..0000000 --- a/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md +++ /dev/null @@ -1,42 +0,0 @@ -# Faucet flows (user-facing explanation) - -This note explains the Faucet in plain language: what it does for users, why a top-up can fail, and what limits exist. - -The on-chain Faucet contract is used to add a small amount of native gas token to a wallet so the user can pay transaction fees. -Reference implementation: [`contracts/fuseFaucet/Faucet.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/fuseFaucet/Faucet.sol). -Addresses per chain: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example `Faucet` under `production`, `production-celo`, `production-xdc`). - -## What this means for users - -- If your wallet is eligible, Faucet can send a small gas top-up. -- Eligibility usually depends on identity status and anti-abuse limits. -- This is a support mechanism for transaction fees, not a general transfer or swap service. - -## Main actions (in user language) - -- **Top up wallet** (`topWallet`) - Attempts to send gas to the target wallet after checks pass. - -- **Check eligibility first** (`canTop`) - Fast pre-check to see if top-up is currently allowed. - -- **Estimate top-up amount** (`getToppingAmount`) - Shows the amount Faucet would try to send right now. - -## Why a top-up may fail - -- You are not currently authorized by identity rules. -- Daily limit reached. -- Weekly limit reached. -- Wallet is temporarily banned. -- Wallet is too new for current policy. -- Calculated top-up is below minimum threshold. - -## Important safety note - -- The `onTokenTransfer` path includes a swap-like mechanism and is not meant as a normal user swap route. -- It does not enforce slippage protection in the same way users expect from a dedicated swap UI. - -## For developers and agents - -Use `references/guides/faucet.md` for step-by-step execution flow and deterministic preflight calls. diff --git a/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md b/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md deleted file mode 100644 index 261c2d7..0000000 --- a/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md +++ /dev/null @@ -1,38 +0,0 @@ -# Why Fuse to CELO staking migration uses a staged backend flow - -This explains why the migration is split into allowance detection, unstake, bridge, and destination re-stake instead of a single transaction. In this context, Fuse `GovernanceStakingV2` is the old/source staking contract and Celo **`GooddollarSavingsStream`** is the destination staking contract ([source](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol), [0x059ee811414230d1Fb157878D2b491240F4D8d3B](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B)). - -## Why this cannot be one-chain atomic - -Fuse governance staking and CELO destination savings live on different chains, so the system cannot atomically close and reopen stake in one EVM transaction. Cross-chain migration is asynchronous by design and must tolerate timing gaps between source completion and destination finalization. - -## Why user allowance is first - -The migration flow assumes a backend-operated execution wallet. If that wallet needs to pull or operate on user funds in the source path, user approval must exist first. Without allowance, all downstream steps fail, so approval state is the earliest hard gate. - -## Why unstake is separated from bridge - -The source staking position is the canonical balance record on Fuse. The migration must first materialize transferable G$ by closing or reducing stake, then bridge only the confirmed unlocked amount. Bridging before final unstake confirmation introduces mismatch risk. - -## Why bridge finalization must be explicit - -Bridge transfers are eventually consistent across chains. The destination stake step must only run after the bridged G$ is confirmed on CELO. This avoids phantom staking attempts and preserves deterministic accounting. - -## Why destination uses `stakeFor` - -Ubeswap `GooddollarSavingsStream` on Celo supports `stakeFor(amount, recipient)`, which allows the backend execution wallet to stake on behalf of the user after bridge finalization. This fits migration operations where custody is temporary during the transfer window. - -## Main operational risks - -- partial migration from source unstake or bridge limit constraints -- stuck-in-transit bridge messages delaying destination staking -- stale assumptions about contract addresses across networks -- reward expectation mismatch when moving from Fuse governance staking mechanics to CELO savings mechanics - -## Contract/source map - -- Fuse staking family (GoodProtocol): [`GovernanceStaking.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/governance/GovernanceStaking.sol) -- Fuse deployment mapping: [`deployment.json`](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`production.GovernanceStakingV2`) -- Celo savings (Ubeswap): [`GooddollarSavingsStream.sol`](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol) -- Ubeswap contracts repository: [Ubeswap/gooddollar-contracts](https://github.com/Ubeswap/gooddollar-contracts) -- Bridge normalization for LZ quotes: [`BridgeHelperLibrary.normalizeFromTokenTo18Decimals`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol) (off-chain LayerZero fee estimation must match this; `canBridge` and `bridgeToWithLz` use the raw burn amount on the source chain) diff --git a/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md b/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md deleted file mode 100644 index 754a3a0..0000000 --- a/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md +++ /dev/null @@ -1,37 +0,0 @@ -# GoodDAO and DAOStack surface - -GoodProtocol’s on-chain **governance shell** is largely **DAOStack-shaped**: an **Avatar** holds protocol assets and reputation context; a **Controller** registers **schemes** and routes privileged calls. GoodDocs summarizes DAO-facing roles; **Avatar**, **Controller**, and other DAO contract addresses live only in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). Implementation follows [DAOStack Arc](https://github.com/daostack/arc) patterns. - -## Core interfaces (GoodProtocol) - -[`DAOStackInterfaces.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/DAOStackInterfaces.sol) defines the pieces most integrations touch: - -### Avatar - -- **`nativeToken()`** — often the G$ token address for the deployment. -- **`nativeReputation()`** — **`GReputation`** token for voting weight (see `GReputation` in `deployment.json`). -- **`owner()`** — owner of the Avatar, typically the **`Controller`** contract. - -### Controller - -- **`avatar()`** — address of the Avatar contract. -- **`registerScheme` / `unregisterScheme` / `unregisterSelf` / `isSchemeRegistered` / `getSchemePermissions`** — scheme lifecycle and permission bitmask per scheme+avatar. -- **`genericCall(contract, data, avatar, value)`** — executes arbitrary calls **as the avatar** (used heavily by DAO-backed contracts to move tokens or call NameService). -- **`mintTokens`**, **`externalTokenTransfer`**, **`sendEther`** — treasury-style operations through the controller/avatar. - -## How GoodProtocol contracts use it - -- **`DAOUpgradeableContract`** / **`DAOContract`** descendants resolve **`dao`** (Controller) and **`avatar`** from **NameService** keys such as **`CONTROLLER`** and **`AVATAR`** (also written during **NameService.initialize**). -- **Avatar-gated writes** (for example **NameService.setAddress**, **UBISchemeV2** admin functions, **IdentityV3** after `initDAO`) require **`msg.sender == dao.avatar()`** (or equivalent role), not EOAs. -- Schemes that upgrade themselves (for example staking **`upgrade()`**) use **`dao.genericCall`** and **`unregisterSelf`** against the avatar. - -## What agents should do - -1. Treat **DAO calls** as **governance-only** unless the user explicitly controls the avatar or a registered scheme. -2. For **read-only** work, use **NameService** and per-contract **view** functions; use **Controller.isSchemeRegistered** when validating that a target contract is an approved scheme (for example staking migrations). -3. Never fabricate **Avatar** or **Controller** addresses — use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or NameService. - -## References - -- [Core contracts — DAO contracts](https://docs.gooddollar.org/for-developers/core-contracts) (narrative only; addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only) -- DAOStack Arc controller and avatar concepts in the upstream repo linked from GoodDocs. diff --git a/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md b/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md deleted file mode 100644 index 96d321a..0000000 --- a/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md +++ /dev/null @@ -1,68 +0,0 @@ -# How UBI is minted - -This document aligns agent explanations with [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and GoodDocs component pages; **contract addresses** come only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). - -## Monetary creation (protocol level) - -- New G$ is created in connection with reserve mechanics: purchases into the reserve and reserve-side parameters (including reserve ratio) influence how much G$ can be issued while maintaining backing (see sustainability and issuance sections in GoodDocs). -- Selling G$ back to the reserve burns supply in that model. -- G$ that the protocol creates is allocated across UBI, savings incentives, treasury, and ecosystem uses per the distribution section of GoodDocs. - -## Where G$ is actually created - -Creation happens at the G$ token `mint(...)` call site, not inside `UBIScheme.claim()`. - -Current implementation uses the Mento-core expansion path: - -- `GoodDollarExpansionController` mints and routes to distribution helper. - -**DistributionHelper recipients** decide how much eventually lands in the UBI pool. - -## Claim vs reserve minting (important distinction) - -- **Reserve path:** Buying G$ through the reserve-backed AMM (or related Mento rails) is where mint and burn tied to the reserve model most directly apply at the token level. -- **Daily UBI `claim()`:** On `UBISchemeV2`, a successful claim typically **transfers G$ from the scheme contract’s balance** to the user (`token.transfer` in `_transferTokens`). The scheme may be **refilled** from the DAO avatar via internal `_withdrawFromDao` when configured, not necessarily minting in the same transaction as `claim()`. So describe user-facing UBI as **receipt from the UBI scheme balance**; reserve **minting** is the macro story, **transfer** is the usual claim-time mechanism. - -## Mento-core expansion flow (detailed) - -This is the detailed path for modern reserve-ratio-aware expansion. - -1. A caller triggers `mintUBIFromExpansion(exchangeId)` on `GoodDollarExpansionController`. -2. Expansion is time-gated by config (`expansionFrequency`, `lastExpansion`), so it does not run every block. -3. Controller computes a reserve-ratio scalar (effectively compounding `(1 - expansionRate)` for elapsed periods). -4. Controller calls `GoodDollarExchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar)`. -5. Exchange provider updates exchange state (including reserve-ratio math) and returns `amountToMint`. -6. Controller mints G$ to `distributionHelper`. -7. Controller triggers distribution (`onDistribution`) so recipients (including UBIScheme when configured) receive allocation. - -Also in `GoodDollarExpansionController`: - -- `mintUBIFromInterest(exchangeId, reserveInterest)` -- `mintUBIFromReserveBalance(exchangeId)` - -These are additional funding paths that mint to distribution helper as part of reserve-driven policy. - -## Reserve ratio, expansion, and risk - -- Practical reserve-ratio intuition: collateral backing strength per unit of G$ supply. -- Lower reserve ratio means weaker backing and higher risk when adding new supply. -- Expansion uses reserve-ratio-aware math instead of blind fixed minting, but aggressive params can still increase sell pressure. -- Key policy levers are expansion rate and expansion frequency (plus caller cadence/automation quality). - -## What claimers experience - -- Verified users receive daily UBI from a pool split among those who claim in each period (GoodDocs). -- The user-facing transaction is a UBIScheme-style `claim` on chains where it is deployed; ABI and version follow your deployment. - -## On-chain components (typical) - -- Identity system for verification and whitelist roots. -- UBIScheme (or successor) for entitlement and claim execution. -- G$ token: value reaches users via **transfer** from scheme balance and/or broader minting economics from the reserve side depending on which action you analyze. -- DistributionHelper: bridge layer between mint source and recipient buckets (UBI, others). - -## Agent guidance - -- Use GoodDocs for macro issuance and allocation; use UBIScheme + token transfer behavior for **claim** explanations. -- Use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) for contract addresses instead of guessing. -- When users ask "why no UBI funding today", check whether mint functions were executed, then verify DistributionHelper recipient config and UBIScheme balance. diff --git a/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md b/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md deleted file mode 100644 index 267fa74..0000000 --- a/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md +++ /dev/null @@ -1,58 +0,0 @@ -# Inviter and invitee reward model - -This explains why invite rewards sometimes work and sometimes fail, in user-facing terms. - -Invite rewards are handled by `InvitesV2`, but eligibility depends heavily on the identity system (`Identity`, often `IdentityV4`). In practice, most confusing cases are caused by whitelist or reverification state, not by the invite contract itself. - -## How the reward model works - -There are two separate moments: - -- `join`: the invitee registers with an invite code. -- bounty payout: the contract later checks if this invitee is eligible and pays inviter and invitee when rules pass. - -So joining does not guarantee immediate payout. Payout depends on current eligibility at claim time. - -## Why whitelist status is the main gate - -For bounty eligibility, `InvitesV2` checks whitelist state through Identity. - -The important behavior is: - -- A user can still have status `1` in identity storage but fail `isWhitelisted(...)` if reverification is due. -- When reverification is due, bounty checks fail until an admin refreshes authentication(Face Verification). -- Connected-wallet setups can still fail if the specific address used in invite flow does not pass the whitelist check expected by the contract path. - -## Why reverification blocks rewards - -Reverification cadence is defined in `IdentityV4` with day-based options (`reverifyDaysOptions`) and per-user progression (`authCount`). - -When too many days pass since the last authentication for that user’s current step: - -- `shouldReverify(...)` becomes true -- `isWhitelisted(...)` becomes false for bounty gating -- `canCollectBountyFor(...)` fails until authentication is refreshed - -This is why teams may see users who were once valid but are currently not eligible for invite rewards. - -## Common reasons a bounty is not paid - -- Invitee or inviter is not currently whitelisted. -- Reverification is due for invitee or inviter. -- `minimumClaims` or `minimumDays` thresholds are not met yet. -- Bounty was already paid or was zero at join time. -- Contract is inactive, or identity-chain checks do not match the active chain. -- Invite code or join state is invalid (duplicate code, self-invite, already joined). - -## What to measure for analytics - -- Historical pass: account was authenticated at least once (`lastAuthenticated > 0`). -- Current eligibility: account is currently whitelist-valid (`isWhitelisted`/non-zero root, depending on query design). - -Do not treat these as the same metric. Historical pass explains past onboarding success; current eligibility explains current payout success. - -## Contract sources - -- Invite contract: [`InvitesV2.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/invite/InvitesV2.sol) -- Identity contract: [`IdentityV4.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/identity/IdentityV4.sol) -- Deployment addresses: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) diff --git a/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md b/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md deleted file mode 100644 index e46db2a..0000000 --- a/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md +++ /dev/null @@ -1,27 +0,0 @@ -# Mento and reserve economics - -This note ties **macro G$ economics** (GoodDocs) to **Mento trading surfaces** (broker, reserve, expansion) that agents integrate on-chain. - -## Protocol-level story (GoodDocs) - -- G$ is **reserve-backed**; issuance and price discovery follow an **augmented bonding curve** (Bancor-style dynamics) described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). -- **Buying** from the reserve side increases reserve assets and can support **new G$ supply** within reserve rules; **selling back** burns G$ and returns collateral, with parameters governed by **GoodDAO**. -- Distribution of newly created G$ spans UBI, savings incentives, treasury, and ecosystem allocations (same doc), but execution timing depends on the mint path and distribution trigger used by the deployment flow. - -## User-facing buy and sell (historical vs current) - -- [Buy and Sell G$](https://docs.gooddollar.org/user-guides) describes reserve interaction, fees (including exit contribution on some paths), and older explorer flows (GoodMarketMaker / exchangeHelper on Ethereum testnets). Treat that page as **product narrative**; **live contract addresses** must come only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). - -## Mento stack (agent integration) - -On networks where GoodDollar uses Mento (see `MentoBroker` and related keys in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) for your environment): - -- **`IBroker`** (implementation in [mento-core `Broker.sol`](https://github.com/mento-org/mento-core)) is the usual **swap entrypoint**: `getAmountIn` / `getAmountOut`, `swapIn` (exact in), `swapOut` (exact out), plus **trading limits** state per exchange id and token. -- **Reserve** holds collateral; **exchange provider** contracts price trades against the reserve; **GoodDollarExpansionController** and related interfaces in GoodProtocol’s [`MentoInterfaces.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/MentoInterfaces.sol) describe expansion and avatar wiring for governance-facing changes. -- Agents should not hardcode **exchangeId** or provider addresses: read them from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or discover via `getExchangeProviders()` after confirming the broker address for the chain from that file. - -## Agent guidance - -1. Explain **macro** supply and reserve behavior with GoodDocs language. -2. Execute **swaps** with broker quotes, slippage bounds, and allowances per `references/guides/swap.md`. -3. On revert, distinguish **slippage / limit** failures (broker) from **reserve liquidity** messages (see `MentoBroker.abi.yaml` error map and mento-core source). diff --git a/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md b/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md deleted file mode 100644 index b59e5c9..0000000 --- a/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md +++ /dev/null @@ -1,69 +0,0 @@ -# On- and off-ramp service via stable token swap - -Use this note for the service pattern where ramp providers do not list G$ directly. The practical path is: ramp in/out with a listed stable token (for example cUSD), then swap between stable and G$ on-chain. - -## Why this is required - -- Most on-/off-ramp providers list mainstream stable tokens, not G$. -- Service needs a bridge asset for fiat rails. -- Stable token becomes the integration point with ramp providers, while G$ remains the in-app asset. - -## On-chain source of truth - -- Solidity: [`contracts/utils/BuyGDClone.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/utils/BuyGDClone.sol) -- Components: - - `BuyGDCloneFactory` - - `BuyGDCloneV2` - - `DonateGDClone` -- Deployments: [releases/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) - -## Service architecture - -The factory deploys EIP-1167 minimal clones and wires swap infrastructure (router, oracle, quoter, optional Mento broker configuration, G$ token, stable token). - -Swap execution is **dual-path**: - -- **Uniswap-style route** (router/quoter path) -- **Mento-based route** (broker/exchange-provider path) - -For each swap request, the service compares quoted outputs and selects the route with the **larger `amountOut`** (best execution for the same input amount), then enforces `minAmount` guard on the selected path. - -Each user gets a deterministic clone address from owner-based salt: - -- `predict(owner)` for buy clone -- `predictDonation(owner, donor)` for donation clone - -This is why clone-per-user design is used: - -- predictable per-user addresses for audit and routing -- isolated execution context -- cheaper deployment than full contract instances - -## Execution surface (conceptual) - -The operational surface is intentionally small and deterministic: - -- `create(owner)` -- `createAndSwap(owner, minAmount)` -- `predict(owner)` -- `createDonation`, `createDonationAndSwap`, `predictDonation` - -This keeps on-/off-ramp architecture auditable and predictable across users. - -## Risks - -- Wrong factory or wrong chain causes permanent fund loss risk. -- Stale router/oracle/mento config can fail swap or produce bad execution. -- Missing `minAmount` protection increases slippage risk. -- Quote source mismatch or stale quotes across Uniswap/Mento can pick a suboptimal route if not refreshed just before execution. - -## Boundary note - -This file explains **why** this architecture exists for ramp services and why per-user clones matter. -For step-by-step service execution flow, use `references/guides/on-off-ramp.md`. - -## Cross-reference - -- User narrative: [Buy and Sell G$](https://docs.gooddollar.org/user-guides) -- Token integration details: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) -- Broker ABI: `references/contracts/MentoBroker.abi.yaml` diff --git a/.agents/skills/gooddollar/references/guides/bridge.md b/.agents/skills/gooddollar/references/guides/bridge.md deleted file mode 100644 index 71fd6e8..0000000 --- a/.agents/skills/gooddollar/references/guides/bridge.md +++ /dev/null @@ -1,187 +0,0 @@ -# Bridge guide - -Use for moving G$ across supported networks with deployment-specific bridge contracts. - -Primary local ABI reference for MessagePassingBridge flow: - -- `references/contracts/MessagePassingBridge.abi.yaml` - -## GoodDocs alignment - -- User flow and high-level behavior: [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars). -- Resolve supported bridge contract addresses per chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example `MpbBridge` under `production`, `production-celo`, `production-xdc`). Use [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars) for user-facing flow and troubleshooting, not for addresses. - -## Goal - -Bridge with deterministic pre-checks: bridge support, allowance, amount, cross-chain transport fee, and delivered G$ after destination **amount** limits and protocol fee. - -## Required inputs - -- source and destination chain metadata -- bridge contract address for source chain -- source G$ token address -- amount in source token decimals -- signer and rpc url - -## Execution flow - -1. Resolve source bridge and token addresses for the network pair. -2. Run bridge eligibility checks for sender and amount via `canBridge(from, amount)` on the **source** bridge (same contract you call for `bridgeToWithLz` / `bridgeToWithAxelar`). Outbound burn does not invoke `canBridge` inside `_bridgeTo`; destination mint still enforces **amount** limits (see **Bridge amount limit context**). -3. Read allowance and approve bridge spender when required. -4. Resolve transport mode (`LZ` or `AXELAR`) and estimate required native fee. -5. Send bridge transaction with nonzero `msg.value` and explicit transport method. -6. Return tx hash and normalized bridge parameters. - -## Bridge fee context - -Two different costs show up on `MessagePassingBridge`; do not conflate them. - -**1. Cross-chain transport fee (native gas token on the source chain)** -Paid as **`msg.value`** on the outbound call. The contract reverts with **`MISSING_FEE`** if `msg.value` is zero. On the **LayerZero** path the contract compares your `msg.value` to **`estimateSendFee`** and reverts **`LZ_FEE(required, sent)`** if it is too low. Use the same **normalized** amount for `estimateSendFee` as the contract uses internally (see the next section). On the **Axelar** path you still attach native value for Gas Service / execution; there is no single `estimateSendFee` analogue in the snippet—follow Envio/Axelar docs or simulate the exact call for production amounts. - -**2. Protocol fee on minted G$ (destination chain, basis points)** -When the message is executed on the **destination** chain, the bridge applies **`bridgeFees`** (min / max / fee bps via `setBridgeFees`) and mints the recipient **minus** that fee; the fee portion is minted to **`feeRecipient`** when it is non-zero (see `bridgeFees()`, `feeRecipient`, and `_takeFee` / `ExecutedTransfer` in `references/contracts/MessagePassingBridge.abi.yaml`). This is **not** the LayerZero relayer fee; it is a separate cut on the **token amount** delivered on arrival. - -**3. Optional OFT / LayerZero token-adapter path** -If the flow uses the GoodDollar OFT-style adapter instead of `MessagePassingBridge`, fee quoting follows **`quoteSend`** / **`MessagingFee`** on that contract; see `references/contracts/GoodDollarOFTAdapter.abi.yaml`. - -## Bridge amount limit context - -**Bridge limit** means **bridge amount limit**: policy on **how much G$** (token volume) may move—**`minAmount`**, per-transfer cap, per-account daily cap, and aggregate daily cap—plus **`onlyWhitelisted`**. It does **not** mean the cross-chain **native** transport fee (`msg.value`, **`LZ_FEE`**), and it does **not** mean the **destination mint fee** in **`bridgeFees`** (bps); those are covered under **Bridge fee context**. - -Amount caps and counters are **per bridge deployment**: read the **source** contract for outbound **amount** policy and usage meters tied to the burn, and the **destination** contract for **`_enforceLimits`** at inbound mint completion; do not assume identical **`bridgeLimits`** across chains. - -**1. Amount caps and usage meters** -**`bridgeLimits()`** exposes **`dailyLimit`**, **`txLimit`**, **`accountDailyLimit`**, **`minAmount`**, and **`onlyWhitelisted`**. Compare those caps to **`bridgeDailyLimit()`** (aggregate **`bridged24Hours`** and **`lastTransferReset`**) and **`accountsDailyLimit(account)`** (same fields per sender). Updates use **`setBridgeLimits`** (access per the ABI). Field-level notes and accessors live in `references/contracts/MessagePassingBridge.abi.yaml`. - -**2. Source preflight vs destination enforcement** -**`canBridge(from, amount)`** on the **source** is a view-only diagnostic for **that amount**: same policy family as amount limit checks, evaluated on the **raw** burn size (not the LayerZero fee normalization). Outbound **`_bridgeTo`** does **not** call **`canBridge`**; call it from the client if you want **`(false, reason)`** before signing instead of learning only from a revert after burn setup. When the message is executed on the **destination**, **`_enforceLimits`** is the hard gate for **amount** throttles and whitelist behavior at mint time. - -## Outbound pause, approved requests, and inbound source bridges - -These controls are separate from numeric **amount** caps; they still block or relax bridging and can surface as **`BRIDGE_LIMITS`** or inbound skips. - -**`pauseBridge`** sets **`isClosed`**; when closed, outbound flow reverts with **`BRIDGE_LIMITS('closed')`**. **`approvedRequests(requestId)`** on the destination lets **`_bridgeFrom`** skip standard **amount** limit enforcement for that completion when set. **`setDisabledBridges`** toggles **`disabledSourceBridges`** entries keyed by **`keccak256(abi.encode(sourceChainId, BridgeService))`**, controlling whether an inbound relay from that source is accepted before the rest of destination handling. See `references/contracts/MessagePassingBridge.abi.yaml`. - -## Axelar vs LayerZero on GoodDollar deployments - -LayerZero mappings are initialized for Ethereum, Celo, Fuse, and XDC in `initialize` / `upgrade` on `MessagePassingBridge`. The **Axelar** path is only usable where `toAxelarChainId(targetChainId)` returns a non-empty string; the on-chain pure function currently maps **1**, **5**, **42220**, and **44787** only. For **Fuse (122)** or **XDC (50)** targets, use **LZ** (`bridgeToWithLz`) unless governance ships a broader Axelar mapping. - -## LayerZero fee and `estimateSendFee` - -`bridgeToWithLz` burns the G$ **raw** amount in the source token’s `decimals()`. Inside the contract, LayerZero payload and `estimateSendFee` use a value normalized to **18 decimals** the same way as [GoodBridge `BridgeHelperLibrary.normalizeFromTokenTo18Decimals`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol): if `decimals < 18`, multiply by `10^(18 - decimals)`; if `decimals > 18`, divide by `10^(decimals - 18)`; otherwise use the raw amount. - -`canBridge(from, amount)` is evaluated on the **raw** burn amount, not the normalized value. - -Read `decimals()` from the source G$ contract when building off-chain fee quotes so you stay aligned if a deployment differs. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -function normalizedForLzFee(raw, tokenDecimals) { - if (tokenDecimals < 18) return raw * 10n ** BigInt(18 - tokenDecimals); - if (tokenDecimals > 18) return raw / 10n ** BigInt(tokenDecimals - 18); - return raw; -} - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const token = new ethers.Contract( - process.env.GOODDOLLAR_ADDRESS, - [ - "function decimals() view returns (uint8)", - "function allowance(address,address) view returns (uint256)", - "function approve(address,uint256) returns (bool)", - ], - signer, -); - -const bridge = new ethers.Contract( - process.env.BRIDGE_ADDRESS, - [ - "function canBridge(address,uint256) view returns (bool,string)", - "function toLzChainId(uint256) view returns (uint16)", - "function estimateSendFee(uint16,address,address,uint256,bool,bytes) view returns (uint256,uint256)", - "function bridgeToWithLz(address,uint256,uint256,bytes) payable", - "function bridgeToWithAxelar(address,uint256,uint256,address) payable", - ], - signer, -); - -const owner = await signer.getAddress(); -const targetChainId = Number(process.env.TARGET_CHAIN_ID); -const recipient = process.env.RECIPIENT; -const amount = ethers.parseUnits(process.env.AMOUNT, Number(process.env.DECIMALS)); -const transport = (process.env.BRIDGE_TRANSPORT || "LZ").toUpperCase(); -const tokenDecimals = await token.decimals(); -const normalizedForLzEstimate = normalizedForLzFee(amount, Number(tokenDecimals)); - -const [canBridge, reason] = await bridge.canBridge(owner, amount); -if (!canBridge) throw new Error(`Bridge blocked: ${reason}`); - -const allowance = await token.allowance(owner, process.env.BRIDGE_ADDRESS); -if (allowance < amount) { - const approveTx = await token.approve(process.env.BRIDGE_ADDRESS, amount); - await approveTx.wait(); -} - -let tx; - -if (transport === "LZ") { - const dstEid = await bridge.toLzChainId(targetChainId); - if (dstEid === 0) throw new Error("Unsupported target chain for LayerZero"); - - const adapterParams = process.env.LZ_ADAPTER_PARAMS || "0x"; - const [nativeFee] = await bridge.estimateSendFee( - dstEid, - owner, - recipient, - normalizedForLzEstimate, - false, - adapterParams, - ); - if (nativeFee <= 0n) throw new Error("Estimated LayerZero fee is zero"); - - tx = await bridge.bridgeToWithLz(recipient, targetChainId, amount, adapterParams, { - value: nativeFee, - }); -} else if (transport === "AXELAR") { - const nativeFee = ethers.parseEther(process.env.AXELAR_FEE_ETH || "0.01"); - tx = await bridge.bridgeToWithAxelar(recipient, targetChainId, amount, owner, { - value: nativeFee, - }); -} else { - throw new Error("Unsupported BRIDGE_TRANSPORT. Use LZ or AXELAR"); -} - -const receipt = await tx.wait(); -console.log( - JSON.stringify( - { - txHash: receipt.hash, - sourceBridge: process.env.BRIDGE_ADDRESS, - targetChainId, - transport, - recipient, - rawAmount: amount.toString(), - tokenDecimals: Number(tokenDecimals), - normalizedAmountForLz: normalizedForLzEstimate.toString(), - }, - null, - 2, - ), -); -``` - -## Failure handling - -- unsupported destination: return targetChainId, bridge address, and transport mode -- fee too low (`LZ_FEE` or underpriced Axelar fee): re-estimate and retry with user confirmation -- approval or balance issue: return required delta -- credited G$ on destination is reduced by **`bridgeFees`** (bps / min / max); that is independent of the source **`msg.value`** transport fee -- **`canBridge`** false on source: return the **`reason`** string from the view call -- **`BRIDGE_LIMITS(reason)`** custom error (see `references/contracts/MessagePassingBridge.abi.yaml` **errors**): **`reason`** labels the failing check (numeric **amount** limit, whitelist, **`closed`**, or other policy string from the implementation) -- source preflight passed but destination still reverts: re-read **`bridgeLimits`** and daily counters for **amount** caps and **`onlyWhitelisted`**; check **`isClosed`**, **`approvedRequests`**, and **`disabledSourceBridges`** per **Outbound pause, approved requests, and inbound source bridges**; message delivery can cross a reset boundary or policy change diff --git a/.agents/skills/gooddollar/references/guides/check-identity.md b/.agents/skills/gooddollar/references/guides/check-identity.md deleted file mode 100644 index adc0165..0000000 --- a/.agents/skills/gooddollar/references/guides/check-identity.md +++ /dev/null @@ -1,87 +0,0 @@ -# Check identity guide - -Use when the user asks whether an address is eligible for UBI or how identity links wallets. - -## GoodDocs alignment - -- [Connect another wallet address to identity](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity): associated addresses resolve to a verified root in the Identity contract; `connectAccount` links wallets. -- One claim per day applies across all connected addresses for the same verified identity (see the hint on that page). - -## Goal - -Determine whitelist or authentication status with deterministic on-chain reads. - -## Metric semantics - -- **Passed whitelisting (historical):** use `lastAuthenticated(account) > 0`. -- **Still whitelisted (current):** use `getWhitelistedRoot(account) != 0x0` or `isWhitelisted(account) == true`. -- `getWhitelistedRoot(account) != 0x0` is a current-state signal, not an "ever passed" signal. - -## Required inputs - -- `nameServiceAddress` or explicit Identity address -- `account` to check -- `rpcUrl` and chain configuration - -## Execution flow - -1. Resolve `IDENTITY` from NameService when used on the deployment. -2. Read `getWhitelistedRoot(account)` or equivalent for the deployed Identity version. -3. Treat non-zero root as tied to a whitelisted identity tree when that is the protocol rule for the deployment. -4. Read `lastAuthenticated(account)` for historical pass status and `getWhitelistedRoot(account)` or `isWhitelisted(account)` for current status. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); - -const nameService = new ethers.Contract( - process.env.NAMESERVICE_ADDRESS, - ["function getAddress(string) view returns (address)"], - provider, -); - -const identityAddress = await nameService.getAddress("IDENTITY"); -const identity = new ethers.Contract( - identityAddress, - [ - "function getWhitelistedRoot(address) view returns (address)", - "function isWhitelisted(address) view returns (bool)", - "function lastAuthenticated(address) view returns (uint256)", - ], - provider, -); - -const account = process.env.ACCOUNT; -const root = await identity.getWhitelistedRoot(account); -const isWhitelisted = await identity.isWhitelisted(account); -const lastAuthenticated = await identity.lastAuthenticated(account); - -console.log( - JSON.stringify( - { - account, - identityAddress, - whitelistedRoot: root, - isWhitelisted, - lastAuthenticated: lastAuthenticated.toString(), - }, - null, - 2, - ), -); -``` - -## Return shape - -- `isWhitelisted` or equivalent boolean summary -- `whitelistedRoot` or equivalent -- `lastAuthenticated` for historical-pass classification -- optional metadata fields when available - -## Failure handling - -- NameService cannot resolve `IDENTITY`: stop and fix inputs using [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`Identity` / `NameService` for the target environment)—not GoodDocs tables. -- Read failures: return the failing call and next step. diff --git a/.agents/skills/gooddollar/references/guides/claim.md b/.agents/skills/gooddollar/references/guides/claim.md deleted file mode 100644 index bd51abd..0000000 --- a/.agents/skills/gooddollar/references/guides/claim.md +++ /dev/null @@ -1,83 +0,0 @@ -# Claim guide - -Use when the user wants to claim daily UBI. Protocol context: [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and [UBIScheme (GoodDocs behavior)](https://docs.gooddollar.org/for-developers/core-contracts/ubischeme)—contract addresses only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). - -## Goal - -Execute a safe `claim()` with identity pre-checks and clear outputs. - -## GoodDocs alignment - -- UBI is distributed daily to verified users; the active pool is split among claimers in each period (see [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works)). -- UBIScheme deployments vary by chain (Fuse, Celo, XDC); resolve live contract addresses only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`UBIScheme` under `production`, `production-celo`, or `production-xdc`). Use [Core contracts / UBIScheme](https://docs.gooddollar.org/for-developers/core-contracts/ubischeme) for documented behavior, not for addresses. - -## Required inputs - -- `nameServiceAddress` or explicit UBIScheme and Identity addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) -- `rpcUrl` and chain configuration -- signer context - -## Execution flow - -1. Resolve `IDENTITY` and `UBISCHEME` from NameService when NameService is the source of truth for the deployment. -2. Confirm whitelist status for the claiming account. -3. Optionally read entitlement or claimable state before sending `claim()`. -4. Call `claim()` on the resolved UBIScheme (contract generation may differ by deployment; align ABI with your target). -5. Return tx hash and claimed amount when derivable from events or balance delta. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const nameService = new ethers.Contract( - process.env.NAMESERVICE_ADDRESS, - ["function getAddress(string) view returns (address)"], - provider, -); - -const identityAddress = await nameService.getAddress("IDENTITY"); -const ubiAddress = await nameService.getAddress("UBISCHEME"); - -const identity = new ethers.Contract( - identityAddress, - [ - "function isWhitelisted(address) view returns (bool)", - "function getWhitelistedRoot(address) view returns (address)", - ], - provider, -); - -const account = await signer.getAddress(); -const isWhitelisted = await identity.isWhitelisted(account); -if (!isWhitelisted) throw new Error("Account is not whitelisted"); - -const root = await identity.getWhitelistedRoot(account); -if (root === ethers.ZeroAddress) throw new Error("No whitelisted root"); - -const ubi = new ethers.Contract( - ubiAddress, - ["function claim()", "event UBICalculated(address,uint256,uint256,uint256)"], - signer, -); - -const tx = await ubi.claim(); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, account, root }, null, 2)); -``` - -## Pre-check failures - -- Not whitelisted: stop and point the user to identity verification flows in GoodDocs. -- Missing contract address: stop; use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only. -- Zero entitlement: communicate that nothing is claimable in the current period without guessing amounts. - -## Output contract - -- network -- resolved contract addresses -- tx hash -- claim outcome details when available diff --git a/.agents/skills/gooddollar/references/guides/faucet.md b/.agents/skills/gooddollar/references/guides/faucet.md deleted file mode 100644 index d224e85..0000000 --- a/.agents/skills/gooddollar/references/guides/faucet.md +++ /dev/null @@ -1,76 +0,0 @@ -# Faucet top-up guide - -Use when the user needs native gas top-up via GoodProtocol Faucet. - -## Goal - -Run deterministic pre-checks and call `topWallet` only when eligibility and limits pass. - -## Required inputs - -- `rpcUrl`, chain configuration, signer -- Faucet address for the chain (from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) `Faucet` under the matching environment, or `NameService.getAddress` when the deployment documents the key) -- target user address - -## Execution flow - -1. Resolve Faucet contract address for the active chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). -2. Run `canTop(user)` as preflight. -3. Read `getToppingAmount(user)` and communicate expected top-up. -4. If eligible, call `topWallet(user)`. -5. Return tx hash and resulting top-up context. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const faucet = new ethers.Contract( - process.env.FAUCET_ADDRESS, - [ - "function canTop(address) view returns (bool)", - "function getToppingAmount(address) view returns (uint256)", - "function topWallet(address payable)", - ], - signer, -); - -const user = process.env.USER; -const canTop = await faucet.canTop(user); -if (!canTop) throw new Error("Faucet canTop returned false"); - -const amount = await faucet.getToppingAmount(user); -const tx = await faucet.topWallet(user); -const receipt = await tx.wait(); - -console.log( - JSON.stringify( - { - txHash: receipt.hash, - user, - toppingAmount: amount.toString(), - }, - null, - 2, - ), -); -``` - -## Common rejection reasons - -- `not authorized` -- daily or weekly cap reached -- banned address -- low effective `toTop` vs minimum threshold -- faucet inactive or wrong chain/address - -## Output contract - -- network -- faucet address -- `canTop` preflight result -- top-up amount estimate -- tx hash (when executed) diff --git a/.agents/skills/gooddollar/references/guides/gooddocs.md b/.agents/skills/gooddollar/references/guides/gooddocs.md deleted file mode 100644 index 5e5b4b3..0000000 --- a/.agents/skills/gooddollar/references/guides/gooddocs.md +++ /dev/null @@ -1,37 +0,0 @@ -# GoodDocs hub - -Canonical protocol documentation lives at [GoodDocs](https://docs.gooddollar.org/). - -This is a **routing guide** (quick link map), not a deep protocol analysis note. - -## Start here - -- [Welcome](https://docs.gooddollar.org/) -- [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) - -## User guides - -- [Buy and Sell G$](https://docs.gooddollar.org/user-guides) (reserve-backed buy/sell; includes historical Ethereum/Kovan explorer workflows in the doc) -- [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars) (MessagePassingBridge, fees, limits, troubleshooting) -- [Connect another wallet address to identity](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity) - -## Developers - -- [Core contracts](https://docs.gooddollar.org/for-developers/core-contracts) (module overview; **do not** read contract addresses from GoodDocs—use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only) -- [Developer guides index](https://docs.gooddollar.org/for-developers/developer-guides) -- [Integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (ERC-677, ERC-777, decimals by chain, `transferAndCall`, fees) -- [Use G$ streaming](https://docs.gooddollar.org/for-developers/developer-guides/use-gusd-streaming) (Superfluid on Celo, CFAv1Forwarder) - -## Chain IDs (bridge doc) - -| Network | Chain ID | -| -------- | -------- | -| Ethereum | 1 | -| Fuse | 122 | -| Celo | 42220 | -| XDC | 50 | - -## This repo - -- Action playbooks: `references/guides/*.md`. -- Rich ABIs: `references/contracts/*.abi.yaml`. diff --git a/.agents/skills/gooddollar/references/guides/goodsdks.md b/.agents/skills/gooddollar/references/guides/goodsdks.md deleted file mode 100644 index 8092e40..0000000 --- a/.agents/skills/gooddollar/references/guides/goodsdks.md +++ /dev/null @@ -1,110 +0,0 @@ -# GoodSDKs integration guide - -Use this guide when the task is SDK-first (app integration), not raw contract-first. - -## Scope - -GoodSDKs is the app integration layer for GoodDollar: - -- `@goodsdks/citizen-sdk` for identity and claim flows. -- `@goodsdks/react-hooks` for Wagmi React hooks. -- `@goodsdks/good-reserve` for reserve buy or sell flows. -- `@goodsdks/engagement-sdk` for engagement rewards flows. -- `@goodsdks/ui-components` and `@goodsdks/savings-widget` for web components. - -## Routing map - -- Check whitelist, identity root, FV link -> `@goodsdks/citizen-sdk` (`IdentitySDK`) -- Claim UBI with entitlement checks and fallback chains -> `@goodsdks/citizen-sdk` (`ClaimSDK`) -- React app with Wagmi and minimal glue code -> `@goodsdks/react-hooks` -- Buy or sell via reserve rails (Celo or XDC support rules) -> `@goodsdks/good-reserve` -- Reward app registration, claims, reward history -> `@goodsdks/engagement-sdk` -- Embeddable UI in non-React or mixed stacks -> `@goodsdks/ui-components` or `@goodsdks/savings-widget` - -## Deterministic setup - -Monorepo prerequisites: - -```bash -cd ~/Projects/GoodSDKs -corepack enable -yarn install --immutable -yarn build -``` - -Target one workspace: - -```bash -yarn workspace @goodsdks/citizen-sdk build -yarn workspace @goodsdks/react-hooks build -``` - -## Deterministic usage snippets - -Identity SDK: - -```ts -import { createPublicClient, createWalletClient, custom, http } from "viem"; -import { IdentitySDK } from "@goodsdks/citizen-sdk"; - -const publicClient = createPublicClient({ transport: http("https://forno.celo.org") }); -const walletClient = createWalletClient({ transport: custom(window.ethereum) }); - -const identitySDK = await IdentitySDK.init({ - publicClient, - walletClient, - env: "production", -}); - -const { isWhitelisted, root } = await identitySDK.getWhitelistedRoot("0xYourAccount"); -console.log({ isWhitelisted, root }); -``` - -Claim SDK: - -```ts -import { ClaimSDK, IdentitySDK } from "@goodsdks/citizen-sdk"; - -const identitySDK = await IdentitySDK.init({ publicClient, walletClient, env: "production" }); -const claimSDK = await ClaimSDK.init({ - publicClient, - walletClient, - identitySDK, - env: "production", -}); - -const entitlement = await claimSDK.checkEntitlement(); -if (entitlement.amount > 0n) { - const receipt = await claimSDK.claim(); - console.log(receipt.transactionHash); -} -``` - -React hooks: - -```tsx -import { useIdentitySDK, useClaimSDK, useGoodReserve } from "@goodsdks/react-hooks"; - -const identity = useIdentitySDK("production"); -const claim = useClaimSDK("production"); -const reserve = useGoodReserve("production"); -``` - -Reserve SDK: - -```ts -import { GoodReserveSDK } from "@goodsdks/good-reserve"; - -const sdk = new GoodReserveSDK(publicClient, walletClient, "production"); -const quote = await sdk.getBuyQuote(CUSD_ADDRESS, amountIn); -const tx = await sdk.buy(CUSD_ADDRESS, amountIn, (quote * 95n) / 100n); -console.log(tx.hash); -``` - -## Agent rules - -1. Prefer SDK methods first for app tasks. -2. Use contract-level guides only when SDK does not expose required behavior. -3. Do not invent SDK method names; align with package READMEs and exported types. -4. For chain support errors, report chain and env explicitly (do not silently fallback). -5. For UI tasks, prefer hooks or components over bespoke wallet and viem plumbing. diff --git a/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md b/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md deleted file mode 100644 index e7b6f28..0000000 --- a/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md +++ /dev/null @@ -1,110 +0,0 @@ -# Envio HyperSync and HyperRPC - -Use this guide when the task is high-volume historical blockchain data fetch (events, blocks, txs), especially analytics and indexing workflows. - -## Official docs - -- HyperSync overview: [docs.envio.dev/docs/HyperSync/overview](https://docs.envio.dev/docs/HyperSync/overview) -- HyperRPC overview: [docs.envio.dev/docs/HyperRPC/overview-hyperrpc](https://docs.envio.dev/docs/HyperRPC/overview-hyperrpc) -- HyperRPC supported networks: [docs.envio.dev/docs/HyperRPC/hyperrpc-supported-networks](https://docs.envio.dev/docs/HyperRPC/hyperrpc-supported-networks) - -## What to use - -- **HyperSync**: preferred for new data pipelines and heavy historical scans. -- **HyperRPC**: read-only JSON-RPC drop-in for existing RPC code paths. - -## HyperRPC vs HyperSync (avoid mixing them up) - -- **HyperRPC** is a **hosted JSON-RPC URL** (same methods as `eth_getLogs`, `eth_blockNumber`, and so on). Any HTTP client or existing RPC stack can call it; put the API token in the URL path as Envio documents. -- **HyperSync** is a **separate high-throughput query API** used through **Envio client libraries** (for example `@envio-dev/hypersync-client` in Node). It is **not** “just another RPC endpoint” with the same ergonomics as a one-line `fetch` to `eth_getLogs` at large scale. - -## Decision rule - -1. For GoodDollar protocol history that exists on subgraphs, query the subgraph first and validate fields in `references/subgraphs/*-guide.md`. -2. If subgraph schema or freshness cannot satisfy the request, use **HyperSync** for large scans and pipelines, or **HyperRPC** when you must stay inside standard JSON-RPC. -3. For write operations (sending tx), use normal RPC providers; HyperRPC is read-only. - -## GoodDollar-relevant network coverage - -- Celo and XDC are supported on HyperRPC. -- Fuse is not currently listed; treat this as non-blocking and use existing providers for Fuse. - -## Access and auth - -- HyperRPC/HyperSync usage is account-based. -- HyperRPC requires an API key for reliable production use. -- Requests without API token are rate-limited and should be treated as non-production fallback only. -- Add API key in endpoint URL as documented by Envio. -- HyperRPC token pattern example from docs: `https://.rpc.hypersync.xyz/` - -## Agents: Envio API token when HyperSync is the best option - -After you decide **HyperSync** is the right tool for the user query (for example large historical scans or pipeline-scale log pulls where subgraphs are insufficient), check for a usable Envio credential in the execution environment (`ENVIO_API_TOKEN` for `@envio-dev/hypersync-client`, or the token Envio documents for your chosen URL pattern). - -If **no** Envio API token is available and you cannot complete the HyperSync path without it, **stop and explicitly ask the user** to provide an Envio API token (name the env var you need, typically `ENVIO_API_TOKEN`). Do not silently rely on anonymous or heavily rate-limited access as a substitute when HyperSync was already identified as the best approach. - -## Practical use in this repo - -- Keep subgraphs as first option for indexed protocol entities. -- Use HyperSync/HyperRPC when subgraph coverage is missing, stale, or insufficient for bulk historical pulls. -- When an agent chooses **HyperSync** as the best path and no Envio API token is available, follow **Agents: Envio API token when HyperSync is the best option** in this file and ask the user for `ENVIO_API_TOKEN` before proceeding. -- Keep contract addresses from [GoodProtocol/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only; use GoodDocs for product context, not for resolving contract addresses. -- For implementation details (client setup, query structure, supported methods), follow the Envio docs links above directly. - -## From block for historical fetches - -For **`eth_getLogs`**, HyperRPC, and HyperSync range queries, the lower bound is **`fromBlock`** (or the client’s equivalent). Prefer the deployment’s **`creationBlock`** from the matching row in `references/contracts/*.abi.yaml` (or **`meta.deploymentCreationBlocks`** where deployments are plain address strings) so scans do not start at genesis when you only need post-deploy history. Field placement for **`creationBlock`** is defined in `references/contracts/_rich-abi-yaml-format.md`. If you cannot determine the creation block, **`fromBlock` 0** is valid. - -## Prebuilt scripts (developers and local agents) - -These scripts avoid rediscovering HyperRPC wiring on every task. They require **Node.js 18 or newer** (global `fetch`). Paths like `scripts/...` are relative to the **GoodSkills repository root** (the directory that contains both `skills/` and `scripts/`), not relative to `skills/gooddollar/` alone. - -### Last N Identity `WhitelistedAdded` logs via HyperRPC - -- Script: `scripts/fetch-whitelist-events-hyperrpc.mjs` -- Default `EVENT_TOPIC0` matches `WhitelistedAdded(address)` on `IdentityV4`; override `EVENT_TOPIC0` for other events. -- Production Celo defaults: `CONTRACT_ADDRESS` defaults to `Identity` from `production-celo` in [GoodProtocol deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`0xC361A6E67822a0EDc17D899227dd9FC50BD62F42`). If `HYPERRPC_URL` is unset, the script builds `https://celo.rpc.hypersync.xyz/` from `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN`. -- Optional env: `HYPERRPC_URL` (overrides token-based default), `CONTRACT_ADDRESS`, `LIMIT` (default `500`), `STEP` (default `2000`), `FROM_BLOCK` (decimal; omit to read **`creationBlock`** for `CONTRACT_ADDRESS` from `ABI_PATH` or the default `skills/gooddollar/references/contracts/IdentityV4.abi.yaml`), `ABI_PATH`, `TO_BLOCK` (default `latest`). **`fromBlock`** behavior is described in **From block for historical fetches** above. - -```bash -cd /path/to/GoodSkills -export HYPERRPC_API_TOKEN='' -node scripts/fetch-whitelist-events-hyperrpc.mjs -``` - -Web-only assistants without a shell cannot run the file; they should return the same env keys and command text so the user runs it locally. - -## HyperSync client minimal path (install required) - -HyperSync uses the official client. Install and query pattern (Celo example URLs from [Envio Celo docs](https://docs.envio.dev/docs/HyperIndex/celo)): - -```bash -npm install @envio-dev/hypersync-client -export ENVIO_API_TOKEN='' -``` - -Save as a `.mjs` file (or use `"type": "module"` in a local `package.json`) and run with `node`: - -```javascript -import { HypersyncClient, presetQueryLogsOfEvent } from "@envio-dev/hypersync-client"; - -const client = new HypersyncClient({ - url: "https://celo.hypersync.xyz", - apiToken: process.env.ENVIO_API_TOKEN, -}); - -const identity = "0x..."; -const whitelistedAddedTopic0 = - "0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f"; - -const fromBlock = 17237952; -const toBlock = await client.getHeight(); - -const query = presetQueryLogsOfEvent(identity, whitelistedAddedTopic0, fromBlock, toBlock); -const res = await client.get(query); -console.log(res.data.logs.length); -``` - -The example **`fromBlock`** matches **`creationBlock`** for production Celo Identity in `skills/gooddollar/references/contracts/IdentityV4.abi.yaml`; see **From block for historical fetches** above. - -Full API and streaming patterns: [HyperSync clients](https://docs.envio.dev/docs/HyperSync/hypersync-clients) and the package README for `@envio-dev/hypersync-client`. diff --git a/.agents/skills/gooddollar/references/guides/invite-bounties.md b/.agents/skills/gooddollar/references/guides/invite-bounties.md deleted file mode 100644 index d60aafd..0000000 --- a/.agents/skills/gooddollar/references/guides/invite-bounties.md +++ /dev/null @@ -1,115 +0,0 @@ -# Invite bounties guide - -Use when the task is to verify or execute inviter-invitee bounty payout flow and explain why payout is blocked. - -## Goal - -Check eligibility deterministically, execute payout only when eligible, and return exact failure reason when not eligible. - -## Required inputs - -- target chain -- `InvitesV2` address -- `Identity` address -- optional `UBISchemeV2` address when claims threshold is active -- invitee address -- inviter address -- rpc url and signer - -## Execution flow - -1. Resolve contract addresses from `deployment.json`. -2. Read invitee state from `users(invitee)` and global thresholds (`minimumClaims`, `minimumDays`, `active`). -3. Check current eligibility with `canCollectBountyFor(invitee)`. -4. If not eligible, read identity whitelist for invitee and inviter and return concrete blocker. -5. If eligible, execute `bountyFor(invitee)` or `collectBounties()` and return tx hash plus payout values from events. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const invites = new ethers.Contract( - process.env.INVITES_ADDRESS, - [ - "function canCollectBountyFor(address) view returns (bool)", - "function bountyFor(address)", - "function users(address) view returns (bytes32,address,uint40,uint24,bool,uint256)", - "function minimumClaims() view returns (uint256)", - "function minimumDays() view returns (uint256)", - "function active() view returns (bool)", - "event InviterBounty(address indexed inviter,address indexed invitee,uint256 bountyPaid,uint256 inviterLevel,bool earnedLevel)" - ], - signer, -); - -const identity = new ethers.Contract( - process.env.IDENTITY_ADDRESS, - [ - "function isWhitelisted(address) view returns (bool)" - ], - provider, -); - -const invitee = process.env.INVITEE_ADDRESS; -const inviter = process.env.INVITER_ADDRESS; - -const [isActive, eligible, inviteeWhitelisted, inviterWhitelisted, minClaims, minDays] = await Promise.all([ - invites.active(), - invites.canCollectBountyFor(invitee), - identity.isWhitelisted(invitee), - identity.isWhitelisted(inviter), - invites.minimumClaims(), - invites.minimumDays(), -]); - -if (!isActive) throw new Error("Invites contract is inactive"); -if (!eligible) { - throw new Error( - `Not eligible. inviteeWhitelisted=${inviteeWhitelisted} inviterWhitelisted=${inviterWhitelisted} minimumClaims=${minClaims} minimumDays=${minDays}`, - ); -} - -const tx = await invites.bountyFor(invitee); -const receipt = await tx.wait(); -const bountyEvent = receipt.logs - .map((log) => { - try { - return invites.interface.parseLog(log); - } catch { - return null; - } - }) - .find((e) => e && e.name === "InviterBounty"); - -console.log( - JSON.stringify( - { - txHash: receipt.hash, - invitee, - inviter, - bountyPaid: bountyEvent?.args?.bountyPaid?.toString() ?? null, - }, - null, - 2, - ), -); -``` - -## Failure handling - -- invitee or inviter is not currently whitelisted -- reverification is due and whitelist check fails until re-authentication -- minimum claims or minimum days is not met -- bounty already paid or bounty-at-join is zero -- contract inactive or wrong deployment addresses - -## Output contract - -- network and addresses used -- eligibility status and blockers -- tx hash when sent -- payout values when available from logs diff --git a/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md b/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md deleted file mode 100644 index 4ad9fd2..0000000 --- a/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md +++ /dev/null @@ -1,116 +0,0 @@ -# Fuse to CELO staking migration guide - -Use when the user wants to migrate an existing Fuse governance stake into a CELO destination savings flow. In this flow, Fuse `GovernanceStakingV2` is the old staking contract (source) and **`GooddollarSavingsStream`** on Celo is the destination savings contract ([`GooddollarSavingsStream.sol`](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol), [CeloScan](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B)). - -## Goal - -Close a user stake on Fuse, bridge the resulting G$ to CELO, and stake on CELO for that user in a controlled backend flow. - -## Required inputs - -- user address on Fuse and corresponding destination address on CELO -- Fuse `GovernanceStakingV2` address (`production.GovernanceStakingV2` in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json)) -- Fuse G$ token address and bridge contract address (from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json): `GoodDollar`, `MpbBridge` under `production`) -- CELO G$ token address (from `production-celo` in the same file) and destination savings contract address -- backend signer or service wallet with required execution permissions -- chain RPC URLs for Fuse and CELO - -## Address resolution quick table - -| Purpose | Network | Source key/path | Value | -|---|---|---|---| -| Governance staking (source close) | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GovernanceStakingV2` | `0xB7C3e738224625289C573c54d402E9Be46205546` | -| Governance staking (previous) | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GovernanceStaking` | `0xFAF457Fb4A978Be059506F6CD41f9B30fCa753b0` | -| Fuse G$ token | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GoodDollar` | `0x495d133B938596C9984d462F007B676bDc57eCEC` | -| Fuse bridge | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.MpbBridge` | `0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5` | -| Destination savings | CELO (`networkId: 42220`) | [CeloScan](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B) / `GooddollarSavingsStream` | `0x059ee811414230d1Fb157878D2b491240F4D8d3B` (`process.env.CELO_SAVINGS`) | - -Canonical sources: - -- [GoodProtocol deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) -- [Fuse explorer contract (GovernanceStakingV2 address)](https://explorer.fuse.io/address/0xB7C3e738224625289C573c54d402E9Be46205546?tab=contract) -- [Ubeswap gooddollar-contracts](https://github.com/Ubeswap/gooddollar-contracts) -- [GoodBridge bridge helper normalization](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol) - -## Token decimals and `MessagePassingBridge` LZ fees - -Production deployments checked on-chain: Fuse `GoodDollar` uses **2** decimals and Celo `GoodDollar` uses **18**. Fuse `GovernanceStakingV2` (sG$) uses **2** decimals. Resolve `decimals()` from each live token in your runner so you stay correct if deployments change. - -On the Fuse `MpbBridge` (`MessagePassingBridge`), `canBridge(from, amount)` and `bridgeToWithLz(..., amount, ...)` use the **raw G$ burn amount** in source token decimals. Off-chain **`estimateSendFee`’s `_normalizedAmount` argument** must match what the contract builds internally: **`normalizeFromTokenTo18Decimals(amount, IERC20(nativeToken()).decimals())`** ([`BridgeHelperLibrary`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol)). Format displayed amounts per chain (`formatUnits`/UI) using each chain’s G$ decimals. - -## Execution flow - -1. Confirm allowance for whichever asset your flow spends first (for example sG$ allowance to the backend on Fuse `GovernanceStakingV2`, or Fuse G$ allowance if you pull G$ directly), before any transfers or bridges. -2. Verify current stake state on Fuse before closing: - - stake token balance - - withdrawable stake amount - - pending rewards if any -3. Execute Fuse unstake or close flow on governance staking (`withdrawStake` or equivalent full-close path). -4. Compute net G$ available for migration after unstake completion and any reward claim behavior. -5. Bridge G$ from Fuse to CELO using the configured bridge path and track the transfer id or tx hash pair. -6. Wait for destination finalization on CELO and verify credited G$ balance at the backend execution wallet. -7. Approve destination savings contract to spend migrated G$ amount. -8. Stake for the user on CELO with `stakeFor(amount, recipient)` on `GooddollarSavingsStream` (G$ native Super Token; approve the savings contract, not only ERC20 GoodDollar from `deployment.json` if your wallet holds the Super Token). -9. Return a migration result with both chain tx hashes and final CELO staked amount. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const fuse = new ethers.JsonRpcProvider(process.env.FUSE_RPC_URL); -const celo = new ethers.JsonRpcProvider(process.env.CELO_RPC_URL); -const signerFuse = new ethers.Wallet(process.env.BACKEND_PK, fuse); -const signerCelo = new ethers.Wallet(process.env.BACKEND_PK, celo); - -const user = process.env.USER_ADDRESS; -const migrateAmount = BigInt(process.env.MIGRATE_AMOUNT); - -const celoGd = new ethers.Contract( - process.env.CELO_GD_TOKEN, - [ - "function approve(address spender,uint256 amount) returns (bool)", - "function balanceOf(address) view returns (uint256)", - ], - signerCelo, -); - -const savings = new ethers.Contract( - process.env.CELO_SAVINGS, - ["function stakeFor(uint256 amount,address recipient)"], - signerCelo, -); - -const approveTx = await celoGd.approve(process.env.CELO_SAVINGS, migrateAmount); -await approveTx.wait(); - -const stakeTx = await savings.stakeFor(migrateAmount, user); -const receipt = await stakeTx.wait(); - -console.log( - JSON.stringify( - { - user, - celoStakeTx: receipt.hash, - migratedAmount: migrateAmount.toString(), - }, - null, - 2, - ), -); -``` - -## Pre-check failures - -- User allowance missing on Fuse: stop and request allowance tx from user. -- Stake close fails on Fuse: stop and return exact revert reason before bridge. -- Bridge transfer not finalized on CELO: do not call `stakeFor` until destination balance is confirmed. -- CELO savings approval missing or too low: re-approve exact amount before staking. - -## Output contract - -- user address -- Fuse unstake tx hash -- bridge tx hash or transfer identifier -- CELO stake tx hash -- final staked amount on CELO diff --git a/.agents/skills/gooddollar/references/guides/on-off-ramp.md b/.agents/skills/gooddollar/references/guides/on-off-ramp.md deleted file mode 100644 index d212705..0000000 --- a/.agents/skills/gooddollar/references/guides/on-off-ramp.md +++ /dev/null @@ -1,77 +0,0 @@ -# On- and off-ramp service guide - -Use when implementing service flow where fiat ramps support a listed stable token (for example cUSD), and the app needs stable <-> G$ swap on-chain. - -## Goal - -Operate deterministic clone-based swap routing with explicit chain/factory verification and slippage guard. - -## Required inputs - -- target chain and factory address -- owner address used for clone derivation -- stable token and G$ token addresses -- direction: on-ramp or off-ramp -- `minAmount` guard -- signer and rpc url - -## Execution flow - -1. Resolve factory for target chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`BuyGDFactory` / `BuyGDFactoryV2` under `production-celo`, or the key your deployment uses). -2. Compute expected clone via `predict(owner)`. -3. If clone not yet deployed for flow, call `create(owner)` or `createAndSwap(owner, minAmount)`. -4. Execute stable -> G$ (on-ramp) or G$ -> stable (off-ramp) through clone path. -5. Return chain id, factory, predicted clone, effective clone, tx hashes. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const factory = new ethers.Contract( - process.env.BUY_GD_CLONE_FACTORY, - [ - "function predict(address) view returns (address)", - "function create(address) returns (address)", - "function createAndSwap(address,uint256) returns (address)", - ], - signer, -); - -const owner = process.env.OWNER; -const minAmount = ethers.parseUnits(process.env.MIN_AMOUNT, Number(process.env.DECIMALS_OUT)); -const predicted = await factory.predict(owner); - -const tx = await factory.createAndSwap(owner, minAmount); -const receipt = await tx.wait(); - -console.log( - JSON.stringify( - { - txHash: receipt.hash, - owner, - predictedClone: predicted, - chainId: (await provider.getNetwork()).chainId.toString(), - }, - null, - 2, - ), -); -``` - -## Failure handling - -- predicted clone mismatch with trusted expectation -- wrong chain or wrong factory address -- swap output below `minAmount` -- stale router/oracle or exchange configuration - -## Output contract - -- network and chain id -- factory address -- predicted and actual clone addresses -- tx hashes diff --git a/.agents/skills/gooddollar/references/guides/save.md b/.agents/skills/gooddollar/references/guides/save.md deleted file mode 100644 index d0e716c..0000000 --- a/.agents/skills/gooddollar/references/guides/save.md +++ /dev/null @@ -1,93 +0,0 @@ -# Save and stake guide - -Use when the user wants to stake G$, withdraw rewards, or exit stake. Staking economics sit alongside other protocol allocations described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). - -## GoodDocs alignment - -- Token integration and fee awareness: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (`_processFees`, decimals per chain). -- Contract addresses: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example staking and G$ token keys under `production` / `production-celo`). GoodDocs covers behavior and decimals patterns, not canonical deployment addresses. - -## Goal - -Run staking actions with balance and allowance safety checks. - -## Required inputs - -- `nameServiceAddress` or explicit staking and token addresses -- `amount` or `shares` depending on the action -- `rpcUrl`, chain configuration, signer - -## Execution flow - -1. Resolve staking and G$ token addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or, when the deployment documents the key, from `NameService.getAddress` on chain. -2. Read token balance and allowance. -3. Approve the staking contract when `stake` uses `transferFrom`. -4. Execute `stake`, `withdrawRewards`, or `withdrawStake` as requested. -5. Return tx hash and key resulting balances or events. - -## Deterministic snippets - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const token = new ethers.Contract( - process.env.GOODDOLLAR_ADDRESS, - [ - "function balanceOf(address) view returns (uint256)", - "function allowance(address,address) view returns (uint256)", - "function approve(address,uint256) returns (bool)", - ], - signer, -); - -const staking = new ethers.Contract( - process.env.STAKING_ADDRESS, - [ - "function stake(uint256)", - "function withdrawRewards()", - "function withdrawStake(uint256)", - ], - signer, -); -``` - -Stake: - -```js -const amount = ethers.parseUnits(process.env.AMOUNT, Number(process.env.DECIMALS)); -const owner = await signer.getAddress(); -const allowance = await token.allowance(owner, process.env.STAKING_ADDRESS); -if (allowance < amount) { - const approveTx = await token.approve(process.env.STAKING_ADDRESS, amount); - await approveTx.wait(); -} -const tx = await staking.stake(amount); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, action: "stake" }, null, 2)); -``` - -Withdraw rewards: - -```js -const tx = await staking.withdrawRewards(); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, action: "withdrawRewards" }, null, 2)); -``` - -Withdraw stake: - -```js -const shares = ethers.parseUnits(process.env.SHARES, Number(process.env.DECIMALS)); -const tx = await staking.withdrawStake(shares); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, action: "withdrawStake" }, null, 2)); -``` - -## Failure handling - -- Insufficient balance: report shortfall. -- Approval issues: report token, spender, and required allowance. -- Reverts: return attempted function and parameters without guessing custom errors. diff --git a/.agents/skills/gooddollar/references/guides/stream.md b/.agents/skills/gooddollar/references/guides/stream.md deleted file mode 100644 index 6cbf7a4..0000000 --- a/.agents/skills/gooddollar/references/guides/stream.md +++ /dev/null @@ -1,135 +0,0 @@ -# Stream guide - -Primary references for stream execution are local ABI assets in this repo: - -- `references/contracts/CFAv1Forwarder.abi.yaml` -- `references/contracts/ConstantFlowAgreementV1.abi.yaml` -- `references/contracts/Superfluid.abi.yaml` -- `references/contracts/SuperToken.abi.yaml` - -## Goal - -Create, update, or delete Superfluid constant flows using deterministic contract calls and local ABI references. - -## Protocol facts used by this guide - -- Forwarder path uses `CFAv1Forwarder.createFlow`, `updateFlow`, `deleteFlow`. -- Host path uses `Superfluid.callAgreement` with CFA calldata for `createFlow`, `updateFlow`, `deleteFlow`. -- Stream token is a SuperToken; flow rates are `int96` in token-wei per second. -- `getBufferAmountByFlowrate(token, flowRate)` is the canonical pre-check for required buffer. - -## Two implementation styles in this repo - -1. **Forwarder (matches GoodDocs):** call CFAv1Forwarder with token, sender, receiver, flowRate, userData. -2. **Host callAgreement:** encode CFA `createFlow` / `updateFlow` / `deleteFlow` and call `Superfluid.callAgreement`. - -## Minimal method map - -- Forwarder: - - `createFlow(address token, address receiver, int96 flowrate, bytes userData)` - - `updateFlow(address token, address receiver, int96 flowrate, bytes userData)` - - `deleteFlow(address token, address sender, address receiver, bytes userData)` - - `getBufferAmountByFlowrate(address token, int96 flowrate)` -- Host: - - `callAgreement(address agreementClass, bytes callData, bytes userData)` -- CFA: - - `createFlow(address token, address receiver, int96 flowRate, bytes ctx)` - - `updateFlow(address token, address receiver, int96 flowRate, bytes ctx)` - - `deleteFlow(address token, address sender, address receiver, bytes ctx)` - -## Required inputs - -- G$ Super Token address for the environment -- CFA forwarder address, or Superfluid host address plus CFA agreement address -- `action`: create, update, delete -- `receiver`, `flowRate` where applicable -- `rpcUrl`, chain configuration, signer - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const forwarder = new ethers.Contract( - process.env.CFA_FORWARDER, - [ - "function createFlow(address,address,int96,bytes)", - "function updateFlow(address,address,int96,bytes)", - "function deleteFlow(address,address,address,bytes)", - ], - signer, -); - -const token = process.env.SUPER_TOKEN; -const sender = await signer.getAddress(); -const receiver = process.env.RECEIVER; -const flowRate = BigInt(process.env.FLOW_RATE); - -if (process.env.ACTION === "create") { - const tx = await forwarder.createFlow(token, receiver, flowRate, "0x"); - const receipt = await tx.wait(); - console.log(JSON.stringify({ txHash: receipt.hash, action: "create" }, null, 2)); -} - -if (process.env.ACTION === "update") { - const tx = await forwarder.updateFlow(token, receiver, flowRate, "0x"); - const receipt = await tx.wait(); - console.log(JSON.stringify({ txHash: receipt.hash, action: "update" }, null, 2)); -} - -if (process.env.ACTION === "delete") { - const tx = await forwarder.deleteFlow(token, sender, receiver, "0x"); - const receipt = await tx.wait(); - console.log(JSON.stringify({ txHash: receipt.hash, action: "delete" }, null, 2)); -} -``` - -Host callAgreement example: - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const cfa = new ethers.Interface([ - "function createFlow(address,address,int96,bytes)", - "function updateFlow(address,address,int96,bytes)", - "function deleteFlow(address,address,address,bytes)", -]); - -const host = new ethers.Contract( - process.env.SUPERFLUID_HOST, - ["function callAgreement(address,bytes,bytes) returns (bytes)"], - signer, -); - -const token = process.env.SUPER_TOKEN; -const sender = await signer.getAddress(); -const receiver = process.env.RECEIVER; -const flowRate = BigInt(process.env.FLOW_RATE); - -let callData = "0x"; -if (process.env.ACTION === "create") { - callData = cfa.encodeFunctionData("createFlow", [token, receiver, flowRate, "0x"]); -} -if (process.env.ACTION === "update") { - callData = cfa.encodeFunctionData("updateFlow", [token, receiver, flowRate, "0x"]); -} -if (process.env.ACTION === "delete") { - callData = cfa.encodeFunctionData("deleteFlow", [token, sender, receiver, "0x"]); -} - -const tx = await host.callAgreement(process.env.CFA_ADDRESS, callData, "0x"); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, action: process.env.ACTION }, null, 2)); -``` - -## Failure handling - -- Wrong network or missing addresses: stop and return missing host or forwarder or token addresses. -- Insufficient buffer: use `getBufferAmountByFlowrate` and reduce flow rate or top up balance. -- Revert on create or update: verify token is a SuperToken and flowRate is positive. diff --git a/.agents/skills/gooddollar/references/guides/swap.md b/.agents/skills/gooddollar/references/guides/swap.md deleted file mode 100644 index 3db8c2d..0000000 --- a/.agents/skills/gooddollar/references/guides/swap.md +++ /dev/null @@ -1,83 +0,0 @@ -# Swap guide - -Use for buying or selling G$ through Mento-connected contracts on networks where they appear in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `MentoBroker`, `MentoReserve`, `MentoExchangeProvider`, `MentoExpansionController` keys under `production-celo` or `production-xdc`). GoodDocs describes Mento product behavior, not deployment addresses. - -## GoodDocs alignment - -- Reserve and buy or sell mechanics at the protocol level: [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and [Buy and Sell G$ user guide](https://docs.gooddollar.org/user-guides) (includes reserve AMM narrative; older explorer step-by-step for Ethereum testnets remains in that page for reference). -- Integration patterns and decimals: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token). - -## Goal - -Execute bounded swaps using broker quotes and correct allowances. - -## Required inputs - -- `direction` as buy or sell -- broker and exchange identifiers for the deployment -- amounts in correct token decimals for the chain -- `rpcUrl`, chain configuration, signer - -## Execution flow - -1. Confirm Mento Broker (and related) addresses for the chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only. -2. Fetch quote (`getAmountOut` or `getAmountIn` depending on direction and ABI). -3. Apply slippage bounds. -4. Approve the spent token for the broker when required. -5. Call `swapIn` or `swapOut` per your integration. -6. Return tx hash and effective amounts. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const broker = new ethers.Contract( - process.env.BROKER_ADDRESS, - [ - "function getAmountOut(address,address,uint256) view returns (uint256)", - "function swapIn(address,address,uint256,uint256) returns (uint256)", - ], - signer, -); - -const tokenIn = new ethers.Contract( - process.env.TOKEN_IN, - [ - "function allowance(address,address) view returns (uint256)", - "function approve(address,uint256) returns (bool)", - ], - signer, -); - -const amountIn = ethers.parseUnits(process.env.AMOUNT_IN, Number(process.env.DECIMALS_IN)); -const quotedOut = await broker.getAmountOut(process.env.TOKEN_IN, process.env.TOKEN_OUT, amountIn); -const slippageBps = BigInt(process.env.SLIPPAGE_BPS); -const minOut = quotedOut * (10000n - slippageBps) / 10000n; - -const owner = await signer.getAddress(); -const allowance = await tokenIn.allowance(owner, process.env.BROKER_ADDRESS); -if (allowance < amountIn) { - const approveTx = await tokenIn.approve(process.env.BROKER_ADDRESS, amountIn); - await approveTx.wait(); -} - -const tx = await broker.swapIn(process.env.TOKEN_IN, process.env.TOKEN_OUT, amountIn, minOut); -const receipt = await tx.wait(); -console.log( - JSON.stringify( - { txHash: receipt.hash, amountIn: amountIn.toString(), minOut: minOut.toString() }, - null, - 2, - ), -); -``` - -## Failure handling - -- No deployment on chain: direct the user to an environment that defines the needed keys in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `production-celo` with `MentoBroker`). -- Stale quote or tight slippage: refresh quote or relax bounds with user consent. -- Allowance or balance shortfall: report exact delta. diff --git a/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md b/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md deleted file mode 100644 index 36af639..0000000 --- a/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md +++ /dev/null @@ -1,35 +0,0 @@ -# Subgraph query discipline (GoodDollar) - -## Subgraph vs RPC - -- **Subgraph:** historical events, lists, aggregates, time ranges, analytics. Data lags chain head. -- **RPC / SDK:** current balances, live `claim` eligibility, exact view calls. Prefer for user-facing “what is true right now”. - -## Generic GraphQL mechanics - -Graph-node generates `entity`, `entities`, `Entity_filter`, `Entity_orderBy`, pagination, and `_meta` from each deployment’s `schema.graphql`. For scalar rules, filters, `_meta`, and common pitfalls, see [The Graph — Querying a subgraph](https://thegraph.com/docs/en/querying/graphql-api/). - -## GoodDollar-specific - -- **Addresses in `where` clauses:** normalize to **lowercase** hex strings; subgraphs store addresses lowercased. -- **BigInt fields:** query as **string** literals in GraphQL JSON (e.g. `"1000000000000000000"`). -- **Schema truth:** entity names differ per deployment. Run introspection or read the deployment’s `schema.graphql` under the relevant package in [GoodDollar/GoodSubGraphs](https://github.com/GoodDollar/GoodSubGraphs) before assuming field names. - -## Meta block - -Use `_meta { block { number } }` to detect how far behind indexing is when debugging stale data. - -```graphql -{ - _meta { - block { - number - } - hasIndexingErrors - } -} -``` - -## When subgraphs are not enough - -If the subgraph cannot answer the question (missing entities or fields, or stale indexing per `_meta`), switch to the decision rules in `references/guides/hypersync-hyperrpc.md`. When **HyperSync** is the best fit and no Envio API token is available in the environment, **ask the user directly** for `ENVIO_API_TOKEN` (or the token your HyperSync client expects) before running large scans; do not silently use anonymous quota as a stand-in. diff --git a/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md b/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md deleted file mode 100644 index a266cea..0000000 --- a/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md +++ /dev/null @@ -1,54 +0,0 @@ -# GoodCollective — Subgraph Usage Guide - -Companion to `goodcollective.graphql`. - -## Endpoint - -- Explorer: [GoodCollective](https://thegraph.com/explorer/subgraphs/3LbJh9DXhJVvuVDdm5i6StNboJmL9oMNNkBaKyzc4Y8Y?view=Query&chain=arbitrum-one) -- Gateway form: `https://gateway.thegraph.com/api/subgraphs/id/3LbJh9DXhJVvuVDdm5i6StNboJmL9oMNNkBaKyzc4Y8Y` - ---- - -## Terminology: “claim” here is not daily UBI - -In this subgraph, **Claim** and **ClaimEvent** refer to **GoodCollective reward or pool claim flows**, not the protocol’s **daily UBI claim** from `UBIScheme` / `UBISchemeV2`. - -When a user says **“claim”** in normal GoodDollar product language, they almost always mean **claim daily UBI**. For that, use the GoodDollar Celo subgraph (`walletStats` / claim-related aggregates) and on-chain `claim` per `references/guides/claim.md` — do not answer “last N UBI claims” from GoodCollective **Claim** alone. - ---- - -## Entity Overview - -### Core collective graph - -**Collective** — pool identity, limits/settings links, totals, and claim/payment counters. -**Donor** / **Steward** — participant-level donation/support state. -**DonorCollective** / **StewardCollective** — join entities tying participants to a collective. - -### Claim and support flow - -**Claim** and **ClaimEvent** — GoodCollective **reward** claim lifecycle and per-claim reward events (not daily UBI from UBIScheme). -**SupportEvent** — support/donation change events across donor/collective links. - -### Metadata and policy entities - -**IpfsCollective** — IPFS metadata projection. -**PoolSettings**, **UBILimits**, **SafetyLimits** — pool policy and operational bounds. -**ProvableNFT** — NFT linkage used in claim/reward flows. - ---- - -## Typical Questions This Subgraph Answers - -- Which donors/stewards are attached to a collective? -- How much was donated/rewarded per collective and per participant? -- Which claim events occurred and what reward quantities were emitted? -- What limits and settings govern a specific collective pool? - ---- - -## Query Discipline - -- Use authenticated gateway access for programmatic queries. -- Lowercase address-like identifiers where applicable. -- Validate `_meta` before operational dashboards or reporting exports. diff --git a/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql b/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql deleted file mode 100644 index e503db9..0000000 --- a/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql +++ /dev/null @@ -1,200 +0,0 @@ -scalar BigDecimal - -scalar BigInt - -scalar Boolean - -scalar Bytes - -type Claim { - id: String! - collective: Collective! - txHash: String! - networkFee: BigInt! - totalRewards: BigInt! - events: [ClaimEvent!]! - timestamp: Int! -} - -type ClaimEvent { - id: String! - eventType: Int! - timestamp: Int! - quantity: BigInt! - rewardPerContributor: BigInt! - contributors: [Steward!]! - nft: ProvableNFT - claim: Claim! -} - -type Collective { - id: String! - pooltype: PoolType! - ipfs: IpfsCollective - settings: PoolSettings! - ubiLimits: UBILimits - limits: SafetyLimits - donors: [DonorCollective!] - stewards: [StewardCollective!] - projectId: String! - isVerified: Boolean! - poolFactory: String! - timestamp: Int! - paymentsMade: Int! - totalDonations: BigInt! - totalRewards: BigInt! - claims: [Claim!]! -} - -type Donor { - id: String! - timestamp: Int! - totalDonated: BigInt! - collectives: [DonorCollective!]! -} - -""" -Represents the relationship between a Donor and a Collective - -""" - -type DonorCollective { - id: String! - donor: Donor! - collective: Collective! - contribution: BigInt! - flowRate: BigInt! - timestamp: Int! - events: [SupportEvent!]! -} - -scalar Float - -scalar ID - -""" -4 bytes signed integer - -""" - -scalar Int - -""" -8 bytes signed integer - -""" - -scalar Int8 - -type IpfsCollective { - id: String! - name: String! - description: String! - rewardDescription: String - goodidDescription: String - email: String - website: String - twitter: String - instagram: String - threads: String - infoLabel: String - headerImage: String! - logo: String! - images: [String!] -} - -type PoolSettings { - id: String! - nftType: BigInt! - manager: Bytes! - membersValidator: Bytes! - uniquenessValidator: Bytes! - rewardToken: Bytes! -} - -enum PoolType { - DirectPayments - UBI -} - -type ProvableNFT { - id: String! - owner: String! - hash: String! - stewards: [Steward!]! - collective: Collective -} - -type SafetyLimits { - id: String! - maxTotalPerMonth: BigInt! - maxMemberPerMonth: BigInt! - maxMemberPerDay: BigInt! -} - -type Steward { - """ - { user address} - - """ - id: String! - """ - Number of actions performed - - """ - actions: Int! - totalEarned: BigInt! - totalUBIEarned: BigInt! - """ - NFT's minted to steward - - """ - nfts: [ProvableNFT!]! - """ - Collectives the steward is apart of - - """ - collectives: [StewardCollective!]! -} - -""" -Represents the relationship between a Steward and a Collective - -""" - -type StewardCollective { - id: String! - steward: Steward! - collective: Collective! - actions: Int! - totalEarned: BigInt! -} - -scalar String - -type SupportEvent { - id: String! - networkFee: BigInt! - donor: Donor! - collective: Collective! - donorCollective: DonorCollective! - contribution: BigInt! - previousContribution: BigInt! - isFlowUpdate: Boolean! - flowRate: BigInt! - previousFlowRate: BigInt! - timestamp: Int! -} - -scalar Timestamp - -type UBILimits { - id: String! - cycleLengthDays: BigInt! - claimPeriodDays: BigInt! - minActiveUsers: BigInt! - claimForEnabled: Boolean! - maxClaimAmount: BigInt! - maxClaimers: BigInt! - onlyMembers: Boolean! -} diff --git a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md deleted file mode 100644 index 347951f..0000000 --- a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md +++ /dev/null @@ -1,104 +0,0 @@ -# GoodDollar Celo — Subgraph Usage Guide - -Companion to `gooddollar-celo.graphql`. - -## Endpoint - -- Explorer: [GoodDollarCelo](https://thegraph.com/explorer/subgraphs/F7314rxGdcpKPC1nN5KCoFW84EGRoUyzseY2sAT9PEkw?view=Query&chain=arbitrum-one) -- Gateway form: `https://gateway.thegraph.com/api/subgraphs/id/F7314rxGdcpKPC1nN5KCoFW84EGRoUyzseY2sAT9PEkw` - ---- - -## Entity Overview - -### UBI and usage statistics - -**DailyUBI** — day-level UBI pool/quota activity and cycle fields. -**WalletStat** — wallet behavior aggregates: tx counts/values, claim stats, active/whitelist indicators. -**TransactionStat** — day-level transaction totals and circulation view. -**GlobalStatistics** — global claim and distribution rollups. - -### Additional UBI history entities - -**UBICollected** — collected UBI/community-pool values by block event. -**UBIHistory** — timeline totals for daily UBI/community-pool. - ---- - -## Field Availability Reference (use before drafting queries) - -Use this section to validate what the subgraph already provides before switching data sources. - -### DailyUBI - -- `id` — day index key (`unix / 86400`) -- `pool` — UBI cycle pool for that day -- `quota` — daily UBI amount per eligible claimer -- `activeUsers` — active users count in scheme context -- `totalUBIDistributed` — amount actually claimed/distributed that day -- `totalClaims` — claim tx count for that day -- `newClaimers` — newly whitelisted users for that day -- `timestamp` — last update timestamp for the record -- `ubiSchemeAddress` — UBIScheme address used for the record -- `balance` — G$ balance held by UBIScheme -- `cycleLength` — current cycle length -- `dayInCycle` — current day position inside cycle - -### WalletStat - -- `id` — wallet address -- `dateAppeared` — first indexed wallet activity timestamp -- `balance` — running token balance from transfers -- `inTransactionsCount`, `inTransactionsValue` — incoming tx count and value -- `outTransactionsCount`, `outTransactionsValue` — outgoing tx count and value -- `totalTransactionsCount`, `totalTransactionsValue` — total tx count and value -- `inTransactionsCountClean`, `inTransactionsValueClean` — incoming metrics excluding contract-address flows -- `outTransactionsCountClean`, `outTransactionsValueClean` — outgoing metrics excluding contract-address flows -- `totalTransactionsCountClean`, `totalTransactionsValueClean` — total clean traffic metrics -- `lastClaimed` — timestamp of latest UBI claim -- `totalClaimedCount`, `totalClaimedValue` — total claims and cumulative claimed value -- `claimStreak`, `longestClaimStreak` — current and best historical streaks -- `isWhitelisted` — current whitelist status -- `isActiveUser` — current active-user status -- `dateJoined` — first-whitelist timestamp -- `lastTransactionFrom`, `lastTransactionTo` — latest outgoing and incoming tx timestamps - -### TransactionStat - -- `id` — bucket key (day key or `"aggregated"`) -- `dayStartBlockNumber` — first block in bucket -- `transactionsCount`, `transactionsValue` — all transfer tx count and value -- `transactionsCountClean`, `transactionsValueClean` — transfer metrics excluding contract-address flows -- `totalInCirculation` — inferred circulating supply from mint or burn behavior - -### GlobalStatistics - -- `id` — fixed key (`"statistics"`) -- `TransactionStat` — link to aggregated transaction stats -- `totalUBIDistributed` — lifetime distributed UBI -- `uniqueClaimers` — tracked unique claimers via whitelist add or remove -- `totalClaims` — lifetime UBI claim events - -### Explorer naming - -- Singular names (`dailyUBI`, `walletStat`) fetch by `id` -- Plural names (`dailyUBIs`, `walletStats`) query lists with filters and pagination - ---- - -## Typical Questions This Subgraph Answers - -- How much UBI was distributed on a day/cycle? -- Which wallets are active or recently claiming? -- What are aggregate tx/circulation trends? -- How did collected UBI/community-pool values evolve over time? - ---- - -## Query Discipline - -- Use lowercase address strings in filters. -- Use string-safe handling for large integer values. -- Use `_meta` to validate freshness before cross-day analytics. -- Use authenticated gateway access for programmatic queries. -- Before claiming a field or entity is missing, verify availability from this guide and schema first. diff --git a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql deleted file mode 100644 index e544a0d..0000000 --- a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql +++ /dev/null @@ -1,113 +0,0 @@ -scalar BigDecimal - -scalar BigInt - -scalar Boolean - -scalar Bytes - -type DailyUBI { - id: ID! - pool: BigInt! - quota: BigInt! - activeUsers: BigInt! - totalUBIDistributed: BigInt! - totalClaims: BigInt! - newClaimers: BigInt! - timestamp: BigInt! - ubiSchemeAddress: Bytes - balance: BigInt! - cycleLength: BigInt! - dayInCycle: BigInt! -} - -scalar Float - -type GlobalStatistics { - id: ID! - TransactionStat: TransactionStat - totalUBIDistributed: BigInt! - uniqueClaimers: BigInt! - totalClaims: BigInt! -} - -scalar ID - -""" -4 bytes signed integer -""" - -scalar Int - -""" -8 bytes signed integer -""" - -scalar Int8 - -""" -Defines the order direction, either ascending or descending -""" - -scalar String - -""" -A string representation of microseconds UNIX timestamp (16 digits) -""" - -scalar Timestamp - -type TransactionStat { - id: ID! - dayStartBlockNumber: BigInt! - transactionsCount: BigInt! - transactionsCountClean: BigInt! - transactionsValue: BigInt! - transactionsValueClean: BigInt! - totalInCirculation: BigInt! -} - -type UBICollected { - id: ID! - contract: Bytes - block: BigInt! - blockTimestamp: BigInt! - ubi: BigDecimal! - communityPool: BigDecimal! -} - -type UBIHistory { - id: ID! - block: BigInt! - blockTimestamp: BigInt! - totalDailyUBI: BigDecimal! - totalDailyCommunityPool: BigDecimal! -} - -type WalletStat { - id: ID! - dateAppeared: BigInt! - balance: BigInt! - inTransactionsCount: BigInt! - inTransactionsCountClean: BigInt! - inTransactionsValue: BigInt! - inTransactionsValueClean: BigInt! - outTransactionsCount: BigInt! - outTransactionsCountClean: BigInt! - outTransactionsValue: BigInt! - outTransactionsValueClean: BigInt! - totalTransactionsCount: BigInt! - totalTransactionsCountClean: BigInt! - totalTransactionsValue: BigInt! - totalTransactionsValueClean: BigInt! - lastClaimed: BigInt! - totalClaimedCount: BigInt! - totalClaimedValue: BigInt! - claimStreak: BigInt! - longestClaimStreak: BigInt! - isWhitelisted: Boolean! - isActiveUser: Boolean! - dateJoined: BigInt! - lastTransactionFrom: BigInt! - lastTransactionTo: BigInt! -} diff --git a/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md b/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md deleted file mode 100644 index 9f55b73..0000000 --- a/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md +++ /dev/null @@ -1,41 +0,0 @@ -# Reserve Celo — Subgraph Usage Guide - -Companion to `reserve-celo.graphql`. This guide is for reserve pricing and broker swap indexing on Celo. - -## Endpoint - -- Goldsky: `https://api.goldsky.com/api/public/project_cmizuamdtfouu01x4csuk5dk1/subgraphs/reserve_celo/1.0/gn` - ---- - -## Entity Overview - -### Core entity - -**ReservePrice** — one indexed reserve pricing point produced from broker swap flow plus exchange-provider price read. -Key fields: `exchangeId`, `exchangeProvider`, `price`, `timestamp`, `day`, `tokenIn`, `tokenOut`, `amountIn`, `amountOut`, `user`, `blockNumber`, `transactionHash`. - ---- - -## Typical Questions This Subgraph Answers - -- What are the most recent reserve prices? -- What was the reserve price on a specific day window? -- Which token pair and user triggered a pricing point? -- Which tx hash/block produced a given price point? - ---- - -## Query Discipline - -- Lowercase all address values in filters. -- Use string values for large integer variables. -- Use `_meta` before analytics queries when stale indexing is suspected. - ---- - -## Practical Start - -1. Check `_meta` block height and `hasIndexingErrors`. -2. Pull latest `ReservePrice` records sorted by `timestamp desc`. -3. Add `day`-based narrowing for historical windows. diff --git a/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql b/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql deleted file mode 100644 index 56cac37..0000000 --- a/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql +++ /dev/null @@ -1,90 +0,0 @@ -scalar BigDecimal - -scalar BigInt - -scalar Boolean - -scalar Bytes - -scalar Float - -scalar ID - -""" -4 bytes signed integer -""" - -scalar Int - -""" -8 bytes signed integer -""" - -scalar Int8 - -""" -Defines the order direction, either ascending or descending -""" - -type ReservePrice { - """ - Exchange ID combined with timestamp - """ - id: ID! - """ - The exchange ID from the Swap event - """ - exchangeId: Bytes! - """ - The exchange provider address - """ - exchangeProvider: Bytes! - """ - The current price from IBancorExchangeProvider.currentPrice() - """ - price: BigInt! - """ - Transaction timestamp - """ - timestamp: BigInt! - """ - Day ID (timestamp / 86400) - """ - day: BigInt! - """ - Token in address from the swap - """ - tokenIn: Bytes! - """ - Token out address from the swap - """ - tokenOut: Bytes! - """ - Amount in from the swap - """ - amountIn: BigInt! - """ - Amount out from the swap - """ - amountOut: BigInt! - """ - User who initiated the swap - """ - user: Bytes! - """ - Block number - """ - blockNumber: BigInt! - """ - Transaction hash - """ - transactionHash: Bytes! -} - -scalar String - -""" -A string representation of microseconds UNIX timestamp (16 digits) -""" - -scalar Timestamp diff --git a/skills-lock.json b/skills-lock.json deleted file mode 100644 index 9b92c72..0000000 --- a/skills-lock.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 1, - "skills": { - "gooddollar": { - "source": "GoodDollar/GoodSkills", - "sourceType": "github", - "skillPath": "skills/gooddollar/SKILL.md", - "computedHash": "d5f8cc5079f386e7170925e61214fb3334176aa6d0fc6bdc50400e535692b377" - } - } -} From 078ea2d61cb8c96c34a12d0dfc443b52dd153415 Mon Sep 17 00:00:00 2001 From: blueogin Date: Wed, 10 Jun 2026 15:35:14 -0400 Subject: [PATCH 26/27] refactor: remove migration API token from StakingMigrationWidget and related components --- examples/react-web/src/App.tsx | 1 - examples/react-web/src/globals.d.ts | 1 - .../src/StakingMigrationWidget.tsx | 8 +----- .../staking-migration-widget/src/adapter.ts | 27 +++++-------------- .../src/migrationEnvironments.ts | 3 --- .../src/widgetRuntimeContract.ts | 2 -- 6 files changed, 8 insertions(+), 34 deletions(-) diff --git a/examples/react-web/src/App.tsx b/examples/react-web/src/App.tsx index 8a7ac6a..da15a93 100644 --- a/examples/react-web/src/App.tsx +++ b/examples/react-web/src/App.tsx @@ -167,7 +167,6 @@ function OverrideShowcase() { StakingMigrationWidget: diff --git a/examples/react-web/src/globals.d.ts b/examples/react-web/src/globals.d.ts index 001ef1e..7eb9fa0 100644 --- a/examples/react-web/src/globals.d.ts +++ b/examples/react-web/src/globals.d.ts @@ -4,7 +4,6 @@ declare const process: { interface ImportMetaEnv { readonly VITE_MIGRATION_API_BASE_URL?: string - readonly VITE_MIGRATION_API_TOKEN?: string } interface ImportMeta { diff --git a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx index 997fda0..2b16f6e 100644 --- a/packages/staking-migration-widget/src/StakingMigrationWidget.tsx +++ b/packages/staking-migration-widget/src/StakingMigrationWidget.tsx @@ -10,7 +10,6 @@ import type { StakingMigrationWidgetProps } from './widgetRuntimeContract' type StakingMigrationInnerProps = Pick< StakingMigrationWidgetProps, | 'migrationApiBaseUrl' - | 'migrationApiToken' | 'adapterFactory' | 'onMigrationSuccess' | 'onMigrationError' @@ -26,14 +25,12 @@ function formatJourneyLabel(label: string | null): string | null { function StakingMigrationInner({ migrationApiBaseUrl, - migrationApiToken, adapterFactory, onMigrationSuccess, onMigrationError, }: StakingMigrationInnerProps) { const defaultAdapter = useStakingMigrationAdapter({ migrationApiBaseUrl, - migrationApiToken, onMigrationSuccess, onMigrationError, }) @@ -43,10 +40,9 @@ function StakingMigrationInner({ adapterFactory ? adapterFactory({ migrationApiBaseUrl, - migrationApiToken, }) : defaultAdapter, - [adapterFactory, defaultAdapter, migrationApiBaseUrl, migrationApiToken], + [adapterFactory, defaultAdapter, migrationApiBaseUrl], ) const { state, actions } = activeAdapter @@ -140,7 +136,6 @@ export function StakingMigrationWidget({ defaultTheme = 'light', themeOverrides, migrationApiBaseUrl, - migrationApiToken, onMigrationSuccess, onMigrationError, adapterFactory, @@ -154,7 +149,6 @@ export function StakingMigrationWidget({ > void onMigrationError?: (detail: StakingMigrationErrorDetail) => void } @@ -119,21 +118,10 @@ function hasRequiredConfig(migrationConfig: ResolvedStakingMigrationConfig): boo return Boolean(migrationConfig.migrationApiBaseUrl && migrationConfig.migrationOperator) } -function normalizeMigrationApiToken(token: string): string { - return token.startsWith('Bearer ') ? token.slice(7) : token -} - -function buildApiHeaders(migrationConfig: ResolvedStakingMigrationConfig): Record { - const headers: Record = { +function buildApiHeaders(): Record { + return { 'Content-Type': 'application/json', } - - if (migrationConfig.migrationApiToken) { - headers.Authorization = - 'Bearer ' + normalizeMigrationApiToken(migrationConfig.migrationApiToken) - } - - return headers } function buildMigrationApiUrl(baseUrl: string, path: string): string { @@ -253,7 +241,7 @@ async function fetchWorkerMigrationState( const endpoint = `${buildMigrationApiUrl(migrationConfig.migrationApiBaseUrl!, MIGRATION_STATUS_PATH)}?approvalTxHash=${encodeURIComponent(approvalTxHash)}` const response = await fetch(endpoint, { method: 'GET', - headers: buildApiHeaders(migrationConfig), + headers: buildApiHeaders(), }) const responsePayload = (await response.json().catch(() => ({}))) as unknown @@ -284,7 +272,7 @@ async function submitMigrationStart( const endpoint = buildMigrationApiUrl(migrationConfig.migrationApiBaseUrl!, MIGRATION_SUBMIT_PATH) const response = await fetch(endpoint, { method: 'POST', - headers: buildApiHeaders(migrationConfig), + headers: buildApiHeaders(), body: JSON.stringify({ approvalTxHash }), }) @@ -344,7 +332,7 @@ async function watchMigrationViaSse( const endpoint = `${buildMigrationApiUrl(migrationConfig.migrationApiBaseUrl!, MIGRATION_STATUS_STREAM_PATH)}?approvalTxHash=${encodeURIComponent(approvalTxHash)}` const response = await fetch(endpoint, { method: 'GET', - headers: buildApiHeaders(migrationConfig), + headers: buildApiHeaders(), signal, }) @@ -442,15 +430,14 @@ export function derivePrimaryLabel( export function useStakingMigrationAdapter({ migrationApiBaseUrl, - migrationApiToken, onMigrationSuccess, onMigrationError, }: UseStakingMigrationAdapterOptions = {}): StakingMigrationWidgetAdapterResult { const { address, chainId, isConnected, provider, connect } = useWallet() const resolvedConfig = useMemo( - () => resolveMigrationConfig({ migrationApiBaseUrl, migrationApiToken }), - [migrationApiBaseUrl, migrationApiToken], + () => resolveMigrationConfig({ migrationApiBaseUrl }), + [migrationApiBaseUrl], ) const [state, setState] = useState(() => ({ diff --git a/packages/staking-migration-widget/src/migrationEnvironments.ts b/packages/staking-migration-widget/src/migrationEnvironments.ts index facbcf6..aad604b 100644 --- a/packages/staking-migration-widget/src/migrationEnvironments.ts +++ b/packages/staking-migration-widget/src/migrationEnvironments.ts @@ -11,12 +11,10 @@ export const MIGRATION_OPERATOR_ADDRESS: Address = '0xE3441bA0863AEFBf28eca5F6fA export interface ResolvedStakingMigrationConfig { migrationApiBaseUrl?: string migrationOperator: Address - migrationApiToken?: string } export interface ResolveMigrationConfigInput { migrationApiBaseUrl?: string - migrationApiToken?: string } export function resolveMigrationConfig( @@ -25,6 +23,5 @@ export function resolveMigrationConfig( return { migrationApiBaseUrl: input.migrationApiBaseUrl, migrationOperator: MIGRATION_OPERATOR_ADDRESS, - migrationApiToken: input.migrationApiToken, } } diff --git a/packages/staking-migration-widget/src/widgetRuntimeContract.ts b/packages/staking-migration-widget/src/widgetRuntimeContract.ts index 74abab3..b044984 100644 --- a/packages/staking-migration-widget/src/widgetRuntimeContract.ts +++ b/packages/staking-migration-widget/src/widgetRuntimeContract.ts @@ -72,7 +72,6 @@ export interface StakingMigrationWidgetAdapterResult { export interface StakingMigrationWidgetAdapterFactoryInput { migrationApiBaseUrl?: string - migrationApiToken?: string } export type StakingMigrationWidgetAdapterFactory = ( @@ -85,7 +84,6 @@ export interface StakingMigrationWidgetProps { defaultTheme?: 'light' | 'dark' themeOverrides?: GoodWidgetThemeOverrides migrationApiBaseUrl?: string - migrationApiToken?: string onMigrationSuccess?: (detail: StakingMigrationSuccessDetail) => void onMigrationError?: (detail: StakingMigrationErrorDetail) => void adapterFactory?: StakingMigrationWidgetAdapterFactory From 9c09ff63cd9c5f1ccb02080b1f0bac569317bce4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 06:40:51 +0000 Subject: [PATCH 27/27] test: commit staking migration screenshots --- .../staking-migration-widget/states.spec.ts | 4 ++-- .../test-results/smw-01-empty-balance.png | Bin 0 -> 86364 bytes .../test-results/smw-02-ready.png | Bin 0 -> 98375 bytes .../test-results/smw-03-wrong-network.png | Bin 0 -> 98877 bytes .../test-results/smw-04-approval-pending.png | Bin 0 -> 90786 bytes .../test-results/smw-05-migrating.png | Bin 0 -> 92262 bytes .../test-results/smw-06-success.png | Bin 0 -> 93927 bytes .../test-results/smw-07-error.png | Bin 0 -> 97315 bytes 8 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 tests/widgets/staking-migration-widget/test-results/smw-01-empty-balance.png create mode 100644 tests/widgets/staking-migration-widget/test-results/smw-02-ready.png create mode 100644 tests/widgets/staking-migration-widget/test-results/smw-03-wrong-network.png create mode 100644 tests/widgets/staking-migration-widget/test-results/smw-04-approval-pending.png create mode 100644 tests/widgets/staking-migration-widget/test-results/smw-05-migrating.png create mode 100644 tests/widgets/staking-migration-widget/test-results/smw-06-success.png create mode 100644 tests/widgets/staking-migration-widget/test-results/smw-07-error.png diff --git a/tests/widgets/staking-migration-widget/states.spec.ts b/tests/widgets/staking-migration-widget/states.spec.ts index 917c1bb..64c0f79 100644 --- a/tests/widgets/staking-migration-widget/states.spec.ts +++ b/tests/widgets/staking-migration-widget/states.spec.ts @@ -60,7 +60,7 @@ test('StakingMigrationWidget approval pending notice', async ({ page }) => { test('StakingMigrationWidget migrating timeline', async ({ page }) => { await gotoStory(page, STORY_IDS.migrating) await expect(page.getByText('Migration journey')).toBeVisible() - await expect(page.getByText('Bridge received')).toBeVisible() + await expect(page.getByText('Bridge Received', { exact: true })).toBeVisible() await expect(page.getByRole('button', { name: 'Migrating' })).toBeDisabled() await page.screenshot({ path: 'tests/widgets/staking-migration-widget/test-results/smw-05-migrating.png', @@ -80,7 +80,7 @@ test('StakingMigrationWidget success state', async ({ page }) => { test('StakingMigrationWidget error state', async ({ page }) => { await gotoStory(page, STORY_IDS.error) - await expect(page.getByText('Failed')).toBeVisible() + await expect(page.getByText('Failed', { exact: true })).toBeVisible() await expect(page.getByText('Bridge finalization timeout')).toBeVisible() await expect(page.getByRole('button', { name: 'Retry migration' })).toHaveCount(1) await page.screenshot({ diff --git a/tests/widgets/staking-migration-widget/test-results/smw-01-empty-balance.png b/tests/widgets/staking-migration-widget/test-results/smw-01-empty-balance.png new file mode 100644 index 0000000000000000000000000000000000000000..0d0e65a0e606454d094493fa8e21df2aa6732e08 GIT binary patch literal 86364 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

WF8c714nPYOz z6qx^bz`@-y@hnSbZ(fiYSXnoj zA(6E*hM8APJ@E=@;%)I z&s?mL3k^YzjvW0bhQ~hS8vELK93H*(*#K^2dM_{^hC^Z^(Iqe!6>v@lwLeqwFDk_< z&`P&|^k7rY2e8W-BL82=@Aq8Y>(_?TQR$P}Q11gC1gJbCA=*dn-Wx<%#PwfPv|t#} zVJaw5Gh%j&|3wR`{}(MlqPqV5)8CUZL+O`<8i8~?fv|aKK;UHW7{Rnv;$N&?6oz(S zebid;dv;p+{6}vy%0s2TaSt0vvLNb7Ub!`sN1tL;Uj0F}`-eEVfVm$)a0X4p7n8JT zeoz_Yu8hs=p+=C*tDFLJ-%mY5W;8*!ZpL+N1Mu0a3k_$4gz%A&<4hw^OK^#wW*9W*+Yn)!^+ z_U#Y?y|8#kCNE}8gHS7Lc$xjabr9R^`B+NQacgC!=;%LKM4(Q=$+zM8(Z3A!_5c=v z&}sMBeCb)BkGboIy<%y(&Q7Nl+(YSg58+}zrJZxDiT=i+gb{J{?SX;~+Kp#^jE|7i+L;>*zi54bV!@;4n@l@yft|KxRYpQIz_O|4_4VqvV{|uSA)_EG<|ON1ePsKj?t<3f;Ig# z9*m`KY)P7X&Q;|H*hC%MeQ8$yhCOHwnOQm2#^ZiKLfma5@B52(y% zwO^Kn^V9Vhx@{Aa_xH)uIW08e1RPe7EUSz>C&um2`>v zG7Ghp`rj7faIbY;A3?e9doD784`(|4wGa7f7bKf&vtYOn=OKH0<}pSAD_d91vv*3? zG;$0gZi%GVdo zpOwpnWgStCm7ac%A9!+&6B&klf>!Us%gi@1sVpSCmT(zqkQ9 zs0sagY@=#@p^~K!vMrAg0eKDZCYdKO?GiCJ2mQ!swD9x$xi?>XQeDWFYNlgxj6qck zfcTm){I8Jgo%(ej8qP{V`gb;K>9N!(X3cvoNhY{R0_ z_ZrtjFIaQxhXT3}X)qh>Z!0Ci%gjofUMA?4_OqL}i&7ARAvvu#(AU~lQ&e2&-n{>+ zmAU0Eb$YUy|B0{LM1HI)Uvb7!llpGy8EBe;GW+2Ra?in?I3WE{VX!2a&b=ezP#~QnBbIIfzlGpGriWYdhGGthdwaeM(Rjy8ys+t*p zS8e+7nq9v=J;`9l`worrX&If#kQsYbvjhT_zGX3|Lw+>60%0KgKLb=`ZfXaRKBl-q zVB?0qdxF{1T|~4dhJ(E^pTq? z@Oj^)+b;e^H&^2@mQd8a9VMQzO=B8sJIH_ZIjZSUGp8nM9^bpGo$U2uX?dBg)ccCP ztHGjIX_zTPTSz`JicXBL$ubZPW81Y<0#XqkLl1CoSPvfDXx_DLRDMw=HF{+4)w1E% zI3&=|*ZY8l^TOf!zhTVSZrebo+Sl!cVrtD4nA_S}Nb+Z-&LVZ~ed$rtCO>TGTHD?y zVBzg7b~R7oaOOPA6~=eX8mwOnKyjMn?iPS_8hD3pOOHN;CramxZj^bmVCP+B?YeT6 z0i^HR<){~V0b1*mlA4-x!}8Ud^H?WmkO!UQTRf9gI0#3B>0}|tTO8Z_y6acUAjfNg zDw^9fftA0G#^QO_7pv`)<qF^~2?^i&t==B%!uGO&<== zCi)^0OHz2Eb(xWwE|S_)wCd$WpLwpQE-codyC&4cAl~tx6s~HB?(YpWgE1|{?v!B% z2pQL=$0hX@0rQXP9?Cd}QBNH9ZG<(%mL^bn-CA{NVPO|^O&VCNU#5iq*$*C0KG|OClWqA9)FY zQ18dv?eqX8xWMc+D3`NYWD)OA0+mR^p8GRig5CUdS&w z2CxD>P}%;7@ZWv5tbtqz@x5g_Ln6ViSeci6kY26n5X%n2cLa)tn?%XTlq~Y`DPKI* zTP`9~KEq&Zk;C3`F$y+73d2;4aFJGqCugG_TDQ+9h#qis;Nd_~RqFHJhH+%hk%#K7 zPgjUS33J-wmO5f8&b4U%<=d(J9}lospWhXY4=IH-+o_=h`<<)=g#fwNOQDmP7zL!L z0HQ!>ZeNI~?hFCRq@VppFGVcuIAziGHk$V-eI9Vv)yqfYeQ}oe2{V!nUmqcI^B!|( z5M$x4HdI4N_fu$Xkyz%JebNSyR0{&FfTAT0%x7)Rx6SAYeK@NvLps|AEhZ}Zk|JNq z<!MDZ>=mc&K7nO6r(ru`9s!4!Fk^!ab1P{8&b{#)Qpc*`Epb;n+BT2Cx~4^3yn! z)RAxmCANPYsaB&uIAiQ^Zo6x%`XlHHQ4B#a=KJeqq@zYtFR^N2J(39<#04*YYqj?|bY^YJ&HG~1v@G}Xv1WRu( z$6Y;DaUxlXo-9nsWa)1H`!sRvkEp_O=r;#{0gd;!ZSt~ez z*#gd7KZlRljEvkYzj)3*vQ=I=e2~70S}f$zzvkj9B@aLWgg}d4yv&Oz${Wd%zsW=i zPeAf0fn1KhRhvM1MPY+10SPxB0`(Fi-+aQ=;E!obsNsivlk>dz`3?m~(%)ScFO_*I z*nnOnc&ROQ?$%e`f{&mNvA?k3cw`^fKn9C7$es6;8NSU_T?NDp6s!p{T7M6`a@VJR z5aU?}TC`aVb%h0Wa1lHtu{>ti6p8)^j<_Wt<`Hjs%`PyT&8p8<)yxO$GxJ%!+m#zI zMizfZqO77oP%X8g_X`-$wSDINsOoM88;~$w^p)VaG|@*H;JJv>vot6~rHA3;6`9Ek zM@Fjfi;=|xRy-SEKb~GFLH3_jwAUZj+mWs(2`$#mJp7abzq>^&f+8joB$&D{Rw;E) z^bN8qB*}9`CXbS(&|E&t_kgZwt6PDu%sP%H1Q?YP7sr;pQfnF4M!mXCPtYL5Y+{riq}fIo^$HhT5w#`=`bVu7(|$yg%nk+w~y_<{M}zp85h z#Y9S~PeSCEx5H<6xSRFeMlPVn>dR6zvZJ>Z6N-?+^ zn%L0nFlnuecjR_40|qK7VG168TajbB?+7v-H1)$h0lJoN!?8XM^SR3)pvapSP%OF> z4=T4xCgVCYWOaR9@cH)?4Zurnt5Li1AxKsz6C+UkSDc8boWPp<-bqQ5qZOzy09WqS zQVvNx_6F9b>NIFx21&wq$J6cJ8A|#Uiz`G=2wg^Dx6GL&yY_e>ISkO<6Z~p-{^plh zP=4s`n@$ECKn%O`fQV?A)q&Yc&n#{)t9ci=RGcz|L`qq`csMz@taVAi)Ip{p9FEE z@}KemhwEQ&%c21h(I{sBcmcIDf*R_X z?R{g?EI?W6uIdfNma9a{j07f`N*gquNYG|U;0e*kT!Lll$oXVU!4`?;okmWNa*gI# zjG0KuKAUGUlYENrl5*i6jJxkW(FEJhnUwXN?vGD(;Zje$Z&v7!s&RlA`gLfe)Nh_y zhxqC|mi%%HKw`>d4&FGMcrh_lP0hde3=;lZy%Fz|Tj~+JG2etZvya;AN{8x+-f=$f zjc)Ept_`T}k>C0+7BJ@AncOPux+`haw3Hu=JeAp%MxLiyvXYZFxTo^f zae*V#zMZSAzW-(i#(bBpm?>J;`^*i68yYsah$)MuL5WN9N-V~-FWx+4tD&I zRbG;}G3BQCoD+EO?ff1jgSBqqY?sR?!i;`g{Kb(glDB*`6gwUB#A$C z3F)cY&`5oP?XgwQ+z{>7=5Jih*Qky2%R@4eU1w|J5iU4rK-HEWX|k*I@fu3c4lpI& zSSpJeJf>^l_u?^1QwTPr!)Jy(if}cyZaZZ`*ro~jhT?db4=3XfRi*6v4y6l*9*_cP^2UqadQlLY7teD znG&P3^Yc7#*S&9@#eKG28Oxj+ndv!mEIw=ZG&4KMmIkB5Fgm6;elbWqk%0|QPmWA8 z%^DwMH@!sONTVjdrbvfd28I#EJzx#T6?!@eA#p4+gC%Xrv3YXxFIwYe-OqOB!a23j zDF!N;vaai^Rh*~67+=j54{%Ja6GD`~-niapDkIkHWs2>HMy;8YbMFO5gjU9RVLW@- zkN{VY6sAJ%_fP0_@jgrY=Rmx9mZyE)z%G=>Gmwtmfzcc!&x=F8gUKXOA zyL(V39_8J0C^%5%gbm;tQ<|Y}#_VyH29AQwA=?_A$z#0<_tL-{tDVTX{+)u)rmAnRe=fTdCiS?KpPy?$O3+kP)7Gn2xvp4= zr}t|!o&U9%m4A8jG0V;LaHep%eCt^TO(m~c7LQu<8YdpP5fk_}dSATQF^{eUx15+0 z9lSxci>Wr^D(dTeFOlq%?j$*{s=A?-5o014=wYT5_N?W)YNp65`&gL@pX|FI+XV%Q%yyyRI0Me}EjNvKZ@l#gmn zO1$CcR$}!S48OA~9(VH0DO2nYM7{5~f19@b-=(xCu1x}SBVL73nR0l>?I-#_7WT@8 zh0{3^6286+0mmHn;%U<)V{3zx>5&6L3lXq&@;sgVclU+{f9*b7bVW{GUj)`k`W1pf zBI+s&_pK`E%X=k@KO4f0WCoOF8@g0%|KU%Pm_FD~ymgZKY=rhls>)Q_j!0MeLR133 z1m$`h|B+_Qv2}{=VD@zKh^=wnR`KvG4;_zLY(WpP!r>hm-#t5&$gty0&!w{1FfG%6 z1*;o%f81`Mt54Tr9MAe+9UqOZMvEg-IysJ7`hG+>vow>L$Ddj;hSSY5O0Aymnd!@R z$3FH-mks#$SSG8%(vfT`cQp?T546>A!m);7I2GxOIWc=!znW4ysma{#)Vejkf^ zskFjbL&8M6@s1X{ASA`iK8W0e`_F4!KN0WS&6LVjmaw(oi(CFGa5wZ(6AvRKusgsk zA%MXJS_Ft_ojHbb62+TY9v% zZ_4SUq{^;@4h`1vKe;!EQTc79NFK=2O_i2Z3kI)OkH)0MUO#O^#ICNj@_X|-E=X{A zK1yEZ>zAFIS?pIFk!|64=|$(CB3F8rE0O$rjR+{Ox=f9AJppUGR7mG)*Vzx1)oPt6 zB<8J^{&n+5tQL)tS8mTzY=5Pbn&F>u%7LQBYQY-@8&@=I2Z`0&a1FJK52plDW5tbn zCte-|BXocRHEK>~JgLw3y2(KMa;m47C5YDg{y#5U$T3l>4_j-u2*O$B^i0j! zy0}-ZDVAkqp}zIlR!dBL(p&lAq-j58?bJj@s^@$)SC!gV$@A6Lu1C9iu1$KyJEU^f zBc=EHJb`s2cE!mVvCn4}__3#B!IFswcy|`3Z$d|ou2`h~=}CW~`0dQ+Vf@DJ5x5Ql zVv1CXdK2ZA*7VV%OipI=6zvQ6O-KdUdtRig+KoGFi)#u$gx>6J-p9VcF=^n`22N$p z#HYX5|4%%D1Z)xE_AIGt{nxI!Isd%TChKHv9905sqy zCZsQ0h{~Q%vhdEDz2pY2xyed6_i=(=y{V^sN9>632VsK^qK{7WF9jk4nZ){$-4>wY zeeyQoB+(@#tot65mDmOMQ25%Vi*Jv!U|=xiRs^5`q6Y-e^?NXBx__yA;z<<(m&wXJ zHpx4XT3YlBquh_=dqf6=m=6EB$kQ)#QWnb^cfAnhlR-s?15S(SDU*4h`r4J-fQvW~ zT+x5riB5Z>A?A%8j;1zrUP`wp7&<4A#gkwNOaYyr_JRV(N?gi&{yB_UyGou-VE zR-7sUF+p}z{*WTIw@rnefLjpN^%9)L7QN*oPpG|7Hn=u4p7cYeG6fM5)z;YFTTv`N{pKiGGCGrKL{fIWkfJBQ%!ux4}y-cX)f7ZIHo*<~A z+N#m#qHC1;-Oy4;;{z{_#Nk~pNP$(cihVRUQ`SrdJ?Nz~i~`EC*;ihPWjr$x z#Huai{q3VX{{>MVqg~Fh&1bCCr;p%T6MYql08`I+X&RZ8<0?%v zFg#OpbxXpvrUpXuyYDv|%tqT5_%opu6%|W=Q{jR3Q%VI_6Q+{3n)3%3xtn#S@bdzh zm^m*sZ0yr9ztL!1BFr0Y6~BANovd~ryeAGMR|M=9a1#O-7Fvz=QW8A4olHE2M@^VA zq5+LPO^ud5KlHVk=dziR+E!4GOInxuYHgTk+e7J70`eL21_~G?BqWmWC>QoYPIjhk zKfZ!>{Ma;p^m~a@!h7MfSINb)@!Am6_*pqXW@0xKb(FI@GQpLT>8a zkQ2hOHD&_n5iI1ecAyb_*35dbaUYb#=#!XC^#yz@266v#d{|u?h;L?C@el8PJ>Mdx z4AVpuC+D_dXZ=H_AbhXZf+IB#bgb3_XLDcUtD=0)?=&MHHBe%a#t+o|BZa#I#X5og zT<9Y?TsH()wU&?cKiyaa%6iTvdHG@|8gi7hSe#27sq5xKjuZF|o8EO^Fx<(#R{&WbVzrk&=-h{Rjf`vkz4eKqYKP)s z#TT{nVp28$$ZAO4%B$UAS9T)#LWj%TS9pzDl ztVTC9;*hE6MO~pl#cCM|%nDxi6i+fU1JJOUOR<~SMfh1*bV@tBKBXcTU{Jd55zHIC zNlY}%<43{|wE?*8mUf?1^p5lp)U zg-R0G-t=kQ=c}yP^?lhH^sG2FM!sU=30Wp~(*S~|8>728f(-a{wiVi*#2}JgYe4K3 zS=n!AMV&=yE9`gj)OEEvMN8X|-4(j`&ANJgMJCyki?FVo1Na>@FGAcaU>1a%z@L{3 zD@f2}(x6hRJWm<~WRWfDeYT+3Sgt%1kD^9w6Mm=1sv8|xz8yIw=S=mA?Ws3y+4*Rx zXZq20gWPE%EB5!^+Y-FM$c!D+sNLy>l7RD6@NY&dPnWYUG%g@y&b#g$&|o{;agAQT zIFysoMy~t7a3I_HFKnRLImu1@7-qrHeUu|BbkmW3-w5(lkF7Yt!Akyly+?v8?rf*% zctg_%NN#AQ%jiQkhdEi99d3TkE>7O+@|e4WT4)#A*pY&xYokljfAMj8oXz9@fG*%Z zCh}*vj+)d2kWo1Tid%9OAJ9$+AF*0ZiSvv^iuxaJt?gBvQ;BEp#-c~pi~`PUBu2cq zz{enH=8lzdbI&|BPy?%TXPuau1x$C|HVhat`y!`1fAOJIYido+Kr@ z_mjmN&z>oM^p-|M(Tt9Pv5@~Rmf8W~GE%bxNLV0VoEHpXQD_{hJ>ff|bYiq(!NYg! zmz@9Bag{KoKeNjW@T52^YsWt}Ly=q~60R%l5SV*HoV~m)HN@}qv_c-m*(hJuZ&YQw zF_cm3Hlmw-H*F0+R|QRvXW(nU?~hMkx$-NJ{a5Xe$ym3YQ-;`b9?vtRx^Ez4zME63 znrd==)aNu?+i7anvN+tBCp70S!O}2YuA_h?z|>z@(yhi#$3MlP34ata$n3ChsV6p` z-V8u?DGdY~Cya^Zc8GfJ=~<=B@lO8@HJa61Y5Aj*i0#n=OCo-_Vy&#zlozjX4<++3 zo^47(lh;JP6*If|9Ui*k>xA^rYru9`%k`$O1AVKn{kMFmA*g-1XRZOR$vndC4BE1H zy=`b$wIW^M%?LyPOlI|X3U!rWb|>Z+#=iUkTz6xp2GUYi-qSEnx>C}&*E3pKl{Upc z=%)x1m?32buy>*^_RE(eid6*BOs!8dXB5`6Ld|2nXcrC>qZee-pNgJUyq~ZN? z+Xi$Vv#K<|MJs_wpIBwi49b}puX~uOk!fzGp_xlJOZ@QZkZ4f=HpO=zzvUL4Ax=qR zDoSl@qp2 zF;QfqP?OsldJjGtyY6ZPUu)fWnSE1_KPd8?r}H1on8EKs!3W)t3vBJ|nz7?f@y}`} z{l|J}4b56zqcV24H8rzoBe!#q90o3@^DhpMjl_T5HT1IO&zQ!phpUbD2^+{ z%#(HwYan;A=y;T_3lWg%)wZhKmAKgE%)G!N%rR?&7a`m5gWY}X&RY8U0(Jrp_UTLU z)iH~U;J~~_W7HC}UE>mxC0K@Oj}IoSrK4dl|vUvwXUtsG`zFQocB$2WOu18|!~rHdcV+E2>4&PEkD zKr;Lu`K*x$1MGb#s4L|y`MPuw`co7bz^Sn2Qt(7}nbS!h< z3ZJ40%!$sEAv-repS`{pxY>h0n?KJ?lNR81O?g_i6Xl9+|4o;P$L&^`ZWXt&*lMO5 zjGoSE7pa!)f0H2Nxz5Kpl5;L!WX_=19!=N@FQ|f6ZcQBd(c{0++3>C zT4lNCiL=&)-FD=CY(Ph1FqvvAFylinW21X9f>>^dom^MLKL<7S5RfM_lK+4gms5aW z>Gm1&x$Qi>KHRg2U4(*n>m4Mp+$KMnCn_^JC}%dzvnRl)XN=^r{2RF{QHHM!z>RKy zZoMrX$e8Zj!k^+mFt_Z)TQA08%${PhxLW*nen7!fG`DM(UJO|QZ*=v}BB}~W> z#iYu5;^iUOA^42hcJ$3D8NrWBg?>xHZF0f7tl@!Wi8TgjEW*F z6`7rMxo-E=aC^(YsBvv&@xp665q{Jzb9B-ZYyx+i_u|xhRQi-ORg+dY(+n%|j^JA9 z5#mob4vOI&iTe9+2i<(X$PrQOI^cQFR5NXDjKF1a;&6!(N2j~#VjCIz;C(G>+2JSVTiHO_p@!`*BA-I+$^kTqvq+Ao~@bEaW*Ib<&h6!>FwCWpGQN`%G zpZwv}pp(dMV))UXD|xdTcPIf6&@pJv3^!Ta6jOw0#ZZ#i#_=K~7&~Ew$GO}7^(rhp z{ELw1FHXHXI2GWwdePA26a!3o73UUad(7sT)C_6#a2+4VK?F|9Z84qrd(|Oe?12+jZsh zReDAMB2{F(^i6)Qrpi>+6@(+G9nUyKY&+>-l{m&VvvG+q(<&bD!P`!BeC=eLGx7!c zI^?*e9h#xv*ESOQr6L(@Rr4BES|oI&^Bxh221x#ECQBy?lY4H)Hs;{pu}=ipXR2G+ z1Ca47VUmR>Gj?CBx)7eplLvJ}@D0R%VD!ZRXwkmEdIFk6KWA16v=8zB<% z)L4qRni`viD;}9z8VNZ|Ki_|np_-1#6JMx0=<*+z&({lHlWXT=#cdU6VbT7!?P-4a zAjO8pMzT~%Z@`xuYy__)j*Ir>cjK1$OFuA6o}{v3XU}&>9*;QStb5{vBN+^(c>WOV zYUkyGa-b%%8lQKdcBTdozX>1z8z(k-TA#Ve#B>nO*xOX`CdJ;1Jo~QB8hg=>VlE<* z)yWS}r{>yZIUgy`%a{CA*fFM|XD=#Y(zC#1h`BIe4b3WIPEpJO5`}UN6eOEp(QlYh z4D%8T*n?HY%VA~NAgI5#6b#R-a0%V`?DKHQhoyfcYo{SYO<1gwpO|imv9?HKp<9=3 zVgkAUAoImPYWUfNA@-3Njr4sVrp~^s6e#r93>&aOiSo+*2fw7oDYiM zmpB+Pacx4bD`_1492`Lw$91=tO2Llnsp%HHHo9hc^!d`{WMYfvAvq?yvge15_~SWY zIQFN+-EB4Xni;4wm!TB32tTnSIVvLfDPak!d$fZa0O$&_1Yz0R?a0NhBAi*7XA$v1 zUJsB5#f5EAt*Qs!p#*TU>0OM65g=W1ZwoTH@kJ71G%gg-h$t*o5*4Zh!jg z&~<$)KZyr|jRqfzSNVXqagft$DZuBg=&H3*?J?lV-u5q?2qmk+-tZ%SPkY{qL*IV) z5Bxt!eD1mUQN^8C+>1A9a;|HSr|{6j@1*p<`cVIW?q5%@sgl1~*CsGxRl$Xwxd)E) zWP8O{{;)fyW283IHaw7CEYxA_ToLZsk>*&m#ChzI%Jylc#>V+t+-!G@ZnZQtVrocA{vH zLzL!lMX3GOHG4tbeu$VFZcdE3l{ItiP3RX*jruv$oBH`0mEXQiKoTuhny*~m)#&oE zDHM6J+Rew=8quKH>sW=wpvsCvBYs)>JE=tK{q6=uy0d{F#?(ssc%38S<+3vqXaXOb zd1txv9it7uL~>ZZ{a|6g#q4-PqmVHvyyynAgD<+mWbrA*)6(na-acjT2m{R1s zh00C;1K+&4%p=H)#vzL*X`Q98#wjwhbl~%W>hbUX*9W`SHH<#FU#u9;<%Ql*pWq7G zjS9z(zpjY+h09s`Xg`ozQ~rm-QkSf{xfqH3}i8|sd$lHB1SxvgwQ2)@!uGf%ai-?3xmE`Trgj2e9|4KIT|aP1Xhn3Cd#{j zP)tYpYszMk-&X5c>VYC}1em27;|5hGpfPHM#i$f(#YF3L; zIK9k!&Tq>r-c$z~PcAAfZrlBunAKuZ>eBWxoM-w)`$<3!08@!5ZW9~`3Y7po&s&H@ z?RMJ!bg)&cp6dy-K~hbJ@hpW?tZNO27!Q4|lb?WmmH~CMdi258hUlv~Lz@YvXK1F6 zMkT4`S(CqHipyHZCzo|4%0^xmGlp7a8EC{Da)6vkF!$1E3z9W*>|~Zxq)2%@`M2{d zO1p|_!I1-5R^&+j)~#grF3Sqi#Gp6V08j7X-9PSae{Thbl>M)Y1cqXbWm zs!^Muj?LCYYB`=>cnrk35jbXx3TXvnzOsrH!r zH;~JX0OJx7J;P*67Vm5`YujW-zT}32UJ;l>b7J8`VVEa$;mOP`sqjNC`QE$^X*0K- zdli5n3${WL~$wbMqwO`ES44ji&UFfErK#kA;kGBV>&`Wj->e~ zdLU2DNmZD`uL;HaY!TW|MJGbLp7b;V6Vx9q$%Jd1G(XO;n7u^tmavQ(E{kqh=A9Ln zbXAXZ7*<;_my+Gdqv=Bi3qBnOhbP#e6i506Lt?VZigC9MZcd_8Vib;`~7jQ^|#vQXbxSXREKIKuN2M*Ut-u)aS2W9Z;y<6vaWbZCSX)D3S-{1KF+2GC^z zr_(Z9|vnS3#PnvHlO~mkvdLQc1)M@NZ%t&|3 zKAsSU@X}TNeN!Rux-0ChO;sAhg4o%|PEy!%+*^T(wsNuYxXDwap5_rgp-oAffCP2c&656@xux`LLWFjOu;V_Yy7&KAbDk(to6>xV14KMBM z4Pwf3bv`YMM_BIgkB#4l?Y%Ecc}pHI*$sbNpEVrYv?DPLZ%s3& z)E6ojr7$s8FzIbm-CW}x$k51C!Cw2kxtnx7J!l}c>n8!j(V`O86*igQ41)~q&hympnT>%U?HYl<_k^XDT$Z%gF&K)W3B(3Uz zN??xiHA^Om^>%#=59zUdR2|H zzR}gek2TJ&oKc*SCFW5YN(!1#b!($2VWsyX#Ko5ke4@lxJkec}tz89+fBpK?z$kCP zgHvHQiZD~FDafTMRZ!w@?jLoJA2)X)G>9& z5q4@^Nn_14xJNV6Imu9!H1JuE|BJcJ8=+{*w7K_1bqpqap^;^lA0>>qlp=b zJAnp8HKWBL=IzMNGd>~*Sxs!#Nw^SjpVnDsS- zD`EOaYuRJl9tz|;Bk8nO_iT50lhj&OX`*)7M4>-z*6m&<2n!ZiTUg0iI-1xulZd>b z&K;USp1758DjKr z>38*xhIVTIYGoHwNEp4D0uZg4jU6#1iNWDqn+{TW`d;zGKNAANC5Uy7B||Anz+VkX z^|jr3iN_3Y|8=XdXQ~YEAtE9oCUA#8UNP*08)q7}-|Y3UI^#G&Bqr;teFtCXKB3Wx zoXVG|j`^_Wv%1`fn^9c+>rvBE%xSsH_M|@X?`LyG7IsIfKJIsSJU2f(CCKXa)Y2Y` zQ>;#8J8ExVF@RUS{&2cqsfKD@J@J^gUwP$vi9v(h_ZdYM&u!s?(XhR z>5xXcTWaYJDQTo@>F(}SdTEgEl9UulY2JJJy?;LNjlH?|nrr6F%$YNs>;z?8bcdoIfv1_!cjgG9jb+#6P0 z$rp{XpHaplUHs!de1K!b38$MZQ2#x1V&-u4AtU{xfdi*S%f%nXgF-_(xsSr9{4yPr z)~0a-s8!&Q=7ni_Cyi3nZ#dyV@&3R@*I||UiJ?L%anGgzS^Afbr?Es65LlJFv+jo4 z&V1k(hC&yWuI$=&dGynY|pIYc~TYfBF51 zp$$Z)Zzn{_8^C-YS9f_`w9}U1h(KL!X{7!$qpS0NQcdD#jzTG}YkO3iE!egFeI#-F zZ%!A4q#(DC!i(NnS+%9TT6^<)m|B8!75|8vg6arql{nI*cbvJ;WyTsVAo45pdZS-4 znrB?9b}Kal%?*Rhv%ki~6rXa{iO_<9zq_ZcB;fa%6=ykGQHm}ip)|KL;SK=|QT&xR z6moC>Xy^4!ZA?B4L_ZxAL-B-Ze!mAV+YPR$6*$kBsr|rFLv!xJeG|LzHLtjH8(#_A zZ=!uMLEHr|a&}Z!i(_whVY2Eh(aDlc=%L=rBiHLjeZ?~&8jgW9SE%PF2|B3t{Y++w z(sqsRppvn8t!}T9zPu|Q$DsnYoRzSrgQ$%Vy(cwz!19Aya19UqSe$f8m|`(q%>t$1 zy$;KNV?znE@DaEuELkx?_3~?;d8%Xqh`n*7hZ@%jXvK{?^J#Hd-do@<65ylc1GUOe z(6&@hj^Vv#?kCb^9)B%Dv}3l2W?({M{=&-OG!;mav3_qX#f4c;W+`oH8}$u%M5=;f zyN;4&2M@-I2y3m%(@OV^X1<79`zDP<{xe_jNc)C?`LL@Ug59V)>yU55m*83^`L1W} z>UekbFAtN>2;S0~gw<~M1 z1wLrxz>G#(W$%4aZ#}hL=GSJ9`iL?*eI9crmim-iB0r`F@%(Znt6w^j*7EUyA(9ik z#%OHNy-X7Et-P@~iG}=^NG*r?mJyn4{~cV=*mZu+JY;Mc+rRm~F~dFPyEw|Urvz`N z`};9t@jGq-{m3AvBg6cCKC^yS#+w8td>^cY9nWL`diU_q3Os2xECua4ciP#C{3}0nv+3k)QSH<4N76#1J2@5MM|n{_9skCKIZzM_WWcZV~%We(gE_Hg><52;X=rpg2~H>n=7`W?4g3?);0X*_nk$!!L#)tqdg}L9u&~ zc1mijW3Fr2)eb3g&$M%*F(0w&4>ituqwE%Zc~FHfeEyxe>E`UooX}^*{S7ZQDLm0T zyu+xUdcbbzv`Kx5i&T!o%1&^OZme!Qi=C$u&`bJv!4!(Gh z5#$z4kqTVLvYYdCKN3BT6BWr$7S*~U9Gq)gmnA_O3TI%`%doHyi%r}g8GgKIxMHoi7VC9$^HJ%rr0Y#f>&}dIRw+22 z>$$#ATHPSdg8wc$?0sWAXe!>G!}I~+316e8yk(@4M=;@mBp!4IA1KOBD|gF7c(}u3 zdHpTxk*koCX)*|Tfe|^0rn}hWMSLJ+9}+E7#*u9xMlibfq?x= zC=5IZq!J}kfPibd7rxfoan;p}REd!CKgoQ#)a$P+Z>p>*yt>u=tv~azW0ckM@{HzC zU?0I-KU}PmOvGTo;C;P?9VwqL`&VKh^O*?q}j(MybO#HxdT1KbAR4wz-QVZU9 z!RNn^lkN2a+|&D#YLYf?IYrN0>=-BlTFWG$T>n#e2&X4Jt^8NdJdAvX6>)(4`2zKK zVyRBso76<2#{>KQ!4OUJ2j?lRXS1D_T3SOKC|$U1`l}J5Nc~g5`E>Sy6EXT7??ccSGN_P-vV|@ z35m&NF${BIjY|^Paaf0d@`;EbQi{T3^aS^qX3+8%D@KUyU~r&eDb3xC zuY4g$WN1D`R+N1~$!Eba?(_#5vnFUz=&KP-_5kV`oaAODQg?N=mK0xFqF>>rSk zNClscf=!(39eOTsv(l@s6h0sMw$InrGG5$JRW)u^#C6H2&Eai)k@#>S>JoK8z&FfN z(6}Gp-<_9W()7EUm!)onSZ}0fCAwEJ8SNT|fFIYVeB+(oWXIrDWvFgwJ6BzYuM z!~bl_8MvN3sHgwZ-w=u(m+PR7?I+vjTn*3Bq|ItikuH@O)3$oYim(?O$4aFh1Ka`< z-r{(}e|T#57V9iqi_hAdQJKY83Tl|gWoHkkczv~gh9@>jXv3o~{j_)$A0z26Wy=w! zxO*Es%2FCl=o^nqdz==UxNI4(U2pIL{k^6BsdQc>D?|{kkR6;awNV?%ug?KJ>{loN zquY1kc!l74r)}J?_p3PU+Kx~Z%d84X9i&HBvr9BKSZ`KK^90II>;l*C?Jp*4JVC9UD_I#Ld5SorO-yA%rLnW7a-}xkV(Sk5yTP%qkqzN2X2?=qUBc zcg6U{iDp8)EF+&n^Q#Lt#q%N9i4zP0_n+PmhA}O;2ylg%!N5ek60V8n_Vep=Y?)s$ zF>N1-nOX;j^e`8PbtfhCA}|f++J2K#8cbyMJF~5k3k^&3sKnP37suRNvF;BryHi1HxP&>eN>$xOU52pzufy zH|n61O2_5L^|O!a0as#sBX^jB zig5@upMyZ58w`_qok@v>UO^1aZkw1wOpGDR*Fl5rSxy{|Vk%ds8J}D=GlFYI)zj2L zP3BgaFC2+srWR4q}2&A>Rn{*P@t(@tJ zTI?g+wW)~9t|czfh$E-Q+)JCQ9DYqNvsKH4*dY%Ne+S|z^lp*QUmHL3Fwc?c4|S;x zooa$w>W3hDi7}HskSTKTAd6~6+X`HF2}dqUvoYw%q2e0hr*6;a3!bwgw(^gSf0^Hh z2XJAZjzLFXwj8!@&*WC5T23gy>J{1Vh2N?xkSy^JHHe_O`l{cwVRoJlm@x8VJ-K}A zEc7o1=`Q-}4vGTSh*sGIAx+ztHRx2zmR0bN93uvGkFgK!X1wy!PXbsDc-s{v!^rUkc6J z##_NULZ5lmo})O8YWU2b3><5RQ{53;dX)QDm8{cjvLK^gAMuJsQfN?{LVH~U2M=mG zIFqw9G7~+oeU`QZ`GHqV3>b8X4o9c*RzAI>Eg^{8o;e%mvHP>Gh)tOiKtuZI2pdd& z;vWR(`>3=@^ZpM_EK<|q`hg7?wU%C#CD*|_$hblon>iclut-)^#_B012Mt%)b!E3_ z)UGEiZM3NlltI9s@j3k-e()(W6IZ7jHXZx}OXWhSXZHWt|7sb6bqTciYhgfTIExGQ| zx9$|PWVVH-7?@w=qXt%1R>lhdeQx@oG!blW;|K#txxIr5yoqtOUDVbey4uxIt^$o9 zuEz3n*ONCK`=+x!+EPsa*)o+qDWAgATQ)9?9k#X>IzIeP@DG=*3aEB`5N7tu!Cqq~ z)2Z%lKd<9@vJ7!)C^I#q26ZsfQ#e3E_jrkrHCe?miwAq);5pz@W#wvzo|E~8oFRyMF2;I=&+Sqd+QiF zMD%d~*^V}Bg1Gd{yci%tfEP!G2Bv~CuxKFNlkbC z*V;LmpTry*!Po9C^136bY50e$=X57)6tMjv*+NULJ8uqq8vZl>oD&Kr!j(7W^8l6c z(l^~R&#RQ(FpgEpLjJVKUd1TcBh1kZjzu3$FLoi?W&Oy�D}8F({!szTlqWB?C{F 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 literal 0 HcmV?d00001 diff --git a/tests/widgets/staking-migration-widget/test-results/smw-03-wrong-network.png b/tests/widgets/staking-migration-widget/test-results/smw-03-wrong-network.png new file mode 100644 index 0000000000000000000000000000000000000000..a27ddd1030c14bff05ae8d851e7b203a950dff98 GIT binary patch literal 98877 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

WF8c714nPYOz z6qx^bz`@-y@hnSbZ(fiYSXnoj zA(6E*hM8APJ@E=@;%)I z&s?mL3k^YzjvW0bhQ~hS8vELK93H*(*#K^2dM_{^hC^Z^(Iqe!6>v@lwLeqwFDk_< z&`P&|^k7rY2e8W-BL82=@Aq8Y>(_?TQR$P}Q11gC1gJbCA=*dn-Wx<%#PwfPv|t#} zVJaw5Gh%j&|3wR`{}(MlqPqV5)8CUZL+O`<8i8~?fv|aKK;UHW7{Rnv;$N&?6oz(S zebid;dv;p+{6}vy%0s2TaSt0vvLNb7Ub!`sN1tL;Uj0F}`-eEVfVm$)a0X4p7n8JT zeoz_Yu8hs=p+=C*tDFLJ-%mY5W;8*!ZpL+N1Mu0a3k_$4gz%A&<4hw^OK^#wW*9W*+Yn)!^+ z_U#Y?y|8#kCNE}8gHS7Lc$xjabr9R^`B+NQacgC!=;%LKM4(Q=$+zM8(Z3A!_5c=v z&}sMBeCb)BkGboIy<%y(&Q7Nl+(YSg58+}zrJZxDiT=i+gb{J{?SX;~+Kp#^jE|7i+L;>*zi54bV!@;4n@l@yft|KxRYpQIz_O|4_4VqvV{|uSA)_EG<|ON1ePsKj?t<3f;Ig# z9*m`KY)P7X&Q;|H*hC%MeQ8$yhCOHwnOQm2#^ZiKLfma5@B52(y% zwO^Kn^V9Vhx@{Aa_xH)uIW08e1RPe7EUSz>C&um2`>v zG7Ghp`rj7faIbY;A3?e9doD784`(|4wGa7f7bKf&vtYOn=OKH0<}pSAD_d91vv*3? zG;$0gZi%GVdo zpOwpnWgStCm7ac%A9!+&6B&klf>!Us%gi@1sVpSCmT(zqkQ9 zs0sagY@=#@p^~K!vMrAg0eKDZCYdKO?GiCJ2mQ!swD9x$xi?>XQeDWFYNlgxj6qck zfcTm){I8Jgo%(ej8qP{V`gb;K>9N!(X3cvoNhY{R0_ z_ZrtjFIaQxhXT3}X)qh>Z!0Ci%gjofUMA?4_OqL}i&7ARAvvu#(AU~lQ&e2&-n{>+ zmAU0Eb$YUy|B0{LM1HI)Uvb7!llpGy8EBe;GW+2Ra?in?I3WE{VX!2a&b=ezP#~QnBbIIfzlGpGriWYdhGGthdwaeM(Rjy8ys+t*p zS8e+7nq9v=J;`9l`worrX&If#kQsYbvjhT_zGX3|Lw+>60%0KgKLb=`ZfXaRKBl-q zVB?0qdxF{1T|~4dhJ(E^pTq? z@Oj^)+b;e^H&^2@mQd8a9VMQzO=B8sJIH_ZIjZSUGp8nM9^bpGo$U2uX?dBg)ccCP ztHGjIX_zTPTSz`JicXBL$ubZPW81Y<0#XqkLl1CoSPvfDXx_DLRDMw=HF{+4)w1E% zI3&=|*ZY8l^TOf!zhTVSZrebo+Sl!cVrtD4nA_S}Nb+Z-&LVZ~ed$rtCO>TGTHD?y zVBzg7b~R7oaOOPA6~=eX8mwOnKyjMn?iPS_8hD3pOOHN;CramxZj^bmVCP+B?YeT6 z0i^HR<){~V0b1*mlA4-x!}8Ud^H?WmkO!UQTRf9gI0#3B>0}|tTO8Z_y6acUAjfNg zDw^9fftA0G#^QO_7pv`)<qF^~2?^i&t==B%!uGO&<== zCi)^0OHz2Eb(xWwE|S_)wCd$WpLwpQE-codyC&4cAl~tx6s~HB?(YpWgE1|{?v!B% z2pQL=$0hX@0rQXP9?Cd}QBNH9ZG<(%mL^bn-CA{NVPO|^O&VCNU#5iq*$*C0KG|OClWqA9)FY zQ18dv?eqX8xWMc+D3`NYWD)OA0+mR^p8GRig5CUdS&w z2CxD>P}%;7@ZWv5tbtqz@x5g_Ln6ViSeci6kY26n5X%n2cLa)tn?%XTlq~Y`DPKI* zTP`9~KEq&Zk;C3`F$y+73d2;4aFJGqCugG_TDQ+9h#qis;Nd_~RqFHJhH+%hk%#K7 zPgjUS33J-wmO5f8&b4U%<=d(J9}lospWhXY4=IH-+o_=h`<<)=g#fwNOQDmP7zL!L z0HQ!>ZeNI~?hFCRq@VppFGVcuIAziGHk$V-eI9Vv)yqfYeQ}oe2{V!nUmqcI^B!|( z5M$x4HdI4N_fu$Xkyz%JebNSyR0{&FfTAT0%x7)Rx6SAYeK@NvLps|AEhZ}Zk|JNq z<!MDZ>=mc&K7nO6r(ru`9s!4!Fk^!ab1P{8&b{#)Qpc*`Epb;n+BT2Cx~4^3yn! z)RAxmCANPYsaB&uIAiQ^Zo6x%`XlHHQ4B#a=KJeqq@zYtFR^N2J(39<#04*YYqj?|bY^YJ&HG~1v@G}Xv1WRu( z$6Y;DaUxlXo-9nsWa)1H`!sRvkEp_O=r;#{0gd;!ZSt~ez z*#gd7KZlRljEvkYzj)3*vQ=I=e2~70S}f$zzvkj9B@aLWgg}d4yv&Oz${Wd%zsW=i zPeAf0fn1KhRhvM1MPY+10SPxB0`(Fi-+aQ=;E!obsNsivlk>dz`3?m~(%)ScFO_*I z*nnOnc&ROQ?$%e`f{&mNvA?k3cw`^fKn9C7$es6;8NSU_T?NDp6s!p{T7M6`a@VJR z5aU?}TC`aVb%h0Wa1lHtu{>ti6p8)^j<_Wt<`Hjs%`PyT&8p8<)yxO$GxJ%!+m#zI zMizfZqO77oP%X8g_X`-$wSDINsOoM88;~$w^p)VaG|@*H;JJv>vot6~rHA3;6`9Ek zM@Fjfi;=|xRy-SEKb~GFLH3_jwAUZj+mWs(2`$#mJp7abzq>^&f+8joB$&D{Rw;E) z^bN8qB*}9`CXbS(&|E&t_kgZwt6PDu%sP%H1Q?YP7sr;pQfnF4M!mXCPtYL5Y+{riq}fIo^$HhT5w#`=`bVu7(|$yg%nk+w~y_<{M}zp85h z#Y9S~PeSCEx5H<6xSRFeMlPVn>dR6zvZJ>Z6N-?+^ zn%L0nFlnuecjR_40|qK7VG168TajbB?+7v-H1)$h0lJoN!?8XM^SR3)pvapSP%OF> z4=T4xCgVCYWOaR9@cH)?4Zurnt5Li1AxKsz6C+UkSDc8boWPp<-bqQ5qZOzy09WqS zQVvNx_6F9b>NIFx21&wq$J6cJ8A|#Uiz`G=2wg^Dx6GL&yY_e>ISkO<6Z~p-{^plh zP=4s`n@$ECKn%O`fQV?A)q&Yc&n#{)t9ci=RGcz|L`qq`csMz@taVAi)Ip{p9FEE z@}KemhwEQ&%c21h(I{sBcmcIDf*R_X z?R{g?EI?W6uIdfNma9a{j07f`N*gquNYG|U;0e*kT!Lll$oXVU!4`?;okmWNa*gI# zjG0KuKAUGUlYENrl5*i6jJxkW(FEJhnUwXN?vGD(;Zje$Z&v7!s&RlA`gLfe)Nh_y zhxqC|mi%%HKw`>d4&FGMcrh_lP0hde3=;lZy%Fz|Tj~+JG2etZvya;AN{8x+-f=$f zjc)Ept_`T}k>C0+7BJ@AncOPux+`haw3Hu=JeAp%MxLiyvXYZFxTo^f zae*V#zMZSAzW-(i#(bBpm?>J;`^*i68yYsah$)MuL5WN9N-V~-FWx+4tD&I zRbG;}G3BQCoD+EO?ff1jgSBqqY?sR?!i;`g{Kb(glDB*`6gwUB#A$C z3F)cY&`5oP?XgwQ+z{>7=5Jih*Qky2%R@4eU1w|J5iU4rK-HEWX|k*I@fu3c4lpI& zSSpJeJf>^l_u?^1QwTPr!)Jy(if}cyZaZZ`*ro~jhT?db4=3XfRi*6v4y6l*9*_cP^2UqadQlLY7teD znG&P3^Yc7#*S&9@#eKG28Oxj+ndv!mEIw=ZG&4KMmIkB5Fgm6;elbWqk%0|QPmWA8 z%^DwMH@!sONTVjdrbvfd28I#EJzx#T6?!@eA#p4+gC%Xrv3YXxFIwYe-OqOB!a23j zDF!N;vaai^Rh*~67+=j54{%Ja6GD`~-niapDkIkHWs2>HMy;8YbMFO5gjU9RVLW@- zkN{VY6sAJ%_fP0_@jgrY=Rmx9mZyE)z%G=>Gmwtmfzcc!&x=F8gUKXOA zyL(V39_8J0C^%5%gbm;tQ<|Y}#_VyH29AQwA=?_A$z#0<_tL-{tDVTX{+)u)rmAnRe=fTdCiS?KpPy?$O3+kP)7Gn2xvp4= zr}t|!o&U9%m4A8jG0V;LaHep%eCt^TO(m~c7LQu<8YdpP5fk_}dSATQF^{eUx15+0 z9lSxci>Wr^D(dTeFOlq%?j$*{s=A?-5o014=wYT5_N?W)YNp65`&gL@pX|FI+XV%Q%yyyRI0Me}EjNvKZ@l#gmn zO1$CcR$}!S48OA~9(VH0DO2nYM7{5~f19@b-=(xCu1x}SBVL73nR0l>?I-#_7WT@8 zh0{3^6286+0mmHn;%U<)V{3zx>5&6L3lXq&@;sgVclU+{f9*b7bVW{GUj)`k`W1pf zBI+s&_pK`E%X=k@KO4f0WCoOF8@g0%|KU%Pm_FD~ymgZKY=rhls>)Q_j!0MeLR133 z1m$`h|B+_Qv2}{=VD@zKh^=wnR`KvG4;_zLY(WpP!r>hm-#t5&$gty0&!w{1FfG%6 z1*;o%f81`Mt54Tr9MAe+9UqOZMvEg-IysJ7`hG+>vow>L$Ddj;hSSY5O0Aymnd!@R z$3FH-mks#$SSG8%(vfT`cQp?T546>A!m);7I2GxOIWc=!znW4ysma{#)Vejkf^ zskFjbL&8M6@s1X{ASA`iK8W0e`_F4!KN0WS&6LVjmaw(oi(CFGa5wZ(6AvRKusgsk zA%MXJS_Ft_ojHbb62+TY9v% zZ_4SUq{^;@4h`1vKe;!EQTc79NFK=2O_i2Z3kI)OkH)0MUO#O^#ICNj@_X|-E=X{A zK1yEZ>zAFIS?pIFk!|64=|$(CB3F8rE0O$rjR+{Ox=f9AJppUGR7mG)*Vzx1)oPt6 zB<8J^{&n+5tQL)tS8mTzY=5Pbn&F>u%7LQBYQY-@8&@=I2Z`0&a1FJK52plDW5tbn zCte-|BXocRHEK>~JgLw3y2(KMa;m47C5YDg{y#5U$T3l>4_j-u2*O$B^i0j! zy0}-ZDVAkqp}zIlR!dBL(p&lAq-j58?bJj@s^@$)SC!gV$@A6Lu1C9iu1$KyJEU^f zBc=EHJb`s2cE!mVvCn4}__3#B!IFswcy|`3Z$d|ou2`h~=}CW~`0dQ+Vf@DJ5x5Ql zVv1CXdK2ZA*7VV%OipI=6zvQ6O-KdUdtRig+KoGFi)#u$gx>6J-p9VcF=^n`22N$p z#HYX5|4%%D1Z)xE_AIGt{nxI!Isd%TChKHv9905sqy zCZsQ0h{~Q%vhdEDz2pY2xyed6_i=(=y{V^sN9>632VsK^qK{7WF9jk4nZ){$-4>wY zeeyQoB+(@#tot65mDmOMQ25%Vi*Jv!U|=xiRs^5`q6Y-e^?NXBx__yA;z<<(m&wXJ zHpx4XT3YlBquh_=dqf6=m=6EB$kQ)#QWnb^cfAnhlR-s?15S(SDU*4h`r4J-fQvW~ zT+x5riB5Z>A?A%8j;1zrUP`wp7&<4A#gkwNOaYyr_JRV(N?gi&{yB_UyGou-VE zR-7sUF+p}z{*WTIw@rnefLjpN^%9)L7QN*oPpG|7Hn=u4p7cYeG6fM5)z;YFTTv`N{pKiGGCGrKL{fIWkfJBQ%!ux4}y-cX)f7ZIHo*<~A z+N#m#qHC1;-Oy4;;{z{_#Nk~pNP$(cihVRUQ`SrdJ?Nz~i~`EC*;ihPWjr$x z#Huai{q3VX{{>MVqg~Fh&1bCCr;p%T6MYql08`I+X&RZ8<0?%v zFg#OpbxXpvrUpXuyYDv|%tqT5_%opu6%|W=Q{jR3Q%VI_6Q+{3n)3%3xtn#S@bdzh zm^m*sZ0yr9ztL!1BFr0Y6~BANovd~ryeAGMR|M=9a1#O-7Fvz=QW8A4olHE2M@^VA zq5+LPO^ud5KlHVk=dziR+E!4GOInxuYHgTk+e7J70`eL21_~G?BqWmWC>QoYPIjhk zKfZ!>{Ma;p^m~a@!h7MfSINb)@!Am6_*pqXW@0xKb(FI@GQpLT>8a zkQ2hOHD&_n5iI1ecAyb_*35dbaUYb#=#!XC^#yz@266v#d{|u?h;L?C@el8PJ>Mdx z4AVpuC+D_dXZ=H_AbhXZf+IB#bgb3_XLDcUtD=0)?=&MHHBe%a#t+o|BZa#I#X5og zT<9Y?TsH()wU&?cKiyaa%6iTvdHG@|8gi7hSe#27sq5xKjuZF|o8EO^Fx<(#R{&WbVzrk&=-h{Rjf`vkz4eKqYKP)s z#TT{nVp28$$ZAO4%B$UAS9T)#LWj%TS9pzDl ztVTC9;*hE6MO~pl#cCM|%nDxi6i+fU1JJOUOR<~SMfh1*bV@tBKBXcTU{Jd55zHIC zNlY}%<43{|wE?*8mUf?1^p5lp)U zg-R0G-t=kQ=c}yP^?lhH^sG2FM!sU=30Wp~(*S~|8>728f(-a{wiVi*#2}JgYe4K3 zS=n!AMV&=yE9`gj)OEEvMN8X|-4(j`&ANJgMJCyki?FVo1Na>@FGAcaU>1a%z@L{3 zD@f2}(x6hRJWm<~WRWfDeYT+3Sgt%1kD^9w6Mm=1sv8|xz8yIw=S=mA?Ws3y+4*Rx zXZq20gWPE%EB5!^+Y-FM$c!D+sNLy>l7RD6@NY&dPnWYUG%g@y&b#g$&|o{;agAQT zIFysoMy~t7a3I_HFKnRLImu1@7-qrHeUu|BbkmW3-w5(lkF7Yt!Akyly+?v8?rf*% zctg_%NN#AQ%jiQkhdEi99d3TkE>7O+@|e4WT4)#A*pY&xYokljfAMj8oXz9@fG*%Z zCh}*vj+)d2kWo1Tid%9OAJ9$+AF*0ZiSvv^iuxaJt?gBvQ;BEp#-c~pi~`PUBu2cq zz{enH=8lzdbI&|BPy?%TXPuau1x$C|HVhat`y!`1fAOJIYido+Kr@ z_mjmN&z>oM^p-|M(Tt9Pv5@~Rmf8W~GE%bxNLV0VoEHpXQD_{hJ>ff|bYiq(!NYg! zmz@9Bag{KoKeNjW@T52^YsWt}Ly=q~60R%l5SV*HoV~m)HN@}qv_c-m*(hJuZ&YQw zF_cm3Hlmw-H*F0+R|QRvXW(nU?~hMkx$-NJ{a5Xe$ym3YQ-;`b9?vtRx^Ez4zME63 znrd==)aNu?+i7anvN+tBCp70S!O}2YuA_h?z|>z@(yhi#$3MlP34ata$n3ChsV6p` z-V8u?DGdY~Cya^Zc8GfJ=~<=B@lO8@HJa61Y5Aj*i0#n=OCo-_Vy&#zlozjX4<++3 zo^47(lh;JP6*If|9Ui*k>xA^rYru9`%k`$O1AVKn{kMFmA*g-1XRZOR$vndC4BE1H zy=`b$wIW^M%?LyPOlI|X3U!rWb|>Z+#=iUkTz6xp2GUYi-qSEnx>C}&*E3pKl{Upc z=%)x1m?32buy>*^_RE(eid6*BOs!8dXB5`6Ld|2nXcrC>qZee-pNgJUyq~ZN? z+Xi$Vv#K<|MJs_wpIBwi49b}puX~uOk!fzGp_xlJOZ@QZkZ4f=HpO=zzvUL4Ax=qR zDoSl@qp2 zF;QfqP?OsldJjGtyY6ZPUu)fWnSE1_KPd8?r}H1on8EKs!3W)t3vBJ|nz7?f@y}`} z{l|J}4b56zqcV24H8rzoBe!#q90o3@^DhpMjl_T5HT1IO&zQ!phpUbD2^+{ z%#(HwYan;A=y;T_3lWg%)wZhKmAKgE%)G!N%rR?&7a`m5gWY}X&RY8U0(Jrp_UTLU z)iH~U;J~~_W7HC}UE>mxC0K@Oj}IoSrK4dl|vUvwXUtsG`zFQocB$2WOu18|!~rHdcV+E2>4&PEkD zKr;Lu`K*x$1MGb#s4L|y`MPuw`co7bz^Sn2Qt(7}nbS!h< z3ZJ40%!$sEAv-repS`{pxY>h0n?KJ?lNR81O?g_i6Xl9+|4o;P$L&^`ZWXt&*lMO5 zjGoSE7pa!)f0H2Nxz5Kpl5;L!WX_=19!=N@FQ|f6ZcQBd(c{0++3>C zT4lNCiL=&)-FD=CY(Ph1FqvvAFylinW21X9f>>^dom^MLKL<7S5RfM_lK+4gms5aW z>Gm1&x$Qi>KHRg2U4(*n>m4Mp+$KMnCn_^JC}%dzvnRl)XN=^r{2RF{QHHM!z>RKy zZoMrX$e8Zj!k^+mFt_Z)TQA08%${PhxLW*nen7!fG`DM(UJO|QZ*=v}BB}~W> z#iYu5;^iUOA^42hcJ$3D8NrWBg?>xHZF0f7tl@!Wi8TgjEW*F z6`7rMxo-E=aC^(YsBvv&@xp665q{Jzb9B-ZYyx+i_u|xhRQi-ORg+dY(+n%|j^JA9 z5#mob4vOI&iTe9+2i<(X$PrQOI^cQFR5NXDjKF1a;&6!(N2j~#VjCIz;C(G>+2JSVTiHO_p@!`*BA-I+$^kTqvq+Ao~@bEaW*Ib<&h6!>FwCWpGQN`%G zpZwv}pp(dMV))UXD|xdTcPIf6&@pJv3^!Ta6jOw0#ZZ#i#_=K~7&~Ew$GO}7^(rhp z{ELw1FHXHXI2GWwdePA26a!3o73UUad(7sT)C_6#a2+4VK?F|9Z84qrd(|Oe?12+jZsh zReDAMB2{F(^i6)Qrpi>+6@(+G9nUyKY&+>-l{m&VvvG+q(<&bD!P`!BeC=eLGx7!c zI^?*e9h#xv*ESOQr6L(@Rr4BES|oI&^Bxh221x#ECQBy?lY4H)Hs;{pu}=ipXR2G+ z1Ca47VUmR>Gj?CBx)7eplLvJ}@D0R%VD!ZRXwkmEdIFk6KWA16v=8zB<% z)L4qRni`viD;}9z8VNZ|Ki_|np_-1#6JMx0=<*+z&({lHlWXT=#cdU6VbT7!?P-4a zAjO8pMzT~%Z@`xuYy__)j*Ir>cjK1$OFuA6o}{v3XU}&>9*;QStb5{vBN+^(c>WOV zYUkyGa-b%%8lQKdcBTdozX>1z8z(k-TA#Ve#B>nO*xOX`CdJ;1Jo~QB8hg=>VlE<* z)yWS}r{>yZIUgy`%a{CA*fFM|XD=#Y(zC#1h`BIe4b3WIPEpJO5`}UN6eOEp(QlYh z4D%8T*n?HY%VA~NAgI5#6b#R-a0%V`?DKHQhoyfcYo{SYO<1gwpO|imv9?HKp<9=3 zVgkAUAoImPYWUfNA@-3Njr4sVrp~^s6e#r93>&aOiSo+*2fw7oDYiM zmpB+Pacx4bD`_1492`Lw$91=tO2Llnsp%HHHo9hc^!d`{WMYfvAvq?yvge15_~SWY zIQFN+-EB4Xni;4wm!TB32tTnSIVvLfDPak!d$fZa0O$&_1Yz0R?a0NhBAi*7XA$v1 zUJsB5#f5EAt*Qs!p#*TU>0OM65g=W1ZwoTH@kJ71G%gg-h$t*o5*4Zh!jg z&~<$)KZyr|jRqfzSNVXqagft$DZuBg=&H3*?J?lV-u5q?2qmk+-tZ%SPkY{qL*IV) z5Bxt!eD1mUQN^8C+>1A9a;|HSr|{6j@1*p<`cVIW?q5%@sgl1~*CsGxRl$Xwxd)E) zWP8O{{;)fyW283IHaw7CEYxA_ToLZsk>*&m#ChzI%Jylc#>V+t+-!G@ZnZQtVrocA{vH zLzL!lMX3GOHG4tbeu$VFZcdE3l{ItiP3RX*jruv$oBH`0mEXQiKoTuhny*~m)#&oE zDHM6J+Rew=8quKH>sW=wpvsCvBYs)>JE=tK{q6=uy0d{F#?(ssc%38S<+3vqXaXOb zd1txv9it7uL~>ZZ{a|6g#q4-PqmVHvyyynAgD<+mWbrA*)6(na-acjT2m{R1s zh00C;1K+&4%p=H)#vzL*X`Q98#wjwhbl~%W>hbUX*9W`SHH<#FU#u9;<%Ql*pWq7G zjS9z(zpjY+h09s`Xg`ozQ~rm-QkSf{xfqH3}i8|sd$lHB1SxvgwQ2)@!uGf%ai-?3xmE`Trgj2e9|4KIT|aP1Xhn3Cd#{j zP)tYpYszMk-&X5c>VYC}1em27;|5hGpfPHM#i$f(#YF3L; zIK9k!&Tq>r-c$z~PcAAfZrlBunAKuZ>eBWxoM-w)`$<3!08@!5ZW9~`3Y7po&s&H@ z?RMJ!bg)&cp6dy-K~hbJ@hpW?tZNO27!Q4|lb?WmmH~CMdi258hUlv~Lz@YvXK1F6 zMkT4`S(CqHipyHZCzo|4%0^xmGlp7a8EC{Da)6vkF!$1E3z9W*>|~Zxq)2%@`M2{d zO1p|_!I1-5R^&+j)~#grF3Sqi#Gp6V08j7X-9PSae{Thbl>M)Y1cqXbWm zs!^Muj?LCYYB`=>cnrk35jbXx3TXvnzOsrH!r zH;~JX0OJx7J;P*67Vm5`YujW-zT}32UJ;l>b7J8`VVEa$;mOP`sqjNC`QE$^X*0K- zdli5n3${WL~$wbMqwO`ES44ji&UFfErK#kA;kGBV>&`Wj->e~ zdLU2DNmZD`uL;HaY!TW|MJGbLp7b;V6Vx9q$%Jd1G(XO;n7u^tmavQ(E{kqh=A9Ln zbXAXZ7*<;_my+Gdqv=Bi3qBnOhbP#e6i506Lt?VZigC9MZcd_8Vib;`~7jQ^|#vQXbxSXREKIKuN2M*Ut-u)aS2W9Z;y<6vaWbZCSX)D3S-{1KF+2GC^z zr_(Z9|vnS3#PnvHlO~mkvdLQc1)M@NZ%t&|3 zKAsSU@X}TNeN!Rux-0ChO;sAhg4o%|PEy!%+*^T(wsNuYxXDwap5_rgp-oAffCP2c&656@xux`LLWFjOu;V_Yy7&KAbDk(to6>xV14KMBM z4Pwf3bv`YMM_BIgkB#4l?Y%Ecc}pHI*$sbNpEVrYv?DPLZ%s3& z)E6ojr7$s8FzIbm-CW}x$k51C!Cw2kxtnx7J!l}c>n8!j(V`O86*igQ41)~q&hympnT>%U?HYl<_k^XDT$Z%gF&K)W3B(3Uz zN??xiHA^Om^>%#=59zUdR2|H zzR}gek2TJ&oKc*SCFW5YN(!1#b!($2VWsyX#Ko5ke4@lxJkec}tz89+fBpK?z$kCP zgHvHQiZD~FDafTMRZ!w@?jLoJA2)X)G>9& z5q4@^Nn_14xJNV6Imu9!H1JuE|BJcJ8=+{*w7K_1bqpqap^;^lA0>>qlp=b zJAnp8HKWBL=IzMNGd>~*Sxs!#Nw^SjpVnDsS- zD`EOaYuRJl9tz|;Bk8nO_iT50lhj&OX`*)7M4>-z*6m&<2n!ZiTUg0iI-1xulZd>b z&K;USp1758DjKr z>38*xhIVTIYGoHwNEp4D0uZg4jU6#1iNWDqn+{TW`d;zGKNAANC5Uy7B||Anz+VkX z^|jr3iN_3Y|8=XdXQ~YEAtE9oCUA#8UNP*08)q7}-|Y3UI^#G&Bqr;teFtCXKB3Wx zoXVG|j`^_Wv%1`fn^9c+>rvBE%xSsH_M|@X?`LyG7IsIfKJIsSJU2f(CCKXa)Y2Y` zQ>;#8J8ExVF@RUS{&2cqsfKD@J@J^gUwP$viI#)i0XaXMnFR8ZfT^u zC8bkAVF_6}mQLvs>5y7dy1NBQNnz<&U_p>>QM!2#{NDe=clJK#+>_T_GsDM2Ucz6~ z`0i1B{_Fj^O3eAkzf-(dO+Zq)lY%9BJ9$xIYeP{<8e#^-VM1dnAHf+xi_@OxU-DZqo0$_Nq zK9rrP6b$hk(#K*RoeS(-qp}f)v5e(uZ%yu-I_$nmPg68>;In8t`lGg;|A9qWQQ|;E z;rp1iNn9UJ1vI1)KB40Dt{5khI1B*qw{7$s7C3HM%hVG#Z1S+=e}g`PWg7s;s?nKw zInaFQjRqh143C`EM}sSz@tGTEM8pq&4LVh`Z!>)8_Q-khXspTfy9EoEo3|Pk^W>Ez zBnS!@Azu$a2A)tZqo>x7v_oQq->&HH{js%PsZc{NP+SHL3d8jml6c%m0xof_@@A?JL*+Iywo2G z!f%{(saP+6A87U=*z9XiOibYce~lzF6!^a@=5lfo?~^M1#f27lo=7vQ)w8 z#jZe{PhkV`Q)uBN*qoU$0D2jeW0vwd55iMF*u_ZV1eD^2orQIHEw3y{X35F0a{;XK z%|ltLE9=A6`|LL~^Md|56u5ib;f+9t#LSVE;Xw+JBol5e&BcXUj%BK^=o)qRc|^R0 z#CGf^DfMr|3Mi_rN>htBjV66$tbG#)BhJm>9;uP|1lQ}jAw>0hQx3T{!tt&pub*`- zp6sm;{S{;f4U*2ajWisS+5FO6z|}65tRN1-KFE_7M4Q@vIlhahH&jfV=3$y_fPz?5 z%Jash1$s16%YR%I{HSHL%Lr=Ztqm_>F%UFk=d4Y>p!8$EmdPzuxB3lwZLRVJ5JXz? zM;}cDxl%}^x=;bjP+O?@MCdroGz~I+^2IOqkvtwb)5(YS>rq8 za^tdY!-d*axxdaX$d?O(R0UIQIP+RSm%3loL=bIyUs;rGaqLsc22(@i+xgVchPrCa z%Zl`eh7(cB51JAw@S=9HbMZd{PyINM$(W(z#umd*K+^t=UrUz1jooi{is#>J5Bn=% zX(+BPJHl5W@&i$*e?A?w9>}min6+5<#x$A0&whv?LvPBWUpb$l@!8unoO0uHXA;=@ zQaBAJBH*DPm`&`I_SDAeVqi6EYz1iM>(rj1v8SU=}3kgN) zOcqeNMNHB|<4b`)B0MBzIhl9q58kNSxaA-J`{xZV74Ln>Gbp57f%pSZ$$9F$5h33VGW;BMlpTYTaifr4$+)OcR@flL^}pwRe>A z+CAgrDCWk6dyc`NGDZ{5f;|*d{52)rP>4%j==cLK8Ri*2_ex=p|Sf7>*zF!DF=#xZx z&K`FJ@rSjngL5Cgo!K`#Gkfh&%dt(G&Jg_<{IOR2v$cBJxi~C@^0g+#HHTo(0v+F7 zEx!-F)fvVV^zW?0MkL#IL*eQ)fj5Ly;uI$tg^fW4-B_?UVY~g)wwls}MyE=zpg>rx zkiTUuRs~iPQ#yqZ&>|<>a2cC4OZI}&K#^VjYlO2j$7xn%3!Pgqn_~r?fI<&4;IJPF zg@N0F3{sS;z0j&ocu=(+e@(4qg(Qv0t%A?7eoswlLwQyH$;JCEgGt5jLtK`}hfF)7 zleng^yPpdIgNV3<^5^0od5#dXC04m>o1Gg~Ezsdh4p=9Y>YDSQgykK3rGlcR4w^6A z`>EnsORcED#O7B_}HR4^VE=R{{ysMzK>|kDnTD}@N-!aG64DW{=wc! zr#P)HGg3?CV3J!grpHizR(MRbLeetxwc{~|T*#+;^7(6`gH|AX27k^Z zit8Yg{1<9A^YMZyz|H}mlgG?N$EPI3ppn>3fMyD`qU%M=vek^G7B0Dl&x=c_K7BTf z@F7oNZ9KqIQ@Y2_YX$!aq6l z>NmgibmqhxH*8f3an>NH^ar~T(LdCZa8HrQMM%6$m!9d5eeXXh|E%}9g}>&zuM>7I z>wmK345()p&cS)w6AG!lQV=d|Gs!mVXhy{&0GDx>g3Rf}M+3N~P%I z0zc|JN|Ni1U)JvxcR9fu_-)xvUC&4Z{k`PQHQSGpRKqC{FSh^9H8UEi{F(-8*pHzA zl3w?b;|aR!r7pNf|94^NsU3wBp=kxCHbkGLYMm0+Uu#;;bPLH%;m7f^P%~6xr;e&; z@3mN_ty24UQ(Hsiw+CimjhngeIQ%@Oj2@!)lc4%G)Ga&-4_09nGNrm*8<8?X4pQ${ z>4@=*lgjAzw2Zj@oLiZ{ER)+ylrX|7diCace<(ZLMU+3p6bUK(k#J2py_s8^Wy^6- zKwx`A#r~~-K%Zdtr{0*XzVdjpq0jJigIr^T@5oMy%h{;{XDDxR2In<1>-QKfE|-fG zzs@U;9SD8SHY5Q@Y5niL61t+8++O)C}&Mw@}(!hVkRQbEm5O-!}fg`(Xo zaC*F>3?<^onWZNoos2S9?^ROBFYWq1{!Ehf*pXc>$Hj1zlBG|&N(YYu@?BuJo%=r?ix9Cdz>i^Cq2&UOoqNKz}c<#w}-Ux)M zYJx)^^10rJ&<%;*tj4&=LO(BtX}wumH74c*XHdW4>J%R_Zy|%L)1-H{mMQru+uMVb zen@AMdRhH335!oFc|32 z8aQ|lX{sIQ)lY~S>*^h+h4yp4Eo(-gb{6qwV>f~!yAEZi=%adFL-(Z4YD5}sOXnG_ zKl@0KZ}%X(J}V9@7l+CSxu$(OsCL=c=MpH|s?>8L19g(PuD;r5%>?ZSea37egtsn{ z?fL$N5WQJny?z;>p%tm(G3x6rx=|h30H4;I`6V-< z)L)3zPg3_DpxvKeYDDzO0ZcSEj>w-F z_x*!WeQ(s4nV$b)ip6Z$`L%5W#i@QN#hLBk6$~!ZAmaFnxl^DdrC@a%lZ8tn;fh%A z8nWvOO&w~efuxg*q?@N*q4nQJWRQS5k<*}UgqlajaO=6#~$AkGK7o%)R38F?faRZ?H@s3G`DA9}hy;vsd-gEkO`5;?sJeE?)vktq0Vo9V z*O&ft-G9QnX)@KNE64uSmc8Uw;{ffYWj!3W^X*%{MEKmLV>6B_^hfkne0S>6|MU4=7|Q52NwchzlE^(&}p(pCb4*Bg}0Bi-9n; z@WHhga9#5T<;BxS9928-GV~bBdtU+=S9@>OUrJr7BJXcTYhAkD7IS!>9S{>DA#2=8xNjVaYFd)0K-^7+pd&0-4q zkLDB5F|$eLfo(Rv=t20j@ODOOqaE7%x@u)~!_o!Cu{KHcWDkWTmv#i`Yr@5k?<0Ln zbMCSt*I(?g%KD>5sxa&f6G2gch9dDjgq`8_(646_aSBGl3rdx97reW#+GNTA zLdnY7N$&WF`PChX)i5b~p6-HA99jNgvELOZ_w|`?HlEdZpuuIU|E;Hw`2@olNGwbJ z$KmZ(C_ zwjhMbY|YA#?dx{-@M54&W@DBoNnKV`hwxo=7xBb-|F;4ZjaN~9LBHN7|rz z@kwm8jeFG=0vtVsva69@y>C!sM=*1cCi-hu_Ul%a5|uV^a9&fahh^R80LDJf+}-TC zG+Ycrnf>9cpB>rwu~yGJIlf}#jSZE5RdIc7T{e4GdKwx&()T3?gG!1@AgWS+BzIPS z!y>fMS@SK>55|i2aB^dxh2a`u=pp1( zPSoVFq;D7d{}w`q((}+2m2-J$hFFhT2fSJED`LE3Z3qS=SMWiKSUF5UIiZLD_TH)t zZ-7+`XSD{V5|s@vb?spgPbh^;we$McHj4Ao7RDByMjM^bJ|jw&+YFlf} z)=Q6}={6h5&QR9)@STL5T2DxaCq}IhRLDqY`ipl=fIhb|18`gi^mOunSOAie&EwJB zgJ)yjQfjb$#a=u=Wy6@!4x$QOWHVw}(+a%)P36h0IE$=4w_RTnOnJ8Z&@M9CuNXUS zKUwe8?<_MV%d=iuT|wcyOeI8lGs13?6WMZj?fu@hzbCk6PToE?=O@z zy&Bd@U$j{OH8-vJmA31H0u3;em$(IHX#V43)}@H3Ika)9{oDm|s+E_do^0ejbWpgQ zqsE>B7N{UY^5}VDT9pK*U9$Tw&feK_8S@iCSCEA|f`PqBkq=xeCAuP-IB0>1{KyPn z>q>Rf#&EN2MY`hZGjDUi*fnMDj_sO(N0y|S&1?VEce7{`lJOTz(S0S_nf}s^-ETk% z)mFx45i`LT={Qpz`Sw=n{i!dsfEP9CLOT9k# zk|x)UGc#&y*9jKsyRuua1CvAEJxn(Gg0Pl?orDZci&4&G>rAN|9%#gg^%I{Agbo^u zs$Qlrv)z*{6~+3^G)HxjF?=Ng?30eCZGyZpB%N^1^9P>9AA&UrB2y_M=vBhDd!JpE z0cCh65>n|y(VzBxuX><4<YB*q)-N9Ee%^9%B}f zbYlpa#8f{*Dg(J%@JsLezZi`k2mwXI*9JOeXhda>>K~>=)7H6DV4W{+rI5`)a^#tI zS8SU~!X!bbksT%xBX2;xPKiu!^+bMPlQKCG-pVqCMopRL`~WP$@7)Jd#+OLzfT+^j zbPl1YvJZ*xIC~J$@64(0RQ&LnrvIwE?gX21-Y>~r2mqVOj$hF6KpScBM)#97NLWE; z9LR*P;ezLwql1dyGE_R=q_h{dI*ZNMJP6 znM@oTBC_ho3~#lMwHp(tV6vh33DcIfA!Ju*wqq_DrFly0Ego5PJe!#BHIXW&0)k^<1-PH(_YW)aAb)%{ur|OO8?_nSy{Wb20By-E$a--z9`3(ymXh8BJ)q#`SMehR=jA&9Gnt zbTEzcib!Yi1F4y;MW_|0yYWVzU`$|P=g%5uz#%*cbQ!rEUX6fbZ236>7UXXn{n^n;2*M)6FFBh&hyY+Q#4mNjO zCv+Me)fvd82_8~cB~BrGsnQQ4oL96$PZI8u)W7pI=`e*h%bvW5Vb@+e>FoXD5pIOL zZfK4$^*YPX?_}bs!f@|Q)Sw#t(CbNBo3X6Kx}mQxQ+{H3Ea&JT9pS!iWeWP?xz;B} zu64s+k-{af1)ReBaN)XA8TbzQoY2HWBs;=*^{2u;_0w(CWe0)k*H&vfYII>6oJ?)1 zC-39CfICptCA6K_q#_J2p7v}c3ubGyd0Odf)%(TYEP$fUZhxS89rCe7OV$p#%6s8+ zAGaa}8ZZd$0Uan{yuQHGMZ70T6+;@RcEn_y7Bj*x+#Jd>St|lui$ttCdtyh_0GN&k z2XlguWfRIS#a2Ay(9eK9v)fY?Iv(-{;@Z=<*;;3D?a<}CmyrV8Z{%Z^Puw_)MCD^$ zRnYgbsi0OlkaME%otn--032#vXl!Ggx+m-bw^5$@~ z&RsOXs2E9O2ju}*o8kqh*vz>l9OAt${*`e+|Mo1PHd~me0<_T>Db#lsbep7p? z9VVT~+jBr5+hE%*vXs2D%a>O~swZsfZ;bvA(1ZWdEDF`vZxGA%y&b0|Ez{8L83>kT zQS8@HrPtAo7JfHYt9p!ngcKeGWZ#me2I@#(N@=Zq8SUqt8BPhwB@e3Ydr}JxYm7-> znsmzLBe|Cg)}^~gMk*kC@T~7lFYL3KeETz{Qk`3vuqi2D*{a&1AqwU8mTZT3li21# z2h4F)38Vy3&1ZKU6*%XjUNB>6APg`pudtYNv82&NE z^bzv0zQ-6)PI~|7J5EbJH<4Y_SBq-*rude2q}QUVcQQ<=;GsBH!{2BJY(L}s^&$bEc-a^e`K;VF)fO^v0NVPO;EqpyDTwr z=r?{r1u+u!;O}!IX34@GOl{yK&2nCgIP>~_pMr$cb@Av=y%TPkveRkan1U6eB;?mf z`*EEMBrGUQRL6SkBB=sDYbT@ji_%d9CKIydCy`yp4xD1m0wE8S{ElZ?ptco*bB{26 zTHXPB6QJy#J_H#^u1)WQ%f$-I3Nvj%pH8wV8<;B~u&s;Y=y{O6NTnN+Wx^C-YFsN? zB6;5)pjCWzl-ST`u-#{A&FX@>M}N~#-PqWbhoyQQ4>AEjU-Ryx0`Z7By;NGvNa(MW zwHHz%v=2uJWX5_RSspYp64CN3J@u`)EkR(X5=><(FmkO4@F zHXXBkg&%&CWGn~e?|WH^WVl1u zZgkfK44{UVROZL%5(o;=hu1a)H~G%O8*uF96Pxz|kNf`7-(`VrL4bLX54LnD z=a!=gZqbmHY{^rQR}&N2Y61FO z#I{o?Yw{55_r^dosqMstJn!%YdL19|YY*nXIlb2Zlii}3eZ-P;3QmK|jo4LW^t_|d zwFY)8iDI#0hp_^{K*{%y zSF2~3zb!Wiz@xU~IT7_LZWz|DBQysGqqCL1rtNEyVm1*ri=i$Q60&9M_~&^{&a~2~ z9bz~{RM#E~{6Ws&OtvG3U*6BRQg*%E6%V*sJ{*>0{9;F2^JLgGFa&u;IvC?0Z-dLf z(uwZe!anwokjt5-n=zyI9=JH<_N)EQJS4!gah6R2Q6x2eQy}EMmm``35jWi zH=gl%kR9!y<;TtI?5*OjBPbzwbLZtYsb3%d$W4}yFa0~s^vceK?g7M0>=6+RJqvIE z7xLLVF{u6;5$cVv9Oan792o3epaAkfP}t(L^uoCK7a#`|yY+<-f=VG~9Y^*;hkEOK z5>%wX=|}A$xXM6{E`s%=q?@ZxztzdVWrLCTkTIFR$U`%32$P5w#URnFb7YX&?w;zs)mu1 zs!>#Z#dsnY_dGS7)6)r@5*H`;X~CYeYGc)FiQ`vrfSsx-84G-?Etm+6fYnDt07!BW#s z&XBf$NmGioaw%LJraBvW<$5+8{$0f!$G17^ZArh#?idgw)T-`Gz29fgva<$3gOGd|! zEn*jjDW$d#ShE<1l5|36<>u;*NQ+~Kl)J=&4u<9*hXV<;TZ1KwHju*$Q>uG zS5~mqqt2BWVVnvEE^u%1t=5i4MW?DUbHG%9bMBjpqk)Q9EHxBY8zX5SS~=zB(9;hD zxZ`=GN#!UB9v)mO1>c?zn=|Z%pa{CG<1Fs!UA;E?VB41ARo9I9%~>@nMEH+(x9f}4 zd|am^`xi>R2aRsgnSjU-KGNeplS}jO7}@v*GWb<%S`$fm%4ag3KiQN$ccQa_>#omc zUD)5?E|c__$kOjCQL`sSHqddA5on;>yn!VO2qaF2+GM4NQG!gz7N)rSvljZ3Z{g)< zPnJAeCG(QAi6mFvPC2?cwq0T7=Eg^NWIey!D6x@YaO^Ad|9Nxn^XI~+^sh3@FIbGH zsvA+zXZ_qZ>FM3c{@(^et;1Kjiprf82bG>PALU)zi%(j8n;NpXkEfdY_PqC2)Q&q> zV8S$|r|Xj3FHsxY@h|Pi`m5RdAX1pnaeR})-&A?Rl0^l}I=Q>Ko*ybR!=#R1ONxha zd&%1QEA5wmYH*X@ka`vIq=rb53F4290{&han#Ai1la$@- zz4>H)%x+q5*5Z)6*=lNrvJ$`~unhY=87`%yc%^K0@6Dom$fxd?;!(SaX<=T`?>Qp- zXLO3bCd}CG4dzZ?PK5^--+i~%@ zpPwwtXMV?jUfd2O!vvPy1j7D%4>BPo`QM(T|17iu0^PFqXOC9En8cj~7qn?LU#@%G zZ}#^#Ols>K*9N2rr6`!=+gEM6pJHN@xDqmJ;hHEK;nzQJ8CP?VWhj;tfiGs0bcZ6H zvQx(?GuZAGUfY5O6Xi5^oZVxy|C;|kG}Y5wZ~h)vDmCq6@vfp>Sm$6LWxtNfart*T z%j&$;Nl0Du&9z8NU`5hY+$;QY-}p zB<$~9b|W!Wu1 z#YR=C*UG`Me(yknovq0;-84{rpHW>gUskVD(yW6)H1S*Q58+4GOV?!R!nx}Y-4G?Y zKkgjue-{lTHozudQSPmGpAfJ5aSoulRMj4P152_4olpVRf-otK5GY_3o7n8=@D9~j zGW&Yir(=WL$Ki%A1*~$c(vmmOu+*Xz_=sCVGT!qOnS*(Zk9=tYIop?G z^R~CI;w!w#O%R^?Pek|1LKHj-Wb{lZz(=n;YCt$E`Gwy)vd^lq-Rb?br`B^E+F%h1 z8z1VLS6Q0>U7FvtXRR(YAo-eZbuCCK&f5qI6+3&mZitB88A&Et=+R0)@9Y_4W8Q** zdZM~F!&ZHE8!wOEhqjwG8~6*<^}0^|QrQ7LTkms&e|%-JoX2o=x4!%2J+6ZkPx{F_ znYyPyssF!yOuRLrBN0!*MO);ExX+C`bsQs=gAS`jUUlpEsf8~FyyI>_cJIt87qLOn zOGRi95AW&(AzRQ1P34&5x*t5vF0cf{*6vOJ_|U5A@~SGPjZbdz&4S(7$Ir_58}XC* z0Y$|p#0TIvr2^i+iCrY~>0}SNgYnbFST1jV@W{BF&}hS} zmWaE)QrLKQ*|K*TAEI%c8-TziB>6)BPj+m-^E|0xNFq-{hAXFc&HAgS7(MY^->y~2 znD3_-Y5l}^uyOXHtC(ia8zWqNY!GXLDNPt^?m~|yRqZ;2d zJK-JkG}S^c-S_FV--ga-rc-cPw6oqg++2CR@nZAvnTfl7rRjmNsrx{s zW_`bXcZ7Fs;`lzY=&eHBbNA(g`3qwE5Gxyo0B1+L^XpYn8*G;=i-|SU9@xKKQjnBn zv$u2_dEmiT!m0Uo;U5?W?r3q(WgtQq=r2u<<_irVK>B^eBV7p?=dFNjiQhNejf%?}p|6 z*X}qh%aKZYptNxs7eoRAbpS(AbVE4#yr98!<6E8XX(ML+i#*W)LZ1Vfe{YC`*q;g) zPWzlk6|_LFVQAfQmtj0Za{Y1}-z3%Ln1?zWE!JNYaXV(SN)XMYseQC(NI6y=>S!@lCSi~<7*u-fDKA$K}$6@x)cNtZ^oZ7`@LAfDYtEGLn(`$N!xJ41crfF+*qwS>L5L8&ba72f$2 zh&~%1o+XeYHximk?frFan3qEttj>9*pI*$irl#r|!bNhpsj$fdW)5QS39l94j5>8w zLf4hr)>rjZvFDvLH)1uxWJ$H{@j9$>Fi1OHsQE=@k+B|9+DH5&wf7$(#K?vVgLVJ@)VSf}=eE(f6 zyx>Q|^tM>?Galr>$+F@}Un`+r!>8St?gvI$ppL?V+4wp#rp>{acYyPY_z!p3+tX9D zQ}-RZ6L)50sGPL*k^P_{yHR{_TsgCrcwH!UG58$Q5WnH|ypCh1h$+sKk)*O}AyE2( znyG`tU`*Jcf2--O$ie?14m|L4-2kW?i7Keq{x|z(9X*6&*p9ovDnqFvG4`v>jUFo3 z$2%ly10~ZBCwBSXPkJX!Md_0G_$kFj#nLi3aTax_gyGeN53Bkf_8uTT^U+fGW;fn5 zgJ18S_;*G#HCPCu2NzjR=@gQAUf~r|rL+ciS;WFPi<0W5uo0&?$4>n1xXrfy7ElFL z2fML3pXUY?bJ~hqMxkQB%8s9pFO9qB8fw_s~Wj8#^ zvQfk4#?wM6j+>=XQz` ztnKG!y=hopXTIk^|7>x(HT)xL5GveB&;U^^n4};^%_ARb_l^&}F9-F4;>}N2h zrg~W9e0e)Z?$v_gPkCejb!UBec-D~9Q@bPQ8mdaIn5QQq`NbC3s&*T%6dg7BkGI*U z(_v?BHHp_Hj*OhmiCq@q8tqZuia*hL;kg2~EKO9fR14!KBXJ~ytv3mHaS`pAwj*Pk zrAqkD02upNSz+Rx9Z-hYPLrWW&8IZnXpn9EJR2uFnyq!oN2y7-IU#ka6fa!%UGYQZ zdc7O8mj1B>H*FqOI<5oWr8;c9zmJm1qoAKK6=$QXNp*hL*e=se7~Tgw^+TW3@1ZRh zS5o^o^tv2+l1*cE@F33TeAnKo9Z!ZSf`)7JN32cl*C>`$SAQ9nq!z@{IeB3M%umBt zD%}BeO=n}f*}X2Na~hZ`RKn;&cMV1cbPkQ+z2{Wi9-^*|<)Q-4l6<^?IeetfHS#F_ ziW$$Uni10F34AJlJ zulmNWp9R4}JZfS)^Mx!}ro)>zgDfHJ)=={sEBDaGVc6nsJL3wz>#G67*VRkAlFpm} zK1lFryyA?e8;5H{O&3fNzXEq+WTO9v1*8?es9~DYr{}7TF=guHt0iV<81_rza96hH zylNWnKL;#uwx`4hCo|8!a#%zwge$^*5PekSeKh|7B5@I_Mg2*NB~vIQ^|=V7^9m6y zth$e6C;E#cC`Bv_@UHKI9?Gi(1PaX-?Ob+x(R%!BwWNp&*3Y5_G6tWx_?vz#{Y~CE zm6qlB2MW;H&y6|6YpJ@hK<>%%PPJZJm<-}iZ-r@E_Zj6S2c*^WzH)D=F&!!I4;s>MNU=)*c^{oG!8I6CdKQ9Iv0P-U6&brM~U2U0U=M4h?O!?ng9+Pc}E;M&n z)r4fwiwqi|-p+ET=0guZs=k!kdJA{-P7+Z|hq11E*XeDFqjk{tzuG(s5sI28RRC&- zO2m)H#I(G!+-x`-2j~PHapQhj&96ApuTeOAnq^R;V9m7-M zV{YAV4gh+X^W$yxo^42Z#jE)tJhF`$^xrJ}inWuOCrOBq$OGAWH+6JyNd$d}>QcN{ z8WnV%e|pnajYlR_(DTD$D-no%pU^|gDbHTp92_0I#9?V5t=U`;!BVr|0AIc_q0;7Cfcx2caEx2e9a;jQSJBr8+}d57%8UnB4@;e z_Vrxp?ey9?hi^g`4=pvuj#CZ1bG*s#46mFD|(a6H;`(nP$PeB1pw<0q&jJcsy>g|*)k$rq5lmCZj^FG??)UIQNL}~ z!cwEdG7Wa5i6D;j9|H>(sDc(*|cQ3SJ|5SY?% zSlTvg*ya0*fA=6&>bZSLzKg+~9JS91@dE2vGoScGEs0RDU(Rxo_e=j` zG}L)UIIMMNappP9p)5Lh86C+DB!-V1IQc)7ImyZ?fXmr1aUIe^70Oe|mNzzd3d$&r z3oU%gVv3b(E)c@p@3SLsOj6t$pz(d7tXW^@WHprUqPo-?wt4fR$|@U54u{w{8+rH$FQq} z4QdZ5fiUHbZD(eoo)Hs>dHsvVWV{GHeu1Iz-e%zhBPJn=aR{BLcPRW$IO5~6_Tjpt z-s|6a@gCeCPe&%Jio^qCeGsRwjm_V|Vk%DZE12K+8xC}s!f))VH-OECF?F3nVR)A9 z*ZB3>Ka@w!QH=l=AfQx8$%Sh0^V=^R>gvC|k@iU+ zwkZ9f1gO+G@2~c(@ZeXp@Jw_JL)6LGWb_V#r{AyTz5j%GGj&g%&U%alr^aL_Yt^`b zA{s~P*M`D}{rOV2A|x8cV4Vs|e@_1>M&kwl1(IM2;MD_(HkMGQ3#pxt=y{a2P>UjV zW(jPfpE=1YSwc;72=b=!a~nJIr%Nz13N0=C7r;9mDFwt{Djj{qi!#P~AZh#`| zR?F@E_&--nECR8PpRNP0ld(+b;CvQIrzA+4N8A2~fCw+q9`9@Lv2=aNRG^3@ft4}y z=v2kGqiSC5Y{#r}Ad-Asji{x2L|-GBtyd1|cT~kswMiE9LuC0YAHE%sGy%rnQp(!? zQi-9@ov49B{ZGRd<2eg<{C1U(p|@DV3KUdnx?ZIBDu}{$kl;tV41Zc+Wt|XZ`Q9Lu zywJFNI6nJpOtzk5iu_5h?O2bTt)B6cS8D2Wg8+l-zMKc>-Q&T@=1dXDJ8FtWc5O-o zRVw!@F>vHITM4){F&Ze|y@ho`I48Vx87sqT7~brVdoHVLy~}5C^}WAfq4=KF^)#pD zxi5H5;r5B>?VWp~UU#FR_juH?zTjM&Y%Vp!9?OU+w4{};zsfzq zJ$sU1{r2uD(24YL^-1u|1fkRx(yG@W&OZdlRZ6j=UBke9 z116MvcS4B^gI(&C+hOh&%|-R!ZBE*UM_gPfkj|6p47;5ay~%3l_w>1Y)y``S5D95U zy_H!SZ~F~D3)|^su5)0F+)(GJ0lnVC+kaVouy0+a#`4BqI5QBZXtFh-#154ojJa6W z+*|3N!SzbkV_mVKjZiGLxV!j2J`pZ7W7K2+Ty+V^XgjuL#=eeeFR9+w1<{my6oK8p z*@ks_zmaoI;^VxH#FSs_?HRdxc|%}u4S|eR`#}S)eS!IpS}|crNJt>ldN`Ugf_ey*DZ;DO@AJ-NP{M%O@t+;iz@mjzXZov3^h#5B$g(N#cOHo zs1LCk^d-G7JD*)SseA*Shz!wMP1u?$I#(8uwe?UK!KTjp0E(*`L5x7&O#+^e>J-VT3G)s)sdpVO78BNWcd-z7%U#9hp#w!Z-m z0q{j9X<%zA?Tjep=QDj&9nzdL`9}I0ibxFSD1khK$}znQ_ffhgihMY|ersz*T6L-| z^~Ji;)k>Ul1WBJS!g=hdY`eob z(W2T({PkqltQv1rISP%}+NM8MoxXAxwfedNZU_L((t~Amsw+TE$?6&&X7L_LAy@A4 zKxcsAI^(`l<6h*oHR3Z^V8#iTAC9lxUx}Qj*dD5mq$+$-!SJj2@@B#JTz8_}k?u=K zgR3vByntH%``EaYl&W~Fs7H~geF507joq(%LhP4!PGyP|hRGmTINp|x+0yIOIasHh z6!yr}q*@>92JktQo;{?mS1*wb97`b_CeP2&BZ?VjZmk+t#j0R;pd(5{u__)&mQN#mec++Vs? zgF^fZBXs-pM0FTZtrukFt!WICA+q-Zi7)ofKTFr^t~~uFc}6TarebR`cR{vUx5<$N zpvbgOfUpFq*_bO4F2a7ld;nD%#;D?A>M-|{Nn`p!u*C<_Hee9fgLQJb8}GHgbhXwL z|8)s2Si!a_vRX><&A#Ov9Ad~B8cOsCw_55sqOqM*vjyA-y`?!*>`)u$tv*1LtpW9l z{K2gJN*&gEIQ9PsyUMVr!mW#iA|MJ#he{|pG}21LfOI26NDkeNN_P*P(lvBSm$cN- zAl)Dh0|Va~@4dgj^J|`G=A3!Yn|rUd_u4dedw@>Y9?5k-SprsnD-fhEfF7WzGx(E1 z3b1BKo8)H=$S}+&(aMcwn6^(8@w?(C_aaF+ZRj?nZ+6_ zi%Joix_N6Q@@3D;7!N9-*t7&P+IStcIW2%eaPoZm@a5i5s%YEH($?T4?NSN~Gp+w& z0TQ|!!sqc$pWgTZp#swXun#Vo(6)Z@R>@8dep7yOQ38+c$mxwJzxU$0*4eAc--6j$ zt0~IF^On9!Q6?l743KD*<) zi)y`9w?3y3F?B|&&4qJ9w_Z-pk+HM9tXkWi(mQsVznA#_8JC^9F97*?teVoiw{UkR+SOO`s~wWo4Fh5FfVP}KaAVmS$R=ha`& zGIq-qVUsE(Y|L2_Z+>0Q^O3=rVti35-6nv_>8xMga@`+KqayNdP}mNSlDAjo^wmXh zNg32A8>wEho&~28qebA>}<^8G3uht9u&eZ1>ToPcAHPL$ZF7j5ZV|Kt%^` zg<%=m41EeCIxeYGEon*Mk!Kzt5F(FV+-dR>MB_+kP@2d>nv0r~E~to(s|Zs!dtG{! zk6Wh*%xV0Z`&!uoXSo<;<3NfoIr?p141##*BDU@E&{R92(V z&df5!)lBJ8)sGu6P!eRdisl7?=loNuI+YB4sS56%Fx#^<(a)1IqB+EGpeQ!Q7gAR- zvMW`T3%*Jr`Iulfs__i03n$e~@2qyomYrUY!;vc_a+!R`x--YfXhY99!l-7!aL{t+ z#j~wqsa!b!26?-L?c*hX`KNbn&D*}4gflJU!q|IuA#N4W0HpGcrSh2Lw@g&J=(TVg`xge*BBm-Bfrl$*cw@LXWX1JY(IqxXx3ZC{qr%^H?sAx6won z$jSo6XXHrLmJ?80uw>_hVBUV6I{qSe#MhT2Yxa`xlwe>LU+j=8wzGa;5A2pt@{3&7s;l=B1 z13`_LLNMSrk-ETM)c?Kes;^E~M0ETaA13wHgNBk{A6x%8kIaMYG=}Ib!Ucb(0AP_o z#CB5bdu(9VUYLwngnl{mmaf%NXjt@{;dss^qkPXZhuBj+wWqZC|#t~dVb zpOWKgctb?Mq?DN14uW*rY0_e)_KUtDCGJ4_w_xGecch8iKp!5%5tlPt_;MvfS?ltn zvhD70{hB9oH6-(PaR*m?H;1e|dm2hp26pPxmdI#34?)#zhN2spw56mQ>1@`a-B#YxeHJ_GXC$gE9)vwCw~< z0=N+7?%*{QDdz&Yh>mcBl{v8P^w5;o*U%gZ{q=9^51s9K{7htkGTw8)e{W>|h9O4# zIM`}%5^~QUs^_ial$5Ah-N;p|mXs-|f+qk*&G=sF3oaF-@~csPovlSkWdN_{;@cTD z>>Refp}V_ZH+Kp+hcC~Y84vDoqfgdsIo<@OIP>y)v5Q#O^7~@3BafWjzl5^?s^ZYV zs+Mdrq`@rSMlj!?bw3TuVfKa4-}^KuvTZd-Rj)H#njVU(T^mSv#{LP~(ERF+=7zp{ z8@Q8id2{*kaF!#!tBrKa-d3CH8`tjlNPC&;7vXG_FlQQTe8R0e?-|*k_(50_*`E9C zd1-BsdR-cgY@|Uc*}PW_Re?S(yegH?vP1CAOATMnTx#d>)s>36ZWKHO*Cs}jxWO@z z*EhvI&~(<2|46dGaHy4(&moA%=U2_dm}|n19q`0L_Y-cT^3mIwn!ivUKg45CXK`H< z>ZziHQ|Q_kL(tvMnO+s490dWOBNJ8*?9eDQ3-_Cx`61@Q=ALYraX zBov1^)U67_-WgX?u}2Owmu_4R_eIRJPHEJ%v`(Z%$DPlQoGZ3RxSV0Sp7vYK7F5LA zAK=L&;h^lF8XnoJ56!Q>3RJ{ODiBzWq>O5`QW=~dPvXPv1u6U?hB&syq3wv*aQD7u zuRMrV8%y)6J&MczCu@FN3{$}V=e1^PWM`O^789IU^cZ+IcXSnmC<+?C7}X8fw{Z3> z7LUNB3z(*xICB5I?5=xmRcTCP2Gp1jZF&3g9O>_Cv!h3YjV zn&`Q|d!FteOtXGN-dx5NCPdm;#PX}L&c?YFK*>&y{ zRIL?{*ax=^2DND06V{C6;rSc)=(CK^3ZQm439NUR==++HHd08s;B5`~{gedBAiBOJ zQ>{Z9k^FZ&f8GLq5D^+n6Up7ukTHER9n|37%`0Q7pNH#{3+gDL{5n}%=Qnf0vCsWr zcTyjylV=#*?x)%K5URH1r(7;wGlW0sUQOnB_N}OJc3MNByZ>>%xI%|Gq#o5^y>C63iCV)NNz?jZxnXta7~QEkSa2m;zvH-sB6CWbLN*y zLNc=spwT%V0+g^MAPyL)16JA8!*0dWa%tP1da^S(xGmoN=A$lVi{bhAav-D;+OW%X8B{*!c5lyv=>caU_VYTNJ&Na{>|TmyqT#U7+Vcq$K(Em*i;wdG zK{np^+z7*NzRdz`@_|Y}Wi}NX*cYo8R)rT9b5g-PiZH&Sn)A^(GZS;l(wxdmK6iUO zzAIt}yl{$<9DKW;_k8%*RZlkQekRDa^5Lm?t3*Qdn5pOtQ$yIdd1Jk?$Hc>&Gw7Ag zFLXn1nyUOYF9a(5fQd>ivG+m=yHY%|zDI0oYd5HULi-+%N}13KejQ&e&zLxN4@cEd zm9dOhN-XZRrMffa_LMUjzbGUm53;KKeVrW$L`N^wKe8zNV4@M3t=ChonIkTg&j(l@ zkUOpAkZQ7=D^~%z_hHTE)zKbFxfNXOlHLf}cG*d-!qXxzuka%DdD4)u4JhojYeMVg znYT4OlFU3U{Hy;q5ZJfQ|MgE5LF|;VD|kttK zJzvoang^U{cD`9u=p34v&&OQ=dJ4G8RU5rq`@gtw&)Z z?h(w1Mc_owjV!Y`8jlGKR-$%3kaijL%Hyd|%;heB@d^;Z>f*k@XZgV?I$zXstdiZR zQ+>upgZUO*C6g=${Wzih+&KqQwFFP{PJ$vQpTTw)Z5>g@%y!@6Za}eTSImY$EqYvF z8GL=PTl_aA8}cr8{$(&JFPHrxPKmL8v*)Y}W^F{U#M#CWZ5ANrsAmc`H*zxu1kVo> zwn{UtSh6M7)8a|w!u!jA3ian_g|Yv5C3kAjd`dewfoKv3|LTeftg$s+aDio2S-q6E zSo(qWHX-`7I)CstcyGZhxYn?F4Vq&}!zhpqrwTs%US8D;vSYc!XSq19psjjnGpd+#oxNP}*5B*-~3n_9z+RUCGL=;e1WI{a2%(vnO$+ zOSy{sX@hm)w9o z48_bi*LLt?jP=3OYI7vekDP9pwd)^%qt_CnVr0rY6gG!Tz(S?cj4Fu<+@I7x!^)Mm zB&?YvE|3Aj*J{;3226*=^MXOjCJWE@R}-U$T1HBeSheoccMT5p;N(5aDjklQ(}oHR zz&x-VtZquY?7^q;#qI8PHPqh_6(2w|_0-!5Gvjcjb{?0nEGQ1cnYYgvbZ_a9i_Uv~ zs*A6I+ypchyG1#Es8Ougn(q>zh18sNipil$03?SP#$vveDkT2vZk;3?+FF=76K2q3j5f94L)CA3U4uMWzS<<$4ULEPp8l7_x;z2`j1lH^wiGf=6w zTwVs91+~Gx@>iN5+Pw1|ZUL*I*_V&N$`hh+QoCTz3w*;`XP&?Y<&4~jtvNWOwP=0( z)c$PiRCM?x69u9qMG)1vaQe<*By$%ogJcRSbcv;xd`b^%2BKmQ(Dpa8W+nZw9G2bA zlzaatDt#l9-#sqUvSe*vHBo(S8_m(xmc|hx-4qYRnN(^GD#WudS|^D(>td3`#EO-Y z*rY@eM&&h1)s-kfm!alEDWOBDjHXV0Z5UNRFdZJj53zEqx)-puJ&syIb`nl2K?Y-O zID*W_OeJC-%5+86K0P+YD|1SB;Hz<_cgzime-CLYR2KccZ-e9(0ntUsm4Koe4I;zS z+CLqpLB zy(`p?2q|T+G1g=@9O;PwBrg?-YU+YaV8#X7hTLQ##l=Rm55V^TRN_C{fe(tj*hZ86 zf-Aya1O(5v%}%TCyJOj@dtH-DoW^VG8F$G7Y^YsuH1)a{6rr7@e$35bTDb+*?I?v7 zn@CYPe~x_N0+K{ZL{*Wg{OY)-XzH?*`6@T&MctpMm9k-x?|WAbKX`GIUzA= zU3-jUmB>`Yn$;S@QT5O=z#!X1nG=s3izf?bub*^3k*?)EKB&T9uPTjG>E!MBPV?+@ zt--P9+By}`Qzw{PFnD#KtVC=1?<($E~~69&d(U>bk@5_Y(`A!K=` z{ySUo0-x8E?rxbsW)?|Cn0%V_?M%ARRG+3%*>Fwg$56#0U1Ab$`XmT7`%4qcATh*U zw=!?Oi^P)o19PsCWVOJ9V)9~`FBZ2$W%_7*pmwYqCq6+H1$aoSuaHlXNn*SU2;Q;I z((pjUcm&tblo3TesgO`Hx+iryxyaasqM7^WCX_A^*|vC*B46#>lY@3SFJgO{@AU3ga7eRxNBw>pD5U6uLKaS0^S_>4-h z;ikbBounD(sY~SW;AQ?Jmi>bod!($KalhOdL*9r1>}mdK+*L_|AT-11njP^gwRR8442{--YNtq|v{J zk*IhN*uoz>Md=%lU1l$xY|xw@WVrFLzF|20zSDNB;VgS*ms;Ilr2eP|Cv@GFx^xVD z$}{LU-T$+T=RHhGB`a$a* z?g^X7i?Ktu=eOq#TX~>@Z4J;}kq7zpV~k9jxMesP2r33rFr-;IOu87e^J%zGezo$I z?lY~EEXN&Wq`C}09SO4ZzP^y@cWCZrnLN+o^PS1yJg;f2BYkbMH$g^Re6f)`3$7-o zSK4H+lvnhMpV(&@o4Y@&Ge^uxT&|GfV#Fa#L)U|w8e|lCnvdC=s-it#BWxVng{777 z-T}^CcphV}1;MTdiR}%g*FcGxU;9B^k#tYmo@QN=A4L#UEUa6U>cMY{@2YBjU%`C2 z319yCUko=|5DcH>p05^FQ5~CFUIC&cuG`t_c@JSohNh@;I`sXvkB(VhF2l+%dpRgC z6h>#YZ#e|N9-P@r(wq~VBj;)23aPDrOpo+S{GB~L`#qTf`?=JKxm(`o24B{)-EoGW z{}9n=wpSG(c-U2WkJex=X!rh3gnc#A#2Q=kqBNJUdUsiWqqGBm93+Np)?YatdHMqz zC{VIzh1v;daG^|4EFS@s3^MQIU?haUF2INt4rv>+`INL4@g+D$J4qV3X~wvuJ|CzojCK@e=IcB z?*%}si#xd0JuPhrJ9{ltfFL`YUJPxE=%zSa(PgQ6nVUALzgu-a#3@o@{Pw+UQ&0Nq zbF738xEaGuQB|TkhB_>#KRhPb>K9WTVEdas7=x4ZIGfx!_=Gv7{vHQ$j=`@I@GI`P zC7!SXL|eTmz+ADCi>chB50=n3EPbxLKKy<|?8h@XPJ1sG!2!x^nz~(6u057 zlRcePCVnTvPv&E+?ZLVFH5zPoyOnbzm24GLe!*aB{gwCBLHi7brImcb0!~R&*e2JV z>{FKFmI^Tl`;6Rk=;xlqT7{^V&}IZ(4I{j!*Tx1Yt*?-yH>r@R z62iBO^Ho`&A0JP1GNw8u@ zY!!(ZCTP=5G4}6)m3LKtBybk1eBXgj#kawhZ7Mq;I@nOoy`pVcBy1&9>vu}#C3BqA zQ#?UG$2ub*gRCL)#u|e&!GvMqX&-wCWz`@&wNZ9d28ZBT%E%zIVw8nPcmub0?Vc6d zRzlN1K(nB2fz~h%8oXkKN22r7+n% z$nF%I-ZE03Uu z5;zu}9et8(?MkKh!*RPx0E~{db#&opR&wAo!HTjVz}B5*3Af?XOuUF6dT51d9>cG@ z+t&3{r_30}x6RX9H=55(rzMMzS@H(?;x)`y+R`5~Aa|C@zQVI~+cJ1AW46*uF1;g9 zpVbDXB{$C7@Bcb8KC9lURvS+@>;ig301Z#jeEJJ9_NF4ASrVV8)6?v;!iwjBq(F}P z{!}A4c%39Ad^#ytLvXx!H+VZ?Yru?267T?|*8pE2>Z|g=h0KrITyDBtcPhn|M?Yp= zRAeW)N>1Y@Vq!>h!}?RJcwFdyl#wPfG%0#4HkO>1fCC%{C(d8jFlXjNR@DY$^a=8V zz1)O2Q<_8LJNrmR@md=MCo{MaCf<@>^iDGbs4jdewp~jL1zDKw_TKk_FjLB*tj{aO zP@GyhPA@kL^ZmWOC^?E{o^3fVG5{8cZ@V!T_)xnA-Q#wU<1kccU5m+q0GWvBY>lr1 zU`%zLO!_bS*~3Qt(jQc>S7hSfnG8v|&sIr%ggwWAYwqwmn$BtE#cP)iSr)FE8#Za{ zubG%Y=}Oj>2VzjcEnWEW!HZrAT6w|zYXmZbjoR;cI@q|MRglgnUP+=l@jqcEI2I7z z7SuSDE{;vc7$rd&66qrAQDN=kO#C}ifwX?C9#lM#PpK!t6-l2afRM7J#C<#m?WKDY z^(*>$x*a81Q$i>UCUGPXX7~@gp$4Ef4oHT6t@hjT*yyAtW_U#zk6KIataD_9)+~_@ zQFY6pFl51hP>z@fgQ=P`7mI+I?@dmpWjj&4 zg=VTkmH6)zA^bOWIrPHn$O*EguXDJPatz&Kf_zTt@@g)Sea?J!fZD^V_2`;~{ z9CRLSKW0Re&hF?qMkfs$Z(5gY7Hx=l3HlAJZK%T)(cRdZI%`U)DsqO$-2V z=vWpn>0fQ)cxCPmW`&eB0R!QV4QrV_8*s&pVwQ zg7x~r5Zy+h(Lm#lLti#Mm`GP+-)nYeW5$ZKx0YU^_2C!hZO@HL63)nEx2z|?l+piy1}QKl``QN6UzD&hI-D$`$px(w z2_t9+_hhD{ZT960trFhjH{mw%9nxe>f1Ki(iQL=y)f`6GlI=5^kP2B&c!fF-U9Z<{ zy4Jp~aQhEg&)#Sxd?O*-WVy#!)8EaW= zcn_5A`f!KF5l+L;`1MQIWl#bIu%ZH}<5xZQn4g4lx>6}*n%vV!MiZ@XLRc3s-WQ;H zS`b68U_XR{yuJpns&^pY^og0!K1ikk*=relT7=cUr*zLeyI5d$fer;C zi^P72@yF9m(9=CxL`aB*WeDU25tu}HXJ6&alYK14EZ#rWGJMuTd++kguHAhMTfPtI zXfTEgu%A0pt@rH)-+|53e90d|y%D?wKzjI_&*Ho9=NM20c)7)v?G-aVGaibn0dp!% zS>y)OPbGj;P0tocl}C<{C}^19?42E-Yz|rD)_Hlpkpn8`k!^#LZ0oJsovMTcZBEY* ztWE*S!S)~JFbu@}%C}PV4lyM+)Ro=NH?RVYiI(x1l=fL3%(&o{{WcZzjUdyP9ODBA zdZ18U)%R>B*|kVhgdBFFe^3LBd+oUPt9J|{+`l9>%K{^P3b1e}v`Eiy1&a)RMwfdg z%r2szTwDliAQ&WurXx<09)=C84spXRJ+CykPO&k5gU}CV#V3DW#7$JX3m*}2cf>$> z-}@7DdAW+%+xt4Tsiyto`4s(#*(2Ql&=8q-)jGB&#$g|79$~P)mNkE6l#^<|aNq}Y zLEpa_>zjK4KaC|NnVDP=1D+jSR%T|hA$?{;I{;Axf~x-EH!bGAB23Lb3`V3h<4sMC zK%z3;*d*o9AjxaquL$@1G(f1sDK>gT&zJzgCKAcmIX)xLW&ZQDgBLVo!ATyI>c6Fd z*Y*1Q90*~5Q1eN;nW=7)!v1k_4ON_t_<7-PfEg%z~0OR}c?e@9GHn6?;|9gVs z7oaE0{t^@ZcYR_2*MDROw;nuY!m!n9gQx?~_@}}HeDyW%Uq}!7R}AP(f`6Ctec;Xh zT{#cu9|P%iA^7iZH}b-no|aUR@pw8PxmdiR&DH&Q-L7%^7sOkZ9&3^`seQCAK)pal z58TJbh8{GrcdonRDc+tozt)qGXFjc$eUvieNj}HN87((yF&|bq7itthdgoP*@9qKj zd|k}K;sVM~D=IeDzh48NTV~phj;&zGSbfWex7w|>DeaDAwCLc|qMP-Pm1h(5BVqyd zoi&LhXjMD*>5BY&3idbp-#`zFXS!}@-}Glp(sB8o2X25^(nOIAv(t(mL7GL~(T!pZ zh&v6tNBe=&mx`Z1ztY#c7cH#wc=jh+qvqf6FTW`@>l^C`S3o#Un_i334Ir0(^D1A5 z#)tZRR9<{pP3=y`*iupd9cWCO|G)8AHj>>wPVtS%8~#R`3nu>Z0^QJh#RBw^OG5xA zN(iWp<2@yD_%0EeK#ZGteTZRww@R&}TWfeVCeiJ$Vt+qJU~F3PLdj{p=QxBid~`4- z-}@wVy%s2Ek#L7fi`-pRZ3>g6oH^g}Vq$t0`}?MHJ#;{JgLos*eht^=W-??%P;cRN z^Jk^L?3Ea3h^UHaR>i#}kpU2BloBhj^DQ}fN=JC0MUfsLxaYN6>v zAnayk1Z?T>#>U6%NQ05g9n+aSr)@2!k}h|w_T?w6N4{EP9uhRA;qp#SO%_Fq1tp84 zTgaj{f!B=izYgF5YgX35A*{=$&OkDPWM)N4iK2!zOKnK=fW=R2AO^P?LyEq=6__Vt2 zw{+xCvs9Hk9v$bpyd;)qc!DWStGiSVFFQyJIZG)b=a&ZEB>B_1#K+I;s&Wa55DqFW z7S|jpiY=5`Y_Dn@m{r$Nx(F!T9-1ZyDXmsIcwG(LLC00U8ii_DS%`Z${#VSU zE%#+1Jd}uVM?UodHVigg;Cf55PhKNZ2h645jgHY(r)7PEcSE%SCs*f`%(opX3L@@0 zzry0pzx^IG+}^ZO;%f+?9d}EOrt94OxvTK{8ld;10VS6 zr5G>VdnXu_pjp=X@cSc)u@M#*5NaN-E_C`uEp|W#^!c~e?b#$AC~0>O^k)QXD9XDG z_}&P5-h6(8hbZ&u4JwJA)fj-nyo#qQt*&@MEMz(=P88m#fnKAKGBE^G89q}g z?WcB_s?%^2RiZQ zEw4*%`@k8<<%Damo()2{r~6r&d)K~s$)cIfHVaF6eV3bxHKgEDy_c#OKC9}X#{$?c zfAB@o2M72-u}0OGu>lNUcm#$mQWVWjKToWxo)|NE-&mZz@*Yu5)@VI*4bc>%i*d+aKXJf(^$})$~4QeLLI;%w=Vnn|*5df+o~zh+If( zZaNZx(&uBOTD)yUe_ND#HCI|leSS~&L{H_XWAKlt6t!xLp-usHKvD8=qcd7e0E)*g z>6w-N$RVw}>x12Ps{Z=&M<~BXh48>1-u-w14=Gf;{;SeM<6ISEh*5nnb+VrBn%Rj0 zJrRiUsdedCM?R=^Z*(#@;*@+*tHQu*U7)5$o|Y^B%asPTmUVHsf-dq*sz}8Br?R;$ z<)WHWgH2(LSIazsfo%85H46(T{;Y`9g__y{xSJf%%h26GI})tTwnIHd0Zj)}*v4Aj zpObVM_l11U~n7pf_X~EI|h1V?=zm>cm`?Mo{2RuE#jY1WJ2u~6isW+{2h?5J&F?Trc=2;`IaU+ zZ`PtA%RJL;xHCdSN&%s2Q%dFfl2x&OFPg*jO9|-iIOJ{JPJ`n(@;Dr?#A8{fRzOik z?I50R9a6l)74z#BE@l)ONbA(E69sLvVocC%s5{p=cL?fy%M7Npqu{s^=CWh+lfZANuFsYKRbQF-MD+x7=|pi17G zXRT+KV#WPum)aMfu2$!uJLxl*aZVMAGY?!~8h3IH?qf~Bo#)B7m5K;a2%C@W-|Xt|=j+5HF17Gyt+#t8htoxs(WsXr*3;Y5 zo9)p45ch?GtuoV$n?jg(e7tIzPiF(?gNF7t{WIH^pbX=3*|t)(CGD$A`hmYwl(^hB zs*~w*mAAiYYnB+{+?B>sR^6DJ+|TZ&qgs|(^Bq@V$3>{~_zNyos{)JiAz3C})towq z2%euR);^Kd9RdkN;gi&KwLTFOs+Q&?C6s^_EA|8}nX{akUuI!6)aejb*6;}2s-;g! zTd*8%-|*D^TVWqDbLv6KOG#%n#Cxb+bmLUfVw>U+b;D_YtAgv^Cr#pJ%VcUy{suf-9<_7WJx`X9|*3?OsT@W98p&b{Q3S zV@2tU`qrlJRUUe!g(>AYg*1gwe^AE(Yn_%`9ZARPveaE5&mOM{m`UWu7HyGjGEnNC zPV_v&vf0Asx3Bb(wKFLH`RVF%G%q=XcW0pFLhNZgb2GhWm0F8M?DQ?)&mWbISXTgG zVebc+v~A_jtvvJRc?cs*FZPB*G@y@vG#R{NcjG>=k$wV}RhYe3_ge@MPd!fA{X)d* zV=^AldwAf2jL*69y$y}|%IS;M^m_g`p$rb!Xsp2ll;r=g09>rPA>4;O=aov~PS>eJ zKY!qF8v0&Tu&$VrQYinWl4jaH2>6VPm9O{S01sIc`tkOIVWgptt>JCl{Y{0H;J2Rx ze{n5kpHGHp&o#+0>t#RlkDtE1avdt3Tz+lF{|qQWh}MhHcMVD}DV*L}{Hs|MtVT%| zW*@th5k=JaBY0VKHc^_E<2P>@55hloeM!HsD1VB1XZ4wom?RVRY`veVu{ABbyA0@4 zGTD#lo-WGGmV&IBQ||c z5@2#iP1QcuC1scIJdYw60L48yv{BwEk#jzIOqKwj>@9|rT9d@tt8kG;I0|euJbrc8 z<{M2>KA&-)NwC7?V1G!V_VVx?(WHZLKZMxV$rPCJ`)ad7ZF|tbrqKRJn zc5Z00Kx^YVDhxJEnq9vyE5gIZQSOCLIV~I}iCK(4H~!}*wQ^>`-bRPM==EkmX?)FF zETA>%zX8WWe>#Y&sK|A5+0Y}Gs}Wvl*sH%_rOL9W%_*mCEBQ`(XYb-8-EGBH7WPkq z7-Qph((d7FM(&o%9qdSTZmo?=-Q$_=v8+AJxu!=a4iCIn!v0!czsBDt`PALoe$5;) z`KwoqvFv;rp_mB9+<9}s29rHlI9F+G-l-7Ku;R~XlJj>_Ro!p8_(s|jR-H`;24}z4 zLGd=$?VRz?-#)?kQ;GJe+WwA?Yc^NRrc2C0A*CghB~7KLe!J%a*{oQ4DlsAmc95c@ zhSu_*T&0G>La$Fbg^1D7aq4%>>5DEGe?D%@k%x0laPw^#zg*NMbmYmyT^muL;A+Zp zejo15C7mI`N-F?~BCn*b{ct-34#nKB{Pk)Zi+9gKm#Ux9{^}AnuB3Ccb6rcxZoIt{ zk8O430(oS8hYf%N=h^Np|Jbd7KbM6el|CCY}}%P~>vXe~9HcMT56QQR%(Y=4d`L z=(K2HCZ#r1zH`g>K7)?T+CIX4BctNxn*j*a_-L%SXk(E?oqs3&Zq^i< zXGU`^seqWov1n%h3D34zLg9^+4}+Ps8w^(Cj?dszlK1`T>R_-1WwwHCUNZBTKt6f?YE7_$f@dis5eKY?oI}?a)F|0PETz zRMwi6rJg zKQpkeaGZ#TU#o3L9@fn4)A(zF zt~Ls$R=rk{P1n=;U;o;xPBGqoxJtw|_cF<2Uu`pWg)Z_XNaj5Id=^(h_-?cuddvSV zLqc1oTcDKAg^yKijgYm`C9}?#%0kE~Uc|L!&%Zas>aApiqhX28F$rn8?S5W=BOSzf z|Ne9Dd_&jCJD^@~+wX^DfZ~8Sd0m;;Y2k%Oq+Pg?BLDyViEs@ zd_cx8sS!A`|DuSzFEePb*mfRI#^tAl%06519UzGG0S#j3q)P9D)Jr{gN@?Z!ns)V2 zJN@~^1PA6^e+3?-WIK^-l)CQKd}&UiY4SeXO2{ZZsM@wyFq&+viT90jh(nbh4_~)v zRlu7gA9WtW?`G3NTb2021bm)#mRCVCrrX8 zNm6%*!`{~VUm#XiDlx^R{hE%fyL_r@4bXaTZvf^2q35==#BwsEryX9V$W!OiG(QU9U4{#AB| zqV+p29ugdrtM$k^GHVM<9A2Ki0^Qb%B7<_w6=6JY{J#Ri=5h7nN7#4FqXDG~tk-gr z#~@)v{(Lr{{;o_2KNHz{mEkxt#D(lfOUE0IFsBK1!HRkdvF+oR#@#BSV}W?G9v@ol z>|8XFr*AvM<-?cr0s_Qv0aU=^*@f~zB+o%X0l(wLF#{rL!AXa4mvz}3KtX(cIeMM| zN`Ly6!{QvOB|f~2u{)qEDWubY{FHa~HAaK|6?THv^rh8xRBND~uJckX_Y({;P}ODO z($DI7M0;K9=d@+dN3!YD)yoc#Ho~@s-yZ7Jdf%1rV0aGgncLiLi!sV8T}h?Cic@yD zH|HA)u-N~BP0{{N+$!E@6{#~jgw9hBLJRU6_jPkSk3R@eFCw5 zKL6X9(X{9$wfL*{j6A^VVuUHtu`qjY*+y8|vQ_hikm_d_e1pC0zCn;qjR_E1Yj76d z+O`kycx5=~)41>#e13W3mOJ#|u(xBMdl#XO1MkVu;D;=Cw$4|(M?l|ho+pQ^ZH4jw z^7P01S#Cab|3YtF`Kr!SpzIDfL`l&bNG30OMIpC(_)mE$>9d>Cea_D?b;C*cm#s!_ zGqaxo3A6wkfCQG!2ae^70cxF}m*i^o7u#$v7H?OIeDolp$A?kDd!D!CkuV>>U0`F< zz(EUdDu1d+3Jj?4y<=%^c<&?oPNeRY8TDUAvPGS1C9BEe^_MzImz2-eJYH~96cu|J zp-$ja0e0+$X5x`kKU;2RHv6H;iq=3k#{A)t#;~1fvx>sNz^TW@$7pl=01V8(OBxH^ z4e0AX@=oYaujtTzHv0N+xfI~j{|i&1!y(Yu|Mx&N7%R=5HEK9Yt4?Cb>7Zr6ijDaM zLszwQKc_7Cz$)8Getwu{ZFRih1JJ)@{|0HD+U7+Fc2|1+sZ*+l0r^`m29cSMSb#y* z{?)TW2H9X$YdfNQoZlKkzS!;pF!5jvpDRGfJw%CST7f$3E;srXex`RHz(XmrCdWpE z&0`X7@48|xd*D4Sm-m-O!<7LWBMJ@3N@MXQbsV<7xLXNLr^FS!_Z7SotRd9>XvJeG z1mqba;zV>O0}zRnx-Jhf>W__cLtF1mZjx<~(cbq0f|K2?5xkz0cdTD7C4gXBu}6UQ z`KSM)D$ozq^3`MnqjR{QSWv`E`W1lBs-ovjneCnCo15I|1G6^BCGfS!;|AZMQ*7RD zz&yjj@}#~G44r>v_loCHpFGLYJ^e%^;_hT#P(GFIDY|ZZ!2K3{!1I-*N@q#zkbTz1QK}-&y z%x$^t4tow$p?8itf^P?sZPvGod!Z*b-Fh=*JgOH66zc_iG582rWfhC&hL>R zc9P8u$}W3Y!;~Il*p8x)1Ks=MaD1QR$fG{KFL4`UjnWh%A>KAg<;SrdciM+n(37t}_6Bv&J9r-Ie>JeOp}D~0{ZWXC(WZ!w11GTn z?z!2zbxn?}t**+jJd11Xl`yK^vG&3)wRx?xd+Fohk3tQt+#z$=j49mqcPrFd|K56! zw2fIBu*QafA7TVlG|M}h1OX87^AA93vcB{JkIv=cjP@u@b=jPtfZEC#^%*Zh?5s!~6CW zZ=M)(njU)2l$^f5aeLG%Bm#%s^>|xLcp}wEScrh-@W=DtJuf0EH}BXFE@A+uI|I4@ z!6WncTfMWJ4Kc=M-TpQ|HefU`Fdm?D-PpuoOYc5Cjd?&F3s1<{yu=>E-c8w_IQpW+ z9@6&jS7`QzjZhLf(>hNVd5rAjb&F0j&7ZITkm07%qSW8&tu1|${RBu8{vQpnJj`c^ zctREt#VgX7+3M<}0Uk*;GUDi?tWtUiXbfPGkj!IZVd%dbOkv)f>`zlhHn*`SZ9@eb zK=Xqe@&+lww8Ua&>Yn3bh7q@aHFkeSP8m^bx5HS4QJ^D_-g=QM#GR|g=dwnN zj;Xt=3=*TIyIVY+P6Ob*Y#TlTH;g)NQ@&D!P1gFf7J zMs7xr>CR%qNcotz7uH8{9)6EED({nX5uR%X$~M_D*NgJp_R`9* zOvW<1a%ndAY;So=eJ3S4EHHllouK`X#0>e$pUPZxy*g%RL?cx-s#dqVBSbtmp|;JH zR`8q1w}LvF{An^Nvy@rfbCg7StVcBnWR1<&2Z(b51we~S$Xoi@e11qDq8MIUa95;ny!WBS>34l2 z5tIqMYj5vR9W-~2+9`62kZ`UB4XGC6*wE&^w{fpnuYSfx9oG{na)E&ZDlCwOJ)Xa`R+ zs)4tMH|J!aKaDB1;oa@n6RSowz4Un22)IsxRV1S+PLq|=&oBISJ8#FDe$P_sqV6jd zX>Jk*b;U4(gvn>JECx zx{`f-_Mmq^lX1xQXJs#5lrhr7M!}1rV;nr6K>`V2M@fwI_A8sc^I0 zp0gmUIdd~i?CyHC5!am_EGQ8+-Rl@4nzQRmQ(n7xt79MEtozG_*M9!1(z`Fp=;;@Q z)S7-jPowf#HDG@$3MmH4Gm^~of;^(Jhl$mfScfT%nyO##-$=op7?010um42L@hDvZ0-~cY;v=*!HI@!eJI{XkK;@Brcco0fYMA;#&g9&c`?`|) zH$b$vW&LdKYQ%faqZj+;I<0@zZY5bj+%&@g7^L#@uV(9X+oMnD?&BG32J@LPccF$4Pu9Y^Gz}B}hhgyTmnGKL@hyzr?!f zQ={PV-ma|Nq|D(u_q6Hc^X=|kXKOa^T0ASo+QlmWNf-Gl^-aNM!O= ztzw@|=1feBrqAj0QSu7^>=8^(OpoYfeyV;EkSSCZkCJ)%p3~ z>H0(`G;6TrIXQfXLo}>P?mh^oXTashOXm2w0z;de5S>E(zMgJtqQviBq0p+B65DJK zXicS|P4!Es1^3d<^w|mQuH7v9nV8LC{du|1XD$d0*4f2Tr$dQL+D0F^ zNx4JxdZQiipUa8z4=G3axiKZ@JT3;cGEmc6mCfi-*CG{8rNM$UR~f7PeqHMRD$mVA zUu_Zx5f0%jo$TYk&j&!Jxyp3kLO`6R%bXuh zRthUsm3k~QndTkL%oQ_2+AF@V6aMNKA$wl2#isjiQ~xAO9#hqee>lxX6U18WTIo>X zNr>He>>!*lGlsQ936Xn7Ap% zp3$MghqW4L|8vZmUciG*0^a?;*Wl^*+cCe?de1ViL4nc_$RHE%hFy|9@{7ob$DTM< zZx9n{sVIal{0=W+*X$*@kCb3`(Qd;OT`z7EAQUVlG=TSxBV2=68T_ZrvL@v7Fw1ur zRKN5W;x)yyPXrvxab z&Y3T8lNH=jqC{xWa(}(|QZeCl?ZpD}GCNr0t{y@wVcT^}r78IC;l_mX+x1;e2mfgz zX_l^7+goG@#OvQxE9)!UvWx`^w(xl|>VCtbcq8m^_-r&gipjX($u^BVsy$@ZM*%Uc z10^WVUn%f()jkw39}m~(50pAraBh&WOQjy5g8J2yn(C#eh|ll$=>8-Os`>rRGyVK5 znYFN5`{H8O|1z=ZT#!-%B_Acko~*VcU72_{##j46PhM(6H5H@f7RA075n8Xa0 zxhkgId6*G?aabOa7gS~hw|?S?#}%wG%KAL74~!lVf}1xvUl>WtvE4$vvb!>>Pg~dR zP-ZuMq(LzJ<0uFuBUi%7}SO1kqMd2a3xU8(0QgUBZ!9AV*V382-H|4CxW#TFz5*F&HP zmiD08h>Tv~xz#&n^yl6v#Yzb(*7sH%B5FMD@`JhNWo1k3TUtgNBgzLT^yhPz2X>LR zi&66Rae|Tb(CS^}_8?sIE-FMzIr7g*yt?a2Z4sZk?{}3{aT%}g{}|SGSXuYdPAO91 znAO~10sKj-)n@CL9ul=K1;<;>`$%}T3OVaNML+(Fxt+QDW;zYOJc#pQ@qAY@FT+*}gRpVa=}gJ;ME_buZpimtL3a zVU7KVHXZdg9dEI3j6B?7QbLC5JX~fUX@=NptG5@)o8~BLlnuDU50azk3@>anH3!3Z z9r>11Mcvx#eiQIsBK@!z(@mG_QvQonS8AU}{?zVz1cohzy5)x54mXO|+JVqMHn`x? zl``b|#$bP(OrZvLi?>FiYF)lK3lhDbr*N4r(DNW*PY1F{In!r6-1RBFsk{lfJr0yP zX+|69#LZ}VawwBLoI@U@(#S^?UfTyqe)#@{G56Y631{ar{9hfwQ3p}*`X+vsS-2zo zauHogQ{`_`W{oH1_oaANaeoCLyPo|>F~4#&1<`#Gx=!9rBhn?}GM%`9o@7sA;xYZ+ z)B`JTbYFZ2FOY5UPnEEa@gZCs@iPj)Bx;ebS<}q&UH|@1&8cMO?r6h-w5>W>?-M#b zrl)ceJtjFkhQzGe;+Q+JO9_8P-4?0vOc9ZtlTwPamoybu*e7CA;oK7*cWrSc8w0h{ z&8-L=EcBIcd$$aR>20dP>(WO&`kjL<`fUyBV@pigp0aUz7X(VyY0qqaRMB~<>-GK3 zR~@LX7AsAj&(VfHcBbC4E5<6zrpm}`r`bPBXe|jmnoqd6dn}BoT}CIuv@Z*<&Jr6v zEb%s{o8|EXC*$6yjwb968@sw$1@FL+p&s3A{jpFWJIYaDub(;1Wxmlw(CH7dk)C`3jtx=MXOx4b4eoTZ;BBs502rL2tmtP$4yy?%D~ zHxM*hSaRX95P=5Kv6K|uRi0_8di~_pe$a8?Kn^_G#7o{BcmJpg z3bVges)Atng|;Fheh@-v@JtyZ?5eIKX1HIK@w}OGp_)rrMGO&ZTAvqrcBku6K=q45 zAqD-|KfLN?4ax-ACF`Dg_knKE!KI6JZ|#TRB-?D7sr{6itl%XskW1B0%6Jv**Jgce zc#o_0Tm72j8>g=w-0v)YdgZiRn3y>x9g?vaM^a;9?6 z2pRhR-Cg@8A@QvtEW%aYk!X05-q;^V%mhNqxW>LC1 zdBw$D;k7%xLi&&GiMlWPG@xA-(V%)7AOsuFZZuvE5pXRuSuO^@)AH#OPUW>NTG74C z>+yN6I@P_+#;Z;~>Z`4G9Q{}nNB#&3KVw~y8`;|7nvDJxN|P>rCJJ<<`uyk`Kr+Z& zHJN(B_A8IUa4S~E(yI$9b*VEkjxiGDz{!&L0~99)C-jN_#no31Qr*L2xOYPoTlHE$saJ^eAQeeB(>23Y;H?@Z9 zfg@Xs9G<9!vjuEKDc4}T_O`X`;!@RA^FJ}+7M3ftaC0it%tx_ug`A*jj`~vf7~1q) zgH?%LE}WylWN2hPK)e=-y=ftVBSJVen$gk$!NDuqA#A?f;Tcz4dJ@?|t^+I6s!Zd3AAUr$yOk;!?MQQ)}@ z%|$m7lm_wx&wUlx?YN?k`x_+`3Oe`%?lHap`WDB`N;g$V1QW2U$BaP{FH1(<{)MS+)bs^^3#? z&KdFT{m}TSXa1*-xo$PCqGlH?hz5*Cm9}K$l~zd-XmeA_kIhLS-aoE5@o{qPKby_6 zo@Iw3w{U8xZ-rtDpBFGf2$Hbz>f)8J>*Eu2 z>uz$uUbvx@h>E0StDkT34mDMVY|bC(re&!}2B~^{;fG*@&hEpRfZl+qL!c7_;y%IL}UAXk>buRw3_ z@eVuh?~mtMe{tIzt9i%ujeQi_N>hP;SPW9Ss)?L5Q4iKX$Q2yP^ogZR^(+9sjT3Ow zEkYxd;GnTdNy=by+0I%$7M8J!Tlt^504$#2^`5|8V*Qte{(p^ozK$fWxBm-2NDEvu zy+x30%bKkDfJh9Qc{9&u`W^5d@P`qGA3#Z)F}uRF#D%6il64tMoLsf9xgrJ+#cP~1 zN>qRagBwv1ZhB6F_;|qWU-wHQQAf+o!^M^Mw&+4Df!=1lkv5g~%CTEu_!Y;oFqGUW z_nqtO^Q473|L|F|Ur2`~sr`qhP!mIOOJYcmZ0ijT^l+!q+8-g3v-w@r;=y5ABJlw zJI-Fy{nvyT$L#2D4xUUriT)Sk3+hc|gqsIkGk+AlMf9G`Rw$*~RA73aj{0$%Xl?7C z+eJ{Qevd56Ly$H3ryhKmCzN`UBQ=4w?M(@&ZCPuR#xT`pde|`*&ooP&@r?d#vam_` z)*v`HBAy9Y`wbQliaomr`9jcuL3f&~NvpRqrNu zMH#Amr1-+~@_g(Enky^{s@t{|5AG-TU)7(?EpAx4KmRdk;c&5YH(+YQbU$9M+>ns7 zkrCk|$%*#_rRX{0L+kJH&!b=5;z|xATQ?-z6E9?Wk;Y8vM|+o(&6YiV`vtt(2R@rH z#|vwO0lUi?e;aNB9-#_Ylu-flrU11c+0;>MlG4c=BV{~X*x>)MA$HK*a$D_kSJk)p z=vFMe+p7Bf3*juK|DPUSLsmMQVq1vV#Syk~-4N{u7{`_hCZU)KO%OIQeF?k~QbqO+ z6%)x0=ZnKe7E@C>Yf9;V<0I{L2(Uq5uQQ$Ug_z?yX0C?b@-hvmHZSgaS9V2WH89Us zZwwiqpk(a~yQcXAJvhfsxqaK-8E$-)v+*F%2Zf-a3J$43dt+~Gw?sP z`?H_x&_g|)noBFmWA99+K9D|p_1%c7ckJl<`+<)N1JCh_r~VFmM)fo482y$O|MKjg za{NFK(TY>!%dWH{`{-jd>2CYgL4r6H0+r02zI|^Y5E+lO_ z`B#xYj2sUu^^`#2oo^(+wD8ux1cLeU;JMCOGT4uV0RW~-ia*YW{>tv=8Ps+=_zqL- zek9(-kXF3wf@z!@*GU`&)aJm1v5lZO6=J%Y7YK&w@}g9#iqE~6g*D!`ZEU}(!+dTd z2P;aEYt9O_5+AdoS-60;j>YDN-IG_C=X#F;FTP$dJ~4^e8aL7K4A!e0FF(Hb>>GoG zN5+mjxp9@E+H_+P@T^k5zh?h1Lr-Ihs8V$dHDaSP(@E{=HnbIpC0&Vf2E`jT0Msz2 zd1NpBc2kd(f8Y|5IB}L{_Q}pqrvgU}%;qQexrD}7x#c-gWA4vA|Q9Afl_VT@T;Mp4gX}`SqLEB*b z?2FS4pa+y8I?GO5(|0lPPZ_3Y0R;Z^s2U~4PP&4s^MNa^-}$Sg6d2=v+6b0F3U~4H zmgLTtv?tE;kPYt(8doLCi`#4xJqs^w&#ovip}VX6;P-U#c|pd{9=FQ0-Ehd4+*+f} zch9(<&_<)Sg+S(;a|Kk3REeE;T)Ku8g#U%^e6)x|sTu9V9P&#Qoo|Ys2#j}`!Bqpa zB2uT~EgO}*34hD^WB5_?m}Xeu0w}$J$@n`JWR=;ReYNjNMmjSYqDvcch~P+^CKgp_ zWi}6q^P2qPKF{bJil74rY3hGZ=+=F8J4dZ{hbWmrf-k_H6APcpEUsEcj3Ji^thO}{ zZMx+hB3E{cmGVtztbaKCOuVL(>!Y=smi&gek@d7i{Y8k`$3S|C)G+{vTgq&3tB*vt z^G0^1V2OocrR(H)QpYP}Bgim@6GaYEuT~S5dkp^nkVZ0kh@Y(KXov*dlXw#gPVKhgVEw_3f>XdB?7vEY$I9FqovYw_gaQmd~$ZczqtHAep;SI1e@8i=!MtN&riKnt2@H7K9#lcOZ#h zurW?#yO(+XO>Z?*<5NM;4`l*9m=Jqcd?@td*}_P~)i!^==C1|LGCIAYH-?3ETnZk2^E+9*#hzcsYMA)?*GomG zx*FC|?`Mg4guKL#Ks7$?JjX1$8{+!Ngk@78f9e#4^`S1oV>+{WL98f3jlyNPW2&vi zF;^WGBY}RUOfH!d>Ovm&w5xbFdoPF(p@T4MUP^=vIR8DBCJu>y^`lmC9v)a3X~xX2 za6qq%a5Ae-Upw0bqK=rG@Pk|fRyd`asBX@{thM@Dfhz%}h8u4lIfa2B^uz+le%NiA zotfmtXylMSh9&W^l!$V_`V>f!#2?j*G8aI}@}xGzP+Sc6_*P!H$r<1}$6696%2aNa zD`O_?Bx6E%kWNuBJ=n(@cSMb^cSHZ8aj6VtQc=*EC-bNcT~^WVDo)B-`+Y=#NdB~Z z*Yi@#`@PuCN7Gd3GtrQ?LD?*$NyOgB~Yg_nCoN!uvUsTmUBXULn z;rGLG;L_yo4-fI$#XxjkZ1ROsJUZ`(13i&|Ty7vs+urv}M6(Oml83i={^P5S6e9=L z2SIxDY6YICEqk)HEbSvm36)LNww<@)v=4rI`!UgelC^`D8Seo>p3<<)2{X?^PG(R}FL5F= zS7TmaLe9>>){42wJ$~mvW*1!T{6115Rl|kF_{`FW!@FG zc8;&Ft$R$I%9KF*D_Iw0cV_xIJa%pKU>=EYr?eU?Zu9+>SB^@k!<$WA>P}MFsrNhl zK{YAy9BND}gHO(E4Pp5+hmSJ1%?)@J4GBba=tl~{7uSRGmEQt$yGcRn=-a0 z+oDMrD{wMW=I%f@tYvUw=ai$UXZ&1pNB>25=X%LZtL`KFzwZsITlZJL9Vg4~_Cl0X zR86N^&U)@veq|r7$USx5<$B*M9Yf04ky3NWyMrvW5QMm>12Hlu32i#FuQKA+OHo*0 zAq+A{YIb7mW2n90=)K1lFQg*2P>q4q#SjePVs>Ti%~RYZSJ&fp&PbeE7mi!Q7O{oG z%YMCsS5*5#Ti7CB1|F-qM@siSzfNpcX_Fs#0@|H!Hj6tHv?M@@$+Q0jX6E^HgR}@2 zy8i{JxAqN}i%zVOTf7fWEPtMq2ZaNTeGORdcgjW`D}pxWOAe`z3*N3W z0KZ{&ef)noW2MsKxrr0{e*I}tW|O0eK-G~wV-7$b#(p-mao`O911M{7D4eZ;GRwKF zFi+>mD@#0Hu@2j|l15VC1&(8XEz5aD_LqcKpN(v#^wW!+`dERXeWcR@$K=m*qq>@9 zyWL!(%M%?JZ=w{DPwp96=oAS%?9W3Lozs&sq_wluj9jSG)0GH@oLKwG1#e%@Q5)|g zsG)*wh@QyjH=aIZPIJ#i7uL=etYQ@s5-Y0vyOq2hh@@}VwzQW#uq+3U>+obbJmcgE zb7EsaN-laMLWutpZFD+Sl#lEm7aAE$rwwK>+Mnva|B)Cay9)|i5*0T+|2&t;)TUQ|~yFLX?Cg*$4PZvrW3nQYkf_C$^B1#z3N#(vg z`Vz^N1+*fkb9k=d=zt{i|HT2L$}DkM&N(FKeX`}xM&{Lg_Va0%K}|Qyd~|Km4k)Y9 zLyR+}t&<2*C#%+^oY(>5OB~u>MpOsKC<3fCpfG?Y#JfE@dPwcCn9c=O=O({^9#>AH z?@~EF{}9p3@ni(uzXmyBg_M=aE+p#yp}HD0oMDk-HoiCZOX=IVtm!y_g`#7qf;td? zZm&z&0ZSQF)u{ZFqW;0SZoGP_1X=RyeYcQ`Jq}w<@(^{u^briv5J!{3*e@d{`5HQ> zAM-EvF5^D&h}syIdqgVgSMV|2ug{s|Fx6p8=1$;w3R}w!GhJ@!lFDz+;Fe6Y43u57*fqb@LJ%l z76&OvukQFVhc?>Np@QQ*S@X6qUzwN-9Yc(Mt<0qn=^@iWNlS~w=`dKLJI`9sr7B-# z!V3=XLO9U6V5DF4ca|K7c=~0!LVOpc!WhnyxuGgL+L&TgUeD~}Qo*rrsaJVL@U&!p zu>Q!OP-UO-`O{NV^yrqZ_5B z6K|1%fAm~#VXhQ(r;CkLQ;tS7nrkz|0^7S<BXz5j=*kPyk6BAwfIfL; zzHq=G>ub~zi$S3k?3rUWQOh2Q$vV=ytv06=7;N+WTn-w_D8a%dp}(BwhF8_T?h%)4 zN2upOH7@?0uLt=hG;oBmYN2;-c4qk9+g$&IEdi`hTeFlS@E#|B*ds}IrFLstr$<*f z=CjZ<8df+DN%56Z1tj_lnslD$L>pFeS)lBXvzJr;6(v*iFK>jyqJiw5i$qH zX%jLf-MrdRpLyKDwk#Dm(7cpi!LISC5<2fTX0M0tY;?@xs8@+%Z*uW8_5zn_vqG&pL%qH8TTQKW1z5?f=9VNO~| z4qmXLHK+|Jv5AiLG^_e$4ZFIe+_noYbRw_xe`4F)YJOl31bnZ9u8AwxT{qiXq9#2h zKKgbtR7`-C6{5)9yfN5;Z#q30g^shr>kNq#kxAoVL8=odIKBSr+4j*sfhFiw@pC0R zPv>I75M|cV`;mIo^us<~x+e5;{)aD3X-bP%~`#P`lPoR?Qz)#BH zxX&GdV|D*Tkc4%(R4TD*__J`bzfC@S#=EQJrg!qVIMHcqU%??*2CNNdhnVGfs3Rn)eGYjeJ$( zpV8GGfO$Z}T(RLTi4R0CdGfFY%>T>a*!cd3ggsvwD)w{+o?<{47MNQ@yY0dI$5xiv z^hkTY)~UVQ(|Pz%Af``gIW&j}7Js1FH5cMWI$4u@MK9q&=j|yT?lX>hxq_$nI9dk| zSLQNiBcnsFp~m;xc5%6RolkPAP>!?W4dj_w$5dDJBaKVOyR`R%fKa7}_S)isZG78G z-KO-{>FyW=592sL()30-;me5Z1YtM!7Hlcgi!6Hk&UxXZbk=q5B>tA-Sve)Lj%~Wk zDcobh6UpCQ9LdrDHKeVoAmyc~&eiV1)`DQ!MnUiyMQ-7o-1w#=x#YlM=7l>Mq=R41 zC)Pl|+J%!2p>;JYrpp1!f%3G(j~o^jLCh(r}EC`E)QOEiMJY4Mic=$T?XLMb^l)3wO5=k03UENJ#Y{wc($SMQ3zp|8HsQLU<^6|Z zvFehpA5m?BreVAeK#1|@(D7B#!G38*?;PUPPaWN6FY0@bUvdG-i4P_x0?= zf%gc*wHb&rbD>>MP%RK1w@&;us;Z6#PVUKdd|MMUEC^twhY)_51=D0_a-g^(^WQI= zDKo|+3`uY(+T-x~u2tPP3Tj|~+DsVXt$*fKP9qOmTdxCdnQN1gIi;gr$}M&OT=jF~ zR6vAsK!kIU$%$sSG$8__T~EG zj81eAtKRbrPd@@5XMmoXp4>+9;{5Kv=|sOLTesShciiM#z4BT8`{RdvTnd%5ezV)P zyOX|PLLb1q=9x2YbEP(g23R@Vqpnhrm6)<@@1Wtv{gvsd4X=ZSxAIMReDP#^m z!g*C7GN~$cWidND%XGfcoKE|3M9>9Fm4868_39S>+ycIJSrWs^ij#C!BCG$D{M9BY z@Ad7x=^$zVJ7IQxd|JUvH_T`U?*N-QLi`>+gQMvFf+sV_jF z(u4#}hrG#afP-U1aeW=i^xVA}ki)x}Y5{J%g`^{eoD2I3R+ctn+X&>!?wRk$SM5LF zB}ryL7XIuu&#f}bOGU^*0nIgF!NPm}{Y)~vuYc=LIU*L#OZAlP3=JnK+wBS7WWT_$ zqFmsZ#gP{la*7z;&~8lvxTI%;D6uGD#w&h#D!!GP&Y+q$em3F+NsXD*BjFLbiYQCc zM`?9jayR)k9kv8i@``vLEe%KzsnJNjrQ6z?4XfE`k%5yHaBTBzLUz9CI(Sju&Spg5bq7^n#PlMev@Fwt`0;ZDLN58wI8A5AX}L@^#H`+QGz*)Yx$!640i_noH}itk z!t!)jS3Qcwpw)p@E%5Aw(3XG? ze!gN8aJ>51Y8e&Ibv!BV_~0a```{9?7Wk><(*u1N%45xD8oEt~XLsivG~ixPc^*0( z*|!4?w2^0`mK+~(cgDVdc?K5;N4`s(cO#psHUB6u;O0KLT_uETZS24KcWQuf$lqZB zM6=17@_6H9?l=4B9z_OG-Mo`i5=TcIynWMu$^araH|R=Gk|yjRt@A^jgI8W$jc#&( z{)mYecU7|@yD=;$viol`p9joS>|~m5Z4#Ha=qPZ#z@(57(3E_=xxDCTM$|bhL#q!o zAo1(K&PI~xv8A;FXICeM%h84`$8E4`(}|Ctn4ii_Wi|c^%y4>)89REhMC0mjtf&so zo9m&b>81t&J}sBotg*#m*n4|%2UOb|YrDCTJI60Oj07(}yXc=AYHQSZaHSF@gL7uF zmoAm#k67HU2{5kd$`i`fq~SC&H4b#d#$ZNF2v$u9rns-(p@bI zIs9PXvCeTK@Zr2lPbnXA-42URR{cIXwA`P@jhk|^TWi6ucs5`xxIM%^>UjKn>7rx``7U7kb$(pB z*dAVMbL-n5Lu}JN%ve*suNN{MC;aJaXPSg$D-Yv6Oak6k&DqhB%I`F!}%GQaVj1<&G|Xmx@L-Qql}O3JgMYLA$u za%qA~&j{`dr5^IXX?Z!b{J2+IhnJ=0OmE-1u5){1vaL;YhqMk&G^K(kZaeQ-Sw+UF zTfc6xLY^+=t%M)GQ5U9lm@hBsPN`;N^y^usnXsbqS2Nklk4!Q2^A^?ZO*b7oKFvr8 zdN=4;XSIAc9r!}VAzF=QdJ>XPwD==J%DdvEg4fA?rgGmfCHJ@&cV+6h3zMCzd7EF$LByKC_$o__|;`f?CXjUHQLr0ZB8{G!BodqY3(-NecS71Fm8XRcEp2c%l&0z-oOcD+Jg-H$JNy9jn=~? z52{yGcp7+~QnMEatu#`uNr_W_e8@hQeCrz>zh4CE^MpbZs^MU&Yw^5$5VHu5p8+YS z@8u8Aeci2PJYjiQdGC(kX&p4dI8RE%tp=e;A?}pyKL2B7^~}i$8cw0B%vG;n+_H0fDF@DTdk>q6`>u81Fne~!J=rR~Ga=q>4QBRhJ?=^t zt{(CTH;M9?{QHiP)iQ5eG(`ygf&BuXb!O7QgT94xTO{;49JqeVQsC>~I+1$=tomI+ z@4u!>(nJH6+W2SW|KvVP6l&XdHB@a*9ZIk2=uC(6G+#>h@$ypH%^a>Y>bLQ;=0tX- zh{>7OmAiB-FEz3ax|P&tyBAC*LNOPjiQ_b^M|Awja!`K13At1L^7Es6ev+d2*`xvw zwJNeN?A413FAhRlaS5r#2V-F#b-wS~603L8amm&QPUNhQMGlp;D=sSB(egX&tp?sp zSB_q@3K>S_AV`5S!VaQ4SM>kc#-9Xn}>>e`Q zyn*Hpy>n+l0lV75&*GIeO-iaWda$XR5hS&H$?2pDD~4#^JzpOkow~i8em9|IJrnv* zqKaPoA#jq(*(t4*7cAmXcbmPqF5Sr9gD9d*+rjckB@ezt;pIEJ$mFb?S$Ot#WBrBm zy8cV+X5;E!cY;3y5#_$tpIP0yTbefWPcQncMJGaOKx_9awx^9M8%5=Cc;Y5-$58gK zSGQs=p?d_yP{`0)_?n+2gRjoeCBX|a0T!RQ&NWI=j%npSe9SoJa%Se8^JO)Id)Kx| zOJa0u=;nI$F+pVB4gZIFlaDl;uz<&i2LZnPewQgj zMKyIy$+0ErjQe6QN`2nuB|RuwrbCT$F?ZIdBhRtH8)m`+1Ywy0*B%aU-x5@P@N9hb zfl^;}=z^hw=KMQPiTjZaVOM7gL^B2!U6H~#a2oQ&`DC#@sG&JnL)gEPZwdhHmh@(!zL;&Tfz7}pQF`4EU_3x6j{JW%3C5Dc5N7>W3ov&0f zgvPhA!<%*@+faH+U&-yS7|pDK^b6d_Bseuw$DiD`kB0)`%Om9=+t--AsNmFzdaM12 zk&jh`wbn-!)mI0ZzRX{{rMj74OvaafG-9cL%W4=2I9~SL;`j~V90syT>noWr4W72 zXz_;0HS?C9LC8UUd3g%XZz}~6=tkOTM5D~Lm)CE$e&8sXjg2y=X-O|W8U6bEH+m1B zS480&_vPF)cF%M^vIj#T0)W5h@W-xuQ9x~GlX?Lw#MATNkRI=5k$c7N;3&zi8YsQk z2Q_aU35PJh9@8bbr%r<(&7bYWTFKw|5F-Z*f}DMd=q# zz|GCKNfLYKwmUEE89`7TcXa_d~9m8h5fAvYguWvPhPUjxjsTYE=QGMC|r<-&U#yQIqh`FWl zSo!@oA|A)D{AyB+C?x&dI9&Y?Tlea`KS)rFAs1BDC2wN@`{1Pb=X;TeE+=_znioubzkFm59XL{Q0!)}h; zNs{e*B^grucGA{5eA)Gzy>~Xre|=C;#l$+9lsRWz{k`lSg;e2&CB~^y4!G=uk6uoF zT?RZITZykvt6x!hU$N5EHcN2IbKy-*MhUU3=drWJmbPfKndi(f$>R~a^{t4QkK*On z;@K?`W^GOKDff%Gcc>c|A4tvcMVJ_JS6X*4qI&ixu(BKpMH^inZ{!Ql=H$*E zCoSSW7O3)kfcQEfx>$lwY&pwGN!PHPu8yS6mHG;)3Ql=E1oQe?Yxh=t<39neND3f! zzwx)u__iS!Nt?!OH3zKY%5I(B7<{^EKl{_TciFkNGaa1! z{{sE*ae9z#k>nSC%fs37QG@=l3w7hsnFX8Mz2Ly*X@P1p>CYJlskRaoXB0^B+2$0G z!)33Vw1wQxZr@+DSo-M4g43N2AF$$kPldFM)h8A;Yu2rcbcEbg%s7IjtzWAl*#4-)QO4(mjwQ z2t08v%HI*$N6p~(H@%Oam2i9Fq3=_=^`WGjj3mPoKt)k+uqx?hI(m-*p{6L`v!N%OPWFYk1FjYN39 zHRi6hYTDcv|C>5fv% zc5Xg_#P$9St)g`xmP4y`E{IFQp;yRC)s?#^-_^fC20aE@Eb+hw5kb9nuMkG%58P8G zsO!qnX8cl;5PKF|sk*}DG%F5Mc#hO)1(7^w}Hxn z8|yB7jd{qqNNvRUlxvL9KM5n6v3u}mQm_pSKhZLUcp-1L^-PudkCUChmr_nr2_oOM zMh6H#NdAo68NcPK^S27rW<~xtx#-6%fA9=^W^jXmo2^#JDbOnVpwJqrG3bO0nchY~ zzH>rH4u#d}98Nku_pp5vS63_sKfcUgyRZN&e*eBSVz`Hr?`?qp%wKC{Ib(SMmh^I1 zY;KXo{=B!g>Er+x&wiuJV`tzJoeV=#`<d-gc+J5{uA#T7ap~S@p3|QeAV!Yd7vY#QXr=yR-d|E8-ff!?U@Ey zN{pesDP!{Lx;Hq~Saz|D6kn7+vqew*;ahdUCU&gLOwd`KsYbgEddu(|ss3 z?xp>psI=U~ZN3cJzuHafSnBD@LWHVP0j|^-X?NrM)Lo|#UOVZxy{&nV9cMkH3(Bhe zkLLrYWC!_s{7q^Lr;5eCwsi5RY+Mo2Ah8J@s+`83%j%{e1Yy&+80c*v=cb zSL5W{9C*-r2*#bNi0_H{;L;tuYq`W-1Ij@}*H!8-c3AJ^rCLGq3sO^f)c+WMkdhel z)nNid9O$H8(|S(w`*bh8GM=Oq2&7d55DcSuF!H1w zd2v89*rAgCY|2fea3*8i@Nik)0W-K&b2gFsbzmH0^o>rKnT^zM5_<{jIr{aa;V7bP zTwigP4_3C^%k5#DO>Q|YpPO`=J`0Vuts>%P26)cfU{!Eg_GAg`g;p|0YG0 zTF5JNQrqC@;5I*}6usvLH>OW4M(xA7sb?+g6V-Jm$Crvd8zqa8&ZGL=Mxn-FFn?K8_n4Z{2Tj@0UMa zO5Uv1>}DSt_T*7Y1()S>qGp(h8AjNf(t%Ui9vk$iQcfTBfa6}nMjU zZ?#Au@{$X+&6j<2t15L#8j|SSjeo;Z^lsm<6k3FpE_ZuFXqn4=^O-In$g%PhQI9WnUMNKUn~Pk&&-SiIKs^#u^K`N+WJeM{DeUOI{>9 zTPm$qQ#m$ueGLO))qYJYpnQ^*4)b_#l8_4O00_43)6J&5i#IizNzeE_no}R>Q)aW6 zLLFZJ@L&eFsuEdo*iBTHyYk2RfA;;j8=E)1lTSquU{WnimfK1Hb27bH*2os*@ZJN2 z(Ofn%d_tW>l-ghxF+x`h@}0=ND0lt=L%yrDk$k>=3f)3@zn)(zbYnF#e(TjbJL7`P z`(e4eF#aQU?Y3z7^3w=)%^7}fPRL$SiX+0?;I~yIHq}-ucyjM6XPxKT@z;utE;q8L zmTbs02gmkT{jDa*t_xg)g+L_xH_3~&2boj-L;9oKY}C^|{>IYKp3GnFX65)!b-UJ> zJZAyQsPxWEi@bP-UZ=)IP2m{{Dc)FsqwD|I_r}Xh6!XY^j6*kzt>n)l<&NoG#Oq%< zPJ6|NXDPUR8)aiUO)EPU>T11AfOBKh*?H>Ej+G)m55ESex-wh>zrv|yL;Z5vel*^< zMjV?kPU4%Mim9F!7o1=}mj9W5v@yd3iNbu48m7(ow#bmA>2xxsRST5xmpF*N#m;Z- zR{!nWjeh4{?JI`V6Pc)kK&RP9m&QFdS|OgeO7XALLD?*QMtlQfbh|pDdk2A7`&lW^ z`GbLUB3)xhM@*It;qKWGjbDLZWY4pp&Ur!z+HY;5ACFzj6bhcHY$pL9ZBO}K9y(CG z{Ga)gda=HAJ8>4_IV>|Y$a(p9#!O$F{71xxg6*6*YyKZ!X*>PHOLyRqt5r@DK0kZ4 zt)DWu5$WCEo<~LohcN@t`dTDlw)$2Hm-!B7;d64P8JPQM-=@c%26gq(5bTU^y?i6# zuMXCVE&qB+znMoPF5la#>Z*kaCj9=1hluuT^O@2vvBHJ%wmw|^j}*wBxiPqk4}-p)FZzYVX7TfwvnzysxYO@4fbRuGcq`7P`jM{m2*z zN;~lexBdCFh+1QD6VoOcf9E6TVP3)%J8`J_SxSk6Fs<%7iK-x#zscfD)#p%?vr06+ z5)iN#gm3p~QpP%69;np1bmB~y1EcC5MMVklmwa+8HN^6#-j$;k%dqKQG0^Z%et26 zRn^w%bc%-qUQpDYZ*wig_k*;q+xmEVg<12T7qqS)wi9~A4RC!%R{;Sj^h}+uJF(^G zdA1Qrd3c#T?}6AjPO3df4InacZ(1z`=}GY1n>#qD0FK_#tfc$7scwhfFM3B`)3L_l z!w)TyO_52)BS=CFwiADu^)<@CyCcJdYQ6#@xNdnSWCch?4a8KDXDa2?}q`mgmvZ znBRKZGH$Wm;JHBq#My~B*Kzh+8>el&2%7&w*DwY+%jbr@wvjsOTR(Os1bBc}qU%}? zzNdZZn;TmZKXOlU#rSG*G)_jhv5JRFeSNnc&H|z!9Gtt?qtoWCQc8-t5=mx%%|7in zIfJf0{a&_NwP+@w(gEk6Ti3;M6wkpE;yKTMX*E4!XE>5D0=axbt^jT2A|8(ZOWw3n z-avrpwQBiDKdw;oqt&89mb)(x9TNQyEE5y>{>3{r+8Ds$ijo2zG zepjfgJihxfXJR{=Q5Ds5vwtf2jSJXM+2>%w5xSH(h_*qp6yQn;wAsnca7!~T)oaLO zWzi05G>IHtrxAT=+~~m?WKm-J&25CXMv`7gpG-WGIWCH@y5eOKn)X zXQxXlEA0-FN1-L=D?G`IJ0a?AGqOUGu{C*CZr~u@9%ZD`zm)mTZ?S=+2fI7$AZrp= zJQq1>W6XG*!^bCoh%-L?zs|z{3sKX4JUolmY)D?*<%PN<5u9vnT<8CGe){L_KhC{& zQoAFIsZgKzxIxKT9Y;>7z{-p)jy|qlMy5{Xr_`nTs*!U0tJF!Vvb^lFmKIb;@# zd=v$4_h>l9WbZ3h0t*+&)Gsxgd|`RT7pzuzTR5RmPGQPUGN$ld(v7D+}K@OA{ zca_{UYR}Bk&@W>+S(SI>VzZYh<{8wH|ArkdALXUB8&gzDj~7Hx!UBI-hAs3ezonuy zt}!bMiYU?sq1Y_AGNB=VLqdD03w4J_N?$5}UL%lVTWxUYp+C165qTE=XijZ=~iMnOexm}E%E?yTj{z80@)h?&HI73+=8_MBF&m`i<5I6ZtLOXBzq%o7ZyB2 zS#)Sul~+eKTK+?o3fT?9qH8LeRCSFoIH9u4l9Ib_xZL2?Wz`iwbj6Z@q9mR6x45ur zweFXJ!8dl9L(@#}KRwDeoKb9m?O(se-kV`3g=b66kP^RLTjhegq)&AcY@Iv&X^ZtF zQ9>m-k{(6?eDEOlt72oik$APU+0(=e8lbi+z|S73Lr;b8?Civ)jm_eqkL~OT(J!04 zQnIsi_?hOiFD2*}Gxhektqtr1HfMF#SA0SAtYp(PloUrjQntV4a@SF3kM5=#S~rIr zWgmWB?rFK(A9%cx+S7s#Oehvwa-$6NZZ1?i_)xBCz{chrQz!hz{1<{UROdqa1s1KF z_owF)GotIgc6OL~);3gl;)6%--5*d04EGC}$jVH&iyx36`Ck-Sp6R)1p;q_-=uN$s zuU%i+m2dW{t*>%nyBO5dEgTfEWtsE*(~l#gprQ_bgO^_7Dw^`rH(kFq=;`|pBS&KF z3b=A^>1wh}3W=z~Hs*;BLVPDSu(V}|gm(*{5L+1~X56g`_r2FtcZ~Aq5LYG68_(dH z?>v#eB7nwj;6XlNLVHEs~j6Vs7nG-4m8BL+n|q<6jpYyb0g7hxT_2t0xfHSJM_`ar|#7 z)p}if+hH<-n;!`ilg0O6UvoCehDzs&X}j?w6YaQ*>aqJHG4!j~)#3o$DYD`kpBQ`5}tL=;>hq_QnCqyu0$**yKLBD3^&CPvw8&rzNR>__jxAbGo ze|8W3K#92xYNuDC$j7bfsaSQCbZmS~CHzEZPb`h+y%W=flcp+{>t|+eyC`Pyg_L2M z;RSU!R#Id$j+A&U4h1x?U6=|SC=9&B@)lR!_-x4-EW92 zuI{gt93T!3o`^)ky(4^7(laWaFDVMo{c-hN8h2_o53M6Hq%!>b08%pJ@0CY6gUsDY z5U!Q%vqO2RvmT-q1H+PigFK%W_`cz#BxgPx=31-$HMbe<{_uRL%9$A2;_%$-72x!+ zQX0C}+$zqM1M*=xgl5{<_=q;@6`*ro;dIO^Clgkz%kBuf8@%hU4~KpMgS_WjIyXsd zpaK;}xL`IaB{<7)B#@Bi)OK{nOJ^B0W$WFbuv?LOX5WLD?OcJ@xfb*cT_Lhb3vj&nCm> zO1N22qCQK;DuP-ZT^#N}|~@&$`0YJjoZ{#?X#7=bf!3(fmCc zpaPHIRnE+OpPy}G=gu-%H1|@@Lsx5??9L0j+)@3}4tjycyCd&ei@XqLVh*MRCiqnk z_7>SE-ST5SEk&0uE!7cg%j=Vet2|US44e^cH!kj1`bKRx6kB1P;6iWu^aT?-o`!6n zemgo3C!dx`0Lv!8mWdOU6&8)nIgPqYW)?c*m1(bBz|Gr;a*+SJ;XBX5HNP!TM0QRPp}#!SRBt^3!;iB3jg5>SKuqNm%bj zA`W@@s+h~ad($t@I#fp@e~>$g2$t`|f*$EGy4W)bUEh{Ch>+#4+7ylx-#i)~oGo;7 zN=2=mVY)?kJgr~!ZDcN431$~L^f*>$@WBZ`5e`dUQQfa(qP$_TL5*X=)CcogBpyMr z?Y&=8n=(eY$W^Vo8^kJ{{6n)9Jw6k@I`D-x8zB;~k{j07l#;s961prap-5{rKE4vd zuO)_YL_DDix3WdZF|ExO--<@&*}aSRu*W)jUf$_vzJFu~$;^_>HACAt2{wF4yUhJ% zF~(&7iFeC3tLhRl+9aTi#_ z<8VrT;nCg*yE?S3(6C0;LfO=O)lk%~;6y?ERev^6*(WcBglXFd6ua$iK^U`O>T*F=CWp-M5U$FV*B4ctIe1|ta1Iqfg{{ueCQL7G*gh>2lJgrQ)wim| zylUZ51*atHjv_?dhQCG)+aFvty*{l9pABEr&Yy0Bi`CxQX}eqj_c#b7FBp?81v?*tm5kX zC0ws9tf%fp6h&@@hPhVF@WD~t52pIO3a@;U@TTWV@Z0qdIYhQgzCWl(z*7@QDXYsA zGIKCpBhvkQWiqB@=@7Pz?8mJ&E~+5cQ`MZnuzjH84a#TAi1o%9}CU9RxDsM=<4?R)!kCqqV%Z+n>VdLcJIkLRpcN(@$xGpfI2?V&=ImZG08pC9G69xv2gs4*j*2V*2ij!n=Q2agH-I1zRZ2m)`5~(AS z>F(FB4h6j~VyB1hiE<_mzH*!IR%}kb0HKL*IM7tu@A*h)-A-(VeH12Ulq{OQk8lgI zFdMNgZLVnpr;T=o=E6%6D@pF#TZV?Y-UOP10KahO;#55%u2QbPw;~ zoYHZUVk5=5gX>tN4c)lY{n*zt(K9SUY?H4j!9(nT` z3I}_33}ETTGvbN8ahv6O0nWuxMaAj>x~QLvKalIPTp%S)s&Atz0@Zn zRDikAJOdmZP;fn-MB)dPmo-?attwf00BiC(&Ms$Y2L_^5uLCL%$y9tm7sb>dUk=$@ zBQh%W=QTH+l+fn;M;W*GNG}IQSAXv9#DxO{O#3GQq!wJ?+<$@oFT^)NP7pthDfgV1Q|6CK$tx`5i{WF9>?P9;jY!47J zlyTu0f-p~3$B0A49s0?t58E=okj?!|lg(#AplSh0Ap7`#1{cT2FIblAj6Fwa{Xv8z z=&f{$F(~k0H#lwR;Gm_-2y3O?=}^;jTL4g}g1%`o zu%v}>-E|D#2#hf4zD8cIUsI5*fA@Y#N|l?NTu6l==Yp9p-KD)bPc|%J=uHQ zGWQ$Bv$F0g!yH)S0PM~t_VnMeyKwS@IH}|ap29KDx-5=5J%yo%k0%u4t)(*{{mzy; zzE%J!)}Fv<7;weR=@vEmqC-*>J*gGPSnFUfcjgoDKM3@{5)J z&~=J7YygT=MVe2ZovkXgv>hbMX5#VqHUQE`aWO1cKlHDwLsxcrDQHrg#QQnByin6q zY7$wNwzjrL6~%%;scx_U+@1Z2ay;9m;I7Jd<2#AYqPa#c7p+Y??KWZy--*YMesd3M zz7g-J;a5x``?P4%3Lbs^ediV6&a%pynwwXDeWvQ$o+{68e+1c3!24eg-&z|HNx({FC z#;nu7M96+zQB^oFsV4mhPhD|mg?RfYE2w8E4BfH39vx1<86!v>+g}f(4zCtFb>V|H zSEv7i(SXyq3s49>qwqjw)3;Gtk5rB6^8>5~o3q&5V+CRajf)R9DPwkgv^;p5AP@T9 zpbfa$N{uy#35|ojCfM=GO-v6$K|!J8M+hXQhwQ_0rp*lJ@9FtcUDDG$=qJeG6Kg=8|yZHV&ayiDcl(A0`zZ}Ip=Hje0==k zFNHy6#Ca!iDYbY+U$?f;kD+h7{%I?e?Y;4>v;&L%Hxhdr)i1g`Hwniwi#vqwxGk6z3S2-u|V#J%*>b?FLUol!1&Yxj4%lywZ&DrDu@P z?`X2Q-@q3UtACcC)4bBsqM=@{m8v3Hk7>-o3uf?tCVw#wfU4!bmeMJz8RI>0>ockz zet-?VS*oXicQmn^twIqD8|NaQ{5`F^WHEhqc1|Cwh&>2Zvwll{ZWalz?KcUHu*-ND z-@zURukYH5k&=o5=d-CegXiFUZQeQNw#`sqxzkDc-cY{Qb-c4<&q>bXua6^LrhdAU zH1`7~ce;Y-d)J@(P^cLR5iftCTQB3FUH4mQgwB8C0+zGPMlry4u>>tQ-Lp1)5ihd5 z0*xoW8c*9`H6rD6gqW~HmXxra){VcN*OciAL8{{92$V{0U;J+GNOI9%)mo2-E478Y zs~&c4hN9uY+ivusdtM5Ktzq`1IU#p^cNT+K7bI>@qkD`o_=N^iZ&!1ZIJDAk!#n7? z(c4z7CC<`9zt(E}O@u2H(yCVDbx7<@%v4+Vp_K%A5`JTCFG@1q&?5>S0ZkV8N{i=(HIBnoJvT zPYHWh*xu%GVyH2lFgdE+UWcV^8z)!#M-lga@@dc%u1meVJ2^EQG*4gf<}@=4GM*;< zIvkRHiKiB=@iB)47DM!E4_>{}N6Kiz_euRYJpv_iw*z-G1MRu@&Fj!_Z-YIPluOV( zF3E{Vt;@hP^Q5T(WyTTNe1pCRFCAv!%2i#5hU~F{;h{P_d*9un4O5WvW0P{pRuO6y zeenc)=hr0S@0GP{yz465t7ZIR<9^0Zv+GRK5Ql*?0k7VuCPt!g)n7XE{;9s9 z!NK4nm=GfHkbO%m?#7UYwHBw)&!NWAgDqK9Psu`|H8&=rwTS-ktFQ=}U@xzRCX5o7 z*Yuf`JV(-I98w3Z>O>2-{z_S*ki*0+CscLNRongp50eNNZt0W@_jE^No>c4A?yF4W z_wStF+re*--@=A^k^q4O&xlRZZfwW3sa3oIa=tpZlj__*+BHsBtmxQm*#Gb z<&ETD4p}uE^a!{0A^9Z~LRTnj*e2cyf>x0xcYwy$>4}W0@S`qm%+t<&Uu^lO>54aQ zm3AkRBX(YjLF{uL_9MUndY~v#U_sbjc{#bOA_kok9mNo2fXp+Q%%|SoY+^G`AkKR2 zy}qR%VzraYK3lNB-QB>eLdM`ntBbn%%9m;<)t?1!2b0V+ZaHt6aedJ#{k+jP<*Z{O z%A@6y-TxFG5Q2{_PeNBsnmoSS?4x8IaZ1;ssb4#&BGqH<5 zkiE^tC1+kkYKu<|hizVVYjicezN#Nys21s=(-s$i*fx^wtrd^xWsWzgv$GrGUi%T| z*zl*a#BeA&Ohst+a;P+TMc2eM)JD%*!$u|cEd&p&VY$n};DfJ=b2{aN-$o*|*qgzp z9Ln*vJ^L;Q`w9Kzhf*blTgYFgme=S72B-WGY})x6#c4s$3f@LsOi(-y_Ls>T&s8sq zRQwszxAZEn_ZLfy<3-HYh-y~?I8p38Da%#x3~qr?mhUa#B>pG&4!D*78prp|w@>Wg z+qSmhNgbRVX317n6OxXV2s8>C{v$MC>1liSpH^^pH+oSJ%v&s=+^AgqS~N6H+kDx> zy8>8)pdyyf)V2^*Tutoyi!Fh-6~53~S=Y&-ljNo-iXDxS)yVm(pNt>-ZaQdc3C_kZ z9f@dcxZ=0E#ip*bnxzTobj=ozp1uw$`v9D_)fVPjXQD7boG8{3gD~|vr|in1X1;Cx zxP?8~&ot9g&plB+yu`fCL(jfTti3xKhAb=dOgkFTk2WJ5*#3ihMePOT5>E{kfpK?8 zUc{K3YS7MT!)Ygrs;|pQujt($an^nqHJ`LWO_?jt(#M6SZBDB947Kf+7>7IZgetHa zXOxUp6=@MDXIfkRf4YCPO)=~O*PgvzrpVVESGw0G4TUn3NfUEv zp3E!_x7moRQ6-n2RiEdGE*(D2r(qIHKx@?4RqSz zz2Ht-4sSFYEo7fq3mMv0F)S&WmDMDN?`cy$Rn|;$1sn31cine99Ce#~o?K*ynYG6O z!(2PVfooiw7I_VS2b+ca_da=jlE7-3uAS8oBu{n=D!YoMmsY2tbVF^_H6{jruGXUa zuks}Yu>b@FW0YN5RYiH}{C%{Hs*s<@oJ9C|s*OUmL>WjV+Eoy1m-l8mE2p!&|>g##ZKU*aKOmYehngm1=VXW%{s9psyw8b^e!j)NmjUvH!m(~od# zBuJ#PLih$Gqb26m874zX8xadm(;9a#1vU0ak4*E5A6pkM4r5r>Km9_k^Jy3k^YL6= zU47=AyI}LB*fehaA(C1yM4dDWhf!8u&fRZjSx<`NJwAd&(CM;y;=-Tj?9WBgz7_3r zFCA}mkLP;r&1=3yO{Cj39qpG+@Xa1CjBEd#f<}asMR2B}hqy@Ixf;rs!~|@!$HPfg z<6M%WO3cq?HDJ19cow=58xZNoSiw4ew7@o5R63mgXy+4`1r+wWrhzq+tMtu9)hsB^Sbz$O` zV~!dN2oEetjPm%hbdh={o8V(mdYLT*##A}r*{>R zMSku@E{QN z1;YgDu?9{u^)X?lm~!$vo>g~#bh2f0Eb!VpG8<_1_Z8b8#~d%cXlP4WS#J>neNchK z+`_b(+CnlK4Y0&t1~;#Q0vR4mNDC4rY44znX08ehs#(R_i0f-bi=Md4F-caao4k@J zsdqo=ic^s}`1;AL)r%lF;~w?fYxdVs2>Y+u-9K|%anQCOsi1=R*>i!hiAl~kQc^|f z`6$IlpeQ>=ovi7j3sCXx+$|9ccVcxEF z%R_r(-HWe$&-v8(J>K*qq9}2jP&+#dvCjrIe1f1oX`nv$MuPDttkfQXSuluSo&kSL zx;bpTtM0&!u3mxX>>pZ3RQ~A$xcGSxOP_O!1-{H$V&2lrD?L5+GH+*Mn~gpe!Fpal zLs*Iq0iW-j2KcmfbHFP!uUEI76eo+HgGgsz=Oz!`k3s*{Fj*1rm=5}0 zAXoXWztRP~g$xkR9rEbf=|f_&i9^Gt%Vl21Qpfs$ncy=Fy61XP@uv0}$$YQfXI%BR z;gjM7Db5Lh8dnfFN0g1RS47kfAb{NEhXp(r?=G5 z=fo`zZl%9!pDLU1vWONlhWgZcUijV~$mxfYSzBZuR)dOw+Eao0{g~k9>ac*^eD*c% zP299PmspX5_YUL~P>`9)*5;}irk1eDxy^O(u;^h*#EIYUC~)AH?D`wef4nH?fU!jM ziSHoM>6DnKjg*n0p<)O~Nr_QKz5XlhHvV25kz&9h9@-A0ccPZc;Rm_uiGs!lr=!$? zFxqZU&UQivc~m15~3@ zL5ymF;1i%zpq!49yun*JL+;YrI&dUAl(Q9bRy z%>R=_>i^!{-z|fKhKzU(iqZ^X9LNi#{~uzl|3*%Z0b1|)ct=pJ5y-s;=TwOkKpdR!bC8N)TstSgdv zyu8<0NMf%~(>f!jXAB@s$tZ_Evg@v=73bcq4I7)&C{ILFW2z@6vLdqKy7?!gJl<<* zH)DtiX{bdcd3rc=d;I0L{@@?s@fjTSYH4_@I*~mj?osa0?cJ=2)9Qy^*MP?Xj<)3( zj(;Np27)Vh{tZP42_O6$SrZdm{u_P2cCGE-DDKwH;46QJY#eL^M1MyLDhdQw|Biw` z1{2))_lpcy{!cDgeTyL2KJSY)hZTfQTT z7sKFl=SkW}?i~+Qw0EzP0WY`hW$5+)vXGa6H3$(Anw^`X-!o>|p|+51LoHoO36YsD zem&-933j5=r$+P3wjY${cNZqDPuCwcCw@f7HE+!>w1)#%YxBIcn7Auhna7dEp*&1_ z&hYpuzIzWG&P*=$q@9(!Iz7(cwnGnY0yj=`vLmD*h$FlHg*Nkku0IoxnWsF@<%!@v zTEJ!%R6cDwXYt!h8@>`XM17?Gdx9=Qfr!BR0|_=VS{GAE>`0=!Ltvq#(vNs_BrN@w z82BAaMg@{P1Z;Y4`WOk@Q0_s0TEuIj-d5m@o3G;K8LMlK$7K#v9&P3mVzb#=1g}F$ z-~Yj1nM(Ehk?QYNPVaE%r*UtUj%5p~b@obM{ku z8XXZ4pr@iHNpD)T6W^#j_2kHYUG$uGEag z+m%Ji;1WfoI^iu;|CWuJS3f9s>d_0bj?Y;2<&=RJ7aAma<_yEnl$HatSq3_9m6bTA zpGbO^JhN(1*)M`pJ(??Q{YgOZVBvDt3fX?VY1WR*8)!Z)wakBphzCEi5PjA55bdRKvQ1XnlA?JZ z&3cu6Yfj|svwqXt$RPL}YWz2g4rx%0zJrFRu^89}oDd|EUZw3)VU-iG+cP83W5_p1 zfz`(xcSdXlR-3tC8T^HRhY$lsLIM~`o6UN?|I;PU7j^HNUO$pN{aB=qE6~T>UIjxq?E@E$b~T9EhbWeQ5u37U7r z+zlL{Ig2EHd&-j8wzT5+!y99D{~1TV@|sho_padvWy{Hj^?_g?9QNDXF+|MJq~doe zm|@FQh0Zm}pEPv9K;N__vNDL>(Mu~5D(TZCuw#1-r$oB z6^~YHzTCD5rpUIg(nO;xH&NgOUw+D>5hUPZQZKpIfv5A+nLK&l5k-WBW4Ri4Cg@LxPq%i+OMmgTgLoeA{nJn$z5Z<(l;jF+ z?5>cA+|fAJS!WtcV*Tz*S+|q8>pkFx=bynKZxzzV&oUA?*P}11;OLlc@zPel z@pXR7Z86S?E$i1(*{`22%9e04((CHLwSB>}-G@um4K4>sxTVu$Q)C3|Pu4E0M} zd+D7QD8?V`RD0Iu5E~}$EMxnHbF5H&J|=@g*j9tN5nij9yIn+SCHRwK(vt)e?IzgqSuHxjH~6*kguqGBn}@p-xQlog;42fdHuw2p!lex?voEIG-wi$wU&6_fS$u0y-q>?` z%!xwE`t--y2Ndi;+g%51=3GiJwqV#6B4gg+K5K-=K7)cW(jlwz9!f zL0Tp)voi2UUKnzh%zrAW@ceWM!4eu05 zuvZ(>tES&%od2pf4GR7!FJ4hn%eM;eI}OZ(HJ2)l93{?2)IgrPq?!k^`S|44RPu|8 zxLW-#9LfBp?_@oLDk_{B6Ak}J#8WZ(bl_`0LPN^lc$hZAHq}WxXwWOmvDvRTrs?65 zN1?V$S^uans3dNc7Io@bgMQI&n3N{Qlt3kKm0j5BcU+Dn@e!*qdG>%2@Fbq^f#9uh z^`_(Ui4@!%lj=d)y7Hpkpxmn%n_cl}$TVH_75g;HF1YH)wzk)|ROhw2zfzWp3OCB5TYos%@OZ-ExS8~rz0Y6h zhi=s8PP-`;A@MPFKsFcg03tM4bKWXidResZ$l9yU{CINHooQUHuZUP9s{X)K z+$-)`4Hfy@hE@KrT^Wa8vQ;{tY0cFpNBbCA>Q^$FGB7aBs(~xaWfAW!UMQ-B(h0Iz zf8*w5iK@{oEqwV}7OxgxGpU{3wbt%myo^>X_s#Y97T#mRsnC6>>sV!qcB*uEH&=Jf zW%cH@%UxU_b}3pI8X+3CtT_5qPHY>(P-^WS&68@%TBlFq3qr4(rk%XUu1HMsLiNy3 zv+GaDFhv9Tm5=lSWx9Id$nQQB$O==udx`2Z5~>@2hKTe&jjhc+UC$cOp{%C4&mpRjX`8Yj%+{$k7sxk5eBwCJ5a@43PGzE}++j>)%;n*)t_`+?uz z32D1F9IpZ^J<4z?qPm_p9dV~6a{H*3R^e0gyI?nj>)xD8MY>F1^ zshJFaxo_b`xa73=8ojT(z3iGPq)ZFJtISH41%A6v5|F~v$H+?7+?;4iSsUR6at~?5 za-?k=m#5A_6)L@kYX|CIiyA%-mzt?}qtrT!y5Z%#0Lvjmhj%^Q0|ho;DhbiY>5m8q ztgl}hTK~O-I_aelucFN)d|^Jcr{mTTY;-B(#8P)cMd*+LW?n?-?pi=|M3tm)J=Ivb zYIFoNS9io73>hI)Z@u2r*XS3~=i_hkCFQzw@lzIBt$YQ=uWs zWuECY3%0@WdQS#%S8@w8tHxpy(@v4RtCsOc+K+0d>O(T=kZMf0eG-E2Y=0H6+8Jp< zV%L;bIs_r}c@(c2?Xl(?wfs%$B68fIPkb9T5&cO_!sGD-UqS>6u8!|2oipybnx9f% zsMvDil5N#`DdN|*X?+a8mNe_G{+PZpw^=9-JJ!&a_XIk1o}zqoOESbCl(<8tH>YpAenW+g3OW#U? zMY`b&u!zG*Y6I|uZ03G4%GmLlnC-~BYkm>q&-E{(SOwKyWwF;0ebB6_pzaX7r&*=z>59(g zJVH|m7I`ktjG^K|?(!D2_IyMPz8e)w&xzX`R{OzcCTntr4PE=X})$dQq=*1fPMfTZ1Lyl z7wG|fV8PJIFz|y^z;9n%MQXstPV=f!m;Shs!HTh?*;xd>)bd;fx_@`n0DQDRwCy<5 zHxO>U%ufb5Y7O0(YJi-K-o9`kB?#6~;2e-AW2vaAGKw2SYnGBp^VHtPq6{6J@N}t0 z75od>u4VNAQ52^@ZI{{ZG-F`FhBa#F##ZTIV*qTKo_MSw22#N>8mwxPn;4MkYwMTE z-|2?=kSH^doqtv^?l0w|>fkgcb66b^OPs9(O8xB&{!%9S>&{gIx2blEeZbq=^X^!k zx>-ScH;mRs+o3thJaCXlzUe8AJzKV5_)>f9RVJufhJRB*z88mUoIpaN1ykU{)UAH7 zwCM36$zdyJam~FwIq5U>W`q+`mb3LR&0W*A#=!45WT9&hNJ2#}^H%~>K3&uq21*o8 z#T6r~xI6rea40A?VhU}w7x{C7tPX1HrqkOt#m|bBYYq}-xFQS-cO)fYUhM3*XdBUZp%tm2(zi+0i{RO9!>=+zeUw2JjXQI z3b{QeT8&n$4e}e%1QuN7(%lfG#*KYHaHhCB+hJA{2A*&OGSE*;2y@)t_W|j`{3aua zT^o;=!kfK8#nm*GpWL@4f~LN^`ouwo9wOY{R0xyIoqk^5xhe8(#i6i$7bbpH4%@Hq z`V)p$>DE!#In-WBZQ{h6xD1QA>_`pjClsuR?v3}%N_oF79y8M5hFi+9%XvGUx?$)p z8mo5qKGLTgQ>r$^YV3!P%BJlWMkE-*;M@94l82#5*&fCd_mJ+zo<2MpDKBN$6}ecN zR+thVv$gVvRt_9v|VrD z##efmVbeqS<>(W``RMK{H^4>zjQuKQRXZV$opDfrrM zzq>p~asuRFZH4#ZB3UQJWXsW;3G*;yCJREfb+uZJ)cV#6J-{ner~&}jH?MmO=Qd8Y zJsw-eESd@Fi-sH9O*M82i|#f z>2tn3R$_otJ^t6>WSdMI zkW|hdIc(;n*7nv?L>d3&2uXz`ym0$VJj@>mFel7mrToit2tMADvjjv5(O-1Q{r0(* z2K^6H`dg-B3!Hhe$4u!}**ARBs&3T*OsP%jFYF7}P+=>?9F1cnAfl<6+1AoHY0lh9 zE@ljXU;$0_K>Kgn!z9$WA`112aCe^=4ap6N{hT8fO=dkjdVmqvZC{-KypTJ?4~eq& zyZOP9ymbL222qm&>Df}WsvIAu_4(Ui?Ve{ zUySOWHuChrC9(Va8VJ%<2L;cZ<*SovZvup>=RIIT1bR8Hw%ZooSIIOL!wxZ!Tx3ogVY5arKZdTlfjN{w58TS!aMFv)u!pGx+{{2u*RO6#ls_truUzBi23W z_4am9iak##r1=FF4|J^pCz zkYh&ULwmHo&KU=TwGmPPfNB~@Eulrj%AUVaZ3S$6cYKUCBY0w=D6}N7*+SdduaJvk6x0G?auPxp~dgY!iBBPII&lnnYR|NzQX+gjyaM87#irp!mx2|EkvuT{#twhjp3#<^d z+ws6KbX>!wl=St5{E&q3}HZ#Z1L+kb0^qmogPB%7vl^nSBC@lt@ zeZIk88BwAfac6+SuvFH zzB;in6Q+=t&sj+|0613V#{H))&gsshKgs6e69TvVcssvq_TF*{nugmAj-B{T)kL(O zBFo3@1!L%(2O75|Tk;}5nhdK{{_uZ;X9`HDA#t(gky*rl9o!2n4eoUeHV^F1NMm6a&{Zz%i_TLl!1}Z^mN`!(HB2>kksAGlgH2viM z7eIF59tGcya$F=w4H4(gF*9WDFZ=t1v( z(^6eSzlV(VtTL>{2IE;(dfnt(#jI)uB<|Mpk*!4Z3gj---h^5Q=G$`oU;*~4dBA_1 zFzRM9Xn*z=g^GF0y|FbcW-Z`6vs4aG+DyZuBx3{{yLc_kX zlLFUsir+#5*{RZ~Q-x0!+@0(6K{>~9{)_4J;nM6JoaE#6C$BJG1_Kef(&2|AbKOdy59)g@ydxvs z6wbvU)YmG0G-p_phE?6IO`Kz)!%e!1b&;)_XfR8U3a;IM;@6po)69!wVkN&gU=R~( z_cMNF%gP$KU+M#}B*m5$$&*I`KC(yM*(uRlXfk47@54LN6}qToNi5oTn*foT^{ zdx78TpHu`{`>>A+@1Pzx;~4@rh5-ncASgD#9X9RCg{{x)ElZmc+B%IWErqKx22sll zYZnoAOAVU6uJN}-{Qm5F^G*C?d7A(n(h)fl5BH#_)-vq~MOh2@h#R8Nc^e9QkI48M z&Cxz>{;BpWxLe}NIsI!uCG6)_+Q0iwq$>QDV*V6i;3(L0*xc(jZ(+xTcW?s>+$BY3 z^N4SC_WJ;=XiyRWaAl)oFP`dMD82$0NtWHLI7vbE%Jl8TnJ8coQJSVFT#CC3wgL`g zMB!;3W?G^?e0CY}(q&)RVRE@<&_c1;P7hJTtz`N+>0^rr(uR7l&1H{0Fj;SG#9(u9 zaC=# zjDfDK=|(KbaXwd7Ijf0Z4>EBZlSx~I0%EUb8puv-PbeSln2Yo%GLv=KPciv^sQ07Z zEXeZRHs7nd+Dp%FZQb#X85ve%M*sE1`}xEh%=a!sZx7|lc;fk@{>dn#M2&?$o-U&!FD59^13a-Y%Edpqrdstfn5ugIO%&x4oUx_MPPU zN?Q&Qku6`MDq+R#=b>4l_5nMFZ{IzNTsxiSieAVJX61l17wc!AFs9dUP zl7rl(_^#UeWSco|`n?3AGgu!RNos#SF_JCbr*JICZr8%BxZ6_E9?o;L_XzpdX_8&m ziZz+pcgAb21;ile=f#O1!{%|ObfUK22jO%p zhS?21gGRlr>+3r2saR^u0!kSs;k@HZI*CtZhRQ>!yquTHd4i}vKsmC{s#29loTn<~ zG6z8^ZafnXeUEI-ihESmqCL(Rh-ZQk(`Z+24z`Zdwa8+dR^PWyCXKaklF9bEaLdKBr^~r-{BmLsa1(Hw!Z* z*UZ1TG}>kFf9%eA(Lcp;2}Ox1Qjn{yeT_odAy{JFlTRBh6Hep9RPRf$lu|I+IH|5`m?x5=tU@vP==6EQFJ;e*=|%h*SQjFk`EKTnq3FWP@B zxJl})zvBHu`^=>dEb4Yrwmu_) zR#c{C@@t(XhZqf+RJ<=6oaqOBWEYBy40n#wD32YLEz3b%%zQm>qVe<|z@gW_H?B2h z@xQo9J2ty=cs6)3)qNy_;KoCZ!TFsJ!QvP(Ck@g1T7~Q4?g-SIKO=Qwhu?{|3oI@L z8PzW$pte(LdT2X?O7?+h>^IpA-NQ8qP>@;Ao@aHsuFIV}!PwUhL~7e!dYV`^;kn9K zT5}?Xi^eYeaQmG7<58`>xiXxhUD{n0gVg>rZ(K|-hT|BgS~i|6tdw^ar8$EvoA&H? zcNKN(!Zn#Iq?gcwrs<{9Fh4jHi3V_+`38?b{{o+w2h#fKk>F{|8?d;1=JG5Tl(MhI zPAsvxeBbRClp?kq`@8cnqjeQ$Ad}7|LsK50a@#bL!&pQ(Yw+d{S zd{f8M+KHl1<_eTY^t(Uk!!$62fnmEe7HuYgdbqgH?K^gW5(1+jG1MDATSg_cd zZ=O#6(^&`o2h#UJ2MSAXhkWhl%s&S|Fp3h6D3u+LusS2mY+uSd1=K^=?@rS*7(_&#fk-#2NC zo=CG!47@b({=d>fy$5HycC-q0hOvG)E1z`84oue7-2->MPKBgmrIb|K`jYYte)wf) zXwtdj@b;ryA1HH@JdFn0SYTp>4p##cgqeLg#v|DIdU$*u&xAx7t(p47Z7_D6|t33!Dehiosh|6{zqg&y4zMILv+fc_$nB*{1UG1|;p{7dSM(KRbsC~mV=1J*hTl{y*oGc^$ z)@E6|^vN(r%#03LkCzBK*|O0dy>TC5RiQ(*qeOqsY*P|gQ_IId9JoH(! zRke_Wx$|&buS z!ufE#??9efCX+p@x^kvnSdJaq4c%sWai7WsAyi5xY7CLfL}!zy=Ql!g1}Q2S~tbNQcF)vuW>DoooPgwz~gaXmhv?BVfN z>-HA0$XlapWczbLquZDcoBd{3q2$)l>b$A-kFKJu`P^RWO%P_Ru~TxvqWbY%7klNfz(sjWzo(*t6xr|R^J0EEjrWpP zZ3JWee3s_uuXP|Z8gb9VHJDHNydQ7fLwg$q#M0PCnhvRr56eZ$tpaKCCA}(&?vp2I zw^5jEn?tm}=9>?hX_vHq8M-re&i7D*(-clZbV9h%iOrrH|}$ty?mhR@|e0 zYh2r7iBm4H08XpDmjm){0{ff)0Z(fy6+QM`knUAj6CL(^S@I5Jtr^e}Y!e?P(DXSc zHD-hRr~Ko`z+Zg4aS2GALzmtmZMTV=ccH#-08 zi3van3G^;m6M=#{`z?%d+>gh9rfvXtkmX>;etobGlqP|WJNq~S%jveqst=~J3OqUR zvuy*g&tWb?;&QXJG|-4&Hgd7+Km!zNcgp03F9`$5DBG1b&&x_3K$R~k)<1}h``>S% zOd|zwp5>zcS>5?&#n_kMp4;@GLNQig2EiJHK*I9(_+M`iRba3pCa{#^6-rkGF1xy7 zCl5r}BBIMK!pq}->?s9WKmmjp3Q557t!61^yO{i9SKptz_Km@mn)@LczD&lDpC2fn+=bas ziI^BEM|?kv?uW9W`ZC8PuyVkyA}(wC1c_QDMTRo{hOSFd*AraCIibmS^?7nk`-o{@7ihTytaim!;yQLOC7aa zZU~**+@o`K8%7-MV|;AkuSoWo6C7Bn%xFSh$~(^05Bk8rDz?iv8E%T1y>tH(04!hX zWpe_7COg2=GQ9BK+N!ocod)$4O-233vlig(wK|C%KJ*A2ObjBXM&(sm=RDnG(I9<< zwyJ#wO8;HcmA#u`({(MQDMc7e!bCIvfJDkQsuOglBzZ8#Vk)r;Cnoa>6wkXbv@ju~ zmOZ#2D^2}IWBFQU&Y+2tY}#EfH1EVa&~IzEAw7GlSV?UJ6#-C;cyhV<(qt;D-pnMw z@n;OGw*#YPhjwXQW=1wqwZ#+l&Q}akLJJ{B4vi(#SsKe-_ypSMXG%t0GZ#+i$lLqQ zE<2DqgQ^De;-WcQn!>rGTR7*T#ywcp+X5S1S5{zi@Loo@HqHgum$gkiH}d`nA*QHX zqFAimcj*L$qvrK;9d#{wQJ)S;j~XhriQ-1*Cu&W|#p~-Q45nbGpCO+X+anVzefr@N z*&H`b4&t0Q8pi~CiaR!d?&doTXYD{;EU^O_4*?x8ZX`F!lw%Z=R!iFpV@j3;K69KM zMp9(i1_}d2yX|I=;3rCPcfo3HM`Y&lxZ`rbY2J|X;?#XF=LXspaT){gCLgk8w!Sq{ zXxd3m7TpLsh`~Guarm5GCQRxV+tn_w7U13mKK!;}2iLp! z3c2%`-{h0d$0^f&6_31GAD5@1;;te0L6klFO_RhQ_c#%o$xMeLxvHj)$Ey{lm7QXq z<`k0tM&KNw#>ERIisW=W*UB%UDRA0yC5{ii4XcQ?f93!80l7kg!hnsedrj%V1Qll4 z3R=j>aZUSGi__nbn!=*b`Nd=1%=3~$i{Dnsaf+%?K|R8CV+K#w^KsT1 zu z#HXBjk|fETOpbit`eblwo*kaW2m8zk=eR;4lY%O8$VN)P;;X5$<1)UVF;QX}f9@w| zLShbiyA^k#u^pqXMu$QxPEDQgZ+9I=J6_{e9Q4O*DsAlB~^79<}jo{ zH7(*3;pP20f-*>=QMSlco{dUp-8EnKh<<2V1B?2P9LZbs_szPI6CRb4oD<5-LK$lX zPkUFQVE2vxlLf@rmMXFaCntCW2xcOteq7HGn$@IYjX^lm8G!ftfAZy~u2IG9h*nGc ze8Zkk_D#z;xkuDofopvV{q$z+$NdqJzMA~-B!>zwj*Ut)UCdk4Udcl4rOVaO6aWw} zK^kWZ%Hz-*eEMp2+HO?v?rXFCT6`R~8FX+L&L{I;Qf=I@&c%;+MyUKM@_o)%_8l3X zQ!yr~8+sFO2KST3=qkigT^=6tzL}lzfTe!qN5h9aRJh)l9nxK6g|2gk3Bd@~Uzw5R zl`g$9r*ao&>xtgG+)-a{UrJ^I?g$^F=|5Rneoam$FWakpc-IEAzZuH)G;Ta`U>S<2 z$`hBhZ3Wk_7~V^xT$L9P%@K1(1cRETPA0Wr1B(1qHDq@#BE7N^=JAacwF*uKKP^ZC z1YXh6MhtbU^8NHV1-zjU?cZ4nu-jjlyA5qVe)>{V3~^Lb-qZr4dZFET(o;^`H#y{f^)zfksd%uU{jU#KE{&{!!o_|#v5n=i3LaCqr zb)ilzvLi5P0iw*lQ)SRhD;r!gsuiW4@X`~hOa%@<>^0A2RN{vl!~IhLKD#;f%IO!s zQ{J9f!sET{u@cI#<=GPXL`m$TS3+?N%-mV3eNXq?T%>{XTALWX*YyI72YtwGq{?$` z*3rLjso+wjSUTKXo9gjfv0g_eenEQ_iq<=j%e%(dfBMNcxO|<9mq!p*``x4^HPm&p z`WKF`|1)L6h2*WmJn8EA2~wxZ*W`w{#a>OC1@DbyOqcHw>>*A1MEiHbQ7*m(oW91u zsldTa12C>=rqsgL^Y2s>p3>L>hMD2t(ezC2KyhW6G&H*CzOR~?;Eh<(^#aPMqGqR0 zvkq}QAzz{&9Tv>Xvx#P0{RDK_A|MA?YD{KnL z#LF16J}gvqz1<34VyHFBX`1p*e5LtjnLO0h##wcXI(p*oW#4UY0Rc}l0qb=(6AS*T z=y5KDJdVTqP5PNGegmoocSlF=Q8PI5WmRcb#Akz?3^!N;U3GpV75G{lntV^~95QFk zJl4)ljlFj(jP`_(P<3q&e$n1;?>b-5KuyXZbnK$gd1Oj)m9xEBAqwrjDY+8(AopTb13%_Ii#{rphb&!lp=Qd|E18o^A^-pA_2 zW~|8&Cy$`nz;cmlHJRGJ?)j@00bjFvvmB3hi_Q;A>v~`7nCft}M&-BZwxj6dh8h2D z;D4r3_G!?j8Eh;C64`Q@Vq$So=gh$uR1Uz|VH@uxu`-Is?jPdUZ^1+9)b_NEO}ly0 zQEo{EyauNTE~ZKTPV)r z2E6aez*bNG9q1%P!NEzV*4!m4wB2IGYwKFenI|KdEjRVRDV33?KriAWQTxA_@z;D! z7=UWY&b^*|EjxDmGjq2;_fq`3EzPzOR3tj^;>rGu@bN9Jw3@f!1t<3<&yr(w`Ti_y z@5QQ_DMr98TTtz-U&M{(REc4gBO^ZjCf4qMy$bVXXIZe?B8K5ixyiI49n>5HE(D51 zsxB4DYq`m*heqS~Q0wiage|U9XqTT+{Q&{c2A_HnC&wjm}!@h zwwbF>8!_N?9wPo@oBFaL8dRUo56c@QEYoK2%JE`Nh~121bX1%`D@U5rAr#lg2LMO- z&nJBT7`+N}14MWw@Fl5`z7&P)hNF1^OD)n4IClT^;^403?8)!YM2{>A({ZT`{3j6sUx~4!{W10N^I!S?82&P$h?6k`;YJATxJbz9W<)@qX$|{ z1GHwx^YDd32eRNxkD4a*<1!~;Bie=`wRHBP40vW#*){=rS3g_^6!wwGc)<5+g8#(8|?F;Q8rp!?Qx+gM3iKOK)yaM#qhF@y% zI5j7r{t8Xby|vEHa%~gy^+yEI-clrg>COpMo#xxN`!F_J^>=VU9AP8*`)M_IZh+Jg zT4(Zi+7iGV*hDWsz3dupG1q+=heMZpA$~8==vo2HhTzLbOGZwhtK-YjQ+2tX`@$w8!Kw$5@;%;ZCmq@$n3v zkhm)qW!JOI2g2~=n%?2$=ol(7YTz6KO}JI+uyfeXX6M!{Jvyx5BK1B$v74}qk%(SS z5(b&`iLyBU%J>y~q$;#qC=IkJrLmO&GGEjE5xo%aqSeq*^DdVSVF(#Vzo|)nLDt>C z+f8%^m8QsMk)Yd6G3}eFv~tCsQ?>l5N!RpNUuzE35fTK)|3#FIp#?*uIN|bgKKQeg z?U-8S&H`BBBS2;Y6SDq`^RAO$mD2W-s`M5Xgj+PcF}QNCk~<+2&XqHp5-r1LF3(dU%* zwFwJ7V`?dF+r8mw)H9QFhbY|^w@&6X+u#4wfogq(NbZ(-%bVr0 zUXkKY>m$IEV32p8p~3Y-1>NUvKuyWWYuK z`If)G%X4*i-az!UV6O3I&}nKD)5(|yVhP?PuYWK5Y=gXM=!VKx)$h%{D&;NLlPcBq zPIA}D=X%Cj!jqvy=0nd;XQe&d<0j1}=IXrkKi3AHvtIQ0EmPI3R9K8B2b{noQw%M9 zeqt@l-v)0b8V0sh>7V}|Fsa+944ZM?JlR+Yq;}8@+~^Y3upM|#g_-d?YlE_8tx%y# z0yecEuGyI5!OvfNlaqT%6=n8M2C07AW^c7zz(JKaWn5cN4tS=AK93uppN$u_SPPQ^ zeCSebwk6T6ON=kUjOq(I0>9Ece~n4Vo9{f7x#;B=#Yo^PXFTvDrmjD`p|UgnJtG-~ zxhI}1zB|!?bcbJ!b^Q$OS#K_S{^0pis<0>iAX{dJMZI-18ZzUj9B2V<4>=T858F$v zYCNVSZCy)lG;gok3cxdFoFNQ^+=JYNs0%kbqf+P-V~BXhm3rs}<~+NjX+v;2PrK8b zJjWXcKzEyhP_3e<$^@~ATG6xgw^e7Zb-4%)+}WEU@WA|OM$FxhrvCqRDZ)LUsUUe|?ZRPOqc zyUu#7J#9jiE^Iu?J}d9l=XagPu}RN4t*$xx;RhJRXPaM2)cu)xe_{E2_waY(A{PXU z6l(ps6n`GxcuM$W**eR6Pd@j8oOJF;b<*F)yf5TX&LC9p$6Upk_y)ff3fki5uI}hE zh24-0EI4=>*qO|q>N-1i7)8I<*lr;t&}4(@k}MGPTnnq^Hz>#Y6`vOZ)pI=xA5Bd62b4n>^O|ddT}M<>{)GpsI&URM0hJUG%8fB%`=poio!nlfT2TOXI?7wcIv7qNYu zjKF%0vX2Cw&CA86{WP!LEyHYmKi{YnA5@a|!VfmVi}rs8RIRu0N*+Ctw_uPq3shN- zM$A>mJw_cYLW^%~@mjhYTzHyO0zsJiG63EeWZU=AKD4NDdR5v!x>#ala>q_`tnxhB zEW(I(M(reWep8`c&)YX>T=XTF|mm#6Ud0aNfwohA!_DE%N;!6B6ov(KTeX&pKZZCpo~C3ZFBi`~jHz69mG>^QUDyo++ddXA36q1aB=oYE z?(aV@?ECyK>-xwUj=zcV>BFYarOMb^FSUU@ourQ$v#0AWK31-Zzs>|-za0awM$ctM z%18#Rs%GnUw7l}Q`MRTM*72}z@|M%-rvu0{j?d><xsT+iiK-M@HS=)TA)n8 z>3O$uUzHK!4dtA-B|ggQv+i=K9_fu7p8sS4g0h&+GNi{>9>Hf&!QGNw*9(O;(o@R! zm8yTb8=mn6RlQlkHtY2sGOJP1{@USPsQY57)olH~X%an({gyiD0b9^Zd-08Nh~vbaqKG-?UR>-JTUn|o9TGSERjEZOZ4mD zhph7F&DKAi9cL~Y&8k?OYL2cxsg>a8>MRbF1j6~#OBHG9kVwdk*XE-|7S@rg)4!DL zME|5GZD=*zi>d8>n%P>2lx8$8Y(W=oeyYQo*%=ag;!DA@{wsNrKYpnDw?J0}T41Ha zOnjI|Jl$ktW^6-;2;Q=<)L{69?yWWOUHV@V0nKld%w8COV&R=b`3GLKA-8=x5567S z3|boMok@hiA8imlePLrysireQq60~IU6b%8wPMC&DIsC%uR59ovEVInRIcCxS z$v`xUIZ)ScUkg!gRzm!Ez>BGQ@?jbXd61H6Adr<^%uxS*Gh@s9Dn{PkF4pXUg+{(1 zpW6^$^<9kWy8Q;nBZC?LpZk4SI(qJO_b(#XEi@c|FRHe{wdH&6dzauBcq@4aXhEwP zR!=lmx3gBcE0J6=#QBd&PEDlqhP8JOPnBk0q;-An(B|@g-&@fPc2NU7rH^9MuH^jz z{DwLU9pP+BjuJ$8n144WA*>S~Qq<`4)_J*ZT7R0S*=_!B`%Z~BV zawvHH>AZ$8sA!IO#P{+&s$c$BhP2fJm!Y1R4E|JMo|xtkaD#K8+93pCNq_WdlBY|r zhV%Gb-S>wOFNTEBfx>4>q;TE4x!8hPC+c^go77leD*atCru(LWTdaZ;t)J#z)yYtB z@l9aASijieAteH*HiqT=%&mW)Kq!1o=9Y46fh39h^BcRN**e0+#6)rr-5Wj%n|h3* zqm|WXPV(QgSXi{GMjAMOX_W9^S6JIJ$8La{4l?+~1sW4>)QhrIIZAkxjSEwVZ9ta` za~dWlOasXlA^-O-@o9O zdkb_Le2^s@X`;Tsock+xmN#!MQh%MB_NxRutoZF~hbCsT;xp;}kIOWN+IUL+L zB6^ZJ!*KdwwaV^8g`EpY%|0s>wzBwvTS9%ugZB!cpCvE5OZ9Tb<~+;JGom%<<8xx? z)^FUo*rZFgXmK87P!J=>{7wh*<4+3OE2GyFTKB($gzx%B4dv?B*iG&7 zyS8*HoBI@gw$Rl+`oSbGt+f8AmoHVoxB*ur*f}D6P|xQcMocO{3#m3X{FT_T`EIlg z(C!-nJ7eaBH)RnXE4|6$vj*&EtI4st2sbTvn=VydSJdReFqGBo$Bb~#+zQjeupToX zwH$s>sBR#xkxA-2u*H}xH`&JJ8Osi{JyU0M_u?lDP9j0l1N6Q__~X>ji%nvZNrl*A zEmI>YM0xFVvV|O%AYuCzdYnO$VM>SM7tRBcWZ|$e^p*IcE%llydjv`0kBOTRaz0<- z9ImX8N)#aU3Joowho;jr5WVKRK)U?zwd9ULiWZihhW7=XeYu~Z{@V5#t9z>9m-U>K z?Q7j(wB%9Fi^zi(Rk;mkhhL3;X5+gg$ID>_8w#9%`1&$+AZDmHZ(58_d&25_%`f0T zwE~j+CM|6_{^MfLSH?ms^0TscBCiFoTi|q8VGHI} z{^ssG*BA+4$~yXNDLlKjhAB7`Cp4sHg+u;+RdcDEf!y59svlSVOqeK7!geDaZEs5) zy>U$}cAWb5y6F#u!XUyhK}qyAh>Od9dojJy#s1B5Hf`sz_sFY{Y6~TvNL9M@4g%79Cn4oK z;dkHrzH?{p%r`fGWs;L~cJ^L-m1jL`?KD*1H|>_fzwqgCehnSd-`)(Tr%~L_`I6df z0X*gUFm3jD*phJ3cRM2$xFzCvt@W?tA@U7oyKjnub*}5UBegwRJn~tK4|`FdW5*7B zXUl2-8^xdr>^F;V4BMG$@zv|X#%hcFPPKrr*$&VP#ZQZ*(gxe6`T<3Z^R{zRHSYWO zR-~VZ_jfI|US35r1aSMDlxH>X>BjNlrw?&-_X8+I!Y3SKf$lQ>0ygc`#_{Luh%hQK zJ$cdHWB6d(PhJY@t{SD{0N(hB%)u>)-kWO-;5(>1rROqWHA@@Zc$-G>#P zUKVR@%!1<>Jg7*h{m)W+(mq+Lu;0l`#7oPbqm8Q=bf~30N~8ZrRA| zKybRwi>mj+m+=p~`}Z5|GkvnFwv6G5yA{krYr*iQd|or&7R-5Fx72X4K)Ev6`vFrg z>*7JIQu48=+w$)qt$m4#rQ?~VLTa@JKXjK^i-ma;dLGjtt?4k^4s&5BE;4RD&H4L? z8KPb&c+ouAf0?AEI;gz9azasFJV4&Yt!t9RoaIhJnaFFTujKbhnfb&*@0c^*{o*nB z9Xj*A!|)>bBy@N|?>#DhJt-4UI@ihE`ia=tenJh8~T!d9+4H20t$tG$<F9OZi6n=4Y6%`d8iyvKL=Q@ z^W7R}eUE;yO5)fFz4U3zYQO7ul~KqiI)(GLWqtp%!7Q-8M*Q^A{c~?#5RKT!>AAg) zjiK^)+aF(R>~0?h9L~x98an0lY_4~K`1-C*0C;>!O^sjrB6u%XTzxa=o^ab)p9h_z ztGX3L;$oTV~~dNU6wk$@^}jobe79_q!hJ6|^kxWniqQpeD_yNZ-v8Q2953u&)0-+p%*;{ zvjji0Y(58EiUZV8y$vGNNN>hMV84XBszp)howtA9NZoW`1*5(W{pO#q9n7ABt;Z{W z>u9EoTkPNYSS-ds($BkEl?fJ4(wxWWL|mE^^{vcGYpDBi`m1x-u?bXIkT~Nn9sY^@V-97xc<^$=K#^iuvq;^hW`x#%o_05-r^hp;1@%P zerSn-KAT-q_)BL>i57garhwbc5_95yXc^ofAK?%_yn;_ZyK~{-SmVjO{pJQx5hL&! zpUgG5K0sTMxHDCrX7ul|Qx0^@WB*ctkeY`;v61;CY9~3FQrt=Iv`(-Mjy022cv>6t zzj2RtOkY}vOW_0BO(*eM>|XBT$L8(KauvGI|CWxr!^WM#tRUosgmbsZJjBRgteQF0|q1k{SJCb6aubH@xA z8~yoTXcaO?$PF+P3!yv731SFXy?`nMc(eg^UKg$IwO6*suiKlOyyH!0%}L3Ytl|)O z5@JGv4qr?L?|Wvol;^BXP%es!6v2L_ac3Ecj9v6{N-s^7d@lq~ra60a?*@8IxvF2x z*>bnqSX(2?$dZpuyI}iC1xtI zf^%%ltK9?jCwr3+;3b*SUQ;KgMdxV_n$IFond_mS4jS&wez#8a=)X%>$V0&NYrf8C z-#Bf@8RY{lOy@N)tU1-n8e-&yjr@5;V%N0KnJfgUb)DKL)ikVGPTFy8&bCb^Yn~-- zJBeNmrBS|;p7$?ee#KNZydwgyRAmGCT30UrbX{~L9@1~dggUx5oB!2w6+F#%VRX)_ z`@e%9K@VM=ydP7F-rUQg72RhPTk|NTC~w{JlB)E`V)c8P?l?n@8EDtTG|UXo!zUXyTumq@#IN$GT{;XlKO7fTg_V2k3ze|J23OG3Y z`1WU!<#b&hy}=1ou{Y!-AkvO9ubt_D^UugOIpt8AL}#E2-_MQ5!$Vr<-v1}g!pbG8 ze0T2?w*qP4Kh|y!P7Y4J#rHIbvq-{R%F)Xlnt zpB}NqJ_$oMp-DQww)`pHqv$8h!V&-{3a{rrF2VfuwzOk!T0Y|VCsX@>VF4Kou*<$6 z#Z7zUKMfz%_6{N9jD;)|vU~PVnpWYEr%RJ9T8OhhZ3W?0SzLdv$aW z{{Fk;`9-Zan^^rcR;K>X8umzTsDkM_JmWH`}@dQuOplkG!R;lv~Sd40(wLCjz_O@ zCl_2-_o)a(OI6hK2jW1Ft2KNPd22Lt{s zLvl-)tU)h-_UH}H^LoUVJ=kZ;`k z?!U2s&NeC$oqCI#^6M)6uQxz{O*PDRDkQ8O3i~AbIEbAwT7wHCLYg2)E$_onckk?l zM@0P=>MGu~$Lu72;L&50WDqOnl%VR_?!pi|~N==G!lFd=+{-EaqNmEi~zu zi~jK~v3A=+MOY||iD7>WuiI*uHTxmrW-4;#H>HbZ*q0tc?we3bE`&M?#Ch*II$p>p zN5U3w@YhUdxBikb*OjmOEA3tI(#x}j_xk}U3+c~3U(c+UQ{4F%FoLR*%rc3sSq*`P=|D-onw*r2tRuOhKGWLD0-K4BIEz9?Bu2h)OpWK(0I%|8t7d-6 zJh+wlJO?vW5Q)jJVN71J`1MyuWopq~Ymt>iRh1ISyRke3Xp6XOZ9#ST8a7CAdJ(>^ z6KwMLlSLL)%!{~xdaq4kCK{L{qK#a$r!etRgQ-}T^eHn9GJKiAkwk36k+76s-sjJ> zGEbXST)}R|flbqvN3*>mUX4&VT1y3S@{5L)ZfolTfgg=4q~Gsdu@KfE2X#UAjB%pl5QI%)7gHnQpAPBM3j)!+4*wx6cjhX%F=6@bElDm8(+D zp~0wo0wZ*ePUKOn4-4d?LgAI#@Qqw-$sok z*6Qj%OQ&<*=b7Qr;X+rVsD2J58KtKEV3vbp<$^V>HXkmhg62Cx^y=x2l^-fJdxjs) zNiJYaM@M-F%Gsb?M4ou0;GktT1H=78p;mMcX7cxMn;K58MTq2HX%>YkmRo+4rCWGY z+Iur9i|+aP6YE5kq%uQ~u_TKvO8>NnK>)v_^*<}B*p@lg)-JRk(ybtV%BZOHKLbyy z{|-FIJCVvV`DylJA-S=4?p@xr^2*eDx*WekCt#vRv-D8B7Jg=xu5T(TF#XP(N2gX9 zR`v3+oIE@A`mQ|j#t>7z8vjvO^g8K#Qv{0as>vg{x%A5@S6qwz*GW+CqbZvSUz&OgokzesFpP4v``ULcH>n(UO7MD97 z&1G~0W^Rx50yg$%?EfC(MFnU6JJ@^}5|`i4(M^2A^YLb6o7Jtn=rPt7JyY;rGcyc- zFfi<@@63_o78$#zGP=;=K6jhr!lb`#rjzK*hbld?yX!+x_wcONbKUr==!}3>()_p0 zweH;=>a)}VeKQsCWe&H{8y#IUEL2#;}7<#L7=ruBHu(bXVRT`>+B_+#%k zD>w1fQnh_l?SUeYndmqZso>C#eq?C>Kcd0DBf6So69b16$Y?$+Nj!Jdwt_4gi%dSK zGt*=O7?9U32dHS^wg-_dEh5I=dxH(8`Btex^dN;A=?B!$FHa@lq#qU=1J}5{LpN?n zogpI$$G%V29lYIJjF~iT`xjcSH~ahqFaF^-@og{o)^wxGUK>bXJ@{o5;r+k?P=j`j z*VGAmssdEww$?1at=JuS(6p{jJ&PtHO?0z_oV56p8p8wE>ZJ{B8D&5o3v-0Z6(;e1 zPb-cdh&EvB^zulirsivPo?87tPmd4;4w6;zE;!hiME>x?-+#_5zp%G_BP;yBf&{y* z@cGxp7niVxlPPG^`3VT6ZP!|Q|2(kT4c%eIJ3Dqb8~Lv%-9dP%!MMr7L^W(t40bx( zwCsub?-*(}7=K~0z#DW`H1z2wP&U5$g&*js`}2c_F@G4|tD6WFmzjF=p5kQhHvb!` z4|(dM&w(BsYMbxyT2zRyKgn(1nX63P^DLla#dU_r%e_uW&CbJRmP~{rdmU#m3cT zT)+MYe<3&uAk=;~{|a~>ck+R@9W2-F&U{|4-2V82A0-ql2>>I}IQPGY!(cO?A_fny zjIT2*);6v1PA9O(a6669Z*n9#F7G`h{O8S&y1NGt9WJ#@tjR+ke!7A8h6EQv3w|A$ z5W5+Y*(GRv3Jn9+ezsE>`kN1@tm+JhT7XcGI;;?9Yf@gK>1iNhz3@1t` zQ}I;qY|#p)0v_@P7sfFDP=PlGsFYKXl?q4Q0(zjlE~?;iLhUd&3(|+CTHkv^K!B&n z0Nn4D;pi6_l39tPQz`kz&oy4mMR@(-mwEv{0WT68uJhjyKE1p9Uza)%jq=jM#6J^e7rmy>Evm4*Va^uPv?EMY~-lx zCJ`~VBJIaB!9bnczvU>jp3C=j#QO0P-=1?rl3=-W!rRw1`S^ie@k{RkvJsB64*HQ;mQ*hE zsL!X3Lyj{MU22lHX7{RW13^5i3dP_FH7~j6ptKOow)xTeA1>WTx^uDugFp2W7Mc8xi=f8M z-%gc%`g@R)lM53tVR*QXCLg;55fjhRp^%n5kxyB#*lOZ>}BO#5!M7r~^pnMDc>KoGjeChPqW zFNGySW&N8C4?ii9rtB*JbVQvfUUo1?mfY+t=d7bl zSrrYFqXf$?PkjrV*z6}C=xsh@Xe!WaVpP{s0#vPKr;m*3*)R8Q?w~15@L63EMC8Rx zN>J2`lm}$Qoj8HVNj;ylaT?zDAE;6#cx)hi`LsVI{pFl4q__T50F^17zfcIMqDyo6 z$b40Gf}OyJs7tu)6a8ZR{@8m19avkA+wm$Ux8C3peB25Kf_XCvnkHp|n;!yl7@hlV zl|x!E5}XoC=lz3#KH}TAnibDcJS;-L^-bnX+B}xf zZkrJA2d?76EGNB@O;<0wN}M=XaPl~{Yi^(HHZG>OcX8Fe1oRA735H-5P{&J!zH8Z_Ws4eUh~DTW z8JQ7)Xo%x}Wd9`rQe3|SMb|_AUZB`-;5B}}oNAbJz4AA){p)t8;7bc0UNKG!%h}mT zKS#c|wpas^l{XZfpxsrW7UP_Fja_!O_}=@@MuQC9GG5@)?Nl6gw6_`{#gZ=J&$y5!J`TGsiudTJY#DMV5O!1@9Xi zo+VeiDjFpiRFL-YE|&;15R_U1a`f%qM06Fk!9TZ=c2eM0N(u1b1o)R6bwSQ?rmqgX zc&}dL*jr2wuiE(M`=iYQfxV~v9K!g*14sC*48{#fo|~bEV);TDFGW)j-;^1-Lkw}f zf!Bpw45~jbTmeqK{oeylfW?LDKE;=9sZWgO*87(i_~c>7Q(&w~f2Jt$3ZRD|5EThA zP>+S{rtt4nw*Tp4{GSQ0iQ4Y~*Vtb-^j5vBBf~4;Lfrnkf30^ zIL}$pR$cw!O>~r^1;|?k{p(<=AuKvp0$VWWQdW7DegQvCa@VdfJBl)bW?1hy&Z3o_ ziNoooQ6Iidt4ukyIh(_sAEuK5qLA)NsQ3kr~$D_JP5-vGHu6{4c_?jTM0;1#62 z^*eZdrL<>S7vRJFr*|OK_(cOB=&zDnWnnqukdPs3D>2Hb@I)AKD69!&J{LSXSlRWs zpk>HD&ZGo|`->AJ?m3vvt)cc!qi9;m+U@x&*|1>}#aMY`nKNK;srjhDr+%cZr<184 zIZrDFU1giN{DI8rLJ??c>2SLgPMpDHEKXF2>HZD~#&x&2Z&YLl3E$Sz4l*AGZn70~ z4GKGJ@Ij8sXxG%G8Um_}7*v{awrT1YTL;SXmdt)K{0l*?^f|gTIcG?O<#EF;x&v6D zCeLsCwKPlk`}rd~48Gq&x4nGXma(_+GU!q_sD1x@$i>@8eI~zVZcc(}Ey2Y9 z*ZK|x1?LT+&HJ~U#RTS6Tv}IqwsV-UJgmOy;sGPU=?(LZ1)48K9}=SDz;AlLTbEK$ z*e`jwRYF}gzzcC=oRd=nQROQqeH)Zva5_q5&i)fo_CE^~6|hKp2ovK~;;)wrdnOB? zs&Wxm6C|(oeh55$m|}bxNsRaO1+H2xE_|S>_s^C0@iZ~#1_c`=B2dZgU&1hNub796 z20T1B;AlRWxDXM+obR>gE;ZcdH{m2xu1h~%gMAB4SZiJhovTO_vh3|6x8;E?y&3IH z*3nDOMEs;N;+w}T??3~K%GmTP?fWOJ>)QpcE?Va-*!=s@g^mypL*H*Y&HMFp$^UN8 z#q9a+$w7g>i^h73)pEV3?a{eFG;Kat>7F^Y=-1hSGAMmVs1U;ADJ7^D)VTc z&}&Y-*oA9c={mQ0E1KAOCpA|ig_h@+jz?S<#aR=fGtKtquy4u9ov|`?mjjfIlu2yn zAVl@g{Yr99qRS8HEfLVWr8h&U9qF3)(N9f%uxD+yNLhtmc4G9iy1-TuW8cu;E<3vl z_s}$8-;?3?{m%p&Z=Hk<&ZCQtk(l(W9BZXPolMfz&V}n5TjN4_*5vZfkumC@$Dqv((DMATlu}?|xJ3ckfP^}o)Iw?f%d7()MRWzCT8E}#5&X+`ALhJMsLRs3jt3-uNO6UZDSg$6 z_X_?k{h0Eour_-GvJS+Qg{=A7uAIFohOcSEceeYsg{Hnqj(U`2Ghd`dbo`NarOk@Z z?GbrjDg9v2h=yvHmMM|cLhoC)pxUeLm*6r^S9Hg=bv{FkUep-9I5*iQ-4_b$wRX&n z@W*pLYqbeJyy@zUM{|O-5>qb3bS)}-lQiseC7cB6dx*gn?$~l#@*U}mkUxyr?%XSB z2cqQh35%Ar0smpaEJw!vq{jnn#caHGo7-_(t z(qiS3f#uxdxQ2}|<%AoaI!u3h9KTm7XrihFr+rVSyAe~(dR&|Ih1)hy=hY`Ob14#K z?*oB;Ju?Cu$V%zOu_$Gr2G?&aOHZ%%VcS+B0?;=PuMI%&H zJP=9G)&zIu$=4igclHY?mdxOV`L;7(pM(%^P)>S#vP5By+8co7DG}(B#ZdV*NGpxSHjyI`PxmxmkN^RmlntKd%Q0ag@}y}G!L3A8M4li zQR&6tk6GBWNCvN4M4)P)iGVY|9s(Lp5-Q>xun zHik6?Y~3nZ`GX&22{5WxlIsn5Pfq<3j?b&m@&|1VF-p0@oxHm*lAF_{%AnJ=Iiiqd zr2hsXs%!rax@Bq#b+k-ThxSM&W2Y?RNUd@^vE}T3%KLc*y79Si>KiXUo#z?lI!MAl z;3nS_2eI?_T3gM1s1>0GXQRFQ4-|@K+$(c2pEUgHk`A4>8;&y&McKz%{mQf+6<4R< z9Z|~5--l~x1q`Y8#gC4*x*NE9v+By4F9&`FSo>x+&o9wD=}&7I{yqFmU_Md)*KFLN z)8lP?W9|^O_dnKhXYU)0+8dDqIVB>TD`^(7a-vL8BFk|*?c%`qWZ;#Zf$r8sx(i5` zuckF;Q@h2+s0m)$wzNXk>CDh}hX0f!%OaGaB{5*M!0{-3z!_E^tiQV4y8ON+fIr(d^u zpff0kY6XQpus_oU4Ul{g90wV@4VFDQDJ^Xdn`rL|rmKUty z!t0s}K;r+ODh|x(0VVb^JQ4Hx(i1+dH^Kg+QJ}rg8@H@F&2$dJ;(BDAh|DPxiZ?h;{ z#B-%Bfn8w4@kKvLoiCL+fO)(k20~Z7rx|}FZs+DLCvY9}bpWZ2j}(BC&(?U6rQ2CQwSMd?<+ElXRB5R*X%va+8y)>llJ!t%)_G~=bc_3#WVdkJvuehP-+4&Hfd8SL0ABw`G=ALDi`?axrN4cF43AVo-K z_7wE=guaCPnXB(T$vUf0q88&+u?^gI4aB&@H`Jb>W+7*V-hn6FypeQlkclbmi5|$5 zdMRfu8PvLgg{@BPtu?QgVvl+6mwP!-Afzsj^kA5=3yHWkDr@`u-pq}<<=E3g7nu28 zU&*mWa(}97m#J%H$JmE;w>=u@f!OHgqj>sus=Rk3xcS1vE$>?`L@DOq7AU|&r`}$^ zeyq)`?%1QVz#r4tcD@9|oIw^hh~)HG`r5`X6PUUD9g@Q=vdvkTsJGtqTNr8VEt}{; zwpg*pe>e(R29Q#h#c_eso8$jfY%b$dKwtC~ET&4XFxyw4wwl=|QpuY#pKt2rA8WO3 zBUQ5y9SxY-nkxp_1sXej@KC8_a-@Jta+^a4m`5vISB8&Utn*WPZj`pHrZl?M=P?kZ z)^b1O()+5lbNgf@`l~fvXs2^JiO##Evi8c8G(yPnxZc#Z16&~xc_@N;qXaLIb;N#U z$gl&zC$duWX;Z|OD|#nJy5-d!-s4R1aW~XuDzYGyH0y7fVXMPd$;1>_DkB?Bvj`ad zZg_b(5Q~Ow%EWb7Ws{mVeCIvP#X)VCMYftj- zDU?Qz*p}{Y*PPE5!cQ_Ffo<$&k|}&1XN9oCHEFo3N8iSmw)1^yL?o|M5l_}5 z2X!^rrSvbU1IG(5AL^K?seK4S1YS&%JT}4x6kQcutAZ4IGWn%v!&k%D4Xh;m;uEq! zs_rFJ19qOd2SH~H%a#5pDcLzN)M>?f)_rmwZG2NFG~q+yRe65IeIHY%Bc>;26jjuQ%;Q z$kcYaQe;LLhHFJgZ3Z zc6>=k#f}?2J5G_MY?Hlpd3@N0jbwm94iPUET4(Jz>c=s=VIW=*@&w97ZqYuh(=?lb z^;3YK5qG(}N@|b#(23B?#1k%DKG)jPcjH+=UTQ5hXY2cCk^sXN!R{}pjjXVxp^Mv& z*RQa<{oW>8VsqqAR!Va_=SDxYE~C9c{%eW%jh-L8EIrl&H8P^2!3UUC?3`~auUDmh z+!T{})hZf<*7P6h^Qxf1oL+(8J;hFA7e_;o%L~v%3+yr`Mt9HmUq5zc<%6E-N%55w zcFX-8TxIu(1#Y(y4{OpD=R`Du}E8~&Tckb!i{cbkD z+vMUsSKsNfnVvLBr-NEQC^ZTE8F4RIy9|$!#{6A~8Ajer`_!eDLY1yG$Jro-x%75P zEjHlEyhJd9&@CcH@+Z43v-o#U_A!gnsLg!@lNWn}HKKd1PkpVArs92lfLm=o^N)Rw zM%{(?IzX>adTckOb@cI{udU>+jLv6+Y3h*HqffE8WR=79u4V7jzEq_zKh3ZV33!IoeyQf#|K2$@g*r5* zT2xd^eoA)%6{a(C+3Zfzlz|F5{;~R39exH#{Y5a0OEAaqx8E%2kgmUT|3#JKzECq@zy1vNp0T)P;&+TCkyl)N-Ts6~7OWU_Uy z4I1ig>Uy}9Q_N1JP>~3;ul;u56&C&1xyX6|WmvKKw8cKtpkwx907uoLxmJbp z;lyy8_4nBkdd`LhIZrA}F3@^QdK~$o1w#}~B8vwilIgL2w+f_7Q&JrD-up4iTgK#&n^}e} zYD9eHdb|$VCxlG=IZlS#Q3=CtTX?Pn)xa@W81k}|lB=(%>%*JZn1KKpF>TkessgE! z;+i*Vin}{8bwBDiC^zP0t7@QQb0W0TivK?ER^)T)C2frPDC=M!w6QK?8|Ck@&V9T% zlptx#awoqtg+gE(7OpZ_mxwmujEZ#E*!{<1Qk;`f@GX`+kh{LN#ntEE0VLq+`{fOA z9pt1fYtrkDNHzswgP%Ne3kh6m7@pc}HOe$Kz1%$5#?axq`>uQ}cc&acx+Syx|pYxf4Fm^^Gvc zVT+%v+YRrM?lS9PXO>Xt!WBl1F!gM}0JZG3Z=`j($@nyGiKwFwY%4H*4%t?F4CvD{ z-dd6RZx%t#oTc*$q@`L6FJ9NapvuQ7s6ZT9jPEv|P0CRiiyB;TUTUT`AG~z{zL3V8 z5%+)Iypig235Q{lvHs%&f0~Qj)1t$ua{u~e&sUw?FUP(+U!F;>iVaiUu2h8Hx%$I*B8Rep-yo7krnd3 z;SPB{LSzAv$m+E(5!km%7eU``;EO(hVBt%M!6gi4Gyq+v zPXB53{ES z7CPq9ytJEkjel~>ch)yH=2jIV_b8QGxm&R4?p)c~v`W5%MF#Aih)X8-pFCH+XTtDH zEO>uNI$(Dxu;~I_5WU$y*n+HpV#lDSz8Ker0m|prwpBGv#f%@AO;mK*bXN4OQ&Kvl zv#ah_+9Yr}57xsmQP|7%SGN1d5;OB!a>+&4EU|VTB4S^C( zb5oBqRDj7}5n-&&SBC=8QBwkE(cETZPNbc)8Jpz!8J@+VaWh@-&6F=)UR7KQv=6a_m< z{$d4N(P;=gzYn`qfSh(qBUgFRBMe~7eyQy*SvvAQC~O0kzCrZ0C*IA;+x$E&oq{Nh z2UdO25i+#=N7(rCvdW28`7+^_;nGzY6!P+Wkfrpw`~`N?6|>UJ|19ZUF(zb1I`%Wy zd4|O0EN|c`B?yC>^7bE3cB^Rhm4DwD&E*w%xE*w^VY!uPbYQ#{@dgyhM9Vu9>Jns5 zv(Wa%R%a<3gx-lPoKUDZ&hNhYVu7?otYS@dTv855kbdmN!S8tSKgV}xGzHrovK&P9 z7A`KNuQrxIWZ@NkFT$un=$=oIBas8I2#S$pyXX0BsO>8T@F_3+{LAqPCFp8agdXPq zUMlJ!4sLZKg53&Pz+QPT1(>apsm^O9V|U=-4e`SINZ0<6SphIorm~}5ls+!;-u(Hw zbigW&ggWUK5}y}?TEgySfsCA{bW#ST<~m)<)ZU}#^-Fiv+Rfe)HgE|APGrN2zX^0g0}jqe za;dRR3#eIf48*ppu8l-kZ2ow?<}B^ERnO4fm(Fzl;~+!f06RuG6gn$6=62Jv*}$`o z_u*nzqweD6&j=cZOPil_K=qx?+AxndQ9;8~dViOV?C8r^27&bVH<&n|S<(9umPjAr4hN)1@chSjt%HbSy1A*kSmK=TX_}t7|q|n^H z#OY}a=As0G?$imc{D=944FX?4GiSXE)cs9k=k2}kdP$5F8st~MJVCaINYI%2uUf9{ z;8R2o9tm&1Mx14}yuYEtxf}IMB4FEL4E5W7FkY@s;2~@C1+YgS;SMLo<0%##g+4oA zUSvrV4zIUm7m{0YTfb{7u#jz0o*>))jryirS$VZ)5qIWGsOY)2zm@xP9UL<<98I#R zCL%tk@qwn#;)zc0Psr6ERu+TtztHXya9bn?`JsOO8*#;~9yI4D=wU%@ux<5BOWKf&KV4Bd$-Pg9F|A^(3*S4G1^%jqkmjZXw7emok z(wB#;jI7kHYBCG<7+@y0qK91;wD+_dj(33@5?A#rbGaxB5(o@xt=jy?xyE8RWZ^HF zl50h7LI_AzYf(Qsf?z?PUH=i7aj)E?KCJb%A3W#!Dr2G8+T?usjY5*oZkWl#IVqqc?wg#pyD>N9D^^ z-+HpUB!xNfH@cba_?X(ce`<%ZYt673v(LP95bcae#hzTiDU0pLUUgzIa0s~YQ;$~Q zuKVVqQ=euf4}KU3Xo~ii?AqF>nE%@nQCb$E7_y@t(YqQI)J?TwT7-%4Ym-MY|63N| z#ZIXDW6>ovB8z^jp{2S5>Cyty)%FmD{*ZbB%Avr_?X#TtYGy;z915004s<%|_>%9HdAoVh?$LJLSed=F^tOt;0 zO@yf&+C(j>`1m$atkR~EQ;zK*g!TN`>wgp*@Kuc}!nNTnzBL<6Elg1`IR+OJ8FBOd zx`BZ-^Rh9b^YEm61i4@oJkN%g0s@j7dur_cwm=M0&~%51+PG6iA0<<^>at1vlwv0I z4XJ#}`hY#^vRAo%R-8{s_wE}yKqavJ=UAT~wWF?a1tSzQiQ6J?AA`f|+iRqdXA9-%*`h_(h$%M6lW7xT?0m4C5tuD zy4n|@5@{9zZ7_)uOB5{Du;AH+A*^}%DbwDMbj_SQgJoCuex>p3I8>xDy*(?><-Xf7 zB0M~sG{96JULap9&n`O2DeCujK?Wy~W)9cqC%qRgIHgf{OhO3QFHbe*vh*3sflfAc z*+Em4lJBGh1O-@nz}$R0WTvX!{d6WI3pc{h*3#lE8lVn6%zbMv)_d~! z9($|3wVEH-etCzwesdiw?6QBdYpbpmSGPB&>d)>?@8D6n}j( zQqQ3NnCn0FG#^Wcsv9-;dJ4E#3CHC?Ca={_-Mdz_E!(3sUyu4sg=fC#e4Z4bH@IUM zYA!n}SQlL|62%ri9N`q*{`J*RIQkRb+nc!9f{HL%80~Q45VuJqkzk_}vFRxEl59dSzJ93JX8f@5 zU0w^nN~P|}Z`vBqs2l^-OMVL8;hv>01sMWDwUKIMpB~kHDWC{bUZ549d_;9E+_>{H z@xF!;sl$=QU<1DpP~=csfyoEkBC!S^kb%X$iSg6@+MQHPF|$XR5H!l_%#O=yM7oyM za`V~S+GpCJN5b{N(FC*l_a?$zjy)Lj4v1q_?&zUO6=*% zquN8uN=-@)tQCv9wC@TFhLL6wrT@G)@GTe+#)0d^%Uk^jV}8>ukwKf`Dif}g^5P&t zp~Qgl_!`XTCqi5TlA~FNMf!3w9v*fjsaA?(aw%I4&*d0Cb-tM3*wK_Fc94yshy?GeX-Nf~Q3(JOR8AjG<;>IwKZ{osuAlD-2(YYH z{U9Z$w>y!MAtjly#`!Y1{ipnpF{T%&ec2lOBb7VU_dBOq{(%rPrrTnXma~IWv*+u7r^deKz5K#EvT0 zTZ*sudX8#)5&~7lw2}kQWKClMjb^z#g-Wrju39Enl63zytm1FAH$%3n6=|0AMt98F zf@{Gc?&(-oIQu=CxXOyBw5r*1M&rx{?k~p993~Tk2Z1dcASNUd&yqG;DTeQOzo_kj zy)_`GA4jg$!^j`u9-;ELh3Agb{Psv8RwIYS#Y*EPTha1ON4DGX+D*ySluRl%Tc3q( z+L$MxqxDKG$d~t&L}8M`iitK?ii4Bd>vb*R4f}hEspisRsl#^lQJ;sNWbQW%7Zxe5fbNo3DS`;TkuDb2&bP^N<9x) z%A*U|kEol+9g-?<5%TR*IeV57H*jIRQect1FS^ZaW>f6MX|!^7$iBM}s7+9avkQI( zwCTI+m5Ehq**W+<2K8;u{V)c#jscAUjT5?apyvStTFg&`pEh6gpB_bNW?HQkNedG$ z-|nz}C>8l$m>T?pM_ciE;wL$3n`5`j8?j-DAxxXBou90HK6am&pXS|L8`h_)f7@{s z-ICVuAf|mpl-ic+73XAJ>Lv$tyhQ9&&dN;r4{pcyP+m);hT+zfL)J(>$iynj+10Mo z>373y2$JN^i|BIe_ZI9PP^nXn_acQ~tcK0%jnfi46SCZ3LNAhpd}S8+`yF(Alr-|% z+coIVFmq+eQEUb~r84T;{Bb%p(6|^AlISP61cO zaf&Nma+?Rg-lpZE9zS{(Gn2selJPjL`fayM4y{+^r*N1)G731JN-1T_B_eyx!zZ@> zpNi04s%O7}WV>b#%^3%Vqw2d;V$hkwQ~=AVy63y(f{7P1w*iN>Jp z=F|;u{~RQi$W?BS@J%v_I~hz#<2w%XN7LYLJjHXD>XkRO$6 ztxa>Xn3R^r#k`((Y20NYYQDv~^z@z4=5&^z-_5F~gfB;5#&d*e=WgMadh{B?w6FQv z*u#v}AsXYQGeQb68I2jf=8EX75>$J8MLooX6dWBgAj)Z~(RV4Pmg-c94=%fq+;JpA z*J>&;J(5xSjV|xI&*<3~&S&}qVLfi3cCw%?_T((Vrns<(X*o!j-z`?DbGs$K%Ylp2 z!%B7bxp=*5-eV`W6S5ti_NtkfT3J~mTQf3~*I%DZ&p*+JsP*XJc;4THuO~3F;tcxk zI0#x~uUAsM;t=!g4U!~`Ee!})N;_P+cAuspr=m8C@*U_JddJxotQf;#7uwb5mEBl)cCbv9Gvc%EDQt<%0d}DWvI1Rni+{FUR8_f)=LU-z%OPT|XE&a;*-lqx*%fl|pU*PK3Vq%*l=~%|o2FU*4F8d8 z|LQ|qvjseh9=FnRBATTdYl7%kvUY%!5#n}iUXY=ra&>I+A6o~Lw`BQCn>OKwQt@(I zdT{0mS63;UoK44JO96^+t)quMPDNpL4DoHh?0>|(4Vrf>AUGb1ci&L58Qw6SsI|I1 z=v5uu)Fr0iOt~=D)g_fvO5m6#{%9{l+3>C~m*D3)E9Y#>pvmt+_N^*>s^hZLK1u7u zA4C5DizaowXwTYWv(8EfOdzZG;|iDlrA{Ve75CMfjT7JORw`)8M*v7HA&bem?}(9Z-jIbb%ZaK~ZdDcanN$|A8Jb4V0Mlkr!d zI{G}2N?88gtmx$Pjw}XNPs{J|qMtLeb#0$T2#GZOEtvlu9ydqK(Rf^H-#+iwVmG^_ zEUsSp*G!TijAK5n*XNA;d$A5+*_BembD+S?Z_LR(NwIV2=8^$))(bMB&plso6L1%| z6io8m^vpBA6MX6>ELE$StCq_w(%-4((3>NpXuoZP*y=TCz^jCMk0^e{1|yu_kT3f% zOIB5kIh^jfGA0Kt+B7w!{!4VPffmRUkNX_3WiLeKH7NP!3MTQ9e3E{2Qly!7KG6FJ z>Pf(oM&Gf#`fd>QQ~Sg)^hVW}VgWvRoJdeHUQdEuS5%h$aRt0th+;3^^0rZ&*XD$? zdYq7dft=o@4o2FR#eOpdvIC4}_9AE1{2JD&w&}Fwmo>V$|fTTghh@r{G z;<~ny(?)rfCU(V)FDGh!ZEi?QG^n3Xf`+o~|K^d%%ZS#&(9gst8P-nlw*b~wEOcsYb043wwbpv;O6+=u^cF(VIR0`D?RBUFu zWBbb}G`od&-KiD740Z*9nnBtuvo-I=ar=U*p5{tXKrvD+1boYo95LEmqpd>OML;#k?+rE z13xPT`SjGW-KI(7Q8+Nkx#zSiKns5Rr6AWe=ib|%$p_-i^ngEmEkXK3F*rVP%AujVl6sB=nT{h(wt*^SIihX&vB$fD+;93<+N;W*}>kEnrQ2hC+ zhs^j2>q^wndu6JHRr-!pkJGv&@OL&(oFhX2Oh#(lBEZ8_!8tZ&AqB?f99YeMlGAq4 zV+PfNiGBIH`-+n7OiJ*c0VhRxw#{%tiE}0~Wzl^U-EnyGZ;kswLywMs%DWut3gIW4 zsQRnL@4xA>5Mh!bm@Kaw_*q3umw^$Hx?gFov3UwSGuhsBn*Wvp1(3W*aRW+aUd3!# zgzAn~L07gE;r`nd@HFNl!YWG^RrnF1Am%AjbbdE=02ca~TbrVfGN$iWVCX50$at>t zgSF}+5Xoa6ZJvZ#Wn&Zb);N1(UyD`)Ijy-V3WR+y^Jl6sb%B6yDHCBaWRdFjcT~o; z00YN=9a?YNQA@M_e>7cXKowoI1!+X-?(XjH?(XjJqe~i*2I0~m-JQ~LrMp|{5ReXO zcyoF0`CI0mIkWfLYqxmZw+}M|PvCUV502&R&l(ok1!dasoM+O#hiS`K`u9JWIuc+b zl6zSlKW7h%zcX-CYh!ZIHbSPuRcJGk8vbTu2HKQCi}911YYPbL|3>u4+1wQp=(mFq zTlA8yVFf@A+nfALm0i$tGpnX5l%ZiWCDH#X%`!q6bvqx>%o$y?6<}HdZJ+k>4v`nAs)cttOwNuIx&APZI`|i-i%>mV3tD$Gc3aVo zZLF`+yrdPSwIB`I4gb8`p4&K?6gH;O0sAMFEtLw!@m*DOlVf5?)Zt&$31gKzq?t}J zp^fmDF_}wfX*57q-ZrI2Di+>47hJL`*A>!CgY24TQja}~jpyckyb4MJCgKVfwm|bO zn;wn&x)`JN&3%_q#$ocXv*e1GBWZh!-|&XU8XY3tRkl%_TIsctRBl?n83@ObCZr3% z5FUu?YV|0!d^b3a)Bzy$t>FQRfU+XJnEzkOfFm9T6Fac`RXZR`dRhVK3E}44XJ5h| z<-8xyy3A-nj71ad&=q-XegOaYKAv7XDJC^L30|EFu9>TCW96M$b1hg&OqhucPZ^FB znFT!mwmvR7zyVD3IW?`kto0n_7Mhl#kx+`uy@8f*sTW}?&JB!7YY6p~B1ly^r4Y77 zibZx5rb2jVt1w&{r%uL|vxVVQK__ZI3P(yP$fEkr8HF}C4t~&2+$PMjk%$(K6e`SI z`BwJd`X*mAKM=?OLqY0dBMB7c*iwTEQ9pbkRJUL{ap0wSXjQy1Psg9F{fD@<>YS1p z7=cjyU`p{EkN#JPenC_={Ml*Hf3LS9#^Y0d)Ts?~yY<9?w;GoIiJX&T3&*Ce8 z<~RCTRwyVbhzlI-VAU1$qkc|rc!9O_K@#0Pa-qaX`Pe-CyAy%D-j&*7eqw)04MlH3?hyQ|mz3VI#9q zxGBprKvy3AUBBY!SM3h%Ka&=T%|qdWBwb%F3|P4LO)R+Mxn@Y2_w4j6pCf@pE~I2F zcyf5GhwJAh6$0<|FqcfBlyT4xLm85?C??wyA;*-7lc&HwfY5TrTI)e7e(FQSc(P59}{!h|4!JBL!nDvhjkK2xJ#R&%&q^JECZ47t zk<1Y{IhKFcso8!e$Bci@T6kY$iaz6H$;a&Q zWAQU979gyUO+jQk#is0YDccw+LEIVrXr9|ygc`2L!8rO$N7#}X91WPsUSK@biU61I*OnF=!<)R_7|g)H`>hf4B&?s zn&no!L2}Rz!XmBtj$47^z|7uONbU0YR|RQRjag^2U~n-yUP3a;I3A-x)%rWn#M4DQ z+H#ISjk1NyM4XTh;;R*i-&GDF@QwknEioBF*FrT*e%)VSZvTF%y$!p|dZyP|*tlHg zj+a$f4#(ol(qn$H&r|5F$)B_K-XkFGB^&1bMvR#RUl0>ZoXg5wu@2rm!|rEotQ2v;DbTYm{RYE|quP3h#G6i2**4l?}_@06(_?&7KXlq?+zDlnts4@KF-qz zKh5X(c?y)Ippz%ORn}Mq_5E-8Dl0ztN#YpBKud~BANf2Al?#Zi)Zmt zKd3Nj>&h$>bNPI$X}BvKj%xv4zz*moOQ?UlY3+xT+$5)xtQr~|C$4NB=KU9Hq|u)G z@P_b9=dyP~h0!YwDg6U1{$ggMYROi<5I~{l;C|kN50lTwL zkr!Z$a1+s+&c&l64wGuUU$^eaCnz;7MQmg|KuK4uf)l zhiF1@-Kq$kI@E`o^&Wm-S}V)+dJjmLx0h9|r6+{mE9hCQ#aY#5f#KlQB+WB&hP?16N`cBm4#;!~F&f3%S;znE9NI=L?ifKXd>g}qT0Lql ziS3md>f>3}sX@X&U`yEeh4}Qlvo5_X(U;NGc3RdMemMM*EXa(m-%4L3U8AZT&w>UX z1>tSOeB}aA-`bxW_fA)Js``&MOSWn~_BN1vFRK19;kU|}G5mG3?)tNkRs5SH<>glb z+m7s4sTQgWV!?Ctd?-dOR~nU z_0uvn8M#jhUIY8jCcNW#sxVgXXt}WlZqh$1Tw+5n9(0o$0}WRCYqZO?f+*jr_tHyS3Z2 zu18W!(BcXD1Wzika-u~?m7_F$1(wGp@%lS8n{t`^yW19aL!J>93!DuPy#LypYk)ZW z9T;uSm5Ff>iJI|W0ni>QRsuQNFq;Rg;*NS4&DvK`(=XA#-^|#bw6DK)aumQ<$h@0F z{`;TC>o6;-a3KyUiH#j|ZV`4& zj?qW4D7bZ$Uo@Iu9CCa$cZeS;Nt0XqfVYDCf0!R^-Nn1&HL|FtS2B7R z|8FY$18hnDd7Ndw%<fm8l4R9YnA>AyVfrd;rOtG!zAmFWC-m4z0?K=kt#J%C9ssM#DsVz=&Z81mdb2zP*Rm}vDQOa6W+oIz2XQb@|p}0t~ zcS@(ut&ypbniZP>v4nb)qU!@oB;^I@pI?h(3I72B;}Lqs*xoRz4s zF#~1}PEvVe&GScy^c~QYkHCfj$l8fuC6En^nY?4q$RgDtlvCFi_InqhF%~t_$4Sie z?%ozwzwquIUU{#-@&zR`hyD(jw#Vux$Uv(wBjk@!l3O1HX4mWsxDP#`;yn*zc>;Bs z(UXV!8}m&l9uSpG3sk$JIsmE2ax>dc&jDX zu_#Y3`5jL)`Ex`N%J2lFS@g2>hn#KgtFy&jcm>l?6~#z9G-jo19Vm!`Uc43nLpjKf z3$ngAI!r18=rIRIOFETW7?BucyTi#2tPP%u+(WAQDz7?jT59 zy=@V)klr26w z`ga$bU@==i#x|pICi;ny2~CowZ0C>g5hGY7zZEhK3)Xe|5^T7hgx|jxj{Nj#IcqR& zkf(5e!UKUjTfBi73V^)GU!~ z%|5>8?p|V)EdNtw1#WlB)-M5;T%4F6I~Tq(1psp3*}Vg-@(PIdYf~un4HeO;$F7G@@` z^!5L1{G~Ydr$YAN?0uv~O(S^lSLjc3Q`Or|G1EITYxn&t9us8vlrCgsTN~?@XPAIB zVxZgOm`L^|mLiKeW~YowUh>PAt~KH<%Bf6YUd6hx2LJqfrB=@xjqDs_c2oJo?dgP; zg~Cn`bM<=T;d~KOE zxd6J`=e+=Hc_dRg|2;@`4=Lf)v>^$HyuN*@%Uc`bz{>HYmABq1_X7ACXjBQ;^lYF zXhqxiD=P{@I*Y3f4VT|1^&9_$WtpR1SMm#8yCv?>Z3zjfG>A4ajHOcV@+BjTW-vnQ ztr(A-Q}4MmO$AkG{#WcK=^JvPKX@jkA*#~M)DTMj49>3s2Eow0HM=fLGj@(MOimYd zgq2-QTa||-eDF%D-|`U=l>OvRNJK*NkS~e^a zgsgr3_cr59IwsQhjir@elz5e{)eU*-_t;$dfC{~u6B?sF&}qRhOhEGCAPvtP_{w!= zRA2d0{XIi>(s^4V+7Ca*w>`U?1p2I+2ho{4JW?{J65rqN$31@bqd@gfM3C@`_kwY? zlaucmh1u&-$kZmG^E9KjbZj|va@GVjV-e|qT)b5NYsLC~_Luk)br>|iwq>2~Ytty{ zX-qRFng3K(;z5nz-l@T;v(e(#oIe2ws=kfFI12;4XUfE6RQ$emCq|n1o=PmK6tS7GLy1MJ|RG#aLBXW{nNU z`d;Gl6{~T@YCT3@L0Xiy=3}$}k&S_DcJ6pW4*reC*9S;eQoPnF%&>GQMRkITw8B%| z5ly2)y1T!tv04}Z8*}yNt3j3e^0{pHF9pp}>Kq#3vBpM@@RCDuxs%{5MU71fRi$MC z6964=BWv(O>Z8j2)aSlnN#4?t^V$O%3(2vnrv7J z&6lexnmfnn)6?vvK^HPc2VIrvjDt30cu!bx+$`}*F?FYfQ|5f&shx|B??(22rvM)xZA+o65X z-1Z0*&j*=Z*<#szB_Kydc()3w4dLU$innc>40V#NX|Ob}XA~Mtp$qT036BQnf38hP zvPAt6v>2~8ad9L%pUc7zshmBhU-Fnu(X8ruh4>wQB|j? z+KwbD9wIcq4#AX@6iVDFh8SjDhfd+_^<_=!3jtfK@Gtck>3x!t5g$H0q$G`xjxsRk zGLAK9%+Smk9E3CQ@(OI6(#g~l7*3?URWtOznE?7Yip6;UG{3i!%W{Zq=yrA(b2W46-s(Q~>+4X7pQe{9tJi&-VlW4^iZ0ZzU0sh`TNphTpy*)S5njuAMJEKy_+5%ACd!7LK{H|U7wh(4Bl_3vBUt@ zL<#y0Wy)fmMmM$Qn!qgUZ|3k8jvCuT7Ql)}zOf&5Mb2#+5*)?G z^sj`}BS;-fy*>q8jEo$)Nf>Pr5inAmnYhJ9Dkdg6c4l>3TU$S00ta`O4XkTJ3$~A) zJi|`%$%CAAWkD)4TZ9Vy%1|ooN7xulz{$w~h69yO(2H;H6c9|V?A4L|hq4F8g@>@% zq2prNK-MJ3c=)3z4s6a*#9^ko7SUhF?5wofDgfM+?i3P5z|CCUO!A`76tqCK2OY!{ z0neiy(Ts*lrkNpbCRay(2@xw;oKHiDZ^_6RgmLld{41r}NZcl$LqNyNb^Y$GeOB`w z{7)bp2mA|;eVc%#`9-MeDPpw&Nl8c_nZ{Ru2$9NHpokQ~kM6K;IgyZ{y$$%kNgtK- zAeCDQ*1eTq#4*!-Ld3`zV-Dbd&Ys`ia!NFPF|%L#=iy1`_~VNiwO{f1hZJj&!Nc0; z8Gukh$v{RoLCM4w^P+k1)lBaqU$mDEHehT_CIPu zrXvhXCq7_268$50p$R@+NSoj@FGhR*=)?k%@W`TEGoT{%kP%=|GGF8RaN4W;s$D+4 z3mKC?2?oHa>-y42o*iP9%(MgCtt+83AK;W%yDlM#foiJO`H;`@HOKDme; zA6>(9R|kD1ig1_7pRoDEN040fGbGT|+tw$g`NmC*XmpHq^OOG1O&4eHQm@_8-~P6N z;^b3T8f*twH+U*vrbgXTCTTKhra2WA+}*B{5!M$|J45j$>;~6B;>2?-CDk%%}2MV{YbYLbPX(fR88`)__k??+Bip2DE3w+gN?L|D2ms9xA-hN?Irhqb5GgfTwI$`nICuZ zPDzc>UEwn{(Ft+;&+2pZZ?)7(GQEpzM8J#ehpZhBjujiySX2(pASpWT$~Zv#DZVGl z^-F#*OGE0@&G%1NT({v09YjO))TTGk&Wvk#Nub-$wY<@;VEUpD!B_V@1J!~J`Y9wVHA{N~iFQIoCl~F?%8Pj0z;KO7I|RDHo5QTW|H3 zS?y|2=MkkcF53Q|636<}vk3qEYjZ8{tcd>bB)NkzFYkBGlwkv*%5iO-972bTDmCPR zRm#YMRStu43`TYS&VHLQ)0rOI1?OrP|J``O!4xzs)s;<36n$6X z6N*q(_2sAbWVAgcJ-B*&9J$77%kN8dhszRyFFdfH9FJil1%ilW5E&;+%E+W{gdLOL zrWxnDKZ32bUl&3f_34Gx3bss|=Ad>ORejY`cU)rOjxK#Z z*HdsfL%9DnqPX$_F$qaLv#P6w_>MXM=*UdpXam+?45kmNzr>5&qUT@Xm7sI%U$Row zQINCkutNbCdH*f%RUZ>>pumi4C~dhWU@eNnf)Rc-+ZqjLOnuLAm{vBTIZ;_Z*$!Bh zKvQQBW&gVha(=kNpL$3&^=hOXtf;A+c zH;yI@Yt+4!@Y53Q0r_Qy5Pc~7Xd9Hw*0i%>8U&lwh$FF|ZV9XID?U?U$ysOPUb_s| z8@(2~KJu;9SJTsKcUYJ2ksFVOCpibv0a%=-6_X8+$DBzUwU7;X)itq1XuLi+l|9MxkgQ{bfPhd?_;vJ?v+B32#fhBL8Z_MX zwH4RO0d_K@LPigB&&nSwOX`b}GrewCo_?fDij7V;>30J%n|4S$;M&7;t6b#T|t<8NZ|Ar<0ZLcJYlwT$Z)y4AHU zL#|!wSH&USB7V=*_joGo;YUYyrKlsCw&!1)csA6S@_*>-U&Kepmy8&7{^O?L0O6EH zb+FcSi^Tut$GeC=4Kzj73-NMF5F0~fzgM#a{O-4vuw+>N;K6Bvli~q7iVPD-3QBKwMEg;_5>D^mwO^mF<&{N0VaS#pkM1%6sp-7bj;` z`_L9VdpoT@7kn2f;@(V*0m7Kb5LDmAyQS{nsHpUpPl(TsAoC z%y6fvb_LjaS%7(fklhYsMu;s0$kqa{(rx4$$jh$=!Sp_d#GrRv(fez!f4ii&=kU4` zdwio}RZ1I$?2o2f=IeVXQM%tl z@iAE-Zu*>)Cu^1WZ}tUPsY+G_;Ynfn!EnpwQ#EDt7qhs#@3pSKP**UVRcFAM*Jr(W;Uz;i+|XKgDkPrTBybbGdW4EH!uvZw72nFJp*W7lg7@am zLrT;;zu7R_IZqzi6^Y_Cg?7*R5Y%*d=T?>d?K>xsjoAjDKaHUZ$2m!zAH>uFF%U=? z(459BO_YFo!)z`wIB6`#bCmjVG`RDK`d;B*GshPq%v>4JAGB6UHqAeY=~S~D%*P^e zl*;vU#k^vm<du`7y+turFQZ4qsI-V4Kl1tr(QcF-Rpcc^$1%OB|79{C# zhg82z=)|F|wncrB0_zONe0UIfE^fiI<3v^yNJIbSfD=(E@BxL!HN0-@ApQywP5C)Q z*EmOe&+vk8rWIOalhXa*iE!Hr2sE@n4hJ=Z$O7|`i}b8^U%s)1X-_DRQJaBqf`Rxv({bLC6cDZN9bfiNBx~qup)7J~pwNc+vmI0(z*#boDM_PrFGq zNDu4giZIxB#NQ?9g|X|leoAQ!Aw5n@3)0@xH_^bPsjy7AP?mhU`dInwUM36VV#6Rs zc)@jCRfu^gWg#?b-#|^DZ8Nfq&=ni zdwI!P8~c+&=Aojf=5MZ&rxu*xQ>gZ5m?^;bG)I8IyniT{)?%hcA$Akh_9zV?@{SnJ zx_axofV7R_Y61v9=Nrd{jy~v&mYz&u!;CxH$Y?^6qD{%Y zDjMOH9L_}z92QbG8SKI}Zw;Q6U)bZ5uGvhfrCa4s5|cI?neU_`D31p3E-uA72Fn|Z zfo#yzoj`v8*_}-|B{p^~@$p?c8a^X%MF>m3dkk0XYk_90(5d5|PgS zeNn39rmQs?cZmV}*HO3xp|0p=Mwqx_!L=7wji8)$N6eBl<1oYgx+ebPd#PaL zj9hcK&B!IOGS<+Sirhb9*%6o%7gE^O3z)-s&zw+V%Wv#>deoeNTx$KAOHEhr<&pft zU<{~afbk+`8ecmhTVtl8V6d3Pgr$Ktmz9!Atg&f#?2OWIM7^B#AOL3Awmv2vqam+9 zW7iU)E0xUw*{wB*u_ufDx{hqH){xy_9{b}tkoaNzoph~+n>&>3?iro2lHfo40$95J0BQH;L zO>|&Wtw?0LvBH;3K6ocvWU8R+iYBpTXJpHo^|Cjn&BdvDL)(im3w|PxtCd>4O}W#0 zlKre98M-83BX{0GDj%VRaD;}y1856mJWg;O&+&fVsM$DUrF-vF7%>a5d&uL3Xo zZxyb{g(^CzNcWUwpf$xTH=aU>+gO3sw62ID?sZmRLjua2NqSM-N z{A8fvlLeCg66-ZSh?7oflYZ%)mNuinXZ^iJpD)9cXTFCJ~faY^H1teDP`=`9Mb`0TOVG!}v6m}#RZ zE*_2Vy+t*0OV0<)4kvGT15?i&SM*USvcQ}spRv$K4CxkQmOg9yu`~q%W(D{VP>9L? z*th#BDDds1Am`2kT*ezWD{XO4VKsH#-Mtb0ac~&QHHgrZtp=KAkHkhrB*aycmopus#9oV(w7sVj#1-})Or0u;OUu~BZQnvnI5jf&?Zh<>&>RP0ULiAAJs#&)S(ng{XH8`q zc9Kh(Bb_?;eLox;H*j4Zl~FK_0$mPorDylSP+!;LcIhV>CAaf9R+(xdL~=stN+ z5)#s9ZMNEaA}A;*UdZLR5GpH8%MpX>6e5cPn8+LOikOZV8fyjz!0TyUikJ+3#BoZ0 zr44p4S>RQ^%bQhEO-8obaszKrtLs6@=)A*pc`81|qFuZ#){&ktJ=)W`y^$yeOn3j^ zr=!81A~S|j)CCnebaIRgJ9dV_Iw4LBC$Xt>XQp&!*Ij<%8C(&gD4;kCAmEiDAgmIZ zUF+kS3(ROT#^GrwAW?j0`tY|Wbg6~UB-L%3V={2>c7fR6s9i% z4lTzKam8<9s)OR5*);{Kh%hU!`;Q-d)ZhfMw1=wiT-9Cx>&*)JnFKtY>exgCZ0R@_ zGiSXcB1Nqa`M&oS5lH|c_$pGLJ{;)_OgBPB$D_%g`s3HF%u6=sVc>Kx^!@s;VJySx z$CbMkQTCbkn?$Qf{7G|xDoG%~dFlnBl$*Sr6m>S5+Mb)|xKL3#l6EFKE1E*o(Wz5X zI^^@C;9gl59x1MDD<9qdV}Bbjk&py-caoU&E50pbrMa}CtM=pe4e~kGiyvQg`i)1{ zlT8-?ZtO@kkYeu+r3m4U4P9k&7CZs;RRi*4WV}_;8C8f?cI!-2D&3IWwg9mTFEs*Rri)9OJk8FCyW$v4 zk>HzraNBSE^q1pLh2rt}2i7H%F-F%gAfX5`vJrQRvsaNdH4k@bqsQ+qC#QrYa0)%3 zP6pw<{FRFo4){tHhe*5yc}$AOCDc7rpkv}~-niOU1ou~+|I}G~aSNU>F|F;>U?;(0 zOP`})fr{+xkhYH{7p6*6%DptmYZenGlbm;`xhQL79*xs+l8j&_@Aqp){-!O0JeQ!8 zT7{mYfc}VvN1?OBXI+d~?vMQU*92+FsGjK`1MmPmS&pKBf`W2Gp=1Wj$JliyFfM3w z4+^jI)(6qtqQ$YCE;gO|4#34wMh)V<2Kz0OTHvWFVVGV{9FU%O?PeR@{XB;uY76`> z&vT){fj|)&no1a;WjKMuxITM)ChqUF>(rA!6}w+tLYo*cXi>4t zLup2Eja0_+y(|_-{-sTajt}n|mfo;M(Wg3|i6e804zrhFbp0H^xk%j`Lo9-z(i4W7 z8i@Tk)?&NRp{JunV{oV&h($kGPOJ9b2T*8$Y(VmlT(B(f9AjOkHd)R`qq{=(Q{u0& zo$>V#7h7Ph_4O)09@D>5GzH;&ANbaLE?lj&BO0|8C^Cu0?u_l+I^}tf320ldnvp{P zciQwKeGfIef4}`RNsq5^0_E$j$CC1AYbzT6d-X+@z~b$ofR1jL3Mc?xUDpo_^3Hj0 zNr!jSDDB(tkNn3fZLXgF#{F3RkiR!4hv@_^nIX3^8>#FbT$FG>U`?mLx}(3cdy46N zMW`v!tO*Zd|3ZX>>Q6r!{q~;q zo{@BR#?p5gjv|)VODRrhKNAQsgkpxMQXaD&`m(C}WQvV}%q0cswS}2#oKC9Q{WNFY zt%doaqLkYwwhLnnCVz&PMe35m#G>&(m^M=5KEd)CXYEg@v!6qLFN954>f!M-ew!JX zD^>dQ!R<=u@NA}i30-~A%)a49^g}{m6xB0B<&YI$+RNprc>#$|kOK7EcYkZAf#vZ) z*K>q7oz|w+I#leQX8en$$RzW52u=HXVHn(v#eAk0vt;-2q+1e$8SXZrL^HNAq$o3r zh5T~e`RXL*bRTENl|C^8PH9_7VEEdb<0In+rN2hCA2GUb3HC1%RN7|;ugCF9T*ao@ zS}Pohrq6+?-SH3wM|qq328-2%ST)=t^>+a(u2>jdFI6$3=E1?nY=w|xy$lC{5=HN{ zPqlupcue6$=Cs(?SoAKk<}YWtK{NIN-=nY&$Qs)ezH8%DBnS zmCBBfo0nKcPo+;1DphK?A{#tLn;zCxPziQ|oD;4YYmiyxYcJK9m49>|lP=VS?#=@O zAZ7-U0JPZB3W3q^X@5jq7(Wwg5RstPH8rTBHr>g0L@Z%{I_%aI``8hEO=pVM7IbIN z#U-OMvYQ&cP_hrl>#<-jqUx1zTK^L~uTFIDa2SKGFz}CQTTn?ykd?DF8vxu|pQTm({2zPvlKhgV z>l)Qe+VExRpK^hq(FosqGQgC~5{;ETv{FQ$D}^To7Ki9RXUHc|#X|Ha?5!LPPc_`q z2jgG5*^g$Z5Bb*gx&n->mDrqCKKSY9FK}hGmwUb|0+*K>buuw_aocr&-kIx)o;jhN zrea|$V%VHVaYnHu%I8Ru_Hz$=^IQ;nZEbZ9qPST-A?5SB(iE!N+Sv=33q(a;cTO*n zU+*lqW}~z!cE1=1&&Z{R=?)spBpOGFPCIb9(KLt&9$zy)ijA_broB{%hCWdz2TGqZ zwp^4VLy^4|HiLcq8&h|_aD}B(tGwFX41ixyl#;6pCC9;1oG^;*0~8e~OpI`t)zg(_ z@P0sU0<@q*>p?w01A?W@W&0vkADd${lM%Lyh>2|8nt0uG*>w`;NZa+vN~xa;lSkez zoH$Ft?zEWNt~`V!e_gSVb!qDn5tndDo2is=GJq zWN9y>@LH@}w_4ETfM3l9VQD|0Ow0OS{Jf?~@?rWdMdaZBDdSVYFs9K~{)W#I!*&f2 zU)AN-0zS9_`|=V?W~<=n5t$M5oB`u?3O46gc&!|p`2!!o2ESq$U95f1mKYi{%1QuM zGLmfGg_uaZAK0GiW-+HQ7K*Zx*Ft$7na)+OQgg7u#Mc?``dqNuuN1_4*hLNN^f2eg z-OyRiiA4XPu){*W+NFnH9TO-Y{Fzl9D?)zyn(>)8?3$2j=EClY$m)>{83yVt=O2b> zN)4BPgQ%R0ug4R^3Zs|xAFEwWS;Ll3wC7g*No5veH$_X;;7#7iMfjK{ zck7M3X3b9+#-2DMfoGHCn;CsbDPWQwI=F-r+91?&n3rYJr!t zs(Co=PXY;h9f`Xstkj69z>UY_S#q+G8Q8V$&t!sj;l&!zDa*%ku+`oI;7fY^cR5*g<* zW3rchRBKOCKD!~v(JEip%Rrp*7Nfwm&{&4UB-88*c3_m~goD?e5|-{eOzVdf75OF^ z23->hyN3lnsJ;X@%nKm*`wkKmnWp-nL`Lb1NDK&piH5b=gi3yt4skk~KI=4 zwf{v)hi3=+O3WXo(MlGZ4p>#BMP72j4PU?lqqoH%mF~d`=M5O$+MrEuX%ImmJPq`! z1SUmH$LN3ck-Vah4E+YhWl_Ozq(!~s2yew_E$vAS_u z3(r>cTI^{$msURduqkks1+fj$a!QdEeYPB_hrE16rs$%(mE=xJ|Cfo#1EXr+4d5z- zw3jiUZ(5jb%q0S!C#Ivb%DUb5Yh(qM(!1Y}X~S6#m6mlqt4zM{C*}vnfNHmW#tb^E z5DS;!yt2uzZJhl*ORIWK&WDN!YQ@F>Xq4Z5qb*eES>2lGXiE$fKL3LVW#$91zePU! z^(yz&f_k{aTal>6Qk4f%gC}h19xc z({&t`;`!7bp@veP&lp-fp-6vy=Dk{upKxC^^n{v%)~B2(DnitZz&-R#^->ECl0)6U z-kkpT%~eh@7ZvhAC=s%0jSSIbwgprQ%>rGUzzWqy5Ai<9}qecduuD=v!A@!Z}<;^G;8#_DWQ%R2S2@RRpJr1 zb{yP&5mp9}4U4*m3Ns4jwpiXCc}q6dTE)lhrV28p$iWH*%ptuZE)f^RYhlelCIzFp z(f`#TCa#@E!v6QFG^{p}6Ga=$E_DN<+ zW|@b=Vdw&q7ar;%8LRJmK-ETnGiHOz3jNMYXKyJ+r)tub+9)Fd78su>oI(oI&=+i{ z5`4~>WU){Dqe6LP?3=LYcR|3~L&6PqGj}DEoa;iH4*cJc>v~&l%_zQ^ps+qh*UDMv zwE0w5{k=pmHeuWzO&u8v1cA-<5+fEGePl3BA_kMY&C@P5?ZQn4DlR*%#t`n)JFCd5 zx(56Or5lubD_I`)QaY6CF|Adjtkk6fq0lCf&^EA)%HJC^>$sY-&2GSjcsv507z#xD zGZ)j`>_gGs)nx=rfFu7Bq%1*~Q9h=7Da2BWg8kb8jf(AB+fhZOcD_Tpu@0+u}gFek5Dqe>$QeV0oS|w*IYpNKHPK8J&q_OA`DNjFhUz^mLe5ZFDnb$G< z9mu^Kq%_fIcGp&>=B4MI|570{ITOSI_mK>3-Sqe}u;-SC+v9`5>!v|NV(|CtMG0t4 zWiV0`1!ippH3kKv90Nnai>~iTrBnm&-h$c0{?8DH&RMNP-y_h1Tngh(_TbfY1=1Br z`ksa|L>H_nvuKb@zG(Dt5ip~x{fXwXx|HroVMEuL{hxAK8{>~#ZX}=YKCjXb+V0Hj za<8yavhOH=c}sQk0#RFtM+yFNvD6<)MTWk|>vwzm62ki8ymXxMG=C4fE0Yz1@7xMf z$(sa6+xv{DZd5kxcU+Q{A~tn8a7cI9y&Ba3vh;nh7$srVmAA>h-{Y0IIq|}Alba$S`Hh`dxo}mRT%2sKL!y{2m+#n9v-ezJhH9~9wpj& zsprlsjBA%?s5W6EKV`!A+$)|sze@f_@6NgRS&0YaIT)hi^Hk7c7uXxQL`MD*0p~`C zo3pQ9f=3UIz*5~mqj=NHHBxQ)JDoCO_f05A$z-vj;%{p)Y8ko|`q*ZaaypL(d!i|A zedZN)B0e2*N$pU-UibG*jvg!Q0*qOOE)tcdsh72OIcL@$i!@G7Cki$L>QHyi>{JzD z>X_D;*XlWr@n5DJ@R@=k3onDC&wdp#+1_{CT*mUIUgxfgrdxQeb)*K5E?b!uARCXO zFny?(0wL_8YpHK(4Vsx?e~t=bIiVsVLt!l4>KZT+35#$T4IZOios)B~J)5%+vz6c~ zsP+GHw=V7XZemS5RED)^W5$j7?sPNR_t{oKXwP)r`&9gMgJh8pkas6R`3BDk9~5Z@8+-cIJC zpw!fQ@D_tIS4~8tnQM?!kkXW+Z+JZ>ajM2Be)q&@;WFhf!EWG>d?^8u13MEx3wCJs z%q>}B*?V#d#@f?A`dviTReOdUn5`?E;t#}aKY!~Yl%_wtJiMY!rLe6x5}p#%9mdAV zsZ6x;9`!p-rS1dk?_6jh`&q?eSgGW?&mSF_E##+z*v}(g*Ebe8>T04@^~J3%<>$7|Bg-K1k!Z|$=n5$S+=0qV+YvrZ<=^+{8rB2@Sx=}Teg7Hkg;0l1t03N!Uu z*t65l`+V7b{dcA4NK}&woC+kb{D6WZFtMct130WK8XP9a0Pa0Rj}xka5t2sDUI9rnFvfT{lz39FYicPSLn`txtLhu^hY$DwxhSR0=O)BIq5Kq8;5mP&e1G@}%3iOE4xLNFb*2>bZ!X%Pcy*MpJsaEakepv-|H z7PD;>*m+-OG}ojnP^o|L$esB|x!eccG2zgQn*6gh7+M6GZZs2NcG1~c=ku>}ngw|7 z>0ozx_@~_XGLa6C?XCY*xn;U{FIm>&bjA2=XuWQ$X8nD!jOv0@40{VbFghKGclI>iDu7MZQ1VbAb;`Kicy(TOu>-593)jTP(CAn&ch`Qqu!^28j z%Mt0cSxsuQ%Q9)K%qzUQ$)^EUy=}h^pCIIav7TJUMJSb|>{CXfC4QrMFUqWR`kI?% zB(3Gr=*Rm?`k0EFFYyrxjIwn*kMec1pCA1cO5QN?vuHiap3|%1c#&?z5Yn1X=<2#+ zIe8{+Esc1B*5HHM9L!>j^q`9gl^-AB*}fS&1%_!KBT#>A5RR)zur8}P`6f$!1B)_R zft6fO^ys9t8&u2T1XhM4H^ZWP9=E%XeGVKLQDuSQZ&9~Q>w52=Z$4Eo)7t>A;wjjw z)xjv9>cEu4O#e)MNvAz2WxV|Q;jFGmWtd;=EG~4zM}{&bRKJtw+QN!@MV|&JJ$khz z9&PuG1sk%kGwW{m6zEm_4wc^f@zM-%#9jQq`1t02+cy7j;2B1HIJKL`>d}_>Jf#cY zfKuNia3J@U+Ol2pF`=U;rQj3&Fk|}o4lJ;2&5RY*)i;SJEcNKj*MqRgC#;TV=a3Cu zts}=NHN1lXB7a}oTk2$T9bC5{#&Lw4&=uUR2T!~)F*1^+3Ipva2o_Pj}$6@Daq>X8{S9mydx3_KEcYdqvel#;^{99J8_gdpgdx6MuL7&|~rM0}VeE zPd{T;q?2_ViN%{k+$GZN1>pyDFF+sk!``z|KI;-2jt=ITQnQ~{cqspB+Y#FKR9t4W zHok+wKM#q+Ntwe(Nb%rnNDHi{BYy)z*6Y{!E@epd_+0Q(zNB?13f%Sn?jf2ligcj& zvP*@5jWVaAsHO=cWWkcj2-R-6Lzi4V1$>oZ!;N+oS_<}e8o=Yg$LDpX?iZ!k; z!ma2N14YCXR=LifqZ}4u9fbH9b|4O~4k*GM{E!jrhF@P4FaSiTwyr-`ZB6xbpd{Z( zeDYTAA9-Aw2F|#6gqd;XBacH(0?*=AK6kNx-_lLD;dB*AlT?PY`0%Vv(;kpt1Ca(~jNsz4Cs5=8y7-^@uz=ROgEnNoYB{ zDs(e{Q4AWfcL6Bh2vJG;x3}(6KZ-pdT>2tLPrOQ@r4N_h27bR4_$W%P6-LM@!ZI-m zp~{Uehc!PD7W;lhR++i=nsY4cI$Y{_R2kJWjN%_@1;4y0`1{_P1XTAbPP}e{D-c`S zAn#+10_#L@hZxu0OFzAV%v_pbX`OHWtDmGakKqk^4;4P8{=vqep1j1^u5ms><}yd-c+>FSYpklNyZfr! zx8ceK5jZU_i3hYH-rd`S?;h^#?;h^&J2^R=pP##?Bp#)Egzo(W9xP(jw()A%xZw^ zblWE0bDAG1`Sf7=xQ2x+gC)_d&8^=7+5CqnE#|{XA@Ob5wwC5|QzNl%PnbJz8Qh`^ z8)&>Nu`N{-K5C74nK4;y>HnutH|=I&^UyltRdqQJmUI?SGU%lkfRg+ezlO6=dR&Kw zhY!1&eXm}J)$XDCHd}9#69=rrCIbk?#U&7kPl{T>F96eoO6QH|tKY}vuLK4L22>HI zSL^KV?ot31vP16+v%GEW-ZqVC|BW-!!`J$Yj`k9jcKa0AT^`#~Ag_|HTXU2?q*?}a z=YHmJeZqz43)pg9S#ETB4U``iLDkuQ`TjVWU!vi0;?|UD?)in?@4JWXkN5U>VF<)j zK|v$S#ku1zzYs6rYLvs2`FZ)rw~|U1=DX=i#r$pCWfrt0g4r@I`Lv>l8S!S#oR=ds zW2h=7|1zNO9MUyyXsElh79OJ2zv^N|__|PRS}X%{bF_!3CHnN|#;z{n%b&i>vkTw} z7jp{&q-8=0k?*DMKLKpCm;Y87Z0B~~^PHvY^x1JGL=f^A?JxJv&L%G2nlJizI>XN> zz!dy@G*+tP5bAZ(;E;&C6(OLrqt)cp6e%I0^lhW6<&!X=6%?y4EucLU)ysWUGnX9_ z{+5V{3JtCP*&SZ)l=x}q3f0b&_68!h zsG*>acZ{eXKGp+2e6$A2zY+Lo?UDccDbz)u)KHrP>iALdPbP=@sQ=&+P-Qk0Z`1qQ)dnBdrQ9g&0^)XbqHLhH>Z4xwJYqET!5{~+sPc|qO3@hC*gIWye&l5Lg zu#S3c@rPB}jJuPpA;PjR!#Y%D*F56Z)RbM5yWfx|J-h<+x_CX*RBTOqeP3h+^(x|W%X9K- z+H4FnKJQLcRBOp+74EZS&ws{Xja){+EsGB`!3k+?>^$DKb!<~+GSirIR4R$tvpY7t z8v7R#0g$r%6Fu<}*Mi)iEFiC}8jp|*>ha#%Hoi&~?%wP9A&{4n$sQ@xD)P+!6mynf z+>yhS1lykqp_5uY4>hYzFDm?~^xbo*j8FA%Som4Y+hn57aY zJI11+tCT1@OD}3RJd(C}ddDRI8hIeBk=>{#Ydf*Kv_I^ME@;coJa17tLuq_7SZqZ` z^^|)ewXKO&m_@+-=YZK4`!>~T$f${+vaGOIk8N|yLBovgNr&?+L?@@x)-iwWa?qJk zKHBqEb;gppK1YxrtvwA@&HnF&olceA=`fUL6p<(XMBUh3&b{?}^$xIXIBxOmu~h;( zEJLH1^r==_u^WZwDnUar65b`D6QqQ&$-Z6e9*jDx>&?JJwjmNso#k54-J`+#BEOf> zca!DPJjOj{BLq1-M&ki=AuoirTWp&T+UrKZaq0s7SBBUjA$6??g^6g+O-uwQo~MzZ z%GE7{)^fp(Q(NO!BvZ|P(U=ohGsb9$VohsRfMK zS zSLwdGGfOpy>$N2v)AEH;W~r$b#Bdh2Z`+VH&Fc{#m8S8%Tpc>I3@)Bzk$(85p~U<= zd{TF4vV4DY7=GXe=`Yc66&;dTX~LH?>xbw6u-qqfF+-dT+De zoU>qnv@|m3>@O&dxxxUT{*d-*J77eKO$C(RTP1=t)KeLtICK+QHFAi!8->jWL)&|@ zh@O->uq&erItQaCzjFi6T;7D|L1$%?YK-mX#Q{jxu$Ggl#CG~(dGRggwc?~&tKlpn ztBRqyLT|GrvzROfVy)P*RC#B#exly{yCNwbc^i|jFOv}qL9iK))13|RSoOzvsyJCs zglDu7oxDnkZ?>JCQHBOCnwB%i)t}|#l?L~OwX^0766lARC@ts!vtIuy`O_n` zB`3$}8uYlf1WzM&bnb{MiGf;yH*yC3k>{$|j5Gg@1#I`c8nrEheG%``FNXB2X5#RNLCpp^lL(Bq z$`~0n=*{yN==43f(O@CFba4WN0~H=X*Xz$!O+&7D@0mBfVI$miGX=wwSZ&qejW~O zNDyM_7qx#df(^tkpp#*|pK|e>(A~bIBQt{7O~dW?p9M0{65G$#b=f1r1O3`bR=HK6thFt<8<~zsuk z@Un$#u+j*MB~i; zeA@0>xvfN}!G}N+r)a-tP;{ZfPI5$8t}1geQXNZY6wo6M8Nxo_-7ppgEBaAi z#?^FLXD#aS;aMT@ZA^TN+6 zgVv?W+@~4QA<^}L`Q;&1%GSt8;E+GxE z!y77^6k#cNLFizdDz{~`5?QTD_Vc8i!uz!BfFYnkbjBaY9&2k|fNVE^LMw7+Zoi*2X6lIX`fRjmFxv27au%CU{5q*b z;>Oh7ao)V|>Hv@e5y_*hPK@7_U1qMtI>=G<{HMiYVr&ZU=<2?5=+ui(`$s-eFDq(v z^|uHt)pA-yaZQmeMtu>pf~Wo2H>P7r^5i&HALnFf9v8eq)C!lIv^elT!dN&SZh8Q< zSrQ6qk}0))NbQU7@B*A-)3xZIYF|QM>B+r$7kM`C=W60pbXQ$k)Hx{vFt0~_K|tg{ zgI!iuwcnqw<(d~;TuLh-q~c79kJ&tlg=g5$P7MXuDDpB$Rgf){P1?_Z*|=^3~0 zy>RQp6c-lF#vkUvt;4^R^7+CVU9l;hdhls^Em`Vl_<+(aY4cdNT4;r^nQg6n-D7)x z;xgOT&cyg7_sswY_f3Xk?u3X9X2tMz;99lzyTf2*fC<`bKOI&&Cd+h-jC62 z`b2=#jo!(2wQ7ROP_mt%`J2F>M32RPa||&VU`1GeVmLKRJ1@kC&7w z7*kLk!spyEo`4ZC&{LB-Wn!fiD$TxSOd1B zt|TR1dgdxCHBAJy>M<8KsHE#94$;xbVAwH8;3}PK^1SP`E$!W%s+OHp%Hn__)jT;g zpM*u4WKvi?{$9|f;<}M~lK!b!cOlj^*307VVUzLOZ&3Yh7vquKrdxYUAs)0C&hxa}%&5F(9Bm>=IW>1B-3jfimzHHHF6~Z)xFijHqWR!9YPM3@NhX~) zf&s(7%U+7D2kW{TpVOtdM6sr%r)8){W+bFEUp=@qHK!HGtmG`KY}&99?KmY~0K-ZO zc?~jTwuI5p^cqmHFb(0Tx~`(Oe6MsstvRLRm{L8Dr-MEVcpaDx%xcpLw*L`}FNk9na?fQOzw<@0s z8Tgsw{;nt7o5aIaYdL^A1A79Kb5>1Ml!Q1Zu8Q2v{xn=(^gb3+_h_+hzfNYbSh#Pk z`fR@V^04?B$zf_Ox$4Y$^gy4h+Zc=-qHXoTuv1+#zkxhjRn)lAIC>fh*vo&D)fql@ zk}^W-?C~a(&b0izJ%Z=3NM(7-?8+JaV!b3UWwz}^KeIR~t-z~(v&XqOz_Mw{w0sFt zZ4hb~?k-@db-q+=TzeoYvwh{UBCk?aT-dGN zWFPqHG4&k_?g5?bnaESIY@ejn5=a~Vs3tya-R6u}R8_aNsPggxWK;MkhuD`=as

z#v7~I(Ar>TT*k*CEAN)(lydIiC~ht_Z!3_bS<+%0Zqt@D!FlhXF~n2OucJ`Y+MriC zpUO4Xj$4q^p`)qj`MYF8FAOsF?PaGz1EPva*hbB(rI43p(TM0!(3~rNi&wj9vvDwb znlxB7Ic!J5u&6`ti-BLn;=WnUBAL_Taj<7zTwUdEp!;LBY*YMW+XOGI zwU+iWGg^D}!8HOTR(1R-=Ss4&o{<_trouWZ>X#0-`3^?q=Qe)l+;0H{hn1TseG9;% z(%x|~`N+K9S&_KRyb^U8Cm$mBJUUCl+&Jt>o;_gk6oqbrm%Yo}4@XEI{|HJ*Y+h&a zrj6JBOS+pk%Bq04ogKOq79JD+C2$S1-@8c5P$EQQ(cwf^=jgIK1ipIPiKb7~EBteR zfEV;%KqbnMJQXOhwY7G#XirZ+58<)&R(I_py(zRHorE8>=cmL8vf95OZ89KH4T9ZL zpXO4g_I)kds~7<+=fi(X+UX4D*nClDTpGYI^g`uNYTW`!Pwz;H{%7lsCfSs9-TW5H z^3y5Ln-2$@8P05OMcFn}#VkMzdKDYT=@|mwO4}GS`lS|#np1IwG=5vhKb!mlwXd!Q zI=fM9{Ovt6U?-DS_*(W;JE9e?rWc&i`=(~H#5L?SKGkq%Gz zeN}G$t4$~10iRj>JVtZIT0#fjJVyQJhX9_1dLO9cf5Y_4`sn*lEQI>> z6caU7sN*HU|1&`Nf4HF^3iH%AVfCWn@C*W35M)G)1KjlnrH5$iL2$dsc#Ztp_(s=s z^C~e1?GJ#aPvBo(f^fEz{D{NOg^+N zz@Pb|fM)`&N^_sFd#<7raab?mV>IjH-(yG?FNx9iQ)D2bt3FYOvjgzbdAA>9qWwMG zn%uz%q0jb?@j1g}m13QZkc4}?{B0(0&RzrHWSQ3}#IQMIZ{RZf{+{w7T8zxbino}z z;QueZhmU-W9xDvLDy@Bt#+8o?eD8lW0yLF@Gp8PdBB)_L`+-6`@xnfY4%Jv3 z&5L?KNhi;WnS@4aZ;@|2E;2!f^)BqUmtP0omjcE|#-vmQU`75L2g*~+Ze-6`JN1^x zHt=VYKUwPNgN8etbEp>e+l+&!vFO{;Vo0YHh6)manF|$0B|KCI7_?_ zGK$5M{L;%hQR%aCyQv4GXMZE3Rb(U1h9+YGP)qQ@?||X{E0s(W4%+pY#Y2K=M6T<3 zwB=_QsgO66RLl#)>`XtYdLrk{)}%WeBW~jZ zCNcX-Mqf+3H{{7F%RfpetOf2*uKZ5y=tJr{dub^^b!`nUb=;QU@nu|#-oO`&#&;NWA3bE9C-vA?EtyJvM_mABEa^izA zP&Q2kE@Soou<=FEe3xn@ty7v$Vsb`WZcb{N`XG}6a6Cm33Pg)$&Xxqd(duZCuhJN3p8ChI+$*j~Ju3 zGa~`6a{v*nymQ`bYc6f_YoBA^IG{(KS4)h{rjmoNM`2YG;lM!KiBQ?g#HStxO=mkJ z)>Z3xC#CKK_96W>k53|_Jj|#C@6uHd4mv4mzBceo@|eV=V(I@qE#6?+>OX%0e;C6t z;G#EWk7+-7I~I2B7lJ+gpM-)<-+$d!q-mGAZ6haSV~YR82EDMYkWKobG}P;BV5<}x zP{WB|#O&PxVanIu2cThw;ke}pPD*d9vg0p)kO-M~@zb_TzOCz4e?T`S*%BJ?g9^1Y zeZ~8geNnmZ)^5h?5T0mi*x}U>d9Yt^^8r_L zowAAbT*aj2pT3u-qO#X1noi2W-;=Z}3U4FI6V*i;>~C}!^k)ILfE{o`Ms2Is(yYMo znmw?k$$^X8ZCOpI{~CVdyhPdBhvK?7b~gNq9k9|A72^~vb2t5D6Zf<{19-Fhi$+s< zrn&Qg%Mf2K^D+?8$r*F}gnALAZqU zCNxa|QvB*^N_yMsW7coFtXkU=7{G6l2_rhqKn;8g)=<9VNB9M@G1H?>UQ=M#A2F=%|UN)p@J4Q@pDX zAT<7T_0L*juQ_%yg-08H++6Y2p;-E1mwf1@a9wL7b<{m!x=G0V3f}EdW&nTdG{h3Z zu`{=yo!1-emO{@+y=i@K%)FPvN9lZbbAzX=hSbO_(HdrE^txeMQ7T#enO%RmH}9At zy@{C{1G&V2m^PUpytUmrE-sx2O{ES~hVzN5)0fUdZhy6mbT-$h(T8i%yLr8(FmEij z3&R+)?7ShVQiX-g&MZX4K8sUMD?AFjYo0av5+7sK?@@APRn2C52U`I|xBjW6?knzW znk-538TvZ+5bdE;Oda*MMy=NS-cGW%z+(^bqZA)^m^yO9VKGzePG#Fz7z0S>H z@Q3t;7Nk9!MDmQaH{DqK;0J>Pa`^2kUWCx8Pg(|7vt%y*tqW*XOXd2)vibbTyY+g3 z)LrOzO-(N2&9M_0VO9V3b~>f}u4q5jMR9{@YJp5sl|)@bfT_GD_>`G$SdNgOKi`B9 z7BtJsqm-bz?+N54W)2Jxo6O4lZu%qj>1vk?aK_Hv{7vxuxk9+ksn3MSWj;^KPC~O% zBQngY*-O=sP1w4QJp*Yc1y@@6tO*&za7BNkjz@l|5ING9W zq-1^YiEbehQrDj5waDbIt9IUf7n1H`u^hqzzx`_%QXpH)hTJ4eKLa5h#^hV8*ECnM z1f+SL<}kSLEJ2_7rD-zE8CUXw|5X-Ad9nojsbRHIku zj>%OI99yU^ezul1LD&#|>N>BDWO!iRTub+)2n5BQbW7yBd0@Z#8n6C`m_<0sM{z}! z{bPNVm`;#=dFI2#6m^FE{2)oVGKyutlkA5;Ewl4Tx>T!c#LEbzi$sHlTA}H%yNTQ| zvsUA{;2!{9w%W6a?wX!CKON>|Fvce^iBpqj0Of^u!4E%v6WxQF-UFHz@Ewzq#9VNE^g`Xu@_vgk>2?7J!RxISO>OH8OYE#gp}^a{J*tZ*vu#$ZcvV+( zV)^BwhER7(cGTY=74CMN?3C{k)@dE65z% zdl#3IW-(lGc=Hqsds6W1K+!cU3V7W?C`VuBG2s4Z)TzB`g;8O&zW3&5SUUUNl_nS} zwV|K$(w}reN~^|(;qRf>s!;o-{0C{1XmVKOzQc((IUXWBo;fmoe6`!gwvgBH_V^P*1Bu=GNO#;xxY-z&$7}JtjERj=dHq~)h@Fu z>ZY~)U+TWu<*mk-wNar7Fvb-u7q9g3`9bI{pT}oA4p6wADmeYx{K&+q{UU72tzn@R zp-x#ET<&F6f3&l3P+(DnqpI?@u!IjDYuTH7%Oliy?47m7Yw2g4B2v+H< z8cp9MQzqH$jANtyeSu<#0KeOz0M}Fgl_`Ix`Qnr|iY2Td*dl4#hAp1DJNb16IVRsV z65+csLOqt~$MDk~@oc>{pu+TLosW?WvHh$Ia~k!2)dAAcw)xh0D!6kW!nN)ys#h>Y z+^IBJ())}M!5@!L0MqDxa${q4}MABaox<+s<>HgfVz@mM@^=-ZE_ z@5Q!%g_K!l>*y40><849nwQL1l>L4OPLc|=k?N(ECz$H%m9H19uMEZdDQolrXuABZ z!s9DH#g?l!O%QeGbe(53Hj9qEz#K)YesXWLm>W>vc*6KZR0o$Ie9a&8Z6G~vhd;P%-ocmLyhFRXxG#DtSGN^qnB9eeXimO;xgH66+3SRowZi)Hva-prrXP0ZOtWA!vX31SYNsX4H z=r+9V_|sP-1p`D1(0f(cb^ms@UTTw^QF;f#IJ3c#v*AO_njy?Y^wWow?ph73DjDfk zCpC}2^|J%b?_hzP{LgC*Fb42(U6ai#rF5+cp}Z5ZX`8Nul4%83!(215WMF2sG>N(W zHF8p{L;mzrHFd`{cXP5AAZ-rYu}$8qb*}P$_*U-Jp1MW_HGOLJgv6|TLvj+A@st7F zHA|*wj57rrdR|Vp)mhj!&jA{r@_@Z$o*d;UW>(pJQ!wN-i9z;sFsSB=*2=0_(K%g6J`WD>!##In&YgM@6SJdCs4k_Ta&p!zGZYm#Ly+B~(GXQA z_yr~?4{PDS8m9NxUbDXN@HKB}kEG9c=gVktt6+^YpE~7ul!3l2fUhse)!^{V8_ut~ zLC4-79s5S;x1ehN{90f7jwThb~Py%GK*5>h_S0bSWamyuNqaVxOY$2 z$w}kF@AE|?W_5N@V~EhACvbBCI`=ulXA@${KESwo#FLNHnh$%Rk=#P1ZWK$7*L&&Th-u>b+z@Tw4wtwzGC^}Jpt_}eUU6|h#SZiBU`?OWB8 zu5SP+zuyL|)ny}5HbIY(>k?;N|+xOP)9OYg_ec+?1^4UM0$ zX#Y`*Ub>%kwH&Kc{c3s7UMO_)#Be7bLv59&#;+6mC-n-9%&EY9vkk1fLplMv`#a(s z!Mia~w`|R`A&k^$^_CIs(G+0V_1R*U9JvvYjt{K={FS}j%eEPLvaGZCU{(7zCcWr) z$sIJG5pM5pJa4O;zqv1YvHvj=;(qWk>k?+=O*cp5nQ-yL)7)8Y5IV2=4vHR>rdn}d z`)~lEdT(bcZdh?EgwAgnhaqp>k6ZU~Lik(D<^z4IS!uNa>@Z)63D~b%d4dv8aW)>n z)rGav%vW8Gc=8=CzS~K#LYxcp4AAgPSvMmB2LtY$bFVza|7<~5M<@F|^Eynd1YTZW zYgVy2#NP$0BNF%El5bsf4KHROQQjVsMB|$ctq>J6oz+9N@JgiQA1E?XexY+tC!z*Y zruFMU_`MbK)#Mid6XUcYr2^vO_pg5zdw)fD%QLigigAvJM#6>fVwQI)Ou>=s*i;Iq zEN7p<-33StVoik1%7OPHQ-Fn0m~YUOQ_G~iE;(e}v6^#!;R396IS4NTk@5l&O+9@}l=9)L<5JnXLb$sq81NVZT?ceCse0Oz5qwzQ|~9xU465VLJfjxqQsL!5x-rUscQ`NBr>6@> z=XXioK&z=!?~FDv%FB=wa>u`{%JL8+TPh_h$5}mx!Y90a)I+Pn2d*G_W5WjSG1)Eh zo1u0NIF1may5m{SRhO zP&PW-*%i0$Vbw(6Ig|Wnkn4R~V~gdPs?&yaa8W?(Jzup0r=b z0}OK^+~aG3PUY_Uv8P_jge=L(`3V-k@zJL^&TgheQh^DA#HLIm*Z}=i?mzG!pgg5K zIS%oWZ|M^23pV^yK!M>Jb|wvK6G1=_bPt}I`fO3&gC>5HR@AZNP1mKAY;xsM`lq7XmpNBOnfC-TMD*GQO~4Mz};+f7!P z32ejt)fTe*2mrT%LjqN_`R_&M&v-thBy8=07sBVZq|c?xAldhN?)7S3y2|8qH&7F- zOod~lfM^yiNsQm3t~3#1^}Dd))Ln&XK@@ctnpY^pu^8m930q(jA-4`8d?LHz~TbZ8os+nV$7if9KTh zB{F5g(H32!KjtlFYFaJl)7aNL&f|5HbL;tzd8VUER$~(}mG};*qMW&e;=mLHMt;Yv zLwcwl;{siD)O!xuH8Y^8F&P<`XEPbq;c!c4&_;j%PW7zSa)p9!H8RXuexA%xr5UPM z$~C9iFu};#znG*#K*+-SCsuVq<0%$iv96X{i=6z6cx9gOv3p{768BX5xsuu!DfT78 z54UR4>NQBnw8#OpUf?Av;qr~&uw!U3Bq1_cdyl&nY#U4|eHkfc-(gX0U-LPRE0;77 z(qsg0{VFdsUwz!{{B3foaGhGstUcYQ`7lg zv9l%OzK^NA>8{JnO1(Q$TnE4F`rh=gb(3L7rUy>G0AU|MI(aXqr)+z2ad~%*ef8t| zXkT-CE@Gc;ggMMKckeYF#BnIGpu8zCllZ7MZJLF42~mBwnSNlCy^GV#eKpgKRKIx3 zPtQ#$Tyu#rj}A`>WU6#-sqjRue5_7Dmh?B1Kvlgz>E>lz5BjxPj>u72THNPfz6fG) zTHVfCX4xQF3IordbwVU}xLG*AF5Z>RdrK@;@QWVeIPKquT?H|x7V_61r$wg5OP%KT z2?uekH1kaftr6?$`g>_~D>aGJTbAYg_W5Vi*aBXnNQm9FnECON*i^8r0F2^R=ZvS#_oE=b?NlPRPw|;>!SjM8aLOS0w1m_|ke2 z%{YK@7T@&LLS$wyOnc^`5cstN3pf+whx>qO_`M0}b0)#xNt*HWWeBJXvsth?2YdrjmhkNeFjy5&{BO{uYNz1#wBDKzB7PR zrxb{7tZvZ1sN|7rw1vZ?`QSP|*#5r(3ttiyf zo|wt7s=d>bHqPmD}izdOEZTHYHeII`5J`;;G*kZgt}v%1Fx`Y99QRf#0V<7gUkc_) zu-z1cAh?tRRyHuR$iNW3$Y;QALVV7|W!!u*A4KPD?A3wLZVocM)8u-A``A?A3|2Qx zCNNVq-RuN_@wsFAdBWA(!PJ{&?o!@1TfMVIX>O3}C^kl}6oE601f1U4D*2cFZD+cvm9=0WMk=6^SSS!>QT81&ui3W939 zH7dR3Whp^AsTq(zmb6S0OY7)6eUrqP(786}Qm9+Q#t0>M*IIUVE%-p5de2MA;M~#) zvBPb%3Agqr?oCo}xjP_qhFI&K=HO~RJxEnIT+N@3b8uFj**WEb&)x4bo31xmNxnga zcBsnOXqiEraWnI&;S#3pbh!3Bmcz3dZtlxCmkoGwQuD7QcgjO}O2^9blRl^Res4T2 zO*<6RxHuG;Z-nE&h7r*Z9n4Tp$2-a~Ph0aT&~g9dj{KFt8*_{OW*TnkULQ=uV^C20 z?UMGUgNanv8KbU?`vM0}xVhu(<$aev)!D2(Q@PpJ&!T1d+uEU@gWgows_X@$#q%auvXA))l0KU-|=VTAB8FX{*^0*pQ{Q3ID(CN8=q>=XY(|92u#VnswEsH z+HF0?ddXcfViWf1mTQppDZwY9_~)%POW%&;x2j_ywc;7-O^%+nhgnwB&>=-Dj5^Dk zu%kxHyu~`>_da$CW|$+g@%RSpCL~f=PhBYAAxny^&adZsFuW%rvWpQN94 z{D;x`@QQu`TSP7iHs@kqGNv7S1JKw|5n2nco_NPrx7c^h46PHkmIR1DUS>@QXU>)IrS) zg$}TD&fGN?JRoc^dN%~rycuMg`+-0XGqxf_+Vjcvz9NeBp=Q;3^M?{uyV z;0stYdYm-j4d5-E6eBpE^B9y=XT|%AWtqpS!SrGT@iuAh&817``cF`$8ky9!Ftm1W zbM3Yf@v^>Dh(nl%qt{C;^s=jS(x58IQO$!io}ej0b+I9I|6@!Ca;L;e9+F_sxsX;e zVf55Wldpse+7)fTaKI#%Vf`m30-zX6p;S<4Hd|8RiP(o{*~EV8kk4Zm3uMu9j(h-- z(pVAF5n;0Z!rWB7GqYJ6^u^bY4w`7sf0wiLNG@Ol+jn+OKaVdw<_y+jo7&)YhIB(z zs;@I_>bAVk$of21R3AX$Jao9chdtvHM~z`wCn3-WvvxUO!H!mrD&^Jw-lV?mUU8pb ztT|y`0O~}*2u&^^zh|dfc4NIP5yacWhg|s@0C2f4zW4mJ`fG_}{;z2hh5GWfSXNXS+(Ge>6FqhDv}x$JQNnBR zve^ZT@X>KQmx|>LMJ3qPysNkVIgfRHMo|eZ6CIzLAsn;LN>DP>^YJo&HKX7d#mas+ zge=VMQL%kFvEUp2Tui6(Qs&$+YT=TP;J`(z2WT=XC=SRcX&px{mBWGkxa)j{V(~q8 zz15{4pi}aHcmdALA#6nE--j11ouUM3+Bbj zo(*D(`cRR>xnfP7W36|C%&gXtePkr^d46CL19b|jYhCUyI{BJW#+wt+Hi}fvk9d%d zPlh%jJQ~E?%tuV4Qq|d3%l@5luwY8R>!j|wwn- z##Qb#`bn$p-CJowU$N|&gdq&LH=J!|RQGSc_sFGnq^f?aaq9A~cTQ>GN{JxyvD9?_ z5l8+rE8A3cTk25`g>v1z-@z9})D(MZR>j$$HJkM-l6` z^4WA)m+KZ9ar0)TC_1fqs3aA-Vf}~9Xpw)f2(;@K=Gr;&TPGnC@VZm$7o;=}zv?1D(jE;`3o>vVp3|`i9Wl2il zYwOy)O1Bm0*l9gJ+@pz4yq*-hP1npM`(~59Oo>;`GRMyX>~NJPhY1%5P4)N zl(Xa$V7_`g(GE$YwLH@`;`B3t*i zHkK9ir%bjsO!tcG?~F2P!GVH@ytUx5)mv>)J=|;}A(#$GpRr{dma&wm=(>ejj?UMq0Dgag1oH;|yC@fD zWzGr`T+&|8@ z#&8UVELO7SnrqJISDxn?iuT^uj zV?JgiXg5`d>!q_Nla^wd+*=?$(ASRnATpIJJ?2JVQ3!+VEwlZAj~^Vn-dzrnu->gJ zQ9WH&mXwjmqq~G+Ss!`LIT_8^zAgT4V4<_ys5sXI%FrP^ruJ&X$5CA=_Booe##Y>W zpaLgbVw+=+4};|#w!N;@&ugYBY-Nsz$74!kkKfcgckC9}jMIuSa*-Mv@?uZ`Py8Th z+L1a35*xA*LF0kW{aG-^iGUfZ-w$D2Fzk1-PZyqQgyfX!z%(tya{gVe5*xv(8 zXmk+S85%J)qE$d$sIxXf1yvIf2z}GiUFO+Zq z`)sv8fgW&1(ug+2g?evorz!9#PPQm&Coldbv5N;KhLsnt5yhhJh>ZYn$BT^w)YJ9 zTwa1j#7~k?n(FNMa~r*hp;NNX8P?hlzrs>{w-8Pe>jMVGBG!P7IHXeOF{pAn0M%q* zV@TH^6xu>3z8*tG$8+>LMt2b|$PZH(h^5`|)MNbztr5xccIvbea3coQoACuBA$@xy zSC3Ygf--B#(Jh{(l_x9Ur4sKvV|C!2 zyp4WrQ=MYP<5iDcB75E1!4VnPpl9M#R3KxiaKK_LRLWmHCR2UOr%dp0D6#iCm=M$J zXZ_ZpyUTsn@A#4TdKhg&ChKb2RPxP93)Jv$z9S}(zIl8DM%60=wvx}d*xb>)yY_o; z`mcN=4-;8*W_hcNYo=W>;w*MmGow}c@i zaxdb1QV+cRaB_UB7>*sKtgAU_p%ZDSI+$Q}JM(ExJO~Aw{P67R&yKGvAdl*50_A$? zE_HO0ewit=`|?1$9tVnkN7SzEZkpZU@78Fq+stuHUq`my?UBO_be!I%373g}V;Qel z6BVYE=>OtlqhK2m(b3b*kBQR3=~lC^YpBA%*NKgNDIMC3ZhpyaJaCqdubs{HuOJ7bv7P7sgU&wk4E~~fQ^N+DdfaU4_4i?p4BSn64ANvD zZHix}+6`J>fAenme4XV$|}I!RlF{O*B;qm=VzHRzh54sVCzlY;(iZOasxdH2j-D z3Ww54_(5LD<0|i>Ca2yRokUyHHOajE<*RD5(scJq0j1kGK|-K*{o-d|>gsay--rR` z^9L(+km4S#1K-3Tsm;Ezu`umxSM%?AVQ_1I1qX-L-@Tq8Yxyp_ZuB+RINr%|-5}Gm z1;J!bXIa{mm+!=VU6~bSY4I&Wc%3r>qB`74FF=vs1O(j$heDKwrb4HU=+({vbhsgQ zdA*<`=8+*52;uByYgrWYD+{xu-1| z8a^n^TR5h%ju9Dge6zT~6l*ZK+ZT1>xwQ7kX?o(4B)w5oJJW8G=4Gt$A*6V+W5gye zC|fdL*mW|vgGt!iXhdtFZDI>GBA9k$en>Nx%EU5SePuGIM{hgRm2q#-K{rT$%ENMx zcXFicrM;wv*6cbu%qG`&4`)2^;qSE{3S-9_%wDbFEv*XUm+Ap!QzRc+>yaeh>qADY zKiqn^nJybAi=V~_(zS`{)LF?KiwBdl413(R-Z=WeF_b8JTyb<4I?}pfpJ#U)^aKw+ z$a|0uzIECQ4}D@8<#)@Q8>^ zjtJZ1c$XCs`+f3@z#e_y!ig4K&g2f^RUIDy3t9qp|D|)_AG`0_e|9CXle7GyAzbAm z)A6gj|MWPoTTmN{CbYr`4tMb3+5QSfW(;2wnO*+=3s^D;cII`bkApM))` z>dXcoa8B=#X@4W`%arVQ3hWgi1fC_$zrOvqO~etjf%CBCyZww?AehRHDka_h^db(xjQev5GP`q`WT;7`M;)Z8iTyZ0G8-wRGkr zDZLS@#_RT@L|2$j5dH)u85#|cv_}9FM zC)8k)7tqYi(%`lni$N2crSIP#N?3gVyYr;sq;NeoGP1C+kQs~s5s}%MZdH*eAq>Hn zeOg+wYT0jVQ`Dk^f4Cyb&7ECTgv`ywEflz&ot#N1pW?vS-2AQ7vMXH{vpJofS1^Hj zyD^8yL&mesTx#xV<)Bw+qhn}YYnH;pF(gf&K(!r5f^Gut`QurE6e}<2(W>zzCJ z!^7kx`^mbI)}0bEkO(f5!ekgRl%%N<4Z&x>I7sy@Efg7dkdXYvH!qa}or3DRG&(Kk zw>W9tOoT&WyU~Z3Ms4E(X4%)bGXeB*-OZ?Ex5Ot`{Qw4>^w00Rb zFq`k6T|XrOw3j{Z^SAifwKBwXyL;Yg3y0gqCPGMv3rf28@qGi6Mv+8vZPC%#(~8}( z89wQvCxX^yvp8P$RLlxvN5`e5zN93kiHW5uN|01~HhyBx7CE4O-FV#qvlspvKZWyJ z4XeNdr{zy?UvkXa61--%>cpJ`MLJoa*k3;muiwzf>8t!EYIHh;RKxD=g`qp) zf;J~dp1*!6Nx%yW>L*a+eXCaGBg<(#KG9l!Ef6PX78-rw(_;r*xy=wu2kZ?V3*pMk z%+1WnsjQ#yT*!~Lpz3lp(RCa68mk!{v(%i%l$)W9VC`Bs*z4?2QG6fu-oy%bxT-x^ zQ^R?||4nwU(dol5agE-u0XE}9qmCi&9R(rADa z*^Fq-z9QbgZyPBUBq1e*tVcvfMuvrDN6`x+m^pT znZsAyTSgqP@{Esh;TWY9@GkY(G*mfeCRRxncX$iMGyk$(>$Zt56$c{F3?HQ^0-_zplY`!n}L;^Tht~J`Vd)FWVmCSq>UMJ;U~(;=}DKZt*JTP&pD-; z@>=en+u!$)*nHjcll=PlPJ}rX`gc+iZ+{7#l&YL$` zA2{dECA7}IQooPhND(h>#@dt{iTVC`WHad*#naK5Qtz?#gHxMv$&1F1I^&uNUi0lJ zAMB(~q2c3Ws?{($TTmQJ`p7{<8Z)c>wN#+5$k@KdF*5{V?iI*#PgYT(uar;FQY{C` zYHPbz7JYLrf`(>BP@s?37{X^cQR{*U&@LQ>v>Me}-G)BE`VY;7406d!eT zG>w&RITDs$U+*kb5I;7WLX3f+P%o75ffc)y0+ecS3GW>XMy4v)ya*DLno%sr@!;gd zgsMW{Z}Sf3^|iHZ|BM#YsN9N~|7FUWq-6b#T3$|ZT^1HPI%@idF?^RBvAL%0q$PBy zG8NLun+033pIWfW+1c4Lxc*BLjms`G3H0ck+eA$lrS?_@8-DN51%9gS<$BT!gZlo> z+^LzPcF--Tj(WaK-Y9B6i62vVC1Hr&3r%wsJ&8halv?4ld7%aeQM+!7(Jgz@n~k64 z>=mf?Ly=leH$~07MnB#Eh3=!k#TO;1IAfHBe>NQ9S0or((mWULgJqZYud_+f{PQP` zyARWRd9BNCMMY$PKS-pxO=2=N5s;br8OoYjACV&dibJk}#|KP(X<7v|y?1v?Ij9d? zmQrqVA~n@qR)+h7s-M1fvT6q~MX z9OmHX2Q_q!m6cZ-8%-1xawdEuEMCmW(=wx(pM$1?hIM|EOqcnoz}#HaTGb{%AYe+^xM=;& zgZl1rpGLpPtd1v{PPie`jDa(yrsS(X!$xG4bM@qXy~^~(P`HZ-CTpOIJkH5dskby* z=uAY<%*2!UwNVmB?a|jxXi46UFrn& zGmV0RqW7cWdXk`P`jLtLn1|sG#p|baBsvnosaqKeZ9rFj$)1LDY z?ahKuurIGl8({7y&#^&2RHEy8bk(VTmT(=nRQuLLo;e8+=IGUjgea;y>`PW{PJDRM zu%_C}mv7#qMMaG0!WuDxIX%cBAsyw}tUjT``-@me@jdZS;Y2z{wClsyMBm&RJST^G>h1Nx?kTTrGQB&9Ce?My zcVFzeyc0-B_Pn2>+K%^UqtslDo5=XQ7Q4v|jV6>~!WJsY{LsLnY`$r4f)9jU*=JX+*WsAS9LKHAT{CloPn3?vkKH@WkqTK{FFQspaC+h-GnVpkY+ zx#I+@tEo^g587Cj!9?Dghd?CfrV(r$6Yj=?kEyurY~1DfsB?2QLu%<4yOJxH{4%Ru z9UMM11c|R0mDcK~Po1o*YZnTsBwM66smmU}QLv=rb_+pU@tpQ!ie_eJuXm;?2=WyW z!we2W%6IQXtdv^NLmc;Od!RX)nE?xy#lQ@LHm3F~cKH3dpa^DyI<_eFl#zbbKQb_2 z#WIuM%W*392+b=Uc9oIwF&a?U6hAS7i>4D39bItA!{II7%On{=$c<2dv~FmhO}lr& zmBq_7o;vX~Xf`EH8@Ju#5VEbZPtVAaLXmo0mXOyX*dQTcNsU7F?C+9FMLD=d{mRj$ zRb#j$ugLc5I_6+ivFmux5|y$VR<)Q<_t_Pd=xoVR-SOk}XlUzjLqEMCeQ446RDzkY zsXG<^kanXzHtLnu$*jYG<<4SHCRNRXCV~wuY;WykfJCjj%(N@r2nMS|!zu1iu4r(` zID@K15l{Yzr6H$p1VUZ4y^)4b9lUv{akI*|{+DQ?OG5M{OTx+DDPpg4a}WNew0x6A zyO;gr_?Q^PHmxjzYVIg-tIoL)`Zu2yPep1-TUdSsoame^1?2P?+-fQ|IWdSE(6zD6 zJWa|&Kegm;%gX+`L$?efWk_^PjFxnHN#Xi-V`HQAUV;_k>@IPfW@eKR>356r*^A!s|1?uABaayH0#UeUf00O!E>xVTOX zh7Og|-Ce|M2p9CWP@%AC?_644MMQxR|K!BKp}|-08i;^G!M~`eYxYbkfqnc6HprVp z3WN)oWD%k+J7NZNUV}O*SL;43g<#61IR&GlthbIs3T$ z3_`05!tuYA%>94I-1g@i60&LS^1!5g0`)O?t5U|}1y;03-EX%_kwwV7TCgzX<>kF2 zoZEPY$g6A-YN|;x+;=4;^!Q~21fmC?apk<}XD9y|P7;3pRe*t( z)Fq_EmaJ{uCB`JH8+;kuF*o2mp#vB!NKKe~0_TxnHLLl~H@_Wg^KZ|LJ;{hg)_brM z)IU8rIYBHWgNL(K_x60`N)DB!g7$3r!lzGfl6)aA8!RXD^Dc{~bgK!dh*Mckxf{oA zK+<}vYHG`~2lNhIm6dHaJ<~j-?7LGaBsLd`VYc?}Upe@0Xk0=bvoTe<$DUIF*BOmwoR=#6o4d5PZ1$%n)R9rKq2_3lB3RoR zHH5Y?E=mAXSoC!7f18=TqB*q|o0L@IvGquz7mXHdCrXKntC6x#G)h}y6f}P(0A*uq zg<(;kvuuKZKp(PN09nxGnSN9xXrbc0tk1c0KUOvtstBgL#vk;=&o7~Om`&UjnLJb^ z`6#7her9E54I_xAAkn)B-@;|*)z{yom+8nYz1}f0$96m$CHsuk*u@2#uiIK`S&&4l zATQrXq-H4&`)z{+(jh{iHQ>G&Hzw%$_R#<~rEnATuq@|I?qenhZE*`eM^jNP$K=?&CKPXx(0(vwiNzM3X#Kg#m zZ94yx3&0^dM`j#mh3Aucrx{jMTdaj#i;Qrp#)rRk=`kR5xrXi(?1ExHW>4-t^TMLC zm;WB7RiEoh^Pxb=G-0$1ig4Fi~M|wgcE%Q-oobg`L=J-Ml1C_DG$`Fndwg z!xsQ@?8~-AVPmkHsf@#iQD`|Z<1=$&3WbgKxD7@x=-otYJXKHcge${l_t7~$1F5Ha zDbdVkh)(e`Qv-ePkEzlbX1=H2(-a0vKwuR72W9+$p7cBa1A5YDvFC>PCg*?}0nUrv zd3m1z!U25B3u9PptD;`#%Q^|zi4_0wWX*tlz=Z-InXUcIJ~FFEzMJ1$W;R23)eP&b z^vl|OkXhtB*4-((rugiG&3!WRf;WrOHC%$uyE851MFzH3km_6yG!>_e(YNVTLU>C# z)bI1Up#ENDPLMf&V_A7QS|YWh1s$95F*!1;1L>3fu=u8)ErR3L``R&Ey~{;JdO?)Z3e!Bp@t%N?k+G;^nY4 z-=U3SaC7>6&E!rB?uvwAjslK#stvQejCjX6fx?2Z--as1rp|-7AJZDa?qz@q*pxeX zC=s1nlql{jak&yZoEbZJ%l=7JO?bGAmrx%qr*gcE$NX0~GffyBcb0mP%x2O}ayp({ zAk6T;1Bk?MWLDM%20BP$aXs{6v87G9!b|Vmtk^nW zE^bg|S*WPfk~ZvbyLi`@mgCsDt(7>^Uwp7ooUya=+RcyUS!^aFkGS3kX~rs#AHR^s z^@rL+ZCd(se@)J-v)-mX3_@6iHh7js$W!BuW)gHuBbG6bAwqUS-C4CERr+4Ao#cZ& zZCFekra2zrDO-C!LbRFbYt+e3)$_?p<4p0;suvciI-20ks-yJbq&xA z-5ZESsFi;uzO*OgP`gR=7=VD2-4_Fv3V~aIV#4;5aG!#97-_x$1NX=r9<7dy-m+hG zs(x^yr=&jK>nxAu8ZHweW`YQGWy@kPG&RFb@A5#9qjKx4-o_&zA1=qAL)6B#oS#c8 z%U!{CltKR1X5+?SohgS-adC0kptW0kLEM6rTv&Y{WQl=9_dHztrh|mE{o3k>0^$hd zjNH2{bbe%{8viwKgwm$UY;DQV&!G=uR~tg^a?XlZr>m>HR9DvykjF1AEw!~>WT=K% zP?o6ZEye9#+{VPl4#otal#whs?qj^#KU9p(A-L+qNJe9VeaSU{#%9J_2}P z$CZPb%Fo!#G5#KxzqwFIe}y2j7lV_Mqz%z6Ev+>Msb&F-v7!%XVdP7>_mW~T{_!bU zRNHPzd*UmY-%ICWK4l^Q8z%>zm1*(6&W!0Ts zKT>I0Oc|k%l08{^xfh#gEmRgp?4>kZg__e<>uwq;`<$G@h4q{F#CXLlIH{#du^J7( z73x0PT|TczqOP9yF;Sebwll+2suHSmtDma0>`mFLDN$<$+kHtCL~tA8r;iSHgyKh5 z)l^+H=cJRAD{X@v7wY$t-q$|zXgXASEg_1*Gz-g(SHKml$B!-Q~~0yd+Dlk6>jokv0nN(a0y@})!?#T32(R7nvKGc z_a@CI8;@nDIbVg;+&)L&cc4h-~g)MSzL)o@Nc#UEWz zJ#_J6$P%odu%X>E@2j*cr%u4~7?j~~R9rPvDm@Dh1}xG+Xe%VMPaGgv0})ezlSL6H zBXnCJ>9^rB&O9PKgMiyzA0PP{3A@ky?mMF~A+#ox#ZM3uT8L00apMgQmp3*(1(d(F zo6XG&h6e$poGph$KU-RiYTYt-k6Ny&bH%GU|I$*=Lxf#Vq@|_B57bz4_N^gyFp8viGGTXcD1z38Fs#g8_-_V*R$(uH!?N# z(TGkBcFUlhMl9rMuO*RZW#Huwj3G=>*Qua;=VTz_=wY!Z@Ba>Y(qq1QXpE%7T0@Vo z;h%?>=>*y=K-IHL(RUo?r(|)`1>(EeVscd1rrz|ue&z);@uEok1Nc28Ut}tL+>kc_ zdr4vIY945X=b;R`o6$bSSE8ivom^qY{@hkzk%C%yRC*Zl!FW2B^UEtxu&(SxZkvbf z+^^us=j!Y%4B-$2u0>XF0SW?TtL+>!nZhH|a~HkAnw^)br>pIrn95VV$V@9OjcuXJ z3HBBV)vL2Qu~a}_RSV+((F9df8SI$6S3Axa_b*+`OzvQ*D0+!hkeJ=Qs0x;R9rr!$ zm&tnC;M}Z3sfF2n1SXy5P+i}QsUrkL-@TkYeB$JYodr}K<6Zsc! zb>gsC-~HbT1RVD#!&%Nb{kH2f(?>Vy%bTD4rhL5Ct>kssTUfY`guW!C6cMqr5Nec} z+;MSp+uL>G=YRX?=!*a!_i0IfafZD(am=k6ICd{sL zwXJTPwQN~h`qR7wjL=zOJG6sN^3Qgkk1J-b(N@>xh*dKA}6f7Ey0dYr5bQh z|Mtv65JNwEQiTQY-1&?|B6ANGx{__Bc$58R@tCF&)PP5U_}#mS2xErWjYQEE(993TOWMySt z>{iM#e=Q(UmL+vp95PwgnbUxK0+!=(|Ie90Q4T%QiC|%)rejnlvP%mO#}Z=L0wAJE zNSUu%t`8z%#OJThRd& z0bU&k^_MTNc9aOBbBt?Vs<+Y~@3TOU$e5Nen7a<7pBKGOY}YDVUV9nkW@eTaZ{g!P zda$^NdGo(qAgW!F>vk>%q@Y60ABkDK6b)J1TPWD=MHjt)onSM>^zp?<%^Wrqh0n<& zX{N{WSp^f2yxQ&FfL|_RJRB2iqI0dBJ9$Iqy= zw~%^FvY+plY;mfrEJSaOqq=%e1Y?)WF(&7RQ|wZkz4XC_EfIgCxWngKgY{U9G1WF&N z7Iyt=n&gS7j*=$7_Fo{o2E@I<%yyhdtR{w#Vbg)l^i{qmnB} zn~9|IUws9bm}J@$A#O^9AOr8Hgv0zZVpZhhLJDrhLNDv=+D@KMWpz21y<0@;fgs@e zwEod^XMv|jU&JQv#Z6D}B9M3OVAL{xE+6a|ryYNdi|hn3cq-B7MfN3>2ozuTN7PQ| zP<>xZE9u2x1;i_?OJ8~GN<$EuA6Om!l2+ZxDWPh4jW@!6NRW&_Y26~I+tR1yVt1vd z-n6GY=4{34wEyz-i_BP5mH(d{gIo=&|B-w~ZnbOoY9dgNC#{{g9~LqXK0EziEwA zYCKRCRv!<1l}=yv6J+c!fLS5P*qkH(CS&XA*+pk`rtsTVc$2wY<~4435b3k@y~T9; z=x2@s29JLKh?ZS-x+9*K^Z4<>bb1&GGykzs$v~R8b7XJ?!1w%46z;KBs~P1o4AB_t z-ma%Vd;5y_qlaMwA2@g9c8>wQ@pQrLCo=0Gl;pvA+8`~zkXljU8JVvv2l8s#+M+;G z^bCE+(vA2T%bVL*Dm7Fj3gpTs=xxzBX`l3?TTDvok)0{x@ZhnE3b(D7inp+65P$o{;ZW<*pJ>v}L^W}t;b zO-Ep{`?^AC^B1Lm(ZvC=K+f!!O`qBt1v^=(DzakN+Oet-N?t^hG7T30Fx8DyRZ~M` zizI1>coX~Xjx5e_HFU=*e0yeW{8d@W^Fy2)8f_^gV&opbANZ=q?YQ*=C$NP$j|#n3 zt})=jQ(Im6T7-=L2udRLkcF2nrb1gqxs0Y7fb{;{>rQ4yhU0SoeL$$)$LIb2FX>R!&BI(4ZXkI90wjI_x_4nPd4ex3Hr~Z?X>oaH4y8_G|p`w%;XYmE;GcY%a z0>XQi!mWsz9;qF|u6^b? zOG*?^ftmnv%Lt6k%0o#4c&3zwTB4~I} zrx4#K_1O7hK`F{LQzRp;0*i`zYK=^BNG%eEz>)vR%Sks zj~phx4gkrGKXlut+NbjlwFP}_g9t0PJ55U;U0(D9D0Bd7A!RT88n%_iK3 zo;=}0MKiEF!WwyzQmVARQ=kT}It#gKFwDW^?ZFv6&BLSeL=`$%PKJUuV-2PTf zsUR>-VbVN;&hC3Helb1ngk2C(2c<0y?^H* zH2^mez)3Si|-`ct%y~&jELd_oZtZ_~iLurvI=(@=LpKH0l5d%vE5a4YJY_Z19 zzz&-KS7O;qR}hjW`dINH&Xu6NonPu{{-3A^5YtVkdaGcuy>!MY4C^EJtN{v4V>;Fl zyViPEq`?TGegJJ3Y-TWTZv{=~osrV=21$-{ zfz|(NTR1Z~SWyaPsVS+vXU&XDxHta#%Y?X+Jy?c@dg||Qx`q36ttb$sXaQh^A_7fr&ny;!APMAc&pZbeP$+~F}N*YA`gH6eraRF-Wu@B7K}X|?9)}gcn3EZVIvzgB_#wV zUVh$tp{r)pp$W6f)|4~EA+z6jC*ZcQhzO!Ger0LtEu?y+f81w2)G%fG#zlJ1coDa$I|d*xP~s{d9|nblS9;7mcX=#A=|P2|j4wA(fSpiIhR8tD_CWcmJ9WTJWBonE?^pyK8Ddd5N?vTd2dua-XLDcu2nV;L25$AI`0QVsiI%B-9PIB` zR;B(^k@YR5&|MqhGwGPWJ0etGRixQP=inMLgZygk!t? z!~iugP+)Wm9*uC9aKL>voEX*Z%mX(}Fjfjrdp|Q1fdYJd%zKKfd@jN@E}wQZdvBs+ zXQ9h5wX)Ow8_Jmn5D1Xi1m@wz9vFA3^RMm(S->A@GcrJac>r^Qp`l^gkdNc6yr84I zY5bnmI+)~upd4Z-I=s$fd!CyTy;tLu{T*7h{l^slI5qk3!+mGUf*V7FMx6xO;NX}P z#;nyr3HCI4!s-+vM~+12WcT_rs&c=%nv|puxN32z*K&b~Q*k*20)kvko1YVy87eCvxIoZb?P|MSq)*%fLBVEhzJ9! zpN!k8^6N|X)L!kM<`kHJeFVJ9b_OY3Snf7U8goGr@|j@M*Bq_B2kYDO9nVgm%ZPoc z+xG-inCUHujZKcLczhTP5E2-y%N)=nxZe^^(Wdvl6pnzCYtWTzGVmFF{yj^46sL&+fidcJYwXen_{Hy7wru z06I`-6Kw%WX;w-Sillo>Yc_kM&W}!nSmc(!9F0m^A}85v5vAcDE)8k-N`&-mgT?pU z(0d8o@?`$bl*^J9cw zVD7P}MMjU!4I8W**}D)+He-CUcBE3qtG^zTgQ+oF089dTN%Ix+VRrf_*#MLf@hAo~-zS&fV zukH2MlR3?L9bU%Bd|;G?%W&A=VZ6&l?U&{VJDPs7TybD+C!KOnoG8;q@-_8 z?PmP*q)Uw@vdU?ZF6&1)UY530UeUQB1>e}YjG~4U-#2euHd5h-dmBbevstAWaHFXy zDfxw8Kly&f#a@FOx{KP|eR$WMTI(^}Y8x`cp>|W7$j=AFUUD*5uN{11GLDb&jfDzq zZaP>^)M$7%Q4oJCVc2TQX*hntu)Z=FJ2W()*!665EBkwNXsBzLO2Cm1dN5Ogs%v5$ z=fOPo`5irIDxQ6*U|aX;(;)n92v&CYNUl*P3x68y>AAV7MAtvUUHe82Gu_HZ<|{@G zWzukrY=6C}rVMh{9i|}qAP54rxqzQM51t)pb0^Okku@3>aWpL|-_;cTo_*u4qc%_9 z%v6nQ%rlW@+nwg^dT;k%xaQ`-K+9kLeVeB{X_IMttXVYruU?IO+o}$sqf9MUPT$s@ zouxjwb+82U*;kMCFD~8+y=3#LO1L~0?oa$nQA$K+<;X~|+O0o2pwZraa-`C}GB6R3 z*ZWm@T#9b!Z<^im7@R_Pb3foNuAQ)pngD&XU*o4N;WDIR(6CjaXo$XEVU}CeqcnRj ziDH|?g;}#{9NA{ywY%cFZv7@oCLg1c-QlH*Z#3KzqNC;P?O_nUFGNJ1W%B2UQe}QA zwqH`DwI0E$h?5UUEqbjD#77|ymK&h8%gff*)?X{$LKF*#*sejKtfnyjgJ_vFZTpYA z^^|KdF(^_hk{kr0(st}u+mBOHH&W#le9W_qH^Hf+xNfX*A!(&Y8r1tyz!sYu4wcJPySiTBXUqM|~* zss5_+Vo1jwDCai$m!Ff!?QInC;oh7B%|?Mf+zGlgJxLeEG$?3tj%Yg=IL7A!I+$N1 z{w{kJhAz+Tr;$+>3}0SuUtY#V*{-kFNJihR7w6;CF*16Pp*Q3)R#*;pvEtfV?enSq zxon*BnW&PI={v@l{#=$x-$R`1ym_>LCTh4vIbJ)v;jY!^Oj@Y@&fS7#HSRwV_||UamJ=AK zT;S?jel2!>-rStVF?*QtQqY*B-DH)cBrZjnyMU)s`GJYoIis~4^kNo?zP={6|8tG0 zcn&0V4=!p^U!I5;2o{=BPd!@S+i0M^cy(i>^zr$f*gT{!c8gBqPF`N=;83jS$DY_e zUG<*7^W>n;%kSWlNmBSX9ou=R;=mSex9tLdGAiX9WDF5vg0Mqd_XPbCB$Vc zzX_mgBRid%y-hu8Zu=?|0tt=Ors--H5v*$w%G~Cg~ zJWr>KkO%6^>$=kSYc%TXk8^Xfr>@zDzC)aJW%#_6Fu2SKh5Lp{Kn&!Hk_}!8z(+*q(#Cq zu1WNb0c17%`MhtLrSZ$oW(?zYXXZ+iKE8&#V-lN`l0p#`VLLS`CM=Az@|c^+?qFi7 zyh?P77Z3s;s6Mu3A;0u)@ZMxHv?GcWeKEK>&B2#;-R~&u3A@ZOHtRf5{sjwvaPb?( zL#y1obYIOmc`B|aGtrwLJSQUZTLl#TEtVg@w?aaciiTKSP0A@WPH-NVvQqQuSpNd{ zf`zkB34V>m>8^AQ4ixlwnlgCC;AU4)FS) z^U^{>tcKvs)h}MW80>u>|N8;lX7ENb!}8o+nt=Iv4S6l_nQhDf3T;2Q%CNG!49H)w zV<$5(9w~Q`bE{j84_>*rpS?RaFu={LO*ofq1#DUk5s|_6<8`%;k0XXwM(T29ZZgMr z412I$4^znG&%IPT!BaVA_;L=dOv;((B6Yuf>esI?09nfwg4_Rgo~^yY-6b|Q_GJNY z{QFu(CY?Kuu+!-#g-k*sM+YDo{I2b6{(f)3c6?nqx3YJI$Dwvm&l1s`gD^u4d%U@G zl_6&48@OC!3`myuyM}lt#|i#W%3KBHp@YNW0vJMYG*S~jEiim=`7giN*6){y;+_zJ zf*C|aV&}pA3Yc|Y)&7i1=qT4Z*LdmU;}19ATdXI)gq^!ZsH&(F%70jq(b~!?Q=|ZX zy>(FMbgNH+&HTphi*^$VAtJ620M!z|jl~DQ9(Dim`+b?rHKP10m;myi{CTv>Uqo$x zK7#F(Z@}u_YW8^wlX(wl>T{3INMy(|FVQVp!ZS?C?I|MyC&Ja zWY9Pko@-BnAadLrXYb#8uzjDXjTMRPU30_XE&~bHCmM+Pbt&B9IY9$)gwrHkEoG`Fk8jFuey|6lmZIq$&UfJ zWW|2irE6fYH4Z1L%~nPh@)odVB^}5?yn@z2ye1evO_H4|A|39*SA%&{N!}j)j4Fq+ zEA8JZ@(oKZ_PPsDR{6Dy{^$L2GArEb$JVUtoyQwDh>&L06( z>-ULtE`u+i6%k9>Sn|F1`%q-Vpqv%l-(25T)!b7zq)d$UV@O+bmch?i%}Yd1^G*f0 zgF5iM3g=A(#e~Muf+q8ZlEIiGqozL$JYaHA{%LFro z9R6jeeQTPR?CN2dPf*b0-{&jO`x!jVEZ*7**#z0MLP8P@k*zZ!now2gech3~k}l(u zSA>E0Q$9zuU>5sw;f)Ej#PBQG*F0VXRQ7t9a)^b4gAV4&+y7@TOq&@Ms@v?kZX6eI z<}dS;t-7-2i}m}}ynJ>H2KJzca5i_iY_!4e$hCkMPa-Rq&C1+%OEo!a5ra|sEynU^ z8jJpWs69H8n6CD>sDZH|2bd}n96yUSu4P-1sS>nqW=8w0UoSiDC!aSFmzd4L!0pAq zuWGHb_ODsrzI@iIJeU7S^O#L+4)A`+1klOTC#FqY#T?yx$wkxRV}uc-@h`~(n_e_` z+P;iirGC~&ruLE$Fju930;zlEevUOlkA>fAZkY6SR*I>`#|@d6Ojsvu{^FEp^m$gX zT;!WjhK8Ns{Kaa#;p>EXd{%}Ag=tTclEl_L`QZvo+cWKU-MpZ@;OnOSRlioZa_wId z&k!L5%y-ka6|nh5Hy`og-u>$j&`pkyA|`olKKAC|IiQLYvY?!~$wSuSXHUvU^NTif zMH@X2Gl-?1saIb-^NHtvh6#$GP|JGBlVhK~ayBq^Ueaf<@dSC!=@F03Wnc_|7<~hwpe=J2c&l5(|8me&&OkZiKX7^5 l|Nqy&PT3B00UIc2nHdacTGTj6f3pNR%G1@)Wt~$(695N0*YN-V literal 0 HcmV?d00001 diff --git a/tests/widgets/staking-migration-widget/test-results/smw-05-migrating.png b/tests/widgets/staking-migration-widget/test-results/smw-05-migrating.png new file mode 100644 index 0000000000000000000000000000000000000000..32ddaddcff386af2f18b5629f502b010840f0ea4 GIT binary patch literal 92262 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¥fxtwPrYZwAU$~a%1R$NgpFIe@D;bY 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 z

AyzC~nrJfXrK^rI7=cjjrE^?HlM4?1#B>FbwHsIyi*BhQ^Q!5j@rZ4IySxqf|9 zTX9mhmqogR*jzcq5LT{Gg)ajk4KJ$KQhXE;W5;vTomQrjQd*Ld^n%AxFeak)!OhfW zmgdIKeAn(XHCeF=Vs&<_U;b^Tavn3yF4$vil4QKKH3$)U$_VI4^;DpO#k1eu4!8l4ll{jZXhA& zz7qXXS3?uE=z-I#fbiPWAcj_E%^`OTy^(?KlG^}s?0_uI<^3}T#=|z=c%NoYp_>c{ z!@nL)v=M&_m6rkSNUV$vc3PqzP8cn^y~>!z4UQQ|q;EMO#ju+I%>}lib54HTrJiJ* z<#0F##fdK3W@H?SEzxEqh<2lzvB)eJM3vcXTp1QM6nNCP9O%3&XTP|5=vnYwU96wP z^pD+FNWce4q32LGd9sQmVK*&L7cm>9LAf17&xym3q7f(stz>NtjU&Vi)zhUR-N`h} zBH9{*s78&?y4Uw#u+hK&{A;e2sfp(f&|Yv=Vkr8)z0#4eZPCi=jSULqz%wZ{!DK_e zhux@%$xLNLObs-)`g-04qZABxL~Ag&^Wm_ApwqD72it$yVaM{>54%CgiqXxU;CkV1N_UpI&kjZr+#|fzfwI{QVSZ%5N5HN!gn!yR+oHMFExA&G^hH?40- zm-Aq`ccGwUBADF@_v?nM$dt@N&Z`J6Q595pnrz5rvF@zCvXns5G1g3LrI$)DH#%QB zH5m>*c^inV0Xrn)v4!Y(JV4fr@W6R<;)XuPW$9k^ROgfvAtAP!DFY#XI1Nxs!G6`1 z9Bc`RiYCm&_Nq+AJoe5V_zs$%Aak*197GXh&6I0|Y8F(l0NVox!~2}uHl?83{cIKM zG8S~(tjh4e4~h2^nx;rVXCpMVeL5v{&K>#PW0l@iF0KuU1lhNO`dZ3Vzbb_ zD>P-|4TXUA0<>_%4-6F6CmgMf|Ks3FukrwTxvYL3Ve4KK84mfJ0)gwY^?Q0#**pPCSt{`vqa{nFOtru zQbzan>o^MJ>)XCR1|0PBI7f>P)}5&-7budAppt#g-4&%d4r=lA7}Dhpdo-F~>q^kg zFOY6s>6Xji8U%PY(=xQ9x)5Mi2r_p#GE4g3`!?pO^v5_WvU%_7AFf zoaL}p6|o1z&mgCH;PpQw<^Dkf5a@(gyFxr{e0h2Qt{qeGRu*|Zt^h^vEJ~idUTi=l N9PJ4BDqB+GzX5ubGI;<1 literal 0 HcmV?d00001 diff --git a/tests/widgets/staking-migration-widget/test-results/smw-06-success.png b/tests/widgets/staking-migration-widget/test-results/smw-06-success.png new file mode 100644 index 0000000000000000000000000000000000000000..1502450f93d559c4dac9ff57dd399767f9a431b9 GIT binary patch literal 93927 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 GUj7eG!^*z^ literal 0 HcmV?d00001 diff --git a/tests/widgets/staking-migration-widget/test-results/smw-07-error.png b/tests/widgets/staking-migration-widget/test-results/smw-07-error.png new file mode 100644 index 0000000000000000000000000000000000000000..decd05de08c44272b13257f290892a088f9f1d67 GIT binary patch literal 97315 zcmdqIWmFtdw=GISAi)BG;O@Z*?gY2s4vmH2?%o8~K!D&D5-h=jOXCpS9U6xQI=D8T zSMc3)@BMqmJLCO$#SixA8ntW7+H1`<*Q_Wt6*(+)5_BXaBrFAaX$>SK6rhPb|Kcg| zufqx_00{{PNkLjd%R6gt3G3Z!M7H?-p_k>n_809hGLa>3BC%KTh(0qgkeJhBe`avt z*>6)gELZSr>$$kL^KFCBwL$DA>@t_7CU4H!;Y+-Y5k1DxG`=XC4C@@uw+Y?PC?#G? zjLUoh{s_G0(+>kJtt_|Atju2FIImQwfh;YptgH$L{KQwDKR1sBM)Yuj_5~gW2V7sC z;(U7Y*rKARLPCCQy;FRLg!B$qK|FkroKIQyREC1Ou)Bm*O(O$Iw zr}YK*|5b<2pCA!2y1H87AR!U?EkH+bkUkwF;r~y+<$rwG|NrUG8CdyGgImjcmlK-J zgH>mVmluVtUA51E-8E&EK>6P`zzpoS?h`^bKWYbMLFF2;BgxCu6*Fos9FC=<=xRFt zaa+&7$44cjuY%gAX#+wwFh-9ZT^re>AoXYaE2=I3BiY}gxJ|m*j6+U1fIFb2$2-L3$^Pn5*21P0R@{f#?~p#w_}zeA+K7m} zcbtJyxjX}o1eM48RRV{?+MA6nsgtok__|FpIEU0Z+F89yOBA?gmF;Nbx-ycqc=aIq zDMQ62nW=pSQL#!J;nSO8Qtv3J(xh9|pt6zUxiH%APw~&7dhz1d?W*NG{Qb;7=DDq# z+*3ewn^qFyyTxYr3sX5!12Pi@N=!yo#1meBX-?A7P~_#Cj2YRtHyfghi?mv?Ct)p> z??U{??{4&02TWXmNxD3oOkHl-$e(6zo+yU_3=^x|4x8nxF_vk$UQr2X3bm1*PM-d7 zrsaj-*Z*s`FH{vwy2^Z}-g1E3%h)T;3_l{cS#hY!d2d^w_FH&DXYW>od#*wk&%$-H z2DkOD1`X)0d(vR;V;KS^} zw^frYFQ;g~ct)DGWx5&$AvS!_eeC_=dxGLuhPG)=_1^xByW6z*2$F+^vfJivUGWJ9 zKKpoooEGXQfSCjVX5x%5Oih8|Qdpcm$o7k>D2H3Cf3e28njZOgO7@2yhGpgO%ouM< z1;}Z~vjlT4-I{6r-+WcOW_eMV_=kD)D;}lLZu;8@72gf)<9=-a{j?5_z-W`(cOMZO zv}q!u*aD7z{_W#^))&mZ;rq9>!n76Qtk;@UIgR`$UiWir3cr%YJW@{%G`O6~aOOyD zs!G;RWZcg+Z7g8OAYVCley0+*<$(3ph?`%Y1)kI7NUMK?Bz_|ym4Bf2^oPuu8%Vcg zUfE04cqF^Tuqu307HN+8+}$^#aow`p@noPZPNR2!RA_62Xo0bYJ>#ER8yr#P1^PR8 z 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 literal 0 HcmV?d00001