-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(aa-signers): implemented support for Arcana Auth in the AA signe…
…rs package (#319)
- Loading branch information
Showing
19 changed files
with
750 additions
and
33 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
120 changes: 120 additions & 0 deletions
120
packages/signers/src/arcana-auth/__tests__/signer.test.ts
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,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; | ||
}; |
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 { ArcanaAuthSigner } from "./signer.js"; | ||
export type * from "./types"; |
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,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(); | ||
}; | ||
} |
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,4 @@ | ||
export interface ArcanaAuthAuthenticationParams { | ||
init: () => Promise<void>; | ||
connect: () => Promise<void>; | ||
} |
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
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,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). |
Oops, something went wrong.