Skip to content

Commit afd6585

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

File tree

14 files changed

+466
-132
lines changed

14 files changed

+466
-132
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ scaleAmuletConfig amuletPrice config = AmuletConfig with
3333
packageConfig = config.packageConfig
3434
transferPreapprovalFee = fmap (/ amuletPrice) config.transferPreapprovalFee
3535
featuredAppActivityMarkerAmount = fmap (/ amuletPrice) config.featuredAppActivityMarkerAmount
36+
externalPartyConfigStateTickDuration = config.externalPartyConfigStateTickDuration
3637

3738
test : Script ()
3839
test = script do

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,14 @@ genericSetupApp dsoPrefix = do
7676

7777
_ <- submit dso $ createCmd ExternalPartyAmuletRules with dso
7878

79-
submitExerciseAmuletRulesByKey app [dso] [] AmuletRules_Bootstrap_Rounds with
79+
result <- submitExerciseAmuletRulesByKey app [dso] [] AmuletRules_Bootstrap_Rounds with
8080
amuletPrice = 1.0
8181
round0Duration = hours 24 -- extra time for all initial svs to join
8282
initialRound = None
8383

84+
_ <- submitExerciseAmuletRulesByKey app [dso] [] AmuletRules_BootstrapExternalPartyConfigState with
85+
openRoundCid = result.openMiningRoundCid
86+
8487
-- return the off-ledger reference to the app for later script steps
8588
return app
8689

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: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,41 +45,33 @@ data TwoStepTransfer = TwoStepTransfer with
4545
allowFeaturing : Bool
4646
-- ^ Whether the second step can be featured.
4747

48-
-- | The extra amount locked to guard against the transfer fees changing between
49-
-- the time of locking the amulet and the time of executing the actual transfer.
50-
feeReserveMultiplier : Decimal
51-
feeReserveMultiplier = 4.0
52-
5348
-- | Converting a set of holding inputs to inputs for an amulet transfer,
5449
-- unlocking any expired LockedAmulet holdings on the fly.
55-
holdingToTransferInputs : ForOwner -> PaymentTransferContext -> [ContractId Holding] -> Update [TransferInput]
56-
holdingToTransferInputs forOwner paymentContext inputHoldingCids =
50+
holdingToTransferInputs : ForOwner -> Either PaymentTransferContext ExternalPartyPaymentTransferContext -> [ContractId Holding] -> Update [TransferInput]
51+
holdingToTransferInputs forOwner paymentContextE inputHoldingCids =
5752
forA inputHoldingCids $ \holdingCid -> do
5853
holding <- fetchCheckedInterface @Holding forOwner holdingCid
5954
case fromInterface holding of
6055
Some (LockedAmulet {}) -> do
6156
let lockedAmuletCid : ContractId LockedAmulet = fromInterfaceContractId holdingCid
6257
-- 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
58+
case paymentContextE of
59+
Left paymentContext -> do
60+
result <- exercise lockedAmuletCid LockedAmulet_OwnerExpireLock with
61+
openRoundCid = paymentContext.context.openMiningRound
62+
pure $ InputAmulet result.amuletSum.amulet
63+
Right externalPartyPaymentContext -> do
64+
result <- exercise lockedAmuletCid LockedAmulet_ExternalPartyOwnerExpireLock with
65+
externalPartyConfigStateCid = externalPartyPaymentContext.context.externalPartyConfigState
66+
pure $ InputAmulet result.amuletSum.amulet
6667
None -> pure $ InputAmulet $ coerceContractId holdingCid
6768

6869
-- | Prepare a two-step transfer of amulet by locking the funds.
6970
prepareTwoStepTransfer
70-
: TwoStepTransfer -> Time -> [ContractId Holding] -> PaymentTransferContext
71+
: TwoStepTransfer -> Time -> [ContractId Holding] -> Either PaymentTransferContext ExternalPartyPaymentTransferContext
7172
-> Update (ContractId LockedAmulet, [ContractId Holding], Metadata)
7273
prepareTwoStepTransfer TwoStepTransfer{..} requestedAt inputHoldingCids paymentContext = do
7374
require "requestedAt < transferBefore" (requestedAt < transferBefore)
74-
-- 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 feesReserveAmount = expectedTransferFees * feeReserveMultiplier
82-
8375
-- lock the amulet
8476
transferInputs <- holdingToTransferInputs (ForOwner with dso; owner = sender) paymentContext inputHoldingCids
8577
let transfer = Splice.AmuletRules.Transfer with
@@ -88,8 +80,8 @@ prepareTwoStepTransfer TwoStepTransfer{..} requestedAt inputHoldingCids paymentC
8880
outputs =
8981
[ TransferOutput with
9082
receiver = sender
91-
amount = amount + feesReserveAmount
92-
receiverFeeRatio = 0.0 -- locking fees are paid by the sender
83+
amount = amount
84+
receiverFeeRatio = 0.0 -- irrelevant as fees are always zero
9385
lock = Some TimeLock with
9486
expiresAt = transferBefore
9587
holders = [dso]
@@ -98,7 +90,7 @@ prepareTwoStepTransfer TwoStepTransfer{..} requestedAt inputHoldingCids paymentC
9890
inputs = transferInputs
9991
beneficiaries = None
10092

101-
result <- exercisePaymentTransfer dso paymentContext transfer
93+
result <- exerciseExternalPartyPaymentTransferWithFallback dso paymentContext transfer
10294
let [TransferResultLockedAmulet lockedAmulet] = result.createdAmulets
10395
pure
10496
( lockedAmulet
@@ -121,9 +113,8 @@ executeTwoStepTransfer TwoStepTransfer{..} lockedAmuletCid extraArgs = do
121113
-- ignore beneficiaries in case we are not allowing featuring
122114
context <- unfeaturedPaymentContextFromChoiceContext dso extraArgs.context
123115
pure (context, None)
124-
let openRoundCid = paymentContext.context.openMiningRound
125116
-- unlock amulet
126-
unlockResult <- exercise lockedAmuletCid LockedAmulet_Unlock with openRoundCid
117+
unlockResult <- unlockAmulet paymentContext lockedAmuletCid
127118
let amuletCid = unlockResult.amuletSum.amulet
128119
-- execute transfer
129120
let receiverOutput = TransferOutput with
@@ -137,7 +128,7 @@ executeTwoStepTransfer TwoStepTransfer{..} lockedAmuletCid extraArgs = do
137128
inputs = [InputAmulet amuletCid]
138129
outputs = [receiverOutput]
139130
beneficiaries
140-
result <- exercisePaymentTransfer dso paymentContext amuletRulesTransfer
131+
result <- exerciseExternalPartyPaymentTransferWithFallback dso paymentContext amuletRulesTransfer
141132
pure
142133
( optionalToList (toInterfaceContractId <$> result.senderChangeAmulet)
143134
, createdAmuletToHolding <$> result.createdAmulets
@@ -149,6 +140,7 @@ abortTwoStepTransfer TwoStepTransfer{..} lockedAmuletCid extraArgs = do
149140
expireLockedAmulet <- getFromContextU @Bool extraArgs.context expireLockKey
150141
if expireLockedAmulet
151142
then do
143+
-- FIXME: switch this to ExternalPartyConfigState
152144
openRoundCid <- getFromContextU @(ContractId OpenMiningRound) extraArgs.context openRoundContextKey
153145
-- prudent engineering: check the DSO party
154146
_ <- fetchChecked (ForDso with dso) openRoundCid
@@ -159,3 +151,10 @@ abortTwoStepTransfer TwoStepTransfer{..} lockedAmuletCid extraArgs = do
159151
assertDeadlineExceeded transferBeforeDeadline transferBefore
160152
pure []
161153

154+
unlockAmulet : Either PaymentTransferContext ExternalPartyPaymentTransferContext -> ContractId LockedAmulet -> Update LockedAmulet_UnlockResult
155+
unlockAmulet transferContextE lockedAmuletCid = do
156+
case transferContextE of
157+
Left paymentContext -> do
158+
exercise lockedAmuletCid LockedAmulet_Unlock with openRoundCid = paymentContext.context.openMiningRound
159+
Right externalPartyPaymentContext -> do
160+
exercise lockedAmuletCid LockedAmulet_ExternalPartyUnlock with externalPartyConfigStateCid = externalPartyPaymentContext.context.externalPartyConfigState

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
module Splice.AmuletConfig where
55

6+
import DA.Optional
67
import DA.Time
78

89
import Splice.Fees
@@ -45,8 +46,12 @@ data AmuletConfig unit = AmuletConfig with
4546
-- that should be used for command submissions.
4647
transferPreapprovalFee : Optional Decimal -- ^ Fee for keeping a transfer pre-approval around.
4748
featuredAppActivityMarkerAmount : Optional Decimal -- ^ $-amount used for the conversion from FeaturedAppActivityMarker -> AppRewardCoupon
49+
externalPartyConfigStateTickDuration : Optional RelTime
4850
deriving (Eq, Show)
4951

52+
getExternalPartyConfigStateTickDuration : AmuletConfig a -> RelTime
53+
getExternalPartyConfigStateTickDuration config = fromOptional (hours 24) config.externalPartyConfigStateTickDuration
54+
5055
-- $1/year specified as a daily rate
5156
defaultTransferPreapprovalFee : Decimal
5257
defaultTransferPreapprovalFee = 0.00274
@@ -69,7 +74,8 @@ validAmuletConfig AmuletConfig
6974
tickDuration > days 0 &&
7075
validPackageConfig packageConfig &&
7176
optional True (>= 0.0) transferPreapprovalFee &&
72-
optional True (>= 0.0) featuredAppActivityMarkerAmount
77+
optional True (>= 0.0) featuredAppActivityMarkerAmount &&
78+
featuredAppActivityMarkerAmount == Some transferConfig.extraFeaturedAppRewardAmount
7379

7480
validTransferConfig : TransferConfig unit -> Bool
7581
validTransferConfig TransferConfig
@@ -83,10 +89,11 @@ validTransferConfig TransferConfig
8389
, maxNumLockHolders
8490
}
8591
=
86-
positiveFixedFee createFee &&
92+
createFee == FixedFee 0.0 &&
8793
positiveRatePerRound holdingFee &&
88-
validSteppedRate transferFee &&
89-
positiveFixedFee lockHolderFee &&
94+
transferFee.initialRate == 0.0 &&
95+
all (\(_, r) -> r == 0.0) transferFee.steps &&
96+
lockHolderFee == FixedFee 0.0 &&
9097
extraFeaturedAppRewardAmount >= 0.0 &&
9198
maxNumInputs >= 1 &&
9299
maxNumOutputs >= 1 &&
@@ -120,6 +127,7 @@ instance Patchable (AmuletConfig USD) where
120127
packageConfig = patch new.packageConfig base.packageConfig current.packageConfig
121128
transferPreapprovalFee = patch new.transferPreapprovalFee base.transferPreapprovalFee current.transferPreapprovalFee
122129
featuredAppActivityMarkerAmount = patch new.featuredAppActivityMarkerAmount base.featuredAppActivityMarkerAmount current.featuredAppActivityMarkerAmount
130+
externalPartyConfigStateTickDuration = patch new.externalPartyConfigStateTickDuration base.externalPartyConfigStateTickDuration current.externalPartyConfigStateTickDuration
123131

124132
instance Patchable (TransferConfig USD) where
125133
patch new base current = TransferConfig with

0 commit comments

Comments
 (0)