diff --git a/.gitignore b/.gitignore index 41b70c9..de58d41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -/basin +/vaults /cover.out .vscode/* diff --git a/Makefile b/Makefile index 6e96c4d..f45d4ab 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ lint: # Build build: - go build -o basin cmd/basin/* + go build -o vaults cmd/vaults/* .PHONY: build # Test diff --git a/README.md b/README.md index 0ade378..f1accd9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# basin-cli +# vaults-cli [![License](https://img.shields.io/github/license/tablelandnetwork/basin-cli.svg)](./LICENSE) [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg)](https://github.com/RichardLitt/standard-readme) @@ -12,20 +12,20 @@ - [Self-hosted](#self-hosted) - [Amazon RDS](#amazon-rds) - [Supabase](#supabase) -- [Create a publication](#create-a-publication) -- [Start replicating a publication](#start-replicating-a-publication) +- [Create a vault](#create-a-vault) +- [Start replicating a database](#start-replicating-a-database) - [Upload a Parquet file](#upload-a-parquet-file) -- [Listing Publications](#listing-publications) -- [Listing Deals](#listing-deals) +- [Listing Vaults](#listing-vaults) +- [Listing Events](#listing-events) - [Running](#running) - [Run tests](#run-tests) - [Retrieving](#retrieving) # Background -Tableland Basin is a secure and verifiable open data platform. The Basin CLI is a tool that allows you to continuously replicate a table or view from your database to the network. Currently, only PostgreSQL is supported. +Textile Vaults is a secure and verifiable open data platform. The Vaults CLI is a tool that allows you to continuously replicate a table or view from your database to the network. Currently, only PostgreSQL is supported. -> 🚧 Basin is currently not in a production-ready state. Any data that is pushed to the network may be subject to deletion. 🚧 +> 🚧 Vaults is currently not in a production-ready state. Any data that is pushed to the network may be subject to deletion. 🚧 # Usage @@ -34,14 +34,14 @@ Tableland Basin is a secure and verifiable open data platform. The Basin CLI is ```bash git clone https://github.com/tablelandnetwork/basin-cli.git cd basin-cli -go install ./cmd/basin +go install ./cmd/vaults ``` ## Postgres Setup ### Self-hosted -- Make sure you have access to a superuser role. For example, you can create a new role such as `CREATE ROLE basin WITH PASSWORD NULL LOGIN SUPERUSER;`. +- Make sure you have access to a superuser role. For example, you can create a new role such as `CREATE ROLE vaults WITH PASSWORD NULL LOGIN SUPERUSER;`. - Check that your Postgres installation has the [wal2json](https://github.com/eulerto/wal2json) plugin installed. - Check if logical replication is enabled: @@ -69,7 +69,7 @@ go install ./cmd/basin WHERE name = 'rds.logical_replication'; ``` -- If it's on, go to [Create a publication](#create-a-publication) +- If it's on, go to [Create a vault](#create-a-vault) - If it's off, follow the next steps: - [Create a custom RDS parameter group](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithDBInstanceParamGroups.html#USER_WorkingWithParamGroups.Creating) - After creation, edit it and set the `rds.logical_replication` parameter to `1` @@ -83,92 +83,92 @@ go install ./cmd/basin - Log into the [Supabase](https://supabase.io/) dashboard and go to your project, or create a new one. - Check if logical replication is enabled. This should be the default setting, so you shouldn't have to change anything. You can do this in the `SQL Editor` section on the left hand side of the Supabase dashboard by running `SHOW wal_level;` query, which should log `logical`. - You can find the database connection information on the left hand side under `Project Settings` > `Database`. You will need the `Host`, `Port`, `Database`, `Username`, and `Password` to connect to your database. - - When you create a publication, the `--dburi` should follow this format: + - When you create a vault, the `--dburi` should follow this format: ```sh postgresql://postgres:[PASSWORD]@db.[PROJECT_ID].supabase.co:5432/postgres ``` -## Create a publication +## Create a vault -_Publications_ define the data you are pushing to Basin. +_Vaults_ define the place you push data into. -Basin uses public key authentication, so you will need an Ethereum style (ECDSA, secp256k1) wallet to create a new publication. You can use an existing wallet or set up a new one with `basin wallet create`. Your private key is only used locally for signing. +Vaults uses public key authentication, so you will need an Ethereum style (ECDSA, secp256k1) wallet to create a new vault. You can use an existing wallet or set up a new one with `vaults wallet create`. Your private key is only used locally for signing. ```bash -basin wallet create [FILENAME] +vaults wallet create [FILENAME] ``` A new private key will be written to `FILENAME`. -The name of a publication contains a `namespace` (e.g. `my_company`) and the name of an existing database relation (e.g. `my_table`), separated by a period (`.`). Use `basin publication create` to create a new publication. See `basin publication create --help` for more info. +The name of a vault contains a `namespace` (e.g. `my_company`) and the name of an existing database relation (e.g. `my_table`), separated by a period (`.`). Use `vaults create` to create a new vault. See `vaults create --help` for more info. ```bash -basin publication create --dburi [DBURI] --address [WALLET_ADDRESS] namespace.relation_name +vaults create --dburi [DBURI] --address [WALLET_ADDRESS] namespace.relation_name ``` -🚧 Basin currently only replicates `INSERT` statements, which means that it only replicates append-only data (e.g., log-style data). Row updates and deletes will be ignored. 🚧 +🚧 Vaults currently only replicates `INSERT` statements, which means that it only replicates append-only data (e.g., log-style data). Row updates and deletes will be ignored. 🚧 -## Start replicating a publication +## Start replicating a database -Use `basin publication start` to start a daemon that will continuously push changes to the underlying table/view to the network. See `basin publication start --help` for more info. +Use `vaults stream` to start a daemon that will continuously push changes to the underlying table/view to the network. See `vaults stream --help` for more info. ```bash -basin publication start --private-key [PRIVATE_KEY] namespace.relation_name +vaults stream --private-key [PRIVATE_KEY] namespace.relation_name ``` ## Upload a Parquet file -Before uploading a Parquet file, you need to [Create a publication](#create-a-publication), if not already created. You can omit the `--dburi` flag, in this case. +Before uploading a Parquet file, you need to [Create a vault](#create-a-vault), if not already created. You can omit the `--dburi` flag, in this case. -Then, use `basin publication upload` to upload a Parquet file. +Then, use `vaults write` to write a Parquet file. ```bash -basin publication upload --name [namespace.relation_name] --private-key [PRIVATE_KEY] filepath +vaults write --vault [namespace.relation_name] --private-key [PRIVATE_KEY] filepath ``` You can attach a timestamp to that file upload, e.g. ```bash -basin publication upload --name [namespace.relation_name] --private-key [PRIVATE_KEY] --timestamp 1699984703 filepath +vaults upload --vault [namespace.relation_name] --private-key [PRIVATE_KEY] --timestamp 1699984703 filepath # or use data format -basin publication upload --name [namespace.relation_name] --private-key [PRIVATE_KEY] --timestamp 2006-01-02 filepath +vaults upload --vault [namespace.relation_name] --private-key [PRIVATE_KEY] --timestamp 2006-01-02 filepath # or use RFC3339 format -basin publication upload --name [namespace.relation_name] --private-key [PRIVATE_KEY] --timestamp 2006-01-02T15:04:05Z07:00 filepath +vaults upload --vault [namespace.relation_name] --private-key [PRIVATE_KEY] --timestamp 2006-01-02T15:04:05Z07:00 filepath ``` If a timestamp is not provided, the CLI will assume the timestamp is the current client epoch in UTC. -## Listing Publications +## Listing Vaults -You can list the publications from an address by running: +You can list the vaults from an account by running: ```bash -basin publication list --address [ETH_ADDRESS] +vaults list --account [ETH_ADDRESS] ``` -## Listing Deals +## Listing Events -You can list deals of a given publication by running: +You can list events of a given vault by running: ```bash -basin publication deals --publication [PUBLICATION] --latest 5 +vaults events --vault [VAULT_NAME] --latest 5 ``` -Deals command accept `--before`,`--after` , and `--at` flags to filter deals by timestamp +Events command accept `--before`,`--after` , and `--at` flags to filter events by timestamp ```bash # examples -basin publication deals --publication demotest.data --at 1699569502 -basin publication deals --publication demotest.data --before 2023-11-09T19:38:23-03:00 -basin publication deals --publication demotest.data --after 2023-11-09 +vaults events --vault demotest.data --at 1699569502 +vaults events --vault demotest.data --before 2023-11-09T19:38:23-03:00 +vaults events --vault demotest.data --after 2023-11-09 ``` ## Retrieving ```bash -basin publication retrieve bafybeifr5njnrw67yyb2h2t7k6ukm3pml4fgphsxeurqcmgmeb7omc2vlq +vaults retrieve bafybeifr5njnrw67yyb2h2t7k6ukm3pml4fgphsxeurqcmgmeb7omc2vlq ``` # Development @@ -185,7 +185,7 @@ PORT=8888 ./scripts/server.sh ./scripts/run.sh wallet create pk.out # Start replicating -./scripts/run.sh publication start --private-key [PRIVATE_KEY] namespace.relation_name +./scripts/run.sh vaults stream --private-key [PRIVATE_KEY] namespace.relation_name ``` ## Run tests diff --git a/cmd/basin/publication_test.go b/cmd/basin/publication_test.go deleted file mode 100644 index 655cdbf..0000000 --- a/cmd/basin/publication_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "context" - "crypto/ecdsa" - "database/sql" - "encoding/hex" - "fmt" - "testing" - "time" - - "github.com/ethereum/go-ethereum/crypto" - _ "github.com/lib/pq" - "github.com/stretchr/testify/require" - "github.com/tablelandnetwork/basin-cli/test" - "github.com/urfave/cli/v2" -) - -func TestPublication(t *testing.T) { - // This is supposed to be the closest to an e2E test we get - // skipping this for now until I think of a way to assert the data that went to Basin Provider - t.Skip() - - t.Parallel() - - pool := test.GetDockerPool() - - db, resource, dburi := pool.RunPostgres() - - cliApp := &cli.App{ - Name: "basin", - Usage: "basin replicates your database as logs and store them in Filecoin", - Commands: []*cli.Command{ - newPublicationCommand(), - }, - } - - h, err := newHelper(cliApp, db, dburi, t.TempDir()) - require.NoError(t, err) - - h.CreateTable(t) - h.CreatePublication(t) - - go func() { - h.StartReplication(t) - }() - - time.Sleep(1 * time.Second) - - h.AddNewRecord(t) - h.AddNewRecord(t) - - pool.Purge(resource) -} - -type helper struct { - app *cli.App - db *sql.DB - dburi string - pk *ecdsa.PrivateKey - ns string - rel string - dir string -} - -func newHelper(app *cli.App, db *sql.DB, dburi string, dir string) (*helper, error) { - pk, err := crypto.GenerateKey() - if err != nil { - return nil, err - } - - return &helper{ - pk: pk, - ns: "n", - rel: "t", - dir: dir, - - app: app, - db: db, - dburi: dburi, - }, nil -} - -func (h *helper) CreateTable(t *testing.T) { - _, err := h.db.ExecContext( - context.Background(), - fmt.Sprintf("create table %s(id serial primary key, name text);", h.rel), - ) - require.NoError(t, err) -} - -func (h *helper) CreatePublication(t *testing.T) { - name := fmt.Sprintf("%s.%s", h.ns, h.rel) - err := h.app.Run([]string{ - "basin", - "publication", - "--dir", h.dir, - "create", - "--dburi", h.dburi, - "--address", crypto.PubkeyToAddress(h.pk.PublicKey).Hex(), - "--provider", "mock", - name, - }) - require.NoError(t, err) -} - -func (h *helper) StartReplication(t *testing.T) { - ctx := context.Background() - name := fmt.Sprintf("%s.%s", h.ns, h.rel) - err := h.app.RunContext(ctx, []string{ - "basin", - "publication", - "--dir", h.dir, - "start", - "--private-key", hex.EncodeToString(crypto.FromECDSA(h.pk)), - name, - }) - require.NoError(t, err) -} - -func (h *helper) AddNewRecord(t *testing.T) { - _, err := h.db.ExecContext(context.Background(), fmt.Sprintf("insert into %s (name) values ('foo');", h.rel)) - require.NoError(t, err) -} diff --git a/cmd/basin/wallet.go b/cmd/basin/wallet.go deleted file mode 100644 index da70df1..0000000 --- a/cmd/basin/wallet.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "crypto/ecdsa" - "errors" - "fmt" - "os" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" - "github.com/urfave/cli/v2" -) - -func newWalletCommand() *cli.Command { - return &cli.Command{ - Name: "wallet", - Usage: "wallet commands", - Subcommands: []*cli.Command{ - { - Name: "create", - Usage: "creates a new wallet", - Action: func(cCtx *cli.Context) error { - filename := cCtx.Args().Get(0) - if filename == "" { - return errors.New("filename is empty") - } - - privateKey, err := crypto.GenerateKey() - if err != nil { - return fmt.Errorf("generate key: %s", err) - } - privateKeyBytes := crypto.FromECDSA(privateKey) - - if err := os.WriteFile(filename, []byte(hexutil.Encode(privateKeyBytes)[2:]), 0o644); err != nil { - return fmt.Errorf("writing to file %s: %s", filename, err) - } - pubk, _ := privateKey.Public().(*ecdsa.PublicKey) - publicKey := common.HexToAddress(crypto.PubkeyToAddress(*pubk).Hex()) - - fmt.Printf("Wallet address %s created\n", publicKey) - fmt.Printf("Private key saved in %s\n", filename) - return nil - }, - }, - { - Name: "pubkey", - Usage: "print the public key for a private key", - Action: func(cCtx *cli.Context) error { - filename := cCtx.Args().Get(0) - if filename == "" { - return errors.New("filename is empty") - } - - privateKey, err := crypto.LoadECDSA(filename) - if err != nil { - return fmt.Errorf("loading key: %s", err) - } - - pubk, _ := privateKey.Public().(*ecdsa.PublicKey) - publicKey := common.HexToAddress(crypto.PubkeyToAddress(*pubk).Hex()) - - fmt.Println(publicKey) - return nil - }, - }, - }, - } -} diff --git a/cmd/basin/publication.go b/cmd/vaults/commands.go similarity index 78% rename from cmd/basin/publication.go rename to cmd/vaults/commands.go index 6cfacfc..bebad8a 100644 --- a/cmd/basin/publication.go +++ b/cmd/vaults/commands.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/ecdsa" "encoding/json" "errors" "fmt" @@ -12,6 +13,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/filecoin-project/lassie/pkg/lassie" "github.com/filecoin-project/lassie/pkg/storage" @@ -25,42 +27,21 @@ import ( "github.com/olekukonko/tablewriter" "github.com/schollz/progressbar/v3" "github.com/tablelandnetwork/basin-cli/internal/app" - "github.com/tablelandnetwork/basin-cli/pkg/basinprovider" "github.com/tablelandnetwork/basin-cli/pkg/pgrepl" + "github.com/tablelandnetwork/basin-cli/pkg/vaultsprovider" "github.com/urfave/cli/v2" "gopkg.in/yaml.v3" ) var pubNameRx = regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_]*)[.]([a-zA-Z_][a-zA-Z0-9_]*$)`) -func newPublicationCommand() *cli.Command { - return &cli.Command{ - Name: "publication", - Usage: "publication commands", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "dir", - Usage: "The directory where config will be stored (default: $HOME)", - }, - }, - Subcommands: []*cli.Command{ - newPublicationCreateCommand(), - newPublicationStartCommand(), - newPublicationUploadCommand(), - newPublicationListCommand(), - newPublicationDealsCommand(), - newPublicationRetrieveCommand(), - }, - } -} - -func newPublicationCreateCommand() *cli.Command { +func newVaultCreateCommand() *cli.Command { var owner, dburi, provider string var winSize, cache int64 return &cli.Command{ Name: "create", - Usage: "create a new publication", + Usage: "create a new vault", Flags: []cli.Flag{ &cli.StringFlag{ Name: "address", @@ -98,7 +79,7 @@ func newPublicationCreateCommand() *cli.Command { } pub := cCtx.Args().First() - ns, rel, err := parsePublicationName(pub) + ns, rel, err := parseVaultName(pub) if err != nil { return err } @@ -144,13 +125,13 @@ func newPublicationCreateCommand() *cli.Command { return fmt.Errorf("encode: %s", err) } - exists, err := createPublication(cCtx.Context, dburi, ns, rel, provider, owner, cache) + exists, err := createVault(cCtx.Context, dburi, ns, rel, provider, owner, cache) if err != nil { - return fmt.Errorf("failed to create publication: %s", err) + return fmt.Errorf("failed to create vault: %s", err) } if exists { - fmt.Printf("Publication %s.%s already exists.\n\n", ns, rel) + fmt.Printf("Vault %s.%s already exists.\n\n", ns, rel) return nil } @@ -158,18 +139,18 @@ func newPublicationCreateCommand() *cli.Command { return fmt.Errorf("mk db dir: %s", err) } - fmt.Printf("\033[32mPublication %s.%s created.\033[0m\n\n", ns, rel) + fmt.Printf("\033[32mVault %s.%s created.\033[0m\n\n", ns, rel) return nil }, } } -func newPublicationStartCommand() *cli.Command { +func newStreamCommand() *cli.Command { var privateKey string return &cli.Command{ - Name: "start", - Usage: "start a daemon process that replicates Postgres changes to Basin server", + Name: "stream", + Usage: "starts a daemon process that streams Postgres changes to a vault", Flags: []cli.Flag{ &cli.StringFlag{ Name: "private-key", @@ -183,8 +164,8 @@ func newPublicationStartCommand() *cli.Command { return errors.New("one argument should be provided") } - publication := cCtx.Args().First() - ns, rel, err := parsePublicationName(publication) + vault := cCtx.Args().First() + ns, rel, err := parseVaultName(vault) if err != nil { return err } @@ -200,11 +181,11 @@ func newPublicationStartCommand() *cli.Command { } connString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s", - cfg.Publications[publication].User, - cfg.Publications[publication].Password, - cfg.Publications[publication].Host, - cfg.Publications[publication].Port, - cfg.Publications[publication].Database, + cfg.Publications[vault].User, + cfg.Publications[vault].Password, + cfg.Publications[vault].Host, + cfg.Publications[vault].Port, + cfg.Publications[vault].Database, ) r, err := pgrepl.New(connString, pgrepl.Publication(rel)) @@ -217,7 +198,7 @@ func newPublicationStartCommand() *cli.Command { return err } - bp := basinprovider.New(cfg.Publications[publication].ProviderHost) + bp := vaultsprovider.New(cfg.Publications[vault].ProviderHost) pgxConn, err := pgx.Connect(cCtx.Context, connString) if err != nil { @@ -243,9 +224,9 @@ func newPublicationStartCommand() *cli.Command { } // Creates a new db manager when replication starts - dbDir := path.Join(dir, publication) - winSize := time.Duration(cfg.Publications[publication].WindowSize) * time.Second - uploader := app.NewBasinUploader(ns, rel, bp, privateKey) + dbDir := path.Join(dir, vault) + winSize := time.Duration(cfg.Publications[vault].WindowSize) * time.Second + uploader := app.NewVaultsUploader(ns, rel, bp, privateKey) dbm := app.NewDBManager(dbDir, rel, cols, winSize, uploader) // Before starting replication, upload the remaining data @@ -253,8 +234,8 @@ func newPublicationStartCommand() *cli.Command { return fmt.Errorf("upload all: %s", err) } - basinStreamer := app.NewBasinStreamer(ns, r, dbm) - if err := basinStreamer.Run(cCtx.Context); err != nil { + vaultsStreamer := app.NewVaultsStreamer(ns, r, dbm) + if err := vaultsStreamer.Run(cCtx.Context); err != nil { return fmt.Errorf("run: %s", err) } @@ -263,13 +244,13 @@ func newPublicationStartCommand() *cli.Command { } } -func newPublicationUploadCommand() *cli.Command { - var privateKey, publicationName string +func newWriteCommand() *cli.Command { + var privateKey, vaultName string var timestamp string return &cli.Command{ - Name: "upload", - Usage: "upload a Parquet file", + Name: "write", + Usage: "write a Parquet file", Flags: []cli.Flag{ &cli.StringFlag{ Name: "private-key", @@ -278,9 +259,9 @@ func newPublicationUploadCommand() *cli.Command { Required: true, }, &cli.StringFlag{ - Name: "name", - Usage: "Publication name", - Destination: &publicationName, + Name: "vault", + Usage: "Vault name", + Destination: &vaultName, Required: true, }, &cli.StringFlag{ @@ -293,7 +274,7 @@ func newPublicationUploadCommand() *cli.Command { if cCtx.NArg() != 1 { return errors.New("one argument should be provided") } - ns, rel, err := parsePublicationName(publicationName) + ns, rel, err := parseVaultName(vaultName) if err != nil { return err } @@ -313,7 +294,7 @@ func newPublicationUploadCommand() *cli.Command { return fmt.Errorf("load config: %s", err) } - bp := basinprovider.New(cfg.Publications[publicationName].ProviderHost) + bp := vaultsprovider.New(cfg.Publications[vaultName].ProviderHost) filepath := cCtx.Args().First() @@ -332,7 +313,7 @@ func newPublicationUploadCommand() *cli.Command { bar := progressbar.DefaultBytes( fi.Size(), - "Uploading file...", + "Writing...", ) if timestamp == "" { @@ -344,8 +325,8 @@ func newPublicationUploadCommand() *cli.Command { return err } - basinStreamer := app.NewBasinUploader(ns, rel, bp, privateKey) - if err := basinStreamer.Upload(cCtx.Context, filepath, bar, ts, fi.Size()); err != nil { + vaultsStreamer := app.NewVaultsUploader(ns, rel, bp, privateKey) + if err := vaultsStreamer.Upload(cCtx.Context, filepath, bar, ts, fi.Size()); err != nil { return fmt.Errorf("upload: %s", err) } @@ -354,17 +335,17 @@ func newPublicationUploadCommand() *cli.Command { } } -func newPublicationListCommand() *cli.Command { - var owner, provider string +func newListCommand() *cli.Command { + var address, provider string return &cli.Command{ Name: "list", - Usage: "list publications of a given address", + Usage: "list vaults of a given account", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "address", + Name: "account", Usage: "Ethereum wallet address", - Destination: &owner, + Destination: &address, Required: true, }, &cli.StringFlag{ @@ -375,15 +356,15 @@ func newPublicationListCommand() *cli.Command { }, }, Action: func(cCtx *cli.Context) error { - account, err := app.NewAccount(owner) + account, err := app.NewAccount(address) if err != nil { - return fmt.Errorf("%s is not a valid Ethereum wallet address", owner) + return fmt.Errorf("%s is not a valid Ethereum wallet address", address) } - bp := basinprovider.New(provider) + bp := vaultsprovider.New(provider) vaults, err := bp.ListVaults(cCtx.Context, app.ListVaultsParams{Account: account}) if err != nil { - return fmt.Errorf("failed to list publications: %s", err) + return fmt.Errorf("failed to list vaults: %s", err) } for _, vault := range vaults { @@ -395,18 +376,18 @@ func newPublicationListCommand() *cli.Command { } } -func newPublicationDealsCommand() *cli.Command { - var publication, provider, before, after, at, format string +func newListEventsCommand() *cli.Command { + var vault, provider, before, after, at, format string var limit, offset, latest int return &cli.Command{ - Name: "deals", - Usage: "list deals of a given publications", + Name: "events", + Usage: "list events of a given vault", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "publication", - Usage: "Publication name", - Destination: &publication, + Name: "vault", + Usage: "vault name", + Destination: &vault, Required: true, }, &cli.StringFlag{ @@ -458,12 +439,12 @@ func newPublicationDealsCommand() *cli.Command { }, }, Action: func(cCtx *cli.Context) error { - ns, rel, err := parsePublicationName(publication) + ns, rel, err := parseVaultName(vault) if err != nil { return err } - bp := basinprovider.New(provider) + bp := vaultsprovider.New(provider) b, a, err := validateBeforeAndAfter(before, after, at) if err != nil { @@ -534,10 +515,10 @@ func newPublicationDealsCommand() *cli.Command { } } -func newPublicationRetrieveCommand() *cli.Command { +func newRetrieveCommand() *cli.Command { return &cli.Command{ Name: "retrieve", - Usage: "Retrieve files by CID", + Usage: "Retrieve an event by CID", Action: func(cCtx *cli.Context) error { arg := cCtx.Args().Get(0) if arg == "" { @@ -584,10 +565,66 @@ func newPublicationRetrieveCommand() *cli.Command { } } -func parsePublicationName(name string) (ns string, rel string, err error) { +func newWalletCommand() *cli.Command { + return &cli.Command{ + Name: "wallet", + Usage: "wallet commands", + Subcommands: []*cli.Command{ + { + Name: "create", + Usage: "creates a new wallet", + Action: func(cCtx *cli.Context) error { + filename := cCtx.Args().Get(0) + if filename == "" { + return errors.New("filename is empty") + } + + privateKey, err := crypto.GenerateKey() + if err != nil { + return fmt.Errorf("generate key: %s", err) + } + privateKeyBytes := crypto.FromECDSA(privateKey) + + if err := os.WriteFile(filename, []byte(hexutil.Encode(privateKeyBytes)[2:]), 0o644); err != nil { + return fmt.Errorf("writing to file %s: %s", filename, err) + } + pubk, _ := privateKey.Public().(*ecdsa.PublicKey) + publicKey := common.HexToAddress(crypto.PubkeyToAddress(*pubk).Hex()) + + fmt.Printf("Wallet address %s created\n", publicKey) + fmt.Printf("Private key saved in %s\n", filename) + return nil + }, + }, + { + Name: "pubkey", + Usage: "print the public key for a private key", + Action: func(cCtx *cli.Context) error { + filename := cCtx.Args().Get(0) + if filename == "" { + return errors.New("filename is empty") + } + + privateKey, err := crypto.LoadECDSA(filename) + if err != nil { + return fmt.Errorf("loading key: %s", err) + } + + pubk, _ := privateKey.Public().(*ecdsa.PublicKey) + publicKey := common.HexToAddress(crypto.PubkeyToAddress(*pubk).Hex()) + + fmt.Println(publicKey) + return nil + }, + }, + }, + } +} + +func parseVaultName(name string) (ns string, rel string, err error) { match := pubNameRx.FindStringSubmatch(name) if len(match) != 3 { - return "", "", errors.New("publication name must be of the form `namespace.relation_name` using only letters, numbers, and underscores (_), where `namespace` and `relation` do not start with a number") // nolint + return "", "", errors.New("vault name must be of the form `namespace.relation_name` using only letters, numbers, and underscores (_), where `namespace` and `relation` do not start with a number") // nolint } ns = match[1] rel = match[2] @@ -651,7 +688,7 @@ func inspectTable(ctx context.Context, tx pgx.Tx, rel string) ([]app.Column, err return columns, nil } -func createPublication( +func createVault( ctx context.Context, dburi string, ns string, @@ -665,7 +702,7 @@ func createPublication( return false, fmt.Errorf("not a valid account: %s", err) } - bp := basinprovider.New(provider) + bp := vaultsprovider.New(provider) req := app.CreateVaultParams{ Account: account, Vault: app.Vault(fmt.Sprintf("%s.%s", ns, rel)), diff --git a/cmd/basin/config.go b/cmd/vaults/config.go similarity index 96% rename from cmd/basin/config.go rename to cmd/vaults/config.go index 63b4e61..0c7ef80 100644 --- a/cmd/basin/config.go +++ b/cmd/vaults/config.go @@ -9,7 +9,7 @@ import ( "gopkg.in/yaml.v3" ) -// DefaultProviderHost is the address of Basin Provider. +// DefaultProviderHost is the address of Vaults Provider. const DefaultProviderHost = "https://basin.tableland.xyz" // DefaultWindowSize is the number of seconds for which WAL updates diff --git a/cmd/basin/main.go b/cmd/vaults/main.go similarity index 68% rename from cmd/basin/main.go rename to cmd/vaults/main.go index e00abe4..ae64a9b 100644 --- a/cmd/basin/main.go +++ b/cmd/vaults/main.go @@ -9,10 +9,15 @@ import ( func main() { cliApp := &cli.App{ - Name: "basin", + Name: "vaults", Usage: "Continuously publish data from your database to the Tableland network.", Commands: []*cli.Command{ - newPublicationCommand(), + newVaultCreateCommand(), + newStreamCommand(), + newWriteCommand(), + newListCommand(), + newListEventsCommand(), + newRetrieveCommand(), newWalletCommand(), }, } diff --git a/goreleaser.yml b/goreleaser.yml index 8131285..67f57b7 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -6,12 +6,12 @@ before: env: - CGO_ENABLED=1 -project_name: basin +project_name: vaults builds: -- id: basin-darwin-amd64 - binary: basin - main: ./cmd/basin +- id: vaults-darwin-amd64 + binary: vaults + main: ./cmd/vaults goarch: - amd64 goos: @@ -23,9 +23,9 @@ builds: - -trimpath ldflags: - -s -w -X main.version={{.Version}} -X main.commit={{.FullCommit}} -X main.date={{.CommitDate}} -- id: basin-darwin-arm64 - binary: basin - main: ./cmd/basin +- id: vaults-darwin-arm64 + binary: vaults + main: ./cmd/vaults goarch: - arm64 goos: @@ -37,9 +37,9 @@ builds: - -trimpath ldflags: - -s -w -X main.version={{.Version}} -X main.commit={{.FullCommit}} -X main.date={{.CommitDate}} -- id: basin-linux-amd64 - binary: basin - main: ./cmd/basin +- id: vaults-linux-amd64 + binary: vaults + main: ./cmd/vaults goarch: - amd64 goos: @@ -51,9 +51,9 @@ builds: - -trimpath ldflags: - -s -w -X main.version={{.Version}} -X main.commit={{.FullCommit}} -X main.date={{.CommitDate}} -- id: basin-linux-arm64 - binary: basin - main: ./cmd/basin +- id: vaults-linux-arm64 + binary: vaults + main: ./cmd/vaults goarch: - arm64 goos: @@ -67,15 +67,15 @@ builds: - -s -w -X main.version={{.Version}} -X main.commit={{.FullCommit}} -X main.date={{.CommitDate}} archives: - - id: basin-archive + - id: vaults-archive format: tar.gz files: - none* builds: - - basin-darwin-amd64 - - basin-darwin-arm64 - - basin-linux-amd64 - - basin-linux-arm64 + - vaults-darwin-amd64 + - vaults-darwin-arm64 + - vaults-linux-amd64 + - vaults-linux-arm64 name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" checksum: diff --git a/internal/app/basin_provider.go b/internal/app/basin_provider.go index 8dec2b4..dd6b6f5 100644 --- a/internal/app/basin_provider.go +++ b/internal/app/basin_provider.go @@ -5,8 +5,8 @@ import ( "io" ) -// BasinProvider ... -type BasinProvider interface { +// VaultsProvider defines Vaults API. +type VaultsProvider interface { CreateVault(context.Context, CreateVaultParams) error ListVaults(context.Context, ListVaultsParams) ([]Vault, error) ListVaultEvents(context.Context, ListVaultEventsParams) ([]EventInfo, error) diff --git a/internal/app/db.go b/internal/app/db.go index 88242cf..504509f 100644 --- a/internal/app/db.go +++ b/internal/app/db.go @@ -32,11 +32,11 @@ type DBManager struct { createdAT time.Time cols []Column winSize time.Duration - uploader *BasinUploader + uploader *VaultsUploader } // NewDBManager creates a new DBManager. -func NewDBManager(dbDir, table string, cols []Column, winSize time.Duration, uploader *BasinUploader) *DBManager { +func NewDBManager(dbDir, table string, cols []Column, winSize time.Duration, uploader *VaultsUploader) *DBManager { return &DBManager{ dbDir: dbDir, table: table, diff --git a/internal/app/streamer.go b/internal/app/streamer.go index ef8fbab..b7bdf13 100644 --- a/internal/app/streamer.go +++ b/internal/app/streamer.go @@ -19,24 +19,24 @@ type Replicator interface { Shutdown() } -// BasinStreamer contains logic of streaming Postgres changes to Basin Provider. -type BasinStreamer struct { +// VaultsStreamer contains logic of streaming Postgres changes to Vaults Provider. +type VaultsStreamer struct { namespace string replicator Replicator dbMngr *DBManager } -// NewBasinStreamer creates new streamer. -func NewBasinStreamer(ns string, r Replicator, dbm *DBManager) *BasinStreamer { - return &BasinStreamer{ +// NewVaultsStreamer creates new streamer. +func NewVaultsStreamer(ns string, r Replicator, dbm *DBManager) *VaultsStreamer { + return &VaultsStreamer{ namespace: ns, replicator: r, dbMngr: dbm, } } -// Run runs the BasinStreamer logic. -func (b *BasinStreamer) Run(ctx context.Context) error { +// Run runs the VaultsStreamer logic. +func (b *VaultsStreamer) Run(ctx context.Context) error { // Open a local DB for replaying txs if err := b.dbMngr.NewDB(ctx); err != nil { return err diff --git a/internal/app/streamer_test.go b/internal/app/streamer_test.go index 281180e..fa09b5c 100644 --- a/internal/app/streamer_test.go +++ b/internal/app/streamer_test.go @@ -29,7 +29,7 @@ var cols = []Column{ // Test when window threshold is crossed before // second Tx is received: . -func TestBasinStreamerOne(t *testing.T) { +func TestVaultsStreamerOne(t *testing.T) { // used for testing privateKey, err := crypto.HexToECDSA(pk) require.NoError(t, err) @@ -38,15 +38,15 @@ func TestBasinStreamerOne(t *testing.T) { feed := make(chan *pgrepl.Tx) testDBDir := t.TempDir() winSize := 3 * time.Second - providerMock := &basinProviderMock{ + providerMock := &vaultsProviderMock{ owner: make(map[string]string), uploaderInputs: make(chan *os.File), } - uploader := NewBasinUploader(testNS, testTable, providerMock, privateKey) + uploader := NewVaultsUploader(testNS, testTable, providerMock, privateKey) dbm := NewDBManager( testDBDir, testTable, cols, winSize, uploader) - streamer := NewBasinStreamer(testNS, &replicatorMock{feed: feed}, dbm) + streamer := NewVaultsStreamer(testNS, &replicatorMock{feed: feed}, dbm) go func() { // start listening to WAL records in a separate goroutine err = streamer.Run(context.Background()) @@ -105,7 +105,7 @@ func TestBasinStreamerOne(t *testing.T) { dbm.db = nil dbm.dbFname = "" dbm.createdAT = time.Time{} - uploader.provider = &basinProviderMock{ + uploader.provider = &vaultsProviderMock{ owner: make(map[string]string), uploaderInputs: ch2, } @@ -132,7 +132,7 @@ func TestBasinStreamerOne(t *testing.T) { // Test when window threshold is crossed after // second Tx is received: . -func TestBasinStreamerTwo(t *testing.T) { +func TestVaultsStreamerTwo(t *testing.T) { privateKey, err := crypto.HexToECDSA(pk) require.NoError(t, err) @@ -140,14 +140,14 @@ func TestBasinStreamerTwo(t *testing.T) { feed := make(chan *pgrepl.Tx) testDBDir := t.TempDir() winSize := 3 * time.Second - providerMock := &basinProviderMock{ + providerMock := &vaultsProviderMock{ owner: make(map[string]string), uploaderInputs: make(chan *os.File), } - uploader := NewBasinUploader(testNS, testTable, providerMock, privateKey) + uploader := NewVaultsUploader(testNS, testTable, providerMock, privateKey) dbm := NewDBManager( testDBDir, testTable, cols, winSize, uploader) - streamer := NewBasinStreamer(testNS, &replicatorMock{feed: feed}, dbm) + streamer := NewVaultsStreamer(testNS, &replicatorMock{feed: feed}, dbm) go func() { // start listening to WAL records in a separate goroutine err = streamer.Run(context.Background()) @@ -234,29 +234,29 @@ func (rm *replicatorMock) Shutdown() { close(rm.feed) } -type basinProviderMock struct { +type vaultsProviderMock struct { owner map[string]string uploaderInputs chan *os.File } -func (bp *basinProviderMock) CreateVault( +func (bp *vaultsProviderMock) CreateVault( _ context.Context, params CreateVaultParams, ) error { bp.owner[string(params.Vault)] = params.Account.Hex() return nil } -func (bp *basinProviderMock) ListVaults(_ context.Context, _ ListVaultsParams) ([]Vault, error) { +func (bp *vaultsProviderMock) ListVaults(_ context.Context, _ ListVaultsParams) ([]Vault, error) { return []Vault{}, nil } -func (bp *basinProviderMock) ListVaultEvents( +func (bp *vaultsProviderMock) ListVaultEvents( context.Context, ListVaultEventsParams, ) ([]EventInfo, error) { return []EventInfo{}, nil } -func (bp *basinProviderMock) WriteVaultEvent( +func (bp *vaultsProviderMock) WriteVaultEvent( _ context.Context, params WriteVaultEventParams, ) error { file := params.Content.(*os.File) diff --git a/internal/app/uploader.go b/internal/app/uploader.go index 8b573c4..13facc0 100644 --- a/internal/app/uploader.go +++ b/internal/app/uploader.go @@ -15,19 +15,19 @@ import ( "golang.org/x/crypto/sha3" ) -// BasinUploader contains logic of uploading Parquet files to Basin Provider. -type BasinUploader struct { +// VaultsUploader contains logic of uploading Parquet files to Vaults Provider. +type VaultsUploader struct { namespace string relation string privateKey *ecdsa.PrivateKey - provider BasinProvider + provider VaultsProvider } -// NewBasinUploader creates new uploader. -func NewBasinUploader( - ns string, rel string, bp BasinProvider, pk *ecdsa.PrivateKey, -) *BasinUploader { - return &BasinUploader{ +// NewVaultsUploader creates new uploader. +func NewVaultsUploader( + ns string, rel string, bp VaultsProvider, pk *ecdsa.PrivateKey, +) *VaultsUploader { + return &VaultsUploader{ namespace: ns, relation: rel, provider: bp, @@ -36,7 +36,7 @@ func NewBasinUploader( } // Upload sends file to provider for upload. -func (bu *BasinUploader) Upload( +func (bu *VaultsUploader) Upload( ctx context.Context, filepath string, progress io.Writer, ts Timestamp, sz int64, ) error { f, err := os.Open(filepath) diff --git a/pkg/basinprovider/provider.go b/pkg/vaultsprovider/provider.go similarity index 86% rename from pkg/basinprovider/provider.go rename to pkg/vaultsprovider/provider.go index 79034fa..4d50bb3 100644 --- a/pkg/basinprovider/provider.go +++ b/pkg/vaultsprovider/provider.go @@ -1,4 +1,4 @@ -package basinprovider +package vaultsprovider import ( "context" @@ -14,28 +14,28 @@ import ( "github.com/tablelandnetwork/basin-cli/internal/app" ) -// BasinProvider implements the app.BasinProvider interface. -type BasinProvider struct { +// VaultsProvider implements the app.VaultsProvider interface. +type VaultsProvider struct { provider string client *http.Client } -var _ app.BasinProvider = (*BasinProvider)(nil) +var _ app.VaultsProvider = (*VaultsProvider)(nil) -// New creates a new BasinProvider. -func New(provider string) *BasinProvider { +// New creates a new VaultsProvider. +func New(provider string) *VaultsProvider { client := &http.Client{ Timeout: 10 * time.Second, } - return &BasinProvider{ + return &VaultsProvider{ provider: provider, client: client, } } // CreateVault creates a vault. -func (bp *BasinProvider) CreateVault(ctx context.Context, params app.CreateVaultParams) error { +func (bp *VaultsProvider) CreateVault(ctx context.Context, params app.CreateVaultParams) error { form := url.Values{} form.Add("account", params.Account.Hex()) form.Add("cache", fmt.Sprint(params.CacheDuration)) @@ -63,7 +63,7 @@ func (bp *BasinProvider) CreateVault(ctx context.Context, params app.CreateVault } // ListVaults lists all vaults from a given account. -func (bp *BasinProvider) ListVaults( +func (bp *VaultsProvider) ListVaults( ctx context.Context, params app.ListVaultsParams, ) ([]app.Vault, error) { req, err := http.NewRequestWithContext( @@ -88,7 +88,7 @@ func (bp *BasinProvider) ListVaults( } // ListVaultEvents lists all events from a given vault. -func (bp *BasinProvider) ListVaultEvents( +func (bp *VaultsProvider) ListVaultEvents( ctx context.Context, params app.ListVaultEventsParams, ) ([]app.EventInfo, error) { req, err := http.NewRequestWithContext( @@ -120,7 +120,7 @@ func (bp *BasinProvider) ListVaultEvents( } // WriteVaultEvent write an event. -func (bp *BasinProvider) WriteVaultEvent(ctx context.Context, params app.WriteVaultEventParams) error { +func (bp *VaultsProvider) WriteVaultEvent(ctx context.Context, params app.WriteVaultEventParams) error { req, err := http.NewRequestWithContext( ctx, http.MethodPost, diff --git a/scripts/run.sh b/scripts/run.sh index afa075a..1653a64 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -1,3 +1,3 @@ #!/bin/sh -go run $( ls -1 cmd/basin/*.go | grep -v _test.go) $@ \ No newline at end of file +go run $( ls -1 cmd/vaults/*.go | grep -v _test.go) $@ \ No newline at end of file diff --git a/test/postgres.go b/test/postgres.go index 08f4a5b..0039537 100644 --- a/test/postgres.go +++ b/test/postgres.go @@ -55,7 +55,7 @@ func (dp *DockerPool) RunPostgres() (db *sql.DB, resource *dockertest.Resource, Env: []string{ "POSTGRES_PASSWORD=secret", "POSTGRES_USER=admin", - "POSTGRES_DB=basin", + "POSTGRES_DB=vaults", "listen_addresses = '*'", }, }, func(config *docker.HostConfig) { @@ -68,7 +68,7 @@ func (dp *DockerPool) RunPostgres() (db *sql.DB, resource *dockertest.Resource, _ = resource.Expire(120) // Tell docker to hard kill the container in 120 seconds - uri = fmt.Sprintf("postgres://admin:secret@%s/basin?sslmode=disable", resource.GetHostPort("5432/tcp")) + uri = fmt.Sprintf("postgres://admin:secret@%s/vaults?sslmode=disable", resource.GetHostPort("5432/tcp")) db, err = sql.Open("postgres", uri) if err != nil { log.Fatalf("Could not open the database: %s", err)