From aa1344d294e9ee22a59ed891e15e8139e36c935f Mon Sep 17 00:00:00 2001 From: Cesare Naldi <3353250+cesarenaldi@users.noreply.github.com> Date: Mon, 25 May 2026 11:42:05 +0200 Subject: [PATCH 1/7] feat: support beacon deposit wallets --- README.md | 6 ++-- package.json | 2 ++ pnpm-lock.yaml | 6 ++++ src/builder/derive.ts | 52 ++++++++++++++++++++++++++++++---- src/client.ts | 4 +++ src/config/index.ts | 7 +++-- tests/signatures/index.test.ts | 32 +++++++++++++++++++-- 7 files changed, 97 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e23f52a..8eb5cc2 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ console.log("Proxy redeem completed:", proxyResult.transactionHash); ### Deposit Wallet -Deposit Wallets are UUPS-upgradeable smart contract wallets that support EIP-712 signed batch execution. Unlike Safe and Proxy wallets which use the `execute()` method, Deposit Wallets have dedicated methods. +Deposit Wallets are smart contract wallets that support EIP-712 signed batch execution. Unlike Safe and Proxy wallets which use the `execute()` method, Deposit Wallets have dedicated methods. #### Derive Deposit Wallet Address @@ -329,9 +329,9 @@ console.log("Expected deposit wallet address:", walletAddress); Or use the standalone function directly: ```typescript -import { deriveDepositWallet } from "@polymarket/builder-relayer-client"; +import { deriveBeaconDepositWallet } from "@polymarket/builder-relayer-client"; -const walletAddress = deriveDepositWallet(ownerAddress, factoryAddress, implementationAddress); +const walletAddress = deriveBeaconDepositWallet(ownerAddress, factoryAddress, beaconAddress); ``` #### Deploy Deposit Wallet diff --git a/package.json b/package.json index b5257a1..9419cd3 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "ws": "^8.11.0" }, "dependencies": { + "@ethersproject/providers": "5.8.0", + "@ethersproject/wallet": "5.8.0", "@polymarket/builder-abstract-signer": "0.0.1", "@polymarket/builder-signing-sdk": "^0.0.8", "axios": "^0.27.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79f6c6b..34d502c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@ethersproject/providers': + specifier: 5.8.0 + version: 5.8.0 + '@ethersproject/wallet': + specifier: 5.8.0 + version: 5.8.0 '@polymarket/builder-abstract-signer': specifier: 0.0.1 version: 0.0.1 diff --git a/src/builder/derive.ts b/src/builder/derive.ts index ffbb8e9..8bc6f60 100644 --- a/src/builder/derive.ts +++ b/src/builder/derive.ts @@ -24,6 +24,10 @@ export const deriveSafe = (address: string, safeFactory: string) : string => { const ERC1967_CONST1: Hex = "0xcc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3"; const ERC1967_CONST2: Hex = "0x5155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076"; const ERC1967_PREFIX = 0x61003d3d8160233d3973n; +const ERC1967_BEACON_CONST1: Hex = "0xb3582b35133d50545afa5036515af43d6000803e604d573d6000fd5b3d6000f3"; +const ERC1967_BEACON_CONST2: Hex = "0x1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6c"; +const ERC1967_BEACON_CONST3: Hex = "0x60195155f3363d3d373d3d363d602036600436635c60da"; +const ERC1967_BEACON_PREFIX = 0x6100523d8160233d3973n; /** * Replicates Solady LibClone.initCodeHashERC1967(implementation, args). @@ -45,6 +49,33 @@ function initCodeHashERC1967(implementation: Address, args: Hex): Hex { ); } +/** + * Replicates Solady LibClone.initCodeHashERC1967Beacon(beacon, args). + */ +function initCodeHashERC1967Beacon(beacon: Address, args: Hex): Hex { + const n = BigInt((args.length - 2) / 2); + const combined = ERC1967_BEACON_PREFIX + (n << 56n); + + return keccak256( + concat([ + toHex(combined, { size: 10 }), + beacon as Hex, + ERC1967_BEACON_CONST3, + ERC1967_BEACON_CONST2, + ERC1967_BEACON_CONST1, + args, + ]), + ); +} + +function depositWalletArgs(owner: string, factory: string): Hex { + const walletId = pad(owner as Hex, { dir: "left", size: 32 }); + return encodeAbiParameters( + [{ type: "address" }, { type: "bytes32" }], + [factory as Address, walletId], + ); +} + /** * Computes the deterministic deposit wallet address for a given owner. * walletId is derived as bytes32(owner) - the 20-byte address left-padded to 32 bytes. @@ -54,13 +85,24 @@ export const deriveDepositWallet = ( factory: string, implementation: string, ): string => { - const walletId = pad(owner as Hex, { dir: "left", size: 32 }); - const args = encodeAbiParameters( - [{ type: "address" }, { type: "bytes32" }], - [factory as Address, walletId], - ); + const args = depositWalletArgs(owner, factory); const salt = keccak256(args); const bytecodeHash = initCodeHashERC1967(implementation as Address, args); return getCreate2Address({ from: factory as Hex, salt, bytecodeHash }); }; + +/** + * Computes the deterministic beacon deposit wallet address for a given owner. + */ +export const deriveBeaconDepositWallet = ( + owner: string, + factory: string, + beacon: string, +): string => { + const args = depositWalletArgs(owner, factory); + const salt = keccak256(args); + const bytecodeHash = initCodeHashERC1967Beacon(beacon as Address, args); + + return getCreate2Address({ from: factory as Hex, salt, bytecodeHash }); +}; diff --git a/src/client.ts b/src/client.ts index 32b8603..5fcbf01 100644 --- a/src/client.ts +++ b/src/client.ts @@ -42,6 +42,7 @@ import { buildDepositWalletBatchRequest, buildDepositWalletCreateRequest, deriveSafe, + deriveBeaconDepositWallet, deriveDepositWallet, } from "./builder"; import { sleep } from "./utils"; @@ -384,6 +385,9 @@ export class RelayClient { throw CONFIG_UNSUPPORTED_ON_CHAIN; } const address = await (this.signer as IAbstractSigner).getAddress(); + if (config.DepositWalletBeacon) { + return deriveBeaconDepositWallet(address, config.DepositWalletFactory, config.DepositWalletBeacon); + } return deriveDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation); } diff --git a/src/config/index.ts b/src/config/index.ts index deb6222..c874e82 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -11,6 +11,7 @@ export interface SafeContractConfig { export interface DepositWalletContractConfig { DepositWalletFactory: string; DepositWalletImplementation: string; + DepositWalletBeacon: string; } export interface ContractConfig { @@ -32,6 +33,7 @@ const AMOY: ContractConfig = { DepositWalletContracts: { DepositWalletFactory: "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07", DepositWalletImplementation: "0x50a88fE9a441cB4c9c2aD6A2207CE2795C7D7Fbd", + DepositWalletBeacon: "", }, }; @@ -47,6 +49,7 @@ const POL: ContractConfig = { DepositWalletContracts: { DepositWalletFactory: "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07", DepositWalletImplementation: "0x58CA52ebe0DadfdF531Cde7062e76746de4Db1eB", + DepositWalletBeacon: "0x7A18EDfe055488A3128f01F563e5B479D92ffc3a", }, }; @@ -65,7 +68,7 @@ export function isSafeContractConfigValid( export function isDepositWalletContractConfigValid( config: DepositWalletContractConfig ): boolean { - return !!config.DepositWalletFactory && !!config.DepositWalletImplementation; + return !!config.DepositWalletFactory && (!!config.DepositWalletImplementation || !!config.DepositWalletBeacon); } export const getContractConfig = (chainId: number): ContractConfig => { @@ -77,4 +80,4 @@ export const getContractConfig = (chainId: number): ContractConfig => { default: throw new Error("Invalid network"); } -}; \ No newline at end of file +}; diff --git a/tests/signatures/index.test.ts b/tests/signatures/index.test.ts index d3fada8..833019a 100644 --- a/tests/signatures/index.test.ts +++ b/tests/signatures/index.test.ts @@ -8,7 +8,13 @@ import { createWalletClient, http, WalletClient, zeroAddress } from "viem"; import { polygon } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; import { encodeProxyTransactionData } from "../../src/encode"; -import { buildProxyTransactionRequest, buildSafeCreateTransactionRequest, buildSafeTransactionRequest } from "../../src/builder"; +import { + buildProxyTransactionRequest, + buildSafeCreateTransactionRequest, + buildSafeTransactionRequest, + deriveBeaconDepositWallet, + deriveDepositWallet, +} from "../../src/builder"; import { CallType, OperationType, @@ -160,4 +166,26 @@ describe("setup", () => { }); }); -}); \ No newline at end of file + + describe("derive deposit wallet address", () => { + it("derives the legacy UUPS deposit wallet address", () => { + const wallet = deriveDepositWallet( + "0x0000000000000000000000000000000000000001", + contractConfig.DepositWalletContracts.DepositWalletFactory, + contractConfig.DepositWalletContracts.DepositWalletImplementation, + ); + + expect(wallet.toLowerCase()).equal("0x57ffbc34de23124faeb8387fcd689d314e57accd"); + }); + + it("derives the beacon deposit wallet address", () => { + const wallet = deriveBeaconDepositWallet( + "0x0000000000000000000000000000000000000001", + contractConfig.DepositWalletContracts.DepositWalletFactory, + contractConfig.DepositWalletContracts.DepositWalletBeacon, + ); + + expect(wallet.toLowerCase()).equal("0x94bf330955a0b957662feaf878de77bf25f76cd9"); + }); + }); +}); From 2e7ad2582540ab90665b6f7fa11ec833f1c58363 Mon Sep 17 00:00:00 2001 From: Cesare Naldi <3353250+cesarenaldi@users.noreply.github.com> Date: Mon, 25 May 2026 12:08:14 +0200 Subject: [PATCH 2/7] fix: detect beacon deposit wallet factory --- src/client.ts | 71 ++++++++++++++++++++++++++++++++-- src/config/index.ts | 5 +-- tests/signatures/index.test.ts | 36 ++++++++++++++++- 3 files changed, 104 insertions(+), 8 deletions(-) diff --git a/src/client.ts b/src/client.ts index 5fcbf01..166f0bc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,17 @@ import { Wallet } from "@ethersproject/wallet"; import { JsonRpcSigner } from "@ethersproject/providers"; -import { WalletClient, zeroAddress } from "viem"; +import { + BaseError, + ContractFunctionRevertedError, + createPublicClient, + ExecutionRevertedError, + http, + RawContractError, + type PublicClient, + WalletClient, + zeroAddress, +} from "viem"; +import { polygon, polygonAmoy } from "viem/chains"; import { createAbstractSigner, IAbstractSigner } from "@polymarket/builder-abstract-signer"; import { GET, @@ -52,6 +63,38 @@ import { BuilderConfig, BuilderHeaderPayload } from "@polymarket/builder-signing import { CONFIG_UNSUPPORTED_ON_CHAIN, SAFE_DEPLOYED, SAFE_NOT_DEPLOYED, SIGNER_UNAVAILABLE } from "./errors"; import { encodeProxyTransactionData } from "./encode"; +const FACTORY_BEACON_SELECTOR = "0x49493a4d"; + +function getViemChain(chainId: number) { + switch (chainId) { + case 137: + return polygon; + case 80002: + return polygonAmoy; + default: + throw new Error("Invalid network"); + } +} + +function decodeAddressReturnData(data?: string): string { + if (data === undefined || data.length < 66) { + return zeroAddress; + } + return `0x${data.slice(-40)}`; +} + +function isContractRevert(error: unknown): boolean { + if (!(error instanceof BaseError)) { + return false; + } + + return error.walk((err) => ( + err instanceof ContractFunctionRevertedError || + err instanceof ExecutionRevertedError || + (err instanceof RawContractError && err.code === 3) + )) !== null; +} + export class RelayClient { readonly relayerUrl: string; @@ -64,6 +107,8 @@ export class RelayClient { readonly httpClient: HttpClient; + readonly publicClient: PublicClient; + readonly signer?: IAbstractSigner; readonly builderConfig?: BuilderConfig; @@ -83,6 +128,10 @@ export class RelayClient { this.relayTxType = relayTxType; this.contractConfig = getContractConfig(chainId); this.httpClient = new HttpClient(); + this.publicClient = createPublicClient({ + chain: getViemChain(chainId), + transport: http(), + }); if (signer != undefined) { this.signer = createAbstractSigner(chainId, signer); @@ -385,12 +434,28 @@ export class RelayClient { throw CONFIG_UNSUPPORTED_ON_CHAIN; } const address = await (this.signer as IAbstractSigner).getAddress(); - if (config.DepositWalletBeacon) { - return deriveBeaconDepositWallet(address, config.DepositWalletFactory, config.DepositWalletBeacon); + const beacon = await this.getDepositWalletFactoryBeacon(config.DepositWalletFactory); + if (beacon.toLowerCase() !== zeroAddress) { + return deriveBeaconDepositWallet(address, config.DepositWalletFactory, beacon); } return deriveDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation); } + private async getDepositWalletFactoryBeacon(factory: string): Promise { + try { + const { data } = await this.publicClient.call({ + to: factory as `0x${string}`, + data: FACTORY_BEACON_SELECTOR, + }); + return decodeAddressReturnData(data); + } catch (error) { + if (isContractRevert(error)) { + return zeroAddress; + } + throw error; + } + } + /** * Periodically polls the transaction id until it reaches a desired state * Returns the relayer transaction if it does each the desired state diff --git a/src/config/index.ts b/src/config/index.ts index c874e82..18ebf7f 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -11,7 +11,6 @@ export interface SafeContractConfig { export interface DepositWalletContractConfig { DepositWalletFactory: string; DepositWalletImplementation: string; - DepositWalletBeacon: string; } export interface ContractConfig { @@ -33,7 +32,6 @@ const AMOY: ContractConfig = { DepositWalletContracts: { DepositWalletFactory: "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07", DepositWalletImplementation: "0x50a88fE9a441cB4c9c2aD6A2207CE2795C7D7Fbd", - DepositWalletBeacon: "", }, }; @@ -49,7 +47,6 @@ const POL: ContractConfig = { DepositWalletContracts: { DepositWalletFactory: "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07", DepositWalletImplementation: "0x58CA52ebe0DadfdF531Cde7062e76746de4Db1eB", - DepositWalletBeacon: "0x7A18EDfe055488A3128f01F563e5B479D92ffc3a", }, }; @@ -68,7 +65,7 @@ export function isSafeContractConfigValid( export function isDepositWalletContractConfigValid( config: DepositWalletContractConfig ): boolean { - return !!config.DepositWalletFactory && (!!config.DepositWalletImplementation || !!config.DepositWalletBeacon); + return !!config.DepositWalletFactory && !!config.DepositWalletImplementation; } export const getContractConfig = (chainId: number): ContractConfig => { diff --git a/tests/signatures/index.test.ts b/tests/signatures/index.test.ts index 833019a..2a54d15 100644 --- a/tests/signatures/index.test.ts +++ b/tests/signatures/index.test.ts @@ -8,6 +8,7 @@ import { createWalletClient, http, WalletClient, zeroAddress } from "viem"; import { polygon } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; import { encodeProxyTransactionData } from "../../src/encode"; +import { RelayClient } from "../../src/client"; import { buildProxyTransactionRequest, buildSafeCreateTransactionRequest, @@ -43,6 +44,7 @@ describe("setup", () => { // Calldata to approve CTF as spender on USDC const usdc = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; const approveCalldata = "0x095ea7b30000000000000000000000004d97dcd97ec945f40cf65f87097ace5ea0476045ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + const depositWalletBeacon = "0x7A18EDfe055488A3128f01F563e5B479D92ffc3a"; // ethers signer const w = new Wallet(privateKey); @@ -182,10 +184,42 @@ describe("setup", () => { const wallet = deriveBeaconDepositWallet( "0x0000000000000000000000000000000000000001", contractConfig.DepositWalletContracts.DepositWalletFactory, - contractConfig.DepositWalletContracts.DepositWalletBeacon, + depositWalletBeacon, ); expect(wallet.toLowerCase()).equal("0x94bf330955a0b957662feaf878de77bf25f76cd9"); }); + + it("uses factory beacon detection for the client expected address", async () => { + const client = new RelayClient("http://localhost:8080", chainId, ethersWallet); + (client as unknown as { publicClient: { call: () => Promise<{ data: string }> } }).publicClient = { + call: async () => ({ data: `0x000000000000000000000000${depositWalletBeacon.slice(2)}` }), + }; + + const wallet = await client.deriveDepositWalletAddress(); + const expectedWallet = deriveBeaconDepositWallet( + address, + contractConfig.DepositWalletContracts.DepositWalletFactory, + depositWalletBeacon, + ); + + expect(wallet).equal(expectedWallet); + }); + + it("falls back to the legacy UUPS address when the factory has no beacon", async () => { + const client = new RelayClient("http://localhost:8080", chainId, ethersWallet); + (client as unknown as { publicClient: { call: () => Promise<{ data: string }> } }).publicClient = { + call: async () => ({ data: `0x000000000000000000000000${zeroAddress.slice(2)}` }), + }; + + const wallet = await client.deriveDepositWalletAddress(); + const expectedWallet = deriveDepositWallet( + address, + contractConfig.DepositWalletContracts.DepositWalletFactory, + contractConfig.DepositWalletContracts.DepositWalletImplementation, + ); + + expect(wallet).equal(expectedWallet); + }); }); }); From 539dbf90d0d33c1c55f74b1b41c92c6954fb3e95 Mon Sep 17 00:00:00 2001 From: Cesare Naldi <3353250+cesarenaldi@users.noreply.github.com> Date: Mon, 25 May 2026 12:16:28 +0200 Subject: [PATCH 3/7] feat: allow custom viem chain option --- src/client.ts | 12 +++++++++++- tests/signatures/index.test.ts | 13 ++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index 166f0bc..f164d0f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -7,6 +7,7 @@ import { ExecutionRevertedError, http, RawContractError, + type Chain, type PublicClient, WalletClient, zeroAddress, @@ -95,6 +96,10 @@ function isContractRevert(error: unknown): boolean { )) !== null; } +export interface RelayClientOptions { + chain?: Chain; +} + export class RelayClient { readonly relayerUrl: string; @@ -119,6 +124,7 @@ export class RelayClient { signer?: Wallet | JsonRpcSigner | WalletClient, builderConfig?: BuilderConfig, relayTxType?: RelayerTxType, + options?: RelayClientOptions, ) { this.relayerUrl = relayerUrl.endsWith("/") ? relayerUrl.slice(0, -1) : relayerUrl; this.chainId = chainId; @@ -128,8 +134,12 @@ export class RelayClient { this.relayTxType = relayTxType; this.contractConfig = getContractConfig(chainId); this.httpClient = new HttpClient(); + const chain = options?.chain ?? getViemChain(chainId); + if (chain.id !== chainId) { + throw new Error("chain id does not match chainId"); + } this.publicClient = createPublicClient({ - chain: getViemChain(chainId), + chain, transport: http(), }); diff --git a/tests/signatures/index.test.ts b/tests/signatures/index.test.ts index 2a54d15..582f49c 100644 --- a/tests/signatures/index.test.ts +++ b/tests/signatures/index.test.ts @@ -5,7 +5,7 @@ import { Wallet } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; import { createWalletClient, http, WalletClient, zeroAddress } from "viem"; -import { polygon } from "viem/chains"; +import { polygon, polygonAmoy } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; import { encodeProxyTransactionData } from "../../src/encode"; import { RelayClient } from "../../src/client"; @@ -221,5 +221,16 @@ describe("setup", () => { expect(wallet).equal(expectedWallet); }); + + it("rejects an options chain that does not match the chain id", () => { + expect(() => new RelayClient( + "http://localhost:8080", + chainId, + ethersWallet, + undefined, + undefined, + { chain: polygonAmoy }, + )).to.throw("chain id does not match chainId"); + }); }); }); From 2ef087dfff87515ed6129b2047b0421c3172bdda Mon Sep 17 00:00:00 2001 From: Cesare Naldi <3353250+cesarenaldi@users.noreply.github.com> Date: Mon, 25 May 2026 12:44:03 +0200 Subject: [PATCH 4/7] fix: prefer deployed legacy deposit wallet --- src/client.ts | 30 ++++++++++++++---------------- tests/signatures/index.test.ts | 29 ++++++++++++++++++----------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/client.ts b/src/client.ts index f164d0f..9214f0d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -7,7 +7,6 @@ import { ExecutionRevertedError, http, RawContractError, - type Chain, type PublicClient, WalletClient, zeroAddress, @@ -96,11 +95,6 @@ function isContractRevert(error: unknown): boolean { )) !== null; } -export interface RelayClientOptions { - chain?: Chain; -} - - export class RelayClient { readonly relayerUrl: string; @@ -112,7 +106,7 @@ export class RelayClient { readonly httpClient: HttpClient; - readonly publicClient: PublicClient; + private readonly publicClient: PublicClient; readonly signer?: IAbstractSigner; @@ -124,7 +118,6 @@ export class RelayClient { signer?: Wallet | JsonRpcSigner | WalletClient, builderConfig?: BuilderConfig, relayTxType?: RelayerTxType, - options?: RelayClientOptions, ) { this.relayerUrl = relayerUrl.endsWith("/") ? relayerUrl.slice(0, -1) : relayerUrl; this.chainId = chainId; @@ -134,12 +127,8 @@ export class RelayClient { this.relayTxType = relayTxType; this.contractConfig = getContractConfig(chainId); this.httpClient = new HttpClient(); - const chain = options?.chain ?? getViemChain(chainId); - if (chain.id !== chainId) { - throw new Error("chain id does not match chainId"); - } this.publicClient = createPublicClient({ - chain, + chain: getViemChain(chainId), transport: http(), }); @@ -444,11 +433,20 @@ export class RelayClient { throw CONFIG_UNSUPPORTED_ON_CHAIN; } const address = await (this.signer as IAbstractSigner).getAddress(); + const legacyAddress = deriveDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation); const beacon = await this.getDepositWalletFactoryBeacon(config.DepositWalletFactory); - if (beacon.toLowerCase() !== zeroAddress) { - return deriveBeaconDepositWallet(address, config.DepositWalletFactory, beacon); + if (beacon.toLowerCase() === zeroAddress) { + return legacyAddress; } - return deriveDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation); + if (await this.isContractDeployed(legacyAddress)) { + return legacyAddress; + } + return deriveBeaconDepositWallet(address, config.DepositWalletFactory, beacon); + } + + private async isContractDeployed(address: string): Promise { + const code = await this.publicClient.getCode({ address: address as `0x${string}` }); + return code !== undefined && code !== "0x"; } private async getDepositWalletFactoryBeacon(factory: string): Promise { diff --git a/tests/signatures/index.test.ts b/tests/signatures/index.test.ts index 582f49c..b7185e9 100644 --- a/tests/signatures/index.test.ts +++ b/tests/signatures/index.test.ts @@ -5,7 +5,7 @@ import { Wallet } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; import { createWalletClient, http, WalletClient, zeroAddress } from "viem"; -import { polygon, polygonAmoy } from "viem/chains"; +import { polygon } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; import { encodeProxyTransactionData } from "../../src/encode"; import { RelayClient } from "../../src/client"; @@ -192,8 +192,9 @@ describe("setup", () => { it("uses factory beacon detection for the client expected address", async () => { const client = new RelayClient("http://localhost:8080", chainId, ethersWallet); - (client as unknown as { publicClient: { call: () => Promise<{ data: string }> } }).publicClient = { + (client as unknown as { publicClient: { call: () => Promise<{ data: string }>; getCode: () => Promise } }).publicClient = { call: async () => ({ data: `0x000000000000000000000000${depositWalletBeacon.slice(2)}` }), + getCode: async () => undefined, }; const wallet = await client.deriveDepositWalletAddress(); @@ -222,15 +223,21 @@ describe("setup", () => { expect(wallet).equal(expectedWallet); }); - it("rejects an options chain that does not match the chain id", () => { - expect(() => new RelayClient( - "http://localhost:8080", - chainId, - ethersWallet, - undefined, - undefined, - { chain: polygonAmoy }, - )).to.throw("chain id does not match chainId"); + it("returns the legacy UUPS address when it is already deployed", async () => { + const client = new RelayClient("http://localhost:8080", chainId, ethersWallet); + (client as unknown as { publicClient: { call: () => Promise<{ data: string }>; getCode: () => Promise } }).publicClient = { + call: async () => ({ data: `0x000000000000000000000000${depositWalletBeacon.slice(2)}` }), + getCode: async () => "0x01", + }; + + const wallet = await client.deriveDepositWalletAddress(); + const expectedWallet = deriveDepositWallet( + address, + contractConfig.DepositWalletContracts.DepositWalletFactory, + contractConfig.DepositWalletContracts.DepositWalletImplementation, + ); + + expect(wallet).equal(expectedWallet); }); }); }); From a74e346ad66da2145143c3ff439c4c81d69b2d14 Mon Sep 17 00:00:00 2001 From: Cesare Naldi <3353250+cesarenaldi@users.noreply.github.com> Date: Mon, 25 May 2026 13:16:01 +0200 Subject: [PATCH 5/7] fix: keep beacon derivation internal --- README.md | 10 ++-------- src/builder/derive.ts | 1 + src/builder/index.ts | 2 +- src/client.ts | 27 +++++++++++++-------------- tests/signatures/index.test.ts | 15 +++++++++++++-- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 8eb5cc2..547ffc5 100644 --- a/README.md +++ b/README.md @@ -317,7 +317,7 @@ Deposit Wallets are smart contract wallets that support EIP-712 signed batch exe #### Derive Deposit Wallet Address -You can predict the deposit wallet address before deployment using CREATE2: +You can predict the deposit wallet address before deployment: ```typescript const client = new RelayClient(relayerUrl, chainId, wallet, builderConfig); @@ -326,13 +326,7 @@ const walletAddress = await client.deriveDepositWalletAddress(); console.log("Expected deposit wallet address:", walletAddress); ``` -Or use the standalone function directly: - -```typescript -import { deriveBeaconDepositWallet } from "@polymarket/builder-relayer-client"; - -const walletAddress = deriveBeaconDepositWallet(ownerAddress, factoryAddress, beaconAddress); -``` +The standalone `deriveDepositWallet()` helper only derives legacy UUPS deposit wallet addresses and is deprecated. Prefer `client.deriveDepositWalletAddress()`. #### Deploy Deposit Wallet diff --git a/src/builder/derive.ts b/src/builder/derive.ts index 8bc6f60..c5154f7 100644 --- a/src/builder/derive.ts +++ b/src/builder/derive.ts @@ -79,6 +79,7 @@ function depositWalletArgs(owner: string, factory: string): Hex { /** * Computes the deterministic deposit wallet address for a given owner. * walletId is derived as bytes32(owner) - the 20-byte address left-padded to 32 bytes. + * @deprecated Use RelayClient.deriveDepositWalletAddress(). This helper only derives legacy UUPS deposit wallet addresses. */ export const deriveDepositWallet = ( owner: string, diff --git a/src/builder/index.ts b/src/builder/index.ts index 3b0ee19..113875e 100644 --- a/src/builder/index.ts +++ b/src/builder/index.ts @@ -1,5 +1,5 @@ export * from "./safe"; export * from "./create"; -export * from "./derive"; +export { deriveDepositWallet, deriveProxyWallet, deriveSafe } from "./derive"; export * from "./proxy"; export * from "./deposit-wallet"; diff --git a/src/client.ts b/src/client.ts index 9214f0d..a61c631 100644 --- a/src/client.ts +++ b/src/client.ts @@ -7,11 +7,12 @@ import { ExecutionRevertedError, http, RawContractError, + type Chain, type PublicClient, WalletClient, zeroAddress, } from "viem"; -import { polygon, polygonAmoy } from "viem/chains"; +import { polygon } from "viem/chains"; import { createAbstractSigner, IAbstractSigner } from "@polymarket/builder-abstract-signer"; import { GET, @@ -53,9 +54,9 @@ import { buildDepositWalletBatchRequest, buildDepositWalletCreateRequest, deriveSafe, - deriveBeaconDepositWallet, deriveDepositWallet, } from "./builder"; +import { deriveBeaconDepositWallet } from "./builder/derive"; import { sleep } from "./utils"; import { ClientRelayerTransactionResponse } from "./response"; import { ContractConfig, getContractConfig, isProxyContractConfigValid, isSafeContractConfigValid, isDepositWalletContractConfigValid } from "./config"; @@ -65,17 +66,6 @@ import { encodeProxyTransactionData } from "./encode"; const FACTORY_BEACON_SELECTOR = "0x49493a4d"; -function getViemChain(chainId: number) { - switch (chainId) { - case 137: - return polygon; - case 80002: - return polygonAmoy; - default: - throw new Error("Invalid network"); - } -} - function decodeAddressReturnData(data?: string): string { if (data === undefined || data.length < 66) { return zeroAddress; @@ -95,6 +85,10 @@ function isContractRevert(error: unknown): boolean { )) !== null; } +export interface RelayClientOptions { + chain?: Chain; +} + export class RelayClient { readonly relayerUrl: string; @@ -118,6 +112,7 @@ export class RelayClient { signer?: Wallet | JsonRpcSigner | WalletClient, builderConfig?: BuilderConfig, relayTxType?: RelayerTxType, + options?: RelayClientOptions, ) { this.relayerUrl = relayerUrl.endsWith("/") ? relayerUrl.slice(0, -1) : relayerUrl; this.chainId = chainId; @@ -127,8 +122,12 @@ export class RelayClient { this.relayTxType = relayTxType; this.contractConfig = getContractConfig(chainId); this.httpClient = new HttpClient(); + const chain = options?.chain ?? polygon; + if (chain.id !== chainId) { + throw new Error("chain id does not match chainId"); + } this.publicClient = createPublicClient({ - chain: getViemChain(chainId), + chain, transport: http(), }); diff --git a/tests/signatures/index.test.ts b/tests/signatures/index.test.ts index b7185e9..36626e1 100644 --- a/tests/signatures/index.test.ts +++ b/tests/signatures/index.test.ts @@ -5,7 +5,7 @@ import { Wallet } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; import { createWalletClient, http, WalletClient, zeroAddress } from "viem"; -import { polygon } from "viem/chains"; +import { polygon, polygonAmoy } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; import { encodeProxyTransactionData } from "../../src/encode"; import { RelayClient } from "../../src/client"; @@ -13,9 +13,9 @@ import { buildProxyTransactionRequest, buildSafeCreateTransactionRequest, buildSafeTransactionRequest, - deriveBeaconDepositWallet, deriveDepositWallet, } from "../../src/builder"; +import { deriveBeaconDepositWallet } from "../../src/builder/derive"; import { CallType, OperationType, @@ -239,5 +239,16 @@ describe("setup", () => { expect(wallet).equal(expectedWallet); }); + + it("rejects an options chain that does not match the chain id", () => { + expect(() => new RelayClient( + "http://localhost:8080", + chainId, + ethersWallet, + undefined, + undefined, + { chain: polygonAmoy }, + )).to.throw("chain id does not match chainId"); + }); }); }); From e6bb23a2a31cf1b2e11798b5a3e2c35ecf6f5b5c Mon Sep 17 00:00:00 2001 From: Cesare Naldi <3353250+cesarenaldi@users.noreply.github.com> Date: Mon, 25 May 2026 13:21:04 +0200 Subject: [PATCH 6/7] refactor: clarify UUPS deposit wallet derivation --- src/builder/derive.ts | 15 ++++++++++++--- src/client.ts | 5 ++--- tests/signatures/index.test.ts | 9 ++++----- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/builder/derive.ts b/src/builder/derive.ts index c5154f7..f7c082a 100644 --- a/src/builder/derive.ts +++ b/src/builder/derive.ts @@ -77,11 +77,10 @@ function depositWalletArgs(owner: string, factory: string): Hex { } /** - * Computes the deterministic deposit wallet address for a given owner. + * Computes the deterministic legacy UUPS deposit wallet address for a given owner. * walletId is derived as bytes32(owner) - the 20-byte address left-padded to 32 bytes. - * @deprecated Use RelayClient.deriveDepositWalletAddress(). This helper only derives legacy UUPS deposit wallet addresses. */ -export const deriveDepositWallet = ( +export const deriveUupsDepositWallet = ( owner: string, factory: string, implementation: string, @@ -93,6 +92,16 @@ export const deriveDepositWallet = ( return getCreate2Address({ from: factory as Hex, salt, bytecodeHash }); }; +/** + * Computes the deterministic legacy UUPS deposit wallet address for a given owner. + * @deprecated Use RelayClient.deriveDepositWalletAddress(). This helper only derives legacy UUPS deposit wallet addresses. + */ +export const deriveDepositWallet = ( + owner: string, + factory: string, + implementation: string, +): string => deriveUupsDepositWallet(owner, factory, implementation); + /** * Computes the deterministic beacon deposit wallet address for a given owner. */ diff --git a/src/client.ts b/src/client.ts index a61c631..6b5cfff 100644 --- a/src/client.ts +++ b/src/client.ts @@ -54,9 +54,8 @@ import { buildDepositWalletBatchRequest, buildDepositWalletCreateRequest, deriveSafe, - deriveDepositWallet, } from "./builder"; -import { deriveBeaconDepositWallet } from "./builder/derive"; +import { deriveBeaconDepositWallet, deriveUupsDepositWallet } from "./builder/derive"; import { sleep } from "./utils"; import { ClientRelayerTransactionResponse } from "./response"; import { ContractConfig, getContractConfig, isProxyContractConfigValid, isSafeContractConfigValid, isDepositWalletContractConfigValid } from "./config"; @@ -432,7 +431,7 @@ export class RelayClient { throw CONFIG_UNSUPPORTED_ON_CHAIN; } const address = await (this.signer as IAbstractSigner).getAddress(); - const legacyAddress = deriveDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation); + const legacyAddress = deriveUupsDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation); const beacon = await this.getDepositWalletFactoryBeacon(config.DepositWalletFactory); if (beacon.toLowerCase() === zeroAddress) { return legacyAddress; diff --git a/tests/signatures/index.test.ts b/tests/signatures/index.test.ts index 36626e1..28bf8e5 100644 --- a/tests/signatures/index.test.ts +++ b/tests/signatures/index.test.ts @@ -13,9 +13,8 @@ import { buildProxyTransactionRequest, buildSafeCreateTransactionRequest, buildSafeTransactionRequest, - deriveDepositWallet, } from "../../src/builder"; -import { deriveBeaconDepositWallet } from "../../src/builder/derive"; +import { deriveBeaconDepositWallet, deriveUupsDepositWallet } from "../../src/builder/derive"; import { CallType, OperationType, @@ -171,7 +170,7 @@ describe("setup", () => { describe("derive deposit wallet address", () => { it("derives the legacy UUPS deposit wallet address", () => { - const wallet = deriveDepositWallet( + const wallet = deriveUupsDepositWallet( "0x0000000000000000000000000000000000000001", contractConfig.DepositWalletContracts.DepositWalletFactory, contractConfig.DepositWalletContracts.DepositWalletImplementation, @@ -214,7 +213,7 @@ describe("setup", () => { }; const wallet = await client.deriveDepositWalletAddress(); - const expectedWallet = deriveDepositWallet( + const expectedWallet = deriveUupsDepositWallet( address, contractConfig.DepositWalletContracts.DepositWalletFactory, contractConfig.DepositWalletContracts.DepositWalletImplementation, @@ -231,7 +230,7 @@ describe("setup", () => { }; const wallet = await client.deriveDepositWalletAddress(); - const expectedWallet = deriveDepositWallet( + const expectedWallet = deriveUupsDepositWallet( address, contractConfig.DepositWalletContracts.DepositWalletFactory, contractConfig.DepositWalletContracts.DepositWalletImplementation, From ce0b6842af375f5a16546a78048a48aaa826700a Mon Sep 17 00:00:00 2001 From: Cesare Naldi <3353250+cesarenaldi@users.noreply.github.com> Date: Mon, 25 May 2026 13:25:49 +0200 Subject: [PATCH 7/7] docs: avoid legacy deposit wallet wording --- README.md | 2 +- src/builder/derive.ts | 6 +++--- src/client.ts | 8 ++++---- tests/signatures/index.test.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 547ffc5..6e920fa 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ const walletAddress = await client.deriveDepositWalletAddress(); console.log("Expected deposit wallet address:", walletAddress); ``` -The standalone `deriveDepositWallet()` helper only derives legacy UUPS deposit wallet addresses and is deprecated. Prefer `client.deriveDepositWalletAddress()`. +The standalone `deriveDepositWallet()` helper only derives UUPS deposit wallet addresses and is deprecated. Prefer `client.deriveDepositWalletAddress()`. #### Deploy Deposit Wallet diff --git a/src/builder/derive.ts b/src/builder/derive.ts index f7c082a..32ebab0 100644 --- a/src/builder/derive.ts +++ b/src/builder/derive.ts @@ -77,7 +77,7 @@ function depositWalletArgs(owner: string, factory: string): Hex { } /** - * Computes the deterministic legacy UUPS deposit wallet address for a given owner. + * Computes the deterministic UUPS deposit wallet address for a given owner. * walletId is derived as bytes32(owner) - the 20-byte address left-padded to 32 bytes. */ export const deriveUupsDepositWallet = ( @@ -93,8 +93,8 @@ export const deriveUupsDepositWallet = ( }; /** - * Computes the deterministic legacy UUPS deposit wallet address for a given owner. - * @deprecated Use RelayClient.deriveDepositWalletAddress(). This helper only derives legacy UUPS deposit wallet addresses. + * Computes the deterministic UUPS deposit wallet address for a given owner. + * @deprecated Use RelayClient.deriveDepositWalletAddress(). This helper only derives UUPS deposit wallet addresses. */ export const deriveDepositWallet = ( owner: string, diff --git a/src/client.ts b/src/client.ts index 6b5cfff..2ca627d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -431,13 +431,13 @@ export class RelayClient { throw CONFIG_UNSUPPORTED_ON_CHAIN; } const address = await (this.signer as IAbstractSigner).getAddress(); - const legacyAddress = deriveUupsDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation); + const uupsAddress = deriveUupsDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation); const beacon = await this.getDepositWalletFactoryBeacon(config.DepositWalletFactory); if (beacon.toLowerCase() === zeroAddress) { - return legacyAddress; + return uupsAddress; } - if (await this.isContractDeployed(legacyAddress)) { - return legacyAddress; + if (await this.isContractDeployed(uupsAddress)) { + return uupsAddress; } return deriveBeaconDepositWallet(address, config.DepositWalletFactory, beacon); } diff --git a/tests/signatures/index.test.ts b/tests/signatures/index.test.ts index 28bf8e5..8622bd7 100644 --- a/tests/signatures/index.test.ts +++ b/tests/signatures/index.test.ts @@ -169,7 +169,7 @@ describe("setup", () => { }); describe("derive deposit wallet address", () => { - it("derives the legacy UUPS deposit wallet address", () => { + it("derives the UUPS deposit wallet address", () => { const wallet = deriveUupsDepositWallet( "0x0000000000000000000000000000000000000001", contractConfig.DepositWalletContracts.DepositWalletFactory, @@ -206,7 +206,7 @@ describe("setup", () => { expect(wallet).equal(expectedWallet); }); - it("falls back to the legacy UUPS address when the factory has no beacon", async () => { + it("falls back to the UUPS address when the factory has no beacon", async () => { const client = new RelayClient("http://localhost:8080", chainId, ethersWallet); (client as unknown as { publicClient: { call: () => Promise<{ data: string }> } }).publicClient = { call: async () => ({ data: `0x000000000000000000000000${zeroAddress.slice(2)}` }), @@ -222,7 +222,7 @@ describe("setup", () => { expect(wallet).equal(expectedWallet); }); - it("returns the legacy UUPS address when it is already deployed", async () => { + it("returns the UUPS address when it is already deployed", async () => { const client = new RelayClient("http://localhost:8080", chainId, ethersWallet); (client as unknown as { publicClient: { call: () => Promise<{ data: string }>; getCode: () => Promise } }).publicClient = { call: async () => ({ data: `0x000000000000000000000000${depositWalletBeacon.slice(2)}` }),