This article will help you understand how to integrate ETH and ERC20 tokens as a payment option in an arbitrary service (shop, exchange, token sale, etc.). We'll explore the possible implementation pitfalls and ways of avoiding them. Due to its nature, the work with ERC20 tokens is identical to the work with any kind of Ethereum smart contract events.
In the first part of this article, we'll focus on ETH, inthe second – on ERC20 tokens.
Let’s outline the task. I need to receive ETH payments, (1) link them to a specific purchase, and (2) accumulate in a single wallet. In the off-chain world of bank transfers, this issue is usually addressed in the following way. I register a bank account and give its details to customers along with a unique purpose (which is a universally available, a standard field in a bank transfer details). A sender must specify this information while making the transaction. In this way, (2) is achieved naturally as all the transfers end up in the same place, and (1) is achieved by looking for purpose in the incoming payments.
Let’s now apply the same architecture to Ethereum. I create an Ethereum account, give its address to customers along with a unique data that sender must specify in the transaction. Again, (2) is achieved naturally as all the senders will use the same address, and (1) is achieved by looking for data in the incoming transactions. Here is theexample of such a transaction, data is a hex encoded ‘payment#12345’ string.
The problem here is that, on the contrary to a bank transfer purpose, a data field is rarely available for a sender to specify. If a sender uses MEW, MetaMask, or other private general purpose Ethereum wallets, then they have access to the data field. If a sender pays by any other means (e.g., a withdraw from an exchange), he will not be able to specify the data, and I won’t be able to link it to a purchase. One of the possible solutions is to add an alert on the payment page that says “Only payments from private Ethereum wallets are allowed.” It will make the whole thing inconvenient for a part of the users, and such situation will result in a revenue loss. I don’t want a solution that will restrict users from using it. From now on, I will deem any solution unsuitable if it adds any conditions on how the payment should be performed.
What if I create unique wallets for every purchase? This operation is fast and free and solves (1) naturally. To solve (2), I will have to add one more step to payment processing and sweep incoming ETH to my main wallet. In this solution, users can send ETH by any means, and as soon as I receive it, I can link the payment to the purchase and process it. But how do I know that payment already hit the unique wallet? What will trigger an actual payment processing? A naïve way to go is by polling the balance of a unique wallet every time a new block is mined (about 15 seconds) till the moment the balance is greater than 0 and then proceed with handling. This is, by the way, exactly how the Ambisafe’s EtherSweeper is implemented. This approach, though absolutely failsafe, have some scalability issues. What if I have 100,000 pending purchases? It means that I have to do 100,000 balance checks every 15 seconds, so 6600+ requests/second. After what time of no payment should I stop polling a particular wallet? What if I stop, but the payment arrives later? I can add some heuristics and restart polling based on a user's actions, like checking the purchase status. Or I can trigger the wallet processing manually, in case the user raise a support ticket. No matter which optimizations I apply, it seems that the number of simultaneous polling requests will increase linearly over time, assuming that my service is performing and the number of users/purchases increases.
How can I redo the trigger logic so that it works in real time and doesn’t require users to contact the support? I can inspect every transaction to see if there were a receiver from my list of wallets. Now it doesn’t matter how many pending purchases I have. What matters is how many transactions there is to parse. So, if there are 10 million transactions per day, then I only need to be fast enough to parse 120 transactions per second. And it is doable.
Let’s dive into details on a transaction parser implementation. For the sake of simplicity, I’m not even interested in the amount of incoming transaction. As long as I notice one of my unique wallets in there, I trigger a balance check and do the sweep.
There is a To field in every Ethereum transaction. So, I’m just going to check if it matches any of my wallets and to trigger processing if it does. Now everything is covered, right? But is there any other way ETH might hit my wallet without having my address in the To field?
Here I see that To field doesn’t match my wallet, though ETH arrived. How can I trigger processing in such case? Note: the previous approach with the balance polling processes, in this case, is out of the box. the previous approach with the balance polling processes, in this case, is out of the box. The only feasible way to identify such transactions, currently, is by using aParity trace module’s trace_replayTransaction. It will show me all the internal transactions.
And again, there is a To field in every internal transaction. So, I’m just going to check if it matches any of my wallets and to trigger processing if it does. I know that the first entry in the trace is the original external transaction, so I now have a single routine to find them all. But wait, there is yet another, hopefully, the last way of sending/receiving ETH I need to deal with.
It is called a selfdestruct. I assume it is safe to leave this case to the support. I've never heard of anyone paying for something in this way. I don’t like an idea about an unaddressed case, so I will just add another check to my parser. If the refundAddress of an internal transaction is my wallet, then I trigger processing for it.
To my knowledge, a vast majority of ETH integrations only triggers the payment processing for the first situation (e.g., an initial transaction To their wallet). They completely or partially ignore the second case. Some of them will trigger the processing manually through the support; others will just take money to themselves, arguing that “Only normal transactions are accepted.” I want to emphasize that all cases described above are normal because they all are the part of Ethereum, and that is how Ethereum works. It isforecastedthat in the future all ETH transfers will be of the second type (e.g., through an internal transaction).
“... we take initial steps towards a model where, in the long term, all accounts are contracts...”