-
Notifications
You must be signed in to change notification settings - Fork 125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add portal to aa-signers #303
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ab11d5b
feat: add portal to aa-signers
avasisht23 c0f9dd1
feat: add portal to aa-signers
avasisht23 dacceee
feat: add portal to aa-signers
avasisht23 3c3fd1c
feat: add portal to aa-signers
avasisht23 cadd82f
feat: add portal to aa-signers
avasisht23 25ef3af
feat: bump portal version to build
avasisht23 926fd84
feat: remove unneeded check
avasisht23 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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,130 @@ | ||
import Portal from "@portal-hq/web"; | ||
import { sepolia } from "viem/chains"; | ||
import { PortalSigner } from "../signer.js"; | ||
|
||
// taken from Portal SDK since not exported | ||
interface RequestArguments { | ||
method: string; | ||
params?: unknown[]; | ||
} | ||
|
||
describe("Portal Signer Tests", () => { | ||
it("should correctly get address", 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", async () => { | ||
const signer = await givenSigner(); | ||
|
||
const details = await signer.getAuthDetails(); | ||
expect(details).toMatchInlineSnapshot(` | ||
{ | ||
"address": "0x1234567890123456789012345678901234567890", | ||
"backupStatus": null, | ||
"custodian": { | ||
"id": "1", | ||
"name": "test", | ||
}, | ||
"id": "0", | ||
"signingStatus": null, | ||
} | ||
`); | ||
}); | ||
|
||
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 Portal({ | ||
autoApprove: true, | ||
gatewayConfig: `${sepolia.rpcUrls.alchemy.http}/${process.env.ALCHEMY_API_KEY}`, | ||
chainId: sepolia.id, | ||
}); | ||
|
||
inner.getClient = vi.fn().mockResolvedValue({ | ||
id: "0", | ||
address: "0x1234567890123456789012345678901234567890", | ||
backupStatus: null, | ||
custodian: { | ||
id: "1", | ||
name: "test", | ||
}, | ||
signingStatus: null, | ||
}); | ||
|
||
inner.provider.request = vi.fn(async <R>(args: RequestArguments) => { | ||
switch (args.method) { | ||
case "eth_accounts": | ||
return Promise.resolve([ | ||
"0x1234567890123456789012345678901234567890", | ||
]) as R; | ||
case "personal_sign": | ||
return Promise.resolve("0xtest") as R; | ||
case "eth_signTypedData_v4": | ||
return Promise.resolve("0xtest") as R; | ||
default: | ||
return Promise.reject(new Error("Method not found")); | ||
} | ||
}); | ||
|
||
const signer = new PortalSigner({ inner }); | ||
|
||
if (auth) { | ||
await signer.authenticate(); | ||
} | ||
|
||
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 { PortalSigner } 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,75 @@ | ||
import { | ||
WalletClientSigner, | ||
type SignTypedDataParams, | ||
type SmartAccountAuthenticator, | ||
} from "@alchemy/aa-core"; | ||
import Portal, { type PortalOptions } from "@portal-hq/web"; | ||
import { createWalletClient, custom, type Hash } from "viem"; | ||
import type { PortalAuthenticationParams, PortalUserInfo } from "./types.js"; | ||
|
||
/** | ||
* This class requires the `@portal-hq/web` dependency. | ||
* `@alchemy/aa-signers` lists it as an optional dependency. | ||
* | ||
* @see: https://docs.portalhq.io/sdk/web-beta | ||
*/ | ||
export class PortalSigner | ||
implements | ||
SmartAccountAuthenticator< | ||
PortalAuthenticationParams, | ||
PortalUserInfo, | ||
Portal | ||
> | ||
{ | ||
inner: Portal; | ||
private signer: WalletClientSigner | undefined; | ||
|
||
constructor(params: PortalOptions | { inner: Portal }) { | ||
if ("inner" in params) { | ||
this.inner = params.inner; | ||
return; | ||
} | ||
|
||
this.inner = new Portal(params); | ||
} | ||
|
||
readonly signerType = "portal"; | ||
|
||
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 () => { | ||
this.signer = new WalletClientSigner( | ||
createWalletClient({ | ||
transport: custom(this.inner.provider), | ||
}), | ||
this.signerType | ||
); | ||
|
||
return this.inner.getClient() as Promise<PortalUserInfo>; | ||
}; | ||
|
||
getAuthDetails = async () => { | ||
if (!this.signer) throw new Error("Not authenticated"); | ||
|
||
return this.inner.getClient() as Promise<PortalUserInfo>; | ||
}; | ||
} |
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,15 @@ | ||
import type { Address } from "viem"; | ||
|
||
export interface PortalAuthenticationParams {} | ||
|
||
// taken from Portal SDK since not exported | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can export this for y'all! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll queue up an update. |
||
export type PortalUserInfo = { | ||
id: string; | ||
address: Address; | ||
backupStatus?: string | null; | ||
custodian: { | ||
id: string; | ||
name: string; | ||
}; | ||
signingStatus?: string | null; | ||
}; |
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,54 @@ | ||
--- | ||
outline: deep | ||
head: | ||
- - meta | ||
- property: og:title | ||
content: PortalSigner • authenticate | ||
- - meta | ||
- name: description | ||
content: Overview of the authenticate method on PortalSigner | ||
- - meta | ||
- property: og:description | ||
content: Overview of the authenticate method on PortalSigner | ||
--- | ||
|
||
# authenticate | ||
|
||
`authenticate` is a method on the `PortalSigner` which leverages the `Portal` SDK to authenticate a user. | ||
|
||
You must call this method before accessing the other methods available on the `PortalSigner`, such as signing messages or typed data or accessing user details. | ||
|
||
## Usage | ||
|
||
::: code-group | ||
|
||
```ts [example.ts] | ||
// [!code focus:99] | ||
import { PortalSigner } from "@alchemy/aa-signers"; | ||
|
||
const portalSigner = new PortalSigner({ | ||
autoApprove: true, | ||
gatewayConfig: `${sepolia.rpcUrls.alchemy.http}/${process.env.ALCHEMY_API_KEY}`, | ||
chainId: sepolia.id, | ||
}); | ||
|
||
await portalSigner.authenticate(); | ||
``` | ||
|
||
::: | ||
|
||
## Returns | ||
|
||
### `Promise<PortalUserInfo>` | ||
|
||
A Promise containing the `PortalUserInfo`, an object with the following fields: | ||
|
||
- `id: string` -- ID of the Portal Signer. | ||
- `address: string` -- EOA address of the Portal Signer. | ||
- `backupStatus: string | null` -- [optional] status of wallet backup. | ||
- `custodian: Obect` -- [optional] EOA address of the Portal Signer. | ||
- `id: string` -- ID of the Signer's custodian. | ||
- `name: string` -- Name of the Signer's custodian. | ||
- `signingStatus: string | null` -- [optional] status of signing. | ||
|
||
This derives from the return type of a Portal provider's `getClient()` method. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just create this in the constructor and have authenticate return
this.getAuthDetails
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just for convention, because we want the dev to call
authenticate
before calling getAddress, sendMessage, etc. like is necessary with the other signers