The contract

The wallet’s and the ledger’s contracts: the two .proto files, buf’s lint and breaking rules, and buf generate, which writes walletpb and ledgerpb with protoc-gen-go and protoc-gen-go-grpc as go tools.

The contract is two protobuf packages under proto/, ledger.v1 and wallet.v1, generated into ledgerpb and walletpb.

The ledger’s service

proto/ledger/v1/ledger.proto declares five methods:

service Ledger {
  rpc OpenAccount(OpenAccountRequest) returns (OpenAccountResponse);
  rpc GetAccount(GetAccountRequest) returns (GetAccountResponse);
  rpc Deposit(DepositRequest) returns (DepositResponse);
  rpc Transfer(TransferRequest) returns (TransferResponse);
  rpc WatchAccount(WatchAccountRequest) returns (stream WatchAccountResponse);
}

An account is its ID, its balance, and when the balance last changed. A transfer is its ID, the two accounts, the amount and when it was made. Amounts are whole cents, in one currency:

message Account {
  string id = 1;
  int64 balance_cents = 2;
  google.protobuf.Timestamp balance_change_time = 3;
}

message Transfer {
  string id = 1;
  string from_account_id = 2;
  string to_account_id = 3;
  int64 amount_cents = 4;
  google.protobuf.Timestamp create_time = 5;
}

A transfer’s request carries its ID, so a transfer sent twice is posted once:

message TransferRequest {
  string transfer_id = 1;
  string from_account_id = 2;
  string to_account_id = 3;
  int64 amount_cents = 4;
}

A deposit’s request is the same, with one account, since a deposit comes from the ledger’s own external account:

message DepositRequest {
  string transfer_id = 1;
  string account_id = 2;
  int64 amount_cents = 3;
}

The wallet’s service

proto/wallet/v1/wallet.proto declares four. Its Account and Transfer have the ledger’s fields:

service Wallet {
  rpc OpenAccount(OpenAccountRequest) returns (OpenAccountResponse);
  rpc GetAccount(GetAccountRequest) returns (GetAccountResponse);
  rpc Send(SendRequest) returns (SendResponse);
  rpc WatchAccount(WatchAccountRequest) returns (stream WatchAccountResponse);
}

No request names the caller’s account, because the bearer token’s subject is the only account a caller can use:

message OpenAccountRequest {}

A send names its own ID, request_id, so a repeat by the app is the same transfer:

message SendRequest {
  string request_id = 1;
  string to_account_id = 2;
  int64 amount_cents = 3;
}

buf’s rules

proto/buf.yaml lints with buf’s STANDARD rules, and checks breaking changes file by file:

version: v2
lint:
  use:
    - STANDARD
  except:
    - SERVICE_SUFFIX
breaking:
  use:
    - FILE

SERVICE_SUFFIX is left out, since the services are Ledger and Wallet, not LedgerService and WalletService.

buf generate

proto/buf.gen.yaml runs protoc-gen-go and protoc-gen-go-grpc as go tools, so the module’s go.mod, which lists both under tool, pins their versions:

version: v2
plugins:
  - local: ["go", "tool", "protoc-gen-go"]
    out: ..
    opt: module=github.com/z5labs/humus/example/grpc/wallet
  - local: ["go", "tool", "protoc-gen-go-grpc"]
    out: ..
    opt: module=github.com/z5labs/humus/example/grpc/wallet

Run it in proto/:

cd example/grpc/wallet/proto
buf lint
buf generate

It writes ledgerpb and walletpb, from each file’s go_package:

option go_package = "github.com/z5labs/humus/example/grpc/wallet/ledgerpb";

The generated code is checked in. CI runs buf lint, buf breaking against main’s contract, and buf generate, and fails when the generated code differs from what is checked in:

buf breaking --against "$GITHUB_WORKSPACE/main/example/grpc/wallet/proto"