1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-05-29 20:28:45 +00:00

refactor(core/solana): restructure programs.json and related code

This commit is contained in:
matejcik 2025-04-25 14:05:29 +02:00 committed by matejcik
parent e3af93e89f
commit e4e6d60e64
13 changed files with 1359 additions and 3416 deletions

File diff suppressed because it is too large Load Diff

View File

@ -12,14 +12,14 @@ _This file is generated by `programs.md.mako` via `make solana_templates`, do no
| Deposit | `lamports` | `lamports` |
| From | `funding_account` | `account` |
| _(not shown)_ | `space` | `u64` |
| _(not shown)_ | `owner` | `authority` |
| _(not shown)_ | `owner` | `pubkey` |
### (1) Assign
| Label | Value | Type |
|-------|-------|------|
| Assign account | `assigned_account` | `account` |
| Assign account to program | `owner` | `authority` |
| Assign account to program | `owner` | `pubkey` |
### (2) Transfer
@ -66,7 +66,7 @@ _This file is generated by `programs.md.mako` via `make solana_templates`, do no
| Label | Value | Type |
|-------|-------|------|
| Initialize nonce account | `nonce_account` | `account` |
| New authority | `nonce_authority` | `authority` |
| New authority | `nonce_authority` | `pubkey` |
| _(not shown)_ | `recent_blockhashes_sysvar` | `account` |
| _(not shown)_ | `rent_sysvar` | `account` |
@ -75,7 +75,7 @@ _This file is generated by `programs.md.mako` via `make solana_templates`, do no
| Label | Value | Type |
|-------|-------|------|
| Set nonce authority | `nonce_account` | `account` |
| New authority | `nonce_authority` | `authority` |
| New authority | `nonce_authority` | `pubkey` |
| Authorized by | `nonce_authority` | `account` |
### (8) Allocate
@ -132,11 +132,11 @@ _This file is generated by `programs.md.mako` via `make solana_templates`, do no
| Label | Value | Type |
|-------|-------|------|
| Initialize stake account | `uninitialized_stake_account` | `account` |
| New stake authority | `staker` | `authority` |
| New withdraw authority | `withdrawer` | `authority` |
| New stake authority | `staker` | `pubkey` |
| New withdraw authority | `withdrawer` | `pubkey` |
| Lockup time | `unix_timestamp` | `unix_timestamp` |
| Lockup epoch | `epoch` | `u64` |
| Lockup authority | `custodian` | `authority` |
| Lockup authority | `custodian` | `pubkey` |
| _(not shown)_ | `rent_sysvar` | `account` |
### (1) Authorize
@ -340,7 +340,7 @@ _This file is generated by `programs.md.mako` via `make solana_templates`, do no
| Label | Value | Type |
|-------|-------|------|
| Set authority for | `mint_account` | `account` |
| New authority | `new_authority` | `authority` |
| New authority | `new_authority` | `pubkey` |
| Authority type | `authority_type` | `AuthorityType` |
| Current authority | `current_authority` | `account` |
@ -509,7 +509,7 @@ _This file is generated by `programs.md.mako` via `make solana_templates`, do no
| Label | Value | Type |
|-------|-------|------|
| Set authority for | `mint_account` | `account` |
| New authority | `new_authority` | `authority` |
| New authority | `new_authority` | `pubkey` |
| Authority type | `authority_type` | `AuthorityType` |
| Current authority | `current_authority` | `account` |

View File

@ -9,7 +9,7 @@ ${'##'} ${program.name}
${'###'} (${instruction.id}) ${instruction.name}
<%
all_params = { param.name: param for param in instruction.parameters }
all_accounts = [ref.name for ref in instruction.references]
all_accounts = list(instruction.references)
%>
| Label | Value | Type |
|-------|-------|------|

View File

@ -168,14 +168,22 @@ The `programs.json` file serves as a structured configuration file in the Solana
- `name`: The parameter name.
- `type`: The data type of the parameter, such as `u64` for 64-bit unsigned integers.
- `optional`: Indicates whether the parameter is optional.
- `references`: Defines the references to accounts that this instruction requires.
- `name`: The account name.
- `is_authority`: A boolean specifying whether the account is considered an authority for this instruction.
- `optional`: Indicates whether the account is optional.
- `args`: An optional dict of arguments for the formatter, see explanation below.
- `references`: An array of account names that are used by the instruction.
- `references_required`: The number of references required by the instruction. If more `references` are specified than required, the extra ones are optional, and may or may not be present in the transaction.
- `ui_properties`: Contains user interface-related information for this instruction.
- `account`: Reference to one account in the references list identified by its `name`
- `parameter`: Reference to one parameter in the parameters list identified by its `name`
- `display_name`: A human-readable label for the parameter or account, suitable for user interfaces.
- `default_value_to_hide`: Optional. If this value is found in the account / parameter, the UI property will not be shown for confirmation. This is useful when the default value is considered safe. In particular, if the value of the property is a public key, and the special word `"signer"` is used for `default_value_to_hide`, the UI property will be hidden if the public key matches the Trezor's account.
Certain types of parameters, specified in `types` dict of the `programs.json` file, have special formatting capabilities.
In particular, the type `token_amount` is a regular `u64` type, but the formatter function accepts additional parameters:
* a special parameter `#definitions` that will be pre-set to the loadable definitions manager
* a parameter `decimals` that specifies the number of decimals of the token
* a parameter `mint` that specifies the mint address of the token
The corresponding parameter of `token_amount` type must provide the `args` dict, mapping the `decimals` and `mint` arguments to fields of the instruction. E.g.: in a hypothetical Swap instruction, you would have two parameters of `token_amount` type. On the first one, the `args` dict would map `decimals` to the `from_amount_decimals` field and `mint` to the `from_amount_mint` field. On the second one, the mapping would go to the `to_amount_decimals` and `to_amount_mint` fields.
After the message has been parsed, the Solana app utilizes the Trezor UI engine to present all the necessary information to the user for review and confirmation. If all the programs and instructions contained within the message are recognized and known, the software ensures that all the relevant information is displayed to the user. Each piece of data, including parameters, account references, and instruction details, is presented on the Trezor's user interface for user confirmation.

View File

@ -3,10 +3,10 @@ from typing import TYPE_CHECKING
from trezor.strings import format_amount, format_timestamp
if TYPE_CHECKING:
from .transaction.instructions import Instruction
from .definitions import Definitions
def format_pubkey(_: Instruction, value: bytes | None) -> str:
def format_pubkey(value: bytes | None) -> str:
from trezor.crypto import base58
if value is None:
@ -15,25 +15,31 @@ def format_pubkey(_: Instruction, value: bytes | None) -> str:
return base58.encode(value)
def format_lamports(_: Instruction, value: int) -> str:
def format_lamports(value: int) -> str:
formatted = format_amount(value, decimals=9)
return f"{formatted} SOL"
def format_token_amount(instruction: Instruction, value: int) -> str:
assert hasattr(instruction, "decimals") # enforced in instructions.py.mako
def format_token_amount(
value: int, definitions: Definitions, decimals: int, mint: bytes
) -> str:
formatted = format_amount(value, decimals=decimals)
token = definitions.get_token(mint)
if token:
symbol = token.symbol
else:
symbol = "[UNKN]"
formatted = format_amount(value, decimals=instruction.decimals)
return f"{formatted}"
return f"{formatted} {symbol}"
def format_unix_timestamp(_: Instruction, value: int) -> str:
def format_unix_timestamp(value: int) -> str:
return format_timestamp(value)
def format_int(_: Instruction, value: int) -> str:
def format_int(value: int) -> str:
return str(value)
def format_identity(_: Instruction, value: str) -> str:
def format_identity(value: str) -> str:
return value

View File

@ -84,45 +84,57 @@ async def confirm_instruction(
property_template = instruction.get_property_template(ui_property.parameter)
value = instruction.parsed_data[ui_property.parameter]
if property_template.is_authority and signer_public_key == value:
continue
if property_template.is_optional and value is None:
if property_template.optional and value is None:
continue
if ui_property.default_value_to_hide == value:
continue
if (
property_template.is_pubkey()
and ui_property.default_value_to_hide == "signer"
and signer_public_key == value
):
continue
args = []
for arg in property_template.args:
if arg == "#definitions":
args.append(definitions)
elif arg in instruction.parsed_data:
args.append(instruction.parsed_data[arg])
elif arg in instruction.parsed_accounts:
args.append(instruction.parsed_accounts[arg][0])
else:
raise ValueError # Invalid property template
await confirm_properties(
"confirm_instruction",
f"{instruction_index}/{instructions_count}: {instruction.ui_name}",
(
(
ui_property.display_name,
property_template.format(instruction, value),
property_template.format(value, *args),
),
),
)
elif ui_property.account is not None:
account_template = instruction.get_account_template(ui_property.account)
# optional account, skip if not present
if ui_property.account not in instruction.parsed_accounts:
continue
account_value = instruction.parsed_accounts[ui_property.account]
if account_template.is_authority:
if signer_public_key == account_value[0]:
if ui_property.default_value_to_hide == "signer" and signer_public_key == account_value[0]:
continue
account_data: list[tuple[str, str]] = []
# account included in the transaction directly
if len(account_value) == 2:
account_description = f"{base58.encode(account_value[0])}"
if account_template.is_token_mint:
token = definitions.get_token(account_value[0])
account_description = f"{token.symbol}\n{account_description}"
if token is not None:
account_description = f"{token.name}\n{account_description}"
elif account_value[0] == signer_public_key:
account_description = f"{account_description} ({TR.words__signer})"

View File

@ -5,25 +5,20 @@ if TYPE_CHECKING:
from typing_extensions import Self
from ..types import (
Account,
AccountTemplate,
InstructionData,
PropertyTemplate,
UIProperty,
)
from ..types import Account, InstructionData, PropertyTemplate, UIProperty
class Instruction:
program_id: str
instruction_id: int | None
property_templates: list[PropertyTemplate]
accounts_template: list[AccountTemplate]
property_templates: tuple[PropertyTemplate, ...]
accounts_required: int
account_templates: tuple[str, ...]
ui_name: str
ui_properties: list[UIProperty]
ui_properties: tuple[UIProperty, ...]
parsed_data: dict[str, Any]
parsed_accounts: dict[str, Account]
@ -40,7 +35,8 @@ class Instruction:
@staticmethod
def parse_instruction_data(
instruction_data: InstructionData, property_templates: list[PropertyTemplate]
instruction_data: InstructionData,
property_templates: tuple[PropertyTemplate, ...],
) -> dict[str, Any]:
from trezor.utils import BufferReader
from trezor.wire import DataError
@ -50,7 +46,7 @@ class Instruction:
parsed_data = {}
for property_template in property_templates:
is_included = True
if property_template.is_optional:
if property_template.optional:
is_included = True if reader.get() == 1 else False
parsed_data[property_template.name] = (
@ -64,18 +60,19 @@ class Instruction:
@staticmethod
def parse_instruction_accounts(
accounts: list[Account], accounts_template: list[AccountTemplate]
accounts: list[Account],
accounts_required: int,
account_templates: tuple[str, ...],
) -> dict[str, Account]:
parsed_account = {}
for i, account_template in enumerate(accounts_template):
if i >= len(accounts):
if account_template.optional:
continue
else:
parsed_accounts = {}
if len(accounts) < accounts_required:
raise ValueError # "Account is missing
parsed_account[account_template.name] = accounts[i]
return parsed_account
for i, account_name in enumerate(account_templates):
if i >= len(accounts):
break
parsed_accounts[account_name] = accounts[i]
return parsed_accounts
def __init__(
self,
@ -83,9 +80,10 @@ class Instruction:
program_id: str,
accounts: list[Account],
instruction_id: int | None,
property_templates: list[PropertyTemplate],
accounts_template: list[AccountTemplate],
ui_properties: list[UIProperty],
property_templates: tuple[PropertyTemplate, ...],
accounts_required: int,
account_templates: tuple[str, ...],
ui_properties: tuple[UIProperty, ...],
ui_name: str,
is_program_supported: bool = True,
is_instruction_supported: bool = True,
@ -97,7 +95,8 @@ class Instruction:
self.instruction_id = instruction_id
self.property_templates = property_templates
self.accounts_template = accounts_template
self.accounts_required = accounts_required
self.account_templates = account_templates
self.ui_name = ui_name
@ -118,10 +117,12 @@ class Instruction:
)
self.parsed_accounts = self.parse_instruction_accounts(
accounts, accounts_template
accounts,
accounts_required,
account_templates,
)
self.multisig_signers = accounts[len(accounts_template) :]
self.multisig_signers = accounts[len(account_templates) :]
if self.multisig_signers and not supports_multisig:
raise ValueError # Multisig not supported
else:
@ -144,13 +145,6 @@ class Instruction:
raise ValueError # Property not found
def get_account_template(self, account_name: str) -> AccountTemplate:
for account_template in self.accounts_template:
if account_template.name == account_name:
return account_template
raise ValueError # Account not found
@classmethod
def is_type_of(cls, ins: Any) -> TypeGuard[Self]:
# gets overridden in `instructions.py` `FakeClass`

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,41 @@
# generated from instructions.py.mako
# (by running `make solana_templates` in root)
# do not edit manually!
<%def name="getProgramId(program)">${"_" + "_".join(program["name"].upper().split(" ") + ["ID"])}</%def>\
<%def name="getInstructionIdText(program, instruction)">${"_".join([getProgramId(program)] + ["INS"] + instruction["name"].upper().split(" "))}</%def>\
<%def name="getClassName(program, instruction)">${program["name"].replace(" ", "")}${instruction["name"].replace(" ", "")}Instruction</%def>\
<%def name="getReferenceName(reference)">${"_".join(reference["name"].lower().split(" "))}</%def>\
<%def name="getReferenceOptionalType(reference)">\
% if reference["optional"]:
| None\
% endif
</%def>\
<%def name="getReferenceOptionalTemplate(reference)">\
% if reference["optional"]:
, True\
% else:
, False\
% endif
</%def>\
<%def name="getPythonType(type)">\
% if type in ("u32", "u64", "i32", "i64", "timestamp", "lamports", "token_amount"):
int\
% elif type in ("pubKey", "authority"):
Account\
% elif type in ("string", "memo"):
str\
% else:
int\
% endif
</%def>\
<%
def getProgramId(program):
return "_" + "_".join(program["name"].upper().split(" ") + ["ID"])
def getInstructionIdText(program, instruction):
return "_".join([getProgramId(program)] + ["INS"] + instruction["name"].upper().split(" "))
def getClassName(program, instruction):
return program["name"].replace(" ", "") + instruction["name"].replace(" ", "") + "Instruction"
INT_TYPES = ("u8", "u32", "u64", "i32", "i64", "timestamp", "lamports", "token_amount", "unix_timestamp")
def getPythonType(type):
if type in INT_TYPES:
return "int"
elif type in ("pubkey", "authority"):
return "Account"
elif type in ("string", "memo"):
return "str"
elif type in programs["types"] and programs["types"][type].get("is_enum"):
return "int"
else:
raise Exception(f"Unknown type: {type}")
def args_tuple(required_parameters, args_dict):
args = []
for required_parameter in required_parameters:
if required_parameter.startswith("#"):
args.append(required_parameter)
else:
args.append(args_dict[required_parameter])
return repr(tuple(args))
%>\
from micropython import const
from typing import TYPE_CHECKING
@ -34,7 +43,7 @@ from trezor.wire import DataError
from apps.common.readers import read_uint32_le, read_uint64_le
from ..types import AccountTemplate, PropertyTemplate, UIProperty
from ..types import PropertyTemplate, UIProperty
from ..format import (
format_int,
format_lamports,
@ -106,8 +115,11 @@ if TYPE_CHECKING:
% endfor
## generates properties for reference accounts
% for reference in instruction["references"]:
${getReferenceName(reference)}: Account${getReferenceOptionalType(reference)}
% for reference in instruction["references"][:instruction["references_required"]]:
${reference}: Account
% endfor
% for reference in instruction["references"][instruction["references_required"]:]:
${reference}: Account | None
% endfor
% endfor
% endfor
@ -123,7 +135,7 @@ def get_instruction_id_length(program_id: str) -> int:
% for _, type in programs["types"].items():
% if "is_enum" in type and type["is_enum"]:
def ${type["format"]}(_: Instruction, value: int) -> str:
def ${type["format"]}(value: int) -> str:
% for variant in type["fields"]:
if value == ${variant["value"]}:
return "${variant["name"]}"
@ -132,23 +144,24 @@ def ${type["format"]}(_: Instruction, value: int) -> str:
% endif
% endfor
<%def name="getOptionalString(obj, string)">\
% if string in obj:
"${obj[string]}"\
%else:
None\
% endif
</%def>\
<%
# Make sure that all required parameters are present in the instruction.
for program in programs["programs"]:
for instruction in program["instructions"]:
param_names = [parameter["name"] for parameter in instruction["parameters"]]
for parameter in instruction["parameters"]:
if "required_parameters" in programs["types"][parameter["type"]]:
for required_parameter in programs["types"][parameter["type"]]["required_parameters"]:
instruction_parameter_names = [parameter["name"] for parameter in instruction["parameters"]]
if required_parameter not in instruction_parameter_names:
raise Exception(f"Instruction \"{instruction['name']}\" is missing the required parameter \"{required_parameter}\" from paremeter \"{parameter['name']}\".")
required_parameters = programs["types"][parameter["type"]].get("required_parameters")
if not required_parameters:
continue
args = parameter.get("args", {})
for required_parameter in required_parameters:
if required_parameter.startswith("#"):
continue
if required_parameter not in args:
raise Exception(f"Parameter \"{parameter['name']}\" is missing the required argument \"{required_parameter}\".")
target = args[required_parameter]
if target not in param_names and target not in instruction["references"]:
raise Exception(f"Instruction \"{instruction['name']}\" is missing the required parameter \"{required_parameter}\" from parameter \"{parameter['name']}\".")
%>
def get_instruction(
@ -164,44 +177,35 @@ def get_instruction(
program_id,
instruction_accounts,
${getInstructionIdText(program, instruction)},
[
(
% for parameter in instruction["parameters"]:
PropertyTemplate(
"${parameter["name"]}",
${parameter["type"] == "authority"},
${parameter["optional"]},
${programs["types"][parameter["type"]]["parse"]},
${programs["types"][parameter["type"]]["format"]},
${args_tuple(programs["types"][parameter["type"]].get("required_parameters", []), parameter.get("args", {}))},
),
% endfor
],
[
% for reference in instruction["references"]:
AccountTemplate(
"${reference["name"]}",
${reference["is_authority"]},
${reference["optional"]},
${reference.get("is_token_mint", False)},
),
% endfor
],
[
${instruction["references_required"]},
${repr(tuple(instruction["references"]))},
(
% for ui_property in instruction["ui_properties"]:
UIProperty(
${getOptionalString(ui_property, "parameter")},
${getOptionalString(ui_property, "account")},
${repr(ui_property.get("parameter"))},
${repr(ui_property.get("account"))},
"${ui_property["display_name"]}",
${ui_property["is_authority"] if "is_authority" in ui_property else False},
${ui_property["default_value_to_hide"] if "default_value_to_hide" in ui_property else None},
${repr(ui_property.get("default_value_to_hide"))},
),
% endfor
],
),
"${program["name"]}: ${instruction["name"]}",
True,
True,
${instruction.get("is_ui_hidden", False)},
${instruction["is_multisig"]},
${getOptionalString(instruction, "is_deprecated_warning")},
${repr(instruction.get("is_deprecated_warning"))},
)
% endfor
return Instruction(
@ -209,9 +213,10 @@ def get_instruction(
program_id,
instruction_accounts,
instruction_id,
[],
[],
[],
(),
0,
(),
(),
"${program["name"]}",
True,
False,
@ -225,9 +230,10 @@ def get_instruction(
program_id,
instruction_accounts,
0,
[],
[],
[],
(),
0,
(),
(),
"Unsupported program",
False,
False,

View File

@ -1,6 +1,7 @@
from typing import TYPE_CHECKING
from .definitions import Definitions
from .transaction.parse import parse_pubkey
if TYPE_CHECKING:
from enum import IntEnum
@ -10,8 +11,6 @@ if TYPE_CHECKING:
from trezor.utils import BufferReader
from typing_extensions import Self
from .transaction import Instruction
Address = tuple[bytes, "AddressType"]
AddressReference = tuple[bytes, int, "AddressType"]
Account = Address | AddressReference
@ -42,26 +41,19 @@ class PropertyTemplate(Generic[T]):
def __init__(
self,
name: str,
is_authority: bool,
is_optional: bool,
optional: bool,
parse: Callable[[BufferReader], T],
format: Callable[[Instruction, T], str],
format: Callable[..., str],
args: tuple[str, ...],
) -> None:
self.name = name
self.is_authority = is_authority
self.is_optional = is_optional
self.optional = optional
self.parse = parse
self.format = format
self.args = args
class AccountTemplate:
def __init__(
self, name: str, is_authority: bool, optional: bool, is_token_mint: bool
) -> None:
self.name = name
self.is_authority = is_authority
self.optional = optional
self.is_token_mint = is_token_mint
def is_pubkey(self) -> bool:
return self.parse is parse_pubkey
class UIProperty:
@ -70,13 +62,11 @@ class UIProperty:
parameter: str | None,
account: str | None,
display_name: str,
is_authority: bool,
default_value_to_hide: Any | None,
) -> None:
self.parameter = parameter
self.account = account
self.display_name = display_name
self.is_authority = is_authority
self.default_value_to_hide = default_value_to_hide

View File

@ -99,7 +99,8 @@ def create_mock_instruction(
accounts=[],
instruction_id=instruction_id,
property_templates=[],
accounts_template=[],
accounts_required=0,
account_templates=[],
ui_properties=[],
ui_name="",
is_program_supported=True,

View File

@ -28,7 +28,8 @@ def create_mock_instruction(
accounts=[],
instruction_id=instruction_id,
property_templates=[],
accounts_template=[],
accounts_required=0,
account_templates=[],
ui_properties=[],
ui_name="",
is_program_supported=True,

View File

@ -16,24 +16,31 @@ CONSTRUCT_TYPES = {
"string": "String",
"memo": "Memo",
}
INSTRUCTION_TYPES = {
0: "Pass",
1: "Byte",
4: "Int32ul",
}
def upper_snake_case(name):
return "_".join(name.split(" ")).upper()
def camelcase(name):
return "".join([word.capitalize() for word in name.split(" ")])
def instruction_id(instruction):
return "INS_" + upper_snake_case(instruction.name)
def instruction_struct_name(program, instruction):
return camelcase(program.name) + "_" + camelcase(instruction.name) + "_Instruction"
def instruction_subcon(program, instruction):
if instruction.id is None:
return "Pass"
instruction_id_type = INSTRUCTION_TYPES[program.instruction_id_length]
return f"Const({instruction.id}, {instruction_id_type})"
%>\
from enum import Enum
from construct import (
@ -76,12 +83,11 @@ class ${camelcase(program.name)}Instruction(Enum):
${camelcase(program.name)}_${camelcase(instruction.name)} = Struct(
"program_index" / Byte,
"accounts" / CompactStruct(
% for reference in instruction.references:
% if reference.optional:
"${reference.name}" / Optional(Byte),
% else:
"${reference.name}" / Byte,
% endif
% for reference in instruction.references[:instruction.references_required]:
"${reference}" / Byte,
% endfor
% for reference in instruction.references[instruction.references_required:]:
"${reference}" / Optional(Byte),
% endfor
% if instruction.is_multisig:
"multisig_signers" / Optional(GreedyRange(Byte))