> For the complete documentation index, see [llms.txt](https://docs.rome.builders/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rome.builders/vi/huong-dan-cho-nha-phat-trien/integrate-solana-program.md).

# Tích hợp một chương trình Solana

Tích hợp một chương trình Solana với Rome EVM — ai là người ký Solana, giá trị đi vào chương trình của bạn và quay trở lại như thế nào, quy tắc một luồng, atomic so với iterative, và cấp phát account.

Hướng dẫn này dành cho một đội phát triển chương trình Solana muốn người dùng Rome EVM sử dụng chương trình của họ — một chương trình bảo mật, cho vay hoặc kho lưu trữ, cần giữ giá trị cho các tài khoản EVM. Nó xây dựng dựa trên [Gọi Solana từ EVM](/vi/huong-dan-cho-nha-phat-trien/call-solana-from-evm.md) (cơ chế precompile) và trả lời các câu hỏi xuất hiện khi một chương trình thực sự nằm ở phía bên kia: *ai* ký, *làm thế nào* token đi vào và đi ra, và *những* giới hạn nào định hình thiết kế.

Luồng được dùng xuyên suốt: một tài khoản EVM chuyển USDC vào chương trình của bạn ("shield" hoặc "deposit"), chương trình của bạn giữ nó cho người dùng đó, rồi sau đó trả lại ("unshield" hoặc "withdraw"). Hãy thay các động từ bằng của riêng bạn.

## 1. Mỗi địa chỉ EVM có một danh tính Solana

Rome tạo ra một tài khoản Solana cho mỗi địa chỉ EVM — cả người dùng lẫn hợp đồng — gọi là **quyền bên ngoài**:

```
external_auth(addr) = find_program_address(["EXTERNAL_AUTHORITY", addr_h160], rome_evm_program_id)
```

Nó sở hữu các tài khoản token liên kết của địa chỉ đó (đó là nơi `wUSDC.balanceOf(addr)` đọc từ) và là khóa ký các CPI được thực hiện thay mặt cho địa chỉ đó. Chương trình của bạn chỉ thấy một pubkey Solana 32 byte; nó không bao giờ thấy một H160.

**Ai ký được quyết định bởi ai gọi precompile:**

| Precompile được gọi bởi                                                           | Signer Solana                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Một ứng dụng **hợp đồng**, với một `CALL`                                         | Các **của chính hợp đồng** quyền bên ngoài (`external_auth(contract)`), cùng với mọi sub-PDA có gắn salt của hợp đồng mà nó yêu cầu                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Một ứng dụng **người dùng**, trong một giao dịch gửi thẳng đến địa chỉ precompile | Các **của chính người dùng** quyền bên ngoài                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Một hợp đồng thông qua `DELEGATECALL`                                             | Quyền tối thiểu bị từ chối (`DelegatecallOwnerAuthority`). `invoke` không có signer, và `invoke_signed` với các sub-PDA có salt vẫn chạy — nhưng các sub-PDA đó được đặt trong không gian tên theo địa chỉ của hợp đồng đang chạy (`["EXTERNAL_AUTHORITY", caller, running_contract, salt]`), vì vậy `pda_with_salt` không suy ra chúng. Trong HelperProgram chỉ các selector tạo và cấp vốn được thông qua (`create_ata*`, `create_pda*`, `swap_gas_to_lamports`, `init_spl_mint`, `create_and_init_mint` ); chúng được miễn vì đích đến của chúng được ràng buộc với bên gọi hoặc được cấp vốn bởi operator, chứ không phải vì chúng không ký gì |

Hệ quả là quy tắc ERC-20 mà bạn đã biết: **một hợp đồng không thể tự mình chuyển tài sản của người dùng.** Nó hành động như chính danh tính của nó, và người dùng cấp quyền cho nó trước. Với các token phải di chuyển ở phía Solana, quyền đó là một SPL delegation trên tài khoản token của người dùng; phê duyệt ERC-20 của wrapper `approve` chỉ là một allowance ở phía EVM mà các lệnh chuyển của chính wrapper sẽ tôn trọng. Đó là cách các wrapper token của Rome hoạt động, và đó cũng là cách adapter của bạn sẽ hoạt động (mục 3).

Sub-PDA có salt: `invoke_signed` nhận một danh sách `bytes32` salt, và Rome ký với `["EXTERNAL_AUTHORITY", caller_h160, salt]` cho mỗi phần tử (bump được thêm vào; caller là hợp đồng trong một cuộc gọi trực tiếp). `HelperProgram.pda_with_salt(addr, salt)` trả về địa chỉ từ Solidity. Hãy dùng một salt cho mỗi người dùng (ví dụ địa chỉ của người dùng) khi chương trình của bạn muốn có một tài khoản chủ sở hữu riêng cho mỗi vị thế.

Cấp vốn: một PDA quyền bên ngoài không nhất thiết phải tồn tại để sở hữu một ATA (`create_ata` là idempotent và fee payer Solana của giao dịch sẽ chi trả rent). Nó phải tồn tại và chỉ giữ lamport khi nó cấp vốn cho các tài khoản mà instruction của bạn tạo ra. `HelperProgram.create_pda(addr)` tạo nó như một `CreateAccount`System thuần, vì vậy hãy gọi nó một lần; nó không idempotent. `swap_gas_to_lamports(n)` ghi có vào PDA của địa chỉ `n` lamport từ fee payer Solana của giao dịch, và bên gọi trả phí cho chúng bằng Rome gas: ảnh hưởng của mọi CPI trên track legacy lên số dư fee payer được cộng vào phí giao dịch và tính cho người dùng theo giá gas, giống như rent cho các tài khoản mà một CPI tạo ra cũng được tính. Bước cấp tài nguyên của lane Solana được mô tả ở [Gọi EVM từ Solana](/vi/huong-dan-cho-nha-phat-trien/call-evm-from-solana.md); một hợp đồng cấp phát PDA của chính nó trong constructor hoặc lần gọi đầu tiên.

## 2. Ba cách để giữ một vị thế của người dùng

**A. Một hợp đồng adapter (khuyến nghị cho một adapter có thể tái sử dụng).** Một hợp đồng Solidity đứng trước chương trình của bạn. Người dùng cấp cho quyền của adapter một SPL delegation trên tài khoản token của họ; adapter kéo token vào một tài khoản token mà nó sở hữu, gọi chương trình của bạn như chính nó, và định danh vị thế của người dùng bên trong chương trình của bạn bằng một sub-PDA theo từng người dùng hoặc bằng một định danh mà nó chuyển trong dữ liệu instruction. Khi đi ra, nó bảo chương trình của bạn giải phóng về tài khoản token của chính người dùng. Adapter là danh tính Solana duy nhất mà chương trình của bạn cần biết. Quyền cấp là một SPL delegation, không phải một phê duyệt ERC-20 `approve`; mục 3 giải thích vì sao và điều đó tốn gì cho người dùng.

**B. Người dùng là signer Solana.** Front end gửi một giao dịch từ tài khoản EVM của người dùng **thẳng đến precompile CpiProgram** (`to = 0xff…08`, calldata = ABI-encoded `invoke` hoặc `invoke_signed`), vì vậy quyền bên ngoài của chính người dùng sẽ ký và chương trình của bạn định danh vị thế theo nó. Không có hợp đồng adapter và không có delegation, nhưng mỗi hành động là một giao dịch calldata không thể đọc dễ dàng trong ví, và mọi thứ mà luồng cần ngoài CPI (ATA, một lần chuyển trước đó) là một giao dịch riêng. Cách này phù hợp cho các quyền cấp một lần (một `approve_spl` của người dùng đối với một wrapper) và cho các chương trình đơn giản, không phù hợp cho các luồng nhiều bước. [Cardo](https://github.com/rome-protocol/cardo) dùng dạng này: front end của nó gửi `invoke` trực tiếp đến precompile từ ví của người dùng.

**C. Một router không lưu ký.** Một hợp đồng không bao giờ giữ token của người dùng. Người dùng cấp cho quyền của router một SPL delegation trên tài khoản token của chính họ; router CPIs vào chương trình của bạn với quyền đó làm transfer authority, vì thế token di chuyển từ tài khoản token của người dùng sang tài khoản của chương trình bạn (hoặc ngược lại về người dùng), và router không giữ gì giữa các lần gọi. Router suy ra mọi tài khoản phía người dùng từ `msg.sender`, vì vậy quyền cấp của một người dùng không thể bị trỏ sang tài khoản của người dùng khác, và nó gọi một chương trình cố định với các dạng instruction cố định thay vì hoạt động như một `invoke` passthrough chung.  `RomeDexRouter` trên [rome-dex](https://github.com/rome-protocol/rome-dex) được viết theo cách này; lane EVM của rome-dex cũng cung cấp trực tiếp thô `invoke` của dạng B. Nó cần cùng một quyền cấp như A, với cùng chi phí một delegate (mục 3), và không có bước kéo.

Phần còn lại của hướng dẫn này theo A; C dùng cùng quyền cấp và cùng quy tắc ký.

## 3. Token vào, token ra

USDC trên Rome là một SPL mint duy nhất được nhìn từ hai phía: `wUSDC` wrapper ERC-20 ở phía EVM và ATA của quyền bên ngoài ở phía Solana. Với địa chỉ riêng của một người dùng, hai thứ này là cùng một số dư: `wUSDC.balanceOf(user)` đọc ATA của người dùng. Không có cầu nối hay bước wrapping ở giữa.

**Số dư wrapper của một hợp đồng không phải là một tài khoản token mà hợp đồng kiểm soát.** Factory triển khai `SPL_ERC20_cached` cho một mint thông thường, và của Hadrian `wUSDC` là một trong số đó. Wrapper đó không thể ký thay cho một hợp đồng, vì vậy khi `wUSDC.transferFrom(user, adapter, amount)` hoặc `chuyển` gửi token đến một hợp đồng, các SPL token di chuyển vào **tài khoản token escrow của chính wrapper** (`escrow_ata()`), và wrapper ghi lại số tiền theo từng hợp đồng trong storage của nó. `wUSDC.balanceOf(adapter)` trả về bản ghi đó, một sổ cái escrow, không phải số dư của một tài khoản token do `external_auth(adapter)`. Adapter của bạn không thể chuyển các token đó cho chương trình của bạn trong một CPI; chúng chỉ có thể rời đi thông qua wrapper một lần nữa. Vì vậy một adapter tự cấp vốn bằng `transferFrom` không có gì để chi ở phía Solana.

Đường đi hoạt động là một SPL delegation:

* **Cấp quyền.** Người dùng gửi `approve_spl(adapter, amount, mint)` như một giao dịch **trực tiếp đến HelperProgram** (`to = 0xff…09`), trong đó `mint` là của wrapper `mint_id()`. Quyền của chính người dùng ký, và delegate được đặt trên tài khoản token của người dùng là quyền bên ngoài của adapter.
* **Vào.** Của adapter `deposit(amount)` kéo với overload delegate `HelperProgram.transfer_spl(srcAta, dstAta, amount, mint)`, với ATA của người dùng làm nguồn và ATA của chính adapter (`HelperProgram.ata(address(this), mint)`, được tạo một lần với `create_ata`) làm đích. Quyền của adapter ký với tư cách delegate. Các token giờ nằm trong một tài khoản token do `external_auth(adapter)`sở hữu, chính là tài khoản mà CPI của bạn nêu tên; signer và nguồn đều là của chính adapter, nên không cần thêm quyền nào nữa. Chúng không xuất hiện trong `wUSDC.balanceOf(adapter)`.
* **Ra.** Instruction withdraw của chương trình bạn chuyển đến **ATA của** người dùng, `HelperProgram.ata(user, mint)`. Việc nhận không cần chữ ký của người dùng, và số dư ngay lập tức hiển thị là `wUSDC.balanceOf(user)`. Nếu ATA chưa tồn tại, hãy tạo nó trước với `HelperProgram.create_ata(user, mint)` (idempotent).

**Một tài khoản token chỉ có một delegate.** Quyền cấp ở trên thay thế quyền cấp của chính wrapper trên tài khoản đó. Cho đến khi người dùng cấp lại cho wrapper, mọi `wUSDC` `chuyển` hoặc `transferFrom` lệnh chuyển ra khỏi địa chỉ của họ sẽ revert với lỗi SPL `0x4`. Sau bất kỳ luồng nào dùng quyền cấp này, hãy gọi `wUSDC.isEnabled(user)`; khi nó trả về `false`, hãy yêu cầu người dùng gửi `approve_spl(wrapper, 2^64-1, mint)` đến HelperProgram một lần nữa, mức quyền tối đa mà wrapper này được thiết kế xoay quanh (xem [Một lần: cấp cho wrapper](/vi/huong-dan-cho-nha-phat-trien/dual-lane-app.md#one-time-grant-the-wrapper)). Một quyền cấp bị giới hạn sẽ làm hỏng các lệnh chuyển thông thường của người dùng một lần nữa khi nó cạn, và `wUSDC` isEnabled `vẫn` true `trong khi còn bất kỳ phần nào, vì vậy ứng dụng sẽ không biết để hỏi lại.` Để kiểm tra mint nào có wrapper nào, hãy đọc

wrapper\_kind\_by\_mint(mint) `của factory (địa chỉ factory nằm trong` của chain trong `contracts.json` trong [registry](https://github.com/rome-protocol/rome-registry)). `1` là wrapper đã được cache mà mục này mô tả. `2` là wrapper direct-CPI mà factory triển khai cho một mint Token-2022 có transfer hook đã được kích hoạt; hướng dẫn này không đề cập đến nó. Registry `tokens.json` liệt kê địa chỉ wrapper và mint nhưng không cho biết đó là loại nào.

## 4. Mỗi track cho mỗi hợp đồng, và cho mỗi giao dịch

Rome có hai họ precompile thay đổi trạng thái Solana, và **một giao dịch chỉ dùng đúng một trong số chúng** — runtime từ chối một giao dịch trộn các lời gọi làm thay đổi trạng thái từ cả hai, kể cả giữa các hợp đồng.

| Track      | Precompile                                                                                          | Hiệu ứng xuất hiện                                                                                       | Hoạt động trong thực thi lặp                                                                    |
| ---------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| **Legacy** | `CpiProgram` `0xff…08` (`invoke`, `invoke_signed`), `HelperProgram` `0xff…09`, `Rút` `0x42…16`      | Ngay lập tức, như một CPI thực sự bên trong bước EVM                                                     | **Không** — một CPI legacy trong một giao dịch lặp sẽ thất bại với `CpiProhibitedInIterativeTx` |
| **Cached** | `SplCached` `0xff…05`, `ASplCached` `0xff…06`, `SystemCached` `0xff…04`, `WithdrawCached` `0xff…0b` | Được dàn xếp trong một overlay và commit dưới dạng CPI ở cuối giao dịch; một EVM revert sẽ loại bỏ chúng | Có                                                                                              |

Điều này có nghĩa gì đối với một tích hợp chương trình:

* **Gọi các instruction của chính chương trình bạn chỉ có thể ở track legacy.** Track cached không có `invoke`chung; nó bao gồm chuyển SPL, tạo ATA, các thao tác system-program và rút gas.
* Vì vậy bước cấp vốn cho adapter cũng phải ở track legacy nếu nó chạy trong cùng một giao dịch với CPI. Lệnh `transferFrom` của wrapper cached là một lời gọi track cached, nên nó không thể chia sẻ một giao dịch với CPI của bạn (và, như mục 3 cho thấy, nó chỉ ghi có vào một sổ cái escrow). Hãy kéo với `HelperProgram.transfer_spl(userAta, adapterAta, amount, mint)` thay vào đó, sau khi người dùng đã làm cho adapter trở thành SPL delegate (`approve_spl`được người dùng gửi trực tiếp đến HelperProgram). Lệnh kéo và CPI đều ở track legacy, nên chúng chạy trong một giao dịch.
* **Giữ wrapper ra khỏi giao dịch đó.** Đừng gọi các hàm thay đổi trạng thái của wrapper (`wUSDC.transfer`, `transferFrom`) từ adapter trong cùng một giao dịch với một legacy CPI. Kiểm tra `isEnabled(user)` và số dư từ front end với `eth_call`.
* **Và giao dịch CPI phải là nguyên tử.** Đó là giới hạn mà thiết kế phải nằm dưới.

Các phép đọc nằm ngoài quy tắc: `account_info`, `account_data_at`, `account_u64_at`, `account_lamports`, `pdas_batch_derive` và `HelperProgram.user_balance` / `allowance_of` là các phép đọc từ những tài khoản đã được nạp sẵn (không có CPI), được phép ở cả hai track và trong cả hai chế độ thực thi. Hãy đặt các kiểm tra trước của bạn lên chúng. Một quy tắc về thứ tự: một khi một thay đổi cached đã được kích hoạt trong một giao dịch, các phép đọc track legacy sẽ bị chặn trong phần còn lại của giao dịch đó — hãy đọc trước, rồi mới thay đổi.

## 5. Tính nguyên tử là ngân sách

Một giao dịch Rome nguyên tử là một giao dịch Solana: khoảng **1,4 triệu compute units**, một **danh sách tài khoản** 64 địa chỉ và một **4 KB** kích thước wire (giao dịch Solana v1). Chương trình Rome và CPI của bạn chia sẻ ngân sách đó. Một lần chuyển của wrapper tiêu tốn khoảng 170K CU trên Hadrian và instruction của bạn cộng thêm phần riêng; hãy đo bằng `computeUnitsConsumed`của biên nhận Solana, không phải EVM `gasUsed`.

Khi mô phỏng quyết định một giao dịch không vừa, Rome sẽ chạy nó **lặp lại** qua nhiều giao dịch Solana, và một CPI legacy bên trong nó khi đó sẽ thất bại. Hãy giữ mỗi thao tác deposit và withdraw chỉ gồm một lần di chuyển token cộng với một CPI vào chương trình của bạn, và giữ phần Solidity xung quanh chúng nhỏ. Chia bất kỳ thứ gì lớn hơn thành hai giao dịch của người dùng.

Độ sâu CPI: Solana cho phép 4 cấp và Rome dùng một, vì vậy instruction của bạn chạy ở cấp 2 với hai cấp bên dưới nó (chương trình của bạn → SPL Token còn lại một).

## 6. Cấp phát tài khoản

Chương trình của bạn không nhận các tài khoản từ Solidity ở runtime. Proxy **mô phỏng** mọi giao dịch trước khi gửi, ghi lại mọi tài khoản Solana mà các CPI đã chạm tới, và đưa chúng vào giao dịch Solana. Adapter của bạn đặt tên các tài khoản trong `invoke_signed` danh sách tài khoản; mô phỏng chứng minh rằng chúng phân giải được. Ba điều sau đây rút ra:

* Mọi tài khoản mà instruction của bạn chạm tới phải có thể suy ra từ các input mà adapter có (PDA và sub-PDA của chính nó, program ID của bạn, mint, địa chỉ người dùng). `SystemProgram.find_program_address` và `CpiProgram.pdas_batch_derive` thực hiện các phép suy ra trong Solidity.
* Một tài khoản chưa tồn tại (một vị thế mới, một ATA mới) vẫn phải được nêu trong danh sách tài khoản; địa chỉ của nó là thứ đi vào giao dịch Solana.
* Mọi thứ mà giao dịch chạm tới — bao gồm các tài khoản của chính Rome cho người dùng, adapter và wrapper — phải nằm dưới 64 địa chỉ. Một thiết kế chạm tới mười thị trường trong một lần gọi sẽ không vừa.

Đối với lane ví Solana, `rome_emulateCallAccounts` cung cấp cùng cơ chế khám phá cho client; SDK sử dụng nó.

Hai tài khoản mà một CPI không bao giờ được nêu tên: fee payer Solana gửi giao dịch (trừ bên trong một instruction chuyển System thuần) và chính chương trình Rome. Một CPI quay ngược vào chương trình Rome sẽ bị từ chối.

## 7. Một deposit và một withdraw, bằng Solidity

Một phác thảo tối thiểu của một adapter trên track legacy. Người dùng đã gửi `approve_spl(adapter, amount, mint)` đến HelperProgram (mục 3). `PRIVACY_PROGRAM` là program ID của bạn; `encodeDeposit` / `encodeWithdraw` xây dựng dữ liệu instruction của bạn (Borsh, little-endian); các account meta theo bố cục của instruction của bạn.

```solidity
import {ICrossProgramInvocation, IHelperProgram}
    from "@rome-protocol/rome-solidity/contracts/interface.sol";

contract PrivacyAdapter {
    ICrossProgramInvocation constant Cpi    = ICrossProgramInvocation(0xFF00000000000000000000000000000000000008);
    IHelperProgram          constant Helper = IHelperProgram(0xFF00000000000000000000000000000000000009);
    bytes32 constant SPL_TOKEN = 0x06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9; // TokenkegQ...

    bytes32 immutable PRIVACY_PROGRAM;
    bytes32 immutable USDC_MINT;

    constructor(bytes32 program, bytes32 mint) {
        PRIVACY_PROGRAM = program; USDC_MINT = mint;
        Helper.create_pda(address(this));                 // danh tính Solana của chính adapter
        Helper.create_ata(address(this), USDC_MINT);      // và một tài khoản USDC mà nó sở hữu
    }

    function _salt(address user) internal pure returns (bytes32) { return bytes32(uint256(uint160(user))); }

    function deposit(uint64 amount) external {
        bytes32 self       = Helper.pda(address(this));                   // signer: quyền của adapter
        bytes32 adapterAta = Helper.ata(address(this), USDC_MINT);
        bytes32 position   = Helper.pda_with_salt(address(this), _salt(msg.sender)); // sub-PDA theo từng người dùng, cũng ký

        // Kéo từ tài khoản token của người dùng. Quyền của adapter ký với tư cách SPL
        // delegate mà người dùng đã cấp; đây là một lời gọi track legacy, giống như CPI bên dưới.
        Helper.transfer_spl(Helper.ata(msg.sender, USDC_MINT), adapterAta, amount, USDC_MINT);

        ICrossProgramInvocation.AccountMeta[] memory accs = new ICrossProgramInvocation.AccountMeta[](5);
        accs[0] = ICrossProgramInvocation.AccountMeta(self,       true,  true);
        accs[1] = ICrossProgramInvocation.AccountMeta(position,   true,  true);
        accs[2] = ICrossProgramInvocation.AccountMeta(adapterAta, false, true);                          // nguồn: thuộc về quyền của adapter
        accs[3] = ICrossProgramInvocation.AccountMeta(_vaultFor(position), false, true);                 // kho lưu trữ của chương trình bạn
        accs[4] = ICrossProgramInvocation.AccountMeta(SPL_TOKEN, false, false);

        bytes32[] memory salts = new bytes32[](1);
        salts[0] = _salt(msg.sender);
        Cpi.invoke_signed(PRIVACY_PROGRAM, accs, encodeDeposit(amount), salts);
    }

    function withdraw(uint64 amount) external {
        bytes32 position = Helper.pda_with_salt(address(this), _salt(msg.sender));
        bytes32 userAta  = Helper.ata(msg.sender, USDC_MINT);            // đích đến: tài khoản của chính người dùng
        // ... cùng dạng: chương trình của bạn chuyển `amount` từ vault sang userAta, được ký bởi `position`
        Cpi.invoke_signed(PRIVACY_PROGRAM, accs, encodeWithdraw(amount), salts);
    }
}
```

Adapter không bao giờ gọi `wUSDC` hợp đồng. Lệnh kéo là một lời gọi HelperProgram dùng quyền delegation của người dùng, và các token nằm trong một tài khoản token mà `external_auth(adapter)` sở hữu, đó là lý do vì sao CPI có thể chi tiêu chúng.

Ghi chú về signer. Rome ký cho chính xác hai loại tài khoản: quyền tối thiểu của hợp đồng gọi (`Helper.pda(address(this))`), ký mà không cần được liệt kê trong `salt`và một sub-PDA có salt cho mỗi mục trong `salt`. Mọi salt bạn liệt kê phải xuất hiện trong danh sách tài khoản như một signer, nếu không lời gọi sẽ thất bại với `tài khoản signer được tính từ seed đã cung cấp không được tìm thấy`. Bất kỳ tài khoản nào khác được đánh dấu là signer, bao gồm quyền của chính người dùng, sẽ không nhận seed nào từ Rome, và runtime Solana từ chối CPI. Quyền của người dùng đối với token của họ đến từ delegation, không bao giờ từ việc adapter ký thay cho người dùng.

Delegation không cần kéo: nếu chương trình của bạn nên chi tiêu thẳng từ tài khoản token của người dùng, người dùng gửi `HelperProgram.approve_spl_raw_delegate(userAta, yourProgramPda, amount, mint, decimals)` trực tiếp đến HelperProgram, chỉ định PDA của chương trình bạn làm SPL delegate. Cầu Rome dùng cách này cho Wormhole. Quy tắc một delegate từ mục 3 cũng áp dụng ở đây: quyền cấp này thay thế quyền của wrapper.

## 8. Kiểm thử

* Triển khai trên **Hadrian** (testnet, chain 200010) trước; địa chỉ và RPC ở trong [Địa chỉ Hợp đồng](/vi/tai-lieu-tham-khao/contract-addresses.md) và các [registry](https://github.com/rome-protocol/rome-registry).
* `eth_call` và `eth_estimateGas` chạy cùng phép mô phỏng mà proxy dùng để gửi, nên một CPI thất bại ở đó cũng sẽ thất bại trên chuỗi vì cùng lý do, và dữ liệu hoàn tác chứa lỗi Solana cơ bản (`lỗi gọi không phải EVM: …` trong nhật ký chương trình Solana).
* Đọc phía Solana của một giao dịch EVM đã được ghi nhận bằng `rome_solanaTxForEvmTx(hash)`; biên lai Solana là nơi chứa CU và nhật ký của chương trình bạn.
* Một CPI hoạt động trong `eth_call` nhưng bị từ chối khi gửi thường đã chạm một giới hạn mà mô phỏng báo cáo: giới hạn 64 địa chỉ, ngân sách CU nguyên tử (giao dịch đã chuyển sang lặp), hoặc quy tắc theo dõi.

## 9. Tham khảo

* [Tài liệu tham chiếu precompile](/vi/tai-lieu-tham-khao/precompiles.md) — mọi selector trên mọi precompile, cùng lớp dispatch và quy tắc delegatecall.
* [Ràng buộc](/vi/cac-khai-niem-cot-loi/constraints.md) — các giới hạn ở trên trong một bảng.
* [Tương tác token](/vi/cac-khai-niem-cot-loi/token-interop.md) — wrapper, mint chuẩn, nạp và rút.
* Các ứng dụng tham khảo trong các kho lưu trữ công khai: [cardo](https://github.com/rome-protocol/cardo) (người dùng gọi trực tiếp precompile CPI, dạng B), [rome-dex](https://github.com/rome-protocol/rome-dex) (trực tiếp `invoke` cùng một bộ định tuyến không lưu ký, dạng C), và [aerarium](https://github.com/rome-protocol/aerarium) (một ứng dụng cho vay Compound III trên một thị trường dùng chung; nó không gọi các chương trình Solana từ Solidity (không có CPI), nên không phải là ví dụ về một adapter).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.rome.builders/vi/huong-dan-cho-nha-phat-trien/integrate-solana-program.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
