-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add magic signer to aa-signers (#229)
- Loading branch information
1 parent
66abca6
commit 860d177
Showing
18 changed files
with
681 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { MagicSigner, type MagicAuthParams } from "./magic/index.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { MagicSigner } from "./signer.js"; | ||
export type * from "./types.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.