@seapay-ai/erc3009 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,80 +1,286 @@
1
1
  # @seapay-ai/erc3009
2
2
 
3
- TypeScript utilities for ERC-3009 (Transfer With Authorization) EIP-712 signing.
3
+ Simplified TypeScript library for ERC-3009 (TransferWithAuthorization) EIP-712 signing and verification.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @seapay-ai/erc3009
9
- # or
10
8
  pnpm add @seapay-ai/erc3009
11
9
  # or
12
- yarn add @seapay-ai/erc3009
10
+ npm install @seapay-ai/erc3009
13
11
  ```
14
12
 
15
- ## Usage
16
-
17
- ### Basic Example
13
+ ## Quick Start
18
14
 
19
15
  ```typescript
16
+ import {
17
+ buildTypedData,
18
+ buildMessage,
19
+ resolveDomain,
20
+ nowPlusSeconds,
21
+ } from "@seapay-ai/erc3009";
20
22
  import { Wallet } from "ethers";
23
+
24
+ // 1. Build the message
25
+ const message = buildMessage({
26
+ from: wallet.address,
27
+ to: "0xRecipient...",
28
+ value: 1000000n, // 1 USDC (6 decimals)
29
+ validBefore: nowPlusSeconds(300), // Valid for 5 minutes
30
+ });
31
+
32
+ // 2. Build typed data (includes domain resolution from registry)
33
+ const {
34
+ domain,
35
+ types,
36
+ message: msg,
37
+ } = buildTypedData({
38
+ chainId: 8453, // Base mainnet
39
+ token: "USDC",
40
+ message,
41
+ });
42
+
43
+ // 3. Sign with ethers wallet
44
+ const signature = await wallet.signTypedData(domain, types, msg);
45
+ ```
46
+
47
+ ## Core Functions
48
+
49
+ ### 1. Build Message
50
+
51
+ ```typescript
52
+ import { buildMessage, nowPlusSeconds, randomNonce } from "@seapay-ai/erc3009";
53
+
54
+ const message = buildMessage({
55
+ from: "0xSender...",
56
+ to: "0xRecipient...",
57
+ value: 1000000n, // Amount in token's smallest unit
58
+ validAfter: 0n, // Optional, defaults to 0
59
+ validBefore: nowPlusSeconds(300), // Unix timestamp
60
+ nonce: randomNonce(), // Optional, auto-generated if not provided
61
+ });
62
+ ```
63
+
64
+ ### 2. Build Typed Data
65
+
66
+ Automatically resolves token info from the registry:
67
+
68
+ ```typescript
69
+ import { buildTypedData } from "@seapay-ai/erc3009";
70
+
71
+ const typedData = buildTypedData({
72
+ chainId: 8453, // Base mainnet
73
+ token: "USDC", // Symbol from registry
74
+ message,
75
+ });
76
+
77
+ // Returns:
78
+ // {
79
+ // domain: { name, version, chainId, verifyingContract },
80
+ // types: { TransferWithAuthorization: [...] },
81
+ // message: { from, to, value, ... },
82
+ // primaryType: "TransferWithAuthorization"
83
+ // }
84
+ ```
85
+
86
+ For custom tokens not in the registry:
87
+
88
+ ```typescript
89
+ const typedData = buildTypedData({
90
+ chainId: 8453,
91
+ token: "0xCustomTokenAddress",
92
+ message,
93
+ domainOverrides: {
94
+ name: "My Token",
95
+ version: "1",
96
+ verifyingContract: "0xCustomTokenAddress",
97
+ },
98
+ });
99
+ ```
100
+
101
+ ### 3. Resolve Domain
102
+
103
+ Resolve EIP-712 domain from chain ID and token:
104
+
105
+ ```typescript
106
+ import { resolveDomain } from "@seapay-ai/erc3009";
107
+
108
+ // Resolve USDC domain on Base
109
+ const domain = resolveDomain(8453, "USDC");
110
+ // Returns:
111
+ // {
112
+ // name: "USD Coin",
113
+ // version: "2",
114
+ // chainId: 8453,
115
+ // verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
116
+ // }
117
+
118
+ // For custom tokens
119
+ const customDomain = resolveDomain(8453, "0xCustomTokenAddress", {
120
+ name: "My Token",
121
+ version: "1",
122
+ verifyingContract: "0xCustomTokenAddress",
123
+ });
124
+ ```
125
+
126
+ This function is useful when you need just the domain object without building the complete typed data structure. It's used internally by `buildTypedData`.
127
+
128
+ ### 4. Verify Signature
129
+
130
+ ```typescript
131
+ import { verifySignature, recoverSigner } from "@seapay-ai/erc3009";
132
+
133
+ // Verify signature matches expected signer
134
+ const isValid = verifySignature(domain, message, signature, expectedSigner);
135
+
136
+ // Or recover the signer address
137
+ const signer = recoverSigner(domain, message, signature);
138
+ console.log("Signed by:", signer);
139
+ ```
140
+
141
+ ## Registry
142
+
143
+ The package includes a built-in registry of USDC deployments across multiple chains.
144
+
145
+ ### Query the Registry
146
+
147
+ ```typescript
148
+ import { registry, getTokenInfo } from "@seapay-ai/erc3009";
149
+
150
+ // Get token config
151
+ const usdcBase = registry.getToken("USDC", 8453);
152
+ // => { symbol: "USDC", chainId: 8453, address: "0x...", name: "USD Coin", version: "2", decimals: 6 }
153
+
154
+ // Check if token is supported
155
+ if (registry.isSupported("USDC", 8453)) {
156
+ console.log("USDC supported on Base");
157
+ }
158
+
159
+ // List all chains
160
+ const chains = registry.listChains();
161
+
162
+ // List tokens on a chain
163
+ const tokens = registry.listTokensOnChain(8453);
164
+
165
+ // Get chain info
166
+ const baseChain = registry.getChain(8453);
167
+ // => { chainId: 8453, name: "Base", testnet: false }
168
+ ```
169
+
170
+ ### Supported Chains
171
+
172
+ | Chain | Chain ID | Testnet |
173
+ | ---------------- | -------- | ------- |
174
+ | Ethereum | 1 | - |
175
+ | Sepolia | 11155111 | ✅ |
176
+ | Base | 8453 | - |
177
+ | Base Sepolia | 84532 | ✅ |
178
+ | Arbitrum One | 42161 | - |
179
+ | Arbitrum Sepolia | 421614 | ✅ |
180
+ | Optimism | 10 | - |
181
+ | Optimism Sepolia | 11155420 | ✅ |
182
+ | Polygon | 137 | - |
183
+ | Polygon Amoy | 80002 | ✅ |
184
+
185
+ ### Supported Tokens
186
+
187
+ Currently includes **USDC** on all chains above.
188
+
189
+ ### ⚠️ Important: Base Network Domain Names
190
+
191
+ USDC has **different EIP-712 domain names** on Base networks:
192
+
193
+ | Network | Chain ID | Domain Name |
194
+ | ------------ | -------- | ------------ |
195
+ | Base Mainnet | 8453 | `"USD Coin"` |
196
+ | Base Sepolia | 84532 | `"USDC"` |
197
+
198
+ The registry handles this automatically. Always use the registry to ensure correct domain parameters.
199
+
200
+ ## Complete Example
201
+
202
+ ```typescript
21
203
  import {
22
204
  buildTypedData,
23
- buildTypes,
24
- message_5_minutes,
25
- signTransferWithAuthorization,
26
- type EIP712Domain,
27
- type TransferWithAuthorization,
205
+ buildMessage,
206
+ nowPlusSeconds,
207
+ verifySignature,
28
208
  } from "@seapay-ai/erc3009";
209
+ import { Wallet } from "ethers";
29
210
 
30
- // Create a wallet
211
+ // Create wallet
31
212
  const wallet = new Wallet("0x...");
32
213
 
33
- // Define the token domain (EIP-712 domain)
34
- const domain: EIP712Domain = {
35
- name: "USD Coin",
36
- version: "2",
214
+ // Build message
215
+ const message = buildMessage({
216
+ from: wallet.address,
217
+ to: "0xRecipient...",
218
+ value: 1000000n, // 1 USDC
219
+ validBefore: nowPlusSeconds(300), // 5 minutes
220
+ });
221
+
222
+ // Build typed data
223
+ const {
224
+ domain,
225
+ types,
226
+ message: msg,
227
+ } = buildTypedData({
37
228
  chainId: 84532, // Base Sepolia
38
- verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
39
- };
40
-
41
- // Create a transfer authorization message
42
- const message = message_5_minutes(
43
- wallet.address, // from
44
- "0x...", // to
45
- BigInt("1000000") // value (1 USDC if 6 decimals)
46
- );
47
-
48
- // Sign the authorization
49
- const signature = await signTransferWithAuthorization(wallet, domain, message);
50
-
51
- // Or build typed data manually
52
- const typedData = buildTypedData({ domain, message });
53
- const types = buildTypes();
54
- // Use with wallet.signTypedData(typedData.domain, types, typedData.message)
229
+ token: "USDC",
230
+ message,
231
+ });
232
+
233
+ // Sign
234
+ const signature = await wallet.signTypedData(domain, types, msg);
235
+
236
+ // Verify
237
+ const isValid = verifySignature(domain, message, signature, wallet.address);
238
+ console.log("Signature valid:", isValid);
55
239
  ```
56
240
 
57
- ### API Reference
241
+ ## API Reference
58
242
 
59
- #### Types
243
+ ### Build Functions
60
244
 
61
- - `TransferWithAuthorization`: The message structure for ERC-3009 transfers
62
- - `EIP712Domain`: The EIP-712 domain structure
245
+ - **`buildMessage(params)`** - Create TransferWithAuthorization message
246
+ - **`buildTypedData(params)`** - Create complete EIP-712 typed data with domain resolution
63
247
 
64
- #### Functions
248
+ ### Domain Resolution
65
249
 
66
- - `buildDomain(domain: EIP712Domain)`: Converts EIP712Domain to TypedDataDomain
67
- - `buildTypes()`: Returns the EIP-712 types for TransferWithAuthorization
68
- - `buildMessage(message: TransferWithAuthorization)`: Normalizes message values
69
- - `buildTypedData(params)`: Builds complete typed data for signing
70
- - `signTransferWithAuthorization(wallet, domain, message)`: Convenience function to sign
71
- - `message_5_minutes(from, to, value)`: Creates a message valid for 5 minutes
72
- - `USDC_Domain()`: Returns USDC domain for Base Sepolia (example)
250
+ - **`resolveDomain(chainId, token, domainOverrides?)`** - Resolve EIP-712 domain from chain and token
73
251
 
74
- ## Requirements
252
+ ### Verification Functions
75
253
 
76
- - Node.js 18+
77
- - ethers v6
254
+ - **`verifySignature(domain, message, signature, expectedSigner)`** - Verify signature
255
+ - **`recoverSigner(domain, message, signature)`** - Recover signer address
256
+
257
+ ### Registry Functions
258
+
259
+ - **`getTokenInfo(symbol, chainId)`** - Get token configuration
260
+ - **`registry.getToken(symbol, chainId)`** - Get token config
261
+ - **`registry.getChain(chainId)`** - Get chain config
262
+ - **`registry.listChains()`** - List all chains
263
+ - **`registry.listChainIds()`** - List all chain IDs
264
+ - **`registry.isSupported(symbol, chainId)`** - Check if token is supported
265
+ - **`registry.listTokensOnChain(chainId)`** - List tokens on a chain
266
+
267
+ ### Utility Functions
268
+
269
+ - **`randomNonce()`** - Generate random bytes32 nonce
270
+ - **`nowPlusSeconds(seconds)`** - Get Unix timestamp N seconds from now
271
+ - **`nowSeconds()`** - Get current Unix timestamp
272
+
273
+ ## TypeScript Types
274
+
275
+ ```typescript
276
+ import type {
277
+ TransferWithAuthorization,
278
+ EIP712Domain,
279
+ TypedData,
280
+ TokenConfig,
281
+ ChainConfig,
282
+ } from "@seapay-ai/erc3009";
283
+ ```
78
284
 
79
285
  ## License
80
286
 
@@ -0,0 +1,27 @@
1
+ import type { TransferWithAuthorization, EIP712Domain, TypedData } from "./types/index.js";
2
+ /**
3
+ * Build TransferWithAuthorization message
4
+ */
5
+ export declare function buildMessage(params: {
6
+ from: string;
7
+ to: string;
8
+ value: bigint;
9
+ validAfter?: bigint;
10
+ validBefore: bigint;
11
+ nonce?: string;
12
+ }): TransferWithAuthorization;
13
+ /**
14
+ * Build complete EIP-712 typed data for signing
15
+ *
16
+ * @param chainId - Chain ID (e.g. 8453 for Base)
17
+ * @param token - Token symbol (e.g. "USDC") or contract address
18
+ * @param message - TransferWithAuthorization message
19
+ * @param domainOverrides - Optional domain parameter overrides for custom tokens
20
+ */
21
+ export declare function buildTypedData(params: {
22
+ chainId: number;
23
+ token: string;
24
+ message: TransferWithAuthorization;
25
+ domainOverrides?: Partial<EIP712Domain>;
26
+ }): TypedData;
27
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,YAAY,EACZ,SAAS,EACV,MAAM,kBAAkB,CAAC;AAgB1B;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,yBAAyB,CAS5B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,yBAAyB,CAAC;IACnC,eAAe,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;CACzC,GAAG,SAAS,CAcZ"}
package/dist/build.js ADDED
@@ -0,0 +1,47 @@
1
+ import { randomNonce } from "./utils.js";
2
+ import { resolveDomain } from "./domain.js";
3
+ /**
4
+ * ERC-3009 TransferWithAuthorization type definition
5
+ */
6
+ const TRANSFER_WITH_AUTHORIZATION_TYPE = [
7
+ { name: "from", type: "address" },
8
+ { name: "to", type: "address" },
9
+ { name: "value", type: "uint256" },
10
+ { name: "validAfter", type: "uint256" },
11
+ { name: "validBefore", type: "uint256" },
12
+ { name: "nonce", type: "bytes32" },
13
+ ];
14
+ /**
15
+ * Build TransferWithAuthorization message
16
+ */
17
+ export function buildMessage(params) {
18
+ return {
19
+ from: params.from,
20
+ to: params.to,
21
+ value: params.value,
22
+ validAfter: params.validAfter ?? 0n,
23
+ validBefore: params.validBefore,
24
+ nonce: params.nonce ?? randomNonce(),
25
+ };
26
+ }
27
+ /**
28
+ * Build complete EIP-712 typed data for signing
29
+ *
30
+ * @param chainId - Chain ID (e.g. 8453 for Base)
31
+ * @param token - Token symbol (e.g. "USDC") or contract address
32
+ * @param message - TransferWithAuthorization message
33
+ * @param domainOverrides - Optional domain parameter overrides for custom tokens
34
+ */
35
+ export function buildTypedData(params) {
36
+ const { chainId, token, message, domainOverrides } = params;
37
+ // Resolve domain using the resolveDomain function
38
+ const domain = resolveDomain(chainId, token, domainOverrides);
39
+ return {
40
+ domain,
41
+ types: {
42
+ TransferWithAuthorization: TRANSFER_WITH_AUTHORIZATION_TYPE,
43
+ },
44
+ message,
45
+ primaryType: "TransferWithAuthorization",
46
+ };
47
+ }
@@ -0,0 +1,29 @@
1
+ import type { EIP712Domain } from "./types/index.js";
2
+ /**
3
+ * Resolve EIP-712 domain from chain ID and token
4
+ *
5
+ * @param chainId - Chain ID (e.g. 8453 for Base)
6
+ * @param token - Token symbol (e.g. "USDC") or contract address (0x...)
7
+ * @param domainOverrides - Optional domain parameter overrides for custom tokens
8
+ * @returns EIP-712 Domain object
9
+ *
10
+ * @example
11
+ * // Resolve USDC on Base
12
+ * const domain = resolveDomain(8453, "USDC");
13
+ * // Returns:
14
+ * // {
15
+ * // name: "USD Coin",
16
+ * // version: "2",
17
+ * // chainId: 8453,
18
+ * // verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
19
+ * // }
20
+ *
21
+ * @example
22
+ * // Resolve custom token with overrides
23
+ * const domain = resolveDomain(8453, "0x123...", {
24
+ * name: "My Token",
25
+ * version: "1"
26
+ * });
27
+ */
28
+ export declare function resolveDomain(chainId: number, token: string, domainOverrides?: Partial<EIP712Domain>): EIP712Domain;
29
+ //# sourceMappingURL=domain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGrD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,eAAe,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GACtC,YAAY,CAmCd"}
package/dist/domain.js ADDED
@@ -0,0 +1,57 @@
1
+ import { getTokenInfo } from "./registry.js";
2
+ /**
3
+ * Resolve EIP-712 domain from chain ID and token
4
+ *
5
+ * @param chainId - Chain ID (e.g. 8453 for Base)
6
+ * @param token - Token symbol (e.g. "USDC") or contract address (0x...)
7
+ * @param domainOverrides - Optional domain parameter overrides for custom tokens
8
+ * @returns EIP-712 Domain object
9
+ *
10
+ * @example
11
+ * // Resolve USDC on Base
12
+ * const domain = resolveDomain(8453, "USDC");
13
+ * // Returns:
14
+ * // {
15
+ * // name: "USD Coin",
16
+ * // version: "2",
17
+ * // chainId: 8453,
18
+ * // verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
19
+ * // }
20
+ *
21
+ * @example
22
+ * // Resolve custom token with overrides
23
+ * const domain = resolveDomain(8453, "0x123...", {
24
+ * name: "My Token",
25
+ * version: "1"
26
+ * });
27
+ */
28
+ export function resolveDomain(chainId, token, domainOverrides) {
29
+ let domain;
30
+ if (token.startsWith("0x")) {
31
+ // Custom token address - use overrides or fail
32
+ if (!domainOverrides?.name || !domainOverrides?.verifyingContract) {
33
+ throw new Error("Custom token address requires domain overrides (name, verifyingContract)");
34
+ }
35
+ domain = {
36
+ name: domainOverrides.name,
37
+ version: domainOverrides.version ?? "1",
38
+ chainId,
39
+ verifyingContract: domainOverrides.verifyingContract,
40
+ };
41
+ }
42
+ else {
43
+ // Try registry lookup
44
+ const tokenInfo = getTokenInfo(token, chainId);
45
+ if (!tokenInfo) {
46
+ throw new Error(`Token ${token} not found in registry for chain ${chainId}. Use domainOverrides for custom tokens.`);
47
+ }
48
+ domain = {
49
+ name: tokenInfo.name,
50
+ version: tokenInfo.version,
51
+ chainId: tokenInfo.chainId,
52
+ verifyingContract: tokenInfo.address,
53
+ ...domainOverrides, // Allow overriding registry values
54
+ };
55
+ }
56
+ return domain;
57
+ }
package/dist/index.d.ts CHANGED
@@ -1,38 +1,13 @@
1
- import { type TypedDataDomain, type TypedDataField, Wallet } from "ethers";
2
- export type TransferWithAuthorization = {
3
- from: string;
4
- to: string;
5
- value: bigint;
6
- validAfter: bigint;
7
- validBefore: bigint;
8
- nonce: string;
9
- };
10
- export type EIP712Domain = {
11
- name: string;
12
- version: string;
13
- chainId: bigint | number;
14
- verifyingContract: string;
15
- };
16
- export declare const TRANSFER_WITH_AUTHORIZATION_TYPE = "TransferWithAuthorization";
17
- export declare function buildDomain(domain: EIP712Domain): TypedDataDomain;
18
- export declare function USDC_Domain(): EIP712Domain;
19
- export declare function message_5_minutes(from: string, to: string, value: bigint): TransferWithAuthorization;
20
- export declare function buildTypes(): Record<string, TypedDataField[]>;
21
- export declare function buildMessage(message: TransferWithAuthorization): TransferWithAuthorization;
22
1
  /**
23
- * Produce the parameters needed for EIP-712 signing (domain, types, message).
2
+ * @seapay-ai/erc3009
3
+ *
4
+ * Simplified ERC-3009 (TransferWithAuthorization) library
5
+ * for building, signing, and verifying EIP-712 typed data.
24
6
  */
25
- export declare function buildTypedData(params: {
26
- domain: EIP712Domain;
27
- message: TransferWithAuthorization;
28
- }): {
29
- domain: TypedDataDomain;
30
- types: Record<string, TypedDataField[]>;
31
- message: TransferWithAuthorization;
32
- };
33
- /**
34
- * Convenience helper to sign the ERC-3009 TransferWithAuthorization payload.
35
- * Requires an ethers Wallet (or Signer with signTypedData support).
36
- */
37
- export declare function signTransferWithAuthorization(wallet: Wallet, domain: EIP712Domain, message: TransferWithAuthorization): Promise<string>;
7
+ export type { TransferWithAuthorization, EIP712Domain, TokenConfig, ChainConfig, } from "./types/index.js";
8
+ export { registry, getTokenInfo } from "./registry.js";
9
+ export { resolveDomain } from "./domain.js";
10
+ export { buildTypedData, buildMessage } from "./build.js";
11
+ export { verifySignature, recoverSigner } from "./verify.js";
12
+ export { randomNonce, nowPlusSeconds } from "./utils.js";
38
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,MAAM,EACP,MAAM,QAAQ,CAAC;AAEhB,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAWF,eAAO,MAAM,gCAAgC,8BAA8B,CAAC;AAE5E,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,CAOjE;AAED,wBAAgB,WAAW,IAAI,YAAY,CAO1C;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,GACZ,yBAAyB,CAS3B;AAED,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAI7D;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,yBAAyB,GACjC,yBAAyB,CAO3B;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE;IACrC,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,yBAAyB,CAAC;CACpC,GAAG;IACF,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IACxC,OAAO,EAAE,yBAAyB,CAAC;CACpC,CAMA;AAED;;;GAGG;AACH,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,CAGjB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,YAAY,EACV,yBAAyB,EACzB,YAAY,EACZ,WAAW,EACX,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGvD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG1D,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG7D,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,67 +1,16 @@
1
- import { randomBytes, hexlify, } from "ethers";
2
- const TRANSFER_WITH_AUTHORIZATION_FIELDS = [
3
- { name: "from", type: "address" },
4
- { name: "to", type: "address" },
5
- { name: "value", type: "uint256" },
6
- { name: "validAfter", type: "uint256" },
7
- { name: "validBefore", type: "uint256" },
8
- { name: "nonce", type: "bytes32" },
9
- ];
10
- export const TRANSFER_WITH_AUTHORIZATION_TYPE = "TransferWithAuthorization";
11
- export function buildDomain(domain) {
12
- return {
13
- name: domain.name,
14
- version: domain.version,
15
- chainId: domain.chainId,
16
- verifyingContract: domain.verifyingContract,
17
- };
18
- }
19
- export function USDC_Domain() {
20
- return {
21
- name: "USD Coin",
22
- version: "2",
23
- chainId: 84532,
24
- verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
25
- };
26
- }
27
- export function message_5_minutes(from, to, value) {
28
- return {
29
- from: from,
30
- to: to,
31
- value: value,
32
- validAfter: 0n,
33
- validBefore: BigInt(Math.floor(Date.now() / 1000) + 300),
34
- nonce: hexlify(randomBytes(32)),
35
- };
36
- }
37
- export function buildTypes() {
38
- return {
39
- [TRANSFER_WITH_AUTHORIZATION_TYPE]: TRANSFER_WITH_AUTHORIZATION_FIELDS,
40
- };
41
- }
42
- export function buildMessage(message) {
43
- return {
44
- ...message,
45
- value: BigInt(message.value),
46
- validAfter: BigInt(message.validAfter),
47
- validBefore: BigInt(message.validBefore),
48
- };
49
- }
50
1
  /**
51
- * Produce the parameters needed for EIP-712 signing (domain, types, message).
2
+ * @seapay-ai/erc3009
3
+ *
4
+ * Simplified ERC-3009 (TransferWithAuthorization) library
5
+ * for building, signing, and verifying EIP-712 typed data.
52
6
  */
53
- export function buildTypedData(params) {
54
- return {
55
- domain: buildDomain(params.domain),
56
- types: buildTypes(),
57
- message: buildMessage(params.message),
58
- };
59
- }
60
- /**
61
- * Convenience helper to sign the ERC-3009 TransferWithAuthorization payload.
62
- * Requires an ethers Wallet (or Signer with signTypedData support).
63
- */
64
- export async function signTransferWithAuthorization(wallet, domain, message) {
65
- const typed = buildTypedData({ domain, message });
66
- return await wallet.signTypedData(typed.domain, typed.types, typed.message);
67
- }
7
+ // === Registry ===
8
+ export { registry, getTokenInfo } from "./registry.js";
9
+ // === Domain Resolution ===
10
+ export { resolveDomain } from "./domain.js";
11
+ // === Build Functions ===
12
+ export { buildTypedData, buildMessage } from "./build.js";
13
+ // === Verify Functions ===
14
+ export { verifySignature, recoverSigner } from "./verify.js";
15
+ // === Utils ===
16
+ export { randomNonce, nowPlusSeconds } from "./utils.js";
@@ -0,0 +1,44 @@
1
+ import type { TokenConfig, ChainConfig } from "./types/index.js";
2
+ /**
3
+ * Chain registry - supported chains
4
+ */
5
+ export declare const CHAINS: Record<number, ChainConfig>;
6
+ /**
7
+ * Token registry - USDC addresses and domain parameters per chain
8
+ * Note: Base mainnet uses "USD Coin" while Base Sepolia uses "USDC"
9
+ */
10
+ export declare const TOKENS: Record<string, Record<number, TokenConfig>>;
11
+ /**
12
+ * Get token information for a given chain
13
+ */
14
+ export declare function getTokenInfo(tokenSymbol: string, chainId: number): TokenConfig | null;
15
+ /**
16
+ * Registry utilities
17
+ */
18
+ export declare const registry: {
19
+ /**
20
+ * Get token configuration
21
+ */
22
+ readonly getToken: (symbol: string, chainId: number) => TokenConfig | null;
23
+ /**
24
+ * Get chain configuration
25
+ */
26
+ readonly getChain: (chainId: number) => ChainConfig | null;
27
+ /**
28
+ * List all supported chains
29
+ */
30
+ readonly listChains: () => ChainConfig[];
31
+ /**
32
+ * List all supported chain IDs
33
+ */
34
+ readonly listChainIds: () => number[];
35
+ /**
36
+ * Check if a token is supported on a chain
37
+ */
38
+ readonly isSupported: (symbol: string, chainId: number) => boolean;
39
+ /**
40
+ * List all tokens on a chain
41
+ */
42
+ readonly listTokensOnChain: (chainId: number) => TokenConfig[];
43
+ };
44
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEjE;;GAEG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAW9C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAwF9D,CAAC;AAEF;;GAEG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,WAAW,GAAG,IAAI,CAMpB;AAED;;GAEG;AACH,eAAO,MAAM,QAAQ;IACnB;;OAEG;gCACgB,MAAM,WAAW,MAAM,KAAG,WAAW,GAAG,IAAI;IAI/D;;OAEG;iCACiB,MAAM,KAAG,WAAW,GAAG,IAAI;IAI/C;;OAEG;+BACa,WAAW,EAAE;IAI7B;;OAEG;iCACe,MAAM,EAAE;IAI1B;;OAEG;mCACmB,MAAM,WAAW,MAAM,KAAG,OAAO;IAIvD;;OAEG;0CAC0B,MAAM,KAAG,WAAW,EAAE;CAU3C,CAAC"}
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Chain registry - supported chains
3
+ */
4
+ export const CHAINS = {
5
+ 1: { chainId: 1, name: "Ethereum", testnet: false },
6
+ 11155111: { chainId: 11155111, name: "Sepolia", testnet: true },
7
+ 8453: { chainId: 8453, name: "Base", testnet: false },
8
+ 84532: { chainId: 84532, name: "Base Sepolia", testnet: true },
9
+ 42161: { chainId: 42161, name: "Arbitrum One", testnet: false },
10
+ 421614: { chainId: 421614, name: "Arbitrum Sepolia", testnet: true },
11
+ 10: { chainId: 10, name: "Optimism", testnet: false },
12
+ 11155420: { chainId: 11155420, name: "Optimism Sepolia", testnet: true },
13
+ 137: { chainId: 137, name: "Polygon", testnet: false },
14
+ 80002: { chainId: 80002, name: "Polygon Amoy", testnet: true },
15
+ };
16
+ /**
17
+ * Token registry - USDC addresses and domain parameters per chain
18
+ * Note: Base mainnet uses "USD Coin" while Base Sepolia uses "USDC"
19
+ */
20
+ export const TOKENS = {
21
+ USDC: {
22
+ // Ethereum
23
+ 1: {
24
+ symbol: "USDC",
25
+ chainId: 1,
26
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
27
+ name: "USD Coin",
28
+ version: "2",
29
+ decimals: 6,
30
+ },
31
+ 11155111: {
32
+ symbol: "USDC",
33
+ chainId: 11155111,
34
+ address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
35
+ name: "USD Coin",
36
+ version: "2",
37
+ decimals: 6,
38
+ },
39
+ // Base
40
+ 8453: {
41
+ symbol: "USDC",
42
+ chainId: 8453,
43
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
44
+ name: "USD Coin", // Important: Base mainnet uses "USD Coin"
45
+ version: "2",
46
+ decimals: 6,
47
+ },
48
+ 84532: {
49
+ symbol: "USDC",
50
+ chainId: 84532,
51
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
52
+ name: "USDC", // Important: Base Sepolia uses "USDC"
53
+ version: "2",
54
+ decimals: 6,
55
+ },
56
+ // Arbitrum
57
+ 42161: {
58
+ symbol: "USDC",
59
+ chainId: 42161,
60
+ address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
61
+ name: "USD Coin",
62
+ version: "2",
63
+ decimals: 6,
64
+ },
65
+ 421614: {
66
+ symbol: "USDC",
67
+ chainId: 421614,
68
+ address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
69
+ name: "USD Coin",
70
+ version: "2",
71
+ decimals: 6,
72
+ },
73
+ // Optimism
74
+ 10: {
75
+ symbol: "USDC",
76
+ chainId: 10,
77
+ address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
78
+ name: "USD Coin",
79
+ version: "2",
80
+ decimals: 6,
81
+ },
82
+ 11155420: {
83
+ symbol: "USDC",
84
+ chainId: 11155420,
85
+ address: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
86
+ name: "USD Coin",
87
+ version: "2",
88
+ decimals: 6,
89
+ },
90
+ // Polygon
91
+ 137: {
92
+ symbol: "USDC",
93
+ chainId: 137,
94
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
95
+ name: "USD Coin",
96
+ version: "2",
97
+ decimals: 6,
98
+ },
99
+ 80002: {
100
+ symbol: "USDC",
101
+ chainId: 80002,
102
+ address: "0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582",
103
+ name: "USD Coin",
104
+ version: "2",
105
+ decimals: 6,
106
+ },
107
+ },
108
+ };
109
+ /**
110
+ * Get token information for a given chain
111
+ */
112
+ export function getTokenInfo(tokenSymbol, chainId) {
113
+ const token = TOKENS[tokenSymbol.toUpperCase()];
114
+ if (!token)
115
+ return null;
116
+ const config = token[chainId];
117
+ return config || null;
118
+ }
119
+ /**
120
+ * Registry utilities
121
+ */
122
+ export const registry = {
123
+ /**
124
+ * Get token configuration
125
+ */
126
+ getToken: (symbol, chainId) => {
127
+ return getTokenInfo(symbol, chainId);
128
+ },
129
+ /**
130
+ * Get chain configuration
131
+ */
132
+ getChain: (chainId) => {
133
+ return CHAINS[chainId] || null;
134
+ },
135
+ /**
136
+ * List all supported chains
137
+ */
138
+ listChains: () => {
139
+ return Object.values(CHAINS);
140
+ },
141
+ /**
142
+ * List all supported chain IDs
143
+ */
144
+ listChainIds: () => {
145
+ return Object.keys(CHAINS).map(Number);
146
+ },
147
+ /**
148
+ * Check if a token is supported on a chain
149
+ */
150
+ isSupported: (symbol, chainId) => {
151
+ return getTokenInfo(symbol, chainId) !== null;
152
+ },
153
+ /**
154
+ * List all tokens on a chain
155
+ */
156
+ listTokensOnChain: (chainId) => {
157
+ const tokens = [];
158
+ for (const symbol of Object.keys(TOKENS)) {
159
+ const config = TOKENS[symbol][chainId];
160
+ if (config) {
161
+ tokens.push(config);
162
+ }
163
+ }
164
+ return tokens;
165
+ },
166
+ };
@@ -0,0 +1,45 @@
1
+ import type { TypedDataDomain, TypedDataField } from "ethers";
2
+ /**
3
+ * ERC-3009 TransferWithAuthorization message
4
+ */
5
+ export interface TransferWithAuthorization {
6
+ from: string;
7
+ to: string;
8
+ value: bigint;
9
+ validAfter: bigint;
10
+ validBefore: bigint;
11
+ nonce: string;
12
+ }
13
+ /**
14
+ * EIP-712 Domain
15
+ */
16
+ export type EIP712Domain = TypedDataDomain;
17
+ /**
18
+ * Token configuration
19
+ */
20
+ export interface TokenConfig {
21
+ symbol: string;
22
+ chainId: number;
23
+ address: string;
24
+ name: string;
25
+ version: string;
26
+ decimals: number;
27
+ }
28
+ /**
29
+ * Chain configuration
30
+ */
31
+ export interface ChainConfig {
32
+ chainId: number;
33
+ name: string;
34
+ testnet: boolean;
35
+ }
36
+ /**
37
+ * Complete EIP-712 typed data structure
38
+ */
39
+ export interface TypedData {
40
+ domain: EIP712Domain;
41
+ types: Record<string, TypedDataField[]>;
42
+ message: TransferWithAuthorization;
43
+ primaryType: string;
44
+ }
45
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,eAAe,CAAC;AAE3C;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IACxC,OAAO,EAAE,yBAAyB,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;CACrB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Generate a random bytes32 nonce for TransferWithAuthorization
3
+ */
4
+ export declare function randomNonce(): string;
5
+ /**
6
+ * Get current Unix timestamp + N seconds (for validBefore)
7
+ */
8
+ export declare function nowPlusSeconds(seconds: number): bigint;
9
+ /**
10
+ * Get current Unix timestamp in seconds
11
+ */
12
+ export declare function nowSeconds(): bigint;
13
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC"}
package/dist/utils.js ADDED
@@ -0,0 +1,19 @@
1
+ import { randomBytes, hexlify } from "ethers";
2
+ /**
3
+ * Generate a random bytes32 nonce for TransferWithAuthorization
4
+ */
5
+ export function randomNonce() {
6
+ return hexlify(randomBytes(32));
7
+ }
8
+ /**
9
+ * Get current Unix timestamp + N seconds (for validBefore)
10
+ */
11
+ export function nowPlusSeconds(seconds) {
12
+ return BigInt(Math.floor(Date.now() / 1000) + seconds);
13
+ }
14
+ /**
15
+ * Get current Unix timestamp in seconds
16
+ */
17
+ export function nowSeconds() {
18
+ return BigInt(Math.floor(Date.now() / 1000));
19
+ }
@@ -0,0 +1,14 @@
1
+ import type { TransferWithAuthorization, EIP712Domain } from "./types/index.js";
2
+ /**
3
+ * Recover the signer address from a signature
4
+ */
5
+ export declare function recoverSigner(domain: EIP712Domain, message: TransferWithAuthorization, signature: string): string;
6
+ /**
7
+ * Verify that a signature was created by the expected signer
8
+ */
9
+ export declare function verifySignature(domain: EIP712Domain, message: TransferWithAuthorization, signature: string, expectedSigner: string): boolean;
10
+ /**
11
+ * Get the EIP-712 hash for a message (useful for debugging)
12
+ */
13
+ export declare function getMessageHash(domain: EIP712Domain, message: TransferWithAuthorization): string;
14
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,YAAY,EACb,MAAM,kBAAkB,CAAC;AAc1B;;GAEG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,yBAAyB,EAClC,SAAS,EAAE,MAAM,GAChB,MAAM,CAOR;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,yBAAyB,EAClC,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,OAAO,CAOT;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,yBAAyB,GACjC,MAAM,CAMR"}
package/dist/verify.js ADDED
@@ -0,0 +1,36 @@
1
+ import { TypedDataEncoder, verifyTypedData } from "ethers";
2
+ /**
3
+ * ERC-3009 type definition for signature recovery
4
+ */
5
+ const TRANSFER_WITH_AUTHORIZATION_TYPE = [
6
+ { name: "from", type: "address" },
7
+ { name: "to", type: "address" },
8
+ { name: "value", type: "uint256" },
9
+ { name: "validAfter", type: "uint256" },
10
+ { name: "validBefore", type: "uint256" },
11
+ { name: "nonce", type: "bytes32" },
12
+ ];
13
+ /**
14
+ * Recover the signer address from a signature
15
+ */
16
+ export function recoverSigner(domain, message, signature) {
17
+ return verifyTypedData(domain, { TransferWithAuthorization: TRANSFER_WITH_AUTHORIZATION_TYPE }, message, signature);
18
+ }
19
+ /**
20
+ * Verify that a signature was created by the expected signer
21
+ */
22
+ export function verifySignature(domain, message, signature, expectedSigner) {
23
+ try {
24
+ const recovered = recoverSigner(domain, message, signature);
25
+ return recovered.toLowerCase() === expectedSigner.toLowerCase();
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ /**
32
+ * Get the EIP-712 hash for a message (useful for debugging)
33
+ */
34
+ export function getMessageHash(domain, message) {
35
+ return TypedDataEncoder.hash(domain, { TransferWithAuthorization: TRANSFER_WITH_AUTHORIZATION_TYPE }, message);
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seapay-ai/erc3009",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript utilities for ERC-3009 (Transfer With Authorization) EIP-712 signing",
5
5
  "keywords": [
6
6
  "erc3009",