Skip to content

Commit

Permalink
Ported Bank of Anthos. (ServiceWeaver#438)
Browse files Browse the repository at this point in the history
This PR adds @nipun-sehrawat's port of Bank of Anthos to Service Weaver.
I updated the port to work with the latest version of Service Weaver and
added a README with some instructions, but otherwise didn't change much.

I also added Docker instructions for running BOA.
  • Loading branch information
mwhittaker committed Oct 2, 2023
1 parent af2bdf7 commit fc71056
Show file tree
Hide file tree
Showing 43 changed files with 6,330 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
cmd/weaver/weaver
examples/bankofanthos/bankofanthos
examples/chat/chat
examples/collatz/collatz
examples/factors/factors
examples/onlineboutique/onlineboutique
examples/hello/hello
examples/helloworld/helloworld
website/public/
Expand Down
115 changes: 115 additions & 0 deletions examples/bankofanthos/1_create_transactions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/bin/bash
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http:https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Create demo transactions in the ledger for the demo user accounts.
#
# Gerenated transactions follow a pattern of biweekly large deposits with
# periodic small payments to randomly choosen accounts.
#
# To run, set environment variable USE_DEMO_DATA="True"

set -u


# skip adding transactions if not enabled
if [ -z "$USE_DEMO_DATA" ] && [ "$USE_DEMO_DATA" != "True" ]; then
echo "\$USE_DEMO_DATA not \"True\"; no demo transactions added"
exit 0
fi


# Expected environment variables
readonly ENV_VARS=(
"POSTGRES_DB"
"POSTGRES_USER"
"POSTGRES_PASSWORD"
"LOCAL_ROUTING_NUM"
)


add_transaction() {
DATE=$(date -u +"%Y-%m-%d %H:%M:%S.%3N%z" --date="@$(($6))")
echo "adding demo transaction: $1 -> $2"
PGPASSWORD="$POSTGRES_PASSWORD" psql -X -v ON_ERROR_STOP=1 -v fromacct="$1" -v toacct="$2" -v fromroute="$3" -v toroute="$4" -v amount="$5" --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
INSERT INTO TRANSACTIONS (FROM_ACCT, TO_ACCT, FROM_ROUTE, TO_ROUTE, AMOUNT, TIMESTAMP)
VALUES (:'fromacct', :'toacct', :'fromroute', :'toroute', :'amount', '$DATE');
EOSQL
}


create_transactions() {
PAY_PERIODS=3
DAYS_BETWEEN_PAY=14
SECONDS_IN_PAY_PERIOD=$(( 86400 * $DAYS_BETWEEN_PAY ))
DEPOSIT_AMOUNT=250000

# create a UNIX timestamp in seconds since the Epoch
START_TIMESTAMP=$(( $(date +%s) - $(( $(($PAY_PERIODS+1)) * $SECONDS_IN_PAY_PERIOD )) ))

for i in $(seq 1 $PAY_PERIODS); do
# create deposit transaction for each user
for account in ${USER_ACCOUNTS[@]}; do
add_transaction "$EXTERNAL_ACCOUNT" "$account" "$EXTERNAL_ROUTING" "$LOCAL_ROUTING_NUM" $DEPOSIT_AMOUNT $START_TIMESTAMP
done

# create 15-20 payments between users
TRANSACTIONS_PER_PERIOD=$(shuf -i 15-20 -n1)
for p in $(seq 1 $TRANSACTIONS_PER_PERIOD); do
# randomly generate an amount between $10-$100
AMOUNT=$(shuf -i 1000-10000 -n1)

# randomly select a sender and receiver
SENDER_ACCOUNT=${USER_ACCOUNTS[$RANDOM % ${#USER_ACCOUNTS[@]}]}
RECIPIENT_ACCOUNT=${USER_ACCOUNTS[$RANDOM % ${#USER_ACCOUNTS[@]}]}
# if sender equals receiver, send to a random anonymous account
if [[ "$SENDER_ACCOUNT" == "$RECIPIENT_ACCOUNT" ]]; then
RECIPIENT_ACCOUNT=$(shuf -i 1000000000-9999999999 -n1)
fi

TIMESTAMP=$(( $START_TIMESTAMP + $(( $SECONDS_IN_PAY_PERIOD * $p / $(($TRANSACTIONS_PER_PERIOD + 1 )) )) ))

add_transaction "$SENDER_ACCOUNT" "$RECIPIENT_ACCOUNT" "$LOCAL_ROUTING_NUM" "$LOCAL_ROUTING_NUM" $AMOUNT $TIMESTAMP
done

START_TIMESTAMP=$(( $START_TIMESTAMP + $(( $i * $SECONDS_IN_PAY_PERIOD )) ))
done
}


create_ledger() {
# Account numbers for users 'testuser', 'alice', 'bob', and 'eve'.
USER_ACCOUNTS=("1011226111" "1033623433" "1055757655" "1077441377")
# Numbers for external account 'External Bank'
EXTERNAL_ACCOUNT="9099791699"
EXTERNAL_ROUTING="808889588"

create_transactions
}


main() {
# Check environment variables are set
for env_var in ${ENV_VARS[@]}; do
if [[ -z "${env_var}" ]]; then
echo "Error: environment variable '$env_var' not set. Aborting."
exit 1
fi
done

create_ledger
}


main
88 changes: 88 additions & 0 deletions examples/bankofanthos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Bank of Anthos

This directory contains a port of Google Cloud's [Bank of Anthos][boa] demo
application.

```mermaid
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
graph TD
%% Nodes.
github.com/ServiceWeaver/weaver/Main(weaver.Main)
github.com/ServiceWeaver/weaver/examples/bankofanthos/balancereader/T(balancereader.T)
github.com/ServiceWeaver/weaver/examples/bankofanthos/contacts/T(contacts.T)
github.com/ServiceWeaver/weaver/examples/bankofanthos/ledgerwriter/T(ledgerwriter.T)
github.com/ServiceWeaver/weaver/examples/bankofanthos/transactionhistory/T(transactionhistory.T)
github.com/ServiceWeaver/weaver/examples/bankofanthos/userservice/T(userservice.T)
%% Edges.
github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/bankofanthos/balancereader/T
github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/bankofanthos/contacts/T
github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/bankofanthos/ledgerwriter/T
github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/bankofanthos/transactionhistory/T
github.com/ServiceWeaver/weaver/Main --> github.com/ServiceWeaver/weaver/examples/bankofanthos/userservice/T
github.com/ServiceWeaver/weaver/examples/bankofanthos/ledgerwriter/T --> github.com/ServiceWeaver/weaver/examples/bankofanthos/balancereader/T
```

## Running Locally

- TODO(mwhittaker): Re-write the app to use the JWT credentials shipped with the
original bank of anthos app.

First, run and initialize a local [Postgres][postgres] instance.

1. Create an `admin` user with password `admin`.
2. Create two databases, `postgresdb` and `accountsdb`, both owned by `admin`.
3. Use `postgresdb.sql` and `accountsdb.sql` to initialize the `postgresdb` and
`accountsdb` databases respectively.
4. Run `1_create_transactions.sh` to populate `postgresdb`.

Note that these scripts were taken from [`ledger-db/initdb/`][ledger-db] and
[`accounts-db/initdb/`][accounts-db].

We recommend using Docker to perform these steps:

```shell
# Run the Postgres instance.
$ docker run \
--rm \
--detach \
--name postgres \
--env POSTGRES_PASSWORD=password \
--volume="$(realpath postgres.sh):/app/postgres.sh" \
--volume="$(realpath postgresdb.sql):/app/postgresdb.sql" \
--volume="$(realpath accountsdb.sql):/app/accountsdb.sql" \
--volume="$(realpath 1_create_transactions.sh):/app/1_create_transactions.sh" \
--publish 127.0.0.1:5432:5432 \
postgres

# Wait about 10 seconds for the Postgres instance to start. Then, run the
# postgres.sh script in the container.
docker exec -it postgres /app/postgres.sh
```

Next, create a private key and public key for JWT called `jwtRS256.key` and
`jwtRS256.key.pub` inside `/tmp/.ssh`.

```shell
$ openssl genrsa -out jwtRS256.key 4096
$ openssl rsa -in jwtRS256.key -outform PEM -pubout -out jwtRS256.key.pub
$ mkdir -p /tmp/.ssh
$ mv jwtRS256.key jwtRS256.key.pub /tmp/.ssh
```

Finally, run the application.

```shell
$ go build .

# Run the application in a single process.
$ weaver single deploy weaver.toml

# Run the application in multiple processes.
$ weaver multi deploy weaver.toml
```

[accounts-db]: https://github.com/GoogleCloudPlatform/bank-of-anthos/tree/main/src/accounts/accounts-db/initdb
[boa]: https://github.com/GoogleCloudPlatform/bank-of-anthos
[ledger-db]: https://github.com/GoogleCloudPlatform/bank-of-anthos/tree/main/src/ledger/ledger-db/initdb
[postgres]: https://www.postgresql.org/
80 changes: 80 additions & 0 deletions examples/bankofanthos/accountsdb.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2020, Google LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

-- users stores information about Bank of Anthos customers, including their
-- username, password, name, etc.
CREATE TABLE IF NOT EXISTS users (
accountid CHAR(10) PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
passhash BYTEA NOT NULL,
firstname VARCHAR(64) NOT NULL,
lastname VARCHAR(64) NOT NULL,
birthday DATE NOT NULL,
timezone VARCHAR(8) NOT NULL,
address VARCHAR(64) NOT NULL,
state CHAR(2) NOT NULL,
zip VARCHAR(5) NOT NULL,
ssn CHAR(11) NOT NULL
);

CREATE INDEX IF NOT EXISTS ON users (accountid);
CREATE INDEX IF NOT EXISTS ON users (username);

-- contacts stores the contacts for every user. A contact is a bank account to
-- which a user can send funds. For example, if Alice has Bob as a contact,
-- Alice can send funds to Bob.
CREATE TABLE IF NOT EXISTS contacts (
username VARCHAR(64) NOT NULL,
label VARCHAR(128) NOT NULL,
account_num CHAR(10) NOT NULL,
routing_num CHAR(9) NOT NULL,
is_external BOOLEAN NOT NULL,
FOREIGN KEY (username) REFERENCES users(username)
);

CREATE INDEX IF NOT EXISTS ON contacts (username);

-- Populate the users table.
INSERT INTO users VALUES
('1011226111', 'testuser', '\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61', 'Test', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333'),
('1033623433', 'alice', '\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61', 'Alice', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333'),
('1055757655', 'bob', '\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61', 'Bob', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333'),
('1077441377', 'eve', '\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61', 'Eve', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333')
ON CONFLICT DO NOTHING;

-- Populate the contacts table with internal contacts.
INSERT INTO contacts VALUES
('testuser', 'Alice', '1033623433', '883745000', 'false'),
('testuser', 'Bob', '1055757655', '883745000', 'false'),
('testuser', 'Eve', '1077441377', '883745000', 'false'),
('alice', 'Testuser', '1011226111', '883745000', 'false'),
('alice', 'Bob', '1055757655', '883745000', 'false'),
('alice', 'Eve', '1077441377', '883745000', 'false'),
('bob', 'Testuser', '1011226111', '883745000', 'false'),
('bob', 'Alice', '1033623433', '883745000', 'false'),
('bob', 'Eve', '1077441377', '883745000', 'false'),
('eve', 'Testuser', '1011226111', '883745000', 'false'),
('eve', 'Alice', '1033623433', '883745000', 'false'),
('eve', 'Bob', '1055757655', '883745000', 'false')
ON CONFLICT DO NOTHING;

-- Populate the contacts table with internal contacts.
INSERT INTO contacts VALUES
('testuser', 'External Bank', '9099791699', '808889588', 'true'),
('alice', 'External Bank', '9099791699', '808889588', 'true'),
('bob', 'External Bank', '9099791699', '808889588', 'true'),
('eve', 'External Bank', '9099791699', '808889588', 'true')
ON CONFLICT DO NOTHING;
87 changes: 87 additions & 0 deletions examples/bankofanthos/balancereader/balancereader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http:https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package balancereader

import (
"context"

"github.com/ServiceWeaver/weaver"
"github.com/ServiceWeaver/weaver/examples/bankofanthos/common"
"github.com/ServiceWeaver/weaver/examples/bankofanthos/model"
)

// T is a component that reads user balances.
type T interface {
// GetBalance returns the balance of an account id.
GetBalance(ctx context.Context, accountID string) (int64, error)
}

type config struct {
LocalRoutingNum string `toml:"local_routing_num"`
DataSourceURL string `toml:"data_source_url"`
}

type impl struct {
weaver.Implements[T]
weaver.WithConfig[config]
txnRepo *transactionRepository
balanceCache *balanceCache
ledgerReader *common.LedgerReader
}

var _ common.LedgerReaderCallback = (*impl)(nil)

// ProcessTransaction implements the common.LedgerReaderCallback interface.
func (i *impl) ProcessTransaction(transaction model.Transaction) {
fromID := transaction.FromAccountNum
fromRoutingNum := transaction.FromRoutingNum
toID := transaction.ToAccountNum
toRouting := transaction.ToRoutingNum
amount := transaction.Amount
if fromRoutingNum == i.Config().LocalRoutingNum {
if got, ok := i.balanceCache.c.GetIfPresent(fromID); ok {
prevBalance := got.(int64)
i.balanceCache.c.Put(fromID, prevBalance-int64(amount))
}
}
if toRouting == i.Config().LocalRoutingNum {
if got, ok := i.balanceCache.c.GetIfPresent(toID); ok {
prevBalance := got.(int64)
i.balanceCache.c.Put(toID, prevBalance+int64(amount))
}
}
}

func (i *impl) Init(ctx context.Context) error {
var err error
i.txnRepo, err = newTransactionRepository(i.Config().DataSourceURL)
if err != nil {
return err
}
const cacheSize = 1000000
i.balanceCache = newBalanceCache(i.txnRepo, cacheSize, i.Config().LocalRoutingNum)
i.ledgerReader = common.NewLedgerReader(i.txnRepo, i.Logger(ctx))
i.ledgerReader.StartWithCallback(i)
return nil
}

func (i *impl) GetBalance(ctx context.Context, accountID string) (int64, error) {
// Load from cache.
got, err := i.balanceCache.c.Get(accountID)
if err != nil {
return 0, err
}
return got.(int64), nil
}
Loading

0 comments on commit fc71056

Please sign in to comment.