From eab44285e177d82fdf6d639de5bcdba16c21ce06 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Wed, 15 Oct 2025 08:59:57 +0000 Subject: [PATCH 01/10] sketch [no ci] --- .../daml.yaml | 21 ++ .../Api/Token/TransferPreapprovalV1.daml | 190 ++++++++++++ .../openapi/docker-compose.yml | 16 + .../openapi/transfer-preapproval-v1.yaml | 285 ++++++++++++++++++ 4 files changed, 512 insertions(+) create mode 100644 token-standard/splice-api-token-transfer-preapproval-v1/daml.yaml create mode 100644 token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml create mode 100644 token-standard/splice-api-token-transfer-preapproval-v1/openapi/docker-compose.yml create mode 100644 token-standard/splice-api-token-transfer-preapproval-v1/openapi/transfer-preapproval-v1.yaml diff --git a/token-standard/splice-api-token-transfer-preapproval-v1/daml.yaml b/token-standard/splice-api-token-transfer-preapproval-v1/daml.yaml new file mode 100644 index 0000000000..18cc97310f --- /dev/null +++ b/token-standard/splice-api-token-transfer-preapproval-v1/daml.yaml @@ -0,0 +1,21 @@ +# 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 +- ../splice-api-token-holding-v1/.daml/dist/splice-api-token-holding-v1-current.dar +- ../splice-api-token-holding-v1/.daml/dist/splice-api-token-transfer-instruction-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 diff --git a/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml b/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml new file mode 100644 index 0000000000..00a9ca5fbb --- /dev/null +++ b/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml @@ -0,0 +1,190 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- | Instruct transfers of holdings between parties. +module Splice.Api.Token.TransferPreapprovalV1 where + +import qualified DA.Map as Map + +import Splice.Api.Token.MetadataV1 +import Splice.Api.Token.HoldingV1 + +{- + +Ideas left out: +- network wide blanket approvals: + - tricky wrt confidentiality: DSO must only be an observer + - do not replace registry-specific preapprovals: registries want their + own preapprovals to support featured app markers and reward sharing + - more complex management: wallets need to understand them as distinct + from registry-specific preapprovals, and know how to allocate them and + how to display them. +- volume-bounded approvals: too complex to implement as they require mutable + on-ledger state, which likely results in contention + +- allowing to not create the intermediate TransferInstruction: + - not done to avoid + + + +-} + + +-- TransferPreapproval +------------------------ + +-- | Specification of which incoming transfers are pre-approved by a receiver. +data PreapprovalSpecification = PreapprovalSpecification with + admin : Party + -- ^ Admin party of the holidings whose transfers are preapproved. + receiver : Party + -- ^ Receiver that preapproves incoming transfers. + senderFilter : [Party] + -- ^ Optional filter for which parties are allowed to send funds. + -- Empty list means no filter is applied. + idFilter : [Text] + -- ^ Optional filter on the instrument identifiers that are preapproved. + -- Empty list means no filter is applied. + minAmount : Optional Decimal + -- ^ Optional lower bound on the amount whose transfer is preapproved. + maxAmount : Optional Decimal + -- ^ Optional upper bound on the amount whose transfer is preapproved. + + +-- | View for `TransferPreapproval`. +data TransferPreapprovalView = TransferPreapprovalView with + specification : PreapprovalSpecification + -- ^ Specification of which transfers are preapproved. + requestedAt : Time + -- ^ When the preapproval was requested to be created. + expiresAt : Time + -- ^ When the preapproval expires. + meta : Metadata + -- ^ Additional metadata specific to the preapproval, used for extensibility. + deriving (Show, Eq) + +-- | An interface for tracking the status of a transfer instruction, +-- i.e., a request to a registry app to execute a transfer. +-- +-- Registries MAY evolve the transfer instruction in multiple steps. They SHOULD +-- do so using only the choices on this interface, so that wallets can reliably +-- parse the transaction history and determine whether the instruction ultimately +-- succeeded or failed. +interface TransferPreapproval where + viewtype TransferPreapprovalView + + -- FIX + transferPreapproval_acceptImpl : ContractId TransferPreapproval -> TransferPreapproval_Accept -> Update TransferPreapprovalResult + transferPreapproval_rejectImpl : ContractId TransferPreapproval -> TransferPreapproval_Reject -> Update TransferPreapprovalResult + transferPreapproval_withdrawImpl : ContractId TransferPreapproval -> TransferPreapproval_Withdraw -> Update TransferPreapprovalResult + transferPreapproval_updateImpl : ContractId TransferPreapproval -> TransferPreapproval_Update -> Update TransferPreapprovalResult + +{- + nonconsuming choice TransferPreapproval_AcceptTransfer : TransferPreapprovalResult + -- ^ Allow the sender to accept a preapproved transfer in the name of the receiver. + with + sender : Party + -- ^ Sender of the transfer. + transferInstructionCid : ContractId TransferInstruction + -- ^ The transfer instruction contract to be accepted. + extraArgs : ExtraArgs + -- ^ Additional context required in order to exercise the choice. + controller sender + do transferPreapproval_acceptTransferImpl this self arg + + -- QUESTION: do we need this in addition to direct transfers? + -- transfer instruction? That would allow for smaller txs, but complicates tx + -- history parsing. +-} + + nonconsuming choice TransferPreapproval_Transfer : TransferPreapprovalResult + -- ^ Perform a preapproved direct transfer to the receiver. + with + transfer : Transfer + -- ^ The transfer instruction contract to be accepted. + extraArgs : ExtraArgs + -- ^ Additional context required in order to exercise the choice. + controller transfer.sender + do transferPreapproval_acceptTransferImpl this self arg + + + choice TransferPreapproval_Renew : TransferPreapprovalResult + -- ^ Renew the preapproval as the receiver. + -- + -- IMPL. IDEA: + -- + with + specification : PreapprovalSpecification + requestedAt : Time + expiresAt : Optional Time + extraArgs : ExtraArgs + -- ^ Additional context required in order to exercise the choice. + controller (view this).transfer.receiver + do transferPreapproval_renewImpl this self arg + + choice TransferPreapproval_Withdraw : TransferPreapprovalResult + -- ^ Withdraw the transfer preapproval. + with + extraArgs : ExtraArgs + -- ^ Additional context required in order to exercise the choice. + controller (view this).receiver + do transferPreapproval_withdrawImpl this self arg + + + +-- Preapproval Factory +---------------------- + +-- | A factory contract to create transfer preapprovals. +interface PreapprovalFactory where + viewtype PreapprovalFactoryView + + transferFactory_transferImpl : ContractId PreapprovalFactory -> PreapprovalFactory_Transfer -> Update TransferPreapprovalResult + transferFactory_publicFetchImpl : ContractId PreapprovalFactory -> PreapprovalFactory_PublicFetch -> Update PreapprovalFactoryView + + nonconsuming choice PreapprovalFactory_CreatePreapproval : TransferPreapprovalResult + -- ^ Create a new transfer preapproval. + -- Implementations MUST ensure that this choice fails if `transfer.executeBefore` is in the past. + -- + -- Implementations MAY limit the number of active preapprovals per receiver. + with + expectedAdmin : Party + -- ^ The expected admin party issuing the factory. Implementations MUST validate that this matches + -- the admin of the factory. + -- Callers SHOULD ensure they get `expectedAdmin` from a trusted source, e.g., a read against + -- their own participant. That way they can ensure that it is safe to exercise a choice + -- on a factory contract acquired from an untrusted source *provided* + -- all vetted Daml packages only contain interface implementations + -- that check the expected admin party. + preapproval : Preapproval + -- ^ The transfer to execute. + extraArgs : ExtraArgs + -- ^ The extra arguments to pass to the transfer implementation. + controller transfer.sender + do transferFactory_transferImpl this self arg + + nonconsuming choice PreapprovalFactory_PublicFetch : PreapprovalFactoryView + -- ^ Fetch the view of the factory contract. + with + expectedAdmin : Party + -- ^ The expected admin party issuing the factory. Implementations MUST validate that this matches + -- the admin of the factory. + -- Callers SHOULD ensure they get `expectedAdmin` from a trusted source, e.g., a read against + -- their own participant. That way they can ensure that it is safe to exercise a choice + -- on a factory contract acquired from an untrusted source *provided* + -- all vetted Daml packages only contain interface implementations + -- that check the expected admin party. + actor : Party + -- ^ The party fetching the contract. + controller actor + do transferFactory_publicFetchImpl this self arg + +-- | View for `PreapprovalFactory`. +data PreapprovalFactoryView = PreapprovalFactoryView + with + admin : Party + -- ^ The party representing the registry app that administers the instruments for + -- which this transfer factory can be used. + meta : Metadata + -- ^ Additional metadata specific to the transfer factory, used for extensibility. + deriving (Show, Eq) diff --git a/token-standard/splice-api-token-transfer-preapproval-v1/openapi/docker-compose.yml b/token-standard/splice-api-token-transfer-preapproval-v1/openapi/docker-compose.yml new file mode 100644 index 0000000000..b62e0dd5dc --- /dev/null +++ b/token-standard/splice-api-token-transfer-preapproval-v1/openapi/docker-compose.yml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Description: Docker compose file for running Swagger UI with the transfer-instruction OpenAPI specification. +# Usage: docker-compose up +version: '3.7' + +services: + swagger-ui: + image: swaggerapi/swagger-ui + ports: + - "8080:8080" + environment: + SWAGGER_JSON: /spec/transfer-preapproval.yaml + volumes: + - ./transfer-instruction.yaml:/spec/transfer-preapproval.yaml diff --git a/token-standard/splice-api-token-transfer-preapproval-v1/openapi/transfer-preapproval-v1.yaml b/token-standard/splice-api-token-transfer-preapproval-v1/openapi/transfer-preapproval-v1.yaml new file mode 100644 index 0000000000..48104f96fe --- /dev/null +++ b/token-standard/splice-api-token-transfer-preapproval-v1/openapi/transfer-preapproval-v1.yaml @@ -0,0 +1,285 @@ +# Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +openapi: 3.0.0 +info: + title: transfer instruction off-ledger API + description: | + Implemented by token registries for the purpose of supporting the initiation + of asset transfers; e.g. to settle off-ledger obligations. + version: 1.0.0 +paths: + + /registry/transfer-instruction/v1/transfer-factory: + post: + operationId: "getTransferFactory" + description: | + Get the factory and choice context for executing a direct transfer. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GetFactoryRequest" + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "#/components/schemas/TransferFactoryWithChoiceContext" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + + + /registry/transfer-instruction/v1/{transferInstructionId}/choice-contexts/accept: + post: + operationId: "getTransferInstructionAcceptContext" + description: | + Get the choice context to accept a transfer instruction. + parameters: + - name: transferInstructionId + description: "The contract ID of the transfer instruction to accept." + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GetChoiceContextRequest" + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "#/components/schemas/ChoiceContext" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + + /registry/transfer-instruction/v1/{transferInstructionId}/choice-contexts/reject: + post: + operationId: "getTransferInstructionRejectContext" + description: | + Get the choice context to reject a transfer instruction. + parameters: + - name: transferInstructionId + description: "The contract ID of the transfer instruction to reject." + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GetChoiceContextRequest" + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "#/components/schemas/ChoiceContext" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + + /registry/transfer-instruction/v1/{transferInstructionId}/choice-contexts/withdraw: + post: + operationId: "getTransferInstructionWithdrawContext" + description: | + Get the choice context to withdraw a transfer instruction. + parameters: + - name: transferInstructionId + description: "The contract ID of the transfer instruction to withdraw." + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GetChoiceContextRequest" + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "#/components/schemas/ChoiceContext" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + +components: + responses: + "400": + description: "bad request" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "404": + description: "not found" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + schemas: + # Note: intentionally not shared with the other APIs to keep the self-contained, and because not all OpenAPI codegens support such shared definitions. + GetFactoryRequest: + type: object + properties: + choiceArguments: + type: object + description: | + The arguments that are intended to be passed to the choice provided by the factory. + To avoid repeating the Daml type definitions, they are specified as JSON objects. + However the concrete format is given by how the choice arguments are encoded using the Daml JSON API + (with the `extraArgs.context` and `extraArgs.meta` fields set to the empty object). + + The choice arguments are provided so that the registry can also provide choice-argument + specific contracts, e.g., the configuration for a specific instrument-id. + excludeDebugFields: + description: "If set to true, the response will not include debug fields." + default: false + type: boolean + required: + [ + "choiceArguments", + ] + + GetChoiceContextRequest: + description: | + A request to get the context for executing a choice on a contract. + type: object + properties: + meta: + description: | + Metadata that will be passed to the choice, and should be incorporated + into the choice context. Provided for extensibility. + type: object + additionalProperties: + type: string + + TransferFactoryWithChoiceContext: + description: | + The transfer factory contract together with the choice context required to exercise the choice + provided by the factory. Typically used to implement the generic initiation of on-ledger workflows + via a Daml interface. + + Clients SHOULD avoid reusing the same `FactoryWithChoiceContext` for exercising multiple choices, + as the choice context MAY be specific to the choice being exercised. + type: object + properties: + factoryId: + description: "The contract ID of the contract implementing the factory interface." + type: string + transferKind: + description: | + The kind of transfer workflow that will be used: + * `offer`: offer a transfer to the receiver and only transfer if they accept + * `direct`: transfer directly to the receiver without asking them for approval. + Only chosen if the receiver has pre-approved direct transfers. + * `self`: a self-transfer where the sender and receiver are the same party. + No approval is required, and the transfer is typically immediate. + type: string + enum: + - "self" + - "direct" + - "offer" + choiceContext: + $ref: "#/components/schemas/ChoiceContext" + required: + [ + "factoryId", + "choiceContext", + "transferKind", + ] + + ChoiceContext: + description: | + The context required to exercise a choice on a contract via an interface. + Used to retrieve additional reference data that is passed in via disclosed contracts, + which are in turn referred to via their contract ID in the `choiceContextData`. + type: object + properties: + choiceContextData: + description: "The additional data to use when exercising the choice." + type: object + disclosedContracts: + description: | + The contracts that are required to be disclosed to the participant node for exercising + the choice. + type: array + items: + $ref: "#/components/schemas/DisclosedContract" + required: + [ + "choiceContextData", + "disclosedContracts", + ] + + # Note: intentionally not shared with the other APIs to keep the self-contained, and because not all OpenAPI codegens support such shared definitions. + DisclosedContract: + type: object + properties: + templateId: + type: string + contractId: + type: string + createdEventBlob: + type: string + synchronizerId: + description: | + The synchronizer to which the contract is currently assigned. + If the contract is in the process of being reassigned, then a "409" response is returned. + type: string + debugPackageName: + description: | + The name of the Daml package that was used to create the contract. + Use this data only if you trust the provider, as it might not match the data in the + `createdEventBlob`. + type: string + debugPayload: + description: | + The contract arguments that were used to create the contract. + Use this data only if you trust the provider, as it might not match the data in the + `createdEventBlob`. + type: object + debugCreatedAt: + description: | + The ledger effective time at which the contract was created. + Use this data only if you trust the provider, as it might not match the data in the + `createdEventBlob`. + type: string + format: date-time + required: + [ + "templateId", + "contractId", + "createdEventBlob", + "synchronizerId" + ] + + ErrorResponse: + type: object + required: + - error + properties: + error: + type: string From 77836c3f230d1bb1d730f56574b2382ac8d5432b Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Wed, 15 Oct 2025 14:14:41 +0000 Subject: [PATCH 02/10] daml interfaces compile --- build.sbt | 30 ++++ .../daml.yaml | 2 - .../Api/Token/TransferPreapprovalV1.daml | 132 +++++++++--------- 3 files changed, 97 insertions(+), 67 deletions(-) diff --git a/build.sbt b/build.sbt index 9253de6faf..c2d98e9705 100644 --- a/build.sbt +++ b/build.sbt @@ -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`, @@ -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 ++ @@ -377,6 +379,34 @@ 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, + // FIXME: enable when the OpenAPI spec is ready + // 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")) diff --git a/token-standard/splice-api-token-transfer-preapproval-v1/daml.yaml b/token-standard/splice-api-token-transfer-preapproval-v1/daml.yaml index 18cc97310f..bc2480cdbc 100644 --- a/token-standard/splice-api-token-transfer-preapproval-v1/daml.yaml +++ b/token-standard/splice-api-token-transfer-preapproval-v1/daml.yaml @@ -10,8 +10,6 @@ dependencies: - daml-stdlib data-dependencies: - ../splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar -- ../splice-api-token-holding-v1/.daml/dist/splice-api-token-holding-v1-current.dar -- ../splice-api-token-holding-v1/.daml/dist/splice-api-token-transfer-instruction-v1-current.dar build-options: - --target=2.1 codegen: diff --git a/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml b/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml index 00a9ca5fbb..2276014017 100644 --- a/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml +++ b/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml @@ -4,10 +4,8 @@ -- | Instruct transfers of holdings between parties. module Splice.Api.Token.TransferPreapprovalV1 where -import qualified DA.Map as Map import Splice.Api.Token.MetadataV1 -import Splice.Api.Token.HoldingV1 {- @@ -35,20 +33,21 @@ Ideas left out: -- | Specification of which incoming transfers are pre-approved by a receiver. data PreapprovalSpecification = PreapprovalSpecification with - admin : Party - -- ^ Admin party of the holidings whose transfers are preapproved. - receiver : Party - -- ^ Receiver that preapproves incoming transfers. - senderFilter : [Party] - -- ^ Optional filter for which parties are allowed to send funds. - -- Empty list means no filter is applied. - idFilter : [Text] - -- ^ Optional filter on the instrument identifiers that are preapproved. - -- Empty list means no filter is applied. - minAmount : Optional Decimal - -- ^ Optional lower bound on the amount whose transfer is preapproved. - maxAmount : Optional Decimal - -- ^ Optional upper bound on the amount whose transfer is preapproved. + admin : Party + -- ^ Admin party of the holidings whose transfers are preapproved. + receiver : Party + -- ^ Receiver that preapproves incoming transfers. + senderFilter : [Party] + -- ^ Optional filter for which parties are allowed to send funds. + -- Empty list means no filter is applied. + idFilter : [Text] + -- ^ Optional filter on the instrument identifiers that are preapproved. + -- Empty list means no filter is applied. + minAmount : Optional Decimal + -- ^ Optional lower bound on the amount whose transfer is preapproved. + maxAmount : Optional Decimal + -- ^ Optional upper bound on the amount whose transfer is preapproved. + deriving (Eq, Show) -- | View for `TransferPreapproval`. @@ -61,7 +60,7 @@ data TransferPreapprovalView = TransferPreapprovalView with -- ^ When the preapproval expires. meta : Metadata -- ^ Additional metadata specific to the preapproval, used for extensibility. - deriving (Show, Eq) + deriving (Eq, Show) -- | An interface for tracking the status of a transfer instruction, -- i.e., a request to a registry app to execute a transfer. @@ -73,63 +72,66 @@ data TransferPreapprovalView = TransferPreapprovalView with interface TransferPreapproval where viewtype TransferPreapprovalView - -- FIX - transferPreapproval_acceptImpl : ContractId TransferPreapproval -> TransferPreapproval_Accept -> Update TransferPreapprovalResult - transferPreapproval_rejectImpl : ContractId TransferPreapproval -> TransferPreapproval_Reject -> Update TransferPreapprovalResult - transferPreapproval_withdrawImpl : ContractId TransferPreapproval -> TransferPreapproval_Withdraw -> Update TransferPreapprovalResult - transferPreapproval_updateImpl : ContractId TransferPreapproval -> TransferPreapproval_Update -> Update TransferPreapprovalResult - -{- - nonconsuming choice TransferPreapproval_AcceptTransfer : TransferPreapprovalResult - -- ^ Allow the sender to accept a preapproved transfer in the name of the receiver. - with - sender : Party - -- ^ Sender of the transfer. - transferInstructionCid : ContractId TransferInstruction - -- ^ The transfer instruction contract to be accepted. - extraArgs : ExtraArgs - -- ^ Additional context required in order to exercise the choice. - controller sender - do transferPreapproval_acceptTransferImpl this self arg - - -- QUESTION: do we need this in addition to direct transfers? - -- transfer instruction? That would allow for smaller txs, but complicates tx - -- history parsing. --} - - nonconsuming choice TransferPreapproval_Transfer : TransferPreapprovalResult - -- ^ Perform a preapproved direct transfer to the receiver. - with - transfer : Transfer - -- ^ The transfer instruction contract to be accepted. - extraArgs : ExtraArgs - -- ^ Additional context required in order to exercise the choice. - controller transfer.sender - do transferPreapproval_acceptTransferImpl this self arg + transferPreapproval_renewImpl : ContractId TransferPreapproval -> TransferPreapproval_Renew -> Update TransferPreapproval_RenewResult + transferPreapproval_withdrawImpl : ContractId TransferPreapproval -> TransferPreapproval_Withdraw -> Update ChoiceExecutionMetadata + transferPreapproval_rejectImpl : ContractId TransferPreapproval -> TransferPreapproval_Reject -> Update ChoiceExecutionMetadata - choice TransferPreapproval_Renew : TransferPreapprovalResult - -- ^ Renew the preapproval as the receiver. - -- - -- IMPL. IDEA: + choice TransferPreapproval_Renew : TransferPreapproval_RenewResult + -- ^ Renew the preapproval as the delegate or receiver. -- + -- Can also be used to update the specification and expiration time. with specification : PreapprovalSpecification + -- ^ New specification of which transfers are preapproved. requestedAt : Time + -- ^ When the preapproval was requested to be (re-)created. expiresAt : Optional Time + -- ^ New expiration time of the preapproval. + -- + -- Will be capped by the maximal expiration time allowed by the instrument admin; + -- and if not provided, the maximal expiration time will be used. extraArgs : ExtraArgs -- ^ Additional context required in order to exercise the choice. - controller (view this).transfer.receiver + -- + -- FIXME: decide whether we really want to the full extra context here? + controller (view this).specification.receiver do transferPreapproval_renewImpl this self arg - choice TransferPreapproval_Withdraw : TransferPreapprovalResult - -- ^ Withdraw the transfer preapproval. + choice TransferPreapproval_Withdraw : ChoiceExecutionMetadata + -- ^ Withdraw the transfer preapproval as the receiver. with extraArgs : ExtraArgs -- ^ Additional context required in order to exercise the choice. - controller (view this).receiver + -- + -- FIXME: decide whether we really want to the full extra context here? + controller (view this).specification.receiver do transferPreapproval_withdrawImpl this self arg + choice TransferPreapproval_Reject : ChoiceExecutionMetadata + -- ^ Reject the transfer preapproval as the instrument admin. + with + extraArgs : ExtraArgs + -- ^ Additional context required in order to exercise the choice. + -- + -- FIXME: decide whether we really want to the full extra context here? + controller (view this).specification.admin + do transferPreapproval_rejectImpl this self arg + +data TransferPreapproval_RenewResult = TransferPreapproval_RenewResult with + preapprovalCid : ContractId TransferPreapproval + -- ^ The renewed preapproval contract. + meta : Metadata + -- ^ Additional metadata specific to the result of the choice, used for extensibility. + deriving (Show, Eq) + + +data PreapprovalFactory_PreapproveResult = PreapprovalFactory_PreapproveResult with + preapprovalCid : ContractId TransferPreapproval + -- ^ The created preapproval contract. + meta : Metadata + -- ^ Additional metadata specific to the result of the choice, used for extensibility. + deriving (Show, Eq) -- Preapproval Factory @@ -139,10 +141,10 @@ interface TransferPreapproval where interface PreapprovalFactory where viewtype PreapprovalFactoryView - transferFactory_transferImpl : ContractId PreapprovalFactory -> PreapprovalFactory_Transfer -> Update TransferPreapprovalResult - transferFactory_publicFetchImpl : ContractId PreapprovalFactory -> PreapprovalFactory_PublicFetch -> Update PreapprovalFactoryView + preapprovalFactory_preapproveImpl : ContractId PreapprovalFactory -> PreapprovalFactory_Preapprove -> Update PreapprovalFactory_PreapproveResult + preapprovalFactory_publicFetchImpl : ContractId PreapprovalFactory -> PreapprovalFactory_PublicFetch -> Update PreapprovalFactoryView - nonconsuming choice PreapprovalFactory_CreatePreapproval : TransferPreapprovalResult + nonconsuming choice PreapprovalFactory_Preapprove : PreapprovalFactory_PreapproveResult -- ^ Create a new transfer preapproval. -- Implementations MUST ensure that this choice fails if `transfer.executeBefore` is in the past. -- @@ -156,12 +158,12 @@ interface PreapprovalFactory where -- on a factory contract acquired from an untrusted source *provided* -- all vetted Daml packages only contain interface implementations -- that check the expected admin party. - preapproval : Preapproval + specification : PreapprovalSpecification -- ^ The transfer to execute. extraArgs : ExtraArgs -- ^ The extra arguments to pass to the transfer implementation. - controller transfer.sender - do transferFactory_transferImpl this self arg + controller specification.receiver + do preapprovalFactory_preapproveImpl this self arg nonconsuming choice PreapprovalFactory_PublicFetch : PreapprovalFactoryView -- ^ Fetch the view of the factory contract. @@ -177,7 +179,7 @@ interface PreapprovalFactory where actor : Party -- ^ The party fetching the contract. controller actor - do transferFactory_publicFetchImpl this self arg + do preapprovalFactory_publicFetchImpl this self arg -- | View for `PreapprovalFactory`. data PreapprovalFactoryView = PreapprovalFactoryView From b333fbc1527b69840dabe037cabfcee2f5b99616 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Wed, 15 Oct 2025 15:32:32 +0000 Subject: [PATCH 03/10] preapproval instnacde poc for amulet --- build.sbt | 1 + daml/splice-amulet/daml.yaml | 1 + .../daml/Splice/AmuletRules.daml | 21 +++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/build.sbt b/build.sbt index c2d98e9705..7f26c6e190 100644 --- a/build.sbt +++ b/build.sbt @@ -706,6 +706,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 ++ diff --git a/daml/splice-amulet/daml.yaml b/daml/splice-amulet/daml.yaml index e646b82cf9..468a4e7750 100644 --- a/daml/splice-amulet/daml.yaml +++ b/daml/splice-amulet/daml.yaml @@ -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 diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index 392e25fa9f..4bda32da0d 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -23,6 +23,7 @@ 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.TransferPreapprovalV1 qualified as Api.Token.TransferPreapprovalV1 import Splice.Amulet import Splice.Amulet.TokenApiUtils import Splice.AmuletConfig (AmuletConfig(..), TransferConfig(..), validAmuletConfig, defaultTransferPreapprovalFee) @@ -1457,6 +1458,26 @@ 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 + admin = dso + receiver + senderFilter = [] + idFilter = [] + minAmount = None + maxAmount = None + requestedAt = lastRenewedAt + expiresAt + meta = emptyMetadata + + -- FIXME + transferPreapproval_renewImpl _self _arg = error "implement" + transferPreapproval_withdrawImpl _self _arg = error "implement" + transferPreapproval_rejectImpl _self _arg = error "implement" + + nonconsuming choice TransferPreapproval_Fetch : TransferPreapproval with p : Party From 4758f93c33c8ee2d092c0eb3f69752044f717d04 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 16 Oct 2025 04:13:37 +0000 Subject: [PATCH 04/10] towards transfer interface implementation --- .../daml/Splice/AmuletRules.daml | 93 +++++++++++++++++++ .../daml/Splice/ExternalPartyAmuletRules.daml | 2 - 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index 4bda32da0d..43135cbd1e 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -23,6 +23,7 @@ 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 @@ -1477,6 +1478,16 @@ template TransferPreapproval transferPreapproval_withdrawImpl _self _arg = error "implement" transferPreapproval_rejectImpl _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 = error "implement" + transferFactory_publicFetchImpl _self arg = do + requireExpectedAdminMatch arg.expectedAdmin dso + pure (view $ toInterface @Api.Token.TransferInstructionV1.TransferFactory this) + nonconsuming choice TransferPreapproval_Fetch : TransferPreapproval with @@ -1662,3 +1673,85 @@ 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 + -> Api.Token.TransferInstructionV1.Transfer + -> Api.Token.MetadataV1.ExtraArgs + -> Update Api.Token.TransferInstructionV1.TransferInstructionResult +tokenStdTransfer dso 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 + + assertWithinDeadline "TransferPreapproval.expiresAt" expiresAt + transferResult <- exercisePaymentTransfer dso context Transfer with + sender + provider + inputs + outputs = + [ TransferOutput with + receiver + receiverFeeRatio = 0.0 + amount = 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 + let result = transferResult with meta = None + pure (TransferPreapproval_SendResult result (Some (Metadata meta))) + + + -- return result + pure Api.Token.TransferInstructionV1.TransferInstructionResult with + senderChangeCids = toInterfaceContractId <$> optionalToList result.result.senderChangeAmulet + output = Api.Token.TransferInstructionV1.TransferInstructionResult_Completed with + receiverHoldingCids = createdAmuletToHolding <$> result.result.createdAmulets + meta = copyOnlyBurnMeta result.meta diff --git a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml index 3ce599fa0c..c51400c973 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml @@ -407,5 +407,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) From c29ead4939f51d678e9f4d5df5e45bf522634ac9 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 16 Oct 2025 06:48:15 +0000 Subject: [PATCH 05/10] compile the factory interface impl. --- .../daml/Splice/Amulet/TwoStepTransfer.daml | 18 ------- .../daml/Splice/AmuletRules.daml | 50 ++++++++++++++----- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml b/daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml index db8a885664..0fbc5b01e7 100644 --- a/daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml +++ b/daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml @@ -10,9 +10,6 @@ module Splice.Amulet.TwoStepTransfer ( prepareTwoStepTransfer, executeTwoStepTransfer, abortTwoStepTransfer, - - -- * Shared support code - holdingToTransferInputs, ) where import DA.Assert @@ -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 diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index 43135cbd1e..4f1060b844 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -1463,6 +1463,7 @@ template TransferPreapproval 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 = [] @@ -1483,7 +1484,10 @@ template TransferPreapproval admin = dso meta = emptyMetadata - transferFactory_transferImpl _self _arg = error "implement" + 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) @@ -1495,7 +1499,9 @@ template TransferPreapproval controller p do pure this - -- Transfer amulet to the receiver + -- ^ Transfer amulet to the receiver + -- + -- DEPRECATED: use the `TransferFactory_Transfer` method instead nonconsuming choice TransferPreapproval_Send : TransferPreapproval_SendResult with context : PaymentTransferContext @@ -1690,10 +1696,11 @@ requireExpectedAdminMatch expected actual = require ("Expected admin " <> show e tokenStdTransfer : Party + -> Party -> Api.Token.TransferInstructionV1.Transfer -> Api.Token.MetadataV1.ExtraArgs -> Update Api.Token.TransferInstructionV1.TransferInstructionResult -tokenStdTransfer dso transfer extraArgs = do +tokenStdTransfer dso provider transfer extraArgs = do -- == validate each field of the transfer specification == -- sender: nothing to validate -- receiver: validate preapproval if given @@ -1717,7 +1724,7 @@ tokenStdTransfer dso transfer extraArgs = do -- 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 + inputs <- holdingToTransferInputs (ForOwner with dso; owner = transfer.sender) paymentContext transfer.inputHoldingCids -- result <- exercise preapprovalCid TransferPreapproval_Send -- with -- sender = transfer.sender @@ -1726,16 +1733,18 @@ tokenStdTransfer dso transfer extraArgs = do -- amount = transfer.amount -- description = reason - assertWithinDeadline "TransferPreapproval.expiresAt" expiresAt - transferResult <- exercisePaymentTransfer dso context Transfer with - sender + -- FIXME: enforce + -- assertWithinDeadline "TransferPreapproval.expiresAt" expiresAt + + transferResult <- exercisePaymentTransfer dso paymentContext Transfer with + sender = transfer.sender provider inputs outputs = [ TransferOutput with - receiver + receiver = transfer.receiver receiverFeeRatio = 0.0 - amount = amount + amount = transfer.amount lock = None ] beneficiaries = None @@ -1743,15 +1752,30 @@ tokenStdTransfer dso transfer extraArgs = do -- 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 + -- let meta = optionalMetadata reasonMetaKey identity description (fromOptional emptyMetadata transferResult.meta).values -- strip metadata to avoid duplicating it needlessly let result = transferResult with meta = None - pure (TransferPreapproval_SendResult result (Some (Metadata meta))) + -- pure (TransferPreapproval_SendResult result (Some (Metadata meta))) -- return result pure Api.Token.TransferInstructionV1.TransferInstructionResult with - senderChangeCids = toInterfaceContractId <$> optionalToList result.result.senderChangeAmulet + senderChangeCids = toInterfaceContractId <$> optionalToList result.senderChangeAmulet output = Api.Token.TransferInstructionV1.TransferInstructionResult_Completed with - receiverHoldingCids = createdAmuletToHolding <$> result.result.createdAmulets + 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 From 3245306cf67620e8946fc95d50bdd68ea753f830 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 16 Oct 2025 07:28:28 +0000 Subject: [PATCH 06/10] first standard transfer based on preapproval --- .../Splice/Testing/Registries/AmuletRegistry.daml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry.daml b/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry.daml index 1e2c84a58f..1a12e19cb3 100644 --- a/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry.daml +++ b/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry.daml @@ -221,11 +221,15 @@ registryApi_getTransferFactory registry arg = do (optPreapproval, preapprovalC) <- lookupPreapprovalWithContext registry arg.transfer.receiver featuredAppRightC <- case optPreapproval of None -> pure emptyOpenApiChoiceContext - Some preapproval -> getFeaturedAppRightContext registry preapproval.provider + Some preapproval -> getFeaturedAppRightContext registry preapproval._2.provider let fullContext = withExtraDisclosures extAmuletRulesD (transferC <> preapprovalC <> featuredAppRightC) + -- FIXME: proper context pure EnrichedFactoryChoice with - factoryCid = toInterfaceContractId @TransferFactory extAmuletRulesCid + factoryCid = case optPreapproval of + None -> toInterfaceContractId @TransferFactory extAmuletRulesCid + Some preapproval -> toInterfaceContractId @TransferFactory preapproval._1 + arg = arg with extraArgs = arg.extraArgs with context = fullContext.choiceContext disclosures = fullContext.disclosures @@ -371,7 +375,7 @@ getAmuletRulesTransferContext registry = do roundC <- getOpenRoundContext registry pure $ rulesC <> roundC -lookupPreapprovalWithContext : AmuletRegistry -> Party -> Script (Optional TransferPreapproval, OpenApiChoiceContext) +lookupPreapprovalWithContext : AmuletRegistry -> Party -> Script (Optional (ContractId TransferPreapproval, TransferPreapproval), OpenApiChoiceContext) lookupPreapprovalWithContext registry receiver = do preapprovals <- queryFilter @TransferPreapproval receiver (\preapproval -> preapproval.receiver == receiver) case preapprovals of @@ -379,7 +383,7 @@ lookupPreapprovalWithContext registry receiver = do (preapprovalCid, preapproval) :: _ -> do preapprovalD <- queryDisclosure' @TransferPreapproval registry.dso preapprovalCid pure - ( Some preapproval + ( Some (preapprovalCid, preapproval) , OpenApiChoiceContext with disclosures = preapprovalD choiceContext = mkAmuletContext $ From a4633112551314fda6f7cf50f60fc894cf78c3d8 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 16 Oct 2025 08:40:28 +0000 Subject: [PATCH 07/10] poc changes ready --- daml/splice-amulet/daml/Splice/AmuletRules.daml | 3 +-- .../daml/Splice/ExternalPartyAmuletRules.daml | 14 ++++++++++++++ daml/splice-wallet/daml/Splice/Wallet/Install.daml | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index 4f1060b844..6b9b2db140 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -1736,7 +1736,7 @@ tokenStdTransfer dso provider transfer extraArgs = do -- FIXME: enforce -- assertWithinDeadline "TransferPreapproval.expiresAt" expiresAt - transferResult <- exercisePaymentTransfer dso paymentContext Transfer with + result <- exercisePaymentTransfer dso paymentContext Transfer with sender = transfer.sender provider inputs @@ -1754,7 +1754,6 @@ tokenStdTransfer dso provider transfer extraArgs = do -- 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 - let result = transferResult with meta = None -- pure (TransferPreapproval_SendResult result (Some (Metadata meta))) diff --git a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml index c51400c973..2dbd33de4e 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml @@ -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 @@ -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 = + -} pure (TransferCommandResultSuccess result) catch (ex : InvalidTransfer) -> mergeInputsAndReportError ex.reason diff --git a/daml/splice-wallet/daml/Splice/Wallet/Install.daml b/daml/splice-wallet/daml/Splice/Wallet/Install.daml index 8c4fe19b84..3f170c868d 100644 --- a/daml/splice-wallet/daml/Splice/Wallet/Install.daml +++ b/daml/splice-wallet/daml/Splice/Wallet/Install.daml @@ -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 From 4628f421d32c317d10381c6dc934b4dc24f7b258 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 16 Oct 2025 11:09:21 +0000 Subject: [PATCH 08/10] polish interface definition --- .../daml/Splice/AmuletRules.daml | 6 +- .../Api/Token/TransferPreapprovalV1.daml | 153 +++++++++++------- 2 files changed, 97 insertions(+), 62 deletions(-) diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index 6b9b2db140..b69d938893 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -1470,14 +1470,16 @@ template TransferPreapproval 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_withdrawImpl _self _arg = error "implement" - transferPreapproval_rejectImpl _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 diff --git a/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml b/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml index 2276014017..b699ee53d0 100644 --- a/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml +++ b/token-standard/splice-api-token-transfer-preapproval-v1/daml/Splice/Api/Token/TransferPreapprovalV1.daml @@ -1,18 +1,33 @@ -- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 --- | Instruct transfers of holdings between parties. +-- | Manage preapprovals for incoming transfers. module Splice.Api.Token.TransferPreapprovalV1 where - import Splice.Api.Token.MetadataV1 {- +REVIEWER NOTE: + +Core design ideas: +- make the Preapproval contract implement TransferFactory_Transfer to execute direct transfers +- add the API below to manage the set of preapprovals +- add the obvious and easy to implemnet filters for preapprovals, but don't aim to cover all use-cases +- aim to make preapprovals expire after one year by default + - creates about ~ 0.3 TPS per 1M parties with an average of 10 preapprovals per party + + +Amulet implementation concerns: +- we'll limit the number of allowed preapprovals per party to 10, so that the DSO ACS size is reasonably bounded + - soft enforcement via the off-ledger API endpoint failing if there are too many + - after the fact enforcement via a DsoRules choice that uses ActionWithConfirmation to cancel the oldest + preapproval if there are too many + Ideas left out: - network wide blanket approvals: - tricky wrt confidentiality: DSO must only be an observer - - do not replace registry-specific preapprovals: registries want their + - does not replace registry-specific preapprovals: registries want their own preapprovals to support featured app markers and reward sharing - more complex management: wallets need to understand them as distinct from registry-specific preapprovals, and know how to allocate them and @@ -20,11 +35,6 @@ Ideas left out: - volume-bounded approvals: too complex to implement as they require mutable on-ledger state, which likely results in contention -- allowing to not create the intermediate TransferInstruction: - - not done to avoid - - - -} @@ -34,7 +44,7 @@ Ideas left out: -- | Specification of which incoming transfers are pre-approved by a receiver. data PreapprovalSpecification = PreapprovalSpecification with admin : Party - -- ^ Admin party of the holidings whose transfers are preapproved. + -- ^ Admin party of the holdings whose transfers are preapproved. receiver : Party -- ^ Receiver that preapproves incoming transfers. senderFilter : [Party] @@ -49,11 +59,29 @@ data PreapprovalSpecification = PreapprovalSpecification with -- ^ Optional upper bound on the amount whose transfer is preapproved. deriving (Eq, Show) +-- FIXME: consdier the naming scheme -- when do we use 'TranferPreapproval' vs 'Preapproval' +data PreapprovalStatus + = PS_Active + -- ^ Preapproval is ready to use. + | PS_PendingWalletProviderAcceptance + -- ^ Pending acceptance by the wallet provider. + | PS_PendingInstrumentAdminAcceptance + -- ^ Pending acceptance by the instrument admin. + deriving (Eq, Show) -- | View for `TransferPreapproval`. data TransferPreapprovalView = TransferPreapprovalView with specification : PreapprovalSpecification -- ^ Specification of which transfers are preapproved. + walletProvider : Party + -- ^ Wallet provider that should get featured app rewards for incoming + -- transfers via this preapproval. + -- + -- QUESTION/FIXME: consider whether it is really right that this field is outside the spec. + -- It currently is as the `walletProvider` is not required to determine whether a transfer + -- is pre-approved; and the spec contains exactly that data. + status : PreapprovalStatus + -- ^ Status of the pre-approval. requestedAt : Time -- ^ When the preapproval was requested to be created. expiresAt : Time @@ -62,26 +90,27 @@ data TransferPreapprovalView = TransferPreapprovalView with -- ^ Additional metadata specific to the preapproval, used for extensibility. deriving (Eq, Show) --- | An interface for tracking the status of a transfer instruction, --- i.e., a request to a registry app to execute a transfer. +-- | An interface for reading and managing transfer preapprovals. -- --- Registries MAY evolve the transfer instruction in multiple steps. They SHOULD --- do so using only the choices on this interface, so that wallets can reliably --- parse the transaction history and determine whether the instruction ultimately --- succeeded or failed. +-- Contracts representing preapprovals are expected to also implement the `TransferFactory` interface, +-- so that `TransferFactory_Transfer` can be used to execute a preapproved transfer. +-- +-- Note that we do not capture that interface dependency here to maximize the options +-- for future evoluations. interface TransferPreapproval where viewtype TransferPreapprovalView - transferPreapproval_renewImpl : ContractId TransferPreapproval -> TransferPreapproval_Renew -> Update TransferPreapproval_RenewResult - transferPreapproval_withdrawImpl : ContractId TransferPreapproval -> TransferPreapproval_Withdraw -> Update ChoiceExecutionMetadata - transferPreapproval_rejectImpl : ContractId TransferPreapproval -> TransferPreapproval_Reject -> Update ChoiceExecutionMetadata - + transferPreapproval_renewImpl : ContractId TransferPreapproval -> TransferPreapproval_Renew -> Update TransferPreapprovalCreationResult + transferPreapproval_acceptImpl : ContractId TransferPreapproval -> TransferPreapproval_Accept -> Update ChoiceExecutionMetadata + transferPreapproval_cancelImpl : ContractId TransferPreapproval -> TransferPreapproval_Cancel -> Update ChoiceExecutionMetadata - choice TransferPreapproval_Renew : TransferPreapproval_RenewResult - -- ^ Renew the preapproval as the delegate or receiver. + choice TransferPreapproval_Renew : TransferPreapprovalCreationResult + -- ^ Renew the preapproval as the receiver, wallet provider or instrument admin. -- -- Can also be used to update the specification and expiration time. with + actor : Party + -- ^ The party executing the renewal. specification : PreapprovalSpecification -- ^ New specification of which transfers are preapproved. requestedAt : Time @@ -92,41 +121,37 @@ interface TransferPreapproval where -- Will be capped by the maximal expiration time allowed by the instrument admin; -- and if not provided, the maximal expiration time will be used. extraArgs : ExtraArgs - -- ^ Additional context required in order to exercise the choice. + -- ^ Additional data required in order to exercise the choice. -- - -- FIXME: decide whether we really want to the full extra context here? - controller (view this).specification.receiver + -- Intended to be called with an empty `ChoiceContext` by default. + -- Provided for extensibility. + controller actor do transferPreapproval_renewImpl this self arg - choice TransferPreapproval_Withdraw : ChoiceExecutionMetadata - -- ^ Withdraw the transfer preapproval as the receiver. + choice TransferPreapproval_Accept : ChoiceExecutionMetadata + -- ^ Accept the transfer preapproval as the admin or wallet provider. with + actor : Party + -- ^ The party executing the cancellation. extraArgs : ExtraArgs - -- ^ Additional context required in order to exercise the choice. - -- - -- FIXME: decide whether we really want to the full extra context here? - controller (view this).specification.receiver - do transferPreapproval_withdrawImpl this self arg + -- ^ Additional data required in order to exercise the choice. + controller actor + do transferPreapproval_acceptImpl this self arg - choice TransferPreapproval_Reject : ChoiceExecutionMetadata - -- ^ Reject the transfer preapproval as the instrument admin. + choice TransferPreapproval_Cancel : ChoiceExecutionMetadata + -- ^ Cancel the transfer preapproval as the instrument admin or wallet provider + -- depending on whether the status is `PS_PendingWalletProviderAcceptance` or + -- `PS_PendingInstrumentAdminAcceptance` with + actor : Party + -- ^ The party executing the cancellation. extraArgs : ExtraArgs - -- ^ Additional context required in order to exercise the choice. - -- - -- FIXME: decide whether we really want to the full extra context here? - controller (view this).specification.admin - do transferPreapproval_rejectImpl this self arg - -data TransferPreapproval_RenewResult = TransferPreapproval_RenewResult with - preapprovalCid : ContractId TransferPreapproval - -- ^ The renewed preapproval contract. - meta : Metadata - -- ^ Additional metadata specific to the result of the choice, used for extensibility. - deriving (Show, Eq) + -- ^ Additional data required in order to exercise the choice. + controller actor + do transferPreapproval_cancelImpl this self arg -data PreapprovalFactory_PreapproveResult = PreapprovalFactory_PreapproveResult with +data TransferPreapprovalCreationResult = TransferPreapprovalCreationResult with preapprovalCid : ContractId TransferPreapproval -- ^ The created preapproval contract. meta : Metadata @@ -137,29 +162,46 @@ data PreapprovalFactory_PreapproveResult = PreapprovalFactory_PreapproveResult w -- Preapproval Factory ---------------------- +-- | View for `PreapprovalFactory`. +data PreapprovalFactoryView = PreapprovalFactoryView + with + admin : Party + -- ^ The party representing the registry app that administers the instruments for + -- which this transfer factory can be used. + meta : Metadata + -- ^ Additional metadata specific to the transfer factory, used for extensibility. + deriving (Show, Eq) + -- | A factory contract to create transfer preapprovals. interface PreapprovalFactory where viewtype PreapprovalFactoryView - preapprovalFactory_preapproveImpl : ContractId PreapprovalFactory -> PreapprovalFactory_Preapprove -> Update PreapprovalFactory_PreapproveResult + preapprovalFactory_preapproveImpl : ContractId PreapprovalFactory -> PreapprovalFactory_Preapprove -> Update TransferPreapprovalCreationResult preapprovalFactory_publicFetchImpl : ContractId PreapprovalFactory -> PreapprovalFactory_PublicFetch -> Update PreapprovalFactoryView - nonconsuming choice PreapprovalFactory_Preapprove : PreapprovalFactory_PreapproveResult + nonconsuming choice PreapprovalFactory_Preapprove : TransferPreapprovalCreationResult -- ^ Create a new transfer preapproval. - -- Implementations MUST ensure that this choice fails if `transfer.executeBefore` is in the past. -- -- Implementations MAY limit the number of active preapprovals per receiver. with expectedAdmin : Party -- ^ The expected admin party issuing the factory. Implementations MUST validate that this matches -- the admin of the factory. + -- -- Callers SHOULD ensure they get `expectedAdmin` from a trusted source, e.g., a read against -- their own participant. That way they can ensure that it is safe to exercise a choice -- on a factory contract acquired from an untrusted source *provided* -- all vetted Daml packages only contain interface implementations -- that check the expected admin party. specification : PreapprovalSpecification - -- ^ The transfer to execute. + -- ^ The preapproval to create. + requestedAt : Time + -- ^ When the preapproval was requested to be (re-)created. + expiresAt : Optional Time + -- ^ New expiration time of the preapproval. + -- + -- Will be capped by the maximal expiration time allowed by the instrument admin; + -- and if not provided, the maximal expiration time will be used. extraArgs : ExtraArgs -- ^ The extra arguments to pass to the transfer implementation. controller specification.receiver @@ -171,6 +213,7 @@ interface PreapprovalFactory where expectedAdmin : Party -- ^ The expected admin party issuing the factory. Implementations MUST validate that this matches -- the admin of the factory. + -- -- Callers SHOULD ensure they get `expectedAdmin` from a trusted source, e.g., a read against -- their own participant. That way they can ensure that it is safe to exercise a choice -- on a factory contract acquired from an untrusted source *provided* @@ -180,13 +223,3 @@ interface PreapprovalFactory where -- ^ The party fetching the contract. controller actor do preapprovalFactory_publicFetchImpl this self arg - --- | View for `PreapprovalFactory`. -data PreapprovalFactoryView = PreapprovalFactoryView - with - admin : Party - -- ^ The party representing the registry app that administers the instruments for - -- which this transfer factory can be used. - meta : Metadata - -- ^ Additional metadata specific to the transfer factory, used for extensibility. - deriving (Show, Eq) From ca36f668d81bdef70f83486588df5d62bc02ae3e Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 16 Oct 2025 11:17:15 +0000 Subject: [PATCH 09/10] polish http definition --- .../openapi/transfer-preapproval-v1.yaml | 66 ++++++++----------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/token-standard/splice-api-token-transfer-preapproval-v1/openapi/transfer-preapproval-v1.yaml b/token-standard/splice-api-token-transfer-preapproval-v1/openapi/transfer-preapproval-v1.yaml index 48104f96fe..280ebbff9e 100644 --- a/token-standard/splice-api-token-transfer-preapproval-v1/openapi/transfer-preapproval-v1.yaml +++ b/token-standard/splice-api-token-transfer-preapproval-v1/openapi/transfer-preapproval-v1.yaml @@ -3,18 +3,22 @@ openapi: 3.0.0 info: - title: transfer instruction off-ledger API + title: Transfer preapproval management off-ledger API description: | - Implemented by token registries for the purpose of supporting the initiation - of asset transfers; e.g. to settle off-ledger obligations. + Implemented by token registries for the purpose of managing the set + of transfer preapprovals. + + These preapprovals will be used to determine how to best execute a + transfer instructed via the separate transfer-preapproval API. version: 1.0.0 paths: - /registry/transfer-instruction/v1/transfer-factory: + /registry/transfer-preapproval/v1/preapproval-factory: post: - operationId: "getTransferFactory" + # FIXME: reconsider naming 'Transfer' prefix or not, once done so on in the Daml interface definition + operationId: "getPreapprovalFactory" description: | - Get the factory and choice context for executing a direct transfer. + Get the factory and choice context for creating a transfer pre-approval. requestBody: required: true content: @@ -27,21 +31,23 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/TransferFactoryWithChoiceContext" + $ref: "#/components/schemas/PreapprovalFactoryWithChoiceContext" + # FIXME: check whether we can add a dedicated error for too many preapprovals + # FIXME: consider adding an info endpoint, or extending the registry metadata info to learn about the number of allowed preapprovals "400": $ref: "#/components/responses/400" "404": $ref: "#/components/responses/404" - /registry/transfer-instruction/v1/{transferInstructionId}/choice-contexts/accept: + /registry/transfer-preapproval/v1/{preapprovalId}/choice-contexts/renew: post: - operationId: "getTransferInstructionAcceptContext" + operationId: "getTransferInstructionRenewContext" description: | - Get the choice context to accept a transfer instruction. + Get the choice context to renew a preapproval. parameters: - - name: transferInstructionId - description: "The contract ID of the transfer instruction to accept." + - name: preapprovalId + description: "The contract ID of the preapproval to accept." in: path required: true schema: @@ -64,14 +70,14 @@ paths: "404": $ref: "#/components/responses/404" - /registry/transfer-instruction/v1/{transferInstructionId}/choice-contexts/reject: + /registry/transfer-preapproval/v1/{preapprovalId}/choice-contexts/accept: post: - operationId: "getTransferInstructionRejectContext" + operationId: "getTransferInstructionAcceptContext" description: | - Get the choice context to reject a transfer instruction. + Get the choice context to accept a preapproval. parameters: - - name: transferInstructionId - description: "The contract ID of the transfer instruction to reject." + - name: preapprovalId + description: "The contract ID of the preapproval to accept." in: path required: true schema: @@ -94,14 +100,14 @@ paths: "404": $ref: "#/components/responses/404" - /registry/transfer-instruction/v1/{transferInstructionId}/choice-contexts/withdraw: + /registry/transfer-preapproval/v1/{preapprovalId}/choice-contexts/cancel: post: operationId: "getTransferInstructionWithdrawContext" description: | - Get the choice context to withdraw a transfer instruction. + Get the choice context to cancel a preapproval. parameters: - - name: transferInstructionId - description: "The contract ID of the transfer instruction to withdraw." + - name: preapprovalId + description: "The contract ID of the preapproval to cancel." in: path required: true schema: @@ -176,9 +182,9 @@ components: additionalProperties: type: string - TransferFactoryWithChoiceContext: + PreapprovalFactoryWithChoiceContext: description: | - The transfer factory contract together with the choice context required to exercise the choice + The preapproval factory contract together with the choice context required to exercise the choice provided by the factory. Typically used to implement the generic initiation of on-ledger workflows via a Daml interface. @@ -189,26 +195,12 @@ components: factoryId: description: "The contract ID of the contract implementing the factory interface." type: string - transferKind: - description: | - The kind of transfer workflow that will be used: - * `offer`: offer a transfer to the receiver and only transfer if they accept - * `direct`: transfer directly to the receiver without asking them for approval. - Only chosen if the receiver has pre-approved direct transfers. - * `self`: a self-transfer where the sender and receiver are the same party. - No approval is required, and the transfer is typically immediate. - type: string - enum: - - "self" - - "direct" - - "offer" choiceContext: $ref: "#/components/schemas/ChoiceContext" required: [ "factoryId", "choiceContext", - "transferKind", ] ChoiceContext: From dc5011521a82fb8c8c0b6e37c0a923e81b1b6081 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 16 Oct 2025 11:21:53 +0000 Subject: [PATCH 10/10] more polish --- apps/package-lock.json | 15 +++++++++ build.sbt | 31 +++++++++---------- .../daml/Splice/AmuletRules.daml | 4 +-- ...transfer-preapproval-v1-index-template.rst | 13 ++++++++ 4 files changed, 45 insertions(+), 18 deletions(-) create mode 100644 docs/api-templates/splice-api-token-transfer-preapproval-v1-index-template.rst diff --git a/apps/package-lock.json b/apps/package-lock.json index 8f6428ae2d..6cce8c7468 100644 --- a/apps/package-lock.json +++ b/apps/package-lock.json @@ -326,6 +326,7 @@ "@daml.js/splice-api-token-holding-v1-1.0.0": "file:../splice-api-token-holding-v1-1.0.0", "@daml.js/splice-api-token-metadata-v1-1.0.0": "file:../splice-api-token-metadata-v1-1.0.0", "@daml.js/splice-api-token-transfer-instruction-v1-1.0.0": "file:../splice-api-token-transfer-instruction-v1-1.0.0", + "@daml.js/splice-api-token-transfer-preapproval-v1-1.0.0": "file:../splice-api-token-transfer-preapproval-v1-1.0.0", "@mojotech/json-type-validation": "^3.1.0" } }, @@ -514,6 +515,16 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-api-token-transfer-preapproval-v1-1.0.0": { + "name": "@daml.js/splice-api-token-transfer-preapproval-v1-1.0.0", + "version": "0.0.0", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-api-token-metadata-v1-1.0.0": "file:../splice-api-token-metadata-v1-1.0.0", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splice-dso-governance-0.1.18": { "name": "@daml.js/splice-dso-governance-0.1.18", "version": "0.0.0", @@ -1373,6 +1384,10 @@ "resolved": "common/frontend/daml.js/splice-api-token-transfer-instruction-v1-1.0.0", "link": true }, + "node_modules/@daml.js/splice-api-token-transfer-preapproval-v1-1.0.0": { + "resolved": "common/frontend/daml.js/splice-api-token-transfer-preapproval-v1-1.0.0", + "link": true + }, "node_modules/@daml.js/splice-dso-governance": { "resolved": "common/frontend/daml.js/splice-dso-governance-0.1.20", "link": true diff --git a/build.sbt b/build.sbt index 7f26c6e190..fe747fbd3b 100644 --- a/build.sbt +++ b/build.sbt @@ -388,22 +388,21 @@ lazy val `splice-api-token-transfer-preapproval-v1-daml` = Compile / damlDependencies := (`splice-api-token-metadata-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-holding-v1-daml` / Compile / damlBuild).value, - // FIXME: enable when the OpenAPI spec is ready - // 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" }, + 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`) diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index b69d938893..b2311693c7 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -1501,9 +1501,9 @@ template TransferPreapproval controller p do pure this - -- ^ Transfer amulet to the receiver + -- Transfer amulet to the receiver -- - -- DEPRECATED: use the `TransferFactory_Transfer` method instead + -- DEPRECATED: use the `TransferFactory_Transfer` method from the `TransferFactory` interface instead nonconsuming choice TransferPreapproval_Send : TransferPreapproval_SendResult with context : PaymentTransferContext diff --git a/docs/api-templates/splice-api-token-transfer-preapproval-v1-index-template.rst b/docs/api-templates/splice-api-token-transfer-preapproval-v1-index-template.rst new file mode 100644 index 0000000000..183286e6e4 --- /dev/null +++ b/docs/api-templates/splice-api-token-transfer-preapproval-v1-index-template.rst @@ -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}}}