Skip to content

Commit 58408a6

Browse files
committed
Implement and test Amulet settlment with auth via Allocation
1 parent b85b094 commit 58408a6

File tree

13 files changed

+430
-322
lines changed

13 files changed

+430
-322
lines changed

daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ import Splice.Util
3838
data TwoStepTransfer = TwoStepTransfer with
3939
dso : Party
4040
sender : Party
41-
receiver : Party
42-
amount : Decimal
41+
outputs : [(Party, Decimal)]
4342
lockContext : Text
4443
-- ^ Context description of the lock. This is used to display the reason for
4544
-- the lock in wallets.
@@ -77,26 +76,29 @@ prepareTwoStepTransfer
7776
prepareTwoStepTransfer TwoStepTransfer{..} requestedAt inputHoldingCids paymentContext = do
7877
require "requestedAt < transferBefore" (requestedAt < transferBefore)
7978
-- over-approximate fees that will be due on the actual transfer
80-
let receiverOutputForActualTransfer = TransferOutput with
81-
receiver
82-
amount
83-
receiverFeeRatio = 0.0 -- all fees are paid by the sender
84-
lock = None
85-
[expectedTransferFees] <- exerciseComputeFees paymentContext sender [receiverOutputForActualTransfer]
79+
let receiverOutputsForActualTransfer = map (\(receiver, amount) -> TransferOutput with
80+
receiver
81+
amount
82+
receiverFeeRatio = 0.0 -- all fees are paid by the sender
83+
lock = None
84+
)
85+
outputs
86+
expectedTransferFees <- sum <$> exerciseComputeFees paymentContext sender receiverOutputsForActualTransfer
8687
openRound <- fetchChecked (ForDso with dso) paymentContext.context.openMiningRound
8788
let lockDuration = transferBefore `subTime` requestedAt
8889
let approximateHoldingFees = holdingFeesForDuration lockDuration openRound
8990
let feesReserveAmount = (expectedTransferFees + approximateHoldingFees) * feeReserveMultiplier
9091

9192
-- lock the amulet
9293
transferInputs <- holdingToTransferInputs (ForOwner with dso; owner = sender) paymentContext inputHoldingCids
94+
let totalAmount = sum $ (map snd outputs)
9395
let transfer = Splice.AmuletRules.Transfer with
9496
sender
9597
provider = sender -- the sender is serving as its own "app provider"
9698
outputs =
9799
[ TransferOutput with
98100
receiver = sender
99-
amount = amount + feesReserveAmount
101+
amount = totalAmount + feesReserveAmount
100102
receiverFeeRatio = 0.0 -- locking fees are paid by the sender
101103
lock = Some TimeLock with
102104
expiresAt = transferBefore
@@ -134,16 +136,18 @@ executeTwoStepTransfer TwoStepTransfer{..} lockedAmuletCid extraArgs = do
134136
unlockResult <- exercise lockedAmuletCid LockedAmulet_Unlock with openRoundCid
135137
let amuletCid = unlockResult.amuletSum.amulet
136138
-- execute transfer
137-
let receiverOutput = TransferOutput with
138-
receiver = receiver
139-
amount = amount
140-
receiverFeeRatio = 0.0 -- all fees are paid by the sender
141-
lock = None
139+
let receiverOutputs = map (\(receiver, amount) -> TransferOutput with
140+
receiver
141+
amount
142+
receiverFeeRatio = 0.0 -- all fees are paid by the sender
143+
lock = None
144+
)
145+
outputs
142146
let amuletRulesTransfer = Splice.AmuletRules.Transfer with
143147
sender
144148
provider
145149
inputs = [InputAmulet amuletCid]
146-
outputs = [receiverOutput]
150+
outputs = receiverOutputs
147151
beneficiaries
148152
result <- exerciseCheckedPaymentTransfer dso paymentContext amuletRulesTransfer
149153
pure

daml/splice-amulet/daml/Splice/AmuletAllocation.daml

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
module Splice.AmuletAllocation (
55
AmuletAllocation(..),
66
allocationToTwoStepTransfer,
7-
allocationSender,
87
) where
98

9+
import DA.Assert((===), (=/=))
1010
import DA.Text as Text
1111
import DA.TextMap qualified as TextMap
12-
import DA.List (dedupSort)
12+
import DA.List ((\\), dedupSort)
13+
import DA.Optional(fromSome)
1314

1415
import Splice.Api.Token.MetadataV1
1516
import Splice.Api.Token.HoldingV2
@@ -26,21 +27,32 @@ template AmuletAllocation
2627
with
2728
lockedAmulet : ContractId LockedAmulet -- ^ Locked amulet that holds the funds for the allocation
2829
allocation : AllocationSpecification
30+
sender : Party
31+
admin : Party
2932
where
30-
signatory allocationInstrumentAdmin allocation, allocationSender allocation
33+
signatory admin, sender
3134
observer allocation.settlement.executor
3235

33-
-- Only allow a single sender.
34-
ensure all (\(_,tl) -> tl.sender == allocationSender allocation) (TextMap.toList allocation.transferLegs)
36+
ensure
37+
all
38+
(\(_,tl) -> (
39+
-- Sender needs to appear as sender or receiver of each leg.
40+
sender `elem` [tl.sender, tl.receiver])
41+
-- Only one admin Id allowed.
42+
&& tl.instrumentId.admin == admin)
43+
(TextMap.toList allocation.transferLegs)
3544

3645
interface instance Allocation for AmuletAllocation where
3746
view = AllocationView with
3847
allocation
3948
holdingCids = [toInterfaceContractId lockedAmulet]
4049
meta = emptyMetadata
41-
transferExtraAuth = [allocationSender allocation]
50+
senders = [sender]
51+
requiredReceiverAuth = (defaultAllocationControllers allocation) \\ (sender::allocationControllers allocation)
4252

43-
allocation_executeTransferImpl _self Allocation_ExecuteTransfer{..} = transferAmuletAllocation this extraArgs
53+
allocation_executeTransferImpl self Allocation_ExecuteTransfer{..} = case extraAuth of
54+
[] -> transferAmuletAllocation this extraArgs
55+
ea -> collectAuthAndSettle (fromInterfaceContractId self) extraArgs ea []
4456

4557
allocation_withdrawImpl _self Allocation_Withdraw{..} = do
4658
senderHoldingCids <- unlockAmuletAllocation this extraArgs
@@ -56,7 +68,12 @@ template AmuletAllocation
5668
senderHoldingCids
5769
meta = emptyMetadata
5870

59-
allocation_executeAuthorizeIncomingImpl = error "unimplemented"
71+
allocation_executeAuthorizeIncomingImpl _self Allocation_AuthorizeIncoming{..} = do
72+
cid <- create AmuletAllocationTransferAuthorization with
73+
allocation
74+
receiver = sender
75+
admin
76+
return $ toInterfaceContractId cid
6077

6178
interface instance AllocationV1.Allocation for AmuletAllocation where
6279
view = allocation_view_v2_to_v1 (view (toInterface @Allocation this))
@@ -65,46 +82,53 @@ template AmuletAllocation
6582
AllocationV1.allocation_withdrawImpl = allocation_v1_withdrawImpl (toInterface @Allocation this)
6683
AllocationV1.allocation_cancelImpl = allocation_v1_cancelImpl (toInterface @Allocation this)
6784

68-
allocationInstrumentAdmin : AllocationSpecification -> Party
69-
allocationInstrumentAdmin AllocationSpecification{..} =
70-
let tl::_ = (TextMap.toList transferLegs)
71-
in tl._2.instrumentId.admin
85+
choice AmuletAllocation_InternalSettleWithExtraAuth : Allocation_ExecuteTransferResult
86+
with
87+
extraArgs : ExtraArgs
88+
extraControllers : [Party]
89+
controller extraControllers ++ allocationControllers allocation
90+
do transferAmuletAllocation this extraArgs
7291

73-
-- Amulet only supports a single sender!
74-
allocationSender : AllocationSpecification -> Party
75-
allocationSender AllocationSpecification{..} =
76-
let tl::_ = (TextMap.toList transferLegs)
77-
in tl._2.sender
7892

79-
allocationInstrumentReceivers : AllocationSpecification -> [Party]
80-
allocationInstrumentReceivers AllocationSpecification{..} =
93+
94+
allocationReceivers : AllocationSpecification -> [Party]
95+
allocationReceivers AllocationSpecification{..} =
8196
dedupSort $ map ((.receiver) . snd) (TextMap.toList transferLegs)
8297

8398
-- Allocation usage
8499
-------------------
85100

86-
allocationToTwoStepTransfer : AllocationSpecification -> TwoStepTransfer
87-
allocationToTwoStepTransfer allocation =
101+
allocationToTwoStepTransfer : Party -> Party -> AllocationSpecification -> TwoStepTransfer
102+
allocationToTwoStepTransfer sender admin allocation =
88103
TwoStepTransfer with
89-
dso = transferLeg.instrumentId.admin
90-
sender = transferLeg.sender
91-
receiver = transferLeg.receiver
92-
amount = transferLeg.amount
104+
dso = admin
105+
sender = sender
106+
outputs
93107
provider = allocation.settlement.executor
94108
transferBefore = allocation.settlement.settleBefore
95109
transferBeforeDeadline = "allocation.settlement.settleBefore"
96110
allowFeaturing = True
97111
lockContext = Text.implode
98112
-- We don't show more context to avoid bloating the response here.
99-
["allocation for transfer leg ", show transferLegId, " to ", show transferLeg.receiver]
113+
["allocation for settlement ", allocation.settlement.settlementRef.id]
100114
where
101-
(transferLegId, transferLeg) = case TextMap.toList allocation.transferLegs of
102-
[tl] -> tl
103-
_ -> error "Only one leg supported" -- TODO.
115+
senderLegs = filter (\tl -> tl.sender == sender) $ map snd (TextMap.toList allocation.transferLegs)
116+
outputs = map (\tl -> (tl.receiver, tl.amount)) senderLegs
117+
118+
collectAuthAndSettle : ContractId AmuletAllocation -> ExtraArgs -> [ContractId AllocationTransferAuthorization] -> [Party] -> Update Allocation_ExecuteTransferResult
119+
collectAuthAndSettle allocationCid extraArgs extraAuth extraControllers = do
120+
case extraAuth of
121+
eaCid::eas -> exercise (fromInterfaceContractId @AmuletAllocationTransferAuthorization eaCid) AmuletAllocationTransferAuthorization_AuthorizeTransfer with
122+
extraAuth = eas
123+
extraControllers
124+
..
125+
[] -> exercise allocationCid AmuletAllocation_InternalSettleWithExtraAuth with
126+
extraArgs
127+
extraControllers
104128

105129
transferAmuletAllocation : AmuletAllocation -> ExtraArgs -> Update Allocation_ExecuteTransferResult
106130
transferAmuletAllocation amuletAllocation extraArgs = do
107-
let twoStepTransfer = allocationToTwoStepTransfer amuletAllocation.allocation
131+
let twoStepTransfer = allocationToTwoStepTransfer amuletAllocation.sender amuletAllocation.admin amuletAllocation.allocation
108132
(senderHoldingCids, receiverHoldingCids, meta) <-
109133
executeTwoStepTransfer twoStepTransfer amuletAllocation.lockedAmulet extraArgs
110134
pure Allocation_ExecuteTransferResult
@@ -115,5 +139,40 @@ transferAmuletAllocation amuletAllocation extraArgs = do
115139

116140
unlockAmuletAllocation : AmuletAllocation -> ExtraArgs -> Update [ContractId Holding]
117141
unlockAmuletAllocation amuletAllocation extraArgs = do
118-
let twoStepTransfer = allocationToTwoStepTransfer amuletAllocation.allocation
142+
let twoStepTransfer = allocationToTwoStepTransfer amuletAllocation.sender amuletAllocation.admin amuletAllocation.allocation
119143
abortTwoStepTransfer twoStepTransfer amuletAllocation.lockedAmulet extraArgs
144+
145+
template AmuletAllocationTransferAuthorization
146+
with
147+
allocation : AllocationSpecification
148+
receiver : Party
149+
admin : Party
150+
where
151+
signatory admin, receiver, allocation.settlement.executor
152+
153+
interface instance AllocationTransferAuthorization for AmuletAllocationTransferAuthorization where
154+
view = AllocationTransferAuthorizationView with
155+
allocation
156+
receiver
157+
admin
158+
159+
choice AmuletAllocationTransferAuthorization_AuthorizeTransfer : Allocation_ExecuteTransferResult
160+
with
161+
allocationCid : ContractId AmuletAllocation
162+
extraArgs : ExtraArgs
163+
extraAuth : [ContractId AllocationTransferAuthorization]
164+
extraControllers : [Party]
165+
controller extraControllers ++ allocation.settlement.executor :: fromSome (allocation.settlement.controllerOverride) -- should never be None if this is called.
166+
do
167+
-- Validate that the receiver is only a receiver on a matching settlement
168+
amuletAllocation <- fetch allocationCid
169+
let allocation' = amuletAllocation.allocation
170+
allocation === allocation'
171+
admin === amuletAllocation.admin
172+
receiver =/= amuletAllocation.sender
173+
let receivers = allocationReceivers allocation
174+
assertMsg ("Receiver " <> show receiver <> " not found in receivers " <> show receivers)
175+
(receiver `elem` receivers)
176+
-- Transfer with added authority.
177+
collectAuthAndSettle allocationCid extraArgs extraAuth (receiver::extraControllers)
178+

daml/splice-amulet/daml/Splice/AmuletTransferInstruction.daml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ standardTransferToTwoStepTransfer transfer =
6464
dso = transfer.instrumentId.admin
6565
sender = transfer.sender
6666
provider = transfer.sender
67-
receiver = transfer.receiver
68-
amount = transfer.amount
67+
outputs = [(transfer.receiver, transfer.amount)]
6968
transferBefore = transfer.executeBefore
7069
transferBeforeDeadline = "Transfer.executeBefore"
7170
allowFeaturing = False -- unfeatured as the sender is serving as its own "app provider"

daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ amulet_allocationFactory_allocateImpl
365365
-> Update AllocationInstructionResult
366366
amulet_allocationFactory_allocateImpl externalAmuletRules _self arg = do
367367
let dso = externalAmuletRules.dso
368-
let AllocationFactory_Allocate {allocation, requestedAt, inputHoldingCids, extraArgs} = arg
368+
let AllocationFactory_Allocate {allocation, requestedAt, inputHoldingCids, extraArgs, creator} = arg
369369
-- validate call to factory and retrieve context
370370
requireExpectedAdminMatch arg.expectedAdmin dso
371371

@@ -409,13 +409,16 @@ amulet_allocationFactory_allocateImpl externalAmuletRules _self arg = do
409409
require "At least one input holding must be provided" (not $ null inputHoldingCids)
410410

411411
-- lock the funds
412-
let twoStepTransfer = allocationToTwoStepTransfer arg.allocation
412+
let sender = creator
413+
let twoStepTransfer = allocationToTwoStepTransfer sender dso arg.allocation
413414
(lockedAmulet, senderChangeCids, meta) <-
414415
prepareTwoStepTransfer twoStepTransfer arg.requestedAt inputHoldingCids paymentContext
415416
-- create the amulet allocation
416417
allocationCid <- toInterfaceContractId <$> create AmuletAllocation with
417418
allocation = arg.allocation
418419
lockedAmulet
420+
admin = dso
421+
sender
419422

420423
-- finaly done: return the result
421424
pure AllocationInstructionResult with

daml/splice-wallet-test/daml/Splice/Scripts/TestWallet.daml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,22 +1106,22 @@ testTokenStandardAllocate = script do
11061106
meta = emptyMetadata
11071107
let aliceLeg = mkTransfer alice alice 100.0
11081108

1109-
-- alice proposes trade with herself
1110-
proposalCid <- submit alice $ createCmd OTCTradeProposal with
1109+
-- provider proposes a trade for alice with herself
1110+
now <- getTime
1111+
let settleBefore = now `addRelTime` hours 2
1112+
proposalCid <- submit provider $ createCmd OTCTrade with
11111113
venue = provider
1112-
tradeCid = None
11131114
transferLegs = TextMap.fromList [("leg0", aliceLeg)]
1114-
approvers = [alice]
1115+
prepareUntil = now `addRelTime` hours 1
1116+
settleBefore
1117+
createdAt = now
1118+
11151119

11161120
-- provider initiates settlement
1117-
now <- getTime
1118-
let settleBefore = now `addRelTime` hours 2
11191121
_ <- submit provider $
1120-
exerciseCmd proposalCid OTCTradeProposal_InitiateSettlement with
1121-
prepareUntil = now `addRelTime` hours 1
1122-
settleBefore
1122+
exerciseCmd proposalCid OTCTrade_RequestAllocations with
11231123

1124-
[aliceAlloc] <- WalletClient.listRequestedAllocations alice amuletId
1124+
[aliceAlloc] <- WalletClient.listRequestedAllocationsForAdmin alice amuletId.admin
11251125

11261126
holdingCid <- AmuletRegistry.tapFaucet registry alice 200.0
11271127

@@ -1131,6 +1131,7 @@ testTokenStandardAllocate = script do
11311131
inputHoldingCids = [coerceInterfaceContractId holdingCid]
11321132
requestedAt = now
11331133
extraArgs = emptyExtraArgs
1134+
creator = alice
11341135

11351136
result <- submitMulti [aliceValidator] [alice, registry.dso] $ exerciseCmd aliceInstall WalletAppInstall_AllocationFactory_Allocate with
11361137
allocationFactory = enrichedChoice.factoryCid

daml/splice-wallet/daml/Splice/Wallet/Install.daml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import qualified Splice.Api.Token.AllocationV2
1515
import qualified Splice.Api.Token.AllocationInstructionV2
1616
import qualified Splice.Api.Token.TransferInstructionV2
1717
import Splice.Amulet
18-
import Splice.AmuletAllocation (allocationSender)
1918
import Splice.Amulet.TokenApiUtils
2019
import Splice.Types
2120
import Splice.AmuletRules
@@ -640,7 +639,8 @@ template WalletAppInstall
640639
withdrawArg : Splice.Api.Token.AllocationV2.Allocation_Withdraw
641640
controller validatorParty
642641
do allocation <- fetchCheckedInterface (ForDso dsoParty) allocationCid
643-
let sender = allocationSender (view allocation).allocation
642+
-- Amulet only supports a single sender per allocation
643+
let [sender] = (view allocation).senders
644644
require ("sender " <> show sender <> " must match endUserParty " <> show endUserParty) (sender == endUserParty)
645645
exercise allocationCid withdrawArg
646646

token-standard/splice-api-token-allocation-instruction-v2/daml/Splice/Api/Token/AllocationInstructionV2.daml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ data AllocationInstructionView = AllocationInstructionView with
3434
-- ^ The holdings to be used to fund the allocation.
3535
--
3636
-- MAY be empty for registries that do not represent their holdings on-ledger.
37+
senders : [Party]
38+
-- ^ The senders of the allocation - who typically instructed the allocation instruction
39+
-- and later appear on the Allocation as senders.
3740
meta : Metadata
3841
-- ^ Additional metadata specific to the allocation instruction, used for
3942
-- extensibility; e.g., more detailed status information.
@@ -57,7 +60,7 @@ interface AllocationInstruction where
5760
with
5861
extraArgs : ExtraArgs
5962
-- ^ Additional context required in order to exercise the choice.
60-
controller allocationSenders (view this).allocation
63+
controller (view this).senders
6164
do allocationInstruction_withdrawImpl this self arg
6265

6366
choice AllocationInstruction_Update : AllocationInstructionResult
@@ -124,7 +127,8 @@ interface AllocationFactory where
124127
-- deliberate contention on holdings to prevent duplicate allocations.
125128
extraArgs : ExtraArgs
126129
-- ^ Additional choice arguments.
127-
controller allocationSenders allocation
130+
creator : Party
131+
controller creator
128132
do allocationFactory_allocateImpl this self arg
129133

130134
nonconsuming choice AllocationFactory_PublicFetch : AllocationFactoryView

0 commit comments

Comments
 (0)