1

I am building a system that requires the client to pay for the transaction. Once the user has successfully paid for the transaction, my backend will give the user some Web2 benefits. The main problem that I am facing is, how I can make sure that the user has:

  1. Signed the expected transaction.
  2. If the user paid for the correct transaction, the user should be getting the web2 benefits even if the backend server went down at the time of the user signing the transaction later when the server gets back up by clicking on some "Revalidate" button.

I came up with the following flow to make the above work but I am not completely sure if that is the right way to it, or if there is a better way:

  1. create the transaction in the backend.
  2. save serializedTransactionAsStringWithoutSignatures in web2 DB.
  3. send serializedTransactionAsStringWithSignatures to the client.
  4. the client will receive the encoded transaction, sign it, & and pay for it.
  5. the signed transaction will again get sent to backend.
  6. the backend will decode the transaction & remove the signatures and compare it with the transaction (serializedTransactionAsStringWithoutSignatures) saved in web2 DB.
  7. if both match, the backend will save the transaction signature in web2 DB.
  8. then the backend will send a request to the blockchain to process the transaction.
  9. once the blockchain transaction is confirmed, the user will receive some web2 benefits.

I think this flow will be able to handle network failures like let's say the server shuts down between steps 8 & 9, still, the transaction signature is already saved in web2 DB, and we can later use that to verify the transaction once the server comes back up.

Open to better alternatives and suggestions, thanks!

1 Answer 1

2

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.

3
  • 1
    Quite an insightful technique! how can I parse and verify the transaction? I assume it can bit tricky considering it can have multiple instructions in it, each using a different program. I have implemented the solution using your suggested way of signing the transaction in the backend, the only difference is that I am also using the inert account for signature instead of signing using the backend key, then I use the inert account public key for both fetching the signatures and also for validating the signatures, it can helpful in scenarios where backend key is compromised I guess. Tyvm ;)
    – Nishchit
    Commented Nov 22 at 14:39
  • I'm glad it helped! Yea you probably just want to check a small number of programs. Basically just decode the instructions you need to. I wrote this example for the new web3js version of how to parse a transaction which might help: github.com/solana-labs/solana-web3.js/blob/master/examples/… If you have any specific questions about it though then I'd suggest opening a new question because these comments are very limited.
    – Callum M
    Commented Nov 22 at 18:16
  • 1
    Yeah just went through the example you wrote for parsing a transaction, and it makes complete sense! I'll create a new question if I have any more doubts. Thanks!
    – Nishchit
    Commented Nov 23 at 17:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.