-
Notifications
You must be signed in to change notification settings - Fork 143
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add fordefi signer to aa-signers (#665)
* 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
1 parent
e8444f9
commit 7673fc5
Showing
19 changed files
with
763 additions
and
2 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 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,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; | ||
}; |
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 { FordefiSigner } from "./signer.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,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"); | ||
}; | ||
} |
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
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,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(); | ||
``` | ||
|
||
::: |
Oops, something went wrong.