sRFC 28: Blinks Chaining

Blinks as they exist now have a single depth to interactions. You fetch the entrypoint of the blink with a GET request, then use one of the button actions to make a POST request to the server to fetch a transaction with the user’s account info.

This single depth prevents good error handling, and can be easily expanded to allow for blink chaining by just reusing the ActionGetResult for the POST request (with an optional transaction field) and rerendering the blink.

This allows branching logic on the part of the blink, where actions can be shown specific to the user’s account, and even multiple transactions could be carried out. This also allows for better error messages and error handling, giving more info to the end user.

So what does this look like?

Currently ActionGetRequest looks like this:

export interface ActionGetResponse {
  /** url of some descriptive image for the action */
  icon: string;
  /** title of the action */
  title: string;
  /** brief description of the action */
  description: string;
  /** text to be rendered on the action button */
  label: string;
  /** optional state for disabling the action button(s) */
  disabled?: boolean;
  /** optional list of related Actions */
  links?: {
    actions: LinkedAction[];
  };
  /** optional (non-fatal) error message */
  error?: ActionError;
}

we can expand it to add:

export interface ActionUnifiedResponse {
  /** url of some descriptive image for the action */
  icon: string;
  /** title of the action */
  title: string;
  /** brief description of the action */
  description: string;
  /** text to be rendered on the action button */
  label: string;
  /** optional state for disabling the action button(s) */
  disabled?: boolean;
  /** optional list of related Actions */
  links?: {
    actions: LinkedAction[];
  };
  /** optional (non-fatal) error message */
  error?: ActionError;
  /** optional b64 encoded transaction */
  transaction?: string
}

This would get rid of the ActionPostResponse completely, and just use this unified response for all blinks.

So what would this look like?

  1. User fetches the entrypoint to the blink with standard GET request and gets a list of actions
  2. User clicks on an action, which POST requests with their account to the given action URL
  3. This returns a new ActionUnifiedResponse with an optional transaction they can sign and rerender of potential links or errors the server had in case the account + path from previous request resulted in a transaction that didn’t work (like if that account was specifically timed out from interacting with that path).
  4. Loop until done

Notes

  1. Does chaining mean that servers have to hold state?

    1. Not necessarily, all state can be URL encoded as path params
  2. What are some other cool things this allows?

    1. Right now one of the biggest challenges is blink generation unique to user has to be done off platform :- if you want users to have their own blinks they have to go to telegram or discord or a website or something to generate a blink cause twitter api access is trash. With this you could give out new blinks to uses on twitter itself
3 Likes

Hi @spacemandev this is a great idea, and definitely something I’m interested in.

For a secure authenticated session to take place, there needs to still be some form of transaction or signature. In the current implementation of Blinks only transactions are supported (looks like signatures are coming soon) - and it seems that once a transaction is sent, the blink reaches a final state. I would love to see this proposal also enable multiple subsequent states after a transaction takes place

Actually, this implementation would support secure sessions. After the first transaction (which can be a memo ix or transfer of lamport from and to the user), you can give a JWT to the user encoded in the query parameters.

This allows you do to a multi state system with a secure session

1 Like

I like this general idea of “blink chaining”. Support for chaining the Actions API responses in a sequential way like this could open some interesting designs and applications for actions/blinks.

Aside: I think technically it should be title “actions chaining” since the blink is just the renderer and not always required to be used. The actions are being chained and can be client agnostic.

Questions:

  1. How many actions could be chained together? No limit or some limit?

Having no limit might be nice for some use cases, but I suspect it will lead to a lot of user drop of while submitting actions and signing transactions. Chaining alone might lead to some users getting confused when the UI gets updated on the subsequent chained action if the messaging in the previous aciton is unclear (I guess these are all general UI design best practices and not necessary actions specific though)

  1. How would the action-aware client (like a blink) know to stop requesting for subsequent chained actions? Is it simply if the proposed UnifiedResponse has no links.actions declared?

In the current actions spec, if the GET response has no links.actions is not provided, then a single button is expected to be rendered in the client and the POST request be made to the same url as the GET request (aka backwards compatible with Solana Pay transaction requests). However, if links.actions does exist, then the POST request is made to the corresponding href value for the action a user submits via button click.

  1. What should happen if the action api returns a transaction and the rest of the metadata to support chaining an additional action? Should the user be promoted to sign the transaction before rendering the new chained actions? Should it be before?

  2. Why remove the ActionPostResponse in favor of your proposed ActionUnifiedResponse that seems to only add the optional transaction field?

Even in your proposal, it seems like after the very first GET response to collect the initial metadata and available actions, you are proposing to continue to make POST requests. Why not just add the optional transaction field to the ActionPostResponse interface? If this transaction field exists, it is the action api implicitly saying “I am performing action chaining”

  1. With the idea of action chaining via your proposed “unified response” interface, should the error field become fatal? Halting the chain of events? Or should the only error drive fatal halting be when a proper http error coded response is returned from the action api?

Also, with a goal to maintains backwards compatability between action-aware clients that support action chaining and those that do not, do you have concerns or thoughts on how an action api should react if the client does not perform the chaining?

With another change to the spec for action-aware clients somehow declaring “what features they support” (which I dislike the idea of), there is no way for an action api to know if their request to chain multiple actions will actually be facilitated by the client UI

  1. How many actions could be chained together?
  • With the session based model on actions, it’d be up to the app developer to make sure they aren’t making their sessions so long that users drop off and have good handle on recovery
  1. How would the action-aware client (like a blink) know to stop requesting for subsequent chained actions? Is it simply if the proposed UnifiedResponse has no links.actions declared?
  • Yes, you could still have ActionGet flow be the kick off flow to remain backwards compatibility and have no links.actions on that. You “end” the session when the user is done pressing buttons, not when there’s no links.actions left. I’m assuming the final branch of a flow would just return disabled is true to end the session
  1. What should happen if the action api returns a transaction and the rest of the metadata to support chaining an additional action? Should the user be promoted to sign the transaction before rendering the new chained actions? Should it be after?
  • Definitely before. As a follow on, it’s also important that there’s a way to fetch the txn signature from the client and then have it in the context of the following post request so the backend can confirm it
  1. Why remove the ActionPostResponse in favor of your proposed ActionUnifiedResponse that seems to only add the optional transaction field?
  • You’re confusing ActionPostResponse with ActionGetResponse. ActionPostResponse in the current spec only has transaction and message fields. Specifically the reason you wouldn’t want to add transaction field to ActionGetResponse is because passive rendering of an action (such as through a blink on twitter) should not popup a client transaction. It’d be pretty bad UX if you were scrolling on twitter and your wallet kept popping up. Only after the first GET should there be an option of POST requests with transactions
  1. With the idea of action chaining via your proposed “unified response” interface, should the error field become fatal? Halting the chain of events? Or should the only error drive fatal halting be when a proper http error coded response is returned from the action api?
  • I don’t think the error field should become fatal. It can be used for a “try again” by users for example when the transaction cannot be confirmed or in case of other app logic.
  1. In terms of backwards compatibility, this is a tough one. You could add a client version header on the initial GET request maybe?
  • Yes, you could still have ActionGet flow be the kick off flow to remain backwards compatibility and have no links.actions on that. You “end” the session when the user is done pressing buttons, not when there’s no links.actions left. I’m assuming the final branch of a flow would just return disabled is true to end the session

Makes sense. I think using disabled to end the chaining session would be the easiest implementation for sure. For some reason, by brain does like it and it seems like not the best dev experience, but I cannot articulate why…

  • Definitely before. As a follow on, it’s also important that there’s a way to fetch the txn signature from the client and then have it in the context of the following post request so the backend can confirm it

A few people have suggested wanting some sort of “callback” functionality to verify the signature on their server side. With action chaining, it makes sense to desire this too. But on the flip side, since the transaction id is being provided by the client, it can easily be spoofed.

You’re confusing ActionPostResponse with ActionGetResponse. ActionPostResponse in the current spec only has transaction and message fields. Specifically the reason you wouldn’t want to add transaction field to ActionGetResponse is because passive rendering of an action (such as through a blink on twitter) should not popup a client transaction. It’d be pretty bad UX if you were scrolling on twitter and your wallet kept popping up. Only after the first GET should there be an option of POST requests with transactions

Ohh you are right. I misthough / mistyped. I meant in the PostResponse to add the same fields as the GetResponse. Updating the interface to be this is what I meant to suggest:

/**
 * Response body payload returned from the Action POST Request
 */
export interface ActionPostResponse extends ActionGetResponse {
  /** base64 encoded serialized transaction */
  transaction: string;
  /** describes the nature of the transaction */
  message?: string;
}

Specifically the reason you wouldn’t want to add transaction field to ActionGetResponse is because passive rendering of an action (such as through a blink on twitter) should not popup a client transaction. It’d be pretty bad UX if you were scrolling on twitter and your wallet kept popping up.

Totally agree lol. This is also why the initial GET request has no body payload since if it did, it would in theory send the user’s wallet address to every blink on the page. Bad experience and removes the user’s ability to interact or not interact with a specific blink.

  • I don’t think the error field should become fatal. It can be used for a “try again” by users for example when the transaction cannot be confirmed or in case of other app logic.

Makes sense, but the current non-fatal error does not follow this “try again” flow really.

1 Like

Currently the ActionPostResponse looks like

/**
 * Response body payload returned from the Action POST Request
 */
export interface ActionPostResponse {
  /** base64 encoded serialized transaction */
  transaction: string;
  /** describes the nature of the transaction */
  message?: string;

When the user signs the transaction, the action server has no option to verify/know if the user has signed the transaction, therefore the server needs to scan all the transactions for the given programId and check if the user has made the transaction or not. Scanning all the transactions is not a feasible approach, which can be replaced by a callback URL , the callback URL would accept a signature ( signed by the user ) and the account ( base58-encoded representation of the public key of the user ) in the body to link the transaction.

/**
 * Response body payload returned from the Action POST Request
 */
export interface ActionPostResponse {
  /** base64 encoded serialized transaction */
  transaction: string;
  /** describes the nature of the transaction */
  message?: string;
  /** callback URL to be called after the transaction is confirmed */
  callback?: string;
}

After the user has signed the transaction, then the blink-client would use the callback URL field to send a HTTP OK JSON with the following payload

/**
* Response body payload returned from the Callback Post Request
*/
export interface CallbackPostResponse {
  /** user signature */
  signature: string;
  /** base58-encoded representation of the public key of the user  */
  account: string;
}

with regards to @spacemandev’s proposal for action chaining, how does your callback idea fit into it?

dev’s proposal suggests the post response would return the optimistic UI items that should be rendered after the previous transaction is successful, allowing 1 less network request and the user to immediately interact with the next action in the chain (as if it was a freshly rendered blink)

for your callback proposal, is the purpose to simply tell the action api server that the transaction was successful or to get the next action in the chain only after it was successful? or something else?