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.
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 ),
};
}
}