From f0ddd6f097eae35e6ef3b923bde718260d96bad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:11:05 +0200 Subject: [PATCH 1/6] Add examples --- examples/tests/examples_test.go | 852 ++++++++++++++++++++++++++++++++ 1 file changed, 852 insertions(+) create mode 100644 examples/tests/examples_test.go diff --git a/examples/tests/examples_test.go b/examples/tests/examples_test.go new file mode 100644 index 000000000..697ed6259 --- /dev/null +++ b/examples/tests/examples_test.go @@ -0,0 +1,852 @@ +package tests + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "math/big" + "net/http" + "slices" + "strconv" + "strings" + "testing" + "time" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + chainsel "github.com/smartcontractkit/chain-selectors" + routerwrapper "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v2_0_0/onramp" + "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" + v1 "github.com/smartcontractkit/chainlink-ccv/indexer/pkg/api/handlers/v1" + indexerclient "github.com/smartcontractkit/chainlink-ccv/indexer/pkg/client" + "github.com/smartcontractkit/chainlink-ccv/protocol" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider" + "github.com/smartcontractkit/go-daml/pkg/service/ledger" + "github.com/smartcontractkit/go-daml/pkg/types" + + "github.com/smartcontractkit/chainlink-canton/bindings" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipruntime" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/core" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/receiver" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_metadata_v1" + "github.com/smartcontractkit/chainlink-canton/contracts" + "github.com/smartcontractkit/chainlink-canton/deployment/authentication/authorizationcode" + oapiCCIP "github.com/smartcontractkit/chainlink-canton/openapi/gen/eds/ccip" + oapiCCV "github.com/smartcontractkit/chainlink-canton/openapi/gen/eds/ccv" + oapiCommon "github.com/smartcontractkit/chainlink-canton/openapi/gen/eds/common" + oapiExecutor "github.com/smartcontractkit/chainlink-canton/openapi/gen/eds/executor" + oapiTokenPool "github.com/smartcontractkit/chainlink-canton/openapi/gen/eds/tokenpool" + oapiTransferInstruction "github.com/smartcontractkit/chainlink-canton/openapi/gen/transferInstructionV1" + "github.com/smartcontractkit/chainlink-canton/testhelpers" + "github.com/smartcontractkit/chainlink-canton/testhelpers/eds" +) + +var ( + // EDS + edsURL = "" + indexerURL = "" + + // Canton - Local Chain + cantonSelector = chainsel.CANTON_TESTNET.Selector + authorizationServerURL = "" + authClientID = "" + participantGrpcLedgerApiURL = "" + validatorApiURL = "" + userID = "" + partyID = "" + + // Eth - Remote Chain + ethSelector = chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector + ethRpcURL = "" + ethPrivateKeyHex = "" + ethRouterAddress = common.HexToAddress("0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59") + ethTokenAddress = common.HexToAddress("0xeEe6675b20fE5950eb51361b93021D076289F612") + noExecutionTag = common.HexToAddress("0xEBa517d200000000000000000000000000000000") + + // Example Receiver contract that emits an event if a message is received + ccipReceiverContract = common.HexToAddress("0x1E340B34Cc732a71f5e59804da7E645a52e10E1B") + receiverAddress = common.HexToAddress("0x0") +) + +const ( + dsoPartyID = "DSO::1220f22a8b8f2d813c25b9a684dc4dd52b532a0174d8e73a13cdf2baabfff7518337" + ccipOwnerPartyID = "ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551" +) + +var amuletInstrumentID = &splice_api_token_holding_v1.InstrumentId{ + Admin: dsoPartyID, + Id: "Amulet", +} + +var linkInstrumentId = &splice_api_token_holding_v1.InstrumentId{ + Admin: ccipOwnerPartyID, + Id: "link-token", +} + +func TestMulti(t *testing.T) { + authProvider, err := authorizationcode.NewDiscoveryProvider(t.Context(), authorizationServerURL, authClientID) + require.NoError(t, err) + require.NotNil(t, authProvider) + + token, err := authProvider.TokenSource().Token() + require.NoError(t, err) + require.NotNil(t, token) + + fmt.Println(token.AccessToken) + + rpcProviderConfig := provider.RPCChainProviderConfig{ + Participants: []provider.ParticipantConfig{ + { + Endpoints: provider.Endpoints{ + JSONLedgerAPIURL: "json", + GRPCLedgerAPIURL: participantGrpcLedgerApiURL, + ValidatorAPIURL: validatorApiURL, + }, + UserID: userID, + PartyID: partyID, + AuthProvider: authProvider, + }, + }, + } + + chain, err := provider.NewRPCChainProvider(chainsel.CANTON_LOCALNET.Selector, rpcProviderConfig).Initialize(t.Context()) + require.NoError(t, err) + require.NotNil(t, chain) + + cantonChain := chain.(*canton.Chain) + participant := cantonChain.Participants[0] + + _, _, transferInstructionClient, err := testhelpers.NewValidatorAPIClients(participant) + require.NoError(t, err) + + t.Run("GetVersion", func(t *testing.T) { + versionResp, err := participant.LedgerServices.Version.GetLedgerApiVersion(t.Context(), &apiv2.GetLedgerApiVersionRequest{}) + require.NoError(t, err) + + fmt.Println(versionResp.GetVersion()) + }) + + t.Run("AcceptIncomingTransferInstruction", func(t *testing.T) { + transferInstructionCid := "" + + contextResp, err := transferInstructionClient.GetTransferInstructionAcceptContextWithResponse(t.Context(), transferInstructionCid, oapiTransferInstruction.GetTransferInstructionAcceptContextJSONRequestBody{ + Meta: nil, + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, contextResp.StatusCode(), "Unexpected status code, response: %v", string(contextResp.Body)) + + var disclosedContracts []*apiv2.DisclosedContract + for _, contract := range contextResp.JSON200.DisclosedContracts { + id, err := testhelpers.TemplateIdFromString(contract.TemplateId) + require.NoError(t, err) + createdEventBlob, err := base64.StdEncoding.DecodeString(contract.CreatedEventBlob) + require.NoError(t, err) + disclosedContracts = append(disclosedContracts, &apiv2.DisclosedContract{ + TemplateId: id, + ContractId: contract.ContractId, + CreatedEventBlob: createdEventBlob, + SynchronizerId: contract.SynchronizerId, + }) + } + + acceptContext, err := testhelpers.ChoiceContextFromData(contextResp.JSON200.ChoiceContextData) + require.NoError(t, err) + fmt.Println(acceptContext) + + resp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{ + { + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#splice-api-token-transfer-instruction-v1", ModuleName: "Splice.Api.Token.TransferInstructionV1", EntityName: "TransferInstruction"}, + ContractId: transferInstructionCid, + Choice: "TransferInstruction_Accept", + ChoiceArgument: &apiv2.Value{Sum: &apiv2.Value_Record{Record: &apiv2.Record{Fields: []*apiv2.RecordField{ + {Label: "extraArgs", Value: &apiv2.Value{Sum: &apiv2.Value_Record{Record: &apiv2.Record{Fields: []*apiv2.RecordField{ + {Label: "context", Value: acceptContext}, + {Label: "meta", Value: &apiv2.Value{Sum: &apiv2.Value_Record{Record: &apiv2.Record{Fields: []*apiv2.RecordField{{ + Label: "values", + Value: &apiv2.Value{Sum: &apiv2.Value_TextMap{TextMap: &apiv2.TextMap{Entries: nil}}}, + }}}}}}, + }}}}}, + }}}}, + }}, + }, + }, + ActAs: []string{participant.PartyID}, + DisclosedContracts: disclosedContracts, + }, + }) + require.NoError(t, err) + + fmt.Println("Accepted in update: ", resp.GetTransaction().GetUpdateId()) + }) + + t.Run("FilterContracts", func(t *testing.T) { + activeContracts, err := testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-core", ModuleName: "CCIP.Events", EntityName: "CCIPMessageSent"}) + require.NoError(t, err) + for i, contract := range activeContracts { + fmt.Printf("Active contract %d: ID=%s, CreateArguments=%s\n", i, contract.GetCreatedEvent().GetContractId(), contract.GetCreatedEvent().CreateArguments) + } + }) + + t.Run("Enumerate Holdings", func(t *testing.T) { + holdings, err := testhelpers.ListActiveContractsByInterfaceId(t.Context(), participant, &apiv2.Identifier{ + PackageId: "#splice-api-token-holding-v1", + ModuleName: "Splice.Api.Token.HoldingV1", + EntityName: "Holding", + }) + require.NoError(t, err) + for _, holding := range holdings { + for _, view := range holding.GetCreatedEvent().GetInterfaceViews() { + var holdingView splice_api_token_holding_v1.HoldingView + err = ledger.RecordToStruct(view.GetViewValue(), &holdingView) + require.NoError(t, err) + fmt.Printf("InstrumentId: %v@%v Owner: %v Amount: %v \n", holdingView.InstrumentId.Id, holdingView.InstrumentId.Admin, holdingView.Owner, holdingView.Amount) + } + } + }) + + t.Run("Create PerPartyRouter", func(t *testing.T) { + ccipEdsClient, err := oapiCCIP.NewClientWithResponses(edsURL) + require.NoError(t, err) + + routerCid := getRouter(t, participant, ccipEdsClient) + fmt.Println("PerPartyRouter Contract ID: ", routerCid) + }) + + t.Run("Create CCIPReceiver", func(t *testing.T) { + receiverCid := getReceiver(t, participant) + fmt.Println("CCIPReceiver Contract ID: ", receiverCid) + }) + + t.Run("Create CCIPSender", func(t *testing.T) { + senderCid := getSender(t, participant) + fmt.Println("CCIPSender Contract ID: ", senderCid) + }) + + t.Run("Send: EVM -> Canton (Token Transfer, Native)", func(t *testing.T) { + // Source + finality := protocol.FinalityWaitForFinality + execGasLimit := uint32(0) + executorAddress := noExecutionTag + + // Dest + receiverParty := participant.PartyID + + // Message + tokenAmount := big.NewInt(1e18) + + chainIdString, err := chainsel.GetChainIDFromSelector(ethSelector) + require.NoError(t, err) + sourceChainId, err := strconv.ParseUint(chainIdString, 10, 64) + require.NoError(t, err) + client, err := ethclient.DialContext(t.Context(), ethRpcURL) + require.NoError(t, err) + privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(ethPrivateKeyHex, "0x")) + require.NoError(t, err) + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, new(big.Int).SetUint64(sourceChainId)) + require.NoError(t, err) + + router, err := routerwrapper.NewRouter(ethRouterAddress, client) + require.NoError(t, err) + + receiverPartyHashed := contracts.HashedPartyFromString(receiverParty) + + extraArgs, err := evm.NewV3ExtraArgs(finality, execGasLimit, executorAddress.Hex(), nil, nil, nil, nil) + require.NoError(t, err) + + msg := routerwrapper.ClientEVM2AnyMessage{ + Receiver: receiverPartyHashed.Bytes(), + Data: nil, + TokenAmounts: []routerwrapper.ClientEVMTokenAmount{{ + Token: ethTokenAddress, + Amount: tokenAmount, + }}, + FeeToken: common.Address{}, // Native + ExtraArgs: extraArgs, + } + + fee, err := router.GetFee(&bind.CallOpts{Context: t.Context()}, cantonSelector, msg) + require.NoError(t, err) + fmt.Printf("Fee for sending message: %s\n", fee.String()) + auth.Value = fee + + tx, err := router.CcipSend(auth, cantonSelector, msg) + require.NoError(t, err) + fmt.Printf("Sent message with tx hash: %s\n", tx.Hash().Hex()) + + receipt, err := bind.WaitMined(t.Context(), client, tx) + require.NoError(t, err) + fmt.Printf("Transaction mined in block: %d\n", receipt.BlockNumber.Uint64()) + + var sentEvent *onramp.OnRampCCIPMessageSent + eventTopic := (onramp.OnRampCCIPMessageSent{}).Topic() + for _, lg := range receipt.Logs { + if len(lg.Topics) == 0 || lg.Topics[0] != eventTopic { + continue + } + + onRamp, err := onramp.NewOnRamp(lg.Address, client) + require.NoError(t, err) + + messageSentEvent, err := onRamp.ParseCCIPMessageSent(*lg) + require.NoError(t, err) + sentEvent = messageSentEvent + break + } + require.NotNil(t, sentEvent, "Sent event not found in transaction logs") + fmt.Printf("Message sent with ID: %s\n", common.Bytes2Hex(sentEvent.MessageId[:])) + }) + + t.Run("Execute: Canton (Token Transfer)", func(t *testing.T) { + messageId := common.HexToHash("3bb03d186c6adb90baab2004d683dca394804e721c9bda207d4a72ba3d86a351") + timeout := time.Minute + + indexer, err := indexerclient.NewIndexerClient(indexerURL, &http.Client{Timeout: 15 * time.Second}) + require.NoError(t, err) + ccipEdsClient, err := oapiCCIP.NewClientWithResponses(edsURL) + require.NoError(t, err) + ccvEdsClient, err := oapiCCV.NewClientWithResponses(edsURL) + require.NoError(t, err) + tokenPoolEdsClient, err := oapiTokenPool.NewClientWithResponses(edsURL) + require.NoError(t, err) + + start := time.Now() + var response v1.VerifierResultsByMessageIDResponse + for { + if time.Now().Sub(start) > timeout { + t.Fatal("Timed out waiting for message to be processed by indexer") + } + status, resp, err := indexer.VerifierResultsByMessageID(t.Context(), v1.VerifierResultsByMessageIDInput{MessageID: messageId.Hex()}) + if err != nil { + t.Logf("Error querying indexer: %v", err) + time.Sleep(5 * time.Second) + continue + } else if status != http.StatusOK { + t.Logf("Unexpected status code from indexer: %d", status) + time.Sleep(5 * time.Second) + continue + } + + response = resp + break + } + require.NotEmptyf(t, response.Results, "Expected at least one verifier result for message ID %s", messageId.Hex()) + t.Logf("Received response from indexer: %+v", response) + + verifierResult := response.Results[0].VerifierResult + message := verifierResult.Message + encodedMessage, err := message.Encode() + require.NoError(t, err) + + targetInstrumentId := contracts.BytesToEncodedInstrumentID(message.TokenTransfer.DestTokenAddress) + ccvAddress := contracts.BytesToInstanceAddress(verifierResult.VerifierDestAddress) + + tokenPoolAddress, err := eds.GetTokenPoolForToken(t.Context(), ccipEdsClient, targetInstrumentId) + require.NoError(t, err) + ccipExecuteDisclosure, err := eds.GetCCIPExecuteDisclosure(t.Context(), ccipEdsClient, hex.EncodeToString(encodedMessage)) + require.NoError(t, err) + ccvExecuteDisclosure, err := eds.GetCCVExecuteDisclosure(t.Context(), ccvEdsClient, hex.EncodeToString(encodedMessage), ccvAddress) + require.NoError(t, err) + tokenPoolExecuteDisclosure, err := eds.GetTokenPoolExecuteDisclosure(t.Context(), tokenPoolEdsClient, hex.EncodeToString(encodedMessage), tokenPoolAddress.InstanceAddress()) + require.NoError(t, err) + + fmt.Println("CCIP Execute Disclosure:", ccipExecuteDisclosure) + fmt.Println("CCV Execute Disclosure:", ccvExecuteDisclosure) + fmt.Println("Token Pool Execute Disclosure:", tokenPoolExecuteDisclosure) + + // Get PerPartyRouter + routerCid := getRouter(t, participant, ccipEdsClient) + + // Get CCIPReceiver + receiverCid := getReceiver(t, participant) + + fmt.Println("PerPartyRouter CID:", routerCid) + fmt.Println("CCIPReceiver CID:", receiverCid) + + executeArgs := receiver.Execute{ + Context: ccipExecuteDisclosure.ChoiceContext, + RouterCid: types.CONTRACT_ID(routerCid), + EncodedMessage: types.TEXT(hex.EncodeToString(encodedMessage)), + TokenTransfer: &receiver.TokenTransferInput{ + TokenPoolCid: types.CONTRACT_ID(tokenPoolExecuteDisclosure.ContractId), + TokenReceiverParty: types.PARTY(participant.PartyID), + PoolExtraContext: tokenPoolExecuteDisclosure.ChoiceContext, + }, + CcvInputs: []receiver.CCVInput{ + { + CcvCid: types.CONTRACT_ID(ccvExecuteDisclosure.ContractId), + VerifierResults: types.TEXT(hex.EncodeToString(verifierResult.CCVData)), + CcvExtraContext: ccvExecuteDisclosure.ChoiceContext, + }, + }, + } + + resp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-receiver", ModuleName: "CCIP.CCIPReceiver", EntityName: "CCIPReceiver"}, + ContractId: receiverCid, + Choice: "Execute", + ChoiceArgument: ledger.MapToValue(executeArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: slices.Concat( + tokenPoolExecuteDisclosure.DisclosedContracts, + ccipExecuteDisclosure.DisclosedContracts, + ccvExecuteDisclosure.DisclosedContracts, + ), + }, + }) + require.NoError(t, err) + fmt.Println("Message executed in Update: ", resp.GetTransaction().GetUpdateId()) + }) + + t.Run("Send: Canton->EVM (Message Only, Native)", func(t *testing.T) { + messageReceiver := ccipReceiverContract + + ccipEdsClient, err := oapiCCIP.NewClientWithResponses(edsURL) + require.NoError(t, err) + ccvEdsClient, err := oapiCCV.NewClientWithResponses(edsURL) + require.NoError(t, err) + executorEdsClient, err := oapiExecutor.NewClientWithResponses(edsURL) + require.NoError(t, err) + + // Get Fee Input holding + feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, amuletInstrumentID) + require.NoError(t, err) + + transferFactoryCid, transferFactoryDisclosures, choiceContextRaw, err := testhelpers.GetTransferFactory(t.Context(), transferInstructionClient, string(amuletInstrumentID.Admin), participant.PartyID, ccipOwnerPartyID) + require.NoError(t, err) + choiceContext, err := contracts.ChoiceContextFromData(choiceContextRaw) + require.NoError(t, err) + + // Get PerPartyRouter + routerCid := getRouter(t, participant, ccipEdsClient) + + // Get CCIPSender + senderCid := getSender(t, participant) + + msg := oapiCommon.Message{ + DestinationChainSelector: strconv.FormatUint(ethSelector, 10), + Executor: struct { + Address *oapiCommon.RawOrHashedAddress `json:"address,omitempty"` + Type oapiCommon.MessageExecutorType `json:"type"` + }{ + Type: oapiCommon.Empty, + }, + FeeToken: oapiCommon.InstrumentId{ + Admin: oapiCommon.PartyId(amuletInstrumentID.Admin), + Id: string(amuletInstrumentID.Id), + }, + GasLimit: 50_000, + Payload: hex.EncodeToString([]byte("Hello, EVM from Canton!")), + Receiver: hex.EncodeToString(messageReceiver.Bytes()), + TokenTransfer: nil, + } + + ccipSendDisclosure, err := eds.GetCCIPSendDisclosure(t.Context(), ccipEdsClient, msg, nil, nil) + require.NoError(t, err) + // EDS returns the default CCV(s) if no input is specified + defaultCCVAddress, err := contracts.RawInstanceAddressFromString(ccipSendDisclosure.CCVs[0]) + require.NoError(t, err) + // EDS returns the default Executor (if set) if no specific executor input is specified + defaultExecutorAddress, err := contracts.RawInstanceAddressFromString(*ccipSendDisclosure.Executor) + require.NoError(t, err) + ccvSendDisclosure, err := eds.GetCCVSendDisclosure(t.Context(), ccvEdsClient, msg, defaultCCVAddress.InstanceAddress()) + require.NoError(t, err) + executorSendDisclosure, err := eds.GetExecutorSendDisclosure(t.Context(), executorEdsClient, msg, defaultExecutorAddress.InstanceAddress(), ccipSendDisclosure.CCVs) + require.NoError(t, err) + + feeTokenInputCids := make([]types.CONTRACT_ID, len(feeTokenHoldings)) + for i, holding := range feeTokenHoldings { + feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } + + sendArgs := sender.Send{ + DestinationChainSelector: types.NUMERIC(msg.DestinationChainSelector), + Message: core.Canton2AnyMessage{ + Receiver: types.TEXT(msg.Receiver), + Payload: types.TEXT(msg.Payload), + TokenTransfer: nil, + FeeToken: *amuletInstrumentID, + ExtraArgs: core.ExtraArgs{ + V3: &core.GenericExtraArgsV3{ + GasLimit: types.INT64(msg.GasLimit), + Ccvs: nil, + Executor: core.ExecutorExtraArg{ + ExecutorUseDefault: &core.ExecutorUseDefault{ExecutorArgs: ""}, + }, + TokenReceiver: "", + TokenArgs: "", + }, + }, + }, + Context: ccipSendDisclosure.ChoiceContext, + RouterCid: types.CONTRACT_ID(routerCid), + FeeTokenInput: sender.FeeTokenInput{ + SenderInputCids: feeTokenInputCids, + FeeTokenConfigCid: types.CONTRACT_ID(ccipSendDisclosure.FeeTokenConfigCid), + FeeTokenTransferFactory: types.CONTRACT_ID(transferFactoryCid), + FeeTokenExtraArgs: splice_api_token_metadata_v1.ExtraArgs{ + Context: choiceContext, + Meta: splice_api_token_metadata_v1.Metadata{ + Values: map[string]types.TEXT{}, + }, + }, + }, + CcvSendInputs: []sender.CCVSendInput{ + { + CcvAddress: ccvSendDisclosure.Address.Binding(), + CcvCid: types.CONTRACT_ID(ccvSendDisclosure.ContractId), + CcvExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, + }, + }, + TokenTransferInput: nil, + ExecutorInput: &sender.ExecutorInput{ + ExecutorCid: types.CONTRACT_ID(executorSendDisclosure.ContractId), + ExecutorExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, + }, + } + allDisclosures := slices.Concat( + transferFactoryDisclosures, + ccipSendDisclosure.DisclosedContracts, + ccvSendDisclosure.DisclosedContracts, + executorSendDisclosure.DisclosedContracts, + ) + resp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.NoError(t, err) + fmt.Println("Message sent in Update: ", resp.GetTransaction().GetUpdateId()) + + var returnedMessageId string + for _, event := range resp.GetTransaction().GetEvents() { + if e, ok := event.GetEvent().(*apiv2.Event_Created); ok { + if e.Created.GetTemplateId().GetEntityName() == "CCIPMessageSent" { + ccipMessageSent, err := bindings.UnmarshalCreatedEvent[core.CCIPMessageSent](e.Created) + require.NoError(t, err) + returnedMessageId = string(ccipMessageSent.Event.MessageId) + break + } + } + } + require.NotEmpty(t, returnedMessageId) + fmt.Println("Message sent with MessageID: ", returnedMessageId) + }) + + t.Run("Send: Canton->EVM (Token Only, Native)", func(t *testing.T) { + // The address that will receive the tokens + messageReceiver := common.HexToAddress("0x90392A1E8A941098a3C75E0BDB172cFdE7E4f1f4") + + ccipEdsClient, err := oapiCCIP.NewClientWithResponses(edsURL) + require.NoError(t, err) + ccvEdsClient, err := oapiCCV.NewClientWithResponses(edsURL) + require.NoError(t, err) + executorEdsClient, err := oapiExecutor.NewClientWithResponses(edsURL) + require.NoError(t, err) + tokenPoolEdsClient, err := oapiTokenPool.NewClientWithResponses(edsURL) + require.NoError(t, err) + + // Get Fee Input holding + feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, amuletInstrumentID) + require.NoError(t, err) + + transferFactoryCid, transferFactoryDisclosures, choiceContextRaw, err := testhelpers.GetTransferFactory(t.Context(), transferInstructionClient, string(amuletInstrumentID.Admin), participant.PartyID, ccipOwnerPartyID) + require.NoError(t, err) + choiceContext, err := contracts.ChoiceContextFromData(choiceContextRaw) + require.NoError(t, err) + + // Get Token input holding + tokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, linkInstrumentId) + require.NoError(t, err) + + // Get PerPartyRouter + routerCid := getRouter(t, participant, ccipEdsClient) + + // Get CCIPSender + senderCid := getSender(t, participant) + + msg := oapiCommon.Message{ + DestinationChainSelector: strconv.FormatUint(ethSelector, 10), + Executor: struct { + Address *oapiCommon.RawOrHashedAddress `json:"address,omitempty"` + Type oapiCommon.MessageExecutorType `json:"type"` + }{ + Type: oapiCommon.Empty, + }, + FeeToken: oapiCommon.InstrumentId{ + Admin: oapiCommon.PartyId(amuletInstrumentID.Admin), + Id: string(amuletInstrumentID.Id), + }, + GasLimit: 0, + Payload: "", + Receiver: hex.EncodeToString(messageReceiver.Bytes()), + TokenTransfer: &oapiCommon.TokenTransfer{ + Amount: "0.5", + Token: oapiCommon.InstrumentId{ + Admin: oapiCommon.PartyId(linkInstrumentId.Admin), + Id: string(linkInstrumentId.Id), + }, + }, + } + + tokenPoolAddress, err := eds.GetTokenPoolForToken(t.Context(), ccipEdsClient, contracts.EncodeInstrumentID(*linkInstrumentId)) + require.NoError(t, err) + tokenPoolSendDisclosure, err := eds.GetTokenPoolSendDisclosure(t.Context(), tokenPoolEdsClient, msg, tokenPoolAddress.InstanceAddress()) + require.NoError(t, err) + ccipSendDisclosure, err := eds.GetCCIPSendDisclosure(t.Context(), ccipEdsClient, msg, nil, tokenPoolSendDisclosure.RequiredCCVs) + require.NoError(t, err) + // EDS returns the default CCV(s) if no input is specified + defaultCCVAddress, err := contracts.RawInstanceAddressFromString(ccipSendDisclosure.CCVs[0]) + require.NoError(t, err) + // EDS returns the default Executor (if set) if no specific executor input is specified + defaultExecutorAddress, err := contracts.RawInstanceAddressFromString(*ccipSendDisclosure.Executor) + require.NoError(t, err) + ccvSendDisclosure, err := eds.GetCCVSendDisclosure(t.Context(), ccvEdsClient, msg, defaultCCVAddress.InstanceAddress()) + require.NoError(t, err) + executorSendDisclosure, err := eds.GetExecutorSendDisclosure(t.Context(), executorEdsClient, msg, defaultExecutorAddress.InstanceAddress(), ccipSendDisclosure.CCVs) + require.NoError(t, err) + + feeTokenInputCids := make([]types.CONTRACT_ID, len(feeTokenHoldings)) + for i, holding := range feeTokenHoldings { + feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } + tokenTransferInputCids := make([]types.CONTRACT_ID, len(tokenHoldings)) + for i, holding := range tokenHoldings { + tokenTransferInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } + + sendArgs := sender.Send{ + DestinationChainSelector: types.NUMERIC(msg.DestinationChainSelector), + Message: core.Canton2AnyMessage{ + Receiver: types.TEXT(msg.Receiver), + Payload: types.TEXT(msg.Payload), + TokenTransfer: &core.TokenTransfer{ + Token: *linkInstrumentId, + Amount: types.NUMERIC(msg.TokenTransfer.Amount), + }, + FeeToken: *amuletInstrumentID, + ExtraArgs: core.ExtraArgs{ + V3: &core.GenericExtraArgsV3{ + GasLimit: types.INT64(msg.GasLimit), + Ccvs: nil, + Executor: core.ExecutorExtraArg{ + ExecutorUseDefault: &core.ExecutorUseDefault{ExecutorArgs: ""}, + }, + TokenReceiver: "", + TokenArgs: "", + }, + }, + }, + Context: ccipSendDisclosure.ChoiceContext, + RouterCid: types.CONTRACT_ID(routerCid), + FeeTokenInput: sender.FeeTokenInput{ + SenderInputCids: feeTokenInputCids, + FeeTokenConfigCid: types.CONTRACT_ID(ccipSendDisclosure.FeeTokenConfigCid), + FeeTokenTransferFactory: types.CONTRACT_ID(transferFactoryCid), + FeeTokenExtraArgs: splice_api_token_metadata_v1.ExtraArgs{ + Context: choiceContext, + Meta: splice_api_token_metadata_v1.Metadata{ + Values: map[string]types.TEXT{}, + }, + }, + }, + CcvSendInputs: []sender.CCVSendInput{ + { + CcvAddress: ccvSendDisclosure.Address.Binding(), + CcvCid: types.CONTRACT_ID(ccvSendDisclosure.ContractId), + CcvExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, + }, + }, + TokenTransferInput: &sender.TokenTransferInput{ + SenderInputCids: tokenTransferInputCids, + TokenPoolCid: types.CONTRACT_ID(tokenPoolSendDisclosure.ContractId), + PoolExtraContext: tokenPoolSendDisclosure.ChoiceContext, + }, + ExecutorInput: &sender.ExecutorInput{ + ExecutorCid: types.CONTRACT_ID(executorSendDisclosure.ContractId), + ExecutorExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, + }, + } + allDisclosures := slices.Concat( + transferFactoryDisclosures, + tokenPoolSendDisclosure.DisclosedContracts, + ccipSendDisclosure.DisclosedContracts, + ccvSendDisclosure.DisclosedContracts, + executorSendDisclosure.DisclosedContracts, + ) + resp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.NoError(t, err) + fmt.Println("Message sent in Update: ", resp.GetTransaction().GetUpdateId()) + + var returnedMessageId string + for _, event := range resp.GetTransaction().GetEvents() { + if e, ok := event.GetEvent().(*apiv2.Event_Created); ok { + if e.Created.GetTemplateId().GetEntityName() == "CCIPMessageSent" { + ccipMessageSent, err := bindings.UnmarshalCreatedEvent[core.CCIPMessageSent](e.Created) + require.NoError(t, err) + returnedMessageId = string(ccipMessageSent.Event.MessageId) + break + } + } + } + require.NotEmpty(t, returnedMessageId) + fmt.Println("Message sent with MessageID: ", returnedMessageId) + }) +} + +// getRouter returns the PerPartyRouter for the participant's party, or creates one if it doesn't exist yet. +func getRouter(t *testing.T, participant canton.Participant, ccipEdsClient oapiCCIP.ClientWithResponsesInterface) string { + // Get active contracts. If one exists, return it + activeContracts, err := testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-runtime", ModuleName: "CCIP.PerPartyRouter", EntityName: "PerPartyRouter"}) + require.NoError(t, err) + if len(activeContracts) > 0 { + return activeContracts[0].GetCreatedEvent().GetContractId() + } + + t.Logf("No active PerPartyRouter found for party %s, deploying one...", participant.PartyID) + disclosures, err := eds.GetPerPartyRouterFactoryDisclosure(t.Context(), ccipEdsClient, participant.PartyID) + require.NoError(t, err) + + resp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-runtime", ModuleName: "CCIP.PerPartyRouter", EntityName: "PerPartyRouterFactory"}, + ContractId: disclosures.ContractId, + Choice: "CreateRouter", + ChoiceArgument: ledger.MapToValue(ccipruntime.CreateRouter{ + PartyOwner: types.PARTY(participant.PartyID), + InstanceId: "default-router", + }), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: disclosures.DisclosedContracts, + }, + }) + require.NoError(t, err) + t.Logf("Created PerPartyRouter in update: %v", resp.GetTransaction().GetUpdateId()) + + activeContracts, err = testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-runtime", ModuleName: "CCIP.PerPartyRouter", EntityName: "PerPartyRouterFactory"}) + require.NoError(t, err) + require.NotEmptyf(t, activeContracts, "Expected to find active PerPartyRouter after creation for party %s", participant.PartyID) + + return activeContracts[0].GetCreatedEvent().GetContractId() +} + +func getSender(t *testing.T, participant canton.Participant) string { + activeContracts, err := testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}) + require.NoError(t, err) + if len(activeContracts) > 0 { + return activeContracts[0].GetCreatedEvent().GetContractId() + } + + t.Logf("No active CCIPSender found for party %s, deploying one...", participant.PartyID) + resp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Create{Create: &apiv2.CreateCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + CreateArguments: &apiv2.Record{Fields: []*apiv2.RecordField{ + {Label: "instanceId", Value: &apiv2.Value{Sum: &apiv2.Value_Text{Text: "ccipsender"}}}, + {Label: "owner", Value: &apiv2.Value{Sum: &apiv2.Value_Party{Party: participant.PartyID}}}, + }}, + }}, + }}, + ActAs: []string{participant.PartyID}, + }, + }) + require.NoError(t, err) + t.Logf("Created CCIPSender in update: %v", resp.GetTransaction().GetUpdateId()) + + activeContracts, err = testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}) + require.NoError(t, err) + require.NotEmptyf(t, activeContracts, "Expected to find active CCIPSender after creation for party %s", participant.PartyID) + + return activeContracts[0].GetCreatedEvent().GetContractId() +} + +func getReceiver(t *testing.T, participant canton.Participant) string { + activeContracts, err := testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-receiver", ModuleName: "CCIP.CCIPReceiver", EntityName: "CCIPReceiver"}) + require.NoError(t, err) + if len(activeContracts) > 0 { + return activeContracts[0].GetCreatedEvent().GetContractId() + } + + t.Logf("No active CCIPReceiver found for party %s, deploying one...", participant.PartyID) + resp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Create{Create: &apiv2.CreateCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-receiver", ModuleName: "CCIP.CCIPReceiver", EntityName: "CCIPReceiver"}, + CreateArguments: &apiv2.Record{Fields: []*apiv2.RecordField{ + {Label: "instanceId", Value: &apiv2.Value{Sum: &apiv2.Value_Text{Text: "ccipreceiver-waitForFinality"}}}, + {Label: "owner", Value: &apiv2.Value{Sum: &apiv2.Value_Party{Party: participant.PartyID}}}, + {Label: "receiverFinalityConfig", Value: &apiv2.Value{Sum: &apiv2.Value_Variant{Variant: &apiv2.Variant{ + Constructor: "WaitForFinality", + Value: &apiv2.Value{Sum: &apiv2.Value_Unit{}}, + }}}}, + {Label: "requiredCCVs", Value: &apiv2.Value{Sum: &apiv2.Value_List{List: &apiv2.List{Elements: nil}}}}, + {Label: "optionalCCVs", Value: &apiv2.Value{Sum: &apiv2.Value_List{List: &apiv2.List{Elements: nil}}}}, + {Label: "optionalThreshold", Value: &apiv2.Value{Sum: &apiv2.Value_Int64{Int64: 0}}}, + }}, + }}, + }}, + ActAs: []string{participant.PartyID}, + }, + }) + require.NoError(t, err) + t.Logf("Created CCIPReceiver in update: %v", resp.GetTransaction().GetUpdateId()) + + activeContracts, err = testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-receiver", ModuleName: "CCIP.CCIPReceiver", EntityName: "CCIPReceiver"}) + require.NoError(t, err) + require.NotEmptyf(t, activeContracts, "Expected to find active CCIPReceiver after creation for party %s", participant.PartyID) + + return activeContracts[0].GetCreatedEvent().GetContractId() +} From 37d4536ff294f75431e1b24cca03950ce1eec04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:05:37 +0200 Subject: [PATCH 2/6] Update --- examples/tests/examples_test.go | 215 +++++++++++++++++--------------- 1 file changed, 117 insertions(+), 98 deletions(-) diff --git a/examples/tests/examples_test.go b/examples/tests/examples_test.go index 697ed6259..294acf695 100644 --- a/examples/tests/examples_test.go +++ b/examples/tests/examples_test.go @@ -1,8 +1,10 @@ +//nolint:paralleltest package tests import ( "encoding/base64" "encoding/hex" + "encoding/json" "fmt" "math/big" "net/http" @@ -39,6 +41,7 @@ import ( "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/sender" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1" "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_metadata_v1" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_transfer_instruction_v1" "github.com/smartcontractkit/chainlink-canton/contracts" "github.com/smartcontractkit/chainlink-canton/deployment/authentication/authorizationcode" oapiCCIP "github.com/smartcontractkit/chainlink-canton/openapi/gen/eds/ccip" @@ -102,8 +105,6 @@ func TestMulti(t *testing.T) { require.NoError(t, err) require.NotNil(t, token) - fmt.Println(token.AccessToken) - rpcProviderConfig := provider.RPCChainProviderConfig{ Participants: []provider.ParticipantConfig{ { @@ -126,7 +127,22 @@ func TestMulti(t *testing.T) { cantonChain := chain.(*canton.Chain) participant := cantonChain.Participants[0] - _, _, transferInstructionClient, err := testhelpers.NewValidatorAPIClients(participant) + // Create HTTP clients + _, _, amuletTransferInstructionClient, err := testhelpers.NewValidatorAPIClients(participant) + require.NoError(t, err) + + ccipEdsClient, err := oapiCCIP.NewClientWithResponses(edsURL) + require.NoError(t, err) + ccvEdsClient, err := oapiCCV.NewClientWithResponses(edsURL) + require.NoError(t, err) + executorEdsClient, err := oapiExecutor.NewClientWithResponses(edsURL) + require.NoError(t, err) + tokenPoolEdsClient, err := oapiTokenPool.NewClientWithResponses(edsURL) + require.NoError(t, err) + // transferInstructionEdsClient, err := oapiTransferInstruction.NewClientWithResponses(edsURL) + require.NoError(t, err) + + indexerClient, err := indexerclient.NewIndexerClient(indexerURL, &http.Client{Timeout: 15 * time.Second}) require.NoError(t, err) t.Run("GetVersion", func(t *testing.T) { @@ -139,24 +155,24 @@ func TestMulti(t *testing.T) { t.Run("AcceptIncomingTransferInstruction", func(t *testing.T) { transferInstructionCid := "" - contextResp, err := transferInstructionClient.GetTransferInstructionAcceptContextWithResponse(t.Context(), transferInstructionCid, oapiTransferInstruction.GetTransferInstructionAcceptContextJSONRequestBody{ + contextResp, err := amuletTransferInstructionClient.GetTransferInstructionAcceptContextWithResponse(t.Context(), transferInstructionCid, oapiTransferInstruction.GetTransferInstructionAcceptContextJSONRequestBody{ Meta: nil, }) require.NoError(t, err) require.Equal(t, http.StatusOK, contextResp.StatusCode(), "Unexpected status code, response: %v", string(contextResp.Body)) - var disclosedContracts []*apiv2.DisclosedContract - for _, contract := range contextResp.JSON200.DisclosedContracts { + disclosedContracts := make([]*apiv2.DisclosedContract, len(contextResp.JSON200.DisclosedContracts)) + for i, contract := range contextResp.JSON200.DisclosedContracts { id, err := testhelpers.TemplateIdFromString(contract.TemplateId) require.NoError(t, err) createdEventBlob, err := base64.StdEncoding.DecodeString(contract.CreatedEventBlob) require.NoError(t, err) - disclosedContracts = append(disclosedContracts, &apiv2.DisclosedContract{ + disclosedContracts[i] = &apiv2.DisclosedContract{ TemplateId: id, ContractId: contract.ContractId, CreatedEventBlob: createdEventBlob, SynchronizerId: contract.SynchronizerId, - }) + } } acceptContext, err := testhelpers.ChoiceContextFromData(contextResp.JSON200.ChoiceContextData) @@ -193,11 +209,29 @@ func TestMulti(t *testing.T) { fmt.Println("Accepted in update: ", resp.GetTransaction().GetUpdateId()) }) - t.Run("FilterContracts", func(t *testing.T) { + t.Run("List CCIPMessageSent", func(t *testing.T) { activeContracts, err := testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-core", ModuleName: "CCIP.Events", EntityName: "CCIPMessageSent"}) require.NoError(t, err) for i, contract := range activeContracts { - fmt.Printf("Active contract %d: ID=%s, CreateArguments=%s\n", i, contract.GetCreatedEvent().GetContractId(), contract.GetCreatedEvent().CreateArguments) + messageSentEvent, err := bindings.UnmarshalCreatedEvent[core.CCIPMessageSent](contract.GetCreatedEvent()) + require.NoError(t, err) + data, err := json.MarshalIndent(messageSentEvent, "", "\t") + require.NoError(t, err) + fmt.Printf(" ---------- Active contract %d: Contract ID=%s ----------\n", i, contract.GetCreatedEvent().GetContractId()) + fmt.Println(string(data)) + } + }) + + t.Run("List ExecutionStateChanged", func(t *testing.T) { + activeContracts, err := testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-core", ModuleName: "CCIP.Events", EntityName: "ExecutionStateChanged"}) + require.NoError(t, err) + for i, contract := range activeContracts { + messageSentEvent, err := bindings.UnmarshalCreatedEvent[core.ExecutionStateChanged](contract.GetCreatedEvent()) + require.NoError(t, err) + data, err := json.MarshalIndent(messageSentEvent, "", "\t") + require.NoError(t, err) + fmt.Printf(" ---------- Active contract %d: Contract ID=%s ----------\n", i, contract.GetCreatedEvent().GetContractId()) + fmt.Println(string(data)) } }) @@ -219,9 +253,6 @@ func TestMulti(t *testing.T) { }) t.Run("Create PerPartyRouter", func(t *testing.T) { - ccipEdsClient, err := oapiCCIP.NewClientWithResponses(edsURL) - require.NoError(t, err) - routerCid := getRouter(t, participant, ccipEdsClient) fmt.Println("PerPartyRouter Contract ID: ", routerCid) }) @@ -246,7 +277,7 @@ func TestMulti(t *testing.T) { receiverParty := participant.PartyID // Message - tokenAmount := big.NewInt(1e18) + tokenAmount := big.NewInt(5e18) chainIdString, err := chainsel.GetChainIDFromSelector(ethSelector) require.NoError(t, err) @@ -304,6 +335,7 @@ func TestMulti(t *testing.T) { messageSentEvent, err := onRamp.ParseCCIPMessageSent(*lg) require.NoError(t, err) sentEvent = messageSentEvent + break } require.NotNil(t, sentEvent, "Sent event not found in transaction logs") @@ -311,25 +343,16 @@ func TestMulti(t *testing.T) { }) t.Run("Execute: Canton (Token Transfer)", func(t *testing.T) { - messageId := common.HexToHash("3bb03d186c6adb90baab2004d683dca394804e721c9bda207d4a72ba3d86a351") - timeout := time.Minute - - indexer, err := indexerclient.NewIndexerClient(indexerURL, &http.Client{Timeout: 15 * time.Second}) - require.NoError(t, err) - ccipEdsClient, err := oapiCCIP.NewClientWithResponses(edsURL) - require.NoError(t, err) - ccvEdsClient, err := oapiCCV.NewClientWithResponses(edsURL) - require.NoError(t, err) - tokenPoolEdsClient, err := oapiTokenPool.NewClientWithResponses(edsURL) - require.NoError(t, err) + messageId := common.HexToHash("1a5650e2fc6e8f874274b836122c8ccf99a52c96786cd0264669454b0aa145de") + timeout := time.Minute * 15 start := time.Now() var response v1.VerifierResultsByMessageIDResponse for { - if time.Now().Sub(start) > timeout { + if time.Since(start) > timeout { t.Fatal("Timed out waiting for message to be processed by indexer") } - status, resp, err := indexer.VerifierResultsByMessageID(t.Context(), v1.VerifierResultsByMessageIDInput{MessageID: messageId.Hex()}) + status, resp, err := indexerClient.VerifierResultsByMessageID(t.Context(), v1.VerifierResultsByMessageIDInput{MessageID: messageId.Hex()}) if err != nil { t.Logf("Error querying indexer: %v", err) time.Sleep(5 * time.Second) @@ -341,6 +364,7 @@ func TestMulti(t *testing.T) { } response = resp + break } require.NotEmptyf(t, response.Results, "Expected at least one verifier result for message ID %s", messageId.Hex()) @@ -419,21 +443,26 @@ func TestMulti(t *testing.T) { t.Run("Send: Canton->EVM (Message Only, Native)", func(t *testing.T) { messageReceiver := ccipReceiverContract + feeToken := amuletInstrumentID - ccipEdsClient, err := oapiCCIP.NewClientWithResponses(edsURL) - require.NoError(t, err) - ccvEdsClient, err := oapiCCV.NewClientWithResponses(edsURL) - require.NoError(t, err) - executorEdsClient, err := oapiExecutor.NewClientWithResponses(edsURL) - require.NoError(t, err) - - // Get Fee Input holding + // Get fee token input holdings feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, amuletInstrumentID) require.NoError(t, err) + feeTokenInputCids := make([]types.CONTRACT_ID, len(feeTokenHoldings)) + for i, holding := range feeTokenHoldings { + feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } - transferFactoryCid, transferFactoryDisclosures, choiceContextRaw, err := testhelpers.GetTransferFactory(t.Context(), transferInstructionClient, string(amuletInstrumentID.Admin), participant.PartyID, ccipOwnerPartyID) + transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), amuletTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ + Sender: types.PARTY(participant.PartyID), + Receiver: ccipOwnerPartyID, + Amount: "1.0", + InstrumentId: *feeToken, + InputHoldingCids: feeTokenInputCids, + Meta: splice_api_token_metadata_v1.Metadata{Values: map[string]types.TEXT{}}, + }) require.NoError(t, err) - choiceContext, err := contracts.ChoiceContextFromData(choiceContextRaw) + choiceContext, err := contracts.ChoiceContextFromData(transferFactory.ChoiceContextData) require.NoError(t, err) // Get PerPartyRouter @@ -473,11 +502,6 @@ func TestMulti(t *testing.T) { executorSendDisclosure, err := eds.GetExecutorSendDisclosure(t.Context(), executorEdsClient, msg, defaultExecutorAddress.InstanceAddress(), ccipSendDisclosure.CCVs) require.NoError(t, err) - feeTokenInputCids := make([]types.CONTRACT_ID, len(feeTokenHoldings)) - for i, holding := range feeTokenHoldings { - feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) - } - sendArgs := sender.Send{ DestinationChainSelector: types.NUMERIC(msg.DestinationChainSelector), Message: core.Canton2AnyMessage{ @@ -502,7 +526,7 @@ func TestMulti(t *testing.T) { FeeTokenInput: sender.FeeTokenInput{ SenderInputCids: feeTokenInputCids, FeeTokenConfigCid: types.CONTRACT_ID(ccipSendDisclosure.FeeTokenConfigCid), - FeeTokenTransferFactory: types.CONTRACT_ID(transferFactoryCid), + FeeTokenTransferFactory: types.CONTRACT_ID(transferFactory.FactoryID), FeeTokenExtraArgs: splice_api_token_metadata_v1.ExtraArgs{ Context: choiceContext, Meta: splice_api_token_metadata_v1.Metadata{ @@ -524,7 +548,7 @@ func TestMulti(t *testing.T) { }, } allDisclosures := slices.Concat( - transferFactoryDisclosures, + transferFactory.DisclosedContracts, ccipSendDisclosure.DisclosedContracts, ccvSendDisclosure.DisclosedContracts, executorSendDisclosure.DisclosedContracts, @@ -547,46 +571,42 @@ func TestMulti(t *testing.T) { require.NoError(t, err) fmt.Println("Message sent in Update: ", resp.GetTransaction().GetUpdateId()) - var returnedMessageId string - for _, event := range resp.GetTransaction().GetEvents() { - if e, ok := event.GetEvent().(*apiv2.Event_Created); ok { - if e.Created.GetTemplateId().GetEntityName() == "CCIPMessageSent" { - ccipMessageSent, err := bindings.UnmarshalCreatedEvent[core.CCIPMessageSent](e.Created) - require.NoError(t, err) - returnedMessageId = string(ccipMessageSent.Event.MessageId) - break - } - } - } - require.NotEmpty(t, returnedMessageId) - fmt.Println("Message sent with MessageID: ", returnedMessageId) + messageId := getMessageIdFromTransaction(t, resp.GetTransaction()) + fmt.Println("Message sent with MessageID: ", messageId) }) t.Run("Send: Canton->EVM (Token Only, Native)", func(t *testing.T) { // The address that will receive the tokens messageReceiver := common.HexToAddress("0x90392A1E8A941098a3C75E0BDB172cFdE7E4f1f4") + feeToken := amuletInstrumentID - ccipEdsClient, err := oapiCCIP.NewClientWithResponses(edsURL) - require.NoError(t, err) - ccvEdsClient, err := oapiCCV.NewClientWithResponses(edsURL) - require.NoError(t, err) - executorEdsClient, err := oapiExecutor.NewClientWithResponses(edsURL) - require.NoError(t, err) - tokenPoolEdsClient, err := oapiTokenPool.NewClientWithResponses(edsURL) - require.NoError(t, err) - - // Get Fee Input holding - feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, amuletInstrumentID) + // Get fee token input holdings + feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, feeToken) require.NoError(t, err) + feeTokenInputCids := make([]types.CONTRACT_ID, len(feeTokenHoldings)) + for i, holding := range feeTokenHoldings { + feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } - transferFactoryCid, transferFactoryDisclosures, choiceContextRaw, err := testhelpers.GetTransferFactory(t.Context(), transferInstructionClient, string(amuletInstrumentID.Admin), participant.PartyID, ccipOwnerPartyID) + transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), amuletTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ + Sender: types.PARTY(participant.PartyID), + Receiver: ccipOwnerPartyID, + Amount: "1.0", + InstrumentId: *feeToken, + InputHoldingCids: feeTokenInputCids, + Meta: splice_api_token_metadata_v1.Metadata{Values: map[string]types.TEXT{}}, + }) require.NoError(t, err) - choiceContext, err := contracts.ChoiceContextFromData(choiceContextRaw) + choiceContext, err := contracts.ChoiceContextFromData(transferFactory.ChoiceContextData) require.NoError(t, err) - // Get Token input holding + // Get token transfer input holdings tokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, linkInstrumentId) require.NoError(t, err) + tokenTransferInputCids := make([]types.CONTRACT_ID, len(tokenHoldings)) + for i, holding := range tokenHoldings { + tokenTransferInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } // Get PerPartyRouter routerCid := getRouter(t, participant, ccipEdsClient) @@ -603,8 +623,8 @@ func TestMulti(t *testing.T) { Type: oapiCommon.Empty, }, FeeToken: oapiCommon.InstrumentId{ - Admin: oapiCommon.PartyId(amuletInstrumentID.Admin), - Id: string(amuletInstrumentID.Id), + Admin: oapiCommon.PartyId(feeToken.Admin), + Id: string(feeToken.Id), }, GasLimit: 0, Payload: "", @@ -635,15 +655,6 @@ func TestMulti(t *testing.T) { executorSendDisclosure, err := eds.GetExecutorSendDisclosure(t.Context(), executorEdsClient, msg, defaultExecutorAddress.InstanceAddress(), ccipSendDisclosure.CCVs) require.NoError(t, err) - feeTokenInputCids := make([]types.CONTRACT_ID, len(feeTokenHoldings)) - for i, holding := range feeTokenHoldings { - feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) - } - tokenTransferInputCids := make([]types.CONTRACT_ID, len(tokenHoldings)) - for i, holding := range tokenHoldings { - tokenTransferInputCids[i] = types.CONTRACT_ID(holding.ContractID) - } - sendArgs := sender.Send{ DestinationChainSelector: types.NUMERIC(msg.DestinationChainSelector), Message: core.Canton2AnyMessage{ @@ -671,7 +682,7 @@ func TestMulti(t *testing.T) { FeeTokenInput: sender.FeeTokenInput{ SenderInputCids: feeTokenInputCids, FeeTokenConfigCid: types.CONTRACT_ID(ccipSendDisclosure.FeeTokenConfigCid), - FeeTokenTransferFactory: types.CONTRACT_ID(transferFactoryCid), + FeeTokenTransferFactory: types.CONTRACT_ID(transferFactory.FactoryID), FeeTokenExtraArgs: splice_api_token_metadata_v1.ExtraArgs{ Context: choiceContext, Meta: splice_api_token_metadata_v1.Metadata{ @@ -697,7 +708,7 @@ func TestMulti(t *testing.T) { }, } allDisclosures := slices.Concat( - transferFactoryDisclosures, + transferFactory.DisclosedContracts, tokenPoolSendDisclosure.DisclosedContracts, ccipSendDisclosure.DisclosedContracts, ccvSendDisclosure.DisclosedContracts, @@ -721,19 +732,8 @@ func TestMulti(t *testing.T) { require.NoError(t, err) fmt.Println("Message sent in Update: ", resp.GetTransaction().GetUpdateId()) - var returnedMessageId string - for _, event := range resp.GetTransaction().GetEvents() { - if e, ok := event.GetEvent().(*apiv2.Event_Created); ok { - if e.Created.GetTemplateId().GetEntityName() == "CCIPMessageSent" { - ccipMessageSent, err := bindings.UnmarshalCreatedEvent[core.CCIPMessageSent](e.Created) - require.NoError(t, err) - returnedMessageId = string(ccipMessageSent.Event.MessageId) - break - } - } - } - require.NotEmpty(t, returnedMessageId) - fmt.Println("Message sent with MessageID: ", returnedMessageId) + messageId := getMessageIdFromTransaction(t, resp.GetTransaction()) + fmt.Println("Message sent with MessageID: ", messageId) }) } @@ -760,7 +760,7 @@ func getRouter(t *testing.T, participant canton.Participant, ccipEdsClient oapiC Choice: "CreateRouter", ChoiceArgument: ledger.MapToValue(ccipruntime.CreateRouter{ PartyOwner: types.PARTY(participant.PartyID), - InstanceId: "default-router", + InstanceId: types.TEXT(fmt.Sprintf("router-%s", participant.PartyID)), }), }}, }}, @@ -771,6 +771,7 @@ func getRouter(t *testing.T, participant canton.Participant, ccipEdsClient oapiC require.NoError(t, err) t.Logf("Created PerPartyRouter in update: %v", resp.GetTransaction().GetUpdateId()) + time.Sleep(5 * time.Second) // Wait for propagation activeContracts, err = testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-runtime", ModuleName: "CCIP.PerPartyRouter", EntityName: "PerPartyRouterFactory"}) require.NoError(t, err) require.NotEmptyf(t, activeContracts, "Expected to find active PerPartyRouter after creation for party %s", participant.PartyID) @@ -804,6 +805,7 @@ func getSender(t *testing.T, participant canton.Participant) string { require.NoError(t, err) t.Logf("Created CCIPSender in update: %v", resp.GetTransaction().GetUpdateId()) + time.Sleep(5 * time.Second) // Wait for propagation activeContracts, err = testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}) require.NoError(t, err) require.NotEmptyf(t, activeContracts, "Expected to find active CCIPSender after creation for party %s", participant.PartyID) @@ -844,9 +846,26 @@ func getReceiver(t *testing.T, participant canton.Participant) string { require.NoError(t, err) t.Logf("Created CCIPReceiver in update: %v", resp.GetTransaction().GetUpdateId()) + time.Sleep(5 * time.Second) // Wait for propagation activeContracts, err = testhelpers.ListActiveContractsByTemplateId(t.Context(), participant, &apiv2.Identifier{PackageId: "#ccip-receiver", ModuleName: "CCIP.CCIPReceiver", EntityName: "CCIPReceiver"}) require.NoError(t, err) require.NotEmptyf(t, activeContracts, "Expected to find active CCIPReceiver after creation for party %s", participant.PartyID) return activeContracts[0].GetCreatedEvent().GetContractId() } + +func getMessageIdFromTransaction(t *testing.T, tx *apiv2.Transaction) string { + for _, event := range tx.GetEvents() { + if e, ok := event.GetEvent().(*apiv2.Event_Created); ok { + if e.Created.GetTemplateId().GetEntityName() == "CCIPMessageSent" { + ccipMessageSent, err := bindings.UnmarshalCreatedEvent[core.CCIPMessageSent](e.Created) + require.NoError(t, err) + return string(ccipMessageSent.Event.MessageId) + } + } + } + + t.Fatal("CCIPMessageSent event not found in transaction events") + + return "" +} From 0bc3fc439d6c43272c764f3307860b340ec45d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:26:29 +0200 Subject: [PATCH 3/6] Add CreateTransfer --- examples/tests/examples_test.go | 176 +++++++++++++++++++++++++------- testhelpers/registry.go | 3 +- 2 files changed, 140 insertions(+), 39 deletions(-) diff --git a/examples/tests/examples_test.go b/examples/tests/examples_test.go index 294acf695..826dc12d0 100644 --- a/examples/tests/examples_test.go +++ b/examples/tests/examples_test.go @@ -69,12 +69,14 @@ var ( partyID = "" // Eth - Remote Chain - ethSelector = chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector - ethRpcURL = "" - ethPrivateKeyHex = "" - ethRouterAddress = common.HexToAddress("0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59") - ethTokenAddress = common.HexToAddress("0xeEe6675b20fE5950eb51361b93021D076289F612") - noExecutionTag = common.HexToAddress("0xEBa517d200000000000000000000000000000000") + ethSelector = chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector + ethRpcURL = "" + ethPrivateKeyHex = "" + ethRouterAddress = common.HexToAddress("0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59") + ethTokenAddress = common.HexToAddress("0xeEe6675b20fE5950eb51361b93021D076289F612") + noExecutionTag = common.HexToAddress("0xEBa517d200000000000000000000000000000000") + ethLinkTokenAddress = common.HexToAddress("0x779877A7B0D9E8603169DdbD7836e478b4624789") + ethEmptyAddress = common.HexToAddress("") // Example Receiver contract that emits an event if a message is received ccipReceiverContract = common.HexToAddress("0x1E340B34Cc732a71f5e59804da7E645a52e10E1B") @@ -139,7 +141,7 @@ func TestMulti(t *testing.T) { require.NoError(t, err) tokenPoolEdsClient, err := oapiTokenPool.NewClientWithResponses(edsURL) require.NoError(t, err) - // transferInstructionEdsClient, err := oapiTransferInstruction.NewClientWithResponses(edsURL) + transferInstructionEdsClient, err := oapiTransferInstruction.NewClientWithResponses(edsURL) require.NoError(t, err) indexerClient, err := indexerclient.NewIndexerClient(indexerURL, &http.Client{Timeout: 15 * time.Second}) @@ -247,11 +249,19 @@ func TestMulti(t *testing.T) { var holdingView splice_api_token_holding_v1.HoldingView err = ledger.RecordToStruct(view.GetViewValue(), &holdingView) require.NoError(t, err) - fmt.Printf("InstrumentId: %v@%v Owner: %v Amount: %v \n", holdingView.InstrumentId.Id, holdingView.InstrumentId.Admin, holdingView.Owner, holdingView.Amount) + fmt.Printf("ContractId: %v InstrumentId: %v@%v Owner: %v Amount: %v \n", holding.GetCreatedEvent().GetContractId(), holdingView.InstrumentId.Id, holdingView.InstrumentId.Admin, holdingView.Owner, holdingView.Amount) } } }) + t.Run("CreateTransfer", func(t *testing.T) { + inputHoldingCids := []types.CONTRACT_ID{ + "00d963cc68c3f3999a5cdcb43144cbdb356c7c3688f71f567dcc7b89425e7baee3ca1212208853dbc5e273f39e38d71281eeda96c15aaca39ef31839fad86c27bc46ac5361", + } + output, change := createSelfTransfer(t, participant, transferInstructionEdsClient, *linkInstrumentId, inputHoldingCids, "0.1") + fmt.Printf("Created Transfer, OutputCid: %v ChangeCid: %v\n", output, change) + }) + t.Run("Create PerPartyRouter", func(t *testing.T) { routerCid := getRouter(t, participant, ccipEdsClient) fmt.Println("PerPartyRouter Contract ID: ", routerCid) @@ -267,17 +277,18 @@ func TestMulti(t *testing.T) { fmt.Println("CCIPSender Contract ID: ", senderCid) }) - t.Run("Send: EVM -> Canton (Token Transfer, Native)", func(t *testing.T) { + t.Run("Send: EVM -> Canton (Token Transfer)", func(t *testing.T) { // Source finality := protocol.FinalityWaitForFinality execGasLimit := uint32(0) executorAddress := noExecutionTag + feeToken := ethLinkTokenAddress // Dest receiverParty := participant.PartyID // Message - tokenAmount := big.NewInt(5e18) + tokenAmount := big.NewInt(1e17) chainIdString, err := chainsel.GetChainIDFromSelector(ethSelector) require.NoError(t, err) @@ -305,14 +316,16 @@ func TestMulti(t *testing.T) { Token: ethTokenAddress, Amount: tokenAmount, }}, - FeeToken: common.Address{}, // Native + FeeToken: feeToken, ExtraArgs: extraArgs, } fee, err := router.GetFee(&bind.CallOpts{Context: t.Context()}, cantonSelector, msg) require.NoError(t, err) fmt.Printf("Fee for sending message: %s\n", fee.String()) - auth.Value = fee + if feeToken == ethEmptyAddress { + auth.Value = fee + } tx, err := router.CcipSend(auth, cantonSelector, msg) require.NoError(t, err) @@ -343,7 +356,7 @@ func TestMulti(t *testing.T) { }) t.Run("Execute: Canton (Token Transfer)", func(t *testing.T) { - messageId := common.HexToHash("1a5650e2fc6e8f874274b836122c8ccf99a52c96786cd0264669454b0aa145de") + messageId := common.HexToHash("0x67a7ef535faac33a130af67911a3d7f7c42a62ee3c0d4156e5f3c20720de89af") timeout := time.Minute * 15 start := time.Now() @@ -441,19 +454,21 @@ func TestMulti(t *testing.T) { fmt.Println("Message executed in Update: ", resp.GetTransaction().GetUpdateId()) }) - t.Run("Send: Canton->EVM (Message Only, Native)", func(t *testing.T) { + t.Run("Send: Canton->EVM (Message Only)", func(t *testing.T) { messageReceiver := ccipReceiverContract - feeToken := amuletInstrumentID + + feeToken := linkInstrumentId + feeTokenTransferInstructionClient := transferInstructionEdsClient // Get fee token input holdings - feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, amuletInstrumentID) + feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, feeToken) require.NoError(t, err) feeTokenInputCids := make([]types.CONTRACT_ID, len(feeTokenHoldings)) for i, holding := range feeTokenHoldings { feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) } - transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), amuletTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ + transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), feeTokenTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ Sender: types.PARTY(participant.PartyID), Receiver: ccipOwnerPartyID, Amount: "1.0", @@ -480,8 +495,8 @@ func TestMulti(t *testing.T) { Type: oapiCommon.Empty, }, FeeToken: oapiCommon.InstrumentId{ - Admin: oapiCommon.PartyId(amuletInstrumentID.Admin), - Id: string(amuletInstrumentID.Id), + Admin: oapiCommon.PartyId(feeToken.Admin), + Id: string(feeToken.Id), }, GasLimit: 50_000, Payload: hex.EncodeToString([]byte("Hello, EVM from Canton!")), @@ -508,7 +523,7 @@ func TestMulti(t *testing.T) { Receiver: types.TEXT(msg.Receiver), Payload: types.TEXT(msg.Payload), TokenTransfer: nil, - FeeToken: *amuletInstrumentID, + FeeToken: *feeToken, ExtraArgs: core.ExtraArgs{ V3: &core.GenericExtraArgsV3{ GasLimit: types.INT64(msg.GasLimit), @@ -575,20 +590,36 @@ func TestMulti(t *testing.T) { fmt.Println("Message sent with MessageID: ", messageId) }) - t.Run("Send: Canton->EVM (Token Only, Native)", func(t *testing.T) { + t.Run("Send: Canton->EVM (Token Only)", func(t *testing.T) { // The address that will receive the tokens messageReceiver := common.HexToAddress("0x90392A1E8A941098a3C75E0BDB172cFdE7E4f1f4") - feeToken := amuletInstrumentID - // Get fee token input holdings - feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, feeToken) - require.NoError(t, err) - feeTokenInputCids := make([]types.CONTRACT_ID, len(feeTokenHoldings)) - for i, holding := range feeTokenHoldings { - feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) + feeToken := linkInstrumentId + feeTokenTransferInstructionClient := transferInstructionEdsClient + + // Manually set fee token input holding Cids + // If empty, will automatically select all holdings + feeTokenInputCids := []types.CONTRACT_ID{ + "00f99c215863a3bfa8326eaf83ceb10af1eb4cf2f550821f34f13b368881fce61aca1212207833cd9fb4cae5e9ebc600476f1160d3437233c7f52e1f12ab437124027558b4", + } + + // Manually set token transfer input holding Cids + // If empty, will automatically select all holdings + tokenTransferInputCids := []types.CONTRACT_ID{ + "002382eab5e38f5e101685cde6e79be6f89fe930f20d6b6680adccbb7e27a7b160ca121220b4057ae7a4bc32a7c8091d359a013555c30abbe4d3e3233defc44166095d35ef", + } + + // Get fee token input holdings, if not manually specified + if len(feeTokenInputCids) == 0 { + feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, feeToken) + require.NoError(t, err) + feeTokenInputCids = make([]types.CONTRACT_ID, len(feeTokenHoldings)) + for i, holding := range feeTokenHoldings { + feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } } - transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), amuletTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ + transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), feeTokenTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ Sender: types.PARTY(participant.PartyID), Receiver: ccipOwnerPartyID, Amount: "1.0", @@ -600,12 +631,14 @@ func TestMulti(t *testing.T) { choiceContext, err := contracts.ChoiceContextFromData(transferFactory.ChoiceContextData) require.NoError(t, err) - // Get token transfer input holdings - tokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, linkInstrumentId) - require.NoError(t, err) - tokenTransferInputCids := make([]types.CONTRACT_ID, len(tokenHoldings)) - for i, holding := range tokenHoldings { - tokenTransferInputCids[i] = types.CONTRACT_ID(holding.ContractID) + // Get token transfer input holdings if not manually specified + if len(tokenTransferInputCids) == 0 { + tokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, linkInstrumentId) + require.NoError(t, err) + tokenTransferInputCids = make([]types.CONTRACT_ID, len(tokenHoldings)) + for i, holding := range tokenHoldings { + tokenTransferInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } } // Get PerPartyRouter @@ -630,7 +663,7 @@ func TestMulti(t *testing.T) { Payload: "", Receiver: hex.EncodeToString(messageReceiver.Bytes()), TokenTransfer: &oapiCommon.TokenTransfer{ - Amount: "0.5", + Amount: "0.1", Token: oapiCommon.InstrumentId{ Admin: oapiCommon.PartyId(linkInstrumentId.Admin), Id: string(linkInstrumentId.Id), @@ -664,7 +697,7 @@ func TestMulti(t *testing.T) { Token: *linkInstrumentId, Amount: types.NUMERIC(msg.TokenTransfer.Amount), }, - FeeToken: *amuletInstrumentID, + FeeToken: *feeToken, ExtraArgs: core.ExtraArgs{ V3: &core.GenericExtraArgsV3{ GasLimit: types.INT64(msg.GasLimit), @@ -869,3 +902,72 @@ func getMessageIdFromTransaction(t *testing.T, tx *apiv2.Transaction) string { return "" } + +func createSelfTransfer(t *testing.T, participant canton.Participant, transferInstructionClient oapiTransferInstruction.ClientWithResponsesInterface, instrumentId splice_api_token_holding_v1.InstrumentId, holdingCids []types.CONTRACT_ID, amount string) (string, string) { + transfer := splice_api_token_transfer_instruction_v1.Transfer{ + Sender: types.PARTY(participant.PartyID), + Receiver: types.PARTY(participant.PartyID), + Amount: types.NUMERIC(amount), + InstrumentId: instrumentId, + RequestedAt: types.TIMESTAMP(time.Now()), + ExecuteBefore: types.TIMESTAMP(time.Now().Add(time.Hour * 24)), + InputHoldingCids: holdingCids, + Meta: splice_api_token_metadata_v1.Metadata{Values: map[string]types.TEXT{}}, + } + transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), transferInstructionClient, string(instrumentId.Admin), transfer) + require.NoError(t, err) + choiceContext, err := contracts.ChoiceContextFromData(transferFactory.ChoiceContextData) + require.NoError(t, err) + + resp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#splice-api-token-transfer-instruction-v1", ModuleName: "Splice.Api.Token.TransferInstructionV1", EntityName: "TransferFactory"}, + ContractId: transferFactory.FactoryID, + Choice: "TransferFactory_Transfer", + ChoiceArgument: ledger.MapToValue(splice_api_token_transfer_instruction_v1.TransferFactoryTransfer{ + ExpectedAdmin: instrumentId.Admin, + Transfer: transfer, + ExtraArgs: splice_api_token_metadata_v1.ExtraArgs{ + Context: choiceContext, + }, + }), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: transferFactory.DisclosedContracts, + }, + TransactionFormat: &apiv2.TransactionFormat{ + EventFormat: &apiv2.EventFormat{ + FiltersByParty: map[string]*apiv2.Filters{ + participant.PartyID: {Cumulative: []*apiv2.CumulativeFilter{{IdentifierFilter: &apiv2.CumulativeFilter_WildcardFilter{WildcardFilter: &apiv2.WildcardFilter{}}}}}, + }, + Verbose: true, + }, + TransactionShape: apiv2.TransactionShape_TRANSACTION_SHAPE_LEDGER_EFFECTS, + }, + }) + require.NoError(t, err) + + for i, event := range resp.GetTransaction().GetEvents() { + fmt.Println("Event ", i, ": ", event) + if e, ok := event.GetEvent().(*apiv2.Event_Exercised); ok { + if e.Exercised.GetInterfaceId().GetEntityName() == "TransferFactory" && e.Exercised.GetChoice() == "TransferFactory_Transfer" { + var transferResult splice_api_token_transfer_instruction_v1.TransferInstructionResult + err = ledger.RecordToStruct(e.Exercised.GetExerciseResult().GetRecord(), &transferResult) + require.NoError(t, err) + + require.NotNil(t, transferResult.Output.TransferInstructionResultCompleted, "Expected self-transfer to complete immediately") + require.Len(t, transferResult.Output.TransferInstructionResultCompleted.ReceiverHoldingCids, 1, "Expected transfer to result in one holding") + require.Len(t, transferResult.SenderChangeCids, 1, "Expected sender change to result in one holding") + + return string(transferResult.Output.TransferInstructionResultCompleted.ReceiverHoldingCids[0]), string(transferResult.SenderChangeCids[0]) + } + } + } + + t.Fatal("TransferFactory_Transfer result not found in transaction events") + return "", "" +} diff --git a/testhelpers/registry.go b/testhelpers/registry.go index bd618cdb5..1b9bd2bfe 100644 --- a/testhelpers/registry.go +++ b/testhelpers/registry.go @@ -263,7 +263,6 @@ func GetTransferFactoryV2( "admin": string(transfer.InstrumentId.Admin), "id": string(transfer.InstrumentId.Id), }, - "lock": nil, "requestedAt": now.Add(time.Hour * -1).Format(time.RFC3339), "executeBefore": now.Add(time.Hour * 24).Format(time.RFC3339), "inputHoldingCids": inputCids, @@ -286,7 +285,7 @@ func GetTransferFactoryV2( return nil, fmt.Errorf("get transfer factory: %w", err) } if transferFactoryResponse.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("get transfer factory: status %d: %v", transferFactoryResponse.StatusCode(), transferFactoryResponse.Body) + return nil, fmt.Errorf("get transfer factory: status %d: %v", transferFactoryResponse.StatusCode(), string(transferFactoryResponse.Body)) } if transferFactoryResponse.JSON200 == nil { return nil, fmt.Errorf("get transfer factory: empty response body") From ca963bb7b0b452240312eee14387d4f81a1136b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Fri, 19 Jun 2026 21:28:10 +0200 Subject: [PATCH 4/6] Add failure cases --- examples/tests/examples_test.go | 617 +++++++++++++++++++++++++++----- 1 file changed, 525 insertions(+), 92 deletions(-) diff --git a/examples/tests/examples_test.go b/examples/tests/examples_test.go index 826dc12d0..54fb25356 100644 --- a/examples/tests/examples_test.go +++ b/examples/tests/examples_test.go @@ -2,6 +2,7 @@ package tests import ( + "bytes" "encoding/base64" "encoding/hex" "encoding/json" @@ -23,8 +24,10 @@ import ( "github.com/stretchr/testify/require" chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/latest/offramp" routerwrapper "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router" "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v2_0_0/onramp" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/latest/ccip_offramp" "github.com/smartcontractkit/chainlink-ccv/build/devenv/evm" v1 "github.com/smartcontractkit/chainlink-ccv/indexer/pkg/api/handlers/v1" indexerclient "github.com/smartcontractkit/chainlink-ccv/indexer/pkg/client" @@ -98,7 +101,7 @@ var linkInstrumentId = &splice_api_token_holding_v1.InstrumentId{ Id: "link-token", } -func TestMulti(t *testing.T) { +func TestCanton(t *testing.T) { authProvider, err := authorizationcode.NewDiscoveryProvider(t.Context(), authorizationServerURL, authClientID) require.NoError(t, err) require.NotNil(t, authProvider) @@ -155,7 +158,7 @@ func TestMulti(t *testing.T) { }) t.Run("AcceptIncomingTransferInstruction", func(t *testing.T) { - transferInstructionCid := "" + transferInstructionCid := "006541ce989dc9512e1d2f6f605f26a11c72e4838f22151bd49b4cc8cef72a3788ca121220ab31806e820cf811056c11b5cbdc4009a2db67ac4d19c1cc9b08952fb8f99c82" contextResp, err := amuletTransferInstructionClient.GetTransferInstructionAcceptContextWithResponse(t.Context(), transferInstructionCid, oapiTransferInstruction.GetTransferInstructionAcceptContextJSONRequestBody{ Meta: nil, @@ -256,9 +259,9 @@ func TestMulti(t *testing.T) { t.Run("CreateTransfer", func(t *testing.T) { inputHoldingCids := []types.CONTRACT_ID{ - "00d963cc68c3f3999a5cdcb43144cbdb356c7c3688f71f567dcc7b89425e7baee3ca1212208853dbc5e273f39e38d71281eeda96c15aaca39ef31839fad86c27bc46ac5361", + "0084a444179ca02a773153a9cc4bc4dbae5ebbc07114bb12afab8d0ec772975b13ca1212208769076c741f15456478f98e60eefa1c1409ed88018d213496a6ab24e1724c16", } - output, change := createSelfTransfer(t, participant, transferInstructionEdsClient, *linkInstrumentId, inputHoldingCids, "0.1") + output, change := createSelfTransfer(t, participant, transferInstructionEdsClient, *linkInstrumentId, inputHoldingCids, "0.01") fmt.Printf("Created Transfer, OutputCid: %v ChangeCid: %v\n", output, change) }) @@ -277,86 +280,8 @@ func TestMulti(t *testing.T) { fmt.Println("CCIPSender Contract ID: ", senderCid) }) - t.Run("Send: EVM -> Canton (Token Transfer)", func(t *testing.T) { - // Source - finality := protocol.FinalityWaitForFinality - execGasLimit := uint32(0) - executorAddress := noExecutionTag - feeToken := ethLinkTokenAddress - - // Dest - receiverParty := participant.PartyID - - // Message - tokenAmount := big.NewInt(1e17) - - chainIdString, err := chainsel.GetChainIDFromSelector(ethSelector) - require.NoError(t, err) - sourceChainId, err := strconv.ParseUint(chainIdString, 10, 64) - require.NoError(t, err) - client, err := ethclient.DialContext(t.Context(), ethRpcURL) - require.NoError(t, err) - privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(ethPrivateKeyHex, "0x")) - require.NoError(t, err) - auth, err := bind.NewKeyedTransactorWithChainID(privateKey, new(big.Int).SetUint64(sourceChainId)) - require.NoError(t, err) - - router, err := routerwrapper.NewRouter(ethRouterAddress, client) - require.NoError(t, err) - - receiverPartyHashed := contracts.HashedPartyFromString(receiverParty) - - extraArgs, err := evm.NewV3ExtraArgs(finality, execGasLimit, executorAddress.Hex(), nil, nil, nil, nil) - require.NoError(t, err) - - msg := routerwrapper.ClientEVM2AnyMessage{ - Receiver: receiverPartyHashed.Bytes(), - Data: nil, - TokenAmounts: []routerwrapper.ClientEVMTokenAmount{{ - Token: ethTokenAddress, - Amount: tokenAmount, - }}, - FeeToken: feeToken, - ExtraArgs: extraArgs, - } - - fee, err := router.GetFee(&bind.CallOpts{Context: t.Context()}, cantonSelector, msg) - require.NoError(t, err) - fmt.Printf("Fee for sending message: %s\n", fee.String()) - if feeToken == ethEmptyAddress { - auth.Value = fee - } - - tx, err := router.CcipSend(auth, cantonSelector, msg) - require.NoError(t, err) - fmt.Printf("Sent message with tx hash: %s\n", tx.Hash().Hex()) - - receipt, err := bind.WaitMined(t.Context(), client, tx) - require.NoError(t, err) - fmt.Printf("Transaction mined in block: %d\n", receipt.BlockNumber.Uint64()) - - var sentEvent *onramp.OnRampCCIPMessageSent - eventTopic := (onramp.OnRampCCIPMessageSent{}).Topic() - for _, lg := range receipt.Logs { - if len(lg.Topics) == 0 || lg.Topics[0] != eventTopic { - continue - } - - onRamp, err := onramp.NewOnRamp(lg.Address, client) - require.NoError(t, err) - - messageSentEvent, err := onRamp.ParseCCIPMessageSent(*lg) - require.NoError(t, err) - sentEvent = messageSentEvent - - break - } - require.NotNil(t, sentEvent, "Sent event not found in transaction logs") - fmt.Printf("Message sent with ID: %s\n", common.Bytes2Hex(sentEvent.MessageId[:])) - }) - t.Run("Execute: Canton (Token Transfer)", func(t *testing.T) { - messageId := common.HexToHash("0x67a7ef535faac33a130af67911a3d7f7c42a62ee3c0d4156e5f3c20720de89af") + messageId := common.HexToHash("0x20b01049ee0c69c8b814b616ae5758e8f1c43f194626833c07023b8484675c9b") timeout := time.Minute * 15 start := time.Now() @@ -467,6 +392,9 @@ func TestMulti(t *testing.T) { for i, holding := range feeTokenHoldings { feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) } + feeTokenInputCids = []types.CONTRACT_ID{ + "009201860ca740bf85a8bdea12031d004bd6366eddc68d7db2a389fa5cba54672bca121220b33626465b9e80654058caf00196fcdff9fad802edc7441a58421308decdb901", + } transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), feeTokenTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ Sender: types.PARTY(participant.PartyID), @@ -593,21 +521,18 @@ func TestMulti(t *testing.T) { t.Run("Send: Canton->EVM (Token Only)", func(t *testing.T) { // The address that will receive the tokens messageReceiver := common.HexToAddress("0x90392A1E8A941098a3C75E0BDB172cFdE7E4f1f4") + transferAmount := "0.123456789" - feeToken := linkInstrumentId - feeTokenTransferInstructionClient := transferInstructionEdsClient + feeToken := amuletInstrumentID + feeTokenTransferInstructionClient := amuletTransferInstructionClient // Manually set fee token input holding Cids // If empty, will automatically select all holdings - feeTokenInputCids := []types.CONTRACT_ID{ - "00f99c215863a3bfa8326eaf83ceb10af1eb4cf2f550821f34f13b368881fce61aca1212207833cd9fb4cae5e9ebc600476f1160d3437233c7f52e1f12ab437124027558b4", - } + feeTokenInputCids := []types.CONTRACT_ID{} // Manually set token transfer input holding Cids // If empty, will automatically select all holdings - tokenTransferInputCids := []types.CONTRACT_ID{ - "002382eab5e38f5e101685cde6e79be6f89fe930f20d6b6680adccbb7e27a7b160ca121220b4057ae7a4bc32a7c8091d359a013555c30abbe4d3e3233defc44166095d35ef", - } + tokenTransferInputCids := []types.CONTRACT_ID{} // Get fee token input holdings, if not manually specified if len(feeTokenInputCids) == 0 { @@ -663,7 +588,7 @@ func TestMulti(t *testing.T) { Payload: "", Receiver: hex.EncodeToString(messageReceiver.Bytes()), TokenTransfer: &oapiCommon.TokenTransfer{ - Amount: "0.1", + Amount: transferAmount, Token: oapiCommon.InstrumentId{ Admin: oapiCommon.PartyId(linkInstrumentId.Admin), Id: string(linkInstrumentId.Id), @@ -768,6 +693,514 @@ func TestMulti(t *testing.T) { messageId := getMessageIdFromTransaction(t, resp.GetTransaction()) fmt.Println("Message sent with MessageID: ", messageId) }) + + t.Run("Send: Canton failure cases", func(t *testing.T) { + // The address that will receive the tokens + messageReceiver := common.HexToAddress("0x90392A1E8A941098a3C75E0BDB172cFdE7E4f1f4") + transferAmount := "0.123456789" + + feeToken := amuletInstrumentID + feeTokenTransferInstructionClient := amuletTransferInstructionClient + + // Manually set fee token input holding Cids + // If empty, will automatically select all holdings + feeTokenInputCids := []types.CONTRACT_ID{} + + // Manually set token transfer input holding Cids + // If empty, will automatically select all holdings + tokenTransferInputCids := []types.CONTRACT_ID{} + + // Get fee token input holdings, if not manually specified + if len(feeTokenInputCids) == 0 { + feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, feeToken) + require.NoError(t, err) + feeTokenInputCids = make([]types.CONTRACT_ID, len(feeTokenHoldings)) + for i, holding := range feeTokenHoldings { + feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } + } + + transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), feeTokenTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ + Sender: types.PARTY(participant.PartyID), + Receiver: ccipOwnerPartyID, + Amount: "1.0", + InstrumentId: *feeToken, + InputHoldingCids: feeTokenInputCids, + Meta: splice_api_token_metadata_v1.Metadata{Values: map[string]types.TEXT{}}, + }) + require.NoError(t, err) + choiceContext, err := contracts.ChoiceContextFromData(transferFactory.ChoiceContextData) + require.NoError(t, err) + + // Get token transfer input holdings if not manually specified + if len(tokenTransferInputCids) == 0 { + tokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, linkInstrumentId) + require.NoError(t, err) + tokenTransferInputCids = make([]types.CONTRACT_ID, len(tokenHoldings)) + for i, holding := range tokenHoldings { + tokenTransferInputCids[i] = types.CONTRACT_ID(holding.ContractID) + } + } + + // Get PerPartyRouter + routerCid := getRouter(t, participant, ccipEdsClient) + + // Get CCIPSender + senderCid := getSender(t, participant) + + msg := oapiCommon.Message{ + DestinationChainSelector: strconv.FormatUint(ethSelector, 10), + Executor: struct { + Address *oapiCommon.RawOrHashedAddress `json:"address,omitempty"` + Type oapiCommon.MessageExecutorType `json:"type"` + }{ + Type: oapiCommon.Empty, + }, + FeeToken: oapiCommon.InstrumentId{ + Admin: oapiCommon.PartyId(feeToken.Admin), + Id: string(feeToken.Id), + }, + GasLimit: 0, + Payload: "", + Receiver: hex.EncodeToString(messageReceiver.Bytes()), + TokenTransfer: &oapiCommon.TokenTransfer{ + Amount: transferAmount, + Token: oapiCommon.InstrumentId{ + Admin: oapiCommon.PartyId(linkInstrumentId.Admin), + Id: string(linkInstrumentId.Id), + }, + }, + } + + tokenPoolAddress, err := eds.GetTokenPoolForToken(t.Context(), ccipEdsClient, contracts.EncodeInstrumentID(*linkInstrumentId)) + require.NoError(t, err) + tokenPoolSendDisclosure, err := eds.GetTokenPoolSendDisclosure(t.Context(), tokenPoolEdsClient, msg, tokenPoolAddress.InstanceAddress()) + require.NoError(t, err) + ccipSendDisclosure, err := eds.GetCCIPSendDisclosure(t.Context(), ccipEdsClient, msg, nil, tokenPoolSendDisclosure.RequiredCCVs) + require.NoError(t, err) + // EDS returns the default CCV(s) if no input is specified + defaultCCVAddress, err := contracts.RawInstanceAddressFromString(ccipSendDisclosure.CCVs[0]) + require.NoError(t, err) + // EDS returns the default Executor (if set) if no specific executor input is specified + defaultExecutorAddress, err := contracts.RawInstanceAddressFromString(*ccipSendDisclosure.Executor) + require.NoError(t, err) + ccvSendDisclosure, err := eds.GetCCVSendDisclosure(t.Context(), ccvEdsClient, msg, defaultCCVAddress.InstanceAddress()) + require.NoError(t, err) + executorSendDisclosure, err := eds.GetExecutorSendDisclosure(t.Context(), executorEdsClient, msg, defaultExecutorAddress.InstanceAddress(), ccipSendDisclosure.CCVs) + require.NoError(t, err) + + sendArgs := sender.Send{ + DestinationChainSelector: types.NUMERIC(msg.DestinationChainSelector), + Message: core.Canton2AnyMessage{ + Receiver: types.TEXT(msg.Receiver), + Payload: types.TEXT(msg.Payload), + TokenTransfer: &core.TokenTransfer{ + Token: *linkInstrumentId, + Amount: types.NUMERIC(msg.TokenTransfer.Amount), + }, + FeeToken: *feeToken, + ExtraArgs: core.ExtraArgs{ + V3: &core.GenericExtraArgsV3{ + GasLimit: types.INT64(msg.GasLimit), + Ccvs: nil, + Executor: core.ExecutorExtraArg{ + ExecutorUseDefault: &core.ExecutorUseDefault{ExecutorArgs: ""}, + }, + TokenReceiver: "", + TokenArgs: "", + }, + }, + }, + Context: ccipSendDisclosure.ChoiceContext, + RouterCid: types.CONTRACT_ID(routerCid), + FeeTokenInput: sender.FeeTokenInput{ + SenderInputCids: feeTokenInputCids, + FeeTokenConfigCid: types.CONTRACT_ID(ccipSendDisclosure.FeeTokenConfigCid), + FeeTokenTransferFactory: types.CONTRACT_ID(transferFactory.FactoryID), + FeeTokenExtraArgs: splice_api_token_metadata_v1.ExtraArgs{ + Context: choiceContext, + Meta: splice_api_token_metadata_v1.Metadata{ + Values: map[string]types.TEXT{}, + }, + }, + }, + CcvSendInputs: []sender.CCVSendInput{ + { + CcvAddress: ccvSendDisclosure.Address.Binding(), + CcvCid: types.CONTRACT_ID(ccvSendDisclosure.ContractId), + CcvExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, + }, + }, + TokenTransferInput: &sender.TokenTransferInput{ + SenderInputCids: tokenTransferInputCids, + TokenPoolCid: types.CONTRACT_ID(tokenPoolSendDisclosure.ContractId), + PoolExtraContext: tokenPoolSendDisclosure.ChoiceContext, + }, + ExecutorInput: &sender.ExecutorInput{ + ExecutorCid: types.CONTRACT_ID(executorSendDisclosure.ContractId), + ExecutorExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, + }, + } + allDisclosures := slices.Concat( + transferFactory.DisclosedContracts, + tokenPoolSendDisclosure.DisclosedContracts, + ccipSendDisclosure.DisclosedContracts, + ccvSendDisclosure.DisclosedContracts, + executorSendDisclosure.DisclosedContracts, + ) + + t.Run("Exceed max data size", func(t *testing.T) { + // Exceed the max of 32kb + sendArgs.Message.Payload = types.TEXT(hex.EncodeToString(bytes.Repeat([]byte("A"), 32_001))) + + _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.Errorf(t, err, "Expected send to return an error") + fmt.Println(err) + }) + + t.Run("Exceed gas limit", func(t *testing.T) { + // Exceed the max of 15M + sendArgs.Message.ExtraArgs.V3.GasLimit = 15_000_001 + + _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.Errorf(t, err, "Expected send to return an error") + fmt.Println(err) + }) + + t.Run("Invalid extraArgs", func(t *testing.T) { + sendArgs.Message.ExtraArgs.V3 = nil + + _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.Errorf(t, err, "Expected send to return an error") + fmt.Println(err) + }) + + t.Run("Invalid dest chain selector", func(t *testing.T) { + sendArgs.DestinationChainSelector = types.NUMERIC("1234") + + _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.Errorf(t, err, "Expected send to return an error") + fmt.Println(err) + }) + + t.Run("Invalid fee token", func(t *testing.T) { + sendArgs.Message.FeeToken = splice_api_token_holding_v1.InstrumentId{ + Admin: ccipOwnerPartyID, + Id: "invalid-fee-token", + } + + _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.Errorf(t, err, "Expected send to return an error") + fmt.Println(err) + }) + + t.Run("No fee token holding", func(t *testing.T) { + sendArgs.FeeTokenInput.SenderInputCids = []types.CONTRACT_ID{} + + _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.Errorf(t, err, "Expected send to return an error") + fmt.Println(err) + }) + + t.Run("Invalid token transfer instrument", func(t *testing.T) { + sendArgs.Message.TokenTransfer.Token = splice_api_token_holding_v1.InstrumentId{ + Admin: ccipOwnerPartyID, + Id: "invalid-token-transfer", + } + + _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.Errorf(t, err, "Expected send to return an error") + fmt.Println(err) + }) + + t.Run("Zero transfer amount", func(t *testing.T) { + sendArgs.Message.TokenTransfer.Amount = "0.0" + + _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.Errorf(t, err, "Expected send to return an error") + fmt.Println(err) + }) + + t.Run("Empty receiver", func(t *testing.T) { + sendArgs.Message.Receiver = "" + + _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-sender", ModuleName: "CCIP.CCIPSender", EntityName: "CCIPSender"}, + ContractId: senderCid, + Choice: "Send", + ChoiceArgument: ledger.MapToValue(sendArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: allDisclosures, + }, + }) + require.Errorf(t, err, "Expected send to return an error") + fmt.Println(err) + }) + }) +} + +func TestEVM(t *testing.T) { + indexerClient, err := indexerclient.NewIndexerClient(indexerURL, &http.Client{Timeout: 15 * time.Second}) + require.NoError(t, err) + + t.Run("Send: EVM -> Canton (Token Transfer)", func(t *testing.T) { + // Source + finality := protocol.FinalityWaitForFinality + execGasLimit := uint32(0) + executorAddress := noExecutionTag + feeToken := ethLinkTokenAddress + + // Dest + receiverParty := partyID + + // Message + tokenAmount := big.NewInt(1e17) + + chainIdString, err := chainsel.GetChainIDFromSelector(ethSelector) + require.NoError(t, err) + sourceChainId, err := strconv.ParseUint(chainIdString, 10, 64) + require.NoError(t, err) + client, err := ethclient.DialContext(t.Context(), ethRpcURL) + require.NoError(t, err) + privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(ethPrivateKeyHex, "0x")) + require.NoError(t, err) + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, new(big.Int).SetUint64(sourceChainId)) + require.NoError(t, err) + + router, err := routerwrapper.NewRouter(ethRouterAddress, client) + require.NoError(t, err) + + receiverPartyHashed := contracts.HashedPartyFromString(receiverParty) + + extraArgs, err := evm.NewV3ExtraArgs(finality, execGasLimit, executorAddress.Hex(), nil, nil, nil, nil) + require.NoError(t, err) + + msg := routerwrapper.ClientEVM2AnyMessage{ + Receiver: receiverPartyHashed.Bytes(), + Data: nil, + TokenAmounts: []routerwrapper.ClientEVMTokenAmount{{ + Token: ethTokenAddress, + Amount: tokenAmount, + }}, + FeeToken: feeToken, + ExtraArgs: extraArgs, + } + + fee, err := router.GetFee(&bind.CallOpts{Context: t.Context()}, cantonSelector, msg) + require.NoError(t, err) + fmt.Printf("Fee for sending message: %s\n", fee.String()) + if feeToken == ethEmptyAddress { + auth.Value = fee + } + + tx, err := router.CcipSend(auth, cantonSelector, msg) + require.NoError(t, err) + fmt.Printf("Sent message with tx hash: %s\n", tx.Hash().Hex()) + + receipt, err := bind.WaitMined(t.Context(), client, tx) + require.NoError(t, err) + fmt.Printf("Transaction mined in block: %d\n", receipt.BlockNumber.Uint64()) + + var sentEvent *onramp.OnRampCCIPMessageSent + eventTopic := (onramp.OnRampCCIPMessageSent{}).Topic() + for _, lg := range receipt.Logs { + if len(lg.Topics) == 0 || lg.Topics[0] != eventTopic { + continue + } + + onRamp, err := onramp.NewOnRamp(lg.Address, client) + require.NoError(t, err) + + messageSentEvent, err := onRamp.ParseCCIPMessageSent(*lg) + require.NoError(t, err) + sentEvent = messageSentEvent + + break + } + require.NotNil(t, sentEvent, "Sent event not found in transaction logs") + fmt.Printf("Message sent with ID: %s\n", common.Bytes2Hex(sentEvent.MessageId[:])) + }) + + t.Run("Execute: EVM", func(t *testing.T) { + messageId := common.HexToHash("0x2505e5aba3dcc2a736285b3cf6277dc26685542d774c44ef8cc90c4d6c4dd2da") + timeout := time.Minute * 15 + + start := time.Now() + var response v1.VerifierResultsByMessageIDResponse + for { + if time.Since(start) > timeout { + t.Fatal("Timed out waiting for message to be processed by indexer") + } + status, resp, err := indexerClient.VerifierResultsByMessageID(t.Context(), v1.VerifierResultsByMessageIDInput{MessageID: messageId.Hex()}) + if err != nil { + t.Logf("Error querying indexer: %v", err) + time.Sleep(5 * time.Second) + continue + } else if status != http.StatusOK { + t.Logf("Unexpected status code from indexer: %d", status) + time.Sleep(5 * time.Second) + continue + } + + response = resp + + break + } + require.NotEmptyf(t, response.Results, "Expected at least one verifier result for message ID %s", messageId.Hex()) + t.Logf("Received response from indexer: %+v", response) + + verifierResult := response.Results[0].VerifierResult + message := verifierResult.Message + encodedMessage, err := message.Encode() + require.NoError(t, err) + offRampAddress := common.BytesToAddress(message.OffRampAddress) + + chainIdString, err := chainsel.GetChainIDFromSelector(ethSelector) + require.NoError(t, err) + sourceChainId, err := strconv.ParseUint(chainIdString, 10, 64) + require.NoError(t, err) + client, err := ethclient.DialContext(t.Context(), ethRpcURL) + require.NoError(t, err) + privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(ethPrivateKeyHex, "0x")) + require.NoError(t, err) + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, new(big.Int).SetUint64(sourceChainId)) + require.NoError(t, err) + + offRamp, err := offramp.NewOffRamp(offRampAddress, client) + require.NoError(t, err) + + execState, err := offRamp.GetExecutionState(&bind.CallOpts{Context: t.Context()}, messageId) + require.NoError(t, err) + if ccip_offramp.MessageExecutionState(execState) == ccip_offramp.Success_MessageExecutionState { + fmt.Println("Message already executed") + } + + tx, err := offRamp.Execute(auth, encodedMessage, []common.Address{common.BytesToAddress(verifierResult.VerifierDestAddress)}, [][]byte{verifierResult.CCVData}, 0) + require.NoError(t, err) + fmt.Printf("Sent message with tx hash: %s\n", tx.Hash().Hex()) + + receipt, err := bind.WaitMined(t.Context(), client, tx) + require.NoError(t, err) + fmt.Printf("Transaction mined in block: %d\n", receipt.BlockNumber.Uint64()) + }) } // getRouter returns the PerPartyRouter for the participant's party, or creates one if it doesn't exist yet. From 80090e53b5ee53ed785a3f266fb29fe61bf693ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:31:20 +0200 Subject: [PATCH 5/6] Update --- examples/tests/examples_test.go | 197 ++++++++++++++++++++++++++++---- 1 file changed, 177 insertions(+), 20 deletions(-) diff --git a/examples/tests/examples_test.go b/examples/tests/examples_test.go index 54fb25356..a3fa561fe 100644 --- a/examples/tests/examples_test.go +++ b/examples/tests/examples_test.go @@ -71,6 +71,18 @@ var ( userID = "" partyID = "" + dsoPartyID = types.PARTY("DSO::1220f22a8b8f2d813c25b9a684dc4dd52b532a0174d8e73a13cdf2baabfff7518337") + ccipOwnerPartyID = types.PARTY("ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551") + + amuletInstrumentID = &splice_api_token_holding_v1.InstrumentId{ + Admin: dsoPartyID, + Id: "Amulet", + } + linkInstrumentId = &splice_api_token_holding_v1.InstrumentId{ + Admin: ccipOwnerPartyID, + Id: "link-token", + } + // Eth - Remote Chain ethSelector = chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector ethRpcURL = "" @@ -86,21 +98,6 @@ var ( receiverAddress = common.HexToAddress("0x0") ) -const ( - dsoPartyID = "DSO::1220f22a8b8f2d813c25b9a684dc4dd52b532a0174d8e73a13cdf2baabfff7518337" - ccipOwnerPartyID = "ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551" -) - -var amuletInstrumentID = &splice_api_token_holding_v1.InstrumentId{ - Admin: dsoPartyID, - Id: "Amulet", -} - -var linkInstrumentId = &splice_api_token_holding_v1.InstrumentId{ - Admin: ccipOwnerPartyID, - Id: "link-token", -} - func TestCanton(t *testing.T) { authProvider, err := authorizationcode.NewDiscoveryProvider(t.Context(), authorizationServerURL, authClientID) require.NoError(t, err) @@ -280,6 +277,94 @@ func TestCanton(t *testing.T) { fmt.Println("CCIPSender Contract ID: ", senderCid) }) + t.Run("Execute: Canton (Message Only)", func(t *testing.T) { + messageId := common.HexToHash("0xb52dac849de0a1901f8c935877f08d5890e64ec68f8d74ff4c6bfdd57978270a") + timeout := time.Minute * 15 + + start := time.Now() + var response v1.VerifierResultsByMessageIDResponse + for { + if time.Since(start) > timeout { + t.Fatal("Timed out waiting for message to be processed by indexer") + } + status, resp, err := indexerClient.VerifierResultsByMessageID(t.Context(), v1.VerifierResultsByMessageIDInput{MessageID: messageId.Hex()}) + if err != nil { + t.Logf("Error querying indexer: %v", err) + time.Sleep(5 * time.Second) + continue + } else if status != http.StatusOK { + t.Logf("Unexpected status code from indexer: %d", status) + time.Sleep(5 * time.Second) + continue + } + + response = resp + + break + } + require.NotEmptyf(t, response.Results, "Expected at least one verifier result for message ID %s", messageId.Hex()) + t.Logf("Received response from indexer: %+v", response) + + verifierResult := response.Results[0].VerifierResult + message := verifierResult.Message + encodedMessage, err := message.Encode() + require.NoError(t, err) + + ccvAddress := contracts.BytesToInstanceAddress(verifierResult.VerifierDestAddress) + + ccipExecuteDisclosure, err := eds.GetCCIPExecuteDisclosure(t.Context(), ccipEdsClient, hex.EncodeToString(encodedMessage)) + require.NoError(t, err) + ccvExecuteDisclosure, err := eds.GetCCVExecuteDisclosure(t.Context(), ccvEdsClient, hex.EncodeToString(encodedMessage), ccvAddress) + require.NoError(t, err) + + fmt.Println("CCIP Execute Disclosure:", ccipExecuteDisclosure) + fmt.Println("CCV Execute Disclosure:", ccvExecuteDisclosure) + + // Get PerPartyRouter + routerCid := getRouter(t, participant, ccipEdsClient) + + // Get CCIPReceiver + receiverCid := getReceiver(t, participant) + + fmt.Println("PerPartyRouter CID:", routerCid) + fmt.Println("CCIPReceiver CID:", receiverCid) + + executeArgs := receiver.Execute{ + Context: ccipExecuteDisclosure.ChoiceContext, + RouterCid: types.CONTRACT_ID(routerCid), + EncodedMessage: types.TEXT(hex.EncodeToString(encodedMessage)), + TokenTransfer: nil, + CcvInputs: []receiver.CCVInput{ + { + CcvCid: types.CONTRACT_ID(ccvExecuteDisclosure.ContractId), + VerifierResults: types.TEXT(hex.EncodeToString(verifierResult.CCVData)), + CcvExtraContext: ccvExecuteDisclosure.ChoiceContext, + }, + }, + } + + resp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{PackageId: "#ccip-receiver", ModuleName: "CCIP.CCIPReceiver", EntityName: "CCIPReceiver"}, + ContractId: receiverCid, + Choice: "Execute", + ChoiceArgument: ledger.MapToValue(executeArgs), + }}, + }}, + ActAs: []string{participant.PartyID}, + DisclosedContracts: slices.Concat( + ccipExecuteDisclosure.DisclosedContracts, + ccvExecuteDisclosure.DisclosedContracts, + ), + }, + }) + require.NoError(t, err) + fmt.Println("Message executed in Update: ", resp.GetTransaction().GetUpdateId()) + }) + t.Run("Execute: Canton (Token Transfer)", func(t *testing.T) { messageId := common.HexToHash("0x20b01049ee0c69c8b814b616ae5758e8f1c43f194626833c07023b8484675c9b") timeout := time.Minute * 15 @@ -382,8 +467,8 @@ func TestCanton(t *testing.T) { t.Run("Send: Canton->EVM (Message Only)", func(t *testing.T) { messageReceiver := ccipReceiverContract - feeToken := linkInstrumentId - feeTokenTransferInstructionClient := transferInstructionEdsClient + feeToken := amuletInstrumentID + feeTokenTransferInstructionClient := amuletTransferInstructionClient // Get fee token input holdings feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, feeToken) @@ -392,9 +477,6 @@ func TestCanton(t *testing.T) { for i, holding := range feeTokenHoldings { feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) } - feeTokenInputCids = []types.CONTRACT_ID{ - "009201860ca740bf85a8bdea12031d004bd6366eddc68d7db2a389fa5cba54672bca121220b33626465b9e80654058caf00196fcdff9fad802edc7441a58421308decdb901", - } transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), feeTokenTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ Sender: types.PARTY(participant.PartyID), @@ -1061,6 +1143,81 @@ func TestEVM(t *testing.T) { indexerClient, err := indexerclient.NewIndexerClient(indexerURL, &http.Client{Timeout: 15 * time.Second}) require.NoError(t, err) + t.Run("Send: EVM -> Canton (Message Only)", func(t *testing.T) { + // Source + finality := protocol.FinalityWaitForFinality + execGasLimit := uint32(0) + executorAddress := noExecutionTag + feeToken := ethLinkTokenAddress + + // Dest + receiverParty := partyID + + // Message + payload := []byte("Hello, Canton!") + + chainIdString, err := chainsel.GetChainIDFromSelector(ethSelector) + require.NoError(t, err) + sourceChainId, err := strconv.ParseUint(chainIdString, 10, 64) + require.NoError(t, err) + client, err := ethclient.DialContext(t.Context(), ethRpcURL) + require.NoError(t, err) + privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(ethPrivateKeyHex, "0x")) + require.NoError(t, err) + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, new(big.Int).SetUint64(sourceChainId)) + require.NoError(t, err) + + router, err := routerwrapper.NewRouter(ethRouterAddress, client) + require.NoError(t, err) + + receiverPartyHashed := contracts.HashedPartyFromString(receiverParty) + + extraArgs, err := evm.NewV3ExtraArgs(finality, execGasLimit, executorAddress.Hex(), nil, nil, nil, nil) + require.NoError(t, err) + + msg := routerwrapper.ClientEVM2AnyMessage{ + Receiver: receiverPartyHashed.Bytes(), + Data: payload, + TokenAmounts: nil, + FeeToken: feeToken, + ExtraArgs: extraArgs, + } + + fee, err := router.GetFee(&bind.CallOpts{Context: t.Context()}, cantonSelector, msg) + require.NoError(t, err) + fmt.Printf("Fee for sending message: %s\n", fee.String()) + if feeToken == ethEmptyAddress { + auth.Value = fee + } + + tx, err := router.CcipSend(auth, cantonSelector, msg) + require.NoError(t, err) + fmt.Printf("Sent message with tx hash: %s\n", tx.Hash().Hex()) + + receipt, err := bind.WaitMined(t.Context(), client, tx) + require.NoError(t, err) + fmt.Printf("Transaction mined in block: %d\n", receipt.BlockNumber.Uint64()) + + var sentEvent *onramp.OnRampCCIPMessageSent + eventTopic := (onramp.OnRampCCIPMessageSent{}).Topic() + for _, lg := range receipt.Logs { + if len(lg.Topics) == 0 || lg.Topics[0] != eventTopic { + continue + } + + onRamp, err := onramp.NewOnRamp(lg.Address, client) + require.NoError(t, err) + + messageSentEvent, err := onRamp.ParseCCIPMessageSent(*lg) + require.NoError(t, err) + sentEvent = messageSentEvent + + break + } + require.NotNil(t, sentEvent, "Sent event not found in transaction logs") + fmt.Printf("Message sent with ID: %s\n", common.Bytes2Hex(sentEvent.MessageId[:])) + }) + t.Run("Send: EVM -> Canton (Token Transfer)", func(t *testing.T) { // Source finality := protocol.FinalityWaitForFinality From 606115395f23ff9d6a671d3d0649ee7139c8443f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedemann=20F=C3=BCrst?= <59653747+friedemannf@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:45:18 +0200 Subject: [PATCH 6/6] Update --- examples/tests/examples_test.go | 118 ++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 51 deletions(-) diff --git a/examples/tests/examples_test.go b/examples/tests/examples_test.go index a3fa561fe..70ded312d 100644 --- a/examples/tests/examples_test.go +++ b/examples/tests/examples_test.go @@ -467,8 +467,8 @@ func TestCanton(t *testing.T) { t.Run("Send: Canton->EVM (Message Only)", func(t *testing.T) { messageReceiver := ccipReceiverContract - feeToken := amuletInstrumentID - feeTokenTransferInstructionClient := amuletTransferInstructionClient + feeToken := linkInstrumentId + feeTokenTransferInstructionClient := transferInstructionEdsClient // Get fee token input holdings feeTokenHoldings, err := testhelpers.ListHoldingsForInstrument(t.Context(), participant, feeToken) @@ -477,6 +477,7 @@ func TestCanton(t *testing.T) { for i, holding := range feeTokenHoldings { feeTokenInputCids[i] = types.CONTRACT_ID(holding.ContractID) } + feeTokenInputCids = []types.CONTRACT_ID{"00a923ff750bde2770df24811836a28e4106b1d4087e52ddd995f196d70e90283cca12122083dc750c95db42fe29438e9ee2a6ac5ed85a7d19938e2d99a1c0032e2776acab"} transferFactory, err := testhelpers.GetTransferFactoryV2(t.Context(), feeTokenTransferInstructionClient, string(feeToken.Admin), splice_api_token_transfer_instruction_v1.Transfer{ Sender: types.PARTY(participant.PartyID), @@ -605,16 +606,20 @@ func TestCanton(t *testing.T) { messageReceiver := common.HexToAddress("0x90392A1E8A941098a3C75E0BDB172cFdE7E4f1f4") transferAmount := "0.123456789" - feeToken := amuletInstrumentID - feeTokenTransferInstructionClient := amuletTransferInstructionClient + feeToken := linkInstrumentId + feeTokenTransferInstructionClient := transferInstructionEdsClient // Manually set fee token input holding Cids // If empty, will automatically select all holdings - feeTokenInputCids := []types.CONTRACT_ID{} + feeTokenInputCids := []types.CONTRACT_ID{ + "00a923ff750bde2770df24811836a28e4106b1d4087e52ddd995f196d70e90283cca12122083dc750c95db42fe29438e9ee2a6ac5ed85a7d19938e2d99a1c0032e2776acab", + } // Manually set token transfer input holding Cids // If empty, will automatically select all holdings - tokenTransferInputCids := []types.CONTRACT_ID{} + tokenTransferInputCids := []types.CONTRACT_ID{ + "00e4b346645763de5dd202f20b1fa89e96b82b45b1ca3ce836587f9d3c6893e7a7ca12122099eb432b785c0a10610ac25cb1be32df24ba876bcea043057b12cae90c793481", + } // Get fee token input holdings, if not manually specified if len(feeTokenInputCids) == 0 { @@ -871,57 +876,59 @@ func TestCanton(t *testing.T) { executorSendDisclosure, err := eds.GetExecutorSendDisclosure(t.Context(), executorEdsClient, msg, defaultExecutorAddress.InstanceAddress(), ccipSendDisclosure.CCVs) require.NoError(t, err) - sendArgs := sender.Send{ - DestinationChainSelector: types.NUMERIC(msg.DestinationChainSelector), - Message: core.Canton2AnyMessage{ - Receiver: types.TEXT(msg.Receiver), - Payload: types.TEXT(msg.Payload), - TokenTransfer: &core.TokenTransfer{ - Token: *linkInstrumentId, - Amount: types.NUMERIC(msg.TokenTransfer.Amount), + getSendArgs := func() sender.Send { + return sender.Send{ + DestinationChainSelector: types.NUMERIC(msg.DestinationChainSelector), + Message: core.Canton2AnyMessage{ + Receiver: types.TEXT(msg.Receiver), + Payload: types.TEXT(msg.Payload), + TokenTransfer: &core.TokenTransfer{ + Token: *linkInstrumentId, + Amount: types.NUMERIC(msg.TokenTransfer.Amount), + }, + FeeToken: *feeToken, + ExtraArgs: core.ExtraArgs{ + V3: &core.GenericExtraArgsV3{ + GasLimit: types.INT64(msg.GasLimit), + Ccvs: nil, + Executor: core.ExecutorExtraArg{ + ExecutorUseDefault: &core.ExecutorUseDefault{ExecutorArgs: ""}, + }, + TokenReceiver: "", + TokenArgs: "", + }, + }, }, - FeeToken: *feeToken, - ExtraArgs: core.ExtraArgs{ - V3: &core.GenericExtraArgsV3{ - GasLimit: types.INT64(msg.GasLimit), - Ccvs: nil, - Executor: core.ExecutorExtraArg{ - ExecutorUseDefault: &core.ExecutorUseDefault{ExecutorArgs: ""}, + Context: ccipSendDisclosure.ChoiceContext, + RouterCid: types.CONTRACT_ID(routerCid), + FeeTokenInput: sender.FeeTokenInput{ + SenderInputCids: feeTokenInputCids, + FeeTokenConfigCid: types.CONTRACT_ID(ccipSendDisclosure.FeeTokenConfigCid), + FeeTokenTransferFactory: types.CONTRACT_ID(transferFactory.FactoryID), + FeeTokenExtraArgs: splice_api_token_metadata_v1.ExtraArgs{ + Context: choiceContext, + Meta: splice_api_token_metadata_v1.Metadata{ + Values: map[string]types.TEXT{}, }, - TokenReceiver: "", - TokenArgs: "", }, }, - }, - Context: ccipSendDisclosure.ChoiceContext, - RouterCid: types.CONTRACT_ID(routerCid), - FeeTokenInput: sender.FeeTokenInput{ - SenderInputCids: feeTokenInputCids, - FeeTokenConfigCid: types.CONTRACT_ID(ccipSendDisclosure.FeeTokenConfigCid), - FeeTokenTransferFactory: types.CONTRACT_ID(transferFactory.FactoryID), - FeeTokenExtraArgs: splice_api_token_metadata_v1.ExtraArgs{ - Context: choiceContext, - Meta: splice_api_token_metadata_v1.Metadata{ - Values: map[string]types.TEXT{}, + CcvSendInputs: []sender.CCVSendInput{ + { + CcvAddress: ccvSendDisclosure.Address.Binding(), + CcvCid: types.CONTRACT_ID(ccvSendDisclosure.ContractId), + CcvExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, }, }, - }, - CcvSendInputs: []sender.CCVSendInput{ - { - CcvAddress: ccvSendDisclosure.Address.Binding(), - CcvCid: types.CONTRACT_ID(ccvSendDisclosure.ContractId), - CcvExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, + TokenTransferInput: &sender.TokenTransferInput{ + SenderInputCids: tokenTransferInputCids, + TokenPoolCid: types.CONTRACT_ID(tokenPoolSendDisclosure.ContractId), + PoolExtraContext: tokenPoolSendDisclosure.ChoiceContext, }, - }, - TokenTransferInput: &sender.TokenTransferInput{ - SenderInputCids: tokenTransferInputCids, - TokenPoolCid: types.CONTRACT_ID(tokenPoolSendDisclosure.ContractId), - PoolExtraContext: tokenPoolSendDisclosure.ChoiceContext, - }, - ExecutorInput: &sender.ExecutorInput{ - ExecutorCid: types.CONTRACT_ID(executorSendDisclosure.ContractId), - ExecutorExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, - }, + ExecutorInput: &sender.ExecutorInput{ + ExecutorCid: types.CONTRACT_ID(executorSendDisclosure.ContractId), + ExecutorExtraContext: splice_api_token_metadata_v1.ChoiceContext{}, + }, + } } allDisclosures := slices.Concat( transferFactory.DisclosedContracts, @@ -933,6 +940,7 @@ func TestCanton(t *testing.T) { t.Run("Exceed max data size", func(t *testing.T) { // Exceed the max of 32kb + sendArgs := getSendArgs() sendArgs.Message.Payload = types.TEXT(hex.EncodeToString(bytes.Repeat([]byte("A"), 32_001))) _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ @@ -956,6 +964,7 @@ func TestCanton(t *testing.T) { t.Run("Exceed gas limit", func(t *testing.T) { // Exceed the max of 15M + sendArgs := getSendArgs() sendArgs.Message.ExtraArgs.V3.GasLimit = 15_000_001 _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ @@ -978,6 +987,7 @@ func TestCanton(t *testing.T) { }) t.Run("Invalid extraArgs", func(t *testing.T) { + sendArgs := getSendArgs() sendArgs.Message.ExtraArgs.V3 = nil _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ @@ -1000,6 +1010,7 @@ func TestCanton(t *testing.T) { }) t.Run("Invalid dest chain selector", func(t *testing.T) { + sendArgs := getSendArgs() sendArgs.DestinationChainSelector = types.NUMERIC("1234") _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ @@ -1022,6 +1033,7 @@ func TestCanton(t *testing.T) { }) t.Run("Invalid fee token", func(t *testing.T) { + sendArgs := getSendArgs() sendArgs.Message.FeeToken = splice_api_token_holding_v1.InstrumentId{ Admin: ccipOwnerPartyID, Id: "invalid-fee-token", @@ -1047,6 +1059,7 @@ func TestCanton(t *testing.T) { }) t.Run("No fee token holding", func(t *testing.T) { + sendArgs := getSendArgs() sendArgs.FeeTokenInput.SenderInputCids = []types.CONTRACT_ID{} _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ @@ -1069,6 +1082,7 @@ func TestCanton(t *testing.T) { }) t.Run("Invalid token transfer instrument", func(t *testing.T) { + sendArgs := getSendArgs() sendArgs.Message.TokenTransfer.Token = splice_api_token_holding_v1.InstrumentId{ Admin: ccipOwnerPartyID, Id: "invalid-token-transfer", @@ -1094,6 +1108,7 @@ func TestCanton(t *testing.T) { }) t.Run("Zero transfer amount", func(t *testing.T) { + sendArgs := getSendArgs() sendArgs.Message.TokenTransfer.Amount = "0.0" _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ @@ -1116,6 +1131,7 @@ func TestCanton(t *testing.T) { }) t.Run("Empty receiver", func(t *testing.T) { + sendArgs := getSendArgs() sendArgs.Message.Receiver = "" _, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(t.Context(), &apiv2.SubmitAndWaitForTransactionRequest{ @@ -1247,7 +1263,7 @@ func TestEVM(t *testing.T) { receiverPartyHashed := contracts.HashedPartyFromString(receiverParty) - extraArgs, err := evm.NewV3ExtraArgs(finality, execGasLimit, executorAddress.Hex(), nil, nil, nil, nil) + extraArgs, err := evm.NewV3ExtraArgs(finality, execGasLimit, executorAddress.Hex(), nil, receiverPartyHashed.Bytes(), nil, nil) require.NoError(t, err) msg := routerwrapper.ClientEVM2AnyMessage{