Skip to content

Commit a2de820

Browse files
committed
Support transfers with 24h validity period
Signed-off-by: Moritz Kiefer <moritz.kiefer@purelyfunctional.org>
1 parent 01e85f4 commit a2de820

File tree

13 files changed

+520
-97
lines changed

13 files changed

+520
-97
lines changed

daml/splice-amulet-test/daml/Splice/Scripts/Util.daml

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import Splice.AmuletConfig (AmuletConfig(..))
2727
import qualified Splice.AmuletConfig as Unit
2828
import Splice.Expiry
2929
import Splice.ExternalPartyAmuletRules
30+
import Splice.ExternalPartyConfigState
3031
import Splice.Issuance
3132
import Splice.Round
3233
import Splice.Fees
@@ -76,11 +77,44 @@ genericSetupApp dsoPrefix = do
7677

7778
_ <- submit dso $ createCmd ExternalPartyAmuletRules with dso
7879

79-
submitExerciseAmuletRulesByKey app [dso] [] AmuletRules_Bootstrap_Rounds with
80+
result <- submitExerciseAmuletRulesByKey app [dso] [] AmuletRules_Bootstrap_Rounds with
8081
amuletPrice = 1.0
8182
round0Duration = hours 24 -- extra time for all initial svs to join
8283
initialRound = None
8384

85+
Some openMiningRound <- queryContractId dso result.openMiningRoundCid
86+
87+
now <- getTime
88+
89+
-- FIXME: Consider moving the bootstrapping to AmuletRules so we can reuse AmuletRules_BootstrapExternalPartyConfigState
90+
-- upside: no duplication here
91+
-- downside: requires moving the tick parameter + the code into splice-amulet so changes are more disruptive and outside of testing it doesn't
92+
-- seem to have a clear advantage
93+
94+
submit dso $ createCmd ExternalPartyConfigState with
95+
dso
96+
openRoundNumber = openMiningRound.round
97+
amuletPrice = openMiningRound.amuletPrice
98+
transferConfig = openMiningRound.transferConfigUsd
99+
validFrom = now
100+
validUntil = now `addRelTime` hours 24
101+
102+
submit dso $ createCmd ExternalPartyConfigState with
103+
dso
104+
openRoundNumber = openMiningRound.round
105+
amuletPrice = openMiningRound.amuletPrice
106+
transferConfig = openMiningRound.transferConfigUsd
107+
validFrom = now
108+
validUntil = now `addRelTime` hours 48
109+
110+
submit dso $ createCmd ExternalPartyConfigState with
111+
dso
112+
openRoundNumber = openMiningRound.round
113+
amuletPrice = openMiningRound.amuletPrice
114+
transferConfig = openMiningRound.transferConfigUsd
115+
validFrom = now `addRelTime` hours 24
116+
validUntil = now `addRelTime` hours 72
117+
84118
-- return the off-ledger reference to the app for later script steps
85119
return app
86120

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Splice.Api.Token.HoldingV1 qualified as Api.Token.HoldingV1
1616

1717
import Splice.Amulet.TokenApiUtils
1818
import Splice.Expiry
19+
import Splice.ExternalPartyConfigState
1920
import Splice.Fees
2021
import qualified Splice.Api.FeaturedAppRightV1
2122
import Splice.Round
@@ -81,7 +82,7 @@ data SvRewardCoupon_ArchiveAsBeneficiaryResult = SvRewardCoupon_ArchiveAsBenefic
8182

8283
data UnclaimedActivityRecord_ArchiveAsBeneficiaryResult = UnclaimedActivityRecord_ArchiveAsBeneficiaryResult
8384

84-
data UnclaimedActivityRecord_DsoExpireResult = UnclaimedActivityRecord_DsoExpireResult with
85+
data UnclaimedActivityRecord_DsoExpireResult = UnclaimedActivityRecord_DsoExpireResult with
8586
unclaimedRewardCid : ContractId UnclaimedReward
8687

8788
-- | A amulet, which can be locked and whose amount expires over time.
@@ -159,6 +160,19 @@ template LockedAmulet
159160
return LockedAmulet_UnlockResult with
160161
meta = Some (simpleHoldingTxMeta TxKind_Unlock (Some "holders released lock") None), ..
161162

163+
choice LockedAmulet_ExternalPartyUnlock : LockedAmulet_UnlockResult
164+
with
165+
externalPartyConfigStateCid : ContractId ExternalPartyConfigState
166+
controller amulet.owner :: lock.holders
167+
do externalPartyConfigState <- fetchReferenceData (ForDso with dso = amulet.dso) externalPartyConfigStateCid
168+
amuletCid <- create amulet
169+
let amuletSum = AmuletCreateSummary with
170+
amulet = amuletCid
171+
amuletPrice = externalPartyConfigState.amuletPrice
172+
round = externalPartyConfigState.openRoundNumber
173+
return LockedAmulet_UnlockResult with
174+
meta = Some (simpleHoldingTxMeta TxKind_Unlock (Some "holders released lock") None), ..
175+
162176
choice LockedAmulet_OwnerExpireLock : LockedAmulet_OwnerExpireLockResult
163177
with
164178
openRoundCid : ContractId OpenMiningRound
@@ -173,6 +187,20 @@ template LockedAmulet
173187
return LockedAmulet_OwnerExpireLockResult with
174188
meta = Some (simpleHoldingTxMeta TxKind_Unlock (Some "lock expired") None), ..
175189

190+
choice LockedAmulet_ExternalPartyOwnerExpireLock : LockedAmulet_OwnerExpireLockResult
191+
with
192+
externalPartyConfigStateCid : ContractId ExternalPartyConfigState
193+
controller amulet.owner
194+
do externalPartyConfigState <- fetchReferenceData (ForDso with dso = amulet.dso) externalPartyConfigStateCid
195+
assertDeadlineExceeded "Lock.expiresAt" lock.expiresAt
196+
amuletCid <- create amulet
197+
let amuletSum = AmuletCreateSummary with
198+
amulet = amuletCid
199+
amuletPrice = externalPartyConfigState.amuletPrice
200+
round = externalPartyConfigState.openRoundNumber
201+
return LockedAmulet_OwnerExpireLockResult with
202+
meta = Some (simpleHoldingTxMeta TxKind_Unlock (Some "lock expired") None), ..
203+
176204
choice LockedAmulet_ExpireAmulet : LockedAmulet_ExpireAmuletResult
177205
with
178206
roundCid : ContractId OpenMiningRound
@@ -392,29 +420,29 @@ template UnclaimedReward with
392420

393421
signatory dso
394422

395-
-- | A record of activity that can be minted by the beneficiary.
396-
-- Note that these do not come out of the per-round issuance but are instead created by burning
397-
-- UnclaimedRewardCoupon as defined through a vote by the SVs. That's also why expiry is a separate
423+
-- | A record of activity that can be minted by the beneficiary.
424+
-- Note that these do not come out of the per-round issuance but are instead created by burning
425+
-- UnclaimedRewardCoupon as defined through a vote by the SVs. That's also why expiry is a separate
398426
-- time-based expiry instead of being tied to a round like the other activity records.
399427
template UnclaimedActivityRecord
400428
with
401429
dso : Party
402430
beneficiary : Party -- ^ The owner of the `Amulet` to be minted.
403431
amount : Decimal -- ^ The amount of `Amulet` to be minted.
404-
reason : Text -- ^ A reason to mint the `Amulet`.
405-
expiresAt : Time -- ^ Selected timestamp defining the lifetime of the contract.
406-
where
432+
reason : Text -- ^ A reason to mint the `Amulet`.
433+
expiresAt : Time -- ^ Selected timestamp defining the lifetime of the contract.
434+
where
407435
signatory dso
408436
observer beneficiary
409437
ensure amount > 0.0
410438

411439
choice UnclaimedActivityRecord_DsoExpire : UnclaimedActivityRecord_DsoExpireResult
412440
controller dso
413-
do
441+
do
414442
assertDeadlineExceeded "UnclaimedActivityRecord.expiresAt" expiresAt
415443
unclaimedRewardCid <- create UnclaimedReward with dso; amount
416444
pure UnclaimedActivityRecord_DsoExpireResult with unclaimedRewardCid
417-
445+
418446

419447
requireAmuletExpiredForAllOpenRounds : ContractId OpenMiningRound -> Amulet -> Update ()
420448
requireAmuletExpiredForAllOpenRounds roundCid amulet = do
@@ -456,4 +484,4 @@ instance HasCheckedFetch FeaturedAppActivityMarker ForDso where
456484
contractGroupId FeaturedAppActivityMarker {..} = ForDso with dso
457485

458486
instance HasCheckedFetch UnclaimedActivityRecord ForOwner where
459-
contractGroupId UnclaimedActivityRecord{..} = ForOwner with dso; owner = beneficiary
487+
contractGroupId UnclaimedActivityRecord{..} = ForOwner with dso; owner = beneficiary

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ nonZeroMetadata k n m
4343

4444
-- | Add an metadata entry for an optional value if it is non-zero number.
4545
optionalNonZeroMetadata : (Eq a, Additive a, Show a) => Text -> Optional a -> TextMap Text -> TextMap Text
46-
optionalNonZeroMetadata k optN m =
46+
optionalNonZeroMetadata k optN m =
4747
case optN of
4848
None -> m
4949
Some n -> nonZeroMetadata k n m
@@ -146,6 +146,9 @@ amuletRulesContextKey = "amulet-rules"
146146
openRoundContextKey : Text
147147
openRoundContextKey = "open-round"
148148

149+
externalPartyConfigStateContextKey : Text
150+
externalPartyConfigStateContextKey = "external-party-config-state"
151+
149152
featuredAppRightContextKey : Text
150153
featuredAppRightContextKey = "featured-app-right"
151154

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

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,32 +52,40 @@ feeReserveMultiplier = 4.0
5252

5353
-- | Converting a set of holding inputs to inputs for an amulet transfer,
5454
-- unlocking any expired LockedAmulet holdings on the fly.
55-
holdingToTransferInputs : ForOwner -> PaymentTransferContext -> [ContractId Holding] -> Update [TransferInput]
56-
holdingToTransferInputs forOwner paymentContext inputHoldingCids =
55+
holdingToTransferInputs : ForOwner -> Either PaymentTransferContext ExternalPartyPaymentTransferContext -> [ContractId Holding] -> Update [TransferInput]
56+
holdingToTransferInputs forOwner paymentContextE inputHoldingCids =
5757
forA inputHoldingCids $ \holdingCid -> do
5858
holding <- fetchCheckedInterface @Holding forOwner holdingCid
5959
case fromInterface holding of
6060
Some (LockedAmulet {}) -> do
6161
let lockedAmuletCid : ContractId LockedAmulet = fromInterfaceContractId holdingCid
6262
-- We assume the lock is expired, and if not then we rely `LockedAmulet_OwnerExpireLock` to fail
63-
result <- exercise lockedAmuletCid LockedAmulet_OwnerExpireLock with
64-
openRoundCid = paymentContext.context.openMiningRound
65-
pure $ InputAmulet result.amuletSum.amulet
63+
case paymentContextE of
64+
Left paymentContext -> do
65+
result <- exercise lockedAmuletCid LockedAmulet_OwnerExpireLock with
66+
openRoundCid = paymentContext.context.openMiningRound
67+
pure $ InputAmulet result.amuletSum.amulet
68+
Right externalPartyPaymentContext -> do
69+
result <- exercise lockedAmuletCid LockedAmulet_ExternalPartyOwnerExpireLock with
70+
externalPartyConfigStateCid = externalPartyPaymentContext.context.externalPartyConfigState
71+
pure $ InputAmulet result.amuletSum.amulet
6672
None -> pure $ InputAmulet $ coerceContractId holdingCid
6773

6874
-- | Prepare a two-step transfer of amulet by locking the funds.
6975
prepareTwoStepTransfer
70-
: TwoStepTransfer -> Time -> [ContractId Holding] -> PaymentTransferContext
76+
: TwoStepTransfer -> Time -> [ContractId Holding] -> Either PaymentTransferContext ExternalPartyPaymentTransferContext
7177
-> Update (ContractId LockedAmulet, [ContractId Holding], Metadata)
7278
prepareTwoStepTransfer TwoStepTransfer{..} requestedAt inputHoldingCids paymentContext = do
7379
require "requestedAt < transferBefore" (requestedAt < transferBefore)
7480
-- over-approximate fees that will be due on the actual transfer
75-
let receiverOutputForActualTransfer = TransferOutput with
76-
receiver
77-
amount
78-
receiverFeeRatio = 0.0 -- all fees are paid by the sender
79-
lock = None
80-
[expectedTransferFees] <- exerciseComputeFees dso paymentContext sender [receiverOutputForActualTransfer]
81+
-- let receiverOutputForActualTransfer = TransferOutput with
82+
-- receiver
83+
-- amount
84+
-- receiverFeeRatio = 0.0 -- all fees are paid by the sender
85+
-- lock = None
86+
-- FIXME: consider if we do need to preserve this in some form but don't see why
87+
-- [expectedTransferFees] <- exerciseComputeFees dso paymentContext sender [receiverOutputForActualTransfer]
88+
let expectedTransferFees = 0.0
8189
let feesReserveAmount = expectedTransferFees * feeReserveMultiplier
8290

8391
-- lock the amulet
@@ -98,7 +106,7 @@ prepareTwoStepTransfer TwoStepTransfer{..} requestedAt inputHoldingCids paymentC
98106
inputs = transferInputs
99107
beneficiaries = None
100108

101-
result <- exercisePaymentTransfer dso paymentContext transfer
109+
result <- exerciseExternalPartyPaymentTransferWithFallback dso paymentContext transfer
102110
let [TransferResultLockedAmulet lockedAmulet] = result.createdAmulets
103111
pure
104112
( lockedAmulet
@@ -121,9 +129,8 @@ executeTwoStepTransfer TwoStepTransfer{..} lockedAmuletCid extraArgs = do
121129
-- ignore beneficiaries in case we are not allowing featuring
122130
context <- unfeaturedPaymentContextFromChoiceContext dso extraArgs.context
123131
pure (context, None)
124-
let openRoundCid = paymentContext.context.openMiningRound
125132
-- unlock amulet
126-
unlockResult <- exercise lockedAmuletCid LockedAmulet_Unlock with openRoundCid
133+
unlockResult <- unlockAmulet paymentContext lockedAmuletCid
127134
let amuletCid = unlockResult.amuletSum.amulet
128135
-- execute transfer
129136
let receiverOutput = TransferOutput with
@@ -137,7 +144,7 @@ executeTwoStepTransfer TwoStepTransfer{..} lockedAmuletCid extraArgs = do
137144
inputs = [InputAmulet amuletCid]
138145
outputs = [receiverOutput]
139146
beneficiaries
140-
result <- exercisePaymentTransfer dso paymentContext amuletRulesTransfer
147+
result <- exerciseExternalPartyPaymentTransferWithFallback dso paymentContext amuletRulesTransfer
141148
pure
142149
( optionalToList (toInterfaceContractId <$> result.senderChangeAmulet)
143150
, createdAmuletToHolding <$> result.createdAmulets
@@ -149,6 +156,7 @@ abortTwoStepTransfer TwoStepTransfer{..} lockedAmuletCid extraArgs = do
149156
expireLockedAmulet <- getFromContextU @Bool extraArgs.context expireLockKey
150157
if expireLockedAmulet
151158
then do
159+
-- FIXME
152160
openRoundCid <- getFromContextU @(ContractId OpenMiningRound) extraArgs.context openRoundContextKey
153161
-- prudent engineering: check the DSO party
154162
_ <- fetchChecked (ForDso with dso) openRoundCid
@@ -159,3 +167,10 @@ abortTwoStepTransfer TwoStepTransfer{..} lockedAmuletCid extraArgs = do
159167
assertDeadlineExceeded transferBeforeDeadline transferBefore
160168
pure []
161169

170+
unlockAmulet : Either PaymentTransferContext ExternalPartyPaymentTransferContext -> ContractId LockedAmulet -> Update LockedAmulet_UnlockResult
171+
unlockAmulet transferContextE lockedAmuletCid = do
172+
case transferContextE of
173+
Left paymentContext -> do
174+
exercise lockedAmuletCid LockedAmulet_Unlock with openRoundCid = paymentContext.context.openMiningRound
175+
Right externalPartyPaymentContext -> do
176+
exercise lockedAmuletCid LockedAmulet_ExternalPartyUnlock with externalPartyConfigStateCid = externalPartyPaymentContext.context.externalPartyConfigState

daml/splice-amulet/daml/Splice/AmuletConfig.daml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ validAmuletConfig AmuletConfig
6969
tickDuration > days 0 &&
7070
validPackageConfig packageConfig &&
7171
optional True (>= 0.0) transferPreapprovalFee &&
72-
optional True (>= 0.0) featuredAppActivityMarkerAmount
72+
optional True (>= 0.0) featuredAppActivityMarkerAmount &&
73+
-- FIXME: If we don't want to do that we could instead
74+
-- create app reward coupons instead of markers when they are not the same. That
75+
-- seems kind of needless complexity given that we don't expect them to deviate.
76+
featuredAppActivityMarkerAmount == Some transferConfig.extraFeaturedAppRewardAmount
7377

7478
validTransferConfig : TransferConfig unit -> Bool
7579
validTransferConfig TransferConfig

0 commit comments

Comments
 (0)