Skip to content
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

[SDK] Feature: Adds eagerDeployment on smart account creation #5845

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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 @@
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
Loading