The third in a series from our engineering team focused on the intersection of money and technology. See our previous from J. Otto on Why Blockchains matter. Check out our open engineering roles here.
Most are familiar with Stripe's "achievement" of simplifying online credit card payments for web merchants to 2 Jira tasks:
But Bitcoin pioneered a new era of "send money to my public key". An abstraction for the idea of money so simple yet effective enough to do what previously required: Stripe -> credit card companies -> banks -> governments.
Wanna receive Bitcoin as a web merchant? Generate a keypair, hash the public key, email/text the hash (or QR code) to the customer/payer. No KYC, no 3rd party custodian, no governments, and it can be done offline. Instant settlement bliss.
"Come on, all devs and Bitcoin fanatics say that! Talk is cheap! Show me the code!"
It's so easy that the only thinking ("brain damage" as the @TwoBitIdiot would say) you'll have to do is how to manage (rotate, and cold storage) your private keys securely (outside the scope of this article).
We'll use Node.js and the following NPM packages (NPM being the Node.js package manager du jour):
And 2 of our own functions:
createBitcoinAddress(), and getBalanceOfAddressInSatoshis()
Technically we could receive all money in a single Bitcoin address, but creating addresses per invoice is a “best practice” because it gives us: pseudo-anonymity, and simpler billing business logic (does the expected balance for an account/invoice exist or not).
So if we're creating Bitcoin addresses (public keys) at scale, here are 2 options:
We'll use the seed phrase approach for 3 reasons: the ability to monitor all addresses created from that seed phrase in the Electrum desktop wallet app, so we only have to store an index, and so we can inject the production seed phrase via environment variable so the keys aren't in a codebase or database.
Here's what it looks like:
const bitcoin = require("bitcoinjs-lib");
// Electrum has their own methodology for seed phrases
// https://electrum.readthedocs.io/en/latest/seedphrase.html
// Use this library to parse them for us
const mn = require("electrum-mnemonic");
function createBitcoinAddress(hdIndex) {
// The 12-word seed phrase generated when creating
// a new Electrum wallet (and most other wallets)
const seedPhrase = process.env.SEED_PHRASE;
const seed = mn.mnemonicToSeedSync(seedPhrase, {
prefix: mn.PREFIXES.segwit,
});
const root = bitcoin.bip32.fromSeed(seed, bitcoin.networks.bitcoin);
// https://ethereum.stackexchange.com/a/70029
// use the same "derivation path" that Electrum uses
// so the seed phrase maps to the same addresses
// that Electrum would show
const node = root.derivePath(`m/0'/0/${hdIndex}`);
const bech32Address = bitcoin.payments.p2wpkh({
pubkey: node.publicKey,
network: bitcoin.networks.bitcoin,
}).address;
return bech32Address;
}
But what value for the hdIndex parameter should you pass? One simple option is using an auto incrementing serial bigint column from an “invoices” table in a relational database. The database will safely ensure uniqueness of that column so we can use it for creating unique (but retrievable) Bitcoin addresses.
In our frontend, we'd present the address returned by this function call: createBitcoinAddress(invoice.id) and an amount due. Our customer can paste that address into their Bitcoin wallet (we can also present this Bitcoin address as a QR code) and enter/paste the appropriate amount to be paid.
Now for getBalanceOfAddressInSatoshis(), we'll actually need to read from the Bitcoin network. We could run and query our own Bitcoin node directly, or we can hit a public Bitcoin gateway offered by plenty of vendors competing for YOUR business. 2 examples from the first page of google: Sochain or Blockcypher.
const got = require("got");
const sb = require("satoshi-bitcoin");
function getBalanceOfAddressInSatoshis(address) {
// 1. this data can come from a Bitcoin node or
// any Bitcoin gateway service like sochain,
// blockcypher
// 2. wait for at least 1 (usually this should be 6+)
// confirmation(s) before considering it ours
// 3. parse the stringified bitcoin units into
// numeric satoshi units
// 4. sum the results, and yes, JavaScript’s
// Number.MAX_SAFE_INTEGER > 2100000000000000
// (which is the total number of Bitcoin satoshi
// units that will ever exist)
return got
.get(`https://sochain.com/api/v2/get_tx_received/BTC/${address}`)
.json()
.then((res) => res.data.txs)
.then((txs) => txs.filter((tx) => tx.confirmations > 0))
.then((txs) => txs.map((tx) => sb.toSatoshi(tx.value)))
.then((txSatoshis) => txSatoshis.reduce((memo, s) => memo + s, 0));
}
The final part of accepting a payment is the general business logic that would be similar for Stripe or Bitcoin: did I get paid or not?! Here's what that could look like:
const sb = require("satoshi-bitcoin");
const bitcoinAddress = createBitcoinAddress(invoice.id);
const balanceOfAddressInSatoshis = await getBalanceOfAddressInSatoshis(
bitcoinAddress
);
const balanceOfAddressInBitcoin = sb.toBitcoin(balanceOfAddressInSatoshis);
const paymentDueInBitcoin = 0.01;
if (balanceOfAddressInBitcoin >= paymentDueInBitcoin) {
// 1. update invoices set paid_at = now() where id = $1
// 2. deliver value to customer for completed payment
// 3. possibly send all or some of this to a cold wallet
}
That's it! Bitcoin is readily usable in apps and servers with just a few lines of code. Stripe is easy, Bitcoin is easier.
-J. Otto
Come work with us!
If you’re a software engineer interested in helping us contextualize and categorize the world’s crypto data, we’re hiring. Check out our open roles for frontend and backend engineers and drop us a line at careers@messari.io.