aip | title | author | discussions-to (*optional) | Status | last-call-end-date (*optional) | type | created |
---|---|---|---|---|---|---|---|
62 |
Wallet Standard |
0xmaayan, hardsetting, NorbertBodziony |
Accepted |
Ecosystem Standard |
29/01/2024 |
The Wallet standard defines a universal API for wallet and application interactions. This AIP introduces a new wallet standard for the Aptos ecosystem.
Most web wallets today come in the form of browser extensions. These extensions interact with dApps by injecting themselves to the global window object and expect a dapp to detect them by reading the window object. There are several issues with the way it works today.
- This method requires the dapp to be made aware of how they can find these objects and must choose to support a limited number of wallets that may not be relevant to the user.
- For a dapp to detect the wallets, it needs to run an endless process that keeps scanning the window object to detect wallets that have been injected before and after the dapp has been loaded.
- Relying solely on a dapp detecting process logic can create a race condition risk in the case the dapp loads before a wallet and the dapp is not aware of the new wallets.
In addition, there are some problems with how the standard is implemented in the Aptos ecosystem these days.
- The standard is deeply integrated within the Aptos wallet adapter, and any change can cause breaking changes for dApps and wallets, creating endless maintenance work by requiring a dApp or wallet to implement these changes.
- Since each dApp needs to install and maintain a wallet plugin dependency, it is exposed to a potential supply chain attack.
- The standard supports only the legacy TS SDK input, types, and logic. That means that it doesn't enjoy the features and enhancements of the new TS SDK. In addition, the legacy TS SDK does not receive any more support or new features.
In this proposal, we suggest bringing a generalized event-based model communication between a wallet and a dapp to Aptos that eliminates all the above issues.
- Dapp developers
- Familiarize themselves with the new standard
- Migrate to the new TypeScript SDK
- Support a wallet discoverable function and filter out non-aptos wallets
- Wallet developers
- Familiarize themselves with the new standard
- Migrate to the new TypeScript SDK
- Or hold a conversion layer from the new TypeScript SDK types to the legacy TypeScipt SDK types
- Register the wallet so it will be discoverable by the dapp
- Implementation of a AptosWallet class that conforms with the new standard
This chain agnostic solution has been introduced as a generalized standard for wallets and dapps communication and has already been implemented on Solana and Sui, and the Ethereum community recently proposed a similar solution.
The Wallet Standard is a chain-agnostic set of interfaces and conventions that aim to improve how applications interact with injected wallets.
Standard Features
A standard feature is a method that must or should be supported and implemented by a wallet. Here is a list of the suggested Aptos features
a feature marked with
*
is an optional feature
aptos:connect
method to establish a connection between a dapp and a wallet.
// `silent?: boolean` - gives ability to trigger connection without user prompt (for example, for auto-connect)
// `networkInfo?: NetworkInfo` - defines the network that the dapp will use (shortcut for connect and change network)
connect(silent?: boolean, networkInfo?: NetworkInfo): Promise<UserResponse<AccountInfo>>;
aptos:disconnect
method to disconnect a connection established between a dapp and a wallet
disconnect(): Promise<void>;
aptos:getAccount
to get the current connected account in the wallet
getAccount():Promise<UserResponse<AccountInfo>>
aptos:getNetwork
to get the current network in the wallet
getNetwork(): Promise<UserResponse<NetworkInfo>>;
aptos:signTransaction
for the current connected account in the wallet to sign a transaction using the wallet.
// `transaction: AnyRawTransaction` - a generated raw transaction created with Aptos’ TS SDK
signTransaction(transaction: AnyRawTransaction):AccountAuthenticator
aptos:signMessage
for the current connected account in the wallet to sign a message using the wallet.
// `message: AptosSignMessageInput` - a message to sign
signMessage(message: AptosSignMessageInput):Promise<UserResponse<AptosSignMessageOutput>>;
aptos:onAccountChange
event for the wallet to fire when an account has been changed in the wallet.
// `newAccount: AccountInfo` - The new connected account
onAccountChange(newAccount: AccountInfo): Promise<void>
aptos:onNetworkChange
event for the wallet to fire when the network has been changed in the wallet.
// `newNetwork: NetworkInfo` - The new wallet current network
onNetworkChange(newNetwork: NetworkInfo):Promise<void>
aptos:signAndSubmitTransaction*
method to sign and submit a transaction using the current connected account in the wallet.
/**
* interface AptosSignAndSubmitTransactionInput {
* gasUnitPrice?: number; // defaults to estimated gas unit price
* maxGasAmount?: number; // defaults to estimated max gas amount
* payload: InputGenerateTransactionPayloadData; // the transaction payload
* }
*/
// `AptosSignAndSubmitTransactionInput` - the transaction details in a JSON format
signAndSubmitTransaction(AptosSignAndSubmitTransactionInput): Promise<UserResponse<{string:hash}>>;
aptos:changeNetwork*
event for the dapp to send to the wallet to change the wallet’s current network
// `network:NetworkInfo` - The network for the wallet to change to
changeNetwork(network:NetworkInfo):Promise<UserResponse<{success: boolean,reason?: string}>>
aptos:openInMobileApp*
a function that supports redirecting a user from a web browser on mobile to a native mobile app. The wallet plugin should add the location url a wallet should open the in-app browser at.
openInMobileApp(): void
Types
Note:
UserResponse
type is used for when a user rejects a rejectable request. For example, when user wants to connect but instead closes the window popup.
export enum UserResponseStatus {
APPROVED = 'Approved',
REJECTED = 'Rejected'
}
export interface UserApproval<TResponseArgs> {
status: UserResponseStatus.APPROVED
args: TResponseArgs
}
export interface UserRejection {
status: UserResponseStatus.REJECTED
}
export type UserResponse<TResponseArgs> = UserApproval<TResponseArgs> | UserRejection;
export interface AccountInfo = { account: Account, ansName?: string }
export interface NetworkInfo {
name: Network
chainId: number
url?: string
}
export type AptosSignMessageInput = {
address?: boolean
application?: boolean
chainId?: boolean
message: string
nonce: string
}
export type AptosSignMessageOutput = {
address?: string
application?: string
chainId?: number
fullMessage: string
message: string
nonce: string
prefix: 'APTOS'
signature: Signature
}
Errors
A wallet must throw a AptosWalletError. The standard requires to support Unauthorized
and InternalError
but a wallet can throw a custom AptosWalletError
error
Using the default message
if (error) {
throw new AptosWalletError(AptosWalletErrorCode.Unauthorized);
}
Using a custom message
if (error) {
throw new AptosWalletError(
AptosWalletErrorCode.Unauthorized,
"My custom unauthorized message"
);
}
Using a custom error
if (error) {
throw new AptosWalletError(-32000, "Invalid Input");
}
The standard exposes a detect functionality to detect if existing wallets conform with the Aptos standard by validating required functions are available in the wallet. These functions are called features. Each feature should be defined with an aptos
namespace, colon
and the {method}
name, i.e aptos:connect
.
Wallet Provider
AptosWallet interface implementation
A wallet must implement a AptosWallet interface with the wallet provider info and features:
class MyWallet implements AptosWallet {
url: string;
version: "1.0.0";
name: string;
icon:
| `data:image/svg+xml;base64,${string}`
| `data:image/webp;base64,${string}`
| `data:image/png;base64,${string}`
| `data:image/gif;base64,${string}`;
chains: AptosChain;
features: AptosFeatures;
accounts: readonly AptosWalletAccount[];
}
AptosWalletAccount interface implementation
A wallet must implement a AptosWalletAccount interface that represents the accounts that have been authorized by the dapp.
enum AptosAccountVariant {
Ed25519,
MultiEd25519,
SingleKey,
MultiKey,
}
class AptosWalletAccount implements WalletAccount {
address: string;
publicKey: Uint8Array;
chains: AptosChain;
features: AptosFeatures;
variant: AptosAccountVariant;
label?: string;
icon?:
| `data:image/svg+xml;base64,${string}`
| `data:image/webp;base64,${string}`
| `data:image/png;base64,${string}`
| `data:image/gif;base64,${string}`
| undefined;
}
Register Wallet
A wallet registers itself using the registerWallet method to notify the dapp it is ready to be registered.
const myWallet = new MyWallet();
registerWallet(myWallet);
Dapp
Get Wallets
A dapp uses the getAptosWallets() function which gets all the Aptos standard compatible wallets.
import { getAptosWallets } from "@aptos-labs/wallet-standard";
let { aptosWallets, on } = getAptosWallets();
Register Events
On first load, and before the dapp has been loaded, it gets all the wallets that have been registered so far. To keep getting all the registered wallets after this point, the dapp must add an event listener for new wallets that get registered receiving an unsubscribe function, which it can later use to remove the listener.
const removeRegisterListener = on("register", function () {
// The dapp can add new aptos wallets to its own state context as they are registered
let { aptosWallets } = getAptosWallets();
});
const removeUnregisterListener = on("unregister", function () {
let { aptosWallets } = getAptosWallets();
});
The dapp has an event listener now, so it sees new wallets immediately and doesn't need to poll or list them again. This also works if the dapp loads before any wallets (it will initialize, see no wallets, then see wallets as they load)
Wallet Request
A dapp makes a wallet request by calling the feature name that coresponds to the desired action.
const onConnect = () => {
this.wallet.features["aptos:connect"].connect();
};
The new standard uses the new TypeScript SDK types and therefore requires dapps and wallets to use/migrate to the new TypeScript SDK or hold a conversion layer from the new TypeScript SDK types to the legacy TypeScript SDK types.
This solution is a general implementation that has already been used by different chains and wallets and will probably be adopted by more projects. With that, the migration effort of a wallet from one chain to another is minimal. In addition, multi-chain dApps can easily detect any wallet that conforms to the standard.
Both dApps' and wallets' integration and implementation are straightforward and painless. Mostly, each needs to use a provided method for registration/detection.
The addition of any future features and/or enhancements should not introduce any breaking change, as each wallet holds its own plugin code, and any feature/method lives in its own context.
Once the AIP is approved, dapps and wallets can implement the required changes (described in the "Reference Implementation" section) to conform with the new standard.
With the new discovery method we aim to remove the dapp responsibility on installing and maintaining different wallet packages and therefore eliminate a supply chain attack risk.
The new method has been implemented on Solana and Sui, and the Ethereum community recently proposed a similar solution. Additionally, differene wallets have already integrated and implemented the new standard such as Phantom, Nightly, Brave Wallet, Martian, etc.