Metadata-Version: 2.5
Name: fusorlabs-sdk
Version: 0.1.2
Summary: One-line token launches on Fusor — bring your own private key
Project-URL: Homepage, https://fusor.fun/docs
Author: Fusor Labs
License: MIT
License-File: LICENSE
Keywords: evm,fusor,launch,token,web3
Requires-Python: >=3.11
Requires-Dist: web3<8,>=7
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# fusorlabs-sdk

[![PyPI](https://img.shields.io/pypi/v/fusorlabs-sdk.svg)](https://pypi.org/project/fusorlabs-sdk/)
[![Python](https://img.shields.io/pypi/pyversions/fusorlabs-sdk.svg)](https://pypi.org/project/fusorlabs-sdk/)
![license](https://img.shields.io/pypi/l/fusorlabs-sdk.svg)

Launch a token on Fusor in one call. Bring your own private key — no API key, no
registration, no allowlist.

```bash
pip install fusorlabs-sdk
```

The distribution is `fusorlabs-sdk`; the import name is `fusor`.

Also available for TypeScript: [`@fusorlabs/sdk`](https://www.npmjs.com/package/@fusorlabs/sdk).

## Quick start

```python
import os
from fusor import FusorClient

fusor = FusorClient(private_key=os.environ["PRIVATE_KEY"])

launch = fusor.launch_token(
    name="My Token",
    symbol="MTK",
    markets=["AAPL"],
    metadata_uri="https://example.com/metadata.json",
)

print(launch.token_address)
print(launch.tx_hash)
print(launch.explorer_url)
```

One call, one transaction. When it returns, the token exists on chain and
`token_address` is read from the receipt — not predicted.

Behind that call are seven steps, each reading live chain state rather than a
cached or hard-coded value:

| # | Step | What it does |
|---|---|---|
| 1 | Preflight | Confirms the proxy is a Fusor launchpad, reads its live fee, staleness limit and registry addresses |
| 2 | Resolve markets | Turns symbols into addresses, validates weights, reads `getConfig` for each market |
| 3 | Oracle freshness | Reads each asset's feed, so a stale oracle becomes "which market, how old" |
| 4 | Metadata | Uses your URI as-is, or uploads for you if `api_base_url` is configured |
| 5 | Build params | Fills the launch struct, including the developer-buy skip sentinels |
| 6 | Simulate | `eth_call` against pending state; reverts cost nothing and decode to a named error |
| 7 | Send and confirm | Signs the exact simulated request, waits for the receipt, reads the address from the event |

## Dry run — check before you spend

Pass `dry_run=True` to stop after step 6. No transaction is sent and nothing is
spent, but it still exercises the real contract against real state.

```python
preview = fusor.launch_token(
    name="My Token",
    symbol="MTK",
    markets=["AAPL"],
    metadata_uri="https://example.com/metadata.json",
    dry_run=True,
)

preview.launch_fee_wei   # what it will cost, read from the chain
preview.estimated_gas
preview.prices           # every feed the launch depends on
preview.token_address    # predicted only — see the warning below
```

In a dry run, `tx_hash` and `explorer_url` are `None`, and `params` holds the
exact struct that would have been submitted.

> ⚠️ **The address a dry run returns is not authoritative.** It is the address
> the token *would* get at the launchpad's current nonce. If another launch
> confirms before yours, the real address differs. Use it for preview only —
> never pre-register it or bake it into anything. In a real launch,
> `token_address` comes from the receipt event and is authoritative.

## Quote markets

One to five markets, given as symbols, addresses, or a mix. Weighted markets are
`(market, weight_bps)` tuples:

```python
markets=["AAPL"]                                # one market → 100%
markets=["AAPL", "TSLA"]                        # two        → 50% each
markets=["AAPL", "TSLA", "NVDA"]                # three      → 3334/3333/3333
markets=[("AAPL", 6000), ("0xaf3d…", 4000)]     # explicit weights
```

Weights are in basis points and must sum to exactly `10000`. Omit them and the
SDK splits evenly, giving the remainder to the first market — an even `3333 * 3`
would sum to `9999` and revert with `WeightsNotOneHundredPercent`. Give weights
to every market or to none; mixing the two is ambiguous and rejected locally
before any request is made.

To see which symbols are available:

```python
fusor.list_quote_markets()
# (QuoteAsset(symbol='AAPL', token='0xaf3d…', decimals=18), …)
```

The symbol table ships with the SDK because the registry has no enumeration
method — it cannot be queried from chain. Passing addresses does not depend on
that table at all: every address is validated against a live
`registry.getConfig()` call for `enabled` and `decimals`.

## Metadata

Two paths. Passing `metadata_uri` means **the SDK makes no HTTP request at
all** — one RPC connection is all it needs:

```python
fusor.launch_token(..., metadata_uri="https://example.com/metadata.json")
```

Or let the SDK upload it, which requires `api_base_url`:

```python
fusor = FusorClient(private_key=..., api_base_url="https://api.example.com")

fusor.launch_token(
    name="My Token",
    symbol="MTK",
    markets=["AAPL"],
    metadata={
        "description": "What this token is",
        "image": "data:image/png;base64,…",   # or a publicly reachable http(s) URL
        "twitter": "https://x.com/…",
        "website": "https://…",
    },
)
```

`metadata_hash` is the keccak256 of the canonical JSON bytes the SDK submits —
computed over what is actually sent, not taken from a server response. The hash
on chain therefore always corresponds to the real metadata.

## Error handling

Every failure arrives as one of three typed errors. **Which type you get tells
you whether anything was spent and whether retrying can help** — that
distinction is the whole reason they are separate types.

```python
from fusor import FusorConfigError, FusorPreflightError, FusorLaunchError

try:
    launch = fusor.launch_token(...)
except FusorConfigError:
    # Your input. Nothing was sent, nothing was spent, and the identical call
    # will fail identically forever. Never retry this one.
    raise
except FusorPreflightError as err:
    # A chain precondition failed during preflight — the transaction was never
    # built. Nothing was spent. Retrying later can work; retrying now will not.
    schedule_retry(err)
except FusorLaunchError as err:
    # The contract reverted. Branch on error_name, never on the message text.
    if err.error_name in ("IncorrectLaunchFee", "DeadlineExpired"):
        retry_now()               # launch_token re-reads every input
    elif err.error_name in ("StaleOracle", "StockTokenDisabled"):
        schedule_retry(err)       # may clear on its own
    else:
        raise
```

| Error | Anything spent? | Retry? |
|---|---|---|
| `FusorConfigError` | No — caught before any request | Never; the input is wrong |
| `FusorPreflightError` | No — the transaction was never built | Later, once the chain state changes |
| `FusorLaunchError` | Possibly gas | Depends on `error_name` |

All 32 on-chain custom errors are decoded to a name. Branch on `error_name`, not
on `str(err)` — the text may change, the names will not. Both SDKs share one
error catalogue, so the same revert produces the same name and message in
Python and TypeScript.

## Bring your own signer (KMS, HSM, multisig)

Every step is also exported on its own, so the signing step can live outside
your process:

```python
from fusor import (
    preflight, resolve_markets, build_launch_params, simulate_launch,
    token_address_from_receipt,
)

pre     = preflight(w3, launchpad)
markets = resolve_markets(w3, pre.stock_registry, ["AAPL"])
params  = build_launch_params(..., markets=markets, creator=addr, deadline=...)
sim     = simulate_launch(
    w3, launchpad=launchpad, account=addr, params=params,
    launch_fee_wei=pre.launch_fee_wei,
)

# Build and sign the transaction yourself, then parse the receipt:
token_address = token_address_from_receipt(receipt)
```

`FusorClient` also accepts any `eth_account` `LocalAccount` instead of a private
key, which covers most custom-signer cases without dropping to the step
functions:

```python
FusorClient(account=my_local_account)
```

## Configuration

```python
FusorClient(
    private_key="0x…",            # or `account`, below
    account=my_local_account,     # any eth_account LocalAccount (KMS, hardware)
    launchpad="0x…",              # optional; see below
    chain_id=4663,                # defaults to Robinhood Chain
    rpc_url="https://…",          # override the default RPC (public RPCs are rate-limited)
    rpc_headers={"Origin": "…"},  # some providers authenticate by header
    api_base_url="https://…",     # only needed for SDK-side metadata upload
)
```

`launchpad` is optional: the deployment record ships in the SDK and is selected
by `chain_id`. Passing it explicitly still wins, which is how you point at a
different deployment.

If a chain has no deployment record, the SDK **raises and asks you to pass one
explicitly** rather than guessing. There are other launchpads on the same chain,
and guessing wrong would mean launching your token into a contract that isn't
Fusor — silently, with no error.

## API reference

| Export | Type | Notes |
|---|---|---|
| `FusorClient` | class | `launch_token`, `preflight`, `list_quote_markets`, `launchpad` |
| `LaunchResult` | dataclass | `token_address`, `tx_hash`, `explorer_url`, `launch_fee_wei`, `markets`, `metadata_uri`, `implementation`, `prices`, `estimated_gas`, `params` |
| `FusorError` | class | Base class for every error below |
| `FusorConfigError` / `FusorPreflightError` / `FusorLaunchError` | class | See [Error handling](#error-handling) |
| `decode_launch_error` | fn | Decodes a revert into `(message, error_name)` |
| `preflight` / `onchain_selectors` / `Preflight` | fn, type | Step 1 — live launchpad parameters |
| `resolve_markets` / `normalize_markets` / `decode_bytes32_symbol` | fn | Step 2 — market resolution |
| `MarketInput` / `ResolvedMarket` | type | Market input and resolved forms |
| `check_oracle_freshness` / `classify_price_reading` / `PriceReading` | fn, type | Step 3 — oracle staleness |
| `resolve_metadata` / `canonical_metadata_json` / `hash_metadata` | fn | Step 4 — metadata and hashing |
| `TokenMetadata` / `ResolvedMetadata` | type | Metadata input and resolved forms |
| `build_launch_params` / `simulate_launch` / `token_address_from_receipt` | fn | Steps 5–7 |
| `LegacyLaunchParams` / `SimulatedLaunch` | dataclass | The launch struct and simulation result |
| `CONSTANTS` / `CHAINS` / `DEPLOYMENTS` / `QUOTE_ASSETS` / `ERROR_MESSAGES` | const | Compile-time contract constants and shipped spec |
| `QuoteAsset` / `quote_asset_by_symbol` | type, fn | Quote-asset table lookups |
| `LAUNCHPAD_ABI` / `REGISTRY_ABI` / `FEED_ABI` / `LAUNCH_SELECTOR` | const | ABIs verified against the deployed bytecode |
| `DEFAULT_CHAIN_ID` / `DEFAULT_DEADLINE_SECONDS` / `__version__` | const | Defaults and package version |

### `launch_token(...)`

Keyword-only. Returns `LaunchResult`.

| Parameter | Type | Default | |
|---|---|---|---|
| `name` | `str` | — | required |
| `symbol` | `str` | — | required |
| `markets` | `Sequence[MarketInput]` | — | required; 1–5 entries |
| `metadata_uri` | `str` | `None` | required unless `metadata` is given |
| `metadata` | `TokenMetadata` | `None` | uploaded by the SDK; needs `api_base_url` |
| `metadata_hash` | `bytes` | computed | override the canonical-JSON hash |
| `creator_fee_recipient` | `str` | the signer | who receives creator fees |
| `deadline_seconds` | `int` | `1800` | transaction deadline |
| `dry_run` | `bool` | `False` | stop after simulation |

## Notes

- **Requires Python 3.11+** and `web3>=7,<8`.
- `launch_fee_wei` and the oracle staleness limit are **deployment-time
  parameters**, read live on every launch rather than treated as constants.
- The implementation address behind the proxy is deliberately **not pinned** —
  proxies get upgraded, and pinning would break every user at the moment of an
  upgrade. The SDK verifies behaviour by scanning the implementation's function
  selectors instead.
- Launching with a developer buy in the same transaction is not supported yet.
  Without it, a launch is a single transaction.

## Links

- **Documentation:** https://fusor.fun/docs
- **TypeScript SDK:** https://www.npmjs.com/package/@fusorlabs/sdk

## License

MIT
