Skip to content

Commit

Permalink
New command: contract storage update (#434)
Browse files Browse the repository at this point in the history
* move contract storage to contract storage get + new command contract storage update

* rename method with dotnet format

* change storage command description

---------

Co-authored-by: Mirella de Medeiros <[email protected]>
  • Loading branch information
meevee98 and Mirella de Medeiros authored Apr 2, 2024
1 parent 9cdfc5b commit e7eb3a5
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 70 deletions.
2 changes: 1 addition & 1 deletion src/bctklib/ContractParameterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ public ContractParameter ParseParameter(JToken? json)
};
}

internal ContractParameter ParseStringParameter(string value)
public ContractParameter ParseStringParameter(string value)
{
if (value.Length >= 1)
{
Expand Down
256 changes: 187 additions & 69 deletions src/neoxp/Commands/ContractCommand.Storage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,119 +11,237 @@

using McMaster.Extensions.CommandLineUtils;
using Neo;
using Neo.BlockchainToolkit;
using Neo.SmartContract.Manifest;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
using System.Numerics;
using System.Text;

namespace NeoExpress.Commands
{
partial class ContractCommand
{
[Command(Name = "storage", Description = "Display storage for specified contract")]
[Command(Name = "storage", Description = "Manage smart contracts storages")]
[Subcommand(
typeof(StorageGet),
typeof(StorageUpdateKeyValue))]
internal class Storage
{
readonly ExpressChainManagerFactory chainManagerFactory;

public Storage(ExpressChainManagerFactory chainManagerFactory)
[Command("get", Description = "Display storage for specified contract")]
public class StorageGet
{
this.chainManagerFactory = chainManagerFactory;
}
readonly ExpressChainManagerFactory chainManagerFactory;

[Argument(0, Description = "Contract name or invocation hash")]
[Required]
internal string Contract { get; init; } = string.Empty;
public StorageGet(ExpressChainManagerFactory chainManagerFactory)
{
this.chainManagerFactory = chainManagerFactory;
}

[Option(Description = "Path to neo-express data file")]
internal string Input { get; init; } = string.Empty;
[Argument(0, Description = "Contract name or invocation hash")]
[Required]
internal string Contract { get; init; } = string.Empty;

[Option(Description = "Output as JSON")]
internal bool Json { get; }
[Option(Description = "Path to neo-express data file")]
internal string Input { get; init; } = string.Empty;

internal async Task WriteStoragesAsync(IExpressNode expressNode, TextWriter writer, IReadOnlyList<(UInt160 hash, ContractManifest)> contracts)
{
if (Json)
{
using var jsonWriter = new JsonTextWriter(writer);

if (contracts.Count > 1)
await jsonWriter.WriteStartArrayAsync().ConfigureAwait(false);
[Option(Description = "Output as JSON")]
internal bool Json { get; }

for (int i = 0; i < contracts.Count; i++)
internal async Task WriteStoragesAsync(IExpressNode expressNode, TextWriter writer, IReadOnlyList<(UInt160 hash, ContractManifest)> contracts)
{
if (Json)
{
var storages = await expressNode.ListStoragesAsync(contracts[i].hash).ConfigureAwait(false);

await jsonWriter.WriteStartObjectAsync().ConfigureAwait(false);
using var jsonWriter = new JsonTextWriter(writer);

await jsonWriter.WritePropertyNameAsync("script-hash").ConfigureAwait(false);
await jsonWriter.WriteValueAsync(contracts[i].hash.ToString()).ConfigureAwait(false);
if (contracts.Count > 1)
await jsonWriter.WriteStartArrayAsync().ConfigureAwait(false);

await jsonWriter.WritePropertyNameAsync("storages").ConfigureAwait(false);
await jsonWriter.WriteStartArrayAsync().ConfigureAwait(false);
for (int j = 0; j < storages.Count; j++)
for (int i = 0; i < contracts.Count; i++)
{
var storages = await expressNode.ListStoragesAsync(contracts[i].hash).ConfigureAwait(false);

await jsonWriter.WriteStartObjectAsync().ConfigureAwait(false);
await jsonWriter.WritePropertyNameAsync("key").ConfigureAwait(false);
await jsonWriter.WriteValueAsync($"0x{storages[j].key}").ConfigureAwait(false);
await jsonWriter.WritePropertyNameAsync("value").ConfigureAwait(false);
await jsonWriter.WriteValueAsync($"0x{storages[j].value}").ConfigureAwait(false);

await jsonWriter.WritePropertyNameAsync("script-hash").ConfigureAwait(false);
await jsonWriter.WriteValueAsync(contracts[i].hash.ToString()).ConfigureAwait(false);

await jsonWriter.WritePropertyNameAsync("storages").ConfigureAwait(false);
await jsonWriter.WriteStartArrayAsync().ConfigureAwait(false);
for (int j = 0; j < storages.Count; j++)
{
await jsonWriter.WriteStartObjectAsync().ConfigureAwait(false);
await jsonWriter.WritePropertyNameAsync("key").ConfigureAwait(false);
await jsonWriter.WriteValueAsync($"0x{storages[j].key}").ConfigureAwait(false);
await jsonWriter.WritePropertyNameAsync("value").ConfigureAwait(false);
await jsonWriter.WriteValueAsync($"0x{storages[j].value}").ConfigureAwait(false);
await jsonWriter.WriteEndObjectAsync().ConfigureAwait(false);
}
await jsonWriter.WriteEndArrayAsync().ConfigureAwait(false);
await jsonWriter.WriteEndObjectAsync().ConfigureAwait(false);
}
await jsonWriter.WriteEndArrayAsync().ConfigureAwait(false);
await jsonWriter.WriteEndObjectAsync().ConfigureAwait(false);
}

if (contracts.Count > 1)
await jsonWriter.WriteEndArrayAsync().ConfigureAwait(false);
}
else
{
if (contracts.Count == 0)
{
await writer.WriteLineAsync($"No contracts found matching the name {Contract}").ConfigureAwait(false);
if (contracts.Count > 1)
await jsonWriter.WriteEndArrayAsync().ConfigureAwait(false);
}
else
{
for (int i = 0; i < contracts.Count; i++)
if (contracts.Count == 0)
{
var storages = await expressNode.ListStoragesAsync(contracts[i].hash).ConfigureAwait(false);
await writer.WriteLineAsync($"contract: {contracts[i].hash}").ConfigureAwait(false);
for (int j = 0; j < storages.Count; j++)
await writer.WriteLineAsync($"No contracts found matching the name {Contract}").ConfigureAwait(false);
}
else
{
for (int i = 0; i < contracts.Count; i++)
{
await writer.WriteLineAsync($" key: 0x{storages[j].key}").ConfigureAwait(false);
await writer.WriteLineAsync($" value: 0x{storages[j].value}").ConfigureAwait(false);
var storages = await expressNode.ListStoragesAsync(contracts[i].hash).ConfigureAwait(false);
await writer.WriteLineAsync($"contract: {contracts[i].hash}").ConfigureAwait(false);
for (int j = 0; j < storages.Count; j++)
{
await writer.WriteLineAsync($" key: 0x{storages[j].key}").ConfigureAwait(false);
await writer.WriteLineAsync($" value: 0x{storages[j].value}").ConfigureAwait(false);
}
}
}
}
}
}

internal async Task ExecuteAsync(TextWriter writer)
{
var (chainManager, _) = chainManagerFactory.LoadChain(Input);
var expressNode = chainManager.GetExpressNode();

if (UInt160.TryParse(Contract, out var hash))
internal async Task ExecuteAsync(TextWriter writer)
{
await WriteStoragesAsync(expressNode, writer, new (UInt160, ContractManifest)[] { (hash, null!) }).ConfigureAwait(false);
var (chainManager, _) = chainManagerFactory.LoadChain(Input);
var expressNode = chainManager.GetExpressNode();

if (UInt160.TryParse(Contract, out var hash))
{
await WriteStoragesAsync(expressNode, writer, new (UInt160, ContractManifest)[] { (hash, null!) }).ConfigureAwait(false);
}
else
{
var contracts = await expressNode.ListContractsAsync(Contract).ConfigureAwait(false);
await WriteStoragesAsync(expressNode, writer, contracts).ConfigureAwait(false);
}
}
else

internal async Task<int> OnExecuteAsync(CommandLineApplication app, IConsole console)
{
var contracts = await expressNode.ListContractsAsync(Contract).ConfigureAwait(false);
await WriteStoragesAsync(expressNode, writer, contracts).ConfigureAwait(false);
try
{
await ExecuteAsync(console.Out).ConfigureAwait(false);
return 0;
}
catch (Exception ex)
{
app.WriteException(ex);
return 1;
}
}
}

internal async Task<int> OnExecuteAsync(CommandLineApplication app, IConsole console)
[Command("update", Description = "Update the storage of a contract given the new key-value pair")]
public class StorageUpdateKeyValue
{
try
readonly ExpressChainManagerFactory chainManagerFactory;

public StorageUpdateKeyValue(ExpressChainManagerFactory chainManagerFactory)
{
this.chainManagerFactory = chainManagerFactory;
}

[Argument(0, Description = "Contract name or invocation hash")]
[Required]
internal string Contract { get; init; } = string.Empty;

[Argument(1, Description = "Storage key to update")]
[Required]
internal string Key { get; init; } = string.Empty;

[Argument(2, Description = "Storage value to update")]
[Required]
internal string Value { get; init; } = string.Empty;

[Option(Description = "Path to neo-express data file")]
internal string Input { get; init; } = string.Empty;

internal static async Task ExecuteAsync(ExpressChainManager chainManager, string contract, string key, string value, TextWriter writer)
{
await ExecuteAsync(console.Out).ConfigureAwait(false);
return 0;
var expressNode = chainManager.GetExpressNode();
ContractParameterParser parser = await expressNode.GetContractParameterParserAsync(chainManager.Chain).ConfigureAwait(false);
var scriptHash = parser.TryLoadScriptHash(contract, out var hash)
? hash
: UInt160.TryParse(contract, out var uint160)
? uint160
: throw new InvalidOperationException($"contract \"{contract}\" not found");

var internalPair = (
ConvertArg(parser, key),
ConvertArg(parser, value)
);
await expressNode.PersistStorageKeyValueAsync(scriptHash, internalPair);

static string ConvertArg(ContractParameterParser parser, string arg)
{
var parameter = parser.ParseStringParameter(arg);
var paramValue = parameter.Value;

byte[] result;
if (paramValue is string strValue)
{
try
{
var fromBase64 = Convert.FromBase64String(strValue);
return strValue;
}
catch
{
if (BigInteger.TryParse(strValue, out var integerValue))
{
result = integerValue.ToByteArray();
}
else if (bool.TryParse(strValue, out var boolValue))
{
result = new byte[] { Convert.ToByte(boolValue) };
}
else
{
result = Encoding.ASCII.GetBytes(strValue);
}
}
}
else if (paramValue is byte[] value)
{
result = value;
}
else if (paramValue is UInt160 hashValue)
{
result = ((byte[])parser.ParseStringParameter(hashValue.ToString()).Value).Reverse().ToArray();
}
else
{
result = Encoding.ASCII.GetBytes(parameter.ToString());
}
return Convert.ToBase64String(result);
}
}
catch (Exception ex)

internal async Task<int> OnExecuteAsync(CommandLineApplication app, IConsole console)
{
app.WriteException(ex);
return 1;
try
{
var (chainManager, _) = chainManagerFactory.LoadChain(Input);

if (chainManager.Chain.ConsensusNodes.Count != 1)
{
throw new ArgumentException("Contract storage manipulation is only supported for single-node consensus");
}

await ExecuteAsync(chainManager, Contract, Key, Value, console.Out).ConfigureAwait(false);
return 0;
}
catch (Exception ex)
{
app.WriteException(ex);
return 1;
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/neoxp/IExpressNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ enum CheckpointMode { Online, Offline }
Task<IReadOnlyList<TokenContract>> ListTokenContractsAsync();

Task<int> PersistContractAsync(ContractState state, IReadOnlyList<(string key, string value)> storagePairs, ContractCommand.OverwriteForce force);
Task<int> PersistStorageKeyValueAsync(UInt160 scripthash, (string key, string value) storagePair);
IAsyncEnumerable<(uint blockIndex, NotificationRecord notification)> EnumerateNotificationsAsync(IReadOnlySet<UInt160>? contractFilter, IReadOnlySet<string>? eventFilter);

Task<bool> IsNep17CompliantAsync(UInt160 contractHash);
Expand Down
22 changes: 22 additions & 0 deletions src/neoxp/Node/Modules/ExpressRpcServerPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,28 @@ public JToken ExpressPersistContract(JArray @params)
return NodeUtility.PersistContract(neoSystem, state, storagePairs, force);
}

[RpcMethod]
public JToken ExpressPersistStorage(JArray @params)
{
if (neoSystem is null)
throw new NullReferenceException(nameof(neoSystem));
var state = RpcClient.ContractStateFromJson((JObject)@params[0]!["state"]!);
var storagePairs = ((JArray)@params[0]!["storage"]!)
.Select(s => (
s!["key"]!.AsString(),
s!["value"]!.AsString())
).ToArray();

var force = Enum.Parse<ContractCommand.OverwriteForce>(@params[0]!["force"]!.AsString());

JToken result = 0;
foreach (var pair in storagePairs)
{
result = NodeUtility.PersistStorageKeyValuePair(neoSystem, state, pair, force);
}
return result;
}

[RpcMethod]
public JToken ExpressIsNep11Compliant(JToken @param)
{
Expand Down
Loading

0 comments on commit e7eb3a5

Please sign in to comment.