Skip to content

Commit 3678a61

Browse files
authored
Merge pull request #34 from ProvableHQ/feature/adapter-transaction-status-method
Adds transactionStatus() method
2 parents 0c9b7d3 + c549f18 commit 3678a61

15 files changed

Lines changed: 391 additions & 202 deletions

File tree

examples/react-app/src/components/functions/ExecuteTransaction.tsx

Lines changed: 143 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { useState, useEffect, useMemo } from 'react';
1+
import { useState, useEffect, useMemo, useRef } from 'react';
22
import { Button } from '@/components/ui/button';
33
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
44
import { Label } from '@/components/ui/label';
55
import { Input } from '@/components/ui/input';
66
import { Alert, AlertDescription } from '@/components/ui/alert';
7-
import { Send, Copy, CheckCircle, Loader2, Zap, Code, Code2 } from 'lucide-react';
7+
import { Send, Copy, CheckCircle, Loader2, Zap, Code, Code2, XCircle } from 'lucide-react';
88
import { toast } from 'sonner';
99
import { useWallet } from '@provablehq/aleo-wallet-adaptor-react';
10-
import { Network } from '@provablehq/aleo-types';
10+
import { Network, TransactionStatus } from '@provablehq/aleo-types';
1111
import { HookCodeModal } from '../HookCodeModal';
1212
import { ProgramAutocomplete } from '../ProgramAutocomplete';
1313
import { FunctionSelector } from '../FunctionSelector';
@@ -18,14 +18,23 @@ import { functionNameAtom, programAtom, useDynamicInputsAtom } from '@/lib/store
1818
import { useAtom } from 'jotai';
1919

2020
export function ExecuteTransaction() {
21-
const { connected, executeTransaction, network } = useWallet();
21+
const {
22+
connected,
23+
executeTransaction,
24+
transactionStatus: getTransactionStatus,
25+
network,
26+
} = useWallet();
2227
const [program, setProgram] = useAtom(programAtom);
2328
const [functionName, setFunctionName] = useAtom(functionNameAtom);
2429
const [inputs, setInputs] = useState('');
2530
const [dynamicInputValues, setDynamicInputValues] = useState<string[]>([]);
2631
const [fee, setFee] = useState('100000');
27-
const [transactionHash, setTransactionHash] = useState<string | null>(null);
32+
const [onchainTransactionId, setOnchainTransactionId] = useState<string | null>(null);
2833
const [isExecutingTransaction, setIsExecutingTransaction] = useState(false);
34+
const [isPollingStatus, setIsPollingStatus] = useState(false);
35+
const [transactionStatus, setTransactionStatus] = useState<string | null>(null);
36+
const [transactionError, setTransactionError] = useState<string | null>(null);
37+
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
2938
const [isCodeModalOpen, setIsCodeModalOpen] = useState(false);
3039
const [isProgramCodeModalOpen, setIsProgramCodeModalOpen] = useState(false);
3140
const [programCode, setProgramCode] = useState<string>('');
@@ -54,6 +63,15 @@ export function ExecuteTransaction() {
5463
return functions.find(f => f.name === functionName);
5564
}, [functions, functionName]);
5665

66+
useEffect(() => {
67+
if (!connected) {
68+
setTransactionStatus(null);
69+
setOnchainTransactionId(null);
70+
setTransactionError(null);
71+
setIsPollingStatus(false);
72+
}
73+
}, [connected]);
74+
5775
useEffect(() => {
5876
if (functionNames.length > 0 && isLoading) {
5977
setIsLoading(false);
@@ -115,12 +133,66 @@ export function ExecuteTransaction() {
115133
}
116134
}, [programIsError]);
117135

136+
// Cleanup polling interval on unmount
137+
useEffect(() => {
138+
return () => {
139+
if (pollingIntervalRef.current) {
140+
clearInterval(pollingIntervalRef.current);
141+
}
142+
};
143+
}, []);
144+
145+
// Function to poll transaction status
146+
const pollTransactionStatus = async (tempTransactionId: string) => {
147+
function clear() {
148+
if (pollingIntervalRef.current) {
149+
clearInterval(pollingIntervalRef.current);
150+
pollingIntervalRef.current = null;
151+
}
152+
}
153+
try {
154+
const statusResponse = await getTransactionStatus(tempTransactionId);
155+
setTransactionStatus(statusResponse.status);
156+
if (statusResponse.transactionId) {
157+
// Transaction is now onchain, we have the final transaction ID
158+
setOnchainTransactionId(statusResponse.transactionId);
159+
}
160+
161+
if (statusResponse.status.toLowerCase() === TransactionStatus.ACCEPTED.toLowerCase()) {
162+
setIsPollingStatus(false);
163+
clear();
164+
toast.success('Transaction ' + statusResponse.status);
165+
} else if (
166+
statusResponse.status.toLowerCase() === TransactionStatus.FAILED.toLowerCase() ||
167+
statusResponse.status.toLowerCase() === TransactionStatus.REJECTED.toLowerCase()
168+
) {
169+
// Transaction failed
170+
setIsPollingStatus(false);
171+
if (statusResponse.error) {
172+
setTransactionError(statusResponse.error);
173+
}
174+
clear();
175+
toast.error('Transaction ' + statusResponse.status);
176+
}
177+
} catch (error) {
178+
console.error('Error polling transaction status, will stop polling. Error:', error);
179+
toast.error('Error polling transaction status. Check console for details.');
180+
setTransactionError('Error polling transaction status');
181+
setIsPollingStatus(false);
182+
setTransactionStatus(TransactionStatus.FAILED);
183+
clear();
184+
}
185+
};
186+
118187
const handleExecuteTransaction = async () => {
119188
if (!program.trim() || !functionName.trim() || !fee.trim()) {
120189
toast.error('Please enter program, function, and fee');
121190
return;
122191
}
123192
setIsExecutingTransaction(true);
193+
setOnchainTransactionId(null);
194+
setTransactionStatus(null);
195+
setTransactionError(null);
124196
try {
125197
let inputArray: string[];
126198

@@ -138,8 +210,21 @@ export function ExecuteTransaction() {
138210
inputs: inputArray,
139211
fee: Number(fee),
140212
});
141-
setTransactionHash(tx?.id ?? null);
142-
toast.success('Transaction submitted successfully');
213+
214+
if (tx?.transactionId) {
215+
toast.success('Transaction submitted successfully');
216+
setIsPollingStatus(true);
217+
218+
// Start polling for transaction status every 1 second
219+
pollingIntervalRef.current = setInterval(() => {
220+
pollTransactionStatus(tx.transactionId);
221+
}, 1000);
222+
223+
// Initial status check
224+
pollTransactionStatus(tx.transactionId);
225+
} else {
226+
toast.error('Failed to get transaction ID');
227+
}
143228
} catch (error) {
144229
console.error(error);
145230
toast.error('Failed to execute transaction. Check console for details.');
@@ -343,6 +428,7 @@ export function ExecuteTransaction() {
343428
disabled={
344429
!connected ||
345430
isExecutingTransaction ||
431+
isPollingStatus ||
346432
!program.trim() ||
347433
!functionName.trim() ||
348434
!fee.trim()
@@ -354,6 +440,11 @@ export function ExecuteTransaction() {
354440
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
355441
Executing Transaction...
356442
</>
443+
) : isPollingStatus ? (
444+
<>
445+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
446+
Waiting for Confirmation...
447+
</>
357448
) : (
358449
<>
359450
<Zap className="mr-2 h-4 w-4" />
@@ -362,36 +453,54 @@ export function ExecuteTransaction() {
362453
)}
363454
</Button>
364455

365-
{transactionHash && (
456+
{(transactionStatus || onchainTransactionId) && (
366457
<Alert>
367-
<CheckCircle className="h-4 w-4 text-green-500 dark:text-green-400" />
458+
{transactionStatus?.toLowerCase() === TransactionStatus.ACCEPTED.toLowerCase() ? (
459+
<CheckCircle className="h-4 w-4 " />
460+
) : transactionStatus?.toLowerCase() === TransactionStatus.REJECTED.toLowerCase() ||
461+
transactionStatus?.toLowerCase() === TransactionStatus.FAILED.toLowerCase() ? (
462+
<XCircle className="h-4 w-4" />
463+
) : (
464+
<Loader2 className="h-4 w-4 animate-spin" />
465+
)}
368466
<AlertDescription>
369467
<div className="space-y-2">
370-
<p className="font-medium">Transaction Executed Successfully!</p>
371-
<div className="flex items-center justify-between bg-muted p-2 rounded text-xs font-mono break-all border">
372-
<span className="truncate">Tx Hash: {transactionHash}</span>
373-
<Button
374-
variant="ghost"
375-
size="sm"
376-
onClick={() => copyToClipboard(transactionHash)}
377-
className="transition-all duration-200"
378-
>
379-
<Copy className="h-4 w-4" />
380-
</Button>
381-
</div>
382-
<Button
383-
variant="outline"
384-
size="sm"
385-
onClick={() => {
386-
window.open(
387-
`https://${network === Network.TESTNET3 ? 'testnet.' : network === Network.CANARY ? 'canary.' : ''}explorer.provable.com/transaction/${transactionHash}`,
388-
'_blank',
389-
);
390-
}}
391-
className="transition-all duration-200"
392-
>
393-
See on the explorer
394-
</Button>
468+
<p className="font-medium">
469+
Transaction status:{' '}
470+
<span className="font-bold capitalize">{transactionStatus || 'Pending'}</span>
471+
</p>
472+
473+
{onchainTransactionId ? (
474+
<>
475+
<div className="flex items-center justify-between bg-muted p-2 rounded text-xs font-mono break-all border">
476+
<span className="truncate">Transaction Id: {onchainTransactionId}</span>
477+
<Button
478+
variant="ghost"
479+
size="sm"
480+
onClick={() => copyToClipboard(onchainTransactionId)}
481+
className="transition-all duration-200"
482+
>
483+
<Copy className="h-4 w-4" />
484+
</Button>
485+
</div>
486+
<Button
487+
variant="outline"
488+
size="sm"
489+
onClick={() => {
490+
window.open(
491+
`https://${network === Network.TESTNET3 ? 'testnet.' : network === Network.CANARY ? 'canary.' : ''}explorer.provable.com/transaction/${onchainTransactionId}`,
492+
'_blank',
493+
);
494+
}}
495+
className="transition-all duration-200"
496+
>
497+
See on the explorer
498+
</Button>
499+
</>
500+
) : null}
501+
{transactionError && (
502+
<div className="text-sm text-destructive">Error: {transactionError}</div>
503+
)}
395504
</div>
396505
</AlertDescription>
397506
</Alert>

packages/aleo-types/src/transaction.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
*/
44
export enum TransactionStatus {
55
PENDING = 'pending',
6-
CONFIRMED = 'confirmed',
6+
ACCEPTED = 'accepted',
77
FAILED = 'failed',
8+
REJECTED = 'rejected',
89
}
910

1011
/**
@@ -66,3 +67,23 @@ export interface TransactionOptions {
6667
*/
6768
recordIndices?: number[];
6869
}
70+
71+
/**
72+
* Transaction status response
73+
*/
74+
export interface TransactionStatusResponse {
75+
/**
76+
* The transaction status
77+
*/
78+
status: string;
79+
80+
/**
81+
* The onchain transaction ID (if already exists)
82+
*/
83+
transactionId?: string;
84+
85+
/**
86+
* The error message (if any)
87+
*/
88+
error?: string;
89+
}

packages/aleo-wallet-adaptor/core/src/adapter.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { Account, Network, Transaction, TransactionOptions } from '@provablehq/aleo-types';
1+
import {
2+
Account,
3+
Network,
4+
TransactionOptions,
5+
TransactionStatusResponse,
6+
} from '@provablehq/aleo-types';
27
import {
38
AleoChain,
49
StandardWallet,
@@ -149,9 +154,9 @@ export abstract class BaseAleoWalletAdapter
149154
/**
150155
* Execute a transaction
151156
* @param options Transaction options
152-
* @returns The executed transaction
157+
* @returns The executed temporary transaction ID
153158
*/
154-
async executeTransaction(options: TransactionOptions): Promise<Transaction> {
159+
async executeTransaction(options: TransactionOptions): Promise<{ transactionId: string }> {
155160
if (!this._wallet || !this.account) {
156161
throw new WalletNotConnectedError();
157162
}
@@ -162,6 +167,22 @@ export abstract class BaseAleoWalletAdapter
162167
return feature.executeTransaction(options);
163168
}
164169

170+
/**
171+
* Get transaction status
172+
* @param transactionId The transaction ID
173+
* @returns The transaction status
174+
*/
175+
async transactionStatus(transactionId: string): Promise<TransactionStatusResponse> {
176+
if (!this._wallet || !this.account) {
177+
throw new WalletNotConnectedError();
178+
}
179+
const feature = this._wallet.features[WalletFeatureName.TRANSACTION_STATUS];
180+
if (!feature || !feature.available) {
181+
throw new WalletFeatureNotAvailableError(WalletFeatureName.TRANSACTION_STATUS);
182+
}
183+
return feature.transactionStatus(transactionId);
184+
}
185+
165186
async switchNetwork(network: Network): Promise<void> {
166187
if (!this._wallet || !this.account) {
167188
throw new WalletNotConnectedError();
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { BaseAleoWalletAdapter } from './adapter';
2-
export * from './transaction';
32
export * from './account';
43
export * from './errors';
54
export * from './types';

0 commit comments

Comments
 (0)