There are a few features of Solana that work really nicely to enable this flow. Here's how I'd do it:
- When the user requests a transaction from the server, generate a random keypair and store the public key in your database against the user as a reference key.
- Now generate your transaction, and add this reference key to any instruction. Make it not writable and not a signer, so it's just an inert account referenced by the transaction. Also make sure your backend key is a signer on the transaction.
- Sign the transaction with your backend key, and then serialize the transaction and send it to the frontend.
- The user can sign and send the transaction in any way that makes sense
Now to detect the transaction being confirmed, you probably want to use the signatureSubscribe
subscription method. The signature that you'll want to subscribe to is the first signature on the transaction, ie the fee payer. So if you're paying the network fee then your backend server will know this as soon as it signs it. If the user is the fee payer, then you won't know this until they sign it.
You could have them send the transaction signature to the backend, and then initiate the subscription on your server. Or you could have the frontend subscribe and only send the signature to the backend after the transaction has confirmed.
Either way, once the transaction is confirmed the backend just needs to fetch the transaction from the network (using the signature), and verify that it is a signer on it and it was also signed by the correct user - that proves that the transaction originated from your server and is what you expect.
If the backend goes down or there's some other issue and you need to refetch, all you need to do is look up the reference key in your database, make a single RPC call to the getSignaturesForAddress
RPC method and you should get back one transaction signature. Then you just need to verify that this transaction is signed by you, and was also signed by the expected user.
Note that you could also poll getSignaturesForAddress
instead of using the signatureSubscribe
to detect the transaction being confirmed. But this will be slower and less efficient.
Your current design relies on the transaction the user signs being identical to the one you generated, which is not a safe assumption since you're not partially signing the transaction. If you don't sign the transaction, you should assume that wallets might do things like replacing the blockhash with a more recent one, or adding priority fee instructions to make the transaction land quicker. If you sign it before sending it to the user, the wallet won't be able to do this though.
If you'd like to allow the wallet to do this (it generally has a better view of network conditions etc so this can make transactions more reliable) then you need to not sign the transaction. This is ok, but you'll need to parse the transaction to verify that it does what you expect in this case, instead of just checking that you signed it. This does also mean that the user will need to be the fee payer, since they're the only signer.