Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trampoline demo - verifying paymaster #20

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Expand Up @@ -2,6 +2,11 @@

Trampoline is a chrome extension boilerplate code to showcase your own Smart Contract Wallets with React 18 and Webpack 5 support.

> [!NOTE]
> This branch is the implementation of how to send transactions using verifying paymaster.
> You can read more about it's implementation in this [blog](https://erc4337.mirror.xyz/RKG9kt7af3B_Dj0KHjuhwOOpEP6JxLXr-2vW4DOlhQM)


## Installation and Running

### Steps:
Expand Down
1 change: 1 addition & 0 deletions hardhat.config.ts
Expand Up @@ -42,6 +42,7 @@ const config: HardhatUserConfig = {
},
networks: {
goerli: getNetwork('goerli'),
sepolia: getNetwork('sepolia'),
mumbai: getNetwork('polygon-mumbai'),
},
etherscan: {
Expand Down
4 changes: 4 additions & 0 deletions package.json
Expand Up @@ -34,7 +34,9 @@
"@simplewebauthn/typescript-types": "^7.0.0",
"base64url": "^3.0.1",
"buffer": "^6.0.3",
"cors": "^2.8.5",
"daisyui": "^2.49.0",
"dotenv": "^16.3.1",
"elliptic": "^6.5.4",
"emittery": "^1.0.1",
"ethers": "^5.7.2",
Expand All @@ -50,6 +52,7 @@
"redux-persist": "^6.0.0",
"siwe": "^1.1.6",
"ssestream": "^1.1.0",
"ts-node": "^10.9.1",
"uuid": "^9.0.0",
"wagmi": "^0.11.3",
"webext-redux": "^2.1.9",
Expand All @@ -70,6 +73,7 @@
"@typechain/hardhat": "^6.1.5",
"@types/chai": "^4.3.4",
"@types/chrome": "^0.0.212",
"@types/cors": "^2.8.13",
"@types/mocha": "^10.0.1",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
Expand Down
3 changes: 3 additions & 0 deletions server/.env.copy
@@ -0,0 +1,3 @@
NODE_ENV=development
PAYMASTER_SERVICE_URL=<URL>
PORT=8080
17 changes: 17 additions & 0 deletions server/app.ts
@@ -0,0 +1,17 @@
import express, { Express, Request, Response } from 'express';
import cors from 'cors';
import routes from './routes';

const app: Express = express();

app.use(cors());

app.use(express.json());

app.get('/', (req: Request, res: Response) => {
res.send('TypeScript Server');
});

routes(app);

export default app;
11 changes: 11 additions & 0 deletions server/index.ts
@@ -0,0 +1,11 @@
import './setup-env';
import http from 'http';
import app from './app';

const server = http.createServer(app);

server.listen(process.env.PORT, () => {
console.log(
`⚡️[server]: Server is running at http://localhost:${process.env.PORT} in ${process.env.NODE_ENV} mode`
);
});
153 changes: 153 additions & 0 deletions server/routes.ts
@@ -0,0 +1,153 @@
import express, { Express } from 'express';
import { ethers } from 'ethers';
import { UserOperationStruct } from '@account-abstraction/contracts';

const apirouter = express.Router();

const paymasterRouter = express.Router();

const getStackupPaymasterAndData = async (
provider: ethers.providers.JsonRpcProvider,
{
userOp,
entryPoint,
}: {
userOp: UserOperationStruct;
entryPoint: string;
}
): Promise<{
callGasLimit: string;
paymasterAndData: string;
preVerificationGas: string;
verificationGasLimit: string;
}> => {
const paymasterRPC = new ethers.providers.JsonRpcProvider(
process.env.PAYMASTER_SERVICE_URL,
{
name: 'Paymaster',
chainId: (await provider.getNetwork()).chainId,
}
);

const response: {
callGasLimit: string;
paymasterAndData: string;
preVerificationGas: string;
verificationGasLimit: string;
} = await paymasterRPC.send('pm_sponsorUserOperation', [
userOp,
entryPoint,
{
type: 'payg',
},
]);

return response;
};

const getAlchecmyPaymasterAndData = async (
provider: ethers.providers.JsonRpcProvider,
{
userOp,
entryPoint,
}: {
userOp: UserOperationStruct;
entryPoint: string;
}
): Promise<{
callGasLimit: string;
paymasterAndData: string;
preVerificationGas: string;
verificationGasLimit: string;
}> => {
const paymasterRPC = new ethers.providers.JsonRpcProvider(
process.env.PAYMASTER_SERVICE_URL,
{
name: 'Paymaster',
chainId: (await provider.getNetwork()).chainId,
}
);

const response: {
callGasLimit: string;
paymasterAndData: string;
preVerificationGas: string;
verificationGasLimit: string;
} = await paymasterRPC.send('alchemy_requestGasAndPaymasterAndData', [
{
policyId: process.env.POLICY_ID,
dummySignature: userOp.signature,
entryPoint,
userOperation: userOp,
},
]);

return response;
};

paymasterRouter.route('/').post(async (req, res) => {
const {
method,
params: [userOp, entryPoint],
}: { method: string; params: [UserOperationStruct, string] } = req.body;

if (!method || !userOp || !entryPoint) {
res.status(400).send('Bad Request');
return;
}

const provider = new ethers.providers.JsonRpcProvider(
process.env.PROVIDER_URL
);

switch (req.body.method) {
case 'local_getPaymasterAndData':
/**
* Alchemy's implementation
*/
// const {
// callGasLimit,
// paymasterAndData,
// preVerificationGas,
// verificationGasLimit,
// } = await getAlchecmyPaymasterAndData(provider, {
// userOp,
// entryPoint,
// });

/**
* Stackup's implementation
*/
const {
callGasLimit,
paymasterAndData,
preVerificationGas,
verificationGasLimit,
} = await getStackupPaymasterAndData(provider, {
userOp,
entryPoint,
});

res.send({
id: req.body.id,
jsonrpc: '2.0',
result: {
callGasLimit,
paymasterAndData,
preVerificationGas,
verificationGasLimit,
},
});
break;
default:
res.status(400).send('Bad Request');
break;
}
});

const routes = (app: Express) => {
apirouter.use('/paymaster', paymasterRouter);
app.use('/api/v1', apirouter);
};

export default routes;
4 changes: 4 additions & 0 deletions server/setup-env.ts
@@ -0,0 +1,4 @@
import dotenv from 'dotenv';
import path from 'path';

dotenv.config({ path: path.resolve(__dirname, './.env') });
3 changes: 2 additions & 1 deletion src/exconfig.ts
Expand Up @@ -3,14 +3,15 @@ export default {
enablePasswordEncryption: false,
showTransactionConfirmationScreen: true,
factory_address: '0x9406Cc6185a346906296840746125a0E44976454',
paymaster_url: 'http://localhost:8080/api/v1/paymaster',
stateVersion: '0.1',
network: {
chainID: '11155111',
family: 'EVM',
name: 'Sepolia',
provider: 'https://sepolia.infura.io/v3/bdabe9d2f9244005af0f566398e648da',
entryPointAddress: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
bundler: 'https://sepolia.voltaire.candidewallet.com/rpc',
bundler: 'http://localhost:3000/rpc',
baseAsset: {
symbol: 'ETH',
name: 'ETH',
Expand Down
61 changes: 58 additions & 3 deletions src/pages/Account/account-api/account-api.ts
Expand Up @@ -6,6 +6,7 @@ import { MessageSigningRequest } from '../../Background/redux-slices/signing';
import { TransactionDetailsForUserOp } from '@account-abstraction/sdk/dist/src/TransactionDetailsForUserOp';
import config from '../../../exconfig';
import { SimpleAccountAPI } from '@account-abstraction/sdk';
import { resolveProperties } from 'ethers/lib/utils.js';

const FACTORY_ADDRESS = config.factory_address;

Expand All @@ -20,6 +21,8 @@ class SimpleAccountTrampolineAPI
extends SimpleAccountAPI
implements AccountApiType
{
paymasterRPC?: ethers.providers.JsonRpcProvider;

/**
*
* We create a new private key or use the one provided in the
Expand All @@ -36,6 +39,18 @@ class SimpleAccountTrampolineAPI
});
}

async init(): Promise<this> {
this.paymasterRPC = new ethers.providers.JsonRpcProvider(
config.paymaster_url,
{
name: 'Paymaster',
chainId: (await this.provider.getNetwork()).chainId,
}
);

return this;
}

/**
*
* @returns the serialized state of the account that is saved in
Expand All @@ -58,6 +73,13 @@ class SimpleAccountTrampolineAPI
throw new Error('signMessage method not implemented.');
};

dummySignUserOp = (userOp: UserOperationStruct): UserOperationStruct => {
return Object.assign(Object.assign({}, userOp), {
signature:
'0xe8fe34b166b64d118dccf44c7198648127bf8a76a48a042862321af6058026d276ca6abb4ed4b60ea265d1e57e33840d7466de75e13f072bbd3b7e64387eebfe1b',
});
};

/**
* Called after the user is presented with the pre-transaction confirmation screen
* The context passed to this method is the same as the one passed to the
Expand All @@ -67,11 +89,44 @@ class SimpleAccountTrampolineAPI
info: TransactionDetailsForUserOp,
preTransactionConfirmationContext?: any
): Promise<UserOperationStruct> {
if (!this.paymasterRPC) throw new Error('paymasterRPC not initialized');

const userOp = await resolveProperties(
await this.createUnsignedUserOp(info)
);

userOp.nonce = ethers.BigNumber.from(userOp.nonce).toHexString();
userOp.callGasLimit = ethers.BigNumber.from(
userOp.callGasLimit
).toHexString();
userOp.verificationGasLimit = ethers.BigNumber.from(
userOp.verificationGasLimit
).toHexString();
userOp.preVerificationGas = ethers.BigNumber.from(
userOp.preVerificationGas
).toHexString();
userOp.maxFeePerGas = ethers.BigNumber.from(
userOp.maxFeePerGas
).toHexString();
userOp.maxPriorityFeePerGas = ethers.BigNumber.from(
userOp.maxPriorityFeePerGas
).toHexString();

const paymasterData: {
callGasLimit: string;
paymasterAndData: string;
preVerificationGas: string;
verificationGasLimit: string;
} = await this.paymasterRPC.send('local_getPaymasterAndData', [
await resolveProperties(this.dummySignUserOp(userOp)),
config.network.entryPointAddress,
]);

console.log('paymasterData', paymasterData);

return {
...(await this.createUnsignedUserOp(info)),
paymasterAndData: preTransactionConfirmationContext?.paymasterAndData
? preTransactionConfirmationContext?.paymasterAndData
: '0x',
...paymasterData,
};
}

Expand Down
Expand Up @@ -122,25 +122,21 @@ const PreTransactionConfirmationComponent: PreTransactionConfirmation = ({
<>
<CardContent>
<Typography variant="h3" gutterBottom>
Dummy Component
Paymaster Demo
</Typography>
<Typography variant="body1" color="text.secondary">
You can show as many steps as you want in this dummy component. You
need to call the function <b>onComplete</b> passed as a props to this
component. <br />
<br />
The function takes a modifiedTransactions & context as a parameter,
the context will be passed to your AccountApi when creating a new
account. While modifiedTransactions will be agreed upon by the user.
<br />
This Component is defined in exported in{' '}
We will be using{' '}
<a href="https://docs.stackup.sh/docs/paymaster-api-rpc-methods">
Stackup's paymaster
</a>{' '}
as our paymaster for the purpose of this demo.
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mt: 4 }}>
File name:
</Typography>
<Typography variant="caption">
trampoline/src/pages/Account/components/transaction/pre-transaction-confirmation.ts
</Typography>
<Box sx={{ mt: 4, mb: 4 }}>
<AddPaymasterAndData setPaymasterAndData={setPaymasterAndDataLocal} />
</Box>
</CardContent>
<CardActions sx={{ width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Expand Up @@ -17,6 +17,6 @@
"typeRoots": ["./src/types"]
},
"include": ["src", "deploy"],
"exclude": ["build", "node_modules", "dist"],
"exclude": ["build", "node_modules", "dist", "server"],
"files": ["./hardhat.config.ts"]
}