Robinhood-CryptoTrading.psm1
<#
.SYNOPSIS Root module file for Robinhood Crypto Trading .DESCRIPTION This module provides functions to interact with the Robinhood Crypto Trading API .NOTES Author: Chris Masters Tags: Robinhood, RobinhoodCrypto, Crypto, Cryptocurrency, Trading, API, Finance, Investing, Investment #> #region EULA function Test-RHCEulaAccepted { <# .SYNOPSIS Checks if the EULA has been accepted. .DESCRIPTION Verifies if the EULA has been accepted by checking for the presence of a marker file or the existence of an environment variable. .OUTPUTS [System.Boolean] Returns $true if the EULA has been accepted, $false otherwise. #> [CmdletBinding()] [OutputType([bool])] Param() $eulaAcceptedMarkerDir = Join-Path $env:LOCALAPPDATA "RobinhoodCryptoTrading" $eulaAcceptedMarkerPath = Join-Path $eulaAcceptedMarkerDir "EulaAccepted.txt" return (Test-Path $eulaAcceptedMarkerPath) } function Set-RHCEulaAccepted { <# .SYNOPSIS Marks the EULA as accepted. .DESCRIPTION Creates a marker file indicating that the EULA has been accepted. .PARAMETER Force If specified, creates the marker file without prompting for confirmation. .OUTPUTS [System.Boolean] Returns $true if the operation was successful, $false otherwise. #> [CmdletBinding()] [OutputType([bool])] Param( [switch]$Force ) $eulaAcceptedMarkerDir = Join-Path $env:LOCALAPPDATA "RobinhoodCryptoTrading" $eulaAcceptedMarkerPath = Join-Path $eulaAcceptedMarkerDir "EulaAccepted.txt" try { if (-not (Test-Path $eulaAcceptedMarkerDir)) { New-Item -ItemType Directory -Path $eulaAcceptedMarkerDir -Force -ErrorAction Stop | Out-Null } Set-Content -Path $eulaAcceptedMarkerPath -Value "Accepted on $(Get-Date -Format 'u')" -Force -ErrorAction Stop if (-not $Force) { Write-Host "EULA accepted. The Robinhood Crypto module will now load." -ForegroundColor Green } return $true } catch { Write-Error "Could not save EULA acceptance status to '$eulaAcceptedMarkerPath'. Error: $($_.Exception.Message)" return $false } } function Show-RHCEulaPrompt { <# .SYNOPSIS Shows the EULA prompt and waits for user acceptance. .DESCRIPTION Displays the EULA text and prompts the user to accept or decline. If accepted, records the acceptance. .OUTPUTS [System.Boolean] Returns $true if the EULA was accepted, $false otherwise. #> [CmdletBinding()] [OutputType([bool])] Param() $moduleName = $MyInvocation.MyCommand.Module.Name $eulaText = @" =============================================================================== END USER LICENSE AGREEMENT FOR Robinhood-CryptoTrading PowerShell Module =============================================================================== IMPORTANT: PLEASE READ THIS AGREEMENT CAREFULLY BEFORE USING THIS SOFTWARE. BY USING THIS SOFTWARE, YOU AGREE TO BE BOUND BY THE TERMS OUTLINED BELOW. 1. LICENSE: This software is licensed under the MIT License. You can view the full license terms here: https://github.com/masters274/Robinhood-CryptoTrading/blob/main/LICENSE 2. DISCLAIMER: Please review the important disclaimer regarding the use of this software and the risks associated with cryptocurrency trading: https://github.com/masters274/Robinhood-CryptoTrading/blob/main/DISCLAIMER.md Trading cryptocurrencies involves significant risk. The author is not liable for any financial losses incurred using this software. Use at your own risk. 3. GETTING STARTED: For instructions on how to setup this module, please refer to the README file: https://github.com/masters274/Robinhood-CryptoTrading/blob/main/README.md 4. ACCEPTANCE: By typing 'yes' below and continuing to use this software, you acknowledge that you have read, understood, and agree to be bound by the MIT License (1) and the terms stated in the Disclaimer(2). If you do not agree to these terms, type 'no' to decline, and do not use the software. The module will not load. =============================================================================== "@ Write-Host $eulaText -ForegroundColor Yellow Write-Host "" # Prompt for acceptance $accepted = $false while (-not $accepted) { $response = Read-Host "Do you accept the terms of the EULA? (Enter 'yes' to accept or 'no' to decline)" if ($response -eq 'yes') { $accepted = Set-RHCEulaAccepted return $true } elseif ($response -eq 'no') { Write-Warning "EULA not accepted. Module '$moduleName' cannot be used." return $false } else { Write-Warning "Invalid input. Please enter 'yes' or 'no'." } } return $false } function Initialize-RHCEula { <# .SYNOPSIS Initializes the EULA acceptance process. .DESCRIPTION Checks if the EULA has been accepted, and if not, handles the acceptance process through either environment variables or by prompting the user. .OUTPUTS [System.Boolean] Returns $true if the EULA was accepted, $false otherwise. #> [CmdletBinding()] [OutputType([bool])] Param() if (Test-RHCEulaAccepted) { return $true } if ($env:RHC_EULA_ACCEPTED -eq 'yes') { return Set-RHCEulaAccepted -Force } return Show-RHCEulaPrompt } $eulaAccepted = Initialize-RHCEula if (-not $eulaAccepted) { throw "EULA not accepted. Module cannot be loaded." } #endregion #region Classes class RHMessage { [string] $ApiKey [string] $Path [string] $Method [string] $Body [Int64] $Timestamp [string] $Signature # Constructor: sets up the message properties and automatically assigns a current timestamp. RHMessage([string] $ApiKey = [string] $ApiKey, [string] $path, [string] $method, [string] $body = "") { $this.ApiKey = $apiKey $this.Path = $path $this.Method = $method $this.Body = $body $this.Timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() } # The Sign method uses BouncyCastle to sign the message. [void] Sign([string] $privateKeySeed) { # Construct the message to be signed. # Format: ApiKey + Timestamp + Path + Method + Body $messageToSign = "$($this.ApiKey)$($this.Timestamp)$($this.Path)$($this.Method)$($this.Body)" $messageBytes = [System.Text.Encoding]::UTF8.GetBytes($messageToSign) try { # Convert the Base64-encoded private key seed to a byte array. $privateKeySeedBytes = [Convert]::FromBase64String($privateKeySeed) } catch { throw "Invalid private key seed: not a valid Base64 string." } try { # Create an Ed25519 private key parameter using the provided seed. $privateKeyParams = New-Object Org.BouncyCastle.Crypto.Parameters.Ed25519PrivateKeyParameters($privateKeySeedBytes, 0) } catch { throw "Error creating Ed25519 private key parameters: $_" } try { # Instantiate the Ed25519Signer. $signer = New-Object Org.BouncyCastle.Crypto.Signers.Ed25519Signer # Initialize the signer for signing. $signer.Init($true, $privateKeyParams) # Feed the message bytes into the signer. $signer.BlockUpdate($messageBytes, 0, $messageBytes.Length) # Generate the signature as a byte array. $signatureBytes = $signer.GenerateSignature() # Convert the signature to a Base64 string. $this.Signature = [Convert]::ToBase64String($signatureBytes) } catch { throw "Error signing the message: $_" } } # Returns a hashtable containing the headers for an API call. [hashtable] GetHeaders() { return @{ "x-api-key" = $this.ApiKey "x-timestamp" = "$($this.Timestamp)" "x-signature" = $this.Signature "Content-Type" = "application/json; charset=utf-8" } } # Validates that the RHMessage has all required fields. # For GET requests, Body can be empty; for other methods, Body must be non-empty. [bool] IsValid() { if ([string]::IsNullOrWhiteSpace($this.ApiKey)) { return $false } if ([string]::IsNullOrWhiteSpace($this.Path)) { return $false } if ([string]::IsNullOrWhiteSpace($this.Method)) { return $false } if ($this.Method.ToUpper() -ne "GET" -and [string]::IsNullOrWhiteSpace($this.Body)) { return $false } return $true } } #endregion #region Main $privateFunctionPath = Join-Path -Path $PSScriptRoot -ChildPath 'PrivateFunctions' $privateFunctions = @() if (Test-Path -Path $privateFunctionPath) { $privateFunctionFiles = Get-ChildItem -Path $privateFunctionPath -Filter *.ps1 foreach ($pfile in $privateFunctionFiles) { try { . $pfile.FullName $privateFunctions += $pfile.BaseName Write-Verbose "Imported function $($pfile.BaseName)" } catch { Write-Error "Failed to import function $($pfile.FullName): $_" } } } else { Write-Warning "No PrivateFunctions directory found at $privateFunctionPath" } $publicFunctionPath = Join-Path -Path $PSScriptRoot -ChildPath 'PublicFunctions' $publicFunctions = @() if (Test-Path -Path $publicFunctionPath) { $functionFiles = Get-ChildItem -Path $publicFunctionPath -Filter *.ps1 foreach ($file in $functionFiles) { try { . $file.FullName $publicFunctions += $file.BaseName Write-Verbose "Imported function $($file.BaseName)" } catch { Write-Error "Failed to import function $($file.FullName): $_" } } } else { Write-Warning "No PublicFunctions directory found at $publicFunctionPath" } # Export all public functions Export-ModuleMember -Function $publicFunctions #endregion |