Skip to content

Commit

Permalink
feat: add magic signer to aa-signers (#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
avasisht23 authored and moldy530 committed Dec 1, 2023
1 parent 66abca6 commit 860d177
Show file tree
Hide file tree
Showing 18 changed files with 681 additions and 25 deletions.
4 changes: 4 additions & 0 deletions packages/signers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
},
"devDependencies": {
"jsdom": "^22.1.0",
"magic-sdk": "^21.3.0",
"typescript": "^5.0.4",
"typescript-template": "*",
"vitest": "^0.31.0"
Expand All @@ -60,5 +61,8 @@
"dependencies": {
"@alchemy/aa-core": "^1.2.0",
"viem": "^1.16.2"
},
"optionalDependencies": {
"magic-sdk": "^21.3.0"
}
}
5 changes: 0 additions & 5 deletions packages/signers/src/__tests__/signer.test.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/signers/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MagicSigner, type MagicAuthParams } from "./magic/index.js";
119 changes: 119 additions & 0 deletions packages/signers/src/magic/__tests__/signer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Magic } from "magic-sdk";
import { MagicSigner } from "../signer.js";

describe("Magic 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 Magic("test");

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

inner.wallet.getProvider = vi.fn().mockResolvedValue({
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 MagicSigner({ inner });

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

return signer;
};
2 changes: 2 additions & 0 deletions packages/signers/src/magic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { MagicSigner } from "./signer.js";
export type * from "./types.js";
73 changes: 73 additions & 0 deletions packages/signers/src/magic/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
WalletClientSigner,
type SignTypedDataParams,
type SmartAccountAuthenticator,
} from "@alchemy/aa-core";
import { Magic, type MagicUserMetadata } from "magic-sdk";
import { createWalletClient, custom, type Hash } from "viem";
import type { MagicAuthParams, MagicSDKParams } from "./types.js";

/**
* This class requires the `magic-sdk` dependency.
* `@alchemy/aa-signers` lists it as an optional dependency.
*
* @see: https://github.com/magiclabs/magic-js)
*/
export class MagicSigner
implements
SmartAccountAuthenticator<MagicAuthParams, MagicUserMetadata, Magic>
{
inner: Magic;
private signer: WalletClientSigner | undefined;

constructor(params: MagicSDKParams | { inner: Magic }) {
if ("inner" in params) {
this.inner = params.inner;
return;
}

this.inner = new Magic(params.apiKey, params.options);
}

readonly signerType = "magic";

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

const address = (await this.inner.user.getInfo()).publicAddress;
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: MagicAuthParams) => {
await params.authenticate();

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

return this.inner.user.getInfo();
};

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

return this.inner.user.getInfo();
};
}
16 changes: 16 additions & 0 deletions packages/signers/src/magic/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {
MagicSDKAdditionalConfiguration,
MagicSDKExtensionsOption,
} from "magic-sdk";

export interface MagicAuthParams {
authenticate: () => Promise<void>;
}

export type MagicSDKParams = {
apiKey: string;
options?: MagicSDKAdditionalConfiguration<
string,
MagicSDKExtensionsOption<string>
>;
};
14 changes: 14 additions & 0 deletions site/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,20 @@ export default defineConfig({
text: "Getting Started",
link: "/",
},
{
text: "Magic Signer",
collapsed: true,
base: "/packages/aa-signers/magic",
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: "Contributing", link: "/contributing" },
],
},
Expand Down
60 changes: 60 additions & 0 deletions site/packages/aa-signers/magic/authenticate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
outline: deep
head:
- - meta
- property: og:title
content: MagicSigner • authenticate
- - meta
- name: description
content: Overview of the authenticate method on MagicSigner
- - meta
- property: og:description
content: Overview of the authenticate method on MagicSigner
---

# authenticate

`authenticate` is a method on the `MagicSigner` which leverages the `Magic` web SDK to authenticate a user.

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

## Usage

::: code-group

```ts [example.ts]
// [!code focus:99]
import { MagicSigner } from "@alchemy/aa-signers";

const magicSigner = new MagicSigner({ apiKey: MAGIC_API_KEY });
const authParams = {
authenticate: async () => {
await magicSigner.inner.wallet.connectWithUI();
},
};

await magicSigner.authenticate(authParams);
```

:::

## Returns

### `Promise<MagicUserMetadata>`

A Promise containing the `MagicUserMetadata`, and object with the following fields:

- `issuer: string | null` -- the Decentralized ID of the user.
- `publicAddress: string | null` -- the authenticated user's public address (EOA public key).
- `email: string | null` -- email address of the authenticated user.
- `phoneNumber: string | null` -- phone number of the authenticated user.
- `isMfaEnabled: boolean` -- whether or not multi-factor authentication is enabled for the user.
- `recoveryFactors: RecoveryFactor[]` -- any recovery methods that have been enabled (ex. `[{ type: 'phone_number', value: '+99999999' }]`).

## Parameters

### `authParams: <MagicAuthParams>`

An object with the following fields:

- `authenticate: () => Promise<void>` -- a method you can define as necessary to leverage the `Magic` SDK for authentication. For instance, in the example above, `authenticate` uses the [`connectWithUI`](https://magic.link/docs/api/client-side-sdks/web#connectwithui) method.
64 changes: 64 additions & 0 deletions site/packages/aa-signers/magic/constructor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
outline: deep
head:
- - meta
- property: og:title
content: MagicSigner • constructor
- - meta
- name: description
content: Overview of the constructor method on MagicSigner in aa-signers
- - meta
- property: og:description
content: Overview of the constructor method on MagicSigner in aa-signers
---

# constructor

To initialize a `MagicSigner`, you must provide a set of parameters detailed below.

## Usage

::: code-group

```ts [example.ts]
import { MagicSigner } from "@alchemy/aa-signers";

// instantiates using every possible parameter, as a reference
const magicSigner = new MagicSigner({
apiKey: "MAGIC_API_KEY",
options: {
endpoint: "MAGIC_IFRAME_URL",
locale: "en_US",
network: "sepolia",
testMode: false,
},
});
```

:::

## Returns

### `MagicSigner`

A new instance of a `MagicSigner`.

## Parameters

### `params: MagicSDKParams | { inner: Magic }`

You can either pass in a constructed `Magic` object, or directly pass into the `MagicSigner` the `MagicSDKParams` used to construct a `Magic` object. These parameters are listed on the [Magic Docs](https://magic.link/docs/api/client-side-sdks/web#constructor-NaN) as well.

`MagicSDKParams` takes in the following parameters:

- `apiKey: string` -- a Magic API Key. You can get one at [Magic Dashboard](https://dashboard.magic.link/).

- `options: MagicSDKAdditionalConfiguration` -- [optional]

- `endpoint: string` -- [optional] a URL pointing to the Magic `<iframe` application.

- `locale: string` -- [optional] customize the language of Magic's modal, email and confirmation screen.

- `network: string` -- [optional] a representation of the connected Ethereum network (mainnet or goerli).

- `testMode: boolean` -- [optional] toggle the login behavior to not have to go through the auth flow.

0 comments on commit 860d177

Please sign in to comment.