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

Feat(SMA-598): Smart Account Withdrawals #424

Merged
merged 30 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
da340fb
Get smart account balances
joepegler Feb 14, 2024
f8a1b2e
lint:fix
joepegler Feb 14, 2024
b7e0d98
fix comments
joepegler Feb 14, 2024
d8d2cb2
remove log
joepegler Feb 14, 2024
66eb3c4
fix build
joepegler Feb 14, 2024
8a7eb0d
continued
joepegler Feb 14, 2024
7374c79
Merge remote-tracking branch 'origin/develop' into feature/SMA-599_ba…
joepegler Feb 15, 2024
e6cd7db
fix yarn
joepegler Feb 26, 2024
891dd98
Merge remote-tracking branch 'origin/develop' into feature/SMA-598_wi…
joepegler Feb 26, 2024
b689dc9
fix comment
joepegler Feb 26, 2024
5264016
Merge remote-tracking branch 'origin/develop' into feature/SMA-598_wi…
joepegler Feb 26, 2024
4bb17a7
default recipient
joepegler Feb 26, 2024
fa30abb
default with empty string
joepegler Feb 26, 2024
480e47f
Merge remote-tracking branch 'origin/develop' into feature/SMA-598_wi…
joepegler Feb 26, 2024
7a2bd59
Merge branch 'develop' into feature/SMA-598_withdrawal
joepegler Mar 1, 2024
e4f067c
usdt fix
joepegler Mar 1, 2024
37ed719
Merge remote-tracking branch 'origin/develop' into feature/SMA-598_wi…
joepegler Mar 1, 2024
e2193ad
cont.
joepegler Mar 1, 2024
01f4c4d
Merge remote-tracking branch 'origin/develop' into feature/SMA-598_wi…
joepegler Mar 4, 2024
de02980
reinclude publicClient
joepegler Mar 4, 2024
eeb55fd
Merge remote-tracking branch 'origin/develop' into feature/SMA-598_wi…
joepegler Mar 4, 2024
a8af7fd
merge develop
joepegler Mar 6, 2024
007b703
lint: fix
joepegler Mar 6, 2024
28422f1
continued
joepegler Apr 3, 2024
7a93d87
Merge branch 'develop' into feature/SMA-598_withdrawal
joepegler Apr 3, 2024
079def2
lint:fix
joepegler Apr 3, 2024
eaef063
yarn lock
joepegler Apr 3, 2024
44d2529
fix comment
joepegler Apr 3, 2024
cb4b60a
fix comment
joepegler Apr 3, 2024
050d542
Merge remote-tracking branch 'origin/develop' into feature/SMA-598_wi…
joepegler Apr 5, 2024
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
102 changes: 101 additions & 1 deletion packages/account/src/BiconomySmartAccountV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
BatchUserOperationCallData,
SmartAccountSigner,
} from "@alchemy/aa-core";
import { compareChainIds, isNullOrUndefined, packUserOp, isValidRpcUrl } from "./utils/Utils.js";
import { isNullOrUndefined, isValidRpcUrl, packUserOp, compareChainIds, addressEquals } from "./utils/Utils.js";
import { BaseValidationModule, ModuleInfo, SendUserOpParams, createECDSAOwnershipValidationModule } from "@biconomy/modules";
import {
IHybridPaymaster,
Expand Down Expand Up @@ -54,6 +54,7 @@ import {
SimulationType,
BalancePayload,
SupportedToken,
WithdrawalRequest,
} from "./utils/Types.js";
import {
ADDRESS_RESOLVER_ADDRESS,
Expand Down Expand Up @@ -359,6 +360,105 @@ export class BiconomySmartAccountV2 extends BaseSmartContractAccount {
return result;
}

/**
* Transfers funds from Smart Account to recipient (usually EOA)
* @param recipient - Address of the recipient
* @param withdrawalRequests - Array of withdrawal requests {@link WithdrawalRequest}. If withdrawal request is an empty array, it will transfer the balance of the native token. Using a paymaster will ensure no dust remains in the smart account.
* @param buildUseropDto - Optional. {@link BuildUserOpOptions}
*
* @returns Promise<UserOpResponse> - An object containing the status of the transaction.
*
* @example
* import { createClient } from "viem"
* import { createSmartAccountClient, NATIVE_TOKEN_ALIAS } from "@biconomy/account"
* import { createWalletClient, http } from "viem";
* import { polygonMumbai } from "viem/chains";
*
* const USDT = "0xda5289fcaaf71d52a80a254da614a192b693e977";
* const signer = createWalletClient({
* account,
* chain: polygonMumbai,
* transport: http(),
* });
*
* const smartAccount = await createSmartAccountClient({ signer, bundlerUrl, biconomyPaymasterApiKey });
*
* const { wait } = await smartAccount.withdraw(
* [
* { address: USDT }, // omit the amount to withdraw the full balance
* { address: NATIVE_TOKEN_ALIAS, amount: BigInt(1) }
* ],
* account.pubKey, // Default recipient used if no recipient is present in the withdrawal request
* {
* paymasterServiceData: { mode: PaymasterMode.SPONSORED },
* }
* );
*
* // OR to withdraw all of the native token, leaving no dust in the smart account
*
* const { wait } = await smartAccount.withdraw([], account.pubKey, {
* paymasterServiceData: { mode: PaymasterMode.SPONSORED },
* });
*
* const { success } = await wait();
*/
public async withdraw(
withdrawalRequests?: WithdrawalRequest[] | null,
defaultRecipient?: Hex | null,
buildUseropDto?: BuildUserOpOptions,
): Promise<UserOpResponse> {
const accountAddress = this.accountAddress ?? (await this.getAccountAddress());

if (!defaultRecipient && withdrawalRequests?.some(({ recipient }) => !recipient)) {
throw new Error(ERROR_MESSAGES.NO_RECIPIENT);
}

// Remove the native token from the withdrawal requests
let tokenRequests = withdrawalRequests?.filter(({ address }) => !addressEquals(address, NATIVE_TOKEN_ALIAS)) ?? [];

// Check if the amount is not present in all withdrawal requests
const shouldFetchMaxBalances = tokenRequests.some(({ amount }) => !amount);

// Get the balances of the tokens if the amount is not present in the withdrawal requests
if (shouldFetchMaxBalances) {
const balances = await this.getBalances(tokenRequests.map(({ address }) => address));
tokenRequests = tokenRequests.map(({ amount, address }, i) => ({
address,
amount: amount ?? balances[i].amount,
}));
}

// Create the transactions
const txs: Transaction[] = tokenRequests.map(({ address, amount, recipient: recipientFromRequest }) => ({
to: address,
data: encodeFunctionData({
abi: parseAbi(ERC20_ABI),
functionName: "transfer",
args: [recipientFromRequest || defaultRecipient, amount],
}),
}));

// Check if eth alias is present in the original withdrawal requests
const nativeTokenRequest = withdrawalRequests?.find(({ address }) => addressEquals(address, NATIVE_TOKEN_ALIAS));
const hasNoRequests = !withdrawalRequests?.length;
if (!!nativeTokenRequest || hasNoRequests) {
// Check that an amount is present in the withdrawal request, if no paymaster service data is present, as max amounts cannot be calculated without a paymaster.
if (!nativeTokenRequest?.amount && !buildUseropDto?.paymasterServiceData?.mode) {
throw new Error(ERROR_MESSAGES.NATIVE_TOKEN_WITHDRAWAL_WITHOUT_AMOUNT);
}

// get eth balance if not present in withdrawal requests
const nativeTokenAmountToWithdraw = nativeTokenRequest?.amount ?? (await this.provider.getBalance({ address: accountAddress }));

txs.push({
to: (nativeTokenRequest?.recipient ?? defaultRecipient) as Hex,
value: nativeTokenAmountToWithdraw,
});
}

return this.sendTransaction(txs, buildUseropDto);
}

/**
* Return the account's address. This value is valid even before deploying the contract.
*/
Expand Down
5 changes: 4 additions & 1 deletion packages/account/src/utils/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Hex } from "viem";
import {
EntryPointAddresses,
BiconomyFactories,
Expand Down Expand Up @@ -60,13 +61,15 @@ export const DefaultGasLimit = {
export const ERROR_MESSAGES = {
ACCOUNT_ALREADY_DEPLOYED: "Account already deployed",
NO_NATIVE_TOKEN_BALANCE_DURING_DEPLOY: "Native token balance is not available during deploy",
NO_RECIPIENT: "One or more of your withdrawals is missing a recipient",
SPENDER_REQUIRED: "spender is required for ERC20 mode",
NO_FEE_QUOTE: "FeeQuote was not provided, please call smartAccount.getTokenFees() to get feeQuote",
FAILED_FEE_QUOTE_FETCH: "Failed to fetch fee quote",
CHAIN_NOT_FOUND: "Chain not found",
NATIVE_TOKEN_WITHDRAWAL_WITHOUT_AMOUNT: "'Amount' is required for withdrawal of native token without using a paymaster",
};

export const NATIVE_TOKEN_ALIAS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
export const NATIVE_TOKEN_ALIAS: Hex = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
export const ERC20_ABI = [
"function transfer(address to, uint256 value) external returns (bool)",
"function transferFrom(address from, address to, uint256 value) external returns (bool)",
Expand Down
9 changes: 9 additions & 0 deletions packages/account/src/utils/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export interface BalancePayload {
formattedAmount: string;
}

export interface WithdrawalRequest {
/** The address of the asset */
address: Hex;
livingrockrises marked this conversation as resolved.
Show resolved Hide resolved
/** The amount to withdraw. Expects unformatted amount. Will use max amount if unset */
amount?: bigint;
/** The destination address of the funds. The second argument from the `withdraw(...)` function will be used as the default if left unset. */
recipient?: Hex;
}

export interface GasOverheads {
/** fixed: fixed gas overhead */
fixed: number;
Expand Down
4 changes: 2 additions & 2 deletions packages/account/src/utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export function packUserOp(op: Partial<UserOperationStruct>, forSignature = true
}
}

export const addressEquals = (a?: string, b?: string): boolean => !!a && !!b && a?.toLowerCase() === b.toLowerCase();

export const isNullOrUndefined = (value: any): value is undefined => {
return value === null || value === undefined;
};
Expand Down Expand Up @@ -86,8 +88,6 @@ export const isValidRpcUrl = (url: string): boolean => {
return regex.test(url);
};

export const addressEquals = (a?: string, b?: string): boolean => !!a && !!b && a?.toLowerCase() === b.toLowerCase();

/**
* Utility method for converting a chainId to a {@link Chain} object
*
Expand Down
Loading
Loading