Skip to content

Commit

Permalink
Add RpcWallet (neo-project#44)
Browse files Browse the repository at this point in the history
* Add RpcWallet

* Add `importprivkey`

* Add `from` parameter to `sendmany`

* Add `getunclaimedgas`

* Add `claimgas`

* Add reference: Neo 2.10.0

* Fix `importprivkey`
  • Loading branch information
erikzhang authored and 陈志同 committed Oct 13, 2020
1 parent 7b67430 commit 18fb530
Show file tree
Hide file tree
Showing 3 changed files with 384 additions and 0 deletions.
365 changes: 365 additions & 0 deletions RpcWallet/RpcWallet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
using Akka.Actor;
using Microsoft.AspNetCore.Http;
using Neo.IO;
using Neo.IO.Json;
using Neo.Ledger;
using Neo.Network.P2P;
using Neo.Network.P2P.Payloads;
using Neo.Network.RPC;
using Neo.Persistence;
using Neo.SmartContract;
using Neo.Wallets;
using Neo.Wallets.NEP6;
using System.Collections.Generic;
using System.Linq;

namespace Neo.Plugins
{
public class RpcWallet : Plugin, IRpcPlugin
{
private Wallet Wallet => System.RpcServer.Wallet;

public override void Configure()
{
}

public void PreProcess(HttpContext context, string method, JArray _params)
{
}

public JObject OnProcess(HttpContext context, string method, JArray _params)
{
switch (method)
{
case "claimgas":
{
UInt160 to = _params.Count >= 1 ? _params[0].AsString().ToScriptHash() : null;
return ClaimGas(to);
}
case "dumpprivkey":
{
UInt160 scriptHash = _params[0].AsString().ToScriptHash();
return DumpPrivKey(scriptHash);
}
case "getbalance":
{
UIntBase asset_id = UIntBase.Parse(_params[0].AsString());
return GetBalance(asset_id);
}
case "getnewaddress":
{
return GetNewAddress();
}
case "getunclaimedgas":
{
return GetUnclaimedGas();
}
case "getwalletheight":
{
return GetWalletHeight();
}
case "importprivkey":
{
string privkey = _params[0].AsString();
return ImportPrivKey(privkey);
}
case "listaddress":
{
return ListAddress();
}
case "sendfrom":
{
UIntBase assetId = UIntBase.Parse(_params[0].AsString());
UInt160 from = _params[1].AsString().ToScriptHash();
UInt160 to = _params[2].AsString().ToScriptHash();
string value = _params[3].AsString();
Fixed8 fee = _params.Count >= 5 ? Fixed8.Parse(_params[4].AsString()) : Fixed8.Zero;
UInt160 change_address = _params.Count >= 6 ? _params[5].AsString().ToScriptHash() : null;
return SendFrom(assetId, from, to, value, fee, change_address);
}
case "sendmany":
{
int to_start = 0;
UInt160 from = null;
if (_params[0] is JString)
{
from = _params[0].AsString().ToScriptHash();
to_start = 1;
}
JArray to = (JArray)_params[to_start + 0];
Fixed8 fee = _params.Count >= to_start + 2 ? Fixed8.Parse(_params[to_start + 1].AsString()) : Fixed8.Zero;
UInt160 change_address = _params.Count >= to_start + 3 ? _params[to_start + 2].AsString().ToScriptHash() : null;
return SendMany(from, to, fee, change_address);
}
case "sendtoaddress":
{
UIntBase assetId = UIntBase.Parse(_params[0].AsString());
UInt160 scriptHash = _params[1].AsString().ToScriptHash();
string value = _params[2].AsString();
Fixed8 fee = _params.Count >= 4 ? Fixed8.Parse(_params[3].AsString()) : Fixed8.Zero;
UInt160 change_address = _params.Count >= 5 ? _params[4].AsString().ToScriptHash() : null;
return SendToAddress(assetId, scriptHash, value, fee, change_address);
}
default:
return null;
}
}

public void PostProcess(HttpContext context, string method, JArray _params, JObject result)
{
switch (method)
{
case "invoke":
case "invokefunction":
case "invokescript":
ProcessInvoke(result);
break;
}
}

private void ProcessInvoke(JObject result)
{
if (Wallet != null)
{
InvocationTransaction tx = new InvocationTransaction
{
Version = 1,
Script = result["script"].AsString().HexToBytes(),
Gas = Fixed8.Parse(result["gas_consumed"].AsString())
};
tx.Gas -= Fixed8.FromDecimal(10);
if (tx.Gas < Fixed8.Zero) tx.Gas = Fixed8.Zero;
tx.Gas = tx.Gas.Ceiling();
tx = Wallet.MakeTransaction(tx);
if (tx != null)
{
ContractParametersContext context = new ContractParametersContext(tx);
Wallet.Sign(context);
if (context.Completed)
tx.Witnesses = context.GetWitnesses();
else
tx = null;
}
result["tx"] = tx?.ToArray().ToHexString();
}
}

private void CheckWallet()
{
if (Wallet is null)
throw new RpcException(-400, "Access denied");
}

private JObject SignAndRelay(Transaction tx)
{
ContractParametersContext context = new ContractParametersContext(tx);
Wallet.Sign(context);
if (context.Completed)
{
tx.Witnesses = context.GetWitnesses();
Wallet.ApplyTransaction(tx);
System.LocalNode.Tell(new LocalNode.Relay { Inventory = tx });
return tx.ToJson();
}
else
{
return context.ToJson();
}
}

private JObject ClaimGas(UInt160 to)
{
CheckWallet();
const int MAX_CLAIMS_AMOUNT = 50;
CoinReference[] claims = Wallet.GetUnclaimedCoins().Select(p => p.Reference).ToArray();
if (claims.Length == 0)
throw new RpcException(-300, "No gas to claim");
ClaimTransaction tx;
using (Snapshot snapshot = Blockchain.Singleton.GetSnapshot())
{
tx = new ClaimTransaction
{
Claims = claims.Take(MAX_CLAIMS_AMOUNT).ToArray(),
Attributes = new TransactionAttribute[0],
Inputs = new CoinReference[0],
Outputs = new[]
{
new TransactionOutput
{
AssetId = Blockchain.UtilityToken.Hash,
Value = snapshot.CalculateBonus(claims.Take(MAX_CLAIMS_AMOUNT)),
ScriptHash = to ?? Wallet.GetChangeAddress()
}
}

};
}
return SignAndRelay(tx);
}

private JObject DumpPrivKey(UInt160 scriptHash)
{
CheckWallet();
WalletAccount account = Wallet.GetAccount(scriptHash);
return account.GetKey().Export();
}

private JObject GetBalance(UIntBase asset_id)
{
CheckWallet();
JObject json = new JObject();
switch (asset_id)
{
case UInt160 asset_id_160: //NEP-5 balance
json["balance"] = Wallet.GetAvailable(asset_id_160).ToString();
break;
case UInt256 asset_id_256: //Global Assets balance
IEnumerable<Coin> coins = Wallet.GetCoins().Where(p => !p.State.HasFlag(CoinState.Spent) && p.Output.AssetId.Equals(asset_id_256));
json["balance"] = coins.Sum(p => p.Output.Value).ToString();
json["confirmed"] = coins.Where(p => p.State.HasFlag(CoinState.Confirmed)).Sum(p => p.Output.Value).ToString();
break;
}
return json;
}

private JObject GetNewAddress()
{
CheckWallet();
WalletAccount account = Wallet.CreateAccount();
if (Wallet is NEP6Wallet nep6)
nep6.Save();
return account.Address;
}

private JObject GetUnclaimedGas()
{
CheckWallet();
using (Snapshot snapshot = Blockchain.Singleton.GetSnapshot())
{
uint height = snapshot.Height + 1;
Fixed8 unavailable;
try
{
unavailable = snapshot.CalculateBonus(Wallet.FindUnspentCoins().Where(p => p.Output.AssetId.Equals(Blockchain.GoverningToken.Hash)).Select(p => p.Reference), height);
}
catch
{
unavailable = Fixed8.Zero;
}
return new JObject
{
["available"] = snapshot.CalculateBonus(Wallet.GetUnclaimedCoins().Select(p => p.Reference)).ToString(),
["unavailable"] = unavailable.ToString()
};
}
}

private JObject GetWalletHeight()
{
CheckWallet();
return (Wallet.WalletHeight > 0) ? Wallet.WalletHeight - 1 : 0;
}

private JObject ImportPrivKey(string privkey)
{
CheckWallet();
WalletAccount account = Wallet.Import(privkey);
if (Wallet is NEP6Wallet nep6wallet)
nep6wallet.Save();
return new JObject
{
["address"] = account.Address,
["haskey"] = account.HasKey,
["label"] = account.Label,
["watchonly"] = account.WatchOnly
};
}

private JObject ListAddress()
{
CheckWallet();
return Wallet.GetAccounts().Select(p =>
{
JObject account = new JObject();
account["address"] = p.Address;
account["haskey"] = p.HasKey;
account["label"] = p.Label;
account["watchonly"] = p.WatchOnly;
return account;
}).ToArray();
}

private JObject SendFrom(UIntBase assetId, UInt160 from, UInt160 to, string value, Fixed8 fee, UInt160 change_address)
{
CheckWallet();
AssetDescriptor descriptor = new AssetDescriptor(assetId);
BigDecimal amount = BigDecimal.Parse(value, descriptor.Decimals);
if (amount.Sign <= 0)
throw new RpcException(-32602, "Invalid params");
if (fee < Fixed8.Zero)
throw new RpcException(-32602, "Invalid params");
Transaction tx = Wallet.MakeTransaction(null, new[]
{
new TransferOutput
{
AssetId = assetId,
Value = amount,
ScriptHash = to
}
}, from: from, change_address: change_address, fee: fee);
if (tx == null)
throw new RpcException(-300, "Insufficient funds");
return SignAndRelay(tx);
}

private JObject SendMany(UInt160 from, JArray to, Fixed8 fee, UInt160 change_address)
{
CheckWallet();
if (to.Count == 0)
throw new RpcException(-32602, "Invalid params");
TransferOutput[] outputs = new TransferOutput[to.Count];
for (int i = 0; i < to.Count; i++)
{
UIntBase asset_id = UIntBase.Parse(to[i]["asset"].AsString());
AssetDescriptor descriptor = new AssetDescriptor(asset_id);
outputs[i] = new TransferOutput
{
AssetId = asset_id,
Value = BigDecimal.Parse(to[i]["value"].AsString(), descriptor.Decimals),
ScriptHash = to[i]["address"].AsString().ToScriptHash()
};
if (outputs[i].Value.Sign <= 0)
throw new RpcException(-32602, "Invalid params");
}
if (fee < Fixed8.Zero)
throw new RpcException(-32602, "Invalid params");
Transaction tx = Wallet.MakeTransaction(null, outputs, from: from, change_address: change_address, fee: fee);
if (tx == null)
throw new RpcException(-300, "Insufficient funds");
return SignAndRelay(tx);
}

private JObject SendToAddress(UIntBase assetId, UInt160 scriptHash, string value, Fixed8 fee, UInt160 change_address)
{
CheckWallet();
AssetDescriptor descriptor = new AssetDescriptor(assetId);
BigDecimal amount = BigDecimal.Parse(value, descriptor.Decimals);
if (amount.Sign <= 0)
throw new RpcException(-32602, "Invalid params");
if (fee < Fixed8.Zero)
throw new RpcException(-32602, "Invalid params");
Transaction tx = Wallet.MakeTransaction(null, new[]
{
new TransferOutput
{
AssetId = assetId,
Value = amount,
ScriptHash = scriptHash
}
}, change_address: change_address, fee: fee);
if (tx == null)
throw new RpcException(-300, "Insufficient funds");
return SignAndRelay(tx);
}
}
}
13 changes: 13 additions & 0 deletions RpcWallet/RpcWallet.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Version>2.10.0</Version>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Neo.Plugins</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Neo" Version="2.10.0" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions neo-plugins.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImportBlocks", "ImportBlock
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimplePolicy.UnitTests", "SimplePolicy.UnitTests\SimplePolicy.UnitTests.csproj", "{0DA81327-DC81-4960-91C8-4C5F8B9B804C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RpcWallet", "RpcWallet\RpcWallet.csproj", "{EF32A7E5-EDF9-438C-8041-8DA6E675A7FD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -45,6 +47,10 @@ Global
{0DA81327-DC81-4960-91C8-4C5F8B9B804C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DA81327-DC81-4960-91C8-4C5F8B9B804C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DA81327-DC81-4960-91C8-4C5F8B9B804C}.Release|Any CPU.Build.0 = Release|Any CPU
{EF32A7E5-EDF9-438C-8041-8DA6E675A7FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF32A7E5-EDF9-438C-8041-8DA6E675A7FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF32A7E5-EDF9-438C-8041-8DA6E675A7FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF32A7E5-EDF9-438C-8041-8DA6E675A7FD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down

0 comments on commit 18fb530

Please sign in to comment.