Skip to content

Commit 72a051e

Browse files
committed
feat: add pinata to cloud provider
1 parent 046282e commit 72a051e

File tree

5 files changed

+133
-0
lines changed

5 files changed

+133
-0
lines changed

config.example.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ api:
5454
cloud:
5555
gcpCredentialJSONPath: "./gcp.json"
5656
bucketName: "oracle-bucket-dev"
57+
pinataGatewayURL: "https://gateway.pinata.cloud"
58+
pinataJWT: ""
59+
5760

5861
oracle:
5962
updateInterval: 2m

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ require (
2929
github.com/sirupsen/logrus v1.9.3
3030
github.com/stretchr/testify v1.11.0
3131
github.com/swaggo/swag v1.16.6
32+
github.com/zde37/pinata-go-sdk v0.0.0-00010101000000-000000000000
3233
github.com/zenGate-Global/cardano-connector-go v0.2.1-0.20250811042717-4d037378a6df
3334
go.uber.org/automaxprocs v1.6.0
3435
go.uber.org/zap v1.27.0
@@ -177,4 +178,6 @@ require (
177178

178179
replace github.com/Salvionied/apollo => github.com/zenGate-Global/apollo v1.1.1-0.20250625074329-37f3a9174ddd
179180

181+
replace github.com/zde37/pinata-go-sdk => github.com/zenGate-Global/pinata-go-sdk v0.0.0-20240826130715-09fba9a6bf2e
182+
180183
replace github.com/maestro-org/go-sdk => github.com/mgpai22/maestro-cardano-go-sdk v0.0.0-20250808070843-b2b1302fb8b4

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ github.com/zenGate-Global/apollo v1.1.1-0.20250625074329-37f3a9174ddd h1:oYs02El
394394
github.com/zenGate-Global/apollo v1.1.1-0.20250625074329-37f3a9174ddd/go.mod h1:DEEos8tyVwA6CgJPedo8alDlI1QVZ+ovYRg+WyAaSVc=
395395
github.com/zenGate-Global/cardano-connector-go v0.2.1-0.20250811042717-4d037378a6df h1:6toWLdAFUiLnYRkz6GviCu5Kmq+V1+uyp4wycaQdmGk=
396396
github.com/zenGate-Global/cardano-connector-go v0.2.1-0.20250811042717-4d037378a6df/go.mod h1:QyFJ9GId/KNkF9SSFvpluc+OkFKJP8vXJVRA+7NzLf8=
397+
github.com/zenGate-Global/pinata-go-sdk v0.0.0-20240826130715-09fba9a6bf2e h1:vCBn7T1Y4bg4nFJMvbHfi3RXOQOOewahOEvo5lQFii0=
398+
github.com/zenGate-Global/pinata-go-sdk v0.0.0-20240826130715-09fba9a6bf2e/go.mod h1:DIWC7UQnfCXidPdndFJIBDe2512SpJUAJpPDP9+wkpU=
397399
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
398400
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
399401
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=

internal/cloud/pinata.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package cloud
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"mime/multipart"
8+
"net/http"
9+
10+
"github.com/google/uuid"
11+
"github.com/zde37/pinata-go-sdk/pinata"
12+
)
13+
14+
type PinataCloud struct {
15+
client *pinata.Client
16+
gatewayURL string
17+
}
18+
19+
var _ Cloud = (*PinataCloud)(nil)
20+
21+
func NewPinataCloud(jwt string, gatewayURL ...string) (*PinataCloud, error) {
22+
if jwt == "" {
23+
return nil, fmt.Errorf("pinata JWT cannot be empty")
24+
}
25+
26+
auth := pinata.NewAuthWithJWT(jwt)
27+
client := pinata.New(auth)
28+
29+
// ensure JWT is valid.
30+
if _, err := client.TestAuthentication(); err != nil {
31+
return nil, fmt.Errorf("pinata authentication failed: %w", err)
32+
}
33+
34+
gwURL := "https://gateway.pinata.cloud"
35+
if len(gatewayURL) > 0 && gatewayURL[0] != "" {
36+
gwURL = gatewayURL[0]
37+
}
38+
39+
return &PinataCloud{
40+
client: client,
41+
gatewayURL: gwURL,
42+
}, nil
43+
}
44+
45+
// Upload pins the given data to IPFS via Pinata and returns the content identifier (CID) as a Ref.
46+
func (p *PinataCloud) Upload(data []byte, contentType ...string) (Ref, error) {
47+
// We must use the file pinning endpoint to upload raw bytes without modification.
48+
// We build the multipart request body in memory to avoid writing a temporary file.
49+
body := &bytes.Buffer{}
50+
writer := multipart.NewWriter(body)
51+
52+
// Create a form file part with a random name.
53+
// Pinata doesn't use this filename for the CID.
54+
part, err := writer.CreateFormFile("file", uuid.New().String())
55+
if err != nil {
56+
return "", fmt.Errorf("failed to create form file: %w", err)
57+
}
58+
59+
// Copy the raw byte data into the form file part.
60+
if _, err = io.Copy(part, bytes.NewReader(data)); err != nil {
61+
return "", fmt.Errorf(
62+
"failed to copy data to multipart writer: %w",
63+
err,
64+
)
65+
}
66+
67+
if err = writer.Close(); err != nil {
68+
return "", fmt.Errorf("failed to close multipart writer: %w", err)
69+
}
70+
71+
// The Pinata SDK doesn't expose a method to pin raw bytes directly,
72+
// so we use the underlying requestBuilder to send the request.
73+
var response struct {
74+
IpfsHash string `json:"IpfsHash"`
75+
}
76+
77+
err = p.client.NewRequest(http.MethodPost, "/pinning/pinFileToIPFS").
78+
SetBody(body, writer.FormDataContentType()).
79+
Send(&response)
80+
81+
if err != nil {
82+
return "", fmt.Errorf("pinata API request failed: %w", err)
83+
}
84+
85+
if response.IpfsHash == "" {
86+
return "", fmt.Errorf("pinata API response did not include an IpfsHash")
87+
}
88+
89+
return Ref(response.IpfsHash), nil
90+
}
91+
92+
// Read fetches the data from the IPFS gateway corresponding to the Ref (CID).
93+
func (p *PinataCloud) Read(ref Ref) ([]byte, error) {
94+
if ref == "" {
95+
return nil, fmt.Errorf("ref (CID) cannot be empty")
96+
}
97+
98+
url := fmt.Sprintf("%s/ipfs/%s", p.gatewayURL, string(ref))
99+
100+
resp, err := http.Get(url)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to fetch from IPFS gateway: %w", err)
103+
}
104+
defer func() {
105+
if closeErr := resp.Body.Close(); closeErr != nil {
106+
fmt.Printf("Warning: failed to close response body: %v\n", closeErr)
107+
}
108+
}()
109+
110+
if resp.StatusCode != http.StatusOK {
111+
return nil, fmt.Errorf(
112+
"IPFS gateway returned non-200 status: %s",
113+
resp.Status,
114+
)
115+
}
116+
117+
data, err := io.ReadAll(resp.Body)
118+
if err != nil {
119+
return nil, fmt.Errorf("failed to read response body: %w", err)
120+
}
121+
122+
return data, nil
123+
}

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ type ApiConfig struct {
7373
type CloudConfig struct {
7474
GCPCredentialJSONPath string `yaml:"gcpCredentialJSONPath" envconfig:"GCP_CREDENTIAL_JSON_PATH"`
7575
BucketName string `yaml:"bucketName" envconfig:"BUCKET_NAME"`
76+
PinataGatewayURL string `yaml:"pinataGatewayURL" envconfig:"PINATA_GATEWAY_URL"`
77+
PinataJWT string `yaml:"pinataJWT" envconfig:"PINATA_JWT"`
7678
}
7779

7880
type Logging struct {

0 commit comments

Comments
 (0)