|
9 | 9 |
|
10 | 10 | This package provides a runtime Solana instruction builder that dynamically constructs `Instruction` (`@solana/instructions`). It provides instruction arguments encoding and validation, accounts resolution. Powers [`@codama/dynamic-client`](../dynamic-client/README.md) with `InstructionsBuilder`. |
11 | 11 |
|
| 12 | +It also provides a **clear-signing display** layer that turns a concrete instruction into human-readable text — see [Instruction display](#instruction-display-clear-signing). |
| 13 | + |
12 | 14 | ## Installation |
13 | 15 |
|
14 | 16 | ```sh |
@@ -112,3 +114,62 @@ import type { TransferArgs } from './generated/<idl-name>-instruction-types'; |
112 | 114 |
|
113 | 115 | const data = encodeInstructionArguments<TransferArgs>(root, ixNode, { amount: 1_000_000_000n }); |
114 | 116 | ``` |
| 117 | + |
| 118 | +## Instruction display (clear signing) |
| 119 | + |
| 120 | +Given an IDL enriched with `display` metadata (per [sRFC 39](https://github.com/solana-foundation/SRFCs/discussions/4)), this package resolves a concrete instruction into human-readable text for user verification. The result carries both presentation modes and lets the renderer choose: |
| 121 | + |
| 122 | +```ts |
| 123 | +type InstructionDisplay = { |
| 124 | + // A short imperative label, e.g. "Transfer" (derived from the instruction name when absent). |
| 125 | + intent: string; |
| 126 | + // The interpolated sentence, e.g. "Transfer 1.5 USDC to toly.sol", or `null` when a |
| 127 | + // placeholder cannot be resolved (the renderer then falls back to `fields`). |
| 128 | + interpolatedIntent: string | null; |
| 129 | + // The structured fallback list of labelled fields, e.g. [{ label: 'Amount', value: '1.5 USDC' }]. |
| 130 | + fields: { label: string; value: string }[]; |
| 131 | +}; |
| 132 | +``` |
| 133 | + |
| 134 | +### `getInstructionDisplay(root, instruction, options?)` |
| 135 | + |
| 136 | +Parses a raw `Instruction` (`@solana/instructions`) against the root and resolves its display. Returns `null` when the instruction cannot be identified or decoded (e.g. an instruction from an unknown program). |
| 137 | + |
| 138 | +```ts |
| 139 | +import { getInstructionDisplay } from '@codama/dynamic-instructions'; |
| 140 | + |
| 141 | +const display = await getInstructionDisplay(root, instruction); |
| 142 | +// => { intent: 'Transfer', interpolatedIntent: 'Transfer 1500000 to 3Wnd5…5PxJX', fields: [...] } | null |
| 143 | +``` |
| 144 | + |
| 145 | +### `getInstructionDisplayFromParsedInstruction(root, parsedInstruction, options?)` |
| 146 | + |
| 147 | +The same, starting from an already-parsed instruction (`ParsedInstruction` from [`@codama/dynamic-parsers`](../dynamic-parsers/README.md)). Useful when you have already called `parseInstruction`. |
| 148 | + |
| 149 | +```ts |
| 150 | +import { parseInstruction } from '@codama/dynamic-parsers'; |
| 151 | +import { getInstructionDisplayFromParsedInstruction } from '@codama/dynamic-instructions'; |
| 152 | + |
| 153 | +const parsed = parseInstruction(root, instruction); |
| 154 | +if (parsed) { |
| 155 | + const display = await getInstructionDisplayFromParsedInstruction(root, parsed); |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +### Options |
| 160 | + |
| 161 | +Some display values live in on-chain account state (e.g. a token's `decimals`/`symbol` injected into an amount, or interpolation paths that read an account field). Supply `fetchAccount` to resolve them; without it, such values degrade gracefully (amounts stay raw, `whenInjected` fields remain visible). |
| 162 | + |
| 163 | +`fetchAccount` returns Kit's `MaybeEncodedAccount` — an `exists` flag plus, when the account exists, its raw bytes. No decoding is required on your side: the display layer decodes the bytes itself using the referenced account's `accountLink` from the IDL, which already describes the layout. This makes `fetchEncodedAccount` a drop-in. |
| 164 | + |
| 165 | +```ts |
| 166 | +import type { Address } from '@solana/addresses'; |
| 167 | +import { fetchEncodedAccount } from '@solana/accounts'; |
| 168 | + |
| 169 | +const display = await getInstructionDisplay(root, instruction, { |
| 170 | + // Forward Kit's MaybeEncodedAccount for an address. |
| 171 | + fetchAccount: (address: Address) => fetchEncodedAccount(rpc, address), |
| 172 | +}); |
| 173 | +``` |
| 174 | + |
| 175 | +Address presentation (`.sol` names, address-book aliases, truncation) is intentionally left to the renderer: `fields` and `interpolatedIntent` contain raw base58 addresses that the consuming wallet/UI formats as it sees fit. |
0 commit comments