Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ lazy val root: Project = (project in file("."))
`splice-api-token-metadata-v1-daml`,
`splice-api-token-holding-v1-daml`,
`splice-api-token-transfer-instruction-v1-daml`,
`splice-api-token-transfer-preapproval-v1-daml`,
`splice-api-token-allocation-v1-daml`,
`splice-api-token-allocation-request-v1-daml`,
`splice-api-token-allocation-instruction-v1-daml`,
Expand Down Expand Up @@ -231,6 +232,7 @@ lazy val docs = project
(`splice-api-token-metadata-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-holding-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-transfer-instruction-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-transfer-preapproval-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-allocation-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-allocation-request-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-allocation-instruction-v1-daml` / Compile / damlBuild).value ++
Expand Down Expand Up @@ -377,6 +379,33 @@ lazy val `splice-api-token-transfer-instruction-v1-daml` =
)
.dependsOn(`canton-bindings-java`)

lazy val `splice-api-token-transfer-preapproval-v1-daml` =
project
.in(file("token-standard/splice-api-token-transfer-preapproval-v1"))
.enablePlugins(DamlPlugin)
.settings(
BuildCommon.damlSettings,
Compile / damlDependencies :=
(`splice-api-token-metadata-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-holding-v1-daml` / Compile / damlBuild).value,
templateDirectory := (`openapi-typescript-template` / patchTemplate).value,
Compile / sourceGenerators +=
Def.taskDyn {
val transferPreapprovalOpenApiFile =
baseDirectory.value / "openapi/transfer-preapproval-v1.yaml"

BuildCommon.TS.generateOpenApiClient(
unscopedNpmName = "transfer-preapproval-openapi",
openApiSpec = "transfer-preapproval-v1.yaml",
cacheFileDependencies = Set(transferPreapprovalOpenApiFile),
directory = "openapi-ts-client",
subPath = "openapi",
)
},
cleanFiles += { baseDirectory.value / "openapi-ts-client" },
)
.dependsOn(`canton-bindings-java`)

lazy val `splice-api-token-allocation-v1-daml` =
project
.in(file("token-standard/splice-api-token-allocation-v1"))
Expand Down Expand Up @@ -676,6 +705,7 @@ lazy val `splice-amulet-daml` =
(`splice-api-token-metadata-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-holding-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-transfer-instruction-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-transfer-preapproval-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-allocation-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-allocation-request-v1-daml` / Compile / damlBuild).value ++
(`splice-api-token-allocation-instruction-v1-daml` / Compile / damlBuild).value ++
Expand Down
1 change: 1 addition & 0 deletions daml/splice-amulet/daml.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ data-dependencies:
- ../../token-standard/splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar
- ../../token-standard/splice-api-token-holding-v1/.daml/dist/splice-api-token-holding-v1-current.dar
- ../../token-standard/splice-api-token-transfer-instruction-v1/.daml/dist/splice-api-token-transfer-instruction-v1-current.dar
- ../../token-standard/splice-api-token-transfer-preapproval-v1/.daml/dist/splice-api-token-transfer-preapproval-v1-current.dar
- ../../token-standard/splice-api-token-allocation-v1/.daml/dist/splice-api-token-allocation-v1-current.dar
- ../../token-standard/splice-api-token-allocation-instruction-v1/.daml/dist/splice-api-token-allocation-instruction-v1-current.dar
- ../splice-util/.daml/dist/splice-util-current.dar
Expand Down
18 changes: 0 additions & 18 deletions daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ module Splice.Amulet.TwoStepTransfer (
prepareTwoStepTransfer,
executeTwoStepTransfer,
abortTwoStepTransfer,

-- * Shared support code
holdingToTransferInputs,
) where

import DA.Assert
Expand Down Expand Up @@ -50,21 +47,6 @@ data TwoStepTransfer = TwoStepTransfer with
feeReserveMultiplier : Decimal
feeReserveMultiplier = 4.0

-- | Converting a set of holding inputs to inputs for an amulet transfer,
-- unlocking any expired LockedAmulet holdings on the fly.
holdingToTransferInputs : ForOwner -> PaymentTransferContext -> [ContractId Holding] -> Update [TransferInput]
holdingToTransferInputs forOwner paymentContext inputHoldingCids =
forA inputHoldingCids $ \holdingCid -> do
holding <- fetchCheckedInterface @Holding forOwner holdingCid
case fromInterface holding of
Some (LockedAmulet {}) -> do
let lockedAmuletCid : ContractId LockedAmulet = fromInterfaceContractId holdingCid
-- We assume the lock is expired, and if not then we rely `LockedAmulet_OwnerExpireLock` to fail
result <- exercise lockedAmuletCid LockedAmulet_OwnerExpireLock with
openRoundCid = paymentContext.context.openMiningRound
pure $ InputAmulet result.amuletSum.amulet
None -> pure $ InputAmulet $ coerceContractId holdingCid

-- | Prepare a two-step transfer of amulet by locking the funds.
prepareTwoStepTransfer
: TwoStepTransfer -> Time -> [ContractId Holding] -> PaymentTransferContext
Expand Down
139 changes: 139 additions & 0 deletions daml/splice-amulet/daml/Splice/AmuletRules.daml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import DA.Time
import Splice.Api.FeaturedAppRightV1 (AppRewardBeneficiary(..))
import Splice.Api.Token.MetadataV1 as Api.Token.MetadataV1
import Splice.Api.Token.HoldingV1 qualified as Api.Token.HoldingV1
import Splice.Api.Token.TransferInstructionV1 qualified as Api.Token.TransferInstructionV1
import Splice.Api.Token.TransferPreapprovalV1 qualified as Api.Token.TransferPreapprovalV1
import Splice.Amulet
import Splice.Amulet.TokenApiUtils
import Splice.AmuletConfig (AmuletConfig(..), TransferConfig(..), validAmuletConfig, defaultTransferPreapprovalFee)
Expand Down Expand Up @@ -1457,13 +1459,51 @@ template TransferPreapproval
signatory receiver, provider, dso
ensure (expiresAt > validFrom) && (lastRenewedAt >= validFrom)


interface instance Api.Token.TransferPreapprovalV1.TransferPreapproval for TransferPreapproval where
view = Api.Token.TransferPreapprovalV1.TransferPreapprovalView with
specification = Api.Token.TransferPreapprovalV1.PreapprovalSpecification with
-- FIXME: add support for the extended config
admin = dso
receiver
senderFilter = []
idFilter = []
minAmount = None
maxAmount = None
walletProvider = provider
status = Api.Token.TransferPreapprovalV1.PS_Active
requestedAt = lastRenewedAt
expiresAt
meta = emptyMetadata

-- FIXME
transferPreapproval_renewImpl _self _arg = error "implement"
transferPreapproval_acceptImpl _self _arg = error "implement"
transferPreapproval_cancelImpl _self _arg = error "implement"

interface instance Api.Token.TransferInstructionV1.TransferFactory for TransferPreapproval where
view = Api.Token.TransferInstructionV1.TransferFactoryView with
admin = dso
meta = emptyMetadata

transferFactory_transferImpl _self arg = do
requireExpectedAdminMatch arg.expectedAdmin dso
tokenStdTransfer dso provider arg.transfer arg.extraArgs

transferFactory_publicFetchImpl _self arg = do
requireExpectedAdminMatch arg.expectedAdmin dso
pure (view $ toInterface @Api.Token.TransferInstructionV1.TransferFactory this)


nonconsuming choice TransferPreapproval_Fetch : TransferPreapproval
with
p : Party
controller p
do pure this

-- Transfer amulet to the receiver
--
-- DEPRECATED: use the `TransferFactory_Transfer` method from the `TransferFactory` interface instead
nonconsuming choice TransferPreapproval_Send : TransferPreapproval_SendResult
with
context : PaymentTransferContext
Expand Down Expand Up @@ -1641,3 +1681,102 @@ unfeaturedPaymentContextFromChoiceContext dso choiceContext = do
pure PaymentTransferContext with
amuletRules
context = context with featuredAppRight = None


-- Token standard admin checks
------------------------------

-- FIXME: push to utils
requireExpectedAdminMatch : Party -> Party -> Update ()
requireExpectedAdminMatch expected actual = require ("Expected admin " <> show expected <> " matches actual admin " <> show actual) (expected == actual)


-- Transfer
-----------

-- FIXME: adjust sectioning and sharing with external party amulet rules

tokenStdTransfer
: Party
-> Party
-> Api.Token.TransferInstructionV1.Transfer
-> Api.Token.MetadataV1.ExtraArgs
-> Update Api.Token.TransferInstructionV1.TransferInstructionResult
tokenStdTransfer dso provider transfer extraArgs = do
-- == validate each field of the transfer specification ==
-- sender: nothing to validate
-- receiver: validate preapproval if given
optPreapprovalCid <- lookupFromContextU @(ContractId TransferPreapproval) extraArgs.context transferPreapprovalContextKey
forA_ optPreapprovalCid \preapprovalCid ->
fetchChecked (ForOwner with dso; owner = transfer.receiver) preapprovalCid
-- instrumentId:
let expectedInstrumentId = amuletInstrumentId dso
require
("Expected instrumentId " <> show expectedInstrumentId <> " matches actual instrumentId " <> show transfer.instrumentId)
(expectedInstrumentId == transfer.instrumentId)
-- amount:
require "Amount must be positive" (transfer.amount > 0.0)
-- requestedAt:
assertDeadlineExceeded "Transfer.requestedAt" transfer.requestedAt
-- executeBefore:
assertWithinDeadline "Transfer.executeBefore" transfer.executeBefore
-- inputHoldingCids: note that their detailed validation is done in the transfer itself
require "At least one holding must be provided" (not $ null transfer.inputHoldingCids)

-- use a payment context with featuring so the preapproval provider can be featured
paymentContext <- paymentFromChoiceContext dso extraArgs.context
-- execute a direct transfer
inputs <- holdingToTransferInputs (ForOwner with dso; owner = transfer.sender) paymentContext transfer.inputHoldingCids
-- result <- exercise preapprovalCid TransferPreapproval_Send
-- with
-- sender = transfer.sender
-- context = paymentContext
-- inputs
-- amount = transfer.amount
-- description = reason

-- FIXME: enforce
-- assertWithinDeadline "TransferPreapproval.expiresAt" expiresAt

result <- exercisePaymentTransfer dso paymentContext Transfer with
sender = transfer.sender
provider
inputs
outputs =
[ TransferOutput with
receiver = transfer.receiver
receiverFeeRatio = 0.0
amount = transfer.amount
lock = None
]
beneficiaries = None
-- We don't make this configurable. Rewards should
-- go to the party hosting the receiver. Allowing the sender
-- to configure arbitrary beneficiaries doesn't make sense.
-- If needed, we could extend preapprovals to track beneficiaries later.
-- let meta = optionalMetadata reasonMetaKey identity description (fromOptional emptyMetadata transferResult.meta).values
-- strip metadata to avoid duplicating it needlessly
-- pure (TransferPreapproval_SendResult result (Some (Metadata meta)))


-- return result
pure Api.Token.TransferInstructionV1.TransferInstructionResult with
senderChangeCids = toInterfaceContractId <$> optionalToList result.senderChangeAmulet
output = Api.Token.TransferInstructionV1.TransferInstructionResult_Completed with
receiverHoldingCids = createdAmuletToHolding <$> result.createdAmulets
meta = copyOnlyBurnMeta result.meta

-- | Converting a set of holding inputs to inputs for an amulet transfer,
-- unlocking any expired LockedAmulet holdings on the fly.
holdingToTransferInputs : ForOwner -> PaymentTransferContext -> [ContractId Api.Token.HoldingV1.Holding] -> Update [TransferInput]
holdingToTransferInputs forOwner paymentContext inputHoldingCids =
forA inputHoldingCids $ \holdingCid -> do
holding <- fetchCheckedInterface @Api.Token.HoldingV1.Holding forOwner holdingCid
case fromInterface holding of
Some (LockedAmulet {}) -> do
let lockedAmuletCid : ContractId LockedAmulet = fromInterfaceContractId holdingCid
-- We assume the lock is expired, and if not then we rely `LockedAmulet_OwnerExpireLock` to fail
result <- exercise lockedAmuletCid LockedAmulet_OwnerExpireLock with
openRoundCid = paymentContext.context.openMiningRound
pure $ InputAmulet result.amuletSum.amulet
None -> pure $ InputAmulet $ coerceContractId holdingCid
16 changes: 14 additions & 2 deletions daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ instance HasCheckedFetch TransferCommandCounter ForOwner where
-- We externally sign this instead of the transfer itself to support a longer delay between
-- prepare/execute which would be prevented by signing the transfer directly as that one pins
-- down the mining rounds that are relatively short lived.
--
-- DEPRECATED: in favor of token standard transfers.
template TransferCommand
with
dso : Party
Expand Down Expand Up @@ -187,6 +189,18 @@ template TransferCommand
result <-
try do
TransferPreapproval_SendResult result _meta <- exercise transferPreapprovalCid (TransferPreapproval_Send context inputs amount sender description)
{-
let transfer = Api.Token.TransferInstructionV1.Transfer with

result <- exercise transferPreapprovalCid Api.Token.TransferInstructionV1.TransferFactory_Transfer with
expectedAdmin = dso
transfer
extraArgs = ExtraArgs with

-- FIXME: test that reason arg is checked, and add it
meta = emptyMetadata
context =
-}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP

pure (TransferCommandResultSuccess result)
catch
(ex : InvalidTransfer) -> mergeInputsAndReportError ex.reason
Expand Down Expand Up @@ -407,5 +421,3 @@ amulet_allocationFactory_allocateImpl externalAmuletRules _self arg = do
output = AllocationInstructionResult_Completed with allocationCid
meta

requireExpectedAdminMatch : Party -> Party -> Update ()
requireExpectedAdminMatch expected actual = require ("Expected admin " <> show expected <> " matches actual admin " <> show actual) (expected == actual)
1 change: 1 addition & 0 deletions daml/splice-wallet/daml/Splice/Wallet/Install.daml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ executeAmuletOperationRec executionContext inputs prevResults (operation::remain
-- with that of the app provider hosting the receiver which must be passed as an argument.
let transferContext = context.context with featuredAppRight = providerFeaturedAppRightCid
let paymentTransferContext = context with context = transferContext
-- FIXME: switch to use token standard transers when possible
result <- catchAll $ exercise transferPreapprovalCid TransferPreapproval_Send with
sender = executionContext.endUser
context = paymentTransferContext
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
..
Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
..
SPDX-License-Identifier: Apache-2.0

splice-api-token-transfer-preapproval-v1 docs
=============================================

.. toctree::
:maxdepth: 3
:titlesonly:

{{{body}}}
19 changes: 19 additions & 0 deletions token-standard/splice-api-token-transfer-preapproval-v1/daml.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2
name: splice-api-token-transfer-preapproval-v1
version: 1.0.0
source: daml
dependencies:
- daml-prim
- daml-stdlib
data-dependencies:
- ../splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar
build-options:
- --target=2.1
codegen:
java:
package-prefix: org.lfdecentralizedtrust.splice.codegen.java
decoderClass: org.lfdecentralizedtrust.splice.codegen.java.DecoderSpliceApiTokenTransferPreapprovalV1
output-directory: target/scala-2.13/src_managed/main/daml-codegen-java
Loading
Loading