From 40cfe541444d6500d5315d1879d72c4fd19e4892 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Thu, 18 Mar 2021 14:15:58 +0100 Subject: [PATCH] make payments --- nitro/wallet/balances.nim | 42 +++++++++++++++++++ nitro/wallet/signedstate.nim | 3 +- nitro/wallet/wallet.nim | 79 +++++++++++++++++++++++++++++++++++- tests/nitro/testWallet.nim | 77 ++++++++++++++++++++++++++++++----- 4 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 nitro/wallet/balances.nim diff --git a/nitro/wallet/balances.nim b/nitro/wallet/balances.nim new file mode 100644 index 0000000..43603ec --- /dev/null +++ b/nitro/wallet/balances.nim @@ -0,0 +1,42 @@ +import std/tables +import std/sequtils +import ../basics +import ../protocol + +include questionable/errorban + +type + Balances* = OrderedTable[Destination, UInt256] + +func `balances`*(outcome: Outcome, asset: EthAddress): ?Balances = + for assetOutcome in seq[AssetOutcome](outcome): + if assetOutcome.assetHolder == asset: + if assetOutcome.kind == allocationType: + let allocation = assetOutcome.allocation + let items = seq[AllocationItem](allocation) + return items.toOrderedTable.some + Balances.none + +func `update`*(outcome: var Outcome, asset: EthAddress, table: Balances) = + for assetOutcome in seq[AssetOutcome](outcome).mitems: + if assetOutcome.assetHolder == asset: + if assetOutcome.kind == allocationType: + assetOutcome.allocation = Allocation(toSeq(table.pairs)) + +func move*(balances: var Balances, + source: Destination, + destination: Destination, + amount: UInt256): ?!void = + try: + if balances[source] < amount: + return void.failure "insufficient funds" + + balances[source] -= amount + if (balances.contains(destination)): + balances[destination] += amount + else: + balances[destination] = amount + + ok() + except KeyError: + void.failure "no funds" diff --git a/nitro/wallet/signedstate.nim b/nitro/wallet/signedstate.nim index 5baf0ed..ed776ff 100644 --- a/nitro/wallet/signedstate.nim +++ b/nitro/wallet/signedstate.nim @@ -6,7 +6,8 @@ include questionable/errorban type SignedState* = object state*: State - signatures*: seq[(EthAddress, Signature)] + signatures*: Signatures + Signatures* = seq[(EthAddress, Signature)] func hasParticipant*(signed: SignedState, participant: EthAddress): bool = signed.state.channel.participants.contains(participant) diff --git a/nitro/wallet/wallet.nim b/nitro/wallet/wallet.nim index 37e0b43..a41dacc 100644 --- a/nitro/wallet/wallet.nim +++ b/nitro/wallet/wallet.nim @@ -4,18 +4,23 @@ import ../keys import ../protocol import ./signedstate import ./ledger +import ./balances include questionable/errorban export basics export keys export signedstate +export balances type Wallet* = object key: PrivateKey channels: Table[ChannelId, SignedState] ChannelId* = Destination + Payment* = tuple + destination: Destination + amount: UInt256 func init*(_: type Wallet, key: PrivateKey): Wallet = result.key = key @@ -23,8 +28,8 @@ func init*(_: type Wallet, key: PrivateKey): Wallet = func address*(wallet: Wallet): EthAddress = wallet.key.toPublicKey.toAddress -func `[]`*(wallet: Wallet, channel: ChannelId): ?SignedState = - wallet.channels[channel].catch.option +func destination*(wallet: Wallet): Destination = + wallet.address.toDestination func sign(wallet: Wallet, state: SignedState): SignedState = var signed = state @@ -37,6 +42,11 @@ func createChannel(wallet: var Wallet, state: SignedState): ChannelId = wallet.channels[id] = signed id +func updateChannel(wallet: var Wallet, state: SignedState) = + let signed = wallet.sign(state) + let id = getChannelId(signed.state.channel) + wallet.channels[id] = signed + func openLedgerChannel*(wallet: var Wallet, hub: EthAddress, chainId: UInt256, @@ -54,3 +64,68 @@ func acceptChannel*(wallet: var Wallet, signed: SignedState): ?!ChannelId = return ChannelId.failure "incorrect signatures" wallet.createChannel(signed).success + +func state*(wallet: Wallet, channel: ChannelId): ?State = + try: + wallet.channels[channel].state.some + except KeyError: + State.none + +func signatures*(wallet: Wallet, channel: ChannelId): ?Signatures = + try: + wallet.channels[channel].signatures.some + except KeyError: + Signatures.none + +func signature*(wallet: Wallet, + channel: ChannelId, + address: EthAddress): ?Signature = + if signatures =? wallet.signatures(channel): + for (signer, signature) in signatures: + if signer == address: + return signature.some + Signature.none + +func balance*(wallet: Wallet, + channel: ChannelId, + asset: EthAddress, + destination: Destination): UInt256 = + if state =? wallet.state(channel): + if balances =? state.outcome.balances(asset): + try: + return balances[destination] + except KeyError: + return 0.u256 + 0.u256 + +func balance*(wallet: Wallet, + channel: ChannelId, + asset: EthAddress, + address: EthAddress): UInt256 = + wallet.balance(channel, asset, address.toDestination) + +func pay*(wallet: var Wallet, + channel: ChannelId, + asset: EthAddress, + receiver: Destination, + amount: UInt256): ?!void = + if var state =? wallet.state(channel): + if var balances =? state.outcome.balances(asset): + ?balances.move(wallet.destination, receiver, amount) + try: + state.outcome.update(asset, balances) + wallet.updateChannel(SignedState(state: state)) + ok() + except KeyError as error: + void.failure error + else: + void.failure "asset not found" + else: + void.failure "channel not found" + +func pay*(wallet: var Wallet, + channel: ChannelId, + asset: EthAddress, + receiver: EthAddress, + amount: UInt256): ?!void = + wallet.pay(channel, asset, receiver.toDestination, amount) diff --git a/tests/nitro/testWallet.nim b/tests/nitro/testWallet.nim index 721ccaa..fad378a 100644 --- a/tests/nitro/testWallet.nim +++ b/tests/nitro/testWallet.nim @@ -2,7 +2,7 @@ import ./basics suite "wallet": - test "wallet can be created from private key": + test "wallet is created from private key": let key = PrivateKey.random() let wallet = Wallet.init(key) check wallet.address == key.toPublicKey.toAddress @@ -24,24 +24,23 @@ suite "wallet: opening ledger channel": channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, amount) test "sets correct channel definition": - let definition = wallet[channel].get.state.channel + let definition = wallet.state(channel).get.channel check definition.chainId == chainId check definition.nonce == nonce check definition.participants == @[wallet.address, hub] test "provides correct outcome": - let outcome = wallet[channel].get.state.outcome - let destination = wallet.address.toDestination - check outcome == Outcome.init(asset, {destination: amount}) + let outcome = wallet.state(channel).get.outcome + check outcome == Outcome.init(asset, {wallet.destination: amount}) test "signs the state": - let state = wallet[channel].get.state - let signatures = wallet[channel].get.signatures + let state = wallet.state(channel).get + let signatures = wallet.signatures(channel).get check signatures == @{wallet.address: key.sign(state)} test "sets app definition and app data to zero": - check wallet[channel].get.state.appDefinition == EthAddress.zero - check wallet[channel].get.state.appData.len == 0 + check wallet.state(channel).get.appDefinition == EthAddress.zero + check wallet.state(channel).get.appData.len == 0 suite "wallet: accepting incoming channel": @@ -56,12 +55,12 @@ suite "wallet: accepting incoming channel": test "returns the new channel id": let channel = wallet.acceptChannel(signed).get - check wallet[channel].get.state == signed.state + check wallet.state(channel).get == signed.state test "signs the channel state": let channel = wallet.acceptChannel(signed).get let expectedSignatures = @{wallet.address: key.sign(signed.state)} - check wallet[channel].get.signatures == expectedSignatures + check wallet.signatures(channel).get == expectedSignatures test "fails when wallet address is not a participant": let wrongParticipants = seq[EthAddress].example @@ -75,3 +74,59 @@ suite "wallet: accepting incoming channel": signed.state.channel.participants &= @[otherWallet.address] signed.signatures = @{wrongAddress: otherKey.sign(signed.state)} check wallet.acceptChannel(signed).isErr + +suite "wallet: making payments": + + let key = PrivateKey.random() + let asset = EthAddress.example + let hub = EthAddress.example + let chainId = UInt256.example + let nonce = UInt48.example + + var wallet: Wallet + var channel: ChannelId + + test "paying updates the channel state": + wallet = Wallet.init(key) + let me = wallet.address + channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, 100.u256) + + check wallet.pay(channel, asset, hub, 1.u256).isOk + check wallet.balance(channel, asset, me) == 99.u256 + check wallet.balance(channel, asset, hub) == 1.u256 + + check wallet.pay(channel, asset, hub, 2.u256).isOk + check wallet.balance(channel, asset, me) == 97.u256 + check wallet.balance(channel, asset, hub) == 3.u256 + + test "paying updates signatures": + wallet = Wallet.init(key) + channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, 100.u256) + check wallet.pay(channel, asset, hub, 1.u256).isOk + let expectedSignature = key.sign(wallet.state(channel).get) + check wallet.signature(channel, wallet.address) == expectedSignature.some + + test "payment fails when channel not found": + wallet = Wallet.init(key) + check wallet.pay(channel, asset, hub, 1.u256).isErr + + test "payment fails when asset not found": + wallet = Wallet.init(key) + var state = State.example + state.channel.participants &= wallet.address + channel = wallet.acceptChannel(SignedState(state: state)).get + check wallet.pay(channel, asset, hub, 1.u256).isErr + + test "payment fails when payer has no allocation": + wallet = Wallet.init(key) + var state: State + state.channel = ChannelDefinition(participants: @[wallet.address]) + state.outcome = Outcome.init(asset, @[]) + channel = wallet.acceptChannel(SignedState(state: state)).get + check wallet.pay(channel, asset, hub, 1.u256).isErr + + test "payment fails when payer has insufficient funds": + wallet = Wallet.init(key) + channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, 1.u256) + check wallet.pay(channel, asset, hub, 1.u256).isOk + check wallet.pay(channel, asset, hub, 1.u256).isErr