Skip to content
Merged
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
44 changes: 44 additions & 0 deletions src/app/rosetta/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Mina Rosetta integration examples

Runnable scripts demonstrating common integration patterns against Mina's [Rosetta (Mesh) API](https://docs.cdp.coinbase.com/mesh/docs/welcome) implementation.

## Prerequisites

A running Mina Rosetta endpoint. The fastest way is the Docker Compose stack in `../docker-compose/`:

```bash
cd ../docker-compose
make devnet
```

This exposes Rosetta at `http://localhost:3087`.

## Available examples

### TypeScript (`ts/`)

Uses [`@o1-labs/mina-rosetta-sdk`](https://www.npmjs.com/package/@o1-labs/mina-rosetta-sdk) for the typed Rosetta HTTP surface and [`mina-signer`](https://www.npmjs.com/package/mina-signer) for Pallas-curve transaction signing.

| Script | What it does |
| --- | --- |
| `account-balance.ts` | Query a single account balance (smoke test) |
| `scan-blocks.ts` | Poll `/network/status` and fetch new blocks as they arrive |
| `track-deposits.ts` | Watch an address for incoming MINA deposits |
| `send-transaction.ts` | Full Construction API flow: derive → preprocess → metadata → payloads → sign → combine → submit |
| `offline-sign.ts` | Same flow split for cold-signing setups: metadata online, signing offline |

See `ts/README.md` for setup and run instructions.

### Go (`go/`)

Read-side examples using [`coinbase/mesh-sdk-go`](https://github.com/coinbase/mesh-sdk-go) (the canonical Go SDK). Send-transaction is intentionally not included because Pallas signing has no pure-Go implementation today — see `go/README.md` for options.

| Program | What it does |
| --- | --- |
| `account-balance/` | Query a single account balance |
| `scan-blocks/` | Poll for new blocks via `fetcher` |
| `track-deposits/` | Filter `payment_receiver_inc` operations for an address |

## Adding examples in other languages

Drop a sibling directory (`py/`, `rust/`, etc.). Keep each language self-contained with its own `README.md` and follow the same pattern: thin client wrappers (or generated client over the [Mesh OpenAPI spec](https://github.com/coinbase/mesh-specifications)), one program per integration scenario, runnable against a live Rosetta endpoint.
35 changes: 35 additions & 0 deletions src/app/rosetta/examples/go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Go Rosetta examples

Read-side Rosetta integration examples in Go using [`coinbase/mesh-sdk-go`](https://github.com/coinbase/mesh-sdk-go) — the canonical Go SDK Coinbase ships and uses internally.

These examples are intentionally minimal. For Go, the upstream `mesh-sdk-go/examples/` directory already covers the generic Rosetta flow well. The point here is to show the **Mina-specific** bits (operation types, transfer layout, default token ID) wired up against `fetcher.New(...)`.

## Setup

```bash
cd src/app/rosetta/examples/go
go mod download
```

## Run

Each example is a separate `main` package. Pass configuration via environment variables (same names as the TypeScript examples):

```bash
export ROSETTA_URL=http://localhost:3087
export NETWORK=devnet
export TEST_ADDRESS=B62q...

go run ./account-balance
go run ./scan-blocks
go run ./track-deposits
```

## Why no send-transaction in Go

Mina uses the Pallas curve for signatures, and there is no pure-Go Pallas signer. To send transactions from Go you would either:

- Shell out to the [`mina-ocaml-signer`](../../../rosetta/ocaml-signer) CLI shipped with the Rosetta image, or
- Run the TypeScript [`offline-sign.ts`](../ts/offline-sign.ts) example for the signing step and submit from Go

For most exchange integrators this is a non-issue — signing usually happens in a separate cold-signing service. The Go examples here cover the read-side patterns (`/account/balance`, `/block`, `/network/status`) that real integrators run from their main service.
42 changes: 42 additions & 0 deletions src/app/rosetta/examples/go/account-balance/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Smoke test: fetch a single account's balance from Mina Rosetta.
package main

import (
"context"
"fmt"
"log"

"github.com/MinaProtocol/mina/src/app/rosetta/examples/go/internal/config"
"github.com/coinbase/rosetta-sdk-go/fetcher"
"github.com/coinbase/rosetta-sdk-go/types"
)

func main() {
address, err := config.Required("TEST_ADDRESS")
if err != nil {
log.Fatal(err)
}

env := config.Load()
f := fetcher.New(env.URL)

block, balances, _, ferr := f.AccountBalance(
context.Background(),
env.Network,
&types.AccountIdentifier{
Address: address,
Metadata: map[string]interface{}{"token_id": config.DefaultTokenID},
},
nil,
nil,
)
if ferr != nil {
log.Fatalf("AccountBalance: %s", ferr.Err)
}

fmt.Printf("Address: %s\n", address)
fmt.Printf("As of block %d (%s)\n", block.Index, block.Hash)
for _, b := range balances {
fmt.Printf(" %s %s (%d decimals)\n", b.Value, b.Currency.Symbol, b.Currency.Decimals)
}
}
5 changes: 5 additions & 0 deletions src/app/rosetta/examples/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/MinaProtocol/mina/src/app/rosetta/examples/go

go 1.21

require github.com/coinbase/rosetta-sdk-go v0.8.6
51 changes: 51 additions & 0 deletions src/app/rosetta/examples/go/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Package config centralizes the Mina-specific Rosetta knobs and shared env
// parsing the example programs need.
package config

import (
"fmt"
"os"

"github.com/coinbase/rosetta-sdk-go/types"
)

const (
Blockchain = "mina"
CurveType = types.Pallas
DefaultTokenID = "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"
DefaultURL = "http://localhost:3087"
DefaultNetwork = "devnet"
)

var MinaCurrency = &types.Currency{Symbol: "MINA", Decimals: 9}

// Env returns the example-script configuration sourced from environment
// variables. Defaults match the TypeScript examples.
type Env struct {
URL string
Network *types.NetworkIdentifier
}

func Load() *Env {
url := os.Getenv("ROSETTA_URL")
if url == "" {
url = DefaultURL
}
network := os.Getenv("NETWORK")
if network == "" {
network = DefaultNetwork
}
return &Env{
URL: url,
Network: &types.NetworkIdentifier{Blockchain: Blockchain, Network: network},
}
}

// Required reads a required env var, returning a friendly error if unset.
func Required(name string) (string, error) {
v := os.Getenv(name)
if v == "" {
return "", fmt.Errorf("missing env var: %s", name)
}
return v, nil
}
59 changes: 59 additions & 0 deletions src/app/rosetta/examples/go/scan-blocks/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Poll Mina Rosetta for new blocks and print transaction hashes.
package main

import (
"context"
"fmt"
"log"
"os"
"strconv"
"time"

"github.com/MinaProtocol/mina/src/app/rosetta/examples/go/internal/config"
"github.com/coinbase/rosetta-sdk-go/fetcher"
"github.com/coinbase/rosetta-sdk-go/types"
)

const pollInterval = 10 * time.Second

func main() {
env := config.Load()
f := fetcher.New(env.URL)
ctx := context.Background()

height := startHeight(ctx, f, env.Network)
fmt.Printf("Scanning from block %d\n", height)

for {
block, ferr := f.Block(ctx, env.Network, &types.PartialBlockIdentifier{Index: &height})
if ferr != nil {
log.Fatalf("Block %d: %s", height, ferr.Err)
}
if block == nil {
time.Sleep(pollInterval)
continue
}

fmt.Printf("[%s] block %d (%s) — %d tx\n",
time.UnixMilli(block.Timestamp).UTC().Format(time.RFC3339),
block.BlockIdentifier.Index, block.BlockIdentifier.Hash,
len(block.Transactions))
for _, tx := range block.Transactions {
fmt.Printf(" %s\n", tx.TransactionIdentifier.Hash)
}
height++
}
}

func startHeight(ctx context.Context, f *fetcher.Fetcher, network *types.NetworkIdentifier) int64 {
if v := os.Getenv("START_HEIGHT"); v != "" {
if h, err := strconv.ParseInt(v, 10, 64); err == nil && h > 0 {
return h
}
}
status, ferr := f.NetworkStatus(ctx, network, nil)
if ferr != nil {
log.Fatalf("NetworkStatus: %s", ferr.Err)
}
return status.CurrentBlockIdentifier.Index
}
100 changes: 100 additions & 0 deletions src/app/rosetta/examples/go/track-deposits/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Watch a Mina address for incoming MINA deposits.
package main

import (
"context"
"fmt"
"log"
"os"
"strconv"
"time"

"github.com/MinaProtocol/mina/src/app/rosetta/examples/go/internal/config"
"github.com/coinbase/rosetta-sdk-go/fetcher"
"github.com/coinbase/rosetta-sdk-go/types"
)

const (
pollInterval = 10 * time.Second
depositOp = "payment_receiver_inc"
)

func main() {
address, err := config.Required("TEST_ADDRESS")
if err != nil {
log.Fatal(err)
}

env := config.Load()
f := fetcher.New(env.URL)
ctx := context.Background()

height := startHeight(ctx, f, env.Network)
fmt.Printf("Watching %s for deposits starting at block %d\n", address, height)

for {
block, ferr := f.Block(ctx, env.Network, &types.PartialBlockIdentifier{Index: &height})
if ferr != nil {
log.Fatalf("Block %d: %s", height, ferr.Err)
}
if block == nil {
time.Sleep(pollInterval)
continue
}

for _, deposit := range findDeposits(block, address) {
fmt.Printf("DEPOSIT block=%d tx=%s amount=%s nanomina\n",
deposit.height, deposit.txHash, deposit.amount)
}
height++
}
}

type deposit struct {
height int64
txHash string
amount string
}

func findDeposits(block *types.Block, address string) []deposit {
var out []deposit
for _, tx := range block.Transactions {
for _, op := range tx.Operations {
if !isDeposit(op, address) {
continue
}
out = append(out, deposit{
height: block.BlockIdentifier.Index,
txHash: tx.TransactionIdentifier.Hash,
amount: op.Amount.Value,
})
}
}
return out
}

func isDeposit(op *types.Operation, address string) bool {
if op.Type != depositOp {
return false
}
if op.Status != nil && *op.Status == "Failed" {
return false
}
if op.Account == nil || op.Account.Address != address {
return false
}
return op.Amount != nil
}

func startHeight(ctx context.Context, f *fetcher.Fetcher, network *types.NetworkIdentifier) int64 {
if v := os.Getenv("START_HEIGHT"); v != "" {
if h, err := strconv.ParseInt(v, 10, 64); err == nil && h > 0 {
return h
}
}
status, ferr := f.NetworkStatus(ctx, network, nil)
if ferr != nil {
log.Fatalf("NetworkStatus: %s", ferr.Err)
}
return status.CurrentBlockIdentifier.Index
}
5 changes: 5 additions & 0 deletions src/app/rosetta/examples/ts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
.env
dist/
*.log
.cold-signing/
Loading