diff --git a/idl/raydium_launchlab_idl.json b/idl/raydium_launchlab_idl.json index e035478..70c90c5 100644 --- a/idl/raydium_launchlab_idl.json +++ b/idl/raydium_launchlab_idl.json @@ -2,7 +2,7 @@ "address": "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", "metadata": { "name": "raydium_launchpad", - "version": "0.1.0", + "version": "0.2.0", "spec": "0.1.0", "description": "Created with Anchor" }, @@ -387,44 +387,52 @@ ] }, { - "name": "claim_platform_fee", + "name": "claim_creator_fee", "docs": [ - "Claim platform fee", + "Claim the fee from the exclusive creator fee vault.", "# Arguments", "", "* `ctx` - The context of accounts", "" ], "discriminator": [ - 156, - 39, - 208, - 135, - 76, - 237, - 61, - 72 + 26, + 97, + 138, + 203, + 132, + 171, + 141, + 252 ], "accounts": [ { - "name": "platform_fee_wallet", + "name": "creator", "docs": [ - "Only the wallet stored in platform_config can collect platform fees" + "The pool creator" ], "writable": true, "signer": true }, { - "name": "authority", - "docs": [ - "PDA that acts as the authority for pool vault and mint operations", - "Generated using AUTH_SEED" - ], + "name": "fee_vault_authority", "pda": { "seeds": [ { "kind": "const", "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 95, + 102, + 101, + 101, + 95, 118, 97, 117, @@ -446,71 +454,36 @@ } }, { - "name": "pool_state", + "name": "creator_fee_vault", "docs": [ - "Account that stores the pool's state and parameters", - "PDA generated using POOL_SEED and both token mints" + "The creator fee vault" ], - "writable": true - }, - { - "name": "platform_config", - "docs": [ - "The platform config account" - ] - }, - { - "name": "quote_vault", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "creator" + }, + { + "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" + "path": "creator" }, { - "kind": "const", - "value": [ - 6, - 221, - 246, - 225, - 215, - 101, - 161, - 147, - 217, - 203, - 225, - 70, - 206, - 235, - 121, - 172, - 28, - 180, - 133, - 237, - 95, - 91, - 55, - 145, - 58, - 140, - 245, - 133, - 126, - 255, - 0, - 169 - ] + "kind": "account", + "path": "token_program" }, { "kind": "account", @@ -559,13 +532,13 @@ { "name": "quote_mint", "docs": [ - "The mint of quote token vault" + "The mint for the quote token" ] }, { "name": "token_program", "docs": [ - "SPL program for input token transfers" + "SPL Token program for the quote token" ], "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, @@ -587,26 +560,29 @@ "args": [] }, { - "name": "claim_vested_token", + "name": "claim_platform_fee", "docs": [ - "Claim vested token", - "# Arguments" + "Claim platform fee", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" ], "discriminator": [ - 49, - 33, - 104, - 30, - 189, - 157, - 79, - 35 + 156, + 39, + 208, + 135, + 76, + 237, + 61, + 72 ], "accounts": [ { - "name": "beneficiary", + "name": "platform_fee_wallet", "docs": [ - "The beneficiary of the vesting account" + "Only the wallet stored in platform_config can collect platform fees" ], "writable": true, "signer": true @@ -651,66 +627,85 @@ "writable": true }, { - "name": "vesting_record", + "name": "platform_config", "docs": [ - "The vesting record account" + "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": "const", - "value": [ - 112, - 111, - 111, - 108, - 95, - 118, - 101, - 115, - 116, - 105, - 110, - 103 - ] + "kind": "account", + "path": "platform_fee_wallet" }, { "kind": "account", - "path": "pool_state" + "path": "token_program" }, { "kind": "account", - "path": "beneficiary" + "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": "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, - "signer": true - }, - { - "name": "base_token_mint", + "name": "quote_mint", "docs": [ - "The mint for the base token (token being sold)", - "Created in this instruction with specified decimals" + "The mint of quote token vault" ] }, { - "name": "base_token_program", + "name": "token_program", "docs": [ - "SPL Token program for the base token", - "Must be the standard Token program" + "SPL program for input token transfers" ], "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, @@ -732,39 +727,53 @@ "args": [] }, { - "name": "collect_fee", + "name": "claim_platform_fee_from_vault", "docs": [ - "Collects accumulated fees from the pool", + "Claim the fee from the exclusive platform fee vault.", "# Arguments", "", "* `ctx` - The context of accounts", "" ], "discriminator": [ - 60, - 173, - 247, - 103, - 4, - 93, - 130, - 48 + 117, + 241, + 198, + 168, + 248, + 218, + 80, + 29 ], "accounts": [ { - "name": "owner", + "name": "platform_fee_wallet", "docs": [ - "Only protocol_fee_owner saved in global_config can collect protocol fee now" + "Only the wallet stored in platform_config can collect platform fees" ], + "writable": true, "signer": true }, { - "name": "authority", + "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, @@ -786,37 +795,95 @@ } }, { - "name": "pool_state", - "docs": [ - "Pool state stores accumulated protocol fee amount" - ], - "writable": true - }, - { - "name": "global_config", + "name": "platform_config", "docs": [ - "Global config account stores owner" + "The platform config account" ] }, { - "name": "quote_vault", + "name": "platform_fee_vault", "docs": [ - "The address that holds pool tokens for quote token" + "The platform fee vault" ], - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "platform_config" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } }, { - "name": "quote_mint", + "name": "recipient_token_account", "docs": [ - "The mint of quote token vault" - ] + "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": "recipient_token_account", + "name": "quote_mint", "docs": [ - "The address that receives the collected quote token fees" - ], - "writable": true + "The mint of quote token vault" + ] }, { "name": "token_program", @@ -824,39 +891,55 @@ "SPL program for input token transfers" ], "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "docs": [ + "Required for associated token program" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" } ], "args": [] }, { - "name": "collect_migrate_fee", + "name": "claim_vested_token", "docs": [ - "Collects migrate fees from the pool", - "# Arguments", - "", - "* `ctx` - The context of accounts", - "" + "Claim vested token", + "# Arguments" ], "discriminator": [ - 255, - 186, - 150, - 223, - 235, - 118, - 201, - 186 + 49, + 33, + 104, + 30, + 189, + 157, + 79, + 35 ], "accounts": [ { - "name": "owner", + "name": "beneficiary", "docs": [ - "Only migrate_fee_owner saved in global_config can collect migrate fee now" + "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": [ { @@ -885,71 +968,363 @@ { "name": "pool_state", "docs": [ - "Pool state stores accumulated protocol fee amount" + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" ], "writable": true }, { - "name": "global_config", + "name": "vesting_record", "docs": [ - "Global config account stores owner" - ] + "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": "quote_vault", + "name": "base_vault", "docs": [ - "The address that holds pool tokens for quote token" + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" ], "writable": true }, { - "name": "quote_mint", + "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 of quote token vault" + "The mint for the base token (token being sold)", + "Created in this instruction with specified decimals" ] }, { - "name": "recipient_token_account", + "name": "base_token_program", "docs": [ - "The address that receives the collected quote token fees" + "SPL Token program for the base token", + "Must be the standard Token program" ], - "writable": true + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, { - "name": "token_program", + "name": "system_program", "docs": [ - "SPL program for input token transfers" + "Required for account creation" ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "docs": [ + "Required for associated token program" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" } ], "args": [] }, { - "name": "create_config", + "name": "collect_fee", "docs": [ - "Creates a new configuration", + "Collects accumulated fees from the pool", "# 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", + "* `ctx` - The context of accounts", "" ], "discriminator": [ - 201, - 207, - 243, - 114, - 75, - 111, - 47, - 189 - ], - "accounts": [ - { - "name": "owner", + 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": [ + "SPL program for input token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "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": [ + "SPL program for input token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "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", @@ -1134,12 +1509,18 @@ ] } }, + { + "name": "cpswap_config" + }, { "name": "system_program", "docs": [ "Required for account creation" ], "address": "11111111111111111111111111111111" + }, + { + "name": "transfer_fee_extension_authority" } ], "args": [ @@ -1185,6 +1566,11 @@ }, { "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 }, { @@ -1210,59 +1596,662 @@ 111, 108, 95, - 118, - 101, - 115, + 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": [ + "Initializes a new trading pool", + "# 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", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "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", + "# 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": [ + "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", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, 116, + 104, + 111, + 114, 105, - 110, - 103 + 116, + 121 ] - }, - { - "kind": "account", - "path": "pool_state" - }, - { - "kind": "account", - "path": "beneficiary" } ] } }, { - "name": "system_program", - "docs": [ - "Required for account creation" - ], - "address": "11111111111111111111111111111111" + "name": "program" } ], "args": [ { - "name": "share_amount", - "type": "u64" + "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", + "name": "initialize_with_token_2022", "docs": [ - "Initializes a new trading pool", + "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": [ - 175, - 175, - 109, - 31, - 13, - 152, - 155, - 237 + 37, + 190, + 126, + 222, + 44, + 154, + 171, + 17 ], "accounts": [ { @@ -1437,21 +2426,12 @@ ] } }, - { - "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" + "SPL Token program for the base token" ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" }, { "name": "quote_token_program", @@ -1460,14 +2440,6 @@ ], "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, - { - "name": "metadata_program", - "docs": [ - "Metaplex Token Metadata program", - "Used to create metadata for the base token" - ], - "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" - }, { "name": "system_program", "docs": [ @@ -1475,13 +2447,6 @@ ], "address": "11111111111111111111111111111111" }, - { - "name": "rent_program", - "docs": [ - "Required for rent exempt calculations" - ], - "address": "SysvarRent111111111111111111111111111111111" - }, { "name": "event_authority", "pda": { @@ -1539,6 +2504,24 @@ "name": "VestingParams" } } + }, + { + "name": "amm_fee_on", + "type": { + "defined": { + "name": "AmmCreatorFeeOn" + } + } + }, + { + "name": "transfer_fee_extension_param", + "type": { + "option": { + "defined": { + "name": "TransferFeeExtensionParams" + } + } + } } ] }, @@ -2168,7 +3151,8 @@ "name": "base_mint", "docs": [ "The mint for the base token (token being sold)" - ] + ], + "writable": true }, { "name": "quote_mint", @@ -2526,8 +3510,7 @@ "docs": [ "SPL Token program for the base token", "Must be the standard Token program" - ], - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + ] }, { "name": "quote_token_program", @@ -2567,6 +3550,77 @@ ], "args": [] }, + { + "name": "remove_platform_curve_param", + "docs": [ + "Remove platform launch param", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `index` - The index of the curve param to remove", + "" + ], + "discriminator": [ + 27, + 30, + 62, + 169, + 93, + 224, + 24, + 145 + ], + "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": "index", + "type": "u8" + } + ] + }, { "name": "sell_exact_in", "docs": [ @@ -3064,10 +4118,104 @@ ], "args": [ { - "name": "param", + "name": "param", + "type": { + "defined": { + "name": "PlatformConfigParam" + } + } + } + ] + }, + { + "name": "update_platform_curve_param", + "docs": [ + "Update platform launch param", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `bonding_curve_param` - Parameter to update", + "" + ], + "discriminator": [ + 138, + 144, + 138, + 250, + 220, + 128, + 4, + 57 + ], + "accounts": [ + { + "name": "platform_admin", + "docs": [ + "The account paying for the initialization costs" + ], + "writable": true, + "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" + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Includes settings like quote token mint and fee parameters" + ] + }, + { + "name": "system_program", + "docs": [ + "System program for lamport transfers" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "index", + "type": "u8" + }, + { + "name": "bonding_curve_param", "type": { "defined": { - "name": "PlatformConfigParam" + "name": "BondingCurveParam" } } } @@ -3262,9 +4410,123 @@ "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" } ], "types": [ + { + "name": "AmmCreatorFeeOn", + "docs": [ + "migrate to cpmm, creator fee on quote token or both token" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "QuoteToken" + }, + { + "name": "BothToken" + } + ] + } + }, + { + "name": "BondingCurveParam", + "type": { + "kind": "struct", + "fields": [ + { + "name": "migrate_type", + "docs": [ + "Migrate to AMM or CpSwap, 0: amm, 1: cpswap,", + "Neither 0 nor 1: invalid" + ], + "type": "u8" + }, + { + "name": "migrate_cpmm_fee_on", + "docs": [ + "The migrate fee on, 0 means fee on the quote token, 1 means fee on both token", + "Neither 0 nor 1: invalid" + ], + "type": "u8" + }, + { + "name": "supply", + "docs": [ + "The supply of the token,", + "0: invalid" + ], + "type": "u64" + }, + { + "name": "total_base_sell", + "docs": [ + "The total base sell of the token", + "0: invalid" + ], + "type": "u64" + }, + { + "name": "total_quote_fund_raising", + "docs": [ + "The total quote fund raising of the token", + "0: invalid" + ], + "type": "u64" + }, + { + "name": "total_locked_amount", + "docs": [ + "total amount of tokens to be unlocked", + "u64::MAX: invalid" + ], + "type": "u64" + }, + { + "name": "cliff_period", + "docs": [ + "Waiting time in seconds before unlocking after fundraising ends", + "u64::MAX: invalid" + ], + "type": "u64" + }, + { + "name": "unlock_period", + "docs": [ + "Unlocking period in seconds", + "u64::MAX: invalid" + ], + "type": "u64" + } + ] + } + }, { "name": "ClaimVestedEvent", "docs": [ @@ -3710,6 +4972,29 @@ ] } }, + { + "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": "padding", "docs": [ @@ -3718,9 +5003,70 @@ "type": { "array": [ "u8", - 256 + 180 ] } + }, + { + "name": "curve_params", + "docs": [ + "The parameters for launching the pool" + ], + "type": { + "vec": { + "defined": { + "name": "PlatformCurveParam" + } + } + } + } + ] + } + }, + { + "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" } ] } @@ -3775,6 +5121,71 @@ "fields": [ "string" ] + }, + { + "name": "CpSwapConfig" + }, + { + "name": "AllInfo", + "fields": [ + { + "defined": { + "name": "PlatformConfigInfo" + } + } + ] + } + ] + } + }, + { + "name": "PlatformCurveParam", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "The epoch for update interval, 0 means not update" + ], + "type": "u64" + }, + { + "name": "index", + "docs": [ + "The curve params index" + ], + "type": "u8" + }, + { + "name": "global_config", + "docs": [ + "The global config address" + ], + "type": "pubkey" + }, + { + "name": "bonding_curve_param", + "docs": [ + "bonding curve param" + ], + "type": { + "defined": { + "name": "BondingCurveParam" + } + } + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u64", + 50 + ] + } } ] } @@ -3788,7 +5199,8 @@ "* `fee_rate` - Fee rate of the platform", "* `name` - Name of the platform", "* `web` - Website of the platform", - "* `img` - Image link 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", @@ -3816,6 +5228,10 @@ { "name": "img", "type": "string" + }, + { + "name": "creator_fee_rate", + "type": "u64" } ] } @@ -3863,6 +5279,14 @@ "name": "VestingParams" } } + }, + { + "name": "amm_fee_on", + "type": { + "defined": { + "name": "AmmCreatorFeeOn" + } + } } ] } @@ -3917,7 +5341,7 @@ { "name": "migrate_type", "docs": [ - "Migrate to AMM or CpSwap" + "Migrate to AMM or CpSwap, 0: amm, 1: cpswap" ], "type": "u8" }, @@ -4059,6 +5483,31 @@ ], "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": "padding", "docs": [ @@ -4066,8 +5515,8 @@ ], "type": { "array": [ - "u64", - 8 + "u8", + 62 ] } } @@ -4171,6 +5620,10 @@ "name": "platform_fee", "type": "u64" }, + { + "name": "creator_fee", + "type": "u64" + }, { "name": "share_fee", "type": "u64" @@ -4190,6 +5643,32 @@ "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" } ] } diff --git a/learning-examples/letsbonk-buy-sell/idl_parser.py b/learning-examples/letsbonk-buy-sell/idl_parser.py new file mode 100644 index 0000000..f1442f1 --- /dev/null +++ b/learning-examples/letsbonk-buy-sell/idl_parser.py @@ -0,0 +1,391 @@ +""" +IDL Parser module for Solana programs. +Provides functionality to load and parse Anchor IDL files and decode instruction data. +""" + +import json +import struct +from typing import Any + +import base58 + +# Constants for Anchor data layout +DISCRIMINATOR_SIZE = 8 +PUBLIC_KEY_SIZE = 32 +STRING_LENGTH_PREFIX_SIZE = 4 +ENUM_DISCRIMINATOR_SIZE = 1 + + +class IDLParser: + """Parser for automatically decoding instructions using IDL definitions.""" + + # A single source of truth for primitive type information, mapping the type name + # to its struct format character and size in bytes. + _PRIMITIVE_TYPE_INFO = { + # type_name: (format_char, size_in_bytes) + "u8": (" dict[str, bytes]: + """Get a mapping of instruction names to their discriminators.""" + return {instr["name"]: disc for disc, instr in self.instructions.items()} + + def get_instruction_names(self) -> list[str]: + """Get a list of all available instruction names.""" + return [instr["name"] for instr in self.instructions.values()] + + def validate_instruction_data_length( + self, ix_data: bytes, discriminator: bytes + ) -> bool: + """Validate that instruction data meets minimum length requirements.""" + if discriminator not in self.instruction_min_sizes: + return True # Allow if we don't know the expected size + + expected_min_size = self.instruction_min_sizes[discriminator] + actual_size = len(ix_data) + + if actual_size < expected_min_size: + instruction_name = self.instructions[discriminator]["name"] + if self.verbose: + print( + f"āš ļø Instruction data for '{instruction_name}' is shorter than the expected minimum " + f"({actual_size}/{expected_min_size} bytes)." + ) + return False + + return True + + def decode_instruction( + self, ix_data: bytes, keys: list[bytes], accounts: list[int] + ) -> dict[str, Any] | None: + """Decode instruction data using IDL definitions.""" + if len(ix_data) < DISCRIMINATOR_SIZE: + return None + + discriminator = ix_data[:DISCRIMINATOR_SIZE] + if discriminator not in self.instructions: + return None + + if not self.validate_instruction_data_length(ix_data, discriminator): + return None + + instruction = self.instructions[discriminator] + data_args = ix_data[DISCRIMINATOR_SIZE:] + + # Decode instruction arguments + args = {} + decode_offset = 0 + for arg in instruction.get("args", []): + try: + value, decode_offset = self._decode_type( + data_args, decode_offset, arg["type"] + ) + args[arg["name"]] = value + except Exception as e: + if self.verbose: + print(f"āŒ Decode error in argument '{arg['name']}': {e}") + return None + + # Helper to safely retrieve account public keys + def get_account_key(index: int) -> str | None: + if index < len(accounts): + account_index = accounts[index] + if account_index < len(keys): + return base58.b58encode(keys[account_index]).decode("utf-8") + return None # Return None for invalid indices + + # Build account info based on instruction definition + account_info = {} + instruction_accounts = instruction.get("accounts", []) + for i, account_def in enumerate(instruction_accounts): + account_info[account_def["name"]] = get_account_key(i) + + return { + "instruction_name": instruction["name"], + "args": args, + "accounts": account_info, + } + + def decode_account_data( + self, + account_data: bytes, + account_type_name: str, + skip_discriminator: bool = True, + ) -> dict[str, Any] | None: + """ + Decode account data using a specific account type from the IDL. + + Args: + account_data: Raw account data bytes. + account_type_name: Name of the account type in the IDL (e.g., "MyAccount"). + skip_discriminator: Whether to skip the first 8 bytes, which Anchor uses as a + type discriminator for account data. Set to False if your + data does not have this prefix. + + Returns: + Decoded account data as a dictionary, or None if decoding fails. + """ + try: + if account_type_name not in self.types: + if self.verbose: + print(f"Account type '{account_type_name}' not found in IDL") + return None + + data = account_data + if skip_discriminator: + if len(account_data) < DISCRIMINATOR_SIZE: + if self.verbose: + print( + f"Account data too short to contain a discriminator: {len(account_data)} bytes" + ) + return None + data = account_data[DISCRIMINATOR_SIZE:] + + decoded_data, _ = self._decode_defined_type(data, 0, account_type_name) + return decoded_data + + except Exception as e: + if self.verbose: + print(f"Error decoding account data for {account_type_name}: {e}") + return None + + # -------------------------------------------------------------------------- + # Internal Helper Methods + # -------------------------------------------------------------------------- + + def _build_instruction_map(self): + """Build a map of discriminators to instruction definitions.""" + for instruction in self.idl.get("instructions", []): + # The discriminator from the JSON IDL is a list of u8 integers. + discriminator = bytes(instruction["discriminator"]) + self.instructions[discriminator] = instruction + + def _build_type_map(self): + """Build a map of type names to their definitions.""" + for type_def in self.idl.get("types", []): + self.types[type_def["name"]] = type_def + + def _calculate_instruction_sizes(self): + """Calculate minimum data sizes for each instruction.""" + for discriminator, instruction in self.instructions.items(): + try: + min_size = DISCRIMINATOR_SIZE + for arg in instruction.get("args", []): + min_size += self._calculate_type_min_size(arg["type"]) + self.instruction_min_sizes[discriminator] = min_size + if self.verbose and instruction["name"] == "initialize": + print(f"šŸ“ Initialize instruction min size: {min_size} bytes") + except Exception as e: + if self.verbose: + print(f"āš ļø Could not calculate size for {instruction['name']}: {e}") + self.instruction_min_sizes[discriminator] = DISCRIMINATOR_SIZE + + def _calculate_type_min_size(self, type_def: str | dict) -> int: + """Calculate minimum size in bytes for a type definition.""" + if isinstance(type_def, str): + return self._get_primitive_size(type_def) + + if isinstance(type_def, dict): + if "defined" in type_def: + type_name = self._get_defined_type_name(type_def) + return self._calculate_defined_type_min_size(type_name) + if "array" in type_def: + element_type, array_length = type_def["array"] + element_size = self._calculate_type_min_size(element_type) + return element_size * array_length + + raise ValueError( + f"Invalid or unknown type definition for size calculation: {type_def}" + ) + + def _get_primitive_size(self, type_name: str) -> int: + """Get size in bytes for primitive types from the central map.""" + info = self._PRIMITIVE_TYPE_INFO.get(type_name) + return info[1] if info else 0 + + def _get_defined_type_name(self, type_def: dict[str, Any]) -> str: + """Extracts the type name from a 'defined' type, handling old and new IDL formats.""" + defined_value = type_def["defined"] + # New format: {'defined': {'name': 'MyType'}} + # Old format: {'defined': 'MyType'} + return ( + defined_value["name"] if isinstance(defined_value, dict) else defined_value + ) + + def _calculate_defined_type_min_size(self, type_name: str) -> int: + """Calculate minimum size for user-defined types (structs and enums).""" + if type_name not in self.types: + raise ValueError(f"Unknown defined type: {type_name}") + + type_def = self.types[type_name]["type"] + + if type_def["kind"] == "struct": + return sum( + self._calculate_type_min_size(field["type"]) + for field in type_def["fields"] + ) + + if type_def["kind"] == "enum": + # The size of an enum is its discriminator plus the size of its LARGEST variant, + # as the data layout must accommodate any possible variant. + max_variant_size = 0 + for variant in type_def["variants"]: + variant_size = 0 + for field in variant.get("fields", []): + # A field can be a type string/dict (tuple variant) or a dict with a 'type' key (struct variant) + field_type = field["type"] if isinstance(field, dict) else field + variant_size += self._calculate_type_min_size(field_type) + max_variant_size = max(max_variant_size, variant_size) + return ENUM_DISCRIMINATOR_SIZE + max_variant_size + + raise ValueError( + f"Unsupported type kind for size calculation: {type_def['kind']}" + ) + + def _decode_type( + self, data: bytes, offset: int, type_def: str | dict + ) -> tuple[Any, int]: + """Decode a value based on its type definition.""" + if isinstance(type_def, str): + return self._decode_primitive(data, offset, type_def) + + if isinstance(type_def, dict): + if "defined" in type_def: + type_name = self._get_defined_type_name(type_def) + return self._decode_defined_type(data, offset, type_name) + if "array" in type_def: + return self._decode_array(data, offset, type_def["array"]) + + raise ValueError(f"Invalid or unknown type definition for decoding: {type_def}") + + def _decode_array( + self, data: bytes, offset: int, array_def: list + ) -> tuple[list[Any], int]: + """Decode fixed-size array types.""" + element_type, array_length = array_def + array_data = [] + for _ in range(array_length): + value, offset = self._decode_type(data, offset, element_type) + array_data.append(value) + return array_data, offset + + def _decode_primitive( + self, data: bytes, offset: int, type_name: str + ) -> tuple[Any, int]: + """Decode primitive types.""" + if type_name not in self._PRIMITIVE_TYPE_INFO: + raise ValueError(f"Unknown primitive type: {type_name}") + + if type_name == "string": + length = struct.unpack_from(" tuple[dict[str, Any], int]: + """Decode user-defined types (structs and enums).""" + if type_name not in self.types: + raise ValueError(f"Unknown defined type: {type_name}") + + type_def = self.types[type_name]["type"] + + if type_def["kind"] == "struct": + struct_data = {} + for field in type_def["fields"]: + value, offset = self._decode_type(data, offset, field["type"]) + struct_data[field["name"]] = value + return struct_data, offset + + if type_def["kind"] == "enum": + variant_index = struct.unpack_from("= len(variants): + raise ValueError( + f"Invalid enum variant index {variant_index} for type {type_name}" + ) + + variant = variants[variant_index] + result = {"variant": variant["name"]} + variant_fields = variant.get("fields", []) + + if variant_fields: + # Check if it's a struct variant (fields are dicts) or tuple variant (fields are strings/dicts) + if isinstance(variant_fields[0], dict): + struct_data = {} + for field in variant_fields: + value, offset = self._decode_type(data, offset, field["type"]) + struct_data[field["name"]] = value + result["data"] = struct_data + else: # Tuple variant + tuple_data = [] + for field_type in variant_fields: + value, offset = self._decode_type(data, offset, field_type) + tuple_data.append(value) + result["data"] = tuple_data + + return result, offset + + raise ValueError(f"Unsupported type kind for decoding: {type_def['kind']}") + + +def load_idl_parser(idl_path: str, verbose: bool = False) -> IDLParser: + """ + Convenience function to load an IDL parser. + + Args: + idl_path: Path to the IDL JSON file + verbose: Whether to print debug information + + Returns: + Initialized IDLParser instance + """ + return IDLParser(idl_path, verbose) diff --git a/learning-examples/letsbonk-buy-sell/manual_buy_exact_in.py b/learning-examples/letsbonk-buy-sell/manual_buy_exact_in.py new file mode 100644 index 0000000..e34f683 --- /dev/null +++ b/learning-examples/letsbonk-buy-sell/manual_buy_exact_in.py @@ -0,0 +1,734 @@ +""" +Manual Buy Exact In Example for Raydium LaunchLab + +This script demonstrates how to buy tokens using the buy_exact_in instruction +from the Raydium LaunchLab program. It follows the IDL structure. + +Key features: +- Uses buy_exact_in instruction +- Implements proper account ordering as per IDL +- Includes slippage protection with minimum_amount_out +- Handles WSOL wrapping/unwrapping automatically +- Follows the exact transaction structure from the Solscan example +- User configurable SOL amount and slippage +- Uses idempotent ATA creation +""" + +import asyncio +import os +import struct +import sys + +import base58 +from dotenv import load_dotenv +from idl_parser import load_idl_parser +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed +from solana.rpc.types import TxOpts +from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price +from solders.instruction import AccountMeta, Instruction +from solders.keypair import Keypair +from solders.message import Message +from solders.pubkey import Pubkey +from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed +from solders.transaction import VersionedTransaction + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +# Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging +IDL_PARSER = load_idl_parser("idl/raydium_launchlab_idl.json", verbose=True) + +load_dotenv() + +TOKEN_MINT_ADDRESS = Pubkey.from_string( + "MYcq5mUyoAtCfyDWYAWvioou3cgnYjnCvFd7U6fspot" +) # Replace with actual token mint address + +# Configuration constants +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) +PAYER = Keypair.from_bytes(PRIVATE_KEY) + +# User configurable parameters +SOL_AMOUNT_TO_SPEND = float(os.environ.get("SOL_AMOUNT", "0.001")) +SLIPPAGE_TOLERANCE = float(os.environ.get("SLIPPAGE", "0.25")) + +# Transaction parameters +SHARE_FEE_RATE = 0 + +# Program IDs and addresses from Raydium LaunchLab +RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string( + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj" +) +GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX") +LETSBONK_PLATFORM_CONFIG = Pubkey.from_string( + "5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA" +) + +# Token program and system addresses +TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111") +WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112") +COMPUTE_BUDGET_PROGRAM_ID = Pubkey.from_string( + "ComputeBudget111111111111111111111111111111" +) +ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string( + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" +) +SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string( + "SysvarRent111111111111111111111111111111111" +) + +# Instruction discriminator for buy_exact_in (from IDL) +BUY_EXACT_IN_DISCRIMINATOR = bytes([250, 234, 13, 123, 213, 156, 19, 236]) + +# Compute budget settings +COMPUTE_UNIT_LIMIT = 150_000 +COMPUTE_UNIT_PRICE = 1_000 + +LAMPORTS_PER_SOL = 1_000_000_000 + + +def derive_authority_pda() -> Pubkey: + """ + Derive the authority PDA for the Raydium LaunchLab program. + + This PDA acts as the authority for pool vault operations and is generated + using the AUTH_SEED as specified in the IDL. + + Returns: + Pubkey: The derived authority PDA + """ + AUTH_SEED = b"vault_auth_seed" + authority_pda, _ = Pubkey.find_program_address( + [AUTH_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return authority_pda + + +def derive_event_authority_pda() -> Pubkey: + """ + Derive the event authority PDA for the Raydium LaunchLab program. + + This PDA is used for emitting program events during swaps. + + Returns: + Pubkey: The derived event authority PDA + """ + EVENT_AUTHORITY_SEED = b"__event_authority" + event_authority_pda, _ = Pubkey.find_program_address( + [EVENT_AUTHORITY_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return event_authority_pda + + +def derive_pool_state_for_token(base_token_mint: Pubkey) -> Pubkey | None: + """ + Derive the pool state account for a given base token mint. + + Args: + base_token_mint: The token mint address to search for + + Returns: + Pubkey of the pool state account, or None if not found + """ + seeds = [b"pool", bytes(base_token_mint), bytes(WSOL_MINT)] + pool_state_pda, _ = Pubkey.find_program_address(seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID) + return pool_state_pda + + +def derive_creator_fee_vault(creator: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Derive the creator fee vault PDA. + + This vault accumulates creator fees from trades. + + Args: + creator: The pool creator's pubkey + quote_mint: The quote token mint (WSOL) + + Returns: + Pubkey of the creator fee vault + """ + seeds = [bytes(creator), bytes(quote_mint)] + creator_fee_vault_pda, _ = Pubkey.find_program_address( + seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return creator_fee_vault_pda + + +def derive_platform_fee_vault(platform_config: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Derive the platform fee vault PDA. + + This vault accumulates platform fees from trades. + + Args: + platform_config: The platform config account + quote_mint: The quote token mint (WSOL) + + Returns: + Pubkey of the platform fee vault + """ + seeds = [bytes(platform_config), bytes(quote_mint)] + platform_fee_vault_pda, _ = Pubkey.find_program_address( + seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return platform_fee_vault_pda + + +def decode_pool_state(account_data: bytes) -> dict | None: + """ + Decode pool state account data using the IDL parser. + + Args: + account_data: Raw account data from the pool state account + + Returns: + Dictionary containing decoded pool state data, or None if decoding fails + """ + try: + result = IDL_PARSER.decode_account_data( + account_data, "PoolState", skip_discriminator=True + ) + if result: + return result + + return None + + except Exception as e: + print(f"Error decoding pool state: {e}") + import traceback + + traceback.print_exc() + return None + + +async def get_pool_state_data(client: AsyncClient, pool_state: Pubkey) -> dict | None: + """ + Get and decode the pool state account data. + + Args: + client: Solana RPC client + pool_state: The pool state account address + + Returns: + Dictionary containing decoded pool state data, or None if error + """ + try: + account_info = await client.get_account_info(pool_state) + if not account_info.value: + print("Pool state account not found") + return None + + return decode_pool_state(account_info.value.data) + + except Exception as e: + print(f"Error getting pool state data: {e}") + return None + + +def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey: + """ + Calculate the associated token account address for a given owner and mint. + + This manually implements the ATA derivation without requiring the spl-token package. + + Args: + owner: The wallet that owns the token account + mint: The token mint address + + Returns: + Pubkey of the associated token account + """ + ata_address, _ = Pubkey.find_program_address( + [bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)], + ASSOCIATED_TOKEN_PROGRAM_ID, + ) + return ata_address + + +def create_associated_token_account_idempotent_instruction( + payer: Pubkey, owner: Pubkey, mint: Pubkey +) -> Instruction: + """ + Create an idempotent instruction to create an Associated Token Account. + + This uses the CreateIdempotent instruction which doesn't fail if the ATA already exists. + + Args: + payer: The account that will pay for the creation + owner: The owner of the new token account + mint: The token mint + + Returns: + Instruction for creating the ATA idempotently + """ + ata_address = get_associated_token_address(owner, mint) + + accounts = [ + AccountMeta(pubkey=payer, is_signer=True, is_writable=True), # Funding account + AccountMeta( + pubkey=ata_address, is_signer=False, is_writable=True + ), # Associated token account + AccountMeta(pubkey=owner, is_signer=False, is_writable=False), # Wallet address + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), # Token mint + AccountMeta( + pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False + ), # System program + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # Token program + ] + + data = bytes([1]) + + return Instruction( + program_id=ASSOCIATED_TOKEN_PROGRAM_ID, data=data, accounts=accounts + ) + + +def create_initialize_account_instruction( + account: Pubkey, mint: Pubkey, owner: Pubkey +) -> Instruction: + """ + Create an InitializeAccount instruction for the Token Program. + + Args: + account: The account to initialize + mint: The token mint + owner: The account owner + + Returns: + Instruction for initializing the account + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=owner, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False), + ] + + # InitializeAccount instruction discriminator (instruction 1 in Token Program) + data = bytes([1]) + + return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts) + + +def create_close_account_instruction( + account: Pubkey, destination: Pubkey, owner: Pubkey +) -> Instruction: + """ + Create a CloseAccount instruction for the Token Program. + + Args: + account: The account to close + destination: Where to send the remaining lamports + owner: The account owner (must sign) + + Returns: + Instruction for closing the account + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + AccountMeta(pubkey=destination, is_signer=False, is_writable=True), + AccountMeta(pubkey=owner, is_signer=True, is_writable=False), + ] + + data = bytes([9]) + + return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts) + + +def create_wsol_account_with_seed( + payer: Pubkey, seed: str, lamports: int +) -> tuple[Pubkey, Instruction, Instruction]: + """ + Create a WSOL account using createAccountWithSeed and initialize it. + + This replicates the exact pattern from the Solscan example where a new account + is created with a seed and then initialized as a token account. + + Args: + payer: The account that will pay for and own the new account + seed: String seed for deterministic account generation + lamports: Amount of lamports to transfer to the new account + + Returns: + Tuple of (new_account_pubkey, create_instruction, initialize_instruction) + """ + new_account = Pubkey.create_with_seed(payer, seed, TOKEN_PROGRAM_ID) + + create_ix = create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=payer, + to_pubkey=new_account, + base=payer, + seed=seed, + lamports=lamports, + space=165, # Size of a token account + owner=TOKEN_PROGRAM_ID, + ) + ) + + initialize_ix = create_initialize_account_instruction(new_account, WSOL_MINT, payer) + + return new_account, create_ix, initialize_ix + + +def get_user_base_token_account(payer: Pubkey, base_mint: Pubkey) -> Pubkey: + """ + Get the user's associated token account for the base token. + + In a real implementation, this should check if the account exists and create it if needed. + For this example, we'll derive the standard ATA address. + + Args: + payer: The user's wallet address + base_mint: The base token mint address + + Returns: + Pubkey of the user's base token account + """ + return get_associated_token_address(payer, base_mint) + + +def calculate_minimum_amount_out_from_pool_state( + pool_state_data: dict, amount_in: int, slippage_tolerance: float +) -> int: + """ + Calculate the minimum amount out based on pool state data and slippage tolerance. + + Uses the actual pool reserves to calculate expected output using constant product formula. + + Args: + pool_state_data: Decoded pool state data containing reserves + amount_in: Amount of quote tokens being swapped in (in lamports) + slippage_tolerance: Slippage tolerance as a decimal (0.25 = 25%) + + Returns: + Minimum amount of base tokens to receive + """ + try: + # Extract pool reserves from decoded state + virtual_base = pool_state_data["virtual_base"] + virtual_quote = pool_state_data["virtual_quote"] + real_base = pool_state_data["real_base"] + real_quote = pool_state_data["real_quote"] + + print("Pool State:") + print(f" Virtual Base: {virtual_base:,}") + print(f" Virtual Quote: {virtual_quote:,}") + print(f" Real Base: {real_base:,}") + print(f" Real Quote: {real_quote:,}") + + # Use virtual reserves for bonding curve calculation + # This follows the constant product AMM formula: x * y = k + # amount_out = (amount_in * virtual_base) / (virtual_quote + amount_in) + + # Calculate expected output using constant product formula + numerator = amount_in * virtual_base + denominator = virtual_quote + amount_in + expected_output = numerator // denominator + + # Apply slippage tolerance + minimum_with_slippage = int(expected_output * (1 - slippage_tolerance)) + + print(f"Amount in: {amount_in:,} lamports") + print(f"Expected output: {expected_output:,} tokens") + print( + f"Minimum with {slippage_tolerance * 100}% slippage: {minimum_with_slippage:,} tokens" + ) + + return minimum_with_slippage + + except Exception as e: + print(f"Error calculating minimum amount out from pool state: {e}") + return None + + +async def buy_exact_in( + client: AsyncClient, + base_token_mint: Pubkey, + amount_in_sol: float, + slippage_tolerance: float, +) -> str | None: + """ + Execute a buy_exact_in transaction on Raydium LaunchLab. + + This function implements the exact transaction flow from the Solscan example: + 1. SetComputeUnitPrice + 2. SetComputeUnitLimit + 3. Create Associated Token Account for base token (idempotent) + 4. Create WSOL account with seed + 5. Initialize WSOL account + 6. Execute buy_exact_in instruction (15 main accounts + 3 remaining accounts) + 7. Close WSOL account + + The buy_exact_in instruction requires 18 total accounts: + - 15 main accounts (as per IDL) + - 3 remaining accounts: System Program, Creator Fee Vault, Platform Fee Vault + + Args: + client: Solana RPC client + base_token_mint: Address of the token to buy + amount_in_sol: Amount of SOL to spend + slippage_tolerance: Slippage tolerance as decimal + + Returns: + Transaction signature if successful, None otherwise + """ + try: + print(f"Finding pool state for token: {base_token_mint}") + pool_state = derive_pool_state_for_token(base_token_mint) + if not pool_state: + print("Pool state not found for this token") + return None + + # Get and decode pool state data using IDL parser + pool_state_data = await get_pool_state_data(client, pool_state) + if not pool_state_data: + print("Failed to decode pool state data") + return None + + # Extract vault addresses and creator from decoded pool state (convert from base58 strings to Pubkey objects) + base_vault = Pubkey.from_string(pool_state_data["base_vault"]) + quote_vault = Pubkey.from_string(pool_state_data["quote_vault"]) + creator = Pubkey.from_string(pool_state_data["creator"]) + + print(f"Found pool state: {pool_state}") + print(f"Base vault: {base_vault}") + print(f"Quote vault: {quote_vault}") + print(f"Creator: {creator}") + print(f"Pool status: {pool_state_data['status']}") + + # Derive necessary PDAs + authority = derive_authority_pda() + event_authority = derive_event_authority_pda() + creator_fee_vault = derive_creator_fee_vault(creator, WSOL_MINT) + platform_fee_vault = derive_platform_fee_vault( + LETSBONK_PLATFORM_CONFIG, WSOL_MINT + ) + + print(f"Creator fee vault: {creator_fee_vault}") + print(f"Platform fee vault: {platform_fee_vault}") + + # Calculate amounts using pool state data + amount_in = int(amount_in_sol * LAMPORTS_PER_SOL) + minimum_amount_out = calculate_minimum_amount_out_from_pool_state( + pool_state_data, amount_in, slippage_tolerance + ) + + print(f"Amount in: {amount_in} lamports ({amount_in_sol} SOL)") + print(f"Minimum amount out: {minimum_amount_out}") + + # Step 1: Create Associated Token Account for base token (idempotent) + user_base_token = get_associated_token_address(PAYER.pubkey(), base_token_mint) + create_ata_ix = create_associated_token_account_idempotent_instruction( + PAYER.pubkey(), PAYER.pubkey(), base_token_mint + ) + + # Step 2: Create WSOL account with seed + import hashlib + import time + + # Generate a unique seed based on timestamp and user pubkey + seed_data = f"{int(time.time())}{PAYER.pubkey()!s}" + wsol_seed = hashlib.sha256(seed_data.encode()).hexdigest()[:32] + + # Calculate required lamports (amount + small buffer for account creation) + account_creation_lamports = 2_039_280 # Standard account creation cost + total_lamports = amount_in + account_creation_lamports + + user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed( + PAYER.pubkey(), wsol_seed, total_lamports + ) + + print(f"User base token account: {user_base_token}") + print(f"User quote token account: {user_quote_token}") + + # Step 3: Build the buy_exact_in instruction + accounts = [ + AccountMeta( + pubkey=PAYER.pubkey(), is_signer=True, is_writable=False + ), # payer + AccountMeta( + pubkey=authority, is_signer=False, is_writable=False + ), # authority + AccountMeta( + pubkey=GLOBAL_CONFIG, is_signer=False, is_writable=False + ), # global_config + AccountMeta( + pubkey=LETSBONK_PLATFORM_CONFIG, is_signer=False, is_writable=False + ), # platform_config + AccountMeta( + pubkey=pool_state, is_signer=False, is_writable=True + ), # pool_state + AccountMeta( + pubkey=user_base_token, is_signer=False, is_writable=True + ), # user_base_token + AccountMeta( + pubkey=user_quote_token, is_signer=False, is_writable=True + ), # user_quote_token + AccountMeta( + pubkey=base_vault, is_signer=False, is_writable=True + ), # base_vault + AccountMeta( + pubkey=quote_vault, is_signer=False, is_writable=True + ), # quote_vault + AccountMeta( + pubkey=base_token_mint, is_signer=False, is_writable=False + ), # base_token_mint + AccountMeta( + pubkey=WSOL_MINT, is_signer=False, is_writable=False + ), # quote_token_mint + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # base_token_program + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # quote_token_program + AccountMeta( + pubkey=event_authority, is_signer=False, is_writable=False + ), # event_authority + AccountMeta( + pubkey=RAYDIUM_LAUNCHLAB_PROGRAM_ID, is_signer=False, is_writable=False + ), # program + ] + + # Add remaining accounts (not explicitly listed in IDL but required by the program) + # These accounts are used for fee collection during swaps + accounts.append( + AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False) + ) # #16: System Program + accounts.append( + AccountMeta(pubkey=platform_fee_vault, is_signer=False, is_writable=True) + ) # #17: Platform fee vault + accounts.append( + AccountMeta(pubkey=creator_fee_vault, is_signer=False, is_writable=True) + ) # #18: Creator fee vault + + # Instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate + instruction_data = ( + BUY_EXACT_IN_DISCRIMINATOR + + struct.pack(" Pubkey: + """ + Derive the authority PDA for the Raydium LaunchLab program. + + This PDA acts as the authority for pool vault operations and is generated + using the AUTH_SEED as specified in the IDL. + + Returns: + Pubkey: The derived authority PDA + """ + AUTH_SEED = b"vault_auth_seed" + authority_pda, _ = Pubkey.find_program_address( + [AUTH_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return authority_pda + + +def derive_event_authority_pda() -> Pubkey: + """ + Derive the event authority PDA for the Raydium LaunchLab program. + + This PDA is used for emitting program events during swaps. + + Returns: + Pubkey: The derived event authority PDA + """ + EVENT_AUTHORITY_SEED = b"__event_authority" + event_authority_pda, _ = Pubkey.find_program_address( + [EVENT_AUTHORITY_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return event_authority_pda + + +def derive_pool_state_for_token(base_token_mint: Pubkey) -> Pubkey | None: + """ + Derive the pool state account for a given base token mint. + + Args: + base_token_mint: The token mint address to search for + + Returns: + Pubkey of the pool state account, or None if not found + """ + seeds = [b"pool", bytes(base_token_mint), bytes(WSOL_MINT)] + pool_state_pda, _ = Pubkey.find_program_address(seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID) + return pool_state_pda + + +def derive_creator_fee_vault(creator: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Derive the creator fee vault PDA. + + This vault accumulates creator fees from trades. + + Args: + creator: The pool creator's pubkey + quote_mint: The quote token mint (WSOL) + + Returns: + Pubkey of the creator fee vault + """ + seeds = [bytes(creator), bytes(quote_mint)] + creator_fee_vault_pda, _ = Pubkey.find_program_address( + seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return creator_fee_vault_pda + + +def derive_platform_fee_vault(platform_config: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Derive the platform fee vault PDA. + + This vault accumulates platform fees from trades. + + Args: + platform_config: The platform config account + quote_mint: The quote token mint (WSOL) + + Returns: + Pubkey of the platform fee vault + """ + seeds = [bytes(platform_config), bytes(quote_mint)] + platform_fee_vault_pda, _ = Pubkey.find_program_address( + seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return platform_fee_vault_pda + + +def decode_pool_state(account_data: bytes) -> dict | None: + """ + Decode pool state account data using the IDL parser. + + Args: + account_data: Raw account data from the pool state account + + Returns: + Dictionary containing decoded pool state data, or None if decoding fails + """ + try: + result = IDL_PARSER.decode_account_data( + account_data, "PoolState", skip_discriminator=True + ) + if result: + return result + + return None + + except Exception as e: + print(f"Error decoding pool state: {e}") + import traceback + + traceback.print_exc() + return None + + +async def get_pool_state_data(client: AsyncClient, pool_state: Pubkey) -> dict | None: + """ + Get and decode the pool state account data. + + Args: + client: Solana RPC client + pool_state: The pool state account address + + Returns: + Dictionary containing decoded pool state data, or None if error + """ + try: + account_info = await client.get_account_info(pool_state) + if not account_info.value: + print("Pool state account not found") + return None + + return decode_pool_state(account_info.value.data) + + except Exception as e: + print(f"Error getting pool state data: {e}") + return None + + +def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey: + """ + Calculate the associated token account address for a given owner and mint. + + This manually implements the ATA derivation without requiring the spl-token package. + + Args: + owner: The wallet that owns the token account + mint: The token mint address + + Returns: + Pubkey of the associated token account + """ + ata_address, _ = Pubkey.find_program_address( + [bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)], + ASSOCIATED_TOKEN_PROGRAM_ID, + ) + return ata_address + + +def create_associated_token_account_idempotent_instruction( + payer: Pubkey, owner: Pubkey, mint: Pubkey +) -> Instruction: + """ + Create an idempotent instruction to create an Associated Token Account. + + This uses the CreateIdempotent instruction which doesn't fail if the ATA already exists. + + Args: + payer: The account that will pay for the creation + owner: The owner of the new token account + mint: The token mint + + Returns: + Instruction for creating the ATA idempotently + """ + ata_address = get_associated_token_address(owner, mint) + + accounts = [ + AccountMeta(pubkey=payer, is_signer=True, is_writable=True), # Funding account + AccountMeta( + pubkey=ata_address, is_signer=False, is_writable=True + ), # Associated token account + AccountMeta(pubkey=owner, is_signer=False, is_writable=False), # Wallet address + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), # Token mint + AccountMeta( + pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False + ), # System program + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # Token program + ] + + data = bytes([1]) + + return Instruction( + program_id=ASSOCIATED_TOKEN_PROGRAM_ID, data=data, accounts=accounts + ) + + +def create_initialize_account_instruction( + account: Pubkey, mint: Pubkey, owner: Pubkey +) -> Instruction: + """ + Create an InitializeAccount instruction for the Token Program. + + Args: + account: The account to initialize + mint: The token mint + owner: The account owner + + Returns: + Instruction for initializing the account + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=owner, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False), + ] + + # InitializeAccount instruction discriminator (instruction 1 in Token Program) + data = bytes([1]) + + return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts) + + +def create_close_account_instruction( + account: Pubkey, destination: Pubkey, owner: Pubkey +) -> Instruction: + """ + Create a CloseAccount instruction for the Token Program. + + Args: + account: The account to close + destination: Where to send the remaining lamports + owner: The account owner (must sign) + + Returns: + Instruction for closing the account + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + AccountMeta(pubkey=destination, is_signer=False, is_writable=True), + AccountMeta(pubkey=owner, is_signer=True, is_writable=False), + ] + + data = bytes([9]) + + return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts) + + +def create_wsol_account_with_seed( + payer: Pubkey, seed: str, lamports: int +) -> tuple[Pubkey, Instruction, Instruction]: + """ + Create a WSOL account using createAccountWithSeed and initialize it. + + This replicates the exact pattern from the Solscan example where a new account + is created with a seed and then initialized as a token account. + + Args: + payer: The account that will pay for and own the new account + seed: String seed for deterministic account generation + lamports: Amount of lamports to transfer to the new account + + Returns: + Tuple of (new_account_pubkey, create_instruction, initialize_instruction) + """ + new_account = Pubkey.create_with_seed(payer, seed, TOKEN_PROGRAM_ID) + + create_ix = create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=payer, + to_pubkey=new_account, + base=payer, + seed=seed, + lamports=lamports, + space=165, # Size of a token account + owner=TOKEN_PROGRAM_ID, + ) + ) + + initialize_ix = create_initialize_account_instruction(new_account, WSOL_MINT, payer) + + return new_account, create_ix, initialize_ix + + +def get_user_base_token_account(payer: Pubkey, base_mint: Pubkey) -> Pubkey: + """ + Get the user's associated token account for the base token. + + In a real implementation, this should check if the account exists and create it if needed. + For this example, we'll derive the standard ATA address. + + Args: + payer: The user's wallet address + base_mint: The base token mint address + + Returns: + Pubkey of the user's base token account + """ + return get_associated_token_address(payer, base_mint) + + +def calculate_maximum_amount_in_from_pool_state( + pool_state_data: dict, amount_out: int, slippage_tolerance: float +) -> int: + """ + Calculate the maximum amount in based on pool state data and slippage tolerance. + + Uses the actual pool reserves to calculate expected input using constant product formula. + For buy_exact_out, we know the output amount and need to calculate the required input. + + Args: + pool_state_data: Decoded pool state data containing reserves + amount_out: Amount of base tokens to receive (exact output) + slippage_tolerance: Slippage tolerance as a decimal (0.25 = 25%) + + Returns: + Maximum amount of quote tokens to spend + """ + try: + # Extract pool reserves from decoded state + virtual_base = pool_state_data["virtual_base"] + virtual_quote = pool_state_data["virtual_quote"] + real_base = pool_state_data["real_base"] + real_quote = pool_state_data["real_quote"] + + print("Pool State:") + print(f" Virtual Base: {virtual_base:,}") + print(f" Virtual Quote: {virtual_quote:,}") + print(f" Real Base: {real_base:,}") + print(f" Real Quote: {real_quote:,}") + + # Use virtual reserves for bonding curve calculation + # For exact output, we need to solve: amount_out = (amount_in * virtual_base) / (virtual_quote + amount_in) + # Rearranging: amount_in = (amount_out * virtual_quote) / (virtual_base - amount_out) + + if virtual_base <= amount_out: + raise ValueError( + f"Amount out ({amount_out}) cannot be >= virtual base reserves ({virtual_base})" + ) + + # Calculate required input using rearranged constant product formula + numerator = amount_out * virtual_quote + denominator = virtual_base - amount_out + expected_input = numerator // denominator + + # Apply slippage tolerance (add buffer for price movement) + maximum_with_slippage = int(expected_input * (1 + slippage_tolerance)) + + print(f"Amount out: {amount_out:,} tokens") + print( + f"Expected input: {expected_input:,} lamports ({expected_input / LAMPORTS_PER_SOL:.6f} SOL)" + ) + print( + f"Maximum with {slippage_tolerance * 100}% slippage: {maximum_with_slippage:,} lamports ({maximum_with_slippage / LAMPORTS_PER_SOL:.6f} SOL)" + ) + + return maximum_with_slippage + + except Exception as e: + print(f"Error calculating maximum amount in from pool state: {e}") + return None + + +async def buy_exact_out( + client: AsyncClient, + base_token_mint: Pubkey, + amount_out: int, + slippage_tolerance: float, +) -> str | None: + """ + Execute a buy_exact_out transaction on Raydium LaunchLab. + + This function implements the exact transaction flow similar to buy_exact_in: + 1. SetComputeUnitPrice + 2. SetComputeUnitLimit + 3. Create Associated Token Account for base token (idempotent) + 4. Create WSOL account with seed + 5. Initialize WSOL account + 6. Execute buy_exact_out instruction + 7. Close WSOL account + + Args: + client: Solana RPC client + base_token_mint: Address of the token to buy + amount_out: Exact amount of tokens to receive + slippage_tolerance: Slippage tolerance as decimal + + Returns: + Transaction signature if successful, None otherwise + """ + try: + print(f"Finding pool state for token: {base_token_mint}") + pool_state = derive_pool_state_for_token(base_token_mint) + if not pool_state: + print("Pool state not found for this token") + return None + + # Get and decode pool state data using IDL parser + pool_state_data = await get_pool_state_data(client, pool_state) + if not pool_state_data: + print("Failed to decode pool state data") + return None + + # Extract vault addresses and creator from decoded pool state (convert from base58 strings to Pubkey objects) + base_vault = Pubkey.from_string(pool_state_data["base_vault"]) + quote_vault = Pubkey.from_string(pool_state_data["quote_vault"]) + creator = Pubkey.from_string(pool_state_data["creator"]) + + print(f"Found pool state: {pool_state}") + print(f"Base vault: {base_vault}") + print(f"Quote vault: {quote_vault}") + print(f"Creator: {creator}") + print(f"Pool status: {pool_state_data['status']}") + + # Derive necessary PDAs + authority = derive_authority_pda() + event_authority = derive_event_authority_pda() + creator_fee_vault = derive_creator_fee_vault(creator, WSOL_MINT) + platform_fee_vault = derive_platform_fee_vault( + LETSBONK_PLATFORM_CONFIG, WSOL_MINT + ) + + print(f"Creator fee vault: {creator_fee_vault}") + print(f"Platform fee vault: {platform_fee_vault}") + + # Calculate amounts using pool state data + maximum_amount_in = calculate_maximum_amount_in_from_pool_state( + pool_state_data, amount_out, slippage_tolerance + ) + + if maximum_amount_in is None: + print("Failed to calculate maximum amount in") + return None + + print(f"Amount out: {amount_out} tokens") + print( + f"Maximum amount in: {maximum_amount_in} lamports ({maximum_amount_in / LAMPORTS_PER_SOL:.6f} SOL)" + ) + + # Step 1: Create Associated Token Account for base token (idempotent) + user_base_token = get_associated_token_address(PAYER.pubkey(), base_token_mint) + create_ata_ix = create_associated_token_account_idempotent_instruction( + PAYER.pubkey(), PAYER.pubkey(), base_token_mint + ) + + # Step 2: Create WSOL account with seed + import hashlib + import time + + # Generate a unique seed based on timestamp and user pubkey + seed_data = f"{int(time.time())}{PAYER.pubkey()!s}" + wsol_seed = hashlib.sha256(seed_data.encode()).hexdigest()[:32] + + # Calculate required lamports (maximum_amount_in + small buffer for account creation) + account_creation_lamports = 2_039_280 # Standard account creation cost + total_lamports = maximum_amount_in + account_creation_lamports + + user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed( + PAYER.pubkey(), wsol_seed, total_lamports + ) + + print(f"User base token account: {user_base_token}") + print(f"User quote token account: {user_quote_token}") + + # Step 3: Build the buy_exact_out instruction + accounts = [ + AccountMeta( + pubkey=PAYER.pubkey(), is_signer=True, is_writable=False + ), # payer + AccountMeta( + pubkey=authority, is_signer=False, is_writable=False + ), # authority + AccountMeta( + pubkey=GLOBAL_CONFIG, is_signer=False, is_writable=False + ), # global_config + AccountMeta( + pubkey=LETSBONK_PLATFORM_CONFIG, is_signer=False, is_writable=False + ), # platform_config + AccountMeta( + pubkey=pool_state, is_signer=False, is_writable=True + ), # pool_state + AccountMeta( + pubkey=user_base_token, is_signer=False, is_writable=True + ), # user_base_token + AccountMeta( + pubkey=user_quote_token, is_signer=False, is_writable=True + ), # user_quote_token + AccountMeta( + pubkey=base_vault, is_signer=False, is_writable=True + ), # base_vault + AccountMeta( + pubkey=quote_vault, is_signer=False, is_writable=True + ), # quote_vault + AccountMeta( + pubkey=base_token_mint, is_signer=False, is_writable=False + ), # base_token_mint + AccountMeta( + pubkey=WSOL_MINT, is_signer=False, is_writable=False + ), # quote_token_mint + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # base_token_program + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # quote_token_program + AccountMeta( + pubkey=event_authority, is_signer=False, is_writable=False + ), # event_authority + AccountMeta( + pubkey=RAYDIUM_LAUNCHLAB_PROGRAM_ID, is_signer=False, is_writable=False + ), # program + ] + + # Add remaining accounts (not explicitly listed in IDL but required by the program) + # These accounts are used for fee collection during swaps + accounts.append( + AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False) + ) # #16: System Program + accounts.append( + AccountMeta(pubkey=platform_fee_vault, is_signer=False, is_writable=True) + ) # #17: Platform fee vault + accounts.append( + AccountMeta(pubkey=creator_fee_vault, is_signer=False, is_writable=True) + ) # #18: Creator fee vault + + # Instruction data: discriminator + amount_out + maximum_amount_in + share_fee_rate + instruction_data = ( + BUY_EXACT_OUT_DISCRIMINATOR + + struct.pack(" Pubkey: + """ + Derive the authority PDA for the Raydium LaunchLab program. + + This PDA acts as the authority for pool vault operations and is generated + using the AUTH_SEED as specified in the IDL. + + Returns: + Pubkey: The derived authority PDA + """ + AUTH_SEED = b"vault_auth_seed" + authority_pda, _ = Pubkey.find_program_address( + [AUTH_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return authority_pda + + +def derive_event_authority_pda() -> Pubkey: + """ + Derive the event authority PDA for the Raydium LaunchLab program. + + This PDA is used for emitting program events during swaps. + + Returns: + Pubkey: The derived event authority PDA + """ + EVENT_AUTHORITY_SEED = b"__event_authority" + event_authority_pda, _ = Pubkey.find_program_address( + [EVENT_AUTHORITY_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return event_authority_pda + + +def derive_pool_state_for_token(base_token_mint: Pubkey) -> Pubkey | None: + """ + Derive the pool state account for a given base token mint. + + Args: + base_token_mint: The token mint address to search for + + Returns: + Pubkey of the pool state account, or None if not found + """ + seeds = [b"pool", bytes(base_token_mint), bytes(WSOL_MINT)] + pool_state_pda, _ = Pubkey.find_program_address(seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID) + return pool_state_pda + + +def derive_creator_fee_vault(creator: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Derive the creator fee vault PDA. + + This vault accumulates creator fees from trades. + + Args: + creator: The pool creator's pubkey + quote_mint: The quote token mint (WSOL) + + Returns: + Pubkey of the creator fee vault + """ + seeds = [bytes(creator), bytes(quote_mint)] + creator_fee_vault_pda, _ = Pubkey.find_program_address( + seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return creator_fee_vault_pda + + +def derive_platform_fee_vault(platform_config: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Derive the platform fee vault PDA. + + This vault accumulates platform fees from trades. + + Args: + platform_config: The platform config account + quote_mint: The quote token mint (WSOL) + + Returns: + Pubkey of the platform fee vault + """ + seeds = [bytes(platform_config), bytes(quote_mint)] + platform_fee_vault_pda, _ = Pubkey.find_program_address( + seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return platform_fee_vault_pda + + +def decode_pool_state(account_data: bytes) -> dict | None: + """ + Decode pool state account data using the IDL parser. + + Args: + account_data: Raw account data from the pool state account + + Returns: + Dictionary containing decoded pool state data, or None if decoding fails + """ + try: + result = IDL_PARSER.decode_account_data( + account_data, "PoolState", skip_discriminator=True + ) + if result: + return result + + return None + + except Exception as e: + print(f"Error decoding pool state: {e}") + import traceback + + traceback.print_exc() + return None + + +async def get_pool_state_data(client: AsyncClient, pool_state: Pubkey) -> dict | None: + """ + Get and decode the pool state account data. + + Args: + client: Solana RPC client + pool_state: The pool state account address + + Returns: + Dictionary containing decoded pool state data, or None if error + """ + try: + account_info = await client.get_account_info(pool_state) + if not account_info.value: + print("Pool state account not found") + return None + + return decode_pool_state(account_info.value.data) + + except Exception as e: + print(f"Error getting pool state data: {e}") + return None + + +def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey: + """ + Calculate the associated token account address for a given owner and mint. + + This manually implements the ATA derivation without requiring the spl-token package. + + Args: + owner: The wallet that owns the token account + mint: The token mint address + + Returns: + Pubkey of the associated token account + """ + ata_address, _ = Pubkey.find_program_address( + [bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)], + ASSOCIATED_TOKEN_PROGRAM_ID, + ) + return ata_address + + +def create_associated_token_account_idempotent_instruction( + payer: Pubkey, owner: Pubkey, mint: Pubkey +) -> Instruction: + """ + Create an idempotent instruction to create an Associated Token Account. + + This uses the CreateIdempotent instruction which doesn't fail if the ATA already exists. + + Args: + payer: The account that will pay for the creation + owner: The owner of the new token account + mint: The token mint + + Returns: + Instruction for creating the ATA idempotently + """ + ata_address = get_associated_token_address(owner, mint) + + accounts = [ + AccountMeta(pubkey=payer, is_signer=True, is_writable=True), # Funding account + AccountMeta( + pubkey=ata_address, is_signer=False, is_writable=True + ), # Associated token account + AccountMeta(pubkey=owner, is_signer=False, is_writable=False), # Wallet address + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), # Token mint + AccountMeta( + pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False + ), # System program + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # Token program + ] + + data = bytes([1]) + + return Instruction( + program_id=ASSOCIATED_TOKEN_PROGRAM_ID, data=data, accounts=accounts + ) + + +def create_initialize_account_instruction( + account: Pubkey, mint: Pubkey, owner: Pubkey +) -> Instruction: + """ + Create an InitializeAccount instruction for the Token Program. + + Args: + account: The account to initialize + mint: The token mint + owner: The account owner + + Returns: + Instruction for initializing the account + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=owner, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False), + ] + + # InitializeAccount instruction discriminator (instruction 1 in Token Program) + data = bytes([1]) + + return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts) + + +def create_close_account_instruction( + account: Pubkey, destination: Pubkey, owner: Pubkey +) -> Instruction: + """ + Create a CloseAccount instruction for the Token Program. + + Args: + account: The account to close + destination: Where to send the remaining lamports + owner: The account owner (must sign) + + Returns: + Instruction for closing the account + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + AccountMeta(pubkey=destination, is_signer=False, is_writable=True), + AccountMeta(pubkey=owner, is_signer=True, is_writable=False), + ] + + data = bytes([9]) + + return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts) + + +def create_wsol_account_with_seed( + payer: Pubkey, seed: str, lamports: int +) -> tuple[Pubkey, Instruction, Instruction]: + """ + Create a WSOL account using createAccountWithSeed and initialize it. + + This replicates the exact pattern from the Solscan example where a new account + is created with a seed and then initialized as a token account. + + Args: + payer: The account that will pay for and own the new account + seed: String seed for deterministic account generation + lamports: Amount of lamports to transfer to the new account + + Returns: + Tuple of (new_account_pubkey, create_instruction, initialize_instruction) + """ + new_account = Pubkey.create_with_seed(payer, seed, TOKEN_PROGRAM_ID) + + create_ix = create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=payer, + to_pubkey=new_account, + base=payer, + seed=seed, + lamports=lamports, + space=165, # Size of a token account + owner=TOKEN_PROGRAM_ID, + ) + ) + + initialize_ix = create_initialize_account_instruction(new_account, WSOL_MINT, payer) + + return new_account, create_ix, initialize_ix + + +def get_user_base_token_account(payer: Pubkey, base_mint: Pubkey) -> Pubkey: + """ + Get the user's associated token account for the base token. + + In a real implementation, this should check if the account exists and create it if needed. + For this example, we'll derive the standard ATA address. + + Args: + payer: The user's wallet address + base_mint: The base token mint address + + Returns: + Pubkey of the user's base token account + """ + return get_associated_token_address(payer, base_mint) + + +def calculate_minimum_amount_out_from_pool_state( + pool_state_data: dict, amount_in: int, slippage_tolerance: float +) -> int: + """ + Calculate the minimum amount out based on pool state data and slippage tolerance. + + Uses the actual pool reserves to calculate expected output using constant product formula. + This is for selling base tokens to get quote tokens (WSOL). + + Args: + pool_state_data: Decoded pool state data containing reserves + amount_in: Amount of base tokens being sold + slippage_tolerance: Slippage tolerance as a decimal (0.25 = 25%) + + Returns: + Minimum amount of quote tokens (WSOL) to receive + """ + try: + # Extract pool reserves from decoded state + virtual_base = pool_state_data["virtual_base"] + virtual_quote = pool_state_data["virtual_quote"] + real_base = pool_state_data["real_base"] + real_quote = pool_state_data["real_quote"] + + print("Pool State:") + print(f" Virtual Base: {virtual_base:,}") + print(f" Virtual Quote: {virtual_quote:,}") + print(f" Real Base: {real_base:,}") + print(f" Real Quote: {real_quote:,}") + + # Use virtual reserves for bonding curve calculation + # For selling base tokens: amount_out = (amount_in * virtual_quote) / (virtual_base + amount_in) + + # Calculate expected output using constant product formula + # Note: The program deducts fees before calculating output, so we need to account for that + # Trade fee is typically around 0.25% - 1% + # For safety, we'll calculate without fee adjustment and let slippage handle it + numerator = amount_in * virtual_quote + denominator = virtual_base + amount_in + expected_output = numerator // denominator + + # Apply slippage tolerance (be more conservative for small amounts) + minimum_with_slippage = int(expected_output * (1 - slippage_tolerance)) + + print(f"Amount in: {amount_in:,} tokens") + print( + f"Expected output: {expected_output:,} lamports ({expected_output / LAMPORTS_PER_SOL:.6f} SOL)" + ) + print( + f"Minimum with {slippage_tolerance * 100}% slippage: {minimum_with_slippage:,} lamports ({minimum_with_slippage / LAMPORTS_PER_SOL:.6f} SOL)" + ) + + return minimum_with_slippage + + except Exception as e: + print(f"Error calculating minimum amount out from pool state: {e}") + return None + + +async def sell_exact_in( + client: AsyncClient, + base_token_mint: Pubkey, + amount_in_tokens: int, + slippage_tolerance: float, +) -> str | None: + """ + Execute a sell_exact_in transaction on Raydium LaunchLab. + + This function implements the exact transaction flow similar to buy_exact_in: + 1. SetComputeUnitPrice + 2. SetComputeUnitLimit + 3. Create WSOL account with seed + 4. Initialize WSOL account + 5. Execute sell_exact_in instruction + 6. Close WSOL account + 7. Optional: Transfer remaining SOL (as seen in the example) + + Args: + client: Solana RPC client + base_token_mint: Address of the token to sell + amount_in_tokens: Amount of tokens to sell + slippage_tolerance: Slippage tolerance as decimal + + Returns: + Transaction signature if successful, None otherwise + """ + try: + print(f"Finding pool state for token: {base_token_mint}") + pool_state = derive_pool_state_for_token(base_token_mint) + if not pool_state: + print("Pool state not found for this token") + return None + + # Get and decode pool state data using IDL parser + pool_state_data = await get_pool_state_data(client, pool_state) + if not pool_state_data: + print("Failed to decode pool state data") + return None + + # Extract vault addresses and creator from decoded pool state (convert from base58 strings to Pubkey objects) + base_vault = Pubkey.from_string(pool_state_data["base_vault"]) + quote_vault = Pubkey.from_string(pool_state_data["quote_vault"]) + creator = Pubkey.from_string(pool_state_data["creator"]) + + print(f"Found pool state: {pool_state}") + print(f"Base vault: {base_vault}") + print(f"Quote vault: {quote_vault}") + print(f"Creator: {creator}") + print(f"Pool status: {pool_state_data['status']}") + + # Derive necessary PDAs + authority = derive_authority_pda() + event_authority = derive_event_authority_pda() + creator_fee_vault = derive_creator_fee_vault(creator, WSOL_MINT) + platform_fee_vault = derive_platform_fee_vault( + LETSBONK_PLATFORM_CONFIG, WSOL_MINT + ) + + print(f"Creator fee vault: {creator_fee_vault}") + print(f"Platform fee vault: {platform_fee_vault}") + + # Calculate amounts using pool state data + minimum_amount_out = calculate_minimum_amount_out_from_pool_state( + pool_state_data, amount_in_tokens, slippage_tolerance + ) + + if minimum_amount_out is None or minimum_amount_out == 0: + print("Failed to calculate minimum amount out or amount is too small") + return None + + print(f"Amount in: {amount_in_tokens:,} tokens") + print( + f"Minimum amount out: {minimum_amount_out:,} lamports ({minimum_amount_out / LAMPORTS_PER_SOL:.6f} SOL)" + ) + + # Get user's base token account (where tokens will be debited from) + user_base_token = get_associated_token_address(PAYER.pubkey(), base_token_mint) + + # Step 1: Create WSOL account with seed (where WSOL will be received) + import hashlib + import time + + # Generate a unique seed based on timestamp and user pubkey + seed_data = f"{int(time.time())}{PAYER.pubkey()!s}" + wsol_seed = hashlib.sha256(seed_data.encode()).hexdigest()[:32] + + # Calculate required lamports (minimal amount for account creation) + account_creation_lamports = 2_039_280 # Standard account creation cost + + user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed( + PAYER.pubkey(), wsol_seed, account_creation_lamports + ) + + print(f"User base token account: {user_base_token}") + print(f"User quote token account: {user_quote_token}") + + # Step 2: Build the sell_exact_in instruction + accounts = [ + AccountMeta( + pubkey=PAYER.pubkey(), is_signer=True, is_writable=False + ), # payer + AccountMeta( + pubkey=authority, is_signer=False, is_writable=False + ), # authority + AccountMeta( + pubkey=GLOBAL_CONFIG, is_signer=False, is_writable=False + ), # global_config + AccountMeta( + pubkey=LETSBONK_PLATFORM_CONFIG, is_signer=False, is_writable=False + ), # platform_config + AccountMeta( + pubkey=pool_state, is_signer=False, is_writable=True + ), # pool_state + AccountMeta( + pubkey=user_base_token, is_signer=False, is_writable=True + ), # user_base_token (tokens being sold) + AccountMeta( + pubkey=user_quote_token, is_signer=False, is_writable=True + ), # user_quote_token (WSOL received) + AccountMeta( + pubkey=base_vault, is_signer=False, is_writable=True + ), # base_vault (receives tokens) + AccountMeta( + pubkey=quote_vault, is_signer=False, is_writable=True + ), # quote_vault (sends WSOL) + AccountMeta( + pubkey=base_token_mint, is_signer=False, is_writable=False + ), # base_token_mint + AccountMeta( + pubkey=WSOL_MINT, is_signer=False, is_writable=False + ), # quote_token_mint + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # base_token_program + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # quote_token_program + AccountMeta( + pubkey=event_authority, is_signer=False, is_writable=False + ), # event_authority + AccountMeta( + pubkey=RAYDIUM_LAUNCHLAB_PROGRAM_ID, is_signer=False, is_writable=False + ), # program + ] + + # Add remaining accounts (not explicitly listed in IDL but required by the program) + # These accounts are used for fee collection during swaps + accounts.append( + AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False) + ) # #16: System Program + accounts.append( + AccountMeta(pubkey=platform_fee_vault, is_signer=False, is_writable=True) + ) # #17: Platform fee vault + accounts.append( + AccountMeta(pubkey=creator_fee_vault, is_signer=False, is_writable=True) + ) # #18: Creator fee vault + + # Instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate + instruction_data = ( + SELL_EXACT_IN_DISCRIMINATOR + + struct.pack(" Pubkey: + """ + Derive the authority PDA for the Raydium LaunchLab program. + + This PDA acts as the authority for pool vault operations and is generated + using the AUTH_SEED as specified in the IDL. + + Returns: + Pubkey: The derived authority PDA + """ + AUTH_SEED = b"vault_auth_seed" + authority_pda, _ = Pubkey.find_program_address( + [AUTH_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return authority_pda + + +def derive_event_authority_pda() -> Pubkey: + """ + Derive the event authority PDA for the Raydium LaunchLab program. + + This PDA is used for emitting program events during swaps. + + Returns: + Pubkey: The derived event authority PDA + """ + EVENT_AUTHORITY_SEED = b"__event_authority" + event_authority_pda, _ = Pubkey.find_program_address( + [EVENT_AUTHORITY_SEED], RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return event_authority_pda + + +def derive_pool_state_for_token(base_token_mint: Pubkey) -> Pubkey | None: + """ + Derive the pool state account for a given base token mint. + + Args: + base_token_mint: The token mint address to search for + + Returns: + Pubkey of the pool state account, or None if not found + """ + seeds = [b"pool", bytes(base_token_mint), bytes(WSOL_MINT)] + pool_state_pda, _ = Pubkey.find_program_address(seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID) + return pool_state_pda + + +def derive_creator_fee_vault(creator: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Derive the creator fee vault PDA. + + This vault accumulates creator fees from trades. + + Args: + creator: The pool creator's pubkey + quote_mint: The quote token mint (WSOL) + + Returns: + Pubkey of the creator fee vault + """ + seeds = [bytes(creator), bytes(quote_mint)] + creator_fee_vault_pda, _ = Pubkey.find_program_address( + seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return creator_fee_vault_pda + + +def derive_platform_fee_vault(platform_config: Pubkey, quote_mint: Pubkey) -> Pubkey: + """ + Derive the platform fee vault PDA. + + This vault accumulates platform fees from trades. + + Args: + platform_config: The platform config account + quote_mint: The quote token mint (WSOL) + + Returns: + Pubkey of the platform fee vault + """ + seeds = [bytes(platform_config), bytes(quote_mint)] + platform_fee_vault_pda, _ = Pubkey.find_program_address( + seeds, RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return platform_fee_vault_pda + + +def decode_pool_state(account_data: bytes) -> dict | None: + """ + Decode pool state account data using the IDL parser. + + Args: + account_data: Raw account data from the pool state account + + Returns: + Dictionary containing decoded pool state data, or None if decoding fails + """ + try: + result = IDL_PARSER.decode_account_data( + account_data, "PoolState", skip_discriminator=True + ) + if result: + return result + + return None + + except Exception as e: + print(f"Error decoding pool state: {e}") + import traceback + + traceback.print_exc() + return None + + +async def get_pool_state_data(client: AsyncClient, pool_state: Pubkey) -> dict | None: + """ + Get and decode the pool state account data. + + Args: + client: Solana RPC client + pool_state: The pool state account address + + Returns: + Dictionary containing decoded pool state data, or None if error + """ + try: + account_info = await client.get_account_info(pool_state) + if not account_info.value: + print("Pool state account not found") + return None + + return decode_pool_state(account_info.value.data) + + except Exception as e: + print(f"Error getting pool state data: {e}") + return None + + +def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey: + """ + Calculate the associated token account address for a given owner and mint. + + This manually implements the ATA derivation without requiring the spl-token package. + + Args: + owner: The wallet that owns the token account + mint: The token mint address + + Returns: + Pubkey of the associated token account + """ + ata_address, _ = Pubkey.find_program_address( + [bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)], + ASSOCIATED_TOKEN_PROGRAM_ID, + ) + return ata_address + + +def create_associated_token_account_idempotent_instruction( + payer: Pubkey, owner: Pubkey, mint: Pubkey +) -> Instruction: + """ + Create an idempotent instruction to create an Associated Token Account. + + This uses the CreateIdempotent instruction which doesn't fail if the ATA already exists. + + Args: + payer: The account that will pay for the creation + owner: The owner of the new token account + mint: The token mint + + Returns: + Instruction for creating the ATA idempotently + """ + ata_address = get_associated_token_address(owner, mint) + + accounts = [ + AccountMeta(pubkey=payer, is_signer=True, is_writable=True), # Funding account + AccountMeta( + pubkey=ata_address, is_signer=False, is_writable=True + ), # Associated token account + AccountMeta(pubkey=owner, is_signer=False, is_writable=False), # Wallet address + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), # Token mint + AccountMeta( + pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False + ), # System program + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # Token program + ] + + data = bytes([1]) + + return Instruction( + program_id=ASSOCIATED_TOKEN_PROGRAM_ID, data=data, accounts=accounts + ) + + +def create_initialize_account_instruction( + account: Pubkey, mint: Pubkey, owner: Pubkey +) -> Instruction: + """ + Create an InitializeAccount instruction for the Token Program. + + Args: + account: The account to initialize + mint: The token mint + owner: The account owner + + Returns: + Instruction for initializing the account + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=owner, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False), + ] + + # InitializeAccount instruction discriminator (instruction 1 in Token Program) + data = bytes([1]) + + return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts) + + +def create_close_account_instruction( + account: Pubkey, destination: Pubkey, owner: Pubkey +) -> Instruction: + """ + Create a CloseAccount instruction for the Token Program. + + Args: + account: The account to close + destination: Where to send the remaining lamports + owner: The account owner (must sign) + + Returns: + Instruction for closing the account + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + AccountMeta(pubkey=destination, is_signer=False, is_writable=True), + AccountMeta(pubkey=owner, is_signer=True, is_writable=False), + ] + + data = bytes([9]) + + return Instruction(program_id=TOKEN_PROGRAM_ID, data=data, accounts=accounts) + + +def create_wsol_account_with_seed( + payer: Pubkey, seed: str, lamports: int +) -> tuple[Pubkey, Instruction, Instruction]: + """ + Create a WSOL account using createAccountWithSeed and initialize it. + + This replicates the exact pattern from the Solscan example where a new account + is created with a seed and then initialized as a token account. + + Args: + payer: The account that will pay for and own the new account + seed: String seed for deterministic account generation + lamports: Amount of lamports to transfer to the new account + + Returns: + Tuple of (new_account_pubkey, create_instruction, initialize_instruction) + """ + new_account = Pubkey.create_with_seed(payer, seed, TOKEN_PROGRAM_ID) + + create_ix = create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=payer, + to_pubkey=new_account, + base=payer, + seed=seed, + lamports=lamports, + space=165, # Size of a token account + owner=TOKEN_PROGRAM_ID, + ) + ) + + initialize_ix = create_initialize_account_instruction(new_account, WSOL_MINT, payer) + + return new_account, create_ix, initialize_ix + + +def get_user_base_token_account(payer: Pubkey, base_mint: Pubkey) -> Pubkey: + """ + Get the user's associated token account for the base token. + + In a real implementation, this should check if the account exists and create it if needed. + For this example, we'll derive the standard ATA address. + + Args: + payer: The user's wallet address + base_mint: The base token mint address + + Returns: + Pubkey of the user's base token account + """ + return get_associated_token_address(payer, base_mint) + + +def calculate_maximum_amount_in_from_pool_state( + pool_state_data: dict, amount_out: int, slippage_tolerance: float +) -> int: + """ + Calculate the maximum amount in based on pool state data and slippage tolerance. + + Uses the actual pool reserves to calculate required input using constant product formula. + This is for selling base tokens to get an exact amount of quote tokens (WSOL). + + Args: + pool_state_data: Decoded pool state data containing reserves + amount_out: Amount of quote tokens (WSOL) desired to receive + slippage_tolerance: Slippage tolerance as a decimal (0.25 = 25%) + + Returns: + Maximum amount of base tokens to sell + """ + try: + # Extract pool reserves from decoded state + virtual_base = pool_state_data["virtual_base"] + virtual_quote = pool_state_data["virtual_quote"] + real_base = pool_state_data["real_base"] + real_quote = pool_state_data["real_quote"] + + print("Pool State:") + print(f" Virtual Base: {virtual_base:,}") + print(f" Virtual Quote: {virtual_quote:,}") + print(f" Real Base: {real_base:,}") + print(f" Real Quote: {real_quote:,}") + + # Use virtual reserves for bonding curve calculation + # For selling base tokens to get exact quote: amount_in = (amount_out * virtual_base) / (virtual_quote - amount_out) + # This is the inverse of the sell formula + + # Calculate required input using constant product formula + numerator = amount_out * virtual_base + denominator = virtual_quote - amount_out + + if denominator <= 0: + print("Error: Amount out is too large for current pool state") + return None + + expected_input = numerator // denominator + + # Apply slippage tolerance (allow selling more tokens than expected) + maximum_with_slippage = int(expected_input * (1 + slippage_tolerance)) + + print( + f"Amount out: {amount_out:,} lamports ({amount_out / LAMPORTS_PER_SOL:.6f} SOL)" + ) + print(f"Expected input: {expected_input:,} tokens") + print( + f"Maximum with {slippage_tolerance * 100}% slippage: {maximum_with_slippage:,} tokens" + ) + + return maximum_with_slippage + + except Exception as e: + print(f"Error calculating maximum amount in from pool state: {e}") + return None + + +async def sell_exact_out( + client: AsyncClient, + base_token_mint: Pubkey, + amount_out_sol: float, + slippage_tolerance: float, +) -> str | None: + """ + Execute a sell_exact_out transaction on Raydium LaunchLab. + + This function implements the exact transaction flow similar to sell_exact_in: + 1. SetComputeUnitPrice + 2. SetComputeUnitLimit + 3. Create WSOL account with seed + 4. Initialize WSOL account + 5. Execute sell_exact_out instruction + 6. Close WSOL account + 7. Optional: Transfer remaining SOL (as seen in the example) + + Args: + client: Solana RPC client + base_token_mint: Address of the token to sell + amount_out_sol: Exact amount of SOL to receive + slippage_tolerance: Slippage tolerance as decimal + + Returns: + Transaction signature if successful, None otherwise + """ + try: + print(f"Finding pool state for token: {base_token_mint}") + pool_state = derive_pool_state_for_token(base_token_mint) + if not pool_state: + print("Pool state not found for this token") + return None + + # Get and decode pool state data using IDL parser + pool_state_data = await get_pool_state_data(client, pool_state) + if not pool_state_data: + print("Failed to decode pool state data") + return None + + # Extract vault addresses and creator from decoded pool state (convert from base58 strings to Pubkey objects) + base_vault = Pubkey.from_string(pool_state_data["base_vault"]) + quote_vault = Pubkey.from_string(pool_state_data["quote_vault"]) + creator = Pubkey.from_string(pool_state_data["creator"]) + + print(f"Found pool state: {pool_state}") + print(f"Base vault: {base_vault}") + print(f"Quote vault: {quote_vault}") + print(f"Creator: {creator}") + print(f"Pool status: {pool_state_data['status']}") + + # Derive necessary PDAs + authority = derive_authority_pda() + event_authority = derive_event_authority_pda() + creator_fee_vault = derive_creator_fee_vault(creator, WSOL_MINT) + platform_fee_vault = derive_platform_fee_vault( + LETSBONK_PLATFORM_CONFIG, WSOL_MINT + ) + + print(f"Creator fee vault: {creator_fee_vault}") + print(f"Platform fee vault: {platform_fee_vault}") + + # Calculate amounts using pool state data + amount_out = int(amount_out_sol * LAMPORTS_PER_SOL) + maximum_amount_in = calculate_maximum_amount_in_from_pool_state( + pool_state_data, amount_out, slippage_tolerance + ) + + if maximum_amount_in is None: + print("Failed to calculate maximum amount in") + return None + + print(f"Amount out: {amount_out:,} lamports ({amount_out_sol} SOL)") + print(f"Maximum amount in: {maximum_amount_in:,} tokens") + + # Get user's base token account (where tokens will be debited from) + user_base_token = get_associated_token_address(PAYER.pubkey(), base_token_mint) + + # Step 1: Create WSOL account with seed (where WSOL will be received) + import hashlib + import time + + # Generate a unique seed based on timestamp and user pubkey + seed_data = f"{int(time.time())}{PAYER.pubkey()!s}" + wsol_seed = hashlib.sha256(seed_data.encode()).hexdigest()[:32] + + # Calculate required lamports (minimal amount for account creation) + account_creation_lamports = 2_039_280 # Standard account creation cost + + user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed( + PAYER.pubkey(), wsol_seed, account_creation_lamports + ) + + print(f"User base token account: {user_base_token}") + print(f"User quote token account: {user_quote_token}") + + # Step 2: Build the sell_exact_out instruction + accounts = [ + AccountMeta( + pubkey=PAYER.pubkey(), is_signer=True, is_writable=False + ), # payer + AccountMeta( + pubkey=authority, is_signer=False, is_writable=False + ), # authority + AccountMeta( + pubkey=GLOBAL_CONFIG, is_signer=False, is_writable=False + ), # global_config + AccountMeta( + pubkey=LETSBONK_PLATFORM_CONFIG, is_signer=False, is_writable=False + ), # platform_config + AccountMeta( + pubkey=pool_state, is_signer=False, is_writable=True + ), # pool_state + AccountMeta( + pubkey=user_base_token, is_signer=False, is_writable=True + ), # user_base_token (tokens being sold) + AccountMeta( + pubkey=user_quote_token, is_signer=False, is_writable=True + ), # user_quote_token (WSOL received) + AccountMeta( + pubkey=base_vault, is_signer=False, is_writable=True + ), # base_vault (receives tokens) + AccountMeta( + pubkey=quote_vault, is_signer=False, is_writable=True + ), # quote_vault (sends WSOL) + AccountMeta( + pubkey=base_token_mint, is_signer=False, is_writable=False + ), # base_token_mint + AccountMeta( + pubkey=WSOL_MINT, is_signer=False, is_writable=False + ), # quote_token_mint + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # base_token_program + AccountMeta( + pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False + ), # quote_token_program + AccountMeta( + pubkey=event_authority, is_signer=False, is_writable=False + ), # event_authority + AccountMeta( + pubkey=RAYDIUM_LAUNCHLAB_PROGRAM_ID, is_signer=False, is_writable=False + ), # program + ] + + # Add remaining accounts (not explicitly listed in IDL but required by the program) + # These accounts are used for fee collection during swaps + accounts.append( + AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False) + ) # #16: System Program + accounts.append( + AccountMeta(pubkey=platform_fee_vault, is_signer=False, is_writable=True) + ) # #17: Platform fee vault + accounts.append( + AccountMeta(pubkey=creator_fee_vault, is_signer=False, is_writable=True) + ) # #18: Creator fee vault + + # Instruction data: discriminator + amount_out + maximum_amount_in + share_fee_rate + instruction_data = ( + SELL_EXACT_OUT_DISCRIMINATOR + + struct.pack("