Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/signed_vp.json
/raw_vc.json
/decrypted.json
/wr/

# Verifiable Presentation manual-test fixtures — generated, deliberately not committed.
# They carry throwaway private keys, and every credential and presentation is bound to the
Expand Down
698 changes: 361 additions & 337 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,24 @@
},
"dependencies": {
"@inquirer/prompts": "^5.3.8",
"@trustvc/trustvc": "2.16.0-beta.3",
"@trustvc/trustvc": "^2.16.0-beta.4",
"@types/yargs": "^17.0.32",
"chalk": "^4.1.2",
"dotenv": "^16.0.0",
"ethers": "^6.15.0",
"inquirer": "^13.1.0",
"node-fetch": "^3.3.2",
"ox": "0.14.29",
"permissionless": "0.3.6",
"signale": "^1.4.0",
"viem": "2.53.1",
"yargs": "^17.7.2"
},
"overrides": {
"permissionless": {
"ox": "$ox"
}
},
Comment thread
RishabhS7 marked this conversation as resolved.
"devDependencies": {
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
Expand Down
29 changes: 29 additions & 0 deletions src/commands/gasless/admin/add-authorized-caller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { error } from 'signale';
import { addAuthorizedCaller } from '@trustvc/trustvc';
import { promptAddress } from '../../../utils';
import { promptForPaymasterAdminWalletInputs, runPaymasterAdminAction } from './common';

export const command = 'add-authorized-caller';

export const describe =
'Authorizes an address to trigger sponsored holder/beneficiary title-escrow or registry calls (Path A) on a PlatformPaymaster';

export const handler = async (): Promise<string | undefined> => {
try {
const base = await promptForPaymasterAdminWalletInputs();
const caller = await promptAddress('caller', 'address to authorize on the paymaster');

return await runPaymasterAdminAction({
...base,
actionLabel: `Authorizing caller ${caller}`,
execute: (wallet) =>
addAuthorizedCaller(
wallet,
base.paymasterAddress as `0x${string}`,
caller as `0x${string}`,
),
});
} catch (err: unknown) {
error(err instanceof Error ? err.message : String(err));
}
};
25 changes: 25 additions & 0 deletions src/commands/gasless/admin/add-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { error } from 'signale';
import { addRegistry } from '@trustvc/trustvc';
import { promptAddress } from '../../../utils';
import { promptForPaymasterAdminWalletInputs, runPaymasterAdminAction } from './common';

export const command = 'add-registry';

export const describe =
'Authorizes a token registry so its calls can be sponsored by a PlatformPaymaster';

export const handler = async (): Promise<string | undefined> => {
try {
const base = await promptForPaymasterAdminWalletInputs();
const registry = await promptAddress('registry', 'token registry address to authorize');

return await runPaymasterAdminAction({
...base,
actionLabel: `Authorizing registry ${registry}`,
execute: (wallet) =>
addRegistry(wallet, base.paymasterAddress as `0x${string}`, registry as `0x${string}`),
});
} catch (err: unknown) {
error(err instanceof Error ? err.message : String(err));
}
};
29 changes: 29 additions & 0 deletions src/commands/gasless/admin/add-title-escrow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { error } from 'signale';
import { addTitleEscrow } from '@trustvc/trustvc';
import { promptAddress } from '../../../utils';
import { promptForPaymasterAdminWalletInputs, runPaymasterAdminAction } from './common';

export const command = 'add-title-escrow';

export const describe =
'Authorizes a title escrow so its calls can be sponsored by a PlatformPaymaster';

export const handler = async (): Promise<string | undefined> => {
try {
const base = await promptForPaymasterAdminWalletInputs();
const titleEscrow = await promptAddress('title escrow', 'title escrow address to authorize');

return await runPaymasterAdminAction({
...base,
actionLabel: `Authorizing title escrow ${titleEscrow}`,
execute: (wallet) =>
addTitleEscrow(
wallet,
base.paymasterAddress as `0x${string}`,
titleEscrow as `0x${string}`,
),
});
} catch (err: unknown) {
error(err instanceof Error ? err.message : String(err));
}
};
85 changes: 85 additions & 0 deletions src/commands/gasless/admin/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { error, info, success } from 'signale';
import {
getErrorMessage,
getEtherscanAddress,
getWalletOrSigner,
promptAddress,
promptNetworkSelection,
promptWalletSelection,
} from '../../../utils';
import { assertGaslessSupportedNetwork, redactPimlicoApiKey } from '../config';
import { GaslessWalletOption } from '../types';

/**
* PlatformPaymaster admin functions (add/remove authorized caller, registry, title escrow;
* user whitelist credits; daily limit) are all owner-only, regular (non-gasless) transactions —
* the owner pays gas directly to configure their own paymaster.
*/
export type PaymasterAdminWalletCommand = GaslessWalletOption & {
network: string;
paymasterAddress: string;
};

export const promptForPaymasterAdminWalletInputs =
async (): Promise<PaymasterAdminWalletCommand> => {
const network = await promptNetworkSelection();
assertGaslessSupportedNetwork(network);

const paymasterAddress = await promptAddress(
'paymaster',
'PlatformPaymaster contract to administer',
);

const { encryptedWalletPath, key, keyFile } = await promptWalletSelection();

return {
network,
paymasterAddress: paymasterAddress as string,
encryptedWalletPath,
key,
keyFile,
};
};

export interface RunPaymasterAdminActionArgs extends PaymasterAdminWalletCommand {
actionLabel: string;
/** Wallet is a raw ethers Signer here, not a viem WalletClient — no smart account involved. */
execute: (wallet: Awaited<ReturnType<typeof getWalletOrSigner>>) => Promise<`0x${string}`>;
}

/**
* Shared runner for every paymaster admin action: resolves a regular wallet, runs the given
* on-chain call, and reports the transaction hash. Errors are caught and logged here, so callers
* only need to guard their own prompting step.
*/
export const runPaymasterAdminAction = async ({
network,
paymasterAddress,
encryptedWalletPath,
key,
keyFile,
actionLabel,
execute,
}: RunPaymasterAdminActionArgs): Promise<string | undefined> => {
try {
const assertedNetwork = assertGaslessSupportedNetwork(network);

const wallet = await getWalletOrSigner({
network: assertedNetwork,
encryptedWalletPath,
key,
keyFile,
});

info(`${actionLabel} on PlatformPaymaster ${paymasterAddress}...`);

const txHash = await execute(wallet);

success(`${actionLabel} — done`);
info(`Find more details at ${getEtherscanAddress({ network: assertedNetwork })}/tx/${txHash}`);

return txHash;
} catch (e) {
error(redactPimlicoApiKey(getErrorMessage(e)));
}
};
82 changes: 82 additions & 0 deletions src/commands/gasless/admin/delegate-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { error, info, success } from 'signale';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import {
getErrorMessage,
getEtherscanAddress,
getWalletOrSigner,
promptNetworkSelection,
promptWalletSelection,
} from '../../../utils';
import {
assertGaslessSupportedNetwork,
getEip7702ImplementationAddress,
getGaslessRpcUrl,
getViemChain,
redactPimlicoApiKey,
} from '../config';

// Submits a standalone EIP-7702 authorization for a user's own EOA — the first of the three
// setup steps ("Delegate" in the README) needed before an account can act as a smart account.
// Regular, non-gasless transaction: the user pays gas to delegate their own account. Every gasless
// command already bundles this automatically with its first sponsored UserOperation, so running
// this separately is only needed to set delegation up ahead of time.
export const command = 'delegate-user';

export const describe =
"Delegates a user's EOA to the deployed EIP7702Implementation contract via a standalone EIP-7702 authorization. Regular transaction — the user pays gas to delegate their own account.";

export const handler = async (): Promise<string | undefined> => {
try {
const network = await promptNetworkSelection();
const assertedNetwork = assertGaslessSupportedNetwork(network);

const { encryptedWalletPath, key, keyFile } = await promptWalletSelection();

// Only used to recover the raw private key: viem needs a LocalAccount to sign the
// authorization, not an ethers Signer.
const wallet = await getWalletOrSigner({
network: assertedNetwork,
encryptedWalletPath,
key,
keyFile,
});

const privateKey = (wallet as { privateKey?: string }).privateKey;
if (!privateKey) {
throw new Error(
'Delegating requires direct access to a private key (encrypted wallet file, --key, --key-file, or OA_PRIVATE_KEY). AWS KMS signers are not supported.',
);
}

const implementationAddress = getEip7702ImplementationAddress(assertedNetwork);
const account = privateKeyToAccount(privateKey as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: getViemChain(assertedNetwork),
transport: http(getGaslessRpcUrl(assertedNetwork)),
});

info(`Delegating ${account.address} to ${implementationAddress} on ${assertedNetwork}...`);

// `executor: 'self'` tells viem the account signing the authorization is also the one
// submitting the transaction, so it correctly uses nonce + 1 rather than the current nonce.
const authorization = await walletClient.signAuthorization({
contractAddress: implementationAddress,
executor: 'self',
});

const txHash = await walletClient.sendTransaction({
authorizationList: [authorization],
to: account.address,
value: 0n,
});

success(`Account ${account.address} delegated to ${implementationAddress}`);
info(`Find more details at ${getEtherscanAddress({ network: assertedNetwork })}/tx/${txHash}`);

return txHash;
} catch (e) {
error(redactPimlicoApiKey(getErrorMessage(e)));
}
};
45 changes: 45 additions & 0 deletions src/commands/gasless/admin/fund-paymaster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { error } from 'signale';
import { input } from '@inquirer/prompts';
import { ethers } from 'ethers';
import { eip7702Abis } from '@trustvc/trustvc';
import { promptForPaymasterAdminWalletInputs, runPaymasterAdminAction } from './common';

export const command = 'fund-paymaster';

export const describe =
"Deposits ETH into a PlatformPaymaster's EntryPoint gas balance so it can sponsor UserOperations. Regular transaction — the caller pays the deposited amount plus gas.";

export const handler = async (): Promise<string | undefined> => {
try {
const base = await promptForPaymasterAdminWalletInputs();

const amountEth = await input({
message: 'Enter the amount to deposit in ETH:',
required: true,
validate: (value: string) => {
if (!/^\d*\.?\d+$/.test(value) || Number(value) <= 0) {
return 'Amount must be a positive number (ETH)';
}
return true;
},
});
const amount = ethers.parseEther(amountEth);

return await runPaymasterAdminAction({
...base,
actionLabel: `Depositing ${amountEth} ETH`,
execute: async (wallet) => {
const contract = new ethers.Contract(
base.paymasterAddress,
eip7702Abis.platformPaymasterAbi,
wallet,
);
const tx = await contract.deposit({ value: amount });
const receipt = await tx.wait();
return receipt.hash as `0x${string}`;
},
});
} catch (err: unknown) {
error(err instanceof Error ? err.message : String(err));
}
};
11 changes: 11 additions & 0 deletions src/commands/gasless/admin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Argv } from 'yargs';

export const command = 'paymaster-admin <method>';

export const describe =
'Administer a PlatformPaymaster contract (owner-only; regular, non-gasless transactions)';

export const builder = (yargs: Argv): Argv =>
yargs.commandDir(__dirname, { extensions: ['ts', 'js'] });

export const handler = (): void => {};
29 changes: 29 additions & 0 deletions src/commands/gasless/admin/remove-authorized-caller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { error } from 'signale';
import { removeAuthorizedCaller } from '@trustvc/trustvc';
import { promptAddress } from '../../../utils';
import { promptForPaymasterAdminWalletInputs, runPaymasterAdminAction } from './common';

export const command = 'remove-authorized-caller';

export const describe =
'Removes an address from the authorized-caller list (Path A) on a PlatformPaymaster';

export const handler = async (): Promise<string | undefined> => {
try {
const base = await promptForPaymasterAdminWalletInputs();
const caller = await promptAddress('caller', 'address to remove from the paymaster');

return await runPaymasterAdminAction({
...base,
actionLabel: `Removing caller ${caller}`,
execute: (wallet) =>
removeAuthorizedCaller(
wallet,
base.paymasterAddress as `0x${string}`,
caller as `0x${string}`,
),
});
} catch (err: unknown) {
error(err instanceof Error ? err.message : String(err));
}
};
Loading
Loading