using System.Numerics;
using System.Text;
using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Native;
using Neo.SmartContract.Framework.Services;
using Neo.Sol.Runtime.ABI;
using Neo.Sol.Runtime.Context;
using Neo.Sol.Runtime.Crypto;
using ExecutionContext = Neo.Sol.Runtime.Context.ExecutionContext;
using NeoFrameworkCallFlags = Neo.SmartContract.Framework.Services.CallFlags;
using NeoFrameworkContract = Neo.SmartContract.Framework.Services.Contract;
using NeoFrameworkContractManagement = Neo.SmartContract.Framework.Native.ContractManagement;
using NeoFrameworkGas = Neo.SmartContract.Framework.Native.GAS;
using NeoFrameworkRuntime = Neo.SmartContract.Framework.Services.Runtime;
namespace Neo.Sol.Runtime.Calls;
public sealed class ExternalCallManager
{
private readonly ExecutionContext _context;
private uint _callCount;
public ExternalCallManager(ExecutionContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_callCount = 0;
}
public uint GetCallCount() => _callCount;
public CallResult Call(UInt160 target, BigInteger value, uint gasLimit, byte[] callData)
{
return ExecuteCall(target, value, gasLimit, callData, CallType.Call);
}
public CallResult DelegateCall(UInt160 target, uint gasLimit, byte[] callData)
{
return ExecuteCall(target, 0, gasLimit, callData, CallType.DelegateCall);
}
public CallResult StaticCall(UInt160 target, uint gasLimit, byte[] callData)
{
return ExecuteCall(target, 0, gasLimit, callData, CallType.StaticCall);
}
public CreateResult Create(BigInteger value, byte[] initCode, uint gasLimit)
{
_ = value;
_ = initCode;
_ = gasLimit;
return CreateResult.Failed(
"Contract creation is not supported by the optional Neo.Sol.Runtime shim"
);
}
public CreateResult Create2(BigInteger value, byte[] initCode, byte[] salt, uint gasLimit)
{
_ = value;
_ = initCode;
_ = salt;
_ = gasLimit;
return CreateResult.Failed(
"CREATE2 is not supported by the optional Neo.Sol.Runtime shim"
);
}
private CallResult ExecuteCall(
UInt160 target,
BigInteger value,
uint gasLimit,
byte[] callData,
CallType callType
)
{
var originalSender = _context.Msg.Sender;
var originalValue = _context.Msg.Value;
var originalData = _context.Msg.Data;
try
{
if (!IsContractDeployed(target))
{
return CallResult.Failed("Target contract not deployed");
}
if (callData.Length < 4)
{
return CallResult.Failed("Invalid call data: too short");
}
if (callType == CallType.StaticCall && value > 0)
{
return CallResult.Failed("Static calls cannot transfer value");
}
if (callType == CallType.Call && value > 0 && !TransferGas(_context.Msg.Sender, target, value))
{
return CallResult.Failed("Value transfer failed");
}
var selector = callData[..4];
var parameters = callData.Length > 4 ? callData[4..] : Array.Empty<byte>();
var methodName = GetMethodNameFromSelector(selector);
object[] args = parameters.Length > 0
? new object[] { parameters }
: Array.Empty<object>();
var frameworkTarget = ToFrameworkAddress(target);
var flags = callType switch
{
CallType.Call => NeoFrameworkCallFlags.All,
CallType.DelegateCall => NeoFrameworkCallFlags.ReadStates | NeoFrameworkCallFlags.WriteStates,
CallType.StaticCall => NeoFrameworkCallFlags.ReadOnly,
_ => NeoFrameworkCallFlags.All,
};
_context.Msg.Sender = NeoTypeConversions.ToCoreUInt160(NeoFrameworkRuntime.ExecutingScriptHash);
_context.Msg.Value = value;
_context.Msg.Data = callData;
var result = callType switch
{
CallType.Call => NeoFrameworkContract.Call(frameworkTarget, methodName, flags, args),
CallType.DelegateCall => NeoFrameworkContract.Call(frameworkTarget, methodName, flags, args),
CallType.StaticCall => NeoFrameworkContract.Call(frameworkTarget, methodName, flags, args),
_ => throw new ArgumentException($"Unsupported call type: {callType}")
};
_callCount++;
var returnData = result != null ? SerializeResult(result) : Array.Empty<byte>();
return CallResult.Succeeded(returnData, EstimateGasUsed(callData.Length));
}
catch (Exception ex)
{
return CallResult.Failed($"Execution failed: {ex.Message}");
}
finally
{
_context.Msg.Sender = originalSender;
_context.Msg.Value = originalValue;
_context.Msg.Data = originalData;
}
}
private static Neo.SmartContract.Framework.UInt160 ToFrameworkAddress(UInt160 address)
=> (Neo.SmartContract.Framework.UInt160)NeoTypeConversions.ToByteArray(address);
private static bool IsContractDeployed(UInt160 address)
{
try
{
return NeoFrameworkContractManagement.GetContract(ToFrameworkAddress(address)) != null;
}
catch
{
return false;
}
}
private bool TransferGas(UInt160 from, UInt160 to, BigInteger amount)
{
if (amount <= 0 || to == UInt160.Zero)
{
return false;
}
try
{
return NeoFrameworkGas.Transfer(
ToFrameworkAddress(from),
ToFrameworkAddress(to),
amount,
null
);
}
catch
{
return false;
}
}
private string GetMethodNameFromSelector(byte[] selector)
{
_ = selector;
return "invoke";
}
private byte[] SerializeResult(object result)
{
if (result is byte[] bytes)
return bytes;
if (result is ByteString byteString)
return (byte[])byteString;
if (result is string str)
return Encoding.UTF8.GetBytes(str);
if (result is BigInteger bi)
return AbiEncoder.EncodeUint256(bi);
if (result is bool boolean)
return AbiEncoder.EncodeUint256(boolean ? 1 : 0);
return Encoding.UTF8.GetBytes(result.ToString() ?? string.Empty);
}
private uint EstimateGasUsed(int dataSize)
{
return (uint)(21000 + dataSize * 16); }
}
public enum CallType
{
Call, DelegateCall, StaticCall }
public sealed record CallResult
{
public bool Success { get; init; }
public byte[] ReturnData { get; init; } = Array.Empty<byte>();
public string Error { get; init; } = "";
public uint GasUsed { get; init; }
public static CallResult Succeeded(byte[] returnData, uint gasUsed = 0)
=> new() { Success = true, ReturnData = returnData, GasUsed = gasUsed };
public static CallResult Failed(string error, uint gasUsed = 0)
=> new() { Success = false, Error = error, GasUsed = gasUsed };
}
public sealed record CreateResult
{
public bool Success { get; init; }
public UInt160 Address { get; init; } = UInt160.Zero;
public byte[] ReturnData { get; init; } = Array.Empty<byte>();
public string Error { get; init; } = "";
public uint GasUsed { get; init; }
public static CreateResult Succeeded(UInt160 address, byte[] returnData, uint gasUsed = 0)
=> new() { Success = true, Address = address, ReturnData = returnData, GasUsed = gasUsed };
public static CreateResult Failed(string error, uint gasUsed = 0)
=> new() { Success = false, Error = error, GasUsed = gasUsed };
}