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

Expose Sponsorship Policy Fetching #82

Merged
merged 1 commit into from
Oct 21, 2024
Merged
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
14 changes: 6 additions & 8 deletions examples/send-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ethers } from "ethers";
import { isAddress } from "viem";

import { loadArgs, loadEnv } from "./cli";
import { DEFAULT_SAFE_SALT_NONCE, NearSafe } from "../src";
import { DEFAULT_SAFE_SALT_NONCE, NearSafe, Network } from "../src";

dotenv.config();

Expand Down Expand Up @@ -31,11 +31,7 @@ async function main(): Promise<void> {
},
];
// Add Recovery if safe not deployed & recoveryAddress was provided.
if (
!(await txManager.safeDeployed(chainId)) &&
recoveryAddress &&
isAddress(recoveryAddress)
) {
if (recoveryAddress && isAddress(recoveryAddress)) {
const recoveryTx = txManager.addOwnerTx(recoveryAddress);
// This would happen (sequentially) after the userTx, but all executed in a single
transactions.push(recoveryTx);
Expand Down Expand Up @@ -75,8 +71,10 @@ async function main(): Promise<void> {
});
console.log("userOpHash:", userOpHash);

const userOpReceipt = await txManager.getOpReceipt(chainId, userOpHash);
console.log("userOpReceipt:", userOpReceipt);
const { receipt } = await txManager.getOpReceipt(chainId, userOpHash);
console.log(
`View Transaction: ${Network.fromChainId(chainId).scanUrl}/tx/${receipt.transactionHash}`
);
}

main().catch((err) => {
Expand Down
22 changes: 22 additions & 0 deletions src/lib/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
import {
GasPrices,
PaymasterData,
SponsorshipPoliciesResponse,
SponsorshipPolicyData,
UnsignedUserOperation,
UserOperation,
UserOperationReceipt,
Expand Down Expand Up @@ -128,6 +130,26 @@ export class Erc4337Bundler {
})
);
}

// New method to query sponsorship policies
async getSponsorshipPolicies(): Promise<SponsorshipPolicyData[]> {
const url = `https://api.pimlico.io/v2/account/sponsorship_policies?apikey=${this.apiKey}`;
const allPolocies = await handleRequest<SponsorshipPoliciesResponse>(
async () => {
const response = await fetch(url);

if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}: ${response.statusText}`
);
}
return response.json();
}
);
return allPolocies.data.filter((p) =>
p.chain_ids.allowlist.includes(this.chainId)
);
}
}

async function handleRequest<T>(clientMethod: () => Promise<T>): Promise<T> {
Expand Down
9 changes: 6 additions & 3 deletions src/near-safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
EncodedTxData,
EvmTransactionData,
MetaTransaction,
SponsorshipPolicyData,
UserOperation,
UserOperationReceipt,
} from "./types";
Expand Down Expand Up @@ -166,9 +167,6 @@ export class NearSafe {
if (transactions.length === 0) {
throw new Error("Empty transaction set!");
}
console.log(
`Building UserOp on chainId ${chainId} with ${transactions.length} transaction(s)`
);
const bundler = this.bundlerForChainId(chainId);
const [gasFees, nonce, safeDeployed] = await Promise.all([
bundler.getGasPrice(),
Expand Down Expand Up @@ -473,4 +471,9 @@ export class NearSafe {
}
}
}

async policyForChainId(chainId: number): Promise<SponsorshipPolicyData[]> {
const bundler = this.bundlerForChainId(chainId);
return bundler.getSponsorshipPolicies();
}
}
33 changes: 33 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,36 @@ export interface EncodedTxData {
request: SignArgs;
}>;
}

export interface SponsorshipPoliciesResponse {
has_more: boolean;
data: SponsorshipPolicyData[];
}

export interface SponsorshipPolicyData {
id: string;
policy_name: string;
limits: PolicyLimits;
start_time: string; // ISO 8601 format string
end_time: string; // ISO 8601 format string
chain_ids: ChainIds;
policy_status: string; // Possibly "active" or other status strings
created_at: string; // ISO 8601 format string
}

export interface PolicyLimits {
global: GlobalLimits;
}

export interface GlobalLimits {
user_operation_spending: SpendingLimit;
}

export interface SpendingLimit {
amount: number;
currency: string; // Example: "USD"
}

export interface ChainIds {
allowlist: number[]; // Array of chain IDs
}
14 changes: 12 additions & 2 deletions tests/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import dotenv from "dotenv";
import { DEFAULT_SAFE_SALT_NONCE, NearSafe } from "../src";

dotenv.config();

describe("Near Safe Requests", () => {
it("buildTransaction", async () => {
const adapter = await NearSafe.create({
let adapter: NearSafe;
beforeAll(async () => {
// Initialize the NearSafe adapter once before all tests
adapter = await NearSafe.create({
accountId: "neareth-dev.testnet",
mpcContractId: "v1.signer-prod.testnet",
pimlicoKey: process.env.PIMLICO_KEY!,
safeSaltNonce: DEFAULT_SAFE_SALT_NONCE,
});
});

it("buildTransaction", async () => {
const irrelevantData = {
data: "0xbeef",
value: "0",
Expand Down Expand Up @@ -41,4 +47,8 @@ describe("Near Safe Requests", () => {
})
).rejects.toThrow();
});

it("bundler: getSponsorshipPolicy", async () => {
await expect(adapter.policyForChainId(100)).resolves.not.toThrow();
});
});
4 changes: 2 additions & 2 deletions tests/unit/lib/bundler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ describe("Safe Pack", () => {
);
});

it.only("Strips API Key from error message", () => {
it("Strips API Key from error message", () => {
const apiKey = "any-thirty-six-character-long-string";
const message = (x: string): string => `Unexpected Error
URL: https://api.pimlico.io/v2/11155111/rpc?apikey=${x}
Additional Error Context`;
Request body: {"method":"pm_sponsorUserOperation",{"sponsorshipPolicyId":"sp_clear_vampiro"}]}`;
expect(stripApiKey(new Error(message(apiKey)))).toEqual(message("***"));

expect(stripApiKey(new Error(message("TopSecret")))).toEqual(
Expand Down