Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added cardano connector plugin #1636

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Changes from 18 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
bf9b6c4
Add WIP cardano support
SupernaviX May 30, 2024
0b47496
Fix WS connection to cardano connector
SupernaviX Jun 2, 2024
7b9e934
Pass-through FFI validation
SupernaviX Jun 2, 2024
fb574c6
Fix signature of cardano plugin
SupernaviX Oct 31, 2024
b9b766a
Update tags for cardano images
SupernaviX Oct 31, 2024
0238a58
feat: implement invoking cardano contracts
SupernaviX Nov 6, 2024
2e9baf2
feat: support DeployContract
SupernaviX Dec 6, 2024
67c8b1c
fix: pass namespace to HandleReceipt
SupernaviX Feb 6, 2025
ef68f8e
feat: support contract listeners
SupernaviX Feb 7, 2025
89c1a24
fix: correct linter error
SupernaviX Feb 8, 2025
1e5f445
fix: correct image paths
SupernaviX Feb 8, 2025
a95c7af
fix: correct copyright year
SupernaviX Feb 8, 2025
07f4dbd
fix: fix documentation
SupernaviX Feb 8, 2025
dba31c7
test: add coverage
SupernaviX Feb 10, 2025
89dd513
fix: use newer curl version
SupernaviX Feb 10, 2025
8a1372f
fix: use same curl version everywhere
SupernaviX Feb 10, 2025
e992174
fix: downgrade to older curl version everywhere
SupernaviX Feb 10, 2025
b4e295f
test: add test on invalid input to reach 100% coverage
SupernaviX Feb 10, 2025
1f4d2f9
feat: use different websockets for different namespaces
SupernaviX Feb 11, 2025
4efbdd8
feat: copy event signature logic to error signatures
SupernaviX Feb 11, 2025
581623f
fix: use proper i18n errors
SupernaviX Feb 11, 2025
77b0e82
Merge branch 'main' into cardano
SupernaviX Feb 20, 2025
d219791
fix: remove unneeded dockerfile change
SupernaviX Feb 20, 2025
0fbba39
Merge branch 'main' into cardano
SupernaviX Feb 24, 2025
85486a8
Merge branch 'main' into cardano
SupernaviX Feb 26, 2025
5bf7193
fix: correct OperationUpdate mock
SupernaviX Feb 26, 2025
3bd61a9
Merge branch 'main' into cardano
SupernaviX Mar 3, 2025
bc93388
Merge branch 'main' into cardano
SupernaviX Mar 7, 2025
96531e3
feat: remove fake websocket code from GetTransactionStatus
SupernaviX Mar 11, 2025
710b94e
feat: expect receipts to arrive in batches
SupernaviX Mar 11, 2025
1649d7c
fix: remove redundant Cardano in logs
SupernaviX Mar 11, 2025
3b39818
fix: correct copyright on new files
SupernaviX Mar 11, 2025
d22629b
Merge branch 'main' into cardano
SupernaviX Mar 12, 2025
976a0b1
fix: fix overflow warning
SupernaviX Mar 12, 2025
995e2bf
feat: update manifest images
SupernaviX Mar 13, 2025
0d3cc67
docs: add docs
SupernaviX Mar 13, 2025
a6d26e7
feat: support QueryContract
SupernaviX Mar 13, 2025
18e6eec
Merge branch 'main' into cardano
SupernaviX Mar 14, 2025
17bab49
fix: handle unchecked error
SupernaviX Mar 14, 2025
c177dcb
fix: pass full signature in listener
SupernaviX Mar 14, 2025
f424f72
feat: use synchronous bulk operation updates for receipt handling
SupernaviX Mar 15, 2025
34390c6
docs: document contract creation
SupernaviX Mar 15, 2025
6ef9ba7
fix: address captialization issues
SupernaviX Mar 15, 2025
e19609d
fix: update cardano images
SupernaviX Mar 15, 2025
0c170a7
fix: finish sentences in docs
SupernaviX Mar 17, 2025
725f706
fix: correct timing-out test
SupernaviX Mar 17, 2025
05bab1b
feat: update docs and connector images
SupernaviX Mar 21, 2025
7321ba9
Merge branch 'main' into cardano
SupernaviX Mar 21, 2025
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
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ ARG GIT_REF
RUN apk add make=4.4.1-r2 \
gcc=13.2.1_git20231014-r0 \
build-base=0.5-r3 \
curl=8.11.1-r0 \
curl=8.11.1-r1 \
git=2.43.6-r0
WORKDIR /firefly
RUN chgrp -R 0 /firefly \
@@ -64,7 +64,7 @@ RUN mkdir -p build/contracts \
FROM alpine:3.19 AS sbom
WORKDIR /
ADD . /SBOM
RUN apk add --no-cache curl
RUN apk add --no-cache curl=8.11.1-r1
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.48.3
RUN trivy fs --format spdx-json --output /sbom.spdx.json /SBOM
RUN trivy sbom /sbom.spdx.json --severity UNKNOWN,HIGH,CRITICAL --db-repository public.ecr.aws/aquasecurity/trivy-db --exit-code 1
@@ -76,7 +76,7 @@ ARG UI_RELEASE
RUN apk add --update --no-cache \
sqlite=3.44.2-r0 \
postgresql16-client=16.6-r0 \
curl=8.11.1-r0 \
curl=8.11.1-r1 \
jq=1.7.1-r0
WORKDIR /firefly
RUN chgrp -R 0 /firefly \
76 changes: 76 additions & 0 deletions doc-site/docs/reference/config.md
Original file line number Diff line number Diff line change
@@ -596,6 +596,82 @@ title: Configuration Reference
|name|The name of the configured Blockchain plugin|`string`|`<nil>`
|type|The type of the configured Blockchain Connector plugin|`string`|`<nil>`

## plugins.blockchain[].cardano.cardanoconnect

|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
|batchSize|The number of events Cardanoconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream|`int`|`50`
|batchTimeout|How long Cardanoconnect should wait for new events to arrive and fill a batch, before sending the batch to FireFly core. Only applies when automatically creating a new event stream|[`time.Duration`](https://pkg.go.dev/time#Duration)|`500`
|connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s`
|headers|Adds custom headers to HTTP requests|`map[string]string`|`<nil>`
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s`
|topic|The websocket listen topic that the node should register on, which is important if there are multiple nodes using a single cardanoconnect|`string`|`<nil>`
|url|The URL of the Cardanoconnect instance|URL `string`|`<nil>`

## plugins.blockchain[].cardano.cardanoconnect.auth

|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
|password|Password|`string`|`<nil>`
|username|Username|`string`|`<nil>`

## plugins.blockchain[].cardano.cardanoconnect.proxy

|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
|url|Optional HTTP proxy server to connect through|`string`|`<nil>`

## plugins.blockchain[].cardano.cardanoconnect.retry

|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
|count|The maximum number of times to retry|`int`|`5`
|enabled|Enables retries|`boolean`|`false`
|errorStatusCodeRegex|The regex that the error response status code must match to trigger retry|`string`|`<nil>`
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`

## plugins.blockchain[].cardano.cardanoconnect.throttle

|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|`<nil>`
|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|`<nil>`

## plugins.blockchain[].cardano.cardanoconnect.tls

|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|`<nil>`
|caFile|The path to the CA file for TLS on this API|`string`|`<nil>`
|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|`<nil>`
|certFile|The path to the certificate file for TLS on this API|`string`|`<nil>`
|clientAuth|Enables or disables client auth for TLS on this API|`string`|`<nil>`
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|`<nil>`
|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|`<nil>`
|keyFile|The path to the private key file for TLS on this API|`string`|`<nil>`
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|`<nil>`

## plugins.blockchain[].cardano.cardanoconnect.ws

|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
|connectionTimeout|The amount of time to wait while establishing a connection (or auto-reconnection)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`45s`
|heartbeatInterval|The amount of time to wait between heartbeat signals on the WebSocket connection|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
|initialConnectAttempts|The number of attempts FireFly will make to connect to the WebSocket when starting up, before failing|`int`|`5`
|path|The WebSocket sever URL to which FireFly should connect|WebSocket URL `string`|`<nil>`
|readBufferSize|The size in bytes of the read buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb`
|url|URL to use for WebSocket - overrides url one level up (in the HTTP config)|`string`|`<nil>`
|writeBufferSize|The size in bytes of the write buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb`

## plugins.blockchain[].ethereum.addressResolver

|Key|Description|Type|Default Value|
2 changes: 1 addition & 1 deletion doc-site/docs/reference/types/verifier.md
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ title: Verifier
| `hash` | Hash used as a globally consistent identifier for this namespace + type + value combination on every node in the network | `Bytes32` |
| `identity` | The UUID of the parent identity that has claimed this verifier | [`UUID`](simpletypes.md#uuid) |
| `namespace` | The namespace of the verifier | `string` |
| `type` | The type of the verifier | `FFEnum`:<br/>`"ethereum_address"`<br/>`"tezos_address"`<br/>`"fabric_msp_id"`<br/>`"dx_peer_id"` |
| `type` | The type of the verifier | `FFEnum`:<br/>`"cardano_address"`<br/>`"ethereum_address"`<br/>`"tezos_address"`<br/>`"fabric_msp_id"`<br/>`"dx_peer_id"` |
| `value` | The verifier string, such as an Ethereum address, or Fabric MSP identifier | `string` |
| `created` | The time this verifier was created on this node | [`FFTime`](simpletypes.md#fftime) |

18 changes: 18 additions & 0 deletions doc-site/docs/swagger/swagger.yaml
Original file line number Diff line number Diff line change
@@ -9396,6 +9396,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -10098,6 +10099,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -22859,6 +22861,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -23596,6 +23599,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -26379,6 +26383,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -26514,6 +26519,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -28617,6 +28623,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -35331,6 +35338,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -35400,6 +35408,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -35442,6 +35451,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -35461,6 +35471,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -35792,6 +35803,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -35920,6 +35932,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -37925,6 +37938,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -44421,6 +44435,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -44483,6 +44498,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -44518,6 +44534,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
@@ -44537,6 +44554,7 @@ paths:
type:
description: The type of the verifier
enum:
- cardano_address
- ethereum_address
- tezos_address
- fabric_msp_id
4 changes: 3 additions & 1 deletion internal/blockchain/bifactory/factory.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2025 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -21,6 +21,7 @@ import (

"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly/internal/blockchain/cardano"
"github.com/hyperledger/firefly/internal/blockchain/ethereum"
"github.com/hyperledger/firefly/internal/blockchain/fabric"
"github.com/hyperledger/firefly/internal/blockchain/tezos"
@@ -30,6 +31,7 @@ import (
)

var pluginsByType = map[string]func() blockchain.Plugin{
(*cardano.Cardano)(nil).Name(): func() blockchain.Plugin { return &cardano.Cardano{} },
(*ethereum.Ethereum)(nil).Name(): func() blockchain.Plugin { return &ethereum.Ethereum{} },
(*fabric.Fabric)(nil).Name(): func() blockchain.Plugin { return &fabric.Fabric{} },
(*tezos.Tezos)(nil).Name(): func() blockchain.Plugin { return &tezos.Tezos{} },
653 changes: 653 additions & 0 deletions internal/blockchain/cardano/cardano.go

Large diffs are not rendered by default.

1,530 changes: 1,530 additions & 0 deletions internal/blockchain/cardano/cardano_test.go

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions internal/blockchain/cardano/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright © 2025 Kaleido, Inc.
Copy link
Contributor

Choose a reason for hiding this comment

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

Update all copyrights to be the company you work for in the new files added

//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cardano

import (
"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/wsclient"
)

const (
defaultBatchSize = 50
defaultBatchTimeout = 500
)

const (
// CardanoconnectConfigKey is a sub-key in the config to contain all the cardanoconnect specific config
CardanoconnectConfigKey = "cardanoconnect"
// CardanoconnectConfigTopic is the websocket listen topic that the node should register on, which is important if there are multiple
// nodes using a single cardanoconnect
CardanoconnectConfigTopic = "topic"
// CardanoconnectConfigBatchSize is the batch size to configure on event streams, when auto-defining them
CardanoconnectConfigBatchSize = "batchSize"
// CardanoconnectConfigBatchTimeout is the batch timeout to configure on event streams, when auto-defining them
CardanoconnectConfigBatchTimeout = "batchTimeout"
)

func (c *Cardano) InitConfig(config config.Section) {
c.cardanoconnectConf = config.SubSection(CardanoconnectConfigKey)
wsclient.InitConfig(c.cardanoconnectConf)
c.cardanoconnectConf.AddKnownKey(CardanoconnectConfigTopic)
c.cardanoconnectConf.AddKnownKey(CardanoconnectConfigBatchSize, defaultBatchSize)
c.cardanoconnectConf.AddKnownKey(CardanoconnectConfigBatchTimeout, defaultBatchTimeout)
}
213 changes: 213 additions & 0 deletions internal/blockchain/cardano/eventstream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright © 2025 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cardano

import (
"context"
"fmt"

"github.com/go-resty/resty/v2"
"github.com/hyperledger/firefly-common/pkg/ffresty"
"github.com/hyperledger/firefly-common/pkg/log"
"github.com/hyperledger/firefly/internal/coremsgs"
)

type streamManager struct {
client *resty.Client
batchSize uint
batchTimeout uint
}

type eventStream struct {
ID string `json:"id"`
Name string `json:"name"`
ErrorHandling string `json:"errorHandling"`
BatchSize uint `json:"batchSize"`
BatchTimeoutMS uint `json:"batchTimeoutMS"`
Type string `json:"type"`
Timestamps bool `json:"timestamps"`
}

type listener struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
}

type filter struct {
Event eventfilter `json:"event"`
}

type eventfilter struct {
Contract string `json:"contract"`
EventPath string `json:"eventPath"`
}

func newStreamManager(client *resty.Client, batchSize, batchTimeout uint) *streamManager {
return &streamManager{
client: client,
batchSize: batchSize,
batchTimeout: batchTimeout,
}
}

func (s *streamManager) getEventStreams(ctx context.Context) (streams []*eventStream, err error) {
res, err := s.client.R().
SetContext(ctx).
SetResult(&streams).
Get("/eventstreams")
if err != nil || !res.IsSuccess() {
return nil, ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgCardanoconnectRESTErr)
}
return streams, nil
}

func buildEventStream(topic string, batchSize, batchTimeout uint) *eventStream {
return &eventStream{
Name: topic,
ErrorHandling: "block",
BatchSize: batchSize,
BatchTimeoutMS: batchTimeout,
Type: "websocket",
Timestamps: true,
}
}

func (s *streamManager) createEventStream(ctx context.Context, topic string) (*eventStream, error) {
stream := buildEventStream(topic, s.batchSize, s.batchTimeout)
res, err := s.client.R().
SetContext(ctx).
SetBody(stream).
SetResult(stream).
Post("/eventstreams")
if err != nil || !res.IsSuccess() {
return nil, ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgCardanoconnectRESTErr)
}
return stream, nil
}

func (s *streamManager) updateEventStream(ctx context.Context, topic string, batchSize, batchTimeout uint, eventStreamID string) (*eventStream, error) {
stream := buildEventStream(topic, batchSize, batchTimeout)
res, err := s.client.R().
SetContext(ctx).
SetBody(stream).
SetResult(stream).
Patch("/eventstreams/" + eventStreamID)
if err != nil || !res.IsSuccess() {
return nil, ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgCardanoconnectRESTErr)
}
return stream, nil
}

func (s *streamManager) ensureEventStream(ctx context.Context, topic string) (*eventStream, error) {
existingStreams, err := s.getEventStreams(ctx)
if err != nil {
return nil, err
}
for _, stream := range existingStreams {
if stream.Name == topic {
stream, err = s.updateEventStream(ctx, topic, s.batchSize, s.batchTimeout, stream.ID)
if err != nil {
return nil, err
}
return stream, nil
}
}
return s.createEventStream(ctx, topic)
}

func (s *streamManager) getListener(ctx context.Context, streamID string, listenerID string, okNotFound bool) (listener *listener, err error) {
res, err := s.client.R().
SetContext(ctx).
SetResult(&listener).
Get(fmt.Sprintf("/eventstreams/%s/listeners/%s", streamID, listenerID))
if err != nil || !res.IsSuccess() {
if okNotFound && res.StatusCode() == 404 {
return nil, nil
}
return nil, ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgCardanoconnectRESTErr)
}
return listener, nil
}

func (s *streamManager) getListeners(ctx context.Context, streamID string) (listeners *[]listener, err error) {
res, err := s.client.R().
SetContext(ctx).
SetResult(&listeners).
Get(fmt.Sprintf("/eventstreams/%s/listeners", streamID))
if err != nil || !res.IsSuccess() {
return nil, ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgCardanoconnectRESTErr)
}
return listeners, nil
}

func (s *streamManager) createListener(ctx context.Context, streamID, name, lastEvent string, filters []filter) (listener *listener, err error) {
body := map[string]interface{}{
"name": name,
"type": "events",
"fromBlock": lastEvent,
"filters": filters,
}

res, err := s.client.R().
SetContext(ctx).
SetBody(body).
SetResult(&listener).
Post(fmt.Sprintf("/eventstreams/%s/listeners", streamID))

if err != nil || !res.IsSuccess() {
return nil, ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgCardanoconnectRESTErr)
}

return listener, nil
}

func (s *streamManager) deleteListener(ctx context.Context, streamID, listenerID string) error {
res, err := s.client.R().
SetContext(ctx).
Delete(fmt.Sprintf("/eventstreams/%s/listeners/%s", streamID, listenerID))

if err != nil || !res.IsSuccess() {
return ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgCardanoconnectRESTErr)
}
return nil
}

func (s *streamManager) ensureFireFlyListener(ctx context.Context, namespace string, version int, address, firstEvent, streamID string) (l *listener, err error) {
existingListeners, err := s.getListeners(ctx, streamID)
if err != nil {
return nil, err
}

name := fmt.Sprintf("%s_%d_BatchPin", namespace, version)
for _, l := range *existingListeners {
if l.Name == name {
return &l, nil
}
}

filters := []filter{{
eventfilter{
Contract: address,
EventPath: "BatchPin",
},
}}
if l, err = s.createListener(ctx, streamID, name, firstEvent, filters); err != nil {
return nil, err
}
log.L(ctx).Infof("BatchPin subscription: %s", l.ID)
return l, nil
}
7 changes: 6 additions & 1 deletion internal/coremsgs/en_config_descriptions.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2024 Kaleido, Inc.
// Copyright © 2025 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -195,6 +195,11 @@ var (
ConfigPluginBlockchainTezosAddressResolverURL = ffc("config.plugins.blockchain[].tezos.addressResolver.url", "The URL of the Address Resolver", i18n.StringType)
ConfigPluginBlockchainTezosAddressResolverURLTemplate = ffc("config.plugins.blockchain[].tezos.addressResolver.urlTemplate", "The URL Go template string to use when calling the Address Resolver. The template input contains '.Key' and '.Intent' string variables.", i18n.GoTemplateType)

ConfigPluginBlockchainCardanoCardanoconnectBatchSize = ffc("config.plugins.blockchain[].cardano.cardanoconnect.batchSize", "The number of events Cardanoconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType)
ConfigPluginBlockchainCardanoCardanoconnectBatchTimeout = ffc("config.plugins.blockchain[].cardano.cardanoconnect.batchTimeout", "How long Cardanoconnect should wait for new events to arrive and fill a batch, before sending the batch to FireFly core. Only applies when automatically creating a new event stream", i18n.TimeDurationType)
ConfigPluginBlockchainCardanoCardanoconnectTopic = ffc("config.plugins.blockchain[].cardano.cardanoconnect.topic", "The websocket listen topic that the node should register on, which is important if there are multiple nodes using a single cardanoconnect", i18n.StringType)
ConfigPluginBlockchainCardanoCardanoconnectURL = ffc("config.plugins.blockchain[].cardano.cardanoconnect.url", "The URL of the Cardanoconnect instance", urlStringType)

ConfigPluginBlockchainTezosTezosconnectBackgroundStart = ffc("config.plugins.blockchain[].tezos.tezosconnect.backgroundStart.enabled", "Start the Tezosconnect plugin in the background and enter retry loop if failed to start", i18n.BooleanType)
ConfigPluginBlockchainTezosTezosconnectBackgroundStartInitialDelay = ffc("config.plugins.blockchain[].tezos.tezosconnect.backgroundStart.initialDelay", "Delay between restarts in the case where we retry to restart the tezos plugin", i18n.TimeDurationType)
ConfigPluginBlockchainTezosTezosconnectBackgroundStartMaxDelay = ffc("config.plugins.blockchain[].tezos.tezosconnect.backgroundStart.maxDelay", "Max delay between restarts in the case where we retry to restart the tezos plugin", i18n.TimeDurationType)
4 changes: 3 additions & 1 deletion internal/coremsgs/en_error_messages.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2024 Kaleido, Inc.
// Copyright © 2025 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -59,6 +59,7 @@ var (
MsgSerializationFailed = ffe("FF10137", "Serialization failed")
MsgMissingPluginConfig = ffe("FF10138", "Missing configuration '%s' for %s")
MsgMissingDataHashIndex = ffe("FF10139", "Missing data hash for index '%d' in message", 400)
MsgInvalidCardanoAddress = ffe("FF10140", "Supplied cardano address is invalid", 400)
MsgInvalidEthAddress = ffe("FF10141", "Supplied ethereum address is invalid", 400)
MsgInvalidTezosAddress = ffe("FF10142", "Supplied tezos address is invalid", 400)
Msg404NoResult = ffe("FF10143", "No result found", 404)
@@ -146,6 +147,7 @@ var (
MsgAuthorOrgSigningKeyMismatch = ffe("FF10279", "Author organization '%s' is not associated with signing key '%s'")
MsgCannotTransferToSelf = ffe("FF10280", "From and to addresses must be different", 400)
MsgLocalOrgNotSet = ffe("FF10281", "Unable to resolve the local root org. Please ensure org.name is configured", 500)
MsgCardanoconnectRESTErr = ffe("FF10282", "Error from cardano connector: %s")
MsgTezosconnectRESTErr = ffe("FF10283", "Error from tezos connector: %s")
MsgFabconnectRESTErr = ffe("FF10284", "Error from fabconnect: %s")
MsgInvalidIdentity = ffe("FF10285", "Supplied Fabric signer identity is invalid", 400)
13 changes: 12 additions & 1 deletion internal/networkmap/did.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2025 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -75,6 +75,8 @@ func (nm *networkMap) generateDIDDocument(ctx context.Context, identity *core.Id

func (nm *networkMap) generateDIDAuthentication(ctx context.Context, identity *core.Identity, verifier *core.Verifier) *VerificationMethod {
switch verifier.Type {
case core.VerifierTypeCardanoAddress:
return nm.generateCardanoAddressVerifier(identity, verifier)
case core.VerifierTypeEthAddress:
return nm.generateEthAddressVerifier(identity, verifier)
case core.VerifierTypeTezosAddress:
@@ -89,6 +91,15 @@ func (nm *networkMap) generateDIDAuthentication(ctx context.Context, identity *c
}
}

func (nm *networkMap) generateCardanoAddressVerifier(identity *core.Identity, verifier *core.Verifier) *VerificationMethod {
return &VerificationMethod{
ID: verifier.Hash.String(),
Type: "PaymentVerificationKeyShelley_ed25519", // hope that it's safe to assume we always use Shelley
Copy link
Contributor

Choose a reason for hiding this comment

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

What happenes when it's a script witness?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can that happen? I thought DID documents were for identities attached to firefly nodes, i.e. the wallets they could spend from.

Controller: identity.DID,
BlockchainAccountID: verifier.Value,
}
}

func (nm *networkMap) generateEthAddressVerifier(identity *core.Identity, verifier *core.Verifier) *VerificationMethod {
return &VerificationMethod{
ID: verifier.Hash.String(),
17 changes: 17 additions & 0 deletions internal/networkmap/did_test.go
Original file line number Diff line number Diff line change
@@ -34,6 +34,15 @@ func TestDIDGenerationOK(t *testing.T) {

org1 := testOrg("org1")

verifierCardano := (&core.Verifier{
Identity: org1.ID,
Namespace: org1.Namespace,
VerifierRef: core.VerifierRef{
Type: core.VerifierTypeCardanoAddress,
Value: "addr_test1vqhkukz0285zvk0xrwk9jlq0075tx6furuzcjvzpnhtgelsuhhqc4",
},
Created: fftypes.Now(),
})
verifierEth := (&core.Verifier{
Identity: org1.ID,
Namespace: org1.Namespace,
@@ -83,6 +92,7 @@ func TestDIDGenerationOK(t *testing.T) {
mdi := nm.database.(*databasemocks.Plugin)
mdi.On("GetIdentityByID", nm.ctx, "ns1", mock.Anything).Return(org1, nil)
mdi.On("GetVerifiers", nm.ctx, "ns1", mock.Anything).Return([]*core.Verifier{
verifierCardano,
verifierEth,
verifierTezos,
verifierMSP,
@@ -99,6 +109,12 @@ func TestDIDGenerationOK(t *testing.T) {
},
ID: org1.DID,
VerificationMethods: []*VerificationMethod{
{
ID: verifierCardano.Hash.String(),
Type: "PaymentVerificationKeyShelley_ed25519",
Controller: org1.DID,
BlockchainAccountID: verifierCardano.Value,
},
{
ID: verifierEth.Hash.String(),
Type: "EcdsaSecp256k1VerificationKey2019",
@@ -125,6 +141,7 @@ func TestDIDGenerationOK(t *testing.T) {
},
},
Authentication: []string{
fmt.Sprintf("#%s", verifierCardano.Hash.String()),
fmt.Sprintf("#%s", verifierEth.Hash.String()),
fmt.Sprintf("#%s", verifierTezos.Hash.String()),
fmt.Sprintf("#%s", verifierMSP.Hash.String()),
8 changes: 8 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{
"cardanoconnect": {
"image": "hyperledger/firefly-cardanoconnect",
"tag": "main"
},
"cardanosigner": {
"image": "hyperledger/firefly-cardanosigner",
"tag": "main"
},
"ethconnect": {
"image": "ghcr.io/hyperledger/firefly-ethconnect",
"tag": "v3.3.2",
4 changes: 3 additions & 1 deletion pkg/core/verifier.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2025 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -26,6 +26,8 @@ import (
type VerifierType = fftypes.FFEnum

var (
// VerifierTypeCardanoAddress is a Cardano address string
VerifierTypeCardanoAddress = fftypes.FFEnumValue("verifiertype", "cardano_address")
// VerifierTypeEthAddress is an Ethereum (secp256k1) address string
VerifierTypeEthAddress = fftypes.FFEnumValue("verifiertype", "ethereum_address")
// VerifierTypeTezosAddress is a Tezos (ed25519) address string