Algorand Wallet Transaction Signing API | Algorand Developer Portal

Algorand Wallet Transaction Signing API

Abstract

The goal of this API is to propose a standard way for a dApp to request the signature of a list of transactions to an Algorand wallet. This document also includes detailed security requirements to reduce the risks of users being tricked to sign dangerous transactions. As the Algorand blockchain adds new features, these requirements may change.

Specification

The key words “ MUST”, “ MUST NOT”, “ REQUIRED”, “ SHALL”, “ SHALL NOT”, “ SHOULD”, “ SHOULD NOT”, “ RECOMMENDED”, “ MAY”, and “ OPTIONAL” in this document are to be interpreted as described in RFC-2119.

Comments like this are non-normative.

Overview

This overview section is non-normative.

After this overview, the syntax of the interfaces are described followed by the semantics and the security requirements.

At a high-level the API allows to sign:

Signatures are requested by calling a function signTxns(txns) on a list txns of transactions. The dApp may also provide an optional parameter opts.

Each transaction is represented by a WalletTransaction object. The only required field of a WalletTransaction is txn, a base64 encoding of the canonical msgpack encoding of the unsigned transaction. There are three main use cases:

  1. The transaction needs to be signed and the sender of the transaction is an account known by the wallet. This is the most common case. Example:
{
   "txn": "iaNhbXT..."
}

The wallet is free to generate the resulting signed transaction in any way it wants. In particular, the signature may be a multisig, may involve rekeying, or for very advanced wallets may use logicsigs.

Remark: If the wallet uses a large logicsig to sign the transaction and there is congestion, the fee estimated by the dApp may be too low. A future standard may provide a wallet API allowing the dApp to compute correctly the estimated fee. Before such a standard, the dApp may need to retry with a higher fee when this issue arises.

  1. The transaction does not need to be signed. This happens when the transaction is part of a group of transaction and is signed by another party or by a logicsig. In that case, the field signers is set to an empty array. Example:
{
   "txn": "iaNhbXT...",
   "signers": []
}
  1. ( OPTIONAL) The transaction needs to be signed but the sender of the transaction is not an account known by the wallet. This happens when the dApp uses a sender account derived from one or more accounts of the wallet. For example, the sender account may be a multisig account with public keys corresponding to some accounts of the wallet, or the sender account may be rekeyed to an account of the wallet. Example:
{
   "txn": "iaNhbXT...",
   "authAddr": "HOLQV2G65F6PFM36MEUKZVHK3XM7UEIFLG35UJGND77YDXHKXHKX4UXUQU",
   "msig": {
       "version": 1,
       "threshold": 2,
       "addrs": [
           "5MF575NQUDMRWOTS27KIBL2MFPJHKQEEF4LZEN6H3CZDAYVUKESMGZPK3Q",
           "FS7G3AHTDVMQNQQBHZYMGNWAX7NV2XAQSACQH3QDBDOW66DYTAQQW76RYA",
           "DRSHY5ONWKVMWWASTB7HOELVF5HRUTRQGK53ZK3YNMESZJR6BBLMNH4BBY"
       ]
   },
   "signers": ...
}

Note that in both the first and the third use cases, the wallet may sign the transaction using a multisig and may use a different authorized address (authAddr) than the sender address (i.e., rekeying). The main difference is that in the first case, the wallet knows how to sign the transaction (i.e., whether the sender address is a multisig and/or rekeyed), while in the third case, the wallet may not know it.

Syntax and Interfaces

Interfaces are defined in TypeScript. All the objects that are defined are valid JSON objects.

Interface SignTxnsFunction

A wallet transaction signing function signTxns is defined by the following interface:

export type SignTxnsFunction = (
   txns: WalletTransaction[],
   opts?: SignTxnsOpts
)
   => Promise<(SignedTxnStr | null)[]>;

where:

In case of error, the wallet (i.e., the signTxns function in this document) MUST reject the promise with an error object SignTxnsError defined below. This ARC uses interchangeably the terms “throw an error” and “reject a promise with an error”.

Interface AlgorandAddress

An Algorand address is represented by a 58-character base32 string. It includes the checksum.

export type AlgorandAddress = string;

An Algorand address is valid is it is a valid base32 string without padding and if the checksum is valid.

Example: "6BJ32SU3ABLWSBND7U5H2QICQ6GGXVD7AXSSMRYM2GO3RRNHCZIUT4ISAQ" is a valid Algorand address.

Interface SignedTxnStr

SignedTxnStr is the base64 encoding of the canonical msgpack encoding of a SignedTxn object, as defined in the Algorand specs. For Algorand version 2.5.5, see the authorization and signatures Section of the specs or the Go structure

export type SignedTxnStr = string;

Interface MultisigMetadata

A MultisigMetadata object specifies the parameters of an Algorand multisig address.

export interface MultisigMetadata {
   /**
    * Multisig version.
    */
   version: number;
   /**
    * Multisig threshold value. Authorization requires a subset of signatures,
    * equal to or greater than the threshold value.
    */
   threshold: number;
   /**
    * List of Algorand addresses of possible signers for this
    * multisig. Order is important.
    */
   addrs: AlgorandAddress[];
}
// Interface originally from github.com/algorand/js-algorand-sdk/blob/e07d99a2b6bd91c4c19704f107cfca398aeb9619/src/types/multisig.ts, where `string` has been replaced by `AlgorandAddress`.

Interface WalletTransaction

A WalletTransaction object represents a transaction to be signed by a wallet.

export interface WalletTransaction {
   /**
    * Base64 encoding of the canonical msgpack encoding of a Transaction.
    */
   txn: string;
   /**
    * Optional authorized address used to sign the transaction when the account
    * is rekeyed. Also called the signor/sgnr.
    */
   authAddr?: AlgorandAddress;
   /**
    * Multisig metadata used to sign the transaction
    */
   msig?: MultisigMetadata;
   /**
    * Optional list of addresses that must sign the transactions
    */
   signers?: AlgorandAddress[];
   /**
    * Optional base64 encoding of the canonical msgpack encoding of a
    * SignedTxn corresponding to txn, when signers=[]
    */
   stxn?: SignedTxnStr;
   /**
    * Optional message explaining the reason of the transaction
    */
   message?: string;
   /**
    * Optional message explaining the reason of this group of transaction
    * Field only allowed in the first transaction of a group
    */
   groupMessage?: string;
}

Interface SignTxnsOpts

A SignTxnsOps specifies optional parameters of the signTxns function:

export type SignTxnsOpts = {
   /**
    * Optional message explaining the reason of the group of transactions
    */
   message?: string;
}

Error Interface SignTxnsError

In case of error, the signTxns function MUST return a SignTxnsError object

interface SignTxnsError extends Error {
  code: number;
  data?: any;
}

where:

Inspired from github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md

Error Standards

Status Code Name Description
4001 User Rejected Request The user rejected the request.
4100 Unauthorized The requested operation and/or account has not been authorized by the user.
4200 Unsupported Operation The wallet does not support the requested operation.
4201 Too Many Transactions The wallet does not support signing that many transactions at a time.
4202 Uninitialized Wallet The wallet was not initialized properly beforehand.
4300 Invalid Input The input provided is invalid.

Wallet-specific extensions

Wallets MAY use specific extension fields in WalletTransaction and in SignTxnsOpts. These fields must start with: _walletName, where walletName is the name of the wallet. Wallet designers SHOULD ensure that their wallet name is not already used.

Example of a wallet-specific fields in opts (for the wallet theBestAlgorandWallet): _theBestAlgorandWalletIcon for displaying an icon related to the transactions.

Wallet-specific extensions MUST be designed such that a wallet not understanding them would not provide a lower security level.

Example of a forbidden wallet-specific field in WalletTransaction: _theWorstAlgorandWalletDisable requires this transaction not to be signed. This is dangerous for security as any signed transaction may leak and be committed by an attacker. Therefore, the dApp should never submit transactions that should not be signed, and that some wallets (not supporting this extension) may still sign.

Semantic and Security Requirements

The call signTxns(txns, opts) MUST either throws an error or return an array ret of the same length of the txns array:

  1. If txns[i].signers is an empty array, the wallet MUST NOT sign the transaction txns[i], and:
  1. Otherwise, the wallet MUST sign the transaction txns[i].txn and ret[i] MUST be set to the corresponding SignedTxnStr.

Note that if any transaction txns[i] that should be signed (i.e., where txns[i].signers is not an empty array) cannot be signed for any reason, the wallet MUST throw an error.

Terminology: Validation, Warnings, Fields

All the field names below are the ones in the Go SignedTxn structure and . Field of the actual transaction are prefixed with txn. (as opposed to fields of the WalletTransaction such as signers). For example, the sender of a transaction is txn.Sender.

Rejecting means throwing a 4300 error.

Strong warning / warning / weak warning / informational messages are different level of alerts. Strong warnings MUST be displayed in such a way that the user cannot miss the importance of them.

Semantic of WalletTransaction

Particular Case without signers, nor msig, nor senders

When neither signers, nor msig, nor authAddr are specified, the wallet MAY still sign the transaction using a multisig or a different authorized address than the sender address txn.Sender. It may also sign the transaction using a logicsig.

However, in all these cases, the resulting SignedTxn MUST be such that it can be committed to the blockchain (assuming the transaction itself can be executed and that the account is not rekeyed in the meantime).

In particular, if a multisig is used, the numbers of subsigs provided must be at least equal to the multisig threshold. This is different from the case where msig is provided, where the wallet MAY provide fewer subsigs than the threshold.

Semantic of SignTxnsOpts

General Validation

The goal is to ensure the highest level of security for the end-user, even when the transaction is generated by a malicious dApp. Every input must be validated.

Validation:

Note: We will be working with the algosdk team to provide helper functions for validation in some cases and to ensure the security of the de-serialization of potentially malicious transactions.

If there is any unexpected field at any level (both in the transaction itself or in the object WalletTransaction), the wallet MUST immediately reject. The only exception is for the “wallet-specific extension” fields (see above).

Group Validation

The wallet should support the following two use cases:

  1. ( REQUIRED) txns is a non-empty array of transactions that belong to the same group of transactions. In other words, either txns is an array of a single transaction with a zero group ID (txn.Group), or txns is an array of one or more transactions with the same non-zero group ID. The wallet MUST reject if the transactions do not match their group ID. (The dApp must provide the transactions in the order defined by the group ID.)

An early draft of this ARC required that the size of a group of transactions must be greater than 1 but, since the Algorand protocol supports groups of size 1, this requirement had been changed so dApps don’t have to have special cases for single transactions and can always send a group to the wallet.

  1. ( OPTIONAL) txns is a concatenation of txns arrays of transactions of type 1:
    • All transactions with the same non-zero group ID must be consecutive and must match their group ID. The wallet MUST reject if the above is not satisfied.
    • The wallet UI MUST be designed so that it is clear to the user when transactions are grouped (aka form an atomic transfers) and when they are not. It SHOULD provide very clear explanations that are understandable by beginner users, so that they cannot easily be tricked to sign what they believe is an atomic exchange while it is in actuality a one-sided payment.

If txns does not match any of the formats above, the wallet MUST reject.

The wallet MAY choose to restrict the maximum size of the array txns. The maximum size allowed by a wallet MUST be at least the maximum size of a group of transactions in the current Algorand protocol on MainNet. (When this ARC was published, this maximum size was 16.) If the wallet rejects txns because of its size, it MUST throw a 4201 error.

An early draft of this API allowed to sign single transactions in a group without providing the other transactions in the group. For security reasons, this use case is now deprecated and SHALL not be allowed in new implementations. Existing implementations may continue allowing for single transactions to be signed if a very clear warning is displayed to the user. The warning MUST stress that signing the transaction may incur losses that are much higher than the amount of tokens indicated in the transaction. That is because potential future features of Algorand may later have such consequences (e.g., a signature of a transaction may actually authorize the full group under some circumstances).

Transaction Validation

Inputs that Must Be Systematically Rejected
Inputs that Warrant Display of Warnings

The wallet MUST:

The above is for version 2.5.6 of the Algorand software. Future consensus versions may require additional checks.

Before supporting any new transaction field or type (for a new version of the Algorand blockchain), the wallet authors MUST be perform a careful security analysis.

Genesis Validation

The wallet MUST check that the genesis hash (field txn.GenesisHash) and the genesis ID (field txn.GenesisID, if provided) match the network used by the wallet. If the wallet supports multiple networks, it MUST make clear to the user which network is used.

UI

In general, the UI MUST ensure that the user cannot be confused by the dApp to perform dangerous operations. In particular, the wallet MUST make clear to the user what is part of the wallet UI from what is part of what the dApp provided.

Special care MUST be taken of when:

Usual precautions MUST be taken regarding the fact that the inputs are provided by an untrusted dApp (e.g., preventing code injection and so on).

Rationale

The API was designed to:

The API was not designed to:

The last two items are expected to be defined in other documents.

Rationale for Group Validation

The requirements around group validation have been designed to prevent the following attack.

The dApp pretends to buy 1 Algo for 10 USDC, but instead creates an atomic transfer with the user sending 1 Algo to the dApp and the dApp sending 0.01 USDC to the user. However, it sends to the wallet a 1 Algo and 10 USDC transactions. If the wallet does not verify that this is a valid group, it will make the user believe that they are signing for the correct atomic transfer.

Reference Implementation

Sign a Group of Two Transactions

Here is an example in node.js how to use the wallet interface to sign a group of two transactions and send them to the network. The function signTxns is assumed to be a method of algorandWallet.

Note: We will be working with the algosdk development to add two helper functions to facilitate the use of the wallet. Current idea is to add: Transaction.toBase64 that does the same as Transaction.toByte except it outputs a base64 string Algodv2.sendBase64RawTransactions that does the same as Algodv2.sendRawTransactions except it takes an array of base64 string instead of an array of Uint8array

import algosdk from 'algosdk';

import * as algorandWallet from './wallet';

import {Buffer} from "buffer";

const firstRound = 13809129;

const suggestedParams = {
   flatFee: false,
   fee: 0,
   firstRound: firstRound,
   lastRound: firstRound + 1000,
   genesisID: 'testnet-v1.0',
   genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI='
};

const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
   from: "37MSZIPXHGNCKTDJTJDSYIOF4C57JAL2FTKESD2HBVELXYHEIXVZ4JVGFU",
   to: "PKSE2TARC645D4O2IO6QNWVW6PLJDTR6IOKNKMGSHQL7JIJHNGNFVISUHI",
   amount: 1000,
   suggestedParams,
});

const txn2 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
   from: "37MSZIPXHGNCKTDJTJDSYIOF4C57JAL2FTKESD2HBVELXYHEIXVZ4JVGFU",
   to: "PKSE2TARC645D4O2IO6QNWVW6PLJDTR6IOKNKMGSHQL7JIJHNGNFVISUHI",
   amount: 2000,
   suggestedParams,
});

const txs = [txn1, txn2];

algosdk.assignGroupID(txs);

const txn1B64 = Buffer.from(txn1.toByte()).toString("base64");

const txn2B64 = Buffer.from(txn2.toByte()).toString("base64");

(async () => {
   const signedTxs = await algorandWallet.signTxns([
       {txn: txn1B64},
       {txn: txn2B64, signers: []}
   ]);

const algodClient = new algosdk.Algodv2("", "...", "");

algodClient.sendRawTransaction(
       signedTxs.map(stxB64 => Buffer.from(stxB64, "base64"))
   )
})();

Security Considerations

None.

Copyright

Copyright and related rights waived via CCO.