Skip to main content
Эта страница переведена с помощью ИИ. За эталон принимается английская версия.Открыть английскую версию →

Когда CPI — правильный инструмент

Пользовательская программа имеет смысл, когда своп должен произойти атомарно с другими изменениями состояния на цепи, которые может сделать только ваша программа. Типичные случаи:
  • Программы эскроу / лимитных ордеров — пользователь депонирует монету в ваше эскроу, ваша программа отслеживает условие цены, и при его срабатывании программа атомарно выполняет своп через Raydium и зачисляет результат на счёт пользователя.
  • Прокси-агрегаторы — одна инструкция, которая маршрутизирует своп через Raydium и один или несколько других DEX, со всеми переходами под единой проверкой проскальзывания, принадлежащей вашей программе.
  • Автокомпаундирующие хранилища — депонируйте LP или ставку фермы в ваше хранилище, хранилище собирает награды по расписанию, переинвестирует ликвидность, выпускает токены доли.
  • Стратегические хранилища — позиции LP с кредитным плечом, которые перебалансируются путём свопа через CLMM; ликвидаторы, которые закрывают позиции и свопят залог в одной транзакции.
  • Платформы запуска токенов с пользовательским вестингом — ваша программа держит токены вестинга и выпускает их в пул Raydium по расписанию.
Если вы просто хотите отправить своп из офчейн-кода, CPI — это избыточно. Используйте SDK. CPI оправдывает свою сложность только когда атомарность с вашим собственным состоянием — это требование.

Паттерны композиции

Паттерн 1: Тонкий прокси

Ваша программа предоставляет одну инструкцию, которая проверяет некоторую политику (например, белый список пар монет, скидка комиссии для проверенных пользователей) и затем перенаправляет в Raydium.
Состояние находится в ATA пользователя. Ваша программа не владеет токенами. Минимальный отпечаток доверия.

Паттерн 2: Эскроу

Ваша программа владеет PDA, который держит входную монету пользователя. При срабатывании PDA подписывает CPI в Raydium для свопа своего собственного баланса.
Критический момент: PDA подписывает через CpiContext::new_with_signer. См. Семена подписантов PDA.

Паттерн 3: Составной многоскачковый своп

Ваша программа выполняет несколько CPI в одной инструкции, обеспечивая единую границу проскальзывания для всех них. Инструкции свопа Raydium имеют свой собственный minimum_amount_out, но вы устанавливаете их на 0 (или очень свободный минимум) и обеспечиваете строгий финальный минимум сами после последнего скачка.
Это даёт вам единую точку отката для всего маршрута. Используйте этот паттерн только когда вы доверяете каждому скачку быть безопасным по проскальзыванию; в противном случае позвольте каждому скачку обеспечивать свой собственный минимум.

Паттерн 4: Хранилище / стратегия

Ваша программа держит LP-токены или ставку фермы в PDA. Хранитель (или пользователь) вызывает compound(), который:
  1. Собирает награды с фермы.
  2. Свопит награды на токены пула (CPI в CPMM или CLMM).
  3. Депонирует результаты обратно в LP (ещё один CPI).
  4. Ставит новый LP (ещё один CPI).
Всё в одной транзакции, чтобы NAV хранилища двигался атомарно. Бюджет вычислений обычно составляет 600k–1M CU; таблицы поиска адресов обязательны.

Конструкция списка аккаунтов

Структура Accounts вызывающей программы отражает порядок аккаунтов программы Raydium, но большинство аккаунтов на стороне Raydium — это UncheckedAccount, потому что Raydium сам их проверяет. Вы добавляете ограничения только на аккаунты, которыми вы владеете:
Асимметрия — строгая проверка ваших аккаунтов, UncheckedAccount на стороне Raydium — это не лень. Получатель проверяет свои собственные; двойная проверка у вызывающей стороны просто сжигает CU и рискует выйти из синхронизации, когда Raydium выпустит новое поле структуры.

Сам вызов CPI

Семена подписантов PDA

CPI успешен только если PDA, переданный как authority, совпадает с выводом, который заявляет вызывающая сторона. Оба должны согласиться на:
  1. Последовательность байтов семени (здесь [b"escrow", user.key().as_ref()]).
  2. Bump.
  3. ID вызывающей программы (ваша программа, не Raydium).
Обратите внимание, чему именно должен соответствовать PDA. Слот authority в CPMM — это его собственный vault PDA: фиксированный аккаунт уровня программы, который CPMM выводит и подписывает сам и который ваша программа не контролирует и не подменяет. Аккаунт, с которым должны совпадать сиды вашего PDA, — это payer: проверка происходит внутри собственного хелпера CPMM transfer_from_user_to_pool_vault, который требует, чтобы аккаунт, переданный как payer, был владельцем input_token_account. Частая ошибка: передача user как payer, когда escrow_input_ata принадлежит PDA эскроу. Программа SPL Token отклоняет с owner mismatch. Всегда делайте payer владельцем ATA — и подписывайте за него через new_with_signer, когда этот владелец является PDA.

Оставшиеся аккаунты

Несколько инструкций Raydium принимают список аккаунтов переменной длины, добавленный после фиксированных — оставшиеся аккаунты.
  • CLMM SwapV2: 1–8 аккаунтов TickArrayState для массивов тиков, которые своп может пересечь, в направлении свопа.
  • Farm v6 Deposit / Harvest / Withdraw: пары (reward_vault, user_reward_ata), одна пара на активный слот награды.
  • Монеты Token-2022 с transfer-hook: программа transfer-hook плюс любые аккаунты, которые нужны хуку.
Помощники Anchor CPI не проверяют типы оставшихся аккаунтов. Передайте их через:
Порядок имеет значение. Для CLMM:
Для сбора наград farm v6:
Ваша вызывающая программа должна передать оставшиеся аккаунты, которые она получает от клиента, без изменений. Не пытайтесь их фильтровать или переупорядочивать.

Бюджет вычислений для составных вызовов

CPI стоит ~1,500 CU для самого фрейма вызова; собственное использование CU вызываемой стороны складывается сверху. Приведённые ниже цифры для вызываемой стороны измерены на реальных транзакциях mainnet в высокообъёмных пулах 2026-09-09 и считаны из строки лога Program <id> consumed N of M compute units для собственного вызова программы Raydium (то есть они включают её внутренние CPI к token-программе): Добавьте сверху ~1,500 на каждый фрейм CPI и накладные расходы вашей собственной программы. Стоимость свопа в CLMM растёт с числом пересечённых тиков, поэтому считайте её цифру нижней границей. Минты Token-2022 добавляют стоимость обработки расширения при самой передаче; измеряйте её для своих минтов, а не применяйте единый множитель.
В более ранних редакциях этой страницы приводились оценки в 5–7 раз выше (~150,000 CU на своп CPMM, ~180,000 для CLMM). Они никогда не измерялись. Планируйте бюджет по собственному показанию computeUnitsConsumed, а не по числу из документации — и учитывайте, что полная транзакция стоит больше, чем одна инструкция Raydium, как только учтены создание ATA, обёртывание wSOL и инструкции compute-budget.
Всегда устанавливайте явный ComputeBudgetProgram::set_compute_unit_limit:
Потолок по умолчанию 200k CU молча исчерпается задолго до завершения составного вызова.

Распространение ошибок

Программы Raydium возвращают ошибки Anchor со стабильными кодами ошибок. Ваша вызывающая программа видит их как Err(ProgramError::Custom(code)). Пробросьте по умолчанию:
Или перехватите для конкретных кодов:
Обратите внимание на слагаемое ERROR_CODE_OFFSET: варианты #[error_code] нумеруются начиная с 6000, поэтому сравнение с «сырым» дискриминантом перечисления никогда не совпадёт. (Ни в anchor-lang, ни в raydium_cp_swap нет вспомогательной функции is_err — в более ранних редакциях этой страницы использовалась функция, которой не существует.) Сопоставление кода ошибки со значением стабильно согласно политике IDL (sdk-api/anchor-idl); новые коды добавляются в конец, существующие коды никогда не меняют значение.

Полный рабочий пример: эскроу лимитного ордера

Поток:
  1. open_order — пользователь депонирует amount_in из input_mint в PDA эскроу; записывает целевой min_amount_out и срок действия.
  2. execute_order — кто-либо (хранитель) вызывает с текущими аккаунтами пула. Программа проверяет текущую котировку ≥ min_amount_out, затем выполняет CPI своп Raydium и держит выход в эскроу.
  3. claim — пользователь снимает выходную монету из эскроу.
Хранитель платит комиссию за транзакцию (они получают комиссию хранителя где-то ещё — не показано). PDA order подписывает CPI как payer, поскольку ему принадлежит входной ATA эскроу; поэтому ExecuteOrder также нужно поле pool_authority: UncheckedAccount<'info> для собственного vault PDA программы CPMM. Как проверка проскальзывания на стороне Raydium, так и собственная проверка дельты эскроу обеспечивают минимум — подстраховка.

Тестирование

Подтягивание программ Raydium в локальный валидатор для интеграционных тестов (из Anchor.toml):
Также клонируйте аккаунты состояния пула, чтобы ваши тесты могли фактически выполнять свопы; anchor test получает их из mainnet при запуске. См. sdk-api/rust-cpi.

Подводные камни, специфичные для композиции

Реентерабельность

Solana не имеет истинной реентерабельности — CPI не может вызвать обратно в исходную программу в одном вызове. Но вы всё ещё можете построить себя в логическую реентерабельность: CPI, который читает ваше состояние, затем ваш код читает его снова, предполагая, что CPI его не изменил. Для Raydium CPI не трогают ваше состояние, поэтому это менее проблема, чем, например, в контекстах flash-loan. Но если вы составляете Raydium с протоколом кредитования, будьте осторожны.

Дрейф изменяемости аккаунта

Если ваша программа передаёт аккаунт как mut, но Raydium ожидает его только для чтения (или наоборот), runtime отклоняет вызов с InvalidAccountData. Всегда проверяйте ожидаемую изменяемость инструкции Raydium в IDL; raydium_cp_swap::cpi::accounts::Swap выставляет изменяемость каждого аккаунта за вас, исходя из маркеров #[account(mut)] на собственной структуре Swap в CPMM — сгенерированные поля все имеют простой тип AccountInfo<'info>, так что флаги несёт производная реализация ToAccountMetas, а не типы полей.

Поле программы Token-2022

Входные и выходные монеты могут быть под разными программами токенов — одна SPL Token, одна Token-2022. CPI имеет отдельные поля input_token_program и output_token_program по этой причине. Всегда проверяйте поле owner каждой монеты и маршрутизируйте правильную программу в каждый слот.

Версионированные транзакции

Составная транзакция, которая делает 2+ CPI Raydium плюс создание ATA, редко помещается в legacy (v0-без-LUT) транзакцию. Используйте V0 с таблицами поиска адресов; получите публичные LUT Raydium через raydium.getRaydiumLutAddresses().

Указатели

Источники: