Last active
May 9, 2019 02:10
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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