@coinbase/create-cdp-app 0.0.26 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +39 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/template-react-native/App.tsx +359 -0
- package/template-react-native/EOATransaction.tsx +360 -0
- package/template-react-native/README.md +57 -0
- package/template-react-native/SmartAccountTransaction.tsx +304 -0
- package/template-react-native/Transaction.tsx +1 -0
- package/template-react-native/_gitignore +37 -0
- package/template-react-native/android/app/build.gradle +177 -0
- package/template-react-native/android/app/debug.keystore +0 -0
- package/template-react-native/android/app/proguard-rules.pro +14 -0
- package/template-react-native/android/app/src/debug/AndroidManifest.xml +7 -0
- package/template-react-native/android/app/src/main/AndroidManifest.xml +25 -0
- package/template-react-native/android/app/src/main/java/com/anonymous/reactnativeexpo/MainActivity.kt +61 -0
- package/template-react-native/android/app/src/main/java/com/anonymous/reactnativeexpo/MainApplication.kt +57 -0
- package/template-react-native/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
- package/template-react-native/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/template-react-native/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
- package/template-react-native/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
- package/template-react-native/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
- package/template-react-native/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
- package/template-react-native/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/template-react-native/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/template-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
- package/template-react-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- package/template-react-native/android/app/src/main/res/values/colors.xml +6 -0
- package/template-react-native/android/app/src/main/res/values/strings.xml +5 -0
- package/template-react-native/android/app/src/main/res/values/styles.xml +10 -0
- package/template-react-native/android/app/src/main/res/values-night/colors.xml +1 -0
- package/template-react-native/android/build.gradle +37 -0
- package/template-react-native/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template-react-native/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/template-react-native/android/gradle.properties +59 -0
- package/template-react-native/android/gradlew +251 -0
- package/template-react-native/android/gradlew.bat +94 -0
- package/template-react-native/android/settings.gradle +39 -0
- package/template-react-native/app.json +34 -0
- package/template-react-native/assets/adaptive-icon.png +0 -0
- package/template-react-native/assets/favicon.png +0 -0
- package/template-react-native/assets/icon.png +0 -0
- package/template-react-native/assets/splash-icon.png +0 -0
- package/template-react-native/env.example +5 -0
- package/template-react-native/index.ts +20 -0
- package/template-react-native/ios/.xcode.env +11 -0
- package/template-react-native/ios/Podfile +64 -0
- package/template-react-native/ios/Podfile.lock +2125 -0
- package/template-react-native/ios/Podfile.properties.json +5 -0
- package/template-react-native/ios/reactnativeexpo/AppDelegate.swift +70 -0
- package/template-react-native/ios/reactnativeexpo/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
- package/template-react-native/ios/reactnativeexpo/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/template-react-native/ios/reactnativeexpo/Images.xcassets/Contents.json +6 -0
- package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
- package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/Contents.json +23 -0
- package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/image.png +0 -0
- package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/image@2x.png +0 -0
- package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/image@3x.png +0 -0
- package/template-react-native/ios/reactnativeexpo/Info.plist +74 -0
- package/template-react-native/ios/reactnativeexpo/PrivacyInfo.xcprivacy +48 -0
- package/template-react-native/ios/reactnativeexpo/SplashScreen.storyboard +44 -0
- package/template-react-native/ios/reactnativeexpo/Supporting/Expo.plist +12 -0
- package/template-react-native/ios/reactnativeexpo/reactnativeexpo-Bridging-Header.h +3 -0
- package/template-react-native/ios/reactnativeexpo/reactnativeexpo.entitlements +5 -0
- package/template-react-native/ios/reactnativeexpo.xcodeproj/project.pbxproj +545 -0
- package/template-react-native/ios/reactnativeexpo.xcodeproj/xcshareddata/xcschemes/reactnativeexpo.xcscheme +88 -0
- package/template-react-native/ios/reactnativeexpo.xcworkspace/contents.xcworkspacedata +10 -0
- package/template-react-native/package.json +30 -0
- package/template-react-native/tsconfig.json +6 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { useEvmAddress, useSendEvmTransaction } from "@coinbase/cdp-hooks";
|
|
2
|
+
import * as Clipboard from "expo-clipboard";
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
// eslint-disable-next-line import/namespace
|
|
5
|
+
import { View, Text, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from "react-native";
|
|
6
|
+
import { createPublicClient, http, formatEther } from "viem";
|
|
7
|
+
import { baseSepolia } from "viem/chains";
|
|
8
|
+
|
|
9
|
+
const client = createPublicClient({
|
|
10
|
+
chain: baseSepolia,
|
|
11
|
+
transport: http(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
onSuccess?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This component demonstrates how to send an EVM transaction using EOA (Externally Owned Accounts).
|
|
20
|
+
*
|
|
21
|
+
* @param {Props} props - The props for the EOATransaction component.
|
|
22
|
+
* @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
|
|
23
|
+
* @returns A component that displays a transaction form and result.
|
|
24
|
+
*/
|
|
25
|
+
function EOATransaction(props: Props) {
|
|
26
|
+
const { onSuccess } = props;
|
|
27
|
+
const { evmAddress } = useEvmAddress();
|
|
28
|
+
const [balance, setBalance] = useState<bigint | undefined>(undefined);
|
|
29
|
+
const [error, setError] = useState("");
|
|
30
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
31
|
+
const { sendEvmTransaction: sendTransaction, data: transactionData } = useSendEvmTransaction();
|
|
32
|
+
|
|
33
|
+
const formattedBalance = useMemo(() => {
|
|
34
|
+
if (balance === undefined) return undefined;
|
|
35
|
+
return formatEther(balance);
|
|
36
|
+
}, [balance]);
|
|
37
|
+
|
|
38
|
+
const hasBalance = useMemo(() => {
|
|
39
|
+
return formattedBalance && formattedBalance !== "0";
|
|
40
|
+
}, [formattedBalance]);
|
|
41
|
+
|
|
42
|
+
const getBalance = useCallback(async () => {
|
|
43
|
+
if (!evmAddress) return;
|
|
44
|
+
const balance = await client.getBalance({
|
|
45
|
+
address: evmAddress,
|
|
46
|
+
});
|
|
47
|
+
setBalance(balance);
|
|
48
|
+
}, [evmAddress]);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
getBalance();
|
|
52
|
+
const interval = setInterval(getBalance, 5000);
|
|
53
|
+
return () => clearInterval(interval);
|
|
54
|
+
}, [getBalance]);
|
|
55
|
+
|
|
56
|
+
const handleSendTransaction = async () => {
|
|
57
|
+
if (!evmAddress || !hasBalance) {
|
|
58
|
+
Alert.alert("Error", "Insufficient balance or no address available");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setIsLoading(true);
|
|
63
|
+
setError("");
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await sendTransaction({
|
|
67
|
+
network: "base-sepolia",
|
|
68
|
+
evmAccount: evmAddress,
|
|
69
|
+
transaction: {
|
|
70
|
+
to: evmAddress,
|
|
71
|
+
value: 1000000000000n,
|
|
72
|
+
gas: 21000n,
|
|
73
|
+
chainId: 84532,
|
|
74
|
+
type: "eip1559",
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
Alert.alert("Success", "Transaction sent successfully!");
|
|
79
|
+
|
|
80
|
+
onSuccess?.();
|
|
81
|
+
getBalance();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const errorMessage = err instanceof Error ? err.message : "Transaction failed";
|
|
84
|
+
setError(errorMessage);
|
|
85
|
+
Alert.alert("Transaction Failed", errorMessage);
|
|
86
|
+
} finally {
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const copyToClipboard = async (text: string, label: string) => {
|
|
92
|
+
try {
|
|
93
|
+
await Clipboard.setStringAsync(text);
|
|
94
|
+
Alert.alert("Copied!", `${label} copied to clipboard`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
Alert.alert("Error", "Failed to copy to clipboard");
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<View style={styles.container}>
|
|
102
|
+
<Text style={styles.title}>EOA Transaction</Text>
|
|
103
|
+
|
|
104
|
+
<View style={styles.infoContainer}>
|
|
105
|
+
<Text style={styles.label}>EVM Address:</Text>
|
|
106
|
+
<TouchableOpacity
|
|
107
|
+
style={styles.copyableInfo}
|
|
108
|
+
onPress={() => copyToClipboard(evmAddress!, "EVM Address")}
|
|
109
|
+
>
|
|
110
|
+
<Text style={styles.copyableText}>{evmAddress || "Not available"}</Text>
|
|
111
|
+
<Text style={styles.addressCopyHint}>Tap to copy</Text>
|
|
112
|
+
</TouchableOpacity>
|
|
113
|
+
</View>
|
|
114
|
+
|
|
115
|
+
<View style={styles.infoContainer}>
|
|
116
|
+
<Text style={styles.label}>Balance:</Text>
|
|
117
|
+
<Text style={styles.value}>
|
|
118
|
+
{formattedBalance === undefined ? "Loading..." : `${formattedBalance} ETH`}
|
|
119
|
+
</Text>
|
|
120
|
+
</View>
|
|
121
|
+
|
|
122
|
+
<TouchableOpacity
|
|
123
|
+
style={[styles.button, (!hasBalance || isLoading) && styles.buttonDisabled]}
|
|
124
|
+
onPress={handleSendTransaction}
|
|
125
|
+
disabled={!hasBalance || isLoading}
|
|
126
|
+
>
|
|
127
|
+
{isLoading ? (
|
|
128
|
+
<ActivityIndicator color="#fff" />
|
|
129
|
+
) : (
|
|
130
|
+
<Text style={styles.buttonText}>Send Transaction</Text>
|
|
131
|
+
)}
|
|
132
|
+
</TouchableOpacity>
|
|
133
|
+
|
|
134
|
+
{error ? (
|
|
135
|
+
<View style={styles.errorContainer}>
|
|
136
|
+
<Text style={styles.errorText}>{error}</Text>
|
|
137
|
+
</View>
|
|
138
|
+
) : null}
|
|
139
|
+
|
|
140
|
+
{transactionData.status === "success" ? (
|
|
141
|
+
<View style={styles.successContainer}>
|
|
142
|
+
<Text style={styles.successTitle}>Transaction Sent!</Text>
|
|
143
|
+
<View style={styles.hashContainer}>
|
|
144
|
+
<Text style={styles.hashLabel}>Transaction Hash:</Text>
|
|
145
|
+
<TouchableOpacity
|
|
146
|
+
style={styles.hashButton}
|
|
147
|
+
onPress={() =>
|
|
148
|
+
copyToClipboard(transactionData.receipt.transactionHash, "Transaction Hash")
|
|
149
|
+
}
|
|
150
|
+
>
|
|
151
|
+
<Text style={styles.hashText}>{transactionData.receipt.transactionHash}</Text>
|
|
152
|
+
<Text style={styles.copyHint}>📋 Tap to copy</Text>
|
|
153
|
+
</TouchableOpacity>
|
|
154
|
+
</View>
|
|
155
|
+
</View>
|
|
156
|
+
) : null}
|
|
157
|
+
|
|
158
|
+
{!hasBalance && (
|
|
159
|
+
<View style={styles.fundingContainer}>
|
|
160
|
+
<Text style={styles.fundingTitle}>Get Testnet Funds</Text>
|
|
161
|
+
<Text style={styles.fundingText}>
|
|
162
|
+
This example transaction sends a tiny amount of ETH from your wallet to itself.
|
|
163
|
+
</Text>
|
|
164
|
+
<Text style={styles.fundingText}>Get some testnet ETH from the Base Sepolia Faucet:</Text>
|
|
165
|
+
<TouchableOpacity
|
|
166
|
+
style={styles.faucetButton}
|
|
167
|
+
onPress={() => {
|
|
168
|
+
Alert.alert(
|
|
169
|
+
"Base Sepolia Faucet",
|
|
170
|
+
`Visit: https://portal.cdp.coinbase.com/products/faucet?token=ETH&address=${evmAddress}`,
|
|
171
|
+
[
|
|
172
|
+
{
|
|
173
|
+
text: "Copy URL",
|
|
174
|
+
onPress: async () => {
|
|
175
|
+
try {
|
|
176
|
+
await Clipboard.setStringAsync(
|
|
177
|
+
`https://portal.cdp.coinbase.com/products/faucet?token=ETH&address=${evmAddress}`,
|
|
178
|
+
);
|
|
179
|
+
Alert.alert("Copied!", "Faucet URL copied to clipboard");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
Alert.alert("Error", "Failed to copy URL");
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{ text: "OK", style: "default" },
|
|
186
|
+
],
|
|
187
|
+
);
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
<Text style={styles.faucetButtonText}>Base Sepolia Faucet</Text>
|
|
191
|
+
</TouchableOpacity>
|
|
192
|
+
</View>
|
|
193
|
+
)}
|
|
194
|
+
</View>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const styles = StyleSheet.create({
|
|
199
|
+
container: {
|
|
200
|
+
padding: 32,
|
|
201
|
+
backgroundColor: "#ffffff",
|
|
202
|
+
borderRadius: 16,
|
|
203
|
+
borderWidth: 1,
|
|
204
|
+
borderColor: "#dcdcdc",
|
|
205
|
+
marginHorizontal: 0,
|
|
206
|
+
marginVertical: 8,
|
|
207
|
+
alignItems: "stretch",
|
|
208
|
+
},
|
|
209
|
+
title: {
|
|
210
|
+
fontSize: 20,
|
|
211
|
+
fontWeight: "500",
|
|
212
|
+
marginBottom: 16,
|
|
213
|
+
textAlign: "center",
|
|
214
|
+
color: "#111111",
|
|
215
|
+
},
|
|
216
|
+
infoContainer: {
|
|
217
|
+
marginBottom: 16,
|
|
218
|
+
width: "100%",
|
|
219
|
+
},
|
|
220
|
+
label: {
|
|
221
|
+
fontSize: 14,
|
|
222
|
+
fontWeight: "600",
|
|
223
|
+
color: "#757575",
|
|
224
|
+
marginBottom: 4,
|
|
225
|
+
},
|
|
226
|
+
value: {
|
|
227
|
+
fontSize: 14,
|
|
228
|
+
color: "#111111",
|
|
229
|
+
fontFamily: "monospace",
|
|
230
|
+
textAlign: "left",
|
|
231
|
+
},
|
|
232
|
+
button: {
|
|
233
|
+
backgroundColor: "#008080",
|
|
234
|
+
padding: 15,
|
|
235
|
+
borderRadius: 8,
|
|
236
|
+
alignItems: "center",
|
|
237
|
+
alignSelf: "center",
|
|
238
|
+
marginVertical: 16,
|
|
239
|
+
minWidth: 188,
|
|
240
|
+
paddingHorizontal: 32,
|
|
241
|
+
},
|
|
242
|
+
buttonDisabled: {
|
|
243
|
+
opacity: 0.6,
|
|
244
|
+
},
|
|
245
|
+
buttonText: {
|
|
246
|
+
color: "#ffffff",
|
|
247
|
+
fontSize: 16,
|
|
248
|
+
fontWeight: "600",
|
|
249
|
+
},
|
|
250
|
+
errorContainer: {
|
|
251
|
+
backgroundColor: "#ffebee",
|
|
252
|
+
padding: 12,
|
|
253
|
+
borderRadius: 8,
|
|
254
|
+
borderColor: "#f44336",
|
|
255
|
+
borderWidth: 1,
|
|
256
|
+
width: "100%",
|
|
257
|
+
},
|
|
258
|
+
errorText: {
|
|
259
|
+
color: "#c62828",
|
|
260
|
+
fontSize: 14,
|
|
261
|
+
},
|
|
262
|
+
successContainer: {
|
|
263
|
+
backgroundColor: "rgba(76, 175, 80, 0.1)",
|
|
264
|
+
padding: 12,
|
|
265
|
+
borderRadius: 8,
|
|
266
|
+
borderColor: "rgba(76, 175, 80, 0.3)",
|
|
267
|
+
borderWidth: 1,
|
|
268
|
+
width: "100%",
|
|
269
|
+
},
|
|
270
|
+
successTitle: {
|
|
271
|
+
color: "#4caf50",
|
|
272
|
+
fontSize: 16,
|
|
273
|
+
fontWeight: "600",
|
|
274
|
+
marginBottom: 8,
|
|
275
|
+
},
|
|
276
|
+
hashText: {
|
|
277
|
+
color: "#4caf50",
|
|
278
|
+
fontSize: 12,
|
|
279
|
+
fontFamily: "monospace",
|
|
280
|
+
marginBottom: 4,
|
|
281
|
+
},
|
|
282
|
+
fundingContainer: {
|
|
283
|
+
marginTop: 20,
|
|
284
|
+
padding: 16,
|
|
285
|
+
backgroundColor: "rgba(255, 193, 7, 0.1)",
|
|
286
|
+
borderRadius: 8,
|
|
287
|
+
borderWidth: 1,
|
|
288
|
+
borderColor: "rgba(255, 193, 7, 0.3)",
|
|
289
|
+
width: "100%",
|
|
290
|
+
},
|
|
291
|
+
fundingTitle: {
|
|
292
|
+
fontSize: 16,
|
|
293
|
+
fontWeight: "600",
|
|
294
|
+
color: "#111111",
|
|
295
|
+
marginBottom: 8,
|
|
296
|
+
textAlign: "center",
|
|
297
|
+
},
|
|
298
|
+
fundingText: {
|
|
299
|
+
fontSize: 14,
|
|
300
|
+
color: "#757575",
|
|
301
|
+
marginBottom: 8,
|
|
302
|
+
textAlign: "center",
|
|
303
|
+
lineHeight: 20,
|
|
304
|
+
},
|
|
305
|
+
faucetButton: {
|
|
306
|
+
backgroundColor: "#008080",
|
|
307
|
+
padding: 12,
|
|
308
|
+
borderRadius: 8,
|
|
309
|
+
alignItems: "center",
|
|
310
|
+
marginTop: 8,
|
|
311
|
+
},
|
|
312
|
+
faucetButtonText: {
|
|
313
|
+
color: "#ffffff",
|
|
314
|
+
fontSize: 14,
|
|
315
|
+
fontWeight: "600",
|
|
316
|
+
},
|
|
317
|
+
hashContainer: {
|
|
318
|
+
marginBottom: 12,
|
|
319
|
+
},
|
|
320
|
+
hashLabel: {
|
|
321
|
+
color: "#4caf50",
|
|
322
|
+
fontSize: 14,
|
|
323
|
+
fontWeight: "600",
|
|
324
|
+
marginBottom: 4,
|
|
325
|
+
},
|
|
326
|
+
hashButton: {
|
|
327
|
+
backgroundColor: "rgba(76, 175, 80, 0.1)",
|
|
328
|
+
borderRadius: 8,
|
|
329
|
+
padding: 8,
|
|
330
|
+
borderWidth: 1,
|
|
331
|
+
borderColor: "rgba(76, 175, 80, 0.3)",
|
|
332
|
+
},
|
|
333
|
+
copyHint: {
|
|
334
|
+
color: "#4caf50",
|
|
335
|
+
fontSize: 10,
|
|
336
|
+
fontStyle: "italic",
|
|
337
|
+
textAlign: "center",
|
|
338
|
+
},
|
|
339
|
+
copyableInfo: {
|
|
340
|
+
backgroundColor: "rgba(0, 128, 128, 0.1)",
|
|
341
|
+
borderRadius: 8,
|
|
342
|
+
padding: 12,
|
|
343
|
+
borderWidth: 1,
|
|
344
|
+
borderColor: "rgba(0, 128, 128, 0.3)",
|
|
345
|
+
},
|
|
346
|
+
copyableText: {
|
|
347
|
+
fontSize: 12,
|
|
348
|
+
fontFamily: "monospace",
|
|
349
|
+
color: "#008080",
|
|
350
|
+
textAlign: "center",
|
|
351
|
+
},
|
|
352
|
+
addressCopyHint: {
|
|
353
|
+
fontSize: 10,
|
|
354
|
+
color: "#008080",
|
|
355
|
+
fontStyle: "italic",
|
|
356
|
+
textAlign: "center",
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
export default EOATransaction;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# React Native Expo Example
|
|
2
|
+
|
|
3
|
+
A React Native Expo example demonstrating CDP Embedded Wallet SDK integration with React Native hooks.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js 20+ and pnpm.
|
|
8
|
+
- For iOS: Xcode and iOS Simulator.
|
|
9
|
+
- For Android: Android Studio and Android emulator.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
### Setting up Simulators
|
|
14
|
+
|
|
15
|
+
#### iOS Simulator
|
|
16
|
+
|
|
17
|
+
1. Install [Xcode](https://developer.apple.com/xcode/).
|
|
18
|
+
2. On initial load, Xcode will ask which platforms you want to install. Select the iOS platform.
|
|
19
|
+
- a. If you missed it on initial load, then open Xcode → Preferences → Components.
|
|
20
|
+
- b. Install the iOS simulator version you want to test
|
|
21
|
+
3. Expo will automatically start the simulator when you run the app.
|
|
22
|
+
|
|
23
|
+
#### Android Emulator
|
|
24
|
+
|
|
25
|
+
1. Install Android Studio.
|
|
26
|
+
2. Open Android Studio → AVD Manager.
|
|
27
|
+
3. Create a new virtual device:
|
|
28
|
+
- Choose a device definition (e.g., Pixel 7).
|
|
29
|
+
- Select a system image (API level 30+).
|
|
30
|
+
- Configure settings and finish.
|
|
31
|
+
4. Expo will automatically start the emulator when you run the app.
|
|
32
|
+
|
|
33
|
+
### Setting up the app
|
|
34
|
+
|
|
35
|
+
1. Install dependencies:
|
|
36
|
+
```bash
|
|
37
|
+
pnpm install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. Copy environment variables:
|
|
41
|
+
```bash
|
|
42
|
+
cp .env.example .env
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. Fill in your CDP API credentials in `.env`.
|
|
46
|
+
|
|
47
|
+
## Running the App
|
|
48
|
+
|
|
49
|
+
**iOS:**
|
|
50
|
+
```bash
|
|
51
|
+
pnpm run ios
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Android:**
|
|
55
|
+
```bash
|
|
56
|
+
pnpm run android
|
|
57
|
+
```
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { useCurrentUser, useSendUserOperation } from "@coinbase/cdp-hooks";
|
|
2
|
+
import * as Clipboard from "expo-clipboard";
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { View, Text, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from "react-native";
|
|
5
|
+
import { createPublicClient, http, formatEther } from "viem";
|
|
6
|
+
import { baseSepolia } from "viem/chains";
|
|
7
|
+
|
|
8
|
+
const client = createPublicClient({
|
|
9
|
+
chain: baseSepolia,
|
|
10
|
+
transport: http(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
onSuccess?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This component demonstrates how to send a gasless transaction using Smart Accounts.
|
|
19
|
+
*
|
|
20
|
+
* @param {Props} props - The props for the SmartAccountTransaction component.
|
|
21
|
+
* @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
|
|
22
|
+
* @returns A component that displays a Smart Account transaction form and result.
|
|
23
|
+
*/
|
|
24
|
+
function SmartAccountTransaction(props: Props) {
|
|
25
|
+
const { onSuccess } = props;
|
|
26
|
+
const { currentUser } = useCurrentUser();
|
|
27
|
+
const { sendUserOperation, data, error, status } = useSendUserOperation();
|
|
28
|
+
const [balance, setBalance] = useState<bigint | undefined>(undefined);
|
|
29
|
+
const [errorMessage, setErrorMessage] = useState("");
|
|
30
|
+
|
|
31
|
+
const smartAccount = currentUser?.evmSmartAccounts?.[0];
|
|
32
|
+
|
|
33
|
+
const formattedBalance = useMemo(() => {
|
|
34
|
+
if (balance === undefined) return undefined;
|
|
35
|
+
return formatEther(balance);
|
|
36
|
+
}, [balance]);
|
|
37
|
+
|
|
38
|
+
const getBalance = useCallback(async () => {
|
|
39
|
+
if (!smartAccount) return;
|
|
40
|
+
const balance = await client.getBalance({
|
|
41
|
+
address: smartAccount,
|
|
42
|
+
});
|
|
43
|
+
setBalance(balance);
|
|
44
|
+
}, [smartAccount]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
getBalance();
|
|
48
|
+
const interval = setInterval(getBalance, 5000);
|
|
49
|
+
return () => clearInterval(interval);
|
|
50
|
+
}, [getBalance]);
|
|
51
|
+
|
|
52
|
+
const handleSendUserOperation = async () => {
|
|
53
|
+
if (!smartAccount) {
|
|
54
|
+
Alert.alert("Error", "No Smart Account available");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setErrorMessage("");
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Example: Send a simple transaction (you can customize this)
|
|
62
|
+
const result = await sendUserOperation({
|
|
63
|
+
evmSmartAccount: smartAccount,
|
|
64
|
+
network: "base-sepolia",
|
|
65
|
+
calls: [
|
|
66
|
+
{
|
|
67
|
+
to: smartAccount,
|
|
68
|
+
value: 0n, // 0 ETH
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (result?.userOperationHash) {
|
|
74
|
+
Alert.alert("Success", "User Operation sent successfully!");
|
|
75
|
+
onSuccess?.();
|
|
76
|
+
getBalance();
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const message = err instanceof Error ? err.message : "Failed to send user operation";
|
|
80
|
+
setErrorMessage(message);
|
|
81
|
+
Alert.alert("Transaction Failed", message);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const isLoading = status === "pending";
|
|
86
|
+
const isSuccess = status === "success" && data;
|
|
87
|
+
|
|
88
|
+
const copyToClipboard = async (text: string, label: string) => {
|
|
89
|
+
try {
|
|
90
|
+
await Clipboard.setStringAsync(text);
|
|
91
|
+
Alert.alert("Copied!", `${label} copied to clipboard`);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
Alert.alert("Error", "Failed to copy to clipboard");
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<View style={styles.container}>
|
|
99
|
+
<Text style={styles.title}>Smart Account Transaction</Text>
|
|
100
|
+
<Text style={styles.subtitle}>Gasless transactions with Smart Accounts</Text>
|
|
101
|
+
|
|
102
|
+
<View style={styles.infoContainer}>
|
|
103
|
+
<Text style={styles.label}>Smart Account Address:</Text>
|
|
104
|
+
<TouchableOpacity
|
|
105
|
+
style={styles.copyableInfo}
|
|
106
|
+
onPress={() => copyToClipboard(smartAccount!, "Smart Account Address")}
|
|
107
|
+
>
|
|
108
|
+
<Text style={styles.copyableText}>{smartAccount}</Text>
|
|
109
|
+
<Text style={styles.addressCopyHint}>Tap to copy</Text>
|
|
110
|
+
</TouchableOpacity>
|
|
111
|
+
</View>
|
|
112
|
+
|
|
113
|
+
<View style={styles.infoContainer}>
|
|
114
|
+
<Text style={styles.label}>Balance:</Text>
|
|
115
|
+
<Text style={styles.value}>
|
|
116
|
+
{formattedBalance === undefined ? "Loading..." : `${formattedBalance} ETH`}
|
|
117
|
+
</Text>
|
|
118
|
+
</View>
|
|
119
|
+
|
|
120
|
+
<TouchableOpacity
|
|
121
|
+
style={[styles.button, (!smartAccount || isLoading) && styles.buttonDisabled]}
|
|
122
|
+
onPress={handleSendUserOperation}
|
|
123
|
+
disabled={!smartAccount || isLoading}
|
|
124
|
+
>
|
|
125
|
+
{isLoading ? (
|
|
126
|
+
<ActivityIndicator color="#fff" />
|
|
127
|
+
) : (
|
|
128
|
+
<Text style={styles.buttonText}>Send User Operation</Text>
|
|
129
|
+
)}
|
|
130
|
+
</TouchableOpacity>
|
|
131
|
+
|
|
132
|
+
{errorMessage || error ? (
|
|
133
|
+
<View style={styles.errorContainer}>
|
|
134
|
+
<Text style={styles.errorText}>{errorMessage || error?.message}</Text>
|
|
135
|
+
</View>
|
|
136
|
+
) : null}
|
|
137
|
+
|
|
138
|
+
{isSuccess && data?.transactionHash ? (
|
|
139
|
+
<View style={styles.successContainer}>
|
|
140
|
+
<Text style={styles.successTitle}>User Operation Sent!</Text>
|
|
141
|
+
|
|
142
|
+
<View style={styles.hashContainer}>
|
|
143
|
+
<Text style={styles.hashLabel}>Transaction Hash:</Text>
|
|
144
|
+
<TouchableOpacity
|
|
145
|
+
style={styles.hashButton}
|
|
146
|
+
onPress={() => copyToClipboard(data.transactionHash!, "Transaction Hash")}
|
|
147
|
+
>
|
|
148
|
+
<Text style={styles.hashText}>{data.transactionHash}</Text>
|
|
149
|
+
<Text style={styles.copyHint}>📋 Tap to copy</Text>
|
|
150
|
+
</TouchableOpacity>
|
|
151
|
+
</View>
|
|
152
|
+
|
|
153
|
+
{data.userOpHash && (
|
|
154
|
+
<View style={styles.hashContainer}>
|
|
155
|
+
<Text style={styles.hashLabel}>User Op Hash:</Text>
|
|
156
|
+
<TouchableOpacity
|
|
157
|
+
style={styles.hashButton}
|
|
158
|
+
onPress={() => copyToClipboard(data.userOpHash!, "User Operation Hash")}
|
|
159
|
+
>
|
|
160
|
+
<Text style={styles.hashText}>{data.userOpHash}</Text>
|
|
161
|
+
<Text style={styles.copyHint}>📋 Tap to copy</Text>
|
|
162
|
+
</TouchableOpacity>
|
|
163
|
+
</View>
|
|
164
|
+
)}
|
|
165
|
+
</View>
|
|
166
|
+
) : null}
|
|
167
|
+
</View>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const styles = StyleSheet.create({
|
|
172
|
+
container: {
|
|
173
|
+
padding: 32,
|
|
174
|
+
backgroundColor: "#ffffff",
|
|
175
|
+
borderRadius: 16,
|
|
176
|
+
borderWidth: 1,
|
|
177
|
+
borderColor: "#dcdcdc",
|
|
178
|
+
marginHorizontal: 0,
|
|
179
|
+
marginVertical: 8,
|
|
180
|
+
alignItems: "stretch",
|
|
181
|
+
},
|
|
182
|
+
title: {
|
|
183
|
+
fontSize: 20,
|
|
184
|
+
fontWeight: "500",
|
|
185
|
+
marginBottom: 4,
|
|
186
|
+
textAlign: "center",
|
|
187
|
+
color: "#111111",
|
|
188
|
+
},
|
|
189
|
+
subtitle: {
|
|
190
|
+
fontSize: 14,
|
|
191
|
+
color: "#757575",
|
|
192
|
+
textAlign: "center",
|
|
193
|
+
marginBottom: 16,
|
|
194
|
+
},
|
|
195
|
+
infoContainer: {
|
|
196
|
+
marginBottom: 16,
|
|
197
|
+
width: "100%",
|
|
198
|
+
},
|
|
199
|
+
label: {
|
|
200
|
+
fontSize: 14,
|
|
201
|
+
fontWeight: "600",
|
|
202
|
+
color: "#757575",
|
|
203
|
+
marginBottom: 4,
|
|
204
|
+
},
|
|
205
|
+
value: {
|
|
206
|
+
fontSize: 14,
|
|
207
|
+
color: "#111111",
|
|
208
|
+
fontFamily: "monospace",
|
|
209
|
+
textAlign: "left",
|
|
210
|
+
},
|
|
211
|
+
button: {
|
|
212
|
+
backgroundColor: "#4CAF50",
|
|
213
|
+
padding: 15,
|
|
214
|
+
borderRadius: 8,
|
|
215
|
+
alignItems: "center",
|
|
216
|
+
alignSelf: "center",
|
|
217
|
+
marginVertical: 16,
|
|
218
|
+
minWidth: 188,
|
|
219
|
+
paddingHorizontal: 32,
|
|
220
|
+
},
|
|
221
|
+
buttonDisabled: {
|
|
222
|
+
opacity: 0.6,
|
|
223
|
+
},
|
|
224
|
+
buttonText: {
|
|
225
|
+
color: "#ffffff",
|
|
226
|
+
fontSize: 16,
|
|
227
|
+
fontWeight: "600",
|
|
228
|
+
},
|
|
229
|
+
errorContainer: {
|
|
230
|
+
backgroundColor: "#ffebee",
|
|
231
|
+
padding: 12,
|
|
232
|
+
borderRadius: 8,
|
|
233
|
+
borderColor: "#f44336",
|
|
234
|
+
borderWidth: 1,
|
|
235
|
+
width: "100%",
|
|
236
|
+
},
|
|
237
|
+
errorText: {
|
|
238
|
+
color: "#c62828",
|
|
239
|
+
fontSize: 14,
|
|
240
|
+
},
|
|
241
|
+
successContainer: {
|
|
242
|
+
backgroundColor: "rgba(76, 175, 80, 0.1)",
|
|
243
|
+
padding: 12,
|
|
244
|
+
borderRadius: 8,
|
|
245
|
+
borderColor: "rgba(76, 175, 80, 0.3)",
|
|
246
|
+
borderWidth: 1,
|
|
247
|
+
width: "100%",
|
|
248
|
+
},
|
|
249
|
+
successTitle: {
|
|
250
|
+
color: "#4caf50",
|
|
251
|
+
fontSize: 16,
|
|
252
|
+
fontWeight: "600",
|
|
253
|
+
marginBottom: 8,
|
|
254
|
+
},
|
|
255
|
+
hashContainer: {
|
|
256
|
+
marginBottom: 12,
|
|
257
|
+
},
|
|
258
|
+
hashLabel: {
|
|
259
|
+
color: "#4caf50",
|
|
260
|
+
fontSize: 14,
|
|
261
|
+
fontWeight: "600",
|
|
262
|
+
marginBottom: 4,
|
|
263
|
+
},
|
|
264
|
+
hashButton: {
|
|
265
|
+
backgroundColor: "rgba(76, 175, 80, 0.1)",
|
|
266
|
+
borderRadius: 8,
|
|
267
|
+
padding: 8,
|
|
268
|
+
borderWidth: 1,
|
|
269
|
+
borderColor: "rgba(76, 175, 80, 0.3)",
|
|
270
|
+
},
|
|
271
|
+
hashText: {
|
|
272
|
+
color: "#4caf50",
|
|
273
|
+
fontSize: 12,
|
|
274
|
+
fontFamily: "monospace",
|
|
275
|
+
marginBottom: 4,
|
|
276
|
+
},
|
|
277
|
+
copyHint: {
|
|
278
|
+
color: "#4caf50",
|
|
279
|
+
fontSize: 10,
|
|
280
|
+
fontStyle: "italic",
|
|
281
|
+
textAlign: "center",
|
|
282
|
+
},
|
|
283
|
+
copyableInfo: {
|
|
284
|
+
backgroundColor: "rgba(0, 128, 128, 0.1)",
|
|
285
|
+
borderRadius: 8,
|
|
286
|
+
padding: 12,
|
|
287
|
+
borderWidth: 1,
|
|
288
|
+
borderColor: "rgba(0, 128, 128, 0.3)",
|
|
289
|
+
},
|
|
290
|
+
copyableText: {
|
|
291
|
+
fontSize: 12,
|
|
292
|
+
fontFamily: "monospace",
|
|
293
|
+
color: "#008080",
|
|
294
|
+
textAlign: "center",
|
|
295
|
+
},
|
|
296
|
+
addressCopyHint: {
|
|
297
|
+
fontSize: 10,
|
|
298
|
+
color: "#008080",
|
|
299
|
+
fontStyle: "italic",
|
|
300
|
+
textAlign: "center",
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
export default SmartAccountTransaction;
|