@@ -47,6 +47,7 @@ const (
4747 spliceTransferFactory = "TransferFactory"
4848 transferInstrEntity = "TransferInstruction"
4949 acceptChoice = "TransferInstruction_Accept"
50+ withdrawChoice = "TransferInstruction_Withdraw"
5051
5152 spliceHoldingModule = "Splice.Api.Token.HoldingV1"
5253 spliceHoldingEntity = "Holding"
@@ -141,6 +142,17 @@ type Token interface {
141142 PrepareAcceptTransfer (
142143 ctx context.Context , partyID , instructionCID , instrumentAdmin string ,
143144 ) (* PreparedTransfer , error )
145+
146+ // WithdrawTransferInstruction reclaims a pending/expired offer-based transfer for a
147+ // custodial sender by exercising TransferInstruction_Withdraw server-side.
148+ WithdrawTransferInstruction (ctx context.Context , partyID , instructionCID , instrumentAdmin string ) error
149+
150+ // PrepareWithdrawTransfer builds a Canton transaction for a non-custodial sender to
151+ // reclaim a pending/expired offer and returns the hash to sign externally. Use
152+ // ExecuteTransfer to complete it.
153+ PrepareWithdrawTransfer (
154+ ctx context.Context , partyID , instructionCID , instrumentAdmin string ,
155+ ) (* PreparedTransfer , error )
144156}
145157
146158// Client implements CIP-56 token operations.
@@ -1285,22 +1297,66 @@ func (c *Client) AcceptTransferInstruction(ctx context.Context, partyID, instruc
12851297 if partyID == "" || instructionCID == "" || instrumentAdmin == "" {
12861298 return fmt .Errorf ("partyID, instructionCID, and instrumentAdmin are required" )
12871299 }
1300+ cmd , disclosed , err := c .buildInstructionChoiceCommand (ctx , instructionCID , instrumentAdmin , "accept" , acceptChoice )
1301+ if err != nil {
1302+ return err
1303+ }
1304+ if err := c .exerciseInstructionAsCustodial (ctx , partyID , cmd , disclosed ); err != nil {
1305+ return fmt .Errorf ("accept transfer instruction: %w" , err )
1306+ }
1307+ return nil
1308+ }
1309+
1310+ // WithdrawTransferInstruction reclaims a pending or expired offer-based transfer for a
1311+ // custodial sender: the middleware holds the user's key and exercises
1312+ // TransferInstruction_Withdraw server-side, returning the locked holding to the sender.
1313+ // Mirrors AcceptTransferInstruction but for the sender side.
1314+ func (c * Client ) WithdrawTransferInstruction (ctx context.Context , partyID , instructionCID , instrumentAdmin string ) error {
1315+ if partyID == "" || instructionCID == "" || instrumentAdmin == "" {
1316+ return fmt .Errorf ("partyID, instructionCID, and instrumentAdmin are required" )
1317+ }
1318+ cmd , disclosed , err := c .buildInstructionChoiceCommand (ctx , instructionCID , instrumentAdmin , "withdraw" , withdrawChoice )
1319+ if err != nil {
1320+ return err
1321+ }
1322+ if err := c .exerciseInstructionAsCustodial (ctx , partyID , cmd , disclosed ); err != nil {
1323+ return fmt .Errorf ("withdraw transfer instruction: %w" , err )
1324+ }
1325+ return nil
1326+ }
1327+
1328+ // buildInstructionChoiceCommand fetches the registrar's choice-context for the given
1329+ // action ("accept"/"withdraw") on a TransferInstruction and builds the exercise command
1330+ // plus its disclosed contracts. Shared by the accept and withdraw (claim-back) flows,
1331+ // which differ only in the registry endpoint and the on-ledger choice name.
1332+ func (c * Client ) buildInstructionChoiceCommand (
1333+ ctx context.Context , instructionCID , instrumentAdmin , action , choice string ,
1334+ ) (* lapiv2.Command , []* lapiv2.DisclosedContract , error ) {
12881335 if c .registryClient == nil {
1289- return fmt .Errorf ("no registry client configured for accept" )
1336+ return nil , nil , fmt .Errorf ("no registry client configured for %s" , action )
12901337 }
12911338 extCfg , ok := c .cfg .ExternalTokens [instrumentAdmin ]
12921339 if ! ok {
1293- return fmt .Errorf ("unsupported external token issuer: %s" , instrumentAdmin )
1340+ return nil , nil , fmt .Errorf ("unsupported external token issuer: %s" , instrumentAdmin )
12941341 }
12951342
1296- acceptResp , err := c .registryClient .GetAcceptChoiceContext (ctx , extCfg .RegistryURL , instrumentAdmin , instructionCID )
1343+ var ctxResp * AcceptContextResponse
1344+ var err error
1345+ switch action {
1346+ case "accept" :
1347+ ctxResp , err = c .registryClient .GetAcceptChoiceContext (ctx , extCfg .RegistryURL , instrumentAdmin , instructionCID )
1348+ case "withdraw" :
1349+ ctxResp , err = c .registryClient .GetWithdrawChoiceContext (ctx , extCfg .RegistryURL , instrumentAdmin , instructionCID )
1350+ default :
1351+ return nil , nil , fmt .Errorf ("unsupported instruction action: %s" , action )
1352+ }
12971353 if err != nil {
1298- return fmt .Errorf ("get accept choice context: %w" , err )
1354+ return nil , nil , fmt .Errorf ("get %s choice context: %w" , action , err )
12991355 }
13001356
1301- ctxValue , err := encodeChoiceContextRecord (acceptResp .ChoiceContextData )
1357+ ctxValue , err := encodeChoiceContextRecord (ctxResp .ChoiceContextData )
13021358 if err != nil {
1303- return fmt .Errorf ("encode choice context: %w" , err )
1359+ return nil , nil , fmt .Errorf ("encode choice context: %w" , err )
13041360 }
13051361
13061362 extraArgs := & lapiv2.Value {
@@ -1314,9 +1370,9 @@ func (c *Client) AcceptTransferInstruction(ctx context.Context, partyID, instruc
13141370 },
13151371 }
13161372
1317- disclosed , err := convertDisclosedContractSlice (acceptResp .DisclosedContracts , c .cfg .DomainID )
1373+ disclosed , err := convertDisclosedContractSlice (ctxResp .DisclosedContracts , c .cfg .DomainID )
13181374 if err != nil {
1319- return fmt .Errorf ("convert disclosed contracts: %w" , err )
1375+ return nil , nil , fmt .Errorf ("convert disclosed contracts: %w" , err )
13201376 }
13211377
13221378 cmd := & lapiv2.Command {
@@ -1328,7 +1384,7 @@ func (c *Client) AcceptTransferInstruction(ctx context.Context, partyID, instruc
13281384 EntityName : transferInstrEntity ,
13291385 },
13301386 ContractId : instructionCID ,
1331- Choice : acceptChoice ,
1387+ Choice : choice ,
13321388 ChoiceArgument : & lapiv2.Value {
13331389 Sum : & lapiv2.Value_Record {
13341390 Record : & lapiv2.Record {
@@ -1341,19 +1397,23 @@ func (c *Client) AcceptTransferInstruction(ctx context.Context, partyID, instruc
13411397 },
13421398 },
13431399 }
1400+ return cmd , disclosed , nil
1401+ }
13441402
1345- // External parties (the only kind this api-server creates for custodial users)
1346- // can't be submitted as via plain SubmitAndWait — the participant doesn't hold
1347- // the signing key. Use Interactive Submission with the user's stored key.
1403+ // exerciseInstructionAsCustodial submits a TransferInstruction choice on behalf of a
1404+ // custodial party via Interactive Submission with the user's middleware-held key.
1405+ // External parties can't use plain SubmitAndWait (the participant doesn't hold the key).
1406+ func (c * Client ) exerciseInstructionAsCustodial (
1407+ ctx context.Context , partyID string , cmd * lapiv2.Command , disclosed []* lapiv2.DisclosedContract ,
1408+ ) error {
13481409 if c .keyResolver == nil {
1349- return fmt .Errorf ("accept transfer instruction: no key resolver configured " +
1350- "(custodial accept requires the api-server to hold the user's signing key)" )
1410+ return fmt .Errorf ("no key resolver configured " +
1411+ "(custodial instruction exercise requires the api-server to hold the user's signing key)" )
13511412 }
13521413 signerKey , err := c .keyResolver (partyID )
13531414 if err != nil {
1354- return fmt .Errorf ("accept transfer instruction: resolve signing key for %s: %w" , partyID , err )
1415+ return fmt .Errorf ("resolve signing key for %s: %w" , partyID , err )
13551416 }
1356-
13571417 commands := & lapiv2.Commands {
13581418 SynchronizerId : c .cfg .DomainID ,
13591419 CommandId : uuid .NewString (),
@@ -1362,10 +1422,7 @@ func (c *Client) AcceptTransferInstruction(ctx context.Context, partyID, instruc
13621422 Commands : []* lapiv2.Command {cmd },
13631423 DisclosedContracts : disclosed ,
13641424 }
1365- if err := c .prepareAndExecuteAsUser (ctx , commands , signerKey , partyID ); err != nil {
1366- return fmt .Errorf ("accept transfer instruction: %w" , err )
1367- }
1368- return nil
1425+ return c .prepareAndExecuteAsUser (ctx , commands , signerKey , partyID )
13691426}
13701427
13711428func (c * Client ) PrepareAcceptTransfer (
@@ -1374,63 +1431,35 @@ func (c *Client) PrepareAcceptTransfer(
13741431 if partyID == "" || instructionCID == "" || instrumentAdmin == "" {
13751432 return nil , fmt .Errorf ("partyID, instructionCID, and instrumentAdmin are required" )
13761433 }
1377- if c .registryClient == nil {
1378- return nil , fmt .Errorf ("no registry client configured for accept" )
1379- }
1380- extCfg , ok := c .cfg .ExternalTokens [instrumentAdmin ]
1381- if ! ok {
1382- return nil , fmt .Errorf ("unsupported external token issuer: %s" , instrumentAdmin )
1383- }
1384-
1385- acceptResp , err := c .registryClient .GetAcceptChoiceContext (ctx , extCfg .RegistryURL , instrumentAdmin , instructionCID )
1386- if err != nil {
1387- return nil , fmt .Errorf ("get accept choice context: %w" , err )
1388- }
1389-
1390- ctxValue , err := encodeChoiceContextRecord (acceptResp .ChoiceContextData )
1434+ cmd , disclosed , err := c .buildInstructionChoiceCommand (ctx , instructionCID , instrumentAdmin , "accept" , acceptChoice )
13911435 if err != nil {
1392- return nil , fmt . Errorf ( "encode choice context: %w" , err )
1436+ return nil , err
13931437 }
1438+ return c .prepareInstructionTx (ctx , partyID , instructionCID , cmd , disclosed )
1439+ }
13941440
1395- extraArgs := & lapiv2.Value {
1396- Sum : & lapiv2.Value_Record {
1397- Record : & lapiv2.Record {
1398- Fields : []* lapiv2.RecordField {
1399- {Label : "context" , Value : ctxValue },
1400- {Label : "meta" , Value : values .EmptyMetadata ()},
1401- },
1402- },
1403- },
1441+ // PrepareWithdrawTransfer builds a Canton transaction for a non-custodial sender to
1442+ // reclaim a pending or expired offer-based transfer (TransferInstruction_Withdraw) and
1443+ // returns the hash the sender signs externally. Use ExecuteTransfer to complete it.
1444+ func (c * Client ) PrepareWithdrawTransfer (
1445+ ctx context.Context , partyID , instructionCID , instrumentAdmin string ,
1446+ ) (* PreparedTransfer , error ) {
1447+ if partyID == "" || instructionCID == "" || instrumentAdmin == "" {
1448+ return nil , fmt .Errorf ("partyID, instructionCID, and instrumentAdmin are required" )
14041449 }
1405-
1406- disclosed , err := convertDisclosedContractSlice (acceptResp .DisclosedContracts , c .cfg .DomainID )
1450+ cmd , disclosed , err := c .buildInstructionChoiceCommand (ctx , instructionCID , instrumentAdmin , "withdraw" , withdrawChoice )
14071451 if err != nil {
1408- return nil , fmt .Errorf ("convert disclosed contracts: %w" , err )
1409- }
1410-
1411- cmd := & lapiv2.Command {
1412- Command : & lapiv2.Command_Exercise {
1413- Exercise : & lapiv2.ExerciseCommand {
1414- TemplateId : & lapiv2.Identifier {
1415- PackageId : c .cfg .SpliceTransferPackageID ,
1416- ModuleName : spliceTransferModule ,
1417- EntityName : transferInstrEntity ,
1418- },
1419- ContractId : instructionCID ,
1420- Choice : acceptChoice ,
1421- ChoiceArgument : & lapiv2.Value {
1422- Sum : & lapiv2.Value_Record {
1423- Record : & lapiv2.Record {
1424- Fields : []* lapiv2.RecordField {
1425- {Label : "extraArgs" , Value : extraArgs },
1426- },
1427- },
1428- },
1429- },
1430- },
1431- },
1452+ return nil , err
14321453 }
1454+ return c .prepareInstructionTx (ctx , partyID , instructionCID , cmd , disclosed )
1455+ }
14331456
1457+ // prepareInstructionTx prepares a TransferInstruction-choice exercise for external
1458+ // (non-custodial) signing and returns the hash to sign. Shared by the accept and
1459+ // withdraw prepare flows.
1460+ func (c * Client ) prepareInstructionTx (
1461+ ctx context.Context , partyID , instructionCID string , cmd * lapiv2.Command , disclosed []* lapiv2.DisclosedContract ,
1462+ ) (* PreparedTransfer , error ) {
14341463 authCtx := c .ledger .AuthContext (ctx )
14351464 prepResp , err := c .ledger .Interactive ().PrepareSubmission (authCtx , & interactivev2.PrepareSubmissionRequest {
14361465 UserId : c .cfg .UserID ,
@@ -1441,7 +1470,7 @@ func (c *Client) PrepareAcceptTransfer(
14411470 DisclosedContracts : disclosed ,
14421471 })
14431472 if err != nil {
1444- return nil , fmt .Errorf ("prepare accept submission: %w" , err )
1473+ return nil , fmt .Errorf ("prepare submission: %w" , err )
14451474 }
14461475
14471476 pt := & PreparedTransfer {
@@ -1453,7 +1482,7 @@ func (c *Client) PrepareAcceptTransfer(
14531482 ExpiresAt : time .Now ().UTC ().Add (preparedTxCacheTTL ),
14541483 }
14551484
1456- c .logger .Info ("prepared non-custodial accept " ,
1485+ c .logger .Info ("prepared non-custodial instruction tx " ,
14571486 zap .String ("transfer_id" , pt .TransferID ),
14581487 zap .String ("party_id" , partyID ),
14591488 zap .String ("contract_id" , instructionCID ),
0 commit comments