|
| 1 | +--- |
| 2 | +title: Configuring Extra Accounts |
| 3 | +--- |
| 4 | + |
| 5 | +As mentioned previously, programs who implement the Transfer Hook interface can |
| 6 | +provide additional custom functionality to token transfers. However, this |
| 7 | +functionality may require additional accounts beyond those that exist in a |
| 8 | +transfer instruction (source, mint, destination, etc.). |
| 9 | + |
| 10 | +Part of the Transfer Hook interface specification is the validation account - an |
| 11 | +account which stores configurations for additional accounts required by the |
| 12 | +transfer hook program. |
| 13 | + |
| 14 | +### The Validation Account |
| 15 | + |
| 16 | +The validation account is a PDA off of the transfer hook program derived from |
| 17 | +the following seeds: |
| 18 | + |
| 19 | +``` |
| 20 | +"extra-account-metas" + <mint-address> |
| 21 | +``` |
| 22 | + |
| 23 | +As you can see, one validation account maps to one mint account. This means you |
| 24 | +can customize the additional required accounts on a per-mint basis! |
| 25 | + |
| 26 | +The validation account stores configurations for extra accounts using |
| 27 | +[Type-Length-Value](https://en.wikipedia.org/wiki/Type%E2%80%93length%E2%80%93value) |
| 28 | +(TLV) encoding: |
| 29 | +- **Type:** The instruction discriminator, in this case `Execute` |
| 30 | +- **Length:** The total length of the subsequent data buffer, in this case a |
| 31 | + `u32` |
| 32 | +- **Data:** The data itself, in this case containing the extra account |
| 33 | + configurations |
| 34 | + |
| 35 | +When a transfer hook program seeks to deserialize extra account configurations |
| 36 | +from a validation account, it can find the 8-byte instruction discriminator for |
| 37 | +`Execute`, then read the length, then use that length to deserialize the data. |
| 38 | + |
| 39 | +The data itself is a list of fixed-size configuration objects serialized into a |
| 40 | +byte slab. Because the entries are fixed-length, we can use a custom "slice" |
| 41 | +structure which divides the length by the fixed-length to determine the number |
| 42 | +of entries. |
| 43 | + |
| 44 | +This custom slice structure is called a `PodSlice` and is part of the Solana |
| 45 | +Program Library's |
| 46 | +[Pod](https://github.com/solana-labs/solana-program-library/tree/master/libraries/pod) |
| 47 | +library. The Pod library provides a handful of fixed-length types that |
| 48 | +implement the `bytemuck` |
| 49 | +[`Pod`](https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html) trait, as well |
| 50 | +as the `PodSlice`. |
| 51 | + |
| 52 | +Another SPL library |
| 53 | +useful for Type-Length-Value encoded data is |
| 54 | +[Type-Length-Value](https://github.com/solana-labs/solana-program-library/tree/master/libraries/type-length-value) |
| 55 | +which is used extensively to manage TLV-encoded data structures. |
| 56 | + |
| 57 | +### Dynamic Account Resolution |
| 58 | + |
| 59 | +When clients build a transfer instruction to the token program, they must |
| 60 | +ensure the instruction includes all required accounts, especially the extra |
| 61 | +required accounts you've specified in the validation account. |
| 62 | + |
| 63 | +These additional accounts must be _resolved_, and another library used to pull off |
| 64 | +the resolution of additional accounts for transfer hooks is |
| 65 | +[TLV Account Resolution](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution). |
| 66 | + |
| 67 | +Using the TLV Account Resolution library, transfer hook programs can empower |
| 68 | +**dynamic account resolution** of additional required accounts. This means that |
| 69 | +no particular client or program needs to know the specific accounts your |
| 70 | +transfer hook requires. Instead, they can be automatically resolved from the |
| 71 | +validation account's data. |
| 72 | + |
| 73 | +In fact, the Transfer Hook interface offers helpers that perform this account |
| 74 | +resolution in the |
| 75 | +[onchain](https://github.com/solana-labs/solana-program-library/blob/master/token/transfer-hook/interface/src/onchain.rs) |
| 76 | +and |
| 77 | +[offchain](https://github.com/solana-labs/solana-program-library/blob/master/token/transfer-hook/interface/src/offchain.rs) |
| 78 | +modules of the Transfer Hook interface crate. |
| 79 | + |
| 80 | +The account resolution is powered by the way configurations for additional |
| 81 | +accounts are stored, and how they can be used to derive actual Solana addresses |
| 82 | +and roles (signer, writeable, etc.) for accounts. |
| 83 | + |
| 84 | +### The `ExtraAccountMeta` Struct |
| 85 | + |
| 86 | +A member of the TLV Account Resolution library, the |
| 87 | +[`ExtraAccountMeta`](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/libraries/tlv-account-resolution/src/account.rs#L75) |
| 88 | +struct allows account configurations to be serialized into a fixed-length data |
| 89 | +format of length 35 bytes. |
| 90 | + |
| 91 | +```rust |
| 92 | +pub struct ExtraAccountMeta { |
| 93 | + /// Discriminator to tell whether this represents a standard |
| 94 | + /// `AccountMeta` or a PDA |
| 95 | + pub discriminator: u8, |
| 96 | + /// This `address_config` field can either be the pubkey of the account |
| 97 | + /// or the seeds used to derive the pubkey from provided inputs |
| 98 | + pub address_config: [u8; 32], |
| 99 | + /// Whether the account should sign |
| 100 | + pub is_signer: PodBool, |
| 101 | + /// Whether the account should be writable |
| 102 | + pub is_writable: PodBool, |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +As the documentation on the struct conveys, an `ExtraAccountMeta` can store |
| 107 | +configurations for three types of accounts: |
| 108 | + |
| 109 | +|Discriminator|Account Type| |
| 110 | +|:------------|:-----------| |
| 111 | +|`0` | An account with a static address | |
| 112 | +| `1` | A PDA off of the transfer hook program itself | |
| 113 | +| `(1 << 7) + i ` | A PDA off of another program, where `i` is that program's index in the accounts list | |
| 114 | + |
| 115 | +`1 << 7` is the top bit of the `u8`, or `128`. If the program you are deriving |
| 116 | +this PDA from is at index `9` of the accounts list for `Execute`, then the |
| 117 | +discriminator for this account configuration is `128 + 9 = 137`. More on |
| 118 | +determining this index later. |
| 119 | + |
| 120 | +#### Accounts With Static Addresses |
| 121 | + |
| 122 | +Static-address additional accounts are straightforward to serialize with |
| 123 | +`ExtraAccountMeta`. The discriminator is simply `0` and the `address_config` is |
| 124 | +the 32-byte public key. |
| 125 | + |
| 126 | +#### PDAs Off the Transfer Hook Program |
| 127 | + |
| 128 | +You might be wondering: "how can I store all of my PDA seeds in only 32 bytes?". |
| 129 | +Well, you don't. Instead, you tell the account resolution functionality _where_ |
| 130 | +to find the seeds you need. |
| 131 | + |
| 132 | +To do this, the transfer hook program can use the |
| 133 | +[`Seed`](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/libraries/tlv-account-resolution/src/seeds.rs#L38) |
| 134 | +enum to describe their seeds and where to find them. With the exception of |
| 135 | +literals, these seed configurations comprise only a small handful of bytes. |
| 136 | + |
| 137 | +The following types of seeds are supported by the `Seed` enum and can be used to |
| 138 | +create an `address_config` array of bytes. |
| 139 | +- **Literal**: The literal seed itself encoded to bytes |
| 140 | +- **Instruction Data:** A slice of the instruction data, denoted by the `index` |
| 141 | + (offset) and `length` of bytes to slice |
| 142 | +- **AccountKey:** The address of some account in the list as bytes, denoted by |
| 143 | + the `index` at which this account can be found in the accounts list |
| 144 | +- **Account Data:** A slice of an account's data, denoted by the `account_index` |
| 145 | + at which this account can be found in the accounts list, as well as the |
| 146 | + `data_index` (offset) and `length` of bytes to slice |
| 147 | + |
| 148 | +Here's an example of packing a list of `Seed` entries into a 32-byte |
| 149 | +`address_config`: |
| 150 | + |
| 151 | +```rust |
| 152 | +let seed1 = Seed::Literal { bytes: vec![1; 8] }; |
| 153 | +let seed2 = Seed::InstructionData { |
| 154 | + index: 0, |
| 155 | + length: 4, |
| 156 | +}; |
| 157 | +let seed3 = Seed::AccountKey { index: 0 }; |
| 158 | +let address_config: [u8; 32] = Seed::pack_into_address_config( |
| 159 | + &[seed1, seed2, seed3] |
| 160 | +)?; |
| 161 | +``` |
| 162 | + |
| 163 | +#### PDAs Off Another Program |
| 164 | + |
| 165 | +Storing configurations for seeds for an address that is a PDA off of another |
| 166 | +program is the same as above. However, the program whose address this account is |
| 167 | +a PDA off of must be present in the account list. Its index in the accounts list |
| 168 | +is required to build the proper discriminator, and thus resolve the proper PDA. |
| 169 | + |
| 170 | +```rust |
| 171 | +let program_index = 7; |
| 172 | +let seeds = &[seed1, seed2, seed3]; |
| 173 | +let is_signer = false; |
| 174 | +let is_writable = true; |
| 175 | + |
| 176 | +let extra_meta = ExtraAccountMeta::new_external_pda_with_seeds( |
| 177 | + program_index, |
| 178 | + seeds, |
| 179 | + is_signer, |
| 180 | + is_writable, |
| 181 | +)?; |
| 182 | +``` |
| 183 | + |
0 commit comments