Skip to content

Commit 026315d

Browse files
committed
conditional exports based on env
1 parent 3f83a87 commit 026315d

File tree

8 files changed

+360
-258
lines changed

8 files changed

+360
-258
lines changed

examples/testapp/next.config.mjs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import webpack from 'webpack';
2-
31
export default {
42
output: 'export',
53
basePath: process.env.NODE_ENV === 'production' ? '/account-sdk' : undefined,
@@ -9,21 +7,4 @@ export default {
97
// GitHub discussion for supporting biome: https://github.com/vercel/next.js/discussions/59347
108
ignoreDuringBuilds: true,
119
},
12-
webpack: (config, { isServer }) => {
13-
// Exclude Node.js built-in modules from client bundle
14-
if (!isServer) {
15-
config.resolve.fallback = {
16-
...config.resolve.fallback,
17-
zlib: false,
18-
};
19-
20-
// Ignore node:zlib imports in client-side code
21-
config.plugins.push(
22-
new webpack.IgnorePlugin({
23-
resourceRegExp: /^node:zlib$/,
24-
})
25-
);
26-
}
27-
return config;
28-
},
2910
};

packages/account-sdk/package.json

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,21 @@
8686
},
8787
"./prolink": {
8888
"types": "./dist/interface/public-utilities/prolink/index.d.ts",
89-
"import": "./dist/interface/public-utilities/prolink/index.js",
90-
"require": "./dist/interface/public-utilities/prolink/index.js"
89+
"browser": {
90+
"types": "./dist/interface/public-utilities/prolink/index.d.ts",
91+
"import": "./dist/interface/public-utilities/prolink/index.js",
92+
"require": "./dist/interface/public-utilities/prolink/index.js"
93+
},
94+
"node": {
95+
"types": "./dist/interface/public-utilities/prolink/index.node.d.ts",
96+
"import": "./dist/interface/public-utilities/prolink/index.node.js",
97+
"require": "./dist/interface/public-utilities/prolink/index.node.js"
98+
},
99+
"default": {
100+
"types": "./dist/interface/public-utilities/prolink/index.d.ts",
101+
"import": "./dist/interface/public-utilities/prolink/index.js",
102+
"require": "./dist/interface/public-utilities/prolink/index.js"
103+
}
91104
},
92105
"./ui-assets": {
93106
"types": "./dist/ui/assets/index.d.ts",
@@ -121,16 +134,13 @@
121134
"size": "size-limit"
122135
},
123136
"dependencies": {
124-
"@bufbuild/protobuf": "^1.7.0",
125137
"@coinbase/cdp-sdk": "^1.0.0",
126-
"@noble/hashes": "1.4.0",
127138
"brotli-wasm": "^3.0.0",
128139
"clsx": "1.2.1",
129140
"eventemitter3": "5.0.1",
130141
"idb-keyval": "6.2.1",
131142
"ox": "0.6.9",
132143
"preact": "10.24.2",
133-
"qrcode": "^1.5.3",
134144
"viem": "^2.31.7",
135145
"zustand": "5.0.3"
136146
},
@@ -144,7 +154,6 @@
144154
"@testing-library/jest-dom": "^6.5.0",
145155
"@testing-library/preact": "^3.2.4",
146156
"@types/node": "^14.18.54",
147-
"@types/qrcode": "^1.5.5",
148157
"@vitest/coverage-v8": "2.1.2",
149158
"@vitest/web-worker": "3.2.1",
150159
"fake-indexeddb": "^6.0.0",
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright (c) 2018-2025 Coinbase, Inc. <https://www.coinbase.com/>
2+
3+
/**
4+
* Prolink URI encoding/decoding - Node.js version
5+
* Implements the Compressed RPC Link Format ERC specification
6+
* Uses Node.js zlib for compression
7+
*/
8+
9+
import { decodeGenericRpc, encodeGenericRpc } from './shortcuts/generic.js';
10+
import { decodeWalletSendCalls, encodeWalletSendCalls } from './shortcuts/sendCalls.js';
11+
import { decodeWalletSign, encodeWalletSign } from './shortcuts/sign.js';
12+
import type { ProlinkDecoded, ProlinkRequest, RpcLinkPayload } from './types.js';
13+
import { decodeBase64url, encodeBase64url } from './utils/base64url.js';
14+
import { compressPayload, decompressPayload } from './utils/compression.node.js';
15+
import { decodeCapabilities, encodeCapabilities } from './utils/encoding.js';
16+
import { decodeRpcLinkPayload, encodeRpcLinkPayload } from './utils/protobuf.js';
17+
18+
const PROTOCOL_VERSION = 1;
19+
const SHORTCUT_VERSION = 0;
20+
21+
const SHORTCUT_GENERIC = 0;
22+
const SHORTCUT_WALLET_SEND_CALLS = 1;
23+
const SHORTCUT_WALLET_SIGN = 2;
24+
25+
/**
26+
* Encode a JSON-RPC request to prolink format
27+
* @param request - JSON-RPC request with method, params, optional chainId and capabilities
28+
* @returns Base64url-encoded prolink payload
29+
*/
30+
export async function encodeProlink(request: ProlinkRequest): Promise<string> {
31+
let payload: RpcLinkPayload;
32+
33+
// Auto-detect shortcut based on method
34+
if (request.method === 'wallet_sendCalls') {
35+
// Validate params structure
36+
if (!Array.isArray(request.params) || request.params.length === 0) {
37+
throw new Error('wallet_sendCalls requires params array with at least one element');
38+
}
39+
40+
const params = request.params[0];
41+
if (typeof params !== 'object' || !params) {
42+
throw new Error('wallet_sendCalls params[0] must be an object');
43+
}
44+
45+
// Extract chainId from params
46+
const chainIdHex = (params as { chainId?: string }).chainId;
47+
if (!chainIdHex) {
48+
throw new Error('wallet_sendCalls requires chainId in params');
49+
}
50+
const chainId = Number.parseInt(chainIdHex, 16);
51+
52+
const walletSendCalls = encodeWalletSendCalls(
53+
params as Parameters<typeof encodeWalletSendCalls>[0]
54+
);
55+
56+
payload = {
57+
protocolVersion: PROTOCOL_VERSION,
58+
chainId,
59+
shortcutId: SHORTCUT_WALLET_SEND_CALLS,
60+
shortcutVersion: SHORTCUT_VERSION,
61+
body: {
62+
case: 'walletSendCalls',
63+
value: walletSendCalls,
64+
},
65+
capabilities: request.capabilities ? encodeCapabilities(request.capabilities) : undefined,
66+
};
67+
} else if (request.method === 'wallet_sign') {
68+
// Validate params structure
69+
if (!Array.isArray(request.params) || request.params.length === 0) {
70+
throw new Error('wallet_sign requires params array with at least one element');
71+
}
72+
73+
const params = request.params[0];
74+
if (typeof params !== 'object' || !params) {
75+
throw new Error('wallet_sign params[0] must be an object');
76+
}
77+
78+
// Extract chainId from params
79+
const chainIdHex = (params as { chainId?: string }).chainId;
80+
if (!chainIdHex) {
81+
throw new Error('wallet_sign requires chainId in params');
82+
}
83+
const chainId = Number.parseInt(chainIdHex, 16);
84+
85+
const walletSign = encodeWalletSign(params as Parameters<typeof encodeWalletSign>[0]);
86+
87+
payload = {
88+
protocolVersion: PROTOCOL_VERSION,
89+
chainId,
90+
shortcutId: SHORTCUT_WALLET_SIGN,
91+
shortcutVersion: SHORTCUT_VERSION,
92+
body: {
93+
case: 'walletSign',
94+
value: walletSign,
95+
},
96+
capabilities: request.capabilities ? encodeCapabilities(request.capabilities) : undefined,
97+
};
98+
} else {
99+
// Generic JSON-RPC
100+
const generic = encodeGenericRpc(request.method, request.params);
101+
102+
payload = {
103+
protocolVersion: PROTOCOL_VERSION,
104+
chainId: request.chainId,
105+
shortcutId: SHORTCUT_GENERIC,
106+
shortcutVersion: SHORTCUT_VERSION,
107+
body: {
108+
case: 'generic',
109+
value: generic,
110+
},
111+
capabilities: request.capabilities ? encodeCapabilities(request.capabilities) : undefined,
112+
};
113+
}
114+
115+
// Serialize to protobuf
116+
const protoBytes = encodeRpcLinkPayload(payload);
117+
118+
// Compress (with flag byte)
119+
const { compressed, flag } = await compressPayload(protoBytes);
120+
const withFlag = new Uint8Array(compressed.length + 1);
121+
withFlag[0] = flag;
122+
withFlag.set(compressed, 1);
123+
124+
// Base64url encode
125+
return encodeBase64url(withFlag);
126+
}
127+
128+
/**
129+
* Decode a prolink payload to JSON-RPC request
130+
* @param payload - Base64url-encoded prolink payload
131+
* @returns Decoded JSON-RPC request
132+
*/
133+
export async function decodeProlink(payload: string): Promise<ProlinkDecoded> {
134+
// Base64url decode
135+
const bytes = decodeBase64url(payload);
136+
137+
// Decompress
138+
const decompressed = await decompressPayload(bytes);
139+
140+
// Deserialize protobuf
141+
const rpcPayload = decodeRpcLinkPayload(decompressed);
142+
143+
// Validate protocol version
144+
if (rpcPayload.protocolVersion !== PROTOCOL_VERSION) {
145+
throw new Error(
146+
`Unsupported protocol version: ${rpcPayload.protocolVersion} (expected ${PROTOCOL_VERSION})`
147+
);
148+
}
149+
150+
// Decode capabilities
151+
const capabilities = rpcPayload.capabilities
152+
? decodeCapabilities(rpcPayload.capabilities)
153+
: undefined;
154+
155+
// Dispatch to shortcut decoder
156+
if (rpcPayload.shortcutId === SHORTCUT_GENERIC) {
157+
if (rpcPayload.body.case !== 'generic') {
158+
throw new Error('Invalid payload: shortcut 0 requires generic body');
159+
}
160+
161+
const { method, params } = decodeGenericRpc(rpcPayload.body.value);
162+
163+
return {
164+
method,
165+
params,
166+
chainId: rpcPayload.chainId,
167+
capabilities,
168+
};
169+
}
170+
171+
if (rpcPayload.shortcutId === SHORTCUT_WALLET_SEND_CALLS) {
172+
if (rpcPayload.body.case !== 'walletSendCalls') {
173+
throw new Error('Invalid payload: shortcut 1 requires walletSendCalls body');
174+
}
175+
176+
if (!rpcPayload.chainId) {
177+
throw new Error('wallet_sendCalls requires chainId');
178+
}
179+
180+
const params = decodeWalletSendCalls(rpcPayload.body.value, rpcPayload.chainId);
181+
182+
return {
183+
method: 'wallet_sendCalls',
184+
params: [params],
185+
chainId: rpcPayload.chainId,
186+
capabilities,
187+
};
188+
}
189+
190+
if (rpcPayload.shortcutId === SHORTCUT_WALLET_SIGN) {
191+
if (rpcPayload.body.case !== 'walletSign') {
192+
throw new Error('Invalid payload: shortcut 2 requires walletSign body');
193+
}
194+
195+
if (!rpcPayload.chainId) {
196+
throw new Error('wallet_sign requires chainId');
197+
}
198+
199+
const params = decodeWalletSign(rpcPayload.body.value, rpcPayload.chainId);
200+
201+
return {
202+
method: 'wallet_sign',
203+
params: [params],
204+
chainId: rpcPayload.chainId,
205+
capabilities,
206+
};
207+
}
208+
209+
throw new Error(`Unsupported shortcut ID: ${rpcPayload.shortcutId}`);
210+
}
211+
212+
// Re-export types
213+
export type { ProlinkDecoded, ProlinkRequest } from './types.js';

packages/account-sdk/src/interface/public-utilities/prolink/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2018-2025 Coinbase, Inc. <https://www.coinbase.com/>
22

33
import { describe, expect, it } from 'vitest';
4-
import { decodeProlink, encodeProlink } from './index.js';
4+
import { decodeProlink, encodeProlink } from './index.node.js';
55

66
describe('prolink end-to-end', () => {
77
describe('wallet_sendCalls', () => {

packages/account-sdk/src/interface/public-utilities/prolink/utils/base64url.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
1313
* @returns Base64url encoded string without padding
1414
*/
1515
export function encodeBase64url(data: Uint8Array): string {
16-
// Use browser's built-in btoa for base64 encoding
1716
let binary = '';
1817
for (let i = 0; i < data.length; i++) {
1918
binary += String.fromCharCode(data[i]);
@@ -66,7 +65,6 @@ export function decodeBase64url(payload: string): Uint8Array {
6665
base64 += '='.repeat(paddingNeeded);
6766

6867
try {
69-
// Use browser's built-in atob for base64 decoding
7068
const binary = atob(base64);
7169
const bytes = new Uint8Array(binary.length);
7270
for (let i = 0; i < binary.length; i++) {

0 commit comments

Comments
 (0)