From b244b0aa66259d2754b7b02872e0286015b4f515 Mon Sep 17 00:00:00 2001 From: Ludovic Levalleux Date: Wed, 29 Apr 2026 17:51:52 +0100 Subject: [PATCH 1/2] docs: import agentic-commerce doc files (allow to keep agentic-commerce repo private for now) --- docs/agentic-commerce/AGENTS.md | 732 +++++++++++++ docs/agentic-commerce/README.md | 70 ++ docs/agentic-commerce/README.npm.md | 70 ++ .../docs/fermion/create-mcp-client.md | 22 + .../docs/fermion/create-offer-sequence.png | Bin 0 -> 74343 bytes .../docs/fermion/example/create-mcp-client.ts | 35 + .../docs/fermion/example/create-offer.ts | 184 ++++ .../docs/fermion/example/docker-compose.yaml | 107 ++ .../docs/fermion/example/ipfs-config.sh | 5 + .../docs/fermion/example/runit.ts | 27 + .../fermion/example/seller-entity-creation.ts | 105 ++ .../docs/fermion/example/sign-transaction.ts | 62 ++ .../fermion/fermion-local-environment.png | Bin 0 -> 76153 bytes .../fermion/interact-with-fermion-protocol.md | 40 + .../docs/fermion/setup-local-env.md | 178 ++++ .../complete-marketplace-journeys.test.ts | 970 ++++++++++++++++++ .../scripts/sign-transaction.ts | 142 +++ docs/agentic-commerce/src/boson/README.md | 5 + .../src/boson/goat-sdk-plugin/README.md | 42 + .../goat-sdk-plugin/examples/anthropic.ts | 132 +++ .../boson/goat-sdk-plugin/examples/vercel.ts | 138 +++ .../src/boson/mcp-client/README.md | 73 ++ .../src/boson/mcp-server/README.md | 288 ++++++ docs/agentic-commerce/src/fermion/README.md | 136 +++ 24 files changed, 3563 insertions(+) create mode 100644 docs/agentic-commerce/AGENTS.md create mode 100644 docs/agentic-commerce/README.md create mode 100644 docs/agentic-commerce/README.npm.md create mode 100644 docs/agentic-commerce/docs/fermion/create-mcp-client.md create mode 100644 docs/agentic-commerce/docs/fermion/create-offer-sequence.png create mode 100644 docs/agentic-commerce/docs/fermion/example/create-mcp-client.ts create mode 100644 docs/agentic-commerce/docs/fermion/example/create-offer.ts create mode 100644 docs/agentic-commerce/docs/fermion/example/docker-compose.yaml create mode 100644 docs/agentic-commerce/docs/fermion/example/ipfs-config.sh create mode 100644 docs/agentic-commerce/docs/fermion/example/runit.ts create mode 100644 docs/agentic-commerce/docs/fermion/example/seller-entity-creation.ts create mode 100644 docs/agentic-commerce/docs/fermion/example/sign-transaction.ts create mode 100644 docs/agentic-commerce/docs/fermion/fermion-local-environment.png create mode 100644 docs/agentic-commerce/docs/fermion/interact-with-fermion-protocol.md create mode 100644 docs/agentic-commerce/docs/fermion/setup-local-env.md create mode 100644 docs/agentic-commerce/e2e/boson/tests/complete-marketplace-journeys.test.ts create mode 100644 docs/agentic-commerce/scripts/sign-transaction.ts create mode 100644 docs/agentic-commerce/src/boson/README.md create mode 100644 docs/agentic-commerce/src/boson/goat-sdk-plugin/README.md create mode 100644 docs/agentic-commerce/src/boson/goat-sdk-plugin/examples/anthropic.ts create mode 100644 docs/agentic-commerce/src/boson/goat-sdk-plugin/examples/vercel.ts create mode 100644 docs/agentic-commerce/src/boson/mcp-client/README.md create mode 100644 docs/agentic-commerce/src/boson/mcp-server/README.md create mode 100644 docs/agentic-commerce/src/fermion/README.md diff --git a/docs/agentic-commerce/AGENTS.md b/docs/agentic-commerce/AGENTS.md new file mode 100644 index 0000000..a6248e6 --- /dev/null +++ b/docs/agentic-commerce/AGENTS.md @@ -0,0 +1,732 @@ +# Boson Protocol Agentic Commerce — AI Agent Guide + +This file is the primary reference for AI agents working with this repository. It covers everything needed to perform end-to-end selling and buying on the [Boson Protocol](https://bosonprotocol.io). + +Follows the [`AGENTS.md` convention](https://agents.md) used by Codex, Amp, Aider, Cursor, Jules, and others. Claude Code loads it via `CLAUDE.md` → `@AGENTS.md`. For Gemini CLI or GitHub Copilot, symlink or copy to `GEMINI.md` or `.github/copilot-instructions.md`. + +**Canonical sources of truth** (consult these when this guide is silent or ambiguous): + +| Need | Source | +|---|---| +| Protocol concepts, glossary, lifecycles | https://docs.bosonprotocol.io | +| Agent integration (MCP + GOAT SDK) | https://docs.bosonprotocol.io/using-the-protocol/agent-integration | +| Full list of MCP tools with every parameter | [src/boson/mcp-server/README.md](src/boson/mcp-server/README.md) | +| Runnable reference for every flow | [e2e/boson/tests/](e2e/boson/tests/complete-marketplace-journeys.test.ts) (especially `complete-marketplace-journeys.test.ts`) | +| SDK type definitions & state enums | `node_modules/@bosonprotocol/core-sdk/src/subgraph.ts` (ExchangeState, DisputeState) and `node_modules/@bosonprotocol/common/src/types/accounts.ts` (AuthTokenType) | +| Solidity ground truth (enums, structs) | [boson-protocol-contracts/contracts/domain/BosonTypes.sol](https://github.com/bosonprotocol/boson-protocol-contracts/blob/main/contracts/domain/BosonTypes.sol) | + +--- + +## What This Repo Provides + +This is the official MCP integration layer for the Boson Protocol — a Decentralized Agentic Commerce Protocol (dACP) for on-chain exchange of physical and digital goods. + +| Component | Path | Purpose | +|---|---|---| +| Boson MCP Server | `src/boson/mcp-server/` | 54 tools + 2 prompts + 10 resources for all Boson operations | +| Boson MCP Client | `src/boson/mcp-client/` | Typed TypeScript client (camelCase methods mirroring server tools) | +| GOAT SDK Plugin | `src/boson/goat-sdk-plugin/` | Drop-in plugin for GOAT SDK / Vercel AI / Anthropic agents | +| High Value Asset Module MCP Server | `src/fermion/mcp-server/` | Tools for the High Value Asset Module (formerly Fermion Protocol, integrated into Boson in 2025 — adds verifier + custodian roles for high-value assets) | +| E2E tests | `e2e/boson/tests/` | Trustworthy reference for every end-to-end flow | + +--- + +## Boson Protocol Core Concepts + +**Offer** — A seller's on-chain listing: price, deposits, validity period, metadata, dispute resolver. May be publicly listed, *non-listed* (private, EIP-712 signed off-chain), *token-gated*, *price-discovery* (auction), or part of a *bundle*. + +**Exchange** — A single sale's lifecycle: buyer commits → funds locked in escrow → voucher NFT minted → buyer redeems → dispute period → funds released. + +**Voucher (rNFT)** — ERC-721 token issued to the buyer on commit. Transferable / tradeable on secondary markets. Redemption burns the NFT and triggers off-chain fulfillment. May be *preminted* by the seller to list on external marketplaces before any commit. + +**Seller** — Protocol entity with three roles (Ethereum addresses): +- `admin` — governance; can re-assign roles; controls auth-token binding +- `assistant` — day-to-day ops (create/void offers, revoke vouchers, choose royalty recipients per offer) +- `treasury` — receives proceeds + +At creation all three **must be the same address**; they can be re-assigned later. Re-assigning admin/assistant requires confirmation from the new address; treasury does not. + +**Buyer** — Entity representing a purchaser. **Auto-created on first commit** in most cases; `create_buyer` is only needed up-front for buyer-initiated offers (so the buyer can `deposit_funds` first). + +**Dispute Resolver (DR)** — Neutral third-party arbitrator. Has its own admin/assistant/treasury, sets its own fee token + amount, and may maintain an allow-list of sellers. Assigned per-offer via `disputeResolverId`. + +**Agent** (protocol entity, not AI) — A marketplace / surfacing platform that earns a fee percentage per offer. **Do not confuse with "AI agent".** + +**Royalty Recipient** — First-class account type; admin maintains an allow-list, assistant wires one per offer. + +**Bundle** — An item bundle (multiple ProductV1 and/or NFT components) listed as a single offer. + +For the full glossary see https://docs.bosonprotocol.io/learn-about-boson-protocol/glossary. + +--- + +## Quick Start: Hosted MCP Servers + +Boson operates cloud-hosted MCP servers — no local setup required for the server itself: + +| Environment | URL | +|---|---| +| Staging | `https://mcp-staging.bosonprotocol.io/mcp` | +| Production | `https://mcp.bosonprotocol.io/mcp` | + +Add to your MCP client config: + +```json +{ + "mcpServers": { + "boson": { + "url": "https://mcp-staging.bosonprotocol.io/mcp" + } + } +} +``` + +Write operations still require your own wallet and signing — the server never holds keys. + +--- + +## Critical Concept: Transaction Signing Pattern + +**Every state-changing tool returns an unsigned transaction. It does NOT execute automatically.** + +The 3-step pattern for all write operations: + +``` +1. Call tool → returns { to, data, ... } (unsigned tx fields) +2. Sign → locally with your wallet (e.g. ethers `wallet.signTransaction(tx)`, + viem `walletClient.signTransaction(tx)`) — never send your private key + over the network +3. Broadcast → send_signed_transaction with the signed hex +``` + +Every write tool takes a `signerAddress` — it tells the protocol *which account* is performing the action (for authorization). It does **not** mean the server signs for you. + +The MCP server never holds keys. All signing happens in your own infrastructure; the server only returns unsigned data and broadcasts pre-signed hex. + +### executionMode + +Most write tools accept an optional `executionMode`: + +- `"direct"` (default) — standard on-chain tx; sign locally + `send_signed_transaction` +- `"metaTx"` — gasless relay via Biconomy; use `send_meta_transaction` / `send_native_meta_transaction` / `send_forwarded_meta_transaction` (ERC-20 gas payment). Typed data is returned for local EIP-712 signing; the relayer pays gas. + +### Indexing latency + +After any write, the Boson subgraph needs a few blocks to index. Reads (`get_offers`, `get_exchanges`, etc.) will lag the chain. If you must read state you just wrote, use `CoreSDK.waitForGraphNodeIndexing(blockNumber)` (see e2e tests) or poll with retries. + +--- + +## ConfigId: Choosing Your Network + +All tools require a `configId` parameter identifying the network + protocol version. Format: `{env}-{chainId}-{version}`. + +| Environment | configId | Network | +|---|---|---| +| Local dev | `local-31337-0` | Hardhat local | +| Testnet | `testing-80002-0` | Polygon Amoy | +| Testnet | `testing-84532-0` | Base Sepolia | +| Testnet | `testing-11155111-0` | Ethereum Sepolia | +| Testnet | `testing-11155420-0` | Optimism Sepolia | +| Testnet | `testing-421614-0` | Arbitrum Sepolia | +| Staging | `staging-80002-0` | Polygon Amoy | +| Staging | `staging-84532-0` | Base Sepolia | +| Staging | `staging-11155111-0` | Ethereum Sepolia | +| Staging | `staging-11155420-0` | Optimism Sepolia | +| Staging | `staging-421614-0` | Arbitrum Sepolia | +| Production | `production-137-0` | Polygon | +| Production | `production-1-0` | Ethereum | +| Production | `production-8453-0` | Base | +| Production | `production-10-0` | Optimism | +| Production | `production-42161-0` | Arbitrum | + +Call `get_config_ids` to see which configIds the running server actually supports (set via the `CONFIG_IDS` env var). + +--- + +## Canonical Enum Values + +**Pass these as numbers, not strings.** + +### AuthTokenType (for seller admin binding) +From `@bosonprotocol/common` accounts.ts: + +| Value | Meaning | +|---:|---| +| `0` | NONE — standard wallet admin (default) | +| `1` | CUSTOM — reserved | +| `2` | LENS — Lens Protocol profile NFT | +| `3` | ENS — ENS name | + +When `authTokenType !== 0`, set `admin: "0x0000...0000"` and `authTokenId: `. + +### EvaluationMethod (token-gating `condition.method`) +From `@bosonprotocol/common` groups.ts: + +| Value | Meaning | +|---:|---| +| `0` | None | +| `1` | Threshold — hold `≥ threshold` tokens | +| `2` | TokenRange — hold any token ID in `[minTokenId, maxTokenId]` | + +> Note: `validation.ts` currently describes method `2` as "SpecificToken" — this is a stale string alias. The canonical SDK name is `TokenRange`. + +### TokenType (`condition.tokenType`) + +| Value | Meaning | +|---:|---| +| `0` | FungibleToken (ERC-20) | +| `1` | NonFungibleToken (ERC-721) | +| `2` | MultiToken (ERC-1155) | + +### GatingType (`condition.gatingType`) + +| Value | Meaning | +|---:|---| +| `0` | PerAddress — each qualifying wallet commits `maxCommits` times | +| `1` | PerTokenId — each qualifying token ID used `maxCommits` times | + +### OfferCreator / `creator` (enum accepted by MCP as string) + +`"SELLER"` (default, enum 0) or `"BUYER"` (enum 1). When `creator === "BUYER"`, `quantityAvailable` must be `1`. + +### priceType (offer `priceType`) + +`0` = static fixed price. `1` = price discovery (auction / order book / resolve-at-commit). + +--- + +## Seller Flow: End-to-End + +### Step 1 — Create Seller Account (if needed) + +Check first — call `get_sellers_by_address` with `signerAddress`. If a seller exists, use its `id` as `sellerId` directly. The `create-seller-if-needed` MCP prompt automates this. + +``` +Tool: create_seller +Required: configId, signerAddress +Seller metadata (required by schema): type: "SELLER", kind: , contactPreference: +Optional metadata: name, description, legalTradingName, website, images[], contactLinks[], socialLinks[], salesChannels[] +Other params: + contractUri: "" | ipfs://... (OpenSea-style storefront metadata) + royaltyPercentage: "0"–"10000" (basis points; default for all offers) + authTokenType: 0 (NONE) — pass a number, not a string + authTokenId: "0" +``` + +Note: the `create_seller` tool automatically sets `admin`, `assistant`, and `treasury` to `signerAddress`. If you need distinct roles, use the plugin / client variant that accepts all three explicitly, or call `update_seller` afterwards. + +→ Sign and broadcast. Extract `sellerId` from transaction logs via `CoreSDK.getCreatedSellerIdFromLogs(receipt.logs)`. + +### Step 2 — Store Product Metadata on IPFS + +Required before `create_offer`. Three variants: + +``` +store_product_v1_metadata — standard physical/digital product +store_bundle_metadata — bundle of multiple items (requires pre-stored items) +store_base_metadata — minimal custom metadata +``` + +For bundles: first call `store_bundle_item_product_v1_metadata` and/or `store_bundle_item_nft_metadata` for each item, include their returned `url`s in the `items` array, then call `store_bundle_metadata`. + +Optional: run `validate_metadata` first to catch schema errors without uploading. + +→ Returns `{ metadataUri: "ipfs://...", metadataHash: "0x..." }`. Store both. + +### Step 3 — (Optional) Choose a Dispute Resolver + +Offers must reference a `disputeResolverId`. Call `get_dispute_resolvers` to list available resolvers (shows fees, response period, supported tokens). Required in most deployments; omit only if the protocol version does not require one. + +### Step 4 — Create Offer + +``` +Tool: create_offer +Required: configId, signerAddress, metadataUri, metadataHash, and: + price: "1000000000000000000" # wei (smallest unit of exchange token) + sellerDeposit: "0" # seller collateral; forfeited on revoke + buyerCancellationPenalty: "0" # deducted from buyer on cancel + quantityAvailable: 10 # 1 if creator="BUYER" + validFromDateInMS: Date.now() + validUntilDateInMS: + voucherRedeemableFromDateInMS: # ≥ validFromDateInMS + voucherRedeemableUntilDateInMS: 0 # 0 → use voucherValidDurationInMS + voucherValidDurationInMS: 604800000 + disputePeriodDurationInMS: 604800000 # window to raise dispute post-redeem + resolutionPeriodDurationInMS: 259200000 # window to resolve a raised dispute + disputeResolverId: "" +Optional: + exchangeTokenAddress: "0x..." # ERC-20; omit / address(0) for native + agentId: "0" # dACP protocol-agent id (not AI) + priceType: 0 # 0 = static, 1 = discovery + feeLimit: "" + collectionIndex: "0" + royaltyInfo: { recipients: ["0x..."], bps: ["500"] } # sum ≤ 10000 + creator: "SELLER" # default; "BUYER" makes it an RFQ + executionMode: "direct" | "metaTx" +``` + +→ Sign and broadcast. Extract `offerId` from logs. + +For token-gated offers use `create_offer_with_condition` and add a `condition` object (see the `condition` schema under "Token-Gated Offers" below). + +### Step 5 — Monitor Exchanges + +``` +Tool: get_exchanges + exchangesFilter: + sellerId: "" + state: "COMMITTED" | "REDEEMED" | "DISPUTED" | ... +``` + +After `REDEEMED`, start the dispute-period timer. After redemption + `disputePeriodDurationInMS` with no dispute, anyone can `complete_exchange`. + +### Step 6 — Withdraw Proceeds + +After exchanges complete: + +``` +Tool: withdraw_funds + entityId: "" + tokenAddress: "0x0000000000000000000000000000000000000000" # 0x0 for native + amount: "" +``` + +Query balances with `get_funds`. + +--- + +## Buyer Flow: End-to-End + +### Step 1 — Browse Offers + +``` +get_offers # with filters (voided, sellerId, price, etc.) +get_all_products_with_not_voided_variants # storefront-style grouping +``` + +### Step 2 — Approve Payment Token (ERC-20 only) + +If the offer's `exchangeToken.address` ≠ zero address: + +``` +Tool: approve_exchange_token + offerId: "" +``` + +→ Sign and broadcast. Grants the protocol allowance to pull tokens on `commit_to_offer`. + +### Step 3 — Commit to Offer + +``` +Tool: commit_to_offer + offerId: "" + buyer: "" # optional; defaults to signerAddress +``` + +→ Sign and broadcast. Funds locked in escrow, voucher NFT minted. Extract `exchangeId` from logs via `CoreSDK.getCommittedExchangeIdFromLogs(receipt.logs)`. + +### Step 4 — Redeem Voucher + +``` +Tool: redeem_voucher + exchangeId: "" +``` + +→ Sign and broadcast. Voucher NFT burned. Dispute period clock starts. Seller is expected to deliver off-chain. + +### Step 5 (optional) — Raise Dispute + +Must be within `disputePeriodDurationInMS` of redemption: + +``` +Tool: raise_dispute + exchangeId: "" +``` + +Exchange state transitions to `DISPUTED`; initial dispute state is `RESOLVING`. See dispute section below. + +### Step 6 — Complete Exchange + +After redemption with no dispute (either anyone after dispute period, or buyer at any time): + +``` +Tool: complete_exchange + exchangeId: "" +``` + +Releases funds to the seller's treasury balance. Seller then `withdraw_funds`. + +--- + +## Exchange & Dispute State Machines + +**Exchange state and dispute state are distinct.** When a dispute is raised, the *exchange* state becomes `DISPUTED`, but the *dispute entity* has its own state starting at `RESOLVING`. + +### ExchangeState (`@bosonprotocol/core-sdk/subgraph.ts`) + +`COMMITTED`, `REDEEMED`, `COMPLETED`, `DISPUTED`, `CANCELLED`, `REVOKED`. + +Plus one **client-side derived** state (not on-chain): +- `EXPIRED` — `COMMITTED` exchange whose `voucherRedeemableUntil` has passed without redemption; triggered via `cancel_voucher` by anyone (behaves like CANCELLED). + +### DisputeState + +`RESOLVING` (initial after `raise_dispute`), `RETRACTED`, `RESOLVED`, `ESCALATED`, `DECIDED`, `REFUSED`. + +### Transition diagram + +``` +[Offer listed] + │ + ▼ commit_to_offer + COMMITTED ─── revoke_voucher ──► REVOKED (seller revokes; buyer gets deposit back) + │ + ├── cancel_voucher ─────────► CANCELLED (buyer cancels or voucher expires; penalty forfeited) + │ + ▼ redeem_voucher + REDEEMED ─── complete_exchange ► COMPLETED (success; seller paid) + │ + └── raise_dispute (Exchange → DISPUTED, Dispute → RESOLVING) + │ + DISPUTED/RESOLVING + ├── retract_dispute ─► DISPUTED/RETRACTED (buyer withdraws; flows to completion) + ├── resolve_dispute ─► DISPUTED/RESOLVED (mutual agreement, custom split) + ├── expire_dispute ──► DISPUTED/RETRACTED (resolution timeout) + └── escalate_dispute (buyer pays escalation deposit) + │ + DISPUTED/ESCALATED + ├── resolve_dispute ──────────► DISPUTED/RESOLVED + ├── decide_dispute ───────────► DISPUTED/DECIDED (DR rules; basis-points split) + ├── refuse_escalated_dispute ─► DISPUTED/RESOLVING (escalation fee refunded) + └── expire_escalated_dispute ─► DISPUTED/REFUSED (DR didn't respond in time) +``` + +### State-transition reference + +| Action | Exchange before → after | Dispute before → after | Caller | +|---|---|---|---| +| `commit_to_offer` | (listed) → COMMITTED | — | Buyer | +| `revoke_voucher` | COMMITTED → REVOKED | — | Seller assistant | +| `cancel_voucher` | COMMITTED → CANCELLED | — | Buyer (or anyone if expired) | +| `redeem_voucher` | COMMITTED → REDEEMED | — | Buyer | +| `complete_exchange` | REDEEMED → COMPLETED | — | Anyone (after dispute period) | +| `raise_dispute` | REDEEMED → DISPUTED | — → RESOLVING | Buyer (within disputePeriod) | +| `retract_dispute` | DISPUTED | RESOLVING/ESCALATED → RETRACTED | Buyer | +| `resolve_dispute` | DISPUTED | RESOLVING/ESCALATED → RESOLVED | Either (both signed) | +| `extend_dispute_timeout` | DISPUTED | RESOLVING | Either | +| `expire_dispute` | DISPUTED | RESOLVING → RETRACTED | Anyone (post-timeout) | +| `escalate_dispute` | DISPUTED | RESOLVING → ESCALATED | Buyer (pays escalation deposit) | +| `decide_dispute` | DISPUTED | ESCALATED → DECIDED | Dispute Resolver | +| `refuse_escalated_dispute` | DISPUTED | ESCALATED → RESOLVING | Dispute Resolver | +| `expire_escalated_dispute` | DISPUTED | ESCALATED → REFUSED | Anyone (post-response-period) | + +Docs: https://docs.bosonprotocol.io/using-the-protocol/dacp-tools/exchange-mechanism + +### Payout outcomes + +| Final state | Seller receives | Buyer receives | +|---|---|---| +| COMPLETED / RETRACTED | Price − protocol fee − agent fee + seller deposit | — | +| CANCELLED / EXPIRED | Cancellation penalty | Price − penalty | +| REVOKED | — | Price + seller deposit | +| RESOLVED | Per signed agreement (basis points) | Per signed agreement | +| DECIDED | Per DR ruling (basis points) | Per DR ruling | +| REFUSED | Seller deposit back | Price + escalation deposit | + +Protocol fee is zero when the exchange token is `$BOSON`. Agent fee is set per-offer via `agentId`. + +Fees reference: https://docs.bosonprotocol.io/learn-about-boson-protocol/core-concepts/fees + +--- + +## Non-Listed (Private / Bilateral) Offers + +For peer-to-peer deals not publicly listed on-chain. Uses EIP-712 signatures. Contract-wallet signers are supported via EIP-1271. + +**Seller-initiated private offer:** +1. Seller calls `sign_full_offer` → returns EIP-712 typed data +2. Seller signs the typed data locally with their wallet (EIP-712) +3. Seller shares the `signature` with the buyer (off-chain) +4. Buyer calls `create_offer_and_commit` with the signature → atomic create + commit + +**Buyer-initiated private offer:** +1. Buyer calls `create_buyer` to get a `buyerId` +2. Buyer calls `deposit_funds` to pre-fund the price +3. Buyer calls `sign_full_offer` with `creator: "BUYER"` → EIP-712 typed data +4. Buyer signs and shares signature + offer details with seller +5. Seller calls `create_offer_and_commit` → atomic create + commit + +**Cancel before fulfillment:** +- `void_non_listed_offer` — cancel one private offer (same params as `sign_full_offer`) +- `void_non_listed_offer_batch` — batch cancel + +--- + +## Token-Gated Offers + +``` +Tool: create_offer_with_condition +(same params as create_offer, plus:) + condition: + method: 1 # 0=None, 1=Threshold, 2=TokenRange + tokenType: 1 # 0=ERC-20, 1=ERC-721, 2=ERC-1155 + tokenAddress: "0x..." + gatingType: 0 # 0=PerAddress, 1=PerTokenId + minTokenId: "0" + maxTokenId: "0" + threshold: "1" # min token balance + maxCommits: "1" # max commits per qualifying address or token ID +``` + +Conditions also support ERC-20, ERC-721, and ERC-1155 holders; multiple offers can share a condition via a group (see docs). + +--- + +## Dispute Resolution (Mutual Agreement) + +``` +1. Both parties negotiate the split off-chain. +2. create_dispute_resolution_proposal → EIP-712 typed data (buyerPercentBasisPoints 0–10000). +3. Both parties sign the typed data locally with their wallet (EIP-712). +4. Either party submits resolve_dispute with both signatures' sigR/sigS/sigV. + → Dispute state: RESOLVED; funds split per agreement. +``` + +EIP-1271 contract-wallet signatures are accepted. + +--- + +## Buyer-Initiated (RFQ) Offers + +A buyer can publish an offer (request for quote): + +``` +create_offer with creator: "BUYER", quantityAvailable: 1 +``` + +Then the seller accepts: + +``` +commit_to_buyer_offer + offerId: "" +``` + +This is distinct from the non-listed/private flow above (which is bilateral & off-chain-first). + +--- + +## Tool Inventory (54 tools) + +Full documentation with every parameter lives in [src/boson/mcp-server/README.md](src/boson/mcp-server/README.md). This is an orientation map — call that file when you need exact signatures. + +**Configuration & discovery:** `get_config_ids`, `get_supported_tokens`, `get_dispute_resolvers`, `get_registered_agents`. + +**Read-only queries (also available as MCP resources):** `get_offers`, `get_exchanges`, `get_sellers`, `get_sellers_by_address`, `get_disputes`, `get_dispute_by_id`, `get_funds`, `get_all_products_with_not_voided_variants`. + +**Seller / buyer accounts:** `create_seller`, `update_seller`, `create_buyer`. + +**Metadata (IPFS):** `validate_metadata`, `store_product_v1_metadata`, `store_bundle_metadata`, `store_base_metadata`, `store_bundle_item_product_v1_metadata`, `store_bundle_item_nft_metadata`, `render_contractual_agreement`. + +**Offer lifecycle:** `create_offer`, `create_offer_with_condition`, `sign_full_offer`, `create_offer_and_commit`, `void_offer`, `void_non_listed_offer`, `void_non_listed_offer_batch`. + +**Exchange lifecycle:** `approve_exchange_token`, `commit_to_offer`, `commit_to_buyer_offer`, `redeem_voucher`, `cancel_voucher`, `revoke_voucher`, `complete_exchange`. + +**Disputes:** `raise_dispute`, `retract_dispute`, `create_dispute_resolution_proposal`, `resolve_dispute`, `extend_dispute_timeout`, `expire_dispute`, `expire_dispute_batch`, `escalate_dispute`, `decide_dispute`, `refuse_escalated_dispute`, `expire_escalated_dispute`. + +**Funds:** `deposit_funds`, `withdraw_funds`. + +**Transaction sending:** `send_signed_transaction`, `send_meta_transaction`, `send_native_meta_transaction`, `send_forwarded_meta_transaction`. (Signing is done locally with your wallet; the server never receives private keys.) + +**Agent registry:** `register_agent`, `get_registered_agents`. + +### MCP prompts + +- `create-seller-if-needed` — checks for existing seller; creates if absent; returns `sellerId`. Args: `configId`, `signerAddress`. +- `create-offer` — guided metadata + offer creation. Args: `configId`, `signerAddress`. + +### MCP resources + +Same data as the `get_*` tools but accessed via URI templates: `config-ids://ids`, `offers://entities`, `products://entities`, `exchanges://entities`, `disputes://entities`, `funds://entities`, `sellers://entities`, `sellers-by-address://entities`, `dispute-resolvers://entities`, `registered-agents://`. + +Default to the `get_*` tools when in doubt — they are equivalent and mirror exact argument schemas. + +--- + +## Agent Registry (dACP) + +Register your agent in the dACP registry to make it discoverable: + +``` +register_agent +``` + +Requires `GITHUB_PERSONAL_ACCESS_TOKEN`; opens a PR against `AGENT_REGISTRY_REPO` (default `bosonprotocol/dACP-agents-registry`). + +Registry JSON schema: https://github.com/bosonprotocol/dACP-agents-registry/blob/main/schemas/v1.0.0.json + +Required top-level fields: `uuid`, `name`, `description`, `schemaVersion: "1.0.0"`, `entities[]` (each with `configId`, `role: seller|buyer|disputeResolver|marketplace|royaltyRecipient|other`, `entityId`, `signerAddress`). + +List everything registered: + +``` +get_registered_agents +``` + +--- + +## Configuration + +### Environment Variables + +```bash +# Required for metadata storage (IPFS) +INFURA_IPFS_PROJECT_ID= +INFURA_IPFS_PROJECT_SECRET= + +# Networks this server supports (comma-separated configIds) +CONFIG_IDS=local-31337-0,testing-80002-0,production-137-0 + +# Required for agent registry tool +GITHUB_PERSONAL_ACCESS_TOKEN= +AGENT_REGISTRY_REPO=bosonprotocol/dACP-agents-registry +AGENT_REGISTRY_DIRECTORY=agents/local # or agents/testing, agents/staging, agents/production + +# Optional overrides +SUBGRAPH_URL= +IPFS_METADATA_URL= +THE_GRAPH_IPFS_URL= +META_TX_RELAYER_URL= +API_KEY= +API_KEYS= +API_IDS={"0xContract":"apiId",...} +LOCALHOST_SUBSTITUTE= + +# HTTP server +BIND_ADDRESS=127.0.0.1 # 0.0.0.0 for Docker +ALLOWED_HOSTS=127.0.0.1,localhost +PORT=3000 +``` + +### Running Locally + +```bash +pnpm i --frozen-lockfile +pnpm build + +# HTTP mode (recommended — works with MCP Inspector) +pnpm start:boson:http +pnpm dev:boson:http # watch mode + +# Stdio mode (direct MCP client integration) +pnpm start:boson +``` + +### E2E tests + +```bash +pnpm e2e:boson:services # starts docker-compose (hardhat, subgraph, IPFS, MCP server) +pnpm e2e:boson:test # runs the full suite +``` + +Read [e2e/boson/tests/complete-marketplace-journeys.test.ts](e2e/boson/tests/complete-marketplace-journeys.test.ts) for a concrete reference implementation of every flow. + +--- + +## Integration Options + +### Option 1: Hosted MCP Server (zero setup) + +Point your MCP client at a hosted endpoint. Write operations still require your wallet & signing. + +### Option 2: GOAT SDK Plugin (for agent frameworks) + +```typescript +import { bosonProtocolPlugin } from "@bosonprotocol/agentic-commerce"; +import { getOnChainTools } from "@goat-sdk/adapter-vercel-ai"; +import { viem } from "@goat-sdk/wallet-viem"; + +const tools = await getOnChainTools({ + wallet: viem(walletClient), + plugins: [ + bosonProtocolPlugin({ + url: "https://mcp-staging.bosonprotocol.io/mcp", + }), + ], +}); +``` + +Working end-to-end examples: +- [src/boson/goat-sdk-plugin/examples/anthropic.ts](src/boson/goat-sdk-plugin/examples/anthropic.ts) +- [src/boson/goat-sdk-plugin/examples/vercel.ts](src/boson/goat-sdk-plugin/examples/vercel.ts) + +### Option 3: Direct MCP Client + +```typescript +import { BosonMCPClient } from "@bosonprotocol/agentic-commerce"; + +const client = new BosonMCPClient(); +await client.connectToServer("https://mcp-staging.bosonprotocol.io/mcp"); + +// Every server tool → camelCase method +const offers = await client.getOffers({ configId: "production-137-0" }); +const seller = await client.createSeller({ ...params }); +// Every server resource → readX method +const funds = await client.readFunds({ configId: "production-137-0" }); +``` + +The client helpers `signAndSendTransactionData` and `relayMetaTransaction` (see e2e `test-utils.ts`) encapsulate the 3-step sign+send pattern for direct and meta-tx modes. + +### Option 4: Self-Hosted MCP Server + +Copy `.env.example` to `.env`, fill values, build, run. See [src/boson/mcp-server/README.md](src/boson/mcp-server/README.md). + +--- + +## High Value Asset Module (formerly Fermion Protocol) + +"Fermion" is no longer used as a brand. The former Fermion Protocol was integrated into the Boson codebase in 2025 and is now the **High Value Asset Module** of Boson Protocol. Directory names (`src/fermion/`, `docs/fermion/`, `e2e/fermion/`), SDK package imports (`@fermionprotocol/core-sdk`, `@fermionprotocol/common`, `@fermionprotocol/metadata`), Docker image names, and script aliases (`start:fermion`, `dev:fermion`, etc.) are retained at the code level for compatibility. + +The module is a marketplace built atop Boson's primitives, adding verifier + custodian roles for high-value / luxury assets (fNFTs). See: + +- [src/fermion/README.md](src/fermion/README.md) — tool list & setup +- [docs/fermion/interact-with-fermion-protocol.md](docs/fermion/interact-with-fermion-protocol.md) — agent flow walkthrough +- [docs/fermion/setup-local-env.md](docs/fermion/setup-local-env.md) — local dev +- https://docs.fermionprotocol.io — legacy protocol docs (pre-integration) + +Key module-only tools: `create_offer` (requires `verifierId`, `custodianId`, `facilitatorId`), `mint_wrap_nfts`, `mint_wrap_fixed_priced`, `create_fixed_price_order`, `cancel_fixed_price_orders`, `generate_asset_verification_email`, `generate_asset_custody_email`, `check_in`, `check_out`, `request_check_out`, `top_up_custodian_vault`, `submit_tax`. Use the same sign → `send_signed_transaction` pattern as Boson. + +--- + +## Available Claude Code Skills + +Skills in `.claude/skills/`: + +- `boson-commerce:summary` — read-only status snapshot (active offers, exchanges by state). +- `boson-commerce:boson-commerce` — human-facing orchestrator for buying and selling. +- `boson-commerce:autonomous-seller-agent` — fully autonomous seller agent (A2A commerce). +- `boson-commerce:autonomous-buyer-agent` — fully autonomous buyer agent (A2A commerce). +- `anthropic-skills:buyer-agent` — human-assisted buyer flow orchestration (phases B–G). + +--- + +## External Resources + +| Resource | URL | +|---|---| +| Boson Protocol Docs | https://docs.bosonprotocol.io | +| Agent Integration (MCP + GOAT) | https://docs.bosonprotocol.io/using-the-protocol/agent-integration | +| dACP Tools index | https://docs.bosonprotocol.io/using-the-protocol/dacp-tools | +| Exchange Mechanism (lifecycle) | https://docs.bosonprotocol.io/using-the-protocol/dacp-tools/exchange-mechanism | +| Glossary | https://docs.bosonprotocol.io/learn-about-boson-protocol/glossary | +| Fees | https://docs.bosonprotocol.io/learn-about-boson-protocol/core-concepts/fees | +| Boson Protocol Website | https://www.bosonprotocol.io/ | +| Agent Commerce landing | https://www.bosonprotocol.io/agent-commerce-tech/ | +| agent-builder (LangChain, Telegram, XMTP) | https://github.com/bosonprotocol/agent-builder | +| direct-source-agent (Claude Code template) | https://github.com/bosonprotocol/direct-source-agent | +| dACP Agents Registry | https://github.com/bosonprotocol/dACP-agents-registry | +| Registry JSON schema | https://github.com/bosonprotocol/dACP-agents-registry/blob/main/schemas/v1.0.0.json | +| Boson Smart Contracts | https://github.com/bosonprotocol/boson-protocol-contracts | +| Contract ground-truth enums | https://github.com/bosonprotocol/boson-protocol-contracts/blob/main/contracts/domain/BosonTypes.sol | +| Boson Core Components SDK | https://github.com/bosonprotocol/core-components | +| Fermion Protocol (legacy; now Boson's High Value Asset Module since 2025) | https://fermionprotocol.io | +| Fermion Protocol Docs (legacy) | https://docs.fermionprotocol.io | diff --git a/docs/agentic-commerce/README.md b/docs/agentic-commerce/README.md new file mode 100644 index 0000000..37a05ca --- /dev/null +++ b/docs/agentic-commerce/README.md @@ -0,0 +1,70 @@ +# @bosonprotocol/agentic-commerce + +![tests](https://img.shields.io/badge/tests-995%2F1011%20passing-orange) +![coverage](https://img.shields.io/badge/coverage-58.9%25-orange) +![ESLint](https://img.shields.io/badge/ESLint-0%20warnings-brightgreen) +![TypeScript](https://img.shields.io/badge/TypeScript-passing-brightgreen) + +MCP server, MCP client, and GOAT SDK plugin for [Boson Protocol](https://bosonprotocol.io) — the decentralized Agentic Commerce Protocol (dACP) that enables agents to securely buy and sell anything — digital or physical — with game-theoretic guarantees. + +## 🤖 For AI Agents + +**See [AGENTS.md](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/AGENTS.md) for a comprehensive AI agent guide** covering end-to-end selling and buying flows, exchange lifecycle, configuration, and integration options. Claude Code auto-loads it via the `CLAUDE.md` → `@AGENTS.md` import. + +**Hosted MCP servers (no setup required):** +- Staging: `https://mcp-staging.bosonprotocol.io/mcp` +- Production: `https://mcp.bosonprotocol.io/mcp` + +## 📚 Documentation + +- [AI Agent Guide](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/AGENTS.md) — End-to-end flows, exchange & dispute lifecycles, enums, integration options +- [MCP Server README](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/src/boson/mcp-server/README.md) — All 56 tools, 2 prompts, 10 resources with full parameter reference +- [MCP Client README](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/src/boson/mcp-client/README.md) — TypeScript client usage +- [GOAT SDK Plugin README](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/src/boson/goat-sdk-plugin/README.md) — Agent framework integration (Anthropic, Vercel AI) +- [High Value Asset Module README](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/src/fermion/README.md) — High-value/luxury asset marketplace (formerly Fermion Protocol, integrated into Boson in 2025) +- [E2E Test Suite](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/e2e/boson/tests/complete-marketplace-journeys.test.ts) — Runnable reference for every seller/buyer/dispute flow +- [Boson Protocol Docs](https://docs.bosonprotocol.io) — Protocol concepts and architecture +- [Agent Integration Docs](https://docs.bosonprotocol.io/using-the-protocol/agent-integration) — MCP + GOAT SDK guide on the protocol site + +--- + +## 🧪 Development + +```bash +# Install dependencies +pnpm i --frozen-lockfile + +# Watch mode for development +pnpm watch + +# Run each in a terminal: +pnpm dev:boson:http +pnpm dev:boson:http:inspector + +# Run all tests +pnpm test + +# Run with coverage +pnpm test:coverage + +# Linting and type checking +pnpm lint +pnpm typecheck +``` + +## 📋 Environment Setup + +1. Copy `mcpServer.example.json` to `mcpServer.json` if not automatically done +2. Update the args array to match the real path to the file to be run + +## 📋 Requirements + +- Node.js 23+ +- Infura project ID and secret (check `.env.example`) + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Submit a pull request diff --git a/docs/agentic-commerce/README.npm.md b/docs/agentic-commerce/README.npm.md new file mode 100644 index 0000000..37a05ca --- /dev/null +++ b/docs/agentic-commerce/README.npm.md @@ -0,0 +1,70 @@ +# @bosonprotocol/agentic-commerce + +![tests](https://img.shields.io/badge/tests-995%2F1011%20passing-orange) +![coverage](https://img.shields.io/badge/coverage-58.9%25-orange) +![ESLint](https://img.shields.io/badge/ESLint-0%20warnings-brightgreen) +![TypeScript](https://img.shields.io/badge/TypeScript-passing-brightgreen) + +MCP server, MCP client, and GOAT SDK plugin for [Boson Protocol](https://bosonprotocol.io) — the decentralized Agentic Commerce Protocol (dACP) that enables agents to securely buy and sell anything — digital or physical — with game-theoretic guarantees. + +## 🤖 For AI Agents + +**See [AGENTS.md](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/AGENTS.md) for a comprehensive AI agent guide** covering end-to-end selling and buying flows, exchange lifecycle, configuration, and integration options. Claude Code auto-loads it via the `CLAUDE.md` → `@AGENTS.md` import. + +**Hosted MCP servers (no setup required):** +- Staging: `https://mcp-staging.bosonprotocol.io/mcp` +- Production: `https://mcp.bosonprotocol.io/mcp` + +## 📚 Documentation + +- [AI Agent Guide](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/AGENTS.md) — End-to-end flows, exchange & dispute lifecycles, enums, integration options +- [MCP Server README](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/src/boson/mcp-server/README.md) — All 56 tools, 2 prompts, 10 resources with full parameter reference +- [MCP Client README](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/src/boson/mcp-client/README.md) — TypeScript client usage +- [GOAT SDK Plugin README](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/src/boson/goat-sdk-plugin/README.md) — Agent framework integration (Anthropic, Vercel AI) +- [High Value Asset Module README](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/src/fermion/README.md) — High-value/luxury asset marketplace (formerly Fermion Protocol, integrated into Boson in 2025) +- [E2E Test Suite](https://github.com/bosonprotocol/agent-builder/blob/HEAD/docs/agentic-commerce/e2e/boson/tests/complete-marketplace-journeys.test.ts) — Runnable reference for every seller/buyer/dispute flow +- [Boson Protocol Docs](https://docs.bosonprotocol.io) — Protocol concepts and architecture +- [Agent Integration Docs](https://docs.bosonprotocol.io/using-the-protocol/agent-integration) — MCP + GOAT SDK guide on the protocol site + +--- + +## 🧪 Development + +```bash +# Install dependencies +pnpm i --frozen-lockfile + +# Watch mode for development +pnpm watch + +# Run each in a terminal: +pnpm dev:boson:http +pnpm dev:boson:http:inspector + +# Run all tests +pnpm test + +# Run with coverage +pnpm test:coverage + +# Linting and type checking +pnpm lint +pnpm typecheck +``` + +## 📋 Environment Setup + +1. Copy `mcpServer.example.json` to `mcpServer.json` if not automatically done +2. Update the args array to match the real path to the file to be run + +## 📋 Requirements + +- Node.js 23+ +- Infura project ID and secret (check `.env.example`) + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Submit a pull request diff --git a/docs/agentic-commerce/docs/fermion/create-mcp-client.md b/docs/agentic-commerce/docs/fermion/create-mcp-client.md new file mode 100644 index 0000000..4d5d3a8 --- /dev/null +++ b/docs/agentic-commerce/docs/fermion/create-mcp-client.md @@ -0,0 +1,22 @@ +# Create a High Value Asset Module MCP-Client + +> **Note:** "Fermion" is no longer used as a brand. The former Fermion Protocol was integrated into the Boson codebase in 2025 and is now the **High Value Asset Module** of Boson Protocol. The directory name `fermion/`, service names, and SDK package names are retained for code-level compatibility. + +The following documentation describes how to create an MCP client to connect an AI Agent to the High Value Asset Module's MCP server. + +In this example, the AI Agent using the MCP client will be able to use the module's MCP server to act as a Seller on the High Value Asset Module, create an offer, mint and list tokens (FNFTs), and so on (see the module's Seller flow). + +## Prerequisite + +Make sure: +- you have a functional Local Environment running (see [setup-local-env](./setup-local-env.md)) + +## Create an mcp-client + +Creating an MCP client is straightforward using the typescript_sdk and the example given in their GitHub repo (See [MCP typescript_sdk](https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#writing-mcp-clients)). + +The Client needs to connect using Streamable HTTP (as it's the transport type activated on the MCP-Server container), via the URL "http://localhost:3000/mcp". + +Other SDKs are also available in different languages (Python, Java, Kotlin, C#). See [modelcontextprotocol](https://github.com/modelcontextprotocol). + +An example is given in [./example/create-mcp-client.ts](./example/create-mcp-client.ts) diff --git a/docs/agentic-commerce/docs/fermion/create-offer-sequence.png b/docs/agentic-commerce/docs/fermion/create-offer-sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..cab5ddbf59f02ab7ce9f159e4fd7dbf1f25d2d1d GIT binary patch literal 74343 zcmeFZXIPZo(k_UI+K5Oa2m%tCEJ#k0u@MkK5D`$KGS-fF?@1F0>oH^&mT=Qdl;U!={E7Yo0b=O^0&-*7T3dC2buj1k15i34? z@Dva4;wJbz3Bd=y*=Q;I2L8G5`l$jOFTeBl5*{7{p5lZ1&)hDoCIuRhw;Z+j__X-4 z7&X4xm04w>h=e?PM=MkWReEN$N_>qXcu1%IhM@2&LxjZx;~S7FRYs_y6j2qw{aGj+AqWn@9(T0nhkIJn0di;ekh@{oO?VW&` z6%Q42&plS`-?k=$RlE3aMI&@0R9vd0WWUFV|ja8~We~!H| za;uMppslUVZnD;EZ*}g} zw<71|ei}b<*Y!m3x8Cm}jfkCY;mhwjM-jX9rFqZdQd=4uZN|R|v9Qedq$}-jOb+UN z+ZCXPpl7RIYPpv!C_H#DWrLo@RPEj6;CQ5>k}Tow*qiaVU(_*o{{Gv`O)V{Fd!zQc z-HfrJkh#Uhjj^iNeOYSGD+A8CBNTN-Yoiq@(mtmo(#M}uamE~Q2st@9Gb?LAV4#P) z`+FXV3hJWA3Bs=zekm#`kwrX@yX=P%kIlnip6qhZZEVy5k?p1n4M))q7LiUNZ=@8!E9_5+3H`@)8AFG_{os&rEOJ#sVU(N>C`Yjai= zQ?t*F8`M2z#(|o+7CdJDJ>2+NeERXG`&PoNv3y)yt`F13`}u zw{>=EZlIsdTL;fPFQ9Jt;)f>S*83)C)CifM9H$#Uj^Vyq=VX^x#oSMP)?Qn3vhv#! zte2d@cC>uF>UAdiS+~ADLG8)j=>DXSf#S-QlW$*jM7IW_9@{;iV-z`fVD;;(rR<50 zhFX)W)kh;I@9>R4aOkmXfn`2AR1X~21Dbmtzn=j*AnnNbW3lUFG-;VdQ(sGtD zg}WP$XU|Bjlp(rKw@bGpIOn?^9JLd*qjH=wfCM))z8WwSj! zgp}-v)8mDS$y|F3k5F4BxgiXS<4t69&VeipPW2f?7%r=LpCf-9m z-X_(6`o+fH`WEx}HhsL8d3EfKAM8Rm?s-X+E14U>AD$fUhS5oKWKlZ_UqdDf!K{cX z_8%qW7^h`s_P)i3HO5U6=g2ox6X63>lbU6&Li=>~{sm%k{3Dmsb}nAt%?Y#z#j~x{ zdt^<9_tr)!$;m6#ryrK*;l3PvbGByNID5laWx<+U|0`P-JB?i_ zsEhvkgIBNQU^YxTV`;SFb@K0-@9%;M7#EL^FhM|+-*krI*cSpS`zCNmL5ys+>Vde6 zsMJc|c-gp?ftOs#o^20fv+;%a7tmIp!fLIqa%4D4Ht?G@?UGmZl6_tX4ccQjGlXKP zAq>G(Xg6QCKc6Du{&d6N{~#OkKu(SnzTBU?(>pAL;NPD@N%q=~kD^|s;=m`lqxVcS@vqKa9CYRECd`E^5^!dYjs z}&%PIa|=|UPS zT|Gnf;M}l)qcFWAu((KwGB?W7wQtx*xb7+@hx<@ z{ATYcVe8Ddwzh1jWH?C)A}6&)>>KU#_|Xbqe6SwKa4E0D$e5TQcJkn90zdUkmG0EZ zZ&75qk`hMgQiPZ*%Ff_2R8|hXKW^{43Ga&J`X)^Vuaq`@dZ6Jq7$u=RYK)Vo_rj6F zOoqbs4<#_I_j18?!=R_Sq|XEq2MzM+)x12q<-yeau(7Q)lcJ zEK`Uhih9P}JG*xFT-Qw)kq;CUR_cCL{(`~{1mqugr2?n)MD@o<^n!9>***7dW;w)} zFr9GMPU(Ch?51j8vgm789XiY-9el7uWVnm1PUTMB@V4F2$BPC~`0_wr&}c{5xN2h1 zX@|%p6s|33kIUhGF=6z2G%nMtIE;@x&U3n5zfhd^` z7nWX<;@45DZI}_Y?e8Xm+@_&*)p7YkmHm6$&LiAngdU*A9q7~LJGCXZfzQh zz#l$)7OAQ6rc#B>qBnyWMi5hKiRQ7OOp$7JJj5-j489?_z`A8>%*tJk^l!%af7KSAunjYaxBK0Z!pXhEfSh7ROKSYAvVHO51sbsnfw?UNWb|`0-19O$ z38oUb7X$gR#V|EHD+~FqzIbUB5`Ue~!IK?k0(^qR2lyZ#m?l$!6v4$^^<4;T&9|xv z0G4b!iiM3q9Nu*2_v|-cbt*zdxhz`a>Whq9Ud>=`9Wh8MZY(agR6T%pTH>2`C9@!7 zwT1ZP)2ntgP13Xy)9icF-uKF(_VJ*H@d9aMzQ#LvEzG5q7l`w+`9teyxDyus%5yW~e?xIP+TsloToTG|h| z7)_~Xr7pukOaa+~t`p(L3KV2;G1qk)AFkHEi|3cNspdf6IaOCCOks>S{kvJJfO#hr zj&QM`?F{7N8?7}v$V7(UQO_7shSc^UWps6Qn~3#irJYS~hP< zRZU3{VEXkkYN0bhcCy2BZA9oKl4?Ir2K(Y8M^_90)?J;QW@~2XBuA<+3e|>Esjr50 zsIF_L5=NiJ=}Q`Ij)uwFIZto(lw=wPDYnoEJIp*7V`t3h^bhGNE1`$Y{qQFe%NO5| zr$%phI>d;wM@3$Y2cQ6jQHFXQvNl;b!AvNsriwk3w6;0o5b?`YHEd*LDk_VcJthL$>@zW#Ka5T>Z8=otB#=GQiG zT&&2|VW}2gp#Wkk(HXCX$-_&iirJ|&T}4%h!$OYh{P#U1G%>k?>4I=VD82M)hpjSU zLdJA!x{#ka31_|?Bl38sOIrB#8=eC)>`2*4f(N^g1p3j+U8Y)Ie6kiT39K2w>c{Pu zNNE{2PmgYH#{dxV=dO5PFGJz2z=hG~Kt#pvW7pb09sc@etWt2ASAQUIvez+z$y*ZO zB-se*Ijr>ky%6z?gd|L)Za~WCbPOYu-V$OxTs)r;!v)-zz%qsxxZPj|cRGL~Ur2ZU?2ELI)dlk-8+}RX-3FX>9VPdNbLC3aHie5!dXqQ#*9s-=V z>yAJigxKa4j5Os>5{yxc>@>?_<>+q1aKHS2R>6?^CP_h5MDQ+mHV3iWNPXq`pxc+= z8#p_|F+2(t)vik5NkBM~k(s#x#0PNQ`}gkwiquN!&eWot{pQZKj1TA8o`RqTzXZH6 zf@9?ds$z>6aCf74ii!UojmMEB!v1bhyM5Nz(bKxRI%)36KK5QH;^P$s2wL7$*Z4UE z-6}>YK!L1nE~Xpk*Mj0JPfwKz?$4C+iMdYmvYkU6v z(D_!aB>$Yu|LO^)f#-D2mm9p?b|2<1ii(Si^YQU9GBOGZmfDSfdxpJ;j}Lx|<}nCO zFgSQpPIc+edVlpKic@QhF5n*C zUyHA7^Y&o=dmX21)UGG0Py}BTk0Ut*y|T|qWsZk(RUBaWbI~#6Rl_y`n4qt(`Hb&l z^~ywE&`|z8MTO?tbNe7X#W zboEErWhADWIpnXED1EqXj+LXRDg?sMA4&KcLWH8!JpOU{4Mr!|(KTK5!>wMk4IPN-`9w2Eqb}vTRupxl>hRM5q^1sw( zVz-@|d1dQ@=@MOn6g%lh^h`AUQ*&}TXjJ8|D} zl$e({`A9@V1g;$V=SXG#&BI)2Tio{HzSqvObe(EEl;bOF|3xo4zHq3cgz0-kx>Gw8 zVcj3PTMm)I+IDxZii4#y-9_q`!1H0eoqubAH9KP3VC6m*M4$H*)oyZK|P(^?$@^#8dg1Qel_6h^7J_vXrf zJ9RladKfPLrX#W6`9pu)cmvsPqBHF?xWVyn4Dc?j!NhUA!X~`|cX+l**vqtIeO`9I zQKR&JzI$le_P#H+$oNntPJ&^vg0}6D^56=9=i5KAQ;KA8lJ)m#lr59hwL)T!b1e-Q z7dvl9yBm7BZ>h1o9H~%R?4V8t!B=s!Zp5R}K;r99uAqn?rLfpOmt5MMU>Ebd^XUqb z`1-rh+wL2On$85@f!O{{xm$pZ_;1Rc`xPWy69ja5}iQdo)eb;o2l zg*Q&Qr43Rv+MBz6d-B54P!(E7E?nE|8(&shd~WpQS+5-KPHqVu^c9?4hneWQ)%6#V zNk~YfHS7j|xHeMe`B>_)R}CR7Ni2T- zxY%Eqo#Wmu>K^BpXX{J~H5*Gy-8rVS6dg3Aa0yAtP}&z?kvD7bQ2*_6OZ|1ZXRrNj z_X569o|Yy1*Je#keZDYGgYYobf3C2yBX6dwPQQT(&j+jlxV6v5toyfD$9_8PEV`u! zEFs{nAmps|jx*v`H7DM_>Fmebc%=hEKt8ehU&& zlut%y0(~*nR`$ih`E*FBH2avyx82KUbf>lLx)F$lqvya2I&-q+5DA%0$BvX)>q(So zlOlKuzQs`vZ;*{=;oHlqw^ha^*={2qr-=BX35XHveP+Mp*ZX@ zR|GT;I9jK(Jz?j~q1YliIy$zxqlnXFC}5pblC-Io&Q2pFyQb*-?u^kW$X&bQHEe6f0)u;aP_Zf_Lv?{wSwg$+}v(Y(8v{h zL7iomKcac!K)-2NG1C&*cl5AqB`otU40(G<(qP=N2l=GTJ=ec>gkd3~FUVYTFkjCa z;2=5d=x-5^NOX(*^Ia|cXc+E?E0gRs2r&NCu)o(3LoV8h;k;>)cIhu`xqKg+Cppgw z#FUd?And(7rctd`oqQGDB(Pu$IY}}o7qr9K^-tl351b^?bMjUA3d0&)KR8yJ?#ldM zw`p`e$O*U{Gk;y#m*9;4bYg=~a6jq>N-zbXNlyy&lTv&@fg}WMjILM3qHlNl=vf&J|)016L^U|a4S&W9q*2{qS1?Ag}%Q^>)=K~z+<{H+W&sv*}G zh-~lC$z4I$f+lBVr-#f^3G-ne{%!+;=ya%; zr}Fh&BI%=J9WCMU*5FyGZ#U|i!Kz=&t-0CLUPa4$JW%`o9Z+0XPlsev;`EFkw~gY# zH-4Xfeyl1<1SC^d^V_1Q_7W~c1?TcyRW*z%g(Tz5%EBvPLN_A*?4|4Pte5i>y7u-3 z=VonB9fB6tc=7rf2y25^Z7%>L5WBYb0h6GdvAOxmTBVB}hofVFKeYKZWJNr6x@L6YWcdg@#N#pn!#kA_7cYH(mvHC=<+S)6p`IVSE z0Lxp9W>npXgZ32586zS~@5xBFO#s>|?y$&6n9&+{6FK41ZMDzkiqcxvl=f&mQ0EkP z7R=uFpSZ9VNjYiptcUv=5o*xR$Kh_VwSO|2oM?xnax+FBai|zXrn5>gq%(6zBy^2v zeFEUt3VeQkek>JY{!C3xO`~jr8@0c=`J|3e)$7R+wMZ0nW_lWUaJHw$fF!s9zr43m z%W0_D_8qH7SOR{S5XmA#M}PdFR*-C)=_PD2V*Zx(F(lpm$z{)>j*+HNr?DT$C&u!Z z(N$g9*-K}O{!zCf+G zcE1G_fnKwK9_DCp98p8i6MOuk3l~v~$(eI=CPb)A>70W4A|>qXpw9sC%DYGTOWhny zUbbk2vv7Xg>f%Z&1VDrC;oiU-@V$+A^V!du8rZ1)#XG1RmpW%tby@p@di}MOh4s8c zE|a_8%is>J+YhD_D-~JEtcOM4`zb#UB~fU8{}!ZGpVUpPlZ^Q>%8^k&kg#d=5IL` zgL_)1KYklLI6Lk4+WdNZO`(3?k>w<}9-Tn6{+l|RpKP+1_ks=fUx4d6X_JJqlRw+L zDdgHXxYgUHe;XCUItuYPan$V_BX!?*MYl>9S~f(&E_&n}2rX$C$|a zUKvLE*h|a0E(0KsyZR=1PH9B_SJ8(j?xUv8O@G${P^>M5t!Sz+BW*$wwAU(}()}@7 z==m&OpEJyT$t5}QTGkZck^A%YRrq*y+%SvzXNwvRDF3bEp(rbNTWW|gV8)j+?3X@k}HON%SqgO3> zI+%c{s`-u8vSxz7KvHPddK={|KD+b1?WP4-6>eu&)WxI-4-TCCaCxeGn-EB zxD_>|d#alYThyc7-1{Gk-Wmr?!?-#l>zXgPZ)zRN6WLnCfNU z^pBVolQ3RjB?mDZ6T;OaG;LwDFIJb9_|{79_f1V6OFMQ#NJvQLMV=ojIRb2{d6zcx z!DpUY(L#^fr#(KBkDuyzhZrthe_HL52XJ=O$;0;xzwnWNhd!NsB~nN5zmUL`BdTDz z`toyQ9qeP)PiVO6q??GzOV#<5;|7mZz+rs)SUUj6E=kV;J2fPw@&e)o+=`d@qPu&) z?d;^;qs~36qo+dl-8J1Oqh}|m&sIu72hSmr=sigzBcI)Q0xK`n+Dcz`)W=#)LKPlO zO@mPYvC6bo7R1+(`Xn9%==J%dRKAG`ePlRdclkjF?R_7f^mHZral5ra-1J6uq)~=y z2K9*pJ!1aa&BJ%^PoiG$0R(lW&d$2Oxad&iIiidPy&lSPcl^Fh4Mrg@3{3<@jIZjS zsSv>J?TJ_FKzwiS5qEpXp$bDUGGh)nVPS7BMHQ9ul?WQdiaE%t%+I^v3~&G|<9c*< zSwvh2(T6WQaO3pC-13WzI41;f?wBh>y=NPv2a^VuvFx_T2`_Z{1ufz1Nc~Xb4_475 zgpyuY(yaF(azr2K=^q`Ng>Ewpf*?X&JhN^rAi&9Mb456P%HE{8%%(eby#^3I95`Sg zG`@6Cl9{Q}=h)ENE=*6~=ixFYRB|n?bWX``aC{Mf$-Sye83aXe`{(5{KoNSY+C7hd zl)-suxuXde=jNW59BgiG1_xg&8D90p^r}1Iwnm-e^xN$mv}O15k1nYL-W{wV%`J!G|>qKu3|^Z zEXcn4ye&L6s1H$F073nip|7m0wDM2Rk)Z9ofXbxo-n(wG55V8~5hRNep}GKK0+xT| zSF>O6sfYcVqx)+`V4WRs7xO2axYe!#Jp zA}dq^d@-CJDd*r`Y`BU!;W9GW<44YjCrBzZv0sM(oU~iNegHy(Rpx z^g=fglB=YTyf}zTPqW`i{~R7oFDY1?%+Di~L}$F4P`**}D)?}lCb|%cxZ?ul{)GH{ zWx6iJ6-4-`#RSKo-kmlfVT#mytT>!zRWuhh2qRX&Vf4I>@s~+ZdI~7Y91!A2J*Lju z;4Hh>Bg%>cuCGDi5*a*0mFnwQvNh?jkrc(7Q$bhziu^q5nqAE*hWmv2c^Laiem%>V z#urqpirA$MrOU1QKR~xv`a6V+jR&VWTQp0Llk-=0{PA~U851$?`sb5x52u%x% zaZtvBU>1Ps0IA`Tu0eC>RV0WZ?_5b8EQrT#ZPwGo-L!Wt1_yuvvQWP+4{R_1PEC17 zs{r&bv1u{e#0xt%9^tr$a@Ll*fnCd9Dc!P<_I8vT=s_F2naWOa#-5Pe&g_U0J%#Goo|kn!Bq_uvigL*v`30rQZ~Jg{E9^gbqdkO zR+7XJbiNCpB`a{piaEFZCk|5O&!d@|n#?MX{xEM>DMzp%@V(`$c`sEL|Madg&q%bw z34vWTBS6RAk?6ov)|aVF$O#rXJK=;XB6P@I0q-WSuqQ4AqfyG%LK~;~U*5b8RyVcG zPKQ&nm_BtWHFf z*Tt!r3Q*tZE$uU(8hltN91mXb8Cu1lj7^frqDmD9q@!(aOAiV|lbWVic@t=f|BB%Y z61cP_JnQKv0CK;+IK2a7wpxdnrZ*6cRw!%L`87`8+jBvlhpAA+DH4V|j^lrT#Eg2l zIcI>V7R4-NiH~y4u-hG`gctnGpLy^EO^DvUe0hHO@?YrjRMBNPLN(ypYL#n?uJ#0& zCGlZM94NF1_mljSEJJU{%nkCWb0_HlTVkIiR(4Lt?G1mHBa*-46IRx(HtYaA(ny{4 zMC8wl@;+ncpa@0!wob;W^(J~nCzVMdfcSc>APhH^{4l6RLI#_}u9yMBVpe4rA)urF zL0ZNTwBxUvg?Ok%jf%ZyK=88Lf0vcPu8k9w+7-y6GF@Hn!0YVh9{|=2WWkOEEddhj z#*79{>_(ow-1jA^CWTWAeciC$&Neusg+FX;JZ%aiiBo!NuQ!a-r<6vdJ_}k10o>Th z8*$3xq88lOHpv^(O9{+C%Q$Cq^19&Iv;@o?r&jR80Tbl!bwt4EIU*weN$XK)5 z^s$TSG6JC~(HE5*rIp{WCOua_b6^I1UUVpK`YJIR4T^W35}s~%*HskUmLy-Bp5AhA zlriOx>CE%mdBrg=0)4H>ft+|BVX}M`W&XoNiwT0(t8^0rgncO#A_?xl$o0QDYx@oT z&(0otgkDP}X`d#&maMn8wsUg5BgrIp^J{13J9%`dMG%8CmxyqeelE9&Y1~ABMGyxN zrJi~zmmmf|tG>1U18I%+tr#0O0~gHBv)2U z2W#~Qvis-MRg2~gcnuoeQ>&Ll&L$~apbxl;bXCm|o&C6;VD84dx7Gike2(|4@-_UE z(~ZlRdVb>^KVP7CNu}IlPc@Jz`R1~weU6EQKt%E=BAXMLO!!GJgaPL=J=1>+UT|Cf z65yHK`}5EafJST6*#tD2$3A}EC}69jS~OX0;$7xvF7L*i}+n}UygWY?%Uh>+(aPWEB*Sxw|w>PpTr_KNiFv&Wet@1(!;J9dcXqGCUQ%6 zjZTVx)&qPOD8R_AlPvU*f|&ra%ZcSr>k}q`t9f`zc?Et(Pvoa$XlRHG2ROX%3i(~@ z2OzywSUO34)f_9tVM!U_kEzp`5&M1rDJpZnt{k{AK$bu4swlAAw(NP?TA$puV-6Sz zjiYlGEPXbQGXcc+{vW=_Sw^2LCfD5eW2!cdczO2;Mg*5TX2tl z0EJSfVuWL(n6d#G_LqQpKG$3#xquop(Q;tSN`u>TD@uaEEHhIf%T| z5G)2v=mSr2M*}Tn#{rxB)cyU12wA?V=kDsXRCMTklLAoQ8*qG&GHJ)DAV7w0Jc_+p z@=96le$yw;^LT05VtYHMTF7o0util)cD6%Pc<|OkjZcv)uNZJDk<98^5J@uDYHE;M z{n3=mhtwCn?TQ$!zx-cXfzOw&pG(~xXTWzaM{xg| zly*@zILteg8l)Dln@)WbRRHdai>KtyZacZD1q40d zOoUogy#?<|xg+}k*v2t(U>nWZ6(7g{Yu@-(4iy6DJk0Ambw7?Tf|$O$cd`HSU5^$aul_zXee|8)0Hmt> zXTYX{jE(x&Rz^x;iodjtm;v`AM=RRzJYhz=VoU0a8U2zv?0TN=()^#Yxxg4@dwxun z!TN8=2i$_fHzEYjQ&b?C`zJa7V(tgXUj9MOb0hViZxPWz;P!bVMH7Ek*~WWaK~cJx z2*#xSPZJPA|4P_;itsmrii?;u5OAL-cU=`fp64q5v2?&+d@vZ2`ENO{LMA3a>1s9! zU1_HJD>vxX;mlKh>0e{J(DSze4k^au#`)Oq1pc>{-bjV$$f;cTN8++-y{X*yZ?QD3 z%gR#`1F>Nw@V~9At4KD)j{f|t+)Y{tPNpH{9AwHe8 zO2buI{n z{@PIf2sl{A72RL|HRoT)c0Q+;s*LTOKL zP7Z^FWUI6ar~nvQ0F`QaiJAMLKw4Qj(uZuf_R{~0+-T^oJ!xU#!Mk^mlHtVI*esXB z`SzH){wva(pjMmG{S^|klW#C^o@ME8sA;U$+flQbDq^O~vDKT`Cd^Y&AxDgzoNytq{U&M@pxY9}s-~SpSX`@LL|P zznr^91m-mNiI*QhM7@vQLWUJC@)0zL(W^oZ#JuBSD;<&|R-5&7$Z#O5_jh11!2l;h zs}FCu!`}%dWOj2UN(l<~eOK%55cA8^ski|L71cAz3Y;Hxyd+EsVQGNb`m>CQ*Xz&; z-U@23Km5Aa+!HA?D%?Q`)5_QF&ar?Fs^EY82+Ep3Wwl_3_?sYROrCX9>LXB0Y;O*# zm~O*29+rc0hsa!u?fGSWx5RWNY~H@2Vs>SrxL{iDpIhx1phAZ8 z88<)gx?J1+p&lqV1O`gDUH{nXV6IjmG$J7(SEHLDKZ!utW=IAC$XMS$wwjZz+C(D_ zPQm)miS1p>`@%9o#gX|2Nv65^yi7YN8zF;x#tXjcN{wpG10^sJ7&EBN_Y*_~eIjsK z9r82KmD8*qFdqL_ywuT8>%81|hlj_tSQwK;=?6qe3P)Ti?G-zE#}iNwN|d{Mvq5du zoCdR4FsQ%gxnq^!y4psSm{}24DVeuKcG9DoPyRai)u@{Gkq`?qQOav1dG0e8J$_YH z70w{zjr3ln?ImiLUo1gd1tt!;q_ERqDDI44E(Ezr9TS!>dS}vaayA?3G=> z+>VsKhwa=D@xutPbp2RZ%1w<$Xj`V$W)4hF(ub9l{`gtjKk2nUp1^kKyl!&CCH7T= zUo_VN61u&VnKBYXQ@Wt=vOh<|p1bNg{ms>YdGo@@=V4beVP|{$tQNz&Mn4%(&*KE8 z{J`tWmW80Gz2gqYy@&h7U%%eH9)+5Pg1W&^soh5?!K*qud$%f@a=WQmn2@(6+%T2SY5QE&E^8wREzyGoXDWa*Hi`&U6J#u$U5Rsh87Z}2G3k92 zqCGV$O(xVP_2UEV)yw^3kub*9?$kgj&X7L}E$d6&h9oB2jqlQ#zu&srs^T(wL7J^p-yx{0l8C^i!O?Ck76;+6UF$@H$L-@jTh z7dkv{8V(Gl72nQ+G)}2b1=4$eK8rv0=0r8yeDRYav>Dainz?RZVBqTZfgCxptR~|X z=Mhp6T1@RfO+=LGy5OP{&>cK!GEwuPr1j#F>)_&++{6nnS_6l!BE2vxA@8y(JEAKj zbjpx8@z=36H|IMAc)UHc6lX8r6cJ%#{g(fZ*O8b&KJ@ncD?Li+l*5BVDE!8a8&<8Q zu-X8OR*L);eOe7KhVnAE2a+^w8JY(N2Y|g`MS{48YxmL%{-d1zyb^#Tcv>k+p zTWnA|eVG6FxANxkqMBu0<;1rhjM=Oi85!F=L=Wa3`A)?2*Xj|C(Q=w?ObI<&p7wX* z5q~v>H~0Z#&kI4D+Vu(ZoGw!aDfF3}o9lFvNSBb_;xi&}P6C`k&2^|9jXAi35ZFe| zPA5;@@s9xdya!OX(_{C=&Ll!us&up7RL@vN45&@X(@qBUlR=b0SvP{GZ~OK1^!WPX zN&I-4?{jK*di!j>8Wi*HEOxnapYiVoV9sK=f&x2&D%{0x5@ojBj6O4MO-^6zN^xC! zy44)5M!)-UPt88zKX)(D>vI8}b^xRHDLZ`uhv0GZ766;f>?m z)Wka%c26qZ)*>62Zw{VY-HFh9#lHfj*b<<{Ad(9t^9zl?@$k{3FFr3qL#Q~F^r{vm z&T1-{1k*TS%nS^lOG}^i=E%INB>-wGzh8V_J=(P#WtvNGx{tm2D3STTb)lB^L6-ed1iQo#)aiukku6SK+wQ`;rYxNvW`349ZQ;GaGD_?P9Q3 zOJgJDw((?e&>7(gntt}?s8c}z^;P2ToAunos>Y@nhiI^2u`I+9qk&iwz#Lh zz2dT}OGi`R-+G@McKbY`r>xB;#n@@Gui2$BpwHWm09(RIbp-!kP?Zpy_){v z4=UZs;DFa?$vgVx61|mo3m@baB^-{g;HG9rI7OZH(Z$L<0sC5r8U_FU1?xAz-`kl?loO)uUyuQxJ ztge-T8stiT)G}-02!bTM6krWR%%pOl~P^pRyv0taFY)@v)#VX zo-{T#V(Yw16HrH2(NkIqqMzUrUnZy!%e8DOiBDAekK#RzC_>MJynbV3b>7cEJF!5< zG_9qvIfMqZ;oa{bZg(VosH~jEMoI`nBg*wE92vPupjGNAfDIF}W%m+h0WqekR23-0 z*JVKt4?LU#-FSM{nqxD%TgzX#y+NF>TNkWV^WFoonr&>*nILqW3iSp3O& zw&wctd06rOSZlEO!FWs&pYcVHGOZ_lIU1ax>NP8?RX@$5Ko&ni*w$*IX6BUlaH+4m z^yP%YBz;KLVQ(`KCP9r*qBi)aX5x`@ zOS{%H<-B-yw(C)AEXZrrg6vXKr_&9AoeXjYXi~S`Wsn1|mJgxUsIb#>{Bi{k1uFiXG?oB^kX(UE*9r4420;Q`^sH4+lg7FhfAVjH|9W2imxq@}O=yU$_1 zuWfHeN22I!U`0z?v(xy-Rj~b9Bdw-8r9bW_>h~1)_Luhd_SXHL3Bq77pes<&0wW`; zf6EO8kV9>4t)HJC8LRTWS5v;#l3%s7=Gn~nO-)TfoRGk0EdT1&D?c8!8p2&86;<=7lX&(VHYmk# zU%Y(nd6AJnjZ{QL`UqHHRPWA+U9f>zoP%=$1kNlf$P+ux1wkp&8-Pq z`0xte@y{Cj%NQC?k5{|euJK;?YAM1k>6d$-AVT@70>6RNH1Ihw_2<((Q>b4o_m1HoP4&0Fwmpu_f7=Pfxg5Zo?34Iorf z02Gv7n~m}RM;|w^vI2RkI4v^dt5`YsyUxflvOw{{unHaU0LUK;Z zHitk^B6fB~rYeA`5celZy6@|UhfKZ@A^4a9cdUL1wb<2H3F^C=JGjb!-2t;@ZlVMr zyii|^fJ{UNAu`z~2YfD$T7(ffQCZa#EefiVH^`-&E?>9|K79FMkDwoBNA)J)fYoQ^ zkr*10iXr|?P(GXV11N6249E@QDaHdX^fdboAP%j@-a!$w3lusFq*$zx_Y)FER zO`JN|xyrPxIrJsy?l<>keoO|DJbQK@dmtcW2S_4^*FjvbKLDBB@x)%kzXm>hEj$fy z|BERc|4ZSVr7w3TiG$u_4jvvYO;gZU^UNFcq*a3so;xf3L>eCVvHhNfHVI&KVS3M% zaxl1E0>Y+}_neQ&x^#^Ibw;@jlt##;HC!`KTheV)X4?Ds_ZH|v174VopMQI6wxtbW ziW<@P^slX{@dV8y=N*-wpYz@T!ueSI-gf#SkRK^4+)t(qjNWhb5pb2KlZ~b(COB+2 z6Fa-b(cap!E}gXZSXUPhxbWzwFY?PN4NyKftXhhcSH34GZd#0# zW)~D}04Nao-thY^Fn@C^Q~4WlaGtJd5C}d6_zi_j+9{xuKh`RQ(SZ)@wYi_4Na!T6 zU_5#GfQV-_fD9W1EsW$!?QLxUbR>e3*l*4&zsz3S+1Y{9iRRXePe?cdEovgx1FP8X zRI?IM9-aa)l9ZGbkWYJi`)Gx|0q7hhp_kIt)<%HP2(&LHi#daHEdU5B?iNMxeVIEq zjcu-%;}f_6VeK%!zsK-01RN|i71f|Dmb2L9_vUnceLXNDF;P+AYTK=kRXqZ1ExJs9 zWw0PqoRD}M%&d|mriKEi=wV@Dp{7Q9!okYQdiU;E&}75I(*;flbcpBkaB|XsKK3=o z-DM3BZ0I_snl!!ybrWw{$P*esGeI(?V&jDi)<7-;t*w=GaQlqC+Y}VhPY>P)2e0;J z_kx0X3$Z*xbcnuE^j$R~FHk^dVZmOuqxgyS;OI#8Xfji)K)-Jz8*^UCpN&$CqAwY~ zhSG|?<#+*Z-&$&vn#cxjRWgjx*Vh&(>Zs@3AqCvN)_Em5EKD_sg3FUrI#0VSM6rkoWgi~wVQSZ5Yw0W`qsU;(|PEIk=nDxK0#&>M4q+78IU zwf$as%ts&xQu25|-v%Iu>#4nl3a*rgK=@ZKU*=exwG05&gU83mpf$wwG1hkf5x^(n zKS9vj*53XpA_7!#XJZUN{f%4@1&DMJCx>OjHr#jb{_5yR7il^?IMDF;*^wmf3Pw;# zk;-dW{vP?aOm0PhGY_|Hl3xNgLtks@l_UQ;H;zp;*`_xmIu!#+;IplEBMJB5+vHW2}x6N$3FUwI40#B3@ZqiVF{ase=XjOTn#+ zot{?9)#NJM5|49p8YBc#Q@P=1i3QrGZ%_5zGZ+|p`?SPw)fnrKN^)2tA|fRvrGAkn zj)t2Q6z2`BQb_$5mc}R$SQi#zBQj1H|B9Jnzg~u!o02AZ?h^k(S2% z?GpwgVmD?nT+A{-*hF6n1BnIVkMcGIKH&6El}Skn{(AYtm^84Oi-5rLa$<|)H*Et= z{ZByy^m)0!@Xm3I?1MP)!WYv{x`ligm@wcKDK!BH_)W+&2p9+SKvyYd9|I2`HG$vm z!~7sJ1?=bawh@sa46KTP^UWLIbCF(>Jp@g}XXK#&P+E|Y`rKQ+y3=%d3w&s?fmBVV zUJQ)wEWGUniW5@={+A~DKmO}K&K;=G|ABM=kLLV;ra8uPp1^woC}sXDmd|mnRV*rH zYHA7)h6TD++ig*t%_EvN#E^Rc>TEFFbGmrJ3>YS}XEy{vhC}tnIj&57psb>D{pwXl zVQGLb^vi9A0i3pt3~6@p^<@GMBJ_zYJ+v?oC<2`}!X6|EQ()(oV_QMOV{a8iL|L)N zSLi(pL8u|=v6q&f4s8EAXm$o&^x(Aw(%nNwU)iC2Ng6rc=*?E0H6%HJ}%=M z2iy=<6ag`)ANfQ8fbz5LcK}Lc#_I#S2&f_{@8hB7%e0b+QrC@%(i4DNY)c!p7+$^v z8@R|unUFySzDZ(!(=Nz>)8(zbNdP~ixL$E)*!@MWuQLas!2E{JbY@X+|1Ne z$a)|cx_z*iB2IAuEhF<3JgP_*XsL9b?N%$cO5{M+%0F-s0SV5KU+-wI5)m!Y<{Nl! z`3sEhH;_nUCa>Ja16ENSwFG_^XA!s4~(1JjWcxR`KWNK zfCnXn>3wsW7M}#?PYMUR-6V4Ni=VF)aLK!Il)u^@0@2F1v4Z$b_`Lk-(M>)U7P+9o z>h+?hR=yg9QSeL%QTX(ASYkjXu5cC>7WJT&<%;NV)V%j6QercE!02DYgGXpxX(V$JF zKUV~BVM9ab{ikya3(GbcvDYCFRe>sdUDcrRRkC}=dyfl_74?f0x`xyr_+>cJN!r@l z${VLVJv|LLg+2Li9{cNyDLxa$rb>rT%dO5P0e$oPx0?r3swBkMR|sgFwTX!?2{120 z=jP?991^1x#jPAo;|IEj5J|y5O+Ew#8TTEwgwZR=%X3`bn4J}{>`4Ph)zZ=ea%>(Q z@j`&0CLt!)pmtgvasa;?V-1mEm})*hsz*Q}{ywVim+-U0-4$T1bZb3_U2YT?7XzdB z6uhgTEH0!VvU#-JmPXvg#=(Ja_SGYWMM_3aX|s~5%1XD@p(5~qucb0PQP%;T)7=b6A1uyFCXrm6uqSk%KY*Z1*3_DpTjmA`!mf`@{$D?2KS+CFF1<@CX0PS_^^_d{TSHUhzO8AY$r39tj%y(G8qZFZ+BQ2Y<} z-aH)3c5NSy6s02bkkDkxSW#xTG)HC`E0WAfWKM1rN)wWdr7|T+GB=@;+z~P-$&JkO z{Owmg&$HHgpY^=!+qU1g{l0g5w`cvaT8rCtpVv7Y=W*=Ee(Xov*WFMDButl^!L$=f zaN^hhn&GWCDGJZ$F?CQ9lN1&{GLXign8FF zZW;1GI$m{|;QHN{zwtgTv*{75U|hZ&yATQFOjs@~C;l6-?7-(NOY7{Z(e4_bnQ?Jp zVPOdg&Aoe<5udHMAhd)G4DNX!myNL~_RhneBLEN;6+aYpzj*Ot^mWF%(Vq7C`I&+9 z@3hR#Ti-`-xql#9$JqE*e7uScB?0q_Lh|?>r|;o)VEAhtH4F^uaEG78TVV*;vN+#A zFe(I>$W1dXTlN+^va-?)1R?}`smpyJ19s>Tg&6mxAJ5+n4-fl^ssIC(5EJtPgxVGn zkBJ4yk#_g)-DktPdU~Tc8=R8nY>2M>=5Nn0U^hTj%tvR+V|MbCn%WBl+{0ojSUbQ6 z2q7y|W@m9_`ppjQc5@pKcvl7e7r%wMiS+dJHc=HM%?dC^co@EOZCwCma}lI^jC{ZJ zt_mZ{G%FadkSsCHm^ICF>BK`mynkP4I)yhNgu%2_=3KLeN1&f!{>-_3c50-1VTO>m z12%)J;oeen8MwFFTv@y!LgK^+5ML%)Sy*mF7(w{*2%`HFCz2N; zae8+Uh?}VX&Vzx<+qa(uI@{(wQb~+S-xrC;;`!-bxKoZPv-XH)A*g{{^HfB9X7RKw z+#+6Cdn1df!x%nDpY`d7vw`a1run!4r_H4_guOycB`YhdB2v5n$iaD2htErvEWv&- zEAdsD8^^0|v3{T6el<12tSH^hXX7Tbgp#$C839xRBx8XZ6Vw78O!IGdKG&+ezXX5d zvvcQb_;eXt4LE$~r@C+qklH?eslon3RTY*vv4kvqqH}$lvbrBm6Rui|!n4{fAMIts zxwqW2>%0%JnDFU<7~v%q^n4qEWj!3Oc{+@@;`Q@eKze6J-xsh|T8_zt=?=enZxS&2 z+!4d)2+%JvR|D;+&$KYvz^HP55yK}qq897dUE~4SD1=(Y_!Pa>IUJre91kWRCKY~a zOrc<3LJOR1_ft2h$4x1-_&?oI9JXyrlS;7zNnA&aK{7R#toV zyh&Q{31Z@ZIPP`)80>X=y8YX08?hYWVPVJ?D9`mE9gagH=hUiaEE=QgcV_BGb`=1H zQ^)<9F;5Ys^E`; zD>dC7%s-wvyMjyV2+}%0^mt6CJWj8yh4cLC<;x&_U%ZX-vDX*Em8Yp4@2Vh+w;Q5r z_MfGiZa|Xf0E4!#xZa@rQwlWoNN?cj^Yvaf7x?nwr)Tm{*PXUq;zH6eD<5Bxb2}+D zClsggvDIFnlGX33ke1G~Z+Hwq$+OxA`%cX8;oYtdnD`>NJlL5Jp|%v4-3HiZi`W9aNge z9vygBMZj0@-kF#9&S`a)H=5CKjC|}0M`3gwSy@rh8y6AzimXH9;4Q_Lme$$EGBJU# z=g!RlrRNo?C-4^pOTUhKYiB)WqOYm`XrkZnx4jTVba^-jeXe zgl1FM_wUvxPSj+#V9<9A8*fBLY67roes(G_Fc3ErTtUEasN?~0E;g2rjqQnL`KmFB z(Zj2%IrqCdzJ9%N89HqOWauZ74tLDHyeVzTAc&oG=4VJ^@)Im?`R8H#oH{iG3uv6{ z$j8j=Jo5b@3OlS%baaI{9#&vf-W)t`oE8P`BsE*)c)&Ai9z{ZHXJ4D*O{`uCARv zunCu{UJf1BQ(|9rN-SfWU#WfF3vkwjMV!(9tpdz1BE9aHR;Abqp_Z4fs{Scs_VfKyssrDKtVh1CoM*lN zpHozNNUG^1GhSyWGIy7^?V|m2!Yla+ zt5?#kK%D7xmrb2Ib%OPm$ME;s|A#}Ns+)+rxZs)ii#-BI7_f8I?+&$HLhcP9$WLVC zf8^1CF=8R%27T`S24OUb0di&g**`WTqu#m)WyAlcgJ)3^_@hU_zvRUKKI4BIzklK_ z{Cz9@uemNIR!_s1Ms@z}+iOw|9evzS@N3Mc%IbRJ;1o=VJhnW~?5S`>S~iff<-CmT zmLXF7_3M_Yv#(1_xg$iMRxE<6vvkio*%yBTe6=4@#ANv)OwTTKpso+@PK!k6=OaD@ zn602PUb%|o#4j0H$Lz_mZ>i&Y9qMTpmzS?z9o)of-};>W_|1a>6y8k{pP4NQH-3G% zs~A{7zx#eK$!ldL_05hv2C^Hnh|U#5&P;RNzJr^g==!3x?p2AIOG6YgSk{j|@^DWT z39uI5*0`xpg$UYq(3+LwM-beb>D;^oakN5aw z_NihYuYCUSo%~SG276f(*Av~>Zr@IibG~E-{N$6_wd>bw@tJp4_O9A0OnOD)Jrb^m zFQ)gi5Wcc-Prr3`LsBS<==$~Q4IF1UL+K-oT@CY%Tyt&ig|Qe;C0@CH{dz?C#)h>T zC-3!NyLxpsJG+MZVb}1G5Wr16l*E8*2Y>Ta;pP8KSp3;j)z(d9Vr0DI{-U}|Pjn%E zn;nO^Vrdqf=d)+GZ{*aQN~{301M-!k_wx56B+U<<-en@cu8&lpYoX^3FcBl(*%Ml~ z>uyz-wquvRyPvzpI?@Dhh<%zTvI?NvW>bjm2rR$)ucDTfTuoq0{9|d(Y~`w`^C2C0|W~KfXbfE6W`xQ zkIJvH^>U5&=tko4XT~8@mx{R_XGMsoM@Qbc<#_aKP`@6VTI+?oNp@7`Jr^FVfAi&bIDclJ ztNfP5^xtpUeVBP~=lj{~`};FRnG<8)2PZn-4!w`=>9Oq#>4~;C-)DBJs**nQ#|{nr zk2s}(n=jS1*-GnOTZ%sw_0IdKDzr?pojKD|vY2Y1tx_8>=^ifEdNx4Xt=`N%MT4xk z!+TQ7a;+@iMdUEw022s3}jlEf(RYdPinD}B;!PKLS`%c zqNAgec)Hv_M5Ft`2{qqdmYB+*-fpG#FBcjZyn&2|Wr6gfeF@bz?qOLfv+Ybt%ac|y zkXI3pdi+n1TDfxNEZI|EKjKO&*wAmdygDNc3k?Elyh_gpY}(+A6)&PY_!X~n7{Iq|6=+{iw`RElXq0ak4i9*HxXlV3I6>xUxXkf4Bn4r7|Pe) z#*f?C*#)oQe#zB@#8lgvPcs)LlvWI!S9M-y3b0jP{+4uPR8*Ii5TI#@=7wd>&U*Xx z9KmBZ6OT>$Zd7+ZK4{v~+B!r+y|E_qWnX=en~{WZHcz;sM3>0P&qp>_)P2lum|Pm2c>av?$|r1P0jk2d zi%dj(7>XvLgYY$4=i%gNjn?T?9#frBmj*0>d7(@$EX%7u?|x$9i^-i~bTp!L2ASrS z3pGIex6SM70?HdRb8ji-DnPRkCO-L{$Fr%dbO(UoI=wu!w1tK2OQCvek|{GgSyP<~ zfexCQ_M^}QS&U4l6{vIS1FSPSxf`PX(8%@)FE0>KjaszuQqI!;+ncLS#Z&hxI(sZ7 zRb~fY;rCO1Jm3{yx^&5BMVSfmNpr~1PJ~{dEW-F70dI(|&NMPGaH~?9aOMPAkLX8( z#KmMOr^OniEC5auqXnz#F?yu{O&gJ(?A$*cZAl-*FxzXf;5qn$eyZFTbbjQBrpi_3 zF;UuvhVHPS7>G4!S(xgWttYQd$CsV1QmQZKi(iouqK0vV&?e?q-v`K`e2}5&>^*~& z$vP9Kp&73SwqK_s_~yZb2Mrpwj?>W2u_Al3+uOUa%WqZwdM(AFojyK33!PH5 zbgIu6VZYGqu?hk%vU^D)XVoWTc5FQhkIo1jS1=g~8aSXx;A}`jA@|&k^AMyPUbm08 zfP0aDP*6}(QWB9oiBp`4G;zdL&AY|=0QB>aCIN9>UO|EW5TiPhjMKnO2Ev(>CkPX9 z9wx%JB?PG+*_f3=d>JuPZnAr5896pTYOn|7Hzm+z$-{6?)erk`ZgtWq$Zy`pgq6!M z0ZF*UK5I(}9Gkh9!EXhQ{LJZZkWt4{-1+UX%$~j0p|MkhOU0>tU@6UFJZ7oDRc^>s z^RpA%xYvM8F_AO*@m>;_Tgl*283ry`LAga2-!o%X9}uf*oD1f2e#5a7zJP;f0-3I> z{duPH*#$@IDmfHJ2sLO)@0%-Vmtt5eRo88y!?K7H%TkxYkeKWE^mDWY#^Brj%8FSE z7`>@0#aPG%+oIpy;{S}#X!R_r)m@J19fe060vQ?^-cj#2fq8he7%xVQ9{BZ zYp2BLSrKY5vrH^&?)jfPio@{g0mBNo6->*B!>~~lY!>yjA^g#f62)pK<+96sX{+A6 zed}Lmvd+!^*cBYL0J`9l0!PpeuvJQwb}>wjI$<036&};oz3yqCtfb_FZ!NZJamFG#Klzv-T>(R3vT2`& zHa`2yHG8y<^OTB1+<=0RP_T65_3P@)i;b81|Mi56ya<`q+(we{bF#JlTI+l*?h8GQ zIMZLhaf<%8+}oTko^ejG@dM?1J#G2vMitHyTOj+@0cNRPc=IsUOWK&69Y5#Q;Ob27 zm6{Rn-kug0JiYM*&D^U0_UUrDS@0Eky9`?{WQaUcxsl2d+)C3aDo2v!q%XHIKORmU$k_u(gs6sN;b zk&%VBbMrBbgM*Q+Ntz%_FH`4s-H+XLXJiM2##j&96uzkBCGqo|w2bnHsb^w4{Ar25 z1#S#K!J~l-XaW!qCY7SD4sMmZUrE`Uz^9TsDP>bZA|AS&wS4owW+&{&um(` z)Zy?2e1>$CL|x3hiLAWnkVpiXqPvf==vD-Np8hsLvX|JaLi=wgGVP4SP!4U>VEHjCH8l6;QUBh%%yuPrW^;nnb41(}~3U;B$h-CG)H z9k_ZA6fPU=!9C@^Qs+0`aAMiAWzFA^O3OO(;yhXg4kFw`h-UBLkdcbcC_%A(ilnb;XqVz+EHGelwO*Z;qoY;r;Zar&?NE20oFD2uJ#l#Rsp5DF&F_A>n z{)y_dUR*l_XlED3QbvSo&ZeI{C#15qJ$|wxVz^5ma?^HzxY3V_)v}W#ZG31dOeSUN_pr#wFiY~FtLLCc>h0m7ZO7oK)_yDku1yD8C`cAfk zBUPJbxPU~FqWt}uDbS@*tOcr(A|lAyT^rAwIfFF{yLd5L(PJ3>rqFmYMR?!-{S}Oo zg;&4_xOx>{f4f_OPoN)yFHnm{Zk_J>kdcw0&z4+J;01;WLJ%M~A|fJob?G0Ay%Due z-Xy9xV+hFMRky2XNOfy#vCYRMkPwK5l6UW-4`ywB{`OXS|F2(<^>KUljo@G3C7KXV z(bHqI{E-}G-g8H<#AyzL2J^=%e#6yXG)WqQN zy&ih{`X^K5-*jx8mZ= z4Grs?n+v_ChCzxtWmb$%n0xoWMKok$ViFn>qNAg;W>qj|xxn4Y#f7MkYiepTlQ6Wi z>wR*vCL|=p*u(^3-i@0#jkL8B9n^jL-3q39+zN_va?YUC3v&a0MiFW@y}V{l%keun zIPmfDHAaC4)6>`IgRnCD@ng+4Z>!_Si5e$RfHE@ruI^qNt?c9J>+5T2*@4yll!NU2 zrX_jk%K~mp&e}|$xZAf)&CH5Ub?oxBHruF36#@x*iAs$h3dw%$o5RGu(fFr*vsiQ# zjKz8$oiA~y%C*>EbocJJp>lpS{(Rj31!vO>ls?}_ckWn%)z&=5uCc?t-_+JNwV!FR zhIV6PBc|?HZWFskN_zT%g9r5`^F+kNbQhK$VmpquhTy7#_fRA8ELzQwzi^CUM()*Yx#HB z7rhHt8aLvPxrJQ`A|sb&y!Y_M%OhXRt*xzP%0M67`m7t)E|GA7l1Kl*bW)G02F^b$ z=zn*0SXflFTBlw<%d_zAT`N?@&8{g-5{65)AABc35+zec#oiv?-ii}mxlME6wng5! z0fwJlcrV!(bRNWg-kAD%#NkhTGapve@7udqyU0}g2G;sy&Al_OuFYKcdJSe^w`G)- zeIbJ59nsv}OeT}ze4R?1m}a8o_nXaP`^ItD3WmvtVJ=A{l*18pN6CK+;SgHP0d3`Y+{WZ67JsJDb`F3{J??DA|g8yJf1v# zI$U81;{-NY>*vpBQMw7WbcyLWQhY>2aOPx{5EqW2y>+3YDDXo{x?~l!T#sCetCT(!&kW|8B zAExgr(q1T(C=PBn1LDF5o4SgjI|>)T5aAqc)c<&mRDx4;Kh86?pumJTP?I{fO?K9p zr|%KI7Bz-r-zKnqVMGd-7uD-ON&eKeDtNuP_`!n6@#!@gFWPP<`5ZPeiMf5d`Sa%o zkzchSyDJdj1tQ|U*z%F6o+1(GV7%o@OG)82^~ubR+Ymr0RlPQCjnW}pK^yeTQe z7EVmxZ+3V8@;NZRKqUmjG`m-he^v0E++0JU(VmHeCdS5W8iXjot9ELCe?OH9xt&JP zhzHzY3JSn*v(IjLMlsaa-zX#mI^8XySyU9DEg(p2==0JWk_6&&UQSM}-K1k~K|;b2 zG!u@EjSWd!qvGogMJb&1j)0t{ejOLalWV%sU$`At8JLMl!XXu%pq{N+(6Prp|3L9d zb{I^kuNgtNzuMZ`LXx|?+`_6f&*;b(?|hg#RODh}V_PPqh)eR{+~>wR)D}l~505<0 z#n-`HvMBP@7a9#o+8+A>G*xIT430d;vH4d29DEm~V;~gJ?B3=BN~PHoo7ILVVE$oI z(TG@a@cH7|^OI<3DFI|ntEBf~f%4>+#TM-fu^+&x9LfQU9e0441Sfy`wn^UIu95&g zy0FcuRGXMe4nDrvm>6(Z^=~Imlr~JBs`4^O6|D;0x^0_unPbtILC3aBVY8DR;oO!r z_qF6?xGd3FT@MzCJ#a}Ju$*A@9wMC7{mYfKp7*)AySk2gdDeEi<1ZdWMDr4cX-*&paVRne?2MSqkOiMcg{fwTsE-Y;P@k;a4ADeXUAh zRFGRiwnYWb&_WmxuMLP0_LKPrlX0=F#0`_0D+$MDzMrPKWx#kni`%gO5m7F1z* zDdNwl8D^S6Ddia@T@g#*k)Fl!XGG~*Dnp_WJLjzkf*BOP&4BjLxD;If(C)|Pzv}}0 z3d2t2^neMErr8*HHgT@yLEH#wEW0<1*I8OnPhQ*jDa!1zWC;wj1m zr%}%5Io*9HHE#E=$03|CR%l5~9ZDS^NF|!5D+P9>O802`_}i(yHoqXKGwp)H4+O*u@(f8qVB$ z87>y`7{BUc8}m4GCdZN8LFCM56b;tu06m-;u8;(Wv1E3*;^|XRoF3-nSKPDp-$28EhIWx508*2A8g*2F$$_8bVl4Uj@<;*Sz5BY)4HyYPFQhkoX2lCy zAUc-RYWGH$8mrlbWd)FwF1*jlX#nIhTU%Q|rx+jrsmp72d|kF%`v%x_p5W^NKL_v* ztkKqoJtavv?o`@9&sPI=>6@{R`B$HTqto6t*YAF;Bw6X$YlbqTg9k6=ODaOEPYSJQ zoX9K@(s0)I{sQ5ojYt+|W)36cM!Al$Ny4%*;yZRk2fTmx?s0>CbF0Ba(~|(ULWY)` zJ_V5Nwp=nj_N7_Lvu8#{LYv~_A|gr+KWcV~dB)0|tlr>INT7|tJ0*{2Y5%M)Cc>H*{zhJK*oSKcH$HJ);vRx{!Jxm}y|j9e$CG^6R(Q zU_eKY_Z`W2Ar_KU>{`$w3EUA`g@mWrhc(sJYgW;7PABh=N_A?vGRC{Ms={-5QB$8e zU^aPGv)FnMREe@_e0-r@J}3nA9t50co@1%~XI)$#OE|8jTUgLjbc~CgUe?~gebGe! zbk(z*njHQ|fd8;R%UURZYO^Cnpj8-+o7dRzy-pY6hDNyWU5S z-1`2a>Q0)_p|rHLTHU<3`%ZbmLn1xg)6hGxxo;Dnc7p05(#Lj2BluNf%**v>wy#0i zVtrZKX#JSWb;KIHH^jebOMUR*Sy!AF+L!wPOhql>2u*R% z+0>^ePU%zg9HMUAc+i*g6F1k_*Y_ya*U^!2YE|^r59<=0wP1lvU+MmwCr=KEaVudec9Vq$XkpRb@&-L7tPPH|&+l5qaBGis9|O?%vINAsDC_z6vi zChKhJvU#MK>|)l8j(qy0SN0hdiGMXMNff(S^D$YS!BL^a9P@|5Ax(V}A|mLK*9sXq zBqWdm!9ks2_temqFfTOH(Ybc>=5`e9e*gYm8Lu^dj%+@+JHUUr4-hd>%pg_3rjhdC z0kT^3^W(WCgtbgbIr-uC4)ee`3%=2pjFO-G+(fT+#CBC4HbbSgbL4=`&%fj0;7vJ)-6mjvrPlQbi|$0le}bZ%XHs`*49=KWPMHg zQ8J#2Lp$K*Gwb%W+kA;3HASJQf5Q_^DzZ1&dY@Rw!Dxgnky!`DNf&_lfX6Fi(qj|G~$FSZ}*Ta`+bMWS@}zGe#wi&?c46AOMA`Q)|F=kf|3KL` zvq68Jea(0lu(}^@SRZblZZG8@@IJXiLkk;D!=}Tg;z%wA%jcw(8x5pu$G|K zXr@ybOK6^6SqY_wS0hbc0T0u4<8hFh`o@uWLGEzu_5J#<_X1yW?bp(pXdFpk8ZKc4 zuX}~T7YjrEk}O+i14{7N__$-^lauggkWx%TZh4tf$(oH45)y)f?YO0zu)fIDOmj@5 z+8ep1ve&P%xMgK!61Wx@Iu9cWnsXUPK~j=fys6yP9FpeVfWZF+Q-}Gi!DMCKC-Q{$ zPXS#6GY4rf+M-1nhg#z6?bLoA&oRdLNSH!gVKhbezTV{6*ci_AEvcg~Cx<%Zz_7Kh z_XB%Npwi&)W@KW*%gI^J zg$!51<2L{P)g5Z81XG#rO?4y+Mt6%JqW1RmC_0@hLg6}I2}vFoqzW-w1#kjTaE^2> z5}oE}+m}eNh9se_KsM+@NlEA1vS!c^U>=FWDg8HCF}@N*ckDM5wnJp)fpAEsJQB@q zC4MTB$P*8NfQ^eQ7_#~*3$Yc)D#c?n^Sn3xzCfLNs5TGcXIvc;vsdEgzQiK>FAIoBm*}I2`!AoFX*wA_KLsyHFvUK z>go)};?}MTRxjcY9p6!65iGx-uWFzxqxh5N$5!E8+BYB;daHefQX8$=eZ%$7Ss7WO zn!KnaRMGMWR742D8U7d|@ebc|vTYH_?QwF{AC?lWv^30v$bU>&ktPUYryFfxYj5AS zW0sb+l1qB>Gm&20zrRD}$YERvH*UE0H}6os_w?yF9E7}6H0l^W6jeO8J$gv%mr~b0 z0TJ^IzLWyi3!dWsj)l1OQOUl*8(^W3=hm5Wdi@jg^#UKX6|C{RbE~rO|wIzPDNvN2^KDQ z`t#xHwB&d)YpFipKnHTe6vV+@4xR{W@fs+SEE=tbqg{lVMRpgu7bS3r7t_|$8kNO?8MYC#&Vhc}F}a9CM~HqBQsVx_Ot>{#`9d7@GF;ttv;IL@n!7qe!V7f1wP2 znO3X9n#)TVIact=>pi1zvax-c=&KbTdVP$p80_-H+15i&?^0bdy<`x$Kwiy8QtmMq zojpC>2E?2Dm#(FBBG)vu)R?M&ysJKGv(Y0Wu?dUiar*QKRE}!hePAZT=F8*;4bqLI zA*BHMi-t}Y@FW`S__Wlz3iUXFOc>)pM}JHH_ly<94A;^&BKYmB#3D=G3V1kT&t^?`DD-f9<~Erj0}q8IM5w+_KSXrz4Jy=4h>b#|WAU6 z8)O~$v0+|z;}wp%@a%k#J3UmSwaDv9pPY8~8F?OPBlb%RM(> z3$$S7=jHWM_v81ZCPE{pGES)ggbZyq{l`O>9mJ`n1SROU+Vap(?MSKt-bU>%xY3&3K4BUFMYP^IVc$@ z<-MUu?ZA||Tpjx;MJ{IA9^fs;Oj3DcASHg5?$ha>d5gW7k;_{hAIUJhfvj!R!8N^s zL~=7i=AY_WW;YbY->*f8sG^iPmnwa5EtQ9EDe})Mi;TFI(*}T9$fR$iJruFMmSm_3hg;yY!Iu8XDi66$@$){LQ`Y#KmyV z^Xh*c58fxhEDRc}>QxOh=1K4!=Q&vnCQA?{ir{RQx`&1MZ#V>T$Eq4|in-i9$0TV! zdkef$a~Q$yh>_!UwPwe=r)`*8n4kgK>>gdB`}Q?oXLgmITYV_A=xEqj+F1Sz8oT9z zYR^AmdG1}NuRQ!Oi6eZA#~eJdp^Q7no-@FspSRPU<$Z{gv_y_y!PkS0jm?HWj8B21 z?fgn39^nHhjfhj-V{>&orhaIVUyU+(UT!j8M`?&tXRGnS7knL zw;q@(!LyS3%_UFU88Onm#;`*>FWUYqABnr#Xivx=Xir_n&B#a;TlVz!W)x`rSV6ZGv`!T+ z+nb9))yDEMh-agraHy6rg^0RiZ{H@s4!m@dE9S5JO_sn){L8Z=asN9!_eM?4#Hz%4 z)}e(Qy2RWDdw_b^Wz?4tQ~*_`O@!pgQDK|61ToiOIUDe%N>#?9$;;Z(XYrF4mf;Zx z4+!Q?|(hnb`V^U`yy)8 z(9lr6Rl$>W+S-KHvg6GBFF(H(Lp=G7 zEFto*IIus5=Yqn{B_m)T#|SaH9uvj4>X!OjZzeF_WTW2^U=%Fx4ja@A3y+AeBsi$tr($4@Ob$jH1m47EEU3g`tudoY0?D zlK-hf$e&{xQG4@K9dCOz@KBgo&Th6Nlozvw$n#XpMoraVJs-iC5$rjf5Exfq9(7Ek zlf79U-#c91^IuuuDx2A_2rm`~E9whx7R_D>E)Bua7YUcQEyE15GAt8k`Ga81J>B+& zd6;yb!r0BK@t$A_{l!MsJx}4}OY_JGsFNTB_zg6^z|69n|&zjRTqCJPWlN6 z>brf!N#BRc(Vr*%Rr_xo7XQczjO@@dm-M~s4{dMB@#AEG^uAa!@U|Oqi=BEQ>Rv5q4kvdRlT9M zp98e1T%81hw6Ro=%B~@@dGnJ7dx%1!RtPtsz-aYVOEn(+yVZphbpsP2a{vB7i!Euu zRle%>4qbiyvA06;s6183T+$&8|G3Gq0Aihc{Y_?5?#B)57e07kgU~&giGLp04ZNvQ z|BjruSb~Eb42{x6iOS(?s{D07O9Gy9te(Dg>lQQ?wQg;_*gj-JD;wj08czsfN<>xc z<2SANv4kj%$XflUS{U+E3IsY`5bnXRG``3ueeAW;MDxhtD<(NtR{713KytC$*GRl!2f6iH)-m0A*om1cW_HyCC)lInDT-%QDkb;H zlg$>zrtEcyO?^;s8^l)Y=;+WlfLMbC>cUj(HBNP4%E}HO zwc3L#AXu;Yv;85wbijEf)zE6z>A1K!yyfnlJDt@l-@jLclHxh4QHs5~`YTjWpeaUK zUS+&WNpW%OKHd%aoD51Izfjx~F!zfOa*d%(fS@5tC#szKy7Snvb%xcu*Lr*dRWUc> z<&{UA!LNqkV4pq*?O!AySU5^qe6TAfXy}~+VVr%0?}ryvU>`-jI{ZQO26=v@N`)Uy z>nwi&d}z*fieIA|X_}mvC@3ZtYBz^+fMai!bQ;*mar<Ddt1m_pbQnnnn>=@- zUvEb4vvX)QF$YaggLeW(UWgM{?!E*(Vm@g)%kO(IO_gV0p=m- zVTa^WXU|mg>8d4s(Gfgn0nu+KaxW$)Ey&}vi9&1~#u2!fB(KZXEkB4q6YJDer6S5h zPkMv`1|+9zMW6e|k>ryuLpXW#S|llTC4S~6LURbNq%F2yX3<4ZX{oFp1avN(2%smBwfXbP8B@1o7sqbr$I>TCz2m@ zRmo(!vd@URGw52#UJZpFTimB1r+w>@5@Rl$G3RHWZ>iwp^ORSn|H!J`m0uqy)G-0DQiV?b&K4|Ea25d-PLN;{7Qyw=s9#^dV=#oCfw z7RQl+K);jiIrZjS{L<>E%p|8vYqA*I@h2~@CJ8W5v3bv^?kU-tLa9RMAnEb%nTy{1# zk67$Py|`6;0TcneO0JgbRz@{ghx&*=OcB!>6TPLsf!OD{iRMAK+nUSvB`oXfM2LSZ zObLW+vgf@fQF5H48&xp@&!;I3{VI?kyd0Mp|Kcsnl}h}OWiGbaNFk38cSczZXO^+_ zL_4N2d*(MJj-10aMb9P~LbMV&7EiNfc~PacW!T(I+rm>WJ5m$(He1w<#5ffTe%kWU zKyY+tfO?ZZ=8I|T(NFX=w^5FM;<+oy5Bj-fpZVtp>BY1+hrbw1?@FAhh4QuRBJQN@(nI#X#C2Lil>qel;~3%gvIK;ig^Rc!DU`LszW) zbtt*h$iyj&sFsVR3^eCmmyW!8_2|ZT-^L*%{EjO|!CfNn!`p4>1Io$G6N!jIGI)!@ zvR}nLPV$KhKQquKi2sQ%VJ!K02nopl?hh>TTsB(TBh_h;bih#(nU1h5=#jd_Ubo}= zx9jLisw%lWg(CdJ7iIE_e)6;-Bl`A?=EI{uBi~1BuqAig&f52f{y{`F+fz=4BB>j5 z?B^*HyBP+8g<*v*EDd+q4{s)lLE+1OHR|HaH^1T!Po}6*lM~BGPq5ieb_P0%V6Aiy zR2o(h+nhALoosGAi%&hsViw1zjXe@gLa_X7x+Aq)fVoCrQC^i8#i8=^2=65N4H=kI zBiF&tDE;I;wjyytW7A!5{F6(W5~G567(XJ2z8}GvpA1O_E$M9*O6pXftIN7O@C8Ho z)ZfGSPqR_S34AH``gPXTtF`s@lamK=m`h*3hM4{^^giRt(PfXQTUd*}N|s@`!c%sC z5<RjLr0#yAg-o^#u1<6-?^Ej=!9U-JxNw&uH-vr+O0834s;;iCkZ5^&<9Qcx zam+|8NgRk_YF&94H}jQCYk8wkWh=Qdha8wL2`}B55BemvwBnMIU2dij>Q*8ICmua| zgoIZreL;RcL}gZ^YK)&BaG1x~cE7JBl;FU?%IGaZh9g8F53+F@8iAhnNGPJH5;Zvb z;i%?ucXR8xwR#MtkRetjXQ0%u?&Udxz0XwHoMjh{-6wOB-ZSG0^E9iFCf*ztxTe!3 zMRjC&I0%ோ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 npx ts-node runit.ts -- createMcpClient + */ + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; + +const mcpServerUrl = "http://localhost:3000/mcp"; + +export async function createMcpClient(listTools = true) { + const transport = new StreamableHTTPClientTransport(new URL(mcpServerUrl)); + const client = new Client( + { + name: "fermion-mcp-client", + version: "1.0.0" + } + ); + await client.connect(transport); + + if (listTools) { + // List tools + const { tools } = await client.listTools(); + console.log(tools.map((tool) => { + return { + name: tool.name, + description: tool.description, + input_schema: JSON.stringify(tool.inputSchema), + }; + })); + } + return client; +} diff --git a/docs/agentic-commerce/docs/fermion/example/create-offer.ts b/docs/agentic-commerce/docs/fermion/example/create-offer.ts new file mode 100644 index 0000000..a8a58f4 --- /dev/null +++ b/docs/agentic-commerce/docs/fermion/example/create-offer.ts @@ -0,0 +1,184 @@ +/** + * Create an offer for a given entity identified with a given wallet. + * + * This script can be run using ts-node: + * > npx ts-node runit.ts -- createOffer + */ + +import { Interface } from "@ethersproject/abi"; +import { Wallet } from "ethers"; +import { + EnvironmentType, + ConfigId, + TransactionRequest, + Log, + abis, +} from "@fermionprotocol/common"; +import { createMcpClient } from "./create-mcp-client"; +import { checkSellerEntity } from "./seller-entity-creation"; +import { ReturnTypeMcp } from "../../../src/common/mcp-server/mcpTypes"; +import { signTransaction } from "./sign-transaction"; + +const context = { + // EnvironmentType of the Local Environment + envName: "local" as EnvironmentType, + // ConfigId of the Local Environment + configId: "local-31337-0" as ConfigId, + // One of the pre-funded accounts in the Local Environment (https://github.com/fermionprotocol/contracts/blob/main/e2e/accounts.ts) + privateKey: + "0xa2e78cd4c87191e50d6a8f1610b1cf160b17216e9090dde7a92960a34c310482", + // Local Environment RPC Node + rpcNode: "http://localhost:8545", + // ERC20 token deployed on Local Environment + exchangeToken: "0x82e01223d51Eb87e16A03E24687EDF0F294da6f1", +}; + +const offerMetadata = { + schemaUrl: "https://json-schema.org", + type: "FERMION_OFFER", + uuid: "ecf2a6dc-555b-41b5-aca8-b7e29eebbb30", + name: "Boson X MetaFactory Hoodie", + description: + "The future is here and it starts with a limited edition MetaFactory X Boson Protocol Hoodie... This unisex hoodie, in a metablack coloureway with reflective stripes, is made from a heavyweight French terry cotton. It has an adjustable hood and full colour interior, elasticated trims, reflective lining, and reflective back panels... Nothing says open metaverse more than this collab between MetaFactory and Boson Protocol. Limited Edition: Only 75 ever made.", + externalUrl: + "https://app.bosonportal.io/#/offer-uuid/ecf2a6dc-555b-41b5-aca8-b7e29eebbb30", + licenseUrl: + "https://app.bosonportal.io/#/license/ecf2a6dc-555b-41b5-aca8-b7e29eebbb30", + image: + "https://bsn-portal-production-image-upload-storage.s3.amazonaws.com/fc11acc4-e27b-4ede-b7d3-f16735fab406", + attributes: [] as unknown[], + product: { + uuid: "77593bb2-f797-11ec-b939-0242ac120002", + version: 1, + title: "Boson X MetaFactory Hoodie", + description: + "The future is here and it starts with a limited edition MetaFactory X Boson Protocol Hoodie... This unisex hoodie, in a metablack coloureway with reflective stripes, is made from a heavyweight French terry cotton. It has an adjustable hood and full colour interior, elasticated trims, reflective lining, and reflective back panels... Nothing says open metaverse more than this collab between MetaFactory and Boson Protocol. Limited Edition: Only 75 ever made.", + productionInformation_brandName: "Boson X MetaFactory", + details_offerCategory: "PHYSICAL", + condition: { + value: "new", + }, + category: { + value: "fashion", + }, + visuals_images: [ + { + url: "https://bsn-portal-production-image-upload-storage.s3.amazonaws.com/5ba6461b-e4b4-444d-90bf-0c9de1835c35", + }, + ], + }, + shipping: { + returnPolicy: { + url: "https://example.com/return-policy", + }, + methods: [ + { + value: "pickup", + }, + ], + }, +}; + +const offerParams = { + exchangeToken: context.exchangeToken, + sellerDeposit: "0", + verifierFee: "0", + custodianFee: { + amount: 0, + period: 3600 * 24 * 30, + }, + facilitatorFeePercent: 0, + ...offerMetadata, +}; + +export async function createOffer() { + // Create the wallet (using ether.js v5*) + const wallet = new Wallet(context.privateKey); + // Check the seller Entity exists + const { entityId: sellerId } = await checkSellerEntity(wallet); + // Create the MCP Client + const mcpClient = await createMcpClient(false); + // Call the "initialize_sdk" tool first + await mcpClient.callTool({ + name: "initialize_sdk", + arguments: { + configId: context.configId, + signerAddress: wallet.address, + }, + }); + // Call the "create_offer" tool + const createOfferResult = await mcpClient.callTool({ + name: "create_offer", + arguments: { + sellerId, + facilitatorId: sellerId, + verifierId: sellerId, + custodianId: sellerId, + ...offerParams, + }, + }); + // Extract the TransactionData from the tool result + const { transactionData } = getContent<{ + transactionData: Pick; + }>(createOfferResult as ReturnTypeMcp); + // Build and sign the blockchain transaction + const signedTransaction = await signTransaction( + transactionData, + context.privateKey, + context.rpcNode, + ); + // Send the signed transaction calling the "send_signed_transaction" tool + const sendSignedTransactionResult = await mcpClient.callTool({ + name: "send_signed_transaction", + arguments: { + signedTransaction, + }, + }); + // Extract the TransactionReceipt from the tool result + const { logs, hash } = getContent<{ + hash: string; + logs: Log[]; + blockNumber: number; + }>(sendSignedTransactionResult as ReturnTypeMcp); + // Find the offerId from the transaction Logs + const abi = new Interface(abis.IOfferEventsABI); + const [offerId] = logs + .map((log) => { + try { + return abi.parseLog(log); + } catch (error) { + // assume that failing to parse is irrelevant log + return null; + } + }) + .filter((log) => log !== null) + .filter((log) => log.name === "OfferCreated") + .map((log) => log.args["bosonOfferId"]); + if (!offerId) { + throw new Error( + `Unable to retrieve the offerId from transation logs (hash: ${hash})`, + ); + } + console.log(`Offer ${offerId} has been successfully created`); +} + +function getContent(toolResult: ReturnTypeMcp): T { + const contentText = toolResult?.content?.[0]?.text; + if (!contentText) { + throw new Error( + `Invalid return content from tool: ${JSON.stringify(toolResult)}`, + ); + } + let contentJson; + try { + contentJson = JSON.parse(contentText); + } catch (e) { + console.error( + `Unable to parse JSON from ${contentText}. Error: ${e.toString()}`, + ); + } + if (!contentJson.success) { + throw new Error(`Unsuccessful return from tool: ${contentText}`); + } + return contentJson as T; +} diff --git a/docs/agentic-commerce/docs/fermion/example/docker-compose.yaml b/docs/agentic-commerce/docs/fermion/example/docker-compose.yaml new file mode 100644 index 0000000..ed8fa1d --- /dev/null +++ b/docs/agentic-commerce/docs/fermion/example/docker-compose.yaml @@ -0,0 +1,107 @@ +services: + + fermion-protocol-node: + image: ghcr.io/fermionprotocol/contracts/fermion-protocol-node@sha256:356b4a45e355bc388a0c25ec47b303126321dbbdb6a476b2583d814293f58b3b + # image: fermionprotocol/fermion-protocol-node@sha256:356b4a45e355bc388a0c25ec47b303126321dbbdb6a476b2583d814293f58b3b # can be pulled from Docker Hub as well + ports: + - "8545:8545" + + fermion-mcp-server: + image: ghcr.io/bosonprotocol/mcp-server/fermion-mcp-server:main + # image: bosonprotocol/fermion-mcp-server:main + ports: + - "3000:3000" + environment: + - IPFS_METADATA_URL=http://host.docker.internal:5001 + - THE_GRAPH_IPFS_URL=http://host.docker.internal:5001 + - LOCALHOST_SUBSTITUTE=host.docker.internal + extra_hosts: + - host.docker.internal:host-gateway + + fermion-subgraph: + image: ghcr.io/fermionprotocol/core-components/fermion-subgraph@sha256:9d39cf36b798612c6cf3a00cf347df174db577ca280cc537260c6b3bfeaaa093 + # image: fermionprotocol/fermion-subgraph@sha256:9d39cf36b798612c6cf3a00cf347df174db577ca280cc537260c6b3bfeaaa093 # can be pulled from Docker Hub as well + ports: + - "8000:8000" + - "8001:8001" + - "8020:8020" + - "8030:8030" + - "8040:8040" + depends_on: + - ipfs + - postgres + - fermion-protocol-node + extra_hosts: + - host.docker.internal:host-gateway + environment: + postgres_host: postgres + postgres_user: graph-node + postgres_pass: let-me-in + postgres_db: graph-node + ipfs: "ipfs:5001" + ethereum: "localhost:http://host.docker.internal:8545" + GRAPH_LOG: debug + GRAPH_ALLOW_NON_DETERMINISTIC_IPFS: "true" + + postgres: + image: postgres:14 + ports: + - "5432:5432" + command: + [ + "postgres", + "-cshared_preload_libraries=pg_stat_statements", + "-cmax_connections=200" + ] + environment: + POSTGRES_USER: graph-node + POSTGRES_PASSWORD: let-me-in + POSTGRES_DB: graph-node + # FIXME: remove this env. var. which we shouldn't need. Introduced by + # , maybe as a + # workaround for https://github.com/docker/for-mac/issues/6270? + PGDATA: "/var/lib/postgresql/data" + POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C" + + ipfs: + image: ipfs/go-ipfs:master-2022-05-25-e8f1ce0 + ports: + - "5001:5001" + - "8080:8080" + volumes: + - ./ipfs-config.sh:/container-init.d/ipfs-config.sh + + meta-tx-gateway: + image: ghcr.io/bosonprotocol/meta-tx-gateway:main + # image: bosonprotocol/meta-tx-gateway:main # can be pulled from Docker Hub as well + user: "node:node" + ports: + - "8888:8888" + environment: + - ENV_NAME=local + - CONFIG_ID=local-31337-0 + - PRIVATE_KEY=0x316b234f5fea007dcc40404188b588fb90cb9bb1e33fc163e212eab2f8565293 # ACCOUNT_9 from ./contracts/accounts.js + - RPC_NODE=http://host.docker.internal:8545 + - FORWARDER_ADDRESS=0x4c5859f0F772848b2D91F1D83E2Fe57935348029 + - API_KEYS=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + - 'API_IDS={"BOSON": ["xxxxxxxx-xxxx-xxxx-xxxx-111111111111"], "FERMION": ["xxxxxxxx-xxxx-xxxx-xxxx-222222222222"], "ERC20": ["xxxxxxxx-xxxx-xxxx-xxxx-333333333333"], "FORWARDER": ["xxxxxxxx-xxxx-xxxx-xxxx-444444444444"]}' + - BOSON_VOUCHER_INTERFACE_ID=0x6a474d2c + depends_on: + - fermion-protocol-node + extra_hosts: + - host.docker.internal:host-gateway + command: ["sh", "-c", "sleep 3 && npm run start"] + + opensea-api-mock: + image: ghcr.io/bosonprotocol/core-components/opensea-api-mock:main + # image: bosonprotocol/opensea-api-mock:main # can be pulled from Docker Hub as well + user: "node:node" + ports: + - "3334:3334" + environment: + - ENV_NAME=local + - RPC_NODE=http://host.docker.internal:8545 + depends_on: + - fermion-protocol-node + extra_hosts: + - host.docker.internal:host-gateway diff --git a/docs/agentic-commerce/docs/fermion/example/ipfs-config.sh b/docs/agentic-commerce/docs/fermion/example/ipfs-config.sh new file mode 100644 index 0000000..4509c14 --- /dev/null +++ b/docs/agentic-commerce/docs/fermion/example/ipfs-config.sh @@ -0,0 +1,5 @@ +#!/bin/sh +echo "Setting IPFS config..." +ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' +ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]' +ipfs config profile apply lowpower \ No newline at end of file diff --git a/docs/agentic-commerce/docs/fermion/example/runit.ts b/docs/agentic-commerce/docs/fermion/example/runit.ts new file mode 100644 index 0000000..52f1478 --- /dev/null +++ b/docs/agentic-commerce/docs/fermion/example/runit.ts @@ -0,0 +1,27 @@ +import { program } from "commander"; +import { createMcpClient } from "./create-mcp-client"; +import { createSellerEntity } from "./seller-entity-creation"; +import { createOffer } from "./create-offer"; + +program + .name("runit") + .description("Sign a transaction using a private key") + .command("createMcpClient") + .action(async () => { await createMcpClient() }); + +program + .name("runit") + .description("Sign a transaction using a private key") + .command("createSellerEntity") + .action(async () => createSellerEntity()); + +program + .name("runit") + .description("Sign a transaction using a private key") + .command("createOffer") + .action(async () => createOffer()); + +if (require.main === module) { + program.parse(); +} + diff --git a/docs/agentic-commerce/docs/fermion/example/seller-entity-creation.ts b/docs/agentic-commerce/docs/fermion/example/seller-entity-creation.ts new file mode 100644 index 0000000..83d0d9f --- /dev/null +++ b/docs/agentic-commerce/docs/fermion/example/seller-entity-creation.ts @@ -0,0 +1,105 @@ +/** + * Create an entity for a given wallet, with Seller, Verifier and Custodian roles. + * This would allow to create an offer, reusing the same entity as Verifier and Custody. + * + * This script can be run using ts-node: + * > npx ts-node runit.ts -- createSellerEntity + */ + +import { EthersAdapter } from "@bosonprotocol/ethers-sdk"; +import { providers, Wallet } from "ethers"; +import { + EnvironmentType, + ConfigId, + getEnvConfigById, + eEntityRole, + TransactionRequest +} from "@fermionprotocol/common"; +import { BaseEntity, CoreSDK } from "@fermionprotocol/core-sdk"; + +const context = { + // EnvironmentType of the Local Environment + envName: "local" as EnvironmentType, + // ConfigId of the Local Environment + configId: "local-31337-0" as ConfigId, + // One of the pre-funded accounts in the Local Environment + // (https://github.com/fermionprotocol/contracts/blob/main/e2e/accounts.ts) + privateKey: "0xa2e78cd4c87191e50d6a8f1610b1cf160b17216e9090dde7a92960a34c310482" +}; + +// The roles the Seller Entity should have to be able to create offers without any need for other entities +const sellerRoles = [eEntityRole.Seller, eEntityRole.Verifier, eEntityRole.Custodian]; + +export async function createSellerEntity() { + // Create the wallet (using ethers.js v5*) + const wallet = new Wallet(context.privateKey); + // Create the Fermion CoreSDK with the default config for the given envName/configId + const defaultConfig = getEnvConfigById(context.envName, context.configId); + const coreSDK = CoreSDK.fromDefaultConfig({ + web3Lib: new EthersAdapter( + new providers.JsonRpcProvider(defaultConfig.jsonRpcUrl), + wallet + ), + envName: context.envName, + configId: context.configId + }); + // Check if an entity already exists for the given wallet + const baseEntities = await coreSDK.getBaseEntities({ + baseEntitiesFilter: { adminAccount: wallet.address.toLowerCase() } + }); + if (baseEntities?.length) { + console.log( + `BaseEntity already exists for wallet ${wallet.address}: ${JSON.stringify(baseEntities)}` + ); + return; + } + // Create an Entity with required roles + const tx = await coreSDK.createEntity( + { + roles: sellerRoles, + metadataURI: "", + } + ); + console.log(`Transaction submitted: ${tx.hash} ...`); + // Wait for the transaction to be confirmed + const txReceipt = await tx.wait(); + // Get the entityId for the created entity + const entityId = coreSDK.getCreatedEntityIdFromLogs(txReceipt.logs); + console.log(`Entity created with id '${entityId}'`); + // Wait for the subgraph to be updated + console.log(`Wait for the subgraph to update...`); + await coreSDK.waitForGraphNodeIndexing(tx); + // Extract the data about the entity + const entity = await coreSDK.getEntity(entityId); + console.log(`${JSON.stringify(entity)}`); +} + +export async function checkSellerEntity(wallet: Wallet): Promise { + // Create the Fermion CoreSDK with the default config for the given envName/configId + const defaultConfig = getEnvConfigById(context.envName, context.configId); + const coreSDK = CoreSDK.fromDefaultConfig({ + web3Lib: new EthersAdapter( + new providers.JsonRpcProvider(defaultConfig.jsonRpcUrl), + wallet + ), + envName: context.envName, + configId: context.configId + }); + // Ensure the given wallet has an Entity with required roles Seller + Verifier + Custodian + const [sellerEntity] = await coreSDK.getBaseEntities({ + baseEntitiesFilter: { adminAccount: wallet.address.toLowerCase() } + }); + if (!sellerEntity) { + throw new Error(`No entity exists for the given wallet ${wallet.address}`); + } + if (!sellerEntity.roles?.includes(eEntityRole.Seller) && sellerRoles.includes(eEntityRole.Seller)) { + throw new Error(`Entity created with wallet ${wallet.address} is not a Seller`); + } + if (!sellerEntity.roles?.includes(eEntityRole.Verifier) && sellerRoles.includes(eEntityRole.Verifier)) { + throw new Error(`Entity created with wallet ${wallet.address} is not a Verifier`); + } + if (!sellerEntity.roles?.includes(eEntityRole.Custodian) && sellerRoles.includes(eEntityRole.Custodian)) { + throw new Error(`Entity created with wallet ${wallet.address} is not a Custodian`); + } + return sellerEntity; +} diff --git a/docs/agentic-commerce/docs/fermion/example/sign-transaction.ts b/docs/agentic-commerce/docs/fermion/example/sign-transaction.ts new file mode 100644 index 0000000..91766ed --- /dev/null +++ b/docs/agentic-commerce/docs/fermion/example/sign-transaction.ts @@ -0,0 +1,62 @@ +import { ethers } from "ethers"; + +interface TransactionRequest { + to?: string; + from?: string; + value?: string | number; + data?: string; + gasLimit?: string | number; + gasPrice?: string | number; + maxFeePerGas?: string | number; + maxPriorityFeePerGas?: string | number; + nonce?: number; + type?: number; + chainId?: number; +} + +async function signTransaction( + transactionData: TransactionRequest, + privateKey: string, + rpcUrl?: string, +): Promise { + try { + const wallet = new ethers.Wallet(privateKey); + + // Auto-fetch missing values from RPC if URL is provided + if (rpcUrl) { + const provider = new ethers.providers.JsonRpcProvider(rpcUrl); + + // Fetch nonce if not provided + if (transactionData.nonce === undefined) { + const connectedWallet = wallet.connect(provider); + transactionData.nonce = await connectedWallet.getTransactionCount(); + } + + // Fetch gas price if not provided + if (transactionData.gasPrice === undefined) { + const gasPriceBigNumber = await provider.getGasPrice(); + transactionData.gasPrice = gasPriceBigNumber.toHexString(); + } + + // Estimate gas limit if not provided + if (transactionData.gasLimit === undefined) { + const estimatedGas = await provider.estimateGas({ + ...transactionData, + from: wallet.address, + }); + transactionData.gasLimit = estimatedGas.toHexString(); + } + } + + const signedTransaction = await wallet.signTransaction(transactionData); + return signedTransaction; + } catch (error) { + throw new Error( + `Failed to sign transaction: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + ); + } +} + +export { signTransaction, TransactionRequest }; diff --git a/docs/agentic-commerce/docs/fermion/fermion-local-environment.png b/docs/agentic-commerce/docs/fermion/fermion-local-environment.png new file mode 100644 index 0000000000000000000000000000000000000000..7211b631f00c2f58976c29e2108bb633b52b5120 GIT binary patch literal 76153 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�XM8R_ZKpDry&6ZEyEFbTA!uh|gJMPs(Qq+E-=O9WhItl#Q+YfG)d*1JCf zX5X!65V4_JJMxr~_S_;JX?f?ZiFLv9H?sOkr+h-~;~i}jc1jlqg%``Ro|Sz@fVn0M zI=5F9KW~)>fPdxp##Yc5)xkk=d#`D7Jd>H5V%IcgrrSg*DHdg~8^Yprsx>si0RKMN zZPH0S!Po#y!dll~N~M$4eF@s0=_v#JN6p89Sbg@6d&Javvq8*0TkO@T2J4wo-6Hq5 zpNeMH1Fv0fv2>mMmCijc*C3W#oM3$+*!v+QAI zWpX7_oOpm1*S3%zU<$K>g2DwevplmlFj+OOq1FYoQ`4ct<7PM7EH+I8#rTPt!N zOx8HoWCu-+$nbnVkdfR_o$TY5eS5Z>ry^9#=-M^byXs@zQwZXAi<{kc80Q1<(H(00 z?$!Yv(S|D-tZh1?xej9GAAUHQ>KQdhW-8*Y4haeZ0v3Mq<`antQ9SuI<23d(VC=I; z{Aw=%gPx|873OBRpPs2c(W?VcP_*HOn3?Xb=iiTN%|H8iFH5WUXU)ukjl%rSWBb^; ztiTG67CpS7==lg_r(K4KLsi}Nit-~M;v(9;^N)r-Qyu}%38GeKRtpr%O zb>W#1_URUe2U|u%z2Xzg4mkPS?XizI&nNUnTg;z8i-@dRMe^m78$KN%!hG-eR?}^7 zTz*8`W0;ShwDwTPZ`uOtl31cIW%{;^Lm<3eVq$xZ2?=UT+|!BsdHoa6%(u$KEAhbWw{5%zl*-h zo0|UGaZ2^R&$ip5*f

^S8bp2g6QM|xHC44;+uMyf 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 **Note:** "Fermion" is no longer used as a brand. The former Fermion Protocol was integrated into the Boson codebase in 2025 and is now the **High Value Asset Module** of Boson Protocol. The `fermion` directory and file names, service names (`fermion-mcp-server`, `fermion-protocol-node`, `fermion-subgraph`), Docker images, and SDK package names are retained at the code level for compatibility. + +The following documentation illustrates how an AI Agent can interact with the module. + +## Prerequisites + +Make sure you have: +- a functional Local Environment running (see [setup-local-env](./setup-local-env.md)) +- a functional MCP Client implementation connecting to the High Value Asset Module MCP Server on this local environment (see [create-mcp-client](./create-mcp-client.md)) +- identified a pre-funded wallet private key your AI Agent will use: + The mcp-client needs to manage the wallet that will identify the Seller entity on the blockchain. + As the following example will run on the Local Environment, the wallet/privateKey we're going to use for our agent is one of the pre-funded wallets in the [Local Environment](./setup-local-env.md). +- a basic understanding on web3 concepts, including signing a transaction with your wallet (for instance using [ethers.js](https://docs.ethers.org/v5/api/signer/)) + +In addition: +- as the MCP server doesn't support entity creation yet, creating a seller entity for your AI Agent needs to be done in advance. An easy way to do it is using the module's Core-SDK (published as `@fermionprotocol/core-sdk` for code-compatibility reasons) in a javascript/typescript script, as described in [./example/seller-entity-creation.ts](./example/seller-entity-creation.ts) +- the ERC20 token used as the exchange token for the offers to be created also needs to be registered on the protocol. + +NOTE: Make sure you are creating the seller entity with the same wallet that you're planning to use in the following steps. + +## General Mechanisms + +The AI Agent is supposed to manage their own web3 wallet. This means the AI Agent will be in charge of signing the transaction data returned by the MCP Server when the tools are used. + +An example of how to implement signing a transaction data is given in [../../scripts/sign-transaction.ts](../../scripts/sign-transaction.ts). + +Once the signed transaction has been built by the AI Agent, the MCP Server provides the tool called "send_signed_transaction" to submit it to the blockchain. See example below for details about how to use the "send_signed_transaction" tool. + +## Creating an Offer + +To create an offer, the MCP server provides a tool called "create_offer", which returns the transaction data that needs to be signed by the web3 wallet before being submitted to the blockchain. + +The following example shows the complete flow the AI Agent should implement to: +- create the transaction data to create an offer ("create_offer" tool) +- sign the transaction locally with the AI Agent's wallet (see [../../scripts/sign-transaction.ts](../../scripts/sign-transaction.ts) for an ethers-based example) +- submit the transaction to the blockchain ("send_signed_transaction" tool). + +![Create Offer Sequence](create-offer-sequence.png) diff --git a/docs/agentic-commerce/docs/fermion/setup-local-env.md b/docs/agentic-commerce/docs/fermion/setup-local-env.md new file mode 100644 index 0000000..88ca296 --- /dev/null +++ b/docs/agentic-commerce/docs/fermion/setup-local-env.md @@ -0,0 +1,178 @@ +# Setup Local Environment for the High Value Asset Module Core Components + +> **Note:** "Fermion" is no longer used as a brand. The former Fermion Protocol was integrated into the Boson codebase in 2025 and is now the **High Value Asset Module** of Boson Protocol. Service names (`fermion-protocol-node`, `fermion-mcp-server`, `fermion-subgraph`), Docker image names (`ghcr.io/fermionprotocol/*`, `ghcr.io/bosonprotocol/mcp-server/fermion-mcp-server`, etc.), SDK package names (`@fermionprotocol/core-sdk`), and the `local-31337-0` configId are retained below for code-level compatibility. + +The local environment runs a set of Docker containers to provide a complete runtime environment for the High Value Asset Module and its core components, including the module's MCP server. + +Unlike a staging environment on a public testnet blockchain (where the module is also deployed), the local environment can be used for automated testing, as the blockchain state and all data created during testing are reset each time the containers are restarted. + +![High Value Asset Module Local Environment](fermion-local-environment.png) + +## Prerequisites + +Make sure you have: +- [Installed the latest version of Docker Compose](https://docs.docker.com/compose/install/) +- A basic understanding of Docker concepts and how Docker works + +## Docker Compose Specification + +The local environment defines a set of services, each run as a container using Docker Compose. +The configuration follows the module's Core-SDK default setup (published as `@fermionprotocol/core-sdk`) for the *local-31337-0* config, which is intended for use in this local containerized environment (please refer to the SDK documentation for more details). + +[see docker-compose example](./example/docker-compose.yaml) + +### Services + +#### fermion-protocol-node + +The *fermion-protocol-node* service runs a local blockchain node, with all the smart contracts required to operate the High Value Asset Module. + +This blockchain is pre-initialized with [a set pre-funded of Wallets](https://github.com/fermionprotocol/contracts/blob/main/e2e/accounts.ts) that can be used in order to send transactions (and pay for the transaction fees). +However, you can also use other Wallets/Private Keys in your tests after you have transferred them enough funds from some pre-funded wallets to manage the transaction fees. + +The defined chainId is 31337, as stated in [this configuration file](https://github.com/fermionprotocol/contracts/blob/main/e2e/hardhat.config-node.ts), in accordance with the module Core-SDK's default configuration for the *local-31337-0* config. + +Contracts for the High Value Asset Module, the core Boson Protocol, and additional components (ERC20 token, Forwarder, Seaport) are deployed at addresses consistent with the module Core-SDK's default configuration for the *local-31337-0* config. + +Docker images: +- ghcr.io/fermionprotocol/contracts/fermion-protocol-node:main +- fermionprotocol/fermion-protocol-node:main + +Dependencies: +None + +Configuration (see ./example/docker-compose.yaml for complete technical details): +- export PORT 8545 + +The RPC Node (URL used by client/tools to interact with the blockchain) is defined to be http://localhost:8545 (in accordance with the module Core-SDK default configuration for *local-31337-0* config). + +#### fermion-mcp-server + +The *fermion-mcp-server* service is the MCP Server for the High Value Asset Module. + +Docker images: +- ghcr.io/bosonprotocol/mcp-server/fermion-mcp-server:main +- bosonprotocol/fermion-mcp-server:main + +Dependencies: +- the mcp-server is connecting to the blockchain node (= *fermion-protocol-node* service) +- the mcp-server is connecting to the IPFS node (= *ipfs* service) + +Configuration (see ./example/docker-compose.yaml for complete technical details): +- export PORT 3000 +- define connections with *ipfs* and *fermion-protocol-node* services + +#### fermion-subgraph + +The *fermion-subgraph* service provides an instance of [graph node](https://thegraph.com/docs/en/indexing/tooling/graph-node/) and publishes the subgraph used to index blockchain data about the High Value Asset Module, to structure it and to serve it to clients via a GraphQL API. + +Docker images: +- ghcr.io/fermionprotocol/core-components/fermion-subgraph:main +- fermionprotocol/fermion-subgraph:main + +Dependencies: +- the graph node needs a *postgres* service running in the background +- the graph node needs an *ipfs* service to access the metadata files linked with the on-chain events +- the graph node is connecting to the blockchain node (= *fermion-protocol-node* service) + +Configuration (see ./example/docker-compose.yaml for complete technical details): +- export PORTS 8000,8001,8020,8030,8040 +- define connections with *postgres*, *ipfs* and *fermion-protocol-node* services + +The built subgraph can be explored locally at http://localhost:8000/subgraphs/name/fermion/corecomponents, in accordance with the module Core-SDK's default configuration for the *local-31337-0* config. It allows manual monitoring of all user interactions with the High Value Asset Module. + +#### postgres + +The *postgres* database is required by the graph node run with the *fermion-subgraph* service. + +Docker image: +- postgres:14 + +Configuration: +- export PORT 5432 +- user/password matching the configuration given to the *fermion-subgraph* service. + +#### ipfs + +The *ipfs* service is required to host the metadata files used to describe the module's entities and offers. This IPFS node should be accessed by the *fermion-subgraph* service so that it can read the metadata file contents and populate data in the subgraph. + +Docker image: +- ipfs/go-ipfs:master-2022-05-25-e8f1ce0 + +Configuration: +- export PORTS 5001,8080 +- a volume is defined to set the appropriate configuration flags to the node + +#### meta-tx-gateway + +The *meta-tx-gateway* service is used by the module Core-SDK to relay meta-transactions to the High Value Asset Module on-chain. + +Meta-transactions are an optional feature supported by the module, allowing transactions to be relayed to the protocol contracts instead of being sent directly by the end-user wallet. This is especially useful for sponsoring transaction fees, so that end-users don't need to have native funds (e.g., ETH on Ethereum) in their wallets. + +For more details: [Meta-transactions Concepts](https://docs.polygon.technology/pos/concepts/transactions/meta-transactions/), [EIP-2771](https://eips.ethereum.org/EIPS/eip-2771) + +Docker images: +- ghcr.io/bosonprotocol/meta-tx-gateway:main +- bosonprotocol/meta-tx-gateway:main + +Configuration (see ./example/docker-compose.yaml for complete technical details): +- export PORT 8888 +- the PRIVATE_KEY of one of the pre-founded wallets is provided to effectively send the transactions on-chain + +Meta-transaction relayer URL is defined to be http://localhost:8888 (in accordance with module Core-SDK default configuration for *local-31337-0* config) + +#### opensea-api-mock + +The *opensea-api-mock* service allows the module Core-SDK to rely on a few services provided by [Opensea API](https://docs.opensea.io/reference/openapi-definition) on public networks, and accessed through [opensea-sdk](https://github.com/ProjectOpenSea/opensea-js). + +Mocked endpoints are: +- GET /api/v2/chain/:chain/payment_token/:token +- GET /api/v2/collections/:slug +- GET /api/v2/chain/:chain/contract/:assetContractAddress/nfts/:tokenId +- GET /api/v2/orders/:chain/:protocol/:sidePath +- POST /api/v2/orders/:chain/:protocol/:sidePath +- POST /api/v2/:sidePath/fulfillment_data + +## Run the Local Environment + +Copy all files from [./example](./example) in your project + +Run a terminal and move to the folder which contains the files copied from [./example](./example) (= where the docker-compose.yaml is). + +- Launch Docker Compose configuration + + ```shell + docker compose up -d + + ``` + +-Now you need to wait for everything to be ready (in particular, for all contracts to be deployed in the *fermion-protocol-node* service). + + ```shell + docker compose exec fermion-protocol-node ls /app/deploy.done + while [ $? -ne 0 ]; do + sleep 15 + echo "Waiting for contracts to be deployed..." + docker compose exec fermion-protocol-node ls /app/deploy.done + done + ``` + + The */app/deploy.done* file is created on the *fermion-protocol-node* when everything is ready. It usually takes a few minutes. + +## Create an MCP Client + +Once the environment is ready, any AI Agent can connect to the MCP server to interact with the High Value Asset Module by implementing an MCP client. + +See [create-mcp-client](./create-mcp-client.md) for details about how to implement an MCP Client and connect to the module's MCP Server. + +If you already have connected your MCP Client to the module's MCP Server, you can [interact with the High Value Asset Module](./interact-with-fermion-protocol.md). + +## Stop the Local Environment + +To stop the containers, run +```shell +docker compose down -v +``` + +Note: the *-v, --volumes* option is required to drop the Postgres database, to ensure the subgraph will get a clean environment on the next start. + diff --git a/docs/agentic-commerce/e2e/boson/tests/complete-marketplace-journeys.test.ts b/docs/agentic-commerce/e2e/boson/tests/complete-marketplace-journeys.test.ts new file mode 100644 index 0000000..1804d7e --- /dev/null +++ b/docs/agentic-commerce/e2e/boson/tests/complete-marketplace-journeys.test.ts @@ -0,0 +1,970 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + AuthTokenType, + EvaluationMethod, + GatingType, + TokenType, +} from "@bosonprotocol/common"; +import { CoreSDK, subgraph } from "@bosonprotocol/core-sdk"; +import { OfferFieldsFragment } from "@bosonprotocol/core-sdk/src/subgraph"; +import { ethers, Wallet } from "ethers"; + +import { ReturnTypeMcp } from "../../../src/common"; +import { mcpServerUrl } from "../../common/constants"; +import { provider } from "../../common/protocol-utils"; +import { + getSignatureData, + resultIsSuccessful, + signAndSendTransactionData, +} from "../../common/test-utils"; +import { + ensureMintedAndAllowedTokens, + initCoreSDKWithFundedWallet, + storeProductV1Metadata, +} from "./boson-utils"; +import { MOCK_ERC20_ADDRESS } from "./constants"; +import { BosonMCPClient } from "./mcp-client"; +import minimalOfferMetadata from "./metadata/minimalOffer.json"; + +jest.setTimeout(90_000); // Extended timeout for complex workflows + +const TEST_KEY = 3; // must be unique per test file +const TEST_KEY_BUYER = 4; // must be unique per test file +const LONG_OFFER_VALIDITY_WINDOW_MS = 1000 * 60 * 60 * 24 * 30; + +describe("Boson MCP Server - Complete Marketplace Journeys", () => { + let sellerCoreSdk: CoreSDK; + let sellerWallet: Wallet; + let buyerCoreSdk: CoreSDK; + let buyerWallet: Wallet; + let mcpClient: BosonMCPClient; + let sellerId: string; + let metadataUri: string; + let metadataHash: string; + + beforeAll(async () => { + ({ coreSDK: sellerCoreSdk, fundedWallet: sellerWallet } = + await initCoreSDKWithFundedWallet(TEST_KEY)); + + // Create a separate buyer wallet + ({ coreSDK: buyerCoreSdk, fundedWallet: buyerWallet } = + await initCoreSDKWithFundedWallet(TEST_KEY_BUYER)); + + // Setup MCP client and create seller + mcpClient = new BosonMCPClient(); + await mcpClient.connectToServer(mcpServerUrl); + + const createSellerResult = await mcpClient.createSeller({ + admin: sellerWallet.address, + assistant: sellerWallet.address, + treasury: sellerWallet.address, + authTokenId: "0", + authTokenType: AuthTokenType.NONE, + contractUri: "", + royaltyPercentage: "0", + type: "SELLER", + kind: "lens", + contactPreference: "xmtp", + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + + resultIsSuccessful(createSellerResult, /"success": true/); + + let createSellerReceipt; + await signAndSendTransactionData( + createSellerResult, + mcpClient, + sellerWallet, + (_receipt) => (createSellerReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = createSellerReceipt; + sellerId = sellerCoreSdk.getCreatedSellerIdFromLogs(logs)?.toString() || ""; + expect(sellerId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = createSellerReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + + // Create shared metadata for marketplace tests + ({ metadataUri, metadataHash } = await storeProductV1Metadata(mcpClient, { + ...minimalOfferMetadata, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + })); + }); + + afterAll(async () => { + if (mcpClient) { + await mcpClient.disconnect(); + } + }); + + describe("Simple Purchase Journey", () => { + let offerId: string; + let exchangeId: string; + + test("create simple offer", async () => { + // Use early timestamp to avoid blockchain time issues + const validFromTime = 1577836800000; // January 1, 2020 00:00:00 UTC + const currentBlock = await provider.getBlock("latest"); + const blockTime = currentBlock.timestamp * 1000; + + const createOfferResult = await mcpClient.createOffer({ + price: "5000000", + sellerDeposit: "0", + buyerCancellationPenalty: "250000", + quantityAvailable: 10, + validFromDateInMS: validFromTime, + validUntilDateInMS: blockTime + 1000 * 60 * 60 * 24, // 1 day + voucherRedeemableFromDateInMS: validFromTime, + voucherRedeemableUntilDateInMS: 0, + disputePeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + voucherValidDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + resolutionPeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + metadataUri, + metadataHash, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + + resultIsSuccessful(createOfferResult, /"success": true/); + + let createOfferReceipt: any; + await signAndSendTransactionData( + createOfferResult, + mcpClient, + sellerWallet, + (_receipt) => (createOfferReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = createOfferReceipt; + offerId = sellerCoreSdk.getCreatedOfferIdFromLogs(logs)!; + expect(offerId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = createOfferReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + }); + + test("buyer commits to offer", async () => { + const commitResult = await mcpClient.commitToOffer({ + offerId, + buyer: buyerWallet.address, + configId: "local-31337-0", + signerAddress: buyerWallet.address, + }); + + resultIsSuccessful(commitResult, /"success": true/); + + let commitReceipt: any; + await signAndSendTransactionData( + commitResult, + mcpClient, + buyerWallet, + (_receipt) => (commitReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = commitReceipt; + exchangeId = + sellerCoreSdk.getCommittedExchangeIdFromLogs(logs)?.toString() || ""; + expect(exchangeId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = commitReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + }); + + test("complete purchase with voucher redemption", async () => { + // Redeem voucher (buyer action) + const redeemResult = await mcpClient.redeemVoucher({ + exchangeId, + configId: "local-31337-0", + signerAddress: buyerWallet.address, + }); + + resultIsSuccessful(redeemResult, /"success": true/); + + let redeemReceipt: any; + await signAndSendTransactionData( + redeemResult, + mcpClient, + buyerWallet, + (_receipt) => (redeemReceipt = _receipt), + "local-31337-0", + ); + + // Wait for subgraph to index + const { blockNumber } = redeemReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + + // Verify exchange state + const exchange = await sellerCoreSdk.getExchangeById(exchangeId); + expect(exchange.state).toBe(subgraph.ExchangeState.REDEEMED); + expect(exchange.buyer.wallet.toLowerCase()).toBe( + buyerWallet.address.toLowerCase(), + ); + }); + }); + + describe("Buyer-initiated Offer Journey", () => { + let offerId: string; + let exchangeId: string; + + test("create buyer-initiated offer", async () => { + // Use early timestamp to avoid blockchain time issues + const validFromTime = 1577836800000; // January 1, 2020 00:00:00 UTC + const currentBlock = await provider.getBlock("latest"); + const blockTime = currentBlock.timestamp * 1000; + + const createOfferResult = await mcpClient.createOffer({ + creator: "BUYER", + price: "5000000", + sellerDeposit: "0", + buyerCancellationPenalty: "250000", + quantityAvailable: 1, + validFromDateInMS: validFromTime, + validUntilDateInMS: blockTime + 1000 * 60 * 60 * 24, // 1 day + voucherRedeemableFromDateInMS: validFromTime, + voucherRedeemableUntilDateInMS: 0, + disputePeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + voucherValidDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + resolutionPeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + metadataUri, + metadataHash, + configId: "local-31337-0", + signerAddress: buyerWallet.address, + }); + + resultIsSuccessful(createOfferResult, /"success": true/); + + let createOfferReceipt: any; + await signAndSendTransactionData( + createOfferResult, + mcpClient, + sellerWallet, + (_receipt) => (createOfferReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = createOfferReceipt; + offerId = sellerCoreSdk.getCreatedOfferIdFromLogs(logs)!; + expect(offerId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = createOfferReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + }); + + test("buyer deposits offer price", async () => { + expect(offerId).toBeTruthy(); + const getOffersResult = await mcpClient.getOffers({ + configId: "local-31337-0", + offersFilter: { id: offerId }, + }); + expect(getOffersResult).toBeTruthy(); + expect(getOffersResult.content).toBeTruthy(); + const offersData = JSON.parse((getOffersResult.content as any)[0].text); + expect(offersData).toBeTruthy(); + expect(Array.isArray(offersData)).toBe(true); + expect(offersData.length).toEqual(1); + const [offer] = offersData as OfferFieldsFragment[]; + expect(offer).toBeTruthy(); + expect(offer.buyer).toBeTruthy(); + await ensureMintedAndAllowedTokens([buyerWallet], offer.price); + + const depositResult = await mcpClient.depositFunds({ + entityId: (offer.buyer as { id: string }).id, + amount: offer.price, + tokenAddress: offer.exchangeToken.address, + configId: "local-31337-0", + signerAddress: buyerWallet.address, + }); + resultIsSuccessful(depositResult, /"success": true/); + let depositReceipt: any; + await signAndSendTransactionData( + depositResult, + mcpClient, + sellerWallet, + (_receipt) => (depositReceipt = _receipt), + "local-31337-0", + ); + expect(depositReceipt).toBeTruthy(); + expect(depositReceipt.hash).toBeTruthy(); + }); + + test("seller commits to offer", async () => { + const commitResult = await mcpClient.commitToBuyerOffer({ + offerId, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + + resultIsSuccessful(commitResult, /"success": true/); + + let commitReceipt: any; + await signAndSendTransactionData( + commitResult, + mcpClient, + sellerWallet, + (_receipt) => (commitReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = commitReceipt; + exchangeId = + sellerCoreSdk.getCommittedExchangeIdFromLogs(logs)?.toString() || ""; + expect(exchangeId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = commitReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + }); + }); + + describe("Non listed offers", () => { + let fullOfferBase: any; + beforeAll(async () => { + const validFromTime = 1577836800000; // January 1, 2020 00:00:00 UTC + const currentBlock = await provider.getBlock("latest"); + const blockTime = currentBlock.timestamp * 1000; + fullOfferBase = { + creator: "SELLER", + seller: sellerId, + price: "5000000", + // price: "0xinvalid", + sellerDeposit: "0", + buyerCancellationPenalty: "250000", + quantityAvailable: 2, + validFromDateInMS: validFromTime, + validUntilDateInMS: blockTime + 1000 * 60 * 60 * 24, // 1 day + voucherRedeemableFromDateInMS: validFromTime, + voucherRedeemableUntilDateInMS: 0, + disputePeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + voucherValidDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + resolutionPeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + metadataUri, + metadataHash, + useDepositedFunds: true, + } as const; + }); + describe("Seller initiated offer", () => { + let offerId: string; + let exchangeId: string; + let fullOffer: any; + beforeAll(() => { + fullOffer = { + ...fullOfferBase, + offerCreator: sellerWallet.address, + committer: buyerWallet.address, + buyerId: "0", + sellerId, + }; + }); + + // Offer creator is the seller, which has to provide the buyer with the full offer signature + // so that the buyer can call createOfferAndCommit + test("create non listed offer and commit", async () => { + const offerArgs = { + ...fullOffer, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }; + const signFullOfferResult = await mcpClient.signFullOffer({ + ...offerArgs, + }); + resultIsSuccessful(signFullOfferResult, /"success": true/); + const signatureData = getSignatureData( + signFullOfferResult as ReturnTypeMcp, + ); + if (signatureData.types && signatureData.types.EIP712Domain) { + delete (signatureData.types as any).EIP712Domain; // we don't need to hash the domain + } + const signature = await sellerWallet._signTypedData( + signatureData.domain, + signatureData.types, + signatureData.message, + ); + const commitResult = await mcpClient.createOfferAndCommit({ + ...offerArgs, + signature, + signerAddress: buyerWallet.address, + }); + resultIsSuccessful(commitResult, /"success": true/); + + let commitReceipt: any; + await signAndSendTransactionData( + commitResult, + mcpClient, + buyerWallet, + (_receipt) => (commitReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = commitReceipt; + exchangeId = + sellerCoreSdk.getCommittedExchangeIdFromLogs(logs)?.toString() || ""; + expect(exchangeId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = commitReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + }); + test("seller can void the offer before the first commit (calling voidNonListedOffer)", async () => { + const offerArgs = { + ...fullOffer, + name: "Offer to be voided", + description: "This offer will be voided", + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }; + const voidResult = await mcpClient.voidNonListedOffer({ + ...offerArgs, + }); + resultIsSuccessful(voidResult, /"success": true/); + let voidReceipt: any; + await signAndSendTransactionData( + voidResult, + mcpClient, + sellerWallet, + (_receipt) => (voidReceipt = _receipt), + "local-31337-0", + ); + expect(voidReceipt).toBeTruthy(); + // Wait for subgraph to index + const { blockNumber } = voidReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + const signFullOfferResult = await mcpClient.signFullOffer({ + ...offerArgs, + }); + resultIsSuccessful(signFullOfferResult, /"success": true/); + const signatureData = getSignatureData( + signFullOfferResult as ReturnTypeMcp, + ); + if (signatureData.types && signatureData.types.EIP712Domain) { + delete (signatureData.types as any).EIP712Domain; // we don't need to hash the domain + } + const signature = await sellerWallet._signTypedData( + signatureData.domain, + signatureData.types, + signatureData.message, + ); + const commitResult = await mcpClient.createOfferAndCommit({ + ...offerArgs, + signature, + signerAddress: buyerWallet.address, + }); + resultIsSuccessful(commitResult, /The offer has been voided/); + }); + test("void non listed offer batched", async () => { + const fullOffers = (await Promise.all( + [1, 2, 3].map(async (i) => { + const { metadataUri, metadataHash } = await storeProductV1Metadata( + mcpClient, + { + ...minimalOfferMetadata, + name: `Offer to be voided ${i}`, + description: `This offer will be voided ${i}`, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }, + ); + return { + ...fullOffer, + metadataUri, + metadataHash, + } as Parameters< + typeof mcpClient.voidNonListedOfferBatch + >[0]["fullOffers"][number]; + }), + )) as Parameters< + typeof mcpClient.voidNonListedOfferBatch + >[0]["fullOffers"]; + const voidResult = await mcpClient.voidNonListedOfferBatch({ + fullOffers, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + resultIsSuccessful(voidResult, /"success": true/); + let voidReceipt: any; + await signAndSendTransactionData( + voidResult, + mcpClient, + sellerWallet, + (_receipt) => (voidReceipt = _receipt), + "local-31337-0", + ); + expect(voidReceipt).toBeTruthy(); + // Wait for subgraph to index + const { blockNumber } = voidReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + for (const offerArgs of fullOffers) { + const signFullOfferResult = await mcpClient.signFullOffer({ + ...offerArgs, + offerCreator: sellerWallet.address, + committer: buyerWallet.address, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + resultIsSuccessful(signFullOfferResult, /"success": true/); + const signatureData = getSignatureData( + signFullOfferResult as ReturnTypeMcp, + ); + if (signatureData.types && signatureData.types.EIP712Domain) { + delete (signatureData.types as any).EIP712Domain; // we don't need to hash the domain + } + const signature = await sellerWallet._signTypedData( + signatureData.domain, + signatureData.types, + signatureData.message, + ); + const commitResult = await mcpClient.createOfferAndCommit({ + ...offerArgs, + offerCreator: sellerWallet.address, + committer: buyerWallet.address, + signature, + configId: "local-31337-0", + signerAddress: buyerWallet.address, + }); + resultIsSuccessful(commitResult, /The offer has been voided/); + } + }); + }); + describe("Buyer initiated offer", () => { + let buyerId: string; + let offerId: string; + let exchangeId: string; + let fullOffer: any; + let buyerCoreSdk2: CoreSDK; + let buyerWallet2: Wallet; + beforeAll(async () => { + // Create another buyer wallet + ({ coreSDK: buyerCoreSdk2, fundedWallet: buyerWallet2 } = + await initCoreSDKWithFundedWallet(TEST_KEY_BUYER)); + fullOffer = { + ...fullOfferBase, + quantityAvailable: 1, + creator: "BUYER", + offerCreator: buyerWallet2.address, + committer: sellerWallet.address, + sellerId: "0", + }; + }); + test("create buyer and buyerId", async () => { + const createBuyerResult = await mcpClient.createBuyer({ + configId: "local-31337-0", + signerAddress: buyerWallet2.address, + }); + + resultIsSuccessful(createBuyerResult, /"success": true/); + + let createBuyerReceipt; + await signAndSendTransactionData( + createBuyerResult, + mcpClient, + buyerWallet2, + (_receipt) => (createBuyerReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = createBuyerReceipt; + buyerId = + buyerCoreSdk2.getCreatedBuyerIdFromLogs(logs)?.toString() || ""; + expect(buyerId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = createBuyerReceipt; + await buyerCoreSdk2.waitForGraphNodeIndexing(blockNumber); + }); + test("buyer deposits offer price", async () => { + expect(buyerId).toBeTruthy(); + await ensureMintedAndAllowedTokens([buyerWallet2], fullOffer.price); + + const depositResult = await mcpClient.depositFunds({ + entityId: buyerId, + amount: fullOffer.price, + tokenAddress: fullOffer.exchangeToken || ethers.constants.AddressZero, + configId: "local-31337-0", + signerAddress: buyerWallet2.address, + }); + resultIsSuccessful(depositResult, /"success": true/); + let depositReceipt: any; + await signAndSendTransactionData( + depositResult, + mcpClient, + buyerWallet2, + (_receipt) => (depositReceipt = _receipt), + "local-31337-0", + ); + expect(depositReceipt).toBeTruthy(); + expect(depositReceipt.hash).toBeTruthy(); + }); + test("create non listed offer and commit", async () => { + expect(buyerId).toBeTruthy(); + const signFullOfferResult = await mcpClient.signFullOffer({ + ...fullOffer, + buyerId, + configId: "local-31337-0", + signerAddress: buyerWallet2.address, + }); + resultIsSuccessful(signFullOfferResult, /"success": true/); + const signatureData = getSignatureData( + signFullOfferResult as ReturnTypeMcp, + ); + if (signatureData.types && signatureData.types.EIP712Domain) { + delete (signatureData.types as any).EIP712Domain; // we don't need to hash the domain + } + const signature = await buyerWallet2._signTypedData( + signatureData.domain, + signatureData.types, + signatureData.message, + ); + const commitResult = await mcpClient.createOfferAndCommit({ + ...fullOffer, + buyerId, + signature, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + resultIsSuccessful(commitResult, /"success": true/); + + let commitReceipt: any; + await signAndSendTransactionData( + commitResult, + mcpClient, + sellerWallet, + (_receipt) => (commitReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = commitReceipt; + exchangeId = + sellerCoreSdk.getCommittedExchangeIdFromLogs(logs)?.toString() || ""; + expect(exchangeId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = commitReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + }); + }); + }); + + describe("Conditional Offer Journey", () => { + let conditionalOfferId: string; + let conditionalExchangeId: string; + + test("create conditional offer with token gating", async () => { + // Use a very early timestamp to avoid any blockchain time issues + const validFromTime = 1577836800000; // January 1, 2020 00:00:00 UTC + const currentBlock = await provider.getBlock("latest"); + const blockTime = currentBlock.timestamp * 1000; // Convert to milliseconds + + const createConditionalOfferResult = + await mcpClient.createOfferWithCondition({ + price: "5000000", + sellerDeposit: "0", + buyerCancellationPenalty: "250000", + quantityAvailable: 5, + validFromDateInMS: validFromTime, + validUntilDateInMS: blockTime + LONG_OFFER_VALIDITY_WINDOW_MS, + voucherRedeemableFromDateInMS: validFromTime, + voucherRedeemableUntilDateInMS: 0, + disputePeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + voucherValidDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + resolutionPeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + condition: { + method: EvaluationMethod.Threshold, + tokenType: TokenType.FungibleToken, + tokenAddress: MOCK_ERC20_ADDRESS, + gatingType: GatingType.PerAddress, + minTokenId: "0", // Fungible token cannot have token id range + maxTokenId: "0", + threshold: "1", + maxCommits: "1", + }, + metadataUri, + metadataHash, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + + resultIsSuccessful(createConditionalOfferResult, /"success": true/); + + let createConditionalOfferReceipt: any; + await signAndSendTransactionData( + createConditionalOfferResult, + mcpClient, + sellerWallet, + (_receipt) => (createConditionalOfferReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = createConditionalOfferReceipt; + conditionalOfferId = sellerCoreSdk.getCreatedOfferIdFromLogs(logs)!; + expect(conditionalOfferId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = createConditionalOfferReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + + // Mine a few additional blocks to ensure blockchain time advances + await provider.send("evm_mine", []); + await provider.send("evm_mine", []); + await provider.send("evm_mine", []); + }); + + test("buyer commits to conditional offer", async () => { + expect(conditionalOfferId).toBeTruthy(); + await ensureMintedAndAllowedTokens([buyerWallet], "5"); + + const commitResult = await mcpClient.commitToOffer({ + offerId: conditionalOfferId, + buyer: buyerWallet.address, + configId: "local-31337-0", + signerAddress: buyerWallet.address, + }); + + resultIsSuccessful(commitResult, /"success": true/); + + let commitReceipt: any; + await signAndSendTransactionData( + commitResult, + mcpClient, + buyerWallet, + (_receipt) => (commitReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = commitReceipt; + conditionalExchangeId = + sellerCoreSdk.getCommittedExchangeIdFromLogs(logs)?.toString() || ""; + expect(conditionalExchangeId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = commitReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + }); + + test("complete conditional offer purchase", async () => { + expect(conditionalExchangeId).toBeTruthy(); + + const redeemResult = await mcpClient.redeemVoucher({ + exchangeId: conditionalExchangeId, + configId: "local-31337-0", + signerAddress: buyerWallet.address, + }); + + resultIsSuccessful(redeemResult, /"success": true/); + + let redeemReceipt: any; + await signAndSendTransactionData( + redeemResult, + mcpClient, + buyerWallet, + (_receipt) => (redeemReceipt = _receipt), + "local-31337-0", + ); + + // Wait for subgraph to index + const { blockNumber } = redeemReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + + // Verify conditional exchange completion + const exchange = await sellerCoreSdk.getExchangeById( + conditionalExchangeId, + ); + expect(exchange.state).toBe(subgraph.ExchangeState.REDEEMED); + expect(exchange.offer.id).toBe(conditionalOfferId); + }); + }); + + describe("Voucher Cancellation Journey", () => { + let cancellationOfferId: string; + let cancellationExchangeId: string; + + test("create offer for cancellation scenario", async () => { + // Use early timestamp to avoid blockchain time issues + const validFromTime = 1577836800000; // January 1, 2020 00:00:00 UTC + const currentBlock = await provider.getBlock("latest"); + const blockTime = currentBlock.timestamp * 1000; + + const createOfferResult = await mcpClient.createOffer({ + price: "5000000", + sellerDeposit: "0", + buyerCancellationPenalty: "250000", + quantityAvailable: 3, + validFromDateInMS: validFromTime, + validUntilDateInMS: blockTime + 1000 * 60 * 60 * 24, // 1 day + voucherRedeemableFromDateInMS: validFromTime, + voucherRedeemableUntilDateInMS: 0, + disputePeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + voucherValidDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + resolutionPeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + metadataUri, + metadataHash, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + + resultIsSuccessful(createOfferResult, /"success": true/); + + let createOfferReceipt: any; + await signAndSendTransactionData( + createOfferResult, + mcpClient, + sellerWallet, + (_receipt) => (createOfferReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = createOfferReceipt; + cancellationOfferId = sellerCoreSdk.getCreatedOfferIdFromLogs(logs)!; + expect(cancellationOfferId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = createOfferReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + }); + + test("buyer commits and then cancels voucher", async () => { + expect(cancellationOfferId).toBeTruthy(); + + const commitResult = await mcpClient.commitToOffer({ + offerId: cancellationOfferId, + buyer: buyerWallet.address, + configId: "local-31337-0", + signerAddress: buyerWallet.address, + }); + + resultIsSuccessful(commitResult, /"success": true/); + + let commitReceipt: any; + await signAndSendTransactionData( + commitResult, + mcpClient, + buyerWallet, + (_receipt) => (commitReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = commitReceipt; + cancellationExchangeId = + sellerCoreSdk.getCommittedExchangeIdFromLogs(logs)?.toString() || ""; + expect(cancellationExchangeId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = commitReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + + // Cancel the voucher + const cancelResult = await mcpClient.cancelVoucher({ + exchangeId: cancellationExchangeId, + configId: "local-31337-0", + signerAddress: buyerWallet.address, + }); + + resultIsSuccessful(cancelResult, /"success": true/); + + let cancelReceipt: any; + await signAndSendTransactionData( + cancelResult, + mcpClient, + buyerWallet, + (_receipt) => (cancelReceipt = _receipt), + "local-31337-0", + ); + + // Wait for subgraph to index + const { blockNumber: cancelBlockNumber } = cancelReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(cancelBlockNumber); + + // Verify voucher cancellation + const exchange = await sellerCoreSdk.getExchangeById( + cancellationExchangeId, + ); + expect(exchange.state).toBe(subgraph.ExchangeState.CANCELLED); + }); + }); + + describe("Offer Voiding Journey", () => { + let voidOfferId: string; + + test("create and void offer", async () => { + // Use early timestamp to avoid blockchain time issues + const validFromTime = 1577836800000; // January 1, 2020 00:00:00 UTC + const currentBlock = await provider.getBlock("latest"); + const blockTime = currentBlock.timestamp * 1000; + + const createOfferResult = await mcpClient.createOffer({ + price: "5000000", + sellerDeposit: "0", + buyerCancellationPenalty: "250000", + quantityAvailable: 5, + validFromDateInMS: validFromTime, + validUntilDateInMS: blockTime + 1000 * 60 * 60 * 24, // 1 day + voucherRedeemableFromDateInMS: validFromTime, + voucherRedeemableUntilDateInMS: 0, + disputePeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + voucherValidDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + resolutionPeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + metadataUri, + metadataHash, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + + resultIsSuccessful(createOfferResult, /"success": true/); + + let createOfferReceipt: any; + await signAndSendTransactionData( + createOfferResult, + mcpClient, + sellerWallet, + (_receipt) => (createOfferReceipt = _receipt), + "local-31337-0", + ); + + const { logs } = createOfferReceipt; + voidOfferId = sellerCoreSdk.getCreatedOfferIdFromLogs(logs)!; + expect(voidOfferId).toBeTruthy(); + + // Wait for subgraph to index + const { blockNumber } = createOfferReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(blockNumber); + + // Verify offer is valid initially + const initialOffer = await sellerCoreSdk.getOfferById(voidOfferId); + expect(initialOffer.voided).toBe(false); + + // Void the offer + const voidResult = await mcpClient.voidOffer({ + offerId: voidOfferId, + configId: "local-31337-0", + signerAddress: sellerWallet.address, + }); + + resultIsSuccessful(voidResult, /"success": true/); + + let voidReceipt: any; + await signAndSendTransactionData( + voidResult, + mcpClient, + sellerWallet, + (_receipt) => (voidReceipt = _receipt), + "local-31337-0", + ); + + // Wait for subgraph to index + const { blockNumber: voidBlockNumber } = voidReceipt; + await sellerCoreSdk.waitForGraphNodeIndexing(voidBlockNumber); + + // Verify offer is voided + const voidedOffer = await sellerCoreSdk.getOfferById(voidOfferId); + expect(voidedOffer.voided).toBe(true); + }); + }); +}); diff --git a/docs/agentic-commerce/scripts/sign-transaction.ts b/docs/agentic-commerce/scripts/sign-transaction.ts new file mode 100644 index 0000000..a7d19d4 --- /dev/null +++ b/docs/agentic-commerce/scripts/sign-transaction.ts @@ -0,0 +1,142 @@ +#!/usr/bin/env ts-node + +// Usage: +// # CLI mode +// npx ts-node scripts/sign-transaction.ts sign \ +// --private-key "0x..." \ +// --to "0x742d35Cc6634C0532925a3b8D91319Fd" \ +// --value "1000000000000000000" \ +// --data "0x..." \ +// --gas-limit "21000" +// # JSON mode +// npx ts-node scripts/sign-transaction.ts sign-json \ +// --private-key "0x..." \ +// --json '{"to":"0x742d35Cc...","value":"1000000000000000000"}' + +import { program } from "commander"; +import { signTransaction, TransactionRequest } from "../docs/fermion/example/sign-transaction"; + +program + .name("sign-transaction") + .description("Sign a transaction using a private key") + .version("1.0.0") + .command("sign") + .description("Sign transaction data") + .requiredOption("-k, --private-key ", "Private key to sign with") + .requiredOption("-t, --to

", "Recipient address") + .option("-v, --value ", "Value to send (in wei)", "0") + .option("-d, --data ", "Transaction data", "0x") + .option("-g, --gas-limit ", "Gas limit") + .option("-p, --gas-price ", "Gas price (in wei)") + .option("--max-fee-per-gas ", "Max fee per gas (EIP-1559)") + .option( + "--max-priority-fee-per-gas ", + "Max priority fee per gas (EIP-1559)", + ) + .option("-n, --nonce ", "Transaction nonce", parseInt) + .option("-c, --chain-id ", "Chain ID", parseInt) + .option("--type ", "Transaction type (0, 1, or 2)", parseInt) + .option( + "-r, --rpc-url ", + "RPC URL to fetch nonce and gas price automatically", + ) + .action(async (options) => { + try { + const transactionData: TransactionRequest = { + to: options.to, + value: options.value, + data: options.data, + }; + + if (options.gasLimit) transactionData.gasLimit = options.gasLimit; + if (options.gasPrice) transactionData.gasPrice = options.gasPrice; + if (options.maxFeePerGas) + transactionData.maxFeePerGas = options.maxFeePerGas; + if (options.maxPriorityFeePerGas) + transactionData.maxPriorityFeePerGas = options.maxPriorityFeePerGas; + if (options.nonce !== undefined) transactionData.nonce = options.nonce; + if (options.chainId !== undefined) + transactionData.chainId = options.chainId; + if (options.type !== undefined) transactionData.type = options.type; + + const signedTransaction = await signTransaction( + transactionData, + options.privateKey, + options.rpcUrl, + ); + + console.log( + JSON.stringify( + { + success: true, + signedTransaction, + transactionData, + }, + null, + 2, + ), + ); + } catch (error) { + console.error( + JSON.stringify( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + null, + 2, + ), + ); + process.exit(1); + } + }); + +program + .command("sign-json") + .description("Sign transaction from JSON input") + .requiredOption("-k, --private-key ", "Private key to sign with") + .requiredOption("-j, --json ", "Transaction data as JSON string") + .option( + "-r, --rpc-url ", + "RPC URL to fetch nonce and gas price automatically", + ) + .action(async (options) => { + try { + const transactionData: TransactionRequest = JSON.parse(options.json); + + const signedTransaction = await signTransaction( + transactionData, + options.privateKey, + options.rpcUrl, + ); + + console.log( + JSON.stringify( + { + success: true, + signedTransaction, + transactionData, + }, + null, + 2, + ), + ); + } catch (error) { + console.error( + JSON.stringify( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + null, + 2, + ), + ); + process.exit(1); + } + }); + +if (require.main === module) { + program.parse(); +} + diff --git a/docs/agentic-commerce/src/boson/README.md b/docs/agentic-commerce/src/boson/README.md new file mode 100644 index 0000000..efa8367 --- /dev/null +++ b/docs/agentic-commerce/src/boson/README.md @@ -0,0 +1,5 @@ +## Subdirectories + +- [goat-sdk-plugin](./goat-sdk-plugin/README.md) +- [mcp-client](./mcp-client/README.md) +- [mcp-server](./mcp-server/README.md) diff --git a/docs/agentic-commerce/src/boson/goat-sdk-plugin/README.md b/docs/agentic-commerce/src/boson/goat-sdk-plugin/README.md new file mode 100644 index 0000000..e6bd37c --- /dev/null +++ b/docs/agentic-commerce/src/boson/goat-sdk-plugin/README.md @@ -0,0 +1,42 @@ +# GOAT SDK Plugin + +This directory contains the GOAT SDK plugin for the Boson Protocol. + +## Overview + +The `BosonProtocolPlugin` provides a seamless integration with the GOAT SDK, allowing developers to interact with the Boson Protocol through a simple and intuitive interface. It abstracts away the complexity of the underlying MCP server communication, enabling developers to focus on building their applications. + +### Key Features + +- **Offer Management:** Create, void, and manage offers on the Boson Protocol. +- **Dispute Resolution:** Handle disputes, including raising, retracting, and escalating them. +- **Fund Management:** Deposit and withdraw funds from the protocol. +- **Data Queries:** Retrieve information about offers, exchanges, sellers, and disputes. +- **Metadata Storage:** Store various types of metadata on IPFS. + +## How it Works + +The plugin consists of two main components: + +- **`BosonProtocolPlugin`:** The main entry point for the plugin, which registers the available tools with the GOAT SDK. +- **`BosonProtocolPluginService`:** A service that handles the direct communication with the MCP server via the MCP client, including sending requests, parsing responses, and executing transactions. + +The plugin leverages the `@goat-sdk/core` and `@goat-sdk/wallet-evm` packages to provide a consistent and reliable experience for developers. It also uses `zod` for robust validation of MCP responses and transaction data. + +## How to use + +To get started with the Boson Protocol MCP server plugin: + +- **Check out the example files in the `examples` folder** for practical usage patterns, including how to use the plugin in your own application. +- The example files import local modules (e.g. `import { bosonProtocolPlugin } from "../boson-protocol.plugin"`), but when developing your own application you should import from the published npm package: + + ```ts + import { bosonProtocolPlugin } from "@bosonprotocol/agentic-commerce"; + ``` + +- By default, the examples connect to a local MCP server (e.g. `http://localhost:3000/mcp`). If you want to use a deployed instance, **replace the URL in the examples with one of the following**: + + - **Staging:** `https://mcp-staging.bosonprotocol.io/mcp` + - **Production:** `https://mcp.bosonprotocol.io/mcp` + +--- diff --git a/docs/agentic-commerce/src/boson/goat-sdk-plugin/examples/anthropic.ts b/docs/agentic-commerce/src/boson/goat-sdk-plugin/examples/anthropic.ts new file mode 100644 index 0000000..f9e36d7 --- /dev/null +++ b/docs/agentic-commerce/src/boson/goat-sdk-plugin/examples/anthropic.ts @@ -0,0 +1,132 @@ +import readline from "node:readline"; + +import { createAnthropic } from "@ai-sdk/anthropic"; +import { viem } from "@goat-sdk/wallet-viem"; +import { generateText } from "ai"; +import { createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { polygonAmoy } from "viem/chains"; + +import { getOnChainTools } from "../adapters/vercel-ai"; +import { bosonProtocolPlugin } from "../boson-protocol.plugin"; +// Example test for the Boson MCP Server plugin +async function testBosonMcpServerPlugin() { + // Initialize wallet client with private key + const rawPrivateKey = process.env.PRIVATE_KEY; + if (!rawPrivateKey) { + throw new Error("PRIVATE_KEY environment variable is required"); + } + const anthopicApiKey = process.env.ANTHROPIC_API_KEY; + if (!anthopicApiKey) { + throw new Error("ANTHROPIC_API_KEY environment variable is required"); + } + + // Ensure private key has 0x prefix and is the correct length + const privateKey = rawPrivateKey.startsWith("0x") + ? rawPrivateKey + : `0x${rawPrivateKey}`; + + // Validate private key format + if (privateKey.length !== 66) { + // 0x + 64 hex characters = 66 total + throw new Error( + `Invalid private key length: expected 66 characters (0x + 64 hex), got ${privateKey.length}`, + ); + } + + if (!/^0x[0-9a-fA-F]{64}$/.test(privateKey)) { + throw new Error("Invalid private key format: must be hex string"); + } + + const account = privateKeyToAccount(privateKey as `0x${string}`); + + // Use testnet for development (change chain as needed) + const chain = polygonAmoy; + + // Define custom RPC URL (optional) + const rpcUrl = process.env.RPC_URL || "https://rpc-amoy.polygon.technology"; + console.log("Using RPC URL:", rpcUrl); + + const walletClient = createWalletClient({ + account, + chain, + transport: http(rpcUrl), + }); + + // Check wallet balance before proceeding + console.log("Wallet address:", account.address); + console.log("Chain:", chain.name, "ID:", chain.id); + + // Check balance using a public client + const { createPublicClient } = await import("viem"); + const publicClient = createPublicClient({ + chain, + transport: http(rpcUrl), + }); + + const balance = await publicClient.getBalance({ address: account.address }); + + if (balance === 0n) { + console.error("❌ Wallet has no MATIC tokens!"); + console.log("🚰 Get test MATIC from: https://faucet.polygon.technology/"); + console.log("💳 Your address:", account.address); + process.exit(1); + } + + // Get tools with the Boson MCP Server plugin + const tools = await getOnChainTools({ + wallet: viem(walletClient), + plugins: [ + bosonProtocolPlugin({ url: "http://localhost:3000/mcp" }), + // ...other plugins + ], + }); + + console.log("Available tools:", Object.keys(tools)); + const anthropic = createAnthropic({ + apiKey: anthopicApiKey, + }); + // Example usage of the plugin's tool + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + while (true) { + const prompt = await new Promise((resolve) => { + rl.question('Enter your prompt (or "exit" to quit): ', resolve); + }); + + if (prompt === "exit") { + rl.close(); + break; + } + + console.log("\n-------------------\n"); + console.log("TOOLS CALLED"); + console.log("\n-------------------\n"); + try { + const result = await generateText({ + model: anthropic("claude-4-sonnet-20250514"), + tools: tools, + maxSteps: 10, // Maximum number of tool invocations per request + prompt: prompt, + onStepFinish: (event) => { + console.log(event.toolResults); + }, + }); + + console.log("\n-------------------\n"); + console.log("RESPONSE"); + console.log("\n-------------------\n"); + console.log(result.text); + } catch (error) { + console.error(error); + } + console.log("\n-------------------\n"); + } + process.exit(0); +} + +// Run the test +testBosonMcpServerPlugin().catch(console.error); diff --git a/docs/agentic-commerce/src/boson/goat-sdk-plugin/examples/vercel.ts b/docs/agentic-commerce/src/boson/goat-sdk-plugin/examples/vercel.ts new file mode 100644 index 0000000..2b13aff --- /dev/null +++ b/docs/agentic-commerce/src/boson/goat-sdk-plugin/examples/vercel.ts @@ -0,0 +1,138 @@ +import { viem } from "@goat-sdk/wallet-viem"; +import { createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { polygonAmoy } from "viem/chains"; + +import validMinimalOfferMetadata from "../../../../e2e/boson/tests/metadata/minimalOffer.json"; +import { getOnChainTools } from "../adapters/vercel-ai"; +import { bosonProtocolPlugin } from "../boson-protocol.plugin"; +// Example test for the Boson MCP Server plugin +async function testBosonMcpServerPlugin() { + // Initialize wallet client with private key + const rawPrivateKey = process.env.PRIVATE_KEY; + if (!rawPrivateKey) { + throw new Error("PRIVATE_KEY environment variable is required"); + } + + // Ensure private key has 0x prefix and is the correct length + const privateKey = rawPrivateKey.startsWith("0x") + ? rawPrivateKey + : `0x${rawPrivateKey}`; + + // Validate private key format + if (privateKey.length !== 66) { + // 0x + 64 hex characters = 66 total + throw new Error( + `Invalid private key length: expected 66 characters (0x + 64 hex), got ${privateKey.length}`, + ); + } + + if (!/^0x[0-9a-fA-F]{64}$/.test(privateKey)) { + throw new Error("Invalid private key format: must be hex string"); + } + + const account = privateKeyToAccount(privateKey as `0x${string}`); + + // Use testnet for development (change chain as needed) + const chain = polygonAmoy; // or sepolia for Ethereum testnet + + // Define custom RPC URL (optional) + const rpcUrl = process.env.RPC_URL || "https://rpc-amoy.polygon.technology"; + console.log("Using RPC URL:", rpcUrl); + + const walletClient = createWalletClient({ + account, + chain, + transport: http(rpcUrl), + }); + + // Check wallet balance before proceeding + console.log("Wallet address:", account.address); + console.log("Chain:", chain.name, "ID:", chain.id); + + // Check balance using a public client + const { createPublicClient } = await import("viem"); + const publicClient = createPublicClient({ + chain, + transport: http(rpcUrl), + }); + + const balance = await publicClient.getBalance({ address: account.address }); + + if (balance === 0n) { + console.error("❌ Wallet has no MATIC tokens!"); + console.log("🚰 Get test MATIC from: https://faucet.polygon.technology/"); + console.log("💳 Your address:", account.address); + process.exit(1); + } + + // Get tools with the Boson MCP Server plugin + const tools = await getOnChainTools({ + wallet: viem(walletClient), + plugins: [ + bosonProtocolPlugin({ url: "http://localhost:3000/mcp" }), + // ...other plugins + ], + }); + + console.log("Available tools:", Object.keys(tools)); + // Example usage of the plugin's tool + try { + // First, store the metadata to IPFS + console.log("📄 Storing metadata to IPFS..."); + const storeMetadataResult = await tools.store_product_v1_metadata.execute( + { + ...validMinimalOfferMetadata, + configId: "staging-80002-0", + signerAddress: walletClient, + }, + { + toolCallId: "store_product_v1_metadata", + messages: [], + }, + ); + console.log("Metadata storage result:", storeMetadataResult); + + if (storeMetadataResult.success) { + const metadataResponse = JSON.parse(storeMetadataResult.data as string); + const { metadataUri, metadataHash } = metadataResponse; + + console.log("✅ Metadata stored successfully:"); + console.log(" URI:", metadataUri); + console.log(" Hash:", metadataHash); + + // Now create the offer with the metadata URI and hash + console.log("🎁 Creating offer..."); + const createOfferResult = await tools.create_offer.execute( + { + price: "1", + sellerDeposit: "0", + buyerCancellationPenalty: "0", + quantityAvailable: 99, + validFromDateInMS: Date.now(), + validUntilDateInMS: Date.now() + 1000 * 60 * 60 * 24, // 1 day + voucherRedeemableFromDateInMS: Date.now(), + voucherRedeemableUntilDateInMS: 0, + disputePeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + voucherValidDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + resolutionPeriodDurationInMS: 1000 * 60 * 60 * 24 * 7, // 7 day + metadataUri, + metadataHash, + }, + { + toolCallId: "create_offer", + messages: [], + }, + ); + console.log("✅ Offer created successfully:", createOfferResult); + } else { + console.error("❌ Failed to store metadata:", storeMetadataResult); + } + } catch (error) { + console.error("Error executing tool:", error); + } + process.exit(0); +} + +// Run the test +testBosonMcpServerPlugin().catch(console.error); diff --git a/docs/agentic-commerce/src/boson/mcp-client/README.md b/docs/agentic-commerce/src/boson/mcp-client/README.md new file mode 100644 index 0000000..49a4c8a --- /dev/null +++ b/docs/agentic-commerce/src/boson/mcp-client/README.md @@ -0,0 +1,73 @@ +# MCP Client + +This directory contains the MCP client for the Boson Protocol. + +## Overview + +The `BosonMCPClient` is a client-side library that simplifies interaction with the Boson Protocol MCP server. It provides a high-level API that abstracts away the details of the MCP communication protocol, allowing developers to easily integrate Boson Protocol functionality into their applications. + +### Key Features + +- **Tool-Based Interaction:** The client exposes methods that directly correspond to the tools available on the MCP server, providing a clear and intuitive way to perform actions on the protocol. +- **Parameter Validation:** It uses `zod` schemas to validate the parameters for each tool, ensuring that requests are correctly formatted and reducing the likelihood of errors. +- **Resource Access:** The client provides methods for reading resources from the protocol, such as funds, disputes, exchanges, offers, and sellers. +- **Simplified Metadata Storage:** It includes helper methods for storing metadata on IPFS, automatically handling the creation of fair exchange policies. + +## How it Works + +The `BosonMCPClient` extends the `BaseMCPClient` and implements methods for each of the tools available on the Boson Protocol MCP server. When a method is called, the client constructs a tool call request with the specified parameters and sends it to the MCP server. It then returns the server's response, which can be used to execute transactions or retrieve data. + +The client also includes client-side validation schemas that ensure the correctness of the data before it is sent to the server. This helps to prevent errors and provides a better developer experience. + +## Installation & Setup + +To use the MCP client in your own project: + +1. **Installation:** + + ```sh + pnpm add @bosonprotocol/agentic-commerce + ``` + +2. **Import and use the client:** + ```ts + import { BosonMCPClient } from "@bosonprotocol/agentic-commerce"; + const client = new BosonMCPClient(); + // Example: await client.getOffers({ ...params }) + ``` + +## Usage + +The `BosonMCPClient` exposes async methods that correspond 1:1 to the tools and resources available on the MCP server. All tool names and parameters match the server exactly (see the server README for the full list). + +**Example:** + +```ts +const offers = await client.getOffers({ ...params }); +const funds = await client.readFunds({ ...params }); +``` + +### Available Methods + +- All server tools (e.g. `create_seller`, `create_offer`, `void_offer`, `get_funds`, etc.) are available as camelCase methods (e.g. `createSeller`, `createOffer`, `voidOffer`, `getFunds`, ...). +- All server resources (e.g. `funds`, `offers`, `exchanges`, etc.) are available as `readX` methods (e.g. `readFunds`, `readOffers`, ...). +- All parameters are validated with `zod` schemas before sending. + +See the source code for the full list of methods and their signatures. + +## Running Tests + +To run the unit tests for the MCP client in this repository: + +```sh +pnpm test src/boson/mcp-client/ +``` + +## Development Notes + +- The client is designed to be used with a running MCP server (see the server README for setup). +- There are read-only tools that perform the same function as their corresponding resources. If you're unsure which one to use, default to the tool version (e.g. use the `getFunds` function instead of the `readFunds` function). + +--- + +For more details, see the code in this directory and the Boson Protocol documentation. diff --git a/docs/agentic-commerce/src/boson/mcp-server/README.md b/docs/agentic-commerce/src/boson/mcp-server/README.md new file mode 100644 index 0000000..2888178 --- /dev/null +++ b/docs/agentic-commerce/src/boson/mcp-server/README.md @@ -0,0 +1,288 @@ +# MCP Server + +This directory contains the MCP server for the Boson Protocol. For a full end-to-end agent guide covering buying, selling, and exchange lifecycle, see the [AI Agent Guide](../../../AGENTS.md) at the repository root. + +## Overview + +The `BosonMCPServer` is a server-side application that exposes the functionality of the Boson Protocol through the Model Context Protocol (MCP). It acts as a gateway, allowing clients to interact with the protocol by calling a set of defined tools and resources. + +### Key Features + +- **Tool-Based Interface:** The server provides a comprehensive set of tools for interacting with the Boson Protocol, including creating offers, managing disputes, and querying data. +- **Request Handling:** It handles incoming requests from MCP clients, validates them, and routes them to the appropriate handlers. +- **Core SDK Integration:** The server uses the Boson Protocol Core SDK to interact with the underlying blockchain and smart contracts. +- **Resource Endpoints:** It exposes resource endpoints that allow clients to query data from the protocol. +- **Validation:** The server uses `zod` schemas to validate incoming requests and ensure data integrity. + +## How it Works + +The `BosonMCPServer` class initializes an MCP server and registers a set of tool handlers. Each tool handler is responsible for a specific action on the protocol, such as creating a seller or voiding an offer. When a client calls a tool, the server receives the request, validates the parameters, and then executes the corresponding handler. + +The server also sets up resource endpoints for querying data. These endpoints use URI templates to define the structure of the resource URLs and allow for flexible filtering and pagination. There are some tools that do the same as their resource counterparty, if possible, choose the tool version as it's more flexible. + +The server can be run in two modes: as a standalone HTTP server or as a stdio-based server. The HTTP server provides a web interface and a health check endpoint, while the stdio server is suitable for use in environments where a direct connection to the server is required. + +## Interacting with the MCP Server + +You can interact with the MCP server using the [MCP Inspector](https://www.npmjs.com/package/@modelcontextprotocol/inspector), a CLI and UI tool for exploring and testing MCP servers. This is useful for development, debugging, and learning about available tools and resources. + +Example usage: + +```sh +pnpm start:boson:http:inspector +# or, for development mode: +pnpm dev:boson:http:inspector +``` + +This will connect the Inspector to your running MCP server (in HTTP mode) and let you browse, call tools, and inspect responses interactively. + +## Tools (Actions) + +The MCP server exposes the following tools (actions) for interacting with the Boson Protocol: + +- **Seller & Buyer Management** + + - `create_seller`: Create a new seller on Boson Protocol. + - `update_seller`: Update an existing seller on Boson Protocol. + - `create_buyer`: Create a new buyer on Boson Protocol. + - `get_sellers`: Get seller entities with flexible query parameters including pagination, ordering, and filtering. + - `get_sellers_by_address`: Get seller entities by address with flexible query parameters including pagination, ordering, and filtering. + - `get_dispute_resolvers`: Get dispute resolver entities with flexible query parameters including pagination, ordering, and filtering. + +- **Offer Management** + + - `create_offer`: Create a new offer on Boson Protocol. Tip: Consider storing metadata first using store_product_v1_metadata, store_bundle_metadata, or store_base_metadata tools to get URIs for the metadata fields. + - `create_offer_with_condition`: Create a new offer with a specific condition on Boson Protocol. Tip: Consider storing metadata first using store_product_v1_metadata, store_bundle_metadata, or store_base_metadata tools to get URIs for the metadata fields. + - `sign_full_offer`: Return the typed data message to sign from the offer data of a non-listed offer. The message must be signed by the offer creator and the signature passed to `create_offer_and_commit` (to create and commit in one step) or to `void_non_listed_offer` (to void the offer before it is listed). + - `create_offer_and_commit`: Create and commit to a non-listed offer in one atomic step. Requires a signature previously obtained via `sign_full_offer`. + - `void_offer`: Void an existing listed offer by calling the OfferHandlerFacet contract. + - `void_non_listed_offer`: Void a non-listed offer. Useful if the offer creator changes their mind before listing. Requires a signature from `sign_full_offer`. + - `void_non_listed_offer_batch`: Void a batch of non-listed offers in one transaction. Each offer requires a signature from `sign_full_offer`. + - `get_offers`: Get offer entities with flexible query parameters including pagination, ordering, and filtering. + - `get_all_products_with_not_voided_variants`: Get all products with not voided variants with flexible query parameters including pagination, ordering, and filtering. + - `store_product_v1_metadata`: Store ProductV1 metadata to IPFS and return metadata URI and hash. + - `store_bundle_metadata`: Store Bundle metadata to IPFS. You should call the `store_bundle_item_*` tools before calling this. + - `store_base_metadata`: Store custom metadata to IPFS and return metadata URI and hash. + - `store_bundle_item_product_v1_metadata`: Store Bundle Item ProductV1 metadata to IPFS, useful for store_bundle_metadata tool. + - `store_bundle_item_nft_metadata`: Store Bundle Item NFT metadata to IPFS, useful for store_bundle_metadata tool. + - `validate_metadata`: Validate metadata against the Boson Protocol schema without storing it to IPFS. Useful for checking metadata correctness before storage. + - `render_contractual_agreement`: Render a contractual agreement using renderContractualAgreement from Boson Protocol SDK. + +- **Exchange Management** + + - `get_exchanges`: Get exchange entities with flexible query parameters including pagination, ordering, and filtering. + - `approve_exchange_token`: Grant ERC-20 allowance to the Boson Protocol contract. Must be called before `commit_to_offer` or `deposit_funds` when the offer uses a non-native (ERC-20) exchange token. + - `commit_to_offer`: Commit to a seller-created offer to create an exchange. For ERC-20 offers, call `approve_exchange_token` first. + - `commit_to_buyer_offer`: Commit to a buyer-initiated offer to create an exchange (seller side). + - `complete_exchange`: Complete an exchange after voucher redemption. + - `cancel_voucher`: Cancel an existing voucher by buyer. + - `revoke_voucher`: Revoke an existing voucher by seller assistant. + - `redeem_voucher`: Redeem an existing voucher by buyer. + +- **Dispute Management** + + - `raise_dispute`: Raise a dispute for an exchange. + - `resolve_dispute`: Resolve a dispute with mutual agreement. + - `retract_dispute`: Retract a previously raised dispute. + - `escalate_dispute`: Escalate a dispute to the dispute resolver. + - `decide_dispute`: Decide an escalated dispute (dispute resolver only). + - `refuse_escalated_dispute`: Refuse an escalated dispute (dispute resolver only). + - `expire_dispute`: Expire a dispute. + - `expire_dispute_batch`: Expire multiple disputes in a batch. + - `expire_escalated_dispute`: Expire an escalated dispute. + - `extend_dispute_timeout`: Extend the timeout for a dispute. + - `get_disputes`: Get dispute entities with flexible query parameters including pagination, ordering, and filtering. + - `get_dispute_by_id`: Get a specific dispute by ID with optional query variables. + - `create_dispute_resolution_proposal`: Create a dispute resolution proposal signature. + +- **Funds Management** + + - `deposit_funds`: Deposit funds to an entity treasury. + - `withdraw_funds`: Withdraw funds from an entity treasury. + - `get_funds`: Get funds entities with flexible query parameters including pagination, ordering, and filtering. + +- **Meta Transactions** + + - `send_meta_transaction`: Send a meta transaction using Boson Protocol SDK. + - `send_native_meta_transaction`: Send a native meta transaction using Boson Protocol SDK. + - `send_forwarded_meta_transaction`: Send a forwarded meta transaction using Boson Protocol SDK. + +- **Transaction Sending** + + - `send_signed_transaction`: Broadcasts a transaction signed locally with your wallet (e.g. ethers `wallet.signTransaction(tx)`) to the Ethereum network. Signing is done in your own infrastructure — the server never receives private keys. + +- **Agent Registry** + + - `register_agent`: Register a dACP agent with the MCP server. Validates the agent definition against the dACP registry schema and opens a GitHub pull request to add the agent to the registry. + - `get_registered_agents`: Get all dACP agents currently registered with the MCP server. + +- **Configuration** + - `get_config_ids`: Get the list of supported config IDs for this MCP server. + - `get_supported_tokens`: Get the list of supported exchange tokens for the given configId. + +## Resources + +The server exposes resource endpoints for querying protocol data. Each resource supports filtering, ordering, and pagination: + +- `offers://entities` — Query offers +- `products://entities` — Query products +- `exchanges://entities` — Query exchanges +- `disputes://entities` — Query disputes +- `funds://entities` — Query funds +- `sellers://entities` — Query sellers +- `sellers-by-address://entities` — Query sellers by address +- `dispute-resolvers://entities` — Query dispute resolvers +- `config://ids` — Query supported config IDs +- `registered-agents://` — List all dACP agents registered with this server + +Each resource supports specific filter parameters (see code for details). + +## Prompts + +The server exposes MCP prompts that guide an AI agent through multi-step workflows: + +- `create-seller-if-needed`: Create a seller if one does not already exist for the given `signerAddress`, returning the `sellerId`. Requires `configId` and `signerAddress` parameters. +- `create-offer`: Create an offer on Boson Protocol for the given `configId` and `signerAddress` (seller). Tips for metadata storage and seller creation are included in the prompt. Requires `configId` and `signerAddress` parameters. + +## Transaction Signing Pattern + +Every state-changing tool returns **unsigned transaction data** — it does not execute automatically. The caller is responsible for signing and broadcasting. The 3-step pattern: + +``` +1. Call tool → returns { unsignedTx: { to, data, ... } } +2. Sign locally with wallet → e.g. ethers `wallet.signTransaction(tx)` returns "0x..." +3. send_signed_transaction → returns { txHash, blockNumber, ... } +``` + +The `signerAddress` parameter on every write tool specifies which Ethereum account is performing the action (authorization check). The MCP server never receives private keys — all signing happens locally in your own infrastructure. + +## Exchange Lifecycle + +``` +[Listed Offer] + │ + ▼ commit_to_offer (buyer) + COMMITTED ──── revoke_voucher (seller) ──► REVOKED + │ + ├── cancel_voucher (buyer) ───────────► CANCELLED + │ + ▼ redeem_voucher (buyer) + REDEEMED ───── complete_exchange ──────► COMPLETED + │ + └── raise_dispute (buyer) + │ + DISPUTED ── retract_dispute ─────► RETRACTED + │ + ├── resolve_dispute (mutual) ─► RESOLVED + │ + └── escalate_dispute (buyer) + │ + ESCALATED ── decide_dispute (DR) ──► DECIDED + │ + ├── refuse_escalated_dispute ────► REFUSED + └── resolve_dispute (mutual) ────► RESOLVED +``` + +## Seller Quick-Start Flow + +1. Call `get_sellers_by_address` — check if you already have a seller account +2. If not: `create_seller` → sign → `send_signed_transaction` → extract `sellerId` from logs +3. `store_product_v1_metadata` → get `metadataUri` + `metadataHash` +4. `create_offer` (with metadata URI/hash) → sign → `send_signed_transaction` → extract `offerId` +5. Monitor: `get_exchanges` with `sellerId` filter +6. Withdraw: `withdraw_funds` after exchanges complete + +## Buyer Quick-Start Flow + +1. `get_offers` or `get_all_products_with_not_voided_variants` — browse listings +2. If ERC-20 offer: `approve_exchange_token` → sign → `send_signed_transaction` +3. `commit_to_offer` → sign → `send_signed_transaction` → extract `exchangeId` +4. `redeem_voucher` → sign → `send_signed_transaction` (starts dispute period) +5. If issue: `raise_dispute` (within dispute period) +6. `complete_exchange` — after redemption + dispute period (or call early to release funds) + +## Hosted MCP Endpoints + +No local setup required for connecting to Boson's hosted servers: + +- **Staging:** `https://mcp-staging.bosonprotocol.io/mcp` +- **Production:** `https://mcp.bosonprotocol.io/mcp` + +## How to Run + +First, build the project: + +```sh +pnpm build +``` + +Then, you can run the MCP server in different modes: + +### HTTP Server Mode + +```sh +pnpm start:boson:http +``` + +This starts the server with HTTP endpoints and a health check. + +### Stdio Server Mode + +```sh +pnpm start:boson +``` + +This starts the server in stdio mode (for direct integration with MCP Inspector or other tools). + +### Development Mode + +```sh +pnpm dev:boson:http +``` + +or + +```sh +pnpm dev:boson +``` + +These commands start the server in watch mode for development. + +### Configuration + +The server uses `mcpServer.json` for configuration. You can copy and modify `mcpServer.example.json` as needed. + +Otherwise run this command to create your `mcpServer.json`: + +```sh +pnpm setup-mcp-server +``` + +#### Note on `CONFIG_IDS` and `configId` + +All read-only tools and resources require a `configId` parameter. This value must match one of the IDs set in the `CONFIG_IDS` environment variable (see `.env` or `.env.example`) or in your `mcpServer.json`, depending on how you run the server. If you call a read-only tool/resource, you must provide a valid configId (e.g. `local-31337-0`, `staging-80002-0`, `production-137-0`, etc.). + +##### Example CONFIG_IDS values for different deployments: + +- **Local development:** + ```env + CONFIG_IDS=local-31337-0 + ``` +- **Testing (Amoy, Base, Sepolia, etc):** + ```env + CONFIG_IDS=testing-80002-0,testing-84532-0,testing-11155111-0,testing-11155420-0,testing-421614-0 + ``` +- **Staging:** + ```env + CONFIG_IDS=staging-80002-0,staging-84532-0,staging-11155111-0,staging-11155420-0,staging-421614-0 + ``` +- **Production:** + ```env + CONFIG_IDS=production-137-0,production-42161-0,production-8453-0,production-10-0,production-1-0 + ``` + +For non read-only tools (i.e., those that perform write or state-changing actions), you must also pass a `signerAddress` (an Ethereum address) along with the `configId` parameter. These tools do not sign actions themselves—instead, they return the data you need to sign locally on your side (for example, using your wallet or signing infrastructure). The MCP server never receives private keys. + +--- + +For more details on each tool and resource, see the handler and filter files in this directory. diff --git a/docs/agentic-commerce/src/fermion/README.md b/docs/agentic-commerce/src/fermion/README.md new file mode 100644 index 0000000..ad68155 --- /dev/null +++ b/docs/agentic-commerce/src/fermion/README.md @@ -0,0 +1,136 @@ +# High Value Asset Module MCP Server + +![Tests](https://img.shields.io/badge/tests-50%2F50%20passing-brightgreen) + +> **Note:** "Fermion" is no longer used as a brand. The former Fermion Protocol was integrated into the Boson codebase in 2025 and is now the **High Value Asset Module** of Boson Protocol. The `fermion` directory name, `@fermionprotocol/*` package imports, and `fermion` script aliases are retained for code-level compatibility. + +An MCP (Model Context Protocol) server that integrates with the High Value Asset Module's core-components to enable AI agents to create offers on Boson's high-value/luxury asset marketplace with advanced features and verification systems. + +## 🚀 Overview + +The High Value Asset Module (formerly Fermion Protocol) is a Boson Protocol marketplace layer that adds verifier and custodian roles, enhanced verification systems, custodian services, and advanced royalty mechanisms on top of Boson's primitives. This MCP server provides AI agents with the ability to interact with that module. + +## 🛠️ Installation + +```bash +pnpm install +pnpm build +pnpm start:fermion +``` + +and open http://127.0.0.1:6274/ in your web browser + +## 🧪 Development + +```bash +pnpm install +pnpm watch +pnpm dev:fermion +``` + +and open http://127.0.0.1:6274/ in your web browser + +## 🔧 Environment Setup + +1. Copy `mcpServer.example.json` to `mcpServer.json` if not automatically done +2. Fill in your INFURA_IPFS_PROJECT_ID and INFURA_IPFS_PROJECT_SECRET +3. Change CONFIG_ID if needed +4. Set SIGNER_ADDRESS to your Ethereum address + +## 🤖 Usage with AI Agents + +Add the High Value Asset Module server to your MCP configuration: + +```json +{ + "mcpServers": { + "fermion-server": { + "command": "node", + "args": [ + "/Users//Documents/boson-protocol-mcp-server/dist/fermion/index.js" + ], + "env": {} + } + } +} +``` + +## 🔨 Available Tools + +### `initialize_sdk` + +Manually initialize the High Value Asset Module SDK with custom configuration. + +**Parameters:** + +- `signerAddress` (optional): Ethereum address of the signer (defaults to SIGNER_ADDRESS env var) +- `configId` (optional): Configuration ID (defaults to CONFIG_ID env var) +- `infuraIpfsProjectId` (optional): Infura IPFS project ID +- `infuraIpfsProjectSecret` (optional): Infura IPFS project secret + +### `create_offer` + +Creates a new offer on the High Value Asset Module with advanced features and verification systems. + +**Parameters:** + +- `productTitle` (required): Title of the product +- `productDescription` (required): Description of the product +- `productCategory` (required): Category of the product +- `sellerId` (required): Seller ID in the protocol +- `sellerDeposit` (required): Seller deposit in wei as string +- `verifierId` (required): Verifier ID for product verification +- `verifierFee` (required): Verifier fee in wei as string +- `custodianId` (required): Custodian ID for custody services +- `custodianFee` (required): Custodian fee object with amount and period +- `facilitatorId` (required): Facilitator ID +- `facilitatorFeePercent` (required): Facilitator fee percentage +- `exchangeToken` (required): Token address for payments +- `withPhygital` (optional): Whether the product includes physical components +- `royaltyInfo` (optional): Royalty information with recipients and BPS + +### `send_signed_transaction` + +Send a signed transaction to the Ethereum network. + +**Parameters:** + +- `signedTransaction` (required): The signed transaction data as hex string + +### `generate_asset_verification_email` + +Generate email parameters for asset verification delivery arrangement. + +**Parameters:** + +- `tokenId` (required): The fNFT token ID to generate verification email for +- `ipfsGateway` (optional): Custom IPFS gateway URL (defaults to IPFS_GATEWAY env var or Boson Protocol's Infura IPFS gateway) + +## 🔗 Links + +- [Boson Protocol Website](https://www.bosonprotocol.io/) +- [Boson Protocol Documentation](https://docs.bosonprotocol.io) + +### Legacy Fermion Protocol references + +These external resources predate the 2025 integration into Boson but are retained because the published packages, images, and contracts are still named `fermion*`: + +- [Fermion Protocol Website (legacy)](https://fermionprotocol.io) +- [Fermion Protocol Documentation (legacy)](https://docs.fermionprotocol.io) +- [Fermion Protocol GitHub (legacy)](https://github.com/fermionprotocol) +- [Fermion Protocol Core Components (legacy)](https://github.com/fermionprotocol/core-components) + +## 🏗️ Architecture + +The High Value Asset Module MCP server is structured as follows: + +``` +src/fermion/ +├── index.ts # Main server entry point +├── schemas.ts # Zod validation schemas +├── validation.ts # Input validation logic +└── handlers/ + ├── sdk.ts # SDK initialization handler + ├── offers.ts # Advanced offer creation handler + └── transactions.ts # Transaction handling +``` From 84509cc999e21977757a3b4d144c045236e07d17 Mon Sep 17 00:00:00 2001 From: Ludovic Levalleux Date: Wed, 29 Apr 2026 17:55:43 +0100 Subject: [PATCH 2/2] fix: add docs/agentic-commerce/ to eslint ignore list --- eslint.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 7b35290..5d01c2f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -21,6 +21,7 @@ export default [ "coverage/", "eslint.config.mjs", "scripts/", + "docs/agentic-commerce/", ], }, // Type-aware linting setup