Skip to content

Commit bfc74d1

Browse files
smypmsaclaude
andauthored
fix: align instruction builders with IDL signer/writable flags and data encoding (#18)
* fix: align instruction builders with IDL signer/writable flags and data encoding Audited all instruction builders against pump_fun_idl.json and pump_fun_amm_idl.json. Fixed 8 discrepancies: OptionBool encoding (1 byte not 2), removed spurious track_volume from bonding curve buy/sell, corrected writable/signer flags on create_v2 and extend_account, fixed claim_cashback signer flag, and added missing PUMP_AMM_PROGRAM account to collect_coin_creator_fee. E2e mainnet tests confirm PumpSwap buy+sell, bonding curve buy+sell, and token launch all work correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review comments on PR #18 - extend_account: fix user account is_signer=True → False per IDL; update matching test assertion to expect is_signer=False - mainnet-test.sh: introduce AMM_OVERFLOW_ONLY flag so non-6023 AMM failures are not misclassified as "All graduated pools hit error 6023" - mainnet-test.sh: validate parsed mint from --json launch output before recording PASS; fall back to FAIL with "missing mint" message for both launch and launch --buy branches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ccfc039 commit bfc74d1

6 files changed

Lines changed: 266 additions & 30 deletions

File tree

scripts/mainnet-test.sh

Lines changed: 133 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,17 @@ else:
221221
GRADUATED_MINT=$(echo "$TRENDING" | python3 -c "
222222
import json, sys
223223
tokens = json.load(sys.stdin)
224+
# Prefer graduated tokens with higher SOL reserves — pools with extreme
225+
# base/quote ratios can trigger on-chain overflow at buy.rs:400.
226+
best = None
224227
for t in tokens:
225228
if t.get('complete') and t.get('pump_swap_pool'):
226-
print(t['mint'])
227-
break
229+
if best is None:
230+
best = t
231+
elif t.get('market_cap', 0) > best.get('market_cap', 0):
232+
best = t
233+
if best:
234+
print(best['mint'])
228235
else:
229236
print('')
230237
" 2>/dev/null)
@@ -259,22 +266,22 @@ if [[ "$SKIP_TRADING" == true ]]; then
259266
else
260267
echo "=== Group 5: Bonding Curve Trading ==="
261268
if [[ -n "$ACTIVE_MINT" ]]; then
262-
BUY_OUT=$(uv run pumpfun buy "$ACTIVE_MINT" 0.001 --confirm 2>&1)
269+
BUY_OUT=$(uv run pumpfun buy "$ACTIVE_MINT" 0.001 --slippage 50 --confirm 2>&1)
263270
BUY_EXIT=$?
264271
if [[ $BUY_EXIT -eq 0 ]]; then
265272
record "BC Trade" "buy 0.001 --confirm" "PASS"
266273
echo " Buy OK. Waiting 5s for RPC state..."
267274
sleep 5
268275

269-
SELL_OUT=$(uv run pumpfun sell "$ACTIVE_MINT" all --confirm 2>&1)
276+
SELL_OUT=$(uv run pumpfun sell "$ACTIVE_MINT" all --slippage 50 --confirm 2>&1)
270277
SELL_EXIT=$?
271278
if [[ $SELL_EXIT -eq 0 ]]; then
272279
record "BC Trade" "sell all --confirm" "PASS"
273280
else
274281
# Retry once after delay
275282
echo " Sell failed, retrying in 5s..."
276283
sleep 5
277-
SELL_OUT=$(uv run pumpfun sell "$ACTIVE_MINT" all --confirm 2>&1)
284+
SELL_OUT=$(uv run pumpfun sell "$ACTIVE_MINT" all --slippage 50 --confirm 2>&1)
278285
SELL_EXIT=$?
279286
if [[ $SELL_EXIT -eq 0 ]]; then
280287
record "BC Trade" "sell all --confirm" "PASS" "Needed retry (RPC lag)"
@@ -297,20 +304,133 @@ else
297304

298305
echo "=== Group 6: PumpSwap AMM Trading ==="
299306
if [[ -n "$GRADUATED_MINT" ]]; then
300-
AMM_OUT=$(uv run pumpfun buy "$GRADUATED_MINT" 0.001 --force-amm --confirm 2>&1)
301-
AMM_EXIT=$?
302-
if [[ $AMM_EXIT -eq 0 ]]; then
303-
record "PumpSwap" "buy --force-amm --confirm" "PASS" "BUG-1 may be fixed!"
304-
elif echo "$AMM_OUT" | grep -q "6023"; then
305-
record "PumpSwap" "buy --force-amm --confirm" "ISSUE" "Error 6023 — known BUG-1"
306-
else
307-
record "PumpSwap" "buy --force-amm --confirm" "FAIL" "$(echo "$AMM_OUT" | head -1 | cut -c1-60)"
307+
# Build a list of graduated mints to try (some pools have on-chain overflow bugs)
308+
ALL_GRADUATED=$(echo "$TRENDING" | python3 -c "
309+
import json, sys
310+
tokens = json.load(sys.stdin)
311+
for t in tokens:
312+
if t.get('complete') and t.get('pump_swap_pool'):
313+
print(t['mint'])
314+
" 2>/dev/null)
315+
316+
AMM_BUY_OK=false
317+
AMM_MINT=""
318+
AMM_OVERFLOW_ONLY=true
319+
while IFS= read -r CANDIDATE; do
320+
[[ -z "$CANDIDATE" ]] && continue
321+
AMM_OUT=$(uv run pumpfun buy "$CANDIDATE" 0.001 --slippage 50 --force-amm --confirm 2>&1)
322+
AMM_EXIT=$?
323+
if [[ $AMM_EXIT -eq 0 ]]; then
324+
AMM_BUY_OK=true
325+
AMM_MINT="$CANDIDATE"
326+
break
327+
elif echo "$AMM_OUT" | grep -q "6023"; then
328+
echo " Pool overflow (6023) on ${CANDIDATE:0:12}…, trying next"
329+
continue
330+
else
331+
# Non-overflow failure — record and stop
332+
AMM_OVERFLOW_ONLY=false
333+
record "PumpSwap" "buy --force-amm --confirm" "FAIL" "$(echo "$AMM_OUT" | head -1 | cut -c1-60)"
334+
break
335+
fi
336+
done <<< "$ALL_GRADUATED"
337+
338+
if [[ "$AMM_BUY_OK" == "true" ]]; then
339+
record "PumpSwap" "buy --force-amm --confirm" "PASS"
340+
echo " PumpSwap buy OK. Waiting 5s for RPC state..."
341+
sleep 5
342+
343+
AMM_SELL_OUT=$(uv run pumpfun sell "$AMM_MINT" all --slippage 50 --confirm 2>&1)
344+
AMM_SELL_EXIT=$?
345+
if [[ $AMM_SELL_EXIT -eq 0 ]]; then
346+
record "PumpSwap" "sell all --confirm" "PASS"
347+
else
348+
echo " PumpSwap sell failed, retrying in 5s..."
349+
sleep 5
350+
AMM_SELL_OUT=$(uv run pumpfun sell "$AMM_MINT" all --slippage 50 --confirm 2>&1)
351+
AMM_SELL_EXIT=$?
352+
if [[ $AMM_SELL_EXIT -eq 0 ]]; then
353+
record "PumpSwap" "sell all --confirm" "PASS" "Needed retry (RPC lag)"
354+
else
355+
record "PumpSwap" "sell all --confirm" "FAIL" "Failed after retry"
356+
fi
357+
fi
358+
elif [[ "$AMM_OVERFLOW_ONLY" == "true" ]]; then
359+
record "PumpSwap" "buy --force-amm --confirm" "ISSUE" "All graduated pools hit error 6023"
308360
fi
309361
else
310362
record "PumpSwap" "buy --force-amm" "ISSUE" "No graduated mint found"
311363
fi
312364
echo " Done."
313365

366+
# --- Group 6b: Token Launch ---
367+
368+
echo "=== Group 6b: Token Launch ==="
369+
# Generate a minimal test image
370+
python3 -c "from PIL import Image; Image.new('RGB',(100,100),'blue').save('/tmp/e2e_test_token.png')" 2>/dev/null
371+
LAUNCH_IMG=""
372+
[[ -f /tmp/e2e_test_token.png ]] && LAUNCH_IMG="--image /tmp/e2e_test_token.png"
373+
374+
LAUNCH_OUT=$(uv run pumpfun --json launch --name "E2E Test $(date +%s)" --ticker "E2ET" --desc "Automated e2e test token" $LAUNCH_IMG 2>&1)
375+
LAUNCH_EXIT=$?
376+
echo "$LAUNCH_OUT" > "$LAST_OUTPUT_FILE"
377+
if [[ $LAUNCH_EXIT -eq 0 ]]; then
378+
LAUNCHED_MINT=$(echo "$LAUNCH_OUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('mint',''))" 2>/dev/null)
379+
if [[ -n "$LAUNCHED_MINT" ]]; then
380+
record "Launch" "launch" "PASS"
381+
echo " Launched: $LAUNCHED_MINT"
382+
else
383+
record "Launch" "launch" "FAIL" "Invalid --json payload (missing mint)"
384+
LAUNCHED_MINT=""
385+
fi
386+
else
387+
LAUNCH_ERR=$(echo "$LAUNCH_OUT" | grep -m1 "Error:" | cut -c1-60)
388+
[[ -z "$LAUNCH_ERR" ]] && LAUNCH_ERR="exit=$LAUNCH_EXIT"
389+
record "Launch" "launch" "FAIL" "$LAUNCH_ERR"
390+
LAUNCHED_MINT=""
391+
fi
392+
393+
# Launch + buy
394+
LAUNCH_BUY_OUT=$(uv run pumpfun --json launch --name "E2E Buy Test $(date +%s)" --ticker "E2EB" --desc "Automated e2e launch+buy" $LAUNCH_IMG --buy 0.001 2>&1)
395+
LAUNCH_BUY_EXIT=$?
396+
echo "$LAUNCH_BUY_OUT" > "$LAST_OUTPUT_FILE"
397+
if [[ $LAUNCH_BUY_EXIT -eq 0 ]]; then
398+
LAUNCHED_BUY_MINT=$(echo "$LAUNCH_BUY_OUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('mint',''))" 2>/dev/null)
399+
if [[ -n "$LAUNCHED_BUY_MINT" ]]; then
400+
record "Launch" "launch --buy 0.001" "PASS"
401+
echo " Launched+bought: $LAUNCHED_BUY_MINT"
402+
else
403+
record "Launch" "launch --buy 0.001" "FAIL" "Invalid --json payload (missing mint)"
404+
LAUNCHED_BUY_MINT=""
405+
fi
406+
407+
# Sell from the launched token to test full cycle
408+
if [[ -n "$LAUNCHED_BUY_MINT" ]]; then
409+
echo " Waiting 5s for RPC state..."
410+
sleep 5
411+
LSELL_OUT=$(uv run pumpfun sell "$LAUNCHED_BUY_MINT" all --slippage 50 --confirm 2>&1)
412+
LSELL_EXIT=$?
413+
if [[ $LSELL_EXIT -eq 0 ]]; then
414+
record "Launch" "sell launched token" "PASS"
415+
else
416+
sleep 5
417+
LSELL_OUT=$(uv run pumpfun sell "$LAUNCHED_BUY_MINT" all --slippage 50 --confirm 2>&1)
418+
LSELL_EXIT=$?
419+
if [[ $LSELL_EXIT -eq 0 ]]; then
420+
record "Launch" "sell launched token" "PASS" "Needed retry"
421+
else
422+
record "Launch" "sell launched token" "FAIL" "Failed after retry"
423+
fi
424+
fi
425+
fi
426+
else
427+
LAUNCH_BUY_ERR=$(echo "$LAUNCH_BUY_OUT" | grep -m1 "Error:" | cut -c1-60)
428+
[[ -z "$LAUNCH_BUY_ERR" ]] && LAUNCH_BUY_ERR="exit=$LAUNCH_BUY_EXIT"
429+
record "Launch" "launch --buy 0.001" "FAIL" "$LAUNCH_BUY_ERR"
430+
fi
431+
rm -f /tmp/e2e_test_token.png
432+
echo " Done."
433+
314434
# --- Group 7: Extras ---
315435

316436
echo "=== Group 7: Extras ==="

src/pumpfun_cli/protocol/instructions.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939
)
4040
from pumpfun_cli.protocol.idl_parser import IDLParser
4141

42-
# Trailing bytes appended after the two u64 args (track_volume flag).
43-
_TRACK_VOLUME = bytes([1, 1])
42+
# OptionBool(true) = 1 byte: the IDL defines OptionBool as struct { bool },
43+
# so it serialises to a single 0x01 byte, NOT 2 bytes (Option<bool>).
44+
_TRACK_VOLUME = bytes([1])
4445

4546

4647
def build_buy_instructions(
@@ -103,10 +104,7 @@ def build_buy_instructions(
103104

104105
discriminators = idl.get_instruction_discriminators()
105106
instruction_data = (
106-
discriminators["buy"]
107-
+ struct.pack("<Q", token_amount)
108-
+ struct.pack("<Q", max_sol_cost)
109-
+ _TRACK_VOLUME
107+
discriminators["buy"] + struct.pack("<Q", token_amount) + struct.pack("<Q", max_sol_cost)
110108
)
111109

112110
buy_ix = Instruction(
@@ -180,7 +178,6 @@ def build_buy_exact_sol_in_instructions(
180178
BUY_EXACT_SOL_IN_DISCRIMINATOR
181179
+ struct.pack("<Q", spendable_sol_in)
182180
+ struct.pack("<Q", min_tokens_out)
183-
+ _TRACK_VOLUME
184181
)
185182

186183
buy_ix = Instruction(
@@ -254,7 +251,6 @@ def build_sell_instructions(
254251
discriminators["sell"]
255252
+ struct.pack("<Q", token_amount)
256253
+ struct.pack("<Q", min_sol_output)
257-
+ _TRACK_VOLUME
258254
)
259255

260256
sell_ix = Instruction(
@@ -323,7 +319,7 @@ def build_create_instructions(
323319
AccountMeta(pubkey=ASSOCIATED_TOKEN_PROGRAM, is_signer=False, is_writable=False),
324320
# Mayhem accounts are always required by create_v2 per IDL,
325321
# regardless of is_mayhem_mode flag value.
326-
AccountMeta(pubkey=MAYHEM_PROGRAM_ID, is_signer=False, is_writable=False),
322+
AccountMeta(pubkey=MAYHEM_PROGRAM_ID, is_signer=False, is_writable=True),
327323
AccountMeta(pubkey=MAYHEM_GLOBAL_PARAMS, is_signer=False, is_writable=False),
328324
AccountMeta(pubkey=MAYHEM_SOL_VAULT, is_signer=False, is_writable=True),
329325
AccountMeta(pubkey=mayhem_state, is_signer=False, is_writable=True),
@@ -369,7 +365,7 @@ def build_extend_account_instruction(
369365
"""
370366
accounts = [
371367
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
372-
AccountMeta(pubkey=user, is_signer=True, is_writable=True),
368+
AccountMeta(pubkey=user, is_signer=False, is_writable=False),
373369
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
374370
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
375371
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
@@ -486,7 +482,10 @@ def build_pumpswap_buy_instructions(
486482
]
487483

488484
instruction_data = (
489-
PUMPSWAP_BUY_DISCRIMINATOR + struct.pack("<Q", amount_out) + struct.pack("<Q", max_sol_in)
485+
PUMPSWAP_BUY_DISCRIMINATOR
486+
+ struct.pack("<Q", amount_out)
487+
+ struct.pack("<Q", max_sol_in)
488+
+ _TRACK_VOLUME
490489
)
491490

492491
buy_ix = Instruction(
@@ -591,6 +590,7 @@ def build_pumpswap_buy_exact_quote_in_instructions(
591590
PUMPSWAP_BUY_EXACT_QUOTE_IN_DISCRIMINATOR
592591
+ struct.pack("<Q", spendable_quote_in)
593592
+ struct.pack("<Q", min_base_amount_out)
593+
+ _TRACK_VOLUME
594594
)
595595

596596
buy_ix = Instruction(
@@ -705,7 +705,7 @@ def build_claim_cashback_instruction(
705705
from pumpfun_cli.protocol.address import find_user_volume_accumulator
706706

707707
accounts = [
708-
AccountMeta(pubkey=user, is_signer=True, is_writable=True),
708+
AccountMeta(pubkey=user, is_signer=False, is_writable=True),
709709
AccountMeta(pubkey=find_user_volume_accumulator(user), is_signer=False, is_writable=True),
710710
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
711711
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
@@ -880,6 +880,7 @@ def build_collect_coin_creator_fee_instruction(
880880
AccountMeta(pubkey=creator_vault_ata, is_signer=False, is_writable=True),
881881
AccountMeta(pubkey=creator_wsol_ata, is_signer=False, is_writable=True),
882882
AccountMeta(pubkey=PUMP_SWAP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
883+
AccountMeta(pubkey=PUMP_AMM_PROGRAM, is_signer=False, is_writable=False),
883884
]
884885

885886
return Instruction(

tests/test_protocol/test_extras_instructions.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def test_claim_cashback_has_5_accounts():
3131
assert len(ix.accounts) == 5
3232
assert ix.program_id == PUMP_PROGRAM
3333
assert ix.accounts[0].pubkey == _USER
34-
assert ix.accounts[0].is_signer is True
34+
assert ix.accounts[0].is_signer is False
3535
assert ix.accounts[0].is_writable is True
3636

3737

@@ -74,8 +74,22 @@ def test_collect_creator_fee_has_5_accounts():
7474
assert ix.data[:8] == COLLECT_CREATOR_FEE_DISCRIMINATOR
7575

7676

77-
def test_collect_coin_creator_fee_has_7_accounts():
77+
def test_claim_cashback_user_not_signer():
78+
"""claim_cashback account[0] (user) must be is_signer=False per IDL."""
79+
idl = IDLParser(str(IDL_PATH))
80+
ix = build_claim_cashback_instruction(idl=idl, user=_USER)
81+
assert ix.accounts[0].pubkey == _USER
82+
assert ix.accounts[0].is_signer is False
83+
84+
85+
def test_collect_coin_creator_fee_has_8_accounts():
7886
ix = build_collect_coin_creator_fee_instruction(creator=_USER)
79-
assert len(ix.accounts) == 7
87+
assert len(ix.accounts) == 8
8088
assert ix.program_id == PUMP_AMM_PROGRAM
8189
assert ix.data[:8] == COLLECT_COIN_CREATOR_FEE_DISCRIMINATOR
90+
91+
92+
def test_collect_coin_creator_fee_last_account_is_program():
93+
"""collect_coin_creator_fee 8th account must be PUMP_AMM_PROGRAM."""
94+
ix = build_collect_coin_creator_fee_instruction(creator=_USER)
95+
assert ix.accounts[7].pubkey == PUMP_AMM_PROGRAM

tests/test_protocol/test_instructions.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
build_buy_exact_sol_in_instructions,
1515
build_buy_instructions,
1616
build_create_instructions,
17+
build_extend_account_instruction,
1718
build_sell_instructions,
1819
)
1920

@@ -165,3 +166,65 @@ def test_create_instructions_mayhem_and_cashback():
165166
# Last byte = cashback true, second-to-last = mayhem true
166167
assert create_ix.data[-1:] == b"\x01"
167168
assert create_ix.data[-2:-1] == b"\x01"
169+
170+
171+
def test_sell_instruction_data_no_track_volume():
172+
"""sell instruction data should be 24 bytes: 8 disc + 8 amount + 8 min_sol. No _TRACK_VOLUME."""
173+
idl = IDLParser(str(IDL_PATH))
174+
ixs = build_sell_instructions(
175+
idl=idl,
176+
mint=_MINT,
177+
user=_USER,
178+
bonding_curve=_BC,
179+
assoc_bc=_ABC,
180+
creator=_USER,
181+
is_mayhem=False,
182+
token_amount=1000,
183+
min_sol_output=0,
184+
)
185+
sell_ix = ixs[0]
186+
assert len(bytes(sell_ix.data)) == 24 # 8 + 8 + 8, no trailing track_volume
187+
188+
189+
def test_sell_instruction_data_no_track_volume_with_cashback():
190+
"""sell with is_cashback=True also has 24-byte data (no _TRACK_VOLUME)."""
191+
idl = IDLParser(str(IDL_PATH))
192+
ixs = build_sell_instructions(
193+
idl=idl,
194+
mint=_MINT,
195+
user=_USER,
196+
bonding_curve=_BC,
197+
assoc_bc=_ABC,
198+
creator=_USER,
199+
is_mayhem=False,
200+
token_amount=1000,
201+
min_sol_output=0,
202+
is_cashback=True,
203+
)
204+
sell_ix = ixs[0]
205+
assert len(bytes(sell_ix.data)) == 24
206+
207+
208+
def test_create_v2_mayhem_program_is_writable():
209+
"""create_v2 account[9] (MAYHEM_PROGRAM_ID) must be is_writable=True per IDL."""
210+
idl = IDLParser(str(IDL_PATH))
211+
ixs = build_create_instructions(
212+
idl=idl,
213+
mint=_MINT,
214+
user=_USER,
215+
name="Test",
216+
symbol="TST",
217+
uri="https://example.com",
218+
)
219+
create_ix = ixs[0]
220+
assert create_ix.accounts[9].pubkey == MAYHEM_PROGRAM_ID
221+
assert create_ix.accounts[9].is_writable is True
222+
223+
224+
def test_extend_account_user_not_writable():
225+
"""extend_account account[1] (user) must be is_writable=False, is_signer=False per IDL."""
226+
idl = IDLParser(str(IDL_PATH))
227+
ix = build_extend_account_instruction(idl=idl, bonding_curve=_BC, user=_USER)
228+
assert ix.accounts[1].pubkey == _USER
229+
assert ix.accounts[1].is_signer is False
230+
assert ix.accounts[1].is_writable is False

0 commit comments

Comments
 (0)