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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions pkg/reader/usdc_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
153 changes: 153 additions & 0 deletions pkg/reader/usdc_reader_sui.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading