MetacoSA / NBitcoin

Comprehensive Bitcoin library for the .NET framework.
MIT License
1.87k stars 846 forks source link

Question BIP70, 71 ,72 and 73 #263

Closed layinka closed 6 years ago

layinka commented 7 years ago

Hi, Does Nbitcoin support Paymentrequests?

Where can i find sample code or sample implementation using nbitcoin? I cant find any mention of this in the ebook.

NicolasDorier commented 7 years ago

Yes it supports, check PaymentTests.cs.

var uri = server.GetPaymentRequestUri(2);
BitcoinUrlBuilder btcUri = new BitcoinUrlBuilder(uri);
var request = btcUri.GetPaymentRequest();
Assert.True(request.VerifySignature());
Assert.Equal(2, BitConverter.ToInt32(request.Details.MerchantData, 0));
var ack = request.CreatePayment().SubmitPayment();
Assert.NotNull(ack);
layinka commented 7 years ago

Thanks,i am trying that now.

Please can you confirm how i can handle Certificates.

I am using NBitCoin with a .net Core app, and i am getting this error.

DefaultCertificateServiceProvider or CertificateServiceProvider must be set before calling this method

Thanks

NicolasDorier commented 7 years ago

@layinka ok thanks a lot, new version 4.0.0.10 should solve the situation, can you update your package and let me know how it goes ?

It seems I deactivated certificate verification on .NET Core because I was unaware I could do it, I just fixed that.

layinka commented 7 years ago

Great, That works.

But a few more questions?

So how do i specify certificates in .Net core, will that be handled automatically ? or do you have a sample implementation?

Do you know how i can write out the request in .net core? This is the code i have presently, but i dont think its getting signed accurately, and something is definitely broken as the wallets cant understand the output. Please can you help check?


public IActionResult GetBitcoinPaymentRequest(string payId){
    string baseUrl = "http://localhost:5000";

    foreach(var provider in new ICertificateServiceProvider[]{})
    {
        PaymentRequest.DefaultCertificateServiceProvider = provider;    
    }

    PaymentRequest request = new PaymentRequest();

    request.Details.Outputs.Add(new PaymentOutput(new Money(0.00001m, MoneyUnit.BTC), new BitcoinPubKeyAddress("addrs1").ScriptPubKey )); //Destination 
    request.Details.Outputs.Add(new PaymentOutput(new Money(0.00001m, MoneyUnit.BTC), new BitcoinPubKeyAddress("addrs2").ScriptPubKey));// dest charge 2

    request.Details.MerchantData = ASCIIEncoding.ASCII.GetBytes(payId.ToString());

    request.Details.PaymentUrl = new Uri(baseUrl + "?id=" + payId);

    request.Sign(System.IO.File.ReadAllBytes("data/cert.pfx"), PKIType.X509SHA256);

    request.WriteTo(Response.Body);

    return Ok();

}
NicolasDorier commented 7 years ago

Normally now the correct DefaultCertificateServiceProvider is set for .NETCore, and you should not touch it.

The test CanCreatePaymentRequest in PaymentTests show how to sign. Your request seems fine to me, check it is correctly signed with

Assert.True(request.VerifySignature());
Assert.True(request.VerifyChain());

Wallets might reject if your certificate is not trusted.

Nicolas,

NicolasDorier commented 7 years ago

You can try your own server with:

BitcoinUrlBuilder.GetPaymentRequest

I guess your problem is that your server does not set the content type.

layinka commented 7 years ago

Hi Nicolas, I am not sure i understad how i can use

BitcoinUrlBuilder.GetPaymentRequest

When i try using it,i either get a Task Error or i get an error saying No Valid PaymentRequest.

Let me explain what i am trying to do again, so maybe you can help better.(The fact that the final request is a binary format that i cannot open to see is also very limiting)

I want to automate the payment on my site so that when a user clicks a link like bticoin:?r=myweburl/myproductid , i can use the myproductid to detect the price the user wants to pay and then compose a paymentrequest matching the price and send it to some multiple outputs.

Is this something you have implemented before? I cannot use the sample codes you have in your test files as they are mixed with test codes that i am not sure i understand.

And i have been able to set the content type from the server so thats not a problem, i suspect the problem is in the actual response written out, as even when i send out a PaymentRequest that is not signed (PKIType.None), i get an error of "Invalid Paymentrequest Url" on the wallet.

Here is my code once again.

public IActionResult GetBitcoinPaymentRequest(string payScheduleId){
            string baseUrl = "http://localhost:5000";            
            // BitcoinUrlBuilder bb=new BitcoinUrlBuilder(){
            //     PaymentRequestUrl = new Uri(baseUrl + "/api/paywithbtc/tre23")
            // };
            // PaymentRequest request = bb.GetPaymentRequest(); 
            PaymentRequest request = new PaymentRequest();
            request.Details.Outputs.Add(new PaymentOutput(new Money(0.001m, MoneyUnit.BTC), new BitcoinPubKeyAddress("wallet address").ScriptPubKey )); //Destination 
            request.Details.Outputs.Add(new PaymentOutput(new Money(0.001m, MoneyUnit.BTC), new BitcoinPubKeyAddress("TWT wallet address").ScriptPubKey));//Transaction charge

            request.Details.MerchantData = ASCIIEncoding.ASCII.GetBytes(payScheduleId.ToString());
            request.Details.Memo= "Payment for ...";
            request.Details.Network= Network.Main;

            request.Details.PaymentUrl = new Uri(baseUrl + "?id=" + payScheduleId);

            request.Details.Expires = new DateTimeOffset(2017,7,25,22,0,0,0,TimeSpan.Zero);

            request.PKIType = PKIType.None; //Unsigned
            // request.Sign(System.IO.File.ReadAllBytes("data/cert.pfx"), PKIType.X509SHA256);

            Response.ContentType="application/bitcoin-paymentrequest";

            Response.Headers.AddOrReplace("Content-Transfer-Encoding","binary");
            request.WriteTo(Response.Body);
            return Ok();

}
NicolasDorier commented 7 years ago
new BitcoinUrlBuilder("bitcoin://bitcoinaddress?r=http://localhost:5000/GetBitcoinPaymentRequest/2939393").GetPaymentRequest()
NicolasDorier commented 7 years ago

Your error is with the URL, not with the paymentrequest.

layinka commented 7 years ago

Thanks for always answering Nicolas.

To clarify, Is this what I should place as a link in my application? if its not, then what?

<a href="bitcoin://bitcoinaddress?r=http://localhost:5000/GetBitcoinPaymentRequest/2939393">Pay</a>

and then change my code to

public IActionResult GetBitcoinPaymentRequest(string payScheduleId){
            string baseUrl = "http://localhost:5000";            
            BitcoinUrlBuilder bb=new BitcoinUrlBuilder("bitcoin://bitcoinaddress?r=http://localhost:5000/GetBitcoinPaymentRequest/2939393");

            PaymentRequest request = bb.GetPaymentRequest(); 
            // PaymentRequest request = new PaymentRequest();
            request.Details.Outputs.Add(new PaymentOutput(new Money(0.001m, MoneyUnit.BTC), new BitcoinPubKeyAddress("wallet address").ScriptPubKey )); //Destination 
            request.Details.Outputs.Add(new PaymentOutput(new Money(0.001m, MoneyUnit.BTC), new BitcoinPubKeyAddress("TWT wallet address").ScriptPubKey));//Transaction charge

            request.Details.MerchantData = ASCIIEncoding.ASCII.GetBytes(payScheduleId.ToString());
            request.Details.Memo= "Payment for ...";
            request.Details.Network= Network.Main;

            request.Details.PaymentUrl = new Uri(baseUrl + "?id=" + payScheduleId);

            request.Details.Expires = new DateTimeOffset(2017,7,25,22,0,0,0,TimeSpan.Zero);

            request.PKIType = PKIType.None; //Unsigned
            // request.Sign(System.IO.File.ReadAllBytes("data/cert.pfx"), PKIType.X509SHA256);

            Response.ContentType="application/bitcoin-paymentrequest";

            Response.Headers.AddOrReplace("Content-Transfer-Encoding","binary");
            request.WriteTo(Response.Body);
            return Ok();

}

But wont that become a cyclic and recursive call, it looks like its calling itself

NicolasDorier commented 7 years ago

Yes, this is what you need, with bitcoinaddress has to be the destination address, this is in case the wallet does not support BIP37.

There is no recursion, PaymentUrl is different from the r= address as you can see https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki

layinka commented 7 years ago

Hi,

Ok, i will try it out, but how will i handle mutliple outputs if the wallet doesnt support BIP37 , i dont think i can add the two adresses to the url or can i?

Thanks

layinka commented 7 years ago

Just tried this,

new BitcoinUrlBuilder("bitcoin://bitcoinaddressr=http://localhost:5000/GetBitcoinPaymentRequest/2939393")
   .GetPaymentRequest()

i got this error

Exception has occurred: CLR/System.Threading.Tasks.TaskCanceledException
An exception of type 'System.Threading.Tasks.TaskCanceledException' occurred in System.Private.CoreLib.ni.dll but was not handled in user code: 'A task was canceled.'
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at NBitcoin.Payment.BitcoinUrlBuilder.GetPaymentRequest()
layinka commented 7 years ago

Hi, I keep getting that TaskcancelledException when i try to use the bitcoin builder, but it doesnt show up if i build the paymentrequest directly.

Since the main error i am gettig on the wallet is

Payment Request Url is invalid

Could this error be showing up on the wallets because i am using a localhost address? Do you know if this could be what is causing it, because its even showing up for unsigned requests.

NicolasDorier commented 7 years ago

Please give me the Bitcoin url you are using, this is a parsing error, I think you misunderstand the URI format of bitcoin://

layinka commented 7 years ago

I am testing on my localhost server. So you might not have access to the url.

but essentially the url is something in the anchor tag looks like this <a href="bitcoin:n1wKTykkSox49o2FzQfyewmNZTnjQSeUxn?r= http://localhost:5000/api/groups/paywithbtc/tre23 ">Pay

When this is clicked, the wallet opens and is supposed to call the api via http://localhost:5000/api/groups/paywithbtc/tre23 which should then return the Payment request.

Thats why i was asking that wont using bitcoinUrlBuilder the way you were mentioning lead to a cyclic call, since getPaymentRequest on BitcoinUrlBuilder also calls http://localhost:5000/api/groups/paywithbtc/tre23 behind the scene.

On Fri, Jul 28, 2017 at 3:59 PM, Nicolas Dorier notifications@github.com wrote:

Please give me the Bitcoin url you are using.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/MetacoSA/NBitcoin/issues/263#issuecomment-318675413, or mute the thread https://github.com/notifications/unsubscribe-auth/AAmbROdg10QoNXwbtxaKLO0DoSeZ3HdRks5sSfdhgaJpZM4Ob7AC .

-- Olayinka Tanimomo (MCTS,CCNA) Software/Web/Mobile Developer

NicolasDorier commented 7 years ago

your error "Payment Request Url is invalid" appears nowhere in NBitcoin code, which software are you using ? I suspect they have a bug in parsing the bitcoin: url

NicolasDorier commented 7 years ago

any progress?

layinka commented 7 years ago

Not really, i had to compose and write my own protobuf to the wire directly.

NicolasDorier commented 7 years ago

which software gave the error? It should recognize my format, if it does not can you tell me the difference between the one you created and mine?

layinka commented 7 years ago

I really cant tell the difference in the final output, all i see is that this one works with all the wallets. I am using silentorbit.com/protobuf to generate the protobuf.

On Mon, Aug 7, 2017 at 3:52 PM, Nicolas Dorier notifications@github.com wrote:

which software are you using please ? It should recognize my format, if it does not can you tell me the difference between the one you created and mine?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/MetacoSA/NBitcoin/issues/263#issuecomment-320685896, or mute the thread https://github.com/notifications/unsubscribe-auth/AAmbRIK0NNHxwMGkmAJBI81wMY4kCGY2ks5sVySfgaJpZM4Ob7AC .

-- Olayinka Tanimomo (MCTS,CCNA) Software/Web/Mobile Developer

NicolasDorier commented 7 years ago

please can you give me a way to reproduce the error or something, I spend significant time trying to debug your issue, I would like at least to find out what was wrong

NicolasDorier commented 7 years ago

Something like the byte serialization of my code versus yours.

NicolasDorier commented 7 years ago

Or even posting your code which work would help me

layinka commented 7 years ago

I understand.

Maybe i was doing something wrong with your library,but i just couldnt get it to work.

This is a working sample from my working code

public void  GetBitcoinPaymentRequest(string payScheduleId){
    string baseUrl = "http://127.0.0.1:5000";

    var businessId = "treas";

    NewPayments.PaymentDetails pd = new NewPayments.PaymentDetails();
    //pd.Time = DateTime.UtcNow.;
    pd.Memo = "Payment fr TWT";
    pd.Network = "test";
    pd.MerchantData = ASCIIEncoding.ASCII.GetBytes(payScheduleId.ToString());
    pd.Time =(ulong) DateTimeOffset.Now.ToUnixTimeSeconds();
    DateTime foo = DateTime.Now.AddDays(1);
    long unixTime = ((DateTimeOffset)foo).ToUnixTimeSeconds();
    pd.Expires =(ulong) unixTime;

    NewPayments.Output out1 = new NewPayments.Output( (ulong)( 0.00001*1000000), "address...");
    NewPayments.Output transactionChargeOutput = new NewPayments.Output( (ulong)( 0.0000001m*1000000), "addresss.....");
    pd.Outputs = new List<NewPayments.Output> { out1, transactionChargeOutput };

    pd.PaymentUrl = baseUrl + "?id=" + payScheduleId;

    var payReq = new NewPayments.PaymentRequest();

    payReq.SerializedPaymentDetails = NewPayments.PaymentDetails.SerializeToBytes(pd);

    // payReq.PkiType = "none";//Unsigned
    payReq.PkiType = "x509+sha256";
    // payReq.PkiData
    payReq.Sign(System.IO.File.ReadAllBytes("certificates/mycert.pfx"), PKIType.X509SHA256);
    // request.Sign(System.IO.File.ReadAllBytes("data/NicolasDorierMerchant.pfx"), PKIType.X509SHA256);

    //payReq.Serialize();

    // PaymentRequest.SerializeToBytes(payReq);
    Response.ContentType="application/bitcoin-paymentrequest";

    Response.Headers.AddOrReplace("Content-Transfer-Encoding","binary");
    NewPayments.PaymentRequest.Serialize(Response.Body,payReq);

}

I am using this proto file

package newPayments;
option java_package = "org.bitcoin.protocols.payments";
option java_outer_classname = "Protos";

// Generalized form of "send payment to this/these bitcoin addresses"
message Output {
        optional uint64 amount = 1 [default = 0]; // amount is integer-number-of-satoshis
        required bytes script = 2; // usually one of the standard Script forms
}
message PaymentDetails {
        optional string network = 1 [default = "main"]; // "main" or "test"
        repeated Output outputs = 2;        // Where payment should be sent
        required uint64 time = 3;           // Timestamp; when payment request created
        optional uint64 expires = 4;        // Timestamp; when this request should be considered invalid
        optional string memo = 5;           // Human-readable description of request for the customer
        optional string payment_url = 6;    // URL to send Payment and get PaymentACK
        optional bytes merchant_data = 7;   // Arbitrary data to include in the Payment message
}
message PaymentRequest {
        optional uint32 payment_details_version = 1 [default = 1];
        optional string pki_type = 2 [default = "none"];  // none / x509+sha256 / x509+sha1
        optional bytes pki_data = 3;                      // depends on pki_type
        required bytes serialized_payment_details = 4;    // PaymentDetails
        optional bytes signature = 5;                     // pki-dependent signature
}
message X509Certificates {
        repeated bytes certificate = 1;    // DER-encoded X.509 certificate chain
}
message Payment {
        optional bytes merchant_data = 1;  // From PaymentDetails.merchant_data
        repeated bytes transactions = 2;   // Signed transactions that satisfy PaymentDetails.outputs
        repeated Output refund_to = 3;     // Where to send refunds, if a refund is necessary
        optional string memo = 4;          // Human-readable message for the merchant
}
message PaymentACK {
        required Payment payment = 1;      // Payment message that triggered this ACK
        optional string memo = 2;          // human-readable message for customer
}
NicolasDorier commented 7 years ago

thanks a lot, will take a look today

NicolasDorier commented 7 years ago

what is your Sign Method ?

NicolasDorier commented 7 years ago

mmh your way of doing might not work on other software.... BIP37 is really a shitty BIP. As there is so many way to serialize the same data, the gap are left to interpretation.

I advise you to test your implementation with another client (like copay or bitcoin core) to be sure that it works.

NicolasDorier commented 7 years ago

Can you also send me the hex of a signed request from you ? Just to check if I can verify correctly. Also can you send me your Sign method ?

layinka commented 7 years ago

I have tested with bitcoin core and it works.

Sent from my Windows Phone

-----Original Message----- From: "Nicolas Dorier" notifications@github.com Sent: ‎8/‎8/‎2017 3:33 AM To: "MetacoSA/NBitcoin" NBitcoin@noreply.github.com Cc: "layinka" xcelsis02@gmail.com; "Mention" mention@noreply.github.com Subject: Re: [MetacoSA/NBitcoin] Question BIP70, 71 ,72 and 73 (#263)

mmh your way of doing might not work on other software.... BIP37 is really a shitty BIP. As there is so many way to serialize the same data, the gap are left to interpretation. I advise you to test your stuff with another client (like copay or bitcoin core) to be sure that it works. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

NicolasDorier commented 7 years ago

Can you also send me the hex of a signed request from you ? Just to check if I can verify correctly. Also can you send me your Sign method ?

layinka commented 7 years ago

How do i get the hex?

I am using your sign method since you already implemented that.

On Tue, Aug 8, 2017 at 7:56 AM, Nicolas Dorier notifications@github.com wrote:

Can you also send me the hex of a signed request from you ? Just to check if I can verify correctly. Also can you send me your Sign method ?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/MetacoSA/NBitcoin/issues/263#issuecomment-320867575, or mute the thread https://github.com/notifications/unsubscribe-auth/AAmbRLllIPMLGb5oPgxsLcEjB89mpwvjks5sWAaJgaJpZM4Ob7AC .

-- Olayinka Tanimomo (MCTS,CCNA) Software/Web/Mobile Developer

NicolasDorier commented 7 years ago

Please can you tell me which software said to you Payment Request Url is invalid ? I tested my implementation against bitcoin core and it worked fine.

layinka commented 7 years ago

All of them.

Bitcoin core, copay, Electrum.

On Tue, Aug 8, 2017 at 10:26 AM, Nicolas Dorier notifications@github.com wrote:

Please can you tell me which software said to you Payment Request Url is invalid ?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/MetacoSA/NBitcoin/issues/263#issuecomment-320902263, or mute the thread https://github.com/notifications/unsubscribe-auth/AAmbRLHUhUjoKSBKBtpAY0ZU1Fatn1-6ks5sWCnhgaJpZM4Ob7AC .

-- Olayinka Tanimomo (MCTS,CCNA) Software/Web/Mobile Developer

NicolasDorier commented 7 years ago

very strange, look my test

                                var businessId = int.Parse(context.Request.QueryString.Get("id"));
                var now = DateTimeOffset.UtcNow;
                var expire = now + TimeSpan.FromDays(1);
                TxOut txOut = new TxOut(Money.Coins(1), new Key().ScriptPubKey);
                if(type == "Request")
                {
                    Assert.Equal(PaymentRequest.MediaType, context.Request.AcceptTypes[0]);
                    context.Response.ContentType = PaymentRequest.MediaType;
                    PaymentRequest request = new PaymentRequest();
                    request.Details.MerchantData = BitConverter.GetBytes(businessId);
                    request.Details.Network = Network.RegTest;
                    request.Details.Expires = expire;
                    request.Details.Time = now;
                    request.Details.PaymentUrl = new Uri(_Prefix + "?id=" + businessId + "&type=Payment");
                    request.Details.Outputs.Add(new PaymentOutput(txOut));
                    request.Sign(File.ReadAllBytes("data/NicolasDorierMerchant.pfx"), PKIType.X509SHA256);
                    request.WriteTo(context.Response.OutputStream);
                }

This request is accepted by Core on for me. I am thinking, you may have forgot to specify some mandatory property.

layinka commented 7 years ago

Maybe, thats possible. But then ,thats why i also copied my code here for u to see several times.

Its fine, i will test yours again later.

But it will be good if you have a form of documentaion on this apart from the test code, i will see if i can write something on it later.

Thanks a lot for your help

On Tue, Aug 8, 2017 at 11:58 AM, Nicolas Dorier notifications@github.com wrote:

very strange, look my test

                            var businessId = int.Parse(context.Request.QueryString.Get("id"));
          var now = DateTimeOffset.UtcNow;
          var expire = now + TimeSpan.FromDays(1);
          TxOut txOut = new TxOut(Money.Coins(1), new Key().ScriptPubKey);
          if(type == "Request")
          {
              Assert.Equal(PaymentRequest.MediaType, context.Request.AcceptTypes[0]);
              context.Response.ContentType = PaymentRequest.MediaType;
              PaymentRequest request = new PaymentRequest();
              request.Details.MerchantData = BitConverter.GetBytes(businessId);
              request.Details.Network = Network.RegTest;
              request.Details.Expires = expire;
              request.Details.Time = now;
              request.Details.PaymentUrl = new Uri(_Prefix + "?id=" + businessId + "&type=Payment");
              request.Details.Outputs.Add(new PaymentOutput(txOut));
              request.Sign(File.ReadAllBytes("data/NicolasDorierMerchant.pfx"), PKIType.X509SHA256);
              request.WriteTo(context.Response.OutputStream);
          }

This request is accepted by Core on for me. I am thinking, you may have forgot to specify some mandatory property.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/MetacoSA/NBitcoin/issues/263#issuecomment-320922340, or mute the thread https://github.com/notifications/unsubscribe-auth/AAmbRHNL5tn98SLYhFhywW-_u3rxTiVWks5sWD9IgaJpZM4Ob7AC .

-- Olayinka Tanimomo (MCTS,CCNA) Software/Web/Mobile Developer

NicolasDorier commented 7 years ago

Thanks, I updated my test code to include more specified field. Maybe it will do the trick, let me know when you have time to test.

layinka commented 7 years ago

Hi, Its working now. Though its still not working with copay, but it works with Bitcoin core.

Please how do i handle broadcasting the transaction on the network? Do you have any sample code?

Thanks

NicolasDorier commented 7 years ago

Sorry forget my previous deleted comment, I mixed up your issue with someone else.

I advise you to use your own Bitcoin Core node and use RPCClient.SendRawTransaction, if not possible, using QBitNinjaClient, you can see on https://programmingblockchain.gitbooks.io/

NicolasDorier commented 7 years ago

@layinka try to set VersionDetails field explicitely request.DetailsVersion = 1;, and tell me if better for copay.

layinka commented 7 years ago

Hi Nicolas, It still didnt work, Is it possible you try it on your end?

NicolasDorier commented 7 years ago

I do not plan to support Copay for now since they are forking to another crypto currency. I will fix it if either they come back to bitcoin, or if you can give me the bytes of a request generated by NBitcoin versus generated by yourself so I can compare.

NicolasDorier commented 6 years ago

Closing here, feel free to open again once you have an example of payment request not accepted by Copay but created by NBitcoin.

mrviit commented 6 years ago

Hello, I have installed your certifacete (NicolasDorierMerchant.pfx). I'm trying to implement a merchant server that supports Payment Protocol BIP70. This is my simple code for the server:

public class PaymentServer : IDisposable
    {
        HttpListener _Listener;
        Random rand = new Random();
        string _Prefix;
        public PaymentServer()
        {
            while (true)
            {
                try
                {
                    _Prefix = "http://localhost:37391" /*+ rand.Next(2000, 50000)*/ + "/";
                    _Listener = new HttpListener();
                    _Listener.Prefixes.Add(_Prefix);
                    _Listener.Start();
                    _Listener.BeginGetContext(ListenerCallback, null);
                    break;
                }
                catch (HttpListenerException)
                {
                }
            }
        }

        void ListenerCallback(IAsyncResult ar)
        {
            try
            {
                var context = _Listener.EndGetContext(ar);
                //var type = context.Request.QueryString.Get("type");
                var businessId = 0;// int.Parse(context.Request.QueryString.Get("id"));
                var now = DateTimeOffset.UtcNow;
                var expire = now + TimeSpan.FromDays(1);
                TxOut txOut = new TxOut(Money.Satoshis(1), new Key().ScriptPubKey);
                //TxOut txOut = new TxOut(Money.Satoshis(123), new BitcoinPubKeyAddress("mim5ukG6fbWn91dPiuyYPZLqB9pyQTbn61").ScriptPubKey);
                //if (type == "Request")
                //{
                //Assert.Equal(PaymentRequest.MediaType, context.Request.AcceptTypes[0]);
                context.Response.ContentType = PaymentRequest.MediaType;
                    PaymentRequest request = new PaymentRequest();
                    //request.Details.MerchantData = BitConverter.GetBytes(businessId);
                    request.Details.Network = Network.TestNet;
                    request.Details.Expires = expire;
                    request.Details.Time = now;
                request.Details.PaymentUrl = new Uri(_Prefix + "?id=" + businessId + "&type=Payment");
                request.Details.Outputs.Add(new PaymentOutput(txOut));

                //request.Details.Outputs.Add(new PaymentOutput(new Money(0.0000124m, MoneyUnit.BTC), new BitcoinPubKeyAddress("mim5ukG6fbWn91dPiuyYPZLqB9pyQTbn61").ScriptPubKey)); //Destination 
                //request.Details.Outputs.Add(new PaymentOutput(new Money(0.00001m, MoneyUnit.BTC), new BitcoinPubKeyAddress("addrs2").ScriptPubKey));// dest charge 2
                //request.Details.Memo = "Payment for ...";
                request.PKIType = PKIType.X509SHA256;

                //context.Response.ContentType = "application/bitcoin-paymentrequest";
                //context.Response.Headers.AddOrReplace("Content-Transfer-Encoding", "binary");

                var cert = File.ReadAllBytes("data/NicolasDorierMerchant.pfx"); 
                request.Sign(cert, PKIType.X509SHA256);

                Assert.True(request.VerifySignature());
                //Assert.True(request.VerifyChain());

                request.WriteTo(context.Response.OutputStream);
                //}
                //else if (type == "Payment")
                //{
                //    //Assert.Equal(PaymentMessage.MediaType, context.Request.ContentType);
                //    //Assert.Equal(PaymentACK.MediaType, context.Request.AcceptTypes[0]);

                //    var payment = PaymentMessage.Load(context.Request.InputStream);
                //    //Assert.Equal(businessId, BitConverter.ToInt32(payment.MerchantData, 0));

                //    context.Response.ContentType = PaymentACK.MediaType;
                //    var ack = payment.CreateACK();
                //    ack.Memo = "Thanks for your purchase";
                //    ack.WriteTo(context.Response.OutputStream);
                //}
                //else
                //    Assert.False(true, "Impossible");

                context.Response.Close();
                _Listener.BeginGetContext(ListenerCallback, null);
            }
            catch (Exception)
            {
                if (!_Stopped)
                    throw;
            }
        }
}

After click on the bitcoin: URI link, the wallet is launched then send a request to the server. The server then send back a PaymentRequest as above code. I'm testing with Copay and Electrum.

Copay said: "Payment Protocol Invalid". As I investigated, it seems because of src/js/services/payproService.js :

else if (!paypro.verified) {
   $log.warn('Failed to verify payment protocol signatures');
   return cb(gettextCatalog.getString('Payment Protocol Invalid'));
}

Electrum said: "ERROR: CA Certificate Chain Not Provided by Payment Processor". As I investigated, it seems because of electrum/blob/master/lib/paymentrequest.py:

def verify_cert_chain(chain):
    """ Verify a chain of certificates. The last certificate is the CA"""
    load_ca_list()
    # parse the chain
    cert_num = len(chain)
    x509_chain = []
    for i in range(cert_num):
        x = x509.X509(bytearray(chain[i]))
        x509_chain.append(x)
        if i == 0:
            x.check_date()
        else:
            if not x.check_ca():
                raise BaseException("ERROR: Supplied CA Certificate Error")
    if not cert_num > 1:
        raise BaseException("ERROR: CA Certificate Chain Not Provided by Payment Processor")

Please help me!

NicolasDorier commented 6 years ago

NicolasDorier.pfx is a test certificate, it is not trusted. You need your own certificate. I advise you against using certificates in your payment requests. Copay is not verifying right for some reason, and they do not add much security.

mrviit commented 6 years ago

Thanks Nocolas,

I have no experience in PKI. I'm just researching about payment system and trying to implement a demo. So I must buy a certificate, right? Is there a free certificate for testing? Now I can test with Electrum without signing.

NicolasDorier commented 6 years ago

@mrviit remove the certificate for tests. And really I advise to remove for prod as well, as this protocol does not really offer any security at all with certs.