Skip to content

[SDK] Feature: Adds deploySmartAccount and re-adds 1271 signatures #5845

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

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 16 additions & 0 deletions .changeset/polite-trains-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"thirdweb": minor
---

Feature: Adds `deploySmartAccount` function to force the deployment of a smart account.

```ts
const account = await deploySmartAccount({
smartAccount,
chain,
client,
accountContract,
});
```

Fix: Uses 1271 signatures if the smart account is already deployed.
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the actual issue here?

6492 should automatically use 1271 if the account is already deployed, the deployed check happens inside the universal validator contract.

We shouldn't need to force it manually ever

// 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally really want to log something for the dev to understand why the sig was not valid, same with the catch above

return false;
}
}
2 changes: 2 additions & 0 deletions packages/thirdweb/src/exports/thirdweb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,5 @@ export {
type VerifyTypedDataParams,
verifyTypedData,
} from "../auth/verify-typed-data.js";

export { deploySmartAccount } from "../wallets/smart/lib/signing.js";
2 changes: 2 additions & 0 deletions packages/thirdweb/src/exports/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,5 @@ export * as EIP1193 from "../adapters/eip1193/index.js";
export { injectedProvider } from "../wallets/injected/mipdStore.js";

export type { ConnectionManager } from "../wallets/manager/index.js";

export { deploySmartAccount } from "../wallets/smart/lib/signing.js";
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
8 changes: 4 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,8 @@ async function createSmartAccount(
});
}

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

const { deployAndSignTypedData } = await import("./lib/signing.js");
return deployAndSignTypedData({
const { smartAccountSignTypedData } = await import("./lib/signing.js");
return smartAccountSignTypedData({
accountContract,
factoryContract: options.factoryContract,
options,
Expand Down
213 changes: 154 additions & 59 deletions packages/thirdweb/src/wallets/smart/lib/signing.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
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({
accountContract,
factoryContract,
options,
Expand Down Expand Up @@ -55,40 +68,51 @@ 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");
}
const initCode = await encode(deployTx);
const erc6492Sig = serializeErc6492Signature({
address: factoryContract.address,
data: initCode,
signature: sig,
});
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");
} 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");
}
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,
});
// 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;
if (isValid) {
return erc6492Sig;
}
throw new Error("Unable to verify ERC-6492 signature after signing.");
}
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,
>({
Expand Down Expand Up @@ -142,37 +166,50 @@ 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");
}
const initCode = await encode(deployTx);
const erc6492Sig = serializeErc6492Signature({
address: factoryContract.address,
data: initCode,
signature: sig,
});
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");
} 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");
}
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,
});
// 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;
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.",
);
}
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 +266,61 @@ async function checkFor712Factory({
return false;
}
}

/**
* Deployes a smart account via a dummy transaction.
*
* @param args - Arguments for the deployment.
* @param args.smartAccount - The smart account to deploy.
* @param args.chain - The chain to deploy on.
* @param args.client - The client to use for the deployment.
* @param args.accountContract - The account contract to deploy.
*
* @example
* ```ts
* import { deploySmartAccount } from "thirdweb";
*
* const account = await deploySmartAccount({
* smartAccount,
* chain,
* client,
* accountContract,
* });
* ```
*
* @wallets
*/
export async function deploySmartAccount(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,
});

await confirmContractDeployment({
accountContract,
});

return deployResult;
}
Loading
Loading