Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions examples/wallet-actions/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_PRIVY_APP_ID=<privy_app_id>
VITE_PRIVY_CLIENT_ID=<privy_app_secret>
6 changes: 6 additions & 0 deletions examples/wallet-actions/.stackblitzrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"env": {
"VITE_PRIVY_APP_ID": "cmdisgoa00075js0j9m0720ah",
"VITE_PRIVY_CLIENT_ID": "client-WY6NwHTKdBr537W6x1ZK38ZohDVFkbsBSYssDZJP4BXQx"
}
}
3 changes: 3 additions & 0 deletions examples/wallet-actions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Aave React SDK + Privy Wallet

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/aave/aave-v4-sdk/tree/main/examples/react-privy)
14 changes: 14 additions & 0 deletions examples/wallet-actions/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/lens.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css">
<title>Aave React SDK + Privy Wallet</title>
</head>
<body>
<main id="root"></main>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
24 changes: 24 additions & 0 deletions examples/wallet-actions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "react-privy",
"description": "Aave React SDK + Privy Wallet",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite"
},
"dependencies": {
"@aave/react": "workspace:*",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"viem": "^2.37.5",
"wagmi": "^2.15.6"
},
"devDependencies": {
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react-swc": "^3.7.2",
"typescript": "^5.6.3",
"vite": "^5.4.9"
}
}
1 change: 1 addition & 0 deletions examples/wallet-actions/public/aave.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions examples/wallet-actions/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Suspense } from 'react';
import { SupplyForm } from './SupplyForm';
import { walletClient } from './wallet';

export function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<h1>Aave React SDK + Privy Wallet</h1>
<p>
This example lets you supply GHO on the Core Hub in Aave v4 using a
Privy-embedded or connected wallet.
</p>
<SupplyForm walletClient={walletClient} />
</Suspense>
);
}
77 changes: 77 additions & 0 deletions examples/wallet-actions/src/SupplyForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { bigDecimal, evmAddress, reserveId, useSupply } from '@aave/react';
import { useState } from 'react';
import type { WalletClient } from 'viem';
import { useExecutionPlan } from './useExecutionPlan';

const RESERVE_ID = reserveId(
'MTIzNDU2Nzg5OjoweEJhOTdjNUU1MmNkNUJDM0Q3OTUwQWU3MDc3OUY4RmZFOTJkNDBDZEM6OjY=',
);

type SupplyFormProps = {
walletClient: WalletClient;
};

export function SupplyForm({ walletClient }: SupplyFormProps) {
const [status, setStatus] = useState<string>('');

const handler = useExecutionPlan(walletClient);
const [supply, { loading, error }] = useSupply(handler);

const submit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

const amount = e.currentTarget.amount.value as string;
if (!amount) {
setStatus('Please enter an amount');
return;
}

const result = await supply({
reserve: RESERVE_ID,
amount: {
erc20: {
value: bigDecimal(amount),
},
},
sender: evmAddress(walletClient.account!.address),
});

if (result.isOk()) {
setStatus('Supply successful!');
} else {
setStatus('Supply failed!');
}
};

return (
<form onSubmit={submit}>
<label
style={{
marginBottom: '5px',
}}
>
<strong style={{ display: 'block' }}>Amount:</strong>
<input
name='amount'
type='number'
step='0.000000000000000001'
defaultValue='1'
disabled={loading}
style={{ width: '100%', padding: '8px' }}
placeholder='Amount to supply (in token units)'
/>
<small style={{ color: '#666' }}>
Human-friendly amount (e.g. 1.23, 4.56, 7.89)
</small>
</label>

<button type='submit' disabled={loading}>
Supply
</button>

{status && <p style={{ marginBottom: '10px' }}>{status}</p>}

{error && <p style={{ color: '#f44336' }}>Error: {error.toString()}</p>}
</form>
);
}
5 changes: 5 additions & 0 deletions examples/wallet-actions/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AaveClient, staging } from '@aave/react';

export const client = AaveClient.create({
environment: staging,
});
10 changes: 10 additions & 0 deletions examples/wallet-actions/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AaveProvider } from '@aave/react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import { client } from './client';

createRoot(document.getElementById('root')!).render(
<AaveProvider client={client}>
<App />
</AaveProvider>,
);
110 changes: 110 additions & 0 deletions examples/wallet-actions/src/useExecutionPlan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
type Erc20ApprovalRequired,
errAsync,
nonNullable,
okAsync,
PendingTransaction,
type PendingTransactionError,
type PreContractActionRequired,
ResultAsync,
type SendTransactionError,
SigningError,
TransactionError,
type TransactionHandler,
type TransactionResult,
txHash,
type UnexpectedError,
} from '@aave/react';
import { useSendTransaction } from '@aave/react/viem';
import type { WalletClient } from 'viem';
import {
getCapabilities as getCapabilitiesWithViem,
sendCalls,
waitForCallsStatus,
} from 'viem/actions';

function supportsAtomicBatch(
walletClient: WalletClient,
): ResultAsync<boolean, UnexpectedError> {
return ResultAsync.fromPromise(
getCapabilitiesWithViem(walletClient),
(error) => error,
)
.orElse(() => okAsync(false))
.andThen((_capabilities) => {
console.log('supportsAtomicBatch', _capabilities); // TODO: check for the current chain
return okAsync(true);
});
}

function waitForAtomicBatch(
walletClient: WalletClient,
plan: Erc20ApprovalRequired | PreContractActionRequired,
actionId: string,
): ResultAsync<TransactionResult, PendingTransactionError> {
return ResultAsync.fromPromise(
waitForCallsStatus(walletClient, { id: actionId }),
// TODO: handle timeout error
(error) => TransactionError.from(error),
).andThen((result) => {
if (result.statusCode !== 200 || !result.receipts) {
// TODO: proper error handling from result.receipts if available
return errAsync(TransactionError.from(result.statusCode));
}

return okAsync({
txHash: txHash(
nonNullable(result.receipts.slice().pop()?.transactionHash),
),
operations: plan.transaction.operations,
});
});
}

function batchCalls(
walletClient: WalletClient,
plan: Erc20ApprovalRequired | PreContractActionRequired,
): ResultAsync<string, SendTransactionError> {
return ResultAsync.fromPromise(
sendCalls(walletClient, {
account: walletClient.account,
calls: [plan.transaction, plan.originalTransaction].map(
(transaction) => ({
to: transaction.to,
data: transaction.data,
}),
),
}),
(error) => {
// TODO handle CancelError
return SigningError.from(error);
},
).map((result) => result.id);
}

export function useExecutionPlan(
walletClient: WalletClient,
): TransactionHandler {
const [sendTransaction] = useSendTransaction(walletClient);

return (plan) => {
switch (plan.__typename) {
case 'TransactionRequest':
return sendTransaction(plan);
case 'Erc20ApprovalRequired':
case 'PreContractActionRequired':
return supportsAtomicBatch(walletClient).andThen((supports) => {
if (supports) {
return batchCalls(walletClient, plan).map(
(actionId) =>
new PendingTransaction(() =>
waitForAtomicBatch(walletClient, plan, actionId),
),
);
}

return sendTransaction(plan.transaction);
});
}
};
}
10 changes: 10 additions & 0 deletions examples/wallet-actions/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_PRIVY_APP_ID: string;
readonly VITE_PRIVY_CLIENT_ID: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
28 changes: 28 additions & 0 deletions examples/wallet-actions/src/wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'viem/window';

import { supportedChains } from '@aave/react/viem';
import { type Address, createWalletClient, custom } from 'viem';

const chain = supportedChains[0];

const [address]: [Address] = await window.ethereum!.request({
method: 'eth_requestAccounts',
});

export const walletClient = createWalletClient({
account: address,
chain,
transport: custom(window.ethereum!),
});

const chainId = await walletClient.getChainId();

if (chainId !== chain.id) {
try {
await walletClient.switchChain({ id: chain.id });
} catch {
await walletClient.addChain({ chain });
}
}

export { address };
20 changes: 20 additions & 0 deletions examples/wallet-actions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions examples/wallet-actions/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
});
5 changes: 4 additions & 1 deletion packages/client/src/viem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ import type {
TransactionResult,
} from './types';

function isRpcError(err: unknown): err is RpcError {
/**
* @internal
*/
export function isRpcError(err: unknown): err is RpcError {
return isObject(err) && 'code' in err && 'message' in err;
}

Expand Down
6 changes: 1 addition & 5 deletions packages/core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,12 @@ export type TransactionErrorArgs = {
export class TransactionError extends ResultAwareError {
name = 'TransactionError' as const;

protected constructor(message: string, cause: UnsignedTransactionRequest) {
super(message, { cause });
}

static new(args: TransactionErrorArgs) {
const { txHash, request, link } = args;
const message = link
? `Transaction failed: ${txHash}\n→ View on explorer: ${link}`
: `Transaction failed: ${txHash}`;
return new TransactionError(message, request);
return new TransactionError(message, { cause: request });
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export type {
AsyncTaskLoading,
AsyncTaskState,
AsyncTaskSuccess,
PendingTransaction,
PendingTransactionError,
SendTransactionError,
TransactionHandler,
TransactionHandlerOptions,
UseAsyncTask,
UseSendTransactionResult,
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/viem/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { supportedChains as supportedChainsMap } from '@aave/client/viem';
import type { Chain } from 'viem';

// TODO remove me
export { isRpcError } from '@aave/client/viem';

export * from './adapters';
export * from './useNetworkFee';

Expand Down
Loading
Loading