pumpfun-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pumpfun/__init__.py +53 -0
- pumpfun/bonding_curve.py +364 -0
- pumpfun/constants.py +43 -0
- pumpfun/pda.py +65 -0
- pumpfun/pumpswap.py +262 -0
- pumpfun/py.typed +0 -0
- pumpfun_python-0.1.0.dist-info/METADATA +208 -0
- pumpfun_python-0.1.0.dist-info/RECORD +10 -0
- pumpfun_python-0.1.0.dist-info/WHEEL +4 -0
- pumpfun_python-0.1.0.dist-info/licenses/LICENSE +21 -0
pumpfun/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pumpfun-python — buy and sell on PumpFun bonding curves + PumpSwap AMM.
|
|
3
|
+
Directly from Python. No Jupiter needed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .bonding_curve import (
|
|
7
|
+
BondingCurveState,
|
|
8
|
+
build_buy_instruction,
|
|
9
|
+
build_sell_instruction,
|
|
10
|
+
calculate_buy_amount,
|
|
11
|
+
calculate_sell_amount,
|
|
12
|
+
fetch_bonding_curve_state,
|
|
13
|
+
)
|
|
14
|
+
from .constants import (
|
|
15
|
+
PUMP_AMM_PROGRAM,
|
|
16
|
+
PUMP_FUN_PROGRAM,
|
|
17
|
+
PUMP_SWAP_PROGRAM,
|
|
18
|
+
)
|
|
19
|
+
from .pda import (
|
|
20
|
+
get_associated_token_address,
|
|
21
|
+
get_bonding_curve_pda,
|
|
22
|
+
)
|
|
23
|
+
from .pumpswap import (
|
|
24
|
+
PoolState,
|
|
25
|
+
build_swap_instruction,
|
|
26
|
+
calculate_swap_output,
|
|
27
|
+
fetch_pool_state,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Bonding curve (pre-graduation)
|
|
34
|
+
"BondingCurveState",
|
|
35
|
+
"build_buy_instruction",
|
|
36
|
+
"build_sell_instruction",
|
|
37
|
+
"calculate_buy_amount",
|
|
38
|
+
"calculate_sell_amount",
|
|
39
|
+
"fetch_bonding_curve_state",
|
|
40
|
+
# PumpSwap AMM (post-graduation)
|
|
41
|
+
"PoolState",
|
|
42
|
+
"build_swap_instruction",
|
|
43
|
+
"calculate_swap_output",
|
|
44
|
+
"fetch_pool_state",
|
|
45
|
+
# PDA helpers
|
|
46
|
+
"get_associated_token_address",
|
|
47
|
+
"get_bonding_curve_pda",
|
|
48
|
+
# Program IDs
|
|
49
|
+
"PUMP_FUN_PROGRAM",
|
|
50
|
+
"PUMP_SWAP_PROGRAM",
|
|
51
|
+
"PUMP_AMM_PROGRAM",
|
|
52
|
+
"__version__",
|
|
53
|
+
]
|
pumpfun/bonding_curve.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PumpFun bonding curve v2 — buy/sell instructions and on-chain state reading.
|
|
3
|
+
|
|
4
|
+
Program: 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
|
|
5
|
+
|
|
6
|
+
BUY: 16 accounts
|
|
7
|
+
SELL: 14 accounts (creator_vault and token_program are SWAPPED vs buy)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import struct
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from solders.instruction import AccountMeta, Instruction # type: ignore[import-untyped]
|
|
18
|
+
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
19
|
+
|
|
20
|
+
from .constants import (
|
|
21
|
+
PUMP_BUY_DISCRIMINATOR,
|
|
22
|
+
PUMP_FEE_PROGRAM,
|
|
23
|
+
PUMP_FUN_EVENT_AUTHORITY,
|
|
24
|
+
PUMP_FUN_GLOBAL,
|
|
25
|
+
PUMP_FUN_PROGRAM,
|
|
26
|
+
PUMP_SELL_DISCRIMINATOR,
|
|
27
|
+
SYSTEM_PROGRAM,
|
|
28
|
+
TOKEN_PROGRAM,
|
|
29
|
+
)
|
|
30
|
+
from .pda import (
|
|
31
|
+
get_associated_token_address,
|
|
32
|
+
get_bonding_curve_pda,
|
|
33
|
+
get_bonding_curve_v2_pda,
|
|
34
|
+
get_creator_vault_pda,
|
|
35
|
+
get_fee_config_pda,
|
|
36
|
+
get_global_volume_accumulator_pda,
|
|
37
|
+
get_user_volume_accumulator_pda,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PumpFunError(Exception):
|
|
42
|
+
"""Raised when a PumpFun operation fails."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True)
|
|
46
|
+
class BondingCurveState:
|
|
47
|
+
"""Parsed on-chain bonding curve account state."""
|
|
48
|
+
|
|
49
|
+
virtual_token_reserves: int
|
|
50
|
+
virtual_sol_reserves: int
|
|
51
|
+
real_token_reserves: int
|
|
52
|
+
real_sol_reserves: int
|
|
53
|
+
token_total_supply: int
|
|
54
|
+
complete: bool
|
|
55
|
+
creator: Pubkey
|
|
56
|
+
is_mayhem_mode: bool = False
|
|
57
|
+
is_cashback_coin: bool = False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Price calculations (pure functions — no RPC needed)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def calculate_buy_amount(
|
|
66
|
+
sol_amount_lamports: int,
|
|
67
|
+
virtual_sol_reserves: int,
|
|
68
|
+
virtual_token_reserves: int,
|
|
69
|
+
) -> int:
|
|
70
|
+
"""
|
|
71
|
+
Calculate tokens received for a given SOL input.
|
|
72
|
+
|
|
73
|
+
Uses PumpFun's constant-product formula with 1% fee on SOL input.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
sol_amount_lamports: SOL amount in lamports.
|
|
77
|
+
virtual_sol_reserves: Virtual SOL reserves from bonding curve state.
|
|
78
|
+
virtual_token_reserves: Virtual token reserves from bonding curve state.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Number of tokens received.
|
|
82
|
+
"""
|
|
83
|
+
fee = sol_amount_lamports // 100
|
|
84
|
+
sol_after_fee = sol_amount_lamports - fee
|
|
85
|
+
tokens_out = (virtual_token_reserves * sol_after_fee) // (virtual_sol_reserves + sol_after_fee)
|
|
86
|
+
return tokens_out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def calculate_sell_amount(
|
|
90
|
+
token_amount: int,
|
|
91
|
+
virtual_sol_reserves: int,
|
|
92
|
+
virtual_token_reserves: int,
|
|
93
|
+
) -> int:
|
|
94
|
+
"""
|
|
95
|
+
Calculate SOL received for selling tokens.
|
|
96
|
+
|
|
97
|
+
Uses PumpFun's constant-product formula with 1% fee on SOL output.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
token_amount: Number of tokens to sell.
|
|
101
|
+
virtual_sol_reserves: Virtual SOL reserves from bonding curve state.
|
|
102
|
+
virtual_token_reserves: Virtual token reserves from bonding curve state.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
SOL output in lamports (after fee).
|
|
106
|
+
"""
|
|
107
|
+
if virtual_token_reserves + token_amount == 0:
|
|
108
|
+
return 0
|
|
109
|
+
sol_out_raw = (virtual_sol_reserves * token_amount) // (virtual_token_reserves + token_amount)
|
|
110
|
+
fee = sol_out_raw // 100
|
|
111
|
+
return sol_out_raw - fee
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# On-chain state reading
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def fetch_bonding_curve_state(
|
|
120
|
+
rpc_url: str,
|
|
121
|
+
token_mint: str | Pubkey,
|
|
122
|
+
*,
|
|
123
|
+
http_client: httpx.AsyncClient | None = None,
|
|
124
|
+
) -> BondingCurveState:
|
|
125
|
+
"""
|
|
126
|
+
Fetch the PumpFun bonding curve state from on-chain.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
rpc_url: Solana RPC endpoint URL.
|
|
130
|
+
token_mint: Token mint address (string or Pubkey).
|
|
131
|
+
http_client: Optional httpx client (creates one if not provided).
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
BondingCurveState with reserves, creator, completion status.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
PumpFunError: If the account can't be read or parsed.
|
|
138
|
+
"""
|
|
139
|
+
if isinstance(token_mint, str):
|
|
140
|
+
token_mint = Pubkey.from_string(token_mint)
|
|
141
|
+
|
|
142
|
+
bonding_curve = get_bonding_curve_pda(token_mint)
|
|
143
|
+
|
|
144
|
+
should_close = http_client is None
|
|
145
|
+
client = http_client or httpx.AsyncClient(timeout=httpx.Timeout(15.0))
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
resp = await client.post(rpc_url, json={
|
|
149
|
+
"jsonrpc": "2.0",
|
|
150
|
+
"id": 1,
|
|
151
|
+
"method": "getAccountInfo",
|
|
152
|
+
"params": [str(bonding_curve), {"encoding": "base64"}],
|
|
153
|
+
})
|
|
154
|
+
resp.raise_for_status()
|
|
155
|
+
data = resp.json()
|
|
156
|
+
finally:
|
|
157
|
+
if should_close:
|
|
158
|
+
await client.aclose()
|
|
159
|
+
|
|
160
|
+
result = data.get("result", {})
|
|
161
|
+
value = result.get("value")
|
|
162
|
+
if not value:
|
|
163
|
+
raise PumpFunError(f"Bonding curve account {bonding_curve} not found")
|
|
164
|
+
|
|
165
|
+
raw_data = value.get("data", [])
|
|
166
|
+
if not isinstance(raw_data, list) or not raw_data:
|
|
167
|
+
raise PumpFunError("Cannot decode bonding curve account data")
|
|
168
|
+
|
|
169
|
+
account_bytes = base64.b64decode(raw_data[0])
|
|
170
|
+
|
|
171
|
+
if len(account_bytes) < 81:
|
|
172
|
+
raise PumpFunError(f"Bonding curve data too short: {len(account_bytes)} bytes")
|
|
173
|
+
|
|
174
|
+
off = 8 # skip Anchor discriminator
|
|
175
|
+
virtual_token_reserves = struct.unpack_from("<Q", account_bytes, off)[0]; off += 8
|
|
176
|
+
virtual_sol_reserves = struct.unpack_from("<Q", account_bytes, off)[0]; off += 8
|
|
177
|
+
real_token_reserves = struct.unpack_from("<Q", account_bytes, off)[0]; off += 8
|
|
178
|
+
real_sol_reserves = struct.unpack_from("<Q", account_bytes, off)[0]; off += 8
|
|
179
|
+
token_total_supply = struct.unpack_from("<Q", account_bytes, off)[0]; off += 8
|
|
180
|
+
complete = account_bytes[off] != 0; off += 1
|
|
181
|
+
creator = Pubkey.from_bytes(account_bytes[off: off + 32]); off += 32
|
|
182
|
+
|
|
183
|
+
is_mayhem_mode = len(account_bytes) >= 82 and account_bytes[81] != 0
|
|
184
|
+
is_cashback_coin = len(account_bytes) >= 83 and account_bytes[82] != 0
|
|
185
|
+
|
|
186
|
+
return BondingCurveState(
|
|
187
|
+
virtual_token_reserves=virtual_token_reserves,
|
|
188
|
+
virtual_sol_reserves=virtual_sol_reserves,
|
|
189
|
+
real_token_reserves=real_token_reserves,
|
|
190
|
+
real_sol_reserves=real_sol_reserves,
|
|
191
|
+
token_total_supply=token_total_supply,
|
|
192
|
+
complete=complete,
|
|
193
|
+
creator=creator,
|
|
194
|
+
is_mayhem_mode=is_mayhem_mode,
|
|
195
|
+
is_cashback_coin=is_cashback_coin,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def fetch_fee_recipient(
|
|
200
|
+
rpc_url: str,
|
|
201
|
+
*,
|
|
202
|
+
http_client: httpx.AsyncClient | None = None,
|
|
203
|
+
) -> Pubkey:
|
|
204
|
+
"""
|
|
205
|
+
Read the fee_recipient from the PumpFun Global account.
|
|
206
|
+
|
|
207
|
+
Falls back to default on failure.
|
|
208
|
+
"""
|
|
209
|
+
default = Pubkey.from_string("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV")
|
|
210
|
+
|
|
211
|
+
should_close = http_client is None
|
|
212
|
+
client = http_client or httpx.AsyncClient(timeout=httpx.Timeout(15.0))
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
resp = await client.post(rpc_url, json={
|
|
216
|
+
"jsonrpc": "2.0",
|
|
217
|
+
"id": 1,
|
|
218
|
+
"method": "getAccountInfo",
|
|
219
|
+
"params": [str(PUMP_FUN_GLOBAL), {"encoding": "base64"}],
|
|
220
|
+
})
|
|
221
|
+
resp.raise_for_status()
|
|
222
|
+
data = resp.json()
|
|
223
|
+
value = data.get("result", {}).get("value")
|
|
224
|
+
if value:
|
|
225
|
+
raw = value.get("data", [])
|
|
226
|
+
if isinstance(raw, list) and raw:
|
|
227
|
+
buf = base64.b64decode(raw[0])
|
|
228
|
+
if len(buf) >= 73:
|
|
229
|
+
return Pubkey.from_bytes(buf[41:73])
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
finally:
|
|
233
|
+
if should_close:
|
|
234
|
+
await client.aclose()
|
|
235
|
+
|
|
236
|
+
return default
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# Instruction builders (pure functions)
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def build_buy_instruction(
|
|
245
|
+
user: Pubkey,
|
|
246
|
+
token_mint: Pubkey,
|
|
247
|
+
bonding_curve: Pubkey,
|
|
248
|
+
sol_amount_lamports: int,
|
|
249
|
+
min_tokens_out: int,
|
|
250
|
+
creator: Pubkey,
|
|
251
|
+
fee_recipient: Pubkey,
|
|
252
|
+
token_program: Pubkey = TOKEN_PROGRAM,
|
|
253
|
+
) -> Instruction:
|
|
254
|
+
"""
|
|
255
|
+
Build the PumpFun v2 BUY instruction (16 accounts).
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
user: Buyer's wallet (signer).
|
|
259
|
+
token_mint: Token mint address.
|
|
260
|
+
bonding_curve: Bonding curve PDA (from get_bonding_curve_pda).
|
|
261
|
+
sol_amount_lamports: Max SOL to spend (lamports).
|
|
262
|
+
min_tokens_out: Minimum tokens expected (slippage protection).
|
|
263
|
+
creator: Token creator from bonding curve state.
|
|
264
|
+
fee_recipient: Fee recipient (from fetch_fee_recipient or default).
|
|
265
|
+
token_program: TOKEN_PROGRAM or TOKEN_2022_PROGRAM.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Solders Instruction ready to add to a transaction.
|
|
269
|
+
"""
|
|
270
|
+
associated_user = get_associated_token_address(user, token_mint, token_program)
|
|
271
|
+
associated_bonding_curve = get_associated_token_address(bonding_curve, token_mint, token_program)
|
|
272
|
+
creator_vault = get_creator_vault_pda(creator)
|
|
273
|
+
global_vol = get_global_volume_accumulator_pda()
|
|
274
|
+
user_vol = get_user_volume_accumulator_pda(user)
|
|
275
|
+
fee_config = get_fee_config_pda()
|
|
276
|
+
|
|
277
|
+
data = (
|
|
278
|
+
PUMP_BUY_DISCRIMINATOR
|
|
279
|
+
+ struct.pack("<Q", min_tokens_out)
|
|
280
|
+
+ struct.pack("<Q", sol_amount_lamports)
|
|
281
|
+
+ b"\x01" # track_volume = Some(true)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
accounts = [
|
|
285
|
+
AccountMeta(PUMP_FUN_GLOBAL, is_signer=False, is_writable=False),
|
|
286
|
+
AccountMeta(fee_recipient, is_signer=False, is_writable=True),
|
|
287
|
+
AccountMeta(token_mint, is_signer=False, is_writable=False),
|
|
288
|
+
AccountMeta(bonding_curve, is_signer=False, is_writable=True),
|
|
289
|
+
AccountMeta(associated_bonding_curve, is_signer=False, is_writable=True),
|
|
290
|
+
AccountMeta(associated_user, is_signer=False, is_writable=True),
|
|
291
|
+
AccountMeta(user, is_signer=True, is_writable=True),
|
|
292
|
+
AccountMeta(SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
|
293
|
+
AccountMeta(token_program, is_signer=False, is_writable=False),
|
|
294
|
+
AccountMeta(creator_vault, is_signer=False, is_writable=True),
|
|
295
|
+
AccountMeta(PUMP_FUN_EVENT_AUTHORITY, is_signer=False, is_writable=False),
|
|
296
|
+
AccountMeta(PUMP_FUN_PROGRAM, is_signer=False, is_writable=False),
|
|
297
|
+
AccountMeta(global_vol, is_signer=False, is_writable=True),
|
|
298
|
+
AccountMeta(user_vol, is_signer=False, is_writable=True),
|
|
299
|
+
AccountMeta(fee_config, is_signer=False, is_writable=False),
|
|
300
|
+
AccountMeta(PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
|
301
|
+
AccountMeta(get_bonding_curve_v2_pda(token_mint), is_signer=False, is_writable=False),
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
return Instruction(PUMP_FUN_PROGRAM, data, accounts)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def build_sell_instruction(
|
|
308
|
+
user: Pubkey,
|
|
309
|
+
token_mint: Pubkey,
|
|
310
|
+
bonding_curve: Pubkey,
|
|
311
|
+
token_amount: int,
|
|
312
|
+
min_sol_output: int,
|
|
313
|
+
creator: Pubkey,
|
|
314
|
+
fee_recipient: Pubkey,
|
|
315
|
+
token_program: Pubkey = TOKEN_PROGRAM,
|
|
316
|
+
) -> Instruction:
|
|
317
|
+
"""
|
|
318
|
+
Build the PumpFun v2 SELL instruction (14 accounts).
|
|
319
|
+
|
|
320
|
+
NOTE: creator_vault [8] and token_program [9] are SWAPPED vs buy!
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
user: Seller's wallet (signer).
|
|
324
|
+
token_mint: Token mint address.
|
|
325
|
+
bonding_curve: Bonding curve PDA.
|
|
326
|
+
token_amount: Number of tokens to sell.
|
|
327
|
+
min_sol_output: Minimum SOL output (lamports, slippage protection).
|
|
328
|
+
creator: Token creator from bonding curve state.
|
|
329
|
+
fee_recipient: Fee recipient.
|
|
330
|
+
token_program: TOKEN_PROGRAM or TOKEN_2022_PROGRAM.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Solders Instruction.
|
|
334
|
+
"""
|
|
335
|
+
associated_user = get_associated_token_address(user, token_mint, token_program)
|
|
336
|
+
associated_bonding_curve = get_associated_token_address(bonding_curve, token_mint, token_program)
|
|
337
|
+
creator_vault = get_creator_vault_pda(creator)
|
|
338
|
+
fee_config = get_fee_config_pda()
|
|
339
|
+
|
|
340
|
+
data = (
|
|
341
|
+
PUMP_SELL_DISCRIMINATOR
|
|
342
|
+
+ struct.pack("<Q", token_amount)
|
|
343
|
+
+ struct.pack("<Q", min_sol_output)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
accounts = [
|
|
347
|
+
AccountMeta(PUMP_FUN_GLOBAL, is_signer=False, is_writable=False),
|
|
348
|
+
AccountMeta(fee_recipient, is_signer=False, is_writable=True),
|
|
349
|
+
AccountMeta(token_mint, is_signer=False, is_writable=False),
|
|
350
|
+
AccountMeta(bonding_curve, is_signer=False, is_writable=True),
|
|
351
|
+
AccountMeta(associated_bonding_curve, is_signer=False, is_writable=True),
|
|
352
|
+
AccountMeta(associated_user, is_signer=False, is_writable=True),
|
|
353
|
+
AccountMeta(user, is_signer=True, is_writable=True),
|
|
354
|
+
AccountMeta(SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
|
355
|
+
AccountMeta(creator_vault, is_signer=False, is_writable=True), # [8] SWAPPED
|
|
356
|
+
AccountMeta(token_program, is_signer=False, is_writable=False), # [9] SWAPPED
|
|
357
|
+
AccountMeta(PUMP_FUN_EVENT_AUTHORITY, is_signer=False, is_writable=False),
|
|
358
|
+
AccountMeta(PUMP_FUN_PROGRAM, is_signer=False, is_writable=False),
|
|
359
|
+
AccountMeta(fee_config, is_signer=False, is_writable=False),
|
|
360
|
+
AccountMeta(PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
|
361
|
+
AccountMeta(get_bonding_curve_v2_pda(token_mint), is_signer=False, is_writable=False),
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
return Instruction(PUMP_FUN_PROGRAM, data, accounts)
|
pumpfun/constants.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solana program IDs and instruction discriminators for PumpFun and PumpSwap.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
8
|
+
|
|
9
|
+
# ── PumpFun bonding curve program ──────────────────────────────────────────
|
|
10
|
+
PUMP_FUN_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
|
11
|
+
PUMP_FUN_GLOBAL = Pubkey.from_string("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf")
|
|
12
|
+
PUMP_FUN_EVENT_AUTHORITY = Pubkey.from_string("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1")
|
|
13
|
+
|
|
14
|
+
# ── PumpSwap AMM — legacy program ─────────────────────────────────────────
|
|
15
|
+
PUMP_SWAP_PROGRAM = Pubkey.from_string("PSwapMdSai8tjrEXcxFeQth87xC4rRsa4VA5mhGhXkP")
|
|
16
|
+
|
|
17
|
+
# ── Pump AMM — current production program (pAMMBay) ───────────────────────
|
|
18
|
+
PUMP_AMM_PROGRAM = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
|
19
|
+
PUMP_AMM_GLOBAL_CONFIG = Pubkey.from_string("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw")
|
|
20
|
+
PUMP_AMM_EVENT_AUTHORITY = Pubkey.from_string("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR")
|
|
21
|
+
|
|
22
|
+
# ── Pump Fee program ──────────────────────────────────────────────────────
|
|
23
|
+
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
|
24
|
+
|
|
25
|
+
# ── PumpSwap fee accounts ─────────────────────────────────────────────────
|
|
26
|
+
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string("ADyA8hdefbpth3kCbkbEVuNyGPfbmSYdoMjMCuKLxMJo")
|
|
27
|
+
PUMP_SWAP_FEE_RECIPIENT = Pubkey.from_string("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV")
|
|
28
|
+
PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string("GS4CU59F31iL7aR2Q8xmZRbmMfXdK4cqeLGSBDmkXSWm")
|
|
29
|
+
|
|
30
|
+
# ── System / Token Programs ───────────────────────────────────────────────
|
|
31
|
+
TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
|
32
|
+
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
|
33
|
+
ASSOCIATED_TOKEN_PROGRAM = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
|
|
34
|
+
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
|
35
|
+
SOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
|
36
|
+
|
|
37
|
+
LAMPORTS_PER_SOL = 1_000_000_000
|
|
38
|
+
|
|
39
|
+
# ── Instruction discriminators ─────────────────────────────────────────────
|
|
40
|
+
# Anchor convention: sha256("global:<instruction_name>")[:8]
|
|
41
|
+
PUMP_BUY_DISCRIMINATOR = bytes([102, 6, 61, 18, 1, 218, 235, 234])
|
|
42
|
+
PUMP_SELL_DISCRIMINATOR = bytes([51, 230, 133, 164, 1, 127, 131, 173])
|
|
43
|
+
PUMPSWAP_SWAP_DISCRIMINATOR = hashlib.sha256(b"global:swap").digest()[:8]
|
pumpfun/pda.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PDA derivation helpers for PumpFun and PumpSwap.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
8
|
+
|
|
9
|
+
from .constants import (
|
|
10
|
+
ASSOCIATED_TOKEN_PROGRAM,
|
|
11
|
+
PUMP_FEE_PROGRAM,
|
|
12
|
+
PUMP_FUN_PROGRAM,
|
|
13
|
+
TOKEN_PROGRAM,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _find_pda(seeds: list[bytes], program_id: Pubkey) -> Pubkey:
|
|
18
|
+
"""Derive a Program Derived Address."""
|
|
19
|
+
pubkey, _bump = Pubkey.find_program_address(seeds, program_id)
|
|
20
|
+
return pubkey
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_bonding_curve_pda(token_mint: Pubkey) -> Pubkey:
|
|
24
|
+
"""Derive the PumpFun bonding curve PDA for a token mint."""
|
|
25
|
+
return _find_pda([b"bonding-curve", bytes(token_mint)], PUMP_FUN_PROGRAM)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_associated_token_address(
|
|
29
|
+
owner: Pubkey,
|
|
30
|
+
mint: Pubkey,
|
|
31
|
+
token_program: Pubkey = TOKEN_PROGRAM,
|
|
32
|
+
) -> Pubkey:
|
|
33
|
+
"""Derive the Associated Token Account (ATA) address."""
|
|
34
|
+
return _find_pda(
|
|
35
|
+
[bytes(owner), bytes(token_program), bytes(mint)],
|
|
36
|
+
ASSOCIATED_TOKEN_PROGRAM,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_creator_vault_pda(creator: Pubkey) -> Pubkey:
|
|
41
|
+
"""PDA["creator-vault", creator] on the PumpFun program."""
|
|
42
|
+
return _find_pda([b"creator-vault", bytes(creator)], PUMP_FUN_PROGRAM)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_bonding_curve_v2_pda(token_mint: Pubkey) -> Pubkey:
|
|
46
|
+
"""PDA["bonding-curve-v2", mint] — required remaining account for v2."""
|
|
47
|
+
return _find_pda([b"bonding-curve-v2", bytes(token_mint)], PUMP_FUN_PROGRAM)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_global_volume_accumulator_pda() -> Pubkey:
|
|
51
|
+
"""PDA["global_volume_accumulator"] on the PumpFun program."""
|
|
52
|
+
return _find_pda([b"global_volume_accumulator"], PUMP_FUN_PROGRAM)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_user_volume_accumulator_pda(user: Pubkey) -> Pubkey:
|
|
56
|
+
"""PDA["user_volume_accumulator", user] on the PumpFun program."""
|
|
57
|
+
return _find_pda([b"user_volume_accumulator", bytes(user)], PUMP_FUN_PROGRAM)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_fee_config_pda() -> Pubkey:
|
|
61
|
+
"""PDA["fee_config", pump_program] on the fee program."""
|
|
62
|
+
return _find_pda(
|
|
63
|
+
[b"fee_config", bytes(PUMP_FUN_PROGRAM)],
|
|
64
|
+
PUMP_FEE_PROGRAM,
|
|
65
|
+
)
|
pumpfun/pumpswap.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PumpSwap AMM — constant-product pool swap instructions and on-chain state.
|
|
3
|
+
|
|
4
|
+
Supports both legacy PumpSwap (PSwapMdSai...) and Pump AMM (pAMMBay...) pools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import struct
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from solders.instruction import AccountMeta, Instruction # type: ignore[import-untyped]
|
|
15
|
+
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
16
|
+
|
|
17
|
+
from .constants import (
|
|
18
|
+
PUMP_AMM_PROGRAM,
|
|
19
|
+
PUMP_SWAP_EVENT_AUTHORITY,
|
|
20
|
+
PUMP_SWAP_FEE_RECIPIENT,
|
|
21
|
+
PUMP_SWAP_GLOBAL_CONFIG,
|
|
22
|
+
PUMP_SWAP_PROGRAM,
|
|
23
|
+
PUMPSWAP_SWAP_DISCRIMINATOR,
|
|
24
|
+
SOL_MINT,
|
|
25
|
+
TOKEN_PROGRAM,
|
|
26
|
+
)
|
|
27
|
+
from .pda import get_associated_token_address
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PumpSwapError(Exception):
|
|
31
|
+
"""Raised when a PumpSwap operation fails."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(slots=True)
|
|
35
|
+
class PoolState:
|
|
36
|
+
"""Parsed on-chain PumpSwap/Pump AMM pool state."""
|
|
37
|
+
|
|
38
|
+
pool_bump: int
|
|
39
|
+
index: int
|
|
40
|
+
creator: Pubkey
|
|
41
|
+
coin_creator: Pubkey
|
|
42
|
+
base_mint: Pubkey
|
|
43
|
+
quote_mint: Pubkey
|
|
44
|
+
lp_mint: Pubkey
|
|
45
|
+
pool_base_token_account: Pubkey
|
|
46
|
+
pool_quote_token_account: Pubkey
|
|
47
|
+
lp_fee_basis_points: int
|
|
48
|
+
protocol_fee_basis_points: int
|
|
49
|
+
is_pump_amm: bool = False
|
|
50
|
+
base_is_sol: bool = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Swap math (pure functions — no RPC needed)
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def calculate_swap_output(
|
|
59
|
+
amount_in: int,
|
|
60
|
+
reserve_in: int,
|
|
61
|
+
reserve_out: int,
|
|
62
|
+
lp_fee_bps: int,
|
|
63
|
+
protocol_fee_bps: int,
|
|
64
|
+
) -> tuple[int, int]:
|
|
65
|
+
"""
|
|
66
|
+
Calculate output for a constant-product AMM swap.
|
|
67
|
+
|
|
68
|
+
Fee is deducted from the input amount before computing swap.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
amount_in: Input token amount.
|
|
72
|
+
reserve_in: Reserve of the input token in the pool.
|
|
73
|
+
reserve_out: Reserve of the output token in the pool.
|
|
74
|
+
lp_fee_bps: LP fee in basis points.
|
|
75
|
+
protocol_fee_bps: Protocol fee in basis points.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Tuple of (amount_out, total_fee).
|
|
79
|
+
"""
|
|
80
|
+
total_fee_bps = lp_fee_bps + protocol_fee_bps
|
|
81
|
+
fee = (amount_in * total_fee_bps) // 10_000
|
|
82
|
+
amount_in_after_fee = amount_in - fee
|
|
83
|
+
|
|
84
|
+
if reserve_in + amount_in_after_fee == 0:
|
|
85
|
+
return 0, fee
|
|
86
|
+
|
|
87
|
+
amount_out = (reserve_out * amount_in_after_fee) // (reserve_in + amount_in_after_fee)
|
|
88
|
+
return amount_out, fee
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# On-chain state reading
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def fetch_pool_state(
|
|
97
|
+
rpc_url: str,
|
|
98
|
+
pool_address: str,
|
|
99
|
+
*,
|
|
100
|
+
http_client: httpx.AsyncClient | None = None,
|
|
101
|
+
) -> PoolState:
|
|
102
|
+
"""
|
|
103
|
+
Read a PumpSwap/Pump AMM pool account from on-chain.
|
|
104
|
+
|
|
105
|
+
Layout (after 8-byte Anchor discriminator):
|
|
106
|
+
- offset 8: pool_bump (u8)
|
|
107
|
+
- offset 9: index (u16 LE)
|
|
108
|
+
- offset 11: creator (Pubkey, 32B)
|
|
109
|
+
- offset 43: base_mint (Pubkey, 32B)
|
|
110
|
+
- offset 75: quote_mint (Pubkey, 32B)
|
|
111
|
+
- offset 107: lp_mint (Pubkey, 32B)
|
|
112
|
+
- offset 139: pool_base_token_account (Pubkey, 32B)
|
|
113
|
+
- offset 171: pool_quote_token_account (Pubkey, 32B)
|
|
114
|
+
- offset 203: lp_fee_basis_points (u64 LE) — legacy only
|
|
115
|
+
- offset 211: protocol_fee_basis_points (u64 LE) — legacy only
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
rpc_url: Solana RPC endpoint URL.
|
|
119
|
+
pool_address: Pool account address.
|
|
120
|
+
http_client: Optional httpx client.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
PoolState with reserves, mints, and vault addresses.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
PumpSwapError: If the account can't be read or parsed.
|
|
127
|
+
"""
|
|
128
|
+
should_close = http_client is None
|
|
129
|
+
client = http_client or httpx.AsyncClient(timeout=httpx.Timeout(15.0))
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
resp = await client.post(rpc_url, json={
|
|
133
|
+
"jsonrpc": "2.0",
|
|
134
|
+
"id": 1,
|
|
135
|
+
"method": "getAccountInfo",
|
|
136
|
+
"params": [pool_address, {"encoding": "base64"}],
|
|
137
|
+
})
|
|
138
|
+
resp.raise_for_status()
|
|
139
|
+
data = resp.json()
|
|
140
|
+
finally:
|
|
141
|
+
if should_close:
|
|
142
|
+
await client.aclose()
|
|
143
|
+
|
|
144
|
+
result = data.get("result", {})
|
|
145
|
+
value = result.get("value")
|
|
146
|
+
if not value:
|
|
147
|
+
raise PumpSwapError(f"Pool account {pool_address} not found")
|
|
148
|
+
|
|
149
|
+
raw_data = value.get("data", [])
|
|
150
|
+
if not isinstance(raw_data, list) or not raw_data:
|
|
151
|
+
raise PumpSwapError("Cannot decode pool account data")
|
|
152
|
+
|
|
153
|
+
account_bytes = base64.b64decode(raw_data[0])
|
|
154
|
+
if len(account_bytes) < 219:
|
|
155
|
+
raise PumpSwapError(f"Pool data too short: {len(account_bytes)} bytes (need >= 219)")
|
|
156
|
+
|
|
157
|
+
owner = value.get("owner", "")
|
|
158
|
+
valid_owners = {str(PUMP_SWAP_PROGRAM), str(PUMP_AMM_PROGRAM)}
|
|
159
|
+
if owner and owner not in valid_owners:
|
|
160
|
+
raise PumpSwapError(f"Account owned by {owner}, not PumpSwap/PumpAMM program")
|
|
161
|
+
|
|
162
|
+
is_pump_amm = owner == str(PUMP_AMM_PROGRAM)
|
|
163
|
+
|
|
164
|
+
def _pubkey_at(offset: int) -> Pubkey:
|
|
165
|
+
return Pubkey.from_bytes(account_bytes[offset: offset + 32])
|
|
166
|
+
|
|
167
|
+
off = 8
|
|
168
|
+
pool_bump = account_bytes[off]; off += 1
|
|
169
|
+
index = struct.unpack_from("<H", account_bytes, off)[0]; off += 2
|
|
170
|
+
creator = _pubkey_at(off); off += 32
|
|
171
|
+
base_mint = _pubkey_at(off); off += 32
|
|
172
|
+
quote_mint = _pubkey_at(off); off += 32
|
|
173
|
+
lp_mint = _pubkey_at(off); off += 32
|
|
174
|
+
pool_base_token_account = _pubkey_at(off); off += 32
|
|
175
|
+
pool_quote_token_account = _pubkey_at(off); off += 32
|
|
176
|
+
|
|
177
|
+
if is_pump_amm:
|
|
178
|
+
lp_fee_basis_points = 200
|
|
179
|
+
protocol_fee_basis_points = 100
|
|
180
|
+
coin_creator = Pubkey.default()
|
|
181
|
+
if len(account_bytes) >= 243:
|
|
182
|
+
coin_creator = _pubkey_at(211)
|
|
183
|
+
else:
|
|
184
|
+
lp_fee_basis_points = struct.unpack_from("<Q", account_bytes, off)[0]; off += 8
|
|
185
|
+
protocol_fee_basis_points = struct.unpack_from("<Q", account_bytes, off)[0]; off += 8
|
|
186
|
+
coin_creator = creator
|
|
187
|
+
|
|
188
|
+
base_is_sol = is_pump_amm and str(base_mint) == str(SOL_MINT)
|
|
189
|
+
|
|
190
|
+
return PoolState(
|
|
191
|
+
pool_bump=pool_bump,
|
|
192
|
+
index=index,
|
|
193
|
+
creator=creator,
|
|
194
|
+
coin_creator=coin_creator,
|
|
195
|
+
base_mint=base_mint,
|
|
196
|
+
quote_mint=quote_mint,
|
|
197
|
+
lp_mint=lp_mint,
|
|
198
|
+
pool_base_token_account=pool_base_token_account,
|
|
199
|
+
pool_quote_token_account=pool_quote_token_account,
|
|
200
|
+
lp_fee_basis_points=lp_fee_basis_points,
|
|
201
|
+
protocol_fee_basis_points=protocol_fee_basis_points,
|
|
202
|
+
is_pump_amm=is_pump_amm,
|
|
203
|
+
base_is_sol=base_is_sol,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Instruction builder
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_swap_instruction(
|
|
213
|
+
pool: Pubkey,
|
|
214
|
+
user: Pubkey,
|
|
215
|
+
user_base_token_account: Pubkey,
|
|
216
|
+
user_quote_token_account: Pubkey,
|
|
217
|
+
pool_base_token_account: Pubkey,
|
|
218
|
+
pool_quote_token_account: Pubkey,
|
|
219
|
+
protocol_fee_token_account: Pubkey,
|
|
220
|
+
base_in: bool,
|
|
221
|
+
amount_in: int,
|
|
222
|
+
min_amount_out: int,
|
|
223
|
+
base_token_program: Pubkey = TOKEN_PROGRAM,
|
|
224
|
+
) -> Instruction:
|
|
225
|
+
"""
|
|
226
|
+
Build the PumpSwap swap instruction.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
pool: Pool account address.
|
|
230
|
+
user: Trader's wallet (signer).
|
|
231
|
+
user_base_token_account: User's ATA for the base (meme) token.
|
|
232
|
+
user_quote_token_account: User's ATA for the quote token (WSOL).
|
|
233
|
+
pool_base_token_account: Pool's base token vault.
|
|
234
|
+
pool_quote_token_account: Pool's quote token vault.
|
|
235
|
+
protocol_fee_token_account: Protocol fee recipient's quote ATA.
|
|
236
|
+
base_in: True = sell base for quote, False = buy base with quote.
|
|
237
|
+
amount_in: Input amount (base units / lamports).
|
|
238
|
+
min_amount_out: Minimum output (slippage protection).
|
|
239
|
+
base_token_program: Token program for base token (SPL or Token-2022).
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Solders Instruction ready to add to a transaction.
|
|
243
|
+
"""
|
|
244
|
+
data = PUMPSWAP_SWAP_DISCRIMINATOR + struct.pack("<BQQ", int(base_in), amount_in, min_amount_out)
|
|
245
|
+
|
|
246
|
+
accounts = [
|
|
247
|
+
AccountMeta(PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
|
|
248
|
+
AccountMeta(PUMP_SWAP_FEE_RECIPIENT, is_signer=False, is_writable=True),
|
|
249
|
+
AccountMeta(pool, is_signer=False, is_writable=True),
|
|
250
|
+
AccountMeta(user, is_signer=True, is_writable=True),
|
|
251
|
+
AccountMeta(user_base_token_account, is_signer=False, is_writable=True),
|
|
252
|
+
AccountMeta(user_quote_token_account, is_signer=False, is_writable=True),
|
|
253
|
+
AccountMeta(pool_base_token_account, is_signer=False, is_writable=True),
|
|
254
|
+
AccountMeta(pool_quote_token_account, is_signer=False, is_writable=True),
|
|
255
|
+
AccountMeta(protocol_fee_token_account, is_signer=False, is_writable=True),
|
|
256
|
+
AccountMeta(base_token_program, is_signer=False, is_writable=False),
|
|
257
|
+
AccountMeta(TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
|
258
|
+
AccountMeta(PUMP_SWAP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
|
|
259
|
+
AccountMeta(PUMP_SWAP_PROGRAM, is_signer=False, is_writable=False),
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
return Instruction(PUMP_SWAP_PROGRAM, data, accounts)
|
pumpfun/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pumpfun-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Buy and sell on PumpFun bonding curves + PumpSwap AMM. Directly from Python.
|
|
5
|
+
Project-URL: Homepage, https://github.com/JinUltimate1995/pumpfun-python
|
|
6
|
+
Project-URL: Repository, https://github.com/JinUltimate1995/pumpfun-python
|
|
7
|
+
Project-URL: Issues, https://github.com/JinUltimate1995/pumpfun-python/issues
|
|
8
|
+
Author: JinUltimate1995
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: amm,async,bonding-curve,crypto,defi,dex,pumpfun,pumpswap,python,solana,trading
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: solders>=0.21
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# pumpfun-python
|
|
34
|
+
|
|
35
|
+
**Buy and sell on PumpFun bonding curves + PumpSwap AMM. Directly from Python. No Jupiter needed.**
|
|
36
|
+
|
|
37
|
+
[](https://www.python.org/downloads/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
[](https://peps.python.org/pep-0561/)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Why?
|
|
44
|
+
|
|
45
|
+
PumpFun tokens start on a **bonding curve** and graduate to a **PumpSwap AMM pool**. Jupiter doesn't always route through these — and when it does, you're at the mercy of their routing engine. This library builds the raw Solana instructions so you can swap directly.
|
|
46
|
+
|
|
47
|
+
- **Pre-graduation** — buy/sell against the PumpFun bonding curve (v2, 16/14 accounts)
|
|
48
|
+
- **Post-graduation** — swap on PumpSwap AMM pools (constant-product, 13 accounts)
|
|
49
|
+
- **Zero dependencies beyond solders + httpx** — no SDK bloat
|
|
50
|
+
- **Production-tested** — extracted from a live trading system
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install pumpfun-python
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or from source:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install git+https://github.com/JinUltimate1995/pumpfun-python.git
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
### 💰 Calculate buy/sell amounts (no RPC needed)
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from pumpfun import calculate_buy_amount, calculate_sell_amount
|
|
74
|
+
|
|
75
|
+
# How many tokens for 0.1 SOL?
|
|
76
|
+
tokens = calculate_buy_amount(
|
|
77
|
+
sol_amount_lamports=100_000_000, # 0.1 SOL
|
|
78
|
+
virtual_sol_reserves=30_000_000_000,
|
|
79
|
+
virtual_token_reserves=1_000_000_000_000,
|
|
80
|
+
)
|
|
81
|
+
print(f"Tokens received: {tokens:,}")
|
|
82
|
+
|
|
83
|
+
# How much SOL for selling 1M tokens?
|
|
84
|
+
sol_out = calculate_sell_amount(
|
|
85
|
+
token_amount=1_000_000,
|
|
86
|
+
virtual_sol_reserves=30_000_000_000,
|
|
87
|
+
virtual_token_reserves=1_000_000_000_000,
|
|
88
|
+
)
|
|
89
|
+
print(f"SOL received: {sol_out / 1e9:.6f}")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 📈 Read bonding curve state
|
|
93
|
+
|
|
94
|
+
> **RPC calls require your own Solana RPC endpoint.** Get a free key from [Helius](https://helius.dev), [QuickNode](https://quicknode.com), or use the public endpoint (rate-limited).
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import asyncio
|
|
98
|
+
from pumpfun import fetch_bonding_curve_state
|
|
99
|
+
|
|
100
|
+
async def main():
|
|
101
|
+
state = await fetch_bonding_curve_state(
|
|
102
|
+
rpc_url="https://api.mainnet-beta.solana.com",
|
|
103
|
+
token_mint="YourTokenMintAddress",
|
|
104
|
+
)
|
|
105
|
+
print(f"Reserves: {state.virtual_sol_reserves} SOL, {state.virtual_token_reserves} tokens")
|
|
106
|
+
print(f"Complete (graduated): {state.complete}")
|
|
107
|
+
print(f"Creator: {state.creator}")
|
|
108
|
+
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 🔨 Build a buy instruction
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from solders.pubkey import Pubkey
|
|
116
|
+
from pumpfun import build_buy_instruction, get_bonding_curve_pda
|
|
117
|
+
|
|
118
|
+
user = Pubkey.from_string("YourWalletAddress")
|
|
119
|
+
token_mint = Pubkey.from_string("TokenMintAddress")
|
|
120
|
+
bonding_curve = get_bonding_curve_pda(token_mint)
|
|
121
|
+
|
|
122
|
+
ix = build_buy_instruction(
|
|
123
|
+
user=user,
|
|
124
|
+
token_mint=token_mint,
|
|
125
|
+
bonding_curve=bonding_curve,
|
|
126
|
+
sol_amount_lamports=100_000_000, # 0.1 SOL max
|
|
127
|
+
min_tokens_out=950_000, # slippage protection
|
|
128
|
+
creator=Pubkey.from_string("CreatorAddress"),
|
|
129
|
+
fee_recipient=Pubkey.from_string("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV"),
|
|
130
|
+
)
|
|
131
|
+
# Add `ix` to your transaction
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 🏊 PumpSwap AMM swap
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from pumpfun import calculate_swap_output, build_swap_instruction, fetch_pool_state
|
|
138
|
+
|
|
139
|
+
# Read pool state
|
|
140
|
+
pool = await fetch_pool_state(rpc_url, "PoolAddress")
|
|
141
|
+
|
|
142
|
+
# Calculate output
|
|
143
|
+
amount_out, fee = calculate_swap_output(
|
|
144
|
+
amount_in=100_000_000, # 0.1 SOL
|
|
145
|
+
reserve_in=5_000_000_000,
|
|
146
|
+
reserve_out=1_000_000_000_000,
|
|
147
|
+
lp_fee_bps=pool.lp_fee_basis_points,
|
|
148
|
+
protocol_fee_bps=pool.protocol_fee_basis_points,
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## API Reference
|
|
155
|
+
|
|
156
|
+
### Bonding Curve (pre-graduation)
|
|
157
|
+
|
|
158
|
+
| Function | Description |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `calculate_buy_amount()` | Tokens received for SOL input (1% fee) |
|
|
161
|
+
| `calculate_sell_amount()` | SOL received for token input (1% fee) |
|
|
162
|
+
| `build_buy_instruction()` | Build PumpFun v2 BUY ix (16 accounts) |
|
|
163
|
+
| `build_sell_instruction()` | Build PumpFun v2 SELL ix (14 accounts) |
|
|
164
|
+
| `fetch_bonding_curve_state()` | Read reserves, creator, completion from chain |
|
|
165
|
+
| `fetch_fee_recipient()` | Read fee recipient from Global account |
|
|
166
|
+
|
|
167
|
+
### PumpSwap AMM (post-graduation)
|
|
168
|
+
|
|
169
|
+
| Function | Description |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `calculate_swap_output()` | Output amount for constant-product swap |
|
|
172
|
+
| `build_swap_instruction()` | Build PumpSwap swap ix (13 accounts) |
|
|
173
|
+
| `fetch_pool_state()` | Read pool vaults, mints, fees from chain |
|
|
174
|
+
|
|
175
|
+
### PDA Helpers
|
|
176
|
+
|
|
177
|
+
| Function | Description |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `get_bonding_curve_pda()` | Derive bonding curve PDA for a mint |
|
|
180
|
+
| `get_associated_token_address()` | Derive ATA (SPL Token or Token-2022) |
|
|
181
|
+
|
|
182
|
+
### Constants
|
|
183
|
+
|
|
184
|
+
All program IDs are exported: `PUMP_FUN_PROGRAM`, `PUMP_SWAP_PROGRAM`, `PUMP_AMM_PROGRAM`, plus system programs, discriminators, and fee accounts.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Account Layout Notes
|
|
189
|
+
|
|
190
|
+
**BUY instruction** uses 16 accounts + 1 remaining account (bonding-curve-v2 PDA).
|
|
191
|
+
|
|
192
|
+
**SELL instruction** uses 14 accounts + 1 remaining — but **creator_vault [8] and token_program [9] are SWAPPED** relative to buy. This is an undocumented PumpFun quirk that causes failed transactions if you copy the buy layout.
|
|
193
|
+
|
|
194
|
+
**Pump AMM pools** (pAMMBay program) can have **reversed base/quote** — base_mint=SOL, quote_mint=TOKEN. The `PoolState.base_is_sol` flag indicates this.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Also by JinUltimate1995
|
|
199
|
+
|
|
200
|
+
- **[jupiter-swap-python](https://github.com/JinUltimate1995/jupiter-swap-python)** — Jupiter swap client for Python. Async. Typed.
|
|
201
|
+
- **[solana-rpc-resilient](https://github.com/JinUltimate1995/solana-rpc-resilient)** — Fault-tolerant Solana RPC with automatic failover.
|
|
202
|
+
- **[dexscreener-python](https://github.com/JinUltimate1995/dexscreener-python)** — DexScreener API client for Python.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pumpfun/__init__.py,sha256=Sbnl2OdOjn4PfX_OMrxgdBO5gFutHkBse1k4GDi5FhQ,1190
|
|
2
|
+
pumpfun/bonding_curve.py,sha256=1_bVU5KfHkmoux4GHXf772hmGjJaIK2fOAxlkJj6gkQ,12797
|
|
3
|
+
pumpfun/constants.py,sha256=XP0PHH9EuQW5kYsPY1RrEz37_4jotvb4nybsHSbnjOs,3055
|
|
4
|
+
pumpfun/pda.py,sha256=7FfwymxsPQjxpKoQdMNG6Osn8cvzuc2dwl0wWJN1iVA,2018
|
|
5
|
+
pumpfun/pumpswap.py,sha256=cpvFPOLbBpjZlVSt1c3OGihDNc5-lToA75gP4iRRsMc,8990
|
|
6
|
+
pumpfun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
pumpfun_python-0.1.0.dist-info/METADATA,sha256=fwssk43-kbP0qiO4QlXrW9hj0B8wSafqCVxfK5ll1-0,7072
|
|
8
|
+
pumpfun_python-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
pumpfun_python-0.1.0.dist-info/licenses/LICENSE,sha256=D7y6S1R0LcQMuzvEM3af4NM2zASwS0H82DKH-3g_FDE,1072
|
|
10
|
+
pumpfun_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 JinUltimate1995
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|