Coin.psm1

$CoinApis = @{
    "original" = "https://www.cryptocompare.com/api/data"
    "min-api" = "https://min-api.cryptocompare.com/data"
}
function ConvertFrom-UnixTime
{
    [CmdletBinding()]
    [OutputType([DateTime])]
    param
    (
        [Parameter(Mandatory)]
        [int]
        $TimeStamp
    )
    
    $Origin = [DateTime]"1/1/1970"
    $Origin.AddSeconds($TimeStamp)
}
function ConvertTo-Symbol
{
    [CmdletBinding()]
    param
    (
        [string[]]
        $Symbols
    )

    $Symbols.ToUpper() -join ','
}
function ConvertTo-UnixTime
{
    [CmdletBinding()]
    [OutputType([DateTime])]
    param
    (
        [Parameter(Mandatory)]
        [DateTime]
        $TimeStamp
    )
    
    $Origin = [DateTime]"1/1/1970"
    [Math]::Floor(($TimeStamp - $Origin | Select-Object -ExpandProperty TotalSeconds))
}
function Invoke-CoinRestMethod
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        [ValidateSet("original", "min-api")]
        $Api,

        [string]
        $Endpoint,

        [hashtable]
        $Body
    )

    try
    {
        $ApiBase = $CoinApis.$Api
        $Splat = @{
            Uri = "{0}/{1}" -f $ApiBase, $Endpoint
        }

        if ($Body)
        {
            $Splat["Body"] = $Body
        }

        # test that the response type value is greater than 100, if not, throw an error

        $Response = Invoke-RestMethod @Splat
        if ($Response.Response -eq "Error")
        {

            throw $Response.Message
        }
        else
        {
            $Response
        }
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}
function New-ErrorRecord
{
    [CmdletBinding()]
    [OutputType([System.Management.Automation.ErrorRecord])]
    param
    (
        [string]
        $ExceptionType,
        [string]
        $Message,
        $ErrorId,
        [System.Management.Automation.ErrorCategory]
        $ErrorCategory,
        $TargetObject
    )

    $Exception = New-Object -TypeName "System.$ExceptionType" -ArgumentList $Message
    [System.Management.Automation.ErrorRecord]::new(
        $Exception,
        $ErrorId,
        $ErrorCategory,
        $TargetObject
    )
}
function Test-Timespan
{
    [CmdletBinding()]
    param
    (
        [Timespan]
        $Timespan
    )

    if ($Timespan.Ticks -lt 0)
    {
        $Splat = @{
            ExceptionType = "InvalidOperationException"
            Message = "You specified a negative timespan! Make sure the beginning comes before the end."
            ErrorCategory = "InvalidArgument"
        }

        $ErrorRecord = New-ErrorRecord @Splat
        $PSCmdlet.ThrowTerminatingError($ErrorRecord)
    }
}
function Get-Coin
{
    [CmdletBinding()]
    param
    (
        [switch]
        $All
    )
    
    $ListObject = Invoke-CoinRestMethod -Api "min-api" -Endpoint "all/coinlist"
    $Coins = $ListObject.Data.PSObject.Properties | Select-Object -ExpandProperty Value
    $Watched = $ListObject.DefaultWatchList.CoinIs -split ','

    if ($PSBoundParameters.ContainsKey("All"))
    {
        $Coins | Select-Object -Property @{n="SortOrder"; e={[int]$_.SortOrder}},* -ExcludeProperty SortOrder
    }
    else
    {
        $Coins | Where-Object Id -in $Watched | Select-Object -Property @{n = "SortOrder"; e = {[int]$_.SortOrder}},* -ExcludeProperty SortOrder
    }
}
function Get-CoinPrice
{
    [CmdletBinding()]
    param
    (
        [string[]]
        $FromSymbols,

        [string[]]
        $ToSymbols
    )

    $Body = @{
        tsyms = ConvertTo-Symbol -Symbols $ToSymbols
        fsyms = ConvertTo-Symbol -Symbols $FromSymbols
    }

    $Raw = Invoke-CoinRestMethod -Api "min-api" -Endpoint "pricemultifull" -Body $Body | Select-Object -ExpandProperty Raw

    foreach ($FromSymbol in $FromSymbols)
    {
        foreach ($ToSymbol in $ToSymbols)
        {
            $Raw.$FromSymbol.$ToSymbol
        }
    }
}
function Get-CoinPriceHistory
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]
        $FromSymbol,

        [Parameter(Mandatory)]
        [string]
        $ToSymbol,

        [Parameter()]
        [ValidateSet("Day", "Hour", "Minute")]
        [string]
        $DataInterval = "Hour",

        [int]
        $Aggregate = 1,

        [Parameter(Mandatory)]
        [DateTime]
        $Since = (Get-Date).AddDays(-1),

        [Parameter(Mandatory)]
        [DateTime]
        $Until = (Get-Date)
    )
    
    try
    {
        $Timespan = $Until - $Since
        Test-Timespan -Timespan $Timespan

        $Splat = @{
            Api = "min-api"
            Body = @{
                tsym = ConvertTo-Symbol -Symbols $ToSymbol
                fsym = ConvertTo-Symbol -Symbols $FromSymbol
                toTs = ConvertTo-UnixTime -TimeStamp $Until
                aggregate = $Aggregate
            }
        }
    
        # how to validate that the timespan selected fits within the acceptable range for each endpoint?
        switch ($DataInterval)
        {
            Day
            {
                $Aggregate = [System.Math]::Min(30, $Aggregate)
                $Splat["Endpoint"] = "histoday"
                $Splat["Body"]["limit"] = [int]($Timespan.TotalDays / $Aggregate)
                # the histoday endpoint's aggregate parameter has a max value of 30
                $Splat["Body"]["aggregate"] = $Aggregate
            }
    
            Hour
            {
                $Splat["Endpoint"] = "histohour"
                $Splat["Body"]["limit"] = [int]($Timespan.TotalHours / $Aggregate)
            }
    
            Minute
            {
                $Splat["Endpoint"] = "histominute"
                $Splat["Body"]["limit"] = [int]($Timespan.TotalMinutes / $Aggregate)
            }
        }

        Invoke-CoinRestMethod @Splat |
            Select-Object -ExpandProperty Data |
            Select-Object -Property @{n="Time"; e={ConvertFrom-UnixTime $_.Time}},* -ExcludeProperty Time
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}