Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/Neo/Extensions/SmartContract/GovernanceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (C) 2015-2026 The Neo Project.
//
// GovernanceExtensions.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Neo.Persistence;
using Neo.SmartContract;
using Neo.SmartContract.Native;
using System.Numerics;

namespace Neo.Extensions.SmartContract;

public static class GovernanceExtensions
{
public static IEnumerable<(UInt160 Address, BigInteger Balance)> GetAccounts(this Governance gasToken, IReadOnlyStore snapshot)
{
ArgumentNullException.ThrowIfNull(gasToken);

ArgumentNullException.ThrowIfNull(snapshot);

var kb = new KeyBuilder(TokenManagement.TokenId, TokenManagement.Prefix_AccountState)
.Add(gasToken.GasTokenId)
.ToArray();
var kbLength = kb.Length;

foreach (var (key, value) in snapshot.Find(kb, SeekDirection.Forward))
{
var keyBytes = key.ToArray();
var accountHash = new UInt160(keyBytes.AsSpan(kb.Length));
yield return new(accountHash, value.GetInteroperable<AccountState>().Balance);
}
}
}
11 changes: 6 additions & 5 deletions src/Neo/SmartContract/Native/TokenManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ namespace Neo.SmartContract.Native;
[ContractEvent(0, "Created", "assetId", ContractParameterType.Hash160, "type", ContractParameterType.Integer)]
public sealed partial class TokenManagement : NativeContract
{
const byte Prefix_TokenState = 10;
const byte Prefix_AccountState = 12;
internal const byte Prefix_TokenState = 10;
internal const byte Prefix_AccountState = 12;
internal const int TokenId = -12;

internal TokenManagement() : base(-12) { }
internal TokenManagement() : base(TokenId) { }

partial void Initialize_Fungible(ApplicationEngine engine, Hardfork? hardfork);
partial void Initialize_NonFungible(ApplicationEngine engine, Hardfork? hardfork);
Expand Down Expand Up @@ -63,7 +64,7 @@ public BigInteger BalanceOf(IReadOnlyStore snapshot, UInt160 assetId, UInt160 ac
StorageKey key = CreateStorageKey(Prefix_TokenState, assetId);
if (!snapshot.Contains(key))
throw new InvalidOperationException("The asset id does not exist.");
key = CreateStorageKey(Prefix_AccountState, account, assetId);
key = CreateStorageKey(Prefix_AccountState, assetId, account);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting "account" first is a more logical approach. This is because finding all assets in a wallet is a common operation, while finding all accounts for a specific token is a very rare operation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If its done this way than you can't lookup the storage key without knowing an account that has a balance of the asset. So without the account you wont find any assets for a given token. This is basic relational data/database techniques and standard data/database normalization

AccountState? accountState = snapshot.TryGet(key)?.GetInteroperable<AccountState>();
if (accountState is null) return BigInteger.Zero;
return accountState.Balance;
Expand Down Expand Up @@ -123,7 +124,7 @@ TokenState AddTotalSupply(ApplicationEngine engine, TokenType type, UInt160 asse
async ContractTask<bool> AddBalance(ApplicationEngine engine, UInt160 assetId, TokenState token, UInt160 account, BigInteger amount, bool callOnBalanceChanged)
{
if (amount.IsZero) return true;
StorageKey key = CreateStorageKey(Prefix_AccountState, account, assetId);
StorageKey key = CreateStorageKey(Prefix_AccountState, assetId, account);
AccountState? accountState = engine.SnapshotCache.GetAndChange(key)?.GetInteroperable<AccountState>();
BigInteger balanceOld = accountState?.Balance ?? BigInteger.Zero;
if (amount > 0)
Expand Down
39 changes: 39 additions & 0 deletions tests/Neo.UnitTests/Extensions/UT_GovernanceTextensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (C) 2015-2026 The Neo Project.
//
// UT_GovernanceTextensions.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Neo.Extensions.SmartContract;
using Neo.SmartContract.Native;

namespace Neo.UnitTests.Extensions;

[TestClass]
public class UT_GovernanceExtensions
{
private NeoSystem _system = null!;

[TestInitialize]
public void Setup()
{
_system = TestBlockchain.GetSystem();
}

[TestMethod]
public void TestGetAccounts()
{
UInt160 expected = "0x9f8f056a53e39585c7bb52886418c7bed83d126b";

var accounts = NativeContract.Governance.GetAccounts(_system.StoreView);
var (address, balance) = accounts.FirstOrDefault();

Assert.AreEqual(expected, address);
Assert.AreEqual(5200000000000000, balance);
}
}
14 changes: 7 additions & 7 deletions tests/Neo.UnitTests/Ledger/UT_Blockchain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public void TestValidTransaction()

// Fake balance - GAS token uses TokenManagement with Prefix_AccountState = 12
// First, create TokenState for GAS token (required by TokenManagement.BalanceOf)
var tokenStateKey = new KeyBuilder(NativeContract.TokenManagement.Id, 10).Add(NativeContract.Governance.GasTokenId);
var tokenStateKey = new KeyBuilder(TokenManagement.TokenId, TokenManagement.Prefix_TokenState).Add(NativeContract.Governance.GasTokenId);
if (!snapshot.Contains(tokenStateKey))
{
var tokenState = new TokenState
Expand All @@ -67,7 +67,7 @@ public void TestValidTransaction()
snapshot.Add(tokenStateKey, new StorageItem(tokenState));
}
// Then set account balance
var key = new KeyBuilder(NativeContract.TokenManagement.Id, 12).Add(acc.ScriptHash).Add(NativeContract.Governance.GasTokenId);
var key = new KeyBuilder(TokenManagement.TokenId, TokenManagement.Prefix_AccountState).Add(NativeContract.Governance.GasTokenId).Add(acc.ScriptHash);
var entry = snapshot.GetAndChange(key, () => new StorageItem(new AccountState()));
entry.GetInteroperable<AccountState>().Balance = 100_000_000 * Governance.GasTokenFactor;
snapshot.Commit();
Expand All @@ -92,7 +92,7 @@ public void TestInvalidTransaction()

// Fake balance - GAS token uses TokenManagement with Prefix_AccountState = 12
// First, create TokenState for GAS token (required by TokenManagement.BalanceOf)
var tokenStateKey = new KeyBuilder(NativeContract.TokenManagement.Id, 10).Add(NativeContract.Governance.GasTokenId);
var tokenStateKey = new KeyBuilder(TokenManagement.TokenId, TokenManagement.Prefix_TokenState).Add(NativeContract.Governance.GasTokenId);
if (!snapshot.Contains(tokenStateKey))
{
var tokenState = new TokenState
Expand All @@ -108,7 +108,7 @@ public void TestInvalidTransaction()
snapshot.Add(tokenStateKey, new StorageItem(tokenState));
}
// Then set account balance
var key = new KeyBuilder(NativeContract.TokenManagement.Id, 12).Add(acc.ScriptHash).Add(NativeContract.Governance.GasTokenId);
var key = new KeyBuilder(TokenManagement.TokenId, TokenManagement.Prefix_AccountState).Add(NativeContract.Governance.GasTokenId).Add(acc.ScriptHash);
var entry = snapshot.GetAndChange(key, () => new StorageItem(new AccountState()));
entry.GetInteroperable<AccountState>().Balance = 100_000_000 * Governance.GasTokenFactor;
snapshot.Commit();
Expand Down Expand Up @@ -148,7 +148,7 @@ public void TestMaliciousOnChainConflict()

// Fake balance for accounts A and B - GAS token uses TokenManagement with Prefix_AccountState = 12
// First, create TokenState for GAS token (required by TokenManagement.BalanceOf)
var tokenStateKey = new KeyBuilder(NativeContract.TokenManagement.Id, 10).Add(NativeContract.Governance.GasTokenId);
var tokenStateKey = new KeyBuilder(TokenManagement.TokenId, TokenManagement.Prefix_TokenState).Add(NativeContract.Governance.GasTokenId);
if (!snapshot.Contains(tokenStateKey))
{
var tokenState = new TokenState
Expand All @@ -164,12 +164,12 @@ public void TestMaliciousOnChainConflict()
snapshot.Add(tokenStateKey, new StorageItem(tokenState));
}
// Then set account balances
var key = new KeyBuilder(NativeContract.TokenManagement.Id, 12).Add(accA.ScriptHash).Add(NativeContract.Governance.GasTokenId);
var key = new KeyBuilder(TokenManagement.TokenId, TokenManagement.Prefix_AccountState).Add(NativeContract.Governance.GasTokenId).Add(accA.ScriptHash);
var entry = snapshot.GetAndChange(key, () => new StorageItem(new AccountState()));
entry.GetInteroperable<AccountState>().Balance = 100_000_000 * Governance.GasTokenFactor;
snapshot.Commit();

key = new KeyBuilder(NativeContract.TokenManagement.Id, 12).Add(accB.ScriptHash).Add(NativeContract.Governance.GasTokenId);
key = new KeyBuilder(TokenManagement.TokenId, TokenManagement.Prefix_AccountState).Add(NativeContract.Governance.GasTokenId).Add(accB.ScriptHash);
entry = snapshot.GetAndChange(key, () => new StorageItem(new AccountState()));
entry.GetInteroperable<AccountState>().Balance = 100_000_000 * Governance.GasTokenFactor;
snapshot.Commit();
Expand Down
Loading