Skip to content

Commit

Permalink
Merge pull request #900 from dahlia/assets
Browse files Browse the repository at this point in the history
Fungible asset states
  • Loading branch information
dahlia authored Jun 11, 2020
2 parents 02daf27 + bb9b3f6 commit 092f2a1
Show file tree
Hide file tree
Showing 24 changed files with 1,360 additions and 251 deletions.
20 changes: 20 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ To be released.

- Added `IAction.RenderError()` and `IAction.UnrenderError()` methods.
[[#860], [#875]]
- Added methods related fungible asset states to `IAccountStateDelta`:
[[#861], [#900]]
- `UpdatedFungibleAssetsAccounts` property
- `MintAsset(Address, Currency, BigInteger)` method
- `TransferAsset(Address, Address, Currency, BigInteger)` method
- `BurnAsset(Address, Currency, BigInteger)` method
- `GetBalance(Address, Currency)` method
- Added `IAccountStateDelta.StateUpdatedAddresses` property in order to
distinguish state updates from asset states. [[#861], [#900]]
- Added an optional parameter `AccountBalanceGetter accountBalanceGetter` to
`Block<T>.EvaluateActionsPerTx()` method. [[#861], [#900]]
- `BlockChain<T>.StageTransaction()` became to throw
`InvalidTxGenesisHashException` when it takes a `Transaction<T>` from
a heterogeneous `BlockChain<T>` with a different genesis block.
Expand All @@ -25,9 +36,14 @@ To be released.

### Added APIs

- Added `Currency` struct. [[#861], [#900]]
- Added `AccountBalanceGetter` delegate. [[#861], [#900]]
- Added `TurnClient.BindProxies()` method. [[#756], [#868]]
- Added `ActionEvaluation.Exception` property. [[#860], [[#875]]]
- Added `InvalidTxGenesisHashException` class. [[#796], [#878]]
- Added `CurrencyPermissionException` class. [[#861], [#900]]
- Added `InsufficientBalanceException` class. [[#861], [#900]]
- Added `BlockChain<T>.GetBalance()` method. [[#861], [#900]]

### Behavioral changes

Expand All @@ -39,6 +55,8 @@ To be released.
`UnexpectedlyTerminatedActionException` directly. Instead, it records
an exception to `ActionEvaluation`s. [[#860], [#875]]
- Added `Transaction<T>.GenesisHash` property. [[#796], [#878]]
- Added `IAccountStateDelta.UpdatedAddresses` property contains
asset updates besides state updates. [[#861], [#900]]
- `Swarm<T>` became to ignore received transaction with different
genesis hash. [[#796], [#878]]
- `Swarm<T>` became to ignore invalid `BlockHeader`s immediately. [[#898]]
Expand All @@ -58,6 +76,7 @@ To be released.
[#858]: https://github.com/planetarium/libplanet/issues/858
[#859]: https://github.com/planetarium/libplanet/pull/859
[#860]: https://github.com/planetarium/libplanet/issues/860
[#861]: https://github.com/planetarium/libplanet/issues/861
[#868]: https://github.com/planetarium/libplanet/pull/868
[#871]: https://github.com/planetarium/libplanet/issues/871
[#875]: https://github.com/planetarium/libplanet/pull/875
Expand All @@ -66,6 +85,7 @@ To be released.
[#883]: https://github.com/planetarium/libplanet/pull/883
[#890]: https://github.com/planetarium/libplanet/pull/890
[#898]: https://github.com/planetarium/libplanet/pull/898
[#900]: https://github.com/planetarium/libplanet/pull/900
[#902]: https://github.com/planetarium/libplanet/pull/902


Expand Down
36 changes: 36 additions & 0 deletions Libplanet.Tests/Action/AccountStateDeltaExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
using Bencodex.Types;
using Libplanet.Action;

namespace Libplanet.Tests.Action
{
public static class AccountStateDeltaExtensions
{
public static IImmutableDictionary<Address, IValue> GetUpdatedStates(
this IAccountStateDelta delta
)
{
return delta.StateUpdatedAddresses.Select(address =>
new KeyValuePair<Address, IValue>(
address,
delta.GetState(address)
)
).ToImmutableDictionary();
}

public static IImmutableDictionary<(Address, Currency), BigInteger> GetUpdatedBalances(
this IAccountStateDelta delta
) =>
delta.UpdatedFungibleAssets.SelectMany(kv =>
kv.Value.Select(currency =>
new KeyValuePair<(Address, Currency), BigInteger>(
(kv.Key, currency),
delta.GetBalance(kv.Key, currency)
)
)
).ToImmutableDictionary();
}
}
242 changes: 208 additions & 34 deletions Libplanet.Tests/Action/AccountStateDeltaImplTest.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
using Bencodex.Types;
using Libplanet.Action;
using Libplanet.Crypto;
using Xunit;
using Xunit.Abstractions;

namespace Libplanet.Tests.Action
{
public class AccountStateDeltaImplTest
{
private readonly Address[] _addr;
private readonly Currency[] _currencies;
private readonly IImmutableDictionary<Address, IValue> _states;
private readonly IImmutableDictionary<(Address, Currency), BigInteger> _assets;
private readonly IAccountStateDelta _init;

public AccountStateDeltaImplTest()
public AccountStateDeltaImplTest(ITestOutputHelper output)
{
Address Addr() => new PrivateKey().ToAddress();

Expand All @@ -23,70 +30,237 @@ public AccountStateDeltaImplTest()
Addr(),
};

_currencies = new[]
{
new Currency("FOO", minter: _addr[0]),
new Currency("BAR", minters: _addr.Take(2).ToImmutableHashSet()),
new Currency("BAZ", minter: null),
};

_states = new Dictionary<Address, IValue>
{
[_addr[0]] = (Text)"a",
[_addr[1]] = (Text)"b",
}.ToImmutableDictionary();

_assets = new Dictionary<(Address, Currency), BigInteger>
{
[(_addr[0], _currencies[0])] = 5,
[(_addr[0], _currencies[1])] = -10,
[(_addr[1], _currencies[1])] = 15,
[(_addr[1], _currencies[2])] = 20,
}.ToImmutableDictionary();

output.WriteLine("Fixtures {0,-42} FOO BAR BAZ State", "Address");
int i = 0;
foreach (Address a in _addr)
{
output.WriteLine(
"_addr[{0}] {1} {2,3} {3,3} {4,3} {5}",
i++,
a,
GetBalance(a, _currencies[0]),
GetBalance(a, _currencies[1]),
GetBalance(a, _currencies[2]),
GetState(a)
);
}

_init = new AccountStateDeltaImpl(GetState, GetBalance, _addr[0]);
}

[Fact]
public void CreateNullDelta()
public void NullDelta()
{
IAccountStateDelta delta = new AccountStateDeltaImpl(GetState);
Assert.Empty(delta.UpdatedAddresses);
Assert.Equal("a", (Text)delta.GetState(_addr[0]));
Assert.Equal("b", (Text)delta.GetState(_addr[1]));
Assert.Null(delta.GetState(_addr[2]));
Assert.Empty(_init.UpdatedAddresses);
Assert.Empty(_init.StateUpdatedAddresses);
Assert.Empty(_init.UpdatedFungibleAssets);
Assert.Equal("a", (Text)_init.GetState(_addr[0]));
Assert.Equal("b", (Text)_init.GetState(_addr[1]));
Assert.Null(_init.GetState(_addr[2]));
Assert.Equal(5, _init.GetBalance(_addr[0], _currencies[0]));
Assert.Equal(-10, _init.GetBalance(_addr[0], _currencies[1]));
Assert.Equal(0, _init.GetBalance(_addr[0], _currencies[2]));
Assert.Equal(0, _init.GetBalance(_addr[1], _currencies[0]));
Assert.Equal(15, _init.GetBalance(_addr[1], _currencies[1]));
Assert.Equal(20, _init.GetBalance(_addr[1], _currencies[2]));
Assert.Equal(0, _init.GetBalance(_addr[2], _currencies[0]));
Assert.Equal(0, _init.GetBalance(_addr[2], _currencies[1]));
Assert.Equal(0, _init.GetBalance(_addr[2], _currencies[2]));
}

[Fact]
public void GetSetState()
public void States()
{
IAccountStateDelta init = new AccountStateDeltaImpl(GetState);
IAccountStateDelta a = init.SetState(_addr[0], (Text)"A");
IAccountStateDelta a = _init.SetState(_addr[0], (Text)"A");
Assert.Equal("A", (Text)a.GetState(_addr[0]));
Assert.Equal("a", (Text)init.GetState(_addr[0]));
Assert.Equal("a", (Text)_init.GetState(_addr[0]));
Assert.Equal("b", (Text)a.GetState(_addr[1]));
Assert.Equal("b", (Text)init.GetState(_addr[1]));
Assert.Equal("b", (Text)_init.GetState(_addr[1]));
Assert.Null(a.GetState(_addr[2]));
Assert.Null(init.GetState(_addr[2]));
Assert.Equal(
new[] { _addr[0] }.ToImmutableHashSet(),
a.UpdatedAddresses
);
Assert.Empty(init.UpdatedAddresses);
Assert.Null(_init.GetState(_addr[2]));
Assert.Equal(new[] { _addr[0] }.ToImmutableHashSet(), a.StateUpdatedAddresses);
Assert.Equal(a.StateUpdatedAddresses, a.UpdatedAddresses);
Assert.Empty(a.UpdatedFungibleAssets);
Assert.Empty(_init.UpdatedAddresses);
Assert.Empty(_init.StateUpdatedAddresses);
Assert.Empty(_init.UpdatedFungibleAssets);

IAccountStateDelta b = a.SetState(_addr[0], (Text)"z");
Assert.Equal("z", (Text)b.GetState(_addr[0]));
Assert.Equal("A", (Text)a.GetState(_addr[0]));
Assert.Equal("a", (Text)init.GetState(_addr[0]));
Assert.Equal("a", (Text)_init.GetState(_addr[0]));
Assert.Equal("b", (Text)b.GetState(_addr[1]));
Assert.Equal("b", (Text)a.GetState(_addr[1]));
Assert.Null(b.GetState(_addr[2]));
Assert.Null(a.GetState(_addr[2]));
Assert.Equal(
new[] { _addr[0] }.ToImmutableHashSet(),
a.UpdatedAddresses
);
Assert.Empty(init.UpdatedAddresses);
Assert.Equal(new[] { _addr[0] }.ToImmutableHashSet(), a.StateUpdatedAddresses);
Assert.Equal(a.StateUpdatedAddresses, a.UpdatedAddresses);
Assert.Empty(_init.UpdatedAddresses);
Assert.Empty(_init.StateUpdatedAddresses);

IAccountStateDelta c = b.SetState(_addr[0], (Text)"a");
Assert.Equal("a", (Text)c.GetState(_addr[0]));
Assert.Equal("z", (Text)b.GetState(_addr[0]));
Assert.Empty(init.UpdatedAddresses);
Assert.Empty(_init.UpdatedAddresses);
Assert.Empty(_init.StateUpdatedAddresses);
}

[Fact]
public void FungibleAssets()
{
IAccountStateDelta a = _init.TransferAsset(_addr[1], _addr[2], _currencies[2], 5);
Assert.Equal(15, a.GetBalance(_addr[1], _currencies[2]));
Assert.Equal(5, a.GetBalance(_addr[2], _currencies[2]));
Assert.Equal(5, a.GetBalance(_addr[0], _currencies[0]));
Assert.Equal(-10, a.GetBalance(_addr[0], _currencies[1]));
Assert.Equal(0, a.GetBalance(_addr[0], _currencies[2]));
Assert.Equal(0, a.GetBalance(_addr[1], _currencies[0]));
Assert.Equal(15, a.GetBalance(_addr[1], _currencies[1]));
Assert.Equal(0, a.GetBalance(_addr[2], _currencies[0]));
Assert.Equal(0, a.GetBalance(_addr[2], _currencies[1]));
Assert.Equal(
new Dictionary<Address, IImmutableSet<Currency>>
{
[_addr[1]] = ImmutableHashSet.Create(_currencies[2]),
[_addr[2]] = ImmutableHashSet.Create(_currencies[2]),
}.ToImmutableDictionary(),
a.UpdatedFungibleAssets
);
Assert.Equal(a.UpdatedFungibleAssets.Keys.ToImmutableHashSet(), a.UpdatedAddresses);
Assert.Empty(a.StateUpdatedAddresses);
Assert.Empty(_init.UpdatedAddresses);
Assert.Empty(_init.StateUpdatedAddresses);
Assert.Empty(_init.UpdatedFungibleAssets);
}

private IValue GetState(Address address)
[Fact]
public void TransferAsset()
{
try
{
return _states[address];
}
catch (KeyNotFoundException)
{
return null;
}
Assert.Throws<ArgumentOutOfRangeException>(() =>
_init.TransferAsset(_addr[0], _addr[1], _currencies[0], 0)
);
Assert.Throws<ArgumentOutOfRangeException>(() =>
_init.TransferAsset(_addr[0], _addr[1], _currencies[0], -1)
);
Assert.Throws<InsufficientBalanceException>(() =>
_init.TransferAsset(_addr[0], _addr[1], _currencies[0], 6)
);

IAccountStateDelta a = _init.TransferAsset(
_addr[0],
_addr[1],
_currencies[0],
6,
allowNegativeBalance: true
);
Assert.Equal(-1, a.GetBalance(_addr[0], _currencies[0]));
Assert.Equal(6, a.GetBalance(_addr[1], _currencies[0]));
}

[Fact]
public void MintAsset()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
_init.MintAsset(_addr[0], _currencies[0], 0)
);
Assert.Throws<ArgumentOutOfRangeException>(() =>
_init.MintAsset(_addr[0], _currencies[0], -1)
);

IAccountStateDelta delta0 = _init;
// currencies[0] (FOO) allows only _addr[0] to mint
delta0 = delta0.MintAsset(_addr[0], _currencies[0], 10);
Assert.Equal(15, delta0.GetBalance(_addr[0], _currencies[0]));

// currencies[1] (BAR) allows _addr[0] & _addr[1] to mint
delta0 = delta0.MintAsset(_addr[1], _currencies[1], 10);
Assert.Equal(25, delta0.GetBalance(_addr[1], _currencies[1]));

// currencies[2] (BAZ) allows everyone to mint
delta0 = delta0.MintAsset(_addr[2], _currencies[2], 10);
Assert.Equal(10, delta0.GetBalance(_addr[2], _currencies[2]));

IAccountStateDelta delta1 = new AccountStateDeltaImpl(GetState, GetBalance, _addr[1]);
// currencies[0] (FOO) disallows _addr[1] to mint
Assert.Throws<CurrencyPermissionException>(() =>
delta1.MintAsset(_addr[1], _currencies[0], 10)
);

// currencies[1] (BAR) allows _addr[0] & _addr[1] to mint
delta1 = delta1.MintAsset(_addr[0], _currencies[1], 20);
Assert.Equal(10, delta1.GetBalance(_addr[0], _currencies[1]));

// currencies[2] (BAZ) allows everyone to mint
delta1 = delta1.MintAsset(_addr[2], _currencies[2], 10);
Assert.Equal(10, delta1.GetBalance(_addr[2], _currencies[2]));
}

[Fact]
public void BurnAsset()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
_init.BurnAsset(_addr[0], _currencies[0], 0)
);
Assert.Throws<ArgumentOutOfRangeException>(() =>
_init.BurnAsset(_addr[0], _currencies[0], -1)
);
Assert.Throws<InsufficientBalanceException>(() =>
_init.BurnAsset(_addr[0], _currencies[0], 6)
);

IAccountStateDelta delta0 = _init;
// currencies[0] (FOO) allows only _addr[0] to burn
delta0 = delta0.BurnAsset(_addr[0], _currencies[0], 4);
Assert.Equal(1, delta0.GetBalance(_addr[0], _currencies[0]));

// currencies[1] (BAR) allows _addr[0] & _addr[1] to burn
delta0 = delta0.BurnAsset(_addr[1], _currencies[1], 10);
Assert.Equal(5, delta0.GetBalance(_addr[1], _currencies[1]));

// currencies[2] (BAZ) allows everyone to burn
delta0 = delta0.BurnAsset(_addr[1], _currencies[2], 10);
Assert.Equal(10, delta0.GetBalance(_addr[1], _currencies[2]));

IAccountStateDelta delta1 = new AccountStateDeltaImpl(GetState, GetBalance, _addr[1]);
// currencies[0] (FOO) disallows _addr[1] to burn
Assert.Throws<CurrencyPermissionException>(() =>
delta1.BurnAsset(_addr[0], _currencies[0], 5)
);

// currencies[1] (BAR) allows _addr[0] & _addr[1] to burn
delta1 = delta1.BurnAsset(_addr[1], _currencies[1], 10);
Assert.Equal(5, delta1.GetBalance(_addr[1], _currencies[1]));

// currencies[2] (BAZ) allows everyone to burn
delta1 = delta1.BurnAsset(_addr[1], _currencies[2], 10);
Assert.Equal(10, delta1.GetBalance(_addr[1], _currencies[2]));
}

private IValue GetState(Address address) =>
_states.TryGetValue(address, out IValue v) ? v : null;

private BigInteger GetBalance(Address address, Currency currency) =>
_assets.TryGetValue((address, currency), out BigInteger balance) ? balance : 0;
}
}
Loading

0 comments on commit 092f2a1

Please sign in to comment.