Skip to content

Commit

Permalink
[WIP] smart wallet v5 (#2301)
Browse files Browse the repository at this point in the history
  • Loading branch information
joaquim-verges authored Feb 14, 2024
1 parent ccd9548 commit d55c7e2
Show file tree
Hide file tree
Showing 22 changed files with 844 additions and 58 deletions.
2 changes: 1 addition & 1 deletion packages/thirdweb/src/contract/verification/publisher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ThirdwebClient } from "../../client/client.js";
import { download } from "../../storage/download.js";
import { readContract } from "../../transaction/index.js";
import { readContract } from "../../transaction/read-contract.js";
import { extractIPFSUri, resolveImplementation } from "../../utils/index.js";
import { getContract, type ThirdwebContract } from "../contract.js";

Expand Down
6 changes: 3 additions & 3 deletions packages/thirdweb/src/event/actions/get-events.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Abi, AbiEvent } from "abitype";
import type { BlockTag } from "viem";
import { resolveAbiEvent } from "./resolve-abi.js";
import type { ContractEvent } from "../event.js";
import type { ContractEvent, EventLog } from "../event.js";
import {
resolveContractAbi,
type ThirdwebContract,
Expand Down Expand Up @@ -75,10 +75,10 @@ export async function getEvents<
abi.filter((item) => item.type === "event"),
) as Promise<abiEvent[]>));

return await eth_getLogs(rpcRequest, {
return (await eth_getLogs(rpcRequest, {
fromBlock: options.fromBlock,
toBlock: options.toBlock,
address: options.contract.address,
events: parsedEvents,
});
})) as EventLog<abiEvent>[];
}
6 changes: 2 additions & 4 deletions packages/thirdweb/src/event/actions/watch-events.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { Abi, AbiEvent } from "abitype";
import { resolveAbiEvent } from "./resolve-abi.js";
import type { ContractEvent, EventLog } from "../event.js";
import {
resolveContractAbi,
type ThirdwebContract,
} from "../../contract/index.js";
import { resolveContractAbi } from "../../contract/actions/resolve-abi.js";
import {
eth_getLogs,
getRpcClient,
watchBlockNumber,
} from "../../rpc/index.js";
import type { ThirdwebContract } from "../../contract/contract.js";

export type WatchContractEventsOptions<
abi extends Abi,
Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/extensions/erc20/write/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type TransferParams = { to: string } & (
*/
export function transfer(options: BaseTransactionOptions<TransferParams>) {
return prepareContractCall({
...options,
contract: options.contract,
method: "function transfer(address to, uint256 value)",
params: async () => {
let amount: bigint;
Expand Down
13 changes: 5 additions & 8 deletions packages/thirdweb/src/gas/fee-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,6 @@ export async function getDefaultGasOverrides(
client: ThirdwebClient,
chain: Chain,
) {
/**
* TODO: do we want to re-enable this?
*/
// If we're running in the browser, let users configure gas price in their wallet UI
// if (isBrowser()) {
// return {};
// }

const feeData = await getDynamicFeeData(client, chain);
if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
return {
Expand Down Expand Up @@ -137,6 +129,11 @@ async function getDynamicFeeData(
// good article on the subject: https://www.blocknative.com/blog/eip-1559-fees
maxFeePerGas = baseBlockFee * 2n + maxPriorityFeePerGas_;

// special cased for Celo gas fees
if (chainId === 42220n || chainId === 44787n || chainId === 62320n) {
maxPriorityFeePerGas_ = maxFeePerGas;
}

return {
maxFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas_,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function useContractEvents<
const contractEvents extends ContractEvent<abiEvent>[],
>(
options: UseContractEventsOptions<abi, abiEvent, contractEvents>,
): UseQueryResult<EventLog<AbiEvent>[], Error> {
): UseQueryResult<EventLog<abiEvent>[], Error> {
const {
contract,
events,
Expand Down
32 changes: 15 additions & 17 deletions packages/thirdweb/src/react/hooks/contract/useWaitForReceipt.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
import type { TransactionReceipt } from "viem";
import type { TransactionOrUserOpHash } from "../../../transaction/types.js";
import { getChainIdFromChain } from "../../../chain/index.js";
import { waitForReceipt } from "../../../transaction/actions/wait-for-tx-receipt.js";
import type { PreparedTransaction } from "../../../transaction/index.js";

export type TransactionHashOptions = TransactionOrUserOpHash & {
transaction: PreparedTransaction;
transactionHash?: string;
};
import {
waitForReceipt,
type WaitForReceiptOptions,
} from "../../../transaction/actions/wait-for-tx-receipt.js";

/**
* A hook to wait for a transaction receipt.
Expand All @@ -21,24 +17,26 @@ export type TransactionHashOptions = TransactionOrUserOpHash & {
* ```
*/
export function useWaitForReceipt(
options: TransactionHashOptions | undefined,
options: WaitForReceiptOptions | undefined,
): UseQueryResult<TransactionReceipt> {
// TODO: here contract can be undfined so we go to a `-1` chain but this feels wrong
const chainId = getChainIdFromChain(
options?.transaction.chain ?? -1,
).toString();
return useQuery({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: ["waitForReceipt", chainId, options?.transactionHash] as const,
queryKey: [
"waitForReceipt",
chainId,
options?.transactionHash || options?.userOpHash,
] as const,
queryFn: async () => {
if (!options?.transactionHash) {
throw new Error("No transaction hash");
if (!options?.transactionHash && !options?.userOpHash) {
throw new Error("No transaction hash or user op hash provided");
}
return waitForReceipt({
transaction: options.transaction,
transactionHash: options.transactionHash,
});
return waitForReceipt(options);
},
enabled: !!options?.transactionHash,
enabled: !!options?.transactionHash || !!options?.userOpHash,
retry: false,
});
}
15 changes: 6 additions & 9 deletions packages/thirdweb/src/transaction/actions/estimate-gas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export async function estimateGas(
options: EstimateGasOptions,
): Promise<EstimateGasResult> {
if (cache.has(options.transaction)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return cache.get(options.transaction)!;
}
const promise = (async () => {
Expand All @@ -38,22 +39,18 @@ export async function estimateGas(
return predefinedGas;
}

// if the account itself overrides the estimateGas function, use that
if (options.account && options.account.estimateGas) {
return await options.account.estimateGas(options.transaction);
}

// load up encode function if we need it
const { encode } = await import("./encode.js");
const [encodedData, toAddress] = await Promise.all([
encode(options.transaction),
resolvePromisedValue(options.transaction.to),
]);

// if the account itself overrides the estimateGas function, use that
if (
options.account &&
options.account.wallet &&
options.account.wallet.estimateGas
) {
return await options.account.wallet.estimateGas(options.transaction);
}

// load up the rpc client and the estimateGas function if we need it
const [{ getRpcClient }, { eth_estimateGas }] = await Promise.all([
import("../../rpc/rpc.js"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { PreparedTransaction } from "../prepare-transaction.js";
type SendTransactionOptions = {
transaction: PreparedTransaction;
account: Account;
gasless?: boolean;
};

/**
Expand Down
56 changes: 47 additions & 9 deletions packages/thirdweb/src/transaction/actions/wait-for-tx-receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getRpcClient,
watchBlockNumber,
} from "../../rpc/index.js";
import { getUserOpEventFromEntrypoint } from "../../wallets/smart/lib/receipts.js";
import type { PreparedTransaction } from "../prepare-transaction.js";

const MAX_BLOCKS_WAIT_TIME = 10;
Expand Down Expand Up @@ -44,7 +45,7 @@ export function waitForReceipt(
}
const promise = new Promise<TransactionReceipt>((resolve, reject) => {
// TODO: handle useropHash
if (!transactionHash) {
if (!transactionHash && !userOpHash) {
reject(
new Error("Transaction has no txHash to wait for, did you execute it?"),
);
Expand All @@ -54,25 +55,62 @@ export function waitForReceipt(

// start at -1 because the first block doesn't count
let blocksWaited = -1;
let lastBlockNumber: bigint | undefined;

const unwatch = watchBlockNumber({
client: transaction.client,
chain: transaction.chain,
onNewBlockNumber: async () => {
onNewBlockNumber: async (blockNumber) => {
blocksWaited++;
if (blocksWaited >= MAX_BLOCKS_WAIT_TIME) {
unwatch();
reject(new Error("Transaction not found after 10 blocks"));
return;
}
try {
const receipt = await eth_getTransactionReceipt(request, {
hash: transactionHash as Hex,
});
if (transactionHash) {
const receipt = await eth_getTransactionReceipt(request, {
hash: transactionHash as Hex,
});

// stop the polling
unwatch();
// resolve the top level promise with the receipt
resolve(receipt);
// stop the polling
unwatch();
// resolve the top level promise with the receipt
resolve(receipt);
} else if (userOpHash) {
let event;
try {
event = await getUserOpEventFromEntrypoint({
blockNumber: blockNumber,
blockRange: lastBlockNumber ? 2n : 2000n, // query backwards further on first tick
chain: transaction.chain,
client: transaction.client,
userOpHash: userOpHash,
});
} catch (e) {
console.error(e);
// stop the polling
unwatch();
// userOp reverted
reject(e);
return;
}

lastBlockNumber = blockNumber;
if (event) {
console.log("event", event);
const receipt = await eth_getTransactionReceipt(request, {
hash: event.transactionHash,
});

// TODO check if the event has success = false and decode the revert reason

// stop the polling
unwatch();
// resolve the top level promise with the receipt
resolve(receipt);
}
}
} catch {
// noop, we'll try again on the next blocks
}
Expand Down
4 changes: 2 additions & 2 deletions packages/thirdweb/src/utils/bytecode/extractIPFS.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { hexToBytes } from "@noble/hashes/utils";
import { encode } from "bs58";
import bs58 from "bs58";
import { decode } from "./cbor-decode.js";

/**
Expand Down Expand Up @@ -33,7 +33,7 @@ export function extractIPFSUri(bytecode: string): string | undefined {

const cborData = decode(bytecodeBuffer);
if ("ipfs" in cborData) {
return `ipfs://${encode(cborData["ipfs"])}`;
return `ipfs://${bs58.encode(cborData["ipfs"])}`;
}

return undefined;
Expand Down
3 changes: 3 additions & 0 deletions packages/thirdweb/src/wallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@ export {
} from "./wallet-connect/index.js";

export type { WalletConnectConnectionOptions } from "./wallet-connect/types.js";

// smart
export { smartWallet } from "./smart/index.js";
8 changes: 6 additions & 2 deletions packages/thirdweb/src/wallets/injected/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,12 @@ export class InjectedWallet implements Wallet {
}

const messageToSign = (() => {
if (typeof message === "string") return stringToHex(message);
if (message.raw instanceof Uint8Array) return toHex(message.raw);
if (typeof message === "string") {
return stringToHex(message);
}
if (message.raw instanceof Uint8Array) {
return toHex(message.raw);
}
return message.raw;
})();

Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/wallets/interfaces/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export type Wallet = {

// OPTIONAL
chainId?: bigint;
estimateGas?: (transaction: PreparedTransaction) => Promise<bigint>;

events?: {
addListener: WalletEventListener;
Expand All @@ -53,6 +52,7 @@ export type Account = {

// OPTIONAL
signTransaction?: (tx: TransactionSerializable) => Promise<Hex>;
estimateGas?: (tx: PreparedTransaction) => Promise<bigint>;

// TODO: figure out a path to remove this (or reduce it to the minimum possible interface)
/**
Expand Down
Loading

0 comments on commit d55c7e2

Please sign in to comment.