Any plans to support coinbase advanced trading?
godkane1 opened this issue · comments
Looks like the advanced trading api is now up and running. Any plans to convert over or make a different api?
@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.
@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! :-)
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.
Supporting Coinbase's APIs has been a very painful process in the face of all their API version changes.
Indeed.
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.
@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.
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?
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
.
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?
@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 fill
s 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 fill
s 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?
@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...?
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.
@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.
@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
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).
Figure 1. Snapshot of the MS website discussing .NET Core EOL versions.
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.
Figure 1. Setting the Target Framework
of the Example
project to .NET 7.0
.
Figure 2. Targeting the Coinbase
project to net461;net7.0
.
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!
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...
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.
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.
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.
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>();
}
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.
Commit: 7df8d9a
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);
}
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();
}
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.
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;
}
));
}
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();
}
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.
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;
}
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.
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.
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?
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.