From 4f5d54346fa941e124afe4feff5a62284752db97 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 7 Jul 2026 08:27:03 +0100 Subject: [PATCH] Document the instruction display API in the README This PR documents the clear-signing instruction display API in the @codama/dynamic-instructions README. It adds an Instruction display section covering getInstructionDisplay and getInstructionDisplayFromParsedInstruction, the InstructionDisplay result shape (intent, interpolatedIntent, fields), and the fetchAccountData option for resolving account-sourced values, noting that address presentation is left to the renderer. No changeset is included, consistent with the rest of the 1.7.0 stack. --- packages/dynamic-instructions/README.md | 61 +++++++++++++++++++ .../display/get-instruction-display.test.ts | 9 ++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/packages/dynamic-instructions/README.md b/packages/dynamic-instructions/README.md index 8d3872e7e..871c370a2 100644 --- a/packages/dynamic-instructions/README.md +++ b/packages/dynamic-instructions/README.md @@ -9,6 +9,8 @@ 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`. +It also provides a **clear-signing display** layer that turns a concrete instruction into human-readable text — see [Instruction display](#instruction-display-clear-signing). + ## Installation ```sh @@ -112,3 +114,62 @@ import type { TransferArgs } from './generated/-instruction-types'; const data = encodeInstructionArguments(root, ixNode, { amount: 1_000_000_000n }); ``` + +## Instruction display (clear signing) + +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: + +```ts +type InstructionDisplay = { + // A short imperative label, e.g. "Transfer" (derived from the instruction name when absent). + intent: string; + // The interpolated sentence, e.g. "Transfer 1.5 USDC to toly.sol", or `null` when a + // placeholder cannot be resolved (the renderer then falls back to `fields`). + interpolatedIntent: string | null; + // The structured fallback list of labelled fields, e.g. [{ label: 'Amount', value: '1.5 USDC' }]. + fields: { label: string; value: string }[]; +}; +``` + +### `getInstructionDisplay(root, instruction, options?)` + +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). + +```ts +import { getInstructionDisplay } from '@codama/dynamic-instructions'; + +const display = await getInstructionDisplay(root, instruction); +// => { intent: 'Transfer', interpolatedIntent: 'Transfer 1500000 to 3Wnd5…5PxJX', fields: [...] } | null +``` + +### `getInstructionDisplayFromParsedInstruction(root, parsedInstruction, options?)` + +The same, starting from an already-parsed instruction (`ParsedInstruction` from [`@codama/dynamic-parsers`](../dynamic-parsers/README.md)). Useful when you have already called `parseInstruction`. + +```ts +import { parseInstruction } from '@codama/dynamic-parsers'; +import { getInstructionDisplayFromParsedInstruction } from '@codama/dynamic-instructions'; + +const parsed = parseInstruction(root, instruction); +if (parsed) { + const display = await getInstructionDisplayFromParsedInstruction(root, parsed); +} +``` + +### Options + +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). + +`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. + +```ts +import type { Address } from '@solana/addresses'; +import { fetchEncodedAccount } from '@solana/accounts'; + +const display = await getInstructionDisplay(root, instruction, { + // Forward Kit's MaybeEncodedAccount for an address. + fetchAccount: (address: Address) => fetchEncodedAccount(rpc, address), +}); +``` + +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. diff --git a/packages/dynamic-instructions/test/display/get-instruction-display.test.ts b/packages/dynamic-instructions/test/display/get-instruction-display.test.ts index 3707a904a..776ff05e2 100644 --- a/packages/dynamic-instructions/test/display/get-instruction-display.test.ts +++ b/packages/dynamic-instructions/test/display/get-instruction-display.test.ts @@ -161,8 +161,13 @@ describe('getInstructionDisplayFromParsedInstruction', () => { describe('getInstructionDisplay', () => { test('it returns null when the instruction cannot be parsed', async () => { - // Given a root whose program has no instruction matching the raw bytes. - const root = makeRoot([instructionNode({ accounts: [], arguments: [], name: 'noop' })]); + // Given a root whose program has no instruction matching the raw bytes. Two + // discriminator-less instructions make the single-candidate fallback ambiguous, so the + // parser cannot identify one. + const root = makeRoot([ + instructionNode({ accounts: [], arguments: [], name: 'noop' }), + instructionNode({ accounts: [], arguments: [], name: 'other' }), + ]); // When we resolve the display of an unrecognized instruction. const display = await getInstructionDisplay(root, {