/niftyx-poc

The xumm application example for xApps using hybrid patterns.

Primary LanguageJavaScript

niftyx-poc

A proof of concept for a xumm app demonstrating the concept of "Hybrid xApps" which are xApps that have three authentication modes:

  • xApp - xumm apps are authenticated by the using the application inside the xumm mobile app and the xumm API via the xAppToken generated by the xumm app.
  • Browser OAuth2 - xApps can also be authenticated by the user via a browser based OAuth2 flow. This is the most common way to authenticate a web app.
  • Server Side JWT Backend - Both modes produce a JWT, this JWT can be used to protect backend calls.

Architecture

The project includes all implementations required to run a xumm app, a browser based OAuth2 flow and a server side JWT backend. The project is a simple FastAPI app that can be run locally or deployed to a server. The app is a simple stub that can be used to build any new xapp, as a ReactJS app that can be built and deployed to a server or as a static site.

out/docs/puml/core-components/core-components.png

xApp

This application uses the new "unified" xumm SDK to authenticate the user and get a JWT token. The JWT token can then also be used to make backend calls to the FastAPI server.

Xumm-Universal-SDKT

/**
 * Scenarios:
 *   - xApp: apikey:        » Go ahead
 *   - xApp: apikey+secret:             » ERROR
 *   - xApp: jwt:           » Go ahead
 * »»» Load SDKJWT + XAPP (UI INTERACTION)
 *
 *   - Browser: apikey      » PKCE
 *   - Browser: api+secret              » ERROR
 *   - Browser: jwt:        » Go ahead
 * »»» Load SDKJWT
 *
 *   - CLI: apikey                      » ERROR
 *   - CLI: jwt             » Go ahead
 *   - CLI: apikey+secret   » Go ahead
 * »»» Load SDK
 */

There are some considerations when using ReactJS to write a xumm app. The xumm app is a ReactJS app that is embedded in the xumm app, but it can also run as an application on the web browser. This means that the app needs to be able to handle to manage the state of what kind of mode it is running in and provide different UX for each mode:

So essentially needs to be able to handle the following scenarios:

  • xApp - xumm apps are authenticated by the using the application inside the xumm mobile app and the xumm API via the xAppToken generated by the xumm app.
  • Browser OAuth2 - xApps can also be authenticated by the user via a browser based OAuth2 flow. This is the most common way to authenticate a web app.

This means that state management needs to be aware of which mode the app is running in and handle the different scenarios.

  • ott - xApp "One Time Token" - this is the token generated by the xumm app and is used to authenticate the xApp. This model is only available in the xumm app.
  • openid - this is the OAuth JWT token header generated by the xumm SDK and is used to authenticate the browser based app. This model is only available in the browser PKCE flow.

The code to manage the state is in these places:

App.js Management of App Type State

{ott && isXApp && <> {ott.nodetype} XAPP</>}
{openid && isWebApp && <> {openid.networkType} BROWSER</>}    

XummAuthService.js

  • getXumm - returns the xumm SDK based by checking at local storage, usually used for the Browser mode. If no token is found, it will return a null, telling the app to login.
  • setBearer (token) - sets the token in Axios for authorization to the backend.

Global Xumm SDK and Promise Based SDK

If you are using ReactJS, you should use the global a Xumm instance to access the xumm SDK via a promise. This is the recommended way to access the xumm app because instantiating the SDK in the scope of the component can cause SDK sate issues.

/**
 * IMPORTANT!
 * this is the Xumm SDK, its super important that you
 * you create this as a global top level reference.
 * Creating this within a component will cause the
 * component to re-render every time the state changes
 * and breaks the sdk
 */
const xumm = XummAuthService.getXumm();

The xumm SDK is a promise based SDK, so you need to use the then method to get the SDK.

xumm.then((xummSDK) => {
    // do something with the xumm SDK
    xummSDK.environment.bearer?.then(r => {
        Axios.defaults.headers.common['Authorization'] = `Bearer ${r}`;
    });
})

Managing Transactions

When a tx payload is created by the xumm SDK it generates a UUID that is used to manage the transaction lifecycle.

{
	"uuid": "3f01e760-4c37-9b8b-e5f3e6a34417",
	"next": {
		"always": "https://xumm.app/sign/3f01e760-2130-4c37-e5f3e6a34417"
	},
	"refs": {
		"qr_png": "https://xumm.app/sign/3f01e760-2130-4c37-e5f3e6a34417_q.png",
		"qr_matrix": "https://xumm.app/sign/3f01e760-2130-4c37-e5f3e6a34417_q.json",
		"qr_uri_quality_opts": ["m", "q", "h"],
		"websocket_status": "wss://xumm.app/sign/3f01e760-2130-4c37-e5f3e6a34417"
	},
	"pushed": false
}

There are two ways to then sign transactions by the xumm app:

  • Browser - the backend creates the payload and displays a QR code the user can scan a QR using xumm to sign.
  • Broswer Mobile - The app is running on a mobile browser and the user can use the xumm app to sign the transaction.
  • xApp - the xumm app uses the xumm.xapp.openSignRequest({ uuid: '...' }) method to open the transaction in the xumm app.

Broswer Mobile Signing

In this implementation the app identifies that it is running on a mobile browser

  useEffect(() => {
    if (/Mobi/.test(navigator.userAgent)) {
      setIsMobile(true);
    }
  }, []);

and then uses the window.location method to open the transaction in the xumm app.

const handleMobileBrowserSign = async (uuid) => {
    if (isWebApp && isMobile) {
        window.location.href = `https://xumm.app/sign/${uuid}`;
    } 
};

xApp Signing

In the case of running as a xApp, since when xumm is a promise, we need to use the then method to get the response.

xumm.then((xummSDK) => {
    xummSDK.xapp.openSignRequest({ uuid: res.data.uuid });
});    

Then client can then use the websocket to monitor the status of the transaction.

In the example below the API is getting called to create a payment payload on the backend (instead of in the browser) because some backend orchestration needs to be done at time of payment. This is what makes this application "hybrid" becuase payloads are being generated both using the xumm SDK on the front and backends, all protected by the xumm JWT.

PayloadService.getPayment(formState.prompt).then((res) => {  
    console.log("payment response", res.data);
    setStage(1);
    setPayment(res.data);
    setModalTitle('Use xumm wallet to sign the payment transaction');

    const client = new W3CWebSocket(res.data.refs.websocket_status);

    client.onopen = () => {
        console.log('WebSocket Client Connected');
    };

    client.onclose = () => {
        console.log('WebSocket Client Closed');
    };

    client.onmessage = (message) => {
        const dataFromServer = JSON.parse(message.data);

        let keys = Object.keys(dataFromServer);
        if (
            keys.includes('payload_uuidv4') && 
            keys.includes('signed') && 
            dataFromServer.signed === true
        ){
            setStage(-1);
            setModalTitle(`Payment signed, generating image (${remaining+1} remaining)`);
            PayloadService.postGenerate(dataFromServer.payload_uuidv4).then((res) => {
                console.log(res.data);
                setGenerateURL(res.data.img_src);
                setDataFromServer(dataFromServer);
                client.close();
                setStage(2);
                setModalTitle(`Select this image or generate another one (${remaining} remaining)`);
            }).catch((err) => {
                console.log(err);
                setError(err);
            });
        };      
        setWsclient(client);
    }

    if (xumm && isXApp) {
    	xumm.then((xummSDK) => {
        	xummSDK.xapp.openSignRequest({ uuid: res.data.uuid });
	}
    } 

}).catch((err) => {
    console.log(err);
    setError(err);
});

Server Side JWT Backend

verification of xumm JWT

@staticmethod
def verify_jwt(jwt_token, jwks=None, kid="default"):

    if jwks is None:
        raise ValueError("jwks is required")

    jwt_body = jwt.decode(jwt_token, options={"verify_signature": False})

    public_keys = {}
    for jwk in jwks['keys']:
        kid = jwk['kid']
        public_keys[kid] = 
        jwt.algorithms.RSAAlgorithm.from_jwk(
            json.dumps(jwk))
        
    key = public_keys[kid]
    logger.info(f"=== kid {kid} {public_keys} {key}")
    payload = jwt.decode(jwt_token, key, 
        algorithms=['RS256'], audience=jwt_body['aud'],
        issuer='https://oauth2.xumm.app')
    
    logger.info(f"=== payload {payload} VERIFIED")

this method gets called on incoming requests to the backend API

@router.get("/account/info")
@verify_xumm_jwt
async def get_account_info(request: Request,  token: str = Depends(oauth2_scheme)):
    
    jwt_body = get_token_body(token)

    xrp_network = get_xrp_network_from_jwt(jwt_body)
    client = xrpl.clients.JsonRpcClient(xrp_network.json_rpc)

    acct_info = AccountInfo(
        account=jwt_body['sub'],
        ledger_index="current",
        queue=True,
        strict=True,
    )

    response = await client.request_impl(acct_info)
    return response.to_dict()

Setup

running the web app

cd webapp
npm install
npm run build
npm run serve-local

running the uvicorn server

You need to have a .env file in the root of the project with the following contents:

API_VERSION=0.1.1
XUMM_API_KEY="<your_info>"
XUMM_API_SECRET="<your_info>"
NIFTYX_ADDRESS="rrnR8qAP8t..."
BANANA_API_KEY="<your_info>"
BANANA_MODEL_KEY="<your_info>"
AWS_BUCKET_NAME="my-bucket-name"
AWS_UPLOADED_IMAGES_PATH="uploaded_images"
AWS_ACCESS_KEY_ID="<your_info>"
AWS_SECRET_ACCESS_KEY="<your_info>"
AWS_REGION="us-west-2"
PINATA_API_KEY="<your_info>"
PINATA_SECRET_KEY="<your_info>"
PINATA_PINNING_ENDPOINT="https://api.pinata.cloud/pinning/pinJSONToIPFS"
PINATA_OUTDIR="/home/mydir/data/out"
XAPP_PREFIX="http://localhost:3010/xapp"

and then start the uvicorn server

#!/bin/bash
# Usage: runapi.sh <env>
APP_CONFIG=env/$1/api.env python -m api