Skip to content

Commit

Permalink
Push demo
Browse files Browse the repository at this point in the history
  • Loading branch information
dastansam committed Mar 21, 2024
1 parent d436ec9 commit e62912d
Show file tree
Hide file tree
Showing 29 changed files with 1,507 additions and 788 deletions.
62 changes: 62 additions & 0 deletions DEMO-M2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Demo for M2

For demonstration purposes, `docker-compose` configuration is provided. It will start the following services:

- [Demo merchant application](./interface/README.md)
- [Payment Processor Server](./payment-processor/README.md)
- PCIDSS compliant [oracle](./pcidss/README.md)
- Substrate chain

To start the demo, first pull the images:

```bash
docker-compose pull
```

Then start the services:

```bash
docker-compose up
```

You will be able to access the demo merchant application at `http://localhost:3002`.

And for the Substrate chain, you can access the explorer [here](https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:9944#/explorer).

## Milestone Goals

1. On-chain addresses can be associated with bank accounts
2. On-chain balance is synced with off-chain balance, off-chain ledger serves as a source of truth
3. It is possible to trigger ISO-8583 transactions (both payment and reversal) both from POS and on-chain transactions
4. On-chain messages are converted to ISO-8583 format and processed by the oracle
5. Oracles settle finalised ISO-8583 transactions on-chain

## Demo flow

### On-chain address association

By opening the demo merchant application, main page is a simulation of a bank dashboard, where you can see basic details about the bank account and its transactions. To ease the testing, there is a button on the top right corner that allows you to switch between development accounts. You can use development accounts to simulate different scenarios.

When switching between accounts, you will notice that balance and transactions are different.

When an address you switched to is not associated with any bank account, you will be redirected to the registration page, which will ask for your card details. After submitting the form, you will be redirected back to the dashboard. Registration request is ISO-8583 message, which is processed and settled on chain by the oracle.

And now when you select the address, it will show bank account details and transactions.

### On-chain balance synchronization

If you check for account balances from the explorer, you will see that they match what is shown on the dashboard.

### ISO-8583 transactions

Now, to actually see how on-chain balance is synced and how ISO-8583 transactions are processed, you can use the checkout page. It is a simple form that asks for card details and amount to transfer. It is a simulation of a POS terminal, part of delivery of Milestone 1. Submit the form with one of the test accounts and you will see the transaction on the dashboard.

By checking the explorer, you will notice that on-chain balance is updated after couple of blocks (10 blocks ~30s currently). This is not a limitation of the system, because the source of truth is always off-chain ledger.

With Milestone 2, we added the ability to trigger ISO-8583 transactions with on-chain transaction. To do that, you have to switch to `Crypto` tab and click on `Pay` button. It will trigger an extrinsic which you have to sign and submit. After that, you will see the transaction both on the dashboard and in the explorer.

### Settlement

Note that in the explorer, you will initially see `InitiateTransfer` event, and after couple of blocks `ProcessedTransaction` event. This is because of event driven nature of current implementation. Most of the times, however, transaction is initiated and processed in the same or next block. Since we are using off-chain ledger as a source of truth, on-chain settlement is not really important, however it is important for UX since wallets need to be notified when transaction is settled (i.e by tracking `ProcessedTransaction` event).

## Notes
16 changes: 14 additions & 2 deletions interface/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ Main page is a simulation of a bank dashboard, where you can see basic details a

This is a React app and while running in development node, you need to run the [PCIDSS Oracle Gateway](../pcidss/README.md) and [Payment Processor](../payment-processor/README.md) in separate terminals. Please follow the instructions in the respective README files.

It also uses the payment processor server running at `http://localhost:3000` as a proxy. If you want to change the port, you need to update the `package.json` file.
It also uses the payment processor server running at `http://localhost:3001` as a proxy. If you want to change the port, you need to update the `package.json` file.

1. Run `yarn` to install dependencies
2. Run `yarn start` to start the interface
3. Open `http://localhost:3001` in your browser
3. Open `http://localhost:3002` in your browser

## How to use

Expand Down Expand Up @@ -64,6 +64,18 @@ Here are the test accounts you can use:
"balance": 1000000000,
"card_expiry": "09/27"
},
{
"card_holder_first_name": "Alice_stash",
"card_number": "4169812345678908",
"card_cvv": "999",
"balance": 0,
},
{
"card_holder_first_name": "Bob_stash",
"card_number": "4169812345678909",
"card_cvv": "888",
"balance": 0,
},
]
```

Expand Down
13 changes: 11 additions & 2 deletions interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
"version": "0.0.1",
"private": true,
"dependencies": {
"@polkadot/api": "^8.9.1",
"@polkadot/api": "^10.6.1",
"@polkadot/extension-dapp": "^0.46.2",
"@polkadot/keyring": "^12.1.2",
"@polkadot/networks": "^12.1.2",
"@polkadot/types": "^10.6.1",
"@polkadot/ui-keyring": "^3.3.1",
"@polkadot/ui-settings": "^3.3.1",
"@polkadot/util": "^12.1.2",
"@polkadot/util-crypto": "^12.1.2",
"bn.js": "^5.2.1",
"payment": "2.3.0",
"react": "^18.2.0",
Expand All @@ -15,7 +23,8 @@
"react-router-dom": "^6.16.0",
"react-scripts": "^5.0.1",
"semantic-ui-css": "^2.5.0",
"semantic-ui-react": "^2.1.4"
"semantic-ui-react": "^2.1.4",
"prop-types": "^15.8.1"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.16.7"
Expand Down
85 changes: 64 additions & 21 deletions interface/src/App.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { WsProvider } from "@polkadot/api";
import React, { useEffect, useState } from "react";
import React, { createRef, useEffect, useState } from "react";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import { Dimmer, Grid, Loader, Message, Sticky } from "semantic-ui-react";

import AccountSelector from "./components/AccountSelector";
import Checkout from "./components/Checkout";
import Dashboard from "./components/Dashboard";
import Register from "./components/Register";
import Success from "./components/Success";
import { DEV_ACCOUNTS } from "./constants";
import { SubstrateContextProvider, useSubstrateState } from "./substrate-lib";

function App() {
function Main() {
let [oracleRpc, setOracleRpc] = useState(null);
let [isConnected, setIsConnected] = useState(false);

const loadWs = async () => {
const ws = new WsProvider("ws://0.0.0.0:3030");
await ws.connect();
setOracleRpc(ws);
};

Expand All @@ -26,25 +27,67 @@ function App() {
loadWs();
}, []);

const { apiState, apiError, keyringState } = useSubstrateState();

const loader = (text) => (
<Dimmer active>
<Loader size="small">{text}</Loader>
</Dimmer>
);

const message = (errObj) => (
<Grid centered columns={2} padded>
<Grid.Column>
<Message
negative
compact
floating
header="Error Connecting to Substrate"
content={`Connection to websocket '${errObj.target.url}' failed.`}
/>
</Grid.Column>
</Grid>
);

if (apiState === "ERROR") return message(apiError);
else if (apiState !== "READY") return loader("Connecting to Substrate");
if (keyringState !== "READY") {
return loader(
"Loading accounts (please review any extension's authorization)"
);
}

const contextRef = createRef();

if (isConnected) {
return (
<Router>
<Routes>
<Route path="/success" element={<Success />}></Route>
<Route
path="/checkout-demo"
element={<Checkout state={{ oracleRpc }} />}
></Route>
<Route
path="/"
element={
<Dashboard state={{ accounts: DEV_ACCOUNTS, oracleRpc }} />
}
></Route>
</Routes>
</Router>
<div ref={contextRef}>
<Sticky>
<AccountSelector />
</Sticky>
<Router>
<Routes>
<Route path="/success" element={<Success />}></Route>
<Route
path="/checkout-demo"
element={<Checkout state={{ oracleRpc }} />}
></Route>
<Route
path="/"
element={<Dashboard state={{ oracleRpc }} />}
></Route>
<Route path="/register" element={<Register />}></Route>
</Routes>
</Router>
</div>
);
}
}

export default App;
export default function App() {
return (
<SubstrateContextProvider>
<Main />
</SubstrateContextProvider>
);
}
143 changes: 143 additions & 0 deletions interface/src/components/AccountSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useEffect, useState } from "react";
import { CopyToClipboard } from "react-copy-to-clipboard";

import {
Button,
Container,
Dropdown,
Icon,
Image,
Label,
Menu,
} from "semantic-ui-react";

import { useSubstrate, useSubstrateState } from "../substrate-lib";
import { formatAmount } from "../utils";

const CHROME_EXT_URL =
"https://chrome.google.com/webstore/detail/polkadot%7Bjs%7D-extension/mopnmbcafieddcagagdcbnhejhlodfdd";
const FIREFOX_ADDON_URL =
"https://addons.mozilla.org/en-US/firefox/addon/polkadot-js-extension/";

const acctAddr = (acct) => (acct ? acct.address : "");

function Main(props) {
const {
setCurrentAccount,
state: { keyring, currentAccount },
} = useSubstrate();

// Get the list of accounts we possess the private key for
const keyringOptions = keyring.getPairs().map((account) => ({
key: account.address,
value: account.address,
text: account.meta.name.toUpperCase(),
icon: "user",
}));

const initialAddress =
keyringOptions.length > 0 ? keyringOptions[0].value : "";

// Set the initial address
useEffect(() => {
// `setCurrentAccount()` is called only when currentAccount is null (uninitialized)
!currentAccount &&
initialAddress.length > 0 &&
setCurrentAccount(keyring.getPair(initialAddress));
}, [currentAccount, setCurrentAccount, keyring, initialAddress]);

const onChange = (addr) => {
setCurrentAccount(keyring.getPair(addr));
};

return (
<Menu
attached="top"
tabular
style={{
backgroundColor: "#fff",
borderColor: "#fff",
paddingTop: "1em",
paddingBottom: "1em",
}}
>
<Container>
<Menu.Menu>
<Image
src={`${process.env.PUBLIC_URL}/assets/substrate-logo.png`}
size="mini"
/>
</Menu.Menu>
<Menu.Menu position="right" style={{ alignItems: "center" }}>
{!currentAccount ? (
<span>
Create an account with Polkadot-JS Extension (
<a target="_blank" rel="noreferrer" href={CHROME_EXT_URL}>
Chrome
</a>
,&nbsp;
<a target="_blank" rel="noreferrer" href={FIREFOX_ADDON_URL}>
Firefox
</a>
)&nbsp;
</span>
) : null}
<CopyToClipboard text={acctAddr(currentAccount)}>
<Button
basic
circular
size="large"
icon="user"
color={currentAccount ? "green" : "red"}
/>
</CopyToClipboard>
<Dropdown
search
selection
clearable
placeholder="Select an account"
options={keyringOptions}
onChange={(_, dropdown) => {
onChange(dropdown.value);
}}
value={acctAddr(currentAccount)}
/>
<BalanceAnnotation />
</Menu.Menu>
</Container>
</Menu>
);
}

function BalanceAnnotation(props) {
const { api, currentAccount } = useSubstrateState();
const [accountBalance, setAccountBalance] = useState(0);

// When account address changes, update subscriptions
useEffect(() => {
let unsubscribe;

// If the user has selected an address, create a new subscription
currentAccount &&
api.query.system
.account(acctAddr(currentAccount), (balance) =>
setAccountBalance(balance.data.free)
)
.then((unsub) => (unsubscribe = unsub))
.catch(console.error);

return () => unsubscribe && unsubscribe();
}, [api, currentAccount]);

return currentAccount ? (
<Label pointing="left">
<Icon name="money" color="green" />
{formatAmount(accountBalance)}
</Label>
) : null;
}

export default function AccountSelector(props) {
const { api, keyring } = useSubstrateState();
return keyring.getPairs && api.query ? <Main {...props} /> : null;
}
Loading

0 comments on commit e62912d

Please sign in to comment.