mirror of
https://github.com/logos-storage/nim-nitro.git
synced 2026-01-04 06:33:12 +00:00
make payments
This commit is contained in:
parent
e173575dd0
commit
40cfe54144
42
nitro/wallet/balances.nim
Normal file
42
nitro/wallet/balances.nim
Normal file
@ -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"
|
||||||
@ -6,7 +6,8 @@ include questionable/errorban
|
|||||||
type
|
type
|
||||||
SignedState* = object
|
SignedState* = object
|
||||||
state*: State
|
state*: State
|
||||||
signatures*: seq[(EthAddress, Signature)]
|
signatures*: Signatures
|
||||||
|
Signatures* = seq[(EthAddress, Signature)]
|
||||||
|
|
||||||
func hasParticipant*(signed: SignedState, participant: EthAddress): bool =
|
func hasParticipant*(signed: SignedState, participant: EthAddress): bool =
|
||||||
signed.state.channel.participants.contains(participant)
|
signed.state.channel.participants.contains(participant)
|
||||||
|
|||||||
@ -4,18 +4,23 @@ import ../keys
|
|||||||
import ../protocol
|
import ../protocol
|
||||||
import ./signedstate
|
import ./signedstate
|
||||||
import ./ledger
|
import ./ledger
|
||||||
|
import ./balances
|
||||||
|
|
||||||
include questionable/errorban
|
include questionable/errorban
|
||||||
|
|
||||||
export basics
|
export basics
|
||||||
export keys
|
export keys
|
||||||
export signedstate
|
export signedstate
|
||||||
|
export balances
|
||||||
|
|
||||||
type
|
type
|
||||||
Wallet* = object
|
Wallet* = object
|
||||||
key: PrivateKey
|
key: PrivateKey
|
||||||
channels: Table[ChannelId, SignedState]
|
channels: Table[ChannelId, SignedState]
|
||||||
ChannelId* = Destination
|
ChannelId* = Destination
|
||||||
|
Payment* = tuple
|
||||||
|
destination: Destination
|
||||||
|
amount: UInt256
|
||||||
|
|
||||||
func init*(_: type Wallet, key: PrivateKey): Wallet =
|
func init*(_: type Wallet, key: PrivateKey): Wallet =
|
||||||
result.key = key
|
result.key = key
|
||||||
@ -23,8 +28,8 @@ func init*(_: type Wallet, key: PrivateKey): Wallet =
|
|||||||
func address*(wallet: Wallet): EthAddress =
|
func address*(wallet: Wallet): EthAddress =
|
||||||
wallet.key.toPublicKey.toAddress
|
wallet.key.toPublicKey.toAddress
|
||||||
|
|
||||||
func `[]`*(wallet: Wallet, channel: ChannelId): ?SignedState =
|
func destination*(wallet: Wallet): Destination =
|
||||||
wallet.channels[channel].catch.option
|
wallet.address.toDestination
|
||||||
|
|
||||||
func sign(wallet: Wallet, state: SignedState): SignedState =
|
func sign(wallet: Wallet, state: SignedState): SignedState =
|
||||||
var signed = state
|
var signed = state
|
||||||
@ -37,6 +42,11 @@ func createChannel(wallet: var Wallet, state: SignedState): ChannelId =
|
|||||||
wallet.channels[id] = signed
|
wallet.channels[id] = signed
|
||||||
id
|
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,
|
func openLedgerChannel*(wallet: var Wallet,
|
||||||
hub: EthAddress,
|
hub: EthAddress,
|
||||||
chainId: UInt256,
|
chainId: UInt256,
|
||||||
@ -54,3 +64,68 @@ func acceptChannel*(wallet: var Wallet, signed: SignedState): ?!ChannelId =
|
|||||||
return ChannelId.failure "incorrect signatures"
|
return ChannelId.failure "incorrect signatures"
|
||||||
|
|
||||||
wallet.createChannel(signed).success
|
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)
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import ./basics
|
|||||||
|
|
||||||
suite "wallet":
|
suite "wallet":
|
||||||
|
|
||||||
test "wallet can be created from private key":
|
test "wallet is created from private key":
|
||||||
let key = PrivateKey.random()
|
let key = PrivateKey.random()
|
||||||
let wallet = Wallet.init(key)
|
let wallet = Wallet.init(key)
|
||||||
check wallet.address == key.toPublicKey.toAddress
|
check wallet.address == key.toPublicKey.toAddress
|
||||||
@ -24,24 +24,23 @@ suite "wallet: opening ledger channel":
|
|||||||
channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, amount)
|
channel = wallet.openLedgerChannel(hub, chainId, nonce, asset, amount)
|
||||||
|
|
||||||
test "sets correct channel definition":
|
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.chainId == chainId
|
||||||
check definition.nonce == nonce
|
check definition.nonce == nonce
|
||||||
check definition.participants == @[wallet.address, hub]
|
check definition.participants == @[wallet.address, hub]
|
||||||
|
|
||||||
test "provides correct outcome":
|
test "provides correct outcome":
|
||||||
let outcome = wallet[channel].get.state.outcome
|
let outcome = wallet.state(channel).get.outcome
|
||||||
let destination = wallet.address.toDestination
|
check outcome == Outcome.init(asset, {wallet.destination: amount})
|
||||||
check outcome == Outcome.init(asset, {destination: amount})
|
|
||||||
|
|
||||||
test "signs the state":
|
test "signs the state":
|
||||||
let state = wallet[channel].get.state
|
let state = wallet.state(channel).get
|
||||||
let signatures = wallet[channel].get.signatures
|
let signatures = wallet.signatures(channel).get
|
||||||
check signatures == @{wallet.address: key.sign(state)}
|
check signatures == @{wallet.address: key.sign(state)}
|
||||||
|
|
||||||
test "sets app definition and app data to zero":
|
test "sets app definition and app data to zero":
|
||||||
check wallet[channel].get.state.appDefinition == EthAddress.zero
|
check wallet.state(channel).get.appDefinition == EthAddress.zero
|
||||||
check wallet[channel].get.state.appData.len == 0
|
check wallet.state(channel).get.appData.len == 0
|
||||||
|
|
||||||
suite "wallet: accepting incoming channel":
|
suite "wallet: accepting incoming channel":
|
||||||
|
|
||||||
@ -56,12 +55,12 @@ suite "wallet: accepting incoming channel":
|
|||||||
|
|
||||||
test "returns the new channel id":
|
test "returns the new channel id":
|
||||||
let channel = wallet.acceptChannel(signed).get
|
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":
|
test "signs the channel state":
|
||||||
let channel = wallet.acceptChannel(signed).get
|
let channel = wallet.acceptChannel(signed).get
|
||||||
let expectedSignatures = @{wallet.address: key.sign(signed.state)}
|
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":
|
test "fails when wallet address is not a participant":
|
||||||
let wrongParticipants = seq[EthAddress].example
|
let wrongParticipants = seq[EthAddress].example
|
||||||
@ -75,3 +74,59 @@ suite "wallet: accepting incoming channel":
|
|||||||
signed.state.channel.participants &= @[otherWallet.address]
|
signed.state.channel.participants &= @[otherWallet.address]
|
||||||
signed.signatures = @{wrongAddress: otherKey.sign(signed.state)}
|
signed.signatures = @{wrongAddress: otherKey.sign(signed.state)}
|
||||||
check wallet.acceptChannel(signed).isErr
|
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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user