sRFC 33 - Sign message in Actions/blinks
TLDR
Add the ability for Actions and blinks to ask users to sign a plaintext message. This is a common and no-cost way to validate a user actually controls a given wallet address.
Background
Currently, all actions ultimately require the user to sign and send a transaction to be confirmed on chain. So there is always transaction fee for users and therefore always a realized cost. Even in action chaining, the user must sign a transaction before proceeding to the next action. Each time paying a transaction fee.
A very common flow within dApps is asking the user to sign a plaintext message to validate they do in fact control a specific wallet address. This is great because it is completely free for users. There is no transaction fee paid.
Adding the ability for actions/blinks to request users to sign message will unlock new use cases for blinks, including:
- reducing costs for users
- blinks could authenticate a user’s wallet address natively within the blink
- allow blinks to craft customized blink metadata based on the wallet interacting with it
Proposal
Note: specific implementation details to be worked out in PRs.
- add the ability for an action to request a user sign a transaction OR plaintext message via updating
LinkedAction
to support both ActionPostResponse
should handle different “action response types”, one being for transaction signature requests and another being message signature request- messaged being signed should have a consistent structure and ensures their can be a security mechanism to ensure messages originated from its own action api
Requesting the user sign a message
LinkedAction
be updated to support multiple types. Something similar to this:
export type LinkedActionType = "transaction" | "sign-message";
export interface LinkedAction {
type?: LinkedActionType;
href: string;
label: string;
parameters?: Array<TypedActionParameter>;
}
When the LinkedActionType
is a sign message type:
- the blink client will make a POST request to the
href
with the user’s account address (just as transaction focused actions do now) - the api server will respond with the message for the user to sign (see message sign request)
- after the user signs the message, the blink client should make a POST request to
Response payload for a sign message request
When an action api server is requesting a user sign a plaintext message, vice a transaction
, the server should respond with an updated ActionPostResponse
described below:
Note: Sign message requires action chaining. Therefore when an action api sends a sign message request to the client, the api server must also include a PostNextActionLink
to inform action-clients where to send the signature of the user signed message.
export type ActionPostResponse = TransactionResponse | SignMessageResponse;
export interface TransactionResponse {
type?: "transaction"
transaction: string;
message?: string;
links?: {
next: NextActionLink;
};
}
export interface SignMessageResponse {
type: "sign-message"
data: SignMessageData; // see "Structured sign message data"
state?: string;
message?: string;
links: {
next: PostNextActionLink;
};
}
The state
should be a utf-8 string of a MAC created by the action api server using a secret stored on that server. Action clients should pass this value back to the api server in the PostNextActionLink
request. This enables api servers to cryptographically verify that the initial sign message request came from their server by generating a HMAC on their server. It also make it so they are not required to maintain server state of which messages their api requested users sign.
After the user signs the provided message, the blink client will make a POST request to the included next action (i.e. perform action chaining) with a payload as follows:
account
(required) - the user’s wallet address that signed the messagedata
(required) - the structured data that the user was requested to sign. See Structured sign message datasignature
(required) - the signature created by theaccount
singing thedata
(as a base58 encoded string)state
(optional) - the samestate
value the action api initial provided, relayed back from the client.
An example of the updated NextActionPostRequest
looks like this:
export interface NextActionPostRequest extends ActionPostRequest {
/** signature produced from the previous action (either a transaction id or message signature) */
signature: string;
/** */
data: SignMessageData; // see "Structured sign message data"
/** */
state?: string
}
Note: since the data
already supports a key-value object, no change to that type should be required.
After receiving the NextActionPostRequest
, the action api should perform all required validation checks on the signature
, data
, and state
to satisfy their business constraints.
The action api can now return the metadata for the next action, and the user can continue within the blink experience.
Structured sign message data
For better user experience and improved security, the plaintext message a user will be prompted to sign should be structure with a few required fields:
domain
(required) - domain requesting the user to sign the messageaddress
(required) - base58 string of the Solana address requested to sign this messagestatement
(required) - human readable string message to the user. it should not contain new line characters (i.e.\n
)nonce
(required) - a random alpha-numeric string at least 8 characters. this value is generated by the action api, should only be used once, and is used to prevent replay attacksissuedAt
(required) - ISO 8601 datetime string. This represents the time at which the sign request was issued by the action api.chainId
(optional) - Chain id compatible with CAIPs, defaults to Solana mainnet-beta in clients. If not provided, the blink client should not include chain id in the message to be signed by the user.
type SignMessageData = {
domain: string;
address: string;
statement: string;
nonce: string;
issuedAt: string;
chainId?: string;
}
Note: this structured data is similar to the Sign In With Solana spec, but without the additional rarely used fields. Therefore structure of this data is compatible with SIWS.
chainId
is consistent with sRFC 31: Compatibility of Blinks and Actions.
issuedAt
should be validated by the action api during their signature verification process in order to perform any desired expiration checks (i.e. was this sign request issued in the last 10 minutes? if not, my api will reject it)
At a minimum, the required fields in the structured message should be presented to the user at or before they are prompted to sign said message.