Skip to content

Commit

Permalink
feat(sdk): adds eagerDeployment option to smart wallets
Browse files Browse the repository at this point in the history
feat(sdk): add eagerDeployment option to smart wallets
  • Loading branch information
gregfromstl committed Dec 27, 2024
1 parent 7293dfa commit 4a5b4b4
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 72 deletions.
15 changes: 15 additions & 0 deletions .changeset/polite-trains-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"thirdweb": minor
---

Feature: Adds eagerDeployment option for smart accounts that need EIP-1271 signatures.

When setting `eagerDeployment` to `true`, smart accounts will use the legacy behavior of deploying prior to signing a message or typed data.

```ts
const wallet = smartWallet({
chain,
gasless: true,
eagerDeployment: true,
});
```
21 changes: 12 additions & 9 deletions packages/thirdweb/src/auth/verify-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,7 @@ export async function verifyHash({
try {
const result = await eth_call(rpcRequest, verificationData);
return hexToBool(result);
} catch (err) {
console.error("Error verifying ERC-6492 signature", err);
} catch {
// Some chains do not support the eth_call simulation and will fail, so we fall back to regular EIP1271 validation
const validEip1271 = await verifyEip1271Signature({
hash,
Expand All @@ -154,7 +153,7 @@ export async function verifyHash({
}

const EIP_1271_MAGIC_VALUE = "0x1626ba7e";
async function verifyEip1271Signature({
export async function verifyEip1271Signature({
hash,
signature,
contract,
Expand All @@ -163,10 +162,14 @@ async function verifyEip1271Signature({
signature: Hex;
contract: ThirdwebContract;
}): Promise<boolean> {
const result = await isValidSignature({
hash,
signature,
contract,
});
return result === EIP_1271_MAGIC_VALUE;
try {
const result = await isValidSignature({
hash,
signature,
contract,
});
return result === EIP_1271_MAGIC_VALUE;
} catch {
return false;
}
}
16 changes: 16 additions & 0 deletions packages/thirdweb/src/wallets/create-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ import { createWalletEmitter } from "./wallet-emitter.js";
*
* [View Coinbase wallet creation options](https://portal.thirdweb.com/references/typescript/v5/CoinbaseWalletCreationOptions)
*
* ## Connecting with a smart wallet
*
* ```ts
* import { createWallet } from "thirdweb/wallets";
*
* const wallet = createWallet("smart", {
* chain: sepolia,
* sponsorGas: true,
* });
*
* const account = await wallet.connect({
* client,
* personalAccount, // pass the admin account
* });
* ```
*
* @wallet
*/
export function createWallet<const ID extends WalletId>(
Expand Down
10 changes: 6 additions & 4 deletions packages/thirdweb/src/wallets/smart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,9 @@ async function createSmartAccount(
});
}

const { deployAndSignMessage } = await import("./lib/signing.js");
return deployAndSignMessage({
const { smartAccountSignMessage } = await import("./lib/signing.js");
return smartAccountSignMessage({
smartAccount: account,
accountContract,
factoryContract: options.factoryContract,
options,
Expand All @@ -298,8 +299,9 @@ async function createSmartAccount(
});
}

const { deployAndSignTypedData } = await import("./lib/signing.js");
return deployAndSignTypedData({
const { smartAccountSignTypedData } = await import("./lib/signing.js");
return smartAccountSignTypedData({
smartAccount: account,
accountContract,
factoryContract: options.factoryContract,
options,
Expand Down
214 changes: 157 additions & 57 deletions packages/thirdweb/src/wallets/smart/lib/signing.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
import type * as ox__TypedData from "ox/TypedData";
import { serializeErc6492Signature } from "../../../auth/serialize-erc6492-signature.js";
import { verifyHash } from "../../../auth/verify-hash.js";
import {
verifyEip1271Signature,
verifyHash,
} from "../../../auth/verify-hash.js";
import type { Chain } from "../../../chains/types.js";
import type { ThirdwebClient } from "../../../client/client.js";
import {
type ThirdwebContract,
getContract,
} from "../../../contract/contract.js";
import { encode } from "../../../transaction/actions/encode.js";
import { readContract } from "../../../transaction/read-contract.js";
import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js";
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
import type { Hex } from "../../../utils/encoding/hex.js";
import { hashMessage } from "../../../utils/hashing/hashMessage.js";
import { hashTypedData } from "../../../utils/hashing/hashTypedData.js";
import type { SignableMessage } from "../../../utils/types.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import type { SmartAccountOptions } from "../types.js";
import { prepareCreateAccount } from "./calls.js";

export async function deployAndSignMessage({
/**
* If the account is already deployed, generate an ERC-1271 signature.
* If the account is not deployed, generate an ERC-6492 signature unless otherwise specified.
*
* @internal
*/
export async function smartAccountSignMessage({
smartAccount,
accountContract,
factoryContract,
options,
message,
}: {
smartAccount: Account;
accountContract: ThirdwebContract;
factoryContract: ThirdwebContract;
options: SmartAccountOptions;
Expand Down Expand Up @@ -55,48 +70,73 @@ export async function deployAndSignMessage({
sig = await options.personalAccount.signMessage({ message });
}

const deployTx = prepareCreateAccount({
factoryContract,
adminAddress: options.personalAccount.address,
accountSalt: options.overrides?.accountSalt,
createAccountOverride: options.overrides?.createAccount,
});
if (!deployTx) {
throw new Error("Create account override not provided");
if (options.eagerDeployment) {
await forceDeploySmartAccount({
chain: options.chain,
client: options.client,
smartAccount,
accountContract,
});
await confirmContractDeployment({
accountContract,
});
}
const initCode = await encode(deployTx);
const erc6492Sig = serializeErc6492Signature({
address: factoryContract.address,
data: initCode,
signature: sig,
});

// check if the signature is valid
const isValid = await verifyHash({
hash: originalMsgHash,
signature: erc6492Sig,
address: accountContract.address,
chain: accountContract.chain,
client: accountContract.client,
});
const isDeployed = await isContractDeployed(accountContract);
if (isDeployed) {
const isValid = await verifyEip1271Signature({
hash: originalMsgHash,
signature: sig,
contract: accountContract,
});
if (isValid) {
return sig;
}
throw new Error("Failed to verify signature");

Check warning on line 95 in packages/thirdweb/src/wallets/smart/lib/signing.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/smart/lib/signing.ts#L95

Added line #L95 was not covered by tests
} else {
const deployTx = prepareCreateAccount({
factoryContract,
adminAddress: options.personalAccount.address,
accountSalt: options.overrides?.accountSalt,
createAccountOverride: options.overrides?.createAccount,
});
if (!deployTx) {
throw new Error("Create account override not provided");
}

Check warning on line 105 in packages/thirdweb/src/wallets/smart/lib/signing.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/smart/lib/signing.ts#L104-L105

Added lines #L104 - L105 were not covered by tests
const initCode = await encode(deployTx);
const erc6492Sig = serializeErc6492Signature({
address: factoryContract.address,
data: initCode,
signature: sig,
});

if (isValid) {
return erc6492Sig;
// check if the signature is valid
const isValid = await verifyHash({
hash: originalMsgHash,
signature: erc6492Sig,
address: accountContract.address,
chain: accountContract.chain,
client: accountContract.client,
});

if (isValid) {
return erc6492Sig;
}
throw new Error("Unable to verify ERC-6492 signature after signing.");

Check warning on line 125 in packages/thirdweb/src/wallets/smart/lib/signing.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/smart/lib/signing.ts#L125

Added line #L125 was not covered by tests
}
throw new Error(
"Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.",
);
}

export async function deployAndSignTypedData<
export async function smartAccountSignTypedData<
const typedData extends ox__TypedData.TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
>({
smartAccount,
accountContract,
factoryContract,
options,
typedData,
}: {
smartAccount: Account;
accountContract: ThirdwebContract;
factoryContract: ThirdwebContract;
options: SmartAccountOptions;
Expand Down Expand Up @@ -142,37 +182,62 @@ export async function deployAndSignTypedData<
sig = await options.personalAccount.signTypedData(typedData);
}

const deployTx = prepareCreateAccount({
factoryContract,
adminAddress: options.personalAccount.address,
accountSalt: options.overrides?.accountSalt,
createAccountOverride: options.overrides?.createAccount,
});
if (!deployTx) {
throw new Error("Create account override not provided");
if (options.eagerDeployment) {
await forceDeploySmartAccount({
chain: options.chain,
client: options.client,
smartAccount,
accountContract,
});
await confirmContractDeployment({
accountContract,
});

Check warning on line 194 in packages/thirdweb/src/wallets/smart/lib/signing.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/smart/lib/signing.ts#L186-L194

Added lines #L186 - L194 were not covered by tests
}
const initCode = await encode(deployTx);
const erc6492Sig = serializeErc6492Signature({
address: factoryContract.address,
data: initCode,
signature: sig,
});

// check if the signature is valid
const isValid = await verifyHash({
hash: originalMsgHash,
signature: erc6492Sig,
address: accountContract.address,
chain: accountContract.chain,
client: accountContract.client,
});
const isDeployed = await isContractDeployed(accountContract);
if (isDeployed) {
const isValid = await verifyEip1271Signature({
hash: originalMsgHash,
signature: sig,
contract: accountContract,
});
if (isValid) {
return sig;
}
throw new Error("Failed to verify signature");

Check warning on line 207 in packages/thirdweb/src/wallets/smart/lib/signing.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/smart/lib/signing.ts#L207

Added line #L207 was not covered by tests
} else {
const deployTx = prepareCreateAccount({
factoryContract,
adminAddress: options.personalAccount.address,
accountSalt: options.overrides?.accountSalt,
createAccountOverride: options.overrides?.createAccount,
});
if (!deployTx) {
throw new Error("Create account override not provided");
}

Check warning on line 217 in packages/thirdweb/src/wallets/smart/lib/signing.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/smart/lib/signing.ts#L216-L217

Added lines #L216 - L217 were not covered by tests
const initCode = await encode(deployTx);
const erc6492Sig = serializeErc6492Signature({
address: factoryContract.address,
data: initCode,
signature: sig,
});

if (isValid) {
return erc6492Sig;
// check if the signature is valid
const isValid = await verifyHash({
hash: originalMsgHash,
signature: erc6492Sig,
address: accountContract.address,
chain: accountContract.chain,
client: accountContract.client,
});

if (isValid) {
return erc6492Sig;
}
throw new Error(
"Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.",
);

Check warning on line 239 in packages/thirdweb/src/wallets/smart/lib/signing.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/smart/lib/signing.ts#L237-L239

Added lines #L237 - L239 were not covered by tests
}
throw new Error(
"Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.",
);
}

export async function confirmContractDeployment(args: {
Expand Down Expand Up @@ -229,3 +294,38 @@ async function checkFor712Factory({
return false;
}
}

/**
* Forces smart account deployment via a dummy transaction
*
* @internal
*/
export async function forceDeploySmartAccount(args: {
smartAccount: Account;
chain: Chain;
client: ThirdwebClient;
accountContract: ThirdwebContract;
}) {
const { chain, client, smartAccount, accountContract } = args;
const isDeployed = await isContractDeployed(accountContract);
if (isDeployed) {
return;
}

const [{ sendTransaction }, { prepareTransaction }] = await Promise.all([
import("../../../transaction/actions/send-transaction.js"),
import("../../../transaction/prepare-transaction.js"),
]);
const dummyTx = prepareTransaction({
client: client,
chain: chain,
to: accountContract.address,
value: 0n,
gas: 50000n, // force gas to avoid simulation error
});
const deployResult = await sendTransaction({
transaction: dummyTx,
account: smartAccount,
});
return deployResult;
}
Loading

0 comments on commit 4a5b4b4

Please sign in to comment.