Skip to content

Commit

Permalink
feat: add fordefi signer to aa-signers (#665)
Browse files Browse the repository at this point in the history
* feat: add fordefi signer to aa-signers

* fix: use @fordefi/web3-provider v0.2.0

* fix: remove user info from docs

* fix: apply adjustments pushed to the PR that targeted `development`

* docs: fix wrong markdown, casing and code block closing

* docs: add Fordefi to examples of  MPC wallets

* fix: remove new viem version from lock file

* docs: add jsdoc and remove void type aliases

---------

Co-authored-by: Gil Meir <[email protected]>
  • Loading branch information
gilmeir-arnac and gilmeir-arnac authored May 21, 2024
1 parent e8444f9 commit 7673fc5
Show file tree
Hide file tree
Showing 19 changed files with 763 additions and 2 deletions.
7 changes: 7 additions & 0 deletions packages/signers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
"import": "./dist/esm/passport/index.js",
"default": "./dist/cjs/passport/index.js"
},
"./fordefi": {
"types": "./dist/types/fordefi/index.d.ts",
"import": "./dist/esm/fordefi/index.js",
"default": "./dist/cjs/fordefi/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
Expand All @@ -90,6 +95,7 @@
"@0xpass/webauthn-signer": "^1.0.0",
"@arcana/auth": "^1.0.8",
"@fireblocks/fireblocks-web3-provider": "^1.2.6",
"@fordefi/web3-provider": "^0.2.0",
"@lit-protocol/crypto": "3.0.24",
"@lit-protocol/lit-node-client": "3.0.24",
"@lit-protocol/pkp-ethers": "3.0.24",
Expand Down Expand Up @@ -132,6 +138,7 @@
"optionalDependencies": {
"@arcana/auth": "^1.0.8",
"@fireblocks/fireblocks-web3-provider": "^1.2.6",
"@fordefi/web3-provider": "^0.2.0",
"@lit-protocol/crypto": "3.0.24",
"@lit-protocol/lit-node-client": "3.0.24",
"@lit-protocol/pkp-ethers": "3.0.24",
Expand Down
127 changes: 127 additions & 0 deletions packages/signers/src/fordefi/__tests__/signer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
type FordefiMethodName,
type FordefiRpcSchema,
FordefiWeb3Provider,
type MethodReturnType,
type RequestArgs,
} from "@fordefi/web3-provider";
import { numberToHex } from "viem";
import { FordefiSigner } from "../signer.js";

const fixtures = {
address: "0x1234567890123456789012345678901234567890",
chainId: 11155111,
message: "test",
signedMessage: "0xtest",
apiUserToken: "123-456",
} as const;

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

const address = await signer.getAddress();
expect(address).toMatchInlineSnapshot(`"${fixtures.address}"`);
});

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).toBeUndefined();
});

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(fixtures.message);
expect(signMessage).toMatchInlineSnapshot(`"${fixtures.signedMessage}"`);
});

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

const signMessage = signer.signMessage(fixtures.message);
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(`"${fixtures.signedMessage}"`);
});
});

const givenSigner = async (auth = true) => {
FordefiWeb3Provider.prototype.request = vi.fn((async <
M extends FordefiMethodName
>(
args: RequestArgs<FordefiRpcSchema, M>
) => {
switch (args.method) {
case "eth_accounts":
return Promise.resolve([fixtures.address]) as Promise<
MethodReturnType<FordefiRpcSchema, "eth_accounts">
>;
case "eth_chainId":
return Promise.resolve(numberToHex(fixtures.chainId)) as Promise<
MethodReturnType<FordefiRpcSchema, "eth_chainId">
>;
case "personal_sign":
return Promise.resolve(fixtures.signedMessage) as Promise<
MethodReturnType<FordefiRpcSchema, "personal_sign">
>;
case "eth_signTypedData_v4":
return Promise.resolve(fixtures.signedMessage) as Promise<
MethodReturnType<FordefiRpcSchema, "eth_signTypedData_v4">
>;
default:
return Promise.reject(new Error("Method not found"));
}
}) as FordefiWeb3Provider["request"]);

const inner = new FordefiWeb3Provider({
chainId: fixtures.chainId,
address: fixtures.address,
apiUserToken: fixtures.apiUserToken,
apiPayloadSignKey: "fakeApiKey",
});

const signer = new FordefiSigner({ inner });

if (auth) {
await signer.authenticate();
}

return signer;
};
1 change: 1 addition & 0 deletions packages/signers/src/fordefi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FordefiSigner } from "./signer.js";
121 changes: 121 additions & 0 deletions packages/signers/src/fordefi/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
WalletClientSigner,
type SmartAccountAuthenticator,
} from "@alchemy/aa-core";
import {
type FordefiProviderConfig,
FordefiWeb3Provider,
} from "@fordefi/web3-provider";
import {
createWalletClient,
custom,
type Hash,
type SignableMessage,
type TypedData,
type TypedDataDefinition,
} from "viem";
import { signerTypePrefix } from "../constants.js";

/**
* This class requires the `@fordefi/web3-provider` dependency.
* `@alchemy/aa-signers` lists it as optional dependency.
*
* @see https://github.com/FordefiHQ/web3-provider
*/
export class FordefiSigner
implements SmartAccountAuthenticator<void, void, FordefiWeb3Provider>
{
inner: FordefiWeb3Provider;
private signer: WalletClientSigner | undefined;

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

this.inner = new FordefiWeb3Provider(params);
}

readonly signerType = `${signerTypePrefix}fordefi`;

/**
* Returns the address managed by this signer.
*
* @returns the address managed by this signer
* @throws if the provider is not authenticated, or if the address was not found
*/
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 satisfies Hash;
};

/**
* Signs a message with the authenticated account.
*
* @param msg the message to sign
* @returns the address of the authenticated account
* @throws if the provider is not authenticated
*/
signMessage = async (msg: SignableMessage) => {
if (!this.signer) throw new Error("Not authenticated");

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

/**
* Signs a typed data object with the authenticated account.
*
* @param params the data object to sign
* @returns the signed data as a hex string
* @throws if the provider is not authenticated
*/
signTypedData = async <
const TTypedData extends TypedData | { [key: string]: unknown },
TPrimaryType extends string = string
>(
params: TypedDataDefinition<TTypedData, TPrimaryType>
) => {
if (!this.signer) throw new Error("Not authenticated");

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

/**
* Authenticates with the Fordefi platform and verifies that this client
* is authorized to manage the account.
* This step is required before any signing operations can be performed.
*
* @returns void
* @throws if no provider was found, or if authentication failed
*/
authenticate = async (): Promise<void> => {
if (this.inner == null) throw new Error("No provider found");

await this.inner.connect();

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

return this.getAuthDetails();
};

/**
* Verifies that this signer is authenticated, and throws an error otherwise.
* Authentication details are not available.
*
* @returns void
* @throws Error if this signer is not authenticated
*/
getAuthDetails = async (): Promise<void> => {
if (!this.signer) throw new Error("Not authenticated");
};
}
1 change: 1 addition & 0 deletions packages/signers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ export {
ArcanaAuthSigner,
type ArcanaAuthAuthenticationParams,
} from "./arcana-auth/index.js";
export { FordefiSigner } from "./fordefi/index.js";
1 change: 1 addition & 0 deletions site/.vitepress/sidebar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export const sidebar: DefaultTheme.Sidebar = [
{ text: "Dfns", link: "/dfns" },
{ text: "WalletKit", link: "/walletkit" },
{ text: "Passport", link: "/passport" },
{ text: "Fordefi", link: "/fordefi" },
],
},
{ text: "EOA signer", link: "/eoa" },
Expand Down
14 changes: 14 additions & 0 deletions site/.vitepress/sidebar/packages/aa-signers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,20 @@ export const aaSignersSidebar: DefaultTheme.SidebarItem = {
{ text: "getAuthDetails", link: "/getAuthDetails" },
],
},
{
text: "Fordefi Signer",
collapsed: true,
base: "/packages/aa-signers/fordefi",
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" },
],
};
39 changes: 39 additions & 0 deletions site/packages/aa-signers/fordefi/authenticate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
outline: deep
head:
- - meta
- property: og:title
content: FordefiSigner • authenticate
- - meta
- name: description
content: Overview of the authenticate method on FordefiSigner
- - meta
- property: og:description
content: Overview of the authenticate method on FordefiSigner
---

# authenticate

`authenticate` is a method on the `FordefiSigner` which leverages the `Fordefi` provider to authenticate a user.

You must call this method before accessing the other methods available on the `FordefiSigner`, such as signing messages or typed data or accessing user details.

## Usage

::: code-group

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

const fordefiSigner = new FordefiSigner({
chainId: 11155111,
address: "0x1234567890123456789012345678901234567890",
apiUserToken: process.env.FORDEFI_API_USER_TOKEN!,
apiPayloadSignKey: process.env.FORDEFI_API_PAYLOAD_SIGNING_KEY!,
});

await fordefiSigner.authenticate();
```

:::
Loading

0 comments on commit 7673fc5

Please sign in to comment.