/socialname

Primary LanguageTypeScript

Integrate Rebase

This section integrates the functionality to generate a credential via Rebase.

A completed version of this part can be found in the example repository (03_rebase).

In this step, you must install Rebase as a new dependency, as it is not yet supported by SSX. Run the following to add the dependency and create the new component file:

npm i @spruceid/rebase-client@^0.16.2 ethers@5.7.2
mkdir utils
touch utils/rebase.ts \
      components/RebaseCredentialComponent.tsx

You need to update the NextConfig to support WebAssembly. Add the following to my-app/next.config.js file:

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config) => {
    config.externals.push({
      'utf-8-validate': 'commonjs utf-8-validate',
      'bufferutil': 'commonjs bufferutil',
    });
    config.experiments.asyncWebAssembly = true;
    return config
  },
}

module.exports = nextConfig

Add the following to my-app/utils/rebase.ts to add some util methods of Rebase to the project:

export interface BasicPostCredential {
  type: "BasicPostAttestation", // The URN of the UUID of the credential.
  id: string, // The DID of the user who is the credential subject, comes from the VC.credentialSubject.id
  subject: string,
  title: string,
  body: string,
}

export const encode = (c: string): string => {
  return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
};

export const parseJWT = (jwt_str: string): any => {
  const v = jwt_str.split('.');
  if (v.length !== 3) throw new Error('Invalid JWT format');
  const u = v[1];
  const b64 = u.replace(/-/g, '+').replace(/_/g, '/');
  const encoded = atob(b64).split('').map(encode).join('');
  const json_str = decodeURIComponent(encoded);
  return JSON.parse(json_str);
};

export const toCredentialContent = (jwt_str: string): Record<string, any> | void => {
  const parsed = parseJWT(jwt_str);
  const vc = parsed?.vc;
  if (!vc) throw new Error('Malformed jwt, no vc property');
  const t = vc?.type;
  if (!t) throw new Error('Malformed credential, no type property');
  if (t.length !== 2) throw new Error('Malformed credential, type property did not have length of 2');
  const credType = t[1];
  const credID = vc?.id;
  if (!credID) throw new Error('No id property found under vc property in JWT credential');
  const subjID = vc?.credentialSubject?.id;
  if (!subjID) throw new Error('No id property found under vc.credentialSubject property in JWT credential');
  const issuanceDate = vc?.issuanceDate;
  if (!issuanceDate) throw new Error('No issuanceDate property found under vc property in JWT credential');
  const c = {
    type: credType,
    id: credID,
    subject: subjID,
    issuanceDate
  };
  switch (credType) {
    case "BasicPostAttestation": {
      let next = {
        title: getCredSubjProp("title", vc),
        body: getCredSubjProp("body", vc)
      };
      try {
        let reply_to = getCredSubjProp("reply_to", vc);
        next = Object.assign({}, next, { reply_to });
      } catch (_e) { }
      return Object.assign({}, c, next) as BasicPostCredential;
    }
    default:
      return vc;
  }
};

export const toCredentialEntry = (jwt_str: string): Record<string, any> => {
  const content = toCredentialContent(jwt_str);
  return { jwt: jwt_str, content: content };
};

const getCredSubjProp = (prop: string, vc: any): any => {
  let x = vc?.credentialSubject[prop];
  if (!x) throw new Error(`No ${prop} property found under vc.credentialSubject property in JWT credential`)
  return x;
}

Then, add the following into my-app/components/RebaseCredentialComponent.tsx:

"use client";
import { toCredentialEntry } from "@/utils/rebase";
import { SSX } from "@spruceid/ssx";
import { ethers } from "ethers";
import { useEffect, useState } from "react";
import { defaultClientConfig, type Types } from '@spruceid/rebase-client';
import type { AttestationProof, AttestationStatement } from '@spruceid/rebase-client/bindings';

interface IRebaseCredentialComponent {
  ssx: SSX;
}

const RebaseCredentialComponent = ({ ssx }: IRebaseCredentialComponent) => {
  const [rebaseClient, setRebaseClient] = useState<any>();
  const [signer, setSigner] = useState<ethers.Signer>();
  const [title, setTitle] = useState<string>('');
  const [body, setBody] = useState<string>('');
  const [loading, setLoading] = useState<boolean>(false);
  const [credentialList, setCredentialList] = useState<Array<string>>([]);
  const [viewingContent, setViewingContent] = useState<string | null>(null);

  useEffect(() => {
    getContentList();
    createClient();
    createSigner();
  }, []);

  const getContentList = async () => {
    setLoading(true);
    let { data } = await ssx.storage.list();
    data = data.filter((d: string) => d.includes('/credentials/'))
    setCredentialList(data);
    setLoading(false);
  };

  const createClient = async () => {
    const Client = (await import('@spruceid/rebase-client')).Client;
    const WasmClient = (await import('@spruceid/rebase-client/wasm')).WasmClient;
    setRebaseClient(new Client(new WasmClient(JSON.stringify(defaultClientConfig()))))
  };

  const createSigner = async () => {
    const ethSigner = await ssx.getSigner();
    setSigner(ethSigner);
  };

  const toSubject = () => {
    return {
      pkh: {
        eip155: {
          address: ssx.address(),
          chain_id: '1'
        }
      }
    }
  };

  const sanityCheck = () => {
    if (!rebaseClient) throw new Error('Rebase client is not configured');
    if (!signer) throw new Error('Signer is not connected');
  };

  const statement = async (credentialType: Types.AttestationTypes, content: any): Promise<string> => {
    sanityCheck();
    const o = {};
    (o as any)[credentialType] = Object.assign({ subject: toSubject() }, content);
    const req: Types.Statements = {
      Attestation: o as AttestationStatement
    };
    const resp = await rebaseClient?.statement(req);
    if (!resp?.statement) {
      throw new Error('No statement found in witness response');
    }
    return resp.statement;
  };

  const witness = async (
    credentialType: Types.AttestationTypes,
    content: any,
    signature: string
  ): Promise<string> => {
    sanityCheck();
    const o = {};
    (o as any)[credentialType] = {
      signature,
      statement: Object.assign({ subject: toSubject() }, content)
    };
    const req: Types.Proofs = {
      Attestation: o as AttestationProof
    };
    const resp = await rebaseClient?.witness_jwt(req);
    if (!resp?.jwt) {
      throw new Error('No jwt found in witness response');
    }
    return resp.jwt;
  };

  const issue = async () => {
    setLoading(true);
    try {
      const fileName = 'credentials/post_' + Date.now();
      const credentialType = 'BasicPostAttestation';
      const content = {
        title,
        body
      }
      const stmt = await statement(credentialType, content);
      const sig = (await signer?.signMessage(stmt)) ?? '';
      const jwt_str = await witness(credentialType, content, sig);
      await ssx.storage.put(fileName, jwt_str);
      setCredentialList((prevList) => [...prevList, `my-app/${fileName}`]);
    } catch (e) {
      console.error(e);
    }
    setLoading(false);
  };

  const handleGetContent = async (content: string) => {
    setLoading(true);
    try {
      const contentName = content.replace('my-app/', '')
      const { data } = await ssx.storage.get(contentName);
      setViewingContent(`${content}:\n${JSON.stringify(toCredentialEntry(data), null, 2)}`);
    } catch (e) {
      console.error(e);
    }
    setLoading(false);
  };

  const handleDeleteContent = async (content: string) => {
    setLoading(true);
    const contentName = content.replace('my-app/', '')
    await ssx.storage.delete(contentName);
    setCredentialList((prevList) => prevList.filter((c) => c !== content));
    setLoading(false);
  };

  return (
    <div style={{ marginTop: 25 }}>
      <h2>Rebase</h2>
      <p>Input data for credential issuance</p>
      <p style={{ maxWidth: 500, fontSize: 12 }}>
        You can issue a BasicPostAttestation by filling the fields and clicking the button bellow.
        Title and body can be any string.
      </p>
      <input
        type="text"
        placeholder="Title"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        disabled={loading}
      />
      <br />
      <input
        type="text"
        placeholder="Body"
        value={body}
        onChange={(e) => setBody(e.target.value)}
        disabled={loading}
      />
      <br />
      <button
        onClick={issue}
        disabled={loading}
        style={{ marginTop: 15 }}
      >
        <span>
          ISSUE AND POST
        </span>
      </button>
      <p><b>My credentials</b></p>
      <table>
        <tbody>
          {credentialList?.map((content, i) => <tr key={i}>
            <td>
              {content}
            </td>
            <td>
              <button
                onClick={() => handleGetContent(content)}
                disabled={loading}
              >
                <span>
                  GET
                </span>
              </button>
            </td>
            <td>
              <button
                onClick={() => handleDeleteContent(content)}
                disabled={loading}
              >
                <span>
                  DELETE
                </span>
              </button>
            </td>
          </tr>)}
        </tbody>
      </table>
      <pre style={{ marginTop: 25 }}>
        {viewingContent}
      </pre>
    </div>
  );
}

export default RebaseCredentialComponent;

Now update the SSXComponent to import the credential component module by adding the following into my-app/components/SSXComponent.tsx:

"use client";
import { SSX } from "@spruceid/ssx";
import { useState } from "react";
import KeplerStorageComponent from "./KeplerStorageComponent";
import RebaseCredentialComponent from "./RebaseCredentialComponent";

const SSXComponent = () => {

  const [ssxProvider, setSSX] = useState<SSX | null>(null);

  const ssxHandler = async () => {
    const ssx = new SSX({
      providers: {
        server: {
          host: "http://localhost:3000/api"
        }
      },
      modules: {
        storage: {
          prefix: 'my-app',
          hosts: ['https://kepler.spruceid.xyz'],
          autoCreateNewOrbit: true
        }
      }
    });
    await ssx.signIn();
    setSSX(ssx);
  };

  const ssxLogoutHandler = async () => {
    ssxProvider?.signOut();
    setSSX(null);
  };

  const address = ssxProvider?.address() || '';

  return (
    <>
      <h2>User Authorization Module</h2>
      <p>Authenticate and Authorize using your ETH keys</p>
      {
        ssxProvider ?
          <>
            {
              address &&
              <p>
                <b>Ethereum Address:</b> <code>{address}</code>
              </p>
            }
            <br />
            <button onClick={ssxLogoutHandler}>
              <span>
                Sign-Out
              </span>
            </button>
            <br />
            <KeplerStorageComponent ssx={ssxProvider} />
            <br />
            <RebaseCredentialComponent ssx={ssxProvider} />
          </> :
          <button onClick={ssxHandler}>
            <span>
              Sign-In with Ethereum
            </span>
          </button>
      }
    </>
  );
};

export default SSXComponent;

Finally, you can run the app by using:

npm run dev