From 5e727f97ce63e9ef1c4743e3a1955e5555d2693b Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Thu, 25 Sep 2025 13:49:33 -0400 Subject: [PATCH] add usdc reader for sui --- pkg/reader/usdc_reader.go | 27 ++++++ pkg/reader/usdc_reader_sui.go | 153 ++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 pkg/reader/usdc_reader_sui.go diff --git a/pkg/reader/usdc_reader.go b/pkg/reader/usdc_reader.go index 79b910c2f..7deb6a3ce 100644 --- a/pkg/reader/usdc_reader.go +++ b/pkg/reader/usdc_reader.go @@ -40,6 +40,7 @@ var CCTPDestDomains = map[uint64]uint32{ sel.SOLANA_MAINNET.Selector: 5, sel.ETHEREUM_MAINNET_BASE_1.Selector: 6, sel.POLYGON_MAINNET.Selector: 7, + sel.SUI_MAINNET.Selector: 8, // ---------- Testnet Domains ---------- sel.ETHEREUM_TESTNET_SEPOLIA.Selector: 0, sel.AVALANCHE_TESTNET_FUJI.Selector: 1, @@ -48,6 +49,7 @@ var CCTPDestDomains = map[uint64]uint32{ sel.SOLANA_DEVNET.Selector: 5, sel.ETHEREUM_TESTNET_SEPOLIA_BASE_1.Selector: 6, sel.POLYGON_TESTNET_AMOY.Selector: 7, + sel.SUI_TESTNET.Selector: 8, } type eventID [32]byte @@ -140,6 +142,31 @@ func NewUSDCMessageReader( lggr: lggr, contractReader: contractReaders[chainSelector], } + case sel.FamilySui: + // Bind the TokenPool contract, the contract re-emits the USDC MessageSent event along with other metadata. + bytesAddress, err := addrCodec.AddressStringToBytes(token.SourceMessageTransmitterAddr, chainSelector) + if err != nil { + return nil, err + } + + // Bind the USDC Token Pool contract, this is where CCTP MessageSent events are emitted for Sui. + _, err = bindReaderContract( + ctx, + lggr, + contractReaders, + chainSelector, + consts.ContractNameUSDCTokenPool, + bytesAddress, + addrCodec, + ) + if err != nil { + return nil, err + } + + readers[chainSelector] = suiUSDCMessageReader{ + lggr: lggr, + contractReader: contractReaders[chainSelector], + } default: return nil, fmt.Errorf("unsupported chain selector family %s for chain %d", family, chainSelector) } diff --git a/pkg/reader/usdc_reader_sui.go b/pkg/reader/usdc_reader_sui.go new file mode 100644 index 000000000..784d443dc --- /dev/null +++ b/pkg/reader/usdc_reader_sui.go @@ -0,0 +1,153 @@ +package reader + +import ( + "context" + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/query" + "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + + "github.com/smartcontractkit/chainlink-ccip/pkg/consts" + "github.com/smartcontractkit/chainlink-ccip/pkg/contractreader" +) + +type suiUSDCMessageReader struct { + lggr logger.Logger + contractReader contractreader.Extended +} + +// getMessageTokenData extracts token data from the CCTP MessageSent event. +func (u suiUSDCMessageReader) getMessageTokenData( + tokens map[MessageTokenID]cciptypes.RampTokenAmount, +) (map[MessageTokenID]SourceTokenDataPayload, error) { + messageTransmitterEvents := make(map[MessageTokenID]SourceTokenDataPayload) + + for id, token := range tokens { + sourceTokenPayload, err := extractABIPayload(token.ExtraData) + if err != nil { + return nil, err + } + messageTransmitterEvents[id] = *sourceTokenPayload + } + return messageTransmitterEvents, nil +} + +// SuiCCTPUSDCMessageEvent matches the onChain MessageSent event for Sui. +type SuiCCTPUSDCMessageEvent struct { + Version uint32 + SourceDomain uint32 + DestinationDomain uint32 + Nonce uint64 + Sender []byte + Recipient []byte + DestinationCaller []byte + MessageBody []byte +} + +// MessagesByTokenID retrieves the CCTP MessageSent events for Sui. +func (u suiUSDCMessageReader) MessagesByTokenID( + ctx context.Context, + source, dest cciptypes.ChainSelector, + tokens map[MessageTokenID]cciptypes.RampTokenAmount, +) (map[MessageTokenID]cciptypes.Bytes, error) { + if len(tokens) == 0 { + return map[MessageTokenID]cciptypes.Bytes{}, nil + } + u.lggr.Debugw("Searching for Sui CCTP USDC logs", + "numExpected", len(tokens)) + + // Parse the extra data field to get the CCTP nonces and source domains. + cctpData, err := u.getMessageTokenData(tokens) + if err != nil { + return nil, err + } + + // Query the token pool contract for the MessageSent event data. + // Similar to Solana, we need separate expressions for each nonce and source domain pair. + expressions := []query.Expression{} + for _, data := range cctpData { + expressions = append(expressions, query.And( + query.Comparator( + consts.EventAttributeCCTPNonce, + primitives.ValueComparator{ + Value: data.Nonce, + Operator: primitives.Eq, + }, + ), + query.Comparator( + consts.EventAttributeSourceDomain, + primitives.ValueComparator{ + Value: data.SourceDomain, + Operator: primitives.Eq, + }, + ), + )) + } + + // Parent expressions for the query. + keyFilter, err := query.Where( + // Using same as EVM for consistency. Only used by off-chain components so name does not have to align with on-chain + consts.EventNameCCTPMessageSent, + query.Confidence(primitives.Finalized), + query.Or(expressions...), + ) + if err != nil { + return nil, err + } + + iter, err := u.contractReader.ExtendedQueryKey( + ctx, + consts.ContractNameUSDCTokenPool, + keyFilter, + query.NewLimitAndSort( + query.Limit{Count: uint64(len(cctpData))}, + query.NewSortBySequence(query.Asc), + ), + &SuiCCTPUSDCMessageEvent{}, + ) + if err != nil { + return nil, fmt.Errorf("error querying contract reader for chain %d: %w", source, err) + } + + u.lggr.Debugw("CR query results", "count", len(iter)) + + // Read CR responses and store the results. + out := make(map[MessageTokenID]cciptypes.Bytes) + for _, item := range iter { + u.lggr.Debugw("CR query item", "item", item) + event, ok1 := item.Data.(*SuiCCTPUSDCMessageEvent) + if !ok1 { + return nil, fmt.Errorf("failed to cast %v to Message", item.Data) + } + u.lggr.Debugw("CR query event", "event", event) + + // This is O(n^2). We could optimize it by storing the cctpData in a map with a composite key. + for tokenID, metadata := range cctpData { + if metadata.Nonce == event.Nonce && metadata.SourceDomain == event.SourceDomain { + out[tokenID] = event.MessageBody + u.lggr.Infow("Found CCTP event", "tokenID", tokenID, "event", event) + break + } + } + + u.lggr.Warnw("Found unexpected CCTP event", "event", event) + } + + // Check if any were missed. + for tokenID := range tokens { + if _, ok := out[tokenID]; !ok { + // Token is not available in the source chain, it should never happen at this stage + u.lggr.Warnw("Message not found in the source chain", + "seqNr", tokenID.SeqNr, + "tokenIndex", tokenID.Index, + "chainSelector", source, + "data", cctpData[tokenID], + ) + } + } + + return out, nil +}