using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Execution;
/// <summary>Reconciles an unaccepted key only in the exact original live game session.</summary>
public static class NativeUnsubmittedTradeIntents
{
public static async Task ReconcileAsync(string directory,IOriginalCommerceChannel channel,PlayerState player,CancellationToken token)
{
if(!Directory.Exists(directory)||!player.InGame||!player.Docked||!player.CargoReliable||!player.CreditsReliable)return;
foreach(var pending in Directory.EnumerateFiles(directory,"*.pending.json"))
{
var confirmed=pending.Replace(".pending.json",".confirmed.json",StringComparison.Ordinal);
if(File.Exists(confirmed))continue;
byte[] bytes;
// Exclude an active writer before inspecting older incomplete files.
await using(var input=new FileStream(pending,FileMode.Open,FileAccess.Read,FileShare.Read))
{using var buffer=new MemoryStream();await input.CopyToAsync(buffer,token);bytes=buffer.ToArray();}
if(bytes.Length==0)
{
// Only the still-live process that spans this file's creation
// can prove a key was never accepted. A reload cannot do so.
var created=new DateTimeOffset(File.GetCreationTimeUtc(pending));
if(channel.ProcessStartedAt is not {} birth||created<birth||player.LoadGameAt is not {} loaded||created<loaded)continue;
var emptyKey=Path.GetFileName(pending).Replace(".pending.json","",StringComparison.Ordinal);
if(!Guid.TryParseExact(emptyKey,"N",out _))continue;
var emptyStatus=await channel.CallAsync(new{op="commerce.operation.status",operationKey=emptyKey},token);
if(emptyStatus.GetProperty("known").GetBoolean()||emptyStatus.GetProperty("mutationsSent").GetInt32()!=0)continue;
var inventory=await channel.CallAsync(new{op="commerce.inventory"},token);
if(!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=player.CargoCount)continue;
var stable=await channel.CallAsync(new{op="commerce.inventory"},token);
if(!stable.GetProperty("verified").GetBoolean()||stable.GetProperty("count").GetInt32()!=inventory.GetProperty("count").GetInt32()
||stable.GetProperty("credits").GetInt64()!=inventory.GetProperty("credits").GetInt64())continue;
var proof=JsonSerializer.SerializeToUtf8Bytes(new{operationKey=emptyKey,submitted=false,malformedIntent=true,channel=channel.Identity,
processStartedAt=birth,fileCreatedAt=created,sessionStartedAt=loaded,pendingSha256=Convert.ToHexStringLower(SHA256.HashData(bytes)),
originalOperationStatus=emptyStatus,actualClientInventory=inventory,stableClientInventory=stable,reconciledAt=DateTimeOffset.UtcNow});
await File.WriteAllBytesAsync(confirmed+".tmp",proof,token);token.ThrowIfCancellationRequested();File.Move(confirmed+".tmp",confirmed,false);
continue;
}
using var document=JsonDocument.Parse(bytes);var row=document.RootElement;
if(row.GetProperty("channel").GetString()!=channel.Identity||row.GetProperty("CommanderId").GetString()!=player.CommanderId
||row.GetProperty("CurrentShipId").GetInt64()!=player.CurrentShipId||row.GetProperty("LoadGameAt").GetDateTimeOffset()!=player.LoadGameAt)continue;
var key=row.GetProperty("operationKey").GetString()!;
var status=await channel.CallAsync(new{op="commerce.operation.status",operationKey=key},token);
// The native map retains every accepted key for this process's
// lifetime. A new process/session may never use absence as proof.
if(status.GetProperty("known").GetBoolean()||status.GetProperty("mutationsSent").GetInt32()!=0)continue;
var before=row.GetProperty("intent").GetProperty("before");
var current=await channel.CallAsync(new{op="commerce.inventory"},token);
if(!current.GetProperty("verified").GetBoolean()||!before.GetProperty("verified").GetBoolean()
||current.GetProperty("count").GetInt32()!=before.GetProperty("count").GetInt32()
||current.GetProperty("credits").GetInt64()!=before.GetProperty("credits").GetInt64())continue;
await using var file=new FileStream(confirmed,FileMode.CreateNew,FileAccess.Write,FileShare.Read);
await JsonSerializer.SerializeAsync(file,new{operationKey=key,submitted=false,reconciledAt=DateTimeOffset.UtcNow,channel=channel.Identity,
pendingSha256=Convert.ToHexStringLower(SHA256.HashData(bytes)),originalOperationStatus=status,actualClientInventory=current},cancellationToken:token);
}
}
}
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Execution;
/// <summary>Reconciles an unaccepted key only in the exact original live game session.</summary>
public static class NativeUnsubmittedTradeIntents
{
public static async Task ReconcileAsync(string directory,IOriginalCommerceChannel channel,PlayerState player,CancellationToken token)
{
if(!Directory.Exists(directory)||!player.InGame||!player.Docked||!player.CargoReliable||!player.CreditsReliable)return;
foreach(var pending in Directory.EnumerateFiles(directory,"*.pending.json"))
{
var confirmed=pending.Replace(".pending.json",".confirmed.json",StringComparison.Ordinal);
if(File.Exists(confirmed))continue;
byte[] bytes;
// Exclude an active writer before inspecting older incomplete files.
await using(var input=new FileStream(pending,FileMode.Open,FileAccess.Read,FileShare.Read))
{using var buffer=new MemoryStream();await input.CopyToAsync(buffer,token);bytes=buffer.ToArray();}
if(bytes.Length==0)
{
// Only the still-live process that spans this file's creation
// can prove a key was never accepted. A reload cannot do so.
var created=new DateTimeOffset(File.GetCreationTimeUtc(pending));
if(channel.ProcessStartedAt is not {} birth||created<birth||player.LoadGameAt is not {} loaded||created<loaded)continue;
var emptyKey=Path.GetFileName(pending).Replace(".pending.json","",StringComparison.Ordinal);
if(!Guid.TryParseExact(emptyKey,"N",out _))continue;
var emptyStatus=await channel.CallAsync(new{op="commerce.operation.status",operationKey=emptyKey},token);
if(emptyStatus.GetProperty("known").GetBoolean()||emptyStatus.GetProperty("mutationsSent").GetInt32()!=0)continue;
var inventory=await channel.CallAsync(new{op="commerce.inventory"},token);
if(!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=player.CargoCount)continue;
var stable=await channel.CallAsync(new{op="commerce.inventory"},token);
if(!stable.GetProperty("verified").GetBoolean()||stable.GetProperty("count").GetInt32()!=inventory.GetProperty("count").GetInt32()
||stable.GetProperty("credits").GetInt64()!=inventory.GetProperty("credits").GetInt64())continue;
var proof=JsonSerializer.SerializeToUtf8Bytes(new{operationKey=emptyKey,submitted=false,malformedIntent=true,channel=channel.Identity,
processStartedAt=birth,fileCreatedAt=created,sessionStartedAt=loaded,pendingSha256=Convert.ToHexStringLower(SHA256.HashData(bytes)),
originalOperationStatus=emptyStatus,actualClientInventory=inventory,stableClientInventory=stable,reconciledAt=DateTimeOffset.UtcNow});
await File.WriteAllBytesAsync(confirmed+".tmp",proof,token);token.ThrowIfCancellationRequested();File.Move(confirmed+".tmp",confirmed,false);
continue;
}
using var document=JsonDocument.Parse(bytes);var row=document.RootElement;
if(row.GetProperty("channel").GetString()!=channel.Identity||row.GetProperty("CommanderId").GetString()!=player.CommanderId
||row.GetProperty("CurrentShipId").GetInt64()!=player.CurrentShipId||row.GetProperty("LoadGameAt").GetDateTimeOffset()!=player.LoadGameAt)continue;
var key=row.GetProperty("operationKey").GetString()!;
var status=await channel.CallAsync(new{op="commerce.operation.status",operationKey=key},token);
// The native map retains every accepted key for this process's
// lifetime. A new process/session may never use absence as proof.
if(status.GetProperty("known").GetBoolean()||status.GetProperty("mutationsSent").GetInt32()!=0)continue;
var before=row.GetProperty("intent").GetProperty("before");
var current=await channel.CallAsync(new{op="commerce.inventory"},token);
if(!current.GetProperty("verified").GetBoolean()||!before.GetProperty("verified").GetBoolean()
||current.GetProperty("count").GetInt32()!=before.GetProperty("count").GetInt32()
||current.GetProperty("credits").GetInt64()!=before.GetProperty("credits").GetInt64())continue;
await using var file=new FileStream(confirmed,FileMode.CreateNew,FileAccess.Write,FileShare.Read);
await JsonSerializer.SerializeAsync(file,new{operationKey=key,submitted=false,reconciledAt=DateTimeOffset.UtcNow,channel=channel.Identity,
pendingSha256=Convert.ToHexStringLower(SHA256.HashData(bytes)),originalOperationStatus=status,actualClientInventory=current},cancellationToken:token);
}
}
}