Skip to content

Commit 40a7202

Browse files
authored
PRO-2949-Arka_Changes (#175)
1 parent 2ef38d8 commit 40a7202

File tree

11 files changed

+342
-1
lines changed

11 files changed

+342
-1
lines changed

backend/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
# Changelog
2+
## [3.1.0] - 2025-01-28
3+
### New
4+
- Added a separate mode for all the common erc20 paymasters in mind that in future will replace the current erc20 mode
5+
6+
## [3.0.3] - 2025-02-11
7+
### Fixes
8+
- Checked for undefined values on body params
9+
10+
## [3.0.2] - 2025-02-11
11+
- Using Skandha for gas data for better transaction inclusion.
12+
- USE_SKANDHA_FOR_GAS_DATA set this property to false to disable.
13+
214
## [3.0.1] - 2025-02-04
315
### Fixes
416
- Changed error message to more meaningful reply
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const { Sequelize } = require('sequelize')
2+
3+
async function up({ context: queryInterface }) {
4+
await queryInterface.createTable('multi_token_paymaster', {
5+
ID: {
6+
type: Sequelize.INTEGER,
7+
primaryKey: true,
8+
autoIncrement: true,
9+
allowNull: false
10+
},
11+
TOKEN_ADDRESS: {
12+
type: Sequelize.STRING,
13+
allowNull: false,
14+
primaryKey: true
15+
},
16+
PAYMASTER_ADDRESS: {
17+
type: Sequelize.STRING,
18+
allowNull: false
19+
},
20+
ORACLE_ADDRESS: {
21+
type: Sequelize.STRING,
22+
allowNull: true
23+
},
24+
CHAIN_ID: {
25+
type: Sequelize.BIGINT,
26+
allowNull: false,
27+
get() {
28+
const value = this.getDataValue('chainId');
29+
return +value;
30+
}
31+
},
32+
DECIMALS: {
33+
type: Sequelize.INTEGER,
34+
allowNull: false
35+
},
36+
CREATED_AT: {
37+
type: Sequelize.DATE,
38+
allowNull: false,
39+
defaultValue: Sequelize.NOW
40+
},
41+
UPDATED_AT: {
42+
type: Sequelize.DATE,
43+
allowNull: false,
44+
defaultValue: Sequelize.NOW
45+
},
46+
}, {
47+
schema: process.env.DATABASE_SCHEMA_NAME
48+
});
49+
}
50+
async function down({ context: queryInterface }) {
51+
await queryInterface.dropTable({
52+
tableName: 'multi_token_paymaster',
53+
schema: process.env.DATABASE_SCHEMA_NAME
54+
})
55+
}
56+
57+
module.exports = { up, down }

backend/migrations/2025012500001-seed-data.cjs

Lines changed: 59 additions & 0 deletions
Large diffs are not rendered by default.

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "arka",
3-
"version": "3.0.1",
3+
"version": "3.1.0",
44
"description": "ARKA - (Albanian for Cashier's case) is the first open source Paymaster as a service software",
55
"type": "module",
66
"directories": {

backend/src/constants/ErrorMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default {
77
API_KEY_NOT_CONFIGURED_IN_DATABASE: 'Api Key not configured in database',
88
UNSUPPORTED_NETWORK: 'Unsupported network',
99
UNSUPPORTED_NETWORK_TOKEN: 'Unsupported network/token',
10+
UNSUPPORTED_TOKEN: 'Unsupported token',
1011
MISSING_PARAMS: 'You have not supplied the required input parameters for this function/endpoint',
1112
API_KEY_IS_REQUIRED_IN_HEADER: 'Api Key is required in header',
1213
API_KEY_DOES_NOT_EXIST_FOR_THE_WALLET_ADDRESS: 'Api Key does not exist for the wallet address',
@@ -52,6 +53,7 @@ export default {
5253
FAILED_TO_DEPLOY_VP: 'Failed to deploy verifying paymaster',
5354
FAILED_TO_ADD_STAKE: 'Failed to add stake',
5455
INVALID_AMOUNT_TO_STAKE: 'Invalid amount to stake',
56+
NO_KEY_SET: 'No MTP key set',
5557
MULTI_NOT_DEPLOYED: 'Token Paymaster not deployed on the current chainID: ',
5658
COINGECKO_PRICE_NOT_FETCHED: 'Token price not updated, Pls retry.'
5759
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Sequelize, DataTypes, Model } from 'sequelize';
2+
3+
export class MultiTokenPaymaster extends Model {
4+
public id!: number; // Note that the `null assertion` `!` is required in strict mode.
5+
public tokenAddress!: string;
6+
public oracleAddress!: string;
7+
public paymasterAddress!: string;
8+
public chainId!: number;
9+
public decimals!: string;
10+
public readonly createdAt!: Date;
11+
public readonly updatedAt!: Date;
12+
}
13+
14+
const initializeMTPModel = (sequelize: Sequelize, schema: string) => {
15+
MultiTokenPaymaster.init({
16+
id: {
17+
type: DataTypes.INTEGER,
18+
primaryKey: true,
19+
autoIncrement: true,
20+
allowNull: false,
21+
field: 'ID'
22+
},
23+
tokenAddress: {
24+
type: DataTypes.STRING,
25+
allowNull: false,
26+
primaryKey: true,
27+
field: 'TOKEN_ADDRESS'
28+
},
29+
paymasterAddress: {
30+
type: DataTypes.STRING,
31+
allowNull: false,
32+
field: 'PAYMASTER_ADDRESS'
33+
},
34+
oracleAddress: {
35+
type: DataTypes.STRING,
36+
allowNull: true,
37+
field: 'ORACLE_ADDRESS'
38+
},
39+
chainId: {
40+
type: DataTypes.BIGINT,
41+
allowNull: false,
42+
field: 'CHAIN_ID',
43+
get() {
44+
const value = this.getDataValue('chainId');
45+
return +value;
46+
}
47+
},
48+
decimals: {
49+
type: DataTypes.INTEGER,
50+
allowNull: false,
51+
field: 'DECIMALS'
52+
},
53+
createdAt: {
54+
type: DataTypes.DATE,
55+
allowNull: false,
56+
defaultValue: DataTypes.NOW,
57+
field: 'CREATED_AT'
58+
},
59+
updatedAt: {
60+
type: DataTypes.DATE,
61+
allowNull: false,
62+
defaultValue: DataTypes.NOW,
63+
field: 'UPDATED_AT'
64+
},
65+
}, {
66+
sequelize,
67+
tableName: 'multi_token_paymaster',
68+
modelName: 'MultiTokenPaymaster',
69+
timestamps: true,
70+
createdAt: 'createdAt',
71+
updatedAt: 'updatedAt',
72+
freezeTableName: true,
73+
schema: schema,
74+
});
75+
};
76+
77+
export { initializeMTPModel };

backend/src/plugins/sequelizePlugin.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { ContractWhitelistRepository } from "../repository/contract-whitelist-re
1414
import { initializeContractWhitelistModel } from "../models/contract-whitelist.js";
1515
import { CoingeckoTokensRepository } from "../repository/coingecko-token-repository.js";
1616
import { initializeCoingeckoModel } from "../models/coingecko.js";
17+
import { initializeMTPModel } from "../models/multiTokenPaymaster.js";
18+
import { MultiTokenPaymasterRepository } from "../repository/multi-token-paymaster.js";
19+
1720
const pg = await import('pg');
1821
const Client = pg.default.Client;
1922

@@ -58,6 +61,7 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => {
5861
initializeArkaWhitelistModel(sequelize, server.config.DATABASE_SCHEMA_NAME);
5962
initializeContractWhitelistModel(sequelize, server.config.DATABASE_SCHEMA_NAME);
6063
initializeCoingeckoModel(sequelize, server.config.DATABASE_SCHEMA_NAME);
64+
initializeMTPModel(sequelize, server.config.DATABASE_SCHEMA_NAME);
6165
server.log.info('Initialized SponsorshipPolicy model...');
6266

6367
server.log.info('Initialized all models...');
@@ -76,6 +80,8 @@ const sequelizePlugin: FastifyPluginAsync = async (server) => {
7680
server.decorate('contractWhitelistRepository', contractWhitelistRepository);
7781
const coingeckoRepo: CoingeckoTokensRepository = new CoingeckoTokensRepository(sequelize);
7882
server.decorate('coingeckoRepo', coingeckoRepo);
83+
const multiTokenPaymasterRepository: MultiTokenPaymasterRepository = new MultiTokenPaymasterRepository(sequelize);
84+
server.decorate('multiTokenPaymasterRepository', multiTokenPaymasterRepository);
7985

8086
server.log.info('decorated fastify server with models...');
8187

@@ -94,6 +100,7 @@ declare module "fastify" {
94100
sponsorshipPolicyRepository: SponsorshipPolicyRepository;
95101
whitelistRepository: WhitelistRepository;
96102
contractWhitelistRepository: ContractWhitelistRepository;
103+
multiTokenPaymasterRepository: MultiTokenPaymasterRepository;
97104
}
98105
}
99106

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Sequelize } from 'sequelize';
2+
import { MultiTokenPaymaster } from '../models/multiTokenPaymaster.js';
3+
4+
export class MultiTokenPaymasterRepository {
5+
private sequelize: Sequelize;
6+
7+
constructor(sequelize: Sequelize) {
8+
this.sequelize = sequelize;
9+
}
10+
11+
async findAll(): Promise<MultiTokenPaymaster[]> {
12+
const result = await this.sequelize.models.MultiTokenPaymaster.findAll();
13+
return result.map(id => id.get() as MultiTokenPaymaster);
14+
}
15+
16+
async findOneByChainIdAndTokenAddress(chainId: number, tokenAddress: string): Promise<MultiTokenPaymaster | null> {
17+
const result = await this.sequelize.models.MultiTokenPaymaster.findOne({
18+
where: {
19+
chainId: chainId, tokenAddress: tokenAddress
20+
}
21+
}) as MultiTokenPaymaster;
22+
23+
if (!result) {
24+
return null;
25+
}
26+
27+
return result.get() as MultiTokenPaymaster;
28+
}
29+
30+
async findOneById(id: number): Promise<MultiTokenPaymaster | null> {
31+
const multiTokenPaymaster = await this.sequelize.models.MultiTokenPaymaster.findOne({ where: { id: id } }) as MultiTokenPaymaster;
32+
if (!multiTokenPaymaster) {
33+
return null;
34+
}
35+
36+
const dataValues = multiTokenPaymaster.get();
37+
return dataValues as MultiTokenPaymaster;
38+
}
39+
40+
async getAllDistinctPaymasterAddrWithChainId(): Promise<MultiTokenPaymaster[]> {
41+
const result = await this.sequelize.models.MultiTokenPaymaster.findAll({
42+
attributes: [[Sequelize.fn('DISTINCT', Sequelize.col('PAYMASTER_ADDRESS')), 'PAYMASTER_ADDRESS'], 'chainId'],
43+
});
44+
return result.map(id => id.get() as MultiTokenPaymaster);
45+
}
46+
}

backend/src/routes/paymaster-routes.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,41 @@ const paymasterRoutes: FastifyPluginAsync<PaymasterRoutesOpts> = async (server,
394394
}
395395
break;
396396
}
397+
case 'commonerc20': {
398+
if (epVersion !== EPVersions.EPV_06)
399+
throw new Error('Currently only EPV06 entryPoint address is supported')
400+
const multiTokenRec = await server.multiTokenPaymasterRepository.findOneByChainIdAndTokenAddress(chainId, gasToken)
401+
if (multiTokenRec) {
402+
const date = new Date();
403+
const provider = new providers.JsonRpcProvider(bundlerUrl);
404+
const commonPrivateKey = process.env.MTP_PRIVATE_KEY;
405+
if (!commonPrivateKey) return reply.code(ReturnCode.FAILURE).send({error: ErrorMessage.NO_KEY_SET})
406+
const signer = new Wallet(commonPrivateKey, provider)
407+
const validUntil = context.validUntil ? new Date(context.validUntil) : date;
408+
const validAfter = context.validAfter ? new Date(context.validAfter) : date;
409+
const hex = (Number((validUntil.valueOf() / 1000).toFixed(0)) + 600).toString(16);
410+
const hex1 = (Number((validAfter.valueOf() / 1000).toFixed(0)) - 60).toString(16);
411+
let str = '0x'
412+
let str1 = '0x'
413+
for (let i = 0; i < 14 - hex.length; i++) {
414+
str += '0';
415+
}
416+
for (let i = 0; i < 14 - hex1.length; i++) {
417+
str1 += '0';
418+
}
419+
str += hex;
420+
str1 += hex1;
421+
if (!networkConfig.MultiTokenPaymasterOracleUsed ||
422+
!(networkConfig.MultiTokenPaymasterOracleUsed == "orochi" || networkConfig.MultiTokenPaymasterOracleUsed == "chainlink" || networkConfig.MultiTokenPaymasterOracleUsed == "etherspotChainlink"))
423+
throw new Error("Oracle is not Defined/Invalid");
424+
if (networkConfig.MultiTokenPaymasterOracleUsed == "chainlink" && !NativeOracles[chainId])
425+
throw new Error("Native Oracle address not set for this chainId")
426+
result = await paymaster.signMultiTokenPaymaster(userOp, str, str1, entryPoint, multiTokenRec.paymasterAddress, gasToken, multiTokenRec.oracleAddress ?? '', bundlerUrl, signer, networkConfig.MultiTokenPaymasterOracleUsed, NativeOracles[chainId], chainId, server.log);
427+
} else {
428+
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_TOKEN })
429+
}
430+
break;
431+
}
397432
default: {
398433
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_MODE });
399434
}

backend/src/routes/pimlico-routes.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ReturnCode from "../constants/ReturnCode.js";
99
import { decode } from "../utils/crypto.js";
1010
import { printRequest, getNetworkConfig } from "../utils/common.js";
1111
import { APIKey } from "../models/api-key.js";
12+
import { ethers } from "ethers";
1213

1314
const pimlicoRoutes: FastifyPluginAsync = async (server) => {
1415

@@ -105,6 +106,42 @@ const pimlicoRoutes: FastifyPluginAsync = async (server) => {
105106
}
106107
)
107108

109+
server.post("/getAllCommonERC20PaymasterAddress",
110+
ResponseSchema,
111+
async function (request, reply) {
112+
try {
113+
printRequest("/getAllCommonERC20PaymasterAddress", request, server.log);
114+
const query: any = request.query;
115+
const body: any = request.body;
116+
const entryPoint = body.params[0];
117+
const api_key = query['apiKey'] ?? body.params[1];
118+
if (!api_key || typeof(api_key) !== "string")
119+
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY })
120+
if (!server.config.EPV_06.includes(entryPoint ?? '')) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.UNSUPPORTED_ENTRYPOINT })
121+
const apiKeyData = await server.apiKeyRepository.findOneByApiKey(api_key);
122+
if (!apiKeyData) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY })
123+
const multiTokenRec = await server.multiTokenPaymasterRepository.findAll();
124+
const result = multiTokenRec.map((record) => {
125+
return {
126+
paymasterAddress: record.paymasterAddress,
127+
gasToken: ethers.utils.getAddress(record.tokenAddress),
128+
chainId: record.chainId,
129+
decimals: record.decimals
130+
}
131+
});
132+
server.log.info(result, 'getAllCommonERC20PaymasterAddress Response sent: ');
133+
if (body.jsonrpc)
134+
return reply.code(ReturnCode.SUCCESS).send({ jsonrpc: body.jsonrpc, id: body.id, message: JSON.stringify(result), error: null })
135+
return reply.code(ReturnCode.SUCCESS).send({message: JSON.stringify(result)});
136+
} catch (err: any) {
137+
request.log.error(err);
138+
if (err.name == "ResourceNotFoundException")
139+
return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY });
140+
return reply.code(ReturnCode.FAILURE).send({ error: err.message ?? ErrorMessage.FAILED_TO_PROCESS });
141+
}
142+
}
143+
)
144+
108145
};
109146

110147
export default pimlicoRoutes;

0 commit comments

Comments
 (0)