diff --git a/CLAUDE.md b/CLAUDE.md index 591fd62..265cedb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,8 +52,8 @@ src/pumpfun_cli/ ├── crypto.py # AES-256-GCM wallet encryption (scrypt KDF) ├── output.py # render() + error() — TTY-aware output ├── commands/ # Thin CLI layer (config, info, launch, tokens, trade, tx_status, wallet) -├── core/ # Business logic (config, info, launch, pumpswap, tokens, trade, tx_status, validate, wallet) -└── protocol/ # Solana primitives (address, client, contracts, curve, idl_parser, instructions, pumpswap, quote) +├── core/ # Business logic (config, info, launch, launchlab, pumpswap, tokens, trade, tx_status, validate, wallet) +└── protocol/ # Solana primitives (address, client, contracts, curve, idl_parser, instructions, launchlab, pumpswap, quote) tests/ ├── test_commands/ # CLI smoke tests @@ -68,7 +68,7 @@ tests/ **Error handling:** `error()` raises `SystemExit` — code after it is unreachable. Core functions return `dict` with `"error"` key for expected failures (graduated tokens, not found, slippage exceeded, insufficient_balance). Catch `ValueError` for wrong wallet password in every command that decrypts. Buy/sell functions perform pre-trade balance validation — SOL for fees and rent, the quote asset for the spend, token balance for sells with specific amounts. -**Auto-routing:** When `buy_token` / `sell_token` returns `{"error": "graduated"}`, the command layer in `commands/trade.py` automatically retries via `buy_pumpswap` / `sell_pumpswap` (PumpSwap AMM). Never suppress or swallow the `"graduated"` error in core — the routing decision belongs in the command layer. +**Auto-routing:** `_route` in `commands/trade.py` picks the venue from the errors core returns: `{"error": "graduated"}` from `buy_token` / `sell_token` retries on PumpSwap, `{"error": "not_found"}` retries on LaunchLab (`buy_launchlab` / `sell_launchlab`). Never suppress or swallow those errors in core — the routing decision belongs in the command layer. `info` falls back to LaunchLab the same way in `core/info.py`. **Imports:** stdlib → third-party → local. Example: ```python @@ -133,6 +133,13 @@ lists; these are the ones this CLI depends on. - `migrate_v2` also carries accounts the IDL omits: after its 27, the new pool's boost vault authority (`["boost_vault", pool]` on PumpSwap) and that authority's quote token account. Without them PumpSwap reverts with 6027. +- LaunchLab (`protocol/launchlab.py`) pools are keyed by base and quote mint. + The SOL pool is derived; any other quote is found by `getProgramAccounts` + on the base mint (PoolState offset 205). Swaps are the IDL's 15 accounts plus + the system program and the platform and creator fee vaults + (`[platform_config, quote]`, `[creator, quote]`). The curve fee is + GlobalConfig's trade fee plus PlatformConfig's fee and creator fee, read + live; a Token-2022 coin's transfer fee comes off what arrives. - `send_tx` sends v0 and falls back to v1 when the transaction exceeds 1232 bytes (a cashback-pool buy, a launch with a buy). v1 carries the compute budget in its header. diff --git a/README.md b/README.md index 3f8aa39..30764fd 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,13 @@ pump.fun no longer accepts anonymous metadata uploads, so `launch` takes the URI of a metadata JSON you host (`name`, `symbol`, `description`, `image`). Without `--uri` it still tries the upload and says so when it is refused. +`buy`, `sell` and `info` also work on coins launched on Raydium LaunchLab, the +program behind letsbonk.fun, StonkFun and other launchpads: a mint with no +pump.fun bonding curve is looked up there, and the output says `"venue": +"launchlab"` and which launchpad (`platform`). A LaunchLab coin priced in +anything but SOL is found with `getProgramAccounts`, so it needs an RPC that +serves it. Token-2022 transfer fees are taken into account in the amounts. + Amounts are in the coin's quote asset. Most pump.fun coins are priced in SOL, but a coin can be priced in USDC or another admitted mint; `buy`, `sell` and `info` read it from the coin and label their output with it. Buying such a coin @@ -106,7 +113,7 @@ Token discovery (zero config; price and progress added when an RPC is set): pumpfun tokens search Search by name or ticker Token info (requires RPC): - pumpfun info Price, bonding progress, reserves + pumpfun info Price, bonding progress, reserves (pump.fun or LaunchLab) Trading (requires RPC + wallet): pumpfun buy [--slippage N] [--force-amm] [--dry-run] [--confirm] @@ -157,9 +164,9 @@ Global options: | `tokens recommended` | `frontend-api-v3.pump.fun/coins/recommendations` | HTTPS | | `tokens search` | `frontend-api-v3.pump.fun/coins/search-unrestricted` | HTTPS | | `tokens *` with an RPC set | + Solana RPC (`getMultipleAccounts` for the bonding curves) | JSON-RPC | -| `info` | Solana RPC (`getAccountInfo` for bonding curve PDA) | JSON-RPC | -| `buy` | Solana RPC (`getAccountInfo`, `getLatestBlockhash`, `sendTransaction`) | JSON-RPC | -| `sell` | Solana RPC (`getAccountInfo`, `getTokenAccountBalance`, `getLatestBlockhash`, `sendTransaction`) | JSON-RPC | +| `info` | Solana RPC (`getAccountInfo` for bonding curve PDA; LaunchLab: `getMultipleAccounts`, `getProgramAccounts`) | JSON-RPC | +| `buy` | Solana RPC (`getAccountInfo`, `getLatestBlockhash`, `sendTransaction`; LaunchLab adds `getMultipleAccounts`, `getProgramAccounts`) | JSON-RPC | +| `sell` | Solana RPC (`getAccountInfo`, `getTokenAccountBalance`, `getLatestBlockhash`, `sendTransaction`; LaunchLab as `buy`) | JSON-RPC | | `launch` | Solana RPC (`sendTransaction`); `pump.fun/api/ipfs` only without `--uri` | JSON-RPC (+ HTTPS) | | `tx-status` | Solana RPC (`getTransaction`) | JSON-RPC | | `migrate` | Solana RPC (`sendTransaction`) | JSON-RPC | diff --git a/idl/raydium_launchlab_idl.json b/idl/raydium_launchlab_idl.json new file mode 100644 index 0000000..6f2d807 --- /dev/null +++ b/idl/raydium_launchlab_idl.json @@ -0,0 +1,6360 @@ +{ + "address": "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", + "metadata": { + "name": "raydium_launchpad", + "version": "0.2.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "buy_exact_in", + "docs": [ + "Use the given amount of quote tokens to purchase base tokens.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `amount_in` - Amount of quote token to purchase", + "* `minimum_amount_out` - Minimum amount of base token to receive (slippage protection)", + "* `share_fee_rate` - Fee rate for the share", + "" + ], + "discriminator": [ + 250, + 234, + 13, + 123, + 213, + 156, + 19, + 236 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The user performing the swap operation", + "Must sign the transaction and pay for fees" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Used to read protocol fee rates and curve type" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account where the swap will be performed", + "Contains current pool parameters and balances" + ], + "writable": true + }, + { + "name": "user_base_token", + "docs": [ + "The user's token account for base tokens (tokens being bought)", + "Will receive the output tokens after the swap" + ], + "writable": true + }, + { + "name": "user_quote_token", + "docs": [ + "The user's token account for quote tokens (tokens being sold)", + "Will be debited for the input amount" + ], + "writable": true + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will receive the input tokens from the user" + ], + "writable": true + }, + { + "name": "base_token_mint", + "docs": [ + "The mint of the base token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint of the quote token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "base_token_program", + "docs": [ + "Token program owning the base token: either SPL Token or Token-2022" + ] + }, + { + "name": "quote_token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "minimum_amount_out", + "type": "u64" + }, + { + "name": "share_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "buy_exact_out", + "docs": [ + "Use quote tokens to purchase the given amount of base tokens.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `amount_out` - Amount of base token to receive", + "* `maximum_amount_in` - Maximum amount of quote token to purchase (slippage protection)", + "* `share_fee_rate` - Fee rate for the share" + ], + "discriminator": [ + 24, + 211, + 116, + 40, + 105, + 3, + 153, + 56 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The user performing the swap operation", + "Must sign the transaction and pay for fees" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Used to read protocol fee rates and curve type" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account where the swap will be performed", + "Contains current pool parameters and balances" + ], + "writable": true + }, + { + "name": "user_base_token", + "docs": [ + "The user's token account for base tokens (tokens being bought)", + "Will receive the output tokens after the swap" + ], + "writable": true + }, + { + "name": "user_quote_token", + "docs": [ + "The user's token account for quote tokens (tokens being sold)", + "Will be debited for the input amount" + ], + "writable": true + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will receive the input tokens from the user" + ], + "writable": true + }, + { + "name": "base_token_mint", + "docs": [ + "The mint of the base token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint of the quote token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "base_token_program", + "docs": [ + "Token program owning the base token: either SPL Token or Token-2022" + ] + }, + { + "name": "quote_token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount_out", + "type": "u64" + }, + { + "name": "maximum_amount_in", + "type": "u64" + }, + { + "name": "share_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "claim_creator_fee", + "docs": [ + "Claim the fee from the exclusive creator fee vault.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 26, + 97, + 138, + 203, + 132, + 171, + 141, + 252 + ], + "accounts": [ + { + "name": "creator", + "docs": [ + "The pool creator" + ], + "writable": true, + "signer": true + }, + { + "name": "fee_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 102, + 101, + 101, + 95, + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "creator_fee_vault", + "docs": [ + "The creator fee vault" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "recipient_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "quote_mint", + "docs": [ + "The mint for the quote token" + ] + }, + { + "name": "token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "docs": [ + "Required for associated token program" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [] + }, + { + "name": "claim_platform_fee", + "docs": [ + "Claim platform fee", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 156, + 39, + 208, + 135, + 76, + 237, + 61, + 72 + ], + "accounts": [ + { + "name": "platform_fee_wallet", + "docs": [ + "Only the wallet stored in platform_config can collect platform fees" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault and mint operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config account" + ] + }, + { + "name": "quote_vault", + "writable": true + }, + { + "name": "recipient_token_account", + "docs": [ + "The address that receives the collected quote token fees" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "platform_fee_wallet" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "quote_mint", + "docs": [ + "The mint of quote token vault" + ] + }, + { + "name": "token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "docs": [ + "Required for associated token program" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [] + }, + { + "name": "claim_platform_fee_from_vault", + "docs": [ + "Claim the fee from the exclusive platform fee vault.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 117, + 241, + 198, + 168, + 248, + 218, + 80, + 29 + ], + "accounts": [ + { + "name": "platform_fee_wallet", + "docs": [ + "Only the wallet stored in platform_config can collect platform fees" + ], + "writable": true, + "signer": true + }, + { + "name": "fee_vault_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 102, + 101, + 101, + 95, + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "platform_config", + "docs": [ + "The platform config account" + ] + }, + { + "name": "platform_fee_vault", + "docs": [ + "The platform fee vault" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "recipient_token_account", + "docs": [ + "The address that receives the collected quote token fees" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "platform_fee_wallet" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "quote_mint", + "docs": [ + "The mint of quote token vault" + ] + }, + { + "name": "token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "docs": [ + "Required for associated token program" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [] + }, + { + "name": "claim_vested_token", + "docs": [ + "Claim vested token", + "# Arguments" + ], + "discriminator": [ + 49, + 33, + 104, + 30, + 189, + 157, + 79, + 35 + ], + "accounts": [ + { + "name": "beneficiary", + "docs": [ + "The beneficiary of the vesting account" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault and mint operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true + }, + { + "name": "vesting_record", + "docs": [ + "The vesting record account" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 101, + 115, + 116, + 105, + 110, + 103 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "beneficiary" + } + ] + } + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "user_base_token", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "beneficiary" + }, + { + "kind": "account", + "path": "base_token_program" + }, + { + "kind": "account", + "path": "base_token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "base_token_mint", + "docs": [ + "The mint for the base token (token being sold)", + "Created in this instruction with specified decimals" + ] + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program or Token-2022 program for the base token", + "Bound to the base mint's owner via `base_token_mint.mint::token_program`" + ] + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "docs": [ + "Required for associated token program" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [] + }, + { + "name": "close_platform_allow_config", + "docs": [ + "Stop allowing the global config to be used to create pools with the platform config", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 82, + 205, + 36, + 216, + 16, + 6, + 240, + 215 + ], + "accounts": [ + { + "name": "platform_admin", + "docs": [ + "The owner of the platform config, it receives the rent of the closed account" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the global config was allowed for" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_admin" + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "The global config the platform no longer allows" + ] + }, + { + "name": "platform_allow_config", + "docs": [ + "The account that records the allowed global config" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 97, + 108, + 108, + 111, + 119, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + } + ], + "args": [] + }, + { + "name": "close_platform_curve_rule", + "docs": [ + "Close the launch param rule of the global config, the platform then no longer", + "restricts the launch params of the global config", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 194, + 114, + 82, + 45, + 136, + 88, + 169, + 159 + ], + "accounts": [ + { + "name": "curve_rule_authority", + "docs": [ + "The curve rule manager of the platform config or the platform admin itself,", + "it receives the rent of the closed account, see PlatformConfig::curve_rule_manager" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the rule belongs to, it can only be managed by the dedicated", + "curve rule manager wallet or by the platform admin" + ] + }, + { + "name": "global_config", + "docs": [ + "The global config the rule applies to" + ] + }, + { + "name": "platform_curve_rule", + "docs": [ + "The launch param rule to be closed" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 117, + 114, + 118, + 101, + 95, + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + } + ], + "args": [] + }, + { + "name": "collect_excess_lamports", + "docs": [ + "Collect excess lamports, including the token accounts and mints owned by the", + "program authorities and the program pda accounts.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 28, + 189, + 19, + 40, + 164, + 176, + 118, + 17 + ], + "accounts": [ + { + "name": "collect_lamports_wallet", + "docs": [ + "Only admin or collect_lamports can collect lamports" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "authority, the platform fee vault authority or the creator fee vault authority" + ] + }, + { + "name": "token_program", + "docs": [ + "The SPL program to perform token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "token_program_2022", + "docs": [ + "The SPL program 2022 to perform token transfers" + ], + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + } + ], + "args": [] + }, + { + "name": "collect_fee", + "docs": [ + "Collects accumulated fees from the pool", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 60, + 173, + 247, + 103, + 4, + 93, + 130, + 48 + ], + "accounts": [ + { + "name": "owner", + "docs": [ + "Only protocol_fee_owner saved in global_config can collect protocol fee now" + ], + "signer": true + }, + { + "name": "authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Pool state stores accumulated protocol fee amount" + ], + "writable": true + }, + { + "name": "global_config", + "docs": [ + "Global config account stores owner" + ] + }, + { + "name": "quote_vault", + "docs": [ + "The address that holds pool tokens for quote token" + ], + "writable": true + }, + { + "name": "quote_mint", + "docs": [ + "The mint of quote token vault" + ] + }, + { + "name": "recipient_token_account", + "docs": [ + "The address that receives the collected quote token fees" + ], + "writable": true + }, + { + "name": "token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + } + ], + "args": [] + }, + { + "name": "collect_migrate_fee", + "docs": [ + "Collects migrate fees from the pool", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 255, + 186, + 150, + 223, + 235, + 118, + 201, + 186 + ], + "accounts": [ + { + "name": "owner", + "docs": [ + "Only migrate_fee_owner saved in global_config can collect migrate fee now" + ], + "signer": true + }, + { + "name": "authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Pool state stores accumulated protocol fee amount" + ], + "writable": true + }, + { + "name": "global_config", + "docs": [ + "Global config account stores owner" + ] + }, + { + "name": "quote_vault", + "docs": [ + "The address that holds pool tokens for quote token" + ], + "writable": true + }, + { + "name": "quote_mint", + "docs": [ + "The mint of quote token vault" + ] + }, + { + "name": "recipient_token_account", + "docs": [ + "The address that receives the collected quote token fees" + ], + "writable": true + }, + { + "name": "token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + } + ], + "args": [] + }, + { + "name": "create_config", + "docs": [ + "Creates a new configuration", + "# Arguments", + "", + "* `ctx` - The accounts needed by instruction", + "* `curve_type` - The type of bonding curve (0: ConstantProduct)", + "* `index` - The index of config, there may be multiple config with the same curve type.", + "* `trade_fee_rate` - Trade fee rate, must be less than RATE_DENOMINATOR_VALUE", + "" + ], + "discriminator": [ + 201, + 207, + 243, + 114, + 75, + 111, + 47, + 189 + ], + "accounts": [ + { + "name": "owner", + "docs": [ + "The protocol owner/admin account", + "Must match the predefined admin address or the create config authority", + "Has authority to create and modify protocol configurations" + ], + "writable": true, + "signer": true + }, + { + "name": "global_config", + "docs": [ + "Global configuration account that stores protocol-wide settings", + "PDA generated using GLOBAL_CONFIG_SEED, quote token mint, and curve type", + "Stores fee rates and protocol parameters" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "quote_token_mint" + }, + { + "kind": "arg", + "path": "curve_type" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint address of the quote token (token used for buying)", + "This will be the standard token used for all pools with this config", + "May be owned by either the SPL Token program or the Token-2022 program" + ] + }, + { + "name": "protocol_fee_owner", + "docs": [ + "Account that will receive protocol fees" + ] + }, + { + "name": "migrate_fee_owner", + "docs": [ + "Account that will receive migrate fees" + ] + }, + { + "name": "migrate_to_amm_wallet", + "docs": [ + "The control wallet address for migrating to amm" + ] + }, + { + "name": "migrate_to_cpswap_wallet", + "docs": [ + "The control wallet address for migrating to cpswap" + ] + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "curve_type", + "type": "u8" + }, + { + "name": "index", + "type": "u16" + }, + { + "name": "migrate_fee", + "type": "u64" + }, + { + "name": "trade_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "create_platform_allow_config", + "docs": [ + "Allow the global config to be used to create pools with the platform config,", + "it takes effect only when platform_config.restrict_global_config is 1", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 69, + 71, + 168, + 7, + 214, + 250, + 107, + 102 + ], + "accounts": [ + { + "name": "platform_admin", + "docs": [ + "The owner of the platform config, it pays for the initialization costs" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the global config is allowed for" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_admin" + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "The global config the platform allows to create pools with" + ] + }, + { + "name": "platform_allow_config", + "docs": [ + "The account that records the allowed global config" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 97, + 108, + 108, + 111, + 119, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "create_platform_config", + "docs": [ + "Create platform config account", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "# Fields", + "* `fee_rate` - Fee rate of the platform", + "* `name` - Name of the platform", + "* `web` - Website of the platform", + "* `img` - Image link of the platform", + "" + ], + "discriminator": [ + 176, + 90, + 196, + 175, + 253, + 113, + 220, + 20 + ], + "accounts": [ + { + "name": "platform_admin", + "docs": [ + "The account paying for the initialization costs" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_fee_wallet" + }, + { + "name": "platform_nft_wallet" + }, + { + "name": "platform_config", + "docs": [ + "The platform config account" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_admin" + } + ] + } + }, + { + "name": "cpswap_config" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "transfer_fee_extension_authority" + }, + { + "name": "platform_vesting_wallet" + } + ], + "args": [ + { + "name": "platform_params", + "type": { + "defined": { + "name": "PlatformParams" + } + } + } + ] + }, + { + "name": "create_platform_curve_rule", + "docs": [ + "Create an empty launch param rule for the global config, the check groups are", + "added with the `update_platform_curve_rule` instruction. It is signed by the", + "curve rule manager wallet or by the platform admin.", + "It takes effect only when platform_config.restrict_curve_param is 1", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 148, + 71, + 180, + 136, + 122, + 144, + 59, + 21 + ], + "accounts": [ + { + "name": "curve_rule_authority", + "docs": [ + "The curve rule manager of the platform config or the platform admin itself,", + "it pays for the initialization costs, see PlatformConfig::curve_rule_manager" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the rule belongs to, it can only be managed by the dedicated", + "curve rule manager wallet or by the platform admin" + ] + }, + { + "name": "global_config", + "docs": [ + "The global config the rule applies to" + ] + }, + { + "name": "platform_curve_rule", + "docs": [ + "The account that records the launch param check groups of the global config" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 117, + 114, + 118, + 101, + 95, + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "create_platform_vesting_account", + "docs": [ + "Create vesting account", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `share` - The share amount of base token to be vested", + "" + ], + "discriminator": [ + 146, + 71, + 173, + 69, + 98, + 19, + 15, + 106 + ], + "accounts": [ + { + "name": "platform_vesting_wallet", + "docs": [ + "The account paying for the initialization costs", + "This can be any account with sufficient SOL to cover the transaction" + ], + "writable": true, + "signer": true + }, + { + "name": "beneficiary", + "docs": [ + "The beneficiary is used to receive the allocated linear release of tokens.", + "Once this account is set, it cannot be modified, so please ensure the validity of this account,", + "otherwise, the unlocked tokens will not be claimable." + ], + "writable": true + }, + { + "name": "platform_config", + "docs": [ + "Platform config account to be changed" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account" + ], + "writable": true + }, + { + "name": "platform_vesting_record", + "docs": [ + "The vesting record account" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 101, + 115, + 116, + 105, + 110, + 103 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "beneficiary" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "create_vesting_account", + "docs": [ + "Create vesting account", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `share` - The share amount of base token to be vested", + "" + ], + "discriminator": [ + 129, + 178, + 2, + 13, + 217, + 172, + 230, + 218 + ], + "accounts": [ + { + "name": "creator", + "docs": [ + "The account paying for the initialization costs", + "This can be any account with sufficient SOL to cover the transaction" + ], + "writable": true, + "signer": true + }, + { + "name": "beneficiary", + "docs": [ + "The beneficiary is used to receive the allocated linear release of tokens.", + "Once this account is set, it cannot be modified, so please ensure the validity of this account,", + "otherwise, the unlocked tokens will not be claimable." + ], + "writable": true + }, + { + "name": "pool_state", + "docs": [ + "The pool state account" + ], + "writable": true + }, + { + "name": "vesting_record", + "docs": [ + "The vesting record account" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 101, + 115, + 116, + 105, + 110, + 103 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "beneficiary" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "share_amount", + "type": "u64" + } + ] + }, + { + "name": "initialize", + "docs": [ + "Deprecated, it always fails with `NotApproved`, use `initialize_v2`", + "# Arguments", + "", + "* `ctx` - The context of accounts containing pool and token information", + "" + ], + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The account paying for the initialization costs", + "This can be any account with sufficient SOL to cover the transaction" + ], + "writable": true, + "signer": true + }, + { + "name": "creator" + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Includes settings like quote token mint and fee parameters" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform info", + "Includes settings like the fee_rate, name, web, img of the platform" + ] + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault and mint operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "base_mint", + "docs": [ + "The mint for the base token (token being sold)", + "Created in this instruction with specified decimals" + ], + "writable": true, + "signer": true + }, + { + "name": "quote_mint", + "docs": [ + "The mint for the quote token (token used to buy)", + "Must match the quote_mint specified in global config" + ] + }, + { + "name": "base_vault", + "docs": [ + "Token account that holds the pool's base tokens", + "PDA generated using POOL_VAULT_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "quote_vault", + "docs": [ + "Token account that holds the pool's quote tokens", + "PDA generated using POOL_VAULT_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "metadata_account", + "docs": [ + "Account to store the base token's metadata", + "Created using Metaplex metadata program" + ], + "writable": true + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for the base token", + "Must be the standard Token program" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "quote_token_program", + "docs": [ + "SPL Token program for the quote token" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "metadata_program", + "docs": [ + "Metaplex Token Metadata program", + "Used to create metadata for the base token" + ], + "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "rent_program", + "docs": [ + "Required for rent exempt calculations" + ], + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "_base_mint_param", + "type": { + "defined": { + "name": "MintParams" + } + } + }, + { + "name": "_curve_param", + "type": { + "defined": { + "name": "CurveParams" + } + } + }, + { + "name": "_vesting_param", + "type": { + "defined": { + "name": "VestingParams" + } + } + } + ] + }, + { + "name": "initialize_v2", + "docs": [ + "Initializes a new trading pool,", + "pool created by this instruction must be migrated to cpswap after fundraising ends, i.e., curve_param.migrate_type = 1", + "# Arguments", + "", + "* `ctx` - The context of accounts containing pool and token information", + "" + ], + "discriminator": [ + 67, + 153, + 175, + 39, + 218, + 16, + 38, + 32 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The account paying for the initialization costs", + "This can be any account with sufficient SOL to cover the transaction" + ], + "writable": true, + "signer": true + }, + { + "name": "creator" + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Includes settings like quote token mint and fee parameters" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform info", + "Includes settings like the fee_rate, name, web, img of the platform" + ] + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault and mint operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "base_mint", + "docs": [ + "The mint for the base token (token being sold)", + "Created in this instruction with specified decimals" + ], + "writable": true, + "signer": true + }, + { + "name": "quote_mint", + "docs": [ + "The mint for the quote token (token used to buy)", + "Must match the quote_mint specified in global config" + ] + }, + { + "name": "base_vault", + "docs": [ + "Token account that holds the pool's base tokens", + "PDA generated using POOL_VAULT_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "quote_vault", + "docs": [ + "Token account that holds the pool's quote tokens", + "PDA generated using POOL_VAULT_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "metadata_account", + "docs": [ + "Account to store the base token's metadata", + "Created using Metaplex metadata program" + ], + "writable": true + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for the base token", + "Must be the standard Token program" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "quote_token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + }, + { + "name": "metadata_program", + "docs": [ + "Metaplex Token Metadata program", + "Used to create metadata for the base token" + ], + "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "rent_program", + "docs": [ + "Required for rent exempt calculations" + ], + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "base_mint_param", + "type": { + "defined": { + "name": "MintParams" + } + } + }, + { + "name": "curve_param", + "type": { + "defined": { + "name": "CurveParams" + } + } + }, + { + "name": "vesting_param", + "type": { + "defined": { + "name": "VestingParams" + } + } + }, + { + "name": "amm_fee_on", + "type": { + "defined": { + "name": "AmmCreatorFeeOn" + } + } + } + ] + }, + { + "name": "initialize_with_token_2022", + "docs": [ + "Initializes a new trading pool with base token belongs to spl-token-2022,", + "pool created by this instruction must be migrated to cpswap after fundraising ends, i.e., curve_param.migrate_type = 1", + "# Arguments", + "", + "* `ctx` - The context of accounts containing pool and token information", + "" + ], + "discriminator": [ + 37, + 190, + 126, + 222, + 44, + 154, + 171, + 17 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The account paying for the initialization costs", + "This can be any account with sufficient SOL to cover the transaction" + ], + "writable": true, + "signer": true + }, + { + "name": "creator" + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Includes settings like quote token mint and fee parameters" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform info", + "Includes settings like the fee_rate, name, web, img of the platform" + ] + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault and mint operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "base_mint", + "docs": [ + "The mint for the base token (token being sold)", + "Created in this instruction with specified decimals" + ], + "writable": true, + "signer": true + }, + { + "name": "quote_mint", + "docs": [ + "The mint for the quote token (token used to buy)", + "Must match the quote_mint specified in global config" + ] + }, + { + "name": "base_vault", + "docs": [ + "Token account that holds the pool's base tokens", + "PDA generated using POOL_VAULT_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "quote_vault", + "docs": [ + "Token account that holds the pool's quote tokens", + "PDA generated using POOL_VAULT_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for the base token" + ], + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "quote_token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "base_mint_param", + "type": { + "defined": { + "name": "MintParams" + } + } + }, + { + "name": "curve_param", + "type": { + "defined": { + "name": "CurveParams" + } + } + }, + { + "name": "vesting_param", + "type": { + "defined": { + "name": "VestingParams" + } + } + }, + { + "name": "amm_fee_on", + "type": { + "defined": { + "name": "AmmCreatorFeeOn" + } + } + }, + { + "name": "transfer_fee_extension_param", + "type": { + "option": { + "defined": { + "name": "TransferFeeExtensionParams" + } + } + } + } + ] + }, + { + "name": "migrate_to_amm", + "docs": [ + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 207, + 82, + 192, + 145, + 254, + 207, + 145, + 223 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "Only migrate_to_amm_wallet can migrate to cpswap pool", + "This signer must match the migrate_to_amm_wallet saved in global_config" + ], + "writable": true, + "signer": true + }, + { + "name": "base_mint", + "docs": [ + "The mint for the base token (token being sold)" + ] + }, + { + "name": "quote_mint", + "docs": [ + "The mint for the quote token (token used to buy)" + ] + }, + { + "name": "market", + "docs": [ + "Account created and asigned to openbook_program but not been initialized" + ], + "writable": true + }, + { + "name": "amm_program", + "address": "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" + }, + { + "name": "amm_pool", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 97, + 109, + 109, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 109, + 109, + 32, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_lp_mint", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 108, + 112, + 95, + 109, + 105, + 110, + 116, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_base_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 99, + 111, + 105, + 110, + 95, + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_quote_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 112, + 99, + 95, + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_target_orders", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 116, + 97, + 114, + 103, + 101, + 116, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 109, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_create_fee_destination", + "writable": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global config account stores owner" + ] + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be fully drained during migration" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will be fully drained during migration" + ], + "writable": true + }, + { + "name": "pool_lp_token", + "writable": true + }, + { + "name": "spl_token_program", + "docs": [ + "SPL Token program for the base token", + "Must be the standard Token program" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "docs": [ + "Program to create an ATA for receiving fee NFT" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "rent_program", + "docs": [ + "Required for rent exempt calculations" + ], + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "migrate_to_cpswap", + "docs": [ + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 136, + 92, + 200, + 103, + 28, + 218, + 144, + 140 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "Only migrate_to_cpswap_wallet can migrate to cpswap pool", + "This signer must match the migrate_to_cpswap_wallet saved in global_config" + ], + "writable": true, + "signer": true + }, + { + "name": "base_mint", + "docs": [ + "The mint for the base token (token being sold)", + "May be owned by either token program, the instruction picks the matching one" + ], + "writable": true + }, + { + "name": "quote_mint", + "docs": [ + "The mint for the quote token (token used to buy)", + "May be owned by either token program, the instruction picks the matching one" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "cpswap_program", + "address": "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C" + }, + { + "name": "cpswap_pool", + "docs": [ + "PDA account:", + "seeds = [", + "b\"pool\",", + "cpswap_config.key().as_ref(),", + "token_0_mint.key().as_ref(),", + "token_1_mint.key().as_ref(),", + "],", + "seeds::program = cpswap_program,", + "", + "Or random account: must be signed by cli" + ], + "writable": true + }, + { + "name": "cpswap_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 110, + 100, + 95, + 108, + 112, + 95, + 109, + 105, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "cpswap_lp_mint", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 108, + 112, + 95, + 109, + 105, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "cpswap_pool" + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "cpswap_base_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "cpswap_pool" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "cpswap_quote_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "cpswap_pool" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "cpswap_config" + }, + { + "name": "cpswap_create_pool_fee", + "writable": true + }, + { + "name": "cpswap_observation", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 111, + 98, + 115, + 101, + 114, + 118, + 97, + 116, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "cpswap_pool" + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "lock_program", + "address": "LockrWmn6K5twhz3y9w1dQERbmgSaRkfnTeTKbpofwE" + }, + { + "name": "lock_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 99, + 107, + 95, + 99, + 112, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "lock_program" + } + } + }, + { + "name": "lock_lp_vault", + "writable": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global config account stores owner" + ] + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be fully drained during migration" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will be fully drained during migration" + ], + "writable": true + }, + { + "name": "pool_lp_token", + "writable": true + }, + { + "name": "token_program", + "docs": [ + "The legacy SPL Token program.", + "Always required: the cpswap lp mint and the locked liquidity fee NFT live on it, and it", + "serves the base or quote token whenever that mint is a legacy mint." + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "token_program_2022", + "docs": [ + "The Token-2022 program, serving the base or quote token when that mint is a", + "Token-2022 mint. Always required, even when neither mint uses it." + ], + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "associated_token_program", + "docs": [ + "Program to create an ATA for receiving fee NFT" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "rent_program", + "docs": [ + "Required for rent exempt calculations" + ], + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "metadata_program", + "docs": [ + "Program to create NFT metadata accunt" + ], + "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + } + ], + "args": [] + }, + { + "name": "remove_platform_curve_rule", + "docs": [ + "Remove one check group of the launch param rule", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `group_id` - The id of the group to remove", + "" + ], + "discriminator": [ + 236, + 77, + 154, + 138, + 32, + 145, + 127, + 219 + ], + "accounts": [ + { + "name": "curve_rule_authority", + "docs": [ + "The curve rule manager of the platform config or the platform admin itself,", + "it receives the rent the account shrinks by,", + "see PlatformConfig::curve_rule_manager" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the rule belongs to, it can only be managed by the dedicated", + "curve rule manager wallet or by the platform admin" + ] + }, + { + "name": "global_config", + "docs": [ + "The global config the rule applies to" + ] + }, + { + "name": "platform_curve_rule", + "docs": [ + "The launch param rule to be changed" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 117, + 114, + 118, + 101, + 95, + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required to settle the rent of the resized account" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "group_id", + "type": "u16" + } + ] + }, + { + "name": "sell_exact_in", + "docs": [ + "Use the given amount of base tokens to sell for quote tokens.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `amount_in` - Amount of base token to sell", + "* `minimum_amount_out` - Minimum amount of quote token to receive (slippage protection)", + "* `share_fee_rate` - Fee rate for the share", + "" + ], + "discriminator": [ + 149, + 39, + 222, + 155, + 211, + 124, + 152, + 26 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The user performing the swap operation", + "Must sign the transaction and pay for fees" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Used to read protocol fee rates and curve type" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account where the swap will be performed", + "Contains current pool parameters and balances" + ], + "writable": true + }, + { + "name": "user_base_token", + "docs": [ + "The user's token account for base tokens (tokens being bought)", + "Will receive the output tokens after the swap" + ], + "writable": true + }, + { + "name": "user_quote_token", + "docs": [ + "The user's token account for quote tokens (tokens being sold)", + "Will be debited for the input amount" + ], + "writable": true + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will receive the input tokens from the user" + ], + "writable": true + }, + { + "name": "base_token_mint", + "docs": [ + "The mint of the base token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint of the quote token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "base_token_program", + "docs": [ + "Token program owning the base token: either SPL Token or Token-2022" + ] + }, + { + "name": "quote_token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "minimum_amount_out", + "type": "u64" + }, + { + "name": "share_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "sell_exact_out", + "docs": [ + "Sell base tokens for the given amount of quote tokens.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `amount_out` - Amount of quote token to receive", + "* `maximum_amount_in` - Maximum amount of base token to purchase (slippage protection)", + "* `share_fee_rate` - Fee rate for the share", + "" + ], + "discriminator": [ + 95, + 200, + 71, + 34, + 8, + 9, + 11, + 166 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The user performing the swap operation", + "Must sign the transaction and pay for fees" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Used to read protocol fee rates and curve type" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account where the swap will be performed", + "Contains current pool parameters and balances" + ], + "writable": true + }, + { + "name": "user_base_token", + "docs": [ + "The user's token account for base tokens (tokens being bought)", + "Will receive the output tokens after the swap" + ], + "writable": true + }, + { + "name": "user_quote_token", + "docs": [ + "The user's token account for quote tokens (tokens being sold)", + "Will be debited for the input amount" + ], + "writable": true + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will receive the input tokens from the user" + ], + "writable": true + }, + { + "name": "base_token_mint", + "docs": [ + "The mint of the base token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint of the quote token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "base_token_program", + "docs": [ + "Token program owning the base token: either SPL Token or Token-2022" + ] + }, + { + "name": "quote_token_program", + "docs": [ + "Token program owning the quote token: either SPL Token or Token-2022" + ] + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount_out", + "type": "u64" + }, + { + "name": "maximum_amount_in", + "type": "u64" + }, + { + "name": "share_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "update_config", + "docs": [ + "Updates configuration parameters", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `param` - Parameter to update:", + "- 0: Update trade_fee_rate", + "- 1: Update fee owner", + "* `value` - New value for the selected parameter", + "" + ], + "discriminator": [ + 29, + 158, + 252, + 191, + 10, + 83, + 219, + 99 + ], + "accounts": [ + { + "name": "owner", + "docs": [ + "The global config owner or admin" + ], + "signer": true + }, + { + "name": "global_config", + "docs": [ + "Global config account to be changed" + ], + "writable": true + } + ], + "args": [ + { + "name": "param", + "type": "u8" + }, + { + "name": "value", + "type": "u64" + } + ] + }, + { + "name": "update_platform_config", + "docs": [ + "Update platform config", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `param` - Parameter to update", + "" + ], + "discriminator": [ + 195, + 60, + 76, + 129, + 146, + 45, + 67, + 143 + ], + "accounts": [ + { + "name": "platform_admin", + "docs": [ + "The account paying for the initialization costs" + ], + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "Platform config account to be changed" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_admin" + } + ] + } + } + ], + "args": [ + { + "name": "param", + "type": { + "defined": { + "name": "PlatformConfigParam" + } + } + } + ] + }, + { + "name": "update_platform_curve_rule", + "docs": [ + "Add or replace one check group of the launch param rule.", + "The constraints of a group must all hold together, a launch is allowed when it", + "satisfies any group, a group with no constraint allows everything.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `group_id` - The stable id of the group, an existing group with the id is replaced", + "* `constraints` - The constraints of the group, see `param_field` and `param_op`", + "" + ], + "discriminator": [ + 90, + 100, + 206, + 196, + 54, + 117, + 120, + 139 + ], + "accounts": [ + { + "name": "curve_rule_authority", + "docs": [ + "The curve rule manager of the platform config or the platform admin itself,", + "it pays the rent the account grows by, and receives the rent it shrinks by,", + "see PlatformConfig::curve_rule_manager" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config the rule belongs to, it can only be managed by the dedicated", + "curve rule manager wallet or by the platform admin" + ] + }, + { + "name": "global_config", + "docs": [ + "The global config the rule applies to" + ] + }, + { + "name": "platform_curve_rule", + "docs": [ + "The launch param rule to be changed" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 117, + 114, + 118, + 101, + 95, + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "global_config" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required to settle the rent of the resized account" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "group_id", + "type": "u16" + }, + { + "name": "constraints", + "type": { + "vec": { + "defined": { + "name": "ParamConstraint" + } + } + } + } + ] + } + ], + "accounts": [ + { + "name": "GlobalConfig", + "discriminator": [ + 149, + 8, + 156, + 202, + 160, + 252, + 176, + 217 + ] + }, + { + "name": "PlatformAllowConfig", + "discriminator": [ + 248, + 57, + 34, + 138, + 222, + 238, + 186, + 75 + ] + }, + { + "name": "PlatformConfig", + "discriminator": [ + 160, + 78, + 128, + 0, + 248, + 83, + 230, + 160 + ] + }, + { + "name": "PlatformCurveRule", + "discriminator": [ + 12, + 20, + 122, + 169, + 247, + 155, + 104, + 234 + ] + }, + { + "name": "PoolState", + "discriminator": [ + 247, + 237, + 227, + 245, + 215, + 195, + 222, + 70 + ] + }, + { + "name": "VestingRecord", + "discriminator": [ + 106, + 243, + 221, + 205, + 230, + 126, + 85, + 83 + ] + } + ], + "events": [ + { + "name": "ClaimVestedEvent", + "discriminator": [ + 21, + 194, + 114, + 87, + 120, + 211, + 226, + 32 + ] + }, + { + "name": "CreateVestingEvent", + "discriminator": [ + 150, + 152, + 11, + 179, + 52, + 210, + 191, + 125 + ] + }, + { + "name": "PoolCreateEvent", + "discriminator": [ + 151, + 215, + 226, + 9, + 118, + 161, + 115, + 174 + ] + }, + { + "name": "TradeEvent", + "discriminator": [ + 189, + 219, + 127, + 211, + 78, + 230, + 97, + 238 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "NotApproved", + "msg": "Not approved" + }, + { + "code": 6001, + "name": "InvalidOwner", + "msg": "Input account owner is not the program address" + }, + { + "code": 6002, + "name": "InvalidInput", + "msg": "InvalidInput" + }, + { + "code": 6003, + "name": "InputNotMatchCurveConfig", + "msg": "The input params are not match with curve type in config" + }, + { + "code": 6004, + "name": "ExceededSlippage", + "msg": "Exceeds desired slippage limit" + }, + { + "code": 6005, + "name": "PoolFunding", + "msg": "Pool funding" + }, + { + "code": 6006, + "name": "PoolMigrated", + "msg": "Pool migrated" + }, + { + "code": 6007, + "name": "MigrateTypeNotMatch", + "msg": "Migrate type not match" + }, + { + "code": 6008, + "name": "MathOverflow", + "msg": "Math overflow" + }, + { + "code": 6009, + "name": "NoAssetsToCollect", + "msg": "No assets to collect" + }, + { + "code": 6010, + "name": "VestingRatioTooHigh", + "msg": "Vesting ratio too high" + }, + { + "code": 6011, + "name": "VestingSettingEnded", + "msg": "Vesting setting ended" + }, + { + "code": 6012, + "name": "VestingNotStarted", + "msg": "Vesting not started" + }, + { + "code": 6013, + "name": "NoVestingSchedule", + "msg": "No vesting schedule" + }, + { + "code": 6014, + "name": "InvalidPlatformInfo", + "msg": "The platform info input is invalid" + }, + { + "code": 6015, + "name": "PoolNotMigrated", + "msg": "Pool not migrated" + }, + { + "code": 6016, + "name": "InvalidCpSwapConfig", + "msg": "The input cp swap config account is invalid" + }, + { + "code": 6017, + "name": "NoSupportExtension", + "msg": "No support extension" + }, + { + "code": 6018, + "name": "NotEnoughRemainingAccounts", + "msg": "Not enough remaining accounts" + }, + { + "code": 6019, + "name": "TransferFeeCalculateNotMatch", + "msg": "TransferFee calculate not match" + }, + { + "code": 6020, + "name": "CurveParamIsNotExist", + "msg": "Curve param is not exist" + }, + { + "code": 6021, + "name": "InvalidTotalLockedAmount", + "msg": "Total locked amount must great or equal to the platform vesting share amount" + }, + { + "code": 6022, + "name": "InvalidPlatformAllowConfig", + "msg": "Invalid platform allow config account" + }, + { + "code": 6023, + "name": "CalculateOverflow", + "msg": "Calculation overflowed" + }, + { + "code": 6024, + "name": "InvalidPlatformCurveRule", + "msg": "Invalid platform curve rule account" + }, + { + "code": 6025, + "name": "CurveParamNotMatchPlatformRule", + "msg": "The curve param does not match any check group of the platform curve rule" + }, + { + "code": 6026, + "name": "CurveRuleGroupNotExist", + "msg": "The curve rule group is not exist" + }, + { + "code": 6027, + "name": "CurveRuleGroupsExceeded", + "msg": "The number of curve rule groups exceeds the limit" + }, + { + "code": 6028, + "name": "InvalidCurveRuleConstraint", + "msg": "The curve rule constraint is invalid" + }, + { + "code": 6029, + "name": "InvalidCurveRuleAuthority", + "msg": "The signer is neither the curve rule manager nor the platform admin" + }, + { + "code": 6030, + "name": "CurveRuleFieldNotSupportedByCurve", + "msg": "The curve rule field is not supported by the curve of the global config" + }, + { + "code": 6031, + "name": "LamportsCalculateError", + "msg": "Lamports calculate error" + } + ], + "types": [ + { + "name": "AmmCreatorFeeOn", + "docs": [ + "migrate to cpmm, creator fee on quote token or both token" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "QuoteToken" + }, + { + "name": "BothToken" + } + ] + } + }, + { + "name": "ClaimVestedEvent", + "docs": [ + "Emitted when vesting token claimed by beneficiary" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_state", + "type": "pubkey" + }, + { + "name": "beneficiary", + "type": "pubkey" + }, + { + "name": "claim_amount", + "type": "u64" + } + ] + } + }, + { + "name": "ConstantCurve", + "type": { + "kind": "struct", + "fields": [ + { + "name": "supply", + "type": "u64" + }, + { + "name": "total_base_sell", + "type": "u64" + }, + { + "name": "total_quote_fund_raising", + "type": "u64" + }, + { + "name": "migrate_type", + "type": "u8" + } + ] + } + }, + { + "name": "CreateVestingEvent", + "docs": [ + "Emitted when vest_account created" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_state", + "type": "pubkey" + }, + { + "name": "beneficiary", + "type": "pubkey" + }, + { + "name": "share_amount", + "type": "u64" + } + ] + } + }, + { + "name": "CurveParams", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Constant", + "fields": [ + { + "name": "data", + "type": { + "defined": { + "name": "ConstantCurve" + } + } + } + ] + }, + { + "name": "Fixed", + "fields": [ + { + "name": "data", + "type": { + "defined": { + "name": "FixedCurve" + } + } + } + ] + }, + { + "name": "Linear", + "fields": [ + { + "name": "data", + "type": { + "defined": { + "name": "LinearCurve" + } + } + } + ] + } + ] + } + }, + { + "name": "CurveRuleGroup", + "docs": [ + "A set of constraints that must all hold together, e.g.", + "`supply = 50W && total_base_sell >= 20W`." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "group_id", + "docs": [ + "The stable id of the group, used to update or remove it" + ], + "type": "u16" + }, + { + "name": "epoch", + "docs": [ + "The epoch the group was written" + ], + "type": "u64" + }, + { + "name": "constraints", + "docs": [ + "The constraints of the group, an empty list allows everything" + ], + "type": { + "vec": { + "defined": { + "name": "ParamConstraint" + } + } + } + } + ] + } + }, + { + "name": "FixedCurve", + "type": { + "kind": "struct", + "fields": [ + { + "name": "supply", + "type": "u64" + }, + { + "name": "total_quote_fund_raising", + "type": "u64" + }, + { + "name": "migrate_type", + "type": "u8" + } + ] + } + }, + { + "name": "GlobalConfig", + "docs": [ + "Holds the current owner of the factory" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "Account update epoch" + ], + "type": "u64" + }, + { + "name": "curve_type", + "docs": [ + "0: Constant Product Curve", + "1: Fixed Price Curve", + "2: Linear Price Curve" + ], + "type": "u8" + }, + { + "name": "index", + "docs": [ + "Config index" + ], + "type": "u16" + }, + { + "name": "migrate_fee", + "docs": [ + "The fee of migrate to amm" + ], + "type": "u64" + }, + { + "name": "trade_fee_rate", + "docs": [ + "The trade fee rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "max_share_fee_rate", + "docs": [ + "The maximum share fee rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "min_base_supply", + "docs": [ + "The minimum base supply, the value without decimals" + ], + "type": "u64" + }, + { + "name": "max_lock_rate", + "docs": [ + "The maximum lock rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "min_base_sell_rate", + "docs": [ + "The minimum base sell rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "min_base_migrate_rate", + "docs": [ + "The minimum base migrate rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "min_quote_fund_raising", + "docs": [ + "The minimum quote fund raising, the value with decimals" + ], + "type": "u64" + }, + { + "name": "quote_mint", + "docs": [ + "Mint information for quote token" + ], + "type": "pubkey" + }, + { + "name": "protocol_fee_owner", + "docs": [ + "Protocol Fee owner" + ], + "type": "pubkey" + }, + { + "name": "migrate_fee_owner", + "docs": [ + "Migrate Fee owner" + ], + "type": "pubkey" + }, + { + "name": "migrate_to_amm_wallet", + "docs": [ + "Migrate to amm control wallet" + ], + "type": "pubkey" + }, + { + "name": "migrate_to_cpswap_wallet", + "docs": [ + "Migrate to cpswap wallet" + ], + "type": "pubkey" + }, + { + "name": "padding_alignment", + "docs": [ + "padding alignment" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u64", + 15 + ] + } + } + ] + } + }, + { + "name": "LinearCurve", + "type": { + "kind": "struct", + "fields": [ + { + "name": "supply", + "type": "u64" + }, + { + "name": "total_quote_fund_raising", + "type": "u64" + }, + { + "name": "migrate_type", + "type": "u8" + } + ] + } + }, + { + "name": "MigrateNftInfo", + "docs": [ + "Represents the parameters for initializing a platform config account(Only support MigrateType::CPSWAP)", + "# Fields", + "* `platform_scale` - Scale of the platform liquidity quantity rights will be converted into NFT", + "* `creator_scale` - Scale of the token creator liquidity quantity rights will be converted into NFT", + "* `burn_scale` - Scale of liquidity directly to burn", + "", + "* platform_scale + creator_scale + burn_scale = RATE_DENOMINATOR_VALUE" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "platform_scale", + "type": "u64" + }, + { + "name": "creator_scale", + "type": "u64" + }, + { + "name": "burn_scale", + "type": "u64" + } + ] + } + }, + { + "name": "MintParams", + "docs": [ + "Represents the parameters for initializing a new token mint", + "# Fields", + "* `decimals` - Number of decimal places for the token", + "* `name` - Name of the token", + "* `symbol` - Symbol/ticker of the token", + "* `uri` - URI pointing to token metadata" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "decimals", + "type": "u8" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + } + ] + } + }, + { + "name": "ParamConstraint", + "docs": [ + "A single check on one launch parameter, e.g. `(SUPPLY, GTE, 1_000_000)`.", + "A range check is expressed with two constraints(`GTE` + `LTE`) in the same group." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "field", + "docs": [ + "The parameter this constraint applies to, see `param_field`" + ], + "type": "u8" + }, + { + "name": "op", + "docs": [ + "The comparison operator, see `param_op`" + ], + "type": "u8" + }, + { + "name": "value", + "docs": [ + "The value to compare the actual launch parameter with" + ], + "type": "u128" + } + ] + } + }, + { + "name": "PlatformAllowConfig", + "docs": [ + "The existence of this account means the platform allows the global config to be used", + "with its platform config, it is only checked when the platform restricts the global", + "config, see PlatformConfig::restrict_global_config" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "platform_config", + "type": "pubkey" + }, + { + "name": "global_config", + "type": "pubkey" + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u64", + 8 + ] + } + } + ] + } + }, + { + "name": "PlatformConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "The epoch for update interval" + ], + "type": "u64" + }, + { + "name": "platform_fee_wallet", + "docs": [ + "The platform fee wallet" + ], + "type": "pubkey" + }, + { + "name": "platform_nft_wallet", + "docs": [ + "The platform nft wallet to receive the platform NFT after migration if platform_scale is not 0(Only support MigrateType::CPSWAP)" + ], + "type": "pubkey" + }, + { + "name": "platform_scale", + "docs": [ + "Scale of the platform liquidity quantity rights will be converted into NFT(Only support MigrateType::CPSWAP)" + ], + "type": "u64" + }, + { + "name": "creator_scale", + "docs": [ + "Scale of the token creator liquidity quantity rights will be converted into NFT(Only support MigrateType::CPSWAP)" + ], + "type": "u64" + }, + { + "name": "burn_scale", + "docs": [ + "Scale of liquidity directly to burn" + ], + "type": "u64" + }, + { + "name": "fee_rate", + "docs": [ + "The platform fee rate" + ], + "type": "u64" + }, + { + "name": "name", + "docs": [ + "The platform name" + ], + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "web", + "docs": [ + "The platform website" + ], + "type": { + "array": [ + "u8", + 256 + ] + } + }, + { + "name": "img", + "docs": [ + "The platform img link" + ], + "type": { + "array": [ + "u8", + 256 + ] + } + }, + { + "name": "cpswap_config", + "docs": [ + "The platform specifies the trade fee rate after migration to cp swap" + ], + "type": "pubkey" + }, + { + "name": "creator_fee_rate", + "docs": [ + "Creator fee rate" + ], + "type": "u64" + }, + { + "name": "transfer_fee_extension_auth", + "docs": [ + "If the base token belongs to token2022, then you can choose to support the transferfeeConfig extension, which includes permissions such as `transfer_fee_config_authority`` and `withdraw_withheld_authority`.", + "When initializing mint, `withdraw_withheld_authority` and `transfer_fee_config_authority` both belongs to the contract.", + "Once the token is migrated to AMM, the authorities will be reset to this value" + ], + "type": "pubkey" + }, + { + "name": "platform_vesting_wallet", + "type": "pubkey" + }, + { + "name": "platform_vesting_scale", + "type": "u64" + }, + { + "name": "platform_cp_creator", + "docs": [ + "If a valid platform_cp_creator is configured for the platform,", + "it will be used as the creator for the AMM pool during migration to cpswap pool." + ], + "type": "pubkey" + }, + { + "name": "restrict_global_config", + "docs": [ + "Whether the platform restricts the global config used to create pools with this", + "platform config, 0: no restriction, 1: only the allowed global config can be used," + ], + "type": "u8" + }, + { + "name": "restrict_curve_param", + "docs": [ + "Whether the platform restricts the launch params of the pools created with this", + "platform config, 0: no restriction, 1: the launch params must satisfy the", + "`PlatformCurveRule` account of the used global config" + ], + "type": "u8" + }, + { + "name": "curve_rule_manager", + "docs": [ + "The wallet allowed to manage the `PlatformCurveRule` accounts of this platform", + "config, the platform admin is often a multisig wallet which is inconvenient for", + "the day to day rule changes. The platform admin can always manage the rules too,", + "and it is the only one that can set this wallet." + ], + "type": "pubkey" + }, + { + "name": "padding", + "docs": [ + "padding for future updates. The last 4 bytes were the length prefix of the removed", + "`curve_params` vec, the migration left them zeroed, so the layout keeps exactly the", + "byte count every platform config already has." + ], + "type": { + "array": [ + "u8", + 78 + ] + } + } + ] + } + }, + { + "name": "PlatformConfigInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "fee_wallet", + "type": "pubkey" + }, + { + "name": "nft_wallet", + "type": "pubkey" + }, + { + "name": "migrate_nft_info", + "type": { + "defined": { + "name": "MigrateNftInfo" + } + } + }, + { + "name": "fee_rate", + "type": "u64" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "web", + "type": "string" + }, + { + "name": "img", + "type": "string" + }, + { + "name": "transfer_fee_extension_auth", + "type": "pubkey" + }, + { + "name": "creator_fee_rate", + "type": "u64" + }, + { + "name": "platform_vesting_scale", + "type": "u64" + }, + { + "name": "vesting_wallet", + "type": "pubkey" + } + ] + } + }, + { + "name": "PlatformConfigParam", + "type": { + "kind": "enum", + "variants": [ + { + "name": "FeeWallet", + "fields": [ + "pubkey" + ] + }, + { + "name": "NFTWallet", + "fields": [ + "pubkey" + ] + }, + { + "name": "MigrateNftInfo", + "fields": [ + { + "defined": { + "name": "MigrateNftInfo" + } + } + ] + }, + { + "name": "FeeRate", + "fields": [ + "u64" + ] + }, + { + "name": "Name", + "fields": [ + "string" + ] + }, + { + "name": "Web", + "fields": [ + "string" + ] + }, + { + "name": "Img", + "fields": [ + "string" + ] + }, + { + "name": "CpSwapConfig" + }, + { + "name": "AllInfo", + "fields": [ + { + "defined": { + "name": "PlatformConfigInfo" + } + } + ] + }, + { + "name": "VestingWallet", + "fields": [ + "pubkey" + ] + }, + { + "name": "PlatformVestingScale", + "fields": [ + "u64" + ] + }, + { + "name": "PlatformCPCreator", + "fields": [ + "pubkey" + ] + }, + { + "name": "RestrictGlobalConfig", + "fields": [ + "u64" + ] + }, + { + "name": "RestrictCurveParam", + "fields": [ + "u64" + ] + }, + { + "name": "CurveRuleManager", + "fields": [ + "pubkey" + ] + } + ] + } + }, + { + "name": "PlatformCurveRule", + "docs": [ + "The platform launch parameter rule for one global config.", + "", + "The existence of this account is only checked when the platform restricts the launch", + "parameters, see `PlatformConfig::restrict_curve_param`. It narrows the launch parameters", + "down from what the global config allows, it can never loosen them." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "version", + "docs": [ + "The layout version of the account" + ], + "type": "u8" + }, + { + "name": "platform_config", + "docs": [ + "The platform config the rule belongs to" + ], + "type": "pubkey" + }, + { + "name": "global_config", + "docs": [ + "The global config the rule applies to" + ], + "type": "pubkey" + }, + { + "name": "epoch", + "docs": [ + "The epoch of the last update" + ], + "type": "u64" + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u64", + 8 + ] + } + }, + { + "name": "groups", + "docs": [ + "The check groups, a launch is allowed when it satisfies any of them.", + "An empty list means the platform does not restrict the launch parameters of", + "this global config." + ], + "type": { + "vec": { + "defined": { + "name": "CurveRuleGroup" + } + } + } + } + ] + } + }, + { + "name": "PlatformParams", + "docs": [ + "Represents the parameters for initializing a platform config account", + "# Fields", + "* `migrate_nft_info` - The platform configures liquidity info during migration(Only support MigrateType::CPSWAP)", + "* `fee_rate` - Fee rate of the platform", + "* `name` - Name of the platform", + "* `web` - Website of the platform", + "* `img` - Image link of the platform", + "/// * `creator_fee_rate` - The fee rate charged by the creator for each transaction." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "migrate_nft_info", + "type": { + "defined": { + "name": "MigrateNftInfo" + } + } + }, + { + "name": "fee_rate", + "type": "u64" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "web", + "type": "string" + }, + { + "name": "img", + "type": "string" + }, + { + "name": "creator_fee_rate", + "type": "u64" + }, + { + "name": "platform_vesting_scale", + "type": "u64" + } + ] + } + }, + { + "name": "PoolCreateEvent", + "docs": [ + "Emitted when pool created" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_state", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "config", + "type": "pubkey" + }, + { + "name": "base_mint_param", + "type": { + "defined": { + "name": "MintParams" + } + } + }, + { + "name": "curve_param", + "type": { + "defined": { + "name": "CurveParams" + } + } + }, + { + "name": "vesting_param", + "type": { + "defined": { + "name": "VestingParams" + } + } + }, + { + "name": "amm_fee_on", + "type": { + "defined": { + "name": "AmmCreatorFeeOn" + } + } + } + ] + } + }, + { + "name": "PoolState", + "docs": [ + "Represents the state of a trading pool in the protocol", + "Stores all essential information about pool balances, fees, and configuration" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "Account update epoch" + ], + "type": "u64" + }, + { + "name": "auth_bump", + "docs": [ + "Bump seed used for PDA address derivation" + ], + "type": "u8" + }, + { + "name": "status", + "docs": [ + "Current status of the pool", + "* 0: Pool is funding", + "* 1: Pool funding is end, waiting for migration", + "* 2: Pool migration is done" + ], + "type": "u8" + }, + { + "name": "base_decimals", + "docs": [ + "Decimals of the pool base token" + ], + "type": "u8" + }, + { + "name": "quote_decimals", + "docs": [ + "Decimals of the pool quote token" + ], + "type": "u8" + }, + { + "name": "migrate_type", + "docs": [ + "Migrate to AMM or CpSwap, 0: amm\uff0c 1: cpswap" + ], + "type": "u8" + }, + { + "name": "supply", + "docs": [ + "Supply of the pool base token" + ], + "type": "u64" + }, + { + "name": "total_base_sell", + "docs": [ + "Total sell amount of the base token" + ], + "type": "u64" + }, + { + "name": "virtual_base", + "docs": [ + "For different curves, virtual_base and virtual_quote have different meanings", + "For constant product curve, virtual_base and virtual_quote are virtual liquidity, virtual_quote/virtual_base is the initial price", + "For linear price curve, virtual_base is the price slope parameter a, virtual_quote has no effect", + "For fixed price curve, virtual_quote/virtual_base is the initial price" + ], + "type": "u64" + }, + { + "name": "virtual_quote", + "type": "u64" + }, + { + "name": "real_base", + "docs": [ + "Actual base token amount in the pool", + "Represents the real tokens available for trading" + ], + "type": "u64" + }, + { + "name": "real_quote", + "docs": [ + "Actual quote token amount in the pool", + "Represents the real tokens available for trading" + ], + "type": "u64" + }, + { + "name": "total_quote_fund_raising", + "docs": [ + "The total quote fund raising of the pool" + ], + "type": "u64" + }, + { + "name": "quote_protocol_fee", + "docs": [ + "Accumulated trading fees in quote tokens", + "Can be collected by the protocol fee owner" + ], + "type": "u64" + }, + { + "name": "platform_fee", + "docs": [ + "Accumulated platform fees in quote tokens", + "Can be collected by the platform wallet stored in platform config" + ], + "type": "u64" + }, + { + "name": "migrate_fee", + "docs": [ + "The fee of migrate to amm" + ], + "type": "u64" + }, + { + "name": "vesting_schedule", + "docs": [ + "Vesting schedule for the base token" + ], + "type": { + "defined": { + "name": "VestingSchedule" + } + } + }, + { + "name": "global_config", + "docs": [ + "Public key of the global configuration account", + "Contains protocol-wide settings this pool adheres to" + ], + "type": "pubkey" + }, + { + "name": "platform_config", + "docs": [ + "Public key of the platform configuration account", + "Contains platform-wide settings this pool adheres to" + ], + "type": "pubkey" + }, + { + "name": "base_mint", + "docs": [ + "Public key of the base mint address" + ], + "type": "pubkey" + }, + { + "name": "quote_mint", + "docs": [ + "Public key of the quote mint address" + ], + "type": "pubkey" + }, + { + "name": "base_vault", + "docs": [ + "Public key of the base token vault", + "Holds the actual base tokens owned by the pool" + ], + "type": "pubkey" + }, + { + "name": "quote_vault", + "docs": [ + "Public key of the quote token vault", + "Holds the actual quote tokens owned by the pool" + ], + "type": "pubkey" + }, + { + "name": "creator", + "docs": [ + "The creator of base token" + ], + "type": "pubkey" + }, + { + "name": "token_program_flag", + "docs": [ + "token program bits", + "bit0: base token program flag", + "0: spl_token_program", + "1: token_program_2022", + "", + "bit1: quote token program flag", + "0: spl_token_program", + "1: token_program_2022" + ], + "type": "u8" + }, + { + "name": "amm_creator_fee_on", + "docs": [ + "migrate to cpmm, creator fee on quote token or both token" + ], + "type": { + "defined": { + "name": "AmmCreatorFeeOn" + } + } + }, + { + "name": "platform_vesting_share", + "docs": [ + "platform vesting token amount" + ], + "type": "u64" + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u8", + 54 + ] + } + } + ] + } + }, + { + "name": "PoolStatus", + "docs": [ + "Represents the different states a pool can be in", + "* Fund - Initial state where pool is accepting funds", + "* Migrate - Pool funding has ended and waiting for migration", + "* Trade - Pool migration is complete and amm trading is enabled" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Fund" + }, + { + "name": "Migrate" + }, + { + "name": "Trade" + } + ] + } + }, + { + "name": "TradeDirection", + "docs": [ + "Specifies the direction of a trade in the bonding curve", + "This is important because curves can treat tokens differently through weights or offsets" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Buy" + }, + { + "name": "Sell" + } + ] + } + }, + { + "name": "TradeEvent", + "docs": [ + "Emitted when trade process" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_state", + "type": "pubkey" + }, + { + "name": "total_base_sell", + "type": "u64" + }, + { + "name": "virtual_base", + "type": "u64" + }, + { + "name": "virtual_quote", + "type": "u64" + }, + { + "name": "real_base_before", + "type": "u64" + }, + { + "name": "real_quote_before", + "type": "u64" + }, + { + "name": "real_base_after", + "type": "u64" + }, + { + "name": "real_quote_after", + "type": "u64" + }, + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "amount_out", + "type": "u64" + }, + { + "name": "protocol_fee", + "type": "u64" + }, + { + "name": "platform_fee", + "type": "u64" + }, + { + "name": "creator_fee", + "type": "u64" + }, + { + "name": "share_fee", + "type": "u64" + }, + { + "name": "trade_direction", + "type": { + "defined": { + "name": "TradeDirection" + } + } + }, + { + "name": "pool_status", + "type": { + "defined": { + "name": "PoolStatus" + } + } + }, + { + "name": "exact_in", + "type": "bool" + } + ] + } + }, + { + "name": "TransferFeeExtensionParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "transfer_fee_basis_points", + "docs": [ + "denominator is 10000, currently, this value cannot exceed 5%, which is 500." + ], + "type": "u16" + }, + { + "name": "maximum_fee", + "docs": [ + "Maximum fee on each transfers, the value must exceed supply * transfer_fee_basis_points / 10000" + ], + "type": "u64" + } + ] + } + }, + { + "name": "VestingParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "total_locked_amount", + "type": "u64" + }, + { + "name": "cliff_period", + "type": "u64" + }, + { + "name": "unlock_period", + "type": "u64" + } + ] + } + }, + { + "name": "VestingRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "Account update epoch" + ], + "type": "u64" + }, + { + "name": "pool", + "docs": [ + "The pool state account" + ], + "type": "pubkey" + }, + { + "name": "beneficiary", + "docs": [ + "The beneficiary of the vesting account" + ], + "type": "pubkey" + }, + { + "name": "claimed_amount", + "docs": [ + "The amount of tokens claimed" + ], + "type": "u64" + }, + { + "name": "token_share_amount", + "docs": [ + "The share amount of the token to be vested" + ], + "type": "u64" + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u64", + 8 + ] + } + } + ] + } + }, + { + "name": "VestingSchedule", + "type": { + "kind": "struct", + "fields": [ + { + "name": "total_locked_amount", + "type": "u64" + }, + { + "name": "cliff_period", + "type": "u64" + }, + { + "name": "unlock_period", + "type": "u64" + }, + { + "name": "start_time", + "type": "u64" + }, + { + "name": "allocated_share_amount", + "docs": [ + "Total allocated share amount of the base token, not greater than total_locked_amount" + ], + "type": "u64" + } + ] + } + } + ] +} diff --git a/src/pumpfun_cli/commands/info.py b/src/pumpfun_cli/commands/info.py index 1ed2f7d..4dde602 100644 --- a/src/pumpfun_cli/commands/info.py +++ b/src/pumpfun_cli/commands/info.py @@ -31,11 +31,15 @@ def info( error(result["message"]) if not render(result, state.json_mode): typer.echo(f"Mint: {result['mint']}") + if result.get("venue") == "launchlab": + typer.echo( + f"Launchpad: {result['platform'] or 'LaunchLab'} (Raydium LaunchLab)" + ) symbol = result["quote_symbol"] typer.echo(f"Price: {result['price']:.10f} {symbol}") typer.echo(f"Bonding progress: {result['bonding_progress']}%") typer.echo(f"Graduated: {result['graduated']}") - if result["graduated"] and result.get("pool_address"): + if result["graduated"] and result.get("base_reserves") is not None: typer.echo(f"Pool: {result['pool_address']}") typer.echo(f"Base reserves: {result['base_reserves']:.2f}") typer.echo(f"Quote reserves: {result['quote_reserves']:.4f} {symbol}") @@ -44,11 +48,17 @@ def info( if result.get("pumpswap_warning"): typer.echo(f"Warning: {result['pumpswap_warning']}") typer.echo(f"Creator: {result['creator'] or '\u2014'}") - if result["is_mayhem_mode"]: + if result.get("is_mayhem_mode"): typer.echo("Mayhem mode: Yes") - if result["is_cashback_coin"]: + if result.get("is_cashback_coin"): typer.echo("Cashback: Yes") if result.get("is_holder_reward"): typer.echo("Holder rewards: Yes") if result.get("creator_fee_bps"): typer.echo(f"Creator fee: {result['creator_fee_bps']} bps") + if result.get("curve_fee_pct") is not None: + typer.echo(f"Curve fee: {result['curve_fee_pct']:g}%") + if result.get("transfer_fee_bps"): + typer.echo(f"Transfer fee: {result['transfer_fee_bps']} bps") + if result.get("migrate_type"): + typer.echo(f"Migrated to: {result['migrate_type']}") diff --git a/src/pumpfun_cli/commands/trade.py b/src/pumpfun_cli/commands/trade.py index 5ba4bdb..8f60c50 100644 --- a/src/pumpfun_cli/commands/trade.py +++ b/src/pumpfun_cli/commands/trade.py @@ -4,6 +4,7 @@ import typer +from pumpfun_cli.core.launchlab import buy_launchlab, sell_launchlab from pumpfun_cli.core.pumpswap import buy_pumpswap, sell_pumpswap from pumpfun_cli.core.trade import ( buy_token, @@ -22,6 +23,24 @@ from pumpfun_cli.output import error, render +def _route(curve, amm, launchlab, force_amm: bool, mint: str, call) -> dict: + """Trade where the coin lives: pump.fun's curve, then PumpSwap once it has + graduated, or a LaunchLab curve when pump.fun has no such coin. + + `call(fn)` runs one venue's core function with the command's arguments. + """ + if force_amm: + return call(amm) + result = call(curve) + if result.get("error") == "graduated": + return call(amm) + if result.get("error") == "not_found": + result = call(launchlab) + if result.get("error") == "not_found": + result["message"] = f"No pump.fun or LaunchLab coin found for {mint}." + return result + + def _get_overrides(ctx: typer.Context) -> dict: """Extract priority_fee and compute_units from global state.""" state = ctx.obj @@ -90,23 +109,14 @@ def buy( rpc, keyfile, password = _require_rpc_and_wallet(ctx) overrides = _get_overrides(ctx) try: - if force_amm: - result = asyncio.run( - buy_pumpswap( - rpc, - keyfile, - password, - mint, - amount, - slippage, - confirm=confirm, - dry_run=dry_run, - **overrides, - ) - ) - else: - result = asyncio.run( - buy_token( + result = _route( + buy_token, + buy_pumpswap, + buy_launchlab, + force_amm, + mint, + lambda fn: asyncio.run( + fn( rpc, keyfile, password, @@ -117,21 +127,8 @@ def buy( dry_run=dry_run, **overrides, ) - ) - if result.get("error") == "graduated": - result = asyncio.run( - buy_pumpswap( - rpc, - keyfile, - password, - mint, - amount, - slippage, - confirm=confirm, - dry_run=dry_run, - **overrides, - ) - ) + ), + ) except (ValueError, RuntimeError) as exc: error(str(exc)) if result.get("dry_run"): @@ -183,9 +180,14 @@ def sell( rpc, keyfile, password = _require_rpc_and_wallet(ctx) overrides = _get_overrides(ctx) try: - if force_amm: - result = asyncio.run( - sell_pumpswap( + result = _route( + sell_token, + sell_pumpswap, + sell_launchlab, + force_amm, + mint, + lambda fn: asyncio.run( + fn( rpc, keyfile, password, @@ -196,35 +198,8 @@ def sell( dry_run=dry_run, **overrides, ) - ) - else: - result = asyncio.run( - sell_token( - rpc, - keyfile, - password, - mint, - amount, - slippage, - confirm=confirm, - dry_run=dry_run, - **overrides, - ) - ) - if result.get("error") == "graduated": - result = asyncio.run( - sell_pumpswap( - rpc, - keyfile, - password, - mint, - amount, - slippage, - confirm=confirm, - dry_run=dry_run, - **overrides, - ) - ) + ), + ) except (ValueError, RuntimeError) as exc: error(str(exc)) if result.get("dry_run"): diff --git a/src/pumpfun_cli/core/info.py b/src/pumpfun_cli/core/info.py index 4f6103e..a232e43 100644 --- a/src/pumpfun_cli/core/info.py +++ b/src/pumpfun_cli/core/info.py @@ -1,5 +1,6 @@ """Fetch on-chain token info.""" +from pumpfun_cli.core.launchlab import get_launchlab_info from pumpfun_cli.core.pumpswap import get_pumpswap_info from pumpfun_cli.core.validate import invalid_pubkey_error, parse_pubkey from pumpfun_cli.protocol.address import derive_bonding_curve @@ -22,7 +23,10 @@ async def get_token_info(rpc_url: str, mint_str: str, rpc_timeout: float = 30.0) try: resp = await client.get_account_info(bonding_curve) if not resp.value: - return {"error": "not_found", "message": f"No bonding curve found for {mint_str}"} + result = await get_launchlab_info(rpc_url, mint_str, rpc_timeout=rpc_timeout) + if result.get("error") == "not_found": + result["message"] = f"No pump.fun or LaunchLab coin found for {mint_str}." + return result state = parse_bonding_curve(bytes(resp.value.data)) quote_mint = state["quote_mint"] _, quote_decimals = await get_mint_info(client, quote_mint) @@ -32,6 +36,7 @@ async def get_token_info(rpc_url: str, mint_str: str, rpc_timeout: float = 30.0) result = { "mint": mint_str, + "venue": "pumpswap" if graduated else "bonding_curve", "bonding_curve": str(bonding_curve), "quote_mint": str(quote_mint), "quote_symbol": quote_symbol(quote_mint), diff --git a/src/pumpfun_cli/core/launchlab.py b/src/pumpfun_cli/core/launchlab.py new file mode 100644 index 0000000..890d444 --- /dev/null +++ b/src/pumpfun_cli/core/launchlab.py @@ -0,0 +1,465 @@ +"""Raydium LaunchLab bonding-curve trades and info, for any launchpad on it. + +A coin's pool is keyed by its base and quote mints. The SOL pool is derived +first; a coin priced in anything else is found by searching LaunchLab's pools +for its mint. Amounts are in the pool's quote asset. +""" + +from pathlib import Path + +from solana.rpc.models import MemcmpOpts +from solders.pubkey import Pubkey +from spl.token.instructions import create_idempotent_associated_token_account + +from pumpfun_cli.core.amounts import ( + amount_fields, + confirmed_token_delta, + received_fields, + to_raw, +) +from pumpfun_cli.core.trade import DEFAULT_PRIORITY_FEE, _handle_tx_error +from pumpfun_cli.core.validate import invalid_pubkey_error, parse_pubkey +from pumpfun_cli.crypto import decrypt_keypair +from pumpfun_cli.protocol.address import find_ata +from pumpfun_cli.protocol.client import RpcClient, TransactionFailedError +from pumpfun_cli.protocol.contracts import ( + ATA_RENT_LAMPORTS, + LAMPORTS_PER_SOL, + SOL_RENT_EXEMPT_MIN, + WSOL_MINT, +) +from pumpfun_cli.protocol.instructions import quote_settlement_account +from pumpfun_cli.protocol.launchlab import ( + CONSTANT_PRODUCT_CURVE, + FEE_RATE_DENOMINATOR, + LAUNCHLAB_PROGRAM, + LAUNCHLAB_SLIPPAGE_ERROR_CODES, + MIGRATE_TO_CPMM, + POOL_BASE_MINT_OFFSET, + POOL_STATE_SIZE, + STATUS_TRADING, + build_launchlab_swap_instruction, + buy_amount_out, + curve_type, + derive_launchlab_pool, + mint_transfer_fee, + parse_fee_rate, + parse_pool_state, + platform_name, + sell_amount_out, + spot_price, + transfer_fee_bps, +) +from pumpfun_cli.protocol.quote import is_sol, quote_symbol + +LAUNCHLAB_COMPUTE_UNITS = 150_000 + + +async def find_launchlab_pool(client: RpcClient, mint: Pubkey) -> tuple[Pubkey, dict] | None: + """The coin's LaunchLab pool and its decoded state, or None if it has none. + + Raises: + RuntimeError: If the pool search fails, e.g. on an RPC that refuses + getProgramAccounts + """ + address = derive_launchlab_pool(mint, WSOL_MINT) + resp = await client.get_account_info(address) + if resp.value and resp.value.owner == LAUNCHLAB_PROGRAM: + return address, parse_pool_state(bytes(resp.value.data)) + try: + found = await client.get_program_accounts( + LAUNCHLAB_PROGRAM, + [POOL_STATE_SIZE, MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=str(mint))], + ) + except Exception as exc: # noqa: BLE001 - any RPC refusal means the same + raise RuntimeError( + f"No SOL-priced LaunchLab pool for {mint}, and pools in other quote assets" + f" could not be searched ({type(exc).__name__}): this RPC may not serve" + " getProgramAccounts" + ) from None + for item in found: + return item.pubkey, parse_pool_state(bytes(item.account.data)) + return None + + +async def _load_market(client: RpcClient, mint: Pubkey) -> dict | None: + """The pool with everything a trade or quote needs, or None without a pool. + + Adds `address`, `fee_rate` (parts per million), `transfer_fee` + ((bps, max) of the coin), `platform`, and both token programs. + """ + found = await find_launchlab_pool(client, mint) + if found is None: + return None + address, pool = found + global_config, platform_config, base_mint, quote_mint = await client.get_multiple_accounts( + [pool["global_config"], pool["platform_config"], mint, pool["quote_mint"]] + ) + if None in (global_config, platform_config, base_mint, quote_mint): + raise RuntimeError(f"LaunchLab pool {address}: config or mint accounts unreadable") + if curve_type(bytes(global_config.data)) != CONSTANT_PRODUCT_CURVE: + raise RuntimeError(f"LaunchLab pool {address} uses a curve type this CLI does not price") + return { + **pool, + "address": address, + "fee_rate": parse_fee_rate(bytes(global_config.data), bytes(platform_config.data)), + "transfer_fee": mint_transfer_fee(bytes(base_mint.data)), + "platform": platform_name(bytes(platform_config.data)), + "base_token_program": base_mint.owner, + "quote_token_program": quote_mint.owner, + } + + +async def buy_shortfall( + client: RpcClient, + user: Pubkey, + quote_mint: Pubkey, + quote_token_program: Pubkey, + quote_decimals: int, + amount_raw: int, + fee_lamports: int, +) -> dict | None: + """What a buy spending `amount_raw` of the quote is short of, or None. + + SOL covers the fee, the coin's token account and, on a SOL pool, the spend + plus the throwaway WSOL account until it closes. A non-SOL quote must + already be in the wallet's account for it. Returns `message` plus the + available and required amounts of whichever asset is short. + """ + sol = is_sol(quote_mint) + required = ( + (amount_raw if sol else 0) + fee_lamports + 2 * ATA_RENT_LAMPORTS + SOL_RENT_EXEMPT_MIN + ) + balance = await client.get_balance(user) + if balance < required: + return { + "message": f"Wallet has {balance / LAMPORTS_PER_SOL:.6f} SOL" + f" but trade requires ~{required / LAMPORTS_PER_SOL:.6f} SOL", + "available_sol": balance / LAMPORTS_PER_SOL, + "required_sol": required / LAMPORTS_PER_SOL, + } + if sol: + return None + held = await client.get_token_account_balance(find_ata(user, quote_mint, quote_token_program)) + if held >= amount_raw: + return None + symbol = quote_symbol(quote_mint) + return { + "message": f"Wallet holds {held / 10**quote_decimals} {symbol}" + f" but the buy spends {amount_raw / 10**quote_decimals} {symbol}", + **amount_fields("available_quote", held, quote_decimals), + **amount_fields("required_quote", amount_raw, quote_decimals), + } + + +def _not_found(mint_str: str) -> dict: + return {"error": "not_found", "message": f"No LaunchLab pool for {mint_str}"} + + +def _graduated(market: dict) -> dict: + where = "Raydium CPMM" if market["migrate_type"] == MIGRATE_TO_CPMM else "Raydium AMM v4" + return { + "error": "graduated", + "message": f"Token has graduated from its LaunchLab curve to {where}.", + "migrate_type": "cpmm" if market["migrate_type"] == MIGRATE_TO_CPMM else "amm_v4", + } + + +def _market_fields(market: dict) -> dict: + return { + "venue": "launchlab", + "platform": market["platform"], + "quote_mint": str(market["quote_mint"]), + "quote_symbol": quote_symbol(market["quote_mint"]), + } + + +async def buy_launchlab( + rpc_url: str, + keystore_path: str, + password: str, + mint_str: str, + amount: float, + slippage: int = 15, + confirm: bool = False, + priority_fee: int | None = None, + compute_units: int | None = None, + dry_run: bool = False, +) -> dict: + """Spend exactly `amount` of the pool's quote asset on a LaunchLab curve (buy_exact_in).""" + keypair = decrypt_keypair(Path(keystore_path), password) + mint = parse_pubkey(mint_str, "mint address") + if mint is None: + return invalid_pubkey_error(mint_str, "mint address") + client = RpcClient(rpc_url) + try: + market = await _load_market(client, mint) + if market is None: + return _not_found(mint_str) + if market["status"] != STATUS_TRADING: + return _graduated(market) + + quote_mint = market["quote_mint"] + base_decimals, quote_decimals = market["base_decimals"], market["quote_decimals"] + user = keypair.pubkey() + + amount_raw = to_raw(amount, quote_decimals) + tokens_out = buy_amount_out(market, amount_raw, market["fee_rate"], market["transfer_fee"]) + min_tokens_out = max(1, tokens_out * (100 - slippage) // 100) + + eff_priority_fee = priority_fee if priority_fee is not None else DEFAULT_PRIORITY_FEE + eff_compute_units = compute_units if compute_units is not None else LAUNCHLAB_COMPUTE_UNITS + shortfall = await buy_shortfall( + client, + user, + quote_mint, + market["quote_token_program"], + quote_decimals, + amount_raw, + eff_priority_fee * eff_compute_units // 1_000_000, + ) + + if dry_run: + spot = spot_price(market) + expected = tokens_out / 10**base_decimals + effective_price = amount / expected if expected > 0 else 0 + result = { + "dry_run": True, + "action": "buy", + "mint": mint_str, + **_market_fields(market), + **amount_fields("quote_in", amount_raw, quote_decimals), + **amount_fields("expected_tokens", tokens_out, base_decimals), + "effective_price": effective_price, + "spot_price": spot, + "price_impact_pct": round((effective_price - spot) / spot * 100, 4) + if spot > 0 + else 0, + **amount_fields("min_tokens_out", min_tokens_out, base_decimals), + "slippage_pct": slippage, + } + if shortfall: + result["balance_warning"] = shortfall["message"] + return result + + if shortfall: + return { + **shortfall, + "error": "insufficient_balance", + "message": f"Insufficient balance. {shortfall['message']}.", + } + + base_program = market["base_token_program"] + quote_account, open_ixs, close_ixs = quote_settlement_account( + user, quote_mint, market["quote_token_program"], funding=amount_raw + ) + ixs = [ + create_idempotent_associated_token_account(user, user, mint, base_program), + *open_ixs, + build_launchlab_swap_instruction( + buy=True, + pool_address=market["address"], + pool=market, + user=user, + user_base_token=find_ata(user, mint, base_program), + user_quote_token=quote_account, + base_token_program=base_program, + quote_token_program=market["quote_token_program"], + amount_in=amount_raw, + minimum_amount_out=min_tokens_out, + ), + *close_ixs, + ] + try: + sig = await client.send_tx( + ixs, + [keypair], + confirm=confirm, + priority_fee=eff_priority_fee, + compute_units=eff_compute_units, + ) + except TransactionFailedError as exc: + return _handle_tx_error(exc, LAUNCHLAB_SLIPPAGE_ERROR_CODES) + received = await confirmed_token_delta(client, sig, user, mint) if confirm else None + result = { + "action": "buy", + "mint": mint_str, + **_market_fields(market), + **amount_fields("quote_spent", amount_raw, quote_decimals), + **received_fields("tokens_received", received, tokens_out, base_decimals), + "signature": sig, + "explorer": f"https://solscan.io/tx/{sig}", + } + if confirm: + result["confirmed"] = True + return result + except (RuntimeError, ValueError) as exc: + return {"error": "launchlab_error", "message": str(exc)} + finally: + await client.close() + + +async def sell_launchlab( + rpc_url: str, + keystore_path: str, + password: str, + mint_str: str, + amount_str: str, + slippage: int = 15, + confirm: bool = False, + priority_fee: int | None = None, + compute_units: int | None = None, + dry_run: bool = False, +) -> dict: + """Sell tokens into a LaunchLab curve (sell_exact_in), paid in its quote asset.""" + keypair = decrypt_keypair(Path(keystore_path), password) + mint = parse_pubkey(mint_str, "mint address") + if mint is None: + return invalid_pubkey_error(mint_str, "mint address") + client = RpcClient(rpc_url) + try: + market = await _load_market(client, mint) + if market is None: + return _not_found(mint_str) + if market["status"] != STATUS_TRADING: + return _graduated(market) + + quote_mint = market["quote_mint"] + base_decimals, quote_decimals = market["base_decimals"], market["quote_decimals"] + base_program = market["base_token_program"] + user = keypair.pubkey() + user_ata = find_ata(user, mint, base_program) + + held = await client.get_token_account_balance(user_ata) + if amount_str.lower() == "all": + token_amount = held + else: + token_amount = to_raw(amount_str, base_decimals) + if held < token_amount: + return { + "error": "insufficient_balance", + "message": ( + f"Insufficient token balance. Have {held / 10**base_decimals:.6f}" + f" tokens, need {token_amount / 10**base_decimals:.6f} tokens." + ), + "available_tokens": held / 10**base_decimals, + "required_tokens": token_amount / 10**base_decimals, + } + if token_amount <= 0: + return {"error": "no_tokens", "message": "No tokens to sell."} + + quote_out = sell_amount_out( + market, token_amount, market["fee_rate"], market["transfer_fee"] + ) + if quote_out <= 0: + # The program refuses a sell that pays nothing. + return { + "error": "dust", + "message": "Too few tokens: the curve would pay nothing for them.", + } + min_quote_out = max(1, quote_out * (100 - slippage) // 100) + + if dry_run: + spot = spot_price(market) + tokens_human = token_amount / 10**base_decimals + effective_price = quote_out / 10**quote_decimals / tokens_human + return { + "dry_run": True, + "action": "sell", + "mint": mint_str, + **_market_fields(market), + **amount_fields("tokens_in", token_amount, base_decimals), + **amount_fields("expected_quote", quote_out, quote_decimals), + "effective_price": effective_price, + "spot_price": spot, + "price_impact_pct": round((effective_price - spot) / spot * 100, 4) + if spot > 0 + else 0, + **amount_fields("min_quote_out", min_quote_out, quote_decimals), + "slippage_pct": slippage, + } + + quote_account, open_ixs, close_ixs = quote_settlement_account( + user, quote_mint, market["quote_token_program"] + ) + ixs = [ + *open_ixs, + build_launchlab_swap_instruction( + buy=False, + pool_address=market["address"], + pool=market, + user=user, + user_base_token=user_ata, + user_quote_token=quote_account, + base_token_program=base_program, + quote_token_program=market["quote_token_program"], + amount_in=token_amount, + minimum_amount_out=min_quote_out, + ), + *close_ixs, + ] + try: + sig = await client.send_tx( + ixs, + [keypair], + confirm=confirm, + priority_fee=priority_fee if priority_fee is not None else DEFAULT_PRIORITY_FEE, + compute_units=compute_units + if compute_units is not None + else LAUNCHLAB_COMPUTE_UNITS, + ) + except TransactionFailedError as exc: + return _handle_tx_error(exc, LAUNCHLAB_SLIPPAGE_ERROR_CODES) + result = { + "action": "sell", + "mint": mint_str, + **_market_fields(market), + **amount_fields("tokens_sold", token_amount, base_decimals), + "quote_received": quote_out / 10**quote_decimals, + "signature": sig, + "explorer": f"https://solscan.io/tx/{sig}", + } + if confirm: + result["confirmed"] = True + return result + except (RuntimeError, ValueError) as exc: + return {"error": "launchlab_error", "message": str(exc)} + finally: + await client.close() + + +async def get_launchlab_info(rpc_url: str, mint_str: str, rpc_timeout: float = 30.0) -> dict: + """On-chain info for a LaunchLab coin: price, progress, reserves, fees.""" + mint = parse_pubkey(mint_str, "mint address") + if mint is None: + return invalid_pubkey_error(mint_str, "mint address") + client = RpcClient(rpc_url, timeout=rpc_timeout) + try: + market = await _load_market(client, mint) + if market is None: + return _not_found(mint_str) + quote_decimals = market["quote_decimals"] + progress = ( + market["real_base"] / market["total_base_sell"] if market["total_base_sell"] else 0.0 + ) + graduated = market["status"] != STATUS_TRADING + result = { + "mint": mint_str, + **_market_fields(market), + "platform_config": str(market["platform_config"]), + "launchlab_pool": str(market["address"]), + "price": spot_price(market), + "bonding_progress": round(min(progress, 1.0) * 100, 1), + "graduated": graduated, + "real_quote_reserves": market["real_quote"] / 10**quote_decimals, + "creator": str(market["creator"]), + "curve_fee_pct": market["fee_rate"] * 100 / FEE_RATE_DENOMINATOR, + "transfer_fee_bps": transfer_fee_bps(market["transfer_fee"]), + } + if graduated: + result["migrate_type"] = _graduated(market)["migrate_type"] + return result + except (RuntimeError, ValueError) as exc: + return {"error": "launchlab_error", "message": str(exc)} + finally: + await client.close() diff --git a/src/pumpfun_cli/protocol/launchlab.py b/src/pumpfun_cli/protocol/launchlab.py new file mode 100644 index 0000000..1291527 --- /dev/null +++ b/src/pumpfun_cli/protocol/launchlab.py @@ -0,0 +1,296 @@ +"""Raydium LaunchLab: addresses, account layouts, curve math and swap instructions. + +LaunchLab is one program serving many launchpads (letsbonk.fun, StonkFun and +others); a launchpad is a set of platform configs its pools are created under. +Nothing here depends on which: every account a trade needs comes from the pool. + +All amounts are raw integer units with the program's own rounding: fees round +up, curve outputs round down. +""" + +import struct + +from solders.instruction import AccountMeta, Instruction +from solders.pubkey import Pubkey + +from pumpfun_cli.protocol.contracts import SYSTEM_PROGRAM + +LAUNCHLAB_PROGRAM = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") + +BUY_EXACT_IN_DISCRIMINATOR = bytes([250, 234, 13, 123, 213, 156, 19, 236]) +SELL_EXACT_IN_DISCRIMINATOR = bytes([149, 39, 222, 155, 211, 124, 152, 26]) +POOL_STATE_DISCRIMINATOR = bytes([247, 237, 227, 245, 215, 195, 222, 70]) + +# ExceededSlippage. +LAUNCHLAB_SLIPPAGE_ERROR_CODES: set[int] = {6004} + +# A trade sends no front-end share fee. +SHARE_FEE_RATE = 0 + +# Fee rates on GlobalConfig and PlatformConfig are parts per million. +FEE_RATE_DENOMINATOR = 1_000_000 +_BASIS_POINTS = 10_000 + +# PoolState: discriminator, epoch, five u8 (auth_bump, status, base_decimals, +# quote_decimals, migrate_type), ten u64 amounts, a five-u64 vesting schedule, +# then seven pubkeys from global_config on. +POOL_STATE_SIZE = 429 +_POOL_U8_OFFSET = 16 +_POOL_AMOUNTS_OFFSET = 21 +_POOL_KEYS_OFFSET = 8 + 8 + 5 + 10 * 8 + 5 * 8 +_POOL_KEYS = ( + "global_config", + "platform_config", + "base_mint", + "quote_mint", + "base_vault", + "quote_vault", + "creator", +) +POOL_BASE_MINT_OFFSET = _POOL_KEYS_OFFSET + 2 * 32 + +# GlobalConfig: discriminator, epoch u64, curve_type u8, index u16, migrate_fee u64. +_GLOBAL_CURVE_TYPE_OFFSET = 16 +_GLOBAL_TRADE_FEE_RATE_OFFSET = 8 + 8 + 1 + 2 + 8 +# The curve math here is for constant-product curves; LaunchLab also defines +# fixed-price (1) and linear (2) ones. +CONSTANT_PRODUCT_CURVE = 0 +# PlatformConfig: discriminator, epoch, two wallets, three u64 scales, fee_rate, +# name[64], web[256], img[256], cpswap_config, creator_fee_rate. +_PLATFORM_FEE_RATE_OFFSET = 8 + 8 + 32 + 32 + 3 * 8 +_PLATFORM_NAME_OFFSET = _PLATFORM_FEE_RATE_OFFSET + 8 +PLATFORM_CPSWAP_CONFIG_OFFSET = _PLATFORM_NAME_OFFSET + 64 + 256 + 256 +_PLATFORM_CREATOR_FEE_RATE_OFFSET = PLATFORM_CPSWAP_CONFIG_OFFSET + 32 + +# Token-2022 mint: 82-byte base mint, padding to 165, one account-type byte, +# then type-length-value extensions. TransferFeeConfig holds two authorities +# and a withheld amount (72 bytes), then the older and newer fees, each +# (epoch u64, maximum_fee u64, basis_points u16). +_EXTENSIONS_OFFSET = 166 +_TRANSFER_FEE_CONFIG = 1 +_OLDER_FEE_OFFSET = 72 +_NEWER_FEE_OFFSET = 90 + +# Pool status: 0 trades on the curve, 1 waits for migration, 2 has migrated. +STATUS_TRADING = 0 +# PoolState.migrate_type: 0 migrates to Raydium AMM v4, 1 to CPMM. +MIGRATE_TO_CPMM = 1 + + +def _pda(*seeds: bytes) -> Pubkey: + return Pubkey.find_program_address(list(seeds), LAUNCHLAB_PROGRAM)[0] + + +def derive_launchlab_pool(base_mint: Pubkey, quote_mint: Pubkey) -> Pubkey: + """The pool for a base/quote pair.""" + return _pda(b"pool", bytes(base_mint), bytes(quote_mint)) + + +def derive_launchlab_authority() -> Pubkey: + """The PDA that signs for every pool vault.""" + return _pda(b"vault_auth_seed") + + +def derive_launchlab_event_authority() -> Pubkey: + return _pda(b"__event_authority") + + +def derive_platform_fee_vault(platform_config: Pubkey, quote_mint: Pubkey) -> Pubkey: + """Where a platform's share of curve fees accrues, per quote mint.""" + return _pda(bytes(platform_config), bytes(quote_mint)) + + +def derive_creator_fee_vault(creator: Pubkey, quote_mint: Pubkey) -> Pubkey: + """Where a creator's share of curve fees accrues, per quote mint.""" + return _pda(bytes(creator), bytes(quote_mint)) + + +def parse_pool_state(data: bytes) -> dict: + """Decode a PoolState account. + + Raises: + ValueError: If the data is not a PoolState + """ + if len(data) < POOL_STATE_SIZE or data[:8] != POOL_STATE_DISCRIMINATOR: + raise ValueError("Not a LaunchLab pool account") # noqa: TRY003 + status, base_decimals, quote_decimals, migrate_type = data[ + _POOL_U8_OFFSET + 1 : _POOL_U8_OFFSET + 5 + ] + ( + supply, + total_base_sell, + virtual_base, + virtual_quote, + real_base, + real_quote, + total_quote_fund_raising, + ) = struct.unpack_from("<7Q", data, _POOL_AMOUNTS_OFFSET) + pool = { + "status": status, + "base_decimals": base_decimals, + "quote_decimals": quote_decimals, + "migrate_type": migrate_type, + "supply": supply, + "total_base_sell": total_base_sell, + "virtual_base": virtual_base, + "virtual_quote": virtual_quote, + "real_base": real_base, + "real_quote": real_quote, + "total_quote_fund_raising": total_quote_fund_raising, + } + for i, name in enumerate(_POOL_KEYS): + start = _POOL_KEYS_OFFSET + 32 * i + pool[name] = Pubkey.from_bytes(data[start : start + 32]) + return pool + + +def curve_type(global_config: bytes) -> int: + """GlobalConfig.curve_type: 0 constant product, 1 fixed price, 2 linear.""" + return global_config[_GLOBAL_CURVE_TYPE_OFFSET] + + +def parse_fee_rate(global_config: bytes, platform_config: bytes) -> int: + """Total curve fee in parts per million: trade, platform and creator fees.""" + return ( + struct.unpack_from(" str: + """The launchpad's name as its PlatformConfig stores it.""" + raw = platform_config[_PLATFORM_NAME_OFFSET : _PLATFORM_NAME_OFFSET + 64] + return raw.split(b"\0", 1)[0].decode("utf-8", errors="replace").strip() + + +def mint_transfer_fee(mint_data: bytes) -> tuple[tuple[int, int], ...]: + """A Token-2022 mint's scheduled transfer fees, each (basis points, maximum fee raw). + + Both the older and the newer fee: a change only applies two epochs after it + is set, and `withheld_fee` charges whichever withholds more, so an amount is + never overestimated without reading the epoch. Empty for a mint without + the extension. + """ + offset = _EXTENSIONS_OFFSET + while offset + 4 <= len(mint_data): + kind, length = struct.unpack_from(" int: + """The highest scheduled rate, for display.""" + return max((bps for bps, _ in fees), default=0) + + +def _ceil_div(numerator: int, denominator: int) -> int: + return -(-numerator // denominator) + + +def withheld_fee(amount: int, fees: tuple[tuple[int, int], ...]) -> int: + """What Token-2022 withholds from a transfer of `amount`: rounded up, capped, + and the larger of the scheduled fees.""" + return max( + (min(_ceil_div(amount * bps, _BASIS_POINTS), cap) for bps, cap in fees if bps), + default=0, + ) + + +def _reserves(pool: dict) -> tuple[int, int]: + """The (base, quote) reserves the curve trades against. + + The virtual reserves are fixed at launch; the real ones record every trade. + """ + return pool["virtual_base"] - pool["real_base"], pool["virtual_quote"] + pool["real_quote"] + + +def spot_price(pool: dict) -> float: + """Whole quote units per whole token at the current point on the curve.""" + base, quote = _reserves(pool) + if base <= 0 or quote <= 0: + return 0.0 + return (quote / 10 ** pool["quote_decimals"]) / (base / 10 ** pool["base_decimals"]) + + +def buy_amount_out( + pool: dict, amount_in: int, fee_rate: int, transfer_fee: tuple[tuple[int, int], ...] = () +) -> int: + """Raw tokens that reach the buyer for exactly `amount_in` raw quote. + + The curve fee comes off the input, the rest trades on the curve, and the + coin's transfer fee is withheld on delivery. + """ + net_in = amount_in - _ceil_div(amount_in * fee_rate, FEE_RATE_DENOMINATOR) + base, quote = _reserves(pool) + out = base * net_in // (quote + net_in) + return out - withheld_fee(out, transfer_fee) + + +def sell_amount_out( + pool: dict, amount_in: int, fee_rate: int, transfer_fee: tuple[tuple[int, int], ...] = () +) -> int: + """Raw quote paid for sending `amount_in` raw tokens. + + The transfer fee is withheld on the way into the pool, the curve trades on + what arrives, and the curve fee comes off the quote. + """ + arrived = amount_in - withheld_fee(amount_in, transfer_fee) + base, quote = _reserves(pool) + gross = quote * arrived // (base + arrived) + return gross - _ceil_div(gross * fee_rate, FEE_RATE_DENOMINATOR) + + +def build_launchlab_swap_instruction( + *, + buy: bool, + pool_address: Pubkey, + pool: dict, + user: Pubkey, + user_base_token: Pubkey, + user_quote_token: Pubkey, + base_token_program: Pubkey, + quote_token_program: Pubkey, + amount_in: int, + minimum_amount_out: int, +) -> Instruction: + """buy_exact_in (spend exactly `amount_in` quote) or sell_exact_in. + + The IDL's 15 accounts, then three the program reads positionally: the + system program and the platform and creator fee vaults. + """ + quote_mint = pool["quote_mint"] + + def meta(pubkey: Pubkey, writable: bool = False, signer: bool = False) -> AccountMeta: + return AccountMeta(pubkey, is_signer=signer, is_writable=writable) + + accounts = [ + meta(user, writable=True, signer=True), + meta(derive_launchlab_authority()), + meta(pool["global_config"]), + meta(pool["platform_config"]), + meta(pool_address, writable=True), + meta(user_base_token, writable=True), + meta(user_quote_token, writable=True), + meta(pool["base_vault"], writable=True), + meta(pool["quote_vault"], writable=True), + meta(pool["base_mint"]), + meta(quote_mint), + meta(base_token_program), + meta(quote_token_program), + meta(derive_launchlab_event_authority()), + meta(LAUNCHLAB_PROGRAM), + meta(SYSTEM_PROGRAM), + meta(derive_platform_fee_vault(pool["platform_config"], quote_mint), writable=True), + meta(derive_creator_fee_vault(pool["creator"], quote_mint), writable=True), + ] + discriminator = BUY_EXACT_IN_DISCRIMINATOR if buy else SELL_EXACT_IN_DISCRIMINATOR + data = discriminator + struct.pack(" int: async def get_token_account_balance(self, account: Pubkey) -> int: return self.token_balances.get(account, 0) + async def get_program_accounts(self, program_id: Pubkey, filters: list): + """Accounts owned by `program_id` that pass every dataSize/memcmp filter.""" + + def passes(data: bytes) -> bool: + for f in filters: + if isinstance(f, int): + if len(data) != f: + return False + elif data[f.offset : f.offset + 32] != bytes(Pubkey.from_string(f.bytes)): + return False + return True + + return [ + SimpleNamespace(pubkey=address, account=account) + for address, account in self.accounts.items() + if account.owner == program_id and passes(account.data) + ] + async def send_tx( self, instructions, signers, compute_units=100_000, priority_fee=200_000, confirm=False ): @@ -278,3 +299,21 @@ def add_pool( def add_quote_control(client: FakeClient, entries: dict[Pubkey, int]) -> None: client.add(find_quote_control(), build_quote_control_data(entries), owner=Pubkey.new_unique()) + + +FIXTURES = Path(__file__).parents[1] / "fixtures" + + +def add_launchlab_fixture(client: FakeClient, name: str) -> dict: + """Load a LaunchLab pool, its configs and mints as captured from mainnet. + + Returns each account's address by role (pool, base_mint, quote_mint, ...). + """ + bundle = json.loads((FIXTURES / name).read_text()) + for account in bundle.values(): + client.add( + Pubkey.from_string(account["address"]), + base64.b64decode(account["data"]), + owner=Pubkey.from_string(account["owner"]), + ) + return {role: Pubkey.from_string(account["address"]) for role, account in bundle.items()} diff --git a/tests/test_core/test_info.py b/tests/test_core/test_info.py index 42b32c3..4f09210 100644 --- a/tests/test_core/test_info.py +++ b/tests/test_core/test_info.py @@ -22,12 +22,13 @@ def _patched(client): return ( patch("pumpfun_cli.core.info.RpcClient", return_value=client), patch("pumpfun_cli.core.pumpswap.RpcClient", return_value=client), + patch("pumpfun_cli.core.launchlab.RpcClient", return_value=client), ) async def _info(client, mint=MINT_STR): - a, b = _patched(client) - with a, b: + a, b, c = _patched(client) + with a, b, c: return await get_token_info("http://rpc", mint) diff --git a/tests/test_core/test_launchlab.py b/tests/test_core/test_launchlab.py new file mode 100644 index 0000000..95b1b16 --- /dev/null +++ b/tests/test_core/test_launchlab.py @@ -0,0 +1,244 @@ +"""LaunchLab buys, sells and info, against mainnet pool accounts held in memory.""" + +from unittest.mock import patch + +import pytest +from solders.pubkey import Pubkey + +from pumpfun_cli.core.info import get_token_info +from pumpfun_cli.core.launchlab import buy_launchlab, get_launchlab_info, sell_launchlab +from pumpfun_cli.protocol.address import find_ata +from pumpfun_cli.protocol.client import TransactionFailedError +from pumpfun_cli.protocol.contracts import ( + ASSOCIATED_TOKEN_PROGRAM, + TOKEN_2022_PROGRAM, + TOKEN_PROGRAM, +) +from pumpfun_cli.protocol.launchlab import ( + BUY_EXACT_IN_DISCRIMINATOR, + LAUNCHLAB_PROGRAM, + SELL_EXACT_IN_DISCRIMINATOR, + buy_amount_out, + parse_pool_state, + sell_amount_out, +) +from tests.test_core.helpers import FakeClient, add_launchlab_fixture + +SOL_FIXTURE = "launchlab_sol_pool.json" +T22_FIXTURE = "launchlab_token2022_quote_pool.json" + + +def _patched(client: FakeClient): + return patch("pumpfun_cli.core.launchlab.RpcClient", return_value=client) + + +def _user(tmp_keystore) -> Pubkey: + from pathlib import Path + + from pumpfun_cli.crypto import decrypt_keypair + + return decrypt_keypair(Path(tmp_keystore), "testpass").pubkey() + + +def _chain(fixture: str) -> tuple[FakeClient, dict, dict]: + client = FakeClient() + roles = add_launchlab_fixture(client, fixture) + pool = parse_pool_state(client.accounts[roles["pool"]].data) + return client, roles, pool + + +def _swap(sent: dict): + return next(i for i in sent["instructions"] if i.program_id == LAUNCHLAB_PROGRAM) + + +def _u64s(ix) -> tuple[int, int]: + data = bytes(ix.data) + return int.from_bytes(data[8:16], "little"), int.from_bytes(data[16:24], "little") + + +async def test_buy_on_a_sol_pool_spends_exactly_the_amount(tmp_keystore): + client, roles, pool = _chain(SOL_FIXTURE) + with _patched(client): + result = await buy_launchlab( + "http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), 0.01, slippage=10 + ) + assert result["venue"] == "launchlab" + assert result["platform"] == "Spots.fun" + assert result["quote_symbol"] == "SOL" + swap = _swap(client.sent[0]) + assert bytes(swap.data[:8]) == BUY_EXACT_IN_DISCRIMINATOR + amount_in, min_out = _u64s(swap) + assert amount_in == 10_000_000 + expected = buy_amount_out(pool, amount_in, 17_500) + assert result["tokens_received"] == expected / 10**6 + assert min_out == expected * 90 // 100 + # Coin account, then the throwaway WSOL account opened and closed around the swap. + ixs = client.sent[0]["instructions"] + assert ixs[0].program_id == ASSOCIATED_TOKEN_PROGRAM + assert ixs[-1].program_id == TOKEN_PROGRAM + user = _user(tmp_keystore) + assert swap.accounts[5].pubkey == find_ata(user, roles["base_mint"], TOKEN_PROGRAM) + assert swap.accounts[6].pubkey != find_ata(user, roles["quote_mint"], TOKEN_PROGRAM) + + +async def test_a_coin_priced_in_another_asset_is_found_by_searching(tmp_keystore): + client, roles, pool = _chain(T22_FIXTURE) + user = _user(tmp_keystore) + quote_ata = find_ata(user, roles["quote_mint"], TOKEN_2022_PROGRAM) + client.token_balances[quote_ata] = 5 * 10**8 + with _patched(client): + result = await buy_launchlab( + "http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), 1 + ) + assert result["platform"] == "StonkFun" + assert result["quote_mint"] == str(roles["quote_mint"]) + swap = _swap(client.sent[0]) + amount_in, min_out = _u64s(swap) + assert amount_in == 10**8 # the quote has 8 decimals + # The coin's 1% transfer fee comes off what arrives. + assert result["tokens_received"] * 10**6 == buy_amount_out( + pool, amount_in, 12_500, ((100, 10**15),) + ) + assert min_out == buy_amount_out(pool, amount_in, 12_500, ((100, 10**15),)) * 85 // 100 + assert swap.accounts[6].pubkey == quote_ata + assert swap.accounts[5].pubkey == find_ata(user, roles["base_mint"], TOKEN_2022_PROGRAM) + assert swap.accounts[12].pubkey == TOKEN_2022_PROGRAM + # No WSOL wrapping: create the coin and quote accounts, then swap. + assert [i.program_id for i in client.sent[0]["instructions"]] == [ + ASSOCIATED_TOKEN_PROGRAM, + ASSOCIATED_TOKEN_PROGRAM, + LAUNCHLAB_PROGRAM, + ] + + +async def test_buy_needs_the_quote_asset(tmp_keystore): + client, roles, _ = _chain(T22_FIXTURE) + with _patched(client): + result = await buy_launchlab( + "http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), 1 + ) + assert result["error"] == "insufficient_balance" + assert result["available_quote_raw"] == 0 and result["required_quote_raw"] == 10**8 + assert "available_sol" not in result + assert client.sent == [] + + +async def test_a_curve_type_it_cannot_price_is_refused(tmp_keystore): + client, roles, pool = _chain(SOL_FIXTURE) + config = bytearray(client.accounts[pool["global_config"]].data) + config[16] = 2 # linear + client.accounts[pool["global_config"]].data = bytes(config) + with _patched(client): + result = await buy_launchlab( + "http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), 0.01 + ) + assert result["error"] == "launchlab_error" + assert client.sent == [] + + +async def test_buy_dry_run_sends_nothing(tmp_keystore): + client, roles, _ = _chain(SOL_FIXTURE) + with _patched(client): + result = await buy_launchlab( + "http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), 0.01, dry_run=True + ) + assert result["dry_run"] is True and result["venue"] == "launchlab" + assert result["expected_tokens_raw"] > result["min_tokens_out_raw"] > 0 + assert client.sent == [] + + +async def test_sell_all_of_a_transfer_fee_coin(tmp_keystore): + client, roles, pool = _chain(T22_FIXTURE) + user = _user(tmp_keystore) + client.token_balances[find_ata(user, roles["base_mint"], TOKEN_2022_PROGRAM)] = 10**10 + with _patched(client): + result = await sell_launchlab( + "http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), "all" + ) + swap = _swap(client.sent[0]) + assert bytes(swap.data[:8]) == SELL_EXACT_IN_DISCRIMINATOR + amount_in, min_out = _u64s(swap) + assert amount_in == 10**10 + expected = sell_amount_out(pool, 10**10, 12_500, ((100, 10**15),)) + assert result["quote_received"] == expected / 10**8 + assert min_out == expected * 85 // 100 + + +async def test_sell_more_than_held(tmp_keystore): + client, roles, _ = _chain(SOL_FIXTURE) + with _patched(client): + result = await sell_launchlab( + "http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), "5" + ) + assert result["error"] == "insufficient_balance" + + +def _set_status(client: FakeClient, pool: Pubkey, status: int) -> None: + data = bytearray(client.accounts[pool].data) + data[17] = status + client.accounts[pool].data = bytes(data) + + +@pytest.mark.parametrize("fn,arg", [(buy_launchlab, 0.01), (sell_launchlab, "all")]) +async def test_a_graduated_curve_defers_to_its_pool(tmp_keystore, fn, arg): + client, roles, _ = _chain(SOL_FIXTURE) + _set_status(client, roles["pool"], 2) + with _patched(client): + result = await fn("http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), arg) + assert result["error"] == "graduated" + assert result["migrate_type"] == "cpmm" + + +async def test_no_pool(tmp_keystore): + client = FakeClient() + with _patched(client): + result = await buy_launchlab( + "http://rpc", tmp_keystore, "testpass", str(Pubkey.new_unique()), 0.01 + ) + assert result["error"] == "not_found" + + +async def test_slippage_is_reported(tmp_keystore): + client, roles, _ = _chain(SOL_FIXTURE) + client.send_error = TransactionFailedError({"InstructionError": [3, {"Custom": 6004}]}) + with _patched(client): + result = await buy_launchlab( + "http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), 0.01 + ) + assert result["error"] == "slippage" + + +async def test_info(): + client, roles, pool = _chain(T22_FIXTURE) + with _patched(client): + info = await get_launchlab_info("http://rpc", str(roles["base_mint"])) + assert info["venue"] == "launchlab" + assert info["platform"] == "StonkFun" + assert info["launchlab_pool"] == str(roles["pool"]) + assert info["graduated"] is False + assert info["curve_fee_pct"] == 1.25 + assert info["transfer_fee_bps"] == 100 + assert info["bonding_progress"] == round(pool["real_base"] / pool["total_base_sell"] * 100, 1) + + +async def test_token_info_falls_back_to_launchlab(): + client, roles, _ = _chain(SOL_FIXTURE) + with ( + patch("pumpfun_cli.core.info.RpcClient", return_value=client), + _patched(client), + ): + info = await get_token_info("http://rpc", str(roles["base_mint"])) + assert info["venue"] == "launchlab" + assert info["platform"] == "Spots.fun" + + +async def test_a_sell_the_curve_would_pay_nothing_for_is_refused(tmp_keystore): + client, roles, _ = _chain(SOL_FIXTURE) + user = _user(tmp_keystore) + client.token_balances[find_ata(user, roles["base_mint"], TOKEN_PROGRAM)] = 1 + with _patched(client): + result = await sell_launchlab( + "http://rpc", tmp_keystore, "testpass", str(roles["base_mint"]), "all" + ) + assert result["error"] == "dust" + assert client.sent == [] diff --git a/tests/test_protocol/test_launchlab.py b/tests/test_protocol/test_launchlab.py new file mode 100644 index 0000000..8f6657f --- /dev/null +++ b/tests/test_protocol/test_launchlab.py @@ -0,0 +1,162 @@ +"""Raydium LaunchLab: instructions against the IDL, layouts against mainnet accounts.""" + +import base64 +import json +import struct +from pathlib import Path + +import pytest +from solders.pubkey import Pubkey + +from pumpfun_cli.protocol.contracts import SYSTEM_PROGRAM, TOKEN_2022_PROGRAM, TOKEN_PROGRAM +from pumpfun_cli.protocol.launchlab import ( + LAUNCHLAB_PROGRAM, + POOL_STATE_DISCRIMINATOR, + build_launchlab_swap_instruction, + buy_amount_out, + curve_type, + derive_creator_fee_vault, + derive_launchlab_pool, + derive_platform_fee_vault, + mint_transfer_fee, + parse_fee_rate, + parse_pool_state, + platform_name, + sell_amount_out, + withheld_fee, +) +from tests.test_protocol.test_instructions import assert_matches_idl + +ROOT = Path(__file__).parents[2] +IDL = json.loads((ROOT / "idl" / "raydium_launchlab_idl.json").read_text()) + + +def _fixture(name: str) -> dict: + """A pool and its configs and mints as they were on mainnet.""" + bundle = json.loads((ROOT / "tests" / "fixtures" / name).read_text()) + return {key: {**acc, "bytes": base64.b64decode(acc["data"])} for key, acc in bundle.items()} + + +SOL_POOL = _fixture("launchlab_sol_pool.json") +T22_POOL = _fixture("launchlab_token2022_quote_pool.json") + + +def _swap(buy: bool, pool: dict, **overrides): + args = { + "buy": buy, + "pool_address": Pubkey.new_unique(), + "pool": pool, + "user": Pubkey.new_unique(), + "user_base_token": Pubkey.new_unique(), + "user_quote_token": Pubkey.new_unique(), + "base_token_program": TOKEN_PROGRAM, + "quote_token_program": TOKEN_PROGRAM, + "amount_in": 123, + "minimum_amount_out": 45, + } + return build_launchlab_swap_instruction(**{**args, **overrides}) + + +@pytest.mark.parametrize("buy,name", [(True, "buy_exact_in"), (False, "sell_exact_in")]) +def test_swap_matches_idl(buy, name): + pool = parse_pool_state(SOL_POOL["pool"]["bytes"]) + built = _swap(buy, pool) + assert_matches_idl(IDL, name, built) + assert bytes(built.data[8:]) == struct.pack(" str: + mint = os.environ.get(var) + if not mint: + pytest.skip(f"Set {var} to a LaunchLab coin on its curve") + return mint + + +async def _round_trip(rpc, keystore, password, mint, amount): + bought = await buy_launchlab(rpc, str(keystore), password, mint, amount, 25, confirm=True) + assert "error" not in bought, bought + assert bought["venue"] == "launchlab" + assert bought["tokens_received_raw"] > 0 + sold = await sell_launchlab(rpc, str(keystore), password, mint, "all", 25, confirm=True) + assert "error" not in sold, sold + assert sold["tokens_sold_raw"] == bought["tokens_received_raw"] + return bought, sold + + +@pytest.mark.asyncio +async def test_sol_coin_round_trip(surfpool_rpc, funded_keypair, test_keystore, test_password): + mint = _mint("SURFPOOL_LAUNCHLAB_MINT") + info = await get_token_info(surfpool_rpc, mint) + assert info["venue"] == "launchlab" and info["quote_symbol"] == "SOL", info + await _round_trip(surfpool_rpc, test_keystore, test_password, mint, 0.01) + + +@pytest.mark.asyncio +async def test_other_quote_round_trip(surfpool_rpc, funded_keypair, test_keystore, test_password): + mint = _mint("SURFPOOL_LAUNCHLAB_QUOTE_MINT") + info = await get_token_info(surfpool_rpc, mint) + assert info["venue"] == "launchlab" and info["quote_symbol"] != "SOL", info + quote = info["quote_mint"] + async with httpx.AsyncClient() as http: + owner = ( + await http.post( + surfpool_rpc, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "getAccountInfo", + "params": [quote, {"encoding": "base64"}], + }, + ) + ).json()["result"]["value"]["owner"] + funded = ( + await http.post( + surfpool_rpc, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "surfnet_setTokenAccount", + "params": [str(funded_keypair.pubkey()), quote, {"amount": 10**12}, owner], + }, + ) + ).json() + assert "error" not in funded, funded + bought, sold = await _round_trip(surfpool_rpc, test_keystore, test_password, mint, 1) + assert bought["quote_mint"] == quote and sold["quote_received"] > 0