@openzeppelin/adapter-stellar 1.0.0

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.
Files changed (165) hide show
  1. package/README.md +272 -0
  2. package/dist/config.cjs +21 -0
  3. package/dist/config.cjs.map +1 -0
  4. package/dist/config.d.cts +8 -0
  5. package/dist/config.d.cts.map +1 -0
  6. package/dist/config.d.mts +8 -0
  7. package/dist/config.d.mts.map +1 -0
  8. package/dist/config.mjs +20 -0
  9. package/dist/config.mjs.map +1 -0
  10. package/dist/index.cjs +7564 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +261 -0
  13. package/dist/index.d.cts.map +1 -0
  14. package/dist/index.d.mts +263 -0
  15. package/dist/index.d.mts.map +1 -0
  16. package/dist/index.mjs +7529 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/metadata.cjs +22 -0
  19. package/dist/metadata.cjs.map +1 -0
  20. package/dist/metadata.d.cts +7 -0
  21. package/dist/metadata.d.cts.map +1 -0
  22. package/dist/metadata.d.mts +7 -0
  23. package/dist/metadata.d.mts.map +1 -0
  24. package/dist/metadata.mjs +21 -0
  25. package/dist/metadata.mjs.map +1 -0
  26. package/dist/networks-BrV516-R.d.cts +15 -0
  27. package/dist/networks-BrV516-R.d.cts.map +1 -0
  28. package/dist/networks-C0MmhJcu.d.mts +15 -0
  29. package/dist/networks-C0MmhJcu.d.mts.map +1 -0
  30. package/dist/networks-DgUFSTiC.cjs +76 -0
  31. package/dist/networks-DgUFSTiC.cjs.map +1 -0
  32. package/dist/networks-QbEPbaGT.mjs +46 -0
  33. package/dist/networks-QbEPbaGT.mjs.map +1 -0
  34. package/dist/networks.cjs +8 -0
  35. package/dist/networks.d.cts +2 -0
  36. package/dist/networks.d.mts +2 -0
  37. package/dist/networks.mjs +3 -0
  38. package/dist/vite-config.cjs +43 -0
  39. package/dist/vite-config.cjs.map +1 -0
  40. package/dist/vite-config.d.cts +35 -0
  41. package/dist/vite-config.d.cts.map +1 -0
  42. package/dist/vite-config.d.mts +35 -0
  43. package/dist/vite-config.d.mts.map +1 -0
  44. package/dist/vite-config.mjs +42 -0
  45. package/dist/vite-config.mjs.map +1 -0
  46. package/package.json +114 -0
  47. package/src/__tests__/getDefaultServiceConfig.test.ts +105 -0
  48. package/src/access-control/actions.ts +214 -0
  49. package/src/access-control/feature-detection.ts +238 -0
  50. package/src/access-control/index.ts +54 -0
  51. package/src/access-control/indexer-client.ts +1474 -0
  52. package/src/access-control/onchain-reader.ts +446 -0
  53. package/src/access-control/service.ts +1431 -0
  54. package/src/access-control/validation.ts +256 -0
  55. package/src/adapter.ts +659 -0
  56. package/src/config.ts +43 -0
  57. package/src/configuration/__tests__/explorer.test.ts +80 -0
  58. package/src/configuration/__tests__/rpc.test.ts +355 -0
  59. package/src/configuration/execution.ts +83 -0
  60. package/src/configuration/explorer.ts +105 -0
  61. package/src/configuration/index.ts +5 -0
  62. package/src/configuration/network-services.ts +210 -0
  63. package/src/configuration/rpc.ts +270 -0
  64. package/src/configuration.ts +2 -0
  65. package/src/contract/__tests__/complete-type-coverage.test.ts +78 -0
  66. package/src/contract/index.ts +3 -0
  67. package/src/contract/loader.ts +498 -0
  68. package/src/contract/transformer.ts +1 -0
  69. package/src/contract/type.ts +65 -0
  70. package/src/index.ts +23 -0
  71. package/src/mapping/constants.ts +89 -0
  72. package/src/mapping/enum-metadata.ts +237 -0
  73. package/src/mapping/field-generator.ts +296 -0
  74. package/src/mapping/index.ts +5 -0
  75. package/src/mapping/struct-fields.ts +106 -0
  76. package/src/mapping/tuple-components.ts +43 -0
  77. package/src/mapping/type-coverage-validator.ts +151 -0
  78. package/src/mapping/type-mapper.ts +203 -0
  79. package/src/metadata.ts +16 -0
  80. package/src/networks/README.md +84 -0
  81. package/src/networks/index.ts +19 -0
  82. package/src/networks/mainnet.ts +20 -0
  83. package/src/networks/testnet.ts +20 -0
  84. package/src/networks.ts +2 -0
  85. package/src/query/handler.ts +411 -0
  86. package/src/query/index.ts +4 -0
  87. package/src/query/view-checker.ts +32 -0
  88. package/src/sac/spec-cache.ts +68 -0
  89. package/src/sac/spec-source.ts +35 -0
  90. package/src/sac/xdr.ts +101 -0
  91. package/src/transaction/components/AdvancedInfo.tsx +34 -0
  92. package/src/transaction/components/FeeConfiguration.tsx +41 -0
  93. package/src/transaction/components/StellarRelayerOptions.tsx +60 -0
  94. package/src/transaction/components/TransactionTiming.tsx +77 -0
  95. package/src/transaction/components/index.ts +5 -0
  96. package/src/transaction/components/useStellarRelayerOptions.ts +114 -0
  97. package/src/transaction/eoa.ts +229 -0
  98. package/src/transaction/execution-strategy.ts +33 -0
  99. package/src/transaction/formatter.ts +296 -0
  100. package/src/transaction/index.ts +4 -0
  101. package/src/transaction/relayer.ts +575 -0
  102. package/src/transaction/sender.ts +156 -0
  103. package/src/transform/index.ts +4 -0
  104. package/src/transform/input-parser.ts +9 -0
  105. package/src/transform/output-formatter.ts +133 -0
  106. package/src/transform/parsers/complex-parser.ts +157 -0
  107. package/src/transform/parsers/generic-parser.ts +171 -0
  108. package/src/transform/parsers/index.ts +86 -0
  109. package/src/transform/parsers/primitive-parser.ts +123 -0
  110. package/src/transform/parsers/scval-converter.ts +405 -0
  111. package/src/transform/parsers/struct-parser.ts +324 -0
  112. package/src/transform/parsers/types.ts +35 -0
  113. package/src/types/__tests__/artifacts.test.ts +89 -0
  114. package/src/types/artifacts.ts +19 -0
  115. package/src/utils/__tests__/artifacts.test.ts +77 -0
  116. package/src/utils/artifacts.ts +30 -0
  117. package/src/utils/formatting.ts +122 -0
  118. package/src/utils/index.ts +6 -0
  119. package/src/utils/input-parsing.ts +336 -0
  120. package/src/utils/safe-type-parser.ts +303 -0
  121. package/src/utils/stellar-types.ts +35 -0
  122. package/src/utils/type-detection.ts +163 -0
  123. package/src/utils/xdr-ordering.ts +36 -0
  124. package/src/validation/__tests__/address.test.ts +267 -0
  125. package/src/validation/address.ts +136 -0
  126. package/src/validation/eoa.ts +33 -0
  127. package/src/validation/index.ts +3 -0
  128. package/src/validation/relayer.ts +13 -0
  129. package/src/vite-config.ts +67 -0
  130. package/src/wallet/README.md +93 -0
  131. package/src/wallet/__tests__/connection.test.ts +72 -0
  132. package/src/wallet/components/StellarWalletUiRoot.tsx +161 -0
  133. package/src/wallet/components/account/AccountDisplay.tsx +50 -0
  134. package/src/wallet/components/connect/ConnectButton.tsx +100 -0
  135. package/src/wallet/components/connect/ConnectorDialog.tsx +125 -0
  136. package/src/wallet/components/index.ts +3 -0
  137. package/src/wallet/connection.ts +151 -0
  138. package/src/wallet/context/StellarWalletContext.ts +32 -0
  139. package/src/wallet/context/index.ts +4 -0
  140. package/src/wallet/context/useStellarWalletContext.ts +17 -0
  141. package/src/wallet/hooks/facade-hooks.ts +31 -0
  142. package/src/wallet/hooks/index.ts +7 -0
  143. package/src/wallet/hooks/useStellarAccount.ts +27 -0
  144. package/src/wallet/hooks/useStellarConnect.ts +60 -0
  145. package/src/wallet/hooks/useStellarDisconnect.ts +47 -0
  146. package/src/wallet/hooks/useUiKitConfig.ts +40 -0
  147. package/src/wallet/implementation/wallets-kit-implementation.ts +379 -0
  148. package/src/wallet/index.ts +11 -0
  149. package/src/wallet/services/__tests__/configResolutionService.test.ts +163 -0
  150. package/src/wallet/services/configResolutionService.ts +65 -0
  151. package/src/wallet/stellar-wallets-kit/StellarWalletsKitConnectButton.tsx +82 -0
  152. package/src/wallet/stellar-wallets-kit/__mocks__/@creit.tech/stellar-wallets-kit.ts +48 -0
  153. package/src/wallet/stellar-wallets-kit/__tests__/export-service.test.ts +93 -0
  154. package/src/wallet/stellar-wallets-kit/__tests__/stellarUiKitManager.test.ts +0 -0
  155. package/src/wallet/stellar-wallets-kit/config-generator.ts +75 -0
  156. package/src/wallet/stellar-wallets-kit/export-service.ts +19 -0
  157. package/src/wallet/stellar-wallets-kit/index.ts +3 -0
  158. package/src/wallet/stellar-wallets-kit/stellarUiKitManager.ts +235 -0
  159. package/src/wallet/types.ts +19 -0
  160. package/src/wallet/utils/__tests__/filterWalletComponents.test.ts +150 -0
  161. package/src/wallet/utils/__tests__/uiKitService.test.ts +189 -0
  162. package/src/wallet/utils/filterWalletComponents.ts +89 -0
  163. package/src/wallet/utils/index.ts +3 -0
  164. package/src/wallet/utils/stellarWalletImplementationManager.ts +118 -0
  165. package/src/wallet/utils/uiKitService.ts +74 -0
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { Control } from 'react-hook-form';
3
+
4
+ import { BooleanField, NumberField } from '@openzeppelin/ui-components';
5
+
6
+ import type { StellarRelayerFormData } from './useStellarRelayerOptions';
7
+
8
+ interface FeeConfigurationProps {
9
+ control: Control<StellarRelayerFormData>;
10
+ showBasicFeeOnly: boolean;
11
+ }
12
+
13
+ export const FeeConfiguration: React.FC<FeeConfigurationProps> = ({
14
+ control,
15
+ showBasicFeeOnly,
16
+ }) => {
17
+ return (
18
+ <div className="space-y-4">
19
+ <NumberField
20
+ id="maxFee"
21
+ label="Maximum Fee (stroops)"
22
+ name="transactionOptions.maxFee"
23
+ control={control}
24
+ placeholder="e.g., 1000000 (0.1 XLM)"
25
+ helperText="Maximum fee you're willing to pay in stroops (1 XLM = 10,000,000 stroops). Leave empty to use network defaults."
26
+ min={0}
27
+ step={1}
28
+ />
29
+
30
+ {!showBasicFeeOnly && (
31
+ <BooleanField
32
+ id="feeBump"
33
+ label="Enable Fee Bump"
34
+ name="transactionOptions.feeBump"
35
+ control={control}
36
+ helperText="Automatically increase fee if transaction gets stuck in the network."
37
+ />
38
+ )}
39
+ </div>
40
+ );
41
+ };
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@openzeppelin/ui-components';
4
+
5
+ import { AdvancedInfo } from './AdvancedInfo';
6
+ import { FeeConfiguration } from './FeeConfiguration';
7
+ import { TransactionTiming } from './TransactionTiming';
8
+ import { useStellarRelayerOptions } from './useStellarRelayerOptions';
9
+
10
+ /**
11
+ * Stellar-specific relayer transaction options component.
12
+ *
13
+ * Provides configuration for Stellar transaction parameters:
14
+ * - Basic: Simple max fee configuration with reasonable defaults
15
+ * - Advanced: Full control over maxFee, validUntil, and feeBump options
16
+ *
17
+ * Stellar transactions have different parameters compared to EVM:
18
+ * - maxFee: Maximum fee willing to pay (in stroops)
19
+ * - validUntil: Transaction expiration time
20
+ * - feeBump: Enable fee bump for stuck transactions
21
+ */
22
+ export const StellarRelayerOptions: React.FC<{
23
+ options: Record<string, unknown>;
24
+ onChange: (options: Record<string, unknown>) => void;
25
+ }> = ({ options, onChange }) => {
26
+ const [showAdvancedInfo, setShowAdvancedInfo] = React.useState(false);
27
+
28
+ const { control, configMode, handleModeChange } = useStellarRelayerOptions({
29
+ options,
30
+ onChange,
31
+ });
32
+
33
+ return (
34
+ <div className="space-y-4">
35
+ <AdvancedInfo
36
+ showAdvancedInfo={showAdvancedInfo}
37
+ onToggle={() => setShowAdvancedInfo(!showAdvancedInfo)}
38
+ />
39
+
40
+ <Tabs value={configMode} onValueChange={handleModeChange}>
41
+ <TabsList className="grid w-full grid-cols-2">
42
+ <TabsTrigger value="basic">Basic</TabsTrigger>
43
+ <TabsTrigger value="advanced">Advanced</TabsTrigger>
44
+ </TabsList>
45
+
46
+ <TabsContent value="basic" className="space-y-4">
47
+ <FeeConfiguration control={control} showBasicFeeOnly={true} />
48
+ </TabsContent>
49
+
50
+ <TabsContent value="advanced" className="space-y-4">
51
+ <div className="space-y-6">
52
+ <FeeConfiguration control={control} showBasicFeeOnly={false} />
53
+
54
+ <TransactionTiming control={control} />
55
+ </div>
56
+ </TabsContent>
57
+ </Tabs>
58
+ </div>
59
+ );
60
+ };
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { Control, Controller } from 'react-hook-form';
3
+
4
+ import { Button, DateTimeField } from '@openzeppelin/ui-components';
5
+
6
+ import type { StellarRelayerFormData } from './useStellarRelayerOptions';
7
+
8
+ interface TransactionTimingProps {
9
+ control: Control<StellarRelayerFormData>;
10
+ }
11
+
12
+ export const TransactionTiming: React.FC<TransactionTimingProps> = ({ control }) => {
13
+ // Helper function to generate a datetime-local value for 1 hour from now
14
+ const getOneHourFromNow = () => {
15
+ const now = new Date();
16
+ now.setHours(now.getHours() + 1);
17
+ // Format for datetime-local input (YYYY-MM-DDTHH:mm)
18
+ return now.toISOString().slice(0, 16);
19
+ };
20
+
21
+ // Helper function to generate a datetime-local value for 24 hours from now
22
+ const getTwentyFourHoursFromNow = () => {
23
+ const now = new Date();
24
+ now.setHours(now.getHours() + 24);
25
+ return now.toISOString().slice(0, 16);
26
+ };
27
+
28
+ return (
29
+ <div className="space-y-4">
30
+ <DateTimeField
31
+ id="validUntil"
32
+ label="Transaction Expiration"
33
+ name="transactionOptions.validUntil"
34
+ control={control}
35
+ placeholder="YYYY-MM-DDTHH:mm"
36
+ helperText="Set when this transaction should expire. Leave empty for no expiration limit."
37
+ />
38
+ <div className="flex gap-2 pt-1">
39
+ <Controller
40
+ name="transactionOptions.validUntil"
41
+ control={control}
42
+ render={({ field }) => (
43
+ <>
44
+ <Button
45
+ type="button"
46
+ variant="outline"
47
+ size="sm"
48
+ onClick={() => field.onChange(new Date(getOneHourFromNow()).toISOString())}
49
+ className="text-xs"
50
+ >
51
+ +1 Hour
52
+ </Button>
53
+ <Button
54
+ type="button"
55
+ variant="outline"
56
+ size="sm"
57
+ onClick={() => field.onChange(new Date(getTwentyFourHoursFromNow()).toISOString())}
58
+ className="text-xs"
59
+ >
60
+ +24 Hours
61
+ </Button>
62
+ <Button
63
+ type="button"
64
+ variant="outline"
65
+ size="sm"
66
+ onClick={() => field.onChange('')}
67
+ className="text-xs"
68
+ >
69
+ Clear
70
+ </Button>
71
+ </>
72
+ )}
73
+ />
74
+ </div>
75
+ </div>
76
+ );
77
+ };
@@ -0,0 +1,5 @@
1
+ export { StellarRelayerOptions } from './StellarRelayerOptions';
2
+ export { useStellarRelayerOptions } from './useStellarRelayerOptions';
3
+ export { AdvancedInfo } from './AdvancedInfo';
4
+ export { FeeConfiguration } from './FeeConfiguration';
5
+ export { TransactionTiming } from './TransactionTiming';
@@ -0,0 +1,114 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useForm } from 'react-hook-form';
3
+
4
+ import type { StellarRelayerTransactionOptions } from '../relayer';
5
+
6
+ export interface StellarRelayerFormData {
7
+ transactionOptions: StellarRelayerTransactionOptions;
8
+ }
9
+
10
+ interface UseStellarRelayerOptionsProps {
11
+ options: Record<string, unknown>;
12
+ onChange: (options: Record<string, unknown>) => void;
13
+ }
14
+
15
+ export const useStellarRelayerOptions = ({ options, onChange }: UseStellarRelayerOptionsProps) => {
16
+ // Store the latest onChange callback in a ref to avoid dependency issues
17
+ const onChangeRef = useRef(onChange);
18
+ onChangeRef.current = onChange;
19
+
20
+ // Initialize form with options from parent (only on mount to prevent loops)
21
+ const initialOptions: StellarRelayerTransactionOptions = {
22
+ maxFee: options.maxFee as number | undefined,
23
+ validUntil: options.validUntil as string | undefined,
24
+ feeBump: options.feeBump as boolean | undefined,
25
+ };
26
+
27
+ const { control, setValue, watch } = useForm<StellarRelayerFormData>({
28
+ defaultValues: {
29
+ transactionOptions: initialOptions,
30
+ },
31
+ });
32
+
33
+ const formValues = watch('transactionOptions');
34
+ const isInitialMount = useRef(true);
35
+
36
+ // Track user's intended mode (separate from auto-detection)
37
+ const [userMode, setUserMode] = useState<'basic' | 'advanced'>(() => {
38
+ // Determine initial mode based on existing settings
39
+ const hasAdvancedSettings = Boolean(formValues.validUntil || formValues.feeBump);
40
+ return hasAdvancedSettings ? 'advanced' : 'basic';
41
+ });
42
+
43
+ const configMode = userMode;
44
+
45
+ // Handle initial mount - ensure any default values are communicated to parent
46
+ useEffect(() => {
47
+ if (isInitialMount.current) {
48
+ isInitialMount.current = false;
49
+ // No default values to set for Stellar (unlike EVM's Speed.FAST)
50
+ return;
51
+ }
52
+ }, []);
53
+
54
+ // Notify parent of changes after initial mount - watch specific fields
55
+ useEffect(() => {
56
+ if (isInitialMount.current) {
57
+ return;
58
+ }
59
+
60
+ const timeoutId = setTimeout(() => {
61
+ const newOptions: StellarRelayerTransactionOptions = {};
62
+
63
+ // Set maxFee if provided
64
+ if (formValues.maxFee !== undefined && formValues.maxFee !== null) {
65
+ newOptions.maxFee = formValues.maxFee;
66
+ }
67
+
68
+ // Set validUntil if provided (check for actual date string, not just truthy)
69
+ if (formValues.validUntil && formValues.validUntil.trim() !== '') {
70
+ newOptions.validUntil = formValues.validUntil;
71
+ }
72
+
73
+ // Set feeBump if enabled
74
+ if (formValues.feeBump !== undefined) {
75
+ newOptions.feeBump = formValues.feeBump;
76
+ }
77
+
78
+ // Soroban transactions do not support memos; do not propagate any
79
+
80
+ onChangeRef.current(newOptions as Record<string, unknown>);
81
+ }, 100);
82
+
83
+ return () => clearTimeout(timeoutId);
84
+ }, [formValues.maxFee, formValues.validUntil, formValues.feeBump]);
85
+
86
+ // Event handlers
87
+ const handleModeChange = (mode: string) => {
88
+ const newMode = mode as 'basic' | 'advanced';
89
+ setUserMode(newMode);
90
+
91
+ if (newMode === 'basic') {
92
+ // Reset advanced options when switching to basic mode
93
+ setValue('transactionOptions', {
94
+ ...formValues,
95
+ validUntil: undefined,
96
+ feeBump: undefined,
97
+ });
98
+ } else {
99
+ // When switching to advanced mode, keep existing values or set reasonable defaults
100
+ setValue('transactionOptions', {
101
+ ...formValues,
102
+ validUntil: formValues.validUntil || undefined,
103
+ feeBump: formValues.feeBump || false,
104
+ });
105
+ }
106
+ };
107
+
108
+ return {
109
+ control,
110
+ formValues,
111
+ configMode,
112
+ handleModeChange,
113
+ };
114
+ };
@@ -0,0 +1,229 @@
1
+ import {
2
+ Account,
3
+ BASE_FEE,
4
+ Contract,
5
+ rpc as StellarRpc,
6
+ TransactionBuilder,
7
+ } from '@stellar/stellar-sdk';
8
+
9
+ import type {
10
+ ExecutionConfig,
11
+ StellarNetworkConfig,
12
+ TransactionStatusUpdate,
13
+ TxStatus,
14
+ } from '@openzeppelin/ui-types';
15
+ import { logger, userRpcConfigService } from '@openzeppelin/ui-utils';
16
+
17
+ import { CALLER_PLACEHOLDER } from '../access-control/actions';
18
+ import { valueToScVal } from '../transform/input-parser';
19
+ import { getStellarWalletConnectionStatus, signTransaction } from '../wallet/connection';
20
+ import { ExecutionStrategy } from './execution-strategy';
21
+ import type { StellarTransactionData } from './formatter';
22
+
23
+ const SYSTEM_LOG_TAG = 'EoaExecutionStrategy';
24
+
25
+ /**
26
+ * Get Soroban RPC Server instance with proper configuration
27
+ */
28
+ function getSorobanRpcServer(networkConfig: StellarNetworkConfig): StellarRpc.Server {
29
+ const customRpcConfig = userRpcConfigService.getUserRpcConfig(networkConfig.id);
30
+ const rpcUrl = customRpcConfig?.url || networkConfig.sorobanRpcUrl;
31
+
32
+ if (!rpcUrl) {
33
+ throw new Error(`No Soroban RPC URL available for network ${networkConfig.name}`);
34
+ }
35
+
36
+ // Allow HTTP for localhost development
37
+ const allowHttp = new URL(rpcUrl).hostname === 'localhost';
38
+
39
+ return new StellarRpc.Server(rpcUrl, {
40
+ allowHttp,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Implements the ExecutionStrategy for a standard Externally Owned Account (EOA).
46
+ * This strategy involves signing and broadcasting a transaction directly from the user's
47
+ * connected Stellar wallet, which is the most common way of interacting with Stellar/Soroban contracts.
48
+ */
49
+ export class EoaExecutionStrategy implements ExecutionStrategy {
50
+ public async execute(
51
+ transactionData: StellarTransactionData,
52
+ executionConfig: ExecutionConfig,
53
+ networkConfig: StellarNetworkConfig,
54
+ onStatusChange: (status: TxStatus, details: TransactionStatusUpdate) => void,
55
+ // runtimeApiKey is unused in EOA strategy but required by the interface
56
+ _runtimeApiKey?: string
57
+ ): Promise<{ txHash: string }> {
58
+ logger.info(SYSTEM_LOG_TAG, 'Using Stellar EOA execution strategy');
59
+
60
+ // Validate execution config is for EOA
61
+ if (executionConfig.method !== 'eoa') {
62
+ throw new Error(`Expected EOA execution config, got: ${executionConfig.method}`);
63
+ }
64
+
65
+ return this.executeEoaTransaction(transactionData, networkConfig, onStatusChange);
66
+ }
67
+
68
+ private async executeEoaTransaction(
69
+ txData: StellarTransactionData,
70
+ stellarConfig: StellarNetworkConfig,
71
+ onStatusChange: (status: TxStatus, details: TransactionStatusUpdate) => void
72
+ ): Promise<{ txHash: string }> {
73
+ try {
74
+ // --- Step 1: Get RPC Server and Connected Wallet Address --- //
75
+ const rpcServer = getSorobanRpcServer(stellarConfig);
76
+ const connectedAddress = this.getConnectedWalletAddress();
77
+
78
+ logger.info(SYSTEM_LOG_TAG, `Connected address: ${connectedAddress}`);
79
+
80
+ // --- Step 2: Get Account Details --- //
81
+ let sourceAccount: Account;
82
+ try {
83
+ const accountResponse = await rpcServer.getAccount(connectedAddress);
84
+ sourceAccount = new Account(connectedAddress, accountResponse.sequenceNumber());
85
+ } catch (error) {
86
+ throw new Error(`Failed to load account details: ${(error as Error).message}`);
87
+ }
88
+
89
+ // --- Step 3: Build Transaction --- //
90
+ const contract = new Contract(txData.contractAddress);
91
+
92
+ const transactionBuilder = new TransactionBuilder(sourceAccount, {
93
+ fee: BASE_FEE,
94
+ networkPassphrase: stellarConfig.networkPassphrase,
95
+ });
96
+
97
+ // Replace CALLER_PLACEHOLDER with the connected wallet address
98
+ // This supports OpenZeppelin Stellar access control functions that require a caller parameter
99
+ const resolvedArgs = txData.args.map((arg) =>
100
+ arg === CALLER_PLACEHOLDER ? connectedAddress : arg
101
+ );
102
+
103
+ // Add the contract call operation (convert args to ScVal with comprehensive type support)
104
+ const scValArgs = resolvedArgs.map((arg, index) => {
105
+ const argType = txData.argTypes[index];
106
+ const argSchema = txData.argSchema?.[index]; // Pass schema for struct field type resolution
107
+
108
+ return valueToScVal(arg, argType, argSchema);
109
+ });
110
+
111
+ transactionBuilder.addOperation(contract.call(txData.functionName, ...scValArgs));
112
+
113
+ // Set timeout (default 30 seconds)
114
+ transactionBuilder.setTimeout(30);
115
+
116
+ let transaction = transactionBuilder.build();
117
+
118
+ // --- Step 4: Simulate Transaction First --- //
119
+ try {
120
+ const simulation = await rpcServer.simulateTransaction(transaction);
121
+
122
+ if (StellarRpc.Api.isSimulationError(simulation)) {
123
+ throw new Error(`Transaction simulation failed: ${simulation.error}`);
124
+ }
125
+
126
+ // Prepare the transaction with simulation results
127
+ transaction = await rpcServer.prepareTransaction(transaction);
128
+ } catch (error) {
129
+ throw new Error(`Transaction simulation/preparation failed: ${(error as Error).message}`);
130
+ }
131
+
132
+ onStatusChange('pendingSignature', {});
133
+
134
+ // --- Step 5: Sign Transaction --- //
135
+ try {
136
+ const signResult = await signTransaction(transaction.toXDR(), connectedAddress);
137
+ const signedTx = TransactionBuilder.fromXDR(
138
+ signResult.signedTxXdr,
139
+ stellarConfig.networkPassphrase
140
+ );
141
+
142
+ // Type guard to ensure we have a regular Transaction, not a FeeBumpTransaction
143
+ if ('memo' in signedTx && 'sequence' in signedTx) {
144
+ transaction = signedTx;
145
+ } else {
146
+ throw new Error('Unexpected transaction type returned from signing');
147
+ }
148
+ } catch (error) {
149
+ if ((error as Error).message.includes('User declined')) {
150
+ throw new Error('Transaction was rejected by user');
151
+ }
152
+ throw new Error(`Failed to sign transaction: ${(error as Error).message}`);
153
+ }
154
+
155
+ onStatusChange('pendingConfirmation', {});
156
+
157
+ // --- Step 6: Send Transaction --- //
158
+ let sendResult: StellarRpc.Api.SendTransactionResponse;
159
+ try {
160
+ sendResult = await rpcServer.sendTransaction(transaction);
161
+ } catch (error) {
162
+ throw new Error(`Failed to broadcast transaction: ${(error as Error).message}`);
163
+ }
164
+
165
+ if (sendResult.status !== 'PENDING') {
166
+ throw new Error(`Transaction failed to submit: ${sendResult.status}`);
167
+ }
168
+
169
+ const txHash = sendResult.hash;
170
+ logger.info(SYSTEM_LOG_TAG, `Transaction submitted successfully: ${txHash}`);
171
+
172
+ // Status remains 'pendingConfirmation' during confirmation wait
173
+
174
+ // --- Step 7: Wait for Confirmation --- //
175
+ try {
176
+ let txResponse;
177
+ const MAX_ATTEMPTS = 10;
178
+ let attempts = 0;
179
+
180
+ while (attempts++ < MAX_ATTEMPTS && txResponse?.status !== 'SUCCESS') {
181
+ await new Promise((resolve) => setTimeout(resolve, 1000));
182
+ txResponse = await rpcServer.getTransaction(txHash);
183
+
184
+ switch (txResponse.status) {
185
+ case 'FAILED':
186
+ throw new Error(`Transaction failed: ${JSON.stringify(txResponse.resultXdr)}`);
187
+ case 'NOT_FOUND':
188
+ continue;
189
+ case 'SUCCESS':
190
+ break;
191
+ default:
192
+ // Continue waiting
193
+ }
194
+ }
195
+
196
+ if (attempts >= MAX_ATTEMPTS || txResponse?.status !== 'SUCCESS') {
197
+ logger.warn(SYSTEM_LOG_TAG, `Transaction confirmation timeout for ${txHash}`);
198
+ // Don't throw error, just return the hash - transaction might still succeed
199
+ }
200
+ } catch (confirmError) {
201
+ // Log the error but don't fail the transaction - it was already submitted
202
+ logger.error(SYSTEM_LOG_TAG, 'Error waiting for confirmation:', confirmError);
203
+ }
204
+
205
+ onStatusChange('success', {
206
+ txHash,
207
+ });
208
+
209
+ return { txHash };
210
+ } catch (error) {
211
+ const errorMessage = `Failed to execute Stellar EOA transaction: ${(error as Error).message}`;
212
+ logger.error(SYSTEM_LOG_TAG, errorMessage, error);
213
+
214
+ onStatusChange('error', {});
215
+
216
+ throw new Error(errorMessage);
217
+ }
218
+ }
219
+
220
+ private getConnectedWalletAddress(): string {
221
+ const connectionStatus = getStellarWalletConnectionStatus();
222
+
223
+ if (!connectionStatus.isConnected || !connectionStatus.address) {
224
+ throw new Error('No connected wallet found. Please connect your Stellar wallet first.');
225
+ }
226
+
227
+ return connectionStatus.address;
228
+ }
229
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ ExecutionConfig,
3
+ StellarNetworkConfig,
4
+ TransactionStatusUpdate,
5
+ TxStatus,
6
+ } from '@openzeppelin/ui-types';
7
+
8
+ import type { StellarTransactionData } from './formatter';
9
+
10
+ /**
11
+ * Defines a common interface for different transaction execution strategies.
12
+ * This allows the adapter to remain a lean orchestrator that selects the appropriate strategy
13
+ * at runtime based on the user's configuration.
14
+ */
15
+ export interface ExecutionStrategy {
16
+ /**
17
+ * Executes a transaction according to the specific strategy.
18
+ *
19
+ * @param transactionData The contract call parameters, including contract address, function name, and args.
20
+ * @param executionConfig The configuration for the selected execution method.
21
+ * @param networkConfig The Stellar network configuration to use for the transaction.
22
+ * @param onStatusChange A callback to report real-time status updates to the UI.
23
+ * @param runtimeApiKey Optional session-only API key for methods like Relayer.
24
+ * @returns A promise that resolves to an object containing the final transaction hash.
25
+ */
26
+ execute(
27
+ transactionData: StellarTransactionData,
28
+ executionConfig: ExecutionConfig,
29
+ networkConfig: StellarNetworkConfig,
30
+ onStatusChange: (status: TxStatus, details: TransactionStatusUpdate) => void,
31
+ runtimeApiKey?: string
32
+ ): Promise<{ txHash: string }>;
33
+ }