Skip to content

Commit

Permalink
feat(aa-signers): implemented support for Arcana Auth in the AA signe…
Browse files Browse the repository at this point in the history
…rs package (#319)
  • Loading branch information
mmjee authored Dec 20, 2023
1 parent b0f8dd5 commit c82dbf7
Show file tree
Hide file tree
Showing 19 changed files with 750 additions and 33 deletions.
9 changes: 8 additions & 1 deletion packages/signers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@
"import": "./dist/esm/lit-protocol/index.js",
"default": "./dist/cjs/lit-protocol/index.js"
},
"./arcana-auth": {
"types": "./dist/types/arcana-auth/index.d.ts",
"import": "./dist/esm/arcana-auth/index.js",
"default": "./dist/cjs/arcana-auth/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
Expand Down Expand Up @@ -88,6 +93,7 @@
"@lit-protocol/pkp-ethers": "3.0.24",
"@lit-protocol/types": "3.0.24",
"@lit-protocol/crypto": "3.0.24",
"@arcana/auth": "^1.0.8",
"jsdom": "^22.1.0",
"magic-sdk": "^21.3.0",
"typescript": "^5.0.4",
Expand Down Expand Up @@ -125,6 +131,7 @@
"@lit-protocol/lit-node-client": "3.0.24",
"@lit-protocol/pkp-ethers": "3.0.24",
"@lit-protocol/types": "3.0.24",
"@lit-protocol/crypto": "3.0.24"
"@lit-protocol/crypto": "3.0.24",
"@arcana/auth": "^1.0.8"
}
}
120 changes: 120 additions & 0 deletions packages/signers/src/arcana-auth/__tests__/signer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { AuthProvider } from "@arcana/auth";
import { ArcanaAuthSigner } from "../signer.js";

describe("ArcanaAuth Signer Tests", () => {
it("should correctly get address if authenticated", async () => {
const signer = await givenSigner();

const address = await signer.getAddress();
expect(address).toMatchInlineSnapshot(
'"0x1234567890123456789012345678901234567890"'
);
});

it("should correctly fail to get address if unauthenticated", async () => {
const signer = await givenSigner(false);

const address = signer.getAddress();
await expect(address).rejects.toThrowErrorMatchingInlineSnapshot(
'"Not authenticated"'
);
});

it("should correctly get auth details if authenticated", async () => {
const signer = await givenSigner();

const details = await signer.getAuthDetails();
expect(details).toMatchInlineSnapshot(`
{
"email": "test",
"isMfaEnabled": false,
"issuer": null,
"phoneNumber": "1234567890",
"publicAddress": "0x1234567890123456789012345678901234567890",
"recoveryFactors": [],
}
`);
});

it("should correctly fail to get auth details if unauthenticated", async () => {
const signer = await givenSigner(false);

const details = signer.getAuthDetails();
await expect(details).rejects.toThrowErrorMatchingInlineSnapshot(
'"Not authenticated"'
);
});

it("should correctly sign message if authenticated", async () => {
const signer = await givenSigner();

const signMessage = await signer.signMessage("test");
expect(signMessage).toMatchInlineSnapshot('"0xtest"');
});

it("should correctly fail to sign message if unauthenticated", async () => {
const signer = await givenSigner(false);

const signMessage = signer.signMessage("test");
await expect(signMessage).rejects.toThrowErrorMatchingInlineSnapshot(
'"Not authenticated"'
);
});

it("should correctly sign typed data if authenticated", async () => {
const signer = await givenSigner();

const typedData = {
types: {
Request: [{ name: "hello", type: "string" }],
},
primaryType: "Request",
message: {
hello: "world",
},
};
const signTypedData = await signer.signTypedData(typedData);
expect(signTypedData).toMatchInlineSnapshot('"0xtest"');
});
});

const givenSigner = async (auth = true) => {
const inner = new AuthProvider("EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE");

inner.getUser = vi.fn().mockResolvedValue({
publicAddress: "0x1234567890123456789012345678901234567890",
issuer: null,
email: "test",
phoneNumber: "1234567890",
isMfaEnabled: false,
recoveryFactors: [],
});

vi.spyOn(inner, "provider", "get").mockReturnValue({
request: ({ method }: { method: string; params: any[] }) => {
switch (method) {
case "eth_accounts":
return Promise.resolve([
"0x1234567890123456789012345678901234567890",
]);
case "personal_sign":
return Promise.resolve("0xtest");
case "eth_signTypedData_v4":
return Promise.resolve("0xtest");
default:
return Promise.reject(new Error("Method not found"));
}
},
});

const signer = new ArcanaAuthSigner({ inner });

if (auth) {
await signer.authenticate({
init: () => Promise.resolve(),
connect: () => Promise.resolve(),
});
}

return signer;
};
2 changes: 2 additions & 0 deletions packages/signers/src/arcana-auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ArcanaAuthSigner } from "./signer.js";
export type * from "./types";
97 changes: 97 additions & 0 deletions packages/signers/src/arcana-auth/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
WalletClientSigner,
type SignTypedDataParams,
type SmartAccountAuthenticator,
} from "@alchemy/aa-core";
import {
AuthProvider,
type ConstructorParams,
type UserInfo,
} from "@arcana/auth";
import { createWalletClient, custom, type Hash } from "viem";
import type { ArcanaAuthAuthenticationParams } from "./types";

/**
* This class requires the `@arcana/auth` package as a dependency.
* `@alchemy/aa-signers` lists it as optional dependencies.
*
* @see: https://github.com/arcana-network/auth
*/
export class ArcanaAuthSigner
implements
SmartAccountAuthenticator<
ArcanaAuthAuthenticationParams,
UserInfo,
AuthProvider
>
{
inner: AuthProvider;
private signer: WalletClientSigner | undefined;

constructor(
params:
| { clientId: string; params: Partial<ConstructorParams> }
| { inner: AuthProvider }
) {
if ("inner" in params) {
this.inner = params.inner;
return;
}

this.inner = new AuthProvider(params.clientId, params.params);
}

readonly signerType = "arcana-auth";

getAddress = async () => {
if (!this.signer) throw new Error("Not authenticated");

const address = await this.signer.getAddress();
if (address == null) throw new Error("No address found");

return address as Hash;
};

signMessage = async (msg: Uint8Array | string) => {
if (!this.signer) throw new Error("Not authenticated");

return this.signer.signMessage(msg);
};

signTypedData = (params: SignTypedDataParams) => {
if (!this.signer) throw new Error("Not authenticated");

return this.signer.signTypedData(params);
};

authenticate = async (
params: ArcanaAuthAuthenticationParams = {
init: async () => {
await this.inner.init();
},
connect: async () => {
await this.inner.connect();
},
}
) => {
await params.init();
await params.connect();

if (this.inner.provider == null) throw new Error("No provider found");

this.signer = new WalletClientSigner(
createWalletClient({
transport: custom(this.inner.provider),
}),
this.signerType
);

return this.inner.getUser();
};

getAuthDetails = async () => {
if (!this.signer) throw new Error("Not authenticated");

return this.inner.getUser();
};
}
4 changes: 4 additions & 0 deletions packages/signers/src/arcana-auth/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ArcanaAuthAuthenticationParams {
init: () => Promise<void>;
connect: () => Promise<void>;
}
4 changes: 4 additions & 0 deletions packages/signers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ export {
type LitConfig,
type LitAuthenticateProps,
} from "./lit-protocol/index.js";
export {
ArcanaAuthSigner,
type ArcanaAuthAuthenticationParams,
} from "./arcana-auth/index.js";
14 changes: 14 additions & 0 deletions site/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,20 @@ export default defineConfig({
text: "Getting Started",
link: "/",
},
{
text: "Arcana Auth Signer",
collapsed: true,
base: "/packages/aa-signers/arcana-auth",
items: [
{ text: "Introduction", link: "/introduction" },
{ text: "constructor", link: "/constructor" },
{ text: "authenticate", link: "/authenticate" },
{ text: "getAddress", link: "/getAddress" },
{ text: "signMessage", link: "/signMessage" },
{ text: "signTypedData", link: "/signTypedData" },
{ text: "getAuthDetails", link: "/getAuthDetails" },
],
},
{
text: "Magic Signer",
collapsed: true,
Expand Down
65 changes: 65 additions & 0 deletions site/packages/aa-signers/arcana-auth/authenticate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
outline: deep
head:
- - meta
- property: og:title
content: ArcanaAuthSigner • authenticate
- - meta
- name: description
content: Overview of the authenticate method on ArcanaAuthSigner
- - meta
- property: og:description
content: Overview of the authenticate method on ArcanaAuthSigner
---

# authenticate

`authenticate` is a method on the `ArcanaAuthSigner` that leverages the `Arcana` Auth Web SDK to authenticate a user.

This method must be called before accessing the other methods available on the `ArcanaAuthSigner`, such as signing messages or typed data or accessing user details.

## Usage

::: code-group

```ts [example.ts]
// [!code focus:99]
import { ArcanaAuthSigner } from "@alchemy/aa-signers";
// Register app through Arcana Developer Dashboard to get clientId
// ARCANA_AUTH_CLIENTID = "xar_live_nnnnnnnnnn"
const newArcanaAuthSigner = new ArcanaAuthSigner({
clientId: ARCANA_AUTH_CLIENTID,
});
// or
// import { AuthProvider } from "@arcana/auth";
// const inner = new AuthProvider ("xar_live_nnnn");
// const newArcanaAuthSigner = new ArcanaAuthSigner({inner});
const getUserInfo = await newArcanaAuthSigner.authenticate();
```

:::

## Returns

### `Promise<UserInfo>`

A Promise containing the `UserInfo`, an object with the following fields:

- `address: string | null` -- the EoA account address associated with the authenticated user's wallet.
- `email: string | null` -- email address of the authenticated user.
- `id: string | null` -- the decentralized user identifier.
- `loginToken: string` -- JWT token returned after the user authenticates.
- `loginType: Logins | passwordless` -- login provider type or passwordless used for authentication (ex. `[{ Logins: "google" | "github" | "discord" | "twitch" | "twitter" | "aws" | "firebase" | "steam" }]`).
- `name: string` -- user name associated with the email id
- `picture: string` -- url pointing to the user profile image
- `publicKey: string` -- public key associated with the user account

See [Arcana Auth SDK Reference Guide](https://authsdk-ref-guide.netlify.app/interfaces/userinfo) for details.

## Parameters

### `clientId`: Unique app identifier assigned after app registration via the Arcana Developer Dashboard

or

### `inner`: An AuthProvifer object. For field details, see [AuthProvider constructor](https://authsdk-ref-guide.netlify.app/classes/authprovider#constructor).
Loading

0 comments on commit c82dbf7

Please sign in to comment.