Code/AutoRig/Vast/VastRental.cs

Parsers and data models for vast.ai rental/instance JSON payloads. Defines VastRental to represent the single-rental ledger entry with serialization/parse, and VastInstances with parsers for create responses, instance lists, single instance state, and utility to parse port mappings.

Networking
using System.Text.Json;

namespace AutoRig.Vast;

/// <summary>
/// The ownership ledger record: EXACTLY the one instance this library created.
/// It is written the moment vast.ai confirms creation and deleted only after a
/// VERIFIED destroy. The destroy path takes its id from here and nowhere else —
/// pre-existing user instances can never be touched because we never enumerate.
/// </summary>
public sealed class VastRental
{
    public required long InstanceId { get; init; }
    public required long OfferId { get; init; }
    public required string CreatedUtc { get; init; }
    public required string Label { get; init; }

    public string Serialize() => JsonSerializer.Serialize( new Dictionary<string, object>
    {
        ["instance_id"] = InstanceId,
        ["offer_id"] = OfferId,
        ["created_utc"] = CreatedUtc,
        ["label"] = Label,
    } );

    /// <summary>Null when the ledger is empty/blank (no rental outstanding).</summary>
    public static VastRental Parse( string json )
    {
        if ( string.IsNullOrWhiteSpace( json ) )
            return null;
        using var doc = JsonDocument.Parse( json );
        var root = doc.RootElement;
        if ( !root.TryGetProperty( "instance_id", out var id ) )
            return null;
        return new VastRental
        {
            InstanceId = id.GetInt64(),
            OfferId = root.TryGetProperty( "offer_id", out var offer ) ? offer.GetInt64() : 0,
            CreatedUtc = root.TryGetProperty( "created_utc", out var created )
                ? created.GetString() : "",
            Label = root.TryGetProperty( "label", out var label ) ? label.GetString() : "",
        };
    }
}

/// <summary>Pure parsers for the instance lifecycle endpoints.</summary>
public static class VastInstances
{
    /// <summary>PUT api/v0/asks/{id}/ response → the new instance id
    /// ("new_contract"). Throws when the API refused the rental.</summary>
    public static long ParseCreateResponse( string json )
    {
        ArgumentNullException.ThrowIfNull( json );
        using var doc = JsonDocument.Parse( json );
        var root = doc.RootElement;
        if ( root.TryGetProperty( "success", out var success )
            && success.ValueKind == JsonValueKind.False )
        {
            var message = root.TryGetProperty( "msg", out var msg ) ? msg.GetString() : "unknown";
            throw new FormatException( $"vast.ai refused the rental: {message}" );
        }
        if ( !root.TryGetProperty( "new_contract", out var contract ) )
            throw new FormatException( "vast.ai create response has no 'new_contract'." );
        return contract.GetInt64();
    }

    public sealed class InstanceState
    {
        public required string ActualStatus { get; init; }   // loading/running/...
        public required string PublicIp { get; init; }
        public required IReadOnlyDictionary<int, int> Ports { get; init; } // container → host
    }

    /// <summary>One row of GET api/v0/instances/ — a box the user already has
    /// running, shown so a rig can be OFFLOADED to it instead of renting anew.
    /// Read-only: listing never destroys anything (offload never touches the
    /// ledger or destroys the box - that is the whole point of reuse).</summary>
    public sealed class InstanceSummary
    {
        public required long Id { get; init; }
        public required string GpuName { get; init; }
        public required string ActualStatus { get; init; }
        public required string PublicIp { get; init; }
        public required double DollarsPerHour { get; init; }
        public required string Label { get; init; }          // instance label, if any
        public required IReadOnlyDictionary<int, int> Ports { get; init; }

        /// <summary>True when our rig server's port is published — a strong hint
        /// this box was started by auto-rig and can take an offloaded rig.</summary>
        public bool HasRigServer => Ports.ContainsKey( VastProtocol.ServerPort );
        public bool IsRunning => ActualStatus == "running";
    }

    /// <summary>Parses the user's instance list. Read-only enumeration used only
    /// to DISPLAY offload targets; nothing here can rent or destroy.</summary>
    public static List<InstanceSummary> ParseInstanceList( string json )
    {
        var result = new List<InstanceSummary>();
        if ( string.IsNullOrWhiteSpace( json ) )
            return result;
        using var doc = JsonDocument.Parse( json );
        var root = doc.RootElement;
        if ( !root.TryGetProperty( "instances", out var list )
            || list.ValueKind != JsonValueKind.Array )
            return result;
        foreach ( var instance in list.EnumerateArray() )
        {
            if ( instance.ValueKind != JsonValueKind.Object
                || !instance.TryGetProperty( "id", out var id ) )
                continue;
            result.Add( new InstanceSummary
            {
                Id = id.GetInt64(),
                GpuName = instance.TryGetProperty( "gpu_name", out var gpu )
                    ? gpu.GetString() ?? "GPU" : "GPU",
                ActualStatus = instance.TryGetProperty( "actual_status", out var status )
                    ? status.GetString() ?? "" : "",
                PublicIp = instance.TryGetProperty( "public_ipaddr", out var ip )
                    ? ip.GetString()?.Trim() ?? "" : "",
                DollarsPerHour = instance.TryGetProperty( "dph_total", out var dph )
                    && dph.ValueKind == JsonValueKind.Number ? dph.GetDouble() : 0,
                Label = instance.TryGetProperty( "label", out var label )
                    ? label.GetString() ?? "" : "",
                Ports = ParsePorts( instance ),
            } );
        }
        return result;
    }

    /// <summary>Shared container→host port map reader ("8188/tcp" → host port).</summary>
    static IReadOnlyDictionary<int, int> ParsePorts( JsonElement instance )
    {
        var ports = new Dictionary<int, int>();
        if ( !instance.TryGetProperty( "ports", out var portsElement )
            || portsElement.ValueKind != JsonValueKind.Object )
            return ports;
        foreach ( var entry in portsElement.EnumerateObject() )
        {
            var slash = entry.Name.IndexOf( '/' );
            var key = slash > 0 ? entry.Name[..slash] : entry.Name;
            if ( !int.TryParse( key, out var containerPort ) )
                continue;
            if ( entry.Value.ValueKind == JsonValueKind.Array
                && entry.Value.GetArrayLength() > 0
                && entry.Value[0].TryGetProperty( "HostPort", out var hostPort )
                && int.TryParse( hostPort.GetString(), out var host ) )
                ports[containerPort] = host;
        }
        return ports;
    }

    /// <summary>GET api/v0/instances/{id}/ response. Null when the instance is
    /// gone (the verified-destroy signal).</summary>
    public static InstanceState ParseInstance( string json )
    {
        if ( string.IsNullOrWhiteSpace( json ) )
            return null;
        using var doc = JsonDocument.Parse( json );
        var root = doc.RootElement;
        var instance = root.TryGetProperty( "instances", out var wrapped ) ? wrapped : root;
        if ( instance.ValueKind == JsonValueKind.Array )
        {
            if ( instance.GetArrayLength() == 0 )
                return null;
            instance = instance[0];
        }
        if ( instance.ValueKind != JsonValueKind.Object
            || !instance.TryGetProperty( "id", out _ ) )
            return null;

        return new InstanceState
        {
            ActualStatus = instance.TryGetProperty( "actual_status", out var status )
                ? status.GetString() ?? "" : "",
            PublicIp = instance.TryGetProperty( "public_ipaddr", out var ip )
                ? ip.GetString()?.Trim() ?? "" : "",
            Ports = ParsePorts( instance ),
        };
    }
}