Skip to content

Commit

Permalink
feat: support enhanced apis in alchemy provider (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
avasisht23 authored and moldy530 committed Nov 10, 2023
1 parent a88d84c commit 69dded7
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 3 deletions.
55 changes: 55 additions & 0 deletions packages/alchemy/e2e-tests/simple-account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
type SmartAccountSigner,
AA_SDK_TESTS_SIGNER_TYPE,
} from "@alchemy/aa-core";
import { Alchemy, Network } from "alchemy-sdk";
import { toHex, type Address, type Chain, type Hash } from "viem";
import { mnemonicToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
import { AlchemyProvider } from "../src/provider.js";
import { API_KEY, OWNER_MNEMONIC, PAYMASTER_POLICY_ID } from "./constants.js";

const chain = sepolia;
const network = Network.ETH_SEPOLIA;

describe("Simple Account Tests", () => {
const ownerAccount = mnemonicToAccount(OWNER_MNEMONIC);
Expand Down Expand Up @@ -148,6 +150,59 @@ describe("Simple Account Tests", () => {
const txnHash = signer.waitForUserOperationTransaction(replacedResult.hash);
await expect(txnHash).resolves.not.toThrowError();
}, 50000);

it("should get token balances for the smart account", async () => {
const alchemy = new Alchemy({
apiKey: API_KEY!,
network,
});
const provider = givenConnectedProvider({
owner,
chain,
})
.withAlchemyGasManager({
policyId: PAYMASTER_POLICY_ID,
})
.withAlchemyEnhancedApis(alchemy);

const address = await provider.getAddress();
const balances = await provider.core.getTokenBalances(address);
expect(balances.tokenBalances).toMatchInlineSnapshot(`
[
{
"contractAddress": "0x489c5cb7fd158b0a9e7975076d758268a756c025",
"tokenBalance": "0x000000000000000000000000000000000000000000000000000000000065b9aa",
},
{
"contractAddress": "0x54fa517f05e11ffa87f4b22ae87d91cec0c2d7e1",
"tokenBalance": "0x000000000000000000000000000000000000000000000000000000000065b9aa",
},
{
"contractAddress": "0xdcf5d3e08c5007dececdb34808c49331bd82a247",
"tokenBalance": "0x00000000000000000000000000000000000000000000000000000000000f423f",
},
]
`);
}, 50000);

it("should get owned nfts for the smart account", async () => {
const alchemy = new Alchemy({
apiKey: API_KEY!,
network,
});
const provider = givenConnectedProvider({
owner,
chain,
})
.withAlchemyGasManager({
policyId: PAYMASTER_POLICY_ID,
})
.withAlchemyEnhancedApis(alchemy);

const address = await provider.getAddress();
const nfts = await provider.nft.getNftsForOwner(address);
expect(nfts.ownedNfts).toMatchInlineSnapshot("[]");
}, 50000);
});

const givenConnectedProvider = ({
Expand Down
5 changes: 4 additions & 1 deletion packages/alchemy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,8 @@
"url": "https://github.com/alchemyplatform/aa-sdk/issues"
},
"homepage": "https://github.com/alchemyplatform/aa-sdk#readme",
"gitHead": "ee46e8bb857de3b631044fa70714ea706d9e317d"
"gitHead": "ee46e8bb857de3b631044fa70714ea706d9e317d",
"peerDependencies": {
"alchemy-sdk": "^2.10.1"
}
}
160 changes: 160 additions & 0 deletions packages/alchemy/src/__tests__/__snapshots__/provider.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,163 @@ exports[`Alchemy Provider Tests > should correctly do runtime validation when mu
}
]"
`;

exports[`Alchemy Provider Tests > should have called propety 1`] = `
Alchemy {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
"core": CoreNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"debug": DebugNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"nft": NftNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"notify": NotifyNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"transact": TransactNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"ws": WebSocketNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
}
`;

exports[`Alchemy Provider Tests > should have enhanced api properties extended from the Alchemy SDK 1`] = `
Alchemy {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
"core": CoreNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"debug": DebugNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"nft": NftNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"notify": NotifyNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"transact": TransactNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
"ws": WebSocketNamespace {
"config": AlchemyConfig {
"apiKey": "test",
"authToken": undefined,
"batchRequests": false,
"maxRetries": 5,
"network": "eth-sepolia",
"requestTimeout": 0,
"url": undefined,
},
},
}
`;
32 changes: 31 additions & 1 deletion packages/alchemy/src/__tests__/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
type BatchUserOperationCallData,
type SmartAccountSigner,
} from "@alchemy/aa-core";
import { Alchemy, Network } from "alchemy-sdk";
import { toHex, type Address, type Chain } from "viem";
import { mnemonicToAccount } from "viem/accounts";
import { polygonMumbai } from "viem/chains";
import { polygonMumbai, sepolia } from "viem/chains";
import { AlchemyProvider } from "../provider.js";

describe("Alchemy Provider Tests", () => {
Expand Down Expand Up @@ -101,6 +102,35 @@ describe("Alchemy Provider Tests", () => {
})
).toThrowErrorMatchingSnapshot();
});

it("should have enhanced api properties extended from the Alchemy SDK", async () => {
const provider = new AlchemyProvider({
apiKey: "test",
chain: sepolia,
});
const spy = vi.spyOn(provider, "withAlchemyEnhancedApis");

const alchemy = new Alchemy({
network: Network.ETH_SEPOLIA,
apiKey: "test",
});
provider.withAlchemyEnhancedApis(alchemy);

expect(spy.mock.calls[0][0]).toMatchSnapshot();
});

it("should throw runtime error if chains don't match between provider and AlchemySDK client", async () => {
expect(() => {
const alchemy = new Alchemy({
network: Network.MATIC_MAINNET,
apiKey: "test",
});

givenConnectedProvider({ owner, chain }).withAlchemyEnhancedApis(alchemy);
}).toThrowErrorMatchingInlineSnapshot(
'"Alchemy SDK client JSON-RPC URL must match AlchemyProvider JSON-RPC URL"'
);
});
});

const givenConnectedProvider = ({
Expand Down
52 changes: 51 additions & 1 deletion packages/alchemy/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
resolveProperties,
type AccountMiddlewareFn,
} from "@alchemy/aa-core";
import { Alchemy } from "alchemy-sdk";
import { type HttpTransport } from "viem";
import {
arbitrum,
Expand All @@ -18,12 +19,16 @@ import {
withAlchemyGasManager,
type AlchemyGasManagerConfig,
} from "./middleware/gas-manager.js";
import { AlchemyProviderConfigSchema } from "./schema.js";
import {
AlchemyProviderConfigSchema,
AlchemySdkClientSchema,
} from "./schema.js";
import type { AlchemyProviderConfig } from "./type.js";

export class AlchemyProvider extends SmartAccountProvider<HttpTransport> {
private pvgBuffer: bigint;
private feeOptsSet: boolean;
private rpcUrl: string;

constructor(config: AlchemyProviderConfig) {
AlchemyProviderConfigSchema.parse(config);
Expand Down Expand Up @@ -82,6 +87,7 @@ export class AlchemyProvider extends SmartAccountProvider<HttpTransport> {
}

this.feeOptsSet = !!feeOpts;
this.rpcUrl = rpcUrl;
}

override gasEstimator: AccountMiddlewareFn = async (struct) => {
Expand Down Expand Up @@ -114,4 +120,48 @@ export class AlchemyProvider extends SmartAccountProvider<HttpTransport> {

return withAlchemyGasManager(this, config, !this.feeOptsSet);
}

/**
* This methods adds Alchemy Enhanced APIs to the provider, via a peer dependency on `alchemy-sdk`.
* @see: https://github.com/alchemyplatform/alchemy-sdk-js
*
* The Alchemy SDK client must be configured with the same API key and network as the AlchemyProvider.
* This method validates such at runtime.
*
* Additionally, since the Alchemy SDK client does not support JWT authentication, AlchemyProviders initialized with JWTs cannot use this method.
* They must be initialized with an API key or RPC URL.
* There is an open issue on the Alchemy SDK repo to add JWT support in the meantime.
* @see: https://github.com/alchemyplatform/alchemy-sdk-js/issues/386
*
* @param alchemy - an initialized Alchemy SDK client
* @returns - a new AlchemyProvider extended with Alchemy SDK client methods
*/
withAlchemyEnhancedApis(alchemy: Alchemy): this & Alchemy {
AlchemySdkClientSchema.parse(alchemy);

if (alchemy.config.url && alchemy.config.url !== this.rpcUrl) {
throw new Error(
"Alchemy SDK client JSON-RPC URL must match AlchemyProvider JSON-RPC URL"
);
}

const alchemyUrl = `https://${alchemy.config.network}.g.alchemy.com/v2/${alchemy.config.apiKey}`;
if (alchemyUrl !== this.rpcUrl) {
throw new Error(
"Alchemy SDK client JSON-RPC URL must match AlchemyProvider JSON-RPC URL"
);
}

return this.extend(() => {
return {
core: alchemy.core,
nft: alchemy.nft,
transact: alchemy.transact,
debug: alchemy.debug,
ws: alchemy.ws,
notify: alchemy.notify,
config: alchemy.config,
};
});
}
}
3 changes: 3 additions & 0 deletions packages/alchemy/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSmartAccountProviderConfigSchema } from "@alchemy/aa-core";
import { Alchemy } from "alchemy-sdk";
import z from "zod";

export const ConnectionConfigSchema = z.union([
Expand Down Expand Up @@ -53,3 +54,5 @@ export const AlchemyProviderConfigSchema = z
})
.and(createSmartAccountProviderConfigSchema().omit({ rpcProvider: true }))
.and(ConnectionConfigSchema);

export const AlchemySdkClientSchema = z.instanceof(Alchemy);

0 comments on commit 69dded7

Please sign in to comment.