class Books::StandardClient

Attributes

books_authority_client[R]
books_client[R]
eddsa_private_key[R]
eddsa_public_key[R]

Public Class Methods

new(books_client: nil, books_authority_client: nil, connector: nil, eddsa_public_key_path:, eddsa_private_key_path:) click to toggle source

Instantiate a new client that can connect to Books.

** eddsa_public_key_path and eddsa_private_key_path (required) ** :

Both are paths to your eddsa keys you intend to use to sign journal entries.
The private key will be used to sign journal entries as they are sent, while
the public key can be used to verify journal entries afterwards. Note that
these are generally not the same keys used to intitiate TLS.

** connector ** :

A connector from which we can request the clients "books" and "booksAuthority".
This is only required if either books_client or books_authority_client
are not provided. Connectors are provided by the square_connector gem or from
sq/common.

** books_client and books_authority_client (optional) ** :

RPC clients that would otherwise be provided by the connector. Helpful for testing.
If provided, will take precedence over what would have been returned by the connector
# File lib/books/client.rb, line 31
def initialize(books_client: nil,
  books_authority_client: nil,
  connector: nil,
  eddsa_public_key_path:,
  eddsa_private_key_path:)
  @books_client = books_client || begin
    fail "Neither connector nor books_client given" unless connector
    books_http_client = connector.create(:books)
    Sq::Protos::RpcClient.new(books_http_client, Squareup::Books::Service::BooksService)
  end

  @books_authority_client = books_authority_client || begin
    fail "Neither connector nor books_authority_client given" unless connector
    books_authority_http_client = connector.create(:booksAuthority)
    Sq::Protos::RpcClient.new(books_authority_http_client,
      Squareup::Booksauthority::Service::BooksAuthorityService)
  end

  @eddsa_private_key = Ed25519::SigningKey.new(extract_key(eddsa_private_key_path, "ED25519 PRIVATE KEY"))
  @eddsa_public_key_data = File.read(eddsa_public_key_path) # want PEM format
  @eddsa_public_key = Ed25519::VerifyKey.new(extract_key(eddsa_public_key_path, "ED25519 PUBLIC KEY"))
end

Public Instance Methods

create_book(req) click to toggle source
# File lib/books/client.rb, line 72
def create_book(req)
  bas_response = @books_authority_client.https_rpc(:generate_sskg, {})
  with_sskg = req.dup
  with_sskg[:first_sskg_key] = bas_response[:first_key]
  with_sskg[:root_sskg_token] = bas_response[:root_sskg_token]
  @books_client.https_rpc(:create_book, with_sskg)
end
create_denomination(req) click to toggle source
# File lib/books/client.rb, line 68
def create_denomination(req)
  @books_client.https_rpc(:create_denomination, req)
end
get_books_matching(req) click to toggle source
# File lib/books/client.rb, line 54
def get_books_matching(req)
  @books_client.https_rpc(:get_books_matching, req)
end
journal(req) click to toggle source
# File lib/books/client.rb, line 80
def journal(req)
  raise "Journal entry type not provided" unless req[:journal_entry_type]
  raise "Journal idempotence token not provided" unless req[:idempotence_token]
  raise "Journal entries not provided" unless req[:entries] && req[:entries].size > 1

  total = req[:entries].map do |entry|
    raise "Must set exactly one of to_amount or from_amount" unless (entry[:to_amount] ^ entry[:from_amount])
    entry[:to_amount] - entry[:from_amount]
  end.sum

  raise "Total change in book entry amounts is not 0" unless total == 0

  raise "The signature field must not be set (the SDK will assign this field for you)" if req[:signature]

  # sign the request body
  jreq = Squareup::Books::Service::JournalRequest.new(req)

  encoded_proto = jreq.encode
  signature = @eddsa_private_key.sign(encoded_proto)
  req[:signature] = signature

  journal_response = @books_client.https_rpc(:journal, req)
  if journal_response.status != Squareup::Books::Service::JournalStatus::JOURNAL_SUCCESS
    raise "Journaling error received: #{journal_resp.status}"
  end
  journal_response
end
register_client(req) click to toggle source
# File lib/books/client.rb, line 64
def register_client(req)
  @books_client.https_rpc(:register_client, req)
end
register_myself() click to toggle source
# File lib/books/client.rb, line 58
def register_myself
  register_client({
    public_key: @eddsa_public_key_data
  })
end

Private Instance Methods

extract_key(pem_filepath, expected_pem_header) click to toggle source

Given a filepath, read the file as a PEM file. Assert that the given header is present in the PEM header and footer.

# File lib/books/client.rb, line 112
def extract_key(pem_filepath, expected_pem_header)
  pem_data = File.read(pem_filepath)
  contents = pem_data.split("\n")
  raise "Invalid PEM file, too short" unless contents.size > 2

  unless contents[0].include?(expected_pem_header)
    raise "Header for #{pem_filepath} isn't what we expected. Is it a #{expected_pem_header} ?"
  end
  unless contents[contents.length - 1].include?(expected_pem_header)
    raise "Footer for #{pem_filepath} isn't what we expected. Is it a #{expected_pem_header} ?"
  end

  base64Encoded = contents[1..contents.size-2].join("")
  return Base64.decode64(base64Encoded)
end