Interactive Transaction Request Protocol

Improvement Idea: Interactive Transaction Request Protocol

Problem:

Sui’s existing payment URI scheme works well when payment details can be determined in advance. However, it does not provide a neutral way for a merchant or application to dynamically construct a Programmable Transaction Block after receiving the user’s address.

Dynamic construction is necessary for many payment and application flows, including:

  • swapping the user’s asset into the merchant’s preferred asset
  • selecting routes based on current liquidity
  • sponsoring gas
  • applying fees, discounts, or rewards
  • selecting objects owned by the user
  • interacting with Move packages
  • combining multiple actions into one atomic transaction

For example, a customer may hold EURC while a merchant accepts USDC. A payment server could construct one PTB that swaps EURC to USDC, pays the merchant, sponsors gas, and performs additional settlement actions atomically.

Sui supports these transactions, but wallets and applications currently require wallet specific links, proprietary integrations, or custom signing flows. This creates fragmentation and makes it harder for wallets, merchants, payment processors, point of sale systems, and applications to interoperate.

Description:

Add an optional r parameter to the sui:pay URI scheme. The parameter would contain a URL for an interactive transaction-request endpoint.

sui:pay?r=https%3A%2F%2Fmerchant.example%2Frequests%2Fabc123

Existing payment parameters could also be included as a fallback for wallets that do not support interactive requests:

sui:pay?receiver=0x123...&amount=1000000&coinType=0x...&r=https%3A%2F%2Fmerchant.example%2Frequests%2Fabc123

The URI could be delivered through a QR code, NFC tag, NFC enabled point of sale terminal, universal link, application link, or copied text.

A supporting wallet would check for the r parameter and initiate the following flow.

1. Retrieve request metadata

The wallet sends a GET request to the URL contained in r.

GET /requests/abc123
Accept: application/json

The server returns basic information identifying the requester:

{
  "name": "Example Merchant",
  "icon": "https://merchant.example/icon.png"
}

For the first version, the metadata response would contain only:

  • name: the merchant, application, organization, or person requesting the transaction
  • icon: an optional HTTPS image URL representing the requester

The wallet displays the name, icon, and request domain to the user before continuing.

2. Request the transaction

Next, the wallet sends a POST request to the same URL:

POST /requests/abc123
Content-Type: application/json
{
  "address": "0x123...",
  "wallet": "Slush"
}

The request contains:

  • address: the Sui address that will act as the transaction sender
  • wallet: an optional informational wallet name

The wallet name could reuse the name exposed through the Sui Wallet Standard, provided wallet developers agree that this value is sufficiently standardized.

The wallet name must not be used for authentication or security decisions. It may be useful for compatibility handling, debugging, and implementation analytics.

3. Return the transaction

The server constructs a transaction using the supplied address and returns the serialized transaction block:

{
  "transaction": "<base64-encoded serialized Sui transaction>"
}

We should discuss a canonical transaction serialization format. We may also need to support existing signatures when the transaction uses gas sponsorship:

{
  "transaction": "<base64-encoded serialized Sui transaction>",
  "signatures": [
    "<optional sponsor signature>"
  ]
}

The returned transaction may contain any valid Sui PTB. It does not have to be a payment.

The transaction could transfer assets, swap tokens, interact with a Move package, claim or mint an object, redeem a voucher, create or modify objects, or execute several application actions atomically.

Although the request is initiated through sui:pay, the interactive transaction request mechanism would be a general purpose transaction primitive.

4. Review, sign, and submit

The wallet must treat the server provided transaction as untrusted.

Before asking the user to sign, the wallet should deserialize, inspect, and simulate the transaction where possible. It should clearly present the transaction’s actual effects, including:

  • assets leaving the user’s account
  • transfers to the merchant or other addresses
  • Move calls
  • swaps and protocol interactions
  • object changes
  • gas costs and sponsorship
  • additional required signers

The wallet should verify that the transaction sender matches the address supplied in the POST request.

The user may then approve or reject the transaction. If approved, the wallet signs and submits it to the Sui network.

5. Notify the request server

After submission, the wallet may send another POST request to the same URL containing the resulting transaction digest:

{
  "digest": "TransactionDigest..."
}

This lets the server associate the submitted transaction with the original request and begin verification immediately.

The digest notification must not be treated as proof of payment or successful execution. The server must independently query the Sui network and verify that:

  • the transaction exists
  • the transaction finalized successfully
  • the sender is the expected address
  • the transaction corresponds to the transaction created by the server
  • the transaction’s effects satisfy the original request

The confirmation POST should be idempotent so the wallet can safely retry it.

Payment-specific implementations may additionally verify application events or receipt objects, but these should not be required by the protocol because the returned transaction may perform any valid Sui operation.

Backward compatibility

The r parameter would be optional.

Wallets that support interactive transaction requests would detect r and initiate the extended flow. Wallets that do not support it could ignore the parameter and process any standard payment fields included in the URI.

When the request represents a non-payment transaction and no static fallback is available, an unsupported wallet should report that it cannot process the request.

This proposal takes influence from simple interactive transaction request protocols used by other major blockchain ecosystems while adapting the model to Sui’s Programmable Transaction Blocks, object model, sponsored transactions, and finality model.

3 Likes

Thanks for the proposal, it’s specified well enough that I could pass it along as-is. I’ve escalated it to the team that owns the payment URI scheme.

Two things I’d like to get from you while it sits in their queue, since it will speed up review:

  1. On serialization, are you proposing the full TransactionData BCS, or just the transaction kind with the wallet supplying gas data? That choice changes how sponsorship and the optional signatures field work.

  2. For the confirmation POST, do you intend the digest notification to hit the same path as the initial request, or a separate one? Worth pinning down given the idempotency requirement you mention.

If you have a reference implementation or a wallet that has agreed to prototype this, send it over and I’ll attach it to the ticket. Prior art tends to move proposals along faster than the spec alone.

2 Likes

Thanks for pushing that up. I thought about serialization more and I think the protocol should support both sponsored and non-sponsored responses rather than selecting one serialization for every request.

For an unsponsored transaction, the server would return the base64-encoded BCS “TransactionKind”:

{
  "transactionFormat": "bcs-transaction-kind",
  "transaction": "<base64-encoded BCS TransactionKind>"
}

The absence of “signatures” means the server is not sponsoring the transaction. The wallet reconstructs the complete “TransactionData” by:

  • setting the sender to the address supplied in the request
  • setting the gas owner to that same address
  • selecting the gas payment
  • determining the gas price and budget
  • selecting an appropriate expiration

The wallet then reviews, simulates, signs, and submits the resulting transaction.

For a sponsored transaction, the server would instead return the base64-encoded BCS of the complete “TransactionData”, together with the sponsor signature:

{
  "transactionFormat": "bcs-transaction-data",
  "transaction": "<base64-encoded BCS TransactionData>",
  "signatures": [
    "<base64-encoded sponsor signature>"
  ]
}

In this case the wallet must verify that:

  • the transaction sender is the address supplied in the request
  • the supplied signature is valid for the returned “TransactionData”
  • the gas owner is authorized by the supplied signature
  • the gas payment, budget, price, and expiration are acceptable
  • the transaction is otherwise safe for the user to approve

Because the sponsor signature commits to the entire “TransactionData”, including “GasData”, the wallet must not modify the returned transaction. It adds the sender’s signature to the existing signature set and submits the transaction.

I would make these response forms mutually exclusive:

  • “bcs-transaction-kind” must not contain “signatures”
  • “bcs-transaction-data” must contain at least one valid pre-existing signature
  • unsigned full “TransactionData” is not supported

We could do without the transactionFormat property and let the existence of the signature field communicate sponsorship, but transactionFormat makes the BCS type explicit rather than requiring wallets to attempt decoding one of two possible types.

This is conceptually similar to Solana Pay Transaction Requests: an unsigned response allows the wallet to supply fee-related transaction data, while a partially signed response must be preserved and its existing signatures verified. The explicit format field is needed because Sui represents the incomplete and complete cases as different BCS types.

For confirmation, I think it makes sense to use a separate URL rather than sending a third kind of request to the transaction-construction endpoint.

The transaction response could optionally include it (if it’s not included, the server doesn’t require confirmation):

{
  "transactionFormat": "bcs-transaction-data",
  "transaction": "<base64-encoded BCS TransactionData>",
  "signatures": [
    "<base64-encoded sponsor signature>"
  ],
  "confirmationUrl": "https://merchant.example/requests/abc123/confirm"
}

After submission, the wallet may send:

POST /requests/abc123/confirm
Content-Type: application/json


{
  "digest": "TransactionDigest..."
}

I would require “confirmationUrl” to be HTTPS and to have the same origin as the original request URL. Wallets should use the supplied URL directly rather than deriving “/confirm” or another path themselves.

The endpoint must be idempotent for a given request and digest. Repeating the same notification must not create duplicate processing and should receive a successful response.

The notification remains advisory. The server must independently retrieve the transaction from Sui and verify its finality, sender, contents, and effects.

As far as reference implementation and prior art goes, I do not currently have a Sui reference implementation (though I can put one together) or a wallet commitment to attach.

The closest prior art I am using is:

  • Solana Pay Transaction Requests for the GET metadata, POST account, and server-constructed transaction flow, including its distinction between unsigned and partially signed responses
  • BIP72 for extending an existing payment URI with an optional “r” request URL while retaining static payment fallback fields
  • BIP70 for returning a separate payment/callback URL and requiring retry-safe handling of repeated notifications. Though, this is mainly relevant here as historical transport and acknowledgement prior art. I am not proposing that Sui adopt its certificate, refund, or Bitcoin-specific payment-message design.
1 Like

Here is a minimal reference implementation in JavaScript:

https://github.com/kodaxx/sui-transaction-requests-reference

1 Like

@goodylili is there a channel in either Discord/TG/Other where I could help facilitate discussion around this directly?

1 Like

Hi @kodaxx! Feel free to share your work and contributions in the shinami riders TG group or the #dev-discussion Discord channel. I would highly recommend starting with shinami riders, as there are usually more active discussions and brainstorming happening there. It’s a great place to get direct feedback.

Plus, if other builders agree on your proposal and see an urgent need for the fix, it will help you catch the foundation’s attention and speed up the SIP discussion and submission process!

Thanks for the heads up. I’ve found a few links to the Suinami TG group online but all links have expired. Any idea where I can find a link?

1 Like

can you help add my TG account (@jarekkkkk) ? the group is invite only, i can help add you in

1 Like