Skip to content

Instantly share code, notes, and snippets.

@bumi
Last active May 9, 2019 02:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bumi/7cf797bc084a23a98023 to your computer and use it in GitHub Desktop.
Save bumi/7cf797bc084a23a98023 to your computer and use it in GitHub Desktop.
example client/wallet code for the Bitcoin Payment Protocol BIP70 - https://github.com/bumi/bip70-example
# also have a look at the nice Takecharge Server: https://github.com/controlshift/prague-server and its BOP70 implementation this is based on
class PaymentRequest
def initialize(options)
@options = options
output = create_output
details = create_payment_details(output)
@payment_request = Payments::PaymentRequest.new
@payment_request.payment_details_version = 1
@payment_request.serialized_payment_details = details.to_s
@payment_request.pki_type, @payment_request.pki_data = create_pk_infrastructure
@payment_request.signature = create_signature(@payment_request.to_s)
end
def to_s
@payment_request.to_s
end
private
def create_output
output = Payments::Output.new
output.amount = @options[:amount]
output.script = BTC::Address.parse(@options[:address]).script.data
output
end
def create_payment_details(output)
payment_details = Payments::PaymentDetails.new
payment_details.network = @options[:test_mode] ? 'test' : 'main'
payment_details.time = Time.now.to_i
payment_details.expires = (Time.now + 3600).to_i
payment_details.memo = @options[:memo]
payment_details.payment_url = @options[:payment_url]
payment_details.merchant_data = @options[:merchant_data]
payment_details.outputs << output
payment_details
end
def create_pk_infrastructure
if SIGNED_CERT && !CERT.nil?
pki_data = create_pki_data
['x509+sha256', pki_data.to_s]
else
['none', '']
end
end
def create_pki_data
pki_data = Payments::X509Certificates.new
CERT.each_line("-----END CERTIFICATE-----\n") do |cert|
pki_data.certificate << OpenSSL::X509::Certificate.new(cert).to_der
end
pki_data
end
def create_signature(data)
private_key = OpenSSL::PKey::RSA.new(PRIVATE_KEY)
private_key.sign(OpenSSL::Digest::SHA256.new, data)
end
end
# https://github.com/controlshift/prague-server
# look also at their BIP70 implementation
require 'protocol_buffers'
module Payments
# forward declarations
class Output < ::ProtocolBuffers::Message; end
class PaymentDetails < ::ProtocolBuffers::Message; end
class PaymentRequest < ::ProtocolBuffers::Message; end
class X509Certificates < ::ProtocolBuffers::Message; end
class Payment < ::ProtocolBuffers::Message; end
class PaymentACK < ::ProtocolBuffers::Message; end
class Output < ::ProtocolBuffers::Message
set_fully_qualified_name "payments.Output"
optional :uint64, :amount, 1, :default => 0
required :bytes, :script, 2
end
class PaymentDetails < ::ProtocolBuffers::Message
set_fully_qualified_name "payments.PaymentDetails"
optional :string, :network, 1, :default => "main"
repeated ::Payments::Output, :outputs, 2
required :uint64, :time, 3
optional :uint64, :expires, 4
optional :string, :memo, 5
optional :string, :payment_url, 6
optional :bytes, :merchant_data, 7
end
class PaymentRequest < ::ProtocolBuffers::Message
set_fully_qualified_name "payments.PaymentRequest"
optional :uint32, :payment_details_version, 1, :default => 1
optional :string, :pki_type, 2, :default => "none"
optional :bytes, :pki_data, 3
required :bytes, :serialized_payment_details, 4
optional :bytes, :signature, 5
end
class X509Certificates < ::ProtocolBuffers::Message
set_fully_qualified_name "payments.X509Certificates"
repeated :bytes, :certificate, 1
end
class Payment < ::ProtocolBuffers::Message
set_fully_qualified_name "payments.Payment"
optional :bytes, :merchant_data, 1
repeated :bytes, :transactions, 2
repeated ::Payments::Output, :refund_to, 3
optional :string, :memo, 4
end
class PaymentACK < ::ProtocolBuffers::Message
set_fully_qualified_name "payments.PaymentACK"
required ::Payments::Payment, :payment, 1
optional :string, :memo, 2
end
end
require 'sinatra'
require 'btcruby'
require 'rest-client'
require './payments.pb'
require './payment_request'
# configure your certificate files
CERT = File.read File.join(File.dirname(__FILE__), 'cert/cert.crt')
PRIVATE_KEY = File.read File.join(File.dirname(__FILE__), 'cert/private.key')
SIGNED_CERT = false # you should get a certificate signed by a accepted root certificate
# /invoice is called by the wallet to receive the Payment Request
# a possible bitcoin URL could be: bitcoin:1D3PknG4Lw1gFuJ9SYenA7pboF9gtXtdcD?amount=100000&r=https://yourdomain.com/invoice
get '/invoice' do
amount = (params[:amount] || 100000).to_i
address = params[:address] = '1D3PknG4Lw1gFuJ9SYenA7pboF9gtXtdcD'
test_mode = !params['test_mode'].nil?
memo = params[:memo] || 'merchant server says hello'
# using the PaymentRequest class to create the payment request.
payment_request = PaymentRequest.new(amount: amount, address: address, test_mode: test_mode, memo: memo, payment_url: 'http://localhost:4567/ack')
headers['Content-Type'] = 'application/bitcoin-paymentrequest' # set the proper Content-Type, see BIP71
headers['Content-Disposition'] = 'inline; filename=demo.btcpaymentrequest'
headers['Content-Transfer-Encoding'] = 'binary'
headers['Expires'] = '0'
headers['Cache-Control'] = 'must-revalidate'
payment_request.to_s
end
# we have passed the URL to /ack in the payment request.
# the wallet will send the payment with its transactions to this URL
# we process/broadcast these transactions and return an ACK
# please not that publishing the transactions does NOT mean they get confirmed you still should make sure that the payment is received
#
# also in this example wo do not do any validation of the transactions. You would want to validate that it fulfills the payment request and sends you the requested amount
post '/ack' do
request.body.rewind
# parse the payment from the wallet and get the HEX values of the embedded transactions
payment = Payments::Payment.parse(request.body.read)
transactions_hex = payment.transactions.map {|t| t.unpack('H*').first }
transactions_hex.each do |t|
# normally you want to validate the transaction and then broadcast it to the bitcoin network
# we use here the blockr API to simply publish the transaction using a HTTP post request
r = RestClient.post 'http://btc.blockr.io/api/v1/tx/push', hex: t
end
#create the ACK and return a nice confirmation message
ack = Payments::PaymentACK.new
ack.payment = payment
ack.memo = 'Thanks, you are awesome. Your payment is processed'
headers['Content-Type'] = 'application/bitcoin-paymentack' # again set the proper Content-Type
headers['Content-Disposition'] = "inline; filename=i#{Time.now.to_i}.bitcoinpaymentack"
headers['Content-Transfer-Encoding'] = 'binary'
headers['Expires'] = '0'
headers['Cache-Control'] = 'must-revalidate, post-check=0, pre-check=0'
ack.to_s
end
// setup a wallet
// example using the WalletAppKit: https://github.com/bitcoinj/bitcoinj/blob/master/examples/src/main/java/org/bitcoinj/examples/Kit.java
NetworkParameters params = TestNet3Params.get();
WalletAppKit kit = new WalletAppKit(params, new File("."), "walletappkit-example");
kit.startAsync();
kit.awaitRunning();
// ok let's look at the payment protocol stuff:
// up to you where your app get the URL from. For example from scanning a QR code or when the user clicks on a bitcoin: link and your wallet has registered a protocol handler
String url = "https://example.com/invoice/42"; // or: bitcoin:1LCBEVPm4BpHb89Vv6LKSNE1gaPSsJe7YL?amount=1.42&r=https://example.com/invoice/42
ListenableFuture<PaymentSession> future;
if (url.startsWith("http")) { // if we directly have gotten an URL to a payment request.
future = PaymentSession.createFromUrl(url);
} else if (url.startsWith("bitcoin:")) {
future = PaymentSession.createFromBitcoinUri(new BitcoinURI(url)); // getting the payment request URL from bitcoin:..?r=URL
}
PaymentSession session = future.get(); // bitcoinj requests the URL and parses the payment request which is returned as protocol buffer. see:
String memoFromMerchant = session.getMemo(); // the message from the merchant. Probably says what your are paying for.
Coin amountToPay = session.getValue(); // the amount you have to pay
PaymentProtocol.PkiVerificationData identity = session.verifyPki(); // botcoinj verifies the request. The merchant has to sign the payment request using a certificate signed from a from the wallet's computer "trusted" root authority
boolean isVerified = identity != null;
System.out.println("Memo: " + memoFromMerchant);
System.out.println("Amount: " + amountToPay.toFriendlyString());
System.out.println("Date: " + session.getDate());
if(isVerified) {
System.out.println("Verification:");
System.out.println("Name: " + identity.displayName); // only when the payment request is verified we can display the name to whom we are paying to
System.out.println("verified by: " + identity.rootAuthorityName);
}
// payment requests are only valid for a certain amount of time. Don't send money if it is expired
if (session.isExpired()) {
System.out.println("request is expired!");
} else {
// now the user would have to confirm the transaction.
Wallet.SendRequest req = session.getSendRequest(); // get a SendRequest creatin transactions that fulfill the payment request
kit.wallet().completeTx(req); // adding transaction outputs, sign inputs. see: https://bitcoinj.github.io/javadoc/0.13.5/org/bitcoinj/core/Wallet.html#completeTx-org.bitcoinj.core.Wallet.SendRequest-
String refundAddress = "mjhr9mQqCNpuzcjjFRq71MbUBA9Dv8SoPV"; // we can send a refund address
String customerMemo = "thanks for your service"; // and a message to the merchant
ListenableFuture<PaymentProtocol.Ack> paymentFuture = session.sendPayment(req.tx, refundAddress, customerMemo);
if(future != null) { // null if the merchant has not provided a payment_url that we should send the transactions to
PaymentProtocol.Ack ack = future.get(); // the ack holds the response from the merchant after posting the payment to the provided payment_url
kit.wallet().commitTx(req.tx); // commit the transaction, sets the spent flags. see: https://bitcoinj.github.io/javadoc/0.13.5/org/bitcoinj/core/Wallet.html#commitTx-org.bitcoinj.core.Transaction-
System.out.println("Transaction sent");
System.out.println("Ack memo from server: " + ack.getMemo()); // the user gets instant feedback about his payment.
} else {
// the merchant has NOT provided a payment_url in the request. which means we simply broadcast the transaction
Wallet.SendResult sendResult = new Wallet.SendResult();
sendResult.tx = req.tx;
sendResult.broadcast = kit.peerGroup().broadcastTransaction(req.tx);
sendResult.broadcastComplete = sendResult.broadcast.future();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment