1
- import { Static , Type } from "@sinclair/typebox" ;
2
- import { FastifyInstance } from "fastify" ;
1
+ import { Type , type Static } from "@sinclair/typebox" ;
2
+ import type { FastifyInstance } from "fastify" ;
3
3
import { StatusCodes } from "http-status-codes" ;
4
4
import {
5
- Address ,
6
- estimateGasCost ,
7
- prepareTransaction ,
8
- sendTransaction ,
5
+ toSerializableTransaction ,
6
+ toTokens ,
7
+ type Address ,
8
+ type Hex ,
9
9
} from "thirdweb" ;
10
+ import { getChainMetadata } from "thirdweb/chains" ;
10
11
import { getWalletBalance } from "thirdweb/wallets" ;
11
12
import { getAccount } from "../../../utils/account" ;
12
13
import { getChain } from "../../../utils/chain" ;
14
+ import { logger } from "../../../utils/logger" ;
15
+ import { getChecksumAddress , maybeBigInt } from "../../../utils/primitiveTypes" ;
13
16
import { thirdwebClient } from "../../../utils/sdk" ;
14
- import { AddressSchema } from "../../schemas/address" ;
17
+ import { createCustomError } from "../../middleware/error" ;
18
+ import { AddressSchema , TransactionHashSchema } from "../../schemas/address" ;
19
+ import { TokenAmountStringSchema } from "../../schemas/number" ;
15
20
import {
16
21
requestQuerystringSchema ,
17
22
standardResponseSchema ,
18
23
} from "../../schemas/sharedApiSchemas" ;
24
+ import { txOverridesSchema } from "../../schemas/txOverrides" ;
19
25
import {
20
26
walletHeaderSchema ,
21
27
walletWithAddressParamSchema ,
@@ -29,11 +35,13 @@ const requestBodySchema = Type.Object({
29
35
...AddressSchema ,
30
36
description : "Address to withdraw all funds to" ,
31
37
} ,
38
+ ...txOverridesSchema . properties ,
32
39
} ) ;
33
40
34
41
const responseBodySchema = Type . Object ( {
35
42
result : Type . Object ( {
36
- transactionHash : Type . String ( ) ,
43
+ transactionHash : TransactionHashSchema ,
44
+ amount : TokenAmountStringSchema ,
37
45
} ) ,
38
46
} ) ;
39
47
@@ -62,45 +70,69 @@ export async function withdraw(fastify: FastifyInstance) {
62
70
} ,
63
71
handler : async ( request , reply ) => {
64
72
const { chain : chainQuery } = request . params ;
65
- const { toAddress } = request . body ;
66
- const {
67
- "x-backend-wallet-address" : walletAddress ,
68
- "x-idempotency-key" : idempotencyKey ,
69
- } = request . headers as Static < typeof walletHeaderSchema > ;
73
+ const { toAddress, txOverrides } = request . body ;
74
+ const { "x-backend-wallet-address" : walletAddress } =
75
+ request . headers as Static < typeof walletHeaderSchema > ;
70
76
71
77
const chainId = await getChainIdFromChain ( chainQuery ) ;
72
78
const chain = await getChain ( chainId ) ;
73
- const from = walletAddress as Address ;
79
+ const from = getChecksumAddress ( walletAddress ) ;
80
+
81
+ // Populate a transfer transaction with 2x gas.
82
+ const populatedTransaction = await toSerializableTransaction ( {
83
+ from,
84
+ transaction : {
85
+ to : toAddress ,
86
+ chain,
87
+ client : thirdwebClient ,
88
+ data : "0x" ,
89
+ // Dummy value, replaced below.
90
+ value : 1n ,
91
+ gas : maybeBigInt ( txOverrides ?. gas ) ,
92
+ maxFeePerGas : maybeBigInt ( txOverrides ?. maxFeePerGas ) ,
93
+ maxPriorityFeePerGas : maybeBigInt ( txOverrides ?. maxPriorityFeePerGas ) ,
94
+ } ,
95
+ } ) ;
96
+
97
+ // Compute the maximum amount to withdraw taking into account gas fees.
98
+ const value = await getWithdrawValue ( from , populatedTransaction ) ;
99
+ populatedTransaction . value = value ;
74
100
75
101
const account = await getAccount ( { chainId, from } ) ;
76
- const value = await getWithdrawValue ( { chainId, from } ) ;
102
+ let transactionHash : Hex | undefined ;
103
+ try {
104
+ const res = await account . sendTransaction ( populatedTransaction ) ;
105
+ transactionHash = res . transactionHash ;
106
+ } catch ( e ) {
107
+ logger ( {
108
+ level : "warn" ,
109
+ message : `Error withdrawing funds: ${ e } ` ,
110
+ service : "server" ,
111
+ } ) ;
77
112
78
- const transaction = prepareTransaction ( {
79
- to : toAddress ,
80
- chain,
81
- client : thirdwebClient ,
82
- value,
83
- } ) ;
84
- const { transactionHash } = await sendTransaction ( {
85
- account,
86
- transaction,
87
- } ) ;
113
+ const metadata = await getChainMetadata ( chain ) ;
114
+ throw createCustomError (
115
+ `Insufficient ${ metadata . nativeCurrency ?. symbol } on ${ metadata . name } in ${ from } . Try again when network gas fees are lower. See: https://portal.thirdweb.com/engine/troubleshooting` ,
116
+ StatusCodes . BAD_REQUEST ,
117
+ "INSUFFICIENT_FUNDS" ,
118
+ ) ;
119
+ }
88
120
89
121
reply . status ( StatusCodes . OK ) . send ( {
90
122
result : {
91
123
transactionHash,
124
+ amount : toTokens ( value , 18 ) ,
92
125
} ,
93
126
} ) ;
94
127
} ,
95
128
} ) ;
96
129
}
97
130
98
- const getWithdrawValue = async ( args : {
99
- chainId : number ;
100
- from : Address ;
101
- } ) : Promise < bigint > => {
102
- const { chainId, from } = args ;
103
- const chain = await getChain ( chainId ) ;
131
+ const getWithdrawValue = async (
132
+ from : Address ,
133
+ populatedTransaction : Awaited < ReturnType < typeof toSerializableTransaction > > ,
134
+ ) : Promise < bigint > => {
135
+ const chain = await getChain ( populatedTransaction . chainId ) ;
104
136
105
137
// Get wallet balance.
106
138
const { value : balanceWei } = await getWalletBalance ( {
@@ -109,17 +141,13 @@ const getWithdrawValue = async (args: {
109
141
chain,
110
142
} ) ;
111
143
112
- // Estimate gas for a transfer.
113
- const transaction = prepareTransaction ( {
114
- chain,
115
- client : thirdwebClient ,
116
- value : 1n , // dummy value
117
- to : from , // dummy value
118
- } ) ;
119
- const { wei : transferCostWei } = await estimateGasCost ( { transaction } ) ;
120
-
121
- // Add a +20% buffer for gas variance.
122
- const buffer = transferCostWei / 5n ;
144
+ // Set the withdraw value to be the amount of gas that isn't reserved to send the transaction.
145
+ const gasPrice =
146
+ populatedTransaction . maxFeePerGas ?? populatedTransaction . gasPrice ;
147
+ if ( ! gasPrice ) {
148
+ throw new Error ( "Unable to estimate gas price for withdraw request." ) ;
149
+ }
123
150
124
- return balanceWei - transferCostWei - buffer ;
151
+ const transferCostWei = populatedTransaction . gas * gasPrice ;
152
+ return balanceWei - transferCostWei ;
125
153
} ;
0 commit comments