Skip to content

Implement native canton driver#225

Open
PawelBis wants to merge 23 commits intomainfrom
canton
Open

Implement native canton driver#225
PawelBis wants to merge 23 commits intomainfrom
canton

Conversation

@PawelBis
Copy link
Copy Markdown
Contributor

No description provided.

@PawelBis PawelBis force-pushed the canton branch 4 times, most recently from 1508c97 to e48912b Compare March 19, 2026 09:56
@PawelBis PawelBis force-pushed the canton branch 4 times, most recently from a5b4628 to 645b15b Compare March 26, 2026 06:45
@PawelBis PawelBis marked this pull request as ready for review March 26, 2026 11:58
@PawelBis PawelBis changed the title CANTON: Wip Implement native canton driver Mar 26, 2026
@PawelBis PawelBis requested a review from conorpp March 26, 2026 12:00
func computeFingerprint(rawPubKey []byte) string {
// HashPurpose.PublicKeyFingerprint id=12 encoded as big-endian int32 (4 bytes)
var purposeBytes [4]byte
binary.BigEndian.PutUint32(purposeBytes[:], 12)
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.

would be nice to see a defined constant for this "12" purpose and any comment or link to what the purpose means, or if there are others.

Comment on lines +102 to +119
for _, seed := range []byte{0, 1, 127, 255} {
key := make([]byte, 32)
for i := range key {
key[i] = seed
}
addr, err := builder.GetAddressFromPublicKey(key)
require.NoError(t, err)
_, fp, err := ParsePartyID(addr)
require.NoError(t, err)
require.Equal(t, 68, len(fp), "fingerprint must always be 68 chars")
expected := map[byte]string{
0: "1220ea618da83b6c6b2c4557ffa17d722045169f52b8f50f3b31fc867e266de7e53d",
1: "1220974cb80e78f2fea077628a02faa4c57d68a65036eea27fb3463088a1c8527a99",
127: "122087fab42073577d1d066f0cc217b347aedf5a73ee5feaa75d5e96538dad977b91",
255: "122044e19d94c296e8397d61e759ff1692e5dff8efbcd70d7a9b4033d8b4a259ccd0",
}
require.Equal(t, expected[seed], fp)
}
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.

little messy, please refactor into full table-based test.

name: "valid - pubkey hex name",
addr: xc.Address("aabbccdd::" + validFP),
expectedName: "aabbccdd",
expectedFP: validFP,
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.

expectedFP: "1220" + hex.EncodeToString(make([]byte, 32)), // "1220" + 64 zeros

Comment on lines +17 to +18
PreparedTransaction *interactive.PreparedTransaction
HashingSchemeVersion interactive.HashingSchemeVersion
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.

	PreparedTransaction  *interactive.PreparedTransaction `json:"prepared_transaction"`
	HashingSchemeVersion interactive.HashingSchemeVersion `json:"hashing_scheme_version"`

Comment on lines +383 to +394
var req interactive.ExecuteSubmissionRequest
if err := proto.Unmarshal(payload, &req); err != nil {
return fmt.Errorf("failed to unmarshal Canton execute request: %w", err)
}

andWaitReq := &interactive.ExecuteSubmissionAndWaitRequest{
PreparedTransaction: req.PreparedTransaction,
PartySignatures: req.PartySignatures,
SubmissionId: req.SubmissionId,
UserId: req.UserId,
HashingSchemeVersion: req.HashingSchemeVersion,
}
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 unmarshal to ExecuteSubmissionRequest to then convert to ExecuteSubmissionAndWaitRequest?

Wouldn't it be more simple to serialize to ExecuteSubmissionAndWaitRequest originally?

Comment on lines +607 to +824
func extractTransferOutputs(ex *v2.ExercisedEvent, decimals int32) ([]amuletCreation, bool) {
if !isTransferExercise(ex) {
return nil, false
}

arg := ex.GetChoiceArgument()
if arg == nil {
return nil, false
}
root := arg.GetRecord()
if root == nil {
return nil, false
}

transferRecord := findRecordField(root, "transfer")
if transferRecord == nil {
return nil, false
}

var outputs []*v2.Value
for _, field := range transferRecord.GetFields() {
if field.GetLabel() == "outputs" {
if list := field.GetValue().GetList(); list != nil {
outputs = list.GetElements()
}
break
}
}
if len(outputs) == 0 {
return nil, false
}

parsed := make([]amuletCreation, 0, len(outputs))
for _, output := range outputs {
record := output.GetRecord()
if record == nil {
continue
}
var receiver string
var amount xc.AmountBlockchain
var ok bool
for _, field := range record.GetFields() {
switch field.GetLabel() {
case "receiver":
receiver = field.GetValue().GetParty()
case "amount":
amount, ok = extractNumericValue(field.GetValue(), decimals)
}
}
if receiver == "" || !ok {
continue
}
parsed = append(parsed, amuletCreation{owner: receiver, amount: amount})
}
if len(parsed) == 0 {
return nil, false
}
return parsed, true
}

func extractNumericValue(value *v2.Value, decimals int32) (xc.AmountBlockchain, bool) {
if value == nil {
return xc.AmountBlockchain{}, false
}
numeric := value.GetNumeric()
if numeric == "" {
return xc.AmountBlockchain{}, false
}
human, err := xc.NewAmountHumanReadableFromStr(numeric)
if err != nil {
return xc.AmountBlockchain{}, false
}
return human.ToBlockchain(decimals), true
}

func extractTransferFee(ex *v2.ExercisedEvent, decimals int32) (xc.AmountBlockchain, bool) {
if !isTransferExercise(ex) {
return xc.AmountBlockchain{}, false
}

result := ex.GetExerciseResult()
if result == nil {
return xc.AmountBlockchain{}, false
}
record := result.GetRecord()
if record == nil {
return xc.AmountBlockchain{}, false
}
record = unwrapTransferResultRecord(record)

if burned, ok := extractBurnedFee(record, decimals); ok {
return burned, true
}
if summaryFee, ok := extractSummaryFee(record, decimals); ok {
return summaryFee, true
}
return xc.AmountBlockchain{}, false
}

func isTransferExercise(ex *v2.ExercisedEvent) bool {
tid := ex.GetTemplateId()
if tid == nil || tid.GetModuleName() != "Splice.AmuletRules" {
return false
}
switch ex.GetChoice() {
case "AmuletRules_Transfer", "TransferPreapproval_Send":
return true
default:
return false
}
}

func unwrapTransferResultRecord(record *v2.Record) *v2.Record {
if nested := findRecordField(record, "result"); nested != nil {
return nested
}
return record
}

func extractBurnedFee(record *v2.Record, decimals int32) (xc.AmountBlockchain, bool) {
metaRecord := findRecordField(record, "meta")
if metaRecord == nil {
return xc.AmountBlockchain{}, false
}
valuesRecord := findRecordField(metaRecord, "values")
if valuesRecord == nil {
return xc.AmountBlockchain{}, false
}
burnedText, ok := extractTextMapValue(valuesRecord, "splice.lfdecentralizedtrust.org/burned")
if !ok || burnedText == "" {
return xc.AmountBlockchain{}, false
}
return parseHumanAmountToBlockchain(burnedText, decimals)
}

func extractSummaryFee(record *v2.Record, decimals int32) (xc.AmountBlockchain, bool) {
summaryRecord := findRecordField(record, "summary")
if summaryRecord == nil {
return xc.AmountBlockchain{}, false
}

total := xc.NewAmountBlockchainFromUint64(0)
found := false

if senderChangeFeeValue, ok := findValueField(summaryRecord, "senderChangeFee"); ok {
if fee, ok := extractNumericValue(senderChangeFeeValue, decimals); ok {
total = total.Add(&fee)
found = true
}
}
if outputFeesValue, ok := findValueField(summaryRecord, "outputFees"); ok {
if list := outputFeesValue.GetList(); list != nil {
for _, elem := range list.GetElements() {
fee, ok := extractNumericValue(elem, decimals)
if !ok {
continue
}
total = total.Add(&fee)
found = true
}
}
}

if !found {
return xc.AmountBlockchain{}, false
}
return total, true
}

func findRecordField(record *v2.Record, label string) *v2.Record {
value, ok := findValueField(record, label)
if !ok {
return nil
}
return value.GetRecord()
}

func findValueField(record *v2.Record, label string) (*v2.Value, bool) {
if record == nil {
return nil, false
}
for _, field := range record.GetFields() {
if field.GetLabel() == label {
return field.GetValue(), true
}
}
return nil, false
}

func extractTextMapValue(record *v2.Record, key string) (string, bool) {
for _, field := range record.GetFields() {
if field.GetLabel() != key {
continue
}
return field.GetValue().GetText(), true
}
return "", false
}

func parseHumanAmountToBlockchain(value string, decimals int32) (xc.AmountBlockchain, bool) {
human, err := xc.NewAmountHumanReadableFromStr(value)
if err != nil {
return xc.AmountBlockchain{}, false
}
return human.ToBlockchain(decimals), true
}

func parseRecoveryLookupId(value string) (int64, string, bool) {
idx := strings.Index(value, "-")
if idx <= 0 || idx == len(value)-1 {
return 0, "", false
}
beginExclusive, err := strconv.ParseInt(value[:idx], 10, 64)
if err != nil {
return 0, "", false
}
return beginExclusive, value[idx+1:], true
}
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.

If you refactor the protobuf to be generated locally, then you can refactor all of these helpers to be local methods on the pb types, to support TxInfo method.

E.g.

path/to/proto/record.pb.go
path/to/proto/record.go // <-- Add our own helpers in our own files like these

Comment on lines +904 to +906
func (client *Client) FetchBlock(ctx context.Context, args *xclient.BlockArgs) (*txinfo.BlockWithTransactions, error) {
return &txinfo.BlockWithTransactions{}, errors.New("not implemented")
}
Copy link
Copy Markdown
Contributor

@conorpp conorpp Apr 2, 2026

Choose a reason for hiding this comment

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

Is there any equivalent grouping of transactions on Canton that we could build on? Not a big deal if not.

Perhaps just fetching the latest transaction and reporting a "block" with it? Return not-support/not-implemented error kind if height is specified.

Comment on lines +26 to +28
CreateAccountCallRequired AccountStateEnum = "CreateAccountCallRequired"
Pending AccountStateEnum = "Pending"
Created AccountStateEnum = "Created"
Copy link
Copy Markdown
Contributor

@conorpp conorpp Apr 2, 2026

Choose a reason for hiding this comment

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

Use these values for consistency with other cordialsys apis:

inactive // CreateAccountCall required
registering
active

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.

2 participants