Skill: element-som/element-nft-drops (version 1.0.2)

--- name: element-drop description: Use when the user mentions Element plus drops, NFT creation, NFT collection launch, mint setup, sale stages, prereveal media, preview media, or publishing/pausing/resuming a drop. Also use when they want to create an NFT on Element, create an Element collection/drop, configure mint price/supply/payment token/stages, preview current drop settings/design, update drop media or description, publish a configured drop, or provide chainName, slug, contractAddress, paymentToken, or a local image path for drop work. Read-only preview/list actions may run immediately; every state-changing flow must show an execution preview first, clearly distinguish preview from real execution, then wait for confirmation. requires: ["node", "jq"] metadata: {"openclaw":{"requires":{"bins":["node","npm"],"env":["ELEMENT_WALLET_PRIVATE_KEY"]},"primaryEnv":"ELEMENT_WALLET_PRIVATE_KEY"}} --- # Element Drop Use this skill for Element Drop lifecycle work: create a drop, preview current configuration, update settings or design, publish a configured drop, resume a paused drop, or pause a live drop. This skill is not for marketplace trading, collection analytics, wallet portfolio inspection, or generic Element protocol research. Run the bundled lifecycle commands under `scripts/`; do not default to ad-hoc API calls. ## Runtime Setup Required environment variable: - `ELEMENT_WALLET_PRIVATE_KEY` Example: ```bash export ELEMENT_WALLET_PRIVATE_KEY="your_wallet_private_key_here" ``` The private key must only be provided through `ELEMENT_WALLET_PRIVATE_KEY`; do not paste it into chat, JSON payloads, files for this skill, or command arguments. The scripts validate it as a `0x`-prefixed 32-byte hex key, use it only for local message and transaction signing, and redact the configured key from known error output. The raw private key is never sent over the network. The scripts may upload media, call Element APIs, and broadcast signed transactions after confirmation, so use a dedicated low-risk wallet. Only upload user-provided NFT media images. Do not upload secrets, keys, dotfiles, shell configs, environment files, source archives, or arbitrary local documents. The bundled scripts validate uploads as regular local image files and reject symlinks, unsupported extensions, oversized files, and files whose bytes do not match the declared image type. ## Routing Contract First classify the task, then read the matching reference before you answer with field guidance, collect parameters, or run commands: - `Create`: new Element NFT collection/drop, stopping before publish - `List`: show current drops and their `name` / `slug` - `Preview`: inspect existing settings, design, prereveal media, or public availability - `Update`: change settings, prereveal image, design fields, design media, or stages - `Publish`: publish, resume a paused drop, or pause a live drop - `List chains`: inspect supported chains or payment tokens - Create: [references/create.md](./references/create.md) - List: [references/list.md](./references/list.md) - Preview: [references/preview.md](./references/preview.md) - Update: [references/update.md](./references/update.md) - Publish: [references/publish.md](./references/publish.md) Do not guess supported chains, supported payment tokens, lifecycle defaults, or field constraints from memory. If the user is doing a relevant operation and asks or implies uncertainty about those values, read the matching reference first, then use the read-only command when live data is needed. Do not use this skill for buying, selling, bidding, canceling, floor price, volume, rankings, activity history, inventory, or portfolio queries. If the user asks about field meaning or code behavior in the context of Element Drop lifecycle work, this skill may be used for explanation, but do not run lifecycle commands unless the user is actually performing a lifecycle action. ## Confirmation Rules Read-only commands run immediately: - `list-user-drops` - `preview-drop` - `list-chains` State-changing commands require preview first: - `create-drop` - `update-drop` - `publish-drop` For any state-changing command: 1. Gather the minimum required parameters for the lifecycle stage. 2. Run preview mode first. 3. Show a concise execution preview using business language. 4. Wait for explicit positive confirmation. 5. Execute only after confirmation. If execution includes an onchain transaction, also state that continuing will send a real blockchain transaction, identify the user-visible action that sends it, and make clear that preview mode does not broadcast. Every preview, execution confirmation, and failure report must repeat the selected chain. For create/deploy flows, also repeat the contract `symbol`. Never silently switch chains after an RPC or network problem; if the requested chain fails, report that chain and ask for a new explicit user instruction before using another chain. ## User-Facing Parameters General: - Use `chainName`; do not ask users for chain IDs. - Use `slug` as the main identifier for existing drops. - Resolve backend identifiers internally. - Do not ask for hidden backend IDs when `slug` is enough. - Local media paths must be absolute. - Local media paths must point to real image files (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`); never use paths to credentials or system files. - `paymentToken` accepts token name, symbol, or address. Settings: - `edition`: `limited` or `open` - `maxSupply`: required when switching to limited edition - `dropBeginTime`: mint start time - `paymentToken`: sale currency - `stages`: full mint stage array replacement when provided - `preReveal`: pre-reveal image path Design: - `previewMedia`: design preview image path array - `name` - `description` / `desc` - `website`: must be a complete URL - `twitter`, `instagram`, `discord`, `telegram`, `medium`: users may provide suffix or full link; send suffix to backend - `bannerFilePath` - `dropFeaturedImage` Do not expose backend-only identifiers, status flags, or compatibility storage names in user-facing summaries. Use business language such as chain, slug, live/paused/draft, prereveal image, preview images, and availability. ## Lifecycle Rules Create: - Required: `chainName`, `name`, `symbol`, `preReveal`. - When a create request starts, proactively state these required fields, ask for any that are missing, and then state the defaults that will be applied if the user does not override them. - Optional overrides include settings, stages, design media, design copy, website/social links, and payment token. - Default mint start time is the next day. - Default edition is limited edition with total supply `999`. - Default preview image is the same image as `preReveal`. - Default stage is one public stage with sale price `0`, duration `3 days`, phase supply equal to the collection supply, max `1` mint per wallet, no interval, and public mode. - Create deploys and configures the drop but does not publish by default. - After create succeeds, tell the user the drop is configured but not live yet, then guide them to preview publishing the same `chainName` + `slug` before confirmed publish execution. Update: - Required: `chainName`, `slug`. - Patch semantics: only explicitly provided fields change; omitted fields keep remote values. - If `stages` is provided, it replaces the full stage array. - Stage IDs are not user-controlled. - Updating settings or prereveal on a live drop requires republish and therefore an onchain confirmation. - Updating a paused drop, design-only fields, design media, or collection profile fields does not force republish. Publish: - Required: `chainName`, `slug`. - Covers first publish, resuming a paused drop, and pausing a live drop. - Default target is live. - If the user asks to keep it paused or pause a live drop, use the paused publish target. - Publish requires an existing configured prereveal image. The publish flow prepares that image for the live drop configuration automatically; if readiness times out, tell the user to re-upload the prereveal image and retry. ## Web-Only Features Element Drop supports these features, but this skill does not currently configure them: - Whitelist stage configuration - Design detail sections For those, use the web editor and replace `your-slug` with the actual drop slug: - [https://element.market/collections/your-slug/edit/drop](https://element.market/collections/your-slug/edit/drop) ## Reporting Contract For previews, clearly label the response as preview-only and say no changes were applied. For execution, summarize only the user-visible result. Include the relevant fields for the lifecycle stage: - Create: chain, symbol, slug, contract address, collection URL, drop URL, edit URL, settings summary, design summary, recommended next publish action - List: chain, wallet, drop names, slugs, returned count - Preview: chain, slug, availability state, mint start time, edition, supply, payment token, stages, design/prereveal media status, links - Update: chain, slug, fields changed, whether republish was needed, updated settings/design summaries, links - Publish: chain, slug, transaction hash if sent, final availability state, settings summary, links Use these business labels in user-facing output: - `draft`, `live`, `paused` for availability - `limited edition`, `open edition` for edition - `prereveal image` for prereveal media - `preview images` for design preview media - `sale currency` for payment token ## Failure Reporting Do not say only "failed". State the failed lifecycle stage, the relevant error or response summary, and the actionable recovery. If required parameters are missing, state exactly which user-facing parameters are missing. For upload, API, or onchain failures, identify the segment in business language. Always repeat the selected chain in the failure message. For create/deploy failures, repeat the symbol too. ## Absolute Do-Nots - Do not request or echo the private key. - Do not execute state-changing commands before confirmation. - Do not interpret "create" as "create and publish" by default. - Do not overwrite unspecified fields with defaults during `update-drop`. - Do not show internal script choreography or backend-only identifiers in normal user-facing output.

path: references/publish.md
content: # Publish Stage Reference

## When to Use

- The drop has already been created and configured, and is now ready to publish
- The drop is currently paused and should be switched back to live state
- The drop is already live and should be switched into paused state

## Minimum Required Parameters

- `chainName`
  - A user-facing chain name
  - Examples: `base`, `arbitrum`, `bsc`
  - The script resolves it internally to the corresponding chain configuration
- `slug`
  - The user-facing Element drop or collection slug
  - The script resolves it internally to the identifiers required by the backend

## Preconditions

- The drop has already been created
- Settings and design have already been configured
- A valid prereveal asset has already been configured on the drop
- Publish uses the configured prereveal image and resolves the publish-ready prereveal URI automatically
- If prereveal readiness times out, ask the user to re-upload the prereveal image and retry
- If the user needs to change the sale currency first, use `update-drop` with `paymentToken` before publishing
- `publish-drop` can be used for first publish, for resuming a paused drop, and for pausing an already live drop

## Optional Behavior

- By default, publish makes the drop live
- If the user explicitly asks to keep the drop paused, the agent uses the internal paused publish mode
- A paused drop can be resumed by running `publish-drop` without paused mode
- An already live drop can be paused by running `publish-drop` in paused mode

## Execution Preview

First enter the installed skill's `scripts` directory:

```bash
cd <skill-directory>/scripts
npx tsx src/cli.ts publish-drop --preview /tmp/element-drop-publish.json
```

The preview should be treated as a transaction preview for the publish step:

- It should confirm a prereveal image is configured and can become publish-ready
- It should warn that confirmed execution sends a real onchain publish transaction
- It should show the transaction summary when available
- It should make clear that preview mode does not broadcast the transaction

## Execute

```bash
cd <skill-directory>/scripts
npx tsx src/cli.ts publish-drop /tmp/element-drop-publish.json
```

## Example Payload

```json
{
  "chainName": "base",
  "slug": "ffffff-510bce"
}
```

## Output Focus

- `slug`
- resolved `contractAddress`
- publish transaction hash
- whether platform synchronization completed
- final availability state
- final `dropBeginTime`
- final `edition`
- final `maxSupply`
- final resolved `paymentToken`
- final full stage summary
- final `Drop URL`
- final `Collection URL`
- final `Edit URL`


path: references/list.md
content: # List Stage Reference

## When to Use

- Show the user's current Element Drop list on a supported chain
- Look up existing drop `name` and `slug` values before preview, update, or publish
- Confirm which collections on a chain are actual Element Drops

## Minimum Required Parameters

- `chainName`
  - A user-facing chain name
  - Examples: `base`, `arbitrum`, `bsc`
  - The script resolves it internally to the corresponding chain configuration

## Common Optional Parameters

- `walletAddress`
  - Optional override
  - If omitted, the script derives the address from `ELEMENT_WALLET_PRIVATE_KEY`
- `first`
  - Forward pagination size
- `after`
  - Forward pagination cursor
- `last`
  - Backward pagination size
- `before`
  - Backward pagination cursor

## Run

First enter the installed skill's `scripts` directory:

```bash
cd <skill-directory>/scripts
npx tsx src/cli.ts list-user-drops /tmp/element-drop-list.json
```

## Example Payload

```json
{
  "chainName": "base"
}
```

## Output Focus

- `drops[].name`
- `drops[].slug`
- `drops[].isVerified`
- `pageInfo`


path: references/create.md
content: # Create Stage Reference

## When to Use

- Create a new Element Drop
- Deploy the contract and complete the initial media, settings, and design setup
- Explicitly stop before publish

## Minimum Required Parameters

When the user starts a create flow, proactively collect these fields first. If any are missing, ask for the missing required fields and state the defaults below before preparing the preview.

- `chainName`
  - A user-facing chain name
  - Example values: `base`, `bsc`, `arbitrum`
  - Do not guess whether a chain is supported
  - If the user is unsure, run `list-chains` before continuing
  - The script resolves it internally to the corresponding chain configuration
- `name`
  - Collection name shown as the contract name onchain
  - Usually the project or collection name users will recognize
  - This value cannot be changed after the contract is deployed
- `symbol`
  - Short onchain identifier for the contract
  - Usually an uppercase shorthand, such as `AZUKI` or `BAYC`
  - This value cannot be changed after the contract is deployed
- `preReveal`
  - Pre-reveal image path
  - Must be a local absolute path
  - The workflow uploads this prereveal asset during create
  - The workflow also uses this image as the default initial design preview media unless explicit design preview assets are provided

If the user is unsure which chains are currently supported, run:

```bash
cd <skill-directory>/scripts
npx tsx src/cli.ts list-chains
```

## Create Defaults

If the user does not override these fields, create uses:

- Mint start time: next day
- Edition: limited edition
- Total supply: `999`
- Sale currency: native chain currency
- Stage set: one public stage
- Default stage name: `Public`
- Default stage sale price: `0`
- Default stage duration: `3 days`
- Default phase supply: same as the collection supply
- Default max mints per wallet: `1`
- Default mint interval: `0`
- Default stage mode: public
- Default banner: prereveal image
- Default preview image: same image as `preReveal`, unless `previewMedia` is provided

Do not describe these as facts if you have not read this reference in the current operation.

## Common Optional Parameters

- `previewMedia`
  - Optional preview image path or path array
  - Must use local absolute path(s)
  - If omitted, defaults to the same image as `preReveal`
  - The workflow uploads these files and stores them as the preview images
- `bannerFilePath`
  - Optional design banner image path
  - If omitted, the prereveal image is used as the initial banner
- `description`
  - Drop description
- `desc`
  - Alias for `description`; accepted for user convenience and normalized before sending to the backend
- `website`
  - Collection website URL; must be a complete URL
- `twitter`
  - Twitter/X suffix or full link; normalized to suffix before backend write
- `instagram`
  - Instagram suffix or full link; normalized to suffix before backend write
- `discord`
  - Discord invite suffix or full link; normalized to suffix before backend write
- `telegram`
  - Telegram suffix or full link; normalized to suffix before backend write
- `medium`
  - Medium suffix or full link; normalized to suffix before backend write
- `dropFeaturedImage`
  - Optional featured image URL for the drop design payload
- `paymentToken`
  - Sale currency
  - Defaults to native chain currency
  - Accepts token `name`, `symbol`, or token address
  - Do not guess the supported token list; use `list-chains` when the user asks what is supported or provides an ambiguous token
- `edition`
  - `limited` = limited edition
  - `open` = open edition
  - Defaults to `limited`
  - Open edition uses the platform default supply behavior automatically
- `maxSupply`
  - Total supply cap for the collection
  - Defaults to `999` for limited edition
- `dropBeginTime`
  - Mint start time
  - Unix timestamp in seconds
  - If omitted, defaults to the next day
- `stages`
  - Stage array
  - If provided, it defines the full mint stage set
  - If omitted, the default public stage above is used

## Stage Rules

Each stage may include:

- `stageName`
- `price`
- `duration`
- `maxSupplyAtThisStage`
- `maxMintedPerWallet`
- `interval`
- `stageMode`

Notes:

- `stageID` is never user-controlled
- If the user does not provide stages, do not ask for stage fields unless they want custom mint phases
- The script always assigns stage IDs in order:
  - first stage `256`
  - second stage `257`
  - third stage `258`
  - and so on
- Element stages also support whitelist mode on web
- This skill does not support configuring whitelist mode
- Design detail sections are web-only
- If any web-only feature is needed, edit it in the web UI instead, replacing `your-slug` with the actual drop slug:
  - [https://element.market/collections/your-slug/edit/drop](https://element.market/collections/your-slug/edit/drop)

## Execution Preview

First enter the installed skill's `scripts` directory:

```bash
cd <skill-directory>/scripts
npx tsx src/cli.ts create-drop --preview /tmp/element-drop-create.json
```

The preview should be treated as a transaction preview for the deployment step:

- It should repeat the selected chain and symbol
- It should warn that confirmed execution deploys a real onchain contract
- It should show the transaction summary when available
- It should make clear that preview mode does not broadcast anything

## Execute

```bash
cd <skill-directory>/scripts
npx tsx src/cli.ts create-drop /tmp/element-drop-create.json
```

## Output Focus

- `slug`
- selected chain
- `symbol`
- resolved `contractAddress`
- `Drop URL`
- `Collection URL`
- `Edit URL`
- settings summary
- design summary
- next step: preview publishing this same chain and slug, then publish only after confirmation

## Example Payload

```json
{
  "chainName": "base",
  "name": "ffffff",
  "symbol": "F",
  "paymentToken": "ETH",
  "preReveal": "/absolute/path/to/image.png"
}
```

## Recovery Note

If contract deployment succeeds but later setup fails or times out, do not immediately rerun `create-drop`: that may deploy another contract. Preview the new slug, then use `update-drop` to complete missing settings, prereveal, design, or stage configuration.
If deployment or setup fails, repeat the selected chain and symbol in the error summary. Do not switch to another chain unless the user explicitly asks for that chain.


path: references/preview.md
content: # Preview Stage Reference

## When to Use

- Inspect the current drop settings
- Inspect the current design
- Inspect current prereveal media status

## Minimum Required Parameters

- `chainName`
  - A user-facing chain name
  - Examples: `base`, `arbitrum`, `bsc`
  - The script resolves it internally to the corresponding chain configuration
- `slug`
  - The user-facing Element drop or collection slug
  - The script resolves it internally to the matching `contractAddress`

## Run

First enter the installed skill's `scripts` directory:

```bash
cd <skill-directory>/scripts
npx tsx src/cli.ts preview-drop /tmp/element-drop-preview.json
```

## Example Payload

```json
{
  "chainName": "base",
  "slug": "ffffff-510bce"
}
```

## Output Focus

- `slug`
- resolved `contractAddress`
- availability state: draft, live, or paused
- `dropBeginTime`
- `edition`
- `maxSupply`
- resolved `paymentToken`
- full stage summary for each stage
- whether design exists
- design preview images
- prereveal image status


path: references/update.md
content: # Update Stage Reference

## When to Use

- Update settings on an existing drop
- Update design copy or media
- Adjust mint stages

## Minimum Required Parameters

- `chainName`
  - A user-facing chain name
  - Examples: `base`, `arbitrum`, `bsc`
  - The script resolves it internally to the corresponding chain configuration
- `slug`
  - The user-facing Element drop or collection slug
  - The script resolves it internally to `contractAddress`

## Patch Semantics

- The script fetches current remote `settings` and `design` first
- It only overwrites fields the user explicitly provides
- Any omitted field keeps its current remote value
- If the current drop is live and the patch changes `settings` or `preReveal`, `update-drop` does not stop at offchain modification
- In that case it forcibly continues into the publish flow after the modification step
- If the current drop is already paused, the same patch stays offchain and does not force republish
- If the patch only changes `design`, design assets, or collection profile fields, no republish is triggered
- The settings- or prereveal-changing case becomes an onchain-affecting flow and must be confirmed as such

## Common Patchable Fields

### Settings

- `preReveal`
  - Pre-reveal media absolute path
- `edition`
  - `limited` = limited edition
  - `open` = open edition
  - Switching to open edition uses the platform default supply behavior automatically
  - Switching to limited edition requires an explicit `maxSupply`
- `maxSupply`
  - Supply cap for limited edition
- `dropBeginTime`
- `paymentToken`
  - Accepts token `name`, `symbol`, or token address
- `stages`

### Design

- `previewMedia`
  - Design preview image path array
  - The workflow uploads each file, then stores the resulting URLs as design preview images
- `name`
- `description`
- `website`
  - Must be a complete URL
- `twitter`
  - Accepts either suffix or full link; backend payload uses suffix only
- `instagram`
  - Accepts either suffix or full link; backend payload uses suffix only
- `discord`
  - Accepts either suffix or full link; backend payload uses suffix only
- `telegram`
  - Accepts either suffix or full link; backend payload uses suffix only
- `medium`
  - Accepts either suffix or full link; backend payload uses suffix only
- `dropFeaturedImage`

### Design Assets

- `bannerFilePath`
  - Banner image absolute path

Notes:

- `previewMedia` is the user-facing input field for design preview images
- If `preReveal` is uploaded and `previewMedia` is not explicitly provided, the workflow also uses the uploaded prereveal image as the initial design preview image
- Element stages also support whitelist mode on web
- This skill does not support configuring whitelist mode
- Design detail sections are web-only
- If any web-only feature is needed, edit it in the web UI instead, replacing `your-slug` with the actual drop slug:
  - [https://element.market/collections/your-slug/edit/drop](https://element.market/collections/your-slug/edit/drop)

## Stage Rules

- If `stages` is omitted, the current remote stage config is preserved
- If `stages` is provided, it replaces the full current stage array
- `stageID` is never user-controlled
- The script always rewrites stage IDs to `256 + index`

## Execution Preview

First enter the installed skill's `scripts` directory:

```bash
cd <skill-directory>/scripts
npx tsx src/cli.ts update-drop --preview /tmp/element-drop-update.json
```

Current preview behavior:

- show the current values that matter for the requested patch
- show the merged result that would be applied
- show whether the update remains offchain or requires republish
- if republish is required, include the onchain transaction preview

## Execute

```bash
cd <skill-directory>/scripts
npx tsx src/cli.ts update-drop /tmp/element-drop-update.json
```

## Output Focus

- `slug`
- resolved `contractAddress`
- fields actually updated
- settings summary, if settings were updated
- design summary, if design was updated
- whether republish was triggered
- `Drop URL`
- `Collection URL`
- `Edit URL`

## Example Payload

```json
{
  "chainName": "base",
  "slug": "ffffff-510bce",
  "maxSupply": 999,
  "paymentToken": "WETH",
  "description": "updated description"
}
```

## Example Payload With Media Updates

```json
{
  "chainName": "base",
  "slug": "ffff",
  "paymentToken": "0x4200000000000000000000000000000000000006",
  "preReveal": "/absolute/path/to/image.png",
  "bannerFilePath": "/absolute/path/to/banner.png",
  "previewMedia": [
    "/absolute/path/to/image.png"
  ],
  "description": "updated description"
}
```

## Payment Token Guidance

- Use `list-chains` if the user needs the currently supported payment token list for a chain
- Prefer token address when there is any ambiguity between display names or symbols
- If `paymentToken` is omitted during `update-drop`, the current remote payment token setting is preserved


path: scripts/pnpm-lock.yaml
content: lockfileVersion: '9.0'

settings:
  autoInstallPeers: true
  excludeLinksFromLockfile: false

importers:

  .:
    dependencies:
      cuimp:
        specifier: ^1.10.0
        version: 1.10.0
      ethers:
        specifier: ^6.13.5
        version: 6.16.0
      zod:
        specifier: ^3.24.1
        version: 3.25.76
    devDependencies:
      '@types/node':
        specifier: ^24.9.1
        version: 24.12.2
      tsx:
        specifier: ^4.20.6
        version: 4.21.0
      typescript:
        specifier: ^5.9.3
        version: 5.9.3
      vitest:
        specifier: ^3.2.4
        version: 3.2.4(@types/node@24.12.2)(tsx@4.21.0)

packages:

  '@adraffy/ens-normalize@1.10.1':
    resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==}

  '@esbuild/aix-ppc64@0.27.7':
    resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
    engines: {node: '>=18'}
    cpu: [ppc64]
    os: [aix]

  '@esbuild/android-arm64@0.27.7':
    resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [android]

  '@esbuild/android-arm@0.27.7':
    resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
    engines: {node: '>=18'}
    cpu: [arm]
    os: [android]

  '@esbuild/android-x64@0.27.7':
    resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [android]

  '@esbuild/darwin-arm64@0.27.7':
    resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [darwin]

  '@esbuild/darwin-x64@0.27.7':
    resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [darwin]

  '@esbuild/freebsd-arm64@0.27.7':
    resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [freebsd]

  '@esbuild/freebsd-x64@0.27.7':
    resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [freebsd]

  '@esbuild/linux-arm64@0.27.7':
    resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [linux]

  '@esbuild/linux-arm@0.27.7':
    resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
    engines: {node: '>=18'}
    cpu: [arm]
    os: [linux]

  '@esbuild/linux-ia32@0.27.7':
    resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
    engines: {node: '>=18'}
    cpu: [ia32]
    os: [linux]

  '@esbuild/linux-loong64@0.27.7':
    resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
    engines: {node: '>=18'}
    cpu: [loong64]
    os: [linux]

  '@esbuild/linux-mips64el@0.27.7':
    resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
    engines: {node: '>=18'}
    cpu: [mips64el]
    os: [linux]

  '@esbuild/linux-ppc64@0.27.7':
    resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
    engines: {node: '>=18'}
    cpu: [ppc64]
    os: [linux]

  '@esbuild/linux-riscv64@0.27.7':
    resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
    engines: {node: '>=18'}
    cpu: [riscv64]
    os: [linux]

  '@esbuild/linux-s390x@0.27.7':
    resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
    engines: {node: '>=18'}
    cpu: [s390x]
    os: [linux]

  '@esbuild/linux-x64@0.27.7':
    resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [linux]

  '@esbuild/netbsd-arm64@0.27.7':
    resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [netbsd]

  '@esbuild/netbsd-x64@0.27.7':
    resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [netbsd]

  '@esbuild/openbsd-arm64@0.27.7':
    resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [openbsd]

  '@esbuild/openbsd-x64@0.27.7':
    resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [openbsd]

  '@esbuild/openharmony-arm64@0.27.7':
    resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [openharmony]

  '@esbuild/sunos-x64@0.27.7':
    resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [sunos]

  '@esbuild/win32-arm64@0.27.7':
    resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
    engines: {node: '>=18'}
    cpu: [arm64]
    os: [win32]

  '@esbuild/win32-ia32@0.27.7':
    resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
    engines: {node: '>=18'}
    cpu: [ia32]
    os: [win32]

  '@esbuild/win32-x64@0.27.7':
    resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
    engines: {node: '>=18'}
    cpu: [x64]
    os: [win32]

  '@isaacs/fs-minipass@4.0.1':
    resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
    engines: {node: '>=18.0.0'}

  '@jridgewell/sourcemap-codec@1.5.5':
    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}

  '@noble/curves@1.2.0':
    resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==}

  '@noble/hashes@1.3.2':
    resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==}
    engines: {node: '>= 16'}

  '@rollup/rollup-android-arm-eabi@4.60.1':
    resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
    cpu: [arm]
    os: [android]

  '@rollup/rollup-android-arm64@4.60.1':
    resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==}
    cpu: [arm64]
    os: [android]

  '@rollup/rollup-darwin-arm64@4.60.1':
    resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==}
    cpu: [arm64]
    os: [darwin]

  '@rollup/rollup-darwin-x64@4.60.1':
    resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==}
    cpu: [x64]
    os: [darwin]

  '@rollup/rollup-freebsd-arm64@4.60.1':
    resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==}
    cpu: [arm64]
    os: [freebsd]

  '@rollup/rollup-freebsd-x64@4.60.1':
    resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==}
    cpu: [x64]
    os: [freebsd]

  '@rollup/rollup-linux-arm-gnueabihf@4.60.1':
    resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
    cpu: [arm]
    os: [linux]
    libc: [glibc]

  '@rollup/rollup-linux-arm-musleabihf@4.60.1':
    resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
    cpu: [arm]
    os: [linux]
    libc: [musl]

  '@rollup/rollup-linux-arm64-gnu@4.60.1':
    resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
    cpu: [arm64]
    os: [linux]
    libc: [glibc]

  '@rollup/rollup-linux-arm64-musl@4.60.1':
    resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
    cpu: [arm64]
    os: [linux]
    libc: [musl]

  '@rollup/rollup-linux-loong64-gnu@4.60.1':
    resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
    cpu: [loong64]
    os: [linux]
    libc: [glibc]

  '@rollup/rollup-linux-loong64-musl@4.60.1':
    resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
    cpu: [loong64]
    os: [linux]
    libc: [musl]

  '@rollup/rollup-linux-ppc64-gnu@4.60.1':
    resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
    cpu: [ppc64]
    os: [linux]
    libc: [glibc]

  '@rollup/rollup-linux-ppc64-musl@4.60.1':
    resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
    cpu: [ppc64]
    os: [linux]
    libc: [musl]

  '@rollup/rollup-linux-riscv64-gnu@4.60.1':
    resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
    cpu: [riscv64]
    os: [linux]
    libc: [glibc]

  '@rollup/rollup-linux-riscv64-musl@4.60.1':
    resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
    cpu: [riscv64]
    os: [linux]
    libc: [musl]

  '@rollup/rollup-linux-s390x-gnu@4.60.1':
    resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
    cpu: [s390x]
    os: [linux]
    libc: [glibc]

  '@rollup/rollup-linux-x64-gnu@4.60.1':
    resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
    cpu: [x64]
    os: [linux]
    libc: [glibc]

  '@rollup/rollup-linux-x64-musl@4.60.1':
    resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
    cpu: [x64]
    os: [linux]
    libc: [musl]

  '@rollup/rollup-openbsd-x64@4.60.1':
    resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
    cpu: [x64]
    os: [openbsd]

  '@rollup/rollup-openharmony-arm64@4.60.1':
    resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==}
    cpu: [arm64]
    os: [openharmony]

  '@rollup/rollup-win32-arm64-msvc@4.60.1':
    resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==}
    cpu: [arm64]
    os: [win32]

  '@rollup/rollup-win32-ia32-msvc@4.60.1':
    resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==}
    cpu: [ia32]
    os: [win32]

  '@rollup/rollup-win32-x64-gnu@4.60.1':
    resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==}
    cpu: [x64]
    os: [win32]

  '@rollup/rollup-win32-x64-msvc@4.60.1':
    resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==}
    cpu: [x64]
    os: [win32]

  '@types/chai@5.2.3':
    resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}

  '@types/deep-eql@4.0.2':
    resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}

  '@types/estree@1.0.8':
    resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}

  '@types/node@22.7.5':
    resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==}

  '@types/node@24.12.2':
    resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}

  '@vitest/expect@3.2.4':
    resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}

  '@vitest/mocker@3.2.4':
    resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
    peerDependencies:
      msw: ^2.4.9
      vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
    peerDependenciesMeta:
      msw:
        optional: true
      vite:
        optional: true

  '@vitest/pretty-format@3.2.4':
    resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}

  '@vitest/runner@3.2.4':
    resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}

  '@vitest/snapshot@3.2.4':
    resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}

  '@vitest/spy@3.2.4':
    resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}

  '@vitest/utils@3.2.4':
    resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}

  aes-js@4.0.0-beta.5:
    resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==}

  assertion-error@2.0.1:
    resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
    engines: {node: '>=12'}

  cac@6.7.14:
    resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
    engines: {node: '>=8'}

  chai@5.3.3:
    resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
    engines: {node: '>=18'}

  check-error@2.1.3:
    resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
    engines: {node: '>= 16'}

  chownr@3.0.0:
    resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
    engines: {node: '>=18'}

  cuimp@1.10.0:
    resolution: {integrity: sha512-+cIbWHQ+ZQ7Nc3TJoCxqfDl06/ormSN5zV6mV0qcJVzTM2lHLTs/8ujgSJpsbzg6DNKNeHSX6g/HoTDWgCKF3w==}
    engines: {node: '>=18.17'}

  debug@4.4.3:
    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
    engines: {node: '>=6.0'}
    peerDependencies:
      supports-color: '*'
    peerDependenciesMeta:
      supports-color:
        optional: true

  deep-eql@5.0.2:
    resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
    engines: {node: '>=6'}

  es-module-lexer@1.7.0:
    resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}

  esbuild@0.27.7:
    resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
    engines: {node: '>=18'}
    hasBin: true

  estree-walker@3.0.3:
    resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}

  ethers@6.16.0:
    resolution: {integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==}
    engines: {node: '>=14.0.0'}

  expect-type@1.3.0:
    resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
    engines: {node: '>=12.0.0'}

  fdir@6.5.0:
    resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
    engines: {node: '>=12.0.0'}
    peerDependencies:
      picomatch: ^3 || ^4
    peerDependenciesMeta:
      picomatch:
        optional: true

  fsevents@2.3.3:
    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
    os: [darwin]

  get-tsconfig@4.14.0:
    resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}

  js-tokens@9.0.1:
    resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}

  loupe@3.2.1:
    resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}

  magic-string@0.30.21:
    resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}

  minipass@7.1.3:
    resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
    engines: {node: '>=16 || 14 >=14.17'}

  minizlib@3.1.0:
    resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
    engines: {node: '>= 18'}

  mkdirp@3.0.1:
    resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
    engines: {node: '>=10'}
    hasBin: true

  ms@2.1.3:
    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}

  nanoid@3.3.11:
    resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
    hasBin: true

  pathe@2.0.3:
    resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}

  pathval@2.0.1:
    resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
    engines: {node: '>= 14.16'}

  picocolors@1.1.1:
    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}

  picomatch@4.0.4:
    resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
    engines: {node: '>=12'}

  postcss@8.5.10:
    resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
    engines: {node: ^10 || ^12 || >=14}

  resolve-pkg-maps@1.0.0:
    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}

  rollup@4.60.1:
    resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
    hasBin: true

  siginfo@2.0.0:
    resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}

  source-map-js@1.2.1:
    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
    engines: {node: '>=0.10.0'}

  stackback@0.0.2:
    resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}

  std-env@3.10.0:
    resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}

  strip-literal@3.1.0:
    resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}

  tar@7.4.3:
    resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
    engines: {node: '>=18'}
    deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [REDACTED_SECRET]

  tinybench@2.9.0:
    resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}

  tinyexec@0.3.2:
    resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}

  tinyglobby@0.2.16:
    resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
    engines: {node: '>=12.0.0'}

  tinypool@1.1.1:
    resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
    engines: {node: ^18.0.0 || >=20.0.0}

  tinyrainbow@2.0.0:
    resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
    engines: {node: '>=14.0.0'}

  tinyspy@4.0.4:
    resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
    engines: {node: '>=14.0.0'}

  tslib@2.7.0:
    resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}

  tsx@4.21.0:
    resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
    engines: {node: '>=18.0.0'}
    hasBin: true

  typescript@5.9.3:
    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
    engines: {node: '>=14.17'}
    hasBin: true

  undici-types@6.19.8:
    resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}

  undici-types@7.16.0:
    resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}

  vite-node@3.2.4:
    resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
    hasBin: true

  vite@7.3.2:
    resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==}
    engines: {node: ^20.19.0 || >=22.12.0}
    hasBin: true
    peerDependencies:
      '@types/node': ^20.19.0 || >=22.12.0
      jiti: '>=1.21.0'
      less: ^4.0.0
      lightningcss: ^1.21.0
      sass: ^1.70.0
      sass-embedded: ^1.70.0
      stylus: '>=0.54.8'
      sugarss: ^5.0.0
      terser: ^5.16.0
      tsx: ^4.8.1
      yaml: ^2.4.2
    peerDependenciesMeta:
      '@types/node':
        optional: true
      jiti:
        optional: true
      less:
        optional: true
      lightningcss:
        optional: true
      sass:
        optional: true
      sass-embedded:
        optional: true
      stylus:
        optional: true
      sugarss:
        optional: true
      terser:
        optional: true
      tsx:
        optional: true
      yaml:
        optional: true

  vitest@3.2.4:
    resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
    hasBin: true
    peerDependencies:
      '@edge-runtime/vm': '*'
      '@types/debug': ^4.1.12
      '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
      '@vitest/browser': 3.2.4
      '@vitest/ui': 3.2.4
      happy-dom: '*'
      jsdom: '*'
    peerDependenciesMeta:
      '@edge-runtime/vm':
        optional: true
      '@types/debug':
        optional: true
      '@types/node':
        optional: true
      '@vitest/browser':
        optional: true
      '@vitest/ui':
        optional: true
      happy-dom:
        optional: true
      jsdom:
        optional: true

  why-is-node-running@2.3.0:
    resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
    engines: {node: '>=8'}
    hasBin: true

  ws@8.17.1:
    resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
    engines: {node: '>=10.0.0'}
    peerDependencies:
      bufferutil: ^4.0.1
      utf-8-validate: '>=5.0.2'
    peerDependenciesMeta:
      bufferutil:
        optional: true
      utf-8-validate:
        optional: true

  yallist@5.0.0:
    resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
    engines: {node: '>=18'}

  zod@3.25.76:
    resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}

snapshots:

  '@adraffy/ens-normalize@1.10.1': {}

  '@esbuild/aix-ppc64@0.27.7':
    optional: true

  '@esbuild/android-arm64@0.27.7':
    optional: true

  '@esbuild/android-arm@0.27.7':
    optional: true

  '@esbuild/android-x64@0.27.7':
    optional: true

  '@esbuild/darwin-arm64@0.27.7':
    optional: true

  '@esbuild/darwin-x64@0.27.7':
    optional: true

  '@esbuild/freebsd-arm64@0.27.7':
    optional: true

  '@esbuild/freebsd-x64@0.27.7':
    optional: true

  '@esbuild/linux-arm64@0.27.7':
    optional: true

  '@esbuild/linux-arm@0.27.7':
    optional: true

  '@esbuild/linux-ia32@0.27.7':
    optional: true

  '@esbuild/linux-loong64@0.27.7':
    optional: true

  '@esbuild/linux-mips64el@0.27.7':
    optional: true

  '@esbuild/linux-ppc64@0.27.7':
    optional: true

  '@esbuild/linux-riscv64@0.27.7':
    optional: true

  '@esbuild/linux-s390x@0.27.7':
    optional: true

  '@esbuild/linux-x64@0.27.7':
    optional: true

  '@esbuild/netbsd-arm64@0.27.7':
    optional: true

  '@esbuild/netbsd-x64@0.27.7':
    optional: true

  '@esbuild/openbsd-arm64@0.27.7':
    optional: true

  '@esbuild/openbsd-x64@0.27.7':
    optional: true

  '@esbuild/openharmony-arm64@0.27.7':
    optional: true

  '@esbuild/sunos-x64@0.27.7':
    optional: true

  '@esbuild/win32-arm64@0.27.7':
    optional: true

  '@esbuild/win32-ia32@0.27.7':
    optional: true

  '@esbuild/win32-x64@0.27.7':
    optional: true

  '@isaacs/fs-minipass@4.0.1':
    dependencies:
      minipass: 7.1.3

  '@jridgewell/sourcemap-codec@1.5.5': {}

  '@noble/curves@1.2.0':
    dependencies:
      '@noble/hashes': 1.3.2

  '@noble/hashes@1.3.2': {}

  '@rollup/rollup-android-arm-eabi@4.60.1':
    optional: true

  '@rollup/rollup-android-arm64@4.60.1':
    optional: true

  '@rollup/rollup-darwin-arm64@4.60.1':
    optional: true

  '@rollup/rollup-darwin-x64@4.60.1':
    optional: true

  '@rollup/rollup-freebsd-arm64@4.60.1':
    optional: true

  '@rollup/rollup-freebsd-x64@4.60.1':
    optional: true

  '@rollup/rollup-linux-arm-gnueabihf@4.60.1':
    optional: true

  '@rollup/rollup-linux-arm-musleabihf@4.60.1':
    optional: true

  '@rollup/rollup-linux-arm64-gnu@4.60.1':
    optional: true

  '@rollup/rollup-linux-arm64-musl@4.60.1':
    optional: true

  '@rollup/rollup-linux-loong64-gnu@4.60.1':
    optional: true

  '@rollup/rollup-linux-loong64-musl@4.60.1':
    optional: true

  '@rollup/rollup-linux-ppc64-gnu@4.60.1':
    optional: true

  '@rollup/rollup-linux-ppc64-musl@4.60.1':
    optional: true

  '@rollup/rollup-linux-riscv64-gnu@4.60.1':
    optional: true

  '@rollup/rollup-linux-riscv64-musl@4.60.1':
    optional: true

  '@rollup/rollup-linux-s390x-gnu@4.60.1':
    optional: true

  '@rollup/rollup-linux-x64-gnu@4.60.1':
    optional: true

  '@rollup/rollup-linux-x64-musl@4.60.1':
    optional: true

  '@rollup/rollup-openbsd-x64@4.60.1':
    optional: true

  '@rollup/rollup-openharmony-arm64@4.60.1':
    optional: true

  '@rollup/rollup-win32-arm64-msvc@4.60.1':
    optional: true

  '@rollup/rollup-win32-ia32-msvc@4.60.1':
    optional: true

  '@rollup/rollup-win32-x64-gnu@4.60.1':
    optional: true

  '@rollup/rollup-win32-x64-msvc@4.60.1':
    optional: true

  '@types/chai@5.2.3':
    dependencies:
      '@types/deep-eql': 4.0.2
      assertion-error: 2.0.1

  '@types/deep-eql@4.0.2': {}

  '@types/estree@1.0.8': {}

  '@types/node@22.7.5':
    dependencies:
      undici-types: 6.19.8

  '@types/node@24.12.2':
    dependencies:
      undici-types: 7.16.0

  '@vitest/expect@3.2.4':
    dependencies:
      '@types/chai': 5.2.3
      '@vitest/spy': 3.2.4
      '@vitest/utils': 3.2.4
      chai: 5.3.3
      tinyrainbow: 2.0.0

  '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@24.12.2)(tsx@4.21.0))':
    dependencies:
      '@vitest/spy': 3.2.4
      estree-walker: 3.0.3
      magic-string: 0.30.21
    optionalDependencies:
      vite: 7.3.2(@types/node@24.12.2)(tsx@4.21.0)

  '@vitest/pretty-format@3.2.4':
    dependencies:
      tinyrainbow: 2.0.0

  '@vitest/runner@3.2.4':
    dependencies:
      '@vitest/utils': 3.2.4
      pathe: 2.0.3
      strip-literal: 3.1.0

  '@vitest/snapshot@3.2.4':
    dependencies:
      '@vitest/pretty-format': 3.2.4
      magic-string: 0.30.21
      pathe: 2.0.3

  '@vitest/spy@3.2.4':
    dependencies:
      tinyspy: 4.0.4

  '@vitest/utils@3.2.4':
    dependencies:
      '@vitest/pretty-format': 3.2.4
      loupe: 3.2.1
      tinyrainbow: 2.0.0

  aes-js@4.0.0-beta.5: {}

  assertion-error@2.0.1: {}

  cac@6.7.14: {}

  chai@5.3.3:
    dependencies:
      assertion-error: 2.0.1
      check-error: 2.1.3
      deep-eql: 5.0.2
      loupe: 3.2.1
      pathval: 2.0.1

  check-error@2.1.3: {}

  chownr@3.0.0: {}

  cuimp@1.10.0:
    dependencies:
      tar: 7.4.3

  debug@4.4.3:
    dependencies:
      ms: 2.1.3

  deep-eql@5.0.2: {}

  es-module-lexer@1.7.0: {}

  esbuild@0.27.7:
    optionalDependencies:
      '@esbuild/aix-ppc64': 0.27.7
      '@esbuild/android-arm': 0.27.7
      '@esbuild/android-arm64': 0.27.7
      '@esbuild/android-x64': 0.27.7
      '@esbuild/darwin-arm64': 0.27.7
      '@esbuild/darwin-x64': 0.27.7
      '@esbuild/freebsd-arm64': 0.27.7
      '@esbuild/freebsd-x64': 0.27.7
      '@esbuild/linux-arm': 0.27.7
      '@esbuild/linux-arm64': 0.27.7
      '@esbuild/linux-ia32': 0.27.7
      '@esbuild/linux-loong64': 0.27.7
      '@esbuild/linux-mips64el': 0.27.7
      '@esbuild/linux-ppc64': 0.27.7
      '@esbuild/linux-riscv64': 0.27.7
      '@esbuild/linux-s390x': 0.27.7
      '@esbuild/linux-x64': 0.27.7
      '@esbuild/netbsd-arm64': 0.27.7
      '@esbuild/netbsd-x64': 0.27.7
      '@esbuild/openbsd-arm64': 0.27.7
      '@esbuild/openbsd-x64': 0.27.7
      '@esbuild/openharmony-arm64': 0.27.7
      '@esbuild/sunos-x64': 0.27.7
      '@esbuild/win32-arm64': 0.27.7
      '@esbuild/win32-ia32': 0.27.7
      '@esbuild/win32-x64': 0.27.7

  estree-walker@3.0.3:
    dependencies:
      '@types/estree': 1.0.8

  ethers@6.16.0:
    dependencies:
      '@adraffy/ens-normalize': 1.10.1
      '@noble/curves': 1.2.0
      '@noble/hashes': 1.3.2
      '@types/node': 22.7.5
      aes-js: 4.0.0-beta.5
      tslib: 2.7.0
      ws: 8.17.1
    transitivePeerDependencies:
      - bufferutil
      - utf-8-validate

  expect-type@1.3.0: {}

  fdir@6.5.0(picomatch@4.0.4):
    optionalDependencies:
      picomatch: 4.0.4

  fsevents@2.3.3:
    optional: true

  get-tsconfig@4.14.0:
    dependencies:
      resolve-pkg-maps: 1.0.0

  js-tokens@9.0.1: {}

  loupe@3.2.1: {}

  magic-string@0.30.21:
    dependencies:
      '@jridgewell/sourcemap-codec': 1.5.5

  minipass@7.1.3: {}

  minizlib@3.1.0:
    dependencies:
      minipass: 7.1.3

  mkdirp@3.0.1: {}

  ms@2.1.3: {}

  nanoid@3.3.11: {}

  pathe@2.0.3: {}

  pathval@2.0.1: {}

  picocolors@1.1.1: {}

  picomatch@4.0.4: {}

  postcss@8.5.10:
    dependencies:
      nanoid: 3.3.11
      picocolors: 1.1.1
      source-map-js: 1.2.1

  resolve-pkg-maps@1.0.0: {}

  rollup@4.60.1:
    dependencies:
      '@types/estree': 1.0.8
    optionalDependencies:
      '@rollup/rollup-android-arm-eabi': 4.60.1
      '@rollup/rollup-android-arm64': 4.60.1
      '@rollup/rollup-darwin-arm64': 4.60.1
      '@rollup/rollup-darwin-x64': 4.60.1
      '@rollup/rollup-freebsd-arm64': 4.60.1
      '@rollup/rollup-freebsd-x64': 4.60.1
      '@rollup/rollup-linux-arm-gnueabihf': 4.60.1
      '@rollup/rollup-linux-arm-musleabihf': 4.60.1
      '@rollup/rollup-linux-arm64-gnu': 4.60.1
      '@rollup/rollup-linux-arm64-musl': 4.60.1
      '@rollup/rollup-linux-loong64-gnu': 4.60.1
      '@rollup/rollup-linux-loong64-musl': 4.60.1
      '@rollup/rollup-linux-ppc64-gnu': 4.60.1
      '@rollup/rollup-linux-ppc64-musl': 4.60.1
      '@rollup/rollup-linux-riscv64-gnu': 4.60.1
      '@rollup/rollup-linux-riscv64-musl': 4.60.1
      '@rollup/rollup-linux-s390x-gnu': 4.60.1
      '@rollup/rollup-linux-x64-gnu': 4.60.1
      '@rollup/rollup-linux-x64-musl': 4.60.1
      '@rollup/rollup-openbsd-x64': 4.60.1
      '@rollup/rollup-openharmony-arm64': 4.60.1
      '@rollup/rollup-win32-arm64-msvc': 4.60.1
      '@rollup/rollup-win32-ia32-msvc': 4.60.1
      '@rollup/rollup-win32-x64-gnu': 4.60.1
      '@rollup/rollup-win32-x64-msvc': 4.60.1
      fsevents: 2.3.3

  siginfo@2.0.0: {}

  source-map-js@1.2.1: {}

  stackback@0.0.2: {}

  std-env@3.10.0: {}

  strip-literal@3.1.0:
    dependencies:
      js-tokens: 9.0.1

  tar@7.4.3:
    dependencies:
      '@isaacs/fs-minipass': 4.0.1
      chownr: 3.0.0
      minipass: 7.1.3
      minizlib: 3.1.0
      mkdirp: 3.0.1
      yallist: 5.0.0

  tinybench@2.9.0: {}

  tinyexec@0.3.2: {}

  tinyglobby@0.2.16:
    dependencies:
      fdir: 6.5.0(picomatch@4.0.4)
      picomatch: 4.0.4

  tinypool@1.1.1: {}

  tinyrainbow@2.0.0: {}

  tinyspy@4.0.4: {}

  tslib@2.7.0: {}

  tsx@4.21.0:
    dependencies:
      esbuild: 0.27.7
      get-tsconfig: 4.14.0
    optionalDependencies:
      fsevents: 2.3.3

  typescript@5.9.3: {}

  undici-types@6.19.8: {}

  undici-types@7.16.0: {}

  vite-node@3.2.4(@types/node@24.12.2)(tsx@4.21.0):
    dependencies:
      cac: 6.7.14
      debug: 4.4.3
      es-module-lexer: 1.7.0
      pathe: 2.0.3
      vite: 7.3.2(@types/node@24.12.2)(tsx@4.21.0)
    transitivePeerDependencies:
      - '@types/node'
      - jiti
      - less
      - lightningcss
      - sass
      - sass-embedded
      - stylus
      - sugarss
      - supports-color
      - terser
      - tsx
      - yaml

  vite@7.3.2(@types/node@24.12.2)(tsx@4.21.0):
    dependencies:
      esbuild: 0.27.7
      fdir: 6.5.0(picomatch@4.0.4)
      picomatch: 4.0.4
      postcss: 8.5.10
      rollup: 4.60.1
      tinyglobby: 0.2.16
    optionalDependencies:
      '@types/node': 24.12.2
      fsevents: 2.3.3
      tsx: 4.21.0

  vitest@3.2.4(@types/node@24.12.2)(tsx@4.21.0):
    dependencies:
      '@types/chai': 5.2.3
      '@vitest/expect': 3.2.4
      '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@24.12.2)(tsx@4.21.0))
      '@vitest/pretty-format': 3.2.4
      '@vitest/runner': 3.2.4
      '@vitest/snapshot': 3.2.4
      '@vitest/spy': 3.2.4
      '@vitest/utils': 3.2.4
      chai: 5.3.3
      debug: 4.4.3
      expect-type: 1.3.0
      magic-string: 0.30.21
      pathe: 2.0.3
      picomatch: 4.0.4
      std-env: 3.10.0
      tinybench: 2.9.0
      tinyexec: 0.3.2
      tinyglobby: 0.2.16
      tinypool: 1.1.1
      tinyrainbow: 2.0.0
      vite: 7.3.2(@types/node@24.12.2)(tsx@4.21.0)
      vite-node: 3.2.4(@types/node@24.12.2)(tsx@4.21.0)
      why-is-node-running: 2.3.0
    optionalDependencies:
      '@types/node': 24.12.2
    transitivePeerDependencies:
      - jiti
      - less
      - lightningcss
      - msw
      - sass
      - sass-embedded
      - stylus
      - sugarss
      - supports-color
      - terser
      - tsx
      - yaml

  why-is-node-running@2.3.0:
    dependencies:
      siginfo: 2.0.0
      stackback: 0.0.2

  ws@8.17.1: {}

  yallist@5.0.0: {}

  zod@3.25.76: {}


path: scripts/package.json
content: {
  "name": "element-drop-scripts",
  "private": true,
  "type": "module",
  "scripts": {
    "typecheck": "tsc --noEmit",
    "check": "pnpm run typecheck",
    "test": "vitest run",
    "dev": "tsx src/cli.ts"
  },
  "dependencies": {
    "cuimp": "^1.10.0",
    "ethers": "^6.13.5",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@types/node": "^24.9.1",
    "tsx": "^4.20.6",
    "typescript": "^5.9.3",
    "vitest": "^3.2.4"
  }
}


path: scripts/tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "lib": ["ES2022", "DOM"],
    "types": ["node", "vitest/globals"],
    "outDir": "dist"
  },
  "include": ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts"]
}


path: scripts/vitest.config.ts
content: import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "node",
    restoreMocks: true
  }
});


path: scripts/src/schema.ts
content: import { z } from "zod";
import type { CreateDropInput, CreateStageInput, DropInput, StageInput } from "./types";
import { DEFAULT_MAX_SUPPLY, resolveDropMode } from "./drop-mode";
const DEFAULT_STAGE_DURATION_SECONDS = 3 * 24 * 60 * 60;
const DEFAULT_STAGE_NAME = "Public";
const DEFAULT_STAGE_PRICE = "0";
const DEFAULT_STAGE_INTERVAL = 0;
const DEFAULT_STAGE_MAX_MINTED_PER_WALLET = 1;
const DEFAULT_STAGE_ID = 256;

const stageSchema = z.object({
  stageID: z.number().int().nonnegative(),
  stageName: z.string().min(1),
  price: z.string().min(1),
  duration: z.number().int().positive(),
  maxSupplyAtThisStage: z.number().int().positive(),
  maxMintedPerWallet: z.number().int().positive(),
  interval: z.number().int().nonnegative(),
  stageMode: z.union([z.literal(0), z.literal(1)])
});

const createStageSchema = z.object({
  stageID: z.number().int().nonnegative().optional(),
  stageName: z.string().min(1),
  price: z.string().min(1),
  duration: z.number().int().positive(),
  maxSupplyAtThisStage: z.number().int().positive(),
  maxMintedPerWallet: z.number().int().positive(),
  interval: z.number().int().nonnegative(),
  stageMode: z.union([z.literal(0), z.literal(1)])
});

const createDropInputSchema = z.object({
  chainMId: z.number().int().positive(),
  name: z.string().min(1),
  symbol: z.string().min(1),
  paymentToken: z.string().min(1).optional(),
  preReveal: z.string().min(1).optional(),
  previewMedia: z.array(z.string().min(1)).optional(),
  description: z.string().optional(),
  desc: z.string().optional(),
  website: z.string().optional(),
  twitter: z.string().optional(),
  instagram: z.string().optional(),
  discord: z.string().optional(),
  telegram: z.string().optional(),
  medium: z.string().optional(),
  dropFeaturedImage: z.string().optional(),
  edition: z.union([z.literal("limited"), z.literal("open")]).optional(),
  dropType: z.union([z.literal(0), z.literal(1)]).optional(),
  maxSupply: z.number().int().positive().optional(),
  dropBeginTime: z.number().int().positive().optional(),
  stages: z.array(createStageSchema).min(1).optional()
}).superRefine((input, ctx) => {
  if (!input.preReveal) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "preReveal is required",
      path: ["preReveal"]
    });
  }
});

export const dropInputSchema = z
  .object({
    chainMId: z.number().int().positive(),
    name: z.string().min(1),
    symbol: z.string().min(1),
    paymentToken: z.string().min(1).optional(),
    preReveal: z.string().min(1),
    description: z.string(),
    dropType: z.union([z.literal(0), z.literal(1)]),
    maxSupply: z.number().int().positive(),
    dropBeginTime: z.number().int().positive(),
    stages: z.array(stageSchema).min(1)
  })
  .superRefine((input, ctx) => {
    const stageSupply = input.stages.reduce((sum, stage) => sum + stage.maxSupplyAtThisStage, 0);
    if (stageSupply > input.maxSupply) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "stages total maxSupply exceeds drop maxSupply"
      });
    }
  });

function defaultDropBeginTime(): number {
  return Math.floor(Date.now() / 1000) + 24 * 60 * 60;
}

function assignStageIds(stages: CreateStageInput[]): StageInput[] {
  return stages.map((stage, index) => ({
    ...stage,
    stageID: DEFAULT_STAGE_ID + index
  }));
}

function buildDefaultStage(maxSupply: number): StageInput {
  return {
    stageID: DEFAULT_STAGE_ID,
    stageName: DEFAULT_STAGE_NAME,
    price: DEFAULT_STAGE_PRICE,
    duration: DEFAULT_STAGE_DURATION_SECONDS,
    maxSupplyAtThisStage: maxSupply,
    maxMintedPerWallet: DEFAULT_STAGE_MAX_MINTED_PER_WALLET,
    interval: DEFAULT_STAGE_INTERVAL,
    stageMode: 0
  };
}

export function normalizeCreateDropInput(input: CreateDropInput): DropInput {
  const parsed = createDropInputSchema.parse(input);
  const { dropType, maxSupply } = resolveDropMode({
    requestedEdition: parsed.edition,
    requestedDropType: parsed.dropType,
    requestedMaxSupply: parsed.maxSupply,
    fallbackMaxSupply: DEFAULT_MAX_SUPPLY
  });

  const normalized: DropInput = {
    chainMId: parsed.chainMId,
    name: parsed.name,
    symbol: parsed.symbol,
    paymentToken: parsed.paymentToken,
    preReveal: parsed.preReveal ?? "",
    description: parsed.description ?? parsed.desc ?? "",
    dropType,
    maxSupply,
    dropBeginTime: parsed.dropBeginTime ?? defaultDropBeginTime(),
    stages: parsed.stages ? assignStageIds(parsed.stages) : [buildDefaultStage(maxSupply)]
  };

  return dropInputSchema.parse(normalized);
}

export type DropInputSchema = z.infer<typeof dropInputSchema>;


path: scripts/src/drop-mode.ts
content: import type { DropEdition, DropType } from "./types";

export const DEFAULT_DROP_TYPE = 0 as const;
export const DEFAULT_MAX_SUPPLY = 999;
export const OPEN_EDITION_MAX_SUPPLY = 1_000_000_000;

export function deriveEditionFromMaxSupply(maxSupply: unknown): DropEdition | null {
  if (typeof maxSupply !== "number" || !Number.isFinite(maxSupply)) {
    return null;
  }
  return maxSupply >= OPEN_EDITION_MAX_SUPPLY ? "open" : "limited";
}

export function resolveMaxSupplyForEdition(input: {
  requestedEdition?: DropEdition;
  requestedMaxSupply?: number;
  fallbackMaxSupply?: number;
}): number {
  if (input.requestedEdition === "open") {
    return OPEN_EDITION_MAX_SUPPLY;
  }

  if (input.requestedEdition === "limited" && input.requestedMaxSupply === undefined) {
    throw new Error("limited edition requires an explicit maxSupply");
  }

  return input.requestedMaxSupply ?? input.fallbackMaxSupply ?? DEFAULT_MAX_SUPPLY;
}

export function resolveDropMode(input: {
  requestedEdition?: DropEdition;
  requestedDropType?: DropType;
  requestedMaxSupply?: number;
  fallbackDropType?: DropType;
  fallbackMaxSupply?: number;
}): { dropType: DropType; maxSupply: number } {
  return {
    dropType: input.requestedDropType ?? input.fallbackDropType ?? DEFAULT_DROP_TYPE,
    maxSupply: resolveMaxSupplyForEdition(input)
  };
}


path: scripts/src/security/upload-guard.ts
content: import { lstat, readFile, realpath, stat } from "node:fs/promises";
import { basename, isAbsolute } from "node:path";

const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
const ALLOWED_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);

export function inferUploadExtension(filePath: string): string {
  const name = basename(filePath);
  const index = name.lastIndexOf(".");
  return index >= 0 ? name.slice(index) : "";
}

export function inferUploadImageMimeType(filePath: string): string {
  const extension = inferUploadExtension(filePath).toLowerCase();
  switch (extension) {
    case ".png":
      return "image/png";
    case ".jpg":
    case ".jpeg":
      return "image/jpeg";
    case ".gif":
      return "image/gif";
    case ".webp":
      return "image/webp";
    default:
      throw new Error(`Unsupported upload image extension: ${extension || "(none)"}`);
  }
}

function isSupportedImageHeader(extension: string, header: Buffer): boolean {
  if (extension === ".png") {
    return header.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
  }
  if (extension === ".jpg" || extension === ".jpeg") {
    return header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff;
  }
  if (extension === ".gif") {
    const signature = header.subarray(0, 6).toString("ascii");
    return signature === "GIF87a" || signature === "GIF89a";
  }
  if (extension === ".webp") {
    return header.subarray(0, 4).toString("ascii") === "RIFF" && header.subarray(8, 12).toString("ascii") === "WEBP";
  }
  return false;
}

export function assertSafeUploadFileName(fileName: string) {
  if (fileName !== basename(fileName) || fileName.startsWith(".") || fileName.includes("\0")) {
    throw new Error(`Unsafe upload file name: ${fileName}`);
  }
}

export async function validateLocalImageUpload(filePath: string): Promise<{ realPath: string; mimeType: string }> {
  if (!isAbsolute(filePath)) {
    throw new Error(`Upload path must be absolute: ${filePath}`);
  }

  const linkStats = await lstat(filePath);
  if (linkStats.isSymbolicLink()) {
    throw new Error(`Upload path must not be a symbolic link: ${filePath}`);
  }
  if (!linkStats.isFile()) {
    throw new Error(`Upload path must be a regular image file: ${filePath}`);
  }
  if (linkStats.size <= 0 || linkStats.size > MAX_UPLOAD_BYTES) {
    throw new Error(`Upload image size must be between 1 byte and ${MAX_UPLOAD_BYTES} bytes: ${filePath}`);
  }

  const realPath = await realpath(filePath);
  const realStats = await stat(realPath);
  if (!realStats.isFile()) {
    throw new Error(`Upload path must resolve to a regular image file: ${filePath}`);
  }

  const extension = inferUploadExtension(realPath).toLowerCase();
  if (!ALLOWED_IMAGE_EXTENSIONS.has(extension)) {
    throw new Error(`Unsupported upload image extension: ${extension || "(none)"}`);
  }

  const header = await readFile(realPath, { flag: "r" }).then((buffer) => buffer.subarray(0, 12));
  if (!isSupportedImageHeader(extension, header)) {
    throw new Error(`Upload file does not match supported image signature: ${filePath}`);
  }

  return {
    realPath,
    mimeType: inferUploadImageMimeType(realPath)
  };
}

export async function readValidatedLocalImageUpload(filePath: string): Promise<{
  bytes: ArrayBuffer;
  fileName: string;
  mimeType: string;
  realPath: string;
}> {
  const validated = await validateLocalImageUpload(filePath);
  const bytes = await readFile(validated.realPath);
  return {
    ...validated,
    bytes: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer,
    fileName: basename(validated.realPath)
  };
}


path: scripts/src/auth/jwt.ts
content: import { Wallet } from "ethers";
import type { ElementIdentity, ElementLoginInput } from "../types";
import { ELEMENT_CHAIN_METADATA_BY_CHAIN_MID } from "../network/chains";

export function getElementBlockChainByChainMId(chainMId: number): { chain: string; chainId: string } {
  const chain = ELEMENT_CHAIN_METADATA_BY_CHAIN_MID[chainMId];
  if (!chain) {
    throw new Error(`Unsupported chainMId for Element login: ${chainMId}`);
  }

  return {
    chain: chain.chain,
    chainId: `0x${chain.chainId.toString(16)}`
  };
}

export interface BuildElementAuthorizationDeps {
  getLoginNonce: (identity: ElementIdentity) => Promise<string>;
  loginAuth: (input: ElementLoginInput) => Promise<string>;
}

export interface ElementAuthorizationResult {
  authorization: string;
  nonce: string;
  message: string;
  identity: ElementIdentity;
}

export async function deriveWalletAddress(input: { privateKey: string }): Promise<string> {
  const wallet = new Wallet(input.privateKey);
  return wallet.address;
}

export function getElementIdentityByChainMId(chainMId: number, walletAddress: string): ElementIdentity {
  const blockChain = getElementBlockChainByChainMId(chainMId);

  return {
    address: walletAddress,
    blockChain
  };
}

export function buildElementLoginMessage(input: { walletAddress: string; nonce: string }): string {
  return `Welcome to Element!\n   \nClick "Sign" to sign in. No password needed!\n   \nI accept the Element Terms of Service: \n https://element.market/tos\n   \nWallet address:\n${input.walletAddress}\n   \nNonce:\n${input.nonce}`;
}

export async function buildElementAuthorization(
  deps: BuildElementAuthorizationDeps,
  input: {
    privateKey: string;
    walletAddress?: string;
    chainMId: number;
  }
): Promise<ElementAuthorizationResult> {
  const walletAddress =
    (input.walletAddress ?? (await deriveWalletAddress({ privateKey: input.privateKey }))).toLowerCase();
  const identity = getElementIdentityByChainMId(input.chainMId, walletAddress);
  const nonce = await deps.getLoginNonce(identity);
  const message = buildElementLoginMessage({ walletAddress, nonce });
  const wallet = new Wallet(input.privateKey);
  const signature = await wallet.signMessage(message);
  const token = await deps.loginAuth({
    identity,
    message,
    signature
  });

  return {
    authorization: `Bearer ${token}`,
    nonce,
    message,
    identity
  };
}


path: scripts/src/network/rpc.ts
content: import { getJson } from "../api/http";

export const RPC_CONF_API_URL = "https://api.element.market/v1/quote/rpcConfInfo";

export interface RpcConfInfo {
  chainMId: number;
  rpcUrl: string;
  isWalletPriority: boolean;
}

interface RpcConfResponse {
  code: number;
  status: string;
  data: RpcConfInfo[];
}

const STATIC_RPC_URLS: Record<number, string> = {
  1: "https://api.zan.top/node/v1/eth/mainnet/6e96cfbcaff949bfbdaeb5fbc554ac7c"
};

let cachedRpcConfigs: Map<number, RpcConfInfo> | null = null;
let lastFetchTime = 0;
const CACHE_DURATION_MS = 5 * 60 * 1000;

export async function fetchRpcConfigs(): Promise<RpcConfInfo[]> {
  const result = await getJson<RpcConfResponse>(RPC_CONF_API_URL);
  if (result.code !== 0) {
    throw new Error(`Failed to fetch rpc configs: ${result.status}`);
  }

  return result.data;
}

export async function getCachedRpcConfigs(): Promise<Map<number, RpcConfInfo>> {
  const now = Date.now();
  if (cachedRpcConfigs && now - lastFetchTime < CACHE_DURATION_MS) {
    return cachedRpcConfigs;
  }

  const configs = await fetchRpcConfigs();
  const filtered = configs.filter((config) => !config.isWalletPriority);
  cachedRpcConfigs = new Map(filtered.map((config) => [config.chainMId, config]));

  Object.entries(STATIC_RPC_URLS).forEach(([chainMId, rpcUrl]) => {
    const numericChainMId = Number(chainMId);
    if (!cachedRpcConfigs?.has(numericChainMId)) {
      cachedRpcConfigs?.set(numericChainMId, {
        chainMId: numericChainMId,
        rpcUrl,
        isWalletPriority: false
      });
    }
  });

  lastFetchTime = now;
  return cachedRpcConfigs;
}

export async function getRpcUrlForChainMId(chainMId: number): Promise<string> {
  const configs = await getCachedRpcConfigs();
  const rpcUrl = configs.get(chainMId)?.rpcUrl;
  if (!rpcUrl) {
    throw new Error(`Unsupported chainMId for rpc resolution: ${chainMId}`);
  }
  return rpcUrl;
}

export function __resetRpcConfigCacheForTests(): void {
  cachedRpcConfigs = null;
  lastFetchTime = 0;
}


path: scripts/src/network/transaction.ts
content: import { Interface, JsonRpcProvider, Wallet } from "ethers";
import { getRequiredWalletPrivateKey } from "../env";
import type { EncodedTransaction, ExecutedTransactionResult } from "../types";

const TOKEN_FACTORY_INTERFACE = new Interface([
  "event CreateToken(address indexed tokenAddress, uint8 tokenType, string name, string symbol, address implementation, address protocolRecipient, uint16 protocolPoint)"
]);

function getConfiguredMinPriorityFeePerGasWei(): bigint | undefined {
  const configured = process.env.ELEMENT_MIN_PRIORITY_FEE_PER_GAS_WEI;
  if (!configured) {
    return undefined;
  }
  const parsed = BigInt(configured);
  if (parsed <= 0n) {
    throw new Error("ELEMENT_MIN_PRIORITY_FEE_PER_GAS_WEI must be greater than 0");
  }
  return parsed;
}

export function buildFeeOverrides(input: {
  maxFeePerGas?: bigint | null;
  maxPriorityFeePerGas?: bigint | null;
  gasPrice?: bigint | null;
  minPriorityFeePerGas?: bigint;
}) {
  const minPriorityFeePerGas = input.minPriorityFeePerGas ?? getConfiguredMinPriorityFeePerGasWei();
  if (input.maxFeePerGas !== null && input.maxFeePerGas !== undefined) {
    const feeOverrides: { maxFeePerGas: bigint; maxPriorityFeePerGas?: bigint } = {
      maxFeePerGas: input.maxFeePerGas
    };
    const priorityFee =
      input.maxPriorityFeePerGas !== null && input.maxPriorityFeePerGas !== undefined
        ? minPriorityFeePerGas !== undefined && input.maxPriorityFeePerGas < minPriorityFeePerGas
          ? minPriorityFeePerGas
          : input.maxPriorityFeePerGas
        : minPriorityFeePerGas;
    if (priorityFee !== undefined) {
      feeOverrides.maxPriorityFeePerGas = priorityFee;
      if (feeOverrides.maxFeePerGas < priorityFee) {
        feeOverrides.maxFeePerGas = priorityFee;
      }
    }
    return feeOverrides;
  }
  if (input.gasPrice !== null && input.gasPrice !== undefined) {
    return {
      gasPrice:
        minPriorityFeePerGas !== undefined && input.gasPrice < minPriorityFeePerGas
          ? minPriorityFeePerGas
          : input.gasPrice
    };
  }
  return minPriorityFeePerGas === undefined
    ? {}
    : {
        maxPriorityFeePerGas: minPriorityFeePerGas,
        maxFeePerGas: minPriorityFeePerGas
      };
}

export function parseMinimumTipCapWei(error: unknown): bigint | null {
  const message = error instanceof Error ? error.message : String(error);
  const match = message.match(/minimum needed\s+(\d+)/i);
  return match ? BigInt(match[1]) : null;
}

export async function sendEncodedTransaction(input: {
  rpcUrl: string;
  transaction: EncodedTransaction;
  waitConfirmations?: number;
  logger?: (message: string, meta?: Record<string, unknown>) => void;
}): Promise<ExecutedTransactionResult> {
  input.logger?.("send transaction:start", {
    rpcUrl: input.rpcUrl,
    to: input.transaction.to,
    value: input.transaction.value,
    waitConfirmations: input.waitConfirmations ?? 1
  });
  const privateKey = getRequiredWalletPrivateKey();
  const provider = new JsonRpcProvider(input.rpcUrl);
  const wallet = new Wallet(privateKey, provider);
  let minPriorityFeePerGas: bigint | undefined;
  let response;
  for (let attempt = 1; attempt <= 2; attempt += 1) {
    const feeData = await provider.getFeeData();
    const feeOverrides = buildFeeOverrides({
      maxFeePerGas: feeData.maxFeePerGas,
      maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
      gasPrice: feeData.gasPrice,
      minPriorityFeePerGas
    });
    try {
      response = await wallet.sendTransaction({
        to: input.transaction.to,
        value: input.transaction.value,
        data: input.transaction.data,
        ...feeOverrides
      });
      break;
    } catch (error) {
      const minimumTipCapWei = parseMinimumTipCapWei(error);
      if (attempt >= 2 || minimumTipCapWei === null) {
        throw error;
      }
      minPriorityFeePerGas = minimumTipCapWei;
      input.logger?.("send transaction:retry-min-tip-cap", {
        minimumTipCapWei: minimumTipCapWei.toString()
      });
    }
  }
  if (!response) {
    throw new Error("Transaction response missing after send attempt");
  }
  const receipt = await response.wait(input.waitConfirmations ?? 1);
  if (!receipt) {
    throw new Error(`Transaction receipt missing for ${response.hash}`);
  }
  if (receipt.status !== 1) {
    throw new Error(`Transaction failed: ${response.hash}`);
  }

  let contractAddress: string | undefined;
  for (const log of receipt.logs) {
    try {
      const parsed = TOKEN_FACTORY_INTERFACE.parseLog(log);
      if (parsed?.name === "CreateToken") {
        contractAddress = String(parsed.args.tokenAddress).toLowerCase();
        break;
      }
    } catch {
      // ignore unrelated logs
    }
  }

  input.logger?.("send transaction:confirmed", {
    hash: response.hash,
    blockNumber: receipt.blockNumber,
    contractAddress
  });

  return {
    hash: response.hash,
    contractAddress,
    receipt: {
      hash: receipt.hash,
      blockNumber: receipt.blockNumber,
      status: receipt.status,
      gasUsed: receipt.gasUsed.toString()
    }
  };
}


path: scripts/src/network/chains.ts
content: import type { ChainListPaymentToken, ChainListWithGasItem, ElementApiResponse, GetChainsWithGasResponse } from "../types";

export interface ElementChainMetadata {
  chainMId: number;
  chain: string;
  chainId: number;
  chainName: string;
  aliases: string[];
}

export const ELEMENT_CHAIN_METADATA_BY_CHAIN_MID: Record<number, ElementChainMetadata> = {
  1: { chainMId: 1, chain: "eth", chainId: 1, chainName: "ethereum", aliases: ["ethereum", "mainnet", "eth"] },
  101: { chainMId: 101, chain: "polygon", chainId: 137, chainName: "polygon", aliases: ["polygon", "matic"] },
  201: { chainMId: 201, chain: "bsc", chainId: 56, chainName: "bsc", aliases: ["bsc", "bnb", "binance", "binance smart chain"] },
  401: { chainMId: 401, chain: "avalanche", chainId: 43114, chainName: "avalanche", aliases: ["avalanche", "avax"] },
  601: { chainMId: 601, chain: "arbitrum", chainId: 42161, chainName: "arbitrum", aliases: ["arbitrum", "arbitrum one"] },
  701: { chainMId: 701, chain: "zksync", chainId: 324, chainName: "zksync", aliases: ["zksync", "zksync era"] },
  901: { chainMId: 901, chain: "linea", chainId: 59144, chainName: "linea", aliases: ["linea"] },
  1101: { chainMId: 1101, chain: "opbnb", chainId: 204, chainName: "opbnb", aliases: ["opbnb"] },
  1201: { chainMId: 1201, chain: "base", chainId: 8453, chainName: "base", aliases: ["base"] },
  1301: { chainMId: 1301, chain: "scroll", chainId: 534352, chainName: "scroll", aliases: ["scroll"] },
  1401: { chainMId: 1401, chain: "manta_pacific", chainId: 169, chainName: "manta", aliases: ["manta", "manta pacific"] },
  1501: { chainMId: 1501, chain: "optimism", chainId: 10, chainName: "optimism", aliases: ["optimism", "op"] },
  1601: { chainMId: 1601, chain: "mantle", chainId: 5000, chainName: "mantle", aliases: ["mantle"] },
  1701: { chainMId: 1701, chain: "zkfair", chainId: 42766, chainName: "zkfair", aliases: ["zkfair"] },
  1801: { chainMId: 1801, chain: "blast", chainId: 81457, chainName: "blast", aliases: ["blast"] },
  1901: { chainMId: 1901, chain: "merlin", chainId: 4200, chainName: "merlin", aliases: ["merlin", "merlin chain"] },
  2001: { chainMId: 2001, chain: "mode", chainId: 34443, chainName: "mode", aliases: ["mode"] },
  2101: { chainMId: 2101, chain: "cyber", chainId: 7560, chainName: "cyber", aliases: ["cyber", "cyberconnect"] },
  2201: { chainMId: 2201, chain: "bob", chainId: 60808, chainName: "bob", aliases: ["bob"] },
  2301: { chainMId: 2301, chain: "lightlink", chainId: 1890, chainName: "lightlink", aliases: ["lightlink"] },
  2501: { chainMId: 2501, chain: "nanon", chainId: 2748, chainName: "nanon", aliases: ["nanon"] },
  2601: { chainMId: 2601, chain: "bera", chainId: 80094, chainName: "berachain", aliases: ["bera", "berachain"] },
  2701: { chainMId: 2701, chain: "zeta", chainId: 7000, chainName: "zetachain", aliases: ["zeta", "zetachain"] },
  2801: { chainMId: 2801, chain: "nibiru", chainId: 6900, chainName: "nibiru", aliases: ["nibiru"] },
  2901: { chainMId: 2901, chain: "abstract", chainId: 2741, chainName: "abstract", aliases: ["abstract"] },
  3001: { chainMId: 3001, chain: "monad", chainId: 143, chainName: "monad", aliases: ["monad"] },
  3101: { chainMId: 3101, chain: "bitlayer", chainId: 200901, chainName: "bitlayer", aliases: ["bitlayer"] },
  3201: { chainMId: 3201, chain: "mantra", chainId: 5888, chainName: "mantra", aliases: ["mantra"] }
};

const CHAIN_MID_BY_ALIAS = new Map(
  Object.values(ELEMENT_CHAIN_METADATA_BY_CHAIN_MID).flatMap((metadata) =>
    metadata.aliases.map((alias) => [normalizeChainName(alias), metadata.chainMId] as const)
  )
);

let cachedChainsWithGas: ChainListWithGasItem[] | null = null;

function normalizeChainName(input: string): string {
  return input.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
}

export function getElementChainNameByChainMId(chainMId: number): string {
  const metadata = ELEMENT_CHAIN_METADATA_BY_CHAIN_MID[chainMId];
  if (!metadata) {
    throw new Error(`Unsupported chainMId for Element chain resolution: ${chainMId}`);
  }
  return metadata.chainName;
}

function sortSupportedChainNames(chainMIds: number[]): string[] {
  return chainMIds
    .map((chainMId) => ELEMENT_CHAIN_METADATA_BY_CHAIN_MID[chainMId]?.chainName)
    .filter((name): name is string => Boolean(name))
    .sort((a, b) => a.localeCompare(b));
}

export interface ResolveElementChainInput {
  chainName?: string;
  chainMId?: number;
}

export interface ResolveElementChainResult {
  chainMId: number;
  chainName: string;
  paymentTokens: ChainListPaymentToken[];
  currency: string;
}

function normalizePaymentTokenSelector(input: string): string {
  return input.trim().toLowerCase();
}

export function resolvePaymentToken(input: {
  paymentToken?: string;
  availablePaymentTokens?: ChainListPaymentToken[];
}): ChainListPaymentToken | null {
  if (!input.paymentToken) {
    return null;
  }

  const availablePaymentTokens = input.availablePaymentTokens ?? [];
  const selector = normalizePaymentTokenSelector(input.paymentToken);
  return (
    availablePaymentTokens.find((token) => {
      return (
        normalizePaymentTokenSelector(token.name) === selector ||
        (token.symbol ? normalizePaymentTokenSelector(token.symbol) === selector : false) ||
        normalizePaymentTokenSelector(token.TokenAddress) === selector
      );
    }) ?? null
  );
}

function formatSupportedPaymentTokens(paymentTokens: ChainListPaymentToken[]): string {
  return paymentTokens
    .map((token) => {
      const symbol = token.symbol ? `/${token.symbol}` : "";
      return `${token.name}${symbol} (${token.TokenAddress})`;
    })
    .join(", ");
}

export function resolveMintingTypeFromPaymentToken(input: {
  paymentToken?: string;
  availablePaymentTokens?: ChainListPaymentToken[];
  fallbackMintingType?: number;
}): number {
  if (!input.paymentToken) {
    return input.fallbackMintingType ?? 0;
  }

  const availablePaymentTokens = input.availablePaymentTokens ?? [];
  const matchedToken = resolvePaymentToken({
    paymentToken: input.paymentToken,
    availablePaymentTokens
  });

  if (!matchedToken) {
    throw new Error(
      `Unsupported paymentToken: ${input.paymentToken}. Supported payment tokens: ${formatSupportedPaymentTokens(
        availablePaymentTokens
      )}`
    );
  }

  return matchedToken.SerId << 4;
}

export function resolvePaymentTokenFromMintingType(input: {
  mintingType?: number;
  availablePaymentTokens?: ChainListPaymentToken[];
}): ChainListPaymentToken | null {
  if (!Number.isFinite(input.mintingType) || (input.mintingType ?? 0) <= 0) {
    return null;
  }

  const serId = Math.floor((input.mintingType as number) / 16);
  if (serId <= 0) {
    return null;
  }

  return (input.availablePaymentTokens ?? []).find((token) => token.SerId === serId) ?? null;
}

export async function resolveElementChain(
  input: ResolveElementChainInput,
  deps: {
    getChainsWithGas: () => Promise<ElementApiResponse<GetChainsWithGasResponse>>;
  }
): Promise<ResolveElementChainResult> {
  if (!cachedChainsWithGas) {
    const response = await deps.getChainsWithGas();
    cachedChainsWithGas = response.data.chains;
  }

  const supportedChainIds = cachedChainsWithGas.map((item) => item.chainMId);
  let chainMId = input.chainMId;

  if (input.chainName) {
    chainMId = CHAIN_MID_BY_ALIAS.get(normalizeChainName(input.chainName));
    if (!chainMId) {
      throw new Error(
        `Unsupported chainName: ${input.chainName}. Supported chains: ${sortSupportedChainNames(supportedChainIds).join(", ")}`
      );
    }
  }

  if (!chainMId) {
    throw new Error("chainName is required");
  }

  const supportedChain = cachedChainsWithGas.find((item) => item.chainMId === chainMId);
  if (!supportedChain) {
    throw new Error(
      `Unsupported chain for Element Drop: ${getElementChainNameByChainMId(chainMId)}. Supported chains: ${sortSupportedChainNames(
        supportedChainIds
      ).join(", ")}`
    );
  }

  return {
    chainMId,
    chainName: getElementChainNameByChainMId(chainMId),
    paymentTokens: supportedChain.paymentTokens,
    currency: supportedChain.currency
  };
}


path: scripts/src/types.ts
content: export type DropType = 0 | 1;
export type DropEdition = "limited" | "open";
export type StageMode = 0 | 1;

export interface StageInput {
  stageID: number;
  stageName: string;
  price: string;
  duration: number;
  maxSupplyAtThisStage: number;
  maxMintedPerWallet: number;
  interval: number;
  stageMode: StageMode;
}

export interface CreateStageInput {
  stageID?: number;
  stageName: string;
  price: string;
  duration: number;
  maxSupplyAtThisStage: number;
  maxMintedPerWallet: number;
  interval: number;
  stageMode: StageMode;
}

export interface DropInput {
  chainMId: number;
  name: string;
  symbol: string;
  paymentToken?: string;
  preReveal: string;
  description: string;
  dropType: DropType;
  maxSupply: number;
  dropBeginTime: number;
  stages: StageInput[];
}

export interface CreateDropInput {
  chainMId: number;
  name: string;
  symbol: string;
  paymentToken?: string;
  preReveal?: string;
  previewMedia?: string[];
  description?: string;
  desc?: string;
  website?: string;
  twitter?: string;
  instagram?: string;
  discord?: string;
  telegram?: string;
  medium?: string;
  dropFeaturedImage?: string;
  edition?: DropEdition;
  dropType?: DropType;
  maxSupply?: number;
  dropBeginTime?: number;
  stages?: CreateStageInput[];
}

export interface CreateTokenRequest {
  chainMId: number;
  name: string;
  symbol: string;
}

export interface ElementBlockChainIdentity {
  chain: string;
  chainId: string;
}

export interface ElementIdentity {
  address: string;
  blockChain: ElementBlockChainIdentity;
}

export interface ElementChainCollectionSummary {
  id: string;
  slug: string;
}

export interface ElementCollectionContractResponse {
  chainCollection: ElementChainCollectionSummary | null;
}

export interface ElementCollectionSummary {
  id: string;
  name: string;
  slug: string;
}

export interface ElementUserCollectionListItem extends ElementCollectionSummary {
  imageUrl: string | null;
  featuredImageUrl: string | null;
  bannerImageUrl: string | null;
  isVerified: boolean;
  description: string | null;
  stats: {
    assetCount: number;
  };
  contracts: Array<{
    sourceType: string | null;
  }>;
}

export interface ElementCollectionContractInfo {
  blockChain: ElementBlockChainIdentity;
  address: string;
  sourceType: string | null;
}

export interface ElementCollectionDetailFromEditors {
  contracts: ElementCollectionContractInfo[];
  id: string;
  name: string;
  slug: string;
  description: string | null;
  imageUrl: string | null;
  bannerImageUrl: string | null;
  featuredImageUrl: string | null;
  externalUrl: string | null;
  weiboUrl: string | null;
  twitterUrl: string | null;
  instagramUrl: string | null;
  facebookUrl: string | null;
  mediumUrl: string | null;
  telegramUrl: string | null;
  discordUrl: string | null;
  categories: Array<{
    id: string;
    name: string;
    slug: string;
  }>;
  paymentTokens: Array<{
    id: string;
    name: string;
    address: string;
    symbol: string;
    chain: string;
    chainId: string;
  }>;
  royalty: number;
  royaltyAddress: string | null;
  isVerified: boolean;
}

export interface ElementLoginInput {
  identity: ElementIdentity;
  message: string;
  signature: string;
  realm?: string;
  source?: string;
}

export interface ElementCollectionEditInput {
  collectionId: string;
  [REDACTED_SECRET];
  imageFilePath?: string;
  name?: string;
  slug?: string;
  description?: string;
  image?: unknown;
  featuredImage?: unknown;
  bannerImage?: unknown;
  externalUrl?: string;
  weiboUrl?: string;
  twitterUrl?: string;
  instagramUrl?: string;
  facebookUrl?: string;
  mediumUrl?: string;
  telegramUrl?: string;
  discordUrl?: string;
  categories?: string[];
  paymentTokens?: string[];
  royalty?: number;
  royaltyAddress?: string;
}

export interface EncodedTransaction {
  from: string;
  to: string;
  value: string;
  data: string;
}

export interface ElementApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

export interface OssSignSingleRequest {
  chainMId: number;
  contractAddress: string;
  mediaType: "prereveal" | "design";
}

export interface OssSignedPostData {
  accessid: string;
  policy: string;
  signature: string;
  dir: string;
  host: string;
  expire?: string;
  x_oss_credential?: string;
  x_oss_date?: string;
  security_token?: string;
  callback?: string;
}

export interface PreRevealUploadPayload {
  chainMId: number;
  contractAddress: string;
  dropType: DropType;
  preRevealExt: {
    image_url: string;
    animation_url: string;
  };
}

export interface DesignUploadPayload {
  dropID: number;
  chainMId?: number;
  contractAddress?: string;
  dropName: string;
  bannerURL: string;
  previewMediaExt: Array<{
    image_url: string;
    animation_url: string;
  }>;
  dropFeaturedImage: string;
  description: string;
  website: string;
  twitter: string;
  instagram: string;
  discord: string;
  telegram: string;
  medium: string;
  detailsUpdate: Array<{
    detailID?: number;
    template?: number;
    title?: string;
    description?: string;
    imageURL?: string;
    textAlign?: number | null;
    faq?: unknown;
  }>;
}

export interface CreateTokenPreflight {
  walletAddress: string;
  rpcUrl: string;
  authorization: string;
  nonce: string;
  loginMessage: string;
  identity: ElementIdentity;
}

export interface ExecutedTransactionReceipt {
  hash: string;
  blockNumber: number;
  status: number;
  gasUsed: string;
}

export interface ExecutedTransactionResult {
  hash: string;
  receipt: ExecutedTransactionReceipt;
  contractAddress?: string;
}

export interface GetDropSettingsQuery {
  chainMId: number;
  contractAddress: string;
}

export interface GetDropSettingsStage {
  stageID: number;
  stageName: string;
  price: string;
  interval: number;
  duration: number;
  maxSupplyAtThisStage: number;
  maxMintedPerWallet: number;
  stageMode: number;
  whiteListNum: number;
  enableCallFromContract: boolean;
  enableMintToOther: boolean;
  address?: string;
}

export interface PaymentTokenInfo {
  symbol?: string;
  address?: string;
  decimals?: number;
  chain?: string;
  chainId?: string;
  logoUrl?: string;
}

export interface GetDropSettingsResponse {
  dropID: number;
  published: number;
  maxSupply?: number;
  feeRecipient?: string;
  fee?: number;
  dropBeginTime?: number;
  isMinted: boolean;
  dropType: DropType;
  baseURI: string;
  isStageQuotaSharedForWallet: boolean;
  isPaused: boolean;
  mintingType: number;
  paymentToken?: PaymentTokenInfo;
  paymentTokens: PaymentTokenInfo[];
  mintFee: string;
  totalMint: number;
  batch: string;
  batchTime: number;
  stages: GetDropSettingsStage[];
}

export interface PostDropSettingsAllowList {
  address: string;
  buyCount: number;
}

export interface PostDropSettingsStageUpdate {
  stageID: number;
  stageName: string;
  price: string;
  interval: number;
  duration: number;
  maxMintedPerWallet: number;
  maxSupplyAtThisStage: number;
  stageMode: StageMode | null;
  allowListsNew: PostDropSettingsAllowList[];
}

export interface PostDropSettingsRequest {
  dropID: number;
  chainMId: number;
  contractAddress: string;
  dropType: DropType;
  maxSupply: number;
  fee: number;
  feeRecipient: string;
  mintingType: number;
  dropBeginTime: number;
  stagesUpdate: PostDropSettingsStageUpdate[];
}

export interface GetDropDesignQuery {
  dropID: number;
  chainMId: number;
  contractAddress: string;
}

export interface DropDesignDetail {
  detailID: number;
  template: number;
  title: string;
  description: string;
  imageURL: string;
  textAlign: number | null;
  faq: unknown;
}

export interface GetDropDesignResponse {
  dropID: number;
  dropName: string;
  bannerURL: string;
  previewMedia: string[];
  previewMediaExt: Array<{
    image_url?: string;
    animation_url?: string;
  }>;
  description: string;
  website: string;
  twitter: string;
  instagram: string;
  discord: string;
  telegram: string;
  medium: string;
  dropFeaturedImage: string;
  details: DropDesignDetail[];
}

export interface TempUrlMetaJson {
  [key: string]: unknown;
}

export interface GetTempURLQuery {
  chainMId: number;
  contractAddress: string;
  dropID: number;
  page: number;
  pageSize: number;
}

export interface GetTempURLResponse {
  preReveal: {
    image_url?: string;
    animation_url?: string;
    imageURL?: string;
    animationURL?: string;
  };
  metaPublish: boolean;
  metaDataIPFS: string;
  metaJsons: TempUrlMetaJson[];
  imgSum: number;
  metaSum: number;
  animationSum: number;
}

export interface GetPreRevealIPFSQuery {
  chainMId: number;
  contractAddress: string;
  dropID: number;
}

export interface GetPreRevealIPFSResponse {
  preRevealIPFS: string;
}

export interface CallbackUpdateProjectConfigRequest {
  dropId: number;
  chainMId: number;
  contractAddress: string;
}

export interface SetProjectConfigStageInput {
  stageID: number;
  stageName?: string;
  stageMode: number;
  price: string;
  address?: string;
  duration: number;
  maxSupplyAtThisStage: number;
  maxMintedPerWallet: number;
  interval: number;
  enableCallFromContract?: boolean;
  enableMintToOther?: boolean;
}

export interface SetProjectConfigRequest {
  chainMId: number;
  contractAddress: string;
  nftAddress: string;
  isPaused: boolean;
  dropBeginTime: number;
  maxSupply: string;
  baseURI: string;
  mintingType: number;
  stages: SetProjectConfigStageInput[];
}

export interface ChainListPaymentToken {
  ChainMId: number;
  SerId: number;
  TokenAddress: string;
  Enable: boolean;
  name: string;
  symbol?: string;
  icon: string;
  decimal: number;
  accuracy: number;
}

export interface ChainListWithGasItem {
  chainMId: number;
  currency: string;
  tokenCount: number;
  usdPrice: number;
  paymentTokens: ChainListPaymentToken[];
}

export interface GetChainsWithGasResponse {
  chains: ChainListWithGasItem[];
}


path: scripts/src/workflow/configure-drop.ts
content: import type { CreateStageInput, DropEdition, GetDropDesignResponse, GetDropSettingsStage, StageInput } from "../types";
import { resolveMintingTypeFromPaymentToken } from "../network/chains";
import { DEFAULT_MAX_SUPPLY, resolveDropMode } from "../drop-mode";
import { buildDropUrls, buildInitialSettingsPayload, createWorkflowLogger, runStage } from "./create-drop";
import {
  buildCollectionEditPayload,
  buildDisplayDesignAfterUpdate,
  buildMergedDesignPayload,
  hasCollectionMetadataOverride,
  hasDesignOverride
} from "./design-payloads";
import { uploadWithExistingAuthorization } from "./uploads";
import type {
  ChainListPaymentToken,
  DesignUploadPayload,
  ElementApiResponse,
  ElementCollectionDetailFromEditors,
  ElementCollectionEditInput,
  ElementCollectionSummary,
  GetDropSettingsResponse,
  OssSignedPostData,
  PostDropSettingsRequest,
  PreRevealUploadPayload
} from "../types";

export interface ConfigureDropFlowInput {
  chainMId: number;
  authorization: string;
  walletAddress: string;
  contractAddress: string;
  slug?: string;
  name?: string;
  symbol?: string;
  paymentToken?: string;
  paymentTokens?: ChainListPaymentToken[];
  preReveal?: string;
  previewMedia?: string[];
  description?: string;
  edition?: DropEdition;
  dropType?: 0 | 1;
  maxSupply?: number;
  dropBeginTime?: number;
  stages?: CreateStageInput[];
  bannerFilePath?: string;
  previewFilePaths?: string[];
  website?: string;
  twitter?: string;
  instagram?: string;
  discord?: string;
  telegram?: string;
  medium?: string;
  dropFeaturedImage?: string;
}

export interface ConfigureDropFlowDeps {
  uploadWithExistingAuthorization: typeof uploadWithExistingAuthorization;
  getDropSettings: (
    query: { chainMId: number; contractAddress: string },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetDropSettingsResponse>>;
  getDropDesign: (
    query: { dropID: number; chainMId: number; contractAddress: string },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetDropDesignResponse>>;
  postDropSettings: (
    authorization: string,
    body: PostDropSettingsRequest,
    walletAddress: string
  ) => Promise<ElementApiResponse<null>>;
  getOssSignSingle: (
    authorization: string,
    query: { chainMId: number; contractAddress: string; mediaType: "prereveal" | "design" },
    walletAddress?: string
  ) => Promise<ElementApiResponse<OssSignedPostData>>;
  postPreReveal: (
    authorization: string,
    body: PreRevealUploadPayload,
    walletAddress?: string
  ) => Promise<ElementApiResponse<null>>;
  postDesign: (
    authorization: string,
    body: DesignUploadPayload,
    walletAddress?: string
  ) => Promise<ElementApiResponse<null>>;
  getMutateToken: (authorization?: string) => Promise<string>;
  getCollectionDetailFromEditors: (
    slug: string,
    authorization?: string
  ) => Promise<ElementCollectionDetailFromEditors>;
  collectionEdit: (
    input: ElementCollectionEditInput,
    authorization?: string
  ) => Promise<ElementCollectionSummary>;
  uploadAsset: (input: {
    filePath: string;
    fileName: string;
    oss: OssSignedPostData;
  }) => Promise<{
    sourcePath: string;
    fileName: string;
    objectKey: string;
    publicUrl: string;
  }>;
}

const DEFAULT_STAGE_DURATION_SECONDS = 3 * 24 * 60 * 60;
const DEFAULT_STAGE_NAME = "Public";
const DEFAULT_STAGE_PRICE = "0";
const DEFAULT_STAGE_INTERVAL = 0;
const DEFAULT_STAGE_MAX_MINTED_PER_WALLET = 1;
const DEFAULT_STAGE_ID = 256;

function hasSettingsOverride(input: ConfigureDropFlowInput): boolean {
  return Boolean(
    input.preReveal !== undefined ||
      input.edition !== undefined ||
      input.dropType !== undefined ||
      input.maxSupply !== undefined ||
      input.dropBeginTime !== undefined ||
      input.paymentToken !== undefined ||
      (input.stages !== undefined && input.stages.length >= 0)
  );
}

function defaultDropBeginTime(): number {
  return Math.floor(Date.now() / 1000) + 24 * 60 * 60;
}

function assignStageIds(stages: CreateStageInput[]): StageInput[] {
  return stages.map((stage, index) => ({
    ...stage,
    stageID: DEFAULT_STAGE_ID + index
  }));
}

function buildDefaultStage(maxSupply: number): StageInput {
  return {
    stageID: DEFAULT_STAGE_ID,
    stageName: DEFAULT_STAGE_NAME,
    price: DEFAULT_STAGE_PRICE,
    duration: DEFAULT_STAGE_DURATION_SECONDS,
    maxSupplyAtThisStage: maxSupply,
    maxMintedPerWallet: DEFAULT_STAGE_MAX_MINTED_PER_WALLET,
    interval: DEFAULT_STAGE_INTERVAL,
    stageMode: 0
  };
}

export async function previewConfigureDrop(input: {
  authorization: string;
  walletAddress: string;
  chainMId: number;
  contractAddress: string;
  slug?: string;
  patch: Omit<ConfigureDropFlowInput, "authorization" | "walletAddress" | "chainMId" | "contractAddress" | "slug">;
}, deps: Pick<ConfigureDropFlowDeps, "getDropSettings" | "getDropDesign" | "getMutateToken" | "getCollectionDetailFromEditors">) {
  const logger = createWorkflowLogger();
  const mergedInput: ConfigureDropFlowInput = {
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    [REDACTED_SECRET],
    walletAddress: input.walletAddress,
    slug: input.slug,
    ...input.patch
  };
  const currentSettingsResponse = await runStage(logger, "previewUpdate:getCurrentSettings", () =>
    deps.getDropSettings(
      {
        chainMId: input.chainMId,
        contractAddress: input.contractAddress
      },
      input.walletAddress
    )
  );
  const currentSettings = currentSettingsResponse.data;
  const shouldUpdateSettings = hasSettingsOverride(mergedInput);
  const settingsPayload = shouldUpdateSettings
    ? buildMergedSettingsPayload({
        current: currentSettings,
        patch: mergedInput,
        contractAddress: input.contractAddress
      })
    : null;
  const shouldUpdateDesign = hasDesignOverride(mergedInput);
  const currentDesignResponse =
    currentSettings.dropID > 0 && shouldUpdateDesign
      ? await runStage(logger, "previewUpdate:getCurrentDesign", () =>
          deps.getDropDesign(
            {
              dropID: currentSettings.dropID,
              chainMId: input.chainMId,
              contractAddress: input.contractAddress
            },
            input.walletAddress
          )
        )
      : null;
  const currentDesign = currentDesignResponse?.data ?? null;
  const designPayload =
    shouldUpdateDesign
      ? buildMergedDesignPayload({
          current: currentDesign,
          patch: mergedInput,
          chainMId: input.chainMId,
          contractAddress: input.contractAddress,
          dropID: currentSettings.dropID,
          uploadedBannerUrl: null,
          uploadedPreviewUrls: undefined
        })
      : null;
  const collectionEditPayload =
    input.slug && hasCollectionMetadataOverride(mergedInput)
      ? buildCollectionEditPayload({
          current: await runStage(logger, "previewUpdate:getCurrentCollectionDetail", () =>
            deps.getCollectionDetailFromEditors(input.slug!, input.authorization)
          ),
          patch: mergedInput,
          token: await runStage(logger, "previewUpdate:getMutateToken", () => deps.getMutateToken(input.authorization))
        })
      : null;

  return {
    contractAddress: input.contractAddress,
    chainMId: input.chainMId,
    slug: input.slug ?? null,
    urls: input.slug ? buildDropUrls(input.slug) : null,
    currentSettings: currentSettingsResponse,
    currentDesign: currentDesignResponse,
    settingsPayload,
    designPayload,
    collectionEditPayload,
    summary: {
      dropID: currentSettings.dropID,
      willReadCurrentSettings: true,
      willReadCurrentDesign: shouldUpdateDesign,
      willUpdateSettings: shouldUpdateSettings,
      willUpdatePreReveal: Boolean(mergedInput.preReveal),
      willUpdateDesign: shouldUpdateDesign,
      willUpdateCollectionProfile: Boolean(collectionEditPayload),
      willUploadImages: Boolean(
        mergedInput.preReveal ??
          mergedInput.bannerFilePath ??
          (mergedInput.previewFilePaths && mergedInput.previewFilePaths.length > 0)
      ),
      patchKeys: Object.keys(mergedInput).filter((key) => {
        const value = mergedInput[key as keyof ConfigureDropFlowInput];
        return value !== undefined && !["authorization", "walletAddress", "chainMId", "contractAddress", "slug"].includes(key);
      }),
      nextSettings: settingsPayload
        ? {
            dropType: settingsPayload.dropType,
            maxSupply: settingsPayload.maxSupply,
            dropBeginTime: settingsPayload.dropBeginTime,
            mintingType: settingsPayload.mintingType,
            stageCount: settingsPayload.stagesUpdate.length
          }
        : null,
      nextDesign: designPayload
        ? {
            dropName: designPayload.dropName,
            bannerURL: designPayload.bannerURL,
            previewCount: designPayload.previewMediaExt.length,
            description: designPayload.description,
            website: designPayload.website,
            twitter: designPayload.twitter,
            instagram: designPayload.instagram,
            discord: designPayload.discord,
            telegram: designPayload.telegram,
            medium: designPayload.medium
          }
        : null,
      nextCollectionProfile: collectionEditPayload
        ? {
            externalUrl: collectionEditPayload.externalUrl ?? null,
            twitterUrl: collectionEditPayload.twitterUrl ?? null,
            instagramUrl: collectionEditPayload.instagramUrl ?? null,
            discordUrl: collectionEditPayload.discordUrl ?? null,
            telegramUrl: collectionEditPayload.telegramUrl ?? null,
            mediumUrl: collectionEditPayload.mediumUrl ?? null
          }
        : null
    }
  };
}

function mapCurrentStages(stages: GetDropSettingsStage[] | null | undefined): StageInput[] {
  if (!stages || stages.length === 0) {
    return [];
  }
  return stages.map((stage) => ({
    stageID: stage.stageID,
    stageName: stage.stageName,
    price: stage.price,
    duration: stage.duration,
    maxSupplyAtThisStage: stage.maxSupplyAtThisStage,
    maxMintedPerWallet: stage.maxMintedPerWallet,
    interval: stage.interval,
    stageMode: (stage.stageMode === 1 ? 1 : 0) as 0 | 1
  }));
}

function buildMergedSettingsPayload(input: {
  current: GetDropSettingsResponse;
  patch: ConfigureDropFlowInput;
  contractAddress: string;
}): PostDropSettingsRequest {
  const { dropType, maxSupply } = resolveDropMode({
    requestedEdition: input.patch.edition,
    requestedDropType: input.patch.dropType,
    requestedMaxSupply: input.patch.maxSupply,
    fallbackDropType: input.current.dropType ?? undefined,
    fallbackMaxSupply: input.current.maxSupply ?? DEFAULT_MAX_SUPPLY
  });
  const stages =
    input.patch.stages && input.patch.stages.length > 0
      ? assignStageIds(input.patch.stages)
      : (() => {
          const currentStages = mapCurrentStages(input.current.stages);
          return currentStages.length > 0 ? currentStages : [buildDefaultStage(maxSupply)];
        })();

  return {
    dropID: input.current.dropID ?? 0,
    chainMId: input.patch.chainMId,
    contractAddress: input.contractAddress,
    dropType,
    maxSupply,
    fee: input.current.fee ?? 0,
    feeRecipient: input.current.feeRecipient ?? "",
    mintingType: resolveMintingTypeFromPaymentToken({
      paymentToken: input.patch.paymentToken,
      availablePaymentTokens: input.patch.paymentTokens,
      fallbackMintingType: input.current.mintingType ?? 0
    }),
    dropBeginTime: input.patch.dropBeginTime ?? input.current.dropBeginTime ?? defaultDropBeginTime(),
    stagesUpdate: stages.map((stage) => ({
      stageID: stage.stageID,
      stageName: stage.stageName,
      price: stage.price,
      interval: stage.interval,
      duration: stage.duration,
      maxMintedPerWallet: stage.maxMintedPerWallet,
      maxSupplyAtThisStage: stage.maxSupplyAtThisStage,
      stageMode: stage.stageMode,
      allowListsNew: []
    }))
  };
}

function hasCurrentBanner(current: GetDropDesignResponse | null): boolean {
  return Boolean(current?.bannerURL?.trim());
}

function hasCurrentPreviewMedia(current: GetDropDesignResponse | null): boolean {
  return Boolean(
    current?.previewMediaExt?.some((item) => item.image_url?.trim() || item.animation_url?.trim()) ||
      current?.previewMedia?.some((item) => {
        if (typeof item === "string") {
          return item.trim().length > 0;
        }
        if (item && typeof item === "object") {
          const record = item as Record<string, unknown>;
          return Boolean(
            (typeof record.image_url === "string" && record.image_url.trim()) ||
              (typeof record.imageURL === "string" && record.imageURL.trim()) ||
              (typeof record.animation_url === "string" && record.animation_url.trim()) ||
              (typeof record.animationURL === "string" && record.animationURL.trim())
          );
        }
        return false;
      })
  );
}

export function resolveDesignAssetUrlsForUpdate(input: {
  currentDesign: GetDropDesignResponse | null;
  preRevealUrl?: string | null;
  explicitBannerProvided: boolean;
  explicitPreviewProvided: boolean;
  uploadedBannerUrl?: string | null;
  uploadedPreviewUrls?: string[];
}): { uploadedBannerUrl: string | null; uploadedPreviewUrls: string[] | undefined } {
  const preRevealUrl = input.preRevealUrl?.trim();
  const uploadedPreviewUrls =
    input.uploadedPreviewUrls && input.uploadedPreviewUrls.length > 0
      ? input.uploadedPreviewUrls
      : !input.explicitPreviewProvided && preRevealUrl && !hasCurrentPreviewMedia(input.currentDesign)
        ? [preRevealUrl]
        : undefined;

  return {
    uploadedBannerUrl:
      input.uploadedBannerUrl ??
      (!input.explicitBannerProvided && preRevealUrl && !hasCurrentBanner(input.currentDesign) ? preRevealUrl : null),
    uploadedPreviewUrls
  };
}

export async function configureDropFlow(input: ConfigureDropFlowInput, deps: ConfigureDropFlowDeps) {
  const logger = createWorkflowLogger();
  const auth = {
    [REDACTED_SECRET],
    walletAddress: input.walletAddress
  };
  const preRevealPath = input.preReveal;

  const currentSettingsResponse = await runStage(logger, "getCurrentSettings", () =>
    deps.getDropSettings(
      {
        chainMId: input.chainMId,
        contractAddress: input.contractAddress
      },
      auth.walletAddress
    )
  );
  const currentSettings = currentSettingsResponse.data;
  const currentDropID = currentSettings.dropID ?? 0;
  const shouldUpdateSettings = hasSettingsOverride(input);
  const shouldUpdateDesign = hasDesignOverride(input);
  const shouldUpdateCollectionMetadata = Boolean(input.slug && hasCollectionMetadataOverride(input));

  const settingsPayload = shouldUpdateSettings
    ? buildMergedSettingsPayload({
        current: currentSettings,
        patch: input,
        contractAddress: input.contractAddress
      })
    : null;
  const settingsResponse = settingsPayload
    ? await runStage(logger, "postSettings", () =>
        deps.postDropSettings(auth.authorization, settingsPayload, auth.walletAddress)
      )
    : null;
  const settingsAfterConfigure =
    settingsPayload
      ? await runStage(logger, "getSettingsAfterConfigure", () =>
          deps.getDropSettings(
            {
              chainMId: input.chainMId,
              contractAddress: input.contractAddress
            },
            auth.walletAddress
          )
        )
      : currentSettingsResponse;
  const dropID = settingsAfterConfigure.data.dropID ?? currentDropID;

  const preRevealUpload = preRevealPath
    ? await runStage(logger, "uploadPreReveal", () =>
        deps.uploadWithExistingAuthorization(
          auth,
          {
            mode: "prereveal",
            chainMId: input.chainMId,
            contractAddress: input.contractAddress,
            dropType: resolveDropMode({
              requestedEdition: input.edition,
              requestedDropType: input.dropType,
              requestedMaxSupply: input.maxSupply,
              fallbackDropType: currentSettings.dropType ?? undefined,
              fallbackMaxSupply: currentSettings.maxSupply ?? DEFAULT_MAX_SUPPLY
            }).dropType,
            filePath: preRevealPath
          },
          {
            getOssSignSingle: deps.getOssSignSingle,
            postPreReveal: deps.postPreReveal,
            uploadAsset: deps.uploadAsset
          }
        )
      )
    : null;

  if ((shouldUpdateDesign || input.bannerFilePath || (input.previewFilePaths && input.previewFilePaths.length > 0)) && !(dropID > 0)) {
    throw new Error(`dropID is missing for design update on ${input.contractAddress}`);
  }

  const currentDesignResponse =
    dropID > 0 && shouldUpdateDesign
      ? await runStage(logger, "getCurrentDesign", () =>
          deps.getDropDesign(
            {
              dropID,
              chainMId: input.chainMId,
              contractAddress: input.contractAddress
            },
            auth.walletAddress
          )
        )
      : null;
  const currentDesign = currentDesignResponse?.data ?? null;

  const assetDesignUpload =
    input.bannerFilePath ||
    (input.previewMedia && input.previewMedia.length > 0) ||
    (input.previewFilePaths && input.previewFilePaths.length > 0)
      ? await runStage(logger, "uploadDesignAssets", () => {
          const designBannerFilePath = input.bannerFilePath;
          const designPreviewFilePaths = input.previewMedia?.length
            ? input.previewMedia
            : input.previewFilePaths?.length
            ? input.previewFilePaths
            : [];
          if (!designBannerFilePath && designPreviewFilePaths.length === 0) {
            throw new Error("design asset update requires at least one banner or preview source");
          }
          return deps.uploadWithExistingAuthorization(
            auth,
            {
              mode: "design",
              chainMId: input.chainMId,
              contractAddress: input.contractAddress,
              dropID,
              dropName: input.name ?? currentDesign?.dropName ?? "",
              bannerFilePath: designBannerFilePath,
              previewFilePaths: designPreviewFilePaths
            },
            {
              getOssSignSingle: deps.getOssSignSingle,
              postPreReveal: deps.postPreReveal,
              uploadAsset: deps.uploadAsset
            }
          );
        })
      : null;

  const designAssetUrls = resolveDesignAssetUrlsForUpdate({
    currentDesign,
    preRevealUrl: preRevealUpload?.publicUrls[0],
    explicitBannerProvided: Boolean(input.bannerFilePath),
    explicitPreviewProvided: Boolean(
      (input.previewMedia && input.previewMedia.length > 0) ||
        (input.previewFilePaths && input.previewFilePaths.length > 0)
    ),
    uploadedBannerUrl: assetDesignUpload?.mode === "design" ? assetDesignUpload.payload.bannerURL : null,
    uploadedPreviewUrls:
      assetDesignUpload?.mode === "design"
        ? assetDesignUpload.payload.previewMediaExt.map((item) => item.image_url ?? "").filter(Boolean)
        : undefined
  });

  const designPayload =
    shouldUpdateDesign
      ? buildMergedDesignPayload({
          current: currentDesign,
          patch: input,
          chainMId: input.chainMId,
          contractAddress: input.contractAddress,
          dropID,
          uploadedBannerUrl: designAssetUrls.uploadedBannerUrl,
          uploadedPreviewUrls: designAssetUrls.uploadedPreviewUrls
        })
      : null;

  const designResponse =
    designPayload
      ? await runStage(logger, "postDesign", () =>
          deps.postDesign(auth.authorization, designPayload, auth.walletAddress)
        )
      : null;
  const designBannerUrl = designPayload?.bannerURL ?? null;
  const collectionEditPayload =
    shouldUpdateCollectionMetadata
      ? buildCollectionEditPayload({
          current: await runStage(logger, "getCurrentCollectionDetail", () =>
            deps.getCollectionDetailFromEditors(input.slug!, auth.authorization)
          ),
          patch: input,
          token: await runStage(logger, "getMutateToken", () => deps.getMutateToken(auth.authorization))
        })
      : null;
  const collectionEditResponse =
    collectionEditPayload
      ? await runStage(logger, "collectionEdit", () =>
          deps.collectionEdit(collectionEditPayload, auth.authorization)
        )
      : null;
  const urls = input.slug ? buildDropUrls(input.slug) : null;
  return {
    currentSettings,
    contractAddress: input.contractAddress,
    currentSettingsResponse,
    settingsPayload,
    settingsResponse,
    settingsAfterConfigure,
    dropID,
    preRevealUpload,
    currentDesignResponse,
    designPayload,
    designResponse,
    collectionEditPayload,
    collectionEditResponse,
    designUpload: assetDesignUpload,
    designBannerUrl,
    slug: input.slug ?? null,
    urls
  };
}


path: scripts/src/workflow/post-create-collection.ts
content: import { waitForCollectionContract } from "./wait-collection-contract";
import { buildCollectionEditPayload, type CollectionMetadataPatch } from "./design-payloads";
import type {
  ElementChainCollectionSummary,
  ElementCollectionDetailFromEditors,
  ElementCollectionEditInput,
  ElementCollectionSummary
} from "../types";

export interface PostCreateCollectionInput {
  chainMId: number;
  contractAddress: string;
  authorization?: string;
  imageFilePath?: string;
  collectionMetadata?: CollectionMetadataPatch;
  pollIntervalMs?: number;
  timeoutMs?: number;
}

export interface PostCreateCollectionDeps {
  getCollectionContract: (input: {
    address: string;
    blockChain: {
      chain: string;
      chainId: string;
    };
  }) => Promise<ElementChainCollectionSummary | null>;
  getMutateToken: (authorization?: string) => Promise<string>;
  getCollectionDetailFromEditors: (
    slug: string,
    authorization?: string
  ) => Promise<ElementCollectionDetailFromEditors>;
  collectionEdit: (input: ElementCollectionEditInput, authorization?: string) => Promise<ElementCollectionSummary>;
  sleep?: (ms: number) => Promise<void>;
  now?: () => number;
  logger?: (message: string, meta?: Record<string, unknown>) => void;
}

export async function postCreateCollectionFlow(
  input: PostCreateCollectionInput,
  deps: PostCreateCollectionDeps
) {
  const collectionContract = await waitForCollectionContract(
    {
      chainMId: input.chainMId,
      contractAddress: input.contractAddress,
      pollIntervalMs: input.pollIntervalMs,
      timeoutMs: input.timeoutMs
    },
    {
      getCollectionContract: deps.getCollectionContract,
      sleep: deps.sleep,
      now: deps.now,
      logger: deps.logger
    }
  );

  deps.logger?.("postCreateCollection:get mutate token", {
    contractAddress: input.contractAddress
  });
  const mutateToken = await deps.getMutateToken(input.authorization);
  deps.logger?.("postCreateCollection:get detail", {
    slug: collectionContract.collection.slug
  });
  const collectionDetail = await deps.getCollectionDetailFromEditors(
    collectionContract.collection.slug,
    input.authorization
  );
  deps.logger?.("postCreateCollection:collection edit", {
    collectionId: collectionContract.collection.id,
    slug: collectionContract.collection.slug
  });
  const collectionEdit = await deps.collectionEdit(
    buildCollectionEditPayload({
      current: collectionDetail,
      patch: input.collectionMetadata ?? {},
      [REDACTED_SECRET],
      imageFilePath: input.imageFilePath
    }),
    input.authorization
  );

  return {
    collectionContract,
    mutateToken,
    collectionDetail,
    collectionEdit
  };
}


path: scripts/src/workflow/create-token.ts
content: import { buildElementAuthorization, deriveWalletAddress } from "../auth/jwt";
import { getRequiredWalletPrivateKey } from "../env";
import { getRpcUrlForChainMId } from "../network/rpc";
import { sendEncodedTransaction } from "../network/transaction";
import { normalizeCreateDropInput } from "../schema";
import type {
  CreateDropInput,
  CreateTokenPreflight,
  ElementApiResponse,
  EncodedTransaction,
  ExecutedTransactionResult
} from "../types";

export interface CreateTokenFlowDeps {
  deriveAddress: typeof deriveWalletAddress;
  resolveRpcUrl: typeof getRpcUrlForChainMId;
  createAuthorization: (input: {
    privateKey: string;
    walletAddress?: string;
    chainMId: number;
  }) => Promise<{
    authorization: string;
    nonce: string;
    message: string;
    identity: {
      address: string;
      blockChain: {
        chain: string;
        chainId: string;
      };
    };
  }>;
  postCreateToken: (
    authorization: string,
    body: { chainMId: number; name: string; symbol: string },
    walletAddress?: string
  ) => Promise<ElementApiResponse<EncodedTransaction>>;
  sendTransaction: typeof sendEncodedTransaction;
  logger?: (message: string, meta?: Record<string, unknown>) => void;
}

export async function createTokenFlow(input: CreateDropInput, deps: CreateTokenFlowDeps) {
  const parsed = normalizeCreateDropInput(input);
  const privateKey = getRequiredWalletPrivateKey();
  const walletAddress = (await deps.deriveAddress({ privateKey })).toLowerCase();
  const rpcUrl = await deps.resolveRpcUrl(parsed.chainMId);
  const auth = await deps.createAuthorization({
    privateKey,
    walletAddress,
    chainMId: parsed.chainMId
  });
  const response = await deps.postCreateToken(
    auth.authorization,
    {
      chainMId: parsed.chainMId,
      name: parsed.name,
      symbol: parsed.symbol
    },
    walletAddress
  );

  if (response.code !== 0) {
    throw new Error(`createToken failed: ${response.message}`);
  }
  const transaction = await deps.sendTransaction({
    rpcUrl,
    transaction: response.data,
    logger: deps.logger
  });

  return {
    ...response,
    transaction: transaction satisfies ExecutedTransactionResult,
    preflight: {
      walletAddress,
      rpcUrl,
      [REDACTED_SECRET],
      nonce: auth.nonce,
      loginMessage: auth.message,
      identity: auth.identity
    } satisfies CreateTokenPreflight
  };
}


path: scripts/src/workflow/publish-drop.ts
content: import type {
  CallbackUpdateProjectConfigRequest,
  ElementApiResponse,
  EncodedTransaction,
  ExecutedTransactionResult,
  GetDropSettingsResponse,
  GetPreRevealIPFSResponse,
  GetTempURLResponse,
  PostDropSettingsRequest,
  StageMode,
  SetProjectConfigRequest
} from "../types";

export interface WaitForPreRevealIPFSInput {
  chainMId: number;
  contractAddress: string;
  dropID: number;
  walletAddress: string;
  pollIntervalMs?: number;
  timeoutMs?: number;
}

export interface WaitForPreRevealIPFSDeps {
  getPreRevealIPFS: (
    query: { chainMId: number; contractAddress: string; dropID: number },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetPreRevealIPFSResponse>>;
  sleep?: (ms: number) => Promise<void>;
  now?: () => number;
  logger?: (message: string, meta?: Record<string, unknown>) => void;
}

export interface WaitForPublishedInput {
  chainMId: number;
  contractAddress: string;
  dropID: number;
  walletAddress: string;
  expectedPublished?: number;
  expectedIsPaused?: boolean;
  pollIntervalMs?: number;
  timeoutMs?: number;
}

export interface WaitForPublishedDeps {
  postCallbackUpdateProjectConfig: (
    body: CallbackUpdateProjectConfigRequest
  ) => Promise<ElementApiResponse<null>>;
  getDropSettings: (
    query: { chainMId: number; contractAddress: string },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetDropSettingsResponse>>;
  sleep?: (ms: number) => Promise<void>;
  now?: () => number;
  logger?: (message: string, meta?: Record<string, unknown>) => void;
}

export interface ResolvePublishPreRevealInput {
  authorization: string;
  walletAddress: string;
  chainMId: number;
  contractAddress: string;
  dropID: number;
  pollIntervalMs?: number;
  timeoutMs?: number;
}

export interface ResolvePublishPreRevealDeps {
  getTempURL: (
    authorization: string,
    query: { chainMId: number; contractAddress: string; dropID: number; page: number; pageSize: number },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetTempURLResponse>>;
  getPreRevealIPFS: (
    query: { chainMId: number; contractAddress: string; dropID: number },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetPreRevealIPFSResponse>>;
  sleep?: (ms: number) => Promise<void>;
  now?: () => number;
  logger?: (message: string, meta?: Record<string, unknown>) => void;
}

export interface PublishDropFlowInput {
  authorization: string;
  walletAddress: string;
  rpcUrl: string;
  chainMId: number;
  contractAddress: string;
  dropID: number;
  isPaused?: boolean;
  getSettingsPollIntervalMs?: number;
  getSettingsTimeoutMs?: number;
  preRevealIPFSPollIntervalMs?: number;
  preRevealIPFSTimeoutMs?: number;
}

export interface PublishDropFlowDeps {
  getDropSettings: (
    query: { chainMId: number; contractAddress: string },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetDropSettingsResponse>>;
  getPreRevealIPFS: (
    query: { chainMId: number; contractAddress: string; dropID: number },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetPreRevealIPFSResponse>>;
  getTempURL: (
    authorization: string,
    query: { chainMId: number; contractAddress: string; dropID: number; page: number; pageSize: number },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetTempURLResponse>>;
  postDropSettings: (
    authorization: string,
    body: PostDropSettingsRequest,
    walletAddress: string
  ) => Promise<ElementApiResponse<null>>;
  postSetProjectConfig: (
    authorization: string,
    body: SetProjectConfigRequest,
    walletAddress: string
  ) => Promise<ElementApiResponse<EncodedTransaction>>;
  sendTransaction: (input: {
    rpcUrl: string;
    transaction: EncodedTransaction;
    waitConfirmations?: number;
    logger?: (message: string, meta?: Record<string, unknown>) => void;
  }) => Promise<ExecutedTransactionResult>;
  postCallbackUpdateProjectConfig: (
    body: CallbackUpdateProjectConfigRequest
  ) => Promise<ElementApiResponse<null>>;
  sleep?: (ms: number) => Promise<void>;
  now?: () => number;
  logger?: (message: string, meta?: Record<string, unknown>) => void;
}

async function defaultSleep(ms: number): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, ms));
}

export function extractPreRevealImageUrl(tempURL: GetTempURLResponse | null | undefined): string {
  return tempURL?.preReveal?.image_url?.trim() ?? tempURL?.preReveal?.imageURL?.trim() ?? "";
}

export function extractPreRevealAnimationUrl(tempURL: GetTempURLResponse | null | undefined): string {
  return tempURL?.preReveal?.animation_url?.trim() ?? tempURL?.preReveal?.animationURL?.trim() ?? "";
}

export function assertPreRevealImageUrlExists(input: {
  contractAddress: string;
  preRevealImageUrl: string;
}) {
  if (!input.preRevealImageUrl) {
    throw new Error(
      `publish requires a configured prereveal image URL for ${input.contractAddress}; ` +
        `the current prereveal media resource is empty, so rerun upload/preReveal and retry publish`
    );
  }
}

export function buildPostDropSettingsPayloadFromSettings(input: {
  settings: GetDropSettingsResponse;
  chainMId: number;
  contractAddress: string;
}): PostDropSettingsRequest {
  return {
    dropID: input.settings.dropID,
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    dropType: input.settings.dropType,
    maxSupply: input.settings.maxSupply ?? 0,
    fee: input.settings.fee ?? 0,
    feeRecipient: input.settings.feeRecipient ?? "",
    mintingType: input.settings.mintingType,
    dropBeginTime: input.settings.dropBeginTime ?? 0,
    stagesUpdate: input.settings.stages.map((stage) => ({
      stageID: stage.stageID,
      stageName: stage.stageName,
      price: stage.price,
      interval: stage.interval,
      duration: stage.duration,
      maxMintedPerWallet: stage.maxMintedPerWallet,
      maxSupplyAtThisStage: stage.maxSupplyAtThisStage,
      stageMode: stage.stageMode as StageMode | null,
      allowListsNew: []
    }))
  };
}

export function buildSetProjectConfigPayload(input: {
  settings: GetDropSettingsResponse;
  chainMId: number;
  contractAddress: string;
  preRevealIPFS: string;
  isPaused?: boolean;
}): SetProjectConfigRequest {
  return {
    isPaused: input.isPaused ?? false,
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    nftAddress: input.contractAddress,
    dropBeginTime: input.settings.dropBeginTime ?? 0,
    maxSupply: String(input.settings.maxSupply ?? 0),
    baseURI: input.preRevealIPFS,
    mintingType: input.settings.mintingType,
    stages: input.settings.stages.map((stage) => ({
      stageID: stage.stageID,
      stageName: stage.stageName,
      stageMode: stage.stageMode,
      price: stage.price,
      address: stage.address ?? "0x0000000000000000000000000000000000000000",
      duration: stage.duration,
      maxSupplyAtThisStage: stage.maxSupplyAtThisStage,
      maxMintedPerWallet: stage.maxMintedPerWallet,
      interval: stage.interval,
      enableCallFromContract: stage.enableCallFromContract,
      enableMintToOther: stage.enableMintToOther
    }))
  };
}

export async function waitForPreRevealIPFS(
  input: WaitForPreRevealIPFSInput,
  deps: WaitForPreRevealIPFSDeps
) {
  const pollIntervalMs = input.pollIntervalMs ?? 5_000;
  const timeoutMs = input.timeoutMs ?? 10 * 60 * 1000;
  const sleep = deps.sleep ?? defaultSleep;
  const now = deps.now ?? Date.now;
  const startedAt = now();
  let attempts = 0;

  while (true) {
    attempts += 1;
    deps.logger?.("preRevealIPFS:poll", {
      attempt: attempts,
      dropID: input.dropID,
      elapsedMs: now() - startedAt
    });
    const response = await deps.getPreRevealIPFS(
      {
        chainMId: input.chainMId,
        contractAddress: input.contractAddress,
        dropID: input.dropID
      },
      input.walletAddress
    );
    const preRevealIPFS = response.data?.preRevealIPFS ?? "";
    if (preRevealIPFS) {
      deps.logger?.("preRevealIPFS:resolved", {
        attempt: attempts,
        dropID: input.dropID,
        preRevealIPFS,
        elapsedMs: now() - startedAt
      });
      return {
        preRevealIPFS,
        attempts,
        elapsedMs: now() - startedAt
      };
    }
    if (now() - startedAt >= timeoutMs) {
      throw new Error(
        `preRevealIPFS timed out after ${attempts} attempts (${timeoutMs}ms) for drop ${input.dropID}; ` +
          `if prereveal OSS to IPFS promotion is stuck, run upload/preReveal again and retry publish`
      );
    }
    await sleep(pollIntervalMs);
  }
}

export async function resolvePublishPreRevealIPFS(
  input: ResolvePublishPreRevealInput,
  deps: ResolvePublishPreRevealDeps
) {
  deps.logger?.("publish:prereveal:read-temp-url", {
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    dropID: input.dropID
  });
  const tempURLResponse = await deps.getTempURL(
    input.authorization,
    {
      chainMId: input.chainMId,
      contractAddress: input.contractAddress,
      dropID: input.dropID,
      page: 1,
      pageSize: 1
    },
    input.walletAddress
  );
  assertPreRevealImageUrlExists({
    contractAddress: input.contractAddress,
    preRevealImageUrl: extractPreRevealImageUrl(tempURLResponse.data)
  });

  deps.logger?.("publish:prereveal:trigger-and-poll-ipfs", {
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    dropID: input.dropID
  });
  const preRevealIPFS = await waitForPreRevealIPFS(
    {
      chainMId: input.chainMId,
      contractAddress: input.contractAddress,
      dropID: input.dropID,
      walletAddress: input.walletAddress,
      pollIntervalMs: input.pollIntervalMs,
      timeoutMs: input.timeoutMs
    },
    {
      getPreRevealIPFS: deps.getPreRevealIPFS,
      sleep: deps.sleep,
      now: deps.now,
      logger: deps.logger
    }
  );

  return {
    tempURL: tempURLResponse.data,
    preRevealIPFS
  };
}

export async function waitForPublishedSettings(
  input: WaitForPublishedInput,
  deps: WaitForPublishedDeps
) {
  const pollIntervalMs = input.pollIntervalMs ?? 5_000;
  const timeoutMs = input.timeoutMs ?? 10 * 60 * 1000;
  const expectedPublished = input.expectedPublished ?? 1;
  const expectedIsPaused = input.expectedIsPaused;
  const sleep = deps.sleep ?? defaultSleep;
  const now = deps.now ?? Date.now;
  const startedAt = now();
  let attempts = 0;

  while (true) {
    attempts += 1;
    deps.logger?.("publishedSettings:callback", {
      attempt: attempts,
      dropID: input.dropID,
      contractAddress: input.contractAddress,
      elapsedMs: now() - startedAt
    });
    const callbackResponse = await deps.postCallbackUpdateProjectConfig({
      dropId: input.dropID,
      chainMId: input.chainMId,
      contractAddress: input.contractAddress
    });
    deps.logger?.("publishedSettings:poll", {
      attempt: attempts,
      dropID: input.dropID,
      contractAddress: input.contractAddress,
      callbackCode: callbackResponse.code,
      elapsedMs: now() - startedAt
    });
    const response = await deps.getDropSettings(
      {
        chainMId: input.chainMId,
        contractAddress: input.contractAddress
      },
      input.walletAddress
    );
    const publishedMatches = (response.data?.published ?? 0) === expectedPublished;
    const pausedMatches =
      expectedIsPaused === undefined ? true : (response.data?.isPaused ?? false) === expectedIsPaused;
    if (publishedMatches && pausedMatches) {
      const finalSettingsResponse = await deps.getDropSettings(
        {
          chainMId: input.chainMId,
          contractAddress: input.contractAddress
        },
        input.walletAddress
      );
      deps.logger?.("publishedSettings:resolved", {
        attempt: attempts,
        dropID: input.dropID,
        contractAddress: input.contractAddress,
        expectedPublished,
        expectedIsPaused,
        published: finalSettingsResponse.data?.published ?? 0,
        isPaused: finalSettingsResponse.data?.isPaused ?? false,
        elapsedMs: now() - startedAt
      });
      return {
        settings: finalSettingsResponse.data,
        callbackResponse,
        attempts,
        elapsedMs: now() - startedAt
      };
    }
    if (now() - startedAt >= timeoutMs) {
      throw new Error(
          `published settings timed out after ${attempts} attempts (${timeoutMs}ms) for ${input.contractAddress}; ` +
          `expected published=${expectedPublished}` +
          `${expectedIsPaused === undefined ? "" : ` and isPaused=${expectedIsPaused}`}, ` +
          `got published=${response.data?.published ?? 0} and isPaused=${response.data?.isPaused ?? false}`
      );
    }
    await sleep(pollIntervalMs);
  }
}

export async function publishDropFlow(
  input: PublishDropFlowInput,
  deps: PublishDropFlowDeps
) {
  deps.logger?.("publish:start", {
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    dropID: input.dropID
  });
  const settingsBeforePublishResponse = await deps.getDropSettings(
    {
      chainMId: input.chainMId,
      contractAddress: input.contractAddress
    },
    input.walletAddress
  );
  const settingsBeforePublish = settingsBeforePublishResponse.data;
  const preRevealResolution = await resolvePublishPreRevealIPFS(
    {
      [REDACTED_SECRET],
      chainMId: input.chainMId,
      contractAddress: input.contractAddress,
      dropID: input.dropID,
      walletAddress: input.walletAddress,
      pollIntervalMs: input.preRevealIPFSPollIntervalMs,
      timeoutMs: input.preRevealIPFSTimeoutMs
    },
    {
      getTempURL: deps.getTempURL,
      getPreRevealIPFS: deps.getPreRevealIPFS,
      sleep: deps.sleep,
      now: deps.now,
      logger: deps.logger
    }
  );

  const postSettingsPayload = buildPostDropSettingsPayloadFromSettings({
    settings: settingsBeforePublish,
    chainMId: input.chainMId,
    contractAddress: input.contractAddress
  });
  const postSettingsResponse = await deps.postDropSettings(
    input.authorization,
    postSettingsPayload,
    input.walletAddress
  );

  const setProjectConfigPayload = buildSetProjectConfigPayload({
    settings: settingsBeforePublish,
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    preRevealIPFS: preRevealResolution.preRevealIPFS.preRevealIPFS,
    isPaused: input.isPaused
  });
  const setProjectConfigResponse = await deps.postSetProjectConfig(
    input.authorization,
    setProjectConfigPayload,
    input.walletAddress
  );
  if (setProjectConfigResponse.code !== 0) {
    throw new Error(`setProjectConfig failed: ${setProjectConfigResponse.message}`);
  }
  const setProjectConfigTransaction = await deps.sendTransaction({
    rpcUrl: input.rpcUrl,
    transaction: setProjectConfigResponse.data,
    logger: deps.logger
  });

  const callbackPayload = {
    dropId: input.dropID,
    chainMId: input.chainMId,
    contractAddress: input.contractAddress
  };

  const published = await waitForPublishedSettings(
    {
      chainMId: input.chainMId,
      contractAddress: input.contractAddress,
      dropID: input.dropID,
      walletAddress: input.walletAddress,
      expectedPublished: 1,
      expectedIsPaused: setProjectConfigPayload.isPaused,
      pollIntervalMs: input.getSettingsPollIntervalMs,
      timeoutMs: input.getSettingsTimeoutMs
    },
    {
      postCallbackUpdateProjectConfig: deps.postCallbackUpdateProjectConfig,
      getDropSettings: deps.getDropSettings,
      sleep: deps.sleep,
      now: deps.now,
      logger: deps.logger
    }
  );

  return {
    settingsBeforePublish,
    tempURLBeforePublish: preRevealResolution.tempURL,
    preRevealIPFS: preRevealResolution.preRevealIPFS,
    postSettingsPayload,
    postSettingsResponse,
    setProjectConfigPayload,
    setProjectConfigResponse,
    setProjectConfigTransaction,
    callbackPayload,
    published
  };
}


path: scripts/src/workflow/uploads.ts
content: import { createCuimpHttp } from "cuimp";
import type { CuimpInstance } from "cuimp";
import { DEFAULT_OSS_UPLOAD_TIMEOUT_MS } from "../config";
import {
  assertSafeUploadFileName,
  inferUploadExtension,
  validateLocalImageUpload
} from "../security/upload-guard";
import type {
  DesignUploadPayload,
  DropType,
  ElementApiResponse,
  OssSignedPostData,
  PreRevealUploadPayload
} from "../types";

export interface UploadedAssetRef {
  sourcePath: string;
  fileName: string;
  objectKey: string;
  publicUrl: string;
}

export interface UploadAuthContext {
  authorization: string;
  walletAddress: string;
}

export interface PreRevealUploadInput {
  mode: "prereveal";
  chainMId: number;
  contractAddress: string;
  dropType: DropType;
  filePath: string;
}

export interface DesignUploadInput {
  mode: "design";
  chainMId: number;
  contractAddress: string;
  dropID: number;
  dropName: string;
  bannerFilePath?: string;
  previewFilePaths: string[];
}

export type UploadInput = PreRevealUploadInput | DesignUploadInput;

export interface UploadDeps {
  getOssSignSingle: (
    authorization: string,
    query: { chainMId: number; contractAddress: string; mediaType: "prereveal" | "design" },
    walletAddress?: string
  ) => Promise<ElementApiResponse<OssSignedPostData>>;
  postPreReveal: (
    authorization: string,
    body: PreRevealUploadPayload,
    walletAddress?: string
  ) => Promise<ElementApiResponse<null>>;
  uploadAsset: (input: {
    filePath: string;
    fileName: string;
    oss: OssSignedPostData;
  }) => Promise<UploadedAssetRef>;
}

export interface CuimpUploadDeps {
  createClient?: () => Pick<CuimpInstance, "request">;
}

function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const timeoutHandle = setTimeout(() => {
      reject(new Error(`${label} timed out after ${ms}ms`));
    }, ms);

    promise.then(
      (value) => {
        clearTimeout(timeoutHandle);
        resolve(value);
      },
      (error) => {
        clearTimeout(timeoutHandle);
        reject(error);
      }
    );
  });
}

export function convertOssUrlToPublicUrl(url: string): string {
  return url
    .replace(
      "https://element-master.oss-cn-hongkong.aliyuncs.com/creator-studio/",
      "https://c.nfte.ai/creator-studio/"
    )
    .replace("https://ele-lpd.nfte.ai/", "https://c.nfte.ai/");
}

export function buildPreRevealUploadPayload(input: {
  chainMId: number;
  contractAddress: string;
  dropType: DropType;
  publicUrl: string;
}): PreRevealUploadPayload {
  return {
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    dropType: input.dropType,
    preRevealExt: {
      image_url: input.publicUrl,
      animation_url: ""
    }
  };
}

export function buildDesignUploadPayload(input: {
  chainMId: number;
  contractAddress: string;
  dropID: number;
  dropName: string;
  bannerUrl?: string;
  previewUrls: string[];
}): DesignUploadPayload {
  return {
    dropID: input.dropID,
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    dropName: input.dropName,
    bannerURL: input.bannerUrl ?? "",
    previewMediaExt: input.previewUrls.map((url) => ({
      image_url: url,
      animation_url: ""
    })),
    dropFeaturedImage: "",
    description: "",
    website: "",
    twitter: "",
    instagram: "",
    discord: "",
    telegram: "",
    medium: "",
    detailsUpdate: []
  };
}

export async function uploadAssetGroupWithExistingAuthorization(input: {
  auth: UploadAuthContext;
  chainMId: number;
  contractAddress: string;
  mediaType: "prereveal" | "design";
  files: Array<{
    filePath: string;
    fileName: string;
  }>;
}, deps: Pick<UploadDeps, "getOssSignSingle" | "uploadAsset">): Promise<UploadedAssetRef[]> {
  const uploads: UploadedAssetRef[] = [];

  for (const file of input.files) {
    const sign = await deps.getOssSignSingle(
      input.auth.authorization,
      {
        chainMId: input.chainMId,
        contractAddress: input.contractAddress,
        mediaType: input.mediaType
      },
      input.auth.walletAddress
    );
    uploads.push(
      await deps.uploadAsset({
        filePath: file.filePath,
        fileName: file.fileName,
        oss: sign.data
      })
    );
  }

  return uploads;
}

export async function uploadSingleAsset(input: {
  filePath: string;
  fileName: string;
  oss: OssSignedPostData;
}, deps: CuimpUploadDeps = {}): Promise<UploadedAssetRef> {
  return uploadAssetWithCuimp(input, deps);
}

export async function uploadAssetWithCuimp(
  input: {
    filePath: string;
    fileName: string;
    oss: OssSignedPostData;
  },
  deps: CuimpUploadDeps = {}
): Promise<UploadedAssetRef> {
  assertSafeUploadFileName(input.fileName);
  const validated = await validateLocalImageUpload(input.filePath);
  const objectKey = `${input.oss.dir.replace(/\/$/, "")}/${input.fileName}`;
  const mimeType = validated.mimeType;
  const extraCurlArgs = [
    "-F",
    `name=${input.fileName}`,
    "-F",
    `key=${objectKey}`,
    "-F",
    "success_action_status=200",
    "-F",
    `policy=${input.oss.policy}`
  ];

  if (input.oss.x_oss_credential && input.oss.x_oss_date) {
    extraCurlArgs.push("-F", `x-oss-signature=${input.oss.signature}`);
    extraCurlArgs.push("-F", [REDACTED_SECRET]);
    extraCurlArgs.push("-F", `x-oss-credential=${input.oss.x_oss_credential}`);
    extraCurlArgs.push("-F", `x-oss-date=${input.oss.x_oss_date}`);
    if (input.oss.security_token) {
      extraCurlArgs.push("-F", `x-oss-security-[REDACTED_SECRET]`);
    }
    if (input.oss.callback) {
      extraCurlArgs.push("-F", `callback=${input.oss.callback}`);
    }
  } else {
    extraCurlArgs.push("-F", `OSSAccessKeyId=${input.oss.accessid}`);
    extraCurlArgs.push("-F", `signature=${input.oss.signature}`);
  }

  extraCurlArgs.push(
    "-F",
    `file=@${validated.realPath};type=${mimeType};filename=${input.fileName}`
  );

  const client =
    (deps.createClient ??
      (() => createCuimpHttp()))();
  const response = await withTimeout(
    client.request({
      url: input.oss.host,
      method: "POST",
      headers: {
        Accept: "*/*",
        Referer: "https://element.market/",
        Origin: "https://element.market"
      },
      extraCurlArgs
    }),
    DEFAULT_OSS_UPLOAD_TIMEOUT_MS,
    `OSS upload request to ${input.oss.host}`
  );
  const responseRecord = response as unknown as Record<string, unknown>;
  const responseBody =
    "data" in responseRecord
      ? responseRecord.data
      : "body" in responseRecord
        ? responseRecord.body
        : response;

  if (response.status !== 200) {
    console.error(
      `[element-drop] external response ${JSON.stringify({
        method: "POST",
        url: input.oss.host,
        status: response.status,
        body: responseBody
      })}`
    );
    throw new Error(
      `OSS upload failed with status ${response.status}: ${typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody)}`
    );
  }

  const publicUrl = convertOssUrlToPublicUrl(`${input.oss.host}/${objectKey}`);
  return {
    sourcePath: validated.realPath,
    fileName: input.fileName,
    objectKey,
    publicUrl
  };
}

function inferExtension(filePath: string): string {
  return inferUploadExtension(filePath);
}

export async function uploadWithExistingAuthorization(
  auth: UploadAuthContext,
  input: UploadInput,
  deps: UploadDeps
) {
  if (input.mode === "prereveal") {
    const [asset] = await uploadAssetGroupWithExistingAuthorization(
      {
        auth,
        chainMId: input.chainMId,
        contractAddress: input.contractAddress,
        mediaType: "prereveal",
        files: [
          {
            filePath: input.filePath,
            fileName: `pre-reveal${inferExtension(input.filePath)}`
          }
        ]
      },
      deps
    );

    const payload = buildPreRevealUploadPayload({
      chainMId: input.chainMId,
      contractAddress: input.contractAddress,
      dropType: input.dropType,
      publicUrl: asset.publicUrl
    });
    const response = await deps.postPreReveal(auth.authorization, payload, auth.walletAddress);
    return {
      mode: "prereveal" as const,
      uploads: [asset],
      publicUrls: [asset.publicUrl],
      payload,
      response
    };
  }

  const uploads = await uploadAssetGroupWithExistingAuthorization(
    {
      auth,
      chainMId: input.chainMId,
      contractAddress: input.contractAddress,
      mediaType: "design",
      files: [
        ...(input.bannerFilePath
          ? [
              {
                filePath: input.bannerFilePath,
                fileName: `banner${inferExtension(input.bannerFilePath)}`
              }
            ]
          : []),
        ...input.previewFilePaths.map((filePath, index) => ({
          filePath,
          fileName: `preview-${index + 1}${inferExtension(filePath)}`
        }))
      ]
    },
    deps
  );
  const bannerUpload = input.bannerFilePath ? uploads[0] : null;
  const previewUploads = input.bannerFilePath ? uploads.slice(1) : uploads;

  const payload = buildDesignUploadPayload({
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    dropID: input.dropID,
    dropName: input.dropName,
    bannerUrl: bannerUpload?.publicUrl,
    previewUrls: previewUploads.map((item) => item.publicUrl)
  });
  return {
    mode: "design" as const,
    uploads,
    publicUrls: uploads.map((item) => item.publicUrl),
    payload
  };
}


path: scripts/src/workflow/design-payloads.ts
content: import type {
  DesignUploadPayload,
  ElementCollectionDetailFromEditors,
  ElementCollectionEditInput,
  GetDropDesignResponse
} from "../types";
import { normalizeSocialLinkSuffix, normalizeWebsiteUrl } from "./links";

export interface CollectionMetadataPatch {
  description?: string;
  website?: string;
  twitter?: string;
  instagram?: string;
  discord?: string;
  telegram?: string;
  medium?: string;
}

export interface DesignPayloadPatch extends CollectionMetadataPatch {
  name?: string;
  preReveal?: string;
  previewMedia?: string[];
  bannerFilePath?: string;
  previewFilePaths?: string[];
  dropFeaturedImage?: string;
}

const EMPTY_DESIGN: GetDropDesignResponse = {
  dropID: 0,
  dropName: "",
  bannerURL: "",
  previewMedia: [],
  previewMediaExt: [],
  description: "",
  website: "",
  twitter: "",
  instagram: "",
  discord: "",
  telegram: "",
  medium: "",
  dropFeaturedImage: "",
  details: []
};

export function hasCollectionMetadataOverride(input: CollectionMetadataPatch): boolean {
  return Boolean(
    input.description !== undefined ||
      input.website !== undefined ||
      input.twitter !== undefined ||
      input.instagram !== undefined ||
      input.discord !== undefined ||
      input.telegram !== undefined ||
      input.medium !== undefined
  );
}

export function hasDesignOverride(input: DesignPayloadPatch): boolean {
  return Boolean(
    input.preReveal !== undefined ||
      input.previewMedia !== undefined ||
      input.bannerFilePath ||
      (input.previewFilePaths && input.previewFilePaths.length > 0) ||
      input.name !== undefined ||
      input.description !== undefined ||
      input.dropFeaturedImage !== undefined
  );
}

export function buildCollectionEditPayload(input: {
  current: ElementCollectionDetailFromEditors;
  patch: CollectionMetadataPatch;
  [REDACTED_SECRET];
  imageFilePath?: string;
}): ElementCollectionEditInput {
  return {
    collectionId: input.current.id,
    [REDACTED_SECRET],
    imageFilePath: input.imageFilePath,
    image: input.imageFilePath ? null : undefined,
    description: input.patch.description ?? input.current.description ?? undefined,
    externalUrl: normalizeWebsiteUrl(input.patch.website ?? input.current.externalUrl) || undefined,
    twitterUrl: normalizeSocialLinkSuffix("twitter", input.patch.twitter ?? input.current.twitterUrl) || undefined,
    instagramUrl:
      normalizeSocialLinkSuffix("instagram", input.patch.instagram ?? input.current.instagramUrl) || undefined,
    discordUrl: normalizeSocialLinkSuffix("discord", input.patch.discord ?? input.current.discordUrl) || undefined,
    telegramUrl:
      normalizeSocialLinkSuffix("telegram", input.patch.telegram ?? input.current.telegramUrl) || undefined,
    mediumUrl: normalizeSocialLinkSuffix("medium", input.patch.medium ?? input.current.mediumUrl) || undefined,
    categories: input.current.categories.map((item) => item.id),
    paymentTokens: input.current.paymentTokens.map((item) => item.id)
  };
}

export function buildMergedDesignPayload(input: {
  current: GetDropDesignResponse | null;
  patch: DesignPayloadPatch;
  chainMId: number;
  contractAddress: string;
  dropID: number;
  uploadedBannerUrl?: string | null;
  uploadedPreviewUrls?: string[];
}): DesignUploadPayload {
  const current = input.current ?? EMPTY_DESIGN;
  return {
    dropID: input.dropID,
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    dropName: input.patch.name ?? current.dropName,
    bannerURL: input.uploadedBannerUrl ?? current.bannerURL ?? "",
    previewMediaExt: (() => {
      if (input.uploadedPreviewUrls && input.uploadedPreviewUrls.length > 0) {
        return input.uploadedPreviewUrls.map((url) => ({
          image_url: url,
          animation_url: ""
        }));
      }
      return current.previewMediaExt.map((item) => ({
        image_url: item.image_url ?? "",
        animation_url: item.animation_url ?? ""
      }));
    })(),
    dropFeaturedImage: input.patch.dropFeaturedImage ?? current.dropFeaturedImage ?? "",
    description: input.patch.description ?? current.description ?? "",
    website: normalizeWebsiteUrl(input.patch.website ?? current.website),
    twitter: normalizeSocialLinkSuffix("twitter", input.patch.twitter ?? current.twitter),
    instagram: normalizeSocialLinkSuffix("instagram", input.patch.instagram ?? current.instagram),
    discord: normalizeSocialLinkSuffix("discord", input.patch.discord ?? current.discord),
    telegram: normalizeSocialLinkSuffix("telegram", input.patch.telegram ?? current.telegram),
    medium: normalizeSocialLinkSuffix("medium", input.patch.medium ?? current.medium),
    detailsUpdate: current.details ?? []
  };
}

export function buildDisplayDesignAfterUpdate(input: {
  current: GetDropDesignResponse | null;
  designPayload: DesignUploadPayload | null;
  collectionEditPayload: ElementCollectionEditInput | null;
}) {
  const hasAnyUpdate = Boolean(input.current || input.designPayload || input.collectionEditPayload);
  if (!hasAnyUpdate) {
    return null;
  }

  const current = input.current ?? EMPTY_DESIGN;
  const design = input.designPayload;
  const collection = input.collectionEditPayload;

  return {
    bannerURL: design?.bannerURL ?? current.bannerURL ?? "",
    previewMediaExt: design?.previewMediaExt ?? current.previewMediaExt ?? [],
    description: design?.description ?? current.description ?? "",
    website: collection?.externalUrl ?? design?.website ?? current.website ?? "",
    twitter: collection?.twitterUrl ?? design?.twitter ?? current.twitter ?? "",
    instagram: collection?.instagramUrl ?? design?.instagram ?? current.instagram ?? "",
    discord: collection?.discordUrl ?? design?.discord ?? current.discord ?? "",
    telegram: collection?.telegramUrl ?? design?.telegram ?? current.telegram ?? "",
    medium: collection?.mediumUrl ?? design?.medium ?? current.medium ?? "",
    dropFeaturedImage: design?.dropFeaturedImage ?? current.dropFeaturedImage ?? ""
  };
}


path: scripts/src/workflow/preview-drop.ts
content: import { buildDropUrls, createWorkflowLogger, runStage } from "./create-drop";
import type {
  ElementApiResponse,
  GetDropDesignResponse,
  GetDropSettingsResponse,
  GetTempURLResponse
} from "../types";

export interface PreviewDropFlowInput {
  authorization: string;
  walletAddress: string;
  chainMId: number;
  contractAddress: string;
  slug?: string;
  page?: number;
  pageSize?: number;
}

export interface PreviewDropFlowDeps {
  getDropSettings: (
    query: { chainMId: number; contractAddress: string },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetDropSettingsResponse>>;
  getDropDesign: (
    query: { dropID: number; chainMId: number; contractAddress: string },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetDropDesignResponse>>;
  getTempURL: (
    authorization: string,
    query: { chainMId: number; contractAddress: string; dropID: number; page: number; pageSize: number },
    walletAddress: string
  ) => Promise<ElementApiResponse<GetTempURLResponse>>;
}

function summarizePublishState(input: { published?: unknown; isPaused?: unknown }) {
  if (input.published !== 1) {
    return {
      status: "draft",
      label: "draft"
    };
  }
  if (input.isPaused === true) {
    return {
      status: "paused",
      label: "paused"
    };
  }
  return {
    status: "live",
    label: "live"
  };
}

export async function previewDropFlow(input: PreviewDropFlowInput, deps: PreviewDropFlowDeps) {
  const logger = createWorkflowLogger();
  const settings = await runStage(logger, "preview:getSettings", () =>
    deps.getDropSettings(
      {
        chainMId: input.chainMId,
        contractAddress: input.contractAddress
      },
      input.walletAddress
    )
  );

  const dropID = settings.data.dropID;
  const design =
    dropID > 0
      ? await runStage(logger, "preview:getDesign", () =>
        deps.getDropDesign(
          {
            dropID,
            chainMId: input.chainMId,
            contractAddress: input.contractAddress
          },
          input.walletAddress
        )
      )
      : null;
  const tempURL =
    dropID > 0
      ? await runStage(logger, "preview:getTempURL", () =>
        deps.getTempURL(
          input.authorization,
          {
            chainMId: input.chainMId,
            contractAddress: input.contractAddress,
            dropID,
            page: input.page ?? 1,
            pageSize: input.pageSize ?? 20
          },
          input.walletAddress
        )
      )
      : null;

  return {
    chainMId: input.chainMId,
    contractAddress: input.contractAddress,
    slug: input.slug ?? null,
    urls: input.slug ? buildDropUrls(input.slug) : null,
    settings,
    design,
    tempURL,
    summary: {
      slug: input.slug ?? null,
      ...(input.slug ? buildDropUrls(input.slug) : { dropUrl: null, collectionUrl: null, editUrl: null }),
      publishState: summarizePublishState({
        published: settings.data.published,
        isPaused: settings.data.isPaused
      }),
      batch: settings.data.batch,
      stageCount: settings.data.stages?.length ?? 0,
      hasDesign: Boolean(design?.data),
      hasPreRevealMedia: Boolean(
        tempURL?.data?.preReveal?.image_url?.trim() ?? tempURL?.data?.preReveal?.imageURL?.trim()
      )
    }
  };
}


path: scripts/src/workflow/links.ts
content: const SOCIAL_LINK_PREFIXES = {
  twitter: ["https://x.com/", "https://twitter.com/"],
  instagram: ["https://www.instagram.com/", "https://instagram.com/"],
  discord: ["https://discord.gg/"],
  telegram: ["https://t.me/"],
  medium: ["https://www.medium.com/@", "https://medium.com/@", "https://www.medium.com/", "https://medium.com/"]
} as const;

export type SocialLinkField = keyof typeof SOCIAL_LINK_PREFIXES;

const SOCIAL_LINK_DISPLAY_PREFIX = {
  twitter: "https://x.com/",
  instagram: "https://www.instagram.com/",
  discord: "https://discord.gg/",
  telegram: "https://t.me/",
  medium: "https://www.medium.com/@"
} as const;

function stripKnownPrefix(value: string, prefixes: readonly string[]) {
  for (const prefix of prefixes) {
    if (value.startsWith(prefix)) {
      return value.slice(prefix.length);
    }
  }
  return value;
}

function trimSlashes(value: string) {
  return value.replace(/^\/+/, "").replace(/\/+$/, "");
}

export function normalizeWebsiteUrl(value: string | null | undefined) {
  return typeof value === "string" ? value.trim() : "";
}

export function normalizeSocialLinkSuffix(field: SocialLinkField, value: string | null | undefined) {
  if (typeof value !== "string") {
    return "";
  }
  let normalized = value.trim();
  if (!normalized) {
    return "";
  }
  normalized = stripKnownPrefix(normalized, SOCIAL_LINK_PREFIXES[field]);
  if (field === "medium" && normalized.startsWith("@")) {
    normalized = normalized.slice(1);
  }
  return trimSlashes(normalized);
}

export function formatSocialLinkForDisplay(field: SocialLinkField, value: string | null | undefined) {
  const suffix = normalizeSocialLinkSuffix(field, value);
  if (!suffix) {
    return "";
  }
  return `${SOCIAL_LINK_DISPLAY_PREFIX[field]}${suffix}`;
}


path: scripts/src/workflow/verify-ref-graphql.ts
content: import { buildElementAuthorization, deriveWalletAddress } from "../auth/jwt";
import { getRequiredWalletPrivateKey } from "../env";
import { createElementGraphqlClient } from "../api/graphql";

export async function verifyRefGraphqlExamples() {
  const privateKey = getRequiredWalletPrivateKey();
  const walletAddress = (await deriveWalletAddress({ privateKey })).toLowerCase();
  const graphql = createElementGraphqlClient();
  const authorization = (
    await buildElementAuthorization(
      {
        getLoginNonce: graphql.getLoginNonce,
        loginAuth: graphql.loginAuth
      },
      {
        privateKey,
        walletAddress,
        chainMId: 1201
      }
    )
  ).authorization;

  const results: Record<string, unknown> = {};

  results.getNonce = await graphql.getLoginNonce({
    address: "0x00000007699893e07f12d7d35ac7e4534c31613e",
    blockChain: {
      chain: "eth",
      chainId: "0x1"
    }
  });

  results.collectionContract = await graphql.getCollectionContract({
    address: "0x7122ad8c3f90fd23cb89d3ffa5ce7feac8d64c6a",
    blockChain: {
      chain: "base",
      chainId: "0x2105"
    }
  });

  results.getMutateToken = await graphql.getMutateToken(authorization);

  results.collectionDetailFromEditors = await graphql.getCollectionDetailFromEditors(
    "1122-5",
    authorization
  );

  try {
    results.collectionEdit = await graphql.collectionEdit(
      {
        collectionId: "18829680",
        [REDACTED_SECRET]),
        image: null
      },
      authorization
    );
  } catch (error) {
    results.collectionEdit = {
      ok: false,
      error: error instanceof Error ? error.message : String(error)
    };
  }

  return {
    authorization,
    walletAddress,
    results
  };
}


path: scripts/src/env.ts
content: export function getRequiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

export function normalizeWalletPrivateKey(value: string): string {
  const trimmed = value.trim();
  if (!/^0x[0-9a-fA-F]{64}$/.test(trimmed)) {
    throw new Error("ELEMENT_WALLET_PRIVATE_KEY must be a 0x-prefixed 32-byte hex private key");
  }
  return trimmed;
}

export function getRequiredWalletPrivateKey(): string {
  return normalizeWalletPrivateKey(getRequiredEnv("ELEMENT_WALLET_PRIVATE_KEY"));
}

export function redactKnownSecrets(text: string): string {
  const walletPrivateKey = process.env.ELEMENT_WALLET_PRIVATE_KEY?.trim();
  if (!walletPrivateKey) {
    return text;
  }
  return text.split(walletPrivateKey).join("[REDACTED:ELEMENT_WALLET_PRIVATE_KEY]");
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)