Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Any plans to support coinbase advanced trading? #84

Open
godkane1 opened this issue Dec 7, 2022 · 35 comments
Open

Any plans to support coinbase advanced trading? #84

godkane1 opened this issue Dec 7, 2022 · 35 comments

Comments

@godkane1
Copy link

godkane1 commented Dec 7, 2022

Looks like the advanced trading api is now up and running. Any plans to convert over or make a different api?

@astrohart
Copy link
Contributor

astrohart commented May 6, 2023

@bchavez > Looks like the advanced trading api is now up and running. Any plans to convert over or make a different api?

Would like to, e.g., list products and find out which are/aren't tradable as can be done w the v3 API that uses the same sign on process.

Thanks.

@astrohart
Copy link
Contributor

astrohart commented Dec 23, 2023

@bchavez Me too. As of 30 Nov 2023, Sign in With Coinbase (the API this library calls) took away the Buys and Sells endpoints. To buy and sell, now it is necessary to access the Advanced Trade API. Please support this. Thanks! :-)

@bchavez
Copy link
Owner

bchavez commented Dec 31, 2023

No plans to support Coinbase's Advanced Trading APIs at the moment unless we get a significant monetary support stipend from Coinbase.

Supporting Coinbase's APIs has been a very painful process in the face of all their API version changes.

@astrohart
Copy link
Contributor

Supporting Coinbase's APIs has been a very painful process in the face of all their API version changes.

Indeed.

@astrohart
Copy link
Contributor

image
Figure 1. Possible new location of Buy and Sell resources.

Hi @bchavez I am doing some further digging using a quick and dirty Coinbase API client I whipped up at home (that signs requests correctly and such) to get the raw JSON of a transaction with an expand=all query parameter passed. I am wondering if this might not yield more resources.

Am doing some unit tests now...will keep you posted. I am hoping that the Buy and Sell resources can still be gotten to, with just a slight modification of your code.... thanks.

@astrohart
Copy link
Contributor

@bchavez Good news! I did some more testing and I discovered, that yes, in fact, one can pass a URL such as

var requestPath =
    $"/v2/accounts/{accountId}/transactions/{transactionId}?expand=all";

Note the ?expand=all on the end. accountId is the value of Coinbase.Models.Account.Id property, and the transactionId is the value of the Coinbase.Models.Entity.Id value for a Coinbase.Models.Transaction object instance from the endpoint call /v2/accounts/:accountId/transactions.

When we pass expand=all for a Buy transaction of, a e.g., BTC account, I get:

{
    "data": {
        "id": "fc988949-d761-510b-a862-9bcb45ea85d9",
        "type": "buy",
        "status": "completed",
        "amount": {
            "amount": "0.00060521",
            "currency": "BTC"
        },
        "native_amount": {
            "amount": "20.00",
            "currency": "USD"
        },
        "description": null,
        "created_at": "2023-04-11T19:14:01Z",
        "updated_at": "2023-04-13T05:44:31Z",
        "resource": "transaction",
        "resource_path": "/v2/accounts/67eed12a-ddd2-569e-b262-54a8287fcfb1/transactions/fc988949-d761-510b-a862-9bcb45ea85d9",
        "instant_exchange": false,
        "buy": {
            "id": "b27a7cde-e784-5848-a9dc-a5ca55e94221",
            "status": "completed",
            "transaction": {
                "id": "fc988949-d761-510b-a862-9bcb45ea85d9",
                "resource": "transaction",
                "resource_path": "/v2/accounts/67eed12a-ddd2-569e-b262-54a8287fcfb1/transactions/fc988949-d761-510b-a862-9bcb45ea85d9"
            },
            "user_reference": "7MYRUATE",
            "created_at": "2023-04-11T19:13:49Z",
            "updated_at": "2023-04-11T19:53:58Z",
            "resource": "buy",
            "resource_path": "/v2/accounts/67eed12a-ddd2-569e-b262-54a8287fcfb1/buys/b27a7cde-e784-5848-a9dc-a5ca55e94221",
            "payment_method": {
                "id": "d9e7cdd2-eebc-51b3-9394-cecd88db1cc8",
                "resource": "payment_method",
                "resource_path": "/v2/payment-methods/d9e7cdd2-eebc-51b3-9394-cecd88db1cc8"
            },
            "committed": true,
            "payout_at": "2023-04-11T19:13:48Z",
            "instant": true,
            "fee": {
                "amount": "1.49",
                "currency": "USD"
            },
            "amount": {
                "amount": "0.00060521",
                "currency": "BTC"
            },
            "total": {
                "amount": "20.00",
                "currency": "USD"
            },
            "subtotal": {
                "amount": "18.51",
                "currency": "USD"
            },
            "unit_price": {
                "amount": "30584.43",
                "currency": "USD",
                "scale": 2
            },
            "hold_until": "2023-04-17T23:00:00Z",
            "hold_days": 7,
            "idem": "dfe26a85-92b0-42f6-adab-aeea9a8c413a",
            "next_step": null,
            "is_first_buy": false,
            "requires_completion_step": false
        },
        "details": {
            "title": "Bought Bitcoin",
            "subtitle": "Using NAVY FEDERAL CREDIT UNION ******8602",
            "header": "Bought 0.00060521 BTC ($20.00)",
            "health": "positive",
            "payment_method_name": "NAVY FEDERAL CREDIT UNION ******8602"
        },
        "hide_native_amount": false
    },
    "warnings": [
        {
            "id": "missing_version",
            "message": "Please supply API version (YYYY-MM-DD) as CB-VERSION header",
            "url": "https://developers.coinbase.com/api#versioning"
        }
    ]
}

Listing 1. JSON returned with an expanded Buy sub-resource.

image
Figure 1. Buy resource circled in the resultant JSON.

We can see that we now have the transaction but augmented with fees, unit_price (the price we actually bought the cryptocurrency at) and the subtotal and total.

I respectfully suggest that your code be modified to always expand the buy and sell resources for all transactions by always passing ?expand=all (BTW this can be done for List Transactions as well as Show Transaction) and update your entity model accordingly.

Or, could I do it myself and then submit a PR?

@astrohart
Copy link
Contributor

I ran the JSON through quicktype.io and it appears that the Transaction with a Buy can be modeled thus:

public partial class Transaction
    {
        [JsonProperty("data")]
        public Data Data { get; set; }

        [JsonProperty("errors")]
        public List<Error> Errors { get; set; }

        [JsonProperty("warnings")]
        public List<Warning> Warnings { get; set; }
    }

    public partial class Data
    {
        [JsonProperty("id")]
        public Guid Id { get; set; }

        [JsonProperty("type")]
        public string Type { get; set; }

        [JsonProperty("status")]
        public string Status { get; set; }

        [JsonProperty("amount")]
        public Amount Amount { get; set; }

        [JsonProperty("native_amount")]
        public Amount NativeAmount { get; set; }

        [JsonProperty("description")]
        public object Description { get; set; }

        [JsonProperty("created_at")]
        public DateTimeOffset CreatedAt { get; set; }

        [JsonProperty("updated_at")]
        public DateTimeOffset UpdatedAt { get; set; }

        [JsonProperty("resource")]
        public string Resource { get; set; }

        [JsonProperty("resource_path")]
        public string ResourcePath { get; set; }

        [JsonProperty("instant_exchange")]
        public bool InstantExchange { get; set; }

        [JsonProperty("buy")]
        public Buy Buy { get; set; }

        [JsonProperty("details")]
        public Details Details { get; set; }

        [JsonProperty("hide_native_amount")]
        public bool HideNativeAmount { get; set; }
    }

    public partial class Amount
    {
        [JsonProperty("amount")]
        public string AmountAmount { get; set; }

        [JsonProperty("currency")]
        public string Currency { get; set; }
    }

    public partial class Buy
    {
        [JsonProperty("id")]
        public Guid Id { get; set; }

        [JsonProperty("status")]
        public string Status { get; set; }

        [JsonProperty("transaction")]
        public PaymentMethod Transaction { get; set; }

        [JsonProperty("user_reference")]
        public string UserReference { get; set; }

        [JsonProperty("created_at")]
        public DateTimeOffset CreatedAt { get; set; }

        [JsonProperty("updated_at")]
        public DateTimeOffset UpdatedAt { get; set; }

        [JsonProperty("resource")]
        public string Resource { get; set; }

        [JsonProperty("resource_path")]
        public string ResourcePath { get; set; }

        [JsonProperty("payment_method")]
        public PaymentMethod PaymentMethod { get; set; }

        [JsonProperty("committed")]
        public bool Committed { get; set; }

        [JsonProperty("payout_at")]
        public DateTimeOffset PayoutAt { get; set; }

        [JsonProperty("instant")]
        public bool Instant { get; set; }

        [JsonProperty("fee")]
        public Amount Fee { get; set; }

        [JsonProperty("amount")]
        public Amount Amount { get; set; }

        [JsonProperty("total")]
        public Amount Total { get; set; }

        [JsonProperty("subtotal")]
        public Amount Subtotal { get; set; }

        [JsonProperty("unit_price")]
        public UnitPrice UnitPrice { get; set; }

        [JsonProperty("hold_until")]
        public DateTimeOffset HoldUntil { get; set; }

        [JsonProperty("hold_days")]
        public long HoldDays { get; set; }

        [JsonProperty("idem")]
        public Guid Idem { get; set; }

        [JsonProperty("next_step")]
        public object NextStep { get; set; }

        [JsonProperty("is_first_buy")]
        public bool IsFirstBuy { get; set; }

        [JsonProperty("requires_completion_step")]
        public bool RequiresCompletionStep { get; set; }
    }

    public partial class PaymentMethod
    {
        [JsonProperty("id")]
        public Guid Id { get; set; }

        [JsonProperty("resource")]
        public string Resource { get; set; }

        [JsonProperty("resource_path")]
        public string ResourcePath { get; set; }
    }

    public partial class UnitPrice
    {
        [JsonProperty("amount")]
        public string Amount { get; set; }

        [JsonProperty("currency")]
        public string Currency { get; set; }

        [JsonProperty("scale")]
        public long Scale { get; set; }
    }

    public partial class Details
    {
        [JsonProperty("title")]
        public string Title { get; set; }

        [JsonProperty("subtitle")]
        public string Subtitle { get; set; }

        [JsonProperty("header")]
        public string Header { get; set; }

        [JsonProperty("health")]
        public string Health { get; set; }

        [JsonProperty("payment_method_name")]
        public string PaymentMethodName { get; set; }
    }

    public partial class Error
    {
        [JsonProperty("id")]
        public string Id { get; set; }

        [JsonProperty("message")]
        public string Message { get; set; }

        [JsonProperty("url")]
        public string Url { get; set; }
    }

    public partial class Warning
    {
        [JsonProperty("id")]
        public string Id { get; set; }

        [JsonProperty("message")]
        public string Message { get; set; }

        [JsonProperty("url")]
        public string Url { get; set; }
    }

The one snag is to deserialize to the proper model when processing a Buy transaction and not a Sell. Actually, that is easy. In Newtonsoft.Json, you can add a method to any of your JSON model classes to conditionally deserialize a property. Let's say the class is MyClass and the property is Foo. Then you can add a ShouldSerializeFoo public method that returns bool. This way the Buy and the Sell can both be modeled and then I can check the value of the Type property. If it's a buy type, then deserialize the Buy property; if the transaction is a sell Type, then deserialize the Sell.

@bchavez
Copy link
Owner

bchavez commented Jan 4, 2024

Really great find. Thanks for the investigation.

I think at a high level; some core questions I have:

  • Are the buy/sell API calls the only calls that are deprecated and problematic? What other API calls in this Coinbase library have problems?

My original implementation for Coinbase Trading AKA. GDAX -> Coinbase Pro.... was this library:

But now they call it Coinbase Cloud? or Coinbase Advanced? It's like every year they have a new name for this stuff. So I've lost track of what it's officially called now and where the exchange "APIs were moved/migrated to".

All this to say:

  • Is Coinbase.Pro (originally written for the Coinbase Exchange API calls) a better place to make those changes/improvements or is salvaging this OG Coinbase library (meant for the consumer-side) the place for these changes/updates and improvements that you're looking to make?

@astrohart
Copy link
Contributor

@bchavez First off, thanks for getting back to me. I am like a kid in a candy store, because when I had initially heard about the deprecation I was pretty bummed.

Coinbase.Pro

I recall reading somewhere and also here that Coinbase Pro will be/has been sunsetted. I think really all these different "products" are just different routes on the API (i.e., /v1, /v2, /v3 etc.)

I think they are trying to steer everyone to Advanced Trade. This library will still work, it's just that the following endpoints (your code) no longer work:

  • IBuysEndpoint
  • ISellsEndpoint
  • IUsersEndpoint

Also, placing an order using the IBuysEndpoint.PlaceBuyOrderAsync and the IBuysEndpoint.CommitBuyAsync methods, along with their corresponding ISellsEndpoint methods, are no longer supported. What you have to do is "create an order" on Advanced Trade. But when you do, the same API Key and API Secret that was originally used with this library will work, and the orders will post to the same Coinbase.com Account that this library originally was designed to interface with. There will just be a new transaction type, advanced_trade_fill in the ListTransactionsAsync method's result. It will tell you automatically amalgamate the fills for that order and give the API caller a compressed view of what took place, so we do not have to iterate over and time-correlate the fills etc and then somehow guess the fee charged and the original purchase price.

For comparison, I am using my home-grown "Coinbase API client" to grab the raw JSON for the same transaction as above, but without the expand=all tacked on:

{
    "data": {
        "id": "fc988949-d761-510b-a862-9bcb45ea85d9",
        "type": "buy",
        "status": "completed",
        "amount": {
            "amount": "0.00060521",
            "currency": "BTC"
        },
        "native_amount": {
            "amount": "20.00",
            "currency": "USD"
        },
        "description": null,
        "created_at": "2023-04-11T19:14:01Z",
        "updated_at": "2023-04-13T05:44:31Z",
        "resource": "transaction",
        "resource_path": "/v2/accounts/67eed12a-ddd2-569e-b262-54a8287fcfb1/transactions/fc988949-d761-510b-a862-9bcb45ea85d9",
        "instant_exchange": false,
        "buy": {
            "id": "b27a7cde-e784-5848-a9dc-a5ca55e94221",
            "resource": "buy",
            "resource_path": "/v2/accounts/67eed12a-ddd2-569e-b262-54a8287fcfb1/buys/b27a7cde-e784-5848-a9dc-a5ca55e94221"
        },
        "details": {
            "title": "Bought Bitcoin",
            "subtitle": "Using NAVY FEDERAL CREDIT UNION ******8602",
            "header": "Bought 0.00060521 BTC ($20.00)",
            "health": "positive",
            "payment_method_name": "NAVY FEDERAL CREDIT UNION ******8602"
        },
        "hide_native_amount": false
    },
    "warnings": [
        {
            "id": "missing_version",
            "message": "Please supply API version (YYYY-MM-DD) as CB-VERSION header",
            "url": "https://developers.coinbase.com/api#versioning"
        }
    ]
}

Listing 1. JSON obtained without using expand=all on the Show Transaction endpoint.

Is Listing 1 what is already modeled in your library?

@astrohart
Copy link
Contributor

astrohart commented Jan 4, 2024

@bchavez I would find it highly useful if this library could be adapted to the expand=all only because i am putting the finishing touches on perhaps the Magnum Opus of my career, a AI that manages your cryptocurrency portfolio for you on Coinbase.com.

BTW - FYI - I found that Coinbase Advanced Trade allows you to use "Legacy API Keys" meaning those that work with coinbase.com -- some of the newer features won't work but my point is, the requests for Advanced Trade can still be signed the same way. Basically all Advanced Trade really is, is putting /api/v3 in request paths instead of /v2 with a more "Coinbase Pro Esque" sugar.

For institutions, and professional traders, Coinbase has rolled out Coinbase Prime.

Coinbase Pro is now called Coinbase Exchange and, oddly enough, my Coinbase Pro API keys still work on the new Coinbase Exchange API, but apparently you are limited to just querying the wallets and order book and such, you cannot place orders anymore on Pro/Exchange.

I highly recommend sunsetting your Coinbase.Pro library (do please keep it up on NuGet) but melding Sign In With Coinbase and Advanced Trade into this library. It all kind of blurs together. Maybe create a Coinbase.Prime API client for Prime...?

@astrohart
Copy link
Contributor

I fixed your Coinbase library! I made it pass the ?expand=all query param (using Flurl) for List Transactions and Show Transaction and it seems to correctly deserialize Buy and Sell natively right in the midst of the transactions returned.

I also removed your code (and corresponding unit tests) for the Buys, Sells, and Users endpoints, which are now deprecated.

I threw together a little dummy console app to test the List Transactions and it works:

using Coinbase;
using Coinbase.Models;

namespace DummyTransactionTester
{
   public static class Program
   {
      public static void Main()
      {
         var xactionsPage = MainImpl()
                            .GetAwaiter()
                            .GetResult();
         Console.ReadKey();
      }

      private static async Task<PagedResponse<Transaction>> MainImpl()
      {
         var config = new ApiKeyConfig { ApiKey = @"*****", ApiSecret = @"****" };
         var client = new CoinbaseClient(config);

         var xactionsPage = await client.Transactions.ListTransactionsAsync("**** (uuid of a wallet)");
         return xactionsPage;
      }
   }
}

Listing 1. Dummy console app.

I set a breakpoint at the Console.ReadKey() line from Listing 1, ran it in the debugger, and it worked! I hovered over the Data property of the xactionsPage variable on the line above, and the Buy property was filled in only for a buy and the Sell property was filled in only for a sell. All fee information and everything is now included natively in each Transaction record. All I had to do was change the Transaction class from this:

   public partial class Transaction
   {
      [JsonProperty("buy")]
      public Entity Buy { get; set; }
   }

Listing 2. Old Transaction class part.

to this:

   public partial class Transaction
   {
      [JsonProperty("buy")]
      public Buy Buy { get; set; }

      [JsonProperty("sell")]
      public Sell Sell { get; set; }

      public bool ShouldSerializeBuy()
      {
         return "buy".Equals(Type);
      }

      public bool ShouldSerializeSell()
      {
         return "sell".Equals(Type);
      }
   }

Listing 3. New Transaction class part with conditional serialization of Buy and Sell.

I just used your existing Buy and Sell classes. Next, in the Transactions endpoint source code, I had to do:

      /// <summary>
      /// Lists account’s transactions.
      /// </summary>
      Task<PagedResponse<Transaction>> ITransactionsEndpoint.ListTransactionsAsync(string accountId, PaginationOptions pagination, CancellationToken cancellationToken)
      {
         return this.AccountsEndpoint
            .AppendPathSegmentsRequire(accountId, "transactions")
            .SetQueryParam("expand", "all")
            .WithPagination(pagination)
            .WithClient(this)
            .GetJsonAsync<PagedResponse<Transaction>>(cancellationToken);
      }

      /// <summary>
      /// Show an individual transaction for an account. See transaction resource for more information.
      /// </summary>
      Task<Response<Transaction>> ITransactionsEndpoint.GetTransactionAsync(string accountId, string transactionId, CancellationToken cancellationToken)
      {
         return this.AccountsEndpoint
            .AppendPathSegmentsRequire(accountId, "transactions", transactionId)
            .SetQueryParam("expand", "all")
            .WithClient(this)
            .GetJsonAsync<Response<Transaction>>(cancellationToken);
      }

Listing 4. New source code for the ListTransactionsAsync and GetTransactionAsync methods.

Notice that, in the new code for the GetTransactionAsync and ListTransactionsAsync methods, all I did was add a call to SetQueryParam after the AppendPathSegmentsRequire call.

There is no support for placing orders with Advanced Trade in this PR that I will post, but at least now we can obtain the transaction info we need, including fees.

I will place a PR, and I ask that you consider merging it and then re-publishing to NuGet. Thank you.

@astrohart
Copy link
Contributor

@bchavez Please see PR #89

@astrohart
Copy link
Contributor

astrohart commented Jan 4, 2024

@bchavez I noticed that AppVeyor is having an error and I am trying to fix it.

BTW - I also noticed a warning:

C:\projects\coinbase\Source\Coinbase\Coinbase.csproj : warning NU1903: Package 'Newtonsoft.Json' 12.0.3 has a known high severity vulnerability, https://github.com/advisories/GHSA-5crp-9r3c-p9vr

I strongly recommend updating Newtonsoft.Json to the latest and greatest as a good cybersecurity practice. Furthrmore, I am hopeful that that will solve the AppVeyor error as well.

@astrohart
Copy link
Contributor

astrohart commented Jan 4, 2024

@bchavez PR #89 is pending an update. I figured I would go ahead and upgrade all the NuGet packages.

BTW I am in my Visual Studio and i am seeing the build error:

System.Runtime.CompilerServices.Unsafe doesn't support netcoreapp2.1. Consider updating your TargetFramework to netcoreapp3.1 or later.

It applies to the Examples project.

Just a FYI, M$ sunsetted/EOLed .NET Core 2. I propose updating the target to .NET Core 3.1. Coinbase.Tests already targets .NET Core 3 among other frameworks (I'll leave those alone) but I noticed that the project Coinbase targets net461;netstandard2.0. I propose to make the Coinbase targeting identically the same as Coinbase.Tests, which is:

net461;netcoreapp3.1;net6.0

Furthermore, I think it would be a good idea to make the Examples project also target the same framework(s) for consistency.

I am going to put the updates in place and then make sure it builds before pushing again.

Regards,

Brian Hart

@astrohart
Copy link
Contributor

At 10:42 hours MDT on 01/04/2024, I am getting a compiler warning:

The target framework 'netcoreapp3.1' is out of support and will not receive security updates in the future. 
Please refer to https://aka.ms/dotnet-core-support for more information about the support policy.

Listing 1. Example project compiler warnings

Let me consult the webpage given and figure out how to do this. .NET Core 2 and 3 have been EOL-ed...so we have to figure out what is the preferred .NET Core version to target (my guess is 7).

@astrohart
Copy link
Contributor

image
Figure 1. Snapshot of the MS website discussing .NET Core EOL versions.

image
Figure 2. NUnit does not want to play nice.

At 10:50 hours MDT on 01/04/2024, I have had a chance to look at https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core. It talks about how the various .NET Core versions have a product lifecycle.

I strongly advise moving to .NET core 7 (and keeping the other targets). This, I believe will resolve everything and also is in line with what the "cool kids" are programming to these days. I'd move it to .NET Core 6, but the EOL date is 12 November 2024, and I suspect it may be better to go to 7, which is long-term-support LTS (3 years till EOL).

I am hoping this will also help with the errors as shown in Figure 2.

@astrohart
Copy link
Contributor

image
Figure 1. Setting the Target Framework of the Example project to .NET 7.0.

image
Figure 2. Targeting the Coinbase project to net461;net7.0.

image
Figure 3. Targeting the Coinbase.Tests project to net461;net7.0.

As you can see, I've standardized the targeting on the Example, Coinbase.Tests, and Coinbase projects. I propose we go with this. It did make NUnit play nice all of a sudden, so that's positive!

@astrohart
Copy link
Contributor

At 11:02 hours MDT on 01/04/2024, so I have 6 compiler errors left, and I've run into a new snag. Apparently, the authors of Flurl, in their infinite wisdom, removed the ClientFlurlHttpSettings class. And the guy who filed the particular pull request that got merged to do this apparently did not bother to tell us how we're supposed to use Flurl in the new modality. :-)

Working on it...

@astrohart
Copy link
Contributor

image
Figure 1. Apparently, NUnit 4.0.1 supports .NET Framework 4.6.2+.

I need to re-target Coinbase.Tests (and why not standardize Coinbase to the same) to .NET Framework 4.6.2 to get NUnit to play nice. I propose making this change.

@astrohart
Copy link
Contributor

image
Figure 1. Angry red text from the new Flurl version.

OK, so now moving along...

The new version of Flurl is not making the compiler happy...as can be seen in Figure 1. They made HttpClientFactory go away. It's replaced with FlurlClient Factory. Figure 1 shows the red errors I'm getting in the CoinbaseClient.cs file.

My proposal is to modify them so, instead of client.Configure we say client.WithSettings (the Configure method got renamed to WithSettings), and to derive the DebugProxyFactory class not from DefaultHttpClientFactory -- which no longer exists -- to derive it from DefaultFlurlClientFactory which is documented as its replacement.

Now, the override of CreateMessageHandler is angry red, and searching the IntelliSense, I found out that we have to override the CreateInnerHandler() method which does the same thing.

private class DebugProxyFactory : DefaultHttpClientFactory
{
   private readonly WebProxy proxy;

   public DebugProxyFactory(WebProxy proxy)
   {
      this.proxy = proxy;
   }

   public override HttpMessageHandler CreateMessageHandler()
   {
      return new HttpClientHandler
         {
            Proxy = this.proxy,
            UseProxy = true
         };
   }
}

changes to:

private class DebugProxyFactory : DefaultFlurlClientFactory
{
   private readonly WebProxy proxy;

   public DebugProxyFactory(WebProxy proxy)
   {
      this.proxy = proxy;
   }

   public override HttpMessageHandler CreateInnerHandler()
   {
      return new HttpClientHandler
         {
            Proxy = this.proxy,
            UseProxy = true
         };
   }
}

Just semantics.

@astrohart
Copy link
Contributor

astrohart commented Jan 4, 2024

RE: Commit 74008b5

Now, to fix the function EnableFiddlerDebugProxy. This is where we wire up the DebugProxyFactory to the CoinbaseClient.

The current code is:

/// <summary>
/// Enable HTTP debugging via Fiddler. Ensure Tools > Fiddler Options... > Connections is enabled and has a port configured.
/// Then, call this method with the following URL format: http://localhost.:PORT where PORT is the port number Fiddler proxy
/// is listening on. (Be sure to include the period after the localhost).
/// </summary>
/// <param name="proxyUrl">The full proxy URL Fiddler proxy is listening on. IE: http://localhost.:8888 - The period after localhost is important to include.</param>
public void EnableFiddlerDebugProxy(string proxyUrl)
{
   var webProxy = new WebProxy(proxyUrl, BypassOnLocal: false);

   this.Configure(settings =>
      {
         settings.HttpClientFactory = new DebugProxyFactory(webProxy);
      });
}

However, the line this.Configure paints the word Configure red. So I tried switching it to this.WithSettings since that has worked before. No dice -- then the settings.HttpClientFactory property is painting red.

So I will head on over to the Flurl docs...

Message Handlers
Flurl.Http is built on top of HttpClient, which uses HttpClientHandler (by default) for most of its heavy lifting.IFlurlClientBuilder exposes methods for configuring both:

// clientless pattern:
FlurlHttp.Clients.WithDefaults(builder => builder
    .ConfigureHttpClient(hc => ...)
    .ConfigureInnerHandler(hch => {
        hch.Proxy = new WebProxy("https://my-proxy.com");
        hch.UseProxy = true;
    }));

So, after perusing the docs, this is how we should implement EnableFiddlerDebugProxy:

Message Handlers
Flurl.Http is built on top of HttpClient, which uses HttpClientHandler (by default) for most of its heavy lifting.IFlurlClientBuilder exposes methods for configuring both:

// clientless pattern:
FlurlHttp.Clients.WithDefaults(builder => builder
    .ConfigureHttpClient(hc => ...)
    .ConfigureInnerHandler(hch => {
        hch.Proxy = new WebProxy("https://my-proxy.com");
        hch.UseProxy = true;
    }));

Actually, EnableFiddlerProxy can now be made static since it does not refer to the current instance. However, so we do not break our users, I am going to leave it as an instance method.

One thing I think should be done is delete the DebugProxyFactory private class from the file since Visual Studio is now telling me no one refers to it. So, I will make that change.

@astrohart
Copy link
Contributor

RE: Commit 8a5e53c

OK, now I am getting a red error in the GetPageAsync method:

/// <summary>
/// Internally used for getting a next or previous page.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="pageUrl"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected internal Task<PagedResponse<T>> GetPageAsync<T>(string pageUrl, CancellationToken cancellationToken = default)
{
   pageUrl = pageUrl.Remove(0, 4);
   return (this.Config.ApiUrl + pageUrl)
      .WithClient(this)
      .GetJsonAsync<PagedResponse<T>>(cancellationToken);
}

The word WithClient is painted red. Upon checking this GitHub issue on Flurl the author of Flurl, in his infinite wisdom, broke your code by making the WithClient method disappear.

He says, instead of doing, e.g.,

"/".WithClient(client)

instead do

client.Request("/")

Aarrghh! Well, I brought this on myself, I suppose, by upgrading Flurl. I am only really proposing this change because I am annoyed with NuGet prompting me to upgrade my packages, to be honest. But I will go ahead and make the change to eliminate the usages of the WithClient extension method.

So now, e.g., your GetPageAsync method used to be:

/// <summary>
/// Internally used for getting a next or previous page.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="pageUrl"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected internal Task<PagedResponse<T>> GetPageAsync<T>(string pageUrl, CancellationToken cancellationToken = default)
{
   pageUrl = pageUrl.Remove(0, 4);
   return (this.Config.ApiUrl + pageUrl)
      .WithClient(this)
      .GetJsonAsync<PagedResponse<T>>(cancellationToken);
}

and it changes to:

/// <summary>
/// Internally used for getting a next or previous page.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="pageUrl"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected internal Task<PagedResponse<T>> GetPageAsync<T>(string pageUrl, CancellationToken cancellationToken = default)
{
   pageUrl = pageUrl.Remove(0, 4);
   return this.Request(Config.ApiUrl + pageUrl)
       .GetJsonAsync<PagedResponse<T>>(cancellationToken: cancellationToken);
}

As you can see, the code just executed a ballet dance, where the WithClient call turned into a Request call.

Personally, I really have a pet peeve with NuGet package authors changing the external interface upon which other software depends.

For this commit, I am proposing to alter all the calls to WithClient throughout the whole Coinbase library to now be this.Request (url...) calls.

@astrohart
Copy link
Contributor

Commit: b7f0c61

For this commit, there are still 70 compiler errors in 14 files. Upon going to the next error, it is complaining about a call

 a.Should().Throw<ArgumentException>();

in ApiParameterTests.cs. Apparently, we need to change such lines to

await a.Should().ThrowAsync<ArgumentException>();

After making the change, one of the tests used to be:

      [Test]
      public async Task withdraw
      (
         [Values(null, "", "  ")]string accountId)
      {
         Func<Task<PagedResponse<Withdrawal>>> a = async () => await client.Withdrawals.ListWithdrawalsAsync(accountId);
         Func<Task<Response<Withdrawal>>> b = async () => await client.Withdrawals.WithdrawalFundsAsync(accountId, null);

         a.Should().Throw<ArgumentException>();
         b.Should().Throw<ArgumentException>();
      }

and is now:

      [Test]
      public async Task withdraw
      (
         [Values(null, "", "  ")]string accountId)
      {
         Func<Task<PagedResponse<Withdrawal>>> a = async () => await client.Withdrawals.ListWithdrawalsAsync(accountId);
         Func<Task<Response<Withdrawal>>> b = async () => await client.Withdrawals.WithdrawalFundsAsync(accountId, null);

         await a.Should().ThrowAsync<ArgumentException>();
         await b.Should().ThrowAsync<ArgumentException>();
      }

@astrohart
Copy link
Contributor

Commit: 6e0e443

I am getting a new compiler error that says 'Argument 1: cannot convert from 'System.Collections.Generic.IReadOnlyList<Flurl.Http.FlurlCall>' to 'Flurl.Http.Testing.HttpTest' on the following method in ExtensionsForTesting:

// https://github.com/tmenier/Flurl/issues/323
public static HttpCallAssertion ShouldHaveExactCall(this HttpTest test, string exactUrl)
{
   test.CallLog[0].Request.Url.ToString().Should().Be(exactUrl);
   return new HttpCallAssertion(test.CallLog);
}

I propose that this method be deleted, which I've done in the Commit having the SHA referenced above. This is because I read the issue #323 of the Flurl repo and the package author modified the package to take wildcards in ShouldHaveCalled and therefore if you do not specify a wildcard, then the URL match will be exact; therefore, the method you wrote is redundant.

@astrohart
Copy link
Contributor

Commit: 7df8d9a

image
Figure 1. How to specify a request body the "new way".

      public static HttpCallAssertion ShouldHaveRequestBody(this HttpTest test, string json)
      {
         test.CallLog.First().RequestBody.Should().Be(json);
         return new HttpCallAssertion(test.CallLog);
      }

Listing 1. The body of the ExtensionsForTesting.ShouldHaveRequestBody method.

The compiler does not like the test.CallLog parameter being passed as an argument to the HttpCallAssertion constructor above. That's no longer the proper way to specify an assertion that the request body is such and such -- see Figure 1.

Accoridng to the Flurl docs, there is now a WithRequestBody chainable method to call for asserting in tests. I propose to, in this commit, remove the malfunctioning ExtensionsForTesting.ShouldHaveRequestBody method, and replace with the WithRequestBody chainable call in all the applicable unit tests.

For example, now the test

      [Test]
      public async Task can_depositfunds()
      {
         SetupServerSingleResponse(Deposit1);

         var create = new DepositFunds
            {
               Amount = 10.0m,
               Currency = "USD",
               PaymentMethod = "B28EB04F-BD70-4308-90A1-96065283A001"
         };
         var r = await client.Deposits.DepositFundsAsync("fff", create );

         var truth = new Response<Deposit>
         {
            Data = Deposit1Model
         };

         truth.Should().BeEquivalentTo(r);

         server.ShouldHaveRequestBody(
            @"{""amount"":10.0,""currency"":""USD"",""payment_method"":""B28EB04F-BD70-4308-90A1-96065283A001"",""commit"":false}");

         server.ShouldHaveExactCall($"https://api.coinbase.com/v2/accounts/fff/deposits")
            .WithVerb(HttpMethod.Post);
      }

should now look like:

      [Test]
      public async Task can_depositfunds()
      {
         SetupServerSingleResponse(Deposit1);

         var create = new DepositFunds { Amount = 10.0m, Currency = "USD", PaymentMethod = "B28EB04F-BD70-4308-90A1-96065283A001" };
         var r = await client.Deposits.DepositFundsAsync("fff", create);

         var truth = new Response<Deposit> { Data = Deposit1Model };

         truth.Should()
              .BeEquivalentTo(r);

         server.ShouldHaveCalled("https://api.coinbase.com/v2/accounts/fff/deposits")
               .WithRequestBody(@"{""amount"":10.0,""currency"":""USD"",""payment_method"":""B28EB04F-BD70-4308-90A1-96065283A001"",""commit"":false}")
               .WithVerb(HttpMethod.Post);
      }

@astrohart
Copy link
Contributor

Commit: b8d281d

A new compiler error is being thrown on the NotThrow() call in the bottom line of Issue51.cs:

[Test]
public async Task can_list_payment_methods()
{
   SetupServerPagedResponse(PaginationJson, PaymentMethods);
   
   Func<Task<PagedResponse<PaymentMethod>>> action = async () =>
      await client.PaymentMethods.ListPaymentMethodsAsync();

   action.Should().NotThrow();
}

The line action.Should().NotThrow() should actually be await action.Should().NotThrowAsync();:

[Test]
public async Task can_list_payment_methods()
{
   SetupServerPagedResponse(PaginationJson, PaymentMethods);
   
   Func<Task<PagedResponse<PaymentMethod>>> action = async () =>
      await client.PaymentMethods.ListPaymentMethodsAsync();

   await action.Should().NotThrowAsync();
}

@astrohart
Copy link
Contributor

Commit: 5972e92

In DataTests.cs, compiler errors were being generated by the Fiddler proxy testing code needing to be re-written due to the author of Flurl refactoring his code.

I removed the ProxyFactory class and changed the implementation of the DataTests.BeforeAllTests method to read:

[OneTimeSetUp]
public void BeforeAllTests()
{
   if( !Environment.OSVersion.IsAppVeyor() && Process.GetProcessesByName("Fiddler").Any() )
   {
      FlurlHttp.Clients.WithDefaults(builder => builder.ConfigureInnerHandler(
         hch =>
            {
               hch.Proxy = new WebProxy("http://localhost.:8888", BypassOnLocal: false);
               hch.UseProxy = true;
            }
      ));

   }

#if NETFRAMEWORK
   ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
#endif
}

This was needed because the compiler was erroring at the old way.

@astrohart
Copy link
Contributor

astrohart commented Jan 4, 2024

Commit: 2187711

Similarly to the previous issue, the compiler was erroring at IntegrationTests.cs when the constructor tries to configure a proxy:

public IntegrationTests()
{
   Directory.SetCurrentDirectory(Path.GetDirectoryName(typeof(IntegrationTests).Assembly.Location));

   ReadSecrets();

   var webProxy = new WebProxy("http://localhost.:8888", BypassOnLocal: false);

   FlurlHttp.Configure(settings =>
      {
         settings.HttpClientFactory = new ProxyFactory(webProxy);
      });
}

This is no longer the way it should be carried out; rather, the constructor should look like this:

public IntegrationTests()
{
   Directory.SetCurrentDirectory(Path.GetDirectoryName(typeof(IntegrationTests).Assembly.Location));

   ReadSecrets();

   FlurlHttp.Clients.WithDefaults(builder => builder.ConfigureInnerHandler(
      hch =>
         {
            hch.Proxy = new WebProxy("http://localhost.:8888", BypassOnLocal: false);
            hch.UseProxy = true;
         }
   ));
}

@astrohart
Copy link
Contributor

Commit: 64df276

Was getting a compiler error in the body of the UserTests.can_hoist_response method, because the client.Configure line is no longer any good with the way Flurl has been refactored.

Originally, the method looks like this:

[Test]
public async Task can_hoist_response()
{
   bool myCustomActionWasCalled = false;
   client.Configure(cf =>
      {
         cf.AfterCall = http =>
            {
               myCustomActionWasCalled = true;
               "AfterCall action set by user.".Dump();
            };
      });

   var list = await client
      .AllowAnyHttpStatus()
      .HoistResponse(out var responseGetter)
      .Accounts
      .ListAccountsAsync();

   var response = responseGetter();

   response.Should().NotBeNull();
   response.StatusCode.Should().NotBe(0);
   myCustomActionWasCalled.Should().BeTrue();
}

The SettingsExtensions.Configure method has gone away. Now, it has to be modified to read:

[Test]
public async Task can_hoist_response()
{
   bool myCustomActionWasCalled = false;
   client.WithSettings(_ =>
      {
         client.AfterCall(
            call =>
               {
                  myCustomActionWasCalled = true;
                  "AfterCall action set by user.".Dump();
               }
         );
      });

   var list = await client
      .AllowAnyHttpStatus()
      .HoistResponse(out var responseGetter)
      .Accounts
      .ListAccountsAsync();

   var response = responseGetter();

   response.Should().NotBeNull();
   response.StatusCode.Should().NotBe(0);
   myCustomActionWasCalled.Should().BeTrue();
}

@astrohart
Copy link
Contributor

Commit: 641d9ed

Getting compiler errors in OAuthTests.cs from calling the (now nonexistent) method, ShouldHaveExactCall. Replaced all those calls with calls to the ShouldHaveCalled method.

@astrohart
Copy link
Contributor

Commit: c7e278d

Next, I had to fix some compiler errors in the CoinbaseClient.HoistResponse method. This method was erroring on the cf.Configure calls. I simplified the code.

Before my change, it was:

public class CoinbaseClient
{
    /*...*/

          /// <summary>
      /// Captures the low-level <seealso cref="HttpResponseMessage" /> from a
      /// underlying request. Useful in advanced scenarios where you
      /// want to check HTTP headers, HTTP status code or
      /// inspect the response body manually.
      /// </summary>
      /// <param name="responseGetter">A function that must be called to
      /// retrieve the <seealso cref="HttpResponseMessage"/>
      /// </param>
      /// <returns>Returns the <seealso cref="HttpResponseMessage"/> of the
      /// underlying HTTP request.</returns>
      public CoinbaseClient HoistResponse(out Func<IFlurlResponse> responseGetter)
      {
         IFlurlResponse msg = null;

         void CaptureResponse(FlurlCall http)
         {
            msg = http.Response;

            this.Configure(cf =>
               {
                  // Remove Action<HttpCall> from Invocation list
                  // to avoid memory leak from further calls to the same
                  // client object.
                  cf.AfterCall -= CaptureResponse;
               });
         }

         this.Configure(cf =>
            {
               cf.AfterCall += CaptureResponse;
            });   

         responseGetter = () => msg;
         return this;
      }

    /*...*/
}

Now, the method is a lot simpler, i.e.:

/// <summary>
/// Captures the low-level <seealso cref="HttpResponseMessage" /> from a
/// underlying request. Useful in advanced scenarios where you
/// want to check HTTP headers, HTTP status code or
/// inspect the response body manually.
/// </summary>
/// <param name="responseGetter">A function that must be called to
/// retrieve the <seealso cref="HttpResponseMessage"/>
/// </param>
/// <returns>Returns the <seealso cref="HttpResponseMessage"/> of the
/// underlying HTTP request.</returns>
public CoinbaseClient HoistResponse(out Func<IFlurlResponse> responseGetter)
{
   IFlurlResponse msg = null;

   this.WithSettings(_ => this.AfterCall(call => msg = call.Response));

   responseGetter = () => msg;
   return this;
}

@astrohart
Copy link
Contributor

Commit: d09fca3

Next, there was a compiler error in the method AutoRefreshTokenHelper.WithAutomaticOAuthTokenRefresh. The very last line is: client.Configure(s => s.OnErrorAsync = TokenExpiredErrorHandler);. This is now incorrect due to the way in which Flurl has been refactored. It now should be: client.WithSettings(_ => client.OnError(TokenExpiredErrorHandler));. I changed it.

@astrohart
Copy link
Contributor

OK! I am done. The dependencies are updated and your source code is fixed. See PR #89. In most of my notes above, I've labeled the note with the specific Commit's SHA so you can trace my changes.

@c0debase
Copy link

c0debase commented Jan 5, 2024

OK! I am done. The dependencies are updated and your source code is fixed. See PR #89. In most of my notes above, I've labeled the note with the specific Commit's SHA so you can trace my changes.

I'm sorry for posting this here as it doesn't belong however I'm unable to find the answer to my question elsewhere.

var accounts = await client.Accounts.ListAccountsAsync();
Console.WriteLine(accounts);

How do I access the data inside of accounts?

@astrohart
Copy link
Contributor

astrohart commented Jan 5, 2024

Hi, @c0debase, this is Brian Hart, a contributor. First off, thanks for your question. To use the ListAccountsAsync() method, first, in your call, you must create and initialize a PaginationOptions and pass it as the first argument to the method, specifying at least the value Limit property. 100 is a good default.

Any websites linked in this response are purely for educational purposes and are not necessarily endorsed by, nor affiliated with, Brian Chavez or me. I'm simply supplying links that might help you.

Here's a section of this repo's README.md that explains pagination.

Some code that I might utilize is the following -- and be careful when using the code below in your application, as the code below is listed purely for example purposes:

// MyClass.cs
//

using Coinbase;
using Coinbase.Models;

namespace MyApp
{
    public class MyClass
    {
        /* ... */
        
        public Task<List<Account>> GetMyAccountsAsync()
        {
            PaginatedResponse<Account> accounts;

            var myAccounts = new List<Account>();

            var currentPage = 1;

            do
            {
                accounts = currentPage == 1
                    ? await client.Accounts.ListAccountsAsync(new PaginationOptions { Limit = 100 })
                    : await client.GetNextPageAsync(accounts);

                currentPage++;

                if (accounts == null) break;    /* something went wrong, stop loop */
                if (accounts.Data.Length == 0) break;
                
                myAccounts.AddRange(accounts.Data);
            }
            while(accounts.HasNextPage());

            return myAccounts;
        }
        
        /* ... */
    }
}

Listing 1. Reading the entire list of accounts into a List<Account>.

I don't make any express guarantees that the code above works or is fit for a particular purpose; it's purely for example and educational purposes. But it should give you a rough idea of what to do. I just came up with this code on the fly to help you -- it has yet to be tested.

The code above is designed to page through all the Account results and then collect them all into a List<Account>. It keeps going until accounts.HasNextPage() returns false. You must put a ; after the while part of the loop.

To talk through the code line by line:

The line PaginatedResponse<Account> accounts; creates a variable to read that holds the contents of each page. Next, we create a new List<Account> in the myAccounts variable. This will hold our results.

Next, we set an integer, currentPage, equal to 1. We're going to increment this with every loop.

We make use of a do/while loop to execute our data gathering. We will keep going as long as accounts.HasNextPage() is true. We do this because a do-while loop allows us to execute the body of the loop at least once, before checking the continuation expression. A normal while loop would check the continuation expression right away --- which is not valid, since before the loop begins, we won't have retrieved any data.

This keeps track of which page we're retrieving. If currentPage equals 1, we'll call ListAccountsAsync; otherwise, we'll call GetNextPageAsync, passing in the accounts variable. This is how pagination works; think of the accounts variable as representing a page of data, so you pass in the current page to the GetNextPageAsync method, and it then goes from there to try to get the next page because all the information that GetNextPageAsync needs is in the accounts variable.

As stated previously, we use the ternary conditional operator to help keep our code clean, in order to retrieve the value of the accounts variable. The lines:

    accounts = currentPage == 1
        ? await client.Accounts.ListAccountsAsync(new PaginationOptions { Limit = 100 })
        : await client.GetNextPageAsync(accounts);

says: "check the value of the currentPage variable. If it equals 1, then we're on the first page, so call the ListAccountsAsync method; otherwise, call the GetNextPageAsync method. Whatever is the result, assign it to the accounts variable." (We're reading the code right-to-left because the = operator works that way.)

Next, we increment our currentPage variable here, because the statements after it might cause the loop to either stop or to continue to the next iteration.


NOTE: If, instead, you wish to keep track of the number of pages of data obtained, set currentPage to zero at first, and then do currentPage == 0 in my ternary operator line above, and then, when the loop is done, the currentPage variable will hold the number of pages of data obtained. Otherwise, to get the number of pages of data obtained using the existing code above, subtract one from the value of currentPage after the loop has exited.


Next, we check if the accounts variable is equal to null for some freak reason; if this is the case, say break; to stop the loop. Then, we check if accounts.Data.Length == 0 which means that no results were obtained. If this is the case, we also say break; to stop the loop, since zero items of data were returned for the current page, meaning we're done.

If neither of the if statements are triggered, then we can proceed to use AddRange on the accounts.Data property (which is just an array of elements, each of which is an account) to add our data to the List<Account>, myAccounts. In the while part of the loop, we call accounts.HasNextPage(). This returns true or false. It returns true if there is more pages of data, or false if we've reached the end of the data. Returning false will cause the loop to stop.

Keep in mind that the above is just one of many possible different implementations. For clarity, I did not include exception-handling or handling of HTTP errors, or input validation or thread safety/concurrency, which are all things you should think about when writing production code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants