@@ -23,6 +23,7 @@ import DA.Time
2323import Splice.Api.FeaturedAppRightV1 (AppRewardBeneficiary(..))
2424import Splice.Api.Token.MetadataV1 as Api.Token.MetadataV1
2525import Splice.Api.Token.HoldingV1 qualified as Api.Token.HoldingV1
26+ import Splice.Api.Token.TransferInstructionV1 qualified as Api.Token.TransferInstructionV1
2627import Splice.Api.Token.TransferPreapprovalV1 qualified as Api.Token.TransferPreapprovalV1
2728import Splice.Amulet
2829import Splice.Amulet.TokenApiUtils
@@ -1477,6 +1478,16 @@ template TransferPreapproval
14771478 transferPreapproval_withdrawImpl _self _arg = error "implement"
14781479 transferPreapproval_rejectImpl _self _arg = error "implement"
14791480
1481+ interface instance Api.Token.TransferInstructionV1.TransferFactory for TransferPreapproval where
1482+ view = Api.Token.TransferInstructionV1.TransferFactoryView with
1483+ admin = dso
1484+ meta = emptyMetadata
1485+
1486+ transferFactory_transferImpl _self _arg = error "implement"
1487+ transferFactory_publicFetchImpl _self arg = do
1488+ requireExpectedAdminMatch arg.expectedAdmin dso
1489+ pure (view $ toInterface @Api.Token.TransferInstructionV1.TransferFactory this)
1490+
14801491
14811492 nonconsuming choice TransferPreapproval_Fetch : TransferPreapproval
14821493 with
@@ -1662,3 +1673,85 @@ unfeaturedPaymentContextFromChoiceContext dso choiceContext = do
16621673 pure PaymentTransferContext with
16631674 amuletRules
16641675 context = context with featuredAppRight = None
1676+
1677+
1678+ -- Token standard admin checks
1679+ ------------------------------
1680+
1681+ -- FIXME: push to utils
1682+ requireExpectedAdminMatch : Party -> Party -> Update ()
1683+ requireExpectedAdminMatch expected actual = require ("Expected admin " <> show expected <> " matches actual admin " <> show actual) (expected == actual)
1684+
1685+
1686+ -- Transfer
1687+ -----------
1688+
1689+ -- FIXME: adjust sectioning and sharing with external party amulet rules
1690+
1691+ tokenStdTransfer
1692+ : Party
1693+ -> Api.Token.TransferInstructionV1.Transfer
1694+ -> Api.Token.MetadataV1.ExtraArgs
1695+ -> Update Api.Token.TransferInstructionV1.TransferInstructionResult
1696+ tokenStdTransfer dso transfer extraArgs = do
1697+ -- == validate each field of the transfer specification ==
1698+ -- sender: nothing to validate
1699+ -- receiver: validate preapproval if given
1700+ optPreapprovalCid <- lookupFromContextU @(ContractId TransferPreapproval) extraArgs.context transferPreapprovalContextKey
1701+ forA_ optPreapprovalCid \preapprovalCid ->
1702+ fetchChecked (ForOwner with dso; owner = transfer.receiver) preapprovalCid
1703+ -- instrumentId:
1704+ let expectedInstrumentId = amuletInstrumentId dso
1705+ require
1706+ ("Expected instrumentId " <> show expectedInstrumentId <> " matches actual instrumentId " <> show transfer.instrumentId)
1707+ (expectedInstrumentId == transfer.instrumentId)
1708+ -- amount:
1709+ require "Amount must be positive" (transfer.amount > 0.0)
1710+ -- requestedAt:
1711+ assertDeadlineExceeded "Transfer.requestedAt" transfer.requestedAt
1712+ -- executeBefore:
1713+ assertWithinDeadline "Transfer.executeBefore" transfer.executeBefore
1714+ -- inputHoldingCids: note that their detailed validation is done in the transfer itself
1715+ require "At least one holding must be provided" (not $ null transfer.inputHoldingCids)
1716+
1717+ -- use a payment context with featuring so the preapproval provider can be featured
1718+ paymentContext <- paymentFromChoiceContext dso extraArgs.context
1719+ -- execute a direct transfer
1720+ -- inputs <- holdingToTransferInputs (ForOwner with dso; owner = transfer.sender) paymentContext transfer.inputHoldingCids
1721+ -- result <- exercise preapprovalCid TransferPreapproval_Send
1722+ -- with
1723+ -- sender = transfer.sender
1724+ -- context = paymentContext
1725+ -- inputs
1726+ -- amount = transfer.amount
1727+ -- description = reason
1728+
1729+ assertWithinDeadline "TransferPreapproval.expiresAt" expiresAt
1730+ transferResult <- exercisePaymentTransfer dso context Transfer with
1731+ sender
1732+ provider
1733+ inputs
1734+ outputs =
1735+ [ TransferOutput with
1736+ receiver
1737+ receiverFeeRatio = 0.0
1738+ amount = amount
1739+ lock = None
1740+ ]
1741+ beneficiaries = None
1742+ -- We don't make this configurable. Rewards should
1743+ -- go to the party hosting the receiver. Allowing the sender
1744+ -- to configure arbitrary beneficiaries doesn't make sense.
1745+ -- If needed, we could extend preapprovals to track beneficiaries later.
1746+ let meta = optionalMetadata reasonMetaKey identity description (fromOptional emptyMetadata transferResult.meta).values
1747+ -- strip metadata to avoid duplicating it needlessly
1748+ let result = transferResult with meta = None
1749+ pure (TransferPreapproval_SendResult result (Some (Metadata meta)))
1750+
1751+
1752+ -- return result
1753+ pure Api.Token.TransferInstructionV1.TransferInstructionResult with
1754+ senderChangeCids = toInterfaceContractId <$> optionalToList result.result.senderChangeAmulet
1755+ output = Api.Token.TransferInstructionV1.TransferInstructionResult_Completed with
1756+ receiverHoldingCids = createdAmuletToHolding <$> result.result.createdAmulets
1757+ meta = copyOnlyBurnMeta result.meta
0 commit comments