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:
- A valid group of transaction (aka atomic transfers).
- (OPTIONAL) A list of groups of transactions.
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:
- 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:
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.1{2"txn": "iaNhbXT..."3}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. 
- 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 signersis set to an empty array. Example:1{2"txn": "iaNhbXT...",3"signers": []4}
- (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:
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 (1{2"txn": "iaNhbXT...",3"authAddr": "HOLQV2G65F6PFM36MEUKZVHK3XM7UEIFLG35UJGND77YDXHKXHKX4UXUQU",4"msig": {5"version": 1,6"threshold": 2,7"addrs": [8"5MF575NQUDMRWOTS27KIBL2MFPJHKQEEF4LZEN6H3CZDAYVUKESMGZPK3Q",9"FS7G3AHTDVMQNQQBHZYMGNWAX7NV2XAQSACQH3QDBDOW66DYTAQQW76RYA",10"DRSHY5ONWKVMWWASTB7HOELVF5HRUTRQGK53ZK3YNMESZJR6BBLMNH4BBY"11]12},13"signers": ...14}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:
1export type SignTxnsFunction = (2   txns: WalletTransaction[],3   opts?: SignTxnsOpts4)5   => Promise<(SignedTxnStr | null)[]>;where:
- txnsis a non-empty list of- WalletTransactionobjects (defined below).
- optsis an optional parameter object- SignTxnsOpts(defined below).
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.
1export 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
1export type SignedTxnStr = string;Interface MultisigMetadata
A MultisigMetadata object specifies the parameters of an Algorand multisig address.
1export interface MultisigMetadata {2   /**3    * Multisig version.4    */5   version: number;6
7   /**8    * Multisig threshold value. Authorization requires a subset of signatures,9    * equal to or greater than the threshold value.10    */11   threshold: number;12
13   /**14    * List of Algorand addresses of possible signers for this15    * multisig. Order is important.16    */17   addrs: AlgorandAddress[];18}- versionshould always be 1.
- thresholdshould be between 1 and the length of- addrs.
Interface originally from github.com/algorand/js-algorand-sdk/blob/e07d99a2b6bd91c4c19704f107cfca398aeb9619/src/types/multisig.ts, where
stringhas been replaced byAlgorandAddress.
Interface WalletTransaction
A WalletTransaction object represents a transaction to be signed by a wallet.
1export interface WalletTransaction {2   /**3    * Base64 encoding of the canonical msgpack encoding of a Transaction.4    */5   txn: string;6
7   /**8    * Optional authorized address used to sign the transaction when the account9    * is rekeyed. Also called the signor/sgnr.10    */11   authAddr?: AlgorandAddress;12
13   /**14    * Multisig metadata used to sign the transaction15    */16   msig?: MultisigMetadata;17
18   /**19    * Optional list of addresses that must sign the transactions20    */21   signers?: AlgorandAddress[];22
23   /**24    * Optional base64 encoding of the canonical msgpack encoding of a25    * SignedTxn corresponding to txn, when signers=[]26    */27   stxn?: SignedTxnStr;28
29   /**30    * Optional message explaining the reason of the transaction31    */32   message?: string;33
34   /**35    * Optional message explaining the reason of this group of transaction36    * Field only allowed in the first transaction of a group37    */38   groupMessage?: string;39}Interface SignTxnsOpts
A SignTxnsOps specifies optional parameters of the signTxns function:
1export type SignTxnsOpts = {2   /**3    * Optional message explaining the reason of the group of transactions4    */5   message?: string;6}Error Interface SignTxnsError
In case of error, the signTxns function MUST return a SignTxnsError object
1interface SignTxnsError extends Error {2  code: number;3  data?: any;4}where:
- message:- MUST be a human-readable string
- SHOULD adhere to the specifications in the Error Standards section below
 
- code:- MUST be an integer number
- MUST adhere to the specifications in the Error Standards section below
 
- data:- SHOULD contain any other useful information about the error
 
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 wallettheBestAlgorandWallet):_theBestAlgorandWalletIconfor 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:_theWorstAlgorandWalletDisablerequires 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:
- If txns[i].signersis an empty array, the wallet MUST NOT sign the transactiontxns[i], and:
- if txns[i].stxnis not present,ret[i]MUST be set tonull.
- if txns[i].stxnis present and is a validSignedTxnStrwith the underlying transaction exactly matchingtxns[i].txn,ret[i]MUST be set totxns[i].stxn. (See section on the semantic ofWalletTransactionfor the exact requirements ontxns[i].stxn.)
- otherwise, the wallet MUST throw a 4300error.
- Otherwise, the wallet MUST sign the transaction txns[i].txnandret[i]MUST be set to the correspondingSignedTxnStr.
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
- txn:- Must a base64 encoding of the canonical msgpack encoding of a Transactionobject 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.
- If txnis not a base64 string or cannot be decoded into aTransactionobject, the wallet MUST reject.
 
- Must a base64 encoding of the canonical msgpack encoding of a 
- authAddr:- The wallet MAY not support this field. In that case, it MUST throw a 4200error.
- If specified, it must be a valid Algorand address. If this is not the case, the wallet MUST reject.
- If specified and supported, the wallet MUST sign the transaction using this authorized address even if it sees the sender address txn.Senderwas not rekeyed toauthAddr. This is because the sender may be rekeyed before the transaction is committed. The wallet SHOULD display an informational message.
 
- The wallet MAY not support this field. In that case, it MUST throw a 
- msig:- The wallet MAY not support this field. In that case, it MUST throw a 4200error.
- If specified, it must be a valid MultisigMetadataobject. If this is not the case, the wallet MUST reject.
- If specified and supported, the wallet MUST verify msigmatchesauthAddr(ifauthAddris specified and supported) or the sender addresstxn.Sender(otherwise). The wallet MUST reject if this is not the case.
- If specified and supported and if signersis not specified, the wallet MUST return aSignedTxnwith all the subsigs that it can provide and that the wallet user agrees to provide. If the wallet can sign more subsigs than the requested threshold (msig.threshold), it MAY only providemsig.thresholdsubsigs. It is also possible that the wallet cannot provide at leastmsig.thresholdsubsigs (either because the user prevented signing with some keys or because the wallet does not know enough keys). In that case, the wallet just provide the subsigs it can provide. However, the wallet MUST provide at least one subsig or throw an error.
 
- The wallet MAY not support this field. In that case, it MUST throw a 
- signers:- If specified and if not a list of valid Algorand addresses, the wallet MUST reject.
- If signersis an empty array, the transaction is for information purpose only and the wallet SHALL NOT sign it, even if it can (e.g., know the secret key of the sender address).
- If signersis an array with more than 1 Algorand addresses:- The wallet MUST reject if msigis not specified.
- The wallet MUST reject if signersis not a subset ofmsig.addrs.
- The wallet MUST try to return a SignedTxnwith all the subsigs corresponding tosignerssigned. If it cannot, it SHOULD throw a4001error. Note that this is different than whensignersis not provided, where the signing is only “best effort”.
 
- The wallet MUST reject if 
- If signersis an array with a single Algorand address:- If msigis specified, the rules as whensignersis an array with more than 1 Algorand addresses apply.
- If authAddris specified butmsigis not, the wallet MUST reject ifsigners[0]is not equal toauthAddr.
- If neither authAddrnormsigare specified, the wallet MUST reject ifsigners[0]is not the sender addresstxn.Sender.
- In all cases, the wallet MUST only try to provide signatures for signers[0]. In particular, if the sender addresstxn.Senderwas rekeyed or is a multisig and ifauthAddrandmsigare not specified, then the wallet MUST reject.
 
- If 
 
- stxnif specified:- If specified and if signersis not the empty array, the wallet MUST reject.
- If specified:
- It must be a valid SignedTxnStr. The wallet MUST reject if this is not the case.
- The wallet MUST reject if the field txninside theSignedTxnobject does not match exactly theTransactionobject intxn.
- The wallet MAY NOT check whether the other fields of the SignedTxnare valid. In particular, it MAY acceptstxneven in the following cases: it contains an invalid signaturesig, it contains both a signaturesigand a logicsiglsig, it contains a logicsiglsigthat always reject.
 
- It must be a valid 
 
- If specified and if 
- message:- The wallet MAY decide to never print the message, to only print the first characters, or to make any changes to the messages that may be used to ensure a higher level of security. The wallet MUST be designed to ensure that the message cannot be easily used to trick the user to do an incorrect action. In particular, if displayed, the message must appear in an area that is easily and clearly identifiable as not trusted by the wallet.
- The wallet MUST prevent HTML/JS injection and must only display plaintext messages.
 
- groupMessageobeys the same rules as- message, except it is a message common to all the transactions of the group containing the current transaction. In addition, the wallet MUST reject if- groupMessageis provided for a transaction that is not the first transaction of the group. Note that- txnsmay contain multiple groups of transactions, one after the other (see the Group Validation section for details).
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
- messageobeys the rules as- WalletTransaction.messageexcept it is a message common to all transactions.
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:
- SHALL NOT rely on TypeScript typing as this can be bypassed. Types MUST be manually verified.
- SHALL NOT assume the Algorand SDK does any validation, as the Algorand SDK is not meant to receive maliciously generated inputs. Furthermore, the SDK allows for dangerous transactions (such as rekeying). The only exception for the above rule is for de-serialization of transactions. Once de-serialized, every field of the transaction must be manually validated.
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:
- (REQUIRED) txnsis a non-empty array of transactions that belong to the same group of transactions. In other words, eithertxnsis an array of a single transaction with a zero group ID (txn.Group), ortxnsis 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.
- (OPTIONAL) txnsis a concatenation oftxnsarrays 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
- Transactions WalletTransaction.txnwith fields that are not known by the wallet MUST be systematically rejected. In particular:- Every field MUST be validated.
- Any extra field MUST systematically make the wallet reject.
- This is to prevent any security issue in case of the introduction of new dangerous fields (such as txn.RekeyToortxn.CloseRemainderTo).
 
- Transactions of an unknown type (field txn.Type) MUST be rejected.
- Transactions containing fields of a different transaction type (e.g., txn.Receiverin an asset transfer transaction) MUST be rejected.
Inputs that Warrant Display of Warnings
The wallet MUST:
- Display a strong warning message when signing a transaction with one of the following fields: txn.RekeyTo,txn.CloseRemainderTo,txn.AssetCloseTo. The warning message MUST clearly explain the risks. No warning message is necessary for transactions that are provided for informational purposes in a group and are not signed (i.e., transactions withsigners=[]).
- Display a strong warning message in case the transaction is signed in the future (first valid round is after current round plus some number, e.g. 500). This is to prevent surprises in the future where a user forgot that they signed a transaction and the dApp maliciously play it later.
- Display a warning message when the fee is too high. The threshold MAY depend on the load of the Algorand network.
- Display a weak warning message when signing a transaction that can increase the minimum balance in a way that may be hard or impossible to undo (asset creation or application creation)
- Display an informational message when signing a transaction that can increase the minimum balance in a way that can be undone (opt-in to asset or transaction)
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:
- Displaying the messagefield ofWalletTransactionand ofSignTxnsOpts.
- Displaying any arbitrary field of transactions including note field (txn.Note), genesis ID (txn.genesisID), asset configuration fields (txn.AssetName,txn.UnitName,txn.URL, …)
- Displaying message hidden in fields that are expected to be base32/base64-strings or addresses. Using a different font for those fields MAY be an option to prevent such confusion.
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:
- Be easily implementable by all Algorand wallets
- Rely on the official specs and the official source code.
- Only use types supported by JSON to simplify interoperability (avoid Uint8Array for example) and to allow easy serialization / deserialization
- Be easy to extend to support future features of Algorand
- Be secure by design: making it hard for malicious dApps to cause the wallet to sign a transaction without the user understanding the implications of their signature.
The API was not designed to:
- Directly support of the SDK objects. SDK objects must first be serialized.
- Support any listing accounts, connecting to the wallet, sending transactions, …
- Support of signing logic signatures.
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
This section is non-normative.
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.toBase64that does the same asTransaction.toByteexcept it outputs a base64 stringAlgodv2.sendBase64RawTransactionsthat does the same asAlgodv2.sendRawTransactionsexcept it takes an array of base64 string instead of an array of Uint8array
1import algosdk from 'algosdk';2import * as algorandWallet from './wallet';3import {Buffer} from "buffer";4
5const firstRound = 13809129;6
7const suggestedParams = {8   flatFee: false,9   fee: 0,10   firstRound: firstRound,11   lastRound: firstRound + 1000,12   genesisID: 'testnet-v1.0',13   genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI='14};15
16const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({17   from: "37MSZIPXHGNCKTDJTJDSYIOF4C57JAL2FTKESD2HBVELXYHEIXVZ4JVGFU",18   to: "PKSE2TARC645D4O2IO6QNWVW6PLJDTR6IOKNKMGSHQL7JIJHNGNFVISUHI",19   amount: 1000,20   suggestedParams,21});22
23const txn2 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({24   from: "37MSZIPXHGNCKTDJTJDSYIOF4C57JAL2FTKESD2HBVELXYHEIXVZ4JVGFU",25   to: "PKSE2TARC645D4O2IO6QNWVW6PLJDTR6IOKNKMGSHQL7JIJHNGNFVISUHI",26   amount: 2000,27   suggestedParams,28});29
30const txs = [txn1, txn2];31algosdk.assignGroupID(txs);32
33const txn1B64 = Buffer.from(txn1.toByte()).toString("base64");34const txn2B64 = Buffer.from(txn2.toByte()).toString("base64");35
36(async () => {37   const signedTxs = await algorandWallet.signTxns([38       {txn: txn1B64},39       {txn: txn2B64, signers: []}40   ]);41
42   const algodClient = new algosdk.Algodv2("", "...", "");43
44   algodClient.sendRawTransaction(45       signedTxs.map(stxB64 => Buffer.from(stxB64, "base64"))46   )47})();Security Considerations
None.
Copyright
Copyright and related rights waived via CCO.