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
9 changes: 5 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,12 @@
"@tradetrust-tt/tradetrust": "^6.10.3",
"@tradetrust-tt/tt-verify": "^9.7.5",
"@trustvc/document-store": "^1.0.3",
"@trustvc/eip7702": "^1.1.0-beta.2",
"@trustvc/w3c": "^2.4.2",
"@trustvc/w3c-context": "^2.4.0",
"@trustvc/w3c-credential-status": "^2.4.0",
"@trustvc/w3c-issuer": "^2.3.0",
"@trustvc/w3c-vc": "^2.4.2",
"@trustvc/eip7702": "^1.0.0-beta.1",
"ethers": "^5.8.0",
"ethersV6": "npm:ethers@^6.14.4",
"js-sha3": "^0.9.3",
Expand Down
201 changes: 199 additions & 2 deletions src/__tests__/eip7702-functions/admin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { vi, describe, beforeEach, it, expect } from 'vitest';
vi.mock('@trustvc/eip7702', () => ({
abis: { platformPaymasterAbi: [] },
constants: {
ChainId: { Sepolia: 11155111 },
contractAddress: { PlatformAccountFactory: {} },
ChainId: { Sepolia: 11155111, Amoy: 80002 },
contractAddress: {
PlatformAccountFactory: { 11155111: '0xfactory', 80002: '0xfactoryAmoy' },
PaymasterImplementation: { 11155111: '0xpaymasterImpl', 80002: '0xpaymasterImplAmoy' },
},
},
}));

Expand All @@ -30,6 +33,9 @@ import {
addAuthorizedCaller,
removeAuthorizedCaller,
setDailyLimit,
stakePaymaster,
fundPaymaster,
delegateUser,
} from '../../eip7702-functions';
import { getEthersContractFromProvider, isV6EthersProvider } from '../../utils/ethers';

Expand Down Expand Up @@ -494,3 +500,194 @@ describe('sendAdminTx error propagation', () => {
);
});
});

// ─── stakePaymaster ───────────────────────────────────────────────────────────

describe('stakePaymaster', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('viem — calls writeContract with addStake, correct args, and value', async () => {
const signer = makeViemSigner();
await stakePaymaster(signer as never, PAYMASTER, 86400, 1000000000000000000n);
expect(signer.writeContract).toHaveBeenCalledWith(
expect.objectContaining({
address: PAYMASTER,
functionName: 'addStake',
args: [86400],
value: 1000000000000000000n,
}),
);
});

it('viem — returns the transaction hash', async () => {
const signer = makeViemSigner();
expect(await stakePaymaster(signer as never, PAYMASTER, 86400, 1n)).toBe(TX_HASH);
});

it('ethers v5 — calls addStake with args and value override', async () => {
const mockContract = makeEthersV5Contract('addStake');
vi.mocked(getEthersContractFromProvider).mockReturnValue(vi.fn(() => mockContract) as never);
vi.mocked(isV6EthersProvider).mockReturnValue(false);
await stakePaymaster(makeEthersV5Signer() as never, PAYMASTER, 86400, 500n);
expect(mockContract.addStake).toHaveBeenCalledWith(86400, { value: 500n });
});

it('ethers v5 — returns the transaction hash', async () => {
setupEthersV5Mock('addStake');
expect(await stakePaymaster(makeEthersV5Signer() as never, PAYMASTER, 86400, 1n)).toBe(TX_HASH);
});

it('ethers v6 — returns tx.hash directly', async () => {
setupEthersV6Mock('addStake');
expect(await stakePaymaster(makeEthersV5Signer() as never, PAYMASTER, 86400, 1n)).toBe(TX_HASH);
});
});

// ─── fundPaymaster ────────────────────────────────────────────────────────────

describe('fundPaymaster', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('viem — calls writeContract with deposit and value', async () => {
const signer = makeViemSigner();
await fundPaymaster(signer as never, PAYMASTER, 2000000000000000000n);
expect(signer.writeContract).toHaveBeenCalledWith(
expect.objectContaining({
address: PAYMASTER,
functionName: 'deposit',
args: [],
value: 2000000000000000000n,
}),
);
});

it('viem — returns the transaction hash', async () => {
const signer = makeViemSigner();
expect(await fundPaymaster(signer as never, PAYMASTER, 1n)).toBe(TX_HASH);
});

it('ethers v5 — calls deposit with value override', async () => {
const mockContract = makeEthersV5Contract('deposit');
vi.mocked(getEthersContractFromProvider).mockReturnValue(vi.fn(() => mockContract) as never);
vi.mocked(isV6EthersProvider).mockReturnValue(false);
await fundPaymaster(makeEthersV5Signer() as never, PAYMASTER, 999n);
expect(mockContract.deposit).toHaveBeenCalledWith({ value: 999n });
});

it('ethers v5 — returns the transaction hash', async () => {
setupEthersV5Mock('deposit');
expect(await fundPaymaster(makeEthersV5Signer() as never, PAYMASTER, 1n)).toBe(TX_HASH);
});

it('ethers v6 — returns tx.hash directly', async () => {
setupEthersV6Mock('deposit');
expect(await fundPaymaster(makeEthersV5Signer() as never, PAYMASTER, 1n)).toBe(TX_HASH);
});
});

// ─── delegateUser ─────────────────────────────────────────────────────────────

const IMPL = '0x5555555555555555555555555555555555555555' as `0x${string}`;
const OWNER_ADDR = '0xowner' as `0x${string}`;
const SIGNED_AUTH = {
contractAddress: IMPL,
chainId: 11155111,
nonce: 1,
r: '0x',
s: '0x',
yParity: 0,
};

const makeOwnerSigner = () => ({
account: { address: OWNER_ADDR },
chain: { id: 11155111 },
signAuthorization: vi.fn(() => Promise.resolve(SIGNED_AUTH)),
sendTransaction: vi.fn(() => Promise.resolve(TX_HASH)),
});

const makePayerSigner = () => ({
account: { address: '0xpayer' as `0x${string}` },
chain: { id: 11155111 },
sendTransaction: vi.fn(() => Promise.resolve(TX_HASH)),
});

describe('delegateUser', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('owner signs authorization with the implementation address', async () => {
const owner = makeOwnerSigner();
await delegateUser(IMPL, owner as never);
expect(owner.signAuthorization).toHaveBeenCalledWith(
expect.objectContaining({ contractAddress: IMPL }),
);
});

it('without payerSigner — signAuthorization uses executor: self', async () => {
const owner = makeOwnerSigner();
await delegateUser(IMPL, owner as never);
expect(owner.signAuthorization).toHaveBeenCalledWith(
expect.objectContaining({ executor: 'self' }),
);
});

it('with payerSigner — signAuthorization does not set executor', async () => {
const owner = makeOwnerSigner();
const payer = makePayerSigner();
await delegateUser(IMPL, owner as never, payer as never);
expect(owner.signAuthorization).toHaveBeenCalledWith(
expect.not.objectContaining({ executor: expect.anything() }),
);
});

it('without payerSigner — owner submits and pays gas', async () => {
const owner = makeOwnerSigner();
await delegateUser(IMPL, owner as never);
expect(owner.sendTransaction).toHaveBeenCalledWith(
expect.objectContaining({ authorizationList: [SIGNED_AUTH], to: OWNER_ADDR, data: '0x' }),
);
});

it('with payerSigner — payer submits the transaction, owner does not', async () => {
const owner = makeOwnerSigner();
const payer = makePayerSigner();
await delegateUser(IMPL, owner as never, payer as never);
expect(payer.sendTransaction).toHaveBeenCalledWith(
expect.objectContaining({ authorizationList: [SIGNED_AUTH], to: OWNER_ADDR, data: '0x' }),
);
expect(owner.sendTransaction).not.toHaveBeenCalled();
});

it('returns the transaction hash', async () => {
const owner = makeOwnerSigner();
expect(await delegateUser(IMPL, owner as never)).toBe(TX_HASH);
});

it('throws if ownerSigner has no account', async () => {
const owner = { ...makeOwnerSigner(), account: undefined as undefined };
await expect(delegateUser(IMPL, owner as never)).rejects.toThrow(
'ownerSigner must have an account',
);
});

it('throws if payerSigner has no account', async () => {
const owner = makeOwnerSigner();
const payer = { ...makePayerSigner(), account: undefined as undefined };
await expect(delegateUser(IMPL, owner as never, payer as never)).rejects.toThrow(
'payerSigner must have an account',
);
});

it('throws if ownerSigner and payerSigner are on different chains', async () => {
const owner = makeOwnerSigner(); // chain id 11155111
const payer = { ...makePayerSigner(), chain: { id: 80002 } };
await expect(delegateUser(IMPL, owner as never, payer as never)).rejects.toThrow(
'chain mismatch',
);
});
});
5 changes: 3 additions & 2 deletions src/__tests__/eip7702-functions/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ vi.mock('@trustvc/eip7702', () => ({
platformAccountFactoryAbi: [],
},
constants: {
ChainId: { Sepolia: 11155111 },
ChainId: { Sepolia: 11155111, Amoy: 80002 },
contractAddress: {
PlatformAccountFactory: { 11155111: '0xfactory' },
PlatformAccountFactory: { 11155111: '0xfactory', 80002: '0xfactoryAmoy' },
PaymasterImplementation: { 11155111: '0xpaymasterImpl', 80002: '0xpaymasterImplAmoy' },
},
},
}));
Expand Down
7 changes: 5 additions & 2 deletions src/__tests__/eip7702-functions/mint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ vi.mock('viem', () => ({
vi.mock('@trustvc/eip7702', () => ({
abis: { platformPaymasterAbi: [] },
constants: {
ChainId: { Sepolia: 11155111 },
contractAddress: { PlatformAccountFactory: {} },
ChainId: { Sepolia: 11155111, Amoy: 80002 },
contractAddress: {
PlatformAccountFactory: { 11155111: '0xfactory', 80002: '0xfactoryAmoy' },
PaymasterImplementation: { 11155111: '0xpaymasterImpl', 80002: '0xpaymasterImplAmoy' },
},
},
}));

Expand Down
16 changes: 14 additions & 2 deletions src/eip7702-functions/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { constants as eip7702Constants } from '@trustvc/eip7702';

export const gaslessConstants = {
// Sepolia
GASLESS_FACTORY_ADDRESS_SEPOLIA: '0x7e9ef6363180baa744eb32ceab367a44f52adc9f' as `0x${string}`,
GASLESS_FACTORY_ADDRESS_SEPOLIA: eip7702Constants.contractAddress.PlatformAccountFactory[
eip7702Constants.ChainId.Sepolia
] as `0x${string}`,
GASLESS_PAYMASTER_IMPL_ADDRESS_SEPOLIA: eip7702Constants.contractAddress.PaymasterImplementation[
eip7702Constants.ChainId.Sepolia
] as `0x${string}`,
GASLESS_EIP7702_IMPL_ADDRESS_SEPOLIA:
'0xa46ec3920ac5fc54f4ba33185a91ae250adf59b8' as `0x${string}`,
TDOC_DEPLOYER_ADDRESS_SEPOLIA: '0x64bc665056dc8be4092e569ed13a7f273be28cd2' as `0x${string}`,

// Amoy
GASLESS_FACTORY_ADDRESS_AMOY: '0xfbe1d336000d567f98ac5318f7c0144501388409' as `0x${string}`,
GASLESS_FACTORY_ADDRESS_AMOY: eip7702Constants.contractAddress.PlatformAccountFactory[
eip7702Constants.ChainId.Amoy
] as `0x${string}`,
GASLESS_PAYMASTER_IMPL_ADDRESS_AMOY: eip7702Constants.contractAddress.PaymasterImplementation[
eip7702Constants.ChainId.Amoy
] as `0x${string}`,
GASLESS_EIP7702_IMPL_ADDRESS_AMOY: '0x044de1d4515a76ed9e431e8ec89e8d600405fd86' as `0x${string}`,
GASLESS_TDOC_DEPLOYER_ADDRESS_AMOY: '0xfcafea839e576967b96ad1fbfb52b5ca26cd1d25' as `0x${string}`,

Expand Down
Loading
Loading