├── .editorconfig ├── .gitignore ├── LICENSE ├── LocalBitcoinsApi.sln ├── NugetPackageLogo.png ├── README.md ├── azure-pipelines.yml └── src ├── LocalBitcoinsApi.UnitTests ├── ClientTests.cs ├── LocalBitcoinsApi.UnitTests.csproj ├── ManualTests.cs ├── MockLocalBitcoinsClient.cs ├── TestSettings.cs ├── TestSettings.json └── data │ └── MySelf.json └── LocalBitcoinsApi ├── GlobalSuppressions.cs ├── LocalBitcoinsAPI.cs ├── LocalBitcoinsApi.csproj ├── LocalBitcoinsApi.ruleset ├── LocalBitcoinsApi.snk ├── LocalBitcoinsClient.cs ├── LocalBitcoinsException.cs ├── LocalBitcoinsRestApi.cs ├── RequestType.cs ├── Utility.cs └── UtilityExtensions.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | # Based on https://github.com/dotnet/corefx/blob/master/.editorconfig 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Default settings: 8 | # A newline ending every file 9 | # Use 4 spaces as indentation 10 | [*] 11 | insert_final_newline = false 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [project.json] 16 | indent_size = 2 17 | 18 | # C# files 19 | [*.cs] 20 | # New line preferences 21 | csharp_new_line_before_open_brace = all 22 | csharp_new_line_before_else = true 23 | csharp_new_line_before_catch = true 24 | csharp_new_line_before_finally = true 25 | csharp_new_line_before_members_in_object_initializers = true 26 | csharp_new_line_before_members_in_anonymous_types = true 27 | csharp_new_line_between_query_expression_clauses = true 28 | 29 | # Indentation preferences 30 | csharp_indent_block_contents = true 31 | csharp_indent_braces = false 32 | csharp_indent_case_contents = true 33 | csharp_indent_switch_labels = true 34 | csharp_indent_labels = one_less_than_current 35 | 36 | # use language keywords instead of BCL types 37 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 38 | dotnet_style_predefined_type_for_member_access = true:suggestion 39 | 40 | # name all constant fields using PascalCase 41 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 42 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 43 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 44 | 45 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 46 | dotnet_naming_symbols.constant_fields.required_modifiers = const 47 | 48 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 49 | 50 | # internal and private fields should be _camelCase 51 | dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion 52 | dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields 53 | dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style 54 | 55 | dotnet_naming_symbols.private_internal_fields.applicable_kinds = field 56 | dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal 57 | 58 | dotnet_naming_style.camel_case_underscore_style.required_prefix = _ 59 | dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case 60 | 61 | # Code style defaults 62 | dotnet_sort_system_directives_first = true 63 | csharp_preserve_single_line_blocks = true 64 | csharp_preserve_single_line_statements = false 65 | 66 | # Pattern matching 67 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 68 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 69 | csharp_style_inlined_variable_declaration = true:suggestion 70 | 71 | # Null checking preferences 72 | csharp_style_throw_expression = true:suggestion 73 | csharp_style_conditional_delegate_call = true:suggestion 74 | 75 | # Space preferences 76 | csharp_space_after_cast = false 77 | csharp_space_after_colon_in_inheritance_clause = true 78 | csharp_space_after_comma = true 79 | csharp_space_after_dot = false 80 | csharp_space_after_keywords_in_control_flow_statements = true 81 | csharp_space_after_semicolon_in_for_statement = true 82 | csharp_space_around_binary_operators = before_and_after 83 | csharp_space_around_declaration_statements = do_not_ignore 84 | csharp_space_before_colon_in_inheritance_clause = true 85 | csharp_space_before_comma = false 86 | csharp_space_before_dot = false 87 | csharp_space_before_open_square_brackets = false 88 | csharp_space_before_semicolon_in_for_statement = false 89 | csharp_space_between_empty_square_brackets = false 90 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 91 | csharp_space_between_method_call_name_and_opening_parenthesis = false 92 | csharp_space_between_method_call_parameter_list_parentheses = false 93 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 94 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 95 | csharp_space_between_method_declaration_parameter_list_parentheses = false 96 | csharp_space_between_parentheses = false 97 | csharp_space_between_square_brackets = false 98 | 99 | # Xml project files 100 | [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] 101 | indent_size = 2 102 | 103 | # Xml build files 104 | [*.builds] 105 | indent_size = 2 106 | 107 | # Xml files 108 | [*.{xml,stylecop,resx,ruleset}] 109 | indent_size = 2 110 | 111 | # Xml config files 112 | [*.{props,targets,config,nuspec}] 113 | indent_size = 2 114 | 115 | # Shell scripts 116 | [*.sh] 117 | end_of_line = lf 118 | [*.{cmd, bat}] 119 | end_of_line = crlf 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | *.bak 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /LocalBitcoinsApi.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalBitcoinsApi", "src\LocalBitcoinsApi\LocalBitcoinsApi.csproj", "{1E2333E7-0C1B-4379-BA2B-398D423E4651}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalBitcoinsApi.UnitTests", "src\LocalBitcoinsApi.UnitTests\LocalBitcoinsApi.UnitTests.csproj", "{8A97E1A3-26EB-41A4-85E6-15C284C5AAB6}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {1E2333E7-0C1B-4379-BA2B-398D423E4651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {1E2333E7-0C1B-4379-BA2B-398D423E4651}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {1E2333E7-0C1B-4379-BA2B-398D423E4651}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {1E2333E7-0C1B-4379-BA2B-398D423E4651}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {8A97E1A3-26EB-41A4-85E6-15C284C5AAB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {8A97E1A3-26EB-41A4-85E6-15C284C5AAB6}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {8A97E1A3-26EB-41A4-85E6-15C284C5AAB6}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {8A97E1A3-26EB-41A4-85E6-15C284C5AAB6}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {3E2704AA-B760-4598-A78A-2BAA9AAC2FC7} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /NugetPackageLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aabiryukov/LocalBitcoinsApi/73e6fea983f974e068a90ed7e82342026c6443e2/NugetPackageLogo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://sitronics.visualstudio.com/GithubPipelineTest/_apis/build/status/aabiryukov.LocalBitcoinsApi?branchName=master)](https://sitronics.visualstudio.com/GithubPipelineTest/_build/latest?definitionId=25?branchName=master) 2 | [![Nuget.org](https://img.shields.io/nuget/v/LocalBitcoinsApi.svg?style=flat)](https://www.nuget.org/packages/LocalBitcoinsApi) 3 | 4 | # LocalBitcoinsApi 5 | It is LocalBitcoins.com API .NET Library 6 | 7 | Package in Nuget: https://www.nuget.org/packages/LocalBitcoinsApi/ 8 | 9 | This LocalBitcoins.com API wrapper written in C# provides a quick access to most available LocalBitcoins features. 10 | Original API documentation available on the official web site: https://localbitcoins.com/api-docs/ 11 | 12 | You can look for code examples in LocalBitcoins.Test project which is a part of the solution. 13 | # Installation 14 | To install LocalBitcoinsApi with Nuget, run the following command in the Package Manager Console 15 | ``` 16 | PM> Install-Package LocalBitcoinsApi 17 | ``` 18 | # Code examples 19 | To create an instance of LocalBitcoinsClient: 20 | ``` 21 | var apiKey = "Your_Key"; 22 | var apiSecret = "Your_Secret"; 23 | var lbClient = new LocalBitcoinsClient(apiKey, apiSecret); 24 | ``` 25 | After creating an instance next and more API methods are available: 26 | ``` 27 | var mySelf = await lbClient.GetMyself(); 28 | Console.WriteLine("My name is: " + mySelf.data.username); 29 | 30 | var accountInfo = await lbClient.GetAccountInfo("SomeAccountName"); 31 | var dashboard = await lbClient.GetDashboard(); 32 | var ownAds = await lbClient.GetOwnAds(); 33 | var walletBalance = await lbClient.GetWalletBalance(); 34 | var contactInfo = await lbClient.GetContactInfo("7652822"); 35 | var contactUrl = await lbClient.CreateContact("11534457", 0.1M, "My message"); 36 | 37 | // Full list of methods you can find in the project sources 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # .NET Core 2 | # Add steps that publish symbols, save build artifacts, and more: 3 | # https://docs.microsoft.com/ru-ru/azure/devops/pipelines/languages/dotnet-core 4 | 5 | pool: 6 | vmImage: 'Ubuntu 16.04' 7 | 8 | variables: 9 | buildConfiguration: 'Release' 10 | 11 | steps: 12 | 13 | - script: dotnet build --configuration $(buildConfiguration) 14 | displayName: 'dotnet build $(buildConfiguration)' 15 | 16 | - task: DotNetCoreCLI@2 17 | displayName: 'Run tests' 18 | inputs: 19 | command: test 20 | projects: '**/*Tests/*.csproj' 21 | arguments: '--configuration $(buildConfiguration)' 22 | 23 | - task: CopyFiles@2 24 | displayName: 'Copy files' 25 | inputs: 26 | #sourceFolder: # Optional 27 | contents: src/LocalBitcoinsApi/bin/** 28 | targetFolder: $(Build.ArtifactStagingDirectory) 29 | #cleanTargetFolder: false # Optional 30 | #overWrite: false # Optional 31 | #flattenFolders: false # Optional 32 | 33 | - task: PublishBuildArtifacts@1 34 | displayName: 'Publish Build Artifacts' 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi.UnitTests/ClientTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using LocalBitcoins; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using Newtonsoft.Json.Linq; 9 | 10 | namespace LocalBitcoinsApi.UnitTests 11 | { 12 | [TestClass] 13 | public class ClientTests 14 | { 15 | private static LocalBitcoinsClient CreateClient() 16 | { 17 | return new MockLocalBitcoinsClient(); 18 | } 19 | 20 | [TestMethod] 21 | public async Task GetMyselfTest() 22 | { 23 | var client = CreateClient(); 24 | 25 | var result = await client.GetMyself(); 26 | Assert.IsTrue(!string.IsNullOrEmpty((string)result.data.username)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi.UnitTests/LocalBitcoinsApi.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | DEBUG;TRACE 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi.UnitTests/ManualTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using LocalBitcoins; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Newtonsoft.Json; 7 | 8 | namespace LocalBitcoinsApi.UnitTests 9 | { 10 | #if (!DEBUG) 11 | [Ignore] 12 | #endif 13 | [TestClass] 14 | public class ManualTests 15 | { 16 | [TestMethod] 17 | public async Task TestRealApiMethods() 18 | { 19 | // Read settings from file "TestSettings.json" 20 | var testSettings = GetSettings(); 21 | 22 | var client = new LocalBitcoinsClient(testSettings.ApiKey, testSettings.ApiSecret); 23 | 24 | dynamic info; 25 | 26 | // info = client.GetContactMessages("7652822"); 27 | // Console.WriteLine("Res: " + info.ToString()); 28 | 29 | 30 | info = await client.GetMyself(); 31 | Console.WriteLine("Myself: " + info.data.username); 32 | 33 | info = await client.GetWalletBalance(); 34 | Console.WriteLine("Wallet Balance: " + (decimal)(info.data.total.balance)); 35 | 36 | // info = client.CheckPinCode("8044011"); 37 | // Console.WriteLine("Pincode: " + info); 38 | 39 | // Update user online 40 | info = await client.GetDashboard(); 41 | Console.WriteLine("Dashboard: " + info.ToString()); 42 | 43 | info = await client.GetOwnAds(); 44 | Console.WriteLine("Ads: " + info.ToString()); 45 | 46 | info = await client.GetContactMessageAttachment("6652854", "38026599"); 47 | File.WriteAllBytes(@"c:\temp\LBImage.jpeg", info); // Requires System.IO 48 | Console.WriteLine("Res: " + info.Length); 49 | 50 | info = await client.GetRecentMessages(); 51 | Console.WriteLine("Res: " + info.ToString()); 52 | 53 | info = await client.GetDashboardReleased(); 54 | Console.WriteLine("Res: " + info.ToString()); 55 | 56 | info = await client.GetFees(); 57 | Console.WriteLine("Deposit Fee: " + info.data.deposit_fee); 58 | 59 | info = await client.GetAccountInfo("SomeAccountName"); 60 | Console.WriteLine("User: " + info.data.username); 61 | 62 | info = await client.Logout(); 63 | Console.WriteLine("Logout: " + info.ToString()); 64 | } 65 | 66 | private static TestSettings GetSettings() 67 | { 68 | const string defaultSettingsFile = "TestSettings.json"; 69 | const string overrideSettingsFile = "TestSettings.override.json"; 70 | 71 | var settingsFile = File.Exists(overrideSettingsFile) ? overrideSettingsFile : defaultSettingsFile; 72 | var settings = JsonConvert.DeserializeObject(File.ReadAllText(settingsFile)); 73 | 74 | if (string.IsNullOrEmpty(settings.ApiKey) || settings.ApiKey == "INSERT-KEY-HERE") 75 | throw new Exception($"Fill settings in file {settingsFile}"); 76 | 77 | return settings; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi.UnitTests/MockLocalBitcoinsClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using LocalBitcoins; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace LocalBitcoinsApi.UnitTests 10 | { 11 | internal class MockLocalBitcoinsClient: LocalBitcoinsClient 12 | { 13 | public MockLocalBitcoinsClient() 14 | : base("xxx", "xxx") 15 | { 16 | } 17 | protected override Task CallApiAsync(string apiCommand, RequestType requestType = RequestType.Get, Dictionary args = null) 18 | { 19 | string resourceName; 20 | 21 | switch (apiCommand.ToLowerInvariant()) 22 | { 23 | case "/api/myself/": 24 | resourceName = "MySelf"; 25 | break; 26 | 27 | default: 28 | throw new ArgumentException($"Unknown api command: {apiCommand}"); 29 | } 30 | 31 | var jobj = JsonConvert.DeserializeObject(GetTestJson(resourceName)); 32 | return Task.FromResult(jobj); 33 | } 34 | 35 | private static string GetTestJson(string resourceName) 36 | { 37 | return File.ReadAllText(Path.Combine("data", resourceName + ".json")); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi.UnitTests/TestSettings.cs: -------------------------------------------------------------------------------- 1 | namespace LocalBitcoinsApi.UnitTests 2 | { 3 | internal class TestSettings 4 | { 5 | public string ApiKey { get; set; } 6 | public string ApiSecret { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi.UnitTests/TestSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApiKey": "INSERT-KEY-HERE", 3 | "ApiSecret": "INSERT-SECRET-HERE" 4 | } 5 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi.UnitTests/data/MySelf.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "username": "bitcoinbaron", 4 | "created_at": "2013-06-25T14:11:30+00:00", 5 | "trading_partners_count": 0, 6 | "feedbacks_unconfirmed_count": 0, 7 | "trade_volume_text": "Less than 25 BTC", 8 | "has_common_trades": false, 9 | "confirmed_trade_count_text": "0", 10 | "blocked_count": 0, 11 | "feedback_score": 0, 12 | "feedback_count": 0, 13 | "url": "https://localbitcoins.com/p/bitcoinbaron/", 14 | "trusted_count": 0, 15 | "identity_verified_at": null, 16 | "real_name_verifications_trusted": 1, 17 | "real_name_verifications_untrusted": 4, 18 | "real_name_verifications_rejected": 0 19 | } 20 | } -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 |  2 | // This file is used by Code Analysis to maintain SuppressMessage 3 | // attributes that are applied to this project. 4 | // Project-level suppressions either have no target or are given 5 | // a specific target and scoped to a namespace, type, member, etc. 6 | 7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "", Scope = "type", Target = "~T:LocalBitcoins.LocalBitcoinsRestApi")] 8 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", Scope = "member", Target = "LocalBitcoins.LocalBitcoinsAPI.#PublicMarket_SellBitcoinsOnlineByCurrency(System.String,System.String,System.Int32)")] 9 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", Scope = "member", Target = "LocalBitcoins.LocalBitcoinsAPI.#PublicMarket_BuyBitcoinsOnlineByCurrency(System.String,System.String,System.Int32)")] 10 | 11 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "", Scope = "member", Target = "~M:LocalBitcoins.LocalBitcoinsClient.PublicMarket_BuyBitcoinsOnlineByCurrency(System.String,System.String,System.Int32)~System.Threading.Tasks.Task{}")] -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/LocalBitcoinsAPI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace LocalBitcoins 5 | { 6 | [Obsolete("This class is obsolete. Use instead LocalBitcoinsClient class with async/await.", false)] 7 | public class LocalBitcoinsAPI 8 | { 9 | private const int DefaultApiTimeoutSec = 10; 10 | 11 | private readonly LocalBitcoinsClient _client; 12 | 13 | /// 14 | /// Unique ctor sets access key and secret key, which cannot be changed later. 15 | /// 16 | /// Your Access Key 17 | /// Your Secret Key 18 | /// API request timeout in seconds 19 | /// Override API base address. Default is "https://localbitcoins.net/" 20 | public LocalBitcoinsAPI(string accessKey, string secretKey, int apiTimeoutSec = DefaultApiTimeoutSec, string overrideBaseAddress = null) 21 | { 22 | _client = new LocalBitcoinsClient(accessKey, secretKey, apiTimeoutSec, overrideBaseAddress); 23 | } 24 | 25 | // Returns public user profile information 26 | public dynamic GetAccountInfo(string userName) 27 | => _client.GetAccountInfo(userName).WaitAndUnwrapException(); 28 | 29 | // Return the information of the currently logged in user(the owner of authentication token). 30 | public dynamic GetMyself() 31 | => _client.GetMyself().WaitAndUnwrapException(); 32 | 33 | // Checks the given PIN code against the user"s currently active PIN code. 34 | // You can use this method to ensure the person using the session is the legitimate user. 35 | public dynamic CheckPinCode(string code) 36 | => _client.CheckPinCode(code).WaitAndUnwrapException(); 37 | 38 | // Return open and active contacts 39 | public dynamic GetDashboard() 40 | => _client.GetDashboard().WaitAndUnwrapException(); 41 | 42 | // Return released(successful) contacts 43 | public dynamic GetDashboardReleased() 44 | => _client.GetDashboardReleased().WaitAndUnwrapException(); 45 | 46 | // Return canceled contacts 47 | public dynamic GetDashboardCanceled() 48 | => _client.GetDashboardCanceled().WaitAndUnwrapException(); 49 | 50 | // Return closed contacts, both released and canceled 51 | public dynamic GetDashboardClosed() 52 | => _client.GetDashboardClosed().WaitAndUnwrapException(); 53 | 54 | // Releases the escrow of contact specified by ID { contact_id }. 55 | // On success there"s a complimentary message on the data key. 56 | public dynamic ContactRelease(string contactId) 57 | => _client.ContactRelease(contactId).WaitAndUnwrapException(); 58 | 59 | // Releases the escrow of contact specified by ID { contact_id }. 60 | // On success there"s a complimentary message on the data key. 61 | public dynamic ContactReleasePin(string contactId, string pincode) 62 | => _client.ContactReleasePin(contactId, pincode).WaitAndUnwrapException(); 63 | 64 | // Reads all messaging from the contact.Messages are on the message_list key. 65 | // On success there"s a complimentary message on the data key. 66 | // attachment_* fields exist only if there is an attachment. 67 | public dynamic GetContactMessages(string contactId) 68 | => _client.GetContactMessages(contactId).WaitAndUnwrapException(); 69 | 70 | public byte[] GetContactMessageAttachment(string contractId, string attachmentId) 71 | => _client.GetContactMessageAttachment(contractId, attachmentId).WaitAndUnwrapException(); 72 | 73 | // Marks a contact as paid. 74 | // It is recommended to access this API through /api/online_buy_contacts/ entries" action key. 75 | public dynamic MarkContactAsPaid(string contactId) 76 | => _client.MarkContactAsPaid(contactId).WaitAndUnwrapException(); 77 | 78 | // Post a message to contact 79 | public dynamic PostMessageToContact(string contactId, string message, string attachFileName = null) 80 | => _client.PostMessageToContact(contactId, message, attachFileName).WaitAndUnwrapException(); 81 | 82 | // Starts a dispute with the contact, if possible. 83 | // You can provide a short description using topic. This helps support to deal with the problem. 84 | public dynamic StartDispute(string contactId, string topic = null) 85 | => _client.StartDispute(contactId, topic).WaitAndUnwrapException(); 86 | 87 | // Cancels the contact, if possible 88 | public dynamic CancelContact(string contactId) 89 | => _client.CancelContact(contactId).WaitAndUnwrapException(); 90 | 91 | // Attempts to fund an unfunded local contact from the seller"s wallet. 92 | public dynamic FundContact(string contactId) 93 | => _client.FundContact(contactId).WaitAndUnwrapException(); 94 | 95 | // Attempts to create a contact to trade bitcoins. 96 | // Amount is a number in the advertisement"s fiat currency. 97 | // Returns the API URL to the newly created contact at actions.contact_url. 98 | // Whether the contact was able to be funded automatically is indicated at data.funded. 99 | // Only non-floating LOCAL_SELL may return unfunded, all other trade types either fund or fail. 100 | public dynamic CreateContact(string contactId, decimal amount, string message = null) 101 | => _client.CreateContact(contactId, amount, message).WaitAndUnwrapException(); 102 | 103 | 104 | // Gets information about a single contact you are involved in. Same fields as in /api/contacts/. 105 | public dynamic GetContactInfo(string contactId) 106 | => _client.GetContactInfo(contactId).WaitAndUnwrapException(); 107 | 108 | // contacts is a comma-separated list of contact IDs that you want to access in bulk. 109 | // The token owner needs to be either a buyer or seller in the contacts, contacts that do not pass this check are simply not returned. 110 | // A maximum of 50 contacts can be requested at a time. 111 | // The contacts are not returned in any particular order. 112 | public dynamic GetContactsInfo(string contacts) 113 | => _client.GetContactsInfo(contacts).WaitAndUnwrapException(); 114 | 115 | // Returns maximum of 50 newest trade messages. 116 | // Messages are ordered by sending time, and the newest one is first. 117 | // The list has same format as /api/contact_messages/, but each message has also contact_id field. 118 | public dynamic GetRecentMessages() 119 | => _client.GetRecentMessages().WaitAndUnwrapException(); 120 | 121 | // Gives feedback to user. 122 | // Possible feedback values are: trust, positive, neutral, block, block_without_feedback as strings. 123 | // You may also set feedback message field with few exceptions. Feedback block_without_feedback clears the message and with block the message is mandatory. 124 | public dynamic PostFeedbackToUser(string userName, string feedback, string message = null) 125 | => _client.PostFeedbackToUser(userName, feedback, message).WaitAndUnwrapException(); 126 | 127 | // Gets information about the token owner"s wallet balance. 128 | public dynamic GetWallet() 129 | => _client.GetWallet().WaitAndUnwrapException(); 130 | 131 | // Same as / api / wallet / above, but only returns the message, receiving_address_list and total fields. 132 | // (There"s also a receiving_address_count but it is always 1: only the latest receiving address is ever returned by this call.) 133 | // Use this instead if you don"t care about transactions at the moment. 134 | public dynamic GetWalletBalance() 135 | => _client.GetWalletBalance().WaitAndUnwrapException(); 136 | 137 | // Sends amount bitcoins from the token owner"s wallet to address. 138 | // Note that this API requires its own API permission called Money. 139 | // On success, this API returns just a message indicating success. 140 | // It is highly recommended to minimize the lifetime of access tokens with the money permission. 141 | // Call / api / logout / to make the current token expire instantly. 142 | public dynamic WalletSend(decimal amount, string address) 143 | => _client.WalletSend(amount, address).WaitAndUnwrapException(); 144 | 145 | // As above, but needs the token owner"s active PIN code to succeed. 146 | // Look before you leap. You can check if a PIN code is valid without attempting a send with / api / pincode /. 147 | // Security concern: To get any security beyond the above API, do not retain the PIN code beyond a reasonable user session, a few minutes at most. 148 | // If you are planning to save the PIN code anyway, please save some headache and get the real no-pin - required money permission instead. 149 | public dynamic WalletSendWithPin(decimal amount, string address, string pincode) 150 | => _client.WalletSendWithPin(amount, address, pincode).WaitAndUnwrapException(); 151 | 152 | // Gets an unused receiving address for the token owner"s wallet, its address given in the address key of the response. 153 | // Note that this API may keep returning the same(unused) address if called repeatedly. 154 | public dynamic GetWalletAddress() 155 | => _client.GetWalletAddress().WaitAndUnwrapException(); 156 | 157 | // Gets the current outgoing and deposit fees in bitcoins (BTC). 158 | public dynamic GetFees() 159 | => _client.GetFees().WaitAndUnwrapException(); 160 | 161 | // Expires the current access token immediately. 162 | // To get a new token afterwards, public apps will need to reauthenticate, confidential apps can turn in a refresh token. 163 | public dynamic Logout() 164 | => _client.Logout().WaitAndUnwrapException(); 165 | 166 | // Lists the token owner"s all ads on the data key ad_list, optionally filtered. If there"s a lot of ads, the listing will be paginated. 167 | // Refer to the ad editing pages for the field meanings.List item structure is like so: 168 | public dynamic GetOwnAds() 169 | => _client.GetOwnAds().WaitAndUnwrapException(); 170 | 171 | // Get ad 172 | public dynamic GetAd(string adId) 173 | => _client.GetAd(adId).WaitAndUnwrapException(); 174 | 175 | public dynamic GetAdList(IEnumerable adList) 176 | => _client.GetAdList(adList).WaitAndUnwrapException(); 177 | 178 | // Delete ad 179 | public dynamic DeleteAd(string adId) 180 | => _client.DeleteAd(adId).WaitAndUnwrapException(); 181 | 182 | // Edit ad visibility 183 | public dynamic EditAdVisiblity(string adId, bool visible) 184 | => _client.EditAdVisiblity(adId, visible).WaitAndUnwrapException(); 185 | 186 | // Edit ad 187 | public dynamic EditAd(string adId, Dictionary values, bool preloadAd = false) 188 | => _client.EditAd(adId, values, preloadAd).WaitAndUnwrapException(); 189 | 190 | // Edit ad Price equation formula 191 | public dynamic EditAdPriceEquation(string adId, decimal priceEquation) 192 | => _client.EditAdPriceEquation(adId, priceEquation).WaitAndUnwrapException(); 193 | 194 | // Retrieves recent notifications. 195 | public dynamic GetNotifications() 196 | => _client.GetNotifications().WaitAndUnwrapException(); 197 | 198 | // Marks specific notification as read. 199 | public dynamic NotificationMarkAsRead(string notificationId) 200 | => _client.NotificationMarkAsRead(notificationId).WaitAndUnwrapException(); 201 | 202 | /// 203 | /// This API returns buy Bitcoin online ads. It is closely modeled after the online ad listings on LocalBitcoins.com. 204 | /// 205 | /// Three letter currency code 206 | /// An example of a valid argument is national-bank-transfer. 207 | /// Page number, by default 1 208 | /// Ads are returned in the same structure as /api/ads/. 209 | public dynamic PublicMarket_BuyBitcoinsOnlineByCurrency(string currency, string paymentMethod = null, int page = 1) 210 | => _client.PublicMarket_BuyBitcoinsOnlineByCurrency(currency, paymentMethod, page).WaitAndUnwrapException(); 211 | 212 | /// 213 | /// This API returns sell Bitcoin online ads. It is closely modeled after the online ad listings on LocalBitcoins.com. 214 | /// 215 | /// Three letter currency code 216 | /// An example of a valid argument is national-bank-transfer. 217 | /// Page number, by default 1 218 | /// Ads are returned in the same structure as /api/ads/. 219 | public dynamic PublicMarket_SellBitcoinsOnlineByCurrency(string currency, string paymentMethod = null, int page = 1) 220 | => _client.PublicMarket_SellBitcoinsOnlineByCurrency(currency, paymentMethod, page).WaitAndUnwrapException(); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/LocalBitcoinsApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | LocalBitcoins .Net API written in C#. Official API documentation here: https://localbitcoins.com/api-docs/ 6 | Copyright © Alexander Biryukov 2016-2018 7 | 8 | Alexander Biryukov 9 | true 10 | true 11 | LocalBitcoinsApi.snk 12 | false 13 | https://raw.githubusercontent.com/aabiryukov/LocalBitcoinsApi/master/LICENSE 14 | https://github.com/aabiryukov/LocalBitcoinsApi/ 15 | https://raw.githubusercontent.com/aabiryukov/LocalBitcoinsApi/master/NugetPackageLogo.png 16 | https://github.com/aabiryukov/LocalBitcoinsApi/ 17 | LocalBitcoins Bitcoin API 18 | 2.0.0.0 19 | 2.0.0.0 20 | 2.0.2 21 | LocalBitcoins .Net API 22 | - Added async/await client (class LocalBitcoinsClient) 23 | - Migrated to .Net Standard 2.0 24 | - Class LocalBitcoinsApi is obslolete, use LocalBitcoinsClient instead 25 | 26 | 27 | 28 | 29 | 0 30 | 31 | 32 | 33 | 34 | all 35 | runtime; build; native; contentfiles; analyzers 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/LocalBitcoinsApi.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/LocalBitcoinsApi.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aabiryukov/LocalBitcoinsApi/73e6fea983f974e068a90ed7e82342026c6443e2/src/LocalBitcoinsApi/LocalBitcoinsApi.snk -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/LocalBitcoinsClient.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | 8 | namespace LocalBitcoins 9 | { 10 | // ReSharper disable once InconsistentNaming 11 | public class LocalBitcoinsClient 12 | { 13 | private const int DefaultApiTimeoutSec = 10; 14 | private readonly LocalBitcoinsRestApi _restApi; 15 | 16 | private class NameValueDictionary : Dictionary { } 17 | 18 | /// 19 | /// Unique ctor sets access key and secret key, which cannot be changed later. 20 | /// 21 | /// Your Access Key 22 | /// Your Secret Key 23 | /// API request timeout in seconds 24 | /// Override API base address. Default is "https://localbitcoins.net/" 25 | public LocalBitcoinsClient(string accessKey, string secretKey, int apiTimeoutSec = DefaultApiTimeoutSec, string overrideBaseAddress = null) 26 | { 27 | _restApi = new LocalBitcoinsRestApi(accessKey, secretKey, apiTimeoutSec, overrideBaseAddress); 28 | } 29 | 30 | protected virtual async Task CallApiAsync(string apiCommand, RequestType requestType = RequestType.Get, Dictionary args = null) 31 | { 32 | return await _restApi.CallApiAsync(apiCommand, requestType, args).ConfigureAwait(false); 33 | } 34 | 35 | // Returns public user profile information 36 | public async Task GetAccountInfo(string userName) 37 | { 38 | return await CallApiAsync("/api/account_info/" + userName + "/").ConfigureAwait(false); 39 | } 40 | 41 | // Return the information of the currently logged in user(the owner of authentication token). 42 | public async Task GetMyself() 43 | { 44 | return await CallApiAsync("/api/myself/").ConfigureAwait(false); 45 | } 46 | 47 | // Checks the given PIN code against the user"s currently active PIN code. 48 | // You can use this method to ensure the person using the session is the legitimate user. 49 | public async Task CheckPinCode(string code) 50 | { 51 | var args = new NameValueDictionary 52 | { 53 | {"code", code}, 54 | }; 55 | 56 | return await CallApiAsync("/api/pincode/", RequestType.Post, args).ConfigureAwait(false); 57 | } 58 | 59 | // Return open and active contacts 60 | public async Task GetDashboard() 61 | { 62 | return await CallApiAsync("/api/dashboard/").ConfigureAwait(false); 63 | } 64 | 65 | // Return released(successful) contacts 66 | public async Task GetDashboardReleased() 67 | { 68 | return await CallApiAsync("/api/dashboard/released/").ConfigureAwait(false); 69 | } 70 | 71 | // Return canceled contacts 72 | public async Task GetDashboardCanceled() 73 | { 74 | return await CallApiAsync("/api/dashboard/canceled/").ConfigureAwait(false); 75 | } 76 | 77 | // Return closed contacts, both released and canceled 78 | public async Task GetDashboardClosed() 79 | { 80 | return await CallApiAsync("/api/dashboard/closed/").ConfigureAwait(false); 81 | } 82 | 83 | // Releases the escrow of contact specified by ID { contact_id }. 84 | // On success there"s a complimentary message on the data key. 85 | public async Task ContactRelease(string contactId) 86 | { 87 | return await CallApiAsync("/api/contact_release/" + contactId + "/", RequestType.Post).ConfigureAwait(false); 88 | } 89 | 90 | // Releases the escrow of contact specified by ID { contact_id }. 91 | // On success there"s a complimentary message on the data key. 92 | public async Task ContactReleasePin(string contactId, string pincode) 93 | { 94 | var args = new NameValueDictionary 95 | { 96 | {"pincode", pincode}, 97 | }; 98 | 99 | return await CallApiAsync("/api/contact_release_pin/" + contactId + "/", RequestType.Post, args).ConfigureAwait(false); 100 | } 101 | 102 | // Reads all messaging from the contact.Messages are on the message_list key. 103 | // On success there"s a complimentary message on the data key. 104 | // attachment_* fields exist only if there is an attachment. 105 | public async Task GetContactMessages(string contactId) 106 | { 107 | return await CallApiAsync("/api/contact_messages/" + contactId + "/").ConfigureAwait(false); 108 | } 109 | 110 | public async Task GetContactMessageAttachment(string contractId, string attachmentId) 111 | { 112 | return await _restApi.CallApiAsync($"/api/contact_message_attachment/{contractId}/{attachmentId}/", RequestType.Get, null, true).ConfigureAwait(false); 113 | } 114 | 115 | // Marks a contact as paid. 116 | // It is recommended to access this API through /api/online_buy_contacts/ entries" action key. 117 | public async Task MarkContactAsPaid(string contactId) 118 | { 119 | return await CallApiAsync("/api/contact_mark_as_paid/" + contactId + "/").ConfigureAwait(false); 120 | } 121 | 122 | // Post a message to contact 123 | public async Task PostMessageToContact(string contactId, string message, string attachFileName = null) 124 | { 125 | if (attachFileName != null && !File.Exists(attachFileName)) 126 | throw new LocalBitcoinsException("PostMessageToContact", "File not found: " + attachFileName); 127 | 128 | NameValueDictionary args = null; 129 | if (!string.IsNullOrEmpty(message)) 130 | { 131 | args = new NameValueDictionary 132 | { 133 | {"msg", message}, 134 | }; 135 | } 136 | 137 | return await _restApi.CallApiPostFileAsync("/api/contact_message_post/" + contactId + "/", args, attachFileName).ConfigureAwait(false); 138 | } 139 | 140 | // Starts a dispute with the contact, if possible. 141 | // You can provide a short description using topic. This helps support to deal with the problem. 142 | public async Task StartDispute(string contactId, string topic = null) 143 | { 144 | NameValueDictionary args = null; 145 | if (topic != null) 146 | { 147 | args = new NameValueDictionary 148 | { 149 | {"topic", topic}, 150 | }; 151 | } 152 | 153 | return await CallApiAsync("/api/contact_dispute/" + contactId + "/", RequestType.Post, args).ConfigureAwait(false); 154 | } 155 | 156 | // Cancels the contact, if possible 157 | public async Task CancelContact(string contactId) 158 | { 159 | return await CallApiAsync("/api/contact_cancel/" + contactId + "/", RequestType.Post).ConfigureAwait(false); 160 | } 161 | 162 | // Attempts to fund an unfunded local contact from the seller"s wallet. 163 | public async Task FundContact(string contactId) 164 | { 165 | return await CallApiAsync("/api/contact_fund/" + contactId + "/", RequestType.Post).ConfigureAwait(false); 166 | } 167 | 168 | // Attempts to create a contact to trade bitcoins. 169 | // Amount is a number in the advertisement"s fiat currency. 170 | // Returns the API URL to the newly created contact at actions.contact_url. 171 | // Whether the contact was able to be funded automatically is indicated at data.funded. 172 | // Only non-floating LOCAL_SELL may return unfunded, all other trade types either fund or fail. 173 | public async Task CreateContact(string contactId, decimal amount, string message = null) 174 | { 175 | var args = new NameValueDictionary 176 | { 177 | {"amount", amount.ToString(CultureInfo.InvariantCulture)}, 178 | }; 179 | 180 | if (message != null) 181 | args.Add("message", message); 182 | 183 | return await CallApiAsync("/api/contact_create/" + contactId + "/", RequestType.Post, args).ConfigureAwait(false); 184 | } 185 | 186 | 187 | // Gets information about a single contact you are involved in. Same fields as in /api/contacts/. 188 | public async Task GetContactInfo(string contactId) 189 | { 190 | return await CallApiAsync("/api/contact_info/" + contactId + "/").ConfigureAwait(false); 191 | } 192 | 193 | // contacts is a comma-separated list of contact IDs that you want to access in bulk. 194 | // The token owner needs to be either a buyer or seller in the contacts, contacts that do not pass this check are simply not returned. 195 | // A maximum of 50 contacts can be requested at a time. 196 | // The contacts are not returned in any particular order. 197 | public async Task GetContactsInfo(string contacts) 198 | { 199 | var args = new NameValueDictionary 200 | { 201 | {"contacts", contacts}, 202 | }; 203 | return await CallApiAsync("/api/contact_info/", RequestType.Get, args).ConfigureAwait(false); 204 | } 205 | 206 | // Returns maximum of 50 newest trade messages. 207 | // Messages are ordered by sending time, and the newest one is first. 208 | // The list has same format as /api/contact_messages/, but each message has also contact_id field. 209 | public async Task GetRecentMessages() 210 | { 211 | return await CallApiAsync("/api/recent_messages/").ConfigureAwait(false); 212 | } 213 | 214 | // Gives feedback to user. 215 | // Possible feedback values are: trust, positive, neutral, block, block_without_feedback as strings. 216 | // You may also set feedback message field with few exceptions. Feedback block_without_feedback clears the message and with block the message is mandatory. 217 | public async Task PostFeedbackToUser(string userName, string feedback, string message = null) 218 | { 219 | var args = new NameValueDictionary 220 | { 221 | {"feedback", feedback}, 222 | }; 223 | 224 | if (message != null) 225 | args.Add("msg", message); 226 | 227 | return await CallApiAsync("/api/feedback/" + userName + "/", RequestType.Post, args).ConfigureAwait(false); 228 | } 229 | 230 | // Gets information about the token owner"s wallet balance. 231 | public async Task GetWallet() 232 | { 233 | return await CallApiAsync("/api/wallet/").ConfigureAwait(false); 234 | } 235 | 236 | // Same as / api / wallet / above, but only returns the message, receiving_address_list and total fields. 237 | // (There"s also a receiving_address_count but it is always 1: only the latest receiving address is ever returned by this call.) 238 | // Use this instead if you don"t care about transactions at the moment. 239 | public async Task GetWalletBalance() 240 | { 241 | return await CallApiAsync("/api/wallet-balance/").ConfigureAwait(false); 242 | } 243 | 244 | // Sends amount bitcoins from the token owner"s wallet to address. 245 | // Note that this API requires its own API permission called Money. 246 | // On success, this API returns just a message indicating success. 247 | // It is highly recommended to minimize the lifetime of access tokens with the money permission. 248 | // Call / api / logout / to make the current token expire instantly. 249 | public async Task WalletSend(decimal amount, string address) 250 | { 251 | var args = new NameValueDictionary 252 | { 253 | {"amount", amount.ToString(CultureInfo.InvariantCulture)}, 254 | {"address", address}, 255 | }; 256 | 257 | return await CallApiAsync("/api/wallet-send/", RequestType.Post, args).ConfigureAwait(false); 258 | } 259 | 260 | // As above, but needs the token owner"s active PIN code to succeed. 261 | // Look before you leap. You can check if a PIN code is valid without attempting a send with / api / pincode /. 262 | // Security concern: To get any security beyond the above API, do not retain the PIN code beyond a reasonable user session, a few minutes at most. 263 | // If you are planning to save the PIN code anyway, please save some headache and get the real no-pin - required money permission instead. 264 | public async Task WalletSendWithPin(decimal amount, string address, string pincode) 265 | { 266 | var args = new NameValueDictionary 267 | { 268 | {"amount", amount.ToString(CultureInfo.InvariantCulture)}, 269 | {"address", address}, 270 | {"pincode", pincode}, 271 | }; 272 | 273 | return await CallApiAsync("/api/wallet-send-pin/", RequestType.Post, args).ConfigureAwait(false); 274 | } 275 | 276 | // Gets an unused receiving address for the token owner"s wallet, its address given in the address key of the response. 277 | // Note that this API may keep returning the same(unused) address if called repeatedly. 278 | public async Task GetWalletAddress() 279 | { 280 | return await CallApiAsync("/api/wallet-addr/", RequestType.Post).ConfigureAwait(false); 281 | } 282 | 283 | // Gets the current outgoing and deposit fees in bitcoins (BTC). 284 | public async Task GetFees() 285 | { 286 | return await CallApiAsync("/api/fees/", RequestType.Get).ConfigureAwait(false); 287 | } 288 | 289 | // Expires the current access token immediately. 290 | // To get a new token afterwards, public apps will need to reauthenticate, confidential apps can turn in a refresh token. 291 | public async Task Logout() 292 | { 293 | return await CallApiAsync("/api/logout/", RequestType.Post).ConfigureAwait(false); 294 | } 295 | 296 | // Lists the token owner"s all ads on the data key ad_list, optionally filtered. If there"s a lot of ads, the listing will be paginated. 297 | // Refer to the ad editing pages for the field meanings.List item structure is like so: 298 | public async Task GetOwnAds() 299 | { 300 | return await CallApiAsync("/api/ads/", RequestType.Get).ConfigureAwait(false); 301 | } 302 | 303 | // Get ad 304 | public async Task GetAd(string adId) 305 | { 306 | return await CallApiAsync("/api/ad-get/" + adId + "/").ConfigureAwait(false); 307 | } 308 | 309 | public async Task GetAdList(IEnumerable adList) 310 | { 311 | var args = new Dictionary 312 | { 313 | {"ads", string.Join(",", adList) }, 314 | }; 315 | 316 | return await CallApiAsync("/api/ad-get/", RequestType.Get, args).ConfigureAwait(false); 317 | } 318 | 319 | // Delete ad 320 | public async Task DeleteAd(string adId) 321 | { 322 | return await CallApiAsync("/api/ad-delete/" + adId + "/", RequestType.Post).ConfigureAwait(false); 323 | } 324 | 325 | private static Dictionary ParseAdData(dynamic adData) 326 | { 327 | var args = new Dictionary 328 | { 329 | {"lat", (string)adData.lat}, 330 | {"price_equation", (string)adData.price_equation}, 331 | {"lon", (string)adData.lon}, 332 | {"countrycode", (string)adData.countrycode}, 333 | {"currency", (string)adData.currency}, 334 | 335 | {"min_amount", (string)adData.min_amount}, 336 | {"max_amount", (string)adData.max_amount}, 337 | 338 | {"msg", (string)adData.msg}, 339 | {"require_identification", (string)adData.require_identification}, 340 | {"sms_verification_required", (string)adData.sms_verification_required}, 341 | {"require_trusted_by_advertiser", (string)adData.require_trusted_by_advertiser}, 342 | {"trusted_required", (string)adData.trusted_required}, 343 | {"track_max_amount", (string)adData.track_max_amount}, 344 | {"email", (string)adData.email}, 345 | 346 | {"visible", (string)adData.visible}, 347 | }; 348 | 349 | if((string)adData.opening_hours != "null") 350 | { 351 | args["opening_hours"] = (string)adData.opening_hours; 352 | } 353 | 354 | if (!string.IsNullOrEmpty((string)adData.limit_to_fiat_amounts)) 355 | { 356 | args["limit_to_fiat_amounts"] = (string)adData.limit_to_fiat_amounts; 357 | } 358 | 359 | if (!string.IsNullOrEmpty((string)adData.bank_name)) 360 | { 361 | args["bank_name"] = (string)adData.bank_name; 362 | } 363 | 364 | string phone_number = adData.account_details?.phone_number; 365 | if (adData.account_details?.phone_number != null) 366 | { 367 | args["details-phone_number"] = phone_number; 368 | } 369 | 370 | return args; 371 | } 372 | 373 | // Edit ad visibility 374 | public async Task EditAdVisiblity(string adId, bool visible) 375 | { 376 | var oldAd = await GetAd(adId).ConfigureAwait(false); 377 | // Debug.WriteLine(ad.ToString()); 378 | 379 | if ((int)oldAd.data.ad_count < 1) 380 | { 381 | throw new LocalBitcoinsException("EditAdVisiblity", "Ad not found. Id=" + adId); 382 | } 383 | 384 | var args = ParseAdData(oldAd.data.ad_list[0].data); 385 | 386 | args["visible"] = visible ? "1" : "0"; 387 | 388 | return await CallApiAsync("/api/ad/" + adId + "/", RequestType.Post, args); 389 | } 390 | 391 | // Edit ad 392 | public async Task EditAd(string adId, Dictionary values, bool preloadAd = false) 393 | { 394 | if (values == null) 395 | throw new ArgumentNullException(nameof(values)); 396 | 397 | Dictionary args; 398 | 399 | if (preloadAd) 400 | { 401 | var oldAd = await GetAd(adId).ConfigureAwait(false); 402 | // Debug.WriteLine(ad.ToString()); 403 | 404 | if ((int)oldAd.data.ad_count < 1) 405 | { 406 | throw new LocalBitcoinsException("EditAd", "Ad not found. Id=" + adId); 407 | } 408 | 409 | args = ParseAdData(oldAd.data.ad_list[0].data); 410 | 411 | // Copy user values 412 | foreach (var val in values) 413 | { 414 | args[val.Key] = val.Value; 415 | } 416 | } 417 | else 418 | { 419 | args = values; 420 | } 421 | 422 | return await CallApiAsync("/api/ad/" + adId + "/", RequestType.Post, args).ConfigureAwait(false); 423 | } 424 | 425 | // Edit ad Price equation formula 426 | public async Task EditAdPriceEquation(string adId, decimal priceEquation) 427 | { 428 | if (priceEquation <= 0) 429 | throw new ArgumentOutOfRangeException(nameof(priceEquation)); 430 | 431 | var args = new Dictionary() 432 | { 433 | ["price_equation"] = priceEquation.ToString(CultureInfo.InvariantCulture) 434 | }; 435 | 436 | return await CallApiAsync("/api/ad-equation/" + adId + "/", RequestType.Post, args).ConfigureAwait(false); 437 | } 438 | 439 | // Retrieves recent notifications. 440 | public async Task GetNotifications() 441 | { 442 | return await CallApiAsync("/api/notifications/").ConfigureAwait(false); 443 | } 444 | 445 | // Marks specific notification as read. 446 | public async Task NotificationMarkAsRead(string notificationId) 447 | { 448 | return await CallApiAsync("/api/notifications/mark_as_read/" + notificationId + "/", RequestType.Post).ConfigureAwait(false); 449 | } 450 | 451 | /// 452 | /// This API returns buy Bitcoin online ads. It is closely modeled after the online ad listings on LocalBitcoins.com. 453 | /// 454 | /// Three letter currency code 455 | /// An example of a valid argument is national-bank-transfer. 456 | /// Page number, by default 1 457 | /// Ads are returned in the same structure as /api/ads/. 458 | public async Task PublicMarket_BuyBitcoinsOnlineByCurrency(string currency, string paymentMethod = null, int page = 1) 459 | { 460 | var uri = "/buy-bitcoins-online/" + currency + "/"; 461 | if (paymentMethod != null) 462 | uri += paymentMethod + "/"; 463 | 464 | uri += ".json"; 465 | 466 | if (page > 1) 467 | uri += "?page=" + page.ToString(CultureInfo.InvariantCulture); 468 | 469 | return await _restApi.CallPublicApiAsync(uri).ConfigureAwait(false); 470 | } 471 | 472 | /// 473 | /// This API returns sell Bitcoin online ads. It is closely modeled after the online ad listings on LocalBitcoins.com. 474 | /// 475 | /// Three letter currency code 476 | /// An example of a valid argument is national-bank-transfer. 477 | /// Page number, by default 1 478 | /// Ads are returned in the same structure as /api/ads/. 479 | public async Task PublicMarket_SellBitcoinsOnlineByCurrency(string currency, string paymentMethod = null, int page = 1) 480 | { 481 | var uri = "/sell-bitcoins-online/" + currency + "/"; 482 | if (paymentMethod != null) 483 | uri += paymentMethod + "/"; 484 | 485 | uri += ".json"; 486 | 487 | if (page > 1) 488 | uri += "?page=" + page.ToString(CultureInfo.InvariantCulture); 489 | 490 | return await _restApi.CallPublicApiAsync(uri).ConfigureAwait(false); 491 | } 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/LocalBitcoinsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Runtime.Serialization; 4 | 5 | namespace LocalBitcoins 6 | { 7 | [Serializable] 8 | public class LocalBitcoinsException : Exception 9 | { 10 | public string RequestMethod { get; } 11 | public dynamic DataJson { get; private set; } 12 | 13 | public LocalBitcoinsException() 14 | { } 15 | 16 | public LocalBitcoinsException(string message, Exception innerException) 17 | : base(message, innerException) 18 | { 19 | } 20 | 21 | public LocalBitcoinsException(string message) 22 | : base(message) 23 | { 24 | } 25 | 26 | public LocalBitcoinsException(string callerMethod, string message) 27 | : base(message) 28 | { 29 | RequestMethod = callerMethod; 30 | } 31 | 32 | public static void ThrowException(string callerMethod, dynamic json) 33 | { 34 | var ex = new LocalBitcoinsException(callerMethod, FormatMessage(callerMethod, json)) 35 | { 36 | DataJson = json 37 | }; 38 | 39 | throw ex; 40 | } 41 | 42 | private static string FormatMessage(string callerMethod, dynamic json) 43 | { 44 | if (json == null) 45 | return string.Format(CultureInfo.InvariantCulture, "Failed request {0}. Message: Null", callerMethod); 46 | 47 | var result = 48 | string.Format(CultureInfo.InvariantCulture, "Failed request {0}. Message: {1}. Error Code: {2}.", callerMethod, (string)json.error.message, (int)json.error.error_code); 49 | 50 | if(json.error.error_lists != null) 51 | { 52 | result += " Details: " + json.error.error_lists.ToString(); 53 | } 54 | 55 | return result; 56 | } 57 | 58 | protected LocalBitcoinsException(SerializationInfo serializationInfo, StreamingContext streamingContext) 59 | : base(serializationInfo, streamingContext) 60 | { 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/LocalBitcoinsRestApi.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Net.Http.Headers; 9 | using System.Security.Cryptography; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace LocalBitcoins 14 | { 15 | internal class LocalBitcoinsRestApi 16 | { 17 | private const string DefaultApiUrl = "https://localbitcoins.net/"; 18 | 19 | private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 20 | 21 | private readonly HttpClient _client; 22 | 23 | private readonly string _accessKey; 24 | private readonly string _secretKey; 25 | 26 | public LocalBitcoinsRestApi(string accessKey, string secretKey, int apiTimeoutSec, string overrideBaseAddress = null) 27 | { 28 | if (overrideBaseAddress == null) 29 | overrideBaseAddress = DefaultApiUrl; 30 | 31 | _accessKey = accessKey; 32 | _secretKey = secretKey; 33 | 34 | _client = new HttpClient() 35 | { 36 | BaseAddress = new Uri(overrideBaseAddress), // apiv3 37 | Timeout = TimeSpan.FromSeconds(apiTimeoutSec) 38 | }; 39 | } 40 | 41 | #region Public Methods 42 | 43 | public async Task CallPublicApiAsync(string relativeUrl) 44 | { 45 | var response = await _client.GetAsync(relativeUrl).ConfigureAwait(false); 46 | var resultAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 47 | var json = JsonConvert.DeserializeObject(resultAsString); 48 | return json; 49 | } 50 | 51 | public async Task CallApiAsync(string apiCommand, RequestType requestType, 52 | Dictionary args, bool getAsBinary = false) 53 | { 54 | HttpContent httpContent = null; 55 | 56 | if (requestType == RequestType.Post) 57 | { 58 | if (args != null && args.Any()) 59 | { 60 | httpContent = new FormUrlEncodedContent(args); 61 | } 62 | } 63 | 64 | try 65 | { 66 | var nonce = GetNonce(); 67 | var signature = GetSignature(apiCommand, nonce, args); 68 | 69 | var relativeUrl = apiCommand; 70 | if (requestType == RequestType.Get) 71 | { 72 | if (args != null && args.Any()) 73 | { 74 | relativeUrl += "?" + UrlEncodeParams(args); 75 | } 76 | } 77 | 78 | using (var request = new HttpRequestMessage( 79 | requestType == RequestType.Get ? HttpMethod.Get : HttpMethod.Post, 80 | new Uri(_client.BaseAddress, relativeUrl) 81 | )) 82 | { 83 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 84 | request.Headers.Add("Apiauth-Key", _accessKey); 85 | request.Headers.Add("Apiauth-Nonce", nonce); 86 | request.Headers.Add("Apiauth-Signature", signature); 87 | request.Content = httpContent; 88 | 89 | var response = await _client.SendAsync(request).ConfigureAwait(false); 90 | if (!response.IsSuccessStatusCode) 91 | { 92 | var resultAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 93 | var json = JsonConvert.DeserializeObject(resultAsString); 94 | LocalBitcoinsException.ThrowException(apiCommand, json); 95 | } 96 | 97 | if (getAsBinary) 98 | { 99 | var resultAsByteArray = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); 100 | return resultAsByteArray; 101 | } 102 | else 103 | { 104 | var resultAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 105 | var json = JsonConvert.DeserializeObject(resultAsString); 106 | return json; 107 | } 108 | } 109 | } 110 | finally 111 | { 112 | httpContent?.Dispose(); 113 | } 114 | } 115 | 116 | public async Task CallApiPostFileAsync(string apiCommand, Dictionary args, string fileName) 117 | { 118 | using (var httpContent = new MultipartFormDataContent()) 119 | { 120 | if (args != null) 121 | { 122 | foreach (var keyValuePair in args) 123 | { 124 | httpContent.Add(new StringContent(keyValuePair.Value), 125 | string.Format(CultureInfo.InvariantCulture, "\"{0}\"", keyValuePair.Key)); 126 | } 127 | } 128 | 129 | if (fileName != null) 130 | { 131 | var fileBytes = File.ReadAllBytes(fileName); 132 | httpContent.Add(new ByteArrayContent(fileBytes), "\"document\"", 133 | "\"" + Path.GetFileName(fileName) + "\""); 134 | } 135 | 136 | var bodyAsBytes = await httpContent.ReadAsByteArrayAsync().ConfigureAwait(false); 137 | 138 | var nonce = GetNonce(); 139 | var signature = GetSignatureBinary(apiCommand, nonce, bodyAsBytes); 140 | 141 | using (var request = new HttpRequestMessage( 142 | HttpMethod.Post, 143 | new Uri(_client.BaseAddress, apiCommand) 144 | )) 145 | { 146 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 147 | request.Headers.Add("Apiauth-Key", _accessKey); 148 | request.Headers.Add("Apiauth-Nonce", nonce); 149 | request.Headers.Add("Apiauth-Signature", signature); 150 | request.Content = httpContent; 151 | 152 | var response = await _client.SendAsync(request).ConfigureAwait(false); 153 | if (!response.IsSuccessStatusCode) 154 | { 155 | var resultAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 156 | var json = JsonConvert.DeserializeObject(resultAsString); 157 | LocalBitcoinsException.ThrowException(apiCommand, json); 158 | } 159 | 160 | { 161 | var resultAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 162 | var json = JsonConvert.DeserializeObject(resultAsString); 163 | return json; 164 | } 165 | } 166 | } 167 | } 168 | 169 | #endregion Public methods 170 | 171 | 172 | #region Private methods 173 | 174 | private static string GetNonce() 175 | { 176 | var nonce = (long)((DateTime.UtcNow - _unixEpoch).TotalMilliseconds * 1000); 177 | return nonce.ToString(CultureInfo.InvariantCulture); 178 | } 179 | 180 | private string GetSignature(string apiCommand, string nonce, Dictionary args) 181 | { 182 | return GetSignature(_accessKey, _secretKey, apiCommand, nonce, args); 183 | } 184 | 185 | private static string GetSignature(string accessKey, string secretKey, string apiCommand, string nonce, Dictionary args) 186 | { 187 | string paramsStr = null; 188 | 189 | if (args != null && args.Any()) 190 | { 191 | paramsStr = UrlEncodeParams(args); 192 | } 193 | 194 | var encoding = new ASCIIEncoding(); 195 | var secretByte = encoding.GetBytes(secretKey); 196 | using (var hmacsha256 = new HMACSHA256(secretByte)) 197 | { 198 | var message = nonce + accessKey + apiCommand; 199 | if (paramsStr != null) 200 | { 201 | message += paramsStr; 202 | } 203 | var messageByte = encoding.GetBytes(message); 204 | 205 | var signature = Utility.ByteToString(hmacsha256.ComputeHash(messageByte)); 206 | return signature; 207 | } 208 | } 209 | 210 | private static byte[] CombineBytes(byte[] first, byte[] second) 211 | { 212 | byte[] ret = new byte[first.Length + second.Length]; 213 | Buffer.BlockCopy(first, 0, ret, 0, first.Length); 214 | Buffer.BlockCopy(second, 0, ret, first.Length, second.Length); 215 | return ret; 216 | } 217 | 218 | private string GetSignatureBinary(string apiCommand, string nonce, byte[] paramBytes) 219 | { 220 | return GetSignatureBinary(_accessKey, _secretKey, apiCommand, nonce, paramBytes); 221 | } 222 | 223 | private static string GetSignatureBinary(string accessKey, string secretKey, string apiCommand, string nonce, byte[] paramBytes) 224 | { 225 | var encoding = new ASCIIEncoding(); 226 | var secretByte = encoding.GetBytes(secretKey); 227 | using (var hmacsha256 = new HMACSHA256(secretByte)) 228 | { 229 | var message = nonce + accessKey + apiCommand; 230 | var messageByte = encoding.GetBytes(message); 231 | if (paramBytes != null) 232 | { 233 | messageByte = CombineBytes(messageByte, paramBytes); 234 | } 235 | 236 | var signature = Utility.ByteToString(hmacsha256.ComputeHash(messageByte)); 237 | return signature; 238 | } 239 | } 240 | 241 | private static string UrlEncodeString(string text) 242 | { 243 | var result = text == null ? "" : Uri.EscapeDataString(text).Replace("%20", "+"); 244 | return result; 245 | } 246 | 247 | private static string UrlEncodeParams(Dictionary args) 248 | { 249 | var sb = new StringBuilder(); 250 | 251 | var arr = 252 | args.Select( 253 | x => 254 | string.Format(CultureInfo.InvariantCulture, "{0}={1}", UrlEncodeString(x.Key), UrlEncodeString(x.Value))).ToArray(); 255 | 256 | sb.Append(string.Join("&", arr)); 257 | return sb.ToString(); 258 | } 259 | 260 | #endregion Private methods 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/RequestType.cs: -------------------------------------------------------------------------------- 1 | namespace LocalBitcoins 2 | { 3 | public enum RequestType 4 | { 5 | Get, 6 | Post 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/Utility.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | using System.Linq; 4 | 5 | namespace LocalBitcoins 6 | { 7 | internal static class Utility 8 | { 9 | public static string ByteToString(IEnumerable buff) 10 | { 11 | return buff.Aggregate("", (current, t) => current + t.ToString("X2", CultureInfo.InvariantCulture)); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/LocalBitcoinsApi/UtilityExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace LocalBitcoins 5 | { 6 | internal static class UtilityExtensions 7 | { 8 | /* 9 | /// 10 | /// Waits for the task to complete, unwrapping any exceptions. 11 | /// 12 | /// The task. May not be null. 13 | public static void WaitAndUnwrapException(this Task task) 14 | { 15 | if (task == null) 16 | throw new ArgumentNullException(nameof(task)); 17 | task.GetAwaiter().GetResult(); 18 | } 19 | */ 20 | /// 21 | /// Waits for the task to complete, unwrapping any exceptions. 22 | /// 23 | /// The type of the result of the task. 24 | /// The task. May not be null. 25 | /// The result of the task. 26 | public static TResult WaitAndUnwrapException(this Task task) 27 | { 28 | if (task == null) 29 | throw new ArgumentNullException(nameof(task)); 30 | return task.GetAwaiter().GetResult(); 31 | } 32 | } 33 | } 34 | --------------------------------------------------------------------------------