| Crates.io | visualsign |
| lib.rs | visualsign |
| version | 0.1.0 |
| created_at | 2025-11-19 12:59:36.807302+00 |
| updated_at | 2025-11-19 12:59:36.807302+00 |
| description | Visualsign package defines the structures helper methods to create VisualSign payloads for Anchorage's Open Source VisualSign |
| homepage | |
| repository | https://github.com/anchorageoss/visualsign-parser |
| max_upload_size | |
| id | 1940085 |
| size | 586,928 |
This document provides specifications for the Visual Sign Protocol (VSP), a structured format for displaying transaction details to users for approval. The VSP is designed to present meaningful, human-readable information about operations requiring signatures.
The SignablePayload JSON format is NOT canonical. It should be treated by signers as an opaque string field. While we maintain deterministic ordering (currently alphabetical) for debugging consistency and cross-implementation compatibility, this is an implementation detail that may change. Signers should not parse or depend on the specific JSON structure or field ordering.
Display elements (wallets, signing interfaces) are responsible for:
FallbackText for each field must be displayedNotes
A SignablePayload is the core structure that defines what is displayed to the user during the signing process. It contains metadata about the transaction and a collection of fields representing the transaction details.
{
"Version": "0",
"Title": "Withdraw",
"Subtitle": "to 0x8a6e30eE13d06311a35f8fa16A950682A9998c71",
"Fields": [
{
"FallbackText": "1 ETH",
"Label": "Amount",
"Type": "amount_v2",
"AmountV2": {
"Amount": "1",
"Abbreviation": "ETH"
}
},
...
],
"EndorsedParamsDigest": "DEADBEEFDEADBEEFDEADBEEFDEADBEEF",
}
| Field | Type | Description |
|---|---|---|
| Version | String | Protocol version |
| Title | String | Primary title for the operation |
| Subtitle | String (optional) | Secondary descriptive text |
| PayloadType | String | Identifier for the SignablePayload (ex: Withdrawal, Swap, etc) |
| Fields | Array of SignablePayloadField | The fields containing transaction details |
| EndorsedParamsDigest | String (optional) | Digest of endorsed parameters |
The Visual Sign Protocol supports various field types to represent different kinds of data.
All field types include these common properties:
{
"Label": "Amount",
"FallbackText": "1 ETH",
"Type": "amount_v2"
}
| Field | Type | Description |
|---|---|---|
| Label | String | Field label shown to the user |
| FallbackText | String | Plain text representation (for limited clients) |
| Type | String | Type identifier for the field |
{
"Label": "Asset",
"FallbackText": "ETH | Ethereum",
"Type": "text_v2",
"TextV2": {
"Text": "ETH | Ethereum"
}
}
{
"Label": "Amount",
"FallbackText": "0.00001234",
"Type": "amount_v2",
"AmountV2": {
"Amount": "0.00001234",
"Abbreviation": "BTC"
}
}
Amount fields are user friendly ways to display the value being transferred
{
"Label": "Amount",
"FallbackText": "0.00001234 BTC",
"Type": "amount_v2",
"AmountV2": {
"Amount": "0.00001234",
"Abbreviation": "BTC"
}
}
{
"Label": "gasLimit",
"FallbackText": "21000",
"Type": "number",
"Number": {
"Value": "21000"
}
}
Divider fields are UI elements to split the UI on. This is used for clarity and to allow the UI to keep views in separate pages if needed.
{
"Label": "",
"Type": "divider",
"Divider": {
"Style": "thin"
}
}
We have additional layout fields for two different use cases - one for creating preview elements, where a condensed view can be optionally expanded by the user.
{
"Type": "preview_layout",
"PreviewLayout": {
"Title": "Delegate",
"Subtitle": "1 SOL Delegated to Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb"
},
"Condensed": {
"Fields": [ /* array of SignablePayloadFields */]
},
"Expanded": {
"Fields": [ /* array of SignablePayloadFields */]
}
}
The Endorsed Params feature allows passing additional parameters for the visualizer to interpret and potentially use for transforming the raw transaction to make meaningful display for user in a deterministic way.
Endorsed parameters are cryptographically bound to the SignablePayload through the EndorsedParamsDigest field, which contains a hash of all endorsed parameters. These are presented as an example - and may be chain or wallet-specific.
{
"EndorsedParams": {
"ChainId": "1",
"ContractAddress": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"MethodSignature": "transfer(address,uint256)",
"Nonce": "42",
"CallData": "0xa9059cbb000000000000000000000000...",
"ABIs": {},
"IDLs": {}
}
}
Transaction Construction: The visualizer service collects all necessary parameters for constructing a valid transaction.
Parameter Separation: Parameters are separated into:
Fields array)EndorsedParams)Digest Creation: The service computes a hash of the endorsed parameters:
EndorsedParamsDigest = sha256(serialize(EndorsedParams))
Payload Assembly: The digest is included in the SignablePayload, cryptographically binding the hidden parameters to the displayed information.
EndorsedParamsDigest matches the endorsed parameters used for transaction constructionNetwork fees and gas parameters
Technical identifiers (contract addresses, chain IDs)
Implementation-specific parameters (nonces, replay protection values)
Method signatures and serialized call data
Below are screenshots corresponding to specific fixture examples:
Bitcoin Withdraw

ERC20 Token Withdraw

Solana Withdraw with Expandable Preview Layouts
Expanding fields, these are expected to be shown when one of the expandable fields is clicked


Field Ordering: Fields should be displayed in the order they appear in the Fields array Version Compatibility: Clients should check the Version field to ensure they can properly render the payload Fallback Rendering: If a client doesn't understand a field type, it should fall back to displaying the FallbackText Security: Implementations should validate the ReplayProtection and EndorsedParamsDigest values
The VisualSign Protocol is designed to be extensible, allowing developers to safely add new field types while maintaining backward compatibility and ensuring data integrity.
The field serialization system uses a trait-based architecture with compile-time and runtime verification that provides multiple layers of protection against incomplete implementations:
trait FieldSerializer {
fn serialize_to_map(&self) -> Result<BTreeMap<String, Value>, Error>;
fn get_expected_fields(&self) -> Vec<&'static str>;
}
DeterministicOrdering trait ensures types implement deterministic serializationFirst, create the data structure for your new field type:
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SignablePayloadFieldCurrency {
#[serde(rename = "CurrencyCode")]
pub currency_code: String,
#[serde(rename = "Symbol")]
pub symbol: String,
#[serde(rename = "ExchangeRate", skip_serializing_if = "Option::is_none")]
pub exchange_rate: Option<String>,
}
Add your new variant to the SignablePayloadField enum:
pub enum SignablePayloadField {
// ... existing variants ...
#[serde(rename = "currency")]
Currency {
#[serde(flatten)]
common: SignablePayloadFieldCommon,
#[serde(rename = "Currency")]
currency: SignablePayloadFieldCurrency,
},
}
Add your field to both required methods in the FieldSerializer implementation:
impl FieldSerializer for SignablePayloadField {
fn serialize_to_map(&self) -> Result<BTreeMap<String, Value>, Error> {
let mut fields = HashMap::new();
match self {
// ... existing variants ...
SignablePayloadField::Currency { common, currency } => {
serialize_field_variant!(fields, "currency", common, ("Currency", currency));
},
}
Ok(fields.into_iter().collect())
}
fn get_expected_fields(&self) -> Vec<&'static str> {
let mut base_fields = vec!["FallbackText", "Label", "Type"];
match self {
// ... existing variants ...
SignablePayloadField::Currency { .. } => base_fields.push("Currency"),
}
base_fields.sort();
base_fields
}
}
Add your variant to the existing helper methods:
impl SignablePayloadField {
pub fn field_type(&self) -> &str {
match self {
// ... existing variants ...
SignablePayloadField::Currency { .. } => "currency",
}
}
// Update other helper methods as needed...
}
Critical: Your new field type must implement the DeterministicOrdering trait to be usable in contexts requiring deterministic serialization:
// This is already implemented for SignablePayloadField, but if creating a new top-level type:
impl DeterministicOrdering for YourNewType {}
Without this implementation, the type cannot be used in functions requiring deterministic ordering, and compilation will fail with a clear error message.
The system automatically verifies field completeness during serialization:
// โ
Successful serialization - all fields present
let currency_field = SignablePayloadField::Currency {
common: SignablePayloadFieldCommon {
fallback_text: "USD ($)".to_string(),
label: "Payment Currency".to_string(),
},
currency: SignablePayloadFieldCurrency {
currency_code: "USD".to_string(),
symbol: "$".to_string(),
exchange_rate: None,
},
};
let json = serde_json::to_string(¤cy_field)?;
// Result: {"Currency":{"CurrencyCode":"USD","Symbol":"$"},"FallbackText":"USD ($)","Label":"Payment Currency","Type":"currency"}
If you forget to serialize a field or have mismatched expectations:
// โ This would fail with detailed error message:
// "Missing expected field 'Currency'. Expected: ["Currency", "FallbackText", "Label", "Type"], Actual: ["FallbackText", "Label", "Type"]"
The system includes extensive tests that prove the verification works:
#[test]
fn test_new_field_type() {
// Test that new field type serializes correctly with verification
let field = SignablePayloadField::Currency { /* ... */ };
// This will succeed only if ALL expected fields are present and correctly serialized
let result = serde_json::to_string(&field);
assert!(result.is_ok());
// Verify alphabetical ordering
let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
if let serde_json::Value::Object(map) = value {
let keys: Vec<_> = map.keys().cloned().collect();
// Keys are automatically in alphabetical order
assert_eq!(keys, vec!["Currency", "FallbackText", "Label", "Type"]);
}
}
๐ก๏ธ Defense in Depth:
DeterministicOrdering trait enforces proper implementation๐ Clear Error Messages:
DeterministicOrdering trait causes compile-time error with clear message๐ Consistent Output:
๐ Easy Extension:
The new system maintains full backward compatibility while adding safety:
This extensible architecture transforms field extension from a error-prone manual process into a safe, verified, and automatic system that catches mistakes before they can cause issues in production.