Skip to content

feat(canton): integrate Canton support#613

Open
RodrigoAD wants to merge 33 commits into
mainfrom
canton-main
Open

feat(canton): integrate Canton support#613
RodrigoAD wants to merge 33 commits into
mainfrom
canton-main

Conversation

@RodrigoAD
Copy link
Copy Markdown
Member

No description provided.

- Adds Canton configurer implementation, unit and e2e tests
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 6, 2026

⚠️ No Changeset found

Latest commit: b1fc944

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Comment thread sdk/canton/resolver.go Fixed
JohnChangUK and others added 5 commits April 7, 2026 22:57
## Desc
Resolve targetCids execution time

- Added TargetTemplateID field to AdditionalFields
- Added ResolveTargetContractID resolver function
- Updated `TimelockExecutor.Execute` and `Executor.ExecuteOperation` for
auto-resolution
Comment thread sdk/canton/resolver.go Fixed
Comment thread sdk/canton/resolver.go Fixed
Comment thread sdk/canton/resolver.go Fixed
stackman27 and others added 3 commits May 20, 2026 23:18
Resolve go.mod/go.sum conflicts by keeping main dependency updates
alongside Canton-specific modules (chainlink-canton, go-daml, dazl-client,
chainlink-deployments-framework).

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown
Contributor

@gustavogama-cll gustavogama-cll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitting a few thoughts after a shallow initial pass because I think there is one request (the last comment) that might require a bigger refactoring.

Comment thread chainwrappers/executors.go Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see canton changes to the chainwrappers/executors_test.go in this PR, nor in #628.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is what I have in mind:
ad7ee04

I don't think we need a separate test case for canton. And this exercises the code paths more thoroughly.

Comment thread chainwrappers/inspectors.go Outdated
Comment thread chainwrappers/inspectors.go Outdated
Comment thread chainwrappers/timelock_executors.go Outdated
Comment thread timelock_proposal.go Outdated
Copilot AI review requested due to automatic review settings May 29, 2026 21:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 43 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • chainwrappers/mocks/chain_accessor.go: Language not supported

AddChainMetadata(s.chainSelector, cancellerMetadata).
SetAction(types.TimelockActionCancel).
SetDelay(delay).
SetSalt((*common.Hash)(new(scheduleProposal.Salt()))).
Comment thread sdk/canton/configurer.go
}

return types.TransactionResult{
Hash: "tx.Digest",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe we should use transaction.GetExternalTransactionHash()?

Hash: "0x" + hex.EncodeToString(transaction.GetExternalTransactionHash()),

Copy link
Copy Markdown
Contributor

@gustavogama-cll gustavogama-cll Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow up question: I see the other methods are returning commandID, as mentioned by Copilot. So, if "commandID" makes more sense for Canton than the transaction hash, we should use it (though I'd be curious to understand why "commandID" is the right choice and not the tx hash).

Either way, a hardcoded "tx.Digest" seems wrong.

Comment thread validation.go
Comment on lines +139 to +152
case chainsel.FamilyCanton:
ch, ok := chains.CantonChain(rawSelector)
if !ok || len(ch.Participants) == 0 {
return nil, fmt.Errorf("missing Canton chain participant for selector %d", rawSelector)
}
participant := ch.Participants[0]
mcmsParties := lo.Map(ch.Participants, func(p cantonsdk.Participant, _ int) string { return p.PartyID })

return cantonsdk.NewTimelockExecutor(
participant.LedgerServices.Command,
participant.LedgerServices.State,
participant.PartyID,
mcmsParties,
), nil
Comment thread sdk/canton/executor.go
Comment on lines +368 to +374
func PadLeft32(hexStr string) string {
if len(hexStr) >= hexWordLen {
return hexStr[:hexWordLen]
}

return strings.Repeat("0", hexWordLen-len(hexStr)) + hexStr
}
Comment thread sdk/canton/configurer.go
Comment on lines +79 to +114
// Build exercise command using generated bindings
mcmsContract := mcmscore.MCMS{}
exerciseCmd := mcmsContract.SetConfig(mcmsContractID, input)

// Parse template ID
packageID, moduleName, entityName, err := parseTemplateIDFromString(mcmsContract.GetTemplateID())
if err != nil {
return types.TransactionResult{}, fmt.Errorf("failed to parse template ID: %w", err)
}

// Convert input to choice argument
choiceArgument := ledger.MapToValue(input)

commandID := uuid.Must(uuid.NewUUID()).String()
submitResp, err := c.client.SubmitAndWaitForTransaction(ctx, &apiv2.SubmitAndWaitForTransactionRequest{
Commands: &apiv2.Commands{
WorkflowId: "mcms-set-config",
CommandId: commandID,
ActAs: []string{c.mcmsParties[0]},
ReadAs: c.mcmsParties,
Commands: []*apiv2.Command{{
Command: &apiv2.Command_Exercise{
Exercise: &apiv2.ExerciseCommand{
TemplateId: &apiv2.Identifier{
PackageId: packageID,
ModuleName: moduleName,
EntityName: entityName,
},
ContractId: exerciseCmd.ContractID,
Choice: exerciseCmd.Choice,
ChoiceArgument: choiceArgument,
},
},
}},
},
})
Copilot AI review requested due to automatic review settings May 31, 2026 16:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings June 1, 2026 19:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 44 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • chainwrappers/mocks/chain_accessor.go: Language not supported

Comment thread sdk/canton/helpers.go
Comment on lines +43 to +54
// parseTemplateIDFromString parses a template ID string like "#package:Module:Entity" into its components
func parseTemplateIDFromString(templateID string) (packageID, moduleName, entityName string, err error) {
if !strings.HasPrefix(templateID, "#") {
return "", "", "", fmt.Errorf("template ID must start with #")
}
parts := strings.Split(templateID, ":")
if len(parts) != templateIDPartCount {
return "", "", "", fmt.Errorf("template ID must have format #package:module:entity, got: %s", templateID)
}

return parts[0], parts[1], parts[2], nil
}
Comment thread sdk/canton/configurer.go
Comment on lines +92 to +114
commandID := uuid.Must(uuid.NewUUID()).String()
submitResp, err := c.client.SubmitAndWaitForTransaction(ctx, &apiv2.SubmitAndWaitForTransactionRequest{
Commands: &apiv2.Commands{
WorkflowId: "mcms-set-config",
CommandId: commandID,
ActAs: []string{c.mcmsParties[0]},
ReadAs: c.mcmsParties,
Commands: []*apiv2.Command{{
Command: &apiv2.Command_Exercise{
Exercise: &apiv2.ExerciseCommand{
TemplateId: &apiv2.Identifier{
PackageId: packageID,
ModuleName: moduleName,
EntityName: entityName,
},
ContractId: exerciseCmd.ContractID,
Choice: exerciseCmd.Choice,
ChoiceArgument: choiceArgument,
},
},
}},
},
})
Comment thread sdk/canton/configurer.go
Comment on lines +140 to +144
return types.TransactionResult{
Hash: "tx.Digest",
ChainFamily: cselectors.FamilyCanton,
RawData: rawDataFromMCMSTx(newMCMSContractID, newMCMSTemplateID, submitResp),
}, nil
Comment on lines +144 to +154
cancelProposal, err := mcms.NewTimelockProposalBuilder().
SetVersion("v1").
SetValidUntil(validUntil).
SetDescription("Canton timelock - cancel scheduled batch").
AddTimelockAddress(s.chainSelector, s.mcmsInstanceAddress).
AddChainMetadata(s.chainSelector, cancellerMetadata).
SetAction(types.TimelockActionCancel).
SetDelay(delay).
SetSalt((*common.Hash)(new(scheduleProposal.Salt()))).
AddOperation(bop).
Build()
Comment on lines +49 to +52
data, err := hex.DecodeString(sb.String())
if err != nil {
panic("HashTimelockOpId: invalid hex encoding: " + err.Error())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the current code paths ensure predecessor and salt are hex, but it's hard to keep track of this type of thing long term. I feels this is making a bad tradeoff just to save ~10 lines of error handling.

Comment thread validation.go
Copilot AI review requested due to automatic review settings June 2, 2026 20:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 47 changed files in this pull request and generated 8 comments.

Files not reviewed (1)
  • chainwrappers/mocks/chain_accessor.go: Language not supported

Comment thread sdk/canton/configurer.go
Comment on lines +140 to +144
return types.TransactionResult{
Hash: "tx.Digest",
ChainFamily: cselectors.FamilyCanton,
RawData: rawDataFromMCMSTx(newMCMSContractID, newMCMSTemplateID, submitResp),
}, nil
Comment thread sdk/canton/helpers.go
return "", "", "", fmt.Errorf("template ID must have format #package:module:entity, got: %s", templateID)
}

return parts[0], parts[1], parts[2], nil
Comment on lines +60 to +66
func encodeOperationDataForHash(operationData string) string {
if isValidHex(operationData) {
return operationData
}

return asciiToHex(operationData)
}
Comment thread sdk/canton/transaction.go
Comment on lines +11 to +22
func ValidateAdditionalFields(additionalFields json.RawMessage) error {
if len(additionalFields) == 0 {
return nil
}

var fields AdditionalFields
if err := json.Unmarshal(additionalFields, &fields); err != nil {
return fmt.Errorf("failed to unmarshal Canton additional fields: %w", err)
}

return fields.Validate()
}
Comment on lines +37 to +39
// AdditionalFieldsMetadata represents the Canton-specific metadata fields.
// MultisigId must be makeMcmsId(instanceId, role) e.g. "mcms-001-proposer" (DAML SetRoot/ExecuteOp).
// InstanceId is the base MCMS instanceId for self-dispatch TargetInstanceId (DAML E_NOT_SELF_DISPATCH).
Comment thread chainwrappers/chainaccessor.go
Comment thread sdk/canton/executor.go
Comment on lines +366 to +372
func PadLeft32(hexStr string) string {
if len(hexStr) >= hexWordLen {
return hexStr[:hexWordLen]
}

return strings.Repeat("0", hexWordLen-len(hexStr)) + hexStr
}
Comment on lines +142 to +154
// Build cancel proposal - reuse the same batch operation (the converter extracts operationId)
// Use the same salt as the schedule proposal to derive the same operation ID
cancelProposal, err := mcms.NewTimelockProposalBuilder().
SetVersion("v1").
SetValidUntil(validUntil).
SetDescription("Canton timelock - cancel scheduled batch").
AddTimelockAddress(s.chainSelector, s.mcmsInstanceAddress).
AddChainMetadata(s.chainSelector, cancellerMetadata).
SetAction(types.TimelockActionCancel).
SetDelay(delay).
SetSalt((*common.Hash)(new(scheduleProposal.Salt()))).
AddOperation(bop).
Build()
@cl-sonarqube-production
Copy link
Copy Markdown

Quality Gate failed Quality Gate failed

Failed conditions
9.4% Coverage on New Code (required ≥ 75%)

See analysis details on SonarQube

Copy link
Copy Markdown
Contributor

@gustavogama-cll gustavogama-cll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to take a break and will resume later. As a general comment, I find the test coverage pretty lacking. According to Sonarqube we're at ~9%. We have the e2e tests, which are definitely important but I do feel like there's a lot of missing unit tests which would help us maintain and validate the scenarios that deviate from the happy paths.

ChainId int64 `json:"chainId"`
MultisigId string `json:"multisigId"`
InstanceId string `json:"instanceId,omitempty"` // base instanceId; converter uses for TargetInstanceId in ScheduleBatch etc.
PreOpCount uint64 `json:"preOpCount"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are PreOpCount, PostOpCount and OverridePreviousRoot part of "AdditionalFieldsMetadata"? The AdditionalFields attribute should be reserved for data that the mcms clients need to manually add.

PreOpCount seems to be the same as StartingOpCount, OverridePreviousRoot is already available as an attribute of the proposal itself, and PostOpCount can be calculated on the fly when needed using txcount and startingOpCount.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent a few minutes commenting out these fields to check my understanding and see it something would break: b6da3ed

Looks ok to my eyes. Let me know if I'm missing something.

Comment thread sdk/canton/configurer.go
}

return types.TransactionResult{
Hash: "tx.Digest",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe we should use transaction.GetExternalTransactionHash()?

Hash: "0x" + hex.EncodeToString(transaction.GetExternalTransactionHash()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is what I have in mind:
ad7ee04

I don't think we need a separate test case for canton. And this exercises the code paths more thoroughly.

Comment thread chainwrappers/chainaccessor.go
Comment thread sdk/canton/encoder.go
if n < 0 {
panic("intToHex: negative numbers not supported")
}
if n == 0 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: fmt.Sprintf("%x", n) returns "0". I don't understand why we need the if n == 0.

Comment thread sdk/canton/encoder.go

// Build the encoded data with domain separator and length prefix for multisigId
encoded := metadataLeafDomainSeparator +
padLeft32(intToHex(int(metadataFields.ChainId))) +
Copy link
Copy Markdown
Contributor

@gustavogama-cll gustavogama-cll Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since intToHex and and padLeft32 can panic, should we add checks to the metadata validation? I see we do chainId != 0 but not chainId < 0. Maybe we could add similar checks to MultisigId and any other fields which we indirectly pass to functions that can panic.

Comment thread sdk/canton/helpers.go
)

const (
MCMSTemplateKey = "MCMS.Main:MCMS"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it feels like some of these constants should be defined closer to where they are used. A dedicated constants.go file is ok as well, but placing everything in helpers.go is confusing.

Comment thread sdk/canton/helpers.go
}

// ParseTemplateIDFromString is the exported version of parseTemplateIDFromString
func ParseTemplateIDFromString(templateID string) (packageID, moduleName, entityName string, err error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need the unexported version?

Comment thread sdk/canton/helpers.go
}

// FormatTemplateID is the exported version of formatTemplateID
func FormatTemplateID(id *apiv2.Identifier) string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question here: why do we need an unexported version?

Comment thread sdk/canton/configurer.go
}

return types.TransactionResult{
Hash: "tx.Digest",
Copy link
Copy Markdown
Contributor

@gustavogama-cll gustavogama-cll Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow up question: I see the other methods are returning commandID, as mentioned by Copilot. So, if "commandID" makes more sense for Canton than the transaction hash, we should use it (though I'd be curious to understand why "commandID" is the right choice and not the tx hash).

Either way, a hardcoded "tx.Digest" seems wrong.

Copy link
Copy Markdown
Contributor

@gustavogama-cll gustavogama-cll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another quick round, will resume later

Comment thread sdk/canton/inspector.go
Comment on lines +102 to +110
// Parse the root from hex string
rootStr := string(expiringRoot.Root)
rootStr = strings.TrimPrefix(rootStr, "0x")
rootBytes, err := hex.DecodeString(rootStr)
if err != nil {
return common.Hash{}, 0, fmt.Errorf("failed to decode root hash: %w", err)
}

root := common.BytesToHash(rootBytes)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Parse the root from hex string
rootStr := string(expiringRoot.Root)
rootStr = strings.TrimPrefix(rootStr, "0x")
rootBytes, err := hex.DecodeString(rootStr)
if err != nil {
return common.Hash{}, 0, fmt.Errorf("failed to decode root hash: %w", err)
}
root := common.BytesToHash(rootBytes)
root := common.HexToHash(string(expiringRoot.Root))

Comment thread sdk/canton/inspector.go
}

// For Canton, MCMAddress is the InstanceAddress hex (stable across SetRoot/ExecuteOp)
mcmAddress := contracts.InstanceID(string(mcmsContract.InstanceId)).RawInstanceAddress(mcmsContract.Owner).InstanceAddress().Hex()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this. It seems we return a different address than the one passed as input. I see aptos and sui doing the same thing but I do want to flag it because this is counterintuitive to me and a potential source of bugs.

Comment thread sdk/canton/inspector.go
}

quorum := uint8(0)
if i < len(bindConfig.GroupQuorums) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can drop this condition and simplify the code a bit if you check that len(bindConfig.GroupQuorums) <= maxMCMSGroups before the for.

Comment thread sdk/canton/inspector.go
// Process in reverse order to build the tree from leaves to root
for i := maxMCMSGroups - 1; i >= 0; i-- {
parent := uint8(0)
if i < len(bindConfig.GroupParents) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here; you can check that len(bindConfig.GroupParents) <= maxMCMSGroups before the for.

@@ -0,0 +1,200 @@
//go:build e2e
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appreciate the unit tests (for a change 😛 ) but why does it need the go:build e2e?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we merge this into the helpers.go file? I don't see a good reason to have two files for generic helpers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the chain_metadata.go file might also be a good option since the CantonRoleFromAction function is related to the TimelockRole that is defined there.

Comment thread sdk/canton/executor.go
}

// Extract metadata fields for chainId and multisigId
var metadataFields struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we have AdditionalFieldsMetadata and parseAdditionalFieldsMetadata already?

Comment thread sdk/canton/executor.go
return types.TransactionResult{}, fmt.Errorf("failed to parse template ID: %w", err)
}

commandID := uuid.Must(uuid.NewUUID()).String()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
commandID := uuid.Must(uuid.NewUUID()).String()
commandID := uuid.NewString()

(unless you need uuid v1)

Comment thread sdk/canton/executor.go
// Convert input to choice argument
choiceArgument := ledger.MapToValue(input)

commandID := uuid.Must(uuid.NewUUID()).String()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same suggestion:

Suggested change
commandID := uuid.Must(uuid.NewUUID()).String()
commandID := uuid.NewString()

Comment thread sdk/canton/executor.go
}, nil
}

func PadLeft32(hexStr string) string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't used; you already have padLeft32 in encoder.go.

ChainSelector: bop.ChainSelector,
Transaction: types.Transaction{
To: mcmAddress,
Data: []byte{0x00}, // placeholder for validators; Canton uses AdditionalFields.OperationData
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use OperationData and not Data?

I'm also curious as to why we need to duplicate mcmAddress as TargetCid. I tried to follow the execution flow but it seems TargetCid isn't really used (other than check for "TargetCid == ""` in executor.go).


// TimelockCallForHash is used for computing the operation ID hash.
// Field semantics match mcms.TimelockCall (TargetInstanceAddress, FunctionName, OperationData).
type TimelockCallForHash struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can probably be made private

// HashTimelockOpId computes the operation ID for timelock operations.
// Matches Canton's hashTimelockOpId: keccak256(encodedCalls || predecessor || salt).
// predecessor and salt should be hex-encoded (e.g. 64-char hex for 32-byte hashes).
func HashTimelockOpId(calls []TimelockCallForHash, predecessor, salt string) string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this too

Comment on lines +49 to +52
data, err := hex.DecodeString(sb.String())
if err != nil {
panic("HashTimelockOpId: invalid hex encoding: " + err.Error())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the current code paths ensure predecessor and salt are hex, but it's hard to keep track of this type of thing long term. I feels this is making a bad tradeoff just to save ~10 lines of error handling.

return asciiToHex(operationData)
}

func isValidHex(s string) bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's some redundancy with IsInstanceAddressHex in resolver.go. Maybe it should call isValidHex.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants