Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/full-rockets-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphprotocol/grc-20": patch
---

add getSmartAccountWalletClient
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ const cid = await Ipfs.publishEdit({
})
```

### Publishing an edit onchain
### Publishing an edit onchain using your wallet

Once you've uploaded the binary encoded Edit to IPFS and have correctly formed `ipfs://hash`, you can write this to a space.

Expand Down Expand Up @@ -274,6 +274,35 @@ const txResult = await walletClient.sendTransaction({
});
```

### Publishing an edit onchain using your Geo Account

The Geo Genesis browser uses a smart account associated with your account to publish edits. There may be situations where you want to use the same account in your code as you do on Geo Genesis. In order to get the smart account wallet client you can use the `getSmartAccountWalletClient` function.

To use `getSmartAccountWalletClient` you'll need the private key associated with your Geo account. You can get your private key using https://www.geobrowser.io/export-wallet.

Transaction costs from your smart account will be sponsored by the Geo team for the duration of the early access period. Eventually you will need to provide your own API key or provide funds to your smart account.

```ts
import { getSmartAccountWalletClient } from '@graphprotocol/grc-20';

// IMPORTANT: Be careful with your private key. Don't commit it to version control.
// You can get your private key using https://www.geobrowser.io/export-wallet
const privateKey = `0x${privateKeyFromGeoWallet}`;
const smartAccountWalletClient = await getSmartAccountWalletClient({
privateKey,
// rpcUrl, // optional
});

// publish an edit to IPFS
// get the calldata for the edit

const txResult = await smartAccountWalletClient.sendTransaction({
to: to,
value: 0n,
data: data,
});
```

### Deploying a space

You can deploy spaces programmatically using the API. Currently there are two types of governance modes for spaces: one with voting and one without. They're called PUBLIC or PERSONAL spaces respectively. The API only supports deploying the PERSONAL governance mode currently.
Expand Down
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,5 @@ export { getChecksumAddress } from './src/core/get-checksum-address.js';
export * as Ipfs from './src/ipfs.js';

export * as Graph from './src/graph/index.js';

export { getSmartAccountWalletClient } from './src/smart-wallet.js';
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@bufbuild/protobuf": "^1.9.0",
"effect": "^3.13.6",
"image-size": "^2.0.0",
"permissionless": "^0.2.35",
"position-strings": "^2.0.1",
"uuid": "^11.1.0",
"viem": "^2.23.6"
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 84 additions & 0 deletions src/smart-wallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { toSafeSmartAccount } from 'permissionless/accounts';
import { http, createPublicClient } from 'viem';
import { entryPoint07Address } from 'viem/account-abstraction';
import { privateKeyToAccount } from 'viem/accounts';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getSmartAccountWalletClient } from './smart-wallet.js';

// mock all external dependencies
vi.mock('permissionless', () => ({
createSmartAccountClient: vi.fn().mockReturnValue({ mockSmartAccountClient: true }),
}));

vi.mock('permissionless/accounts', () => ({
toSafeSmartAccount: vi.fn().mockResolvedValue({ mockSafeAccount: true }),
}));

vi.mock('permissionless/clients/pimlico', () => ({
createPimlicoClient: vi.fn().mockReturnValue({
mockPimlicoClient: true,
getUserOperationGasPrice: vi.fn().mockResolvedValue({
fast: { maxFeePerGas: 1000000000n, maxPriorityFeePerGas: 100000000n },
}),
}),
}));

vi.mock('viem', () => ({
createPublicClient: vi.fn().mockReturnValue({ mockPublicClient: true }),
http: vi.fn().mockImplementation(url => ({ mockTransport: true, url })),
}));

vi.mock('viem/accounts', () => ({
privateKeyToAccount: vi.fn().mockReturnValue({ mockAccount: true }),
}));

describe('getSmartAccountWalletClient', () => {
const mockPrivateKey = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';

beforeEach(() => {
vi.clearAllMocks();
});

it('should create a client with the default RPC URL when no RPC URL is provided', async () => {
await getSmartAccountWalletClient({ privateKey: mockPrivateKey, });

expect(http).toHaveBeenCalledWith('https://rpc-geo-genesis-h0q2s21xx8.t.conduit.xyz');
expect(createPublicClient).toHaveBeenCalledWith(
expect.objectContaining({
transport: { mockTransport: true, url: 'https://rpc-geo-genesis-h0q2s21xx8.t.conduit.xyz' },
}),
);
});

it('should create a client with a custom RPC URL when provided', async () => {
const customRpcUrl = 'https://custom-rpc.example.com';
await getSmartAccountWalletClient({
privateKey: mockPrivateKey,
rpcUrl: customRpcUrl,
});

expect(http).toHaveBeenCalledWith(customRpcUrl);
expect(createPublicClient).toHaveBeenCalledWith(
expect.objectContaining({
transport: { mockTransport: true, url: customRpcUrl },
}),
);
});

it('should initialize safe account with correct parameters', async () => {
await getSmartAccountWalletClient({ privateKey: mockPrivateKey, });

expect(privateKeyToAccount).toHaveBeenCalledWith(mockPrivateKey);
expect(toSafeSmartAccount).toHaveBeenCalledWith(
expect.objectContaining({
client: { mockPublicClient: true },
owners: [{ mockAccount: true }],
entryPoint: {
address: entryPoint07Address,
version: '0.7',
},
version: '1.4.1',
}),
);
});
});
124 changes: 124 additions & 0 deletions src/smart-wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { type SmartAccountClient, createSmartAccountClient } from 'permissionless';
import { type SafeSmartAccountImplementation, toSafeSmartAccount } from 'permissionless/accounts';
import { createPimlicoClient } from 'permissionless/clients/pimlico';
import type { Address, Chain, Hex, HttpTransport } from 'viem';
import { http, createPublicClient } from 'viem';
import { type SmartAccountImplementation, entryPoint07Address } from 'viem/account-abstraction';
import { privateKeyToAccount } from 'viem/accounts';

const DEFAULT_RPC_URL = 'https://rpc-geo-genesis-h0q2s21xx8.t.conduit.xyz';

/**
* We provide a fallback API key for gas sponsorship for the duration of the
* Geo Genesis early access period. This API key is gas-limited.
*/
const DEFAULT_API_KEY = 'pim_KqHm63txxhbCYjdDaWaHqH';

type GetSmartAccountWalletClientParams = {
privateKey: Hex;
rpcUrl?: string;
};

type SafeSmartAccount = SafeSmartAccountImplementation<'0.7'> & {
address: Address;
getNonce: NonNullable<SmartAccountImplementation['getNonce']>;
isDeployed: () => Promise<boolean>;
type: 'smart';
};

type GeoSmartAccount = SmartAccountClient<
HttpTransport<undefined, false>,
Chain,
object &
SafeSmartAccount & {
address: Address;
getNonce: NonNullable<SmartAccountImplementation['getNonce']>;
isDeployed: () => Promise<boolean>;
type: 'smart';
},
undefined,
undefined
>;

// type GeoSmartAccountWalletClient = Promise<ReturnType<typeof createSmartAccountClient>>;

/**
* Get a smart account wallet client for your Geo account.
*
* IMPORTANT: Be careful with your private key. Don't commit it to version control.
* You can get your private key using https://www.geobrowser.io/export-wallet
*
* @example
* ```ts
* const smartAccountWalletClient = await getSmartAccountWalletClient({
* privateKey: '0x...',
* rpcUrl: '...', // optional
* });
* ```
* @param params – {@link GetSmartAccountWalletClientParams}
* @returns – {@link SmartAccountClient}
*/
export const getSmartAccountWalletClient = async ({
privateKey,
rpcUrl = DEFAULT_RPC_URL,
}: GetSmartAccountWalletClientParams): Promise<GeoSmartAccount> => {
const GEOGENESIS: Chain = {
id: Number('80451'),
name: 'Geo Genesis',
nativeCurrency: {
name: 'Ethereum',
symbol: 'ETH',
decimals: 18,
},
rpcUrls: {
default: {
http: [rpcUrl],
},
public: {
http: [rpcUrl],
},
},
};

const transport = http(rpcUrl);

const publicClient = createPublicClient({
transport,
chain: GEOGENESIS,
});

const safeAccount = await toSafeSmartAccount({
client: publicClient,
owners: [privateKeyToAccount(privateKey)],
entryPoint: {
// optional, defaults to 0.7
address: entryPoint07Address,
version: '0.7',
},
version: '1.4.1',
});

const bundlerTransport = http(`https://api.pimlico.io/v2/80451/rpc?apikey=${DEFAULT_API_KEY}`);
const paymasterClient = createPimlicoClient({
transport: bundlerTransport,
chain: GEOGENESIS,
entryPoint: {
address: entryPoint07Address,
version: '0.7',
},
});

const smartAccount = createSmartAccountClient({
chain: GEOGENESIS,
account: safeAccount,
paymaster: paymasterClient,
bundlerTransport,
userOperation: {
estimateFeesPerGas: async () => {
return (await paymasterClient.getUserOperationGasPrice()).fast;
},
},
});

return smartAccount;
};
Loading