diff --git a/idl/pump_fun_idl.json b/idl/pump_fun_idl.json index 35a90726..054c19a9 100644 --- a/idl/pump_fun_idl.json +++ b/idl/pump_fun_idl.json @@ -498,41 +498,8 @@ "path": "bonding_curve" }, { - "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", @@ -592,8 +559,7 @@ "address": "11111111111111111111111111111111" }, { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "name": "token_program" }, { "name": "creator_vault", @@ -661,7 +627,6 @@ }, { "name": "global_volume_accumulator", - "writable": true, "pda": { "seeds": [ { @@ -914,41 +879,8 @@ "path": "bonding_curve" }, { - "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", @@ -1008,8 +940,7 @@ "address": "11111111111111111111111111111111" }, { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "name": "token_program" }, { "name": "creator_vault", @@ -1077,7 +1008,6 @@ }, { "name": "global_volume_accumulator", - "writable": true, "pda": { "seeds": [ { @@ -2016,48 +1946,38 @@ ] }, { - "name": "extend_account", + "name": "create_v2", "docs": [ - "Extends the size of program-owned accounts" + "Creates a new spl-22 coin and bonding curve." ], "discriminator": [ - 234, - 102, - 194, - 203, - 150, - 72, - 62, - 229 + 214, + 144, + 76, + 236, + 95, + 139, + 49, + 180 ], "accounts": [ { - "name": "account", - "writable": true - }, - { - "name": "user", + "name": "mint", + "writable": true, "signer": true }, { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "event_authority", + "name": "mint_authority", "pda": { "seeds": [ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, + 109, + 105, 110, 116, - 95, + 45, 97, 117, 116, @@ -2073,130 +1993,134 @@ } }, { - "name": "program" - } - ], - "args": [] - }, - { - "name": "init_user_volume_accumulator", - "discriminator": [ - 94, - 6, - 202, - 115, - 255, - 96, - 232, - 183 - ], - "accounts": [ - { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "user" - }, - { - "name": "user_volume_accumulator", + "name": "bonding_curve", "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ - 117, - 115, - 101, - 114, - 95, - 118, + 98, 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, + 110, + 100, + 105, + 110, + 103, + 45, 99, 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 114, + 118, + 101 ] }, { "kind": "account", - "path": "user" + "path": "mint" } ] } }, { - "name": "system_program", - "address": "11111111111111111111111111111111" + "name": "associated_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "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": "event_authority", + "name": "global", "pda": { "seeds": [ { "kind": "const", "value": [ - 95, - 95, - 101, - 118, - 101, - 110, - 116, - 95, - 97, - 117, - 116, - 104, + 103, + 108, 111, - 114, - 105, - 116, - 121 + 98, + 97, + 108 ] } ] } }, { - "name": "program" - } - ], - "args": [] - }, - { - "name": "initialize", - "docs": [ - "Creates the global state." - ], - "discriminator": [ - 175, - 175, - 109, - 31, - 13, - 152, - 155, - 237 - ], - "accounts": [ + "name": "user", + "writable": true, + "signer": true + }, { - "name": "global", + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "mayhem_program_id", "writable": true, + "address": "MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e" + }, + { + "name": "global_params", "pda": { "seeds": [ { @@ -2207,88 +2131,134 @@ 111, 98, 97, - 108 + 108, + 45, + 112, + 97, + 114, + 97, + 109, + 115 ] } - ] + ], + "program": { + "kind": "const", + "value": [ + 5, + 42, + 229, + 215, + 167, + 218, + 167, + 36, + 166, + 234, + 176, + 167, + 41, + 84, + 145, + 133, + 90, + 212, + 160, + 103, + 22, + 96, + 103, + 76, + 78, + 3, + 69, + 89, + 128, + 61, + 101, + 163 + ] + } } }, { - "name": "user", + "name": "sol_vault", "writable": true, - "signer": true - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - } - ], - "args": [] - }, - { - "name": "migrate", - "docs": [ - "Migrates liquidity to pump_amm if the bonding curve is complete" - ], - "discriminator": [ - 155, - 234, - 231, - 146, - 236, - 158, - 162, - 30 - ], - "accounts": [ - { - "name": "global", "pda": { "seeds": [ { "kind": "const", "value": [ - 103, - 108, + 115, 111, - 98, + 108, + 45, + 118, 97, - 108 + 117, + 108, + 116 ] } - ] + ], + "program": { + "kind": "const", + "value": [ + 5, + 42, + 229, + 215, + 167, + 218, + 167, + 36, + 166, + 234, + 176, + 167, + 41, + 84, + 145, + 133, + 90, + 212, + 160, + 103, + 22, + 96, + 103, + 76, + 78, + 3, + 69, + 89, + 128, + 61, + 101, + 163 + ] + } } }, { - "name": "withdraw_authority", - "writable": true, - "relations": [ - "global" - ] - }, - { - "name": "mint" - }, - { - "name": "bonding_curve", + "name": "mayhem_state", "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, + 109, + 97, + 121, + 104, + 101, + 109, 45, - 99, - 117, - 114, - 118, + 115, + 116, + 97, + 116, 101 ] }, @@ -2296,54 +2266,58 @@ "kind": "account", "path": "mint" } - ] + ], + "program": { + "kind": "const", + "value": [ + 5, + 42, + 229, + 215, + 167, + 218, + 167, + 36, + 166, + 234, + 176, + 167, + 41, + 84, + 145, + 133, + 90, + 212, + 160, + 103, + 22, + 96, + 103, + 76, + 78, + 3, + 69, + 89, + 128, + 61, + 101, + 163 + ] + } } }, { - "name": "associated_bonding_curve", + "name": "mayhem_token_vault", "writable": true, "pda": { "seeds": [ { "kind": "account", - "path": "bonding_curve" + "path": "sol_vault" }, { - "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", @@ -2390,39 +2364,436 @@ } }, { - "name": "user", - "signer": true - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "name": "pump_amm", - "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" - }, - { - "name": "pool", - "writable": true, + "name": "event_authority", "pda": { "seeds": [ { "kind": "const", "value": [ - 112, - 111, + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, 111, - 108 + 114, + 105, + 116, + 121 ] - }, - { - "kind": "const", - "value": [ - 0, + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" + } + ] + }, + { + "name": "extend_account", + "docs": [ + "Extends the size of program-owned accounts" + ], + "discriminator": [ + 234, + 102, + 194, + 203, + 150, + 72, + 62, + 229 + ], + "accounts": [ + { + "name": "account", + "writable": true + }, + { + "name": "user", + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "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": "init_user_volume_accumulator", + "discriminator": [ + 94, + 6, + 202, + 115, + 255, + 96, + 232, + 183 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "user" + }, + { + "name": "user_volume_accumulator", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "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": "initialize", + "docs": [ + "Creates the global state." + ], + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ] + } + }, + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "migrate", + "docs": [ + "Migrates liquidity to pump_amm if the bonding curve is complete" + ], + "discriminator": [ + 155, + 234, + 231, + 146, + 236, + 158, + 162, + 30 + ], + "accounts": [ + { + "name": "global", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ] + } + }, + { + "name": "withdraw_authority", + "writable": true, + "relations": [ + "global" + ] + }, + { + "name": "mint" + }, + { + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 110, + 100, + 105, + 110, + 103, + 45, + 99, + 117, + 114, + 118, + 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "name": "associated_bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "bonding_curve" + }, + { + "kind": "account", + "path": "mint" + }, + { + "kind": "account", + "path": "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": "user", + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "pump_amm", + "address": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" + }, + { + "name": "pool", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "const", + "value": [ + 0, 0 ] }, @@ -2487,7 +2858,7 @@ }, { "kind": "account", - "path": "token_program" + "path": "mint" }, { "kind": "account", @@ -2517,15 +2888,245 @@ "kind": "account", "path": "wsol_mint" } - ], - "program": { - "kind": "account", - "path": "associated_token_program" - } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "amm_global_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "wsol_mint", + "address": "So11111111111111111111111111111111111111112" + }, + { + "name": "lp_mint", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 108, + 112, + 95, + 109, + 105, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "pool" + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "name": "user_pool_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool_authority" + }, + { + "kind": "account", + "path": "token_2022_program" + }, + { + "kind": "account", + "path": "lp_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_base_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "mint" + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "pool_quote_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "pool" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "wsol_mint" + } + ], + "program": { + "kind": "account", + "path": "associated_token_program" + } + } + }, + { + "name": "token_2022_program", + "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "pump_amm_event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ], + "program": { + "kind": "account", + "path": "pump_amm" + } + } + }, + { + "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": "amm_global_config", + "name": "program" + } + ], + "args": [] + }, + { + "name": "sell", + "docs": [ + "Sells tokens into a bonding curve." + ], + "discriminator": [ + 51, + 230, + 133, + 164, + 1, + 127, + 131, + 173 + ], + "accounts": [ + { + "name": "global", "pda": { "seeds": [ { @@ -2536,142 +3137,155 @@ 111, 98, 97, - 108, - 95, - 99, - 111, - 110, - 102, - 105, - 103 + 108 ] } - ], - "program": { - "kind": "account", - "path": "pump_amm" - } + ] } }, { - "name": "wsol_mint", - "address": "So11111111111111111111111111111111111111112" + "name": "fee_recipient", + "writable": true }, { - "name": "lp_mint", + "name": "mint" + }, + { + "name": "bonding_curve", "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ - 112, - 111, + 98, 111, - 108, - 95, - 108, - 112, - 95, - 109, + 110, + 100, 105, 110, - 116 + 103, + 45, + 99, + 117, + 114, + 118, + 101 ] }, { "kind": "account", - "path": "pool" + "path": "mint" } - ], - "program": { - "kind": "account", - "path": "pump_amm" - } + ] } }, { - "name": "user_pool_token_account", + "name": "associated_bonding_curve", "writable": true, "pda": { "seeds": [ { "kind": "account", - "path": "pool_authority" + "path": "bonding_curve" }, { "kind": "account", - "path": "token_2022_program" + "path": "token_program" }, { "kind": "account", - "path": "lp_mint" + "path": "mint" } ], "program": { - "kind": "account", - "path": "associated_token_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": "pool_base_token_account", + "name": "associated_user", + "writable": true + }, + { + "name": "user", "writable": true, - "pda": { - "seeds": [ - { - "kind": "account", - "path": "pool" - }, - { - "kind": "account", - "path": "token_program" - }, - { - "kind": "account", - "path": "mint" - } - ], - "program": { - "kind": "account", - "path": "associated_token_program" - } - } + "signer": true }, { - "name": "pool_quote_token_account", + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "creator_vault", "writable": true, "pda": { "seeds": [ { - "kind": "account", - "path": "pool" - }, - { - "kind": "account", - "path": "token_program" + "kind": "const", + "value": [ + 99, + 114, + 101, + 97, + 116, + 111, + 114, + 45, + 118, + 97, + 117, + 108, + 116 + ] }, { "kind": "account", - "path": "wsol_mint" + "path": "bonding_curve.creator", + "account": "BondingCurve" } - ], - "program": { - "kind": "account", - "path": "associated_token_program" - } + ] } }, { - "name": "token_2022_program", - "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" - }, - { - "name": "associated_token_program", - "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + "name": "token_program" }, { - "name": "pump_amm_event_authority", + "name": "event_authority", "pda": { "seeds": [ { @@ -2696,64 +3310,115 @@ 121 ] } + ] + } + }, + { + "name": "program", + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + }, + { + "name": "fee_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "const", + "value": [ + 1, + 86, + 224, + 246, + 147, + 102, + 90, + 207, + 68, + 219, + 21, + 104, + 191, + 23, + 91, + 170, + 81, + 137, + 203, + 151, + 245, + 210, + 255, + 59, + 101, + 93, + 43, + 182, + 253, + 109, + 24, + 176 + ] + } ], "program": { "kind": "account", - "path": "pump_amm" + "path": "fee_program" } } }, { - "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": "fee_program", + "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" }, { - "name": "program" + "name": "min_sol_output", + "type": "u64" } - ], - "args": [] + ] }, { - "name": "sell", + "name": "set_creator", "docs": [ - "Sells tokens into a bonding curve." + "Allows Global::set_creator_authority to set the bonding curve creator from Metaplex metadata or input argument" ], "discriminator": [ - 51, - 230, - 133, - 164, - 1, - 127, - 131, - 173 + 254, + 148, + 255, + 112, + 207, + 142, + 170, + 165 ], "accounts": [ + { + "name": "set_creator_authority", + "signer": true, + "relations": [ + "global" + ] + }, { "name": "global", "pda": { @@ -2772,87 +3437,61 @@ ] } }, - { - "name": "fee_recipient", - "writable": true - }, { "name": "mint" }, { - "name": "bonding_curve", - "writable": true, + "name": "metadata", "pda": { "seeds": [ { "kind": "const", "value": [ - 98, - 111, - 110, + 109, + 101, + 116, + 97, 100, - 105, - 110, - 103, - 45, - 99, - 117, - 114, - 118, - 101 + 97, + 116, + 97 ] }, - { - "kind": "account", - "path": "mint" - } - ] - } - }, - { - "name": "associated_bonding_curve", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "account", - "path": "bonding_curve" - }, { "kind": "const", "value": [ - 6, - 221, - 246, - 225, - 215, + 11, + 112, 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 + 177, + 227, + 209, + 124, + 69, + 56, + 157, + 82, + 127, + 107, + 4, + 195, + 205, + 88, + 184, + 108, + 115, + 26, + 160, + 253, + 181, + 73, + 182, + 209, + 188, + 3, + 248, + 41, + 70 ] }, { @@ -2863,90 +3502,72 @@ "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, + 112, + 101, + 177, + 227, + 209, + 124, + 69, + 56, + 157, + 82, + 127, + 107, 4, - 142, - 123, - 216, - 219, - 233, - 248, - 89 - ] - } - } - }, - { - "name": "associated_user", - "writable": true - }, - { - "name": "user", - "writable": true, - "signer": true - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" + 195, + 205, + 88, + 184, + 108, + 115, + 26, + 160, + 253, + 181, + 73, + 182, + 209, + 188, + 3, + 248, + 41, + 70 + ] + } + } }, { - "name": "creator_vault", + "name": "bonding_curve", "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ - 99, - 114, - 101, - 97, - 116, + 98, 111, - 114, + 110, + 100, + 105, + 110, + 103, 45, - 118, - 97, + 99, 117, - 108, - 116 + 114, + 118, + 101 ] }, { "kind": "account", - "path": "bonding_curve.creator", - "account": "BondingCurve" + "path": "mint" } ] } }, - { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, { "name": "event_authority", "pda": { @@ -2977,129 +3598,32 @@ } }, { - "name": "program", - "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" - }, - { - "name": "fee_config", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 102, - 101, - 101, - 95, - 99, - 111, - 110, - 102, - 105, - 103 - ] - }, - { - "kind": "const", - "value": [ - 1, - 86, - 224, - 246, - 147, - 102, - 90, - 207, - 68, - 219, - 21, - 104, - 191, - 23, - 91, - 170, - 81, - 137, - 203, - 151, - 245, - 210, - 255, - 59, - 101, - 93, - 43, - 182, - 253, - 109, - 24, - 176 - ] - } - ], - "program": { - "kind": "account", - "path": "fee_program" - } - } - }, - { - "name": "fee_program", - "address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" + "name": "program" } ], "args": [ { - "name": "amount", - "type": "u64" - }, - { - "name": "min_sol_output", - "type": "u64" + "name": "creator", + "type": "pubkey" } ] }, { - "name": "set_creator", + "name": "set_metaplex_creator", "docs": [ - "Allows Global::set_creator_authority to set the bonding curve creator from Metaplex metadata or input argument" + "Syncs the bonding curve creator with the Metaplex metadata creator if it exists" ], "discriminator": [ - 254, - 148, - 255, - 112, - 207, - 142, - 170, - 165 + 138, + 96, + 174, + 217, + 48, + 85, + 197, + 246 ], "accounts": [ - { - "name": "set_creator_authority", - "signer": true, - "relations": [ - "global" - ] - }, - { - "name": "global", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] - } - ] - } - }, { "name": "mint" }, @@ -3202,35 +3726,232 @@ } }, { - "name": "bonding_curve", + "name": "bonding_curve", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 110, + 100, + 105, + 110, + 103, + 45, + 99, + 117, + 114, + 118, + 101 + ] + }, + { + "kind": "account", + "path": "mint" + } + ] + } + }, + { + "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": "set_params", + "docs": [ + "Sets the global state parameters." + ], + "discriminator": [ + 27, + 234, + 178, + 52, + 147, + 2, + 187, + 141 + ], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ] + } + }, + { + "name": "authority", "writable": true, + "signer": true, + "relations": [ + "global" + ] + }, + { + "name": "event_authority", "pda": { "seeds": [ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, + 95, + 95, + 101, + 118, + 101, 110, - 103, - 45, - 99, + 116, + 95, + 97, 117, + 116, + 104, + 111, 114, - 118, - 101 + 105, + 116, + 121 ] - }, + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "initial_virtual_token_reserves", + "type": "u64" + }, + { + "name": "initial_virtual_sol_reserves", + "type": "u64" + }, + { + "name": "initial_real_token_reserves", + "type": "u64" + }, + { + "name": "token_total_supply", + "type": "u64" + }, + { + "name": "fee_basis_points", + "type": "u64" + }, + { + "name": "withdraw_authority", + "type": "pubkey" + }, + { + "name": "enable_migrate", + "type": "bool" + }, + { + "name": "pool_migration_fee", + "type": "u64" + }, + { + "name": "creator_fee_basis_points", + "type": "u64" + }, + { + "name": "set_creator_authority", + "type": "pubkey" + }, + { + "name": "admin_set_creator_authority", + "type": "pubkey" + } + ] + }, + { + "name": "set_reserved_fee_recipients", + "discriminator": [ + 111, + 172, + 162, + 232, + 114, + 89, + 213, + 142 + ], + "accounts": [ + { + "name": "global", + "writable": true, + "pda": { + "seeds": [ { - "kind": "account", - "path": "mint" + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } ] } }, + { + "name": "authority", + "signer": true, + "relations": [ + "global" + ] + }, { "name": "event_authority", "pda": { @@ -3266,154 +3987,100 @@ ], "args": [ { - "name": "creator", + "name": "whitelist_pda", "type": "pubkey" } ] }, { - "name": "set_metaplex_creator", - "docs": [ - "Syncs the bonding curve creator with the Metaplex metadata creator if it exists" - ], + "name": "sync_user_volume_accumulator", "discriminator": [ - 138, - 96, - 174, - 217, - 48, - 85, - 197, - 246 + 86, + 31, + 192, + 87, + 163, + 87, + 79, + 238 ], "accounts": [ { - "name": "mint" + "name": "user" }, { - "name": "metadata", + "name": "global_volume_accumulator", "pda": { "seeds": [ { "kind": "const", "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 118, + 111, + 108, + 117, 109, 101, - 116, + 95, 97, - 100, + 99, + 99, + 117, + 109, + 117, + 108, 97, 116, - 97 - ] - }, - { - "kind": "const", - "value": [ - 11, - 112, - 101, - 177, - 227, - 209, - 124, - 69, - 56, - 157, - 82, - 127, - 107, - 4, - 195, - 205, - 88, - 184, - 108, - 115, - 26, - 160, - 253, - 181, - 73, - 182, - 209, - 188, - 3, - 248, - 41, - 70 - ] - }, - { - "kind": "account", - "path": "mint" - } - ], - "program": { - "kind": "const", - "value": [ - 11, - 112, - 101, - 177, - 227, - 209, - 124, - 69, - 56, - 157, - 82, - 127, - 107, - 4, - 195, - 205, - 88, - 184, - 108, - 115, - 26, - 160, - 253, - 181, - 73, - 182, - 209, - 188, - 3, - 248, - 41, - 70 - ] - } + 111, + 114 + ] + } + ] } }, { - "name": "bonding_curve", + "name": "user_volume_accumulator", "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ - 98, - 111, - 110, - 100, - 105, - 110, - 103, - 45, - 99, 117, + 115, + 101, 114, + 95, 118, - 101 + 111, + 108, + 117, + 109, + 101, + 95, + 97, + 99, + 99, + 117, + 109, + 117, + 108, + 97, + 116, + 111, + 114 ] }, { "kind": "account", - "path": "mint" + "path": "user" } ] } @@ -3454,19 +4121,16 @@ "args": [] }, { - "name": "set_params", - "docs": [ - "Sets the global state parameters." - ], + "name": "toggle_create_v2", "discriminator": [ - 27, - 234, - 178, - 52, - 147, - 2, - 187, - 141 + 28, + 255, + 230, + 240, + 172, + 107, + 203, + 171 ], "accounts": [ { @@ -3531,69 +4195,27 @@ ], "args": [ { - "name": "initial_virtual_token_reserves", - "type": "u64" - }, - { - "name": "initial_virtual_sol_reserves", - "type": "u64" - }, - { - "name": "initial_real_token_reserves", - "type": "u64" - }, - { - "name": "token_total_supply", - "type": "u64" - }, - { - "name": "fee_basis_points", - "type": "u64" - }, - { - "name": "withdraw_authority", - "type": "pubkey" - }, - { - "name": "enable_migrate", + "name": "enabled", "type": "bool" - }, - { - "name": "pool_migration_fee", - "type": "u64" - }, - { - "name": "creator_fee_basis_points", - "type": "u64" - }, - { - "name": "set_creator_authority", - "type": "pubkey" - }, - { - "name": "admin_set_creator_authority", - "type": "pubkey" } ] }, { - "name": "sync_user_volume_accumulator", + "name": "toggle_mayhem_mode", "discriminator": [ - 86, + 1, + 9, + 111, + 208, + 100, 31, - 192, - 87, - 163, - 87, - 79, - 238 + 255, + 163 ], "accounts": [ { - "name": "user" - }, - { - "name": "global_volume_accumulator", + "name": "global", + "writable": true, "pda": { "seeds": [ { @@ -3604,70 +4226,19 @@ 111, 98, 97, - 108, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 + 108 ] } ] } }, { - "name": "user_volume_accumulator", + "name": "authority", "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 117, - 115, - 101, - 114, - 95, - 118, - 111, - 108, - 117, - 109, - 101, - 95, - 97, - 99, - 99, - 117, - 109, - 117, - 108, - 97, - 116, - 111, - 114 - ] - }, - { - "kind": "account", - "path": "user" - } - ] - } + "signer": true, + "relations": [ + "global" + ] }, { "name": "event_authority", @@ -3702,7 +4273,12 @@ "name": "program" } ], - "args": [] + "args": [ + { + "name": "enabled", + "type": "bool" + } + ] }, { "name": "update_global_authority", @@ -3993,6 +4569,19 @@ 216 ] }, + { + "name": "ReservedFeeRecipientsEvent", + "discriminator": [ + 43, + 188, + 250, + 18, + 221, + 75, + 187, + 95 + ] + }, { "name": "SetCreatorEvent", "discriminator": [ @@ -4278,6 +4867,30 @@ "code": 6042, "name": "BuySlippageBelowMinTokensOut", "msg": "Slippage: Would buy less tokens than expected min_tokens_out" + }, + { + "code": 6043, + "name": "NameTooLong" + }, + { + "code": 6044, + "name": "SymbolTooLong" + }, + { + "code": 6045, + "name": "UriTooLong" + }, + { + "code": 6046, + "name": "CreateV2Disabled" + }, + { + "code": 6047, + "name": "CpitializeMayhemFailed" + }, + { + "code": 6048, + "name": "MayhemModeDisabled" } ], "types": [ @@ -4393,6 +5006,10 @@ { "name": "creator", "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" } ] } @@ -4597,6 +5214,14 @@ { "name": "token_total_supply", "type": "u64" + }, + { + "name": "token_program", + "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" } ] } @@ -4778,6 +5403,31 @@ { "name": "admin_set_creator_authority", "type": "pubkey" + }, + { + "name": "create_v2_enabled", + "type": "bool" + }, + { + "name": "whitelist_pda", + "type": "pubkey" + }, + { + "name": "reserved_fee_recipient", + "type": "pubkey" + }, + { + "name": "mayhem_mode_enabled", + "type": "bool" + }, + { + "name": "reserved_fee_recipients", + "type": { + "array": [ + "pubkey", + 7 + ] + } } ] } @@ -4853,6 +5503,31 @@ ] } }, + { + "name": "ReservedFeeRecipientsEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "reserved_fee_recipient", + "type": "pubkey" + }, + { + "name": "reserved_fee_recipients", + "type": { + "array": [ + "pubkey", + 7 + ] + } + } + ] + } + }, { "name": "SetCreatorEvent", "type": { diff --git a/idl/pump_swap_idl.json b/idl/pump_swap_idl.json index 16e82eea..b10031f1 100644 --- a/idl/pump_swap_idl.json +++ b/idl/pump_swap_idl.json @@ -543,7 +543,6 @@ }, { "name": "global_volume_accumulator", - "writable": true, "pda": { "seeds": [ { @@ -972,7 +971,6 @@ }, { "name": "global_volume_accumulator", - "writable": true, "pda": { "seeds": [ { @@ -2085,6 +2083,10 @@ { "name": "coin_creator", "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" } ] }, @@ -3006,6 +3008,92 @@ ], "args": [] }, + { + "name": "set_reserved_fee_recipients", + "discriminator": [ + 111, + 172, + 162, + 232, + 114, + 89, + 213, + 142 + ], + "accounts": [ + { + "name": "global_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "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": "whitelist_pda", + "type": "pubkey" + } + ] + }, { "name": "sync_user_volume_accumulator", "discriminator": [ @@ -3134,6 +3222,70 @@ ], "args": [] }, + { + "name": "toggle_mayhem_mode", + "discriminator": [ + 1, + 9, + 111, + 208, + 100, + 31, + 255, + 163 + ], + "accounts": [ + { + "name": "admin", + "signer": true, + "relations": [ + "global_config" + ] + }, + { + "name": "global_config", + "writable": true + }, + { + "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": "enabled", + "type": "bool" + } + ] + }, { "name": "update_admin", "discriminator": [ @@ -3644,6 +3796,19 @@ 216 ] }, + { + "name": "ReservedFeeRecipientsEvent", + "discriminator": [ + 43, + 188, + 250, + 18, + 221, + 75, + 187, + 95 + ] + }, { "name": "SellEvent", "discriminator": [ @@ -3902,6 +4067,22 @@ "code": 6040, "name": "BuySlippageBelowMinBaseAmountOut", "msg": "buy: slippage - would buy less tokens than expected min_base_amount_out" + }, + { + "code": 6041, + "name": "MayhemModeDisabled" + }, + { + "code": 6042, + "name": "OnlyPumpPoolsMayhemMode" + }, + { + "code": 6043, + "name": "MayhemModeInDesiredState" + }, + { + "code": 6044, + "name": "NotEnoughRemainingAccounts" } ], "types": [ @@ -4005,6 +4186,10 @@ { "name": "creator", "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" } ] } @@ -4357,6 +4542,10 @@ { "name": "coin_creator", "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" } ] } @@ -4625,6 +4814,27 @@ "The admin authority for setting coin creators" ], "type": "pubkey" + }, + { + "name": "whitelist_pda", + "type": "pubkey" + }, + { + "name": "reserved_fee_recipient", + "type": "pubkey" + }, + { + "name": "mayhem_mode_enabled", + "type": "bool" + }, + { + "name": "reserved_fee_recipients", + "type": { + "array": [ + "pubkey", + 7 + ] + } } ] } @@ -4747,6 +4957,35 @@ { "name": "coin_creator", "type": "pubkey" + }, + { + "name": "is_mayhem_mode", + "type": "bool" + } + ] + } + }, + { + "name": "ReservedFeeRecipientsEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "reserved_fee_recipient", + "type": "pubkey" + }, + { + "name": "reserved_fee_recipients", + "type": { + "array": [ + "pubkey", + 7 + ] + } } ] } diff --git a/learning-examples/bonding-curve-progress/get_bonding_curve_status.py b/learning-examples/bonding-curve-progress/get_bonding_curve_status.py index e00a5c5a..18f829d3 100644 --- a/learning-examples/bonding-curve-progress/get_bonding_curve_status.py +++ b/learning-examples/bonding-curve-progress/get_bonding_curve_status.py @@ -1,9 +1,6 @@ """ Module for checking the status of a token's bonding curve on the Solana network using the Pump.fun program. It allows querying the bonding curve state and completion status. - -Note: creator fee upgrade introduced updates in bonding curve structure. -https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md """ import argparse @@ -42,27 +39,30 @@ class BondingCurveState: real_sol_reserves: Real SOL reserves in the curve token_total_supply: Total token supply in the curve complete: Whether the curve has completed and liquidity migrated + is_mayhem_mode: Whether the curve is in mayhem mode """ - _STRUCT_1 = Struct( + # V2: Struct with creator field (81 bytes total: 8 discriminator + 73 data) + _STRUCT_V2 = Struct( "virtual_token_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul, "real_token_reserves" / Int64ul, "real_sol_reserves" / Int64ul, "token_total_supply" / Int64ul, "complete" / Flag, + "creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey ) - # Struct after creator fee update has been introduced - # https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md - _STRUCT_2 = Struct( + # V3: Struct with creator + mayhem mode (82 bytes total: 8 discriminator + 74 data) + _STRUCT_V3 = Struct( "virtual_token_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul, "real_token_reserves" / Int64ul, "real_sol_reserves" / Int64ul, "token_total_supply" / Int64ul, "complete" / Flag, - "creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey + "creator" / Bytes(32), + "is_mayhem_mode" / Flag, # Added mayhem mode flag - 1 byte ) def __init__(self, data: bytes) -> None: @@ -70,21 +70,26 @@ def __init__(self, data: bytes) -> None: if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid curve state discriminator") - if len(data) < 150: - parsed = self._STRUCT_1.parse(data[8:]) + total_length = len(data) + + if total_length == 81: # V2: Creator only + parsed = self._STRUCT_V2.parse(data[8:]) self.__dict__.update(parsed) + # Convert raw bytes to Pubkey for creator field + self.creator = Pubkey.from_bytes(self.creator) + self.is_mayhem_mode = False - else: - parsed = self._STRUCT_2.parse(data[8:]) + elif total_length >= 82: # V3: Creator + mayhem mode + parsed = self._STRUCT_V3.parse(data[8:]) self.__dict__.update(parsed) # Convert raw bytes to Pubkey for creator field - if hasattr(self, "creator") and isinstance(self.creator, bytes): - self.creator = Pubkey.from_bytes(self.creator) + self.creator = Pubkey.from_bytes(self.creator) + + else: + raise ValueError(f"Unexpected bonding curve size: {total_length} bytes") -def get_associated_bonding_curve_address( - mint: Pubkey, program_id: Pubkey -) -> tuple[Pubkey, int]: +def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]: """ Derives the associated bonding curve address for a given mint. @@ -134,17 +139,14 @@ async def check_token_status(mint_address: str) -> None: """ try: mint = Pubkey.from_string(mint_address) - - # Get the associated bonding curve address - bonding_curve_address, bump = get_associated_bonding_curve_address( - mint, PUMP_PROGRAM_ID - ) + bonding_curve_address, bump = get_bonding_curve_address(mint, PUMP_PROGRAM_ID) print("\nToken status:") print("-" * 50) print(f"Token mint: {mint}") - print(f"Associated bonding curve: {bonding_curve_address}") - print(f"Bump seed: {bump}") + print(f"Bonding curve: {bonding_curve_address}") + if bump is not None: + print(f"Bump seed: {bump}") print("-" * 50) # Check completion status @@ -156,9 +158,25 @@ async def check_token_status(mint_address: str) -> None: print("\nBonding curve status:") print("-" * 50) + print(f"Creator: {curve_state.creator}") print( - f"Completion status: {'Completed' if curve_state.complete else 'Not completed'}" + f"Mayhem Mode: {'✅ Enabled' if curve_state.is_mayhem_mode else '❌ Disabled'}" ) + print( + f"Completed: {'✅ Migrated' if curve_state.complete else '❌ Bonding curve'}" + ) + + print("\nBonding curve reserves:") + print(f"Virtual Token: {curve_state.virtual_token_reserves:,}") + print( + f"Virtual SOL: {curve_state.virtual_sol_reserves:,} lamports" + ) + print(f"Real Token: {curve_state.real_token_reserves:,}") + print( + f"Real SOL: {curve_state.real_sol_reserves:,} lamports" + ) + print(f"Total Supply: {curve_state.token_total_supply:,}") + if curve_state.complete: print( "\nNote: This bonding curve has completed and liquidity has been migrated to PumpSwap." diff --git a/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py b/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py index b7327a4f..e95ef8c6 100644 --- a/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py +++ b/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py @@ -17,7 +17,7 @@ # Constants RPC_URL: Final[str] = os.getenv("SOLANA_NODE_RPC_ENDPOINT") TOKEN_MINT: Final[str] = ( - "YOUR_TOKEN_MINT_ADDRESS_HERE" # Replace with actual token mint address + "5ZHx2GGGj87xpidVJpBqadMUutqBirhL2TqUR9T9taKc" # Replace with actual token mint address ) PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string( "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" @@ -30,7 +30,7 @@ POLL_INTERVAL: Final[int] = 10 # Seconds between each status check -def get_associated_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pubkey: +def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pubkey: """ Derive the bonding curve PDA address from a mint address. @@ -81,8 +81,9 @@ def parse_curve_state(data: bytes) -> dict: if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid discriminator for bonding curve") + # Parse common fields (present in all versions) fields = struct.unpack_from(" dict: "complete": fields[5], } + # Parse creator field if present + data_length = len(data) - 8 + if data_length >= 73: # Has creator field + creator_bytes = data[49:81] # 8 (discriminator) + 41 (base fields) = 49 + result["creator"] = Pubkey.from_bytes(creator_bytes) + + # Parse is_mayhem_mode if present + if data_length >= 74: # Has mayhem mode field + result["is_mayhem_mode"] = bool(data[81]) + else: + result["is_mayhem_mode"] = False + + return result + def print_curve_status(state: dict) -> None: """ @@ -130,9 +145,7 @@ async def track_curve() -> None: return mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT) - curve_pubkey: Pubkey = get_associated_bonding_curve_address( - mint_pubkey, PUMP_PROGRAM_ID - ) + curve_pubkey: Pubkey = get_bonding_curve_address(mint_pubkey, PUMP_PROGRAM_ID) print("Tracking bonding curve for:", mint_pubkey) print("Curve address:", curve_pubkey, "\n") diff --git a/learning-examples/cleanup_accounts.py b/learning-examples/cleanup_accounts.py index 7e6d383d..83bb8c39 100644 --- a/learning-examples/cleanup_accounts.py +++ b/learning-examples/cleanup_accounts.py @@ -19,6 +19,10 @@ # Update this address to MINT address of a token you want to close MINT_ADDRESS = Pubkey.from_string("9WHpYbqG6LJvfCYfMjvGbyo1wHXgroCrixPb33s2pump") +# Token program for the mint - use TOKEN_PROGRAM for legacy SPL tokens, TOKEN_2022_PROGRAM for Token-2022 +# This must match the actual token's program to derive the correct ATA address +TOKEN_PROGRAM = SystemAddresses.TOKEN_PROGRAM + async def close_account_if_exists( client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey @@ -41,7 +45,7 @@ async def close_account_if_exists( mint=mint, owner=wallet.pubkey, amount=balance, - program_id=SystemAddresses.TOKEN_PROGRAM, + program_id=TOKEN_PROGRAM, ) ) await client.build_and_send_transaction([burn_ix], wallet.keypair) @@ -54,7 +58,7 @@ async def close_account_if_exists( account=account, dest=wallet.pubkey, owner=wallet.pubkey, - program_id=SystemAddresses.TOKEN_PROGRAM, + program_id=TOKEN_PROGRAM, ) ix = close_account(close_params) @@ -78,7 +82,7 @@ async def main(): wallet = Wallet(PRIVATE_KEY) # Get user's ATA for the token - ata = wallet.get_associated_token_address(MINT_ADDRESS) + ata = wallet.get_associated_token_address(MINT_ADDRESS, TOKEN_PROGRAM) await close_account_if_exists(client, wallet, ata, MINT_ADDRESS) except Exception as e: diff --git a/learning-examples/copytrading/listen_wallet_transactions.py b/learning-examples/copytrading/listen_wallet_transactions.py index 6a110966..777e966c 100644 --- a/learning-examples/copytrading/listen_wallet_transactions.py +++ b/learning-examples/copytrading/listen_wallet_transactions.py @@ -121,13 +121,27 @@ def parse_trade_event(logs): def decode_trade_event(data): - """Decode TradeEvent structure from raw bytes.""" - if len(data) < 32 + 8 + 8 + 1 + 32: # minimum size check + """Decode TradeEvent structure from raw bytes with progressive parsing. + + Supports both pre-mayhem and post-mayhem IDL versions by parsing fields + progressively based on available bytes. This ensures backward compatibility + with older transaction logs. + + Core fields (always present): mint, sol_amount, token_amount, is_buy, user, + timestamp, virtual_sol_reserves, virtual_token_reserves + + Extended fields (added later): real_sol_reserves, real_token_reserves, + fee_recipient, fee_basis_points, fee, creator, creator_fee_basis_points, + creator_fee, track_volume, total_unclaimed_tokens, total_claimed_tokens, + current_sol_volume, last_update_timestamp, ix_name + """ + # Minimum size for core fields: 32+8+8+1+32+8+8+8 = 105 bytes + if len(data) < 105: return None offset = 0 - # Parse fields according to TradeEvent structure + # Parse core fields (always present in all versions) mint = data[offset : offset + 32] offset += 32 @@ -152,6 +166,73 @@ def decode_trade_event(data): virtual_token_reserves = struct.unpack("= offset + 16: + real_sol_reserves = struct.unpack("= offset + 48: + fee_recipient = data[offset : offset + 32] + offset += 32 + fee_basis_points = struct.unpack("= offset + 48: + creator = data[offset : offset + 32] + offset += 32 + creator_fee_basis_points = struct.unpack("= offset + 33: + track_volume = bool(data[offset]) + offset += 1 + total_unclaimed_tokens = struct.unpack("= offset + 4: + string_length = struct.unpack("= offset + string_length: + ix_name = data[offset : offset + string_length].decode("utf-8") + else: + ix_name = "" + else: + ix_name = "" + return { "mint": base58.b58encode(mint).decode(), "sol_amount": sol_amount, @@ -161,6 +242,20 @@ def decode_trade_event(data): "timestamp": timestamp, "virtual_sol_reserves": virtual_sol_reserves, "virtual_token_reserves": virtual_token_reserves, + "real_sol_reserves": real_sol_reserves, + "real_token_reserves": real_token_reserves, + "fee_recipient": base58.b58encode(fee_recipient).decode() if fee_recipient != b'\x00' * 32 else None, + "fee_basis_points": fee_basis_points, + "fee": fee, + "creator": base58.b58encode(creator).decode() if creator != b'\x00' * 32 else None, + "creator_fee_basis_points": creator_fee_basis_points, + "creator_fee": creator_fee, + "track_volume": track_volume, + "total_unclaimed_tokens": total_unclaimed_tokens, + "total_claimed_tokens": total_claimed_tokens, + "current_sol_volume": current_sol_volume, + "last_update_timestamp": last_update_timestamp, + "ix_name": ix_name, "price_per_token": (sol_amount * 1_000_000) / token_amount if token_amount > 0 else 0, @@ -178,7 +273,10 @@ def display_transaction_info(signature, logs): # Parse trade event data trade_data = parse_trade_event(logs) if trade_data: - print(f" Type: {'BUY' if trade_data['is_buy'] else 'SELL'}") + # Core transaction info (always present) + ix_name = trade_data.get('ix_name', '') + trade_type = 'BUY' if trade_data['is_buy'] else 'SELL' + print(f" Type: {trade_type}{f' ({ix_name})' if ix_name else ''}") print(f" Token: {trade_data['mint']}") print(f" SOL Amount: {trade_data['sol_amount'] / 1_000_000_000:.6f} SOL") print(f" Token Amount: {trade_data['token_amount']:,}") @@ -187,6 +285,25 @@ def display_transaction_info(signature, logs): ) print(f" Trader: {trade_data['user']}") + # Fee info (may not be present in older transactions) + if trade_data['fee'] > 0 or trade_data['fee_basis_points'] > 0: + print(f" Fee: {trade_data['fee'] / 1_000_000_000:.6f} SOL ({trade_data['fee_basis_points']} bps)") + + if trade_data['creator_fee'] > 0 or trade_data['creator_fee_basis_points'] > 0: + print(f" Creator Fee: {trade_data['creator_fee'] / 1_000_000_000:.6f} SOL ({trade_data['creator_fee_basis_points']} bps)") + + if trade_data['creator']: + print(f" Creator: {trade_data['creator']}") + + if trade_data['fee_recipient']: + print(f" Fee Recipient: {trade_data['fee_recipient']}") + + # Reserve info + print(f" Virtual Reserves: {trade_data['virtual_sol_reserves'] / 1_000_000_000:.6f} SOL / {trade_data['virtual_token_reserves']:,} tokens") + + if trade_data['real_sol_reserves'] > 0 or trade_data['real_token_reserves'] > 0: + print(f" Real Reserves: {trade_data['real_sol_reserves'] / 1_000_000_000:.6f} SOL / {trade_data['real_token_reserves']:,} tokens") + # Extract and display program info display_program_info(logs) diff --git a/learning-examples/decode_from_getAccountInfo.py b/learning-examples/decode_from_getAccountInfo.py index 6284a715..7254b8a9 100644 --- a/learning-examples/decode_from_getAccountInfo.py +++ b/learning-examples/decode_from_getAccountInfo.py @@ -11,18 +11,7 @@ class BondingCurveState: - _STRUCT_1 = Struct( - "virtual_token_reserves" / Int64ul, - "virtual_sol_reserves" / Int64ul, - "real_token_reserves" / Int64ul, - "real_sol_reserves" / Int64ul, - "token_total_supply" / Int64ul, - "complete" / Flag, - ) - - # Struct after creator fee update has been introduced - # https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md - _STRUCT_2 = Struct( + _STRUCT = Struct( "virtual_token_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul, "real_token_reserves" / Int64ul, @@ -30,23 +19,43 @@ class BondingCurveState: "token_total_supply" / Int64ul, "complete" / Flag, "creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey + "is_mayhem_mode" / Flag, # Added mayhem mode flag - 1 byte ) def __init__(self, data: bytes) -> None: - """Parse bonding curve data.""" + """Parse bonding curve data - supports all versions.""" if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid curve state discriminator") - if len(data) < 150: - parsed = self._STRUCT_1.parse(data[8:]) - self.__dict__.update(parsed) + # Required fields (always present) + offset = 8 + self.virtual_token_reserves = int.from_bytes( + data[offset : offset + 8], "little" + ) + offset += 8 + self.virtual_sol_reserves = int.from_bytes(data[offset : offset + 8], "little") + offset += 8 + self.real_token_reserves = int.from_bytes(data[offset : offset + 8], "little") + offset += 8 + self.real_sol_reserves = int.from_bytes(data[offset : offset + 8], "little") + offset += 8 + self.token_total_supply = int.from_bytes(data[offset : offset + 8], "little") + offset += 8 + self.complete = bool(data[offset]) + offset += 1 + + # Optional fields (may not be present in older versions) + if len(data) >= offset + 32: + self.creator = Pubkey.from_bytes(data[offset : offset + 32]) + offset += 32 + + if len(data) > offset: + self.is_mayhem_mode = bool(data[offset]) + else: + self.is_mayhem_mode = None else: - parsed = self._STRUCT_2.parse(data[8:]) - self.__dict__.update(parsed) - # Convert raw bytes to Pubkey for creator field - if hasattr(self, "creator") and isinstance(self.creator, bytes): - self.creator = Pubkey.from_bytes(self.creator) + self.creator = None def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float: diff --git a/learning-examples/decode_from_getTransaction.py b/learning-examples/decode_from_getTransaction.py index f071ccf7..db220c8a 100644 --- a/learning-examples/decode_from_getTransaction.py +++ b/learning-examples/decode_from_getTransaction.py @@ -27,6 +27,7 @@ def decode_create_instruction(data): + """Decode legacy Create instruction (Metaplex tokens).""" # The Create instruction has 3 string arguments: name, symbol, uri offset = 8 # Skip the 8-byte discriminator results = [] @@ -36,7 +37,42 @@ def decode_create_instruction(data): string_data = data[offset : offset + length].decode("utf-8") results.append(string_data) offset += length - return {"name": results[0], "symbol": results[1], "uri": results[2]} + return { + "name": results[0], + "symbol": results[1], + "uri": results[2], + "token_standard": "legacy", + "is_mayhem_mode": False, + } + + +def decode_create_v2_instruction(data): + """Decode CreateV2 instruction (Token2022 tokens).""" + # The CreateV2 instruction has 3 string arguments: name, symbol, uri + is_mayhem_mode + offset = 8 # Skip the 8-byte discriminator + results = [] + for _ in range(3): + length = struct.unpack_from(" None: - parsed = self._STRUCT.parse(data[8:]) - self.__dict__.update(parsed) + """Parse bonding curve data - auto-detects version.""" + data_length = len(data) - 8 + + if data_length < 73: # V1: without creator and mayhem mode + parsed = self._STRUCT_V1.parse(data[8:]) + self.__dict__.update(parsed) + self.creator = None + self.is_mayhem_mode = False + elif data_length == 73: # V2: with creator, without mayhem mode + parsed = self._STRUCT_V2.parse(data[8:]) + self.__dict__.update(parsed) + if isinstance(self.creator, bytes): + self.creator = Pubkey.from_bytes(self.creator) + self.is_mayhem_mode = False + else: # V3: with creator and mayhem mode + parsed = self._STRUCT_V3.parse(data[8:]) + self.__dict__.update(parsed) + if isinstance(self.creator, bytes): + self.creator = Pubkey.from_bytes(self.creator) async def get_bonding_curve_state( diff --git a/learning-examples/listen-migrations/compare_migration_listeners.py b/learning-examples/listen-migrations/compare_migration_listeners.py index cbc3ee3f..aaa33ac4 100644 --- a/learning-examples/listen-migrations/compare_migration_listeners.py +++ b/learning-examples/listen-migrations/compare_migration_listeners.py @@ -1,11 +1,19 @@ """ This script compares two methods of detecting migrations: -1. Migration program listener (listens Migration program) - detects markets via successful migration transactions -2. Direct market account listener (listens Pump Fun AMM program aka PumpSwap) - detects markets via program account subscription -The script tracks which method detects new markets first and provides detailed performance statistics. +1. Migration Program Listener - Listens to migration wrapper program (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg) + which emits detailed migration events via logsSubscribe -Note: multiple endpoints available. Scroll down to change providers which you want to test. +2. Direct Pool Account Listener - Listens to pump_amm program (pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA) + for new Pool account creations via programSubscribe + +Note: The migration wrapper program emits a different event structure than +CompletePumpAmmMigrationEvent in pump_fun_idl.json. + +The script tracks which method detects new migrations first and provides detailed performance +statistics including message counts, detection timing, provider latency comparison. + +Configure multiple RPC endpoints in .env file to test provider performance. """ import asyncio @@ -33,7 +41,7 @@ ).decode() MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode() -MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure +MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # Pool account with is_mayhem_mode = 244 bytes class DetectionTracker: @@ -306,9 +314,9 @@ async def fetch_existing_market_pubkeys(): def parse_market_account_data(data): """ - Parse binary market account data into a structured format + Parse binary Pool account data according to pump_swap_idl.json structure - This function matches the parser from the market listener script + Total 11 fields including is_mayhem_mode field added with mayhem update """ parsed_data = {} offset = 8 # Skip discriminator @@ -323,6 +331,8 @@ def parse_market_account_data(data): ("pool_base_token_account", "pubkey"), ("pool_quote_token_account", "pubkey"), ("lp_supply", "u64"), + ("coin_creator", "pubkey"), + ("is_mayhem_mode", "bool"), ] try: @@ -347,6 +357,10 @@ def parse_market_account_data(data): value = data[offset] parsed_data[field_name] = value offset += 1 + elif field_type == "bool": + value = bool(data[offset]) + parsed_data[field_name] = value + offset += 1 except Exception as e: print(f"[ERROR] Failed to parse market data: {e}") @@ -355,9 +369,11 @@ def parse_market_account_data(data): def parse_migrate_instruction(data): """ - Parse binary migration instruction data into a structured format + Parse migration event from the migration wrapper program - This function matches the parser from the migration listener script + Note: This parses the event emitted by the migration wrapper program + (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg), which has a different + structure than CompletePumpAmmMigrationEvent in pump_fun_idl.json. """ if len(data) < 8: print(f"[ERROR] Data length too short: {len(data)} bytes") diff --git a/learning-examples/listen-migrations/listen_logsubscribe.py b/learning-examples/listen-migrations/listen_logsubscribe.py index d968b3cc..b35a57c2 100644 --- a/learning-examples/listen-migrations/listen_logsubscribe.py +++ b/learning-examples/listen-migrations/listen_logsubscribe.py @@ -1,8 +1,11 @@ """ -Listens for 'Migrate' instructions from a Solana migration program via WebSocket. +Listens for 'Migrate' instructions from Solana migration program via WebSocket. Parses and logs transaction details (e.g., mint, liquidity, token accounts) for successful migrations. -Note: skips transactions with truncated logs (no Program data in the logs -> no parsed data). +Note: This uses a migration wrapper program (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg) +that emits a different event structure than the CompletePumpAmmMigrationEvent in pump_fun_idl.json. + +Skips transactions with truncated logs (no Program data in the logs -> no parsed data). To cover those cases, please use an additional RPC call (get transaction data) or additional listener not based on logs. """ @@ -26,6 +29,12 @@ def parse_migrate_instruction(data): + """Parse migration event from the migration wrapper program. + + Note: This parses the event emitted by the migration wrapper program + (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg), which has a different + structure than CompletePumpAmmMigrationEvent in pump_fun_idl.json. + """ if len(data) < 8: print(f"[ERROR] Data length too short: {len(data)} bytes") return None diff --git a/learning-examples/listen-migrations/listen_programsubscribe.py b/learning-examples/listen-migrations/listen_programsubscribe.py index 54a398f2..ebe6f148 100644 --- a/learning-examples/listen-migrations/listen_programsubscribe.py +++ b/learning-examples/listen-migrations/listen_programsubscribe.py @@ -24,7 +24,7 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") -MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure +MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # discriminator + pool_bump + index + 6 pubkeys + lp_supply + coin_creator + is_mayhem_mode = 244 bytes MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode() QUOTE_MINT_SOL = base58.b58encode( bytes(Pubkey.from_string("So11111111111111111111111111111111111111112")) @@ -58,6 +58,10 @@ async def fetch_existing_market_pubkeys(): def parse_market_account_data(data): + """Parse Pool account data according to pump_swap_idl.json structure. + + Total 11 fields including the new is_mayhem_mode field added with mayhem update. + """ parsed_data = {} offset = 8 # Discriminator @@ -72,6 +76,7 @@ def parse_market_account_data(data): ("pool_quote_token_account", "pubkey"), ("lp_supply", "u64"), ("coin_creator", "pubkey"), + ("is_mayhem_mode", "bool"), ] try: @@ -96,6 +101,10 @@ def parse_market_account_data(data): value = data[offset] parsed_data[field_name] = value offset += 1 + elif field_type == "bool": + value = bool(data[offset]) + parsed_data[field_name] = value + offset += 1 except Exception as e: print(f"[ERROR] Failed to parse market data: {e}") diff --git a/learning-examples/listen-new-tokens/compare_listeners.py b/learning-examples/listen-new-tokens/compare_listeners.py index 5f2ace00..4c62df71 100644 --- a/learning-examples/listen-new-tokens/compare_listeners.py +++ b/learning-examples/listen-new-tokens/compare_listeners.py @@ -1,13 +1,36 @@ """ -This script compares four methods of detecting new Pump.fun tokens: -1. Block subscription listener - listens for blocks containing Pump.fun program -2. Geyser gRPC listener - uses Geyser gRPC API to get transactions containing Pump.fun program -3. Logs subscription listener - listens for logs containing Pump.fun program -4. PumpPortal WebSocket listener - connects to PumpPortal WebSocket and listens for token events - -The script tracks which method detects new tokens first and provides detailed performance statistics. - -Note: multiple endpoints available. Scroll down to change providers which you want to test. +Performance Comparison Tool for Pump.fun Token Detection Methods + +This script compares four methods of detecting new Pump.fun tokens in real-time: + +1. Block Subscription (blockSubscribe) + - Method: WebSocket subscription to blocks mentioning Pump.fun program + - Speed: Slowest (processes entire blocks) + - Reference: https://solana.com/docs/rpc/websocket/blocksubscribe + +2. Logs Subscription (logsSubscribe) + - Method: WebSocket subscription to program logs + - Speed: Fast (event data includes all fields) + - Reference: https://solana.com/docs/rpc/websocket/logssubscribe + +3. Geyser gRPC + - Method: Yellowstone Dragon's Mouth gRPC streaming + - Speed: Fastest (optimized streaming protocol) + - Reference: https://docs.triton.one/rpc-pool/grpc-subscriptions + +4. PumpPortal WebSocket + - Method: Third-party aggregated WebSocket feed + - Speed: Fast (pre-processed data) + - Note: Requires trust in third-party provider + +The script tracks which method detects each token first and provides detailed +performance statistics including: +- First detection counts per method +- Average latency between methods +- Message counts per provider +- Token detection coverage + +Configuration: Set provider endpoints in .env or modify the providers dict at the bottom. """ import asyncio @@ -26,13 +49,31 @@ load_dotenv(override=True) -# Constants +# ============ CONSTANTS ============ + +# Pump.fun program ID PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") + +# Instruction discriminators (8-byte identifiers for instruction types) +# Calculated using the first 8 bytes of sha256("global:create") for legacy Create +# and sha256("global:createV2") for Token2022 CreateV2 +# See: learning-examples/calculate_discriminator.py PUMP_CREATE_PREFIX = struct.pack("= 8: + parsed_data["mint"] = account_keys[0] + parsed_data["bondingCurve"] = account_keys[2] + parsed_data["user"] = account_keys[7] + elif len(account_keys) > 0: + parsed_data["mint"] = account_keys[0] + + parsed_data["token_standard"] = "legacy" + parsed_data["is_mayhem_mode"] = False + return parsed_data + except Exception as e: + print(f"[ERROR] Failed to decode create instruction: {e}") + return None + + +def decode_create_v2_instruction(ix_data, account_keys): + """Decode CreateV2 instruction (Token2022 tokens) from instruction data.""" + if len(ix_data) < 8: + return None + + offset = 8 # Skip discriminator + parsed_data = {} + + try: + # Read string fields from instruction data + def read_string(): + nonlocal offset + length = struct.unpack("= 6: + parsed_data["mint"] = account_keys[0] + parsed_data["bondingCurve"] = account_keys[2] + parsed_data["user"] = account_keys[5] + elif len(account_keys) > 0: + parsed_data["mint"] = account_keys[0] + + parsed_data["token_standard"] = "token2022" + return parsed_data + except Exception as e: + print(f"[ERROR] Failed to decode create v2 instruction: {e}") + return None + + +def parse_create_event(data): + """Parse legacy Create event from logs (event data includes all fields).""" if len(data) < 8: return None offset = 8 # Skip discriminator parsed_data = {} + # Parse fields based on CreateEvent structure + fields = [ + ("name", "string"), + ("symbol", "string"), + ("uri", "string"), + ("mint", "publicKey"), + ("bondingCurve", "publicKey"), + ("user", "publicKey"), + ("creator", "publicKey"), + ] + try: - # Parse name (string) - length = struct.unpack(" len(data): + raise ValueError(f"Not enough data for {field_name} length at offset {offset}") + length = struct.unpack(" len(data): + raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}") + value = data[offset : offset + length].decode("utf-8") + offset += length + elif field_type == "publicKey": + if offset + 32 > len(data): + raise ValueError(f"Not enough data for {field_name} at offset {offset}") + value = base58.b58encode(data[offset : offset + 32]).decode("utf-8") + offset += 32 + + parsed_data[field_name] = value + + parsed_data["token_standard"] = "legacy" + parsed_data["is_mayhem_mode"] = False + return parsed_data + except Exception as e: + print(f"[ERROR] Failed to parse create event: {e}") + return None + + +def parse_create_v2_event(data): + """Parse CreateV2 event from logs (event data includes all fields).""" + if len(data) < 8: + return None + + offset = 8 # Skip discriminator + parsed_data = {} + + # Parse fields based on CreateV2Event structure + fields = [ + ("name", "string"), + ("symbol", "string"), + ("uri", "string"), + ("mint", "publicKey"), + ("bondingCurve", "publicKey"), + ("user", "publicKey"), + ("creator", "publicKey"), + ] + try: + for field_name, field_type in fields: + if field_type == "string": + if offset + 4 > len(data): + raise ValueError(f"Not enough data for {field_name} length at offset {offset}") + length = struct.unpack(" len(data): + raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}") + value = data[offset : offset + length].decode("utf-8") + offset += length + elif field_type == "publicKey": + if offset + 32 > len(data): + raise ValueError(f"Not enough data for {field_name} at offset {offset}") + value = base58.b58encode(data[offset : offset + 32]).decode("utf-8") + offset += 32 + + parsed_data[field_name] = value + + # Parse is_mayhem_mode (OptionBool at the end) + if offset < len(data): + is_mayhem_mode = bool(data[offset]) + parsed_data["is_mayhem_mode"] = is_mayhem_mode + else: + parsed_data["is_mayhem_mode"] = False + + parsed_data["token_standard"] = "token2022" return parsed_data except Exception as e: - print(f"[ERROR] Failed to parse create instruction: {e}") + print(f"[ERROR] Failed to parse create v2 event: {e}") return None @@ -269,6 +464,51 @@ def is_transaction_successful(logs): # ============ WEBSOCKET LISTENERS ============ +def get_account_keys(transaction, instruction, loaded_addresses=None): + """ + Safely extract account keys for an instruction from a versioned transaction. + Handles both static account keys and loaded addresses from lookup tables. + + Args: + transaction: VersionedTransaction object + instruction: Instruction object + loaded_addresses: Dict with 'writable' and 'readonly' loaded addresses from tx meta + + Returns: + List of account keys as strings, or None if unable to resolve + """ + account_keys = [] + static_keys = transaction.message.account_keys + + # Combine all available account keys: static + loaded + all_keys = list(static_keys) + + if loaded_addresses: + # Add loaded writable addresses + if "writable" in loaded_addresses: + for addr in loaded_addresses["writable"]: + all_keys.append(Pubkey.from_string(addr)) + + # Add loaded readonly addresses + if "readonly" in loaded_addresses: + for addr in loaded_addresses["readonly"]: + all_keys.append(Pubkey.from_string(addr)) + + # Now resolve account indices + for index in instruction.accounts: + try: + if index < len(all_keys): + account_keys.append(str(all_keys[index])) + else: + print(f"Warning: Account index {index} out of range (max: {len(all_keys)-1})") + return None + except (IndexError, Exception) as e: + print(f"Error resolving account at index {index}: {e}") + return None + + return account_keys + + async def listen_block_subscription(wss_url, provider_name, tracker, known_tokens=None): """ Listen for new tokens via block subscription @@ -330,6 +570,12 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token try: transaction = VersionedTransaction.from_bytes(tx_data) + + # Extract loaded addresses from transaction metadata + loaded_addresses = None + if "meta" in tx and tx["meta"] and "loadedAddresses" in tx["meta"]: + loaded_addresses = tx["meta"]["loadedAddresses"] + for ix in transaction.message.instructions: if ( transaction.message.account_keys[ @@ -339,39 +585,53 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token ): data_bytes = bytes(ix.data) - if not data_bytes.startswith( - PUMP_CREATE_PREFIX - ): + # Check for both Create and CreateV2 instructions + is_create = data_bytes.startswith(PUMP_CREATE_PREFIX) + is_create_v2 = data_bytes.startswith(PUMP_CREATE_V2_PREFIX) + + if not (is_create or is_create_v2): continue - parsed = parse_create_instruction(data_bytes) - if not parsed: + # Get account keys with ALT support + account_keys = get_account_keys( + transaction, ix, loaded_addresses + ) + if account_keys is None: + print("Skipping transaction due to unresolved accounts") continue - if len(ix.accounts) > 0: - try: - mint = str( - transaction.message.account_keys[ - ix.accounts[0] - ] - ) # First account is usually the mint - - if mint in known_tokens: - continue - - ts = time.time() - tracker.add_token( - mint, - parsed["name"], - parsed["symbol"], - f"{provider_name}_block", - ts, - ) - known_tokens.add(mint) - except Exception as e: - print( - f"[ERROR] Failed to process block instruction: {e}" - ) + # Decode based on instruction type + if is_create_v2: + print(f"[{provider_name}_block] Detected: CreateV2 instruction (Token2022)") + decoded = decode_create_v2_instruction(data_bytes, account_keys) + else: + print(f"[{provider_name}_block] Detected: Create instruction (Legacy/Metaplex)") + decoded = decode_create_instruction(data_bytes, account_keys) + + if not decoded: + continue + + mint = decoded.get("mint") + if not mint: + continue + + if mint in known_tokens: + continue + + try: + ts = time.time() + tracker.add_token( + mint, + decoded["name"], + decoded["symbol"], + f"{provider_name}_block", + ts, + ) + known_tokens.add(mint) + except Exception as e: + print( + f"[ERROR] Failed to process block instruction: {e}" + ) except Exception as e: print(f"[ERROR] Failed to process transaction: {e}") @@ -424,9 +684,17 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens log_data = data["params"]["result"]["value"] logs = log_data.get("logs", []) - if not any( - "Program log: Instruction: Create" in log for log in logs - ): + # Detect both Create and CreateV2 instructions + is_create = any( + "Program log: Instruction: Create" in log + for log in logs + ) + is_create_v2 = any( + "Program log: Instruction: CreateV2" in log + for log in logs + ) + + if not (is_create or is_create_v2): continue for log in logs: @@ -435,7 +703,23 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens encoded_data = log.split(": ")[1] data_bytes = base64.b64decode(encoded_data) - parsed = parse_create_instruction(data_bytes) + # Check if this is a CreateEvent by validating discriminator + if len(data_bytes) < 8: + continue + + event_discriminator = data_bytes[:8] + if event_discriminator != CREATE_EVENT_DISCRIMINATOR: + # Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent) + continue + + # Parse based on instruction type + if is_create_v2: + print(f"[{provider_name}_logs] Detected: CreateV2 instruction (Token2022)") + parsed = parse_create_v2_event(data_bytes) + else: + print(f"[{provider_name}_logs] Detected: Create instruction (Legacy/Metaplex)") + parsed = parse_create_event(data_bytes) + if not parsed: continue @@ -492,11 +776,11 @@ async def listen_geyser_grpc( if GEYSER_AUTH_TYPE == "x-token": auth = grpc.metadata_call_credentials( - lambda context, callback: callback((("x-token", api_token),), None) + lambda _context, callback: callback((("x-token", api_token),), None) ) else: auth = grpc.metadata_call_credentials( - lambda context, callback: callback( + lambda _context, callback: callback( (("authorization", f"Basic {api_token}"),), None ) ) @@ -529,28 +813,44 @@ async def listen_geyser_grpc( continue for ix in msg.instructions: - if not ix.data.startswith(PUMP_CREATE_PREFIX): + # Check for both Create and CreateV2 instructions + is_create = ix.data.startswith(PUMP_CREATE_PREFIX) + is_create_v2 = ix.data.startswith(PUMP_CREATE_V2_PREFIX) + + if not (is_create or is_create_v2): continue - parsed = parse_create_instruction(ix.data) - if not parsed: + # Convert account keys to string format + account_keys = [] + for account_idx in ix.accounts: + if account_idx < len(msg.account_keys): + account_keys.append( + base58.b58encode(bytes(msg.account_keys[account_idx])).decode() + ) + + if len(account_keys) == 0: continue - if len(ix.accounts) == 0 or ix.accounts[0] >= len(msg.account_keys): + mint = account_keys[0] + if mint in known_tokens: continue - mint = base58.b58encode( - bytes(msg.account_keys[ix.accounts[0]]) - ).decode() + # Decode based on instruction type + if is_create_v2: + print(f"[{provider_name}_geyser] Detected: CreateV2 instruction (Token2022)") + decoded = decode_create_v2_instruction(ix.data, account_keys) + else: + print(f"[{provider_name}_geyser] Detected: Create instruction (Legacy/Metaplex)") + decoded = decode_create_instruction(ix.data, account_keys) - if mint in known_tokens: + if not decoded: continue ts = time.time() tracker.add_token( mint, - parsed["name"], - parsed["symbol"], + decoded["name"], + decoded["symbol"], f"{provider_name}_geyser", ts, ) diff --git a/learning-examples/listen-new-tokens/listen_blocksubscribe.py b/learning-examples/listen-new-tokens/listen_blocksubscribe.py index 3afc375a..424e82ad 100644 --- a/learning-examples/listen-new-tokens/listen_blocksubscribe.py +++ b/learning-examples/listen-new-tokens/listen_blocksubscribe.py @@ -2,7 +2,17 @@ Listens to Solana blocks for Pump.fun 'create' instructions via WebSocket. Decodes transaction data to extract mint, bonding curve, and user details. -It is usually slower than other listeners. +Performance: Usually slower than other listeners due to block-level processing. + +This script uses blockSubscribe which receives entire blocks containing transactions +that mention the Pump.fun program. It then decodes the instruction data from each +transaction to extract token creation details. + +WebSocket API Reference: +https://solana.com/docs/rpc/websocket/blocksubscribe + +Address Lookup Tables (ALT) Support: +https://solana.com/docs/advanced/lookup-tables """ import asyncio @@ -22,6 +32,93 @@ WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") +# Instruction discriminators (8-byte identifiers for instruction types) +# Calculated using the first 8 bytes of sha256("global:create") for legacy Create +# and sha256("global:createV2") for Token2022 CreateV2 +# See: learning-examples/calculate_discriminator.py +CREATE_DISCRIMINATOR = 8576854823835016728 +CREATE_V2_DISCRIMINATOR = struct.unpack(" 0 or readonly_count > 0: + print(f"ℹ️ [ALT] Used Address Lookup Table: {writable_count} writable, {readonly_count} readonly\n") + + elif discriminator == CREATE_V2_DISCRIMINATOR: + # CreateV2 instruction (Token2022 tokens) + create_v2_ix = next( + (instr for instr in idl["instructions"] + if instr["name"] == "createV2"), + next(instr for instr in idl["instructions"] + if instr["name"] == "create") + ) + account_keys = get_account_keys( + transaction, ix, loaded_addresses ) - print( - json.dumps( - decoded_args, indent=2 - ) + if account_keys is None: + print("⚠️ Skipping transaction due to unresolved accounts") + continue + + # Decode instruction data + decoded_args = decode_create_v2_instruction( + ix_data, + create_v2_ix, + account_keys, ) - print("--------------------") + + # Print token information + print_token_info(decoded_args) + + # Note if using Address Lookup Tables + if loaded_addresses: + writable_count = len(loaded_addresses.get("writable", [])) + readonly_count = len(loaded_addresses.get("readonly", [])) + if writable_count > 0 or readonly_count > 0: + print(f"ℹ️ [ALT] Used Address Lookup Table: {writable_count} writable, {readonly_count} readonly\n") elif "result" in data: print("Subscription confirmed") else: diff --git a/learning-examples/listen-new-tokens/listen_geyser.py b/learning-examples/listen-new-tokens/listen_geyser.py index 4ad10708..d234b8bb 100644 --- a/learning-examples/listen-new-tokens/listen_geyser.py +++ b/learning-examples/listen-new-tokens/listen_geyser.py @@ -1,10 +1,18 @@ """ Monitors Solana for new Pump.fun token creations using Geyser gRPC. Decodes 'create' instructions to extract and display token details (name, symbol, mint, bonding curve). + +Performance: Proven to be the fastest listener method available. + +This script uses Yellowstone Dragon's Mouth Geyser gRPC interface, which provides +real-time streaming of Solana blockchain data with lower latency than WebSocket methods. Requires a Geyser API token for access. -Supports both Basic and X-Token authentication methods. -It is proven to be the fastest listener. +Geyser gRPC Reference: +https://docs.triton.one/rpc-pool/grpc-subscriptions + +Authentication: Supports both Basic and X-Token authentication methods. +Configure via GEYSER_ENDPOINT, GEYSER_API_TOKEN, and AUTH_TYPE variables. """ import asyncio @@ -22,11 +30,53 @@ GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT") GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN") -# Default to x-token auth, can be set to "basic" +# Authentication type: "x-token" or "basic" AUTH_TYPE = "x-token" PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") + +# Instruction discriminators (8-byte identifiers for instruction types) +# Calculated using the first 8 bytes of sha256("global:create") for legacy Create +# and sha256("global:createV2") for Token2022 CreateV2 +# See: learning-examples/calculate_discriminator.py PUMP_CREATE_PREFIX = struct.pack(" dict: - """Decode a create instruction from transaction data.""" + """Decode a legacy create instruction (Metaplex) from transaction data.""" # Skip past the 8-byte discriminator prefix offset = 8 @@ -103,20 +153,68 @@ def read_pubkey(): "system_program": get_account_key(5), "rent": get_account_key(6), "user": get_account_key(7), + "token_standard": "legacy", + "is_mayhem_mode": False, + } + + return token_info + + +def decode_create_v2_instruction(ix_data: bytes, keys, accounts) -> dict: + """Decode a CreateV2 instruction (Token2022) from transaction data.""" + # Skip past the 8-byte discriminator prefix + offset = 8 + + # Extract account keys in base58 format + def get_account_key(index): + if index >= len(accounts): + return "N/A" + account_index = accounts[index] + return base58.b58encode(keys[account_index]).decode() + + # Read string fields (prefixed with length) + def read_string(): + nonlocal offset + # Get string length (4-byte uint) + length = struct.unpack_from(" len(data): + raise ValueError(f"Not enough data for {field_name} length at offset {offset}") + length = struct.unpack(" len(data): + raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}") + value = data[offset : offset + length].decode("utf-8") + offset += length + elif field_type == "publicKey": + # Pubkey is 32 bytes, encoded as base58 + if offset + 32 > len(data): + raise ValueError(f"Not enough data for {field_name} at offset {offset}") + value = base58.b58encode(data[offset : offset + 32]).decode("utf-8") + offset += 32 + + parsed_data[field_name] = value + + parsed_data["token_standard"] = "legacy" + parsed_data["is_mayhem_mode"] = False + return parsed_data + except Exception as e: + print(f"❌ Parse Create error: {e}") + print(f" Data length: {len(data)} bytes, offset: {offset}") + print(f" Data hex: {data.hex()[:200]}...") + return None + + +def parse_create_v2_instruction(data): + """ + Parse CreateEvent data from CreateV2 instruction (Token2022 tokens). + + CreateV2 uses Token-2022 standard with additional features. The event format + is identical to Create, with an additional optional is_mayhem_mode flag at the end. + + Token-2022 Reference: + https://spl.solana.com/token-2022 + + Args: + data: Raw event data bytes from program logs + + Returns: + Dictionary containing decoded token information, or None if parsing fails + """ + if len(data) < 8: + print(f"⚠️ Data too short for CreateV2 event: {len(data)} bytes") + return None + offset = 8 # Skip event discriminator + parsed_data = {} + + # Parse fields based on CreateV2Event structure + fields = [ + ("name", "string"), + ("symbol", "string"), + ("uri", "string"), + ("mint", "publicKey"), + ("bondingCurve", "publicKey"), + ("user", "publicKey"), + ("creator", "publicKey"), + ] + + try: + for field_name, field_type in fields: + if field_type == "string": + # String format: 4-byte length prefix + UTF-8 encoded string + if offset + 4 > len(data): + raise ValueError(f"Not enough data for {field_name} length at offset {offset}") length = struct.unpack(" len(data): + raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}") value = data[offset : offset + length].decode("utf-8") offset += length elif field_type == "publicKey": + # Pubkey is 32 bytes, encoded as base58 + if offset + 32 > len(data): + raise ValueError(f"Not enough data for {field_name} at offset {offset}") value = base58.b58encode(data[offset : offset + 32]).decode("utf-8") offset += 32 parsed_data[field_name] = value + # Parse is_mayhem_mode (OptionBool at the end) + # Format: 1 byte (0 = false/None, 1 = true) + if offset < len(data): + is_mayhem_mode = bool(data[offset]) + parsed_data["is_mayhem_mode"] = is_mayhem_mode + else: + parsed_data["is_mayhem_mode"] = False + + parsed_data["token_standard"] = "token2022" return parsed_data - except: + except Exception as e: + print(f"❌ Parse CreateV2 error: {e}") + print(f" Data length: {len(data)} bytes, offset: {offset}") + print(f" Data hex: {data.hex()[:200]}...") return None @@ -90,10 +244,17 @@ async def listen_for_new_tokens(): log_data = data["params"]["result"]["value"] logs = log_data.get("logs", []) - if any( + # Detect both Create and CreateV2 instructions + is_create = any( "Program log: Instruction: Create" in log for log in logs - ): + ) + is_create_v2 = any( + "Program log: Instruction: CreateV2" in log + for log in logs + ) + + if is_create or is_create_v2: for log in logs: if "Program data:" in log: try: @@ -101,22 +262,40 @@ async def listen_for_new_tokens(): decoded_data = base64.b64decode( encoded_data ) - parsed_data = parse_create_instruction( - decoded_data - ) - if parsed_data and "name" in parsed_data: - print( - "Signature:", - log_data.get("signature"), + + # Check if this is a CreateEvent by validating discriminator + if len(decoded_data) < 8: + continue + + event_discriminator = decoded_data[:8] + if event_discriminator != CREATE_EVENT_DISCRIMINATOR: + # Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent) + continue + + print(f"\n🔍 Found CreateEvent, length: {len(decoded_data)} bytes") + print(f" Signature: {log_data.get('signature')}") + + # Both create and create_v2 emit the same CreateEvent + # The difference is in the optional is_mayhem_mode field + if is_create_v2: + parsed_data = parse_create_v2_instruction( + decoded_data ) - for key, value in parsed_data.items(): - print(f"{key}: {value}") - print( - "##########################################################################################" + else: + parsed_data = parse_create_instruction( + decoded_data + ) + + if parsed_data and "name" in parsed_data: + # Print token information in consistent format + print_token_info( + parsed_data, + signature=log_data.get("signature") ) + else: + print(f"⚠️ Parsing failed for CreateEvent") except Exception as e: - print(f"Failed to decode: {log}") - print(f"Error: {e!s}") + print(f"❌ Error processing log: {e!s}") except Exception as e: print(f"An error occurred while processing message: {e}") diff --git a/learning-examples/listen-new-tokens/listen_logsubscribe_abc.py b/learning-examples/listen-new-tokens/listen_logsubscribe_abc.py index 460ccc61..58cf8f3d 100644 --- a/learning-examples/listen-new-tokens/listen_logsubscribe_abc.py +++ b/learning-examples/listen-new-tokens/listen_logsubscribe_abc.py @@ -1,9 +1,19 @@ """ Listens for new Pump.fun token creations via Solana WebSocket. Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.). -Additionally, calculates an associated bonding curve address for each token. +Additionally, calculates the associated bonding curve address for each token using PDA derivation. -It is usually faster than blockSubscribe, but slower than Geyser. +Performance: Usually faster than blockSubscribe, but slower than Geyser. + +This script demonstrates Program Derived Address (PDA) calculation for the associated +bonding curve, which is the token account owned by the bonding curve that holds +the minted tokens. + +WebSocket API Reference: +https://solana.com/docs/rpc/websocket/logssubscribe + +Program Derived Addresses: +https://solana.com/docs/core/pda """ import asyncio @@ -26,11 +36,67 @@ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" ) +# Event discriminator for CreateEvent (8-byte identifier) +# This is emitted by both Create and CreateV2 instructions +# Calculated using the first 8 bytes of sha256("event:CreateEvent") +CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118]) + + +def print_token_info(token_data, signature=None, associated_bonding_curve=None): + """ + Print token information in a consistent, user-friendly format. + + Args: + token_data: Dictionary containing token fields + signature: Optional transaction signature + associated_bonding_curve: Optional associated bonding curve address + """ + print("\n" + "=" * 80) + print("🎯 NEW TOKEN DETECTED") + print("=" * 80) + print(f"Name: {token_data.get('name', 'N/A')}") + print(f"Symbol: {token_data.get('symbol', 'N/A')}") + print(f"Mint: {token_data.get('mint', 'N/A')}") + + if "bondingCurve" in token_data: + print(f"Bonding Curve: {token_data['bondingCurve']}") + if associated_bonding_curve: + print(f"Associated BC: {associated_bonding_curve}") + if "user" in token_data: + print(f"User: {token_data['user']}") + if "creator" in token_data: + print(f"Creator: {token_data['creator']}") + + print(f"Token Standard: {token_data.get('token_standard', 'N/A')}") + print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}") + + if "uri" in token_data: + print(f"URI: {token_data['uri']}") + if signature: + print(f"Signature: {signature}") + + print("=" * 80 + "\n") + def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey: """ - Find the associated bonding curve for a given mint and bonding curve. - This uses the standard ATA derivation. + Calculate the associated token account (ATA) address for the bonding curve. + + The associated bonding curve is a Program Derived Address (PDA) that holds + the token supply controlled by the bonding curve. It's derived using the + standard Associated Token Account (ATA) derivation. + + ATA Derivation: find_program_address( + [bonding_curve_pubkey, token_program_id, mint_pubkey], + associated_token_program_id + ) + + Args: + mint: The token mint pubkey + bonding_curve: The bonding curve pubkey + + Returns: + The derived associated bonding curve address """ derived_address, _ = Pubkey.find_program_address( [ @@ -44,7 +110,9 @@ def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey def parse_create_instruction(data): + """Parse legacy Create instruction (Metaplex tokens).""" if len(data) < 8: + print(f"⚠️ Data too short for Create instruction: {len(data)} bytes") return None offset = 8 parsed_data = {} @@ -63,18 +131,81 @@ def parse_create_instruction(data): try: for field_name, field_type in fields: if field_type == "string": + if offset + 4 > len(data): + raise ValueError(f"Not enough data for {field_name} length at offset {offset}") length = struct.unpack(" len(data): + raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}") value = data[offset : offset + length].decode("utf-8") offset += length elif field_type == "publicKey": + if offset + 32 > len(data): + raise ValueError(f"Not enough data for {field_name} at offset {offset}") value = base58.b58encode(data[offset : offset + 32]).decode("utf-8") offset += 32 parsed_data[field_name] = value + parsed_data["token_standard"] = "legacy" + parsed_data["is_mayhem_mode"] = False return parsed_data - except: + except Exception as e: + print(f"❌ Parse Create error: {e}") + print(f" Data length: {len(data)} bytes, offset: {offset}") + return None + + +def parse_create_v2_instruction(data): + """Parse CreateV2 instruction (Token2022 tokens).""" + if len(data) < 8: + print(f"⚠️ Data too short for CreateV2 instruction: {len(data)} bytes") + return None + offset = 8 + parsed_data = {} + + # Parse fields based on CreateV2Event structure + fields = [ + ("name", "string"), + ("symbol", "string"), + ("uri", "string"), + ("mint", "publicKey"), + ("bondingCurve", "publicKey"), + ("user", "publicKey"), + ("creator", "publicKey"), + ] + + try: + for field_name, field_type in fields: + if field_type == "string": + if offset + 4 > len(data): + raise ValueError(f"Not enough data for {field_name} length at offset {offset}") + length = struct.unpack(" len(data): + raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}") + value = data[offset : offset + length].decode("utf-8") + offset += length + elif field_type == "publicKey": + if offset + 32 > len(data): + raise ValueError(f"Not enough data for {field_name} at offset {offset}") + value = base58.b58encode(data[offset : offset + 32]).decode("utf-8") + offset += 32 + + parsed_data[field_name] = value + + # Parse is_mayhem_mode (OptionBool at the end) + if offset < len(data): + is_mayhem_mode = bool(data[offset]) + parsed_data["is_mayhem_mode"] = is_mayhem_mode + else: + parsed_data["is_mayhem_mode"] = False + + parsed_data["token_standard"] = "token2022" + return parsed_data + except Exception as e: + print(f"❌ Parse CreateV2 error: {e}") + print(f" Data length: {len(data)} bytes, offset: {offset}") return None @@ -111,10 +242,17 @@ async def listen_for_new_tokens(): log_data = data["params"]["result"]["value"] logs = log_data.get("logs", []) - if any( + # Detect both Create and CreateV2 instructions + is_create = any( "Program log: Instruction: Create" in log for log in logs - ): + ) + is_create_v2 = any( + "Program log: Instruction: CreateV2" in log + for log in logs + ) + + if is_create or is_create_v2: for log in logs: if "Program data:" in log: try: @@ -122,38 +260,56 @@ async def listen_for_new_tokens(): decoded_data = base64.b64decode( encoded_data ) - parsed_data = parse_create_instruction( - decoded_data - ) - if parsed_data and "name" in parsed_data: - print( - "Signature:", - log_data.get("signature"), + + # Check if this is a CreateEvent by validating discriminator + if len(decoded_data) < 8: + continue + + event_discriminator = decoded_data[:8] + if event_discriminator != CREATE_EVENT_DISCRIMINATOR: + # Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent) + continue + + print(f"\n🔍 Found CreateEvent, length: {len(decoded_data)} bytes") + print(f" Signature: {log_data.get('signature')}") + + # Both create and create_v2 emit the same CreateEvent + # The difference is in the optional is_mayhem_mode field + if is_create_v2: + print("📝 Instruction: CreateV2 (Token2022)") + parsed_data = ( + parse_create_v2_instruction( + decoded_data + ) + ) + else: + print("📝 Instruction: Create (Legacy/Metaplex)") + parsed_data = parse_create_instruction( + decoded_data ) - for key, value in parsed_data.items(): - print(f"{key}: {value}") - # Calculate associated bonding curve + if parsed_data and "name" in parsed_data: + # Calculate associated bonding curve using PDA derivation mint = Pubkey.from_string( parsed_data["mint"] ) bonding_curve = Pubkey.from_string( parsed_data["bondingCurve"] ) - associated_curve = ( - find_associated_bonding_curve( - mint, bonding_curve - ) - ) - print( - f"Associated Bonding Curve: {associated_curve}" + associated_curve = find_associated_bonding_curve( + mint, bonding_curve ) - print( - "##########################################################################################" + + # Print token information in consistent format + print_token_info( + parsed_data, + signature=log_data.get("signature"), + associated_bonding_curve=str(associated_curve) ) + else: + print(f"⚠️ Parsing failed for CreateEvent") except Exception as e: - print(f"Failed to decode: {log}") - print(f"Error: {e!s}") + print(f"❌ Error processing log: {e!s}") except Exception as e: print(f"An error occurred while processing message: {e}") diff --git a/learning-examples/listen-new-tokens/listen_pumpportal.py b/learning-examples/listen-new-tokens/listen_pumpportal.py index 0bb5c720..265fc66b 100644 --- a/learning-examples/listen-new-tokens/listen_pumpportal.py +++ b/learning-examples/listen-new-tokens/listen_pumpportal.py @@ -1,5 +1,17 @@ """ Listens for new Pump.fun token creations via PumpPortal WebSocket. + +Performance: Fast, real-time data via third-party API. + +This script uses PumpPortal's WebSocket API, a third-party service that aggregates +and provides real-time Pump.fun token creation events. This provides additional +market data like initial buy amounts and market cap that aren't available in +raw blockchain data. + +PumpPortal API: https://pumpportal.fun/ + +Note: This is a third-party service and requires trust in the data provider. +For trustless monitoring, use the direct blockchain listeners (logs, block, geyser). """ import asyncio @@ -12,12 +24,52 @@ WS_URL = "wss://pumpportal.fun/api/data" -def format_sol(value): - return f"{value:.6f} SOL" +def print_token_info(token_data): + """ + Print token information in a consistent, user-friendly format. + + Args: + token_data: Dictionary containing token fields from PumpPortal + """ + print("\n" + "=" * 80) + print("🎯 NEW TOKEN DETECTED (via PumpPortal)") + print("=" * 80) + print(f"Name: {token_data.get('name', 'N/A')}") + print(f"Symbol: {token_data.get('symbol', 'N/A')}") + print(f"Mint: {token_data.get('mint', 'N/A')}") + + # PumpPortal-specific fields + if "initialBuy" in token_data: + initial_buy_sol = token_data['initialBuy'] + print(f"Initial Buy: {initial_buy_sol:.6f} SOL") + + if "marketCapSol" in token_data: + market_cap_sol = token_data['marketCapSol'] + print(f"Market Cap: {market_cap_sol:.6f} SOL") + + if "bondingCurveKey" in token_data: + print(f"Bonding Curve: {token_data['bondingCurveKey']}") + + if "traderPublicKey" in token_data: + print(f"Creator: {token_data['traderPublicKey']}") + + # Virtual reserves + if "vSolInBondingCurve" in token_data: + v_sol = token_data['vSolInBondingCurve'] + print(f"Virtual SOL: {v_sol:.6f} SOL") + + if "vTokensInBondingCurve" in token_data: + v_tokens = token_data['vTokensInBondingCurve'] + print(f"Virtual Tokens: {v_tokens:,.0f}") + + if "uri" in token_data: + print(f"URI: {token_data['uri']}") + + if "signature" in token_data: + print(f"Signature: {token_data['signature']}") + print("=" * 80 + "\n") -def format_timestamp(timestamp): - return datetime.fromtimestamp(timestamp / 1000).strftime("%Y-%m-%d %H:%M:%S") async def listen_for_new_tokens(): @@ -39,27 +91,8 @@ async def listen_for_new_tokens(): else: continue - print("\n" + "=" * 50) - print( - f"New token created: {token_info.get('name')} ({token_info.get('symbol')})" - ) - print("=" * 50) - print(f"Address: {token_info.get('mint')}") - print(f"Creator: {token_info.get('traderPublicKey')}") - print(f"Initial Buy: {format_sol(token_info.get('initialBuy', 0))}") - print( - f"Market Cap: {format_sol(token_info.get('marketCapSol', 0))}" - ) - print(f"Bonding Curve: {token_info.get('bondingCurveKey')}") - print( - f"Virtual SOL: {format_sol(token_info.get('vSolInBondingCurve', 0))}" - ) - print( - f"Virtual Tokens: {token_info.get('vTokensInBondingCurve', 0):,.0f}" - ) - print(f"Metadata URI: {token_info.get('uri')}") - print(f"Signature: {token_info.get('signature')}") - print("=" * 50) + # Print token information in consistent format + print_token_info(token_info) except websockets.exceptions.ConnectionClosed: print("\nWebSocket connection closed. Reconnecting...") break diff --git a/learning-examples/manual_buy.py b/learning-examples/manual_buy.py index cf6b2613..05cbeff4 100644 --- a/learning-examples/manual_buy.py +++ b/learning-examples/manual_buy.py @@ -7,7 +7,7 @@ import base58 import websockets -from construct import Bytes, Flag, Int64ul, Struct +from construct import Flag, Int64ul, Struct from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts @@ -36,6 +36,7 @@ PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" ) @@ -48,27 +49,59 @@ class BondingCurveState: - _STRUCT = Struct( + """Bonding curve state parser with progressive field parsing. + + Parses bonding curve account data progressively based on available bytes, + making it forward-compatible with future schema versions. + """ + + # Base struct present in all versions + _BASE_STRUCT = Struct( "virtual_token_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul, "real_token_reserves" / Int64ul, "real_sol_reserves" / Int64ul, "token_total_supply" / Int64ul, "complete" / Flag, - "creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey ) def __init__(self, data: bytes) -> None: - """Parse bonding curve data.""" + """Parse bonding curve data progressively based on available bytes. + + Args: + data: Raw account data including discriminator + + Raises: + ValueError: If discriminator is invalid or data is too short + """ + if len(data) < 8: + raise ValueError("Data too short to contain discriminator") + if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid curve state discriminator") - parsed = self._STRUCT.parse(data[8:]) + # Parse base fields (always present) + offset = 8 + base_data = data[offset:] + parsed = self._BASE_STRUCT.parse(base_data) self.__dict__.update(parsed) - # Convert raw bytes to Pubkey for creator field - if hasattr(self, "creator") and isinstance(self.creator, bytes): - self.creator = Pubkey.from_bytes(self.creator) + # Calculate offset after base struct + offset += self._BASE_STRUCT.sizeof() + + # Parse creator if bytes remaining (added in V2) + if len(data) >= offset + 32: + creator_bytes = data[offset : offset + 32] + self.creator = Pubkey.from_bytes(creator_bytes) + offset += 32 + else: + self.creator = None + + # Parse mayhem mode flag if bytes remaining (added in V3) + if len(data) >= offset + 1: + self.is_mayhem_mode = bool(data[offset]) + else: + self.is_mayhem_mode = False async def get_pump_curve_state( @@ -126,11 +159,59 @@ def _find_fee_config() -> Pubkey: return derived_address +async def get_fee_recipient( + client: AsyncClient, curve_state: BondingCurveState +) -> Pubkey: + """Determine the correct fee recipient based on mayhem mode. + + Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account) + instead of the standard fee recipient. This function checks the bonding curve state + and returns the appropriate fee recipient. + + Args: + client: Solana RPC client to fetch Global account data + curve_state: Parsed bonding curve state containing is_mayhem_mode flag + + Returns: + Appropriate fee recipient pubkey (mayhem or standard) + """ + if not curve_state.is_mayhem_mode: + return PUMP_FEE + + # Fetch Global account to get reserved_fee_recipient for mayhem mode tokens + response = await client.get_account_info(PUMP_GLOBAL, encoding="base64") + if not response.value or not response.value.data: + # Fallback to standard fee if Global account cannot be fetched + return PUMP_FEE + + data = response.value.data + + # Parse reserved_fee_recipient from Global account + # Offset calculation based on pump_fun_idl.json Global struct: + # discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) + + # initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) + + # initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) + + # withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) + + # creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) + + # admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483 + RESERVED_FEE_RECIPIENT_OFFSET = 483 + + if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32: + # Fallback if account data is too short + return PUMP_FEE + + reserved_fee_recipient_bytes = data[ + RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32 + ] + return Pubkey.from_bytes(reserved_fee_recipient_bytes) + + async def buy_token( mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, creator_vault: Pubkey, + token_program: Pubkey, amount: float, slippage: float = 0.25, max_retries=5, @@ -139,10 +220,12 @@ async def buy_token( payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: - associated_token_account = get_associated_token_address(payer.pubkey(), mint) + associated_token_account = get_associated_token_address( + payer.pubkey(), mint, token_program_id=token_program + ) amount_lamports = int(amount * LAMPORTS_PER_SOL) - # Fetch the token price + # Fetch bonding curve state to calculate price and determine fee recipient curve_state = await get_pump_curve_state(client, bonding_curve) token_price_sol = calculate_pump_curve_price(curve_state) token_amount = amount / token_price_sol @@ -150,9 +233,12 @@ async def buy_token( # Calculate maximum SOL to spend with slippage max_amount_lamports = int(amount_lamports * (1 + slippage)) + # Determine fee recipient based on whether token uses mayhem mode + fee_recipient = await get_fee_recipient(client, curve_state) + accounts = [ AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), AccountMeta( @@ -168,7 +254,7 @@ async def buy_token( AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta( - pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False + pubkey=token_program, is_signer=False, is_writable=False ), AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), AccountMeta( @@ -178,7 +264,7 @@ async def buy_token( AccountMeta( pubkey=_find_global_volume_accumulator(), is_signer=False, - is_writable=True, + is_writable=False, ), AccountMeta( pubkey=_find_user_volume_accumulator(payer.pubkey()), @@ -200,14 +286,17 @@ async def buy_token( ] discriminator = struct.pack(" None: - """Parse bonding curve data.""" + """Parse bonding curve data progressively based on available bytes. + + Args: + data: Raw account data including discriminator + + Raises: + ValueError: If discriminator is invalid or data is too short + """ + if len(data) < 8: + raise ValueError("Data too short to contain discriminator") + if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid curve state discriminator") - parsed = self._STRUCT.parse(data[8:]) + # Parse base fields (always present) + offset = 8 + base_data = data[offset:] + parsed = self._BASE_STRUCT.parse(base_data) self.__dict__.update(parsed) - # Convert raw bytes to Pubkey for creator field - if hasattr(self, "creator") and isinstance(self.creator, bytes): - self.creator = Pubkey.from_bytes(self.creator) + # Calculate offset after base struct + offset += self._BASE_STRUCT.sizeof() + + # Parse creator if bytes remaining (added in V2) + if len(data) >= offset + 32: + creator_bytes = data[offset : offset + 32] + self.creator = Pubkey.from_bytes(creator_bytes) + offset += 32 + else: + self.creator = None + + # Parse mayhem mode flag if bytes remaining (added in V3) + if len(data) >= offset + 1: + self.is_mayhem_mode = bool(data[offset]) + else: + self.is_mayhem_mode = False async def get_pump_curve_state( @@ -147,6 +180,53 @@ def _find_fee_config() -> Pubkey: return derived_address +async def get_fee_recipient( + client: AsyncClient, curve_state: BondingCurveState +) -> Pubkey: + """Determine the correct fee recipient based on mayhem mode. + + Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account) + instead of the standard fee recipient. This function checks the bonding curve state + and returns the appropriate fee recipient. + + Args: + client: Solana RPC client to fetch Global account data + curve_state: Parsed bonding curve state containing is_mayhem_mode flag + + Returns: + Appropriate fee recipient pubkey (mayhem or standard) + """ + if not curve_state.is_mayhem_mode: + return PUMP_FEE + + # Fetch Global account to get reserved_fee_recipient for mayhem mode tokens + response = await client.get_account_info(PUMP_GLOBAL, encoding="base64") + if not response.value or not response.value.data: + # Fallback to standard fee if Global account cannot be fetched + return PUMP_FEE + + data = response.value.data + + # Parse reserved_fee_recipient from Global account + # Offset calculation based on pump_fun_idl.json Global struct: + # discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) + + # initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) + + # initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) + + # withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) + + # creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) + + # admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483 + RESERVED_FEE_RECIPIENT_OFFSET = 483 + + if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32: + # Fallback if account data is too short + return PUMP_FEE + + reserved_fee_recipient_bytes = data[ + RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32 + ] + return Pubkey.from_bytes(reserved_fee_recipient_bytes) + + def set_loaded_accounts_data_size_limit(bytes_limit: int) -> Instruction: """ Create SetLoadedAccountsDataSizeLimit instruction to reduce CU consumption. @@ -169,6 +249,7 @@ async def buy_token( bonding_curve: Pubkey, associated_bonding_curve: Pubkey, creator_vault: Pubkey, + token_program: Pubkey, amount: float, slippage: float = 0.25, max_retries=5, @@ -177,10 +258,12 @@ async def buy_token( payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: - associated_token_account = get_associated_token_address(payer.pubkey(), mint) + associated_token_account = get_associated_token_address( + payer.pubkey(), mint, token_program_id=token_program + ) amount_lamports = int(amount * LAMPORTS_PER_SOL) - # Fetch the token price + # Fetch bonding curve state to calculate price and determine fee recipient curve_state = await get_pump_curve_state(client, bonding_curve) token_price_sol = calculate_pump_curve_price(curve_state) token_amount = amount / token_price_sol @@ -188,9 +271,12 @@ async def buy_token( # Calculate maximum SOL to spend with slippage max_amount_lamports = int(amount_lamports * (1 + slippage)) + # Determine fee recipient based on whether token uses mayhem mode + fee_recipient = await get_fee_recipient(client, curve_state) + accounts = [ AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), AccountMeta( @@ -206,7 +292,7 @@ async def buy_token( AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta( - pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False + pubkey=token_program, is_signer=False, is_writable=False ), AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), AccountMeta( @@ -216,7 +302,7 @@ async def buy_token( AccountMeta( pubkey=_find_global_volume_accumulator(), is_signer=False, - is_writable=True, + is_writable=False, ), AccountMeta( pubkey=_find_user_volume_accumulator(payer.pubkey()), @@ -242,10 +328,11 @@ async def buy_token( discriminator + struct.pack(" None: - """Parse bonding curve data.""" + """Parse bonding curve data progressively based on available bytes. + + Args: + data: Raw account data including discriminator + + Raises: + ValueError: If discriminator is invalid or data is too short + """ + if len(data) < 8: + raise ValueError("Data too short to contain discriminator") + if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid curve state discriminator") - parsed = self._STRUCT.parse(data[8:]) + # Parse base fields (always present) + offset = 8 + base_data = data[offset:] + parsed = self._BASE_STRUCT.parse(base_data) self.__dict__.update(parsed) - # Convert raw bytes to Pubkey for creator field - if hasattr(self, "creator") and isinstance(self.creator, bytes): - self.creator = Pubkey.from_bytes(self.creator) + # Calculate offset after base struct + offset += self._BASE_STRUCT.sizeof() + + # Parse creator if bytes remaining (added in V2) + if len(data) >= offset + 32: + creator_bytes = data[offset : offset + 32] + self.creator = Pubkey.from_bytes(creator_bytes) + offset += 32 + else: + self.creator = None + + # Parse mayhem mode flag if bytes remaining (added in V3) + if len(data) >= offset + 1: + self.is_mayhem_mode = bool(data[offset]) + else: + self.is_mayhem_mode = False async def get_pump_curve_state( @@ -139,6 +172,53 @@ def _find_fee_config() -> Pubkey: return derived_address +async def get_fee_recipient( + client: AsyncClient, curve_state: BondingCurveState +) -> Pubkey: + """Determine the correct fee recipient based on mayhem mode. + + Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account) + instead of the standard fee recipient. This function checks the bonding curve state + and returns the appropriate fee recipient. + + Args: + client: Solana RPC client to fetch Global account data + curve_state: Parsed bonding curve state containing is_mayhem_mode flag + + Returns: + Appropriate fee recipient pubkey (mayhem or standard) + """ + if not curve_state.is_mayhem_mode: + return PUMP_FEE + + # Fetch Global account to get reserved_fee_recipient for mayhem mode tokens + response = await client.get_account_info(PUMP_GLOBAL, encoding="base64") + if not response.value or not response.value.data: + # Fallback to standard fee if Global account cannot be fetched + return PUMP_FEE + + data = response.value.data + + # Parse reserved_fee_recipient from Global account + # Offset calculation based on pump_fun_idl.json Global struct: + # discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) + + # initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) + + # initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) + + # withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) + + # creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) + + # admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483 + RESERVED_FEE_RECIPIENT_OFFSET = 483 + + if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32: + # Fallback if account data is too short + return PUMP_FEE + + reserved_fee_recipient_bytes = data[ + RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32 + ] + return Pubkey.from_bytes(reserved_fee_recipient_bytes) + + async def create_geyser_connection(): """Establish a secure connection to the Geyser endpoint using the configured auth type.""" if AUTH_TYPE == "x-token": @@ -232,7 +312,13 @@ async def listen_for_create_transaction_geyser(): # Check each instruction in the transaction for ix in msg.instructions: - if not ix.data.startswith(PUMP_CREATE_DISCRIMINATOR): + # Check which create instruction was used + token_program = None + if ix.data.startswith(PUMP_CREATE_DISCRIMINATOR): + token_program = SYSTEM_TOKEN_PROGRAM + elif ix.data.startswith(PUMP_CREATE_V2_DISCRIMINATOR): + token_program = SYSTEM_TOKEN_2022_PROGRAM + else: continue # Found a create instruction @@ -240,6 +326,10 @@ async def listen_for_create_transaction_geyser(): ix.data, msg.account_keys, ix.accounts ) + # Add token program info to decoded args + token_data["token_program"] = str(token_program) + token_data["is_token_2022"] = (token_program == SYSTEM_TOKEN_2022_PROGRAM) + signature = base58.b58encode( bytes(update.transaction.transaction.signature) ).decode() @@ -253,6 +343,7 @@ async def buy_token( bonding_curve: Pubkey, associated_bonding_curve: Pubkey, creator_vault: Pubkey, + token_program: Pubkey, amount: float, slippage: float = 0.25, max_retries=5, @@ -262,22 +353,30 @@ async def buy_token( async with AsyncClient(RPC_ENDPOINT) as client: associated_token_account = get_associated_token_address( - payer.pubkey(), mint, SYSTEM_TOKEN_PROGRAM + payer.pubkey(), mint, token_program ) amount_lamports = int(amount * LAMPORTS_PER_SOL) - # Fetch the token price + # Fetch bonding curve state to calculate price and determine fee recipient + # NOTE: Price calculation is commented out to speed up testing - using fixed values + # For production, uncomment the lines below to: + # 1. Calculate proper token amounts based on current price + # 2. Detect mayhem mode and use correct fee recipient # curve_state = await get_pump_curve_state(client, bonding_curve) # token_price_sol = calculate_pump_curve_price(curve_state) # token_amount = amount / token_price_sol - token_amount = 100 + # fee_recipient = await get_fee_recipient(client, curve_state) + + # Testing values - replace with code above for production + token_amount = 100 # Fixed token amount + fee_recipient = PUMP_FEE # Standard fee recipient (doesn't detect mayhem mode) # Calculate maximum SOL to spend with slippage max_amount_lamports = int(amount_lamports * (1 + slippage)) accounts = [ AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), AccountMeta( @@ -293,7 +392,7 @@ async def buy_token( AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta( - pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False + pubkey=token_program, is_signer=False, is_writable=False ), AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), AccountMeta( @@ -303,7 +402,7 @@ async def buy_token( AccountMeta( pubkey=_find_global_volume_accumulator(), is_signer=False, - is_writable=True, + is_writable=False, ), AccountMeta( pubkey=_find_user_volume_accumulator(payer.pubkey()), @@ -329,10 +428,11 @@ async def buy_token( discriminator + struct.pack(" None: - """Parse bonding curve data.""" + """Parse bonding curve data progressively based on available bytes. + + Args: + data: Raw account data including discriminator + + Raises: + ValueError: If discriminator is invalid or data is too short + """ + if len(data) < 8: + raise ValueError("Data too short to contain discriminator") + if data[:8] != EXPECTED_DISCRIMINATOR: raise ValueError("Invalid curve state discriminator") - parsed = self._STRUCT.parse(data[8:]) + # Parse base fields (always present) + offset = 8 + base_data = data[offset:] + parsed = self._BASE_STRUCT.parse(base_data) self.__dict__.update(parsed) - # Convert raw bytes to Pubkey for creator field - if hasattr(self, "creator") and isinstance(self.creator, bytes): - self.creator = Pubkey.from_bytes(self.creator) + # Calculate offset after base struct + offset += self._BASE_STRUCT.sizeof() + + # Parse creator if bytes remaining (added in V2) + if len(data) >= offset + 32: + creator_bytes = data[offset : offset + 32] + self.creator = Pubkey.from_bytes(creator_bytes) + offset += 32 + else: + self.creator = None + + # Parse mayhem mode flag if bytes remaining (added in V3) + if len(data) >= offset + 1: + self.is_mayhem_mode = bool(data[offset]) + else: + self.is_mayhem_mode = False async def get_pump_curve_state( @@ -84,11 +117,13 @@ def get_bonding_curve_address(mint: Pubkey) -> tuple[Pubkey, int]: return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], PUMP_PROGRAM) -def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey: +def find_associated_bonding_curve( + mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey +) -> Pubkey: derived_address, _ = Pubkey.find_program_address( [ bytes(bonding_curve), - bytes(SYSTEM_TOKEN_PROGRAM), + bytes(token_program_id), bytes(mint), ], SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, @@ -112,6 +147,53 @@ def _find_fee_config() -> Pubkey: return derived_address +async def get_fee_recipient( + client: AsyncClient, curve_state: BondingCurveState +) -> Pubkey: + """Determine the correct fee recipient based on mayhem mode. + + Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account) + instead of the standard fee recipient. This function checks the bonding curve state + and returns the appropriate fee recipient. + + Args: + client: Solana RPC client to fetch Global account data + curve_state: Parsed bonding curve state containing is_mayhem_mode flag + + Returns: + Appropriate fee recipient pubkey (mayhem or standard) + """ + if not curve_state.is_mayhem_mode: + return PUMP_FEE + + # Fetch Global account to get reserved_fee_recipient for mayhem mode tokens + response = await client.get_account_info(PUMP_GLOBAL, encoding="base64") + if not response.value or not response.value.data: + # Fallback to standard fee if Global account cannot be fetched + return PUMP_FEE + + data = response.value.data + + # Parse reserved_fee_recipient from Global account + # Offset calculation based on pump_fun_idl.json Global struct: + # discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) + + # initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) + + # initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) + + # withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) + + # creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) + + # admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483 + RESERVED_FEE_RECIPIENT_OFFSET = 483 + + if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32: + # Fallback if account data is too short + return PUMP_FEE + + reserved_fee_recipient_bytes = data[ + RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32 + ] + return Pubkey.from_bytes(reserved_fee_recipient_bytes) + + def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: raise ValueError("Invalid reserve state") @@ -128,11 +210,31 @@ async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey) return 0 +async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey: + """Determines if a mint uses TokenProgram or Token2022Program.""" + mint_info = await client.get_account_info(mint_address) + + if not mint_info.value: + raise ValueError(f"Could not fetch mint info for {mint_address}") + + owner = mint_info.value.owner + + if owner == SYSTEM_TOKEN_PROGRAM: + return SYSTEM_TOKEN_PROGRAM + elif owner == TOKEN_2022_PROGRAM: + return TOKEN_2022_PROGRAM + else: + raise ValueError( + f"Mint account {mint_address} is owned by an unknown program: {owner}" + ) + + async def sell_token( mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, creator_vault: Pubkey, + token_program_id: Pubkey, slippage: float = 0.25, max_retries=5, ): @@ -140,7 +242,9 @@ async def sell_token( payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: - associated_token_account = get_associated_token_address(payer.pubkey(), mint) + associated_token_account = get_associated_token_address( + payer.pubkey(), mint, token_program_id + ) # Get token balance token_balance = await get_token_balance(client, associated_token_account) @@ -150,7 +254,7 @@ async def sell_token( print("No tokens to sell.") return - # Fetch the token price + # Fetch bonding curve state to calculate price and determine fee recipient curve_state = await get_pump_curve_state(client, bonding_curve) token_price_sol = calculate_pump_curve_price(curve_state) print(f"Price per Token: {token_price_sol:.20f} SOL") @@ -164,9 +268,12 @@ async def sell_token( print(f"Selling {token_balance_decimal} tokens") print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") + # Determine fee recipient based on whether token uses mayhem mode + fee_recipient = await get_fee_recipient(client, curve_state) + accounts = [ AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), AccountMeta( @@ -187,8 +294,8 @@ async def sell_token( is_writable=True, ), AccountMeta( - pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False - ), + pubkey=token_program_id, is_signer=False, is_writable=False + ), # Use dynamic token_program_id AccountMeta( pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False ), @@ -208,10 +315,13 @@ async def sell_token( ] discriminator = struct.pack(" tuple[Pubkey, int]: + """Find the bonding curve PDA for a mint.""" + return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], PUMP_PROGRAM) + + +def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey: + """Find the associated bonding curve token account.""" + derived_address, _ = Pubkey.find_program_address( + [ + bytes(bonding_curve), + bytes(TOKEN_2022_PROGRAM), + bytes(mint), + ], + SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, + ) + return derived_address + + +def find_creator_vault(creator: Pubkey) -> Pubkey: + """Find the creator vault PDA.""" + derived_address, _ = Pubkey.find_program_address( + [b"creator-vault", bytes(creator)], + PUMP_PROGRAM, + ) + return derived_address + + +def find_mayhem_state(mint: Pubkey) -> Pubkey: + """Find the mayhem state PDA for a mint. + + Seeds: ["mayhem-state", mint] (note: hyphen, not underscore) + """ + derived_address, _ = Pubkey.find_program_address( + [b"mayhem-state", bytes(mint)], + MAYHEM_PROGRAM_ID, + ) + return derived_address + + +def find_mayhem_token_vault(mint: Pubkey) -> Pubkey: + """Find the mayhem token vault - this is an ATA for sol_vault. + + This is derived as an Associated Token Account with: + - Owner: SOL_VAULT + - Mint: mint + - Token Program: TOKEN_2022_PROGRAM + """ + return get_associated_token_address(SOL_VAULT, mint, TOKEN_2022_PROGRAM) + + +def _find_global_volume_accumulator() -> Pubkey: + derived_address, _ = Pubkey.find_program_address( + [b"global_volume_accumulator"], + PUMP_PROGRAM, + ) + return derived_address + + +def _find_user_volume_accumulator(user: Pubkey) -> Pubkey: + derived_address, _ = Pubkey.find_program_address( + [b"user_volume_accumulator", bytes(user)], + PUMP_PROGRAM, + ) + return derived_address + + +def _find_fee_config() -> Pubkey: + derived_address, _ = Pubkey.find_program_address( + [b"fee_config", bytes(PUMP_PROGRAM)], + PUMP_FEE_PROGRAM, + ) + return derived_address + + +def create_pump_create_v2_instruction( + mint: Pubkey, + mint_authority: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + global_state: Pubkey, + user: Pubkey, + creator: Pubkey, + name: str, + symbol: str, + uri: str, + is_mayhem_mode: bool = False, +) -> Instruction: + """Create the pump.fun create_v2 instruction for Token2022. + + Account order matches pump_fun_idl.json create_v2 instruction. + """ + accounts = [ + AccountMeta(pubkey=mint, is_signer=True, is_writable=True), + AccountMeta(pubkey=mint_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=global_state, is_signer=False, is_writable=False), + AccountMeta(pubkey=user, is_signer=True, is_writable=True), + AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=TOKEN_2022_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, + is_signer=False, + is_writable=False, + ), + ] + + # Add mayhem accounts if enabled (must come before event_authority and program) + if is_mayhem_mode: + mayhem_state = find_mayhem_state(mint) + mayhem_token_vault = find_mayhem_token_vault(mint) + + accounts.extend( + [ + AccountMeta( + pubkey=MAYHEM_PROGRAM_ID, is_signer=False, is_writable=True + ), + AccountMeta(pubkey=GLOBAL_PARAMS, is_signer=False, is_writable=False), + AccountMeta(pubkey=SOL_VAULT, is_signer=False, is_writable=True), + AccountMeta(pubkey=mayhem_state, is_signer=False, is_writable=True), + AccountMeta( + pubkey=mayhem_token_vault, is_signer=False, is_writable=True + ), + ] + ) + + # Event authority and program come last + accounts.extend( + [ + AccountMeta( + pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False + ), + AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + ] + ) + + # Encode string as length-prefixed + def encode_string(s: str) -> bytes: + encoded = s.encode("utf-8") + return struct.pack(" bytes: + return bytes(pubkey) + + data = ( + CREATE_V2_DISCRIMINATOR + + encode_string(name) + + encode_string(symbol) + + encode_string(uri) + + encode_pubkey(creator) + + struct.pack(" Instruction: + """Create the extend_account instruction to expand bonding curve account size.""" + accounts = [ + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=user, is_signer=True, is_writable=True), + AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + ] + + # No arguments for extend_account instruction + data = EXTEND_ACCOUNT_DISCRIMINATOR + + return Instruction(PUMP_PROGRAM, data, accounts) + + +def create_buy_instruction( + global_state: Pubkey, + fee_recipient: Pubkey, + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + associated_user: Pubkey, + user: Pubkey, + creator_vault: Pubkey, + token_amount: int, + max_sol_cost: int, + track_volume: bool = True, +) -> Instruction: + """Create the buy instruction.""" + accounts = [ + AccountMeta(pubkey=global_state, is_signer=False, is_writable=False), + AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_user, is_signer=False, is_writable=True), + AccountMeta(pubkey=user, is_signer=True, is_writable=True), + AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=TOKEN_2022_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), + AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=_find_user_volume_accumulator(user), + is_signer=False, + is_writable=True, + ), + # Index 14: fee_config (readonly) + AccountMeta( + pubkey=_find_fee_config(), + is_signer=False, + is_writable=False, + ), + # Index 15: fee_program (readonly) + AccountMeta( + pubkey=PUMP_FEE_PROGRAM, + is_signer=False, + is_writable=False, + ), + ] + + # Encode OptionBool for track_volume + # OptionBool: [0] = None, [1, 0] = Some(false), [1, 1] = Some(true) + track_volume_bytes = bytes([1, 1 if track_volume else 0]) + + data = ( + BUY_DISCRIMINATOR + + struct.pack(" Pubkey: + """Get the appropriate fee recipient based on mayhem mode. + + For mayhem tokens, we need to use reserved_fee_recipient from Global account. + For standard tokens, we use the standard PUMP_FEE. + """ + if not is_mayhem: + return PUMP_FEE + + # Fetch Global account to get reserved_fee_recipient for mayhem mode + response = await client.get_account_info(PUMP_GLOBAL, encoding="base64") + if not response.value or not response.value.data: + print("Warning: Could not fetch Global account, using standard fee recipient") + return PUMP_FEE + + data = response.value.data + + # Parse reserved_fee_recipient from Global account at offset 483 + RESERVED_FEE_RECIPIENT_OFFSET = 483 + + if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32: + print("Warning: Global account data too short, using standard fee recipient") + return PUMP_FEE + + reserved_fee_recipient_bytes = data[ + RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32 + ] + reserved_fee_recipient = Pubkey.from_bytes(reserved_fee_recipient_bytes) + + print(f"Using mayhem mode fee recipient: {reserved_fee_recipient}") + return reserved_fee_recipient + + +async def main(): + """Create and buy pump.fun token (Token2022) in a single transaction.""" + private_key_bytes = base58.b58decode(PRIVATE_KEY) + payer = Keypair.from_bytes(private_key_bytes) + mint_keypair = Keypair() + + print("Creating Token2022 token with:") + print(f" Name: {TOKEN_NAME}") + print(f" Symbol: {TOKEN_SYMBOL}") + print(f" Mint: {mint_keypair.pubkey()}") + print(f" Creator: {payer.pubkey()}") + print(f" Mayhem mode: {'Enabled' if ENABLE_MAYHEM_MODE else 'Disabled'}") + + # Derive PDAs + bonding_curve, _ = find_bonding_curve_address(mint_keypair.pubkey()) + associated_bonding_curve = find_associated_bonding_curve( + mint_keypair.pubkey(), bonding_curve + ) + user_ata = get_associated_token_address( + payer.pubkey(), mint_keypair.pubkey(), TOKEN_2022_PROGRAM + ) + creator_vault = find_creator_vault(payer.pubkey()) + + print("\nDerived addresses:") + print(f" Bonding curve: {bonding_curve}") + print(f" Associated bonding curve: {associated_bonding_curve}") + print(f" User ATA: {user_ata}") + print(f" Creator vault: {creator_vault}") + + if ENABLE_MAYHEM_MODE: + mayhem_state = find_mayhem_state(mint_keypair.pubkey()) + mayhem_token_vault = find_mayhem_token_vault(mint_keypair.pubkey()) + print(f" Mayhem state: {mayhem_state}") + print(f" Mayhem token vault: {mayhem_token_vault}") + + # Calculate buy parameters + # For pump.fun, we need to calculate expected tokens based on initial curve state + # Initial virtual reserves (from pump.fun constants) + initial_virtual_token_reserves = 1_073_000_000 * 10**TOKEN_DECIMALS + initial_virtual_sol_reserves = 30 * LAMPORTS_PER_SOL + initial_real_token_reserves = 793_100_000 * 10**TOKEN_DECIMALS + + initial_price = initial_virtual_sol_reserves / initial_virtual_token_reserves + + buy_amount_lamports = int(BUY_AMOUNT_SOL * LAMPORTS_PER_SOL) + expected_tokens = int( + (buy_amount_lamports * 0.99) / initial_price + ) # 1% buffer for fees + max_sol_cost = int(buy_amount_lamports * (1 + MAX_SLIPPAGE)) + + print("\nBuy parameters:") + print(f" Buy amount: {BUY_AMOUNT_SOL} SOL") + print(f" Expected tokens: {expected_tokens / 10**TOKEN_DECIMALS:.6f}") + print(f" Max SOL cost: {max_sol_cost / LAMPORTS_PER_SOL:.6f} SOL") + + # Send transaction + async with AsyncClient(RPC_ENDPOINT) as client: + # Get correct fee recipient based on mayhem mode + fee_recipient = await get_fee_recipient_for_mayhem(client, ENABLE_MAYHEM_MODE) + + instructions = [ + # Priority fee instructions + set_compute_unit_limit(COMPUTE_UNIT_LIMIT), + set_compute_unit_price(PRIORITY_FEE_MICROLAMPORTS), + # Create token with pump.fun create_v2 (Token2022) + create_pump_create_v2_instruction( + mint=mint_keypair.pubkey(), + mint_authority=PUMP_MINT_AUTHORITY, + bonding_curve=bonding_curve, + associated_bonding_curve=associated_bonding_curve, + global_state=PUMP_GLOBAL, + user=payer.pubkey(), + creator=payer.pubkey(), + name=TOKEN_NAME, + symbol=TOKEN_SYMBOL, + uri=TOKEN_URI, + is_mayhem_mode=ENABLE_MAYHEM_MODE, + ), + # Extend bonding curve account (required for frontend visibility) + create_extend_account_instruction( + bonding_curve=bonding_curve, + user=payer.pubkey(), + ), + # Create user ATA + create_idempotent_associated_token_account( + payer.pubkey(), + payer.pubkey(), + mint_keypair.pubkey(), + TOKEN_2022_PROGRAM, + ), + # Buy tokens + create_buy_instruction( + global_state=PUMP_GLOBAL, + fee_recipient=fee_recipient, + mint=mint_keypair.pubkey(), + bonding_curve=bonding_curve, + associated_bonding_curve=associated_bonding_curve, + associated_user=user_ata, + user=payer.pubkey(), + creator_vault=creator_vault, + token_amount=expected_tokens, + max_sol_cost=max_sol_cost, + track_volume=True, + ), + ] + + recent_blockhash = await client.get_latest_blockhash() + message = Message(instructions, payer.pubkey()) + transaction = Transaction( + [payer, mint_keypair], message, recent_blockhash.value.blockhash + ) + + print("\nSending transaction...") + opts = TxOpts(skip_preflight=True, preflight_commitment=Confirmed) + + try: + response = await client.send_transaction(transaction, opts) + tx_hash = response.value + + print(f"Transaction sent: https://solscan.io/tx/{tx_hash}") + + print("Waiting for confirmation...") + await client.confirm_transaction(tx_hash, commitment="confirmed") + print("Transaction confirmed!") + + return tx_hash + + except Exception as e: + print(f"Transaction failed: {e}") + raise + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/learning-examples/pumpswap/manual_buy_pumpswap.py b/learning-examples/pumpswap/manual_buy_pumpswap.py index a013a936..b7fa2c0f 100644 --- a/learning-examples/pumpswap/manual_buy_pumpswap.py +++ b/learning-examples/pumpswap/manual_buy_pumpswap.py @@ -1,12 +1,16 @@ """ -Solana PUMP AMM Interface - -This module provides functionality to interact with the PUMP AMM program on Solana, enabling: -- Finding market addresses by token mint -- Fetching and parsing market data from PUMP AMM pools -- Calculating token prices in AMM pools -- Creating associated token accounts (ATAs) idempotently -- Buying tokens on the PUMP AMM with slippage protection +This standalone script demonstrates how to buy tokens on the PUMP AMM (pAMM) protocol. +It covers the complete flow from finding markets to executing buys with mayhem mode support. + +Key concepts demonstrated: +- Finding AMM pool addresses by token mint +- Parsing binary account data structures +- Dynamic fee recipient calculation (mayhem mode vs standard) +- Program Derived Address (PDA) derivation +- WSOL wrapping (converting SOL to wrapped SOL for SPL token operations) +- Volume tracking incentives integration +- Transaction simulation before sending +- Slippage protection mechanisms """ import asyncio @@ -34,31 +38,33 @@ load_dotenv() -# Configuration constants +# ============================================================================ +# Configuration +# ============================================================================ + RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") -TOKEN_MINT = Pubkey.from_string("...") +TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) PAYER = Keypair.from_bytes(PRIVATE_KEY) -SLIPPAGE = 0.3 # Slippage tolerance (30%) - the maximum price movement you'll accept +SLIPPAGE = 0.3 # 30% - maximum acceptable price movement during trade + +# Token configuration +TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens + +# Program instruction discriminators (first 8 bytes identify the instruction) +BUY_DISCRIMINATOR = bytes.fromhex("66063d1201daebea") -TOKEN_DECIMALS = 6 -BUY_DISCRIMINATOR = bytes.fromhex( - "66063d1201daebea" -) # Program instruction identifier for the buy function +# ============================================================================ +# Solana Program IDs and System Accounts +# ============================================================================ -# Solana system addresses and program IDs SOL = Pubkey.from_string("So11111111111111111111111111111111111111112") PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string( "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw" ) -PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string( - "7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ" -) -PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string( - "7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC" -) SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" @@ -67,61 +73,91 @@ "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR" ) PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") + +# ============================================================================ +# Constants for Account Structure Parsing +# ============================================================================ + +# Pool account structure offsets +POOL_DISCRIMINATOR_SIZE = 8 +POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data +POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored +POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag + +# GlobalConfig structure offsets +GLOBALCONFIG_DISCRIMINATOR_SIZE = 8 +GLOBALCONFIG_ADMIN_SIZE = 32 +GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32 +GLOBALCONFIG_RESERVED_FEE_OFFSET = ( + GLOBALCONFIG_DISCRIMINATOR_SIZE + + GLOBALCONFIG_ADMIN_SIZE + + GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE +) + +# Fee recipients +STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string( + "7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ" +) + +# Solana constants LAMPORTS_PER_SOL = 1_000_000_000 -COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit -COMPUTE_UNIT_BUDGET = 200_000 # Maximum compute units to use +COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit +COMPUTE_UNIT_BUDGET = 200_000 # Max compute units for transaction + +# Buy-specific constants +PROTOCOL_FEE_BUFFER = 0.1 # 10% buffer for protocol fees when wrapping SOL +VOLUME_TRACKING_ENABLED = 1 # 1 = true, 0 = false + + +# ============================================================================ +# Market Discovery +# ============================================================================ async def get_market_address_by_base_mint( client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey ) -> Pubkey: - """Find the market address for a given token mint. + """Find the AMM pool address for a specific token. - Searches for the AMM pool that contains the specified token mint as its base token - by querying program accounts with a filter for the base_mint field. + Uses getProgramAccounts RPC method with a memcmp filter to find the pool + that matches the given token mint address. Args: - client: Solana RPC client instance - base_mint_address: Address of the token mint you want to find the market for - amm_program_id: Address of the AMM program + client: Solana RPC client + base_mint_address: Token mint to find the pool for + amm_program_id: PUMP AMM program address Returns: - The Pubkey of the market (AMM pool) for the token + Address of the AMM pool (market) for the token """ - base_mint_bytes = bytes(base_mint_address) - offset = ( - 43 # Offset where the base_mint field is stored in the account data structure - ) - filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)] - + filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))] response = await client.get_program_accounts( amm_program_id, encoding="base64", filters=filters ) - - market_address = [account.pubkey for account in response.value][0] - return market_address + return response.value[0].pubkey async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: - """Fetch and parse market data from the blockchain. + """Parse binary pool account data into a structured dictionary. - Retrieves and deserializes the binary data stored in the market account - into a structured dictionary containing key market information. + The pool account stores data in a specific binary format. This function + deserializes that data based on the known structure. Args: - client: Solana RPC client instance - market_address: Address of the market (AMM pool) to fetch data for + client: Solana RPC client + market_address: Address of the pool account Returns: - Dictionary containing the parsed market data + Dictionary with parsed pool data fields """ response = await client.get_account_info(market_address, encoding="base64") data = response.value.data parsed_data: dict = {} - # Start after the 8-byte discriminator - offset = 8 - # Define the structure of the market account data + offset = POOL_DISCRIMINATOR_SIZE + + # Field definitions: (name, type) + # Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes fields = [ ("pool_bump", "u8"), ("index", "u16"), @@ -141,39 +177,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: parsed_data[field_name] = base58.b58encode(value).decode("utf-8") offset += 32 elif field_type in {"u64", "i64"}: - value = ( - struct.unpack(" Pubkey: - """Derive the Program Derived Address (PDA) for a coin creator's vault. + """Derive the PDA for the coin creator's fee vault. - Calculates the deterministic PDA that serves as the vault authority - for a specific coin creator in the PUMP AMM protocol. + The creator vault collects fees on behalf of the token creator. + This is a deterministic address that can be recalculated by anyone. Args: - coin_creator: Pubkey of the coin creator account + coin_creator: Public key of the token creator Returns: - Pubkey of the derived coin creator vault authority - - Note: - This vault is used to collect creator fees from token transactions + PDA of the creator's vault authority """ derived_address, _ = Pubkey.find_program_address( [b"creator_vault", bytes(coin_creator)], @@ -183,13 +219,13 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey: def find_global_volume_accumulator() -> Pubkey: - """Derive the Program Derived Address (PDA) for the global volume accumulator. + """Derive the PDA for the global volume accumulator. - Calculates the deterministic PDA that tracks global trading volume - across all pools in the PUMP AMM protocol. + This account tracks total trading volume across all pools. + Volume tracking is used for incentive programs and analytics. Returns: - Pubkey of the derived global volume accumulator account + PDA of the global volume accumulator """ derived_address, _ = Pubkey.find_program_address( [b"global_volume_accumulator"], @@ -199,16 +235,16 @@ def find_global_volume_accumulator() -> Pubkey: def find_user_volume_accumulator(user: Pubkey) -> Pubkey: - """Derive the Program Derived Address (PDA) for a user's volume accumulator. + """Derive the PDA for a user's volume accumulator. - Calculates the deterministic PDA that tracks trading volume - for a specific user in the PUMP AMM protocol. + Tracks individual user's trading volume, which may qualify them + for incentives or rewards based on trading activity. Args: - user: Pubkey of the user account + user: Public key of the user Returns: - Pubkey of the derived user volume accumulator account + PDA of the user's volume accumulator """ derived_address, _ = Pubkey.find_program_address( [b"user_volume_accumulator", bytes(user)], @@ -218,10 +254,9 @@ def find_user_volume_accumulator(user: Pubkey) -> Pubkey: def find_fee_config() -> Pubkey: - """Derive the Program Derived Address (PDA) for the fee config. + """Derive the PDA for the fee configuration account. - Returns: - Pubkey of the derived fee config account + This account stores fee-related configuration for the AMM. """ derived_address, _ = Pubkey.find_program_address( [b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)], @@ -230,41 +265,139 @@ def find_fee_config() -> Pubkey: return derived_address +# ============================================================================ +# Mayhem Mode Fee Handling +# ============================================================================ +# Mayhem mode is a special fee structure where fees go to a different recipient. +# The fee recipient changes dynamically based on the pool's mayhem_mode flag. + + +async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey: + """Fetch the mayhem mode fee recipient from GlobalConfig. + + When mayhem mode is active, fees are redirected to a special recipient + stored in the GlobalConfig account. + + Args: + client: Solana RPC client + + Returns: + Public key of the mayhem mode fee recipient + """ + response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64") + if not response.value or not response.value.data: + msg = "Cannot fetch GlobalConfig account" + raise ValueError(msg) + + data = response.value.data + recipient_bytes = data[ + GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32 + ] + return Pubkey.from_bytes(recipient_bytes) + + +async def get_pumpswap_fee_recipients( + client: AsyncClient, pool: Pubkey +) -> tuple[Pubkey, Pubkey]: + """Determine the correct fee recipient based on pool's mayhem mode status. + + This function checks if mayhem mode is enabled for the pool and returns + the appropriate fee recipient and their WSOL token account. + + Args: + client: Solana RPC client + pool: Address of the AMM pool + + Returns: + Tuple of (fee_recipient_pubkey, fee_recipient_token_account) + """ + response = await client.get_account_info(pool, encoding="base64") + if not response.value or not response.value.data: + msg = "Cannot fetch pool account" + raise ValueError(msg) + + pool_data = response.value.data + + # Check if mayhem mode flag exists and is enabled + is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool( + pool_data[POOL_MAYHEM_MODE_OFFSET] + ) + + # Select appropriate fee recipient + if is_mayhem_mode: + fee_recipient = await get_reserved_fee_recipient_pumpswap(client) + else: + fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT + + # Get the fee recipient's WSOL token account + fee_recipient_token_account = get_associated_token_address( + fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM + ) + + return (fee_recipient, fee_recipient_token_account) + + +# ============================================================================ +# Price Calculation +# ============================================================================ + + async def calculate_token_pool_price( client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey, ) -> float: - """Calculate the current price of tokens in an AMM pool. + """Calculate current token price from AMM pool balances. - Fetches the balance of tokens in the pool and calculates the price ratio - between the base token and quote token (typically SOL). + AMM price is determined by the ratio of tokens in the pool: + price = quote_balance / base_balance Args: - client: Solana RPC client instance - pool_base_token_account: Address of the pool's base token account (your token) - pool_quote_token_account: Address of the pool's quote token account (SOL) + client: Solana RPC client + pool_base_token_account: Pool's token account (the token being priced) + pool_quote_token_account: Pool's SOL account (the quote currency) Returns: - The price of the base token in terms of the quote token (SOL per token) + Price in SOL per token """ base_balance_resp = await client.get_token_account_balance(pool_base_token_account) quote_balance_resp = await client.get_token_account_balance( pool_quote_token_account ) - # Extract the UI amounts (human-readable with decimals) base_amount = float(base_balance_resp.value.ui_amount) quote_amount = float(quote_balance_resp.value.ui_amount) - token_price = quote_amount / base_amount + return quote_amount / base_amount - return token_price + +# ============================================================================ +# Token Buying +# ============================================================================ + + +async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey: + """Determines if a mint uses TokenProgram or Token2022Program.""" + mint_info = await client.get_account_info(mint_address) + + if not mint_info.value: + raise ValueError(f"Could not fetch mint info for {mint_address}") + + owner = mint_info.value.owner + + if owner == SYSTEM_TOKEN_PROGRAM: + return SYSTEM_TOKEN_PROGRAM + elif owner == TOKEN_2022_PROGRAM: + return TOKEN_2022_PROGRAM + else: + raise ValueError( + f"Mint account {mint_address} is owned by an unknown program: {owner}" + ) async def buy_pump_swap( client: AsyncClient, - pump_fun_amm_market: Pubkey, + market: Pubkey, payer: Keypair, base_mint: Pubkey, user_base_token_account: Pubkey, @@ -273,52 +406,64 @@ async def buy_pump_swap( pool_quote_token_account: Pubkey, coin_creator_vault_authority: Pubkey, coin_creator_vault_ata: Pubkey, - sol_amount_to_spend: int, + sol_amount_to_spend: float, slippage: float = 0.25, ) -> str | None: - """Buy tokens on the PUMP AMM with slippage protection. + """Execute a token buy on the PUMP AMM with slippage protection. + + This function: + 1. Calculates expected token output based on current price + 2. Wraps SOL into WSOL (required for SPL token operations) + 3. Constructs and simulates the transaction + 4. Sends the buy transaction if simulation succeeds - Executes a token purchase on the PUMP AMM protocol, calculating the expected - token amount based on the current pool price and applying slippage protection. + Why WSOL wrapping is needed: + SPL tokens can only interact with other SPL tokens. Native SOL must be + wrapped into WSOL (an SPL token representation of SOL) before trading. Args: - client: Solana RPC client instance - pump_fun_amm_market: Address of the AMM market - payer: Keypair of the transaction signer and token buyer - base_mint: Address of the token mint being purchased - user_base_token_account: Address of the user's token account for receiving purchased tokens - user_quote_token_account: Address of the user's SOL token account - pool_base_token_account: Address of the pool's token account for the token being purchased - pool_quote_token_account: Address of the pool's SOL token account - coin_creator_vault_authority: Address of the coin creator's vault authority - coin_creator_vault_ata: Address of the coin creator's associated token account for fees - sol_amount_to_spend: Amount of SOL to spend on the purchase (in SOL, not lamports) - slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%) + client: Solana RPC client + market: AMM pool address + payer: Wallet keypair for signing + base_mint: Token mint address + user_base_token_account: User's token account (for receiving tokens) + user_quote_token_account: User's WSOL account + pool_base_token_account: Pool's token account + pool_quote_token_account: Pool's WSOL account + coin_creator_vault_authority: Creator vault PDA + coin_creator_vault_ata: Creator's WSOL account + sol_amount_to_spend: Amount of SOL to spend (in SOL, not lamports) + slippage: Maximum acceptable slippage (0.25 = 25%) Returns: Transaction signature if successful, None otherwise """ - # Calculate token price + token_program_id = await get_token_program_id(client, base_mint) token_price_sol = await calculate_token_pool_price( client, pool_base_token_account, pool_quote_token_account ) print(f"Token price in SOL: {token_price_sol:.10f} SOL") - # Calculate maximum SOL input with slippage protection + # Calculate expected token amount and maximum SOL we're willing to spend base_amount_out = int((sol_amount_to_spend / token_price_sol) * 10**TOKEN_DECIMALS) - slippage_factor = 1 + slippage - max_sol_input = int((sol_amount_to_spend * slippage_factor) * LAMPORTS_PER_SOL) + max_sol_input = int((sol_amount_to_spend * (1 + slippage)) * LAMPORTS_PER_SOL) print(f"Buying {base_amount_out / (10**TOKEN_DECIMALS):.10f} tokens") print(f"Maximum SOL input: {max_sol_input / LAMPORTS_PER_SOL:.10f} SOL") - # Calculate required PDAs for volume tracking + # Derive volume accumulator PDAs for incentive tracking global_volume_accumulator = find_global_volume_accumulator() user_volume_accumulator = find_user_volume_accumulator(payer.pubkey()) - # Define all accounts needed for the buy instruction + # Get fee recipient based on mayhem mode + fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients( + client, market + ) + + # Build account list for buy instruction + # Order matters! Must match the program's expected account layout accounts = [ - AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True), + AccountMeta(pubkey=market, is_signer=False, is_writable=True), AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False), AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False), @@ -327,15 +472,13 @@ async def buy_pump_swap( AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True), AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True), AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False), AccountMeta( - pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False + pubkey=fee_recipient_token_account, is_signer=False, is_writable=True ), AccountMeta( - pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT, - is_signer=False, - is_writable=True, - ), - AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + pubkey=token_program_id, is_signer=False, is_writable=False + ), # Use dynamic token_program_id AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta( @@ -352,34 +495,43 @@ async def buy_pump_swap( pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False ), AccountMeta( - pubkey=global_volume_accumulator, is_signer=False, is_writable=True + pubkey=global_volume_accumulator, is_signer=False, is_writable=False ), AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True), - # Index 21: fee_config (readonly) AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False), - # Index 22: fee_program (readonly) AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False), ] + # Instruction data format: + # discriminator (8 bytes) + amount_out (8 bytes) + max_in (8 bytes) + track_volume (1 byte) + # All integers are little-endian (<) data = ( BUY_DISCRIMINATOR - + struct.pack(" None: + """Execute the complete buy flow.""" + sol_amount_to_spend = 0.000001 # Amount of SOL to spend on the purchase async with AsyncClient(RPC_ENDPOINT) as client: + # Step 1: Find the pool address for our token market_address = await get_market_address_by_base_mint( client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID ) + + # Step 2: Parse pool data to get necessary accounts market_data = await get_market_data(client, market_address) + + # Determine token program ID for the base mint + token_program_id = await get_token_program_id(client, TOKEN_MINT) + + # Step 3: Derive PDAs needed for the transaction coin_creator_vault_authority = find_coin_creator_vault( Pubkey.from_string(market_data["coin_creator"]) ) coin_creator_vault_ata = get_associated_token_address( - coin_creator_vault_authority, SOL + coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM ) + # Step 4: Execute the buy await buy_pump_swap( client, market_address, PAYER, TOKEN_MINT, - get_associated_token_address(PAYER.pubkey(), TOKEN_MINT), - get_associated_token_address(PAYER.pubkey(), SOL), + get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id), + get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM), Pubkey.from_string(market_data["pool_base_token_account"]), Pubkey.from_string(market_data["pool_quote_token_account"]), coin_creator_vault_authority, diff --git a/learning-examples/pumpswap/manual_sell_pumpswap.py b/learning-examples/pumpswap/manual_sell_pumpswap.py index dfa36f9e..d5796358 100644 --- a/learning-examples/pumpswap/manual_sell_pumpswap.py +++ b/learning-examples/pumpswap/manual_sell_pumpswap.py @@ -1,10 +1,14 @@ """ -This module provides functionality to: -- Find market addresses by token mint. -- Fetch and parse market data from PUMP AMM pools. -- Calculate token prices in AMM pools. -- Create associated token accounts (ATAs) idempotently. -- Sell tokens on the PUMP AMM with slippage protection. +This standalone script demonstrates how to sell tokens on the PUMP AMM (pAMM) protocol. +It covers the complete flow from finding markets to executing sells with mayhem mode support. + +Key concepts demonstrated: +- Finding AMM pool addresses by token mint +- Parsing binary account data structures +- Dynamic fee recipient calculation (mayhem mode vs standard) +- Program Derived Address (PDA) derivation +- Transaction construction with compute budgets +- Slippage protection mechanisms """ import asyncio @@ -26,31 +30,33 @@ load_dotenv() -# Configuration constants +# ============================================================================ +# Configuration +# ============================================================================ + RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") -TOKEN_MINT = Pubkey.from_string("...") +TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) PAYER = Keypair.from_bytes(PRIVATE_KEY) -SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept +SLIPPAGE = 0.25 # 25% - maximum acceptable price movement during trade + +# Token configuration +TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens + +# Program instruction discriminators (first 8 bytes identify the instruction) +SELL_DISCRIMINATOR = bytes.fromhex("33e685a4017f83ad") -TOKEN_DECIMALS = 6 -SELL_DISCRIMINATOR = bytes.fromhex( - "33e685a4017f83ad" -) # Program instruction identifier for the sell function +# ============================================================================ +# Solana Program IDs and System Accounts +# ============================================================================ -# Solana system addresses and program IDs SOL = Pubkey.from_string("So11111111111111111111111111111111111111112") PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string( "ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw" ) -PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string( - "7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ" -) -PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string( - "7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC" -) SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" @@ -59,59 +65,87 @@ "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR" ) PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") + +# ============================================================================ +# Constants for Account Structure Parsing +# ============================================================================ + +# Pool account structure offsets +POOL_DISCRIMINATOR_SIZE = 8 +POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data +POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored +POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag + +# GlobalConfig structure offsets +GLOBALCONFIG_DISCRIMINATOR_SIZE = 8 +GLOBALCONFIG_ADMIN_SIZE = 32 +GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32 +GLOBALCONFIG_RESERVED_FEE_OFFSET = ( + GLOBALCONFIG_DISCRIMINATOR_SIZE + + GLOBALCONFIG_ADMIN_SIZE + + GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE +) + +# Fee recipients +STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string( + "7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ" +) + +# Solana constants LAMPORTS_PER_SOL = 1_000_000_000 -COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit -COMPUTE_UNIT_BUDGET = 100_000 # Maximum compute units to use +COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit +COMPUTE_UNIT_BUDGET = 150_000 # Max compute units for transaction + + +# ============================================================================ +# Market Discovery +# ============================================================================ async def get_market_address_by_base_mint( client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey ) -> Pubkey: - """Find the market address for a given token mint. + """Find the AMM pool address for a specific token. - Searches for the AMM pool that contains the specified token as its base token. + Uses getProgramAccounts RPC method with a memcmp filter to find the pool + that matches the given token mint address. Args: - client: Solana RPC client instance - base_mint_address: Address of the token mint you want to find the market for - amm_program_id: Address of the AMM program + client: Solana RPC client + base_mint_address: Token mint to find the pool for + amm_program_id: PUMP AMM program address Returns: - The Pubkey of the market (AMM pool) for the token + Address of the AMM pool (market) for the token """ - base_mint_bytes = bytes(base_mint_address) - offset = ( - 43 # Offset where the base_mint field is stored in the account data structure - ) - filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)] - + filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))] response = await client.get_program_accounts( amm_program_id, encoding="base64", filters=filters ) - - market_address = [account.pubkey for account in response.value][0] - return market_address + return response.value[0].pubkey async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: - """Fetch and parse market data from the blockchain. + """Parse binary pool account data into a structured dictionary. - Retrieves and deserializes the data stored in the market account. + The pool account stores data in a specific binary format. This function + deserializes that data based on the known structure. Args: - client: Solana RPC client instance - market_address: Address of the market (AMM pool) to fetch data for + client: Solana RPC client + market_address: Address of the pool account Returns: - Dictionary containing the parsed market data + Dictionary with parsed pool data fields """ response = await client.get_account_info(market_address, encoding="base64") data = response.value.data parsed_data: dict = {} - # Start after the 8-byte discriminator - offset = 8 - # Define the structure of the market account data + offset = POOL_DISCRIMINATOR_SIZE + + # Field definitions: (name, type) + # Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes fields = [ ("pool_bump", "u8"), ("index", "u16"), @@ -131,39 +165,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: parsed_data[field_name] = base58.b58encode(value).decode("utf-8") offset += 32 elif field_type in {"u64", "i64"}: - value = ( - struct.unpack(" Pubkey: - """Derive the Program Derived Address (PDA) for a coin creator's vault. + """Derive the PDA for the coin creator's fee vault. - Calculates the deterministic PDA that serves as the vault authority - for a specific coin creator in the PUMP AMM protocol. + The creator vault collects fees on behalf of the token creator. + This is a deterministic address that can be recalculated by anyone. Args: - coin_creator: Pubkey of the coin creator account + coin_creator: Public key of the token creator Returns: - Pubkey of the derived coin creator vault authority - - Note: - This vault is used to collect creator fees from token transactions + PDA of the creator's vault authority """ derived_address, _ = Pubkey.find_program_address( [b"creator_vault", bytes(coin_creator)], @@ -173,10 +207,9 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey: def find_fee_config() -> Pubkey: - """Derive the Program Derived Address (PDA) for the fee config. + """Derive the PDA for the fee configuration account. - Returns: - Pubkey of the derived fee config account + This account stores fee-related configuration for the AMM. """ derived_address, _ = Pubkey.find_program_address( [b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)], @@ -185,48 +218,152 @@ def find_fee_config() -> Pubkey: return derived_address +# ============================================================================ +# Mayhem Mode Fee Handling +# ============================================================================ +# Mayhem mode is a special fee structure where fees go to a different recipient. +# The fee recipient changes dynamically based on the pool's mayhem_mode flag. + + +async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey: + """Fetch the mayhem mode fee recipient from GlobalConfig. + + When mayhem mode is active, fees are redirected to a special recipient + stored in the GlobalConfig account. + + Args: + client: Solana RPC client + + Returns: + Public key of the mayhem mode fee recipient + """ + response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64") + if not response.value or not response.value.data: + msg = "Cannot fetch GlobalConfig account" + raise ValueError(msg) + + data = response.value.data + recipient_bytes = data[ + GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32 + ] + return Pubkey.from_bytes(recipient_bytes) + + +async def get_pumpswap_fee_recipients( + client: AsyncClient, pool: Pubkey +) -> tuple[Pubkey, Pubkey]: + """Determine the correct fee recipient based on pool's mayhem mode status. + + This function checks if mayhem mode is enabled for the pool and returns + the appropriate fee recipient and their WSOL token account. + + Args: + client: Solana RPC client + pool: Address of the AMM pool + + Returns: + Tuple of (fee_recipient_pubkey, fee_recipient_token_account) + """ + response = await client.get_account_info(pool, encoding="base64") + if not response.value or not response.value.data: + msg = "Cannot fetch pool account" + raise ValueError(msg) + + pool_data = response.value.data + + # Check if mayhem mode flag exists and is enabled + is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool( + pool_data[POOL_MAYHEM_MODE_OFFSET] + ) + + # Select appropriate fee recipient + if is_mayhem_mode: + fee_recipient = await get_reserved_fee_recipient_pumpswap(client) + else: + fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT + + # Get the fee recipient's WSOL token account + fee_recipient_token_account = get_associated_token_address( + fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM + ) + + return (fee_recipient, fee_recipient_token_account) + + +# ============================================================================ +# Price Calculation +# ============================================================================ + + async def calculate_token_pool_price( client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey, ) -> float: - """Calculate the price of tokens in the pool. + """Calculate current token price from AMM pool balances. - Fetches the balance of tokens in the pool and calculates the price ratio. + AMM price is determined by the ratio of tokens in the pool: + price = quote_balance / base_balance Args: - client: Solana RPC client instance - pool_base_token_account: Address of the pool's base token account (your token) - pool_quote_token_account: Address of the pool's quote token account (SOL) + client: Solana RPC client + pool_base_token_account: Pool's token account (the token being priced) + pool_quote_token_account: Pool's SOL account (the quote currency) Returns: - The price of the base token in terms of the quote token (usually SOL) + Price in SOL per token """ base_balance_resp = await client.get_token_account_balance(pool_base_token_account) quote_balance_resp = await client.get_token_account_balance( pool_quote_token_account ) - # Extract the UI amounts (human-readable with decimals) base_amount = float(base_balance_resp.value.ui_amount) quote_amount = float(quote_balance_resp.value.ui_amount) - token_price = quote_amount / base_amount + return quote_amount / base_amount + + +# ============================================================================ +# Token Program Determination +# ============================================================================ + + +async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey: + """Determines if a mint uses TokenProgram or Token2022Program.""" + mint_info = await client.get_account_info(mint_address) + + if not mint_info.value: + raise ValueError(f"Could not fetch mint info for {mint_address}") + + owner = mint_info.value.owner + + if owner == SYSTEM_TOKEN_PROGRAM: + return SYSTEM_TOKEN_PROGRAM + elif owner == TOKEN_2022_PROGRAM: + return TOKEN_2022_PROGRAM + else: + raise ValueError( + f"Mint account {mint_address} is owned by an unknown program: {owner}" + ) + - return token_price +# ============================================================================ +# Associated Token Account (ATA) Creation +# ============================================================================ def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction: - """Create an instruction to create an Associated Token Account (ATA) if it doesn't exist. + """Create instruction to initialize a WSOL ATA if it doesn't exist. - This creates an instruction that will create an Associated Token Account for SOL - if it doesn't already exist. + Idempotent means this instruction won't fail if the ATA already exists. + See: https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs Args: - payer_pubkey: The public key of the account that will pay for the creation + payer_pubkey: Account that will pay for ATA creation Returns: - An instruction to create the ATA + Instruction to create the ATA """ associated_token_address = get_associated_token_address(payer_pubkey, SOL) @@ -239,20 +376,23 @@ def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction: AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), ] - # The data for creating an ATA idempotently is just a single byte with value 1 - # Check the details here: - # https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs - data = bytes([1]) + # Instruction data: single byte with value 1 = CreateIdempotent return Instruction( - SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts + SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, bytes([1]), instruction_accounts ) +# ============================================================================ +# Token Selling +# ============================================================================ + + async def sell_pump_swap( client: AsyncClient, - pump_fun_amm_market: Pubkey, + market: Pubkey, payer: Keypair, base_mint: Pubkey, + token_program_id: Pubkey, user_base_token_account: Pubkey, user_quote_token_account: Pubkey, pool_base_token_account: Pubkey, @@ -261,54 +401,61 @@ async def sell_pump_swap( coin_creator_vault_ata: Pubkey, slippage: float = 0.25, ) -> str | None: - """Sell tokens on the PUMP AMM. + """Execute a token sell on the PUMP AMM with slippage protection. - This function sells all tokens in the user's token account with the specified slippage tolerance. + This function: + 1. Fetches current token balance and pool price + 2. Calculates minimum SOL output with slippage tolerance + 3. Constructs and sends the sell transaction Args: - client: Solana RPC client instance - pump_fun_amm_market: Address of the AMM market - payer: Keypair of the transaction signer and token seller - base_mint: Address of the token mint being sold - user_base_token_account: Address of the user's token account for the token being sold - user_quote_token_account: Address of the user's SOL token account - pool_base_token_account: Address of the pool's token account for the token being sold - pool_quote_token_account: Address of the pool's SOL token account - coin_creator_vault_authority: Address of the coin creator's vault authority - coin_creator_vault_ata: Address of the coin creator's associated token account for fees - slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%) + client: Solana RPC client + market: AMM pool address + payer: Wallet keypair for signing + base_mint: Token mint address + user_base_token_account: User's token account + user_quote_token_account: User's WSOL account + pool_base_token_account: Pool's token account + pool_quote_token_account: Pool's WSOL account + coin_creator_vault_authority: Creator vault PDA + coin_creator_vault_ata: Creator's WSOL account + slippage: Maximum acceptable slippage (0.25 = 25%) Returns: Transaction signature if successful, None otherwise """ - # Get token balance token_balance = int( (await client.get_token_account_balance(user_base_token_account)).value.amount ) token_balance_decimal = token_balance / 10**TOKEN_DECIMALS + print(f"Token balance: {token_balance_decimal}") + if token_balance == 0: print("No tokens to sell.") return None - # Calculate token price token_price_sol = await calculate_token_pool_price( client, pool_base_token_account, pool_quote_token_account ) print(f"Price per Token: {token_price_sol:.20f} SOL") - # Calculate minimum SOL output with slippage protection - amount = token_balance - min_sol_output = float(token_balance_decimal) * float(token_price_sol) - slippage_factor = 1 - slippage - min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL) + # Calculate minimum SOL we're willing to receive (slippage protection) + expected_sol_output = token_balance_decimal * token_price_sol + min_sol_output = int((expected_sol_output * (1 - slippage)) * LAMPORTS_PER_SOL) print(f"Selling {token_balance_decimal} tokens") print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") - # Define all accounts needed for the sell instruction + # Get fee recipient based on mayhem mode + fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients( + client, market + ) + + # Build account list for sell instruction + # Order matters! Must match the program's expected account layout accounts = [ - AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True), + AccountMeta(pubkey=market, is_signer=False, is_writable=True), AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False), AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False), @@ -317,15 +464,13 @@ async def sell_pump_swap( AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True), AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True), AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False), AccountMeta( - pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False + pubkey=fee_recipient_token_account, is_signer=False, is_writable=True ), AccountMeta( - pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT, - is_signer=False, - is_writable=True, - ), - AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + pubkey=token_program_id, is_signer=False, is_writable=False + ), # Use dynamic token_program_id AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta( @@ -341,76 +486,88 @@ async def sell_pump_swap( AccountMeta( pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False ), - # Index 19: fee_config (readonly) AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False), - # Index 20: fee_program (readonly) AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False), ] + # Instruction data format: discriminator (8 bytes) + amount (8 bytes) + min_out (8 bytes) + # All integers are little-endian (<) data = ( SELL_DISCRIMINATOR - + struct.pack(" None: + """Execute the complete sell flow.""" async with AsyncClient(RPC_ENDPOINT) as client: + # Step 1: Find the pool address for our token market_address = await get_market_address_by_base_mint( client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID ) + + # Step 2: Parse pool data to get necessary accounts market_data = await get_market_data(client, market_address) + + # Determine token program ID for the base mint + token_program_id = await get_token_program_id(client, TOKEN_MINT) + + # Step 3: Derive PDAs needed for the transaction coin_creator_vault_authority = find_coin_creator_vault( Pubkey.from_string(market_data["coin_creator"]) ) coin_creator_vault_ata = get_associated_token_address( - coin_creator_vault_authority, SOL + coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM ) + # Step 4: Execute the sell await sell_pump_swap( client, market_address, PAYER, TOKEN_MINT, - get_associated_token_address(PAYER.pubkey(), TOKEN_MINT), - get_associated_token_address(PAYER.pubkey(), SOL), + token_program_id, + get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id), + get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM), Pubkey.from_string(market_data["pool_base_token_account"]), Pubkey.from_string(market_data["pool_quote_token_account"]), coin_creator_vault_authority, diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py index ca6290ad..0da3f17a 100644 --- a/src/cleanup/manager.py +++ b/src/cleanup/manager.py @@ -34,12 +34,19 @@ def __init__( self.use_priority_fee = use_priority_fee self.close_with_force_burn = force_burn - async def cleanup_ata(self, mint: Pubkey) -> None: + async def cleanup_ata(self, mint: Pubkey, token_program_id: Pubkey | None = None) -> None: """ Attempt to burn any remaining tokens and close the ATA. Skips if account doesn't exist or is already empty/closed. + + Args: + mint: Token mint address + token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM """ - ata = self.wallet.get_associated_token_address(mint) + if token_program_id is None: + token_program_id = SystemAddresses.TOKEN_2022_PROGRAM + + ata = self.wallet.get_associated_token_address(mint, token_program_id) solana_client = await self.client.get_client() priority_fee = ( @@ -70,7 +77,7 @@ async def cleanup_ata(self, mint: Pubkey) -> None: mint=mint, owner=self.wallet.pubkey, amount=balance, - program_id=SystemAddresses.TOKEN_PROGRAM, + program_id=token_program_id, ) ) instructions.append(burn_ix) @@ -89,7 +96,7 @@ async def cleanup_ata(self, mint: Pubkey) -> None: account=ata, dest=self.wallet.pubkey, owner=self.wallet.pubkey, - program_id=SystemAddresses.TOKEN_PROGRAM, + program_id=token_program_id, ) ) instructions.append(close_ix) diff --git a/src/cleanup/modes.py b/src/cleanup/modes.py index c9732d77..bf25c8da 100644 --- a/src/cleanup/modes.py +++ b/src/cleanup/modes.py @@ -20,6 +20,7 @@ async def handle_cleanup_after_failure( client, wallet, mint, + token_program_id, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, @@ -30,13 +31,14 @@ async def handle_cleanup_after_failure( manager = AccountCleanupManager( client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn ) - await manager.cleanup_ata(mint) + await manager.cleanup_ata(mint, token_program_id) async def handle_cleanup_after_sell( client, wallet, mint, + token_program_id, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, @@ -47,13 +49,14 @@ async def handle_cleanup_after_sell( manager = AccountCleanupManager( client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn ) - await manager.cleanup_ata(mint) + await manager.cleanup_ata(mint, token_program_id) async def handle_cleanup_post_session( client, wallet, mints, + token_program_ids, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, @@ -64,5 +67,5 @@ async def handle_cleanup_post_session( manager = AccountCleanupManager( client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn ) - for mint in mints: - await manager.cleanup_ata(mint) + for mint, token_program_id in zip(mints, token_program_ids): + await manager.cleanup_ata(mint, token_program_id) diff --git a/src/core/pubkeys.py b/src/core/pubkeys.py index c6a8697b..ad7182ef 100644 --- a/src/core/pubkeys.py +++ b/src/core/pubkeys.py @@ -23,6 +23,9 @@ TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ) +TOKEN_2022_PROGRAM: Final[Pubkey] = Pubkey.from_string( + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" +) ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" ) @@ -42,6 +45,7 @@ class SystemAddresses: # Reference the module-level constants SYSTEM_PROGRAM = SYSTEM_PROGRAM TOKEN_PROGRAM = TOKEN_PROGRAM + TOKEN_2022_PROGRAM = TOKEN_2022_PROGRAM ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM RENT = RENT SOL_MINT = SOL_MINT @@ -56,6 +60,7 @@ def get_all_system_addresses(cls) -> dict[str, Pubkey]: return { "system_program": cls.SYSTEM_PROGRAM, "token_program": cls.TOKEN_PROGRAM, + "token_2022_program": cls.TOKEN_2022_PROGRAM, "associated_token_program": cls.ASSOCIATED_TOKEN_PROGRAM, "rent": cls.RENT, "sol_mint": cls.SOL_MINT, diff --git a/src/core/wallet.py b/src/core/wallet.py index 18c59566..0e9c9097 100644 --- a/src/core/wallet.py +++ b/src/core/wallet.py @@ -7,6 +7,8 @@ from solders.pubkey import Pubkey from spl.token.instructions import get_associated_token_address +from core.pubkeys import SystemAddresses + class Wallet: """Manages a Solana wallet for trading operations.""" @@ -30,16 +32,21 @@ def keypair(self) -> Keypair: """Get the keypair for signing transactions.""" return self._keypair - def get_associated_token_address(self, mint: Pubkey) -> Pubkey: + def get_associated_token_address( + self, mint: Pubkey, token_program_id: Pubkey | None = None + ) -> Pubkey: """Get the associated token account address for a mint. Args: mint: Token mint address + token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM Returns: Associated token account address """ - return get_associated_token_address(self.pubkey, mint) + if token_program_id is None: + token_program_id = SystemAddresses.TOKEN_2022_PROGRAM + return get_associated_token_address(self.pubkey, mint, token_program_id) @staticmethod def _load_keypair(private_key: str) -> Keypair: diff --git a/src/interfaces/core.py b/src/interfaces/core.py index 04ecff50..c7c1b912 100644 --- a/src/interfaces/core.py +++ b/src/interfaces/core.py @@ -45,6 +45,8 @@ class TokenInfo: user: Pubkey | None = None creator: Pubkey | None = None creator_vault: Pubkey | None = None + token_program_id: Pubkey | None = None # Token or Token2022 program + is_mayhem_mode: bool = False # pump.fun mayhem mode flag # Metadata creation_timestamp: float | None = None diff --git a/src/platforms/letsbonk/address_provider.py b/src/platforms/letsbonk/address_provider.py index a654b980..3bbad21f 100644 --- a/src/platforms/letsbonk/address_provider.py +++ b/src/platforms/letsbonk/address_provider.py @@ -147,17 +147,22 @@ def derive_quote_vault( ) return quote_vault - def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey: + def derive_user_token_account( + self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None + ) -> Pubkey: """Derive user's associated token account address. Args: user: User's wallet address mint: Token mint address + token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM Returns: User's associated token account address """ - return get_associated_token_address(user, mint) + if token_program_id is None: + token_program_id = SystemAddresses.TOKEN_2022_PROGRAM + return get_associated_token_address(user, mint, token_program_id) def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]: """Get LetsBonk-specific additional accounts needed for trading. @@ -296,6 +301,13 @@ def get_buy_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) + # Determine token program to use + token_program_id = ( + token_info.token_program_id + if token_info.token_program_id + else SystemAddresses.TOKEN_2022_PROGRAM + ) + # Use global_config from TokenInfo if available, otherwise use default global_config = ( token_info.global_config @@ -316,12 +328,12 @@ def get_buy_instruction_accounts( "global_config": global_config, "platform_config": platform_config, "pool_state": additional_accounts["pool_state"], - "user_base_token": self.derive_user_token_account(user, token_info.mint), + "user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id), "base_vault": additional_accounts["base_vault"], "quote_vault": additional_accounts["quote_vault"], "base_token_mint": token_info.mint, "quote_token_mint": SystemAddresses.SOL_MINT, - "base_token_program": SystemAddresses.TOKEN_PROGRAM, + "base_token_program": token_program_id, "quote_token_program": SystemAddresses.TOKEN_PROGRAM, "event_authority": additional_accounts["event_authority"], "program": LetsBonkAddresses.PROGRAM, @@ -351,6 +363,13 @@ def get_sell_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) + # Determine token program to use + token_program_id = ( + token_info.token_program_id + if token_info.token_program_id + else SystemAddresses.TOKEN_2022_PROGRAM + ) + # Use global_config from TokenInfo if available, otherwise use default global_config = ( token_info.global_config @@ -371,12 +390,12 @@ def get_sell_instruction_accounts( "global_config": global_config, "platform_config": platform_config, "pool_state": additional_accounts["pool_state"], - "user_base_token": self.derive_user_token_account(user, token_info.mint), + "user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id), "base_vault": additional_accounts["base_vault"], "quote_vault": additional_accounts["quote_vault"], "base_token_mint": token_info.mint, "quote_token_mint": SystemAddresses.SOL_MINT, - "base_token_program": SystemAddresses.TOKEN_PROGRAM, + "base_token_program": token_program_id, "quote_token_program": SystemAddresses.TOKEN_PROGRAM, "event_authority": additional_accounts["event_authority"], "program": LetsBonkAddresses.PROGRAM, diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py index 1c5eccdb..99c94e3c 100644 --- a/src/platforms/letsbonk/curve_manager.py +++ b/src/platforms/letsbonk/curve_manager.py @@ -194,14 +194,21 @@ def _decode_pool_state_with_idl(self, data: bytes) -> dict[str, Any]: } # Calculate additional metrics - if pool_data["virtual_base"] > 0: - pool_data["price_per_token"] = ( - (pool_data["virtual_quote"] / pool_data["virtual_base"]) - * (10**TOKEN_DECIMALS) - / LAMPORTS_PER_SOL + # Validate reserves are positive before calculating price + if pool_data["virtual_base"] <= 0: + raise ValueError( + f"Invalid virtual_base: {pool_data['virtual_base']} - cannot calculate price" ) - else: - pool_data["price_per_token"] = 0 + if pool_data["virtual_quote"] <= 0: + raise ValueError( + f"Invalid virtual_quote: {pool_data['virtual_quote']} - cannot calculate price" + ) + + pool_data["price_per_token"] = ( + (pool_data["virtual_quote"] / pool_data["virtual_base"]) + * (10**TOKEN_DECIMALS) + / LAMPORTS_PER_SOL + ) logger.debug( f"Decoded pool state: virtual_base={pool_data['virtual_base']}, " diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index 035b3912..58293f63 100644 --- a/src/platforms/letsbonk/event_parser.py +++ b/src/platforms/letsbonk/event_parser.py @@ -13,6 +13,7 @@ from solders.pubkey import Pubkey from solders.transaction import VersionedTransaction +from core.pubkeys import SystemAddresses from interfaces.core import EventParser, Platform, TokenInfo from platforms.letsbonk.address_provider import LetsBonkAddressProvider from utils.idl_parser import IDLParser @@ -114,6 +115,15 @@ def get_account_key(index): }: return None + # Determine token program based on instruction variant + instruction_name = decoded["instruction_name"] + is_token_2022 = instruction_name == "initialize_with_token_2022" + token_program_id = ( + SystemAddresses.TOKEN_2022_PROGRAM + if is_token_2022 + else SystemAddresses.TOKEN_PROGRAM + ) + args = decoded.get("args", {}) # Extract MintParams from the decoded arguments @@ -174,6 +184,7 @@ def get_account_key(index): platform_config=platform_config, user=creator, creator=creator, + token_program_id=token_program_id, creation_timestamp=monotonic(), ) diff --git a/src/platforms/letsbonk/instruction_builder.py b/src/platforms/letsbonk/instruction_builder.py index b47a8331..9f71c5f5 100644 --- a/src/platforms/letsbonk/instruction_builder.py +++ b/src/platforms/letsbonk/instruction_builder.py @@ -75,12 +75,19 @@ async def build_buy_instruction( # Get all required accounts accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) + # Determine token program to use + token_program_id = ( + token_info.token_program_id + if token_info.token_program_id + else SystemAddresses.TOKEN_2022_PROGRAM + ) + # 1. Create idempotent ATA for base token ata_instruction = create_idempotent_associated_token_account( user, # payer user, # owner token_info.mint, # mint - SystemAddresses.TOKEN_PROGRAM, # token program + token_program_id, # token program (dynamic for token2022 support) ) instructions.append(ata_instruction) @@ -151,10 +158,10 @@ async def build_buy_instruction( pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False ), # quote_token_mint AccountMeta( - pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False ), # base_token_program AccountMeta( - pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False ), # quote_token_program AccountMeta( pubkey=accounts_info["event_authority"], @@ -306,10 +313,10 @@ async def build_sell_instruction( pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False ), # quote_token_mint AccountMeta( - pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False ), # base_token_program AccountMeta( - pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False ), # quote_token_program AccountMeta( pubkey=accounts_info["event_authority"], diff --git a/src/platforms/pumpfun/address_provider.py b/src/platforms/pumpfun/address_provider.py index 771023b7..cb412cfd 100644 --- a/src/platforms/pumpfun/address_provider.py +++ b/src/platforms/pumpfun/address_provider.py @@ -31,6 +31,12 @@ class PumpFunAddresses: FEE: Final[Pubkey] = Pubkey.from_string( "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" ) + # Mayhem mode fee recipient (hardcoded to avoid RPC calls) + # To check if this address is up-to-date, fetch Global account data at offset 483 + # from the pump.fun Global account: 4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf + MAYHEM_FEE: Final[Pubkey] = Pubkey.from_string( + "GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS" + ) LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string( "39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg" ) @@ -139,17 +145,22 @@ def derive_pool_address( ) return bonding_curve - def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey: + def derive_user_token_account( + self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None + ) -> Pubkey: """Derive user's associated token account address. Args: user: User's wallet address mint: Token mint address + token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM Returns: User's associated token account address """ - return get_associated_token_address(user, mint) + if token_program_id is None: + token_program_id = SystemAddresses.TOKEN_2022_PROGRAM + return get_associated_token_address(user, mint, token_program_id) def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]: """Get pump.fun-specific additional accounts needed for trading. @@ -177,7 +188,7 @@ def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]: # Derive associated bonding curve if not provided if not token_info.associated_bonding_curve and token_info.bonding_curve: accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve( - token_info.mint, token_info.bonding_curve + token_info.mint, token_info.bonding_curve, token_info.token_program_id ) # Derive creator vault if not provided but creator is available @@ -187,21 +198,25 @@ def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]: return accounts def derive_associated_bonding_curve( - self, mint: Pubkey, bonding_curve: Pubkey + self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None ) -> Pubkey: """Derive the associated bonding curve (ATA of bonding curve for the token). Args: mint: Token mint address bonding_curve: Bonding curve address + token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM Returns: Associated bonding curve address """ + if token_program_id is None: + token_program_id = SystemAddresses.TOKEN_2022_PROGRAM + derived_address, _ = Pubkey.find_program_address( [ bytes(bonding_curve), - bytes(SystemAddresses.TOKEN_PROGRAM), + bytes(token_program_id), bytes(mint), ], SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, @@ -249,6 +264,19 @@ def derive_fee_config(self) -> Pubkey: """ return PumpFunAddresses.find_fee_config() + def get_fee_recipient(self, token_info: TokenInfo) -> Pubkey: + """Get the correct fee recipient based on mayhem mode. + + Args: + token_info: Token information with is_mayhem_mode flag + + Returns: + Fee recipient address (mayhem or standard) + """ + if token_info.is_mayhem_mode: + return PumpFunAddresses.MAYHEM_FEE + return PumpFunAddresses.FEE + def get_buy_instruction_accounts( self, token_info: TokenInfo, user: Pubkey ) -> dict[str, Pubkey]: @@ -263,9 +291,19 @@ def get_buy_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) + # Determine token program to use + token_program_id = ( + token_info.token_program_id + if token_info.token_program_id + else SystemAddresses.TOKEN_PROGRAM + ) + + # Determine fee recipient based on mayhem mode + fee_recipient = self.get_fee_recipient(token_info) + return { "global": PumpFunAddresses.GLOBAL, - "fee": PumpFunAddresses.FEE, + "fee": fee_recipient, "mint": token_info.mint, "bonding_curve": additional_accounts.get( "bonding_curve", token_info.bonding_curve @@ -273,10 +311,12 @@ def get_buy_instruction_accounts( "associated_bonding_curve": additional_accounts.get( "associated_bonding_curve", token_info.associated_bonding_curve ), - "user_token_account": self.derive_user_token_account(user, token_info.mint), + "user_token_account": self.derive_user_token_account( + user, token_info.mint, token_program_id + ), "user": user, "system_program": SystemAddresses.SYSTEM_PROGRAM, - "token_program": SystemAddresses.TOKEN_PROGRAM, + "token_program": token_program_id, "creator_vault": additional_accounts.get( "creator_vault", token_info.creator_vault ), @@ -302,9 +342,19 @@ def get_sell_instruction_accounts( """ additional_accounts = self.get_additional_accounts(token_info) + # Determine token program to use + token_program_id = ( + token_info.token_program_id + if token_info.token_program_id + else SystemAddresses.TOKEN_PROGRAM + ) + + # Determine fee recipient based on mayhem mode + fee_recipient = self.get_fee_recipient(token_info) + return { "global": PumpFunAddresses.GLOBAL, - "fee": PumpFunAddresses.FEE, + "fee": fee_recipient, "mint": token_info.mint, "bonding_curve": additional_accounts.get( "bonding_curve", token_info.bonding_curve @@ -312,13 +362,15 @@ def get_sell_instruction_accounts( "associated_bonding_curve": additional_accounts.get( "associated_bonding_curve", token_info.associated_bonding_curve ), - "user_token_account": self.derive_user_token_account(user, token_info.mint), + "user_token_account": self.derive_user_token_account( + user, token_info.mint, token_program_id + ), "user": user, "system_program": SystemAddresses.SYSTEM_PROGRAM, "creator_vault": additional_accounts.get( "creator_vault", token_info.creator_vault ), - "token_program": SystemAddresses.TOKEN_PROGRAM, + "token_program": token_program_id, "event_authority": PumpFunAddresses.EVENT_AUTHORITY, "program": PumpFunAddresses.PROGRAM, "fee_config": self.derive_fee_config(), diff --git a/src/platforms/pumpfun/curve_manager.py b/src/platforms/pumpfun/curve_manager.py index 7d98011c..3fd1ed00 100644 --- a/src/platforms/pumpfun/curve_manager.py +++ b/src/platforms/pumpfun/curve_manager.py @@ -190,20 +190,28 @@ def _decode_curve_state_with_idl(self, data: bytes) -> dict[str, Any]: "token_total_supply": decoded_curve_state.get("token_total_supply", 0), "complete": decoded_curve_state.get("complete", False), "creator": decoded_curve_state.get("creator", ""), + "is_mayhem_mode": decoded_curve_state.get("is_mayhem_mode", False), } # Calculate additional metrics - if curve_data["virtual_token_reserves"] > 0: - curve_data["price_per_token"] = ( - ( - curve_data["virtual_sol_reserves"] - / curve_data["virtual_token_reserves"] - ) - * (10**TOKEN_DECIMALS) - / LAMPORTS_PER_SOL + # Validate reserves are positive before calculating price + if curve_data["virtual_token_reserves"] <= 0: + raise ValueError( + f"Invalid virtual_token_reserves: {curve_data['virtual_token_reserves']} - cannot calculate price" ) - else: - curve_data["price_per_token"] = 0 + if curve_data["virtual_sol_reserves"] <= 0: + raise ValueError( + f"Invalid virtual_sol_reserves: {curve_data['virtual_sol_reserves']} - cannot calculate price" + ) + + curve_data["price_per_token"] = ( + ( + curve_data["virtual_sol_reserves"] + / curve_data["virtual_token_reserves"] + ) + * (10**TOKEN_DECIMALS) + / LAMPORTS_PER_SOL + ) # Add convenience decimal fields curve_data["token_reserves_decimal"] = ( diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index d12b3b73..e1a45ed5 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -47,6 +47,16 @@ def __init__(self, idl_parser: IDLParser): " Platform: @@ -74,8 +88,12 @@ def parse_token_creation_from_logs( Returns: TokenInfo if token creation found, None otherwise """ - # Check if this is a token creation transaction - if not any("Program log: Instruction: Create" in log for log in logs): + # Check if this is a token creation transaction (create or create_v2 for token2022) + if not any( + "Program log: Instruction: Create" in log + or "Program log: Instruction: Create_v2" in log + for log in logs + ): return None # Skip swaps as the first condition may pass them @@ -92,9 +110,10 @@ def parse_token_creation_from_logs( # First, collect all Program data entries and note when Create instruction happens for i, log in enumerate(logs): - if "Program log: Instruction: Create" in log: + if "Program log: Instruction: Create" in log or "Program log: Instruction: Create_v2" in log: create_instruction_found = True - logger.info(f"📝 Found Create instruction at log index {i}") + instruction_type = "Create_v2" if "Create_v2" in log else "Create" + logger.info(f"📝 Found {instruction_type} instruction at log index {i}") elif "Program data:" in log: # Extract base64 encoded event data encoded_data = log.split("Program data: ")[1].strip() @@ -104,7 +123,7 @@ def parse_token_creation_from_logs( ) if not create_instruction_found: - logger.info("❌ No Create instruction found in logs") + logger.info("❌ No Create or Create_v2 instruction found in logs") return None if not program_data_entries: @@ -224,9 +243,13 @@ def parse_token_creation_from_logs( logger.info(f"❌ Failed to convert pubkey fields: {e}") continue - # Derive additional addresses + # Derive additional addresses (default to TOKEN_2022_PROGRAM as per pump.fun's migration to create_v2) + # Note: As of recent pump.fun updates, all tokens are created via create_v2 instruction + # This is a technical limitation of logs listener - cannot distinguish create vs create_v2 + # Risk is low since pump.fun now defaults to Token2022 for all new tokens + token_program_id = SystemAddresses.TOKEN_2022_PROGRAM associated_bonding_curve = self._derive_associated_bonding_curve( - mint, bonding_curve + mint, bonding_curve, token_program_id ) creator_vault = self._derive_creator_vault(creator) @@ -245,6 +268,7 @@ def parse_token_creation_from_logs( user=user, creator=creator, creator_vault=creator_vault, + token_program_id=token_program_id, creation_timestamp=monotonic(), ) @@ -274,9 +298,16 @@ def parse_token_creation_from_instruction( Returns: TokenInfo if token creation found, None otherwise """ - if not instruction_data.startswith( - self._create_instruction_discriminator_bytes + # Determine which create instruction (standard or v2 for token2022) + is_create_v2 = False + if instruction_data.startswith(self._create_instruction_discriminator_bytes): + is_create_v2 = False + elif ( + self._create_v2_instruction_discriminator_bytes + and instruction_data.startswith(self._create_v2_instruction_discriminator_bytes) ): + is_create_v2 = True + else: return None try: @@ -293,7 +324,8 @@ def get_account_key(index): decoded = self._idl_parser.decode_instruction( instruction_data, account_keys, accounts ) - if not decoded or decoded["instruction_name"] != "create": + expected_instruction_name = "create_v2" if is_create_v2 else "create" + if not decoded or decoded["instruction_name"] != expected_instruction_name: return None args = decoded.get("args", {}) @@ -315,6 +347,13 @@ def get_account_key(index): ) creator_vault = self._derive_creator_vault(creator) + # Determine token program based on instruction type + token_program_id = ( + SystemAddresses.TOKEN_2022_PROGRAM + if is_create_v2 + else SystemAddresses.TOKEN_PROGRAM + ) + return TokenInfo( name=args.get("name", ""), symbol=args.get("symbol", ""), @@ -326,6 +365,7 @@ def get_account_key(index): user=user, creator=creator, creator_vault=creator_vault, + token_program_id=token_program_id, creation_timestamp=monotonic(), ) @@ -435,14 +475,19 @@ def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None: ix_data = bytes(ix.data) - # Check for create discriminator + # Check for create or create_v2 discriminator if len(ix_data) >= 8: discriminator = struct.unpack(" TokenInfo | None: if len(ix_data) >= 8: discriminator = struct.unpack(" Pubkey: return derived_address def _derive_associated_bonding_curve( - self, mint: Pubkey, bonding_curve: Pubkey + self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None ) -> Pubkey: """Derive the associated bonding curve (ATA of bonding curve for the token). Args: mint: Token mint address bonding_curve: Bonding curve address + token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_PROGRAM Returns: Associated bonding curve address """ + if token_program_id is None: + token_program_id = SystemAddresses.TOKEN_PROGRAM + derived_address, _ = Pubkey.find_program_address( [ bytes(bonding_curve), - bytes(SystemAddresses.TOKEN_PROGRAM), + bytes(token_program_id), bytes(mint), ], SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, ) return derived_address + def _parse_bonding_curve_state(self, data: bytes) -> dict[str, Any] | None: + """Parse bonding curve state from raw account data using IDL parser. + + Args: + data: Raw bonding curve account data + + Returns: + Dictionary with parsed bonding curve state or None if parsing fails + """ + try: + decoded = self._idl_parser.decode_account_data( + data, "BondingCurve", skip_discriminator=True + ) + if not decoded: + return None + return decoded + except Exception as e: + logger.debug(f"Failed to parse bonding curve state: {e}") + return None + + def _get_is_mayhem_mode_from_curve(self, bonding_curve_address: Pubkey) -> bool: + """Determine if a token is in mayhem mode based on bonding curve state. + + Note: This would require an RPC call to fetch the bonding curve account. + For now, we return False as a default since the event parser doesn't have + access to an RPC client. The mayhem mode flag will be set by traders + when they fetch bonding curve state for other operations. + + Args: + bonding_curve_address: Address of the bonding curve + + Returns: + True if mayhem mode, False otherwise (or if parsing fails) + """ + # Since event parser doesn't have RPC client access, we cannot fetch + # and parse bonding curve state here. Mayhem mode will be set later + # when traders fetch the bonding curve state. + return False + @property def verbose(self) -> bool: """Check if verbose logging is enabled.""" diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py index 1237b0fb..306e2dbe 100644 --- a/src/platforms/pumpfun/instruction_builder.py +++ b/src/platforms/pumpfun/instruction_builder.py @@ -64,15 +64,16 @@ async def build_buy_instruction( """ instructions = [] - # Get all required accounts + # Get all required accounts (includes mayhem-mode-aware fee recipient) accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) # 1. Create idempotent ATA instruction (won't fail if ATA already exists) + # Use token_program from accounts_info to ensure AddressProvider controls program selection ata_instruction = create_idempotent_associated_token_account( user, # payer user, # owner token_info.mint, # mint - SystemAddresses.TOKEN_PROGRAM, # token program + accounts_info["token_program"], # token program from AddressProvider ) instructions.append(ata_instruction) @@ -123,7 +124,7 @@ async def build_buy_instruction( AccountMeta( pubkey=accounts_info["global_volume_accumulator"], is_signer=False, - is_writable=True, + is_writable=False, ), AccountMeta( pubkey=accounts_info["user_volume_accumulator"], @@ -144,11 +145,14 @@ async def build_buy_instruction( ), ] - # Build instruction data: discriminator + token_amount + max_sol_cost + # Build instruction data: discriminator + token_amount + max_sol_cost + track_volume + # Encode OptionBool for track_volume: [1, 1] = Some(true) + track_volume_bytes = bytes([1, 1]) instruction_data = ( self._buy_discriminator + struct.pack(" TokenInfo | None: creator = user # Derive additional addresses using platform provider + # PumpPortal doesn't distinguish between Token and Token2022. + # Default to TOKEN_2022_PROGRAM as per pump.fun's migration to create_v2. + # Technical limitation: Cannot distinguish from pre-parsed data, but risk is low + # since pump.fun now defaults to Token2022 for all new tokens. + token_program_id = SystemAddresses.TOKEN_2022_PROGRAM + associated_bonding_curve = ( self.address_provider.derive_associated_bonding_curve( - mint, bonding_curve + mint, bonding_curve, token_program_id ) ) creator_vault = self.address_provider.derive_creator_vault(creator) @@ -99,6 +106,7 @@ def process_token_data(self, token_data: dict) -> TokenInfo | None: user=user, creator=creator, creator_vault=creator_vault, + token_program_id=token_program_id, ) except Exception: diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py index 9aaca51f..576b19fd 100644 --- a/src/trading/platform_aware.py +++ b/src/trading/platform_aware.py @@ -66,10 +66,20 @@ async def execute(self, token_info: TokenInfo) -> TradeResult: pool_address = self._get_pool_address(token_info, address_provider) # Regular behavior with RPC call - token_price_sol = await curve_manager.calculate_price(pool_address) - token_amount = ( - self.amount / token_price_sol if token_price_sol > 0 else 0 - ) + # Fetch pool state to get price and mayhem mode status + pool_state = await curve_manager.get_pool_state(pool_address) + token_price_sol = pool_state.get("price_per_token") + + # Validate price_per_token is present and positive + if token_price_sol is None or token_price_sol <= 0: + raise ValueError( + f"Invalid price_per_token: {token_price_sol} for pool {pool_address} " + f"(mint: {token_info.mint}) - cannot execute buy with zero/invalid price" + ) + + # Set is_mayhem_mode from bonding curve state + token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False) + token_amount = self.amount / token_price_sol # Calculate minimum token amount with slippage minimum_token_amount = token_amount * (1 - self.slippage) @@ -225,7 +235,19 @@ async def execute(self, token_info: TokenInfo) -> TradeResult: # Get pool address and current price using platform-agnostic method pool_address = self._get_pool_address(token_info, address_provider) - token_price_sol = await curve_manager.calculate_price(pool_address) + # Fetch pool state to get price and mayhem mode status + pool_state = await curve_manager.get_pool_state(pool_address) + token_price_sol = pool_state.get("price_per_token") + + # Validate price_per_token is present and positive + if token_price_sol is None or token_price_sol <= 0: + raise ValueError( + f"Invalid price_per_token: {token_price_sol} for pool {pool_address} " + f"(mint: {token_info.mint}) - cannot execute sell with zero/invalid price" + ) + + # Set is_mayhem_mode from bonding curve state + token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False) logger.info(f"Price per Token: {token_price_sol:.8f} SOL") diff --git a/src/trading/universal_trader.py b/src/trading/universal_trader.py index c2a3a80c..e4e7a204 100644 --- a/src/trading/universal_trader.py +++ b/src/trading/universal_trader.py @@ -193,6 +193,7 @@ def __init__( # State tracking self.traded_mints: set[Pubkey] = set() + self.traded_token_programs: dict[str, Pubkey] = {} # Maps mint (as string) to token_program_id self.token_queue: asyncio.Queue = asyncio.Queue() self.processing: bool = False self.processed_tokens: set[str] = set() @@ -325,10 +326,17 @@ async def _cleanup_resources(self) -> None: if self.traded_mints: try: logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...") + # Build parallel lists of mints and token_program_ids + mints_list = list(self.traded_mints) + token_program_ids = [ + self.traded_token_programs.get(str(mint)) + for mint in mints_list + ] await handle_cleanup_post_session( self.solana_client, self.wallet, - list(self.traded_mints), + mints_list, + token_program_ids, self.priority_fee_manager, self.cleanup_mode, self.cleanup_with_priority_fee, @@ -447,6 +455,10 @@ async def _handle_successful_buy( buy_result.tx_signature, ) self.traded_mints.add(token_info.mint) + # Track token program for cleanup + mint_str = str(token_info.mint) + if token_info.token_program_id: + self.traded_token_programs[mint_str] = token_info.token_program_id # Choose exit strategy if not self.marry_mode: @@ -469,6 +481,7 @@ async def _handle_failed_buy( self.solana_client, self.wallet, token_info.mint, + token_info.token_program_id, self.priority_fee_manager, self.cleanup_mode, self.cleanup_with_priority_fee, @@ -521,6 +534,7 @@ async def _handle_time_based_exit(self, token_info: TokenInfo) -> None: self.solana_client, self.wallet, token_info.mint, + token_info.token_program_id, self.priority_fee_manager, self.cleanup_mode, self.cleanup_with_priority_fee, @@ -590,6 +604,7 @@ async def _monitor_position_until_exit( self.solana_client, self.wallet, token_info.mint, + token_info.token_program_id, self.priority_fee_manager, self.cleanup_mode, self.cleanup_with_priority_fee,